mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
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:
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user