diff --git a/dUgcServer/Render/UgcHsr.cpp b/dUgcServer/Render/UgcHsr.cpp index 84c3d2620..88373c5ad 100644 --- a/dUgcServer/Render/UgcHsr.cpp +++ b/dUgcServer/Render/UgcHsr.cpp @@ -5,6 +5,7 @@ #include #include #include "UgcRays.h" +#include "UgcRender.h" #include "UgcThrottle.h" namespace { @@ -407,6 +408,17 @@ namespace { } namespace UgcHsr { + std::string_view Name(eMethod method) { + return method == eMethod::FAST ? "fast" : "toolbox"; + } + + std::optional Parse(std::string_view name) { + for (const auto method : { eMethod::TOOLBOX, eMethod::FAST }) { + if (Name(method) == name) return method; + } + return std::nullopt; + } + std::vector SamplePoints(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c, float spacing, size_t minimum) { // Too small to lay out: the centre and one point towards each corner (the centres of its four halved-side // triangles) @@ -501,7 +513,8 @@ namespace UgcHsr { auto& opaque = model.opaque; result.trianglesBefore = opaque.TriangleCount() + model.transparent.TriangleCount(); if (opaque.Empty() || !options.enabled) return result; - result.kept = Visible(opaque, options, &result.points, &result.paths); + result.kept = options.method == eMethod::FAST ? UgcRender::VisibleFromAround(model, options.fastResolution, options.groundPlane) : + Visible(opaque, options, &result.points, &result.paths); for (const bool kept : result.kept) result.trianglesRemoved += kept ? 0 : 1; UgcModel::KeepTriangles(opaque, result.kept); return result; diff --git a/dUgcServer/Render/UgcHsr.h b/dUgcServer/Render/UgcHsr.h index a29d9e448..646507f37 100644 --- a/dUgcServer/Render/UgcHsr.h +++ b/dUgcServer/Render/UgcHsr.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -14,10 +16,21 @@ * points on each opaque triangle under a sky of overwhelming brightness, bouncing off the model; a triangle none of * whose paths reaches the sky is removed. So faces seen only through openings, or lit only by light bounced in * (interiors, recesses), stay; faces sealed inside go. docs/UgcServer.md ("Hidden faces") has the details. + * The fast method (hsr_method=fast) is the test the UGC server used before: renders from 42 directions around the + * model (UgcRender::VisibleFromAround), which removes faces seen only by bounced light too. */ namespace UgcHsr { + // How hidden faces are found (hsr_method): LU Toolbox's paths, or the renders from around the model + enum class eMethod : uint8_t { TOOLBOX = 0, FAST }; + + // The setting's name of a method (toolbox, fast) + std::string_view Name(eMethod method); + // A method by its name; nullopt for anything else + std::optional Parse(std::string_view name); + struct Options { bool enabled{ true }; // remove_hidden_faces + eMethod method{}; // hsr_method bool groundPlane{ false }; // hsr_ground_plane: LU Toolbox's black box under the model (y 0 down to -100) int samples{ 8 }; // hsr_samples: paths traced from each point (LU Toolbox's Samples) int bounces{ 8 }; // hsr_bounces: bounces a path may take (the Cycles bake's Max Bounces) @@ -25,14 +38,15 @@ namespace UgcHsr { int minPoints{ 28 }; // hsr_min_points: points on a triangle at least (LU Toolbox bakes 28 texels a triangle) uint64_t seed{}; // of the paths' random numbers (the same seed gives the same result) UgcRays::eBackend rays{}; // what traces the paths' rays (ugc_ray_backend) + int fastResolution{ 1024 }; // hsr_fast_resolution: pixels square of each of the fast method's renders }; struct Result { size_t trianglesBefore{}; // opaque and transparent size_t trianglesRemoved{}; std::vector kept; // per opaque triangle before: whether it stayed - uint64_t points{}; // sample points traced from - uint64_t paths{}; // paths traced + uint64_t points{}; // sample points traced from (toolbox) + uint64_t paths{}; // paths traced (toolbox) }; /** @@ -41,7 +55,8 @@ namespace UgcHsr { */ std::vector Visible(const UgcModel::Mesh& mesh, const Options& options, uint64_t* points = nullptr, uint64_t* paths = nullptr); - // Removes the opaque triangles that aren't Visible (nothing when options.enabled is off); transparent ones stay + // Removes the opaque triangles that aren't Visible, or with the fast method don't show in its renders (nothing when + // options.enabled is off); transparent ones stay Result RemoveHiddenFaces(UgcModel::Model& model, const Options& options); /** diff --git a/dUgcServer/Render/UgcRender.cpp b/dUgcServer/Render/UgcRender.cpp index 38a06901f..cb1c20792 100644 --- a/dUgcServer/Render/UgcRender.cpp +++ b/dUgcServer/Render/UgcRender.cpp @@ -80,9 +80,110 @@ namespace { float PixelSize() const { return 2.0f * radius / resolution; } }; + + void Bounds(const UgcModel::Model& model, glm::vec3& center, float& radius) { + glm::vec3 min{}, max{}; + if (!model.Bounds(min, max)) { + center = glm::vec3(0.0f); + radius = 1.0f; + return; + } + center = (min + max) * 0.5f; + radius = std::max(glm::length(max - min) * 0.5f, 0.01f); + } } namespace UgcRender { + std::vector SphereDirections() { + const float t = (1.0f + std::sqrt(5.0f)) / 2.0f; + const std::vector corners = { + { -1, t, 0 }, { 1, t, 0 }, { -1, -t, 0 }, { 1, -t, 0 }, + { 0, -1, t }, { 0, 1, t }, { 0, -1, -t }, { 0, 1, -t }, + { t, 0, -1 }, { t, 0, 1 }, { -t, 0, -1 }, { -t, 0, 1 }, + }; + std::vector directions; + for (const auto& corner : corners) directions.push_back(glm::normalize(corner)); + // Edge centres: the pairs of corners that are neighbours (the shortest distance apart) + const float edge = glm::length(corners[0] - corners[1]); + for (size_t i = 0; i < corners.size(); i++) { + for (size_t j = i + 1; j < corners.size(); j++) { + if (std::abs(glm::length(corners[i] - corners[j]) - edge) < 1e-3f) directions.push_back(glm::normalize(corners[i] + corners[j])); + } + } + return directions; + } + + std::vector VisibleFromAround(const UgcModel::Model& model, int resolution, bool groundPlane) { + const auto& opaque = model.opaque; + const size_t triangles = opaque.TriangleCount(); + std::vector visible(triangles, false); + if (opaque.Empty()) return visible; + + glm::vec3 center{}; + float radius{}; + Bounds(model, center, radius); + radius *= 1.02f; + resolution = std::clamp(resolution, 64, 4096); + const size_t pixels = static_cast(resolution) * resolution; + std::vector depth(pixels); + std::vector ids(pixels); + std::vector screen(opaque.positions.size()); + + std::vector facing(triangles, true); + std::vector faceNormals(triangles, glm::vec3(0.0f)); + for (size_t t = 0; t < triangles; t++) { + if (opaque.normals.size() != opaque.positions.size()) break; + faceNormals[t] = glm::normalize(opaque.normals[opaque.indices[t * 3]] + opaque.normals[opaque.indices[t * 3 + 1]] + opaque.normals[opaque.indices[t * 3 + 2]] + glm::vec3(1e-6f)); + } + // Without normals nothing is culled + if (opaque.normals.size() != opaque.positions.size()) std::fill(faceNormals.begin(), faceNormals.end(), glm::vec3(0.0f)); + + for (const auto& direction : SphereDirections()) { + // A ground plane under the model hides everything from below + if (groundPlane && direction.y < -0.05f) continue; + UgcThrottle::Checkpoint(); + const OrthoView view(center, radius, direction, resolution); + const float bias = view.PixelSize(); + std::fill(depth.begin(), depth.end(), INF); + std::fill(ids.begin(), ids.end(), 0); + for (size_t v = 0; v < opaque.positions.size(); v++) screen[v] = view.Project(opaque.positions[v]); + // Faces turned away can't be seen from here (they're seen from the directions they face); their vertex + // normals say which way they face, which doesn't depend on the files' winding + for (size_t t = 0; t < triangles; t++) facing[t] = glm::dot(faceNormals[t], direction) > -0.1f; + for (size_t t = 0; t < triangles; t++) { + if (!facing[t]) continue; + if ((t & 0x3FFF) == 0) UgcThrottle::Checkpoint(); + const auto id = static_cast(t + 1); + Rasterize(resolution, resolution, screen[opaque.indices[t * 3]], screen[opaque.indices[t * 3 + 1]], screen[opaque.indices[t * 3 + 2]], + [&](int x, int y, float z, float, float, float) { + auto& stored = depth[static_cast(y) * resolution + x]; + if (z < stored) { + stored = z; + ids[static_cast(y) * resolution + x] = id; + } + }); + } + for (const auto id : ids) { + if (id != 0) visible[id - 1] = true; + } + + // Triangles too small or thin to cover a pixel centre: kept when their centre isn't behind what was drawn. + // Bigger ones that show would have covered one. + const float smallArea = 2.0f; // pixels + for (size_t t = 0; t < triangles; t++) { + if (visible[t] || !facing[t]) continue; + const auto& a = screen[opaque.indices[t * 3]]; + const auto& b = screen[opaque.indices[t * 3 + 1]]; + const auto& c = screen[opaque.indices[t * 3 + 2]]; + if (std::abs(Edge(a, b, c.x, c.y)) * 0.5f > smallArea) continue; + const auto centre = (a + b + c) / 3.0f; + const int x = static_cast(centre.x), y = static_cast(centre.y); + if (x < 0 || y < 0 || x >= resolution || y >= resolution || centre.z <= depth[static_cast(y) * resolution + x] + bias) visible[t] = true; + } + } + return visible; + } + std::vector AmbientOcclusion(const UgcModel::Mesh& mesh, const UgcModel::Mesh& occluders, float distance, int samples, UgcRays::eBackend rays) { std::vector ao(mesh.positions.size(), 1.0f); if (occluders.Empty() || samples <= 0 || distance <= 0.0f || mesh.normals.size() != mesh.positions.size()) return ao; diff --git a/dUgcServer/Render/UgcRender.h b/dUgcServer/Render/UgcRender.h index 6e6e66f73..fc730a5e3 100644 --- a/dUgcServer/Render/UgcRender.h +++ b/dUgcServer/Render/UgcRender.h @@ -71,6 +71,17 @@ namespace UgcRender { // transparent) get the glitter texture's white flecks over their color before the light (LEGO-AnimUV), still. Image RenderIcon(const UgcModel::Model& model, const IconOptions& options, const std::vector* opaqueAo = nullptr); + /** + * The fast hidden-face test (hsr_method=fast, what the UGC server did before it traced LU Toolbox's paths): the + * opaque mesh rendered from 42 directions around the whole model (SphereDirections), `resolution` pixels square; + * per opaque triangle whether it shows in any of them (with a conservative test, so small visible ones stay). + * Faces seen only by bounced light (interiors, recesses) don't show. `groundPlane`: nothing is seen from below. + */ + std::vector VisibleFromAround(const UgcModel::Model& model, int resolution, bool groundPlane); + + // The 42 directions VisibleFromAround renders from (an icosahedron's corners and edge centres), unit length + std::vector SphereDirections(); + /** * Ambient occlusion of each vertex of `mesh`: the share of `samples` rays (cosine weighted around the vertex * normal, the same pattern every time) that leave without hitting a triangle of `occluders` within `distance`. diff --git a/tests/dUgcTests/UgcTests.cpp b/tests/dUgcTests/UgcTests.cpp index 44a5f168f..1d76406b6 100644 --- a/tests/dUgcTests/UgcTests.cpp +++ b/tests/dUgcTests/UgcTests.cpp @@ -2002,6 +2002,67 @@ namespace { } } +TEST(UgcHsr, FastMethodRendersFromAround) { + EXPECT_EQ(UgcHsr::Parse("fast"), UgcHsr::eMethod::FAST); + EXPECT_EQ(UgcHsr::Parse("toolbox"), UgcHsr::eMethod::TOOLBOX); + EXPECT_FALSE(UgcHsr::Parse("slow")); + EXPECT_EQ(UgcRender::SphereDirections().size(), 42u); + + // A small box inside the big one: its faces can't be seen + UgcBricks::BrickLibrary library(MakeRes(), 0); + std::string error; + const auto parts = UgcModel::ParseLxfml(R"( + + + )", error); + auto model = UgcModel::Build(parts, library); + UgcHsr::Options options; + options.method = UgcHsr::eMethod::FAST; + options.fastResolution = 256; + const auto result = UgcHsr::RemoveHiddenFaces(model, options); + EXPECT_EQ(result.trianglesRemoved, 12u); + EXPECT_EQ(model.opaque.TriangleCount(), 12u); + EXPECT_EQ(result.paths, 0u); + + // A small box in a closed chamber whose only opening is a narrow chimney at the other end: nothing outside sees it + // straight, so the fast method removes it; light bounced in through the chimney reaches some of it, so the toolbox + // method keeps that + UgcModel::Model chamber; + auto& mesh = chamber.opaque; + AddBox(mesh, glm::vec3(-0.9f, 0.0f, -0.15f), glm::vec3(-0.6f, 0.2f, 0.15f)); // the small box + AddBox(mesh, glm::vec3(-1.2f, -0.2f, -1.2f), glm::vec3(1.2f, 0.0f, 1.2f)); // floor + AddBox(mesh, glm::vec3(-1.2f, 0.0f, -1.2f), glm::vec3(-1.0f, 0.5f, 1.2f)); // walls + AddBox(mesh, glm::vec3(1.0f, 0.0f, -1.2f), glm::vec3(1.2f, 0.5f, 1.2f)); + AddBox(mesh, glm::vec3(-1.0f, 0.0f, -1.2f), glm::vec3(1.0f, 0.5f, -1.0f)); + AddBox(mesh, glm::vec3(-1.0f, 0.0f, 1.0f), glm::vec3(1.0f, 0.5f, 1.2f)); + AddBox(mesh, glm::vec3(-1.2f, 0.5f, -1.2f), glm::vec3(0.6f, 0.7f, 1.2f)); // roof around the chimney's hole + AddBox(mesh, glm::vec3(0.9f, 0.5f, -1.2f), glm::vec3(1.2f, 0.7f, 1.2f)); + AddBox(mesh, glm::vec3(0.6f, 0.5f, -1.2f), glm::vec3(0.9f, 0.7f, -0.15f)); + AddBox(mesh, glm::vec3(0.6f, 0.5f, 0.15f), glm::vec3(0.9f, 0.7f, 1.2f)); + AddBox(mesh, glm::vec3(0.5f, 0.7f, -0.25f), glm::vec3(0.6f, 2.2f, 0.25f)); // the chimney + AddBox(mesh, glm::vec3(0.9f, 0.7f, -0.25f), glm::vec3(1.0f, 2.2f, 0.25f)); + AddBox(mesh, glm::vec3(0.6f, 0.7f, -0.25f), glm::vec3(0.9f, 2.2f, -0.15f)); + AddBox(mesh, glm::vec3(0.6f, 0.7f, 0.15f), glm::vec3(0.9f, 2.2f, 0.25f)); + const auto seen = UgcRender::VisibleFromAround(chamber, 512, false); + ASSERT_EQ(seen.size(), mesh.TriangleCount()); + for (size_t t = 0; t < 12; t++) EXPECT_FALSE(seen[t]) << t; + UgcHsr::Options toolbox; + toolbox.samples = 32; + const auto traced = UgcHsr::Visible(mesh, toolbox); + EXPECT_TRUE(std::any_of(traced.begin(), traced.begin() + 12, [](bool kept) { return kept; })); + + // Its files are the same every time + auto settings = SmallSettings(); + settings.hsr.method = UgcHsr::eMethod::FAST; + settings.hsr.fastResolution = 128; + const auto first = UgcJobs::ProcessModel(LXFML5, library, settings, 7); + ASSERT_TRUE(first.ok) << first.error; + EXPECT_EQ(first.files.at("model.nif.checksum"), UgcJobs::ProcessModel(LXFML5, library, settings, 7).files.at("model.nif.checksum")); +#if defined(__linux__) && defined(__x86_64__) && defined(__GNUC__) && !defined(__clang__) + EXPECT_EQ(UgcFormats::Md5Hex(*ZCompression::Gunzip(first.files.at("model.nif.gz"))), "4bd664412c88333431842eb82a0abfac"); +#endif +} + TEST(UgcRays, NamesAndFallback) { for (const auto backend : { UgcRays::eBackend::BUILTIN, UgcRays::eBackend::EMBREE, UgcRays::eBackend::HIPRT }) { EXPECT_EQ(UgcRays::Parse(UgcRays::Name(backend)), backend);