Add SAB-backed rotor transforms and smooth camera controls

Amp-Thread-ID: https://ampcode.com/threads/T-01a01ff8-b91f-724f-8952-f07c6b5042fd
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-08-21 05:30:09 +00:00
co-authored by heaust
parent 0e6d7e367c
commit 9ca12c237e
17 changed files with 411 additions and 141 deletions
+162 -12
View File
@@ -2,15 +2,85 @@ import type { Scene } from "./Scene";
export type NodeOptions = { export type NodeOptions = {
position?: ArrayLike<number>; position?: ArrayLike<number>;
quaternion?: ArrayLike<number>; rotor?: ArrayLike<number>;
scale?: ArrayLike<number>; scale?: ArrayLike<number>;
parent?: Node; parent?: Node;
}; };
function vector(value: ArrayLike<number>, width: number, name: string) { function vector(value: ArrayLike<number>, width: number, name: string) {
if (value.length !== width || Array.from(value).some((lane) => !Number.isFinite(lane))) const lanes = Array.from(value);
if (lanes.length !== width || lanes.some((lane) => !Number.isFinite(lane)))
throw new TypeError(name); throw new TypeError(name);
return value; return lanes;
}
/** Three shared transform components. Component writes go straight to the backing SAB row. */
export interface Vector3 extends Iterable<number> {
[lane: number]: number;
readonly length: 3;
x: number;
y: number;
z: number;
toArray(): [number, number, number];
}
/** Four shared rotor components in `[x, y, z, w]` order. */
export interface Rotor extends Iterable<number> {
[lane: number]: number;
readonly length: 4;
x: number;
y: number;
z: number;
w: number;
toArray(): [number, number, number, number];
}
abstract class XYZView {
[lane: number]: number;
abstract readonly length: 3 | 4;
constructor(
private readonly row: () => Float32Array,
private readonly changed: () => void,
) {}
protected read(lane: number) { return this.row()[lane]; }
protected write(lane: number, value: number) {
this.row()[lane] = value;
this.changed();
}
get 0() { return this.read(0); }
set 0(value: number) { this.write(0, value); }
get 1() { return this.read(1); }
set 1(value: number) { this.write(1, value); }
get 2() { return this.read(2); }
set 2(value: number) { this.write(2, value); }
get x() { return this.read(0); }
set x(value: number) { this.write(0, value); }
get y() { return this.read(1); }
set y(value: number) { this.write(1, value); }
get z() { return this.read(2); }
set z(value: number) { this.write(2, value); }
abstract toArray(): number[];
[Symbol.iterator]() { return this.toArray().values(); }
}
class Vector3View extends XYZView implements Vector3 {
readonly length = 3;
toArray(): [number, number, number] { return [this.x, this.y, this.z]; }
}
class RotorView extends XYZView implements Rotor {
readonly length = 4;
get 3() { return this.read(3); }
set 3(value: number) { this.write(3, value); }
get w() { return this.read(3); }
set w(value: number) { this.write(3, value); }
toArray(): [number, number, number, number] {
return [this.x, this.y, this.z, this.w];
}
} }
/** A thin index into transform SOA rows; transform changes never post a worker message. */ /** A thin index into transform SOA rows; transform changes never post a worker message. */
@@ -19,14 +89,26 @@ export class Node {
id = -1; id = -1;
ready: Promise<this>; ready: Promise<this>;
protected disposed = false; protected disposed = false;
readonly #position = new Vector3View(
() => this.#row("nodePositions").subarray(0, 3),
() => this.transformChanged(),
);
readonly #rotor = new RotorView(
() => this.#row("nodeRotors").subarray(0, 4),
() => this.transformChanged(),
);
readonly #scale = new Vector3View(
() => this.#row("nodeScales").subarray(0, 3),
() => this.transformChanged(),
);
constructor(scene: Scene, options: NodeOptions = {}) { constructor(scene: Scene, options: NodeOptions = {}) {
this.scene = scene; this.scene = scene;
this.ready = scene.allocateNode().then((id) => { this.ready = scene.allocateNode().then((id) => {
this.id = id; this.id = id;
if (options.position) this.position = options.position; if (options.position) this.setPosition(options.position);
if (options.quaternion) this.quaternion = options.quaternion; if (options.rotor) this.setRotor(options.rotor);
if (options.scale) this.scale = options.scale; if (options.scale) this.setScale(options.scale);
if (options.parent) this.parent = options.parent; if (options.parent) this.parent = options.parent;
return this; return this;
}); });
@@ -37,14 +119,82 @@ export class Node {
return this.scene.array(name).row(this.id); return this.scene.array(name).row(this.id);
} }
get position(): Float32Array { return this.#row("nodePositions").subarray(0, 3); } get position(): Vector3 { return this.#position; }
set position(value: ArrayLike<number>) { this.#row("nodePositions").set(vector(value, 3, "position")); } set position(value: ArrayLike<number>) { this.setPosition(value); }
get quaternion(): Float32Array { return this.#row("nodeQuaternions").subarray(0, 4); } get rotor(): Rotor { return this.#rotor; }
set quaternion(value: ArrayLike<number>) { this.#row("nodeQuaternions").set(vector(value, 4, "quaternion")); } set rotor(value: ArrayLike<number>) { this.setRotor(value); }
get scale(): Float32Array { return this.#row("nodeScales").subarray(0, 3); } get scale(): Vector3 { return this.#scale; }
set scale(value: ArrayLike<number>) { this.#row("nodeScales").set(vector(value, 3, "scale")); } set scale(value: ArrayLike<number>) { this.setScale(value); }
/** Replaces all position components with one SAB row write. */
setPosition(value: ArrayLike<number>) {
this.#row("nodePositions").set(vector(value, 3, "position"));
this.transformChanged();
return this;
}
/** Replaces all rotor components with one SAB row write. */
setRotor(value: ArrayLike<number>) {
this.#row("nodeRotors").set(vector(value, 4, "rotor"));
this.transformChanged();
return this;
}
/** Replaces all scale components with one SAB row write. */
setScale(value: ArrayLike<number>) {
this.#row("nodeScales").set(vector(value, 3, "scale"));
this.transformChanged();
return this;
}
/** Adds an offset to the shared position. */
translate(offset: ArrayLike<number>) {
const [x, y, z] = vector(offset, 3, "translation");
return this.setPosition([
this.position.x + x,
this.position.y + y,
this.position.z + z,
]);
}
/** Composes a rotor, or an axis and angle in radians, onto the current rotor. */
rotate(rotor: ArrayLike<number>): this;
rotate(axis: ArrayLike<number>, radians: number): this;
rotate(value: ArrayLike<number>, radians?: number) {
let rotation: number[];
if (radians === undefined) {
rotation = vector(value, 4, "rotor");
} else {
if (!Number.isFinite(radians)) throw new TypeError("radians");
const [x, y, z] = vector(value, 3, "axis");
const length = Math.hypot(x, y, z);
if (!length) throw new RangeError("axis");
const sine = Math.sin(radians / 2) / length;
rotation = [x * sine, y * sine, z * sine, Math.cos(radians / 2)];
}
const [ax, ay, az, aw] = this.rotor;
const [bx, by, bz, bw] = rotation;
const result = [
aw * bx + ax * bw + ay * bz - az * by,
aw * by - ax * bz + ay * bw + az * bx,
aw * bz + ax * by - ay * bx + az * bw,
aw * bw - ax * bx - ay * by - az * bz,
];
const length = Math.hypot(...result);
if (!length) throw new RangeError("rotor");
return this.setRotor(result.map((lane) => lane / length));
}
/** Rotates around the node's local X axis. */
rotateX(radians: number) { return this.rotate([1, 0, 0], radians); }
/** Rotates around the node's local Y axis. */
rotateY(radians: number) { return this.rotate([0, 1, 0], radians); }
/** Rotates around the node's local Z axis. */
rotateZ(radians: number) { return this.rotate([0, 0, 1], radians); }
protected transformChanged() {}
get enabled() { return this.#row("nodes")[0] !== 0; } get enabled() { return this.#row("nodes")[0] !== 0; }
set enabled(value: boolean) { this.#row("nodes")[0] = value ? 1 : 0; } set enabled(value: boolean) { this.#row("nodes")[0] = value ? 1 : 0; }
+41 -17
View File
@@ -33,7 +33,7 @@ type TextureState = GraphTexture & {
const rows = [ const rows = [
["nodes", 16, "u32"], ["nodes", 16, "u32"],
["nodePositions", 16, "f32"], ["nodePositions", 16, "f32"],
["nodeQuaternions", 16, "f32"], ["nodeRotors", 16, "f32"],
["nodeScales", 16, "f32"], ["nodeScales", 16, "f32"],
["meshInfo", 16, "u32"], ["meshInfo", 16, "u32"],
["bounds", 32, "f32"], ["bounds", 32, "f32"],
@@ -104,21 +104,21 @@ struct VertexOutput {
@group(0) @binding(0) var<storage, read> clusters: array<u32>; @group(0) @binding(0) var<storage, read> clusters: array<u32>;
@group(0) @binding(1) var<uniform> accent: Accent; @group(0) @binding(1) var<uniform> accent: Accent;
@group(0) @binding(2) var<storage, read> positions: array<vec4<f32>>; @group(0) @binding(2) var<storage, read> positions: array<vec4<f32>>;
@group(0) @binding(3) var<storage, read> quaternions: array<vec4<f32>>; @group(0) @binding(3) var<storage, read> rotors: array<vec4<f32>>;
@group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>; @group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>;
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>; @group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>; @group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
@group(0) @binding(7) var<storage, read> cameraMatrices: array<vec4<f32>>; @group(0) @binding(7) var<storage, read> cameraMatrices: array<vec4<f32>>;
fn rotate(q: vec4<f32>, value: vec3<f32>) -> vec3<f32> { fn rotate(rotor: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
return value + 2.0 * cross(q.xyz, cross(q.xyz, value) + q.w * value); return value + 2.0 * cross(rotor.xyz, cross(rotor.xyz, value) + rotor.w * value);
} }
@vertex @vertex
fn vertex(@location(0) point: vec3<f32>, @builtin(instance_index) packed: u32) -> VertexOutput { fn vertex(@location(0) point: vec3<f32>, @builtin(instance_index) packed: u32) -> VertexOutput {
let instance = packed & 65535u; let instance = packed & 65535u;
let visible = meshInfo[instance * 4u + 2u]; let visible = meshInfo[instance * 4u + 2u];
let transformed = rotate(quaternions[instance], point * scales[instance].xyz) + positions[instance].xyz; let transformed = rotate(rotors[instance], point * scales[instance].xyz) + positions[instance].xyz;
var clip = vec4<f32>(transformed, 1.0); var clip = vec4<f32>(transformed, 1.0);
if (cameraMatrices[4].w != 0.0) { if (cameraMatrices[4].w != 0.0) {
let world = vec4(transformed, 1.0); let world = vec4(transformed, 1.0);
@@ -131,7 +131,7 @@ fn vertex(@location(0) point: vec3<f32>, @builtin(instance_index) packed: u32) -
} }
var output: VertexOutput; var output: VertexOutput;
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, visible != 0u); output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, visible != 0u);
output.normal = normalize(rotate(quaternions[instance], vec3<f32>(0.0, 0.0, 1.0))); output.normal = normalize(rotate(rotors[instance], vec3<f32>(0.0, 0.0, 1.0)));
output.mesh = instance; output.mesh = instance;
output.material = packed >> 16u; output.material = packed >> 16u;
return output; return output;
@@ -168,7 +168,7 @@ struct VertexOutput {
@group(0) @binding(0) var<storage, read> clusters: array<u32>; @group(0) @binding(0) var<storage, read> clusters: array<u32>;
@group(0) @binding(1) var<uniform> accent: vec4<f32>; @group(0) @binding(1) var<uniform> accent: vec4<f32>;
@group(0) @binding(2) var<storage, read> positions: array<vec4<f32>>; @group(0) @binding(2) var<storage, read> positions: array<vec4<f32>>;
@group(0) @binding(3) var<storage, read> quaternions: array<vec4<f32>>; @group(0) @binding(3) var<storage, read> rotors: array<vec4<f32>>;
@group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>; @group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>;
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>; @group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>; @group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
@@ -178,8 +178,8 @@ ${baseTexture ? "@group(1) @binding(1) var baseTexture: texture_2d<f32>;" : ""}
${materialTexture ? "@group(1) @binding(2) var materialTexture: texture_2d<f32>;" : ""} ${materialTexture ? "@group(1) @binding(2) var materialTexture: texture_2d<f32>;" : ""}
${normalTexture ? "@group(1) @binding(3) var normalTexture: texture_2d<f32>;" : ""} ${normalTexture ? "@group(1) @binding(3) var normalTexture: texture_2d<f32>;" : ""}
fn rotate(q: vec4<f32>, value: vec3<f32>) -> vec3<f32> { fn rotate(rotor: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
return value + 2.0 * cross(q.xyz, cross(q.xyz, value) + q.w * value); return value + 2.0 * cross(rotor.xyz, cross(rotor.xyz, value) + rotor.w * value);
} }
@vertex @vertex
@@ -192,7 +192,7 @@ fn vertex(
) -> VertexOutput { ) -> VertexOutput {
let instance = packed & 65535u; let instance = packed & 65535u;
let scale = scales[instance].xyz; let scale = scales[instance].xyz;
let world = rotate(quaternions[instance], point * scale) + positions[instance].xyz; let world = rotate(rotors[instance], point * scale) + positions[instance].xyz;
var clip = vec4<f32>(world, 1.0); var clip = vec4<f32>(world, 1.0);
if (cameraMatrices[4].w != 0.0) { if (cameraMatrices[4].w != 0.0) {
let homogeneous = vec4(world, 1.0); let homogeneous = vec4(world, 1.0);
@@ -206,9 +206,9 @@ fn vertex(
var output: VertexOutput; var output: VertexOutput;
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, meshInfo[instance * 4u + 2u] != 0u); output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, meshInfo[instance * 4u + 2u] != 0u);
output.world = world; output.world = world;
output.normal = normalize(rotate(quaternions[instance], localNormal / scale)); output.normal = normalize(rotate(rotors[instance], localNormal / scale));
output.uv = uv; output.uv = uv;
output.tangent = ${normalTexture ? "vec4(normalize(rotate(quaternions[instance], localTangent.xyz * scale)), localTangent.w)" : "vec4(1.0, 0.0, 0.0, 1.0)"}; output.tangent = ${normalTexture ? "vec4(normalize(rotate(rotors[instance], localTangent.xyz * scale)), localTangent.w)" : "vec4(1.0, 0.0, 0.0, 1.0)"};
output.mesh = instance; output.mesh = instance;
output.material = packed >> 16u; output.material = packed >> 16u;
return output; return output;
@@ -351,6 +351,9 @@ export class Scene {
#nextTexture = 0; #nextTexture = 0;
#graphBatchDepth = 0; #graphBatchDepth = 0;
#graphBatchDirty = false; #graphBatchDirty = false;
#writeBatchDepth = 0;
#writeBatchDirty = false;
#writeBatchBundleDirty = false;
#signals?: Float32Array; #signals?: Float32Array;
#arrays = new WeakMap<object, object>(); #arrays = new WeakMap<object, object>();
#views = new WeakMap<object, object>(); #views = new WeakMap<object, object>();
@@ -378,7 +381,7 @@ export class Scene {
stride: 16, stride: 16,
format: "u32", format: "u32",
}); });
this.core.array("nodeQuaternions").write(0, [0, 0, 0, 1]); this.core.array("nodeRotors").write(0, [0, 0, 0, 1]);
this.core.array("nodeScales").write(0, [1, 1, 1, 0]); this.core.array("nodeScales").write(0, [1, 1, 1, 0]);
this.core.array("sceneAccent").write(0, [0.28, 0.72, 1, 1]); this.core.array("sceneAccent").write(0, [0.28, 0.72, 1, 1]);
const material = await this.core.allocateObject("materials"); const material = await this.core.allocateObject("materials");
@@ -444,11 +447,32 @@ export class Scene {
} }
markDirty(bundle = false) { markDirty(bundle = false) {
if (this.#writeBatchDepth) {
this.#writeBatchDirty = true;
this.#writeBatchBundleDirty ||= bundle;
return;
}
if (!this.#signals) return; if (!this.#signals) return;
this.#signals[5] = 1; this.#signals[5] = 1;
if (bundle) this.#signals[6] = 1; if (bundle) this.#signals[6] = 1;
} }
/** Defers the dirty signal until a synchronous group of SAB writes is complete. */
batchWrites<T>(operation: () => T) {
this.#writeBatchDepth++;
try {
return operation();
} finally {
this.#writeBatchDepth--;
if (!this.#writeBatchDepth && this.#writeBatchDirty) {
const bundle = this.#writeBatchBundleDirty;
this.#writeBatchDirty = false;
this.#writeBatchBundleDirty = false;
this.markDirty(bundle);
}
}
}
async ensureRows( async ensureRows(
name: string, name: string,
rowCount: number, rowCount: number,
@@ -535,7 +559,7 @@ export class Scene {
: []; : [];
}); });
if (growth.length) await this.core.createRowsBatch(growth); if (growth.length) await this.core.createRowsBatch(growth);
this.array("nodeQuaternions").write(id, [0, 0, 0, 1]); this.array("nodeRotors").write(id, [0, 0, 0, 1]);
this.array("nodeScales").write(id, [1, 1, 1, 0]); this.array("nodeScales").write(id, [1, 1, 1, 0]);
this.array("nodes").write(id, [1, 0, 0, 0]); this.array("nodes").write(id, [1, 0, 0, 0]);
return id; return id;
@@ -545,7 +569,7 @@ export class Scene {
for (const name of [ for (const name of [
"nodes", "nodes",
"nodePositions", "nodePositions",
"nodeQuaternions", "nodeRotors",
"nodeScales", "nodeScales",
"meshInfo", "meshInfo",
"bounds", "bounds",
@@ -835,7 +859,7 @@ export class Scene {
} else { } else {
for (const [id, array] of [ for (const [id, array] of [
["node-positions", "nodePositions"], ["node-positions", "nodePositions"],
["node-quaternions", "nodeQuaternions"], ["node-rotors", "nodeRotors"],
["node-scales", "nodeScales"], ["node-scales", "nodeScales"],
["mesh-info", "meshInfo"], ["mesh-info", "meshInfo"],
["materials", "materials"], ["materials", "materials"],
@@ -1014,7 +1038,7 @@ export class Scene {
"clusters", "clusters",
"accent", "accent",
"node-positions", "node-positions",
"node-quaternions", "node-rotors",
"node-scales", "node-scales",
"mesh-info", "mesh-info",
"materials", "materials",
+1 -1
View File
@@ -38,7 +38,7 @@ export class Picking {
"signals", "signals",
"nodes", "nodes",
"nodePositions", "nodePositions",
"nodeQuaternions", "nodeRotors",
"nodeScales", "nodeScales",
"meshInfo", "meshInfo",
"bounds", "bounds",
+13 -13
View File
@@ -52,27 +52,27 @@ function build(boxes: Box[]): Branch | undefined {
}; };
} }
function rotate(quaternion: number[], value: number[]) { function rotate(rotor: number[], value: number[]) {
const [qx, qy, qz, qw] = quaternion; const [rx, ry, rz, rw] = rotor;
const [x, y, z] = value; const [x, y, z] = value;
const tx = 2 * (qy * z - qz * y); const tx = 2 * (ry * z - rz * y);
const ty = 2 * (qz * x - qx * z); const ty = 2 * (rz * x - rx * z);
const tz = 2 * (qx * y - qy * x); const tz = 2 * (rx * y - ry * x);
return [ return [
x + qw * tx + qy * tz - qz * ty, x + rw * tx + ry * tz - rz * ty,
y + qw * ty + qz * tx - qx * tz, y + rw * ty + rz * tx - rx * tz,
z + qw * tz + qx * ty - qy * tx, z + rw * tz + rx * ty - ry * tx,
]; ];
} }
function rebuild() { function rebuild() {
const bounds = view("bounds"); const bounds = view("bounds");
const positions = view("nodePositions"); const positions = view("nodePositions");
const quaternions = view("nodeQuaternions"); const rotors = view("nodeRotors");
const scales = view("nodeScales"); const scales = view("nodeScales");
const meshes = view("meshInfo"); const meshes = view("meshInfo");
const nodes = view("nodes"); const nodes = view("nodes");
if (!bounds || !positions || !quaternions || !scales || !meshes || !nodes) if (!bounds || !positions || !rotors || !scales || !meshes || !nodes)
return; return;
const count = shares.bounds.descriptor.rows; const count = shares.bounds.descriptor.rows;
const boxes: Box[] = []; const boxes: Box[] = [];
@@ -82,8 +82,8 @@ function rebuild() {
const transform = id * 4; const transform = id * 4;
const min = [Infinity, Infinity, Infinity]; const min = [Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity]; const max = [-Infinity, -Infinity, -Infinity];
const quaternion = [0, 1, 2, 3].map((lane) => const rotor = [0, 1, 2, 3].map((lane) =>
Number(quaternions[transform + lane]), Number(rotors[transform + lane]),
); );
for (let corner = 0; corner < 8; corner++) { for (let corner = 0; corner < 8; corner++) {
const local = [0, 1, 2].map( const local = [0, 1, 2].map(
@@ -91,7 +91,7 @@ function rebuild() {
Number(bounds[offset + (corner & (1 << lane) ? 4 : 0) + lane]) * Number(bounds[offset + (corner & (1 << lane) ? 4 : 0) + lane]) *
Number(scales[transform + lane]), Number(scales[transform + lane]),
); );
const rotated = rotate(quaternion, local); const rotated = rotate(rotor, local);
for (let lane = 0; lane < 3; lane++) { for (let lane = 0; lane < 3; lane++) {
const value = rotated[lane] + Number(positions[transform + lane]); const value = rotated[lane] + Number(positions[transform + lane]);
min[lane] = Math.min(min[lane], value); min[lane] = Math.min(min[lane], value);
+80 -27
View File
@@ -46,18 +46,29 @@ export class ArcRotateCamera extends Camera {
} }
get alpha() { return this.cameraRow()[13]; } get alpha() { return this.cameraRow()[13]; }
set alpha(value: number) { this.cameraRow()[13] = value; this.#updateTransform(); } set alpha(value: number) {
const row = this.cameraRow();
this.#updateTransform(() => { row[13] = value; });
}
get beta() { return this.cameraRow()[14]; } get beta() { return this.cameraRow()[14]; }
set beta(value: number) { this.cameraRow()[14] = Math.min(Math.PI - 0.001, Math.max(0.001, value)); this.#updateTransform(); } set beta(value: number) {
const row = this.cameraRow();
this.#updateTransform(() => {
row[14] = Math.min(Math.PI - 0.001, Math.max(0.001, value));
});
}
get radius() { return this.cameraRow()[15]; } get radius() { return this.cameraRow()[15]; }
set radius(value: number) { this.cameraRow()[15] = Math.max(0.01, value); this.#updateTransform(); } set radius(value: number) {
const row = this.cameraRow();
this.#updateTransform(() => { row[15] = Math.max(0.01, value); });
}
get target() { return this.#target; } get target() { return this.#target; }
set target(value: Node | undefined) { set target(value: Node | undefined) {
if (value && (value.scene !== this.scene || value.id < 0)) throw new Error("Target must be a ready Node in this Scene"); if (value && (value.scene !== this.scene || value.id < 0)) throw new Error("Target must be a ready Node in this Scene");
this.#target = value; this.#target = value;
this.cameraRow()[12] = value ? value.id + 1 : 0; const row = this.cameraRow();
this.#updateTransform(); this.#updateTransform(() => { row[12] = value ? value.id + 1 : 0; });
} }
attachControls(controls: ArcRotateControls) { attachControls(controls: ArcRotateControls) {
@@ -67,6 +78,8 @@ export class ArcRotateCamera extends Camera {
controls.element.addEventListener("pointerdown", this.#down); controls.element.addEventListener("pointerdown", this.#down);
controls.element.addEventListener("pointermove", this.#move); controls.element.addEventListener("pointermove", this.#move);
controls.element.addEventListener("pointerup", this.#up); controls.element.addEventListener("pointerup", this.#up);
controls.element.addEventListener("pointercancel", this.#up);
controls.element.addEventListener("contextmenu", this.#contextMenu);
controls.element.addEventListener("wheel", this.#wheel, { passive: false }); controls.element.addEventListener("wheel", this.#wheel, { passive: false });
} }
if (controls.controller) this.#frame = requestAnimationFrame(this.#pollController); if (controls.controller) this.#frame = requestAnimationFrame(this.#pollController);
@@ -79,6 +92,8 @@ export class ArcRotateCamera extends Camera {
element.removeEventListener("pointerdown", this.#down); element.removeEventListener("pointerdown", this.#down);
element.removeEventListener("pointermove", this.#move); element.removeEventListener("pointermove", this.#move);
element.removeEventListener("pointerup", this.#up); element.removeEventListener("pointerup", this.#up);
element.removeEventListener("pointercancel", this.#up);
element.removeEventListener("contextmenu", this.#contextMenu);
element.removeEventListener("wheel", this.#wheel); element.removeEventListener("wheel", this.#wheel);
} }
cancelAnimationFrame(this.#frame); cancelAnimationFrame(this.#frame);
@@ -88,26 +103,38 @@ export class ArcRotateCamera extends Camera {
} }
#targetPosition() { #targetPosition() {
return this.#target ? this.#target.position : this.cameraRow().subarray(16, 19); const row = this.cameraRow();
if (!this.#target) return row.subarray(16, 19);
const position = this.#target.position;
return [
position.x + row[16],
position.y + row[17],
position.z + row[18],
];
} }
#updateTransform() { #updateTransform(change?: () => void) {
if (this.id < 0 || this.cameraId < 0) return; if (this.id < 0 || this.cameraId < 0) return;
const target = this.#targetPosition(); this.batchTransformChanges(() => {
const sinBeta = Math.sin(this.beta); change?.();
this.position = [ const target = this.#targetPosition();
target[0] + this.radius * sinBeta * Math.sin(this.alpha), const sinBeta = Math.sin(this.beta);
target[1] + this.radius * Math.cos(this.beta), this.setPosition([
target[2] + this.radius * sinBeta * Math.cos(this.alpha), target[0] + this.radius * sinBeta * Math.sin(this.alpha),
]; target[1] + this.radius * Math.cos(this.beta),
this.lookAt(target); target[2] + this.radius * sinBeta * Math.cos(this.alpha),
]);
this.lookAt(target);
});
} }
#down = (event: PointerEvent) => { #down = (event: PointerEvent) => {
event.preventDefault();
this.#pointer = { x: event.clientX, y: event.clientY, button: event.button }; this.#pointer = { x: event.clientX, y: event.clientY, button: event.button };
this.#controls?.element.setPointerCapture?.(event.pointerId); this.#controls?.element.setPointerCapture?.(event.pointerId);
}; };
#up = () => { this.#pointer = undefined; }; #up = () => { this.#pointer = undefined; };
#contextMenu = (event: Event) => event.preventDefault();
#move = (event: PointerEvent) => { #move = (event: PointerEvent) => {
if (!this.#pointer || !this.#controls) return; if (!this.#pointer || !this.#controls) return;
const x = event.clientX - this.#pointer.x; const x = event.clientX - this.#pointer.x;
@@ -115,28 +142,54 @@ export class ArcRotateCamera extends Camera {
this.#pointer.x = event.clientX; this.#pointer.x = event.clientX;
this.#pointer.y = event.clientY; this.#pointer.y = event.clientY;
if (this.#pointer.button === 2) { if (this.#pointer.button === 2) {
const target = this.#targetPosition(); const row = this.cameraRow();
target[0] -= x * (this.#controls.panSpeed ?? 0.005); this.#updateTransform(() => {
target[1] += y * (this.#controls.panSpeed ?? 0.005); row[16] -= x * (this.#controls?.panSpeed ?? 0.005);
this.#updateTransform(); row[17] += y * (this.#controls?.panSpeed ?? 0.005);
});
} else { } else {
const speed = this.#controls.orbitSpeed ?? 0.005; const speed = this.#controls.orbitSpeed ?? 0.005;
this.cameraRow()[13] -= x * speed; const row = this.cameraRow();
this.cameraRow()[14] = Math.min(Math.PI - 0.001, Math.max(0.001, this.beta + y * speed)); this.#updateTransform(() => {
this.#updateTransform(); row[13] -= x * speed;
row[14] = Math.min(
Math.PI - 0.001,
Math.max(0.001, row[14] + y * speed),
);
});
} }
}; };
#wheel = (event: WheelEvent) => { #wheel = (event: WheelEvent) => {
event.preventDefault(); event.preventDefault();
this.radius *= Math.exp(event.deltaY * (this.#controls?.zoomSpeed ?? 0.001)); const row = this.cameraRow();
this.#updateTransform(() => {
row[15] = Math.max(
0.01,
row[15] * Math.exp(event.deltaY * (this.#controls?.zoomSpeed ?? 0.001)),
);
});
}; };
#pollController = () => { #pollController = () => {
const pad = navigator.getGamepads?.().find(Boolean); const pad = navigator.getGamepads?.().find(Boolean);
if (pad) { if (pad) {
this.cameraRow()[13] += (pad.axes[2] ?? pad.axes[0] ?? 0) * 0.03; const row = this.cameraRow();
this.cameraRow()[14] = Math.min(Math.PI - 0.001, Math.max(0.001, this.beta + (pad.axes[3] ?? pad.axes[1] ?? 0) * 0.03)); this.#updateTransform(() => {
this.radius += ((pad.buttons[6]?.value ?? 0) - (pad.buttons[7]?.value ?? 0)) * 0.1; row[13] += (pad.axes[2] ?? pad.axes[0] ?? 0) * 0.03;
this.#updateTransform(); row[14] = Math.min(
Math.PI - 0.001,
Math.max(
0.001,
row[14] + (pad.axes[3] ?? pad.axes[1] ?? 0) * 0.03,
),
);
row[15] = Math.max(
0.01,
row[15] +
((pad.buttons[6]?.value ?? 0) -
(pad.buttons[7]?.value ?? 0)) *
0.1,
);
});
} }
this.#frame = requestAnimationFrame(this.#pollController); this.#frame = requestAnimationFrame(this.#pollController);
}; };
+44 -30
View File
@@ -1,15 +1,15 @@
import { Node, type NodeOptions } from "../Node"; import { Node, type NodeOptions } from "../Node";
import type { Scene } from "../Scene"; import type { Scene } from "../Scene";
function rotate(q: ArrayLike<number>, value: number[]) { function rotate(rotor: ArrayLike<number>, value: number[]) {
const [x, y, z] = value; const [x, y, z] = value;
const tx = 2 * (q[1] * z - q[2] * y); const tx = 2 * (rotor[1] * z - rotor[2] * y);
const ty = 2 * (q[2] * x - q[0] * z); const ty = 2 * (rotor[2] * x - rotor[0] * z);
const tz = 2 * (q[0] * y - q[1] * x); const tz = 2 * (rotor[0] * y - rotor[1] * x);
return [ return [
x + q[3] * tx + q[1] * tz - q[2] * ty, x + rotor[3] * tx + rotor[1] * tz - rotor[2] * ty,
y + q[3] * ty + q[2] * tx - q[0] * tz, y + rotor[3] * ty + rotor[2] * tx - rotor[0] * tz,
z + q[3] * tz + q[0] * ty - q[1] * tx, z + rotor[3] * tz + rotor[0] * ty - rotor[1] * tx,
]; ];
} }
@@ -34,6 +34,8 @@ export type CameraOptions = NodeOptions & {
export class Camera extends Node { export class Camera extends Node {
cameraId = -1; cameraId = -1;
#cameraDisposed = false; #cameraDisposed = false;
#transformBatchDepth = 0;
#transformBatchDirty = false;
constructor(scene: Scene, options: CameraOptions = {}) { constructor(scene: Scene, options: CameraOptions = {}) {
super(scene, options); super(scene, options);
@@ -90,43 +92,55 @@ export class Camera extends Node {
get sensorWidth() { return this.cameraRow()[10]; } get sensorWidth() { return this.cameraRow()[10]; }
set sensorWidth(value: number) { this.cameraRow()[10] = value; } set sensorWidth(value: number) { this.cameraRow()[10] = value; }
override get position(): Float32Array { return super.position; }
override set position(value: ArrayLike<number>) {
super.position = value;
this.refreshMatrix();
}
override get quaternion(): Float32Array { return super.quaternion; }
override set quaternion(value: ArrayLike<number>) {
super.quaternion = value;
this.refreshMatrix();
}
lookAt(target: ArrayLike<number>) { lookAt(target: ArrayLike<number>) {
if (target.length !== 3) throw new RangeError("camera target"); if (target.length !== 3) throw new RangeError("camera target");
const position = this.position; const position = this.position;
const x = target[0] - position[0]; const x = target[0] - position.x;
const y = target[1] - position[1]; const y = target[1] - position.y;
const z = target[2] - position[2]; const z = target[2] - position.z;
const length = Math.hypot(x, y, z) || 1; const length = Math.hypot(x, y, z) || 1;
const direction = [x / length, y / length, z / length]; const direction = [x / length, y / length, z / length];
if (direction[2] > 0.999999) this.quaternion = [0, 1, 0, 0]; if (direction[2] > 0.999999) this.setRotor([0, 1, 0, 0]);
else { else {
const q = [direction[1], -direction[0], 0, 1 - direction[2]]; const rotor = [direction[1], -direction[0], 0, 1 - direction[2]];
const qLength = Math.hypot(...q); const rotorLength = Math.hypot(...rotor);
this.quaternion = q.map((lane) => lane / qLength); this.setRotor(rotor.map((lane) => lane / rotorLength));
} }
return this; return this;
} }
protected override transformChanged() {
if (this.#transformBatchDepth) {
this.#transformBatchDirty = true;
return;
}
this.refreshMatrix();
}
/** Publishes one final camera matrix after a synchronous group of transform writes. */
protected batchTransformChanges<T>(operation: () => T) {
return this.scene.batchWrites(() => {
this.#transformBatchDepth++;
try {
return operation();
} finally {
this.#transformBatchDepth--;
if (!this.#transformBatchDepth && this.#transformBatchDirty) {
this.#transformBatchDirty = false;
this.refreshMatrix();
}
}
});
}
protected refreshMatrix() { protected refreshMatrix() {
if (this.id < 0 || this.cameraId < 0) return; if (this.id < 0 || this.cameraId < 0) return;
const camera = this.cameraRow(); const camera = this.cameraRow();
const position = this.position; const position = this.position;
const quaternion = this.quaternion; const rotor = this.rotor;
const right = rotate(quaternion, [1, 0, 0]); const right = rotate(rotor, [1, 0, 0]);
const up = rotate(quaternion, [0, 1, 0]); const up = rotate(rotor, [0, 1, 0]);
const forward = rotate(quaternion, [0, 0, 1]); const forward = rotate(rotor, [0, 0, 1]);
let rows: number[][]; let rows: number[][];
if (camera[5] === 1) { if (camera[5] === 1) {
const size = Math.max(camera[6], 0.0001); const size = Math.max(camera[6], 0.0001);
+6 -3
View File
@@ -62,15 +62,18 @@ export class FollowCamera extends Camera {
#snap() { #snap() {
const target = this.#target.position; const target = this.#target.position;
this.position = [target[0], target[1] + this.height, target[2] + this.distance]; this.setPosition([target.x, target.y + this.height, target.z + this.distance]);
this.lookAt(target); this.lookAt(target);
} }
#follow = () => { #follow = () => {
const target = this.#target.position; const target = this.#target.position;
const desired = [target[0], target[1] + this.height, target[2] + this.distance];
const position = this.position; const position = this.position;
for (let lane = 0; lane < 3; lane++) position[lane] += (desired[lane] - position[lane]) * this.#smoothing; this.setPosition([
position.x + (target.x - position.x) * this.#smoothing,
position.y + (target.y + this.height - position.y) * this.#smoothing,
position.z + (target.z + this.distance - position.z) * this.#smoothing,
]);
this.lookAt(target); this.lookAt(target);
this.#frame = requestAnimationFrame(this.#follow); this.#frame = requestAnimationFrame(this.#follow);
}; };
+9 -7
View File
@@ -64,15 +64,15 @@ export class FreeCamera extends Camera {
const sensitivity = this.#controls.sensitivity ?? 0.002; const sensitivity = this.#controls.sensitivity ?? 0.002;
this.cameraRow()[13] -= event.movementX * sensitivity; this.cameraRow()[13] -= event.movementX * sensitivity;
this.cameraRow()[14] = Math.min(1.55, Math.max(-1.55, this.cameraRow()[14] - event.movementY * sensitivity)); this.cameraRow()[14] = Math.min(1.55, Math.max(-1.55, this.cameraRow()[14] - event.movementY * sensitivity));
this.#writeQuaternion(); this.#writeRotor();
}; };
#writeQuaternion() { #writeRotor() {
const yaw = this.cameraRow()[13]; const yaw = this.cameraRow()[13];
const pitch = this.cameraRow()[14]; const pitch = this.cameraRow()[14];
const sy = Math.sin(yaw / 2), cy = Math.cos(yaw / 2); const sy = Math.sin(yaw / 2), cy = Math.cos(yaw / 2);
const sx = Math.sin(pitch / 2), cx = Math.cos(pitch / 2); const sx = Math.sin(pitch / 2), cx = Math.cos(pitch / 2);
this.quaternion = [sx * cy, cx * sy, -sx * sy, cx * cy]; this.setRotor([sx * cy, cx * sy, -sx * sy, cx * cy]);
} }
#update = (time: number) => { #update = (time: number) => {
@@ -88,10 +88,12 @@ export class FreeCamera extends Camera {
} }
const speed = (this.#controls.speed ?? 4) * delta; const speed = (this.#controls.speed ?? 4) * delta;
const yaw = this.cameraRow()[13]; const yaw = this.cameraRow()[13];
this.position[0] += (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed; const position = this.position;
this.position[1] += y * speed; this.setPosition([
this.position[2] += (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed; position.x + (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed,
this.refreshMatrix(); position.y + y * speed,
position.z + (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed,
]);
this.#frame = requestAnimationFrame(this.#update); this.#frame = requestAnimationFrame(this.#update);
}; };
+1 -1
View File
@@ -74,7 +74,7 @@ export async function importGltf(scene: Scene, url: string | URL) {
(primitive: any) => (primitive: any) =>
new Mesh(scene, { new Mesh(scene, {
position: primitive.position, position: primitive.position,
quaternion: primitive.quaternion, rotor: primitive.rotor,
scale: primitive.scale, scale: primitive.scale,
material: materials[primitive.material], material: materials[primitive.material],
vertexData: { vertexData: {
+18 -18
View File
@@ -29,16 +29,16 @@ function component(view: DataView, offset: number, type: number) {
throw new Error("GLTF_COMPONENT"); throw new Error("GLTF_COMPONENT");
} }
function rotate(quaternion: number[], value: number[]) { function rotate(rotor: number[], value: number[]) {
const [qx, qy, qz, qw] = quaternion; const [rx, ry, rz, rw] = rotor;
const [x, y, z] = value; const [x, y, z] = value;
const tx = 2 * (qy * z - qz * y); const tx = 2 * (ry * z - rz * y);
const ty = 2 * (qz * x - qx * z); const ty = 2 * (rz * x - rx * z);
const tz = 2 * (qx * y - qy * x); const tz = 2 * (rx * y - ry * x);
return [ return [
x + qw * tx + qy * tz - qz * ty, x + rw * tx + ry * tz - rz * ty,
y + qw * ty + qz * tx - qx * tz, y + rw * ty + rz * tx - rx * tz,
z + qw * tz + qx * ty - qy * tx, z + rw * tz + rx * ty - ry * tx,
]; ];
} }
@@ -61,21 +61,21 @@ function decompose(matrix: number[]) {
const [m00, m01, m02] = [matrix[0] / sx, matrix[4] / sy, matrix[8] / sz]; const [m00, m01, m02] = [matrix[0] / sx, matrix[4] / sy, matrix[8] / sz];
const [m10, m11, m12] = [matrix[1] / sx, matrix[5] / sy, matrix[9] / sz]; const [m10, m11, m12] = [matrix[1] / sx, matrix[5] / sy, matrix[9] / sz];
const [m20, m21, m22] = [matrix[2] / sx, matrix[6] / sy, matrix[10] / sz]; const [m20, m21, m22] = [matrix[2] / sx, matrix[6] / sy, matrix[10] / sz];
let quaternion: number[]; let rotor: number[];
if (m00 + m11 + m22 > 0) { if (m00 + m11 + m22 > 0) {
const s = Math.sqrt(1 + m00 + m11 + m22) * 2; const s = Math.sqrt(1 + m00 + m11 + m22) * 2;
quaternion = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, s / 4]; rotor = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, s / 4];
} else if (m00 > m11 && m00 > m22) { } else if (m00 > m11 && m00 > m22) {
const s = Math.sqrt(1 + m00 - m11 - m22) * 2; const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
quaternion = [s / 4, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s]; rotor = [s / 4, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s];
} else if (m11 > m22) { } else if (m11 > m22) {
const s = Math.sqrt(1 + m11 - m00 - m22) * 2; const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
quaternion = [(m01 + m10) / s, s / 4, (m12 + m21) / s, (m02 - m20) / s]; rotor = [(m01 + m10) / s, s / 4, (m12 + m21) / s, (m02 - m20) / s];
} else { } else {
const s = Math.sqrt(1 + m22 - m00 - m11) * 2; const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
quaternion = [(m02 + m20) / s, (m12 + m21) / s, s / 4, (m10 - m01) / s]; rotor = [(m02 + m20) / s, (m12 + m21) / s, s / 4, (m10 - m01) / s];
} }
return { position: matrix.slice(12, 15), quaternion, scale }; return { position: matrix.slice(12, 15), rotor, scale };
} }
function transform(node: any) { function transform(node: any) {
@@ -83,21 +83,21 @@ function transform(node: any) {
? decompose(node.matrix) ? decompose(node.matrix)
: { : {
position: node.translation ?? [0, 0, 0], position: node.translation ?? [0, 0, 0],
quaternion: node.rotation ?? [0, 0, 0, 1], rotor: node.rotation ?? [0, 0, 0, 1],
scale: node.scale ?? [1, 1, 1], scale: node.scale ?? [1, 1, 1],
}; };
} }
function compose(parent: any, local: any) { function compose(parent: any, local: any) {
const position = rotate( const position = rotate(
parent.quaternion, parent.rotor,
local.position.map( local.position.map(
(value: number, lane: number) => value * parent.scale[lane], (value: number, lane: number) => value * parent.scale[lane],
), ),
); );
return { return {
position: position.map((value, lane) => value + parent.position[lane]), position: position.map((value, lane) => value + parent.position[lane]),
quaternion: multiply(parent.quaternion, local.quaternion), rotor: multiply(parent.rotor, local.rotor),
scale: local.scale.map( scale: local.scale.map(
(value: number, lane: number) => value * parent.scale[lane], (value: number, lane: number) => value * parent.scale[lane],
), ),
@@ -275,7 +275,7 @@ async function load(url: string) {
id: number, id: number,
parent = { parent = {
position: [0, 0, 0], position: [0, 0, 0],
quaternion: [0, 0, 0, 1], rotor: [0, 0, 0, 1],
scale: [1, 1, 1], scale: [1, 1, 1],
}, },
) => { ) => {
+1
View File
@@ -496,6 +496,7 @@ canvas {
height: 100%; height: 100%;
display: block; display: block;
object-fit: contain; object-fit: contain;
touch-action: none;
} }
.output { .output {
position: absolute; position: absolute;
+26 -3
View File
@@ -61,8 +61,8 @@ log("Pointer movement mutates nodePositions; no worker message is sent.");
const move = (event) => { const move = (event) => {
const bounds = canvas.getBoundingClientRect(); const bounds = canvas.getBoundingClientRect();
mesh.position[0] = ((event.clientX - bounds.left) / bounds.width - 0.5) * 1.4; mesh.position.x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 1.4;
mesh.position[1] = (0.5 - (event.clientY - bounds.top) / bounds.height) * 1.2; mesh.position.y = (0.5 - (event.clientY - bounds.top) / bounds.height) * 1.2;
}; };
canvas.addEventListener("pointermove", move); canvas.addEventListener("pointermove", move);
@@ -270,13 +270,35 @@ log("HDR → exposure → color grading → FXAA → canvas");
return { scene, mesh, exposure, grade, fxaa, dispose: () => scene.dispose() };`; return { scene, mesh, exposure, grade, fxaa, dispose: () => scene.dispose() };`;
const importing = `import { ArcRotateCamera, Picking, Scene, importGltf } from "@yawn/handles"; const importing = `import { AmbientLight, ArcRotateCamera, PointLight, Picking, Scene, importGltf } from "@yawn/handles";
const scene = new Scene(canvas, { hdr: true, arenaBytes: 384 * 1024 * 1024 }); const scene = new Scene(canvas, { hdr: true, arenaBytes: 384 * 1024 * 1024 });
await scene.ready; await scene.ready;
scene.array("sceneAccent").row(0).set([1, 1, 1, 1]);
log("Importing /models/sponza.glb in the importer worker…"); log("Importing /models/sponza.glb in the importer worker…");
const meshes = await importGltf(scene, "/models/sponza.glb"); const meshes = await importGltf(scene, "/models/sponza.glb");
// Add a warm ambient/key/fill setup without changing the authored transforms.
const lights = [
new AmbientLight(scene, {
color: [1, 0.93, 0.82],
intensity: 0.18,
}),
new PointLight(scene, {
position: [-10, 14, -4],
color: [1, 0.72, 0.5],
intensity: 2,
range: 28,
}),
new PointLight(scene, {
position: [9, 10, 6],
color: [1, 0.9, 0.75],
intensity: 1.5,
range: 24,
}),
];
await Promise.all(lights.map((light) => light.ready));
const target = [0, 8, 0]; const target = [0, 8, 0];
const camera = new ArcRotateCamera(scene, { const camera = new ArcRotateCamera(scene, {
targetPosition: target, targetPosition: target,
@@ -305,6 +327,7 @@ await pick();
return { return {
scene, scene,
meshes, meshes,
lights,
camera, camera,
picking, picking,
async dispose() { async dispose() {
+1 -1
View File
@@ -72,7 +72,7 @@ follow.stop();
follow.start(); follow.start();
``` ```
The input and follow loops never post camera updates to core: they mutate the position, quaternion, camera, and derived matrix rows directly in shared memory. The input and follow loops never post camera updates to core: they mutate the position, rotor, camera, and derived matrix rows directly in shared memory.
<Playground example="cameras" /> <Playground example="cameras" />
+2 -2
View File
@@ -8,7 +8,7 @@ The shared importer worker fetches and parses glTF/GLB, then the response hydrat
import { importGltf } from "@yawn/handles"; import { importGltf } from "@yawn/handles";
const meshes = await importGltf(scene, "/models/sponza.glb"); const meshes = await importGltf(scene, "/models/sponza.glb");
meshes[0].position[1] = 0.5; meshes[0].position.y = 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. The importer handles triangle primitives, external/data buffers, standard vertex attributes, indices, node transforms, and metallic-roughness values. Application-specific extensions remain application policy.
@@ -34,7 +34,7 @@ If row allocations relocated since `Picking` was created, refresh its shared des
await picking.refresh(); await picking.refresh();
``` ```
The playground below imports the repository's full LFS-backed `sponza.glb` in the importer worker, preserves its authored transforms, places an arc camera in its coordinate system, and sends a real ray to the BVH worker. Click the preview to pick again. The playground below imports the repository's full LFS-backed `sponza.glb` in the importer worker, preserves its authored transforms, adds neutral warm lighting, places an arc camera in its coordinate system, and sends a real ray to the BVH worker. Click the preview to pick again.
<Playground example="importing" /> <Playground example="importing" />
+1 -1
View File
@@ -11,7 +11,7 @@ const point = new PointLight(scene, {
}); });
const sun = new DirectionalLight(scene, { const sun = new DirectionalLight(scene, {
quaternion: [0.2, 0, 0, 0.98], rotor: [0.2, 0, 0, 0.98],
color: [1, 0.95, 0.8], color: [1, 0.95, 0.8],
intensity: 3, intensity: 3,
}); });
+4 -4
View File
@@ -1,6 +1,6 @@
# Scene and shared data # 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. Think of every handle as an array index, not an object mirrored into core. `Node.position`, `Node.rotor`, and `Node.scale` are component views into separate flat SOA arrays.
## Direct transform movement ## Direct transform movement
@@ -11,12 +11,12 @@ const pivot = new Node(scene, { position: [0, 1, 0] });
await pivot.ready; await pivot.ready;
canvas.addEventListener("pointermove", (event) => { canvas.addEventListener("pointermove", (event) => {
pivot.position[0] += event.movementX * 0.002; pivot.position.x += event.movementX * 0.002;
pivot.position[1] -= event.movementY * 0.002; pivot.position.y -= 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). The pointer handler sends no messages. Each component property reads or writes its lane in the arena shared with the render worker. Replace complete transforms with `setPosition([x, y, z])`, `setRotor([x, y, z, w])`, and `setScale([x, y, z])`. `translate(...)`, `rotate(...)`, and `rotateX/Y/Z(...)` are convenience methods over those same SAB lanes. The camera helpers use this same pattern; see [Cameras and controls](/guide/cameras).
<Playground example="sab" /> <Playground example="sab" />
+1 -1
View File
@@ -39,7 +39,7 @@ const mesh = new Mesh(scene, {
}); });
await mesh.ready; await mesh.ready;
mesh.position[0] = 0.25; // direct SAB mutation mesh.position.x = 0.25; // direct SAB mutation
``` ```
`Scene` installs one HDR clustered-forward loadout. Adding compute, custom shaders, textures, or post effects rebuilds that same loadout; changing values already present in shared rows does not send a message. `Scene` installs one HDR clustered-forward loadout. Adding compute, custom shaders, textures, or post effects rebuilds that same loadout; changing values already present in shared rows does not send a message.