Skip to content

feat(viewer): rendering & lighting pass — sun-dominant look, sky backdrop, grounded horizon#493

Merged
wass08 merged 17 commits into
mainfrom
feat/rendering-lighting-pass
Jul 13, 2026
Merged

feat(viewer): rendering & lighting pass — sun-dominant look, sky backdrop, grounded horizon#493
wass08 merged 17 commits into
mainfrom
feat/rendering-lighting-pass

Conversation

@wass08

@wass08 wass08 commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

What

A full rendering/lighting pass for the viewer, tuned against laptop thermal budgets and reviewed round-by-round on the Wawa House testbed.

Lighting & grade

  • Sun-dominant lighting rebalance across all scene themes (key light carries the image; ambient/hemi/IBL are fill), plus dark-theme (night/twilight) recalibration.
  • Scene-referred grade before tone mapping (GRADE_PARAMS: mid-gray contrast 1.05, saturation 1.1) — rendered shading only.
  • Procedural gradient-sky IBL replaces the venice_sunset_1k.hdr fetch: no network dependency, vertical color split so upward faces read cooler than walls (dark themes get 0.2 intensity).
  • Albedo clamp ≈0.83 linear for "white" materials (catalog whites, viewer defaults, WHITE_PALETTE, schema defaults) — pure-white albedo killed GI/shadow contrast. 22 bogus specular→metalness map wirings removed from the catalog.

Shadows (plain knobs only)

  • Visibility comes from MAX_SHADOW_INTENSITY 0.55 → 0.75 and PCF radius 1.5 → 4 → 2 (r184's Vogel-disk PCF respects radius; 2 keeps the soft edge without the visible dither dots). Map size and filter type stay at main's 1024 / PCFShadowMap. Interactive SSGI params are untouched from main (AO-only 1×4/r1) — no added per-frame cost.
  • Shadow-caster-only cutaway (SHADOW_ONLY_LAYER + lib/shadow-only.ts): hidden roofs/levels in solo/dollhouse views keep casting, so interiors show window light patches instead of sun flood.

Sky backdrop & grounded horizon

  • Shared backdropGradient (viewer/lib/backdrop.ts): background → warm horizon haze → theme sky arriving by ≈8° → deep zenith derived per theme in HSL (saturate ×1.5, darken ×0.72). Haze derives from backgroundSky pulled toward a warm sun tint — skyish + sunish, tight to the horizon (exp −11·|dirY| glow).
  • The same formula feeds the viewport backdrop, the snapshot pipeline, and the site horizon disc's far-field dissolve (evaluated per fragment view direction) — ground and backdrop converge to identical colors, so no horizon seam from any camera pose.
  • Contact vignette hugging the lot (strength scales with the theme's strongest light to survive the tonemap shoulder) so the parcel sits on the field instead of floating.
  • AO fades out with raw depth (≈150→350 m, same window as the ink pass) in both pipelines — kills the AO band at the geometry↔sky depth cliff and far-field AO grain.
  • Ink edges fade with depth so the horizon never inks; dark themes get bg-derived ink tint at 70 % alpha instead of glowing near-white lines.
  • New per-theme backgroundSky zenith colors; slab coplanarity epsilon (deterministic sub-3 mm per-node lift) kills z-fighting on duplicated slabs.

Snapshots

  • The transparent flag on camera-controls:generate-thumbnail is honored: preset/item captures keep alpha; studio renders and project thumbnails composite the theme backdrop + sky (uniform-driven, single cached pipeline).
  • Glass gets a fresnel-driven opacity node so it reads as a surface at grazing angles.
  • Dev-only window.__pascalCameraControls getter for deterministic screenshot tooling.

Testing

  • turbo check-types green across packages; viewer/nodes/core built clean.
  • Screenshot-verified headless (WebGPU chromium) across Studio/Paper/Twilight/Night at bird + eye level: no horizon line, seam-free disc↔backdrop from all poses, soft dot-free shadows, softened dark-theme edges.
  • End-to-end studio capture verified: 1920×1080 snapshot with sky composited; preset path keeps alpha.
  • origin/main merged in (cabinets/registry refactor, ?disable=draw, scene-ready cap) — conflicts resolved as unions, all packages rebuilt and typechecked after the merge.

🤖 Generated with Claude Code


Note

Medium Risk
Large cross-package changes to the WebGPU post pipeline, shadows, and default materials affect every rendered view; behavior is mostly visual but cutaway shadow layers and catalog map removals could surprise edge cases.

Overview
This PR overhauls rendered shading: sun-dominant theme lighting, stronger directional shadows (MAX_SHADOW_INTENSITY 0.75, softer PCF), a pre–tone-map grade, and procedural gradient-sky IBL instead of the Venice HDR fetch.

Sky and ground use a shared backdropGradient in post-processing, thumbnails, and a new site horizon disc that dissolves into the same colors (replacing GroundOccluder). Themes gain optional backgroundSky; AO and ink edges fade at far depth to avoid horizon bands.

Cutaway / solo views hide roofs and upper levels from the color pass but keep them in the shadow map via SHADOW_ONLY_LAYER and applyShadowOnly (parametric levels, GLB dollhouse/solo).

Materials: “white” albedos clamp to ~#e9e9e9 across catalog presets, schema defaults, and viewer palettes; many catalog entries drop incorrect specular→metalnessMap wiring. Glass gets fresnel opacity. Slabs get a tiny per-id Y epsilon against z-fighting.

Snapshots honor transparent on thumbnail capture (alpha for presets/items, sky composite for scene shots). Dev-only window.__pascalCameraControls supports screenshot tooling.

Reviewed by Cursor Bugbot for commit 175bd61. Bugbot is set up for automated code reviews on this repo. Configure here.

wass08 and others added 17 commits July 9, 2026 18:07
Exposes a getter for the CameraControls impl in development so
screenshot/automation tooling can set exact camera poses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-sky IBL, albedo clamp, SSGI tune

- Shadow intensity clamp 0.55 → 0.9 (sun no longer leaks into shadow),
  2048 shadow maps, PCFSoft filtering
- Scene-referred contrast/saturation grade before ACES output
  (GRADE_PARAMS in post-processing)
- Procedural gradient-sky IBL (cool zenith / warm horizon / ground
  bounce) replaces the venice_sunset HDR fetch; env-only, background
  unchanged
- Near-white albedos clamped to ~0.83 linear (defaults, white palette,
  catalog preset-white/softwhite, schema presets)
- SSGI: 2 slices / 6 steps, radius 1.6, aoIntensity 1.7, giIntensity 2
  (bounce on); studio hemi 0.6→0.45, fill 0.75→0.6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… specular-map unwiring

- SHADOW_ONLY_LAYER (4) + lib/shadow-only.ts: hidden roofs/levels in
  level-solo (editor) and dollhouse (GLB viewer) stay in the shadow map,
  so interiors keep sun shadows + window light patches; only the sun's
  shadow camera enables the layer
- Glass: fresnel-driven opacity + envMapIntensity on transparent
  standard materials (catalog glass, scene glass, window default)
- Site ground: radial fade into the theme background at the lot
  boundary (TSL colorNode); dead ground-occluder.tsx removed
- Catalog: 22 bogus *specular*→metalnessMap wirings removed (specular
  level maps are not metalness; they darkened/metallized dielectrics)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erNode

Custom TSL filter: Vogel-disk blocker search (textureLoad — the sampled
path would inherit the comparison sampler, which WGSL rejects) →
receiver-blocker penumbra estimate → variable-radius rotated PCF.
shadow.radius scales max penumbra (now 4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Night/twilight: lifted ambient/hemi beds (the 0.9 shadow intensity
crushed them), brightened theme grounds to a lit mid-tone, and dimmed
the daylight gradient-sky IBL to 0.2 for dark appearances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tive SSGI, sky gradients, scoped solo shadows

- Denoise the SSGI GI bounce (it composited raw — the visible grain) and
  drop giIntensity to 1
- Interactive SSGI back to 1 slice (×6 steps); SSGI_BAKE_PARAMS (2×6)
  for the thumbnail/bake pipeline
- Per-theme backgroundSky: vertical zenith→horizon backdrop gradient in
  the post pipeline; makes the lot-edge ground fade read in every theme
- Level solo: only levels above the soloed floor stay shadow-caster-only;
  below-levels plain-hide (they can't block the sun)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ding into the sky

- SSGI back to 2 slices × 6 steps: three's own minimum preset without
  temporal filtering — 1×6 was below the floor and the grain showed on
  flat walls; denoise radius 5 on both AO and GI, aoIntensity 1.5;
  bake preset raised to 3×8 (single-frame renders)
- Site renderer: presentation horizon disc (8× lot radius, min 400 m)
  under the lot in the theme ground colour, fading radially into the
  theme background; lot fill back to plain ground colour (the disc
  carries the fade); never pickable (noop raycast)
- Backdrop sky gradient compressed to the upper half of the screen so
  it meets the disc's far fade at exactly the horizon colour — no seam

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SSGI interactive: 1 slice × 6 steps, GI bounce OFF (denoised AO only)
  — 2 slices was a thermal problem on laptops; the bounce moves to
  SSGI_BAKE_PARAMS (3×8, gi 1) for one-shot renders
- Horizon disc dissolve: albedo fades to black while emissive fades to
  the background colour, so the far end IS the backdrop (no lit-vs-flat
  seam); backdrop gradient graded with the same transform as the scene
- Sky gradient is now world-space: per-pixel view ray reconstructed from
  the scene camera matrices, sky above the true horizon (dir.y 0→0.35),
  pure background below — aligns with the disc at any camera angle
- Studio theme: ground #e9e7e2 / horizon #fbfbfa / sky #dde7ef so the
  fade is actually visible against the white void

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on line

- The eye-level ground grain was measured (high-pass σ on a flat patch:
  2.95 baseline → 1.52 shadows-off → 2.49 ao-off): PCSS's per-pixel IGN
  dither was the dominant source, and it can't be fixed within a laptop
  budget without TAA. Interactive shadows revert to the renderer's
  PCFSoft (clean, cheap); PCSS stays wired behind ?enable=pcss for
  experiments and future bake-time use
- The horizon 'line' was the ink pass edge-detecting the ground disc's
  depth silhouette against the backdrop. Ink now fades with raw depth
  (full below ~150 m, gone past ~350 m) — near silhouettes keep their
  SketchUp line, the horizon dissolves cleanly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ilon

- backgroundSky retuned: studio/paper/blueprint/verdant get a soft blue
  zenith (overcast keeps a bluish gray — it's overcast; mediterranean/
  sunset were already blue; dark themes untouched)
- Slabs duplicated at the exact same position z-fight and no camera
  near/far tuning can separate identical depths; each slab mesh now gets
  a deterministic sub-3mm lift hashed from its node id. Render-only —
  node data, snapping and measurements untouched. (Long-term fix is
  reversedDepthBuffer; parked in plans with the depth-consumer audit.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-theme ink

Review feedback on the rendering pass:
- SSGI back to main's params (1 slice / 4 steps / r1, AO-only) everywhere,
  including thumbnails — SSGI_BAKE_PARAMS removed. Kills the added AO grain
  and the extra per-frame cost; bake workers also stop paying for heavier GI.
- Shadows stay visible via plain knobs only (intensity 0.9, radius 4): the
  custom TSL PCSS filter is gone, shadow map back to 1024, filter back to PCF
  (r184's Vogel-disk PCF respects radius, so edges stay soft).
- Night/twilight ink edges: colour now derived from the theme background
  (lifted toward white) instead of a near-white constant, and dark scenes run
  the ink at 70% alpha — no more glowing wireframe on dark backdrops.
- Snapshots: the `transparent` capture flag is now honored — preset/item
  captures keep their alpha, while studio renders and project thumbnails
  composite the theme background + sky gradient (same world-ray math as the
  viewport backdrop, uniform-driven so the cached pipeline serves both).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e, shadow tune

Follow-ups from review:
- Shadow intensity 0.9 → 0.75 (read too heavy) and PCF radius 4 → 2: the
  filter's per-pixel dither spreads with radius, which showed as dots across
  wide penumbras. Both are free knobs.
- New shared backdrop formula (viewer lib/backdrop.ts): background below,
  theme-derived haze band hugging the horizon (background lifted toward
  white — faint glow on dark themes), sky above. Used by the post pipeline,
  the thumbnail pipeline, and the site horizon disc, whose far-field
  dissolve now evaluates the same gradient per fragment view direction —
  ground and backdrop converge to identical colours, so no horizon seam
  from any camera pose.
- Contact vignette on the horizon disc: a soft albedo darkening hugging the
  lot (15%, fading out by ~2.6 lot radii) so the parcel sits on the field
  instead of floating on it. Albedo-only — never tints the dissolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A fixed 15% albedo cut disappears into the tone mapper's shoulder on themes
with strong key lights (studio runs intensity 4), and the dissolve's bright
emissive diluted what was left. Scale the vignette with the theme's strongest
light (0.13×, clamped at 0.45) and apply the halo to the in-band emissive as
well — it zeroes out by 2.6R while the dissolve completes at 5R, so the far
field stays the pure backdrop and the horizon seam guarantee holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things drew a visible line where the infinite ground met the sky:
- SSGI AO grows a band along the geometry↔sky depth cliff (same disease the
  ink pass had). Fade AO to 1 with raw depth over the ink's ≈150→350 m
  window, in both the viewport and thumbnail pipelines — AO is a near-field
  cue, it has no business shading the horizon.
- The backdrop's flat haze plateau sat between two ramps, which the eye
  amplifies into Mach lines. The gradient is now one smooth background→sky
  ramp crossing the horizon, with the haze applied as an exponential glow
  peaking exactly at dir.y = 0 — C¹-smooth on both sides, edge-free.

The sky ramp also starts at the horizon instead of ~11° up, so the theme's
backgroundSky blue actually reads at eye level instead of hiding at the top
of the frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The single pale backgroundSky stop read as a white void with blue hiding at
the top of the frame. The backdrop is now three derived stops, inZOI-style:
- pale theme sky arrives fast (full by ≈8° elevation),
- then deepens toward a zenith colour derived per theme in HSL (saturate
  ×1.5, darken ×0.72 — hue stays the theme's own: blue studio, lavender
  sunset, near-black night),
- horizon haze now lifts toward a warm white (#fff4de) instead of pure
  white, giving the junction the slight yellow of sun-scattered atmosphere.

All derived from the existing backgroundSky/background fields — no theme
data changes, and the shared-formula seam guarantee (viewport = captures =
horizon disc) carries over.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
White-based haze read as a tall white stripe between ground and sky. Aerial
perspective is sky-coloured light with a little sun scatter, so the haze now
pulls the theme's backgroundSky toward the warm sun tint (50% light themes,
25% dark) and the glow decay tightens (exp −7→−11, weight 0.9→0.8) — the
merge band is skyish, sunish, and half the height, so blue starts right
above the ground line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng-pass

# Conflicts:
#	packages/viewer/src/components/viewer/post-processing.tsx
#	packages/viewer/src/index.ts
#	packages/viewer/src/lib/materials.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 175bd61. Configure here.

scene.environmentIntensity = prevIntensity
texture.dispose()
}
}, [scene, texture, appearance])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IBL texture disposed on theme

High Severity

The useEffect cleanup disposes the memoized procedural environment DataTexture whenever appearance changes. The effect then reassigns this same, now-disposed texture to scene.environment, which breaks IBL reflections and fill lighting when switching themes or during Strict Mode remounts.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 175bd61. Configure here.

)
bgProjInvUniform.current.value.copy(thumbnailCamera.projectionMatrixInverse)
bgCamWorldUniform.current.value.copy(thumbnailCamera.matrixWorld)
bgMixUniform.current.value = transparent ? 0 : 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stacked thumbnails miss shadow-only floors

High Severity

Auto-save captures with snapLevels reset levelMode and call snapLevelsToTruePositions, which sets visible = true but leaves shadow-caster-only layer masks from solo mode. The thumbnail scene pass only renders SCENE_LAYER, so upper floors stay invisible in project thumbnails until a later frame clears shadow-only state.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 175bd61. Configure here.

const finalOutput = vec4(
mix(sceneRgb, mix(bgGradient, sceneRgb, alpha), bgMixUniform.current),
mix(alpha, float(1), bgMixUniform.current),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Studio captures skip viewport grade

Medium Severity

The live viewport applies GRADE_PARAMS contrast and saturation to rendered shading and the composited backdrop in post-processing.tsx, but the thumbnail WebGPU pipeline composites sky and scene without that grade. Studio renders and project thumbnails can look flatter or differently saturated than the editor view the user framed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 175bd61. Configure here.

cropRegion?: { x: number; y: number; width: number; height: number }
standardSize?: { w: number; h: number }
snapLevels?: boolean
// `transparent` is informational here — the render pipeline already

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback capture skips sky composite

Medium Severity

When the WebGPU thumbnail pipeline fails to build, capture falls back to gl.render and never runs the new backdrop/AO compositing path. bgMixUniform and theme backdrop uniforms are updated, but the fallback ignores them, so studio and project thumbnails can lack the theme sky the PR promises.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 175bd61. Configure here.

@wass08 wass08 merged commit 4731326 into main Jul 13, 2026
3 checks passed
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