@ifc-lite/renderer 1.0.0 → 1.1.1
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/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +4 -2
- package/dist/camera.js.map +1 -1
- package/dist/index.d.ts +31 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +469 -25
- package/dist/index.js.map +1 -1
- package/dist/picker.d.ts +4 -0
- package/dist/picker.d.ts.map +1 -1
- package/dist/picker.js +33 -1
- package/dist/picker.js.map +1 -1
- package/dist/pipeline.d.ts +48 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +294 -6
- package/dist/pipeline.js.map +1 -1
- package/dist/scene.d.ts +53 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +207 -0
- package/dist/scene.js.map +1 -1
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @ifc-lite/renderer - WebGPU renderer
|
|
6
6
|
*/
|
|
7
7
|
export { WebGPUDevice } from './device.js';
|
|
8
|
-
export { RenderPipeline } from './pipeline.js';
|
|
8
|
+
export { RenderPipeline, InstancedRenderPipeline } from './pipeline.js';
|
|
9
9
|
export { Camera } from './camera.js';
|
|
10
10
|
export { Scene } from './scene.js';
|
|
11
11
|
export { Picker } from './picker.js';
|
|
@@ -13,18 +13,21 @@ export { MathUtils } from './math.js';
|
|
|
13
13
|
export { SectionPlaneRenderer } from './section-plane.js';
|
|
14
14
|
export * from './types.js';
|
|
15
15
|
import { WebGPUDevice } from './device.js';
|
|
16
|
-
import { RenderPipeline } from './pipeline.js';
|
|
16
|
+
import { RenderPipeline, InstancedRenderPipeline } from './pipeline.js';
|
|
17
17
|
import { Camera } from './camera.js';
|
|
18
18
|
import { Scene } from './scene.js';
|
|
19
19
|
import { Picker } from './picker.js';
|
|
20
20
|
import { FrustumUtils } from '@ifc-lite/spatial';
|
|
21
21
|
import { SectionPlaneRenderer } from './section-plane.js';
|
|
22
|
+
import { deduplicateMeshes } from '@ifc-lite/geometry';
|
|
23
|
+
import { MathUtils } from './math.js';
|
|
22
24
|
/**
|
|
23
25
|
* Main renderer class
|
|
24
26
|
*/
|
|
25
27
|
export class Renderer {
|
|
26
28
|
device;
|
|
27
29
|
pipeline = null;
|
|
30
|
+
instancedPipeline = null;
|
|
28
31
|
camera;
|
|
29
32
|
scene;
|
|
30
33
|
picker = null;
|
|
@@ -52,6 +55,7 @@ export class Renderer {
|
|
|
52
55
|
this.canvas.height = height;
|
|
53
56
|
}
|
|
54
57
|
this.pipeline = new RenderPipeline(this.device, width, height);
|
|
58
|
+
this.instancedPipeline = new InstancedRenderPipeline(this.device, width, height);
|
|
55
59
|
this.picker = new Picker(this.device, width, height);
|
|
56
60
|
this.sectionPlaneRenderer = new SectionPlaneRenderer(this.device.getDevice(), this.device.getFormat());
|
|
57
61
|
this.camera.setAspect(width / height);
|
|
@@ -81,6 +85,217 @@ export class Renderer {
|
|
|
81
85
|
}
|
|
82
86
|
this.scene.addMesh(mesh);
|
|
83
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Add instanced geometry to scene
|
|
90
|
+
* Converts InstancedGeometry from geometry package to InstancedMesh for rendering
|
|
91
|
+
*/
|
|
92
|
+
addInstancedGeometry(geometry) {
|
|
93
|
+
if (!this.instancedPipeline || !this.device.isInitialized()) {
|
|
94
|
+
throw new Error('Renderer not initialized. Call init() first.');
|
|
95
|
+
}
|
|
96
|
+
const device = this.device.getDevice();
|
|
97
|
+
// Upload positions and normals interleaved
|
|
98
|
+
const vertexCount = geometry.positions.length / 3;
|
|
99
|
+
const vertexData = new Float32Array(vertexCount * 6);
|
|
100
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
101
|
+
vertexData[i * 6 + 0] = geometry.positions[i * 3 + 0];
|
|
102
|
+
vertexData[i * 6 + 1] = geometry.positions[i * 3 + 1];
|
|
103
|
+
vertexData[i * 6 + 2] = geometry.positions[i * 3 + 2];
|
|
104
|
+
vertexData[i * 6 + 3] = geometry.normals[i * 3 + 0];
|
|
105
|
+
vertexData[i * 6 + 4] = geometry.normals[i * 3 + 1];
|
|
106
|
+
vertexData[i * 6 + 5] = geometry.normals[i * 3 + 2];
|
|
107
|
+
}
|
|
108
|
+
// Create vertex buffer with exact size needed (ensure it matches data size)
|
|
109
|
+
const vertexBufferSize = vertexData.byteLength;
|
|
110
|
+
const vertexBuffer = device.createBuffer({
|
|
111
|
+
size: vertexBufferSize,
|
|
112
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
113
|
+
});
|
|
114
|
+
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
|
|
115
|
+
// Create index buffer
|
|
116
|
+
const indexBuffer = device.createBuffer({
|
|
117
|
+
size: geometry.indices.byteLength,
|
|
118
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
119
|
+
});
|
|
120
|
+
device.queue.writeBuffer(indexBuffer, 0, geometry.indices);
|
|
121
|
+
// Create instance buffer: each instance is 80 bytes (20 floats: 16 for transform + 4 for color)
|
|
122
|
+
const instanceCount = geometry.instance_count;
|
|
123
|
+
const instanceData = new Float32Array(instanceCount * 20);
|
|
124
|
+
const expressIdToInstanceIndex = new Map();
|
|
125
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
126
|
+
const instance = geometry.get_instance(i);
|
|
127
|
+
if (!instance)
|
|
128
|
+
continue;
|
|
129
|
+
const baseIdx = i * 20;
|
|
130
|
+
// Copy transform (16 floats)
|
|
131
|
+
instanceData.set(instance.transform, baseIdx);
|
|
132
|
+
// Copy color (4 floats)
|
|
133
|
+
instanceData[baseIdx + 16] = instance.color[0];
|
|
134
|
+
instanceData[baseIdx + 17] = instance.color[1];
|
|
135
|
+
instanceData[baseIdx + 18] = instance.color[2];
|
|
136
|
+
instanceData[baseIdx + 19] = instance.color[3];
|
|
137
|
+
expressIdToInstanceIndex.set(instance.expressId, i);
|
|
138
|
+
}
|
|
139
|
+
const instanceBuffer = device.createBuffer({
|
|
140
|
+
size: instanceData.byteLength,
|
|
141
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
142
|
+
});
|
|
143
|
+
device.queue.writeBuffer(instanceBuffer, 0, instanceData);
|
|
144
|
+
// Create and cache bind group to avoid per-frame allocation
|
|
145
|
+
const bindGroup = this.instancedPipeline.createInstanceBindGroup(instanceBuffer);
|
|
146
|
+
const instancedMesh = {
|
|
147
|
+
geometryId: Number(geometry.geometryId),
|
|
148
|
+
vertexBuffer,
|
|
149
|
+
indexBuffer,
|
|
150
|
+
indexCount: geometry.indices.length,
|
|
151
|
+
instanceBuffer,
|
|
152
|
+
instanceCount: instanceCount,
|
|
153
|
+
expressIdToInstanceIndex,
|
|
154
|
+
bindGroup,
|
|
155
|
+
};
|
|
156
|
+
this.scene.addInstancedMesh(instancedMesh);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Convert MeshData array to instanced meshes for optimized rendering
|
|
160
|
+
* Groups identical geometries and creates GPU instanced draw calls
|
|
161
|
+
* Call this in background after initial streaming completes
|
|
162
|
+
*/
|
|
163
|
+
convertToInstanced(meshDataArray) {
|
|
164
|
+
if (!this.instancedPipeline || !this.device.isInitialized()) {
|
|
165
|
+
console.warn('[Renderer] Cannot convert to instanced: renderer not initialized');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Use deduplication function to group identical geometries
|
|
169
|
+
const instancedData = deduplicateMeshes(meshDataArray);
|
|
170
|
+
const device = this.device.getDevice();
|
|
171
|
+
let totalInstances = 0;
|
|
172
|
+
for (const group of instancedData) {
|
|
173
|
+
// Create vertex buffer (interleaved positions + normals)
|
|
174
|
+
const vertexCount = group.positions.length / 3;
|
|
175
|
+
const vertexData = new Float32Array(vertexCount * 6);
|
|
176
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
177
|
+
vertexData[i * 6 + 0] = group.positions[i * 3 + 0];
|
|
178
|
+
vertexData[i * 6 + 1] = group.positions[i * 3 + 1];
|
|
179
|
+
vertexData[i * 6 + 2] = group.positions[i * 3 + 2];
|
|
180
|
+
vertexData[i * 6 + 3] = group.normals[i * 3 + 0];
|
|
181
|
+
vertexData[i * 6 + 4] = group.normals[i * 3 + 1];
|
|
182
|
+
vertexData[i * 6 + 5] = group.normals[i * 3 + 2];
|
|
183
|
+
}
|
|
184
|
+
const vertexBuffer = device.createBuffer({
|
|
185
|
+
size: vertexData.byteLength,
|
|
186
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
187
|
+
});
|
|
188
|
+
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
|
|
189
|
+
// Create index buffer
|
|
190
|
+
const indexBuffer = device.createBuffer({
|
|
191
|
+
size: group.indices.byteLength,
|
|
192
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
193
|
+
});
|
|
194
|
+
device.queue.writeBuffer(indexBuffer, 0, group.indices);
|
|
195
|
+
// Create instance buffer: each instance is 80 bytes (20 floats: 16 for transform + 4 for color)
|
|
196
|
+
const instanceCount = group.instances.length;
|
|
197
|
+
const instanceData = new Float32Array(instanceCount * 20);
|
|
198
|
+
const expressIdToInstanceIndex = new Map();
|
|
199
|
+
// Identity matrix for now (instances use same geometry, different colors)
|
|
200
|
+
const identityTransform = new Float32Array([
|
|
201
|
+
1, 0, 0, 0,
|
|
202
|
+
0, 1, 0, 0,
|
|
203
|
+
0, 0, 1, 0,
|
|
204
|
+
0, 0, 0, 1,
|
|
205
|
+
]);
|
|
206
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
207
|
+
const instance = group.instances[i];
|
|
208
|
+
const baseIdx = i * 20;
|
|
209
|
+
// Copy identity transform (16 floats)
|
|
210
|
+
instanceData.set(identityTransform, baseIdx);
|
|
211
|
+
// Copy color (4 floats)
|
|
212
|
+
instanceData[baseIdx + 16] = instance.color[0];
|
|
213
|
+
instanceData[baseIdx + 17] = instance.color[1];
|
|
214
|
+
instanceData[baseIdx + 18] = instance.color[2];
|
|
215
|
+
instanceData[baseIdx + 19] = instance.color[3];
|
|
216
|
+
expressIdToInstanceIndex.set(instance.expressId, i);
|
|
217
|
+
}
|
|
218
|
+
const instanceBuffer = device.createBuffer({
|
|
219
|
+
size: instanceData.byteLength,
|
|
220
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
221
|
+
});
|
|
222
|
+
device.queue.writeBuffer(instanceBuffer, 0, instanceData);
|
|
223
|
+
// Create and cache bind group to avoid per-frame allocation
|
|
224
|
+
const bindGroup = this.instancedPipeline.createInstanceBindGroup(instanceBuffer);
|
|
225
|
+
// Convert hash string to number for geometryId
|
|
226
|
+
const geometryId = this.hashStringToNumber(group.geometryHash);
|
|
227
|
+
const instancedMesh = {
|
|
228
|
+
geometryId,
|
|
229
|
+
vertexBuffer,
|
|
230
|
+
indexBuffer,
|
|
231
|
+
indexCount: group.indices.length,
|
|
232
|
+
instanceBuffer,
|
|
233
|
+
instanceCount: instanceCount,
|
|
234
|
+
expressIdToInstanceIndex,
|
|
235
|
+
bindGroup,
|
|
236
|
+
};
|
|
237
|
+
this.scene.addInstancedMesh(instancedMesh);
|
|
238
|
+
totalInstances += instanceCount;
|
|
239
|
+
}
|
|
240
|
+
// Clear regular meshes after conversion to avoid double rendering
|
|
241
|
+
const regularMeshCount = this.scene.getMeshes().length;
|
|
242
|
+
this.scene.clearRegularMeshes();
|
|
243
|
+
console.log(`[Renderer] Converted ${meshDataArray.length} meshes to ${instancedData.length} instanced geometries ` +
|
|
244
|
+
`(${totalInstances} total instances, ${(totalInstances / instancedData.length).toFixed(1)}x deduplication). ` +
|
|
245
|
+
`Cleared ${regularMeshCount} regular meshes.`);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Hash string to number for geometryId
|
|
249
|
+
*/
|
|
250
|
+
hashStringToNumber(str) {
|
|
251
|
+
let hash = 0;
|
|
252
|
+
for (let i = 0; i < str.length; i++) {
|
|
253
|
+
const char = str.charCodeAt(i);
|
|
254
|
+
hash = ((hash << 5) - hash) + char;
|
|
255
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
256
|
+
}
|
|
257
|
+
return Math.abs(hash);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Create a GPU Mesh from MeshData (lazy creation for selection highlighting)
|
|
261
|
+
* This is called on-demand when a mesh is selected, avoiding 2x buffer creation during streaming
|
|
262
|
+
*/
|
|
263
|
+
createMeshFromData(meshData) {
|
|
264
|
+
if (!this.device.isInitialized())
|
|
265
|
+
return;
|
|
266
|
+
const device = this.device.getDevice();
|
|
267
|
+
const vertexCount = meshData.positions.length / 3;
|
|
268
|
+
const interleaved = new Float32Array(vertexCount * 6);
|
|
269
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
270
|
+
const base = i * 6;
|
|
271
|
+
const posBase = i * 3;
|
|
272
|
+
interleaved[base] = meshData.positions[posBase];
|
|
273
|
+
interleaved[base + 1] = meshData.positions[posBase + 1];
|
|
274
|
+
interleaved[base + 2] = meshData.positions[posBase + 2];
|
|
275
|
+
interleaved[base + 3] = meshData.normals[posBase];
|
|
276
|
+
interleaved[base + 4] = meshData.normals[posBase + 1];
|
|
277
|
+
interleaved[base + 5] = meshData.normals[posBase + 2];
|
|
278
|
+
}
|
|
279
|
+
const vertexBuffer = device.createBuffer({
|
|
280
|
+
size: interleaved.byteLength,
|
|
281
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
282
|
+
});
|
|
283
|
+
device.queue.writeBuffer(vertexBuffer, 0, interleaved);
|
|
284
|
+
const indexBuffer = device.createBuffer({
|
|
285
|
+
size: meshData.indices.byteLength,
|
|
286
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
287
|
+
});
|
|
288
|
+
device.queue.writeBuffer(indexBuffer, 0, meshData.indices);
|
|
289
|
+
// Add to scene with identity transform (positions already in world space)
|
|
290
|
+
this.scene.addMesh({
|
|
291
|
+
expressId: meshData.expressId,
|
|
292
|
+
vertexBuffer,
|
|
293
|
+
indexBuffer,
|
|
294
|
+
indexCount: meshData.indices.length,
|
|
295
|
+
transform: MathUtils.identity(),
|
|
296
|
+
color: meshData.color,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
84
299
|
/**
|
|
85
300
|
* Ensure all meshes have GPU resources (call after adding meshes if pipeline wasn't ready)
|
|
86
301
|
*/
|
|
@@ -108,7 +323,11 @@ export class Renderer {
|
|
|
108
323
|
}
|
|
109
324
|
}
|
|
110
325
|
if (created > 0) {
|
|
111
|
-
|
|
326
|
+
const totalMeshCount = this.scene.getMeshes().length;
|
|
327
|
+
// Only log every 250 meshes or when creating many at once to reduce noise
|
|
328
|
+
if (totalMeshCount % 250 === 0 || created > 100) {
|
|
329
|
+
console.log(`[Renderer] Created GPU resources for ${created} new meshes (${totalMeshCount} total)`);
|
|
330
|
+
}
|
|
112
331
|
}
|
|
113
332
|
}
|
|
114
333
|
/**
|
|
@@ -134,6 +353,9 @@ export class Renderer {
|
|
|
134
353
|
this.device.configureContext();
|
|
135
354
|
// Also resize the depth texture immediately
|
|
136
355
|
this.pipeline.resize(width, height);
|
|
356
|
+
if (this.instancedPipeline) {
|
|
357
|
+
this.instancedPipeline.resize(width, height);
|
|
358
|
+
}
|
|
137
359
|
}
|
|
138
360
|
// Skip rendering if canvas is invalid
|
|
139
361
|
if (this.canvas.width === 0 || this.canvas.height === 0)
|
|
@@ -147,6 +369,41 @@ export class Renderer {
|
|
|
147
369
|
// Ensure all meshes have GPU resources (in case they were added before pipeline was ready)
|
|
148
370
|
this.ensureMeshResources();
|
|
149
371
|
let meshes = this.scene.getMeshes();
|
|
372
|
+
// Check if visibility filtering is active
|
|
373
|
+
const hasHiddenFilter = options.hiddenIds && options.hiddenIds.size > 0;
|
|
374
|
+
const hasIsolatedFilter = options.isolatedIds !== null && options.isolatedIds !== undefined;
|
|
375
|
+
const hasVisibilityFiltering = hasHiddenFilter || hasIsolatedFilter;
|
|
376
|
+
// When using batched rendering with visibility filtering, we need individual meshes
|
|
377
|
+
// Create them lazily from stored MeshData for visible elements only
|
|
378
|
+
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
379
|
+
if (hasVisibilityFiltering && batchedMeshes.length > 0 && meshes.length === 0) {
|
|
380
|
+
// Collect all expressIds from batched meshes
|
|
381
|
+
const allExpressIds = new Set();
|
|
382
|
+
for (const batch of batchedMeshes) {
|
|
383
|
+
for (const expressId of batch.expressIds) {
|
|
384
|
+
allExpressIds.add(expressId);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Filter to get visible expressIds
|
|
388
|
+
const visibleExpressIds = [];
|
|
389
|
+
for (const expressId of allExpressIds) {
|
|
390
|
+
const isHidden = options.hiddenIds?.has(expressId) ?? false;
|
|
391
|
+
const isIsolated = !hasIsolatedFilter || options.isolatedIds.has(expressId);
|
|
392
|
+
if (!isHidden && isIsolated) {
|
|
393
|
+
visibleExpressIds.push(expressId);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Create individual meshes for visible elements only
|
|
397
|
+
const existingMeshIds = new Set(meshes.map(m => m.expressId));
|
|
398
|
+
for (const expressId of visibleExpressIds) {
|
|
399
|
+
if (!existingMeshIds.has(expressId) && this.scene.hasMeshData(expressId)) {
|
|
400
|
+
const meshData = this.scene.getMeshData(expressId);
|
|
401
|
+
this.createMeshFromData(meshData);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Get updated meshes list
|
|
405
|
+
meshes = this.scene.getMeshes();
|
|
406
|
+
}
|
|
150
407
|
// Frustum culling (if enabled and spatial index available)
|
|
151
408
|
if (options.enableFrustumCulling && options.spatialIndex) {
|
|
152
409
|
try {
|
|
@@ -170,6 +427,9 @@ export class Renderer {
|
|
|
170
427
|
if (this.pipeline.needsResize(this.canvas.width, this.canvas.height)) {
|
|
171
428
|
this.pipeline.resize(this.canvas.width, this.canvas.height);
|
|
172
429
|
}
|
|
430
|
+
if (this.instancedPipeline?.needsResize(this.canvas.width, this.canvas.height)) {
|
|
431
|
+
this.instancedPipeline.resize(this.canvas.width, this.canvas.height);
|
|
432
|
+
}
|
|
173
433
|
// Get current texture safely - may return null if context needs reconfiguration
|
|
174
434
|
const currentTexture = this.device.getCurrentTexture();
|
|
175
435
|
if (!currentTexture) {
|
|
@@ -308,29 +568,172 @@ export class Renderer {
|
|
|
308
568
|
},
|
|
309
569
|
});
|
|
310
570
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
571
|
+
// Check if we have batched meshes (preferred for performance)
|
|
572
|
+
// When visibility filtering is active, we need to render individual meshes instead of batches
|
|
573
|
+
// because batches merge geometry by color and can't be partially rendered
|
|
574
|
+
const allBatchedMeshes = this.scene.getBatchedMeshes();
|
|
575
|
+
if (allBatchedMeshes.length > 0 && !hasVisibilityFiltering) {
|
|
576
|
+
// Separate batches into selected and non-selected
|
|
577
|
+
const nonSelectedBatches = [];
|
|
578
|
+
const selectedExpressIds = new Set();
|
|
579
|
+
if (selectedId !== undefined && selectedId !== null) {
|
|
580
|
+
selectedExpressIds.add(selectedId);
|
|
315
581
|
}
|
|
316
|
-
|
|
317
|
-
|
|
582
|
+
if (selectedIds) {
|
|
583
|
+
for (const id of selectedIds) {
|
|
584
|
+
selectedExpressIds.add(id);
|
|
585
|
+
}
|
|
318
586
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
587
|
+
// Render ALL batches normally (non-selected meshes will render normally)
|
|
588
|
+
// Selected meshes will be rendered individually on top with highlight
|
|
589
|
+
for (const batch of allBatchedMeshes) {
|
|
590
|
+
if (!batch.bindGroup || !batch.uniformBuffer)
|
|
591
|
+
continue;
|
|
592
|
+
// Update uniform buffer for this batch
|
|
593
|
+
const buffer = new Float32Array(48);
|
|
594
|
+
const flagBuffer = new Uint32Array(buffer.buffer, 176, 4);
|
|
595
|
+
buffer.set(viewProj, 0);
|
|
596
|
+
// Identity transform for batched meshes (positions already in world space)
|
|
597
|
+
buffer.set([
|
|
598
|
+
1, 0, 0, 0,
|
|
599
|
+
0, 1, 0, 0,
|
|
600
|
+
0, 0, 1, 0,
|
|
601
|
+
0, 0, 0, 1
|
|
602
|
+
], 16);
|
|
603
|
+
buffer.set(batch.color, 32);
|
|
604
|
+
buffer[36] = 0.0; // metallic
|
|
605
|
+
buffer[37] = 0.6; // roughness
|
|
606
|
+
// Section plane data
|
|
607
|
+
if (sectionPlaneData) {
|
|
608
|
+
buffer[40] = sectionPlaneData.normal[0];
|
|
609
|
+
buffer[41] = sectionPlaneData.normal[1];
|
|
610
|
+
buffer[42] = sectionPlaneData.normal[2];
|
|
611
|
+
buffer[43] = sectionPlaneData.distance;
|
|
612
|
+
}
|
|
613
|
+
// Flags (not selected - batches render normally, selected meshes rendered separately)
|
|
614
|
+
flagBuffer[0] = 0;
|
|
615
|
+
flagBuffer[1] = sectionPlaneData?.enabled ? 1 : 0;
|
|
616
|
+
flagBuffer[2] = 0;
|
|
617
|
+
flagBuffer[3] = 0;
|
|
618
|
+
device.queue.writeBuffer(batch.uniformBuffer, 0, buffer);
|
|
619
|
+
// Single draw call for entire batch!
|
|
620
|
+
pass.setBindGroup(0, batch.bindGroup);
|
|
621
|
+
pass.setVertexBuffer(0, batch.vertexBuffer);
|
|
622
|
+
pass.setIndexBuffer(batch.indexBuffer, 'uint32');
|
|
623
|
+
pass.drawIndexed(batch.indexCount);
|
|
624
|
+
}
|
|
625
|
+
// Render selected meshes individually for proper highlighting
|
|
626
|
+
// First, check if we have Mesh objects for selected IDs
|
|
627
|
+
// If not, create them lazily from stored MeshData
|
|
628
|
+
const allMeshes = this.scene.getMeshes();
|
|
629
|
+
const existingMeshIds = new Set(allMeshes.map(m => m.expressId));
|
|
630
|
+
// Create GPU resources lazily for selected meshes that don't have them yet
|
|
631
|
+
for (const selectedId of selectedExpressIds) {
|
|
632
|
+
if (!existingMeshIds.has(selectedId) && this.scene.hasMeshData(selectedId)) {
|
|
633
|
+
const meshData = this.scene.getMeshData(selectedId);
|
|
634
|
+
this.createMeshFromData(meshData);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Now get selected meshes (includes newly created ones)
|
|
638
|
+
const selectedMeshes = this.scene.getMeshes().filter(mesh => selectedExpressIds.has(mesh.expressId));
|
|
639
|
+
// Ensure selected meshes have uniform buffers and bind groups
|
|
640
|
+
for (const mesh of selectedMeshes) {
|
|
641
|
+
if (!mesh.uniformBuffer && this.pipeline) {
|
|
642
|
+
mesh.uniformBuffer = device.createBuffer({
|
|
643
|
+
size: this.pipeline.getUniformBufferSize(),
|
|
644
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
645
|
+
});
|
|
646
|
+
mesh.bindGroup = device.createBindGroup({
|
|
647
|
+
layout: this.pipeline.getBindGroupLayout(),
|
|
648
|
+
entries: [
|
|
649
|
+
{
|
|
650
|
+
binding: 0,
|
|
651
|
+
resource: { buffer: mesh.uniformBuffer },
|
|
652
|
+
},
|
|
653
|
+
],
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Render selected meshes with highlight
|
|
658
|
+
for (const mesh of selectedMeshes) {
|
|
659
|
+
if (!mesh.bindGroup || !mesh.uniformBuffer) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const buffer = new Float32Array(48);
|
|
663
|
+
const flagBuffer = new Uint32Array(buffer.buffer, 176, 4);
|
|
664
|
+
buffer.set(viewProj, 0);
|
|
665
|
+
buffer.set(mesh.transform.m, 16);
|
|
666
|
+
buffer.set(mesh.color, 32);
|
|
667
|
+
buffer[36] = mesh.material?.metallic ?? 0.0;
|
|
668
|
+
buffer[37] = mesh.material?.roughness ?? 0.6;
|
|
669
|
+
// Section plane data
|
|
670
|
+
if (sectionPlaneData) {
|
|
671
|
+
buffer[40] = sectionPlaneData.normal[0];
|
|
672
|
+
buffer[41] = sectionPlaneData.normal[1];
|
|
673
|
+
buffer[42] = sectionPlaneData.normal[2];
|
|
674
|
+
buffer[43] = sectionPlaneData.distance;
|
|
675
|
+
}
|
|
676
|
+
// Flags (selected)
|
|
677
|
+
flagBuffer[0] = 1; // isSelected
|
|
678
|
+
flagBuffer[1] = sectionPlaneData?.enabled ? 1 : 0;
|
|
679
|
+
flagBuffer[2] = 0;
|
|
680
|
+
flagBuffer[3] = 0;
|
|
681
|
+
device.queue.writeBuffer(mesh.uniformBuffer, 0, buffer);
|
|
682
|
+
// Use selection pipeline to render on top of batched meshes
|
|
683
|
+
pass.setPipeline(this.pipeline.getSelectionPipeline());
|
|
326
684
|
pass.setBindGroup(0, mesh.bindGroup);
|
|
685
|
+
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
686
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
687
|
+
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
327
688
|
}
|
|
328
|
-
|
|
329
|
-
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
// Fallback: render individual meshes (slower but works)
|
|
692
|
+
// Render opaque meshes with per-mesh bind groups
|
|
693
|
+
for (const mesh of opaqueMeshes) {
|
|
694
|
+
if (mesh.bindGroup) {
|
|
695
|
+
pass.setBindGroup(0, mesh.bindGroup);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
pass.setBindGroup(0, this.pipeline.getBindGroup());
|
|
699
|
+
}
|
|
700
|
+
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
701
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
702
|
+
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
703
|
+
}
|
|
704
|
+
// Render transparent meshes with per-mesh bind groups
|
|
705
|
+
for (const mesh of transparentMeshes) {
|
|
706
|
+
if (mesh.bindGroup) {
|
|
707
|
+
pass.setBindGroup(0, mesh.bindGroup);
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
pass.setBindGroup(0, this.pipeline.getBindGroup());
|
|
711
|
+
}
|
|
712
|
+
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
713
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
714
|
+
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Render instanced meshes (much more efficient for repeated geometry)
|
|
718
|
+
if (this.instancedPipeline) {
|
|
719
|
+
const instancedMeshes = this.scene.getInstancedMeshes();
|
|
720
|
+
if (instancedMeshes.length > 0) {
|
|
721
|
+
// Update instanced pipeline uniforms
|
|
722
|
+
this.instancedPipeline.updateUniforms(viewProj, sectionPlaneData);
|
|
723
|
+
// Switch to instanced pipeline
|
|
724
|
+
pass.setPipeline(this.instancedPipeline.getPipeline());
|
|
725
|
+
for (const instancedMesh of instancedMeshes) {
|
|
726
|
+
// Use cached bind group (created at mesh upload time)
|
|
727
|
+
// Falls back to creating one if missing (shouldn't happen in normal flow)
|
|
728
|
+
const bindGroup = instancedMesh.bindGroup ??
|
|
729
|
+
this.instancedPipeline.createInstanceBindGroup(instancedMesh.instanceBuffer);
|
|
730
|
+
pass.setBindGroup(0, bindGroup);
|
|
731
|
+
pass.setVertexBuffer(0, instancedMesh.vertexBuffer);
|
|
732
|
+
pass.setIndexBuffer(instancedMesh.indexBuffer, 'uint32');
|
|
733
|
+
// Draw with instancing: indexCount, instanceCount
|
|
734
|
+
pass.drawIndexed(instancedMesh.indexCount, instancedMesh.instanceCount, 0, 0, 0);
|
|
735
|
+
}
|
|
330
736
|
}
|
|
331
|
-
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
332
|
-
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
333
|
-
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
334
737
|
}
|
|
335
738
|
pass.end();
|
|
336
739
|
// Render section plane visual if enabled
|
|
@@ -357,12 +760,47 @@ export class Renderer {
|
|
|
357
760
|
/**
|
|
358
761
|
* Pick object at screen coordinates
|
|
359
762
|
*/
|
|
360
|
-
async pick(x, y) {
|
|
361
|
-
if (!this.picker)
|
|
763
|
+
async pick(x, y, options) {
|
|
764
|
+
if (!this.picker) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
// Skip picker during streaming for consistent performance
|
|
768
|
+
// Picking during streaming would be slow and incomplete anyway
|
|
769
|
+
if (options?.isStreaming) {
|
|
362
770
|
return null;
|
|
363
|
-
|
|
771
|
+
}
|
|
772
|
+
let meshes = this.scene.getMeshes();
|
|
773
|
+
// If we have batched meshes but no regular meshes, create picking meshes from stored MeshData
|
|
774
|
+
// This implements lazy loading for picking - meshes are created on-demand from MeshData
|
|
775
|
+
if (meshes.length === 0) {
|
|
776
|
+
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
777
|
+
if (batchedMeshes.length > 0) {
|
|
778
|
+
// Collect all expressIds from batched meshes
|
|
779
|
+
const expressIds = new Set();
|
|
780
|
+
for (const batch of batchedMeshes) {
|
|
781
|
+
for (const expressId of batch.expressIds) {
|
|
782
|
+
expressIds.add(expressId);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// Track existing expressIds to avoid duplicates (using Set for O(1) lookup)
|
|
786
|
+
const existingExpressIds = new Set(meshes.map(m => m.expressId));
|
|
787
|
+
// Create picking meshes lazily from stored MeshData
|
|
788
|
+
for (const expressId of expressIds) {
|
|
789
|
+
if (!existingExpressIds.has(expressId) && this.scene.hasMeshData(expressId)) {
|
|
790
|
+
const meshData = this.scene.getMeshData(expressId);
|
|
791
|
+
if (meshData) {
|
|
792
|
+
this.createMeshFromData(meshData);
|
|
793
|
+
existingExpressIds.add(expressId); // Track newly created mesh
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// Get updated meshes list (includes newly created ones)
|
|
798
|
+
meshes = this.scene.getMeshes();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
364
801
|
const viewProj = this.camera.getViewProjMatrix().m;
|
|
365
|
-
|
|
802
|
+
const result = await this.picker.pick(x, y, this.canvas.width, this.canvas.height, meshes, viewProj);
|
|
803
|
+
return result;
|
|
366
804
|
}
|
|
367
805
|
/**
|
|
368
806
|
* Resize canvas
|
|
@@ -378,6 +816,12 @@ export class Renderer {
|
|
|
378
816
|
getScene() {
|
|
379
817
|
return this.scene;
|
|
380
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* Get render pipeline (for batching)
|
|
821
|
+
*/
|
|
822
|
+
getPipeline() {
|
|
823
|
+
return this.pipeline;
|
|
824
|
+
}
|
|
381
825
|
/**
|
|
382
826
|
* Check if renderer is fully initialized and ready to use
|
|
383
827
|
*/
|