fix(ugc): each glitter brick gets its own fleck pattern

Every glitter brick had the same flecks in the same places: the UVs were
the vertex positions projected on an axis plane, so bricks a whole tile
apart (and every brick of the same shape at the same spot in its own
model) looked identical. Each brick now has a number of its own
(UgcGlitter::BrickSeed, from the model's id and the brick's index, kept
per vertex in Mesh::brickSeeds) that turns the projection by an angle
and moves it by an offset under a tile, differently for each axis
plane. A model made again gets the same patterns; every LOD of a brick
the same one. The icon now draws the flecks on the UVs the .nif has
(Mesh::uvs read back by FromNif). New setting glitter_random (1; 0 puts
the same pattern on every brick as before). Non-glitter models are
byte-identical (hash tests unchanged).

Check in game: reprocess a model with several glitter bricks of the
same shape; the fleck patterns differ from brick to brick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 00:17:43 -05:00
parent 0a1e33d3d2
commit 63bfaf545e
11 changed files with 148 additions and 11 deletions

View File

@@ -10,6 +10,7 @@
#include <glm/gtc/matrix_transform.hpp>
#include "NifFile.h"
#include "UgcGlitter.h"
#include "UgcPalette.h"
#include "tinyxml2.h"
@@ -164,6 +165,16 @@ namespace UgcModel {
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);
}
@@ -232,6 +243,7 @@ namespace UgcModel {
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
@@ -306,6 +318,7 @@ namespace UgcModel {
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);
}
@@ -363,6 +376,7 @@ namespace UgcModel {
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
@@ -406,6 +420,8 @@ namespace UgcModel {
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);
}
@@ -428,6 +444,8 @@ namespace UgcModel {
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]);
}
@@ -461,6 +479,8 @@ namespace UgcModel {
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);
}
@@ -514,6 +534,8 @@ namespace UgcModel {
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]);
}