@solidrt/3d 0.0.58 → 0.0.60

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.
Files changed (70) hide show
  1. package/AGENTS.md +734 -264
  2. package/README.md +4 -2
  3. package/examples/README.md +50 -4
  4. package/examples/bloom.tsx +113 -0
  5. package/examples/cascades.tsx +4 -4
  6. package/examples/collision.tsx +6 -6
  7. package/examples/exits.tsx +96 -0
  8. package/examples/first-person.tsx +5 -5
  9. package/examples/fleet.tsx +4 -4
  10. package/examples/fog.tsx +5 -5
  11. package/examples/instanced.tsx +70 -50
  12. package/examples/lamps.tsx +4 -4
  13. package/examples/lod.tsx +125 -0
  14. package/examples/materials.tsx +9 -9
  15. package/examples/morph.tsx +174 -0
  16. package/examples/normals.tsx +118 -0
  17. package/examples/{lit.tsx → phong.tsx} +9 -9
  18. package/examples/polyhedra.tsx +61 -0
  19. package/examples/scene-atlas.tsx +70 -0
  20. package/examples/scene-views.tsx +5 -5
  21. package/examples/shadows.tsx +9 -9
  22. package/examples/sky-lit.tsx +3 -3
  23. package/examples/skybox.tsx +5 -5
  24. package/examples/standard.tsx +2 -2
  25. package/examples/streams.tsx +122 -0
  26. package/examples/sweep-paths.tsx +5 -5
  27. package/examples/wireframe.tsx +24 -2
  28. package/package.json +3 -3
  29. package/src/clip.ts +18 -5
  30. package/src/color.ts +11 -2
  31. package/src/components/context.tsx +11 -2
  32. package/src/components/directional-light.tsx +2 -2
  33. package/src/components/first-person-camera.tsx +4 -4
  34. package/src/components/group.tsx +9 -5
  35. package/src/components/hemisphere-light.tsx +2 -2
  36. package/src/components/index.ts +4 -1
  37. package/src/components/instanced-mesh.tsx +28 -10
  38. package/src/components/lod.tsx +102 -0
  39. package/src/components/mesh.tsx +24 -6
  40. package/src/components/node-props.ts +35 -5
  41. package/src/components/orbit-camera.tsx +15 -12
  42. package/src/components/point-light.tsx +2 -2
  43. package/src/components/record-mesh.tsx +12 -7
  44. package/src/components/scene.tsx +55 -2
  45. package/src/components/spot-light.tsx +2 -2
  46. package/src/components/sprite.tsx +2 -2
  47. package/src/components/view3d.tsx +18 -1
  48. package/src/environment.ts +9 -8
  49. package/src/first-person.ts +125 -38
  50. package/src/geometry-gpu.ts +172 -39
  51. package/src/geometry.ts +1573 -239
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +363 -141
  54. package/src/index.ts +31 -21
  55. package/src/light.ts +7 -8
  56. package/src/lod.ts +148 -0
  57. package/src/material.ts +316 -202
  58. package/src/mesh.ts +490 -256
  59. package/src/mixer.ts +27 -14
  60. package/src/model-file.ts +54 -15
  61. package/src/model.ts +86 -21
  62. package/src/motion.ts +17 -0
  63. package/src/node.ts +421 -22
  64. package/src/orbit.ts +378 -116
  65. package/src/profile.ts +4 -25
  66. package/src/resolve.ts +181 -0
  67. package/src/scene-shadows.ts +94 -93
  68. package/src/scene.ts +605 -185
  69. package/src/sweep.ts +27 -8
  70. package/tools/model.ts +3 -1
@@ -1,7 +1,8 @@
1
1
  // Geometry on the GPU: the lazy buffer step for geometry.ts's data. Buffers
2
- // (and the picking shape - the spatial core's own copy of positions, UVs
3
- // and indices for the triangle narrowphase, one per geometry however many
4
- // meshes share it) are created on first acquire and shared by every mesh
2
+ // (and the shape - the spatial core's own copy of positions, UVs and, for
3
+ // a triangle list, indices, one per geometry however many meshes share
4
+ // it: the box of every node drawing the geometry and the triangle
5
+ // narrowphase) are created on first acquire and shared by every mesh
5
6
  // and scene drawing the geometry; each draw entry holds one reference, and
6
7
  // the buffers are
7
8
  // freed when the last reference is released - deferred to a microtask, so
@@ -11,52 +12,74 @@
11
12
  // disposeGeometry frees immediately, the explicit override; either way the
12
13
  // geometry stays usable - fresh buffers are created on next acquire.
13
14
 
14
- import { createBuffer, destroyBuffer } from "@solidrt/core/gpu"
15
- import type { BufferId, IndexFormat } from "@solidrt/core/gpu"
16
- import { createShape, destroyShape } from "flux:spatial"
15
+ import { createBuffer, createTexture, destroyBuffer, destroyTexture, limits, writeBuffer } from "@solidrt/core/gpu"
16
+ import type { BufferId, IndexFormat, TextureId } from "@solidrt/core/gpu"
17
+ import { computeNormals, createShape, destroyShape, updateShape } from "flux:spatial"
17
18
  import type { ShapeId } from "flux:spatial"
18
- import { geometryTopology, layoutStride } from "./geometry.ts"
19
- import type { Geometry } from "./geometry.ts"
19
+ import { geometrySlot, geometryStreams, geometryTopology, geometryVertexCount, layoutSlot, layoutStride, vertexBytes, MORPH_TEXEL_FLOATS } from "./geometry.ts"
20
+ import type { Geometry, MorphTargets } from "./geometry.ts"
20
21
 
21
22
  /** An acquired reference to a geometry's GPU buffers: what a draw entry
22
23
  * binds, and the token releaseGeometryBuffers takes - releasing the exact
23
24
  * acquisition keeps the pairing correct however the caller's geometry
24
25
  * fields have moved since. */
25
26
  export type GeometryBuffers = {
26
- buffer: BufferId
27
+ /** One vertex buffer per stream, in stream order: what the entry binds
28
+ * before the material's instance buffers. */
29
+ buffers: BufferId[]
27
30
  index: BufferId
28
31
  indexFormat: IndexFormat
29
- /** The picking shape (positions at 0, uv at 6 of every layout); null
30
- * for anything but a triangle list, which picks by its box. */
31
- shape: ShapeId | null
32
+ /** The core's shape: positions (and, when the layout carries a
33
+ * float32x2 aUV, uvs) for every topology, the box of each node set to
34
+ * it; the triangle narrowphase too for a triangle list. */
35
+ shape: ShapeId
36
+ /** The geometry's packed morph targets as an rgba32f texture (what a
37
+ * morphing material binds as `uMorphs`, `width` its `uMorphWidth`), or
38
+ * null for geometry without targets. */
39
+ morphs: { texture: TextureId; width: number } | null
32
40
  }
33
41
 
34
- type GpuEntry = GeometryBuffers & { geometry: Geometry; vertices: Float32Array; refs: number }
42
+ type GpuEntry = GeometryBuffers & { geometry: Geometry; streams: ArrayBufferView[]; refs: number }
43
+
44
+ // The packed texels laid into a texture: as square as the count allows
45
+ // (the least padding), never wider than the device permits; the shader
46
+ // turns a texel index back into coordinates by the width.
47
+ function uploadMorphs(morphs: MorphTargets, label: string | undefined): { texture: TextureId; width: number } {
48
+ let texels = morphs.texels.length / MORPH_TEXEL_FLOATS
49
+ let width = Math.max(1, Math.min(limits.maxTextureSize, Math.ceil(Math.sqrt(texels))))
50
+ let height = Math.max(1, Math.ceil(texels / width))
51
+ if (height > limits.maxTextureSize) {
52
+ throw new Error("Morph targets need " + texels + " texels, past the device's " + limits.maxTextureSize + " x " + limits.maxTextureSize + " texture")
53
+ }
54
+ let data = morphs.texels
55
+ if (data.length !== width * height * MORPH_TEXEL_FLOATS) {
56
+ data = new Float32Array(width * height * MORPH_TEXEL_FLOATS)
57
+ data.set(morphs.texels)
58
+ }
59
+ let texture = createTexture(data, width, height, { format: "rgba32f", filter: "nearest", autoFree: false, label: label ? label + "-morphs" : undefined })
60
+ return { texture, width }
61
+ }
35
62
 
36
63
  let entries = new WeakMap<Geometry, GpuEntry>()
37
64
 
38
65
  /** Vertex uploads keyed by the array itself: geometries sharing one
39
- * vertex array (a wireframe or edges geometry over its source's) share
40
- * one GPU buffer, each entry holding a reference to it. */
41
- let vertexUploads = new WeakMap<Float32Array, { buffer: BufferId; refs: number }>()
66
+ * vertex array (a wireframe or edges geometry over its source's, a
67
+ * stream shared through withAttribute) share one GPU buffer, each entry
68
+ * holding a reference to it - which is also what makes an in-place
69
+ * update (updateVertices) reach every sharer. */
70
+ let vertexUploads = new WeakMap<ArrayBufferView, { buffer: BufferId; refs: number }>()
42
71
 
43
- function acquireVertices(geometry: Geometry): BufferId {
44
- let upload = vertexUploads.get(geometry.vertices)
72
+ function acquireVertices(vertices: ArrayBufferView, label: string | undefined): BufferId {
73
+ let upload = vertexUploads.get(vertices)
45
74
  if (upload === undefined) {
46
- upload = {
47
- buffer: createBuffer(geometry.vertices, {
48
- autoFree: false,
49
- label: geometry.label ? geometry.label + "-verts" : undefined,
50
- }),
51
- refs: 0,
52
- }
53
- vertexUploads.set(geometry.vertices, upload)
75
+ upload = { buffer: createBuffer(vertices, { autoFree: false, label }), refs: 0 }
76
+ vertexUploads.set(vertices, upload)
54
77
  }
55
78
  upload.refs++
56
79
  return upload.buffer
57
80
  }
58
81
 
59
- function releaseVertices(vertices: Float32Array): void {
82
+ function releaseVertices(vertices: ArrayBufferView): void {
60
83
  let upload = vertexUploads.get(vertices)
61
84
  if (upload === undefined) return
62
85
  upload.refs--
@@ -65,6 +88,29 @@ function releaseVertices(vertices: Float32Array): void {
65
88
  destroyBuffer(upload.buffer)
66
89
  }
67
90
 
91
+ // The shape reads positions (and uvs, when they are plain floats in
92
+ // stream 0) through a Float32Array view over the main buffer's bytes:
93
+ // every stride and offset is a multiple of 4, so the view is exact
94
+ // whatever the layout packs elsewhere. A uv in a packed format or in
95
+ // another stream is left out (hits carry no uv); positions are float32x3
96
+ // in stream 0 by the layout rule.
97
+ type ShapeView = { floats: Float32Array; stride: number; uvAt: number }
98
+
99
+ function shapeView(geometry: Geometry): ShapeView {
100
+ let v = geometry.vertices
101
+ let floats = new Float32Array(v.buffer, v.byteOffset, v.byteLength / Float32Array.BYTES_PER_ELEMENT)
102
+ let uv = layoutSlot(geometry.layout, "aUV")
103
+ let uvAt = uv !== null && uv.format === "float32x2" ? uv.offset / Float32Array.BYTES_PER_ELEMENT : -1
104
+ return { floats, stride: layoutStride(geometry.layout) / Float32Array.BYTES_PER_ELEMENT, uvAt }
105
+ }
106
+
107
+ // Only a triangle list hands the core its indices: any other topology is
108
+ // box-only, its shape there for the box alone.
109
+ function createGeometryShape(geometry: Geometry): ShapeId {
110
+ let { floats, stride, uvAt } = shapeView(geometry)
111
+ return createShape(floats, stride, 0, uvAt, geometryTopology(geometry) === "triangles" ? geometry.indices : undefined)
112
+ }
113
+
68
114
  /** The geometry's GPU buffers, created on first use, plus the index format
69
115
  * the draw entry must bind them with. Takes a reference - pair every
70
116
  * acquire with a releaseGeometryBuffers of the returned token when the
@@ -72,19 +118,18 @@ function releaseVertices(vertices: Float32Array): void {
72
118
  export function acquireGeometryBuffers(geometry: Geometry): GeometryBuffers {
73
119
  let entry = entries.get(geometry)
74
120
  if (entry === undefined) {
121
+ let streams = geometryStreams(geometry).map(s => s.vertices)
75
122
  entry = {
76
123
  geometry,
77
- vertices: geometry.vertices,
78
- buffer: acquireVertices(geometry),
124
+ streams,
125
+ buffers: streams.map((vertices, i) => acquireVertices(vertices, geometry.label ? geometry.label + (i === 0 ? "-verts" : "-stream" + i) : undefined)),
79
126
  index: createBuffer(geometry.indices, {
80
127
  autoFree: false,
81
128
  label: geometry.label ? geometry.label + "-indices" : undefined,
82
129
  }),
83
130
  indexFormat: geometry.indices instanceof Uint32Array ? "uint32" : "uint16",
84
- shape:
85
- geometryTopology(geometry) === "triangles"
86
- ? createShape(geometry.vertices, layoutStride(geometry.layout), 0, 6, geometry.indices)
87
- : null,
131
+ shape: createGeometryShape(geometry),
132
+ morphs: geometry.morphs === undefined ? null : uploadMorphs(geometry.morphs, geometry.label),
88
133
  refs: 0,
89
134
  }
90
135
  entries.set(geometry, entry)
@@ -106,12 +151,17 @@ export function releaseGeometryBuffers(acquired: GeometryBuffers): void {
106
151
  queueMicrotask(() => {
107
152
  if (entries.get(entry.geometry) !== entry || entry.refs > 0) return
108
153
  entries.delete(entry.geometry)
109
- releaseVertices(entry.vertices)
110
- destroyBuffer(entry.index)
111
- if (entry.shape !== null) destroyShape(entry.shape)
154
+ freeEntry(entry)
112
155
  })
113
156
  }
114
157
 
158
+ function freeEntry(entry: GpuEntry): void {
159
+ for (let vertices of entry.streams) releaseVertices(vertices)
160
+ destroyBuffer(entry.index)
161
+ destroyShape(entry.shape)
162
+ if (entry.morphs !== null) destroyTexture(entry.morphs.texture)
163
+ }
164
+
115
165
  /**
116
166
  * Free the geometry's GPU buffers now, held references or not - the
117
167
  * explicit override for geometry an app is done with for good. Draw
@@ -123,7 +173,90 @@ export function disposeGeometry(geometry: Geometry): void {
123
173
  let entry = entries.get(geometry)
124
174
  if (entry === undefined) return
125
175
  entries.delete(geometry)
126
- releaseVertices(entry.vertices)
127
- destroyBuffer(entry.index)
128
- if (entry.shape !== null) destroyShape(entry.shape)
176
+ freeEntry(entry)
177
+ }
178
+
179
+ /** Options of `updateVertices`: the stream (default 0, the main buffer)
180
+ * and the vertex range (default the whole stream). */
181
+ export type UpdateVerticesOptions = { stream?: number; first?: number; count?: number }
182
+
183
+ /**
184
+ * Re-upload vertices `[first, first + count)` of one stream from the
185
+ * geometry's own array, in place: Three's `attribute.needsUpdate` with an
186
+ * update range. Write the array first (through geometryAttribute or
187
+ * fillAttribute), then call this; every mesh, view and wireframe over the
188
+ * geometry sees the new bytes, and the other streams are untouched -
189
+ * which is what a per-frame channel in a stream of its own buys. A
190
+ * stream-0 update drops the cached bounds and rewrites the same range of
191
+ * the core's shape, so every node drawing the geometry picks and culls
192
+ * where it is now drawn: the box follows at the next flush, the triangle
193
+ * index is rebuilt by the next query (Three's raycast reads the live
194
+ * attribute the same way; Unity's and Godot's colliders never follow). A
195
+ * geometry not yet on the GPU has nothing to update: its first acquire
196
+ * uploads the array as it is then.
197
+ */
198
+ export function updateVertices(geometry: Geometry, options: UpdateVerticesOptions = {}): void {
199
+ let streams = geometryStreams(geometry)
200
+ let index = options.stream ?? 0
201
+ if (!Number.isInteger(index) || index < 0 || index >= streams.length) {
202
+ throw new Error("updateVertices: stream " + index + " is out of range; the geometry has streams 0.." + (streams.length - 1))
203
+ }
204
+ let total = geometryVertexCount(geometry, "updateVertices")
205
+ let first = options.first ?? 0
206
+ let count = options.count ?? total - first
207
+ if (!Number.isInteger(first) || !Number.isInteger(count) || first < 0 || count < 0 || first + count > total) {
208
+ throw new Error("updateVertices: range [" + first + ", " + (first + count) + ") is outside the geometry's " + total + " vertices")
209
+ }
210
+ if (index === 0) geometry._bounds = undefined
211
+ let stream = streams[index]!
212
+ let upload = vertexUploads.get(stream.vertices)
213
+ if (upload === undefined || count === 0) return
214
+ let stride = layoutStride(stream.layout)
215
+ writeBuffer(upload.buffer, vertexBytes(stream.vertices).subarray(first * stride, (first + count) * stride), first * stride)
216
+ let entry = entries.get(geometry)
217
+ if (index === 0 && entry !== undefined) {
218
+ let view = shapeView(geometry)
219
+ updateShape(entry.shape, view.floats.subarray(first * view.stride, (first + count) * view.stride), view.stride, 0, view.uvAt, first)
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Recompute a triangle geometry's normals in place from its faces
225
+ * (Three's computeVertexNormals, Unity's RecalculateNormals, Godot's
226
+ * generate_normals, all in place): each vertex takes the normals of the
227
+ * faces that name its INDEX, weighted by the corner angle, normalized -
228
+ * a vertex shared across faces shades smooth, a split one per face, so a
229
+ * merged or deformed generator geometry comes out right with no options
230
+ * (except along a uv seam, whose copies each see one side; Three's
231
+ * artifact too, and withNormals, which matches faces by position, has
232
+ * none). The math runs in the core in one call over the vertex floats;
233
+ * only the layout is resolved here. Needs aNormal in float32x3 (or x4):
234
+ * a packed normal is an authoring format, so pack after withNormals
235
+ * instead. Returns the stream carrying aNormal, the one updateVertices
236
+ * re-uploads, which makes the CPU deformation loop `fillAttribute(g,
237
+ * "aPos", ...)`, `computeVertexNormals(g)`, `updateVertices(g)`. That
238
+ * loop is for data that genuinely changes on the CPU - a streamed cloud,
239
+ * a cloth solved in JS, an editor edit. A per-frame ripple, sway or wave
240
+ * belongs in a vertex shader: displace and derive the normal in the
241
+ * vertex stage (shaderMaterial) and no per-vertex work touches the CPU
242
+ * at all. Morph normal deltas are left alone.
243
+ */
244
+ export function computeVertexNormals(geometry: Geometry): ArrayBufferView {
245
+ if (geometryTopology(geometry) !== "triangles") throw new Error("computeVertexNormals: needs a triangle geometry, got " + geometryTopology(geometry))
246
+ let slot = geometrySlot(geometry, "aNormal")
247
+ if (slot === null) throw new Error("computeVertexNormals: geometry has no aNormal channel; withNormals adds one")
248
+ if (slot.format !== "float32x3" && slot.format !== "float32x4") {
249
+ throw new Error("computeVertexNormals: aNormal is " + slot.format + "; a packed normal is an authoring format, compute with withNormals before packing")
250
+ }
251
+ geometryVertexCount(geometry, "computeVertexNormals")
252
+ let { floats, stride } = shapeView(geometry)
253
+ let target = geometryStreams(geometry)[slot.stream]!
254
+ let t = target.vertices
255
+ let normals = new Float32Array(t.buffer, t.byteOffset, t.byteLength / Float32Array.BYTES_PER_ELEMENT)
256
+ computeNormals(
257
+ { data: floats, stride, offset: 0 },
258
+ { data: normals, stride: layoutStride(target.layout) / Float32Array.BYTES_PER_ELEMENT, offset: slot.offset / Float32Array.BYTES_PER_ELEMENT },
259
+ geometry.indices,
260
+ )
261
+ return t
129
262
  }