@solidrt/3d 0.0.50 → 0.0.52
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 +393 -87
- package/README.md +20 -10
- package/demos/README.md +15 -0
- package/demos/assets/icon.svg +23 -0
- package/demos/package.json +9 -0
- package/demos/src/the-third-dimension.tsx +866 -0
- package/demos/tsconfig.json +15 -0
- package/examples/README.md +34 -2
- package/examples/aim.tsx +5 -5
- package/examples/instanced.tsx +158 -0
- 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 +5 -3
- package/src/components.tsx +171 -3
- package/src/geometry-gpu.ts +97 -0
- package/src/geometry.ts +413 -162
- package/src/glsl.ts +83 -3
- package/src/gltf.ts +437 -0
- package/src/index.ts +18 -11
- package/src/material.ts +373 -47
- package/src/math.ts +114 -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 +1270 -281
- package/src/sweep.ts +21 -36
- 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
|
|
@@ -12,18 +17,18 @@
|
|
|
12
17
|
// unused by the unlit materials so the layout is ready for lights without
|
|
13
18
|
// a geometry change (inactive attributes are skipped but keep the stride).
|
|
14
19
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// owner-scoped auto-free would free a buffer other scenes still draw from.
|
|
19
|
-
// disposeGeometry frees them when an app is done with a geometry for good.
|
|
20
|
+
// Pure module by design - geometry is data, and every function here is
|
|
21
|
+
// array math (the check rig checks/geometry-check.ts runs it headless on
|
|
22
|
+
// flux). The GPU buffer step lives in geometry-gpu.ts.
|
|
20
23
|
|
|
21
|
-
import {
|
|
22
|
-
import
|
|
23
|
-
import {
|
|
24
|
-
import type { Vec2, Vec3, Vec4 } from "./math.ts"
|
|
24
|
+
import type { VertexAttribute } from "@solidrt/core/gpu"
|
|
25
|
+
import { add, compose, cross, mat4, normalize, normalMatrix, sub, updateRotation, updateScale } from "./math.ts"
|
|
26
|
+
import type { Quat, TransformUpdate, Vec2, Vec3 } from "./math.ts"
|
|
25
27
|
|
|
26
|
-
|
|
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[]
|
|
27
32
|
|
|
28
33
|
const STANDARD_ATTRIBUTES: VertexAttribute[] = [
|
|
29
34
|
{ name: "aPos", format: "vec3" },
|
|
@@ -31,17 +36,146 @@ const STANDARD_ATTRIBUTES: VertexAttribute[] = [
|
|
|
31
36
|
{ name: "aUV", format: "vec2" },
|
|
32
37
|
]
|
|
33
38
|
|
|
34
|
-
/** The
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
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[]> = {
|
|
38
42
|
standard: STANDARD_ATTRIBUTES,
|
|
39
43
|
colored: [...STANDARD_ATTRIBUTES, { name: "aColor", format: "vec4" }],
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
/** Floats per vertex in the "standard" layout (
|
|
43
|
-
|
|
44
|
-
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
|
+
}
|
|
45
179
|
|
|
46
180
|
/** Uint16 indices when they fit, Uint32Array past 64k vertices - the draw
|
|
47
181
|
* entry follows the array type. The tail of every unbounded generator. */
|
|
@@ -61,8 +195,6 @@ export type Geometry = {
|
|
|
61
195
|
layout?: VertexLayout
|
|
62
196
|
/** Debug name for the lazily-created GPU buffers. */
|
|
63
197
|
label?: string
|
|
64
|
-
_buffer?: BufferId
|
|
65
|
-
_index?: BufferId
|
|
66
198
|
_bounds?: Float32Array
|
|
67
199
|
}
|
|
68
200
|
|
|
@@ -78,7 +210,7 @@ export function geometryBounds(geometry: Geometry): Float32Array {
|
|
|
78
210
|
if (bounds === undefined) {
|
|
79
211
|
bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
|
|
80
212
|
let v = geometry.vertices
|
|
81
|
-
let stride = geometry.layout
|
|
213
|
+
let stride = layoutStride(geometry.layout)
|
|
82
214
|
for (let i = 0; i + 2 < v.length; i += stride) {
|
|
83
215
|
let x = v[i]!, y = v[i + 1]!, z = v[i + 2]!
|
|
84
216
|
if (x < bounds[0]!) bounds[0] = x
|
|
@@ -94,120 +226,216 @@ export function geometryBounds(geometry: Geometry): Float32Array {
|
|
|
94
226
|
return bounds
|
|
95
227
|
}
|
|
96
228
|
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} {
|
|
104
|
-
let buffer = geometry._buffer
|
|
105
|
-
let index = geometry._index
|
|
106
|
-
if (buffer === undefined || index === undefined) {
|
|
107
|
-
buffer = createBuffer(geometry.vertices, {
|
|
108
|
-
autoFree: false,
|
|
109
|
-
label: geometry.label ? geometry.label + "-verts" : undefined,
|
|
110
|
-
})
|
|
111
|
-
index = createBuffer(geometry.indices, {
|
|
112
|
-
autoFree: false,
|
|
113
|
-
label: geometry.label ? geometry.label + "-indices" : undefined,
|
|
114
|
-
})
|
|
115
|
-
geometry._buffer = buffer
|
|
116
|
-
geometry._index = index
|
|
117
|
-
}
|
|
118
|
-
return { buffer, index, indexFormat: geometry.indices instanceof Uint32Array ? "uint32" : "uint16" }
|
|
119
|
-
}
|
|
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
|
|
120
235
|
|
|
121
236
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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.
|
|
125
244
|
*/
|
|
126
|
-
export function
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
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 + "'")
|
|
249
|
+
}
|
|
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")
|
|
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
|
|
258
|
+
let src = geometry.vertices
|
|
259
|
+
let out = new Float32Array(count * stride)
|
|
260
|
+
for (let i = 0; i < count; i++) {
|
|
261
|
+
let s = i * srcStride
|
|
262
|
+
let d = i * stride
|
|
263
|
+
for (let k = 0; k < srcStride; k++) out[d + k] = src[s + k]!
|
|
264
|
+
}
|
|
265
|
+
fillSlot(out, layout, attr.name, fill, 0)
|
|
266
|
+
return {
|
|
267
|
+
vertices: out,
|
|
268
|
+
indices: geometry.indices,
|
|
269
|
+
layout,
|
|
270
|
+
label: label ?? (geometry.label ? geometry.label + "-" + attr.name : undefined),
|
|
271
|
+
}
|
|
131
272
|
}
|
|
132
273
|
|
|
133
|
-
/** Per-vertex aColor values for withColors/fillColors: a flat 4-per-vertex
|
|
134
|
-
* array, or a callback deriving each vertex's vec4 from the vertex data. */
|
|
135
|
-
export type ColorFill = ArrayLike<number> | ((index: number, pos: Vec3, normal: Vec3, uv: Vec2) => Vec4)
|
|
136
|
-
|
|
137
274
|
/**
|
|
138
275
|
* Derive a "colored"-layout geometry from a standard one: the same
|
|
139
276
|
* positions, normals, uvs and indices, plus an aColor vec4 per vertex -
|
|
140
277
|
* the data channel for materials whose vertex stage reads `in vec4 aColor`
|
|
141
278
|
* (a tint, baked ambient occlusion, any four scalars; the name is the
|
|
142
|
-
* standard vocabulary, the contents are yours).
|
|
143
|
-
*
|
|
144
|
-
* geometry is untouched and its GPU buffers stay independent.
|
|
279
|
+
* standard vocabulary, the contents are yours). `withAttribute` with the
|
|
280
|
+
* aColor channel; the "colored" preset name is kept on the result.
|
|
145
281
|
*/
|
|
146
282
|
export function withColors(geometry: Geometry, fill: ColorFill, label?: string): Geometry {
|
|
147
|
-
if (geometry.layout
|
|
283
|
+
if (layoutSlot(geometry.layout, "aColor") !== null) {
|
|
148
284
|
throw new Error("withColors: geometry already carries an aColor channel")
|
|
149
285
|
}
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
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`.
|
|
300
|
+
*/
|
|
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")
|
|
315
|
+
}
|
|
316
|
+
let total = vertices.length / stride
|
|
317
|
+
let n = count ?? total - first
|
|
318
|
+
if (!Number.isInteger(first) || !Number.isInteger(n) || first < 0 || n < 0 || first + n > total) {
|
|
319
|
+
throw new Error("fillAttribute: range [" + first + ", " + (first + n) + ") is outside the buffer's " + total + " vertices")
|
|
320
|
+
}
|
|
321
|
+
let size = slot.size
|
|
322
|
+
let fn = typeof fill === "function" ? fill : null
|
|
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 + ")")
|
|
152
326
|
}
|
|
153
|
-
let
|
|
154
|
-
|
|
155
|
-
|
|
327
|
+
for (let i = 0; i < n; i++) {
|
|
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]!
|
|
156
342
|
}
|
|
343
|
+
return vertices
|
|
344
|
+
}
|
|
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
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Bake a placement (the setTransform shape: Euler XYZ radians or a
|
|
354
|
+
* quaternion, not both; number = uniform scale; absent = identity) into a
|
|
355
|
+
* geometry: a new geometry (the source is
|
|
356
|
+
* untouched, its GPU buffers stay independent) whose positions are moved
|
|
357
|
+
* by the transform and whose normals follow through the inverse-transpose,
|
|
358
|
+
* renormalized - correct under non-uniform scale. UVs, colors, indices and
|
|
359
|
+
* layout copy through. This is Three's `geometry.applyMatrix4`, the first
|
|
360
|
+
* half of authoring a static scene as data: transform each part into place,
|
|
361
|
+
* mergeGeometries the parts, draw one mesh.
|
|
362
|
+
*/
|
|
363
|
+
export function transformGeometry(geometry: Geometry, transform: TransformUpdate, label?: string): Geometry {
|
|
364
|
+
let rot: Quat = [0, 0, 0, 1]
|
|
365
|
+
updateRotation(rot, transform, "transformGeometry")
|
|
366
|
+
let scl: Vec3 = [1, 1, 1]
|
|
367
|
+
if (transform.scale !== undefined) updateScale(scl, transform.scale)
|
|
368
|
+
let m = compose(mat4(), transform.position ?? [0, 0, 0], rot, scl)
|
|
369
|
+
let n = normalMatrix(mat4(), m)
|
|
370
|
+
let stride = layoutStride(geometry.layout)
|
|
157
371
|
let src = geometry.vertices
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
372
|
+
if (src.length % stride !== 0) {
|
|
373
|
+
throw new Error("transformGeometry: vertex data is not a whole number of " + layoutKey(geometry.layout) + " vertices")
|
|
374
|
+
}
|
|
375
|
+
let out = new Float32Array(src)
|
|
376
|
+
for (let i = 0; i < out.length; i += stride) {
|
|
377
|
+
let x = src[i]!, y = src[i + 1]!, z = src[i + 2]!
|
|
378
|
+
out[i] = m[0] * x + m[4] * y + m[8] * z + m[12]
|
|
379
|
+
out[i + 1] = m[1] * x + m[5] * y + m[9] * z + m[13]
|
|
380
|
+
out[i + 2] = m[2] * x + m[6] * y + m[10] * z + m[14]
|
|
381
|
+
let nx = src[i + 3]!, ny = src[i + 4]!, nz = src[i + 5]!
|
|
382
|
+
let tx = n[0] * nx + n[4] * ny + n[8] * nz
|
|
383
|
+
let ty = n[1] * nx + n[5] * ny + n[9] * nz
|
|
384
|
+
let tz = n[2] * nx + n[6] * ny + n[10] * nz
|
|
385
|
+
let len = Math.hypot(tx, ty, tz) || 1
|
|
386
|
+
out[i + 3] = tx / len
|
|
387
|
+
out[i + 4] = ty / len
|
|
388
|
+
out[i + 5] = tz / len
|
|
163
389
|
}
|
|
164
|
-
fillColors(out, fill)
|
|
165
390
|
return {
|
|
166
391
|
vertices: out,
|
|
167
392
|
indices: geometry.indices,
|
|
168
|
-
layout:
|
|
169
|
-
label: label ?? (geometry.label ? geometry.label + "-
|
|
393
|
+
layout: geometry.layout,
|
|
394
|
+
label: label ?? (geometry.label ? geometry.label + "-transformed" : undefined),
|
|
170
395
|
}
|
|
171
396
|
}
|
|
172
397
|
|
|
173
398
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* This trusts the buffer to BE colored-layout data - a bare array carries
|
|
184
|
-
* no layout tag, so only the arithmetic is checked. The Geometry-level
|
|
185
|
-
* withColors stays the checked path.
|
|
399
|
+
* Concatenate geometries into one: vertices appended in order, indices
|
|
400
|
+
* offset to match, uint32 indices past 64k vertices. Every part must share
|
|
401
|
+
* one layout - a mixed list throws, because the strides differ and a merge
|
|
402
|
+
* that picked one would draw garbage, not a mesh missing a channel. The
|
|
403
|
+
* second half of authoring a static scene as data (Three's
|
|
404
|
+
* `BufferGeometryUtils.mergeGeometries`): the result is one draw entry and
|
|
405
|
+
* one uModel write however many parts went in, so only what actually moves
|
|
406
|
+
* keeps a node of its own.
|
|
186
407
|
*/
|
|
187
|
-
export function
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
let
|
|
192
|
-
let
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
408
|
+
export function mergeGeometries(parts: Geometry[], label?: string): Geometry {
|
|
409
|
+
if (parts.length === 0) throw new Error("mergeGeometries: no parts")
|
|
410
|
+
let layout = parts[0]!.layout
|
|
411
|
+
let key = layoutKey(layout)
|
|
412
|
+
let stride = layoutStride(layout)
|
|
413
|
+
let floats = 0
|
|
414
|
+
let indexCount = 0
|
|
415
|
+
for (let part of parts) {
|
|
416
|
+
if (layoutKey(part.layout) !== key) {
|
|
417
|
+
throw new Error("mergeGeometries: mixed layouts (" + key + " and " + layoutKey(part.layout) + ")")
|
|
418
|
+
}
|
|
419
|
+
if (part.vertices.length % stride !== 0) {
|
|
420
|
+
throw new Error("mergeGeometries: a part's vertex data is not a whole number of " + key + " vertices")
|
|
421
|
+
}
|
|
422
|
+
floats += part.vertices.length
|
|
423
|
+
indexCount += part.indices.length
|
|
199
424
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
vertices
|
|
207
|
-
|
|
208
|
-
|
|
425
|
+
let vertexCount = floats / stride
|
|
426
|
+
let vertices = new Float32Array(floats)
|
|
427
|
+
let indices = vertexCount > 65535 ? new Uint32Array(indexCount) : new Uint16Array(indexCount)
|
|
428
|
+
let vOffset = 0
|
|
429
|
+
let iOffset = 0
|
|
430
|
+
for (let part of parts) {
|
|
431
|
+
vertices.set(part.vertices, vOffset)
|
|
432
|
+
let base = vOffset / stride
|
|
433
|
+
let src = part.indices
|
|
434
|
+
for (let i = 0; i < src.length; i++) indices[iOffset + i] = src[i]! + base
|
|
435
|
+
vOffset += part.vertices.length
|
|
436
|
+
iOffset += src.length
|
|
209
437
|
}
|
|
210
|
-
return vertices
|
|
438
|
+
return { vertices, indices, layout, label }
|
|
211
439
|
}
|
|
212
440
|
|
|
213
441
|
// Indices for a row-major (cellRows + 1) x (cellCols + 1) vertex grid: two
|
|
@@ -231,7 +459,10 @@ function gridIndices(cellRows: number, cellCols: number, skipFirst = false, skip
|
|
|
231
459
|
}
|
|
232
460
|
|
|
233
461
|
/** An axis-aligned box centered on the origin: 24 vertices, 36 indices. */
|
|
234
|
-
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
|
|
235
466
|
let x = width / 2
|
|
236
467
|
let y = height / 2
|
|
237
468
|
let z = depth / 2
|
|
@@ -240,7 +471,7 @@ export function box(width = 1, height = 1, depth = 1, label?: string): Geometry
|
|
|
240
471
|
type P = [number, number, number]
|
|
241
472
|
// Corners a (bottom-left) through d (top-left), CCW seen from outside.
|
|
242
473
|
let quad = (a: P, b: P, c: P, d: P, n: P) => {
|
|
243
|
-
let base = verts.length /
|
|
474
|
+
let base = verts.length / STANDARD_FLOATS
|
|
244
475
|
let uv = [[0, 1], [1, 1], [1, 0], [0, 0]]
|
|
245
476
|
let corners = [a, b, c, d]
|
|
246
477
|
for (let i = 0; i < 4; i++) {
|
|
@@ -256,24 +487,27 @@ export function box(width = 1, height = 1, depth = 1, label?: string): Geometry
|
|
|
256
487
|
quad([-x, -y, -z], [-x, -y, z], [-x, y, z], [-x, y, -z], [-1, 0, 0]) // left
|
|
257
488
|
quad([-x, y, z], [x, y, z], [x, y, -z], [-x, y, -z], [0, 1, 0]) // top
|
|
258
489
|
quad([-x, -y, -z], [x, -y, -z], [x, -y, z], [-x, -y, z], [0, -1, 0]) // bottom
|
|
259
|
-
return
|
|
490
|
+
return packGeometry(verts, indices, options)
|
|
260
491
|
}
|
|
261
492
|
|
|
262
493
|
/**
|
|
263
494
|
* A rectangle in the XY plane facing +z, centered on the origin. For a
|
|
264
495
|
* ground plane, rotate it flat: `rotation={[-Math.PI / 2, 0, 0]}`.
|
|
265
496
|
*/
|
|
266
|
-
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
|
|
267
501
|
let x = width / 2
|
|
268
502
|
let y = height / 2
|
|
269
503
|
// prettier-ignore
|
|
270
|
-
let vertices =
|
|
504
|
+
let vertices = [
|
|
271
505
|
-x, -y, 0, 0, 0, 1, 0, 1,
|
|
272
506
|
x, -y, 0, 0, 0, 1, 1, 1,
|
|
273
507
|
x, y, 0, 0, 0, 1, 1, 0,
|
|
274
508
|
-x, y, 0, 0, 0, 1, 0, 0,
|
|
275
|
-
]
|
|
276
|
-
return
|
|
509
|
+
]
|
|
510
|
+
return packGeometry(vertices, [0, 1, 2, 0, 2, 3], options)
|
|
277
511
|
}
|
|
278
512
|
|
|
279
513
|
/**
|
|
@@ -286,15 +520,17 @@ export function plane(width = 1, height = 1, label?: string): Geometry {
|
|
|
286
520
|
* coordinates, so genuinely distinct vertices). UVs: u 0..1 along the knot,
|
|
287
521
|
* v 0..1 around the tube.
|
|
288
522
|
*/
|
|
289
|
-
export
|
|
290
|
-
radius
|
|
291
|
-
tube
|
|
292
|
-
tubularSegments
|
|
293
|
-
radialSegments
|
|
294
|
-
p
|
|
295
|
-
q
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
298
534
|
// A point on the knot curve at parameter t (0..2*PI*p).
|
|
299
535
|
let point = (t: number): Vec3 => {
|
|
300
536
|
let qp = (q / p) * t
|
|
@@ -304,7 +540,8 @@ export function torusKnot(
|
|
|
304
540
|
|
|
305
541
|
let rows = tubularSegments + 1
|
|
306
542
|
let cols = radialSegments + 1
|
|
307
|
-
let
|
|
543
|
+
let stride = generatorStride(options)
|
|
544
|
+
let vertices = new Float32Array(rows * cols * stride)
|
|
308
545
|
let at = 0
|
|
309
546
|
|
|
310
547
|
for (let i = 0; i < rows; i++) {
|
|
@@ -334,13 +571,11 @@ export function torusKnot(
|
|
|
334
571
|
vertices[at + 5] = n[2]
|
|
335
572
|
vertices[at + 6] = i / tubularSegments
|
|
336
573
|
vertices[at + 7] = j / radialSegments
|
|
337
|
-
at +=
|
|
574
|
+
at += stride
|
|
338
575
|
}
|
|
339
576
|
}
|
|
340
577
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return { vertices, indices, label }
|
|
578
|
+
return finishGeometry(vertices, gridIndices(tubularSegments, radialSegments), options)
|
|
344
579
|
}
|
|
345
580
|
|
|
346
581
|
/**
|
|
@@ -351,13 +586,15 @@ export function torusKnot(
|
|
|
351
586
|
* disc map. A zero radius skips that cap and the degenerate side
|
|
352
587
|
* triangles at the apex.
|
|
353
588
|
*/
|
|
354
|
-
export
|
|
355
|
-
radiusTop
|
|
356
|
-
radiusBottom
|
|
357
|
-
height
|
|
358
|
-
radialSegments
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
361
598
|
let h = height / 2
|
|
362
599
|
let cols = radialSegments + 1
|
|
363
600
|
let verts: number[] = []
|
|
@@ -382,7 +619,7 @@ export function cylinder(
|
|
|
382
619
|
// Caps fan around a center vertex; the planar UV map has no seam, so the
|
|
383
620
|
// ring wraps with modulo instead of duplicating a column.
|
|
384
621
|
let cap = (r: number, y: number, up: number) => {
|
|
385
|
-
let base = verts.length /
|
|
622
|
+
let base = verts.length / STANDARD_FLOATS
|
|
386
623
|
verts.push(0, y, 0, 0, up, 0, 0.5, 0.5)
|
|
387
624
|
for (let i = 0; i < radialSegments; i++) {
|
|
388
625
|
let phi = (i / radialSegments) * Math.PI * 2
|
|
@@ -398,34 +635,40 @@ export function cylinder(
|
|
|
398
635
|
}
|
|
399
636
|
if (radiusTop > 0) cap(radiusTop, h, 1)
|
|
400
637
|
if (radiusBottom > 0) cap(radiusBottom, -h, -1)
|
|
401
|
-
return
|
|
638
|
+
return packGeometry(verts, indices, options)
|
|
402
639
|
}
|
|
403
640
|
|
|
404
641
|
/** A capped cone on the y axis, centered on the origin: `cylinder()` with
|
|
405
642
|
* a zero top radius (each apex vertex carries its column's side normal, so
|
|
406
643
|
* the surface shades smoothly around). */
|
|
407
|
-
export
|
|
408
|
-
|
|
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 })
|
|
409
649
|
}
|
|
410
650
|
|
|
411
651
|
/**
|
|
412
652
|
* A torus lying flat, centered on the origin: the ring lies in the XZ
|
|
413
653
|
* plane with the hole on the y axis - the y-up orientation torusKnot also
|
|
414
|
-
* uses (Three's equivalent stands in XY).
|
|
654
|
+
* uses (Three's equivalent stands in XY). Option names are Three's:
|
|
415
655
|
* radialSegments subdivides the tube cross-section, tubularSegments the
|
|
416
656
|
* ring. UVs: u 0..1 around the ring, v 0..1 around the tube, seam
|
|
417
657
|
* row/column duplicated like torusKnot.
|
|
418
658
|
*/
|
|
419
|
-
export
|
|
420
|
-
radius
|
|
421
|
-
tube
|
|
422
|
-
radialSegments
|
|
423
|
-
tubularSegments
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
426
668
|
let rows = tubularSegments + 1
|
|
427
669
|
let cols = radialSegments + 1
|
|
428
|
-
let
|
|
670
|
+
let stride = generatorStride(options)
|
|
671
|
+
let vertices = new Float32Array(rows * cols * stride)
|
|
429
672
|
let at = 0
|
|
430
673
|
for (let i = 0; i < rows; i++) {
|
|
431
674
|
let phi = (i / tubularSegments) * Math.PI * 2
|
|
@@ -444,11 +687,10 @@ export function torus(
|
|
|
444
687
|
vertices[at + 5] = cp * dz
|
|
445
688
|
vertices[at + 6] = i / tubularSegments
|
|
446
689
|
vertices[at + 7] = j / radialSegments
|
|
447
|
-
at +=
|
|
690
|
+
at += stride
|
|
448
691
|
}
|
|
449
692
|
}
|
|
450
|
-
|
|
451
|
-
return { vertices, indices, label }
|
|
693
|
+
return finishGeometry(vertices, gridIndices(tubularSegments, radialSegments), options)
|
|
452
694
|
}
|
|
453
695
|
|
|
454
696
|
/**
|
|
@@ -456,7 +698,10 @@ export function torus(
|
|
|
456
698
|
* like plane()). UVs are the planar map of the disc inscribed in the unit
|
|
457
699
|
* square.
|
|
458
700
|
*/
|
|
459
|
-
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
|
|
460
705
|
let verts: number[] = [0, 0, 0, 0, 0, 1, 0.5, 0.5]
|
|
461
706
|
let indices: number[] = []
|
|
462
707
|
for (let i = 0; i < segments; i++) {
|
|
@@ -468,7 +713,7 @@ export function circle(radius = 0.5, segments = 32, label?: string): Geometry {
|
|
|
468
713
|
for (let i = 0; i < segments; i++) {
|
|
469
714
|
indices.push(0, 1 + i, 1 + ((i + 1) % segments))
|
|
470
715
|
}
|
|
471
|
-
return
|
|
716
|
+
return packGeometry(verts, indices, options)
|
|
472
717
|
}
|
|
473
718
|
|
|
474
719
|
/**
|
|
@@ -476,7 +721,10 @@ export function circle(radius = 0.5, segments = 32, label?: string): Geometry {
|
|
|
476
721
|
* are the planar map of the OUTER disc, so a ring textures like the
|
|
477
722
|
* matching circle() with the middle cut out.
|
|
478
723
|
*/
|
|
479
|
-
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
|
|
480
728
|
let verts: number[] = []
|
|
481
729
|
let indices: number[] = []
|
|
482
730
|
for (let i = 0; i < segments; i++) {
|
|
@@ -491,11 +739,14 @@ export function ring(innerRadius = 0.25, outerRadius = 0.5, segments = 32, label
|
|
|
491
739
|
let j = (i + 1) % segments
|
|
492
740
|
indices.push(i * 2, i * 2 + 1, j * 2 + 1, i * 2, j * 2 + 1, j * 2)
|
|
493
741
|
}
|
|
494
|
-
return
|
|
742
|
+
return packGeometry(verts, indices, options)
|
|
495
743
|
}
|
|
496
744
|
|
|
497
745
|
/** A UV sphere centered on the origin (poles on the y axis). */
|
|
498
|
-
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
|
|
499
750
|
let verts: number[] = []
|
|
500
751
|
for (let iy = 0; iy <= heightSegments; iy++) {
|
|
501
752
|
let v = iy / heightSegments
|
|
@@ -513,5 +764,5 @@ export function sphere(radius = 0.5, widthSegments = 24, heightSegments = 16, la
|
|
|
513
764
|
}
|
|
514
765
|
// Both pole rows are collapsed to the pole point.
|
|
515
766
|
let indices = gridIndices(heightSegments, widthSegments, true, true)
|
|
516
|
-
return
|
|
767
|
+
return packGeometry(verts, indices, options)
|
|
517
768
|
}
|