diff --git a/dDashboardServer/routes/SettingsCatalog.cpp b/dDashboardServer/routes/SettingsCatalog.cpp index 116ef9973..f30e985b3 100644 --- a/dDashboardServer/routes/SettingsCatalog.cpp +++ b/dDashboardServer/routes/SettingsCatalog.cpp @@ -456,9 +456,7 @@ namespace { c.Add(Float(UGC, "icon_sun_strength", "Icon sun strength", "", "2.5", 0, 100)); c.Add(Float(UGC, "icon_ambient", "Icon world light", "", "0.192", 0, 10)); c.Add(Bool(UGC, "icon_shadows", "Icon shadows", "", true)); - c.Add(Bool(UGC, "icon_ao", "Icon ambient occlusion", "", true)); - c.Add(Bool(UGC, "icon_correct_colors", "Icon color corrections", "White and black toned down, as LU Toolbox's icon renderer does.", true)); - c.Add(Unit(Float(UGC, "icon_color_variation", "Icon color variation", "", "0", 0, 100), "%")); + c.Add(Bool(UGC, "icon_ao", "Car and rocket icon ambient occlusion", "Player models' icons show the lighting baked into their mesh instead.", true)); c.Add(Unit(Float(UGC, "modular_icon_yaw", "Car and rocket icon angle around", "", "53.36", -360, 360), "degrees")); c.Add(Unit(Float(UGC, "modular_icon_pitch", "Car and rocket icon angle above", "", "19.54", -90, 90), "degrees")); diff --git a/dUgcServer/UgcJobs.cpp b/dUgcServer/UgcJobs.cpp index 902bbeee5..b53941d91 100644 --- a/dUgcServer/UgcJobs.cpp +++ b/dUgcServer/UgcJobs.cpp @@ -5,6 +5,7 @@ #include "json.hpp" +#include "NifFile.h" #include "Sd0.h" #include "UgcFormats.h" #include "UgcModel.h" @@ -101,12 +102,6 @@ namespace UgcJobs { std::vector models; std::vector> opaquePieces, transparentPieces; UgcModel::Model preview; // LOD 0 before the lighting bake, for the dashboard - // The icon: LU Toolbox's icon renderer imports LOD 0 again, with its own color corrections and no variation. - // The same bricks in the same order, so LOD 0's hidden surface removal and occlusion apply to it too (neither - // changes what the icon's camera sees). - UgcModel::Model iconModel; - std::vector iconAo; - double iconMs = 0; for (size_t i = 0; i < lods.size(); i++) { auto options = settings.build; options.seed = seed; @@ -135,22 +130,10 @@ namespace UgcJobs { if (!outcome.note.empty()) outcome.note += "; "; outcome.note += "removed " + std::to_string(optimized.trianglesRemoved) + " of " + std::to_string(optimized.trianglesBefore) + " triangles"; } - if (i == 0) { - preview = model; - step = std::chrono::steady_clock::now(); - auto iconOptions = settings.build; - iconOptions.seed = seed; - iconOptions.lod = 0; - iconOptions.icon = settings.iconCorrectColors; - iconOptions.colorVariation = settings.iconColorVariation; - iconModel = UgcModel::Build(parts, library, iconOptions); - if (!optimized.kept.empty() && iconModel.opaque.TriangleCount() == optimized.kept.size()) UgcModel::KeepTriangles(iconModel.opaque, optimized.kept); - iconMs += Since(step); - } + if (i == 0) preview = model; step = std::chrono::steady_clock::now(); - auto ao = UgcRender::BakeAo(model, settings.ao); + UgcRender::BakeAo(model, settings.ao); aoMs += Since(step); - if (i == 0) iconAo = std::move(ao); entry["opaqueAfter"] = model.opaque.TriangleCount(); entry["vertices"] = model.opaque.positions.size() + model.transparent.positions.size(); opaquePieces.push_back(UgcModel::Divide(model.opaque)); @@ -185,9 +168,20 @@ namespace UgcJobs { outcome.files["model.noao.nif"] = UgcFormats::WriteLodNif("SceneNode_Model", groups(1, opaque, transparent)); } + // The icon is drawn from the .nif just made (its most detailed LOD, read back like any client .nif), so it + // shows what the game shows: the colors with their variation, hidden faces removed, the baked lighting. The + // lighting is in its vertex colors already, so the icon adds no occlusion of its own. const auto iconStart = std::chrono::steady_clock::now(); - AddIcon(outcome.files, iconModel, settings.icon, iconAo.empty() ? nullptr : &iconAo); - iconMs += Since(iconStart); + std::string nifError; + const auto readBack = NifFile::Parse(nif, 0, nifError); + if (!readBack) { + outcome.error = "the .nif made can't be read back for the icon: " + nifError; + return outcome; + } + auto iconOptions = settings.icon; + iconOptions.ao.enabled = false; + AddIcon(outcome.files, UgcModel::FromNif(*readBack), iconOptions); + const double iconMs = Since(iconStart); stats["ms"] = { { "build", std::lround(buildMs) }, { "hiddenSurfaces", std::lround(hsrMs) }, { "ambientOcclusion", std::lround(aoMs) }, { "icon", std::lround(iconMs) }, { "total", std::lround(Since(started)) } }; diff --git a/dUgcServer/UgcJobs.h b/dUgcServer/UgcJobs.h index 3b34ea706..df49562ae 100644 --- a/dUgcServer/UgcJobs.h +++ b/dUgcServer/UgcJobs.h @@ -27,9 +27,7 @@ namespace UgcJobs { bool combineTransparent{ false }; // one shape for all transparent bricks, else one per brick (Combine Transparent) UgcRender::OptimizeOptions optimize; // hidden surface removal UgcRender::AoOptions ao; // Bake Lighting (AO Only) - UgcRender::IconOptions icon; - bool iconCorrectColors{ true }; // the icon renderer's Correct Colors - float iconColorVariation{ 0.0f }; // percent; its Apply Color Variation is off + UgcRender::IconOptions icon; // player models: drawn from their .nif (LOD 0); its ao is not used UgcRender::IconOptions modularIcon; uint32_t maxBricks{}; // a model with more fails; 0: no limit }; diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index e9e32f621..8af4ec82d 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -101,8 +101,6 @@ namespace { settings.icon.shadows = Setting("icon_shadows", 1) != 0; settings.icon.ao.enabled = Setting("icon_ao", 1) != 0; settings.icon.ao.distance = settings.ao.distance; - settings.iconCorrectColors = Setting("icon_correct_colors", 1) != 0; - settings.iconColorVariation = std::clamp(Setting("icon_color_variation", 0.0f), 0.0f, 100.0f); settings.modularIcon = settings.icon; settings.modularIcon.yawDegrees = Setting("modular_icon_yaw", 53.36f); settings.modularIcon.pitchDegrees = Setting("modular_icon_pitch", 19.54f); diff --git a/docs/UgcServer.md b/docs/UgcServer.md index 53c5b8422..0b7750bb2 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -73,12 +73,13 @@ defaults. The table below goes through it step by step. blending. Opaque shapes are divided at 65535 vertices along their longest side (LU Toolbox's divide_mesh); transparent bricks are one shape each unless `combine_transparent=1`. Vertices are in LDD's Y-up space with identity transforms, like the game's own brick models. -7. The icon is rendered like LU Toolbox's icon renderer (its UGC render add-on's BrickBuild scene) by a software - rasterizer (no GPU, no display), 4x4 supersampled: LOD 0 with the icon's color corrections (white and black toned - down) and no color variation, a 50 mm lens (39.6 degrees) from 53.4 degrees around and 19.5 above, framed at 1.03, - a sun of strength 2.5 from 21 degrees around and 50.3 above with soft shadows, and a grey (0.192) world light - darkened by the ambient occlusion, on a transparent background: `icon.png` for the dashboard and a 32-bit - `icon.dds` for the client. +7. The icon is drawn from the finished `.nif`: it is read back with the same reader as the client's files (NifFile, + LOD 0) and rasterized, so it shows exactly what the game shows, with the color variation, the faces that were + removed and the lighting baked into the vertex colors (so the icon adds no occlusion of its own). The camera and + lights are LU Toolbox's icon renderer's (its UGC render add-on's BrickBuild scene), drawn by a software rasterizer + (no GPU, no display), 4x4 supersampled: a 50 mm lens (39.6 degrees) from 53.4 degrees around and 19.5 above, + framed at 1.03, a sun of strength 2.5 from 21 degrees around and 50.3 above with soft shadows, and a grey (0.192) + world light, on a transparent background: `icon.png` for the dashboard and a 32-bit `icon.dds` for the client. 8. `stats.json` records the bricks, each LOD's triangles before and after hidden faces were removed, vertices, shapes, how long each step took and the settings used; `model.noao.nif` is LOD 0 before the lighting bake. Both, and the icon and mesh of the version before (`previous.*`), are for the dashboard's viewer. @@ -108,9 +109,9 @@ defaults. The table below goes through it step by step. | Bake Lighting, AO Only: 64 AO samples, distance 5, transparent skipped, glow strength 3 x 2, smooth vertex colors | Same (`ao_samples`, `ao_distance`, `glow_strength`); smoothing averages a vertex's corners, and the occlusion is per vertex already | | NifTools export for LU: 20.3.0.9, user version 0 | Same | | Physics (`.hkx`) | Intentionally not done: `.hkx` requests answer 404, so the client makes its own | -| Icon: LOD 0, no hidden face removal, its own color corrections, no color variation | Same (`icon_correct_colors`, `icon_color_variation=0`); LOD 0's hidden face removal is reused (it doesn't change what the camera sees) | +| Icon: LOD 0 imported again with its own color corrections (white and black toned down) and no color variation | Different on purpose: the icon is drawn from the generated `.nif` (LOD 0), so it matches the game, variation and baked lighting included; no icon-only color corrections | | Icon: Bevel Edges and Subdivide | Not done (the rasterizer draws the bricks as they are) | -| Icon: principled materials (roughness 0.16), hashed transparency, Cycles | Approximated: diffuse sun with soft shadow-mapped shadows plus world light with ambient occlusion; no highlights or bounced light; transparent bricks sorted and blended | +| Icon: principled materials (roughness 0.16), hashed transparency, Cycles | Approximated: diffuse sun with soft shadow-mapped shadows plus world light (player models: their baked lighting; cars and rockets: ambient occlusion, `icon_ao`); no highlights or bounced light; transparent bricks sorted and blended | | Icon scene BrickBuild / Car: 50 mm lens, camera 53.4 / 19.5 degrees, sun 2.5 at 21 / 50.3, world 0.192, 128 px, framing 1.03, transparent film | Same (`icon_*`, `modular_icon_*`) | | Icon scene Rocket: 35 mm lens, other angles, two suns | Not done: rockets use the car camera | diff --git a/resources/ugcconfig.ini b/resources/ugcconfig.ini index e8e3b06c7..5303e5dc0 100644 --- a/resources/ugcconfig.ini +++ b/resources/ugcconfig.ini @@ -80,8 +80,9 @@ glow_strength=6 # Icons, set up like LU Toolbox's icon renderer: size in pixels, the camera's angle around (yaw, from the model's # front) and above (pitch) in degrees, its field of view, the empty border (1 fills the icon), the sun's angles and -# strength, the world light, shadows and ambient occlusion, its color corrections and color variation (percent). -# Modular builds (cars, rockets) have their own camera angles. +# strength, the world light and shadows. A player model's icon is drawn from its finished .nif (LOD 0), so it shows +# its colors, variation and baked lighting exactly as the game does. icon_ao: ambient occlusion for car and rocket +# icons (player models have theirs baked in). Modular builds (cars, rockets) have their own camera angles. icon_size=128 icon_yaw=53.36 icon_pitch=19.54 @@ -93,7 +94,5 @@ icon_sun_strength=2.5 icon_ambient=0.192 icon_shadows=1 icon_ao=1 -icon_correct_colors=1 -icon_color_variation=0 modular_icon_yaw=53.36 modular_icon_pitch=19.54 diff --git a/tests/dUgcTests/UgcTests.cpp b/tests/dUgcTests/UgcTests.cpp index ee927e1da..1b40f4082 100644 --- a/tests/dUgcTests/UgcTests.cpp +++ b/tests/dUgcTests/UgcTests.cpp @@ -566,6 +566,10 @@ TEST(UgcJobs, MakesLodsStatsAndIcons) { const auto far = NifFile::Parse(outcome.files.at("model.nif"), 1, error); ASSERT_TRUE(far) << error; EXPECT_TRUE(far->nodes.contains("LOD_2")); + // The icon is the .nif's LOD 0, drawn with the icon camera and no occlusion of its own + auto iconOptions = settings.icon; + iconOptions.ao.enabled = false; + EXPECT_EQ(outcome.files.at("icon.png"), UgcFormats::EncodePng(UgcRender::RenderIcon(UgcModel::FromNif(*nif), iconOptions))); // The same colors when made again EXPECT_EQ(UgcJobs::ProcessModel(LXFML5, library, settings, 99).files.at("model.nif"), outcome.files.at("model.nif"));