fix(ugc): glitter sparkles that move on placed models

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>
This commit is contained in:
Aaron Kimbrell
2026-09-29 00:27:55 -05:00
parent 63bfaf545e
commit fb7850fb9f
19 changed files with 562 additions and 248 deletions

View File

@@ -106,7 +106,10 @@ namespace NifFile {
// Not a shader's: the UGC server's glitter groups (LEGO-AnimUV with the fleck texture it stores in the .nif,
// UgcGlitter), white flecks by the texture's alpha over the lit vertex color, moving with the texture. Set by
// the dashboard's UGC mesh route, not by ShaderLookFor.
GLITTER = 512
GLITTER = 512,
// Not a shader's: the UGC server's glitter sparkles (Distortion Directional with its sparkle texture, alpha
// tested, UgcGlitter), drawn over the glitter bricks. Set by the dashboard's UGC mesh route.
SPARKLE = 1024
};
// eShaderLook bits of a shader (mapShaders.gameValue); 0 for the usual lit look and for fixed function

View File

@@ -474,12 +474,17 @@ namespace {
c.Add(Text(UGC, "metal_material_types", "Metal material types", "Materials.xml MaterialTypes drawn as metal, comma separated (none: only LU Toolbox's metallic colors).", "shinySteel"));
c.Add(Text(UGC, "brushed_material_types", "Brushed steel material types", "Materials.xml MaterialTypes drawn as brushed steel, comma separated (none: no such colors).", "brushedSteel,matteSteel"));
c.Add(Text(UGC, "brushed_colors", "Brushed steel colors", "LEGO color ids drawn as brushed steel whatever their Materials.xml type, comma separated: by default the drum lacquered 298,300,1002,1004 (none: no colors).", "298,300,1002,1004"));
c.Add(Int(UGC, "shader_glitter", "Glitter shader", "mapShaders id for glitter colors, in S<id>_Glitter_Model and (transparent ones) S<id>_GlitterAlpha_Model: 21 is LEGO-AnimUV, which lays a white fleck texture stored in the model over the color and moves it. 0: off, they stay plastic." + notLive, "21", 0, 9999));
c.Add(Int(UGC, "shader_glitter", "Glitter shader", "mapShaders id for glitter colors, in S<id>_Glitter_Model and (transparent ones) S<id>_GlitterAlpha_Model: 21 is LEGO-AnimUV, which lays a white fleck texture stored in the model over the color (still: the client never updates a placed model; see the sparkle shader). 0: off, they stay plastic." + notLive, "21", 0, 9999));
c.Add(Text(UGC, "glitter_material_types", "Glitter material types", "Materials.xml MaterialTypes drawn as glitter, comma separated (none: only the glitter colors below).", "glitter"));
c.Add(Text(UGC, "glitter_colors", "Glitter colors", "LEGO color ids drawn as glitter whatever their Materials.xml type, comma separated: by default 114,117, which LEGO's color data calls glitter and the client's Materials.xml plain plastic (none: no colors).", "114,117"));
c.Add(Float(UGC, "glitter_size", "Glitter tile size", "The fleck texture's tile in model units (a stud is 0.8): how far apart the flecks are, the same on every brick.", "1.6", 0.1f, 100));
c.Add(Int(UGC, "glitter_density", "Glitter flecks", "Flecks in one tile of the glitter texture.", "50", 0, 2000));
c.Add(Float(UGC, "glitter_speed", "Glitter speed", "How fast the flecks drift: 1 moves them a tile in 7 s one way and 11 s the other; 0 keeps them still.", "1", 0, 100));
c.Add(Int(UGC, "shader_glitter_sparkle", "Glitter sparkle shader", "mapShaders id for the glitter bricks' sparkles, in S<id>_GlitterSparkle_Model over both glitter groups (only with the glitter shader on): 79 is Distortion Directional (Ocean), whose texture layers the client moves every frame on its own, so a sparkle flashes where two layers' sparkles meet. Nothing else moves on a placed model (the client never updates it). 0: no sparkles.", "79", 0, 9999));
c.Add(Float(UGC, "glitter_sparkle_size", "Glitter sparkle size", "A sparkle's diameter in model units (a stud is 0.8).", "0.1", 0.01f, 1));
c.Add(Float(UGC, "glitter_sparkle_amount", "Glitter sparkle amount", "Percent of each moving sparkle layer covered by sparkles; a sparkle shows where two meet, so about this share squared of a brick sparkles at once.", "5", 0, 50));
c.Add(Float(UGC, "glitter_speed", "Glitter sparkle speed", "How fast sparkles flash and go out: 1 is about half a second each (the sparkle texture is made bigger so the client's fixed layer motion crosses sparkles faster).", "1", 0.1f, 4));
c.Add(Float(UGC, "glitter_sparkle_tint", "Glitter sparkle tint", "Percent: how far the sparkles take their brick's color (0: white).", "30", 0, 100));
c.Add(Float(UGC, "glitter_sparkle_brightness", "Glitter sparkle brightness", "Percent: the sparkles' vertex color (the client lights them like its other surfaces).", "100", 0, 100));
c.Add(Bool(UGC, "glitter_random", "Glitter placed per brick", "Each glitter brick gets its own fleck pattern (turned and moved by a number of the brick's own, the same every time the model is made); off: the same pattern on every brick.", true));
c.Add(Text(UGC, "satin_colors", "Satin colors", "Satin (opal) color ids, comma separated: they stay transparent plastic (the client has no satin shader) but are made milky and less see-through. By default LEGO's satin colors 360,362,363,364,365,366,367,376 (none: off)." + notLive, "360,362,363,364,365,366,367,376"));
c.Add(Float(UGC, "satin_opacity", "Satin opacity", "Percent: the opacity of transparent satin bricks, instead of the transparent opacity.", "75", 0, 100));

View File

@@ -491,7 +491,7 @@ namespace UgcRoutes {
Route(eHTTPMethod::GET, "/api/ugc/mesh/:id", Perm("properties_view"),
"A player model's generated .nif converted for the 3D view (NifFile::Encode, as the scenery meshes, with each mesh's shader look; the glitter "
"groups' meshes have the GLITTER look, their UVs and uvScroll, and the texture name \"glitter\"). Query: ?lod=0 (most detailed) "
"groups' meshes have the GLITTER look, their UVs and the texture name \"glitter\"; the glitter sparkles the SPARKLE look and \"sparkle\"). Query: ?lod=0 (most detailed) "
"to 3, &version=current|previous, &ao=0 for the mesh before the lighting bake. The header adds triangles and vertices",
[](HTTPReply& reply, const HTTPContext& context) {
const auto id = PathId<LWOOBJID>(context.path, 3);
@@ -501,9 +501,11 @@ namespace UgcRoutes {
const bool baked = QueryValue(context.queryString, "ao") != "0";
const std::string file = std::string(previous ? "previous." : "") + (baked ? "model.nif" : "model.noao.nif");
const auto url = InternalUrl() + "/files/model/" + std::to_string(*id) + "/" + file;
// The glitter groups' tag: the setting's, and the client's LEGO-AnimUV (21) for models made with another
// The glitter groups' tag: the setting's, and the client's LEGO-AnimUV (21) for models made with another;
// the sparkles' likewise (Distortion Directional, 79), alpha tested
const auto glitterTag = GeneralUtils::TryParse<int32_t>(UgcSetting("shader_glitter").value_or("21")).value_or(21);
Workers::Reply(reply, context, false, [url, lod, glitterTag](HTTPReply& out) {
const auto sparkleTag = GeneralUtils::TryParse<int32_t>(UgcSetting("shader_glitter_sparkle").value_or("79")).value_or(79);
Workers::Reply(reply, context, false, [url, lod, glitterTag, sparkleTag](HTTPReply& out) {
const auto fetched = CachedGet(url);
if (fetched->status != 200) return ReplyError(out, *fetched, url);
std::string error;
@@ -513,6 +515,11 @@ namespace UgcRoutes {
std::vector<std::string> textures(model->meshes.size());
for (size_t i = 0; i < model->meshes.size(); i++) {
const auto& material = model->meshes[i].material;
if (material.embeddedTexture >= 0 && material.alphaTest && (material.shaderTag == sparkleTag || material.shaderTag == 79)) {
looks[i] |= NifFile::SPARKLE;
textures[i] = "sparkle";
continue;
}
if (material.embeddedTexture < 0 || (material.shaderTag != glitterTag && material.shaderTag != 21)) continue;
looks[i] |= NifFile::GLITTER;
textures[i] = "glitter";

View File

@@ -174,7 +174,7 @@ async function loadGeneratedModel(url) {
// ---- Viewer ----
const materialCache = new Map();
// The glitter colours' moving flecks (window.LDD_GLITTER, the UGC server's glitter settings), updated each frame
// The glitter colours' flecks (window.LDD_GLITTER, the UGC server's glitter settings)
const glitterMaterials = [];
function material(id) {
if (!materialCache.has(id)) {
@@ -192,9 +192,8 @@ function material(id) {
});
const glitter = window.LDD_GLITTER;
if (glitter && (glitter.colors || []).includes(Number(id))) {
// Moving as the game moves its fleck texture: a tile in U in 7 s and in V in 11 s at speed 1
const speed = glitter.speed || 0;
glitterMaterials.push(addGlitter(created, { coordinates: 'position', tile: glitter.tile || 1.6, flecks: glitter.flecks || 50, scroll: [speed / 7, speed / 11] }));
// Still, as the game draws them on a placed model
glitterMaterials.push(addGlitter(created, { coordinates: 'position', tile: glitter.tile || 1.6, flecks: glitter.flecks || 50 }));
}
materialCache.set(id, created);
}

View File

@@ -56,7 +56,7 @@ export function shaderOf(manifest, asset, mesh) {
// NifFile::eShaderLook bits
export const SHADER_LOOK = { UNLIT: 1, NO_TEXTURE: 2, NO_VERTEX_COLORS: 4, MATERIAL_COLOR: 8, TWO_LAYERS_BLENDED: 16, TWO_LAYERS_ADDED: 32, REFLECTIVE: 64, BRUSHED: 128, EMISSIVE: 256,
GLITTER: 512 };
GLITTER: 512, SPARKLE: 1024 };
/**
* Glitter for a three.js material (the UGC server's glitter colors, UgcGlitter): white flecks over the color before

View File

@@ -123,6 +123,8 @@ export function createNifViewer(container) {
let seeThrough = !!mesh.blend && mesh.alpha < 0.99;
if (mesh.blend && hasColors) for (let i = 3; i < mesh.colors.length && !seeThrough; i += 4) seeThrough = mesh.colors[i] < 250;
const look = mesh.look || 0;
// The glitter sparkles (drawn over the glitter bricks) aren't drawn here
if (look & SHADER_LOOK.SPARKLE) continue;
const metal = metalOf(look);
// Glow: the emissive shader's vertex color, unlit (its vertex alpha is the glow, not opacity)
const material = look & SHADER_LOOK.EMISSIVE

View File

@@ -118,12 +118,6 @@ namespace {
// NiAVObject flags as the game's own brick models (res/BrickModels/ndmade) have them: nodes 0x110, shapes 0x10
constexpr uint16_t NODE_FLAGS = 0x110;
constexpr uint16_t SHAPE_FLAGS = 0x10;
// Nodes and shapes with controllers under them, as the client's own animated files (the AG ocean): the client
// updates an object's scene graph every frame only when its root has the selective update bit (0x02;
// LWOBaseRenderComponent::Run 0x00d5d770, NiAVObject::GetSelectiveUpdate 0x00413050). Without it the model is
// updated once when it loads and its controllers never move.
constexpr uint16_t ANIMATED_NODE_FLAGS = 0x102;
constexpr uint16_t ANIMATED_SHAPE_FLAGS = 0x1A; // selective update, update property controllers, rigid
void WriteNet(Writer& out, int32_t name) {
out.I32(name);
@@ -144,9 +138,40 @@ namespace {
out.I32(-1); // collision object
}
// `glitter`: with a UV set projected for the glitter texture (UgcGlitter::Uv), placed by each vertex's brick
// (Mesh::brickSeeds) when the glitter is random
std::string TriShapeData(const UgcModel::Mesh& mesh, const UgcGlitter::Params* glitter = nullptr) {
// A glitter texture's UV set for `mesh` (UgcGlitter::Uv), placed by each vertex's brick (Mesh::brickSeeds) when
// the glitter is random; empty without normals
std::vector<glm::vec2> GlitterUvs(const UgcModel::Mesh& mesh, const UgcGlitter::Params& glitter, UgcGlitter::eLayer layer) {
std::vector<glm::vec2> uvs;
if (mesh.normals.size() != mesh.positions.size()) return uvs;
const float tile = layer == UgcGlitter::eLayer::SPARKLES ? glitter.SparkleTile() : glitter.tile;
uvs.reserve(mesh.positions.size());
for (size_t v = 0; v < mesh.positions.size(); v++) {
const uint32_t seed = glitter.random && v < mesh.brickSeeds.size() ? mesh.brickSeeds[v] : 0;
uvs.push_back(UgcGlitter::Uv(mesh.positions[v], mesh.normals[v], tile, seed, layer));
}
return uvs;
}
// The sparkles over a glitter mesh: the same triangles lifted off it along the normals (UgcGlitter::SPARKLE_LIFT),
// their vertex colors the sparkles' (UgcGlitter::SparkleColor)
UgcModel::Mesh SparkleMesh(const UgcModel::Mesh& mesh, const UgcGlitter::Params& glitter) {
UgcModel::Mesh out;
out.positions = mesh.positions;
out.normals = mesh.normals;
out.brickSeeds = mesh.brickSeeds;
out.indices = mesh.indices;
if (out.normals.size() == out.positions.size()) {
for (size_t v = 0; v < out.positions.size(); v++) out.positions[v] += out.normals[v] * UgcGlitter::SPARKLE_LIFT;
}
out.colors.reserve(mesh.positions.size());
for (size_t v = 0; v < mesh.positions.size(); v++) {
out.colors.push_back(UgcGlitter::SparkleColor(v < mesh.colors.size() ? mesh.colors[v] : glm::vec4(1.0f), glitter));
}
return out;
}
// `uvs`: a UV set (one per vertex), none when empty
std::string TriShapeData(const UgcModel::Mesh& mesh, const std::vector<glm::vec2>& uvSet = {}) {
Writer out;
const auto count = static_cast<uint16_t>(mesh.positions.size());
out.I32(0); // group ID
@@ -163,7 +188,7 @@ namespace {
max = glm::max(max, p);
}
const bool normals = mesh.normals.size() == mesh.positions.size();
const bool uvs = glitter && normals;
const bool uvs = !uvSet.empty() && uvSet.size() == mesh.positions.size();
out.U16(uvs ? 1 : 0); // data flags: the number of UV sets, no tangents
out.U8(normals ? 1 : 0);
if (normals) {
@@ -191,9 +216,7 @@ namespace {
}
}
if (uvs) {
for (size_t v = 0; v < mesh.positions.size(); v++) {
const uint32_t seed = glitter->random && v < mesh.brickSeeds.size() ? mesh.brickSeeds[v] : 0;
const auto uv = UgcGlitter::Uv(mesh.positions[v], mesh.normals[v], glitter->tile, seed);
for (const auto& uv : uvSet) {
out.Float(uv.x);
out.Float(uv.y);
}
@@ -244,53 +267,11 @@ namespace {
}
/**
* The glitter groups' NiTexturingProperty (made once a file), as the client's own animated textures have it
* (res/mesh/env/env_ag_ocean-maelstrom.nif): the base map wrapping, with a texture transform (Maya method,
* center 0.5), its source stored in the file, and NiTextureTransformControllers on the property looping the
* transform's translation (flags 0x48: active, looping, app time; frequency 1) through an NiFloatInterpolator
* and linear NiFloatData from 0 to 1 tile. The client's NifHasAnimatedControllers (0x00bf4160) finds the
* property's first controller and marks the object animated. Apply mode decal: what fixed function would do
* with it is what the shader does (the texture over the vertex color by its alpha, the vertex alpha kept).
* An NiSourceTexture stored in the file, as the client's own stored textures (res/mesh/env/env_ag_ocean-maelstrom.nif):
* white, `alpha` its mipmaps' alpha (UgcGlitter::Mipmaps, the first the full size), 32-bit (B, G, R, A),
* NiPersistentSrcTextureRendererData for DX9.
*/
int32_t GlitterTexturing(const UgcGlitter::Params& glitter) {
if (m_Glitter >= 0) return m_Glitter;
m_Glitter = m_Nif.Reserve("NiTexturingProperty");
std::vector<std::pair<uint32_t, float>> motions; // TexTransform (0 translate U, 1 translate V), seconds a tile
if (glitter.PeriodU() > 0.0f) motions.emplace_back(0, glitter.PeriodU());
if (glitter.PeriodV() > 0.0f) motions.emplace_back(1, glitter.PeriodV());
std::vector<int32_t> controllers;
for (size_t i = 0; i < motions.size(); i++) controllers.push_back(m_Nif.Reserve("NiTextureTransformController"));
for (size_t i = 0; i < motions.size(); i++) {
const auto [operation, period] = motions[i];
Writer data;
data.U32(2); // keys
data.U32(1); // linear
data.Float(0.0f);
data.Float(0.0f);
data.Float(period);
data.Float(1.0f);
const auto interpolator = m_Nif.Reserve("NiFloatInterpolator");
const auto dataBlock = m_Nif.Add("NiFloatData", std::move(data.Data()));
Writer value;
value.Float(0.0f);
value.I32(dataBlock);
m_Nif.Fill(interpolator, std::move(value.Data()));
Writer controller;
controller.I32(i + 1 < controllers.size() ? controllers[i + 1] : -1); // next controller
controller.U16(0x48); // active, loop, app time (as the client's files)
controller.Float(1.0f); // frequency
controller.Float(0.0f); // phase
controller.Float(0.0f); // start
controller.Float(period); // stop
controller.I32(m_Glitter); // target
controller.I32(interpolator);
controller.U8(0); // not a shader map
controller.U32(0); // the base map
controller.U32(operation);
m_Nif.Fill(controllers[i], std::move(controller.Data()));
}
// The texture: white, its alpha the flecks, mipmapped, 32-bit as the client's stored textures are (B, G, R, A)
int32_t StoredTexture(const std::string& name, const std::vector<std::vector<uint8_t>>& mipmaps) {
const auto source = m_Nif.Reserve("NiSourceTexture");
Writer pixels;
pixels.U32(1); // RGBA
@@ -307,14 +288,13 @@ namespace {
pixels.U8(0); // unsigned
}
pixels.I32(-1); // palette
const auto mipmaps = UgcGlitter::Mipmaps(UgcGlitter::FleckAlpha(glitter.flecks));
pixels.U32(static_cast<uint32_t>(mipmaps.size()));
pixels.U32(4); // bytes per pixel
uint32_t offset = 0;
const auto side = static_cast<uint32_t>(std::lround(std::sqrt(static_cast<double>(mipmaps.empty() ? 0 : mipmaps[0].size()))));
for (size_t level = 0; level < mipmaps.size(); level++) {
const uint32_t side = static_cast<uint32_t>(UgcGlitter::TEXTURE_SIZE) >> level;
pixels.U32(side);
pixels.U32(side);
pixels.U32(side >> level);
pixels.U32(side >> level);
pixels.U32(offset);
offset += static_cast<uint32_t>(mipmaps[level].size()) * 4;
}
@@ -334,7 +314,7 @@ namespace {
Writer texture;
WriteNet(texture, -1);
texture.U8(0); // stored in the file
texture.I32(m_Nif.String("ugc_glitter.dds"));
texture.I32(m_Nif.String(name));
texture.I32(pixelData);
texture.U32(6); // pixel layout: default
texture.U32(2); // mipmaps: default
@@ -343,35 +323,65 @@ namespace {
texture.U8(0); // direct render
texture.U8(1); // persist render data
m_Nif.Fill(source, std::move(texture.Data()));
return source;
}
/**
* An NiTexturingProperty with only a base map, `source`: wrapping in S and T, trilinear, UV set 0; with an
* identity texture transform (Maya method, center 0.5) when `transform` (what LEGO-AnimUV multiplies the UVs
* by, TEXTRANSFORMBASE). No controllers: nothing updates a placed player model, see UgcGlitter.h.
*/
int32_t Texturing(int32_t source, uint16_t applyMode, bool transform) {
Writer texturing;
texturing.I32(-1); // name
texturing.U32(0); // extra data
texturing.I32(controllers.empty() ? -1 : controllers[0]);
texturing.U16(1 << 1); // apply mode decal
texturing.I32(-1); // controller
texturing.U16(applyMode);
texturing.U32(9); // texture slots
texturing.U8(1); // base map
texturing.I32(source);
texturing.U16(0x3200); // wrap S and T, trilinear, UV set 0
texturing.U8(1); // texture transform
texturing.Float(0.0f); // translation
texturing.Float(0.0f);
texturing.Float(1.0f); // scale
texturing.Float(1.0f);
texturing.Float(0.0f); // rotation
texturing.U32(2); // Maya
texturing.Float(0.5f); // center
texturing.Float(0.5f);
texturing.U8(transform ? 1 : 0);
if (transform) {
texturing.Float(0.0f); // translation
texturing.Float(0.0f);
texturing.Float(1.0f); // scale
texturing.Float(1.0f);
texturing.Float(0.0f); // rotation
texturing.U32(2); // Maya
texturing.Float(0.5f); // center
texturing.Float(0.5f);
}
for (int slot = 1; slot < 9; slot++) texturing.U8(0); // dark, detail, gloss, glow, bump, normal, parallax, decal
texturing.U32(0); // shader maps
m_Nif.Fill(m_Glitter, std::move(texturing.Data()));
return m_Nif.Add("NiTexturingProperty", std::move(texturing.Data()));
}
// The glitter groups' NiTexturingProperty (made once a file): the fleck texture, apply mode decal (what fixed
// function would do with it is what LEGO-AnimUV does: the texture over the vertex color by its alpha, the
// vertex alpha kept), with the texture transform LEGO-AnimUV reads
int32_t GlitterTexturing(const UgcGlitter::Params& glitter) {
if (m_Glitter < 0) m_Glitter = Texturing(StoredTexture("ugc_glitter.dds", UgcGlitter::Mipmaps(UgcGlitter::FleckAlpha(glitter.flecks))), 1 << 1, true);
return m_Glitter;
}
// The sparkle group's NiTexturingProperty (made once a file): the sparkle texture (its first two mipmaps keeping
// the sparkles' alpha), apply mode replace and no transform, as the client's own Distortion Directional shapes
// (S79__pond_ripplesShape, res/mesh/env/env_won_gnar_croc_pondfx.nif)
int32_t SparkleTexturing(const UgcGlitter::Params& glitter) {
if (m_Sparkle < 0) m_Sparkle = Texturing(StoredTexture("ugc_sparkle.dds", UgcGlitter::Mipmaps(UgcGlitter::SparkleAlpha(glitter), 2)), 0, false);
return m_Sparkle;
}
enum class eKind : uint8_t { PLAIN, GLITTER, SPARKLE };
// An NiTriShape of `mesh` (-1 when it is empty or too big for the format); `emissive`: its material's
// emissive color, 0 for the shared material without one; `glitter`: with the glitter texture
int32_t Shape(const std::string& name, const UgcModel::Mesh* mesh, bool transparent, float emissive = 0.0f, const UgcGlitter::Params* glitter = nullptr) {
// emissive color, 0 for the shared material without one; `glitter`: with the glitter texture (GLITTER) or
// as sparkles over it (SPARKLE)
int32_t Shape(const std::string& name, const UgcModel::Mesh* mesh, bool transparent, float emissive = 0.0f, const UgcGlitter::Params* glitter = nullptr,
eKind kind = eKind::PLAIN) {
if (!mesh || mesh->Empty() || mesh->positions.size() > 65535 || mesh->TriangleCount() > 65535) return -1;
if (!glitter) kind = eKind::PLAIN;
// The properties every shape of the game's own brick models has, in their order: material, alpha (blending
// by the vertex alpha: 1 on opaque bricks), specular (off) and vertex colors
if (m_Alpha < 0) {
@@ -390,7 +400,7 @@ namespace {
// transparent brick is drawn solid, with blending off. The game's own brick models give their S01_Alpha
// shapes 0.9999, and so do we (made once a file, only when there is a transparent shape).
int32_t material = m_Material;
if (transparent) {
if (transparent && kind != eKind::SPARKLE) {
if (m_MaterialAlpha < 0) m_MaterialAlpha = m_Nif.Add("NiMaterialProperty", Material(0.0f, 0.9999f));
material = m_MaterialAlpha;
}
@@ -399,13 +409,31 @@ namespace {
if (added) it->second = m_Nif.Add("NiMaterialProperty", Material(emissive));
material = it->second;
}
std::vector<int32_t> properties{ material, m_Alpha, m_Specular, m_VertexColor };
if (glitter) properties.push_back(GlitterTexturing(*glitter));
int32_t alpha = m_Alpha;
if (kind == eKind::SPARKLE && m_AlphaTest < 0) {
// Alpha tested: ShaderCommon::GetAlphaFlags puts a shape whose NiAlphaProperty has the test bit (0x200) in
// the alpha test phase, whose own states test GREATEREQUAL 127 without blending (the flags and
// threshold here say the same for anything else reading the file)
Writer test;
WriteNet(test, -1);
test.U16(0x0200 | (6 << 10)); // test, GREATEREQUAL
test.U8(127);
m_AlphaTest = m_Nif.Add("NiAlphaProperty", std::move(test.Data()));
}
if (kind == eKind::SPARKLE) alpha = m_AlphaTest;
std::vector<int32_t> properties{ material, alpha, m_Specular, m_VertexColor };
if (kind == eKind::GLITTER) properties.push_back(GlitterTexturing(*glitter));
if (kind == eKind::SPARKLE) properties.push_back(SparkleTexturing(*glitter));
const auto shapeBlock = m_Nif.Reserve("NiTriShape");
const auto dataBlock = m_Nif.Add("NiTriShapeData", TriShapeData(*mesh, glitter));
std::string data;
if (kind == eKind::GLITTER) data = TriShapeData(*mesh, GlitterUvs(*mesh, *glitter, UgcGlitter::eLayer::FLECKS));
else if (kind == eKind::SPARKLE) {
const auto sparkles = SparkleMesh(*mesh, *glitter);
data = TriShapeData(sparkles, GlitterUvs(*mesh, *glitter, UgcGlitter::eLayer::SPARKLES));
} else data = TriShapeData(*mesh);
const auto dataBlock = m_Nif.Add("NiTriShapeData", std::move(data));
Writer tri;
const bool animated = glitter && (glitter->PeriodU() > 0.0f || glitter->PeriodV() > 0.0f);
WriteAv(tri, m_Nif.String(name), properties, animated ? ANIMATED_SHAPE_FLAGS : SHAPE_FLAGS);
WriteAv(tri, m_Nif.String(name), properties, SHAPE_FLAGS);
tri.I32(dataBlock);
tri.I32(-1); // skin instance
tri.U32(0); // materials
@@ -421,9 +449,11 @@ namespace {
int32_t m_MaterialAlpha{ -1 }; // transparent shapes' (alpha 0.9999)
int32_t m_VertexColor{ -1 };
int32_t m_Alpha{ -1 };
int32_t m_AlphaTest{ -1 }; // the sparkles' (alpha tested)
int32_t m_Specular{ -1 };
std::map<float, int32_t> m_Emissive; // emissive color -> its material
int32_t m_Glitter{ -1 }; // the glitter groups' NiTexturingProperty
int32_t m_Sparkle{ -1 }; // the sparkle group's
};
}
@@ -446,12 +476,9 @@ namespace UgcFormats {
const int32_t root = nif.Reserve("NiNode");
SharedProperties properties(nif);
std::vector<int32_t> groupBlocks;
bool anyAnimated = false;
for (const auto& group : groups) {
if (group.lods.empty()) continue;
const bool animated = group.glitter && (group.glitter->PeriodU() > 0.0f || group.glitter->PeriodV() > 0.0f);
anyAnimated = anyAnimated || animated;
const auto nodeFlags = animated ? ANIMATED_NODE_FLAGS : NODE_FLAGS;
const auto kind = group.sparkle ? SharedProperties::eKind::SPARKLE : group.glitter ? SharedProperties::eKind::GLITTER : SharedProperties::eKind::PLAIN;
const auto lodNode = nif.Reserve("NiLODNode");
std::vector<int32_t> levels;
Writer ranges;
@@ -461,16 +488,16 @@ namespace UgcFormats {
const auto level = nif.Reserve("NiNode");
std::vector<int32_t> shapes;
for (const auto* piece : lod.pieces) {
const auto block = properties.Shape(group.name, piece, group.transparent, group.emissive, group.glitter);
const auto block = properties.Shape(group.name, piece, group.transparent, group.emissive, group.glitter, kind);
if (block >= 0) shapes.push_back(block);
}
nif.Fill(level, NodeData(nif.String(lod.name), shapes, nodeFlags));
nif.Fill(level, NodeData(nif.String(lod.name), shapes));
levels.push_back(level);
ranges.Float(lod.nearDistance);
ranges.Float(lod.farDistance);
}
const auto rangeData = nif.Add("NiRangeLODData", std::move(ranges.Data()));
auto data = NodeData(nif.String(group.name), levels, nodeFlags);
auto data = NodeData(nif.String(group.name), levels);
Writer lod;
lod.Raw(data);
lod.U16(3); // switch flags: update only the active child, and controllers (as the game's own files)
@@ -479,7 +506,7 @@ namespace UgcFormats {
nif.Fill(lodNode, std::move(lod.Data()));
groupBlocks.push_back(lodNode);
}
nif.Fill(root, NodeData(nif.String(rootName), groupBlocks, anyAnimated ? ANIMATED_NODE_FLAGS : NODE_FLAGS));
nif.Fill(root, NodeData(nif.String(rootName), groupBlocks));
return nif.Finish(root);
}

View File

@@ -44,11 +44,13 @@ namespace UgcFormats {
// NiMaterialProperty's emissive color (grey) of the group's shapes; 0 the shared white material with none.
// The client's emissive shader (S46) lerps from the lit color to the vertex color by vertex alpha times its red.
float emissive{};
// The glitter of the UGC server's glitter groups (docs/UgcServer.md, "Metal and glow"): the shapes get UVs
// The glitter of the UGC server's glitter groups (docs/UgcServer.md, "Glitter"): the shapes get UVs
// (UgcGlitter::Uv) and an NiTexturingProperty whose base map is the fleck texture stored in the file
// (NiSourceTexture, NiPersistentSrcTextureRendererData), its texture transform's translation looped by an
// NiTextureTransformController for U and one for V. Null: none.
// (NiSourceTexture, NiPersistentSrcTextureRendererData). Null: none.
const UgcGlitter::Params* glitter{};
// With `glitter`: the group is the sparkles over its pieces (the glitter bricks) instead: each piece lifted off
// along its normals, the sparkles' vertex colors, UVs for the sparkle texture, alpha tested (UgcGlitter.h)
bool sparkle{};
};
/**

View File

@@ -4,6 +4,19 @@
#include <cmath>
namespace UgcGlitter {
namespace {
uint64_t SplitMix(uint64_t x) {
x += 0x9E3779B97F4A7C15ull;
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
return x ^ (x >> 31);
}
// 0..1 from 24 bits of a hash
float Unit(uint64_t bits) { return static_cast<float>(bits >> 40) / static_cast<float>(1ull << 24); }
// The side of a square texture's alpha
int Side(const std::vector<uint8_t>& alpha) { return static_cast<int>(std::lround(std::sqrt(static_cast<double>(alpha.size())))); }
}
std::vector<uint8_t> FleckAlpha(uint32_t flecks) {
constexpr int N = TEXTURE_SIZE;
std::vector<float> alpha(static_cast<size_t>(N) * N, 0.0f);
@@ -36,55 +49,89 @@ namespace UgcGlitter {
return out;
}
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha) {
float Params::SparkleTile() const {
return 75.0f * std::max(sparkleSize, 0.001f) * std::max(speed, 0.01f);
}
int Params::SparkleTextureSize() const {
// A sparkle 3 pixels wide: side = 3 * tile / size (225 at speed 1)
const float wanted = 3.0f * SparkleTile() / std::max(sparkleSize, 0.001f);
int side = 128;
while (side < 1024 && static_cast<float>(side) < wanted) side *= 2;
return side;
}
std::vector<uint8_t> SparkleAlpha(const Params& params) {
const int N = params.SparkleTextureSize();
std::vector<uint8_t> alpha(static_cast<size_t>(N) * N, 0);
const float radius = std::max(params.sparkleSize / params.SparkleTile() * static_cast<float>(N) * 0.5f, 0.75f);
const float share = std::clamp(params.sparkleAmount, 0.0f, 100.0f) / 100.0f;
const auto count = static_cast<uint32_t>(std::lround(share * static_cast<float>(N) * static_cast<float>(N) / (3.14159265f * radius * radius)));
uint64_t state = 0x737061726B6C6500ull;
const auto next = [&state] { return Unit(SplitMix(state++)); };
const int reach = static_cast<int>(std::ceil(radius + 0.5f));
for (uint32_t i = 0; i < count; i++) {
const float cx = next() * N, cy = next() * N;
for (int dy = -reach; dy <= reach; dy++) {
for (int dx = -reach; dx <= reach; dx++) {
const int x = static_cast<int>(std::floor(cx)) + dx, y = static_cast<int>(std::floor(cy)) + dy;
const float ddx = x + 0.5f - cx, ddy = y + 0.5f - cy;
// Flat, with a pixel's worth of edge
const float cover = std::clamp(radius + 0.5f - std::sqrt(ddx * ddx + ddy * ddy), 0.0f, 1.0f);
if (cover <= 0.0f) continue;
auto& value = alpha[static_cast<size_t>(((y % N) + N) % N) * N + ((x % N) + N) % N];
value = std::max(value, static_cast<uint8_t>(std::lround(cover * SPARKLE_ALPHA)));
}
}
}
return alpha;
}
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha, int keepPeaks) {
std::vector<std::vector<uint8_t>> levels{ alpha };
for (int size = TEXTURE_SIZE / 2; size >= 1; size /= 2) {
for (int size = Side(alpha) / 2, level = 0; size >= 1; size /= 2, level++) {
const auto& above = levels.back();
const int from = size * 2;
std::vector<uint8_t> level(static_cast<size_t>(size) * size);
std::vector<uint8_t> next(static_cast<size_t>(size) * size);
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
const auto at = [&](int dx, int dy) { return static_cast<int>(above[static_cast<size_t>(y * 2 + dy) * from + x * 2 + dx]); };
level[static_cast<size_t>(y) * size + x] = static_cast<uint8_t>((at(0, 0) + at(1, 0) + at(0, 1) + at(1, 1) + 2) / 4);
next[static_cast<size_t>(y) * size + x] = static_cast<uint8_t>(level < keepPeaks ? std::max({ at(0, 0), at(1, 0), at(0, 1), at(1, 1) }) :
(at(0, 0) + at(1, 0) + at(0, 1) + at(1, 1) + 2) / 4);
}
}
levels.push_back(std::move(level));
levels.push_back(std::move(next));
}
return levels;
}
namespace {
uint64_t SplitMix(uint64_t x) {
x += 0x9E3779B97F4A7C15ull;
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
return x ^ (x >> 31);
}
// 0..1 from 24 bits of a hash
float Unit(uint64_t bits) { return static_cast<float>(bits >> 40) / static_cast<float>(1ull << 24); }
}
uint32_t BrickSeed(uint64_t modelSeed, uint32_t brick) {
const auto hash = SplitMix(SplitMix(modelSeed ^ 0x676C6974746572ull) + brick);
return static_cast<uint32_t>(hash >> 32) | 1u;
}
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed) {
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed, eLayer layer) {
const auto a = glm::abs(normal);
const float scale = 1.0f / std::max(tile, 1e-3f);
const int plane = a.x >= a.y && a.x >= a.z ? 0 : a.y >= a.z ? 1 : 2;
const glm::vec2 uv = (plane == 0 ? glm::vec2(position.z, position.y) : plane == 1 ? glm::vec2(position.x, position.z) : glm::vec2(position.x, position.y)) * scale;
if (seed == 0) return uv;
const auto hash = SplitMix((static_cast<uint64_t>(seed) << 2) | static_cast<uint64_t>(plane));
const auto hash = SplitMix(((static_cast<uint64_t>(seed) << 2) | static_cast<uint64_t>(plane)) ^ (static_cast<uint64_t>(layer) << 40));
const float angle = Unit(hash) * 6.28318530718f;
const glm::vec2 offset(Unit(SplitMix(hash)), Unit(SplitMix(hash + 1)));
const float c = std::cos(angle), s = std::sin(angle);
return glm::vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y) + offset;
}
glm::vec4 SparkleColor(const glm::vec4& brickColor, const Params& params) {
const float tint = std::clamp(params.sparkleTint, 0.0f, 100.0f) / 100.0f;
const float brightness = std::clamp(params.sparkleBrightness, 0.0f, 100.0f) / 100.0f;
return glm::vec4(glm::clamp(glm::mix(glm::vec3(1.0f), glm::vec3(brickColor), tint) * brightness, 0.0f, 1.0f), 1.0f);
}
float Sample(const std::vector<uint8_t>& alpha, const glm::vec2& uv) {
constexpr int N = TEXTURE_SIZE;
if (alpha.size() != static_cast<size_t>(N) * N) return 0.0f;
const int N = Side(alpha);
if (N == 0 || alpha.size() != static_cast<size_t>(N) * N) return 0.0f;
const float x = (uv.x - std::floor(uv.x)) * N - 0.5f, y = (uv.y - std::floor(uv.y)) * N - 0.5f;
const int x0 = static_cast<int>(std::floor(x)), y0 = static_cast<int>(std::floor(y));
const float fx = x - x0, fy = y - y0;

View File

@@ -6,43 +6,81 @@
#include <glm/glm.hpp>
/**
* The glitter the UGC server gives glitter colors (docs/UgcServer.md, "Metal and glow"): a tileable texture of white
* flecks (its alpha) laid over the brick's color by the client's LEGO-AnimUV shader (lerp(vertex color, texture,
* texture alpha), then the LEGO lighting), on UVs projected from the model's own coordinates so every brick gets the
* same density, turned and moved by a number of each brick's own (BrickSeed) so no two bricks have the same pattern,
* drifting as the texture transform's translation loops. Pure.
* The glitter the UGC server gives glitter colors (docs/UgcServer.md, "Glitter"). Pure.
*
* Flecks: a tileable texture of white flecks (its alpha) laid over the brick's color by the client's LEGO-AnimUV shader
* (lerp(vertex color, texture, texture alpha), then the LEGO lighting). They stay still: a placed player model is never
* updated after it loads (LWOSkinnedRenderComponent::Run with animation off for modelType 2), so texture controllers in
* its .nif never run.
*
* Sparkles: a second shape over each glitter brick drawn by the client's Distortion Directional (Ocean) shader, whose
* texture layers the client moves every frame whatever the object does (the shader's Run sets
* g_vDirectionalMotionLayer1..3). Its texture has flat sparkles at an alpha that one layer's alone keeps under the
* alpha test, so a sparkle shows only where two moving layers' sparkles meet: points that flash and go out.
*
* Both are placed on each brick by UVs projected from the model's own coordinates, turned and moved by a number of the
* brick's own (BrickSeed), so no two bricks have the same pattern and a model made again has the same one.
*/
namespace UgcGlitter {
// The texture's side in pixels (a power of two, mipmapped down to 1)
// The fleck texture's side in pixels (a power of two, mipmapped down to 1)
constexpr int TEXTURE_SIZE = 128;
struct Params {
float tile{ 1.6f }; // glitter_size: the texture's side in model units (LDD units: a stud is 0.8)
// Flecks (LEGO-AnimUV)
float tile{ 1.6f }; // glitter_size: the fleck texture's side in model units (LDD units: a stud is 0.8)
uint32_t flecks{ 50 }; // glitter_density: flecks in one tile
float speed{ 1.0f }; // glitter_speed: 1 moves the flecks a tile in U in 7 s and in V in 11 s; 0 keeps them still
bool random{ true }; // glitter_random: each brick its own pattern (BrickSeed), else the same on every brick
// Sparkles (Distortion Directional)
float sparkleSize{ 0.1f }; // glitter_sparkle_size: a sparkle's diameter in model units
float sparkleAmount{ 5.0f }; // glitter_sparkle_amount: percent of each moving layer covered by sparkles
float speed{ 1.0f }; // glitter_speed: how fast sparkles flash and go out (1: about half a second)
float sparkleTint{ 30.0f }; // glitter_sparkle_tint: percent, how far sparkles take their brick's color
float sparkleBrightness{ 100.0f }; // glitter_sparkle_brightness: percent, the sparkles' vertex color
// Seconds the texture's translation takes to go one tile in U and in V (0: no animation)
float PeriodU() const { return speed > 0.0f ? 7.0f / speed : 0.0f; }
float PeriodV() const { return speed > 0.0f ? 11.0f / speed : 0.0f; }
// The sparkle texture's side in model units. The client moves its layers a fixed share of a tile a second (a
// tile in 24, 48 and 72 s), so the tile sets how fast they cross: 75 sparkle sizes times the speed.
float SparkleTile() const;
// The sparkle texture's side in pixels: the power of two (128 to 1024) that makes a sparkle 3 pixels wide
int SparkleTextureSize() const;
bool operator==(const Params&) const = default;
};
// The texture's alpha (TEXTURE_SIZE squared, rows top to bottom): `flecks` soft dots at the same places every time,
// wrapping around the edges so the texture tiles. Its color is white.
// The sparkles' alpha in their texture. The alpha test of the client's alpha test phase keeps what reaches 127
// (ShaderCommon__SetupPhaseRenderStates: GREATEREQUAL 0x7f); the Directional shader averages 2 layers or 3 (by the
// graphics settings), so one sparkle alone is 115 or 77 and two meeting are 230 or 153
constexpr uint8_t SPARKLE_ALPHA = 230;
// How far the sparkle shapes stand off their brick, along its normals (model units): in front of its surface, so
// the brick (drawn after them when it is transparent) doesn't cover them and they don't fight it for the depth
constexpr float SPARKLE_LIFT = 0.005f;
// The fleck texture's alpha (TEXTURE_SIZE squared, rows top to bottom): `flecks` soft dots at the same places every
// time, wrapping around the edges so the texture tiles. Its color is white.
std::vector<uint8_t> FleckAlpha(uint32_t flecks);
// The texture's mipmaps' alpha, from TEXTURE_SIZE down to 1 (each the mean of 2x2 of the one before)
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha);
// The sparkle texture's alpha (SparkleTextureSize() squared): flat discs of sparkleSize at SPARKLE_ALPHA covering
// sparkleAmount percent of it, at the same places every time, tiling. Its color is white.
std::vector<uint8_t> SparkleAlpha(const Params& params);
// A square texture's mipmaps' alpha, from its own size down to 1: each the mean of 2x2 of the one before, or for
// the first `keepPeaks` the brightest of them (so sparkles keep their alpha at the next few distances)
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha, int keepPeaks = 0);
// A brick's number for placing its glitter (never 0), from the model's seed and the brick's index: the same for
// the brick in every LOD and every time the model is made
uint32_t BrickSeed(uint64_t modelSeed, uint32_t brick);
// A vertex's UV: its position on the axis plane its normal faces most, in tiles, turned by an angle and moved by
// an offset (under a tile) that `seed` (the brick's BrickSeed) picks for each plane; seed 0 leaves it as it is
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed = 0);
// Which texture a UV set is for: a brick's sparkles are placed apart from its flecks
enum class eLayer : uint8_t { FLECKS = 0, SPARKLES };
// The texture's alpha (0..1) at `uv` (wrapping, bilinear)
// A vertex's UV: its position on the axis plane its normal faces most, in tiles, turned by an angle and moved by
// an offset (under a tile) that `seed` (the brick's BrickSeed) picks for each plane and layer; seed 0 leaves it
// as it is
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed = 0, eLayer layer = eLayer::FLECKS);
// A sparkle's vertex color: white taking `sparkleTint` percent of its brick's color (sRGB), at `sparkleBrightness`,
// alpha 1
glm::vec4 SparkleColor(const glm::vec4& brickColor, const Params& params);
// A square texture's alpha (0..1) at `uv` (wrapping, bilinear)
float Sample(const std::vector<uint8_t>& alpha, const glm::vec2& uv);
}

View File

@@ -363,9 +363,10 @@ namespace UgcModel {
return ranges;
}
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks) {
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;

View File

@@ -145,8 +145,9 @@ namespace UgcModel {
// A client .nif's meshes as one model (vertex colors times material color; transparent when blended). `tagLooks`:
// the look of the opaque shapes whose multishader tag (NifFile::ShaderTag, a mapShaders id) is listed, and of the
// transparent ones when it is GLITTER
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks = {});
// transparent ones when it is GLITTER. `overlayTags`: alpha tested shapes with these tags are left out (the UGC
// server's glitter sparkles, drawn over the glitter bricks)
Model FromNif(const NifFile::Model& nif, const std::map<int32_t, eLook>& tagLooks = {}, const std::set<int32_t>& overlayTags = {});
/**
* The mesh's triangles by look ([eLook] -> its triangles; a triangle's look is its first vertex's), the looks not

View File

@@ -128,6 +128,17 @@ namespace UgcJobs {
return looks;
}
std::set<int32_t> Shaders::OverlayTags() const {
std::set<int32_t> tags{ 79 };
if (sparkle != 0) tags.insert(static_cast<int32_t>(sparkle));
return tags;
}
std::string SparkleName(const Settings& settings) {
const auto tag = std::to_string(settings.shaders.sparkle);
return ("S" + std::string(tag.size() < 2 ? "0" : "") + tag + "_GlitterSparkle_Model").substr(0, 60);
}
std::string ShapeName(const Settings& settings, UgcModel::eLook look, bool transparent) {
if (transparent && look != UgcModel::eLook::GLITTER) return "S01_Alpha_Model";
if (look == UgcModel::eLook::PLASTIC) return ("S" + settings.shaderOpaque + "_Opaque_Model").substr(0, 60);
@@ -138,10 +149,10 @@ namespace UgcJobs {
}
bool IconFromNif(const std::string& nif, const UgcRender::IconOptions& options, UgcStorage::Files& files, std::string& error,
const std::map<int32_t, UgcModel::eLook>& tagLooks) {
const std::map<int32_t, UgcModel::eLook>& tagLooks, const std::set<int32_t>& overlayTags) {
const auto readBack = NifFile::Parse(nif, 0, error);
if (!readBack) return false;
AddIcon(files, UgcModel::FromNif(*readBack, tagLooks), options);
AddIcon(files, UgcModel::FromNif(*readBack, tagLooks, overlayTags), options);
return true;
}
@@ -189,6 +200,8 @@ namespace UgcJobs {
std::array<bool, UgcModel::LOOK_COUNT> separate{};
for (size_t look = 1; look < UgcModel::LOOK_COUNT; look++) separate[look] = settings.shaders.TagOf(static_cast<UgcModel::eLook>(look)) != 0;
const bool glowApart = separate[static_cast<size_t>(UgcModel::eLook::GLOW)];
// The glitter bricks' sparkles, a group over both glitter groups
const bool sparkles = separate[static_cast<size_t>(UgcModel::eLook::GLITTER)] && settings.shaders.sparkle != 0;
UgcModel::Model preview; // LOD 0 before the lighting bake, for the dashboard
for (size_t i = 0; i < lods.size(); i++) {
auto options = settings.build;
@@ -251,6 +264,12 @@ namespace UgcJobs {
for (const auto& piece : transparentPieces.back()[kind]) triangles += piece.TriangleCount();
if (triangles > 0) byGroup[ShapeName(settings, kind ? UgcModel::eLook::GLITTER : UgcModel::eLook::PLASTIC, true)] = triangles;
}
if (sparkles) {
size_t triangles = 0;
for (const auto& piece : opaquePieces.back()[static_cast<size_t>(UgcModel::eLook::GLITTER)]) triangles += piece.TriangleCount();
for (const auto& piece : transparentPieces.back()[1]) triangles += piece.TriangleCount();
if (triangles > 0) byGroup[SparkleName(settings)] = triangles;
}
}
lodStats.push_back(entry);
}
@@ -277,6 +296,21 @@ namespace UgcJobs {
}
if (any) out.push_back(std::move(group));
}
// Last, over everything: the sparkles over the opaque and the transparent glitter bricks
if (sparkles) {
UgcFormats::NifLodGroup group{ SparkleName(settings), false, {} };
group.glitter = &settings.shaders.glitterParams;
group.sparkle = true;
bool any = false;
for (size_t i = 0; i < levels; i++) {
UgcFormats::NifLod lod{ ranges[i].first, ranges[i].second, "LOD_" + std::to_string(lods[i]), {} };
for (const auto& piece : opaque[i][static_cast<size_t>(UgcModel::eLook::GLITTER)]) lod.pieces.push_back(&piece);
for (const auto& piece : transparent[i][1]) lod.pieces.push_back(&piece);
any = any || !lod.pieces.empty();
group.lods.push_back(std::move(lod));
}
if (any) out.push_back(std::move(group));
}
return out;
};
const auto nif = UgcFormats::WriteLodNif("SceneNode_Model", groups(lods.size(), opaquePieces, transparentPieces));
@@ -295,7 +329,7 @@ namespace UgcJobs {
auto iconOptions = settings.icon;
UgcIconParams::Apply(iconOptions, iconValues);
std::string nifError;
if (!IconFromNif(nif, iconOptions, outcome.files, nifError, settings.shaders.TagLooks())) {
if (!IconFromNif(nif, iconOptions, outcome.files, nifError, settings.shaders.TagLooks(), settings.shaders.OverlayTags())) {
outcome.error = "the .nif made can't be read back for the icon: " + nifError;
return outcome;
}

View File

@@ -4,6 +4,7 @@
#include <filesystem>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>
@@ -33,8 +34,11 @@ namespace UgcJobs {
uint32_t brushed{}; // shader_brushed: 89 Brushed Steel
uint32_t glow{}; // shader_glow: 46 LEGO-Emissive
uint32_t glitter{}; // shader_glitter: 21 LEGO-AnimUV (opaque and transparent glitter, each a group)
// shader_glitter_sparkle: 79 Distortion Directional (Ocean), the glitter bricks' sparkles, a group over both
// glitter groups (so only with shader_glitter)
uint32_t sparkle{};
float glowEmissive{ 1.0f }; // glow_emissive: the glow shapes' NiMaterialProperty emissive (how much the vertex color shows unlit)
UgcGlitter::Params glitterParams; // glitter_size, glitter_density, glitter_speed
UgcGlitter::Params glitterParams; // glitter_* (flecks and sparkles)
// The mapShaders id of a look's group, 0 for the plastic S01_Opaque_Model
uint32_t TagOf(UgcModel::eLook look) const;
@@ -42,6 +46,9 @@ namespace UgcJobs {
// client's Polished Metal (88), Brushed Steel (89), LEGO-Emissive (46) and LEGO-AnimUV (21) for .nifs made with
// other settings
std::map<int32_t, UgcModel::eLook> TagLooks() const;
// The tags of groups drawn over others (the sparkles: this setting's id and the client's 79), which the icon
// leaves out (UgcModel::FromNif)
std::set<int32_t> OverlayTags() const;
};
struct Settings {
@@ -81,14 +88,18 @@ namespace UgcJobs {
// A model's icon files (icon.png, icon.dds download) drawn from its .nif (LOD 0), its metal and glow groups by
// `tagLooks` (Shaders::TagLooks); false (and `error`) when the .nif can't be read
// (the groups drawn over others, `overlayTags` (Shaders::OverlayTags), left out)
bool IconFromNif(const std::string& nif, const UgcRender::IconOptions& options, UgcStorage::Files& files, std::string& error,
const std::map<int32_t, UgcModel::eLook>& tagLooks = {});
const std::map<int32_t, UgcModel::eLook>& tagLooks = {}, const std::set<int32_t>& overlayTags = {});
// The name of a group of shapes (its NiLODNode and shapes): S01_Opaque_Model, S01_Alpha_Model, S88_Metal_Model,
// S89_Brushed_Model, S46_Glow_Model, S21_Glitter_Model and S21_GlitterAlpha_Model (transparent glitter; the ids from
// the settings), at most 60 characters as LU Toolbox cuts them
std::string ShapeName(const Settings& settings, UgcModel::eLook look, bool transparent);
// The name of the glitter sparkles' group, S79_GlitterSparkle_Model (the id from the settings)
std::string SparkleName(const Settings& settings);
// How many bricks (parts) an LXFML has, counted cheaply (for the memory estimate before a job starts)
size_t CountParts(std::string_view lxfml);

View File

@@ -242,7 +242,7 @@ void UgcProcessor::Worker() {
const auto nif = m_Storage.ReadNif(Kind::MODEL, job.id, "model.nif");
auto options = settings.icon;
UgcIconParams::Apply(options, job.iconValues);
outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, outcome.files, outcome.error, settings.shaders.TagLooks());
outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, outcome.files, outcome.error, settings.shaders.TagLooks(), settings.shaders.OverlayTags());
if (!nif) outcome.error = "the model has no stored .nif yet";
}
if (outcome.ok && outcome.files.contains("assembly.nif")) {
@@ -289,7 +289,7 @@ void UgcProcessor::Worker() {
const auto nif = m_Storage.ReadNif(Kind::MODEL, job.id, "model.nif");
auto options = settings.icon;
UgcIconParams::Apply(options, job.iconValues);
done.outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, done.outcome.files, done.outcome.error, settings.shaders.TagLooks());
done.outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, done.outcome.files, done.outcome.error, settings.shaders.TagLooks(), settings.shaders.OverlayTags());
if (!nif) done.outcome.error = "no stored .nif";
} else {
done.outcome = job.kind == Kind::MODEL

View File

@@ -117,11 +117,17 @@ namespace {
settings.shaders.brushed = std::min(Setting<uint32_t>("shader_brushed", 89), 9999u);
settings.shaders.glow = std::min(Setting<uint32_t>("shader_glow", 46), 9999u);
settings.shaders.glowEmissive = std::clamp(Setting<float>("glow_emissive", 1.0f), 0.0f, 10.0f);
// Glitter colors in S<id>_Glitter_Model and S<id>_GlitterAlpha_Model with drifting flecks (LEGO-AnimUV)
// Glitter colors in S<id>_Glitter_Model and S<id>_GlitterAlpha_Model with flecks (LEGO-AnimUV), and sparkles
// over them in S<id>_GlitterSparkle_Model (Distortion Directional, which moves on its own)
settings.shaders.glitter = std::min(Setting<uint32_t>("shader_glitter", 21), 9999u);
settings.shaders.sparkle = std::min(Setting<uint32_t>("shader_glitter_sparkle", 79), 9999u);
settings.shaders.glitterParams.tile = std::clamp(Setting<float>("glitter_size", 1.6f), 0.1f, 100.0f);
settings.shaders.glitterParams.flecks = std::min(Setting<uint32_t>("glitter_density", 50), 2000u);
settings.shaders.glitterParams.speed = std::clamp(Setting<float>("glitter_speed", 1.0f), 0.0f, 100.0f);
settings.shaders.glitterParams.sparkleSize = std::clamp(Setting<float>("glitter_sparkle_size", 0.1f), 0.01f, 1.0f);
settings.shaders.glitterParams.sparkleAmount = std::clamp(Setting<float>("glitter_sparkle_amount", 5.0f), 0.0f, 50.0f);
settings.shaders.glitterParams.speed = std::clamp(Setting<float>("glitter_speed", 1.0f), 0.1f, 4.0f);
settings.shaders.glitterParams.sparkleTint = std::clamp(Setting<float>("glitter_sparkle_tint", 30.0f), 0.0f, 100.0f);
settings.shaders.glitterParams.sparkleBrightness = std::clamp(Setting<float>("glitter_sparkle_brightness", 100.0f), 0.0f, 100.0f);
settings.shaders.glitterParams.random = Setting<int32_t>("glitter_random", 1) != 0;
// Which Materials.xml MaterialTypes are metal, brushed steel and glitter
for (const auto& [key, look] : { std::pair{ "metal_material_types", UgcModel::eLook::METAL }, std::pair{ "brushed_material_types", UgcModel::eLook::BRUSHED },

View File

@@ -305,7 +305,12 @@ all of its levels, so each look needs a group of its own.
| `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). |
| `shader_glitter_sparkle` | 79 | `S<id>_GlitterSparkle_Model`, the glitter bricks' sparkles (only with `shader_glitter`): 79 is Distortion Directional (Ocean) (gameValue 89), whose layers the client moves on its own; 0: no sparkles. See Glitter below. |
| `glitter_sparkle_size` | 0.1 | A sparkle's diameter in model units. |
| `glitter_sparkle_amount` | 5 | Percent of each moving layer covered by sparkles (about its square's share of a brick sparkles at once). |
| `glitter_speed` | 1 | How fast sparkles flash and go out (0.1 to 4; 1: about half a second each): the sparkle tile is 75 sparkle sizes times it. It used to be how fast the flecks drift, which never showed in game. |
| `glitter_sparkle_tint` | 30 | Percent of the brick's color the sparkles take (0: white). |
| `glitter_sparkle_brightness` | 100 | Percent: the sparkles' vertex color. |
| `glitter_random` | 1 | Each glitter brick its own fleck pattern (turned and moved by the brick); 0: the same pattern on every brick. |
| `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. |
@@ -342,15 +347,39 @@ LEGO-Emissive objects going to their vertex color by its alpha (metal there stay
#### 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
Glitter is two layers: still flecks in the brick (LEGO-AnimUV) and sparkles over it that flash and go out
(Distortion Directional). The client has no glitter shader, and nothing in a placed model's .nif can move:
**Why a placed model never animates** (checked in the 1.10.64 client; Ghidra comments at the addresses). Player models
(LOT 14) have `RenderComponentWrapper` 9845 (`animations\pets\weeble\weeblewobble.kfm`), so
`ObjectLoader2::LoadRenderComponent` (0x010536b0) always makes them an `LWOSkinnedRenderComponent` with the UGC .nif as
the wrapped node. Its per-frame `Run` (0x00d6d3d0) calls `NiAVObject::Update` (the only update of the object's scene
graph, and of its property controllers) only when the position changed or `ShouldAnimate` (0x00bd3860) is true, which
needs `animationEnabled`. `LWOModelBehaviorComponent::EnableAnimation` (0x00be2740, on render ready and whenever the
serialized model type changes) sends `SetAnimationEnabled(modelType != 2)`, and every placed property model is
modelType 2 (`ModelComponent::Serialize` writes 2, as live did). So an `NiTextureTransformController` in the file
never runs, whatever the node flags (`LWOBaseRenderComponent::Run`'s selective update check is not used for these
objects). Glitter made with texture controllers (and root flags 0x102) before this was still in game.
What does move on its own are shader globals that a shader class's own `Run` sets every frame for all its objects:
Distortion Directional (Ocean) (mapShaders 79, gameValue 89, class at vtable 0x015695a0, `Run` 0x010b90c0) adds
`dt/4/6`, `dt/4/12` and `dt/4/18` to the U of `g_vDirectionalMotionLayer1..3` every frame (wrapping at 1; the V of
layers 2 and 3 swing back and forth), which its vertex shader adds to the layers' UVs (`Ocean.fx`
`Technique_Ocean_Distort_Directional_2Layers`: `uv * 0.75 + layer1`, `uv + layer2`; `_3Layers`: `uv * 0.5`, `* 0.75`,
`* 1`; the class's constructor 0x00464500 names the 2-layer technique twice and the 3-layer one once among its six
technique slots, which the graphics settings pick between). Its pixel shader averages the layers' texels (each later layer's
UV moved by `(earlier texel's rg) * 0.2 - 0.5`), multiplied by `(N.L * sun + ambient) * vertex color`; alpha =
average alpha * vertex alpha * fade. The game's own pond ripples use it the same way
(`S79__pond_ripplesShape`, `mesh/env/env_won_gnar_croc_pondfx.nif`).
**Flecks.** 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):
flecks on a brick that is otherwise lit as plastic. 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, then turned by an angle and moved by
@@ -360,43 +389,51 @@ What a glitter shape has, beside what plastic shapes have (white material, alpha
icon draws the flecks on the UVs the .nif has (`Mesh::uvs`, read back by `UgcModel::FromNif`).
- 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 updates an object's scene graph every frame only when its root
NiNode has the selective update bit (0x02; `LWOBaseRenderComponent::Run` 0x00d5d770 calls `NiAVObject::Update` when
`NiAVObject::GetSelectiveUpdate` 0x00413050 is set); otherwise only once when it loads, and the controllers never
move. So a model with moving glitter has flags 0x102 on its root, the glitter NiLODNode and its `LOD_n` nodes, and
0x1A on the glitter shapes, as the client's own AG ocean (`mesh/env/env_ag_ocean-maelstrom.nif`); every other
node keeps 0x110 and shape 0x10.
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).
(translation 0, scale 1, Maya method, center 0.5). No controllers.
- Its source, stored in the file as the client's own stored textures (`res/mesh/env/env_ag_ocean-maelstrom.nif`):
`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.
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.
`S01_Alpha_Model` is drawn with, so transparent glitter gets a group of its own.
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.
**Sparkles** (`shader_glitter_sparkle`, 79; 0: none; only with `shader_glitter` on). A group
`S79_GlitterSparkle_Model` after all the others, with every LOD, whose shapes are the glitter bricks' pieces (opaque
and transparent) again:
- Lifted off the brick along the normals by 0.005 (`UgcGlitter::SPARKLE_LIFT`), so they are in front of its surface:
a transparent brick, drawn later in the blended phase, doesn't cover them, and they don't fight it for the depth.
- Vertex colors: white taking `glitter_sparkle_tint` percent of the brick's color, times `glitter_sparkle_brightness`,
alpha 1 (`UgcGlitter::SparkleColor`).
- UVs as the flecks' but on the sparkle tile and placed apart from them (`UgcGlitter::eLayer::SPARKLES`).
- Material white, alpha 1. An `NiAlphaProperty` with the test bit (flags 0x1A00: test, GREATEREQUAL; threshold 127):
`ShaderCommon::GetAlphaFlags` (0x0109f5a0) puts a shape whose alpha property has the test bit in the alpha test
phase, whose states (`ShaderCommon__SetupPhaseRenderStates` 0x00463300) are blending off, alpha test GREATEREQUAL
0x7f, depth test and write.
- An `NiTexturingProperty`: the base map only, wrapping, trilinear, no texture transform (as the pond ripples), its
source `ugc_sparkle.dds` stored like the flecks'. Its alpha (`UgcGlitter::SparkleAlpha`): flat sparkles of
`glitter_sparkle_size` at 230, covering `glitter_sparkle_amount` percent. Averaged over 2 layers one sparkle alone
is 115 and over 3 it is 77, under the test's 127; two sparkles meeting are 230 or 153. So a sparkle shows only where
two moving layers' sparkles cross: it appears, grows, shrinks and goes out as the layers slide past each other at
different speeds. The first two mipmaps take the brightest of each 2x2 (the sparkles keep their alpha a little
further away), the rest the mean.
- The sparkle tile (`UgcGlitter::Params::SparkleTile`) is `75 * glitter_sparkle_size * glitter_speed` model units: the
layers move a fixed share of a tile a second, so a bigger tile crosses sparkles faster; at speed 1 each flash lasts
about half a second. The texture is the power of two (128 to 1024) that keeps a sparkle 3 pixels wide (256 at the
defaults).
The icon draws the flecks where they are (the same texture and the .nif's UVs, before the light; `glitter_size` and
`glitter_density`), opaque and transparent, and leaves the sparkles out (`Shaders::OverlayTags`: alpha tested shapes
tagged `shader_glitter_sparkle` or 79). 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 flecks from their
UVs, and marks the sparkles (look `SPARKLE` 1024); the property and zone views, which draw bricks from the LXFML, draw
flecks on the colors in `window.LDD_GLITTER` (`/api/bricks/materials.js`: the glitter colors by the current settings)
from their positions.
#### Satin

View File

@@ -92,9 +92,16 @@ brushed_material_types=brushedSteel,matteSteel
brushed_colors=298,300,1002,1004
# shader_glitter: for glitter colors (glitter_material_types and glitter_colors), S<id>_Glitter_Model and, for
# transparent ones, S<id>_GlitterAlpha_Model: 21 is LEGO-AnimUV, which lays a white fleck texture stored in the .nif
# over the color and moves it. 0: off, glitter stays plastic.
# over the color (still: the client never updates a placed model). 0: off, glitter stays plastic.
# glitter_size: the fleck texture's tile in model units (a stud is 0.8); glitter_density: flecks in a tile;
# glitter_speed: 1 moves the flecks a tile in 7 s one way and 11 s the other, 0 keeps them still.
# shader_glitter_sparkle: the glitter bricks' sparkles, S<id>_GlitterSparkle_Model over both glitter groups (only with
# shader_glitter). 79 is Distortion Directional (Ocean), whose texture layers the client moves every frame on its
# own: a sparkle flashes where two layers' sparkles meet. (Nothing else can move on a placed model: the client never
# updates its scene graph, so texture controllers in the .nif never run.) 0: no sparkles.
# glitter_sparkle_size: a sparkle's diameter in model units; glitter_sparkle_amount: percent of each moving layer
# covered (about its square's share of the brick sparkles at once); glitter_speed: how fast sparkles flash and go
# out (1: about half a second each, 0.1 to 4); glitter_sparkle_tint: percent of the brick's color the sparkles take;
# glitter_sparkle_brightness: percent.
# glitter_random: 1 places each brick's flecks its own way (turned and moved by the brick), 0 the same on every brick.
# glitter_colors: LEGO color ids that are glitter whatever their type (empty: the default, 114,117, which LEGO's color
# data calls glitter; none: no colors)
@@ -103,7 +110,12 @@ glitter_material_types=glitter
glitter_colors=114,117
glitter_size=1.6
glitter_density=50
shader_glitter_sparkle=79
glitter_sparkle_size=0.1
glitter_sparkle_amount=5
glitter_speed=1
glitter_sparkle_tint=30
glitter_sparkle_brightness=100
glitter_random=1
# Satin (opal) colors stay transparent plastic (the client has no satin shader) but are made milky:
# satin_colors: color ids (empty: the default, LEGO's satin colors 360,362,363,364,365,366,367,376; none: off),

View File

@@ -1381,37 +1381,28 @@ namespace {
}
}
// The client updates an object's scene graph every frame only when its root has the selective update bit (0x02), so
// a model with moving glitter has it on its root, the glitter group's nodes and shapes (as the client's own animated
// files); still glitter and everything else keep the game's brick model flags
TEST(UgcFormats, MovingGlitterIsUpdatedEveryFrame) {
// Nothing in a placed player model's .nif can move (the client never updates it: LWOSkinnedRenderComponent::Run with
// animation off for modelType 2), so a glitter .nif has no controllers and every node and shape keeps the game's brick
// model flags
TEST(UgcFormats, GlitterNifIsStatic) {
const auto mesh = Quad({ 0.2f, 0.4f, 0.8f, 0.6f });
const UgcGlitter::Params moving{ 1.6f, 50, 1.0f };
const UgcGlitter::Params still{ 1.6f, 50, 0.0f };
for (const auto* glitter : { &moving, &still }) {
const auto nif = UgcFormats::WriteLodNif("SceneNode_Model", {
{ "S01_Opaque_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } } },
{ "S21_Glitter_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } }, 0.0f, glitter } });
const auto blocks = BlockFlags(nif);
const bool animated = glitter == &moving;
ASSERT_EQ(blocks[0].first, "NiNode");
EXPECT_EQ(blocks[0].second, animated ? 0x102 : 0x110) << "root";
std::vector<uint16_t> shapes;
for (const auto& [type, flags] : blocks) if (type == "NiTriShape") shapes.push_back(flags);
ASSERT_EQ(shapes.size(), 2u);
EXPECT_EQ(shapes[0], 0x10); // plastic
EXPECT_EQ(shapes[1], animated ? 0x1A : 0x10);
std::vector<uint16_t> lods;
for (const auto& [type, flags] : blocks) if (type == "NiLODNode") lods.push_back(flags);
ASSERT_EQ(lods.size(), 2u);
EXPECT_EQ(lods[0], 0x110);
EXPECT_EQ(lods[1], animated ? 0x102 : 0x110);
const UgcGlitter::Params glitter;
const auto nif = UgcFormats::WriteLodNif("SceneNode_Model", {
{ "S01_Opaque_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } } },
{ "S21_Glitter_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } }, 0.0f, &glitter },
{ "S79_GlitterSparkle_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } }, 0.0f, &glitter, true } });
for (const auto* type : { "NiTextureTransformController", "NiFloatInterpolator", "NiFloatData" }) EXPECT_EQ(nif.find(type), std::string::npos) << type;
const auto blocks = BlockFlags(nif);
ASSERT_EQ(blocks[0].first, "NiNode");
for (const auto& [type, flags] : blocks) {
if (type == "NiNode" || type == "NiLODNode") EXPECT_EQ(flags, 0x110) << type;
if (type == "NiTriShape") EXPECT_EQ(flags, 0x10) << type;
}
}
TEST(UgcFormats, GlitterNifReadsBack) {
const auto mesh = Quad({ 0.2f, 0.4f, 0.8f, 0.6f });
const UgcGlitter::Params glitter{ 1.6f, 50, 2.0f };
const UgcGlitter::Params glitter;
const auto nif = UgcFormats::WriteLodNif("SceneNode_Model", { { "S21_GlitterAlpha_Model", true, { { 0.0f, 100.0f, "LOD_0", { &mesh, &mesh } } }, 0.0f, &glitter } });
std::string error;
const auto read = NifFile::Parse(nif, 0, error);
@@ -1430,9 +1421,8 @@ TEST(UgcFormats, GlitterNifReadsBack) {
EXPECT_FALSE(shape.material.clampU);
EXPECT_FALSE(shape.material.clampV);
EXPECT_TRUE(shape.material.alphaBlend);
// A tile in 7 s and 11 s at speed 1: twice as fast at 2
EXPECT_NEAR(shape.material.uvScroll[0], 2.0f / 7.0f, 1e-6f);
EXPECT_NEAR(shape.material.uvScroll[1], 2.0f / 11.0f, 1e-6f);
EXPECT_FALSE(shape.material.alphaTest);
EXPECT_EQ(shape.material.uvScroll, (std::array<float, 2>{})); // still
// Vertex colors and the white material as the other groups
EXPECT_EQ(shape.colors[3], 153);
EXPECT_EQ(shape.material.diffuse, (std::array<float, 3>{ 1.0f, 1.0f, 1.0f }));
@@ -1454,29 +1444,103 @@ TEST(UgcFormats, GlitterNifReadsBack) {
ASSERT_EQ(static_cast<uint8_t>((*dds)[128 + i * 4]), 255);
ASSERT_EQ(static_cast<uint8_t>((*dds)[128 + i * 4 + 3]), alpha[i]) << i;
}
// The block types, as the client's own animated textures (res/mesh/env/env_ag_ocean-maelstrom.nif)
for (const auto* type : { "NiTexturingProperty", "NiTextureTransformController", "NiFloatInterpolator", "NiFloatData", "NiSourceTexture", "NiPersistentSrcTextureRendererData" }) {
EXPECT_NE(nif.find(type), std::string::npos) << type;
}
for (const auto* type : { "NiTexturingProperty", "NiSourceTexture", "NiPersistentSrcTextureRendererData" }) EXPECT_NE(nif.find(type), std::string::npos) << type;
// Still (speed 0): the texture without controllers
const UgcGlitter::Params still{ 1.6f, 50, 0.0f };
const auto stillNif = UgcFormats::WriteLodNif("SceneNode_Model", { { "S21_Glitter_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } }, 0.0f, &still } });
const auto stillRead = NifFile::Parse(stillNif, 0, error);
ASSERT_TRUE(stillRead) << error;
EXPECT_EQ(stillRead->meshes[0].material.uvScroll, (std::array<float, 2>{}));
EXPECT_GE(stillRead->meshes[0].material.embeddedTexture, 0);
EXPECT_EQ(stillNif.find("NiTextureTransformController"), std::string::npos);
// The dashboard's encoding carries the motion
// The dashboard's encoding carries the UVs
const auto encoded = NifFile::Encode(*read, { "glitter", "glitter" });
uint32_t length = 0;
std::memcpy(&length, encoded.data(), 4);
const auto header2 = nlohmann::json::parse(encoded.substr(4, length));
EXPECT_NEAR(header2["meshes"][0]["uvScroll"][0].get<float>(), 2.0f / 7.0f, 1e-6f);
EXPECT_TRUE(header2["meshes"][0]["uv"].get<bool>());
}
// The sparkle texture: the same every time, flat sparkles at SPARKLE_ALPHA covering about the amount asked for, a
// sparkle 3 pixels wide; its first mipmaps keep the sparkles' alpha. The tile (how fast the client's fixed layer motion
// crosses sparkles) grows with the speed, the texture with it.
TEST(UgcGlitter, SparkleTexture) {
const UgcGlitter::Params params;
EXPECT_FLOAT_EQ(params.SparkleTile(), 7.5f);
EXPECT_EQ(params.SparkleTextureSize(), 256);
const auto alpha = UgcGlitter::SparkleAlpha(params);
ASSERT_EQ(alpha.size(), 256u * 256u);
EXPECT_EQ(alpha, UgcGlitter::SparkleAlpha(params));
EXPECT_EQ(*std::max_element(alpha.begin(), alpha.end()), UgcGlitter::SPARKLE_ALPHA);
double covered = 0;
for (const auto a : alpha) covered += a / static_cast<double>(UgcGlitter::SPARKLE_ALPHA);
EXPECT_NEAR(covered / alpha.size(), 0.05, 0.015); // overlaps make it a little less
// One sparkle alone stays under the client's alpha test (GREATEREQUAL 127) with 2 or 3 layers averaged, two meet it
EXPECT_LT(UgcGlitter::SPARKLE_ALPHA / 2, 127);
EXPECT_GE(UgcGlitter::SPARKLE_ALPHA * 2 / 3, 127);
EXPECT_LT(UgcGlitter::SPARKLE_ALPHA / 3, 127);
const auto mips = UgcGlitter::Mipmaps(alpha, 2);
ASSERT_EQ(mips.size(), 9u); // 256 .. 1
EXPECT_EQ(*std::max_element(mips[1].begin(), mips[1].end()), UgcGlitter::SPARKLE_ALPHA);
EXPECT_EQ(*std::max_element(mips[2].begin(), mips[2].end()), UgcGlitter::SPARKLE_ALPHA);
EXPECT_LT(*std::max_element(mips[8].begin(), mips[8].end()), 127);
// Faster: a bigger tile and texture; more: more covered
UgcGlitter::Params fast = params;
fast.speed = 2.0f;
EXPECT_FLOAT_EQ(fast.SparkleTile(), 15.0f);
EXPECT_EQ(fast.SparkleTextureSize(), 512);
UgcGlitter::Params more = params;
more.sparkleAmount = 10.0f;
const auto moreAlpha = UgcGlitter::SparkleAlpha(more);
EXPECT_GT(std::count(moreAlpha.begin(), moreAlpha.end(), UgcGlitter::SPARKLE_ALPHA), std::count(alpha.begin(), alpha.end(), UgcGlitter::SPARKLE_ALPHA));
// Colors: white taking the tint of the brick's color, at the brightness
EXPECT_EQ(UgcGlitter::SparkleColor({ 0.0f, 0.5f, 1.0f, 0.4f }, params), glm::vec4(0.7f, 0.85f, 1.0f, 1.0f));
UgcGlitter::Params dim = params;
dim.sparkleTint = 0.0f;
dim.sparkleBrightness = 50.0f;
EXPECT_EQ(UgcGlitter::SparkleColor({ 0.0f, 0.5f, 1.0f, 0.4f }, dim), glm::vec4(0.5f, 0.5f, 0.5f, 1.0f));
}
// The sparkle group as the client's own Distortion Directional shapes (S79__pond_ripplesShape): the glitter bricks'
// triangles lifted off them along their normals, the sparkles' vertex colors, UVs on the sparkle tile placed per
// brick apart from the flecks, the sparkle texture stored in the file (no transform), alpha tested
TEST(UgcFormats, SparkleNifReadsBack) {
auto mesh = Quad({ 0.0f, 0.5f, 1.0f, 0.6f });
mesh.brickSeeds.assign(4, 99);
const UgcGlitter::Params glitter;
const auto nif = UgcFormats::WriteLodNif("SceneNode_Model", {
{ "S21_GlitterAlpha_Model", true, { { 0.0f, 100.0f, "LOD_0", { &mesh } } }, 0.0f, &glitter },
{ "S79_GlitterSparkle_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } }, 0.0f, &glitter, true } });
std::string error;
const auto read = NifFile::Parse(nif, 0, error);
ASSERT_TRUE(read) << error;
ASSERT_EQ(read->meshes.size(), 2u);
EXPECT_TRUE(read->skipped.empty());
const auto& flecks = read->meshes[0];
const auto& sparkles = read->meshes[1];
EXPECT_EQ(sparkles.material.shaderTag, 79);
EXPECT_TRUE(sparkles.material.alphaTest);
EXPECT_EQ(sparkles.material.alphaThreshold, 127);
EXPECT_FALSE(sparkles.material.alphaBlend);
EXPECT_FLOAT_EQ(sparkles.material.alpha, 1.0f);
ASSERT_GE(sparkles.material.embeddedTexture, 0);
EXPECT_NE(sparkles.material.embeddedTexture, flecks.material.embeddedTexture);
ASSERT_EQ(sparkles.positions.size(), 12u);
for (size_t v = 0; v < 4; v++) {
EXPECT_FLOAT_EQ(sparkles.positions[v * 3 + 2], UgcGlitter::SPARKLE_LIFT); // off the quad, along its normal
const auto uv = UgcGlitter::Uv(mesh.positions[v], mesh.normals[v], glitter.SparkleTile(), 99, UgcGlitter::eLayer::SPARKLES);
EXPECT_FLOAT_EQ(sparkles.uvs[v * 2], uv.x);
EXPECT_FLOAT_EQ(sparkles.uvs[v * 2 + 1], uv.y);
EXPECT_NE(sparkles.uvs[v * 2], flecks.uvs[v * 2]);
// White taking 30% of the brick's color, opaque
EXPECT_EQ(sparkles.colors[v * 4], 179);
EXPECT_EQ(sparkles.colors[v * 4 + 2], 255);
EXPECT_EQ(sparkles.colors[v * 4 + 3], 255);
}
const auto dds = NifFile::EmbeddedTexture(nif, sparkles.material.embeddedTexture);
ASSERT_TRUE(dds);
uint32_t header[31];
std::memcpy(header, dds->data() + 4, sizeof(header));
EXPECT_EQ(header[2], 256u);
EXPECT_EQ(header[6], 9u);
// The icon leaves the sparkles out
EXPECT_EQ(UgcModel::FromNif(*read, {}, { 79 }).transparent.TriangleCount() + UgcModel::FromNif(*read, {}, { 79 }).opaque.TriangleCount(), 2u);
EXPECT_EQ(UgcModel::FromNif(*read).opaque.TriangleCount() + UgcModel::FromNif(*read).transparent.TriangleCount(), 4u);
}
// Glitter colors (a Materials.xml glitter type or glitter_colors) get groups of their own, opaque and transparent,
// with every level; off (shader_glitter 0) they stay plastic and nothing changes
TEST(UgcShaders, GlitterGroups) {
@@ -1493,6 +1557,7 @@ TEST(UgcShaders, GlitterGroups) {
auto settings = SmallSettings();
settings.build.colorVariation = 0.0f;
settings.shaders.glitter = 21;
settings.shaders.sparkle = 79;
const auto outcome = UgcJobs::ProcessModel(lxfml, library, settings, 7);
ASSERT_TRUE(outcome.ok) << outcome.error;
const auto nif = *ZCompression::Gunzip(outcome.files.at("model.nif.gz"));
@@ -1500,16 +1565,19 @@ TEST(UgcShaders, GlitterGroups) {
for (const uint32_t level : { 0u, 1u }) {
const auto read = NifFile::Parse(nif, level, error);
ASSERT_TRUE(read) << error;
for (const auto* name : { "S01_Opaque_Model", "S21_Glitter_Model", "S01_Alpha_Model", "S21_GlitterAlpha_Model" }) EXPECT_TRUE(read->nodes.contains(name)) << name;
for (const auto* name : { "S01_Opaque_Model", "S21_Glitter_Model", "S01_Alpha_Model", "S21_GlitterAlpha_Model", "S79_GlitterSparkle_Model" }) EXPECT_TRUE(read->nodes.contains(name)) << name;
std::map<std::pair<int32_t, bool>, size_t> triangles; // (tag, transparent) -> triangles
for (const auto& mesh : read->meshes) {
bool seeThrough = false;
for (size_t i = 3; i < mesh.colors.size(); i += 4) seeThrough = seeThrough || mesh.colors[i] < 250;
triangles[{ mesh.material.shaderTag, seeThrough }] += mesh.indices.size() / 3;
// Only the glitter shapes are textured
EXPECT_EQ(mesh.material.embeddedTexture >= 0, mesh.material.shaderTag == 21);
EXPECT_EQ(!mesh.uvs.empty(), mesh.material.shaderTag == 21);
// Only the glitter and sparkle shapes are textured, only the sparkles alpha tested
EXPECT_EQ(mesh.material.embeddedTexture >= 0, mesh.material.shaderTag == 21 || mesh.material.shaderTag == 79);
EXPECT_EQ(!mesh.uvs.empty(), mesh.material.shaderTag == 21 || mesh.material.shaderTag == 79);
EXPECT_EQ(mesh.material.alphaTest, mesh.material.shaderTag == 79);
}
// The sparkles: over every glitter brick, opaque and transparent, one shape per piece
EXPECT_EQ((triangles[{ 79, false }]), 36u);
EXPECT_EQ((triangles[{ 21, false }]), 12u);
EXPECT_EQ((triangles[{ 21, true }]), 24u); // one shape per brick, as the other transparent bricks
EXPECT_EQ((triangles[{ 1, false }]), 12u);
@@ -1518,11 +1586,22 @@ TEST(UgcShaders, GlitterGroups) {
EXPECT_NE(outcome.stats.find("\"S21_Glitter_Model\":12"), std::string::npos) << outcome.stats;
EXPECT_NE(outcome.stats.find("\"S21_GlitterAlpha_Model\":24"), std::string::npos) << outcome.stats;
EXPECT_NE(outcome.stats.find("\"S01_Alpha_Model\":12"), std::string::npos) << outcome.stats;
EXPECT_NE(outcome.stats.find("\"S79_GlitterSparkle_Model\":36"), std::string::npos) << outcome.stats;
// The icon reads the glitter back by the tag (transparent too)
const auto read = NifFile::Parse(nif, 0, error);
const auto back = UgcModel::FromNif(*read, settings.shaders.TagLooks());
const auto back = UgcModel::FromNif(*read, settings.shaders.TagLooks(), settings.shaders.OverlayTags());
EXPECT_EQ(std::count(back.opaque.looks.begin(), back.opaque.looks.end(), UgcModel::eLook::GLITTER), 8);
EXPECT_EQ(back.opaque.TriangleCount() + back.transparent.TriangleCount(), 60u); // no sparkles
// No sparkles (shader_glitter_sparkle 0): the glitter groups alone
settings.shaders.sparkle = 0;
const auto noSparkles = UgcJobs::ProcessModel(lxfml, library, settings, 7);
ASSERT_TRUE(noSparkles.ok);
const auto noSparklesRead = NifFile::Parse(*ZCompression::Gunzip(noSparkles.files.at("model.nif.gz")), 0, error);
ASSERT_TRUE(noSparklesRead);
EXPECT_FALSE(noSparklesRead->nodes.contains("S79_GlitterSparkle_Model"));
EXPECT_EQ(noSparkles.files.at("icon.png"), outcome.files.at("icon.png"));
settings.shaders.sparkle = 79;
EXPECT_EQ(std::count(back.transparent.looks.begin(), back.transparent.looks.end(), UgcModel::eLook::GLITTER), 16);
// Combined transparent bricks: one glitter shape
@@ -1540,7 +1619,9 @@ TEST(UgcShaders, GlitterGroups) {
settings.shaders.glitter = 0;
const auto off = UgcJobs::ProcessModel(lxfml, library, settings, 7);
settings.build.looks.materialTypes.erase("glitter");
settings.shaders.glitterParams = { 3.0f, 7, 5.0f };
settings.shaders.glitterParams.tile = 3.0f;
settings.shaders.glitterParams.flecks = 7;
settings.shaders.glitterParams.speed = 3.0f;
settings.icon.glitter = settings.shaders.glitterParams;
const auto noRules = UgcJobs::ProcessModel(lxfml, library, settings, 7);
ASSERT_TRUE(off.ok && noRules.ok);
@@ -1629,7 +1710,8 @@ TEST(UgcShaders, IconsDrawGlitterFlecks) {
options.yawDegrees = 0.0f;
options.pitchDegrees = 0.0f;
options.shadows = 0.0f;
options.glitter = { 0.5f, 60, 1.0f };
options.glitter.tile = 0.5f;
options.glitter.flecks = 60;
const auto plain = UgcRender::RenderIcon(model, options);
model.opaque.looks.assign(4, UgcModel::eLook::GLITTER);
const auto glitter = UgcRender::RenderIcon(model, options);