Rewrite core around shared rows and render graphs
Co-authored-by: Heaust Azure <heaust.azure@gmail.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<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";
|
||||
|
||||
const canvas = ref();
|
||||
const status = ref("Starting…");
|
||||
const failed = ref(false);
|
||||
let core;
|
||||
let color;
|
||||
|
||||
function move(event) {
|
||||
if (!color) return;
|
||||
const bounds = canvas.value.getBoundingClientRect();
|
||||
const row = color.row(0);
|
||||
row[0] = (event.clientX - bounds.left) / bounds.width;
|
||||
row[1] = 1 - (event.clientY - bounds.top) / bounds.height;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
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.allocateRows({
|
||||
name: "triangle.color",
|
||||
rows: 1,
|
||||
stride: 16,
|
||||
format: "f32",
|
||||
});
|
||||
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";
|
||||
} catch (error) {
|
||||
failed.value = true;
|
||||
status.value = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
delete window.__yawnPlayground;
|
||||
core?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="playground">
|
||||
<canvas ref="canvas" aria-label="Yawn WebGPU output" @pointermove="move" />
|
||||
<p :class="{ failed }" data-playground-status>{{ status }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.playground { margin: 24px 0; }
|
||||
canvas { width: 100%; aspect-ratio: 16 / 9; display: block; background: #111827; border-radius: 12px; }
|
||||
p { color: var(--vp-c-text-2); }
|
||||
.failed { color: var(--vp-c-danger-1); }
|
||||
</style>
|
||||
+26
-57
@@ -1,67 +1,36 @@
|
||||
import { defineConfig } from "vitepress";
|
||||
|
||||
const headers = {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
};
|
||||
|
||||
const isolation = {
|
||||
name: "cross-origin-isolation",
|
||||
configureServer(server) {
|
||||
server.middlewares.use((_, response, next) => {
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
response.setHeader(name, value);
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
title: "Yawn",
|
||||
description: "Worker-native WebGPU rendering with shared render data.",
|
||||
base: "/docs/",
|
||||
outDir: "../dist/docs",
|
||||
description: "Shared render data and a render graph.",
|
||||
cleanUrls: true,
|
||||
head: [
|
||||
["meta", { name: "theme-color", content: "#0d1117" }],
|
||||
["link", { rel: "icon", href: "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 64 64%22><rect width=%2264%22 height=%2264%22 rx=%2214%22 fill=%22%230d1117%22/><path d=%22M13 14h10l9 17 9-17h10L37 40v11H27V40z%22 fill=%22%23ed7946%22/></svg>" }],
|
||||
],
|
||||
themeConfig: {
|
||||
logo: {
|
||||
light: "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 42 42%22><rect width=%2242%22 height=%2242%22 rx=%229%22 fill=%22%2311161e%22/><path d=%22M8 9h7l6 11 6-11h7l-9 17v7h-8v-7z%22 fill=%22%23ed7946%22/></svg>",
|
||||
dark: "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 42 42%22><rect width=%2242%22 height=%2242%22 rx=%229%22 fill=%22%2311161e%22/><path d=%22M8 9h7l6 11 6-11h7l-9 17v7h-8v-7z%22 fill=%22%23ed7946%22/></svg>",
|
||||
},
|
||||
nav: [
|
||||
{ text: "Learn", link: "/guide/first-scene" },
|
||||
{ text: "Packages", link: "/packages/" },
|
||||
{ text: "Recipes", link: "/recipes/" },
|
||||
{ text: "Playground", link: "/../playground/" },
|
||||
{ text: "Architecture", link: "/" },
|
||||
{ text: "Playground", link: "/playground" },
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
text: "Get started",
|
||||
items: [
|
||||
{ text: "Your first scene", link: "/guide/first-scene" },
|
||||
{ text: "How Yawn fits together", link: "/guide/architecture" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Package tutorials",
|
||||
items: [
|
||||
{ text: "Package map", link: "/packages/" },
|
||||
{ text: "Core and render data", link: "/packages/core" },
|
||||
{ text: "Render graph frontends", link: "/packages/render-graph" },
|
||||
{ text: "glTF import worker", link: "/packages/gltf-import" },
|
||||
{ text: "Conventional handles", link: "/packages/mesh-handles" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Recipes",
|
||||
items: [
|
||||
{ text: "All recipes", link: "/recipes/" },
|
||||
{ text: "Graph authoring", link: "/recipes/graph-authoring" },
|
||||
{ text: "Pipelines and loadouts", link: "/recipes/pipelines" },
|
||||
{ text: "Assets and render data", link: "/recipes/render-data" },
|
||||
{ text: "Runtime interaction", link: "/recipes/runtime" },
|
||||
],
|
||||
},
|
||||
],
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/heaust-ops/yawn" },
|
||||
],
|
||||
search: { provider: "local" },
|
||||
outline: { level: [2, 3] },
|
||||
editLink: {
|
||||
pattern: "https://github.com/heaust-ops/yawn/edit/feat/core/docs/:path",
|
||||
text: "Edit this page on GitHub",
|
||||
},
|
||||
footer: {
|
||||
message: "Core owns render data and render graphs. Addons own conveniences.",
|
||||
copyright: "Yawn is pre-1.0 software.",
|
||||
},
|
||||
},
|
||||
vite: {
|
||||
plugins: [isolation],
|
||||
worker: { format: "es" },
|
||||
server: { allowedHosts: true, headers },
|
||||
preview: { allowedHosts: true, headers },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, default: "Run this recipe against the real Yawn worker." },
|
||||
});
|
||||
|
||||
const query = computed(() => encodeURIComponent(props.id));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="yawn-playground">
|
||||
<div class="yawn-playground__header">
|
||||
<div>
|
||||
<span>LIVE PLAYGROUND</span>
|
||||
<strong>{{ title }}</strong>
|
||||
<p>{{ description }}</p>
|
||||
</div>
|
||||
<a :href="`/playground/?recipe=${query}`">Edit and run ↗</a>
|
||||
</div>
|
||||
<ClientOnly>
|
||||
<iframe
|
||||
:src="`/playground/runner.html?recipe=${query}&embed=1`"
|
||||
:title="`${title} live preview`"
|
||||
loading="lazy"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,90 +0,0 @@
|
||||
:root {
|
||||
--vp-c-brand-1: #d86535;
|
||||
--vp-c-brand-2: #ee7946;
|
||||
--vp-c-brand-3: #f1956d;
|
||||
--vp-c-brand-soft: rgba(224, 104, 54, 0.14);
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: linear-gradient(110deg, #ef7b47, #f5bc75);
|
||||
--vp-home-hero-image-background-image: radial-gradient(circle, #da633950 0%, transparent 68%);
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-bg: #0a0d12;
|
||||
--vp-c-bg-alt: #0f141c;
|
||||
--vp-c-bg-soft: #121821;
|
||||
--vp-c-bg-elv: #151c26;
|
||||
--vp-c-divider: #27303c;
|
||||
--vp-code-block-bg: #0b1017;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title { letter-spacing: 0.08em; }
|
||||
.VPHomeHero .name { letter-spacing: -0.045em; }
|
||||
.VPHomeHero .text { max-width: 760px; letter-spacing: -0.04em; }
|
||||
.VPHomeHero .tagline { max-width: 610px; }
|
||||
.VPFeature { border-color: var(--vp-c-divider); }
|
||||
|
||||
.yawn-playground {
|
||||
margin: 28px 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: #080b10;
|
||||
box-shadow: 0 18px 55px #0003;
|
||||
}
|
||||
|
||||
.yawn-playground__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid #26303c;
|
||||
background: #111720;
|
||||
}
|
||||
|
||||
.yawn-playground__header > div { display: grid; gap: 3px; }
|
||||
.yawn-playground__header span {
|
||||
color: #ef7b47;
|
||||
font: 700 10px ui-monospace, monospace;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
.yawn-playground__header strong { color: #f1f4f8; font-size: 15px; }
|
||||
.yawn-playground__header p { margin: 0; color: #8f9bac; font-size: 12px; }
|
||||
.yawn-playground__header a {
|
||||
flex: none;
|
||||
padding: 8px 11px;
|
||||
border: 1px solid #e47748;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
background: #c65d2f;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
.yawn-playground iframe { display: block; width: 100%; height: 360px; border: 0; }
|
||||
|
||||
.package-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
.package-grid > a {
|
||||
display: block;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
color: var(--vp-c-text-1);
|
||||
background: var(--vp-c-bg-soft);
|
||||
text-decoration: none;
|
||||
}
|
||||
.package-grid > a:hover { border-color: var(--vp-c-brand-1); }
|
||||
.package-grid strong { display: block; margin-bottom: 4px; }
|
||||
.package-grid span { color: var(--vp-c-text-2); font-size: 13px; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.package-grid { grid-template-columns: 1fr; }
|
||||
.yawn-playground__header { align-items: flex-start; flex-direction: column; }
|
||||
.yawn-playground iframe { height: 280px; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import DefaultTheme from "vitepress/theme";
|
||||
import Playground from "./Playground.vue";
|
||||
import "./custom.css";
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
app.component("Playground", Playground);
|
||||
},
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
# How Yawn fits together
|
||||
|
||||
Yawn has two public boundaries: **worker communication** for small, infrequent operations and **shared render data** for values that change often. Everything else is an authoring or convenience layer outside core.
|
||||
|
||||
```text
|
||||
JSO / fluent builder ─┐
|
||||
├──▶ canonical DAG AST ─▶ S-expression ─▶ render worker
|
||||
FXNode snapshot ─────┘ │
|
||||
├─ graph compiler
|
||||
glTF import worker ───── shared upload array ────────────────────┤
|
||||
├─ transient allocator
|
||||
any browser thread ─── lifecycle messages ──────────────────────┤
|
||||
any browser thread ─── atomic SOA writes ────────────────────────┘
|
||||
```
|
||||
|
||||
## What core owns
|
||||
|
||||
`@yawn/core` owns only the protocol client for render data and render graphs. The worker behind it owns graph validation, loadout preparation, transient lifetime analysis, GPU allocation, and rendering.
|
||||
|
||||
Core does **not** own a camera module, scene object model, glTF parser, shader library, editor, or picking system. Camera and material values are ordinary render-data columns. Higher-level objects are optional addon views over those columns.
|
||||
|
||||
## The graph is the program
|
||||
|
||||
Every frontend must produce `@yawn/render-graph-ast` data. A node is named, while an input contains one or more `{ node, socket }` references. Reusing the same reference creates fan-out, so the format describes a DAG instead of duplicating a tree.
|
||||
|
||||
Render and compute pipeline declarations are part of that AST. Their WGSL and state are compiled into a prepared loadout, not linked into core.
|
||||
|
||||
## The SOA is the mutable scene
|
||||
|
||||
Shared arrays are 64-byte aligned and each row stride is a multiple of 16 bytes. The standard columns cover mesh, instance, camera, and material data. Applications can request additional mesh-, instance-, or fixed-domain arrays; domain arrays grow with the corresponding render-data capacity.
|
||||
|
||||
Lifecycle operations such as allocating a column, importing an asset, compiling a graph, or creating an instance cross the command boundary. A frame-rate transform, camera, classification, or material update writes the existing SAB row directly.
|
||||
|
||||
## Thread placement is a choice
|
||||
|
||||
The JS client only requires a Worker-like endpoint. A main thread can own it, or another worker can connect through a `MessagePort`. Shared descriptors can be passed to additional workers, which can then read or write the same SOA data without proxying every update through the main thread.
|
||||
|
||||
::: warning Cross-origin isolation is required
|
||||
Serve the application with `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`. `npm run examples` supplies both headers.
|
||||
:::
|
||||
@@ -1,84 +0,0 @@
|
||||
# Your first scene
|
||||
|
||||
This tutorial composes Yawn the same way an application does: create the worker transport, wait for shared render data, compile a graph, import an asset, and activate the prepared loadout.
|
||||
|
||||
## 1. Start core
|
||||
|
||||
Create one renderer worker and transfer an `OffscreenCanvas` to it. `YawnCore` is deliberately transport-oriented; your bootstrap owns canvas sizing and worker construction.
|
||||
|
||||
```js
|
||||
import { YawnCore } from "@yawn/core";
|
||||
|
||||
const canvas = document.querySelector("canvas");
|
||||
canvas.width = Math.round(canvas.clientWidth * devicePixelRatio);
|
||||
canvas.height = Math.round(canvas.clientHeight * devicePixelRatio);
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
const worker = new Worker(new URL("./render-worker.js", import.meta.url), {
|
||||
type: "module",
|
||||
});
|
||||
const core = new YawnCore({ worker });
|
||||
worker.postMessage({ type: "init", canvas: offscreen }, [offscreen]);
|
||||
await core.ready;
|
||||
```
|
||||
|
||||
`ready` resolves after core receives the standard SOA descriptors. From that point, `core.array("camera.state")` and the other built-in columns are safe to access.
|
||||
|
||||
## 2. Compile a graph
|
||||
|
||||
The optional default-pipelines addon supplies scene WGSL. A JSO graph places those declarations beside graph nodes, then the graph addon serializes the canonical AST for core.
|
||||
|
||||
```js
|
||||
import { loadGraph } from "@yawn/render-graph-js";
|
||||
import { defaultPipelines } from "@yawn/default-pipelines";
|
||||
|
||||
const graph = {
|
||||
id: "main",
|
||||
revision: 1,
|
||||
pipelines: defaultPipelines,
|
||||
nodes: completeSceneNodes,
|
||||
};
|
||||
|
||||
const compiled = await loadGraph(core, graph);
|
||||
```
|
||||
|
||||
Compilation validates the DAG, removes dead work, computes transient resource lifetimes, aliases compatible resources, and allocates the resulting loadout before returning its ID.
|
||||
|
||||
## 3. Import render data
|
||||
|
||||
The glTF addon fetches and parses in its own worker. It asks core for a fixed shared upload array, writes the packet into that SAB, then sends only the array ID and byte count for the commit.
|
||||
|
||||
```js
|
||||
import { GltfImporter } from "@yawn/gltf-import";
|
||||
import { MeshHandles } from "@yawn/mesh-handles";
|
||||
|
||||
const importer = new GltfImporter(core);
|
||||
const result = await importer.load("/assets/scene.glb");
|
||||
const handles = new MeshHandles(core);
|
||||
const meshes = handles.fromImportedScene(result);
|
||||
importer.dispose();
|
||||
```
|
||||
|
||||
## 4. Activate the loadout
|
||||
|
||||
Switching is transactional from the application's perspective: the previously active loadout keeps rendering until the prepared graph becomes active.
|
||||
|
||||
```js
|
||||
await core.switchCompiledGraph(compiled.compiledId);
|
||||
|
||||
meshes[0].defaultInstance.setTransform(nextTransform); // direct SAB write
|
||||
```
|
||||
|
||||
Use messages for setup and teardown. Use shared writes for values that are already present and can change every frame.
|
||||
|
||||
<Playground
|
||||
id="first-scene"
|
||||
title="Complete first scene"
|
||||
description="Open the editor to change the procedural loadout or inspect live telemetry."
|
||||
/>
|
||||
|
||||
## Next steps
|
||||
|
||||
- Learn why these boundaries exist in [How Yawn fits together](./architecture).
|
||||
- Author graphs with [plain objects, a fluent builder, or FXNode](../packages/render-graph).
|
||||
- Add custom shared columns in [Core and render data](../packages/core).
|
||||
- Use familiar objects in [Conventional handles](../packages/mesh-handles).
|
||||
+31
-24
@@ -1,35 +1,42 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: Yawn
|
||||
text: Build the graph. Share the data.
|
||||
tagline: A worker-native WebGPU renderer where infrequent lifecycle commands use messages and hot render data lives in SIMD-aligned shared memory.
|
||||
text: Shared render data and a render graph.
|
||||
tagline: Two core files, one fixed arena, no built-in scene model or shader.
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Build your first scene
|
||||
link: /guide/first-scene
|
||||
- theme: alt
|
||||
text: Open the playground
|
||||
link: /../playground/
|
||||
|
||||
link: /playground
|
||||
features:
|
||||
- title: One graph boundary
|
||||
details: JSO, a fluent builder, and FXNode all export the same immutable DAG AST and S-expression wire format.
|
||||
- title: Shared render data
|
||||
details: Meshes, instances, camera state, materials, and user columns use aligned SOA rows backed by shared WASM memory.
|
||||
- title: External programs
|
||||
details: WGSL, render pipelines, and compute passes travel with a graph loadout; core ships no scene shader.
|
||||
- title: Worker-native
|
||||
details: The same core client runs on the browser main thread or another worker through a Worker-like endpoint.
|
||||
- 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: Graph compilation culls dead work, aliases compatible transients, coalesces passes, and prepares resources before activation.
|
||||
- title: Optional conveniences
|
||||
details: glTF import, mesh handles, material properties, camera controls, and picking stay in focused addons.
|
||||
details: Pipelines, GPU resources, pass order, and compatible transient aliases are prepared before activation.
|
||||
---
|
||||
|
||||
<Playground
|
||||
id="first-scene"
|
||||
title="Your first Yawn scene"
|
||||
description="The preview imports procedural glTF through a worker, activates a graph, and renders shared instance data."
|
||||
/>
|
||||
## The entire boundary
|
||||
|
||||
```js
|
||||
const color = await core.allocateRows({
|
||||
name: "triangle.color",
|
||||
rows: 1,
|
||||
stride: 16,
|
||||
format: "f32",
|
||||
});
|
||||
|
||||
color.write(0, [0.2, 0.65, 1, 1]);
|
||||
color.row(0)[0] = 0.8; // direct SharedArrayBuffer write
|
||||
await loadGraph(core, graph); // infrequent message
|
||||
```
|
||||
|
||||
`@yawn/core` contains only the public shared-row client and its worker. The worker owns the fixed 64-byte-aligned arena, S-expression graph compiler, WebGPU loadout, and transient texture aliasing. Every scene convention and every byte of WGSL comes from an addon or application.
|
||||
|
||||
```text
|
||||
JSO / FXNode ──▶ AST ──▶ S-expression ──▶ core worker ──▶ 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,66 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,55 +0,0 @@
|
||||
# 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);
|
||||
```
|
||||
@@ -1,26 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,59 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,78 +0,0 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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.
|
||||
|
||||
<Playground />
|
||||
|
||||
<script setup>
|
||||
import Playground from "./.vitepress/Playground.vue";
|
||||
</script>
|
||||
@@ -1,82 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,101 +0,0 @@
|
||||
# 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."
|
||||
/>
|
||||
@@ -1,54 +0,0 @@
|
||||
# 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" />
|
||||
@@ -1,91 +0,0 @@
|
||||
# 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