feat(ugc): the fast hidden-face method back, next to LU Toolbox's

UgcHsr::Options::method picks how hidden faces are found: toolbox (LU Toolbox's
paths, the default, unchanged) or fast, the test the UGC server used before
"hidden faces removed as LU Toolbox's Remove Hidden Faces decides them",
restored unchanged as UgcRender::VisibleFromAround: the opaque mesh rendered
from 42 directions around the model (Options::fastResolution pixels square,
1024 as before) and the triangles that show in none removed. It is much
faster, but it also removes faces seen only by bounced light (interiors,
recesses), which LU Toolbox keeps. Nothing sets it yet.

Check: the UGC tests (the fast method removes a box seen only through a
chimney, which the paths keep; its files are the same every time).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 11:11:45 -05:00
parent 57110a7081
commit 00607a20a6
5 changed files with 205 additions and 4 deletions

View File

@@ -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<glm::vec3> SphereDirections() {
const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
const std::vector<glm::vec3> 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<glm::vec3> 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<bool> VisibleFromAround(const UgcModel::Model& model, int resolution, bool groundPlane) {
const auto& opaque = model.opaque;
const size_t triangles = opaque.TriangleCount();
std::vector<bool> 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<size_t>(resolution) * resolution;
std::vector<float> depth(pixels);
std::vector<uint32_t> ids(pixels);
std::vector<glm::vec3> screen(opaque.positions.size());
std::vector<bool> facing(triangles, true);
std::vector<glm::vec3> 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<uint32_t>(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<size_t>(y) * resolution + x];
if (z < stored) {
stored = z;
ids[static_cast<size_t>(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<int>(centre.x), y = static_cast<int>(centre.y);
if (x < 0 || y < 0 || x >= resolution || y >= resolution || centre.z <= depth[static_cast<size_t>(y) * resolution + x] + bias) visible[t] = true;
}
}
return visible;
}
std::vector<float> AmbientOcclusion(const UgcModel::Mesh& mesh, const UgcModel::Mesh& occluders, float distance, int samples, UgcRays::eBackend rays) {
std::vector<float> ao(mesh.positions.size(), 1.0f);
if (occluders.Empty() || samples <= 0 || distance <= 0.0f || mesh.normals.size() != mesh.positions.size()) return ao;