From 175db95c98e587ce22ad577fd560ec830b87648e Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 09:38:51 -0500 Subject: [PATCH] feat(ugc): one shape per transparent brick, as LU Toolbox leaves them LU Toolbox's Combine Transparent is off by default, so each transparent brick is its own object and shape (the client can sort them). combine_transparent=1 joins them as before. Co-Authored-By: Claude Opus 5.5 --- dUgcServer/UgcJobs.cpp | 4 ++-- dUgcServer/UgcJobs.h | 1 + dUgcServer/UgcModel.cpp | 25 +++++++++++++++++++++++++ dUgcServer/UgcModel.h | 4 ++++ dUgcServer/UgcServer.cpp | 1 + resources/ugcconfig.ini | 4 +++- tests/dUgcTests/UgcTests.cpp | 17 +++++++++++++++++ 7 files changed, 53 insertions(+), 3 deletions(-) diff --git a/dUgcServer/UgcJobs.cpp b/dUgcServer/UgcJobs.cpp index cc2e7b986..902bbeee5 100644 --- a/dUgcServer/UgcJobs.cpp +++ b/dUgcServer/UgcJobs.cpp @@ -154,7 +154,7 @@ namespace UgcJobs { entry["opaqueAfter"] = model.opaque.TriangleCount(); entry["vertices"] = model.opaque.positions.size() + model.transparent.positions.size(); opaquePieces.push_back(UgcModel::Divide(model.opaque)); - transparentPieces.push_back(UgcModel::Divide(model.transparent)); + transparentPieces.push_back(settings.combineTransparent ? UgcModel::Divide(model.transparent) : UgcModel::SplitAt(model.transparent, model.transparentBricks)); entry["shapes"] = opaquePieces.back().size() + transparentPieces.back().size(); lodStats.push_back(entry); } @@ -181,7 +181,7 @@ namespace UgcJobs { AddDownload(outcome.files, "model.nif", nif); AddDownload(outcome.files, "model.lxfml", lxfml); { - const std::vector> opaque{ UgcModel::Divide(preview.opaque) }, transparent{ UgcModel::Divide(preview.transparent) }; + const std::vector> opaque{ UgcModel::Divide(preview.opaque) }, transparent{ transparentPieces[0] }; outcome.files["model.noao.nif"] = UgcFormats::WriteLodNif("SceneNode_Model", groups(1, opaque, transparent)); } diff --git a/dUgcServer/UgcJobs.h b/dUgcServer/UgcJobs.h index d9ceaae9a..3b34ea706 100644 --- a/dUgcServer/UgcJobs.h +++ b/dUgcServer/UgcJobs.h @@ -24,6 +24,7 @@ namespace UgcJobs { std::vector lods{ 0, 2 }; // brickprimitives levels made (LU Toolbox imports LOD 0 and 2; the client has no 3) UgcModel::LodDistances lodDistances; 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) UgcRender::OptimizeOptions optimize; // hidden surface removal UgcRender::AoOptions ao; // Bake Lighting (AO Only) UgcRender::IconOptions icon; diff --git a/dUgcServer/UgcModel.cpp b/dUgcServer/UgcModel.cpp index 81edd0875..60c1c0705 100644 --- a/dUgcServer/UgcModel.cpp +++ b/dUgcServer/UgcModel.cpp @@ -204,6 +204,7 @@ namespace UgcModel { transparent = transparent && (luToolbox ? UgcPalette::IsTransparent(materialOf(index)) : library.GetMaterial(id).Transparent()); } auto& mesh = transparent ? model.transparent : model.opaque; + if (transparent) model.transparentBricks.push_back(mesh.indices.size()); const glm::mat3 normalMatrix = glm::transpose(glm::inverse(glm::mat3(part.transform))); for (size_t index = 0; index < design->size(); index++) { const auto& geometry = (*design)[index]; @@ -317,6 +318,30 @@ namespace UgcModel { return model; } + std::vector SplitAt(const Mesh& mesh, const std::vector& starts) { + std::vector pieces; + std::unordered_map remap; + for (size_t i = 0; i < starts.size(); i++) { + const size_t first = starts[i], last = std::min(i + 1 < starts.size() ? starts[i + 1] : mesh.indices.size(), mesh.indices.size()); + if (first >= last) continue; + Mesh piece; + remap.clear(); + for (size_t k = first; k < last; k++) { + const auto source = mesh.indices[k]; + auto [it, added] = remap.try_emplace(source, static_cast(piece.positions.size())); + if (added) { + piece.positions.push_back(mesh.positions[source]); + if (source < mesh.normals.size()) piece.normals.push_back(mesh.normals[source]); + if (source < mesh.colors.size()) piece.colors.push_back(mesh.colors[source]); + if (source < mesh.glow.size()) piece.glow.push_back(mesh.glow[source]); + } + piece.indices.push_back(it->second); + } + pieces.push_back(std::move(piece)); + } + return pieces; + } + void KeepTriangles(Mesh& mesh, const std::vector& keep) { Mesh kept; std::vector remap(mesh.positions.size(), UINT32_MAX); diff --git a/dUgcServer/UgcModel.h b/dUgcServer/UgcModel.h index 0375ffbe3..ef17e9837 100644 --- a/dUgcServer/UgcModel.h +++ b/dUgcServer/UgcModel.h @@ -46,6 +46,7 @@ namespace UgcModel { Mesh opaque; Mesh transparent; std::vector missingDesigns; // designs without geometry in the client, skipped + std::vector transparentBricks; // where each transparent brick's triangles start in transparent.indices size_t bricks{}; bool Empty() const { return opaque.Empty() && transparent.Empty(); } @@ -99,6 +100,9 @@ namespace UgcModel { // A client .nif's meshes as one model (vertex colors times material color; transparent when blended) Model FromNif(const NifFile::Model& nif); + // The mesh cut into pieces at these index offsets (each piece's triangles start at one), e.g. one per brick + std::vector SplitAt(const Mesh& mesh, const std::vector& starts); + // Keeps the triangles whose flag is set (and the vertices they use) void KeepTriangles(Mesh& mesh, const std::vector& keep); diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index 1e5b5ec16..e9e32f621 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -80,6 +80,7 @@ namespace { settings.lodDistances.lod3 = Setting("lod_distance_3", 280.0f); settings.lodDistances.cull = Setting("lod_cull", 10000.0f); if (!Game::config->GetValue("shader_opaque").empty()) settings.shaderOpaque = Game::config->GetValue("shader_opaque"); + settings.combineTransparent = Setting("combine_transparent", 0) != 0; settings.optimize.removeHidden = Setting("remove_hidden_faces", 1) != 0; settings.optimize.groundPlane = Setting("hsr_ground_plane", 0) != 0; settings.optimize.resolution = Setting("optimize_resolution", 1024); diff --git a/resources/ugcconfig.ini b/resources/ugcconfig.ini index 9c7ac289f..e8e3b06c7 100644 --- a/resources/ugcconfig.ini +++ b/resources/ugcconfig.ini @@ -59,8 +59,10 @@ lod_distance_2=100 lod_distance_3=280 lod_cull=10000 -# The shapes' shader number: S_Opaque_Model (transparent shapes are S01_Alpha_Model) +# The shapes' shader number: S_Opaque_Model (transparent shapes are S01_Alpha_Model), and whether all +# transparent bricks are one shape (1) or each its own (0, so the client can sort them) shader_opaque=01 +combine_transparent=0 # Remove faces that can't be seen from anywhere (optimize_resolution: detail of the renders that decide it; # hsr_ground_plane: 1 also removes what can only be seen from below the model) diff --git a/tests/dUgcTests/UgcTests.cpp b/tests/dUgcTests/UgcTests.cpp index 8fe716a18..ee927e1da 100644 --- a/tests/dUgcTests/UgcTests.cpp +++ b/tests/dUgcTests/UgcTests.cpp @@ -375,6 +375,7 @@ TEST(UgcModel, ColorsLikeLuToolbox) { plain.colorVariation = 0.0f; const auto flat = UgcModel::Build(parts, library, plain); EXPECT_EQ(flat.transparent.TriangleCount(), 12u); // only the all-transparent brick + EXPECT_EQ(flat.transparentBricks, std::vector{ 0 }); EXPECT_EQ(flat.opaque.TriangleCount(), 60u); EXPECT_NEAR(flat.opaque.colors[0].r * 255.0f, 222.0f, 0.5f); EXPECT_FLOAT_EQ(flat.opaque.colors[0].a, 1.0f); @@ -447,6 +448,22 @@ TEST(UgcModel, DividesAlongTheLongestSide) { EXPECT_EQ(UgcModel::Divide(mesh, 100, 1000).size(), 1u); } +TEST(UgcModel, SplitsTransparentBricksApart) { + // Two boxes in one mesh, one shape each (LU Toolbox leaves transparent bricks uncombined) + UgcBricks::BrickLibrary library(MakeRes(), 0); + std::string error; + const auto model = UgcModel::Build(UgcModel::ParseLxfml(R"( + + + )", error), library); + ASSERT_EQ(model.transparentBricks.size(), 2u); + const auto pieces = UgcModel::SplitAt(model.transparent, model.transparentBricks); + ASSERT_EQ(pieces.size(), 2u); + EXPECT_EQ(pieces[0].positions.size(), 8u); + EXPECT_EQ(pieces[1].TriangleCount(), 12u); + EXPECT_NEAR(pieces[1].positions[0].x, 5.0f, 1e-5f); +} + TEST(UgcFormats, LodNifReadsBack) { UgcModel::Mesh near, far; near.positions = { { 0, 0, 0 }, { 1, 0, 0 }, { 0, 1, 0 }, { 1, 1, 0 } };