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,354 @@
1
+ """
2
+ Abstract base classes for WebGPU rendering pipelines.
3
+ Provides common functionality for buffer management, pipeline creation, and rendering.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Dict, List, Optional, Tuple, Union
8
+
9
+ import numpy as np
10
+ import wgpu
11
+
12
+ from .webgpu_constants import NGLToWebGPU
13
+
14
+
15
+ class BaseWebGPUPipeline(ABC):
16
+ """
17
+ Abstract base class for all WebGPU rendering pipelines.
18
+
19
+ Provides common functionality for:
20
+ - Buffer management and creation
21
+ - Pipeline configuration
22
+ - Uniform buffer handling
23
+ - Resource cleanup
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ device: wgpu.GPUDevice,
29
+ texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
30
+ depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
31
+ msaa_sample_count: int = 4,
32
+ data_type: str = "Vec3",
33
+ stride: int = 0,
34
+ ):
35
+ """
36
+ Initialize base pipeline.
37
+
38
+ Args:
39
+ device: WebGPU device
40
+ texture_format: Colour attachment format
41
+ depth_format: Depth attachment format
42
+ msaa_sample_count: Number of MSAA samples
43
+ data_type: Vertex data type (e.g., "Vec3", "Vec2")
44
+ stride: Vertex buffer stride. If 0, inferred from data_type
45
+ """
46
+ self.device = device
47
+ self.texture_format = texture_format
48
+ self.depth_format = depth_format
49
+ self.msaa_sample_count = msaa_sample_count
50
+ self._data_type = data_type
51
+
52
+ if stride != 0:
53
+ self._stride = stride
54
+ else:
55
+ self._stride = NGLToWebGPU.stride_from_type(self._data_type)
56
+
57
+ # Core pipeline resources
58
+ self.pipeline: Optional[wgpu.GPURenderPipeline] = None
59
+ self.uniform_buffer: Optional[wgpu.GPUBuffer] = None
60
+ self.bind_group: Optional[wgpu.GPUBindGroup] = None
61
+
62
+ # Initialize uniform data structure
63
+ self.uniform_data = np.zeros((), dtype=self.get_dtype())
64
+ self._set_default_uniforms()
65
+
66
+ # Create the pipeline
67
+ self._create_pipeline()
68
+
69
+ @abstractmethod
70
+ def get_dtype(self) -> np.dtype:
71
+ """Get the numpy dtype for the uniform buffer structure."""
72
+ pass
73
+
74
+ @abstractmethod
75
+ def _get_shader_code(self) -> str:
76
+ """Get the WGSL shader code for this pipeline."""
77
+ pass
78
+
79
+ @abstractmethod
80
+ def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
81
+ """Get vertex buffer layout configurations for the pipeline."""
82
+ pass
83
+
84
+ @abstractmethod
85
+ def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
86
+ """Get the primitive topology for the pipeline."""
87
+ pass
88
+
89
+ @abstractmethod
90
+ def _set_default_uniforms(self) -> None:
91
+ """Set default values for uniform data."""
92
+ pass
93
+
94
+ @abstractmethod
95
+ def _get_pipeline_label(self) -> str:
96
+ """Get the label for the pipeline."""
97
+ pass
98
+
99
+ def _create_pipeline(self) -> None:
100
+ """Create the render pipeline and associated resources."""
101
+ # Load shader
102
+ shader_module = self.device.create_shader_module(code=self._get_shader_code())
103
+
104
+ # Create render pipeline
105
+ self.pipeline = self.device.create_render_pipeline(
106
+ label=self._get_pipeline_label(),
107
+ layout="auto",
108
+ vertex={
109
+ "module": shader_module,
110
+ "entry_point": "vertex_main",
111
+ "buffers": self._get_vertex_buffer_layouts(),
112
+ },
113
+ fragment={
114
+ "module": shader_module,
115
+ "entry_point": "fragment_main",
116
+ "targets": [{"format": self.texture_format}],
117
+ },
118
+ primitive={"topology": self._get_primitive_topology()},
119
+ depth_stencil={
120
+ "format": self.depth_format,
121
+ "depth_write_enabled": True,
122
+ "depth_compare": wgpu.CompareFunction.less,
123
+ },
124
+ multisample={"count": self.msaa_sample_count},
125
+ )
126
+
127
+ # Create uniform buffer
128
+ self.uniform_buffer = self.device.create_buffer_with_data(
129
+ data=self.uniform_data.tobytes(),
130
+ usage=int(wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST),
131
+ label=f"{self._get_pipeline_label()}_uniform_buffer",
132
+ )
133
+
134
+ # Create bind group
135
+ bind_group_layout = self.pipeline.get_bind_group_layout(0)
136
+ self.bind_group = self.device.create_bind_group(
137
+ layout=bind_group_layout,
138
+ entries=[
139
+ {
140
+ "binding": 0,
141
+ "resource": {"buffer": self.uniform_buffer},
142
+ }
143
+ ],
144
+ )
145
+
146
+ def _create_or_update_buffer(
147
+ self,
148
+ current_buffer: Optional[wgpu.GPUBuffer],
149
+ data: Union[np.ndarray, wgpu.GPUBuffer],
150
+ usage: wgpu.BufferUsage,
151
+ buffer_label: str,
152
+ ) -> Tuple[Optional[wgpu.GPUBuffer], int]:
153
+ """
154
+ Create or update a GPU buffer with new data.
155
+
156
+ Args:
157
+ current_buffer: Existing buffer (may be None)
158
+ data: New data (numpy array or GPU buffer)
159
+ usage: Buffer usage flags
160
+ buffer_label: Label for the buffer
161
+
162
+ Returns:
163
+ Tuple of (buffer, data_size)
164
+ """
165
+ if isinstance(data, wgpu.GPUBuffer):
166
+ # Use provided buffer directly
167
+ return data, data.size
168
+
169
+ # Handle numpy array
170
+ data_bytes = data.astype(np.float32).tobytes()
171
+ data_size = len(data_bytes)
172
+
173
+ # Create new buffer if needed or existing one is too small
174
+ if current_buffer is None or current_buffer.size < data_size:
175
+ if current_buffer:
176
+ current_buffer.destroy()
177
+ buffer = self.device.create_buffer_with_data(
178
+ data=data_bytes,
179
+ usage=usage,
180
+ label=buffer_label,
181
+ )
182
+ return buffer, data_size
183
+ else:
184
+ # Update existing buffer
185
+ self.device.queue.write_buffer(current_buffer, 0, data_bytes)
186
+ return current_buffer, data_size
187
+
188
+ def _process_vertex_data(
189
+ self,
190
+ data: Optional[Union[np.ndarray, wgpu.GPUBuffer]],
191
+ default_value: Optional[np.ndarray] = None,
192
+ padding_size: Optional[int] = None,
193
+ buffer_label: str = "vertex_buffer",
194
+ ) -> Optional[Union[wgpu.GPUBuffer, Tuple[wgpu.GPUBuffer, int]]]:
195
+ """
196
+ Process vertex data, handling numpy arrays, GPU buffers, and defaults.
197
+
198
+ Args:
199
+ data: Input data (numpy array, GPU buffer, or None)
200
+ default_value: Default value if data is None
201
+ padding_size: Size to pad arrays to (for alignment)
202
+ buffer_label: Label for created buffers
203
+
204
+ Returns:
205
+ Processed buffer(s) or None
206
+ """
207
+ if data is None and default_value is not None:
208
+ data = default_value
209
+
210
+ if data is None:
211
+ return None
212
+
213
+ if isinstance(data, wgpu.GPUBuffer):
214
+ return data
215
+
216
+ # Handle numpy array
217
+ if padding_size:
218
+ # Pad array to specified size
219
+ if data.ndim == 1:
220
+ padded_data = np.zeros(padding_size, dtype=np.float32)
221
+ padded_data[: len(data)] = data.astype(np.float32)
222
+ else:
223
+ padded_data = np.zeros((data.shape[0], padding_size), dtype=np.float32)
224
+ padded_data[:, : data.shape[1]] = data.astype(np.float32)
225
+ data = padded_data
226
+
227
+ buffer, _ = self._create_or_update_buffer(
228
+ None, # Always create new for processed data
229
+ data,
230
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
231
+ buffer_label,
232
+ )
233
+ return buffer
234
+
235
+ @abstractmethod
236
+ def set_data(self, **kwargs) -> None:
237
+ """
238
+ Set rendering data (vertices, colours, etc.).
239
+
240
+ Args:
241
+ **kwargs: Pipeline-specific data parameters
242
+ """
243
+ pass
244
+
245
+ @abstractmethod
246
+ def update_uniforms(self, **kwargs) -> None:
247
+ """
248
+ Update uniform buffer values.
249
+
250
+ Args:
251
+ **kwargs: Pipeline-specific uniform parameters
252
+ """
253
+ pass
254
+
255
+ @abstractmethod
256
+ def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs) -> None:
257
+ """
258
+ Render using this pipeline.
259
+
260
+ Args:
261
+ render_pass: Active render pass encoder
262
+ **kwargs: Pipeline-specific render parameters
263
+ """
264
+ pass
265
+
266
+ def cleanup(self) -> None:
267
+ """Release pipeline resources. Can be overridden for additional cleanup."""
268
+ if self.uniform_buffer:
269
+ self.uniform_buffer.destroy()
270
+
271
+
272
+ class BasePointPipeline(BaseWebGPUPipeline):
273
+ """
274
+ Base class for point rendering pipelines.
275
+
276
+ Provides common functionality for:
277
+ - Point billboarding
278
+ - Quad generation
279
+ - Circle clipping in fragment shader
280
+ """
281
+
282
+ def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
283
+ """Points are rendered as triangle strips for quad generation."""
284
+ return wgpu.PrimitiveTopology.triangle_strip
285
+
286
+ def _get_default_vertex_layouts(self, has_colour_buffer: bool = False) -> List[Dict[str, Any]]:
287
+ """
288
+ Get default vertex buffer layouts for point rendering.
289
+
290
+ Args:
291
+ has_colour_buffer: Whether to include colour buffer layout
292
+
293
+ Returns:
294
+ List of vertex buffer layout configurations
295
+ """
296
+ layouts = [
297
+ {
298
+ "array_stride": self._stride,
299
+ "step_mode": "instance",
300
+ "attributes": [
301
+ {
302
+ "format": NGLToWebGPU.vertex_format(self._data_type),
303
+ "offset": 0,
304
+ "shader_location": 0,
305
+ },
306
+ ],
307
+ },
308
+ ]
309
+
310
+ if has_colour_buffer:
311
+ layouts.append({
312
+ "array_stride": NGLToWebGPU.stride_from_type("Vec3"),
313
+ "step_mode": "instance",
314
+ "attributes": [
315
+ {
316
+ "format": NGLToWebGPU.vertex_format("Vec3"),
317
+ "offset": 0,
318
+ "shader_location": 1,
319
+ },
320
+ ],
321
+ })
322
+
323
+ return layouts
324
+
325
+ def _render_points(
326
+ self,
327
+ render_pass: wgpu.GPURenderPassEncoder,
328
+ position_buffer: wgpu.GPUBuffer,
329
+ colour_buffer: Optional[wgpu.GPUBuffer] = None,
330
+ num_points: Optional[int] = None,
331
+ ) -> None:
332
+ """
333
+ Common point rendering implementation.
334
+
335
+ Args:
336
+ render_pass: Active render pass encoder
337
+ position_buffer: Buffer containing point positions
338
+ colour_buffer: Optional buffer containing point colours
339
+ num_points: Number of points to render
340
+ """
341
+ if position_buffer is None:
342
+ return
343
+
344
+ count = num_points if num_points is not None else getattr(self, "num_points", 0)
345
+
346
+ render_pass.set_pipeline(self.pipeline)
347
+ render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
348
+ render_pass.set_vertex_buffer(0, position_buffer)
349
+
350
+ if colour_buffer:
351
+ render_pass.set_vertex_buffer(1, colour_buffer)
352
+
353
+ # 4 vertices per quad for point rendering
354
+ render_pass.draw(4, count)
@@ -0,0 +1,288 @@
1
+ """
2
+ Custom WebGPU pipeline that accepts user-provided shader source files.
3
+ Provides flexibility for users to write their own WGSL shaders.
4
+ """
5
+
6
+ import os
7
+ from typing import Dict, List, Optional, Union
8
+
9
+ import numpy as np
10
+ import wgpu
11
+
12
+ from .base_webgpu_pipeline import BaseWebGPUPipeline
13
+ from .webgpu_constants import NGLToWebGPU
14
+
15
+
16
+ class CustomShaderPipeline(BaseWebGPUPipeline):
17
+ """
18
+ A WebGPU pipeline that uses custom shader source provided by the user.
19
+
20
+ This pipeline allows users to provide their own WGSL shader source code
21
+ while handling the boilerplate for buffer management, uniform updates,
22
+ and rendering setup.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ device: wgpu.GPUDevice,
28
+ shader_source: str,
29
+ vertex_formats: Optional[List[Union[str, wgpu.VertexFormat]]] = None,
30
+ primitive_topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.triangle_list,
31
+ texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
32
+ depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
33
+ msaa_sample_count: int = 4,
34
+ uniform_struct_definition: Optional[str] = None,
35
+ pipeline_label: str = "CustomShaderPipeline",
36
+ ):
37
+ """
38
+ Initialize custom shader pipeline.
39
+
40
+ Args:
41
+ device: WebGPU device
42
+ shader_source: WGSL shader source code as string
43
+ vertex_formats: List of vertex data formats (e.g., ["Vec3", "Vec3"] for position+colour)
44
+ primitive_topology: Primitive topology for rendering
45
+ texture_format: Colour attachment format
46
+ depth_format: Depth attachment format
47
+ msaa_sample_count: Number of MSAA samples
48
+ uniform_struct_definition: Custom uniform struct definition (optional)
49
+ pipeline_label: Label for debugging
50
+ """
51
+ self._shader_source = shader_source
52
+ self._vertex_formats = vertex_formats or ["Vec3"]
53
+ self._primitive_topology = primitive_topology
54
+ self._uniform_struct_definition = uniform_struct_definition
55
+ self._pipeline_label = pipeline_label
56
+
57
+ # Calculate stride from vertex formats
58
+ self._stride = sum(
59
+ NGLToWebGPU.stride_from_type(fmt) for fmt in self._vertex_formats
60
+ )
61
+
62
+ # Initialize buffers storage
63
+ self.vertex_buffers: Dict[int, wgpu.GPUBuffer] = {}
64
+ self.num_vertices = 0
65
+
66
+ # Call parent constructor after setting up our attributes
67
+ super().__init__(
68
+ device=device,
69
+ texture_format=texture_format,
70
+ depth_format=depth_format,
71
+ msaa_sample_count=msaa_sample_count,
72
+ data_type=self._vertex_formats[0], # Use first format as default
73
+ stride=self._stride,
74
+ )
75
+
76
+ def get_dtype(self) -> np.dtype:
77
+ """Get the numpy dtype for the uniform buffer structure."""
78
+ if self._uniform_struct_definition:
79
+ # For custom uniforms, we need to parse the struct or use a default
80
+ # For now, use a basic MVP + colour structure
81
+ return np.dtype(
82
+ [
83
+ ("MVP", np.float32, (4, 4)),
84
+ ("colour", np.float32, 4),
85
+ ]
86
+ )
87
+ else:
88
+ # Default uniform structure
89
+ return np.dtype(
90
+ [
91
+ ("MVP", np.float32, (4, 4)),
92
+ ("colour", np.float32, 4),
93
+ ]
94
+ )
95
+
96
+ def _get_shader_code(self) -> str:
97
+ """Return the custom shader source."""
98
+ return self._shader_source
99
+
100
+ def _get_vertex_buffer_layouts(self) -> List[Dict[str, any]]:
101
+ """Get vertex buffer layouts based on provided vertex formats."""
102
+ if len(self._vertex_formats) == 1:
103
+ # Single interleaved buffer
104
+ return [
105
+ {
106
+ "array_stride": self._stride,
107
+ "step_mode": "vertex",
108
+ "attributes": [
109
+ {
110
+ "format": NGLToWebGPU.vertex_format(
111
+ self._vertex_formats[0]
112
+ ),
113
+ "offset": 0,
114
+ "shader_location": 0,
115
+ },
116
+ ],
117
+ },
118
+ ]
119
+ else:
120
+ # Multiple separate buffers or interleaved with multiple attributes
121
+ layouts = []
122
+ current_offset = 0
123
+
124
+ for i, fmt in enumerate(self._vertex_formats):
125
+ stride = NGLToWebGPU.stride_from_type(fmt)
126
+ layouts.append(
127
+ {
128
+ "array_stride": stride,
129
+ "step_mode": "vertex",
130
+ "attributes": [
131
+ {
132
+ "format": NGLToWebGPU.vertex_format(fmt),
133
+ "offset": 0,
134
+ "shader_location": i,
135
+ },
136
+ ],
137
+ }
138
+ )
139
+ current_offset += stride
140
+
141
+ return layouts
142
+
143
+ def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
144
+ """Return the primitive topology."""
145
+ return self._primitive_topology
146
+
147
+ def _set_default_uniforms(self) -> None:
148
+ """Set default uniform values."""
149
+ self.uniform_data["MVP"] = np.eye(4, dtype=np.float32)
150
+ self.uniform_data["colour"] = np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32)
151
+
152
+ def _get_pipeline_label(self) -> str:
153
+ """Return the pipeline label."""
154
+ return self._pipeline_label
155
+
156
+ def set_data(
157
+ self,
158
+ positions: Optional[np.ndarray] = None,
159
+ colours: Optional[np.ndarray] = None,
160
+ interleaved_data: Optional[np.ndarray] = None,
161
+ **kwargs,
162
+ ) -> None:
163
+ """
164
+ Set vertex data for rendering.
165
+
166
+ Args:
167
+ positions: Vertex position data (N, 3)
168
+ colours: Vertex colour data (N, 3) or (N, 4)
169
+ interleaved_data: Pre-interleaved vertex data
170
+ **kwargs: Additional vertex data arrays (e.g., velocities, life, initial_position)
171
+ """
172
+ if interleaved_data is not None:
173
+ # Use pre-interleaved data
174
+ buffer = self._create_or_update_buffer(
175
+ self.vertex_buffers.get(0),
176
+ interleaved_data,
177
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
178
+ f"{self._pipeline_label}_vertex_buffer_0",
179
+ )
180
+ self.vertex_buffers[0] = buffer[0]
181
+ self.num_vertices = len(interleaved_data)
182
+ else:
183
+ # Handle separate vertex data arrays
184
+ binding = 0
185
+
186
+ if positions is not None:
187
+ buffer = self._create_or_update_buffer(
188
+ self.vertex_buffers.get(binding),
189
+ positions,
190
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
191
+ f"{self._pipeline_label}_vertex_buffer_{binding}",
192
+ )
193
+ self.vertex_buffers[binding] = buffer[0]
194
+ self.num_vertices = len(positions)
195
+ binding += 1
196
+
197
+ if colours is not None:
198
+ buffer = self._create_or_update_buffer(
199
+ self.vertex_buffers.get(binding),
200
+ colours,
201
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
202
+ f"{self._pipeline_label}_vertex_buffer_{binding}",
203
+ )
204
+ self.vertex_buffers[binding] = buffer[0]
205
+ binding += 1
206
+
207
+ # Handle additional vertex attributes (velocities, life, initial_position, etc.)
208
+ for attr_name, attr_data in kwargs.items():
209
+ if attr_data is not None:
210
+ buffer = self._create_or_update_buffer(
211
+ self.vertex_buffers.get(binding),
212
+ attr_data,
213
+ wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
214
+ f"{self._pipeline_label}_vertex_buffer_{binding}",
215
+ )
216
+ self.vertex_buffers[binding] = buffer[0]
217
+ binding += 1
218
+
219
+ def update_uniforms(self, **kwargs) -> None:
220
+ """
221
+ Update uniform buffer values.
222
+
223
+ Args:
224
+ **kwargs: Uniform values to update (e.g., mvp=matrix, colour=array)
225
+ """
226
+ if "mvp" in kwargs:
227
+ self.uniform_data["MVP"] = kwargs["mvp"]
228
+
229
+ if "colour" in kwargs:
230
+ colour = kwargs["colour"]
231
+ if len(colour) == 3:
232
+ self.uniform_data["colour"] = np.array([*colour, 1.0], dtype=np.float32)
233
+ else:
234
+ self.uniform_data["colour"] = np.array(colour, dtype=np.float32)
235
+
236
+ # Update the GPU buffer
237
+ if self.uniform_buffer:
238
+ self.device.queue.write_buffer(
239
+ self.uniform_buffer, 0, self.uniform_data.tobytes()
240
+ )
241
+
242
+ def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs) -> None:
243
+ """
244
+ Render using this pipeline.
245
+
246
+ Args:
247
+ render_pass: Active render pass encoder
248
+ **kwargs: Additional render parameters
249
+ """
250
+ if self.pipeline is None or self.num_vertices == 0:
251
+ return
252
+
253
+ render_pass.set_pipeline(self.pipeline)
254
+ render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
255
+
256
+ # Set vertex buffers
257
+ for binding, buffer in self.vertex_buffers.items():
258
+ render_pass.set_vertex_buffer(binding, buffer)
259
+
260
+ # Draw
261
+ render_pass.draw(self.num_vertices)
262
+
263
+ @classmethod
264
+ def from_file(
265
+ cls, device: wgpu.GPUDevice, shader_file: str, **kwargs
266
+ ) -> "CustomShaderPipeline":
267
+ """
268
+ Create a CustomShaderPipeline from a WGSL file.
269
+
270
+ Args:
271
+ device: WebGPU device
272
+ shader_file: Path to WGSL shader file
273
+ **kwargs: Additional arguments to pass to constructor
274
+
275
+ Returns:
276
+ CustomShaderPipeline instance
277
+ """
278
+ if not os.path.exists(shader_file):
279
+ raise FileNotFoundError(f"Shader file not found: {shader_file}")
280
+
281
+ with open(shader_file, "r", encoding="utf-8") as f:
282
+ shader_source = f.read()
283
+
284
+ # Use filename as default pipeline label if not provided
285
+ if "pipeline_label" not in kwargs:
286
+ kwargs["pipeline_label"] = f"Custom_{os.path.basename(shader_file)}"
287
+
288
+ return cls(device, shader_source, **kwargs)