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/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 = False, shift: bool | None = False, ctrl: bool | None = False
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 = False,
65
- shift: bool | None = False,
66
- ctrl: bool | None = False,
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 = False, shift: bool | None = False, ctrl: bool | None = False
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 = False, shift: bool | None = False, ctrl: bool | None = False
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 = False, shift: bool | None = False, ctrl: bool | None = False
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 = False, shift: bool | None = False, ctrl: bool | None = False
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 = False, shift: bool | None = False, ctrl: bool | None = False
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 = False, shift: bool | None = False, ctrl: bool | None = False
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 = False, shift: bool | None = False, ctrl: bool | None = False
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 = False, shift: bool | None = False, ctrl: bool | None = False
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: horizontal alignment of the labels. Can be one of: bottom, b, center, c, top, t
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
- ("length", np.uint16),
64
- ("apply_camera", np.uint8),
65
- ("alignment", np.uint8),
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
- "center": 1,
73
- "r": 2,
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(range(len(self.labels)), self.labels, self.positions):
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
- apply_camera = (
96
- self.apply_camera if isinstance(self.apply_camera, bool) else self.apply_camera[i]
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]["length"] = len(label)
104
- text_data[i]["apply_camera"] = apply_camera
105
- text_data[i]["alignment"] = align
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([len(self.labels)], dtype=np.uint32).tobytes()
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.render(options)
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
- vertex_state = VertexState(
338
- module=shader_module,
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
- self.pipeline = self.device.createRenderPipeline(
350
- pipeline_layout,
351
- vertex=vertex_state,
352
- fragment=FragmentState(
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.fragment_entry_point,
355
- targets=[options.canvas.color_target],
356
- ),
357
- primitive=PrimitiveState(topology=self.topology),
358
- depthStencil=depth_stencil,
359
- multisample=options.canvas.multisample,
360
- label=self.label,
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
- vertex_state.module = select_shader_module
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=vertex_state,
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=depth_stencil,
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.input_handler.set_canvas(html_canvas)
126
+ camera = self.options.camera
127
127
  if html_canvas is not None:
128
- camera = self.options.camera
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
- for obj in self.render_objects:
236
- if obj.active:
237
- obj._update_and_create_render_pipeline(options)
238
- if obj.needs_update:
239
- print("warning: object still needs update after update was done:", obj)
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.render(options)
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