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:
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user