diff --git a/dCommon/UgcKeys.h b/dCommon/UgcKeys.h index 989b72f19..7cd7cf55e 100644 --- a/dCommon/UgcKeys.h +++ b/dCommon/UgcKeys.h @@ -74,22 +74,21 @@ namespace UgcDebounce { /** * How a model is made, where there is more than one way to try (docs/UgcServer.md, "Processing options"): what traces - * the rays (ray_backend), how hidden faces are found (hsr_method) and whether icons are denoised - * (denoise). The UGC settings give the defaults; staff may pick others for one make (the dashboard, /reprocessproperty), - * and each made model records what made it. Written as the chosen names in this order, separated by spaces - * ("embree fast oidn"); a choice left out is the setting's. + * the rays (ray_backend) and whether icons are denoised (denoise). The UGC settings give the defaults; staff may pick + * others for one make (the dashboard, /reprocessproperty), and each made model records what made it. Written as the + * chosen names in this order, separated by spaces ("embree oidn"); a choice left out is the setting's. */ namespace UgcProcessOptions { inline constexpr std::string_view RAYS[] = { "builtin", "embree", "hiprt" }; - inline constexpr std::string_view HSR[] = { "toolbox", "fast" }; inline constexpr std::string_view DENOISE[] = { "off", "oidn" }; + // Hidden-face methods earlier versions' options named (stored options still have them): read and ignored + inline constexpr std::string_view RETIRED[] = { "toolbox", "fast" }; struct Choice { std::string rays; // one of RAYS, or empty for the setting - std::string hsr; // one of HSR, or empty std::string denoise; // one of DENOISE, or empty - bool Empty() const { return rays.empty() && hsr.empty() && denoise.empty(); } + bool Empty() const { return rays.empty() && denoise.empty(); } }; template @@ -97,8 +96,8 @@ namespace UgcProcessOptions { return std::find(std::begin(names), std::end(names), word) != std::end(names); } - // The words, in any order ("fast embree"); "default" or "-" leave a choice to the setting. False on an unknown - // word or two words for one choice. + // The words, in any order ("oidn embree"); "default" or "-" leave a choice to the setting, retired words are + // skipped. False on an unknown word or two words for one choice. inline bool Parse(std::string_view text, Choice& choice) { choice = {}; size_t start = 0; @@ -108,8 +107,8 @@ namespace UgcProcessOptions { while (end < text.size() && text[end] != ' ' && text[end] != ',') end++; const auto word = text.substr(start, end - start); start = end; - if (word.empty() || word == "default" || word == "-") continue; - std::string* slot = Contains(RAYS, word) ? &choice.rays : Contains(HSR, word) ? &choice.hsr : Contains(DENOISE, word) ? &choice.denoise : nullptr; + if (word.empty() || word == "default" || word == "-" || Contains(RETIRED, word)) continue; + std::string* slot = Contains(RAYS, word) ? &choice.rays : Contains(DENOISE, word) ? &choice.denoise : nullptr; if (!slot || !slot->empty()) return false; *slot = std::string(word); } @@ -119,7 +118,7 @@ namespace UgcProcessOptions { // The choices made, in order ("embree oidn"); empty when all are the settings' inline std::string ToString(const Choice& choice) { std::string text; - for (const auto* part : { &choice.rays, &choice.hsr, &choice.denoise }) { + for (const auto* part : { &choice.rays, &choice.denoise }) { if (part->empty()) continue; if (!text.empty()) text += ' '; text += *part; diff --git a/dDashboardServer/routes/SettingsCatalog.cpp b/dDashboardServer/routes/SettingsCatalog.cpp index 9a2ae3b0a..9b4c0c3f4 100644 --- a/dDashboardServer/routes/SettingsCatalog.cpp +++ b/dDashboardServer/routes/SettingsCatalog.cpp @@ -495,15 +495,10 @@ namespace { c.Add(Text(UGC, "satin_colors", "Satin colors", "Satin (opal) color ids, comma separated: they stay transparent plastic (the client has no satin shader) but are made milky and less see-through. By default LEGO's satin colors 360,362,363,364,365,366,367,376 (none: off)." + notLive, "360,362,363,364,365,366,367,376")); c.Add(Float(UGC, "satin_opacity", "Satin opacity", "Percent: the opacity of transparent satin bricks, instead of the transparent opacity.", "75", 0, 100)); c.Add(Float(UGC, "satin_whiten", "Satin whitening", "Percent: how far satin colors go towards white.", "20", 0, 100)); - c.Add(Bool(UGC, "remove_hidden_faces", "Remove faces nobody can see", "LU Toolbox's Remove Hidden Faces: paths are traced from points on each opaque triangle, bouncing off the model; a triangle none of whose paths reaches the sky is removed.", true)); - c.Add(Bool(UGC, "hsr_ground_plane", "Nothing seen from below", "Also removes what can only be seen from under the model (a black ground under LDD's floor).", false)); - c.Add(Int(UGC, "hsr_samples", "Paths per point", "Paths traced from each point on a triangle (LU Toolbox's Samples). More finds more faces seen only through small gaps; slower.", "8", 1, 256)); - c.Add(Int(UGC, "hsr_bounces", "Path bounces", "Bounces a path may take off the model before it gives up (Cycles' Max Bounces in LU Toolbox's bake).", "8", 0, 64)); - c.Add(Float(UGC, "hsr_sample_spacing", "Point spacing", "Distance between the points paths start from, in LDD units (a stud is 0.8; 0.1143 puts 7 x 7 points on a stud-sized square). Smaller finds smaller visible parts of big faces; slower.", "0.1143", 0.01, 10)); - c.Add(Int(UGC, "hsr_min_points", "Points per triangle at least", "Small triangles get this many points however close together (LU Toolbox bakes 28 texels for every triangle). Lower is faster but removes more faces that are only just seen.", "28", 1, 4096)); - c.Add(Labels(Choice(UGC, "hsr_method", "How hidden faces are found", "toolbox: LU Toolbox's paths (above). fast: the model rendered from 42 directions around it, removing what shows in none; much faster, but it also removes faces seen only by bounced light (insides seen through openings, recesses). Staff can pick another for one make when making models again.", "toolbox", { "toolbox", "fast" }), { "LU Toolbox (paths)", "Fast (renders from around)" })); - c.Add(Unit(Int(UGC, "hsr_fast_resolution", "Fast method's render size", "The size of each of the fast method's 42 renders; bigger keeps smaller visible faces.", "1024", 64, 4096), "pixels")); - c.Add(Labels(Choice(UGC, "ray_backend", "Ray tracer", "What traces the rays of the hidden faces' paths and of the occlusion: builtin (the UGC server's own), embree (Intel Embree on the CPU) or hiprt (the GPU, when the server was built with it and has one; else embree). They give the same results but for rounding. Staff can pick another for one make when making models again.", "builtin", { "builtin", "embree", "hiprt" }), { "Built in", "Embree (CPU)", "HIPRT (GPU)" })); + c.Add(Bool(UGC, "remove_hidden_faces", "Remove faces nobody can see", "The opaque bricks are rendered from 42 directions around the model; the triangles that show in none are removed. Faces seen only through openings or by bounced light go too.", true)); + c.Add(Bool(UGC, "hsr_ground_plane", "Nothing seen from below", "Also removes what can only be seen from under the model.", false)); + c.Add(Unit(Int(UGC, "hsr_resolution", "Detail of the visibility renders", "The size of each of the 42 renders; bigger keeps smaller visible faces.", "1024", 64, 4096), "pixels")); + c.Add(Labels(Choice(UGC, "ray_backend", "Ray tracer", "What traces the occlusion rays: builtin (the UGC server's own), embree (Intel Embree on the CPU) or hiprt (the GPU, when the server was built with it and has one; else embree). They give the same results but for rounding. Staff can pick another for one make when making models again.", "builtin", { "builtin", "embree", "hiprt" }), { "Built in", "Embree (CPU)", "HIPRT (GPU)" })); c.Add(Int(UGC, "hiprt_device", "GPU for HIPRT", "Which GPU the hiprt ray tracer uses: 0 is the first HIP (AMD) or CUDA (NVIDIA) device.", "0", 0, 16, true)); c.Add(Labels(Choice(UGC, "denoise", "Denoise icons", "oidn (when the server was built with Intel Open Image Denoise; else off): a model's icon is drawn from its colors before the occlusion bake, with its occlusion traced per pixel with a few rays and the noise removed by the denoiser, instead of the baked occlusion. The model itself keeps its baked occlusion (a denoiser only works on images).", "off", { "off", "oidn" }), { "Off", "Open Image Denoise" })); c.Add(Int(UGC, "denoise_samples", "Denoised occlusion rays per pixel", "Occlusion rays traced from each pixel of a denoised icon (before it is scaled down, so 16 times as many per icon pixel); fewer is faster and noisier.", "4", 1, 256)); diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index 408284fc9..faccf0316 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -477,8 +477,8 @@ namespace UgcRoutes { { "triangles", average(summary.triangles) }, { "removed", summary.trianglesBefore > 0 ? 1.0 - static_cast(summary.triangles) / static_cast(summary.trianglesBefore) : 0.0 } }); } - JsonSuccess(reply, { { "choices", { { "rays", names(UgcProcessOptions::RAYS) }, { "hsr", names(UgcProcessOptions::HSR) }, { "denoise", names(UgcProcessOptions::DENOISE) } } }, - { "defaults", { { "rays", UgcSetting("ray_backend").value_or("builtin") }, { "hsr", UgcSetting("hsr_method").value_or("toolbox") }, + JsonSuccess(reply, { { "choices", { { "rays", names(UgcProcessOptions::RAYS) }, { "denoise", names(UgcProcessOptions::DENOISE) } } }, + { "defaults", { { "rays", UgcSetting("ray_backend").value_or("builtin") }, { "denoise", UgcSetting("denoise").value_or("off") } } }, { "combinations", combos }, { "canManage", Can(context, "ugc_manage") } }); }); @@ -772,7 +772,7 @@ namespace UgcRoutes { Route(eHTTPMethod::POST, "/api/ugc/reprocess", Perm("ugc_manage"), "Have the UGC server make items again. Body: {kind: model|modular, id} for one, {kind: model, property} for every model placed on a property, {kind, failedOnly: true} for the failed ones, {kind} for all; " - "models: options (processing options for this make, e.g. \"embree fast oidn\": ray backend, hidden-face method, denoising; left out: the UGC settings')", + "models: options (processing options for this make, e.g. \"embree oidn\": ray backend, denoising; left out: the UGC settings')", [](HTTPReply& reply, const HTTPContext& context) { const auto body = ParseBody(context); if (!body) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid JSON"); diff --git a/dDashboardServer/static/js/ugc.js b/dDashboardServer/static/js/ugc.js index 4ab1828e0..38195b39d 100644 --- a/dDashboardServer/static/js/ugc.js +++ b/dDashboardServer/static/js/ugc.js @@ -130,9 +130,9 @@ if (!i.processedAt) return ''; return fmt.unix(i.processedAt) + (costText(i) ? ' \u00b7 ' + costText(i) : '') + (i.madeOptions ? ' \u00b7 with ' + i.madeOptions : ''); } - // The processing options picked for Make again (ray tracer, hidden faces, denoise), as /api/ugc/reprocess takes them + // The processing options picked for Make again (ray tracer, denoise), as /api/ugc/reprocess takes them function remakeOptions() { - return ['optRays', 'optHsr', 'optDenoise'].map(function (x) { return $(x).value; }).filter(Boolean).join(' '); + return ['optRays', 'optDenoise'].map(function (x) { return $(x).value; }).filter(Boolean).join(' '); } // A model's file as the player named it: its name with the upload's extension (".lxfml"), else the upload's name function fileName(i) { @@ -727,11 +727,11 @@ // ---- processing options: Make again's choices and the comparison of what made the models ---- - var OPTION_LABELS = { builtin: 'Built in', embree: 'Embree (CPU)', hiprt: 'HIPRT (GPU)', toolbox: 'LU Toolbox (paths)', fast: 'Fast (renders)', off: 'Off', oidn: 'Open Image Denoise' }; + var OPTION_LABELS = { builtin: 'Built in', embree: 'Embree (CPU)', hiprt: 'HIPRT (GPU)', off: 'Off', oidn: 'Open Image Denoise' }; function loadOptions() { return api.get('/api/ugc/options').then(function (d) { if (!d.success) throw new Error(d.error || 'Failed'); - [['optRays', 'rays', 'Ray tracer'], ['optHsr', 'hsr', 'Hidden faces'], ['optDenoise', 'denoise', 'Denoise']].forEach(function (x) { + [['optRays', 'rays', 'Ray tracer'], ['optDenoise', 'denoise', 'Denoise']].forEach(function (x) { var select = $(x[0]), chosen = select.value; select.innerHTML = '' + d.choices[x[1]].map(function (name) { return ''; }).join(''); diff --git a/dDashboardServer/templates/ugc.jinja2 b/dDashboardServer/templates/ugc.jinja2 index f5be691cf..af6b2c0fe 100644 --- a/dDashboardServer/templates/ugc.jinja2 +++ b/dDashboardServer/templates/ugc.jinja2 @@ -35,9 +35,9 @@
Processing options compared
-

The ways of making models to compare: the ray tracer (built in, Embree on the CPU, HIPRT on the GPU), how - hidden faces are found (LU Toolbox's paths, or fast renders from around the model) and denoising of icons. The settings pick the - defaults; Make again can use others for a make (the options next to it, or /reprocessproperty embree fast in game). +

The ways of making models to compare: the ray tracer of the occlusion (built in, Embree on the CPU, + HIPRT on the GPU) and denoising of icons. The settings pick the defaults; Make again can use others for a make (the options next + to it, or /reprocessproperty embree oidn in game). Every make is counted under what made it: the averages per make below, each model's in the List view's Options column.

@@ -83,7 +83,6 @@
- diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index 4ee2a9f3d..079d910bb 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -497,7 +497,7 @@ void SlashCommandHandler::Startup() { Command ReprocessPropertyCommand{ .help = "Make this property's models again and reload it", - .info = "The UGC server makes every model placed on the property you are on again, with the current UGC settings or the processing options given (in any order: builtin, embree or hiprt; toolbox or fast; off or oidn). Once they are made, everyone on the property is sent back into it, so their game loads the new meshes", + .info = "The UGC server makes every model placed on the property you are on again, with the current UGC settings or the processing options given (in any order: builtin, embree or hiprt; off or oidn). Once they are made, everyone on the property is sent back into it, so their game loads the new meshes", .aliases = { "reprocessproperty", "reloadpropertymodels" }, .handle = DEVGMCommands::ReprocessProperty, .requiredLevel = eGameMasterLevel::DEVELOPER diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index b29a08e8d..e715e05f2 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -1990,10 +1990,10 @@ namespace DEVGMCommands { ChatPackets::SendSystemMessage(sysAddr, u"You aren't on a property."); return; } - // Optional processing options for this make: ray backend, hidden-face method, denoising, in any order + // Optional processing options for this make: ray backend and denoising, in any order UgcProcessOptions::Choice choice; if (!UgcProcessOptions::Parse(args, choice)) { - ChatPackets::SendSystemMessage(sysAddr, u"Usage: /reprocessproperty [builtin|embree|hiprt] [toolbox|fast] [off|oidn] (left out: the UGC settings')"); + ChatPackets::SendSystemMessage(sysAddr, u"Usage: /reprocessproperty [builtin|embree|hiprt] [off|oidn] (left out: the UGC settings')"); return; } const auto options = UgcProcessOptions::ToString(choice); diff --git a/dGame/dUtilities/UgcManifest.h b/dGame/dUtilities/UgcManifest.h index f8ef58224..154108ee8 100644 --- a/dGame/dUtilities/UgcManifest.h +++ b/dGame/dUtilities/UgcManifest.h @@ -101,7 +101,7 @@ namespace UgcManifest { // /reprocessproperty: every model placed on the property is made again by the UGC server; once none is waiting // (or after 15 minutes), every player in this world is sent the new mesh checksums and transferred back into it, // so their client loads the property again with the new meshes. Returns how many models were queued. - // `options`: the processing options for this make (UgcProcessOptions, e.g. "embree fast"), empty: the UGC settings' + // `options`: the processing options for this make (UgcProcessOptions, e.g. "embree oidn"), empty: the UGC settings' size_t ReprocessProperty(LWOOBJID propertyId, const std::string& options = ""); // A client left (or went back to character select): its waiting requests and pending switches are dropped diff --git a/dUgcServer/Processing/UgcJobs.cpp b/dUgcServer/Processing/UgcJobs.cpp index 6bd23c594..081510921 100644 --- a/dUgcServer/Processing/UgcJobs.cpp +++ b/dUgcServer/Processing/UgcJobs.cpp @@ -54,12 +54,12 @@ namespace UgcJobs { } uint64_t EstimateMemory(size_t parts, const Settings& settings) { - // Measured: the icon's buffers, and per brick its mesh in each LOD (positions, normals, colors, indices, - // the occlusion tree and copies made along the way), about 40 KB at LOD 0, and the hidden faces' ray tree - // (about 60 bytes a triangle, one LOD at a time) + // Measured: the renders' buffers, and per brick its mesh in each LOD (positions, normals, colors, indices, + // the occlusion tree and copies made along the way), about 40 KB at LOD 0 + const uint64_t resolution = static_cast(std::clamp(settings.hsr.resolution, 64, 4096)); const uint64_t icon = static_cast(settings.icon.size) * settings.icon.supersample; - const uint64_t fixed = icon * icon * 20 + 1024 * 1024 * 4 + 16 * 1024 * 1024; - return fixed + static_cast(parts) * (40 * 1024 * (1 + settings.lods.size()) + 16 * 1024); + const uint64_t fixed = resolution * resolution * 8 + icon * icon * 20 + 1024 * 1024 * 4 + 16 * 1024 * 1024; + return fixed + static_cast(parts) * 40 * 1024 * (1 + settings.lods.size()); } namespace { @@ -177,15 +177,13 @@ namespace UgcJobs { } 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 rays = UgcRays::Parse(choice.rays)) settings.ao.rays = settings.icon.ao.rays = *rays; 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)) }; + return { std::string(UgcRays::Name(UgcRays::Resolve(settings.ao.rays))), std::string(UgcRender::Name(denoise)) }; } Outcome ProcessModel(const std::string& blob, UgcBricks::BrickLibrary& library, const Settings& settings, uint64_t seed, const UgcIconParams::Values& iconValues) { @@ -257,9 +255,7 @@ namespace UgcJobs { nlohmann::json entry{ { "lod", lods[i] }, { "near", ranges[i].first }, { "far", ranges[i].second }, { "opaqueBefore", model.opaque.TriangleCount() }, { "transparent", model.transparent.TriangleCount() } }; step = std::chrono::steady_clock::now(); - auto hsr = settings.hsr; - hsr.seed = seed; - const auto optimized = UgcHsr::RemoveHiddenFaces(model, hsr); + const auto optimized = UgcHsr::RemoveHiddenFaces(model, settings.hsr); hsrMs += Since(step); if (i == 0 && optimized.trianglesRemoved > 0) { if (!outcome.note.empty()) outcome.note += "; "; @@ -373,14 +369,11 @@ namespace UgcJobs { { "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 }, - { "removeHiddenFaces", settings.hsr.enabled }, { "groundPlane", settings.hsr.groundPlane }, { "hsrSamples", settings.hsr.samples }, - { "hsrBounces", settings.hsr.bounces }, { "hsrSampleSpacing", settings.hsr.spacing }, { "hsrMinPoints", settings.hsr.minPoints }, + { "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); 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; diff --git a/dUgcServer/Processing/UgcJobs.h b/dUgcServer/Processing/UgcJobs.h index 116c0dc0e..c564aeb60 100644 --- a/dUgcServer/Processing/UgcJobs.h +++ b/dUgcServer/Processing/UgcJobs.h @@ -59,21 +59,20 @@ namespace UgcJobs { std::string shaderOpaque{ "01" }; // S_Opaque_...; transparent shapes are always S01 bool combineTransparent{ false }; // one shape for all transparent bricks, else one per brick (Combine Transparent) Shaders shaders; // metal and glow groups (all off by default) - UgcHsr::Options hsr; // hidden surface removal (Remove Hidden Faces) + UgcHsr::Options hsr; // hidden surface removal UgcRender::AoOptions ao; // Bake Lighting (AO Only) UgcRender::IconOptions icon; // from the icon_* settings (UgcIconParams); presets and overrides go over it 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'. + * The processing options (UgcProcessOptions) staff picked for one make, over the settings: ray backend (the + * occlusion rays, the icon's too) 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") + // denoiser the build doesn't have; every choice filled ("embree off") UgcProcessOptions::Choice MadeWith(const Settings& settings); struct Outcome { diff --git a/dUgcServer/Render/UgcHsr.cpp b/dUgcServer/Render/UgcHsr.cpp index e41d77290..4e080ac05 100644 --- a/dUgcServer/Render/UgcHsr.cpp +++ b/dUgcServer/Render/UgcHsr.cpp @@ -1,626 +1,14 @@ #include "UgcHsr.h" -#include -#include -#include -#include -#include "UgcRays.h" #include "UgcRender.h" -#include "UgcThrottle.h" - -namespace { - constexpr float INF = std::numeric_limits::infinity(); - constexpr float PI = 3.14159265358979f; - // Blender's default Glossy bounces (LU Toolbox leaves it) - constexpr int GLOSSY_BOUNCES = 4; - // Points on one triangle at most (very big triangles get their points further apart) - constexpr size_t MAX_POINTS = 4096; - - // PCG32 (O'Neill), seeded per path so every path is the same whatever order they're traced in - class Random { - public: - explicit Random(uint64_t seed) { - m_State = 0; - Next(); - m_State += seed; - Next(); - } - - uint32_t Next() { - const uint64_t old = m_State; - m_State = old * 6364136223846793005ull + 1442695040888963407ull; - const auto xorshifted = static_cast(((old >> 18u) ^ old) >> 27u); - const auto rot = static_cast(old >> 59u); - return (xorshifted >> rot) | (xorshifted << ((32u - rot) & 31u)); - } - - // In [0, 1) - float Float() { return static_cast(Next() >> 8) * (1.0f / 16777216.0f); } - - private: - uint64_t m_State; - }; - - uint64_t Mix(uint64_t h) { - h ^= h >> 33; - h *= 0xff51afd7ed558ccdull; - h ^= h >> 33; - h *= 0xc4ceb9fe1a85ec53ull; - h ^= h >> 33; - return h; - } - - uint64_t PathSeed(uint64_t seed, uint64_t triangle, uint64_t point, uint64_t sample) { - return Mix(Mix(Mix(seed ^ 0x9E3779B97F4A7C15ull) ^ triangle) ^ (point << 16 | sample)); - } - - // Directions around a unit normal (Duff et al., "Building an Orthonormal Basis, Revisited") - void Basis(const glm::vec3& n, glm::vec3& t, glm::vec3& b) { - const float sign = std::copysign(1.0f, n.z); - const float a = -1.0f / (sign + n.z); - const float c = n.x * n.y * a; - t = glm::vec3(1.0f + sign * n.x * n.x * a, sign * c, -sign * n.x); - b = glm::vec3(c, sign + n.y * n.y * a, -n.y); - } - - glm::vec3 CosineHemisphere(const glm::vec3& n, float u, float v) { - glm::vec3 t, b; - Basis(n, t, b); - const float r = std::sqrt(u), phi = 2.0f * PI * v; - return t * (r * std::cos(phi)) + b * (r * std::sin(phi)) + n * std::sqrt(std::max(0.0f, 1.0f - u)); - } - - /** - * Cycles' ensure_valid_reflection, which the Principled BSDF applies to its normal: N turned towards Ng just - * enough that the mirror reflection of I (towards where the light goes) stays above the surface. - */ - glm::vec3 EnsureValidReflection(const glm::vec3& ng, const glm::vec3& i, const glm::vec3& n) { - const auto r = 2.0f * glm::dot(n, i) * n - i; - const float threshold = std::min(0.9f * glm::dot(ng, i), 0.01f); - if (glm::dot(ng, r) >= threshold) return n; - const float ndotng = glm::dot(n, ng); - const auto x = glm::normalize(n - ndotng * ng); - const float ix = glm::dot(i, x), iz = glm::dot(i, ng); - const float ix2 = ix * ix, iz2 = iz * iz; - const float a = ix2 + iz2; - const float b = std::sqrt(std::max(ix2 * (a - threshold * threshold), 0.0f)); - const float c = iz * threshold + a; - const float fac = 0.5f / a; - const float n1z2 = fac * (b + c), n2z2 = fac * (-b + c); - bool valid1 = n1z2 > 1e-5f && n1z2 <= 1.0f + 1e-5f; - bool valid2 = n2z2 > 1e-5f && n2z2 <= 1.0f + 1e-5f; - const auto root = [](float v) { return std::sqrt(std::max(v, 0.0f)); }; - glm::vec2 chosen; - if (valid1 && valid2) { - const glm::vec2 n1(root(1.0f - n1z2), root(n1z2)), n2(root(1.0f - n2z2), root(n2z2)); - const float r1 = 2.0f * (n1.x * ix + n1.y * iz) * n1.y - iz; - const float r2 = 2.0f * (n2.x * ix + n2.y * iz) * n2.y - iz; - valid1 = r1 >= 1e-5f; - valid2 = r2 >= 1e-5f; - if (valid1 && valid2) chosen = r1 < r2 ? n1 : n2; - else chosen = r1 > r2 ? n1 : n2; - } else if (valid1 || valid2) { - const float z2 = valid1 ? n1z2 : n2z2; - chosen = glm::vec2(root(1.0f - z2), root(z2)); - } else { - return ng; - } - return chosen.x * x + chosen.y * ng; - } - - float SchlickFresnel(float u) { - const float m = std::clamp(1.0f - u, 0.0f, 1.0f); - const float m2 = m * m; - return m2 * m2 * m; - } - - float FresnelDielectricCos(float cosi, float eta) { - const float c = std::abs(cosi); - float g = eta * eta - 1.0f + c * c; - if (!(g > 0.0f)) return 1.0f; - g = std::sqrt(g); - const float a = (g - c) / (g + c); - const float b = (c * (g + c) - 1.0f) / (c * (g - c) + 1.0f); - return 0.5f * a * a * (1.0f + b * b); - } - - /** - * The bake material: a new material's Principled BSDF (Blender 3.1: base color 0.8, specular 0.5, roughness 0.5, - * GGX) as Cycles 3.1 samples it, a diffuse closure (Burley, with retro-reflection) and a specular one (GGX with a - * dielectric Fresnel, IOR 1.5) picked by their sample weights. Only the directions a path takes and its throughput - * (for the Russian roulette) matter here. - */ - class Principled { - public: - static constexpr float BASE = 0.8f; // Base Color - static constexpr float ROUGHNESS = 0.5f; // Roughness - static constexpr float IOR = 1.5f; // (2 / (1 - sqrt(0.08 * Specular 0.5))) - 1 - static constexpr float CSPEC0 = 0.04f; // Specular 0.5 * 0.08 - - // n: the material's normal (after EnsureValidReflection), i: towards where the path came from, ng: the - // geometric normal on the same side, sn: the shading normal. `first`: the bake point (Cycles' defensive - // sampling there); minRayPdf: the smallest bounce pdf so far (Cycles' Filter Glossy 1 blurs the specular) - Principled(const glm::vec3& n, const glm::vec3& i, const glm::vec3& ng, const glm::vec3& sn, bool first, float minRayPdf) - : m_N(n), m_I(i), m_Ng(ng), m_Sn(sn) { - m_F0 = FresnelDielectricCos(1.0f, IOR); - m_WeightDiffuse = BASE; - m_WeightSpecular = Fresnel(m_I, m_N); // the closure's weight 1 times its Fresnel color's average - if (first) { - const float sum = m_WeightDiffuse + m_WeightSpecular; - m_WeightDiffuse = std::max(m_WeightDiffuse, 0.125f * sum); - m_WeightSpecular = std::max(m_WeightSpecular, 0.125f * sum); - } - m_Alpha = std::clamp(ROUGHNESS * ROUGHNESS, 0.0f, 1.0f); - if (minRayPdf < 1.0f) m_Alpha = std::max(std::sqrt(1.0f - minRayPdf) * 0.5f, m_Alpha); - } - - struct Sample { - glm::vec3 direction{}; - float pdf{}; // of the mixture; 0: nothing sampled, the path ends - float throughput{}; // eval / pdf - bool glossy{}; - }; - - Sample Draw(Random& random) const { - Sample sample; - const float pick = random.Float() * (m_WeightDiffuse + m_WeightSpecular); - const float u = random.Float(), v = random.Float(); - float pdfDiffuse = 0.0f, pdfSpecular = 0.0f; - float eval = 0.0f; - if (pick < m_WeightDiffuse) { - sample.direction = CosineHemisphere(m_N, u, v); - if (!(glm::dot(m_Ng, sample.direction) > 0.0f)) return sample; - eval = EvalDiffuse(sample.direction, pdfDiffuse); - if (!(pdfDiffuse > 0.0f) || !(eval > 0.0f)) return sample; - if (!Transmission(sample.direction)) eval += EvalSpecular(sample.direction, pdfSpecular); - } else { - sample.glossy = true; - if (!SampleSpecular(u, v, sample.direction, eval, pdfSpecular)) return sample; - if (!Transmission(sample.direction)) eval += EvalDiffuse(sample.direction, pdfDiffuse); - } - sample.pdf = (pdfDiffuse * m_WeightDiffuse + pdfSpecular * m_WeightSpecular) / (m_WeightDiffuse + m_WeightSpecular); - sample.throughput = sample.pdf > 0.0f ? eval / sample.pdf : 0.0f; - return sample; - } - - private: - // Cycles decides reflection or transmission by the shading normal; both closures only reflect - bool Transmission(const glm::vec3& l) const { return glm::dot(m_Sn, l) < 0.0f; } - - float Fresnel(const glm::vec3& l, const glm::vec3& h) const { - const float fh = (FresnelDielectricCos(glm::dot(l, h), IOR) - m_F0) / (1.0f - m_F0); - return CSPEC0 * (1.0f - fh) + fh; - } - - // bsdf_principled_diffuse_eval_reflect times the closure weight - float EvalDiffuse(const glm::vec3& l, float& pdf) const { - const float nl = glm::dot(m_N, l); - if (!(nl > 0.0f)) { - pdf = 0.0f; - return 0.0f; - } - pdf = nl / PI; - const float nv = glm::dot(m_N, m_I); - const float fv = SchlickFresnel(nv), fl = SchlickFresnel(nl); - float f = (1.0f - 0.5f * fv) * (1.0f - 0.5f * fl); - const float rr = ROUGHNESS * (glm::dot(l, m_I) + 1.0f); - f += rr * (fl + fv + fl * fv * (rr - 1.0f)); - return BASE * nl / PI * f; - } - - // bsdf_microfacet_ggx_eval_reflect (isotropic, Fresnel) times the closure weight 1 - float EvalSpecular(const glm::vec3& l, float& pdf) const { - pdf = 0.0f; - const float cosNO = glm::dot(m_N, m_I), cosNI = glm::dot(m_N, l); - if (!(cosNI > 0.0f && cosNO > 0.0f) || m_Alpha * m_Alpha <= 1e-7f) return 0.0f; - const auto m = glm::normalize(l + m_I); - const float alpha2 = m_Alpha * m_Alpha; - const float cosThetaM = glm::dot(m_N, m); - const float cosThetaM2 = cosThetaM * cosThetaM; - const float tanThetaM2 = (1.0f - cosThetaM2) / cosThetaM2; - const float d = alpha2 / (PI * cosThetaM2 * cosThetaM2 * (alpha2 + tanThetaM2) * (alpha2 + tanThetaM2)); - const float g1o = 2.0f / (1.0f + std::sqrt(std::max(0.0f, 1.0f + alpha2 * (1.0f - cosNO * cosNO) / (cosNO * cosNO)))); - const float g1i = 2.0f / (1.0f + std::sqrt(std::max(0.0f, 1.0f + alpha2 * (1.0f - cosNI * cosNI) / (cosNI * cosNI)))); - const float common = d * 0.25f / cosNO; - pdf = g1o * common; - return Fresnel(l, m) * g1o * g1i * common; - } - - // bsdf_microfacet_ggx_sample (isotropic): a visible normal (Heitz and d'Eon), the view reflected in it - bool SampleSpecular(float randu, float randv, glm::vec3& l, float& eval, float& pdf) const { - const float cosNO = glm::dot(m_N, m_I); - if (!(cosNO > 0.0f)) return false; - glm::vec3 x, y; - Basis(m_N, x, y); - // Stretch the view, sample the slopes, rotate and unstretch - auto local = glm::normalize(glm::vec3(m_Alpha * glm::dot(x, m_I), m_Alpha * glm::dot(y, m_I), cosNO)); - float costheta = 1.0f, sintheta = 0.0f, cosphi = 1.0f, sinphi = 0.0f; - if (local.z < 0.99999f) { - costheta = local.z; - sintheta = std::sqrt(std::max(0.0f, 1.0f - costheta * costheta)); - cosphi = local.x / sintheta; - sinphi = local.y / sintheta; - } - float slopeX, slopeY, g1o; - if (costheta >= 0.99999f) { - const float r = std::sqrt(randu / (1.0f - randu)); - const float phi = 2.0f * PI * randv; - slopeX = r * std::cos(phi); - slopeY = r * std::sin(phi); - g1o = 1.0f; - } else { - const float tanThetaI = sintheta / costheta; - const float g1Inv = 0.5f * (1.0f + std::sqrt(std::max(0.0f, 1.0f + tanThetaI * tanThetaI))); - g1o = 1.0f / g1Inv; - const float a = 2.0f * randu * g1Inv - 1.0f; - const float aa = a * a; - const float tmp = 1.0f / (aa - 1.0f); - const float b = tanThetaI, bb = b * b; - const float dd = std::sqrt(std::max(0.0f, bb * (tmp * tmp) - (aa - bb) * tmp)); - const float x1 = b * tmp - dd, x2 = b * tmp + dd; - slopeX = (a < 0.0f || x2 * tanThetaI > 1.0f) ? x1 : x2; - float s; - if (randv > 0.5f) { - s = 1.0f; - randv = 2.0f * (randv - 0.5f); - } else { - s = -1.0f; - randv = 2.0f * (0.5f - randv); - } - const float z = (randv * (randv * (randv * 0.27385f - 0.73369f) + 0.46341f)) / (randv * (randv * (randv * 0.093073f + 0.309420f) - 1.0f) + 0.597999f); - slopeY = s * z * std::sqrt(std::max(0.0f, 1.0f + slopeX * slopeX)); - } - const float rotated = cosphi * slopeX - sinphi * slopeY; - slopeY = sinphi * slopeX + cosphi * slopeY; - slopeX = rotated * m_Alpha; - slopeY *= m_Alpha; - const auto localM = glm::normalize(glm::vec3(-slopeX, -slopeY, 1.0f)); - const auto m = x * localM.x + y * localM.y + m_N * localM.z; - const float cosMO = glm::dot(m, m_I); - if (!(cosMO > 0.0f)) return false; - l = 2.0f * cosMO * m - m_I; - if (!(glm::dot(m_Ng, l) > 0.0f)) return false; - const float alpha2 = m_Alpha * m_Alpha; - const float cosThetaM2 = localM.z * localM.z; - const float tanThetaM2 = 1.0f / cosThetaM2 - 1.0f; - const float d = alpha2 / (PI * cosThetaM2 * cosThetaM2 * (alpha2 + tanThetaM2) * (alpha2 + tanThetaM2)); - const float cosNI = glm::dot(m_N, l); - const float g1i = 2.0f / (1.0f + std::sqrt(std::max(0.0f, 1.0f + alpha2 * (1.0f - cosNI * cosNI) / (cosNI * cosNI)))); - const float common = g1o * d * 0.25f / cosNO; - pdf = common; - eval = g1i * common * Fresnel(l, m); - return pdf > 0.0f && eval > 0.0f; - } - - glm::vec3 m_N, m_I, m_Ng, m_Sn; - float m_F0{}; - float m_WeightDiffuse{}, m_WeightSpecular{}; - float m_Alpha{}; - }; - - // LU Toolbox's ground plane: a box 1000 x 1000 x 100 whose top is at LDD y 0, black (a path hitting it ends) - float GroundHit(const glm::vec3& origin, const glm::vec3& direction, float maxT) { - static const glm::vec3 MIN(-500.0f, -100.0f, -500.0f), MAX(500.0f, 0.0f, 500.0f); - float enter = 0.0f, exit = maxT; - for (int axis = 0; axis < 3; axis++) { - if (std::abs(direction[axis]) < 1e-20f) { - if (origin[axis] < MIN[axis] || origin[axis] > MAX[axis]) return INF; - continue; - } - const float inv = 1.0f / direction[axis]; - float t0 = (MIN[axis] - origin[axis]) * inv, t1 = (MAX[axis] - origin[axis]) * inv; - if (t0 > t1) std::swap(t0, t1); - enter = std::max(enter, t0); - exit = std::min(exit, t1); - if (enter > exit) return INF; - } - return enter; - } - - class Tracer { - public: - Tracer(const UgcModel::Mesh& mesh, const UgcHsr::Options& options) : m_Mesh(mesh), m_Rays(UgcRays::Make(options.rays, mesh)), m_Options(options) { - m_Smooth = mesh.normals.size() == mesh.positions.size(); - } - - // The triangle's normal from its winding (unit; zero when it has no area) - glm::vec3 FaceNormal(uint32_t t) const { - const auto& a = m_Mesh.positions[m_Mesh.indices[t * 3]]; - const auto n = glm::cross(m_Mesh.positions[m_Mesh.indices[t * 3 + 1]] - a, m_Mesh.positions[m_Mesh.indices[t * 3 + 2]] - a); - const float length = glm::length(n); - return length > 0.0f ? n / length : glm::vec3(0.0f); - } - - // The vertex normals at barycentric w (Cycles' smooth normal: the face's own when they sum to nothing) - glm::vec3 ShadingNormal(uint32_t t, const glm::vec3& w, const glm::vec3& faceNormal) const { - if (!m_Smooth) return faceNormal; - const auto n = m_Mesh.normals[m_Mesh.indices[t * 3]] * w.x + m_Mesh.normals[m_Mesh.indices[t * 3 + 1]] * w.y + m_Mesh.normals[m_Mesh.indices[t * 3 + 2]] * w.z; - const float length = glm::length(n); - return length > 0.0f ? n / length : faceNormal; - } - - glm::vec3 Position(uint32_t t, const glm::vec3& w) const { - return m_Mesh.positions[m_Mesh.indices[t * 3]] * w.x + m_Mesh.positions[m_Mesh.indices[t * 3 + 1]] * w.y + m_Mesh.positions[m_Mesh.indices[t * 3 + 2]] * w.z; - } - - // A path between its rays - struct Path { - glm::vec3 ng{}, n{}, incoming{}, p{}; - glm::vec3 direction{}; // of the ray it waits for - uint32_t self{}; // the triangle it is on (the ray never hits it) - float throughput{ 1.0f }; - float minRayPdf{ INF }; - int glossy{}; - int bounce{}; - bool sampleGlossy{}; - Random random; - }; - - enum class eStep : uint8_t { RAY, ESCAPED, ENDED }; - - // A path from point w (barycentric) of triangle t, its random numbers from `seed` - Path Start(uint32_t t, const glm::vec3& w, uint64_t seed) const { - Path path{ .random = Random(seed) }; - path.ng = FaceNormal(t); - // The bake looks at the point along its smooth normal (the ray comes from there): that side is lit, and - // when the winding faces the other way Cycles treats it as the back of the face (both normals turned) - path.n = ShadingNormal(t, w, path.ng); - path.incoming = path.n; - if (glm::dot(path.ng, path.incoming) < 0.0f) { - path.ng = -path.ng; - path.n = -path.n; - } - path.p = Position(t, w); - path.self = t; - return path; - } - - // The direction the path goes on in: RAY (from path.p along path.direction), or ENDED - eStep Scatter(Path& path) const { - // The material's normal: the Principled BSDF keeps its mirror reflection above the surface - const Principled material(EnsureValidReflection(path.ng, path.incoming, path.n), path.incoming, path.ng, path.n, path.bounce == 0, path.minRayPdf); - const auto sample = material.Draw(path.random); - // Nothing sampled (below the surface): the path ends - if (!(sample.pdf > 0.0f) || !(sample.throughput > 0.0f)) return eStep::ENDED; - path.direction = sample.direction; - path.throughput *= sample.throughput; - path.minRayPdf = std::min(path.minRayPdf, sample.pdf); - path.sampleGlossy = sample.glossy; - return eStep::RAY; - } - - // What the ray hit: ESCAPED (nothing: the sky), ENDED, or RAY (it bounced: Scatter again) - eStep Bounce(Path& path, const UgcRays::Hit& hit) const { - const auto& direction = path.direction; - if (m_Options.groundPlane && GroundHit(path.p, direction, hit.t) < hit.t) return eStep::ENDED; - if (hit.triangle == UgcRays::NONE) return eStep::ESCAPED; - // Past the bounce limits the next surface doesn't scatter (Cycles: Max Bounces, Glossy 4) - if (path.bounce + 1 > m_Options.bounces) return eStep::ENDED; - if (path.sampleGlossy && ++path.glossy > GLOSSY_BOUNCES) return eStep::ENDED; - if (path.bounce + 1 >= 2) { - const float probability = std::min(std::sqrt(path.throughput), 1.0f); - if (path.random.Float() >= probability) return eStep::ENDED; - path.throughput /= probability; - } - path.self = hit.triangle; - path.ng = FaceNormal(path.self); - path.n = ShadingNormal(path.self, glm::vec3(1.0f - hit.u - hit.v, hit.u, hit.v), path.ng); - path.incoming = -direction; - // Hit from behind: the back is lit (both normals turned towards the ray) - if (glm::dot(path.ng, path.incoming) < 0.0f) { - path.ng = -path.ng; - path.n = -path.n; - } - path.p = path.p + direction * hit.t; - path.bounce++; - return eStep::RAY; - } - - /** - * One path from point w (barycentric) of triangle t, as a Cycles diffuse bake traces it: whether it reaches the - * sky. The path bounces off the model in the directions the bake material draws until a bounce ray hits - * nothing, which is the sky. The sky isn't sampled directly: Cycles doesn't sample a world of one flat color as - * a light, so only rays that happen to leave count. Up to `bounces` bounces (4 of them glossy), and from the - * second bounce on the path may end early (Cycles' Russian roulette: it goes on with probability - * sqrt(throughput)). - */ - bool Escapes(uint32_t t, const glm::vec3& w, uint64_t seed) const { - auto path = Start(t, w, seed); - while (Scatter(path) == eStep::RAY) { - const auto step = Bounce(path, m_Rays->Closest(path.p, path.direction, path.self)); - if (step != eStep::RAY) return step == eStep::ESCAPED; - } - return false; - } - - const UgcRays::Scene& Rays() const { return *m_Rays; } - - private: - const UgcModel::Mesh& m_Mesh; - std::unique_ptr m_Rays; // the nearest hit (never the triangle a ray leaves, as in Cycles) - const UgcHsr::Options& m_Options; - bool m_Smooth{}; - }; -} namespace UgcHsr { - std::string_view Name(eMethod method) { - return method == eMethod::FAST ? "fast" : "toolbox"; - } - - std::optional Parse(std::string_view name) { - for (const auto method : { eMethod::TOOLBOX, eMethod::FAST }) { - if (Name(method) == name) return method; - } - return std::nullopt; - } - - std::vector SamplePoints(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c, float spacing, size_t minimum) { - // Too small to lay out: the centre and one point towards each corner (the centres of its four halved-side - // triangles) - const std::vector fallback{ { 1.0f / 3, 1.0f / 3, 1.0f / 3 }, { 2.0f / 3, 1.0f / 6, 1.0f / 6 }, { 1.0f / 6, 2.0f / 3, 1.0f / 6 }, - { 1.0f / 6, 1.0f / 6, 2.0f / 3 } }; - const std::array corners{ a, b, c }; - // The longest side from corner `first` to the next; the third is the apex - int first = 0; - float longest = -1.0f; - for (int i = 0; i < 3; i++) { - const float length = glm::length(corners[(i + 1) % 3] - corners[i]); - if (length > longest) { - longest = length; - first = i; - } - } - const float area = 0.5f * glm::length(glm::cross(b - a, c - a)); - if (!(longest > 0.0f) || !(area > 0.0f) || !(spacing > 0.0f) || !std::isfinite(area)) return fallback; - minimum = std::min(minimum, MAX_POINTS); - // Very big triangles: points further apart, at most MAX_POINTS - spacing = std::max(spacing, std::sqrt(area / static_cast(MAX_POINTS))); - const float height = 2.0f * area / longest; - std::vector points; - for (int attempt = 0; attempt < 16; attempt++) { - points.clear(); - const int along = std::max(1, static_cast(std::ceil(longest / spacing))); - const int rows = std::max(1, static_cast(std::ceil(height / spacing))); - for (int row = 0; row < rows; row++) { - // v: from the longest side (0) to the apex (1); the row is (1 - v) of the side's length - const float v = (row + 0.5f) / rows; - const int count = std::max(1, static_cast(std::lround(along * (1.0f - v)))); - for (int j = 0; j < count; j++) { - const float u = (j + 0.5f) / count; - glm::vec3 w{}; - w[first] = (1.0f - u) * (1.0f - v); - w[(first + 1) % 3] = u * (1.0f - v); - w[(first + 2) % 3] = v; - points.push_back(w); - } - } - if (points.size() >= minimum) break; - // Fewer than the minimum: closer together - spacing *= 0.95f * std::sqrt(static_cast(points.size()) / static_cast(minimum)); - } - return points.size() < fallback.size() ? fallback : points; - } - - std::vector Visible(const UgcModel::Mesh& mesh, const Options& options, uint64_t* pointCount, uint64_t* pathCount) { - const size_t triangles = mesh.TriangleCount(); - std::vector visible(triangles, true); - if (triangles == 0) return visible; - const Tracer tracer(mesh, options); - const int samples = std::max(options.samples, 1); - uint64_t points = 0, paths = 0, sinceCheckpoint = 0; - if (tracer.Rays().PrefersBatches() || options.sideBySide) { - // Side by side (a GPU): the triangles in groups of about GROUP_PATHS paths; in each round every point of every - // triangle of the group not seen yet starts a path, and the round's paths are traced together a bounce at a - // time. Each path is the one traced one by one below (the same random numbers), and a triangle is kept when - // any of its paths escapes, so the triangles decided are the same; only paths the one by one tracing would - // have skipped after one escaped are traced too. - constexpr size_t GROUP_PATHS = 1u << 18; - uint32_t next = 0; - while (next < triangles) { - std::vector group; - std::vector> groupWeights; - size_t groupPaths = 0; - for (; next < triangles && groupPaths < GROUP_PATHS; next++) { - // A triangle without area draws nothing (LU Toolbox's bake leaves it dark too): removed - if (tracer.FaceNormal(next) == glm::vec3(0.0f)) { - visible[next] = false; - continue; - } - const auto& a = mesh.positions[mesh.indices[next * 3]]; - const auto& b = mesh.positions[mesh.indices[next * 3 + 1]]; - const auto& c = mesh.positions[mesh.indices[next * 3 + 2]]; - groupWeights.push_back(SamplePoints(a, b, c, options.spacing, static_cast(std::max(options.minPoints, 1)))); - group.push_back(next); - groupPaths += groupWeights.back().size(); - points += groupWeights.back().size(); - } - std::vector escaped(group.size(), 0); - for (int sample = 0; sample < samples; sample++) { - std::vector live; - std::vector owner; // the path's triangle in the group - for (uint32_t g = 0; g < group.size(); g++) { - if (escaped[g]) continue; - for (size_t i = 0; i < groupWeights[g].size(); i++) { - auto path = tracer.Start(group[g], groupWeights[g][i], PathSeed(options.seed, group[g], i, static_cast(sample))); - paths++; - if (tracer.Scatter(path) != Tracer::eStep::RAY) continue; - live.push_back(std::move(path)); - owner.push_back(g); - } - } - if (live.empty()) break; - std::vector rays; - std::vector hits; - while (!live.empty()) { - UgcThrottle::Checkpoint(); - rays.resize(live.size()); - hits.resize(live.size()); - for (size_t k = 0; k < live.size(); k++) rays[k] = UgcRays::Ray{ live[k].p, 0.0f, live[k].direction, INF, live[k].self }; - tracer.Rays().Closest(rays.data(), hits.data(), live.size()); - size_t kept = 0; - for (size_t k = 0; k < live.size(); k++) { - if (escaped[owner[k]]) continue; - const auto step = tracer.Bounce(live[k], hits[k]); - if (step == Tracer::eStep::ESCAPED) escaped[owner[k]] = 1; - if (step != Tracer::eStep::RAY || tracer.Scatter(live[k]) != Tracer::eStep::RAY) continue; - if (kept != k) { - live[kept] = std::move(live[k]); - owner[kept] = owner[k]; - } - kept++; - } - live.erase(live.begin() + static_cast(kept), live.end()); - owner.resize(kept); - } - } - for (size_t g = 0; g < group.size(); g++) visible[group[g]] = escaped[g] != 0; - } - if (pointCount) *pointCount = points; - if (pathCount) *pathCount = paths; - return visible; - } - for (uint32_t t = 0; t < triangles; t++) { - const auto& a = mesh.positions[mesh.indices[t * 3]]; - const auto& b = mesh.positions[mesh.indices[t * 3 + 1]]; - const auto& c = mesh.positions[mesh.indices[t * 3 + 2]]; - // A triangle without area draws nothing (LU Toolbox's bake leaves it dark too): removed - if (tracer.FaceNormal(t) == glm::vec3(0.0f)) { - visible[t] = false; - continue; - } - const auto weights = SamplePoints(a, b, c, options.spacing, static_cast(std::max(options.minPoints, 1))); - points += weights.size(); - bool escaped = false; - // A path from each point, then another from each, ...: a triangle that's seen is usually known at once - for (int sample = 0; sample < samples && !escaped; sample++) { - for (size_t i = 0; i < weights.size(); i++) { - paths++; - if (tracer.Escapes(t, weights[i], PathSeed(options.seed, t, i, static_cast(sample)))) { - escaped = true; - break; - } - } - sinceCheckpoint += weights.size(); - if (sinceCheckpoint >= 256) { - UgcThrottle::Checkpoint(); - sinceCheckpoint = 0; - } - } - visible[t] = escaped; - } - if (pointCount) *pointCount = points; - if (pathCount) *pathCount = paths; - return visible; - } - Result RemoveHiddenFaces(UgcModel::Model& model, const Options& options) { Result result; auto& opaque = model.opaque; result.trianglesBefore = opaque.TriangleCount() + model.transparent.TriangleCount(); if (opaque.Empty() || !options.enabled) return result; - result.kept = options.method == eMethod::FAST ? UgcRender::VisibleFromAround(model, options.fastResolution, options.groundPlane) : - Visible(opaque, options, &result.points, &result.paths); + result.kept = UgcRender::VisibleFromAround(model, options.resolution, options.groundPlane); for (const bool kept : result.kept) result.trianglesRemoved += kept ? 0 : 1; UgcModel::KeepTriangles(opaque, result.kept); return result; diff --git a/dUgcServer/Render/UgcHsr.h b/dUgcServer/Render/UgcHsr.h index db6fc3121..84201b9e9 100644 --- a/dUgcServer/Render/UgcHsr.h +++ b/dUgcServer/Render/UgcHsr.h @@ -1,70 +1,29 @@ #pragma once #include -#include -#include -#include #include -#include - #include "UgcModel.h" -#include "UgcRays.h" /** - * Hidden surface removal as LU Toolbox's Remove Hidden Faces decides it, without its texture: paths are traced from - * points on each opaque triangle under a sky of overwhelming brightness, bouncing off the model; a triangle none of - * whose paths reaches the sky is removed. So faces seen only through openings, or lit only by light bounced in - * (interiors, recesses), stay; faces sealed inside go. docs/UgcServer.md ("Hidden faces") has the details. - * The fast method (hsr_method=fast) is the test the UGC server used before: renders from 42 directions around the - * model (UgcRender::VisibleFromAround), which removes faces seen only by bounced light too. + * Hidden surface removal: the opaque mesh rendered from 42 directions around the model (UgcRender::VisibleFromAround), + * and the triangles that show in none of them removed. Transparent bricks hide nothing and aren't touched, as in LU + * Toolbox. Faces seen only by bounced light (insides seen through openings, recesses) don't show in the renders and + * are removed too. docs/UgcServer.md ("Hidden faces") has the details. */ namespace UgcHsr { - // How hidden faces are found (hsr_method): LU Toolbox's paths, or the renders from around the model - enum class eMethod : uint8_t { TOOLBOX = 0, FAST }; - - // The setting's name of a method (toolbox, fast) - std::string_view Name(eMethod method); - // A method by its name; nullopt for anything else - std::optional Parse(std::string_view name); - struct Options { - bool enabled{ true }; // remove_hidden_faces - eMethod method{}; // hsr_method - bool groundPlane{ false }; // hsr_ground_plane: LU Toolbox's black box under the model (y 0 down to -100) - int samples{ 8 }; // hsr_samples: paths traced from each point (LU Toolbox's Samples) - int bounces{ 8 }; // hsr_bounces: bounces a path may take (the Cycles bake's Max Bounces) - 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 (ray_backend) - int fastResolution{ 1024 }; // hsr_fast_resolution: pixels square of each of the fast method's renders - bool sideBySide{}; // trace the paths side by side as for a GPU whatever the backend (to compare the two) + bool enabled{ true }; // remove_hidden_faces + bool groundPlane{ false }; // hsr_ground_plane: nothing is seen from below the model (LU Toolbox's Use Ground Plane) + int resolution{ 1024 }; // hsr_resolution: pixels square of each render }; struct Result { size_t trianglesBefore{}; // opaque and transparent size_t trianglesRemoved{}; std::vector kept; // per opaque triangle before: whether it stayed - uint64_t points{}; // sample points traced from (toolbox) - uint64_t paths{}; // paths traced (toolbox) }; - /** - * Which triangles of `mesh` a path from them reaches the sky from (triangles without area: none). `mesh` is - * everything that occludes (the opaque bricks; transparent ones hide nothing, as in LU Toolbox). - */ - std::vector Visible(const UgcModel::Mesh& mesh, const Options& options, uint64_t* points = nullptr, uint64_t* paths = nullptr); - - // Removes the opaque triangles that aren't Visible, or with the fast method don't show in its renders (nothing when - // options.enabled is off); transparent ones stay + // Removes the opaque triangles that show in none of the renders (nothing when options.enabled is off) Result RemoveHiddenFaces(UgcModel::Model& model, const Options& options); - - /** - * The points on triangle a, b, c paths start from, as barycentric weights (of a, b, c): rows parallel to its - * longest side, `spacing` apart, with points `spacing` apart along each row (so about area / spacing^2 of them). - * When that gives fewer than `minimum` the rows and points are closer together until there are that many; at - * least 4 (its centre and one towards each corner), at most 4096 (big triangles get them further apart). - */ - std::vector SamplePoints(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c, float spacing, size_t minimum = 4); } diff --git a/dUgcServer/Render/UgcRays.cpp b/dUgcServer/Render/UgcRays.cpp index 7427e2f4a..93b3ada25 100644 --- a/dUgcServer/Render/UgcRays.cpp +++ b/dUgcServer/Render/UgcRays.cpp @@ -21,8 +21,7 @@ namespace { using UgcRays::INF; /** - * A bounding volume hierarchy (binned surface area heuristic) over a mesh's triangles, for the paths' rays: the - * nearest hit. A ray never hits the triangle it leaves (`skip`), as in Cycles. + * A bounding volume hierarchy (binned surface area heuristic) over a mesh's triangles, for the nearest hit. A ray never hits the triangle it leaves (`skip`), as in Cycles. */ class ClosestBvh { public: diff --git a/dUgcServer/Render/UgcRays.h b/dUgcServer/Render/UgcRays.h index 241167f58..ba0711297 100644 --- a/dUgcServer/Render/UgcRays.h +++ b/dUgcServer/Render/UgcRays.h @@ -12,8 +12,8 @@ #include "UgcModel.h" /** - * Rays against a mesh's triangles, for the hidden faces' paths (the nearest hit) and the ambient occlusion rays - * (whether anything is hit), by one of several backends (the ugc_ray_backend setting, or per job): + * Rays against a mesh's triangles: the nearest hit (never the triangle a ray leaves) and, for the ambient occlusion + * rays, whether anything is hit, by one of several backends (the ray_backend setting, or per job): * builtin: the UGC server's own bounding volume hierarchies (the ones it always had) * embree: Intel's Embree 4 on the CPU, on the thread that asks (no threads of its own) * hiprt: AMD's HIPRT on the GPU (AMD through HIP, NVIDIA through CUDA, loaded when first asked for by Orochi), diff --git a/dUgcServer/Render/UgcRender.h b/dUgcServer/Render/UgcRender.h index 4a4234f83..fdb6ee8fe 100644 --- a/dUgcServer/Render/UgcRender.h +++ b/dUgcServer/Render/UgcRender.h @@ -93,8 +93,7 @@ namespace UgcRender { Image RenderIcon(const UgcModel::Model& model, const IconOptions& options, const std::vector* opaqueAo = nullptr, const UgcModel::Model* plain = nullptr); /** - * The fast hidden-face test (hsr_method=fast, what the UGC server did before it traced LU Toolbox's paths): the - * opaque mesh rendered from 42 directions around the whole model (SphereDirections), `resolution` pixels square; + * The hidden-face test (UgcHsr): the opaque mesh rendered from 42 directions around the whole model (SphereDirections), `resolution` pixels square; * per opaque triangle whether it shows in any of them (with a conservative test, so small visible ones stay). * Faces seen only by bounced light (interiors, recesses) don't show. `groundPlane`: nothing is seen from below. */ diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index 9b3f6d964..35913b34e 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -155,15 +155,9 @@ namespace { settings.build.satinWhiten = std::clamp(Setting("satin_whiten", 20.0f), 0.0f, 100.0f); settings.hsr.enabled = Setting("remove_hidden_faces", 1) != 0; settings.hsr.groundPlane = Setting("hsr_ground_plane", 0) != 0; - settings.hsr.samples = std::clamp(Setting("hsr_samples", 8), 1, 256); - settings.hsr.bounces = std::clamp(Setting("hsr_bounces", 8), 0, 64); - settings.hsr.spacing = std::clamp(Setting("hsr_sample_spacing", 0.1143f), 0.01f, 10.0f); - settings.hsr.minPoints = std::clamp(Setting("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("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.hsr.resolution = std::clamp(Setting("hsr_resolution", 1024), 64, 4096); + // What traces the occlusion rays (the icon's too) + settings.ao.rays = UgcRays::Parse(Game::config->GetValue("ray_backend")).value_or(UgcRays::eBackend::BUILTIN); // The GPU hiprt uses (read before it is first used; changing it takes a restart) UgcRays::SetGpuDevice(std::max(Setting("hiprt_device", 0), 0)); settings.ao.enabled = Setting("bake_ao", 1) != 0; @@ -180,7 +174,7 @@ 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.ao.rays = settings.ao.rays; settings.icon.denoise = UgcRender::ParseDenoise(Game::config->GetValue("denoise")).value_or(UgcRender::eDenoise::OFF); settings.icon.denoiseSamples = std::clamp(Setting("denoise_samples", 4), 1, 256); settings.icon.bakedAo = settings.ao.enabled ? std::clamp(settings.ao.strength, 0.0f, 1.0f) : 0.0f; @@ -193,8 +187,8 @@ namespace { void LogProcessingOptions(const UgcJobs::Settings& settings) { const auto made = UgcJobs::MadeWith(settings); LOG("Processing options: %s", UgcProcessOptions::ToString(made).c_str()); - if (UgcRays::Resolve(settings.hsr.rays) != settings.hsr.rays) { - LOG("ray_backend=%s can't be used (%s): embree instead", std::string(UgcRays::Name(settings.hsr.rays)).c_str(), UgcRays::Problem(settings.hsr.rays).c_str()); + if (UgcRays::Resolve(settings.ao.rays) != settings.ao.rays) { + LOG("ray_backend=%s can't be used (%s): embree instead", std::string(UgcRays::Name(settings.ao.rays)).c_str(), UgcRays::Problem(settings.ao.rays).c_str()); } if (!UgcRender::Available(settings.icon.denoise)) LOG("denoise=%s can't be used (the server was built without DLU_OIDN): off instead", std::string(UgcRender::Name(settings.icon.denoise)).c_str()); } @@ -579,7 +573,7 @@ namespace { 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"; + std::cerr << "Unknown processing options \"" << options << "\" (ray backend builtin, embree or hiprt; denoise off or oidn)\n"; return EXIT_FAILURE; } UgcJobs::ApplyOptions(settings, choice); diff --git a/docs/Commands.md b/docs/Commands.md index 92afdde0f..f28d47cd6 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -137,7 +137,7 @@ These commands are primarily for development and testing. The usage of many of t |resurrect|`/resurrect`|Resurrects the player.|8| |setminifig|`/setminifig `|Alters your player's minifig. Body part can be one of "Eyebrows", "Eyes", "HairColor", "HairStyle", "Pants", "LeftHand", "Mouth", "RightHand", "Shirt", or "Hands". Changing minifig parts could break the character so this command is limited to GMs.|1| |testmap|`/testmap (clone-id) (instance-id) (spawn-point)`|Transfers you to the given zone by id and clone id and then spawns you at the specified spawn point if one was specified. Ignores instance-id for now. Aliases: `/tm`.|1| -|reprocessproperty|`/reprocessproperty [builtin\|embree\|hiprt] [toolbox\|fast] [off\|oidn]`|Has the UGC server make every brick built model on the property you are on again, with the current UGC settings or the processing options given, in any order (ray backend, hidden-face method, denoising; see docs/UgcServer.md, "Processing options"). When they are all made (at most 15 minutes), everyone on the property is sent back into it, so their game downloads the new meshes. Aliases: `/reloadpropertymodels`.|8| +|reprocessproperty|`/reprocessproperty [builtin\|embree\|hiprt] [off\|oidn]`|Has the UGC server make every brick built model on the property you are on again, with the current UGC settings or the processing options given, in any order (ray backend, denoising; see docs/UgcServer.md, "Processing options"). When they are all made (at most 15 minutes), everyone on the property is sent back into it, so their game downloads the new meshes. Aliases: `/reloadpropertymodels`.|8| |reportproxphys|`/reportproxphys`|Prints to console the position and radius of proximity sensors.|9| |spawnphysicsverts|`/spawnphysicsverts`|Spawns a 1x1 brick at all vertices of phantom physics objects|8| |teleport|`/teleport (y) `|Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z). Any of the coordinates can use the syntax of an exact position (10.0), or a relative position (~+10.0). A ~ means use the current value of that axis as the base value. Addition or subtraction is supported (~+10) (~-10). If source player and target player are players that exist in the world, then the source player will be teleported to target player. Aliases: `/tele`, `/tp`.|6| diff --git a/resources/ugcconfig.ini b/resources/ugcconfig.ini index c90867bee..f11eef59e 100644 --- a/resources/ugcconfig.ini +++ b/resources/ugcconfig.ini @@ -129,23 +129,14 @@ satin_colors=360,362,363,364,365,366,367,376 satin_opacity=75 satin_whiten=20 -# Remove faces that can't be seen from anywhere, as LU Toolbox's Remove Hidden Faces: paths are traced from points on -# each opaque triangle, bouncing off the model; a triangle none of whose paths reaches the sky is removed. -# hsr_ground_plane: 1 also removes what can only be seen from below the model. hsr_samples: paths from each point. -# hsr_bounces: bounces a path may take. hsr_sample_spacing: distance between the points in LDD units (a stud is 0.8). -# hsr_min_points: points on a triangle at least, however small it is (LU Toolbox bakes 28 texels a triangle). +# Remove faces that can't be seen: the opaque bricks are rendered from 42 directions around the model (hsr_resolution +# pixels square) and the triangles that show in none are removed. hsr_ground_plane: 1 also removes what can only be +# seen from below the model. remove_hidden_faces=1 hsr_ground_plane=0 -hsr_samples=8 -hsr_bounces=8 -hsr_sample_spacing=0.1143 -hsr_min_points=28 -# hsr_method: toolbox (LU Toolbox's paths, above) or fast (renders from 42 directions around the model, hsr_fast_resolution -# pixels square; much faster, but it also removes faces seen only by bounced light). -hsr_method=toolbox -hsr_fast_resolution=1024 +hsr_resolution=1024 -# What traces the rays of the hidden faces' paths and of the occlusion: builtin, embree (Intel Embree on the CPU) or +# What traces the occlusion rays: builtin, embree (Intel Embree on the CPU) or # hiprt (the GPU, when built with DLU_HIPRT and the machine has one; else embree). hiprt_device: which GPU (0: the first # HIP or CUDA device; restart to change). ray_backend=builtin diff --git a/tests/dUgcTests/UgcTests.cpp b/tests/dUgcTests/UgcTests.cpp index e308a54ac..d5948917f 100644 --- a/tests/dUgcTests/UgcTests.cpp +++ b/tests/dUgcTests/UgcTests.cpp @@ -224,7 +224,7 @@ TEST(UgcRender, RemovesWhatIsInsideAndDrawsIcons) { )", error); auto model = UgcModel::Build(parts, library); ASSERT_EQ(model.opaque.TriangleCount(), 24u); - const auto result = UgcHsr::RemoveHiddenFaces(model, UgcHsr::Options{}); + const auto result = UgcHsr::RemoveHiddenFaces(model, UgcHsr::Options{ .resolution = 256 }); EXPECT_EQ(result.trianglesRemoved, 12u); EXPECT_EQ(model.opaque.TriangleCount(), 12u); EXPECT_EQ(model.opaque.positions.size(), 8u); @@ -1785,8 +1785,8 @@ TEST(UgcModel, SatinColors) { } TEST(UgcHsr, OffIsByteIdenticalToBefore) { - // remove_hidden_faces=0 makes exactly the files made before the path traced hidden faces replaced the renders. - // The hashes were taken with GCC on x86-64 Linux. + // remove_hidden_faces=0 makes the same files whatever removes hidden faces. The hashes were taken with GCC on + // x86-64 Linux. UgcBricks::BrickLibrary library(MakeRes(), 0); auto settings = SmallSettings(); settings.hsr.enabled = false; @@ -1847,84 +1847,24 @@ namespace { } } -TEST(UgcHsr, KeepsWhatIsSeenThroughADoorwayOrOnlyByBouncedLight) { - const auto open = UgcHsr::Visible(Room(false), UgcHsr::Options{}); - // Every face of the box in the room, the one turned away from the doorway too (only light bounced off the back - // wall reaches it), and every wall - for (size_t t = 0; t < open.size(); t++) EXPECT_TRUE(open[t]) << t; - const auto closed = UgcHsr::Visible(Room(true), UgcHsr::Options{}); - for (size_t t = 0; t < 12; t++) EXPECT_FALSE(closed[t]) << t; -} - -TEST(UgcHsr, IsTheSameEveryTime) { - auto options = UgcHsr::Options{}; - options.seed = 1234; - options.samples = 1; // few paths, so chance matters - options.bounces = 3; - const auto mesh = Room(false); - const auto first = UgcHsr::Visible(mesh, options); - EXPECT_EQ(UgcHsr::Visible(mesh, options), first); - uint64_t points = 0, paths = 0; - UgcHsr::Visible(mesh, options, &points, &paths); - EXPECT_GT(points, 0u); - EXPECT_GT(paths, 0u); -} - TEST(UgcHsr, GroundPlaneHidesTheUnderside) { - UgcModel::Mesh mesh; - AddBox(mesh, glm::vec3(0.0f), glm::vec3(0.8f, 0.96f, 0.8f)); // a brick on LDD's floor - auto options = UgcHsr::Options{}; - const auto without = UgcHsr::Visible(mesh, options); - for (size_t t = 0; t < without.size(); t++) EXPECT_TRUE(without[t]) << t; + // An upside down box open at the bottom: its ceiling (the slab's underside, triangles 4 and 5) is only seen from + // below + UgcModel::Model model; + AddBox(model.opaque, glm::vec3(-1.0f, 1.0f, -1.0f), glm::vec3(1.0f, 1.2f, 1.0f)); + AddBox(model.opaque, glm::vec3(-1.0f, 0.0f, -1.0f), glm::vec3(-0.8f, 1.0f, 1.0f)); + AddBox(model.opaque, glm::vec3(0.8f, 0.0f, -1.0f), glm::vec3(1.0f, 1.0f, 1.0f)); + AddBox(model.opaque, glm::vec3(-0.8f, 0.0f, -1.0f), glm::vec3(0.8f, 1.0f, -0.8f)); + AddBox(model.opaque, glm::vec3(-0.8f, 0.0f, 0.8f), glm::vec3(0.8f, 1.0f, 1.0f)); + UgcHsr::Options options; + options.resolution = 256; + auto without = model; + const auto seen = UgcHsr::RemoveHiddenFaces(without, options); + EXPECT_TRUE(seen.kept[4] && seen.kept[5]); options.groundPlane = true; - const auto with = UgcHsr::Visible(mesh, options); - for (size_t t = 0; t < with.size(); t++) EXPECT_EQ(with[t], t != 4 && t != 5) << t; // triangles 4 and 5: the bottom -} - -TEST(UgcHsr, RemovesTrianglesWithoutArea) { - UgcModel::Mesh mesh; - AddBox(mesh, glm::vec3(0.0f), glm::vec3(0.8f)); - const auto base = static_cast(mesh.positions.size()); - for (int i = 0; i < 3; i++) { - mesh.positions.push_back(glm::vec3(5.0f)); - mesh.normals.push_back(glm::vec3(0, 1, 0)); - } - mesh.indices.insert(mesh.indices.end(), { base, base + 1, base + 2 }); - const auto visible = UgcHsr::Visible(mesh, UgcHsr::Options{}); - ASSERT_EQ(visible.size(), 13u); - for (size_t t = 0; t < 12; t++) EXPECT_TRUE(visible[t]) << t; - EXPECT_FALSE(visible[12]); -} - -TEST(UgcHsr, SamplePointsFollowTheTrianglesSize) { - const auto check = [](const std::vector& points) { - for (const auto& w : points) { - EXPECT_NEAR(w.x + w.y + w.z, 1.0f, 1e-5f); - EXPECT_GT(std::min({ w.x, w.y, w.z }), 0.0f); - } - }; - // Half a stud-sized square: 7 x 7 points on the square - const auto half = UgcHsr::SamplePoints({ 0, 0, 0 }, { 0.8f, 0, 0 }, { 0.8f, 0, 0.8f }, 0.1143f); - check(half); - EXPECT_EQ(half.size(), 25u); - // Four times the area, about four times the points - const auto big = UgcHsr::SamplePoints({ 0, 0, 0 }, { 1.6f, 0, 0 }, { 1.6f, 0, 1.6f }, 0.1143f); - check(big); - EXPECT_EQ(big.size(), 100u); - // A tiny triangle: its centre and one towards each corner - const auto tiny = UgcHsr::SamplePoints({ 0, 0, 0 }, { 0.01f, 0, 0 }, { 0, 0.01f, 0 }, 0.1143f); - check(tiny); - EXPECT_EQ(tiny.size(), 4u); - // A long sliver: points along its length - const auto sliver = UgcHsr::SamplePoints({ 0, 0, 0 }, { 3.2f, 0, 0 }, { 1.6f, 0.01f, 0 }, 0.1143f); - check(sliver); - EXPECT_EQ(sliver.size(), 14u); - // With a minimum (LU Toolbox's 28 texels a triangle) small triangles get their points closer together - const auto dense = UgcHsr::SamplePoints({ 0, 0, 0 }, { 0.01f, 0, 0 }, { 0, 0.01f, 0 }, 0.1143f, 28); - check(dense); - EXPECT_GE(dense.size(), 28u); - EXPECT_LE(dense.size(), 40u); - EXPECT_EQ(UgcHsr::SamplePoints({ 0, 0, 0 }, { 1.6f, 0, 0 }, { 1.6f, 0, 1.6f }, 0.1143f, 28).size(), 100u); // bigger ones as before + const auto with = UgcHsr::RemoveHiddenFaces(model, options); + EXPECT_FALSE(with.kept[4] || with.kept[5]); + EXPECT_TRUE(with.kept[6] && with.kept[7]); // the roof } // Stopping the server cancels the jobs being made: Checkpoint throws until the cancel is cleared @@ -2002,10 +1942,7 @@ namespace { } } -TEST(UgcHsr, FastMethodRendersFromAround) { - EXPECT_EQ(UgcHsr::Parse("fast"), UgcHsr::eMethod::FAST); - EXPECT_EQ(UgcHsr::Parse("toolbox"), UgcHsr::eMethod::TOOLBOX); - EXPECT_FALSE(UgcHsr::Parse("slow")); +TEST(UgcHsr, RendersFromAround) { EXPECT_EQ(UgcRender::SphereDirections().size(), 42u); // A small box inside the big one: its faces can't be seen @@ -2017,16 +1954,13 @@ TEST(UgcHsr, FastMethodRendersFromAround) { )", error); auto model = UgcModel::Build(parts, library); UgcHsr::Options options; - options.method = UgcHsr::eMethod::FAST; - options.fastResolution = 256; + options.resolution = 256; const auto result = UgcHsr::RemoveHiddenFaces(model, options); EXPECT_EQ(result.trianglesRemoved, 12u); EXPECT_EQ(model.opaque.TriangleCount(), 12u); - EXPECT_EQ(result.paths, 0u); // A small box in a closed chamber whose only opening is a narrow chimney at the other end: nothing outside sees it - // straight, so the fast method removes it; light bounced in through the chimney reaches some of it, so the toolbox - // method keeps that + // straight, so it is removed (it would only be seen by light bounced in through the chimney) UgcModel::Model chamber; auto& mesh = chamber.opaque; AddBox(mesh, glm::vec3(-0.9f, 0.0f, -0.15f), glm::vec3(-0.6f, 0.2f, 0.15f)); // the small box @@ -2046,15 +1980,9 @@ TEST(UgcHsr, FastMethodRendersFromAround) { const auto seen = UgcRender::VisibleFromAround(chamber, 512, false); ASSERT_EQ(seen.size(), mesh.TriangleCount()); for (size_t t = 0; t < 12; t++) EXPECT_FALSE(seen[t]) << t; - UgcHsr::Options toolbox; - toolbox.samples = 32; - const auto traced = UgcHsr::Visible(mesh, toolbox); - EXPECT_TRUE(std::any_of(traced.begin(), traced.begin() + 12, [](bool kept) { return kept; })); // Its files are the same every time auto settings = SmallSettings(); - settings.hsr.method = UgcHsr::eMethod::FAST; - settings.hsr.fastResolution = 128; const auto first = UgcJobs::ProcessModel(LXFML5, library, settings, 7); ASSERT_TRUE(first.ok) << first.error; EXPECT_EQ(first.files.at("model.nif.checksum"), UgcJobs::ProcessModel(LXFML5, library, settings, 7).files.at("model.nif.checksum")); @@ -2065,33 +1993,34 @@ TEST(UgcHsr, FastMethodRendersFromAround) { TEST(UgcProcessOptions, ParseApplyAndRecord) { UgcProcessOptions::Choice choice; - ASSERT_TRUE(UgcProcessOptions::Parse("fast embree", choice)); + ASSERT_TRUE(UgcProcessOptions::Parse("oidn embree", choice)); EXPECT_EQ(choice.rays, "embree"); - EXPECT_EQ(choice.hsr, "fast"); - EXPECT_EQ(choice.denoise, ""); - EXPECT_EQ(UgcProcessOptions::ToString(choice), "embree fast"); + EXPECT_EQ(choice.denoise, "oidn"); + EXPECT_EQ(UgcProcessOptions::ToString(choice), "embree oidn"); ASSERT_TRUE(UgcProcessOptions::Parse("default - oidn", choice)); EXPECT_EQ(UgcProcessOptions::ToString(choice), "oidn"); ASSERT_TRUE(UgcProcessOptions::Parse("", choice)); EXPECT_TRUE(choice.Empty()); EXPECT_FALSE(UgcProcessOptions::Parse("embree hiprt", choice)); // two backends EXPECT_FALSE(UgcProcessOptions::Parse("optix", choice)); + // Options stored by earlier versions named a hidden-face method: still read, the method ignored + ASSERT_TRUE(UgcProcessOptions::Parse("embree toolbox off", choice)); + EXPECT_EQ(UgcProcessOptions::ToString(choice), "embree off"); + ASSERT_TRUE(UgcProcessOptions::Parse("fast", choice)); + EXPECT_TRUE(choice.Empty()); // The shared names are the UGC server's for (const auto name : UgcProcessOptions::RAYS) EXPECT_EQ(UgcRays::Name(*UgcRays::Parse(name)), name); - for (const auto name : UgcProcessOptions::HSR) EXPECT_EQ(UgcHsr::Name(*UgcHsr::Parse(name)), name); for (const auto name : UgcProcessOptions::DENOISE) EXPECT_EQ(UgcRender::Name(*UgcRender::ParseDenoise(name)), name); // Applied over the settings; what made a model is recorded as it was used UgcJobs::Settings settings; - EXPECT_EQ(UgcProcessOptions::ToString(UgcJobs::MadeWith(settings)), "builtin toolbox off"); - ASSERT_TRUE(UgcProcessOptions::Parse("embree fast", choice)); + EXPECT_EQ(UgcProcessOptions::ToString(UgcJobs::MadeWith(settings)), "builtin off"); + ASSERT_TRUE(UgcProcessOptions::Parse("embree", choice)); UgcJobs::ApplyOptions(settings, choice); - EXPECT_EQ(settings.hsr.rays, UgcRays::eBackend::EMBREE); EXPECT_EQ(settings.ao.rays, UgcRays::eBackend::EMBREE); EXPECT_EQ(settings.icon.ao.rays, UgcRays::eBackend::EMBREE); - EXPECT_EQ(settings.hsr.method, UgcHsr::eMethod::FAST); - EXPECT_EQ(UgcProcessOptions::ToString(UgcJobs::MadeWith(settings)), "embree fast off"); + EXPECT_EQ(UgcProcessOptions::ToString(UgcJobs::MadeWith(settings)), "embree off"); ASSERT_TRUE(UgcProcessOptions::Parse("hiprt oidn", choice)); UgcJobs::ApplyOptions(settings, choice); const auto made = UgcJobs::MadeWith(settings); @@ -2107,8 +2036,8 @@ TEST(UgcProcessOptions, ParseApplyAndRecord) { EXPECT_EQ(outcome.options, UgcProcessOptions::ToString(UgcJobs::MadeWith(small))); const auto stats = nlohmann::json::parse(outcome.stats); EXPECT_EQ(stats["settings"]["rays"], made.rays); - EXPECT_EQ(stats["settings"]["hsrMethod"], "toolbox"); EXPECT_EQ(stats["settings"]["denoise"], made.denoise); + EXPECT_FALSE(stats["settings"].contains("hsrMethod")); } TEST(UgcRender, DenoisedIconsTraceTheOcclusionPerPixel) { @@ -2215,49 +2144,14 @@ TEST(UgcRays, BackendsFindTheSameHits) { } } -TEST(UgcHsr, SideBySideDecidesTheSameAsOneByOne) { - // The paths traced side by side (as for a GPU) are the same paths, so the same triangles stay; ground plane too - for (const bool ground : { false, true }) { - UgcHsr::Options options; - options.seed = 5; - options.samples = 2; - options.groundPlane = ground; - const auto mesh = Clutter(); - uint64_t points = 0, paths = 0, sidePoints = 0, sidePaths = 0; - const auto oneByOne = UgcHsr::Visible(mesh, options, &points, &paths); - options.sideBySide = true; - EXPECT_EQ(UgcHsr::Visible(mesh, options, &sidePoints, &sidePaths), oneByOne) << ground; - EXPECT_EQ(sidePoints, points); - EXPECT_GE(sidePaths, paths); // it traces a round's other paths too once one escaped - EXPECT_EQ(UgcHsr::Visible(Room(true), options), UgcHsr::Visible(Room(true), UgcHsr::Options{ .groundPlane = ground, .samples = 2, .seed = 5 })); - } -} - -TEST(UgcRays, OtherBackendsMakeTheSameModels) { - // The hidden faces and the occlusion with each backend. The paths bounce, so a hit found a rounding further away - // sends a path on from a slightly different point: the rare triangle decided by a path that only just gets out - // may go the other way. The small test model's files are the same. +TEST(UgcRays, OtherBackendsMakeTheSameOcclusion) { + // The occlusion with each backend, within rounding of builtin's; the small test model's files are the same const auto mesh = Clutter(); - UgcHsr::Options hsr; - hsr.seed = 99; - const auto expected = UgcHsr::Visible(mesh, hsr); const auto aoExpected = UgcRender::AmbientOcclusion(mesh, mesh, 2.0f, 64); UgcBricks::BrickLibrary library(MakeRes(), 0); const auto builtinModel = UgcJobs::ProcessModel(LOOKS_LXFML, library, SmallSettings(), 7); ASSERT_TRUE(builtinModel.ok) << builtinModel.error; for (const auto backend : OtherBackends()) { - hsr.rays = backend; - const auto visible = UgcHsr::Visible(mesh, hsr); - ASSERT_EQ(visible.size(), expected.size()); - size_t differ = 0; - for (size_t t = 0; t < visible.size(); t++) differ += visible[t] != expected[t] ? 1 : 0; - EXPECT_LE(differ, visible.size() / 50) << UgcRays::Name(backend); - // The room's closed-off box is removed and what the doorway shows is kept, as with builtin - const auto closed = UgcHsr::Visible(Room(true), hsr); - for (size_t t = 0; t < 12; t++) EXPECT_FALSE(closed[t]) << UgcRays::Name(backend) << " " << t; - const auto open = UgcHsr::Visible(Room(false), hsr); - for (size_t t = 0; t < open.size(); t++) EXPECT_TRUE(open[t]) << UgcRays::Name(backend) << " " << t; - const auto ao = UgcRender::AmbientOcclusion(mesh, mesh, 2.0f, 64, backend); ASSERT_EQ(ao.size(), aoExpected.size()); double total = 0.0; @@ -2268,7 +2162,6 @@ TEST(UgcRays, OtherBackendsMakeTheSameModels) { EXPECT_LT(total / static_cast(ao.size()), 0.002) << UgcRays::Name(backend); auto settings = SmallSettings(); - settings.hsr.rays = backend; settings.ao.rays = backend; const auto made = UgcJobs::ProcessModel(LOOKS_LXFML, library, settings, 7); ASSERT_TRUE(made.ok) << made.error;
OptionsMakesModelsTookCPUHidden facesOcclusionIconBricksTriangles removed