Replace addons with conventional handles
Provide the single-loadout Scene API, SAB-backed meshes, cameras, materials, lights, workers, post effects, and tutorial playgrounds. Batch matching row growth so active GPU loadouts refresh once. 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:
@@ -1,19 +1,26 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { YawnCore } from "@yawn/core";
|
||||
import { loadGraph } from "@yawn/render-graph-js";
|
||||
import { triangleGraph } from "@yawn/default-pipelines";
|
||||
import {
|
||||
ComputePass,
|
||||
FXAA,
|
||||
Mesh,
|
||||
PBRMaterial,
|
||||
PointLight,
|
||||
Scene,
|
||||
} from "@yawn/handles";
|
||||
|
||||
const props = defineProps({ example: { type: String, default: "triangle" } });
|
||||
|
||||
const canvas = ref();
|
||||
const status = ref("Starting…");
|
||||
const failed = ref(false);
|
||||
let core;
|
||||
let color;
|
||||
let scene;
|
||||
let accent;
|
||||
|
||||
function move(event) {
|
||||
if (!color) return;
|
||||
if (!accent) return;
|
||||
const bounds = canvas.value.getBoundingClientRect();
|
||||
const row = color.row(0);
|
||||
const row = accent.row(0);
|
||||
row[0] = (event.clientX - bounds.left) / bounds.width;
|
||||
row[1] = 1 - (event.clientY - bounds.top) / bounds.height;
|
||||
}
|
||||
@@ -23,18 +30,47 @@ onMounted(async () => {
|
||||
if (!crossOriginIsolated) throw new Error("Cross-origin isolation is disabled");
|
||||
canvas.value.width = 960;
|
||||
canvas.value.height = 540;
|
||||
core = new YawnCore(canvas.value);
|
||||
await core.ready;
|
||||
color = await core.createRows({
|
||||
name: "triangle.color",
|
||||
rows: 1,
|
||||
stride: 16,
|
||||
format: "f32",
|
||||
scene = new Scene(canvas.value, { hdr: true });
|
||||
await scene.ready;
|
||||
accent = scene.array("sceneAccent");
|
||||
|
||||
const material = new PBRMaterial(scene, {
|
||||
baseColor: props.example === "lights" ? [1, 0.55, 0.18, 1] : [0.75, 0.9, 1, 1],
|
||||
metallic: props.example === "materials" ? 0.9 : 0.1,
|
||||
roughness: 0.38,
|
||||
});
|
||||
color.write(0, [0.2, 0.65, 1, 1]);
|
||||
await loadGraph(core, triangleGraph());
|
||||
window.__yawnPlayground = { core, color };
|
||||
status.value = "Running · move the pointer to write the shared color row";
|
||||
await material.ready;
|
||||
const mesh = new Mesh(scene, {
|
||||
material,
|
||||
vertexData: {
|
||||
positions: [-0.7, -0.6, 0, 0.7, -0.6, 0, 0, 0.72, 0],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
});
|
||||
await mesh.ready;
|
||||
|
||||
if (props.example === "instances") {
|
||||
mesh.position = [-0.45, 0, 0];
|
||||
mesh.scale = [0.65, 0.65, 1];
|
||||
const clone = mesh.clone({ position: [0.45, 0, 0], scale: [0.65, 0.65, 1] });
|
||||
await clone.ready;
|
||||
} else if (props.example === "lights") {
|
||||
await new PointLight(scene, { position: [0, 0.4, 0.2], color: [1, 0.4, 0.1], intensity: 8 }).ready;
|
||||
} else if (props.example === "post") {
|
||||
await new FXAA(scene).ready;
|
||||
} else if (props.example === "compute") {
|
||||
await scene.ensureRows("playgroundCompute", 1, 16, "u32");
|
||||
const compute = new ComputePass({
|
||||
id: "playground-compute",
|
||||
code: "@group(0) @binding(0) var<storage, read_write> value: array<u32>; @compute @workgroup_size(1) fn main() { value[0] = value[0] + 1u; }",
|
||||
buffers: [{ id: "playground-value", array: "playgroundCompute", usage: ["storage"] }],
|
||||
bindings: [{ group: 0, binding: 0, resource: "playground-value" }],
|
||||
});
|
||||
await scene.addComputePass(compute);
|
||||
}
|
||||
|
||||
window.__yawnPlayground = { scene, mesh, material, accent };
|
||||
status.value = `${props.example} running · pointer movement writes sceneAccent in the SAB`;
|
||||
} catch (error) {
|
||||
failed.value = true;
|
||||
status.value = error.message;
|
||||
@@ -43,7 +79,7 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
delete window.__yawnPlayground;
|
||||
core?.dispose();
|
||||
scene?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -23,9 +23,29 @@ export default defineConfig({
|
||||
cleanUrls: true,
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: "Architecture", link: "/" },
|
||||
{ text: "Guide", link: "/guide/getting-started" },
|
||||
{ text: "Core", link: "/guide/core" },
|
||||
{ text: "Playground", link: "/playground" },
|
||||
],
|
||||
sidebar: {
|
||||
"/guide/": [
|
||||
{
|
||||
text: "Learn Yawn",
|
||||
items: [
|
||||
{ text: "Getting started", link: "/guide/getting-started" },
|
||||
{ text: "Scene and shared data", link: "/guide/scene-and-sab" },
|
||||
{ text: "Cameras and controls", link: "/guide/cameras" },
|
||||
{ text: "Meshes and instances", link: "/guide/meshes-and-instances" },
|
||||
{ text: "Materials and textures", link: "/guide/materials" },
|
||||
{ text: "Clustered lights", link: "/guide/lights" },
|
||||
{ text: "Compute passes", link: "/guide/compute" },
|
||||
{ text: "Post processing", link: "/guide/post-processing" },
|
||||
{ text: "glTF and picking", link: "/guide/importing-and-picking" },
|
||||
{ text: "Core boundary", link: "/guide/core" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
vite: {
|
||||
plugins: [isolation],
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Cameras and controls
|
||||
|
||||
Every camera allocates a generic `cameras` slot and a transform node. Projection, lens, controller state, and transforms are direct SAB rows after construction.
|
||||
|
||||
## Shared lens and projection controls
|
||||
|
||||
```ts
|
||||
const camera = new Camera(scene, {
|
||||
fov: Math.PI / 3,
|
||||
near: 0.05,
|
||||
far: 2000,
|
||||
focalLength: 50,
|
||||
aperture: 2.8,
|
||||
focusDistance: 8,
|
||||
});
|
||||
await camera.ready;
|
||||
|
||||
camera.projection = "orthographic";
|
||||
camera.orthoSize = 12;
|
||||
camera.projection = "perspective";
|
||||
```
|
||||
|
||||
`fov`, `aspect`, `near`, `far`, `orthoSize`, `focalLength`, `aperture`, `focusDistance`, and `sensorWidth` all write the camera's shared row.
|
||||
|
||||
## Arc rotate
|
||||
|
||||
```ts
|
||||
const orbit = new ArcRotateCamera(scene, {
|
||||
alpha: 0,
|
||||
beta: Math.PI / 3,
|
||||
radius: 6,
|
||||
target: mesh,
|
||||
controls: {
|
||||
element: canvas,
|
||||
pointer: true, // left orbit, right pan, wheel zoom
|
||||
controller: true, // sticks + triggers
|
||||
},
|
||||
});
|
||||
await orbit.ready;
|
||||
```
|
||||
|
||||
## Free spectator camera
|
||||
|
||||
```ts
|
||||
const free = new FreeCamera(scene, {
|
||||
position: [0, 1, 5],
|
||||
controls: {
|
||||
element: canvas,
|
||||
keyboard: true, // WASD + Space/Ctrl
|
||||
pointer: true, // click for pointer lock, mouse to look
|
||||
controller: true,
|
||||
speed: 6,
|
||||
},
|
||||
});
|
||||
await free.ready;
|
||||
```
|
||||
|
||||
## Follow a character
|
||||
|
||||
```ts
|
||||
const follow = new FollowCamera(scene, {
|
||||
target: player,
|
||||
distance: 5,
|
||||
height: 1.8,
|
||||
smoothing: 0.12,
|
||||
});
|
||||
await follow.ready;
|
||||
|
||||
follow.target = anotherPlayer;
|
||||
follow.distance = 7;
|
||||
follow.stop();
|
||||
follow.start();
|
||||
```
|
||||
|
||||
The input and follow loops never post camera updates to core: they read and mutate the same camera, position, and quaternion rows that any other worker can use.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Compute passes
|
||||
|
||||
Compute shaders are graph data, not core code. Declare the shared rows/resources a shader needs and attach the pass to `Scene`.
|
||||
|
||||
```ts
|
||||
const velocity = await scene.ensureRows("velocity", 4096, 16, "f32");
|
||||
|
||||
const simulation = new ComputePass({
|
||||
id: "integrate",
|
||||
code: `
|
||||
@group(0) @binding(0)
|
||||
var<storage, read_write> velocity: array<vec4<f32>>;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
if (id.x < arrayLength(&velocity)) {
|
||||
velocity[id.x].y -= 0.001;
|
||||
}
|
||||
}
|
||||
`,
|
||||
buffers: [{ id: "velocity", array: "velocity", usage: ["storage"] }],
|
||||
bindings: [{ group: 0, binding: 0, resource: "velocity" }],
|
||||
dispatch: [64, 1, 1],
|
||||
});
|
||||
|
||||
await scene.addComputePass(simulation);
|
||||
```
|
||||
|
||||
<Playground example="compute" />
|
||||
|
||||
The graph DAG places an unqualified custom compute pass after light clustering and makes forward rendering depend on all custom compute passes. Use `after` to specify other dependencies.
|
||||
|
||||
```ts
|
||||
await simulation.update({ dispatch: [128, 1, 1] });
|
||||
await scene.removeComputePass(simulation);
|
||||
```
|
||||
|
||||
Both operations are infrequent graph/loadout messages. Existing source row changes are direct SAB writes.
|
||||
|
||||
<script setup>
|
||||
import Playground from "../.vitepress/Playground.vue";
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
# Core boundary
|
||||
|
||||
`@yawn/core` deliberately contains no scene types and no WGSL. It owns two things:
|
||||
|
||||
1. a 64-byte-aligned arena of named f32/u32/i32 SOA rows in one `SharedArrayBuffer`;
|
||||
2. render-graph compilation, up-front WebGPU loadouts, transient resource aliasing, render bundles, and the paced render loop.
|
||||
|
||||
```text
|
||||
infrequent worker messages hot shared mutations
|
||||
┌──────────────────────────────┐ ┌──────────────────────┐
|
||||
│ create/delete rows │ │ transforms │
|
||||
│ allocate/delete object slot │ │ cameras/materials │
|
||||
│ compile/switch graph │ │ lights/app data │
|
||||
│ play/pause/set FPS │ │ info.skipRender │
|
||||
└──────────────┬───────────────┘ └──────────┬───────────┘
|
||||
└─────────────────┬─────────────────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Rust/WASM core │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Use core directly
|
||||
|
||||
```ts
|
||||
import { YawnCore } from "@yawn/core";
|
||||
|
||||
const core = new YawnCore(canvas, { arenaBytes: 64 * 1024 * 1024 });
|
||||
await core.ready;
|
||||
|
||||
const values = await core.createRows({
|
||||
name: "application.values",
|
||||
rows: 1024,
|
||||
stride: 16,
|
||||
format: "f32",
|
||||
});
|
||||
|
||||
const id = await core.allocateObject("application.values");
|
||||
values.row(id).set([1, 2, 3, 4]);
|
||||
```
|
||||
|
||||
Graph frontends serialize plain data to `(yawn-graph 1 ...)`. Named `after` edges preserve DAG fan-out; Rust sorts passes, detects cycles, culls unused declarations, plans compatible transient lifetimes, and allocates the active loadout.
|
||||
|
||||
The `Scene` addon is one such frontend. It is replaceable and has no privileged core API.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Getting started
|
||||
|
||||
Yawn separates the data/render engine from optional scene conventions. Most applications begin with `@yawn/handles`; specialized engines can use `@yawn/core` directly.
|
||||
|
||||
## 1. Serve with isolation headers
|
||||
|
||||
`SharedArrayBuffer` requires `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`. The included VitePress server already sends both.
|
||||
|
||||
```html
|
||||
<canvas id="view"></canvas>
|
||||
<script type="module" src="/src/app.ts"></script>
|
||||
```
|
||||
|
||||
## 2. Start a Scene
|
||||
|
||||
```ts
|
||||
import { Scene } from "@yawn/handles";
|
||||
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#view")!;
|
||||
canvas.width = 1280;
|
||||
canvas.height = 720;
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true, fps: 60 });
|
||||
await scene.ready;
|
||||
```
|
||||
|
||||
`Scene` initializes conventional SOA rows and loads one clustered-forward HDR render graph. The core itself still starts with only its eight-float `info` row.
|
||||
|
||||
## 3. Add a triangle
|
||||
|
||||
```ts
|
||||
import { Mesh, PBRMaterial } from "@yawn/handles";
|
||||
|
||||
const blue = new PBRMaterial(scene, {
|
||||
baseColor: [0.15, 0.55, 1, 1],
|
||||
metallic: 0.15,
|
||||
roughness: 0.4,
|
||||
});
|
||||
await blue.ready;
|
||||
|
||||
const triangle = new Mesh(scene, {
|
||||
material: blue,
|
||||
vertexData: {
|
||||
positions: [-0.7, -0.6, 0, 0.7, -0.6, 0, 0, 0.72, 0],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
});
|
||||
await triangle.ready;
|
||||
```
|
||||
|
||||
Constructors use worker messages only to reserve slots or rebuild the graph. Once `ready` resolves, ordinary property writes mutate shared memory.
|
||||
|
||||
<Playground />
|
||||
|
||||
<script setup>
|
||||
import Playground from "../.vitepress/Playground.vue";
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
# glTF import and picking
|
||||
|
||||
## Import off-thread
|
||||
|
||||
The shared importer worker fetches and parses glTF/GLB, then the response hydrates `Mesh` and `PBRMaterial` handles attached to your scene.
|
||||
|
||||
```ts
|
||||
import { importGltf } from "@yawn/handles";
|
||||
|
||||
const meshes = await importGltf(scene, "/models/helmet.glb");
|
||||
meshes[0].position[1] = 0.5;
|
||||
```
|
||||
|
||||
The importer handles triangle primitives, external/data buffers, standard vertex attributes, indices, node transforms, and metallic-roughness values. Application-specific extensions remain application policy.
|
||||
|
||||
## Pick in the BVH worker
|
||||
|
||||
```ts
|
||||
const picking = new Picking(scene);
|
||||
await picking.ready;
|
||||
|
||||
canvas.addEventListener("click", async () => {
|
||||
const hits = await picking.pick([0, 0, 4], [0, 0, -1]);
|
||||
const nearest = hits[0];
|
||||
if (nearest) console.log(nearest.id, nearest.distance);
|
||||
});
|
||||
```
|
||||
|
||||
The worker reads shared node positions and mesh bounds, updates its BVH when the shared frame counter changes, and returns **all** AABB hits sorted by distance. You can shortlist or run an exact test afterward.
|
||||
|
||||
If row allocations relocated since `Picking` was created, refresh its shared descriptors:
|
||||
|
||||
```ts
|
||||
await picking.refresh();
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Clustered lights
|
||||
|
||||
The default `Scene` graph runs a clustered compute pass before its HDR forward passes. Light handles write flat rows consumed by that pass.
|
||||
|
||||
```ts
|
||||
const point = new PointLight(scene, {
|
||||
position: [0, 1, 1],
|
||||
color: [1, 0.25, 0.05],
|
||||
intensity: 20,
|
||||
range: 8,
|
||||
});
|
||||
|
||||
const sun = new DirectionalLight(scene, {
|
||||
quaternion: [0.2, 0, 0, 0.98],
|
||||
color: [1, 0.95, 0.8],
|
||||
intensity: 3,
|
||||
});
|
||||
|
||||
const fill = new AmbientLight(scene, { color: [0.1, 0.2, 0.4], intensity: 0.2 });
|
||||
await Promise.all([point.ready, sun.ready, fill.ready]);
|
||||
```
|
||||
|
||||
<Playground example="lights" />
|
||||
|
||||
## Rectangles and spots
|
||||
|
||||
```ts
|
||||
const panel = new RectAreaLight(scene, {
|
||||
position: [0, 2, 0],
|
||||
width: 2,
|
||||
height: 0.5,
|
||||
intensity: 12,
|
||||
});
|
||||
|
||||
const spot = new SpotLight(scene, {
|
||||
position: [0, 1, 1],
|
||||
innerAngle: 0.25,
|
||||
outerAngle: 0.6,
|
||||
range: 15,
|
||||
});
|
||||
```
|
||||
|
||||
The rectangle handle selects the default graph's linearly transformed cosine (`ltc`) path. Position, orientation, intensity, angles, and colors remain direct SAB mutations after allocation.
|
||||
|
||||
<script setup>
|
||||
import Playground from "../.vitepress/Playground.vue";
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
# Materials and textures
|
||||
|
||||
`PBRMaterial` is a conventional view over `materials` and `materialTextures` SOA rows.
|
||||
|
||||
```ts
|
||||
const paint = new PBRMaterial(scene, {
|
||||
baseColor: [0.8, 0.05, 0.03, 1],
|
||||
metallic: 0.65,
|
||||
roughness: 0.22,
|
||||
emissive: [0, 0, 0],
|
||||
});
|
||||
await paint.ready;
|
||||
|
||||
paint.roughness = 0.5; // direct SAB write
|
||||
paint.baseColor[1] = 0.35; // direct SAB write
|
||||
mesh.material = paint;
|
||||
```
|
||||
|
||||
<Playground example="materials" />
|
||||
|
||||
## Graph textures
|
||||
|
||||
```ts
|
||||
const albedo = new Texture(scene, {
|
||||
source: "/textures/paint.ktx2",
|
||||
size: [2048, 2048, 1],
|
||||
format: "rgba8unorm-srgb",
|
||||
usage: ["sampled", "copyDst"],
|
||||
});
|
||||
await albedo.ready;
|
||||
|
||||
const textured = new PBRMaterial(scene, { baseColorTexture: albedo });
|
||||
```
|
||||
|
||||
Creating or removing a `Texture` rebuilds the single graph loadout so the GPU resource is allocated up front. The source pointer stays on the handle for an importer or application uploader; core never owns image-loading policy.
|
||||
|
||||
## Custom WGSL
|
||||
|
||||
`ShaderMaterial` adds its external WGSL pipeline to the scene graph. Updating the code updates the loadout.
|
||||
|
||||
```ts
|
||||
const shader = new ShaderMaterial(scene, {
|
||||
code: `
|
||||
struct Out { @builtin(position) position: vec4<f32> }
|
||||
@vertex fn vertex(@builtin(vertex_index) id: u32) -> Out {
|
||||
let p = array(vec2(-.2, -.2), vec2(.2, -.2), vec2(0., .2));
|
||||
var out: Out; out.position = vec4(p[id], 0., 1.); return out;
|
||||
}
|
||||
@fragment fn fragment() -> @location(0) vec4<f32> {
|
||||
return vec4(1., .2, .7, 1.);
|
||||
}
|
||||
`,
|
||||
});
|
||||
await shader.ready;
|
||||
```
|
||||
|
||||
<script setup>
|
||||
import Playground from "../.vitepress/Playground.vue";
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
# Meshes and instances
|
||||
|
||||
Every `Mesh` is an instance. `clone()` shares geometry and allocates only a new node/mesh slot.
|
||||
|
||||
```ts
|
||||
const source = new Mesh(scene, {
|
||||
vertexData: {
|
||||
positions: [-0.2, -0.2, 0, 0.2, -0.2, 0, 0, 0.25, 0],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
});
|
||||
await source.ready;
|
||||
|
||||
for (let x = -4; x <= 4; x++) {
|
||||
const instance = source.clone({ position: [x * 0.2, 0, 0] });
|
||||
await instance.ready;
|
||||
}
|
||||
```
|
||||
|
||||
<Playground example="instances" />
|
||||
|
||||
## Copy-on-write geometry
|
||||
|
||||
The default vertex kinds are `positions`, `normals`, `tangents`, `uvs`, `colors`, and `indices`. Mutating any kind on an instanced clone first makes that mesh's geometry unique.
|
||||
|
||||
```ts
|
||||
const clone = source.clone();
|
||||
await clone.ready;
|
||||
|
||||
await clone.setVertexData("positions", [
|
||||
-0.4, -0.2, 0,
|
||||
0.4, -0.2, 0,
|
||||
0.0, 0.5, 0,
|
||||
]);
|
||||
```
|
||||
|
||||
## Per-face materials and visibility
|
||||
|
||||
```ts
|
||||
await mesh.setMaterialForFaces(red, [0, 2, 4]);
|
||||
mesh.material = blue;
|
||||
mesh.isVisible = false; // one direct u32 write
|
||||
```
|
||||
|
||||
Face rows store optional material pointers; a zero lane falls back to `mesh.material`.
|
||||
|
||||
<script setup>
|
||||
import Playground from "../.vitepress/Playground.vue";
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
# Post processing
|
||||
|
||||
Post-process handles insert or remove graph passes. Intermediate HDR textures are transient and compatible non-overlapping lifetimes alias the same physical allocation.
|
||||
|
||||
```ts
|
||||
const ssao = new SSAO(scene, { amount: 0.8 });
|
||||
const exposure = new DynamicExposure(scene, { exposure: 1.1 });
|
||||
const grade = new ColorGrading(scene, { toneMap: "aces", amount: 1.0 });
|
||||
const fxaa = new FXAA(scene);
|
||||
|
||||
await Promise.all([ssao.ready, exposure.ready, grade.ready, fxaa.ready]);
|
||||
```
|
||||
|
||||
<Playground example="post" />
|
||||
|
||||
Every effect is optional:
|
||||
|
||||
```ts
|
||||
await ssao.setEnabled(false);
|
||||
await grade.update({ toneMap: "reinhard" });
|
||||
await fxaa.dispose();
|
||||
```
|
||||
|
||||
Also available:
|
||||
|
||||
```ts
|
||||
const outline = new Silhouette(scene, { amount: 1 });
|
||||
const edgeImage = new Edges(scene, { amount: 2, enabled: false });
|
||||
await edgeImage.setEnabled(true);
|
||||
```
|
||||
|
||||
The final present pass tone-maps HDR to the canvas. `ColorGrading` supports `aces`, `reinhard`, and `linear` tone maps.
|
||||
|
||||
<script setup>
|
||||
import Playground from "../.vitepress/Playground.vue";
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
# Scene and shared data
|
||||
|
||||
Think of every handle as an array index, not an object mirrored into core. `Node.position`, `Node.quaternion`, and `Node.scale` are views into separate flat SOA arrays.
|
||||
|
||||
## Direct transform movement
|
||||
|
||||
```ts
|
||||
import { Node } from "@yawn/handles";
|
||||
|
||||
const pivot = new Node(scene, { position: [0, 1, 0] });
|
||||
await pivot.ready;
|
||||
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
pivot.position[0] += event.movementX * 0.002;
|
||||
pivot.position[1] -= event.movementY * 0.002;
|
||||
});
|
||||
```
|
||||
|
||||
The pointer handler sends no messages. The typed-array view points directly into the arena shared with the render worker. The camera helpers use this same pattern; see [Cameras and controls](/guide/cameras).
|
||||
|
||||
## Add an application-specific row
|
||||
|
||||
```ts
|
||||
const particles = await scene.ensureRows("particleVelocity", 10_000, 16, "f32");
|
||||
particles.row(42).set([1, 0, 0, 0]);
|
||||
```
|
||||
|
||||
Rows are 16-byte-stride-aligned and arena allocations are 64-byte aligned. Formats are `f32`, `u32`, or `i32`.
|
||||
|
||||
## Timing and render skipping
|
||||
|
||||
Core always creates `info` as:
|
||||
|
||||
```text
|
||||
[deltaTime, frameCount, elapsedTime, targetFps, skipRender, 0, 0, 0]
|
||||
```
|
||||
|
||||
```ts
|
||||
const info = scene.array("info").row(0);
|
||||
info[4] = 1; // keep timing, skip GPU work
|
||||
info[4] = 0; // resume rendering
|
||||
```
|
||||
|
||||
Use messages for rare control changes (`setFps`, graph updates, allocation); use SAB writes for existing hot state.
|
||||
+32
-24
@@ -2,41 +2,49 @@
|
||||
layout: home
|
||||
hero:
|
||||
name: Yawn
|
||||
text: Shared render data and a render graph.
|
||||
tagline: One Rust/WASM core, one fixed arena, no built-in scene model or shader.
|
||||
text: Render graphs over shared data.
|
||||
tagline: A small Rust/WASM core with optional conventional TypeScript handles.
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Open the playground
|
||||
text: Start the tutorial
|
||||
link: /guide/getting-started
|
||||
- theme: alt
|
||||
text: Open playground
|
||||
link: /playground
|
||||
features:
|
||||
- title: Shared rows
|
||||
details: Allocate an SOA row array once by message, then mutate its SAB views directly from any thread.
|
||||
- title: External graphs
|
||||
details: JSO and FXNode addons serialize DAGs to the S-expression AST consumed by the worker.
|
||||
- title: Up-front loadouts
|
||||
details: Pipelines, GPU resources, pass order, and compatible transient aliases are prepared before activation.
|
||||
- title: Hot state stays shared
|
||||
details: Transform, material, light, and camera changes are direct SharedArrayBuffer writes from any thread.
|
||||
- title: Graph-authored GPU work
|
||||
details: WGSL, pipelines, compute, HDR, and post effects live in one externally supplied DAG loadout.
|
||||
- title: Conventional when wanted
|
||||
details: The handles addon supplies Scene, Mesh, materials, lights, glTF import, and BVH picking without adding core semantics.
|
||||
---
|
||||
|
||||
## The entire boundary
|
||||
## The shortest useful scene
|
||||
|
||||
```js
|
||||
const color = await core.allocateRows({
|
||||
name: "triangle.color",
|
||||
rows: 1,
|
||||
stride: 16,
|
||||
format: "f32",
|
||||
```ts
|
||||
import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(document.querySelector("canvas"), { hdr: true });
|
||||
await scene.ready;
|
||||
|
||||
const material = new PBRMaterial(scene, { baseColor: [0.2, 0.7, 1, 1] });
|
||||
await material.ready;
|
||||
const mesh = new Mesh(scene, {
|
||||
material,
|
||||
vertexData: {
|
||||
positions: [-0.7, -0.6, 0, 0.7, -0.6, 0, 0, 0.7, 0],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
});
|
||||
await mesh.ready;
|
||||
|
||||
color.write(0, [0.2, 0.65, 1, 1]);
|
||||
color.row(0)[0] = 0.8; // direct SharedArrayBuffer write
|
||||
await loadGraph(core, graph); // infrequent message
|
||||
mesh.position[0] = 0.25; // direct SAB mutation
|
||||
```
|
||||
|
||||
`@yawn/core` contains only the Rust/WASM render-data arena and graph compiler plus the browser worker required to execute WebGPU. Rust owns the fixed 64-byte-aligned shared arena, DAG ordering, resource culling, and transient texture planning; the worker materializes the resulting loadout. Every scene convention and every byte of WGSL comes from an addon or application.
|
||||
`Scene` installs one HDR clustered-forward loadout. Adding compute, custom shaders, textures, or post effects rebuilds that same loadout; changing values already present in shared rows does not send a message.
|
||||
|
||||
```text
|
||||
JSO / FXNode ──▶ AST ──▶ S-expression ──▶ Rust/WASM ──▶ worker ──▶ WebGPU
|
||||
any JS thread ────────────── direct SAB row writes ────────────────────┘
|
||||
handles ──▶ graph AST ──▶ S-expression ──▶ core worker ──▶ Rust/WebGPU
|
||||
any JS thread ─────────────── direct SAB row writes ────────────────┘
|
||||
```
|
||||
|
||||
The addon packages provide graph serialization, optional WGSL, glTF import directly into shared rows, and conventional camera/material/mesh handles. None of them add semantics to core.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Minimal playground
|
||||
|
||||
This is the one runnable example. It allocates a single `f32` row, sends an externally authored JSO render graph through the AST codec, and changes color by writing the shared row directly on pointer movement.
|
||||
This page creates an HDR `Scene`, one PBR material, and one indexed mesh. Move the pointer over the canvas: the handler writes the `sceneAccent` shared row directly without messaging the renderer.
|
||||
|
||||
<Playground />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user