feat(ugc): settings for the ray tracer, hidden-face method and denoising

New UGC settings (ugcconfig.ini and the dashboard's settings page):

- ray_backend: builtin (default), embree or hiprt (falls back to embree when
  the build or machine can't); the hidden faces' paths and the occlusion rays
- hsr_method: toolbox (default, LU Toolbox's paths) or fast (the renders from
  around the model), and hsr_fast_resolution (1024) for the fast one
- denoise: off (default) or oidn (icons; off until a build has it)

UgcProcessOptions (dCommon/UgcKeys.h) names the choices for everything that
passes them on ("embree fast oidn", any order, left out: the setting's).
UgcJobs::ApplyOptions puts a choice over the settings and MadeWith says what a
make used after fallbacks; a made model's stats.json records it (rays,
hsrMethod, denoise). UgcServer --make-model and --make-modular take the
choices after the folder and print the CPU time and what made it.

Check: the defaults make the same files as before; the settings page shows the
four settings under UGC; UgcServer --make-model model.lxfml out embree fast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 11:17:02 -05:00
parent 00607a20a6
commit 06be40471b
10 changed files with 206 additions and 8 deletions

View File

@@ -169,6 +169,18 @@ namespace UgcJobs {
return parsed.dump();
}
void ApplyOptions(Settings& settings, const UgcProcessOptions::Choice& choice) {
if (const auto rays = UgcRays::Parse(choice.rays)) settings.hsr.rays = settings.ao.rays = settings.icon.ao.rays = *rays;
if (const auto method = UgcHsr::Parse(choice.hsr)) settings.hsr.method = *method;
if (const auto denoise = UgcRender::ParseDenoise(choice.denoise)) settings.icon.denoise = *denoise;
}
UgcProcessOptions::Choice MadeWith(const Settings& settings) {
const auto denoise = UgcRender::Available(settings.icon.denoise) ? settings.icon.denoise : UgcRender::eDenoise::OFF;
return { std::string(UgcRays::Name(UgcRays::Resolve(settings.hsr.rays))), std::string(UgcHsr::Name(settings.hsr.method)),
std::string(UgcRender::Name(denoise)) };
}
Outcome ProcessModel(const std::string& blob, UgcBricks::BrickLibrary& library, const Settings& settings, uint64_t seed, const UgcIconParams::Values& iconValues) {
Outcome outcome;
const auto started = std::chrono::steady_clock::now();
@@ -355,6 +367,12 @@ namespace UgcJobs {
{ "removeHiddenFaces", settings.hsr.enabled }, { "groundPlane", settings.hsr.groundPlane }, { "hsrSamples", settings.hsr.samples },
{ "hsrBounces", settings.hsr.bounces }, { "hsrSampleSpacing", settings.hsr.spacing }, { "hsrMinPoints", settings.hsr.minPoints },
{ "ao", settings.ao.enabled }, { "aoDistance", settings.ao.distance }, { "aoSamples", settings.ao.samples }, { "aoStrength", settings.ao.strength } };
const auto madeWith = MadeWith(settings);
stats["settings"]["rays"] = madeWith.rays;
stats["settings"]["hsrMethod"] = madeWith.hsr;
stats["settings"]["denoise"] = madeWith.denoise;
if (settings.hsr.method == UgcHsr::eMethod::FAST) stats["settings"]["hsrFastResolution"] = settings.hsr.fastResolution;
outcome.options = UgcProcessOptions::ToString(madeWith);
outcome.stats = stats.dump();
outcome.files["stats.json"] = outcome.stats;
outcome.ok = true;

View File

@@ -12,6 +12,7 @@
#include "UgcBricks.h"
#include "UgcHsr.h"
#include "UgcIconParams.h"
#include "UgcKeys.h"
#include "UgcRender.h"
#include "UgcStorage.h"
@@ -64,6 +65,17 @@ namespace UgcJobs {
uint32_t maxBricks{}; // a model with more fails; 0: no limit
};
/**
* The processing options (UgcProcessOptions) staff picked for one make, over the settings: ray backend (the hidden
* faces' paths and the occlusion rays, the icon's too), hidden-face method and denoising. Choices left empty keep
* the settings'.
*/
void ApplyOptions(Settings& settings, const UgcProcessOptions::Choice& choice);
// What the settings make with, as it is used: the ray backend after its fallback (UgcRays::Resolve), off for a
// denoiser the build doesn't have; every choice filled ("embree toolbox off")
UgcProcessOptions::Choice MadeWith(const Settings& settings);
struct Outcome {
bool ok{};
std::string error; // why it failed
@@ -72,6 +84,7 @@ namespace UgcJobs {
bool aoBaked{};
bool empty{}; // nothing to make (no bricks): not a failure
std::string stats; // stats.json: bricks, triangles before and after per LOD, timings
std::string options; // players' models: what made them (MadeWith, as UgcProcessOptions::ToString)
};
// The client's download of `data` (`name` + ".gz" and ".checksum") added to `files`

View File

@@ -37,7 +37,7 @@ namespace UgcHsr {
float spacing{ 0.1143f }; // hsr_sample_spacing: LDD units between points (a stud is 0.8: 7 points along it)
int minPoints{ 28 }; // hsr_min_points: points on a triangle at least (LU Toolbox bakes 28 texels a triangle)
uint64_t seed{}; // of the paths' random numbers (the same seed gives the same result)
UgcRays::eBackend rays{}; // what traces the paths' rays (ugc_ray_backend)
UgcRays::eBackend rays{}; // what traces the paths' rays (ray_backend)
int fastResolution{ 1024 }; // hsr_fast_resolution: pixels square of each of the fast method's renders
};

View File

@@ -94,6 +94,21 @@ namespace {
}
namespace UgcRender {
std::string_view Name(eDenoise denoise) {
return denoise == eDenoise::OIDN ? "oidn" : "off";
}
std::optional<eDenoise> ParseDenoise(std::string_view name) {
for (const auto denoise : { eDenoise::OFF, eDenoise::OIDN }) {
if (Name(denoise) == name) return denoise;
}
return std::nullopt;
}
bool Available(eDenoise denoise) {
return denoise == eDenoise::OFF;
}
std::vector<glm::vec3> SphereDirections() {
const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
const std::vector<glm::vec3> corners = {

View File

@@ -1,6 +1,8 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string_view>
#include <vector>
#include <glm/glm.hpp>
@@ -21,13 +23,23 @@ namespace UgcRender {
std::vector<uint8_t> rgba;
};
// Whether an icon is denoised (the denoise setting): off, or with Intel Open Image Denoise when the build has it
enum class eDenoise : uint8_t { OFF = 0, OIDN };
// The setting's name (off, oidn)
std::string_view Name(eDenoise denoise);
// A value by its name; nullopt for anything else
std::optional<eDenoise> ParseDenoise(std::string_view name);
// Whether this build can denoise that way (off always)
bool Available(eDenoise denoise);
struct AoOptions {
bool enabled{ true };
float distance{ 5.0f }; // LU Toolbox's AO distance (Bake Lighting, AO Only)
int samples{ 64 }; // rays per vertex (AO Samples)
float strength{ 1.0f }; // 0 leaves the colors, 1 is the full bake
float glowStrength{ 6.0f }; // what glowing colors add to the light (Glow Strength 3 x Glow Multiplier 2)
UgcRays::eBackend rays{}; // what traces the occlusion rays (ugc_ray_backend)
UgcRays::eBackend rays{}; // what traces the occlusion rays (ray_backend)
};
/**
@@ -60,6 +72,7 @@ namespace UgcRender {
AoOptions ao{ false, 5.0f, 32, 1.0f, 0.0f };
float glowEmissive{ 1.0f }; // how far glowing shapes go from lit to their plain color (the glow_emissive setting)
UgcGlitter::Params glitter; // the glitter's flecks (glitter_size, glitter_density), drawn where they are at the start
eDenoise denoise{}; // the finished icon denoised (denoise); off when the build can't
};
// The model drawn from the icon's camera, framed to fit, on a transparent background. `opaqueAo`: the opaque mesh's

View File

@@ -159,6 +159,11 @@ namespace {
settings.hsr.bounces = std::clamp(Setting<int32_t>("hsr_bounces", 8), 0, 64);
settings.hsr.spacing = std::clamp(Setting<float>("hsr_sample_spacing", 0.1143f), 0.01f, 10.0f);
settings.hsr.minPoints = std::clamp(Setting<int32_t>("hsr_min_points", 28), 1, 4096);
settings.hsr.method = UgcHsr::Parse(Game::config->GetValue("hsr_method")).value_or(UgcHsr::eMethod::TOOLBOX);
settings.hsr.fastResolution = std::clamp(Setting<int32_t>("hsr_fast_resolution", 1024), 64, 4096);
// What traces the rays of the hidden faces' paths and of the occlusion (the icon's too)
settings.hsr.rays = UgcRays::Parse(Game::config->GetValue("ray_backend")).value_or(UgcRays::eBackend::BUILTIN);
settings.ao.rays = settings.hsr.rays;
settings.ao.enabled = Setting<int32_t>("bake_ao", 1) != 0;
settings.ao.distance = Setting<float>("ao_distance", 5.0f);
settings.ao.samples = std::clamp(Setting<int32_t>("ao_samples", 64), 1, 1024);
@@ -173,6 +178,8 @@ namespace {
settings.icon.glowEmissive = settings.shaders.glowEmissive;
settings.icon.glitter = settings.shaders.glitterParams;
settings.icon.ao.distance = settings.ao.distance;
settings.icon.ao.rays = settings.hsr.rays;
settings.icon.denoise = UgcRender::ParseDenoise(Game::config->GetValue("denoise")).value_or(UgcRender::eDenoise::OFF);
settings.maxBricks = Setting<uint32_t>("max_model_bricks", 0);
return settings;
}
@@ -552,13 +559,20 @@ namespace {
}
// Command line tools: make one model's or modular build's files into a folder, without a database
int MakeFromCommandLine(const std::string& mode, const std::string& input, const std::filesystem::path& output) {
int MakeFromCommandLine(const std::string& mode, const std::string& input, const std::filesystem::path& output, const std::string& options) {
const auto res = ResPath();
const auto settings = ReadSettings();
auto settings = ReadSettings();
UgcProcessOptions::Choice choice;
if (!UgcProcessOptions::Parse(options, choice)) {
std::cerr << "Unknown processing options \"" << options << "\" (ray backend builtin, embree or hiprt; hidden faces toolbox or fast; denoise off or oidn)\n";
return EXIT_FAILURE;
}
UgcJobs::ApplyOptions(settings, choice);
UgcBricks::BrickLibrary library(res, 0, ClientReader());
if (!library.LoadMaterials()) std::cerr << "Couldn't read Materials.xml from " << (res / "brickdb.zip") << "; bricks will be grey\n";
UgcJobs::Outcome outcome;
const auto start = std::chrono::steady_clock::now();
const double cpuStart = UgcThrottle::ThreadCpuSeconds();
if (mode == "--make-model") {
const auto data = UgcBricks::ReadFile(input);
if (!data) {
@@ -585,7 +599,9 @@ namespace {
for (const auto& [name, data] : outcome.files) {
std::ofstream(output / name, std::ios::binary).write(data.data(), static_cast<std::streamsize>(data.size()));
}
std::cout << "Made " << outcome.files.size() << " files in " << ms << " ms" << (outcome.note.empty() ? "" : ": " + outcome.note) << "\n";
const double cpuMs = (UgcThrottle::ThreadCpuSeconds() - cpuStart) * 1000.0;
std::cout << "Made " << outcome.files.size() << " files in " << ms << " ms (" << cpuMs << " ms CPU)" << (outcome.options.empty() ? "" : " with " + outcome.options) <<
(outcome.note.empty() ? "" : ": " + outcome.note) << "\n";
return EXIT_SUCCESS;
}
}
@@ -602,9 +618,12 @@ int main(int argc, char** argv) {
Game::config = new dConfig("ugcconfig.ini");
// UgcServer --make-model <file.lxfml> <folder> or --make-modular "1:4713+1:4714+1:4715" <folder>
if (argc == 4 && (std::string(argv[1]) == "--make-model" || std::string(argv[1]) == "--make-modular")) {
return MakeFromCommandLine(argv[1], argv[2], argv[3]);
// UgcServer --make-model <file.lxfml> <folder> [options] or --make-modular "1:4713+1:4714+1:4715" <folder> [options];
// options: processing options over the settings (UgcProcessOptions), e.g. embree fast
if (argc >= 4 && (std::string(argv[1]) == "--make-model" || std::string(argv[1]) == "--make-modular")) {
std::string options;
for (int i = 4; i < argc; i++) options += std::string(options.empty() ? "" : " ") + argv[i];
return MakeFromCommandLine(argv[1], argv[2], argv[3], options);
}
// Like the other servers: logs/UgcServer/UgcServer_<start time>.log