diff --git a/dCommon/NifFile.h b/dCommon/NifFile.h index 98bab8e8f..5654d2f97 100644 --- a/dCommon/NifFile.h +++ b/dCommon/NifFile.h @@ -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 diff --git a/dDashboardServer/routes/SettingsCatalog.cpp b/dDashboardServer/routes/SettingsCatalog.cpp index 8e530b80c..68e69b572 100644 --- a/dDashboardServer/routes/SettingsCatalog.cpp +++ b/dDashboardServer/routes/SettingsCatalog.cpp @@ -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_Glitter_Model and (transparent ones) S_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_Glitter_Model and (transparent ones) S_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_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)); diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index c305631d3..67be52882 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -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(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(UgcSetting("shader_glitter").value_or("21")).value_or(21); - Workers::Reply(reply, context, false, [url, lod, glitterTag](HTTPReply& out) { + const auto sparkleTag = GeneralUtils::TryParse(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 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"; diff --git a/dDashboardServer/static/js/lddviewer.js b/dDashboardServer/static/js/lddviewer.js index a43141630..3ccb59fe4 100644 --- a/dDashboardServer/static/js/lddviewer.js +++ b/dDashboardServer/static/js/lddviewer.js @@ -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); } diff --git a/dDashboardServer/static/js/scenery-core.js b/dDashboardServer/static/js/scenery-core.js index a401429c7..87e0f6437 100644 --- a/dDashboardServer/static/js/scenery-core.js +++ b/dDashboardServer/static/js/scenery-core.js @@ -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 diff --git a/dDashboardServer/static/js/ugc-viewer.js b/dDashboardServer/static/js/ugc-viewer.js index 6cfb41bf8..63cb6a5cb 100644 --- a/dDashboardServer/static/js/ugc-viewer.js +++ b/dDashboardServer/static/js/ugc-viewer.js @@ -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 diff --git a/dUgcServer/Formats/UgcFormats.cpp b/dUgcServer/Formats/UgcFormats.cpp index 428d0ec4d..d4f9c0058 100644 --- a/dUgcServer/Formats/UgcFormats.cpp +++ b/dUgcServer/Formats/UgcFormats.cpp @@ -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 GlitterUvs(const UgcModel::Mesh& mesh, const UgcGlitter::Params& glitter, UgcGlitter::eLayer layer) { + std::vector 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& uvSet = {}) { Writer out; const auto count = static_cast(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> 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 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>& 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(mipmaps.size())); pixels.U32(4); // bytes per pixel uint32_t offset = 0; + const auto side = static_cast(std::lround(std::sqrt(static_cast(mipmaps.empty() ? 0 : mipmaps[0].size())))); for (size_t level = 0; level < mipmaps.size(); level++) { - const uint32_t side = static_cast(UgcGlitter::TEXTURE_SIZE) >> level; - pixels.U32(side); - pixels.U32(side); + pixels.U32(side >> level); + pixels.U32(side >> level); pixels.U32(offset); offset += static_cast(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 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 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 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 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 levels; Writer ranges; @@ -461,16 +488,16 @@ namespace UgcFormats { const auto level = nif.Reserve("NiNode"); std::vector 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); } diff --git a/dUgcServer/Formats/UgcFormats.h b/dUgcServer/Formats/UgcFormats.h index 5302394d4..1d12a4fba 100644 --- a/dUgcServer/Formats/UgcFormats.h +++ b/dUgcServer/Formats/UgcFormats.h @@ -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{}; }; /** diff --git a/dUgcServer/Model/UgcGlitter.cpp b/dUgcServer/Model/UgcGlitter.cpp index b1063d2a3..12927860a 100644 --- a/dUgcServer/Model/UgcGlitter.cpp +++ b/dUgcServer/Model/UgcGlitter.cpp @@ -4,6 +4,19 @@ #include 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(bits >> 40) / static_cast(1ull << 24); } + // The side of a square texture's alpha + int Side(const std::vector& alpha) { return static_cast(std::lround(std::sqrt(static_cast(alpha.size())))); } + } + std::vector FleckAlpha(uint32_t flecks) { constexpr int N = TEXTURE_SIZE; std::vector alpha(static_cast(N) * N, 0.0f); @@ -36,55 +49,89 @@ namespace UgcGlitter { return out; } - std::vector> Mipmaps(const std::vector& 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(side) < wanted) side *= 2; + return side; + } + + std::vector SparkleAlpha(const Params& params) { + const int N = params.SparkleTextureSize(); + std::vector alpha(static_cast(N) * N, 0); + const float radius = std::max(params.sparkleSize / params.SparkleTile() * static_cast(N) * 0.5f, 0.75f); + const float share = std::clamp(params.sparkleAmount, 0.0f, 100.0f) / 100.0f; + const auto count = static_cast(std::lround(share * static_cast(N) * static_cast(N) / (3.14159265f * radius * radius))); + uint64_t state = 0x737061726B6C6500ull; + const auto next = [&state] { return Unit(SplitMix(state++)); }; + const int reach = static_cast(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(std::floor(cx)) + dx, y = static_cast(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(((y % N) + N) % N) * N + ((x % N) + N) % N]; + value = std::max(value, static_cast(std::lround(cover * SPARKLE_ALPHA))); + } + } + } + return alpha; + } + + std::vector> Mipmaps(const std::vector& alpha, int keepPeaks) { std::vector> 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 level(static_cast(size) * size); + std::vector next(static_cast(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(above[static_cast(y * 2 + dy) * from + x * 2 + dx]); }; - level[static_cast(y) * size + x] = static_cast((at(0, 0) + at(1, 0) + at(0, 1) + at(1, 1) + 2) / 4); + next[static_cast(y) * size + x] = static_cast(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(bits >> 40) / static_cast(1ull << 24); } - } - uint32_t BrickSeed(uint64_t modelSeed, uint32_t brick) { const auto hash = SplitMix(SplitMix(modelSeed ^ 0x676C6974746572ull) + brick); return static_cast(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(seed) << 2) | static_cast(plane)); + const auto hash = SplitMix(((static_cast(seed) << 2) | static_cast(plane)) ^ (static_cast(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& alpha, const glm::vec2& uv) { - constexpr int N = TEXTURE_SIZE; - if (alpha.size() != static_cast(N) * N) return 0.0f; + const int N = Side(alpha); + if (N == 0 || alpha.size() != static_cast(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(std::floor(x)), y0 = static_cast(std::floor(y)); const float fx = x - x0, fy = y - y0; diff --git a/dUgcServer/Model/UgcGlitter.h b/dUgcServer/Model/UgcGlitter.h index a3c5ba23d..185b69301 100644 --- a/dUgcServer/Model/UgcGlitter.h +++ b/dUgcServer/Model/UgcGlitter.h @@ -6,43 +6,81 @@ #include /** - * 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 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> Mipmaps(const std::vector& 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 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> Mipmaps(const std::vector& 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& alpha, const glm::vec2& uv); } diff --git a/dUgcServer/Model/UgcModel.cpp b/dUgcServer/Model/UgcModel.cpp index 9867bdb68..92f8a8a44 100644 --- a/dUgcServer/Model/UgcModel.cpp +++ b/dUgcServer/Model/UgcModel.cpp @@ -363,9 +363,10 @@ namespace UgcModel { return ranges; } - Model FromNif(const NifFile::Model& nif, const std::map& tagLooks) { + Model FromNif(const NifFile::Model& nif, const std::map& tagLooks, const std::set& 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; diff --git a/dUgcServer/Model/UgcModel.h b/dUgcServer/Model/UgcModel.h index 23c2b23b2..605faf6e2 100644 --- a/dUgcServer/Model/UgcModel.h +++ b/dUgcServer/Model/UgcModel.h @@ -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& 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& tagLooks = {}, const std::set& overlayTags = {}); /** * The mesh's triangles by look ([eLook] -> its triangles; a triangle's look is its first vertex's), the looks not diff --git a/dUgcServer/Processing/UgcJobs.cpp b/dUgcServer/Processing/UgcJobs.cpp index bde8d9b21..42e04eb1f 100644 --- a/dUgcServer/Processing/UgcJobs.cpp +++ b/dUgcServer/Processing/UgcJobs.cpp @@ -128,6 +128,17 @@ namespace UgcJobs { return looks; } + std::set Shaders::OverlayTags() const { + std::set tags{ 79 }; + if (sparkle != 0) tags.insert(static_cast(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& tagLooks) { + const std::map& tagLooks, const std::set& 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 separate{}; for (size_t look = 1; look < UgcModel::LOOK_COUNT; look++) separate[look] = settings.shaders.TagOf(static_cast(look)) != 0; const bool glowApart = separate[static_cast(UgcModel::eLook::GLOW)]; + // The glitter bricks' sparkles, a group over both glitter groups + const bool sparkles = separate[static_cast(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(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(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; } diff --git a/dUgcServer/Processing/UgcJobs.h b/dUgcServer/Processing/UgcJobs.h index 5b874a333..d33c9ea31 100644 --- a/dUgcServer/Processing/UgcJobs.h +++ b/dUgcServer/Processing/UgcJobs.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -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 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 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& tagLooks = {}); + const std::map& tagLooks = {}, const std::set& 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); diff --git a/dUgcServer/Processing/UgcProcessor.cpp b/dUgcServer/Processing/UgcProcessor.cpp index d53b7d4f8..b021574bb 100644 --- a/dUgcServer/Processing/UgcProcessor.cpp +++ b/dUgcServer/Processing/UgcProcessor.cpp @@ -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 diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index 64dfd042c..03d4f94fd 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -117,11 +117,17 @@ namespace { settings.shaders.brushed = std::min(Setting("shader_brushed", 89), 9999u); settings.shaders.glow = std::min(Setting("shader_glow", 46), 9999u); settings.shaders.glowEmissive = std::clamp(Setting("glow_emissive", 1.0f), 0.0f, 10.0f); - // Glitter colors in S_Glitter_Model and S_GlitterAlpha_Model with drifting flecks (LEGO-AnimUV) + // Glitter colors in S_Glitter_Model and S_GlitterAlpha_Model with flecks (LEGO-AnimUV), and sparkles + // over them in S_GlitterSparkle_Model (Distortion Directional, which moves on its own) settings.shaders.glitter = std::min(Setting("shader_glitter", 21), 9999u); + settings.shaders.sparkle = std::min(Setting("shader_glitter_sparkle", 79), 9999u); settings.shaders.glitterParams.tile = std::clamp(Setting("glitter_size", 1.6f), 0.1f, 100.0f); settings.shaders.glitterParams.flecks = std::min(Setting("glitter_density", 50), 2000u); - settings.shaders.glitterParams.speed = std::clamp(Setting("glitter_speed", 1.0f), 0.0f, 100.0f); + settings.shaders.glitterParams.sparkleSize = std::clamp(Setting("glitter_sparkle_size", 0.1f), 0.01f, 1.0f); + settings.shaders.glitterParams.sparkleAmount = std::clamp(Setting("glitter_sparkle_amount", 5.0f), 0.0f, 50.0f); + settings.shaders.glitterParams.speed = std::clamp(Setting("glitter_speed", 1.0f), 0.1f, 4.0f); + settings.shaders.glitterParams.sparkleTint = std::clamp(Setting("glitter_sparkle_tint", 30.0f), 0.0f, 100.0f); + settings.shaders.glitterParams.sparkleBrightness = std::clamp(Setting("glitter_sparkle_brightness", 100.0f), 0.0f, 100.0f); settings.shaders.glitterParams.random = Setting("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 }, diff --git a/docs/UgcServer.md b/docs/UgcServer.md index 59499dad4..1ed72f962 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -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_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 diff --git a/resources/ugcconfig.ini b/resources/ugcconfig.ini index 26af58a21..83c3a5c22 100644 --- a/resources/ugcconfig.ini +++ b/resources/ugcconfig.ini @@ -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_Glitter_Model and, for # transparent ones, S_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_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), diff --git a/tests/dUgcTests/UgcTests.cpp b/tests/dUgcTests/UgcTests.cpp index b40ee43ca..7359a9fcc 100644 --- a/tests/dUgcTests/UgcTests.cpp +++ b/tests/dUgcTests/UgcTests.cpp @@ -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 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 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{})); // still // Vertex colors and the white material as the other groups EXPECT_EQ(shape.colors[3], 153); EXPECT_EQ(shape.material.diffuse, (std::array{ 1.0f, 1.0f, 1.0f })); @@ -1454,29 +1444,103 @@ TEST(UgcFormats, GlitterNifReadsBack) { ASSERT_EQ(static_cast((*dds)[128 + i * 4]), 255); ASSERT_EQ(static_cast((*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{})); - 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(), 2.0f / 7.0f, 1e-6f); EXPECT_TRUE(header2["meshes"][0]["uv"].get()); } +// 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(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, 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);