@solidrt/3d 0.0.57 → 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 +1479 -678
- package/README.md +4 -2
- package/examples/README.md +56 -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 +129 -0
- 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 +187 -22
- package/src/geometry.ts +1718 -203
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +457 -154
- package/src/index.ts +32 -21
- package/src/input.ts +11 -2
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +322 -190
- package/src/mesh.ts +492 -259
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +151 -54
- package/src/motion.ts +17 -0
- package/src/node.ts +423 -23
- 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 +608 -185
- package/src/skeleton.ts +211 -0
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/src/geometry.ts
CHANGED
|
@@ -1,69 +1,234 @@
|
|
|
1
1
|
// Geometry: one interleaved vertex buffer described by a layout - an
|
|
2
|
-
// ordered attribute list that
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
2
|
+
// ordered attribute list that starts with the position (float32x3) and
|
|
3
|
+
// may carry any named channels after it, each in one of the vertex formats
|
|
4
|
+
// (the float32 family, halves, normalized integers, and unsigned and
|
|
5
|
+
// signed integers up to 32 bits; see VERTEX_FORMATS). The base prefix (position, normal, uv: 8 floats)
|
|
6
|
+
// is what every generator emits and what the stock materials read; a
|
|
7
|
+
// hand-built layout may leave the normal and uv out when nothing reads
|
|
8
|
+
// them, and may pack a channel (a color in unorm8x4 is 4 bytes, not 16).
|
|
9
|
+
// The layout is open data: `withAttribute` appends any named channel
|
|
10
|
+
// (Three's `setAttribute`), and "colored" names the one common case, the
|
|
11
|
+
// prefix plus an aColor float32x4 as the per-vertex data channel for
|
|
12
|
+
// custom materials (tint, baked AO, any four scalars). Materials read
|
|
13
|
+
// attributes by name and adapt to whatever layout their geometry carries
|
|
14
|
+
// (one pipeline per layout met), so a geometry may carry more channels
|
|
15
|
+
// than a material reads, and a packed channel feeds the same float `in`
|
|
16
|
+
// as a float one; an integer channel (uint*/sint*) feeds an integer `in`
|
|
17
|
+
// (uvec*/ivec*) and nothing else, WebGPU's rule. Indices are uint16 or uint32 (the generators here emit
|
|
18
|
+
// uint16; hand-built geometry past 64k vertices uses a Uint32Array and
|
|
19
|
+
// the draw entry follows the array type). Winding is counter-clockwise
|
|
20
|
+
// seen from outside in the y-up world, which the standard camera rig
|
|
21
|
+
// (perspective() with its baked y flip) presents as the engine's
|
|
22
|
+
// displayed-CCW front faces: every generator here culls correctly with
|
|
23
|
+
// cull: "back". Normals ride along unused by the unlit materials so the
|
|
24
|
+
// layout is ready for lights without a geometry change (inactive
|
|
25
|
+
// attributes are skipped but keep the stride).
|
|
26
|
+
//
|
|
27
|
+
// A geometry may carry more than one vertex buffer: `vertices` under
|
|
28
|
+
// `layout` is stream 0 (where aPos lives), and `streams` holds any number
|
|
29
|
+
// of extra buffers with layouts of their own, every one holding the same
|
|
30
|
+
// vertex count - Unity's vertex streams, Three's one-buffer-per-channel
|
|
31
|
+
// when each stream carries one attribute. A stream is what an app rewrites
|
|
32
|
+
// per frame without touching the rest (updateVertices in geometry-gpu.ts).
|
|
33
|
+
//
|
|
34
|
+
// No function here knows an offset or a stride: vertex data is read and
|
|
35
|
+
// written through `geometryAttribute`, an accessor bound to one attribute
|
|
36
|
+
// that finds the stream carrying it and decodes and encodes through the
|
|
37
|
+
// format's codec.
|
|
19
38
|
//
|
|
20
39
|
// Pure module by design - geometry is data, and every function here is
|
|
21
40
|
// array math (the check rig checks/geometry-check.ts runs it headless on
|
|
22
41
|
// flux). The GPU buffer step lives in geometry-gpu.ts.
|
|
23
42
|
|
|
24
|
-
import type { VertexAttribute } from "@solidrt/core/gpu"
|
|
43
|
+
import type { Topology, VertexAttribute, VertexFormat } from "@solidrt/core/gpu"
|
|
44
|
+
import { premultipliedColor } from "./color.ts"
|
|
25
45
|
import { add, compose, cross, mat4, normalize, normalMatrix, sub, updateRotation, updateScale } from "./math.ts"
|
|
26
46
|
import type { Quat, TransformUpdate, Vec2, Vec3 } from "./math.ts"
|
|
47
|
+
import type { Capsule } from "./scene.ts"
|
|
27
48
|
|
|
28
|
-
/** A vertex layout: the named presets, or an explicit attribute list
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
49
|
+
/** A vertex layout: the named presets, or an explicit attribute list
|
|
50
|
+
* that begins with `aPos` float32x3 (placement is universal) and carries
|
|
51
|
+
* any named channels after it. The base prefix (aPos float32x3,
|
|
52
|
+
* aNormal float32x3, aUV float32x2) is what every generator emits and
|
|
53
|
+
* what the stock materials read, not a rule a layout must follow: a
|
|
54
|
+
* material reads channels by name and a missing one throws at add(), so
|
|
55
|
+
* a point carrying a position and one packed channel is 16 bytes, not
|
|
56
|
+
* 48. Absent on a Geometry means "base". */
|
|
57
|
+
export type VertexLayout = "base" | "colored" | "skinned" | VertexAttribute[]
|
|
32
58
|
|
|
33
|
-
const
|
|
34
|
-
{ name: "aPos", format: "
|
|
35
|
-
{ name: "aNormal", format: "
|
|
36
|
-
{ name: "aUV", format: "
|
|
59
|
+
const BASE_ATTRIBUTES: VertexAttribute[] = [
|
|
60
|
+
{ name: "aPos", format: "float32x3" },
|
|
61
|
+
{ name: "aNormal", format: "float32x3" },
|
|
62
|
+
{ name: "aUV", format: "float32x2" },
|
|
37
63
|
]
|
|
38
64
|
|
|
39
65
|
/** The attribute lists behind the named layouts. Every layout shares the
|
|
40
|
-
*
|
|
41
|
-
export const VERTEX_LAYOUTS: Record<"
|
|
42
|
-
|
|
43
|
-
colored: [...
|
|
44
|
-
// The rigged-model layout: 4 joint indices (
|
|
45
|
-
//
|
|
46
|
-
|
|
66
|
+
* base prefix, so one shader vocabulary serves all of them. */
|
|
67
|
+
export const VERTEX_LAYOUTS: Record<"base" | "colored" | "skinned", VertexAttribute[]> = {
|
|
68
|
+
base: BASE_ATTRIBUTES,
|
|
69
|
+
colored: [...BASE_ATTRIBUTES, { name: "aColor", format: "float32x4" }],
|
|
70
|
+
// The rigged-model layout: 4 joint indices (unsigned bytes, what an
|
|
71
|
+
// exporter writes for a rig under 256 joints; a wider rig lands in list
|
|
72
|
+
// form with uint16x4) and their weights per vertex, what a skinned
|
|
73
|
+
// vertex stage reads (`in uvec4 aJoints`).
|
|
74
|
+
skinned: [...BASE_ATTRIBUTES, { name: "aJoints", format: "uint8x4" }, { name: "aWeights", format: "float32x4" }],
|
|
47
75
|
}
|
|
48
76
|
|
|
49
|
-
/** Floats per vertex in the "
|
|
77
|
+
/** Floats per vertex in the "base" layout (the generators' own write
|
|
50
78
|
* format before packing). */
|
|
51
|
-
export const
|
|
79
|
+
export const BASE_FLOATS = 8
|
|
80
|
+
|
|
81
|
+
/** The shader `in` family a vertex format feeds (WebGPU's rule: the
|
|
82
|
+
* format decides): "float" for the float and normalized formats (`in
|
|
83
|
+
* vec*`), "uint" for uint* (`in uvec*`), "sint" for sint* (`in ivec*`). */
|
|
84
|
+
export type FormatKind = "float" | "uint" | "sint"
|
|
85
|
+
|
|
86
|
+
/** One vertex format's codec: its size, its component count, its kind,
|
|
87
|
+
* and the read and write of component `k` of an attribute at byte `at`
|
|
88
|
+
* of a DataView, in the value the shader sees (a normalized integer
|
|
89
|
+
* decodes to 0..1 / -1..1, an unnormalized one to its exact value).
|
|
90
|
+
* Every format is a multiple of 4 bytes (WebGPU's alignment rule), which
|
|
91
|
+
* is what keeps every offset and stride 4-aligned with no padding
|
|
92
|
+
* arithmetic anywhere. */
|
|
93
|
+
export type FormatCodec = {
|
|
94
|
+
bytes: number
|
|
95
|
+
components: number
|
|
96
|
+
kind: FormatKind
|
|
97
|
+
get(dv: DataView, at: number, k: number): number
|
|
98
|
+
set(dv: DataView, at: number, k: number, v: number): void
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The integer component kinds behind the packed formats: width, the
|
|
102
|
+
// value that maps to 1 (GL ES 3.0's normalization rule, -max mapping to
|
|
103
|
+
// -1 and the one value below it clamped there), and the raw accessors.
|
|
104
|
+
type IntKind = { bytes: number; max: number; get(dv: DataView, at: number): number; set(dv: DataView, at: number, v: number): void }
|
|
105
|
+
const U8: IntKind = { bytes: 1, max: 255, get: (dv, at) => dv.getUint8(at), set: (dv, at, v) => dv.setUint8(at, v) }
|
|
106
|
+
const S8: IntKind = { bytes: 1, max: 127, get: (dv, at) => dv.getInt8(at), set: (dv, at, v) => dv.setInt8(at, v) }
|
|
107
|
+
const U16: IntKind = { bytes: 2, max: 65535, get: (dv, at) => dv.getUint16(at, true), set: (dv, at, v) => dv.setUint16(at, v, true) }
|
|
108
|
+
const S16: IntKind = { bytes: 2, max: 32767, get: (dv, at) => dv.getInt16(at, true), set: (dv, at, v) => dv.setInt16(at, v, true) }
|
|
109
|
+
const U32: IntKind = { bytes: 4, max: 4294967295, get: (dv, at) => dv.getUint32(at, true), set: (dv, at, v) => dv.setUint32(at, v, true) }
|
|
110
|
+
const S32: IntKind = { bytes: 4, max: 2147483647, get: (dv, at) => dv.getInt32(at, true), set: (dv, at, v) => dv.setInt32(at, v, true) }
|
|
111
|
+
|
|
112
|
+
function clamp(v: number, lo: number, hi: number): number {
|
|
113
|
+
return v < lo ? lo : v > hi ? hi : v
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function floatCodec(components: number, bytes: number, get: (dv: DataView, at: number) => number, set: (dv: DataView, at: number, v: number) => void): FormatCodec {
|
|
117
|
+
return {
|
|
118
|
+
bytes: components * bytes,
|
|
119
|
+
components,
|
|
120
|
+
kind: "float",
|
|
121
|
+
get: (dv, at, k) => get(dv, at + k * bytes),
|
|
122
|
+
set: (dv, at, k, v) => set(dv, at + k * bytes, v),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function unormCodec(components: number, int: IntKind): FormatCodec {
|
|
127
|
+
return {
|
|
128
|
+
bytes: components * int.bytes,
|
|
129
|
+
components,
|
|
130
|
+
kind: "float",
|
|
131
|
+
get: (dv, at, k) => int.get(dv, at + k * int.bytes) / int.max,
|
|
132
|
+
set: (dv, at, k, v) => int.set(dv, at + k * int.bytes, Math.round(clamp(v, 0, 1) * int.max)),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function snormCodec(components: number, int: IntKind): FormatCodec {
|
|
137
|
+
return {
|
|
138
|
+
bytes: components * int.bytes,
|
|
139
|
+
components,
|
|
140
|
+
kind: "float",
|
|
141
|
+
get: (dv, at, k) => Math.max(int.get(dv, at + k * int.bytes) / int.max, -1),
|
|
142
|
+
set: (dv, at, k, v) => int.set(dv, at + k * int.bytes, Math.round(clamp(v, -1, 1) * int.max)),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function uintCodec(components: number, int: IntKind): FormatCodec {
|
|
147
|
+
return {
|
|
148
|
+
bytes: components * int.bytes,
|
|
149
|
+
components,
|
|
150
|
+
kind: "uint",
|
|
151
|
+
get: (dv, at, k) => int.get(dv, at + k * int.bytes),
|
|
152
|
+
set: (dv, at, k, v) => int.set(dv, at + k * int.bytes, Math.round(clamp(v, 0, int.max))),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
52
155
|
|
|
53
|
-
|
|
156
|
+
function sintCodec(components: number, int: IntKind): FormatCodec {
|
|
157
|
+
return {
|
|
158
|
+
bytes: components * int.bytes,
|
|
159
|
+
components,
|
|
160
|
+
kind: "sint",
|
|
161
|
+
get: (dv, at, k) => int.get(dv, at + k * int.bytes),
|
|
162
|
+
set: (dv, at, k, v) => int.set(dv, at + k * int.bytes, Math.round(clamp(v, -int.max - 1, int.max))),
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let getF32 = (dv: DataView, at: number) => dv.getFloat32(at, true)
|
|
167
|
+
let setF32 = (dv: DataView, at: number, v: number) => dv.setFloat32(at, v, true)
|
|
168
|
+
let getF16 = (dv: DataView, at: number) => dv.getFloat16(at, true)
|
|
169
|
+
let setF16 = (dv: DataView, at: number, v: number) => dv.setFloat16(at, v, true)
|
|
170
|
+
|
|
171
|
+
/** The vertex vocabulary: one codec per format (the engine's table in
|
|
172
|
+
* the same spelling), so it is also the list a declared format must be
|
|
173
|
+
* one of. A format feeds the shader `in` of its kind and component
|
|
174
|
+
* count (`formatFeeds`). */
|
|
175
|
+
export const VERTEX_FORMATS: Record<VertexFormat, FormatCodec> = {
|
|
176
|
+
float32: floatCodec(1, Float32Array.BYTES_PER_ELEMENT, getF32, setF32),
|
|
177
|
+
float32x2: floatCodec(2, Float32Array.BYTES_PER_ELEMENT, getF32, setF32),
|
|
178
|
+
float32x3: floatCodec(3, Float32Array.BYTES_PER_ELEMENT, getF32, setF32),
|
|
179
|
+
float32x4: floatCodec(4, Float32Array.BYTES_PER_ELEMENT, getF32, setF32),
|
|
180
|
+
float16x2: floatCodec(2, Float16Array.BYTES_PER_ELEMENT, getF16, setF16),
|
|
181
|
+
float16x4: floatCodec(4, Float16Array.BYTES_PER_ELEMENT, getF16, setF16),
|
|
182
|
+
unorm8x4: unormCodec(4, U8),
|
|
183
|
+
snorm8x4: snormCodec(4, S8),
|
|
184
|
+
unorm16x2: unormCodec(2, U16),
|
|
185
|
+
unorm16x4: unormCodec(4, U16),
|
|
186
|
+
snorm16x2: snormCodec(2, S16),
|
|
187
|
+
snorm16x4: snormCodec(4, S16),
|
|
188
|
+
uint8x4: uintCodec(4, U8),
|
|
189
|
+
uint16x2: uintCodec(2, U16),
|
|
190
|
+
uint16x4: uintCodec(4, U16),
|
|
191
|
+
uint32: uintCodec(1, U32),
|
|
192
|
+
uint32x2: uintCodec(2, U32),
|
|
193
|
+
uint32x3: uintCodec(3, U32),
|
|
194
|
+
uint32x4: uintCodec(4, U32),
|
|
195
|
+
sint8x4: sintCodec(4, S8),
|
|
196
|
+
sint16x2: sintCodec(2, S16),
|
|
197
|
+
sint16x4: sintCodec(4, S16),
|
|
198
|
+
sint32: sintCodec(1, S32),
|
|
199
|
+
sint32x2: sintCodec(2, S32),
|
|
200
|
+
sint32x3: sintCodec(3, S32),
|
|
201
|
+
sint32x4: sintCodec(4, S32),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Whether a layout declaring `declared` may feed a program input the
|
|
205
|
+
* engine reflects as `reflected` (float32xN for `vec*`, uint32xN for
|
|
206
|
+
* `uvec*`, sint32xN for `ivec*`): same component count and same kind,
|
|
207
|
+
* the engine's own pipeline rule. */
|
|
208
|
+
export function formatFeeds(declared: VertexFormat, reflected: VertexFormat): boolean {
|
|
209
|
+
let d = VERTEX_FORMATS[declared]
|
|
210
|
+
let r = VERTEX_FORMATS[reflected]
|
|
211
|
+
return d.components === r.components && d.kind === r.kind
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Whether a format is one of the float32 family: what a Float32Array
|
|
215
|
+
* holds directly, and what the generators write. */
|
|
216
|
+
export function isFloatFormat(format: VertexFormat): boolean {
|
|
217
|
+
return format.startsWith("float32")
|
|
218
|
+
}
|
|
54
219
|
|
|
55
220
|
/** The attribute list of a layout (a preset name resolves to its list). */
|
|
56
221
|
export function layoutAttributes(layout?: VertexLayout): VertexAttribute[] {
|
|
57
|
-
if (layout === undefined || layout === "
|
|
222
|
+
if (layout === undefined || layout === "base") return VERTEX_LAYOUTS.base
|
|
58
223
|
if (layout === "colored") return VERTEX_LAYOUTS.colored
|
|
59
224
|
if (layout === "skinned") return VERTEX_LAYOUTS.skinned
|
|
60
225
|
return layout
|
|
61
226
|
}
|
|
62
227
|
|
|
63
|
-
/**
|
|
228
|
+
/** Bytes per vertex of a layout - its interleave stride. */
|
|
64
229
|
export function layoutStride(layout?: VertexLayout): number {
|
|
65
230
|
let stride = 0
|
|
66
|
-
for (let attr of layoutAttributes(layout)) stride +=
|
|
231
|
+
for (let attr of layoutAttributes(layout)) stride += VERTEX_FORMATS[attr.format].bytes
|
|
67
232
|
return stride
|
|
68
233
|
}
|
|
69
234
|
|
|
@@ -75,27 +240,31 @@ export function layoutKey(layout?: VertexLayout): string {
|
|
|
75
240
|
.join(",")
|
|
76
241
|
}
|
|
77
242
|
|
|
78
|
-
/** Where an attribute sits in the interleave: its
|
|
79
|
-
* Null when the layout does not carry that name. */
|
|
80
|
-
export function layoutSlot(layout: VertexLayout | undefined, name: string): { offset: number;
|
|
243
|
+
/** Where an attribute sits in the interleave: its byte offset, format
|
|
244
|
+
* and component count. Null when the layout does not carry that name. */
|
|
245
|
+
export function layoutSlot(layout: VertexLayout | undefined, name: string): { offset: number; format: VertexFormat; components: number } | null {
|
|
81
246
|
let offset = 0
|
|
82
247
|
for (let attr of layoutAttributes(layout)) {
|
|
83
|
-
let
|
|
84
|
-
if (attr.name === name) return { offset,
|
|
85
|
-
offset +=
|
|
248
|
+
let codec = VERTEX_FORMATS[attr.format]
|
|
249
|
+
if (attr.name === name) return { offset, format: attr.format, components: codec.components }
|
|
250
|
+
offset += codec.bytes
|
|
86
251
|
}
|
|
87
252
|
return null
|
|
88
253
|
}
|
|
89
254
|
|
|
90
|
-
/**
|
|
91
|
-
*
|
|
255
|
+
/** Whether every attribute of a layout is float32-family: such a buffer
|
|
256
|
+
* is a Float32Array of `layoutStride / 4` floats per vertex, the
|
|
257
|
+
* generators' write form. */
|
|
258
|
+
export function isFloatLayout(layout?: VertexLayout): boolean {
|
|
259
|
+
return layoutAttributes(layout).every(a => isFloatFormat(a.format))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The check every layout must pass: aPos float32x3 first and no
|
|
263
|
+
* duplicate names. */
|
|
92
264
|
function checkLayout(layout: VertexAttribute[], where: string): void {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (got === undefined || got.name !== want.name || got.format !== want.format) {
|
|
97
|
-
throw new Error(where + ": a layout must start with the standard prefix (aPos vec3, aNormal vec3, aUV vec2)")
|
|
98
|
-
}
|
|
265
|
+
let first = layout[0]
|
|
266
|
+
if (first === undefined || first.name !== "aPos" || first.format !== "float32x3") {
|
|
267
|
+
throw new Error(where + ": a layout must start with aPos float32x3")
|
|
99
268
|
}
|
|
100
269
|
let seen = new Set<string>()
|
|
101
270
|
for (let attr of layout) {
|
|
@@ -104,11 +273,149 @@ function checkLayout(layout: VertexAttribute[], where: string): void {
|
|
|
104
273
|
}
|
|
105
274
|
}
|
|
106
275
|
|
|
276
|
+
/** The bytes of a vertex buffer, whatever view holds them. */
|
|
277
|
+
export function vertexBytes(vertices: ArrayBufferView): Uint8Array {
|
|
278
|
+
return new Uint8Array(vertices.buffer, vertices.byteOffset, vertices.byteLength)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The view a vertex buffer of `layout` is handed out as: a Float32Array
|
|
282
|
+
* over an all-float layout (the generators' form, indexable as floats),
|
|
283
|
+
* a Uint8Array over a layout with a packed channel. */
|
|
284
|
+
export function vertexView(layout: VertexLayout | undefined, buffer: ArrayBuffer, byteOffset = 0, byteLength = buffer.byteLength - byteOffset): ArrayBufferView {
|
|
285
|
+
return isFloatLayout(layout)
|
|
286
|
+
? new Float32Array(buffer, byteOffset, byteLength / Float32Array.BYTES_PER_ELEMENT)
|
|
287
|
+
: new Uint8Array(buffer, byteOffset, byteLength)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Vertices in a buffer of `layout`. Throws naming `where` when the byte
|
|
291
|
+
* length is not a whole number of them, or the view is not 4-aligned
|
|
292
|
+
* (every stride is, and the GPU upload and the picking shape count on
|
|
293
|
+
* it). */
|
|
294
|
+
export function vertexCount(vertices: ArrayBufferView, layout: VertexLayout | undefined, where: string): number {
|
|
295
|
+
let stride = layoutStride(layout)
|
|
296
|
+
if (vertices.byteOffset % Float32Array.BYTES_PER_ELEMENT !== 0) {
|
|
297
|
+
throw new Error(where + ": vertex data must start on a 4-byte boundary")
|
|
298
|
+
}
|
|
299
|
+
if (vertices.byteLength % stride !== 0) {
|
|
300
|
+
throw new Error(where + ": " + vertices.byteLength + " vertex bytes is not a whole number of " + stride + "-byte (" + layoutKey(layout) + ") vertices")
|
|
301
|
+
}
|
|
302
|
+
return vertices.byteLength / stride
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** A view on one attribute of a vertex buffer: component `k` of vertex
|
|
306
|
+
* `i`, read and written as the float the shader sees, through the
|
|
307
|
+
* format's codec. The one way anything touches vertex data, so no
|
|
308
|
+
* caller knows an offset, a stride or a format. */
|
|
309
|
+
export type AttributeAccess = {
|
|
310
|
+
format: VertexFormat
|
|
311
|
+
components: number
|
|
312
|
+
get(i: number, k: number): number
|
|
313
|
+
set(i: number, k: number, v: number): void
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The accessor for `name` over a bare vertex buffer of `layout`; null
|
|
317
|
+
* when the layout lacks the name. */
|
|
318
|
+
export function attributeAccess(vertices: ArrayBufferView, layout: VertexLayout | undefined, name: string): AttributeAccess | null {
|
|
319
|
+
let slot = layoutSlot(layout, name)
|
|
320
|
+
if (slot === null) return null
|
|
321
|
+
let stride = layoutStride(layout)
|
|
322
|
+
let codec = VERTEX_FORMATS[slot.format]
|
|
323
|
+
let offset = slot.offset
|
|
324
|
+
let dv = new DataView(vertices.buffer, vertices.byteOffset, vertices.byteLength)
|
|
325
|
+
return {
|
|
326
|
+
format: slot.format,
|
|
327
|
+
components: codec.components,
|
|
328
|
+
get: (i, k) => codec.get(dv, i * stride + offset, k),
|
|
329
|
+
set: (i, k, v) => codec.set(dv, i * stride + offset, k, v),
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** One extra vertex buffer of a geometry (see `Geometry.streams`): its
|
|
334
|
+
* own attribute list and bytes, holding the geometry's vertex count. */
|
|
335
|
+
export type VertexStream = { layout: VertexAttribute[]; vertices: ArrayBufferView }
|
|
336
|
+
|
|
337
|
+
/** Every vertex buffer of a geometry in pipeline order: stream 0
|
|
338
|
+
* (`vertices` under `layout`), then `streams`. */
|
|
339
|
+
export function geometryStreams(geometry: Geometry): { layout: VertexLayout | undefined; vertices: ArrayBufferView }[] {
|
|
340
|
+
let out: { layout: VertexLayout | undefined; vertices: ArrayBufferView }[] = [{ layout: geometry.layout, vertices: geometry.vertices }]
|
|
341
|
+
for (let stream of geometry.streams ?? []) out.push(stream)
|
|
342
|
+
return out
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The attribute lists of a geometry's streams, in order: what a pipeline
|
|
346
|
+
* declares as its vertex-step buffer layouts. */
|
|
347
|
+
export function geometryLayouts(geometry: Geometry): VertexAttribute[][] {
|
|
348
|
+
return geometryStreams(geometry).map(s => layoutAttributes(s.layout))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** A geometry's layout identity across every stream (`layoutKey` per
|
|
352
|
+
* stream, `|` between them): two geometries with equal keys bind the same
|
|
353
|
+
* pipeline. */
|
|
354
|
+
export function geometryKey(geometry: Geometry): string {
|
|
355
|
+
return geometryStreams(geometry)
|
|
356
|
+
.map(s => layoutKey(s.layout))
|
|
357
|
+
.join("|")
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Where an attribute sits in a geometry: its stream index and its slot
|
|
361
|
+
* there. Null when no stream carries the name. */
|
|
362
|
+
export function geometrySlot(geometry: Geometry, name: string): { stream: number; offset: number; format: VertexFormat; components: number } | null {
|
|
363
|
+
let streams = geometryStreams(geometry)
|
|
364
|
+
for (let i = 0; i < streams.length; i++) {
|
|
365
|
+
let slot = layoutSlot(streams[i]!.layout, name)
|
|
366
|
+
if (slot !== null) return { stream: i, ...slot }
|
|
367
|
+
}
|
|
368
|
+
return null
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The geometry's vertex count: stream 0's, which every extra stream must
|
|
372
|
+
* hold too. Throws naming `where` on a stream of another count. */
|
|
373
|
+
export function geometryVertexCount(geometry: Geometry, where: string): number {
|
|
374
|
+
let streams = geometryStreams(geometry)
|
|
375
|
+
let count = vertexCount(streams[0]!.vertices, streams[0]!.layout, where)
|
|
376
|
+
for (let i = 1; i < streams.length; i++) {
|
|
377
|
+
let n = vertexCount(streams[i]!.vertices, streams[i]!.layout, where + " stream " + i)
|
|
378
|
+
if (n !== count) throw new Error(where + ": stream " + i + " holds " + n + " vertices, stream 0 holds " + count)
|
|
379
|
+
}
|
|
380
|
+
return count
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** `attributeAccess` over whichever of a geometry's streams carries
|
|
384
|
+
* `name`; null when none does. */
|
|
385
|
+
export function geometryAttribute(geometry: Geometry, name: string): AttributeAccess | null {
|
|
386
|
+
for (let stream of geometryStreams(geometry)) {
|
|
387
|
+
let access = attributeAccess(stream.vertices, stream.layout, name)
|
|
388
|
+
if (access !== null) return access
|
|
389
|
+
}
|
|
390
|
+
return null
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// The accessor a reader of the standard channels falls back on when the
|
|
394
|
+
// layout lacks the channel: zeros, so a point cloud without a normal or
|
|
395
|
+
// uv reads (0, 0, 0) and (0, 0).
|
|
396
|
+
const ZERO_ACCESS: AttributeAccess = { format: "float32", components: 0, get: () => 0, set: () => {} }
|
|
397
|
+
|
|
398
|
+
/** The stream rules: every extra stream declares at least one attribute
|
|
399
|
+
* and no name appears in two streams (each is one shader `in`). */
|
|
400
|
+
function checkStreams(geometry: Geometry, where: string): void {
|
|
401
|
+
let seen = new Set<string>()
|
|
402
|
+
let layouts = geometryLayouts(geometry)
|
|
403
|
+
for (let i = 0; i < layouts.length; i++) {
|
|
404
|
+
let attrs = layouts[i]!
|
|
405
|
+
if (i > 0 && attrs.length === 0) throw new Error(where + ": stream " + i + " declares no attributes")
|
|
406
|
+
for (let attr of attrs) {
|
|
407
|
+
if (seen.has(attr.name)) throw new Error(where + ": attribute '" + attr.name + "' appears in two streams")
|
|
408
|
+
seen.add(attr.name)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
107
413
|
/**
|
|
108
|
-
* The structural check for geometry about to draw: the layout
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
414
|
+
* The structural check for geometry about to draw: the layout starts
|
|
415
|
+
* with aPos, every stream holds a whole number of the geometry's
|
|
416
|
+
* vertices under its stride, no attribute name repeats across streams,
|
|
417
|
+
* and indices are present. Throws naming the geometry. The scene runs it at
|
|
418
|
+
* add() so hand-built geometry (a bare `layout: "colored"` over a
|
|
112
419
|
* miscounted array) fails there instead of drawing garbage triangles.
|
|
113
420
|
* Deliberately no max-index scan: that is O(indices) per add, and the
|
|
114
421
|
* generators and merge/transform keep indices in range by construction.
|
|
@@ -117,13 +424,37 @@ export function validateGeometry(geometry: Geometry): void {
|
|
|
117
424
|
let name = geometry.label ? "geometry '" + geometry.label + "'" : "geometry"
|
|
118
425
|
let layout = geometry.layout
|
|
119
426
|
if (layout !== undefined && typeof layout !== "string") checkLayout(layout, name)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
427
|
+
checkStreams(geometry, name)
|
|
428
|
+
geometryVertexCount(geometry, name)
|
|
429
|
+
let topology = geometryTopology(geometry)
|
|
430
|
+
let count = geometry.indices.length
|
|
431
|
+
// Lines and points may be empty: a feature-edge pass over a smooth
|
|
432
|
+
// closed shape has nothing to draw, and that is a result, not a bug.
|
|
433
|
+
switch (topology) {
|
|
434
|
+
case "triangles":
|
|
435
|
+
if (count === 0) throw new Error(name + ": no indices")
|
|
436
|
+
if (count % 3 !== 0) throw new Error(name + ": " + count + " indices is not a whole number of triangles")
|
|
437
|
+
break
|
|
438
|
+
case "lines":
|
|
439
|
+
if (count % 2 !== 0) throw new Error(name + ": " + count + " indices is not a whole number of lines")
|
|
440
|
+
break
|
|
441
|
+
case "line-strip":
|
|
442
|
+
if (count < 2) throw new Error(name + ": a line strip needs at least 2 indices")
|
|
443
|
+
break
|
|
444
|
+
case "triangle-strip":
|
|
445
|
+
if (count < 3) throw new Error(name + ": a triangle strip needs at least 3 indices")
|
|
446
|
+
break
|
|
447
|
+
case "points":
|
|
448
|
+
break
|
|
449
|
+
default:
|
|
450
|
+
throw new Error(name + ": unknown topology '" + String(topology) + "'")
|
|
125
451
|
}
|
|
126
|
-
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** The geometry's topology with the default applied: what its indices
|
|
455
|
+
* list, "triangles" unless it says otherwise. */
|
|
456
|
+
export function geometryTopology(geometry: Geometry): Topology {
|
|
457
|
+
return geometry.topology ?? "triangles"
|
|
127
458
|
}
|
|
128
459
|
|
|
129
460
|
/** The options every generator shares (each generator's own options type
|
|
@@ -134,9 +465,30 @@ export function validateGeometry(geometry: Geometry): void {
|
|
|
134
465
|
* channel geometry is built in one pass instead of generate-then-repack. */
|
|
135
466
|
export type GeometryOptions = { label?: string; layout?: VertexLayout }
|
|
136
467
|
|
|
137
|
-
/** The generator
|
|
468
|
+
/** The generator path writes the standard channels in their standard
|
|
469
|
+
* order into a Float32Array, so a layout a generator is asked to emit
|
|
470
|
+
* must start with the prefix and stay float32-family throughout; past
|
|
471
|
+
* the prefix the names are open. Packing is a pass over the result
|
|
472
|
+
* (`withAttribute` with a packed format), not a generator option. */
|
|
473
|
+
function checkGeneratorLayout(layout: VertexAttribute[], where: string): void {
|
|
474
|
+
checkLayout(layout, where)
|
|
475
|
+
for (let i = 0; i < BASE_ATTRIBUTES.length; i++) {
|
|
476
|
+
let want = BASE_ATTRIBUTES[i]!
|
|
477
|
+
let got = layout[i]
|
|
478
|
+
if (got === undefined || got.name !== want.name || got.format !== want.format) {
|
|
479
|
+
throw new Error(where + ": a generator layout must start with the base prefix (aPos float32x3, aNormal float32x3, aUV float32x2)")
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (let attr of layout) {
|
|
483
|
+
if (!isFloatFormat(attr.format)) {
|
|
484
|
+
throw new Error(where + ": a generator layout is float32-family throughout ('" + attr.name + "' is " + attr.format + "); pack a channel with withAttribute over the result")
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** The generator tail: pack base-layout vertices (number[] of 8 per
|
|
138
490
|
* vertex, or an already-written Float32Array) and indices into a Geometry
|
|
139
|
-
* of the requested layout. A wider layout spreads the
|
|
491
|
+
* of the requested layout. A wider layout spreads the base channels
|
|
140
492
|
* to its stride, leaving the extra slots zero. */
|
|
141
493
|
export function packGeometry(
|
|
142
494
|
verts: ArrayLike<number>,
|
|
@@ -144,40 +496,39 @@ export function packGeometry(
|
|
|
144
496
|
options: GeometryOptions = {},
|
|
145
497
|
): Geometry {
|
|
146
498
|
let { label, layout } = options
|
|
147
|
-
if (verts.length %
|
|
148
|
-
throw new Error("packGeometry: vertex data is not a whole number of
|
|
499
|
+
if (verts.length % BASE_FLOATS !== 0) {
|
|
500
|
+
throw new Error("packGeometry: vertex data is not a whole number of base-layout vertices")
|
|
149
501
|
}
|
|
150
|
-
let count = verts.length /
|
|
502
|
+
let count = verts.length / BASE_FLOATS
|
|
151
503
|
let packedIndices = indices instanceof Uint16Array || indices instanceof Uint32Array ? indices : packIndices(indices, count)
|
|
152
|
-
let
|
|
153
|
-
if (
|
|
154
|
-
let stride = layoutStride(attrs)
|
|
155
|
-
if (stride === STANDARD_FLOATS) {
|
|
504
|
+
let stride = generatorStride(options, "packGeometry")
|
|
505
|
+
if (stride === BASE_FLOATS) {
|
|
156
506
|
let vertices = verts instanceof Float32Array ? verts : new Float32Array(verts)
|
|
157
507
|
return layout === undefined ? { vertices, indices: packedIndices, label } : { vertices, indices: packedIndices, layout, label }
|
|
158
508
|
}
|
|
159
509
|
let vertices = new Float32Array(count * stride)
|
|
160
510
|
for (let i = 0; i < count; i++) {
|
|
161
|
-
let s = i *
|
|
511
|
+
let s = i * BASE_FLOATS
|
|
162
512
|
let d = i * stride
|
|
163
|
-
for (let k = 0; k <
|
|
513
|
+
for (let k = 0; k < BASE_FLOATS; k++) vertices[d + k] = verts[s + k]!
|
|
164
514
|
}
|
|
165
515
|
return { vertices, indices: packedIndices, layout, label }
|
|
166
516
|
}
|
|
167
517
|
|
|
168
|
-
// The stride a generator writing a Float32Array directly must
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
518
|
+
// The stride in FLOATS a generator writing a Float32Array directly must
|
|
519
|
+
// use for the requested layout (float32-family by the generator rule, so
|
|
520
|
+
// the byte stride is a whole number of floats), and the matching finish
|
|
521
|
+
// (no repack: the data is already laid out).
|
|
522
|
+
function generatorStride(options: GeometryOptions, where = "generator layout"): number {
|
|
172
523
|
let { layout } = options
|
|
173
524
|
let attrs = layoutAttributes(layout)
|
|
174
|
-
if (layout !== undefined && typeof layout !== "string")
|
|
175
|
-
return layoutStride(attrs)
|
|
525
|
+
if (layout !== undefined && typeof layout !== "string") checkGeneratorLayout(attrs, where)
|
|
526
|
+
return layoutStride(attrs) / Float32Array.BYTES_PER_ELEMENT
|
|
176
527
|
}
|
|
177
528
|
|
|
178
529
|
function finishGeometry(vertices: Float32Array, indices: number[], options: GeometryOptions): Geometry {
|
|
179
530
|
let { label, layout } = options
|
|
180
|
-
let packed = packIndices(indices, vertices
|
|
531
|
+
let packed = packIndices(indices, vertexCount(vertices, layout, "generator"))
|
|
181
532
|
return layout === undefined ? { vertices, indices: packed, label } : { vertices, indices: packed, layout, label }
|
|
182
533
|
}
|
|
183
534
|
|
|
@@ -188,35 +539,193 @@ export function packIndices(indices: number[], vertexCount: number): Uint16Array
|
|
|
188
539
|
}
|
|
189
540
|
|
|
190
541
|
export type Geometry = {
|
|
191
|
-
/**
|
|
192
|
-
*
|
|
193
|
-
|
|
542
|
+
/** The interleaved vertex bytes in the layout's order: a Float32Array
|
|
543
|
+
* for an all-float layout (the generators' form: [pos.xyz, normal.xyz,
|
|
544
|
+
* uv.xy] per vertex, plus color.rgba in "colored"), any 4-aligned view
|
|
545
|
+
* otherwise. Read and write channels through `geometryAttribute`; the
|
|
546
|
+
* view type is never the contract. */
|
|
547
|
+
vertices: ArrayBufferView
|
|
194
548
|
/** The array type picks the draw's index format: Uint32Array past 64k
|
|
195
549
|
* vertices. */
|
|
196
550
|
indices: Uint16Array | Uint32Array
|
|
197
|
-
/**
|
|
551
|
+
/** Extra vertex buffers beside `vertices` (which is stream 0, where
|
|
552
|
+
* aPos lives): each its own attribute list over its own bytes, holding
|
|
553
|
+
* the same vertex count. A pipeline reads every stream; a material
|
|
554
|
+
* finds a channel by name whichever stream carries it. The shape for
|
|
555
|
+
* data that changes on its own schedule: the per-frame channel in a
|
|
556
|
+
* stream of its own, rewritten in place with updateVertices while the
|
|
557
|
+
* static channels stay put. `withAttribute` with a `stream` option
|
|
558
|
+
* appends to or opens one. Absent means one buffer. */
|
|
559
|
+
streams?: VertexStream[]
|
|
560
|
+
/** What the indices describe; absent means "triangles". The index
|
|
561
|
+
* buffer and the primitive it lists travel together (Godot's surface
|
|
562
|
+
* primitive, Unity's Mesh.SetIndices topology, Three's Line/Points
|
|
563
|
+
* object types over one BufferGeometry): the material draws whatever
|
|
564
|
+
* topology its geometry carries, one pipeline per (layout, topology)
|
|
565
|
+
* pair. Only a triangle list gets the picking shape - a lines, points
|
|
566
|
+
* or strip geometry picks (and collides) by its bounds box, the tier a
|
|
567
|
+
* sprite or an instanced mesh is at, so hits carry no face/uv. Lines
|
|
568
|
+
* are one pixel wide on GL ES whatever the display density. A lines
|
|
569
|
+
* or points geometry may have no indices at all (it then draws
|
|
570
|
+
* nothing; the other topologies reject that at add()).
|
|
571
|
+
* wireframeGeometry/edgesGeometry build "lines" over a triangle
|
|
572
|
+
* geometry's own vertices. */
|
|
573
|
+
topology?: Topology
|
|
574
|
+
/** Vertex layout; absent means "base". Must match the material's
|
|
198
575
|
* layout - the scene rejects a mismatched pair at add(). */
|
|
199
576
|
layout?: VertexLayout
|
|
577
|
+
/** Morph targets (blend shapes), packed sparse by vertex - see
|
|
578
|
+
* `MorphTargets` and `packMorphTargets`. A morphed material displaces
|
|
579
|
+
* positions and normals by the node's weights in the vertex stage
|
|
580
|
+
* before any skinning; picking, the transparent sort and every read of
|
|
581
|
+
* the retained tree see the base shape, and `geometryBounds` grows by
|
|
582
|
+
* the targets' extent. Absent means none. */
|
|
583
|
+
morphs?: MorphTargets
|
|
200
584
|
/** Debug name for the lazily-created GPU buffers. */
|
|
201
585
|
label?: string
|
|
202
586
|
_bounds?: Float32Array
|
|
203
587
|
}
|
|
204
588
|
|
|
589
|
+
/** One morph target as authored, glTF's primitive target: per-vertex
|
|
590
|
+
* POSITION deltas (3 per vertex over the geometry's vertex count) and
|
|
591
|
+
* optional NORMAL deltas, added to the base channels scaled by the
|
|
592
|
+
* target's weight. Deltas only: an absolute target is its shape minus
|
|
593
|
+
* the base. What `packMorphTargets` takes. */
|
|
594
|
+
export type MorphTarget = { name: string; position: ArrayLike<number>; normal?: ArrayLike<number> | null }
|
|
595
|
+
|
|
596
|
+
/** Floats per texel of the morph packing (an rgba32f texel). */
|
|
597
|
+
export const MORPH_TEXEL_FLOATS = 4
|
|
598
|
+
/** Texels per packed entry: [target, dx, dy, dz] then [nx, ny, nz, 0]. */
|
|
599
|
+
export const MORPH_ENTRY_TEXELS = 2
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* A geometry's morph targets packed SPARSE BY VERTEX, the form the vertex
|
|
603
|
+
* stage reads: rgba32f texels as a flat float array (the GPU step lays
|
|
604
|
+
* them into the rows of one texture; a shader turns a texel index into
|
|
605
|
+
* its coordinates by the texture width). Texel v, for every vertex v, is
|
|
606
|
+
* the vertex's header `[offset, count, 0, 0]`: the texel index of its
|
|
607
|
+
* first entry and how many targets displace it. The entries follow, two
|
|
608
|
+
* texels each (`MORPH_ENTRY_TEXELS`): `[target, dx, dy, dz]` and
|
|
609
|
+
* `[nx, ny, nz, 0]`. A vertex no target moves has count 0, and a target
|
|
610
|
+
* that leaves a vertex at zero delta (both position and normal exactly
|
|
611
|
+
* zero) writes no entry for it, so memory is proportional to the deltas
|
|
612
|
+
* the targets author (a mouth on a face, an exporter's sparse accessors)
|
|
613
|
+
* and never vertices x targets - which is what lifts a dense layout's
|
|
614
|
+
* ceiling of about a hundred targets on a 20k-vertex face. `extent` is
|
|
615
|
+
* the per-axis growth of the geometry's box under any weights in [0, 1]:
|
|
616
|
+
* `[minDx, minDy, minDz, maxDx, maxDy, maxDz]`, the most negative and
|
|
617
|
+
* most positive per-vertex sums of the deltas over that vertex's targets
|
|
618
|
+
* (weights past 1 may leave it).
|
|
619
|
+
*/
|
|
620
|
+
export type MorphTargets = {
|
|
621
|
+
/** One name per target, in target order: what setMorphWeights resolves. */
|
|
622
|
+
names: string[]
|
|
623
|
+
texels: Float32Array
|
|
624
|
+
extent: Float32Array
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Pack morph targets for a geometry of `vertexCount` vertices (see
|
|
629
|
+
* `MorphTargets` for the layout). Pure array math, run by the glTF parser
|
|
630
|
+
* at bake time and by `withMorphTargets` for hand-built shapes.
|
|
631
|
+
*/
|
|
632
|
+
export function packMorphTargets(vertexCount: number, targets: MorphTarget[]): MorphTargets {
|
|
633
|
+
targets.forEach((t, i) => {
|
|
634
|
+
if (t.position.length !== vertexCount * 3) {
|
|
635
|
+
throw new Error("packMorphTargets: target " + i + " has " + t.position.length + " position floats for " + vertexCount + " vertices")
|
|
636
|
+
}
|
|
637
|
+
if (t.normal != null && t.normal.length !== vertexCount * 3) {
|
|
638
|
+
throw new Error("packMorphTargets: target " + i + " has " + t.normal.length + " normal floats for " + vertexCount + " vertices")
|
|
639
|
+
}
|
|
640
|
+
})
|
|
641
|
+
let moves = (t: MorphTarget, v: number): boolean => {
|
|
642
|
+
let p = v * 3
|
|
643
|
+
if (t.position[p] !== 0 || t.position[p + 1] !== 0 || t.position[p + 2] !== 0) return true
|
|
644
|
+
return t.normal != null && (t.normal[p] !== 0 || t.normal[p + 1] !== 0 || t.normal[p + 2] !== 0)
|
|
645
|
+
}
|
|
646
|
+
// Pass one: how many targets displace each vertex, which fixes every
|
|
647
|
+
// vertex's run of entries and so the headers.
|
|
648
|
+
let counts = new Uint32Array(vertexCount)
|
|
649
|
+
let total = 0
|
|
650
|
+
for (let t of targets) {
|
|
651
|
+
for (let v = 0; v < vertexCount; v++) {
|
|
652
|
+
if (moves(t, v)) {
|
|
653
|
+
counts[v]!++
|
|
654
|
+
total++
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
let texels = new Float32Array((vertexCount + total * MORPH_ENTRY_TEXELS) * MORPH_TEXEL_FLOATS)
|
|
659
|
+
let cursor = new Uint32Array(vertexCount)
|
|
660
|
+
let next = vertexCount
|
|
661
|
+
for (let v = 0; v < vertexCount; v++) {
|
|
662
|
+
texels[v * MORPH_TEXEL_FLOATS] = next
|
|
663
|
+
texels[v * MORPH_TEXEL_FLOATS + 1] = counts[v]!
|
|
664
|
+
cursor[v] = next
|
|
665
|
+
next += counts[v]! * MORPH_ENTRY_TEXELS
|
|
666
|
+
}
|
|
667
|
+
// Pass two: the entries, in target order per vertex, and the per-vertex
|
|
668
|
+
// negative and positive delta sums the extent reduces over.
|
|
669
|
+
let sums = new Float32Array(vertexCount * 6)
|
|
670
|
+
for (let t = 0; t < targets.length; t++) {
|
|
671
|
+
let target = targets[t]!
|
|
672
|
+
for (let v = 0; v < vertexCount; v++) {
|
|
673
|
+
if (!moves(target, v)) continue
|
|
674
|
+
let at = cursor[v]! * MORPH_TEXEL_FLOATS
|
|
675
|
+
cursor[v] = cursor[v]! + MORPH_ENTRY_TEXELS
|
|
676
|
+
let p = v * 3
|
|
677
|
+
texels[at] = t
|
|
678
|
+
for (let k = 0; k < 3; k++) {
|
|
679
|
+
let d = target.position[p + k]!
|
|
680
|
+
texels[at + 1 + k] = d
|
|
681
|
+
texels[at + MORPH_TEXEL_FLOATS + k] = target.normal != null ? target.normal[p + k]! : 0
|
|
682
|
+
let side = d < 0 ? v * 6 + k : v * 6 + 3 + k
|
|
683
|
+
sums[side] = sums[side]! + d
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
let extent = new Float32Array(6)
|
|
688
|
+
for (let v = 0; v < vertexCount; v++) {
|
|
689
|
+
for (let k = 0; k < 3; k++) {
|
|
690
|
+
if (sums[v * 6 + k]! < extent[k]!) extent[k] = sums[v * 6 + k]!
|
|
691
|
+
if (sums[v * 6 + 3 + k]! > extent[3 + k]!) extent[3 + k] = sums[v * 6 + 3 + k]!
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return { names: targets.map((t) => t.name), texels, extent }
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* A geometry with morph targets: a new geometry over the source's
|
|
699
|
+
* vertices and indices (shared, so both bind the same GPU buffers) whose
|
|
700
|
+
* `morphs` is `targets` packed. Three's `geometry.morphAttributes` for
|
|
701
|
+
* relative targets; a shape's blend shapes from a modeller arrive this
|
|
702
|
+
* way through the glTF loader.
|
|
703
|
+
*/
|
|
704
|
+
export function withMorphTargets(geometry: Geometry, targets: MorphTarget[], label?: string): Geometry {
|
|
705
|
+
let out: Geometry = { ...geometry, morphs: packMorphTargets(geometryVertexCount(geometry, "withMorphTargets"), targets) }
|
|
706
|
+
delete out._bounds
|
|
707
|
+
if (label !== undefined) out.label = label
|
|
708
|
+
return out
|
|
709
|
+
}
|
|
710
|
+
|
|
205
711
|
/**
|
|
206
712
|
* The geometry's LOCAL axis-aligned bounds as [minX, minY, minZ, maxX,
|
|
207
|
-
* maxY, maxZ], computed from the vertices on first use
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
713
|
+
* maxY, maxZ], computed from the vertices on first use (grown by the
|
|
714
|
+
* morph targets' extent when it carries any) and cached; a stream-0
|
|
715
|
+
* updateVertices drops the cache. What the transparent sort and
|
|
716
|
+
* a model's bounds read (the picking index keeps its own copy of the
|
|
717
|
+
* positions and derives its box there); a flat geometry legitimately has
|
|
718
|
+
* zero extent on an axis. An empty geometry yields a zero box at the
|
|
719
|
+
* origin.
|
|
211
720
|
*/
|
|
212
721
|
export function geometryBounds(geometry: Geometry): Float32Array {
|
|
213
722
|
let bounds = geometry._bounds
|
|
214
723
|
if (bounds === undefined) {
|
|
215
724
|
bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
|
|
216
|
-
let
|
|
217
|
-
let
|
|
218
|
-
for (let i = 0; i
|
|
219
|
-
let x =
|
|
725
|
+
let count = geometryVertexCount(geometry, "geometryBounds")
|
|
726
|
+
let pos = geometryAttribute(geometry, "aPos")!
|
|
727
|
+
for (let i = 0; i < count; i++) {
|
|
728
|
+
let x = pos.get(i, 0), y = pos.get(i, 1), z = pos.get(i, 2)
|
|
220
729
|
if (x < bounds[0]!) bounds[0] = x
|
|
221
730
|
if (y < bounds[1]!) bounds[1] = y
|
|
222
731
|
if (z < bounds[2]!) bounds[2] = z
|
|
@@ -225,6 +734,11 @@ export function geometryBounds(geometry: Geometry): Float32Array {
|
|
|
225
734
|
if (z > bounds[5]!) bounds[5] = z
|
|
226
735
|
}
|
|
227
736
|
if (bounds[0]! > bounds[3]!) bounds.fill(0)
|
|
737
|
+
else if (geometry.morphs !== undefined) {
|
|
738
|
+
// Morph targets displace vertices at draw time: the box grows by
|
|
739
|
+
// their extent so any weights in [0, 1] keep the shape inside it.
|
|
740
|
+
for (let k = 0; k < 6; k++) bounds[k] = bounds[k]! + geometry.morphs.extent[k]!
|
|
741
|
+
}
|
|
228
742
|
geometry._bounds = bounds
|
|
229
743
|
}
|
|
230
744
|
return bounds
|
|
@@ -232,124 +746,166 @@ export function geometryBounds(geometry: Geometry): Float32Array {
|
|
|
232
746
|
|
|
233
747
|
/** Per-vertex values for withAttribute/fillAttribute: a flat array of the
|
|
234
748
|
* attribute's size per vertex, or a callback deriving each vertex's value
|
|
235
|
-
* from the
|
|
749
|
+
* from the base channels (what a baker wants). A channel the layout
|
|
750
|
+
* lacks (a point cloud without a normal or uv) arrives as zeros. */
|
|
236
751
|
export type AttributeFill = ArrayLike<number> | ((index: number, pos: Vec3, normal: Vec3, uv: Vec2) => ArrayLike<number>)
|
|
237
752
|
/** AttributeFill for the aColor vec4 channel (4 per vertex). */
|
|
238
753
|
export type ColorFill = AttributeFill
|
|
239
754
|
|
|
755
|
+
/** Options of `withAttribute`: the result's label, and which stream the
|
|
756
|
+
* channel lands in - 0 (the default) interleaves it into the main buffer,
|
|
757
|
+
* 1..n appends it to that extra stream, n + 1 opens a new stream holding
|
|
758
|
+
* it alone (Three's one buffer per channel; the shape for a channel
|
|
759
|
+
* rewritten per frame). */
|
|
760
|
+
export type WithAttributeOptions = { label?: string; stream?: number }
|
|
761
|
+
|
|
240
762
|
/**
|
|
241
763
|
* Append a named channel to a geometry: a new geometry (the source is
|
|
242
764
|
* untouched, its GPU buffers stay independent) whose layout is the
|
|
243
765
|
* source's plus `attr`, every existing channel copied through and the new
|
|
244
766
|
* slots written from `fill`. This is Three's `geometry.setAttribute` for
|
|
245
767
|
* an interleaved buffer - the one generic primitive; `withColors` is its
|
|
246
|
-
* aColor spelling. A material reads the channel by declaring
|
|
247
|
-
*
|
|
768
|
+
* aColor spelling. A material reads the channel by declaring an `in` of
|
|
769
|
+
* the matching name and component count in its vertex stage. A packed
|
|
770
|
+
* format (`unorm8x4` for a color, `snorm16x2`, `float16x4`, ...) is how a
|
|
771
|
+
* channel is compressed: the fill is given in float and encoded on write.
|
|
772
|
+
* `options.stream` picks the stream the channel lands in (see
|
|
773
|
+
* `WithAttributeOptions`); the other streams are shared with the source.
|
|
248
774
|
*/
|
|
249
|
-
export function withAttribute(geometry: Geometry, attr: VertexAttribute, fill: AttributeFill,
|
|
250
|
-
|
|
251
|
-
if (layoutSlot(srcLayout, attr.name) !== null) {
|
|
775
|
+
export function withAttribute(geometry: Geometry, attr: VertexAttribute, fill: AttributeFill, options: WithAttributeOptions = {}): Geometry {
|
|
776
|
+
if (geometrySlot(geometry, attr.name) !== null) {
|
|
252
777
|
throw new Error("withAttribute: geometry already carries '" + attr.name + "'")
|
|
253
778
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
throw new Error("withAttribute: vertex data is not a whole number of " + layoutKey(srcLayout) + " vertices")
|
|
779
|
+
if (!(attr.format in VERTEX_FORMATS)) {
|
|
780
|
+
throw new Error("withAttribute: unknown format '" + String(attr.format) + "' for " + attr.name + " (expected " + Object.keys(VERTEX_FORMATS).join(", ") + ")")
|
|
257
781
|
}
|
|
258
|
-
let
|
|
259
|
-
|
|
260
|
-
let
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
let out = new Float32Array(count * stride)
|
|
264
|
-
for (let i = 0; i < count; i++) {
|
|
265
|
-
let s = i * srcStride
|
|
266
|
-
let d = i * stride
|
|
267
|
-
for (let k = 0; k < srcStride; k++) out[d + k] = src[s + k]!
|
|
782
|
+
let count = geometryVertexCount(geometry, "withAttribute")
|
|
783
|
+
let streams = geometryStreams(geometry)
|
|
784
|
+
let target = options.stream ?? 0
|
|
785
|
+
if (!Number.isInteger(target) || target < 0 || target > streams.length) {
|
|
786
|
+
throw new Error("withAttribute: stream " + target + " is out of range; the geometry has streams 0.." + (streams.length - 1) + " and " + streams.length + " opens a new one")
|
|
268
787
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
788
|
+
let added: VertexAttribute = { name: attr.name, format: attr.format }
|
|
789
|
+
let layout: VertexAttribute[]
|
|
790
|
+
let vertices: ArrayBufferView
|
|
791
|
+
if (target === streams.length) {
|
|
792
|
+
layout = [added]
|
|
793
|
+
vertices = vertexView(layout, new ArrayBuffer(count * layoutStride(layout)))
|
|
794
|
+
} else {
|
|
795
|
+
let source = streams[target]!
|
|
796
|
+
let srcLayout = layoutAttributes(source.layout)
|
|
797
|
+
let srcStride = layoutStride(srcLayout)
|
|
798
|
+
layout = [...srcLayout, added]
|
|
799
|
+
if (target === 0) checkLayout(layout, "withAttribute")
|
|
800
|
+
let stride = layoutStride(layout)
|
|
801
|
+
let src = vertexBytes(source.vertices)
|
|
802
|
+
let buffer = new ArrayBuffer(count * stride)
|
|
803
|
+
let dst = new Uint8Array(buffer)
|
|
804
|
+
for (let i = 0; i < count; i++) dst.set(src.subarray(i * srcStride, (i + 1) * srcStride), i * stride)
|
|
805
|
+
vertices = vertexView(layout, buffer)
|
|
806
|
+
}
|
|
807
|
+
// The callback reads the standard channels from the SOURCE geometry,
|
|
808
|
+
// whichever stream carries them; the new slot is written on the fresh
|
|
809
|
+
// buffer.
|
|
810
|
+
fillWith(attributeAccess(vertices, layout, attr.name)!, geometry, count, attr.name, fill, 0)
|
|
811
|
+
let extra = (geometry.streams ?? []).slice()
|
|
812
|
+
if (target === 0) {
|
|
813
|
+
// The main buffer was rebuilt; the extra streams ride along.
|
|
814
|
+
} else if (target === streams.length) {
|
|
815
|
+
extra.push({ layout, vertices })
|
|
816
|
+
} else {
|
|
817
|
+
extra[target - 1] = { layout, vertices }
|
|
818
|
+
}
|
|
819
|
+
let out: Geometry = {
|
|
820
|
+
vertices: target === 0 ? vertices : geometry.vertices,
|
|
272
821
|
indices: geometry.indices,
|
|
273
|
-
|
|
274
|
-
|
|
822
|
+
topology: geometry.topology,
|
|
823
|
+
layout: target === 0 ? layout : geometry.layout,
|
|
824
|
+
label: options.label ?? (geometry.label ? geometry.label + "-" + attr.name : undefined),
|
|
275
825
|
}
|
|
826
|
+
if (extra.length > 0) out.streams = extra
|
|
827
|
+
return out
|
|
276
828
|
}
|
|
277
829
|
|
|
278
830
|
/**
|
|
279
|
-
* Derive a "colored"-layout geometry from a
|
|
280
|
-
* positions, normals, uvs and indices, plus an aColor
|
|
281
|
-
* the data channel for materials whose vertex stage reads `in
|
|
282
|
-
* (a tint, baked ambient occlusion, any four scalars; the
|
|
283
|
-
* standard vocabulary, the contents are yours
|
|
284
|
-
*
|
|
831
|
+
* Derive a "colored"-layout geometry from a base one: the same
|
|
832
|
+
* positions, normals, uvs and indices, plus an aColor float32x4 per
|
|
833
|
+
* vertex - the data channel for materials whose vertex stage reads `in
|
|
834
|
+
* vec4 aColor` (a tint, baked ambient occlusion, any four scalars; the
|
|
835
|
+
* name is the standard vocabulary, the contents are yours - raw floats,
|
|
836
|
+
* so a tint the stock materials read under `vertexColors` is
|
|
837
|
+
* premultiplied linear: encode an sRGB pick with premultipliedColor).
|
|
838
|
+
* `withAttribute` with the aColor channel; the "colored" preset name is
|
|
839
|
+
* kept on the result. For a 4-byte color use `withAttribute` with
|
|
840
|
+
* `unorm8x4` directly.
|
|
285
841
|
*/
|
|
286
842
|
export function withColors(geometry: Geometry, fill: ColorFill, label?: string): Geometry {
|
|
287
|
-
if (
|
|
843
|
+
if (geometrySlot(geometry, "aColor") !== null) {
|
|
288
844
|
throw new Error("withColors: geometry already carries an aColor channel")
|
|
289
845
|
}
|
|
290
|
-
let out = withAttribute(geometry, { name: "aColor", format: "
|
|
846
|
+
let out = withAttribute(geometry, { name: "aColor", format: "float32x4" }, fill, { label: label ?? (geometry.label ? geometry.label + "-colored" : undefined) })
|
|
291
847
|
if (layoutKey(out.layout) === layoutKey("colored")) out.layout = "colored"
|
|
292
848
|
return out
|
|
293
849
|
}
|
|
294
850
|
|
|
295
851
|
/**
|
|
296
852
|
* The in-place primitive under withAttribute: write one channel the
|
|
297
|
-
* geometry
|
|
298
|
-
* overwrites an existing one). The pos/normal/uv the
|
|
299
|
-
* are read from the
|
|
300
|
-
* writing hands the baker world-space vertices.
|
|
301
|
-
* [first, first + count) - count defaults to the rest of
|
|
302
|
-
* and `fill` indexes relative to `first`, so a per-part
|
|
303
|
-
* unchanged for both APIs. Returns
|
|
853
|
+
* geometry already carries, in whichever stream (withAttribute ADDS a
|
|
854
|
+
* channel; this overwrites an existing one). The pos/normal/uv the
|
|
855
|
+
* callback receives are read from the geometry itself, so a builder
|
|
856
|
+
* baking transforms while writing hands the baker world-space vertices.
|
|
857
|
+
* Fills vertices [first, first + count) - count defaults to the rest of
|
|
858
|
+
* the buffer - and `fill` indexes relative to `first`, so a per-part
|
|
859
|
+
* callback works unchanged for both APIs. Returns the vertices of the
|
|
860
|
+
* stream holding the channel (what updateVertices re-uploads).
|
|
304
861
|
*/
|
|
305
|
-
export function fillAttribute(geometry: Geometry, name: string, fill: AttributeFill, first = 0, count?: number):
|
|
306
|
-
|
|
862
|
+
export function fillAttribute(geometry: Geometry, name: string, fill: AttributeFill, first = 0, count?: number): ArrayBufferView {
|
|
863
|
+
let slot = geometrySlot(geometry, name)
|
|
864
|
+
if (slot === null) throw new Error("fillAttribute: geometry has no '" + name + "' attribute")
|
|
865
|
+
let total = geometryVertexCount(geometry, "fillAttribute")
|
|
866
|
+
fillWith(geometryAttribute(geometry, name)!, geometry, total, name, fill, first, count)
|
|
867
|
+
return geometryStreams(geometry)[slot.stream]!.vertices
|
|
307
868
|
}
|
|
308
869
|
|
|
309
|
-
/** The
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
function
|
|
314
|
-
let slot = layoutSlot(layout, name)
|
|
315
|
-
if (slot === null) throw new Error("fillAttribute: layout has no '" + name + "' attribute")
|
|
316
|
-
let stride = layoutStride(layout)
|
|
317
|
-
if (vertices.length % stride !== 0) {
|
|
318
|
-
throw new Error("fillAttribute: vertex data is not a whole number of " + layoutKey(layout) + " vertices")
|
|
319
|
-
}
|
|
320
|
-
let total = vertices.length / stride
|
|
870
|
+
/** The write loop behind withAttribute and fillAttribute: `slot` is the
|
|
871
|
+
* accessor written, `source` the geometry the callback's standard
|
|
872
|
+
* channels are read from (zeros for a channel it lacks), `total` its
|
|
873
|
+
* vertex count. */
|
|
874
|
+
function fillWith(slot: AttributeAccess, source: Geometry, total: number, name: string, fill: AttributeFill, first: number, count?: number): void {
|
|
321
875
|
let n = count ?? total - first
|
|
322
876
|
if (!Number.isInteger(first) || !Number.isInteger(n) || first < 0 || n < 0 || first + n > total) {
|
|
323
877
|
throw new Error("fillAttribute: range [" + first + ", " + (first + n) + ") is outside the buffer's " + total + " vertices")
|
|
324
878
|
}
|
|
325
|
-
let size = slot.
|
|
879
|
+
let size = slot.components
|
|
326
880
|
let fn = typeof fill === "function" ? fill : null
|
|
327
881
|
let flat = typeof fill === "function" ? null : fill
|
|
328
882
|
if (flat !== null && flat.length !== n * size) {
|
|
329
|
-
throw new Error("fillAttribute: fill has " + flat.length + "
|
|
883
|
+
throw new Error("fillAttribute: fill has " + flat.length + " values, expected " + size + " per vertex (" + n * size + ")")
|
|
330
884
|
}
|
|
885
|
+
let pos = geometryAttribute(source, "aPos")!
|
|
886
|
+
let nrm = geometryAttribute(source, "aNormal") ?? ZERO_ACCESS
|
|
887
|
+
let uv = geometryAttribute(source, "aUV") ?? ZERO_ACCESS
|
|
331
888
|
for (let i = 0; i < n; i++) {
|
|
332
|
-
let
|
|
889
|
+
let v = first + i
|
|
333
890
|
let value: ArrayLike<number>
|
|
334
891
|
let s: number
|
|
335
892
|
if (fn !== null) {
|
|
336
|
-
value = fn(i, [
|
|
893
|
+
value = fn(i, [pos.get(v, 0), pos.get(v, 1), pos.get(v, 2)], [nrm.get(v, 0), nrm.get(v, 1), nrm.get(v, 2)], [uv.get(v, 0), uv.get(v, 1)])
|
|
337
894
|
s = 0
|
|
338
895
|
if (value.length !== size) {
|
|
339
|
-
throw new Error("fillAttribute: fill callback returned " + value.length + "
|
|
896
|
+
throw new Error("fillAttribute: fill callback returned " + value.length + " values for '" + name + "', expected " + size)
|
|
340
897
|
}
|
|
341
898
|
} else {
|
|
342
899
|
value = flat!
|
|
343
900
|
s = i * size
|
|
344
901
|
}
|
|
345
|
-
for (let k = 0; k < size; k++)
|
|
902
|
+
for (let k = 0; k < size; k++) slot.set(v, k, value[s + k]!)
|
|
346
903
|
}
|
|
347
|
-
return vertices
|
|
348
904
|
}
|
|
349
905
|
|
|
350
906
|
/** `fillAttribute` for the aColor channel of a color-carrying geometry
|
|
351
907
|
* (fill is 4 per vertex). */
|
|
352
|
-
export function fillColors(geometry: Geometry, fill: ColorFill, first = 0, count?: number):
|
|
908
|
+
export function fillColors(geometry: Geometry, fill: ColorFill, first = 0, count?: number): ArrayBufferView {
|
|
353
909
|
return fillAttribute(geometry, "aColor", fill, first, count)
|
|
354
910
|
}
|
|
355
911
|
|
|
@@ -358,9 +914,11 @@ export function fillColors(geometry: Geometry, fill: ColorFill, first = 0, count
|
|
|
358
914
|
* quaternion, not both; number = uniform scale; absent = identity) into a
|
|
359
915
|
* geometry: a new geometry (the source is
|
|
360
916
|
* untouched, its GPU buffers stay independent) whose positions are moved
|
|
361
|
-
* by the transform and whose normals
|
|
362
|
-
*
|
|
363
|
-
*
|
|
917
|
+
* by the transform and whose normals (when the layout carries aNormal)
|
|
918
|
+
* follow through the inverse-transpose, renormalized - correct under
|
|
919
|
+
* non-uniform scale. UVs, colors, indices and layout copy through; extra
|
|
920
|
+
* streams are shared with the source (positions and normals live in
|
|
921
|
+
* stream 0). This is Three's `geometry.applyMatrix4`, the first
|
|
364
922
|
* half of authoring a static scene as data: transform each part into place,
|
|
365
923
|
* mergeGeometries the parts, draw one mesh.
|
|
366
924
|
*/
|
|
@@ -371,39 +929,43 @@ export function transformGeometry(geometry: Geometry, transform: TransformUpdate
|
|
|
371
929
|
if (transform.scale !== undefined) updateScale(scl, transform.scale)
|
|
372
930
|
let m = compose(mat4(), transform.position ?? [0, 0, 0], rot, scl)
|
|
373
931
|
let n = normalMatrix(mat4(), m)
|
|
374
|
-
let
|
|
375
|
-
let
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
let nx = src[i + 3]!, ny = src[i + 4]!, nz = src[i + 5]!
|
|
932
|
+
let count = geometryVertexCount(geometry, "transformGeometry")
|
|
933
|
+
let out = vertexView(geometry.layout, vertexBytes(geometry.vertices).slice().buffer)
|
|
934
|
+
let pos = attributeAccess(out, geometry.layout, "aPos")!
|
|
935
|
+
let nrm = attributeAccess(out, geometry.layout, "aNormal")
|
|
936
|
+
for (let i = 0; i < count; i++) {
|
|
937
|
+
let x = pos.get(i, 0), y = pos.get(i, 1), z = pos.get(i, 2)
|
|
938
|
+
pos.set(i, 0, m[0] * x + m[4] * y + m[8] * z + m[12])
|
|
939
|
+
pos.set(i, 1, m[1] * x + m[5] * y + m[9] * z + m[13])
|
|
940
|
+
pos.set(i, 2, m[2] * x + m[6] * y + m[10] * z + m[14])
|
|
941
|
+
if (nrm === null) continue
|
|
942
|
+
let nx = nrm.get(i, 0), ny = nrm.get(i, 1), nz = nrm.get(i, 2)
|
|
386
943
|
let tx = n[0] * nx + n[4] * ny + n[8] * nz
|
|
387
944
|
let ty = n[1] * nx + n[5] * ny + n[9] * nz
|
|
388
945
|
let tz = n[2] * nx + n[6] * ny + n[10] * nz
|
|
389
946
|
let len = Math.hypot(tx, ty, tz) || 1
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
947
|
+
nrm.set(i, 0, tx / len)
|
|
948
|
+
nrm.set(i, 1, ty / len)
|
|
949
|
+
nrm.set(i, 2, tz / len)
|
|
393
950
|
}
|
|
394
|
-
|
|
951
|
+
let result: Geometry = {
|
|
395
952
|
vertices: out,
|
|
396
953
|
indices: geometry.indices,
|
|
954
|
+
topology: geometry.topology,
|
|
397
955
|
layout: geometry.layout,
|
|
398
956
|
label: label ?? (geometry.label ? geometry.label + "-transformed" : undefined),
|
|
399
957
|
}
|
|
958
|
+
if (geometry.streams !== undefined) result.streams = geometry.streams
|
|
959
|
+
return result
|
|
400
960
|
}
|
|
401
961
|
|
|
402
962
|
/**
|
|
403
963
|
* Concatenate geometries into one: vertices appended in order, indices
|
|
404
964
|
* offset to match, uint32 indices past 64k vertices. Every part must share
|
|
405
965
|
* one layout - a mixed list throws, because the strides differ and a merge
|
|
406
|
-
* that picked one would draw garbage, not a mesh missing a channel
|
|
966
|
+
* that picked one would draw garbage, not a mesh missing a channel - and
|
|
967
|
+
* one list topology (a mixed list throws the same way; strips cannot be
|
|
968
|
+
* concatenated at all, the seam would join them). The
|
|
407
969
|
* second half of authoring a static scene as data (Three's
|
|
408
970
|
* `BufferGeometryUtils.mergeGeometries`): the result is one draw entry and
|
|
409
971
|
* one uModel write however many parts went in, so only what actually moves
|
|
@@ -411,35 +973,679 @@ export function transformGeometry(geometry: Geometry, transform: TransformUpdate
|
|
|
411
973
|
*/
|
|
412
974
|
export function mergeGeometries(parts: Geometry[], label?: string): Geometry {
|
|
413
975
|
if (parts.length === 0) throw new Error("mergeGeometries: no parts")
|
|
414
|
-
let
|
|
415
|
-
let key =
|
|
416
|
-
let
|
|
417
|
-
|
|
976
|
+
let first = parts[0]!
|
|
977
|
+
let key = geometryKey(first)
|
|
978
|
+
let topology = geometryTopology(first)
|
|
979
|
+
if (topology === "line-strip" || topology === "triangle-strip") throw new Error("mergeGeometries: cannot merge " + topology + " geometry")
|
|
980
|
+
let vertexTotal = 0
|
|
418
981
|
let indexCount = 0
|
|
419
982
|
for (let part of parts) {
|
|
420
|
-
if (
|
|
421
|
-
throw new Error("mergeGeometries: mixed layouts (" + key + " and " +
|
|
983
|
+
if (geometryKey(part) !== key) {
|
|
984
|
+
throw new Error("mergeGeometries: mixed layouts (" + key + " and " + geometryKey(part) + ")")
|
|
422
985
|
}
|
|
423
|
-
if (part
|
|
424
|
-
throw new Error("mergeGeometries:
|
|
986
|
+
if (geometryTopology(part) !== topology) {
|
|
987
|
+
throw new Error("mergeGeometries: mixed topologies (" + topology + " and " + geometryTopology(part) + ")")
|
|
425
988
|
}
|
|
426
|
-
|
|
989
|
+
vertexTotal += geometryVertexCount(part, "mergeGeometries")
|
|
427
990
|
indexCount += part.indices.length
|
|
428
991
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
let
|
|
432
|
-
let
|
|
992
|
+
// Every stream concatenates the same way: part after part, at the
|
|
993
|
+
// stream's own stride.
|
|
994
|
+
let layouts = geometryStreams(first).map(s => s.layout)
|
|
995
|
+
let merged = layouts.map(layout => {
|
|
996
|
+
let stride = layoutStride(layout)
|
|
997
|
+
let bytes = new Uint8Array(vertexTotal * stride)
|
|
998
|
+
return { layout, stride, bytes }
|
|
999
|
+
})
|
|
1000
|
+
let indices = vertexTotal > 65535 ? new Uint32Array(indexCount) : new Uint16Array(indexCount)
|
|
1001
|
+
let base = 0
|
|
433
1002
|
let iOffset = 0
|
|
434
1003
|
for (let part of parts) {
|
|
435
|
-
|
|
436
|
-
let
|
|
1004
|
+
let streams = geometryStreams(part)
|
|
1005
|
+
for (let k = 0; k < merged.length; k++) merged[k]!.bytes.set(vertexBytes(streams[k]!.vertices), base * merged[k]!.stride)
|
|
437
1006
|
let src = part.indices
|
|
438
1007
|
for (let i = 0; i < src.length; i++) indices[iOffset + i] = src[i]! + base
|
|
439
|
-
|
|
1008
|
+
base += geometryVertexCount(part, "mergeGeometries")
|
|
440
1009
|
iOffset += src.length
|
|
441
1010
|
}
|
|
442
|
-
|
|
1011
|
+
let out: Geometry = { vertices: vertexView(first.layout, merged[0]!.bytes.buffer), indices, topology: first.topology, layout: first.layout, label }
|
|
1012
|
+
if (merged.length > 1) out.streams = merged.slice(1).map(m => ({ layout: layoutAttributes(m.layout), vertices: vertexView(m.layout, m.bytes.buffer) }))
|
|
1013
|
+
return out
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** Vertex values closer than this are equal to the welds: positions
|
|
1017
|
+
* (model units) to the edge builders, every channel to mergeVertices and
|
|
1018
|
+
* the corner weld of withNormals. Three's mergeVertices tolerance. */
|
|
1019
|
+
const WELD_PRECISION = 1e-4
|
|
1020
|
+
|
|
1021
|
+
/** Degrees between two faces' normals from which the edge between them
|
|
1022
|
+
* counts as a feature edge in edgesGeometry; under it the faces read as
|
|
1023
|
+
* one smooth surface and the edge stays hidden. Three's default. */
|
|
1024
|
+
const EDGES_THRESHOLD_ANGLE = 1
|
|
1025
|
+
|
|
1026
|
+
/** Degrees between two faces' normals from which the edge between them
|
|
1027
|
+
* shades as a crease in withNormals; under it the faces blend into one
|
|
1028
|
+
* smooth surface. Three's toCreasedNormals default and Unity's import
|
|
1029
|
+
* smoothing angle. */
|
|
1030
|
+
const CREASE_ANGLE = 60
|
|
1031
|
+
|
|
1032
|
+
/** One id per vertex, shared by every vertex whose `channels` agree to
|
|
1033
|
+
* `precision` in every component; plus the id count. Ids are dense, in
|
|
1034
|
+
* first-occurrence order. The edge builders weld on the position alone
|
|
1035
|
+
* (a uv seam or a per-face normal split is one edge whichever copy a
|
|
1036
|
+
* triangle names); mergeVertices welds on every channel. */
|
|
1037
|
+
function weldVertices(geometry: Geometry, channels: AttributeAccess[], precision: number, where: string): { ids: Uint32Array; count: number } {
|
|
1038
|
+
let count = geometryVertexCount(geometry, where)
|
|
1039
|
+
let ids = new Uint32Array(count)
|
|
1040
|
+
let seen = new Map<string, number>()
|
|
1041
|
+
let scale = 1 / precision
|
|
1042
|
+
for (let i = 0; i < count; i++) {
|
|
1043
|
+
let key = ""
|
|
1044
|
+
for (let c of channels) {
|
|
1045
|
+
for (let k = 0; k < c.components; k++) key += Math.round(c.get(i, k) * scale) + ","
|
|
1046
|
+
}
|
|
1047
|
+
let id = seen.get(key)
|
|
1048
|
+
if (id === undefined) {
|
|
1049
|
+
id = seen.size
|
|
1050
|
+
seen.set(key, id)
|
|
1051
|
+
}
|
|
1052
|
+
ids[i] = id
|
|
1053
|
+
}
|
|
1054
|
+
return { ids, count: seen.size }
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function weldPositions(geometry: Geometry, where: string): { ids: Uint32Array; count: number } {
|
|
1058
|
+
return weldVertices(geometry, [geometryAttribute(geometry, "aPos")!], WELD_PRECISION, where)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** An accessor per attribute of every stream, in layout order. */
|
|
1062
|
+
function allChannels(geometry: Geometry): AttributeAccess[] {
|
|
1063
|
+
let out: AttributeAccess[] = []
|
|
1064
|
+
for (let stream of geometryStreams(geometry)) {
|
|
1065
|
+
for (let attr of layoutAttributes(stream.layout)) out.push(attributeAccess(stream.vertices, stream.layout, attr.name)!)
|
|
1066
|
+
}
|
|
1067
|
+
return out
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/** The edge table of a triangle geometry, welded by position, as a lines
|
|
1071
|
+
* index list: every edge once for `cosThreshold` null, else the feature
|
|
1072
|
+
* edges - those with one face (a border) or whose two faces' normals
|
|
1073
|
+
* dot at or below the threshold. Each edge names the vertex pair of the
|
|
1074
|
+
* first triangle that had it. */
|
|
1075
|
+
function edgeIndices(geometry: Geometry, name: string, cosThreshold: number | null): Uint16Array | Uint32Array {
|
|
1076
|
+
if (geometryTopology(geometry) !== "triangles") {
|
|
1077
|
+
throw new Error(name + ": needs a triangle geometry, got " + geometryTopology(geometry))
|
|
1078
|
+
}
|
|
1079
|
+
let pos = geometryAttribute(geometry, "aPos")!
|
|
1080
|
+
let weld = weldPositions(geometry, name)
|
|
1081
|
+
let src = geometry.indices
|
|
1082
|
+
type Edge = { a: number; b: number; nx: number; ny: number; nz: number; faces: number; sharp: boolean }
|
|
1083
|
+
let edges = new Map<number, Edge>()
|
|
1084
|
+
for (let t = 0; t + 2 < src.length; t += 3) {
|
|
1085
|
+
let i0 = src[t]!, i1 = src[t + 1]!, i2 = src[t + 2]!
|
|
1086
|
+
// The face normal, for the feature test.
|
|
1087
|
+
let ux = pos.get(i1, 0) - pos.get(i0, 0), uy = pos.get(i1, 1) - pos.get(i0, 1), uz = pos.get(i1, 2) - pos.get(i0, 2)
|
|
1088
|
+
let wx = pos.get(i2, 0) - pos.get(i0, 0), wy = pos.get(i2, 1) - pos.get(i0, 1), wz = pos.get(i2, 2) - pos.get(i0, 2)
|
|
1089
|
+
let nx = uy * wz - uz * wy, ny = uz * wx - ux * wz, nz = ux * wy - uy * wx
|
|
1090
|
+
let len = Math.hypot(nx, ny, nz) || 1
|
|
1091
|
+
nx /= len
|
|
1092
|
+
ny /= len
|
|
1093
|
+
nz /= len
|
|
1094
|
+
for (let k = 0; k < 3; k++) {
|
|
1095
|
+
let ia = k === 0 ? i0 : k === 1 ? i1 : i2
|
|
1096
|
+
let ib = k === 0 ? i1 : k === 1 ? i2 : i0
|
|
1097
|
+
let wa = weld.ids[ia]!, wb = weld.ids[ib]!
|
|
1098
|
+
if (wa === wb) continue
|
|
1099
|
+
let key = Math.min(wa, wb) * weld.count + Math.max(wa, wb)
|
|
1100
|
+
let e = edges.get(key)
|
|
1101
|
+
if (e === undefined) {
|
|
1102
|
+
edges.set(key, { a: ia, b: ib, nx, ny, nz, faces: 1, sharp: false })
|
|
1103
|
+
} else {
|
|
1104
|
+
e.faces++
|
|
1105
|
+
if (cosThreshold !== null && !e.sharp && e.nx * nx + e.ny * ny + e.nz * nz <= cosThreshold) e.sharp = true
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
let out: number[] = []
|
|
1110
|
+
for (let e of edges.values()) {
|
|
1111
|
+
if (cosThreshold === null || e.faces === 1 || e.sharp) out.push(e.a, e.b)
|
|
1112
|
+
}
|
|
1113
|
+
return packIndices(out, weld.ids.length)
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Every edge of a triangle geometry as a "lines" geometry over the SAME
|
|
1118
|
+
* vertices (Three's WireframeGeometry): each triangle's three sides, once
|
|
1119
|
+
* each - edges are matched by position, so a uv seam or a per-face
|
|
1120
|
+
* normal split draws one line, not two on top of each other. The
|
|
1121
|
+
* source's vertex array and layout are shared by reference, and so is
|
|
1122
|
+
* their GPU upload, which makes the wireframe a second index buffer over
|
|
1123
|
+
* the same vertices: a "skinned" part's wireframe draws in its pose
|
|
1124
|
+
* under `unlit({ skinned: true })` with the mesh's own palette, and any
|
|
1125
|
+
* material draws it as lines because the topology rides on the
|
|
1126
|
+
* geometry. Lines are one pixel wide.
|
|
1127
|
+
*/
|
|
1128
|
+
export function wireframeGeometry(geometry: Geometry, label?: string): Geometry {
|
|
1129
|
+
return overVertices(geometry, edgeIndices(geometry, "wireframeGeometry", null), label ?? (geometry.label ? geometry.label + "-wireframe" : undefined))
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// A "lines" geometry over the same vertices (every stream shared) as its
|
|
1133
|
+
// source, with its own indices: the edge builders' tail.
|
|
1134
|
+
function overVertices(geometry: Geometry, indices: Uint16Array | Uint32Array, label: string | undefined): Geometry {
|
|
1135
|
+
let out: Geometry = { vertices: geometry.vertices, indices, topology: "lines", layout: geometry.layout, label }
|
|
1136
|
+
if (geometry.streams !== undefined) out.streams = geometry.streams
|
|
1137
|
+
return out
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* The feature edges of a triangle geometry as a "lines" geometry over the
|
|
1142
|
+
* same vertices (Three's EdgesGeometry): an edge draws when its two faces
|
|
1143
|
+
* meet at `thresholdAngle` degrees or more, or when only one face has it
|
|
1144
|
+
* (an open border): the outline that a dense mesh's wireframe, a solid
|
|
1145
|
+
* blob, cannot give. A box draws its twelve edges and no face diagonals
|
|
1146
|
+
* at any threshold; a generated round shape is faceted, so its facet
|
|
1147
|
+
* lines show until the threshold passes its facet angle (360 divided by
|
|
1148
|
+
* the radial segments: a default cylinder loses its side seams and keeps
|
|
1149
|
+
* its two rims from 16 degrees, a default sphere draws nothing from 16).
|
|
1150
|
+
* A smooth closed shape above its facet angle has no edges at all, and
|
|
1151
|
+
* the empty lines geometry that comes back attaches and draws nothing.
|
|
1152
|
+
* Shares vertices, layout and GPU upload with the source like
|
|
1153
|
+
* wireframeGeometry.
|
|
1154
|
+
*/
|
|
1155
|
+
export function edgesGeometry(geometry: Geometry, thresholdAngle = EDGES_THRESHOLD_ANGLE, label?: string): Geometry {
|
|
1156
|
+
return overVertices(geometry, edgeIndices(geometry, "edgesGeometry", Math.cos((thresholdAngle * Math.PI) / 180)), label ?? (geometry.label ? geometry.label + "-edges" : undefined))
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/** The identity index list over `count` vertices, in the width
|
|
1160
|
+
* packIndices picks. */
|
|
1161
|
+
function identityIndices(count: number): Uint16Array | Uint32Array {
|
|
1162
|
+
let out = count > 65535 ? new Uint32Array(count) : new Uint16Array(count)
|
|
1163
|
+
for (let i = 0; i < count; i++) out[i] = i
|
|
1164
|
+
return out
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/** Every stream of `geometry` rebuilt over `count` vertices, vertex `i`
|
|
1168
|
+
* of the result copied from source vertex `from[i]` - the gather behind
|
|
1169
|
+
* toNonIndexed (from = the index list) and mergeVertices (from = the
|
|
1170
|
+
* first vertex of each weld id). Throws on a source index out of range,
|
|
1171
|
+
* which the O(n) pass gets for free. */
|
|
1172
|
+
function gatherVertices(geometry: Geometry, from: ArrayLike<number>, count: number, where: string): ArrayBufferView[] {
|
|
1173
|
+
let total = geometryVertexCount(geometry, where)
|
|
1174
|
+
return geometryStreams(geometry).map(stream => {
|
|
1175
|
+
let stride = layoutStride(stream.layout)
|
|
1176
|
+
let src = vertexBytes(stream.vertices)
|
|
1177
|
+
let dst = new Uint8Array(count * stride)
|
|
1178
|
+
for (let i = 0; i < count; i++) {
|
|
1179
|
+
let v = from[i]!
|
|
1180
|
+
if (v >= total) throw new Error(where + ": index " + v + " is outside the geometry's " + total + " vertices")
|
|
1181
|
+
dst.set(src.subarray(v * stride, (v + 1) * stride), i * stride)
|
|
1182
|
+
}
|
|
1183
|
+
return vertexView(stream.layout, dst.buffer)
|
|
1184
|
+
})
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// A geometry over gathered streams with its own indices: the tail of the
|
|
1188
|
+
// index ops. Layout and topology copy through.
|
|
1189
|
+
function overGathered(geometry: Geometry, gathered: ArrayBufferView[], indices: Uint16Array | Uint32Array, label: string | undefined): Geometry {
|
|
1190
|
+
let out: Geometry = { vertices: gathered[0]!, indices, topology: geometry.topology, layout: geometry.layout, label }
|
|
1191
|
+
if (geometry.streams !== undefined) out.streams = geometry.streams.map((s, k) => ({ layout: s.layout, vertices: gathered[k + 1]! }))
|
|
1192
|
+
return out
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Every index its own vertex (Three's toNonIndexed, Godot's
|
|
1197
|
+
* SurfaceTool.deindex): a new geometry of `indices.length` vertices, each
|
|
1198
|
+
* stream gathered through the index list, whose own indices are the
|
|
1199
|
+
* identity 0..n-1 - a draw entry always indexes here, so "non-indexed"
|
|
1200
|
+
* means no vertex is shared. What per-face data needs: after the split a
|
|
1201
|
+
* face's three corners are its own, so computeVertexNormals gives flat
|
|
1202
|
+
* shading and fillAttribute can write a per-face color. Topology copies
|
|
1203
|
+
* through (a strip stays a strip); mergeVertices is the inverse. Throws
|
|
1204
|
+
* on a morphed geometry: the sparse packing is per base vertex, so split
|
|
1205
|
+
* before withMorphTargets.
|
|
1206
|
+
*/
|
|
1207
|
+
export function toNonIndexed(geometry: Geometry, label?: string): Geometry {
|
|
1208
|
+
if (geometry.morphs !== undefined) throw new Error("toNonIndexed: geometry carries morph targets; split before withMorphTargets")
|
|
1209
|
+
let n = geometry.indices.length
|
|
1210
|
+
return overGathered(geometry, gatherVertices(geometry, geometry.indices, n, "toNonIndexed"), identityIndices(n), label ?? (geometry.label ? geometry.label + "-nonindexed" : undefined))
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Weld the vertices that agree in every channel of every stream to
|
|
1215
|
+
* `tolerance` (model units for positions, the same absolute step for
|
|
1216
|
+
* every other channel) into one (Three's BufferGeometryUtils.mergeVertices,
|
|
1217
|
+
* Godot's SurfaceTool.index): a new geometry keeping the first copy of
|
|
1218
|
+
* each in first-occurrence order, indices remapped, so a triangle soup
|
|
1219
|
+
* (an STL, a hand-written face list, a toNonIndexed result) becomes an
|
|
1220
|
+
* indexed mesh whose shared vertices computeVertexNormals averages
|
|
1221
|
+
* across. Vertices that differ only in a normal or a uv stay apart: this
|
|
1222
|
+
* is the exact inverse of toNonIndexed, not a position weld, so a seam
|
|
1223
|
+
* or a crease survives it (withNormals is the op that decides creases).
|
|
1224
|
+
* Faces are kept as they are, degenerate ones included. Throws on a
|
|
1225
|
+
* morphed geometry like toNonIndexed.
|
|
1226
|
+
*/
|
|
1227
|
+
export function mergeVertices(geometry: Geometry, tolerance = WELD_PRECISION, label?: string): Geometry {
|
|
1228
|
+
if (geometry.morphs !== undefined) throw new Error("mergeVertices: geometry carries morph targets; weld before withMorphTargets")
|
|
1229
|
+
if (!(tolerance > 0)) throw new Error("mergeVertices: tolerance must be positive, got " + tolerance)
|
|
1230
|
+
let weld = weldVertices(geometry, allChannels(geometry), tolerance, "mergeVertices")
|
|
1231
|
+
let first = new Uint32Array(weld.count)
|
|
1232
|
+
let seen = new Uint8Array(weld.count)
|
|
1233
|
+
for (let i = 0; i < weld.ids.length; i++) {
|
|
1234
|
+
let id = weld.ids[i]!
|
|
1235
|
+
if (seen[id] === 0) {
|
|
1236
|
+
seen[id] = 1
|
|
1237
|
+
first[id] = i
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
let src = geometry.indices
|
|
1241
|
+
let indices = weld.count > 65535 ? new Uint32Array(src.length) : new Uint16Array(src.length)
|
|
1242
|
+
for (let i = 0; i < src.length; i++) {
|
|
1243
|
+
let id = weld.ids[src[i]!]
|
|
1244
|
+
if (id === undefined) throw new Error("mergeVertices: index " + src[i] + " is outside the geometry's " + weld.ids.length + " vertices")
|
|
1245
|
+
indices[i] = id
|
|
1246
|
+
}
|
|
1247
|
+
return overGathered(geometry, gatherVertices(geometry, first, weld.count, "mergeVertices"), indices, label ?? (geometry.label ? geometry.label + "-merged" : undefined))
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** Per face its unit normal (3 floats per face) and per corner the
|
|
1251
|
+
* angle the face makes there (radians, 3 per face): the weight every
|
|
1252
|
+
* normal computed here sums with. Weighting by the corner angle rather
|
|
1253
|
+
* than by face area (Three's computeVertexNormals) or not at all (Godot)
|
|
1254
|
+
* makes the result independent of how a surface was triangulated: a
|
|
1255
|
+
* quad split into two triangles contributes the same 90 degrees at each
|
|
1256
|
+
* corner whichever way its diagonal runs, so a fully smoothed cube gets
|
|
1257
|
+
* exact corner diagonals where an area sum leans toward the faces whose
|
|
1258
|
+
* diagonal touches the corner. A degenerate face has a zero normal and
|
|
1259
|
+
* zero angles, so it counts for nothing. */
|
|
1260
|
+
function faceFrames(geometry: Geometry, where: string): { normals: Float32Array; angles: Float32Array } {
|
|
1261
|
+
let count = geometryVertexCount(geometry, where)
|
|
1262
|
+
let pos = geometryAttribute(geometry, "aPos")!
|
|
1263
|
+
let src = geometry.indices
|
|
1264
|
+
let faces = Math.floor(src.length / 3)
|
|
1265
|
+
let normals = new Float32Array(faces * 3)
|
|
1266
|
+
let angles = new Float32Array(faces * 3)
|
|
1267
|
+
let corner = (a2: number, b2: number, c2: number): number => {
|
|
1268
|
+
// The angle opposite side b, by the law of cosines; 0 for a corner
|
|
1269
|
+
// with a zero-length side.
|
|
1270
|
+
let denom = 2 * Math.sqrt(a2 * c2)
|
|
1271
|
+
return denom === 0 ? 0 : Math.acos(Math.min(1, Math.max(-1, (a2 + c2 - b2) / denom)))
|
|
1272
|
+
}
|
|
1273
|
+
for (let f = 0; f < faces; f++) {
|
|
1274
|
+
let i0 = src[f * 3]!, i1 = src[f * 3 + 1]!, i2 = src[f * 3 + 2]!
|
|
1275
|
+
if (i0 >= count || i1 >= count || i2 >= count) throw new Error(where + ": face " + f + " names a vertex outside the geometry's " + count)
|
|
1276
|
+
let x0 = pos.get(i0, 0), y0 = pos.get(i0, 1), z0 = pos.get(i0, 2)
|
|
1277
|
+
let x1 = pos.get(i1, 0), y1 = pos.get(i1, 1), z1 = pos.get(i1, 2)
|
|
1278
|
+
let x2 = pos.get(i2, 0), y2 = pos.get(i2, 1), z2 = pos.get(i2, 2)
|
|
1279
|
+
let ux = x1 - x0, uy = y1 - y0, uz = z1 - z0
|
|
1280
|
+
let wx = x2 - x0, wy = y2 - y0, wz = z2 - z0
|
|
1281
|
+
let nx = uy * wz - uz * wy, ny = uz * wx - ux * wz, nz = ux * wy - uy * wx
|
|
1282
|
+
let len = Math.hypot(nx, ny, nz)
|
|
1283
|
+
if (len === 0) continue
|
|
1284
|
+
normals[f * 3] = nx / len
|
|
1285
|
+
normals[f * 3 + 1] = ny / len
|
|
1286
|
+
normals[f * 3 + 2] = nz / len
|
|
1287
|
+
// Squared side lengths: a = i0-i1, b = i1-i2, c = i2-i0.
|
|
1288
|
+
let a2 = ux * ux + uy * uy + uz * uz
|
|
1289
|
+
let b2 = (x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2
|
|
1290
|
+
let c2 = wx * wx + wy * wy + wz * wz
|
|
1291
|
+
angles[f * 3] = corner(a2, b2, c2)
|
|
1292
|
+
angles[f * 3 + 1] = corner(a2, c2, b2)
|
|
1293
|
+
angles[f * 3 + 2] = corner(b2, a2, c2)
|
|
1294
|
+
}
|
|
1295
|
+
return { normals, angles }
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/** Write the unit vector along (x, y, z) as vertex `i`'s normal; a zero
|
|
1299
|
+
* sum (a vertex no face names, or only degenerate ones) writes zero. */
|
|
1300
|
+
function writeUnit(nrm: AttributeAccess, i: number, x: number, y: number, z: number): void {
|
|
1301
|
+
let len = Math.hypot(x, y, z) || 1
|
|
1302
|
+
nrm.set(i, 0, x / len)
|
|
1303
|
+
nrm.set(i, 1, y / len)
|
|
1304
|
+
nrm.set(i, 2, z / len)
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* A copy with normals computed from its faces, creased where faces meet
|
|
1309
|
+
* at `creaseAngle` degrees or more (Three's toCreasedNormals, Unity's
|
|
1310
|
+
* import smoothing angle; Godot's smooth groups are the same decision
|
|
1311
|
+
* made per vertex): the authoring path, where computeVertexNormals
|
|
1312
|
+
* (geometry-gpu.ts, the core-backed in-place recompute) is the per-frame
|
|
1313
|
+
* one. Faces are matched by POSITION, so a triangle soup,
|
|
1314
|
+
* a hand-written face list, a mergeGeometries result and a geometry
|
|
1315
|
+
* with no aNormal channel at all (one is added, float32x3, after the
|
|
1316
|
+
* existing channels) all work, and a uv seam shades smooth across it.
|
|
1317
|
+
* 0 is flat shading, 180 smooths everything, the default keeps a
|
|
1318
|
+
* hard-edged mesh hard. Each face corner takes the corner-angle-weighted
|
|
1319
|
+
* sum of the faces at its position within the angle of its own face,
|
|
1320
|
+
* then the corners weld back by every channel, so the result is indexed
|
|
1321
|
+
* and split only where a crease or a seam needs it: a box comes out at
|
|
1322
|
+
* its 24 vertices under 90 degrees, and at 180 its normals become the
|
|
1323
|
+
* exact corner diagonals (corners whose uvs coincide weld too). Composed
|
|
1324
|
+
* from three exported ops - toNonIndexed, the corner normals,
|
|
1325
|
+
* mergeVertices - so every stream is rebuilt (a copy) and a morphed
|
|
1326
|
+
* geometry throws like toNonIndexed.
|
|
1327
|
+
*/
|
|
1328
|
+
export function withNormals(geometry: Geometry, creaseAngle = CREASE_ANGLE, label?: string): Geometry {
|
|
1329
|
+
if (geometryTopology(geometry) !== "triangles") throw new Error("withNormals: needs a triangle geometry, got " + geometryTopology(geometry))
|
|
1330
|
+
if (!(creaseAngle >= 0 && creaseAngle <= 180)) throw new Error("withNormals: creaseAngle must be 0..180 degrees, got " + creaseAngle)
|
|
1331
|
+
let count = geometryVertexCount(geometry, "withNormals")
|
|
1332
|
+
let source = geometrySlot(geometry, "aNormal") === null ? withAttribute(geometry, { name: "aNormal", format: "float32x3" }, new Float32Array(count * 3)) : geometry
|
|
1333
|
+
let slot = geometrySlot(source, "aNormal")!
|
|
1334
|
+
// Written through the accessor, so any float-kind format with room for
|
|
1335
|
+
// three components takes the result, a packed snorm16x4 included.
|
|
1336
|
+
if (VERTEX_FORMATS[slot.format].kind !== "float" || slot.components < 3) {
|
|
1337
|
+
throw new Error("withNormals: aNormal is " + slot.format + ", not a float format of three or more components")
|
|
1338
|
+
}
|
|
1339
|
+
let split = toNonIndexed(source)
|
|
1340
|
+
let { normals, angles } = faceFrames(split, "withNormals")
|
|
1341
|
+
let corners = angles.length
|
|
1342
|
+
// The corners at each welded position, as a CSR table: corner c of the
|
|
1343
|
+
// split geometry is its vertex c and belongs to face c / 3.
|
|
1344
|
+
let weld = weldPositions(split, "withNormals")
|
|
1345
|
+
let starts = new Uint32Array(weld.count + 1)
|
|
1346
|
+
for (let c = 0; c < corners; c++) starts[weld.ids[c]! + 1] = starts[weld.ids[c]! + 1]! + 1
|
|
1347
|
+
for (let p = 0; p < weld.count; p++) starts[p + 1] = starts[p + 1]! + starts[p]!
|
|
1348
|
+
let fill = starts.slice(0, weld.count)
|
|
1349
|
+
let at = new Uint32Array(corners)
|
|
1350
|
+
for (let c = 0; c < corners; c++) {
|
|
1351
|
+
let p = weld.ids[c]!
|
|
1352
|
+
at[fill[p]!] = c
|
|
1353
|
+
fill[p] = fill[p]! + 1
|
|
1354
|
+
}
|
|
1355
|
+
let cosCrease = Math.cos((creaseAngle * Math.PI) / 180)
|
|
1356
|
+
let nrm = geometryAttribute(split, "aNormal")!
|
|
1357
|
+
for (let c = 0; c < corners; c++) {
|
|
1358
|
+
let f = Math.floor(c / 3) * 3
|
|
1359
|
+
let p = weld.ids[c]!
|
|
1360
|
+
let sx = 0, sy = 0, sz = 0
|
|
1361
|
+
for (let k = starts[p]!; k < starts[p + 1]!; k++) {
|
|
1362
|
+
let other = at[k]!
|
|
1363
|
+
let g = Math.floor(other / 3) * 3
|
|
1364
|
+
// A face always counts toward its own corners: its self-dot can
|
|
1365
|
+
// fall a rounding step under 1, which the angle 0 test would miss.
|
|
1366
|
+
if (g !== f && normals[f]! * normals[g]! + normals[f + 1]! * normals[g + 1]! + normals[f + 2]! * normals[g + 2]! < cosCrease) continue
|
|
1367
|
+
let w = angles[other]!
|
|
1368
|
+
sx += normals[g]! * w
|
|
1369
|
+
sy += normals[g + 1]! * w
|
|
1370
|
+
sz += normals[g + 2]! * w
|
|
1371
|
+
}
|
|
1372
|
+
writeUnit(nrm, c, sx, sy, sz)
|
|
1373
|
+
}
|
|
1374
|
+
return mergeVertices(split, WELD_PRECISION, label ?? (geometry.label ? geometry.label + "-normals" : undefined))
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// The debug helpers: Three's GridHelper, AxesHelper, Box3Helper and
|
|
1378
|
+
// PlaneHelper as "lines" builders (Godot and Unity keep the equivalents in
|
|
1379
|
+
// the editor; with no editor and topology on the geometry they are plain
|
|
1380
|
+
// data here). A helper is a Geometry like any other: a node places it, a
|
|
1381
|
+
// material draws it. Normals and uvs fill the base prefix and mean
|
|
1382
|
+
// nothing on a line.
|
|
1383
|
+
|
|
1384
|
+
// Three's GridHelper defaults, sRGB: the subdivision lines (0x888888) and
|
|
1385
|
+
// the two through the origin (0x444444).
|
|
1386
|
+
const GRID_COLOR: [number, number, number] = [0.53, 0.53, 0.53]
|
|
1387
|
+
const GRID_CENTER_COLOR: [number, number, number] = [0.27, 0.27, 0.27]
|
|
1388
|
+
// Three's defaults: a 10 by 10 grid of unit cells.
|
|
1389
|
+
const GRID_SIZE = 10
|
|
1390
|
+
const GRID_DIVISIONS = 10
|
|
1391
|
+
// X red, Y green, Z blue: the axis colors of every engine.
|
|
1392
|
+
const AXIS_COLORS: [number, number, number][] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
|
|
1393
|
+
|
|
1394
|
+
// The layout a colored helper emits: "colored" unless the caller asked for
|
|
1395
|
+
// a wider one, which must still carry the aColor channel the helper writes.
|
|
1396
|
+
function helperLayout(options: GeometryOptions, name: string): VertexLayout {
|
|
1397
|
+
let layout = options.layout ?? "colored"
|
|
1398
|
+
if (layoutSlot(layout, "aColor") === null) throw new Error(name + ": layout has no aColor channel to write")
|
|
1399
|
+
return layout
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// The helper tail: pack, mark as lines, write the colors when there are any.
|
|
1403
|
+
function packLines(verts: number[], indices: number[] | Uint16Array | Uint32Array, options: GeometryOptions, colors?: number[]): Geometry {
|
|
1404
|
+
let geometry = packGeometry(verts, indices, options)
|
|
1405
|
+
geometry.topology = "lines"
|
|
1406
|
+
if (colors !== undefined) fillColors(geometry, colors)
|
|
1407
|
+
return geometry
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* A square grid of lines in the XZ plane at y 0, centered on the origin
|
|
1412
|
+
* (Three's GridHelper with its defaults): `divisions` cells of
|
|
1413
|
+
* `size / divisions` along each side, the subdivision lines in `color`
|
|
1414
|
+
* and the two lines through the origin in `centerColor`, which exist only
|
|
1415
|
+
* when `divisions` is even. Colors are sRGB 0..1 like a material's,
|
|
1416
|
+
* written premultiplied linear into the "colored" layout's aColor:
|
|
1417
|
+
* `unlit({ vertexColors: true })` draws them, and a material that reads no
|
|
1418
|
+
* aColor (a plain `unlit({ color })`) draws the grid in its own color.
|
|
1419
|
+
* (divisions + 1) * 4 vertices, one per line end. Lines are one pixel
|
|
1420
|
+
* wide.
|
|
1421
|
+
*/
|
|
1422
|
+
export type GridHelperOptions = GeometryOptions & {
|
|
1423
|
+
/** Side length of the square, default 10. */
|
|
1424
|
+
size?: number
|
|
1425
|
+
/** Cells along each side, default 10 (unit cells). */
|
|
1426
|
+
divisions?: number
|
|
1427
|
+
/** sRGB color of the subdivision lines, default Three's 0x888888. */
|
|
1428
|
+
color?: [number, number, number] | [number, number, number, number]
|
|
1429
|
+
/** sRGB color of the two lines through the origin (present when
|
|
1430
|
+
* `divisions` is even), default Three's 0x444444. */
|
|
1431
|
+
centerColor?: [number, number, number] | [number, number, number, number]
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
export function gridHelper(options: GridHelperOptions = {}): Geometry {
|
|
1435
|
+
let { size = GRID_SIZE, divisions = GRID_DIVISIONS, color = GRID_COLOR, centerColor = GRID_CENTER_COLOR } = options
|
|
1436
|
+
if (!(Number.isInteger(divisions) && divisions >= 1)) throw new Error("gridHelper: divisions must be a whole number of 1 or more, got " + divisions)
|
|
1437
|
+
let half = size / 2
|
|
1438
|
+
let step = size / divisions
|
|
1439
|
+
let line = premultipliedColor(color)
|
|
1440
|
+
let center = premultipliedColor(centerColor)
|
|
1441
|
+
let verts: number[] = []
|
|
1442
|
+
let colors: number[] = []
|
|
1443
|
+
for (let i = 0; i <= divisions; i++) {
|
|
1444
|
+
let k = -half + i * step
|
|
1445
|
+
let t = i / divisions
|
|
1446
|
+
// The line across x at z = k, then the line across z at x = k.
|
|
1447
|
+
verts.push(-half, 0, k, 0, 1, 0, 0, t, half, 0, k, 0, 1, 0, 1, t)
|
|
1448
|
+
verts.push(k, 0, -half, 0, 1, 0, t, 0, k, 0, half, 0, 1, 0, t, 1)
|
|
1449
|
+
let c = i * 2 === divisions ? center : line
|
|
1450
|
+
for (let end = 0; end < 4; end++) colors.push(c[0]!, c[1]!, c[2]!, c[3]!)
|
|
1451
|
+
}
|
|
1452
|
+
let indices: number[] = []
|
|
1453
|
+
for (let i = 0; i < verts.length / BASE_FLOATS; i++) indices.push(i)
|
|
1454
|
+
return packLines(verts, indices, { label: options.label, layout: helperLayout(options, "gridHelper") }, colors)
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** Three segments from the origin along +x, +y and +z, X red, Y green,
|
|
1458
|
+
* Z blue (the axis colors of every engine; Three's AxesHelper), in the
|
|
1459
|
+
* "colored" layout like gridHelper: 6 vertices. */
|
|
1460
|
+
export type AxesHelperOptions = GeometryOptions & {
|
|
1461
|
+
/** Length of each axis segment, default 1. */
|
|
1462
|
+
size?: number
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
export function axesHelper(options: AxesHelperOptions = {}): Geometry {
|
|
1466
|
+
let { size = 1 } = options
|
|
1467
|
+
let verts: number[] = []
|
|
1468
|
+
let colors: number[] = []
|
|
1469
|
+
AXIS_COLORS.forEach((axis, i) => {
|
|
1470
|
+
let d = [0, 0, 0]
|
|
1471
|
+
d[i] = 1
|
|
1472
|
+
verts.push(0, 0, 0, d[0]!, d[1]!, d[2]!, 0, 0)
|
|
1473
|
+
verts.push(d[0]! * size, d[1]! * size, d[2]! * size, d[0]!, d[1]!, d[2]!, 1, 0)
|
|
1474
|
+
let c = premultipliedColor(axis)
|
|
1475
|
+
colors.push(c[0]!, c[1]!, c[2]!, c[3]!, c[0]!, c[1]!, c[2]!, c[3]!)
|
|
1476
|
+
})
|
|
1477
|
+
return packLines(verts, [0, 1, 2, 3, 4, 5], { label: options.label, layout: helperLayout(options, "axesHelper") }, colors)
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* The twelve edges of an axis-aligned box given as `[minX, minY, minZ,
|
|
1482
|
+
* maxX, maxY, maxZ]` - what geometryBounds, a model's `bounds` and the
|
|
1483
|
+
* spatial queries return - as "lines" geometry (Three's Box3Helper): 8
|
|
1484
|
+
* vertices, 24 indices, base layout, so a plain `unlit({ color })`
|
|
1485
|
+
* draws it. The bounds are baked in local space: put the helper under the
|
|
1486
|
+
* node whose bounds they are. For a box that moves every frame do what
|
|
1487
|
+
* Three does - draw `edgesGeometry(box())`, a unit cube's edges, on a
|
|
1488
|
+
* node whose position is the box center and whose scale is its size, and
|
|
1489
|
+
* update the transform instead of rebuilding geometry.
|
|
1490
|
+
*/
|
|
1491
|
+
export function box3Helper(bounds: ArrayLike<number>, options: GeometryOptions = {}): Geometry {
|
|
1492
|
+
if (bounds.length !== 6) throw new Error("box3Helper: bounds must be [minX, minY, minZ, maxX, maxY, maxZ], got " + bounds.length + " values")
|
|
1493
|
+
let verts: number[] = []
|
|
1494
|
+
let indices: number[] = []
|
|
1495
|
+
// Corner i has bit 0 for x, bit 1 for y, bit 2 for z (set = the max
|
|
1496
|
+
// side); an edge joins two corners that differ in exactly one bit. The
|
|
1497
|
+
// normal is the corner's outward diagonal.
|
|
1498
|
+
let n = 1 / Math.sqrt(3)
|
|
1499
|
+
for (let i = 0; i < 8; i++) {
|
|
1500
|
+
let x = (i & 1) !== 0, y = (i & 2) !== 0, z = (i & 4) !== 0
|
|
1501
|
+
verts.push(bounds[x ? 3 : 0]!, bounds[y ? 4 : 1]!, bounds[z ? 5 : 2]!, x ? n : -n, y ? n : -n, z ? n : -n, 0, 0)
|
|
1502
|
+
for (let bit = 1; bit < 8; bit <<= 1) if ((i & bit) === 0) indices.push(i, i | bit)
|
|
1503
|
+
}
|
|
1504
|
+
return packLines(verts, indices, options)
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* A collision capsule's outline as "lines" geometry (Unity's capsule
|
|
1509
|
+
* collider gizmo, Godot's CapsuleShape3D gizmo; Three has no capsule
|
|
1510
|
+
* helper): the `{ a, b, radius }` volume overlap()/sweep()/moveAndSlide
|
|
1511
|
+
* take, drawn in the space it was given, so a scene-space volume goes
|
|
1512
|
+
* under a node with the identity transform like a box3Helper of query
|
|
1513
|
+
* bounds. A ring around each end of the segment, four lines between the
|
|
1514
|
+
* rings, and two half circles over each cap in perpendicular planes; a
|
|
1515
|
+
* == b is a sphere and draws its three great circles. `segments` is the
|
|
1516
|
+
* ring resolution, a multiple of 4 so the lines and arcs land on ring
|
|
1517
|
+
* points. Standard layout, normals radial from the segment.
|
|
1518
|
+
*/
|
|
1519
|
+
export type CapsuleHelperOptions = GeometryOptions & {
|
|
1520
|
+
/** Points per ring, a positive multiple of 4, default 32. */
|
|
1521
|
+
segments?: number
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
export function capsuleHelper(volume: Capsule, options: CapsuleHelperOptions = {}): Geometry {
|
|
1525
|
+
let { segments = 32 } = options
|
|
1526
|
+
if (!Number.isInteger(segments) || segments < 4 || segments % 4 !== 0) throw new Error("capsuleHelper: segments must be a positive multiple of 4")
|
|
1527
|
+
let { a, b, radius } = volume
|
|
1528
|
+
let span = sub(b, a)
|
|
1529
|
+
let single = Math.hypot(span[0], span[1], span[2]) === 0
|
|
1530
|
+
let axis = single ? [0, 1, 0] as Vec3 : normalize(span)
|
|
1531
|
+
// A frame around the axis: u from the world axis least aligned with it.
|
|
1532
|
+
let ax = Math.abs(axis[0]), ay = Math.abs(axis[1]), az = Math.abs(axis[2])
|
|
1533
|
+
let pick: Vec3 = ax <= ay && ax <= az ? [1, 0, 0] : ay <= az ? [0, 1, 0] : [0, 0, 1]
|
|
1534
|
+
let u = normalize(cross(axis, pick))
|
|
1535
|
+
let v = cross(axis, u)
|
|
1536
|
+
let verts: number[] = []
|
|
1537
|
+
let indices: number[] = []
|
|
1538
|
+
// A point on the cap sphere at `center` in direction c*p + s*q.
|
|
1539
|
+
let push = (center: Vec3, p: Vec3, q: Vec3, c: number, sn: number): void => {
|
|
1540
|
+
let nx = c * p[0] + sn * q[0]
|
|
1541
|
+
let ny = c * p[1] + sn * q[1]
|
|
1542
|
+
let nz = c * p[2] + sn * q[2]
|
|
1543
|
+
verts.push(center[0] + radius * nx, center[1] + radius * ny, center[2] + radius * nz, nx, ny, nz, 0, 0)
|
|
1544
|
+
}
|
|
1545
|
+
let ring = (center: Vec3): number => {
|
|
1546
|
+
let base = verts.length / BASE_FLOATS
|
|
1547
|
+
for (let i = 0; i < segments; i++) {
|
|
1548
|
+
let t = (i / segments) * Math.PI * 2
|
|
1549
|
+
push(center, u, v, Math.cos(t), Math.sin(t))
|
|
1550
|
+
indices.push(base + i, base + ((i + 1) % segments))
|
|
1551
|
+
}
|
|
1552
|
+
return base
|
|
1553
|
+
}
|
|
1554
|
+
// Half circle from +p over `out` (the cap's pole direction) to -p.
|
|
1555
|
+
let arc = (center: Vec3, p: Vec3, out: Vec3): void => {
|
|
1556
|
+
let base = verts.length / BASE_FLOATS
|
|
1557
|
+
let cells = segments / 2
|
|
1558
|
+
for (let i = 0; i <= cells; i++) {
|
|
1559
|
+
let t = (i / cells) * Math.PI
|
|
1560
|
+
push(center, p, out, Math.cos(t), Math.sin(t))
|
|
1561
|
+
if (i > 0) indices.push(base + i - 1, base + i)
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
let ringA = ring(a)
|
|
1565
|
+
if (!single) {
|
|
1566
|
+
let ringB = ring(b)
|
|
1567
|
+
let quarter = segments / 4
|
|
1568
|
+
for (let k = 0; k < 4; k++) indices.push(ringA + k * quarter, ringB + k * quarter)
|
|
1569
|
+
}
|
|
1570
|
+
let down: Vec3 = [-axis[0], -axis[1], -axis[2]]
|
|
1571
|
+
arc(a, u, down)
|
|
1572
|
+
arc(a, v, down)
|
|
1573
|
+
arc(b, u, axis)
|
|
1574
|
+
arc(b, v, axis)
|
|
1575
|
+
return packLines(verts, indices, options)
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* A plane marker as "lines" geometry (Three's PlaneHelper without its
|
|
1580
|
+
* translucent fill): a `size` square in the XY plane at the origin facing
|
|
1581
|
+
* +z exactly like plane(), its two diagonals, and a unit segment from the
|
|
1582
|
+
* center along the normal so the facing reads. Place it with the node
|
|
1583
|
+
* transform the way plane() is placed (Three's takes a Plane and does the
|
|
1584
|
+
* lookAt itself; there is no Plane type here). 6 vertices, 14 indices,
|
|
1585
|
+
* base layout.
|
|
1586
|
+
*/
|
|
1587
|
+
export type PlaneHelperOptions = GeometryOptions & {
|
|
1588
|
+
/** Side length of the square, default 1. */
|
|
1589
|
+
size?: number
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
export function planeHelper(options: PlaneHelperOptions = {}): Geometry {
|
|
1593
|
+
let { size = 1 } = options
|
|
1594
|
+
let h = size / 2
|
|
1595
|
+
// prettier-ignore
|
|
1596
|
+
let verts = [
|
|
1597
|
+
-h, -h, 0, 0, 0, 1, 0, 1,
|
|
1598
|
+
h, -h, 0, 0, 0, 1, 1, 1,
|
|
1599
|
+
h, h, 0, 0, 0, 1, 1, 0,
|
|
1600
|
+
-h, h, 0, 0, 0, 1, 0, 0,
|
|
1601
|
+
0, 0, 0, 0, 0, 1, 0.5, 0.5,
|
|
1602
|
+
0, 0, 1, 0, 0, 1, 0.5, 0.5,
|
|
1603
|
+
]
|
|
1604
|
+
// The outline, the two diagonals, the normal tick.
|
|
1605
|
+
return packLines(verts, [0, 1, 1, 2, 2, 3, 3, 0, 0, 2, 1, 3, 4, 5], options)
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Three's ArrowHelper proportions: the head is a fifth of the length and
|
|
1609
|
+
// a fifth as wide as it is long.
|
|
1610
|
+
const ARROW_HEAD_LENGTH = 0.2
|
|
1611
|
+
const ARROW_HEAD_WIDTH = 0.2
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* An arrow from the origin along +y as "lines" geometry (Three's
|
|
1615
|
+
* ArrowHelper, whose local arrow points up +y too, with its solid cone
|
|
1616
|
+
* replaced by a pyramid outline): the shaft, four lines from the tip to a
|
|
1617
|
+
* diamond base, and the base itself. Aim it with the node transform -
|
|
1618
|
+
* `quatFromTo(out, [0, 1, 0], direction)` is the rotation - and put the
|
|
1619
|
+
* origin in `position`; Three's constructor takes both, here they are the
|
|
1620
|
+
* node's. 7 vertices, 18 indices, base layout.
|
|
1621
|
+
*/
|
|
1622
|
+
export type ArrowHelperOptions = GeometryOptions & {
|
|
1623
|
+
/** Tip distance from the origin, default 1. */
|
|
1624
|
+
length?: number
|
|
1625
|
+
/** Head length from base to tip, default a fifth of `length`. */
|
|
1626
|
+
headLength?: number
|
|
1627
|
+
/** Head width across the base, default a fifth of `headLength`. */
|
|
1628
|
+
headWidth?: number
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
export function arrowHelper(options: ArrowHelperOptions = {}): Geometry {
|
|
1632
|
+
let { length = 1 } = options
|
|
1633
|
+
let headLength = options.headLength ?? length * ARROW_HEAD_LENGTH
|
|
1634
|
+
let headWidth = options.headWidth ?? headLength * ARROW_HEAD_WIDTH
|
|
1635
|
+
let base = length - headLength
|
|
1636
|
+
let w = headWidth / 2
|
|
1637
|
+
// prettier-ignore
|
|
1638
|
+
let verts = [
|
|
1639
|
+
0, 0, 0, 0, 1, 0, 0, 0,
|
|
1640
|
+
0, base, 0, 0, 1, 0, 0, 1,
|
|
1641
|
+
0, length, 0, 0, 1, 0, 0, 1,
|
|
1642
|
+
w, base, 0, 0, 1, 0, 1, 1,
|
|
1643
|
+
0, base, w, 0, 1, 0, 1, 1,
|
|
1644
|
+
-w, base, 0, 0, 1, 0, 1, 1,
|
|
1645
|
+
0, base, -w, 0, 1, 0, 1, 1,
|
|
1646
|
+
]
|
|
1647
|
+
// The shaft, the four tip lines, the base diamond.
|
|
1648
|
+
return packLines(verts, [0, 1, 2, 3, 2, 4, 2, 5, 2, 6, 3, 4, 4, 5, 5, 6, 6, 3], options)
|
|
443
1649
|
}
|
|
444
1650
|
|
|
445
1651
|
// Indices for a row-major (cellRows + 1) x (cellCols + 1) vertex grid: two
|
|
@@ -463,6 +1669,45 @@ function gridIndices(cellRows: number, cellCols: number, skipFirst = false, skip
|
|
|
463
1669
|
}
|
|
464
1670
|
|
|
465
1671
|
/** An axis-aligned box centered on the origin: 24 vertices, 36 indices. */
|
|
1672
|
+
/** Options of normalsHelper: the line length and color. */
|
|
1673
|
+
export type NormalsHelperOptions = GeometryOptions & {
|
|
1674
|
+
/** Length of each normal line, default 1 (Three's). */
|
|
1675
|
+
size?: number
|
|
1676
|
+
/** Line color, sRGB 0..1; default red (Three's). */
|
|
1677
|
+
color?: [number, number, number]
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Three's VertexNormalsHelper default, sRGB red.
|
|
1681
|
+
const NORMALS_HELPER_COLOR: [number, number, number] = [1, 0, 0]
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* One line per vertex from its position along its normal, `size` long
|
|
1685
|
+
* (Three's VertexNormalsHelper as a static builder like the other
|
|
1686
|
+
* helpers): 2 vertices per source vertex, "colored" layout in `color`,
|
|
1687
|
+
* drawn by `unlit({ vertexColors: true })` under the same node as the
|
|
1688
|
+
* mesh so the lines follow it. The way to see what computeVertexNormals
|
|
1689
|
+
* and withNormals produced: a crease shows as lines fanning from one
|
|
1690
|
+
* position, a smooth vertex as one line. Any layout carrying aNormal in
|
|
1691
|
+
* any format qualifies; a "skinned" mesh's helper is its rest pose.
|
|
1692
|
+
*/
|
|
1693
|
+
export function normalsHelper(geometry: Geometry, options: NormalsHelperOptions = {}): Geometry {
|
|
1694
|
+
let { size = 1, color = NORMALS_HELPER_COLOR } = options
|
|
1695
|
+
let nrm = geometryAttribute(geometry, "aNormal")
|
|
1696
|
+
if (nrm === null) throw new Error("normalsHelper: geometry has no aNormal channel")
|
|
1697
|
+
let pos = geometryAttribute(geometry, "aPos")!
|
|
1698
|
+
let count = geometryVertexCount(geometry, "normalsHelper")
|
|
1699
|
+
let c = premultipliedColor(color)
|
|
1700
|
+
let verts: number[] = []
|
|
1701
|
+
let colors: number[] = []
|
|
1702
|
+
for (let i = 0; i < count; i++) {
|
|
1703
|
+
let x = pos.get(i, 0), y = pos.get(i, 1), z = pos.get(i, 2)
|
|
1704
|
+
let nx = nrm.get(i, 0), ny = nrm.get(i, 1), nz = nrm.get(i, 2)
|
|
1705
|
+
verts.push(x, y, z, nx, ny, nz, 0, 0, x + nx * size, y + ny * size, z + nz * size, nx, ny, nz, 1, 0)
|
|
1706
|
+
colors.push(c[0]!, c[1]!, c[2]!, c[3]!, c[0]!, c[1]!, c[2]!, c[3]!)
|
|
1707
|
+
}
|
|
1708
|
+
return packLines(verts, identityIndices(count * 2), { label: options.label ?? (geometry.label ? geometry.label + "-normals-helper" : undefined), layout: helperLayout(options, "normalsHelper") }, colors)
|
|
1709
|
+
}
|
|
1710
|
+
|
|
466
1711
|
export type BoxOptions = GeometryOptions & { width?: number; height?: number; depth?: number }
|
|
467
1712
|
|
|
468
1713
|
export function box(options: BoxOptions = {}): Geometry {
|
|
@@ -475,7 +1720,7 @@ export function box(options: BoxOptions = {}): Geometry {
|
|
|
475
1720
|
type P = [number, number, number]
|
|
476
1721
|
// Corners a (bottom-left) through d (top-left), CCW seen from outside.
|
|
477
1722
|
let quad = (a: P, b: P, c: P, d: P, n: P) => {
|
|
478
|
-
let base = verts.length /
|
|
1723
|
+
let base = verts.length / BASE_FLOATS
|
|
479
1724
|
let uv = [[0, 1], [1, 1], [1, 0], [0, 0]]
|
|
480
1725
|
let corners = [a, b, c, d]
|
|
481
1726
|
for (let i = 0; i < 4; i++) {
|
|
@@ -588,17 +1833,20 @@ export function torusKnot(options: TorusKnotOptions = {}): Geometry {
|
|
|
588
1833
|
* case); side normals tilt with the taper. Side UVs: u around the
|
|
589
1834
|
* circumference, v 0 at the top to 1 at the bottom; caps get a planar
|
|
590
1835
|
* disc map. A zero radius skips that cap and the degenerate side
|
|
591
|
-
* triangles at the apex.
|
|
1836
|
+
* triangles at the apex. heightSegments subdivides the side top to
|
|
1837
|
+
* bottom (Three's, default 1) for deformation and per-vertex gradients.
|
|
592
1838
|
*/
|
|
593
1839
|
export type CylinderOptions = GeometryOptions & {
|
|
594
1840
|
radiusTop?: number
|
|
595
1841
|
radiusBottom?: number
|
|
596
1842
|
height?: number
|
|
597
1843
|
radialSegments?: number
|
|
1844
|
+
heightSegments?: number
|
|
598
1845
|
}
|
|
599
1846
|
|
|
600
1847
|
export function cylinder(options: CylinderOptions = {}): Geometry {
|
|
601
|
-
let { radiusTop = 0.5, radiusBottom = 0.5, height = 1, radialSegments = 24 } = options
|
|
1848
|
+
let { radiusTop = 0.5, radiusBottom = 0.5, height = 1, radialSegments = 24, heightSegments = 1 } = options
|
|
1849
|
+
if (!Number.isInteger(heightSegments) || heightSegments < 1) throw new Error("cylinder: heightSegments must be a positive integer")
|
|
602
1850
|
let h = height / 2
|
|
603
1851
|
let cols = radialSegments + 1
|
|
604
1852
|
let verts: number[] = []
|
|
@@ -606,24 +1854,23 @@ export function cylinder(options: CylinderOptions = {}): Geometry {
|
|
|
606
1854
|
let slant = Math.hypot(height, radiusBottom - radiusTop) || 1
|
|
607
1855
|
let nr = height / slant
|
|
608
1856
|
let ny = (radiusBottom - radiusTop) / slant
|
|
609
|
-
let
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
for (let row of rows) {
|
|
1857
|
+
for (let iy = 0; iy <= heightSegments; iy++) {
|
|
1858
|
+
let v = iy / heightSegments
|
|
1859
|
+
let r = radiusTop + (radiusBottom - radiusTop) * v
|
|
1860
|
+
let y = h - height * v
|
|
614
1861
|
for (let ix = 0; ix < cols; ix++) {
|
|
615
1862
|
let u = ix / radialSegments
|
|
616
1863
|
let phi = u * Math.PI * 2
|
|
617
1864
|
let dx = -Math.cos(phi)
|
|
618
1865
|
let dz = Math.sin(phi)
|
|
619
|
-
verts.push(
|
|
1866
|
+
verts.push(r * dx, y, r * dz, nr * dx, ny, nr * dz, u, v)
|
|
620
1867
|
}
|
|
621
1868
|
}
|
|
622
|
-
let indices = gridIndices(
|
|
1869
|
+
let indices = gridIndices(heightSegments, radialSegments, radiusTop <= 0, radiusBottom <= 0)
|
|
623
1870
|
// Caps fan around a center vertex; the planar UV map has no seam, so the
|
|
624
1871
|
// ring wraps with modulo instead of duplicating a column.
|
|
625
1872
|
let cap = (r: number, y: number, up: number) => {
|
|
626
|
-
let base = verts.length /
|
|
1873
|
+
let base = verts.length / BASE_FLOATS
|
|
627
1874
|
verts.push(0, y, 0, 0, up, 0, 0.5, 0.5)
|
|
628
1875
|
for (let i = 0; i < radialSegments; i++) {
|
|
629
1876
|
let phi = (i / radialSegments) * Math.PI * 2
|
|
@@ -645,13 +1892,75 @@ export function cylinder(options: CylinderOptions = {}): Geometry {
|
|
|
645
1892
|
/** A capped cone on the y axis, centered on the origin: `cylinder()` with
|
|
646
1893
|
* a zero top radius (each apex vertex carries its column's side normal, so
|
|
647
1894
|
* the surface shades smoothly around). */
|
|
648
|
-
export type ConeOptions = GeometryOptions & { radius?: number; height?: number; radialSegments?: number }
|
|
1895
|
+
export type ConeOptions = GeometryOptions & { radius?: number; height?: number; radialSegments?: number; heightSegments?: number }
|
|
649
1896
|
|
|
650
1897
|
export function cone(options: ConeOptions = {}): Geometry {
|
|
651
1898
|
let { radius = 0.5, height = 1, radialSegments = 24, ...rest } = options
|
|
652
1899
|
return cylinder({ ...rest, radiusTop: 0, radiusBottom: radius, height, radialSegments })
|
|
653
1900
|
}
|
|
654
1901
|
|
|
1902
|
+
/**
|
|
1903
|
+
* A capsule on the y axis, centered on the origin: a cylinder of `radius`
|
|
1904
|
+
* closed by two hemispheres. `height` is the TOTAL extent pole to pole
|
|
1905
|
+
* like cylinder()'s (Godot's CapsuleMesh and Unity's capsule agree;
|
|
1906
|
+
* Three's `height` is the middle section only, ours minus 2 * radius),
|
|
1907
|
+
* so `height: 2 * radius` is a sphere and anything less throws.
|
|
1908
|
+
* capSegments subdivides each hemisphere pole to equator, radialSegments
|
|
1909
|
+
* the circumference, heightSegments the band (Three's, default 1).
|
|
1910
|
+
* Normals are radial from each cap's center, so the band shades as a
|
|
1911
|
+
* cylinder. UVs: u around, v 0 at the top pole to 1 at the bottom by arc
|
|
1912
|
+
* length. The matching collision volume is
|
|
1913
|
+
* `{ a: [0, -(height / 2 - radius), 0], b: [0, height / 2 - radius, 0], radius }`.
|
|
1914
|
+
*/
|
|
1915
|
+
export type CapsuleOptions = GeometryOptions & {
|
|
1916
|
+
radius?: number
|
|
1917
|
+
height?: number
|
|
1918
|
+
capSegments?: number
|
|
1919
|
+
radialSegments?: number
|
|
1920
|
+
heightSegments?: number
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
export function capsule(options: CapsuleOptions = {}): Geometry {
|
|
1924
|
+
let { radius = 0.5, height = 1, capSegments = 8, radialSegments = 24, heightSegments = 1 } = options
|
|
1925
|
+
if (!Number.isInteger(heightSegments) || heightSegments < 1) throw new Error("capsule: heightSegments must be a positive integer")
|
|
1926
|
+
// Half the middle section: each hemisphere's center sits at +-half.
|
|
1927
|
+
let half = height / 2 - radius
|
|
1928
|
+
if (half < 0) throw new Error("capsule: height must be at least 2 * radius")
|
|
1929
|
+
// Arc length pole to pole: two quarter circles and the band.
|
|
1930
|
+
let arc = Math.PI * radius + 2 * half
|
|
1931
|
+
let verts: number[] = []
|
|
1932
|
+
// A sphere() row at polar angle theta, its center shifted along y.
|
|
1933
|
+
let row = (theta: number, offset: number, v: number): void => {
|
|
1934
|
+
let sinT = Math.sin(theta)
|
|
1935
|
+
let cosT = Math.cos(theta)
|
|
1936
|
+
for (let ix = 0; ix <= radialSegments; ix++) {
|
|
1937
|
+
let u = ix / radialSegments
|
|
1938
|
+
let phi = u * Math.PI * 2
|
|
1939
|
+
let nx = -Math.cos(phi) * sinT
|
|
1940
|
+
let ny = cosT
|
|
1941
|
+
let nz = Math.sin(phi) * sinT
|
|
1942
|
+
verts.push(radius * nx, radius * ny + offset, radius * nz, nx, ny, nz, u, v)
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
// capSegments + 1 rows per hemisphere; the two equator rows sit at the
|
|
1946
|
+
// ends of the band, with heightSegments - 1 equator rows between them.
|
|
1947
|
+
for (let i = 0; i <= capSegments; i++) {
|
|
1948
|
+
let theta = (i / capSegments) * (Math.PI / 2)
|
|
1949
|
+
row(theta, half, (radius * theta) / arc)
|
|
1950
|
+
}
|
|
1951
|
+
for (let i = 1; i < heightSegments; i++) {
|
|
1952
|
+
let t = i / heightSegments
|
|
1953
|
+
row(Math.PI / 2, half - 2 * half * t, (radius * (Math.PI / 2) + 2 * half * t) / arc)
|
|
1954
|
+
}
|
|
1955
|
+
for (let i = 0; i <= capSegments; i++) {
|
|
1956
|
+
let theta = Math.PI / 2 + (i / capSegments) * (Math.PI / 2)
|
|
1957
|
+
row(theta, -half, (radius * theta + 2 * half) / arc)
|
|
1958
|
+
}
|
|
1959
|
+
// Both pole rows are collapsed to the pole point, as in sphere().
|
|
1960
|
+
let indices = gridIndices(2 * capSegments + heightSegments, radialSegments, true, true)
|
|
1961
|
+
return packGeometry(verts, indices, options)
|
|
1962
|
+
}
|
|
1963
|
+
|
|
655
1964
|
/**
|
|
656
1965
|
* A torus lying flat, centered on the origin: the ring lies in the XZ
|
|
657
1966
|
* plane with the hole on the y axis - the y-up orientation torusKnot also
|
|
@@ -770,3 +2079,209 @@ export function sphere(options: SphereOptions = {}): Geometry {
|
|
|
770
2079
|
let indices = gridIndices(heightSegments, widthSegments, true, true)
|
|
771
2080
|
return packGeometry(verts, indices, options)
|
|
772
2081
|
}
|
|
2082
|
+
|
|
2083
|
+
// The polyhedron family, Three's PolyhedronGeometry: a convex solid's
|
|
2084
|
+
// corner list projected onto its circumsphere. `detail` splits every face
|
|
2085
|
+
// into (detail + 1)^2 triangles before the projection, so one builder is
|
|
2086
|
+
// the flat-shaded solid at 0 and a sphere of uniform triangles above it:
|
|
2087
|
+
// icosahedron({ detail: 3 }) is the icosphere, no pole pinch, which the
|
|
2088
|
+
// lat/long sphere() cannot express. Non-indexed as in Three, every
|
|
2089
|
+
// triangle owning its three vertices, which is what lets the normals
|
|
2090
|
+
// switch with detail: face normals at 0 (a dodecahedron reads as twelve
|
|
2091
|
+
// flat pentagons), radial above (smooth). UVs are the spherical map with
|
|
2092
|
+
// Three's per-triangle seam patch, which stretches toward the poles: the
|
|
2093
|
+
// textured sphere stays sphere(), these serve flat-shaded, low-poly and
|
|
2094
|
+
// procedurally shaded looks. Three's corner tables port verbatim, their
|
|
2095
|
+
// winding already CCW from outside. Godot and Unity ship neither the
|
|
2096
|
+
// family nor an icosphere (their primitives are blockout shapes and
|
|
2097
|
+
// modeling happens in Blender); nothing here touches the collision
|
|
2098
|
+
// volumes, which are analytic.
|
|
2099
|
+
|
|
2100
|
+
// The seam patch, Three's thresholds: a triangle with a u above
|
|
2101
|
+
// UV_SEAM_HIGH and one below UV_SEAM_LOW straddles the u = 0/1 seam, so
|
|
2102
|
+
// its u values below UV_SEAM_WRAP are lifted by a full turn and the
|
|
2103
|
+
// triangle samples across the seam instead of the whole texture backwards.
|
|
2104
|
+
const UV_SEAM_HIGH = 0.9
|
|
2105
|
+
const UV_SEAM_LOW = 0.1
|
|
2106
|
+
const UV_SEAM_WRAP = 0.2
|
|
2107
|
+
|
|
2108
|
+
export type PolyhedronOptions = GeometryOptions & {
|
|
2109
|
+
/** Circumsphere radius, default 0.5 like sphere() (Three's is 1). */
|
|
2110
|
+
radius?: number
|
|
2111
|
+
/** Subdivision: every edge split `detail + 1` ways, a non-negative
|
|
2112
|
+
* integer. Default 0, the flat solid. */
|
|
2113
|
+
detail?: number
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* The generic builder over Three's data form: `vertices` a flat xyz list,
|
|
2118
|
+
* `indices` its triangles, CCW seen from outside. F * (detail + 1)^2
|
|
2119
|
+
* triangles, three vertices each, indices 0..n-1.
|
|
2120
|
+
*/
|
|
2121
|
+
export function polyhedron(vertices: ArrayLike<number>, indices: ArrayLike<number>, options: PolyhedronOptions = {}): Geometry {
|
|
2122
|
+
let { radius = 0.5, detail = 0 } = options
|
|
2123
|
+
if (!Number.isInteger(detail) || detail < 0) throw new Error("polyhedron: detail must be a non-negative integer")
|
|
2124
|
+
if (vertices.length % 3 !== 0) throw new Error("polyhedron: vertices must be xyz triples")
|
|
2125
|
+
if (indices.length % 3 !== 0) throw new Error("polyhedron: indices must be triangles")
|
|
2126
|
+
let cols = detail + 1
|
|
2127
|
+
// Unit directions of the subdivided triangles' corners, three per
|
|
2128
|
+
// triangle. A grid point is the integer-weighted mean of its face's
|
|
2129
|
+
// corners (weights summing to cols), always summed in corner order: a
|
|
2130
|
+
// point on a shared edge has one zero weight, so both faces compute the
|
|
2131
|
+
// same two-term sum and it comes out bit-identical, one vertex to the
|
|
2132
|
+
// position weld in edgesGeometry/wireframeGeometry.
|
|
2133
|
+
let dirs: number[] = []
|
|
2134
|
+
let corner = (i: number): Vec3 => [vertices[i * 3]!, vertices[i * 3 + 1]!, vertices[i * 3 + 2]!]
|
|
2135
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
2136
|
+
let a = corner(indices[t]!)
|
|
2137
|
+
let b = corner(indices[t + 1]!)
|
|
2138
|
+
let c = corner(indices[t + 2]!)
|
|
2139
|
+
let point = (wa: number, wb: number, wc: number): void => {
|
|
2140
|
+
let x = (wa * a[0] + wb * b[0] + wc * c[0]) / cols
|
|
2141
|
+
let y = (wa * a[1] + wb * b[1] + wc * c[1]) / cols
|
|
2142
|
+
let z = (wa * a[2] + wb * b[2] + wc * c[2]) / cols
|
|
2143
|
+
let len = Math.hypot(x, y, z)
|
|
2144
|
+
dirs.push(x / len, y / len, z / len)
|
|
2145
|
+
}
|
|
2146
|
+
// Rows climb from the a-b edge to c, a row runs from a's side to b's;
|
|
2147
|
+
// row r holds span + 1 points with weights (span - j, j, r). Per cell
|
|
2148
|
+
// an upright triangle, and below the last an inverted one - Three's
|
|
2149
|
+
// order, so the two agree vertex for vertex.
|
|
2150
|
+
for (let r = 0; r < cols; r++) {
|
|
2151
|
+
let span = cols - r
|
|
2152
|
+
for (let j = 0; j < span; j++) {
|
|
2153
|
+
point(span - j - 1, j + 1, r)
|
|
2154
|
+
point(span - j - 1, j, r + 1)
|
|
2155
|
+
point(span - j, j, r)
|
|
2156
|
+
if (j < span - 1) {
|
|
2157
|
+
point(span - j - 1, j + 1, r)
|
|
2158
|
+
point(span - j - 2, j + 1, r + 1)
|
|
2159
|
+
point(span - j - 1, j, r + 1)
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
let count = dirs.length / 3
|
|
2165
|
+
let verts = new Float32Array(count * BASE_FLOATS)
|
|
2166
|
+
for (let i = 0; i < count; i++) {
|
|
2167
|
+
let d = i * 3
|
|
2168
|
+
let x = dirs[d]!
|
|
2169
|
+
let y = dirs[d + 1]!
|
|
2170
|
+
let z = dirs[d + 2]!
|
|
2171
|
+
let o = i * BASE_FLOATS
|
|
2172
|
+
verts[o] = x * radius
|
|
2173
|
+
verts[o + 1] = y * radius
|
|
2174
|
+
verts[o + 2] = z * radius
|
|
2175
|
+
verts[o + 3] = x
|
|
2176
|
+
verts[o + 4] = y
|
|
2177
|
+
verts[o + 5] = z
|
|
2178
|
+
// The spherical map: u the azimuth around y (from -x, counter-clockwise
|
|
2179
|
+
// seen from above), v the inclination, 0 at the top like sphere().
|
|
2180
|
+
verts[o + 6] = Math.atan2(z, -x) / (2 * Math.PI) + 0.5
|
|
2181
|
+
verts[o + 7] = Math.atan2(-y, Math.hypot(x, z)) / Math.PI + 0.5
|
|
2182
|
+
}
|
|
2183
|
+
for (let t = 0; t < count; t += 3) {
|
|
2184
|
+
let o0 = t * BASE_FLOATS
|
|
2185
|
+
let o1 = o0 + BASE_FLOATS
|
|
2186
|
+
let o2 = o1 + BASE_FLOATS
|
|
2187
|
+
// Three's UV fixes, keyed on the triangle's own azimuth: a corner on
|
|
2188
|
+
// the y axis (atan2(0, 0) says nothing) takes it, and on the -x side
|
|
2189
|
+
// of the seam a u of exactly 1 becomes 0 so the triangle stays whole.
|
|
2190
|
+
let azimuth = Math.atan2(verts[o0 + 5]! + verts[o1 + 5]! + verts[o2 + 5]!, -(verts[o0 + 3]! + verts[o1 + 3]! + verts[o2 + 3]!))
|
|
2191
|
+
let centerU = azimuth / (2 * Math.PI) + 0.5
|
|
2192
|
+
for (let o of [o0, o1, o2]) {
|
|
2193
|
+
if (azimuth < 0 && verts[o + 6] === 1) verts[o + 6] = 0
|
|
2194
|
+
if (verts[o + 3] === 0 && verts[o + 5] === 0) verts[o + 6] = centerU
|
|
2195
|
+
}
|
|
2196
|
+
let u0 = verts[o0 + 6]!
|
|
2197
|
+
let u1 = verts[o1 + 6]!
|
|
2198
|
+
let u2 = verts[o2 + 6]!
|
|
2199
|
+
if (Math.max(u0, u1, u2) > UV_SEAM_HIGH && Math.min(u0, u1, u2) < UV_SEAM_LOW) {
|
|
2200
|
+
for (let o of [o0, o1, o2]) if (verts[o + 6]! < UV_SEAM_WRAP) verts[o + 6] = verts[o + 6]! + 1
|
|
2201
|
+
}
|
|
2202
|
+
if (detail === 0) {
|
|
2203
|
+
let p0: Vec3 = [verts[o0]!, verts[o0 + 1]!, verts[o0 + 2]!]
|
|
2204
|
+
let p1: Vec3 = [verts[o1]!, verts[o1 + 1]!, verts[o1 + 2]!]
|
|
2205
|
+
let p2: Vec3 = [verts[o2]!, verts[o2 + 1]!, verts[o2 + 2]!]
|
|
2206
|
+
let n = normalize(cross(sub(p1, p0), sub(p2, p0)))
|
|
2207
|
+
for (let o of [o0, o1, o2]) {
|
|
2208
|
+
verts[o + 3] = n[0]
|
|
2209
|
+
verts[o + 4] = n[1]
|
|
2210
|
+
verts[o + 5] = n[2]
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
let order: number[] = []
|
|
2215
|
+
for (let i = 0; i < count; i++) order.push(i)
|
|
2216
|
+
return packGeometry(verts, order, options)
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// The golden ratio: the corner coordinate of the icosahedron and the
|
|
2220
|
+
// dodecahedron.
|
|
2221
|
+
const PHI = (1 + Math.sqrt(5)) / 2
|
|
2222
|
+
const PHI_INV = 1 / PHI
|
|
2223
|
+
|
|
2224
|
+
// Three's corner tables: the regular solids centered on the origin, CCW
|
|
2225
|
+
// triangles seen from outside (the dodecahedron's twelve pentagons fanned
|
|
2226
|
+
// into 36).
|
|
2227
|
+
const TETRAHEDRON_VERTICES = [1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1]
|
|
2228
|
+
const TETRAHEDRON_INDICES = [2, 1, 0, 0, 3, 2, 1, 3, 0, 2, 3, 1]
|
|
2229
|
+
const OCTAHEDRON_VERTICES = [1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1]
|
|
2230
|
+
const OCTAHEDRON_INDICES = [0, 2, 4, 0, 4, 3, 0, 3, 5, 0, 5, 2, 1, 2, 5, 1, 5, 3, 1, 3, 4, 1, 4, 2]
|
|
2231
|
+
// prettier-ignore
|
|
2232
|
+
const ICOSAHEDRON_VERTICES = [
|
|
2233
|
+
-1, PHI, 0, 1, PHI, 0, -1, -PHI, 0, 1, -PHI, 0,
|
|
2234
|
+
0, -1, PHI, 0, 1, PHI, 0, -1, -PHI, 0, 1, -PHI,
|
|
2235
|
+
PHI, 0, -1, PHI, 0, 1, -PHI, 0, -1, -PHI, 0, 1,
|
|
2236
|
+
]
|
|
2237
|
+
// prettier-ignore
|
|
2238
|
+
const ICOSAHEDRON_INDICES = [
|
|
2239
|
+
0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11,
|
|
2240
|
+
1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8,
|
|
2241
|
+
3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9,
|
|
2242
|
+
4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1,
|
|
2243
|
+
]
|
|
2244
|
+
// prettier-ignore
|
|
2245
|
+
const DODECAHEDRON_VERTICES = [
|
|
2246
|
+
-1, -1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1,
|
|
2247
|
+
1, -1, -1, 1, -1, 1, 1, 1, -1, 1, 1, 1,
|
|
2248
|
+
0, -PHI_INV, -PHI, 0, -PHI_INV, PHI, 0, PHI_INV, -PHI, 0, PHI_INV, PHI,
|
|
2249
|
+
-PHI_INV, -PHI, 0, -PHI_INV, PHI, 0, PHI_INV, -PHI, 0, PHI_INV, PHI, 0,
|
|
2250
|
+
-PHI, 0, -PHI_INV, PHI, 0, -PHI_INV, -PHI, 0, PHI_INV, PHI, 0, PHI_INV,
|
|
2251
|
+
]
|
|
2252
|
+
// prettier-ignore
|
|
2253
|
+
const DODECAHEDRON_INDICES = [
|
|
2254
|
+
3, 11, 7, 3, 7, 15, 3, 15, 13,
|
|
2255
|
+
7, 19, 17, 7, 17, 6, 7, 6, 15,
|
|
2256
|
+
17, 4, 8, 17, 8, 10, 17, 10, 6,
|
|
2257
|
+
8, 0, 16, 8, 16, 2, 8, 2, 10,
|
|
2258
|
+
0, 12, 1, 0, 1, 18, 0, 18, 16,
|
|
2259
|
+
6, 10, 2, 6, 2, 13, 6, 13, 15,
|
|
2260
|
+
2, 16, 18, 2, 18, 3, 2, 3, 13,
|
|
2261
|
+
18, 1, 9, 18, 9, 11, 18, 11, 3,
|
|
2262
|
+
4, 14, 12, 4, 12, 0, 4, 0, 8,
|
|
2263
|
+
11, 9, 5, 11, 5, 19, 11, 19, 7,
|
|
2264
|
+
19, 5, 14, 19, 14, 4, 19, 4, 17,
|
|
2265
|
+
1, 12, 14, 1, 14, 5, 1, 5, 9,
|
|
2266
|
+
]
|
|
2267
|
+
|
|
2268
|
+
/** The regular tetrahedron: 4 faces. */
|
|
2269
|
+
export function tetrahedron(options: PolyhedronOptions = {}): Geometry {
|
|
2270
|
+
return polyhedron(TETRAHEDRON_VERTICES, TETRAHEDRON_INDICES, options)
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
/** The regular octahedron: 8 faces, corners on the axes. */
|
|
2274
|
+
export function octahedron(options: PolyhedronOptions = {}): Geometry {
|
|
2275
|
+
return polyhedron(OCTAHEDRON_VERTICES, OCTAHEDRON_INDICES, options)
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/** The regular icosahedron: 20 faces; with `detail` above 0 the
|
|
2279
|
+
* icosphere. */
|
|
2280
|
+
export function icosahedron(options: PolyhedronOptions = {}): Geometry {
|
|
2281
|
+
return polyhedron(ICOSAHEDRON_VERTICES, ICOSAHEDRON_INDICES, options)
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/** The regular dodecahedron: 12 pentagons, 36 triangles. */
|
|
2285
|
+
export function dodecahedron(options: PolyhedronOptions = {}): Geometry {
|
|
2286
|
+
return polyhedron(DODECAHEDRON_VERTICES, DODECAHEDRON_INDICES, options)
|
|
2287
|
+
}
|