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