webgpu 1.2.2.dev0__py3-none-any.whl → 1.2.3.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 +1 -0
- webgpu/_version.py +3 -3
- webgpu/camera.py +16 -9
- webgpu/canvas.py +165 -62
- webgpu/gizmo.py +304 -0
- webgpu/input_handler.py +12 -12
- webgpu/labels.py +59 -24
- webgpu/renderer.py +121 -24
- webgpu/scene.py +37 -10
- webgpu/shaders/gizmo_edges.wgsl +61 -0
- webgpu/shaders/gizmo_mesh.wgsl +62 -0
- webgpu/shaders/light.wgsl +2 -1
- webgpu/shaders/shapes.wgsl +32 -3
- webgpu/shaders/text.wgsl +89 -17
- webgpu/shapes.py +39 -1
- webgpu/uniforms.py +14 -9
- {webgpu-1.2.2.dev0.dist-info → webgpu-1.2.3.dev0.dist-info}/METADATA +1 -1
- {webgpu-1.2.2.dev0.dist-info → webgpu-1.2.3.dev0.dist-info}/RECORD +21 -18
- {webgpu-1.2.2.dev0.dist-info → webgpu-1.2.3.dev0.dist-info}/WHEEL +0 -0
- {webgpu-1.2.2.dev0.dist-info → webgpu-1.2.3.dev0.dist-info}/licenses/LICENSE +0 -0
- {webgpu-1.2.2.dev0.dist-info → webgpu-1.2.3.dev0.dist-info}/top_level.txt +0 -0
webgpu/input_handler.py
CHANGED
|
@@ -7,7 +7,7 @@ class InputHandler:
|
|
|
7
7
|
|
|
8
8
|
class Modifiers:
|
|
9
9
|
def __init__(
|
|
10
|
-
self, alt: bool | None =
|
|
10
|
+
self, alt: bool | None = None, shift: bool | None = None, ctrl: bool | None = None
|
|
11
11
|
):
|
|
12
12
|
self.alt = alt
|
|
13
13
|
self.shift = shift
|
|
@@ -61,9 +61,9 @@ class InputHandler:
|
|
|
61
61
|
self,
|
|
62
62
|
event: str,
|
|
63
63
|
func: Callable,
|
|
64
|
-
alt: bool | None =
|
|
65
|
-
shift: bool | None =
|
|
66
|
-
ctrl: bool | None =
|
|
64
|
+
alt: bool | None = None,
|
|
65
|
+
shift: bool | None = None,
|
|
66
|
+
ctrl: bool | None = None,
|
|
67
67
|
):
|
|
68
68
|
if event not in self._callbacks:
|
|
69
69
|
self._callbacks[event] = []
|
|
@@ -87,42 +87,42 @@ class InputHandler:
|
|
|
87
87
|
func(ev, *args)
|
|
88
88
|
|
|
89
89
|
def on_dblclick(
|
|
90
|
-
self, func, alt: bool | None =
|
|
90
|
+
self, func, alt: bool | None = None, shift: bool | None = None, ctrl: bool | None = None
|
|
91
91
|
):
|
|
92
92
|
self.on("dblclick", func, alt, shift, ctrl)
|
|
93
93
|
|
|
94
94
|
def on_click(
|
|
95
|
-
self, func, alt: bool | None =
|
|
95
|
+
self, func, alt: bool | None = None, shift: bool | None = None, ctrl: bool | None = None
|
|
96
96
|
):
|
|
97
97
|
self.on("click", func, alt, shift, ctrl)
|
|
98
98
|
|
|
99
99
|
def on_mousedown(
|
|
100
|
-
self, func, alt: bool | None =
|
|
100
|
+
self, func, alt: bool | None = None, shift: bool | None = None, ctrl: bool | None = None
|
|
101
101
|
):
|
|
102
102
|
self.on("mousedown", func, alt, shift, ctrl)
|
|
103
103
|
|
|
104
104
|
def on_mouseup(
|
|
105
|
-
self, func, alt: bool | None =
|
|
105
|
+
self, func, alt: bool | None = None, shift: bool | None = None, ctrl: bool | None = None
|
|
106
106
|
):
|
|
107
107
|
self.on("mouseup", func, alt, shift, ctrl)
|
|
108
108
|
|
|
109
109
|
def on_mouseout(
|
|
110
|
-
self, func, alt: bool | None =
|
|
110
|
+
self, func, alt: bool | None = None, shift: bool | None = None, ctrl: bool | None = None
|
|
111
111
|
):
|
|
112
112
|
self.on("mouseout", func, alt, shift, ctrl)
|
|
113
113
|
|
|
114
114
|
def on_wheel(
|
|
115
|
-
self, func, alt: bool | None =
|
|
115
|
+
self, func, alt: bool | None = None, shift: bool | None = None, ctrl: bool | None = None
|
|
116
116
|
):
|
|
117
117
|
self.on("wheel", func, alt, shift, ctrl)
|
|
118
118
|
|
|
119
119
|
def on_mousemove(
|
|
120
|
-
self, func, alt: bool | None =
|
|
120
|
+
self, func, alt: bool | None = None, shift: bool | None = None, ctrl: bool | None = None
|
|
121
121
|
):
|
|
122
122
|
self.on("mousemove", func, alt, shift, ctrl)
|
|
123
123
|
|
|
124
124
|
def on_drag(
|
|
125
|
-
self, func, alt: bool | None =
|
|
125
|
+
self, func, alt: bool | None = None, shift: bool | None = None, ctrl: bool | None = None
|
|
126
126
|
):
|
|
127
127
|
self.on("drag", func, alt, shift, ctrl)
|
|
128
128
|
|
webgpu/labels.py
CHANGED
|
@@ -3,7 +3,7 @@ import numpy as np
|
|
|
3
3
|
from .font import Font
|
|
4
4
|
from .renderer import Renderer, RenderOptions, check_timestamp
|
|
5
5
|
from .uniforms import Binding
|
|
6
|
-
from .utils import BufferBinding, buffer_from_array, read_shader_file
|
|
6
|
+
from .utils import BufferBinding, UniformBinding, buffer_from_array, uniform_from_array, read_shader_file
|
|
7
7
|
from .webgpu_api import *
|
|
8
8
|
|
|
9
9
|
|
|
@@ -18,8 +18,11 @@ class Labels(Renderer):
|
|
|
18
18
|
@param positions: list of positions to render the labels at
|
|
19
19
|
@param apply_camera: whether to apply the camera transformation to the labels
|
|
20
20
|
@param h_align: horizontal alignment of the labels. Can be one of: left, l, center, c, right, r
|
|
21
|
-
@param v_align:
|
|
21
|
+
@param v_align: vertical alignment of the labels. Can be one of: bottom, b, center, c, top, t
|
|
22
22
|
@param font_size: font size
|
|
23
|
+
@param overlay: dict with 'corner' (x,y) and 'scale' for fixed-screen overlay mode, or None
|
|
24
|
+
@param normals: per-label normals for visibility culling in overlay mode, or None
|
|
25
|
+
@param colors: per-label RGBA colors (list of [r,g,b,a] floats 0-1), or None (renders black)
|
|
23
26
|
|
|
24
27
|
If any of apply_camera, h_align, or v_align is a list, it must have the same length as labels.
|
|
25
28
|
"""
|
|
@@ -32,6 +35,9 @@ class Labels(Renderer):
|
|
|
32
35
|
h_align: str | list[str] = "left",
|
|
33
36
|
v_align: str | list[str] = "bottom",
|
|
34
37
|
font_size=20,
|
|
38
|
+
overlay: dict | None = None,
|
|
39
|
+
normals: list | None = None,
|
|
40
|
+
colors: list | None = None,
|
|
35
41
|
):
|
|
36
42
|
super().__init__()
|
|
37
43
|
self.labels = labels
|
|
@@ -40,15 +46,22 @@ class Labels(Renderer):
|
|
|
40
46
|
self.apply_camera = apply_camera
|
|
41
47
|
self.h_align = h_align
|
|
42
48
|
self.v_align = v_align
|
|
49
|
+
self.overlay = overlay
|
|
50
|
+
self.normals = normals
|
|
51
|
+
self.colors = colors
|
|
43
52
|
self.buffer = None
|
|
44
|
-
|
|
53
|
+
self._overlay_buf = None
|
|
45
54
|
self.font = None
|
|
46
55
|
|
|
56
|
+
if colors is not None:
|
|
57
|
+
self.fragment_entry_point = "fragmentFontColor"
|
|
58
|
+
|
|
47
59
|
def update(self, options: RenderOptions):
|
|
48
60
|
n_chars = sum(len(label) for label in self.labels)
|
|
49
61
|
n_labels = len(self.labels)
|
|
50
62
|
self.n_vertices = 6
|
|
51
63
|
self.n_instances = n_chars
|
|
64
|
+
|
|
52
65
|
char_t = np.dtype(
|
|
53
66
|
[
|
|
54
67
|
("itext", np.uint32),
|
|
@@ -57,27 +70,22 @@ class Labels(Renderer):
|
|
|
57
70
|
]
|
|
58
71
|
)
|
|
59
72
|
char_data = np.zeros(n_chars, dtype=char_t)
|
|
73
|
+
|
|
74
|
+
# 8 u32s per text: pos(3f) + packed(1u) + normal(3f) + color_packed(1u)
|
|
60
75
|
text_t = np.dtype(
|
|
61
76
|
[
|
|
62
77
|
("pos", np.float32, 3),
|
|
63
|
-
("
|
|
64
|
-
("
|
|
65
|
-
("
|
|
78
|
+
("packed", np.uint32),
|
|
79
|
+
("normal", np.float32, 3),
|
|
80
|
+
("color_packed", np.uint32),
|
|
66
81
|
]
|
|
67
82
|
)
|
|
68
83
|
text_data = np.zeros(n_labels, dtype=text_t)
|
|
69
84
|
|
|
70
85
|
align_map = {
|
|
71
|
-
"c": 1,
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"right": 2,
|
|
75
|
-
"t": 2,
|
|
76
|
-
"top": 2,
|
|
77
|
-
"b": 0,
|
|
78
|
-
"bottom": 0,
|
|
79
|
-
"l": 0,
|
|
80
|
-
"left": 0,
|
|
86
|
+
"c": 1, "center": 1,
|
|
87
|
+
"r": 2, "right": 2, "t": 2, "top": 2,
|
|
88
|
+
"b": 0, "bottom": 0, "l": 0, "left": 0,
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
if self.font is None:
|
|
@@ -88,21 +96,38 @@ class Labels(Renderer):
|
|
|
88
96
|
char_map = self.font.atlas.char_map
|
|
89
97
|
|
|
90
98
|
ichar = 0
|
|
91
|
-
for i, label, pos in zip(
|
|
99
|
+
for i, (label, pos) in enumerate(zip(self.labels, self.positions)):
|
|
92
100
|
h_align = self.h_align if isinstance(self.h_align, str) else self.h_align[i]
|
|
93
101
|
v_align = self.v_align if isinstance(self.v_align, str) else self.v_align[i]
|
|
94
102
|
align = align_map[h_align] + 4 * align_map[v_align]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
|
|
104
|
+
if self.overlay is not None:
|
|
105
|
+
apply_camera = 2
|
|
106
|
+
elif isinstance(self.apply_camera, bool):
|
|
107
|
+
apply_camera = int(self.apply_camera)
|
|
108
|
+
else:
|
|
109
|
+
apply_camera = int(self.apply_camera[i])
|
|
98
110
|
|
|
99
111
|
if len(pos) == 2:
|
|
100
112
|
pos = (*pos, 0)
|
|
101
113
|
|
|
102
114
|
text_data[i]["pos"] = pos
|
|
103
|
-
text_data[i]["
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
text_data[i]["packed"] = (
|
|
116
|
+
(len(label) & 0xFFFF)
|
|
117
|
+
| ((apply_camera & 0xFF) << 16)
|
|
118
|
+
| ((align & 0xFF) << 24)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if self.normals is not None:
|
|
122
|
+
text_data[i]["normal"] = self.normals[i]
|
|
123
|
+
|
|
124
|
+
if self.colors is not None:
|
|
125
|
+
c = self.colors[i]
|
|
126
|
+
r = int(c[0] * 255)
|
|
127
|
+
g = int(c[1] * 255)
|
|
128
|
+
b = int(c[2] * 255)
|
|
129
|
+
a = int(c[3] * 255) if len(c) > 3 else 255
|
|
130
|
+
text_data[i]["color_packed"] = r | (g << 8) | (b << 16) | (a << 24)
|
|
106
131
|
|
|
107
132
|
i0 = ichar
|
|
108
133
|
for c in label:
|
|
@@ -112,13 +137,22 @@ class Labels(Renderer):
|
|
|
112
137
|
ichar += 1
|
|
113
138
|
|
|
114
139
|
data = (
|
|
115
|
-
np.array([
|
|
140
|
+
np.array([n_labels], dtype=np.uint32).tobytes()
|
|
116
141
|
+ text_data.tobytes()
|
|
117
142
|
+ char_data.tobytes()
|
|
118
143
|
)
|
|
119
144
|
|
|
120
145
|
self.buffer = buffer_from_array(data, BufferUsage.STORAGE | BufferUsage.COPY_DST, "labels", self.buffer)
|
|
121
146
|
|
|
147
|
+
# Overlay uniform
|
|
148
|
+
if self.overlay is not None:
|
|
149
|
+
corner = self.overlay["corner"]
|
|
150
|
+
scale = self.overlay["scale"]
|
|
151
|
+
overlay_data = np.array([corner[0], corner[1], scale, 0.0], dtype=np.float32)
|
|
152
|
+
else:
|
|
153
|
+
overlay_data = np.array([0.0, 0.0, 0.0, 0.0], dtype=np.float32)
|
|
154
|
+
self._overlay_buf = uniform_from_array(overlay_data, label="overlay_uni", reuse=self._overlay_buf)
|
|
155
|
+
|
|
122
156
|
def get_shader_code(self):
|
|
123
157
|
return read_shader_file("text.wgsl")
|
|
124
158
|
|
|
@@ -126,4 +160,5 @@ class Labels(Renderer):
|
|
|
126
160
|
return [
|
|
127
161
|
*self.font.get_bindings(),
|
|
128
162
|
BufferBinding(Binding.TEXT, self.buffer),
|
|
163
|
+
UniformBinding(31, self._overlay_buf),
|
|
129
164
|
]
|
webgpu/renderer.py
CHANGED
|
@@ -76,6 +76,11 @@ class RenderOptions:
|
|
|
76
76
|
def __init__(self, camera: Camera, light: Light):
|
|
77
77
|
self.light = light
|
|
78
78
|
self.camera = camera
|
|
79
|
+
self._extra_binding_providers = []
|
|
80
|
+
|
|
81
|
+
def add_bindings(self, provider):
|
|
82
|
+
"""Register an object with a get_bindings() method (e.g. a UniformBase)."""
|
|
83
|
+
self._extra_binding_providers.append(provider)
|
|
79
84
|
|
|
80
85
|
def set_canvas(self, canvas: Canvas):
|
|
81
86
|
self.canvas = canvas
|
|
@@ -90,9 +95,13 @@ class RenderOptions:
|
|
|
90
95
|
self.light.update(self)
|
|
91
96
|
|
|
92
97
|
def get_bindings(self):
|
|
98
|
+
extra = []
|
|
99
|
+
for p in self._extra_binding_providers:
|
|
100
|
+
extra.extend(p.get_bindings())
|
|
93
101
|
return [
|
|
94
102
|
*self.light.get_bindings(),
|
|
95
103
|
*self.camera.get_bindings(),
|
|
104
|
+
*extra,
|
|
96
105
|
]
|
|
97
106
|
|
|
98
107
|
def begin_render_pass(self, **kwargs):
|
|
@@ -163,6 +172,7 @@ class BaseRenderer:
|
|
|
163
172
|
shader_defines: dict[str, str] = None
|
|
164
173
|
_id = None
|
|
165
174
|
_on_select: list[Callable[[SelectEvent], None]]
|
|
175
|
+
transparent: bool = False
|
|
166
176
|
|
|
167
177
|
def __init__(self, label=None):
|
|
168
178
|
self._id = next(_id_counter)
|
|
@@ -280,9 +290,18 @@ class MultipleRenderer(BaseRenderer):
|
|
|
280
290
|
r.create_render_pipeline(options)
|
|
281
291
|
|
|
282
292
|
def render(self, options: RenderOptions) -> None:
|
|
293
|
+
self.render_opaque(options)
|
|
294
|
+
self.render_transparent(options)
|
|
295
|
+
|
|
296
|
+
def render_opaque(self, options: RenderOptions) -> None:
|
|
297
|
+
for r in self.render_objects:
|
|
298
|
+
if r.active:
|
|
299
|
+
r.render_opaque(options)
|
|
300
|
+
|
|
301
|
+
def render_transparent(self, options: RenderOptions) -> None:
|
|
283
302
|
for r in self.render_objects:
|
|
284
303
|
if r.active:
|
|
285
|
-
r.
|
|
304
|
+
r.render_transparent(options)
|
|
286
305
|
|
|
287
306
|
def select(self, options: RenderOptions, x: int, y: int) -> None:
|
|
288
307
|
for r in self.render_objects:
|
|
@@ -306,6 +325,10 @@ class MultipleRenderer(BaseRenderer):
|
|
|
306
325
|
def on_select_set(self):
|
|
307
326
|
return any(r.on_select_set for r in self.render_objects)
|
|
308
327
|
|
|
328
|
+
@property
|
|
329
|
+
def transparent(self):
|
|
330
|
+
return any(r.transparent for r in self.render_objects if r.active)
|
|
331
|
+
|
|
309
332
|
|
|
310
333
|
class Renderer(BaseRenderer):
|
|
311
334
|
"""Base class for renderer classes"""
|
|
@@ -320,61 +343,123 @@ class Renderer(BaseRenderer):
|
|
|
320
343
|
select_entry_point: str = "fragment_select_default"
|
|
321
344
|
vertex_buffer_layouts: list[VertexBufferLayout] = []
|
|
322
345
|
vertex_buffers: list[Buffer] = []
|
|
346
|
+
transparent: bool = False
|
|
323
347
|
|
|
324
348
|
_last_bindings: list[BaseBinding] = []
|
|
349
|
+
_last_transparent: bool = False
|
|
350
|
+
_transparent_pipeline = None
|
|
325
351
|
|
|
326
352
|
def create_render_pipeline(self, options: RenderOptions) -> None:
|
|
327
353
|
bindings = options.get_bindings() + self.get_bindings()
|
|
328
354
|
|
|
329
|
-
if bindings == self._last_bindings:
|
|
355
|
+
if bindings == self._last_bindings and self.transparent == self._last_transparent:
|
|
330
356
|
return
|
|
331
357
|
|
|
332
|
-
shader_module = self.device.createShaderModule(self._get_preprocessed_shader_code())
|
|
333
358
|
layout, self.group = create_bind_group(
|
|
334
359
|
self.device, options.get_bindings() + self.get_bindings()
|
|
335
360
|
)
|
|
336
361
|
pipeline_layout = self.device.createPipelineLayout([layout])
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
entryPoint=self.vertex_entry_point,
|
|
340
|
-
buffers=self.vertex_buffer_layouts,
|
|
341
|
-
)
|
|
342
|
-
depth_stencil = DepthStencilState(
|
|
362
|
+
|
|
363
|
+
depth_stencil_opaque = DepthStencilState(
|
|
343
364
|
format=options.canvas.depth_format,
|
|
344
365
|
depthWriteEnabled=True,
|
|
345
366
|
depthCompare=CompareFunction.less,
|
|
346
367
|
depthBias=self.depthBias,
|
|
347
368
|
depthBiasSlopeScale=self.depthBiasSlopeScale,
|
|
348
369
|
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
370
|
+
|
|
371
|
+
if self.transparent:
|
|
372
|
+
opaque_shader = self.device.createShaderModule(
|
|
373
|
+
self._get_preprocessed_shader_code({"OPAQUE_PASS": "1"})
|
|
374
|
+
)
|
|
375
|
+
vertex_state = VertexState(
|
|
376
|
+
module=opaque_shader,
|
|
377
|
+
entryPoint=self.vertex_entry_point,
|
|
378
|
+
buffers=self.vertex_buffer_layouts,
|
|
379
|
+
)
|
|
380
|
+
self.pipeline = self.device.createRenderPipeline(
|
|
381
|
+
pipeline_layout,
|
|
382
|
+
vertex=vertex_state,
|
|
383
|
+
fragment=FragmentState(
|
|
384
|
+
module=opaque_shader,
|
|
385
|
+
entryPoint=self.fragment_entry_point,
|
|
386
|
+
targets=[options.canvas.color_target],
|
|
387
|
+
),
|
|
388
|
+
primitive=PrimitiveState(topology=self.topology),
|
|
389
|
+
depthStencil=depth_stencil_opaque,
|
|
390
|
+
multisample=options.canvas.multisample,
|
|
391
|
+
label=self.label + " (opaque)",
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
depth_stencil_transparent = DepthStencilState(
|
|
395
|
+
format=options.canvas.depth_format,
|
|
396
|
+
depthWriteEnabled=False,
|
|
397
|
+
depthCompare=CompareFunction.less,
|
|
398
|
+
depthBias=self.depthBias,
|
|
399
|
+
depthBiasSlopeScale=self.depthBiasSlopeScale,
|
|
400
|
+
)
|
|
401
|
+
transparent_shader = self.device.createShaderModule(
|
|
402
|
+
self._get_preprocessed_shader_code({"TRANSPARENT_PASS": "1"})
|
|
403
|
+
)
|
|
404
|
+
vertex_state_t = VertexState(
|
|
405
|
+
module=transparent_shader,
|
|
406
|
+
entryPoint=self.vertex_entry_point,
|
|
407
|
+
buffers=self.vertex_buffer_layouts,
|
|
408
|
+
)
|
|
409
|
+
self._transparent_pipeline = self.device.createRenderPipeline(
|
|
410
|
+
pipeline_layout,
|
|
411
|
+
vertex=vertex_state_t,
|
|
412
|
+
fragment=FragmentState(
|
|
413
|
+
module=transparent_shader,
|
|
414
|
+
entryPoint=self.fragment_entry_point,
|
|
415
|
+
targets=[options.canvas.color_target],
|
|
416
|
+
),
|
|
417
|
+
primitive=PrimitiveState(topology=self.topology),
|
|
418
|
+
depthStencil=depth_stencil_transparent,
|
|
419
|
+
multisample=options.canvas.multisample,
|
|
420
|
+
label=self.label + " (transparent)",
|
|
421
|
+
)
|
|
422
|
+
else:
|
|
423
|
+
shader_module = self.device.createShaderModule(self._get_preprocessed_shader_code())
|
|
424
|
+
vertex_state = VertexState(
|
|
353
425
|
module=shader_module,
|
|
354
|
-
entryPoint=self.
|
|
355
|
-
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
426
|
+
entryPoint=self.vertex_entry_point,
|
|
427
|
+
buffers=self.vertex_buffer_layouts,
|
|
428
|
+
)
|
|
429
|
+
self.pipeline = self.device.createRenderPipeline(
|
|
430
|
+
pipeline_layout,
|
|
431
|
+
vertex=vertex_state,
|
|
432
|
+
fragment=FragmentState(
|
|
433
|
+
module=shader_module,
|
|
434
|
+
entryPoint=self.fragment_entry_point,
|
|
435
|
+
targets=[options.canvas.color_target],
|
|
436
|
+
),
|
|
437
|
+
primitive=PrimitiveState(topology=self.topology),
|
|
438
|
+
depthStencil=depth_stencil_opaque,
|
|
439
|
+
multisample=options.canvas.multisample,
|
|
440
|
+
label=self.label,
|
|
441
|
+
)
|
|
442
|
+
self._transparent_pipeline = None
|
|
362
443
|
|
|
363
444
|
if self.select_entry_point:
|
|
364
445
|
select_shader_module = self.device.createShaderModule(
|
|
365
446
|
self._get_preprocessed_shader_code({"SELECT_PIPELINE": "1"})
|
|
366
447
|
)
|
|
367
|
-
|
|
448
|
+
vertex_state_s = VertexState(
|
|
449
|
+
module=select_shader_module,
|
|
450
|
+
entryPoint=self.vertex_entry_point,
|
|
451
|
+
buffers=self.vertex_buffer_layouts,
|
|
452
|
+
)
|
|
368
453
|
self._select_pipeline = self.device.createRenderPipeline(
|
|
369
454
|
pipeline_layout,
|
|
370
|
-
vertex=
|
|
455
|
+
vertex=vertex_state_s,
|
|
371
456
|
fragment=FragmentState(
|
|
372
457
|
module=select_shader_module,
|
|
373
458
|
entryPoint=self.select_entry_point,
|
|
374
459
|
targets=[options.canvas.select_target],
|
|
375
460
|
),
|
|
376
461
|
primitive=PrimitiveState(topology=self.topology),
|
|
377
|
-
depthStencil=
|
|
462
|
+
depthStencil=depth_stencil_opaque,
|
|
378
463
|
multisample=MultisampleState(),
|
|
379
464
|
label=self.label + " (select)",
|
|
380
465
|
)
|
|
@@ -382,6 +467,7 @@ class Renderer(BaseRenderer):
|
|
|
382
467
|
self._select_pipeline = None
|
|
383
468
|
|
|
384
469
|
self._last_bindings = bindings
|
|
470
|
+
self._last_transparent = self.transparent
|
|
385
471
|
|
|
386
472
|
def render(self, options: RenderOptions) -> None:
|
|
387
473
|
render_pass = options.begin_render_pass()
|
|
@@ -392,6 +478,17 @@ class Renderer(BaseRenderer):
|
|
|
392
478
|
render_pass.draw(self.n_vertices, self.n_instances)
|
|
393
479
|
render_pass.end()
|
|
394
480
|
|
|
481
|
+
def render_opaque(self, options: RenderOptions) -> None:
|
|
482
|
+
self.render(options)
|
|
483
|
+
|
|
484
|
+
def render_transparent(self, options: RenderOptions) -> None:
|
|
485
|
+
if not self._transparent_pipeline:
|
|
486
|
+
return
|
|
487
|
+
saved = self.pipeline
|
|
488
|
+
self.pipeline = self._transparent_pipeline
|
|
489
|
+
self.render(options)
|
|
490
|
+
self.pipeline = saved
|
|
491
|
+
|
|
395
492
|
def select(self, options: RenderOptions, x: int, y: int) -> None:
|
|
396
493
|
if not self._select_pipeline:
|
|
397
494
|
return
|
webgpu/scene.py
CHANGED
|
@@ -123,11 +123,18 @@ class Scene:
|
|
|
123
123
|
|
|
124
124
|
def __on_update_html_canvas(self, html_canvas):
|
|
125
125
|
"""Update event wiring when the underlying HTML canvas element changes."""
|
|
126
|
-
self.
|
|
126
|
+
camera = self.options.camera
|
|
127
127
|
if html_canvas is not None:
|
|
128
|
-
|
|
128
|
+
self.input_handler.set_canvas(html_canvas)
|
|
129
129
|
camera.set_render_functions(self.render, self.get_position)
|
|
130
|
+
camera.register_callbacks(self.input_handler)
|
|
130
131
|
camera.set_canvas(self.canvas)
|
|
132
|
+
else:
|
|
133
|
+
camera.unregister_callbacks(self.input_handler)
|
|
134
|
+
if camera._render_function == self.render:
|
|
135
|
+
camera._render_function = None
|
|
136
|
+
camera._get_position_function = None
|
|
137
|
+
self.input_handler.set_canvas(None)
|
|
131
138
|
|
|
132
139
|
def get_position(self, x: int, y: int):
|
|
133
140
|
"""Return the 3D position under canvas pixel (x, y) using the selection buffer."""
|
|
@@ -226,22 +233,27 @@ class Scene:
|
|
|
226
233
|
return ev
|
|
227
234
|
|
|
228
235
|
# @print_communications
|
|
229
|
-
def _render_objects(self, to_canvas=True):
|
|
236
|
+
def _render_objects(self, to_canvas=True, update_pipelines=True):
|
|
230
237
|
"""Update pipelines and render all active objects, optionally copying to the canvas."""
|
|
231
238
|
if self.canvas is None:
|
|
232
239
|
return
|
|
233
|
-
self._select_buffer_valid = False
|
|
234
240
|
options = self.options
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
|
|
242
|
+
if update_pipelines:
|
|
243
|
+
self._select_buffer_valid = False
|
|
244
|
+
for obj in self.render_objects:
|
|
245
|
+
if obj.active:
|
|
246
|
+
obj._update_and_create_render_pipeline(options)
|
|
247
|
+
if obj.needs_update:
|
|
248
|
+
print("warning: object still needs update after update was done:", obj)
|
|
240
249
|
|
|
241
250
|
options.command_encoder = self.device.createCommandEncoder()
|
|
242
251
|
for obj in self.render_objects:
|
|
243
252
|
if obj.active:
|
|
244
|
-
obj.
|
|
253
|
+
obj.render_opaque(options)
|
|
254
|
+
for obj in self.render_objects:
|
|
255
|
+
if obj.active:
|
|
256
|
+
obj.render_transparent(options)
|
|
245
257
|
|
|
246
258
|
if to_canvas:
|
|
247
259
|
target_texture = self.canvas.target_texture
|
|
@@ -266,6 +278,21 @@ class Scene:
|
|
|
266
278
|
self.device.queue.submit([options.command_encoder.finish()])
|
|
267
279
|
options.command_encoder = None
|
|
268
280
|
|
|
281
|
+
def _render_highlight(self):
|
|
282
|
+
"""Fast re-render for highlight-only uniform changes.
|
|
283
|
+
|
|
284
|
+
Skips pipeline rebuild and select buffer invalidation.
|
|
285
|
+
Caller must already hold _render_mutex.
|
|
286
|
+
"""
|
|
287
|
+
if self.canvas is None or self.canvas.height == 0:
|
|
288
|
+
return
|
|
289
|
+
self._render_objects(to_canvas=False, update_pipelines=False)
|
|
290
|
+
platform.js.patchedRequestAnimationFrame(
|
|
291
|
+
self.canvas.device.handle,
|
|
292
|
+
self.canvas.context,
|
|
293
|
+
self.canvas.target_texture,
|
|
294
|
+
)
|
|
295
|
+
|
|
269
296
|
def redraw(self, blocking=False, fps=10):
|
|
270
297
|
"""Request a redraw, either blocking immediately or debounced on the event loop."""
|
|
271
298
|
self.options.timestamp = time.time()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#import camera
|
|
2
|
+
|
|
3
|
+
@group(0) @binding(90) var<storage> u_edges: array<f32>;
|
|
4
|
+
|
|
5
|
+
struct GizmoUniforms {
|
|
6
|
+
corner: vec2f,
|
|
7
|
+
scale: f32,
|
|
8
|
+
thickness: f32,
|
|
9
|
+
};
|
|
10
|
+
@group(0) @binding(93) var<uniform> u_gizmo: GizmoUniforms;
|
|
11
|
+
|
|
12
|
+
struct VertexOutput {
|
|
13
|
+
@builtin(position) position: vec4f,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
@vertex
|
|
17
|
+
fn vertex_main(@builtin(vertex_index) vertId: u32,
|
|
18
|
+
@builtin(instance_index) edgeId: u32) -> VertexOutput {
|
|
19
|
+
var out: VertexOutput;
|
|
20
|
+
|
|
21
|
+
let p1_3d = vec3f(u_edges[edgeId * 6u], u_edges[edgeId * 6u + 1u], u_edges[edgeId * 6u + 2u]);
|
|
22
|
+
let p2_3d = vec3f(u_edges[edgeId * 6u + 3u], u_edges[edgeId * 6u + 4u], u_edges[edgeId * 6u + 5u]);
|
|
23
|
+
|
|
24
|
+
let r1 = (u_camera.rot_mat * vec4f(p1_3d, 0.0)).xyz;
|
|
25
|
+
let r2 = (u_camera.rot_mat * vec4f(p2_3d, 0.0)).xyz;
|
|
26
|
+
|
|
27
|
+
var off1 = r1.xy * u_gizmo.scale;
|
|
28
|
+
var off2 = r2.xy * u_gizmo.scale;
|
|
29
|
+
off1.x /= u_camera.aspect;
|
|
30
|
+
off2.x /= u_camera.aspect;
|
|
31
|
+
let sp1 = u_gizmo.corner + off1;
|
|
32
|
+
let sp2 = u_gizmo.corner + off2;
|
|
33
|
+
|
|
34
|
+
let v = normalize(sp2 - sp1);
|
|
35
|
+
var normal = vec2f(-v.y, v.x) * u_gizmo.thickness;
|
|
36
|
+
|
|
37
|
+
var pos: vec2f;
|
|
38
|
+
var z: f32;
|
|
39
|
+
if (vertId == 0u) {
|
|
40
|
+
pos = sp1 - normal;
|
|
41
|
+
z = r1.z;
|
|
42
|
+
} else if (vertId == 1u) {
|
|
43
|
+
pos = sp1 + normal;
|
|
44
|
+
z = r1.z;
|
|
45
|
+
} else if (vertId == 2u) {
|
|
46
|
+
pos = sp2 - normal;
|
|
47
|
+
z = r2.z;
|
|
48
|
+
} else {
|
|
49
|
+
pos = sp2 + normal;
|
|
50
|
+
z = r2.z;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let depth = 0.01 - z * 0.015;
|
|
54
|
+
out.position = vec4f(pos, depth, 1.0);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@fragment
|
|
59
|
+
fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
|
|
60
|
+
return vec4f(0.15, 0.15, 0.15, 1.0);
|
|
61
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#import camera
|
|
2
|
+
|
|
3
|
+
@group(0) @binding(90) var<storage> u_positions: array<f32>;
|
|
4
|
+
@group(0) @binding(91) var<storage> u_normals: array<f32>;
|
|
5
|
+
@group(0) @binding(92) var<storage> u_colors: array<f32>;
|
|
6
|
+
|
|
7
|
+
struct GizmoUniforms {
|
|
8
|
+
corner: vec2f,
|
|
9
|
+
scale: f32,
|
|
10
|
+
padding: f32,
|
|
11
|
+
};
|
|
12
|
+
@group(0) @binding(93) var<uniform> u_gizmo: GizmoUniforms;
|
|
13
|
+
|
|
14
|
+
struct VertexOutput {
|
|
15
|
+
@builtin(position) position: vec4f,
|
|
16
|
+
@location(0) color: vec4f,
|
|
17
|
+
@location(1) @interpolate(flat) face_index: u32,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
@vertex
|
|
21
|
+
fn vertex_main(@builtin(vertex_index) vid: u32) -> VertexOutput {
|
|
22
|
+
var out: VertexOutput;
|
|
23
|
+
|
|
24
|
+
let pos = vec3f(u_positions[vid * 3u], u_positions[vid * 3u + 1u], u_positions[vid * 3u + 2u]);
|
|
25
|
+
let normal = vec3f(u_normals[vid * 3u], u_normals[vid * 3u + 1u], u_normals[vid * 3u + 2u]);
|
|
26
|
+
let color = vec4f(u_colors[vid * 4u], u_colors[vid * 4u + 1u], u_colors[vid * 4u + 2u], u_colors[vid * 4u + 3u]);
|
|
27
|
+
|
|
28
|
+
let rotated = (u_camera.rot_mat * vec4f(pos, 0.0)).xyz;
|
|
29
|
+
let rot_normal = normalize((u_camera.rot_mat * vec4f(normal, 0.0)).xyz);
|
|
30
|
+
|
|
31
|
+
// Simple directional lighting
|
|
32
|
+
let light_dir = normalize(vec3f(0.4, 0.7, 1.0));
|
|
33
|
+
let ambient = 0.35;
|
|
34
|
+
let diffuse = max(dot(rot_normal, light_dir), 0.0) * 0.55;
|
|
35
|
+
let back_diffuse = max(dot(-rot_normal, light_dir), 0.0) * 0.2;
|
|
36
|
+
let brightness = ambient + diffuse + back_diffuse;
|
|
37
|
+
|
|
38
|
+
// Aspect-correct positioning: only scale the gizmo part, not the corner
|
|
39
|
+
var gizmo_offset = rotated.xy * u_gizmo.scale;
|
|
40
|
+
gizmo_offset.x /= u_camera.aspect;
|
|
41
|
+
let ndc = u_gizmo.corner + gizmo_offset;
|
|
42
|
+
|
|
43
|
+
// Depth: near 0 so gizmo is always in front, but spread for self-occlusion
|
|
44
|
+
let depth = 0.02 - rotated.z * 0.015;
|
|
45
|
+
|
|
46
|
+
out.position = vec4f(ndc, depth, 1.0);
|
|
47
|
+
out.color = vec4f(color.rgb * brightness, color.a);
|
|
48
|
+
out.face_index = vid / 6u;
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@fragment
|
|
53
|
+
fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
|
|
54
|
+
return vec4f(input.color.rgb * input.color.a, input.color.a);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#ifdef SELECT_PIPELINE
|
|
58
|
+
@fragment
|
|
59
|
+
fn fragment_select(input: VertexOutput) -> @location(0) vec4<u32> {
|
|
60
|
+
return vec4<u32>(@RENDER_OBJECT_ID@, bitcast<u32>(input.position.z), 0u, input.face_index);
|
|
61
|
+
}
|
|
62
|
+
#endif SELECT_PIPELINE
|