mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Why the glitter never moved: player models (LOT 14) are wrapped in weeblewobble.kfm (RenderComponentWrapper 9845), so the client makes them an LWOSkinnedRenderComponent, whose Run (0x00d6d3d0) updates the scene graph (and so any NiTextureTransformController) only while animation is enabled, and LWOModelBehaviorComponent::EnableAnimation (0x00be2740) turns it off for modelType 2, which every placed property model is. The root flags 0x102 added earlier are only read by the base render component. Nothing in a placed model's .nif can move. What does move: shader classes set globals in their own per-frame Run. Distortion Directional (Ocean) (mapShaders 79, Run 0x010b90c0) slides its texture layers by fixed shares of a tile a second, as the game's own pond ripples (S79__pond_ripplesShape). Glitter bricks now get a sparkle group, S79_GlitterSparkle_Model: their triangles lifted 0.005 off the brick, vertex colors white tinted by the brick, UVs placed per brick, alpha tested (ShaderCommon's alpha test phase, GREATEREQUAL 127), with a stored texture of flat sparkles at alpha 230: one layer's sparkle alone averages under the test, two meeting pass, so sparkles flash and go out as the layers cross. The flecks stay (LEGO-AnimUV, now without the controllers and flags that never ran). The icon and the dashboard's 3D view leave the sparkles out. New settings: shader_glitter_sparkle (79, 0 off), glitter_sparkle_size, glitter_sparkle_amount, glitter_sparkle_tint, glitter_sparkle_brightness; glitter_speed is now how fast sparkles flash (the sparkle tile). Only glitter output changes; non-glitter models are byte-identical. Check in game: reprocess a property with glitter models, then look at them from a few angles and distances, on each graphics quality: - sparkles flash on and off all over the glitter bricks, continuously - no flickering fight between the sparkles and the brick surface - transparent glitter bricks still see-through, flecks still visible - nothing drawn where there is no glitter brick; icons unchanged apart from the flecks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
554 lines
25 KiB
C++
554 lines
25 KiB
C++
#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 "UgcGlitter.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());
|
|
}
|
|
if (!brickSeeds.empty() || !other.brickSeeds.empty()) {
|
|
brickSeeds.resize(base, 0);
|
|
if (other.brickSeeds.empty()) brickSeeds.resize(positions.size(), 0);
|
|
else brickSeeds.insert(brickSeeds.end(), other.brickSeeds.begin(), other.brickSeeds.end());
|
|
}
|
|
if (!uvs.empty() || !other.uvs.empty()) {
|
|
uvs.resize(base, glm::vec2(0.0f));
|
|
if (other.uvs.empty()) uvs.resize(positions.size(), glm::vec2(0.0f));
|
|
else uvs.insert(uvs.end(), other.uvs.begin(), other.uvs.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 brickSeed = UgcGlitter::BrickSeed(options.seed, brick);
|
|
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);
|
|
mesh.brickSeeds.push_back(brickSeed);
|
|
}
|
|
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, const std::set<int32_t>& overlayTags) {
|
|
Model model;
|
|
for (const auto& source : nif.meshes) {
|
|
if (source.material.alphaTest && overlayTags.contains(source.material.shaderTag)) continue;
|
|
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);
|
|
if (source.uvs.size() == count * 2) mesh.uvs.emplace_back(source.uvs[v * 2], source.uvs[v * 2 + 1]);
|
|
}
|
|
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]);
|
|
if (source < mesh.brickSeeds.size()) piece.brickSeeds.push_back(mesh.brickSeeds[source]);
|
|
if (source < mesh.uvs.size()) piece.uvs.push_back(mesh.uvs[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]);
|
|
if (source < mesh.brickSeeds.size()) kept.brickSeeds.push_back(mesh.brickSeeds[source]);
|
|
if (source < mesh.uvs.size()) kept.uvs.push_back(mesh.uvs[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]);
|
|
if (source < mesh.brickSeeds.size()) current.brickSeeds.push_back(mesh.brickSeeds[source]);
|
|
if (source < mesh.uvs.size()) current.uvs.push_back(mesh.uvs[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]);
|
|
if (source < mesh.brickSeeds.size()) half.brickSeeds.push_back(mesh.brickSeeds[source]);
|
|
if (source < mesh.uvs.size()) half.uvs.push_back(mesh.uvs[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;
|
|
}
|
|
}
|