diff --git a/dUgcServer/Toolbox/UgcToolbox.cpp b/dUgcServer/Toolbox/UgcToolbox.cpp index 1b0303d6b..39bed0a3b 100644 --- a/dUgcServer/Toolbox/UgcToolbox.cpp +++ b/dUgcServer/Toolbox/UgcToolbox.cpp @@ -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&, const std::function&) { + Result Worker::Make(uint64_t, const std::string&, const std::vector&, const std::function&, 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& lods, const std::function& pace) { + Result Worker::Make(uint64_t id, const std::string& lxfml, const std::vector& lods, const std::function& 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(); diff --git a/dUgcServer/Toolbox/UgcToolbox.h b/dUgcServer/Toolbox/UgcToolbox.h index 6e96aa67c..594f630e8 100644 --- a/dUgcServer/Toolbox/UgcToolbox.h +++ b/dUgcServer/Toolbox/UgcToolbox.h @@ -85,7 +85,7 @@ namespace UgcToolbox { */ using Pause = std::function; Result Make(uint64_t id, const std::string& lxfml, const std::vector& lods, - const std::function& pace = {}); + const std::function& pace = {}, const UgcToolboxProtocol::ColorVariation& variation = {}); // Stops Blender (asks it to quit, then kills it); the next Make starts it again void Stop(); diff --git a/dUgcServer/Toolbox/UgcToolboxJob.cpp b/dUgcServer/Toolbox/UgcToolboxJob.cpp index 36a20dffe..9bb35c6f1 100644 --- a/dUgcServer/Toolbox/UgcToolboxJob.cpp +++ b/dUgcServer/Toolbox/UgcToolboxJob.cpp @@ -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; diff --git a/dUgcServer/Toolbox/UgcToolboxProtocol.cpp b/dUgcServer/Toolbox/UgcToolboxProtocol.cpp index 2ff1b3f17..a778a0120 100644 --- a/dUgcServer/Toolbox/UgcToolboxProtocol.cpp +++ b/dUgcServer/Toolbox/UgcToolboxProtocol.cpp @@ -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& 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& 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) { diff --git a/dUgcServer/Toolbox/UgcToolboxProtocol.h b/dUgcServer/Toolbox/UgcToolboxProtocol.h index fcbfa2137..af3880e25 100644 --- a/dUgcServer/Toolbox/UgcToolboxProtocol.h +++ b/dUgcServer/Toolbox/UgcToolboxProtocol.h @@ -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& 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& 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 diff --git a/dUgcServer/Toolbox/dlu_toolbox_worker.py b/dUgcServer/Toolbox/dlu_toolbox_worker.py index 83e638f8c..0de9021e9 100644 --- a/dUgcServer/Toolbox/dlu_toolbox_worker.py +++ b/dUgcServer/Toolbox/dlu_toolbox_worker.py @@ -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) diff --git a/docs/UgcServer.md b/docs/UgcServer.md index c25a67223..d8c0a265d 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -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. diff --git a/tests/dUgcTests/UgcToolboxTests.cpp b/tests/dUgcTests/UgcToolboxTests.cpp index c97ae4e30..facc681e7 100644 --- a/tests/dUgcTests/UgcToolboxTests.cpp +++ b/tests/dUgcTests/UgcToolboxTests.cpp @@ -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;