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:
@@ -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());
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
73
dUgcServer/UgcGlitter.cpp
Normal 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
41
dUgcServer/UgcGlitter.h
Normal 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);
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user