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>
370 lines
17 KiB
HTML
370 lines
17 KiB
HTML
<!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 Yawn’s 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 page’s
|
||
<code><head></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><script type="importmap">
|
||
{
|
||
"imports": {
|
||
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
|
||
}
|
||
}
|
||
</script></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 Yawn’s 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><canvas id="view" width="1280" height="720"></canvas>
|
||
<script type="module" src="/app.js"></script></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 Yawn’s 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 <= 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 () => {
|
||
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 playground’s 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 Yawn’s 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>
|