Skip to content

Use Three.js inside Pibbl

Read as Markdown

@pibbl/three is a thin integration, not a second 3D API. Your application creates ordinary Three scenes, cameras, objects, materials, loaders, controls, mixers, and composers. Pibbl contributes the boundary around them: Canvas paint order, layout, clips, outer filters, one shared scheduler, event arbitration, focus, component ownership, and teardown.

Install Pibbl, the adapter, and Three:

Terminal window
pnpm add @pibbl/core @pibbl/three three

Keep @pibbl/core as the JSX runtime. Three objects are created with the normal Three API; Pibbl JSX only mounts the completed render layer.

Call defineThreeLayer at module scope. It returns a reusable Pibbl component. One mounted component identity creates one Three renderer and one application resource value.

import { Rectangle, pibbl } from "@pibbl/core";
import { defineThreeLayer } from "@pibbl/three";
import {
BoxGeometry,
Mesh,
MeshNormalMaterial,
PerspectiveCamera,
Scene,
} from "three";
const ProductScene = defineThreeLayer<
{ rotation: number },
{ scene: Scene; camera: PerspectiveCamera; product: Mesh }
>({
create() {
const scene = new Scene();
const camera = new PerspectiveCamera(45, 1, 0.1, 100);
camera.position.z = 5;
const product = new Mesh(
new BoxGeometry(1.5, 1.5, 1.5),
new MeshNormalMaterial(),
);
scene.add(product);
return { scene, camera, product };
},
update(resources, props) {
resources.product.rotation.y = props.rotation;
},
resize(resources, size) {
resources.camera.aspect = size.width / size.height;
resources.camera.updateProjectionMatrix();
},
dispose(resources) {
resources.product.geometry.dispose();
resources.product.material.dispose();
},
});
function App() {
return (
<>
<Rectangle style={{ width: 640, height: 360, fill: "#0f172a" }} />
<ProductScene
rotation={0.5}
missBehavior="pass-through"
style={{ width: 640, height: 360 }}
/>
</>
);
}
pibbl(document.querySelector("canvas")!, <App />);

create() runs once for a mounted identity. Pibbl component updates call update() with current props instead of rebuilding the scene. resize() sees logical and backing dimensions. Without a custom render(), the adapter calls renderer.render(resources.scene, resources.camera). dispose() releases the application-owned resources; the adapter always releases its input bridge and renderer.

Share events without wrapping the scene graph

Section titled “Share events without wrapping the scene graph”

Visible Three geometry participates in Pibbl hit arbitration even when it has no Pibbl handler. That is what prevents a covered Canvas rectangle from receiving a click through a sphere or cube.

Return selected objects from targets() when they should become logical Pibbl targets:

targets(resources, props) {
return [{
object: resources.product,
handlers: { onClick: props.onProductClick },
cursor: "pointer",
keyboardFocusable: true,
}];
}

The default picker raycasts the Three scene. A hit on a registered object—or a descendant of one—becomes that Pibbl target. A hit on other visible geometry blocks lower Canvas paint. A raycast miss follows the component’s missBehavior:

  • "pass-through" lets Pibbl test paint behind the layer;
  • "block" consumes the coordinate without a target;
  • "target-layer" targets the layer itself and can feed bridged controls.

Pibbl remains responsible for capture, target, and bubble propagation, pointer capture, cursor selection, logical focus, keyboard activation, and source paint order. Three’s EventDispatcher is still useful for Three object lifecycles, but it does not replace browser input arbitration.

Use controls and animation on Pibbl’s clock

Section titled “Use controls and animation on Pibbl’s clock”

Controls that expect a DOM element connect through context.input:

create(context) {
const scene = new Scene();
const camera = new PerspectiveCamera();
const controls = context.input.connect(
element => new OrbitControls(camera, element),
);
return { scene, camera, controls };
}

Advance mixers, controls, simulations, or composers from the supplied render(resources, frame, context) callback. Call frame.invalidate() only when another frame is needed. Do not start a separate requestAnimationFrame() loop or call renderer.setAnimationLoop() for a composited layer. Immersive XR and AR presentation are outside this integration.

Compose it like any other Pibbl paint entry

Section titled “Compose it like any other Pibbl paint entry”

The complete Three frame is one transparent Canvas paint entry. Pibbl content can paint before or after it. Group, Clip, layout components, and style.filter apply outside the flattened bitmap. Individual Three objects cannot interleave with individual Pibbl primitives; use multiple layers when separate Canvas paint positions are required.

The Three.js instancing example adapts Three’s open-source instancing/raycasting example and demonstrates OrbitControls, registered targets, blocking instances, miss policies, logical focus, clipping, filters, and later Canvas paint.

Open the interactive workbench

Read the External renderers and Three.js companion for ownership, adaptation, failure modes, and verification. Agent start provides the version-selection workflow.

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