Files
yawn/server/web/docs.html
T
Ampandheaust 7070799862 Add Go CDN and playground server
Build Core and Handles into in-memory CDN modules, add the marketing site and tutorial docs, and provide a SQLite-backed WebGPU playground with TypeScript tooling and profiling.

Amp-Thread-ID: https://ampcode.com/threads/T-01a02485-5574-707c-bff4-5668d83bee8a
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-08-21 16:42:02 +00:00

370 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff" />
<meta name="description" content="Learn Yawn from your first WebGPU scene through shared memory and custom render graphs." />
<title>Learn Yawn — Documentation</title>
<link rel="stylesheet" href="/assets/site.css" />
<link rel="stylesheet" href="/assets/docs.css" />
<script type="module" src="/assets/docs.js"></script>
</head>
<body class="docs-body">
<header class="site-header docs-header">
<a class="wordmark" href="/" aria-label="Yawn home">
<span class="wordmark-mark" aria-hidden="true"><i></i><i></i><i></i></span><span>Yawn</span>
</a>
<span class="docs-product">Docs</span>
<label class="docs-search">
<span aria-hidden="true"></span>
<input type="search" placeholder="Filter this guide" aria-label="Filter documentation sections" />
<kbd>/</kbd>
</label>
<nav class="site-nav" aria-label="Documentation navigation">
<a href="/playground">Playground</a><a href="https://git.heaust.org/heaust/yawn">Source</a>
</nav>
</header>
<div class="docs-layout">
<aside class="docs-sidebar" aria-label="Documentation sections">
<div class="sidebar-group">
<strong>Start here</strong>
<a href="#welcome" class="active">Welcome to Yawn</a>
<a href="#installation">Installation</a>
<a href="#first-scene">Your first scene</a>
<a href="#camera">Add a camera</a>
</div>
<div class="sidebar-group">
<strong>Build a world</strong>
<a href="#meshes">Meshes and instances</a>
<a href="#materials">Materials and lights</a>
<a href="#models">Import a glTF model</a>
<a href="#post-processing">Post processing</a>
</div>
<div class="sidebar-group">
<strong>Understand Yawn</strong>
<a href="#mental-model">The mental model</a>
<a href="#shared-memory">Shared memory</a>
<a href="#profiler">GPU profiler</a>
</div>
<div class="sidebar-group">
<strong>Deep dives</strong>
<a href="#core-deep-dive">Using core directly</a>
<a href="#custom-rows">Craft your own API</a>
<a href="#deployment">Deployment</a>
</div>
<a class="sidebar-playground" href="/playground"><span></span><div><strong>Try as you learn</strong><small>Open the playground</small></div></a>
</aside>
<main class="docs-main">
<article>
<section id="welcome" data-title="Welcome to Yawn">
<div class="doc-eyebrow">Start here · 5 minute read</div>
<h1>Build your first Yawn scene</h1>
<p class="doc-lede">
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.
</p>
<div class="callout callout-blue">
<span class="callout-icon">i</span>
<div><strong>New to programming?</strong><p>Type the examples exactly as shown. Words in <code>code style</code> are names the computer expects; the prose around them explains why.</p></div>
</div>
</section>
<section id="installation" data-title="Installation">
<div class="section-kicker"><span>01</span> Installation</div>
<h2>Load Handles from Yawns CDN</h2>
<p>
You do not build or host Yawn yourself. An <em>import map</em> gives the CDN module a
short name that the rest of this tutorial can use. Put this inside your pages
<code>&lt;head&gt;</code>:
</p>
<div class="code-block" data-language="html">
<div class="code-title"><span>index.html</span><button type="button" data-copy>Copy</button></div>
<pre><code>&lt;script type="importmap"&gt;
{
"imports": {
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
}
}
&lt;/script&gt;</code></pre>
</div>
<p>
That URL serves a ready-to-use ES module. Its render worker, import worker, picking
worker, and WebAssembly module all continue loading from Yawns CDN automatically.
</p>
<h3>Make a canvas</h3>
<p>A <code>canvas</code> is the rectangle where WebGPU will draw. Add one to the page body:</p>
<div class="code-block" data-language="html">
<div class="code-title"><span>index.html</span><button type="button" data-copy>Copy</button></div>
<pre><code>&lt;canvas id="view" width="1280" height="720"&gt;&lt;/canvas&gt;
&lt;script type="module" src="/app.js"&gt;&lt;/script&gt;</code></pre>
</div>
<div class="callout">
<span class="callout-icon">!</span>
<div><strong>Your page needs isolation headers</strong><p>Your own web host must send <code>COOP: same-origin</code> and <code>COEP: require-corp</code> so SharedArrayBuffer is available. The CDN already sends the matching CORS and resource-policy headers for Yawns files.</p></div>
</div>
</section>
<section id="first-scene" data-title="Your first scene">
<div class="section-kicker"><span>02</span> Your first scene</div>
<h2>Scene → material → mesh</h2>
<p>
A <strong>scene</strong> owns the shared data and render graph. A
<strong>material</strong> describes the surface. A <strong>mesh</strong> supplies points
and tells Yawn which order to connect them.
</p>
<div class="code-block code-block-large" data-language="typescript">
<div class="code-title"><span>app.js</span><button type="button" data-copy>Copy</button></div>
<pre><code>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;</code></pre>
</div>
<div class="explain-grid">
<div><code>new</code><p>Creates one object. Here that is a scene, material, or mesh handle.</p></div>
<div><code>await …ready</code><p>Waits for setup to finish before the next object depends on it.</p></div>
<div><code>[x, y, z]</code><p>One point in 3D space: horizontal, vertical, and depth.</p></div>
</div>
<a class="inline-play" href="/playground"><span></span><div><strong>Run this example</strong><small>It is already loaded in the playground</small></div><b>Open →</b></a>
</section>
<section id="camera" data-title="Add a camera">
<div class="section-kicker"><span>03</span> Move around</div>
<h2>Add a camera you can orbit</h2>
<p>
The starter triangle is already in clip space, so it is visible without a camera.
For a 3D world, add an <code>ArcRotateCamera</code>. Drag to orbit and use the wheel to
zoom.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>app.ts</span><button type="button" data-copy>Copy</button></div>
<pre><code>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;</code></pre>
</div>
</section>
<section id="meshes" data-title="Meshes and instances">
<div class="section-kicker"><span>04</span> Build a world</div>
<h2>Clone geometry, not work</h2>
<p>
Every <code>Mesh</code> is an instance. Calling <code>clone()</code> 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.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>instances.ts</span><button type="button" data-copy>Copy</button></div>
<pre><code>for (let x = -4; x &lt;= 4; x++) {
const copy = triangle.clone({ position: [x * 0.35, 0, 0] });
await copy.ready;
}</code></pre>
</div>
</section>
<section id="materials" data-title="Materials and lights">
<h2>Materials and lights are ordinary handles</h2>
<p>
Change a material after it is ready and Yawn writes the new value directly into its
shared row. Lights use the same pattern.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>lighting.ts</span><button type="button" data-copy>Copy</button></div>
<pre><code>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</code></pre>
</div>
</section>
<section id="models" data-title="Import a glTF model">
<h2>Bring in a glTF model</h2>
<p>
<code>importGltf</code> fetches and parses <code>.gltf</code> or <code>.glb</code> data in
a worker, then creates ordinary Yawn meshes and materials.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>model.ts</span><button type="button" data-copy>Copy</button></div>
<pre><code>import { importGltf } from "@yawn/handles";
const meshes = await importGltf(scene, "/models/robot.glb");
meshes[0].position.y = 0.5;</code></pre>
</div>
</section>
<section id="post-processing" data-title="Post processing">
<h2>Add effects without leaving the scene API</h2>
<p>Effect handles add passes to the same render graph. Batch related additions to rebuild that graph once.</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>effects.ts</span><button type="button" data-copy>Copy</button></div>
<pre><code>import { ColorGrading, FXAA } from "@yawn/handles";
await scene.batchGraphUpdates(async () =&gt; {
const grade = new ColorGrading(scene, { toneMap: "aces" });
const fxaa = new FXAA(scene);
await Promise.all([grade.ready, fxaa.ready]);
});</code></pre>
</div>
</section>
<section id="mental-model" data-title="The mental model">
<div class="section-kicker"><span>05</span> Understand Yawn</div>
<h2>Setup is messages. Motion is memory.</h2>
<p>
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.
</p>
<div class="mental-model">
<div><strong>Infrequent control</strong><span>create · allocate · compile · switch</span></div>
<i></i>
<div class="mental-shared"><strong>Shared rows</strong><span>transform · shade · animate</span></div>
<i></i>
<div><strong>Rust/WASM core</strong><span>schedule · upload · render</span></div>
</div>
</section>
<section id="shared-memory" data-title="Shared memory">
<h2>Write your own hot data</h2>
<p>
Handles are views over named rows. Your application can add rows too. Each row is
aligned for predictable CPU and GPU use.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>simulation.ts</span><button type="button" data-copy>Copy</button></div>
<pre><code>const velocity = await scene.ensureRows(
"app.velocity", 10_000, 16, "f32"
);
velocity.row(42).set([1, 0, 0, 0]);</code></pre>
</div>
<p>
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.
</p>
</section>
<section id="profiler" data-title="GPU profiler">
<h2>Measure the passes the GPU actually ran</h2>
<p>
Open <strong>Profile</strong> 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.
</p>
<div class="callout callout-blue"><span class="callout-icon"></span><div><strong>Profile a real scene</strong><p>The playgrounds profiler uses <code>core.onProfile()</code> and <code>core.setProfiler(true)</code>—the same public APIs available to your app.</p></div></div>
</section>
<section id="core-deep-dive" data-title="Using core directly">
<div class="section-kicker"><span>06</span> Deep dive</div>
<h2>Outgrow handles without outgrowing Yawn</h2>
<p>
<code>@yawn/core</code> 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.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>core.ts</span><button type="button" data-copy>Copy</button></div>
<pre><code>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",
});</code></pre>
</div>
<p>
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.
</p>
</section>
<section id="custom-rows" data-title="Craft your own API">
<h2>Craft an API for your problem</h2>
<p>
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.
</p>
<div class="code-block" data-language="typescript">
<div class="code-title"><span>Particle.ts</span><button type="button" data-copy>Copy</button></div>
<pre><code>class Particle {
constructor(
readonly id: number,
readonly positions: SharedRows,
) {}
set x(value: number) {
this.positions.row(this.id)[0] = value;
}
}</code></pre>
</div>
</section>
<section id="deployment" data-title="Deployment">
<h2>Deployment checklist</h2>
<ul class="deploy-list">
<li><span>1</span><div><strong>Serve over HTTPS</strong><p>WebGPU and cross-origin isolation require a secure browser context outside local development.</p></div></li>
<li><span>2</span><div><strong>Keep the isolation headers</strong><p>Send <code>Cross-Origin-Opener-Policy: same-origin</code> and <code>Cross-Origin-Embedder-Policy: require-corp</code>.</p></div></li>
<li><span>3</span><div><strong>Allow Yawns CDN</strong><p>The imported module keeps every worker and WASM request on <code>yawn.heaust.org</code>. If you use a Content Security Policy, allow that origin and <code>blob:</code> workers.</p></div></li>
</ul>
<div class="next-card"><div><small>Next step</small><h3>Make the starter scene yours.</h3><p>Open the playground, choose a snippet, and save a revision you can share.</p></div><a class="button" href="/playground">Open playground →</a></div>
</section>
</article>
</main>
<aside class="docs-toc" aria-label="On this page">
<strong>On this page</strong>
<a href="#installation">Installation</a>
<a href="#first-scene">Your first scene</a>
<a href="#camera">Add a camera</a>
<a href="#mental-model">The mental model</a>
<a href="#core-deep-dive">Using core directly</a>
<a href="#deployment">Deployment</a>
</aside>
</div>
</body>
</html>