Skip to content

fix(shadow): kill cast/self-shadow holes on imported meshes + shadow-parity bench tooling#70

Merged
apresmoi merged 7 commits into
mainfrom
fix/floor-shadow-gaps
Jun 19, 2026
Merged

fix(shadow): kill cast/self-shadow holes on imported meshes + shadow-parity bench tooling#70
apresmoi merged 7 commits into
mainfrom
fix/floor-shadow-gaps

Conversation

@apresmoi

Copy link
Copy Markdown
Collaborator

Summary

Fixes a family of cast-shadow and self-shadow holes on imported (non-watertight / faceted) meshes, and adds shadow-debugging tooling to the bench. All shadow fixes live in packages/core (shared by vanilla/React/Vue) plus the vanilla/React/Vue caster glue; the tooling is bench-only.

The work was driven by a new shadow-mask parity oracle (below): it reduces both engines to a binary "in shadow?" mask and reports a single shadowIoU per view, so each fix was validated against three.js as ground truth rather than by eyeballing.

Shadow fixes (packages/core + renderer glue)

  1. Silhouette-reliability gate. The H9 silhouette fast path mis-chains loops at degree-≠2 vertices (non-manifold / T-junctions / open boundaries from import), leaving gaps. Now it's used only when the silhouette is a clean union of simple closed loops; otherwise it falls back to the gap-free per-polygon union.
  2. Cast from all polygons. Shadow casters were filtered to camera-rendered polygons (atlas plan) and de-duplicated — so a polygon facing the light but not the camera vanished, producing camera-dependent floor-shadow holes. Now every polygon casts; coincident projections merge under the per-mesh fill-rule: nonzero.
  3. Double-sided casting for non-watertight + self-shadow. Single-sided casting dropped light-back-facing occluders. The per-poly path now casts double-sided for unreliable cross-mesh casters AND all self-shadow casters (e.g. flight poly 46 recovered 353 dropped occluders). Closed meshes are unaffected — their far back-faces sit below each lit receiver plane and get above-plane-culled (verified: apple unchanged).
  4. Gated self-shadow seam cull. The seam cull (for smooth-mesh "spiderweb" slivers) was too blunt and deleted real self-shadows on faceted meshes. It now fires only when the caster is near-coplanar (~20°) with the receiver face.

Measured (shadow-mask oracle, IoU = shape parity, 1.0 = perfect)

view before → after
rotated/sunk castle floor IoU 0.978 → 0.992 (missing 2291 → 717 px)
flight self-shadow (seam) IoU 0.866 → 0.951 (missing 5775 → 1221 px)
apple (smooth, regression check) unchanged (no spiderweb)

Bench tooling (bench/)

  • Real-capture shadow oracle (shadow-oracle.html): captures the real compositor (Playwright screenshot / tab-capture) instead of html-to-image, which silently dropped ~99.5% of the 3D-transformed shadow SVGs. Diffs across all meshes with per-polygon attribution.
  • Shadow-mask parity oracle: binary shadow-shape diff with tolerance dilation → shadowIoU, missing/extra pixel counts, rotation-correct occluder attribution. Headless drivers: bench/scripts/shadow-mask-verdict.mjs, shadow-oracle-verdict.mjs.
  • Light-animation panel: drive the directional light with per-axis math expressions of a looping phase t∈[0,2π) (sin/cos/…), with play/pause, speed, and scrub.

Tests & build

pnpm test green (core 1075, polycss 747, react 435, vue 434, fonts 45) and pnpm build (packages + website) green. AGENTS.md updated to reflect the new shadow caster rules.

@apresmoi apresmoi merged commit 5353eff into main Jun 19, 2026
1 check passed
@apresmoi apresmoi deleted the fix/floor-shadow-gaps branch June 19, 2026 00:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant