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,349 @@
1
+ """
2
+ Native point-list rendering pipeline for WebGPU.
3
+ Handles point rendering using WebGPU's native point-list topology instead of billboarding.
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 POINT_LIST_SHADER_MULTI_COLOURED, POINT_LIST_SHADER_SINGLE_COLOUR
13
+ from .webgpu_constants import NGLToWebGPU
14
+
15
+
16
+ class PointListPipelineMultiColour(BaseWebGPUPipeline):
17
+ """
18
+ A pipeline for rendering points using WebGPU's native point-list topology.
19
+
20
+ Features:
21
+ - Native WebGPU point-list rendering (no billboarding)
22
+ - Model, View Projection matrix support
23
+ - MSAA support
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ device: wgpu.GPUDevice,
29
+ data_type: str = "Vec3",
30
+ texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
31
+ depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
32
+ msaa_sample_count: int = 4,
33
+ stride: int = 0,
34
+ ):
35
+ """
36
+ Initialize the point list rendering pipeline.
37
+
38
+ Args:
39
+ device: WebGPU device
40
+ data_type: Vertex data type (e.g., "Vec3", "Vec2")
41
+ texture_format: colour attachment format
42
+ depth_format: Depth attachment format
43
+ msaa_sample_count: Number of MSAA samples
44
+ stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
45
+ """
46
+ # Pipeline-specific buffer tracking
47
+ self.position_buffer: Optional[wgpu.GPUBuffer] = None
48
+ self.colour_buffer: Optional[wgpu.GPUBuffer] = None
49
+ self.num_points: int = 0
50
+
51
+ super().__init__(
52
+ device=device,
53
+ texture_format=texture_format,
54
+ depth_format=depth_format,
55
+ msaa_sample_count=msaa_sample_count,
56
+ data_type=data_type,
57
+ stride=stride,
58
+ )
59
+
60
+ def get_dtype(self) -> np.dtype:
61
+ """Get the data type of the pipeline."""
62
+ return np.dtype([
63
+ ("MVP", "float32", (4, 4)),
64
+ ])
65
+
66
+ def _get_shader_code(self) -> str:
67
+ """Get the WGSL shader code for this pipeline."""
68
+ return POINT_LIST_SHADER_MULTI_COLOURED
69
+
70
+ def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
71
+ """Points are rendered as point list."""
72
+ return wgpu.PrimitiveTopology.point_list
73
+
74
+ def _get_vertex_buffer_layouts(self):
75
+ """Get vertex buffer layout configurations for the pipeline."""
76
+ position_layout = {
77
+ "array_stride": self._stride,
78
+ "step_mode": wgpu.VertexStepMode.vertex,
79
+ "attributes": [
80
+ {
81
+ "format": NGLToWebGPU.vertex_format(self._data_type),
82
+ "offset": 0,
83
+ "shader_location": 0,
84
+ }
85
+ ],
86
+ }
87
+
88
+ colour_layout = {
89
+ "array_stride": 12, # 3 * float32 for RGB
90
+ "step_mode": wgpu.VertexStepMode.vertex,
91
+ "attributes": [
92
+ {
93
+ "format": wgpu.VertexFormat.float32x3,
94
+ "offset": 0,
95
+ "shader_location": 1,
96
+ }
97
+ ],
98
+ }
99
+
100
+ return [position_layout, colour_layout]
101
+
102
+ def _set_default_uniforms(self) -> None:
103
+ """Set default values for uniform data."""
104
+ ...
105
+
106
+ def _get_pipeline_label(self) -> str:
107
+ """Get the label for the pipeline."""
108
+ return "point_list_pipeline_multi_coloured"
109
+
110
+ def set_data(
111
+ self,
112
+ positions,
113
+ colours=None,
114
+ ) -> None:
115
+ """
116
+ Set the point data for rendering.
117
+
118
+ Args:
119
+ positions: Nx3 array of point positions or a pre-existing GPUBuffer.
120
+ colours: Nx3 array of point colours (RGB) or a pre-existing GPUBuffer.
121
+ If None, uses white.
122
+ """
123
+ # Handle positions
124
+ if isinstance(positions, wgpu.GPUBuffer):
125
+ self.position_buffer = positions
126
+ self.num_points = positions.size // self._stride
127
+ else: # numpy array
128
+ self.num_points = len(positions)
129
+ self.position_buffer, _ = self._create_or_update_buffer(
130
+ self.position_buffer,
131
+ positions,
132
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
133
+ "point_list_pipeline_multi_coloured_position_buffer",
134
+ )
135
+
136
+ # Handle colours
137
+ if colours is None:
138
+ # Create default white colours
139
+ default_colours = np.ones((self.num_points, 3), dtype=np.float32)
140
+ self.colour_buffer, _ = self._create_or_update_buffer(
141
+ self.colour_buffer,
142
+ default_colours,
143
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
144
+ "point_list_pipeline_multi_coloured_colour_buffer",
145
+ )
146
+ else:
147
+ self.colour_buffer, _ = self._create_or_update_buffer(
148
+ self.colour_buffer,
149
+ colours,
150
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
151
+ "point_list_pipeline_multi_coloured_colour_buffer",
152
+ )
153
+
154
+ def update_uniforms(self, **kwargs) -> None:
155
+ """
156
+ Update uniform buffer values.
157
+
158
+ Args:
159
+ **kwargs: Pipeline-specific uniform parameters
160
+ - mvp: 4x4 model view projection matrix
161
+ """
162
+ if "mvp" in kwargs and kwargs["mvp"] is not None:
163
+ self.uniform_data["MVP"] = kwargs["mvp"]
164
+
165
+ self.device.queue.write_buffer(self.uniform_buffer, 0, self.uniform_data.tobytes())
166
+
167
+ def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs) -> None:
168
+ """
169
+ Render the points.
170
+
171
+ Args:
172
+ render_pass: Active render pass encoder
173
+ **kwargs: Pipeline-specific render parameters
174
+ - num_points: Number of points to render (defaults to all)
175
+ """
176
+ num_points = kwargs.get("num_points", None)
177
+
178
+ if self.position_buffer is None or self.colour_buffer is None:
179
+ return
180
+
181
+ count = num_points if num_points is not None else self.num_points
182
+
183
+ render_pass.set_pipeline(self.pipeline)
184
+ if self.bind_group:
185
+ render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
186
+ render_pass.set_vertex_buffer(0, self.position_buffer)
187
+ render_pass.set_vertex_buffer(1, self.colour_buffer)
188
+ render_pass.draw(count) # Draw points as point list
189
+
190
+ def cleanup(self) -> None:
191
+ """Release resources."""
192
+ if self.position_buffer:
193
+ self.position_buffer.destroy()
194
+ if self.colour_buffer:
195
+ self.colour_buffer.destroy()
196
+ super().cleanup()
197
+
198
+
199
+ class PointListPipelineSingleColour(BaseWebGPUPipeline):
200
+ """
201
+ A pipeline for rendering points using WebGPU's native point-list topology.
202
+
203
+ Features:
204
+ - Native WebGPU point-list rendering (no billboarding)
205
+ - Single colour for all points
206
+ - Model, View Projection matrix support
207
+ - MSAA support
208
+ """
209
+
210
+ def __init__(
211
+ self,
212
+ device: wgpu.GPUDevice,
213
+ data_type: str = "Vec3",
214
+ texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
215
+ depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
216
+ msaa_sample_count: int = 4,
217
+ stride: int = 0,
218
+ ):
219
+ """
220
+ Initialize the point list rendering pipeline.
221
+
222
+ Args:
223
+ device: WebGPU device
224
+ data_type: Vertex data type (e.g., "Vec3", "Vec2")
225
+ texture_format: colour attachment format
226
+ depth_format: Depth attachment format
227
+ msaa_sample_count: Number of MSAA samples
228
+ stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
229
+ """
230
+ # Pipeline-specific buffer tracking
231
+ self.position_buffer: Optional[wgpu.GPUBuffer] = None
232
+ self.num_points: int = 0
233
+
234
+ super().__init__(
235
+ device=device,
236
+ texture_format=texture_format,
237
+ depth_format=depth_format,
238
+ msaa_sample_count=msaa_sample_count,
239
+ data_type=data_type,
240
+ stride=stride,
241
+ )
242
+
243
+ def get_dtype(self) -> np.dtype:
244
+ """Get the data type of the pipeline."""
245
+ return np.dtype([
246
+ ("MVP", "float32", (4, 4)),
247
+ ("Colour", "float32", 3),
248
+ ("padding", "float32"),
249
+ ])
250
+
251
+ def _get_shader_code(self) -> str:
252
+ """Get the WGSL shader code for this pipeline."""
253
+ return POINT_LIST_SHADER_SINGLE_COLOUR
254
+
255
+ def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
256
+ """Points are rendered as point list."""
257
+ return wgpu.PrimitiveTopology.point_list
258
+
259
+ def _get_vertex_buffer_layouts(self):
260
+ """Get vertex buffer layout configurations for the pipeline."""
261
+ position_layout = {
262
+ "array_stride": self._stride,
263
+ "step_mode": wgpu.VertexStepMode.vertex,
264
+ "attributes": [
265
+ {
266
+ "format": NGLToWebGPU.vertex_format(self._data_type),
267
+ "offset": 0,
268
+ "shader_location": 0,
269
+ }
270
+ ],
271
+ }
272
+
273
+ return [position_layout]
274
+
275
+ def _set_default_uniforms(self) -> None:
276
+ """Set default values for uniform data."""
277
+ self.uniform_data["Colour"] = np.array([1.0, 1.0, 1.0], dtype=np.float32) # Default White
278
+ self.uniform_data["padding"] = 0.0
279
+
280
+ def _get_pipeline_label(self) -> str:
281
+ """Get the label for the pipeline."""
282
+ return "point_list_pipeline_single_colour"
283
+
284
+ def set_data(self, positions, colours=None) -> None:
285
+ """
286
+ Set the point data for rendering.
287
+
288
+ Args:
289
+ positions: Nx3 array of point positions or a pre-existing GPUBuffer.
290
+ colours: Ignored for single colour pipeline
291
+ """
292
+ # Handle positions
293
+ if isinstance(positions, wgpu.GPUBuffer):
294
+ self.position_buffer = positions
295
+ self.num_points = positions.size // self._stride
296
+ else: # numpy array
297
+ self.num_points = len(positions)
298
+ self.position_buffer, _ = self._create_or_update_buffer(
299
+ self.position_buffer,
300
+ positions,
301
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
302
+ "point_list_pipeline_single_colour_position_buffer",
303
+ )
304
+
305
+ def update_uniforms(self, **kwargs) -> None:
306
+ """
307
+ Update uniform buffer values.
308
+
309
+ Args:
310
+ **kwargs: Pipeline-specific uniform parameters
311
+ - mvp: 4x4 model view projection matrix
312
+ - colour: 3-element array of RGB colour values
313
+ - point_size: Size of points
314
+ """
315
+ if "mvp" in kwargs and kwargs["mvp"] is not None:
316
+ self.uniform_data["MVP"] = kwargs["mvp"]
317
+
318
+ if "colour" in kwargs and kwargs["colour"] is not None:
319
+ self.uniform_data["Colour"] = kwargs["colour"]
320
+
321
+ self.device.queue.write_buffer(self.uniform_buffer, 0, self.uniform_data.tobytes())
322
+
323
+ def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs) -> None:
324
+ """
325
+ Render the points.
326
+
327
+ Args:
328
+ render_pass: Active render pass encoder
329
+ **kwargs: Pipeline-specific render parameters
330
+ - num_points: Number of points to render (defaults to all)
331
+ """
332
+ num_points = kwargs.get("num_points", None)
333
+
334
+ if self.position_buffer is None:
335
+ return
336
+
337
+ count = num_points if num_points is not None else self.num_points
338
+
339
+ render_pass.set_pipeline(self.pipeline)
340
+ if self.bind_group:
341
+ render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
342
+ render_pass.set_vertex_buffer(0, self.position_buffer)
343
+ render_pass.draw(count) # Draw points as point list
344
+
345
+ def cleanup(self) -> None:
346
+ """Release resources."""
347
+ if self.position_buffer:
348
+ self.position_buffer.destroy()
349
+ super().cleanup()
@@ -0,0 +1,336 @@
1
+ """
2
+ Generic point rendering pipeline for WebGPU.
3
+ Handles point rendering with customizable size, colour, and projection.
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ import numpy as np
9
+ import wgpu
10
+
11
+ from .base_webgpu_pipeline import BasePointPipeline
12
+ from .pipeline_shaders import POINT_SHADER_MULTI_COLOURED, POINT_SHADER_SINGLE_COLOUR
13
+ from .webgpu_constants import NGLToWebGPU
14
+
15
+
16
+ class PointPipelineMultiColour(BasePointPipeline):
17
+ """
18
+ A reusable pipeline for rendering points in WebGPU.
19
+
20
+ Features:
21
+ - Instanced rendering of points as quads
22
+ - Per-point colours
23
+ - Configurable point size
24
+ - Model, View Projection matrix support pass a projection only for 2D
25
+ - MSAA support
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ device: wgpu.GPUDevice,
31
+ data_type: str = "Vec3",
32
+ texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
33
+ depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
34
+ msaa_sample_count: int = 4,
35
+ stride: int = 0,
36
+ ):
37
+ """
38
+ Initialize the point rendering pipeline.
39
+
40
+ Args:
41
+ device: WebGPU device
42
+ texture_format: colour attachment format
43
+ depth_format: Depth attachment format
44
+ msaa_sample_count: Number of MSAA samples
45
+ stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
46
+ """
47
+ # Pipeline-specific buffer tracking
48
+ self.position_buffer: Optional[wgpu.GPUBuffer] = None
49
+ self.colour_buffer: Optional[wgpu.GPUBuffer] = None
50
+ self.num_points: int = 0
51
+
52
+ super().__init__(
53
+ device=device,
54
+ texture_format=texture_format,
55
+ depth_format=depth_format,
56
+ msaa_sample_count=msaa_sample_count,
57
+ data_type=data_type,
58
+ stride=stride,
59
+ )
60
+
61
+ def get_dtype(self) -> np.dtype:
62
+ """Get the data type of the pipeline."""
63
+ return np.dtype([
64
+ ("MVP", "float32", (4, 4)),
65
+ ("ViewMatrix", "float32", (4, 4)),
66
+ ("size", "float32"),
67
+ ("padding", np.uint32, 3),
68
+ ])
69
+
70
+ def _get_shader_code(self) -> str:
71
+ """Get the WGSL shader code for this pipeline."""
72
+ return POINT_SHADER_MULTI_COLOURED
73
+
74
+ def _get_vertex_buffer_layouts(self):
75
+ """Get vertex buffer layout configurations for the pipeline."""
76
+ return self._get_default_vertex_layouts(has_colour_buffer=True)
77
+
78
+ def _set_default_uniforms(self) -> None:
79
+ """Set default values for uniform data."""
80
+ self.uniform_data["size"] = 1.0 # Default point size
81
+ self.uniform_data["ViewMatrix"] = np.eye(4, dtype=np.float32)
82
+
83
+ def _get_pipeline_label(self) -> str:
84
+ """Get the label for the pipeline."""
85
+ return "point_pipeline_multi_coloured"
86
+
87
+ def set_data(self, positions, colours=None) -> None:
88
+ """
89
+ Set the point data for rendering.
90
+
91
+ Args:
92
+ positions: Nx2 array of point positions or a pre-existing GPUBuffer.
93
+ colours: Nx3 array of point colours (RGB) or a pre-existing GPUBuffer.
94
+ If None, uses white.
95
+ """
96
+ # Handle positions
97
+ if isinstance(positions, wgpu.GPUBuffer):
98
+ self.position_buffer = positions
99
+ self.num_points = positions.size // self._stride
100
+ else: # numpy array
101
+ self.num_points = len(positions)
102
+ self.position_buffer, _ = self._create_or_update_buffer(
103
+ self.position_buffer,
104
+ positions,
105
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
106
+ "point_pipeline_multi_coloured_position_buffer",
107
+ )
108
+
109
+ # Handle colours
110
+ if colours is None:
111
+ # Create default white colours
112
+ default_colours = np.ones((self.num_points, 3), dtype=np.float32)
113
+ colour_result = self._process_vertex_data(
114
+ None,
115
+ default_colours,
116
+ padding_size=4, # Pad to vec4 for alignment
117
+ buffer_label="point_pipeline_multi_coloured_colour_buffer",
118
+ )
119
+ if isinstance(colour_result, wgpu.GPUBuffer):
120
+ self.colour_buffer = colour_result
121
+ elif colour_result:
122
+ self.colour_buffer = colour_result[0]
123
+ else:
124
+ self.colour_buffer = None
125
+ else:
126
+ colour_result = self._process_vertex_data(
127
+ colours,
128
+ None,
129
+ padding_size=4, # Pad to vec4 for alignment
130
+ buffer_label="point_pipeline_multi_coloured_colour_buffer",
131
+ )
132
+ if isinstance(colour_result, wgpu.GPUBuffer):
133
+ self.colour_buffer = colour_result
134
+ elif colour_result:
135
+ self.colour_buffer = colour_result[0]
136
+ else:
137
+ self.colour_buffer = None
138
+
139
+ def update_uniforms(self, **kwargs) -> None:
140
+ """
141
+ Update uniform buffer values.
142
+
143
+ Args:
144
+ **kwargs: Pipeline-specific uniform parameters
145
+ - mvp: 4x4 model view projection matrix
146
+ - view_matrix: 4x4 view matrix for billboarding calculations
147
+ - point_size: Size of points in world units
148
+ """
149
+
150
+ if "mvp" in kwargs and kwargs["mvp"] is not None:
151
+ self.uniform_data["MVP"] = kwargs["mvp"]
152
+
153
+ if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
154
+ self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]
155
+
156
+ if "point_size" in kwargs and kwargs["point_size"] is not None:
157
+ self.uniform_data["size"] = kwargs["point_size"]
158
+
159
+ self.device.queue.write_buffer(self.uniform_buffer, 0, self.uniform_data.tobytes())
160
+
161
+ def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs) -> None:
162
+ """
163
+ Render the points.
164
+
165
+ Args:
166
+ render_pass: Active render pass encoder
167
+ **kwargs: Pipeline-specific render parameters
168
+ - num_points: Number of points to render (defaults to all)
169
+ """
170
+ num_points = kwargs.get("num_points", None)
171
+
172
+ if self.position_buffer is None or self.colour_buffer is None:
173
+ return
174
+
175
+ count = num_points if num_points is not None else self.num_points
176
+
177
+ render_pass.set_pipeline(self.pipeline)
178
+ if self.bind_group:
179
+ render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
180
+ render_pass.set_vertex_buffer(0, self.position_buffer)
181
+ render_pass.set_vertex_buffer(1, self.colour_buffer)
182
+ render_pass.draw(4, count) # 4 vertices per quad, instanced
183
+
184
+ def cleanup(self) -> None:
185
+ """Release resources."""
186
+ if self.position_buffer:
187
+ self.position_buffer.destroy()
188
+ if self.colour_buffer:
189
+ self.colour_buffer.destroy()
190
+ super().cleanup()
191
+
192
+
193
+ class PointPipelineSingleColour(BasePointPipeline):
194
+ """
195
+ A reusable pipeline for rendering points in WebGPU.
196
+
197
+ Features:
198
+ - Instanced rendering of points as quads
199
+ - Single colour for all points
200
+ - Configurable point size
201
+ - Model, View Projection matrix support pass a projection only for 2D
202
+ - MSAA support
203
+ """
204
+
205
+ def __init__(
206
+ self,
207
+ device: wgpu.GPUDevice,
208
+ data_type: str = "Vec3",
209
+ texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
210
+ depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
211
+ msaa_sample_count: int = 4,
212
+ stride: int = 0,
213
+ ):
214
+ """
215
+ Initialize the point rendering pipeline.
216
+
217
+ Args:
218
+ device: WebGPU device
219
+ texture_format: colour attachment format
220
+ depth_format: Depth attachment format
221
+ msaa_sample_count: Number of MSAA samples
222
+ stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
223
+ """
224
+ # Pipeline-specific buffer tracking
225
+ self.position_buffer: Optional[wgpu.GPUBuffer] = None
226
+ self.num_points: int = 0
227
+
228
+ super().__init__(
229
+ device=device,
230
+ texture_format=texture_format,
231
+ depth_format=depth_format,
232
+ msaa_sample_count=msaa_sample_count,
233
+ data_type=data_type,
234
+ stride=stride,
235
+ )
236
+
237
+ def get_dtype(self) -> np.dtype:
238
+ """Get the data type of the pipeline."""
239
+ return np.dtype([
240
+ ("MVP", "float32", (4, 4)),
241
+ ("ViewMatrix", "float32", (4, 4)),
242
+ ("ColourSize", "float32", 4),
243
+ ])
244
+
245
+ def _get_shader_code(self) -> str:
246
+ """Get the WGSL shader code for this pipeline."""
247
+ return POINT_SHADER_SINGLE_COLOUR
248
+
249
+ def _get_vertex_buffer_layouts(self):
250
+ """Get vertex buffer layout configurations for the pipeline."""
251
+ return self._get_default_vertex_layouts(has_colour_buffer=False)
252
+
253
+ def _set_default_uniforms(self) -> None:
254
+ """Set default values for uniform data."""
255
+ self.uniform_data["ColourSize"] = np.array(
256
+ [1.0, 1.0, 1.0, 1.0], dtype=np.float32
257
+ ) # Default White with point size 1
258
+ self.uniform_data["ViewMatrix"] = np.eye(4, dtype=np.float32)
259
+
260
+ def _get_pipeline_label(self) -> str:
261
+ """Get the label for the pipeline."""
262
+ return "point_pipeline_single_colour"
263
+
264
+ def set_data(self, positions, colours=None) -> None:
265
+ """
266
+ Set the point data for rendering.
267
+
268
+ Args:
269
+ positions: Nx2 array of point positions or a pre-existing GPUBuffer.
270
+ colours: Ignored for single colour pipeline
271
+ """
272
+ # Handle positions
273
+ if isinstance(positions, wgpu.GPUBuffer):
274
+ self.position_buffer = positions
275
+ self.num_points = positions.size // self._stride
276
+ else: # numpy array
277
+ self.num_points = len(positions)
278
+ self.position_buffer, _ = self._create_or_update_buffer(
279
+ self.position_buffer,
280
+ positions,
281
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
282
+ "point_pipeline_single_colour_position_buffer",
283
+ )
284
+
285
+ def update_uniforms(self, **kwargs) -> None:
286
+ """
287
+ Update uniform buffer values.
288
+
289
+ Args:
290
+ **kwargs: Pipeline-specific uniform parameters
291
+ - mvp: 4x4 model view projection matrix
292
+ - view_matrix: 4x4 view matrix for billboarding calculations
293
+ - colour: 3-element array of RGB colour values
294
+ - point_size: Size of points in world units
295
+ """
296
+ if "mvp" in kwargs and kwargs["mvp"] is not None:
297
+ self.uniform_data["MVP"] = kwargs["mvp"]
298
+
299
+ if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
300
+ self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]
301
+
302
+ if "colour" in kwargs and kwargs["colour"] is not None:
303
+ self.uniform_data["ColourSize"][:3] = kwargs["colour"]
304
+
305
+ if "point_size" in kwargs and kwargs["point_size"] is not None:
306
+ self.uniform_data["ColourSize"][3] = kwargs["point_size"]
307
+
308
+ self.device.queue.write_buffer(self.uniform_buffer, 0, self.uniform_data.tobytes())
309
+
310
+ def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs) -> None:
311
+ """
312
+ Render the points.
313
+
314
+ Args:
315
+ render_pass: Active render pass encoder
316
+ **kwargs: Pipeline-specific render parameters
317
+ - num_points: Number of points to render (defaults to all)
318
+ """
319
+ num_points = kwargs.get("num_points", None)
320
+
321
+ if self.position_buffer is None:
322
+ return
323
+
324
+ count = num_points if num_points is not None else self.num_points
325
+
326
+ render_pass.set_pipeline(self.pipeline)
327
+ if self.bind_group:
328
+ render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
329
+ render_pass.set_vertex_buffer(0, self.position_buffer)
330
+ render_pass.draw(4, count) # 4 vertices per quad, instanced
331
+
332
+ def cleanup(self) -> None:
333
+ """Release resources."""
334
+ if self.position_buffer:
335
+ self.position_buffer.destroy()
336
+ super().cleanup()