ngsolve-webgpu 0.0.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.
- ngsolve_webgpu/__init__.py +0 -0
- ngsolve_webgpu/_version.py +21 -0
- ngsolve_webgpu/animate.py +76 -0
- ngsolve_webgpu/cf.py +369 -0
- ngsolve_webgpu/clipping.py +155 -0
- ngsolve_webgpu/geometry.py +376 -0
- ngsolve_webgpu/isosurface.py +101 -0
- ngsolve_webgpu/jupyter.py +168 -0
- ngsolve_webgpu/lic.py +115 -0
- ngsolve_webgpu/mesh.py +424 -0
- ngsolve_webgpu/shaders/clipping/common.wgsl +120 -0
- ngsolve_webgpu/shaders/clipping/compute.wgsl +30 -0
- ngsolve_webgpu/shaders/clipping/render.wgsl +37 -0
- ngsolve_webgpu/shaders/compute.wgsl +59 -0
- ngsolve_webgpu/shaders/elements3d.wgsl +83 -0
- ngsolve_webgpu/shaders/eval/common.wgsl +14 -0
- ngsolve_webgpu/shaders/eval/seg.wgsl +30 -0
- ngsolve_webgpu/shaders/eval/tet.wgsl +61 -0
- ngsolve_webgpu/shaders/eval/trig.wgsl +121 -0
- ngsolve_webgpu/shaders/eval.wgsl +0 -0
- ngsolve_webgpu/shaders/geo_edge.wgsl +55 -0
- ngsolve_webgpu/shaders/geo_face.wgsl +47 -0
- ngsolve_webgpu/shaders/geo_vertex.wgsl +71 -0
- ngsolve_webgpu/shaders/isosurface/compute.wgsl +63 -0
- ngsolve_webgpu/shaders/isosurface/negative_clipping.wgsl +13 -0
- ngsolve_webgpu/shaders/isosurface/negative_surface.wgsl +15 -0
- ngsolve_webgpu/shaders/isosurface/render.wgsl +50 -0
- ngsolve_webgpu/shaders/line_integral_convolution.wgsl +100 -0
- ngsolve_webgpu/shaders/mesh.wgsl +296 -0
- ngsolve_webgpu/shaders/numbers.wgsl +40 -0
- ngsolve_webgpu/shaders/shader.wgsl +111 -0
- ngsolve_webgpu/shaders/uniforms.wgsl +93 -0
- ngsolve_webgpu-0.0.1.dist-info/METADATA +13 -0
- ngsolve_webgpu-0.0.1.dist-info/RECORD +37 -0
- ngsolve_webgpu-0.0.1.dist-info/WHEEL +5 -0
- ngsolve_webgpu-0.0.1.dist-info/licenses/LICENSE +504 -0
- ngsolve_webgpu-0.0.1.dist-info/top_level.txt +1 -0
ngsolve_webgpu/lic.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import js
|
|
4
|
+
|
|
5
|
+
from .gpu import WebGPU
|
|
6
|
+
from .mesh import RenderObject
|
|
7
|
+
from .uniforms import Binding, LineIntegralConvolutionUniforms
|
|
8
|
+
from .utils import ShaderStage, StorageTextureBinding, TextureBinding
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LineIntegralConvolutionRenderObject(RenderObject):
|
|
12
|
+
"""Line Integral Convolution (LIC) render object"""
|
|
13
|
+
|
|
14
|
+
texture_input: typing.Any
|
|
15
|
+
texture_output: typing.Any
|
|
16
|
+
uniforms: LineIntegralConvolutionUniforms
|
|
17
|
+
gpu: WebGPU
|
|
18
|
+
_buffers: dict = {}
|
|
19
|
+
|
|
20
|
+
def __init__(self, gpu, width, height):
|
|
21
|
+
self.gpu = gpu
|
|
22
|
+
self.uniforms = LineIntegralConvolutionUniforms(gpu.device)
|
|
23
|
+
self.uniforms.width = width
|
|
24
|
+
self.uniforms.height = height
|
|
25
|
+
self.uniforms.update_buffer()
|
|
26
|
+
self.texture_input = self.device.create_texture(
|
|
27
|
+
{
|
|
28
|
+
"label": "LIC input texture",
|
|
29
|
+
"size": [width, height],
|
|
30
|
+
"format": "rg32float",
|
|
31
|
+
"usage": js.GPUTextureUsage.TEXTURE_BINDING
|
|
32
|
+
| js.GPUTextureUsage.COPY_DST
|
|
33
|
+
| js.GPUTextureUsage.RENDER_ATTACHMENT,
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
self.texture_output = self.device.create_texture(
|
|
37
|
+
{
|
|
38
|
+
"label": "LIC output texture",
|
|
39
|
+
"size": [width, height],
|
|
40
|
+
"format": "r32float",
|
|
41
|
+
"usage": js.GPUTextureUsage.STORAGE_BINDING,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self._create_pipelines()
|
|
46
|
+
|
|
47
|
+
def _create_evaluate_pipeline(self):
|
|
48
|
+
device = self.device
|
|
49
|
+
layout, self.bind_group = device.create_bind_group(
|
|
50
|
+
self.get_bindings(), "LIC evaluate"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self.evaluate_pipeline = device.create_compute_pipeline(
|
|
54
|
+
layout,
|
|
55
|
+
{
|
|
56
|
+
"label": "create_test_mesh",
|
|
57
|
+
"layout": device.create_pipeline_layout(layout, "LIC evaluate"),
|
|
58
|
+
"compute": {
|
|
59
|
+
"module": device.shader_module,
|
|
60
|
+
"entryPoint": "evaluateLineIntegralConvolution",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _create_compute_pipeline(self):
|
|
66
|
+
device = self.device
|
|
67
|
+
layout, self.bind_group = device.create_bind_group(
|
|
68
|
+
self.get_bindings(), "LIC compute"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self.compute_pipeline = device.create_compute_pipeline(
|
|
72
|
+
layout,
|
|
73
|
+
{
|
|
74
|
+
"label": "create_test_mesh",
|
|
75
|
+
"layout": device.create_pipeline_layout(layout, "LIC compute"),
|
|
76
|
+
"compute": {
|
|
77
|
+
"module": device.shader_module,
|
|
78
|
+
"entryPoint": "computeLineIntegralConvolution",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def render(self, encoder):
|
|
84
|
+
pass_encoder = encoder.beginComputePass()
|
|
85
|
+
pass_encoder.setPipeline(self.compute_pipeline)
|
|
86
|
+
pass_encoder.setBindGroup(0, self.bind_group)
|
|
87
|
+
pass_encoder.dispatchWorkgroups(
|
|
88
|
+
(self.uniforms.width + 15 // 16), (self.uniforms.height + 15) // 16, 1
|
|
89
|
+
)
|
|
90
|
+
pass_encoder.end()
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def device(self):
|
|
94
|
+
return self.gpu.device
|
|
95
|
+
|
|
96
|
+
def get_bindings(self):
|
|
97
|
+
return (
|
|
98
|
+
self.uniforms.get_bindings()
|
|
99
|
+
+ self.gpu.get_bindings()
|
|
100
|
+
+ [
|
|
101
|
+
TextureBinding(
|
|
102
|
+
Binding.LINE_INTEGRAL_CONVOLUTION_INPUT_TEXTURE,
|
|
103
|
+
self.texture_input,
|
|
104
|
+
dim=2,
|
|
105
|
+
visibility=ShaderStage.COMPUTE,
|
|
106
|
+
sample_type="unfilterable-float",
|
|
107
|
+
),
|
|
108
|
+
StorageTextureBinding(
|
|
109
|
+
Binding.LINE_INTEGRAL_CONVOLUTION_OUTPUT_TEXTURE,
|
|
110
|
+
self.texture_output,
|
|
111
|
+
dim=2,
|
|
112
|
+
access="write-only",
|
|
113
|
+
),
|
|
114
|
+
]
|
|
115
|
+
)
|
ngsolve_webgpu/mesh.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import netgen.meshing
|
|
3
|
+
import numpy as np
|
|
4
|
+
from webgpu.font import Font
|
|
5
|
+
from webgpu.render_object import (
|
|
6
|
+
RenderObject,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# from webgpu.uniforms import Binding
|
|
10
|
+
from webgpu.uniforms import UniformBase, ct
|
|
11
|
+
from webgpu.utils import (
|
|
12
|
+
BufferBinding,
|
|
13
|
+
UniformBinding,
|
|
14
|
+
read_shader_file,
|
|
15
|
+
buffer_from_array,
|
|
16
|
+
uniform_from_array,
|
|
17
|
+
get_device,
|
|
18
|
+
)
|
|
19
|
+
from webgpu.webgpu_api import *
|
|
20
|
+
from webgpu.clipping import Clipping
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Binding:
|
|
24
|
+
"""Binding numbers for uniforms in shader code in uniforms.wgsl"""
|
|
25
|
+
|
|
26
|
+
EDGES = 8
|
|
27
|
+
TRIGS = 9
|
|
28
|
+
SEG_FUNCTION_VALUES = 11
|
|
29
|
+
VERTICES = 12
|
|
30
|
+
TRIGS_INDEX = 13
|
|
31
|
+
CURVATURE_VALUES_2D = 14
|
|
32
|
+
CURVATURE_SUBDIVISION = 15
|
|
33
|
+
DEFORMATION_VALUES = 16
|
|
34
|
+
DEFORMATION_SCALE = 17
|
|
35
|
+
|
|
36
|
+
MESH = 20
|
|
37
|
+
EDGE = 21
|
|
38
|
+
SEG = 22
|
|
39
|
+
TRIG = 23
|
|
40
|
+
QUAD = 24
|
|
41
|
+
TET = 25
|
|
42
|
+
PYRAMID = 26
|
|
43
|
+
PRISM = 27
|
|
44
|
+
HEX = 28
|
|
45
|
+
|
|
46
|
+
LINE_INTEGRAL_CONVOLUTION = 40
|
|
47
|
+
LINE_INTEGRAL_CONVOLUTION_INPUT_TEXTURE = 41
|
|
48
|
+
LINE_INTEGRAL_CONVOLUTION_OUTPUT_TEXTURE = 42
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _eltype:
|
|
52
|
+
dim: int
|
|
53
|
+
primitive_topology: PrimitiveTopology
|
|
54
|
+
num_vertices_per_primitive: int
|
|
55
|
+
|
|
56
|
+
def __init__(self, dim, primitive_topology, num_vertices_per_primitive):
|
|
57
|
+
self.dim = dim
|
|
58
|
+
self.primitive_topology = primitive_topology
|
|
59
|
+
self.num_vertices_per_primitive = num_vertices_per_primitive
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ElType(Enum):
|
|
63
|
+
POINT = _eltype(0, PrimitiveTopology.point_list, 1)
|
|
64
|
+
SEG = _eltype(1, PrimitiveTopology.line_list, 2)
|
|
65
|
+
TRIG = _eltype(2, PrimitiveTopology.triangle_list, 3)
|
|
66
|
+
QUAD = _eltype(2, PrimitiveTopology.triangle_list, 2 * 3)
|
|
67
|
+
TET = _eltype(3, PrimitiveTopology.triangle_list, 4 * 3)
|
|
68
|
+
HEX = _eltype(3, PrimitiveTopology.triangle_list, 6 * 2 * 3)
|
|
69
|
+
PRISM = _eltype(3, PrimitiveTopology.triangle_list, 2 * 3 + 3 * 2 * 3)
|
|
70
|
+
PYRAMID = _eltype(3, PrimitiveTopology.triangle_list, 4 + 2 * 3)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def from_dim_np(dim: int, np: int):
|
|
74
|
+
if dim == 2:
|
|
75
|
+
if np == 3:
|
|
76
|
+
return ElType.TRIG
|
|
77
|
+
if np == 4:
|
|
78
|
+
return ElType.QUAD
|
|
79
|
+
if dim == 3:
|
|
80
|
+
if np == 4:
|
|
81
|
+
return ElType.TET
|
|
82
|
+
if np == 8:
|
|
83
|
+
return ElType.HEX
|
|
84
|
+
if np == 6:
|
|
85
|
+
return ElType.PRISM
|
|
86
|
+
if np == 5:
|
|
87
|
+
return ElType.PYRAMID
|
|
88
|
+
raise ValueError(f"Unsupported element type dim={dim} np={np}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
ElTypes2D = [ElType.TRIG, ElType.QUAD]
|
|
92
|
+
ElTypes3D = [ElType.TET, ElType.HEX, ElType.PRISM, ElType.PYRAMID]
|
|
93
|
+
|
|
94
|
+
class MeshData:
|
|
95
|
+
# only for drawing the mesh, not needed for function values
|
|
96
|
+
num_elements: dict[str | ElType, int]
|
|
97
|
+
elements: dict[str | ElType, np.ndarray]
|
|
98
|
+
gpu_elements: dict[str | ElType, Buffer]
|
|
99
|
+
curvature_subdivision: int
|
|
100
|
+
|
|
101
|
+
mesh: netgen.meshing.Mesh
|
|
102
|
+
curvature_data = None
|
|
103
|
+
deformation_data = None
|
|
104
|
+
_ngs_mesh = None
|
|
105
|
+
_last_mesh_timestamp: int = -1
|
|
106
|
+
_timestamp = -1
|
|
107
|
+
|
|
108
|
+
def __init__(self, mesh):
|
|
109
|
+
self.on_region = False
|
|
110
|
+
self.need_3d = False
|
|
111
|
+
if isinstance(mesh, netgen.meshing.Mesh):
|
|
112
|
+
self.mesh = mesh
|
|
113
|
+
else:
|
|
114
|
+
self._ngs_mesh = mesh
|
|
115
|
+
import ngsolve as ngs
|
|
116
|
+
|
|
117
|
+
if isinstance(mesh, ngs.Region):
|
|
118
|
+
self.on_region = True
|
|
119
|
+
mesh = mesh.mesh
|
|
120
|
+
self.mesh = mesh.ngmesh
|
|
121
|
+
self.num_elements = {}
|
|
122
|
+
self.elements = {}
|
|
123
|
+
self.gpu_elements = {}
|
|
124
|
+
self.curvature_subdivision = 1
|
|
125
|
+
self._deformation_scale = 1
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def deformation_scale(self):
|
|
129
|
+
return self._deformation_scale
|
|
130
|
+
|
|
131
|
+
@deformation_scale.setter
|
|
132
|
+
def deformation_scale(self, value):
|
|
133
|
+
self._deformation_scale = value
|
|
134
|
+
if self.gpu_elements and "deformation_scale" in self.gpu_elements:
|
|
135
|
+
get_device().queue.writeBuffer(
|
|
136
|
+
self.gpu_elements["deformation_scale"],
|
|
137
|
+
0, np.array([self._deformation_scale], dtype=np.float32).tobytes())
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def ngs_mesh(self):
|
|
141
|
+
import ngsolve
|
|
142
|
+
|
|
143
|
+
if self._ngs_mesh is None:
|
|
144
|
+
self._ngs_mesh = ngsolve.Mesh(self.mesh)
|
|
145
|
+
return self._ngs_mesh
|
|
146
|
+
|
|
147
|
+
def update(self, timestamp):
|
|
148
|
+
if self._last_mesh_timestamp != self.mesh._timestamp:
|
|
149
|
+
self._create_data()
|
|
150
|
+
if timestamp == self._timestamp:
|
|
151
|
+
return
|
|
152
|
+
if "curvature_2d" in self.gpu_elements:
|
|
153
|
+
self.gpu_elements.pop("curvature_2d")
|
|
154
|
+
if "deformation_2d" in self.gpu_elements:
|
|
155
|
+
self.gpu_elements.pop("deformation_2d")
|
|
156
|
+
if self.curvature_data:
|
|
157
|
+
self.curvature_data.update(self.mesh._timestamp)
|
|
158
|
+
self.elements["curvature_2d"] = self.curvature_data.data_2d
|
|
159
|
+
else:
|
|
160
|
+
self.elements["curvature_2d"] = np.array([0], dtype=np.float32)
|
|
161
|
+
|
|
162
|
+
if self.deformation_data:
|
|
163
|
+
self.deformation_data.update(self.mesh._timestamp)
|
|
164
|
+
self.elements["deformation_2d"] = self.deformation_data.data_2d
|
|
165
|
+
else:
|
|
166
|
+
self.elements["deformation_2d"] = np.array([-1], dtype=np.float32)
|
|
167
|
+
self._timestamp = timestamp
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _create_data(self):
|
|
171
|
+
# TODO: implement other element types than triangles
|
|
172
|
+
# TODO: handle region correctly to draw only part of the mesh
|
|
173
|
+
mesh = self.mesh
|
|
174
|
+
self.num_elements = {eltype: 0 for eltype in ElType}
|
|
175
|
+
self.elements = {}
|
|
176
|
+
self.gpu_elements = {}
|
|
177
|
+
|
|
178
|
+
# Vertices
|
|
179
|
+
nv = len(mesh.Points())
|
|
180
|
+
self.num_elements["vertices"] = nv
|
|
181
|
+
vertices = np.array(mesh.Coordinates(), dtype=np.float32)
|
|
182
|
+
if vertices.shape[1] == 2:
|
|
183
|
+
vertices = np.hstack((vertices, np.zeros((nv, 1), dtype=np.float32)))
|
|
184
|
+
|
|
185
|
+
self.pmin = np.min(vertices, axis=0)
|
|
186
|
+
self.pmax = np.max(vertices, axis=0)
|
|
187
|
+
self.elements["vertices"] = vertices
|
|
188
|
+
|
|
189
|
+
# Trigs TODO: Quads
|
|
190
|
+
trigs = mesh.Elements2D().NumPy()
|
|
191
|
+
if self.on_region:
|
|
192
|
+
region = self.ngs_mesh
|
|
193
|
+
import ngsolve as ngs
|
|
194
|
+
|
|
195
|
+
if region.VB() == ngs.VOL and region.mesh.dim == 3:
|
|
196
|
+
region = region.Boundaries()
|
|
197
|
+
indices = np.flatnonzero(region.Mask()) + 1
|
|
198
|
+
trigs = trigs[np.isin(trigs["index"], indices)]
|
|
199
|
+
self.num_elements[ElType.TRIG] = len(trigs)
|
|
200
|
+
trigs_data = np.zeros((len(trigs), 4), dtype=np.uint32)
|
|
201
|
+
trigs_data[:, :3] = trigs["nodes"][:, :3] - 1
|
|
202
|
+
trigs_data[:, 3] = trigs["index"]
|
|
203
|
+
self.elements[ElType.TRIG] = trigs_data
|
|
204
|
+
|
|
205
|
+
# 3d Elements
|
|
206
|
+
if self.need_3d:
|
|
207
|
+
els = mesh.Elements3D().NumPy()
|
|
208
|
+
for num_pts in (4, 5, 6, 8):
|
|
209
|
+
eltype = ElType.from_dim_np(3, num_pts)
|
|
210
|
+
filtered = els[els["np"] == num_pts]
|
|
211
|
+
nels = len(filtered)
|
|
212
|
+
if nels == 0:
|
|
213
|
+
continue
|
|
214
|
+
u32array = np.empty((nels, num_pts + 2), dtype=np.uint32)
|
|
215
|
+
u32array[:, :num_pts] = filtered["nodes"][:, :num_pts] - 1
|
|
216
|
+
u32array[:, num_pts] = filtered["index"]
|
|
217
|
+
self.elements[eltype] = u32array
|
|
218
|
+
self.num_elements[eltype] = len(filtered)
|
|
219
|
+
|
|
220
|
+
curve_order = mesh.GetCurveOrder()
|
|
221
|
+
if curve_order > 1:
|
|
222
|
+
from .cf import FunctionData
|
|
223
|
+
import ngsolve as ngs
|
|
224
|
+
|
|
225
|
+
cf = ngs.CF((ngs.x, ngs.y, ngs.z))
|
|
226
|
+
self.curvature_data = FunctionData(self, cf, curve_order)
|
|
227
|
+
self.curvature_subdivision = curve_order + 3
|
|
228
|
+
|
|
229
|
+
self._last_mesh_timestamp = mesh._timestamp
|
|
230
|
+
|
|
231
|
+
def get_bounding_box(self):
|
|
232
|
+
return (self.pmin, self.pmax)
|
|
233
|
+
|
|
234
|
+
def get_buffers(self):
|
|
235
|
+
for eltype in self.elements:
|
|
236
|
+
if eltype not in self.gpu_elements:
|
|
237
|
+
self.gpu_elements[eltype] = buffer_from_array(
|
|
238
|
+
self.elements[eltype])
|
|
239
|
+
if "curvature_subdivision" not in self.gpu_elements:
|
|
240
|
+
self.gpu_elements["curvature_subdivision"] = uniform_from_array(
|
|
241
|
+
np.array([self.curvature_subdivision], dtype=np.uint32)
|
|
242
|
+
)
|
|
243
|
+
if "deformation_scale" not in self.gpu_elements:
|
|
244
|
+
self.gpu_elements["deformation_scale"] = uniform_from_array(
|
|
245
|
+
np.array([self.deformation_scale], dtype=np.float32)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return self.gpu_elements
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class Mesh2dElementsRenderer(RenderObject):
|
|
252
|
+
depthBias: int = 1
|
|
253
|
+
depthBiasSlopeScale: float = 1.0
|
|
254
|
+
vertex_entry_point: str = "vertexTrigP1Indexed"
|
|
255
|
+
fragment_entry_point: str = "fragment2dElement"
|
|
256
|
+
color = (0, 1, 0, 1)
|
|
257
|
+
|
|
258
|
+
def __init__(self, data: MeshData, label="Mesh2dElementsRenderer"):
|
|
259
|
+
super().__init__(label=label)
|
|
260
|
+
self.data = data
|
|
261
|
+
self.clipping = Clipping()
|
|
262
|
+
|
|
263
|
+
def update(self, timestamp: float):
|
|
264
|
+
if self._timestamp == timestamp:
|
|
265
|
+
return
|
|
266
|
+
self.clipping.update(timestamp)
|
|
267
|
+
self.data.update(timestamp)
|
|
268
|
+
self.curvature_subdivision = self.data.curvature_subdivision
|
|
269
|
+
self.n_vertices = 3 * self.curvature_subdivision**2
|
|
270
|
+
|
|
271
|
+
self._buffers = self.data.get_buffers()
|
|
272
|
+
self.n_instances = self.data.num_elements[ElType.TRIG]
|
|
273
|
+
self.color_uniform = buffer_from_array(np.array(self.color, dtype=np.float32))
|
|
274
|
+
self.create_render_pipeline()
|
|
275
|
+
|
|
276
|
+
def get_bounding_box(self):
|
|
277
|
+
return self.data.get_bounding_box()
|
|
278
|
+
|
|
279
|
+
def get_bindings(self):
|
|
280
|
+
bindings = [
|
|
281
|
+
*self.options.get_bindings(),
|
|
282
|
+
*self.clipping.get_bindings(),
|
|
283
|
+
BufferBinding(Binding.VERTICES, self._buffers["vertices"]),
|
|
284
|
+
BufferBinding(Binding.TRIGS_INDEX, self._buffers[ElType.TRIG]),
|
|
285
|
+
BufferBinding(Binding.CURVATURE_VALUES_2D, self._buffers["curvature_2d"]),
|
|
286
|
+
BufferBinding(Binding.DEFORMATION_VALUES, self._buffers["deformation_2d"]),
|
|
287
|
+
UniformBinding(Binding.DEFORMATION_SCALE, self._buffers["deformation_scale"]),
|
|
288
|
+
UniformBinding(
|
|
289
|
+
Binding.CURVATURE_SUBDIVISION, self._buffers["curvature_subdivision"]
|
|
290
|
+
),
|
|
291
|
+
]
|
|
292
|
+
if hasattr(self, "color_uniform"):
|
|
293
|
+
bindings.append(BufferBinding(54, self.color_uniform))
|
|
294
|
+
return bindings
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_shader_code(self):
|
|
298
|
+
shader_code = ""
|
|
299
|
+
shader_code += self.clipping.get_shader_code()
|
|
300
|
+
for file_name in [
|
|
301
|
+
"eval.wgsl",
|
|
302
|
+
"mesh.wgsl",
|
|
303
|
+
"shader.wgsl",
|
|
304
|
+
"uniforms.wgsl",
|
|
305
|
+
]:
|
|
306
|
+
shader_code += read_shader_file(file_name, __file__)
|
|
307
|
+
# for now as shaders are not seperated well
|
|
308
|
+
import webgpu.colormap
|
|
309
|
+
|
|
310
|
+
shader_code += webgpu.colormap.Colormap().get_shader_code()
|
|
311
|
+
shader_code += self.options.camera.get_shader_code()
|
|
312
|
+
shader_code += self.options.light.get_shader_code()
|
|
313
|
+
return shader_code
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class Mesh2dWireframeRenderer(Mesh2dElementsRenderer):
|
|
317
|
+
depthBias: int = 0
|
|
318
|
+
topology: PrimitiveTopology = PrimitiveTopology.line_strip
|
|
319
|
+
color = (0, 0, 0, 1)
|
|
320
|
+
fragment_entry_point: str = "fragmentWireframe2d"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class El3dUniform(UniformBase):
|
|
324
|
+
_binding = Binding.MESH
|
|
325
|
+
_fields_ = [
|
|
326
|
+
("subdivision", ct.c_uint32),
|
|
327
|
+
("shrink", ct.c_float),
|
|
328
|
+
("padding", ct.c_float * 2),
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
def __init__(self, device, subdivision=0, shrink=1.0):
|
|
332
|
+
super().__init__(device, subdivision=subdivision, shrink=shrink)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class Mesh3dElementsRenderObject(RenderObject):
|
|
336
|
+
n_vertices: int = 3 * 4
|
|
337
|
+
|
|
338
|
+
def __init__(self, data: MeshData):
|
|
339
|
+
super().__init__(label="Mesh3dElementsRenderObject")
|
|
340
|
+
data.need_3d = True
|
|
341
|
+
self.data = data
|
|
342
|
+
self.clipping = Clipping()
|
|
343
|
+
|
|
344
|
+
def get_bounding_box(self) -> tuple[list[float], list[float]] | None:
|
|
345
|
+
return self.data.get_bounding_box()
|
|
346
|
+
|
|
347
|
+
def update(self, timestamp):
|
|
348
|
+
if timestamp == self._timestamp:
|
|
349
|
+
return
|
|
350
|
+
self._timestamp = timestamp
|
|
351
|
+
self.data.update()
|
|
352
|
+
self.uniforms = El3dUniform(self.device)
|
|
353
|
+
self.clipping.options = self.options
|
|
354
|
+
self.clipping.update(timestamp)
|
|
355
|
+
self._buffers = self.data.get_buffers()
|
|
356
|
+
self.uniforms.update_buffer()
|
|
357
|
+
self.n_instances = self.data.num_elements[ElType.TET]
|
|
358
|
+
self.create_render_pipeline()
|
|
359
|
+
|
|
360
|
+
def add_options_to_gui(self, gui):
|
|
361
|
+
def set_shrink(value):
|
|
362
|
+
self.uniforms.shrink = value
|
|
363
|
+
self.uniforms.update_buffer()
|
|
364
|
+
|
|
365
|
+
gui.slider(
|
|
366
|
+
label="Shrink", value=1.0, min=0.0, max=1.0, step=0.01, func=set_shrink
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def get_bindings(self):
|
|
370
|
+
return [
|
|
371
|
+
*self.clipping.get_bindings(),
|
|
372
|
+
BufferBinding(Binding.VERTICES, self._buffers["vertices"]),
|
|
373
|
+
BufferBinding(Binding.TET, self._buffers[ElType.TET]),
|
|
374
|
+
*self.uniforms.get_bindings(),
|
|
375
|
+
*self.options.get_bindings(),
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
def get_shader_code(self):
|
|
379
|
+
code = read_shader_file("elements3d.wgsl", __file__)
|
|
380
|
+
code += self.clipping.get_shader_code()
|
|
381
|
+
code += self.options.camera.get_shader_code()
|
|
382
|
+
code += self.options.light.get_shader_code()
|
|
383
|
+
return code
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class PointNumbersRenderObject(RenderObject):
|
|
387
|
+
"""Render a point numbers of a mesh"""
|
|
388
|
+
|
|
389
|
+
_buffers: dict
|
|
390
|
+
|
|
391
|
+
def __init__(self, data, font_size=20, label=None):
|
|
392
|
+
super().__init__(label=label)
|
|
393
|
+
self.n_digits = 6
|
|
394
|
+
self.data = data
|
|
395
|
+
self.depthBias = -1
|
|
396
|
+
self.vertex_entry_point = "vertexPointNumber"
|
|
397
|
+
self.fragment_entry_point = "fragmentFont"
|
|
398
|
+
self.n_vertices = self.n_digits * 6
|
|
399
|
+
self.font_size = font_size
|
|
400
|
+
self.clipping = Clipping()
|
|
401
|
+
|
|
402
|
+
def update(self, timestamp):
|
|
403
|
+
if timestamp == self._timestamp:
|
|
404
|
+
return
|
|
405
|
+
self._timestamp = timestamp
|
|
406
|
+
self.clipping.update(timestamp)
|
|
407
|
+
self.font = Font(self.canvas, self.font_size)
|
|
408
|
+
self._buffers = self.data.get_buffers()
|
|
409
|
+
self.n_instances = self.data.num_elements["vertices"]
|
|
410
|
+
self.create_render_pipeline()
|
|
411
|
+
|
|
412
|
+
def get_shader_code(self):
|
|
413
|
+
return read_shader_file("numbers.wgsl", __file__)
|
|
414
|
+
|
|
415
|
+
def get_bounding_box(self):
|
|
416
|
+
return self.data.get_bounding_box()
|
|
417
|
+
|
|
418
|
+
def get_bindings(self):
|
|
419
|
+
return [
|
|
420
|
+
*self.clipping.get_bindings(),
|
|
421
|
+
*self.options.get_bindings(),
|
|
422
|
+
*self.font.get_bindings(),
|
|
423
|
+
BufferBinding(Binding.VERTICES, self._buffers["vertices"]),
|
|
424
|
+
]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
|
|
2
|
+
@group(0) @binding(12) var<storage> vertices : array<f32>;
|
|
3
|
+
@group(0) @binding(25) var<storage> u_tets : array<Tet>;
|
|
4
|
+
|
|
5
|
+
struct Tet { p: array<u32, 4>, nr: u32, index: u32 };
|
|
6
|
+
|
|
7
|
+
struct SubTrig {
|
|
8
|
+
lam: array<vec3f, 3>,
|
|
9
|
+
id: u32,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct ClipTetResult {
|
|
13
|
+
n: u32,
|
|
14
|
+
trigs: array<SubTrig, 2>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn get_tet_points(elid: u32) -> array<vec3f, 4>
|
|
18
|
+
{
|
|
19
|
+
var p: array<vec3f, 4>;
|
|
20
|
+
for(var i = 0u; i < 4u; i++) {
|
|
21
|
+
for(var j = 0u; j < 3u; j++) {
|
|
22
|
+
p[i][j] = vertices[u_tets[elid].p[i] * 3u + j];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn clipCheckOrientation(trig: SubTrig, values: array<f32, 4>) -> SubTrig {
|
|
29
|
+
var result = trig;
|
|
30
|
+
let n = cross(
|
|
31
|
+
trig.lam[1] - trig.lam[0],
|
|
32
|
+
trig.lam[2] - trig.lam[0]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
let p = 1.0/3.*(trig.lam[0] + trig.lam[1] + trig.lam[2]) + n;
|
|
36
|
+
let p4 = 1.0 - p.x - p.y - p.z;
|
|
37
|
+
let value = p.x * values[0] + p.y * values[1] + p.z * values[2] + p4 * values[3];
|
|
38
|
+
|
|
39
|
+
if(value<0.0) {
|
|
40
|
+
result.lam[1] = trig.lam[2];
|
|
41
|
+
result.lam[2] = trig.lam[1];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// clip tet such that the clip triangle(s) have value 0 everywhere
|
|
48
|
+
fn clipTet(lam: array<vec3f, 4>, values: array<f32, 4>, ei: u32) -> ClipTetResult {
|
|
49
|
+
var trigs = ClipTetResult(0, array<SubTrig, 2>(SubTrig(array<vec3f, 3>(vec3f(0.0), vec3f(0.0), vec3f(0.0)), 0), SubTrig(array<vec3f, 3>(vec3f(0.0), vec3f(0.0), vec3f(0.0)), 0)));
|
|
50
|
+
var p_pos = array<u32, 4>(0u, 0u, 0u, 0u);
|
|
51
|
+
var p_neg = array<u32, 4>(0u, 0u, 0u, 0u);
|
|
52
|
+
|
|
53
|
+
var n_pos: u32 = 0u;
|
|
54
|
+
var n_neg: u32 = 0u;
|
|
55
|
+
|
|
56
|
+
for (var i = 0u; i < 4u; i++) {
|
|
57
|
+
if (values[i] > 0.0) {
|
|
58
|
+
p_pos[n_pos] = i;
|
|
59
|
+
n_pos++;
|
|
60
|
+
} else {
|
|
61
|
+
p_neg[n_neg] = i;
|
|
62
|
+
n_neg++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if n_pos == 4u || n_neg == 4u {
|
|
67
|
+
return trigs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if n_pos == 3u {
|
|
71
|
+
trigs.n = 1u;
|
|
72
|
+
n_pos = 1u;
|
|
73
|
+
n_neg = 3u;
|
|
74
|
+
p_pos[3] = p_neg[0];
|
|
75
|
+
p_neg[0] = p_pos[0];
|
|
76
|
+
p_neg[1] = p_pos[1];
|
|
77
|
+
p_neg[2] = p_pos[2];
|
|
78
|
+
p_pos[0] = p_pos[3];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if n_pos == 1u {
|
|
82
|
+
trigs.n = 1u;
|
|
83
|
+
trigs.trigs[0].id = ei;
|
|
84
|
+
for (var i = 0u; i < 3u; i++) {
|
|
85
|
+
let t = values[p_pos[0] ] / (values[p_pos[0] ] - values[p_neg[i] ]);
|
|
86
|
+
let lam_trig = mix(lam[p_pos[0] ], lam[p_neg[i] ], t);
|
|
87
|
+
trigs.trigs[0].lam[i] = lam_trig;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
trigs.trigs[0] = clipCheckOrientation(trigs.trigs[0], values);
|
|
91
|
+
return trigs;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// two points before, two points behind clipping plane
|
|
95
|
+
// -> two triangles
|
|
96
|
+
trigs.n = 2u;
|
|
97
|
+
var pairs = array<vec2u,4>(
|
|
98
|
+
vec2u(p_pos[1], p_neg[0]),
|
|
99
|
+
vec2u(p_pos[0], p_neg[0]),
|
|
100
|
+
vec2u(p_pos[0], p_neg[1]),
|
|
101
|
+
vec2u(p_pos[1], p_neg[1])
|
|
102
|
+
);
|
|
103
|
+
var points: array<vec3f, 4> = array<vec3f, 4>(vec3f(0.0), vec3f(0.0), vec3f(0.0), vec3f(0.0));
|
|
104
|
+
for (var i = 0; i < 4; i++) {
|
|
105
|
+
let p0 = pairs[i].x;
|
|
106
|
+
let p1 = pairs[i].y;
|
|
107
|
+
let t = values[p0 ] / (values[p0] - values[p1]);
|
|
108
|
+
let lam_trig = mix(lam[p0], lam[p1], t);
|
|
109
|
+
points[i] = lam_trig;
|
|
110
|
+
}
|
|
111
|
+
trigs.trigs[0].id = ei;
|
|
112
|
+
trigs.trigs[0].lam = array(points[0], points[1], points[2]);
|
|
113
|
+
trigs.trigs[1].id = ei;
|
|
114
|
+
trigs.trigs[1].lam = array(points[0], points[2], points[3]);
|
|
115
|
+
|
|
116
|
+
trigs.trigs[0] = clipCheckOrientation(trigs.trigs[0], values);
|
|
117
|
+
trigs.trigs[1] = clipCheckOrientation(trigs.trigs[1], values);
|
|
118
|
+
return trigs;
|
|
119
|
+
}
|
|
120
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
@group(0) @binding(21) var<storage, read_write> count_trigs: atomic<u32>;
|
|
3
|
+
@group(0) @binding(22) var<uniform> u_ntets: u32;
|
|
4
|
+
@group(0) @binding(23) var<uniform> only_count: u32;
|
|
5
|
+
@group(0) @binding(24) var<storage, read_write> subtrigs: array<SubTrig>;
|
|
6
|
+
|
|
7
|
+
@compute @workgroup_size(256)
|
|
8
|
+
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
9
|
+
for (var i = id.x; i<u_ntets; i+=256*1024) {
|
|
10
|
+
let p = get_tet_points(i);
|
|
11
|
+
let lam = array(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0), vec3f(0.0, 0.0, 0.0));
|
|
12
|
+
|
|
13
|
+
let f = array(dot(vec4<f32>(p[0], 1.0), u_clipping.plane),
|
|
14
|
+
dot(vec4<f32>(p[1], 1.0), u_clipping.plane),
|
|
15
|
+
dot(vec4<f32>(p[2], 1.0), u_clipping.plane),
|
|
16
|
+
dot(vec4<f32>(p[3], 1.0), u_clipping.plane));
|
|
17
|
+
let cuts = clipTet(lam, f, i);
|
|
18
|
+
if(cuts.n == 0) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
let index = atomicAdd(&count_trigs, cuts.n);
|
|
22
|
+
if(only_count == u32(1)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for(var i = 0u; i < cuts.n; i++) {
|
|
26
|
+
subtrigs[index+i] = cuts.trigs[i];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|