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