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.
- ncca/ngl/PrimData/pack_arrays.py +2 -3
- ncca/ngl/__init__.py +3 -4
- ncca/ngl/base_mesh.py +28 -20
- ncca/ngl/image.py +1 -3
- ncca/ngl/mat2.py +79 -53
- ncca/ngl/mat3.py +104 -185
- ncca/ngl/mat4.py +144 -309
- ncca/ngl/prim_data.py +42 -36
- ncca/ngl/primitives.py +2 -2
- ncca/ngl/pyside_event_handling_mixin.py +0 -108
- ncca/ngl/quaternion.py +69 -36
- ncca/ngl/shader.py +0 -116
- ncca/ngl/shader_program.py +94 -117
- ncca/ngl/texture.py +5 -2
- ncca/ngl/util.py +7 -0
- ncca/ngl/vec2.py +58 -292
- ncca/ngl/vec2_array.py +79 -28
- ncca/ngl/vec3.py +59 -340
- ncca/ngl/vec3_array.py +76 -23
- ncca/ngl/vec4.py +90 -190
- ncca/ngl/vec4_array.py +78 -27
- ncca/ngl/vector_base.py +542 -0
- ncca/ngl/webgpu/__init__.py +20 -0
- ncca/ngl/webgpu/__main__.py +640 -0
- ncca/ngl/webgpu/__main__.py.backup +640 -0
- ncca/ngl/webgpu/base_webgpu_pipeline.py +354 -0
- ncca/ngl/webgpu/custom_shader_pipeline.py +288 -0
- ncca/ngl/webgpu/instanced_geometry_pipeline.py +594 -0
- ncca/ngl/webgpu/line_pipeline.py +405 -0
- ncca/ngl/webgpu/pipeline_factory.py +190 -0
- ncca/ngl/webgpu/pipeline_shaders.py +497 -0
- ncca/ngl/webgpu/point_list_pipeline.py +349 -0
- ncca/ngl/webgpu/point_pipeline.py +336 -0
- ncca/ngl/webgpu/triangle_pipeline.py +419 -0
- ncca/ngl/webgpu/webgpu_constants.py +31 -0
- ncca/ngl/webgpu/webgpu_widget.py +322 -0
- ncca/ngl/webgpu/wip/REFACTORING_SUMMARY.md +82 -0
- ncca/ngl/webgpu/wip/UNIFIED_SYSTEM.md +314 -0
- ncca/ngl/webgpu/wip/buffer_manager.py +396 -0
- ncca/ngl/webgpu/wip/pipeline_config.py +463 -0
- ncca/ngl/webgpu/wip/shader_constants.py +328 -0
- ncca/ngl/webgpu/wip/shader_templates.py +563 -0
- ncca/ngl/webgpu/wip/unified_examples.py +390 -0
- ncca/ngl/webgpu/wip/unified_factory.py +449 -0
- ncca/ngl/webgpu/wip/unified_pipeline.py +469 -0
- ncca/ngl/widgets/__init__.py +18 -2
- ncca/ngl/widgets/__main__.py +2 -1
- ncca/ngl/widgets/lookatwidget.py +2 -1
- ncca/ngl/widgets/mat4widget.py +2 -2
- ncca/ngl/widgets/vec2widget.py +1 -1
- ncca/ngl/widgets/vec3widget.py +1 -0
- {ncca_ngl-0.3.4.dist-info → ncca_ngl-0.5.0.dist-info}/METADATA +3 -2
- ncca_ngl-0.5.0.dist-info/RECORD +105 -0
- ncca/ngl/widgets/transformation_widget.py +0 -299
- ncca_ngl-0.3.4.dist-info/RECORD +0 -82
- {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)
|