# 3D

> Cameras and camera moves, lights and lighting rigs, physically based environments, primitives, extruded text and models.

3D and 2D share one depth buffer. The camera is calibrated so **`z = 0` maps 1:1 onto canvas pixels**: a box at `x: 960, y: 540, z: 0` sits in the centre of a 1920×1080 frame. y points down, so "up" is −y.

## The fast path: a lighting rig

```ts
import { Composition, Layer3D, LayerAnimation, Lighting } from "framefields";

const W = 1920, H = 1080;
const comp = new Composition({ width: W, height: H, fps: 30, durationMs: 4000, backgroundColor: "#0b0b0c" });

comp.add(Lighting.studio({ target: [W / 2, H / 2, 0] }));          // environment + key/rim/fill, soft shadow

comp.add(
  Layer3D.text({
    text: "framefields", fontFamily: "Archivo", fontWeight: 800, fontSize: 280,
    extrusionDepth: 26, fill: "#f5e300", bevelColor: "#6b6300", roughness: 0.35,
    x: W / 2, y: H / 2, z: 0,
  }).animate(LayerAnimation.create().keys("rotateY", [[0, -20], [120, 6, "sine.inOut"]])),
);
```

Rigs: `Lighting.studio`, `soft`, `sunset`, `dramatic`. Options: `target`, `distance`, `rotation`, `intensity`, `exposure` (stops), `toneMapping`, `shadows` (default on), `shadowSoftness`, `fog`, `bloom`. A rig returns an array; `comp.add` takes it, or spread it into `children`.

## Camera

```ts
import { CameraAnimation, Layer } from "framefields";

comp.add(
  Layer.camera({ x: 960, y: 540, z: -1000, targetX: 960, targetY: 540, targetZ: 0 }).animate(
    CameraAnimation.camera()
      .orbit({ azimuth: { from: -30, to: 30 }, elevation: { from: 15, to: 15 }, radius: { to: 1200 }, start: 0, end: 300, ease: "sine.inOut" })
      .rackFocus({ fromDistance: 2000, toDistance: 1200, fromFStop: 1.8, toFStop: 4, start: 20, end: 60 }),
  ),
);
```

`CameraAnimation.camera()` moves: `orbit`, `dolly`, `truck`, `pedestal`, `pan`, `tilt`, `roll`, `rackFocus`, `handheld({ translationAmplitude, rotationAmplitude })`. Camera `mode` is `"lookAt"` (default), `"free"` or `"orbit"`.

## Lights by hand

```ts
import { Layer, Light } from "framefields";

comp.add([
  Layer.environment("studio", {   // presets: studio | soft | sunset | night | neutral
    exposure: 0.3,
    toneMapping: "neutral",       // default; "agx" filmic, "aces" punchy, "none"
    rotation: 30,
    fog: { color: "#0b0b0c", start: 2000, end: 5000 },
    bloom: true,                  // or { intensity, threshold, radius }
  }),
  Light.directional({ x: 400, y: -1200, z: -600, targetX: 960, targetY: 540, castShadow: true }),
  Light.ambient("#ffffff", 0.3),
]);
```

`Light.ambient`, `Light.directional`, `Light.point`, `Light.spot` (also `Layer.ambientLight`, `Layer.directionalLight`, `Layer.pointLight`, `Layer.spotLight`).

| Rule | Why |
| --- | --- |
| Add `Layer.environment(...)` for `roughness` / `metallic` to mean anything | Without it the scene uses classic Blinn-Phong (lint: `pbr_without_environment`) |
| One `castShadow` light, directional or spot | Only the first casts (`multiple_shadow_lights`); point/ambient can't (`shadow_unsupported_light`) |
| Tone mapping `"neutral"` keeps brand colors as authored | `"agx"` rolls highlights off filmically |
| Bloom only takes light well past white | Use `emissiveIntensity` ≳ 3 to make something glow |
| Set `fog.color` to the background | Surfaces fade into it seamlessly |

## Primitives

```ts
Layer3D.roundedBox({ x: 960, y: 540, z: 0, width: 300, height: 80, depth: 300, radius: 18, taper: 0.9, color: "#ff5a1f", roughness: 0.45 }); // keycap
Layer3D.sphere({ x: 700, y: 540, z: 0, radius: 90, color: "#e8e8e8", metallic: 1, roughness: 0.15 });                                // chrome
Layer3D.cylinder({ x: 1200, y: 540, z: 0, radius: 12, height: 400, color: "#111", emissive: "#4da3ff", emissiveIntensity: 3 });       // neon tube
```

| Builder | Makes |
| --- | --- |
| `Layer3D.roundedBox / sphere / cylinder` | solid smooth-shaded meshes; equal shapes share GPU buffers, so hundreds are cheap |
| `Layer3D.cube({ size, faces })` | a cube whose faces are 2D layers |
| `Layer3D.carousel({ radius, items })` | 2D items on a ring |
| `Layer3D.prism`, `Layer3D.plane`, `Layer3D.grid` | prisms, flat cards, ground grids |
| `Layer3D.text` / `Layer3D.extrudedText` | extruded, bevelled type from your registered fonts |

Surface props on meshes: `color`, `tint` (animates as `color`), `roughness`, `metallic`, `emissive`, `emissiveIntensity`, `castShadow`. Flat layers cast shadows only with `castShadow: true`; meshes cast by default.

## Models

```ts
comp.add(Layer.glb("assets/models/character.glb", { x: 960, y: 640, z: 0, scale: 2.5, loop: true }));
```

Loaders: `Layer.glb`, `gltf`, `obj`, `fbx`, `stl`, `ply`, `vox`, `threeDS`, `off` (also on `Layer3D`). glTF brings base color, normal, metallic-roughness, occlusion and emissive maps, and skeletal animation (up to 128 bones). Animate playback with `animationProgress`, `animationTime` or `animationSpeed`.

## Animatable 3D props

`z`, `rotateX/Y/Z`, `scaleX/Y/Z`, `roughness`, `metallic`, `emissive`, `emissiveIntensity`, `exposure`, `intensity`, plus camera props (`cameraX/Y/Z`, `targetX/Y/Z`, `cameraFov`, `focusDistance`, `fStop`, `orbitRadius`, `orbitAzimuth`, `orbitElevation`).
