@solidrt/3d 0.0.51 → 0.0.53
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 +354 -91
- package/README.md +13 -9
- package/examples/README.md +37 -2
- package/examples/aim.tsx +5 -5
- package/examples/cascades.tsx +121 -0
- package/examples/instanced.tsx +4 -4
- package/examples/lit.tsx +66 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +51 -0
- package/examples/pick.tsx +5 -5
- package/examples/scene-background.tsx +3 -3
- package/examples/scene-basic.tsx +3 -3
- package/examples/scene-post-effect.tsx +3 -3
- package/examples/scene-views.tsx +95 -0
- package/examples/shadows.tsx +86 -0
- package/examples/sprites.tsx +95 -0
- package/examples/sweep-paths.tsx +11 -27
- package/package.json +4 -3
- package/src/components.tsx +110 -14
- package/src/geometry-gpu.ts +13 -2
- package/src/geometry.ts +331 -127
- package/src/glsl.ts +138 -3
- package/src/gltf.ts +437 -0
- package/src/index.ts +17 -12
- package/src/material.ts +353 -47
- package/src/math.ts +145 -0
- package/src/model-file.ts +122 -0
- package/src/model.ts +105 -0
- package/src/orbit.ts +13 -9
- package/src/order.ts +12 -5
- package/src/profile.ts +4 -8
- package/src/scene.ts +1294 -283
- package/src/sweep.ts +21 -36
- package/tools/model.ts +52 -0
- package/src/bvh.ts +0 -258
package/src/glsl.ts
CHANGED
|
@@ -16,6 +16,22 @@
|
|
|
16
16
|
|
|
17
17
|
import { glsl } from "@solidrt/core/gpu"
|
|
18
18
|
|
|
19
|
+
/** The directional-light cap of the scene's light list (DirectionalLight nodes)
|
|
20
|
+
* and of the `lit` fragment; a custom fragment declares
|
|
21
|
+
* `uniform vec3 uLightDir[MAX_LIGHTS]` / `uLightColor[MAX_LIGHTS]` and
|
|
22
|
+
* loops to `uLightCount`. A shader-source constant, so it is fixed for
|
|
23
|
+
* the app (see okf/backlog/app-runtime-config.md). */
|
|
24
|
+
export const MAX_LIGHTS = 4
|
|
25
|
+
|
|
26
|
+
/** The most cascades one casting light splits its shadow into
|
|
27
|
+
* (`shadow.cascades`, 1..MAX_CASCADES). */
|
|
28
|
+
export const MAX_CASCADES = 4
|
|
29
|
+
|
|
30
|
+
/** The shadow-map slot count of the scene's shadow set: every casting
|
|
31
|
+
* light owns `shadow.cascades` consecutive slots (one per map), so this
|
|
32
|
+
* bounds `uShadowRect`/`uShadowMatrix`. */
|
|
33
|
+
export const MAX_SHADOW_MAPS = MAX_LIGHTS * MAX_CASCADES
|
|
34
|
+
|
|
19
35
|
/**
|
|
20
36
|
* The standard lit vertex stage: clip position via uViewProj * uModel,
|
|
21
37
|
* with world position, world normal (via `mat3(uNormal)`, correct under
|
|
@@ -50,9 +66,9 @@ export const LIT_VERTEX = glsl`
|
|
|
50
66
|
* LIT_VERTEX for "colored"-layout geometry: the same interface plus the
|
|
51
67
|
* per-vertex aColor vec4 forwarded raw as `in vec4 vColor` - what it means
|
|
52
68
|
* (a tint, baked AO in one channel, anything) is the fragment's business.
|
|
53
|
-
* Using this constant
|
|
54
|
-
*
|
|
55
|
-
*
|
|
69
|
+
* Using this constant makes the material read aColor (shaderMaterial
|
|
70
|
+
* collects the vertex stage's `in` declarations), so its meshes need
|
|
71
|
+
* geometry carrying that channel - withColors() - or add() throws.
|
|
56
72
|
*/
|
|
57
73
|
export const LIT_VERTEX_COLORED = glsl`
|
|
58
74
|
in vec3 aPos;
|
|
@@ -110,3 +126,122 @@ export const FRESNEL = glsl`
|
|
|
110
126
|
return pow(1.0 - max(dot(n, v), 0.0), power);
|
|
111
127
|
}
|
|
112
128
|
`
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The scene's shadow set as a receiving program declares it: ONE
|
|
132
|
+
* `uShadowAtlas` (every casting light's depth map is a tile of it, so N
|
|
133
|
+
* maps render as one pass; a white texel when nothing casts), a MAP slot
|
|
134
|
+
* set of `MAX_SHADOW_MAPS` - `uShadowRect[M]` (map slot j's tile as x, y,
|
|
135
|
+
* width, height in atlas 0..1 UV) and `uShadowMatrix[M]` (its light-space
|
|
136
|
+
* viewProj) - and, per directional light index, `uShadowFirst[N]` /
|
|
137
|
+
* `uShadowCount[N]` (light i's maps are slots `first .. first + count - 1`;
|
|
138
|
+
* count 0 = it does not cast; a box light has one map, a cascaded light
|
|
139
|
+
* `shadow.cascades` of them, tightest first), `uShadowBias[N]` and
|
|
140
|
+
* `uShadowNormalBias[N]`. The scene binds and writes all of it on every
|
|
141
|
+
* target a receiving material can draw into; a custom material composes
|
|
142
|
+
* this, then SHADOW, then SHADOW_LOOKUP (in that order) and multiplies
|
|
143
|
+
* light i's term by `lightShadow(i, worldPos, n)` - `lit` is the shape. A
|
|
144
|
+
* material that does not receive composes none of it, so it declares no
|
|
145
|
+
* sampler for nothing.
|
|
146
|
+
*/
|
|
147
|
+
export const SHADOW_SLOTS = glsl`
|
|
148
|
+
uniform sampler2D uShadowAtlas;
|
|
149
|
+
uniform vec4 uShadowRect[${MAX_SHADOW_MAPS}];
|
|
150
|
+
uniform mat4 uShadowMatrix[${MAX_SHADOW_MAPS}];
|
|
151
|
+
uniform int uShadowFirst[${MAX_LIGHTS}];
|
|
152
|
+
uniform int uShadowCount[${MAX_LIGHTS}];
|
|
153
|
+
uniform float uShadowBias[${MAX_LIGHTS}];
|
|
154
|
+
uniform float uShadowNormalBias[${MAX_LIGHTS}];
|
|
155
|
+
`
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The shadow lookup in three steps plus their composition, one map at a
|
|
159
|
+
* time. `vec3 shadowPoint(vec4 coord)` takes a world point carried into a
|
|
160
|
+
* casting light's clip space by that map's `uShadowMatrix[j]` to its map
|
|
161
|
+
* point: xy in 0..1 across the map, z the depth to compare. `bool
|
|
162
|
+
* shadowInside(vec3 p)` is whether the map has it at all (xy in 0..1, z
|
|
163
|
+
* not past the far plane) - the cascade select. `float shadowSample(
|
|
164
|
+
* sampler2D map, vec4 rect, vec3 p, float bias)` is the factor (1 lit, 0
|
|
165
|
+
* shadowed) of a point the map has: a 3x3 PCF over texel neighbours in
|
|
166
|
+
* the map's tile `rect` (x, y, width, height in `map`'s 0..1 UV;
|
|
167
|
+
* `vec4(0, 0, 1, 1)` is a whole map) comparing the map's `.r` (a stage-1
|
|
168
|
+
* depth texture samples nearest, so the softness is this loop, not the
|
|
169
|
+
* sampler); every tap is clamped to the tile inset by half a texel, so
|
|
170
|
+
* no tap reads a neighbouring map's tile; `bias` is subtracted from the
|
|
171
|
+
* point's depth against acne. `float shadow(sampler2D map, vec4 rect,
|
|
172
|
+
* vec4 coord, float bias)` composes the three: 1 (lit) outside the map,
|
|
173
|
+
* else the sample -
|
|
174
|
+
* `shadow(uShadowAtlas, uShadowRect[0], uShadowMatrix[0] * vec4(vWorldPos, 1.0), uShadowBias[0])`.
|
|
175
|
+
* SHADOW_LOOKUP uses the steps, so it projects each map once.
|
|
176
|
+
*/
|
|
177
|
+
export const SHADOW = glsl`
|
|
178
|
+
vec3 shadowPoint(vec4 coord) {
|
|
179
|
+
return coord.xyz / coord.w * 0.5 + 0.5;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
bool shadowInside(vec3 p) {
|
|
183
|
+
return all(greaterThanEqual(p.xy, vec2(0.0))) && all(lessThanEqual(p, vec3(1.0)));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
float shadowSample(sampler2D map, vec4 rect, vec3 p, float bias) {
|
|
187
|
+
vec2 texel = 1.0 / vec2(textureSize(map, 0));
|
|
188
|
+
vec2 lo = rect.xy + 0.5 * texel;
|
|
189
|
+
vec2 hi = rect.xy + rect.zw - 0.5 * texel;
|
|
190
|
+
vec2 base = rect.xy + p.xy * rect.zw;
|
|
191
|
+
float lit = 0.0;
|
|
192
|
+
for (int y = -1; y <= 1; y++) {
|
|
193
|
+
for (int x = -1; x <= 1; x++) {
|
|
194
|
+
float d = texture(map, clamp(base + vec2(float(x), float(y)) * texel, lo, hi)).r;
|
|
195
|
+
lit += p.z - bias <= d ? 1.0 : 0.0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return lit / 9.0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
float shadow(sampler2D map, vec4 rect, vec4 coord, float bias) {
|
|
202
|
+
vec3 p = shadowPoint(coord);
|
|
203
|
+
return shadowInside(p) ? shadowSample(map, rect, p, bias) : 1.0;
|
|
204
|
+
}
|
|
205
|
+
`
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The step from a light index to its shadow factor, over SHADOW_SLOTS and
|
|
209
|
+
* SHADOW (compose both first). `float lightShadow(int i, vec3 worldPos,
|
|
210
|
+
* vec3 n)` is the one to call per light: 1 for a light that does not
|
|
211
|
+
* cast, else the factor for `worldPos` pushed along its normal `n` by
|
|
212
|
+
* `uShadowNormalBias[i]` (the acne knob to reach for first), looked up in
|
|
213
|
+
* the FIRST of light i's maps that has the point (a box light has one; a
|
|
214
|
+
* cascaded light's maps come tightest first, so the sharpest cascade
|
|
215
|
+
* that has the point wins and a point past the last is lit) with that
|
|
216
|
+
* map's `uShadowMatrix[j]`, its tile and `uShadowBias[i]`. Inside the
|
|
217
|
+
* outer SHADOW_BLEND of a map (in map 0..1 units, so 0.1 is its outer
|
|
218
|
+
* 10% on each side) the factor fades into the next cascade's, so the
|
|
219
|
+
* hand-over is a band and not a seam; the last map, a box light's only
|
|
220
|
+
* one, and any rim the next cascade does not reach (the near side, at
|
|
221
|
+
* the camera's feet) have no band. Position and normal are arguments, so
|
|
222
|
+
* no varying name is pinned and a custom vertex stage composes freely.
|
|
223
|
+
*/
|
|
224
|
+
export const SHADOW_LOOKUP = glsl`
|
|
225
|
+
const float SHADOW_BLEND = 0.1;
|
|
226
|
+
|
|
227
|
+
float lightShadow(int i, vec3 worldPos, vec3 n) {
|
|
228
|
+
int count = uShadowCount[i];
|
|
229
|
+
if (count == 0) return 1.0;
|
|
230
|
+
vec4 w = vec4(worldPos + n * uShadowNormalBias[i], 1.0);
|
|
231
|
+
float bias = uShadowBias[i];
|
|
232
|
+
int first = uShadowFirst[i];
|
|
233
|
+
int last = first + count - 1;
|
|
234
|
+
for (int j = first; j <= last; j++) {
|
|
235
|
+
vec3 p = shadowPoint(uShadowMatrix[j] * w);
|
|
236
|
+
if (!shadowInside(p)) continue;
|
|
237
|
+
float s = shadowSample(uShadowAtlas, uShadowRect[j], p, bias);
|
|
238
|
+
if (j == last) return s;
|
|
239
|
+
float edge = min(min(p.x, 1.0 - p.x), min(p.y, 1.0 - p.y));
|
|
240
|
+
if (edge >= SHADOW_BLEND) return s;
|
|
241
|
+
vec3 q = shadowPoint(uShadowMatrix[j + 1] * w);
|
|
242
|
+
if (!shadowInside(q)) return s;
|
|
243
|
+
return mix(shadowSample(uShadowAtlas, uShadowRect[j + 1], q, bias), s, edge / SHADOW_BLEND);
|
|
244
|
+
}
|
|
245
|
+
return 1.0;
|
|
246
|
+
}
|
|
247
|
+
`
|
package/src/gltf.ts
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// glTF 2.0, the subset an app needs to show authored models: the scene's
|
|
2
|
+
// node tree with world transforms BAKED into the vertices (one part per
|
|
3
|
+
// mesh node, its name kept), triangles with positions, normals (flat ones
|
|
4
|
+
// generated when absent, per the spec), one UV set and indices, and
|
|
5
|
+
// materials reduced to what lit()/unlit() draw - base color factor and
|
|
6
|
+
// texture, double-sidedness, alpha blending. Both containers: .gltf JSON
|
|
7
|
+
// with external or data: buffers and images, and single-file .glb.
|
|
8
|
+
//
|
|
9
|
+
// Pure module by design - a parse is JSON plus typed-array views plus one
|
|
10
|
+
// interleave loop per primitive, so it runs the same under bun (the bake
|
|
11
|
+
// tool in tools/model.ts, the check rig) and on flux (loadGltf in
|
|
12
|
+
// model.ts). It never decodes images: material.map indexes the encoded
|
|
13
|
+
// bytes in `images`, and uploading is the engine side's job.
|
|
14
|
+
//
|
|
15
|
+
// Outside the subset: Draco/meshopt-compressed meshes and any other
|
|
16
|
+
// required extension throw naming it; non-triangle primitives, sparse
|
|
17
|
+
// accessors and morph/skin data are skipped or ignored; vertex colors,
|
|
18
|
+
// tangents and further UV sets are dropped (the standard layout has no
|
|
19
|
+
// slot for them yet).
|
|
20
|
+
|
|
21
|
+
import { compose, mat4, multiply, normalMatrix } from "./math.ts"
|
|
22
|
+
import type { Mat4, Quat, Vec3 } from "./math.ts"
|
|
23
|
+
import { packGeometry, STANDARD_FLOATS } from "./geometry.ts"
|
|
24
|
+
import type { Geometry } from "./geometry.ts"
|
|
25
|
+
|
|
26
|
+
/** What lit()/unlit() take from a glTF material. */
|
|
27
|
+
export type ModelMaterial = {
|
|
28
|
+
name: string
|
|
29
|
+
/** Straight [r, g, b, a] 0..1 (glTF baseColorFactor). */
|
|
30
|
+
color: [number, number, number, number]
|
|
31
|
+
/** Index into ModelData.images (the base color texture), or null. */
|
|
32
|
+
map: number | null
|
|
33
|
+
/** glTF doubleSided. Reported, not applied: the standard materials cull
|
|
34
|
+
* back faces. */
|
|
35
|
+
doubleSided: boolean
|
|
36
|
+
/** alphaMode BLEND. MASK draws opaque (no alpha test). */
|
|
37
|
+
transparent: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One drawable: a mesh node's primitive, vertices in WORLD space. */
|
|
41
|
+
export type ModelPart = {
|
|
42
|
+
/** The glTF node's name (or the mesh's, or `node<i>`); a node with
|
|
43
|
+
* several primitives numbers them `name#<k>`. */
|
|
44
|
+
name: string
|
|
45
|
+
geometry: Geometry
|
|
46
|
+
/** Index into ModelData.materials. */
|
|
47
|
+
material: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A parsed model: plain data, no GPU resources. What parseGltf and
|
|
51
|
+
* decodeModel produce and createModel consumes. */
|
|
52
|
+
export type ModelData = {
|
|
53
|
+
parts: ModelPart[]
|
|
54
|
+
materials: ModelMaterial[]
|
|
55
|
+
/** Encoded image files (PNG/JPEG bytes) the materials' `map` index. */
|
|
56
|
+
images: Uint8Array[]
|
|
57
|
+
/** World-space [minX, minY, minZ, maxX, maxY, maxZ] over every part. */
|
|
58
|
+
bounds: Float32Array
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Resolves a relative uri of a .gltf (its .bin buffers, image files) to
|
|
62
|
+
* bytes. Not needed for .glb or data: uris. */
|
|
63
|
+
export type UriResolver = (uri: string) => Uint8Array
|
|
64
|
+
|
|
65
|
+
const GLB_MAGIC = 0x46546c67
|
|
66
|
+
const CHUNK_JSON = 0x4e4f534a
|
|
67
|
+
const CHUNK_BIN = 0x004e4942
|
|
68
|
+
const MODE_TRIANGLES = 4
|
|
69
|
+
|
|
70
|
+
const COMPONENT_BYTES: Record<number, number> = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 }
|
|
71
|
+
const TYPE_ELEMENTS: Record<string, number> = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 }
|
|
72
|
+
|
|
73
|
+
const DEFAULT_MATERIAL: ModelMaterial = { name: "default", color: [1, 1, 1, 1], map: null, doubleSided: false, transparent: false }
|
|
74
|
+
|
|
75
|
+
/** True when the bytes are a .glb container (the "glTF" magic). */
|
|
76
|
+
export function isGlb(bytes: Uint8Array): boolean {
|
|
77
|
+
return bytes.length >= 12 && new DataView(bytes.buffer, bytes.byteOffset, 12).getUint32(0, true) === GLB_MAGIC
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The external uris a .gltf document references (buffers and images), so
|
|
81
|
+
* an async caller can fetch them before parseGltf. Empty for .glb and
|
|
82
|
+
* data: uris. */
|
|
83
|
+
export function gltfExternalUris(bytes: Uint8Array): string[] {
|
|
84
|
+
if (isGlb(bytes)) return []
|
|
85
|
+
let gltf = JSON.parse(new TextDecoder().decode(bytes))
|
|
86
|
+
let uris: string[] = []
|
|
87
|
+
for (let item of [...(gltf.buffers ?? []), ...(gltf.images ?? [])]) {
|
|
88
|
+
if (typeof item.uri === "string" && !item.uri.startsWith("data:")) uris.push(item.uri)
|
|
89
|
+
}
|
|
90
|
+
return uris
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse a .glb or .gltf into ModelData. `resolve` supplies the bytes of a
|
|
95
|
+
* .gltf's external files by their uri as written in the document (still
|
|
96
|
+
* percent-encoded); omit it for .glb.
|
|
97
|
+
*/
|
|
98
|
+
export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
99
|
+
let gltf: any
|
|
100
|
+
let bin: Uint8Array | null = null
|
|
101
|
+
if (isGlb(bytes)) {
|
|
102
|
+
let glb = readGlb(bytes)
|
|
103
|
+
gltf = glb.json
|
|
104
|
+
bin = glb.bin
|
|
105
|
+
} else {
|
|
106
|
+
gltf = JSON.parse(new TextDecoder().decode(bytes))
|
|
107
|
+
}
|
|
108
|
+
if (gltf.asset?.version !== undefined && !String(gltf.asset.version).startsWith("2")) {
|
|
109
|
+
throw new Error("parseGltf: glTF version " + gltf.asset.version + " (only 2.x is supported)")
|
|
110
|
+
}
|
|
111
|
+
for (let ext of gltf.extensionsRequired ?? []) {
|
|
112
|
+
if (ext === "KHR_draco_mesh_compression" || ext === "EXT_meshopt_compression") {
|
|
113
|
+
throw new Error("parseGltf: the file's meshes are compressed (" + ext + "), which is not supported: re-export without mesh compression")
|
|
114
|
+
}
|
|
115
|
+
// Quantized attributes read through the normalized-integer path; every
|
|
116
|
+
// other required extension changes what the file means.
|
|
117
|
+
if (ext !== "KHR_mesh_quantization") {
|
|
118
|
+
throw new Error("parseGltf: the file requires the " + ext + " extension, which is not supported")
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let external = (uri: string, what: string): Uint8Array => {
|
|
123
|
+
if (uri.startsWith("data:")) return decodeDataUri(uri)
|
|
124
|
+
if (resolve === undefined) throw new Error("parseGltf: " + what + " references the external file " + uri + " and no resolver was given")
|
|
125
|
+
return resolve(uri)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let buffers: Uint8Array[] = (gltf.buffers ?? []).map((b: any, i: number): Uint8Array => {
|
|
129
|
+
if (b.uri === undefined) {
|
|
130
|
+
if (bin === null) throw new Error("parseGltf: buffer " + i + " has no uri and the file has no binary chunk")
|
|
131
|
+
return bin
|
|
132
|
+
}
|
|
133
|
+
return external(b.uri, "buffer " + i)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
let bufferViewBytes = (index: number): Uint8Array => {
|
|
137
|
+
let view = gltf.bufferViews[index]
|
|
138
|
+
let buffer = buffers[view.buffer]
|
|
139
|
+
if (buffer === undefined) throw new Error("parseGltf: bufferView " + index + " names a missing buffer")
|
|
140
|
+
return buffer.subarray(view.byteOffset ?? 0, (view.byteOffset ?? 0) + view.byteLength)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Images are pulled in only when a material samples them, in first-use
|
|
144
|
+
// order, so `map` indexes a compact list.
|
|
145
|
+
let images: Uint8Array[] = []
|
|
146
|
+
let imageSlots = new Map<number, number>()
|
|
147
|
+
let imageSlot = (index: number): number => {
|
|
148
|
+
let slot = imageSlots.get(index)
|
|
149
|
+
if (slot === undefined) {
|
|
150
|
+
let image = gltf.images?.[index]
|
|
151
|
+
if (image === undefined) throw new Error("parseGltf: texture names a missing image " + index)
|
|
152
|
+
let bytes = image.uri !== undefined ? external(image.uri, "image " + index) : bufferViewBytes(image.bufferView)
|
|
153
|
+
slot = images.length
|
|
154
|
+
images.push(bytes)
|
|
155
|
+
imageSlots.set(index, slot)
|
|
156
|
+
}
|
|
157
|
+
return slot
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let materials: ModelMaterial[] = (gltf.materials ?? []).map((m: any, i: number): ModelMaterial => {
|
|
161
|
+
let pbr = m.pbrMetallicRoughness ?? {}
|
|
162
|
+
let factor = pbr.baseColorFactor ?? [1, 1, 1, 1]
|
|
163
|
+
let map: number | null = null
|
|
164
|
+
if (pbr.baseColorTexture !== undefined) {
|
|
165
|
+
let texture = gltf.textures?.[pbr.baseColorTexture.index]
|
|
166
|
+
if (texture?.source !== undefined) map = imageSlot(texture.source)
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
name: m.name ?? "material" + i,
|
|
170
|
+
color: [factor[0], factor[1], factor[2], factor[3] ?? 1],
|
|
171
|
+
map,
|
|
172
|
+
doubleSided: m.doubleSided === true,
|
|
173
|
+
transparent: m.alphaMode === "BLEND",
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
// Primitives without a material draw the spec's default; it is appended
|
|
177
|
+
// only when something uses it.
|
|
178
|
+
let defaultMaterial = -1
|
|
179
|
+
|
|
180
|
+
let accessorFloats = (index: number, what: string): { data: Float32Array; elements: number; count: number } => {
|
|
181
|
+
let acc = gltf.accessors[index]
|
|
182
|
+
if (acc === undefined) throw new Error("parseGltf: " + what + " names a missing accessor " + index)
|
|
183
|
+
if (acc.sparse !== undefined) throw new Error("parseGltf: " + what + " uses a sparse accessor, which is not supported")
|
|
184
|
+
let elements = TYPE_ELEMENTS[acc.type]
|
|
185
|
+
let compBytes = COMPONENT_BYTES[acc.componentType]
|
|
186
|
+
if (elements === undefined || compBytes === undefined) throw new Error("parseGltf: " + what + " has an unknown accessor type")
|
|
187
|
+
let out = new Float32Array(acc.count * elements)
|
|
188
|
+
if (acc.bufferView === undefined) return { data: out, elements, count: acc.count }
|
|
189
|
+
let view = gltf.bufferViews[acc.bufferView]
|
|
190
|
+
let bytes = bufferViewBytes(acc.bufferView)
|
|
191
|
+
let stride = view.byteStride ?? compBytes * elements
|
|
192
|
+
let dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
193
|
+
let base = acc.byteOffset ?? 0
|
|
194
|
+
let normalized = acc.normalized === true
|
|
195
|
+
let read = readerFor(acc.componentType, normalized)
|
|
196
|
+
for (let i = 0; i < acc.count; i++) {
|
|
197
|
+
let at = base + i * stride
|
|
198
|
+
for (let e = 0; e < elements; e++) out[i * elements + e] = read(dv, at + e * compBytes)
|
|
199
|
+
}
|
|
200
|
+
return { data: out, elements, count: acc.count }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let parts: ModelPart[] = []
|
|
204
|
+
let bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
|
|
205
|
+
let normal = mat4()
|
|
206
|
+
|
|
207
|
+
let emit = (prim: any, name: string, world: Mat4): void => {
|
|
208
|
+
if ((prim.mode ?? MODE_TRIANGLES) !== MODE_TRIANGLES) return
|
|
209
|
+
if (prim.attributes?.POSITION === undefined) return
|
|
210
|
+
let pos = accessorFloats(prim.attributes.POSITION, name + " POSITION")
|
|
211
|
+
if (pos.elements !== 3) throw new Error("parseGltf: " + name + " POSITION is not VEC3")
|
|
212
|
+
let count = pos.count
|
|
213
|
+
let nrm = prim.attributes.NORMAL !== undefined ? accessorFloats(prim.attributes.NORMAL, name + " NORMAL").data : null
|
|
214
|
+
let uv = prim.attributes.TEXCOORD_0 !== undefined ? accessorFloats(prim.attributes.TEXCOORD_0, name + " TEXCOORD_0").data : null
|
|
215
|
+
let indices: ArrayLike<number> =
|
|
216
|
+
prim.indices !== undefined ? accessorFloats(prim.indices, name + " indices").data : Array.from({ length: count }, (_, i) => i)
|
|
217
|
+
if (indices.length % 3 !== 0) throw new Error("parseGltf: " + name + " index count is not a multiple of 3")
|
|
218
|
+
|
|
219
|
+
normalMatrix(normal, world)
|
|
220
|
+
let flip = det3(world) < 0
|
|
221
|
+
|
|
222
|
+
let vertices: Float32Array
|
|
223
|
+
let packedIndices: number[]
|
|
224
|
+
if (nrm !== null) {
|
|
225
|
+
vertices = new Float32Array(count * STANDARD_FLOATS)
|
|
226
|
+
for (let i = 0; i < count; i++) writeVertex(vertices, i, pos.data, nrm, uv, i, world, normal)
|
|
227
|
+
packedIndices = new Array(indices.length)
|
|
228
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
229
|
+
packedIndices[i] = indices[i]!
|
|
230
|
+
packedIndices[i + 1] = flip ? indices[i + 2]! : indices[i + 1]!
|
|
231
|
+
packedIndices[i + 2] = flip ? indices[i + 1]! : indices[i + 2]!
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// No normals: the spec asks for flat shading, which needs one vertex
|
|
235
|
+
// per triangle corner, so the primitive is un-indexed here and each
|
|
236
|
+
// corner takes its face normal (computed in world space, after the
|
|
237
|
+
// bake, so a mirroring transform is already accounted for).
|
|
238
|
+
let triangles = indices.length / 3
|
|
239
|
+
vertices = new Float32Array(triangles * 3 * STANDARD_FLOATS)
|
|
240
|
+
packedIndices = new Array(triangles * 3)
|
|
241
|
+
let face: Vec3 = [0, 0, 0]
|
|
242
|
+
for (let t = 0; t < triangles; t++) {
|
|
243
|
+
let a = indices[t * 3]!, b = indices[t * 3 + 1]!, c = indices[t * 3 + 2]!
|
|
244
|
+
if (flip) [b, c] = [c, b]
|
|
245
|
+
let out = t * 3
|
|
246
|
+
writeVertex(vertices, out, pos.data, null, uv, a, world, normal)
|
|
247
|
+
writeVertex(vertices, out + 1, pos.data, null, uv, b, world, normal)
|
|
248
|
+
writeVertex(vertices, out + 2, pos.data, null, uv, c, world, normal)
|
|
249
|
+
faceNormal(face, vertices, out)
|
|
250
|
+
for (let k = 0; k < 3; k++) {
|
|
251
|
+
let at = (out + k) * STANDARD_FLOATS + 3
|
|
252
|
+
vertices[at] = face[0]
|
|
253
|
+
vertices[at + 1] = face[1]
|
|
254
|
+
vertices[at + 2] = face[2]
|
|
255
|
+
packedIndices[out + k] = out + k
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
count = triangles * 3
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < count; i++) {
|
|
262
|
+
let at = i * STANDARD_FLOATS
|
|
263
|
+
let x = vertices[at]!, y = vertices[at + 1]!, z = vertices[at + 2]!
|
|
264
|
+
if (x < bounds[0]!) bounds[0] = x
|
|
265
|
+
if (y < bounds[1]!) bounds[1] = y
|
|
266
|
+
if (z < bounds[2]!) bounds[2] = z
|
|
267
|
+
if (x > bounds[3]!) bounds[3] = x
|
|
268
|
+
if (y > bounds[4]!) bounds[4] = y
|
|
269
|
+
if (z > bounds[5]!) bounds[5] = z
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let material = prim.material
|
|
273
|
+
if (material === undefined) {
|
|
274
|
+
if (defaultMaterial < 0) {
|
|
275
|
+
defaultMaterial = materials.length
|
|
276
|
+
materials.push({ ...DEFAULT_MATERIAL })
|
|
277
|
+
}
|
|
278
|
+
material = defaultMaterial
|
|
279
|
+
}
|
|
280
|
+
parts.push({ name, geometry: packGeometry(vertices, packedIndices, { label: name }), material })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let local = mat4()
|
|
284
|
+
let walk = (index: number, parent: Mat4): void => {
|
|
285
|
+
let node = gltf.nodes[index]
|
|
286
|
+
if (node === undefined) throw new Error("parseGltf: scene names a missing node " + index)
|
|
287
|
+
let world = mat4()
|
|
288
|
+
multiply(world, parent, nodeMatrix(local, node))
|
|
289
|
+
if (node.mesh !== undefined) {
|
|
290
|
+
let mesh = gltf.meshes[node.mesh]
|
|
291
|
+
if (mesh === undefined) throw new Error("parseGltf: node " + index + " names a missing mesh " + node.mesh)
|
|
292
|
+
let name = node.name ?? mesh.name ?? "node" + index
|
|
293
|
+
let prims: any[] = mesh.primitives ?? []
|
|
294
|
+
for (let k = 0; k < prims.length; k++) emit(prims[k], prims.length > 1 ? name + "#" + k : name, world)
|
|
295
|
+
}
|
|
296
|
+
for (let child of node.children ?? []) walk(child, world)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let scene = gltf.scenes?.[gltf.scene ?? 0]
|
|
300
|
+
let roots: number[] = scene?.nodes ?? (gltf.nodes ?? []).map((_: unknown, i: number) => i)
|
|
301
|
+
let root = mat4()
|
|
302
|
+
for (let index of roots) walk(index, root)
|
|
303
|
+
|
|
304
|
+
if (parts.length === 0) bounds.fill(0)
|
|
305
|
+
return { parts, materials, images, bounds }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function readGlb(bytes: Uint8Array): { json: any; bin: Uint8Array | null } {
|
|
309
|
+
let dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
310
|
+
let version = dv.getUint32(4, true)
|
|
311
|
+
if (version !== 2) throw new Error("parseGltf: glb version " + version + " (only 2 is supported)")
|
|
312
|
+
let length = Math.min(dv.getUint32(8, true), bytes.byteLength)
|
|
313
|
+
let json: any = null
|
|
314
|
+
let bin: Uint8Array | null = null
|
|
315
|
+
let at = 12
|
|
316
|
+
while (at + 8 <= length) {
|
|
317
|
+
let chunkLength = dv.getUint32(at, true)
|
|
318
|
+
let chunkType = dv.getUint32(at + 4, true)
|
|
319
|
+
let chunk = bytes.subarray(at + 8, at + 8 + chunkLength)
|
|
320
|
+
if (chunkType === CHUNK_JSON) json = JSON.parse(new TextDecoder().decode(chunk))
|
|
321
|
+
else if (chunkType === CHUNK_BIN && bin === null) bin = chunk
|
|
322
|
+
at += 8 + chunkLength
|
|
323
|
+
}
|
|
324
|
+
if (json === null) throw new Error("parseGltf: glb has no JSON chunk")
|
|
325
|
+
return { json, bin }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function decodeDataUri(uri: string): Uint8Array {
|
|
329
|
+
let comma = uri.indexOf(",")
|
|
330
|
+
if (comma < 0) throw new Error("parseGltf: malformed data: uri")
|
|
331
|
+
let meta = uri.slice(0, comma)
|
|
332
|
+
let payload = uri.slice(comma + 1)
|
|
333
|
+
if (!meta.endsWith(";base64")) return new TextEncoder().encode(decodeURIComponent(payload))
|
|
334
|
+
let text = atob(payload)
|
|
335
|
+
let out = new Uint8Array(text.length)
|
|
336
|
+
for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i)
|
|
337
|
+
return out
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function readerFor(componentType: number, normalized: boolean): (dv: DataView, at: number) => number {
|
|
341
|
+
switch (componentType) {
|
|
342
|
+
case 5126:
|
|
343
|
+
return (dv, at) => dv.getFloat32(at, true)
|
|
344
|
+
case 5125:
|
|
345
|
+
return (dv, at) => dv.getUint32(at, true)
|
|
346
|
+
case 5123:
|
|
347
|
+
return normalized ? (dv, at) => dv.getUint16(at, true) / 65535 : (dv, at) => dv.getUint16(at, true)
|
|
348
|
+
case 5122:
|
|
349
|
+
return normalized ? (dv, at) => Math.max(dv.getInt16(at, true) / 32767, -1) : (dv, at) => dv.getInt16(at, true)
|
|
350
|
+
case 5121:
|
|
351
|
+
return normalized ? (dv, at) => dv.getUint8(at) / 255 : (dv, at) => dv.getUint8(at)
|
|
352
|
+
default:
|
|
353
|
+
return normalized ? (dv, at) => Math.max(dv.getInt8(at) / 127, -1) : (dv, at) => dv.getInt8(at)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const NO_TRANSLATION: Vec3 = [0, 0, 0]
|
|
358
|
+
const NO_ROTATION: Quat = [0, 0, 0, 1]
|
|
359
|
+
const NO_SCALE: Vec3 = [1, 1, 1]
|
|
360
|
+
|
|
361
|
+
function nodeMatrix(out: Mat4, node: any): Mat4 {
|
|
362
|
+
if (node.matrix !== undefined) {
|
|
363
|
+
for (let i = 0; i < 16; i++) out[i] = node.matrix[i]
|
|
364
|
+
return out
|
|
365
|
+
}
|
|
366
|
+
return compose(out, node.translation ?? NO_TRANSLATION, node.rotation ?? NO_ROTATION, node.scale ?? NO_SCALE)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function det3(m: Mat4): number {
|
|
370
|
+
return (
|
|
371
|
+
m[0] * (m[5] * m[10] - m[6] * m[9]) -
|
|
372
|
+
m[4] * (m[1] * m[10] - m[2] * m[9]) +
|
|
373
|
+
m[8] * (m[1] * m[6] - m[2] * m[5])
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// One interleaved vertex: position through the world matrix, normal (when
|
|
378
|
+
// given) through the normal matrix and re-normalized, uv copied or zero.
|
|
379
|
+
function writeVertex(
|
|
380
|
+
out: Float32Array,
|
|
381
|
+
slot: number,
|
|
382
|
+
pos: Float32Array,
|
|
383
|
+
nrm: Float32Array | null,
|
|
384
|
+
uv: Float32Array | null,
|
|
385
|
+
src: number,
|
|
386
|
+
world: Mat4,
|
|
387
|
+
normal: Mat4,
|
|
388
|
+
): void {
|
|
389
|
+
let at = slot * STANDARD_FLOATS
|
|
390
|
+
let x = pos[src * 3]!, y = pos[src * 3 + 1]!, z = pos[src * 3 + 2]!
|
|
391
|
+
out[at] = world[0] * x + world[4] * y + world[8] * z + world[12]
|
|
392
|
+
out[at + 1] = world[1] * x + world[5] * y + world[9] * z + world[13]
|
|
393
|
+
out[at + 2] = world[2] * x + world[6] * y + world[10] * z + world[14]
|
|
394
|
+
if (nrm !== null) {
|
|
395
|
+
let nx = nrm[src * 3]!, ny = nrm[src * 3 + 1]!, nz = nrm[src * 3 + 2]!
|
|
396
|
+
let wx = normal[0] * nx + normal[4] * ny + normal[8] * nz
|
|
397
|
+
let wy = normal[1] * nx + normal[5] * ny + normal[9] * nz
|
|
398
|
+
let wz = normal[2] * nx + normal[6] * ny + normal[10] * nz
|
|
399
|
+
let len = Math.hypot(wx, wy, wz)
|
|
400
|
+
if (len > 1e-12) {
|
|
401
|
+
wx /= len
|
|
402
|
+
wy /= len
|
|
403
|
+
wz /= len
|
|
404
|
+
} else {
|
|
405
|
+
wx = 0
|
|
406
|
+
wy = 1
|
|
407
|
+
wz = 0
|
|
408
|
+
}
|
|
409
|
+
out[at + 3] = wx
|
|
410
|
+
out[at + 4] = wy
|
|
411
|
+
out[at + 5] = wz
|
|
412
|
+
}
|
|
413
|
+
if (uv !== null) {
|
|
414
|
+
out[at + 6] = uv[src * 2]!
|
|
415
|
+
out[at + 7] = uv[src * 2 + 1]!
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// The unit normal of the triangle at three consecutive vertex slots.
|
|
420
|
+
function faceNormal(out: Vec3, v: Float32Array, first: number): void {
|
|
421
|
+
let a = first * STANDARD_FLOATS, b = (first + 1) * STANDARD_FLOATS, c = (first + 2) * STANDARD_FLOATS
|
|
422
|
+
let abx = v[b]! - v[a]!, aby = v[b + 1]! - v[a + 1]!, abz = v[b + 2]! - v[a + 2]!
|
|
423
|
+
let acx = v[c]! - v[a]!, acy = v[c + 1]! - v[a + 1]!, acz = v[c + 2]! - v[a + 2]!
|
|
424
|
+
let nx = aby * acz - abz * acy
|
|
425
|
+
let ny = abz * acx - abx * acz
|
|
426
|
+
let nz = abx * acy - aby * acx
|
|
427
|
+
let len = Math.hypot(nx, ny, nz)
|
|
428
|
+
if (len > 1e-12) {
|
|
429
|
+
out[0] = nx / len
|
|
430
|
+
out[1] = ny / len
|
|
431
|
+
out[2] = nz / len
|
|
432
|
+
} else {
|
|
433
|
+
out[0] = 0
|
|
434
|
+
out[1] = 1
|
|
435
|
+
out[2] = 0
|
|
436
|
+
}
|
|
437
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,23 +5,28 @@
|
|
|
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, createInstancedMesh, createMesh, createScene, disposeInstances, getRotation, lookAt, remove, setGeometry, setInstanceCount, setInstances, setMaterial, setMeshParams, setRenderOrder, setTransform, setVisible, worldPosition } from "./scene.ts"
|
|
9
|
-
export type { CameraUpdate, Hit, InstancedMesh as InstancedMeshNode, InstancedMeshOptions, Mesh as MeshNode, MeshInstances, Scene as SceneHandle, SceneHandlers, SceneNode, SceneOptions, ScenePointerEvent, TransformUpdate } from "./scene.ts"
|
|
8
|
+
export { add, createDirectionalLight, createGroup, createHemisphereLight, createInstancedMesh, createMesh, createScene, createSprite, setLight, disposeInstances, getRotation, lookAt, remove, setCastShadow, setGeometry, setInstanceCount, setInstances, setMaterial, setMeshParams, setRenderOrder, setTransform, setTransition, setVisible, worldPosition, MAX_SHADOWS } from "./scene.ts"
|
|
9
|
+
export type { CameraUpdate, DirectionalLight as DirectionalLightNode, DirectionalLightOptions, HemisphereLight as HemisphereLightNode, HemisphereLightOptions, Hit, Light, InstancedMesh as InstancedMeshNode, InstancedMeshOptions, Mesh as MeshNode, MeshInstances, OrthoExtent, Scene as SceneHandle, SceneHandlers, SceneNode, SceneOptions, ScenePointerEvent, ShadowCamera, ShadowOptions, TransformUpdate, TransitionEndEvent, View, ViewOptions } from "./scene.ts"
|
|
10
|
+
export type { NodeTransition, NodeTransitionSpec } from "flux:spatial"
|
|
10
11
|
export { disposeGeometry } from "./geometry-gpu.ts"
|
|
11
|
-
export { box, circle, cone, cylinder, fillColors, geometryBounds, mergeGeometries, plane, ring, sphere, torus, torusKnot, transformGeometry, withColors,
|
|
12
|
-
export type { ColorFill, Geometry, VertexLayout } from "./geometry.ts"
|
|
13
|
-
export { rayBoxDistance } from "./bvh.ts"
|
|
12
|
+
export { box, circle, cone, cylinder, fillAttribute, fillColors, geometryBounds, layoutAttributes, layoutKey, layoutSlot, layoutStride, mergeGeometries, packGeometry, plane, ring, sphere, torus, torusKnot, transformGeometry, validateGeometry, withAttribute, withColors, STANDARD_FLOATS, VERTEX_LAYOUTS } from "./geometry.ts"
|
|
13
|
+
export type { AttributeFill, BoxOptions, CircleOptions, ColorFill, ConeOptions, CylinderOptions, Geometry, GeometryOptions, PlaneOptions, RingOptions, SphereOptions, TorusKnotOptions, TorusOptions, VertexLayout } from "./geometry.ts"
|
|
14
14
|
export { fillet, roundRect, shape, triangulate } from "./profile.ts"
|
|
15
15
|
export type { Profile, ProfilePoint } from "./profile.ts"
|
|
16
16
|
export { extrude, lathe, pathFrames, sweep, tube } from "./sweep.ts"
|
|
17
|
-
export type { PathFrames, PathPoint, SweepPath } from "./sweep.ts"
|
|
18
|
-
export { shaderMaterial, shaderMaterialClass, unlit } from "./material.ts"
|
|
19
|
-
export type { Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, UnlitOptions } from "./material.ts"
|
|
20
|
-
export { Group, InstancedMesh, Mesh, PerspectiveCamera, Scene, useScene } from "./components.tsx"
|
|
21
|
-
export type { InstancedMeshProps, MeshProps, PerspectiveCameraProps, PointerEventProps, SceneProps, TransformProps } from "./components.tsx"
|
|
17
|
+
export type { ExtrudeOptions, LatheOptions, PathFrames, PathPoint, SweepPath, TubeOptions } from "./sweep.ts"
|
|
18
|
+
export { lit, shaderMaterial, shaderMaterialClass, sprite, unlit } from "./material.ts"
|
|
19
|
+
export type { LitOptions, Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, SpriteOptions, UnlitOptions } from "./material.ts"
|
|
20
|
+
export { DirectionalLight, Group, HemisphereLight, InstancedMesh, Mesh, PerspectiveCamera, Scene, Sprite, useScene } from "./components.tsx"
|
|
21
|
+
export type { DirectionalLightProps, HemisphereLightProps, InstancedMeshProps, MeshProps, PerspectiveCameraProps, PointerEventProps, SceneProps, SpriteProps, TransformProps } from "./components.tsx"
|
|
22
|
+
export { gltfExternalUris, isGlb, parseGltf } from "./gltf.ts"
|
|
23
|
+
export type { ModelData, ModelMaterial, ModelPart, UriResolver } from "./gltf.ts"
|
|
24
|
+
export { decodeModel, encodeModel } from "./model-file.ts"
|
|
25
|
+
export { createModel, loadGltf, loadModel } from "./model.ts"
|
|
26
|
+
export type { Model, ModelOptions } from "./model.ts"
|
|
22
27
|
export { createOrbitCamera } from "./orbit.ts"
|
|
23
|
-
export type { OrbitCamera, OrbitCameraOptions, OrbitPose } from "./orbit.ts"
|
|
28
|
+
export type { OrbitCamera, OrbitCameraOptions, OrbitPose, OrbitTarget } from "./orbit.ts"
|
|
24
29
|
// math's lookAt (the camera view matrix) stays on the /math subpath: the
|
|
25
30
|
// root's lookAt is the scene verb, the same split as `add`.
|
|
26
|
-
export { compose, copy, eulerFromQuat, identity, mat4, multiply, normalMatrix, perspective, quat, quatFromAxisAngle, quatFromEuler, quatFromFrame, quatFromTo, quatMultiply, quatNormalize, quatSlerp } from "./math.ts"
|
|
31
|
+
export { rayBoxDistance, compose, copy, eulerFromQuat, identity, mat4, multiply, normalMatrix, orthographic, perspective, quat, quatFromAxisAngle, quatFromEuler, quatFromFrame, quatFromTo, quatMultiply, quatNormalize, quatSlerp } from "./math.ts"
|
|
27
32
|
export type { Mat4, Quat, Vec2, Vec3 } from "./math.ts"
|