From e42fd45c1ff1e5cff7161efca6b140b9a6175725 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 00:39:35 -0500 Subject: [PATCH] feat(dashboard): glitter sparkles and flecks in the UGC 3D views The UGC page's model view now draws the glitter sparkle shapes (look SPARKLE) flashing as the client's Distortion Directional shader makes them: two layers of sparkles sliding at the client's rates (a tile in 24 s at three quarters the scale, a tile in 48 s), shown only where both have one. Flecks are drawn as flat flakes of the fleck size and varied brightness (addGlitter in scenery-core.js), no longer drifting (the game never moves them). The LXFML views (the UGC page's second view, the property and zone views) draw both on the glitter colors from window.LDD_GLITTER, which now carries every glitter setting and is no longer cached, so changing a glitter setting on the settings page previews on any model's LXFML view without making it again. Check in the dashboard (not the game): open a glitter model on the UGC page; both views show still flecks and sparkles flashing on the glitter bricks only; change glitter_sparkle_amount / glitter_fleck_size in the settings and reopen the model: the LXFML view follows. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/routes/ClientAssets.cpp | 12 ++- dDashboardServer/static/js/lddviewer.js | 14 ++- dDashboardServer/static/js/scenery-core.js | 104 +++++++++++++++++---- dDashboardServer/static/js/ugc-viewer.js | 21 +++-- docs/UgcServer.md | 12 ++- 5 files changed, 128 insertions(+), 35 deletions(-) diff --git a/dDashboardServer/routes/ClientAssets.cpp b/dDashboardServer/routes/ClientAssets.cpp index 1bbdb391f..ac469d97d 100644 --- a/dDashboardServer/routes/ClientAssets.cpp +++ b/dDashboardServer/routes/ClientAssets.cpp @@ -457,7 +457,7 @@ 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, 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. " + "(shader_glitter on: glitter_material_types and glitter_colors) and the glitter's settings (flecks, and sparkles when shader_glitter_sparkle is on). " "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 @@ -505,11 +505,17 @@ void RegisterClientAssetRoutes() { { "flecks", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_density").value_or("")).value_or(80) }, { "fleckSize", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_fleck_size").value_or("")).value_or(0.05f) }, { "fleckOpacity", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_fleck_opacity").value_or("")).value_or(80.0f) }, - { "speed", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_speed").value_or("")).value_or(1.0f) } }; + { "speed", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_speed").value_or("")).value_or(1.0f) }, + { "sparkles", GeneralUtils::TryParse(UgcRoutes::Setting("shader_glitter_sparkle").value_or("")).value_or(79) != 0 }, + { "sparkleSize", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_sparkle_size").value_or("")).value_or(0.1f) }, + { "sparkleAmount", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_sparkle_amount").value_or("")).value_or(5.0f) }, + { "sparkleTint", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_sparkle_tint").value_or("")).value_or(30.0f) }, + { "sparkleBrightness", GeneralUtils::TryParse(UgcRoutes::Setting("glitter_sparkle_brightness").value_or("")).value_or(100.0f) } }; reply.status = eHTTPStatusCode::OK; 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"); + // Not kept: the glitter settings change from the settings page, and the preview should follow + reply.headers.push_back("Cache-Control: private, no-cache"); }); Route(eHTTPMethod::GET, "/api/icon/:lot", 0, "PNG icon for an item LOT (requires client_location and ImageMagick)", diff --git a/dDashboardServer/static/js/lddviewer.js b/dDashboardServer/static/js/lddviewer.js index 3ccb59fe4..c3e5433aa 100644 --- a/dDashboardServer/static/js/lddviewer.js +++ b/dDashboardServer/static/js/lddviewer.js @@ -14,7 +14,7 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; -import { parseModel, mergeMeshes, linearColors, nearPlaneFor, addGlitter } from '/js/scenery-core.js'; +import { parseModel, mergeMeshes, linearColors, nearPlaneFor, addGlitter, glitterSettings } from '/js/scenery-core.js'; const GEOMETRY_MAGIC = 0x42473031; // "10GB" const MAX_PARALLEL_FETCHES = 6; @@ -174,7 +174,8 @@ async function loadGeneratedModel(url) { // ---- Viewer ---- const materialCache = new Map(); -// The glitter colours' flecks (window.LDD_GLITTER, the UGC server's glitter settings) +// The glitter colours' flecks and sparkles (window.LDD_GLITTER, the UGC server's current glitter settings), the +// sparkles moved each frame const glitterMaterials = []; function material(id) { if (!materialCache.has(id)) { @@ -192,8 +193,13 @@ function material(id) { }); const glitter = window.LDD_GLITTER; if (glitter && (glitter.colors || []).includes(Number(id))) { - // Still, as the game draws them on a placed model - glitterMaterials.push(addGlitter(created, { coordinates: 'position', tile: glitter.tile || 1.6, flecks: glitter.flecks || 50 })); + // The flecks still and the sparkles flashing, as the game draws them on a placed model; the sparkles white + // taking the tint of the brick's color (UgcGlitter::SparkleColor) + const settings = glitterSettings(); + const tint = Math.min(Math.max(settings.sparkleTint / 100, 0), 1), bright = Math.min(Math.max(settings.sparkleBrightness / 100, 0), 1); + const sparkleColor = [0, 1, 2].map((i) => Math.min(1, ((1 - tint) + tint * c[i] / 255) * bright)); + const linear = new THREE.Color().setRGB(sparkleColor[0], sparkleColor[1], sparkleColor[2], THREE.SRGBColorSpace); + glitterMaterials.push(addGlitter(created, { coordinates: 'position', ...settings, sparkleColor: [linear.r, linear.g, linear.b] })); } materialCache.set(id, created); } diff --git a/dDashboardServer/static/js/scenery-core.js b/dDashboardServer/static/js/scenery-core.js index 87e0f6437..34f40c0c5 100644 --- a/dDashboardServer/static/js/scenery-core.js +++ b/dDashboardServer/static/js/scenery-core.js @@ -59,18 +59,41 @@ export const SHADER_LOOK = { UNLIT: 1, NO_TEXTURE: 2, NO_VERTEX_COLORS: 4, MATER GLITTER: 512, SPARKLE: 1024 }; /** - * 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. + * The UGC server's glitter (UgcGlitter) for a three.js material, drawn in the shader from hashes of cells: + * + * Flecks ({flecks}): flat flakes over the color before the light, as the game's LEGO-AnimUV shader lays its fleck + * texture over the vertex color: `flecks` a tile, `fleckSize` model units across (0.7 to 1.3 of it), each as bright as + * its facet catches the light (0.3 to 1 of `fleckOpacity` percent, mostly dim). On `coordinates` 'uv' (the mesh's + * 'glitterUv' attribute, in tiles) or 'position' (a box projection of the object's position, `tile` units a tile). + * + * Sparkles ({sparkles}): what the game's Distortion Directional shader does with the sparkle texture: two layers of + * sparkles (`sparkleAmount` percent covered, `sparkleSize` across) sliding at the client's rates (a tile in 24 s at + * three quarters the scale, and in 48 s), drawn only where both have one (the alpha test), in `sparkleColor`. On + * 'glitterSparkleUv' (in sparkle tiles) or the projected position (`sparkleTile` units a tile). With `sparklesOnly` + * the rest of the surface is discarded and the sparkles keep the material's color (the made model's sparkle shapes, + * whose vertex colors are the sparkles'). + * + * Returns {update(seconds)} to move the sparkles. */ -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)) } }; +export function addGlitter(material, { coordinates = 'uv', tile = 1.6, flecks = 80, fleckSize = 0.05, fleckOpacity = 80, sparkles = false, + sparkleTile = 7.5, sparkleSize = 0.1, sparkleAmount = 5, sparkleColor = [1, 1, 1], sparklesOnly = false } = {}) { + const fleckCells = Math.max(1, Math.sqrt(flecks)); + // A sparkle's radius in sparkle tiles, and cells of one sparkle each covering `sparkleAmount` percent + const sparkleRadius = sparkleSize / Math.max(sparkleTile, 1e-3) / 2; + const sparkleShare = Math.min(Math.max(sparkleAmount / 100, 0), 0.5); + const sparkleCells = sparkleShare > 0 ? Math.sqrt(sparkleShare / (Math.PI * sparkleRadius * sparkleRadius)) : 1; + const uniforms = { + glitterTime: { value: 0 }, glitterTile: { value: tile }, glitterCells: { value: fleckCells }, + glitterFleckRadius: { value: fleckSize / Math.max(tile, 1e-3) / 2 * fleckCells }, glitterOpacity: { value: Math.min(Math.max(fleckOpacity / 100, 0), 1) }, + glitterFlecksOn: { value: sparklesOnly ? 0 : 1 }, + sparkleOn: { value: sparkles && sparkleShare > 0 ? 1 : 0 }, sparkleTileSize: { value: sparkleTile }, sparkleCells: { value: sparkleCells }, + sparkleCellRadius: { value: sparkleRadius * sparkleCells }, sparkleColor: { value: new Float32Array(sparkleColor) }, sparklesOnly: { value: sparklesOnly ? 1 : 0 } + }; 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 = 'varying vec3 vGlitterPosition;\nvarying vec3 vGlitterNormal;\n' + + (byPosition ? '' : 'attribute vec2 glitterUv;\nvarying vec2 vGlitterUv;\nattribute vec2 glitterSparkleUv;\nvarying vec2 vGlitterSparkleUv;\n') + shader.vertexShader.replace('#include ', '#include \n' + (byPosition ? `vec4 glitterAt = vec4(transformed, 1.0); vec3 glitterNormal = objectNormal; @@ -80,26 +103,75 @@ 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') + ` +` : 'vGlitterUv = glitterUv;\nvGlitterSparkleUv = glitterSparkleUv;\n')); + shader.fragmentShader = `uniform float glitterTime; +uniform float glitterTile; +uniform float glitterCells; +uniform float glitterFleckRadius; +uniform float glitterOpacity; +uniform float glitterFlecksOn; +uniform float sparkleOn; +uniform float sparkleTileSize; +uniform float sparkleCells; +uniform float sparkleCellRadius; +uniform vec3 sparkleColor; +uniform float sparklesOnly; +varying vec3 vGlitterPosition; +varying vec3 vGlitterNormal; +` + (byPosition ? '' : 'varying vec2 vGlitterUv;\nvarying vec2 vGlitterSparkleUv;\n') + ` float glitterHash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +// A flat disc of radius r (in cells) at a random place in each cell, its edge a pixel wide +float glitterDisc(vec2 uv, float cells, float r, float salt) { + vec2 at = uv * cells; + vec2 cell = floor(at); + vec2 centre = r + (1.0 - 2.0 * r) * vec2(glitterHash(cell + salt), glitterHash(cell + salt + 17.0)); + float d = length(fract(at) - centre); + float edge = max(fwidth(d), 1e-4); + return clamp((r - d) / edge + 0.5, 0.0, 1.0); +} 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)); + float size = 0.7 + 0.6 * glitterHash(cell + 29.0); + float facet = glitterHash(cell + 41.0); + return glitterOpacity * (0.3 + 0.7 * facet * facet) * glitterDisc(uv, glitterCells, min(glitterFleckRadius * size, 0.45), 0.0); +} +// Two layers sliding as Distortion Directional's (2-layer technique): uv * 0.75 + a tile in 24 s, uv - 0.3 + a tile in 48 s +float glitterSparkle(vec2 uv) { + float r = min(sparkleCellRadius, 0.45); + float a = glitterDisc(uv * 0.75 + vec2(glitterTime / 24.0, 0.0), sparkleCells, r, 3.0); + float b = glitterDisc(uv - 0.3 + vec2(glitterTime / 48.0, 0.0), sparkleCells, r, 7.0); + return step(0.996, a + b); } ` + shader.fragmentShader.replace('#include ', '#include \n' + (byPosition ? ` vec3 glitterN = abs(vGlitterNormal); - vec2 glitterUv = (glitterN.x >= glitterN.y && glitterN.x >= glitterN.z ? vGlitterPosition.zy : glitterN.y >= glitterN.z ? vGlitterPosition.xz : vGlitterPosition.xy) / glitterTile; -` : 'vec2 glitterUv = vGlitterUv;\n') + 'diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), glitterFleck(glitterUv + glitterScroll * glitterTime));\n'); + vec2 glitterPlane = glitterN.x >= glitterN.y && glitterN.x >= glitterN.z ? vGlitterPosition.zy : glitterN.y >= glitterN.z ? vGlitterPosition.xz : vGlitterPosition.xy; + vec2 glitterUv = glitterPlane / glitterTile; + vec2 glitterSparkleUv = glitterPlane / sparkleTileSize; +` : 'vec2 glitterUv = vGlitterUv;\nvec2 glitterSparkleUv = vGlitterSparkleUv;\n') + ` + if (glitterFlecksOn > 0.5) diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), glitterFleck(glitterUv)); + if (sparkleOn > 0.5) { + float glitterSparkles = glitterSparkle(glitterSparkleUv); + if (sparklesOnly > 0.5 && glitterSparkles < 0.5) discard; + if (sparklesOnly < 0.5) diffuseColor = mix(diffuseColor, vec4(sparkleColor, 1.0), glitterSparkles); + } +`); }; material.customProgramCacheKey = () => 'glitter-' + coordinates; material.needsUpdate = true; return { update(seconds) { uniforms.glitterTime.value = seconds; } }; } +// The UGC server's glitter settings for addGlitter (window.LDD_GLITTER), its defaults without them +export function glitterSettings() { + const g = window.LDD_GLITTER || {}; + const speed = g.speed > 0 ? g.speed : 1, sparkleSize = g.sparkleSize > 0 ? g.sparkleSize : 0.1; + return { + tile: g.tile || 1.6, flecks: g.flecks ?? 80, fleckSize: g.fleckSize || 0.05, fleckOpacity: g.fleckOpacity ?? 80, + sparkles: !!g.sparkles, sparkleSize, sparkleAmount: g.sparkleAmount ?? 5, sparkleTile: 75 * sparkleSize * speed, + sparkleTint: g.sparkleTint ?? 30, sparkleBrightness: g.sparkleBrightness ?? 100 + }; +} + // NifFile::eTechniqueFlag bits (what a technique does besides its family and eShaderLook bits) export const TECHNIQUE = { UV_ANIM: 1, DOUBLE_SIDED: 2, BLEND: 4, ALPHA_TEST: 8, ADDITIVE: 16, NO_AMBIENT: 32, GLOW: 64, IGNORE_VERTEX_ALPHA: 128, diff --git a/dDashboardServer/static/js/ugc-viewer.js b/dDashboardServer/static/js/ugc-viewer.js index 63cb6a5cb..6468af26c 100644 --- a/dDashboardServer/static/js/ugc-viewer.js +++ b/dDashboardServer/static/js/ugc-viewer.js @@ -2,13 +2,14 @@ * 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, the glitter groups with moving white flecks (their UVs and - * uvScroll, as the game moves its fleck texture). + * reflecting the view's environment and as unlit glow, the glitter groups with their flecks on their UVs and the + * glitter sparkles flashing as the game's Distortion Directional shader makes them (addGlitter), by the UGC server's + * current glitter settings (window.LDD_GLITTER, when the page has /api/bricks/materials.js). */ 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, addGlitter, SHADER_LOOK } from '/js/scenery-core.js'; +import { parseModel, mergeMeshes, linearColors, metalOf, addGlitter, glitterSettings, SHADER_LOOK } from '/js/scenery-core.js'; export function createNifViewer(container) { const renderer = new THREE.WebGLRenderer({ antialias: true }); @@ -123,19 +124,23 @@ export function createNifViewer(container) { let seeThrough = !!mesh.blend && mesh.alpha < 0.99; if (mesh.blend && hasColors) for (let i = 3; i < mesh.colors.length && !seeThrough; i += 4) seeThrough = mesh.colors[i] < 250; const look = mesh.look || 0; - // The glitter sparkles (drawn over the glitter bricks) aren't drawn here - if (look & SHADER_LOOK.SPARKLE) continue; const metal = metalOf(look); // Glow: the emissive shader's vertex color, unlit (its vertex alpha is the glow, not opacity) - const material = look & SHADER_LOOK.EMISSIVE + const sparkles = !!(look & SHADER_LOOK.SPARKLE); + const material = sparkles ? new THREE.MeshLambertMaterial({ color: baseColor.clone(), vertexColors: hasColors }) + : look & SHADER_LOOK.EMISSIVE ? 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 + // Glitter: the fleck texture's UVs, one tile of it each; the sparkle shapes' UVs, one sparkle tile each let glitter = null; + const settings = glitterSettings(); 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] }); + glitter = addGlitter(material, { coordinates: 'uv', ...settings, sparkles: false }); + } else if (sparkles && mesh.uvs) { + geometry.setAttribute('glitterSparkleUv', new THREE.BufferAttribute(mesh.uvs, 2)); + glitter = addGlitter(material, { coordinates: 'uv', ...settings, sparkles: true, sparklesOnly: true }); } const object = new THREE.Mesh(geometry, material); if (seeThrough) object.renderOrder = 1; diff --git a/docs/UgcServer.md b/docs/UgcServer.md index 6bf67a9fd..cfff9b00d 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -434,10 +434,14 @@ and transparent) again: The icon draws the flecks where they are (the same texture and the .nif's UVs, before the light; `glitter_size` and `glitter_density`), opaque and transparent, and leaves the sparkles out (`Shaders::OverlayTags`: alpha tested shapes tagged `shader_glitter_sparkle` or 79). The UGC page's 3D view marks glitter meshes (`/api/ugc/mesh`: look -`GLITTER` 512, a mesh with a stored texture in a group tagged `shader_glitter` or 21) and draws flecks from their -UVs, and marks the sparkles (look `SPARKLE` 1024); the property and zone views, which draw bricks from the LXFML, draw -flecks on the colors in `window.LDD_GLITTER` (`/api/bricks/materials.js`: the glitter colors by the current settings) -from their positions. +`GLITTER` 512, a mesh with a stored texture in a group tagged `shader_glitter` or 21) and draws flecks on their UVs, +and marks the sparkles (look `SPARKLE` 1024, alpha tested and tagged `shader_glitter_sparkle` or 79), which it draws +flashing on theirs as the client does (`addGlitter` in `static/js/scenery-core.js`: two layers of sparkles in cells, +one at three quarters the scale sliding a tile in 24 s, the other a tile in 48 s, shown where both have one). The +LXFML views (the UGC page's second view, the property and zone views) draw flecks and sparkles on the glitter colors +from their positions, by `window.LDD_GLITTER` (`/api/bricks/materials.js`: the glitter colors and every glitter +setting as they are now, not cached): a live preview of the settings without making the model again (a brick's own +placement, `glitter_random`, is only in the made model). #### Satin