@solidrt/3d 0.0.56 → 0.0.58
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 +1052 -639
- package/README.md +2 -1
- package/examples/README.md +19 -8
- package/examples/collision.tsx +17 -4
- package/examples/first-person.tsx +20 -10
- package/examples/fleet.tsx +11 -5
- package/examples/model.tsx +3 -3
- package/examples/pick.tsx +144 -34
- package/examples/probe.tsx +10 -4
- package/examples/scene-views.tsx +49 -58
- package/examples/sky-lit.tsx +10 -4
- package/examples/skybox.tsx +10 -4
- package/examples/standard.tsx +10 -4
- package/examples/wireframe.tsx +107 -0
- package/package.json +2 -2
- package/src/collision.ts +14 -193
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/first-person.ts +140 -188
- package/src/geometry-gpu.ts +45 -13
- package/src/geometry.ts +186 -5
- package/src/gltf.ts +97 -16
- package/src/index.ts +15 -10
- package/src/input.ts +79 -0
- package/src/material.ts +26 -8
- package/src/mesh.ts +5 -6
- package/src/model.ts +65 -33
- package/src/node.ts +100 -26
- package/src/orbit.ts +266 -235
- package/src/scene-pointer.ts +229 -101
- package/src/scene.ts +256 -79
- package/src/skeleton.ts +211 -0
- package/src/components.tsx +0 -1101
package/src/geometry.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// array math (the check rig checks/geometry-check.ts runs it headless on
|
|
22
22
|
// flux). The GPU buffer step lives in geometry-gpu.ts.
|
|
23
23
|
|
|
24
|
-
import type { VertexAttribute } from "@solidrt/core/gpu"
|
|
24
|
+
import type { Topology, VertexAttribute } from "@solidrt/core/gpu"
|
|
25
25
|
import { add, compose, cross, mat4, normalize, normalMatrix, sub, updateRotation, updateScale } from "./math.ts"
|
|
26
26
|
import type { Quat, TransformUpdate, Vec2, Vec3 } from "./math.ts"
|
|
27
27
|
|
|
@@ -50,7 +50,10 @@ export const VERTEX_LAYOUTS: Record<"standard" | "colored" | "skinned", VertexAt
|
|
|
50
50
|
* format before packing). */
|
|
51
51
|
export const STANDARD_FLOATS = 8
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
/** Floats per attribute format: the vertex vocabulary itself, so it is
|
|
54
|
+
* also the list a declared format must be one of (vertex attributes and
|
|
55
|
+
* instance attributes both). */
|
|
56
|
+
export const FORMAT_FLOATS: Record<VertexAttribute["format"], number> = { f32: 1, vec2: 2, vec3: 3, vec4: 4 }
|
|
54
57
|
|
|
55
58
|
/** The attribute list of a layout (a preset name resolves to its list). */
|
|
56
59
|
export function layoutAttributes(layout?: VertexLayout): VertexAttribute[] {
|
|
@@ -123,7 +126,35 @@ export function validateGeometry(geometry: Geometry): void {
|
|
|
123
126
|
name + ": " + geometry.vertices.length + " vertex floats is not a whole number of " + stride + "-float (" + layoutKey(layout) + ") vertices",
|
|
124
127
|
)
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
let topology = geometryTopology(geometry)
|
|
130
|
+
let count = geometry.indices.length
|
|
131
|
+
// Lines and points may be empty: a feature-edge pass over a smooth
|
|
132
|
+
// closed shape has nothing to draw, and that is a result, not a bug.
|
|
133
|
+
switch (topology) {
|
|
134
|
+
case "triangles":
|
|
135
|
+
if (count === 0) throw new Error(name + ": no indices")
|
|
136
|
+
if (count % 3 !== 0) throw new Error(name + ": " + count + " indices is not a whole number of triangles")
|
|
137
|
+
break
|
|
138
|
+
case "lines":
|
|
139
|
+
if (count % 2 !== 0) throw new Error(name + ": " + count + " indices is not a whole number of lines")
|
|
140
|
+
break
|
|
141
|
+
case "line-strip":
|
|
142
|
+
if (count < 2) throw new Error(name + ": a line strip needs at least 2 indices")
|
|
143
|
+
break
|
|
144
|
+
case "triangle-strip":
|
|
145
|
+
if (count < 3) throw new Error(name + ": a triangle strip needs at least 3 indices")
|
|
146
|
+
break
|
|
147
|
+
case "points":
|
|
148
|
+
break
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(name + ": unknown topology '" + String(topology) + "'")
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The geometry's topology with the default applied: what its indices
|
|
155
|
+
* list, "triangles" unless it says otherwise. */
|
|
156
|
+
export function geometryTopology(geometry: Geometry): Topology {
|
|
157
|
+
return geometry.topology ?? "triangles"
|
|
127
158
|
}
|
|
128
159
|
|
|
129
160
|
/** The options every generator shares (each generator's own options type
|
|
@@ -194,6 +225,20 @@ export type Geometry = {
|
|
|
194
225
|
/** The array type picks the draw's index format: Uint32Array past 64k
|
|
195
226
|
* vertices. */
|
|
196
227
|
indices: Uint16Array | Uint32Array
|
|
228
|
+
/** What the indices describe; absent means "triangles". The index
|
|
229
|
+
* buffer and the primitive it lists travel together (Godot's surface
|
|
230
|
+
* primitive, Unity's Mesh.SetIndices topology, Three's Line/Points
|
|
231
|
+
* object types over one BufferGeometry): the material draws whatever
|
|
232
|
+
* topology its geometry carries, one pipeline per (layout, topology)
|
|
233
|
+
* pair. Only a triangle list gets the picking shape - a lines, points
|
|
234
|
+
* or strip geometry picks (and collides) by its bounds box, the tier a
|
|
235
|
+
* sprite or an instanced mesh is at, so hits carry no face/uv. Lines
|
|
236
|
+
* are one pixel wide on GL ES whatever the display density. A lines
|
|
237
|
+
* or points geometry may have no indices at all (it then draws
|
|
238
|
+
* nothing; the other topologies reject that at add()).
|
|
239
|
+
* wireframeGeometry/edgesGeometry build "lines" over a triangle
|
|
240
|
+
* geometry's own vertices. */
|
|
241
|
+
topology?: Topology
|
|
197
242
|
/** Vertex layout; absent means "standard". Must match the material's
|
|
198
243
|
* layout - the scene rejects a mismatched pair at add(). */
|
|
199
244
|
layout?: VertexLayout
|
|
@@ -270,6 +315,7 @@ export function withAttribute(geometry: Geometry, attr: VertexAttribute, fill: A
|
|
|
270
315
|
return {
|
|
271
316
|
vertices: out,
|
|
272
317
|
indices: geometry.indices,
|
|
318
|
+
topology: geometry.topology,
|
|
273
319
|
layout,
|
|
274
320
|
label: label ?? (geometry.label ? geometry.label + "-" + attr.name : undefined),
|
|
275
321
|
}
|
|
@@ -394,6 +440,7 @@ export function transformGeometry(geometry: Geometry, transform: TransformUpdate
|
|
|
394
440
|
return {
|
|
395
441
|
vertices: out,
|
|
396
442
|
indices: geometry.indices,
|
|
443
|
+
topology: geometry.topology,
|
|
397
444
|
layout: geometry.layout,
|
|
398
445
|
label: label ?? (geometry.label ? geometry.label + "-transformed" : undefined),
|
|
399
446
|
}
|
|
@@ -403,7 +450,9 @@ export function transformGeometry(geometry: Geometry, transform: TransformUpdate
|
|
|
403
450
|
* Concatenate geometries into one: vertices appended in order, indices
|
|
404
451
|
* offset to match, uint32 indices past 64k vertices. Every part must share
|
|
405
452
|
* one layout - a mixed list throws, because the strides differ and a merge
|
|
406
|
-
* that picked one would draw garbage, not a mesh missing a channel
|
|
453
|
+
* that picked one would draw garbage, not a mesh missing a channel - and
|
|
454
|
+
* one list topology (a mixed list throws the same way; strips cannot be
|
|
455
|
+
* concatenated at all, the seam would join them). The
|
|
407
456
|
* second half of authoring a static scene as data (Three's
|
|
408
457
|
* `BufferGeometryUtils.mergeGeometries`): the result is one draw entry and
|
|
409
458
|
* one uModel write however many parts went in, so only what actually moves
|
|
@@ -414,12 +463,17 @@ export function mergeGeometries(parts: Geometry[], label?: string): Geometry {
|
|
|
414
463
|
let layout = parts[0]!.layout
|
|
415
464
|
let key = layoutKey(layout)
|
|
416
465
|
let stride = layoutStride(layout)
|
|
466
|
+
let topology = geometryTopology(parts[0]!)
|
|
467
|
+
if (topology === "line-strip" || topology === "triangle-strip") throw new Error("mergeGeometries: cannot merge " + topology + " geometry")
|
|
417
468
|
let floats = 0
|
|
418
469
|
let indexCount = 0
|
|
419
470
|
for (let part of parts) {
|
|
420
471
|
if (layoutKey(part.layout) !== key) {
|
|
421
472
|
throw new Error("mergeGeometries: mixed layouts (" + key + " and " + layoutKey(part.layout) + ")")
|
|
422
473
|
}
|
|
474
|
+
if (geometryTopology(part) !== topology) {
|
|
475
|
+
throw new Error("mergeGeometries: mixed topologies (" + topology + " and " + geometryTopology(part) + ")")
|
|
476
|
+
}
|
|
423
477
|
if (part.vertices.length % stride !== 0) {
|
|
424
478
|
throw new Error("mergeGeometries: a part's vertex data is not a whole number of " + key + " vertices")
|
|
425
479
|
}
|
|
@@ -439,7 +493,134 @@ export function mergeGeometries(parts: Geometry[], label?: string): Geometry {
|
|
|
439
493
|
vOffset += part.vertices.length
|
|
440
494
|
iOffset += src.length
|
|
441
495
|
}
|
|
442
|
-
return { vertices, indices, layout, label }
|
|
496
|
+
return { vertices, indices, topology: parts[0]!.topology, layout, label }
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Positions closer than this (model units) are one vertex to the edge
|
|
500
|
+
* builders: a uv seam or a per-face normal split duplicates a position,
|
|
501
|
+
* and an edge is the same edge whichever copy a triangle names. */
|
|
502
|
+
const WELD_PRECISION = 1e-4
|
|
503
|
+
|
|
504
|
+
/** Degrees between two faces' normals from which the edge between them
|
|
505
|
+
* counts as a feature edge in edgesGeometry; under it the faces read as
|
|
506
|
+
* one smooth surface and the edge stays hidden. Three's default. */
|
|
507
|
+
const EDGES_THRESHOLD_ANGLE = 1
|
|
508
|
+
|
|
509
|
+
/** One id per vertex, shared by every vertex at the same position (to
|
|
510
|
+
* WELD_PRECISION) whatever its normal and uv; plus the id count. */
|
|
511
|
+
function weldPositions(geometry: Geometry): { ids: Uint32Array; count: number } {
|
|
512
|
+
let v = geometry.vertices
|
|
513
|
+
let stride = layoutStride(geometry.layout)
|
|
514
|
+
let count = v.length / stride
|
|
515
|
+
let ids = new Uint32Array(count)
|
|
516
|
+
let seen = new Map<string, number>()
|
|
517
|
+
let scale = 1 / WELD_PRECISION
|
|
518
|
+
for (let i = 0; i < count; i++) {
|
|
519
|
+
let b = i * stride
|
|
520
|
+
let key = Math.round(v[b]! * scale) + "," + Math.round(v[b + 1]! * scale) + "," + Math.round(v[b + 2]! * scale)
|
|
521
|
+
let id = seen.get(key)
|
|
522
|
+
if (id === undefined) {
|
|
523
|
+
id = seen.size
|
|
524
|
+
seen.set(key, id)
|
|
525
|
+
}
|
|
526
|
+
ids[i] = id
|
|
527
|
+
}
|
|
528
|
+
return { ids, count: seen.size }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** The edge table of a triangle geometry, welded by position, as a lines
|
|
532
|
+
* index list: every edge once for `cosThreshold` null, else the feature
|
|
533
|
+
* edges - those with one face (a border) or whose two faces' normals
|
|
534
|
+
* dot at or below the threshold. Each edge names the vertex pair of the
|
|
535
|
+
* first triangle that had it. */
|
|
536
|
+
function edgeIndices(geometry: Geometry, name: string, cosThreshold: number | null): Uint16Array | Uint32Array {
|
|
537
|
+
if (geometryTopology(geometry) !== "triangles") {
|
|
538
|
+
throw new Error(name + ": needs a triangle geometry, got " + geometryTopology(geometry))
|
|
539
|
+
}
|
|
540
|
+
let v = geometry.vertices
|
|
541
|
+
let stride = layoutStride(geometry.layout)
|
|
542
|
+
let weld = weldPositions(geometry)
|
|
543
|
+
let src = geometry.indices
|
|
544
|
+
type Edge = { a: number; b: number; nx: number; ny: number; nz: number; faces: number; sharp: boolean }
|
|
545
|
+
let edges = new Map<number, Edge>()
|
|
546
|
+
for (let t = 0; t + 2 < src.length; t += 3) {
|
|
547
|
+
let i0 = src[t]!, i1 = src[t + 1]!, i2 = src[t + 2]!
|
|
548
|
+
let p0 = i0 * stride, p1 = i1 * stride, p2 = i2 * stride
|
|
549
|
+
// The face normal, for the feature test.
|
|
550
|
+
let ux = v[p1]! - v[p0]!, uy = v[p1 + 1]! - v[p0 + 1]!, uz = v[p1 + 2]! - v[p0 + 2]!
|
|
551
|
+
let wx = v[p2]! - v[p0]!, wy = v[p2 + 1]! - v[p0 + 1]!, wz = v[p2 + 2]! - v[p0 + 2]!
|
|
552
|
+
let nx = uy * wz - uz * wy, ny = uz * wx - ux * wz, nz = ux * wy - uy * wx
|
|
553
|
+
let len = Math.hypot(nx, ny, nz) || 1
|
|
554
|
+
nx /= len
|
|
555
|
+
ny /= len
|
|
556
|
+
nz /= len
|
|
557
|
+
for (let k = 0; k < 3; k++) {
|
|
558
|
+
let ia = k === 0 ? i0 : k === 1 ? i1 : i2
|
|
559
|
+
let ib = k === 0 ? i1 : k === 1 ? i2 : i0
|
|
560
|
+
let wa = weld.ids[ia]!, wb = weld.ids[ib]!
|
|
561
|
+
if (wa === wb) continue
|
|
562
|
+
let key = Math.min(wa, wb) * weld.count + Math.max(wa, wb)
|
|
563
|
+
let e = edges.get(key)
|
|
564
|
+
if (e === undefined) {
|
|
565
|
+
edges.set(key, { a: ia, b: ib, nx, ny, nz, faces: 1, sharp: false })
|
|
566
|
+
} else {
|
|
567
|
+
e.faces++
|
|
568
|
+
if (cosThreshold !== null && !e.sharp && e.nx * nx + e.ny * ny + e.nz * nz <= cosThreshold) e.sharp = true
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
let out: number[] = []
|
|
573
|
+
for (let e of edges.values()) {
|
|
574
|
+
if (cosThreshold === null || e.faces === 1 || e.sharp) out.push(e.a, e.b)
|
|
575
|
+
}
|
|
576
|
+
return packIndices(out, v.length / stride)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Every edge of a triangle geometry as a "lines" geometry over the SAME
|
|
581
|
+
* vertices (Three's WireframeGeometry): each triangle's three sides, once
|
|
582
|
+
* each - edges are matched by position, so a uv seam or a per-face
|
|
583
|
+
* normal split draws one line, not two on top of each other. The
|
|
584
|
+
* source's vertex array and layout are shared by reference, and so is
|
|
585
|
+
* their GPU upload, which makes the wireframe a second index buffer over
|
|
586
|
+
* the same vertices: a "skinned" part's wireframe draws in its pose
|
|
587
|
+
* under `unlit({ skinned: true })` with the mesh's own palette, and any
|
|
588
|
+
* material draws it as lines because the topology rides on the
|
|
589
|
+
* geometry. Lines are one pixel wide.
|
|
590
|
+
*/
|
|
591
|
+
export function wireframeGeometry(geometry: Geometry, label?: string): Geometry {
|
|
592
|
+
return {
|
|
593
|
+
vertices: geometry.vertices,
|
|
594
|
+
indices: edgeIndices(geometry, "wireframeGeometry", null),
|
|
595
|
+
topology: "lines",
|
|
596
|
+
layout: geometry.layout,
|
|
597
|
+
label: label ?? (geometry.label ? geometry.label + "-wireframe" : undefined),
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* The feature edges of a triangle geometry as a "lines" geometry over the
|
|
603
|
+
* same vertices (Three's EdgesGeometry): an edge draws when its two faces
|
|
604
|
+
* meet at `thresholdAngle` degrees or more, or when only one face has it
|
|
605
|
+
* (an open border): the outline that a dense mesh's wireframe, a solid
|
|
606
|
+
* blob, cannot give. A box draws its twelve edges and no face diagonals
|
|
607
|
+
* at any threshold; a generated round shape is faceted, so its facet
|
|
608
|
+
* lines show until the threshold passes its facet angle (360 divided by
|
|
609
|
+
* the radial segments: a default cylinder loses its side seams and keeps
|
|
610
|
+
* its two rims from 16 degrees, a default sphere draws nothing from 16).
|
|
611
|
+
* A smooth closed shape above its facet angle has no edges at all, and
|
|
612
|
+
* the empty lines geometry that comes back attaches and draws nothing.
|
|
613
|
+
* Shares vertices, layout and GPU upload with the source like
|
|
614
|
+
* wireframeGeometry.
|
|
615
|
+
*/
|
|
616
|
+
export function edgesGeometry(geometry: Geometry, thresholdAngle = EDGES_THRESHOLD_ANGLE, label?: string): Geometry {
|
|
617
|
+
return {
|
|
618
|
+
vertices: geometry.vertices,
|
|
619
|
+
indices: edgeIndices(geometry, "edgesGeometry", Math.cos((thresholdAngle * Math.PI) / 180)),
|
|
620
|
+
topology: "lines",
|
|
621
|
+
layout: geometry.layout,
|
|
622
|
+
label: label ?? (geometry.label ? geometry.label + "-edges" : undefined),
|
|
623
|
+
}
|
|
443
624
|
}
|
|
444
625
|
|
|
445
626
|
// Indices for a row-major (cellRows + 1) x (cellCols + 1) vertex grid: two
|
package/src/gltf.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// glTF 2.0, the subset an app needs to show authored models: the scene's
|
|
2
2
|
// node tree RETAINED - a node table of local TRS with parent links (matrix
|
|
3
3
|
// nodes decomposed; shear dropped), vertices in NODE-LOCAL space, one part
|
|
4
|
-
// per mesh primitive referencing its node -
|
|
5
|
-
//
|
|
6
|
-
// indices
|
|
4
|
+
// per mesh primitive referencing its node - positions, normals (flat ones
|
|
5
|
+
// generated when absent, per the spec, for triangles), one UV set and
|
|
6
|
+
// indices in the primitive's topology (strips and fans unrolled to
|
|
7
|
+
// triangle lists, a line loop closed into a strip; lines and points keep
|
|
8
|
+
// theirs on the geometry), and materials reduced to what lit()/unlit()
|
|
9
|
+
// draw - base color
|
|
7
10
|
// factor and texture, normal map (with scale), emissive factor and map
|
|
8
11
|
// (KHR_materials_emissive_strength as the intensity), double-sidedness, alpha
|
|
9
12
|
// blending and masking - plus the file's animations as baked clips
|
|
@@ -19,8 +22,8 @@
|
|
|
19
22
|
// bytes in `images`, and uploading is the engine side's job.
|
|
20
23
|
//
|
|
21
24
|
// Outside the subset: Draco/meshopt-compressed meshes and any other
|
|
22
|
-
// required extension throw naming it;
|
|
23
|
-
//
|
|
25
|
+
// required extension throw naming it; sparse accessors and morph
|
|
26
|
+
// targets (the "weights" channel path) are skipped
|
|
24
27
|
// or ignored; vertex colors, tangents and further UV sets are dropped
|
|
25
28
|
// (the standard layout has no slot for them yet). Skins ARE parsed:
|
|
26
29
|
// joints, inverse binds, and the "skinned" vertex layout.
|
|
@@ -30,6 +33,7 @@ import { linearToSrgb } from "./color.ts"
|
|
|
30
33
|
import type { Mat4, Quat, Vec3 } from "./math.ts"
|
|
31
34
|
import { layoutStride, packGeometry, packIndices, STANDARD_FLOATS } from "./geometry.ts"
|
|
32
35
|
import type { Geometry } from "./geometry.ts"
|
|
36
|
+
import type { Topology } from "@solidrt/core/gpu"
|
|
33
37
|
|
|
34
38
|
/** What lit()/unlit() take from a glTF material. */
|
|
35
39
|
export type ModelMaterial = {
|
|
@@ -182,7 +186,39 @@ export type UriResolver = (uri: string) => Uint8Array
|
|
|
182
186
|
const GLB_MAGIC = 0x46546c67
|
|
183
187
|
const CHUNK_JSON = 0x4e4f534a
|
|
184
188
|
const CHUNK_BIN = 0x004e4942
|
|
189
|
+
// glTF primitive modes (the GL enum values).
|
|
190
|
+
const MODE_POINTS = 0
|
|
191
|
+
const MODE_LINES = 1
|
|
192
|
+
const MODE_LINE_LOOP = 2
|
|
193
|
+
const MODE_LINE_STRIP = 3
|
|
185
194
|
const MODE_TRIANGLES = 4
|
|
195
|
+
const MODE_TRIANGLE_STRIP = 5
|
|
196
|
+
const MODE_TRIANGLE_FAN = 6
|
|
197
|
+
|
|
198
|
+
/** A triangle strip unrolled to a list: triangle t is (t, t+1, t+2),
|
|
199
|
+
* every odd one with its last two swapped so the winding stays put. */
|
|
200
|
+
function stripTriangles(strip: ArrayLike<number>): number[] {
|
|
201
|
+
let out: number[] = []
|
|
202
|
+
for (let t = 0; t + 2 < strip.length; t++) {
|
|
203
|
+
if (t % 2 === 0) out.push(strip[t]!, strip[t + 1]!, strip[t + 2]!)
|
|
204
|
+
else out.push(strip[t]!, strip[t + 2]!, strip[t + 1]!)
|
|
205
|
+
}
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** A triangle fan unrolled to a list: every triangle shares the hub. */
|
|
210
|
+
function fanTriangles(fan: ArrayLike<number>): number[] {
|
|
211
|
+
let out: number[] = []
|
|
212
|
+
for (let t = 1; t + 1 < fan.length; t++) out.push(fan[0]!, fan[t]!, fan[t + 1]!)
|
|
213
|
+
return out
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A line loop as a strip that returns to its first vertex. */
|
|
217
|
+
function closeLoop(loop: ArrayLike<number>): number[] {
|
|
218
|
+
let out = Array.from(loop)
|
|
219
|
+
if (loop.length > 0) out.push(loop[0]!)
|
|
220
|
+
return out
|
|
221
|
+
}
|
|
186
222
|
|
|
187
223
|
// The spec's alphaCutoff when a MASK material leaves it out.
|
|
188
224
|
const GLTF_ALPHA_CUTOFF = 0.5
|
|
@@ -383,8 +419,8 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
383
419
|
let bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
|
|
384
420
|
|
|
385
421
|
let emit = (prim: any, name: string, node: number, world: Mat4, skin: number | null): void => {
|
|
386
|
-
if ((prim.mode ?? MODE_TRIANGLES) !== MODE_TRIANGLES) return
|
|
387
422
|
if (prim.attributes?.POSITION === undefined) return
|
|
423
|
+
let mode: number = prim.mode ?? MODE_TRIANGLES
|
|
388
424
|
let pos = accessorFloats(prim.attributes.POSITION, name + " POSITION")
|
|
389
425
|
if (pos.elements !== 3) throw new Error("parseGltf: " + name + " POSITION is not VEC3")
|
|
390
426
|
let count = pos.count
|
|
@@ -408,17 +444,57 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
408
444
|
weights = w.data
|
|
409
445
|
}
|
|
410
446
|
let stride = skinned ? SKINNED_FLOATS : STANDARD_FLOATS
|
|
411
|
-
let
|
|
447
|
+
let raw: ArrayLike<number> =
|
|
412
448
|
prim.indices !== undefined ? accessorFloats(prim.indices, name + " indices").data : Array.from({ length: count }, (_, i) => i)
|
|
413
|
-
|
|
449
|
+
// The primitive's topology, carried on the geometry. Triangle strips
|
|
450
|
+
// and fans are unrolled to lists (what the winding flip, the
|
|
451
|
+
// flat-shading un-index and the picking shape expect) and a line
|
|
452
|
+
// loop, which has no GPU topology, is closed into a strip.
|
|
453
|
+
let topology: Topology
|
|
454
|
+
let indices: ArrayLike<number>
|
|
455
|
+
switch (mode) {
|
|
456
|
+
case MODE_TRIANGLES:
|
|
457
|
+
topology = "triangles"
|
|
458
|
+
indices = raw
|
|
459
|
+
break
|
|
460
|
+
case MODE_TRIANGLE_STRIP:
|
|
461
|
+
topology = "triangles"
|
|
462
|
+
indices = stripTriangles(raw)
|
|
463
|
+
break
|
|
464
|
+
case MODE_TRIANGLE_FAN:
|
|
465
|
+
topology = "triangles"
|
|
466
|
+
indices = fanTriangles(raw)
|
|
467
|
+
break
|
|
468
|
+
case MODE_LINES:
|
|
469
|
+
topology = "lines"
|
|
470
|
+
indices = raw
|
|
471
|
+
break
|
|
472
|
+
case MODE_LINE_STRIP:
|
|
473
|
+
topology = "line-strip"
|
|
474
|
+
indices = raw
|
|
475
|
+
break
|
|
476
|
+
case MODE_LINE_LOOP:
|
|
477
|
+
topology = "line-strip"
|
|
478
|
+
indices = closeLoop(raw)
|
|
479
|
+
break
|
|
480
|
+
case MODE_POINTS:
|
|
481
|
+
topology = "points"
|
|
482
|
+
indices = raw
|
|
483
|
+
break
|
|
484
|
+
default:
|
|
485
|
+
throw new Error("parseGltf: " + name + " has an unknown primitive mode " + mode)
|
|
486
|
+
}
|
|
487
|
+
let triangles = topology === "triangles"
|
|
488
|
+
if (triangles && indices.length % 3 !== 0) throw new Error("parseGltf: " + name + " index count is not a multiple of 3")
|
|
414
489
|
|
|
415
490
|
// A mirroring chain (negative world determinant at the rest pose)
|
|
416
491
|
// flips the displayed winding, so the index order is flipped here to
|
|
417
492
|
// compensate - cull: "back" keeps the outside. Stored normals stay the
|
|
418
493
|
// authored ones: the runtime's inverse-transpose maps them outward for
|
|
419
494
|
// the flipped winding. Baked from the REST pose; a scale animated
|
|
420
|
-
// across zero would unbake it, which is pathological.
|
|
421
|
-
|
|
495
|
+
// across zero would unbake it, which is pathological. Lines and
|
|
496
|
+
// points have no winding.
|
|
497
|
+
let flip = triangles && !skinned && det3(world) < 0
|
|
422
498
|
// Joint boxes need the skin's inverse binds, and skins are built after
|
|
423
499
|
// the walk (their joints are ordinary nodes the walk registers), so
|
|
424
500
|
// the arrays are parked under the FILE's skin index and grown then.
|
|
@@ -426,14 +502,18 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
426
502
|
|
|
427
503
|
let vertices: Float32Array
|
|
428
504
|
let packedIndices: number[]
|
|
429
|
-
if (nrm !== null) {
|
|
505
|
+
if (nrm !== null || !triangles) {
|
|
506
|
+
// Indexed as authored. Lines and points without normals get zero
|
|
507
|
+
// ones (there is no face to take one from; nothing lights them).
|
|
430
508
|
vertices = new Float32Array(count * stride)
|
|
431
509
|
for (let i = 0; i < count; i++) writeVertex(vertices, i, pos.data, nrm, uv, i, stride, joints, weights)
|
|
432
|
-
packedIndices =
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
510
|
+
packedIndices = Array.from(indices)
|
|
511
|
+
if (flip) {
|
|
512
|
+
for (let i = 0; i < packedIndices.length; i += 3) {
|
|
513
|
+
let b = packedIndices[i + 1]!
|
|
514
|
+
packedIndices[i + 1] = packedIndices[i + 2]!
|
|
515
|
+
packedIndices[i + 2] = b
|
|
516
|
+
}
|
|
437
517
|
}
|
|
438
518
|
} else {
|
|
439
519
|
// No normals: the spec asks for flat shading, which needs one vertex
|
|
@@ -508,6 +588,7 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
508
588
|
let geometry: Geometry = skinned
|
|
509
589
|
? { vertices, indices: packIndices(packedIndices, count), layout: "skinned", label: name }
|
|
510
590
|
: packGeometry(vertices, packedIndices, { label: name })
|
|
591
|
+
if (!triangles) geometry.topology = topology
|
|
511
592
|
parts.push({ name, node, skin, geometry, material })
|
|
512
593
|
}
|
|
513
594
|
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// PerspectiveCamera) on top. See AGENTS.md for the model and the traps.
|
|
7
7
|
|
|
8
8
|
export { add, createGroup, getRotation, getTransform, lookAt, remove, setTransform, setTransition, setVisible, worldPosition } from "./node.ts"
|
|
9
|
-
export type { SceneNode, ScenePointerEvent, TransformUpdate, TransitionEndEvent } from "./node.ts"
|
|
9
|
+
export type { NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneEventBase, SceneNode, ScenePointerEvent, ScenePointerListener, SceneTapEvent, SceneWheelEvent, TransformUpdate, TransitionEndEvent } from "./node.ts"
|
|
10
10
|
export { addInstance, createInstancedMesh, createMesh, createRecordMesh, createSprite, disposeInstances, removeInstance, setCastShadow, setCulling, setGeometry, setInstanceStyle, setLayers, setMaterial, setMeshParams, setRecordCount, setRecords, setRenderOrder, INSTANCE_FLOATS } from "./mesh.ts"
|
|
11
11
|
export type { InstancedMesh as InstancedMeshNode, InstancedMeshOptions, InstanceNode, InstanceSlots, InstanceStyle, Mesh as MeshNode, MeshInstances, RecordMesh as RecordMeshNode, RecordMeshOptions } from "./mesh.ts"
|
|
12
12
|
export { createDirectionalLight, createHemisphereLight, createPointLight, createSpotLight, setLight, MAX_SHADOWS } from "./light.ts"
|
|
@@ -26,15 +26,16 @@ export type {
|
|
|
26
26
|
SpotShadowOptions,
|
|
27
27
|
} from "./light.ts"
|
|
28
28
|
export { createScene } from "./scene.ts"
|
|
29
|
-
export
|
|
29
|
+
export { feedPointer } from "./scene-pointer.ts"
|
|
30
|
+
export type { Capsule, EnvironmentOptions, FogOptions, Hit, Impact, MoveOptions, MoveResult, OrientedBox, Overlap, QueryOptions, ReflectionProbe, ReflectionProbeOptions, Scene as SceneHandle, SceneHandlers, SceneOptions, ScreenRay, SkyboxOptions, Sphere, ToneMapping, ViewHandle, ViewOptions, Volume } from "./scene.ts"
|
|
30
31
|
export { moveAndSlide } from "./collision.ts"
|
|
31
|
-
export type {
|
|
32
|
+
export type { MoveScene } from "./collision.ts"
|
|
32
33
|
export { equirectToCube, loadEnvironment } from "./environment.ts"
|
|
33
34
|
export { linearColor, linearToSrgb, srgbToLinear } from "./color.ts"
|
|
34
35
|
export type { CameraState, CameraUpdate, OrthoExtent } from "./camera.ts"
|
|
35
36
|
export type { NodeTransition, NodeTransitionSpec } from "flux:spatial"
|
|
36
37
|
export { disposeGeometry } from "./geometry-gpu.ts"
|
|
37
|
-
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"
|
|
38
|
+
export { box, circle, cone, cylinder, fillAttribute, fillColors, edgesGeometry, geometryBounds, geometryTopology, layoutAttributes, layoutKey, layoutSlot, layoutStride, mergeGeometries, packGeometry, plane, ring, sphere, torus, torusKnot, transformGeometry, validateGeometry, wireframeGeometry, withAttribute, withColors, STANDARD_FLOATS, VERTEX_LAYOUTS } from "./geometry.ts"
|
|
38
39
|
export type { AttributeFill, BoxOptions, CircleOptions, ColorFill, ConeOptions, CylinderOptions, Geometry, GeometryOptions, PlaneOptions, RingOptions, SphereOptions, TorusKnotOptions, TorusOptions, VertexLayout } from "./geometry.ts"
|
|
39
40
|
export { fillet, roundRect, shape, triangulate } from "./profile.ts"
|
|
40
41
|
export type { Profile, ProfilePoint } from "./profile.ts"
|
|
@@ -42,8 +43,9 @@ export { extrude, lathe, pathFrames, sweep, tube } from "./sweep.ts"
|
|
|
42
43
|
export type { ExtrudeOptions, LatheOptions, PathFrames, PathPoint, SweepPath, TubeOptions } from "./sweep.ts"
|
|
43
44
|
export { lit, shaderMaterial, shaderMaterialClass, sprite, standard, unlit } from "./material.ts"
|
|
44
45
|
export type { LitOptions, Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, SpriteOptions, StandardOptions, UnlitOptions } from "./material.ts"
|
|
45
|
-
export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedMesh, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SpotLight, Sprite, useScene } from "./components.
|
|
46
|
+
export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedMesh, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SpotLight, Sprite, View3d, useScene } from "./components/index.ts"
|
|
46
47
|
export type {
|
|
48
|
+
CameraTarget,
|
|
47
49
|
DirectionalLightProps,
|
|
48
50
|
FirstPersonCameraProps,
|
|
49
51
|
HemisphereLightProps,
|
|
@@ -55,25 +57,28 @@ export type {
|
|
|
55
57
|
PointerEventProps,
|
|
56
58
|
PointLightProps,
|
|
57
59
|
RecordMeshProps,
|
|
58
|
-
|
|
59
|
-
SceneInputListener,
|
|
60
|
+
ScenePointerProps,
|
|
60
61
|
SceneProps,
|
|
61
62
|
SpotLightProps,
|
|
62
63
|
SpriteProps,
|
|
63
64
|
TransformProps,
|
|
64
|
-
|
|
65
|
+
View3dProps,
|
|
66
|
+
} from "./components/index.ts"
|
|
65
67
|
export { gltfExternalUris, isGlb, parseGltf } from "./gltf.ts"
|
|
66
68
|
export type { ModelChannel, ModelClip, ModelData, ModelMaterial, ModelNode, ModelPart, ModelSkin, UriResolver } from "./gltf.ts"
|
|
67
69
|
export { decodeModel, encodeModel } from "./model-file.ts"
|
|
68
70
|
export { createModel, loadGltf, loadModel } from "./model.ts"
|
|
69
71
|
export type { Model, ModelMaps, ModelOptions } from "./model.ts"
|
|
72
|
+
export { bindSkeleton } from "./skeleton.ts"
|
|
70
73
|
export { createMixer } from "./mixer.ts"
|
|
71
74
|
export type { Mixer, MixerOptions, MixerPlayOptions } from "./mixer.ts"
|
|
72
75
|
export { sampleChannel } from "./clip.ts"
|
|
73
76
|
export { createOrbitCamera } from "./orbit.ts"
|
|
74
|
-
export type { OrbitCamera as OrbitCameraHandle, OrbitCameraOptions, OrbitPose, OrbitTarget } from "./orbit.ts"
|
|
77
|
+
export type { OrbitAxes, OrbitCamera as OrbitCameraHandle, OrbitCameraOptions, OrbitPose, OrbitTarget } from "./orbit.ts"
|
|
75
78
|
export { createFirstPersonCamera } from "./first-person.ts"
|
|
76
|
-
export type { FirstPersonCamera as FirstPersonCameraHandle, FirstPersonCameraOptions, FirstPersonPose, FirstPersonTarget } from "./first-person.ts"
|
|
79
|
+
export type { FirstPersonAxes, FirstPersonCamera as FirstPersonCameraHandle, FirstPersonCameraOptions, FirstPersonPose, FirstPersonTarget } from "./first-person.ts"
|
|
80
|
+
export { firstPersonActions, firstPersonBindings, orbitActions, orbitBindings } from "./input.ts"
|
|
81
|
+
export type { CameraDevices } from "./input.ts"
|
|
77
82
|
// math's lookAt (the camera view matrix) stays on the /math subpath: the
|
|
78
83
|
// root's lookAt is the scene verb, the same split as `add`.
|
|
79
84
|
export { rayBoxDistance, compose, copy, eulerFromQuat, identity, mat4, multiply, normalMatrix, orthographic, perspective, quat, quatFromAxisAngle, quatFromEuler, quatFromFrame, quatFromTo, quatMultiply, quatNormalize, quatSlerp } from "./math.ts"
|
package/src/input.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The standard input wiring for the 3d camera controls: the action
|
|
2
|
+
// declarations each control consumes (its axes, by the shared vocabulary)
|
|
3
|
+
// and a preset per control that binds the devices an app hands in. A
|
|
4
|
+
// preset is plain data - the bindings the app could write by hand -
|
|
5
|
+
// applied with `input.bind(orbitBindings({ pointer, gamepad }))` and
|
|
6
|
+
// adjusted afterwards with unbind; nothing binds unless the app says so
|
|
7
|
+
// (ARCHITECTURE.md).
|
|
8
|
+
//
|
|
9
|
+
// Devices are the nouns: `pointer` is the feed of the element showing the
|
|
10
|
+
// scene (`createPointerFeed()`, handed to `<Scene pointer>`), `gamepad` a
|
|
11
|
+
// core gamepad device (`gamepad(0)`, or `gamepad()` for any pad), and
|
|
12
|
+
// `keyboard` the core keyboard device, whose key events reach the map
|
|
13
|
+
// through `input.handlers` spread on the window.
|
|
14
|
+
//
|
|
15
|
+
// Keys and sticks move the CAMERA where a drag moves the content (the
|
|
16
|
+
// stick right orbits the eye to the right; a drag right spins the scene
|
|
17
|
+
// to the right), so the rotate and pan bindings of those devices go
|
|
18
|
+
// through invert() and each axis keeps its one meaning. `look` is the
|
|
19
|
+
// exception by nature: a drag and a stick both turn the eye.
|
|
20
|
+
|
|
21
|
+
import { invert } from "@solidrt/core/input"
|
|
22
|
+
import type { Binding, GamepadDevice, KeyboardDevice, PointerFeed } from "@solidrt/core"
|
|
23
|
+
|
|
24
|
+
export type CameraDevices = {
|
|
25
|
+
pointer?: PointerFeed
|
|
26
|
+
gamepad?: GamepadDevice
|
|
27
|
+
keyboard?: KeyboardDevice
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The orbit control's actions: `rotate`, `zoom`, `pan`. */
|
|
31
|
+
export let orbitActions = { rotate: "vec2", zoom: "axis", pan: "vec2" } as const
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The orbit camera's standard bindings: a drag rotates, a Ctrl-drag or a
|
|
35
|
+
* right-drag pans (the desktop pans, Three's and Blender's; a chorded or
|
|
36
|
+
* right-button drag feeds only `pan`, never `rotate`), a pinch and the
|
|
37
|
+
* wheel zoom, two fingers pan; the right stick rotates, the triggers
|
|
38
|
+
* zoom, the left stick pans; the arrow keys rotate and minus/equals zoom.
|
|
39
|
+
*/
|
|
40
|
+
export function orbitBindings(devices: CameraDevices): Binding[] {
|
|
41
|
+
let out: Binding[] = []
|
|
42
|
+
let { pointer, gamepad, keyboard } = devices
|
|
43
|
+
if (pointer) {
|
|
44
|
+
out.push(
|
|
45
|
+
{ action: "rotate", source: pointer.drag },
|
|
46
|
+
{ action: "pan", source: pointer.drag("Ctrl") },
|
|
47
|
+
{ action: "pan", source: pointer.drag("Right") },
|
|
48
|
+
{ action: "pan", source: pointer.pan },
|
|
49
|
+
{ action: "zoom", source: pointer.pinch },
|
|
50
|
+
{ action: "zoom", source: pointer.wheel },
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
if (gamepad) {
|
|
54
|
+
out.push({ action: "rotate", source: invert(gamepad.rightStick) }, { action: "zoom", source: gamepad.triggers }, { action: "pan", source: invert(gamepad.leftStick) })
|
|
55
|
+
}
|
|
56
|
+
if (keyboard) {
|
|
57
|
+
out.push({ action: "rotate", source: invert(keyboard.arrows) }, { action: "zoom", source: keyboard.axis("Minus", "Equal") })
|
|
58
|
+
}
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The first-person control's actions: `look`, `move`, `rise`. */
|
|
63
|
+
export let firstPersonActions = { look: "vec2", move: "vec2", rise: "axis" } as const
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The first-person camera's standard bindings: a drag looks around and so
|
|
67
|
+
* does mouse motion while the pointer is locked (the app locks it: see
|
|
68
|
+
* examples/first-person.tsx); the right stick looks, the left stick
|
|
69
|
+
* walks, the shoulders rise and sink in fly mode; WASD and the arrows
|
|
70
|
+
* walk, Q/E rise and sink.
|
|
71
|
+
*/
|
|
72
|
+
export function firstPersonBindings(devices: CameraDevices): Binding[] {
|
|
73
|
+
let out: Binding[] = []
|
|
74
|
+
let { pointer, gamepad, keyboard } = devices
|
|
75
|
+
if (pointer) out.push({ action: "look", source: pointer.drag }, { action: "look", source: pointer.mouseDelta })
|
|
76
|
+
if (gamepad) out.push({ action: "look", source: gamepad.rightStick }, { action: "move", source: gamepad.leftStick }, { action: "rise", source: gamepad.shoulders })
|
|
77
|
+
if (keyboard) out.push({ action: "move", source: keyboard.wasd }, { action: "move", source: keyboard.arrows }, { action: "rise", source: keyboard.axis("KeyQ", "KeyE") })
|
|
78
|
+
return out
|
|
79
|
+
}
|