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>
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user