# Agent guide - external renderers and Three.js

## Decide whether this boundary fits

Use [defineRenderLayer](/reference/functions/define-render-layer/) for a framework-neutral external renderer, or [defineThreeLayer](/reference/functions/define-three-layer/) from `@pibbl/three` for ordinary Three scenes. Read [the Three guide](/guides/three-js/) first. The adapter is not a Three JSX reconciler or a parallel retained scene graph.

The result is one composited bitmap at one position in Canvas paint order. Pibbl primitives can paint before or after it; they cannot interleave among individual Three objects. HtmlBox remains above Canvas. External layers require `OffscreenCanvas`, `transferToImageBitmap()`, and the selected renderer's capabilities. WebXR is outside the ordinary composited-layer contract.

## Ownership and lifecycle

Define the layer once at module scope. Each mounted identity owns one persistent resource lifetime. The Three adapter creates a transparent renderer and its input bridge; the application creates the scene, camera, geometry, materials, textures, loaders, controls, mixers, and effects it needs.

Use `create` for initial resources, `update` for changed props, `resize` for camera/projection and renderer-dependent sizing, and `dispose` for application resources. The adapter disposes its renderer and bridge. Do not assume disposing a scene frees its geometry or materials. Track shared assets explicitly so they are neither leaked nor disposed twice.

An asynchronous loader may finish after the owner is gone. The application must prevent publishing that stale result and release late resources. Use the actual loader's cancellation mechanism when it provides one; do not invent a Pibbl loader API. Keep a useful fallback while assets are unavailable.

## Frames and interaction

Advance controls, mixers, or effects from the supplied Pibbl frame. Request another frame only while work remains. Do not start another `requestAnimationFrame` loop or call Three's `setAnimationLoop` for a normal Pibbl integration.

Default raycasting distinguishes target, block, and miss. A registered object or descendant becomes its logical Pibbl target. Other hit geometry blocks lower Canvas paint. Miss behavior chooses pass-through, block, or target-layer. A visually empty region is not necessarily a pass-through region unless configured that way.

Connect DOM-style controls through `context.input` as documented. Pibbl still owns coordinate arbitration, capture/bubble routing, pointer capture, focus, and the outer composition. Do not independently bind controls to the visible Canvas and thereby bypass Pibbl's competing targets.

## Complete example and assets

The source below is the existing Three Layer lesson. [Open it in the playground](/playground/#/examples/three/three-layer). Install matching `@pibbl/core`, `@pibbl/three`, and `three` packages, compile with Pibbl JSX, and call its default mount on an attached canvas. This example builds geometry procedurally, so it does not require a model download. Preserve its material/geometry cleanup when substituting a loaded model.

For product assets, specify format, origin/CORS needs, dimensions or bounding box, intended camera framing, loading state, failure state, and ownership. A concept image is not proof a loader or effect has been integrated.

## Verification

Assert one create per mounted identity and an update on prop changes. Resize at two aspect ratios and verify camera projection plus logical/backing dimensions. Test target, unregistered occluder, and empty-space miss separately, including a Canvas target behind the layer.

Test focus and controls without duplicate native listeners. Remove/remount during asset loading and confirm late resources are released. Confirm animation stops when idle and all application resources dispose once. Inspect full composition pixels after state/ownership checks; a nonblank WebGL canvas alone does not prove Pibbl composition or input routing.

## Complete source

Host setup for this source: use a canvas with width 720 and height 420, compile with `jsx: "react-jsx"` and `jsxImportSource: "@pibbl/core"`, import its default `mount`, and call `const controller = mount(canvas)` after attaching the canvas. Call `controller.dispose()` before removing it.

### three-layer.example.tsx

```tsx
import { dropShadow } from '@pibbl/core/filters';
/** @jsxImportSource @pibbl/core */
import {
  FocusManagement,
  Group,
  Rectangle,
  Text,
  pibbl,
  useSignal,
  type PibblController,
} from '@pibbl/core';
import { defineThreeLayer } from '@pibbl/three';
import {
  Color,
  HemisphereLight,
  IcosahedronGeometry,
  Mesh,
  MeshPhongMaterial,
  PerspectiveCamera,
  Scene,
} from 'three';

interface ThreeLayerProps {
  readonly turn: number;
  readonly onTarget: () => void;
}

interface ThreeLayerResources {
  readonly scene: Scene;
  readonly camera: PerspectiveCamera;
  readonly blocker: Mesh<IcosahedronGeometry, MeshPhongMaterial>;
  readonly target: Mesh<IcosahedronGeometry, MeshPhongMaterial>;
}

const ThreeScene = defineThreeLayer<ThreeLayerProps, ThreeLayerResources>({
  renderer: { antialias: true },
  create() {
    const scene = new Scene();
    const camera = new PerspectiveCamera(45, 2, 0.1, 100);
    camera.position.set(0, 0, 7);

    scene.add(new HemisphereLight('#dbeafe', '#172554', 2.8));

    const blocker = new Mesh(
      new IcosahedronGeometry(1.05, 1),
      new MeshPhongMaterial({ color: new Color('#38bdf8'), shininess: 72 }),
    );
    blocker.position.x = -1.6;
    scene.add(blocker);

    const target = new Mesh(
      new IcosahedronGeometry(1.05, 1),
      new MeshPhongMaterial({
        color: new Color('#fbbf24'),
        emissive: new Color('#5b2500'),
        shininess: 96,
      }),
    );
    target.position.x = 1.6;
    scene.add(target);

    return { scene, camera, blocker, target };
  },
  update(resources, props) {
    resources.blocker.rotation.set(props.turn * 0.7, props.turn, 0);
    resources.target.rotation.set(props.turn, props.turn * 0.6, 0.2);
  },
  resize(resources, size) {
    resources.camera.aspect = size.width / size.height;
    resources.camera.updateProjectionMatrix();
  },
  targets(resources, props) {
    return [
      {
        object: resources.target,
        handlers: { onClick: props.onTarget },
        cursor: 'pointer',
        keyboardFocusable: true,
      },
    ];
  },
  dispose(resources) {
    resources.blocker.geometry.dispose();
    resources.blocker.material.dispose();
    resources.target.geometry.dispose();
    resources.target.material.dispose();
  },
});

function ThreeLayerLesson() {
  const state = useSignal({ backgroundClicks: 0, targetClicks: 0 });
  const current = state.get();
  const activateTarget = () =>
    state.update((value) => ({
      ...value,
      targetClicks: value.targetClicks + 1,
    }));

  return (
    <FocusManagement>
      <Group>
        <Rectangle
        style={{
          width: 720,
          height: 420,
          fill: '#0f2742',
          cursor: 'pointer',
        }}
        onClick={() =>
          state.update((value) => ({
            ...value,
            backgroundClicks: value.backgroundClicks + 1,
          }))
        }
        />
        <Group style={{ translateX: 70, translateY: 82 }}>
          <ThreeScene
            turn={current.targetClicks * 0.35}
            onTarget={activateTarget}
            missBehavior="pass-through"
            style={{
              width: 580,
              height: 260,
              filter: [
                dropShadow({
                  
                  offsetX: 0,
                  offsetY: 10,
                  blurRadius: 18,
                  color: '#0009',
                }),
              ],
            }}
          />
        </Group>
        <Text
        pointerEvents="none"
        style={{ left: 28, top: 36, fill: '#f8fafc', font: '700 22px Arial' }}
        >
          Three.js inside Pibbl
        </Text>
        <Text
        pointerEvents="none"
        style={{ left: 28, top: 62, fill: '#bfdbfe', font: '14px Arial' }}
        >
          Blue blocks the Canvas · gold is a Pibbl target · empty pixels pass through
        </Text>
        <Text
        pointerEvents="none"
        style={{ left: 28, top: 392, fill: '#f8fafc', font: '600 14px Arial' }}
        >
          {`Canvas ${current.backgroundClicks} · Three target ${current.targetClicks}`}
        </Text>
      </Group>
    </FocusManagement>
  );
}

export default function mount(canvas: HTMLCanvasElement): PibblController {
  return pibbl(canvas, <ThreeLayerLesson />);
}

```

## Documentation version

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.
