wgpu: corner axis gizmo + orbit pivot indicator (shared geometry)

Both overlays draw the same three positive-axis rays (origin → +X / +Y
/ +Z) — one screen-space-thick-line shader, one vertex buffer, one
bind group layout. The vertex stage transforms each vertex as
`mvp * (origin + position * arm)` so the same primitive serves both
modes:

  corner: viewport set to a 110×110 px box in the bottom-left,
          camera-orientation ortho MVP, origin=0, arm=1.
  pivot : full viewport, main view-proj, origin=camera_target,
          arm = 30 logical px in world units.

Pipelines

  axis_pivot_pipeline_      — MSAA + depth LessEqual   (α=1)
  axis_pivot_xray_pipeline_ — MSAA + depth GreaterEqual (α=0.30)
  axis_corner_pipeline_     — resolved surface, no depth, sampleCount=1

Pivot renders inside the main MSAA pass after geometry (depth
interaction); corner renders on the resolved surface after the edge
silhouette pass so the laplacian can't darken its lines. The pivot's
two passes — x-ray first then visible — give an occluded-side hint
matching GL's renderPivotIndicator.

Screen-space thick lines

WebGPU has no lineWidth, so each axis is a 2-triangle quad expanded
by `line_width / 2` pixels along the screen-space perpendicular in
the vertex shader. Every vertex carries BOTH endpoints (start, end)
plus `t ∈ {0,1}` and `side ∈ {-1,+1}` so the direction is computed
consistently as `s_end - s_start` regardless of which end the vertex
sits at — an earlier "this vertex vs the other end" formulation
flipped sign at the end vertex and produced a bowtie.

Analytical AA

|side_t| ∈ [0,1] is the perpendicular distance from the line centre.
`smoothstep(1-fwidth, 1, |side_t|)` gives a 1-pixel coverage falloff
at the long edges — gizmos read cleanly even on the resolved-surface
corner pass which has no MSAA.

Pivot visibility

  - orbit / pan drag press → on, release → off
  - wheel zoom            → on with 600 ms afterglow via QTimer

Pole fallback for the corner gizmo's lookAt mirrors buildViewProj's
identical fix (swap Y-up when |pitch| ≥ 89°), so top/bottom standard
views don't degenerate.

Tracked under task #59.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-30 14:04:08 +10:00
parent d4169693c9
commit e6c6df905a
2 changed files with 540 additions and 0 deletions
+38
View File
@@ -27,6 +27,7 @@
#include <QPoint>
#include <QSet>
#include <QString>
#include <QTimer>
#include <webgpu/webgpu.h>
@@ -218,6 +219,19 @@ private:
bool buildHizPipeline();
bool buildEdgePipeline();
void encodeEdgePass(WGPUCommandEncoder enc, WGPUTextureView surface_view);
bool buildAxisIndicator();
// Encode the pivot indicator inside the main MSAA render pass — runs
// after geometry so depth interaction is correct.
void encodePivotIndicator(WGPURenderPassEncoder pass,
const QMatrix4x4& view_proj);
// Encode the corner gizmo on the resolved single-sample surface,
// after the edge silhouette pass. Uses setViewport for the corner box.
void encodeCornerAxisGizmo(WGPUCommandEncoder enc,
WGPUTextureView surface_view);
// Show/hide the pivot indicator. hide_after_ms > 0 starts the
// single-shot auto-hide timer used by the wheel-zoom afterglow;
// drag callers pass 0 and toggle manually on press/release.
void setPivotIndicatorVisible(bool visible, int hide_after_ms = 0);
void releaseEdgeResources();
bool buildPickPipeline();
@@ -399,6 +413,30 @@ private:
WGPUBindGroup edge_bind_group_ = nullptr;
bool edges_enabled_ = true;
// Axis indicator overlay — drives both the corner gizmo and the
// orbit pivot indicator from the SAME 6-vertex unit cross + shader.
// One uniform buffer with three 256-byte-aligned slots
// (slot 0 = corner, slot 1 = pivot visible, slot 2 = pivot x-ray),
// one bind group with a dynamic offset, three pipelines that differ
// only in render-target / depth setup:
// axis_pivot_pipeline_: MSAA + depth LessEqual (visible α=1)
// axis_pivot_xray_pipeline_: MSAA + depth GreaterEqual (occluded α≈0.3)
// axis_corner_pipeline_: resolved, no depth, sampleCount=1
// The pivot's x-ray pass renders first so the visible pass overdraws
// it where geometry isn't in the way. Matches GL's renderPivotIndicator.
WGPUShaderModule axis_shader_module_ = nullptr;
WGPUBindGroupLayout axis_bgl_ = nullptr;
WGPUPipelineLayout axis_pipeline_layout_ = nullptr;
WGPURenderPipeline axis_pivot_pipeline_ = nullptr;
WGPURenderPipeline axis_pivot_xray_pipeline_ = nullptr;
WGPURenderPipeline axis_corner_pipeline_ = nullptr;
WGPUBuffer axis_vertex_buffer_ = nullptr; // 6 × 24 B
WGPUBuffer axis_uniform_buffer_ = nullptr; // 3 × 256 B slots
WGPUBindGroup axis_bind_group_ = nullptr; // dynamic-offset
static constexpr uint32_t kAxisUniformSlotSize = 256;
bool pivot_indicator_visible_ = false;
QTimer* pivot_indicator_hide_timer_ = nullptr;
// Pick pass (stage 4). Single-sample R32UInt target + depth, vertex-
// pulled from the same visible_draws / instances buffers as the main
// pass — pick fragment outputs the instance's object_id. The pick