@solidrt/3d 0.0.46 → 0.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +235 -43
- package/README.md +37 -5
- package/examples/README.md +8 -0
- package/examples/aim.tsx +119 -0
- package/examples/scene-post-effect.tsx +82 -0
- package/examples/sweep-paths.tsx +79 -0
- package/package.json +4 -3
- package/src/components.tsx +42 -9
- package/src/geometry.ts +298 -37
- package/src/glsl.ts +112 -0
- package/src/index.ts +11 -5
- package/src/material.ts +43 -6
- package/src/math.ts +334 -19
- package/src/orbit.ts +187 -27
- package/src/profile.ts +315 -0
- package/src/scene.ts +224 -46
- package/src/sweep.ts +460 -0
package/src/glsl.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// The exported lighting GLSL: string constants an app composes into its
|
|
2
|
+
// own shaderMaterial sources with plain template literals - the same
|
|
3
|
+
// pieces the package's future lit materials will be built from, so a
|
|
4
|
+
// custom material never becomes second-class (no preprocessor, no include
|
|
5
|
+
// resolver; the policy is argued in okf/research/3d-differentiators.md).
|
|
6
|
+
//
|
|
7
|
+
// The light-model functions are PURE: normals, view vectors, light
|
|
8
|
+
// directions, colors and exponents all arrive as arguments, so these
|
|
9
|
+
// constants pin nothing but function names. The LIT_VERTEX pair are the
|
|
10
|
+
// deliberate exception - they pin the standard varying interface
|
|
11
|
+
// (vWorldPos, vNormal, vUv, plus vColor on the colored variant) and
|
|
12
|
+
// consume the standard uniform set (uModel, uViewProj, uNormal);
|
|
13
|
+
// fragments written against those names compose with them directly. All directions are expected normalized;
|
|
14
|
+
// every function returns its raw term, weighting and color belong to the
|
|
15
|
+
// caller.
|
|
16
|
+
|
|
17
|
+
import { glsl } from "@solidrt/core/gpu"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The standard lit vertex stage: clip position via uViewProj * uModel,
|
|
21
|
+
* with world position, world normal (via `mat3(uNormal)`, correct under
|
|
22
|
+
* non-uniform scale) and UV as varyings:
|
|
23
|
+
*
|
|
24
|
+
* in vec3 vWorldPos; in vec3 vNormal; in vec2 vUv;
|
|
25
|
+
*
|
|
26
|
+
* Pair it with your own fragment; the view vector there is
|
|
27
|
+
* `normalize(uCamPos - vWorldPos)`.
|
|
28
|
+
*/
|
|
29
|
+
export const LIT_VERTEX = glsl`
|
|
30
|
+
in vec3 aPos;
|
|
31
|
+
in vec3 aNormal;
|
|
32
|
+
in vec2 aUV;
|
|
33
|
+
uniform mat4 uModel;
|
|
34
|
+
uniform mat4 uViewProj;
|
|
35
|
+
uniform mat4 uNormal;
|
|
36
|
+
out vec3 vWorldPos;
|
|
37
|
+
out vec3 vNormal;
|
|
38
|
+
out vec2 vUv;
|
|
39
|
+
|
|
40
|
+
void main() {
|
|
41
|
+
vec4 world = uModel * vec4(aPos, 1.0);
|
|
42
|
+
gl_Position = uViewProj * world;
|
|
43
|
+
vWorldPos = world.xyz;
|
|
44
|
+
vNormal = mat3(uNormal) * aNormal;
|
|
45
|
+
vUv = aUV;
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* LIT_VERTEX for "colored"-layout geometry: the same interface plus the
|
|
51
|
+
* per-vertex aColor vec4 forwarded raw as `in vec4 vColor` - what it means
|
|
52
|
+
* (a tint, baked AO in one channel, anything) is the fragment's business.
|
|
53
|
+
* Using this constant opts the material into the colored layout (its
|
|
54
|
+
* meshes need withColors() geometry), because shaderMaterial detects
|
|
55
|
+
* aColor in the vertex source.
|
|
56
|
+
*/
|
|
57
|
+
export const LIT_VERTEX_COLORED = glsl`
|
|
58
|
+
in vec3 aPos;
|
|
59
|
+
in vec3 aNormal;
|
|
60
|
+
in vec2 aUV;
|
|
61
|
+
in vec4 aColor;
|
|
62
|
+
uniform mat4 uModel;
|
|
63
|
+
uniform mat4 uViewProj;
|
|
64
|
+
uniform mat4 uNormal;
|
|
65
|
+
out vec3 vWorldPos;
|
|
66
|
+
out vec3 vNormal;
|
|
67
|
+
out vec2 vUv;
|
|
68
|
+
out vec4 vColor;
|
|
69
|
+
|
|
70
|
+
void main() {
|
|
71
|
+
vec4 world = uModel * vec4(aPos, 1.0);
|
|
72
|
+
gl_Position = uViewProj * world;
|
|
73
|
+
vWorldPos = world.xyz;
|
|
74
|
+
vNormal = mat3(uNormal) * aNormal;
|
|
75
|
+
vUv = aUV;
|
|
76
|
+
vColor = aColor;
|
|
77
|
+
}
|
|
78
|
+
`
|
|
79
|
+
|
|
80
|
+
/** `vec3 hemisphere(vec3 n, vec3 sky, vec3 ground)` - ambient from a
|
|
81
|
+
* sky/ground gradient by the normal's vertical tilt: sky straight up,
|
|
82
|
+
* ground bounce straight down. */
|
|
83
|
+
export const HEMISPHERE = glsl`
|
|
84
|
+
vec3 hemisphere(vec3 n, vec3 sky, vec3 ground) {
|
|
85
|
+
return mix(ground, sky, n.y * 0.5 + 0.5);
|
|
86
|
+
}
|
|
87
|
+
`
|
|
88
|
+
|
|
89
|
+
/** `float lambert(vec3 n, vec3 l)` - the diffuse term for a directional
|
|
90
|
+
* light pointing TOWARD the light (multiply by your light color). */
|
|
91
|
+
export const LAMBERT = glsl`
|
|
92
|
+
float lambert(vec3 n, vec3 l) {
|
|
93
|
+
return max(dot(n, l), 0.0);
|
|
94
|
+
}
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
/** `float blinnSpecular(vec3 n, vec3 v, vec3 l, float shininess)` - the
|
|
98
|
+
* Blinn-Phong highlight from the half vector between view and light;
|
|
99
|
+
* shininess runs from wide matte sheen (~8) to tight mirror dot (~150). */
|
|
100
|
+
export const BLINN_SPECULAR = glsl`
|
|
101
|
+
float blinnSpecular(vec3 n, vec3 v, vec3 l, float shininess) {
|
|
102
|
+
return pow(max(dot(n, normalize(l + v)), 0.0), shininess);
|
|
103
|
+
}
|
|
104
|
+
`
|
|
105
|
+
|
|
106
|
+
/** `float fresnel(vec3 n, vec3 v, float power)` - the grazing-angle rim
|
|
107
|
+
* weight (1 at silhouettes, 0 face-on); typical power 3 to 5. */
|
|
108
|
+
export const FRESNEL = glsl`
|
|
109
|
+
float fresnel(vec3 n, vec3 v, float power) {
|
|
110
|
+
return pow(1.0 - max(dot(n, v), 0.0), power);
|
|
111
|
+
}
|
|
112
|
+
`
|
package/src/index.ts
CHANGED
|
@@ -5,15 +5,21 @@
|
|
|
5
5
|
// without Solid components) and the component face (Scene/Mesh/Group/
|
|
6
6
|
// PerspectiveCamera) on top. See AGENTS.md for the model and the traps.
|
|
7
7
|
|
|
8
|
-
export { add, createGroup, createMesh, createScene, remove, setGeometry, setMaterial, setMeshParams, setTransform, setVisible } from "./scene.ts"
|
|
8
|
+
export { add, createGroup, createMesh, createScene, getRotation, lookAt, remove, setGeometry, setMaterial, setMeshParams, setTransform, setVisible, worldPosition } from "./scene.ts"
|
|
9
9
|
export type { CameraUpdate, Mesh as MeshNode, Scene as SceneHandle, SceneNode, SceneOptions, TransformUpdate } from "./scene.ts"
|
|
10
|
-
export { box, disposeGeometry, plane, sphere, torusKnot, FLOATS_PER_VERTEX,
|
|
11
|
-
export type { Geometry } from "./geometry.ts"
|
|
10
|
+
export { box, circle, cone, cylinder, disposeGeometry, fillColors, plane, ring, sphere, torus, torusKnot, withColors, FLOATS_PER_VERTEX, VERTEX_LAYOUTS } from "./geometry.ts"
|
|
11
|
+
export type { ColorFill, Geometry, VertexLayout } from "./geometry.ts"
|
|
12
|
+
export { fillet, roundRect, shape, triangulate } from "./profile.ts"
|
|
13
|
+
export type { Profile, ProfilePoint } from "./profile.ts"
|
|
14
|
+
export { extrude, lathe, pathFrames, sweep, tube } from "./sweep.ts"
|
|
15
|
+
export type { PathFrames, PathPoint, SweepPath } from "./sweep.ts"
|
|
12
16
|
export { shaderMaterial, unlit } from "./material.ts"
|
|
13
17
|
export type { Material, ShaderMaterialOptions, UnlitOptions } from "./material.ts"
|
|
14
18
|
export { Group, Mesh, PerspectiveCamera, Scene, useScene } from "./components.tsx"
|
|
15
19
|
export type { MeshProps, PerspectiveCameraProps, SceneProps, TransformProps } from "./components.tsx"
|
|
16
20
|
export { createOrbitCamera } from "./orbit.ts"
|
|
17
21
|
export type { OrbitCamera, OrbitCameraOptions, OrbitPose } from "./orbit.ts"
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// math's lookAt (the camera view matrix) stays on the /math subpath: the
|
|
23
|
+
// root's lookAt is the scene verb, the same split as `add`.
|
|
24
|
+
export { compose, copy, eulerFromQuat, identity, mat4, multiply, normalMatrix, perspective, quat, quatFromAxisAngle, quatFromEuler, quatFromFrame, quatFromTo, quatMultiply, quatNormalize, quatSlerp } from "./math.ts"
|
|
25
|
+
export type { Mat4, Quat, Vec2, Vec3 } from "./math.ts"
|
package/src/material.ts
CHANGED
|
@@ -36,7 +36,8 @@ import type {
|
|
|
36
36
|
TextureId,
|
|
37
37
|
Topology,
|
|
38
38
|
} from "@solidrt/core/gpu"
|
|
39
|
-
import {
|
|
39
|
+
import { VERTEX_LAYOUTS } from "./geometry.ts"
|
|
40
|
+
import type { VertexLayout } from "./geometry.ts"
|
|
40
41
|
|
|
41
42
|
export type Material = {
|
|
42
43
|
/** The pipeline this material draws with (lazily created). */
|
|
@@ -45,6 +46,15 @@ export type Material = {
|
|
|
45
46
|
params: ShaderParams
|
|
46
47
|
/** Per-entry sampler bindings, when the material samples textures. */
|
|
47
48
|
textures?: Record<string, TextureId>
|
|
49
|
+
/** True when the vertex stage declares `uNormal`: the scene then writes
|
|
50
|
+
* the world matrix's inverse-transpose alongside uModel for meshes using
|
|
51
|
+
* this material (set automatically by shaderMaterial). */
|
|
52
|
+
normalMatrix?: boolean
|
|
53
|
+
/** The vertex layout the pipeline is built for; absent means "standard".
|
|
54
|
+
* shaderMaterial sets "colored" when the vertex stage reads `aColor`. A
|
|
55
|
+
* mesh whose geometry layout differs is rejected at add() - the strides
|
|
56
|
+
* disagree, so a mismatch would render garbage, not just miss a channel. */
|
|
57
|
+
layout?: VertexLayout
|
|
48
58
|
/** Present on materials that own their pipeline (shaderMaterial). */
|
|
49
59
|
dispose?(): void
|
|
50
60
|
}
|
|
@@ -97,7 +107,7 @@ function pipelineFor(kind: "color" | "map"): RenderPipelineId {
|
|
|
97
107
|
})
|
|
98
108
|
let program = linkProgram(sharedVertex, fragment, { label: "scene-unlit-" + kind })
|
|
99
109
|
let pipeline = createRenderPipeline(program, {
|
|
100
|
-
attributes:
|
|
110
|
+
attributes: VERTEX_LAYOUTS.standard,
|
|
101
111
|
depth: true,
|
|
102
112
|
cull: "back",
|
|
103
113
|
label: "scene-unlit-" + kind,
|
|
@@ -140,13 +150,24 @@ export type ShaderMaterialOptions = {
|
|
|
140
150
|
* mesh's world matrix, written per entry whenever the mesh moves) and
|
|
141
151
|
* `uniform mat4 uViewProj` (the camera's view-projection, shared by the
|
|
142
152
|
* whole scene target and written once per camera move) - transform with
|
|
143
|
-
* `uViewProj * uModel * vec4(aPos, 1.0)
|
|
153
|
+
* `uViewProj * uModel * vec4(aPos, 1.0)`; a source mentioning neither
|
|
154
|
+
* throws right here. The rest of the standard uniform set is opt-in by
|
|
155
|
+
* declare-and-use: `uniform mat4 uNormal` (either stage) receives the
|
|
156
|
+
* world inverse-transpose beside uModel - take `mat3(uNormal)` for
|
|
157
|
+
* normals, correct under non-uniform scale - and `uniform vec3 uCamPos`
|
|
158
|
+
* the camera's world position, shared like uViewProj (the specular /
|
|
159
|
+
* fresnel view vector: `uCamPos - worldPos`). Declare any of the
|
|
144
160
|
* layout's `in` attributes (aPos vec3, aNormal vec3, aUV vec2);
|
|
145
|
-
* undeclared ones are skipped.
|
|
161
|
+
* undeclared ones are skipped. Reading `in vec4 aColor` opts the
|
|
162
|
+
* material into the "colored" 12-float layout - the per-vertex data
|
|
163
|
+
* channel (tint, baked AO, any four scalars); its meshes then need
|
|
164
|
+
* withColors() geometry, and a layout mismatch throws at add().
|
|
165
|
+
* `@solidrt/3d/glsl` exports a standard
|
|
166
|
+
* vertex stage and lighting pieces built on exactly this contract.
|
|
146
167
|
*/
|
|
147
168
|
vertex: string
|
|
148
169
|
fragment: string
|
|
149
|
-
/** Uniform seeds beyond
|
|
170
|
+
/** Uniform seeds beyond the standard set; update per mesh later with
|
|
150
171
|
* setMeshParams. */
|
|
151
172
|
params?: ShaderParams
|
|
152
173
|
textures?: Record<string, TextureId>
|
|
@@ -171,9 +192,25 @@ export type ShaderMaterialOptions = {
|
|
|
171
192
|
* and `dispose()` it if the app is done with the look for good.
|
|
172
193
|
*/
|
|
173
194
|
export function shaderMaterial(opts: ShaderMaterialOptions): Material {
|
|
195
|
+
// The standard-set contract, checked where the mistake is made: a vertex
|
|
196
|
+
// stage that never mentions the matrices cannot place meshes, and with
|
|
197
|
+
// shared params skipping undeclared names the omission would otherwise
|
|
198
|
+
// surface as a silently untransformed render, not an error.
|
|
199
|
+
for (let name of ["uModel", "uViewProj"]) {
|
|
200
|
+
if (!new RegExp("\\b" + name + "\\b").test(opts.vertex)) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"shaderMaterial vertex stage must declare and use '" + name + "' (see the standard uniform set in AGENTS.md)",
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
174
206
|
let program: ProgramId | undefined
|
|
175
207
|
let pipeline: RenderPipelineId | undefined
|
|
208
|
+
// Attributes live in the vertex stage only, so unlike the uNormal scan
|
|
209
|
+
// there is nothing to look for in the fragment source.
|
|
210
|
+
let layout: VertexLayout = /\baColor\b/.test(opts.vertex) ? "colored" : "standard"
|
|
176
211
|
return {
|
|
212
|
+
normalMatrix: /\buNormal\b/.test(opts.vertex) || /\buNormal\b/.test(opts.fragment),
|
|
213
|
+
layout,
|
|
177
214
|
pipeline() {
|
|
178
215
|
if (pipeline === undefined) {
|
|
179
216
|
let vs = compileShader("vertex", opts.vertex, { header: needsHeader(opts.vertex) })
|
|
@@ -182,7 +219,7 @@ export function shaderMaterial(opts: ShaderMaterialOptions): Material {
|
|
|
182
219
|
destroyShader(vs)
|
|
183
220
|
destroyShader(fs)
|
|
184
221
|
pipeline = createRenderPipeline(program, {
|
|
185
|
-
attributes:
|
|
222
|
+
attributes: VERTEX_LAYOUTS[layout],
|
|
186
223
|
depth: opts.depth ?? true,
|
|
187
224
|
depthWrite: opts.depthWrite,
|
|
188
225
|
blend: opts.blend,
|
package/src/math.ts
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
// interpreter. Mat4 is a 16-tuple so constant-index access stays plain
|
|
10
10
|
// `number` under noUncheckedIndexedAccess.
|
|
11
11
|
|
|
12
|
+
export type Vec2 = [number, number]
|
|
12
13
|
export type Vec3 = [number, number, number]
|
|
14
|
+
export type Vec4 = [number, number, number, number]
|
|
15
|
+
/** A rotation as [x, y, z, w] - glTF's and Three's component order. */
|
|
16
|
+
export type Quat = [number, number, number, number]
|
|
13
17
|
// prettier-ignore
|
|
14
18
|
export type Mat4 = [
|
|
15
19
|
number, number, number, number,
|
|
@@ -39,6 +43,20 @@ export function copy(out: Mat4, m: Mat4): Mat4 {
|
|
|
39
43
|
return out
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Transform a point by m with w = 1, keeping the homogeneous result: the
|
|
48
|
+
* clip-space building block (scene.project, picking). The caller owns the
|
|
49
|
+
* perspective divide and the w <= 0 behind-the-camera test.
|
|
50
|
+
*/
|
|
51
|
+
export function transformPoint(out: Vec4, m: Mat4, p: Vec3): Vec4 {
|
|
52
|
+
let x = p[0], y = p[1], z = p[2]
|
|
53
|
+
out[0] = m[0] * x + m[4] * y + m[8] * z + m[12]
|
|
54
|
+
out[1] = m[1] * x + m[5] * y + m[9] * z + m[13]
|
|
55
|
+
out[2] = m[2] * x + m[6] * y + m[10] * z + m[14]
|
|
56
|
+
out[3] = m[3] * x + m[7] * y + m[11] * z + m[15]
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
/** out = a * b (column vectors: b applies first). out may alias a or b. */
|
|
43
61
|
export function multiply(out: Mat4, a: Mat4, b: Mat4): Mat4 {
|
|
44
62
|
let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]
|
|
@@ -69,30 +87,51 @@ export function multiply(out: Mat4, a: Mat4, b: Mat4): Mat4 {
|
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
/**
|
|
72
|
-
* Compose translation + rotation + scale into a local matrix
|
|
73
|
-
*
|
|
74
|
-
*
|
|
90
|
+
* Compose translation + rotation + scale into a local matrix - Three's
|
|
91
|
+
* `Matrix4.compose` signature, rotation as a quaternion.
|
|
92
|
+
*
|
|
93
|
+
* `rotation` must be a UNIT quaternion: a non-unit one scales the geometry
|
|
94
|
+
* by |q|^2, silently. The scene closes that trap by normalizing on write
|
|
95
|
+
* (setTransform) rather than paying for a check on every compose.
|
|
75
96
|
*/
|
|
76
|
-
export function compose(out: Mat4, position: Vec3, rotation:
|
|
77
|
-
let
|
|
78
|
-
let
|
|
79
|
-
let
|
|
80
|
-
let
|
|
81
|
-
let
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
let r20 = -sy
|
|
87
|
-
let r21 = cy * sx
|
|
88
|
-
let r22 = cy * cx
|
|
89
|
-
out[0] = r00 * scale[0]; out[1] = r10 * scale[0]; out[2] = r20 * scale[0]; out[3] = 0
|
|
90
|
-
out[4] = r01 * scale[1]; out[5] = r11 * scale[1]; out[6] = r21 * scale[1]; out[7] = 0
|
|
91
|
-
out[8] = r02 * scale[2]; out[9] = r12 * scale[2]; out[10] = r22 * scale[2]; out[11] = 0
|
|
97
|
+
export function compose(out: Mat4, position: Vec3, rotation: Quat, scale: Vec3): Mat4 {
|
|
98
|
+
let x = rotation[0], y = rotation[1], z = rotation[2], w = rotation[3]
|
|
99
|
+
let x2 = x + x, y2 = y + y, z2 = z + z
|
|
100
|
+
let xx = x * x2, xy = x * y2, xz = x * z2
|
|
101
|
+
let yy = y * y2, yz = y * z2, zz = z * z2
|
|
102
|
+
let wx = w * x2, wy = w * y2, wz = w * z2
|
|
103
|
+
let sx = scale[0], sy = scale[1], sz = scale[2]
|
|
104
|
+
out[0] = (1 - (yy + zz)) * sx; out[1] = (xy + wz) * sx; out[2] = (xz - wy) * sx; out[3] = 0
|
|
105
|
+
out[4] = (xy - wz) * sy; out[5] = (1 - (xx + zz)) * sy; out[6] = (yz + wx) * sy; out[7] = 0
|
|
106
|
+
out[8] = (xz + wy) * sz; out[9] = (yz - wx) * sz; out[10] = (1 - (xx + yy)) * sz; out[11] = 0
|
|
92
107
|
out[12] = position[0]; out[13] = position[1]; out[14] = position[2]; out[15] = 1
|
|
93
108
|
return out
|
|
94
109
|
}
|
|
95
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The normal matrix for a world matrix: the inverse-transpose of its upper
|
|
113
|
+
* 3x3 (the cofactor matrix over the determinant), packed into a mat4 - the
|
|
114
|
+
* engine's settable uniform set has no mat3, so shaders take `mat3(uNormal)`.
|
|
115
|
+
* Correct under any transform including non-uniform scale, where
|
|
116
|
+
* `mat3(uModel)` would bend normals off the surface. A degenerate
|
|
117
|
+
* (zero-scale) input yields the raw cofactors instead of NaNs.
|
|
118
|
+
*/
|
|
119
|
+
export function normalMatrix(out: Mat4, m: Mat4): Mat4 {
|
|
120
|
+
let a = m[0], b = m[4], c = m[8]
|
|
121
|
+
let d = m[1], e = m[5], f = m[9]
|
|
122
|
+
let g = m[2], h = m[6], i = m[10]
|
|
123
|
+
let c00 = e * i - f * h
|
|
124
|
+
let c01 = f * g - d * i
|
|
125
|
+
let c02 = d * h - e * g
|
|
126
|
+
let det = a * c00 + b * c01 + c * c02
|
|
127
|
+
let s = 1 / (det || 1)
|
|
128
|
+
out[0] = c00 * s; out[1] = (c * h - b * i) * s; out[2] = (b * f - c * e) * s; out[3] = 0
|
|
129
|
+
out[4] = c01 * s; out[5] = (a * i - c * g) * s; out[6] = (c * d - a * f) * s; out[7] = 0
|
|
130
|
+
out[8] = c02 * s; out[9] = (b * g - a * h) * s; out[10] = (a * e - b * d) * s; out[11] = 0
|
|
131
|
+
out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1
|
|
132
|
+
return out
|
|
133
|
+
}
|
|
134
|
+
|
|
96
135
|
/**
|
|
97
136
|
* Right-handed perspective projection with the engine's y-down clip flip
|
|
98
137
|
* BAKED IN (row two is negated): geometry authored y-up displays y-up, and
|
|
@@ -110,6 +149,278 @@ export function perspective(out: Mat4, fovy: number, aspect: number, near: numbe
|
|
|
110
149
|
return out
|
|
111
150
|
}
|
|
112
151
|
|
|
152
|
+
// Quaternions, the rotation the scene actually stores. Euler triples are a
|
|
153
|
+
// boundary format only - authoring (setTransform's `rotation`, the
|
|
154
|
+
// components' `rotation` prop) and reading back (getRotation) - so the order
|
|
155
|
+
// convention and gimbal lock live at that boundary and nowhere else.
|
|
156
|
+
|
|
157
|
+
/** A fresh identity rotation. */
|
|
158
|
+
export function quat(): Quat {
|
|
159
|
+
return [0, 0, 0, 1]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Unit quaternion; a zero-length input comes back as the identity. `out`
|
|
163
|
+
* may alias `q`. */
|
|
164
|
+
export function quatNormalize(out: Quat, q: Quat): Quat {
|
|
165
|
+
let len = Math.hypot(q[0], q[1], q[2], q[3])
|
|
166
|
+
if (len === 0) {
|
|
167
|
+
out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
170
|
+
out[0] = q[0] / len; out[1] = q[1] / len; out[2] = q[2] / len; out[3] = q[3] / len
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Euler radians to a quaternion, in XYZ order: x applied first, then y,
|
|
176
|
+
* then z (R = Rx * Ry * Rz on column vectors), Three's `Euler` default - a
|
|
177
|
+
* triple copied from a Three scene means the same thing here.
|
|
178
|
+
*
|
|
179
|
+
* ONE order exists, deliberately: a per-call order argument is how the same
|
|
180
|
+
* triple ends up meaning two different things in two places, and the
|
|
181
|
+
* quaternion is right there for anything an order was going to express.
|
|
182
|
+
*/
|
|
183
|
+
export function quatFromEuler(out: Quat, euler: Vec3): Quat {
|
|
184
|
+
let c1 = Math.cos(euler[0] / 2), s1 = Math.sin(euler[0] / 2)
|
|
185
|
+
let c2 = Math.cos(euler[1] / 2), s2 = Math.sin(euler[1] / 2)
|
|
186
|
+
let c3 = Math.cos(euler[2] / 2), s3 = Math.sin(euler[2] / 2)
|
|
187
|
+
out[0] = s1 * c2 * c3 + c1 * s2 * s3
|
|
188
|
+
out[1] = c1 * s2 * c3 - s1 * c2 * s3
|
|
189
|
+
out[2] = c1 * c2 * s3 + s1 * s2 * c3
|
|
190
|
+
out[3] = c1 * c2 * c3 - s1 * s2 * s3
|
|
191
|
+
return out
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A quaternion back to Euler radians in the same XYZ order - the inverse of
|
|
196
|
+
* quatFromEuler, a convenience for reading and debugging rather than a peer
|
|
197
|
+
* of the quaternion: the mapping is many-to-one (a triple and that triple
|
|
198
|
+
* plus a full turn agree), and at the poles (local +z straight up or down)
|
|
199
|
+
* only the sum of x and z is determined, so this pins z to 0 and folds the
|
|
200
|
+
* roll into x. Round-tripping the result reproduces the rotation exactly;
|
|
201
|
+
* it need not reproduce the triple you started from.
|
|
202
|
+
*/
|
|
203
|
+
export function eulerFromQuat(out: Vec3, q: Quat): Vec3 {
|
|
204
|
+
let x = q[0], y = q[1], z = q[2], w = q[3]
|
|
205
|
+
let x2 = x + x, y2 = y + y, z2 = z + z
|
|
206
|
+
let xx = x * x2, xy = x * y2, xz = x * z2
|
|
207
|
+
let yy = y * y2, yz = y * z2, zz = z * z2
|
|
208
|
+
let wx = w * x2, wy = w * y2, wz = w * z2
|
|
209
|
+
// The same matrix entries compose() writes: m02 = sin(y) alone, and the
|
|
210
|
+
// x/z pair reads off the rest unless cos(y) is 0 (the pole).
|
|
211
|
+
let m00 = 1 - (yy + zz)
|
|
212
|
+
let m01 = xy - wz
|
|
213
|
+
let m02 = xz + wy
|
|
214
|
+
// cos(y), which both remaining pairs scale with. atan2 against it beats
|
|
215
|
+
// asin(m02) - Three's form - near the poles, where asin's derivative
|
|
216
|
+
// blows up and a 1e-16 error in m02 becomes 1e-8 in the angle. It also
|
|
217
|
+
// lets the pole branch start three orders of magnitude later: the pairs
|
|
218
|
+
// stay well-conditioned until cos(y) approaches the noise floor.
|
|
219
|
+
let cy = Math.hypot(m00, m01)
|
|
220
|
+
out[1] = Math.atan2(m02, cy)
|
|
221
|
+
if (cy > 1e-7) {
|
|
222
|
+
out[0] = Math.atan2(wx - yz, 1 - (xx + yy))
|
|
223
|
+
out[2] = Math.atan2(-m01, m00)
|
|
224
|
+
} else {
|
|
225
|
+
// Only x + z (at +y) or x - z (at -y) is determined; pin z and fold
|
|
226
|
+
// the whole roll into x.
|
|
227
|
+
out[0] = Math.atan2(yz + wx, 1 - (xx + zz))
|
|
228
|
+
out[2] = 0
|
|
229
|
+
}
|
|
230
|
+
return out
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The rotation of `angle` RADIANS about `axis` - Three's `setFromAxisAngle`,
|
|
235
|
+
* Unity's `AngleAxis` (which takes degrees; this takes radians like
|
|
236
|
+
* everything else here). The axis need not be normalized - the named
|
|
237
|
+
* engines all require a unit axis and silently corrupt the rotation
|
|
238
|
+
* otherwise, the same precondition trap `quatFromTo` closes. A zero axis
|
|
239
|
+
* yields the identity.
|
|
240
|
+
*/
|
|
241
|
+
export function quatFromAxisAngle(out: Quat, axis: Vec3, angle: number): Quat {
|
|
242
|
+
let x = axis[0], y = axis[1], z = axis[2]
|
|
243
|
+
let len = Math.hypot(x, y, z)
|
|
244
|
+
if (len === 0) {
|
|
245
|
+
out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1
|
|
246
|
+
return out
|
|
247
|
+
}
|
|
248
|
+
let s = Math.sin(angle / 2) / len
|
|
249
|
+
out[0] = x * s
|
|
250
|
+
out[1] = y * s
|
|
251
|
+
out[2] = z * s
|
|
252
|
+
out[3] = Math.cos(angle / 2)
|
|
253
|
+
return out
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* out = a * b - the same order contract as the mat4 `multiply` above: on
|
|
258
|
+
* column vectors b applies first, so `quatMultiply(q, spin, q)` composes a
|
|
259
|
+
* further world-frame spin onto q while `quatMultiply(q, q, spin)` spins
|
|
260
|
+
* about q's own local frame. `out` may alias `a` or `b`.
|
|
261
|
+
*
|
|
262
|
+
* The product of unit quaternions is unit up to float drift, so this does
|
|
263
|
+
* not renormalize; an accumulator composed every frame drifts slowly, and
|
|
264
|
+
* the scene's setTransform renormalizes on write anyway.
|
|
265
|
+
*/
|
|
266
|
+
export function quatMultiply(out: Quat, a: Quat, b: Quat): Quat {
|
|
267
|
+
let ax = a[0], ay = a[1], az = a[2], aw = a[3]
|
|
268
|
+
let bx = b[0], by = b[1], bz = b[2], bw = b[3]
|
|
269
|
+
out[0] = aw * bx + ax * bw + ay * bz - az * by
|
|
270
|
+
out[1] = aw * by - ax * bz + ay * bw + az * bx
|
|
271
|
+
out[2] = aw * bz + ax * by - ay * bx + az * bw
|
|
272
|
+
out[3] = aw * bw - ax * bx - ay * by - az * bz
|
|
273
|
+
return out
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Spherical interpolation from `a` to `b`: constant angular velocity along
|
|
278
|
+
* the shortest path (the sign of `b` is flipped when the pair straddles the
|
|
279
|
+
* quaternion double cover, so it never takes the long way round). t = 0 is
|
|
280
|
+
* `a`, t = 1 is `b`'s rotation; inputs must be unit and the result is unit.
|
|
281
|
+
* `out` may alias `a` or `b`.
|
|
282
|
+
*
|
|
283
|
+
* The canonical damped follow is
|
|
284
|
+
* `quatSlerp(q, q, target, 1 - Math.exp(-k * dt))` - frame-rate
|
|
285
|
+
* independent, k is the tracking speed.
|
|
286
|
+
*/
|
|
287
|
+
export function quatSlerp(out: Quat, a: Quat, b: Quat, t: number): Quat {
|
|
288
|
+
let ax = a[0], ay = a[1], az = a[2], aw = a[3]
|
|
289
|
+
let bx = b[0], by = b[1], bz = b[2], bw = b[3]
|
|
290
|
+
let cos = ax * bx + ay * by + az * bz + aw * bw
|
|
291
|
+
if (cos < 0) {
|
|
292
|
+
cos = -cos
|
|
293
|
+
bx = -bx; by = -by; bz = -bz; bw = -bw
|
|
294
|
+
}
|
|
295
|
+
let wa: number
|
|
296
|
+
let wb: number
|
|
297
|
+
if (cos < 0.9995) {
|
|
298
|
+
let theta = Math.acos(cos > 1 ? 1 : cos)
|
|
299
|
+
let sin = Math.sin(theta)
|
|
300
|
+
wa = Math.sin((1 - t) * theta) / sin
|
|
301
|
+
wb = Math.sin(t * theta) / sin
|
|
302
|
+
} else {
|
|
303
|
+
// Nearly identical: sin(theta) is noise, and a straight lerp is within
|
|
304
|
+
// float precision of the arc - normalized below like any other result.
|
|
305
|
+
wa = 1 - t
|
|
306
|
+
wb = t
|
|
307
|
+
}
|
|
308
|
+
out[0] = wa * ax + wb * bx
|
|
309
|
+
out[1] = wa * ay + wb * by
|
|
310
|
+
out[2] = wa * az + wb * bz
|
|
311
|
+
out[3] = wa * aw + wb * bw
|
|
312
|
+
return quatNormalize(out, out)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The shortest-arc rotation taking `from` to `to`: Unity's
|
|
317
|
+
* `Quaternion.FromToRotation`, glam's `Quat::from_rotation_arc`. Three
|
|
318
|
+
* calls this `setFromUnitVectors`; renamed because that name states a
|
|
319
|
+
* precondition instead of the operation, and this one has no such
|
|
320
|
+
* precondition - neither input need be normalized.
|
|
321
|
+
*
|
|
322
|
+
* This is how a y-axis solid gets aimed - `quatFromTo(q, [0, 1, 0], dir)`
|
|
323
|
+
* for a cylinder or cone - where lookAt's +z convention would need a
|
|
324
|
+
* correction. Opposite vectors have no shortest arc (every half turn is
|
|
325
|
+
* equally short); a stable perpendicular axis is picked. A zero-length
|
|
326
|
+
* input yields the identity.
|
|
327
|
+
*/
|
|
328
|
+
export function quatFromTo(out: Quat, from: Vec3, to: Vec3): Quat {
|
|
329
|
+
let ax = from[0], ay = from[1], az = from[2]
|
|
330
|
+
let bx = to[0], by = to[1], bz = to[2]
|
|
331
|
+
let la = Math.hypot(ax, ay, az)
|
|
332
|
+
let lb = Math.hypot(bx, by, bz)
|
|
333
|
+
if (la === 0 || lb === 0) {
|
|
334
|
+
out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1
|
|
335
|
+
return out
|
|
336
|
+
}
|
|
337
|
+
ax /= la; ay /= la; az /= la
|
|
338
|
+
bx /= lb; by /= lb; bz /= lb
|
|
339
|
+
let r = ax * bx + ay * by + az * bz + 1
|
|
340
|
+
if (r < 1e-6) {
|
|
341
|
+
// Antiparallel: the cross product vanishes, so take any perpendicular
|
|
342
|
+
// axis - crossing with the smaller of from's x/z components cannot
|
|
343
|
+
// vanish too, and picking off from alone keeps the choice stable.
|
|
344
|
+
r = 0
|
|
345
|
+
if (Math.abs(ax) > Math.abs(az)) {
|
|
346
|
+
out[0] = -ay; out[1] = ax; out[2] = 0
|
|
347
|
+
} else {
|
|
348
|
+
out[0] = 0; out[1] = -az; out[2] = ay
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
out[0] = ay * bz - az * by
|
|
352
|
+
out[1] = az * bx - ax * bz
|
|
353
|
+
out[2] = ax * by - ay * bx
|
|
354
|
+
}
|
|
355
|
+
out[3] = r
|
|
356
|
+
return quatNormalize(out, out)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The rotation that points the local +z axis along `forward`, with `up`
|
|
361
|
+
* choosing the roll about it - the object-aiming counterpart of lookAt(),
|
|
362
|
+
* which builds the camera's inverse frame. Neither input need be
|
|
363
|
+
* normalized. Degenerate inputs (zero forward, up parallel to forward) fall
|
|
364
|
+
* back to a stable perpendicular instead of producing NaNs.
|
|
365
|
+
*/
|
|
366
|
+
export function quatFromFrame(out: Quat, forward: Vec3, up: Vec3): Quat {
|
|
367
|
+
let zx = forward[0], zy = forward[1], zz = forward[2]
|
|
368
|
+
let len = Math.hypot(zx, zy, zz)
|
|
369
|
+
if (len === 0) {
|
|
370
|
+
zx = 0; zy = 0; zz = 1
|
|
371
|
+
} else {
|
|
372
|
+
zx /= len; zy /= len; zz /= len
|
|
373
|
+
}
|
|
374
|
+
let xx = up[1] * zz - up[2] * zy
|
|
375
|
+
let xy = up[2] * zx - up[0] * zz
|
|
376
|
+
let xz = up[0] * zy - up[1] * zx
|
|
377
|
+
len = Math.hypot(xx, xy, xz)
|
|
378
|
+
if (len === 0) {
|
|
379
|
+
// up is parallel to forward: cross with a world axis that cannot be,
|
|
380
|
+
// picked off z's own components so the choice is stable per direction.
|
|
381
|
+
let ax = Math.abs(zx) < 0.9 ? 1 : 0
|
|
382
|
+
let ay = ax === 1 ? 0 : 1
|
|
383
|
+
xx = ay * zz
|
|
384
|
+
xy = -ax * zz
|
|
385
|
+
xz = ax * zy - ay * zx
|
|
386
|
+
len = Math.hypot(xx, xy, xz)
|
|
387
|
+
}
|
|
388
|
+
xx /= len; xy /= len; xz /= len
|
|
389
|
+
let yx = zy * xz - zz * xy
|
|
390
|
+
let yy = zz * xx - zx * xz
|
|
391
|
+
let yz = zx * xy - zy * xx
|
|
392
|
+
// X | Y | Z are the rotation's columns, so its diagonal is xx, yy, zz.
|
|
393
|
+
// Branching on the largest diagonal entry keeps the divisor away from
|
|
394
|
+
// zero; the basis is orthonormal, so the result is already unit.
|
|
395
|
+
let trace = xx + yy + zz
|
|
396
|
+
if (trace > 0) {
|
|
397
|
+
let s = 0.5 / Math.sqrt(trace + 1)
|
|
398
|
+
out[0] = (yz - zy) * s
|
|
399
|
+
out[1] = (zx - xz) * s
|
|
400
|
+
out[2] = (xy - yx) * s
|
|
401
|
+
out[3] = 0.25 / s
|
|
402
|
+
} else if (xx > yy && xx > zz) {
|
|
403
|
+
let s = 2 * Math.sqrt(1 + xx - yy - zz)
|
|
404
|
+
out[0] = 0.25 * s
|
|
405
|
+
out[1] = (yx + xy) / s
|
|
406
|
+
out[2] = (zx + xz) / s
|
|
407
|
+
out[3] = (yz - zy) / s
|
|
408
|
+
} else if (yy > zz) {
|
|
409
|
+
let s = 2 * Math.sqrt(1 + yy - xx - zz)
|
|
410
|
+
out[0] = (yx + xy) / s
|
|
411
|
+
out[1] = 0.25 * s
|
|
412
|
+
out[2] = (zy + yz) / s
|
|
413
|
+
out[3] = (zx - xz) / s
|
|
414
|
+
} else {
|
|
415
|
+
let s = 2 * Math.sqrt(1 + zz - xx - yy)
|
|
416
|
+
out[0] = (zx + xz) / s
|
|
417
|
+
out[1] = (zy + yz) / s
|
|
418
|
+
out[2] = 0.25 * s
|
|
419
|
+
out[3] = (xy - yx) / s
|
|
420
|
+
}
|
|
421
|
+
return out
|
|
422
|
+
}
|
|
423
|
+
|
|
113
424
|
// Vec3 helpers for geometry construction. These allocate (unlike the matrix
|
|
114
425
|
// functions above): they serve generation-time code - curve frames, normals -
|
|
115
426
|
// not the per-frame path. Exposed on the /math subpath only, so `add` does
|
|
@@ -145,6 +456,10 @@ export function normalize(v: Vec3): Vec3 {
|
|
|
145
456
|
* View matrix (world -> camera) for a camera at `eye` looking at `target`
|
|
146
457
|
* with the given `up`. Degenerate inputs (eye == target, up parallel to the
|
|
147
458
|
* view direction) fall back to axis defaults instead of producing NaNs.
|
|
459
|
+
*
|
|
460
|
+
* On the /math subpath ONLY - the package root's `lookAt` is the scene verb
|
|
461
|
+
* that aims a node (the Matrix4/Object3D split Three makes under the same
|
|
462
|
+
* name), the same collision rule the Vec3 helpers follow.
|
|
148
463
|
*/
|
|
149
464
|
export function lookAt(out: Mat4, eye: Vec3, target: Vec3, up: Vec3): Mat4 {
|
|
150
465
|
let zx = eye[0] - target[0]
|