feat(ugc): glitter flecks (LEGO-AnimUV) and milky satin in made models

Glitter colors (Materials.xml type glitter, glitter_colors 114,117) go into
S21_Glitter_Model and, transparent, S21_GlitterAlpha_Model (shader_glitter,
default 21, LEGO-AnimUV). Their shapes get box-projected UVs, an
NiTexturingProperty with a stored 128 px mipmapped fleck texture
(NiSourceTexture + NiPersistentSrcTextureRendererData, as the client's own
env_ag_ocean-maelstrom.nif) and two NiTextureTransformControllers looping
the base map's translation (glitter_size, glitter_density, glitter_speed).
The shader lays the texture over the vertex color by its alpha and outputs
the vertex alpha, so transparent glitter blends as S01_Alpha does.

Satin colors (satin_colors, LEGO's opal colors) stay in S01_Alpha but get
satin_opacity and are whitened by satin_whiten.

NifFile reads the base map's scroll speed (uvScroll) from the controllers;
the icon draws still flecks, the UGC 3D view and the LXFML viewers moving
ones. stats.json counts the glitter groups. With shader_glitter 0 and no
satin colors the files are the same bytes as before (tested). Also keeps
glow_emissive for the icon (it was reset by the icon settings).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 12:53:52 -05:00
parent 9e4f2818c3
commit 7bba8344fc
24 changed files with 966 additions and 93 deletions

View File

@@ -223,7 +223,7 @@ namespace UgcModel {
Model model;
std::set<uint32_t> missing;
const bool luToolbox = options.palette == ePalette::LU_TOOLBOX;
bool anyGlow = false, anyLook = false;
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);
@@ -283,10 +283,16 @@ namespace UgcModel {
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);
const auto look = transparent ? eLook::PLASTIC : LookOf(colorId, library.GetMaterial(colorId), options.looks);
anyLook = anyLook || look != eLook::PLASTIC;
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++) {
@@ -298,10 +304,8 @@ namespace UgcModel {
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);
model.opaque.looks.push_back(look);
}
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);
}
@@ -309,6 +313,7 @@ namespace UgcModel {
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;
}
@@ -376,7 +381,7 @@ namespace UgcModel {
// 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); !seeThrough && look != tagLooks.end() && look->second != eLook::PLASTIC) {
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);