topazcube 0.1.30 → 0.1.33
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/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +18200 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +18183 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +94 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +71 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +173 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +572 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +61 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +643 -0
- package/src/renderer/Renderer.js +1324 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +359 -0
- package/src/renderer/core/CullingSystem.js +307 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +546 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2064 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +417 -0
- package/src/renderer/rendering/passes/FogPass.js +419 -0
- package/src/renderer/rendering/passes/GBufferPass.js +706 -0
- package/src/renderer/rendering/passes/HiZPass.js +714 -0
- package/src/renderer/rendering/passes/LightingPass.js +739 -0
- package/src/renderer/rendering/passes/ParticlePass.js +835 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +282 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +265 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +296 -0
- package/src/renderer/rendering/passes/ShadowPass.js +1822 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +550 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +525 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +272 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +76 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- package/src/utils.ts +0 -403
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
import { calculateBoundingSphere } from "./utils/BoundingSphere.js"
|
|
2
|
+
|
|
3
|
+
var _UID = 30001
|
|
4
|
+
|
|
5
|
+
class Geometry {
|
|
6
|
+
|
|
7
|
+
constructor(engine, attributes) {
|
|
8
|
+
this.uid = _UID++
|
|
9
|
+
this.engine = engine
|
|
10
|
+
const { device } = engine
|
|
11
|
+
|
|
12
|
+
// Bounding sphere (lazy calculated)
|
|
13
|
+
this._bsphere = null
|
|
14
|
+
|
|
15
|
+
this.vertexBufferLayout = {
|
|
16
|
+
arrayStride: 80, // 20 floats * 4 bytes each
|
|
17
|
+
attributes: [
|
|
18
|
+
{ format: "float32x3", offset: 0, shaderLocation: 0 }, // position
|
|
19
|
+
{ format: "float32x2", offset: 12, shaderLocation: 1 }, // uv
|
|
20
|
+
{ format: "float32x3", offset: 20, shaderLocation: 2 }, // normal
|
|
21
|
+
{ format: "float32x4", offset: 32, shaderLocation: 3 }, // color
|
|
22
|
+
{ format: "float32x4", offset: 48, shaderLocation: 4 }, // weights
|
|
23
|
+
{ format: "uint32x4", offset: 64, shaderLocation: 5 }, // joints
|
|
24
|
+
],
|
|
25
|
+
stepMode: 'vertex'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Instance buffer layout for model matrix + sprite data
|
|
29
|
+
this.instanceBufferLayout = {
|
|
30
|
+
arrayStride: 112, // 28 floats * 4 bytes each
|
|
31
|
+
stepMode: 'instance',
|
|
32
|
+
attributes: [
|
|
33
|
+
{ format: "float32x4", offset: 0, shaderLocation: 6 }, // matrix column 0
|
|
34
|
+
{ format: "float32x4", offset: 16, shaderLocation: 7 }, // matrix column 1
|
|
35
|
+
{ format: "float32x4", offset: 32, shaderLocation: 8 }, // matrix column 2
|
|
36
|
+
{ format: "float32x4", offset: 48, shaderLocation: 9 }, // matrix column 3
|
|
37
|
+
{ format: "float32x4", offset: 64, shaderLocation: 10 }, // position + radius
|
|
38
|
+
{ format: "float32x4", offset: 80, shaderLocation: 11 }, // uvTransform (offset.xy, scale.xy)
|
|
39
|
+
{ format: "float32x4", offset: 96, shaderLocation: 12 }, // color (r, g, b, a)
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const vertexCount = attributes.position.length / 3
|
|
44
|
+
|
|
45
|
+
if (attributes.indices == false) {
|
|
46
|
+
// Generate indices for non-indexed geometry
|
|
47
|
+
const positions = attributes.position;
|
|
48
|
+
const vertexCount = positions.length / 3; // 3 components per vertex
|
|
49
|
+
const indices = new Uint32Array(vertexCount);
|
|
50
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
51
|
+
indices[i] = i;
|
|
52
|
+
}
|
|
53
|
+
attributes.indices = indices
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (attributes.normal == false) {
|
|
57
|
+
// Generate flat normals for non-normal geometry
|
|
58
|
+
const positions = attributes.position;
|
|
59
|
+
const indices = attributes.indices;
|
|
60
|
+
const normals = new Float32Array(positions.length);
|
|
61
|
+
|
|
62
|
+
// Calculate normals for each triangle
|
|
63
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
64
|
+
const i0 = indices[i] * 3;
|
|
65
|
+
const i1 = indices[i + 1] * 3;
|
|
66
|
+
const i2 = indices[i + 2] * 3;
|
|
67
|
+
|
|
68
|
+
// Get vertices of triangle
|
|
69
|
+
const v0 = [positions[i0], positions[i0 + 1], positions[i0 + 2]];
|
|
70
|
+
const v1 = [positions[i1], positions[i1 + 1], positions[i1 + 2]];
|
|
71
|
+
const v2 = [positions[i2], positions[i2 + 1], positions[i2 + 2]];
|
|
72
|
+
|
|
73
|
+
// Calculate vectors for cross product
|
|
74
|
+
const vec1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
|
|
75
|
+
const vec2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
|
|
76
|
+
|
|
77
|
+
// Calculate cross product
|
|
78
|
+
const normal = [
|
|
79
|
+
vec1[1] * vec2[2] - vec1[2] * vec2[1],
|
|
80
|
+
vec1[2] * vec2[0] - vec1[0] * vec2[2],
|
|
81
|
+
vec1[0] * vec2[1] - vec1[1] * vec2[0]
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Normalize
|
|
85
|
+
const length = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]);
|
|
86
|
+
normal[0] /= length;
|
|
87
|
+
normal[1] /= length;
|
|
88
|
+
normal[2] /= length;
|
|
89
|
+
|
|
90
|
+
// Assign same normal to all vertices of triangle
|
|
91
|
+
normals[i0] = normal[0];
|
|
92
|
+
normals[i0 + 1] = normal[1];
|
|
93
|
+
normals[i0 + 2] = normal[2];
|
|
94
|
+
normals[i1] = normal[0];
|
|
95
|
+
normals[i1 + 1] = normal[1];
|
|
96
|
+
normals[i1 + 2] = normal[2];
|
|
97
|
+
normals[i2] = normal[0];
|
|
98
|
+
normals[i2 + 1] = normal[1];
|
|
99
|
+
normals[i2 + 2] = normal[2];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
attributes.normal = normals
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
const vertexArray = new Float32Array(vertexCount * 20);
|
|
107
|
+
this.vertexCount = vertexCount;
|
|
108
|
+
const vertexArrayUint32 = new Uint32Array(vertexArray.buffer);
|
|
109
|
+
const vertexBuffer = device.createBuffer({
|
|
110
|
+
size: vertexArray.byteLength,
|
|
111
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Create instance buffer (start small, grows dynamically)
|
|
115
|
+
const initialMaxInstances = 16;
|
|
116
|
+
const instanceBuffer = device.createBuffer({
|
|
117
|
+
size: 112 * initialMaxInstances, // 28 floats per instance
|
|
118
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
119
|
+
});
|
|
120
|
+
this.instanceBuffer = instanceBuffer;
|
|
121
|
+
this.maxInstances = initialMaxInstances;
|
|
122
|
+
this.instanceCount = 0;
|
|
123
|
+
this.instanceData = new Float32Array(28 * initialMaxInstances);
|
|
124
|
+
this._instanceDataDirty = true
|
|
125
|
+
|
|
126
|
+
const indexArray = new Uint32Array(attributes.indices.length);
|
|
127
|
+
const indexBuffer = device.createBuffer({
|
|
128
|
+
size: indexArray.byteLength,
|
|
129
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
this.attributes = attributes
|
|
133
|
+
this.vertexArray = vertexArray
|
|
134
|
+
this.vertexArrayUint32 = vertexArrayUint32
|
|
135
|
+
this.indexArray = indexArray
|
|
136
|
+
|
|
137
|
+
this.vertexBuffer = vertexBuffer
|
|
138
|
+
this.indexBuffer = indexBuffer
|
|
139
|
+
|
|
140
|
+
this.updateVertexBuffer()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Grow instance buffer capacity by doubling it
|
|
146
|
+
* @param {number} minCapacity - Minimum required capacity (optional)
|
|
147
|
+
*/
|
|
148
|
+
growInstanceBuffer(minCapacity = 0) {
|
|
149
|
+
const { device } = this.engine;
|
|
150
|
+
|
|
151
|
+
// Calculate new size: double current, or enough for minCapacity
|
|
152
|
+
let newMaxInstances = this.maxInstances * 2;
|
|
153
|
+
while (newMaxInstances < minCapacity) {
|
|
154
|
+
newMaxInstances *= 2;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Create new larger buffer
|
|
158
|
+
const newInstanceBuffer = device.createBuffer({
|
|
159
|
+
size: 112 * newMaxInstances, // 28 floats per instance
|
|
160
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Create new larger data array and copy existing data
|
|
164
|
+
const newInstanceData = new Float32Array(28 * newMaxInstances);
|
|
165
|
+
newInstanceData.set(this.instanceData);
|
|
166
|
+
|
|
167
|
+
// Destroy old buffer
|
|
168
|
+
this.instanceBuffer.destroy();
|
|
169
|
+
|
|
170
|
+
// Update references
|
|
171
|
+
this.instanceBuffer = newInstanceBuffer;
|
|
172
|
+
this.instanceData = newInstanceData;
|
|
173
|
+
this.maxInstances = newMaxInstances;
|
|
174
|
+
this._instanceDataDirty = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
addInstance(position, radius = 1, uvTransform = [0, 0, 1, 1], color = [1, 1, 1, 1]) {
|
|
178
|
+
if (this.instanceCount >= this.maxInstances) {
|
|
179
|
+
this.growInstanceBuffer();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Create a 4x4 transform matrix for this instance
|
|
183
|
+
const matrix = mat4.create();
|
|
184
|
+
mat4.translate(matrix, matrix, position);
|
|
185
|
+
|
|
186
|
+
// Add the matrix to instance data at the next available slot
|
|
187
|
+
const offset = this.instanceCount * 28;
|
|
188
|
+
this.instanceData.set(matrix, offset);
|
|
189
|
+
// Position + radius (would normally be set from bsphere)
|
|
190
|
+
this.instanceData[offset + 16] = position[0];
|
|
191
|
+
this.instanceData[offset + 17] = position[1];
|
|
192
|
+
this.instanceData[offset + 18] = position[2];
|
|
193
|
+
this.instanceData[offset + 19] = radius;
|
|
194
|
+
// UV transform
|
|
195
|
+
this.instanceData[offset + 20] = uvTransform[0];
|
|
196
|
+
this.instanceData[offset + 21] = uvTransform[1];
|
|
197
|
+
this.instanceData[offset + 22] = uvTransform[2];
|
|
198
|
+
this.instanceData[offset + 23] = uvTransform[3];
|
|
199
|
+
// Color
|
|
200
|
+
this.instanceData[offset + 24] = color[0];
|
|
201
|
+
this.instanceData[offset + 25] = color[1];
|
|
202
|
+
this.instanceData[offset + 26] = color[2];
|
|
203
|
+
this.instanceData[offset + 27] = color[3];
|
|
204
|
+
|
|
205
|
+
this.instanceCount++;
|
|
206
|
+
this._instanceDataDirty = true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
updateAllInstances(matrices) {
|
|
210
|
+
const { device } = this.engine;
|
|
211
|
+
this.instanceCount = matrices.length;
|
|
212
|
+
if (this.instanceCount > this.maxInstances) {
|
|
213
|
+
this.growInstanceBuffer(this.instanceCount);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Copy all matrices into instance data (28 floats per instance)
|
|
217
|
+
for (let i = 0; i < matrices.length; i++) {
|
|
218
|
+
const offset = i * 28;
|
|
219
|
+
this.instanceData.set(matrices[i], offset);
|
|
220
|
+
// Set default UV transform and color if not provided
|
|
221
|
+
this.instanceData[offset + 20] = 0; // uvOffset.x
|
|
222
|
+
this.instanceData[offset + 21] = 0; // uvOffset.y
|
|
223
|
+
this.instanceData[offset + 22] = 1; // uvScale.x
|
|
224
|
+
this.instanceData[offset + 23] = 1; // uvScale.y
|
|
225
|
+
this.instanceData[offset + 24] = 1; // color.r
|
|
226
|
+
this.instanceData[offset + 25] = 1; // color.g
|
|
227
|
+
this.instanceData[offset + 26] = 1; // color.b
|
|
228
|
+
this.instanceData[offset + 27] = 1; // color.a
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this._instanceDataDirty = true
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
updateInstance(index, matrix) {
|
|
235
|
+
this.instanceData.set(matrix, index * 28);
|
|
236
|
+
this._instanceDataDirty = true
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
writeInstanceBuffer() {
|
|
240
|
+
const { device } = this.engine;
|
|
241
|
+
|
|
242
|
+
device.queue.writeBuffer(this.instanceBuffer, 0, this.instanceData);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
updateVertexBuffer() {
|
|
246
|
+
const { device } = this.engine
|
|
247
|
+
const { attributes, vertexArray, vertexArrayUint32 } = this
|
|
248
|
+
// Interleave vertex attributes into a single array
|
|
249
|
+
for (let i = 0; i < this.vertexCount; i++) {
|
|
250
|
+
const vertexOffset = i * 20; // 20 floats per vertex
|
|
251
|
+
|
|
252
|
+
// Position (xyz)
|
|
253
|
+
vertexArray[vertexOffset] = attributes.position[i * 3];
|
|
254
|
+
vertexArray[vertexOffset + 1] = attributes.position[i * 3 + 1];
|
|
255
|
+
vertexArray[vertexOffset + 2] = attributes.position[i * 3 + 2];
|
|
256
|
+
|
|
257
|
+
// UV (xy)
|
|
258
|
+
vertexArray[vertexOffset + 3] = attributes.uv ? attributes.uv[i * 2] : 0;
|
|
259
|
+
vertexArray[vertexOffset + 4] = attributes.uv ? attributes.uv[i * 2 + 1] : 0;
|
|
260
|
+
|
|
261
|
+
// Normal (xyz)
|
|
262
|
+
vertexArray[vertexOffset + 5] = attributes.normal[i * 3]
|
|
263
|
+
vertexArray[vertexOffset + 6] = attributes.normal[i * 3 + 1]
|
|
264
|
+
vertexArray[vertexOffset + 7] = attributes.normal[i * 3 + 2]
|
|
265
|
+
|
|
266
|
+
// Color (rgba)
|
|
267
|
+
vertexArray[vertexOffset + 8] = attributes.color ? attributes.color[i * 4] : 1;
|
|
268
|
+
vertexArray[vertexOffset + 9] = attributes.color ? attributes.color[i * 4 + 1] : 1;
|
|
269
|
+
vertexArray[vertexOffset + 10] = attributes.color ? attributes.color[i * 4 + 2] : 1;
|
|
270
|
+
vertexArray[vertexOffset + 11] = attributes.color ? attributes.color[i * 4 + 3] : 1;
|
|
271
|
+
|
|
272
|
+
// Weights (xyzw)
|
|
273
|
+
vertexArray[vertexOffset + 12] = attributes.weights ? attributes.weights[i * 4] : 0;
|
|
274
|
+
vertexArray[vertexOffset + 13] = attributes.weights ? attributes.weights[i * 4 + 1] : 0;
|
|
275
|
+
vertexArray[vertexOffset + 14] = attributes.weights ? attributes.weights[i * 4 + 2] : 0;
|
|
276
|
+
vertexArray[vertexOffset + 15] = attributes.weights ? attributes.weights[i * 4 + 3] : 0;
|
|
277
|
+
|
|
278
|
+
// Joints (xyzw) - using Float32Array view to write uint32 values
|
|
279
|
+
const jointsOffset = vertexOffset + 16;
|
|
280
|
+
vertexArrayUint32[jointsOffset] = attributes.joints ? attributes.joints[i * 4] : 0;
|
|
281
|
+
vertexArrayUint32[jointsOffset + 1] = attributes.joints ? attributes.joints[i * 4 + 1] : 0;
|
|
282
|
+
vertexArrayUint32[jointsOffset + 2] = attributes.joints ? attributes.joints[i * 4 + 2] : 0;
|
|
283
|
+
vertexArrayUint32[jointsOffset + 3] = attributes.joints ? attributes.joints[i * 4 + 3] : 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Copy indices
|
|
287
|
+
for (let i = 0; i < attributes.indices.length; i++) {
|
|
288
|
+
this.indexArray[i] = attributes.indices[i];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
device.queue.writeBuffer(this.vertexBuffer, 0, this.vertexArray)
|
|
292
|
+
device.queue.writeBuffer(this.indexBuffer, 0, this.indexArray)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
update() {
|
|
296
|
+
if (this._instanceDataDirty) {
|
|
297
|
+
this.writeInstanceBuffer()
|
|
298
|
+
this._instanceDataDirty = false
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get bounding sphere for this geometry (lazy calculated)
|
|
304
|
+
* @returns {{ center: [number, number, number], radius: number }}
|
|
305
|
+
*/
|
|
306
|
+
getBoundingSphere() {
|
|
307
|
+
if (!this._bsphere) {
|
|
308
|
+
this._bsphere = calculateBoundingSphere(this.attributes.position)
|
|
309
|
+
}
|
|
310
|
+
return this._bsphere
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Invalidate cached bounding sphere (call after modifying positions)
|
|
315
|
+
*/
|
|
316
|
+
invalidateBoundingSphere() {
|
|
317
|
+
this._bsphere = null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
static createCullingBuffers(device, maxInstances) {
|
|
321
|
+
// Input buffer containing instance data (position + radius)
|
|
322
|
+
const instanceDataBuffer = device.createBuffer({
|
|
323
|
+
size: maxInstances * 16, // vec3 position + float radius
|
|
324
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Output buffer containing active instance indices and count
|
|
328
|
+
const culledInstancesBuffer = device.createBuffer({
|
|
329
|
+
size: (maxInstances + 1) * 4, // indices + count at start
|
|
330
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Uniform buffer for camera frustum planes
|
|
334
|
+
const frustumBuffer = device.createBuffer({
|
|
335
|
+
size: 96, // 6 planes * vec4
|
|
336
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const computePipeline = device.createComputePipeline({
|
|
340
|
+
layout: 'auto',
|
|
341
|
+
compute: {
|
|
342
|
+
module: device.createShaderModule({
|
|
343
|
+
code: `
|
|
344
|
+
struct Instance {
|
|
345
|
+
position: vec3f,
|
|
346
|
+
radius: f32
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
struct FrustumPlanes {
|
|
350
|
+
planes: array<vec4f, 6>
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@group(0) @binding(0) var<storage, read> instances: array<Instance>;
|
|
354
|
+
@group(0) @binding(1) var<storage, read_write> culledInstances: array<u32>;
|
|
355
|
+
@group(0) @binding(2) var<uniform> frustum: FrustumPlanes;
|
|
356
|
+
|
|
357
|
+
fn sphereAgainstPlane(center: vec3f, radius: f32, plane: vec4f) -> bool {
|
|
358
|
+
let dist = dot(vec4f(center, 1.0), plane);
|
|
359
|
+
return dist > -radius;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@compute @workgroup_size(64)
|
|
363
|
+
fn main(@builtin(global_invocation_id) global_id: vec3u) {
|
|
364
|
+
let index = global_id.x;
|
|
365
|
+
if (index >= arrayLength(&instances)) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let instance = instances[index];
|
|
370
|
+
|
|
371
|
+
// Test against all frustum planes
|
|
372
|
+
var visible = true;
|
|
373
|
+
for (var i = 0u; i < 6u; i++) {
|
|
374
|
+
if (!sphereAgainstPlane(instance.position, instance.radius, frustum.planes[i])) {
|
|
375
|
+
visible = false;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (visible) {
|
|
381
|
+
let oldCount = atomicAdd(&culledInstances[0], 1);
|
|
382
|
+
culledInstances[oldCount + 1] = index;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
`
|
|
386
|
+
}),
|
|
387
|
+
entryPoint: 'main'
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const bindGroup = device.createBindGroup({
|
|
392
|
+
layout: computePipeline.getBindGroupLayout(0),
|
|
393
|
+
entries: [
|
|
394
|
+
{
|
|
395
|
+
binding: 0,
|
|
396
|
+
resource: { buffer: instanceDataBuffer }
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
binding: 1,
|
|
400
|
+
resource: { buffer: culledInstancesBuffer }
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
binding: 2,
|
|
404
|
+
resource: { buffer: frustumBuffer }
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
pipeline: computePipeline,
|
|
411
|
+
bindGroup: bindGroup,
|
|
412
|
+
instanceDataBuffer,
|
|
413
|
+
culledInstancesBuffer,
|
|
414
|
+
frustumBuffer
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
static cullInstances(engine, commandEncoder, cullingData, instances, frustumPlanes) {
|
|
419
|
+
const { device } = engine;
|
|
420
|
+
const { pipeline, bindGroup, instanceDataBuffer, culledInstancesBuffer, frustumBuffer } = cullingData;
|
|
421
|
+
|
|
422
|
+
// Write instance data
|
|
423
|
+
device.queue.writeBuffer(instanceDataBuffer, 0, instances);
|
|
424
|
+
|
|
425
|
+
// Write frustum planes
|
|
426
|
+
device.queue.writeBuffer(frustumBuffer, 0, frustumPlanes);
|
|
427
|
+
|
|
428
|
+
// Clear count to 0
|
|
429
|
+
const zeros = new Uint32Array(1);
|
|
430
|
+
device.queue.writeBuffer(culledInstancesBuffer, 0, zeros);
|
|
431
|
+
|
|
432
|
+
// Dispatch compute shader
|
|
433
|
+
const computePass = commandEncoder.beginComputePass();
|
|
434
|
+
computePass.setPipeline(pipeline);
|
|
435
|
+
computePass.setBindGroup(0, bindGroup);
|
|
436
|
+
computePass.dispatchWorkgroups(Math.ceil(instances.length / 64));
|
|
437
|
+
computePass.end();
|
|
438
|
+
|
|
439
|
+
return culledInstancesBuffer;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
static cube(engine) {
|
|
443
|
+
return Geometry.box(engine, 1, 1, 1, 1)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
static box(engine, width = 1, height = 1, depth = 1, uvSize = 1) {
|
|
447
|
+
const w = width / 2
|
|
448
|
+
const h = height / 2
|
|
449
|
+
const d = depth / 2
|
|
450
|
+
|
|
451
|
+
// UV scale based on dimensions and uvSize
|
|
452
|
+
const uw = width / uvSize
|
|
453
|
+
const uh = height / uvSize
|
|
454
|
+
const ud = depth / uvSize
|
|
455
|
+
|
|
456
|
+
return new Geometry(engine, {
|
|
457
|
+
position: new Float32Array([
|
|
458
|
+
// Front face
|
|
459
|
+
-w, -h, d,
|
|
460
|
+
w, -h, d,
|
|
461
|
+
w, h, d,
|
|
462
|
+
-w, h, d,
|
|
463
|
+
// Back face
|
|
464
|
+
-w, -h, -d,
|
|
465
|
+
-w, h, -d,
|
|
466
|
+
w, h, -d,
|
|
467
|
+
w, -h, -d,
|
|
468
|
+
// Top face
|
|
469
|
+
-w, h, -d,
|
|
470
|
+
-w, h, d,
|
|
471
|
+
w, h, d,
|
|
472
|
+
w, h, -d,
|
|
473
|
+
// Bottom face
|
|
474
|
+
-w, -h, -d,
|
|
475
|
+
w, -h, -d,
|
|
476
|
+
w, -h, d,
|
|
477
|
+
-w, -h, d,
|
|
478
|
+
// Right face
|
|
479
|
+
w, -h, -d,
|
|
480
|
+
w, h, -d,
|
|
481
|
+
w, h, d,
|
|
482
|
+
w, -h, d,
|
|
483
|
+
// Left face
|
|
484
|
+
-w, -h, -d,
|
|
485
|
+
-w, -h, d,
|
|
486
|
+
-w, h, d,
|
|
487
|
+
-w, h, -d,
|
|
488
|
+
]),
|
|
489
|
+
uv: new Float32Array([
|
|
490
|
+
// Front face
|
|
491
|
+
0, 0,
|
|
492
|
+
uw, 0,
|
|
493
|
+
uw, uh,
|
|
494
|
+
0, uh,
|
|
495
|
+
// Back face
|
|
496
|
+
0, 0,
|
|
497
|
+
0, uh,
|
|
498
|
+
uw, uh,
|
|
499
|
+
uw, 0,
|
|
500
|
+
// Top face
|
|
501
|
+
0, 0,
|
|
502
|
+
0, ud,
|
|
503
|
+
uw, ud,
|
|
504
|
+
uw, 0,
|
|
505
|
+
// Bottom face
|
|
506
|
+
0, 0,
|
|
507
|
+
uw, 0,
|
|
508
|
+
uw, ud,
|
|
509
|
+
0, ud,
|
|
510
|
+
// Right face
|
|
511
|
+
0, 0,
|
|
512
|
+
0, uh,
|
|
513
|
+
ud, uh,
|
|
514
|
+
ud, 0,
|
|
515
|
+
// Left face
|
|
516
|
+
0, 0,
|
|
517
|
+
ud, 0,
|
|
518
|
+
ud, uh,
|
|
519
|
+
0, uh,
|
|
520
|
+
]),
|
|
521
|
+
normal: new Float32Array([
|
|
522
|
+
// Front face
|
|
523
|
+
0, 0, 1,
|
|
524
|
+
0, 0, 1,
|
|
525
|
+
0, 0, 1,
|
|
526
|
+
0, 0, 1,
|
|
527
|
+
// Back face
|
|
528
|
+
0, 0, -1,
|
|
529
|
+
0, 0, -1,
|
|
530
|
+
0, 0, -1,
|
|
531
|
+
0, 0, -1,
|
|
532
|
+
// Top face
|
|
533
|
+
0, 1, 0,
|
|
534
|
+
0, 1, 0,
|
|
535
|
+
0, 1, 0,
|
|
536
|
+
0, 1, 0,
|
|
537
|
+
// Bottom face
|
|
538
|
+
0, -1, 0,
|
|
539
|
+
0, -1, 0,
|
|
540
|
+
0, -1, 0,
|
|
541
|
+
0, -1, 0,
|
|
542
|
+
// Right face
|
|
543
|
+
1, 0, 0,
|
|
544
|
+
1, 0, 0,
|
|
545
|
+
1, 0, 0,
|
|
546
|
+
1, 0, 0,
|
|
547
|
+
// Left face
|
|
548
|
+
-1, 0, 0,
|
|
549
|
+
-1, 0, 0,
|
|
550
|
+
-1, 0, 0,
|
|
551
|
+
-1, 0, 0,
|
|
552
|
+
]),
|
|
553
|
+
indices: new Uint32Array([
|
|
554
|
+
0, 1, 2, 2, 3, 0, // Front face
|
|
555
|
+
4, 5, 6, 6, 7, 4, // Back face
|
|
556
|
+
8, 9, 10, 10, 11, 8, // Top face
|
|
557
|
+
12, 13, 14, 14, 15, 12, // Bottom face
|
|
558
|
+
16, 17, 18, 18, 19, 16, // Right face
|
|
559
|
+
20, 21, 22, 22, 23, 20 // Left face
|
|
560
|
+
])
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
static sphere(engine, radius = 1, widthSegments = 32, heightSegments = 16) {
|
|
564
|
+
const positions = []
|
|
565
|
+
const normals = []
|
|
566
|
+
const uvs = []
|
|
567
|
+
const indices = []
|
|
568
|
+
|
|
569
|
+
// Generate vertices
|
|
570
|
+
for (let y = 0; y <= heightSegments; y++) {
|
|
571
|
+
const v = y / heightSegments
|
|
572
|
+
const phi = v * Math.PI
|
|
573
|
+
|
|
574
|
+
for (let x = 0; x <= widthSegments; x++) {
|
|
575
|
+
const u = x / widthSegments
|
|
576
|
+
const theta = u * Math.PI * 2
|
|
577
|
+
|
|
578
|
+
// Calculate vertex position
|
|
579
|
+
const px = -radius * Math.cos(theta) * Math.sin(phi)
|
|
580
|
+
const py = radius * Math.cos(phi)
|
|
581
|
+
const pz = radius * Math.sin(theta) * Math.sin(phi)
|
|
582
|
+
|
|
583
|
+
positions.push(px, py, pz)
|
|
584
|
+
|
|
585
|
+
// Normal is just the normalized position for a sphere
|
|
586
|
+
const length = Math.sqrt(px * px + py * py + pz * pz)
|
|
587
|
+
normals.push(px / length, py / length, pz / length)
|
|
588
|
+
|
|
589
|
+
// UV coordinates
|
|
590
|
+
uvs.push(u, 1 - v)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Generate indices
|
|
595
|
+
for (let y = 0; y < heightSegments; y++) {
|
|
596
|
+
for (let x = 0; x < widthSegments; x++) {
|
|
597
|
+
const a = y * (widthSegments + 1) + x
|
|
598
|
+
const b = a + 1
|
|
599
|
+
const c = a + widthSegments + 1
|
|
600
|
+
const d = c + 1
|
|
601
|
+
|
|
602
|
+
// Generate two triangles for each quad (counter-clockwise winding)
|
|
603
|
+
indices.push(a, d, b)
|
|
604
|
+
indices.push(a, c, d)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return new Geometry(engine, {
|
|
609
|
+
position: new Float32Array(positions),
|
|
610
|
+
normal: new Float32Array(normals),
|
|
611
|
+
uv: new Float32Array(uvs),
|
|
612
|
+
indices: new Uint32Array(indices)
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
static quad(engine) {
|
|
617
|
+
return new Geometry(engine, {
|
|
618
|
+
position: [
|
|
619
|
+
-1, -1, 0,
|
|
620
|
+
1, -1, 0,
|
|
621
|
+
1, 1, 0,
|
|
622
|
+
-1, 1, 0,
|
|
623
|
+
],
|
|
624
|
+
uv: [
|
|
625
|
+
0, 0,
|
|
626
|
+
1, 0,
|
|
627
|
+
1, 1,
|
|
628
|
+
0, 1,
|
|
629
|
+
],
|
|
630
|
+
normal: [
|
|
631
|
+
// Normals up
|
|
632
|
+
0, 1, 0,
|
|
633
|
+
0, 1, 0,
|
|
634
|
+
0, 1, 0,
|
|
635
|
+
0, 1, 0,
|
|
636
|
+
],
|
|
637
|
+
index: [
|
|
638
|
+
0, 1, 2,
|
|
639
|
+
2, 3, 0
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Create a quad geometry for billboard/sprite rendering
|
|
647
|
+
* @param {Engine} engine - Engine instance
|
|
648
|
+
* @param {string} pivot - Pivot mode: 'center', 'bottom', or 'horizontal'
|
|
649
|
+
* @returns {Geometry} Billboard quad geometry
|
|
650
|
+
*/
|
|
651
|
+
static billboardQuad(engine, pivot = 'center') {
|
|
652
|
+
let position, normal
|
|
653
|
+
|
|
654
|
+
if (pivot === 'center') {
|
|
655
|
+
// Center pivot: quad centered at origin, facing +Z
|
|
656
|
+
position = new Float32Array([
|
|
657
|
+
-0.5, -0.5, 0,
|
|
658
|
+
0.5, -0.5, 0,
|
|
659
|
+
0.5, 0.5, 0,
|
|
660
|
+
-0.5, 0.5, 0,
|
|
661
|
+
])
|
|
662
|
+
normal = new Float32Array([
|
|
663
|
+
0, 0, 1,
|
|
664
|
+
0, 0, 1,
|
|
665
|
+
0, 0, 1,
|
|
666
|
+
0, 0, 1,
|
|
667
|
+
])
|
|
668
|
+
} else if (pivot === 'bottom') {
|
|
669
|
+
// Bottom pivot: quad with bottom edge at origin, facing +Z
|
|
670
|
+
position = new Float32Array([
|
|
671
|
+
-0.5, 0, 0,
|
|
672
|
+
0.5, 0, 0,
|
|
673
|
+
0.5, 1, 0,
|
|
674
|
+
-0.5, 1, 0,
|
|
675
|
+
])
|
|
676
|
+
normal = new Float32Array([
|
|
677
|
+
0, 0, 1,
|
|
678
|
+
0, 0, 1,
|
|
679
|
+
0, 0, 1,
|
|
680
|
+
0, 0, 1,
|
|
681
|
+
])
|
|
682
|
+
} else if (pivot === 'horizontal') {
|
|
683
|
+
// Horizontal pivot: quad flat on XZ plane (ground decal)
|
|
684
|
+
// Front face points UP (+Y) with CCW winding when viewed from above
|
|
685
|
+
position = new Float32Array([
|
|
686
|
+
-0.5, 0, -0.5, // 0: back left
|
|
687
|
+
0.5, 0, -0.5, // 1: back right
|
|
688
|
+
0.5, 0, 0.5, // 2: front right
|
|
689
|
+
-0.5, 0, 0.5, // 3: front left
|
|
690
|
+
])
|
|
691
|
+
normal = new Float32Array([
|
|
692
|
+
0, 1, 0,
|
|
693
|
+
0, 1, 0,
|
|
694
|
+
0, 1, 0,
|
|
695
|
+
0, 1, 0,
|
|
696
|
+
])
|
|
697
|
+
// Use reversed winding for horizontal (CCW from above)
|
|
698
|
+
return new Geometry(engine, {
|
|
699
|
+
position,
|
|
700
|
+
uv: new Float32Array([
|
|
701
|
+
0, 1, // back left -> top-left of texture
|
|
702
|
+
1, 1, // back right -> top-right
|
|
703
|
+
1, 0, // front right -> bottom-right
|
|
704
|
+
0, 0, // front left -> bottom-left
|
|
705
|
+
]),
|
|
706
|
+
normal,
|
|
707
|
+
indices: new Uint32Array([
|
|
708
|
+
0, 3, 2, // CCW: back-left, front-left, front-right
|
|
709
|
+
2, 1, 0 // CCW: front-right, back-right, back-left
|
|
710
|
+
])
|
|
711
|
+
})
|
|
712
|
+
} else {
|
|
713
|
+
// Default to center pivot
|
|
714
|
+
return Geometry.billboardQuad(engine, 'center')
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return new Geometry(engine, {
|
|
718
|
+
position,
|
|
719
|
+
uv: new Float32Array([
|
|
720
|
+
0, 0, // Bottom-left: sample from v=0 (bottom of texture)
|
|
721
|
+
1, 0, // Bottom-right
|
|
722
|
+
1, 1, // Top-right: sample from v=1 (top of texture)
|
|
723
|
+
0, 1, // Top-left
|
|
724
|
+
]),
|
|
725
|
+
normal,
|
|
726
|
+
indices: new Uint32Array([
|
|
727
|
+
0, 1, 2,
|
|
728
|
+
2, 3, 0
|
|
729
|
+
])
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
simplify(angleThreshold = 0.1) {
|
|
734
|
+
// Early return if no UV coordinates or not enough vertices
|
|
735
|
+
if (!this.attributes.uv || this.attributes.indices.length < 6) {
|
|
736
|
+
return this;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const positions = this.attributes.position;
|
|
740
|
+
const normals = this.attributes.normal;
|
|
741
|
+
const uvs = this.attributes.uv;
|
|
742
|
+
const indices = this.attributes.indices;
|
|
743
|
+
const colors = this.attributes.color;
|
|
744
|
+
const joints = this.attributes.joints;
|
|
745
|
+
const weights = this.attributes.weights;
|
|
746
|
+
|
|
747
|
+
let newPositions = [];
|
|
748
|
+
let newNormals = [];
|
|
749
|
+
let newUVs = [];
|
|
750
|
+
let newIndices = [];
|
|
751
|
+
let newColors = colors ? [] : null;
|
|
752
|
+
let newJoints = joints ? [] : null;
|
|
753
|
+
let newWeights = weights ? [] : null;
|
|
754
|
+
|
|
755
|
+
// Process triangles in pairs
|
|
756
|
+
for (let i = 0; i < indices.length; i += 6) {
|
|
757
|
+
if (i + 5 >= indices.length) {
|
|
758
|
+
// Add remaining triangle if we can't make a pair
|
|
759
|
+
for (let j = 0; j < 3; j++) {
|
|
760
|
+
const idx = indices[i + j];
|
|
761
|
+
newPositions.push(positions[idx * 3], positions[idx * 3 + 1], positions[idx * 3 + 2]);
|
|
762
|
+
newNormals.push(normals[idx * 3], normals[idx * 3 + 1], normals[idx * 3 + 2]);
|
|
763
|
+
newUVs.push(uvs[idx * 2], uvs[idx * 2 + 1]);
|
|
764
|
+
if (colors) {
|
|
765
|
+
newColors.push(colors[idx * 4], colors[idx * 4 + 1], colors[idx * 4 + 2], colors[idx * 4 + 3]);
|
|
766
|
+
}
|
|
767
|
+
if (joints) {
|
|
768
|
+
newJoints.push(joints[idx * 4], joints[idx * 4 + 1], joints[idx * 4 + 2], joints[idx * 4 + 3]);
|
|
769
|
+
}
|
|
770
|
+
if (weights) {
|
|
771
|
+
newWeights.push(weights[idx * 4], weights[idx * 4 + 1], weights[idx * 4 + 2], weights[idx * 4 + 3]);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
newIndices.push(newPositions.length / 3 - 3, newPositions.length / 3 - 2, newPositions.length / 3 - 1);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Get the two triangles
|
|
779
|
+
const tri1 = [indices[i], indices[i + 1], indices[i + 2]];
|
|
780
|
+
const tri2 = [indices[i + 3], indices[i + 4], indices[i + 5]];
|
|
781
|
+
|
|
782
|
+
// Calculate normals of both triangles
|
|
783
|
+
const normal1 = new Vector3(
|
|
784
|
+
normals[tri1[0] * 3], normals[tri1[0] * 3 + 1], normals[tri1[0] * 3 + 2]
|
|
785
|
+
);
|
|
786
|
+
const normal2 = new Vector3(
|
|
787
|
+
normals[tri2[0] * 3], normals[tri2[0] * 3 + 1], normals[tri2[0] * 3 + 2]
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
// Check if triangles are nearly coplanar
|
|
791
|
+
const angle = Math.acos(normal1.dot(normal2));
|
|
792
|
+
|
|
793
|
+
if (angle < angleThreshold) {
|
|
794
|
+
// Find shared vertices
|
|
795
|
+
const sharedVertices = tri1.filter(v => tri2.includes(v));
|
|
796
|
+
|
|
797
|
+
if (sharedVertices.length === 2) {
|
|
798
|
+
// Get unique vertices
|
|
799
|
+
const uniqueFromTri1 = tri1.find(v => !tri2.includes(v));
|
|
800
|
+
const uniqueFromTri2 = tri2.find(v => !tri1.includes(v));
|
|
801
|
+
|
|
802
|
+
// Create new quad from the four points
|
|
803
|
+
const quadIndices = [uniqueFromTri1, ...sharedVertices, uniqueFromTri2];
|
|
804
|
+
|
|
805
|
+
// Add vertices to new arrays
|
|
806
|
+
for (const idx of quadIndices) {
|
|
807
|
+
newPositions.push(positions[idx * 3], positions[idx * 3 + 1], positions[idx * 3 + 2]);
|
|
808
|
+
newNormals.push(normals[idx * 3], normals[idx * 3 + 1], normals[idx * 3 + 2]);
|
|
809
|
+
newUVs.push(uvs[idx * 2], uvs[idx * 2 + 1]);
|
|
810
|
+
if (colors) {
|
|
811
|
+
newColors.push(colors[idx * 4], colors[idx * 4 + 1], colors[idx * 4 + 2], colors[idx * 4 + 3]);
|
|
812
|
+
}
|
|
813
|
+
if (joints) {
|
|
814
|
+
newJoints.push(joints[idx * 4], joints[idx * 4 + 1], joints[idx * 4 + 2], joints[idx * 4 + 3]);
|
|
815
|
+
}
|
|
816
|
+
if (weights) {
|
|
817
|
+
newWeights.push(weights[idx * 4], weights[idx * 4 + 1], weights[idx * 4 + 2], weights[idx * 4 + 3]);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Add indices for the simplified quad (as two triangles)
|
|
822
|
+
const baseIndex = newPositions.length / 3 - 4;
|
|
823
|
+
newIndices.push(
|
|
824
|
+
baseIndex, baseIndex + 1, baseIndex + 2,
|
|
825
|
+
baseIndex + 2, baseIndex + 3, baseIndex
|
|
826
|
+
);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// If we can't simplify, add original triangles
|
|
832
|
+
for (let j = 0; j < 6; j++) {
|
|
833
|
+
const idx = indices[i + j];
|
|
834
|
+
newPositions.push(positions[idx * 3], positions[idx * 3 + 1], positions[idx * 3 + 2]);
|
|
835
|
+
newNormals.push(normals[idx * 3], normals[idx * 3 + 1], normals[idx * 3 + 2]);
|
|
836
|
+
newUVs.push(uvs[idx * 2], uvs[idx * 2 + 1]);
|
|
837
|
+
if (colors) {
|
|
838
|
+
newColors.push(colors[idx * 4], colors[idx * 4 + 1], colors[idx * 4 + 2], colors[idx * 4 + 3]);
|
|
839
|
+
}
|
|
840
|
+
if (joints) {
|
|
841
|
+
newJoints.push(joints[idx * 4], joints[idx * 4 + 1], joints[idx * 4 + 2], joints[idx * 4 + 3]);
|
|
842
|
+
}
|
|
843
|
+
if (weights) {
|
|
844
|
+
newWeights.push(weights[idx * 4], weights[idx * 4 + 1], weights[idx * 4 + 2], weights[idx * 4 + 3]);
|
|
845
|
+
}
|
|
846
|
+
newIndices.push(newPositions.length / 3 - 1);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const geometry = {
|
|
851
|
+
position: new Float32Array(newPositions),
|
|
852
|
+
normal: new Float32Array(newNormals),
|
|
853
|
+
uv: new Float32Array(newUVs),
|
|
854
|
+
index: new Uint32Array(newIndices)
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
if (colors) {
|
|
858
|
+
geometry.color = new Float32Array(newColors);
|
|
859
|
+
}
|
|
860
|
+
if (joints) {
|
|
861
|
+
geometry.joints = new Uint32Array(newJoints);
|
|
862
|
+
}
|
|
863
|
+
if (weights) {
|
|
864
|
+
geometry.weights = new Float32Array(newWeights);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return new Geometry(geometry);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
static unrepetizeUVs(geometry) {
|
|
871
|
+
const positions = geometry.attributes.position;
|
|
872
|
+
const normals = geometry.attributes.normal;
|
|
873
|
+
const uvs = geometry.attributes.uv;
|
|
874
|
+
const indices = geometry.attributes.indices;
|
|
875
|
+
const colors = geometry.attributes.color;
|
|
876
|
+
const joints = geometry.attributes.joints;
|
|
877
|
+
const weights = geometry.attributes.weights;
|
|
878
|
+
|
|
879
|
+
const newPositions = [];
|
|
880
|
+
const newNormals = [];
|
|
881
|
+
const newUVs = [];
|
|
882
|
+
const newIndices = [];
|
|
883
|
+
const newColors = colors ? [] : null;
|
|
884
|
+
const newJoints = joints ? [] : null;
|
|
885
|
+
const newWeights = weights ? [] : null;
|
|
886
|
+
|
|
887
|
+
// Process each triangle
|
|
888
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
889
|
+
const tri = [indices[i], indices[i + 1], indices[i + 2]];
|
|
890
|
+
|
|
891
|
+
// Get UV coordinates for the triangle
|
|
892
|
+
const uvCoords = tri.map(idx => [uvs[idx * 2], uvs[idx * 2 + 1]]);
|
|
893
|
+
|
|
894
|
+
// Get integer and fractional parts of UVs
|
|
895
|
+
const uvInts = uvCoords.map(uv => [
|
|
896
|
+
Math.floor(Math.abs(uv[0])),
|
|
897
|
+
Math.floor(Math.abs(uv[1]))
|
|
898
|
+
]);
|
|
899
|
+
const uvFracs = uvCoords.map(uv => [
|
|
900
|
+
Math.abs(uv[0]) % 1,
|
|
901
|
+
Math.abs(uv[1]) % 1
|
|
902
|
+
]);
|
|
903
|
+
|
|
904
|
+
// Find max UV integer values to determine grid size
|
|
905
|
+
const maxU = Math.max(...uvInts.map(uv => uv[0]));
|
|
906
|
+
const maxV = Math.max(...uvInts.map(uv => uv[1]));
|
|
907
|
+
|
|
908
|
+
if (maxU > 0 || maxV > 0) {
|
|
909
|
+
// Need to split into grid
|
|
910
|
+
for (let u = 0; u <= maxU; u++) {
|
|
911
|
+
for (let v = 0; v <= maxV; v++) {
|
|
912
|
+
// Generate vertices for this grid cell
|
|
913
|
+
const cellVertices = [];
|
|
914
|
+
|
|
915
|
+
// For each vertex of original triangle
|
|
916
|
+
for (let j = 0; j < 3; j++) {
|
|
917
|
+
const origPos = [
|
|
918
|
+
positions[tri[j] * 3],
|
|
919
|
+
positions[tri[j] * 3 + 1],
|
|
920
|
+
positions[tri[j] * 3 + 2]
|
|
921
|
+
];
|
|
922
|
+
const origNorm = [
|
|
923
|
+
normals[tri[j] * 3],
|
|
924
|
+
normals[tri[j] * 3 + 1],
|
|
925
|
+
normals[tri[j] * 3 + 2]
|
|
926
|
+
];
|
|
927
|
+
|
|
928
|
+
// Calculate UV for this cell
|
|
929
|
+
const cellUV = [
|
|
930
|
+
(uvFracs[j][0] + u) / (maxU + 1),
|
|
931
|
+
(uvFracs[j][1] + v) / (maxV + 1)
|
|
932
|
+
];
|
|
933
|
+
|
|
934
|
+
const idx = newPositions.length / 3;
|
|
935
|
+
newPositions.push(...origPos);
|
|
936
|
+
newNormals.push(...origNorm);
|
|
937
|
+
newUVs.push(...cellUV);
|
|
938
|
+
|
|
939
|
+
if (colors) {
|
|
940
|
+
newColors.push(
|
|
941
|
+
colors[tri[j] * 4],
|
|
942
|
+
colors[tri[j] * 4 + 1],
|
|
943
|
+
colors[tri[j] * 4 + 2],
|
|
944
|
+
colors[tri[j] * 4 + 3]
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
if (joints) {
|
|
948
|
+
newJoints.push(
|
|
949
|
+
joints[tri[j] * 4],
|
|
950
|
+
joints[tri[j] * 4 + 1],
|
|
951
|
+
joints[tri[j] * 4 + 2],
|
|
952
|
+
joints[tri[j] * 4 + 3]
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
if (weights) {
|
|
956
|
+
newWeights.push(
|
|
957
|
+
weights[tri[j] * 4],
|
|
958
|
+
weights[tri[j] * 4 + 1],
|
|
959
|
+
weights[tri[j] * 4 + 2],
|
|
960
|
+
weights[tri[j] * 4 + 3]
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
cellVertices.push(idx);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Add triangle indices for this cell
|
|
968
|
+
newIndices.push(
|
|
969
|
+
cellVertices[0],
|
|
970
|
+
cellVertices[1],
|
|
971
|
+
cellVertices[2]
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
} else {
|
|
977
|
+
// Triangle doesn't need splitting, add as-is with normalized UVs
|
|
978
|
+
for (let j = 0; j < 3; j++) {
|
|
979
|
+
const idx = tri[j];
|
|
980
|
+
newPositions.push(
|
|
981
|
+
positions[idx * 3],
|
|
982
|
+
positions[idx * 3 + 1],
|
|
983
|
+
positions[idx * 3 + 2]
|
|
984
|
+
);
|
|
985
|
+
newNormals.push(
|
|
986
|
+
normals[idx * 3],
|
|
987
|
+
normals[idx * 3 + 1],
|
|
988
|
+
normals[idx * 3 + 2]
|
|
989
|
+
);
|
|
990
|
+
newUVs.push(
|
|
991
|
+
uvFracs[j][0],
|
|
992
|
+
uvFracs[j][1]
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
if (colors) {
|
|
996
|
+
newColors.push(
|
|
997
|
+
colors[idx * 4],
|
|
998
|
+
colors[idx * 4 + 1],
|
|
999
|
+
colors[idx * 4 + 2],
|
|
1000
|
+
colors[idx * 4 + 3]
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
if (joints) {
|
|
1004
|
+
newJoints.push(
|
|
1005
|
+
joints[idx * 4],
|
|
1006
|
+
joints[idx * 4 + 1],
|
|
1007
|
+
joints[idx * 4 + 2],
|
|
1008
|
+
joints[idx * 4 + 3]
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
if (weights) {
|
|
1012
|
+
newWeights.push(
|
|
1013
|
+
weights[idx * 4],
|
|
1014
|
+
weights[idx * 4 + 1],
|
|
1015
|
+
weights[idx * 4 + 2],
|
|
1016
|
+
weights[idx * 4 + 3]
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const baseIdx = newPositions.length / 3 - 3;
|
|
1022
|
+
newIndices.push(baseIdx, baseIdx + 1, baseIdx + 2);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const result = {
|
|
1027
|
+
position: new Float32Array(newPositions),
|
|
1028
|
+
normal: new Float32Array(newNormals),
|
|
1029
|
+
uv: new Float32Array(newUVs),
|
|
1030
|
+
index: new Uint32Array(newIndices)
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
if (colors) {
|
|
1034
|
+
result.color = new Float32Array(newColors);
|
|
1035
|
+
}
|
|
1036
|
+
if (joints) {
|
|
1037
|
+
result.joints = new Uint32Array(newJoints);
|
|
1038
|
+
}
|
|
1039
|
+
if (weights) {
|
|
1040
|
+
result.weights = new Float32Array(newWeights);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return new Geometry(result);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
export { Geometry }
|