mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-20 11:42:49 +00:00
fix(ugc): transparent bricks drawn see-through in mixed models; dUgcServer in folders by role
- The client puts a shape in its sorted, blended pass only when its NiMaterialProperty alpha is under 0.99999 (ShaderCommon::GetAlphaFlags 0x0109f5a0; the NiAlphaProperty blend flag isn't read); at 1.0 it's drawn solid with blending off. Transparent (and transparent glitter) shapes now get a material with alpha 0.9999, as the S01_Alpha shapes of the game's own brick models (res/BrickModels/ndmade) do; opaque shapes keep 1.0. Models with a transparent brick change; the others are byte for byte the same. - dUgcServer's files move into Bricks/, Model/, Render/, Formats/ and Processing/ (the CMakeLists says what each holds); includes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
73
dUgcServer/Model/UgcGlitter.cpp
Normal file
73
dUgcServer/Model/UgcGlitter.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#include "UgcGlitter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace UgcGlitter {
|
||||
std::vector<uint8_t> FleckAlpha(uint32_t flecks) {
|
||||
constexpr int N = TEXTURE_SIZE;
|
||||
std::vector<float> alpha(static_cast<size_t>(N) * N, 0.0f);
|
||||
// SplitMix64 from a fixed seed: the same texture on every platform
|
||||
uint64_t state = 0x6C69747465720000ull;
|
||||
const auto next = [&state] {
|
||||
uint64_t z = (state += 0x9E3779B97F4A7C15ull);
|
||||
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
|
||||
z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
|
||||
return static_cast<float>((z ^ (z >> 31)) >> 40) / static_cast<float>(1ull << 24);
|
||||
};
|
||||
for (uint32_t i = 0; i < flecks; i++) {
|
||||
const float cx = next() * N, cy = next() * N;
|
||||
const float radius = 1.2f + next() * 1.0f;
|
||||
const float peak = 0.65f + next() * 0.35f;
|
||||
const int reach = static_cast<int>(std::ceil(radius));
|
||||
for (int dy = -reach; dy <= reach; dy++) {
|
||||
for (int dx = -reach; dx <= reach; dx++) {
|
||||
const int x = static_cast<int>(std::floor(cx)) + dx, y = static_cast<int>(std::floor(cy)) + dy;
|
||||
const float ddx = x + 0.5f - cx, ddy = y + 0.5f - cy;
|
||||
const float d = std::sqrt(ddx * ddx + ddy * ddy) / radius;
|
||||
if (d >= 1.0f) continue;
|
||||
auto& value = alpha[static_cast<size_t>(((y % N) + N) % N) * N + ((x % N) + N) % N];
|
||||
value = std::max(value, peak * (1.0f - d * d));
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<uint8_t> out(alpha.size());
|
||||
for (size_t i = 0; i < alpha.size(); i++) out[i] = static_cast<uint8_t>(std::lround(std::clamp(alpha[i], 0.0f, 1.0f) * 255.0f));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha) {
|
||||
std::vector<std::vector<uint8_t>> levels{ alpha };
|
||||
for (int size = TEXTURE_SIZE / 2; size >= 1; size /= 2) {
|
||||
const auto& above = levels.back();
|
||||
const int from = size * 2;
|
||||
std::vector<uint8_t> level(static_cast<size_t>(size) * size);
|
||||
for (int y = 0; y < size; y++) {
|
||||
for (int x = 0; x < size; x++) {
|
||||
const auto at = [&](int dx, int dy) { return static_cast<int>(above[static_cast<size_t>(y * 2 + dy) * from + x * 2 + dx]); };
|
||||
level[static_cast<size_t>(y) * size + x] = static_cast<uint8_t>((at(0, 0) + at(1, 0) + at(0, 1) + at(1, 1) + 2) / 4);
|
||||
}
|
||||
}
|
||||
levels.push_back(std::move(level));
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
|
||||
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile) {
|
||||
const auto a = glm::abs(normal);
|
||||
const float scale = 1.0f / std::max(tile, 1e-3f);
|
||||
if (a.x >= a.y && a.x >= a.z) return glm::vec2(position.z, position.y) * scale;
|
||||
if (a.y >= a.z) return glm::vec2(position.x, position.z) * scale;
|
||||
return glm::vec2(position.x, position.y) * scale;
|
||||
}
|
||||
|
||||
float Sample(const std::vector<uint8_t>& alpha, const glm::vec2& uv) {
|
||||
constexpr int N = TEXTURE_SIZE;
|
||||
if (alpha.size() != static_cast<size_t>(N) * N) return 0.0f;
|
||||
const float x = (uv.x - std::floor(uv.x)) * N - 0.5f, y = (uv.y - std::floor(uv.y)) * N - 0.5f;
|
||||
const int x0 = static_cast<int>(std::floor(x)), y0 = static_cast<int>(std::floor(y));
|
||||
const float fx = x - x0, fy = y - y0;
|
||||
const auto at = [&](int px, int py) { return alpha[static_cast<size_t>(((py % N) + N) % N) * N + ((px % N) + N) % N] / 255.0f; };
|
||||
return (at(x0, y0) * (1 - fx) + at(x0 + 1, y0) * fx) * (1 - fy) + (at(x0, y0 + 1) * (1 - fx) + at(x0 + 1, y0 + 1) * fx) * fy;
|
||||
}
|
||||
}
|
||||
41
dUgcServer/Model/UgcGlitter.h
Normal file
41
dUgcServer/Model/UgcGlitter.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
/**
|
||||
* The glitter the UGC server gives glitter colors (docs/UgcServer.md, "Metal and glow"): a tileable texture of white
|
||||
* flecks (its alpha) laid over the brick's color by the client's LEGO-AnimUV shader (lerp(vertex color, texture,
|
||||
* texture alpha), then the LEGO lighting), on UVs projected from the model's own coordinates so every brick gets the
|
||||
* same density, drifting as the texture transform's translation loops. Pure.
|
||||
*/
|
||||
namespace UgcGlitter {
|
||||
// The texture's side in pixels (a power of two, mipmapped down to 1)
|
||||
constexpr int TEXTURE_SIZE = 128;
|
||||
|
||||
struct Params {
|
||||
float tile{ 1.6f }; // glitter_size: the texture's side in model units (LDD units: a stud is 0.8)
|
||||
uint32_t flecks{ 50 }; // glitter_density: flecks in one tile
|
||||
float speed{ 1.0f }; // glitter_speed: 1 moves the flecks a tile in U in 7 s and in V in 11 s; 0 keeps them still
|
||||
|
||||
// Seconds the texture's translation takes to go one tile in U and in V (0: no animation)
|
||||
float PeriodU() const { return speed > 0.0f ? 7.0f / speed : 0.0f; }
|
||||
float PeriodV() const { return speed > 0.0f ? 11.0f / speed : 0.0f; }
|
||||
bool operator==(const Params&) const = default;
|
||||
};
|
||||
|
||||
// The texture's alpha (TEXTURE_SIZE squared, rows top to bottom): `flecks` soft dots at the same places every time,
|
||||
// wrapping around the edges so the texture tiles. Its color is white.
|
||||
std::vector<uint8_t> FleckAlpha(uint32_t flecks);
|
||||
|
||||
// The texture's mipmaps' alpha, from TEXTURE_SIZE down to 1 (each the mean of 2x2 of the one before)
|
||||
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha);
|
||||
|
||||
// A vertex's UV: its position on the axis plane its normal faces most, in tiles
|
||||
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile);
|
||||
|
||||
// The texture's alpha (0..1) at `uv` (wrapping, bilinear)
|
||||
float Sample(const std::vector<uint8_t>& alpha, const glm::vec2& uv);
|
||||
}
|
||||
530
dUgcServer/Model/UgcModel.cpp
Normal file
530
dUgcServer/Model/UgcModel.cpp
Normal file
@@ -0,0 +1,530 @@
|
||||
#include "UgcModel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include "NifFile.h"
|
||||
#include "UgcPalette.h"
|
||||
#include "tinyxml2.h"
|
||||
|
||||
namespace {
|
||||
std::vector<float> ParseFloats(const char* text) {
|
||||
std::vector<float> values;
|
||||
if (!text) return values;
|
||||
std::stringstream stream(text);
|
||||
std::string item;
|
||||
while (std::getline(stream, item, ',')) {
|
||||
try {
|
||||
values.push_back(std::stof(item));
|
||||
} catch (...) {
|
||||
values.push_back(0.0f);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> ParseMaterials(const char* text) {
|
||||
std::vector<uint32_t> values;
|
||||
if (!text) return values;
|
||||
std::stringstream stream(text);
|
||||
std::string item;
|
||||
while (std::getline(stream, item, ',')) {
|
||||
try {
|
||||
values.push_back(static_cast<uint32_t>(std::stoul(item)));
|
||||
} catch (...) {
|
||||
values.push_back(0);
|
||||
}
|
||||
}
|
||||
// Material 0 means "the same as the part's first"
|
||||
for (auto& value : values) {
|
||||
if (value == 0 && !values.empty()) value = values[0];
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
// LXFML 5 bone: a row-major 3x3 rotation followed by a translation
|
||||
glm::mat4 BoneMatrix(const std::vector<float>& t) {
|
||||
glm::mat4 matrix(1.0f);
|
||||
if (t.size() < 12) return matrix;
|
||||
matrix[0] = glm::vec4(t[0], t[1], t[2], 0.0f);
|
||||
matrix[1] = glm::vec4(t[3], t[4], t[5], 0.0f);
|
||||
matrix[2] = glm::vec4(t[6], t[7], t[8], 0.0f);
|
||||
matrix[3] = glm::vec4(t[9], t[10], t[11], 1.0f);
|
||||
return matrix;
|
||||
}
|
||||
|
||||
// LXFML 4: rotate `angle` degrees around (ax, ay, az), then translate
|
||||
glm::mat4 AxisAngleMatrix(const tinyxml2::XMLElement* element) {
|
||||
const glm::vec3 axis(element->FloatAttribute("ax"), element->FloatAttribute("ay"), element->FloatAttribute("az"));
|
||||
const glm::vec3 translation(element->FloatAttribute("tx"), element->FloatAttribute("ty"), element->FloatAttribute("tz"));
|
||||
glm::mat4 matrix = glm::translate(glm::mat4(1.0f), translation);
|
||||
if (glm::dot(axis, axis) > 0.0f) matrix = glm::rotate(matrix, glm::radians(element->FloatAttribute("angle")), glm::normalize(axis));
|
||||
return matrix;
|
||||
}
|
||||
|
||||
bool ParseDesign(const tinyxml2::XMLElement* element, uint32_t& design) {
|
||||
const char* text = element->Attribute("designID");
|
||||
if (!text || !*text) return false;
|
||||
for (const char* c = text; *c; c++) {
|
||||
if (*c < '0' || *c > '9') return false;
|
||||
}
|
||||
try {
|
||||
design = static_cast<uint32_t>(std::stoul(text));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
glm::vec4 ToColor(const UgcBricks::Material& material) {
|
||||
return glm::vec4(material.r, material.g, material.b, material.a) / 255.0f;
|
||||
}
|
||||
}
|
||||
|
||||
namespace UgcModel {
|
||||
std::vector<Part> ParseLxfml(std::string_view lxfml, std::string& error) {
|
||||
std::vector<Part> parts;
|
||||
tinyxml2::XMLDocument doc;
|
||||
if (doc.Parse(lxfml.data(), lxfml.size()) != tinyxml2::XML_SUCCESS) {
|
||||
error = "the LXFML is not valid XML";
|
||||
return parts;
|
||||
}
|
||||
const auto* root = doc.FirstChildElement("LXFML");
|
||||
if (!root) {
|
||||
error = "no LXFML element";
|
||||
return parts;
|
||||
}
|
||||
|
||||
if (const auto* bricks = root->FirstChildElement("Bricks")) {
|
||||
for (const auto* brick = bricks->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
|
||||
for (const auto* partElement = brick->FirstChildElement("Part"); partElement; partElement = partElement->NextSiblingElement("Part")) {
|
||||
const auto* bone = partElement->FirstChildElement("Bone");
|
||||
Part part;
|
||||
if (!bone || !ParseDesign(partElement, part.designId)) continue;
|
||||
const char* materials = partElement->Attribute("materials");
|
||||
if (!materials) materials = partElement->Attribute("materialID");
|
||||
part.materials = ParseMaterials(materials ? materials : "0");
|
||||
part.transform = BoneMatrix(ParseFloats(bone->Attribute("transformation")));
|
||||
parts.push_back(std::move(part));
|
||||
}
|
||||
}
|
||||
if (!parts.empty()) return parts;
|
||||
}
|
||||
|
||||
// LXFML 4: groups (with their own transforms) nest parts
|
||||
std::function<void(const tinyxml2::XMLElement*, const glm::mat4&)> walk = [&](const tinyxml2::XMLElement* element, const glm::mat4& parent) {
|
||||
for (const auto* child = element->FirstChildElement(); child; child = child->NextSiblingElement()) {
|
||||
const std::string_view name = child->Name();
|
||||
if (name != "Group" && name != "Part") continue;
|
||||
const auto matrix = parent * AxisAngleMatrix(child);
|
||||
if (name == "Group") {
|
||||
walk(child, matrix);
|
||||
continue;
|
||||
}
|
||||
Part part;
|
||||
if (!ParseDesign(child, part.designId)) continue;
|
||||
const char* material = child->Attribute("materialID");
|
||||
part.materials = ParseMaterials(material ? material : "0");
|
||||
part.transform = matrix;
|
||||
parts.push_back(std::move(part));
|
||||
}
|
||||
};
|
||||
if (const auto* scene = root->FirstChildElement("Scene")) {
|
||||
for (const auto* model = scene->FirstChildElement("Model"); model; model = model->NextSiblingElement("Model")) walk(model, glm::mat4(1.0f));
|
||||
}
|
||||
if (parts.empty()) error = "the LXFML has no bricks";
|
||||
return parts;
|
||||
}
|
||||
|
||||
bool HasNoBricks(std::string_view lxfml) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
if (doc.Parse(lxfml.data(), lxfml.size()) != tinyxml2::XML_SUCCESS || !doc.FirstChildElement("LXFML")) return false;
|
||||
std::string error;
|
||||
return ParseLxfml(lxfml, error).empty() && lxfml.find("<Part") == std::string_view::npos;
|
||||
}
|
||||
|
||||
void Mesh::Append(const Mesh& other) {
|
||||
const auto base = static_cast<uint32_t>(positions.size());
|
||||
positions.insert(positions.end(), other.positions.begin(), other.positions.end());
|
||||
normals.insert(normals.end(), other.normals.begin(), other.normals.end());
|
||||
colors.insert(colors.end(), other.colors.begin(), other.colors.end());
|
||||
if (!glow.empty() || !other.glow.empty()) {
|
||||
glow.resize(base, glm::vec3(0.0f));
|
||||
if (other.glow.empty()) glow.resize(positions.size(), glm::vec3(0.0f));
|
||||
else glow.insert(glow.end(), other.glow.begin(), other.glow.end());
|
||||
}
|
||||
if (!looks.empty() || !other.looks.empty()) {
|
||||
looks.resize(base, eLook::PLASTIC);
|
||||
if (other.looks.empty()) looks.resize(positions.size(), eLook::PLASTIC);
|
||||
else looks.insert(looks.end(), other.looks.begin(), other.looks.end());
|
||||
}
|
||||
indices.reserve(indices.size() + other.indices.size());
|
||||
for (const auto index : other.indices) indices.push_back(base + index);
|
||||
}
|
||||
|
||||
void Mesh::Transform(const glm::mat4& transform) {
|
||||
const glm::mat3 normalMatrix = glm::transpose(glm::inverse(glm::mat3(transform)));
|
||||
for (auto& position : positions) position = glm::vec3(transform * glm::vec4(position, 1.0f));
|
||||
for (auto& normal : normals) {
|
||||
const auto n = normalMatrix * normal;
|
||||
const auto length = glm::length(n);
|
||||
normal = length > 0.0f ? n / length : n;
|
||||
}
|
||||
}
|
||||
|
||||
bool Model::Bounds(glm::vec3& min, glm::vec3& max) const {
|
||||
bool any = false;
|
||||
for (const auto* mesh : { &opaque, &transparent }) {
|
||||
for (const auto& position : mesh->positions) {
|
||||
min = any ? glm::min(min, position) : position;
|
||||
max = any ? glm::max(max, position) : position;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
eLook LookOf(uint32_t id, const UgcBricks::Material& material, const LookRules& rules) {
|
||||
if (const auto color = rules.colors.find(id); color != rules.colors.end()) return color->second;
|
||||
if (rules.paletteGlow && UgcPalette::Glow(id)) return eLook::GLOW;
|
||||
if (rules.paletteMetallic && UgcPalette::IsMetallic(id)) return eLook::METAL;
|
||||
const auto type = rules.materialTypes.find(material.type);
|
||||
return type != rules.materialTypes.end() ? type->second : eLook::PLASTIC;
|
||||
}
|
||||
|
||||
std::optional<std::array<Mesh, LOOK_COUNT>> SplitLooks(const Mesh& mesh, const std::array<bool, LOOK_COUNT>& separate) {
|
||||
if (mesh.looks.size() != mesh.positions.size()) return std::nullopt;
|
||||
const auto lookOf = [&](size_t triangle) {
|
||||
const auto look = mesh.looks[mesh.indices[triangle * 3]];
|
||||
return separate[static_cast<size_t>(look)] ? look : eLook::PLASTIC;
|
||||
};
|
||||
bool any = false;
|
||||
for (size_t t = 0; t < mesh.TriangleCount() && !any; t++) any = lookOf(t) != eLook::PLASTIC;
|
||||
if (!any) return std::nullopt;
|
||||
std::array<Mesh, LOOK_COUNT> out;
|
||||
for (size_t look = 0; look < LOOK_COUNT; look++) {
|
||||
std::vector<bool> keep(mesh.TriangleCount());
|
||||
bool some = false;
|
||||
for (size_t t = 0; t < keep.size(); t++) some = (keep[t] = static_cast<size_t>(lookOf(t)) == look) || some;
|
||||
if (!some) continue;
|
||||
out[look] = mesh;
|
||||
KeepTriangles(out[look], keep);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Model Build(const std::vector<Part>& parts, UgcBricks::BrickLibrary& library, const BuildOptions& options) {
|
||||
Model model;
|
||||
std::set<uint32_t> missing;
|
||||
const bool luToolbox = options.palette == ePalette::LU_TOOLBOX;
|
||||
bool anyGlow = false, anyLook = false, anyTransparentLook = false;
|
||||
for (uint32_t brick = 0; brick < parts.size(); brick++) {
|
||||
const auto& part = parts[brick];
|
||||
const auto design = library.GetDesign(part.designId, options.lod);
|
||||
if (!design || design->empty()) {
|
||||
missing.insert(part.designId);
|
||||
continue;
|
||||
}
|
||||
model.bricks++;
|
||||
const auto materialOf = [&part, &library](size_t index) {
|
||||
auto id = index < part.materials.size() ? part.materials[index] : (part.materials.empty() ? 0 : part.materials[0]);
|
||||
// Unknown colors are black in LU Toolbox (its name included, so black's variation too). A color LU
|
||||
// Toolbox doesn't know but the client's Materials.xml has (one added to the brick database) keeps its id
|
||||
// and takes its color from there.
|
||||
if (id == 0 || (!UgcPalette::Linear(id) && !library.HasMaterial(id))) id = UgcPalette::FALLBACK_ID;
|
||||
return id;
|
||||
};
|
||||
// Whether LU Toolbox's own palette colors a material (else it's one only the client's Materials.xml has)
|
||||
const auto inToolbox = [](uint32_t id) { return UgcPalette::Linear(id).has_value(); };
|
||||
// A brick is transparent only when all of its materials are (LU Toolbox's IS_TRANSPARENT)
|
||||
bool transparent = true;
|
||||
for (size_t index = 0; index < design->size(); index++) {
|
||||
const auto id = index < part.materials.size() ? part.materials[index] : (part.materials.empty() ? 0 : part.materials[0]);
|
||||
const auto toolboxId = materialOf(index);
|
||||
const auto named = options.transparentColors.contains(luToolbox ? toolboxId : id);
|
||||
transparent = transparent && (named || (luToolbox && inToolbox(toolboxId) ? UgcPalette::IsTransparent(toolboxId) : library.GetMaterial(luToolbox ? toolboxId : id).Transparent()));
|
||||
}
|
||||
auto& mesh = transparent ? model.transparent : model.opaque;
|
||||
if (transparent) model.transparentBricks.push_back(mesh.indices.size());
|
||||
const glm::mat3 normalMatrix = glm::transpose(glm::inverse(glm::mat3(part.transform)));
|
||||
for (size_t index = 0; index < design->size(); index++) {
|
||||
const auto& geometry = (*design)[index];
|
||||
glm::vec3 linear{};
|
||||
float alpha = 1.0f;
|
||||
glm::vec3 glow(0.0f);
|
||||
uint32_t colorId{};
|
||||
if (luToolbox && !inToolbox(materialOf(index))) {
|
||||
// A color only the client's Materials.xml has
|
||||
colorId = materialOf(index);
|
||||
const auto material = library.GetMaterial(colorId);
|
||||
linear = UgcPalette::SrgbToLinear(glm::vec3(material.r, material.g, material.b) / 255.0f);
|
||||
if (transparent) alpha = material.a < 255 ? material.a / 255.0f : std::clamp(options.transparentOpacity / 100.0f, 0.0f, 1.0f);
|
||||
} else if (luToolbox) {
|
||||
colorId = materialOf(index);
|
||||
linear = *UgcPalette::Linear(colorId, options.icon);
|
||||
if (transparent) alpha = std::clamp(options.transparentOpacity / 100.0f, 0.0f, 1.0f);
|
||||
if (!transparent) {
|
||||
if (const auto g = UgcPalette::Glow(colorId)) glow = *g;
|
||||
}
|
||||
} else {
|
||||
colorId = index < part.materials.size() ? part.materials[index] : (part.materials.empty() ? 0 : part.materials[0]);
|
||||
const auto material = library.GetMaterial(colorId);
|
||||
linear = UgcPalette::SrgbToLinear(glm::vec3(material.r, material.g, material.b) / 255.0f);
|
||||
if (transparent) alpha = material.a < 255 ? material.a / 255.0f : std::clamp(options.transparentOpacity / 100.0f, 0.0f, 1.0f);
|
||||
}
|
||||
if (options.colorVariation > 0.0f) {
|
||||
const float variation = options.colorVariation * (luToolbox ? UgcPalette::VariationScale(colorId) : 1.0f);
|
||||
linear = UgcPalette::ApplyVariation(linear, variation, UgcPalette::BrickRandom(options.seed, brick, colorId));
|
||||
}
|
||||
if (!options.icon && options.brightness != 100.0f) linear *= std::max(options.brightness, 0.0f) / 100.0f;
|
||||
if (options.satinColors.contains(colorId)) {
|
||||
// Satin: milky, and less see-through than clear plastic
|
||||
linear = glm::mix(linear, glm::vec3(1.0f), std::clamp(options.satinWhiten / 100.0f, 0.0f, 1.0f));
|
||||
if (transparent) alpha = std::clamp(options.satinOpacity / 100.0f, 0.0f, 1.0f);
|
||||
}
|
||||
const glm::vec4 color(UgcPalette::LinearToSrgb(linear), alpha);
|
||||
anyGlow = anyGlow || glow != glm::vec3(0.0f);
|
||||
auto look = LookOf(colorId, library.GetMaterial(colorId), options.looks);
|
||||
if (transparent && look != eLook::GLITTER) look = eLook::PLASTIC;
|
||||
(transparent ? anyTransparentLook : anyLook) = (transparent ? anyTransparentLook : anyLook) || look != eLook::PLASTIC;
|
||||
const auto base = static_cast<uint32_t>(mesh.positions.size());
|
||||
const size_t vertexCount = geometry.positions.size() / 3;
|
||||
for (size_t v = 0; v < vertexCount; v++) {
|
||||
const glm::vec3 position(geometry.positions[v * 3], geometry.positions[v * 3 + 1], geometry.positions[v * 3 + 2]);
|
||||
glm::vec3 normal(geometry.normals[v * 3], geometry.normals[v * 3 + 1], geometry.normals[v * 3 + 2]);
|
||||
normal = normalMatrix * normal;
|
||||
const auto length = glm::length(normal);
|
||||
if (length > 0.0f) normal /= length;
|
||||
mesh.positions.push_back(glm::vec3(part.transform * glm::vec4(position, 1.0f)));
|
||||
mesh.normals.push_back(normal);
|
||||
mesh.colors.push_back(color);
|
||||
if (&mesh == &model.opaque) model.opaque.glow.push_back(glow);
|
||||
mesh.looks.push_back(look);
|
||||
}
|
||||
for (const auto i : geometry.indices) mesh.indices.push_back(base + i);
|
||||
}
|
||||
}
|
||||
if (!anyGlow) model.opaque.glow.clear();
|
||||
else model.opaque.glow.resize(model.opaque.positions.size(), glm::vec3(0.0f));
|
||||
if (!anyLook) model.opaque.looks.clear();
|
||||
if (!anyTransparentLook) model.transparent.looks.clear();
|
||||
model.missingDesigns.assign(missing.begin(), missing.end());
|
||||
return model;
|
||||
}
|
||||
|
||||
std::vector<std::pair<float, float>> LodRanges(const std::vector<uint32_t>& used, const LodDistances& d) {
|
||||
// LU Toolbox's setup_lod_data ("DYNAMIC LOD HELL"), by the set of levels there are
|
||||
const std::set<uint32_t> set(used.begin(), used.end());
|
||||
const auto is = [&set](std::initializer_list<uint32_t> levels) { return set == std::set<uint32_t>(levels); };
|
||||
std::vector<std::pair<float, float>> ranges;
|
||||
for (const auto level : used) {
|
||||
std::pair<float, float> range{ 0.0f, 0.0f };
|
||||
if (set.size() == 1) {
|
||||
range = { d.lod0, d.cull };
|
||||
} else if (level == 0) {
|
||||
range.first = d.lod0;
|
||||
if (is({ 0, 2 }) || is({ 0, 2, 3 })) range.second = d.lod2;
|
||||
else if (is({ 0, 3 })) range.second = d.lod3;
|
||||
else range.second = d.lod1;
|
||||
} else if (level == 1) {
|
||||
if (is({ 0, 1 })) range = { d.lod1, d.cull };
|
||||
else if (is({ 1, 2 }) || is({ 1, 2, 3 })) range = { d.lod0, d.lod2 };
|
||||
else if (is({ 0, 1, 3 })) range = { d.lod1, d.lod3 };
|
||||
else if (is({ 1, 3 })) range = { d.lod0, d.lod3 };
|
||||
else if (is({ 0, 1, 2 }) || is({ 0, 1, 2, 3 })) range = { d.lod1, d.lod2 };
|
||||
} else if (level == 2) {
|
||||
if (is({ 0, 2 }) || is({ 1, 2 }) || is({ 0, 1, 2 })) range = { d.lod2, d.cull };
|
||||
else if (is({ 2, 3 })) range = { d.lod0, d.lod3 };
|
||||
else if (is({ 0, 2, 3 }) || is({ 1, 2, 3 }) || is({ 0, 1, 2, 3 })) range = { d.lod2, d.lod3 };
|
||||
} else if (level == 3) {
|
||||
range = { d.lod3, d.cull };
|
||||
}
|
||||
ranges.push_back(range);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks) {
|
||||
Model model;
|
||||
for (const auto& source : nif.meshes) {
|
||||
Mesh mesh;
|
||||
const size_t count = source.positions.size() / 3;
|
||||
const bool vertexColors = source.material.vertexColorMode == 2 && source.colors.size() == count * 4;
|
||||
const glm::vec4 materialColor(source.material.diffuse[0], source.material.diffuse[1], source.material.diffuse[2], source.material.alpha);
|
||||
for (size_t v = 0; v < count; v++) {
|
||||
mesh.positions.emplace_back(source.positions[v * 3], source.positions[v * 3 + 1], source.positions[v * 3 + 2]);
|
||||
if (source.normals.size() == count * 3) mesh.normals.emplace_back(source.normals[v * 3], source.normals[v * 3 + 1], source.normals[v * 3 + 2]);
|
||||
glm::vec4 color = materialColor;
|
||||
if (vertexColors) color *= glm::vec4(source.colors[v * 4], source.colors[v * 4 + 1], source.colors[v * 4 + 2], source.colors[v * 4 + 3]) / 255.0f;
|
||||
mesh.colors.push_back(color);
|
||||
}
|
||||
mesh.indices.assign(source.indices.begin(), source.indices.end());
|
||||
// Normals from the faces when the file has none
|
||||
if (mesh.normals.size() != count) {
|
||||
mesh.normals.assign(count, glm::vec3(0.0f));
|
||||
for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) {
|
||||
const auto& a = mesh.positions[mesh.indices[i]];
|
||||
const auto face = glm::cross(mesh.positions[mesh.indices[i + 1]] - a, mesh.positions[mesh.indices[i + 2]] - a);
|
||||
for (int k = 0; k < 3; k++) mesh.normals[mesh.indices[i + k]] += face;
|
||||
}
|
||||
for (auto& normal : mesh.normals) {
|
||||
const auto length = glm::length(normal);
|
||||
normal = length > 0.0f ? normal / length : glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
// Blending only shows where something is see-through (the game's brick models blend every shape)
|
||||
bool seeThrough = source.material.alphaBlend && source.material.alpha < 0.99f;
|
||||
for (size_t v = 0; source.material.alphaBlend && !seeThrough && v < mesh.colors.size(); v++) seeThrough = mesh.colors[v].a < 0.99f;
|
||||
if (const auto look = tagLooks.find(source.material.shaderTag); look != tagLooks.end() && look->second != eLook::PLASTIC && (!seeThrough || look->second == eLook::GLITTER)) {
|
||||
mesh.looks.assign(mesh.positions.size(), look->second);
|
||||
}
|
||||
(seeThrough ? model.transparent : model.opaque).Append(mesh);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
std::vector<Mesh> SplitAt(const Mesh& mesh, const std::vector<size_t>& starts) {
|
||||
std::vector<Mesh> pieces;
|
||||
std::unordered_map<uint32_t, uint32_t> remap;
|
||||
for (size_t i = 0; i < starts.size(); i++) {
|
||||
const size_t first = starts[i], last = std::min(i + 1 < starts.size() ? starts[i + 1] : mesh.indices.size(), mesh.indices.size());
|
||||
if (first >= last) continue;
|
||||
Mesh piece;
|
||||
remap.clear();
|
||||
for (size_t k = first; k < last; k++) {
|
||||
const auto source = mesh.indices[k];
|
||||
auto [it, added] = remap.try_emplace(source, static_cast<uint32_t>(piece.positions.size()));
|
||||
if (added) {
|
||||
piece.positions.push_back(mesh.positions[source]);
|
||||
if (source < mesh.normals.size()) piece.normals.push_back(mesh.normals[source]);
|
||||
if (source < mesh.colors.size()) piece.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) piece.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) piece.looks.push_back(mesh.looks[source]);
|
||||
}
|
||||
piece.indices.push_back(it->second);
|
||||
}
|
||||
pieces.push_back(std::move(piece));
|
||||
}
|
||||
return pieces;
|
||||
}
|
||||
|
||||
void KeepTriangles(Mesh& mesh, const std::vector<bool>& keep) {
|
||||
Mesh kept;
|
||||
std::vector<uint32_t> remap(mesh.positions.size(), UINT32_MAX);
|
||||
for (size_t t = 0; t < mesh.TriangleCount(); t++) {
|
||||
if (t >= keep.size() || !keep[t]) continue;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
const auto source = mesh.indices[t * 3 + k];
|
||||
if (remap[source] == UINT32_MAX) {
|
||||
remap[source] = static_cast<uint32_t>(kept.positions.size());
|
||||
kept.positions.push_back(mesh.positions[source]);
|
||||
if (source < mesh.normals.size()) kept.normals.push_back(mesh.normals[source]);
|
||||
if (source < mesh.colors.size()) kept.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) kept.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) kept.looks.push_back(mesh.looks[source]);
|
||||
}
|
||||
kept.indices.push_back(remap[source]);
|
||||
}
|
||||
}
|
||||
mesh = std::move(kept);
|
||||
}
|
||||
|
||||
std::vector<Mesh> Split(const Mesh& mesh, size_t maxVertices, size_t maxTriangles) {
|
||||
std::vector<Mesh> pieces;
|
||||
if (mesh.positions.size() <= maxVertices && mesh.TriangleCount() <= maxTriangles) {
|
||||
if (!mesh.Empty()) pieces.push_back(mesh);
|
||||
return pieces;
|
||||
}
|
||||
Mesh current;
|
||||
std::unordered_map<uint32_t, uint32_t> remap;
|
||||
const auto flush = [&]() {
|
||||
if (!current.Empty()) pieces.push_back(std::move(current));
|
||||
current = Mesh{};
|
||||
remap.clear();
|
||||
};
|
||||
for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) {
|
||||
size_t newVertices = 0;
|
||||
for (int k = 0; k < 3; k++) newVertices += remap.contains(mesh.indices[i + k]) ? 0 : 1;
|
||||
if (current.positions.size() + newVertices > maxVertices || current.TriangleCount() + 1 > maxTriangles) flush();
|
||||
for (int k = 0; k < 3; k++) {
|
||||
const auto source = mesh.indices[i + k];
|
||||
auto [it, added] = remap.try_emplace(source, static_cast<uint32_t>(current.positions.size()));
|
||||
if (added) {
|
||||
current.positions.push_back(mesh.positions[source]);
|
||||
if (source < mesh.normals.size()) current.normals.push_back(mesh.normals[source]);
|
||||
if (source < mesh.colors.size()) current.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) current.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) current.looks.push_back(mesh.looks[source]);
|
||||
}
|
||||
current.indices.push_back(it->second);
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return pieces;
|
||||
}
|
||||
|
||||
std::vector<Mesh> Divide(const Mesh& mesh, size_t maxVertices, size_t maxTriangles) {
|
||||
if (mesh.Empty()) return {};
|
||||
if (mesh.positions.size() <= maxVertices && mesh.TriangleCount() <= maxTriangles) return { mesh };
|
||||
|
||||
// Connected pieces (vertices joined by triangles), so a brick's faces stay together
|
||||
std::vector<uint32_t> parent(mesh.positions.size());
|
||||
for (uint32_t i = 0; i < parent.size(); i++) parent[i] = i;
|
||||
const std::function<uint32_t(uint32_t)> find = [&](uint32_t x) {
|
||||
while (parent[x] != x) x = parent[x] = parent[parent[x]];
|
||||
return x;
|
||||
};
|
||||
for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) {
|
||||
const auto a = find(mesh.indices[i]);
|
||||
parent[find(mesh.indices[i + 1])] = a;
|
||||
parent[find(mesh.indices[i + 2])] = a;
|
||||
}
|
||||
|
||||
// divide_mesh: vertices below the mean along the longest side of the bounds, and everything linked to them
|
||||
glm::vec3 min = mesh.positions[0], max = mesh.positions[0], mean(0.0f);
|
||||
for (const auto& p : mesh.positions) {
|
||||
min = glm::min(min, p);
|
||||
max = glm::max(max, p);
|
||||
mean += p;
|
||||
}
|
||||
mean /= static_cast<float>(mesh.positions.size());
|
||||
const auto size = max - min;
|
||||
const int axis = size.x >= size.y && size.x >= size.z ? 0 : size.y >= size.z ? 1 : 2;
|
||||
std::vector<bool> below(mesh.positions.size(), false);
|
||||
for (uint32_t v = 0; v < mesh.positions.size(); v++) {
|
||||
if (mesh.positions[v][axis] < mean[axis]) below[find(v)] = true;
|
||||
}
|
||||
|
||||
Mesh halves[2];
|
||||
std::vector<uint32_t> remap(mesh.positions.size(), UINT32_MAX);
|
||||
for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) {
|
||||
auto& half = halves[below[find(mesh.indices[i])] ? 1 : 0];
|
||||
for (int k = 0; k < 3; k++) {
|
||||
const auto source = mesh.indices[i + k];
|
||||
if (remap[source] == UINT32_MAX) {
|
||||
remap[source] = static_cast<uint32_t>(half.positions.size());
|
||||
half.positions.push_back(mesh.positions[source]);
|
||||
if (source < mesh.normals.size()) half.normals.push_back(mesh.normals[source]);
|
||||
if (source < mesh.colors.size()) half.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) half.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) half.looks.push_back(mesh.looks[source]);
|
||||
}
|
||||
half.indices.push_back(remap[source]);
|
||||
}
|
||||
}
|
||||
// LU Toolbox gives up below a 10% share; this splits the old way then
|
||||
const float share = static_cast<float>(halves[1].positions.size()) / static_cast<float>(mesh.positions.size());
|
||||
if (std::min(share, 1.0f - share) < 0.1f) return Split(mesh, maxVertices, maxTriangles);
|
||||
std::vector<Mesh> pieces;
|
||||
for (const auto& half : halves) {
|
||||
for (auto& piece : Divide(half, maxVertices, maxTriangles)) pieces.push_back(std::move(piece));
|
||||
}
|
||||
return pieces;
|
||||
}
|
||||
}
|
||||
160
dUgcServer/Model/UgcModel.h
Normal file
160
dUgcServer/Model/UgcModel.h
Normal file
@@ -0,0 +1,160 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "UgcBricks.h"
|
||||
|
||||
namespace NifFile {
|
||||
struct Model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Brick models as triangle meshes: LXFML parts, the mesh built from them (opaque and transparent bricks apart, with
|
||||
* the material colors as vertex colors) and the mesh of a client .nif. Pure apart from BrickLibrary's file reads.
|
||||
*/
|
||||
namespace UgcModel {
|
||||
// A brick in a model: its primitive, materials (one per geometry part) and where it is
|
||||
struct Part {
|
||||
uint32_t designId{};
|
||||
std::vector<uint32_t> materials;
|
||||
glm::mat4 transform{ 1.0f };
|
||||
};
|
||||
|
||||
// The parts of an LXFML 5 (Bricks/Brick/Part with bones) or 4 (Scene/Model/Group/Part with axis angles) model.
|
||||
// Empty with `error` set when it can't be read.
|
||||
std::vector<Part> ParseLxfml(std::string_view lxfml, std::string& error);
|
||||
|
||||
// Whether an LXFML reads but has no bricks at all (nothing to make; not a failure)
|
||||
bool HasNoBricks(std::string_view lxfml);
|
||||
|
||||
/**
|
||||
* How a color looks in the game when the UGC server's shader settings give it a shader of its own
|
||||
* (docs/UgcServer.md, "Metal and glow"): the LEGO plastic of S01_Opaque_Model, polished metal, brushed steel, glow
|
||||
* or glitter. Transparent bricks are plastic or glitter.
|
||||
*/
|
||||
enum class eLook : uint8_t { PLASTIC = 0, METAL, BRUSHED, GLOW, GLITTER };
|
||||
constexpr size_t LOOK_COUNT = 5;
|
||||
|
||||
struct Mesh {
|
||||
std::vector<glm::vec3> positions;
|
||||
std::vector<glm::vec3> normals;
|
||||
std::vector<glm::vec4> colors; // sRGB, 0..1, alpha is opacity
|
||||
std::vector<glm::vec3> glow; // linear glow color per vertex (LU Toolbox's "Glow" layer); empty when nothing glows
|
||||
std::vector<eLook> looks; // per vertex; empty when everything is plastic (transparent meshes: plastic or glitter)
|
||||
std::vector<uint32_t> indices;
|
||||
|
||||
size_t TriangleCount() const { return indices.size() / 3; }
|
||||
bool Empty() const { return indices.empty(); }
|
||||
void Append(const Mesh& other);
|
||||
void Transform(const glm::mat4& transform);
|
||||
};
|
||||
|
||||
struct Model {
|
||||
Mesh opaque;
|
||||
Mesh transparent;
|
||||
std::vector<uint32_t> missingDesigns; // designs without geometry in the client, skipped
|
||||
std::vector<size_t> transparentBricks; // where each transparent brick's triangles start in transparent.indices
|
||||
size_t bricks{};
|
||||
|
||||
bool Empty() const { return opaque.Empty() && transparent.Empty(); }
|
||||
// The bounds of every vertex; false when there are none
|
||||
bool Bounds(glm::vec3& min, glm::vec3& max) const;
|
||||
};
|
||||
|
||||
enum class ePalette {
|
||||
LU_TOOLBOX, // LU Toolbox's colors (UgcPalette), what its importer colors models with
|
||||
BRICKDB, // the brick database's Materials.xml
|
||||
};
|
||||
|
||||
/**
|
||||
* Which colors have which look, from the client's data: a Materials.xml MaterialType (brickdb.zip) and LU Toolbox's
|
||||
* metallic and glow colors (UgcPalette), and colors named in the settings. A named color wins, then glow over metal;
|
||||
* transparent bricks are plastic unless their color is glitter.
|
||||
*/
|
||||
struct LookRules {
|
||||
std::map<uint32_t, eLook> colors; // LEGO color ids given a look by the settings (brushed_colors, glitter_colors)
|
||||
std::map<std::string, eLook> materialTypes{ { "shinySteel", eLook::METAL }, { "brushedSteel", eLook::BRUSHED }, { "matteSteel", eLook::BRUSHED },
|
||||
{ "glitter", eLook::GLITTER } };
|
||||
bool paletteMetallic{ true }; // LU Toolbox's Metallic colors (UgcPalette::IsMetallic) are METAL
|
||||
bool paletteGlow{ true }; // its glow colors (UgcPalette::Glow) are GLOW
|
||||
};
|
||||
|
||||
// The look of an opaque material `id` whose Materials.xml entry is `material`
|
||||
eLook LookOf(uint32_t id, const UgcBricks::Material& material, const LookRules& rules);
|
||||
|
||||
struct BuildOptions {
|
||||
ePalette palette{ ePalette::LU_TOOLBOX };
|
||||
float colorVariation{ 5.0f }; // percent, 0: none (LU Toolbox: Apply Color Variation, 5%)
|
||||
uint64_t seed{}; // of the variation's random numbers
|
||||
float transparentOpacity{ 58.82f }; // percent, transparent bricks' vertex alpha (LU Toolbox palette only)
|
||||
float brightness{ 100.0f }; // percent, the models' vertex colors (not icons'); 100: as the palette has them
|
||||
std::set<uint32_t> transparentColors; // color ids drawn transparent whatever Materials.xml says (129: its alpha is 255)
|
||||
bool icon{}; // the icon renderer's color corrections
|
||||
uint32_t lod{}; // brickprimitives level
|
||||
LookRules looks; // which colors are metal and glow (Mesh::looks)
|
||||
// Satin (opal) colors: transparent bricks of these colors get satinOpacity (percent) as their vertex alpha
|
||||
// instead of the transparent opacity, and every brick of them has its color moved satinWhiten percent towards
|
||||
// white (milky). The client has no satin shader: they stay in the transparent group. Empty: none.
|
||||
std::set<uint32_t> satinColors;
|
||||
float satinOpacity{ 75.0f };
|
||||
float satinWhiten{ 20.0f };
|
||||
};
|
||||
|
||||
/**
|
||||
* The mesh of a model's parts, colored as LU Toolbox's Process Model does: a brick is transparent only when all of
|
||||
* its materials are, each material of each brick has its brightness shifted by the color variation (the same
|
||||
* random number for a brick's material in every LOD and every time), vertex colors are sRGB with alpha 1 for
|
||||
* opaque bricks and the transparent opacity for transparent ones.
|
||||
*/
|
||||
Model Build(const std::vector<Part>& parts, UgcBricks::BrickLibrary& library, const BuildOptions& options = {});
|
||||
|
||||
/**
|
||||
* The distance range (near, far) of each LOD LU Toolbox makes, for the brickprimitives levels in `used` (0 to 3),
|
||||
* from its settings (lod0..lod3, cull): its setup_lod_data, which picks the ranges by which levels are there.
|
||||
* {0, 0} for a level it has no range for.
|
||||
*/
|
||||
struct LodDistances {
|
||||
float lod0{ 0.0f };
|
||||
float lod1{ 50.0f };
|
||||
float lod2{ 100.0f };
|
||||
float lod3{ 280.0f };
|
||||
float cull{ 10000.0f };
|
||||
};
|
||||
std::vector<std::pair<float, float>> LodRanges(const std::vector<uint32_t>& used, const LodDistances& distances);
|
||||
|
||||
/**
|
||||
* Splits a mesh the way LU Toolbox's divide_mesh does while it has too many vertices (or triangles): at the mean
|
||||
* of its vertices along its longest side, keeping connected pieces whole. Falls back to Split when that can't
|
||||
* divide it.
|
||||
*/
|
||||
std::vector<Mesh> Divide(const Mesh& mesh, size_t maxVertices = 65535, size_t maxTriangles = 65535);
|
||||
|
||||
// A client .nif's meshes as one model (vertex colors times material color; transparent when blended). `tagLooks`:
|
||||
// the look of the opaque shapes whose multishader tag (NifFile::ShaderTag, a mapShaders id) is listed, and of the
|
||||
// transparent ones when it is GLITTER
|
||||
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks = {});
|
||||
|
||||
/**
|
||||
* The mesh's triangles by look ([eLook] -> its triangles; a triangle's look is its first vertex's), the looks not
|
||||
* in `separate` staying with PLASTIC. nullopt when nothing is separated: the mesh stays as it is.
|
||||
*/
|
||||
std::optional<std::array<Mesh, LOOK_COUNT>> SplitLooks(const Mesh& mesh, const std::array<bool, LOOK_COUNT>& separate);
|
||||
|
||||
// The mesh cut into pieces at these index offsets (each piece's triangles start at one), e.g. one per brick
|
||||
std::vector<Mesh> SplitAt(const Mesh& mesh, const std::vector<size_t>& starts);
|
||||
|
||||
// Keeps the triangles whose flag is set (and the vertices they use)
|
||||
void KeepTriangles(Mesh& mesh, const std::vector<bool>& keep);
|
||||
|
||||
// Splits a mesh into pieces the .nif format can hold (at most `maxVertices` vertices and `maxTriangles` triangles)
|
||||
std::vector<Mesh> Split(const Mesh& mesh, size_t maxVertices = 65535, size_t maxTriangles = 65535);
|
||||
}
|
||||
132
dUgcServer/Model/UgcModular.cpp
Normal file
132
dUgcServer/Model/UgcModular.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
#include "UgcModular.h"
|
||||
|
||||
#include <deque>
|
||||
#include <set>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include "tinyxml2.h"
|
||||
|
||||
namespace {
|
||||
const tinyxml2::XMLElement* Child(const tinyxml2::XMLElement* element, const char* name) {
|
||||
return element ? element->FirstChildElement(name) : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
namespace UgcModular {
|
||||
std::optional<BuildInfo> ParseBuild(std::string_view xml) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
if (doc.Parse(xml.data(), xml.size()) != tinyxml2::XML_SUCCESS) return std::nullopt;
|
||||
const auto* root = doc.FirstChildElement("ModularBuild");
|
||||
const auto* topology = Child(root, "topology");
|
||||
if (!topology) return std::nullopt;
|
||||
BuildInfo build;
|
||||
if (const auto* rootPart = Child(topology, "rootPart")) build.rootPart = rootPart->UnsignedAttribute("value");
|
||||
if (const auto* count = Child(topology, "numberOfParts")) build.parts = count->UnsignedAttribute("value");
|
||||
for (const auto* connection = topology->FirstChildElement("connection"); connection; connection = connection->NextSiblingElement("connection")) {
|
||||
const char* location = connection->Attribute("myLocation");
|
||||
build.connections.push_back({ connection->UnsignedAttribute("myPartid"), location ? location : "", connection->UnsignedAttribute("connectingPart") });
|
||||
}
|
||||
if (const auto* rotation = Child(Child(Child(root, "Placement"), "AdditionalModelRotation"), "Rotation")) {
|
||||
glm::quat q(rotation->FloatAttribute("w", 1.0f), rotation->FloatAttribute("x"), rotation->FloatAttribute("y"), rotation->FloatAttribute("z"));
|
||||
if (glm::length(q) > 0.0f) build.additionalRotation = glm::mat4_cast(glm::normalize(q));
|
||||
}
|
||||
return build;
|
||||
}
|
||||
|
||||
std::map<std::string, glm::vec3> ParseModuleConnections(std::string_view xml) {
|
||||
std::map<std::string, glm::vec3> connections;
|
||||
tinyxml2::XMLDocument doc;
|
||||
if (doc.Parse(xml.data(), xml.size()) != tinyxml2::XML_SUCCESS) return connections;
|
||||
const auto* root = doc.FirstChildElement("ModuleInfo");
|
||||
if (!root) return connections;
|
||||
for (const auto* connection = root->FirstChildElement("connection"); connection; connection = connection->NextSiblingElement("connection")) {
|
||||
const char* name = connection->Attribute("name");
|
||||
const auto* translation = connection->FirstChildElement("translation");
|
||||
if (!name || !translation) continue;
|
||||
connections[name] = glm::vec3(translation->FloatAttribute("x"), translation->FloatAttribute("y"), translation->FloatAttribute("z"));
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> ParseModuleLots(std::string_view ldf) {
|
||||
std::vector<uint32_t> lots;
|
||||
size_t start = 0;
|
||||
while (start <= ldf.size()) {
|
||||
size_t end = ldf.find_first_of("+;,", start);
|
||||
if (end == std::string_view::npos) end = ldf.size();
|
||||
auto item = ldf.substr(start, end - start);
|
||||
if (const auto colon = item.find(':'); colon != std::string_view::npos) item = item.substr(colon + 1);
|
||||
uint32_t value = 0;
|
||||
bool digits = !item.empty();
|
||||
for (const char c : item) {
|
||||
if (c < '0' || c > '9') {
|
||||
digits = false;
|
||||
break;
|
||||
}
|
||||
value = value * 10 + static_cast<uint32_t>(c - '0');
|
||||
}
|
||||
if (digits && value != 0) lots.push_back(value);
|
||||
start = end + 1;
|
||||
}
|
||||
return lots;
|
||||
}
|
||||
|
||||
UgcModel::Model Assemble(const BuildInfo& build, const std::vector<Module>& modules, std::string& warnings) {
|
||||
UgcModel::Model model;
|
||||
std::map<uint32_t, const Module*> byPart;
|
||||
for (const auto& module : modules) byPart.emplace(module.partCode, &module);
|
||||
|
||||
std::map<uint32_t, glm::mat4> placed;
|
||||
std::deque<uint32_t> pending;
|
||||
if (byPart.contains(build.rootPart)) {
|
||||
placed[build.rootPart] = glm::mat4(1.0f);
|
||||
pending.push_back(build.rootPart);
|
||||
} else if (!modules.empty()) {
|
||||
warnings += "no module for the root part " + std::to_string(build.rootPart) + "; ";
|
||||
placed[modules.front().partCode] = glm::mat4(1.0f);
|
||||
pending.push_back(modules.front().partCode);
|
||||
}
|
||||
|
||||
while (!pending.empty()) {
|
||||
const auto part = pending.front();
|
||||
pending.pop_front();
|
||||
const auto* parent = byPart.at(part);
|
||||
for (const auto& connection : build.connections) {
|
||||
if (connection.part != part || placed.contains(connection.connectingPart)) continue;
|
||||
const auto childIt = byPart.find(connection.connectingPart);
|
||||
if (childIt == byPart.end()) continue; // optional parts
|
||||
glm::vec3 anchor(0.0f);
|
||||
if (const auto node = parent->nif.nodes.find(connection.location); node != parent->nif.nodes.end()) {
|
||||
anchor = glm::vec3(node->second.translation[0], node->second.translation[1], node->second.translation[2]);
|
||||
} else if (const auto offset = parent->connections.find(connection.location); offset != parent->connections.end()) {
|
||||
anchor = offset->second;
|
||||
} else {
|
||||
warnings += "part " + std::to_string(part) + " has no " + connection.location + "; ";
|
||||
}
|
||||
const auto& child = *childIt->second;
|
||||
if (const auto node = child.nif.nodes.find(connection.location); node != child.nif.nodes.end()) {
|
||||
anchor -= glm::vec3(node->second.translation[0], node->second.translation[1], node->second.translation[2]);
|
||||
}
|
||||
placed[connection.connectingPart] = placed[part] * glm::translate(glm::mat4(1.0f), anchor);
|
||||
pending.push_back(connection.connectingPart);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& module : modules) {
|
||||
const auto it = placed.find(module.partCode);
|
||||
if (it == placed.end()) {
|
||||
warnings += "part " + std::to_string(module.partCode) + " isn't connected; ";
|
||||
continue;
|
||||
}
|
||||
auto mesh = UgcModel::FromNif(module.nif);
|
||||
mesh.opaque.Transform(it->second);
|
||||
mesh.transparent.Transform(it->second);
|
||||
model.opaque.Append(mesh.opaque);
|
||||
model.transparent.Append(mesh.transparent);
|
||||
model.bricks++;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
}
|
||||
57
dUgcServer/Model/UgcModular.h
Normal file
57
dUgcServer/Model/UgcModular.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "NifFile.h"
|
||||
#include "UgcModel.h"
|
||||
|
||||
/**
|
||||
* Modular builds (cars, rockets) put together from their modules the way the CDClient describes it:
|
||||
* ModularBuildComponent.xml has the topology (a root part and which part connects to which named location of which
|
||||
* other part), ModuleComponent.xml each module's connection offsets, and each module's render asset has the named
|
||||
* attach-point nodes. Pure.
|
||||
*/
|
||||
namespace UgcModular {
|
||||
struct Connection {
|
||||
uint32_t part{}; // the part the location is on
|
||||
std::string location; // node name on that part
|
||||
uint32_t connectingPart{}; // the part placed there
|
||||
};
|
||||
|
||||
struct BuildInfo {
|
||||
uint32_t rootPart{};
|
||||
uint32_t parts{};
|
||||
std::vector<Connection> connections;
|
||||
glm::mat4 additionalRotation{ 1.0f }; // Placement/AdditionalModelRotation
|
||||
};
|
||||
|
||||
// ModularBuildComponent.xml; nullopt when it has no topology
|
||||
std::optional<BuildInfo> ParseBuild(std::string_view xml);
|
||||
|
||||
// ModuleComponent.xml's connection offsets, by location name
|
||||
std::map<std::string, glm::vec3> ParseModuleConnections(std::string_view xml);
|
||||
|
||||
// The LOTs in a ugc_modular_build.ldf_config ("1:4713+1:4714+1:4715", also with ';' or ',' between)
|
||||
std::vector<uint32_t> ParseModuleLots(std::string_view ldf);
|
||||
|
||||
struct Module {
|
||||
uint32_t partCode{};
|
||||
NifFile::Model nif;
|
||||
std::map<std::string, glm::vec3> connections; // from ParseModuleConnections
|
||||
};
|
||||
|
||||
/**
|
||||
* The modules placed per the build's topology, starting at the root part: a connection puts the connecting part
|
||||
* where the parent's node of the location's name is (or the parent module's connection offset of that name when
|
||||
* its mesh has no such node), less where the connecting part's own node of that name is, if it has one.
|
||||
* Parts without a module are skipped; `warnings` says what was.
|
||||
*/
|
||||
UgcModel::Model Assemble(const BuildInfo& build, const std::vector<Module>& modules, std::string& warnings);
|
||||
}
|
||||
Reference in New Issue
Block a user