feat(ugc): the color variation settings apply to LU Toolbox in Blender too

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 73600acb4e
commit 3e5acd42ab
8 changed files with 85 additions and 11 deletions

View File

@@ -89,7 +89,7 @@ namespace UgcToolbox {
Worker::~Worker() = default;
void Worker::Configure(const Config& config) { std::lock_guard lock(m_ConfigMutex); m_Config = config; }
Config Worker::GetConfig() const { std::lock_guard lock(m_ConfigMutex); return m_Config; }
Result Worker::Make(uint64_t, const std::string&, const std::vector<uint32_t>&, const std::function<void(double, const Pause&)>&) {
Result Worker::Make(uint64_t, const std::string&, const std::vector<uint32_t>&, const std::function<void(double, const Pause&)>&, const UgcToolboxProtocol::ColorVariation&) {
return { false, Problem(GetConfig()) };
}
void Worker::Stop() {}
@@ -386,7 +386,8 @@ namespace UgcToolbox {
return status;
}
Result Worker::Make(uint64_t id, const std::string& lxfml, const std::vector<uint32_t>& lods, const std::function<void(double, const Pause&)>& pace) {
Result Worker::Make(uint64_t id, const std::string& lxfml, const std::vector<uint32_t>& lods, const std::function<void(double, const Pause&)>& pace,
const UgcToolboxProtocol::ColorVariation& variation) {
Result result;
const auto asked = std::chrono::steady_clock::now();
std::unique_lock lock(m_JobMutex);
@@ -434,7 +435,7 @@ namespace UgcToolbox {
};
const auto requestId = m_NextId++;
if (!process.Send(UgcToolboxProtocol::MakeRequest(requestId, input, output, lods))) {
if (!process.Send(UgcToolboxProtocol::MakeRequest(requestId, input, output, lods, variation))) {
result.error = "Blender isn't taking work (" + process.ExitReason() + ")";
StopLocked(true);
cleanUp();

View File

@@ -85,7 +85,7 @@ namespace UgcToolbox {
*/
using Pause = std::function<void(bool)>;
Result Make(uint64_t id, const std::string& lxfml, const std::vector<uint32_t>& lods,
const std::function<void(double cpuSeconds, const Pause& pause)>& pace = {});
const std::function<void(double cpuSeconds, const Pause& pause)>& pace = {}, const UgcToolboxProtocol::ColorVariation& variation = {});
// Stops Blender (asks it to quit, then kills it); the next Make starts it again
void Stop();

View File

@@ -80,10 +80,14 @@ namespace UgcJobs {
// LU Toolbox makes the .nif. Blender's CPU time is this thread's (the budget, the make's CPU time), and the
// budget is kept by pausing Blender.
step = std::chrono::steady_clock::now();
UgcToolboxProtocol::ColorVariation variation;
variation.mode = settings.build.variationMode == UgcModel::BuildOptions::eVariation::LIVE ? "live" : "toolbox";
variation.percent = settings.build.colorVariation;
variation.seed = id;
const auto made = toolbox.Make(id, lxfml, lods, [](double cpuSeconds, const UgcToolbox::Worker::Pause& pause) {
UgcThrottle::Charge(cpuSeconds);
UgcThrottle::Checkpoint(pause);
});
}, variation);
const double toolboxMs = Since(step);
if (!made.ok) {
outcome.error = made.error;

View File

@@ -7,8 +7,10 @@ namespace UgcToolboxProtocol {
return message.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace) + "\n";
}
nlohmann::json MakeRequest(uint64_t id, const std::string& input, const std::string& output, const std::vector<uint32_t>& lods) {
return { { "cmd", "make" }, { "id", id }, { "input", input }, { "output", output }, { "lods", lods } };
nlohmann::json MakeRequest(uint64_t id, const std::string& input, const std::string& output, const std::vector<uint32_t>& lods,
const ColorVariation& variation) {
return { { "cmd", "make" }, { "id", id }, { "input", input }, { "output", output }, { "lods", lods },
{ "colorVariation", { { "mode", variation.mode }, { "percent", variation.percent }, { "seed", variation.seed } } } };
}
void LineReader::Feed(std::string_view data) {

View File

@@ -21,9 +21,18 @@ namespace UgcToolboxProtocol {
// A message as its line: compact JSON with "dlutb" set, and a newline
std::string Frame(nlohmann::json message);
// The color variation a make uses (the color_variation* settings): "toolbox" (LU Toolbox's Apply Color Variation at
// `percent`; 0: off) or "live" (live accurate: -6 to 6 added to each 0-255 channel of opaque bricks, from `seed`)
struct ColorVariation {
std::string mode{ "toolbox" };
float percent{ 5.0f };
uint64_t seed{};
};
// The request to make one model: `input` the LXFML file, `output` the .nif to write, `lods` the brickprimitives
// levels to import (LU Toolbox's importLOD0..3)
nlohmann::json MakeRequest(uint64_t id, const std::string& input, const std::string& output, const std::vector<uint32_t>& lods);
// levels to import (LU Toolbox's importLOD0..3), and its color variation
nlohmann::json MakeRequest(uint64_t id, const std::string& input, const std::string& output, const std::vector<uint32_t>& lods,
const ColorVariation& variation = {});
/**
* Splits what is read from the worker into lines, however it arrives (parts of lines, many at once). A line longer

View File

@@ -6,11 +6,14 @@
#
# Each model goes through LU Toolbox itself, as LU-Toolbox-Standalone's lu_batch_driver.py runs it (its functions are
# used, not copied): the LU Toolbox importer, Process Model, Bake Lighting and the niftools .nif export for LEGO
# Universe. Nothing of LU Toolbox is changed or re-implemented here; this file only keeps Blender up between models.
# Universe. Nothing of LU Toolbox is changed or re-implemented here; this file only keeps Blender up between models,
# and sets the make's color variation (set_color_variation: the percent, or the live accurate one in place of LU
# Toolbox's for that make).
#
# The protocol: one JSON object per line. Requests come on stdin; replies go to the original stdout, which only this
# script writes to (everything Blender and the add-ons print is moved to stderr). Every message has "dlutb": 1.
# -> {"dlutb":1,"cmd":"make","id":7,"input":"/x/7.lxfml","output":"/x/7.nif","lods":[0,2]}
# -> {"dlutb":1,"cmd":"make","id":7,"input":"/x/7.lxfml","output":"/x/7.nif","lods":[0,2],
# "colorVariation":{"mode":"toolbox"|"live","percent":5,"seed":..}}
# <- {"dlutb":1,"type":"done","id":7,"ok":true,"ms":{"reset":..,"import":..,"process":..,"bake":..,"export":..}}
# <- {"dlutb":1,"type":"done","id":7,"ok":false,"error":"..."}
# -> {"dlutb":1,"cmd":"ping","id":8} <- {"dlutb":1,"type":"pong","id":8}
@@ -171,6 +174,7 @@ class Worker:
lod_kwargs = {f"importLOD{n}": n in lods for n in range(4)} if lods else None
self.driver.try_import_lxf(os.path.abspath(request["input"]), op_override=None, lod_kwargs=lod_kwargs)
lap("import")
set_color_variation(request.get("colorVariation") or {})
self.driver.call_op("lutb.process_model", "Process Model")
lap("process")
self.driver.ensure_vertex_colors_exist()
@@ -186,6 +190,52 @@ class Worker:
return ms
def srgb_to_linear(c):
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
def linear_to_srgb(c):
return c * 12.92 if c <= 0.0031308 else 1.055 * c ** (1 / 2.4) - 0.055
def set_color_variation(variation):
"""The make's color variation (the UGC server's color_variation_mode and color_variation settings) on LU Toolbox's
Process Model. toolbox: its own Apply Color Variation at that percent. live (live accurate): instead of its
variation, a whole number from -6 to 6 is added to each 0-255 sRGB channel of each material of each opaque object,
transparent ones left alone; the numbers come from the model's id and repeat for every LOD, as LU Toolbox's do."""
import bpy
import random
from lu_toolbox import process_model
operator = process_model.LUTB_OT_process_model
if "_dlu_toolbox_variation" not in operator.__dict__:
operator._dlu_toolbox_variation = operator.apply_color_variation
scene = bpy.context.scene
if variation.get("mode") != "live":
operator.apply_color_variation = operator._dlu_toolbox_variation
percent = float(variation.get("percent", 5.0))
scene.lutb_use_color_variation = percent > 0
if percent > 0:
scene.lutb_color_variation = percent
return
seed = int(variation.get("seed", 0))
def live_variation(self, context, collections):
for collection in collections:
for lod_collection in collection.children:
rng = random.Random(seed)
for obj in list(lod_collection.objects):
if obj.type != "MESH" or obj.get(process_model.IS_TRANSPARENT):
continue
for material in obj.data.materials:
step = rng.randint(-6, 6)
color = [srgb_to_linear(min(max(round(linear_to_srgb(c) * 255) + step, 0), 255) / 255)
for c in material.diffuse_color[:3]]
material.diffuse_color = (*color, 1.0)
operator.apply_color_variation = live_variation
scene.lutb_use_color_variation = True
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--standalone", required=True)

View File

@@ -136,6 +136,10 @@ and the notices (see Threads). Cars and rockets are made once per combination of
0.025) either way, times the color's own amount (black 0.4, orange 1.5, ...), clamped and taken back; hue and
saturation stay. The random number comes from the model's id, the brick's index and the material, so making a
model again gives the same colors, and every LOD the same (LU Toolbox restarts its random sequence per LOD).
With `color_variation_mode=live` ("Live accurate") instead: a whole number from -6 to 6 (each equally likely, from
the same random number) is added to each of the color's 0-255 sRGB channels, clamped; transparent bricks keep their
color, and `color_variation` isn't used. Both kinds apply to either processor: with `toolbox-blender` the worker
sets LU Toolbox's own variation to `color_variation`, or puts the live accurate one in its place for that make.
4. Faces nobody can see are removed from the opaque bricks (they're rendered from 42 directions and triangles that
never show are dropped; transparent bricks hide nothing and aren't touched; see "Hidden faces" below).
`hsr_ground_plane=1` also drops what can only be seen from below.

View File

@@ -197,6 +197,10 @@ TEST(UgcToolboxProtocol, FramesOneMessagePerLine) {
const auto request = UgcToolboxProtocol::MakeRequest(7, "/w/7.lxfml", "/w/7.nif", { 0, 2 });
EXPECT_EQ(request["cmd"], "make");
EXPECT_EQ(request["lods"], nlohmann::json::array({ 0, 2 }));
EXPECT_EQ(request["colorVariation"]["mode"], "toolbox");
const auto live = UgcToolboxProtocol::MakeRequest(8, "/w/8.lxfml", "/w/8.nif", { 0 }, { "live", 5.0f, 1234 });
EXPECT_EQ(live["colorVariation"]["mode"], "live");
EXPECT_EQ(live["colorVariation"]["seed"], 1234u);
// Lines arrive in pieces and several at once
UgcToolboxProtocol::LineReader reader;