Fix geometry missing faces: backface cone cull math + LOD parent suppression#44
Open
Fix geometry missing faces: backface cone cull math + LOD parent suppression#44
Conversation
Complete implementation of a GPU-driven virtualized geometry renderer inspired by modern cluster-based LOD systems. Features: - Offline mesh clusterization (~64 tri clusters with bounding spheres, normal cones, and geometric error metrics) - Hierarchical cluster DAG for continuous LOD selection - Software page residency manager for geometry streaming - GPU compute passes: instance culling, cluster LOD selection - Vertex pulling render pipeline with storage buffer reads - WGSL shaders for all passes including HZB generation - Interactive viewer at /pebble with orbit camera, LOD controls, wireframe overlay, debug cluster coloring, and real-time stats - 6 test scenes: sphere, HD sphere, terrain, torus, multi-object, massive - All magic constants extracted to constants.ts - 116 tests across 9 test files (vitest) https://claude.ai/code/session_01958Gbh26NWntseNBigbw1k
The flashing was caused by a mismatch between the compute pass and the render pass. The compute pass wrote visible cluster IDs into a compacted list (visibleClusters[0]=5, [1]=12, ...) but the draw loop passed raw cluster indices as instanceIndex, so the vertex shader read visibleClusters[ci] which was stale/garbage for most indices. Fix: replace the compacted visible cluster list with a per-cluster visibility flag buffer. The compute pass writes 0 or 1 at clusterVisibility[clusterIdx]. The vertex shader reads the flag and emits degenerate triangles (behind far plane) for hidden clusters. The draw loop passes ci directly as instanceIndex = cluster ID. Also fixes the backface cone culling logic which was a no-op. https://claude.ai/code/session_01958Gbh26NWntseNBigbw1k
… demo scenes Bug 1 — Backface cone cull (shaders.ts): the cull condition `d > coneCos` was wrong. For a cone with half-angle α, a cluster is fully backfacing when dot(viewDir, coneDir) > sin(α) = sqrt(1 - coneCos²). Using coneCos as the threshold (instead of sin) aggressively culled front-facing clusters whenever coneCos < 0.707, producing large missing-face holes on every curved surface. Bug 2 — LOD parent check (shaders.ts): parent screen error was projected using the parent bounding-sphere center distance. Because the parent sphere encloses multiple children it sits farther away, making its projected error appear smaller and incorrectly suppressing the child cluster. Fixed by reusing the child cluster's already-computed `dist` for both self and parent projections. Tests: shaders.test.ts now asserts the correct formula and absence of parentDist. New demo scenes (mesh-generator.ts + PebbleViewer/ControlsPanel): - Trefoil Knot (16K tris) — high-curvature surface, wide normal variation - LOD Field — 61 torus instances in expanding rings; LOD transitions visible on zoom - Landscape + Spheres — large terrain with 49 scattered sphere instances - Ocean — 130K-tri Gerstner wave surface - Forest — 200 procedural conifer tree instances over terrain (800K+ tris) - Mountains — fractal-layered peaks with radial envelope (700K+ tris) https://claude.ai/code/session_013JrLspqeGiVV78d9yy3LdU
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…angles Sphere, torus, and trefoil-knot generators used winding order (a, b, a+1) which produces inward face normals for parametric surfaces. Reversed to (a, a+1, b) so that cross(e1, e2) points outward, matching the stored vertex normals. This was causing clusters to compute normal cones pointing inward, so the GPU cull test rejected front-facing clusters and accepted back-facing ones. Also fix computeNormalCone and clusterTriangles in cluster-builder to skip degenerate triangles (zero cross-product area, e.g. UV-sphere pole fans). Previously these corrupted the normal cone computation and formed isolated clusters with coneCos=-1 (never culled). Fix the hierarchy-builder test to find children via parentIndex rather than assuming sequential storage at childOffset, which was a coincidental property of the old (incorrect) cluster ordering. Add 18 new tests in backface-culling.test.ts covering winding-order checks, normal cone direction, and simulated GPU cull from all 6 axis-aligned cameras. https://claude.ai/code/session_013JrLspqeGiVV78d9yy3LdU
…king The previous parent geometry generation collected child vertices into a flat list and grouped consecutive samples into triangles, creating edges between vertices from spatially distant clusters — the "wireframe across the scene" bug. Implements the Nanite approach: - Greedy nearest-neighbour cluster grouping (approximates METIS graph partitioning to minimise shared boundary edge count between groups) - Vertex welding: merge coincident positions across child cluster seams so the merged group has proper connectivity - Boundary vertex locking: vertices on open boundary edges (edges shared by exactly one triangle — the group's border with its neighbours) are locked and never moved, preventing T-junction cracks between adjacent LOD clusters - Edge collapse to ~50% triangles: iteratively collapse the cheapest (shortest) non-locked edge, moving the kept vertex to the midpoint for better approximation quality Also fix a pre-existing multi-mesh offset bug: cluster.vertexOffset is a global float offset, but buildHierarchy received a local (0-based) buffer. For single meshes this was coincidentally correct; for the second mesh and beyond, vertex reads went out of bounds. Added globalVertexFloatOffset and globalIndexOffset parameters to correctly translate between global and local buffer addresses. https://claude.ai/code/session_013JrLspqeGiVV78d9yy3LdU
…eometry The vertex shader applied no per-instance transform — all instances of the same mesh were rendering at their object-space positions regardless of their placement transforms, causing them all to overlap. Root cause: buildScene built clusters once per mesh and gave all instances the same clusterOffset. The instance transforms were stored in the Instance record but never applied by any shader. Fix: build clusters once per instance instead of per mesh. Before calling buildClusters, apply the instance's Mat4 transform to every position (mat4TransformPoint) and normal (mat4TransformDir + re-normalise) in the raw mesh. Each instance gets its own unique cluster set in world space, so the vertex shader needs no per-instance transform — the geometry is already correctly positioned. This is consistent with the existing architecture: the GPU vertex shader reads world-space positions directly from the vertex buffer without any model matrix multiplication. Update the scene-builder test that was asserting the old (broken) shared- cluster assumption. https://claude.ai/code/session_013JrLspqeGiVV78d9yy3LdU
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bugs fixed
Bug 1 — Backface cone cull (
shaders.ts): The cull condition wasd > coneCos, but the correct threshold issqrt(1 - coneCos²)(= sin of the half-angle). For any cluster withconeCos < 0.707(half-angle > 45°, common on all curved surfaces), the old formula culled way too aggressively — visible as large patches of missing faces across the entire mesh.Bug 2 — LOD parent check (
shaders.ts): Parent screen error was projected using the parent bounding sphere's own center distance. Since the parent sphere encloses multiple children it sits further away, making its projected error appear smaller and incorrectly suppressing visible child clusters. Fixed by reusing the child cluster'sdistfor both projections.New demo scenes
Test plan
vitest run)sqrt(1.0 - coneCos * coneCos)and noparentDisthttps://claude.ai/code/session_013JrLspqeGiVV78d9yy3LdU