From 77cf535b45112737f70989e770871157240c1884 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 5 Jun 2026 08:33:22 +1000 Subject: [PATCH] ifcviewer: de-Qt math types (Eigen everywhere) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Qt math wrappers with Eigen across ViewportWindow, OverlayRenderer, Federation, and the bonsai-side viewport modules. Eigen was already the canonical type for the actually-important matrix work (InstanceCompose, ModelGpuData, federation matrices); QVector3D/QVector4D/QMatrix4x4 were leftover from when Qt was the path of least resistance. They offered nothing over Eigen for our use case beyond a few graphics helpers (lookAt / perspective / ortho) which were 30 lines to write. Substitutions: QMatrix4x4 → Eigen::Matrix4f QVector2D → Eigen::Vector2f QVector3D → Eigen::Vector3f QVector4D → Eigen::Vector4f API rewrites: .lengthSquared() → .squaredNorm() .length() → .norm() .isNull() → .isZero() .setToIdentity() → .setIdentity() .constData() → .data() .toVector3D() → .head<3>() .inverted(&ok) → tryInvert4f(M, out) Q::dotProduct(a,b) → a.dot(b) Q::crossProduct(a,b) → a.cross(b) QMat4x4(... row-major) → Eigen::Map(col-major buf) QMat4x4().lookAt(...) → lookAtRH(eye, target, up) QMat4x4().perspective(.) → perspectiveYFovGL(fovy, aspect, n, f) QMat4x4().ortho(...) → orthoGL(l, r, b, t, n, f) Default-init divergence handled explicitly (QMatrix4x4() = identity, QVector3D() = zero; Eigen leaves both uninitialized). Public API (CameraState, HomeView, ViewportWindow::computeObjectAabb, the addSectionPlaneAtSurface / pickSurfaceAt / raycast signatures) follows through to Eigen too; bonsai-side View.cpp and Commands.cpp updated to match. Camera helpers (lookAtRH, perspectiveYFovGL, orthoGL, tryInvert4f) extracted to a new CameraMath.h so OverlayRenderer's gizmo MVP and ViewportWindow's buildViewProj share the same definitions. Federation drops its include in favour of (already had the latter for the georef matrices). Builds: desktop IfcViewerMinimal ✓, BonsaiViewer ✓, web IfcViewerWeb ✓. Tests: 100/100 pass. Closes #78 + #79; opens the door for #80-#83. --- src/bonsaiviewer/modules/viewport/View.cpp | 5 +- src/ifcviewer/CameraMath.h | 85 ++++++ src/ifcviewer/Federation.cpp | 6 +- src/ifcviewer/Federation.h | 9 +- src/ifcviewer/OverlayRenderer.cpp | 62 ++-- src/ifcviewer/OverlayRenderer.h | 19 +- src/ifcviewer/ViewportWindow.cpp | 321 ++++++++++----------- src/ifcviewer/ViewportWindow.h | 29 +- 8 files changed, 305 insertions(+), 231 deletions(-) create mode 100644 src/ifcviewer/CameraMath.h diff --git a/src/bonsaiviewer/modules/viewport/View.cpp b/src/bonsaiviewer/modules/viewport/View.cpp index 0c40cf6c33..9b9a781b27 100644 --- a/src/bonsaiviewer/modules/viewport/View.cpp +++ b/src/bonsaiviewer/modules/viewport/View.cpp @@ -30,7 +30,6 @@ #include "../../Measurement.h" #include -#include #include @@ -255,10 +254,10 @@ void ViewportView::updateVolumeReadout() { labels.reserve(per_obj.size()); for (const auto& [oid, v] : per_obj) { total += v; - QVector3D mn, mx; + Eigen::Vector3f mn, mx; if (!viewport_->computeObjectAabb(oid, mn, mx)) continue; OverlayRenderer::Label lbl; - const QVector3D c = (mn + mx) * 0.5f; + const Eigen::Vector3f c = (mn + mx) * 0.5f; lbl.world_pos[0] = c.x(); lbl.world_pos[1] = c.y(); lbl.world_pos[2] = c.z(); diff --git a/src/ifcviewer/CameraMath.h b/src/ifcviewer/CameraMath.h new file mode 100644 index 0000000000..ca1b3834d4 --- /dev/null +++ b/src/ifcviewer/CameraMath.h @@ -0,0 +1,85 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef CAMERAMATH_H +#define CAMERAMATH_H + +// Camera-matrix helpers. Eigen ships no lookAt/perspective/ortho out of +// the box (it's a math library, not a graphics one); QMatrix4x4 used to +// supply them. These functions reproduce QMatrix4x4's behaviour for the +// cases the viewport actually uses (right-handed lookAt, GL-clip +// perspective + ortho), expressed in column-major Eigen::Matrix4f so the +// result lands directly in u.view_proj for the GPU. Note both projection +// matrices target GL clip space [-1, 1] — callers pre-multiply by a +// z-remap matrix to land WebGPU's [0, 1]. + +#include + +#include + +inline Eigen::Matrix4f lookAtRH(const Eigen::Vector3f& eye, + const Eigen::Vector3f& target, + const Eigen::Vector3f& up) { + const Eigen::Vector3f f = (target - eye).normalized(); + const Eigen::Vector3f s = f.cross(up).normalized(); + const Eigen::Vector3f u = s.cross(f); + Eigen::Matrix4f m = Eigen::Matrix4f::Identity(); + m(0, 0) = s.x(); m(0, 1) = s.y(); m(0, 2) = s.z(); m(0, 3) = -s.dot(eye); + m(1, 0) = u.x(); m(1, 1) = u.y(); m(1, 2) = u.z(); m(1, 3) = -u.dot(eye); + m(2, 0) = -f.x(); m(2, 1) = -f.y(); m(2, 2) = -f.z(); m(2, 3) = f.dot(eye); + return m; +} + +inline Eigen::Matrix4f perspectiveYFovGL(float fovy_deg, float aspect, + float near_plane, float far_plane) { + const float fovy_rad = fovy_deg * float(M_PI) / 180.0f; + const float t = std::tan(fovy_rad * 0.5f); + Eigen::Matrix4f m = Eigen::Matrix4f::Zero(); + m(0, 0) = 1.0f / (aspect * t); + m(1, 1) = 1.0f / t; + m(2, 2) = -(far_plane + near_plane) / (far_plane - near_plane); + m(2, 3) = -(2.0f * far_plane * near_plane) / (far_plane - near_plane); + m(3, 2) = -1.0f; + return m; +} + +inline Eigen::Matrix4f orthoGL(float left, float right, + float bottom, float top, + float near_plane, float far_plane) { + Eigen::Matrix4f m = Eigen::Matrix4f::Identity(); + m(0, 0) = 2.0f / (right - left); + m(1, 1) = 2.0f / (top - bottom); + m(2, 2) = -2.0f / (far_plane - near_plane); + m(0, 3) = -(right + left) / (right - left); + m(1, 3) = -(top + bottom) / (top - bottom); + m(2, 3) = -(far_plane + near_plane) / (far_plane - near_plane); + return m; +} + +// Inverse-with-invertibility-check. QMatrix4x4::inverted(bool*) +// returned identity (silently) on a singular matrix and flipped the +// `ok` out-parameter; Eigen's .inverse() always runs even on singular +// input. computeInverseWithCheck is the safe equivalent. +inline bool tryInvert4f(const Eigen::Matrix4f& M, Eigen::Matrix4f& out) { + bool invertible = false; + M.computeInverseWithCheck(out, invertible, /*absDetThreshold=*/0); + return invertible; +} + +#endif // CAMERAMATH_H diff --git a/src/ifcviewer/Federation.cpp b/src/ifcviewer/Federation.cpp index b290dac346..4664554840 100644 --- a/src/ifcviewer/Federation.cpp +++ b/src/ifcviewer/Federation.cpp @@ -719,9 +719,9 @@ bool Federation::load(const QString& path, QJsonArray ta = ho.value("target").toArray(); HomeView v; if (ta.size() == 3) { - v.target = QVector3D(float(ta[0].toDouble()), - float(ta[1].toDouble()), - float(ta[2].toDouble())); + v.target = Eigen::Vector3f(float(ta[0].toDouble()), + float(ta[1].toDouble()), + float(ta[2].toDouble())); } v.distance = float(ho.value("distance").toDouble(50.0)); v.yaw = float(ho.value("yaw").toDouble(45.0)); diff --git a/src/ifcviewer/Federation.h b/src/ifcviewer/Federation.h index bc0268814d..af124ac50e 100644 --- a/src/ifcviewer/Federation.h +++ b/src/ifcviewer/Federation.h @@ -27,7 +27,6 @@ #include #include #include -#include #include #include @@ -184,10 +183,10 @@ class Federation : public QObject { Q_OBJECT public: struct HomeView { - QVector3D target; - float distance = 50.0f; - float yaw = 45.0f; // degrees - float pitch = 30.0f; // degrees + Eigen::Vector3f target = Eigen::Vector3f::Zero(); + float distance = 50.0f; + float yaw = 45.0f; // degrees + float pitch = 30.0f; // degrees }; struct Model { diff --git a/src/ifcviewer/OverlayRenderer.cpp b/src/ifcviewer/OverlayRenderer.cpp index 7791c09a2d..1b3e645272 100644 --- a/src/ifcviewer/OverlayRenderer.cpp +++ b/src/ifcviewer/OverlayRenderer.cpp @@ -19,6 +19,8 @@ #include "OverlayRenderer.h" +#include "CameraMath.h" + #include #include #include @@ -69,11 +71,11 @@ WGPUVertexBufferLayout thickLineVertexLayout(WGPUVertexAttribute attribs[5]) { // Pack the axis uniform's 256-byte slot. Layout matches WGSL AxisUniforms: // mat4 + vec3 + f32 + f32 + f32 + vec2 = 96 B used, padded to 256. void packAxisUniform(uint8_t* dst, - const QMatrix4x4& mvp, const QVector3D& origin, + const Eigen::Matrix4f& mvp, const Eigen::Vector3f& 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)); + std::memcpy(dst, mvp.data(), 16 * sizeof(float)); float ox = origin.x(), oy = origin.y(), oz = origin.z(); std::memcpy(dst + 64, &ox, sizeof(float)); std::memcpy(dst + 68, &oy, sizeof(float)); @@ -89,16 +91,16 @@ void packAxisUniform(uint8_t* dst, // SectionUniforms: mat4 + 4×(vec3 + scalar pad) + vec4 + vec2 + 8 B pad // = 160 B used, padded to 256. void packSectionUniform(uint8_t* dst, - const QMatrix4x4& mvp, - const QVector3D& origin, float half_size, - const QVector3D& tangent, float line_width_px, - const QVector3D& bitangent, - const QVector3D& normal, + const Eigen::Matrix4f& mvp, + const Eigen::Vector3f& origin, float half_size, + const Eigen::Vector3f& tangent, float line_width_px, + const Eigen::Vector3f& bitangent, + const Eigen::Vector3f& normal, float r, float g, float b, float a, float viewport_w, float viewport_h) { std::memset(dst, 0, 256); - std::memcpy(dst, mvp.constData(), 16 * sizeof(float)); - auto put_vec3_pad = [&](size_t off, const QVector3D& v, float pad_val) { + std::memcpy(dst, mvp.data(), 16 * sizeof(float)); + auto put_vec3_pad = [&](size_t off, const Eigen::Vector3f& v, float pad_val) { float vx = v.x(), vy = v.y(), vz = v.z(); std::memcpy(dst + off + 0, &vx, sizeof(float)); std::memcpy(dst + off + 4, &vy, sizeof(float)); @@ -778,24 +780,22 @@ void OverlayRenderer::encodeCornerAxis(WGPUCommandEncoder enc, // Y-up there — mirrors buildViewProj's identical fix on the viewport. const float yaw_rad = qDegreesToRadians(f.camera_yaw_deg); const float pitch_rad = qDegreesToRadians(f.camera_pitch_deg); - const QVector3D eye_dir(std::cos(pitch_rad) * std::cos(yaw_rad), + const Eigen::Vector3f 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(f.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); - QMatrix4x4 z_remap; + const Eigen::Vector3f world_up = (std::abs(f.camera_pitch_deg) >= 89.0f) + ? Eigen::Vector3f(0.0f, 1.0f, 0.0f) + : Eigen::Vector3f(0.0f, 0.0f, 1.0f); + const Eigen::Matrix4f gv = lookAtRH(eye_dir * 3.0f, Eigen::Vector3f::Zero(), world_up); + const Eigen::Matrix4f gp = orthoGL(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f); + Eigen::Matrix4f z_remap = Eigen::Matrix4f::Identity(); z_remap(2, 2) = 0.5f; z_remap(2, 3) = 0.5f; - const QMatrix4x4 mvp = z_remap * gp * gv; + const Eigen::Matrix4f mvp = z_remap * gp * gv; 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, + packAxisUniform(slot, mvp, Eigen::Vector3f(0, 0, 0), 1.0f, 1.0f, line_w, float(gizmo_size), float(gizmo_size)); const uint32_t slot_offset = 0u; wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, slot_offset, slot, sizeof(slot)); @@ -982,15 +982,15 @@ void OverlayRenderer::encodeSectionGizmos(WGPURenderPassEncoder pass, // Stable in-plane basis: pick the world axis least parallel to n // so the cross-product stays well-conditioned at any orientation. - QVector3D nn = p.n.normalized(); + Eigen::Vector3f nn = p.n.normalized(); const float ax = std::abs(nn.x()), ay = std::abs(nn.y()), az = std::abs(nn.z()); - QVector3D seed = (ax < ay && ax < az) ? QVector3D(1, 0, 0) - : (ay < az) ? QVector3D(0, 1, 0) - : QVector3D(0, 0, 1); - QVector3D tangent = QVector3D::crossProduct(nn, seed); - if (tangent.lengthSquared() < 1e-12f) tangent = QVector3D(1, 0, 0); + Eigen::Vector3f seed = (ax < ay && ax < az) ? Eigen::Vector3f(1, 0, 0) + : (ay < az) ? Eigen::Vector3f(0, 1, 0) + : Eigen::Vector3f(0, 0, 1); + Eigen::Vector3f tangent = nn.cross(seed); + if (tangent.squaredNorm() < 1e-12f) tangent = Eigen::Vector3f(1, 0, 0); tangent.normalize(); - QVector3D bitangent = QVector3D::crossProduct(nn, tangent).normalized(); + Eigen::Vector3f bitangent = nn.cross(tangent).normalized(); // Fixed 1 m half-size matches GL's renderSectionPlanes constant. const float half_size = 1.0f; @@ -1502,7 +1502,7 @@ void OverlayRenderer::encodeOverlayLines(WGPURenderPassEncoder pass, // (8 B) into [96..104). The static [64..96) and [104..120) ranges were // filled by setOverlayLines so we don't touch them again. float vp[16]; - std::memcpy(vp, f.view_proj.constData(), sizeof(vp)); + std::memcpy(vp, f.view_proj.data(), sizeof(vp)); const float viewport[2] = { float(f.viewport_w_px), float(f.viewport_h_px) }; @@ -1721,7 +1721,7 @@ void OverlayRenderer::encodeOverlayPoints(WGPURenderPassEncoder pass, if (f.viewport_w_px <= 0 || f.viewport_h_px <= 0) return; float vp[16]; - std::memcpy(vp, f.view_proj.constData(), sizeof(vp)); + std::memcpy(vp, f.view_proj.data(), sizeof(vp)); const float viewport[2] = { float(f.viewport_w_px), float(f.viewport_h_px) }; wgpuQueueWriteBuffer(queue_, overlay_point_uniform_buffer_, 0, vp, sizeof(vp)); @@ -1886,7 +1886,7 @@ void OverlayRenderer::encodeHighlightTriangles(WGPURenderPassEncoder pass, if (!highlight_pipeline_ || highlight_vertex_count_ == 0) return; // Pack mat4 + vec4 into the slot. mat4 is column-major 16 floats. uint8_t slot[80] = {}; - std::memcpy(slot, f.view_proj.constData(), 16 * sizeof(float)); + std::memcpy(slot, f.view_proj.data(), 16 * sizeof(float)); std::memcpy(slot + 64, highlight_color_, 4 * sizeof(float)); wgpuQueueWriteBuffer(queue_, highlight_uniform_buffer_, 0, slot, sizeof(slot)); @@ -2181,7 +2181,7 @@ void OverlayRenderer::encodeLabels(WGPUCommandEncoder enc, // World-anchored labels at point size 9 (matches GL OverlayRenderer). for (const auto& lbl : labels_) { const float* p = lbl.world_pos; - const float* m = f.view_proj.constData(); + const float* m = f.view_proj.data(); // Column-major: M[col*4 + row]. const float wx = m[0]*p[0] + m[4]*p[1] + m[8]*p[2] + m[12]; const float wy = m[1]*p[0] + m[5]*p[1] + m[9]*p[2] + m[13]; diff --git a/src/ifcviewer/OverlayRenderer.h b/src/ifcviewer/OverlayRenderer.h index 8c2c088605..e5866ee2a5 100644 --- a/src/ifcviewer/OverlayRenderer.h +++ b/src/ifcviewer/OverlayRenderer.h @@ -21,10 +21,10 @@ #define WGPUOVERLAYRENDERER_H #include -#include #include #include -#include + +#include #include @@ -35,8 +35,8 @@ // at the top of render() and passed by const-ref to each encodeX() call so // the overlay renderer never reaches back into the viewport. struct OverlayFrame { - QMatrix4x4 view_proj; - QVector3D camera_target; + Eigen::Matrix4f view_proj = Eigen::Matrix4f::Identity(); + Eigen::Vector3f camera_target = Eigen::Vector3f::Zero(); float camera_distance = 5.0f; float camera_yaw_deg = 0.0f; float camera_pitch_deg = 0.0f; @@ -51,12 +51,11 @@ struct OverlayFrame { // reads from a non-owning span every frame. Held by value because the // struct is small and copies happen at most six times per frame. struct SectionPlane { - QVector3D n; // unit normal (camera-facing after auto-flip) - float d; // -dot(n, origin) - QVector3D origin; // surface point at the moment the plane was added - float visual_radius; // unused by the visualizer (kept here so the - // section tool's state struct round-trips - // through this overlay-facing definition). + Eigen::Vector3f n = Eigen::Vector3f::UnitZ(); // unit normal + float d = 0.0f; // -dot(n, origin) + Eigen::Vector3f origin = Eigen::Vector3f::Zero(); // surface point at the + // moment the plane was added + float visual_radius = 0.0f; }; // All viewport overlays in one place: axis indicator (corner + pivot), diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 333eae2034..92167517b8 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -19,6 +19,7 @@ #include "ViewportWindow.h" #include "AreaMeasurement.h" +#include "CameraMath.h" #include "ChunkPlanner.h" #include "InstanceCompose.h" #include "LengthMeasurement.h" @@ -33,8 +34,6 @@ #include #include #include -#include -#include #include #include // wgpu-native extensions (logging, MULTI_DRAW_INDIRECT, …) @@ -97,7 +96,7 @@ static constexpr uint64_t WGPU_BYTES_PER_ROW_ALIGN = 256; // Forward declaration — defined below alongside updateFrameUniforms. Used // by render() to extract camera/frustum state without duplicating the math. -static QVector3D orbitEye(const float target[3], float dist, +static Eigen::Vector3f orbitEye(const float target[3], float dist, float yaw_deg, float pitch_deg); // Forward declaration — defined alongside the Volume tool. Called from @@ -2585,8 +2584,8 @@ void ViewportWindow::releasePickResources() { } uint32_t ViewportWindow::pickObjectAt(int x_pixels, int y_pixels, - QVector3D* normal_out) { - if (normal_out) *normal_out = QVector3D(0, 0, 1); + Eigen::Vector3f* normal_out) { + if (normal_out) *normal_out = Eigen::Vector3f(0, 0, 1); if (!pick_pipeline_ || !device_ || !queue_ || models_gpu_.empty()) return 0; if (configured_w_ <= 0 || configured_h_ <= 0) return 0; if (x_pixels < 0 || y_pixels < 0 || @@ -2749,8 +2748,8 @@ uint32_t ViewportWindow::pickObjectAt(int x_pixels, int y_pixels, const float nx = h2f(halves[0]) * 2.0f - 1.0f; const float ny = h2f(halves[1]) * 2.0f - 1.0f; const float nz = h2f(halves[2]) * 2.0f - 1.0f; - QVector3D n(nx, ny, nz); - if (n.lengthSquared() > 1e-6f) *normal_out = n.normalized(); + Eigen::Vector3f n(nx, ny, nz); + if (n.squaredNorm() > 1e-6f) *normal_out = n.normalized(); } wgpuBufferUnmap(pick_normal_staging_buffer_); } @@ -2767,9 +2766,9 @@ uint32_t ViewportWindow::pickObjectAt(int x_pixels, int y_pixels, // always axis-aligned (walls, slabs, columns) this matches the user's // expectation; for diagonal or curved geometry it falls back to the // closest of {±X, ±Y, ±Z}, which is still a usable cut direction. -static bool rayAABBHit(const QVector3D& origin, const QVector3D& dir, +static bool rayAABBHit(const Eigen::Vector3f& origin, const Eigen::Vector3f& dir, const float mn[3], const float mx[3], - float& t_enter, QVector3D& face_normal) { + float& t_enter, Eigen::Vector3f& face_normal) { float t_min = -std::numeric_limits::infinity(); float t_max = std::numeric_limits::infinity(); const float o[3] = { origin.x(), origin.y(), origin.z() }; @@ -2799,7 +2798,7 @@ static bool rayAABBHit(const QVector3D& origin, const QVector3D& dir, if (hit_axis < 0) { face_normal = -dir; // ray origin inside the box on all axes — fallback } else { - QVector3D n(0, 0, 0); + Eigen::Vector3f n(0, 0, 0); n[hit_axis] = hit_sign; face_normal = n; } @@ -2952,11 +2951,11 @@ std::vector ViewportWindow::picksInRect(int x, int y, int w, int h) { bool ViewportWindow::pickSurfaceAt(int x_pixels, int y_pixels, uint32_t& object_id_out, - QVector3D& world_pos_out, - QVector3D& world_normal_out, + Eigen::Vector3f& world_pos_out, + Eigen::Vector3f& world_normal_out, float* aabb_radius_out) { if (aabb_radius_out) *aabb_radius_out = 0.0f; - QVector3D picked_normal(0, 0, 1); + Eigen::Vector3f picked_normal(0, 0, 1); const uint32_t id = pickObjectAt(x_pixels, y_pixels, &picked_normal); if (id == 0) return false; @@ -2967,30 +2966,29 @@ bool ViewportWindow::pickSurfaceAt(int x_pixels, int y_pixels, // ray-cast against the AABB of every instance carrying the picked // object_id and take the closest hit. Equally accurate for the section // tool's "drop a plane where I clicked" UX, no readback at all. - QMatrix4x4 view, proj; + Eigen::Matrix4f view, proj; buildViewProj(view, proj); - bool ok = false; - const QMatrix4x4 inv_vp = (proj * view).inverted(&ok); - if (!ok) return false; + Eigen::Matrix4f inv_vp; + if (!tryInvert4f(proj * view, inv_vp)) return false; const float ndc_x = (2.0f * float(x_pixels) / float(configured_w_)) - 1.0f; const float ndc_y = 1.0f - (2.0f * float(y_pixels) / float(configured_h_)); // Unproject the far-plane corner (NDC z = 1 for WebGPU) of the // pick-pixel pillar to get a point on the ray. - const QVector4D far_clip(ndc_x, ndc_y, 1.0f, 1.0f); - const QVector4D far_w = inv_vp * far_clip; + const Eigen::Vector4f far_clip(ndc_x, ndc_y, 1.0f, 1.0f); + const Eigen::Vector4f far_w = inv_vp * far_clip; if (std::abs(far_w.w()) < 1e-6f) return false; - const QVector3D far_world = far_w.toVector3D() / far_w.w(); + const Eigen::Vector3f far_world = far_w.head<3>() / far_w.w(); - const QVector3D eye = orbitEye(camera_target_, camera_distance_, + const Eigen::Vector3f eye = orbitEye(camera_target_, camera_distance_, camera_yaw_deg_, camera_pitch_deg_); - QVector3D ray_dir = far_world - eye; - if (ray_dir.lengthSquared() < 1e-8f) return false; + Eigen::Vector3f ray_dir = far_world - eye; + if (ray_dir.squaredNorm() < 1e-8f) return false; ray_dir.normalize(); float best_t = std::numeric_limits::infinity(); - QVector3D best_point; - QVector3D best_normal; + Eigen::Vector3f best_point; + Eigen::Vector3f best_normal; float best_radius = 0.0f; bool found = false; for (const auto& [mid, m] : models_gpu_) { @@ -2998,7 +2996,7 @@ bool ViewportWindow::pickSurfaceAt(int x_pixels, int y_pixels, for (const auto& inst : m.instances) { if (inst.object_id != id) continue; float t = 0.0f; - QVector3D n; + Eigen::Vector3f n; if (!rayAABBHit(eye, ray_dir, inst.world_aabb_min, inst.world_aabb_max, t, n)) continue; @@ -3023,7 +3021,7 @@ bool ViewportWindow::pickSurfaceAt(int x_pixels, int y_pixels, // picked triangle), fall back to the AABB-face normal if the pick pass // returned a degenerate vector (e.g. background sliver). The auto-flip // in addSectionPlaneAtSurface re-orients toward the camera. - world_normal_out = (picked_normal.lengthSquared() > 1e-3f) + world_normal_out = (picked_normal.squaredNorm() > 1e-3f) ? picked_normal : best_normal; object_id_out = id; return true; @@ -3040,27 +3038,27 @@ void ViewportWindow::toggleSectionTool() { if (isExposed()) requestUpdate(); } -bool ViewportWindow::addSectionPlaneAtSurface(const QVector3D& point, - const QVector3D& normal, +bool ViewportWindow::addSectionPlaneAtSurface(const Eigen::Vector3f& point, + const Eigen::Vector3f& normal, float visual_radius) { if (int(section_planes_.size()) >= kMaxSectionPlanes) { qWarning("[wgpu section] cap reached (%d planes)", kMaxSectionPlanes); return false; } - QVector3D n = normal; - if (n.lengthSquared() < 1e-8f) return false; + Eigen::Vector3f n = normal; + if (n.squaredNorm() < 1e-8f) return false; n.normalize(); // Auto-flip the normal so the camera-facing half gets cut away — that // way the first click always reveals the surface the user just clicked. - const QVector3D eye = orbitEye(camera_target_, camera_distance_, + const Eigen::Vector3f eye = orbitEye(camera_target_, camera_distance_, camera_yaw_deg_, camera_pitch_deg_); - const QVector3D eye_dir = eye - point; - if (QVector3D::dotProduct(n, eye_dir) < 0.0f) n = -n; + const Eigen::Vector3f eye_dir = eye - point; + if (n.dot(eye_dir) < 0.0f) n = -n; SectionPlane p; p.n = n; p.origin = point; - p.d = -QVector3D::dotProduct(n, point); + p.d = -n.dot(point); p.visual_radius = (visual_radius > 0.0f) ? visual_radius : 1.0f; section_planes_.push_back(p); qInfo().noquote().nospace() @@ -3136,7 +3134,7 @@ bool ViewportWindow::readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id, bool ViewportWindow::pickMeshLocalAt(int x, int y, MeshLocalPick& out) { uint32_t obj_id = 0; - QVector3D world_pos, world_normal; + Eigen::Vector3f world_pos, world_normal; if (!pickSurfaceAt(x, y, obj_id, world_pos, world_normal)) return false; // O(1) instance lookup via object_id_to_instance — see also the @@ -3152,13 +3150,12 @@ bool ViewportWindow::pickMeshLocalAt(int x, int y, MeshLocalPick& out) { if (it == m.object_id_to_instance.end()) continue; const InstanceCpu& inst = m.instances[it->second]; - QMatrix4x4 T(inst.transform[0], inst.transform[4], inst.transform[8], inst.transform[12], - inst.transform[1], inst.transform[5], inst.transform[9], inst.transform[13], - inst.transform[2], inst.transform[6], inst.transform[10], inst.transform[14], - inst.transform[3], inst.transform[7], inst.transform[11], inst.transform[15]); - bool ok = false; - const QMatrix4x4 Ti = T.inverted(&ok); - if (!ok) return false; + // inst.transform is column-major float[16] — the GPU upload + // layout. Eigen::Matrix4f is also column-major by default, so + // a Map reads it directly with no element swizzling. + const Eigen::Matrix4f T = Eigen::Map(inst.transform); + Eigen::Matrix4f Ti; + if (!tryInvert4f(T, Ti)) return false; if (inst.mesh_id >= m.meshes.size()) return false; @@ -3173,31 +3170,30 @@ bool ViewportWindow::pickMeshLocalAt(int x, int y, MeshLocalPick& out) { // BFS seeds with whatever triangle is closest to the AABB // corner — often a perpendicular face, which produces // bounding-box-shaped patches instead of surface patches. - QVector3D refined_world_pos = world_pos; - QVector3D refined_world_normal = world_normal; + Eigen::Vector3f refined_world_pos = world_pos; + Eigen::Vector3f refined_world_normal = world_normal; if (inst.mesh_id < m.mesh_triangles_cache.size()) { const auto& tris = m.mesh_triangles_cache[inst.mesh_id]; if (!tris.indices.empty() && configured_w_ > 0 && configured_h_ > 0) { - QMatrix4x4 view, proj; + Eigen::Matrix4f view, proj; buildViewProj(view, proj); - bool inv_ok = false; - const QMatrix4x4 inv_vp = (proj * view).inverted(&inv_ok); - if (inv_ok) { + Eigen::Matrix4f inv_vp; + if (tryInvert4f(proj * view, inv_vp)) { const float ndc_x = (2.0f * float(x) / float(configured_w_)) - 1.0f; const float ndc_y = 1.0f - (2.0f * float(y) / float(configured_h_)); - const QVector4D far_clip(ndc_x, ndc_y, 1.0f, 1.0f); - const QVector4D far_w = inv_vp * far_clip; + const Eigen::Vector4f far_clip(ndc_x, ndc_y, 1.0f, 1.0f); + const Eigen::Vector4f far_w = inv_vp * far_clip; if (std::abs(far_w.w()) >= 1e-6f) { - const QVector3D far_world = far_w.toVector3D() / far_w.w(); - const QVector3D eye = orbitEye( + const Eigen::Vector3f far_world = far_w.head<3>() / far_w.w(); + const Eigen::Vector3f eye = orbitEye( camera_target_, camera_distance_, camera_yaw_deg_, camera_pitch_deg_); - QVector3D ray_dir = far_world - eye; - if (ray_dir.lengthSquared() > 1e-8f) { + Eigen::Vector3f ray_dir = far_world - eye; + if (ray_dir.squaredNorm() > 1e-8f) { ray_dir.normalize(); // Inverse-transform the world ray into mesh-local. - const QVector4D ro_l4 = Ti * QVector4D(eye.x(), eye.y(), eye.z(), 1.0f); - const QVector4D rd_l4 = Ti * QVector4D(ray_dir.x(), ray_dir.y(), ray_dir.z(), 0.0f); + const Eigen::Vector4f ro_l4 = Ti * Eigen::Vector4f(eye.x(), eye.y(), eye.z(), 1.0f); + const Eigen::Vector4f rd_l4 = Ti * Eigen::Vector4f(ray_dir.x(), ray_dir.y(), ray_dir.z(), 0.0f); const float ro_l[3] = { ro_l4.x(), ro_l4.y(), ro_l4.z() }; const float rd_l[3] = { rd_l4.x(), rd_l4.y(), rd_l4.z() }; const float ldn = std::sqrt( @@ -3251,11 +3247,11 @@ bool ViewportWindow::pickMeshLocalAt(int x, int y, MeshLocalPick& out) { n_local[2] /= nl; } const float* M = inst.transform; - QVector3D n_world( + Eigen::Vector3f n_world( M[0]*n_local[0] + M[4]*n_local[1] + M[8] *n_local[2], M[1]*n_local[0] + M[5]*n_local[1] + M[9] *n_local[2], M[2]*n_local[0] + M[6]*n_local[1] + M[10]*n_local[2]); - if (n_world.lengthSquared() > 1e-12f) { + if (n_world.squaredNorm() > 1e-12f) { n_world.normalize(); refined_world_normal = n_world; } @@ -3267,7 +3263,7 @@ bool ViewportWindow::pickMeshLocalAt(int x, int y, MeshLocalPick& out) { } } - const QVector4D mp = Ti * QVector4D(refined_world_pos.x(), + const Eigen::Vector4f mp = Ti * Eigen::Vector4f(refined_world_pos.x(), refined_world_pos.y(), refined_world_pos.z(), 1.0f); @@ -3362,15 +3358,11 @@ bool ViewportWindow::raycast(const float origin[3], const float dir[3], // Transform ray into mesh-local frame. We need both a point // (origin) and a direction (dir) inverse-transformed; dir is // a vector so the translation drops out. - QMatrix4x4 T(inst.transform[0], inst.transform[4], inst.transform[8], inst.transform[12], - inst.transform[1], inst.transform[5], inst.transform[9], inst.transform[13], - inst.transform[2], inst.transform[6], inst.transform[10], inst.transform[14], - inst.transform[3], inst.transform[7], inst.transform[11], inst.transform[15]); - bool ok = false; - const QMatrix4x4 Ti = T.inverted(&ok); - if (!ok) continue; - const QVector4D ro_local4 = Ti * QVector4D(origin[0], origin[1], origin[2], 1.0f); - const QVector4D rd_local4 = Ti * QVector4D(dir[0], dir[1], dir[2], 0.0f); + const Eigen::Matrix4f T = Eigen::Map(inst.transform); + Eigen::Matrix4f Ti; + if (!tryInvert4f(T, Ti)) continue; + const Eigen::Vector4f ro_local4 = Ti * Eigen::Vector4f(origin[0], origin[1], origin[2], 1.0f); + const Eigen::Vector4f rd_local4 = Ti * Eigen::Vector4f(dir[0], dir[1], dir[2], 0.0f); const float ro_local[3] = { ro_local4.x(), ro_local4.y(), ro_local4.z() }; const float rd_local[3] = { rd_local4.x(), rd_local4.y(), rd_local4.z() }; @@ -3612,7 +3604,7 @@ void ViewportWindow::invertElementVisibility() { ViewportWindow::CameraState ViewportWindow::cameraState() const { return CameraState{ - QVector3D(camera_target_[0], camera_target_[1], camera_target_[2]), + Eigen::Vector3f(camera_target_[0], camera_target_[1], camera_target_[2]), camera_distance_, camera_yaw_deg_, camera_pitch_deg_, @@ -3760,14 +3752,14 @@ void ViewportWindow::updateVolumeReadout() { // Project a world point to LOGICAL pixel coords (Qt's mouse-event units). // Returns false if behind the camera. -static bool projectWorldToLogicalScreen(const QMatrix4x4& vp, - const QVector3D& world, +static bool projectWorldToLogicalScreen(const Eigen::Matrix4f& vp, + const Eigen::Vector3f& world, int win_w, int win_h, - QVector2D& out) { - const QVector4D clip = vp * QVector4D(world, 1.0f); + Eigen::Vector2f& out) { + const Eigen::Vector4f clip = vp * Eigen::Vector4f(world.x(), world.y(), world.z(), 1.0f); if (clip.w() <= 0.0f) return false; const float invw = 1.0f / clip.w(); - out = QVector2D( + out = Eigen::Vector2f( (clip.x() * invw * 0.5f + 0.5f) * float(win_w), (1.0f - (clip.y() * invw * 0.5f + 0.5f)) * float(win_h)); return true; @@ -3778,15 +3770,15 @@ int ViewportWindow::hitTestSectionGizmo(int x, int y) const { const int w = width(); const int h = height(); if (w <= 0 || h <= 0) return -1; - QMatrix4x4 view, proj; + Eigen::Matrix4f view, proj; buildViewProj(view, proj); - const QMatrix4x4 vp = proj * view; + const Eigen::Matrix4f vp = proj * view; const float grab_px = 12.0f; int best = -1; float best_d2 = grab_px * grab_px; for (int i = 0; i < int(section_planes_.size()); ++i) { const SectionPlane& p = section_planes_[i]; - QVector2D s_origin, s_tip; + Eigen::Vector2f s_origin, s_tip; if (!projectWorldToLogicalScreen(vp, p.origin, w, h, s_origin)) continue; // The gizmo's arrow extends along +n by exactly 1 m in world @@ -3795,14 +3787,14 @@ int ViewportWindow::hitTestSectionGizmo(int x, int y) const { // Mirror that here. if (!projectWorldToLogicalScreen(vp, p.origin + p.n * 1.0f, w, h, s_tip)) continue; - const QVector2D q{float(x), float(y)}; - const QVector2D ab = s_tip - s_origin; - const float ab_len2 = ab.lengthSquared(); + const Eigen::Vector2f q{float(x), float(y)}; + const Eigen::Vector2f ab = s_tip - s_origin; + const float ab_len2 = ab.squaredNorm(); if (ab_len2 < 1e-3f) continue; - float t = QVector2D::dotProduct(q - s_origin, ab) / ab_len2; + float t = (q - s_origin).dot(ab) / ab_len2; t = std::clamp(t, 0.0f, 1.0f); - const QVector2D proj_pt = s_origin + ab * t; - const float d2 = (q - proj_pt).lengthSquared(); + const Eigen::Vector2f proj_pt = s_origin + ab * t; + const float d2 = (q - proj_pt).squaredNorm(); if (d2 < best_d2) { best_d2 = d2; best = i; } } return best; @@ -3817,32 +3809,32 @@ void ViewportWindow::updateSectionDrag(int x, int y) { const int w = width(); const int h = height(); if (w <= 0 || h <= 0) return; - QMatrix4x4 view, proj; + Eigen::Matrix4f view, proj; buildViewProj(view, proj); - const QMatrix4x4 vp = proj * view; + const Eigen::Matrix4f vp = proj * view; // Re-project the press-time origin and origin + n to screen space. // The press-time origin is what `start` should be relative to — so the // plane slides smoothly even as the camera moves (we re-project every // frame to handle mid-drag camera rotation cleanly). - QVector2D s_origin, s_n; + Eigen::Vector2f s_origin, s_n; if (!projectWorldToLogicalScreen(vp, section_drag_start_origin_, w, h, s_origin)) return; if (!projectWorldToLogicalScreen(vp, section_drag_start_origin_ + p.n, w, h, s_n)) return; - const QVector2D screen_axis = s_n - s_origin; - const float screen_axis_len2 = screen_axis.lengthSquared(); + const Eigen::Vector2f screen_axis = s_n - s_origin; + const float screen_axis_len2 = screen_axis.squaredNorm(); if (screen_axis_len2 < 1e-3f) return; // arrow is edge-on // Project pixel delta onto the screen-space axis; convert to metres // via (delta · axis) / |axis|² (axis is 1 m long in world space). - const QVector2D delta_px(float(x - section_drag_start_mouse_.x()), + const Eigen::Vector2f delta_px(float(x - section_drag_start_mouse_.x()), float(y - section_drag_start_mouse_.y())); - const float meters = QVector2D::dotProduct(delta_px, screen_axis) + const float meters = delta_px.dot(screen_axis) / screen_axis_len2; p.origin = section_drag_start_origin_ + p.n * meters; - p.d = -QVector3D::dotProduct(p.n, p.origin); + p.d = -p.n.dot(p.origin); requestUpdate(); } @@ -4094,7 +4086,7 @@ int ViewportWindow::encodeHizResolve(WGPUCommandEncoder enc) { return slot; } -void ViewportWindow::startHizMap(int slot, const QMatrix4x4& vp_used) { +void ViewportWindow::startHizMap(int slot, const Eigen::Matrix4f& vp_used) { if (slot < 0 || slot >= HIZ_SLOTS) return; if (!hiz_staging_buffers_[slot] || hiz_resolve_w_ == 0) return; @@ -4213,7 +4205,7 @@ bool ViewportWindow::aabbOccludedByHiz(const float mn[3], const float mx[3]) con // - min/max NDC x,y (screen-space bounds) // - min projected z (nearest point of the AABB to the camera) // - whether any corner has clip.w <= 0 (AABB straddles near plane) - const float* m = hiz_vp_.constData(); // column-major + const float* m = hiz_vp_.data(); // column-major auto applyVp = [m](float x, float y, float z, float out[4]) { out[0] = m[0]*x + m[4]*y + m[8] *z + m[12]; out[1] = m[1]*x + m[5]*y + m[9] *z + m[13]; @@ -4675,30 +4667,30 @@ void ViewportWindow::render() { hiz_reject_count_ = 0; QElapsedTimer cull_timer; cull_timer.start(); - QMatrix4x4 vp_this_frame; + Eigen::Matrix4f vp_this_frame; { - const QVector3D target(camera_target_[0], camera_target_[1], camera_target_[2]); - const QVector3D eye = orbitEye(camera_target_, camera_distance_, + const Eigen::Vector3f target(camera_target_[0], camera_target_[1], camera_target_[2]); + const Eigen::Vector3f eye = orbitEye(camera_target_, camera_distance_, camera_yaw_deg_, camera_pitch_deg_); - QMatrix4x4 v, p; + Eigen::Matrix4f v, p; buildViewProj(v, p); - const QMatrix4x4 vp = p * v; + const Eigen::Matrix4f vp = p * v; vp_this_frame = vp; float planes[6][4]; - extractFrustumPlanes(vp.constData(), planes); + extractFrustumPlanes(vp.data(), planes); // LOD pick inputs: world-space eye, unit forward, vertical focal in // pixels. focal_px maps view-space depth to projected radius: // projected_px = world_radius * focal_px / view_z. - const QVector3D fwd_q = (target - eye).normalized(); + const Eigen::Vector3f fwd_q = (target - eye).normalized(); // World-up convention: Z-up. Near the poles lookAt degenerates, // so swap to Y-up — mirrors buildViewProj's pitch gate at line // 4701 so cull's camera basis matches the actual view matrix. - 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); - const QVector3D right_q = QVector3D::crossProduct(fwd_q, world_up).normalized(); - const QVector3D up_q = QVector3D::crossProduct(right_q, fwd_q).normalized(); + const Eigen::Vector3f world_up = (std::abs(camera_pitch_deg_) >= 89.0f) + ? Eigen::Vector3f(0.0f, 1.0f, 0.0f) + : Eigen::Vector3f(0.0f, 0.0f, 1.0f); + const Eigen::Vector3f right_q = fwd_q.cross(world_up).normalized(); + const Eigen::Vector3f up_q = right_q.cross(fwd_q).normalized(); const float eye_a[3] = { eye.x(), eye.y(), eye.z() }; const float fwd_a[3] = { fwd_q.x(), fwd_q.y(), fwd_q.z() }; const float right_a[3] = { right_q.x(), right_q.y(), right_q.z() }; @@ -4945,7 +4937,7 @@ void ViewportWindow::render() { // this viewport. OverlayFrame overlay_frame; overlay_frame.view_proj = vp_this_frame; - overlay_frame.camera_target = QVector3D(camera_target_[0], + overlay_frame.camera_target = Eigen::Vector3f(camera_target_[0], camera_target_[1], camera_target_[2]); overlay_frame.camera_distance = camera_distance_; @@ -5289,9 +5281,9 @@ void ViewportWindow::render() { // — same metric driveStreamingLoads uses for priority, // duplicated here so the heartbeat dump can show what // the loader is actually scoring chunks at. - QMatrix4x4 v_dbg, p_dbg; + Eigen::Matrix4f v_dbg, p_dbg; buildViewProj(v_dbg, p_dbg); - const QMatrix4x4 vp_dbg = p_dbg * v_dbg; + const Eigen::Matrix4f vp_dbg = p_dbg * v_dbg; auto chunk_priority_px2 = [&](const ModelGpuData::Chunk& c) -> float { if (configured_w_ <= 0 || configured_h_ <= 0 || c.aabb_min[0] > c.aabb_max[0]) return 0.0f; @@ -5301,12 +5293,12 @@ void ViewportWindow::render() { float ymax = -std::numeric_limits::infinity(); int cif = 0; for (int i = 0; i < 8; ++i) { - const QVector4D corner( + const Eigen::Vector4f corner( (i & 1) ? c.aabb_max[0] : c.aabb_min[0], (i & 2) ? c.aabb_max[1] : c.aabb_min[1], (i & 4) ? c.aabb_max[2] : c.aabb_min[2], 1.0f); - const QVector4D clip = vp_dbg * corner; + const Eigen::Vector4f clip = vp_dbg * corner; if (clip.w() <= 1e-3f) continue; ++cif; const float px_x = (clip.x() / clip.w() * 0.5f + 0.5f) * float(configured_w_); @@ -6196,9 +6188,9 @@ void ViewportWindow::driveStreamingLoads() { // Build the camera's view-projection (still needed for the AABB-based // diagnostic dump in the tracking output below). Cull/render use the // same helper. - QMatrix4x4 v_mat, p_mat; + Eigen::Matrix4f v_mat, p_mat; buildViewProj(v_mat, p_mat); - const QMatrix4x4 vp_mat = p_mat * v_mat; + const Eigen::Matrix4f vp_mat = p_mat * v_mat; // chunk.current_priority was accumulated during cullModelCpuCompute // (one add per frustum-passing instance). No standalone walk needed @@ -6738,7 +6730,7 @@ void ViewportWindow::releaseMsaaColorTexture() { // rotation about Z (positive = anticlockwise looking down +Z); pitch is // elevation above the XY plane. -static QVector3D orbitEye(const float target[3], float dist, +static Eigen::Vector3f 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. @@ -6749,7 +6741,7 @@ static QVector3D orbitEye(const float target[3], float dist, 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 * cy, + return Eigen::Vector3f(target[0] + dist * cp * cy, target[1] + dist * cp * sy, target[2] + dist * sp); } @@ -6758,26 +6750,25 @@ static QVector3D orbitEye(const float target[3], float dist, // streaming projection, pick, or render uniforms calls this so the // projection_ortho_ toggle and the near-vertical up-vector switch land // identically everywhere. -void ViewportWindow::buildViewProj(QMatrix4x4& view_out, - QMatrix4x4& proj_out) const { - const QVector3D target(camera_target_[0], camera_target_[1], camera_target_[2]); - const QVector3D eye = orbitEye(camera_target_, camera_distance_, +void ViewportWindow::buildViewProj(Eigen::Matrix4f& view_out, + Eigen::Matrix4f& proj_out) const { + const Eigen::Vector3f target(camera_target_[0], camera_target_[1], camera_target_[2]); + const Eigen::Vector3f eye = orbitEye(camera_target_, camera_distance_, camera_yaw_deg_, camera_pitch_deg_); // Within 1° of straight-up/down, switch up from world +Z to world +Y // so lookAt's side vector doesn't degenerate (forward × up → 0). Mirrors // GL ViewportWindow::updateCamera; the standard-view top/bottom hotkeys // land at pitch = ±90° exactly so this is the path that keeps them // well-conditioned. - const QVector3D up = (std::abs(camera_pitch_deg_) >= 89.0f) - ? QVector3D(0.0f, 1.0f, 0.0f) - : QVector3D(0.0f, 0.0f, 1.0f); - view_out.setToIdentity(); - view_out.lookAt(eye, target, up); + const Eigen::Vector3f up = (std::abs(camera_pitch_deg_) >= 89.0f) + ? Eigen::Vector3f(0.0f, 1.0f, 0.0f) + : Eigen::Vector3f(0.0f, 0.0f, 1.0f); + view_out = lookAtRH(eye, target, up); const float aspect = (configured_h_ > 0) ? float(configured_w_) / float(configured_h_) : 1.0f; - QMatrix4x4 p; + Eigen::Matrix4f p; if (projection_ortho_) { // Size the ortho box so the same world rectangle fills the view as // the perspective camera at the pivot's distance. Toggling at any @@ -6786,33 +6777,35 @@ void ViewportWindow::buildViewProj(QMatrix4x4& view_out, * std::tan(qDegreesToRadians(camera_fov_y_deg_ * 0.5f)); const float half_w = half_h * aspect; const float depth = camera_distance_ * 10.0f; - p.ortho(-half_w, half_w, -half_h, half_h, -depth, depth); + p = orthoGL(-half_w, half_w, -half_h, half_h, -depth, depth); } else { - p.perspective(camera_fov_y_deg_, aspect, camera_near_, camera_far_); + p = perspectiveYFovGL(camera_fov_y_deg_, aspect, camera_near_, camera_far_); } - // Qt builds a GL-style projection (clip-z in [-1, 1]); WebGPU expects - // clip-z in [0, 1]. Pre-multiply by a remap matrix that maps [-1,1] → [0,1]. - QMatrix4x4 z_remap; + // The helpers above build a GL-style projection (clip-z in [-1, 1]); + // WebGPU expects clip-z in [0, 1]. Pre-multiply by a remap matrix + // that maps [-1,1] → [0,1]. Note we start z_remap from Identity() + // (Eigen doesn't zero-init); QMatrix4x4 used to do this implicitly. + Eigen::Matrix4f z_remap = Eigen::Matrix4f::Identity(); z_remap(2, 2) = 0.5f; z_remap(2, 3) = 0.5f; proj_out = z_remap * p; } void ViewportWindow::updateFrameUniforms() { - QMatrix4x4 view, proj; + Eigen::Matrix4f view, proj; buildViewProj(view, proj); - const QMatrix4x4 view_proj = proj * view; + const Eigen::Matrix4f view_proj = proj * view; FrameUniforms u = {}; - std::memcpy(u.view_proj, view_proj.constData(), 16 * sizeof(float)); + std::memcpy(u.view_proj, view_proj.data(), 16 * sizeof(float)); // Values match the GL viewport's main fragment shader so a side-by-side // diff of the two backends only shows what the wgpu pipeline has yet to // implement (edge silhouette pass, MSAA polish, etc.) — not lighting // model differences. Key + fill are ~unit-length, ~120° apart. - QVector3D L( 0.3f, 0.5f, 0.8f); L.normalize(); - QVector3D F(-0.3f, -0.5f, 0.8f); F.normalize(); + Eigen::Vector3f L( 0.3f, 0.5f, 0.8f); L.normalize(); + Eigen::Vector3f F(-0.3f, -0.5f, 0.8f); F.normalize(); u.light_dir[0] = L.x(); u.light_dir[1] = L.y(); u.light_dir[2] = L.z(); u.light_dir[3] = 0; u.fill_dir [0] = F.x(); u.fill_dir [1] = F.y(); u.fill_dir [2] = F.z(); u.fill_dir [3] = 0; u.sky_color [0] = 0.55f; u.sky_color [1] = 0.60f; u.sky_color [2] = 0.70f; @@ -6935,11 +6928,11 @@ void ViewportWindow::frameAabb(const float mn[3], const float mx[3], } bool ViewportWindow::computeObjectAabb(uint32_t object_id, - QVector3D& mn, QVector3D& mx) const { + Eigen::Vector3f& mn, Eigen::Vector3f& mx) const { float fmin[3], fmax[3]; if (!computeObjectAabb(object_id, fmin, fmax)) return false; - mn = QVector3D(fmin[0], fmin[1], fmin[2]); - mx = QVector3D(fmax[0], fmax[1], fmax[2]); + mn = Eigen::Vector3f(fmin[0], fmin[1], fmin[2]); + mx = Eigen::Vector3f(fmax[0], fmax[1], fmax[2]); return true; } @@ -7053,27 +7046,27 @@ void ViewportWindow::fpsIntegrate() { // Forward = orbit eye -> target, kept as the camera's view direction in // fly mode too so a Shift+F right after orbiting doesn't snap to a new // heading. WASD moves in the screen plane; QE rises/falls along world +Z. - const QVector3D target(camera_target_[0], camera_target_[1], camera_target_[2]); - const QVector3D eye = orbitEye(camera_target_, camera_distance_, + const Eigen::Vector3f target(camera_target_[0], camera_target_[1], camera_target_[2]); + const Eigen::Vector3f eye = orbitEye(camera_target_, camera_distance_, camera_yaw_deg_, camera_pitch_deg_); - QVector3D forward = (target - eye); forward.normalize(); + Eigen::Vector3f forward = (target - eye); forward.normalize(); // When looking straight up/down, cross(forward, worldZ) degenerates; // fall back to worldY so right doesn't go NaN and WASD still works. - const QVector3D world_up(0.0f, 0.0f, 1.0f); - const QVector3D right_basis = (std::abs(camera_pitch_deg_) >= 89.0f) - ? QVector3D(0.0f, 1.0f, 0.0f) + const Eigen::Vector3f world_up(0.0f, 0.0f, 1.0f); + const Eigen::Vector3f right_basis = (std::abs(camera_pitch_deg_) >= 89.0f) + ? Eigen::Vector3f(0.0f, 1.0f, 0.0f) : world_up; - QVector3D right = QVector3D::crossProduct(forward, right_basis); + Eigen::Vector3f right = forward.cross(right_basis); right.normalize(); - QVector3D move(0, 0, 0); + Eigen::Vector3f move(0, 0, 0); if (fps_keys_held_.contains(Qt::Key_W)) move += forward; if (fps_keys_held_.contains(Qt::Key_S)) move -= forward; if (fps_keys_held_.contains(Qt::Key_D)) move += right; if (fps_keys_held_.contains(Qt::Key_A)) move -= right; if (fps_keys_held_.contains(Qt::Key_E)) move += world_up; if (fps_keys_held_.contains(Qt::Key_Q)) move -= world_up; - if (move.isNull()) return; + if (move.isZero()) return; move.normalize(); // Absolute m/s, scrollwheel-adjustable (Blender / GL convention). @@ -7082,7 +7075,7 @@ void ViewportWindow::fpsIntegrate() { // changing it underneath fly mode). const float speed = fps_move_speed_ * (fps_keys_held_.contains(Qt::Key_Shift) ? 5.0f : 1.0f); - const QVector3D delta = move * (speed * dt); + const Eigen::Vector3f delta = move * (speed * dt); camera_target_[0] += delta.x(); camera_target_[1] += delta.y(); @@ -7100,12 +7093,12 @@ void ViewportWindow::fpsIntegrate() { << " render_gap=" << QString::number(double(since_render_ns) / 1e6, 'f', 2) << "ms" << " keys=" << fps_keys_held_.size() << " speed=" << QString::number(speed, 'f', 2) << "m/s" - << " delta=" << QString::number(delta.length(), 'f', 4) << "m"; + << " delta=" << QString::number(delta.norm(), 'f', 4) << "m"; } } float ViewportWindow::chunkScreenAreaPx(const ModelGpuData::Chunk& c, - const QMatrix4x4& vp_mat) const { + const Eigen::Matrix4f& vp_mat) const { if (configured_w_ <= 0 || configured_h_ <= 0) return 0.0f; if (c.aabb_min[0] > c.aabb_max[0]) return 0.0f; const float full_area = float(configured_w_) * float(configured_h_); @@ -7127,7 +7120,7 @@ float ViewportWindow::chunkScreenAreaPx(const ModelGpuData::Chunk& c, // the chunk's true on-screen extent is unmeasurable from 8 corners // alone once any are behind, so over-prioritise rather than // under-prioritise. - const QVector3D eye = orbitEye(camera_target_, camera_distance_, + const Eigen::Vector3f eye = orbitEye(camera_target_, camera_distance_, camera_yaw_deg_, camera_pitch_deg_); if (eye.x() >= c.aabb_min[0] && eye.x() <= c.aabb_max[0] && eye.y() >= c.aabb_min[1] && eye.y() <= c.aabb_max[1] && @@ -7142,12 +7135,12 @@ float ViewportWindow::chunkScreenAreaPx(const ModelGpuData::Chunk& c, int corners_in_front = 0; int corners_behind = 0; for (int i = 0; i < 8; ++i) { - const QVector4D corner_world( + const Eigen::Vector4f corner_world( (i & 1) ? c.aabb_max[0] : c.aabb_min[0], (i & 2) ? c.aabb_max[1] : c.aabb_min[1], (i & 4) ? c.aabb_max[2] : c.aabb_min[2], 1.0f); - const QVector4D clip = vp_mat * corner_world; + const Eigen::Vector4f clip = vp_mat * corner_world; if (clip.w() <= 1e-3f) { ++corners_behind; continue; } ++corners_in_front; const float ndc_x = clip.x() / clip.w(); @@ -7355,7 +7348,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) { if (section_tool_active_ && event->modifiers() == Qt::NoModifier) { uint32_t hit_id = 0; - QVector3D hit_pos, hit_normal; + Eigen::Vector3f hit_pos, hit_normal; float hit_radius = 0.0f; if (pickSurfaceAt(px, py, hit_id, hit_pos, hit_normal, &hit_radius)) { @@ -7533,7 +7526,7 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) { const int dy = pos.y() - fps_press_center_.y(); // Save eye BEFORE rotating so we can pin it after. - const QVector3D pinned_eye = orbitEye(camera_target_, camera_distance_, + const Eigen::Vector3f pinned_eye = orbitEye(camera_target_, camera_distance_, camera_yaw_deg_, camera_pitch_deg_); // Convention: mouse-up looks up, mouse-down looks down (non-inverted). @@ -7596,15 +7589,15 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) { // the world-Z up-reference degenerates (cross with forward is the // zero vector → NaN), so switch to world-Y up — matches the // up-vector switch in buildViewProj so top/bottom views still pan. - const QVector3D target(camera_target_[0], camera_target_[1], camera_target_[2]); - const QVector3D eye = orbitEye(camera_target_, camera_distance_, + const Eigen::Vector3f target(camera_target_[0], camera_target_[1], camera_target_[2]); + const Eigen::Vector3f eye = orbitEye(camera_target_, camera_distance_, camera_yaw_deg_, camera_pitch_deg_); - const QVector3D fwd = (target - eye).normalized(); - 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); - const QVector3D right = QVector3D::crossProduct(fwd, world_up).normalized(); - const QVector3D up = QVector3D::crossProduct(right, fwd).normalized(); + const Eigen::Vector3f fwd = (target - eye).normalized(); + const Eigen::Vector3f world_up = (std::abs(camera_pitch_deg_) >= 89.0f) + ? Eigen::Vector3f(0.0f, 1.0f, 0.0f) + : Eigen::Vector3f(0.0f, 0.0f, 1.0f); + const Eigen::Vector3f right = fwd.cross(world_up).normalized(); + const Eigen::Vector3f up = right.cross(fwd).normalized(); const float half_h_world = camera_distance_ * std::tan(qDegreesToRadians(camera_fov_y_deg_) * 0.5f); @@ -7612,7 +7605,7 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) { ? (2.0f * half_h_world / float(height())) : 0.0f; - const QVector3D shift = -right * (float(dx) * pan_per_pixel) + const Eigen::Vector3f shift = -right * (float(dx) * pan_per_pixel) + up * (float(dy) * pan_per_pixel); camera_target_[0] += shift.x(); camera_target_[1] += shift.y(); diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 0bb1d11b74..b640c54417 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -178,7 +177,7 @@ public: // Snapshot of the orbit camera. Mirrors GL ViewportWindow::CameraState // so bonsai's "save view" / "restore view" commands port unchanged. struct CameraState { - QVector3D target; + Eigen::Vector3f target; float distance; float yaw; // degrees float pitch; // degrees @@ -218,7 +217,7 @@ private: // the view matrix and a WebGPU-correct projection (z mapped to [0, 1]). // Single helper so projection_ortho_ and the up-vector switch at near- // vertical pitch land identically everywhere. - void buildViewProj(QMatrix4x4& view_out, QMatrix4x4& proj_out) const; + void buildViewProj(Eigen::Matrix4f& view_out, Eigen::Matrix4f& proj_out) const; // Per-frame WASD integration when fps_mode_ is true. Called near the // top of render() so the displayed frame already reflects movement. void fpsIntegrate(); @@ -226,11 +225,11 @@ private: bool computeObjectAabb(uint32_t object_id, float mn[3], float mx[3]) const; public: - // QVector3D overload — matches GL ViewportWindow::computeObjectAabb so + // Eigen::Vector3f overload — matches GL ViewportWindow::computeObjectAabb so // bonsai's volume readout / focus callers compile unchanged. Just a // thin wrapper around the float[3] version. bool computeObjectAabb(uint32_t object_id, - QVector3D& mn, QVector3D& mx) const; + Eigen::Vector3f& mn, Eigen::Vector3f& mx) const; private: // Re-aim the orbit camera so the bounding sphere of [mn, mx] fits. void frameAabb(const float mn[3], const float mx[3], float padding); @@ -242,7 +241,7 @@ private: // chunk-priority metric — extracted from driveStreamingLoads as a // member so the click-and-track diagnostic can compare scores. float chunkScreenAreaPx(const ModelGpuData::Chunk& c, - const QMatrix4x4& vp_mat) const; + const Eigen::Matrix4f& vp_mat) const; public: @@ -346,7 +345,7 @@ private: // (decoded from ×0.5+0.5 packing) so the section tool can drop // perpendicular cuts. uint32_t pickObjectAt(int x_pixels, int y_pixels, - QVector3D* normal_out = nullptr); + Eigen::Vector3f* normal_out = nullptr); // Pick + ray-cast — returns the object's id, the world-space point // where the pick-pixel pillar enters that instance's AABB, and a // camera-facing normal. Returns false on a background miss. We do @@ -356,8 +355,8 @@ private: // enough to the click for the section tool's "drop a plane here" UX. bool pickSurfaceAt(int x_pixels, int y_pixels, uint32_t& object_id_out, - QVector3D& world_pos_out, - QVector3D& world_normal_out, + Eigen::Vector3f& world_pos_out, + Eigen::Vector3f& world_normal_out, float* aabb_radius_out = nullptr); // Rectangle pick: render the pick pass, copy the rect region of the // R32UInt color attachment, and return every unique non-zero @@ -374,8 +373,8 @@ public: // Esc deactivate tool bool sectionToolActive() const { return section_tool_active_; } void toggleSectionTool(); - bool addSectionPlaneAtSurface(const QVector3D& point, - const QVector3D& normal, + bool addSectionPlaneAtSurface(const Eigen::Vector3f& point, + const Eigen::Vector3f& normal, float visual_radius = 0.0f); void removeSectionPlane(int index); void clearSectionPlanes(); @@ -561,7 +560,7 @@ private: int encodeHizResolve(WGPUCommandEncoder enc); // Issues a non-blocking mapAsync on `slot` after submit, so the // callback can fire whenever the GPU has actually finished writing. - void startHizMap(int slot, const QMatrix4x4& vp_used); + void startHizMap(int slot, const Eigen::Matrix4f& vp_used); // Drains pending mapAsync callbacks (via processEvents — does NOT // block on GPU work). For any slot that just signalled Mapped, reads // it, unmaps it, max-reduces the mip pyramid, and updates hiz_vp_. @@ -826,7 +825,7 @@ private: bool section_drag_active_ = false; int section_drag_index_ = -1; QPoint section_drag_start_mouse_; - QVector3D section_drag_start_origin_; + Eigen::Vector3f section_drag_start_origin_; // Mirrors GL ViewportWindow::hitTestSectionGizmo: returns the index of // the plane whose arrow gizmo is within grab_px of (x, y), or -1. int hitTestSectionGizmo(int x, int y) const; @@ -838,7 +837,7 @@ private: enum class HizSlotState : uint8_t { Idle, Mapping, Mapped }; static constexpr int HIZ_SLOTS = 2; WGPUBuffer hiz_staging_buffers_[HIZ_SLOTS] = { nullptr, nullptr }; - QMatrix4x4 hiz_slot_vp_ [HIZ_SLOTS]; + Eigen::Matrix4f hiz_slot_vp_ [HIZ_SLOTS]; HizSlotState hiz_slot_state_ [HIZ_SLOTS] = { HizSlotState::Idle, HizSlotState::Idle }; int hiz_write_idx_ = 0; @@ -848,7 +847,7 @@ private: std::vector hiz_mip_offset_; std::vector hiz_mip_w_; std::vector hiz_mip_h_; - QMatrix4x4 hiz_vp_; + Eigen::Matrix4f hiz_vp_; bool hiz_valid_ = false; uint32_t hiz_reject_count_ = 0; // per-frame stat