Amp-Thread-ID: https://ampcode.com/threads/T-01a02485-5574-707c-bff4-5668d83bee8a Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
540 lines
20 KiB
HTML
540 lines
20 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>
|
||
<script src="/assets/theme.js"></script>
|
||
<link rel="stylesheet" href="/assets/site.css" />
|
||
<link rel="stylesheet" href="/assets/docs.css" />
|
||
<link rel="stylesheet" href="/assets/theme.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>
|
||
<select class="theme-select" data-theme-select aria-label="Color theme">
|
||
<option value="auto">Auto</option>
|
||
<option value="light">Light</option>
|
||
<option value="dark">Dark</option>
|
||
</select>
|
||
</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/core": "https://yawn.heaust.org/pkg/core.js",
|
||
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
|
||
}
|
||
}
|
||
</script></code></pre>
|
||
</div>
|
||
<p>
|
||
Core and Handles stay separate: Handles uses the mapped Core
|
||
module instead of containing its own copy. Their 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 "@yawn/core";
|
||
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>
|