Strip core to render data and render graphs
Move glTF, picking, camera controls, and conventional handles into addons. Keep camera and material mutations in SIMD-aligned shared SOA rows and synchronize material updates directly into GPU buffers. 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:
@@ -11,9 +11,3 @@ export async function compileAndSwitch(core, graph) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Return to clear-only mode before releasing an active compiled loadout. */
|
||||
export async function dropActiveGraph(core, compiled) {
|
||||
await core.switchToImmediate();
|
||||
await core.dropCompiledGraph(compiled.compiledId);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { MeshHandles } from "@yawn/mesh-handles";
|
||||
|
||||
/** Query the optional snapshot/BVH worker and receive wrapped instance handles. */
|
||||
export function pickNearest(core, origin, direction) {
|
||||
return new MeshHandles(core).pickRay(origin, direction, {
|
||||
export function pickNearest(meshHandles, origin, direction) {
|
||||
return meshHandles.pickRay(origin, direction, {
|
||||
maxDistance: 10_000,
|
||||
maxHits: 1,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ export async function connectFromWorker(port, options = {}) {
|
||||
worker: port,
|
||||
memory: options.memory,
|
||||
ringPtr: options.ringPtr,
|
||||
pickingWorkerFactory: options.pickingWorkerFactory,
|
||||
free: options.free,
|
||||
});
|
||||
await core.ready;
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { culling } from "../render-graph-studio/render-graph/presets.js";
|
||||
import { importGltf } from "./09-gltf-import-worker.js";
|
||||
import { compileAndSwitch, dropActiveGraph } from "./08-compile-and-switch.js";
|
||||
import { compileAndSwitch } from "./08-compile-and-switch.js";
|
||||
|
||||
/** Combine the complete JSO graph, shared glTF import, and mesh-handle facade. */
|
||||
export async function loadCompleteScene(core, gltfUrl) {
|
||||
const meshes = await importGltf(core, gltfUrl);
|
||||
const compiled = await compileAndSwitch(core, culling);
|
||||
try {
|
||||
const meshes = await importGltf(core, gltfUrl);
|
||||
return {
|
||||
compiled,
|
||||
meshes,
|
||||
dispose: () => dropActiveGraph(core, compiled),
|
||||
};
|
||||
} catch (error) {
|
||||
await dropActiveGraph(core, compiled).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
return { compiled, meshes };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
const MIN_DISTANCE = 0.1;
|
||||
const MAX_PITCH = Math.PI / 2 - 0.01;
|
||||
|
||||
// The camera is ordinary render data, not a core API. This example maps browser
|
||||
// controls straight onto its packed SOA row without sending input messages.
|
||||
export function installCameraRenderDataControls(core, canvas) {
|
||||
const camera = core.array("camera.state");
|
||||
const abort = new AbortController();
|
||||
const options = { signal: abort.signal };
|
||||
const write = (state) => camera.write(0, state);
|
||||
|
||||
canvas.addEventListener(
|
||||
"pointerdown",
|
||||
(event) => {
|
||||
if (event.pointerType === "mouse" && (event.button === 1 || event.button === 2)) {
|
||||
event.preventDefault();
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
}
|
||||
},
|
||||
options,
|
||||
);
|
||||
canvas.addEventListener(
|
||||
"pointermove",
|
||||
(event) => {
|
||||
if (
|
||||
event.pointerType !== "mouse" ||
|
||||
!canvas.hasPointerCapture(event.pointerId) ||
|
||||
(event.buttons & 6) === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const state = camera.read(0);
|
||||
const offset = [state[0] - state[4], state[1] - state[5], state[2] - state[6]];
|
||||
const distance = Math.hypot(...offset);
|
||||
if ((event.buttons & 4) !== 0) {
|
||||
const yaw = Math.atan2(offset[0], offset[2]) + event.movementX * 0.005;
|
||||
const pitch = Math.max(
|
||||
-MAX_PITCH,
|
||||
Math.min(
|
||||
MAX_PITCH,
|
||||
Math.asin(offset[1] / distance) + event.movementY * 0.005,
|
||||
),
|
||||
);
|
||||
const horizontal = Math.cos(pitch) * distance;
|
||||
state[0] = state[4] + Math.sin(yaw) * horizontal;
|
||||
state[1] = state[5] + Math.sin(pitch) * distance;
|
||||
state[2] = state[6] + Math.cos(yaw) * horizontal;
|
||||
} else {
|
||||
const forward = [
|
||||
(state[4] - state[0]) / distance,
|
||||
(state[5] - state[1]) / distance,
|
||||
(state[6] - state[2]) / distance,
|
||||
];
|
||||
const right = [
|
||||
forward[1] * state[10] - forward[2] * state[9],
|
||||
forward[2] * state[8] - forward[0] * state[10],
|
||||
forward[0] * state[9] - forward[1] * state[8],
|
||||
];
|
||||
const rightLength = Math.hypot(...right);
|
||||
right.forEach((value, index) => (right[index] = value / rightLength));
|
||||
const up = [
|
||||
right[1] * forward[2] - right[2] * forward[1],
|
||||
right[2] * forward[0] - right[0] * forward[2],
|
||||
right[0] * forward[1] - right[1] * forward[0],
|
||||
];
|
||||
const units =
|
||||
(2 * distance * Math.tan(state[12] * 0.5)) /
|
||||
Math.max(1, canvas.clientHeight);
|
||||
const translation = right.map(
|
||||
(value, index) =>
|
||||
-value * event.movementX * units + up[index] * event.movementY * units,
|
||||
);
|
||||
for (let index = 0; index < 3; index++) {
|
||||
state[index] += translation[index];
|
||||
state[index + 4] += translation[index];
|
||||
}
|
||||
}
|
||||
write(state);
|
||||
},
|
||||
options,
|
||||
);
|
||||
for (const type of ["pointerup", "pointercancel"]) {
|
||||
canvas.addEventListener(
|
||||
type,
|
||||
(event) => {
|
||||
if (canvas.hasPointerCapture(event.pointerId)) {
|
||||
canvas.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
canvas.addEventListener(
|
||||
"wheel",
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
const delta =
|
||||
event.deltaMode === WheelEvent.DOM_DELTA_LINE
|
||||
? event.deltaY * 16
|
||||
: event.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
||||
? event.deltaY * canvas.clientHeight
|
||||
: event.deltaY;
|
||||
if (!Number.isFinite(delta) || delta === 0) return;
|
||||
const state = camera.read(0);
|
||||
const offset = [state[0] - state[4], state[1] - state[5], state[2] - state[6]];
|
||||
const distance = Math.hypot(...offset);
|
||||
const nextDistance = Math.max(
|
||||
MIN_DISTANCE,
|
||||
Math.min(state[15] * 0.95, distance * Math.exp(0.002 * delta)),
|
||||
);
|
||||
for (let index = 0; index < 3; index++) {
|
||||
state[index] = state[index + 4] + offset[index] * (nextDistance / distance);
|
||||
}
|
||||
write(state);
|
||||
},
|
||||
{ ...options, passive: false },
|
||||
);
|
||||
canvas.addEventListener("contextmenu", (event) => event.preventDefault(), options);
|
||||
|
||||
return () => abort.abort();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { CameraHandle, MaterialHandles, MeshHandles } from "@yawn/mesh-handles";
|
||||
|
||||
/** Wrap one import result in the conventional object API without hiding core. */
|
||||
export function conventionalSceneHandles(core, imported) {
|
||||
return {
|
||||
camera: new CameraHandle(core),
|
||||
meshes: new MeshHandles(core).fromImportedScene(imported),
|
||||
materials: new MaterialHandles(core).fromImportedScene(imported),
|
||||
};
|
||||
}
|
||||
|
||||
/** Property assignments remain direct SAB writes; no camera/material message is sent. */
|
||||
export function restyleScene({ camera, materials }) {
|
||||
camera.lookAt([4, 3, 6], [0, 0, 0]);
|
||||
if (materials[0]) {
|
||||
materials[0].baseColor = [0.2, 0.55, 1, 1];
|
||||
materials[0].roughness = 0.35;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ Import the function you need and pass the same `YawnCore` instance to every addo
|
||||
| `05-default-pipelines.js` | Attaching optional external WGSL declarations |
|
||||
| `06-custom-render-pipeline.js` | Supplying a custom scene render shader |
|
||||
| `07-compute-pipeline.js` | Supplying a binding-free compute pass |
|
||||
| `08-compile-and-switch.js` | Compiling, activating, and safely cleaning up a graph |
|
||||
| `08-compile-and-switch.js` | Compiling and transactionally activating a graph |
|
||||
| `09-gltf-import-worker.js` | Fetching a glTF URL into shared memory from a worker |
|
||||
| `10-mesh-instances.js` | Creating and mutating conventional instance handles |
|
||||
| `11-custom-soa-column.js` | Allocating an instance-sized shared SOA column |
|
||||
@@ -20,6 +20,8 @@ Import the function you need and pass the same `YawnCore` instance to every addo
|
||||
| `13-bvh-picking.js` | Ray picking through the mesh-handles addon |
|
||||
| `14-worker-to-worker.js` | Using core from another worker through a `MessagePort` |
|
||||
| `15-complete-scene.js` | Combining the graph, glTF, and mesh addons |
|
||||
| `16-camera-render-data.js` | Treating camera input as direct render-data SAB writes |
|
||||
| `17-conventional-handles.js` | Conventional camera and material properties over SAB rows |
|
||||
|
||||
Recipes 1–7 isolate graph authoring concepts, so their ASTs are intentionally
|
||||
fragments rather than complete renderable loadouts. Recipe 15 uses the complete
|
||||
|
||||
@@ -13,3 +13,5 @@ export * from "./12-direct-sab-animation.js";
|
||||
export * from "./13-bvh-picking.js";
|
||||
export * from "./14-worker-to-worker.js";
|
||||
export * from "./15-complete-scene.js";
|
||||
export * from "./16-camera-render-data.js";
|
||||
export * from "./17-conventional-handles.js";
|
||||
|
||||
Reference in New Issue
Block a user