@solidrt/3d 0.0.58 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +24 -2
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +605 -185
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/src/geometry.ts
CHANGED
|
@@ -1,72 +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 { Topology, 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) }
|
|
52
111
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
|
155
|
+
|
|
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
|
+
}
|
|
57
219
|
|
|
58
220
|
/** The attribute list of a layout (a preset name resolves to its list). */
|
|
59
221
|
export function layoutAttributes(layout?: VertexLayout): VertexAttribute[] {
|
|
60
|
-
if (layout === undefined || layout === "
|
|
222
|
+
if (layout === undefined || layout === "base") return VERTEX_LAYOUTS.base
|
|
61
223
|
if (layout === "colored") return VERTEX_LAYOUTS.colored
|
|
62
224
|
if (layout === "skinned") return VERTEX_LAYOUTS.skinned
|
|
63
225
|
return layout
|
|
64
226
|
}
|
|
65
227
|
|
|
66
|
-
/**
|
|
228
|
+
/** Bytes per vertex of a layout - its interleave stride. */
|
|
67
229
|
export function layoutStride(layout?: VertexLayout): number {
|
|
68
230
|
let stride = 0
|
|
69
|
-
for (let attr of layoutAttributes(layout)) stride +=
|
|
231
|
+
for (let attr of layoutAttributes(layout)) stride += VERTEX_FORMATS[attr.format].bytes
|
|
70
232
|
return stride
|
|
71
233
|
}
|
|
72
234
|
|
|
@@ -78,27 +240,31 @@ export function layoutKey(layout?: VertexLayout): string {
|
|
|
78
240
|
.join(",")
|
|
79
241
|
}
|
|
80
242
|
|
|
81
|
-
/** Where an attribute sits in the interleave: its
|
|
82
|
-
* Null when the layout does not carry that name. */
|
|
83
|
-
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 {
|
|
84
246
|
let offset = 0
|
|
85
247
|
for (let attr of layoutAttributes(layout)) {
|
|
86
|
-
let
|
|
87
|
-
if (attr.name === name) return { offset,
|
|
88
|
-
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
|
|
89
251
|
}
|
|
90
252
|
return null
|
|
91
253
|
}
|
|
92
254
|
|
|
93
|
-
/**
|
|
94
|
-
*
|
|
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. */
|
|
95
264
|
function checkLayout(layout: VertexAttribute[], where: string): void {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (got === undefined || got.name !== want.name || got.format !== want.format) {
|
|
100
|
-
throw new Error(where + ": a layout must start with the standard prefix (aPos vec3, aNormal vec3, aUV vec2)")
|
|
101
|
-
}
|
|
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")
|
|
102
268
|
}
|
|
103
269
|
let seen = new Set<string>()
|
|
104
270
|
for (let attr of layout) {
|
|
@@ -107,11 +273,149 @@ function checkLayout(layout: VertexAttribute[], where: string): void {
|
|
|
107
273
|
}
|
|
108
274
|
}
|
|
109
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
|
+
|
|
110
413
|
/**
|
|
111
|
-
* The structural check for geometry about to draw: the layout
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
|
115
419
|
* miscounted array) fails there instead of drawing garbage triangles.
|
|
116
420
|
* Deliberately no max-index scan: that is O(indices) per add, and the
|
|
117
421
|
* generators and merge/transform keep indices in range by construction.
|
|
@@ -120,12 +424,8 @@ export function validateGeometry(geometry: Geometry): void {
|
|
|
120
424
|
let name = geometry.label ? "geometry '" + geometry.label + "'" : "geometry"
|
|
121
425
|
let layout = geometry.layout
|
|
122
426
|
if (layout !== undefined && typeof layout !== "string") checkLayout(layout, name)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
throw new Error(
|
|
126
|
-
name + ": " + geometry.vertices.length + " vertex floats is not a whole number of " + stride + "-float (" + layoutKey(layout) + ") vertices",
|
|
127
|
-
)
|
|
128
|
-
}
|
|
427
|
+
checkStreams(geometry, name)
|
|
428
|
+
geometryVertexCount(geometry, name)
|
|
129
429
|
let topology = geometryTopology(geometry)
|
|
130
430
|
let count = geometry.indices.length
|
|
131
431
|
// Lines and points may be empty: a feature-edge pass over a smooth
|
|
@@ -165,9 +465,30 @@ export function geometryTopology(geometry: Geometry): Topology {
|
|
|
165
465
|
* channel geometry is built in one pass instead of generate-then-repack. */
|
|
166
466
|
export type GeometryOptions = { label?: string; layout?: VertexLayout }
|
|
167
467
|
|
|
168
|
-
/** 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
|
|
169
490
|
* vertex, or an already-written Float32Array) and indices into a Geometry
|
|
170
|
-
* of the requested layout. A wider layout spreads the
|
|
491
|
+
* of the requested layout. A wider layout spreads the base channels
|
|
171
492
|
* to its stride, leaving the extra slots zero. */
|
|
172
493
|
export function packGeometry(
|
|
173
494
|
verts: ArrayLike<number>,
|
|
@@ -175,40 +496,39 @@ export function packGeometry(
|
|
|
175
496
|
options: GeometryOptions = {},
|
|
176
497
|
): Geometry {
|
|
177
498
|
let { label, layout } = options
|
|
178
|
-
if (verts.length %
|
|
179
|
-
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")
|
|
180
501
|
}
|
|
181
|
-
let count = verts.length /
|
|
502
|
+
let count = verts.length / BASE_FLOATS
|
|
182
503
|
let packedIndices = indices instanceof Uint16Array || indices instanceof Uint32Array ? indices : packIndices(indices, count)
|
|
183
|
-
let
|
|
184
|
-
if (
|
|
185
|
-
let stride = layoutStride(attrs)
|
|
186
|
-
if (stride === STANDARD_FLOATS) {
|
|
504
|
+
let stride = generatorStride(options, "packGeometry")
|
|
505
|
+
if (stride === BASE_FLOATS) {
|
|
187
506
|
let vertices = verts instanceof Float32Array ? verts : new Float32Array(verts)
|
|
188
507
|
return layout === undefined ? { vertices, indices: packedIndices, label } : { vertices, indices: packedIndices, layout, label }
|
|
189
508
|
}
|
|
190
509
|
let vertices = new Float32Array(count * stride)
|
|
191
510
|
for (let i = 0; i < count; i++) {
|
|
192
|
-
let s = i *
|
|
511
|
+
let s = i * BASE_FLOATS
|
|
193
512
|
let d = i * stride
|
|
194
|
-
for (let k = 0; k <
|
|
513
|
+
for (let k = 0; k < BASE_FLOATS; k++) vertices[d + k] = verts[s + k]!
|
|
195
514
|
}
|
|
196
515
|
return { vertices, indices: packedIndices, layout, label }
|
|
197
516
|
}
|
|
198
517
|
|
|
199
|
-
// The stride a generator writing a Float32Array directly must
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
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 {
|
|
203
523
|
let { layout } = options
|
|
204
524
|
let attrs = layoutAttributes(layout)
|
|
205
|
-
if (layout !== undefined && typeof layout !== "string")
|
|
206
|
-
return layoutStride(attrs)
|
|
525
|
+
if (layout !== undefined && typeof layout !== "string") checkGeneratorLayout(attrs, where)
|
|
526
|
+
return layoutStride(attrs) / Float32Array.BYTES_PER_ELEMENT
|
|
207
527
|
}
|
|
208
528
|
|
|
209
529
|
function finishGeometry(vertices: Float32Array, indices: number[], options: GeometryOptions): Geometry {
|
|
210
530
|
let { label, layout } = options
|
|
211
|
-
let packed = packIndices(indices, vertices
|
|
531
|
+
let packed = packIndices(indices, vertexCount(vertices, layout, "generator"))
|
|
212
532
|
return layout === undefined ? { vertices, indices: packed, label } : { vertices, indices: packed, layout, label }
|
|
213
533
|
}
|
|
214
534
|
|
|
@@ -219,12 +539,24 @@ export function packIndices(indices: number[], vertexCount: number): Uint16Array
|
|
|
219
539
|
}
|
|
220
540
|
|
|
221
541
|
export type Geometry = {
|
|
222
|
-
/**
|
|
223
|
-
*
|
|
224
|
-
|
|
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
|
|
225
548
|
/** The array type picks the draw's index format: Uint32Array past 64k
|
|
226
549
|
* vertices. */
|
|
227
550
|
indices: Uint16Array | Uint32Array
|
|
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[]
|
|
228
560
|
/** What the indices describe; absent means "triangles". The index
|
|
229
561
|
* buffer and the primitive it lists travel together (Godot's surface
|
|
230
562
|
* primitive, Unity's Mesh.SetIndices topology, Three's Line/Points
|
|
@@ -239,29 +571,161 @@ export type Geometry = {
|
|
|
239
571
|
* wireframeGeometry/edgesGeometry build "lines" over a triangle
|
|
240
572
|
* geometry's own vertices. */
|
|
241
573
|
topology?: Topology
|
|
242
|
-
/** Vertex layout; absent means "
|
|
574
|
+
/** Vertex layout; absent means "base". Must match the material's
|
|
243
575
|
* layout - the scene rejects a mismatched pair at add(). */
|
|
244
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
|
|
245
584
|
/** Debug name for the lazily-created GPU buffers. */
|
|
246
585
|
label?: string
|
|
247
586
|
_bounds?: Float32Array
|
|
248
587
|
}
|
|
249
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
|
+
|
|
250
711
|
/**
|
|
251
712
|
* The geometry's LOCAL axis-aligned bounds as [minX, minY, minZ, maxX,
|
|
252
|
-
* maxY, maxZ], computed from the vertices on first use
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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.
|
|
256
720
|
*/
|
|
257
721
|
export function geometryBounds(geometry: Geometry): Float32Array {
|
|
258
722
|
let bounds = geometry._bounds
|
|
259
723
|
if (bounds === undefined) {
|
|
260
724
|
bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
|
|
261
|
-
let
|
|
262
|
-
let
|
|
263
|
-
for (let i = 0; i
|
|
264
|
-
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)
|
|
265
729
|
if (x < bounds[0]!) bounds[0] = x
|
|
266
730
|
if (y < bounds[1]!) bounds[1] = y
|
|
267
731
|
if (z < bounds[2]!) bounds[2] = z
|
|
@@ -270,6 +734,11 @@ export function geometryBounds(geometry: Geometry): Float32Array {
|
|
|
270
734
|
if (z > bounds[5]!) bounds[5] = z
|
|
271
735
|
}
|
|
272
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
|
+
}
|
|
273
742
|
geometry._bounds = bounds
|
|
274
743
|
}
|
|
275
744
|
return bounds
|
|
@@ -277,125 +746,166 @@ export function geometryBounds(geometry: Geometry): Float32Array {
|
|
|
277
746
|
|
|
278
747
|
/** Per-vertex values for withAttribute/fillAttribute: a flat array of the
|
|
279
748
|
* attribute's size per vertex, or a callback deriving each vertex's value
|
|
280
|
-
* 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. */
|
|
281
751
|
export type AttributeFill = ArrayLike<number> | ((index: number, pos: Vec3, normal: Vec3, uv: Vec2) => ArrayLike<number>)
|
|
282
752
|
/** AttributeFill for the aColor vec4 channel (4 per vertex). */
|
|
283
753
|
export type ColorFill = AttributeFill
|
|
284
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
|
+
|
|
285
762
|
/**
|
|
286
763
|
* Append a named channel to a geometry: a new geometry (the source is
|
|
287
764
|
* untouched, its GPU buffers stay independent) whose layout is the
|
|
288
765
|
* source's plus `attr`, every existing channel copied through and the new
|
|
289
766
|
* slots written from `fill`. This is Three's `geometry.setAttribute` for
|
|
290
767
|
* an interleaved buffer - the one generic primitive; `withColors` is its
|
|
291
|
-
* aColor spelling. A material reads the channel by declaring
|
|
292
|
-
*
|
|
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.
|
|
293
774
|
*/
|
|
294
|
-
export function withAttribute(geometry: Geometry, attr: VertexAttribute, fill: AttributeFill,
|
|
295
|
-
|
|
296
|
-
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) {
|
|
297
777
|
throw new Error("withAttribute: geometry already carries '" + attr.name + "'")
|
|
298
778
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
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(", ") + ")")
|
|
302
781
|
}
|
|
303
|
-
let
|
|
304
|
-
|
|
305
|
-
let
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
let out = new Float32Array(count * stride)
|
|
309
|
-
for (let i = 0; i < count; i++) {
|
|
310
|
-
let s = i * srcStride
|
|
311
|
-
let d = i * stride
|
|
312
|
-
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")
|
|
313
787
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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,
|
|
317
821
|
indices: geometry.indices,
|
|
318
822
|
topology: geometry.topology,
|
|
319
|
-
layout,
|
|
320
|
-
label: label ?? (geometry.label ? geometry.label + "-" + attr.name : undefined),
|
|
823
|
+
layout: target === 0 ? layout : geometry.layout,
|
|
824
|
+
label: options.label ?? (geometry.label ? geometry.label + "-" + attr.name : undefined),
|
|
321
825
|
}
|
|
826
|
+
if (extra.length > 0) out.streams = extra
|
|
827
|
+
return out
|
|
322
828
|
}
|
|
323
829
|
|
|
324
830
|
/**
|
|
325
|
-
* Derive a "colored"-layout geometry from a
|
|
326
|
-
* positions, normals, uvs and indices, plus an aColor
|
|
327
|
-
* the data channel for materials whose vertex stage reads `in
|
|
328
|
-
* (a tint, baked ambient occlusion, any four scalars; the
|
|
329
|
-
* standard vocabulary, the contents are yours
|
|
330
|
-
*
|
|
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.
|
|
331
841
|
*/
|
|
332
842
|
export function withColors(geometry: Geometry, fill: ColorFill, label?: string): Geometry {
|
|
333
|
-
if (
|
|
843
|
+
if (geometrySlot(geometry, "aColor") !== null) {
|
|
334
844
|
throw new Error("withColors: geometry already carries an aColor channel")
|
|
335
845
|
}
|
|
336
|
-
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) })
|
|
337
847
|
if (layoutKey(out.layout) === layoutKey("colored")) out.layout = "colored"
|
|
338
848
|
return out
|
|
339
849
|
}
|
|
340
850
|
|
|
341
851
|
/**
|
|
342
852
|
* The in-place primitive under withAttribute: write one channel the
|
|
343
|
-
* geometry
|
|
344
|
-
* overwrites an existing one). The pos/normal/uv the
|
|
345
|
-
* are read from the
|
|
346
|
-
* writing hands the baker world-space vertices.
|
|
347
|
-
* [first, first + count) - count defaults to the rest of
|
|
348
|
-
* and `fill` indexes relative to `first`, so a per-part
|
|
349
|
-
* 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).
|
|
350
861
|
*/
|
|
351
|
-
export function fillAttribute(geometry: Geometry, name: string, fill: AttributeFill, first = 0, count?: number):
|
|
352
|
-
|
|
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
|
|
353
868
|
}
|
|
354
869
|
|
|
355
|
-
/** The
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
function
|
|
360
|
-
let slot = layoutSlot(layout, name)
|
|
361
|
-
if (slot === null) throw new Error("fillAttribute: layout has no '" + name + "' attribute")
|
|
362
|
-
let stride = layoutStride(layout)
|
|
363
|
-
if (vertices.length % stride !== 0) {
|
|
364
|
-
throw new Error("fillAttribute: vertex data is not a whole number of " + layoutKey(layout) + " vertices")
|
|
365
|
-
}
|
|
366
|
-
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 {
|
|
367
875
|
let n = count ?? total - first
|
|
368
876
|
if (!Number.isInteger(first) || !Number.isInteger(n) || first < 0 || n < 0 || first + n > total) {
|
|
369
877
|
throw new Error("fillAttribute: range [" + first + ", " + (first + n) + ") is outside the buffer's " + total + " vertices")
|
|
370
878
|
}
|
|
371
|
-
let size = slot.
|
|
879
|
+
let size = slot.components
|
|
372
880
|
let fn = typeof fill === "function" ? fill : null
|
|
373
881
|
let flat = typeof fill === "function" ? null : fill
|
|
374
882
|
if (flat !== null && flat.length !== n * size) {
|
|
375
|
-
throw new Error("fillAttribute: fill has " + flat.length + "
|
|
883
|
+
throw new Error("fillAttribute: fill has " + flat.length + " values, expected " + size + " per vertex (" + n * size + ")")
|
|
376
884
|
}
|
|
885
|
+
let pos = geometryAttribute(source, "aPos")!
|
|
886
|
+
let nrm = geometryAttribute(source, "aNormal") ?? ZERO_ACCESS
|
|
887
|
+
let uv = geometryAttribute(source, "aUV") ?? ZERO_ACCESS
|
|
377
888
|
for (let i = 0; i < n; i++) {
|
|
378
|
-
let
|
|
889
|
+
let v = first + i
|
|
379
890
|
let value: ArrayLike<number>
|
|
380
891
|
let s: number
|
|
381
892
|
if (fn !== null) {
|
|
382
|
-
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)])
|
|
383
894
|
s = 0
|
|
384
895
|
if (value.length !== size) {
|
|
385
|
-
throw new Error("fillAttribute: fill callback returned " + value.length + "
|
|
896
|
+
throw new Error("fillAttribute: fill callback returned " + value.length + " values for '" + name + "', expected " + size)
|
|
386
897
|
}
|
|
387
898
|
} else {
|
|
388
899
|
value = flat!
|
|
389
900
|
s = i * size
|
|
390
901
|
}
|
|
391
|
-
for (let k = 0; k < size; k++)
|
|
902
|
+
for (let k = 0; k < size; k++) slot.set(v, k, value[s + k]!)
|
|
392
903
|
}
|
|
393
|
-
return vertices
|
|
394
904
|
}
|
|
395
905
|
|
|
396
906
|
/** `fillAttribute` for the aColor channel of a color-carrying geometry
|
|
397
907
|
* (fill is 4 per vertex). */
|
|
398
|
-
export function fillColors(geometry: Geometry, fill: ColorFill, first = 0, count?: number):
|
|
908
|
+
export function fillColors(geometry: Geometry, fill: ColorFill, first = 0, count?: number): ArrayBufferView {
|
|
399
909
|
return fillAttribute(geometry, "aColor", fill, first, count)
|
|
400
910
|
}
|
|
401
911
|
|
|
@@ -404,9 +914,11 @@ export function fillColors(geometry: Geometry, fill: ColorFill, first = 0, count
|
|
|
404
914
|
* quaternion, not both; number = uniform scale; absent = identity) into a
|
|
405
915
|
* geometry: a new geometry (the source is
|
|
406
916
|
* untouched, its GPU buffers stay independent) whose positions are moved
|
|
407
|
-
* by the transform and whose normals
|
|
408
|
-
*
|
|
409
|
-
*
|
|
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
|
|
410
922
|
* half of authoring a static scene as data: transform each part into place,
|
|
411
923
|
* mergeGeometries the parts, draw one mesh.
|
|
412
924
|
*/
|
|
@@ -417,33 +929,34 @@ export function transformGeometry(geometry: Geometry, transform: TransformUpdate
|
|
|
417
929
|
if (transform.scale !== undefined) updateScale(scl, transform.scale)
|
|
418
930
|
let m = compose(mat4(), transform.position ?? [0, 0, 0], rot, scl)
|
|
419
931
|
let n = normalMatrix(mat4(), m)
|
|
420
|
-
let
|
|
421
|
-
let
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
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)
|
|
432
943
|
let tx = n[0] * nx + n[4] * ny + n[8] * nz
|
|
433
944
|
let ty = n[1] * nx + n[5] * ny + n[9] * nz
|
|
434
945
|
let tz = n[2] * nx + n[6] * ny + n[10] * nz
|
|
435
946
|
let len = Math.hypot(tx, ty, tz) || 1
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
947
|
+
nrm.set(i, 0, tx / len)
|
|
948
|
+
nrm.set(i, 1, ty / len)
|
|
949
|
+
nrm.set(i, 2, tz / len)
|
|
439
950
|
}
|
|
440
|
-
|
|
951
|
+
let result: Geometry = {
|
|
441
952
|
vertices: out,
|
|
442
953
|
indices: geometry.indices,
|
|
443
954
|
topology: geometry.topology,
|
|
444
955
|
layout: geometry.layout,
|
|
445
956
|
label: label ?? (geometry.label ? geometry.label + "-transformed" : undefined),
|
|
446
957
|
}
|
|
958
|
+
if (geometry.streams !== undefined) result.streams = geometry.streams
|
|
959
|
+
return result
|
|
447
960
|
}
|
|
448
961
|
|
|
449
962
|
/**
|
|
@@ -460,45 +973,49 @@ export function transformGeometry(geometry: Geometry, transform: TransformUpdate
|
|
|
460
973
|
*/
|
|
461
974
|
export function mergeGeometries(parts: Geometry[], label?: string): Geometry {
|
|
462
975
|
if (parts.length === 0) throw new Error("mergeGeometries: no parts")
|
|
463
|
-
let
|
|
464
|
-
let key =
|
|
465
|
-
let
|
|
466
|
-
let topology = geometryTopology(parts[0]!)
|
|
976
|
+
let first = parts[0]!
|
|
977
|
+
let key = geometryKey(first)
|
|
978
|
+
let topology = geometryTopology(first)
|
|
467
979
|
if (topology === "line-strip" || topology === "triangle-strip") throw new Error("mergeGeometries: cannot merge " + topology + " geometry")
|
|
468
|
-
let
|
|
980
|
+
let vertexTotal = 0
|
|
469
981
|
let indexCount = 0
|
|
470
982
|
for (let part of parts) {
|
|
471
|
-
if (
|
|
472
|
-
throw new Error("mergeGeometries: mixed layouts (" + key + " and " +
|
|
983
|
+
if (geometryKey(part) !== key) {
|
|
984
|
+
throw new Error("mergeGeometries: mixed layouts (" + key + " and " + geometryKey(part) + ")")
|
|
473
985
|
}
|
|
474
986
|
if (geometryTopology(part) !== topology) {
|
|
475
987
|
throw new Error("mergeGeometries: mixed topologies (" + topology + " and " + geometryTopology(part) + ")")
|
|
476
988
|
}
|
|
477
|
-
|
|
478
|
-
throw new Error("mergeGeometries: a part's vertex data is not a whole number of " + key + " vertices")
|
|
479
|
-
}
|
|
480
|
-
floats += part.vertices.length
|
|
989
|
+
vertexTotal += geometryVertexCount(part, "mergeGeometries")
|
|
481
990
|
indexCount += part.indices.length
|
|
482
991
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
let
|
|
486
|
-
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
|
|
487
1002
|
let iOffset = 0
|
|
488
1003
|
for (let part of parts) {
|
|
489
|
-
|
|
490
|
-
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)
|
|
491
1006
|
let src = part.indices
|
|
492
1007
|
for (let i = 0; i < src.length; i++) indices[iOffset + i] = src[i]! + base
|
|
493
|
-
|
|
1008
|
+
base += geometryVertexCount(part, "mergeGeometries")
|
|
494
1009
|
iOffset += src.length
|
|
495
1010
|
}
|
|
496
|
-
|
|
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
|
|
497
1014
|
}
|
|
498
1015
|
|
|
499
|
-
/**
|
|
500
|
-
*
|
|
501
|
-
*
|
|
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. */
|
|
502
1019
|
const WELD_PRECISION = 1e-4
|
|
503
1020
|
|
|
504
1021
|
/** Degrees between two faces' normals from which the edge between them
|
|
@@ -506,18 +1023,27 @@ const WELD_PRECISION = 1e-4
|
|
|
506
1023
|
* one smooth surface and the edge stays hidden. Three's default. */
|
|
507
1024
|
const EDGES_THRESHOLD_ANGLE = 1
|
|
508
1025
|
|
|
509
|
-
/**
|
|
510
|
-
*
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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)
|
|
515
1039
|
let ids = new Uint32Array(count)
|
|
516
1040
|
let seen = new Map<string, number>()
|
|
517
|
-
let scale = 1 /
|
|
1041
|
+
let scale = 1 / precision
|
|
518
1042
|
for (let i = 0; i < count; i++) {
|
|
519
|
-
let
|
|
520
|
-
|
|
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
|
+
}
|
|
521
1047
|
let id = seen.get(key)
|
|
522
1048
|
if (id === undefined) {
|
|
523
1049
|
id = seen.size
|
|
@@ -528,6 +1054,19 @@ function weldPositions(geometry: Geometry): { ids: Uint32Array; count: number }
|
|
|
528
1054
|
return { ids, count: seen.size }
|
|
529
1055
|
}
|
|
530
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
|
+
|
|
531
1070
|
/** The edge table of a triangle geometry, welded by position, as a lines
|
|
532
1071
|
* index list: every edge once for `cosThreshold` null, else the feature
|
|
533
1072
|
* edges - those with one face (a border) or whose two faces' normals
|
|
@@ -537,18 +1076,16 @@ function edgeIndices(geometry: Geometry, name: string, cosThreshold: number | nu
|
|
|
537
1076
|
if (geometryTopology(geometry) !== "triangles") {
|
|
538
1077
|
throw new Error(name + ": needs a triangle geometry, got " + geometryTopology(geometry))
|
|
539
1078
|
}
|
|
540
|
-
let
|
|
541
|
-
let
|
|
542
|
-
let weld = weldPositions(geometry)
|
|
1079
|
+
let pos = geometryAttribute(geometry, "aPos")!
|
|
1080
|
+
let weld = weldPositions(geometry, name)
|
|
543
1081
|
let src = geometry.indices
|
|
544
1082
|
type Edge = { a: number; b: number; nx: number; ny: number; nz: number; faces: number; sharp: boolean }
|
|
545
1083
|
let edges = new Map<number, Edge>()
|
|
546
1084
|
for (let t = 0; t + 2 < src.length; t += 3) {
|
|
547
1085
|
let i0 = src[t]!, i1 = src[t + 1]!, i2 = src[t + 2]!
|
|
548
|
-
let p0 = i0 * stride, p1 = i1 * stride, p2 = i2 * stride
|
|
549
1086
|
// The face normal, for the feature test.
|
|
550
|
-
let ux =
|
|
551
|
-
let wx =
|
|
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)
|
|
552
1089
|
let nx = uy * wz - uz * wy, ny = uz * wx - ux * wz, nz = ux * wy - uy * wx
|
|
553
1090
|
let len = Math.hypot(nx, ny, nz) || 1
|
|
554
1091
|
nx /= len
|
|
@@ -573,7 +1110,7 @@ function edgeIndices(geometry: Geometry, name: string, cosThreshold: number | nu
|
|
|
573
1110
|
for (let e of edges.values()) {
|
|
574
1111
|
if (cosThreshold === null || e.faces === 1 || e.sharp) out.push(e.a, e.b)
|
|
575
1112
|
}
|
|
576
|
-
return packIndices(out,
|
|
1113
|
+
return packIndices(out, weld.ids.length)
|
|
577
1114
|
}
|
|
578
1115
|
|
|
579
1116
|
/**
|
|
@@ -589,13 +1126,15 @@ function edgeIndices(geometry: Geometry, name: string, cosThreshold: number | nu
|
|
|
589
1126
|
* geometry. Lines are one pixel wide.
|
|
590
1127
|
*/
|
|
591
1128
|
export function wireframeGeometry(geometry: Geometry, label?: string): Geometry {
|
|
592
|
-
return
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
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
|
|
599
1138
|
}
|
|
600
1139
|
|
|
601
1140
|
/**
|
|
@@ -614,13 +1153,499 @@ export function wireframeGeometry(geometry: Geometry, label?: string): Geometry
|
|
|
614
1153
|
* wireframeGeometry.
|
|
615
1154
|
*/
|
|
616
1155
|
export function edgesGeometry(geometry: Geometry, thresholdAngle = EDGES_THRESHOLD_ANGLE, label?: string): Geometry {
|
|
617
|
-
return
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
|
623
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)
|
|
624
1649
|
}
|
|
625
1650
|
|
|
626
1651
|
// Indices for a row-major (cellRows + 1) x (cellCols + 1) vertex grid: two
|
|
@@ -644,6 +1669,45 @@ function gridIndices(cellRows: number, cellCols: number, skipFirst = false, skip
|
|
|
644
1669
|
}
|
|
645
1670
|
|
|
646
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
|
+
|
|
647
1711
|
export type BoxOptions = GeometryOptions & { width?: number; height?: number; depth?: number }
|
|
648
1712
|
|
|
649
1713
|
export function box(options: BoxOptions = {}): Geometry {
|
|
@@ -656,7 +1720,7 @@ export function box(options: BoxOptions = {}): Geometry {
|
|
|
656
1720
|
type P = [number, number, number]
|
|
657
1721
|
// Corners a (bottom-left) through d (top-left), CCW seen from outside.
|
|
658
1722
|
let quad = (a: P, b: P, c: P, d: P, n: P) => {
|
|
659
|
-
let base = verts.length /
|
|
1723
|
+
let base = verts.length / BASE_FLOATS
|
|
660
1724
|
let uv = [[0, 1], [1, 1], [1, 0], [0, 0]]
|
|
661
1725
|
let corners = [a, b, c, d]
|
|
662
1726
|
for (let i = 0; i < 4; i++) {
|
|
@@ -769,17 +1833,20 @@ export function torusKnot(options: TorusKnotOptions = {}): Geometry {
|
|
|
769
1833
|
* case); side normals tilt with the taper. Side UVs: u around the
|
|
770
1834
|
* circumference, v 0 at the top to 1 at the bottom; caps get a planar
|
|
771
1835
|
* disc map. A zero radius skips that cap and the degenerate side
|
|
772
|
-
* 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.
|
|
773
1838
|
*/
|
|
774
1839
|
export type CylinderOptions = GeometryOptions & {
|
|
775
1840
|
radiusTop?: number
|
|
776
1841
|
radiusBottom?: number
|
|
777
1842
|
height?: number
|
|
778
1843
|
radialSegments?: number
|
|
1844
|
+
heightSegments?: number
|
|
779
1845
|
}
|
|
780
1846
|
|
|
781
1847
|
export function cylinder(options: CylinderOptions = {}): Geometry {
|
|
782
|
-
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")
|
|
783
1850
|
let h = height / 2
|
|
784
1851
|
let cols = radialSegments + 1
|
|
785
1852
|
let verts: number[] = []
|
|
@@ -787,24 +1854,23 @@ export function cylinder(options: CylinderOptions = {}): Geometry {
|
|
|
787
1854
|
let slant = Math.hypot(height, radiusBottom - radiusTop) || 1
|
|
788
1855
|
let nr = height / slant
|
|
789
1856
|
let ny = (radiusBottom - radiusTop) / slant
|
|
790
|
-
let
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
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
|
|
795
1861
|
for (let ix = 0; ix < cols; ix++) {
|
|
796
1862
|
let u = ix / radialSegments
|
|
797
1863
|
let phi = u * Math.PI * 2
|
|
798
1864
|
let dx = -Math.cos(phi)
|
|
799
1865
|
let dz = Math.sin(phi)
|
|
800
|
-
verts.push(
|
|
1866
|
+
verts.push(r * dx, y, r * dz, nr * dx, ny, nr * dz, u, v)
|
|
801
1867
|
}
|
|
802
1868
|
}
|
|
803
|
-
let indices = gridIndices(
|
|
1869
|
+
let indices = gridIndices(heightSegments, radialSegments, radiusTop <= 0, radiusBottom <= 0)
|
|
804
1870
|
// Caps fan around a center vertex; the planar UV map has no seam, so the
|
|
805
1871
|
// ring wraps with modulo instead of duplicating a column.
|
|
806
1872
|
let cap = (r: number, y: number, up: number) => {
|
|
807
|
-
let base = verts.length /
|
|
1873
|
+
let base = verts.length / BASE_FLOATS
|
|
808
1874
|
verts.push(0, y, 0, 0, up, 0, 0.5, 0.5)
|
|
809
1875
|
for (let i = 0; i < radialSegments; i++) {
|
|
810
1876
|
let phi = (i / radialSegments) * Math.PI * 2
|
|
@@ -826,13 +1892,75 @@ export function cylinder(options: CylinderOptions = {}): Geometry {
|
|
|
826
1892
|
/** A capped cone on the y axis, centered on the origin: `cylinder()` with
|
|
827
1893
|
* a zero top radius (each apex vertex carries its column's side normal, so
|
|
828
1894
|
* the surface shades smoothly around). */
|
|
829
|
-
export type ConeOptions = GeometryOptions & { radius?: number; height?: number; radialSegments?: number }
|
|
1895
|
+
export type ConeOptions = GeometryOptions & { radius?: number; height?: number; radialSegments?: number; heightSegments?: number }
|
|
830
1896
|
|
|
831
1897
|
export function cone(options: ConeOptions = {}): Geometry {
|
|
832
1898
|
let { radius = 0.5, height = 1, radialSegments = 24, ...rest } = options
|
|
833
1899
|
return cylinder({ ...rest, radiusTop: 0, radiusBottom: radius, height, radialSegments })
|
|
834
1900
|
}
|
|
835
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
|
+
|
|
836
1964
|
/**
|
|
837
1965
|
* A torus lying flat, centered on the origin: the ring lies in the XZ
|
|
838
1966
|
* plane with the hole on the y axis - the y-up orientation torusKnot also
|
|
@@ -951,3 +2079,209 @@ export function sphere(options: SphereOptions = {}): Geometry {
|
|
|
951
2079
|
let indices = gridIndices(heightSegments, widthSegments, true, true)
|
|
952
2080
|
return packGeometry(verts, indices, options)
|
|
953
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
|
+
}
|