mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
fix(ugc): each glitter brick gets its own fleck pattern
Every glitter brick had the same flecks in the same places: the UVs were the vertex positions projected on an axis plane, so bricks a whole tile apart (and every brick of the same shape at the same spot in its own model) looked identical. Each brick now has a number of its own (UgcGlitter::BrickSeed, from the model's id and the brick's index, kept per vertex in Mesh::brickSeeds) that turns the projection by an angle and moves it by an offset under a tile, differently for each axis plane. A model made again gets the same patterns; every LOD of a brick the same one. The icon now draws the flecks on the UVs the .nif has (Mesh::uvs read back by FromNif). New setting glitter_random (1; 0 puts the same pattern on every brick as before). Non-glitter models are byte-identical (hash tests unchanged). Check in game: reprocess a model with several glitter bricks of the same shape; the fleck patterns differ from brick to brick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,7 +144,8 @@ namespace {
|
||||
out.I32(-1); // collision object
|
||||
}
|
||||
|
||||
// `glitter`: with a UV set projected for the glitter texture (UgcGlitter::Uv)
|
||||
// `glitter`: with a UV set projected for the glitter texture (UgcGlitter::Uv), placed by each vertex's brick
|
||||
// (Mesh::brickSeeds) when the glitter is random
|
||||
std::string TriShapeData(const UgcModel::Mesh& mesh, const UgcGlitter::Params* glitter = nullptr) {
|
||||
Writer out;
|
||||
const auto count = static_cast<uint16_t>(mesh.positions.size());
|
||||
@@ -191,7 +192,8 @@ namespace {
|
||||
}
|
||||
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);
|
||||
const uint32_t seed = glitter->random && v < mesh.brickSeeds.size() ? mesh.brickSeeds[v] : 0;
|
||||
const auto uv = UgcGlitter::Uv(mesh.positions[v], mesh.normals[v], glitter->tile, seed);
|
||||
out.Float(uv.x);
|
||||
out.Float(uv.y);
|
||||
}
|
||||
|
||||
@@ -53,12 +53,33 @@ namespace UgcGlitter {
|
||||
return levels;
|
||||
}
|
||||
|
||||
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile) {
|
||||
namespace {
|
||||
uint64_t SplitMix(uint64_t x) {
|
||||
x += 0x9E3779B97F4A7C15ull;
|
||||
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
|
||||
x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
|
||||
return x ^ (x >> 31);
|
||||
}
|
||||
// 0..1 from 24 bits of a hash
|
||||
float Unit(uint64_t bits) { return static_cast<float>(bits >> 40) / static_cast<float>(1ull << 24); }
|
||||
}
|
||||
|
||||
uint32_t BrickSeed(uint64_t modelSeed, uint32_t brick) {
|
||||
const auto hash = SplitMix(SplitMix(modelSeed ^ 0x676C6974746572ull) + brick);
|
||||
return static_cast<uint32_t>(hash >> 32) | 1u;
|
||||
}
|
||||
|
||||
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed) {
|
||||
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;
|
||||
const int plane = a.x >= a.y && a.x >= a.z ? 0 : a.y >= a.z ? 1 : 2;
|
||||
const glm::vec2 uv = (plane == 0 ? glm::vec2(position.z, position.y) : plane == 1 ? glm::vec2(position.x, position.z) : glm::vec2(position.x, position.y)) * scale;
|
||||
if (seed == 0) return uv;
|
||||
const auto hash = SplitMix((static_cast<uint64_t>(seed) << 2) | static_cast<uint64_t>(plane));
|
||||
const float angle = Unit(hash) * 6.28318530718f;
|
||||
const glm::vec2 offset(Unit(SplitMix(hash)), Unit(SplitMix(hash + 1)));
|
||||
const float c = std::cos(angle), s = std::sin(angle);
|
||||
return glm::vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y) + offset;
|
||||
}
|
||||
|
||||
float Sample(const std::vector<uint8_t>& alpha, const glm::vec2& uv) {
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
* 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.
|
||||
* same density, turned and moved by a number of each brick's own (BrickSeed) so no two bricks have the same pattern,
|
||||
* 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)
|
||||
@@ -19,6 +20,7 @@ namespace UgcGlitter {
|
||||
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
|
||||
bool random{ true }; // glitter_random: each brick its own pattern (BrickSeed), else the same on every brick
|
||||
|
||||
// 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; }
|
||||
@@ -33,8 +35,13 @@ namespace UgcGlitter {
|
||||
// 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);
|
||||
// A brick's number for placing its glitter (never 0), from the model's seed and the brick's index: the same for
|
||||
// the brick in every LOD and every time the model is made
|
||||
uint32_t BrickSeed(uint64_t modelSeed, uint32_t brick);
|
||||
|
||||
// A vertex's UV: its position on the axis plane its normal faces most, in tiles, turned by an angle and moved by
|
||||
// an offset (under a tile) that `seed` (the brick's BrickSeed) picks for each plane; seed 0 leaves it as it is
|
||||
glm::vec2 Uv(const glm::vec3& position, const glm::vec3& normal, float tile, uint32_t seed = 0);
|
||||
|
||||
// The texture's alpha (0..1) at `uv` (wrapping, bilinear)
|
||||
float Sample(const std::vector<uint8_t>& alpha, const glm::vec2& uv);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include "NifFile.h"
|
||||
#include "UgcGlitter.h"
|
||||
#include "UgcPalette.h"
|
||||
#include "tinyxml2.h"
|
||||
|
||||
@@ -164,6 +165,16 @@ namespace UgcModel {
|
||||
if (other.looks.empty()) looks.resize(positions.size(), eLook::PLASTIC);
|
||||
else looks.insert(looks.end(), other.looks.begin(), other.looks.end());
|
||||
}
|
||||
if (!brickSeeds.empty() || !other.brickSeeds.empty()) {
|
||||
brickSeeds.resize(base, 0);
|
||||
if (other.brickSeeds.empty()) brickSeeds.resize(positions.size(), 0);
|
||||
else brickSeeds.insert(brickSeeds.end(), other.brickSeeds.begin(), other.brickSeeds.end());
|
||||
}
|
||||
if (!uvs.empty() || !other.uvs.empty()) {
|
||||
uvs.resize(base, glm::vec2(0.0f));
|
||||
if (other.uvs.empty()) uvs.resize(positions.size(), glm::vec2(0.0f));
|
||||
else uvs.insert(uvs.end(), other.uvs.begin(), other.uvs.end());
|
||||
}
|
||||
indices.reserve(indices.size() + other.indices.size());
|
||||
for (const auto index : other.indices) indices.push_back(base + index);
|
||||
}
|
||||
@@ -232,6 +243,7 @@ namespace UgcModel {
|
||||
continue;
|
||||
}
|
||||
model.bricks++;
|
||||
const auto brickSeed = UgcGlitter::BrickSeed(options.seed, brick);
|
||||
const auto materialOf = [&part, &library](size_t index) {
|
||||
auto id = index < part.materials.size() ? part.materials[index] : (part.materials.empty() ? 0 : part.materials[0]);
|
||||
// Unknown colors are black in LU Toolbox (its name included, so black's variation too). A color LU
|
||||
@@ -306,6 +318,7 @@ namespace UgcModel {
|
||||
mesh.colors.push_back(color);
|
||||
if (&mesh == &model.opaque) model.opaque.glow.push_back(glow);
|
||||
mesh.looks.push_back(look);
|
||||
mesh.brickSeeds.push_back(brickSeed);
|
||||
}
|
||||
for (const auto i : geometry.indices) mesh.indices.push_back(base + i);
|
||||
}
|
||||
@@ -363,6 +376,7 @@ namespace UgcModel {
|
||||
glm::vec4 color = materialColor;
|
||||
if (vertexColors) color *= glm::vec4(source.colors[v * 4], source.colors[v * 4 + 1], source.colors[v * 4 + 2], source.colors[v * 4 + 3]) / 255.0f;
|
||||
mesh.colors.push_back(color);
|
||||
if (source.uvs.size() == count * 2) mesh.uvs.emplace_back(source.uvs[v * 2], source.uvs[v * 2 + 1]);
|
||||
}
|
||||
mesh.indices.assign(source.indices.begin(), source.indices.end());
|
||||
// Normals from the faces when the file has none
|
||||
@@ -406,6 +420,8 @@ namespace UgcModel {
|
||||
if (source < mesh.colors.size()) piece.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) piece.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) piece.looks.push_back(mesh.looks[source]);
|
||||
if (source < mesh.brickSeeds.size()) piece.brickSeeds.push_back(mesh.brickSeeds[source]);
|
||||
if (source < mesh.uvs.size()) piece.uvs.push_back(mesh.uvs[source]);
|
||||
}
|
||||
piece.indices.push_back(it->second);
|
||||
}
|
||||
@@ -428,6 +444,8 @@ namespace UgcModel {
|
||||
if (source < mesh.colors.size()) kept.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) kept.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) kept.looks.push_back(mesh.looks[source]);
|
||||
if (source < mesh.brickSeeds.size()) kept.brickSeeds.push_back(mesh.brickSeeds[source]);
|
||||
if (source < mesh.uvs.size()) kept.uvs.push_back(mesh.uvs[source]);
|
||||
}
|
||||
kept.indices.push_back(remap[source]);
|
||||
}
|
||||
@@ -461,6 +479,8 @@ namespace UgcModel {
|
||||
if (source < mesh.colors.size()) current.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) current.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) current.looks.push_back(mesh.looks[source]);
|
||||
if (source < mesh.brickSeeds.size()) current.brickSeeds.push_back(mesh.brickSeeds[source]);
|
||||
if (source < mesh.uvs.size()) current.uvs.push_back(mesh.uvs[source]);
|
||||
}
|
||||
current.indices.push_back(it->second);
|
||||
}
|
||||
@@ -514,6 +534,8 @@ namespace UgcModel {
|
||||
if (source < mesh.colors.size()) half.colors.push_back(mesh.colors[source]);
|
||||
if (source < mesh.glow.size()) half.glow.push_back(mesh.glow[source]);
|
||||
if (source < mesh.looks.size()) half.looks.push_back(mesh.looks[source]);
|
||||
if (source < mesh.brickSeeds.size()) half.brickSeeds.push_back(mesh.brickSeeds[source]);
|
||||
if (source < mesh.uvs.size()) half.uvs.push_back(mesh.uvs[source]);
|
||||
}
|
||||
half.indices.push_back(remap[source]);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ namespace UgcModel {
|
||||
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 (transparent meshes: plastic or glitter)
|
||||
// Per vertex: its brick's UgcGlitter::BrickSeed, which places the brick's glitter; empty when not known (a
|
||||
// mesh read from a .nif)
|
||||
std::vector<uint32_t> brickSeeds;
|
||||
// Per vertex: the UV set of a mesh read from a .nif (its glitter's, placed when it was made); empty otherwise
|
||||
std::vector<glm::vec2> uvs;
|
||||
std::vector<uint32_t> indices;
|
||||
|
||||
size_t TriangleCount() const { return indices.size() / 3; }
|
||||
|
||||
@@ -384,7 +384,10 @@ namespace UgcRender {
|
||||
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));
|
||||
// On the mesh's own UVs (read from the .nif: each brick's pattern placed as it was made), else projected
|
||||
const auto uv = mesh.uvs.size() == mesh.positions.size() ? mesh.uvs[i0] * w0 + mesh.uvs[i1] * w1 + mesh.uvs[i2] * w2 :
|
||||
UgcGlitter::Uv(position, normal, options.glitter.tile);
|
||||
const float fleck = UgcGlitter::Sample(glitterAlpha, uv);
|
||||
base = glm::vec4(glm::mix(glm::vec3(base), glm::vec3(1.0f), fleck), base.a);
|
||||
}
|
||||
if (look == UgcModel::eLook::PLASTIC || look == UgcModel::eLook::GLITTER) {
|
||||
|
||||
@@ -122,6 +122,7 @@ namespace {
|
||||
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);
|
||||
settings.shaders.glitterParams.random = Setting<int32_t>("glitter_random", 1) != 0;
|
||||
// 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 } }) {
|
||||
|
||||
Reference in New Issue
Block a user