From 73600acb4e4f91c42d81106cbb8548902a81c3e9 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Wed, 30 Sep 2026 22:49:47 -0500 Subject: [PATCH] feat(ugc): live accurate color variation (-6 to 6 per channel, opaque bricks only) Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/routes/SettingsCatalog.cpp | 3 ++- dUgcServer/Bricks/UgcPalette.cpp | 10 ++++++++++ dUgcServer/Bricks/UgcPalette.h | 6 ++++++ dUgcServer/Model/UgcModel.cpp | 4 +++- dUgcServer/Model/UgcModel.h | 3 +++ dUgcServer/Processing/UgcJobs.cpp | 3 ++- dUgcServer/UgcServer.cpp | 1 + resources/ugcconfig.ini | 5 ++++- tests/dUgcTests/UgcTests.cpp | 19 +++++++++++++++++++ 9 files changed, 50 insertions(+), 4 deletions(-) diff --git a/dDashboardServer/routes/SettingsCatalog.cpp b/dDashboardServer/routes/SettingsCatalog.cpp index 468915cf9..bba8e24c9 100644 --- a/dDashboardServer/routes/SettingsCatalog.cpp +++ b/dDashboardServer/routes/SettingsCatalog.cpp @@ -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")); diff --git a/dUgcServer/Bricks/UgcPalette.cpp b/dUgcServer/Bricks/UgcPalette.cpp index 7b2d8c34a..805d38ec5 100644 --- a/dUgcServer/Bricks/UgcPalette.cpp +++ b/dUgcServer/Bricks/UgcPalette.cpp @@ -208,6 +208,16 @@ namespace UgcPalette { return color * (newValue / value); } + int32_t LiveVariationStep(float random) { + return std::clamp(static_cast(std::floor(random * 13.0f)), 0, 12) - 6; + } + + glm::vec3 ApplyLiveVariation(const glm::vec3& linear, float random) { + const auto step = static_cast(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(brick) << 32)) ^ material); return static_cast(bits >> 40) / static_cast(1ull << 24); diff --git a/dUgcServer/Bricks/UgcPalette.h b/dUgcServer/Bricks/UgcPalette.h index 2c90de290..89bd2de08 100644 --- a/dUgcServer/Bricks/UgcPalette.h +++ b/dUgcServer/Bricks/UgcPalette.h @@ -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 diff --git a/dUgcServer/Model/UgcModel.cpp b/dUgcServer/Model/UgcModel.cpp index 152d76c00..fd90722cf 100644 --- a/dUgcServer/Model/UgcModel.cpp +++ b/dUgcServer/Model/UgcModel.cpp @@ -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)); } diff --git a/dUgcServer/Model/UgcModel.h b/dUgcServer/Model/UgcModel.h index f4956af1a..36a411421 100644 --- a/dUgcServer/Model/UgcModel.h +++ b/dUgcServer/Model/UgcModel.h @@ -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 diff --git a/dUgcServer/Processing/UgcJobs.cpp b/dUgcServer/Processing/UgcJobs.cpp index 3c5224ba1..6fb8b5d53 100644 --- a/dUgcServer/Processing/UgcJobs.cpp +++ b/dUgcServer/Processing/UgcJobs.cpp @@ -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); diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index 51b213622..7bd8c46ae 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -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("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("transparent_opacity", 58.82f), 0.0f, 100.0f); settings.build.brightness = std::clamp(Setting("color_brightness", 100.0f), 0.0f, 200.0f); { diff --git a/resources/ugcconfig.ini b/resources/ugcconfig.ini index 28f8b49be..1c40b6a2b 100644 --- a/resources/ugcconfig.ini +++ b/resources/ugcconfig.ini @@ -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 diff --git a/tests/dUgcTests/UgcTests.cpp b/tests/dUgcTests/UgcTests.cpp index 36152f370..0b6807d46 100644 --- a/tests/dUgcTests/UgcTests.cpp +++ b/tests/dUgcTests/UgcTests.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -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 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)); +}