Skip to content

Commit 67a7195

Browse files
committed
docs(skill): use orbit camera and character move as three.js examples
1 parent ddac9ed commit 67a7195

1 file changed

Lines changed: 65 additions & 26 deletions

File tree

‎skills/math/SKILL.md‎

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,36 +66,75 @@ Marshal in, compute, marshal out — and allocate on neither crossing. Keep the
6666

6767
### three.js
6868

69-
`Vector3`, `Quaternion`, `Matrix4`, and `Euler` all marshal through `toArray(target)` and `fromArray(source)`, and the component order matches `math`'s in every case. Always pass your scratch to `toArray` — called bare, it allocates a fresh array. Writing `object.matrix` directly needs `object.matrixAutoUpdate = false` and `object.matrixWorldNeedsUpdate = true`; otherwise marshal back through `position` / `quaternion` / `scale` and let three compose.
69+
`Vector3`, `Quaternion`, `Matrix4`, and `Euler` all marshal through `toArray(target)` and `fromArray(source)`, and the component order matches `math`'s in every case. Always pass your scratch to `toArray` — called bare, it allocates a fresh array.
70+
71+
**Orbit camera.** The camera's state is a `Spherical` and a target the caller owns; three only ever sees the resulting position.
7072

7173
```ts
72-
import { mat4, vec3, type Vec3 } from 'math';
73-
import type { Object3D } from 'three';
74+
import { spherical, vec3 } from 'math';
75+
import type { Camera } from 'three';
76+
77+
const MIN_RADIUS = 1;
78+
const MAX_RADIUS = 100;
79+
80+
export function createOrbit() {
81+
return { target: vec3.create(), orbit: spherical.fromValues(10, 0, Math.PI / 3) };
82+
}
83+
export type Orbit = ReturnType<typeof createOrbit>;
84+
85+
const _orbit_position = vec3.create();
86+
87+
/** Apply a drag in radians and a zoom factor, then place the camera. */
88+
export function updateOrbit(camera: Camera, orbit: Orbit, dragX: number, dragY: number, zoom: number): void {
89+
const s = orbit.orbit;
90+
91+
s[0] = Math.min(MAX_RADIUS, Math.max(MIN_RADIUS, s[0] * zoom));
92+
s[1] -= dragX;
93+
s[2] -= dragY;
94+
spherical.makeSafe(s, s); // keeps phi off the poles, where the frame degenerates
7495

75-
// scratch allocated once, at module scope
76-
const _steer_position = vec3.create();
77-
const _steer_target = vec3.create();
78-
const _steer_direction = vec3.create();
79-
const _steer_matrix = mat4.create();
96+
vec3.add(_orbit_position, spherical.toVec3(_orbit_position, s), orbit.target);
97+
98+
camera.position.fromArray(_orbit_position);
99+
camera.lookAt(orbit.target[0], orbit.target[1], orbit.target[2]);
100+
}
101+
```
102+
103+
**Camera-relative character move.** Takes the yaw straight from that orbit state, so stick-forward means away-from-camera.
104+
105+
```ts
106+
import { quat, vec3, type Vec3 } from 'math';
107+
import type { Object3D } from 'three';
80108

81109
const UP: Vec3 = [0, 1, 0];
82-
const ARRIVED_SQ = 1e-6;
83-
84-
/** Move `object` toward `target`, facing the direction of travel. Set `object.matrixAutoUpdate = false` first. */
85-
export function steer(object: Object3D, target: Object3D, speed: number, delta: number): void {
86-
// in — three writes into our arrays
87-
object.position.toArray(_steer_position);
88-
target.position.toArray(_steer_target);
89-
90-
// compute — plain math, every result in place
91-
vec3.subtract(_steer_direction, _steer_target, _steer_position);
92-
if (vec3.squaredLength(_steer_direction) < ARRIVED_SQ) return;
93-
vec3.normalize(_steer_direction, _steer_direction);
94-
vec3.scaleAndAdd(_steer_position, _steer_position, _steer_direction, speed * delta);
95-
mat4.targetTo(_steer_matrix, _steer_position, _steer_target, UP);
96-
97-
// out — three reads from our array
98-
object.matrix.fromArray(_steer_matrix);
99-
object.matrixWorldNeedsUpdate = true;
110+
const TURN_RATE = 15;
111+
const DEADZONE_SQ = 0.01;
112+
113+
const _move_yaw = quat.create();
114+
const _move_facing = quat.create();
115+
const _move_rotation = quat.create();
116+
const _move_position = vec3.create();
117+
const _move_direction = vec3.create();
118+
119+
/** Move `character` by `inputX` / `inputZ` (a stick, in [-1, 1]) relative to a camera at `yaw`. */
120+
export function moveCharacter(character: Object3D, inputX: number, inputZ: number, yaw: number, speed: number, delta: number): void {
121+
vec3.set(_move_direction, inputX, 0, inputZ);
122+
if (vec3.squaredLength(_move_direction) < DEADZONE_SQ) return; // idle: leave the facing alone
123+
124+
// swing the stick into camera space, then step along it
125+
quat.setAxisAngle(_move_yaw, UP, yaw);
126+
vec3.transformQuat(_move_direction, _move_direction, _move_yaw);
127+
vec3.normalize(_move_direction, _move_direction);
128+
129+
character.position.toArray(_move_position);
130+
vec3.scaleAndAdd(_move_position, _move_position, _move_direction, speed * delta);
131+
132+
// turn toward travel, rather than snapping
133+
character.quaternion.toArray(_move_rotation);
134+
quat.setAxisAngle(_move_facing, UP, Math.atan2(_move_direction[0], _move_direction[2]));
135+
quat.slerp(_move_rotation, _move_rotation, _move_facing, 1 - TURN_RATE ** -delta);
136+
137+
character.position.fromArray(_move_position);
138+
character.quaternion.fromArray(_move_rotation);
100139
}
101140
```

0 commit comments

Comments
 (0)