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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 09:38:51 -05:00
parent 7c5d69c1ed
commit 175db95c98
7 changed files with 53 additions and 3 deletions

View File

@@ -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<std::vector<UgcModel::Mesh>> opaque{ UgcModel::Divide(preview.opaque) }, transparent{ UgcModel::Divide(preview.transparent) };
const std::vector<std::vector<UgcModel::Mesh>> opaque{ UgcModel::Divide(preview.opaque) }, transparent{ transparentPieces[0] };
outcome.files["model.noao.nif"] = UgcFormats::WriteLodNif("SceneNode_Model", groups(1, opaque, transparent));
}

View File

@@ -24,6 +24,7 @@ namespace UgcJobs {
std::vector<uint32_t> 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<shader>_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;

View File

@@ -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<Mesh> SplitAt(const Mesh& mesh, const std::vector<size_t>& starts) {
std::vector<Mesh> pieces;
std::unordered_map<uint32_t, uint32_t> 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<uint32_t>(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<bool>& keep) {
Mesh kept;
std::vector<uint32_t> remap(mesh.positions.size(), UINT32_MAX);

View File

@@ -46,6 +46,7 @@ namespace UgcModel {
Mesh opaque;
Mesh transparent;
std::vector<uint32_t> missingDesigns; // designs without geometry in the client, skipped
std::vector<size_t> 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<Mesh> SplitAt(const Mesh& mesh, const std::vector<size_t>& starts);
// Keeps the triangles whose flag is set (and the vertices they use)
void KeepTriangles(Mesh& mesh, const std::vector<bool>& keep);

View File

@@ -80,6 +80,7 @@ namespace {
settings.lodDistances.lod3 = Setting<float>("lod_distance_3", 280.0f);
settings.lodDistances.cull = Setting<float>("lod_cull", 10000.0f);
if (!Game::config->GetValue("shader_opaque").empty()) settings.shaderOpaque = Game::config->GetValue("shader_opaque");
settings.combineTransparent = Setting<int32_t>("combine_transparent", 0) != 0;
settings.optimize.removeHidden = Setting<int32_t>("remove_hidden_faces", 1) != 0;
settings.optimize.groundPlane = Setting<int32_t>("hsr_ground_plane", 0) != 0;
settings.optimize.resolution = Setting<int32_t>("optimize_resolution", 1024);