@solidrt/3d 0.0.58 → 0.0.59
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 +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +24 -2
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +605 -185
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/src/geometry-gpu.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Geometry on the GPU: the lazy buffer step for geometry.ts's data. Buffers
|
|
2
|
-
// (and the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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:
|
|
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
|
|
30
|
-
*
|
|
31
|
-
|
|
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;
|
|
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
|
|
40
|
-
* one GPU buffer, each entry
|
|
41
|
-
|
|
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(
|
|
44
|
-
let upload = vertexUploads.get(
|
|
72
|
+
function acquireVertices(vertices: ArrayBufferView, label: string | undefined): BufferId {
|
|
73
|
+
let upload = vertexUploads.get(vertices)
|
|
45
74
|
if (upload === undefined) {
|
|
46
|
-
upload = {
|
|
47
|
-
|
|
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:
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
}
|