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:
+162
-12
@@ -2,15 +2,85 @@ import type { Scene } from "./Scene";
|
||||
|
||||
export type NodeOptions = {
|
||||
position?: ArrayLike<number>;
|
||||
quaternion?: ArrayLike<number>;
|
||||
rotor?: ArrayLike<number>;
|
||||
scale?: ArrayLike<number>;
|
||||
parent?: Node;
|
||||
};
|
||||
|
||||
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);
|
||||
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. */
|
||||
@@ -19,14 +89,26 @@ export class Node {
|
||||
id = -1;
|
||||
ready: Promise<this>;
|
||||
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 = {}) {
|
||||
this.scene = scene;
|
||||
this.ready = scene.allocateNode().then((id) => {
|
||||
this.id = id;
|
||||
if (options.position) this.position = options.position;
|
||||
if (options.quaternion) this.quaternion = options.quaternion;
|
||||
if (options.scale) this.scale = options.scale;
|
||||
if (options.position) this.setPosition(options.position);
|
||||
if (options.rotor) this.setRotor(options.rotor);
|
||||
if (options.scale) this.setScale(options.scale);
|
||||
if (options.parent) this.parent = options.parent;
|
||||
return this;
|
||||
});
|
||||
@@ -37,14 +119,82 @@ export class Node {
|
||||
return this.scene.array(name).row(this.id);
|
||||
}
|
||||
|
||||
get position(): Float32Array { return this.#row("nodePositions").subarray(0, 3); }
|
||||
set position(value: ArrayLike<number>) { this.#row("nodePositions").set(vector(value, 3, "position")); }
|
||||
get position(): Vector3 { return this.#position; }
|
||||
set position(value: ArrayLike<number>) { this.setPosition(value); }
|
||||
|
||||
get quaternion(): Float32Array { return this.#row("nodeQuaternions").subarray(0, 4); }
|
||||
set quaternion(value: ArrayLike<number>) { this.#row("nodeQuaternions").set(vector(value, 4, "quaternion")); }
|
||||
get rotor(): Rotor { return this.#rotor; }
|
||||
set rotor(value: ArrayLike<number>) { this.setRotor(value); }
|
||||
|
||||
get scale(): Float32Array { return this.#row("nodeScales").subarray(0, 3); }
|
||||
set scale(value: ArrayLike<number>) { this.#row("nodeScales").set(vector(value, 3, "scale")); }
|
||||
get scale(): Vector3 { return this.#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; }
|
||||
set enabled(value: boolean) { this.#row("nodes")[0] = value ? 1 : 0; }
|
||||
|
||||
+41
-17
@@ -33,7 +33,7 @@ type TextureState = GraphTexture & {
|
||||
const rows = [
|
||||
["nodes", 16, "u32"],
|
||||
["nodePositions", 16, "f32"],
|
||||
["nodeQuaternions", 16, "f32"],
|
||||
["nodeRotors", 16, "f32"],
|
||||
["nodeScales", 16, "f32"],
|
||||
["meshInfo", 16, "u32"],
|
||||
["bounds", 32, "f32"],
|
||||
@@ -104,21 +104,21 @@ struct VertexOutput {
|
||||
@group(0) @binding(0) var<storage, read> clusters: array<u32>;
|
||||
@group(0) @binding(1) var<uniform> accent: Accent;
|
||||
@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(5) var<storage, read> meshInfo: array<u32>;
|
||||
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
|
||||
@group(0) @binding(7) var<storage, read> cameraMatrices: array<vec4<f32>>;
|
||||
|
||||
fn rotate(q: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
|
||||
return value + 2.0 * cross(q.xyz, cross(q.xyz, value) + q.w * value);
|
||||
fn rotate(rotor: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
|
||||
return value + 2.0 * cross(rotor.xyz, cross(rotor.xyz, value) + rotor.w * value);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex(@location(0) point: vec3<f32>, @builtin(instance_index) packed: u32) -> VertexOutput {
|
||||
let instance = packed & 65535u;
|
||||
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);
|
||||
if (cameraMatrices[4].w != 0.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;
|
||||
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.material = packed >> 16u;
|
||||
return output;
|
||||
@@ -168,7 +168,7 @@ struct VertexOutput {
|
||||
@group(0) @binding(0) var<storage, read> clusters: array<u32>;
|
||||
@group(0) @binding(1) var<uniform> accent: 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(5) var<storage, read> meshInfo: array<u32>;
|
||||
@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>;" : ""}
|
||||
${normalTexture ? "@group(1) @binding(3) var normalTexture: texture_2d<f32>;" : ""}
|
||||
|
||||
fn rotate(q: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
|
||||
return value + 2.0 * cross(q.xyz, cross(q.xyz, value) + q.w * value);
|
||||
fn rotate(rotor: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
|
||||
return value + 2.0 * cross(rotor.xyz, cross(rotor.xyz, value) + rotor.w * value);
|
||||
}
|
||||
|
||||
@vertex
|
||||
@@ -192,7 +192,7 @@ fn vertex(
|
||||
) -> VertexOutput {
|
||||
let instance = packed & 65535u;
|
||||
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);
|
||||
if (cameraMatrices[4].w != 0.0) {
|
||||
let homogeneous = vec4(world, 1.0);
|
||||
@@ -206,9 +206,9 @@ fn vertex(
|
||||
var output: VertexOutput;
|
||||
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, meshInfo[instance * 4u + 2u] != 0u);
|
||||
output.world = world;
|
||||
output.normal = normalize(rotate(quaternions[instance], localNormal / scale));
|
||||
output.normal = normalize(rotate(rotors[instance], localNormal / scale));
|
||||
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.material = packed >> 16u;
|
||||
return output;
|
||||
@@ -351,6 +351,9 @@ export class Scene {
|
||||
#nextTexture = 0;
|
||||
#graphBatchDepth = 0;
|
||||
#graphBatchDirty = false;
|
||||
#writeBatchDepth = 0;
|
||||
#writeBatchDirty = false;
|
||||
#writeBatchBundleDirty = false;
|
||||
#signals?: Float32Array;
|
||||
#arrays = new WeakMap<object, object>();
|
||||
#views = new WeakMap<object, object>();
|
||||
@@ -378,7 +381,7 @@ export class Scene {
|
||||
stride: 16,
|
||||
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("sceneAccent").write(0, [0.28, 0.72, 1, 1]);
|
||||
const material = await this.core.allocateObject("materials");
|
||||
@@ -444,11 +447,32 @@ export class Scene {
|
||||
}
|
||||
|
||||
markDirty(bundle = false) {
|
||||
if (this.#writeBatchDepth) {
|
||||
this.#writeBatchDirty = true;
|
||||
this.#writeBatchBundleDirty ||= bundle;
|
||||
return;
|
||||
}
|
||||
if (!this.#signals) return;
|
||||
this.#signals[5] = 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(
|
||||
name: string,
|
||||
rowCount: number,
|
||||
@@ -535,7 +559,7 @@ export class Scene {
|
||||
: [];
|
||||
});
|
||||
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("nodes").write(id, [1, 0, 0, 0]);
|
||||
return id;
|
||||
@@ -545,7 +569,7 @@ export class Scene {
|
||||
for (const name of [
|
||||
"nodes",
|
||||
"nodePositions",
|
||||
"nodeQuaternions",
|
||||
"nodeRotors",
|
||||
"nodeScales",
|
||||
"meshInfo",
|
||||
"bounds",
|
||||
@@ -835,7 +859,7 @@ export class Scene {
|
||||
} else {
|
||||
for (const [id, array] of [
|
||||
["node-positions", "nodePositions"],
|
||||
["node-quaternions", "nodeQuaternions"],
|
||||
["node-rotors", "nodeRotors"],
|
||||
["node-scales", "nodeScales"],
|
||||
["mesh-info", "meshInfo"],
|
||||
["materials", "materials"],
|
||||
@@ -1014,7 +1038,7 @@ export class Scene {
|
||||
"clusters",
|
||||
"accent",
|
||||
"node-positions",
|
||||
"node-quaternions",
|
||||
"node-rotors",
|
||||
"node-scales",
|
||||
"mesh-info",
|
||||
"materials",
|
||||
|
||||
@@ -38,7 +38,7 @@ export class Picking {
|
||||
"signals",
|
||||
"nodes",
|
||||
"nodePositions",
|
||||
"nodeQuaternions",
|
||||
"nodeRotors",
|
||||
"nodeScales",
|
||||
"meshInfo",
|
||||
"bounds",
|
||||
|
||||
@@ -52,27 +52,27 @@ function build(boxes: Box[]): Branch | undefined {
|
||||
};
|
||||
}
|
||||
|
||||
function rotate(quaternion: number[], value: number[]) {
|
||||
const [qx, qy, qz, qw] = quaternion;
|
||||
function rotate(rotor: number[], value: number[]) {
|
||||
const [rx, ry, rz, rw] = rotor;
|
||||
const [x, y, z] = value;
|
||||
const tx = 2 * (qy * z - qz * y);
|
||||
const ty = 2 * (qz * x - qx * z);
|
||||
const tz = 2 * (qx * y - qy * x);
|
||||
const tx = 2 * (ry * z - rz * y);
|
||||
const ty = 2 * (rz * x - rx * z);
|
||||
const tz = 2 * (rx * y - ry * x);
|
||||
return [
|
||||
x + qw * tx + qy * tz - qz * ty,
|
||||
y + qw * ty + qz * tx - qx * tz,
|
||||
z + qw * tz + qx * ty - qy * tx,
|
||||
x + rw * tx + ry * tz - rz * ty,
|
||||
y + rw * ty + rz * tx - rx * tz,
|
||||
z + rw * tz + rx * ty - ry * tx,
|
||||
];
|
||||
}
|
||||
|
||||
function rebuild() {
|
||||
const bounds = view("bounds");
|
||||
const positions = view("nodePositions");
|
||||
const quaternions = view("nodeQuaternions");
|
||||
const rotors = view("nodeRotors");
|
||||
const scales = view("nodeScales");
|
||||
const meshes = view("meshInfo");
|
||||
const nodes = view("nodes");
|
||||
if (!bounds || !positions || !quaternions || !scales || !meshes || !nodes)
|
||||
if (!bounds || !positions || !rotors || !scales || !meshes || !nodes)
|
||||
return;
|
||||
const count = shares.bounds.descriptor.rows;
|
||||
const boxes: Box[] = [];
|
||||
@@ -82,8 +82,8 @@ function rebuild() {
|
||||
const transform = id * 4;
|
||||
const min = [Infinity, Infinity, Infinity];
|
||||
const max = [-Infinity, -Infinity, -Infinity];
|
||||
const quaternion = [0, 1, 2, 3].map((lane) =>
|
||||
Number(quaternions[transform + lane]),
|
||||
const rotor = [0, 1, 2, 3].map((lane) =>
|
||||
Number(rotors[transform + lane]),
|
||||
);
|
||||
for (let corner = 0; corner < 8; corner++) {
|
||||
const local = [0, 1, 2].map(
|
||||
@@ -91,7 +91,7 @@ function rebuild() {
|
||||
Number(bounds[offset + (corner & (1 << lane) ? 4 : 0) + lane]) *
|
||||
Number(scales[transform + lane]),
|
||||
);
|
||||
const rotated = rotate(quaternion, local);
|
||||
const rotated = rotate(rotor, local);
|
||||
for (let lane = 0; lane < 3; lane++) {
|
||||
const value = rotated[lane] + Number(positions[transform + lane]);
|
||||
min[lane] = Math.min(min[lane], value);
|
||||
|
||||
@@ -46,18 +46,29 @@ export class ArcRotateCamera extends Camera {
|
||||
}
|
||||
|
||||
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]; }
|
||||
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]; }
|
||||
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; }
|
||||
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");
|
||||
this.#target = value;
|
||||
this.cameraRow()[12] = value ? value.id + 1 : 0;
|
||||
this.#updateTransform();
|
||||
const row = this.cameraRow();
|
||||
this.#updateTransform(() => { row[12] = value ? value.id + 1 : 0; });
|
||||
}
|
||||
|
||||
attachControls(controls: ArcRotateControls) {
|
||||
@@ -67,6 +78,8 @@ export class ArcRotateCamera extends Camera {
|
||||
controls.element.addEventListener("pointerdown", this.#down);
|
||||
controls.element.addEventListener("pointermove", this.#move);
|
||||
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 });
|
||||
}
|
||||
if (controls.controller) this.#frame = requestAnimationFrame(this.#pollController);
|
||||
@@ -79,6 +92,8 @@ export class ArcRotateCamera extends Camera {
|
||||
element.removeEventListener("pointerdown", this.#down);
|
||||
element.removeEventListener("pointermove", this.#move);
|
||||
element.removeEventListener("pointerup", this.#up);
|
||||
element.removeEventListener("pointercancel", this.#up);
|
||||
element.removeEventListener("contextmenu", this.#contextMenu);
|
||||
element.removeEventListener("wheel", this.#wheel);
|
||||
}
|
||||
cancelAnimationFrame(this.#frame);
|
||||
@@ -88,26 +103,38 @@ export class ArcRotateCamera extends Camera {
|
||||
}
|
||||
|
||||
#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;
|
||||
const target = this.#targetPosition();
|
||||
const sinBeta = Math.sin(this.beta);
|
||||
this.position = [
|
||||
target[0] + this.radius * sinBeta * Math.sin(this.alpha),
|
||||
target[1] + this.radius * Math.cos(this.beta),
|
||||
target[2] + this.radius * sinBeta * Math.cos(this.alpha),
|
||||
];
|
||||
this.lookAt(target);
|
||||
this.batchTransformChanges(() => {
|
||||
change?.();
|
||||
const target = this.#targetPosition();
|
||||
const sinBeta = Math.sin(this.beta);
|
||||
this.setPosition([
|
||||
target[0] + this.radius * sinBeta * Math.sin(this.alpha),
|
||||
target[1] + this.radius * Math.cos(this.beta),
|
||||
target[2] + this.radius * sinBeta * Math.cos(this.alpha),
|
||||
]);
|
||||
this.lookAt(target);
|
||||
});
|
||||
}
|
||||
|
||||
#down = (event: PointerEvent) => {
|
||||
event.preventDefault();
|
||||
this.#pointer = { x: event.clientX, y: event.clientY, button: event.button };
|
||||
this.#controls?.element.setPointerCapture?.(event.pointerId);
|
||||
};
|
||||
#up = () => { this.#pointer = undefined; };
|
||||
#contextMenu = (event: Event) => event.preventDefault();
|
||||
#move = (event: PointerEvent) => {
|
||||
if (!this.#pointer || !this.#controls) return;
|
||||
const x = event.clientX - this.#pointer.x;
|
||||
@@ -115,28 +142,54 @@ export class ArcRotateCamera extends Camera {
|
||||
this.#pointer.x = event.clientX;
|
||||
this.#pointer.y = event.clientY;
|
||||
if (this.#pointer.button === 2) {
|
||||
const target = this.#targetPosition();
|
||||
target[0] -= x * (this.#controls.panSpeed ?? 0.005);
|
||||
target[1] += y * (this.#controls.panSpeed ?? 0.005);
|
||||
this.#updateTransform();
|
||||
const row = this.cameraRow();
|
||||
this.#updateTransform(() => {
|
||||
row[16] -= x * (this.#controls?.panSpeed ?? 0.005);
|
||||
row[17] += y * (this.#controls?.panSpeed ?? 0.005);
|
||||
});
|
||||
} else {
|
||||
const speed = this.#controls.orbitSpeed ?? 0.005;
|
||||
this.cameraRow()[13] -= x * speed;
|
||||
this.cameraRow()[14] = Math.min(Math.PI - 0.001, Math.max(0.001, this.beta + y * speed));
|
||||
this.#updateTransform();
|
||||
const row = this.cameraRow();
|
||||
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) => {
|
||||
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 = () => {
|
||||
const pad = navigator.getGamepads?.().find(Boolean);
|
||||
if (pad) {
|
||||
this.cameraRow()[13] += (pad.axes[2] ?? pad.axes[0] ?? 0) * 0.03;
|
||||
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.radius += ((pad.buttons[6]?.value ?? 0) - (pad.buttons[7]?.value ?? 0)) * 0.1;
|
||||
this.#updateTransform();
|
||||
const row = this.cameraRow();
|
||||
this.#updateTransform(() => {
|
||||
row[13] += (pad.axes[2] ?? pad.axes[0] ?? 0) * 0.03;
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Node, type NodeOptions } from "../Node";
|
||||
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 tx = 2 * (q[1] * z - q[2] * y);
|
||||
const ty = 2 * (q[2] * x - q[0] * z);
|
||||
const tz = 2 * (q[0] * y - q[1] * x);
|
||||
const tx = 2 * (rotor[1] * z - rotor[2] * y);
|
||||
const ty = 2 * (rotor[2] * x - rotor[0] * z);
|
||||
const tz = 2 * (rotor[0] * y - rotor[1] * x);
|
||||
return [
|
||||
x + q[3] * tx + q[1] * tz - q[2] * ty,
|
||||
y + q[3] * ty + q[2] * tx - q[0] * tz,
|
||||
z + q[3] * tz + q[0] * ty - q[1] * tx,
|
||||
x + rotor[3] * tx + rotor[1] * tz - rotor[2] * ty,
|
||||
y + rotor[3] * ty + rotor[2] * tx - rotor[0] * tz,
|
||||
z + rotor[3] * tz + rotor[0] * ty - rotor[1] * tx,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ export type CameraOptions = NodeOptions & {
|
||||
export class Camera extends Node {
|
||||
cameraId = -1;
|
||||
#cameraDisposed = false;
|
||||
#transformBatchDepth = 0;
|
||||
#transformBatchDirty = false;
|
||||
|
||||
constructor(scene: Scene, options: CameraOptions = {}) {
|
||||
super(scene, options);
|
||||
@@ -90,43 +92,55 @@ export class Camera extends Node {
|
||||
get sensorWidth() { return this.cameraRow()[10]; }
|
||||
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>) {
|
||||
if (target.length !== 3) throw new RangeError("camera target");
|
||||
const position = this.position;
|
||||
const x = target[0] - position[0];
|
||||
const y = target[1] - position[1];
|
||||
const z = target[2] - position[2];
|
||||
const x = target[0] - position.x;
|
||||
const y = target[1] - position.y;
|
||||
const z = target[2] - position.z;
|
||||
const length = Math.hypot(x, y, z) || 1;
|
||||
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 {
|
||||
const q = [direction[1], -direction[0], 0, 1 - direction[2]];
|
||||
const qLength = Math.hypot(...q);
|
||||
this.quaternion = q.map((lane) => lane / qLength);
|
||||
const rotor = [direction[1], -direction[0], 0, 1 - direction[2]];
|
||||
const rotorLength = Math.hypot(...rotor);
|
||||
this.setRotor(rotor.map((lane) => lane / rotorLength));
|
||||
}
|
||||
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() {
|
||||
if (this.id < 0 || this.cameraId < 0) return;
|
||||
const camera = this.cameraRow();
|
||||
const position = this.position;
|
||||
const quaternion = this.quaternion;
|
||||
const right = rotate(quaternion, [1, 0, 0]);
|
||||
const up = rotate(quaternion, [0, 1, 0]);
|
||||
const forward = rotate(quaternion, [0, 0, 1]);
|
||||
const rotor = this.rotor;
|
||||
const right = rotate(rotor, [1, 0, 0]);
|
||||
const up = rotate(rotor, [0, 1, 0]);
|
||||
const forward = rotate(rotor, [0, 0, 1]);
|
||||
let rows: number[][];
|
||||
if (camera[5] === 1) {
|
||||
const size = Math.max(camera[6], 0.0001);
|
||||
|
||||
@@ -62,15 +62,18 @@ export class FollowCamera extends Camera {
|
||||
|
||||
#snap() {
|
||||
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);
|
||||
}
|
||||
|
||||
#follow = () => {
|
||||
const target = this.#target.position;
|
||||
const desired = [target[0], target[1] + this.height, target[2] + this.distance];
|
||||
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.#frame = requestAnimationFrame(this.#follow);
|
||||
};
|
||||
|
||||
@@ -64,15 +64,15 @@ export class FreeCamera extends Camera {
|
||||
const sensitivity = this.#controls.sensitivity ?? 0.002;
|
||||
this.cameraRow()[13] -= event.movementX * 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 pitch = this.cameraRow()[14];
|
||||
const sy = Math.sin(yaw / 2), cy = Math.cos(yaw / 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) => {
|
||||
@@ -88,10 +88,12 @@ export class FreeCamera extends Camera {
|
||||
}
|
||||
const speed = (this.#controls.speed ?? 4) * delta;
|
||||
const yaw = this.cameraRow()[13];
|
||||
this.position[0] += (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed;
|
||||
this.position[1] += y * speed;
|
||||
this.position[2] += (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed;
|
||||
this.refreshMatrix();
|
||||
const position = this.position;
|
||||
this.setPosition([
|
||||
position.x + (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed,
|
||||
position.y + y * speed,
|
||||
position.z + (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed,
|
||||
]);
|
||||
this.#frame = requestAnimationFrame(this.#update);
|
||||
};
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function importGltf(scene: Scene, url: string | URL) {
|
||||
(primitive: any) =>
|
||||
new Mesh(scene, {
|
||||
position: primitive.position,
|
||||
quaternion: primitive.quaternion,
|
||||
rotor: primitive.rotor,
|
||||
scale: primitive.scale,
|
||||
material: materials[primitive.material],
|
||||
vertexData: {
|
||||
|
||||
@@ -29,16 +29,16 @@ function component(view: DataView, offset: number, type: number) {
|
||||
throw new Error("GLTF_COMPONENT");
|
||||
}
|
||||
|
||||
function rotate(quaternion: number[], value: number[]) {
|
||||
const [qx, qy, qz, qw] = quaternion;
|
||||
function rotate(rotor: number[], value: number[]) {
|
||||
const [rx, ry, rz, rw] = rotor;
|
||||
const [x, y, z] = value;
|
||||
const tx = 2 * (qy * z - qz * y);
|
||||
const ty = 2 * (qz * x - qx * z);
|
||||
const tz = 2 * (qx * y - qy * x);
|
||||
const tx = 2 * (ry * z - rz * y);
|
||||
const ty = 2 * (rz * x - rx * z);
|
||||
const tz = 2 * (rx * y - ry * x);
|
||||
return [
|
||||
x + qw * tx + qy * tz - qz * ty,
|
||||
y + qw * ty + qz * tx - qx * tz,
|
||||
z + qw * tz + qx * ty - qy * tx,
|
||||
x + rw * tx + ry * tz - rz * ty,
|
||||
y + rw * ty + rz * tx - rx * tz,
|
||||
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 [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];
|
||||
let quaternion: number[];
|
||||
let rotor: number[];
|
||||
if (m00 + m11 + m22 > 0) {
|
||||
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) {
|
||||
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) {
|
||||
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 {
|
||||
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) {
|
||||
@@ -83,21 +83,21 @@ function transform(node: any) {
|
||||
? decompose(node.matrix)
|
||||
: {
|
||||
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],
|
||||
};
|
||||
}
|
||||
|
||||
function compose(parent: any, local: any) {
|
||||
const position = rotate(
|
||||
parent.quaternion,
|
||||
parent.rotor,
|
||||
local.position.map(
|
||||
(value: number, lane: number) => value * parent.scale[lane],
|
||||
),
|
||||
);
|
||||
return {
|
||||
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(
|
||||
(value: number, lane: number) => value * parent.scale[lane],
|
||||
),
|
||||
@@ -275,7 +275,7 @@ async function load(url: string) {
|
||||
id: number,
|
||||
parent = {
|
||||
position: [0, 0, 0],
|
||||
quaternion: [0, 0, 0, 1],
|
||||
rotor: [0, 0, 0, 1],
|
||||
scale: [1, 1, 1],
|
||||
},
|
||||
) => {
|
||||
|
||||
@@ -496,6 +496,7 @@ canvas {
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
touch-action: none;
|
||||
}
|
||||
.output {
|
||||
position: absolute;
|
||||
|
||||
@@ -61,8 +61,8 @@ 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;
|
||||
mesh.position.x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 1.4;
|
||||
mesh.position.y = (0.5 - (event.clientY - bounds.top) / bounds.height) * 1.2;
|
||||
};
|
||||
canvas.addEventListener("pointermove", move);
|
||||
|
||||
@@ -270,13 +270,35 @@ log("HDR → exposure → color grading → FXAA → canvas");
|
||||
|
||||
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 });
|
||||
await scene.ready;
|
||||
scene.array("sceneAccent").row(0).set([1, 1, 1, 1]);
|
||||
log("Importing /models/sponza.glb in the importer worker…");
|
||||
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 camera = new ArcRotateCamera(scene, {
|
||||
targetPosition: target,
|
||||
@@ -305,6 +327,7 @@ await pick();
|
||||
return {
|
||||
scene,
|
||||
meshes,
|
||||
lights,
|
||||
camera,
|
||||
picking,
|
||||
async dispose() {
|
||||
|
||||
@@ -72,7 +72,7 @@ follow.stop();
|
||||
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" />
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The shared importer worker fetches and parses glTF/GLB, then the response hydrat
|
||||
import { importGltf } from "@yawn/handles";
|
||||
|
||||
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.
|
||||
@@ -34,7 +34,7 @@ If row allocations relocated since `Picking` was created, refresh its shared des
|
||||
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" />
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const point = new PointLight(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],
|
||||
intensity: 3,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -11,12 +11,12 @@ const pivot = new Node(scene, { position: [0, 1, 0] });
|
||||
await pivot.ready;
|
||||
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
pivot.position[0] += event.movementX * 0.002;
|
||||
pivot.position[1] -= event.movementY * 0.002;
|
||||
pivot.position.x += event.movementX * 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" />
|
||||
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ const mesh = new Mesh(scene, {
|
||||
});
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user