# Learn Yawn — Documentation
Learn Yawn from your first WebGPU scene through shared memory and custom render graphs.

Start here · 5 minute read

# 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.

i
New to programming?

Type the examples exactly as shown. Words in `code style` are names the computer expects; the prose around them explains why.

01 Installation

## 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>`:

index.html

    <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:

index.html

    <canvas id="view" width="1280" height="720"></canvas>
    <script type="module" src="/app.js"></script>

!
Your page needs isolation headers

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.

02 Your first scene

## 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.

app.js

    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;

`new`

Creates one object. Here that is a scene, material, or mesh handle.

`await …ready`

Waits for setup to finish before the next object depends on it.

`[x, y, z]`

One point in 3D space: horizontal, vertical, and depth.

▶
Run this example It is already loaded in the playground
Open →
03 Move around

## 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.

app.ts

    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;

04 Build a world

## 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.

instances.ts

    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.

lighting.ts

    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.

model.ts

    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.

effects.ts

    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]);
    });

05 Understand Yawn

## 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.

Infrequent control create · allocate · compile · switch
→
Shared rows transform · shade · animate
→
Rust/WASM core schedule · upload · render

## Write your own hot data

Handles are views over named rows. Your application can add rows too. Each row is aligned for predictable CPU and GPU use.

simulation.ts

    const velocity = await scene.ensureRows(
      "app.velocity", 10_000, 16, "f32"
    );

    velocity.row(42).set([1, 0, 0, 0]);

That final line changes four floats without cloning an object or posting a worker message. The scene marks shared data dirty so the next frame sees it.

## 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.

⌁
Profile a real scene

The playground’s profiler uses `core.onProfile()` and `core.setProfiler(true)`—the same public APIs available to your app.

06 Deep dive

## 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.

core.ts

    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.

Particle.ts

    class Particle {
      constructor(
        readonly id: number,
        readonly positions: SharedRows,
      ) {}

      set x(value: number) {
        this.positions.row(this.id)[0] = value;
      }
    }

## Deployment checklist

- 1
  Serve over HTTPS

  WebGPU and cross-origin isolation require a secure browser context outside local development.

- 2
  Keep the isolation headers

  Send `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`.

- 3
  Allow 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 and `blob:` workers.

Next step

### Make the starter scene yours.

Open the playground, choose a snippet, and save a revision you can share.

Open playground →