---
name: environment-scaffold
description: Use when building the environment of a WebXR scene — floor, sky, lighting, materials, and atmosphere — or when the VR scene looks flat, washed out, or like a grey tech demo. Provides opinionated, VR-safe defaults (lights, tone mapping, fog used carefully, a floor at 1:1 scale) that look premium in a headset without tanking frame rate.
---

# Environment scaffold — a world that looks good and grounds the player

A bare three.js scene (grey background, default light) looks bad and, in VR, feels disorienting — there's no floor, no sense of scale. A few deliberate choices give the player a place to stand: a floor at their feet, a sky around them, light that reveals shape, and an intentional palette. The catch is that VR doubles every rendering cost (two eyes) and punishes a few effects that look fine on a monitor, so the defaults here are tuned for the headset.

**Core principle:** ground the player (floor + sky + 1:1 scale), light for shape, and keep the per-eye cost low.

## When to use

- Setting up or refining the look of a VR environment.
- Symptoms: scene looks flat/washed-out, no floor or horizon, player can't gauge scale, frame rate drops when you "make it pretty".

**Renderer creation itself** (XR enable, render loop, tone mapping/color space) lives in `xr-session-setup` — set those there. This skill is the *contents* of the world.

## Ground the player first

The single biggest comfort win is a visible floor at the real floor height. With a `local-floor` reference space (`xr-reference-space`), y=0 is the physical floor — put the ground there.

```ts
import * as THREE from 'three';

const ground = new THREE.Mesh(
  new THREE.PlaneGeometry(50, 50),
  new THREE.MeshStandardMaterial({ color: 0x3a3f4b, roughness: 0.9, metalness: 0 }),
);
ground.rotation.x = -Math.PI / 2; // lay flat
ground.position.y = 0;            // physical floor with local-floor reference space
ground.receiveShadow = true;
scene.add(ground);

// A subtle grid helps players judge distance and scale — cheap and effective in VR.
scene.add(new THREE.GridHelper(50, 50, 0x556070, 0x2a2f38));
```

Build a quick scale reference while prototyping: a 1.7 m tall box reads as "person height" — if it looks wrong, your units are wrong.

## Light for shape — three lights

```ts
// Ambient: lifts shadows so nothing is pure black (which reads as a hole in VR).
scene.add(new THREE.AmbientLight(0xffffff, 0.4));

// Key directional ("sun"): creates form and (optionally) shadows.
const sun = new THREE.DirectionalLight(0xffffff, 1.0);
sun.position.set(5, 10, 5);
sun.castShadow = true;            // see the shadow note below before enabling
sun.shadow.mapSize.set(1024, 1024);
scene.add(sun);

// Hemisphere: sky/ground tint — dramatically improves stylized/low-poly looks.
scene.add(new THREE.HemisphereLight(0x88aaff, 0x4a3f33, 0.5));
```

**Shadows in VR:** real-time shadows render twice (once per eye) and are a common frame-rate killer. Enable only one shadow-casting light, keep the shadow map at 1024, tighten the shadow camera frustum, and prefer **baked lighting** for static geometry (`lighting-and-baking`). For fast-paced games, skip dynamic shadows entirely.

## Sky and atmosphere

A surrounding sky removes the "floating in void" feeling. Cheapest is a background color; a gradient or skybox is better.

```ts
scene.background = new THREE.Color(0x0e1018); // pick from your palette

// Fog adds depth and hides geometry pop-in — but use it CAREFULLY in VR.
scene.fog = new THREE.Fog(0x0e1018, 15, 60); // match color to background
```

**Fog caveat:** mild distance fog is fine and helps depth, but heavy or fast-changing fog reduces the visual cues the vestibular system relies on and can worsen discomfort during locomotion. Keep it subtle, static, and matched to the background color.

## Materials — pick one family

```ts
// Stylized PBR — physically lit, low roughness reads "premium".
const mat = new THREE.MeshStandardMaterial({ color: 0x55aaff, roughness: 0.6, metalness: 0 });
// Toon — flat illustrated look; needs a directional light to step-shade.
const toon = new THREE.MeshToonMaterial({ color: 0xff5577 });
```

Use **one material family** across the scene and a palette of 4-6 intentional colors. Mixing shading models looks incoherent. Use **low-segment geometry** (sphere 16×12, not 32×32) — it keeps a clean stylized look and halves vertex cost, which matters at 2× eyes.

## A note on post-processing

The desktop "just add bloom" reflex is risky in VR. The standard `EffectComposer` path renders to a full-screen buffer and **does not natively respect the stereo/multiview render targets** — naïvely adding passes can break or double the cost. If you want glow, prefer **emissive materials read directly** or a VR-aware post pipeline, and always profile in-headset (`vr-performance-budget`). When in doubt, skip post-processing in VR.

## Domain principles honored here

- **Room-scale & 1:1 scale** — floor at physical height, scale references in meters.
- **Performance is comfort** — low-poly, one shadow light (or baked), capped effects to protect the per-eye frame budget.
- **Comfort first** — a grounded floor/horizon and only-subtle fog avoid disorientation.

## Common mistakes

| Mistake | Symptom | Fix |
| ------- | ------- | --- |
| No floor / floor not at y=0 | Player feels they're floating; scale unclear | ground plane at physical floor (`local-floor`) |
| Shadows on every light | Frame rate tanks (2× eyes) | one shadow light, or bake (`lighting-and-baking`) |
| Heavy/animated fog | Discomfort during movement | subtle, static fog matched to background |
| Adding bloom via default EffectComposer | Broken stereo or halved FPS | emissive-only, or a VR-aware pipeline; profile first |
| Mixed material families / random colors | Incoherent, cheap look | one family, 4-6 color palette |
| High-segment geometry everywhere | Vertex cost doubled across eyes | low segment counts |

## Verify

- [ ] **On-device:** there's a visible floor at the player's real feet and a sky/horizon around them — no void.
- [ ] A 1.7 m reference object reads as roughly person-height (units are correct).
- [ ] Frame rate holds at the device refresh rate with the lighting in place (`vr-performance-budget`).
- [ ] At most one shadow-casting light (or shadows baked); no per-eye frame drop from shadows.
- [ ] One material family and a small intentional palette across the scene.

## References

- `xr-reference-space` — `local-floor` so y=0 is the real floor, and 1:1 scale.
- `lighting-and-baking` — baked lightmaps for static geometry instead of real-time shadows.
- `vr-performance-budget` — confirming the look stays within the per-eye budget.
