feat(ugc): glitter flecks (LEGO-AnimUV) and milky satin in made models

Glitter colors (Materials.xml type glitter, glitter_colors 114,117) go into
S21_Glitter_Model and, transparent, S21_GlitterAlpha_Model (shader_glitter,
default 21, LEGO-AnimUV). Their shapes get box-projected UVs, an
NiTexturingProperty with a stored 128 px mipmapped fleck texture
(NiSourceTexture + NiPersistentSrcTextureRendererData, as the client's own
env_ag_ocean-maelstrom.nif) and two NiTextureTransformControllers looping
the base map's translation (glitter_size, glitter_density, glitter_speed).
The shader lays the texture over the vertex color by its alpha and outputs
the vertex alpha, so transparent glitter blends as S01_Alpha does.

Satin colors (satin_colors, LEGO's opal colors) stay in S01_Alpha but get
satin_opacity and are whitened by satin_whiten.

NifFile reads the base map's scroll speed (uvScroll) from the controllers;
the icon draws still flecks, the UGC 3D view and the LXFML viewers moving
ones. stats.json counts the glitter groups. With shader_glitter 0 and no
satin colors the files are the same bytes as before (tested). Also keeps
glow_emissive for the icon (it was reset by the icon settings).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 12:53:52 -05:00
parent 9e4f2818c3
commit 7bba8344fc
24 changed files with 966 additions and 93 deletions

View File

@@ -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<uint64_t>(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<float, 2> BaseMapScroll(int32_t first) {
std::array<float, 2> scroll{};
std::set<int32_t> 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<float>(static_cast<uint64_t>(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<int8_t> packed(mesh.normals.size());

View File

@@ -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<float, 2> 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).

View File

@@ -5,10 +5,12 @@
#include <fstream>
#include <map>
#include <set>
#include <sstream>
#include <unordered_map>
#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<uint32_t, std::string> 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<std::string> 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<uint32_t>(UgcRoutes::Setting("shader_glitter").value_or("")).value_or(0) != 0) {
const auto types = list("glitter_material_types");
std::set<uint32_t> 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<uint32_t>(id)) ids.insert(*value);
for (const auto id : ids) glitter.push_back(id);
}
const nlohmann::json settings{ { "colors", glitter },
{ "tile", GeneralUtils::TryParse<float>(UgcRoutes::Setting("glitter_size").value_or("")).value_or(1.6f) },
{ "flecks", GeneralUtils::TryParse<uint32_t>(UgcRoutes::Setting("glitter_density").value_or("")).value_or(50) },
{ "speed", GeneralUtils::TryParse<float>(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");
});

View File

@@ -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<id>_Glitter_Model and (transparent ones) S<id>_GlitterAlpha_Model: 21 is LEGO-AnimUV, which lays a white fleck texture stored in the model over the color and moves it. 0: off, they stay plastic." + notLive, "21", 0, 9999));
c.Add(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"));

View File

@@ -264,6 +264,10 @@ namespace UgcRoutes {
g_IconKinds = LoadIconKinds();
}
std::optional<std::string> 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<LWOOBJID>(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<int32_t>(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<std::string> textures(model->meshes.size());
for (size_t i = 0; i < model->meshes.size(); i++) {
const auto& material = model->meshes[i].material;
if (material.embeddedTexture < 0 || (material.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<std::string>(model->meshes.size()), {}, Scenery::MultishaderLooks(*model));
out.message = NifFile::Encode(*model, textures, {}, looks);
out.headers.push_back("Cache-Control: private, no-cache");
});
});

View File

@@ -1,5 +1,8 @@
#pragma once
#include <optional>
#include <string>
// 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<std::string> Setting(const std::string& name);
}

View File

@@ -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

View File

@@ -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 <begin_vertex>', '#include <begin_vertex>\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 <color_fragment>', '#include <color_fragment>\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);
}

View File

@@ -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;
}

View File

@@ -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()

View File

@@ -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<uint16_t>(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<uint16_t>(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<std::pair<uint32_t, float>> motions; // TexTransform (0 translate U, 1 translate V), seconds a tile
if (glitter.PeriodU() > 0.0f) motions.emplace_back(0, glitter.PeriodU());
if (glitter.PeriodV() > 0.0f) motions.emplace_back(1, glitter.PeriodV());
std::vector<int32_t> controllers;
for (size_t i = 0; i < motions.size(); i++) controllers.push_back(m_Nif.Reserve("NiTextureTransformController"));
for (size_t i = 0; i < motions.size(); i++) {
const auto [operation, period] = motions[i];
Writer data;
data.U32(2); // keys
data.U32(1); // linear
data.Float(0.0f);
data.Float(0.0f);
data.Float(period);
data.Float(1.0f);
const auto interpolator = m_Nif.Reserve("NiFloatInterpolator");
const auto dataBlock = m_Nif.Add("NiFloatData", std::move(data.Data()));
Writer value;
value.Float(0.0f);
value.I32(dataBlock);
m_Nif.Fill(interpolator, std::move(value.Data()));
Writer controller;
controller.I32(i + 1 < controllers.size() ? controllers[i + 1] : -1); // next controller
controller.U16(0x48); // active, loop, app time (as the client's files)
controller.Float(1.0f); // frequency
controller.Float(0.0f); // phase
controller.Float(0.0f); // start
controller.Float(period); // stop
controller.I32(m_Glitter); // target
controller.I32(interpolator);
controller.U8(0); // not a shader map
controller.U32(0); // the base map
controller.U32(operation);
m_Nif.Fill(controllers[i], std::move(controller.Data()));
}
// The texture: white, its alpha the flecks, mipmapped, 32-bit as the client's stored textures are (B, G, R, A)
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<uint32_t>(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<uint32_t>(UgcGlitter::TEXTURE_SIZE) >> level;
pixels.U32(side);
pixels.U32(side);
pixels.U32(offset);
offset += static_cast<uint32_t>(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<int32_t> 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<float, int32_t> 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<int32_t> 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));

View File

@@ -7,6 +7,7 @@
#include <string_view>
#include <vector>
#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<NifLodGroup>& groups);

73
dUgcServer/UgcGlitter.cpp Normal file
View File

@@ -0,0 +1,73 @@
#include "UgcGlitter.h"
#include <algorithm>
#include <cmath>
namespace UgcGlitter {
std::vector<uint8_t> FleckAlpha(uint32_t flecks) {
constexpr int N = TEXTURE_SIZE;
std::vector<float> alpha(static_cast<size_t>(N) * N, 0.0f);
// 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<float>((z ^ (z >> 31)) >> 40) / static_cast<float>(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<int>(std::ceil(radius));
for (int dy = -reach; dy <= reach; dy++) {
for (int dx = -reach; dx <= reach; dx++) {
const int x = static_cast<int>(std::floor(cx)) + dx, y = static_cast<int>(std::floor(cy)) + dy;
const float ddx = x + 0.5f - cx, ddy = y + 0.5f - cy;
const float d = std::sqrt(ddx * ddx + ddy * ddy) / radius;
if (d >= 1.0f) continue;
auto& value = alpha[static_cast<size_t>(((y % N) + N) % N) * N + ((x % N) + N) % N];
value = std::max(value, peak * (1.0f - d * d));
}
}
}
std::vector<uint8_t> out(alpha.size());
for (size_t i = 0; i < alpha.size(); i++) out[i] = static_cast<uint8_t>(std::lround(std::clamp(alpha[i], 0.0f, 1.0f) * 255.0f));
return out;
}
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha) {
std::vector<std::vector<uint8_t>> levels{ alpha };
for (int size = TEXTURE_SIZE / 2; size >= 1; size /= 2) {
const auto& above = levels.back();
const int from = size * 2;
std::vector<uint8_t> level(static_cast<size_t>(size) * size);
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
const auto at = [&](int dx, int dy) { return static_cast<int>(above[static_cast<size_t>(y * 2 + dy) * from + x * 2 + dx]); };
level[static_cast<size_t>(y) * size + x] = static_cast<uint8_t>((at(0, 0) + at(1, 0) + at(0, 1) + at(1, 1) + 2) / 4);
}
}
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<uint8_t>& alpha, const glm::vec2& uv) {
constexpr int N = TEXTURE_SIZE;
if (alpha.size() != static_cast<size_t>(N) * N) return 0.0f;
const float x = (uv.x - std::floor(uv.x)) * N - 0.5f, y = (uv.y - std::floor(uv.y)) * N - 0.5f;
const int x0 = static_cast<int>(std::floor(x)), y0 = static_cast<int>(std::floor(y));
const float fx = x - x0, fy = y - y0;
const auto at = [&](int px, int py) { return alpha[static_cast<size_t>(((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;
}
}

41
dUgcServer/UgcGlitter.h Normal file
View File

@@ -0,0 +1,41 @@
#pragma once
#include <cstdint>
#include <vector>
#include <glm/glm.hpp>
/**
* The glitter the UGC server gives glitter colors (docs/UgcServer.md, "Metal and glow"): a tileable texture of white
* flecks (its alpha) laid over the brick's color by the client's LEGO-AnimUV shader (lerp(vertex color, texture,
* texture alpha), then the LEGO lighting), on UVs projected from the model's own coordinates so every brick gets the
* same density, 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<uint8_t> FleckAlpha(uint32_t flecks);
// The texture's mipmaps' alpha, from TEXTURE_SIZE down to 1 (each the mean of 2x2 of the one before)
std::vector<std::vector<uint8_t>> Mipmaps(const std::vector<uint8_t>& alpha);
// 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<uint8_t>& alpha, const glm::vec2& uv);
}

View File

@@ -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<std::vector<UgcModel::Mesh>, 2> DivideTransparent(const UgcModel::Model& model, bool combine, bool glitterApart) {
std::array<std::vector<UgcModel::Mesh>, 2> out;
std::array<bool, UgcModel::LOOK_COUNT> separate{};
separate[static_cast<size_t>(UgcModel::eLook::GLITTER)] = glitterApart;
const auto glitter = static_cast<size_t>(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<int32_t, UgcModel::eLook> Shaders::TagLooks() const {
std::map<int32_t, UgcModel::eLook> 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<int32_t, UgcModel::eLook> 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<int32_t>(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<UgcModel::Model> models;
std::vector<LookPieces> opaquePieces;
std::vector<std::vector<UgcModel::Mesh>> transparentPieces;
// Per level: the transparent bricks' pieces, plastic and (with the glitter group on) glitter
using TransparentPieces = std::array<std::vector<UgcModel::Mesh>, 2>;
std::vector<TransparentPieces> transparentPieces;
// The looks with a shader of their own (UgcJobs::Shaders), each an NiLODNode apart from the plastic
std::array<bool, UgcModel::LOOK_COUNT> separate{};
for (size_t look = 1; look < UgcModel::LOOK_COUNT; look++) separate[look] = settings.shaders.TagOf(static_cast<UgcModel::eLook>(look)) != 0;
@@ -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<size_t>(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<UgcModel::eLook>(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<LookPieces>& opaque, const std::vector<std::vector<UgcModel::Mesh>>& transparent) {
// Transparent glitter last, after the plain transparent bricks.
const auto groups = [&](size_t levels, const std::vector<LookPieces>& opaque, const std::vector<TransparentPieces>& transparent) {
std::vector<UgcFormats::NifLodGroup> 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<UgcModel::eLook>(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<size_t>(UgcModel::eLook::GLITTER);
const auto look = glitter ? UgcModel::eLook::GLITTER : isTransparent ? UgcModel::eLook::PLASTIC : static_cast<UgcModel::eLook>(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<LookPieces> opaque{ DivideByLook(preview.opaque, separate) };
const std::vector<std::vector<UgcModel::Mesh>> transparent{ transparentPieces[0] };
const std::vector<TransparentPieces> transparent{ transparentPieces[0] };
outcome.files["model.noao.nif.gz"] = ZCompression::Gzip(UgcFormats::WriteLodNif("SceneNode_Model", groups(1, opaque, transparent)));
}

View File

@@ -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<int32_t, UgcModel::eLook> TagLooks() const;
};
@@ -81,7 +84,8 @@ namespace UgcJobs {
const std::map<int32_t, UgcModel::eLook>& 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)

View File

@@ -223,7 +223,7 @@ namespace UgcModel {
Model model;
std::set<uint32_t> 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<uint32_t>(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);

View File

@@ -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<glm::vec3> positions;
std::vector<glm::vec3> normals;
std::vector<glm::vec4> colors; // sRGB, 0..1, alpha is opacity
std::vector<glm::vec3> glow; // linear glow color per vertex (LU Toolbox's "Glow" layer); empty when nothing glows
std::vector<eLook> looks; // per vertex; empty when everything is plastic
std::vector<eLook> looks; // per vertex; empty when everything is plastic (transparent meshes: plastic or glitter)
std::vector<uint32_t> 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<uint32_t, eLook> colors; // LEGO color ids given a look by the settings (brushed_colors)
std::map<std::string, eLook> materialTypes{ { "shinySteel", eLook::METAL }, { "brushedSteel", eLook::BRUSHED }, { "matteSteel", eLook::BRUSHED } };
std::map<uint32_t, eLook> colors; // LEGO color ids given a look by the settings (brushed_colors, glitter_colors)
std::map<std::string, eLook> 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<uint32_t> satinColors;
float satinOpacity{ 75.0f };
float satinWhiten{ 20.0f };
};
/**
@@ -131,7 +139,8 @@ namespace UgcModel {
std::vector<Mesh> 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<int32_t, eLook>& tagLooks = {});
/**

View File

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

View File

@@ -5,6 +5,7 @@
#include <glm/glm.hpp>
#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<float>* opaqueAo = nullptr);
struct OptimizeOptions {

View File

@@ -62,6 +62,19 @@ namespace {
return GeneralUtils::TryParse<T>(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<uint32_t> ColorList(const std::string& key, const std::string& fallback) {
const auto value = Game::config->GetValue(key);
std::set<uint32_t> 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<uint32_t>(id)) colors.insert(*color);
}
return colors;
}
std::vector<uint32_t> ParseLods(const std::string& text) {
std::vector<uint32_t> lods;
std::stringstream stream(text);
@@ -104,9 +117,14 @@ namespace {
settings.shaders.brushed = std::min(Setting<uint32_t>("shader_brushed", 89), 9999u);
settings.shaders.glow = std::min(Setting<uint32_t>("shader_glow", 46), 9999u);
settings.shaders.glowEmissive = std::clamp(Setting<float>("glow_emissive", 1.0f), 0.0f, 10.0f);
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<id>_Glitter_Model and S<id>_GlitterAlpha_Model with drifting flecks (LEGO-AnimUV)
settings.shaders.glitter = std::min(Setting<uint32_t>("shader_glitter", 21), 9999u);
settings.shaders.glitterParams.tile = std::clamp(Setting<float>("glitter_size", 1.6f), 0.1f, 100.0f);
settings.shaders.glitterParams.flecks = std::min(Setting<uint32_t>("glitter_density", 50), 2000u);
settings.shaders.glitterParams.speed = std::clamp(Setting<float>("glitter_speed", 1.0f), 0.0f, 100.0f);
// 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<uint32_t>(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<float>("satin_opacity", 75.0f), 0.0f, 100.0f);
settings.build.satinWhiten = std::clamp(Setting<float>("satin_whiten", 20.0f), 0.0f, 100.0f);
settings.optimize.removeHidden = Setting<int32_t>("remove_hidden_faces", 1) != 0;
settings.optimize.groundPlane = Setting<int32_t>("hsr_ground_plane", 0) != 0;
settings.optimize.resolution = Setting<int32_t>("optimize_resolution", 1024);
@@ -142,6 +158,8 @@ namespace {
return value.empty() ? std::nullopt : std::optional(value);
});
settings.icon.size = Setting<int32_t>("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<uint32_t>("max_model_bricks", 0);
return settings;

View File

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

View File

@@ -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<id>_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<id>_Glitter_Model and, for
# transparent ones, S<id>_GlitterAlpha_Model: 21 is LEGO-AnimUV, which lays a white fleck texture stored in the .nif
# over the color and moves it. 0: off, glitter stays plastic.
# 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)

View File

@@ -1,5 +1,6 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
@@ -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<size_t>(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<long>(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<float, 3>{ 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<uint8_t>((*dds)[128 + i * 4]), 255);
ASSERT_EQ(static_cast<uint8_t>((*dds)[128 + i * 4 + 3]), alpha[i]) << i;
}
// The block types, as the client's own animated textures (res/mesh/env/env_ag_ocean-maelstrom.nif)
for (const auto* type : { "NiTexturingProperty", "NiTextureTransformController", "NiFloatInterpolator", "NiFloatData", "NiSourceTexture", "NiPersistentSrcTextureRendererData" }) {
EXPECT_NE(nif.find(type), std::string::npos) << type;
}
// Still (speed 0): the texture without controllers
const UgcGlitter::Params still{ 1.6f, 50, 0.0f };
const auto stillNif = UgcFormats::WriteLodNif("SceneNode_Model", { { "S21_Glitter_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } }, 0.0f, &still } });
const auto stillRead = NifFile::Parse(stillNif, 0, error);
ASSERT_TRUE(stillRead) << error;
EXPECT_EQ(stillRead->meshes[0].material.uvScroll, (std::array<float, 2>{}));
EXPECT_GE(stillRead->meshes[0].material.embeddedTexture, 0);
EXPECT_EQ(stillNif.find("NiTextureTransformController"), std::string::npos);
// The dashboard's encoding carries the motion
const auto encoded = NifFile::Encode(*read, { "glitter", "glitter" });
uint32_t length = 0;
std::memcpy(&length, encoded.data(), 4);
const auto header2 = nlohmann::json::parse(encoded.substr(4, length));
EXPECT_NEAR(header2["meshes"][0]["uvScroll"][0].get<float>(), 2.0f / 7.0f, 1e-6f);
EXPECT_TRUE(header2["meshes"][0]["uv"].get<bool>());
}
// 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"(<LXFML versionMajor="5"><Bricks>
<Brick><Part designID="3001" materials="5001"><Bone transformation="1,0,0,0,1,0,0,0,1,0,0,0"/></Part></Brick>
<Brick><Part designID="3001" materials="5002"><Bone transformation="1,0,0,0,1,0,0,0,1,3,0,0"/></Part></Brick>
<Brick><Part designID="3001" materials="5002"><Bone transformation="1,0,0,0,1,0,0,0,1,6,0,0"/></Part></Brick>
<Brick><Part designID="3001" materials="21"><Bone transformation="1,0,0,0,1,0,0,0,1,9,0,0"/></Part></Brick>
<Brick><Part designID="3001" materials="40"><Bone transformation="1,0,0,0,1,0,0,0,1,12,0,0"/></Part></Brick>
</Bricks></LXFML>)";
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<std::pair<int32_t, bool>, size_t> triangles; // (tag, transparent) -> triangles
for (const auto& mesh : read->meshes) {
bool seeThrough = false;
for (size_t i = 3; i < mesh.colors.size(); i += 4) seeThrough = seeThrough || mesh.colors[i] < 250;
triangles[{ mesh.material.shaderTag, seeThrough }] += mesh.indices.size() / 3;
// Only the glitter shapes are textured
EXPECT_EQ(mesh.material.embeddedTexture >= 0, mesh.material.shaderTag == 21);
EXPECT_EQ(!mesh.uvs.empty(), mesh.material.shaderTag == 21);
}
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"(<LXFML versionMajor="5"><Bricks>
<Brick><Part designID="3001" materials="367"><Bone transformation="1,0,0,0,1,0,0,0,1,0,0,0"/></Part></Brick>
<Brick><Part designID="3001" materials="43"><Bone transformation="1,0,0,0,1,0,0,0,1,3,0,0"/></Part></Brick>
</Bricks></LXFML>)", 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());
}