webgpu 0.0.1.dev0__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/camera.py ADDED
@@ -0,0 +1,191 @@
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(
30
+ [[1, 0, 0, dx], [0, 1, 0, dy], [0, 0, 1, dz], [0, 0, 0, 1]]
31
+ )
32
+ self._mat = translation @ self._mat
33
+
34
+ def scale(self, s):
35
+ self._scale *= s
36
+
37
+ def rotate(self, ang_x, ang_y=0):
38
+ rx = np.radians(ang_x)
39
+ cx = np.cos(rx)
40
+ sx = np.sin(rx)
41
+
42
+ rotation_x = np.array(
43
+ [
44
+ [1, 0, 0, 0],
45
+ [0, cx, -sx, 0],
46
+ [0, sx, cx, 0],
47
+ [0, 0, 0, 1],
48
+ ]
49
+ )
50
+
51
+ ry = np.radians(ang_y)
52
+ cy = np.cos(ry)
53
+ sy = np.sin(ry)
54
+ rotation_y = np.array(
55
+ [
56
+ [cy, 0, sy, 0],
57
+ [0, 1, 0, 0],
58
+ [-sy, 0, cy, 0],
59
+ [0, 0, 0, 1],
60
+ ]
61
+ )
62
+
63
+ self._rot_mat = rotation_x @ rotation_y @ self._rot_mat
64
+
65
+ @property
66
+ def mat(self):
67
+ return self._mat @ self._rot_mat @ self._scale_mat @ self._center_mat
68
+
69
+ @property
70
+ def normal_mat(self):
71
+ return self._mat @ self._rot_mat @ self._scale_mat @ self._center_mat
72
+
73
+ @property
74
+ def _center_mat(self):
75
+ cx, cy, cz = self._center
76
+ return np.array([[1, 0, 0, -cx], [0, 1, 0, -cy], [0, 0, 1, -cz], [0, 0, 0, 1]])
77
+
78
+ @property
79
+ def _scale_mat(self):
80
+ s = self._scale
81
+ return np.array([[s, 0, 0, 0], [0, s, 0, 0], [0, 0, s, 0], [0, 0, 0, 1]])
82
+
83
+
84
+ class Camera:
85
+ def __init__(self, canvas):
86
+ self.canvas = canvas
87
+ self.uniforms = CameraUniforms(canvas.device)
88
+ self.transform = Transform()
89
+ self._render_function = None
90
+ self._is_moving = False
91
+ self._is_rotating = False
92
+
93
+ canvas.on_resize(self._update_uniforms)
94
+
95
+ def get_bindings(self) -> list[BaseBinding]:
96
+ return self.uniforms.get_bindings()
97
+
98
+ def get_shader_code(self):
99
+ return read_shader_file("camera.wgsl", __file__)
100
+
101
+ def __del__(self):
102
+ del self.uniforms
103
+
104
+ def register_callbacks(self, input_handler, redraw_function):
105
+ self._render_function = redraw_function
106
+ input_handler.on_mousedown(self._on_mousedown)
107
+ input_handler.on_mouseup(self._on_mouseup)
108
+ input_handler.on_mouseout(self._on_mouseup)
109
+ input_handler.on_mousemove(self._on_mousemove)
110
+ input_handler.on_wheel(self._on_wheel)
111
+
112
+ def unregister_callbacks(self, input_handler):
113
+ input_handler.unregister("mousedown", self._on_mousedown)
114
+ input_handler.unregister("mouseup", self._on_mouseup)
115
+ input_handler.unregister("mouseout", self._on_mouseup)
116
+ input_handler.unregister("mousemove", self._on_mousemove)
117
+ input_handler.unregister("wheel", self._on_wheel)
118
+
119
+ def _on_mousedown(self, ev):
120
+ if ev["button"] == 0:
121
+ self._is_rotating = True
122
+ if ev["button"] == 1:
123
+ self._is_moving = True
124
+
125
+ def _on_mouseup(self, _):
126
+ self._is_moving = False
127
+ self._is_rotating = False
128
+ self._is_zooming = False
129
+
130
+ def _on_wheel(self, ev):
131
+ self.transform.scale(1 - ev["deltaY"] / 1000)
132
+ self._render()
133
+ if hasattr(ev, "preventDefault"):
134
+ ev.preventDefault()
135
+
136
+ def _on_mousemove(self, ev):
137
+ if self._is_rotating:
138
+ s = 0.3
139
+ self.transform.rotate(s * ev["movementY"], s * ev["movementX"])
140
+ self._render()
141
+ if self._is_moving:
142
+ s = 0.01
143
+ self.transform.translate(s * ev["movementX"], -s * ev["movementY"])
144
+ self._render()
145
+
146
+ def _render(self):
147
+ self._update_uniforms()
148
+ if self._render_function:
149
+ self._render_function()
150
+
151
+ def _update_uniforms(self):
152
+ near = 0.1
153
+ far = 10
154
+ fov = 45
155
+ aspect = self.canvas.width / self.canvas.height
156
+
157
+ zoom = 1.0
158
+ top = near * (np.tan(np.radians(fov) / 2)) * zoom
159
+ height = 2 * top
160
+ width = aspect * height
161
+ left = -0.5 * width
162
+ right = left + width
163
+ bottom = top - height
164
+
165
+ x = 2 * near / (right - left)
166
+ y = 2 * near / (top - bottom)
167
+
168
+ a = (right + left) / (right - left)
169
+ b = (top + bottom) / (top - bottom)
170
+
171
+ c = -far / (far - near)
172
+ d = (-far * near) / (far - near)
173
+
174
+ proj_mat = np.array(
175
+ [
176
+ [x, 0, a, 0],
177
+ [0, y, b, 0],
178
+ [0, 0, c, d],
179
+ [0, 0, -1, 0],
180
+ ]
181
+ )
182
+
183
+ view_mat = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, -3], [0, 0, 0, 1]])
184
+ model_view = view_mat @ self.transform.mat
185
+ model_view_proj = proj_mat @ model_view
186
+ normal_mat = np.linalg.inv(model_view)
187
+
188
+ self.uniforms.model_view[:] = model_view.transpose().flatten()
189
+ self.uniforms.model_view_projection[:] = model_view_proj.transpose().flatten()
190
+ self.uniforms.normal_mat[:] = normal_mat.flatten()
191
+ self.uniforms.update_buffer()
webgpu/canvas.py ADDED
@@ -0,0 +1,144 @@
1
+ from typing import Callable
2
+
3
+ from .input_handler import InputHandler
4
+ from .utils import get_device
5
+ from .webgpu_api import *
6
+ from . import platform
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,140 @@
1
+ from .render_object import BaseRenderObject
2
+ from .utils import read_shader_file
3
+
4
+ from .uniforms import UniformBase, ct
5
+
6
+ import time
7
+
8
+
9
+ class Binding:
10
+ CLIPPING = 1
11
+
12
+
13
+ class ClippingUniforms(UniformBase):
14
+ _binding = Binding.CLIPPING
15
+ _fields_ = [
16
+ ("plane", ct.c_float * 4),
17
+ ("sphere", ct.c_float * 4),
18
+ ("mode", ct.c_uint32),
19
+ ("padding", ct.c_uint32 * 3),
20
+ ]
21
+
22
+ def __init__(self, device, mode=0, **kwargs):
23
+ super().__init__(device, mode=mode, **kwargs)
24
+
25
+
26
+ class Clipping(BaseRenderObject):
27
+ class Mode:
28
+ DISABLED = 0
29
+ PLANE = 1
30
+ SPHERE = 2
31
+
32
+ def __init__(
33
+ self,
34
+ mode=Mode.DISABLED,
35
+ center=[0.0, 0.0, 0.0],
36
+ normal=[0.0, -1.0, 0.0],
37
+ radius=1.0,
38
+ ):
39
+ self.mode = mode
40
+ self.center = center
41
+ self.normal = normal
42
+ self.radius = radius
43
+ self.callbacks = []
44
+
45
+ def update(self, timestamp):
46
+ if timestamp == self._timestamp:
47
+ return
48
+ self._timestamp = timestamp
49
+ if not hasattr(self, "uniforms"):
50
+ self.uniforms = ClippingUniforms(self.device)
51
+ import numpy as np
52
+
53
+ c, n = (
54
+ np.array(self.center, dtype=np.float32),
55
+ np.array(self.normal, dtype=np.float32),
56
+ )
57
+ if np.linalg.norm(n) == 0:
58
+ n = np.array([0.0, 0.0, -1.0], dtype=np.float32)
59
+ else:
60
+ n = n / np.linalg.norm(n)
61
+ # convert to normal and distance from origin
62
+ d = -np.dot(c, n)
63
+ self.uniforms.mode = self.mode
64
+ for i in range(4):
65
+ self.uniforms.plane[i] = [*n, d][i]
66
+ self.uniforms.sphere[i] = [*c, self.radius][i]
67
+ self.update_buffer()
68
+
69
+ def update_buffer(self):
70
+ self.uniforms.update_buffer()
71
+
72
+ def get_bindings(self):
73
+ return self.uniforms.get_bindings()
74
+
75
+ def get_shader_code(self):
76
+ return read_shader_file("clipping.wgsl", __file__)
77
+
78
+ def get_bounding_box(self) -> tuple[list[float], list[float]] | None:
79
+ return None
80
+
81
+ def __del__(self):
82
+ if hasattr(self, "uniforms"):
83
+ self.uniforms._buffer.destroy()
84
+
85
+ def add_options_to_gui(self, gui):
86
+ folder = gui.folder("Clipping", closed=True)
87
+ folder.checkbox(
88
+ "enabled", self.mode != self.Mode.DISABLED, self.enable_clipping
89
+ )
90
+ folder.value("x", self.center[0], self.set_x_value)
91
+ folder.value("y", self.center[1], self.set_y_value)
92
+ folder.value("z", self.center[2], self.set_z_value)
93
+ folder.value("nx", self.normal[0], self.set_nx_value)
94
+ folder.value("ny", self.normal[1], self.set_ny_value)
95
+ folder.value("nz", self.normal[2], self.set_nz_value)
96
+
97
+ def render(self, encoder):
98
+ pass
99
+
100
+ def enable_clipping(self, value):
101
+ self.mode = self.Mode.PLANE if value else self.Mode.DISABLED
102
+ self.update(time.time())
103
+ for cb in self.callbacks:
104
+ cb()
105
+
106
+ def set_x_value(self, value):
107
+ self.center[0] = value
108
+ self.update(time.time())
109
+ for cb in self.callbacks:
110
+ cb()
111
+
112
+ def set_y_value(self, value):
113
+ self.center[1] = value
114
+ self.update(time.time())
115
+ for cb in self.callbacks:
116
+ cb()
117
+
118
+ def set_z_value(self, value):
119
+ self.center[2] = value
120
+ self.update(time.time())
121
+ for cb in self.callbacks:
122
+ cb()
123
+
124
+ def set_nx_value(self, value):
125
+ self.normal[0] = value
126
+ self.update(time.time())
127
+ for cb in self.callbacks:
128
+ cb()
129
+
130
+ def set_ny_value(self, value):
131
+ self.normal[1] = value
132
+ self.update(time.time())
133
+ for cb in self.callbacks:
134
+ cb()
135
+
136
+ def set_nz_value(self, value):
137
+ self.normal[2] = value
138
+ self.update(time.time())
139
+ for cb in self.callbacks:
140
+ cb()