mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(ugc): draw player models' icons from their generated NIF
The icon was rendered from a second mesh built only for it (the icon renderer's color corrections, no variation, occlusion worked out again). It is now drawn from the .nif just made, read back with NifFile at LOD 0, so it shows exactly what the game shows: the color variation, the removed faces and the lighting baked into the vertex colors (so it adds no occlusion of its own). icon_correct_colors and icon_color_variation are gone; the camera and light settings stay. Cars and rockets are drawn as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"));
|
||||
|
||||
|
||||
@@ -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<UgcModel::Model> models;
|
||||
std::vector<std::vector<UgcModel::Mesh>> 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<float> 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;
|
||||
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);
|
||||
}
|
||||
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)) } };
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -101,8 +101,6 @@ namespace {
|
||||
settings.icon.shadows = Setting<int32_t>("icon_shadows", 1) != 0;
|
||||
settings.icon.ao.enabled = Setting<int32_t>("icon_ao", 1) != 0;
|
||||
settings.icon.ao.distance = settings.ao.distance;
|
||||
settings.iconCorrectColors = Setting<int32_t>("icon_correct_colors", 1) != 0;
|
||||
settings.iconColorVariation = std::clamp(Setting<float>("icon_color_variation", 0.0f), 0.0f, 100.0f);
|
||||
settings.modularIcon = settings.icon;
|
||||
settings.modularIcon.yawDegrees = Setting<float>("modular_icon_yaw", 53.36f);
|
||||
settings.modularIcon.pitchDegrees = Setting<float>("modular_icon_pitch", 19.54f);
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user