mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-27 06:57:15 +00:00
feat(ugc): hidden faces removed as LU Toolbox's Remove Hidden Faces decides them
The 42 depth renders only saw faces in direct view, so faces reached only by bounced light (interiors, recesses, rooms seen through openings) were removed. UgcHsr traces the paths LU Toolbox's Cycles bake traces, directly from points on each opaque triangle, and removes a triangle only when none of its paths reaches the sky: - points in rows along the triangle's longest side, hsr_sample_spacing apart (0.1143, 7 x 7 on a stud-sized square), at least hsr_min_points (28, the texels LU Toolbox bakes for a triangle), at most 4096 - hsr_samples (8) paths from each point, at most hsr_bounces (8) bounces, as Cycles 3.1 samples the bake material (Principled BSDF defaults: Burley diffuse and GGX specular, defensive sampling, Filter Glossy, 4 glossy bounces, Russian roulette from the second bounce, ensure_valid_reflection); no direct sky sampling (Cycles doesn't sample a flat world as a light) - hsr_ground_plane: LU Toolbox's black box under LDD's floor - decided per triangle, deterministic per model; triangles without area removed - the VC pre-pass isn't done Checked against LU Toolbox's operator in Blender 3.1.2 on the same meshes (27 models, 937,814 triangles): 501,362 removed there, 501,172 here, differences as large as LU Toolbox's own between seeds. optimize_resolution is retired; remove_hidden_faces=0 makes the same files as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -223,7 +223,7 @@ TEST(UgcRender, RemovesWhatIsInsideAndDrawsIcons) {
|
||||
</Bricks></LXFML>)", error);
|
||||
auto model = UgcModel::Build(parts, library);
|
||||
ASSERT_EQ(model.opaque.TriangleCount(), 24u);
|
||||
const auto result = UgcRender::Optimize(model, UgcRender::OptimizeOptions{ 256, true });
|
||||
const auto result = UgcHsr::RemoveHiddenFaces(model, UgcHsr::Options{});
|
||||
EXPECT_EQ(result.trianglesRemoved, 12u);
|
||||
EXPECT_EQ(model.opaque.TriangleCount(), 12u);
|
||||
EXPECT_EQ(model.opaque.positions.size(), 8u);
|
||||
@@ -233,7 +233,6 @@ TEST(UgcRender, RemovesWhatIsInsideAndDrawsIcons) {
|
||||
EXPECT_EQ(icon.rgba[3], 0); // a corner is background
|
||||
EXPECT_EQ(icon.rgba[(16 * 32 + 16) * 4 + 3], 255); // the middle is the box
|
||||
EXPECT_GT(icon.rgba[(16 * 32 + 16) * 4], icon.rgba[(16 * 32 + 16) * 4 + 1]); // red
|
||||
EXPECT_EQ(UgcRender::SphereDirections().size(), 42u);
|
||||
}
|
||||
|
||||
TEST(UgcFormats, NifReadsBack) {
|
||||
@@ -693,7 +692,6 @@ TEST(UgcThrottle, KeepsUnderTheBudget) {
|
||||
TEST(UgcJobs, MakesLodsStatsAndIcons) {
|
||||
UgcBricks::BrickLibrary library(MakeRes(), 0);
|
||||
UgcJobs::Settings settings;
|
||||
settings.optimize.resolution = 128;
|
||||
settings.ao.samples = 8;
|
||||
settings.icon.size = 32;
|
||||
settings.icon.supersample = 1;
|
||||
@@ -1076,7 +1074,6 @@ namespace {
|
||||
|
||||
UgcJobs::Settings SmallSettings() {
|
||||
UgcJobs::Settings settings;
|
||||
settings.optimize.resolution = 128;
|
||||
settings.ao.samples = 8;
|
||||
settings.icon.size = 32;
|
||||
settings.icon.supersample = 1;
|
||||
@@ -1615,3 +1612,146 @@ TEST(UgcModel, SatinColors) {
|
||||
// Satin's own group is the transparent one: no look
|
||||
EXPECT_TRUE(satin.transparent.looks.empty());
|
||||
}
|
||||
|
||||
TEST(UgcHsr, OffIsByteIdenticalToBefore) {
|
||||
// remove_hidden_faces=0 makes exactly the files made before the path traced hidden faces replaced the renders.
|
||||
// The hashes were taken with GCC on x86-64 Linux.
|
||||
UgcBricks::BrickLibrary library(MakeRes(), 0);
|
||||
auto settings = SmallSettings();
|
||||
settings.hsr.enabled = false;
|
||||
const auto outcome = UgcJobs::ProcessModel(LOOKS_LXFML, library, settings, 7);
|
||||
ASSERT_TRUE(outcome.ok) << outcome.error;
|
||||
const auto other = UgcJobs::ProcessModel(LXFML5, library, settings, 99);
|
||||
ASSERT_TRUE(other.ok) << other.error;
|
||||
#if defined(__linux__) && defined(__x86_64__) && defined(__GNUC__) && !defined(__clang__)
|
||||
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(outcome.files.at("model.nif.gz"))), "f91b88e46a92e9472710854902b25c9a");
|
||||
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(outcome.files.at("model.noao.nif.gz"))), "58a779933695da04bf9b3459c8369528");
|
||||
EXPECT_EQ(UgcFormats::Md5Hex(outcome.files.at("icon.png")), "032ff7df236a636a4c609071d9b46181");
|
||||
EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(other.files.at("model.nif.gz"))), "7eddc020a3e4bf22ed1df0183b042ced");
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A quad a b c d (in order around it) facing `normal`, as two triangles wound to face it
|
||||
void AddQuad(UgcModel::Mesh& mesh, glm::vec3 a, glm::vec3 b, glm::vec3 c, glm::vec3 d, glm::vec3 normal) {
|
||||
const auto base = static_cast<uint32_t>(mesh.positions.size());
|
||||
for (const auto& p : { a, b, c, d }) {
|
||||
mesh.positions.push_back(p);
|
||||
mesh.normals.push_back(normal);
|
||||
mesh.colors.push_back(glm::vec4(1.0f));
|
||||
}
|
||||
if (glm::dot(glm::cross(b - a, c - a), normal) >= 0.0f) mesh.indices.insert(mesh.indices.end(), { base, base + 1, base + 2, base, base + 2, base + 3 });
|
||||
else mesh.indices.insert(mesh.indices.end(), { base, base + 2, base + 1, base, base + 3, base + 2 });
|
||||
}
|
||||
|
||||
// An axis aligned box [min, max], faces outwards (or inwards)
|
||||
void AddBox(UgcModel::Mesh& mesh, glm::vec3 lo, glm::vec3 hi, bool inwards = false) {
|
||||
const float s = inwards ? -1.0f : 1.0f;
|
||||
AddQuad(mesh, { lo.x, lo.y, lo.z }, { lo.x, hi.y, lo.z }, { lo.x, hi.y, hi.z }, { lo.x, lo.y, hi.z }, { -s, 0, 0 });
|
||||
AddQuad(mesh, { hi.x, lo.y, lo.z }, { hi.x, hi.y, lo.z }, { hi.x, hi.y, hi.z }, { hi.x, lo.y, hi.z }, { s, 0, 0 });
|
||||
AddQuad(mesh, { lo.x, lo.y, lo.z }, { hi.x, lo.y, lo.z }, { hi.x, lo.y, hi.z }, { lo.x, lo.y, hi.z }, { 0, -s, 0 });
|
||||
AddQuad(mesh, { lo.x, hi.y, lo.z }, { hi.x, hi.y, lo.z }, { hi.x, hi.y, hi.z }, { lo.x, hi.y, hi.z }, { 0, s, 0 });
|
||||
AddQuad(mesh, { lo.x, lo.y, lo.z }, { hi.x, lo.y, lo.z }, { hi.x, hi.y, lo.z }, { lo.x, hi.y, lo.z }, { 0, 0, -s });
|
||||
AddQuad(mesh, { lo.x, lo.y, hi.z }, { hi.x, lo.y, hi.z }, { hi.x, hi.y, hi.z }, { lo.x, hi.y, hi.z }, { 0, 0, s });
|
||||
}
|
||||
|
||||
// A room [-2, 2]^3 seen from inside (its walls face inwards), with a doorway in its +Z wall unless `closed`, and a
|
||||
// small box in the middle of it (triangles from 0 to 11, the room's after)
|
||||
UgcModel::Mesh Room(bool closed) {
|
||||
UgcModel::Mesh mesh;
|
||||
AddBox(mesh, glm::vec3(-0.3f), glm::vec3(0.3f));
|
||||
const float w = 2.0f;
|
||||
const glm::vec3 in(0, 0, -1);
|
||||
AddQuad(mesh, { -w, -w, -w }, { w, -w, -w }, { w, w, -w }, { -w, w, -w }, { 0, 0, 1 });
|
||||
AddQuad(mesh, { -w, -w, -w }, { -w, w, -w }, { -w, w, w }, { -w, -w, w }, { 1, 0, 0 });
|
||||
AddQuad(mesh, { w, -w, -w }, { w, w, -w }, { w, w, w }, { w, -w, w }, { -1, 0, 0 });
|
||||
AddQuad(mesh, { -w, -w, -w }, { w, -w, -w }, { w, -w, w }, { -w, -w, w }, { 0, 1, 0 });
|
||||
AddQuad(mesh, { -w, w, -w }, { w, w, -w }, { w, w, w }, { -w, w, w }, { 0, -1, 0 });
|
||||
// The +Z wall around a doorway x -0.5..0.5, y -2..0
|
||||
AddQuad(mesh, { -w, -w, w }, { -0.5f, -w, w }, { -0.5f, w, w }, { -w, w, w }, in);
|
||||
AddQuad(mesh, { 0.5f, -w, w }, { w, -w, w }, { w, w, w }, { 0.5f, w, w }, in);
|
||||
AddQuad(mesh, { -0.5f, 0, w }, { 0.5f, 0, w }, { 0.5f, w, w }, { -0.5f, w, w }, in);
|
||||
if (closed) AddQuad(mesh, { -0.5f, -w, w }, { 0.5f, -w, w }, { 0.5f, 0, w }, { -0.5f, 0, w }, in);
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(UgcHsr, KeepsWhatIsSeenThroughADoorwayOrOnlyByBouncedLight) {
|
||||
const auto open = UgcHsr::Visible(Room(false), UgcHsr::Options{});
|
||||
// Every face of the box in the room, the one turned away from the doorway too (only light bounced off the back
|
||||
// wall reaches it), and every wall
|
||||
for (size_t t = 0; t < open.size(); t++) EXPECT_TRUE(open[t]) << t;
|
||||
const auto closed = UgcHsr::Visible(Room(true), UgcHsr::Options{});
|
||||
for (size_t t = 0; t < 12; t++) EXPECT_FALSE(closed[t]) << t;
|
||||
}
|
||||
|
||||
TEST(UgcHsr, IsTheSameEveryTime) {
|
||||
auto options = UgcHsr::Options{};
|
||||
options.seed = 1234;
|
||||
options.samples = 1; // few paths, so chance matters
|
||||
options.bounces = 3;
|
||||
const auto mesh = Room(false);
|
||||
const auto first = UgcHsr::Visible(mesh, options);
|
||||
EXPECT_EQ(UgcHsr::Visible(mesh, options), first);
|
||||
uint64_t points = 0, paths = 0;
|
||||
UgcHsr::Visible(mesh, options, &points, &paths);
|
||||
EXPECT_GT(points, 0u);
|
||||
EXPECT_GT(paths, 0u);
|
||||
}
|
||||
|
||||
TEST(UgcHsr, GroundPlaneHidesTheUnderside) {
|
||||
UgcModel::Mesh mesh;
|
||||
AddBox(mesh, glm::vec3(0.0f), glm::vec3(0.8f, 0.96f, 0.8f)); // a brick on LDD's floor
|
||||
auto options = UgcHsr::Options{};
|
||||
const auto without = UgcHsr::Visible(mesh, options);
|
||||
for (size_t t = 0; t < without.size(); t++) EXPECT_TRUE(without[t]) << t;
|
||||
options.groundPlane = true;
|
||||
const auto with = UgcHsr::Visible(mesh, options);
|
||||
for (size_t t = 0; t < with.size(); t++) EXPECT_EQ(with[t], t != 4 && t != 5) << t; // triangles 4 and 5: the bottom
|
||||
}
|
||||
|
||||
TEST(UgcHsr, RemovesTrianglesWithoutArea) {
|
||||
UgcModel::Mesh mesh;
|
||||
AddBox(mesh, glm::vec3(0.0f), glm::vec3(0.8f));
|
||||
const auto base = static_cast<uint32_t>(mesh.positions.size());
|
||||
for (int i = 0; i < 3; i++) {
|
||||
mesh.positions.push_back(glm::vec3(5.0f));
|
||||
mesh.normals.push_back(glm::vec3(0, 1, 0));
|
||||
}
|
||||
mesh.indices.insert(mesh.indices.end(), { base, base + 1, base + 2 });
|
||||
const auto visible = UgcHsr::Visible(mesh, UgcHsr::Options{});
|
||||
ASSERT_EQ(visible.size(), 13u);
|
||||
for (size_t t = 0; t < 12; t++) EXPECT_TRUE(visible[t]) << t;
|
||||
EXPECT_FALSE(visible[12]);
|
||||
}
|
||||
|
||||
TEST(UgcHsr, SamplePointsFollowTheTrianglesSize) {
|
||||
const auto check = [](const std::vector<glm::vec3>& points) {
|
||||
for (const auto& w : points) {
|
||||
EXPECT_NEAR(w.x + w.y + w.z, 1.0f, 1e-5f);
|
||||
EXPECT_GT(std::min({ w.x, w.y, w.z }), 0.0f);
|
||||
}
|
||||
};
|
||||
// Half a stud-sized square: 7 x 7 points on the square
|
||||
const auto half = UgcHsr::SamplePoints({ 0, 0, 0 }, { 0.8f, 0, 0 }, { 0.8f, 0, 0.8f }, 0.1143f);
|
||||
check(half);
|
||||
EXPECT_EQ(half.size(), 25u);
|
||||
// Four times the area, about four times the points
|
||||
const auto big = UgcHsr::SamplePoints({ 0, 0, 0 }, { 1.6f, 0, 0 }, { 1.6f, 0, 1.6f }, 0.1143f);
|
||||
check(big);
|
||||
EXPECT_EQ(big.size(), 100u);
|
||||
// A tiny triangle: its centre and one towards each corner
|
||||
const auto tiny = UgcHsr::SamplePoints({ 0, 0, 0 }, { 0.01f, 0, 0 }, { 0, 0.01f, 0 }, 0.1143f);
|
||||
check(tiny);
|
||||
EXPECT_EQ(tiny.size(), 4u);
|
||||
// A long sliver: points along its length
|
||||
const auto sliver = UgcHsr::SamplePoints({ 0, 0, 0 }, { 3.2f, 0, 0 }, { 1.6f, 0.01f, 0 }, 0.1143f);
|
||||
check(sliver);
|
||||
EXPECT_EQ(sliver.size(), 14u);
|
||||
// With a minimum (LU Toolbox's 28 texels a triangle) small triangles get their points closer together
|
||||
const auto dense = UgcHsr::SamplePoints({ 0, 0, 0 }, { 0.01f, 0, 0 }, { 0, 0.01f, 0 }, 0.1143f, 28);
|
||||
check(dense);
|
||||
EXPECT_GE(dense.size(), 28u);
|
||||
EXPECT_LE(dense.size(), 40u);
|
||||
EXPECT_EQ(UgcHsr::SamplePoints({ 0, 0, 0 }, { 1.6f, 0, 0 }, { 1.6f, 0, 1.6f }, 0.1143f, 28).size(), 100u); // bigger ones as before
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user