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.
webgpu/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from .clipping import Clipping
2
+ from .colormap import Colormap
3
+ from .font import Font
4
+ from .render_object import RenderObject
5
+ from .scene import Scene
6
+ from .utils import (
7
+ BaseBinding,
8
+ BufferBinding,
9
+ UniformBinding,
10
+ create_bind_group,
11
+ read_shader_file,
12
+ )
webgpu/_version.py ADDED
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.0.1'
21
+ __version_tuple__ = version_tuple = (0, 0, 1)
webgpu/camera.py ADDED
@@ -0,0 +1,189 @@
1
+ import numpy as np
2
+
3
+ from .uniforms import BaseBinding, Binding, UniformBase, ct
4
+ from .utils import read_shader_file
5
+
6
+
7
+ class CameraUniforms(UniformBase):
8
+ """Uniforms class, derived from ctypes.Structure to ensure correct memory layout"""
9
+
10
+ _binding = Binding.CAMERA
11
+
12
+ _fields_ = [
13
+ ("model_view", ct.c_float * 16),
14
+ ("model_view_projection", ct.c_float * 16),
15
+ ("normal_mat", ct.c_float * 16),
16
+ ("aspect", ct.c_float),
17
+ ("padding", ct.c_uint32 * 3),
18
+ ]
19
+
20
+
21
+ class Transform:
22
+ def __init__(self):
23
+ self._mat = np.identity(4)
24
+ self._rot_mat = np.identity(4)
25
+ self._center = (0.5, 0.5, 0)
26
+ self._scale = 1
27
+
28
+ def translate(self, dx=0.0, dy=0.0, dz=0.0):
29
+ translation = np.array([[1, 0, 0, dx], [0, 1, 0, dy], [0, 0, 1, dz], [0, 0, 0, 1]])
30
+ self._mat = translation @ self._mat
31
+
32
+ def scale(self, s):
33
+ self._scale *= s
34
+
35
+ def rotate(self, ang_x, ang_y=0):
36
+ rx = np.radians(ang_x)
37
+ cx = np.cos(rx)
38
+ sx = np.sin(rx)
39
+
40
+ rotation_x = np.array(
41
+ [
42
+ [1, 0, 0, 0],
43
+ [0, cx, -sx, 0],
44
+ [0, sx, cx, 0],
45
+ [0, 0, 0, 1],
46
+ ]
47
+ )
48
+
49
+ ry = np.radians(ang_y)
50
+ cy = np.cos(ry)
51
+ sy = np.sin(ry)
52
+ rotation_y = np.array(
53
+ [
54
+ [cy, 0, sy, 0],
55
+ [0, 1, 0, 0],
56
+ [-sy, 0, cy, 0],
57
+ [0, 0, 0, 1],
58
+ ]
59
+ )
60
+
61
+ self._rot_mat = rotation_x @ rotation_y @ self._rot_mat
62
+
63
+ @property
64
+ def mat(self):
65
+ return self._mat @ self._rot_mat @ self._scale_mat @ self._center_mat
66
+
67
+ @property
68
+ def normal_mat(self):
69
+ return self._mat @ self._rot_mat @ self._scale_mat @ self._center_mat
70
+
71
+ @property
72
+ def _center_mat(self):
73
+ cx, cy, cz = self._center
74
+ return np.array([[1, 0, 0, -cx], [0, 1, 0, -cy], [0, 0, 1, -cz], [0, 0, 0, 1]])
75
+
76
+ @property
77
+ def _scale_mat(self):
78
+ s = self._scale
79
+ return np.array([[s, 0, 0, 0], [0, s, 0, 0], [0, 0, s, 0], [0, 0, 0, 1]])
80
+
81
+
82
+ class Camera:
83
+ def __init__(self, canvas):
84
+ self.canvas = canvas
85
+ self.uniforms = CameraUniforms(canvas.device)
86
+ self.transform = Transform()
87
+ self._render_function = None
88
+ self._is_moving = False
89
+ self._is_rotating = False
90
+
91
+ canvas.on_resize(self._update_uniforms)
92
+
93
+ def get_bindings(self) -> list[BaseBinding]:
94
+ return self.uniforms.get_bindings()
95
+
96
+ def get_shader_code(self):
97
+ return read_shader_file("camera.wgsl", __file__)
98
+
99
+ def __del__(self):
100
+ del self.uniforms
101
+
102
+ def register_callbacks(self, input_handler, redraw_function):
103
+ self._render_function = redraw_function
104
+ input_handler.on_mousedown(self._on_mousedown)
105
+ input_handler.on_mouseup(self._on_mouseup)
106
+ input_handler.on_mouseout(self._on_mouseup)
107
+ input_handler.on_mousemove(self._on_mousemove)
108
+ input_handler.on_wheel(self._on_wheel)
109
+
110
+ def unregister_callbacks(self, input_handler):
111
+ input_handler.unregister("mousedown", self._on_mousedown)
112
+ input_handler.unregister("mouseup", self._on_mouseup)
113
+ input_handler.unregister("mouseout", self._on_mouseup)
114
+ input_handler.unregister("mousemove", self._on_mousemove)
115
+ input_handler.unregister("wheel", self._on_wheel)
116
+
117
+ def _on_mousedown(self, ev):
118
+ if ev["button"] == 0:
119
+ self._is_rotating = True
120
+ if ev["button"] == 1:
121
+ self._is_moving = True
122
+
123
+ def _on_mouseup(self, _):
124
+ self._is_moving = False
125
+ self._is_rotating = False
126
+ self._is_zooming = False
127
+
128
+ def _on_wheel(self, ev):
129
+ self.transform.scale(1 - ev["deltaY"] / 1000)
130
+ self._render()
131
+ if hasattr(ev, "preventDefault"):
132
+ ev.preventDefault()
133
+
134
+ def _on_mousemove(self, ev):
135
+ if self._is_rotating:
136
+ s = 0.3
137
+ self.transform.rotate(s * ev["movementY"], s * ev["movementX"])
138
+ self._render()
139
+ if self._is_moving:
140
+ s = 0.01
141
+ self.transform.translate(s * ev["movementX"], -s * ev["movementY"])
142
+ self._render()
143
+
144
+ def _render(self):
145
+ self._update_uniforms()
146
+ if self._render_function:
147
+ self._render_function()
148
+
149
+ def _update_uniforms(self):
150
+ near = 0.1
151
+ far = 10
152
+ fov = 45
153
+ aspect = self.canvas.width / self.canvas.height
154
+
155
+ zoom = 1.0
156
+ top = near * (np.tan(np.radians(fov) / 2)) * zoom
157
+ height = 2 * top
158
+ width = aspect * height
159
+ left = -0.5 * width
160
+ right = left + width
161
+ bottom = top - height
162
+
163
+ x = 2 * near / (right - left)
164
+ y = 2 * near / (top - bottom)
165
+
166
+ a = (right + left) / (right - left)
167
+ b = (top + bottom) / (top - bottom)
168
+
169
+ c = -far / (far - near)
170
+ d = (-far * near) / (far - near)
171
+
172
+ proj_mat = np.array(
173
+ [
174
+ [x, 0, a, 0],
175
+ [0, y, b, 0],
176
+ [0, 0, c, d],
177
+ [0, 0, -1, 0],
178
+ ]
179
+ )
180
+
181
+ view_mat = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, -3], [0, 0, 0, 1]])
182
+ model_view = view_mat @ self.transform.mat
183
+ model_view_proj = proj_mat @ model_view
184
+ normal_mat = np.linalg.inv(model_view)
185
+
186
+ self.uniforms.model_view[:] = model_view.transpose().flatten()
187
+ self.uniforms.model_view_projection[:] = model_view_proj.transpose().flatten()
188
+ self.uniforms.normal_mat[:] = normal_mat.flatten()
189
+ self.uniforms.update_buffer()
webgpu/canvas.py ADDED
@@ -0,0 +1,144 @@
1
+ from typing import Callable
2
+
3
+ from . import platform
4
+ from .input_handler import InputHandler
5
+ from .utils import get_device
6
+ from .webgpu_api import *
7
+
8
+
9
+ def init_webgpu(html_canvas):
10
+ """Initialize WebGPU, create device and canvas"""
11
+ device = get_device()
12
+ return Canvas(device, html_canvas)
13
+
14
+
15
+ class Canvas:
16
+ """Canvas management class, handles "global" state, like webgpu device, canvas, frame and depth buffer"""
17
+
18
+ device: Device
19
+ depth_format: TextureFormat
20
+ depth_texture: Texture
21
+ multisample_texture: Texture
22
+ multisample: MultisampleState
23
+
24
+ width: int = 0
25
+ height: int = 0
26
+
27
+ _on_resize_callbacks: list[Callable] = []
28
+
29
+ def __init__(self, device, canvas, multisample_count=4):
30
+
31
+ self._on_resize_callbacks = []
32
+
33
+ self.device = device
34
+ self.format = platform.js.navigator.gpu.getPreferredCanvasFormat()
35
+ self.color_target = ColorTargetState(
36
+ format=self.format,
37
+ blend=BlendState(
38
+ color=BlendComponent(
39
+ srcFactor=BlendFactor.one,
40
+ dstFactor=BlendFactor.one_minus_src_alpha,
41
+ operation=BlendOperation.add,
42
+ ),
43
+ alpha=BlendComponent(
44
+ srcFactor=BlendFactor.one,
45
+ dstFactor=BlendFactor.one_minus_src_alpha,
46
+ operation=BlendOperation.add,
47
+ ),
48
+ ),
49
+ )
50
+
51
+ self.canvas = canvas
52
+
53
+ self.context = canvas.getContext("webgpu")
54
+ self.context.configure(
55
+ toJS(
56
+ {
57
+ "device": device.handle,
58
+ "format": self.format,
59
+ "alphaMode": "premultiplied",
60
+ "sampleCount": multisample_count,
61
+ "usage": TextureUsage.RENDER_ATTACHMENT | TextureUsage.COPY_DST,
62
+ }
63
+ )
64
+ )
65
+
66
+ self.multisample = MultisampleState(count=multisample_count)
67
+ self.depth_format = TextureFormat.depth24plus
68
+ self.input_handler = InputHandler(canvas)
69
+
70
+ self.resize()
71
+
72
+ # platform.js.webgpuOnResize(canvas, create_proxy(self.resize, True))
73
+
74
+ def on_resize(self, func: Callable):
75
+ self._on_resize_callbacks.append(func)
76
+
77
+ def resize(self, *args):
78
+ canvas = self.canvas
79
+ rect = canvas.getBoundingClientRect()
80
+ width = int(rect.width)
81
+ height = int(rect.height)
82
+
83
+ if width == self.width and height == self.height:
84
+ return False
85
+
86
+ if width == 0 or height == 0:
87
+ return False
88
+
89
+ canvas.width = width
90
+ canvas.height = height
91
+
92
+ self.width = width
93
+ self.height = height
94
+
95
+ device = self.device
96
+ self.target_texture = device.createTexture(
97
+ size=[width, height, 1],
98
+ sampleCount=1,
99
+ format=self.format,
100
+ usage=TextureUsage.RENDER_ATTACHMENT | TextureUsage.COPY_SRC,
101
+ label="target",
102
+ )
103
+ self.multisample_texture = device.createTexture(
104
+ size=[width, height, 1],
105
+ sampleCount=self.multisample.count,
106
+ format=self.format,
107
+ usage=TextureUsage.RENDER_ATTACHMENT,
108
+ label="multisample",
109
+ )
110
+
111
+ self.depth_texture = device.createTexture(
112
+ size=[width, height, 1],
113
+ format=self.depth_format,
114
+ usage=TextureUsage.RENDER_ATTACHMENT,
115
+ label="depth_texture",
116
+ sampleCount=self.multisample.count,
117
+ )
118
+
119
+ self.target_texture_view = self.target_texture.createView()
120
+
121
+ for func in self._on_resize_callbacks:
122
+ func()
123
+
124
+ def color_attachments(self, loadOp: LoadOp):
125
+ return [
126
+ RenderPassColorAttachment(
127
+ view=self.multisample_texture.createView(),
128
+ resolveTarget=self.target_texture_view,
129
+ # view=self.context.getCurrentTexture().createView(),
130
+ clearValue=Color(1, 1, 1, 1),
131
+ loadOp=loadOp,
132
+ )
133
+ ]
134
+
135
+ def depth_stencil_attachment(self, loadOp: LoadOp):
136
+ return RenderPassDepthStencilAttachment(
137
+ self.depth_texture.createView(),
138
+ depthClearValue=1.0,
139
+ depthLoadOp=loadOp,
140
+ )
141
+
142
+ def __del__(self):
143
+ # unregister is needed to remove circular references
144
+ self.input_handler.unregister_callbacks()
webgpu/clipping.py ADDED
@@ -0,0 +1,137 @@
1
+ import time
2
+
3
+ from .render_object import BaseRenderObject
4
+ from .uniforms import UniformBase, ct
5
+ from .utils import read_shader_file
6
+
7
+
8
+ class Binding:
9
+ CLIPPING = 1
10
+
11
+
12
+ class ClippingUniforms(UniformBase):
13
+ _binding = Binding.CLIPPING
14
+ _fields_ = [
15
+ ("plane", ct.c_float * 4),
16
+ ("sphere", ct.c_float * 4),
17
+ ("mode", ct.c_uint32),
18
+ ("padding", ct.c_uint32 * 3),
19
+ ]
20
+
21
+ def __init__(self, device, mode=0, **kwargs):
22
+ super().__init__(device, mode=mode, **kwargs)
23
+
24
+
25
+ class Clipping(BaseRenderObject):
26
+ class Mode:
27
+ DISABLED = 0
28
+ PLANE = 1
29
+ SPHERE = 2
30
+
31
+ def __init__(
32
+ self,
33
+ mode=Mode.DISABLED,
34
+ center=[0.0, 0.0, 0.0],
35
+ normal=[0.0, -1.0, 0.0],
36
+ radius=1.0,
37
+ ):
38
+ self.mode = mode
39
+ self.center = center
40
+ self.normal = normal
41
+ self.radius = radius
42
+ self.callbacks = []
43
+
44
+ def update(self, timestamp):
45
+ if timestamp == self._timestamp:
46
+ return
47
+ self._timestamp = timestamp
48
+ if not hasattr(self, "uniforms"):
49
+ self.uniforms = ClippingUniforms(self.device)
50
+ import numpy as np
51
+
52
+ c, n = (
53
+ np.array(self.center, dtype=np.float32),
54
+ np.array(self.normal, dtype=np.float32),
55
+ )
56
+ if np.linalg.norm(n) == 0:
57
+ n = np.array([0.0, 0.0, -1.0], dtype=np.float32)
58
+ else:
59
+ n = n / np.linalg.norm(n)
60
+ # convert to normal and distance from origin
61
+ d = -np.dot(c, n)
62
+ self.uniforms.mode = self.mode
63
+ for i in range(4):
64
+ self.uniforms.plane[i] = [*n, d][i]
65
+ self.uniforms.sphere[i] = [*c, self.radius][i]
66
+ self.update_buffer()
67
+
68
+ def update_buffer(self):
69
+ self.uniforms.update_buffer()
70
+
71
+ def get_bindings(self):
72
+ return self.uniforms.get_bindings()
73
+
74
+ def get_shader_code(self):
75
+ return read_shader_file("clipping.wgsl", __file__)
76
+
77
+ def get_bounding_box(self) -> tuple[list[float], list[float]] | None:
78
+ return None
79
+
80
+ def __del__(self):
81
+ if hasattr(self, "uniforms"):
82
+ self.uniforms._buffer.destroy()
83
+
84
+ def add_options_to_gui(self, gui):
85
+ folder = gui.folder("Clipping", closed=True)
86
+ folder.checkbox("enabled", self.mode != self.Mode.DISABLED, self.enable_clipping)
87
+ folder.value("x", self.center[0], self.set_x_value)
88
+ folder.value("y", self.center[1], self.set_y_value)
89
+ folder.value("z", self.center[2], self.set_z_value)
90
+ folder.value("nx", self.normal[0], self.set_nx_value)
91
+ folder.value("ny", self.normal[1], self.set_ny_value)
92
+ folder.value("nz", self.normal[2], self.set_nz_value)
93
+
94
+ def render(self, encoder):
95
+ pass
96
+
97
+ def enable_clipping(self, value):
98
+ self.mode = self.Mode.PLANE if value else self.Mode.DISABLED
99
+ self.update(time.time())
100
+ for cb in self.callbacks:
101
+ cb()
102
+
103
+ def set_x_value(self, value):
104
+ self.center[0] = value
105
+ self.update(time.time())
106
+ for cb in self.callbacks:
107
+ cb()
108
+
109
+ def set_y_value(self, value):
110
+ self.center[1] = value
111
+ self.update(time.time())
112
+ for cb in self.callbacks:
113
+ cb()
114
+
115
+ def set_z_value(self, value):
116
+ self.center[2] = value
117
+ self.update(time.time())
118
+ for cb in self.callbacks:
119
+ cb()
120
+
121
+ def set_nx_value(self, value):
122
+ self.normal[0] = value
123
+ self.update(time.time())
124
+ for cb in self.callbacks:
125
+ cb()
126
+
127
+ def set_ny_value(self, value):
128
+ self.normal[1] = value
129
+ self.update(time.time())
130
+ for cb in self.callbacks:
131
+ cb()
132
+
133
+ def set_nz_value(self, value):
134
+ self.normal[2] = value
135
+ self.update(time.time())
136
+ for cb in self.callbacks:
137
+ cb()