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

@@ -210,10 +210,10 @@ defaults. The table below goes through it step by step.
### Metal and glow (on by default, not how live looked)
Live's models, LU Toolbox's exports and the client's own builder (`LUNifBuilder_BK`, which writes only `S01_Opaque`
and `S01_Alpha`) all draw every brick with the LEGO shader, so metal colors look like grey plastic and glowing colors
like bright plastic. The UGC server gives them the client's metal and emissive shaders by default. Set the three
shader ids to 0 for live's look: off writes exactly the files it wrote before these settings existed (the same bytes,
tested).
and `S01_Alpha`) all draw every brick with the LEGO shader, so metal colors look like grey plastic, glowing colors
like bright plastic and glitter like plain transparent plastic. The UGC server gives them the client's metal,
emissive and animated UV shaders by default. Set the four shader ids to 0 (and `satin_colors` to `none`) for live's
look: off writes exactly the files it wrote before these settings existed (the same bytes, tested).
How the client picks the shader (checked in the 1.10.64 client; Ghidra bookmarks under "UGCShaders"): player models
(LOT 14, and 6662) have RenderComponent shader 100, mapShaders "Multishader" (gameValue 9999). For a downloaded model
@@ -232,16 +232,28 @@ all of its levels, so each look needs a group of its own.
| `metal_material_types` | `shinySteel` | Materials.xml `MaterialType`s that are metal (empty: the default; `none`: none). |
| `brushed_material_types` | `brushedSteel,matteSteel` | Materials.xml `MaterialType`s that are brushed steel. |
| `brushed_colors` | 298,300,1002,1004 (the drum lacquered colors) | LEGO color ids that are brushed steel whatever their type; they win over the metal and glow colors. Empty: the default; `none`: no colors. |
| `shader_glitter` | 21 | `S<id>_Glitter_Model` (opaque) and `S<id>_GlitterAlpha_Model` (transparent) for glitter colors: 21 is LEGO-AnimUV (gameValue 30), see Glitter below. |
| `glitter_material_types` | `glitter` | Materials.xml `MaterialType`s that are glitter. |
| `glitter_colors` | 114,117 | LEGO color ids that are glitter whatever their type (as `brushed_colors`). The default: the two colors LEGO's own color data (Studio's color categories, "Glitter Colors") files as glitter that the client's Materials.xml types `shinyPlastic` (114 Tr. Medium Reddish-Violet w. Glitter, 117 Transparent Glitter). |
| `glitter_size` | 1.6 | The glitter texture's tile, in model units (a stud is 0.8): the flecks' spacing, the same on every brick. |
| `glitter_density` | 50 | Flecks in one tile. |
| `glitter_speed` | 1 | How fast the flecks drift: a tile in U in 7 s and in V in 11 s at 1; 0 keeps them still (no controllers). |
| `satin_colors` | 360,362,363,364,365,366,367,376 | Satin (opal) colors, see Satin below. The default: LEGO's color data's "Satin Colors" category (the Transparent ... Opal colors). Empty: the default; `none`: off. |
| `satin_opacity` | 75 | Percent: the vertex alpha of transparent satin bricks, instead of `transparent_opacity` or the Materials.xml alpha. |
| `satin_whiten` | 20 | Percent: how far satin colors are moved towards white (in linear RGB, after the color variation). |
Which color has which look is data, not a list in the code (`UgcModel::LookOf`): glow is LU Toolbox's glow table
(`UgcPalette::Glow`: 50, 294, 329, 9000-9027), metal is LU Toolbox's metallic table (`UgcPalette::IsMetallic`) plus
the Materials.xml types above (the clients checked have 8 or 14 `shinySteel` colors, and 1 or 3 `glitter` ones, which stay
plastic, as does pearl: the client has no shader for them). Only opaque bricks get a look: a transparent glowing
color (294 with the brick database palette, alpha 150) stays in `S01_Alpha_Model`.
the Materials.xml types above (the clients checked have 8 or 14 `shinySteel` colors, and 1 or 3 `glitter` ones: 129,
341, 351), glitter is the `glitter` type plus `glitter_colors`. Pearl stays plastic (the client has no shader for it).
Only opaque bricks get metal and glow: a transparent glowing color (294 with the brick database palette, alpha 150)
stays in `S01_Alpha_Model`. Transparent bricks can be glitter (every glitter color the clients have is transparent:
341 and 351 have alpha 150, 129 is in `transparent_colors`).
What is written with a group on: per LOD, the opaque bricks are split by look before being divided at 65535 vertices,
and the .nif gets, in order, `S01_Opaque_Model`, `S88_Metal_Model`, `S89_Brushed_Model`, `S46_Glow_Model` and
`S01_Alpha_Model`, each only when it has triangles, and each with every LOD level (an empty `LOD_<n>` node where it has
and the .nif gets, in order, `S01_Opaque_Model`, `S88_Metal_Model`, `S89_Brushed_Model`, `S46_Glow_Model`,
`S21_Glitter_Model`, `S01_Alpha_Model` and `S21_GlitterAlpha_Model` (transparent glitter bricks, one shape per brick
like the other transparent ones, or one with `combine_transparent`), each only when it has triangles, and each with every LOD level (an empty `LOD_<n>` node where it has
none there), like the plastic groups. Metal shapes are like plastic ones (white material, no textures, the brick color
as vertex color with the lighting baked in). Glow shapes get a material of their own with emissive `glow_emissive`,
vertex alpha 1 (the shader's mask) and their plain color, not the baked one: the shader lights them itself, and the
@@ -259,6 +271,63 @@ brushed). This is an approximation of the game's environment maps. `NifFile::Sha
and draws metal as reflective (metalness 1, the view's environment) and glow unlit, and the zone views draw
LEGO-Emissive objects going to their vertex color by its alpha (metal there stays lit like the rest).
#### Glitter
The client has no glitter shader. LEGO-AnimUV (mapShaders 21, gameValue 30, `LEGOPPLighting.fx` and its `_low`,
`_noenv`, `_noenv_nospec` versions) is the LEGO lighting with the UVs multiplied by `TEXTRANSFORMBASE` (the base map's
texture transform) in the vertex shader. A shape with vertex colors and a base texture gets
`Technique_LEGOPPLightingVertColorTextured_AnimUV` (technique names set up at 0x010ac110), whose pixel shader
(`LEGOPPLighting_PS_VertColorTextured`) is `lerp(vertex color, texture rgb, texture alpha)`, then the LEGO lighting
(`LEGOPP_PixelCommon4`), alpha = vertex alpha times the fade. So a white texture with flecks in its alpha puts white
flecks on a brick that is otherwise lit as plastic, and moving the texture transform moves them.
What a glitter shape has, beside what plastic shapes have (white material, alpha, specular, vertex colors):
- A UV set: each vertex's position on the axis plane its normal faces most, divided by `glitter_size`
(`UgcGlitter::Uv`), so the flecks are as dense on every brick and every side.
- An `NiTexturingProperty` (one per file, shared by both glitter groups): apply mode decal (fixed function would do
what the shader does), 9 slots, the base map only: wrap S and T, trilinear, UV set 0, a texture transform
(translation 0, scale 1, Maya method, center 0.5).
- Its source, stored in the file as the client's own animated textures store theirs
(`res/mesh/env/env_ag_ocean-maelstrom.nif`, RenderComponent 14356): `NiSourceTexture` (use external 0, name
`ugc_glitter.dds`, pixel layout 6, mipmaps 2, alpha 3, static, persist render data) and
`NiPersistentSrcTextureRendererData`: RGBA 32 bit, channels blue, green, red, alpha, platform DX9, 128 x 128 with 8
mipmaps. RGB is white; the alpha is `glitter_density` soft dots (radius 1.2 to 2.2 px, peak 0.65 to 1) at places
from a fixed seed, wrapping at the edges (`UgcGlitter::FleckAlpha`), each mipmap the 2x2 mean of the one above.
- Two `NiTextureTransformController`s on the property (the property's controller, the first linking the second):
flags 0x48 (active, loop, app time), frequency 1, phase 0, start 0, stop the period, target the property,
base map, operation translate U and translate V, each with an `NiFloatInterpolator` and `NiFloatData` of two linear
keys (0, 0) and (period, 1): a tile in `7 / glitter_speed` s in U and `11 / glitter_speed` s in V, looping, and
wrapping makes the loop seamless. The block layouts are the ocean file's (its controllers are 39 bytes, the property
70). With `glitter_speed` 0 the property has no controllers.
The client finds the animation: `SetupRenderNodeExtraData` (0x00c746c0) sets `RenderNodeExtraData.flags0` bit 2 from
`NifHasAnimatedControllers` (0x00bf4160), which returns true for a shape whose `NiTexturingProperty`'s first
controller is an `NiTextureTransformController`. No node transform controllers are added (they would clear the
object's static flag).
Transparent glitter: every UGC shape has the same `NiAlphaProperty` (blend source alpha over one minus source alpha)
and transparent bricks are transparent by their vertex alpha; the LEGO-AnimUV techniques declare
`UsesNiRenderState = true` and their pixel shader outputs the vertex alpha, the same as the LEGO shader's that
`S01_Alpha_Model` is drawn with, so transparent glitter gets a group of its own. There is no shimmer:
LEGO-AnimUV's pixel shaders don't read the material's emissive (only the `_Emissive` ones do), so an
`NiMaterialColorController` would change nothing.
The icon draws the flecks where they are at the start (the same texture and UVs, before the light; `glitter_size`
and `glitter_density`), opaque and transparent. The UGC page's 3D view marks glitter meshes (`/api/ugc/mesh`: look
`GLITTER` 512, a mesh with a stored texture in a group tagged `shader_glitter` or 21) and draws moving flecks from
their UVs and `uvScroll` (what `NifFile` reads from the controllers); the property and zone views, which draw bricks
from the LXFML, draw them on the colors in `window.LDD_GLITTER` (`/api/bricks/materials.js`: the glitter colors by
the current settings) from their positions.
#### Satin
The client has no satin shader either: Clear Plastic (mapShaders 3) has no vertex color, so it can't show a colored
satin. Satin bricks stay in `S01_Alpha_Model` and are made to look satin when their colors are made: a transparent
brick of a `satin_colors` color gets `satin_opacity` as its vertex alpha, and its color (any brick's) is moved
`satin_whiten` percent towards white, milky. These colors are only in a client whose Materials.xml has them (the
opal colors, 360 to 376, are not in the 1.10.64 client's).
Modular builds (`ugc_modular_build` rows, `ldf_config` like `1:4713+1:4714+1:4715`):
1. Each module LOT's `ModuleComponent` (component type 28) gives its part code and build