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:
Aaron Kimbrell
2026-09-28 17:33:47 -05:00
parent 261fc60558
commit bea6b17b25
12 changed files with 1012 additions and 158 deletions

View File

@@ -135,9 +135,10 @@ and the notices (see Threads). Cars and rockets are made once per combination of
0.025) either way, times the color's own amount (black 0.4, orange 1.5, ...), clamped and taken back; hue and
saturation stay. The random number comes from the model's id, the brick's index and the material, so making a
model again gives the same colors, and every LOD the same (LU Toolbox restarts its random sequence per LOD).
4. Faces nobody can see are removed from the opaque bricks (they're rendered from 42 directions and triangles that
never show are dropped; transparent bricks hide nothing and aren't touched). `hsr_ground_plane=1` also drops what
can only be seen from below.
4. Faces nobody can see are removed from the opaque bricks as LU Toolbox's Remove Hidden Faces decides it: paths are
traced from points on each triangle, bouncing off the model, and a triangle none of whose paths reaches the sky is
removed (see "Hidden faces" below). Transparent bricks hide nothing and aren't touched. `hsr_ground_plane=1` also
drops what can only be seen from below.
5. Ambient occlusion is baked like LU Toolbox's Bake Lighting with AO Only: 64 rays per vertex (`ao_samples`) over
the hemisphere around the vertex normal (cosine weighted, the same pattern every time, so a model made again comes
out the same), each blocked when it hits an opaque triangle within 5 (`ao_distance`); the vertex's occlusion is the
@@ -209,7 +210,7 @@ and the notices (see Threads). Cars and rockets are made once per combination of
| Transparent Opacity 58.82% | Same (`transparent_opacity`) |
| Vertex color layers Col, Lit, Alpha (1), Glow | One color set: Col times Lit (glow added to Lit); alpha is the opacity |
| Setup Bake Material (VertexColor / VertexColorTransparent) | Equivalent: white NiMaterialProperty, vertex colors as ambient and diffuse, NiAlphaProperty on transparent shapes |
| Remove Hidden Faces: Cycles bakes with an overexposed world (VC pre-pass 32 samples, tris to quads, 5 pixels between vertices, 8 samples, threshold 0.01), autoremove, transparent bricks hidden | Same result by other means: depth renders from 42 directions (`optimize_resolution`), transparent bricks hidden and untouched; the pre-pass, quads and samples are details of Blender's baking |
| Remove Hidden Faces: Cycles bakes with an overexposed world (VC pre-pass 32 samples, tris to quads, 5 pixels between vertices, 8 samples, threshold 0.01), autoremove, transparent bricks hidden | The same paths traced directly from points on the triangles, no texture (`hsr_samples` 8, `hsr_bounces` 8, `hsr_min_points` 28, `hsr_sample_spacing`); decided per triangle, not per joined quad; no VC pre-pass (see "Hidden faces") |
| Use Ground Plane off | Same (`hsr_ground_plane=0`) |
| Split objects over 65536 vertices (divide_mesh, along the longest side, linked parts together) | Same, also keeping each shape under 65535 triangles (the format's limit) |
| Setup LOD data: SceneNode, NiLODNode per shape name, LOD nodes, near/far by the levels there are, `S01_Opaque_`/`S01_Alpha_` names cut at 60 | Same (`lod_distance_0..3`, `lod_cull`, `shader_opaque`); LU Toolbox's glow, metal and superemissive shader settings are unused by it too; the UGC server's own metal and glow groups are opt in (see below) |
@@ -222,6 +223,56 @@ and the notices (see Threads). Cars and rockets are made once per combination of
| Icon scene BrickBuild / Car: 50 mm lens, camera 53.4 / 19.5 degrees, sun at 21 / 50.3, 128 px, framing 1.03, transparent film | Same framing (`icon_*`); the light is brighter, to match the game's icons |
| Icon scene Rocket: 35 mm lens, other angles, two suns | Not built in; a preset for the rocket build type can be set in the icon editor |
### Hidden faces
`UgcHsr::RemoveHiddenFaces` (`remove_hidden_faces=1`, default) traces the paths LU Toolbox's Remove Hidden Faces
bakes, directly from points on the triangles, without its UV layout and texture. Each LOD's opaque mesh is done on its
own, on the job's worker thread.
* What blocks: the LOD's opaque triangles (LU Toolbox hides the transparent bricks while it bakes). With
`hsr_ground_plane=1`, LU Toolbox's ground plane: a black box 1000 x 1000 x 100 whose top is at LDD y 0; a path that
hits it ends.
* Points: on each triangle, rows parallel to its longest side `hsr_sample_spacing` apart (default 0.1143 LDD units, 7
x 7 points on a stud-sized square), with points the same distance apart along each row. A triangle that gets fewer
than `hsr_min_points` (default 28, the texels LU Toolbox bakes for every triangle) gets its rows and points closer
together until it has that many; at most 4096 (bigger triangles get them further apart). Triangles without area
draw nothing and are removed (LU Toolbox's bake leaves them dark too).
* Paths: `hsr_samples` (default 8, LU Toolbox's Samples) from each point, each as Cycles 3.1 traces LU Toolbox's
diffuse bake: the point is looked at along its vertex normal (the side it faces is baked; a triangle wound the other
way is baked from its back, as Cycles does); the bake material (a new material's Principled BSDF: base color 0.8,
specular 0.5, roughness 0.5, GGX) picks its diffuse part (Burley) or its specular part (GGX, Fresnel of IOR 1.5) by
Cycles' sample weights (at the first point each weight is raised to an eighth of their sum; the specular part is
blurred by Filter Glossy 1 after the first bounce) and the path bounces in the direction drawn. A bounce ray that
hits nothing has reached the sky. A triangle hit from behind is lit on its back. At most `hsr_bounces` (default 8, the bake's Max Bounces) bounces, 4 of them
glossy; from the second bounce on the path goes on with probability sqrt(throughput) (Russian roulette). The sky
isn't sampled directly: Cycles doesn't sample a world of one flat color as a light, so only rays leaving the model
count.
* Decision: a triangle is kept as soon as one of its paths reaches the sky, removed when none does. LU Toolbox's
threshold (0.01 of the baked average) means the same: its world's strength 100000 saturates every texel a path
reaches.
* The same every time: each path's random numbers come from the model's id, the triangle, the point and the sample.
* Differences from LU Toolbox: triangles are decided on their own. LU Toolbox joins triangles into quads first (Tris
to Quads) and keeps both halves of a quad when either is seen, so it keeps some triangles this removes. Big triangles
get more points than LU Toolbox's fixed 7 x 7 texels. The VC pre-pass isn't done (below).
* Cost: the paths from triangles that are seen stop at the first that leaves, so the time goes to the hidden ones
(`hsr_min_points` x `hsr_samples` paths each at least). `hsr_min_points` and `hsr_samples` trade time for how
reliably faces only just seen are kept. The long loop calls UgcThrottle's checkpoint. Measured (one thread, CPU
time, LOD 0): 6 s for 69 bricks, 74 to 86 s for 250 to 330 bricks, 129 s for 883 bricks (the 42 depth renders used
before took 1 to 4 s; LU Toolbox 3, 24 to 29 and 91 s on 8 threads).
Checked against LU Toolbox's operator in Blender 3.1.2 on the same meshes (VC pre-pass off, Tris to Quads off so it
also decides per triangle; 27 models: a closed box, a house with a doorway, a room with a window, a tube, a cup and
stacked plates built for it, 5 test LXFMLs and 16 player models of 1 to 883 bricks, 937,814 triangles): it removed
501,362 triangles and the UGC server 501,172, with 9,370 only there and 9,180 only here, per model as far apart as
LU Toolbox is from itself with another random seed. With the ground plane the same (5 models). The depth renders used
before removed 191,900 triangles LU Toolbox keeps (faces reached only by bounced light).
LU Toolbox's VC pre-pass (on by default there, not done here): before the texture bake it bakes vertex colors (32
samples, 12 bounces) and leaves out of the texture bake every face with a corner above the threshold. Those faces are
kept without the texture bake's test, so with it LU Toolbox keeps somewhat more (for example 1865 instead of 2529
triangles removed from a 61-brick model); it saves LU Toolbox baking texels for faces that are seen, which the paths
here already stop early for.
### Metal and glow (on by default, not how live looked)
Live's models, LU Toolbox's exports and the client's own builder (`LUNifBuilder_BK`, which writes only `S01_Opaque`
@@ -574,7 +625,7 @@ of old items' `.sd0` icons and checksums and of builds' combination ids runs on
Settings in `ugcconfig.ini` (and the dashboard's settings page), picked up while running when the config is reloaded:
* `worker_threads` (restart): how many models are made at once.
* `max_cpu_percent`: the workers together average at most this share of all CPU cores. The long loops (the renders
* `max_cpu_percent`: the workers together average at most this share of all CPU cores. The long loops (the paths
for hidden faces, the occlusion rays, icons) account each thread's CPU time every few milliseconds against a budget
that fills at that rate and sleep while it's overdrawn (UgcThrottle), so it holds for long jobs too and whatever
`worker_threads` is. Short bursts (a quarter second) aren't slowed. 0: no limit.