Add tutorial docs and interactive playgrounds
Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# Core and render data
|
||||
|
||||
`@yawn/core` is a protocol client. It manages the command ring, payload handshakes, graph lifecycle, and typed views over shared render-data arrays.
|
||||
|
||||
## Fast-path shared writes
|
||||
|
||||
Standard instance APIs validate `[slot, generation]`, then write the corresponding guarded SOA row. They do not enqueue a renderer command.
|
||||
|
||||
```js
|
||||
core.setInstanceTransform(instanceHandle, matrix);
|
||||
core.setInstanceType(instanceHandle, sixteenU32Words);
|
||||
```
|
||||
|
||||
The convenience `Instance` methods in `@yawn/mesh-handles` call exactly these APIs.
|
||||
|
||||
<Playground
|
||||
id="shared-animation"
|
||||
title="Direct shared-memory animation"
|
||||
description="A requestAnimationFrame loop updates one instance transform without per-frame messages."
|
||||
/>
|
||||
|
||||
## Request an SOA column
|
||||
|
||||
Array creation is intentionally an infrequent worker command. Choose a domain so core can keep the array's logical length synchronized with fixed, mesh, or instance capacity.
|
||||
|
||||
```js
|
||||
const velocity = await core.allocateArray({
|
||||
name: "instance.velocity",
|
||||
domain: "instance",
|
||||
scalar: "f32",
|
||||
lanes: 4,
|
||||
});
|
||||
|
||||
velocity.write(instanceHandle[0], [1, 0, 0, 0]);
|
||||
```
|
||||
|
||||
Every stride is a multiple of 16 bytes, keeping rows suitable for vectorized consumers. `SharedSoaArray` uses atomic lane access and refreshes its typed views when shared WASM memory grows.
|
||||
|
||||
<Playground
|
||||
id="custom-soa"
|
||||
title="Application-owned velocity rows"
|
||||
description="Allocate an instance-domain column and populate one SIMD-width row per live instance."
|
||||
/>
|
||||
|
||||
## Share a column with another worker
|
||||
|
||||
Use `share()` only during setup. It returns the shared backing buffer and wire descriptor needed to construct a compatible view in another package or worker.
|
||||
|
||||
```js
|
||||
const { buffer, descriptor } = velocity.share();
|
||||
simulationWorker.postMessage({ type: "velocity-layout", buffer, descriptor });
|
||||
```
|
||||
|
||||
The `SharedArrayBuffer` is shared, not transferred. Once installed, the simulation worker should mutate rows directly and reserve messages for layout or lifecycle changes.
|
||||
|
||||
## Graph lifecycle
|
||||
|
||||
Core accepts one graph format: the serialized S-expression produced by `@yawn/render-graph-ast`.
|
||||
|
||||
```js
|
||||
const compiled = await core.compileGraph(serializedAst);
|
||||
await core.switchCompiledGraph(compiled.compiledId);
|
||||
await core.dropCompiledGraph(oldCompiledId);
|
||||
```
|
||||
|
||||
Graph operations are serialized by the client so compile, switch, and drop cannot race each other on one core instance.
|
||||
@@ -0,0 +1,55 @@
|
||||
# glTF import worker
|
||||
|
||||
`@yawn/gltf-import` keeps parsing and bulk upload off the renderer command channel. It fetches a `.gltf` or `.glb` URL in a dedicated worker and writes a format-neutral packet directly into shared memory.
|
||||
|
||||
## Load a scene
|
||||
|
||||
```js
|
||||
import { GltfImporter } from "@yawn/gltf-import";
|
||||
|
||||
const importer = new GltfImporter(core);
|
||||
try {
|
||||
const result = await importer.load("/models/level.glb");
|
||||
console.log(result.meshes, result.materials, result.bounds);
|
||||
} finally {
|
||||
importer.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
The import handshake is:
|
||||
|
||||
1. The import worker fetches and measures the asset packet.
|
||||
2. Core allocates a fixed `upload.renderData` shared array.
|
||||
3. The import worker writes packet bytes into that SAB.
|
||||
4. Core receives only the array ID and byte count, then installs render data.
|
||||
|
||||
No GLB payload is copied through the renderer's message queue.
|
||||
|
||||
<Playground
|
||||
id="gltf-worker"
|
||||
title="Worker-side glTF import"
|
||||
description="A generated GLB is fetched through an object URL and committed from shared upload memory."
|
||||
/>
|
||||
|
||||
## Camera framing
|
||||
|
||||
Import frames the canonical `camera.state` row from scene bounds by default. Select an exterior or interior framing policy, or preserve the current camera.
|
||||
|
||||
```js
|
||||
await importer.load(url, { framing: "exterior" });
|
||||
await importer.load(url, { framing: "interior" });
|
||||
await importer.load(url, { framing: false });
|
||||
```
|
||||
|
||||
Framing is an addon behavior implemented as a shared camera-row write. It is not a camera subsystem in core.
|
||||
|
||||
## Wrap the result when useful
|
||||
|
||||
Import returns protocol descriptors. Less technical consumers can turn those descriptors into generation-safe objects.
|
||||
|
||||
```js
|
||||
import { MeshHandles, MaterialHandles } from "@yawn/mesh-handles";
|
||||
|
||||
const meshes = new MeshHandles(core).fromImportedScene(result);
|
||||
const materials = new MaterialHandles(core).fromImportedScene(result);
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Package map
|
||||
|
||||
Install only the authoring and convenience layers your application needs. None of the addons is required by core's protocol.
|
||||
|
||||
<div class="package-grid">
|
||||
<a href="./core"><strong>@yawn/core</strong><span>Worker commands, render-graph lifecycle, and shared SOA arrays.</span></a>
|
||||
<a href="./render-graph"><strong>@yawn/render-graph-*</strong><span>Canonical AST plus JSO, fluent, and FXNode frontends.</span></a>
|
||||
<a href="./gltf-import"><strong>@yawn/gltf-import</strong><span>Worker-side glTF parsing directly into shared upload memory.</span></a>
|
||||
<a href="./mesh-handles"><strong>@yawn/mesh-handles</strong><span>Generation-safe mesh, instance, camera, material, and picking facades.</span></a>
|
||||
<a href="../recipes/pipelines"><strong>@yawn/default-pipelines</strong><span>Optional scene WGSL and render/compute declarations.</span></a>
|
||||
<a href="/playground/"><strong>Examples</strong><span>Editable playgrounds that compose the public packages as an application would.</span></a>
|
||||
</div>
|
||||
|
||||
## Dependency direction
|
||||
|
||||
Applications create core first, then pass the same `YawnCore` instance to addons. Addons use public commands and shared descriptors; core never imports an addon.
|
||||
|
||||
```text
|
||||
application ─▶ graph frontend ─▶ graph AST
|
||||
│ │
|
||||
├────▶ glTF / handles addons │
|
||||
│ │ │
|
||||
└─────────────┴───────────────▶ core ─▶ render worker
|
||||
```
|
||||
|
||||
This keeps scene policy outside the renderer. You can replace default pipelines, skip conventional handles, or author the AST directly without forking core.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Conventional handles
|
||||
|
||||
`@yawn/mesh-handles` is an optional object-oriented facade. It never hides core: lifecycle methods call core commands, while frequent mutations write shared rows.
|
||||
|
||||
## Meshes and instances
|
||||
|
||||
Wrap imported descriptors, use the default instance created by glTF, or create another generation-safe instance.
|
||||
|
||||
```js
|
||||
const handles = new MeshHandles(core);
|
||||
const [mesh] = handles.fromImportedScene(imported);
|
||||
|
||||
mesh.defaultInstance.setTransform(matrix);
|
||||
const duplicate = await mesh.createInstance(otherMatrix);
|
||||
duplicate.setType(classificationWords);
|
||||
await duplicate.destroy();
|
||||
```
|
||||
|
||||
The handle is `[slot, generation]`. A stale object cannot modify a slot that has since been reused.
|
||||
|
||||
## Camera and material handles
|
||||
|
||||
The camera and materials look conventional, but property updates are writes to `camera.state` and `material.state`.
|
||||
|
||||
```js
|
||||
const camera = new CameraHandle(core);
|
||||
camera.lookAt([4, 3, 6], [0, 0, 0]);
|
||||
|
||||
const materials = new MaterialHandles(core).fromImportedScene(imported);
|
||||
materials[0].baseColor = [0.2, 0.55, 1, 1];
|
||||
materials[0].roughness = 0.35;
|
||||
```
|
||||
|
||||
<Playground
|
||||
id="conventional-handles"
|
||||
title="Camera and material properties"
|
||||
description="The gallery is reframed and one PBR row is restyled with direct shared-memory writes."
|
||||
/>
|
||||
|
||||
## Worker-side picking
|
||||
|
||||
Picking is lazy. The first `pickRay` starts a separate spatial-query worker over versioned render-data snapshots and returns wrapped instance handles.
|
||||
|
||||
```js
|
||||
const result = await handles.pickRay(origin, direction, {
|
||||
maxDistance: 10_000,
|
||||
maxHits: 1,
|
||||
});
|
||||
|
||||
const picked = result.hits[0]?.instance;
|
||||
```
|
||||
|
||||
<Playground
|
||||
id="picking"
|
||||
title="Pick the closest shared instance"
|
||||
description="Build the optional BVH and issue a ray query without adding picking code to core."
|
||||
/>
|
||||
|
||||
Call `handles.dispose()` when the scene ends so its optional picking worker and listeners are released.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Render graph frontends
|
||||
|
||||
Every frontend ends at `@yawn/render-graph-ast`. Choose the authoring style that fits your tooling; the worker receives the same S-expression either way.
|
||||
|
||||
## Plain-object authoring
|
||||
|
||||
`graphFromObject` validates and freezes ordinary JavaScript data. Pipeline declarations, compute dispatches, nodes, and DAG references all become canonical AST fields.
|
||||
|
||||
```js
|
||||
import { graphFromObject } from "@yawn/render-graph-js";
|
||||
|
||||
const graph = graphFromObject({
|
||||
id: "main",
|
||||
revision: 1,
|
||||
pipelines: { render: [scenePipeline], compute: [preparePipeline] },
|
||||
nodes: [
|
||||
{
|
||||
id: "mesh",
|
||||
state: "enabled",
|
||||
executor: { key: "mesh", version: 2 },
|
||||
parameters: {},
|
||||
inputs: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
<Playground
|
||||
id="jso-graph"
|
||||
title="A complete JSO graph"
|
||||
description="The playground compiles a plain object through AST serialization and activates the returned loadout."
|
||||
/>
|
||||
|
||||
## Fluent authoring
|
||||
|
||||
Use `RenderGraph` when a small mutable builder makes generated graphs easier to read. Calling `ast()` is the immutable boundary.
|
||||
|
||||
```js
|
||||
import { RenderGraph, ref } from "@yawn/render-graph-js";
|
||||
|
||||
const graph = new RenderGraph("generated", 1)
|
||||
.renderPipeline(scenePipeline)
|
||||
.node("source", "mesh", { version: 2 })
|
||||
.node("draw", "scene", {
|
||||
version: 2,
|
||||
inputs: { mesh: [ref("source", "mesh")] },
|
||||
});
|
||||
|
||||
const compiled = await graph.load(core);
|
||||
```
|
||||
|
||||
## FXNode export
|
||||
|
||||
`@yawn/render-graph-fxnode` translates editor snapshots into the same AST. It owns editor catalog versions and diagnostic mapping; no FXNode shape crosses into core.
|
||||
|
||||
```js
|
||||
import { adaptFxNodeSnapshot } from "@yawn/render-graph-fxnode";
|
||||
|
||||
const ast = adaptFxNodeSnapshot(snapshot, revision, {
|
||||
pipelines: myPipelines,
|
||||
});
|
||||
```
|
||||
|
||||
Open the <a href="/render-graph-studio/">Render Graph Studio</a> to edit an FXNode graph, compile it beside the JSO preset, and switch prepared loadouts.
|
||||
|
||||
## DAG references
|
||||
|
||||
A reference is data, not a nested expression. Point several consumers at one output to represent fan-out without repeating the source node.
|
||||
|
||||
```js
|
||||
import { reference } from "@yawn/render-graph-ast";
|
||||
|
||||
const shared = reference("sceneColor", "texture");
|
||||
left.inputs.color = [shared];
|
||||
right.inputs.color = [shared];
|
||||
```
|
||||
|
||||
The serializer emits `(ref "sceneColor" "texture")` wherever that edge is consumed.
|
||||
Reference in New Issue
Block a user