mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
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:
@@ -1546,6 +1546,7 @@ bool WgpuViewportWindow::initWgpu() {
|
||||
if (!buildPipelines()) return false;
|
||||
if (!buildHizPipeline()) return false;
|
||||
if (!buildEdgePipeline()) return false;
|
||||
if (!buildAxisIndicator()) return false;
|
||||
if (!buildPickPipeline()) return false;
|
||||
|
||||
qInfo() << "wgpu init OK; surface format =" << int(surface_format_);
|
||||
@@ -2094,12 +2095,497 @@ void WgpuViewportWindow::encodeEdgePass(WGPUCommandEncoder enc,
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Axis indicator: corner gizmo + orbit pivot
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// One shader and one 6-vertex unit-cross VBO drive both the camera-orientation
|
||||
// gizmo in the bottom-left and the orbit-pivot marker at camera_target. The
|
||||
// vertex shader transforms each vertex as `mvp * (origin + position * arm)`
|
||||
// so the same primitive serves both:
|
||||
//
|
||||
// corner: mvp = ortho × lookAt(camera_dir) — camera-orientation only,
|
||||
// origin = 0, arm = 1, viewport set to a small corner box.
|
||||
// pivot : mvp = main view-proj,
|
||||
// origin = camera_target, arm = 30 logical px in world units.
|
||||
//
|
||||
// Two pipelines: pivot draws inside the main MSAA render pass after geometry
|
||||
// so depth interaction is correct; corner draws on the resolved surface after
|
||||
// the edge silhouette pass so the laplacian can't darken its lines.
|
||||
|
||||
static const char* AXIS_WGSL = R"(
|
||||
struct AxisUniforms {
|
||||
mvp: mat4x4<f32>,
|
||||
origin: vec3<f32>,
|
||||
arm: f32,
|
||||
alpha: f32,
|
||||
line_width_px: f32,
|
||||
viewport_size: vec2<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: AxisUniforms;
|
||||
|
||||
struct VsOut {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) color: vec3<f32>,
|
||||
@location(1) alpha: f32,
|
||||
// Signed perpendicular position across the quad (±1 at the long
|
||||
// edges, 0 down the line's centre). The rasterizer interpolates it,
|
||||
// and the fragment shader uses |side_t| + fwidth() as a 1-pixel
|
||||
// smoothstep so the line has analytically anti-aliased edges
|
||||
// without needing MSAA on the corner gizmo's resolved-surface pass.
|
||||
@location(2) side_t: f32,
|
||||
};
|
||||
|
||||
// Each axis is a 4-vertex quad expanded in SCREEN space from a notional
|
||||
// line segment. Every vertex carries BOTH endpoints (start, end) so the
|
||||
// screen-space direction is computed consistently as `s_end - s_start`
|
||||
// regardless of which end this vertex sits at. `t` selects which end
|
||||
// (0 = start, 1 = end) for the base projected point; `side` is +1/-1
|
||||
// for the two sides of the perpendicular offset. Computing direction
|
||||
// from this vertex to the other (a previous design) flipped sign at
|
||||
// the end → the quad became a bowtie. `clip.w` is preserved so depth
|
||||
// interpolation stays correct.
|
||||
@vertex
|
||||
fn vs_main(@location(0) start: vec3<f32>,
|
||||
@location(1) end: vec3<f32>,
|
||||
@location(2) col: vec3<f32>,
|
||||
@location(3) t: f32,
|
||||
@location(4) side: f32) -> VsOut {
|
||||
let p_start = u.mvp * vec4<f32>(u.origin + start * u.arm, 1.0);
|
||||
let p_end = u.mvp * vec4<f32>(u.origin + end * u.arm, 1.0);
|
||||
let p_here = mix(p_start, p_end, t);
|
||||
|
||||
let s_start = (p_start.xy / p_start.w) * u.viewport_size * 0.5;
|
||||
let s_end = (p_end.xy / p_end.w ) * u.viewport_size * 0.5;
|
||||
let dir = normalize(s_end - s_start);
|
||||
let perp = vec2<f32>(-dir.y, dir.x);
|
||||
|
||||
let off_pixels = perp * (u.line_width_px * 0.5) * side;
|
||||
let off_ndc = off_pixels * 2.0 / u.viewport_size;
|
||||
var out: VsOut;
|
||||
out.clip_pos = vec4<f32>(p_here.xy + off_ndc * p_here.w,
|
||||
p_here.zw);
|
||||
out.color = col;
|
||||
out.alpha = u.alpha;
|
||||
out.side_t = side;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
// |side_t| is 0 at line centre, 1 at the long edges. fwidth gives
|
||||
// the per-pixel change — smoothing across that range gives a single
|
||||
// pixel of analytical AA along the perpendicular.
|
||||
let d = abs(in.side_t);
|
||||
let aa = fwidth(in.side_t);
|
||||
let coverage = 1.0 - smoothstep(1.0 - aa, 1.0, d);
|
||||
return vec4<f32>(in.color, in.alpha * coverage);
|
||||
}
|
||||
)";
|
||||
|
||||
bool WgpuViewportWindow::buildAxisIndicator() {
|
||||
// Vertex buffer: three positive-axis rays, each expanded into a
|
||||
// 4-corner quad (6 vertices in triangle-list order) so the vertex
|
||||
// shader can offset by `line_width / 2` pixels in screen space.
|
||||
// Per vertex (11 floats = 44 bytes):
|
||||
// start (vec3) — the line's start endpoint (same for all 6 verts of an axis)
|
||||
// end (vec3) — the line's end endpoint (same for all 6 verts of an axis)
|
||||
// col (vec3) — RGB
|
||||
// t (f32) — 0 if this vertex sits at `start`, 1 if at `end`
|
||||
// side (f32) — -1 or +1, which half of the perpendicular
|
||||
//
|
||||
// The 6-vertex ordering for the two triangles of each quad is
|
||||
// (start,-1) (start,+1) (end,-1) (end,-1) (start,+1) (end,+1)
|
||||
// — a standard triangle-list two-tri quad split.
|
||||
static const float axis_verts[] = {
|
||||
// start end color (RGB) t side
|
||||
// ---- +X (red) — start=(0,0,0), end=(1,0,0) ----
|
||||
0,0,0, 1,0,0, 1.00f, 0.15f, 0.15f, 0.f, -1.f,
|
||||
0,0,0, 1,0,0, 1.00f, 0.15f, 0.15f, 0.f, +1.f,
|
||||
0,0,0, 1,0,0, 1.00f, 0.15f, 0.15f, 1.f, -1.f,
|
||||
0,0,0, 1,0,0, 1.00f, 0.15f, 0.15f, 1.f, -1.f,
|
||||
0,0,0, 1,0,0, 1.00f, 0.15f, 0.15f, 0.f, +1.f,
|
||||
0,0,0, 1,0,0, 1.00f, 0.15f, 0.15f, 1.f, +1.f,
|
||||
|
||||
// ---- +Y (green) — start=(0,0,0), end=(0,1,0) ----
|
||||
0,0,0, 0,1,0, 0.15f, 0.85f, 0.20f, 0.f, -1.f,
|
||||
0,0,0, 0,1,0, 0.15f, 0.85f, 0.20f, 0.f, +1.f,
|
||||
0,0,0, 0,1,0, 0.15f, 0.85f, 0.20f, 1.f, -1.f,
|
||||
0,0,0, 0,1,0, 0.15f, 0.85f, 0.20f, 1.f, -1.f,
|
||||
0,0,0, 0,1,0, 0.15f, 0.85f, 0.20f, 0.f, +1.f,
|
||||
0,0,0, 0,1,0, 0.15f, 0.85f, 0.20f, 1.f, +1.f,
|
||||
|
||||
// ---- +Z (blue) — start=(0,0,0), end=(0,0,1) ----
|
||||
0,0,0, 0,0,1, 0.20f, 0.40f, 1.00f, 0.f, -1.f,
|
||||
0,0,0, 0,0,1, 0.20f, 0.40f, 1.00f, 0.f, +1.f,
|
||||
0,0,0, 0,0,1, 0.20f, 0.40f, 1.00f, 1.f, -1.f,
|
||||
0,0,0, 0,0,1, 0.20f, 0.40f, 1.00f, 1.f, -1.f,
|
||||
0,0,0, 0,0,1, 0.20f, 0.40f, 1.00f, 0.f, +1.f,
|
||||
0,0,0, 0,0,1, 0.20f, 0.40f, 1.00f, 1.f, +1.f,
|
||||
};
|
||||
{
|
||||
WGPUBufferDescriptor bdesc = {};
|
||||
bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
|
||||
bdesc.size = sizeof(axis_verts);
|
||||
bdesc.label = svFromCStr("ifcviewer-wgpu.axis_vbo");
|
||||
axis_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc);
|
||||
wgpuQueueWriteBuffer(queue_, axis_vertex_buffer_, 0, axis_verts, sizeof(axis_verts));
|
||||
}
|
||||
|
||||
// Uniform buffer: three 256-byte-aligned slots
|
||||
// (slot 0 = corner, slot 1 = pivot visible, slot 2 = pivot x-ray)
|
||||
// addressed via a dynamic offset on the bind group.
|
||||
{
|
||||
WGPUBufferDescriptor bdesc = {};
|
||||
bdesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
|
||||
bdesc.size = 3u * kAxisUniformSlotSize;
|
||||
bdesc.label = svFromCStr("ifcviewer-wgpu.axis_uniforms");
|
||||
axis_uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc);
|
||||
}
|
||||
|
||||
// Bind group layout: single uniform with dynamic offset.
|
||||
{
|
||||
WGPUBindGroupLayoutEntry entry = {};
|
||||
entry.binding = 0;
|
||||
entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
|
||||
entry.buffer.type = WGPUBufferBindingType_Uniform;
|
||||
entry.buffer.hasDynamicOffset = 1;
|
||||
entry.buffer.minBindingSize = 96; // mat4 + vec3 + f32 + f32 + f32 + vec2 (struct size = 96)
|
||||
WGPUBindGroupLayoutDescriptor bgl_desc = {};
|
||||
bgl_desc.entryCount = 1;
|
||||
bgl_desc.entries = &entry;
|
||||
bgl_desc.label = svFromCStr("ifcviewer-wgpu.axis_bgl");
|
||||
axis_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc);
|
||||
}
|
||||
|
||||
// Pipeline layout.
|
||||
{
|
||||
WGPUPipelineLayoutDescriptor pl_desc = {};
|
||||
pl_desc.bindGroupLayoutCount = 1;
|
||||
pl_desc.bindGroupLayouts = &axis_bgl_;
|
||||
pl_desc.label = svFromCStr("ifcviewer-wgpu.axis_pipeline_layout");
|
||||
axis_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc);
|
||||
}
|
||||
|
||||
// Bind group: one binding pointing at slot 0 (size = slot size; offset
|
||||
// selected dynamically at setBindGroup time).
|
||||
{
|
||||
WGPUBindGroupEntry entry = {};
|
||||
entry.binding = 0;
|
||||
entry.buffer = axis_uniform_buffer_;
|
||||
entry.offset = 0;
|
||||
entry.size = kAxisUniformSlotSize;
|
||||
WGPUBindGroupDescriptor bg_desc = {};
|
||||
bg_desc.layout = axis_bgl_;
|
||||
bg_desc.entryCount = 1;
|
||||
bg_desc.entries = &entry;
|
||||
bg_desc.label = svFromCStr("ifcviewer-wgpu.axis_bind_group");
|
||||
axis_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc);
|
||||
}
|
||||
|
||||
// Shader module (shared between both pipelines).
|
||||
{
|
||||
WGPUShaderSourceWGSL wgsl_src = {};
|
||||
wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
|
||||
wgsl_src.code = svFromCStr(AXIS_WGSL);
|
||||
WGPUShaderModuleDescriptor sm_desc = {};
|
||||
sm_desc.nextInChain = &wgsl_src.chain;
|
||||
sm_desc.label = svFromCStr("ifcviewer-wgpu.axis_wgsl");
|
||||
axis_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc);
|
||||
}
|
||||
|
||||
// Vertex layout: start(vec3) + end(vec3) + col(vec3) + t(f32) + side(f32),
|
||||
// interleaved, stride 44.
|
||||
WGPUVertexAttribute attribs[5] = {};
|
||||
attribs[0].format = WGPUVertexFormat_Float32x3; // start
|
||||
attribs[0].offset = 0;
|
||||
attribs[0].shaderLocation = 0;
|
||||
attribs[1].format = WGPUVertexFormat_Float32x3; // end
|
||||
attribs[1].offset = 12;
|
||||
attribs[1].shaderLocation = 1;
|
||||
attribs[2].format = WGPUVertexFormat_Float32x3; // color
|
||||
attribs[2].offset = 24;
|
||||
attribs[2].shaderLocation = 2;
|
||||
attribs[3].format = WGPUVertexFormat_Float32; // t
|
||||
attribs[3].offset = 36;
|
||||
attribs[3].shaderLocation = 3;
|
||||
attribs[4].format = WGPUVertexFormat_Float32; // side
|
||||
attribs[4].offset = 40;
|
||||
attribs[4].shaderLocation = 4;
|
||||
WGPUVertexBufferLayout vbl = {};
|
||||
vbl.arrayStride = 44;
|
||||
vbl.stepMode = WGPUVertexStepMode_Vertex;
|
||||
vbl.attributeCount = 5;
|
||||
vbl.attributes = attribs;
|
||||
|
||||
// Standard alpha blend so the corner gizmo can soften over the resolved
|
||||
// background and the pivot can fade against scene colour.
|
||||
WGPUBlendState blend = {};
|
||||
blend.color.srcFactor = WGPUBlendFactor_SrcAlpha;
|
||||
blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.color.operation = WGPUBlendOperation_Add;
|
||||
blend.alpha.srcFactor = WGPUBlendFactor_One;
|
||||
blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.alpha.operation = WGPUBlendOperation_Add;
|
||||
|
||||
// ---- Pivot pipelines: MSAA color + depth, both x-ray + visible ----
|
||||
// The two pipelines differ only in depthCompare: GreaterEqual lets the
|
||||
// x-ray pass land only on pixels where the scene's depth is AT or NEARER
|
||||
// than the pivot (so we know the pivot is behind something there); the
|
||||
// visible pass uses LessEqual to land where the pivot is in front. We
|
||||
// build them in a small helper-like inline block to share state.
|
||||
auto build_pivot = [&](WGPUCompareFunction cmp, const char* label,
|
||||
WGPURenderPipeline& out) {
|
||||
WGPUColorTargetState ct = {};
|
||||
ct.format = surface_format_;
|
||||
ct.blend = &blend;
|
||||
ct.writeMask = WGPUColorWriteMask_All;
|
||||
|
||||
WGPUFragmentState frag = {};
|
||||
frag.module = axis_shader_module_;
|
||||
frag.entryPoint = svFromCStr("fs_main");
|
||||
frag.targetCount = 1;
|
||||
frag.targets = &ct;
|
||||
|
||||
WGPUDepthStencilState depth = {};
|
||||
depth.format = WGPUTextureFormat_Depth32Float;
|
||||
depth.depthWriteEnabled = WGPUOptionalBool_False;
|
||||
depth.depthCompare = cmp;
|
||||
depth.stencilFront.compare = WGPUCompareFunction_Always;
|
||||
depth.stencilBack.compare = WGPUCompareFunction_Always;
|
||||
|
||||
WGPURenderPipelineDescriptor rp_desc = {};
|
||||
rp_desc.layout = axis_pipeline_layout_;
|
||||
rp_desc.label = svFromCStr(label);
|
||||
rp_desc.vertex.module = axis_shader_module_;
|
||||
rp_desc.vertex.entryPoint = svFromCStr("vs_main");
|
||||
rp_desc.vertex.bufferCount = 1;
|
||||
rp_desc.vertex.buffers = &vbl;
|
||||
rp_desc.fragment = &frag;
|
||||
rp_desc.depthStencil = &depth;
|
||||
rp_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
rp_desc.primitive.cullMode = WGPUCullMode_None;
|
||||
rp_desc.multisample.count = SAMPLE_COUNT;
|
||||
rp_desc.multisample.mask = 0xFFFFFFFFu;
|
||||
out = wgpuDeviceCreateRenderPipeline(device_, &rp_desc);
|
||||
};
|
||||
build_pivot(WGPUCompareFunction_LessEqual,
|
||||
"ifcviewer-wgpu.axis_pivot_pipeline",
|
||||
axis_pivot_pipeline_);
|
||||
build_pivot(WGPUCompareFunction_GreaterEqual,
|
||||
"ifcviewer-wgpu.axis_pivot_xray_pipeline",
|
||||
axis_pivot_xray_pipeline_);
|
||||
|
||||
// ---- Corner pipeline: resolved surface, no depth, sampleCount=1 ----
|
||||
{
|
||||
WGPUColorTargetState ct = {};
|
||||
ct.format = surface_format_;
|
||||
ct.blend = &blend;
|
||||
ct.writeMask = WGPUColorWriteMask_All;
|
||||
|
||||
WGPUFragmentState frag = {};
|
||||
frag.module = axis_shader_module_;
|
||||
frag.entryPoint = svFromCStr("fs_main");
|
||||
frag.targetCount = 1;
|
||||
frag.targets = &ct;
|
||||
|
||||
WGPURenderPipelineDescriptor rp_desc = {};
|
||||
rp_desc.layout = axis_pipeline_layout_;
|
||||
rp_desc.label = svFromCStr("ifcviewer-wgpu.axis_corner_pipeline");
|
||||
rp_desc.vertex.module = axis_shader_module_;
|
||||
rp_desc.vertex.entryPoint = svFromCStr("vs_main");
|
||||
rp_desc.vertex.bufferCount = 1;
|
||||
rp_desc.vertex.buffers = &vbl;
|
||||
rp_desc.fragment = &frag;
|
||||
rp_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
rp_desc.primitive.cullMode = WGPUCullMode_None;
|
||||
rp_desc.multisample.count = 1;
|
||||
rp_desc.multisample.mask = 0xFFFFFFFFu;
|
||||
axis_corner_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc);
|
||||
}
|
||||
|
||||
return axis_pivot_pipeline_ && axis_pivot_xray_pipeline_
|
||||
&& axis_corner_pipeline_;
|
||||
}
|
||||
|
||||
// Pack a 256-byte uniform slot. Layout matches the WGSL AxisUniforms
|
||||
// struct's natural alignment: mat4 + vec3 + f32 + f32 + f32 + vec2.
|
||||
static void packAxisUniform(uint8_t* dst,
|
||||
const QMatrix4x4& mvp,
|
||||
const QVector3D& origin,
|
||||
float arm, float alpha,
|
||||
float line_width_px,
|
||||
float viewport_w, float viewport_h) {
|
||||
std::memset(dst, 0, 256);
|
||||
std::memcpy(dst, mvp.constData(), 16 * sizeof(float)); // 0..64 mvp
|
||||
float ox = origin.x(), oy = origin.y(), oz = origin.z();
|
||||
std::memcpy(dst + 64, &ox, sizeof(float)); // 64..68 origin.x
|
||||
std::memcpy(dst + 68, &oy, sizeof(float)); // 68..72 origin.y
|
||||
std::memcpy(dst + 72, &oz, sizeof(float)); // 72..76 origin.z
|
||||
std::memcpy(dst + 76, &arm, sizeof(float)); // 76..80 arm
|
||||
std::memcpy(dst + 80, &alpha, sizeof(float)); // 80..84 alpha
|
||||
std::memcpy(dst + 84, &line_width_px, sizeof(float)); // 84..88 line_width_px
|
||||
std::memcpy(dst + 88, &viewport_w, sizeof(float)); // 88..92 viewport.x
|
||||
std::memcpy(dst + 92, &viewport_h, sizeof(float)); // 92..96 viewport.y
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::encodePivotIndicator(WGPURenderPassEncoder pass,
|
||||
const QMatrix4x4& view_proj) {
|
||||
if (!pivot_indicator_visible_ || !axis_pivot_pipeline_
|
||||
|| !axis_pivot_xray_pipeline_) return;
|
||||
if (configured_h_ <= 0) return;
|
||||
|
||||
// Arm length = 30 logical px projected into world space at the pivot's
|
||||
// distance. world_per_pixel matches GL's renderPivotIndicator math:
|
||||
// 2 · d · tan(fovy/2) / viewport_h (ortho's box height collapses to the
|
||||
// same formula because we size it from camera_distance at the pivot).
|
||||
const float fovy_rad = qDegreesToRadians(camera_fov_y_deg_);
|
||||
const float world_per_pixel = camera_distance_ * std::tan(fovy_rad * 0.5f)
|
||||
* 2.0f / float(configured_h_);
|
||||
const float arm_pixels = 30.0f * float(devicePixelRatio());
|
||||
const float arm_world = arm_pixels * world_per_pixel;
|
||||
|
||||
const QVector3D origin(camera_target_[0], camera_target_[1], camera_target_[2]);
|
||||
|
||||
// Slot 1 = visible (α=1), slot 2 = x-ray (α=0.30). Identical except for
|
||||
// alpha — write both so a single bind group can dispatch both draws.
|
||||
const float dpr = float(devicePixelRatio());
|
||||
const float line_w = 2.5f * dpr;
|
||||
const float vw = float(configured_w_);
|
||||
const float vh = float(configured_h_);
|
||||
uint8_t slot_visible[256];
|
||||
uint8_t slot_xray[256];
|
||||
packAxisUniform(slot_visible, view_proj, origin, arm_world, 1.00f, line_w, vw, vh);
|
||||
packAxisUniform(slot_xray, view_proj, origin, arm_world, 0.30f, line_w, vw, vh);
|
||||
const uint32_t visible_off = 1u * kAxisUniformSlotSize;
|
||||
const uint32_t xray_off = 2u * kAxisUniformSlotSize;
|
||||
wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, visible_off,
|
||||
slot_visible, sizeof(slot_visible));
|
||||
wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, xray_off,
|
||||
slot_xray, sizeof(slot_xray));
|
||||
|
||||
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, axis_vertex_buffer_, 0,
|
||||
WGPU_WHOLE_SIZE);
|
||||
|
||||
// Pass 1: dim x-ray for the occluded portion.
|
||||
wgpuRenderPassEncoderSetPipeline(pass, axis_pivot_xray_pipeline_);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &xray_off);
|
||||
wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0);
|
||||
|
||||
// Pass 2: full-alpha visible portion.
|
||||
wgpuRenderPassEncoderSetPipeline(pass, axis_pivot_pipeline_);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &visible_off);
|
||||
wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0);
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::encodeCornerAxisGizmo(WGPUCommandEncoder enc,
|
||||
WGPUTextureView surface_view) {
|
||||
if (!axis_corner_pipeline_ || !surface_view) return;
|
||||
const int dpr = std::max(1, int(devicePixelRatio()));
|
||||
const uint32_t gizmo_size = uint32_t(110 * dpr);
|
||||
const uint32_t margin = uint32_t(10 * dpr);
|
||||
if (gizmo_size == 0 || configured_w_ <= 0 || configured_h_ <= 0) return;
|
||||
// Pin to the BOTTOM-LEFT corner. WebGPU framebuffer y goes top→bottom,
|
||||
// so a bottom-left corner sits at y = height - margin - gizmo_size.
|
||||
const uint32_t fb_h = uint32_t(configured_h_);
|
||||
if (gizmo_size + margin > fb_h) return;
|
||||
const uint32_t y = fb_h - margin - gizmo_size;
|
||||
|
||||
// Build the gizmo's MVP: a small ortho box looking at the origin from
|
||||
// the camera's direction. The eye direction matches GL's renderAxisGizmo
|
||||
// exactly (yaw/pitch → unit forward, world-up = +Z). Near the poles the
|
||||
// up axis collapses against the look direction, so swap to Y-up there —
|
||||
// mirrors buildViewProj's identical fallback for the main projection.
|
||||
const float yaw_rad = qDegreesToRadians(camera_yaw_deg_);
|
||||
const float pitch_rad = qDegreesToRadians(camera_pitch_deg_);
|
||||
const QVector3D eye_dir(std::cos(pitch_rad) * std::cos(yaw_rad),
|
||||
std::cos(pitch_rad) * std::sin(yaw_rad),
|
||||
std::sin(pitch_rad));
|
||||
const QVector3D world_up = (std::abs(camera_pitch_deg_) >= 89.0f)
|
||||
? QVector3D(0.0f, 1.0f, 0.0f)
|
||||
: QVector3D(0.0f, 0.0f, 1.0f);
|
||||
QMatrix4x4 gv;
|
||||
gv.lookAt(eye_dir * 3.0f, QVector3D(0, 0, 0), world_up);
|
||||
QMatrix4x4 gp;
|
||||
gp.ortho(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f);
|
||||
// Match the main projection's z-remap so the gizmo's NDC z falls in
|
||||
// [0, 1] (WebGPU) rather than [-1, 1] (Qt's GL-style projection).
|
||||
QMatrix4x4 z_remap;
|
||||
z_remap(2, 2) = 0.5f;
|
||||
z_remap(2, 3) = 0.5f;
|
||||
const QMatrix4x4 mvp = z_remap * gp * gv;
|
||||
|
||||
// line_width and viewport_size are in the gizmo's local viewport (not
|
||||
// the framebuffer), because the vertex-shader perpendicular offset is
|
||||
// computed in NDC and NDC is per-viewport.
|
||||
uint8_t slot[256];
|
||||
const float line_w = 2.5f * float(dpr);
|
||||
packAxisUniform(slot, mvp, QVector3D(0, 0, 0), 1.0f, 1.0f, line_w,
|
||||
float(gizmo_size), float(gizmo_size));
|
||||
const uint32_t slot_offset = 0u; // corner lives in slot 0
|
||||
wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, slot_offset, slot, sizeof(slot));
|
||||
|
||||
WGPURenderPassColorAttachment color = {};
|
||||
color.view = surface_view;
|
||||
color.loadOp = WGPULoadOp_Load; // preserve what was drawn before
|
||||
color.storeOp = WGPUStoreOp_Store;
|
||||
color.clearValue = { 0.0, 0.0, 0.0, 1.0 };
|
||||
color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
|
||||
|
||||
WGPURenderPassDescriptor pass_desc = {};
|
||||
pass_desc.colorAttachmentCount = 1;
|
||||
pass_desc.colorAttachments = &color;
|
||||
pass_desc.label = svFromCStr("ifcviewer-wgpu.corner_axis_pass");
|
||||
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc);
|
||||
wgpuRenderPassEncoderSetViewport(pass, float(margin), float(y),
|
||||
float(gizmo_size), float(gizmo_size),
|
||||
0.0f, 1.0f);
|
||||
wgpuRenderPassEncoderSetPipeline(pass, axis_corner_pipeline_);
|
||||
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, axis_vertex_buffer_, 0,
|
||||
WGPU_WHOLE_SIZE);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &slot_offset);
|
||||
wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0);
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::setPivotIndicatorVisible(bool visible, int hide_after_ms) {
|
||||
if (!pivot_indicator_hide_timer_) {
|
||||
pivot_indicator_hide_timer_ = new QTimer(this);
|
||||
pivot_indicator_hide_timer_->setSingleShot(true);
|
||||
QObject::connect(pivot_indicator_hide_timer_, &QTimer::timeout, this,
|
||||
[this]() {
|
||||
pivot_indicator_visible_ = false;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
pivot_indicator_visible_ = visible;
|
||||
if (visible && hide_after_ms > 0) {
|
||||
pivot_indicator_hide_timer_->start(hide_after_ms);
|
||||
} else {
|
||||
pivot_indicator_hide_timer_->stop();
|
||||
}
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::releaseEdgeResources() {
|
||||
if (edge_bind_group_) { wgpuBindGroupRelease(edge_bind_group_); edge_bind_group_ = nullptr; }
|
||||
if (edge_pipeline_) { wgpuRenderPipelineRelease(edge_pipeline_); edge_pipeline_ = nullptr; }
|
||||
if (edge_shader_module_) { wgpuShaderModuleRelease(edge_shader_module_);edge_shader_module_ = nullptr; }
|
||||
if (edge_pipeline_layout_) { wgpuPipelineLayoutRelease(edge_pipeline_layout_); edge_pipeline_layout_ = nullptr; }
|
||||
if (edge_bgl_) { wgpuBindGroupLayoutRelease(edge_bgl_); edge_bgl_ = nullptr; }
|
||||
|
||||
if (axis_bind_group_) { wgpuBindGroupRelease(axis_bind_group_); axis_bind_group_ = nullptr; }
|
||||
if (axis_pivot_pipeline_) { wgpuRenderPipelineRelease(axis_pivot_pipeline_); axis_pivot_pipeline_ = nullptr; }
|
||||
if (axis_pivot_xray_pipeline_){ wgpuRenderPipelineRelease(axis_pivot_xray_pipeline_); axis_pivot_xray_pipeline_ = nullptr; }
|
||||
if (axis_corner_pipeline_) { wgpuRenderPipelineRelease(axis_corner_pipeline_); axis_corner_pipeline_ = nullptr; }
|
||||
if (axis_shader_module_) { wgpuShaderModuleRelease(axis_shader_module_); axis_shader_module_ = nullptr; }
|
||||
if (axis_pipeline_layout_) { wgpuPipelineLayoutRelease(axis_pipeline_layout_); axis_pipeline_layout_ = nullptr; }
|
||||
if (axis_bgl_) { wgpuBindGroupLayoutRelease(axis_bgl_); axis_bgl_ = nullptr; }
|
||||
if (axis_uniform_buffer_) { wgpuBufferRelease(axis_uniform_buffer_); axis_uniform_buffer_ = nullptr; }
|
||||
if (axis_vertex_buffer_) { wgpuBufferRelease(axis_vertex_buffer_); axis_vertex_buffer_ = nullptr; }
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -3315,6 +3801,11 @@ void WgpuViewportWindow::render() {
|
||||
}
|
||||
}
|
||||
|
||||
// Pivot indicator. Encoded inside the main MSAA pass after geometry so
|
||||
// depth interaction is correct — the indicator vanishes behind closer
|
||||
// surfaces. Visibility is driven by orbit/wheel UI handlers.
|
||||
encodePivotIndicator(pass, vp_this_frame);
|
||||
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
|
||||
@@ -3325,6 +3816,10 @@ void WgpuViewportWindow::render() {
|
||||
encodeEdgePass(enc, view);
|
||||
}
|
||||
|
||||
// Corner axis gizmo. Encoded after the edge pass on the resolved
|
||||
// surface, so the laplacian can't darken its lines or its background.
|
||||
encodeCornerAxisGizmo(enc, view);
|
||||
|
||||
// ---- HiZ: resolve MSAA depth → small single-sample → ping-pong slot
|
||||
int hiz_submitted_slot = -1;
|
||||
if (hiz_enabled_) {
|
||||
@@ -5263,9 +5758,11 @@ void WgpuViewportWindow::mousePressEvent(QMouseEvent* event) {
|
||||
if (event->button() == orbit_button_
|
||||
&& (mods & Qt::KeyboardModifierMask) == orbit_mods_) {
|
||||
nav_drag_kind_ = NavDrag::Orbit;
|
||||
setPivotIndicatorVisible(true); // hidden again on release
|
||||
} else if (event->button() == pan_button_
|
||||
&& (mods & Qt::KeyboardModifierMask) == pan_mods_) {
|
||||
nav_drag_kind_ = NavDrag::Pan;
|
||||
setPivotIndicatorVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5352,6 +5849,8 @@ void WgpuViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
}
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
// Drag is over — hide the pivot indicator without afterglow.
|
||||
setPivotIndicatorVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5593,6 +6092,9 @@ void WgpuViewportWindow::wheelEvent(QWheelEvent* event) {
|
||||
// Orbit mode: each notch zooms ~10% in/out; sign matches "wheel up = in".
|
||||
const float factor = std::pow(0.9f, notches);
|
||||
camera_distance_ = std::max(0.01f, camera_distance_ * factor);
|
||||
// Pivot afterglow on wheel — visible for 600 ms so the user can see
|
||||
// what they're zooming around without holding a drag.
|
||||
setPivotIndicatorVisible(true, 600);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user