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

@@ -461,6 +461,11 @@ namespace UgcRender {
};
const glm::vec3 toCamera = glm::normalize(dir);
const glm::vec3 halfway = glm::normalize(light + toCamera);
bool anyGlitter = false;
for (const auto* mesh : { &model.opaque, &model.transparent }) {
anyGlitter = anyGlitter || std::find(mesh->looks.begin(), mesh->looks.end(), UgcModel::eLook::GLITTER) != mesh->looks.end();
}
const auto glitterAlpha = anyGlitter ? UgcGlitter::FleckAlpha(options.glitter.flecks) : std::vector<uint8_t>{};
const auto shade = [&](const UgcModel::Mesh& mesh, bool isOpaque, uint32_t i0, uint32_t i1, uint32_t i2, float w0, float w1, float w2) {
glm::vec3 normal(0.0f, 1.0f, 0.0f);
@@ -483,8 +488,13 @@ namespace UgcRender {
// The sun's highlight (Blinn-Phong), white, on top of the color
const float highlight = direct > 0.0f ? options.specular * options.sunStrength / 3.14159265f * sun * std::pow(std::max(0.0f, glm::dot(normal, halfway)), std::max(options.shininess, 1.0f)) : 0.0f;
const float exposure = std::max(options.exposure, 0.0f);
const auto look = isOpaque && mesh.looks.size() == mesh.positions.size() ? mesh.looks[i0] : UgcModel::eLook::PLASTIC;
if (look == UgcModel::eLook::PLASTIC) {
const auto look = mesh.looks.size() == mesh.positions.size() && (isOpaque || mesh.looks[i0] == UgcModel::eLook::GLITTER) ? mesh.looks[i0] : UgcModel::eLook::PLASTIC;
if (look == UgcModel::eLook::GLITTER) {
// LEGO-AnimUV: lerp(vertex color, the texture's white, its alpha), then lit as plastic
const float fleck = UgcGlitter::Sample(glitterAlpha, UgcGlitter::Uv(position, normal, options.glitter.tile));
base = glm::vec4(glm::mix(glm::vec3(base), glm::vec3(1.0f), fleck), base.a);
}
if (look == UgcModel::eLook::PLASTIC || look == UgcModel::eLook::GLITTER) {
return glm::vec4((ToLinear(base.r) * lighting + highlight) * exposure, (ToLinear(base.g) * lighting + highlight) * exposure,
(ToLinear(base.b) * lighting + highlight) * exposure, std::clamp(base.a, 0.0f, 1.0f));
}