feat(ugc): live accurate color variation (-6 to 6 per channel, opaque bricks only)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-30 22:49:47 -05:00
parent 030efdaa0f
commit 73600acb4e
9 changed files with 50 additions and 4 deletions

View File

@@ -479,7 +479,8 @@ namespace {
c.Add(Int(UGC, "max_attempts", "Attempts before giving up", "", "3", 1, 100, true));
c.AddSection("UGC models", "How a model's mesh is made from its bricks (LU Toolbox's steps).", eLayout::ROWS, Condition{ MASTER, "enable_ugc_server", { "1" } });
c.Add(Labels(Choice(UGC, "color_palette", "Colors", "LU Toolbox's LU palette (unknown colors black), or the client's Materials.xml.", "lu_toolbox", { "lu_toolbox", "brickdb" }), { "LU Toolbox", "Brick database" }));
c.Add(Unit(Float(UGC, "color_variation", "Color variation", "Each brick's brightness is shifted by up to this much (the same every time the model is made). 0: none.", "5", 0, 100), "%"));
c.Add(Labels(Choice(UGC, "color_variation_mode", "Color variation kind", "toolbox: LU Toolbox's, each brick's brightness shifted by up to Color variation. live: live accurate, a whole number from -6 to 6 added to each of a brick's red, green and blue (0-255), transparent bricks left as they are; Color variation is not used. The same every time a model is made.", "toolbox", { "toolbox", "live" }), { "LU Toolbox", "Live accurate" }));
c.Add(Unit(Float(UGC, "color_variation", "Color variation", "Each brick's brightness is shifted by up to this much (the same every time the model is made). 0: none. Only for the LU Toolbox kind.", "5", 0, 100), "%"));
c.Add(Unit(Float(UGC, "transparent_opacity", "Transparent opacity", "", "58.82", 0, 100), "%"));
c.Add(Unit(Float(UGC, "color_brightness", "Model color brightness", "The models' vertex colors (not the icons'). 100: as the palette has them; lower is darker. Models already made keep theirs until they are made again.", "100", 0, 200), "%"));
c.Add(Text(UGC, "transparent_colors", "Always transparent colors", "LEGO color ids drawn transparent whatever Materials.xml says, comma separated: by default 129, the transparent glitter violet, which Materials.xml has opaque (none: no colors). Their opacity is Transparent opacity.", "129"));

View File

@@ -208,6 +208,16 @@ namespace UgcPalette {
return color * (newValue / value);
}
int32_t LiveVariationStep(float random) {
return std::clamp(static_cast<int32_t>(std::floor(random * 13.0f)), 0, 12) - 6;
}
glm::vec3 ApplyLiveVariation(const glm::vec3& linear, float random) {
const auto step = static_cast<float>(LiveVariationStep(random));
const auto srgb = glm::round(LinearToSrgb(linear) * 255.0f);
return SrgbToLinear(glm::clamp(srgb + step, 0.0f, 255.0f) / 255.0f);
}
float BrickRandom(uint64_t seed, uint32_t brick, uint32_t material) {
const auto bits = SplitMix(SplitMix(seed ^ (static_cast<uint64_t>(brick) << 32)) ^ material);
return static_cast<float>(bits >> 40) / static_cast<float>(1ull << 24);

View File

@@ -41,6 +41,12 @@ namespace UgcPalette {
*/
glm::vec3 ApplyVariation(const glm::vec3& color, float variationPercent, float random);
// The live accurate color variation of one color (linear): a whole number from -6 to 6 picked by `random` (0..1)
// added to each of its 0-255 sRGB channels, clamped to 0..255
glm::vec3 ApplyLiveVariation(const glm::vec3& linear, float random);
// That number: -6 to 6 inclusive, each equally likely
int32_t LiveVariationStep(float random);
/**
* The random number (0..1) of one brick's material: the same for the same seed, brick and material every time,
* so making a model again gives the same colors, and the same in every LOD (LU Toolbox restarts its random sequence

View File

@@ -289,7 +289,9 @@ namespace UgcModel {
linear = UgcPalette::SrgbToLinear(glm::vec3(material.r, material.g, material.b) / 255.0f);
if (transparent) alpha = material.a < 255 ? material.a / 255.0f : std::clamp(options.transparentOpacity / 100.0f, 0.0f, 1.0f);
}
if (options.colorVariation > 0.0f) {
if (options.variationMode == BuildOptions::eVariation::LIVE) {
if (!transparent) linear = UgcPalette::ApplyLiveVariation(linear, UgcPalette::BrickRandom(options.seed, brick, colorId));
} else if (options.colorVariation > 0.0f) {
const float variation = options.colorVariation * (luToolbox ? UgcPalette::VariationScale(colorId) : 1.0f);
linear = UgcPalette::ApplyVariation(linear, variation, UgcPalette::BrickRandom(options.seed, brick, colorId));
}

View File

@@ -99,6 +99,9 @@ namespace UgcModel {
struct BuildOptions {
ePalette palette{ ePalette::LU_TOOLBOX };
float colorVariation{ 5.0f }; // percent, 0: none (LU Toolbox: Apply Color Variation, 5%)
// LU Toolbox's (colorVariation percent of the brightness), or live accurate: -6 to 6 added to each 0-255
// channel, opaque bricks only (colorVariation unused)
enum class eVariation : uint8_t { TOOLBOX, LIVE } variationMode{ eVariation::TOOLBOX };
uint64_t seed{}; // of the variation's random numbers
float transparentOpacity{ 58.82f }; // percent, transparent bricks' vertex alpha (LU Toolbox palette only)
float brightness{ 100.0f }; // percent, the models' vertex colors (not icons'); 100: as the palette has them

View File

@@ -369,7 +369,8 @@ namespace UgcJobs {
stats["ms"] = { { "build", std::lround(buildMs) }, { "hiddenSurfaces", std::lround(hsrMs) }, { "ambientOcclusion", std::lround(aoMs) },
{ "icon", std::lround(iconMs) }, { "total", std::lround(Since(started)) } };
stats["settings"] = { { "palette", settings.build.palette == UgcModel::ePalette::LU_TOOLBOX ? "lu_toolbox" : "brickdb" },
{ "colorVariation", settings.build.colorVariation }, { "transparentOpacity", settings.build.transparentOpacity },
{ "colorVariation", settings.build.colorVariation },
{ "colorVariationMode", settings.build.variationMode == UgcModel::BuildOptions::eVariation::LIVE ? "live" : "toolbox" }, { "transparentOpacity", settings.build.transparentOpacity },
{ "removeHiddenFaces", settings.hsr.enabled }, { "groundPlane", settings.hsr.groundPlane }, { "hsrResolution", settings.hsr.resolution },
{ "ao", settings.ao.enabled }, { "aoDistance", settings.ao.distance }, { "aoSamples", settings.ao.samples }, { "aoStrength", settings.ao.strength } };
const auto madeWith = MadeWith(settings);

View File

@@ -94,6 +94,7 @@ namespace {
UgcJobs::Settings settings;
settings.build.palette = Game::config->GetValue("color_palette") == "brickdb" ? UgcModel::ePalette::BRICKDB : UgcModel::ePalette::LU_TOOLBOX;
settings.build.colorVariation = std::clamp(Setting<float>("color_variation", 5.0f), 0.0f, 100.0f);
settings.build.variationMode = Game::config->GetValue("color_variation_mode") == "live" ? UgcModel::BuildOptions::eVariation::LIVE : UgcModel::BuildOptions::eVariation::TOOLBOX;
settings.build.transparentOpacity = std::clamp(Setting<float>("transparent_opacity", 58.82f), 0.0f, 100.0f);
settings.build.brightness = std::clamp(Setting<float>("color_brightness", 100.0f), 0.0f, 200.0f);
{

View File

@@ -44,12 +44,15 @@ max_attempts=3
# Models are made like LU Toolbox's Process Model and Bake Lighting make them; these default to its settings.
# color_palette: lu_toolbox (its LU palette, unknown colors black) or brickdb (the client's Materials.xml).
# color_variation: each brick's brightness is shifted by up to this percent (the same every time; 0: none).
# color_variation_mode: toolbox (LU Toolbox's: the brightness shifted by up to color_variation) or live (live accurate:
# a whole number from -6 to 6 added to each of an opaque brick's 0-255 red, green and blue; transparent bricks unchanged).
# color_variation: each brick's brightness is shifted by up to this percent (the same every time; 0: none; toolbox only).
# transparent_opacity: transparent bricks' opacity in percent.
# color_brightness: the models' colors in percent (not icons'; 100: as the palette has them, lower is darker).
# transparent_colors: color ids drawn transparent whatever Materials.xml says, comma separated (empty: the default, 129,
# the transparent glitter violet, which Materials.xml has opaque; none: no colors).
color_palette=lu_toolbox
color_variation_mode=toolbox
color_variation=5
transparent_opacity=58.82
color_brightness=100

View File

@@ -4,6 +4,7 @@
#include <cstring>
#include <filesystem>
#include <fstream>
#include <set>
#include <sstream>
#include <unistd.h>
@@ -2234,3 +2235,21 @@ TEST(UgcRays, OtherBackendsMakeTheSameOcclusion) {
EXPECT_EQ(made.files.at("icon.png"), embreeModel.files.at("icon.png")) << UgcRays::Name(backend);
}
}
TEST(UgcPalette, LiveVariationAddsMinusSixToSix) {
// Every step from -6 to 6 is reachable and nothing outside it
std::set<int32_t> steps;
for (int i = 0; i < 1000; i++) steps.insert(UgcPalette::LiveVariationStep(i / 1000.0f));
steps.insert(UgcPalette::LiveVariationStep(1.0f));
EXPECT_EQ(steps.size(), 13u);
EXPECT_EQ(*steps.begin(), -6);
EXPECT_EQ(*steps.rbegin(), 6);
// The step is added to each 0-255 sRGB channel and clamped
const auto color = UgcPalette::SrgbToLinear(glm::vec3(100.0f, 0.0f, 253.0f) / 255.0f);
const auto up = glm::round(UgcPalette::LinearToSrgb(UgcPalette::ApplyLiveVariation(color, 0.999f)) * 255.0f);
EXPECT_EQ(up, glm::vec3(106.0f, 6.0f, 255.0f));
const auto down = glm::round(UgcPalette::LinearToSrgb(UgcPalette::ApplyLiveVariation(color, 0.0f)) * 255.0f);
EXPECT_EQ(down, glm::vec3(94.0f, 0.0f, 247.0f));
const auto same = glm::round(UgcPalette::LinearToSrgb(UgcPalette::ApplyLiveVariation(color, 0.5f)) * 255.0f);
EXPECT_EQ(same, glm::vec3(100.0f, 0.0f, 253.0f));
}