feat(ugc): color_brightness and transparent_colors

color_brightness (percent, default 100: unchanged) scales the models' vertex
colors, not the icons', for servers where the made models look brighter than
they should. transparent_colors names color ids drawn transparent whatever
Materials.xml says: 129 (Tr. Bright Bluish Violet with Glitter) has alpha
255 there, so it came out opaque; a named color gets transparent_opacity.
Both default to off, so models come out byte for byte as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 11:07:07 -05:00
parent 2384f46a57
commit 41d336878a
7 changed files with 60 additions and 3 deletions

View File

@@ -442,6 +442,8 @@ namespace {
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(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 (e.g. 129, the transparent glitter violet, which Materials.xml has opaque). Their opacity is Transparent opacity.", ""));
c.Add(Text(UGC, "lods", "Levels of detail", "brickprimitives levels made, 0 (most detailed) to 2, e.g. 0,2.", "0,2"));
c.Add(Float(UGC, "lod_distance_0", "LOD 0 distance", "", "0", 0, 100000));
c.Add(Float(UGC, "lod_distance_1", "LOD 1 distance", "", "50", 0, 100000));

View File

@@ -247,7 +247,8 @@ namespace UgcModel {
for (size_t index = 0; index < design->size(); index++) {
const auto id = index < part.materials.size() ? part.materials[index] : (part.materials.empty() ? 0 : part.materials[0]);
const auto toolboxId = materialOf(index);
transparent = transparent && (luToolbox && inToolbox(toolboxId) ? UgcPalette::IsTransparent(toolboxId) : library.GetMaterial(luToolbox ? toolboxId : id).Transparent());
const auto named = options.transparentColors.contains(luToolbox ? toolboxId : id);
transparent = transparent && (named || (luToolbox && inToolbox(toolboxId) ? UgcPalette::IsTransparent(toolboxId) : library.GetMaterial(luToolbox ? toolboxId : id).Transparent()));
}
auto& mesh = transparent ? model.transparent : model.opaque;
if (transparent) model.transparentBricks.push_back(mesh.indices.size());
@@ -263,7 +264,7 @@ namespace UgcModel {
colorId = materialOf(index);
const auto material = library.GetMaterial(colorId);
linear = UgcPalette::SrgbToLinear(glm::vec3(material.r, material.g, material.b) / 255.0f);
if (transparent) alpha = material.a / 255.0f;
if (transparent) alpha = material.a < 255 ? material.a / 255.0f : std::clamp(options.transparentOpacity / 100.0f, 0.0f, 1.0f);
} else if (luToolbox) {
colorId = materialOf(index);
linear = *UgcPalette::Linear(colorId, options.icon);
@@ -275,12 +276,13 @@ namespace UgcModel {
colorId = index < part.materials.size() ? part.materials[index] : (part.materials.empty() ? 0 : part.materials[0]);
const auto material = library.GetMaterial(colorId);
linear = UgcPalette::SrgbToLinear(glm::vec3(material.r, material.g, material.b) / 255.0f);
if (transparent) alpha = material.a / 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) {
const float variation = options.colorVariation * (luToolbox ? UgcPalette::VariationScale(colorId) : 1.0f);
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;
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);

View File

@@ -3,6 +3,7 @@
#include <array>
#include <cstdint>
#include <map>
#include <set>
#include <optional>
#include <string>
#include <string_view>
@@ -93,6 +94,8 @@ namespace UgcModel {
float colorVariation{ 5.0f }; // percent, 0: none (LU Toolbox: Apply Color Variation, 5%)
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
std::set<uint32_t> transparentColors; // color ids drawn transparent whatever Materials.xml says (129: its alpha is 255)
bool icon{}; // the icon renderer's color corrections
uint32_t lod{}; // brickprimitives level
LookRules looks; // which colors are metal and glow (Mesh::looks)

View File

@@ -79,6 +79,15 @@ namespace {
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.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);
{
std::stringstream stream(Game::config->GetValue("transparent_colors"));
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.transparentColors.insert(*color);
}
}
const auto lods = ParseLods(Game::config->GetValue("lods").empty() ? "0,2" : Game::config->GetValue("lods"));
if (!lods.empty()) settings.lods = lods;
settings.lodDistances.lod0 = Setting<float>("lod_distance_0", 0.0f);

View File

@@ -117,6 +117,10 @@ defaults. The table below goes through it step by step.
`Materials.xml`): LU's colors, the LDD colors LU doesn't have mapped onto the nearest LU one, colors LU Toolbox doesn't know but the
client's `Materials.xml` has (colors added to the brick database) from `Materials.xml`, unknown ones black. A
brick is transparent only when all of its materials are; transparent bricks get `transparent_opacity` (58.82%).
`transparent_colors` names colors that are transparent whatever `Materials.xml` says (129, "Tr. Bright Bluish
Violet with Glitter", has alpha 255 there); an opaque-listed color named there gets `transparent_opacity`.
`color_brightness` (percent, default 100: unchanged) scales the models' colors after the variation below, not
the icons'.
3. Color variation: each material of each brick has its brightness shifted like LU Toolbox's "Apply Color Variation":
the color's HSV value is taken to a 1/2.224 gamma, moved by a random amount of up to `color_variation`/200 (5%:
0.025) either way, times the color's own amount (black 0.4, orange 1.5, ...), clamped and taken back; hue and

View File

@@ -46,9 +46,14 @@ max_attempts=3
# 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).
# 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 (e.g. 129, the
# transparent glitter violet, which Materials.xml has opaque). Empty: none.
color_palette=lu_toolbox
color_variation=5
transparent_opacity=58.82
color_brightness=100
transparent_colors=
# Levels of detail made, from the client's brickprimitives (0 most detailed, 2 least), and the distances they're
# drawn at (LU Toolbox picks each level's range from these by which levels are made; lod_cull: drawn up to).

View File

@@ -1272,3 +1272,35 @@ TEST(UgcShaders, IconsDrawGlowUnlitAndMetalShiny) {
EXPECT_EQ(UgcModel::FromNif(*read, UgcJobs::Shaders{}.TagLooks()).opaque.looks, std::vector<UgcModel::eLook>(4, UgcModel::eLook::GLOW));
EXPECT_TRUE(UgcModel::FromNif(*read).opaque.looks.empty());
}
// color_brightness scales the models' vertex colors (not icons'); transparent_colors makes a color Materials.xml has
// opaque transparent, at transparent_opacity
TEST(UgcModel, BrightnessAndTransparentColors) {
UgcBricks::BrickLibrary library(MakeRes(), 0);
constexpr uint32_t ADDED = 50001;
library.SetMaterials({ { ADDED, { 0, 200, 100, 255 } } });
std::string error;
const auto brick = UgcModel::ParseLxfml("<LXFML versionMajor=\"5\"><Bricks><Brick><Part designID=\"3001\" materials=\"" + std::to_string(ADDED) +
"\"><Bone transformation=\"1,0,0,0,1,0,0,0,1,0,0,0\"/></Part></Brick></Bricks></LXFML>", error);
UgcModel::BuildOptions options;
options.colorVariation = 0.0f;
const auto normal = UgcModel::Build(brick, library, options);
ASSERT_FALSE(normal.opaque.colors.empty());
EXPECT_TRUE(normal.transparent.colors.empty());
options.brightness = 50.0f;
const auto darker = UgcModel::Build(brick, library, options);
ASSERT_FALSE(darker.opaque.colors.empty());
const auto linear = UgcPalette::SrgbToLinear(glm::vec3(normal.opaque.colors[0])) * 0.5f;
EXPECT_NEAR(darker.opaque.colors[0].g, UgcPalette::LinearToSrgb(linear).g, 1e-4f);
options.icon = true;
EXPECT_EQ(UgcModel::Build(brick, library, options).opaque.colors[0], UgcModel::Build(brick, library, [&] { auto o = options; o.brightness = 100.0f; return o; }()).opaque.colors[0]);
options = {};
options.colorVariation = 0.0f;
options.transparentColors.insert(ADDED);
const auto seeThrough = UgcModel::Build(brick, library, options);
EXPECT_TRUE(seeThrough.opaque.colors.empty());
ASSERT_FALSE(seeThrough.transparent.colors.empty());
EXPECT_NEAR(seeThrough.transparent.colors[0].a, 0.5882f, 1e-4f);
}