From f9a9273ecc35d269129cc9a1a32135d738c5fed7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 27 May 2026 14:52:12 +1000 Subject: [PATCH] wgpu backend: match GL orbit + viewAll math so pivots align Reported regression: --benchmark on the same sidecar visibly rotated around a different point in the wgpu binary than in IfcViewerMinimal. Root cause was two camera-convention drifts: 1. orbitEye placed the camera at (sin yaw, -cos yaw) from target; the GL backend uses (cos yaw, sin yaw). Same target, but the camera faces a different side of the model at yaw=0, which made the orbit feel like it pivoted around a different point even though the actual world-space target was the same. Now exactly matches GL ViewportWindow::updateCamera: eye.x = target.x + dist * cos(pitch) * cos(yaw) eye.y = target.y + dist * cos(pitch) * sin(yaw) eye.z = target.z + dist * sin(pitch) 2. viewAll's distance was an ad-hoc 0.6 * diag / tan(half_fov); GL uses frameAabb(mn, mx, 1.10): tan_half = tan(fov/2), min_aspect = min(aspect, 1), distance = (radius / (tan_half * min_aspect)) * 1.10. Aspect-aware so portrait windows pull back enough that the bounding sphere still fits on the tighter axis. Now ported verbatim. Also logs the computed target + distance on viewAll so a follow-up side-by-side run prints both backends' framings and any remaining discrepancy is easy to spot. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-wgpu/WgpuViewportWindow.cpp | 51 +++++++++++++++++------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index cb425e9b05..c9feea589b 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -1471,12 +1471,17 @@ void WgpuViewportWindow::releaseMsaaColorTexture() { static QVector3D orbitEye(const float target[3], float dist, float yaw_deg, float pitch_deg) { + // Matches the GL ViewportWindow::updateCamera convention exactly so the + // orbit pivot, framing, and benchmark camera path align between backends. + // eye.x = target.x + dist * cos(pitch) * cos(yaw) + // eye.y = target.y + dist * cos(pitch) * sin(yaw) + // eye.z = target.z + dist * sin(pitch) const float yaw = qDegreesToRadians(yaw_deg); const float pit = qDegreesToRadians(pitch_deg); const float cp = std::cos(pit), sp = std::sin(pit); const float cy = std::cos(yaw), sy = std::sin(yaw); - return QVector3D(target[0] + dist * cp * sy, - target[1] - dist * cp * cy, + return QVector3D(target[0] + dist * cp * cy, + target[1] + dist * cp * sy, target[2] + dist * sp); } @@ -1541,16 +1546,38 @@ bool WgpuViewportWindow::computeSceneAabb(float mn[3], float mx[3]) const { void WgpuViewportWindow::viewAll() { float mn[3], mx[3]; if (!computeSceneAabb(mn, mx)) return; - camera_target_[0] = 0.5f * (mn[0] + mx[0]); - camera_target_[1] = 0.5f * (mn[1] + mx[1]); - camera_target_[2] = 0.5f * (mn[2] + mx[2]); - const float diag = std::sqrt( - (mx[0] - mn[0]) * (mx[0] - mn[0]) + - (mx[1] - mn[1]) * (mx[1] - mn[1]) + - (mx[2] - mn[2]) * (mx[2] - mn[2])); - // Pull back enough so the bounding sphere fits at half-FOV. - const float half_fov = qDegreesToRadians(camera_fov_y_deg_) * 0.5f; - camera_distance_ = std::max(1.0f, 0.6f * diag / std::tan(half_fov)); + + // Frame the union AABB with the same math as GL's frameAabb(mn, mx, 1.10): + // target at centroid, distance pulls the bounding sphere just inside the + // tighter of the horizontal/vertical FOV. Padding 1.10 matches GL viewAll. + const float cx = 0.5f * (mn[0] + mx[0]); + const float cy = 0.5f * (mn[1] + mx[1]); + const float cz = 0.5f * (mn[2] + mx[2]); + camera_target_[0] = cx; + camera_target_[1] = cy; + camera_target_[2] = cz; + + const float dx = mx[0] - mn[0]; + const float dy = mx[1] - mn[1]; + const float dz = mx[2] - mn[2]; + const float radius = 0.5f * std::sqrt(dx*dx + dy*dy + dz*dz); + + if (radius > 1e-4f) { + const float fovy_rad = qDegreesToRadians(camera_fov_y_deg_); + const float tan_half = std::tan(fovy_rad * 0.5f); + if (tan_half > 1e-6f) { + const int h = std::max(configured_h_, 1); + const float aspect = float(std::max(configured_w_, 1)) / float(h); + const float min_aspect = aspect < 1.0f ? aspect : 1.0f; + camera_distance_ = std::max(0.1f, (radius / (tan_half * min_aspect)) * 1.10f); + } + } + + qInfo().noquote().nospace() + << "[wgpu] viewAll target=(" << cx << ", " << cy << ", " << cz << ")" + << " distance=" << camera_distance_ + << " (scene radius=" << radius << ")"; + if (isExposed()) requestUpdate(); }