Build your first Yawn scene
This guide starts from an empty HTML file and ends with a WebGPU triangle you can move, shade, and inspect. You do not need engine experience. Every new term is explained where it first appears.
Type the examples exactly as shown. Words in code style are names the computer expects; the prose around them explains why.
Load Handles from Yawn’s CDN
You do not build or host Yawn yourself. An import map gives the CDN module a
short name that the rest of this tutorial can use. Put this inside your page’s
<head>:
<script type="importmap">
{
"imports": {
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
}
}
</script>
That URL serves a ready-to-use ES module. Its render worker, import worker, picking worker, and WebAssembly module all continue loading from Yawn’s CDN automatically.
Make a canvas
A canvas is the rectangle where WebGPU will draw. Add one to the page body:
<canvas id="view" width="1280" height="720"></canvas>
<script type="module" src="/app.js"></script>
Your own web host must send COOP: same-origin and COEP: require-corp so SharedArrayBuffer is available. The CDN already sends the matching CORS and resource-policy headers for Yawn’s files.
Scene → material → mesh
A scene owns the shared data and render graph. A material describes the surface. A mesh supplies points and tells Yawn which order to connect them.
import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
const canvas = document.querySelector("#view");
const scene = new Scene(canvas, { hdr: true, fps: 60 });
await scene.ready;
const sky = new PBRMaterial(scene, {
baseColor: [0.12, 0.58, 1, 1],
metallic: 0.15,
roughness: 0.35,
});
await sky.ready;
const triangle = new Mesh(scene, {
material: sky,
vertexData: {
positions: [-0.7, -0.6, 0, 0.7, -0.6, 0, 0, 0.72, 0],
indices: [0, 1, 2],
},
});
await triangle.ready;
newCreates one object. Here that is a scene, material, or mesh handle.
await …readyWaits for setup to finish before the next object depends on it.
[x, y, z]One point in 3D space: horizontal, vertical, and depth.
Add a camera you can orbit
The starter triangle is already in clip space, so it is visible without a camera.
For a 3D world, add an ArcRotateCamera. Drag to orbit and use the wheel to
zoom.
import { ArcRotateCamera } from "@yawn/handles";
const camera = new ArcRotateCamera(scene, {
target: triangle,
alpha: 0,
beta: Math.PI / 2,
radius: 3,
controls: { element: canvas, pointer: true },
});
await camera.ready;
Clone geometry, not work
Every Mesh is an instance. Calling clone() shares its geometry
and creates only a new transform and mesh slot. If one clone later changes its vertex
data, Yawn makes that geometry unique automatically.
for (let x = -4; x <= 4; x++) {
const copy = triangle.clone({ position: [x * 0.35, 0, 0] });
await copy.ready;
}
Materials and lights are ordinary handles
Change a material after it is ready and Yawn writes the new value directly into its shared row. Lights use the same pattern.
import { AmbientLight, PointLight } from "@yawn/handles";
const key = new PointLight(scene, {
position: [0, 1, 1],
color: [1, 0.72, 0.5],
intensity: 12,
range: 8,
});
const fill = new AmbientLight(scene, {
color: [0.08, 0.2, 0.5],
intensity: 0.3,
});
await Promise.all([key.ready, fill.ready]);
sky.roughness = 0.5; // one shared-memory write
Bring in a glTF model
importGltf fetches and parses .gltf or .glb data in
a worker, then creates ordinary Yawn meshes and materials.
import { importGltf } from "@yawn/handles";
const meshes = await importGltf(scene, "/models/robot.glb");
meshes[0].position.y = 0.5;
Add effects without leaving the scene API
Effect handles add passes to the same render graph. Batch related additions to rebuild that graph once.
import { ColorGrading, FXAA } from "@yawn/handles";
await scene.batchGraphUpdates(async () => {
const grade = new ColorGrading(scene, { toneMap: "aces" });
const fxaa = new FXAA(scene);
await Promise.all([grade.ready, fxaa.ready]);
});
Setup is messages. Motion is memory.
Expensive structural changes—creating rows, allocating IDs, or replacing a render graph—go to the render worker as messages. Values that already exist—positions, colors, camera matrices, light strengths—change in shared memory.
Measure the passes the GPU actually ran
Open Profile in the playground to enable timestamp queries. Yawn reports the physical pass names and GPU milliseconds without serializing the render queue. Support depends on the browser and adapter.
The playground’s profiler uses core.onProfile() and core.setProfiler(true)—the same public APIs available to your app.
Outgrow handles without outgrowing Yawn
@yawn/core has no scene, mesh, material, camera, or built-in shader. It
owns a shared arena and a render-graph runtime. Handles are one replaceable frontend
that builds on those two primitives.
import { YawnCore } from "https://yawn.heaust.org/pkg/core.js";
const core = new YawnCore(canvas, {
arenaBytes: 64 * 1024 * 1024,
});
await core.ready;
const particles = await core.createRows({
name: "particles",
rows: 100_000,
stride: 16,
format: "f32",
});
From here, your frontend supplies WGSL, resources, pipelines, and a pass DAG. Core compiles that description into an up-front loadout, aliases compatible transient textures, and records render work.
Craft an API for your problem
A custom handle can be as small as an ID plus getters and setters into shared rows. Keep domain policy in your code and send only structural changes to core.
class Particle {
constructor(
readonly id: number,
readonly positions: SharedRows,
) {}
set x(value: number) {
this.positions.row(this.id)[0] = value;
}
}
Deployment checklist
- 1Serve over HTTPS
WebGPU and cross-origin isolation require a secure browser context outside local development.
- 2Keep the isolation headers
Send
Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corp. - 3Allow Yawn’s CDN
The imported module keeps every worker and WASM request on
yawn.heaust.org. If you use a Content Security Policy, allow that origin andblob:workers.