ncca-ngl 0.3.4__py3-none-any.whl → 0.5.0__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 +3 -4
  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 +7 -0
  16. ncca/ngl/vec2.py +58 -292
  17. ncca/ngl/vec2_array.py +79 -28
  18. ncca/ngl/vec3.py +59 -340
  19. ncca/ngl/vec3_array.py +76 -23
  20. ncca/ngl/vec4.py +90 -190
  21. ncca/ngl/vec4_array.py +78 -27
  22. ncca/ngl/vector_base.py +542 -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 +31 -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.4.dist-info → ncca_ngl-0.5.0.dist-info}/METADATA +3 -2
  53. ncca_ngl-0.5.0.dist-info/RECORD +105 -0
  54. ncca/ngl/widgets/transformation_widget.py +0 -299
  55. ncca_ngl-0.3.4.dist-info/RECORD +0 -82
  56. {ncca_ngl-0.3.4.dist-info → ncca_ngl-0.5.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,640 @@
1
+ #!/usr/bin/env -S uv run --active --script
2
+ import sys
3
+ import time
4
+ from typing import Tuple
5
+
6
+ import numpy as np
7
+ from PySide6.QtCore import Qt, QTimer
8
+ from PySide6.QtGui import QColor
9
+ from PySide6.QtWidgets import QApplication
10
+ from wgpu.utils import get_default_device
11
+
12
+ from ncca.ngl import Mat4, PerspMode, PrimData, Vec3, look_at, perspective
13
+ from ncca.ngl.webgpu import PipelineFactory, PipelineType, WebGPUWidget, __version__
14
+
15
+ NUM_POINTS = 10000
16
+
17
+
18
+ class WebGPUScene(WebGPUWidget):
19
+ """
20
+ A concrete implementation of NumpyBufferWidget for a WebGPU scene.
21
+
22
+ This class implements the abstract methods to provide functionality for initializing,
23
+ painting, and resizing the WebGPU context.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ background_colour: Tuple[float, float, float, float] = (0.4, 0.4, 0.4, 1.0),
29
+ ):
30
+ super().__init__(background_colour=background_colour)
31
+ self.setWindowTitle("WebGPU Pipeline Demo - Dynamic Background Colors")
32
+ self.device = None
33
+ self.pipeline = None
34
+ self.vertex_buffer = None
35
+ self.msaa_sample_count = 4
36
+ self.rotation = 0.0
37
+ self.view = look_at(Vec3(0, 2, 14), Vec3(0, 0, 0), Vec3(0, 1, 0))
38
+ self.animate = True
39
+ self.project = Mat4()
40
+ self._initialize_web_gpu()
41
+ self.update()
42
+
43
+ def _initialize_web_gpu(self) -> None:
44
+ """
45
+ Initialize the WebGPU context.
46
+
47
+ This method sets up the WebGPU context for the scene.
48
+ """
49
+ print("initializeWebGPU")
50
+ try:
51
+ self.device = get_default_device()
52
+ except Exception as e:
53
+ print(f"Failed to initialize WebGPU: {e}")
54
+ return
55
+ self._create_buffers(NUM_POINTS)
56
+
57
+ self.pipelines = []
58
+ self.pipeline_backgrounds = {} # Store background colors for each pipeline
59
+
60
+ # Define subtle background colors for each pipeline type
61
+ pipeline_colors = {
62
+ "MULTI_COLOURED_POINTS": (0.1, 0.1, 0.3, 1.0), # Dark blue
63
+ "SINGLE_COLOUR_POINTS": (0.3, 0.1, 0.1, 1.0), # Dark red
64
+ "MULTI_COLOURED_LINES": (0.1, 0.3, 0.1, 1.0), # Dark green
65
+ "SINGLE_COLOUR_LINES": (0.3, 0.3, 0.1, 1.0), # Dark yellow
66
+ "MULTI_COLOURED_TRIANGLES": (0.3, 0.1, 0.3, 1.0), # Dark magenta
67
+ "SINGLE_COLOUR_TRIANGLES": (0.1, 0.3, 0.3, 1.0), # Dark cyan
68
+ "TRIANGLE_LIST_MULTI_COLOURED": (0.2, 0.1, 0.4, 1.0), # Dark purple
69
+ "TRIANGLE_LIST_SINGLE_COLOUR": (0.4, 0.2, 0.1, 1.0), # Dark orange
70
+ "TRIANGLE_STRIP_MULTI_COLOURED": (0.1, 0.4, 0.2, 1.0), # Dark teal
71
+ "TRIANGLE_STRIP_SINGLE_COLOUR": (0.2, 0.2, 0.2, 1.0), # Dark gray
72
+ "POINT_LIST_MULTI_COLOURED": (0.4, 0.1, 0.2, 1.0), # Dark pink
73
+ "POINT_LIST_SINGLE_COLOUR": (0.1, 0.2, 0.4, 1.0), # Dark indigo
74
+ "MULTI_COLOURED_INSTANCED_GEOMETRY": (0.3, 0.15, 0.05, 1.0), # Dark gold
75
+ "SINGLE_COLOUR_INSTANCED_GEOMETRY": (0.05, 0.15, 0.3, 1.0), # Dark navy
76
+ }
77
+
78
+ self.pipelines.append(
79
+ (
80
+ PipelineFactory.create_pipeline(
81
+ self.device, PipelineType.MULTI_COLOURED_POINTS
82
+ ),
83
+ self._render_multi_colour_point_pipeline,
84
+ "PipelineType.MULTI_COLOURED_POINTS",
85
+ pipeline_colors["MULTI_COLOURED_POINTS"],
86
+ )
87
+ )
88
+ self.pipelines.append(
89
+ (
90
+ PipelineFactory.create_pipeline(
91
+ self.device, PipelineType.SINGLE_COLOUR_POINTS
92
+ ),
93
+ self._render_single_colour_point_pipeline,
94
+ "PipelineType.SINGLE_COLOUR_POINTS",
95
+ pipeline_colors["SINGLE_COLOUR_POINTS"],
96
+ )
97
+ )
98
+ self.pipelines.append(
99
+ (
100
+ PipelineFactory.create_pipeline(
101
+ self.device, PipelineType.MULTI_COLOURED_LINES
102
+ ),
103
+ self._render_multi_colour_line_pipeline,
104
+ "PipelineType.MULTI_COLOURED_LINES",
105
+ pipeline_colors["MULTI_COLOURED_LINES"],
106
+ )
107
+ )
108
+ self.pipelines.append(
109
+ (
110
+ PipelineFactory.create_pipeline(
111
+ self.device, PipelineType.SINGLE_COLOUR_LINES
112
+ ),
113
+ self._render_single_colour_line_pipeline,
114
+ "PipelineType.SINGLE_COLOUR_LINES",
115
+ pipeline_colors["SINGLE_COLOUR_LINES"],
116
+ )
117
+ )
118
+ self.pipelines.append(
119
+ (
120
+ PipelineFactory.create_pipeline(
121
+ self.device, PipelineType.MULTI_COLOURED_TRIANGLES
122
+ ),
123
+ self._render_multi_colour_triangle_pipeline,
124
+ "PipelineType.MULTI_COLOURED_TRIANGLES",
125
+ pipeline_colors["MULTI_COLOURED_TRIANGLES"],
126
+ )
127
+ )
128
+ self.pipelines.append(
129
+ (
130
+ PipelineFactory.create_pipeline(
131
+ self.device, PipelineType.SINGLE_COLOUR_TRIANGLES
132
+ ),
133
+ self._render_single_colour_triangle_pipeline,
134
+ "PipelineType.SINGLE_COLOUR_TRIANGLES",
135
+ pipeline_colors["SINGLE_COLOUR_TRIANGLES"],
136
+ )
137
+ )
138
+ self.pipelines.append(
139
+ (
140
+ PipelineFactory.create_pipeline(
141
+ self.device, PipelineType.TRIANGLE_LIST_MULTI_COLOURED
142
+ ),
143
+ self._render_multi_colour_triangle_pipeline,
144
+ "PipelineType.TRIANGLE_LIST_MULTI_COLOURED",
145
+ pipeline_colors["TRIANGLE_LIST_MULTI_COLOURED"],
146
+ )
147
+ )
148
+ self.pipelines.append(
149
+ (
150
+ PipelineFactory.create_pipeline(
151
+ self.device, PipelineType.TRIANGLE_LIST_SINGLE_COLOUR
152
+ ),
153
+ self._render_triangle_list_single_colour_pipeline,
154
+ "PipelineType.TRIANGLE_LIST_SINGLE_COLOUR",
155
+ pipeline_colors["TRIANGLE_LIST_SINGLE_COLOUR"],
156
+ )
157
+ )
158
+ self.pipelines.append(
159
+ (
160
+ PipelineFactory.create_pipeline(
161
+ self.device, PipelineType.TRIANGLE_STRIP_MULTI_COLOURED
162
+ ),
163
+ self._render_triangle_strip_multi_colour_pipeline,
164
+ "PipelineType.TRIANGLE_STRIP_MULTI_COLOURED",
165
+ pipeline_colors["TRIANGLE_STRIP_MULTI_COLOURED"],
166
+ )
167
+ )
168
+ self.pipelines.append(
169
+ (
170
+ PipelineFactory.create_pipeline(
171
+ self.device, PipelineType.TRIANGLE_STRIP_SINGLE_COLOUR
172
+ ),
173
+ self._render_triangle_strip_single_colour_pipeline,
174
+ "PipelineType.TRIANGLE_STRIP_SINGLE_COLOUR",
175
+ pipeline_colors["TRIANGLE_STRIP_SINGLE_COLOUR"],
176
+ )
177
+ )
178
+ self.pipelines.append(
179
+ (
180
+ PipelineFactory.create_pipeline(
181
+ self.device, PipelineType.POINT_LIST_MULTI_COLOURED
182
+ ),
183
+ self._render_point_list_multi_colour_pipeline,
184
+ "PipelineType.POINT_LIST_MULTI_COLOURED",
185
+ pipeline_colors["POINT_LIST_MULTI_COLOURED"],
186
+ )
187
+ )
188
+ self.pipelines.append(
189
+ (
190
+ PipelineFactory.create_pipeline(
191
+ self.device, PipelineType.POINT_LIST_SINGLE_COLOUR
192
+ ),
193
+ self._render_point_list_single_colour_pipeline,
194
+ "PipelineType.POINT_LIST_SINGLE_COLOUR",
195
+ pipeline_colors["POINT_LIST_SINGLE_COLOUR"],
196
+ )
197
+ )
198
+
199
+ # Add instanced geometry pipelines
200
+ self.pipelines.append(
201
+ (
202
+ PipelineFactory.create_pipeline(
203
+ self.device, PipelineType.MULTI_COLOURED_INSTANCED_GEOMETRY
204
+ ),
205
+ self._render_multi_colour_instanced_geometry_pipeline,
206
+ "PipelineType.MULTI_COLOURED_INSTANCED_GEOMETRY",
207
+ pipeline_colors["MULTI_COLOURED_INSTANCED_GEOMETRY"],
208
+ )
209
+ )
210
+ self.pipelines.append(
211
+ (
212
+ PipelineFactory.create_pipeline(
213
+ self.device, PipelineType.SINGLE_COLOUR_INSTANCED_GEOMETRY
214
+ ),
215
+ self._render_single_colour_instanced_geometry_pipeline,
216
+ "PipelineType.SINGLE_COLOUR_INSTANCED_GEOMETRY",
217
+ pipeline_colors["SINGLE_COLOUR_INSTANCED_GEOMETRY"],
218
+ )
219
+ )
220
+
221
+ self.current_pipeline_index = 1
222
+ # Initialize render textures with default size
223
+ self.texture_size = (1024, 720)
224
+ self._create_render_buffer()
225
+
226
+ # Setup timers
227
+ self.startTimer(16) # Animation timer (~60 FPS)
228
+ self.pipeline_timer = QTimer()
229
+ self.pipeline_timer.timeout.connect(self.switch_pipeline)
230
+ self.pipeline_timer.start(1000) # Switch every 5 seconds
231
+
232
+ def _create_buffers(self, num_points):
233
+ rng = np.random.default_rng(int(time.time()))
234
+ self.colours = rng.random((num_points, 3)).astype(np.float32)
235
+ # Create 3D positions for line rendering with Z elevation
236
+ self.positions = rng.uniform(-4.0, 4.0, size=(num_points, 3)).astype(np.float32)
237
+ # For line rendering, we can use the full 3D positions or just X,Y depending on desired effect
238
+ self.positions_2d = self.positions[
239
+ :, :2
240
+ ] # Take only X,Y components for 2D line effects
241
+
242
+ # Create triangle data for triangle list
243
+ num_triangles = 100
244
+ self.triangle_positions = np.zeros((num_triangles * 3, 3), dtype=np.float32)
245
+ self.triangle_colours = rng.random((num_triangles * 3, 3)).astype(np.float32)
246
+
247
+ for i in range(num_triangles):
248
+ # Create a triangle around a random center point in 3D space
249
+ center = rng.uniform(-3.0, 3.0, 3)
250
+ radius = rng.uniform(0.2, 0.8)
251
+
252
+ # Generate random triangle vertices in 3D using spherical coordinates
253
+ # First vertex
254
+ theta1 = rng.uniform(0, 2 * np.pi) # azimuth
255
+ phi1 = rng.uniform(0, np.pi) # polar
256
+ vertex1 = radius * np.array(
257
+ [
258
+ np.sin(phi1) * np.cos(theta1),
259
+ np.sin(phi1) * np.sin(theta1),
260
+ np.cos(phi1),
261
+ ]
262
+ )
263
+
264
+ # Second vertex (different orientation)
265
+ theta2 = theta1 + rng.uniform(2.0, 3.0)
266
+ phi2 = rng.uniform(0, np.pi)
267
+ vertex2 = radius * np.array(
268
+ [
269
+ np.sin(phi2) * np.cos(theta2),
270
+ np.sin(phi2) * np.sin(theta2),
271
+ np.cos(phi2),
272
+ ]
273
+ )
274
+
275
+ # Third vertex (completes the triangle)
276
+ theta3 = theta2 + rng.uniform(2.0, 3.0)
277
+ phi3 = rng.uniform(0, np.pi)
278
+ vertex3 = radius * np.array(
279
+ [
280
+ np.sin(phi3) * np.cos(theta3),
281
+ np.sin(phi3) * np.sin(theta3),
282
+ np.cos(phi3),
283
+ ]
284
+ )
285
+
286
+ self.triangle_positions[i * 3] = center + vertex1
287
+ self.triangle_positions[i * 3 + 1] = center + vertex2
288
+ self.triangle_positions[i * 3 + 2] = center + vertex3
289
+
290
+ # Create triangle strip data with 3D helix/spring pattern
291
+ strip_length = 50
292
+ self.triangle_strip_positions = np.zeros((strip_length, 3), dtype=np.float32)
293
+ self.triangle_strip_colours = rng.random((strip_length, 3)).astype(np.float32)
294
+
295
+ for i in range(strip_length):
296
+ t = i / (strip_length - 1)
297
+ # Create a 3D helix/spring pattern for triangle strip
298
+ angle = t * 4 * np.pi # Multiple rotations
299
+ radius = 2.0 + 0.5 * np.sin(angle * 2) # Varying radius
300
+
301
+ # Alternate between upper and lower points of strip
302
+ if i % 2 == 0:
303
+ # Upper points
304
+ self.triangle_strip_positions[i] = [
305
+ radius * np.cos(angle), # X
306
+ radius * np.sin(angle) + 1.0, # Y (offset up)
307
+ -2 + t * 4, # Z (forward progression)
308
+ ]
309
+ else:
310
+ # Lower points
311
+ self.triangle_strip_positions[i] = [
312
+ radius * np.cos(angle), # X
313
+ radius * np.sin(angle) - 1.0, # Y (offset down)
314
+ -2 + t * 4, # Z (forward progression)
315
+ ]
316
+
317
+ # Create instanced geometry data
318
+ self._create_instanced_geometry_data(rng)
319
+
320
+ def _create_instanced_geometry_data(self, rng):
321
+ """Create data for instanced geometry rendering."""
322
+ # Create geometry using PrimData (in correct interleaved format)
323
+ geometry_data = PrimData.primitive("teapot")
324
+
325
+ # Ensure data is in (num_vertices, 8) format for new API
326
+ if geometry_data.ndim == 1:
327
+ geometry_data = geometry_data.reshape(-1, 8)
328
+ elif geometry_data.shape[1] != 8:
329
+ raise ValueError(
330
+ f"Expected 8 components per vertex, got {geometry_data.shape[1]}"
331
+ )
332
+
333
+ # Store the complete interleaved geometry data (x,y,z,nx,ny,nz,u,v)
334
+ self.geometry_data = geometry_data
335
+
336
+ # Create instance positions (grid of geometry instances)
337
+ grid_size = 15
338
+ self.instance_positions = []
339
+ self.instance_colours = []
340
+
341
+ for i in range(grid_size):
342
+ for j in range(grid_size):
343
+ x = (i - grid_size / 2 + 0.5) * 1.2
344
+ y = (j - grid_size / 2 + 0.5) * 1.2
345
+ z = rng.uniform(-grid_size / 2, grid_size / 2)
346
+ self.instance_positions.append([x, y, z])
347
+
348
+ self.instance_colours.append(rng.random((1, 3)).astype(np.float32))
349
+
350
+ self.instance_positions = np.array(self.instance_positions, dtype=np.float32)
351
+ self.instance_colours = np.array(self.instance_colours, dtype=np.float32)
352
+
353
+ def _create_render_buffer(self):
354
+ """Delegate to parent class method."""
355
+ super()._create_render_buffer()
356
+
357
+ def paintWebGPU(self) -> None:
358
+ """
359
+ Paint WebGPU content.
360
+
361
+ This method renders WebGPU content for the scene.
362
+ """
363
+ # Get the current pipeline and its background color
364
+ current_pipeline = self.pipelines[self.current_pipeline_index]
365
+ pipeline_name = current_pipeline[2]
366
+ pipeline_bg_color = current_pipeline[3]
367
+
368
+ # Temporarily update the background color for this pipeline
369
+ original_bg_color = self.background_colour
370
+ self.background_colour = pipeline_bg_color
371
+
372
+ self.render_text(
373
+ 10,
374
+ 20,
375
+ f"{pipeline_name}",
376
+ size=20,
377
+ colour=QColor(255, 255, 255), # White text for better contrast
378
+ )
379
+ try:
380
+ # Create a new command encoder for the render pass
381
+ command_encoder = self.device.create_command_encoder()
382
+ render_pass = self._create_render_pass(command_encoder)
383
+ self.update_uniform_buffers()
384
+ render_pass.set_viewport(
385
+ 0, 0, self.texture_size[0], self.texture_size[1], 0, 1
386
+ )
387
+ self.pipelines[self.current_pipeline_index][1](render_pass)
388
+ render_pass.end()
389
+ self.device.queue.submit([command_encoder.finish()])
390
+ except Exception as e:
391
+ print(f"Failed to paint WebGPU content: {e}")
392
+ finally:
393
+ # Restore original background color
394
+ self.background_colour = original_bg_color
395
+
396
+ def resizeWebGPU(self, w, h) -> None:
397
+ """
398
+ Called whenever the window is resized.
399
+ It's crucial to update the viewport and projection matrix here.
400
+
401
+ Args:
402
+ w: The new width of the window.
403
+ h: The new height of the window.
404
+ """
405
+
406
+ # Update texture size to match window dimensions
407
+ self.texture_size = (w, h)
408
+
409
+ # Update projection matrix
410
+ self.project = perspective(
411
+ 45.0, w / h if h > 0 else 1, 0.1, 100.0, PerspMode.WebGPU
412
+ )
413
+
414
+ self.update()
415
+
416
+ def _render_pipeline(
417
+ self, render_pass, positions, colours=None, point_size=None, colour=None
418
+ ):
419
+ """Generic pipeline rendering method to eliminate code duplication."""
420
+ pipeline = self.pipelines[self.current_pipeline_index][0]
421
+
422
+ # Set data based on whether we have colours
423
+ if colours is not None:
424
+ pipeline.set_data(positions, colours)
425
+ else:
426
+ pipeline.set_data(positions)
427
+
428
+ pipeline.render(render_pass)
429
+
430
+ # Update uniforms based on what's provided
431
+ if point_size is not None:
432
+ if colours is not None:
433
+ pipeline.update_uniforms(
434
+ mvp=self.mvp_matrix,
435
+ view_matrix=self.view_matrix,
436
+ point_size=point_size,
437
+ )
438
+ else:
439
+ pipeline.update_uniforms(
440
+ mvp=self.mvp_matrix,
441
+ view_matrix=self.view_matrix,
442
+ colour=colour
443
+ if colour is not None
444
+ else np.array([1, 1, 1], dtype=np.float32),
445
+ point_size=point_size,
446
+ )
447
+ elif colour is not None:
448
+ # Handle color for single-colour pipelines without point_size
449
+ pipeline.update_uniforms(
450
+ mvp=self.mvp_matrix,
451
+ colour=colour,
452
+ )
453
+ else:
454
+ pipeline.update_uniforms(mvp=self.mvp_matrix)
455
+
456
+ def _render_multi_colour_point_pipeline(self, render_pass):
457
+ self._render_pipeline(
458
+ render_pass, self.positions, colours=self.colours, point_size=0.05
459
+ )
460
+
461
+ def _render_single_colour_point_pipeline(self, render_pass):
462
+ # Use bright yellow for single colour points
463
+ yellow_color = np.array([1.0, 1.0, 0.0], dtype=np.float32)
464
+ self._render_pipeline(
465
+ render_pass, self.positions, point_size=0.05, colour=yellow_color
466
+ )
467
+
468
+ def _render_multi_colour_line_pipeline(self, render_pass):
469
+ self._render_pipeline(render_pass, self.positions_2d, colours=self.colours)
470
+
471
+ def _render_single_colour_line_pipeline(self, render_pass):
472
+ # Use bright magenta for single colour lines
473
+ magenta_color = np.array([1.0, 0.0, 1.0], dtype=np.float32)
474
+ self._render_pipeline(render_pass, self.positions_2d, colour=magenta_color)
475
+
476
+ def _render_multi_colour_triangle_pipeline(self, render_pass):
477
+ self._render_pipeline(
478
+ render_pass, self.triangle_positions, colours=self.triangle_colours
479
+ )
480
+
481
+ def _render_single_colour_triangle_pipeline(self, render_pass):
482
+ # Use a bright orange color for single colour triangles
483
+ orange_color = np.array([1.0, 0.5, 0.0], dtype=np.float32)
484
+ self._render_pipeline(render_pass, self.triangle_positions, colour=orange_color)
485
+
486
+ def _render_triangle_list_single_colour_pipeline(self, render_pass):
487
+ # Use a bright purple color for triangle list single colour
488
+ purple_color = np.array([0.8, 0.2, 0.8], dtype=np.float32)
489
+ self._render_pipeline(render_pass, self.triangle_positions, colour=purple_color)
490
+
491
+ def _render_triangle_strip_multi_colour_pipeline(self, render_pass):
492
+ self._render_pipeline(
493
+ render_pass,
494
+ self.triangle_strip_positions,
495
+ colours=self.triangle_strip_colours,
496
+ )
497
+
498
+ def _render_triangle_strip_single_colour_pipeline(self, render_pass):
499
+ # Use a bright cyan color for triangle strip single colour
500
+ cyan_color = np.array([0.0, 0.8, 0.8], dtype=np.float32)
501
+ self._render_pipeline(
502
+ render_pass, self.triangle_strip_positions, colour=cyan_color
503
+ )
504
+
505
+ def _render_point_list_multi_colour_pipeline(self, render_pass):
506
+ self._render_pipeline(
507
+ render_pass, self.positions, colours=self.colours, point_size=5.0
508
+ )
509
+
510
+ def _render_point_list_single_colour_pipeline(self, render_pass):
511
+ # Use bright lime green for point list single colour
512
+ lime_color = np.array([0.5, 1.0, 0.0], dtype=np.float32)
513
+ self._render_pipeline(
514
+ render_pass, self.positions, point_size=5.0, colour=lime_color
515
+ )
516
+
517
+ def _render_multi_colour_instanced_geometry_pipeline(self, render_pass):
518
+ """Render multi-colour instanced geometry."""
519
+ pipeline = self.pipelines[self.current_pipeline_index][0]
520
+
521
+ # Set instanced geometry data using new simplified API
522
+ pipeline.set_data(
523
+ positions=self.instance_positions,
524
+ colours=self.instance_colours,
525
+ geometry_data=self.geometry_data, # Single interleaved parameter!
526
+ )
527
+
528
+ # Update uniforms
529
+ pipeline.update_uniforms(
530
+ mvp=self.mvp_matrix,
531
+ view_matrix=self.view_matrix,
532
+ instance_transform=np.eye(4, dtype=np.float32),
533
+ )
534
+
535
+ pipeline.render(render_pass, num_instances=len(self.instance_positions))
536
+
537
+ def _render_single_colour_instanced_geometry_pipeline(self, render_pass):
538
+ """Render single-colour instanced geometry."""
539
+ pipeline = self.pipelines[self.current_pipeline_index][0]
540
+
541
+ # Set instanced geometry data using new simplified API
542
+ pipeline.set_data(
543
+ positions=self.instance_positions,
544
+ geometry_data=self.geometry_data, # Single interleaved parameter!
545
+ )
546
+
547
+ # Update uniforms with orange color
548
+ pipeline.update_uniforms(
549
+ mvp=self.mvp_matrix,
550
+ view_matrix=self.view_matrix,
551
+ colour=np.array([1.0, 0.6, 0.1], dtype=np.float32), # Orange
552
+ instance_transform=np.eye(4, dtype=np.float32),
553
+ )
554
+
555
+ pipeline.render(render_pass, num_instances=len(self.instance_positions))
556
+
557
+ def update_uniform_buffers(self) -> None:
558
+ """
559
+ update the uniform buffers for the line pipeline.
560
+ """
561
+ rotation = Mat4.rotate_y(self.rotation)
562
+ self.mvp_matrix = (
563
+ (self.project @ self.view @ rotation).to_numpy().astype(np.float32)
564
+ )
565
+ self.view_matrix = (self.view @ rotation).to_numpy().astype(np.float32)
566
+
567
+ def keyPressEvent(self, event) -> None:
568
+ """
569
+ Handles keyboard press events.
570
+
571
+ Args:
572
+ event: The QKeyEvent object containing information about the key press.
573
+ """
574
+ key = event.key()
575
+ if key == Qt.Key.Key_Escape:
576
+ self.close() # Exit the application
577
+ elif key == Qt.Key.Key_Space:
578
+ self.animate = not self.animate
579
+ elif key == Qt.Key.Key_Left:
580
+ # Switch to previous pipeline
581
+ self.current_pipeline_index = (self.current_pipeline_index - 1) % len(
582
+ self.pipelines
583
+ )
584
+ print(f"Switched to {self.pipelines[self.current_pipeline_index][2]}")
585
+ elif key == Qt.Key.Key_Right:
586
+ # Switch to next pipeline
587
+ self.current_pipeline_index = (self.current_pipeline_index + 1) % len(
588
+ self.pipelines
589
+ )
590
+ print(f"Switched to {self.pipelines[self.current_pipeline_index][2]}")
591
+ elif key == Qt.Key.Key_A:
592
+ # Toggle automatic pipeline switching
593
+ if self.pipeline_timer.isActive():
594
+ self.pipeline_timer.stop()
595
+ print("Pipeline auto-switch disabled")
596
+ else:
597
+ self.pipeline_timer.start(1000)
598
+ print("Pipeline auto-switch enabled")
599
+ self.update()
600
+
601
+ # Call the base class implementation for any unhandled events
602
+ super().keyPressEvent(event)
603
+
604
+ def switch_pipeline(self) -> None:
605
+ """Switch to the next pipeline in the list."""
606
+ self.current_pipeline_index = (self.current_pipeline_index + 1) % len(
607
+ self.pipelines
608
+ )
609
+ print(f"Switched to {self.pipelines[self.current_pipeline_index][2]}")
610
+ self.update()
611
+
612
+ def timerEvent(self, event) -> None:
613
+ """
614
+ Handle timer events to update the scene.
615
+ """
616
+ if self.animate:
617
+ self.rotation += 0.5
618
+ self.update()
619
+
620
+
621
+ def main():
622
+ """
623
+ Main function to run the application.
624
+ Parses command line arguments and initializes the WebGPUScene.
625
+ """
626
+ app = QApplication(sys.argv)
627
+
628
+ # Use basic blue background as requested
629
+ # Each pipeline will override this with its own subtle background color
630
+ background_colour = (0.1, 0.1, 0.3, 1.0) # Basic blue background
631
+
632
+ win = WebGPUScene(background_colour=background_colour)
633
+ win.resize(1024, 720)
634
+ win.show()
635
+ sys.exit(app.exec())
636
+
637
+
638
+ if __name__ == "__main__":
639
+ print(f"ncca-ngl.WebGPU {__version__}")
640
+ main()