diff --git a/dCommon/NifFile.cpp b/dCommon/NifFile.cpp index 5f4585921..dace685af 100644 --- a/dCommon/NifFile.cpp +++ b/dCommon/NifFile.cpp @@ -123,6 +123,7 @@ namespace { struct NetHeader { std::string name; + int32_t controller{ -1 }; }; struct AvHeader { @@ -327,7 +328,7 @@ namespace { net.name = String(reader.U32()); const auto extra = reader.U32(); reader.Skip(static_cast(extra) * 4); - reader.I32(); // controller + net.controller = reader.I32(); return net; } @@ -554,6 +555,55 @@ namespace { } } + // Tiles a second the controllers from `first` on (an NiTexturingProperty's) move its base map in U and V: each + // NiTextureTransformController translating the base map, from its NiFloatInterpolator's NiFloatData's first key + // to its last, times its frequency + std::array BaseMapScroll(int32_t first) { + std::array scroll{}; + std::set seen; + for (int32_t index = first; index >= 0 && !seen.contains(index);) { + seen.insert(index); + const auto* type = TypeOf(index); + if (!type || *type != "NiTextureTransformController") break; + m_Used.insert(index); + auto reader = BlockReader(index); + const auto next = reader.I32(); + reader.U16(); // flags + const auto frequency = reader.Float(); + reader.Skip(12); // phase, start, stop + reader.I32(); // target + const auto interpolator = reader.I32(); + const auto shaderMap = reader.U8(); + const auto slot = reader.U32(); + const auto operation = reader.U32(); + const auto* interpolatorType = TypeOf(interpolator); + if (reader.Ok() && !shaderMap && slot == 0 && operation <= 1 && interpolatorType && *interpolatorType == "NiFloatInterpolator") { + m_Used.insert(interpolator); + auto value = BlockReader(interpolator); + value.Float(); + const auto data = value.I32(); + const auto* dataType = TypeOf(data); + if (value.Ok() && dataType && *dataType == "NiFloatData") { + m_Used.insert(data); + auto keys = BlockReader(data); + const auto count = keys.U32(); + const auto keyType = count > 0 ? keys.U32() : 0; + // Linear keys are time and value; quadratic add two tangents; TBC three floats + const uint32_t floats = keyType == 1 ? 2 : keyType == 2 ? 4 : keyType == 3 ? 5 : 0; + if (count >= 2 && floats > 0 && count <= 100000) { + const auto values = keys.Array(static_cast(count) * floats); + if (keys.Ok()) { + const float duration = values[(count - 1) * floats] - values[0]; + if (duration > 0.0f) scroll[operation] = (values[(count - 1) * floats + 1] - values[1]) / duration * frequency; + } + } + } + } + index = next; + } + return scroll; + } + NifFile::Material ReadMaterial(const Properties& properties, uint8_t& baseSet, uint8_t& darkSet) { NifFile::Material material; if (properties.material >= 0) { @@ -597,7 +647,7 @@ namespace { } if (properties.texturing >= 0) { auto reader = BlockReader(properties.texturing); - ReadNet(reader); + material.uvScroll = BaseMapScroll(ReadNet(reader).controller); reader.U16(); // flags reader.U32(); // texture count // TexDesc (nif.xml, 20.1.0.3 on): source, TexturingMapFlags (clamp in bits 12-15, UV set in 0-7), whether a @@ -781,6 +831,7 @@ namespace NifFile { {"shaderTag", material.shaderTag}, {"darkTexture", uv2 ? darkIndex : -1}, {"uv2", uv2} }; if (m < looks.size()) entry["look"] = looks[m]; + if (material.uvScroll[0] != 0.0f || material.uvScroll[1] != 0.0f) entry["uvScroll"] = { material.uvScroll[0], material.uvScroll[1] }; Append(body, mesh.positions.data(), mesh.positions.size() * sizeof(float)); if (!mesh.normals.empty()) { std::vector packed(mesh.normals.size()); diff --git a/dCommon/NifFile.h b/dCommon/NifFile.h index 87337e770..220acb03c 100644 --- a/dCommon/NifFile.h +++ b/dCommon/NifFile.h @@ -16,7 +16,8 @@ * (NiNode, NiLODNode, NiBillboardNode and other nodes) with its transforms baked into the vertices, NiTriShape and * NiTriStrips geometry (positions, normals, the first UV set, vertex colors), and the properties Gamebryo passes down * the tree: NiMaterialProperty, NiAlphaProperty, NiTexturingProperty's base texture (an external NiSourceTexture), - * NiVertexColorProperty and NiStencilProperty's draw mode (double sided). Skipped: hidden subtrees, animation, + * NiVertexColorProperty and NiStencilProperty's draw mode (double sided), and how fast NiTextureTransformControllers move + * the base texture. Skipped: hidden subtrees, other animation, * particles, lights and cameras; skinned geometry is drawn in its bind pose. Blocks are skipped by their stored sizes, * so a block this reader doesn't know never breaks the rest of the file. */ @@ -39,6 +40,9 @@ namespace NifFile { // the base texture; as `texture` and `embeddedTexture` std::string darkTexture; int32_t embeddedDarkTexture{ -1 }; + // How fast the base texture moves (tiles a second in U and V): NiTextureTransformControllers on the + // NiTexturingProperty translating the base map, each from its first key to its last, looping + std::array uvScroll{}; int32_t shaderTag{ -1 }; // mapShaders id from a multishader tag in the name of the mesh or a node above it }; @@ -98,7 +102,11 @@ namespace NifFile { BRUSHED = 128, // LEGO-Emissive: lerp(lit, vertex color, vertex alpha * NiMaterialProperty's emissive red); the vertex alpha is // that mask, not opacity - EMISSIVE = 256 + EMISSIVE = 256, + // 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 }; // eShaderLook bits of a shader (mapShaders.gameValue); 0 for the usual lit look and for fixed function @@ -131,6 +139,7 @@ namespace NifFile { * to a multiple of 4), then the binary data it describes. Per mesh at "offset": float32 positions (3 per vertex), * int8 normals (3 per vertex, times 127, padded to 4 bytes) when "normals", float32 UVs (2 per vertex) when "uv", * float32 dark texture UVs when "uv2", uint8 RGBA colors when "colors", then uint16 indices (padded to 4 bytes). + * A mesh whose base texture moves has "uvScroll" (Material::uvScroll). * `textures[i]` is where mesh i's texture is (empty: none) and `darkTextures[i]` its dark texture; the header * lists each once in "textures" and a mesh's "texture" and "darkTexture" index it (-1: none). `looks[i]`, when * given, is mesh i's "look" (eShaderLook bits of the shader it is drawn with, for views without a scenery manifest). diff --git a/dDashboardServer/routes/ClientAssets.cpp b/dDashboardServer/routes/ClientAssets.cpp index b6b2391bb..76c26dc07 100644 --- a/dDashboardServer/routes/ClientAssets.cpp +++ b/dDashboardServer/routes/ClientAssets.cpp @@ -5,10 +5,12 @@ #include #include #include +#include #include #include "OnceCache.h" #include "UgcBricks.h" +#include "UgcRoutes.h" #include "RouteUtils.h" #include "CDClientDatabase.h" #include "Game.h" @@ -454,22 +456,56 @@ namespace { void RegisterClientAssetRoutes() { Route(eHTTPMethod::GET, "/api/bricks/materials.js", 0, "The brick colours (MatID -> [r, g, b, a]) from Materials.xml in the client's res/brickdb.zip, as a script setting " - "window.LDD_MATERIALS for the 3D viewers. Read once; an empty table when the client's brick database can't be read", + "window.LDD_MATERIALS for the 3D viewers, and window.LDD_GLITTER: the colours the UGC server makes glitter with its settings " + "(shader_glitter on: glitter_material_types and glitter_colors) and the glitter's glitter_size and glitter_density. " + "The colours are read once; an empty table when the client's brick database can't be read", [](HTTPReply& reply, const HTTPContext&) { // Read on first use (thread-safe static init); the client only changes with a restart - static const std::string script = [] { - nlohmann::json colours = nlohmann::json::object(); + struct Colours { + std::string script; + std::map types; // MatID -> MaterialType + }; + static const Colours colours = [] { + Colours out; + nlohmann::json table = nlohmann::json::object(); const auto zip = ClientAssets::ReadResFile("brickdb.zip"); const auto xml = zip ? UgcBricks::ReadZipEntry(*zip, "Materials.xml") : std::nullopt; if (xml) { - for (const auto& [id, m] : UgcBricks::ParseMaterials(*xml)) colours[std::to_string(id)] = { m.r, m.g, m.b, m.a }; + for (const auto& [id, m] : UgcBricks::ParseMaterials(*xml)) { + table[std::to_string(id)] = { m.r, m.g, m.b, m.a }; + out.types[id] = m.type; + } } else { LOG("Couldn't read Materials.xml from the client's brickdb.zip; the 3D viewers' bricks will be grey"); } - return "window.LDD_MATERIALS = " + colours.dump() + ";\n"; + out.script = "window.LDD_MATERIALS = " + table.dump() + ";\n"; + return out; }(); + // The glitter colours as the UGC server picks them (UgcServer.cpp ReadSettings), from its current settings + const auto list = [](const std::string& name) { + std::set items; + std::stringstream stream(UgcRoutes::Setting(name).value_or("")); + std::string item; + while (std::getline(stream, item, ',')) { + std::erase_if(item, [](unsigned char c) { return std::isspace(c); }); + if (!item.empty() && item != "none") items.insert(item); + } + return items; + }; + nlohmann::json glitter = nlohmann::json::array(); + if (GeneralUtils::TryParse(UgcRoutes::Setting("shader_glitter").value_or("")).value_or(0) != 0) { + const auto types = list("glitter_material_types"); + std::set ids; + for (const auto& [id, type] : colours.types) if (types.contains(type)) ids.insert(id); + for (const auto& id : list("glitter_colors")) if (const auto value = GeneralUtils::TryParse(id)) ids.insert(*value); + for (const auto id : ids) glitter.push_back(id); + } + const nlohmann::json settings{ { "colors", glitter }, + { "tile", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_size").value_or("")).value_or(1.6f) }, + { "flecks", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_density").value_or("")).value_or(50) }, + { "speed", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_speed").value_or("")).value_or(1.0f) } }; reply.status = eHTTPStatusCode::OK; - reply.message = script; + reply.message = colours.script + "window.LDD_GLITTER = " + settings.dump() + ";\n"; reply.contentType = eContentType::TEXT_JAVASCRIPT; reply.headers.push_back("Cache-Control: private, max-age=3600"); }); diff --git a/dDashboardServer/routes/SettingsCatalog.cpp b/dDashboardServer/routes/SettingsCatalog.cpp index e922583e4..84d495e29 100644 --- a/dDashboardServer/routes/SettingsCatalog.cpp +++ b/dDashboardServer/routes/SettingsCatalog.cpp @@ -463,6 +463,15 @@ 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(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(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)); + c.Add(Float(UGC, "satin_whiten", "Satin whitening", "Percent: how far satin colors go towards white.", "20", 0, 100)); c.Add(Bool(UGC, "remove_hidden_faces", "Remove faces nobody can see", "", true)); c.Add(Bool(UGC, "hsr_ground_plane", "Nothing seen from below", "Also removes what can only be seen from under the model.", false)); c.Add(Unit(Int(UGC, "optimize_resolution", "Detail of the visibility renders", "", "1024", 64, 4096), "pixels")); diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index 317e780a8..4ba502a87 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -264,6 +264,10 @@ namespace UgcRoutes { g_IconKinds = LoadIconKinds(); } + std::optional Setting(const std::string& name) { + return UgcSetting(name); + } + void RegisterRoutes() { Route(eHTTPMethod::GET, "/ugc", Perm("properties_view"), "What the UGC server made of players' models", [](HTTPReply& reply, const HTTPContext& context) { RenderPage(reply, context, "ugc.jinja2", "ugc"); }); @@ -478,7 +482,8 @@ 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). Query: ?lod=0 (most detailed) " + "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) " "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); @@ -488,15 +493,25 @@ 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; - Workers::Reply(reply, context, false, [url, lod](HTTPReply& out) { + // The glitter groups' tag: the setting's, and the client's LEGO-AnimUV (21) for models made with another + 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 fetched = CachedGet(url); if (fetched->status != 200) return ReplyError(out, *fetched, url); std::string error; const auto model = NifFile::Parse(fetched->body, lod, error); if (!model) return JsonError(out, eHTTPStatusCode::UNPROCESSABLE_ENTITY, "The .nif can't be read: " + error); + auto looks = Scenery::MultishaderLooks(*model); + 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.shaderTag != glitterTag && material.shaderTag != 21)) continue; + looks[i] |= NifFile::GLITTER; + textures[i] = "glitter"; + } out.status = eHTTPStatusCode::OK; out.contentType = eContentType::APPLICATION_OCTET_STREAM; - out.message = NifFile::Encode(*model, std::vector(model->meshes.size()), {}, Scenery::MultishaderLooks(*model)); + out.message = NifFile::Encode(*model, textures, {}, looks); out.headers.push_back("Cache-Control: private, no-cache"); }); }); diff --git a/dDashboardServer/routes/UgcRoutes.h b/dDashboardServer/routes/UgcRoutes.h index 009fea99a..0600a8485 100644 --- a/dDashboardServer/routes/UgcRoutes.h +++ b/dDashboardServer/routes/UgcRoutes.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + // The UGC page: what the UGC server has made of players' models and modular builds, and making them again namespace UgcRoutes { void RegisterRoutes(); @@ -7,4 +10,7 @@ namespace UgcRoutes { // Reads the client data the UGC page needs (modules, build types) once, on the main thread at startup, so the web // threads never query the CDClient void Preload(); + + // Web thread: a ugcconfig.ini setting's value as the UGC server sees it (dashboard value, file value, default) + std::optional Setting(const std::string& name); } diff --git a/dDashboardServer/static/js/lddviewer.js b/dDashboardServer/static/js/lddviewer.js index 5408eab52..b7eb5a30f 100644 --- a/dDashboardServer/static/js/lddviewer.js +++ b/dDashboardServer/static/js/lddviewer.js @@ -14,7 +14,7 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; -import { parseModel, mergeMeshes, linearColors, nearPlaneFor } from '/js/scenery-core.js'; +import { parseModel, mergeMeshes, linearColors, nearPlaneFor, addGlitter } from '/js/scenery-core.js'; const GEOMETRY_MAGIC = 0x42473031; // "10GB" const MAX_PARALLEL_FETCHES = 6; @@ -174,11 +174,13 @@ 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 +const glitterMaterials = []; function material(id) { if (!materialCache.has(id)) { const c = (window.LDD_MATERIALS || {})[id] || [160, 160, 160, 255]; const transparent = c[3] < 255; - materialCache.set(id, new THREE.MeshPhysicalMaterial({ + const created = new THREE.MeshPhysicalMaterial({ color: new THREE.Color().setRGB(c[0] / 255, c[1] / 255, c[2] / 255, THREE.SRGBColorSpace), roughness: 0.28, metalness: 0, @@ -187,7 +189,14 @@ function material(id) { transparent, opacity: c[3] / 255, depthWrite: !transparent - })); + }); + 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] })); + } + materialCache.set(id, created); } return materialCache.get(id); } @@ -749,6 +758,7 @@ export function createViewer(container, { onProgress, onSelect, onTick, brickUrl const dt = Math.min((now - lastTime) / 1000, 0.1); lastTime = now; if (onTick) onTick(dt); + for (const glitter of glitterMaterials) glitter.update(now / 1000); updateBubbles(now); controls.update(); // The near plane follows how far out the camera is (depth precision for the scenery far away), but never diff --git a/dDashboardServer/static/js/scenery-core.js b/dDashboardServer/static/js/scenery-core.js index 61efd872f..de4a2bf2c 100644 --- a/dDashboardServer/static/js/scenery-core.js +++ b/dDashboardServer/static/js/scenery-core.js @@ -67,7 +67,50 @@ 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 }; +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 for a three.js material (the UGC server's glitter colors, UgcGlitter): white flecks over the color before + * the light, as the game's LEGO-AnimUV shader lays its fleck texture over the vertex color. The flecks are drawn in + * the shader from a hash of cells of `coordinates` ('uv': the mesh's UVs, one tile of the texture each; 'position': + * a box projection of the object's position, `tile` units a tile, for meshes without UVs), `flecks` a tile, moving + * `scroll` tiles a second. Returns {update(seconds)} to animate it. + */ +export function addGlitter(material, { coordinates = 'uv', tile = 1.6, flecks = 50, scroll = [0, 0] } = {}) { + const uniforms = { glitterTime: { value: 0 }, glitterScroll: { value: scroll }, glitterTile: { value: tile }, glitterCells: { value: Math.max(1, Math.sqrt(flecks)) } }; + material.onBeforeCompile = (shader) => { + Object.assign(shader.uniforms, uniforms); + const byPosition = coordinates === 'position'; + shader.vertexShader = 'varying vec3 vGlitterPosition;\nvarying vec3 vGlitterNormal;\n' + (byPosition ? '' : 'attribute vec2 glitterUv;\nvarying vec2 vGlitterUv;\n') + + shader.vertexShader.replace('#include ', '#include \n' + + (byPosition ? `vec4 glitterAt = vec4(transformed, 1.0); +vec3 glitterNormal = objectNormal; +#ifdef USE_INSTANCING +glitterAt = instanceMatrix * glitterAt; +glitterNormal = mat3(instanceMatrix) * glitterNormal; +#endif +vGlitterPosition = glitterAt.xyz; +vGlitterNormal = glitterNormal; +` : 'vGlitterUv = glitterUv;\n')); + shader.fragmentShader = 'uniform float glitterTime;\nuniform vec2 glitterScroll;\nuniform float glitterTile;\nuniform float glitterCells;\nvarying vec3 vGlitterPosition;\nvarying vec3 vGlitterNormal;\n' + + (byPosition ? '' : 'varying vec2 vGlitterUv;\n') + ` +float glitterHash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float glitterFleck(vec2 uv) { + vec2 cell = floor(uv * glitterCells); + vec2 centre = vec2(glitterHash(cell), glitterHash(cell + 17.0)); + float d = length(fract(uv * glitterCells) - centre) * 128.0 / glitterCells; + return (0.65 + 0.35 * glitterHash(cell + 41.0)) * (1.0 - smoothstep(0.0, 1.7, d)); +} +` + shader.fragmentShader.replace('#include ', '#include \n' + (byPosition ? ` + vec3 glitterN = abs(vGlitterNormal); + vec2 glitterUv = (glitterN.x >= glitterN.y && glitterN.x >= glitterN.z ? vGlitterPosition.zy : glitterN.y >= glitterN.z ? vGlitterPosition.xz : vGlitterPosition.xy) / glitterTile; +` : 'vec2 glitterUv = vGlitterUv;\n') + 'diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), glitterFleck(glitterUv + glitterScroll * glitterTime));\n'); + }; + material.customProgramCacheKey = () => 'glitter-' + coordinates; + material.needsUpdate = true; + return { update(seconds) { uniforms.glitterTime.value = seconds; } }; +} /** * How a mesh is drawn under the game's shaders, when the manifest has the zone's lighting: {lit, texture, @@ -112,7 +155,7 @@ export function mergeMeshes(meshes) { for (const mesh of meshes) { if (!mesh.vertices || !mesh.indices.length) continue; const key = JSON.stringify([mesh.texture, mesh.diffuse, mesh.emissive, mesh.alpha, mesh.blend, mesh.test, mesh.doubleSided, - mesh.vertexColors, mesh.clampU, mesh.clampV, !!mesh.normals, !!mesh.uvs, !!mesh.colors, mesh.shaderTag, mesh.darkTexture, !!mesh.uvs2]); + mesh.vertexColors, mesh.clampU, mesh.clampV, !!mesh.normals, !!mesh.uvs, !!mesh.colors, mesh.shaderTag, mesh.darkTexture, !!mesh.uvs2, mesh.look, mesh.uvScroll]); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(mesh); } diff --git a/dDashboardServer/static/js/ugc-viewer.js b/dDashboardServer/static/js/ugc-viewer.js index 0a845aea5..6cfb41bf8 100644 --- a/dDashboardServer/static/js/ugc-viewer.js +++ b/dDashboardServer/static/js/ugc-viewer.js @@ -2,12 +2,13 @@ * A 3D view of a .nif the UGC server made, from /api/ugc/mesh/:id (the dashboard converts it with NifFile::Encode, * as it does the scenery's models), with wireframe and vertex color switches and triangle counts. The metal and glow * groups (the UGC server's shader settings; each mesh's "look" is its shader's eShaderLook bits) are drawn as metal - * reflecting the view's environment and as unlit glow. + * reflecting the view's environment and as unlit glow, the glitter groups with moving white flecks (their UVs and + * uvScroll, as the game moves its fleck texture). */ import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; -import { parseModel, mergeMeshes, linearColors, metalOf, SHADER_LOOK } from '/js/scenery-core.js'; +import { parseModel, mergeMeshes, linearColors, metalOf, addGlitter, SHADER_LOOK } from '/js/scenery-core.js'; export function createNifViewer(container) { const renderer = new THREE.WebGLRenderer({ antialias: true }); @@ -31,6 +32,7 @@ export function createNifViewer(container) { scene.add(root); let parts = []; + const clock = new THREE.Clock(); let wireframe = false, vertexColors = true, disposed = false, framed = false; const grey = new THREE.Color(0xbbbbbb); @@ -49,6 +51,8 @@ export function createNifViewer(container) { function loop() { if (disposed) return; controls.update(); + const seconds = clock.getElapsedTime(); + for (const part of parts) if (part.glitter) part.glitter.update(seconds); renderer.render(scene, camera); requestAnimationFrame(loop); } @@ -125,10 +129,16 @@ export function createNifViewer(container) { ? new THREE.MeshBasicMaterial({ color: baseColor.clone(), vertexColors: hasColors }) : new THREE.MeshStandardMaterial({ color: baseColor.clone(), vertexColors: hasColors, transparent: seeThrough, roughness: metal === 'polished' ? 0.18 : metal === 'brushed' ? 0.45 : 0.6, metalness: metal ? 1 : 0 }); + // Glitter: the fleck texture's UVs, one tile of it each, moving as the game moves it + let glitter = null; + if (look & SHADER_LOOK.GLITTER && mesh.uvs) { + geometry.setAttribute('glitterUv', new THREE.BufferAttribute(mesh.uvs, 2)); + glitter = addGlitter(material, { coordinates: 'uv', scroll: mesh.uvScroll || [0, 0] }); + } const object = new THREE.Mesh(geometry, material); if (seeThrough) object.renderOrder = 1; root.add(object); - parts.push({ mesh: object, hasColors, baseColor }); + parts.push({ mesh: object, hasColors, baseColor, glitter }); triangles += mesh.indices.length / 3; vertices += mesh.vertices; } diff --git a/dUgcServer/CMakeLists.txt b/dUgcServer/CMakeLists.txt index 329fe63d8..5393bf49c 100644 --- a/dUgcServer/CMakeLists.txt +++ b/dUgcServer/CMakeLists.txt @@ -2,6 +2,7 @@ set(DUGC_SOURCES "UgcBricks.cpp" "UgcFormats.cpp" + "UgcGlitter.cpp" "UgcIconParams.cpp" "UgcIconPose.cpp" "UgcJobs.cpp" @@ -40,5 +41,5 @@ target_link_libraries(UgcServer ${COMMON_LIBRARIES} dWeb dServer dUgc) # Rendering is far too slow unoptimized (minutes a model instead of seconds), so it is optimized in every build type if(NOT MSVC) - set_source_files_properties("UgcRender.cpp" "UgcModel.cpp" "UgcPalette.cpp" PROPERTIES COMPILE_OPTIONS "-O2") + set_source_files_properties("UgcRender.cpp" "UgcModel.cpp" "UgcPalette.cpp" "UgcGlitter.cpp" PROPERTIES COMPILE_OPTIONS "-O2") endif() diff --git a/dUgcServer/UgcFormats.cpp b/dUgcServer/UgcFormats.cpp index c688c6722..28f114a40 100644 --- a/dUgcServer/UgcFormats.cpp +++ b/dUgcServer/UgcFormats.cpp @@ -138,7 +138,8 @@ namespace { out.I32(-1); // collision object } - std::string TriShapeData(const UgcModel::Mesh& mesh) { + // `glitter`: with a UV set projected for the glitter texture (UgcGlitter::Uv) + std::string TriShapeData(const UgcModel::Mesh& mesh, const UgcGlitter::Params* glitter = nullptr) { Writer out; const auto count = static_cast(mesh.positions.size()); out.I32(0); // group ID @@ -154,8 +155,9 @@ namespace { min = glm::min(min, p); max = glm::max(max, p); } - out.U16(0); // data flags: no texture coordinates or tangents const bool normals = mesh.normals.size() == mesh.positions.size(); + const bool uvs = glitter && normals; + out.U16(uvs ? 1 : 0); // data flags: the number of UV sets, no tangents out.U8(normals ? 1 : 0); if (normals) { for (const auto& n : mesh.normals) { @@ -181,6 +183,13 @@ namespace { out.Float(std::clamp(c.a, 0.0f, 1.0f)); } } + if (uvs) { + for (size_t v = 0; v < mesh.positions.size(); v++) { + const auto uv = UgcGlitter::Uv(mesh.positions[v], mesh.normals[v], glitter->tile); + out.Float(uv.x); + out.Float(uv.y); + } + } out.U16(0x4000); // consistency: static out.I32(-1); // additional data const auto triangles = static_cast(mesh.indices.size() / 3); @@ -226,9 +235,134 @@ namespace { return std::move(material.Data()); } + /** + * 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). + */ + 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) + const auto source = m_Nif.Reserve("NiSourceTexture"); + Writer pixels; + pixels.U32(1); // RGBA + pixels.U8(32); // bits per pixel + pixels.U32(0xFFFFFFFF); // renderer hint + pixels.U32(0); // extra data + pixels.U8(1); // flags + pixels.U32(0); // tiling + pixels.U8(0); // sRGB + for (const uint32_t channel : { 2u, 1u, 0u, 3u }) { // blue, green, red, alpha + pixels.U32(channel); + pixels.U32(0); // convention: fixed + pixels.U8(8); + 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; + 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(offset); + offset += static_cast(mipmaps[level].size()) * 4; + } + pixels.U32(offset); // pixels + pixels.U32(offset); // padded + pixels.U32(1); // faces + pixels.U32(3); // platform: DX9 + for (const auto& level : mipmaps) { + for (const auto a : level) { + pixels.U8(255); + pixels.U8(255); + pixels.U8(255); + pixels.U8(a); + } + } + const auto pixelData = m_Nif.Add("NiPersistentSrcTextureRendererData", std::move(pixels.Data())); + Writer texture; + WriteNet(texture, -1); + texture.U8(0); // stored in the file + texture.I32(m_Nif.String("ugc_glitter.dds")); + texture.I32(pixelData); + texture.U32(6); // pixel layout: default + texture.U32(2); // mipmaps: default + texture.U32(3); // alpha: default + texture.U8(1); // static + texture.U8(0); // direct render + texture.U8(1); // persist render data + m_Nif.Fill(source, std::move(texture.Data())); + + 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.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); + 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_Glitter; + } + // 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 - int32_t Shape(const std::string& name, const UgcModel::Mesh* mesh, bool transparent, float emissive = 0.0f) { + // 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) { if (!mesh || mesh->Empty() || mesh->positions.size() > 65535 || mesh->TriangleCount() > 65535) return -1; // 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 @@ -251,8 +385,9 @@ namespace { material = it->second; } std::vector properties{ material, m_Alpha, m_Specular, m_VertexColor }; + if (glitter) properties.push_back(GlitterTexturing(*glitter)); const auto shapeBlock = m_Nif.Reserve("NiTriShape"); - const auto dataBlock = m_Nif.Add("NiTriShapeData", TriShapeData(*mesh)); + const auto dataBlock = m_Nif.Add("NiTriShapeData", TriShapeData(*mesh, glitter)); Writer tri; WriteAv(tri, m_Nif.String(name), properties, SHAPE_FLAGS); tri.I32(dataBlock); @@ -271,6 +406,7 @@ namespace { int32_t m_Alpha{ -1 }; int32_t m_Specular{ -1 }; std::map m_Emissive; // emissive color -> its material + int32_t m_Glitter{ -1 }; // the glitter groups' NiTexturingProperty }; } @@ -304,7 +440,7 @@ 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); + const auto block = properties.Shape(group.name, piece, group.transparent, group.emissive, group.glitter); if (block >= 0) shapes.push_back(block); } nif.Fill(level, NodeData(nif.String(lod.name), shapes)); diff --git a/dUgcServer/UgcFormats.h b/dUgcServer/UgcFormats.h index e33ff4a8d..5302394d4 100644 --- a/dUgcServer/UgcFormats.h +++ b/dUgcServer/UgcFormats.h @@ -7,6 +7,7 @@ #include #include +#include "UgcGlitter.h" #include "UgcModel.h" #include "UgcRender.h" @@ -43,13 +44,19 @@ 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 + // (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. + const UgcGlitter::Params* glitter{}; }; /** * The layout LU Toolbox exports (setup_lod_data) and the game's own brick models (res/BrickModels/ndmade) have: * the root node, an NiLODNode per group with NiRangeLODData holding each level's distances, a node per level and * the level's shapes under it, named like the group. Properties as WriteNif; a group with - * an emissive color gets a material of its own. + * an emissive color gets a material of its own, a glitter group the glitter texture's NiTexturingProperty (one + * for every glitter group of the file). */ std::string WriteLodNif(const std::string& rootName, const std::vector& groups); diff --git a/dUgcServer/UgcGlitter.cpp b/dUgcServer/UgcGlitter.cpp new file mode 100644 index 000000000..165e033d0 --- /dev/null +++ b/dUgcServer/UgcGlitter.cpp @@ -0,0 +1,73 @@ +#include "UgcGlitter.h" + +#include +#include + +namespace UgcGlitter { + std::vector FleckAlpha(uint32_t flecks) { + constexpr int N = TEXTURE_SIZE; + std::vector alpha(static_cast(N) * N, 0.0f); + // SplitMix64 from a fixed seed: the same texture on every platform + uint64_t state = 0x6C69747465720000ull; + const auto next = [&state] { + uint64_t z = (state += 0x9E3779B97F4A7C15ull); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBull; + return static_cast((z ^ (z >> 31)) >> 40) / static_cast(1ull << 24); + }; + for (uint32_t i = 0; i < flecks; i++) { + const float cx = next() * N, cy = next() * N; + const float radius = 1.2f + next() * 1.0f; + const float peak = 0.65f + next() * 0.35f; + const int reach = static_cast(std::ceil(radius)); + 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; + const float d = std::sqrt(ddx * ddx + ddy * ddy) / radius; + if (d >= 1.0f) continue; + auto& value = alpha[static_cast(((y % N) + N) % N) * N + ((x % N) + N) % N]; + value = std::max(value, peak * (1.0f - d * d)); + } + } + } + std::vector out(alpha.size()); + for (size_t i = 0; i < alpha.size(); i++) out[i] = static_cast(std::lround(std::clamp(alpha[i], 0.0f, 1.0f) * 255.0f)); + return out; + } + + std::vector> Mipmaps(const std::vector& alpha) { + std::vector> levels{ alpha }; + for (int size = TEXTURE_SIZE / 2; size >= 1; size /= 2) { + const auto& above = levels.back(); + const int from = size * 2; + std::vector level(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); + } + } + levels.push_back(std::move(level)); + } + return levels; + } + + glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile) { + const auto a = glm::abs(normal); + const float scale = 1.0f / std::max(tile, 1e-3f); + if (a.x >= a.y && a.x >= a.z) return glm::vec2(position.z, position.y) * scale; + if (a.y >= a.z) return glm::vec2(position.x, position.z) * scale; + return glm::vec2(position.x, position.y) * scale; + } + + 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 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; + const auto at = [&](int px, int py) { return alpha[static_cast(((py % N) + N) % N) * N + ((px % N) + N) % N] / 255.0f; }; + return (at(x0, y0) * (1 - fx) + at(x0 + 1, y0) * fx) * (1 - fy) + (at(x0, y0 + 1) * (1 - fx) + at(x0 + 1, y0 + 1) * fx) * fy; + } +} diff --git a/dUgcServer/UgcGlitter.h b/dUgcServer/UgcGlitter.h new file mode 100644 index 000000000..f3d23c838 --- /dev/null +++ b/dUgcServer/UgcGlitter.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#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, drifting as the texture transform's translation loops. Pure. + */ +namespace UgcGlitter { + // The 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) + 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 + + // 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; } + 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. + 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); + + // A vertex's UV: its position on the axis plane its normal faces most, in tiles + glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile); + + // The texture's alpha (0..1) at `uv` (wrapping, bilinear) + float Sample(const std::vector& alpha, const glm::vec2& uv); +} diff --git a/dUgcServer/UgcJobs.cpp b/dUgcServer/UgcJobs.cpp index 1d7b1c57a..6139ef489 100644 --- a/dUgcServer/UgcJobs.cpp +++ b/dUgcServer/UgcJobs.cpp @@ -78,6 +78,35 @@ namespace UgcJobs { for (size_t look = 0; look < UgcModel::LOOK_COUNT; look++) pieces[look] = UgcModel::Divide((*split)[look]); return pieces; } + + // A model's transparent bricks as the .nif's shapes: one per brick (or all together, `combine`), the glitter + // ones apart when `glitterApart` + std::array, 2> DivideTransparent(const UgcModel::Model& model, bool combine, bool glitterApart) { + std::array, 2> out; + std::array separate{}; + separate[static_cast(UgcModel::eLook::GLITTER)] = glitterApart; + const auto glitter = static_cast(UgcModel::eLook::GLITTER); + if (combine) { + const auto split = UgcModel::SplitLooks(model.transparent, separate); + if (!split) { + out[0] = UgcModel::Divide(model.transparent); + } else { + out[0] = UgcModel::Divide((*split)[0]); + out[1] = UgcModel::Divide((*split)[glitter]); + } + return out; + } + for (auto& piece : UgcModel::SplitAt(model.transparent, model.transparentBricks)) { + const auto split = UgcModel::SplitLooks(piece, separate); + if (!split) { + out[0].push_back(std::move(piece)); + continue; + } + if (!(*split)[0].Empty()) out[0].push_back(std::move((*split)[0])); + if (!(*split)[glitter].Empty()) out[1].push_back(std::move((*split)[glitter])); + } + return out; + } } uint32_t Shaders::TagOf(UgcModel::eLook look) const { @@ -85,23 +114,26 @@ namespace UgcJobs { case UgcModel::eLook::METAL: return metal; case UgcModel::eLook::BRUSHED: return brushed; case UgcModel::eLook::GLOW: return glow; + case UgcModel::eLook::GLITTER: return glitter; default: return 0; } } std::map Shaders::TagLooks() const { - std::map looks{ { 88, UgcModel::eLook::METAL }, { 89, UgcModel::eLook::BRUSHED }, { 46, UgcModel::eLook::GLOW } }; - for (const auto look : { UgcModel::eLook::METAL, UgcModel::eLook::BRUSHED, UgcModel::eLook::GLOW }) { + std::map looks{ { 88, UgcModel::eLook::METAL }, { 89, UgcModel::eLook::BRUSHED }, { 46, UgcModel::eLook::GLOW }, + { 21, UgcModel::eLook::GLITTER } }; + for (const auto look : { UgcModel::eLook::METAL, UgcModel::eLook::BRUSHED, UgcModel::eLook::GLOW, UgcModel::eLook::GLITTER }) { if (const auto tag = TagOf(look); tag != 0) looks[static_cast(tag)] = look; } return looks; } std::string ShapeName(const Settings& settings, UgcModel::eLook look, bool transparent) { - if (transparent) return "S01_Alpha_Model"; + if (transparent && look != UgcModel::eLook::GLITTER) return "S01_Alpha_Model"; if (look == UgcModel::eLook::PLASTIC) return ("S" + settings.shaderOpaque + "_Opaque_Model").substr(0, 60); const auto tag = std::to_string(settings.shaders.TagOf(look)); - const char* name = look == UgcModel::eLook::METAL ? "_Metal_Model" : look == UgcModel::eLook::BRUSHED ? "_Brushed_Model" : "_Glow_Model"; + const char* name = look == UgcModel::eLook::METAL ? "_Metal_Model" : look == UgcModel::eLook::BRUSHED ? "_Brushed_Model" : + look == UgcModel::eLook::GLOW ? "_Glow_Model" : transparent ? "_GlitterAlpha_Model" : "_Glitter_Model"; return ("S" + std::string(tag.size() < 2 ? "0" : "") + tag + name).substr(0, 60); } @@ -150,7 +182,9 @@ namespace UgcJobs { // Every LOD made like LU Toolbox makes each LOD collection: colored, hidden faces removed, lighting baked, divided std::vector models; std::vector opaquePieces; - std::vector> transparentPieces; + // Per level: the transparent bricks' pieces, plastic and (with the glitter group on) glitter + using TransparentPieces = std::array, 2>; + std::vector transparentPieces; // The looks with a shader of their own (UgcJobs::Shaders), each an NiLODNode apart from the plastic std::array separate{}; for (size_t look = 1; look < UgcModel::LOOK_COUNT; look++) separate[look] = settings.shaders.TagOf(static_cast(look)) != 0; @@ -198,11 +232,11 @@ namespace UgcJobs { entry["opaqueAfter"] = model.opaque.TriangleCount(); entry["vertices"] = model.opaque.positions.size() + model.transparent.positions.size(); opaquePieces.push_back(DivideByLook(model.opaque, separate)); - transparentPieces.push_back(settings.combineTransparent ? UgcModel::Divide(model.transparent) : UgcModel::SplitAt(model.transparent, model.transparentBricks)); - size_t shapes = transparentPieces.back().size(); + transparentPieces.push_back(DivideTransparent(model, settings.combineTransparent, separate[static_cast(UgcModel::eLook::GLITTER)])); + size_t shapes = transparentPieces.back()[0].size() + transparentPieces.back()[1].size(); for (const auto& pieces : opaquePieces.back()) shapes += pieces.size(); entry["shapes"] = shapes; - // Triangles per group (NiLODNode) when metal or glow have groups of their own + // Triangles per group (NiLODNode) when metal, glow or glitter have groups of their own if (std::find(separate.begin(), separate.end(), true) != separate.end()) { auto& byGroup = entry["groups"] = nlohmann::json::object(); for (size_t look = 0; look < UgcModel::LOOK_COUNT; look++) { @@ -210,7 +244,11 @@ namespace UgcJobs { for (const auto& piece : opaquePieces.back()[look]) triangles += piece.TriangleCount(); if (triangles > 0) byGroup[ShapeName(settings, static_cast(look), false)] = triangles; } - if (!model.transparent.Empty()) byGroup[ShapeName(settings, UgcModel::eLook::PLASTIC, true)] = model.transparent.TriangleCount(); + for (size_t kind = 0; kind < 2; kind++) { + size_t triangles = 0; + 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; + } } lodStats.push_back(entry); } @@ -218,17 +256,20 @@ namespace UgcJobs { // An NiLODNode for the opaque bricks and one for the transparent ones, as LU Toolbox names them, and one for // each look with a shader of its own between them. Every group has every level (empty where it has nothing). - const auto groups = [&](size_t levels, const std::vector& opaque, const std::vector>& transparent) { + // Transparent glitter last, after the plain transparent bricks. + const auto groups = [&](size_t levels, const std::vector& opaque, const std::vector& transparent) { std::vector out; - for (size_t kind = 0; kind <= UgcModel::LOOK_COUNT; kind++) { - const bool isTransparent = kind == UgcModel::LOOK_COUNT; - const auto look = isTransparent ? UgcModel::eLook::PLASTIC : static_cast(kind); + for (size_t kind = 0; kind < UgcModel::LOOK_COUNT + 2; kind++) { + const bool isTransparent = kind >= UgcModel::LOOK_COUNT; + const bool glitter = kind == UgcModel::LOOK_COUNT + 1 || kind == static_cast(UgcModel::eLook::GLITTER); + const auto look = glitter ? UgcModel::eLook::GLITTER : isTransparent ? UgcModel::eLook::PLASTIC : static_cast(kind); UgcFormats::NifLodGroup group{ ShapeName(settings, look, isTransparent), isTransparent, {} }; if (look == UgcModel::eLook::GLOW) group.emissive = std::max(settings.shaders.glowEmissive, 0.0f); + if (glitter) group.glitter = &settings.shaders.glitterParams; 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 : (isTransparent ? transparent[i] : opaque[i][kind])) lod.pieces.push_back(&piece); + for (const auto& piece : (isTransparent ? transparent[i][glitter ? 1 : 0] : opaque[i][kind])) lod.pieces.push_back(&piece); any = any || !lod.pieces.empty(); group.lods.push_back(std::move(lod)); } @@ -242,7 +283,7 @@ namespace UgcJobs { AddDownload(outcome.files, "model.nif", nif); { const std::vector opaque{ DivideByLook(preview.opaque, separate) }; - const std::vector> transparent{ transparentPieces[0] }; + const std::vector transparent{ transparentPieces[0] }; outcome.files["model.noao.nif.gz"] = ZCompression::Gzip(UgcFormats::WriteLodNif("SceneNode_Model", groups(1, opaque, transparent))); } diff --git a/dUgcServer/UgcJobs.h b/dUgcServer/UgcJobs.h index b62597a37..9bdbc50bb 100644 --- a/dUgcServer/UgcJobs.h +++ b/dUgcServer/UgcJobs.h @@ -23,20 +23,23 @@ namespace UgcJobs { * parity table in docs/UgcServer.md. */ /** - * The shaders of the metal and glow colors (UgcModel::eLook): the mapShaders id each look's own NiLODNode names - * (S88_Metal_Model, ...), 0 for none (the colors stay in S01_Opaque_Model, as on live). Not what live did: live's - * models are all S01 (docs/UgcServer.md, "Metal and glow"). + * The shaders of the metal, glow and glitter colors (UgcModel::eLook): the mapShaders id each look's own NiLODNode + * names (S88_Metal_Model, ...), 0 for none (the colors stay in S01_Opaque_Model, or S01_Alpha_Model when + * transparent, as on live). Not what live did: live's models are all S01 (docs/UgcServer.md, "Metal and glow"). */ struct Shaders { uint32_t metal{}; // shader_metal: 88 Polished Metal 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) 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 // The mapShaders id of a look's group, 0 for the plastic S01_Opaque_Model uint32_t TagOf(UgcModel::eLook look) const; // Multishader tag -> look, for reading the looks back out of a .nif (the icon): these settings' ids, and the - // client's Polished Metal (88), Brushed Steel (89) and LEGO-Emissive (46) for .nifs made with other settings + // 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; }; @@ -81,7 +84,8 @@ namespace UgcJobs { const std::map& tagLooks = {}); // 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 (the ids from the settings), at most 60 characters as LU Toolbox cuts them + // 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); // How many bricks (parts) an LXFML has, counted cheaply (for the memory estimate before a job starts) diff --git a/dUgcServer/UgcModel.cpp b/dUgcServer/UgcModel.cpp index 523ffcd43..80b7cdb5a 100644 --- a/dUgcServer/UgcModel.cpp +++ b/dUgcServer/UgcModel.cpp @@ -223,7 +223,7 @@ namespace UgcModel { Model model; std::set missing; const bool luToolbox = options.palette == ePalette::LU_TOOLBOX; - bool anyGlow = false, anyLook = false; + bool anyGlow = false, anyLook = false, anyTransparentLook = false; for (uint32_t brick = 0; brick < parts.size(); brick++) { const auto& part = parts[brick]; const auto design = library.GetDesign(part.designId, options.lod); @@ -283,10 +283,16 @@ namespace UgcModel { linear = UgcPalette::ApplyVariation(linear, variation, UgcPalette::BrickRandom(options.seed, brick, colorId)); } if (!options.icon && options.brightness != 100.0f) linear *= std::max(options.brightness, 0.0f) / 100.0f; + if (options.satinColors.contains(colorId)) { + // Satin: milky, and less see-through than clear plastic + linear = glm::mix(linear, glm::vec3(1.0f), std::clamp(options.satinWhiten / 100.0f, 0.0f, 1.0f)); + if (transparent) alpha = std::clamp(options.satinOpacity / 100.0f, 0.0f, 1.0f); + } const glm::vec4 color(UgcPalette::LinearToSrgb(linear), alpha); anyGlow = anyGlow || glow != glm::vec3(0.0f); - const auto look = transparent ? eLook::PLASTIC : LookOf(colorId, library.GetMaterial(colorId), options.looks); - anyLook = anyLook || look != eLook::PLASTIC; + auto look = LookOf(colorId, library.GetMaterial(colorId), options.looks); + if (transparent && look != eLook::GLITTER) look = eLook::PLASTIC; + (transparent ? anyTransparentLook : anyLook) = (transparent ? anyTransparentLook : anyLook) || look != eLook::PLASTIC; const auto base = static_cast(mesh.positions.size()); const size_t vertexCount = geometry.positions.size() / 3; for (size_t v = 0; v < vertexCount; v++) { @@ -298,10 +304,8 @@ namespace UgcModel { mesh.positions.push_back(glm::vec3(part.transform * glm::vec4(position, 1.0f))); mesh.normals.push_back(normal); mesh.colors.push_back(color); - if (&mesh == &model.opaque) { - model.opaque.glow.push_back(glow); - model.opaque.looks.push_back(look); - } + if (&mesh == &model.opaque) model.opaque.glow.push_back(glow); + mesh.looks.push_back(look); } for (const auto i : geometry.indices) mesh.indices.push_back(base + i); } @@ -309,6 +313,7 @@ namespace UgcModel { if (!anyGlow) model.opaque.glow.clear(); else model.opaque.glow.resize(model.opaque.positions.size(), glm::vec3(0.0f)); if (!anyLook) model.opaque.looks.clear(); + if (!anyTransparentLook) model.transparent.looks.clear(); model.missingDesigns.assign(missing.begin(), missing.end()); return model; } @@ -376,7 +381,7 @@ namespace UgcModel { // Blending only shows where something is see-through (the game's brick models blend every shape) bool seeThrough = source.material.alphaBlend && source.material.alpha < 0.99f; for (size_t v = 0; source.material.alphaBlend && !seeThrough && v < mesh.colors.size(); v++) seeThrough = mesh.colors[v].a < 0.99f; - if (const auto look = tagLooks.find(source.material.shaderTag); !seeThrough && look != tagLooks.end() && look->second != eLook::PLASTIC) { + if (const auto look = tagLooks.find(source.material.shaderTag); look != tagLooks.end() && look->second != eLook::PLASTIC && (!seeThrough || look->second == eLook::GLITTER)) { mesh.looks.assign(mesh.positions.size(), look->second); } (seeThrough ? model.transparent : model.opaque).Append(mesh); diff --git a/dUgcServer/UgcModel.h b/dUgcServer/UgcModel.h index 73ed97263..d2e26c766 100644 --- a/dUgcServer/UgcModel.h +++ b/dUgcServer/UgcModel.h @@ -37,18 +37,19 @@ namespace UgcModel { bool HasNoBricks(std::string_view lxfml); /** - * How an opaque color looks in the game when the UGC server's shader settings give it a shader of its own - * (docs/UgcServer.md, "Metal and glow"): the LEGO plastic of S01_Opaque_Model, polished metal, brushed steel or glow. + * How a color looks in the game when the UGC server's shader settings give it a shader of its own + * (docs/UgcServer.md, "Metal and glow"): the LEGO plastic of S01_Opaque_Model, polished metal, brushed steel, glow + * or glitter. Transparent bricks are plastic or glitter. */ - enum class eLook : uint8_t { PLASTIC = 0, METAL, BRUSHED, GLOW }; - constexpr size_t LOOK_COUNT = 4; + enum class eLook : uint8_t { PLASTIC = 0, METAL, BRUSHED, GLOW, GLITTER }; + constexpr size_t LOOK_COUNT = 5; struct Mesh { std::vector positions; std::vector normals; std::vector colors; // sRGB, 0..1, alpha is opacity std::vector glow; // linear glow color per vertex (LU Toolbox's "Glow" layer); empty when nothing glows - std::vector looks; // per vertex; empty when everything is plastic + std::vector looks; // per vertex; empty when everything is plastic (transparent meshes: plastic or glitter) std::vector indices; size_t TriangleCount() const { return indices.size() / 3; } @@ -77,11 +78,12 @@ namespace UgcModel { /** * Which colors have which look, from the client's data: a Materials.xml MaterialType (brickdb.zip) and LU Toolbox's * metallic and glow colors (UgcPalette), and colors named in the settings. A named color wins, then glow over metal; - * transparent bricks are always plastic. + * transparent bricks are plastic unless their color is glitter. */ struct LookRules { - std::map colors; // LEGO color ids given a look by the settings (brushed_colors) - std::map materialTypes{ { "shinySteel", eLook::METAL }, { "brushedSteel", eLook::BRUSHED }, { "matteSteel", eLook::BRUSHED } }; + std::map colors; // LEGO color ids given a look by the settings (brushed_colors, glitter_colors) + std::map materialTypes{ { "shinySteel", eLook::METAL }, { "brushedSteel", eLook::BRUSHED }, { "matteSteel", eLook::BRUSHED }, + { "glitter", eLook::GLITTER } }; bool paletteMetallic{ true }; // LU Toolbox's Metallic colors (UgcPalette::IsMetallic) are METAL bool paletteGlow{ true }; // its glow colors (UgcPalette::Glow) are GLOW }; @@ -99,6 +101,12 @@ namespace UgcModel { bool icon{}; // the icon renderer's color corrections uint32_t lod{}; // brickprimitives level LookRules looks; // which colors are metal and glow (Mesh::looks) + // Satin (opal) colors: transparent bricks of these colors get satinOpacity (percent) as their vertex alpha + // instead of the transparent opacity, and every brick of them has its color moved satinWhiten percent towards + // white (milky). The client has no satin shader: they stay in the transparent group. Empty: none. + std::set satinColors; + float satinOpacity{ 75.0f }; + float satinWhiten{ 20.0f }; }; /** @@ -131,7 +139,8 @@ namespace UgcModel { std::vector Divide(const Mesh& mesh, size_t maxVertices = 65535, size_t maxTriangles = 65535); // 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 + // 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 = {}); /** diff --git a/dUgcServer/UgcRender.cpp b/dUgcServer/UgcRender.cpp index 5a53dd9f0..37b636022 100644 --- a/dUgcServer/UgcRender.cpp +++ b/dUgcServer/UgcRender.cpp @@ -461,6 +461,11 @@ namespace UgcRender { }; const glm::vec3 toCamera = glm::normalize(dir); const glm::vec3 halfway = glm::normalize(light + toCamera); + bool anyGlitter = false; + for (const auto* mesh : { &model.opaque, &model.transparent }) { + anyGlitter = anyGlitter || std::find(mesh->looks.begin(), mesh->looks.end(), UgcModel::eLook::GLITTER) != mesh->looks.end(); + } + const auto glitterAlpha = anyGlitter ? UgcGlitter::FleckAlpha(options.glitter.flecks) : std::vector{}; const auto shade = [&](const UgcModel::Mesh& mesh, bool isOpaque, uint32_t i0, uint32_t i1, uint32_t i2, float w0, float w1, float w2) { glm::vec3 normal(0.0f, 1.0f, 0.0f); @@ -483,8 +488,13 @@ namespace UgcRender { // The sun's highlight (Blinn-Phong), white, on top of the color const float highlight = direct > 0.0f ? options.specular * options.sunStrength / 3.14159265f * sun * std::pow(std::max(0.0f, glm::dot(normal, halfway)), std::max(options.shininess, 1.0f)) : 0.0f; const float exposure = std::max(options.exposure, 0.0f); - const auto look = isOpaque && mesh.looks.size() == mesh.positions.size() ? mesh.looks[i0] : UgcModel::eLook::PLASTIC; - if (look == UgcModel::eLook::PLASTIC) { + const auto look = mesh.looks.size() == mesh.positions.size() && (isOpaque || mesh.looks[i0] == UgcModel::eLook::GLITTER) ? mesh.looks[i0] : UgcModel::eLook::PLASTIC; + if (look == UgcModel::eLook::GLITTER) { + // LEGO-AnimUV: lerp(vertex color, the texture's white, its alpha), then lit as plastic + const float fleck = UgcGlitter::Sample(glitterAlpha, UgcGlitter::Uv(position, normal, options.glitter.tile)); + base = glm::vec4(glm::mix(glm::vec3(base), glm::vec3(1.0f), fleck), base.a); + } + if (look == UgcModel::eLook::PLASTIC || look == UgcModel::eLook::GLITTER) { return glm::vec4((ToLinear(base.r) * lighting + highlight) * exposure, (ToLinear(base.g) * lighting + highlight) * exposure, (ToLinear(base.b) * lighting + highlight) * exposure, std::clamp(base.a, 0.0f, 1.0f)); } diff --git a/dUgcServer/UgcRender.h b/dUgcServer/UgcRender.h index 65d412520..5246f55ca 100644 --- a/dUgcServer/UgcRender.h +++ b/dUgcServer/UgcRender.h @@ -5,6 +5,7 @@ #include +#include "UgcGlitter.h" #include "UgcModel.h" /** @@ -56,6 +57,7 @@ namespace UgcRender { float shadows{ 1.0f }; // how much the sun's shadows darken, 0 to 1 AoOptions ao{ false, 5.0f, 32, 1.0f, 0.0f }; float glowEmissive{ 1.0f }; // how far glowing shapes go from lit to their plain color (the glow_emissive setting) + UgcGlitter::Params glitter; // the glitter's flecks (glitter_size, glitter_density), drawn where they are at the start }; // The model drawn from the icon's camera, framed to fit, on a transparent background. `opaqueAo`: the opaque mesh's @@ -63,7 +65,8 @@ namespace UgcRender { // Opaque vertices with a look (UgcModel::Mesh::looks) are drawn roughly as the game's shaders draw them: GLOW goes // from lit to its plain color by glowEmissive (LEGO-Emissive), METAL and BRUSHED dim the diffuse light and add a // sky-and-ground reflection tinted by the color and a highlight, sharp for polished metal and broad for brushed - // steel (Polished Metal, Brushed Steel: an environment map tinted by the vertex color). + // steel (Polished Metal, Brushed Steel: an environment map tinted by the vertex color). GLITTER vertices (opaque or + // transparent) get the glitter texture's white flecks over their color before the light (LEGO-AnimUV), still. Image RenderIcon(const UgcModel::Model& model, const IconOptions& options, const std::vector* opaqueAo = nullptr); struct OptimizeOptions { diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index 9d17976af..087b579a3 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -62,6 +62,19 @@ namespace { return GeneralUtils::TryParse(Game::config->GetValue(key)).value_or(fallback); } + // A list of color ids setting: empty is `fallback`, "none" (or anything without ids) is no colors + std::set ColorList(const std::string& key, const std::string& fallback) { + const auto value = Game::config->GetValue(key); + std::set colors; + std::stringstream stream(value.empty() ? fallback : value); + std::string id; + while (std::getline(stream, id, ',')) { + std::erase_if(id, [](unsigned char c) { return std::isspace(c); }); + if (const auto color = GeneralUtils::TryParse(id)) colors.insert(*color); + } + return colors; + } + std::vector ParseLods(const std::string& text) { std::vector lods; std::stringstream stream(text); @@ -104,9 +117,14 @@ 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); - settings.icon.glowEmissive = settings.shaders.glowEmissive; - // Which Materials.xml MaterialTypes are metal and brushed steel - for (const auto& [key, look] : { std::pair{ "metal_material_types", UgcModel::eLook::METAL }, std::pair{ "brushed_material_types", UgcModel::eLook::BRUSHED } }) { + // Glitter colors in S_Glitter_Model and S_GlitterAlpha_Model with drifting flecks (LEGO-AnimUV) + settings.shaders.glitter = std::min(Setting("shader_glitter", 21), 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); + // 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 }, + std::pair{ "glitter_material_types", UgcModel::eLook::GLITTER } }) { const auto value = Game::config->GetValue(key); if (value.empty()) continue; std::erase_if(settings.build.looks.materialTypes, [look](const auto& entry) { return entry.second == look; }); @@ -117,17 +135,15 @@ namespace { if (!type.empty() && type != "none") settings.build.looks.materialTypes[type] = look; } } - // LEGO color ids drawn as brushed steel whatever their Materials.xml type - { - // Empty: the default (the drum lacquered colors); none: no colors - const auto value = Game::config->GetValue("brushed_colors"); - std::stringstream stream(value.empty() ? "298,300,1002,1004" : value); - std::string id; - while (std::getline(stream, id, ',')) { - std::erase_if(id, [](unsigned char c) { return std::isspace(c); }); - if (const auto color = GeneralUtils::TryParse(id)) settings.build.looks.colors[*color] = UgcModel::eLook::BRUSHED; - } - } + // LEGO color ids drawn as brushed steel (by default the drum lacquered colors) and as glitter (by default the + // ones LEGO's own color data files as glitter that the client's Materials.xml calls shinyPlastic) whatever their + // Materials.xml type; empty: the default, none: no colors + for (const auto color : ColorList("brushed_colors", "298,300,1002,1004")) settings.build.looks.colors[color] = UgcModel::eLook::BRUSHED; + for (const auto color : ColorList("glitter_colors", "114,117")) settings.build.looks.colors[color] = UgcModel::eLook::GLITTER; + // Satin (opal) colors: milky and less see-through in the transparent group (the client has no satin shader) + settings.build.satinColors = ColorList("satin_colors", "360,362,363,364,365,366,367,376"); + settings.build.satinOpacity = std::clamp(Setting("satin_opacity", 75.0f), 0.0f, 100.0f); + settings.build.satinWhiten = std::clamp(Setting("satin_whiten", 20.0f), 0.0f, 100.0f); settings.optimize.removeHidden = Setting("remove_hidden_faces", 1) != 0; settings.optimize.groundPlane = Setting("hsr_ground_plane", 0) != 0; settings.optimize.resolution = Setting("optimize_resolution", 1024); @@ -142,6 +158,8 @@ namespace { return value.empty() ? std::nullopt : std::optional(value); }); settings.icon.size = Setting("icon_size", 128); + settings.icon.glowEmissive = settings.shaders.glowEmissive; + settings.icon.glitter = settings.shaders.glitterParams; settings.icon.ao.distance = settings.ao.distance; settings.maxBricks = Setting("max_model_bricks", 0); return settings; diff --git a/docs/UgcServer.md b/docs/UgcServer.md index 54c4b669d..5b9d8fd20 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -210,10 +210,10 @@ defaults. The table below goes through it step by step. ### Metal and glow (on by default, not how live looked) Live's models, LU Toolbox's exports and the client's own builder (`LUNifBuilder_BK`, which writes only `S01_Opaque` -and `S01_Alpha`) all draw every brick with the LEGO shader, so metal colors look like grey plastic and glowing colors -like bright plastic. The UGC server gives them the client's metal and emissive shaders by default. Set the three -shader ids to 0 for live's look: off writes exactly the files it wrote before these settings existed (the same bytes, -tested). +and `S01_Alpha`) all draw every brick with the LEGO shader, so metal colors look like grey plastic, glowing colors +like bright plastic and glitter like plain transparent plastic. The UGC server gives them the client's metal, +emissive and animated UV shaders by default. Set the four shader ids to 0 (and `satin_colors` to `none`) for live's +look: off writes exactly the files it wrote before these settings existed (the same bytes, tested). How the client picks the shader (checked in the 1.10.64 client; Ghidra bookmarks under "UGCShaders"): player models (LOT 14, and 6662) have RenderComponent shader 100, mapShaders "Multishader" (gameValue 9999). For a downloaded model @@ -232,16 +232,28 @@ all of its levels, so each look needs a group of its own. | `metal_material_types` | `shinySteel` | Materials.xml `MaterialType`s that are metal (empty: the default; `none`: none). | | `brushed_material_types` | `brushedSteel,matteSteel` | Materials.xml `MaterialType`s that are brushed steel. | | `brushed_colors` | 298,300,1002,1004 (the drum lacquered colors) | LEGO color ids that are brushed steel whatever their type; they win over the metal and glow colors. Empty: the default; `none`: no colors. | +| `shader_glitter` | 21 | `S_Glitter_Model` (opaque) and `S_GlitterAlpha_Model` (transparent) for glitter colors: 21 is LEGO-AnimUV (gameValue 30), see Glitter below. | +| `glitter_material_types` | `glitter` | Materials.xml `MaterialType`s that are glitter. | +| `glitter_colors` | 114,117 | LEGO color ids that are glitter whatever their type (as `brushed_colors`). The default: the two colors LEGO's own color data (Studio's color categories, "Glitter Colors") files as glitter that the client's Materials.xml types `shinyPlastic` (114 Tr. Medium Reddish-Violet w. Glitter, 117 Transparent Glitter). | +| `glitter_size` | 1.6 | The glitter texture's tile, in model units (a stud is 0.8): the flecks' spacing, the same on every brick. | +| `glitter_density` | 50 | Flecks in one tile. | +| `glitter_speed` | 1 | How fast the flecks drift: a tile in U in 7 s and in V in 11 s at 1; 0 keeps them still (no controllers). | +| `satin_colors` | 360,362,363,364,365,366,367,376 | Satin (opal) colors, see Satin below. The default: LEGO's color data's "Satin Colors" category (the Transparent ... Opal colors). Empty: the default; `none`: off. | +| `satin_opacity` | 75 | Percent: the vertex alpha of transparent satin bricks, instead of `transparent_opacity` or the Materials.xml alpha. | +| `satin_whiten` | 20 | Percent: how far satin colors are moved towards white (in linear RGB, after the color variation). | Which color has which look is data, not a list in the code (`UgcModel::LookOf`): glow is LU Toolbox's glow table (`UgcPalette::Glow`: 50, 294, 329, 9000-9027), metal is LU Toolbox's metallic table (`UgcPalette::IsMetallic`) plus -the Materials.xml types above (the clients checked have 8 or 14 `shinySteel` colors, and 1 or 3 `glitter` ones, which stay -plastic, as does pearl: the client has no shader for them). Only opaque bricks get a look: a transparent glowing -color (294 with the brick database palette, alpha 150) stays in `S01_Alpha_Model`. +the Materials.xml types above (the clients checked have 8 or 14 `shinySteel` colors, and 1 or 3 `glitter` ones: 129, +341, 351), glitter is the `glitter` type plus `glitter_colors`. Pearl stays plastic (the client has no shader for it). +Only opaque bricks get metal and glow: a transparent glowing color (294 with the brick database palette, alpha 150) +stays in `S01_Alpha_Model`. Transparent bricks can be glitter (every glitter color the clients have is transparent: +341 and 351 have alpha 150, 129 is in `transparent_colors`). What is written with a group on: per LOD, the opaque bricks are split by look before being divided at 65535 vertices, -and the .nif gets, in order, `S01_Opaque_Model`, `S88_Metal_Model`, `S89_Brushed_Model`, `S46_Glow_Model` and -`S01_Alpha_Model`, each only when it has triangles, and each with every LOD level (an empty `LOD_` node where it has +and the .nif gets, in order, `S01_Opaque_Model`, `S88_Metal_Model`, `S89_Brushed_Model`, `S46_Glow_Model`, +`S21_Glitter_Model`, `S01_Alpha_Model` and `S21_GlitterAlpha_Model` (transparent glitter bricks, one shape per brick +like the other transparent ones, or one with `combine_transparent`), each only when it has triangles, and each with every LOD level (an empty `LOD_` node where it has none there), like the plastic groups. Metal shapes are like plastic ones (white material, no textures, the brick color as vertex color with the lighting baked in). Glow shapes get a material of their own with emissive `glow_emissive`, vertex alpha 1 (the shader's mask) and their plain color, not the baked one: the shader lights them itself, and the @@ -259,6 +271,63 @@ brushed). This is an approximation of the game's environment maps. `NifFile::Sha and draws metal as reflective (metalness 1, the view's environment) and glow unlit, and the zone views draw LEGO-Emissive objects going to their vertex color by its alpha (metal there stays lit like the rest). +#### Glitter + +The client has no glitter shader. LEGO-AnimUV (mapShaders 21, gameValue 30, `LEGOPPLighting.fx` and its `_low`, +`_noenv`, `_noenv_nospec` versions) is the LEGO lighting with the UVs multiplied by `TEXTRANSFORMBASE` (the base map's +texture transform) in the vertex shader. A shape with vertex colors and a base texture gets +`Technique_LEGOPPLightingVertColorTextured_AnimUV` (technique names set up at 0x010ac110), whose pixel shader +(`LEGOPPLighting_PS_VertColorTextured`) is `lerp(vertex color, texture rgb, texture alpha)`, then the LEGO lighting +(`LEGOPP_PixelCommon4`), alpha = vertex alpha times the fade. So a white texture with flecks in its alpha puts white +flecks on a brick that is otherwise lit as plastic, and moving the texture transform moves them. + +What a glitter shape has, beside what plastic shapes have (white material, alpha, specular, vertex colors): + +- A UV set: each vertex's position on the axis plane its normal faces most, divided by `glitter_size` + (`UgcGlitter::Uv`), so the flecks are as dense on every brick and every side. +- An `NiTexturingProperty` (one per file, shared by both glitter groups): apply mode decal (fixed function would do + what the shader does), 9 slots, the base map only: wrap S and T, trilinear, UV set 0, a texture transform + (translation 0, scale 1, Maya method, center 0.5). +- Its source, stored in the file as the client's own animated textures store theirs + (`res/mesh/env/env_ag_ocean-maelstrom.nif`, RenderComponent 14356): `NiSourceTexture` (use external 0, name + `ugc_glitter.dds`, pixel layout 6, mipmaps 2, alpha 3, static, persist render data) and + `NiPersistentSrcTextureRendererData`: RGBA 32 bit, channels blue, green, red, alpha, platform DX9, 128 x 128 with 8 + mipmaps. RGB is white; the alpha is `glitter_density` soft dots (radius 1.2 to 2.2 px, peak 0.65 to 1) at places + from a fixed seed, wrapping at the edges (`UgcGlitter::FleckAlpha`), each mipmap the 2x2 mean of the one above. +- Two `NiTextureTransformController`s on the property (the property's controller, the first linking the second): + flags 0x48 (active, loop, app time), frequency 1, phase 0, start 0, stop the period, target the property, + base map, operation translate U and translate V, each with an `NiFloatInterpolator` and `NiFloatData` of two linear + keys (0, 0) and (period, 1): a tile in `7 / glitter_speed` s in U and `11 / glitter_speed` s in V, looping, and + wrapping makes the loop seamless. The block layouts are the ocean file's (its controllers are 39 bytes, the property + 70). With `glitter_speed` 0 the property has no controllers. + +The client finds the animation: `SetupRenderNodeExtraData` (0x00c746c0) sets `RenderNodeExtraData.flags0` bit 2 from +`NifHasAnimatedControllers` (0x00bf4160), which returns true for a shape whose `NiTexturingProperty`'s first +controller is an `NiTextureTransformController`. No node transform controllers are added (they would clear the +object's static flag). + +Transparent glitter: every UGC shape has the same `NiAlphaProperty` (blend source alpha over one minus source alpha) +and transparent bricks are transparent by their vertex alpha; the LEGO-AnimUV techniques declare +`UsesNiRenderState = true` and their pixel shader outputs the vertex alpha, the same as the LEGO shader's that +`S01_Alpha_Model` is drawn with, so transparent glitter gets a group of its own. There is no shimmer: +LEGO-AnimUV's pixel shaders don't read the material's emissive (only the `_Emissive` ones do), so an +`NiMaterialColorController` would change nothing. + +The icon draws the flecks where they are at the start (the same texture and UVs, before the light; `glitter_size` +and `glitter_density`), opaque and transparent. The UGC page's 3D view marks glitter meshes (`/api/ugc/mesh`: look +`GLITTER` 512, a mesh with a stored texture in a group tagged `shader_glitter` or 21) and draws moving flecks from +their UVs and `uvScroll` (what `NifFile` reads from the controllers); the property and zone views, which draw bricks +from the LXFML, draw them on the colors in `window.LDD_GLITTER` (`/api/bricks/materials.js`: the glitter colors by +the current settings) from their positions. + +#### Satin + +The client has no satin shader either: Clear Plastic (mapShaders 3) has no vertex color, so it can't show a colored +satin. Satin bricks stay in `S01_Alpha_Model` and are made to look satin when their colors are made: a transparent +brick of a `satin_colors` color gets `satin_opacity` as its vertex alpha, and its color (any brick's) is moved +`satin_whiten` percent towards white, milky. These colors are only in a client whose Materials.xml has them (the +opal colors, 360 to 376, are not in the 1.10.64 client's). + Modular builds (`ugc_modular_build` rows, `ldf_config` like `1:4713+1:4714+1:4715`): 1. Each module LOT's `ModuleComponent` (component type 28) gives its part code and build diff --git a/resources/ugcconfig.ini b/resources/ugcconfig.ini index 260917e65..7699c1495 100644 --- a/resources/ugcconfig.ini +++ b/resources/ugcconfig.ini @@ -69,7 +69,8 @@ lod_cull=10000 shader_opaque=01 combine_transparent=0 -# Metal and glow colors in NiLODNodes of their own, drawn with the client's metal and emissive shaders (the default). +# Metal, glow and glitter colors in NiLODNodes of their own, drawn with the client's metal, emissive and animated UV +# shaders (the default). # Not how live looked: live's models were all LEGO plastic (S01), which 0 (off) keeps, byte for byte. # shader_metal: mapShaders id for metal colors (Materials.xml types in metal_material_types and LU Toolbox's metallic # colors), S_Metal_Model: 88 is Polished Metal. @@ -89,6 +90,26 @@ brushed_material_types=brushedSteel,matteSteel # brushed_colors: LEGO color ids drawn as brushed steel whatever their type, comma separated (empty: the default, the # drum lacquered 298,300,1002,1004; none: no colors) 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. +# 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. +# 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) +shader_glitter=21 +glitter_material_types=glitter +glitter_colors=114,117 +glitter_size=1.6 +glitter_density=50 +glitter_speed=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), +# satin_opacity: their transparent bricks' opacity in percent (instead of transparent_opacity), +# satin_whiten: how far their color goes towards white, in percent. +satin_colors=360,362,363,364,365,366,367,376 +satin_opacity=75 +satin_whiten=20 # Remove faces that can't be seen from anywhere (optimize_resolution: detail of the renders that decide it; # hsr_ground_plane: 1 also removes what can only be seen from below the model) diff --git a/tests/dUgcTests/UgcTests.cpp b/tests/dUgcTests/UgcTests.cpp index 28d44fc85..45aaa498e 100644 --- a/tests/dUgcTests/UgcTests.cpp +++ b/tests/dUgcTests/UgcTests.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -12,6 +13,7 @@ #include "NifFile.h" #include "UgcBricks.h" #include "UgcFormats.h" +#include "UgcGlitter.h" #include "UgcModel.h" #include "UgcJobs.h" #include "IUgc.h" @@ -1304,3 +1306,247 @@ TEST(UgcModel, BrightnessAndTransparentColors) { ASSERT_FALSE(seeThrough.transparent.colors.empty()); EXPECT_NEAR(seeThrough.transparent.colors[0].a, 0.5882f, 1e-4f); } + +// The glitter texture: the same every time, tiling (flecks wrap around the edges), mipmapped down to 1x1 +TEST(UgcGlitter, TextureIsTheSameEveryTimeAndMipmapped) { + const auto alpha = UgcGlitter::FleckAlpha(50); + ASSERT_EQ(alpha.size(), static_cast(UgcGlitter::TEXTURE_SIZE * UgcGlitter::TEXTURE_SIZE)); + EXPECT_EQ(alpha, UgcGlitter::FleckAlpha(50)); + const auto lit = std::count_if(alpha.begin(), alpha.end(), [](uint8_t a) { return a > 0; }); + EXPECT_GT(lit, 50); + EXPECT_LT(lit, static_cast(alpha.size() / 10)); // sparse + const auto none = UgcGlitter::FleckAlpha(0), dense = UgcGlitter::FleckAlpha(200); + EXPECT_EQ(std::count_if(none.begin(), none.end(), [](uint8_t a) { return a > 0; }), 0); + EXPECT_GT(std::count_if(dense.begin(), dense.end(), [](uint8_t a) { return a > 0; }), lit); + const auto mips = UgcGlitter::Mipmaps(alpha); + ASSERT_EQ(mips.size(), 8u); // 128 .. 1 + EXPECT_EQ(mips.back().size(), 1u); + double mean = 0; + for (const auto a : alpha) mean += a; + EXPECT_NEAR(mips.back()[0], mean / alpha.size(), 2.0); + + // UVs: the axis plane the normal faces most, in tiles; the same density on every side + EXPECT_EQ(UgcGlitter::Uv({ 1.6f, 3.2f, 0.8f }, { 0, 0, 1 }, 1.6f), glm::vec2(1.0f, 2.0f)); + EXPECT_EQ(UgcGlitter::Uv({ 1.6f, 3.2f, 0.8f }, { 0, -1, 0 }, 1.6f), glm::vec2(1.0f, 0.5f)); + EXPECT_EQ(UgcGlitter::Uv({ 1.6f, 3.2f, 0.8f }, { 1, 0.2f, 0 }, 1.6f), glm::vec2(0.5f, 2.0f)); + // Sampling wraps + EXPECT_FLOAT_EQ(UgcGlitter::Sample(alpha, { 0.3f, 0.7f }), UgcGlitter::Sample(alpha, { 2.3f, -0.3f })); +} + +namespace { + // A quad in the XY plane, 4 by 4 units, colored + UgcModel::Mesh Quad(const glm::vec4& color) { + UgcModel::Mesh mesh; + mesh.positions = { { -2, -2, 0 }, { 2, -2, 0 }, { -2, 2, 0 }, { 2, 2, 0 } }; + mesh.normals.assign(4, { 0, 0, 1 }); + mesh.colors.assign(4, color); + mesh.indices = { 0, 1, 2, 1, 3, 2 }; + return mesh; + } +} + +// A glitter group: UVs, the fleck texture stored in the file, and the two texture transform controllers the client +// animates it with; read back as NifFile sees it +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 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); + ASSERT_TRUE(read) << error; + ASSERT_EQ(read->meshes.size(), 2u); + for (const auto& shape : read->meshes) { + EXPECT_EQ(shape.material.shaderTag, 21); + ASSERT_EQ(shape.uvs.size(), 8u); + for (size_t v = 0; v < 4; v++) { + const auto uv = UgcGlitter::Uv(mesh.positions[v], mesh.normals[v], 1.6f); + EXPECT_FLOAT_EQ(shape.uvs[v * 2], uv.x); + EXPECT_FLOAT_EQ(shape.uvs[v * 2 + 1], uv.y); + } + EXPECT_TRUE(shape.material.texture.empty()); + ASSERT_GE(shape.material.embeddedTexture, 0); + 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); + // 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 })); + } + // One texturing property and one texture for every glitter shape; every block is read + EXPECT_EQ(read->meshes[0].material.embeddedTexture, read->meshes[1].material.embeddedTexture); + EXPECT_TRUE(read->skipped.empty()) << read->skipped.begin()->first; + // The texture: 128 square, 32-bit, 8 mipmaps, white with the flecks in its alpha + const auto dds = NifFile::EmbeddedTexture(nif, read->meshes[0].material.embeddedTexture); + ASSERT_TRUE(dds); + uint32_t header[31]; + std::memcpy(header, dds->data() + 4, sizeof(header)); + EXPECT_EQ(header[2], 128u); + EXPECT_EQ(header[3], 128u); + EXPECT_EQ(header[6], 8u); + EXPECT_EQ(header[21], 32u); + const auto alpha = UgcGlitter::FleckAlpha(50); + for (size_t i = 0; i < alpha.size(); i++) { + 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; + } + + // 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 + 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()); +} + +// 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) { + UgcBricks::BrickLibrary library(MakeRes(), 0); + library.SetMaterials({ { 5001, { 67, 84, 147, 255, "glitter" } }, { 5002, { 240, 143, 28, 150, "glitter" } }, { 21, { 200, 0, 0, 255, "shinyPlastic" } }, + { 40, { 238, 238, 238, 150, "shinyPlastic" } } }); + const std::string lxfml = R"( + + + + + + )"; + auto settings = SmallSettings(); + settings.build.colorVariation = 0.0f; + settings.shaders.glitter = 21; + 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")); + std::string error; + 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; + 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); + } + 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); + EXPECT_EQ((triangles[{ 1, true }]), 12u); + } + 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; + + // 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()); + EXPECT_EQ(std::count(back.opaque.looks.begin(), back.opaque.looks.end(), UgcModel::eLook::GLITTER), 8); + EXPECT_EQ(std::count(back.transparent.looks.begin(), back.transparent.looks.end(), UgcModel::eLook::GLITTER), 16); + + // Combined transparent bricks: one glitter shape + settings.combineTransparent = true; + const auto combined = UgcJobs::ProcessModel(lxfml, library, settings, 7); + ASSERT_TRUE(combined.ok); + const auto combinedRead = NifFile::Parse(*ZCompression::Gunzip(combined.files.at("model.nif.gz")), 0, error); + ASSERT_TRUE(combinedRead); + size_t transparentGlitterShapes = 0; + for (const auto& mesh : combinedRead->meshes) transparentGlitterShapes += mesh.material.shaderTag == 21 && mesh.colors[3] < 250; + EXPECT_EQ(transparentGlitterShapes, 1u); + + // Off: the glitter colors are plastic, in S01, and the files are the same as without glitter rules at all + settings.combineTransparent = false; + 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.icon.glitter = settings.shaders.glitterParams; + const auto noRules = UgcJobs::ProcessModel(lxfml, library, settings, 7); + ASSERT_TRUE(off.ok && noRules.ok); + for (const auto* name : { "model.nif.checksum", "model.noao.nif.gz", "icon.png" }) EXPECT_EQ(off.files.at(name), noRules.files.at(name)) << name; + const auto offRead = NifFile::Parse(*ZCompression::Gunzip(off.files.at("model.nif.gz")), 0, error); + ASSERT_TRUE(offRead); + for (const auto& mesh : offRead->meshes) EXPECT_EQ(mesh.material.shaderTag, 1); + EXPECT_EQ(off.stats.find("groups"), std::string::npos); +} + +// Glitter in the icon: the texture's flecks over the color before the light, where they are at the start +TEST(UgcShaders, IconsDrawGlitterFlecks) { + UgcModel::Model model; + model.opaque = Quad({ 0.2f, 0.2f, 0.6f, 1.0f }); + UgcRender::IconOptions options; + options.size = 64; + options.supersample = 1; + options.yawDegrees = 0.0f; + options.pitchDegrees = 0.0f; + options.shadows = 0.0f; + options.glitter = { 0.5f, 60, 1.0f }; + const auto plain = UgcRender::RenderIcon(model, options); + model.opaque.looks.assign(4, UgcModel::eLook::GLITTER); + const auto glitter = UgcRender::RenderIcon(model, options); + ASSERT_EQ(plain.rgba.size(), glitter.rgba.size()); + size_t brighter = 0, same = 0; + for (size_t i = 0; i < plain.rgba.size(); i += 4) { + if (plain.rgba[i + 3] == 0) continue; + if (glitter.rgba[i] > plain.rgba[i] + 20) brighter++; + else if (glitter.rgba[i] == plain.rgba[i]) same++; + } + EXPECT_GT(brighter, 10u); // flecks + EXPECT_GT(same, brighter * 5); // on plain plastic + // Transparent glitter too, and it stays see-through + UgcModel::Model clear; + clear.transparent = Quad({ 0.2f, 0.2f, 0.6f, 0.5f }); + clear.transparent.looks.assign(4, UgcModel::eLook::GLITTER); + const auto clearIcon = UgcRender::RenderIcon(clear, options); + clear.transparent.looks.clear(); + const auto clearPlain = UgcRender::RenderIcon(clear, options); + EXPECT_NE(clearIcon.rgba, clearPlain.rgba); + for (size_t i = 3; i < clearIcon.rgba.size(); i += 4) EXPECT_EQ(clearIcon.rgba[i], clearPlain.rgba[i]); +} + +// Satin colors: transparent at satin_opacity instead of the transparent opacity, and milky; the others as they were +TEST(UgcModel, SatinColors) { + UgcBricks::BrickLibrary library(MakeRes(), 0); + library.SetMaterials({ { 360, { 252, 252, 252, 150 } }, { 367, { 35, 120, 65, 150 } }, { 43, { 0, 50, 200, 150 } } }); + std::string error; + const auto parts = UgcModel::ParseLxfml(R"( + + + )", error); + UgcModel::BuildOptions options; + options.colorVariation = 0.0f; + const auto before = UgcModel::Build(parts, library, options); + options.satinColors = { 360, 367 }; + options.satinOpacity = 80.0f; + options.satinWhiten = 25.0f; + const auto satin = UgcModel::Build(parts, library, options); + ASSERT_EQ(satin.transparent.colors.size(), 16u); + EXPECT_NEAR(before.transparent.colors[0].a, 0.5882f, 1e-4f); + EXPECT_NEAR(satin.transparent.colors[0].a, 0.8f, 1e-6f); + const auto linear = UgcPalette::SrgbToLinear(glm::vec3(before.transparent.colors[0])); + const auto milky = UgcPalette::LinearToSrgb(glm::mix(linear, glm::vec3(1.0f), 0.25f)); + for (int c = 0; c < 3; c++) EXPECT_NEAR(satin.transparent.colors[0][c], milky[c], 1e-5f); + // The other transparent brick as before + EXPECT_EQ(satin.transparent.colors[8], before.transparent.colors[8]); + // Satin's own group is the transparent one: no look + EXPECT_TRUE(satin.transparent.looks.empty()); +}