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:
@@ -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.
|
||||
Reference in New Issue
Block a user