Files

540 lines
20 KiB
HTML
Raw Permalink 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>
<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>&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/core": "https://yawn.heaust.org/pkg/core.js",
"@yawn/handles": "https://yawn.heaust.org/pkg/handles.js"
}
}
&lt;/script&gt;</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>&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 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 &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 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>