ncca-ngl 0.3.5__py3-none-any.whl → 0.5.1__py3-none-any.whl

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.
Files changed (56) hide show
  1. ncca/ngl/PrimData/pack_arrays.py +2 -3
  2. ncca/ngl/__init__.py +5 -5
  3. ncca/ngl/base_mesh.py +28 -20
  4. ncca/ngl/image.py +1 -3
  5. ncca/ngl/mat2.py +79 -53
  6. ncca/ngl/mat3.py +104 -185
  7. ncca/ngl/mat4.py +144 -309
  8. ncca/ngl/prim_data.py +42 -36
  9. ncca/ngl/primitives.py +2 -2
  10. ncca/ngl/pyside_event_handling_mixin.py +0 -108
  11. ncca/ngl/quaternion.py +69 -36
  12. ncca/ngl/shader.py +0 -116
  13. ncca/ngl/shader_program.py +94 -117
  14. ncca/ngl/texture.py +5 -2
  15. ncca/ngl/util.py +50 -0
  16. ncca/ngl/vec2.py +59 -302
  17. ncca/ngl/vec2_array.py +79 -28
  18. ncca/ngl/vec3.py +60 -350
  19. ncca/ngl/vec3_array.py +76 -23
  20. ncca/ngl/vec4.py +90 -200
  21. ncca/ngl/vec4_array.py +78 -27
  22. ncca/ngl/vector_base.py +548 -0
  23. ncca/ngl/webgpu/__init__.py +20 -0
  24. ncca/ngl/webgpu/__main__.py +640 -0
  25. ncca/ngl/webgpu/__main__.py.backup +640 -0
  26. ncca/ngl/webgpu/base_webgpu_pipeline.py +354 -0
  27. ncca/ngl/webgpu/custom_shader_pipeline.py +288 -0
  28. ncca/ngl/webgpu/instanced_geometry_pipeline.py +594 -0
  29. ncca/ngl/webgpu/line_pipeline.py +405 -0
  30. ncca/ngl/webgpu/pipeline_factory.py +190 -0
  31. ncca/ngl/webgpu/pipeline_shaders.py +497 -0
  32. ncca/ngl/webgpu/point_list_pipeline.py +349 -0
  33. ncca/ngl/webgpu/point_pipeline.py +336 -0
  34. ncca/ngl/webgpu/triangle_pipeline.py +419 -0
  35. ncca/ngl/webgpu/webgpu_constants.py +33 -0
  36. ncca/ngl/webgpu/webgpu_widget.py +322 -0
  37. ncca/ngl/webgpu/wip/REFACTORING_SUMMARY.md +82 -0
  38. ncca/ngl/webgpu/wip/UNIFIED_SYSTEM.md +314 -0
  39. ncca/ngl/webgpu/wip/buffer_manager.py +396 -0
  40. ncca/ngl/webgpu/wip/pipeline_config.py +463 -0
  41. ncca/ngl/webgpu/wip/shader_constants.py +328 -0
  42. ncca/ngl/webgpu/wip/shader_templates.py +563 -0
  43. ncca/ngl/webgpu/wip/unified_examples.py +390 -0
  44. ncca/ngl/webgpu/wip/unified_factory.py +449 -0
  45. ncca/ngl/webgpu/wip/unified_pipeline.py +469 -0
  46. ncca/ngl/widgets/__init__.py +18 -2
  47. ncca/ngl/widgets/__main__.py +2 -1
  48. ncca/ngl/widgets/lookatwidget.py +2 -1
  49. ncca/ngl/widgets/mat4widget.py +2 -2
  50. ncca/ngl/widgets/vec2widget.py +1 -1
  51. ncca/ngl/widgets/vec3widget.py +1 -0
  52. {ncca_ngl-0.3.5.dist-info → ncca_ngl-0.5.1.dist-info}/METADATA +3 -2
  53. ncca_ngl-0.5.1.dist-info/RECORD +105 -0
  54. ncca/ngl/widgets/transformation_widget.py +0 -299
  55. ncca_ngl-0.3.5.dist-info/RECORD +0 -82
  56. {ncca_ngl-0.3.5.dist-info → ncca_ngl-0.5.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,594 @@
1
+ """
2
+ Instanced geometry rendering pipeline for WebGPU.
3
+ Renders multiple instances of the same geometry at different positions with optional per-instance colors.
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ import numpy as np
9
+ import wgpu
10
+
11
+ from .base_webgpu_pipeline import BaseWebGPUPipeline
12
+ from .pipeline_shaders import INSTANCED_SHADER_MULTI_COLOURED, INSTANCED_SHADER_SINGLE_COLOUR
13
+ from .webgpu_constants import NGLToWebGPU
14
+
15
+ GEOM_ERROR = "geometry_data is required for instanced geometry pipelines"
16
+
17
+
18
+ class BaseInstancedGeometryPipeline(BaseWebGPUPipeline):
19
+ """
20
+ Base class for instanced geometry rendering pipelines.
21
+
22
+ Provides common functionality for:
23
+ - Instanced rendering of arbitrary geometry
24
+ - Per-instance positioning
25
+ - Optional per-instance colors
26
+ - Geometry buffer management
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ device: wgpu.GPUDevice,
32
+ data_type: str = "Vec3",
33
+ texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
34
+ depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
35
+ msaa_sample_count: int = 4,
36
+ stride: int = 0,
37
+ ):
38
+ """
39
+ Initialize the instanced geometry pipeline.
40
+
41
+ Args:
42
+ device: WebGPU device
43
+ data_type: Instance position data type (e.g., "Vec3", "Vec2")
44
+ texture_format: Color attachment format
45
+ depth_format: Depth attachment format
46
+ msaa_sample_count: Number of MSAA samples
47
+ stride: The stride of the instance buffer. If 0, inferred from data_type.
48
+ """
49
+ # Pipeline-specific buffer tracking
50
+ self.position_buffer: Optional[wgpu.GPUBuffer] = None
51
+ self.colour_buffer: Optional[wgpu.GPUBuffer] = None
52
+ self.instance_id_buffer: Optional[wgpu.GPUBuffer] = None
53
+ self.geometry_buffer: Optional[wgpu.GPUBuffer] = None # Single interleaved buffer x,y,z,nx,ny,nz,u,v
54
+ self.num_instances: int = 0
55
+ self.num_vertices: int = 0
56
+
57
+ super().__init__(
58
+ device=device,
59
+ texture_format=texture_format,
60
+ depth_format=depth_format,
61
+ msaa_sample_count=msaa_sample_count,
62
+ data_type=data_type,
63
+ stride=stride,
64
+ )
65
+
66
+ def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
67
+ """Default to triangle list for geometry rendering."""
68
+ return wgpu.PrimitiveTopology.triangle_list
69
+
70
+ def _get_default_vertex_layouts(self) -> list:
71
+ """
72
+ Get default vertex buffer layouts for instanced geometry rendering.
73
+
74
+
75
+
76
+ Returns:
77
+ List of vertex buffer layout configurations
78
+ """
79
+ layouts = [
80
+ # Instance position buffer (step_mode="instance")
81
+ {
82
+ "array_stride": self._stride,
83
+ "step_mode": "instance",
84
+ "attributes": [
85
+ {
86
+ "format": NGLToWebGPU.vertex_format(self._data_type),
87
+ "offset": 0,
88
+ "shader_location": 0,
89
+ },
90
+ ],
91
+ },
92
+ ]
93
+
94
+ # Always add colour buffer layout (used or not by shader)
95
+ layouts.append({
96
+ "array_stride": NGLToWebGPU.stride_from_type("Vec3"),
97
+ "step_mode": "instance",
98
+ "attributes": [
99
+ {
100
+ "format": NGLToWebGPU.vertex_format("Vec3"),
101
+ "offset": 0,
102
+ "shader_location": 1,
103
+ },
104
+ ],
105
+ })
106
+
107
+ # Add instance ID buffer for potential use in shaders
108
+ layouts.append({
109
+ "array_stride": 4, # float32
110
+ "step_mode": "instance",
111
+ "attributes": [
112
+ {
113
+ "format": wgpu.VertexFormat.float32,
114
+ "offset": 0,
115
+ "shader_location": 2,
116
+ },
117
+ ],
118
+ })
119
+
120
+ # Single interleaved geometry buffer (step_mode="vertex")
121
+ layouts.append({
122
+ "array_stride": 8 * 4, # 8 floats * 4 bytes each
123
+ "step_mode": "vertex",
124
+ "attributes": [
125
+ {
126
+ "format": NGLToWebGPU.vertex_format("Vec3"),
127
+ "offset": 0,
128
+ "shader_location": 3, # geometry_position
129
+ },
130
+ {
131
+ "format": NGLToWebGPU.vertex_format("Vec3"),
132
+ "offset": 3 * 4, # 12 bytes offset
133
+ "shader_location": 4, # geometry_normal
134
+ },
135
+ {
136
+ "format": NGLToWebGPU.vertex_format("Vec2"),
137
+ "offset": 6 * 4, # 24 bytes offset
138
+ "shader_location": 5, # geometry_uv
139
+ },
140
+ ],
141
+ })
142
+
143
+ return layouts
144
+
145
+ def _create_instance_id_buffer(self, num_instances: int) -> wgpu.GPUBuffer:
146
+ """Create a buffer containing instance IDs 0, 1, 2, ..."""
147
+ instance_ids = np.arange(num_instances, dtype=np.float32)
148
+ return self.device.create_buffer_with_data(
149
+ data=instance_ids.tobytes(),
150
+ usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
151
+ label="instance_id_buffer",
152
+ )
153
+
154
+
155
+ class InstancedGeometryPipelineMultiColour(BaseInstancedGeometryPipeline):
156
+ """
157
+ A reusable pipeline for rendering instanced geometry in WebGPU with per-instance colors.
158
+
159
+ Features:
160
+ - Instanced rendering of arbitrary geometry using interleaved x,y,z,nx,ny,nz,u,v format
161
+ - Per-instance colors
162
+ - Per-instance positioning
163
+ - Configurable instance transformation matrix
164
+ - Model, View Projection matrix support
165
+ - MSAA support
166
+ """
167
+
168
+ def get_dtype(self) -> np.dtype:
169
+ """Get the data type of the pipeline."""
170
+ return np.dtype([
171
+ ("MVP", "float32", (4, 4)),
172
+ ("ViewMatrix", "float32", (4, 4)),
173
+ ("instance_transform", "float32", (4, 4)),
174
+ ])
175
+
176
+ def _get_shader_code(self) -> str:
177
+ """Get the WGSL shader code for this pipeline."""
178
+ return INSTANCED_SHADER_MULTI_COLOURED
179
+
180
+ def _get_vertex_buffer_layouts(self) -> list:
181
+ """Get vertex buffer layout configurations for the pipeline."""
182
+ return self._get_default_vertex_layouts()
183
+
184
+ def _set_default_uniforms(self) -> None:
185
+ """Set default values for uniform data."""
186
+ self.uniform_data["instance_transform"] = np.eye(4, dtype=np.float32)
187
+ self.uniform_data["ViewMatrix"] = np.eye(4, dtype=np.float32)
188
+
189
+ def _get_pipeline_label(self) -> str:
190
+ """Get the label for the pipeline."""
191
+ return "instanced_geometry_pipeline_multi_colour"
192
+
193
+ def set_data(self, **kwargs) -> None:
194
+ """
195
+ Set the instanced geometry data for rendering.
196
+
197
+ Args:
198
+ **kwargs: Pipeline-specific data parameters
199
+ - positions: Nx3 array of instance positions or a pre-existing GPUBuffer.
200
+ - colours: Nx3 array of instance colors (RGB) or a pre-existing GPUBuffer.
201
+ If None, uses white.
202
+ - geometry_data: Mx8 array of interleaved geometry data in format
203
+ x,y,z,nx,ny,nz,u,v or pre-existing GPUBuffer.
204
+ Must match the format output by PrimData methods.
205
+ """
206
+ positions = kwargs.get("positions")
207
+ colours = kwargs.get("colours")
208
+ geometry_data = kwargs.get("geometry_data")
209
+
210
+ self._set_position_data(positions)
211
+ self._setup_instance_id_buffer()
212
+ self._set_colour_data(colours)
213
+ self._set_geometry_data(geometry_data)
214
+
215
+ def _set_position_data(self, positions) -> None:
216
+ """Set instance position data from GPUBuffer or numpy array."""
217
+ if isinstance(positions, wgpu.GPUBuffer):
218
+ self.position_buffer = positions
219
+ self.num_instances = positions.size // self._stride
220
+ else:
221
+ self.num_instances = len(positions)
222
+ if self.position_buffer:
223
+ self.position_buffer.destroy()
224
+ self.position_buffer = self.device.create_buffer_with_data(
225
+ data=positions.astype(np.float32).tobytes(),
226
+ usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
227
+ label="instanced_geometry_multi_colour_position_buffer",
228
+ )
229
+
230
+ def _setup_instance_id_buffer(self) -> None:
231
+ """Create buffer containing instance IDs."""
232
+ if self.instance_id_buffer:
233
+ self.instance_id_buffer.destroy()
234
+ self.instance_id_buffer = super()._create_instance_id_buffer(self.num_instances)
235
+
236
+ def _set_colour_data(self, colours) -> None:
237
+ """Set colour data from GPUBuffer, numpy array, or create default."""
238
+ if self.colour_buffer:
239
+ self.colour_buffer.destroy()
240
+ self.colour_buffer = None
241
+
242
+ if colours is None:
243
+ self._create_default_colours()
244
+ else:
245
+ self._create_colour_buffer(colours)
246
+
247
+ def _create_default_colours(self) -> None:
248
+ """Create default white colours for all instances."""
249
+ default_colours = np.ones((self.num_instances, 3), dtype=np.float32)
250
+ self.colour_buffer = self.device.create_buffer_with_data(
251
+ data=default_colours.tobytes(),
252
+ usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
253
+ )
254
+
255
+ def _create_colour_buffer(self, colours) -> None:
256
+ """Create colour buffer from GPUBuffer or numpy array."""
257
+ if isinstance(colours, wgpu.GPUBuffer):
258
+ self.colour_buffer = colours
259
+ else:
260
+ colour_array = colours.astype(np.float32)
261
+ self.colour_buffer = self.device.create_buffer_with_data(
262
+ data=colour_array.tobytes(),
263
+ usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
264
+ )
265
+
266
+ def _set_geometry_data(self, geometry_data) -> None:
267
+ """Set geometry data from GPUBuffer or numpy array."""
268
+ if geometry_data is None:
269
+ raise ValueError(GEOM_ERROR)
270
+
271
+ if isinstance(geometry_data, wgpu.GPUBuffer):
272
+ self.geometry_buffer = geometry_data
273
+ self.num_vertices = geometry_data.size // (8 * 4)
274
+ else:
275
+ self._process_geometry_array(geometry_data)
276
+
277
+ def _process_geometry_array(self, geometry_data) -> None:
278
+ """Process geometry numpy array and create buffer."""
279
+ geometry_data = np.asarray(geometry_data, dtype=np.float32)
280
+ geometry_data = self._validate_and_reshape_geometry(geometry_data)
281
+ self.num_vertices = geometry_data.shape[0]
282
+
283
+ if self.geometry_buffer:
284
+ self.geometry_buffer.destroy()
285
+ self.geometry_buffer = self.device.create_buffer_with_data(
286
+ data=geometry_data.tobytes(),
287
+ usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
288
+ label="instanced_geometry_buffer",
289
+ )
290
+
291
+ def _validate_and_reshape_geometry(self, geometry_data) -> np.ndarray:
292
+ """Validate geometry data dimensions and reshape if needed."""
293
+ if geometry_data.ndim == 1:
294
+ geometry_data = geometry_data.reshape(-1, 8)
295
+ elif geometry_data.ndim != 2:
296
+ raise ValueError(f"geometry_data must be 1D or 2D array, got {geometry_data.ndim}D")
297
+
298
+ if geometry_data.shape[1] != 8:
299
+ raise ValueError(f"geometry_data must have 8 components (x,y,z,nx,ny,nz,u,v), got {geometry_data.shape[1]}")
300
+
301
+ return geometry_data
302
+
303
+ def update_uniforms(self, **kwargs) -> None:
304
+ """
305
+ Update uniform buffer values.
306
+
307
+ Args:
308
+ **kwargs: Pipeline-specific uniform parameters
309
+ - mvp: 4x4 model view projection matrix
310
+ - view_matrix: 4x4 view matrix
311
+ - instance_transform: 4x4 transformation matrix for each instance
312
+ """
313
+ if "mvp" in kwargs and kwargs["mvp"] is not None:
314
+ self.uniform_data["MVP"] = kwargs["mvp"]
315
+
316
+ if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
317
+ self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]
318
+
319
+ if "instance_transform" in kwargs and kwargs["instance_transform"] is not None:
320
+ self.uniform_data["instance_transform"] = kwargs["instance_transform"]
321
+
322
+ self.device.queue.write_buffer(self.uniform_buffer, 0, self.uniform_data.tobytes())
323
+
324
+ def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs) -> None:
325
+ """
326
+ Render the instanced geometry.
327
+
328
+ Args:
329
+ render_pass: Active render pass encoder
330
+ **kwargs: Pipeline-specific render parameters
331
+ - num_instances: Number of instances to render (defaults to all)
332
+ """
333
+ num_instances = kwargs.get("num_instances", None)
334
+
335
+ if (
336
+ self.position_buffer is None
337
+ or self.colour_buffer is None
338
+ or self.instance_id_buffer is None
339
+ or self.geometry_buffer is None
340
+ ):
341
+ return
342
+
343
+ count = num_instances if num_instances is not None else self.num_instances
344
+
345
+ render_pass.set_pipeline(self.pipeline)
346
+ if self.bind_group:
347
+ render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
348
+
349
+ # Set instance buffers (match shader layout)
350
+ render_pass.set_vertex_buffer(0, self.position_buffer) # location(0) position
351
+ render_pass.set_vertex_buffer(1, self.colour_buffer) # location(1) colour
352
+ render_pass.set_vertex_buffer(2, self.instance_id_buffer) # location(2) instance_id
353
+
354
+ # Set single interleaved geometry buffer
355
+ render_pass.set_vertex_buffer(3, self.geometry_buffer) # locations(3,4,5) interleaved
356
+
357
+ render_pass.draw(self.num_vertices, count)
358
+
359
+ def cleanup(self) -> None:
360
+ """Release resources."""
361
+ if self.position_buffer:
362
+ self.position_buffer.destroy()
363
+ if self.colour_buffer:
364
+ self.colour_buffer.destroy()
365
+ if self.instance_id_buffer:
366
+ self.instance_id_buffer.destroy()
367
+ if self.geometry_buffer:
368
+ self.geometry_buffer.destroy()
369
+
370
+ super().cleanup()
371
+
372
+
373
+ class InstancedGeometryPipelineSingleColour(BaseInstancedGeometryPipeline):
374
+ """
375
+ A reusable pipeline for rendering instanced geometry in WebGPU with single color.
376
+
377
+ Features:
378
+ - Instanced rendering of arbitrary geometry using interleaved x,y,z,nx,ny,nz,u,v format
379
+ - Single color for all instances
380
+ - Per-instance positioning
381
+ - Configurable instance transformation matrix
382
+ - Model, View Projection matrix support
383
+ - MSAA support
384
+ """
385
+
386
+ def get_dtype(self) -> np.dtype:
387
+ """Get the data type of the pipeline."""
388
+ return np.dtype([
389
+ ("MVP", "float32", (4, 4)),
390
+ ("ViewMatrix", "float32", (4, 4)),
391
+ ("colour", "float32", 4), # Vec4 for alignment (RGB + padding)
392
+ ("instance_transform", "float32", (4, 4)),
393
+ ])
394
+
395
+ def _get_shader_code(self) -> str:
396
+ """Get the WGSL shader code for this pipeline."""
397
+ return INSTANCED_SHADER_SINGLE_COLOUR
398
+
399
+ def _get_vertex_buffer_layouts(self) -> list:
400
+ """Get vertex buffer layout configurations for the pipeline."""
401
+ return self._get_default_vertex_layouts()
402
+
403
+ def _set_default_uniforms(self) -> None:
404
+ """Set default values for uniform data."""
405
+ self.uniform_data["colour"] = np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32) # White
406
+ self.uniform_data["instance_transform"] = np.eye(4, dtype=np.float32)
407
+ self.uniform_data["ViewMatrix"] = np.eye(4, dtype=np.float32)
408
+
409
+ def _get_pipeline_label(self) -> str:
410
+ """Get the label for the pipeline."""
411
+ return "instanced_geometry_pipeline_single_colour"
412
+
413
+ def set_data(self, **kwargs) -> None:
414
+ """
415
+ Set the instanced geometry data for rendering.
416
+
417
+ Args:
418
+ **kwargs: Pipeline-specific data parameters
419
+ - positions: Nx3 array of instance positions or a pre-existing GPUBuffer.
420
+ - colours: Nx3 array of instance colors (RGB) or a pre-existing GPUBuffer.
421
+ If None, uses white.
422
+ - geometry_data: Mx8 array of interleaved geometry data in format
423
+ x,y,z,nx,ny,nz,u,v or pre-existing GPUBuffer.
424
+ Must match the format output by PrimData methods.
425
+ """
426
+ positions = kwargs.get("positions")
427
+ colours = kwargs.get("colours")
428
+ geometry_data = kwargs.get("geometry_data")
429
+
430
+ self._set_position_data(positions)
431
+ self._setup_instance_id_buffer()
432
+ self._set_colour_data(colours)
433
+ self._set_geometry_data(geometry_data)
434
+
435
+ def _set_position_data(self, positions) -> None:
436
+ """Set instance position data from GPUBuffer or numpy array."""
437
+ if isinstance(positions, wgpu.GPUBuffer):
438
+ self.position_buffer = positions
439
+ self.num_instances = positions.size // self._stride
440
+ else:
441
+ self.num_instances = len(positions)
442
+ if self.position_buffer:
443
+ self.position_buffer.destroy()
444
+ self.position_buffer = self.device.create_buffer_with_data(
445
+ data=positions.astype(np.float32).tobytes(),
446
+ usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
447
+ label="instanced_geometry_multi_colour_position_buffer",
448
+ )
449
+
450
+ def _setup_instance_id_buffer(self) -> None:
451
+ """Create buffer containing instance IDs."""
452
+ if self.instance_id_buffer:
453
+ self.instance_id_buffer.destroy()
454
+ self.instance_id_buffer = super()._create_instance_id_buffer(self.num_instances)
455
+
456
+ def _set_colour_data(self, colours) -> None:
457
+ """Set colour data from GPUBuffer, numpy array, or create default."""
458
+ if self.colour_buffer:
459
+ self.colour_buffer.destroy()
460
+ self.colour_buffer = None
461
+
462
+ if colours is None:
463
+ self._create_default_colours()
464
+ else:
465
+ self._create_colour_buffer(colours)
466
+
467
+ def _create_default_colours(self) -> None:
468
+ """Create default white colours for all instances."""
469
+ default_colours = np.ones((self.num_instances, 3), dtype=np.float32)
470
+ self.colour_buffer = self.device.create_buffer_with_data(
471
+ data=default_colours.tobytes(),
472
+ usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
473
+ )
474
+
475
+ def _create_colour_buffer(self, colours) -> None:
476
+ """Create colour buffer from GPUBuffer or numpy array."""
477
+ if isinstance(colours, wgpu.GPUBuffer):
478
+ self.colour_buffer = colours
479
+ else:
480
+ colour_array = colours.astype(np.float32)
481
+ self.colour_buffer = self.device.create_buffer_with_data(
482
+ data=colour_array.tobytes(),
483
+ usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
484
+ )
485
+
486
+ def _set_geometry_data(self, geometry_data) -> None:
487
+ """Set geometry data from GPUBuffer or numpy array."""
488
+ if geometry_data is None:
489
+ raise ValueError(GEOM_ERROR)
490
+
491
+ if isinstance(geometry_data, wgpu.GPUBuffer):
492
+ self.geometry_buffer = geometry_data
493
+ self.num_vertices = geometry_data.size // (8 * 4)
494
+ else:
495
+ self._process_geometry_array(geometry_data)
496
+
497
+ def _process_geometry_array(self, geometry_data) -> None:
498
+ """Process geometry numpy array and create buffer."""
499
+ geometry_data = np.asarray(geometry_data, dtype=np.float32)
500
+ geometry_data = self._validate_and_reshape_geometry(geometry_data)
501
+ self.num_vertices = geometry_data.shape[0]
502
+
503
+ if self.geometry_buffer:
504
+ self.geometry_buffer.destroy()
505
+ self.geometry_buffer = self.device.create_buffer_with_data(
506
+ data=geometry_data.tobytes(),
507
+ usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
508
+ label="instanced_geometry_buffer",
509
+ )
510
+
511
+ def _validate_and_reshape_geometry(self, geometry_data) -> np.ndarray:
512
+ """Validate geometry data dimensions and reshape if needed."""
513
+ if geometry_data.ndim == 1:
514
+ geometry_data = geometry_data.reshape(-1, 8)
515
+ elif geometry_data.ndim != 2:
516
+ raise ValueError(f"geometry_data must be 1D or 2D array, got {geometry_data.ndim}D")
517
+
518
+ if geometry_data.shape[1] != 8:
519
+ raise ValueError(f"geometry_data must have 8 components (x,y,z,nx,ny,nz,u,v), got {geometry_data.shape[1]}")
520
+
521
+ return geometry_data
522
+
523
+ def update_uniforms(self, **kwargs) -> None:
524
+ """
525
+ Update uniform buffer values.
526
+
527
+ Args:
528
+ **kwargs: Pipeline-specific uniform parameters
529
+ - mvp: 4x4 model view projection matrix
530
+ - view_matrix: 4x4 view matrix
531
+ - colour: 3-element array of RGB color values
532
+ - instance_transform: 4x4 transformation matrix for each instance
533
+ """
534
+ if "mvp" in kwargs and kwargs["mvp"] is not None:
535
+ self.uniform_data["MVP"] = kwargs["mvp"]
536
+
537
+ if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
538
+ self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]
539
+
540
+ if "colour" in kwargs and kwargs["colour"] is not None:
541
+ self.uniform_data["colour"][:3] = kwargs["colour"]
542
+
543
+ if "instance_transform" in kwargs and kwargs["instance_transform"] is not None:
544
+ self.uniform_data["instance_transform"] = kwargs["instance_transform"]
545
+
546
+ self.device.queue.write_buffer(self.uniform_buffer, 0, self.uniform_data.tobytes())
547
+
548
+ def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs) -> None:
549
+ """
550
+ Render the instanced geometry.
551
+
552
+ Args:
553
+ render_pass: Active render pass encoder
554
+ **kwargs: Pipeline-specific render parameters
555
+ - num_instances: Number of instances to render (defaults to all)
556
+ """
557
+ num_instances = kwargs.get("num_instances", None)
558
+
559
+ if (
560
+ self.position_buffer is None
561
+ or self.colour_buffer is None
562
+ or self.instance_id_buffer is None
563
+ or self.geometry_buffer is None
564
+ ):
565
+ return
566
+
567
+ count = num_instances if num_instances is not None else self.num_instances
568
+
569
+ render_pass.set_pipeline(self.pipeline)
570
+ if self.bind_group:
571
+ render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
572
+
573
+ # Set instance buffers (must match base class layout)
574
+ render_pass.set_vertex_buffer(0, self.position_buffer)
575
+ render_pass.set_vertex_buffer(1, self.colour_buffer) # Dummy colour buffer
576
+ render_pass.set_vertex_buffer(2, self.instance_id_buffer)
577
+
578
+ # Set single interleaved geometry buffer
579
+ render_pass.set_vertex_buffer(3, self.geometry_buffer) # locations(3,4,5) interleaved
580
+
581
+ render_pass.draw(self.num_vertices, count)
582
+
583
+ def cleanup(self) -> None:
584
+ """Release resources."""
585
+ if self.position_buffer:
586
+ self.position_buffer.destroy()
587
+ if self.colour_buffer:
588
+ self.colour_buffer.destroy()
589
+ if self.instance_id_buffer:
590
+ self.instance_id_buffer.destroy()
591
+ if self.geometry_buffer:
592
+ self.geometry_buffer.destroy()
593
+
594
+ super().cleanup()