Add interactive tutorial playgrounds
Embed editable split-view examples throughout the guide, restore the full LFS-backed Sponza demo, batch glTF hydration, honor hierarchical transforms, and report transformed BVH picks with a live FPS counter. 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:
+298
-72
@@ -1,98 +1,324 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import {
|
||||
ComputePass,
|
||||
FXAA,
|
||||
Mesh,
|
||||
PBRMaterial,
|
||||
PointLight,
|
||||
Scene,
|
||||
} from "@yawn/handles";
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import * as Handles from "@yawn/handles";
|
||||
import { YawnCore } from "@yawn/core";
|
||||
import { playgrounds } from "./playgrounds";
|
||||
|
||||
const props = defineProps({ example: { type: String, default: "triangle" } });
|
||||
|
||||
const preset = playgrounds[props.example] ?? playgrounds.triangle;
|
||||
const canvas = ref();
|
||||
const source = ref(preset.code);
|
||||
const status = ref("Starting…");
|
||||
const output = ref([]);
|
||||
const failed = ref(false);
|
||||
let scene;
|
||||
let accent;
|
||||
const running = ref(false);
|
||||
const canvasKey = ref(0);
|
||||
const fps = ref(0);
|
||||
let generation = 0;
|
||||
let current;
|
||||
let fpsFrame = 0;
|
||||
let sampledFrame = 0;
|
||||
let sampledAt = 0;
|
||||
|
||||
function move(event) {
|
||||
if (!accent) return;
|
||||
const bounds = canvas.value.getBoundingClientRect();
|
||||
const row = accent.row(0);
|
||||
row[0] = (event.clientX - bounds.left) / bounds.width;
|
||||
row[1] = 1 - (event.clientY - bounds.top) / bounds.height;
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
const api = { ...Handles, YawnCore };
|
||||
|
||||
async function dispose(value = current) {
|
||||
if (!value) return;
|
||||
current = undefined;
|
||||
delete window.__yawnPlayground;
|
||||
if (typeof value === "function") await value();
|
||||
else if (typeof value.dispose === "function") await value.dispose();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
async function run() {
|
||||
const runId = ++generation;
|
||||
running.value = true;
|
||||
failed.value = false;
|
||||
output.value = [];
|
||||
status.value = "Running…";
|
||||
try {
|
||||
if (!crossOriginIsolated) throw new Error("Cross-origin isolation is disabled");
|
||||
await dispose();
|
||||
if (!crossOriginIsolated)
|
||||
throw new Error("Cross-origin isolation is disabled");
|
||||
canvasKey.value++;
|
||||
await nextTick();
|
||||
canvas.value.width = 960;
|
||||
canvas.value.height = 540;
|
||||
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,
|
||||
});
|
||||
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);
|
||||
const executable = source.value.replace(/^\s*import\s+[^;]+;\s*$/gm, "");
|
||||
const names = Object.keys(api);
|
||||
const started = performance.now();
|
||||
const result = await new AsyncFunction(
|
||||
...names,
|
||||
"canvas",
|
||||
"log",
|
||||
executable,
|
||||
)(...names.map((name) => api[name]), canvas.value, (message) =>
|
||||
output.value.push(String(message)),
|
||||
);
|
||||
if (runId !== generation) {
|
||||
await dispose(result);
|
||||
return;
|
||||
}
|
||||
|
||||
window.__yawnPlayground = { scene, mesh, material, accent };
|
||||
status.value = `${props.example} running · pointer movement writes sceneAccent in the SAB`;
|
||||
current = result;
|
||||
window.__yawnPlayground = result;
|
||||
status.value = `Running · ${Math.round(performance.now() - started)} ms`;
|
||||
} catch (error) {
|
||||
failed.value = true;
|
||||
status.value = error.message;
|
||||
status.value = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
if (runId === generation) running.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
source.value = preset.code;
|
||||
run();
|
||||
}
|
||||
|
||||
function tab(event) {
|
||||
if (event.key !== "Tab") return;
|
||||
event.preventDefault();
|
||||
const editor = event.currentTarget;
|
||||
const start = editor.selectionStart;
|
||||
source.value = `${source.value.slice(0, start)} ${source.value.slice(editor.selectionEnd)}`;
|
||||
requestAnimationFrame(() => editor.setSelectionRange(start + 2, start + 2));
|
||||
}
|
||||
|
||||
function sampleFps(time = performance.now()) {
|
||||
try {
|
||||
const core = current?.scene?.core ?? current?.core;
|
||||
if (!core) throw new Error();
|
||||
const frame = Number(core.array("info").row(0)[1]);
|
||||
if (frame < sampledFrame) sampledAt = 0;
|
||||
if (!sampledAt) {
|
||||
sampledFrame = frame;
|
||||
sampledAt = time;
|
||||
} else if (time - sampledAt >= 500) {
|
||||
fps.value = Math.round(
|
||||
((frame - sampledFrame) * 1000) / (time - sampledAt),
|
||||
);
|
||||
sampledFrame = frame;
|
||||
sampledAt = time;
|
||||
}
|
||||
} catch {
|
||||
fps.value = 0;
|
||||
sampledFrame = 0;
|
||||
sampledAt = time;
|
||||
}
|
||||
fpsFrame = requestAnimationFrame(sampleFps);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
sampleFps();
|
||||
run();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
delete window.__yawnPlayground;
|
||||
scene?.dispose();
|
||||
generation++;
|
||||
cancelAnimationFrame(fpsFrame);
|
||||
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>
|
||||
<section class="playground" :aria-label="`${preset.title} playground`">
|
||||
<header>
|
||||
<strong>{{ preset.title }}</strong>
|
||||
<span :class="{ failed }" data-playground-status>{{ status }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
:disabled="running"
|
||||
@click="reset"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button type="button" :disabled="running" @click="run">
|
||||
{{ running ? "Running…" : "Run" }}
|
||||
</button>
|
||||
</header>
|
||||
<div class="workspace">
|
||||
<textarea
|
||||
v-model="source"
|
||||
aria-label="Editable playground code"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
@keydown="tab"
|
||||
/>
|
||||
<div class="preview">
|
||||
<canvas :key="canvasKey" ref="canvas" aria-label="Yawn WebGPU output" />
|
||||
<div v-if="output.length" class="output" data-playground-log>
|
||||
<div v-for="(line, index) in output" :key="index">{{ line }}</div>
|
||||
</div>
|
||||
<div class="fps" data-playground-fps>{{ fps }} FPS</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</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); }
|
||||
.playground {
|
||||
position: relative;
|
||||
left: 50%;
|
||||
width: min(1120px, calc(100vw - 64px));
|
||||
margin: 28px 0 36px;
|
||||
overflow: hidden;
|
||||
transform: translateX(-50%);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background: #0b1020;
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
}
|
||||
:global(.VPContent.has-sidebar .playground) {
|
||||
left: calc(50% + var(--vp-sidebar-width) / 2);
|
||||
width: min(1120px, calc(100vw - var(--vp-sidebar-width) - 64px));
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
:global(.VPDoc.has-sidebar.has-aside .playground) {
|
||||
left: 50%;
|
||||
width: min(1120px, calc(100vw - var(--vp-sidebar-width) - 288px));
|
||||
}
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
min-height: 46px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 7px 10px 7px 16px;
|
||||
color: #dbeafe;
|
||||
border-bottom: 1px solid #263249;
|
||||
background: #111827;
|
||||
}
|
||||
header strong {
|
||||
white-space: nowrap;
|
||||
}
|
||||
header span {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: #94a3b8;
|
||||
font:
|
||||
12px/1.4 ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
monospace;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
button {
|
||||
padding: 6px 14px;
|
||||
color: white;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: #2563eb;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
button.secondary {
|
||||
color: #cbd5e1;
|
||||
background: #293449;
|
||||
}
|
||||
button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.55;
|
||||
}
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
min-height: 510px;
|
||||
}
|
||||
textarea {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 510px;
|
||||
resize: none;
|
||||
padding: 18px;
|
||||
color: #dbeafe;
|
||||
border: 0;
|
||||
border-right: 1px solid #263249;
|
||||
outline: none;
|
||||
background: #0b1020;
|
||||
tab-size: 2;
|
||||
font:
|
||||
13px/1.55 ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
Consolas,
|
||||
monospace;
|
||||
}
|
||||
textarea:focus {
|
||||
box-shadow: inset 0 0 0 2px #2563eb;
|
||||
}
|
||||
.preview {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: #050914;
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
.output {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 48px;
|
||||
left: 12px;
|
||||
max-height: 30%;
|
||||
overflow: auto;
|
||||
padding: 8px 10px;
|
||||
color: #bfdbfe;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
background: rgb(2 6 23 / 82%);
|
||||
font:
|
||||
11px/1.45 ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
monospace;
|
||||
}
|
||||
.fps {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
padding: 5px 9px;
|
||||
color: #dbeafe;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 999px;
|
||||
background: rgb(2 6 23 / 82%);
|
||||
font:
|
||||
700 12px/1 ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
monospace;
|
||||
}
|
||||
.failed {
|
||||
color: #fca5a5;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.playground,
|
||||
:global(.VPContent.has-sidebar .playground) {
|
||||
left: auto;
|
||||
width: 100%;
|
||||
transform: none;
|
||||
}
|
||||
.workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
textarea {
|
||||
height: 360px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #263249;
|
||||
}
|
||||
.preview {
|
||||
min-height: 360px;
|
||||
}
|
||||
header strong {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
const triangle = `import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true });
|
||||
await scene.ready;
|
||||
|
||||
const material = new PBRMaterial(scene, {
|
||||
baseColor: [0.15, 0.55, 1, 1],
|
||||
metallic: 0.15,
|
||||
roughness: 0.4,
|
||||
});
|
||||
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;
|
||||
|
||||
log("Move the pointer to write sceneAccent directly in the SAB.");
|
||||
const move = (event) => {
|
||||
const bounds = canvas.getBoundingClientRect();
|
||||
scene.array("sceneAccent").row(0).set([
|
||||
(event.clientX - bounds.left) / bounds.width,
|
||||
1 - (event.clientY - bounds.top) / bounds.height,
|
||||
1,
|
||||
1,
|
||||
]);
|
||||
};
|
||||
canvas.addEventListener("pointermove", move);
|
||||
|
||||
return {
|
||||
scene,
|
||||
mesh,
|
||||
dispose() {
|
||||
canvas.removeEventListener("pointermove", move);
|
||||
scene.dispose();
|
||||
},
|
||||
};`;
|
||||
|
||||
const sab = `import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true });
|
||||
await scene.ready;
|
||||
const material = new PBRMaterial(scene, { baseColor: [0.2, 0.9, 0.65, 1] });
|
||||
await material.ready;
|
||||
const mesh = new Mesh(scene, {
|
||||
material,
|
||||
vertexData: {
|
||||
positions: [-0.35, -0.35, 0, 0.35, -0.35, 0, 0, 0.42, 0],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
});
|
||||
await mesh.ready;
|
||||
|
||||
const velocity = await scene.ensureRows("app.velocity", 1, 16, "f32");
|
||||
velocity.row(0).set([0.8, 0, 0, 0]);
|
||||
log("Pointer movement mutates nodePositions; no worker message is sent.");
|
||||
|
||||
const move = (event) => {
|
||||
const bounds = canvas.getBoundingClientRect();
|
||||
mesh.position[0] = ((event.clientX - bounds.left) / bounds.width - 0.5) * 1.4;
|
||||
mesh.position[1] = (0.5 - (event.clientY - bounds.top) / bounds.height) * 1.2;
|
||||
};
|
||||
canvas.addEventListener("pointermove", move);
|
||||
|
||||
return {
|
||||
scene,
|
||||
mesh,
|
||||
dispose() {
|
||||
canvas.removeEventListener("pointermove", move);
|
||||
scene.dispose();
|
||||
},
|
||||
};`;
|
||||
|
||||
const cameras = `import { ArcRotateCamera, Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true });
|
||||
await scene.ready;
|
||||
const material = new PBRMaterial(scene, { baseColor: [0.9, 0.3, 0.18, 1] });
|
||||
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;
|
||||
|
||||
const camera = new ArcRotateCamera(scene, {
|
||||
target: mesh,
|
||||
alpha: 0,
|
||||
beta: Math.PI / 2,
|
||||
radius: 3,
|
||||
fov: Math.PI / 3,
|
||||
near: 0.05,
|
||||
far: 100,
|
||||
aspect: canvas.width / canvas.height,
|
||||
controls: { element: canvas, pointer: true, controller: true },
|
||||
});
|
||||
await camera.ready;
|
||||
log("Drag to orbit, right-drag to pan, and wheel to zoom.");
|
||||
|
||||
return {
|
||||
scene,
|
||||
mesh,
|
||||
camera,
|
||||
async dispose() {
|
||||
await camera.dispose();
|
||||
scene.dispose();
|
||||
},
|
||||
};`;
|
||||
|
||||
const instances = `import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true });
|
||||
await scene.ready;
|
||||
const material = new PBRMaterial(scene, { baseColor: [0.25, 0.8, 1, 1] });
|
||||
await material.ready;
|
||||
const source = new Mesh(scene, {
|
||||
position: [-0.48, 0, 0],
|
||||
scale: [0.58, 0.58, 1],
|
||||
material,
|
||||
vertexData: {
|
||||
positions: [-0.7, -0.6, 0, 0.7, -0.6, 0, 0, 0.72, 0],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
});
|
||||
await source.ready;
|
||||
|
||||
const instance = source.clone({ position: [0.48, 0, 0], scale: [0.58, 0.58, 1] });
|
||||
await instance.ready;
|
||||
log(\`Two mesh handles share geometry #\${source.geometryId}.\`);
|
||||
|
||||
return { scene, source, instance, dispose: () => scene.dispose() };`;
|
||||
|
||||
const materials = `import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true });
|
||||
await scene.ready;
|
||||
const paint = new PBRMaterial(scene, {
|
||||
baseColor: [0.85, 0.08, 0.18, 1],
|
||||
metallic: 0.75,
|
||||
roughness: 0.18,
|
||||
});
|
||||
await paint.ready;
|
||||
const mesh = new Mesh(scene, {
|
||||
material: paint,
|
||||
vertexData: {
|
||||
positions: [-0.7, -0.6, 0, 0.7, -0.6, 0, 0, 0.72, 0],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
});
|
||||
await mesh.ready;
|
||||
|
||||
log("Move horizontally to mutate material roughness in shared memory.");
|
||||
const move = (event) => {
|
||||
const bounds = canvas.getBoundingClientRect();
|
||||
paint.roughness = (event.clientX - bounds.left) / bounds.width;
|
||||
};
|
||||
canvas.addEventListener("pointermove", move);
|
||||
|
||||
return {
|
||||
scene,
|
||||
mesh,
|
||||
paint,
|
||||
dispose() {
|
||||
canvas.removeEventListener("pointermove", move);
|
||||
scene.dispose();
|
||||
},
|
||||
};`;
|
||||
|
||||
const lights = `import { AmbientLight, Mesh, PBRMaterial, PointLight, Scene } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true });
|
||||
await scene.ready;
|
||||
const material = new PBRMaterial(scene, { baseColor: [1, 0.45, 0.08, 1], roughness: 0.35 });
|
||||
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;
|
||||
|
||||
const point = new PointLight(scene, {
|
||||
position: [0, 0.5, 0.5],
|
||||
color: [1, 0.18, 0.04],
|
||||
intensity: 12,
|
||||
range: 8,
|
||||
});
|
||||
const ambient = new AmbientLight(scene, { color: [0.04, 0.12, 0.3], intensity: 0.35 });
|
||||
await Promise.all([point.ready, ambient.ready]);
|
||||
log("Point and ambient rows are consumed by the clustered compute pass.");
|
||||
|
||||
return { scene, mesh, point, ambient, dispose: () => scene.dispose() };`;
|
||||
|
||||
const compute = `import { ComputePass, Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true });
|
||||
await scene.ready;
|
||||
const material = new PBRMaterial(scene, { baseColor: [0.15, 0.75, 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.72, 0],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
});
|
||||
await mesh.ready;
|
||||
|
||||
const values = await scene.ensureRows("simulation.values", 1, 16, "u32");
|
||||
const simulation = new ComputePass({
|
||||
id: "increment",
|
||||
code: "@group(0) @binding(0) var<storage, read_write> values: array<u32>; @compute @workgroup_size(1) fn main() { values[0] += 1u; }",
|
||||
buffers: [{ id: "simulation-values", array: "simulation.values", usage: ["storage"] }],
|
||||
bindings: [{ group: 0, binding: 0, resource: "simulation-values" }],
|
||||
});
|
||||
await scene.addComputePass(simulation);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
log(\`Compute wrote \${values.row(0)[0]} into the SAB-backed buffer.\`);
|
||||
|
||||
return { scene, mesh, simulation, dispose: () => scene.dispose() };`;
|
||||
|
||||
const post = `import { ColorGrading, DynamicExposure, FXAA, Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true });
|
||||
await scene.ready;
|
||||
const material = new PBRMaterial(scene, { baseColor: [0.8, 0.18, 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.72, 0],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
});
|
||||
await mesh.ready;
|
||||
|
||||
let exposure, grade, fxaa;
|
||||
await scene.batchGraphUpdates(async () => {
|
||||
exposure = new DynamicExposure(scene, { exposure: 1.25 });
|
||||
grade = new ColorGrading(scene, { toneMap: "aces", amount: 1 });
|
||||
fxaa = new FXAA(scene);
|
||||
await Promise.all([exposure.ready, grade.ready, fxaa.ready]);
|
||||
});
|
||||
log("HDR → exposure → color grading → FXAA → canvas");
|
||||
|
||||
return { scene, mesh, exposure, grade, fxaa, dispose: () => scene.dispose() };`;
|
||||
|
||||
const importing = `import { AmbientLight, ArcRotateCamera, Picking, Scene, importGltf } from "@yawn/handles";
|
||||
|
||||
const scene = new Scene(canvas, { hdr: true, fps: 1, arenaBytes: 384 * 1024 * 1024 });
|
||||
await scene.ready;
|
||||
await scene.core.pause();
|
||||
log("Importing /models/sponza.glb in the importer worker…");
|
||||
const meshes = await importGltf(scene, "/models/sponza.glb");
|
||||
|
||||
const minimum = [Infinity, Infinity, Infinity];
|
||||
const maximum = [-Infinity, -Infinity, -Infinity];
|
||||
const rotate = (q, v) => {
|
||||
const t = [
|
||||
2 * (q[1] * v[2] - q[2] * v[1]),
|
||||
2 * (q[2] * v[0] - q[0] * v[2]),
|
||||
2 * (q[0] * v[1] - q[1] * v[0]),
|
||||
];
|
||||
return [
|
||||
v[0] + q[3] * t[0] + q[1] * t[2] - q[2] * t[1],
|
||||
v[1] + q[3] * t[1] + q[2] * t[0] - q[0] * t[2],
|
||||
v[2] + q[3] * t[2] + q[0] * t[1] - q[1] * t[0],
|
||||
];
|
||||
};
|
||||
const worldBounds = (mesh) => {
|
||||
const bounds = scene.array("bounds").row(mesh.id);
|
||||
const low = [Infinity, Infinity, Infinity];
|
||||
const high = [-Infinity, -Infinity, -Infinity];
|
||||
for (let corner = 0; corner < 8; corner++) {
|
||||
const local = [0, 1, 2].map((lane) =>
|
||||
bounds[(corner & (1 << lane) ? 4 : 0) + lane] * mesh.scale[lane]);
|
||||
const point = rotate(mesh.quaternion, local).map((value, lane) => value + mesh.position[lane]);
|
||||
for (let lane = 0; lane < 3; lane++) {
|
||||
low[lane] = Math.min(low[lane], point[lane]);
|
||||
high[lane] = Math.max(high[lane], point[lane]);
|
||||
}
|
||||
}
|
||||
return [low, high];
|
||||
};
|
||||
for (const mesh of meshes) {
|
||||
const [low, high] = worldBounds(mesh);
|
||||
for (let lane = 0; lane < 3; lane++) {
|
||||
minimum[lane] = Math.min(minimum[lane], low[lane]);
|
||||
maximum[lane] = Math.max(maximum[lane], high[lane]);
|
||||
}
|
||||
}
|
||||
const center = minimum.map((value, lane) => (value + maximum[lane]) * 0.5);
|
||||
const extent = Math.max(...maximum.map((value, lane) => value - minimum[lane]));
|
||||
const scale = 1.5 / extent;
|
||||
for (const mesh of meshes) {
|
||||
mesh.position = mesh.position.map((value, lane) => (value - center[lane]) * scale);
|
||||
mesh.scale = mesh.scale.map((value) => value * scale);
|
||||
}
|
||||
|
||||
const camera = new ArcRotateCamera(scene, {
|
||||
alpha: 0.7,
|
||||
beta: 1.1,
|
||||
radius: 2.7,
|
||||
aspect: canvas.width / canvas.height,
|
||||
controls: { element: canvas, pointer: true },
|
||||
});
|
||||
await camera.ready;
|
||||
const ambient = new AmbientLight(scene, { color: [0.7, 0.8, 1], intensity: 0.7 });
|
||||
await ambient.ready;
|
||||
|
||||
const picking = new Picking(scene);
|
||||
await picking.ready;
|
||||
const [targetLow, targetHigh] = worldBounds(meshes[0]);
|
||||
const pickTarget = targetLow.map((value, lane) => (value + targetHigh[lane]) * 0.5);
|
||||
const pick = async () => {
|
||||
const origin = Array.from(camera.position);
|
||||
const direction = pickTarget.map((value, lane) => value - origin[lane]);
|
||||
const length = Math.hypot(...direction);
|
||||
const hits = await picking.pick(origin, direction.map((value) => value / length));
|
||||
log(\`Imported \${meshes.length} primitives; BVH ray returned \${hits.length} hit(s).\`);
|
||||
};
|
||||
canvas.addEventListener("click", pick);
|
||||
await pick();
|
||||
const play = setTimeout(() => scene.core.play(), 0);
|
||||
|
||||
return {
|
||||
scene,
|
||||
meshes,
|
||||
camera,
|
||||
picking,
|
||||
async dispose() {
|
||||
clearTimeout(play);
|
||||
canvas.removeEventListener("click", pick);
|
||||
picking.dispose();
|
||||
await camera.dispose();
|
||||
scene.dispose();
|
||||
},
|
||||
};`;
|
||||
|
||||
const core = `import { YawnCore } from "@yawn/core";
|
||||
|
||||
const encode = (value) => {
|
||||
if (value === null || ["boolean", "number"].includes(typeof value)) return String(value);
|
||||
if (typeof value === "string") return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return \`(array\${value.map((item) => \` \${encode(item)}\`).join("")})\`;
|
||||
return \`(object\${Object.keys(value).sort().map((key) =>
|
||||
\` (field \${JSON.stringify(key)} \${encode(value[key])})\`).join("")})\`;
|
||||
};
|
||||
|
||||
const core = new YawnCore(canvas);
|
||||
await core.ready;
|
||||
const accent = await core.createRows({ name: "accent", rows: 1, stride: 16, format: "f32" });
|
||||
accent.write(0, [0.2, 0.75, 1, 1]);
|
||||
|
||||
const code = "struct Out { @builtin(position) position: vec4<f32> }; @group(0) @binding(0) var<uniform> color: vec4<f32>; @vertex fn vertex(@builtin(vertex_index) id: u32) -> Out { let points = array(vec2(-.7,-.6), vec2(.7,-.6), vec2(0.,.72)); var out: Out; out.position = vec4(points[id],0.,1.); return out; } @fragment fn fragment() -> @location(0) vec4<f32> { return color; }";
|
||||
const graph = {
|
||||
id: "direct-core",
|
||||
resources: { buffers: [{ id: "accent", array: "accent", usage: ["uniform"] }], textures: [], samplers: [] },
|
||||
pipelines: { render: [{ id: "triangle", code, vertex: { entry: "vertex" }, fragment: { entry: "fragment", targets: [{ format: "canvas" }] } }], compute: [] },
|
||||
passes: [{ id: "triangle", type: "render", pipeline: "triangle", bindings: [{ group: 0, binding: 0, resource: "accent" }], color: [{ resource: "canvas", clear: [0.01, 0.02, 0.04, 1] }], draw: { vertices: 3 } }],
|
||||
};
|
||||
const id = await core.compileGraph(\`(yawn-graph 1 \${encode(graph)})\`);
|
||||
await core.switchLoadout(id);
|
||||
log("The canvas is rendered by a graph sent directly to the Rust/WASM core.");
|
||||
|
||||
return { core, accent, dispose: () => core.dispose() };`;
|
||||
|
||||
export const playgrounds = {
|
||||
triangle: { title: "First scene", code: triangle },
|
||||
sab: { title: "Direct shared-memory movement", code: sab },
|
||||
cameras: { title: "Arc rotate camera", code: cameras },
|
||||
instances: { title: "Geometry instances", code: instances },
|
||||
materials: { title: "PBR material", code: materials },
|
||||
lights: { title: "Clustered lights", code: lights },
|
||||
compute: { title: "Compute pass", code: compute },
|
||||
post: { title: "HDR post processing", code: post },
|
||||
importing: { title: "glTF import and BVH picking", code: importing },
|
||||
core: { title: "Direct core graph", code: core },
|
||||
};
|
||||
Reference in New Issue
Block a user