mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-21 12:12:48 +00:00
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:
@@ -154,7 +154,7 @@ namespace UgcJobs {
|
|||||||
entry["opaqueAfter"] = model.opaque.TriangleCount();
|
entry["opaqueAfter"] = model.opaque.TriangleCount();
|
||||||
entry["vertices"] = model.opaque.positions.size() + model.transparent.positions.size();
|
entry["vertices"] = model.opaque.positions.size() + model.transparent.positions.size();
|
||||||
opaquePieces.push_back(UgcModel::Divide(model.opaque));
|
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();
|
entry["shapes"] = opaquePieces.back().size() + transparentPieces.back().size();
|
||||||
lodStats.push_back(entry);
|
lodStats.push_back(entry);
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ namespace UgcJobs {
|
|||||||
AddDownload(outcome.files, "model.nif", nif);
|
AddDownload(outcome.files, "model.nif", nif);
|
||||||
AddDownload(outcome.files, "model.lxfml", lxfml);
|
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));
|
outcome.files["model.noao.nif"] = UgcFormats::WriteLodNif("SceneNode_Model", groups(1, opaque, transparent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
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;
|
UgcModel::LodDistances lodDistances;
|
||||||
std::string shaderOpaque{ "01" }; // S<shader>_Opaque_...; transparent shapes are always S01
|
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::OptimizeOptions optimize; // hidden surface removal
|
||||||
UgcRender::AoOptions ao; // Bake Lighting (AO Only)
|
UgcRender::AoOptions ao; // Bake Lighting (AO Only)
|
||||||
UgcRender::IconOptions icon;
|
UgcRender::IconOptions icon;
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ namespace UgcModel {
|
|||||||
transparent = transparent && (luToolbox ? UgcPalette::IsTransparent(materialOf(index)) : library.GetMaterial(id).Transparent());
|
transparent = transparent && (luToolbox ? UgcPalette::IsTransparent(materialOf(index)) : library.GetMaterial(id).Transparent());
|
||||||
}
|
}
|
||||||
auto& mesh = transparent ? model.transparent : model.opaque;
|
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)));
|
const glm::mat3 normalMatrix = glm::transpose(glm::inverse(glm::mat3(part.transform)));
|
||||||
for (size_t index = 0; index < design->size(); index++) {
|
for (size_t index = 0; index < design->size(); index++) {
|
||||||
const auto& geometry = (*design)[index];
|
const auto& geometry = (*design)[index];
|
||||||
@@ -317,6 +318,30 @@ namespace UgcModel {
|
|||||||
return model;
|
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) {
|
void KeepTriangles(Mesh& mesh, const std::vector<bool>& keep) {
|
||||||
Mesh kept;
|
Mesh kept;
|
||||||
std::vector<uint32_t> remap(mesh.positions.size(), UINT32_MAX);
|
std::vector<uint32_t> remap(mesh.positions.size(), UINT32_MAX);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ namespace UgcModel {
|
|||||||
Mesh opaque;
|
Mesh opaque;
|
||||||
Mesh transparent;
|
Mesh transparent;
|
||||||
std::vector<uint32_t> missingDesigns; // designs without geometry in the client, skipped
|
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{};
|
size_t bricks{};
|
||||||
|
|
||||||
bool Empty() const { return opaque.Empty() && transparent.Empty(); }
|
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)
|
// A client .nif's meshes as one model (vertex colors times material color; transparent when blended)
|
||||||
Model FromNif(const NifFile::Model& nif);
|
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)
|
// Keeps the triangles whose flag is set (and the vertices they use)
|
||||||
void KeepTriangles(Mesh& mesh, const std::vector<bool>& keep);
|
void KeepTriangles(Mesh& mesh, const std::vector<bool>& keep);
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ namespace {
|
|||||||
settings.lodDistances.lod3 = Setting<float>("lod_distance_3", 280.0f);
|
settings.lodDistances.lod3 = Setting<float>("lod_distance_3", 280.0f);
|
||||||
settings.lodDistances.cull = Setting<float>("lod_cull", 10000.0f);
|
settings.lodDistances.cull = Setting<float>("lod_cull", 10000.0f);
|
||||||
if (!Game::config->GetValue("shader_opaque").empty()) settings.shaderOpaque = Game::config->GetValue("shader_opaque");
|
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.removeHidden = Setting<int32_t>("remove_hidden_faces", 1) != 0;
|
||||||
settings.optimize.groundPlane = Setting<int32_t>("hsr_ground_plane", 0) != 0;
|
settings.optimize.groundPlane = Setting<int32_t>("hsr_ground_plane", 0) != 0;
|
||||||
settings.optimize.resolution = Setting<int32_t>("optimize_resolution", 1024);
|
settings.optimize.resolution = Setting<int32_t>("optimize_resolution", 1024);
|
||||||
|
|||||||
@@ -59,8 +59,10 @@ lod_distance_2=100
|
|||||||
lod_distance_3=280
|
lod_distance_3=280
|
||||||
lod_cull=10000
|
lod_cull=10000
|
||||||
|
|
||||||
# The shapes' shader number: S<shader>_Opaque_Model (transparent shapes are S01_Alpha_Model)
|
# The shapes' shader number: S<shader>_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
|
shader_opaque=01
|
||||||
|
combine_transparent=0
|
||||||
|
|
||||||
# Remove faces that can't be seen from anywhere (optimize_resolution: detail of the renders that decide it;
|
# 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)
|
# hsr_ground_plane: 1 also removes what can only be seen from below the model)
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ TEST(UgcModel, ColorsLikeLuToolbox) {
|
|||||||
plain.colorVariation = 0.0f;
|
plain.colorVariation = 0.0f;
|
||||||
const auto flat = UgcModel::Build(parts, library, plain);
|
const auto flat = UgcModel::Build(parts, library, plain);
|
||||||
EXPECT_EQ(flat.transparent.TriangleCount(), 12u); // only the all-transparent brick
|
EXPECT_EQ(flat.transparent.TriangleCount(), 12u); // only the all-transparent brick
|
||||||
|
EXPECT_EQ(flat.transparentBricks, std::vector<size_t>{ 0 });
|
||||||
EXPECT_EQ(flat.opaque.TriangleCount(), 60u);
|
EXPECT_EQ(flat.opaque.TriangleCount(), 60u);
|
||||||
EXPECT_NEAR(flat.opaque.colors[0].r * 255.0f, 222.0f, 0.5f);
|
EXPECT_NEAR(flat.opaque.colors[0].r * 255.0f, 222.0f, 0.5f);
|
||||||
EXPECT_FLOAT_EQ(flat.opaque.colors[0].a, 1.0f);
|
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);
|
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"(<LXFML versionMajor="5"><Bricks>
|
||||||
|
<Brick><Part designID="3001" materials="40"><Bone transformation="1,0,0,0,1,0,0,0,1,0,0,0"/></Part></Brick>
|
||||||
|
<Brick><Part designID="3001" materials="43"><Bone transformation="1,0,0,0,1,0,0,0,1,5,0,0"/></Part></Brick>
|
||||||
|
</Bricks></LXFML>)", 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) {
|
TEST(UgcFormats, LodNifReadsBack) {
|
||||||
UgcModel::Mesh near, far;
|
UgcModel::Mesh near, far;
|
||||||
near.positions = { { 0, 0, 0 }, { 1, 0, 0 }, { 0, 1, 0 }, { 1, 1, 0 } };
|
near.positions = { { 0, 0, 0 }, { 1, 0, 0 }, { 0, 1, 0 }, { 1, 1, 0 } };
|
||||||
|
|||||||
Reference in New Issue
Block a user