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,82 @@
|
||||
# Graph authoring recipes
|
||||
|
||||
All three authoring styles produce the same canonical immutable AST.
|
||||
|
||||
## 01 — Canonical DAG AST
|
||||
|
||||
Create references separately from nodes. Reusing `shared` makes one output fan out to two consumers.
|
||||
|
||||
```js
|
||||
import { createGraphAst, reference, serializeGraphAst } from "@yawn/render-graph-ast";
|
||||
|
||||
const expression = (id, inputs = {}) => ({
|
||||
id,
|
||||
state: "enabled",
|
||||
executor: { key: "and", version: 2 },
|
||||
parameters: {},
|
||||
inputs,
|
||||
});
|
||||
const shared = reference("source", "value");
|
||||
const ast = createGraphAst({
|
||||
id: "shared_dag",
|
||||
revision: 1,
|
||||
nodes: [
|
||||
expression("source"),
|
||||
expression("left", { inputs: [shared] }),
|
||||
expression("right", { inputs: [shared] }),
|
||||
],
|
||||
});
|
||||
const source = serializeGraphAst(ast);
|
||||
```
|
||||
|
||||
## 02 — Plain JavaScript object graph
|
||||
|
||||
Let `@yawn/render-graph-js` canonicalize an ordinary object when application code does not need to manipulate AST internals.
|
||||
|
||||
```js
|
||||
import { graphFromObject } from "@yawn/render-graph-js";
|
||||
|
||||
const graph = graphFromObject({
|
||||
id: "jso_graph",
|
||||
revision: 1,
|
||||
nodes: [{
|
||||
id: "mesh",
|
||||
state: "enabled",
|
||||
executor: { key: "mesh", version: 2 },
|
||||
parameters: {},
|
||||
inputs: {},
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
<Playground id="jso-graph" title="Compile a complete JSO graph" />
|
||||
|
||||
## 03 — Fluent graph builder
|
||||
|
||||
Use the chainable facade for generated graphs, then call `ast()` or `load(core)` at the boundary.
|
||||
|
||||
```js
|
||||
import { RenderGraph, ref } from "@yawn/render-graph-js";
|
||||
|
||||
const ast = new RenderGraph("fluent_graph", 1)
|
||||
.node("source", "and", { version: 2 })
|
||||
.node("consumer", "not", {
|
||||
inputs: { operand: [ref("source", "value")] },
|
||||
})
|
||||
.ast();
|
||||
```
|
||||
|
||||
## 04 — Export an FXNode snapshot
|
||||
|
||||
Keep editor schemas in the FXNode addon. Attach external pipelines during export so the resulting AST is a self-contained loadout description.
|
||||
|
||||
```js
|
||||
import { defaultPipelines } from "@yawn/default-pipelines";
|
||||
import { adaptFxNodeSnapshot } from "@yawn/render-graph-fxnode";
|
||||
|
||||
const ast = adaptFxNodeSnapshot(snapshot, 1, {
|
||||
pipelines: defaultPipelines,
|
||||
});
|
||||
```
|
||||
|
||||
Use the <a href="/render-graph-studio/">Render Graph Studio</a> for the interactive FXNode version of this recipe.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Recipes
|
||||
|
||||
The old source-only cookbook now lives here as guided, copyable snippets. Open an attached playground when you want a complete browser context and live renderer.
|
||||
|
||||
<div class="package-grid">
|
||||
<a href="./graph-authoring"><strong>01–04 · Graph authoring</strong><span>Canonical AST, JSO, fluent builder, and FXNode export.</span></a>
|
||||
<a href="./pipelines"><strong>05–08 · Pipelines and loadouts</strong><span>Default programs, custom render/compute WGSL, and activation.</span></a>
|
||||
<a href="./render-data"><strong>09–11 · Assets and render data</strong><span>glTF import, mesh instances, and custom SOA columns.</span></a>
|
||||
<a href="./runtime"><strong>12–17 · Runtime interaction</strong><span>SAB animation, picking, worker clients, scenes, camera, and materials.</span></a>
|
||||
</div>
|
||||
|
||||
Recipes 01–07 intentionally demonstrate graph fragments. A renderable loadout also needs compatible resource, scene, and frame-output nodes. Recipe 15 and the playgrounds show complete composition.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Pipeline and loadout recipes
|
||||
|
||||
WGSL belongs to a graph package or your application. Core contains no scene program.
|
||||
|
||||
## 05 — Attach the default pipelines
|
||||
|
||||
The optional package exports plain declarations, so copy only the programs your graph uses.
|
||||
|
||||
```js
|
||||
import { defaultPipelines } from "@yawn/default-pipelines";
|
||||
import { RenderGraph } from "@yawn/render-graph-js";
|
||||
|
||||
const graph = new RenderGraph("default_programs", 1);
|
||||
for (const pipeline of defaultPipelines.render) {
|
||||
graph.renderPipeline(pipeline);
|
||||
}
|
||||
for (const pipeline of defaultPipelines.compute) {
|
||||
graph.computePipeline(pipeline);
|
||||
}
|
||||
const ast = graph.ast();
|
||||
```
|
||||
|
||||
## 06 — Supply a custom render pipeline
|
||||
|
||||
Put source and entry points in the graph declaration. The shader must honor the scene ABI expected by the executor that uses it.
|
||||
|
||||
```js
|
||||
const shader = /* wgsl */ `
|
||||
@group(1) @binding(0) var<uniform> view_projection: mat4x4<f32>;
|
||||
struct Input {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(3) model_0: vec4<f32>,
|
||||
@location(4) model_1: vec4<f32>,
|
||||
@location(5) model_2: vec4<f32>,
|
||||
@location(6) model_3: vec4<f32>,
|
||||
}
|
||||
@vertex fn vertex_main(input: Input) -> @builtin(position) vec4<f32> {
|
||||
let model = mat4x4<f32>(
|
||||
input.model_0,
|
||||
input.model_1,
|
||||
input.model_2,
|
||||
input.model_3,
|
||||
);
|
||||
return view_projection * model * vec4(input.position, 1.0);
|
||||
}
|
||||
@fragment fn fragment_main() -> @location(0) vec4<f32> {
|
||||
return vec4(0.2, 0.7, 1.0, 1.0);
|
||||
}`;
|
||||
|
||||
const ast = new RenderGraph("custom_render_program", 1)
|
||||
.renderPipeline({
|
||||
name: "scene",
|
||||
shader,
|
||||
vertexEntry: "vertex_main",
|
||||
fragmentEntry: "fragment_main",
|
||||
doubleSided: false,
|
||||
})
|
||||
.ast();
|
||||
```
|
||||
|
||||
## 07 — Supply a compute pipeline
|
||||
|
||||
Dispatch dimensions are graph data and are allocated with the rest of the loadout.
|
||||
|
||||
```js
|
||||
const shader = /* wgsl */ `
|
||||
@compute @workgroup_size(8, 1, 1)
|
||||
fn initialize() {}
|
||||
`;
|
||||
|
||||
const ast = new RenderGraph("compute_program", 1)
|
||||
.computePipeline({
|
||||
name: "initialize",
|
||||
shader,
|
||||
entry: "initialize",
|
||||
dispatch: [4, 1, 1],
|
||||
})
|
||||
.ast();
|
||||
```
|
||||
|
||||
## 08 — Compile and switch
|
||||
|
||||
Compile first, then activate the prepared ID. Drop a candidate when your surrounding transaction fails.
|
||||
|
||||
```js
|
||||
import { loadGraph } from "@yawn/render-graph-js";
|
||||
|
||||
const compiled = await loadGraph(core, graph);
|
||||
try {
|
||||
await core.switchCompiledGraph(compiled.compiledId);
|
||||
} catch (error) {
|
||||
await core.dropCompiledGraph(compiled.compiledId).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
<Playground
|
||||
id="jso-graph"
|
||||
title="Compile and activate a pipeline loadout"
|
||||
description="The full preset contains external render/compute declarations and a transient resource graph."
|
||||
/>
|
||||
@@ -0,0 +1,54 @@
|
||||
# Asset and render-data recipes
|
||||
|
||||
Bulk data moves through shared storage. Small descriptors and lifecycle decisions move through messages.
|
||||
|
||||
## 09 — Import glTF in a worker
|
||||
|
||||
```js
|
||||
import { GltfImporter } from "@yawn/gltf-import";
|
||||
import { MeshHandles } from "@yawn/mesh-handles";
|
||||
|
||||
const importer = new GltfImporter(core);
|
||||
try {
|
||||
const imported = await importer.load(url);
|
||||
const meshes = new MeshHandles(core).fromImportedScene(imported);
|
||||
} finally {
|
||||
importer.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
<Playground id="gltf-worker" title="Shared-memory glTF import" />
|
||||
|
||||
## 10 — Create and mutate mesh instances
|
||||
|
||||
Creating or destroying an instance is lifecycle communication. Mutating an existing transform or type is a generation-guarded shared write.
|
||||
|
||||
```js
|
||||
const identity = [
|
||||
1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1,
|
||||
];
|
||||
|
||||
const instance = await mesh.createInstance(identity);
|
||||
instance.setTransform(nextTransform);
|
||||
instance.setType(sixteenU32Words);
|
||||
```
|
||||
|
||||
## 11 — Add a custom SOA column
|
||||
|
||||
Select the instance domain to keep row count aligned with instance capacity. Use four lanes for one SIMD-width velocity row.
|
||||
|
||||
```js
|
||||
const velocity = await core.allocateArray({
|
||||
name: "instance.velocity",
|
||||
domain: "instance",
|
||||
scalar: "f32",
|
||||
lanes: 4,
|
||||
});
|
||||
|
||||
velocity.write(instance.handle[0], [x, y, z, 0]);
|
||||
```
|
||||
|
||||
<Playground id="custom-soa" title="Allocate instance velocity data" />
|
||||
@@ -0,0 +1,91 @@
|
||||
# Runtime interaction recipes
|
||||
|
||||
Once render data exists, keep hot updates on shared rows and leave core free of application policy.
|
||||
|
||||
## 12 — Animate directly through the SAB
|
||||
|
||||
The instance facade performs the live-generation check and writes `instance.transform`.
|
||||
|
||||
```js
|
||||
function frame(time) {
|
||||
instance.setTransform(rotationY(time * 0.001));
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
```
|
||||
|
||||
<Playground id="shared-animation" title="Frame-rate transform writes" />
|
||||
|
||||
## 13 — Pick through the optional BVH worker
|
||||
|
||||
```js
|
||||
const result = await meshHandles.pickRay(origin, direction, {
|
||||
maxDistance: 10_000,
|
||||
maxHits: 1,
|
||||
});
|
||||
const nearest = result.hits[0]?.instance;
|
||||
```
|
||||
|
||||
The picking addon consumes versioned shared snapshots. Core does not know about rays or BVHs.
|
||||
|
||||
<Playground id="picking" title="Pick a shared instance" />
|
||||
|
||||
## 14 — Connect worker to worker
|
||||
|
||||
`MessagePort` implements the Worker-like methods the core client needs. Start it through the normal `YawnCore` constructor.
|
||||
|
||||
```js
|
||||
import { YawnCore } from "@yawn/core";
|
||||
|
||||
const core = new YawnCore({
|
||||
worker: port,
|
||||
memory,
|
||||
ringPtr,
|
||||
free: () => port.close(),
|
||||
});
|
||||
await core.ready;
|
||||
```
|
||||
|
||||
This is why “main thread” is not an architectural role in Yawn: any browser worker can own the client.
|
||||
|
||||
## 15 — Compose a complete scene
|
||||
|
||||
Use one core instance for every addon and activate the graph only after its complete loadout has compiled.
|
||||
|
||||
```js
|
||||
const importer = new GltfImporter(core);
|
||||
const imported = await importer.load(gltfUrl);
|
||||
const meshes = new MeshHandles(core).fromImportedScene(imported);
|
||||
const compiled = await loadGraph(core, completeGraph);
|
||||
await core.switchCompiledGraph(compiled.compiledId);
|
||||
```
|
||||
|
||||
<Playground id="first-scene" title="Complete addon composition" />
|
||||
|
||||
## 16 — Treat camera input as render data
|
||||
|
||||
There is no camera API in core. Read and write the canonical 16-lane row directly from controls or simulation code.
|
||||
|
||||
```js
|
||||
const camera = core.array("camera.state");
|
||||
const state = camera.read(0);
|
||||
state.splice(0, 3, ...nextEye);
|
||||
camera.write(0, state);
|
||||
```
|
||||
|
||||
The row packs eye, target, up, field of view, aspect, near, and far values into 64 bytes.
|
||||
|
||||
## 17 — Use conventional camera and material properties
|
||||
|
||||
Choose addon handles when a property-oriented workflow is more useful than raw SOA rows.
|
||||
|
||||
```js
|
||||
const camera = new CameraHandle(core);
|
||||
const materials = new MaterialHandles(core).fromImportedScene(imported);
|
||||
|
||||
camera.lookAt([4, 3, 6], [0, 0, 0]);
|
||||
materials[0].baseColor = [0.2, 0.55, 1, 1];
|
||||
materials[0].roughness = 0.35;
|
||||
```
|
||||
|
||||
<Playground id="conventional-handles" title="Camera and material handles" />
|
||||
Reference in New Issue
Block a user