@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/geometry.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
// Geometry: interleaved vertex
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// Geometry: one interleaved vertex buffer described by a layout - an
|
|
2
|
+
// ordered attribute list that always starts with the standard prefix
|
|
3
|
+
// (position vec3, normal vec3, uv vec2: 8 floats, what every generator
|
|
4
|
+
// emits) and may carry further channels after it. The layout is open data:
|
|
5
|
+
// `withAttribute` appends any named channel (Three's `setAttribute`), and
|
|
6
|
+
// "colored" names the one common case, the prefix plus an aColor vec4 (12
|
|
7
|
+
// floats) as the per-vertex data channel for custom materials (tint, baked
|
|
8
|
+
// AO, any four scalars). Materials read attributes by name and adapt to
|
|
9
|
+
// whatever layout their geometry carries (one pipeline per layout met),
|
|
10
|
+
// so a geometry may carry more channels than a material reads.
|
|
11
|
+
// Indices are uint16 or uint32 (the generators here emit uint16; hand-built
|
|
7
12
|
// geometry past 64k vertices uses a Uint32Array and the draw entry follows
|
|
8
13
|
// the array type). Winding is counter-clockwise seen from outside in the
|
|
9
14
|
// y-up world, which the standard camera rig (perspective() with its baked
|
|
@@ -18,9 +23,12 @@
|
|
|
18
23
|
|
|
19
24
|
import type { VertexAttribute } from "@solidrt/core/gpu"
|
|
20
25
|
import { add, compose, cross, mat4, normalize, normalMatrix, sub, updateRotation, updateScale } from "./math.ts"
|
|
21
|
-
import type { Quat, TransformUpdate, Vec2, Vec3
|
|
26
|
+
import type { Quat, TransformUpdate, Vec2, Vec3 } from "./math.ts"
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
/** A vertex layout: the named presets, or an explicit attribute list that
|
|
29
|
+
* must begin with the standard prefix (aPos vec3, aNormal vec3, aUV vec2).
|
|
30
|
+
* Absent on a Geometry means "standard". */
|
|
31
|
+
export type VertexLayout = "standard" | "colored" | VertexAttribute[]
|
|
24
32
|
|
|
25
33
|
const STANDARD_ATTRIBUTES: VertexAttribute[] = [
|
|
26
34
|
{ name: "aPos", format: "vec3" },
|
|
@@ -28,17 +36,146 @@ const STANDARD_ATTRIBUTES: VertexAttribute[] = [
|
|
|
28
36
|
{ name: "aUV", format: "vec2" },
|
|
29
37
|
]
|
|
30
38
|
|
|
31
|
-
/** The
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
export const VERTEX_LAYOUTS: Record<VertexLayout, VertexAttribute[]> = {
|
|
39
|
+
/** The attribute lists behind the named layouts. Every layout shares the
|
|
40
|
+
* standard prefix, so one shader vocabulary serves all of them. */
|
|
41
|
+
export const VERTEX_LAYOUTS: Record<"standard" | "colored", VertexAttribute[]> = {
|
|
35
42
|
standard: STANDARD_ATTRIBUTES,
|
|
36
43
|
colored: [...STANDARD_ATTRIBUTES, { name: "aColor", format: "vec4" }],
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
/** Floats per vertex in the "standard" layout (
|
|
40
|
-
|
|
41
|
-
const
|
|
46
|
+
/** Floats per vertex in the "standard" layout (the generators' own write
|
|
47
|
+
* format before packing). */
|
|
48
|
+
export const STANDARD_FLOATS = 8
|
|
49
|
+
|
|
50
|
+
const FORMAT_FLOATS: Record<VertexAttribute["format"], number> = { f32: 1, vec2: 2, vec3: 3, vec4: 4 }
|
|
51
|
+
|
|
52
|
+
/** The attribute list of a layout (a preset name resolves to its list). */
|
|
53
|
+
export function layoutAttributes(layout?: VertexLayout): VertexAttribute[] {
|
|
54
|
+
if (layout === undefined || layout === "standard") return VERTEX_LAYOUTS.standard
|
|
55
|
+
if (layout === "colored") return VERTEX_LAYOUTS.colored
|
|
56
|
+
return layout
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Floats per vertex of a layout - its interleave stride. */
|
|
60
|
+
export function layoutStride(layout?: VertexLayout): number {
|
|
61
|
+
let stride = 0
|
|
62
|
+
for (let attr of layoutAttributes(layout)) stride += FORMAT_FLOATS[attr.format]
|
|
63
|
+
return stride
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A layout's identity as a string (name:format per attribute, in order):
|
|
67
|
+
* two layouts with equal keys interleave identically. */
|
|
68
|
+
export function layoutKey(layout?: VertexLayout): string {
|
|
69
|
+
return layoutAttributes(layout)
|
|
70
|
+
.map(a => a.name + ":" + a.format)
|
|
71
|
+
.join(",")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Where an attribute sits in the interleave: its float offset and size.
|
|
75
|
+
* Null when the layout does not carry that name. */
|
|
76
|
+
export function layoutSlot(layout: VertexLayout | undefined, name: string): { offset: number; size: number; format: VertexAttribute["format"] } | null {
|
|
77
|
+
let offset = 0
|
|
78
|
+
for (let attr of layoutAttributes(layout)) {
|
|
79
|
+
let size = FORMAT_FLOATS[attr.format]
|
|
80
|
+
if (attr.name === name) return { offset, size, format: attr.format }
|
|
81
|
+
offset += size
|
|
82
|
+
}
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The prefix check every layout must pass: standard attributes first, in
|
|
87
|
+
* order, and no duplicate names after them. */
|
|
88
|
+
function checkLayout(layout: VertexAttribute[], where: string): void {
|
|
89
|
+
for (let i = 0; i < STANDARD_ATTRIBUTES.length; i++) {
|
|
90
|
+
let want = STANDARD_ATTRIBUTES[i]!
|
|
91
|
+
let got = layout[i]
|
|
92
|
+
if (got === undefined || got.name !== want.name || got.format !== want.format) {
|
|
93
|
+
throw new Error(where + ": a layout must start with the standard prefix (aPos vec3, aNormal vec3, aUV vec2)")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
let seen = new Set<string>()
|
|
97
|
+
for (let attr of layout) {
|
|
98
|
+
if (seen.has(attr.name)) throw new Error(where + ": duplicate attribute '" + attr.name + "'")
|
|
99
|
+
seen.add(attr.name)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The structural check for geometry about to draw: the layout passes the
|
|
105
|
+
* prefix rule, the vertex float count is a whole number of its stride,
|
|
106
|
+
* and indices are present. Throws naming the geometry. The scene runs it
|
|
107
|
+
* at add() so hand-built geometry (a bare `layout: "colored"` over a
|
|
108
|
+
* miscounted array) fails there instead of drawing garbage triangles.
|
|
109
|
+
* Deliberately no max-index scan: that is O(indices) per add, and the
|
|
110
|
+
* generators and merge/transform keep indices in range by construction.
|
|
111
|
+
*/
|
|
112
|
+
export function validateGeometry(geometry: Geometry): void {
|
|
113
|
+
let name = geometry.label ? "geometry '" + geometry.label + "'" : "geometry"
|
|
114
|
+
let layout = geometry.layout
|
|
115
|
+
if (layout !== undefined && typeof layout !== "string") checkLayout(layout, name)
|
|
116
|
+
let stride = layoutStride(layout)
|
|
117
|
+
if (geometry.vertices.length % stride !== 0) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
name + ": " + geometry.vertices.length + " vertex floats is not a whole number of " + stride + "-float (" + layoutKey(layout) + ") vertices",
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
if (geometry.indices.length === 0) throw new Error(name + ": no indices")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The options every generator shares (each generator's own options type
|
|
126
|
+
* extends this with its dimensions, all optional with defaults): `label`
|
|
127
|
+
* for the GPU buffers, and `layout` to emit vertices in a wider layout
|
|
128
|
+
* directly (the standard channels written, the extra slots zeroed for
|
|
129
|
+
* fillAttribute/fillColors to write in place), so colored or custom
|
|
130
|
+
* channel geometry is built in one pass instead of generate-then-repack. */
|
|
131
|
+
export type GeometryOptions = { label?: string; layout?: VertexLayout }
|
|
132
|
+
|
|
133
|
+
/** The generator tail: pack standard-layout vertices (number[] of 8 per
|
|
134
|
+
* vertex, or an already-written Float32Array) and indices into a Geometry
|
|
135
|
+
* of the requested layout. A wider layout spreads the standard channels
|
|
136
|
+
* to its stride, leaving the extra slots zero. */
|
|
137
|
+
export function packGeometry(
|
|
138
|
+
verts: ArrayLike<number>,
|
|
139
|
+
indices: number[] | Uint16Array | Uint32Array,
|
|
140
|
+
options: GeometryOptions = {},
|
|
141
|
+
): Geometry {
|
|
142
|
+
let { label, layout } = options
|
|
143
|
+
if (verts.length % STANDARD_FLOATS !== 0) {
|
|
144
|
+
throw new Error("packGeometry: vertex data is not a whole number of standard-layout vertices")
|
|
145
|
+
}
|
|
146
|
+
let count = verts.length / STANDARD_FLOATS
|
|
147
|
+
let packedIndices = indices instanceof Uint16Array || indices instanceof Uint32Array ? indices : packIndices(indices, count)
|
|
148
|
+
let attrs = layoutAttributes(layout)
|
|
149
|
+
if (layout !== undefined && typeof layout !== "string") checkLayout(attrs, "packGeometry")
|
|
150
|
+
let stride = layoutStride(attrs)
|
|
151
|
+
if (stride === STANDARD_FLOATS) {
|
|
152
|
+
let vertices = verts instanceof Float32Array ? verts : new Float32Array(verts)
|
|
153
|
+
return layout === undefined ? { vertices, indices: packedIndices, label } : { vertices, indices: packedIndices, layout, label }
|
|
154
|
+
}
|
|
155
|
+
let vertices = new Float32Array(count * stride)
|
|
156
|
+
for (let i = 0; i < count; i++) {
|
|
157
|
+
let s = i * STANDARD_FLOATS
|
|
158
|
+
let d = i * stride
|
|
159
|
+
for (let k = 0; k < STANDARD_FLOATS; k++) vertices[d + k] = verts[s + k]!
|
|
160
|
+
}
|
|
161
|
+
return { vertices, indices: packedIndices, layout, label }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The stride a generator writing a Float32Array directly must use for the
|
|
165
|
+
// requested layout, and the matching finish (no repack: the data is
|
|
166
|
+
// already laid out).
|
|
167
|
+
function generatorStride(options: GeometryOptions): number {
|
|
168
|
+
let { layout } = options
|
|
169
|
+
let attrs = layoutAttributes(layout)
|
|
170
|
+
if (layout !== undefined && typeof layout !== "string") checkLayout(attrs, "generator layout")
|
|
171
|
+
return layoutStride(attrs)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function finishGeometry(vertices: Float32Array, indices: number[], options: GeometryOptions): Geometry {
|
|
175
|
+
let { label, layout } = options
|
|
176
|
+
let packed = packIndices(indices, vertices.length / layoutStride(layout))
|
|
177
|
+
return layout === undefined ? { vertices, indices: packed, label } : { vertices, indices: packed, layout, label }
|
|
178
|
+
}
|
|
42
179
|
|
|
43
180
|
/** Uint16 indices when they fit, Uint32Array past 64k vertices - the draw
|
|
44
181
|
* entry follows the array type. The tail of every unbounded generator. */
|
|
@@ -73,7 +210,7 @@ export function geometryBounds(geometry: Geometry): Float32Array {
|
|
|
73
210
|
if (bounds === undefined) {
|
|
74
211
|
bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
|
|
75
212
|
let v = geometry.vertices
|
|
76
|
-
let stride = geometry.layout
|
|
213
|
+
let stride = layoutStride(geometry.layout)
|
|
77
214
|
for (let i = 0; i + 2 < v.length; i += stride) {
|
|
78
215
|
let x = v[i]!, y = v[i + 1]!, z = v[i + 2]!
|
|
79
216
|
if (x < bounds[0]!) bounds[0] = x
|
|
@@ -89,86 +226,129 @@ export function geometryBounds(geometry: Geometry): Float32Array {
|
|
|
89
226
|
return bounds
|
|
90
227
|
}
|
|
91
228
|
|
|
92
|
-
/** Per-vertex
|
|
93
|
-
*
|
|
94
|
-
|
|
229
|
+
/** Per-vertex values for withAttribute/fillAttribute: a flat array of the
|
|
230
|
+
* attribute's size per vertex, or a callback deriving each vertex's value
|
|
231
|
+
* from the standard channels (what a baker wants). */
|
|
232
|
+
export type AttributeFill = ArrayLike<number> | ((index: number, pos: Vec3, normal: Vec3, uv: Vec2) => ArrayLike<number>)
|
|
233
|
+
/** AttributeFill for the aColor vec4 channel (4 per vertex). */
|
|
234
|
+
export type ColorFill = AttributeFill
|
|
95
235
|
|
|
96
236
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
237
|
+
* Append a named channel to a geometry: a new geometry (the source is
|
|
238
|
+
* untouched, its GPU buffers stay independent) whose layout is the
|
|
239
|
+
* source's plus `attr`, every existing channel copied through and the new
|
|
240
|
+
* slots written from `fill`. This is Three's `geometry.setAttribute` for
|
|
241
|
+
* an interleaved buffer - the one generic primitive; `withColors` is its
|
|
242
|
+
* aColor spelling. A material reads the channel by declaring the matching
|
|
243
|
+
* `in` (name and format) in its vertex stage.
|
|
104
244
|
*/
|
|
105
|
-
export function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (geometry.vertices.length % FLOATS_PER_VERTEX !== 0) {
|
|
110
|
-
throw new Error("withColors: vertex data is not a whole number of standard-layout vertices")
|
|
245
|
+
export function withAttribute(geometry: Geometry, attr: VertexAttribute, fill: AttributeFill, label?: string): Geometry {
|
|
246
|
+
let srcLayout = layoutAttributes(geometry.layout)
|
|
247
|
+
if (layoutSlot(srcLayout, attr.name) !== null) {
|
|
248
|
+
throw new Error("withAttribute: geometry already carries '" + attr.name + "'")
|
|
111
249
|
}
|
|
112
|
-
let
|
|
113
|
-
if (
|
|
114
|
-
throw new Error("
|
|
250
|
+
let srcStride = layoutStride(srcLayout)
|
|
251
|
+
if (geometry.vertices.length % srcStride !== 0) {
|
|
252
|
+
throw new Error("withAttribute: vertex data is not a whole number of " + layoutKey(srcLayout) + " vertices")
|
|
115
253
|
}
|
|
254
|
+
let layout = [...srcLayout, { name: attr.name, format: attr.format }]
|
|
255
|
+
checkLayout(layout, "withAttribute")
|
|
256
|
+
let stride = layoutStride(layout)
|
|
257
|
+
let count = geometry.vertices.length / srcStride
|
|
116
258
|
let src = geometry.vertices
|
|
117
|
-
let out = new Float32Array(count *
|
|
259
|
+
let out = new Float32Array(count * stride)
|
|
118
260
|
for (let i = 0; i < count; i++) {
|
|
119
|
-
let s = i *
|
|
120
|
-
let d = i *
|
|
121
|
-
for (let k = 0; k <
|
|
261
|
+
let s = i * srcStride
|
|
262
|
+
let d = i * stride
|
|
263
|
+
for (let k = 0; k < srcStride; k++) out[d + k] = src[s + k]!
|
|
122
264
|
}
|
|
123
|
-
|
|
265
|
+
fillSlot(out, layout, attr.name, fill, 0)
|
|
124
266
|
return {
|
|
125
267
|
vertices: out,
|
|
126
268
|
indices: geometry.indices,
|
|
127
|
-
layout
|
|
128
|
-
label: label ?? (geometry.label ? geometry.label + "-
|
|
269
|
+
layout,
|
|
270
|
+
label: label ?? (geometry.label ? geometry.label + "-" + attr.name : undefined),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Derive a "colored"-layout geometry from a standard one: the same
|
|
276
|
+
* positions, normals, uvs and indices, plus an aColor vec4 per vertex -
|
|
277
|
+
* the data channel for materials whose vertex stage reads `in vec4 aColor`
|
|
278
|
+
* (a tint, baked ambient occlusion, any four scalars; the name is the
|
|
279
|
+
* standard vocabulary, the contents are yours). `withAttribute` with the
|
|
280
|
+
* aColor channel; the "colored" preset name is kept on the result.
|
|
281
|
+
*/
|
|
282
|
+
export function withColors(geometry: Geometry, fill: ColorFill, label?: string): Geometry {
|
|
283
|
+
if (layoutSlot(geometry.layout, "aColor") !== null) {
|
|
284
|
+
throw new Error("withColors: geometry already carries an aColor channel")
|
|
129
285
|
}
|
|
286
|
+
let out = withAttribute(geometry, { name: "aColor", format: "vec4" }, fill, label ?? (geometry.label ? geometry.label + "-colored" : undefined))
|
|
287
|
+
if (layoutKey(out.layout) === layoutKey("colored")) out.layout = "colored"
|
|
288
|
+
return out
|
|
130
289
|
}
|
|
131
290
|
|
|
132
291
|
/**
|
|
133
|
-
* The in-place primitive under
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* This trusts the buffer to BE colored-layout data - a bare array carries
|
|
143
|
-
* no layout tag, so only the arithmetic is checked. The Geometry-level
|
|
144
|
-
* withColors stays the checked path.
|
|
292
|
+
* The in-place primitive under withAttribute: write one channel the
|
|
293
|
+
* geometry's layout already carries (withAttribute ADDS a channel; this
|
|
294
|
+
* overwrites an existing one). The pos/normal/uv the callback receives
|
|
295
|
+
* are read from the buffer itself, so a builder baking transforms while
|
|
296
|
+
* writing hands the baker world-space vertices. Fills vertices
|
|
297
|
+
* [first, first + count) - count defaults to the rest of the buffer -
|
|
298
|
+
* and `fill` indexes relative to `first`, so a per-part callback works
|
|
299
|
+
* unchanged for both APIs. Returns `geometry.vertices`.
|
|
145
300
|
*/
|
|
146
|
-
export function
|
|
147
|
-
|
|
148
|
-
|
|
301
|
+
export function fillAttribute(geometry: Geometry, name: string, fill: AttributeFill, first = 0, count?: number): Float32Array {
|
|
302
|
+
return fillSlot(geometry.vertices, geometry.layout, name, fill, first, count)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The raw form behind fillAttribute (withAttribute writes its fresh
|
|
306
|
+
* buffer through it, before the Geometry exists): a bare array carries no
|
|
307
|
+
* layout tag, so the caller states the layout and only the arithmetic is
|
|
308
|
+
* checked. */
|
|
309
|
+
function fillSlot(vertices: Float32Array, layout: VertexLayout | undefined, name: string, fill: AttributeFill, first: number, count?: number): Float32Array {
|
|
310
|
+
let slot = layoutSlot(layout, name)
|
|
311
|
+
if (slot === null) throw new Error("fillAttribute: layout has no '" + name + "' attribute")
|
|
312
|
+
let stride = layoutStride(layout)
|
|
313
|
+
if (vertices.length % stride !== 0) {
|
|
314
|
+
throw new Error("fillAttribute: vertex data is not a whole number of " + layoutKey(layout) + " vertices")
|
|
149
315
|
}
|
|
150
|
-
let total = vertices.length /
|
|
316
|
+
let total = vertices.length / stride
|
|
151
317
|
let n = count ?? total - first
|
|
152
318
|
if (!Number.isInteger(first) || !Number.isInteger(n) || first < 0 || n < 0 || first + n > total) {
|
|
153
|
-
throw new Error("
|
|
319
|
+
throw new Error("fillAttribute: range [" + first + ", " + (first + n) + ") is outside the buffer's " + total + " vertices")
|
|
154
320
|
}
|
|
321
|
+
let size = slot.size
|
|
155
322
|
let fn = typeof fill === "function" ? fill : null
|
|
156
|
-
|
|
157
|
-
|
|
323
|
+
let flat = typeof fill === "function" ? null : fill
|
|
324
|
+
if (flat !== null && flat.length !== n * size) {
|
|
325
|
+
throw new Error("fillAttribute: fill has " + flat.length + " floats, expected " + size + " per vertex (" + n * size + ")")
|
|
158
326
|
}
|
|
159
327
|
for (let i = 0; i < n; i++) {
|
|
160
|
-
let d = (first + i) *
|
|
161
|
-
let
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
328
|
+
let d = (first + i) * stride
|
|
329
|
+
let value: ArrayLike<number>
|
|
330
|
+
let s: number
|
|
331
|
+
if (fn !== null) {
|
|
332
|
+
value = fn(i, [vertices[d]!, vertices[d + 1]!, vertices[d + 2]!], [vertices[d + 3]!, vertices[d + 4]!, vertices[d + 5]!], [vertices[d + 6]!, vertices[d + 7]!])
|
|
333
|
+
s = 0
|
|
334
|
+
if (value.length !== size) {
|
|
335
|
+
throw new Error("fillAttribute: fill callback returned " + value.length + " floats for '" + name + "', expected " + size)
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
value = flat!
|
|
339
|
+
s = i * size
|
|
340
|
+
}
|
|
341
|
+
for (let k = 0; k < size; k++) vertices[d + slot.offset + k] = value[s + k]!
|
|
168
342
|
}
|
|
169
343
|
return vertices
|
|
170
344
|
}
|
|
171
345
|
|
|
346
|
+
/** `fillAttribute` for the aColor channel of a color-carrying geometry
|
|
347
|
+
* (fill is 4 per vertex). */
|
|
348
|
+
export function fillColors(geometry: Geometry, fill: ColorFill, first = 0, count?: number): Float32Array {
|
|
349
|
+
return fillAttribute(geometry, "aColor", fill, first, count)
|
|
350
|
+
}
|
|
351
|
+
|
|
172
352
|
/**
|
|
173
353
|
* Bake a placement (the setTransform shape: Euler XYZ radians or a
|
|
174
354
|
* quaternion, not both; number = uniform scale; absent = identity) into a
|
|
@@ -187,10 +367,10 @@ export function transformGeometry(geometry: Geometry, transform: TransformUpdate
|
|
|
187
367
|
if (transform.scale !== undefined) updateScale(scl, transform.scale)
|
|
188
368
|
let m = compose(mat4(), transform.position ?? [0, 0, 0], rot, scl)
|
|
189
369
|
let n = normalMatrix(mat4(), m)
|
|
190
|
-
let stride = geometry.layout
|
|
370
|
+
let stride = layoutStride(geometry.layout)
|
|
191
371
|
let src = geometry.vertices
|
|
192
372
|
if (src.length % stride !== 0) {
|
|
193
|
-
throw new Error("transformGeometry: vertex data is not a whole number of " + (geometry.layout
|
|
373
|
+
throw new Error("transformGeometry: vertex data is not a whole number of " + layoutKey(geometry.layout) + " vertices")
|
|
194
374
|
}
|
|
195
375
|
let out = new Float32Array(src)
|
|
196
376
|
for (let i = 0; i < out.length; i += stride) {
|
|
@@ -227,16 +407,17 @@ export function transformGeometry(geometry: Geometry, transform: TransformUpdate
|
|
|
227
407
|
*/
|
|
228
408
|
export function mergeGeometries(parts: Geometry[], label?: string): Geometry {
|
|
229
409
|
if (parts.length === 0) throw new Error("mergeGeometries: no parts")
|
|
230
|
-
let layout = parts[0]!.layout
|
|
231
|
-
let
|
|
410
|
+
let layout = parts[0]!.layout
|
|
411
|
+
let key = layoutKey(layout)
|
|
412
|
+
let stride = layoutStride(layout)
|
|
232
413
|
let floats = 0
|
|
233
414
|
let indexCount = 0
|
|
234
415
|
for (let part of parts) {
|
|
235
|
-
if ((part.layout
|
|
236
|
-
throw new Error("mergeGeometries: mixed layouts (" +
|
|
416
|
+
if (layoutKey(part.layout) !== key) {
|
|
417
|
+
throw new Error("mergeGeometries: mixed layouts (" + key + " and " + layoutKey(part.layout) + ")")
|
|
237
418
|
}
|
|
238
419
|
if (part.vertices.length % stride !== 0) {
|
|
239
|
-
throw new Error("mergeGeometries: a part's vertex data is not a whole number of " +
|
|
420
|
+
throw new Error("mergeGeometries: a part's vertex data is not a whole number of " + key + " vertices")
|
|
240
421
|
}
|
|
241
422
|
floats += part.vertices.length
|
|
242
423
|
indexCount += part.indices.length
|
|
@@ -254,7 +435,7 @@ export function mergeGeometries(parts: Geometry[], label?: string): Geometry {
|
|
|
254
435
|
vOffset += part.vertices.length
|
|
255
436
|
iOffset += src.length
|
|
256
437
|
}
|
|
257
|
-
return { vertices, indices, layout
|
|
438
|
+
return { vertices, indices, layout, label }
|
|
258
439
|
}
|
|
259
440
|
|
|
260
441
|
// Indices for a row-major (cellRows + 1) x (cellCols + 1) vertex grid: two
|
|
@@ -278,7 +459,10 @@ function gridIndices(cellRows: number, cellCols: number, skipFirst = false, skip
|
|
|
278
459
|
}
|
|
279
460
|
|
|
280
461
|
/** An axis-aligned box centered on the origin: 24 vertices, 36 indices. */
|
|
281
|
-
export
|
|
462
|
+
export type BoxOptions = GeometryOptions & { width?: number; height?: number; depth?: number }
|
|
463
|
+
|
|
464
|
+
export function box(options: BoxOptions = {}): Geometry {
|
|
465
|
+
let { width = 1, height = 1, depth = 1 } = options
|
|
282
466
|
let x = width / 2
|
|
283
467
|
let y = height / 2
|
|
284
468
|
let z = depth / 2
|
|
@@ -287,7 +471,7 @@ export function box(width = 1, height = 1, depth = 1, label?: string): Geometry
|
|
|
287
471
|
type P = [number, number, number]
|
|
288
472
|
// Corners a (bottom-left) through d (top-left), CCW seen from outside.
|
|
289
473
|
let quad = (a: P, b: P, c: P, d: P, n: P) => {
|
|
290
|
-
let base = verts.length /
|
|
474
|
+
let base = verts.length / STANDARD_FLOATS
|
|
291
475
|
let uv = [[0, 1], [1, 1], [1, 0], [0, 0]]
|
|
292
476
|
let corners = [a, b, c, d]
|
|
293
477
|
for (let i = 0; i < 4; i++) {
|
|
@@ -303,24 +487,27 @@ export function box(width = 1, height = 1, depth = 1, label?: string): Geometry
|
|
|
303
487
|
quad([-x, -y, -z], [-x, -y, z], [-x, y, z], [-x, y, -z], [-1, 0, 0]) // left
|
|
304
488
|
quad([-x, y, z], [x, y, z], [x, y, -z], [-x, y, -z], [0, 1, 0]) // top
|
|
305
489
|
quad([-x, -y, -z], [x, -y, -z], [x, -y, z], [-x, -y, z], [0, -1, 0]) // bottom
|
|
306
|
-
return
|
|
490
|
+
return packGeometry(verts, indices, options)
|
|
307
491
|
}
|
|
308
492
|
|
|
309
493
|
/**
|
|
310
494
|
* A rectangle in the XY plane facing +z, centered on the origin. For a
|
|
311
495
|
* ground plane, rotate it flat: `rotation={[-Math.PI / 2, 0, 0]}`.
|
|
312
496
|
*/
|
|
313
|
-
export
|
|
497
|
+
export type PlaneOptions = GeometryOptions & { width?: number; height?: number }
|
|
498
|
+
|
|
499
|
+
export function plane(options: PlaneOptions = {}): Geometry {
|
|
500
|
+
let { width = 1, height = 1 } = options
|
|
314
501
|
let x = width / 2
|
|
315
502
|
let y = height / 2
|
|
316
503
|
// prettier-ignore
|
|
317
|
-
let vertices =
|
|
504
|
+
let vertices = [
|
|
318
505
|
-x, -y, 0, 0, 0, 1, 0, 1,
|
|
319
506
|
x, -y, 0, 0, 0, 1, 1, 1,
|
|
320
507
|
x, y, 0, 0, 0, 1, 1, 0,
|
|
321
508
|
-x, y, 0, 0, 0, 1, 0, 0,
|
|
322
|
-
]
|
|
323
|
-
return
|
|
509
|
+
]
|
|
510
|
+
return packGeometry(vertices, [0, 1, 2, 0, 2, 3], options)
|
|
324
511
|
}
|
|
325
512
|
|
|
326
513
|
/**
|
|
@@ -333,15 +520,17 @@ export function plane(width = 1, height = 1, label?: string): Geometry {
|
|
|
333
520
|
* coordinates, so genuinely distinct vertices). UVs: u 0..1 along the knot,
|
|
334
521
|
* v 0..1 around the tube.
|
|
335
522
|
*/
|
|
336
|
-
export
|
|
337
|
-
radius
|
|
338
|
-
tube
|
|
339
|
-
tubularSegments
|
|
340
|
-
radialSegments
|
|
341
|
-
p
|
|
342
|
-
q
|
|
343
|
-
|
|
344
|
-
|
|
523
|
+
export type TorusKnotOptions = GeometryOptions & {
|
|
524
|
+
radius?: number
|
|
525
|
+
tube?: number
|
|
526
|
+
tubularSegments?: number
|
|
527
|
+
radialSegments?: number
|
|
528
|
+
p?: number
|
|
529
|
+
q?: number
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function torusKnot(options: TorusKnotOptions = {}): Geometry {
|
|
533
|
+
let { radius = 1, tube = 0.4, tubularSegments = 64, radialSegments = 8, p = 2, q = 3 } = options
|
|
345
534
|
// A point on the knot curve at parameter t (0..2*PI*p).
|
|
346
535
|
let point = (t: number): Vec3 => {
|
|
347
536
|
let qp = (q / p) * t
|
|
@@ -351,7 +540,8 @@ export function torusKnot(
|
|
|
351
540
|
|
|
352
541
|
let rows = tubularSegments + 1
|
|
353
542
|
let cols = radialSegments + 1
|
|
354
|
-
let
|
|
543
|
+
let stride = generatorStride(options)
|
|
544
|
+
let vertices = new Float32Array(rows * cols * stride)
|
|
355
545
|
let at = 0
|
|
356
546
|
|
|
357
547
|
for (let i = 0; i < rows; i++) {
|
|
@@ -381,13 +571,11 @@ export function torusKnot(
|
|
|
381
571
|
vertices[at + 5] = n[2]
|
|
382
572
|
vertices[at + 6] = i / tubularSegments
|
|
383
573
|
vertices[at + 7] = j / radialSegments
|
|
384
|
-
at +=
|
|
574
|
+
at += stride
|
|
385
575
|
}
|
|
386
576
|
}
|
|
387
577
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return { vertices, indices, label }
|
|
578
|
+
return finishGeometry(vertices, gridIndices(tubularSegments, radialSegments), options)
|
|
391
579
|
}
|
|
392
580
|
|
|
393
581
|
/**
|
|
@@ -398,13 +586,15 @@ export function torusKnot(
|
|
|
398
586
|
* disc map. A zero radius skips that cap and the degenerate side
|
|
399
587
|
* triangles at the apex.
|
|
400
588
|
*/
|
|
401
|
-
export
|
|
402
|
-
radiusTop
|
|
403
|
-
radiusBottom
|
|
404
|
-
height
|
|
405
|
-
radialSegments
|
|
406
|
-
|
|
407
|
-
|
|
589
|
+
export type CylinderOptions = GeometryOptions & {
|
|
590
|
+
radiusTop?: number
|
|
591
|
+
radiusBottom?: number
|
|
592
|
+
height?: number
|
|
593
|
+
radialSegments?: number
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function cylinder(options: CylinderOptions = {}): Geometry {
|
|
597
|
+
let { radiusTop = 0.5, radiusBottom = 0.5, height = 1, radialSegments = 24 } = options
|
|
408
598
|
let h = height / 2
|
|
409
599
|
let cols = radialSegments + 1
|
|
410
600
|
let verts: number[] = []
|
|
@@ -429,7 +619,7 @@ export function cylinder(
|
|
|
429
619
|
// Caps fan around a center vertex; the planar UV map has no seam, so the
|
|
430
620
|
// ring wraps with modulo instead of duplicating a column.
|
|
431
621
|
let cap = (r: number, y: number, up: number) => {
|
|
432
|
-
let base = verts.length /
|
|
622
|
+
let base = verts.length / STANDARD_FLOATS
|
|
433
623
|
verts.push(0, y, 0, 0, up, 0, 0.5, 0.5)
|
|
434
624
|
for (let i = 0; i < radialSegments; i++) {
|
|
435
625
|
let phi = (i / radialSegments) * Math.PI * 2
|
|
@@ -445,34 +635,40 @@ export function cylinder(
|
|
|
445
635
|
}
|
|
446
636
|
if (radiusTop > 0) cap(radiusTop, h, 1)
|
|
447
637
|
if (radiusBottom > 0) cap(radiusBottom, -h, -1)
|
|
448
|
-
return
|
|
638
|
+
return packGeometry(verts, indices, options)
|
|
449
639
|
}
|
|
450
640
|
|
|
451
641
|
/** A capped cone on the y axis, centered on the origin: `cylinder()` with
|
|
452
642
|
* a zero top radius (each apex vertex carries its column's side normal, so
|
|
453
643
|
* the surface shades smoothly around). */
|
|
454
|
-
export
|
|
455
|
-
|
|
644
|
+
export type ConeOptions = GeometryOptions & { radius?: number; height?: number; radialSegments?: number }
|
|
645
|
+
|
|
646
|
+
export function cone(options: ConeOptions = {}): Geometry {
|
|
647
|
+
let { radius = 0.5, height = 1, radialSegments = 24, ...rest } = options
|
|
648
|
+
return cylinder({ ...rest, radiusTop: 0, radiusBottom: radius, height, radialSegments })
|
|
456
649
|
}
|
|
457
650
|
|
|
458
651
|
/**
|
|
459
652
|
* A torus lying flat, centered on the origin: the ring lies in the XZ
|
|
460
653
|
* plane with the hole on the y axis - the y-up orientation torusKnot also
|
|
461
|
-
* uses (Three's equivalent stands in XY).
|
|
654
|
+
* uses (Three's equivalent stands in XY). Option names are Three's:
|
|
462
655
|
* radialSegments subdivides the tube cross-section, tubularSegments the
|
|
463
656
|
* ring. UVs: u 0..1 around the ring, v 0..1 around the tube, seam
|
|
464
657
|
* row/column duplicated like torusKnot.
|
|
465
658
|
*/
|
|
466
|
-
export
|
|
467
|
-
radius
|
|
468
|
-
tube
|
|
469
|
-
radialSegments
|
|
470
|
-
tubularSegments
|
|
471
|
-
|
|
472
|
-
|
|
659
|
+
export type TorusOptions = GeometryOptions & {
|
|
660
|
+
radius?: number
|
|
661
|
+
tube?: number
|
|
662
|
+
radialSegments?: number
|
|
663
|
+
tubularSegments?: number
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function torus(options: TorusOptions = {}): Geometry {
|
|
667
|
+
let { radius = 0.5, tube = 0.2, radialSegments = 12, tubularSegments = 32 } = options
|
|
473
668
|
let rows = tubularSegments + 1
|
|
474
669
|
let cols = radialSegments + 1
|
|
475
|
-
let
|
|
670
|
+
let stride = generatorStride(options)
|
|
671
|
+
let vertices = new Float32Array(rows * cols * stride)
|
|
476
672
|
let at = 0
|
|
477
673
|
for (let i = 0; i < rows; i++) {
|
|
478
674
|
let phi = (i / tubularSegments) * Math.PI * 2
|
|
@@ -491,11 +687,10 @@ export function torus(
|
|
|
491
687
|
vertices[at + 5] = cp * dz
|
|
492
688
|
vertices[at + 6] = i / tubularSegments
|
|
493
689
|
vertices[at + 7] = j / radialSegments
|
|
494
|
-
at +=
|
|
690
|
+
at += stride
|
|
495
691
|
}
|
|
496
692
|
}
|
|
497
|
-
|
|
498
|
-
return { vertices, indices, label }
|
|
693
|
+
return finishGeometry(vertices, gridIndices(tubularSegments, radialSegments), options)
|
|
499
694
|
}
|
|
500
695
|
|
|
501
696
|
/**
|
|
@@ -503,7 +698,10 @@ export function torus(
|
|
|
503
698
|
* like plane()). UVs are the planar map of the disc inscribed in the unit
|
|
504
699
|
* square.
|
|
505
700
|
*/
|
|
506
|
-
export
|
|
701
|
+
export type CircleOptions = GeometryOptions & { radius?: number; segments?: number }
|
|
702
|
+
|
|
703
|
+
export function circle(options: CircleOptions = {}): Geometry {
|
|
704
|
+
let { radius = 0.5, segments = 32 } = options
|
|
507
705
|
let verts: number[] = [0, 0, 0, 0, 0, 1, 0.5, 0.5]
|
|
508
706
|
let indices: number[] = []
|
|
509
707
|
for (let i = 0; i < segments; i++) {
|
|
@@ -515,7 +713,7 @@ export function circle(radius = 0.5, segments = 32, label?: string): Geometry {
|
|
|
515
713
|
for (let i = 0; i < segments; i++) {
|
|
516
714
|
indices.push(0, 1 + i, 1 + ((i + 1) % segments))
|
|
517
715
|
}
|
|
518
|
-
return
|
|
716
|
+
return packGeometry(verts, indices, options)
|
|
519
717
|
}
|
|
520
718
|
|
|
521
719
|
/**
|
|
@@ -523,7 +721,10 @@ export function circle(radius = 0.5, segments = 32, label?: string): Geometry {
|
|
|
523
721
|
* are the planar map of the OUTER disc, so a ring textures like the
|
|
524
722
|
* matching circle() with the middle cut out.
|
|
525
723
|
*/
|
|
526
|
-
export
|
|
724
|
+
export type RingOptions = GeometryOptions & { innerRadius?: number; outerRadius?: number; segments?: number }
|
|
725
|
+
|
|
726
|
+
export function ring(options: RingOptions = {}): Geometry {
|
|
727
|
+
let { innerRadius = 0.25, outerRadius = 0.5, segments = 32 } = options
|
|
527
728
|
let verts: number[] = []
|
|
528
729
|
let indices: number[] = []
|
|
529
730
|
for (let i = 0; i < segments; i++) {
|
|
@@ -538,11 +739,14 @@ export function ring(innerRadius = 0.25, outerRadius = 0.5, segments = 32, label
|
|
|
538
739
|
let j = (i + 1) % segments
|
|
539
740
|
indices.push(i * 2, i * 2 + 1, j * 2 + 1, i * 2, j * 2 + 1, j * 2)
|
|
540
741
|
}
|
|
541
|
-
return
|
|
742
|
+
return packGeometry(verts, indices, options)
|
|
542
743
|
}
|
|
543
744
|
|
|
544
745
|
/** A UV sphere centered on the origin (poles on the y axis). */
|
|
545
|
-
export
|
|
746
|
+
export type SphereOptions = GeometryOptions & { radius?: number; widthSegments?: number; heightSegments?: number }
|
|
747
|
+
|
|
748
|
+
export function sphere(options: SphereOptions = {}): Geometry {
|
|
749
|
+
let { radius = 0.5, widthSegments = 24, heightSegments = 16 } = options
|
|
546
750
|
let verts: number[] = []
|
|
547
751
|
for (let iy = 0; iy <= heightSegments; iy++) {
|
|
548
752
|
let v = iy / heightSegments
|
|
@@ -560,5 +764,5 @@ export function sphere(radius = 0.5, widthSegments = 24, heightSegments = 16, la
|
|
|
560
764
|
}
|
|
561
765
|
// Both pole rows are collapsed to the pole point.
|
|
562
766
|
let indices = gridIndices(heightSegments, widthSegments, true, true)
|
|
563
|
-
return
|
|
767
|
+
return packGeometry(verts, indices, options)
|
|
564
768
|
}
|