@ifc-lite/renderer 1.0.0 → 1.1.0

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/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
- console.log(`[Renderer] Created GPU resources for ${created} meshes`);
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
- // Render opaque meshes with per-mesh bind groups
312
- for (const mesh of opaqueMeshes) {
313
- if (mesh.bindGroup) {
314
- pass.setBindGroup(0, mesh.bindGroup);
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
- else {
317
- pass.setBindGroup(0, this.pipeline.getBindGroup());
582
+ if (selectedIds) {
583
+ for (const id of selectedIds) {
584
+ selectedExpressIds.add(id);
585
+ }
318
586
  }
319
- pass.setVertexBuffer(0, mesh.vertexBuffer);
320
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
321
- pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
322
- }
323
- // Render transparent meshes with per-mesh bind groups
324
- for (const mesh of transparentMeshes) {
325
- if (mesh.bindGroup) {
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
- else {
329
- pass.setBindGroup(0, this.pipeline.getBindGroup());
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
- const meshes = this.scene.getMeshes();
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
- return this.picker.pick(x, y, this.canvas.width, this.canvas.height, meshes, viewProj);
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
  */