ifcviewer: de-Qt math types (Eigen everywhere)

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<const Matrix4f>(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 <QVector3D> include in favour of <Eigen/Dense> (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.
This commit is contained in:
Dion Moult
2026-06-05 08:33:22 +10:00
parent c314dd3ca8
commit 77cf535b45
8 changed files with 305 additions and 231 deletions
+2 -3
View File
@@ -30,7 +30,6 @@
#include "../../Measurement.h"
#include <Eigen/Dense>
#include <QVector3D>
#include <vector>
@@ -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();
+85
View File
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#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 <Eigen/Dense>
#include <cmath>
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
+3 -3
View File
@@ -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));
+4 -5
View File
@@ -27,7 +27,6 @@
#include <QString>
#include <QStringList>
#include <QDateTime>
#include <QVector3D>
#include <memory>
#include <optional>
@@ -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 {
+31 -31
View File
@@ -19,6 +19,8 @@
#include "OverlayRenderer.h"
#include "CameraMath.h"
#include <QFont>
#include <QFontMetrics>
#include <QImage>
@@ -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];
+9 -10
View File
@@ -21,10 +21,10 @@
#define WGPUOVERLAYRENDERER_H
#include <QHash>
#include <QMatrix4x4>
#include <QPoint>
#include <QString>
#include <QVector3D>
#include <Eigen/Dense>
#include <webgpu/webgpu.h>
@@ -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),
+157 -164
View File
@@ -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 <QElapsedTimer>
#include <QFile>
#include <QFileInfo>
#include <QMatrix4x4>
#include <QVector3D>
#include <QtMath>
#include <webgpu/wgpu.h> // 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<float>::infinity();
float t_max = std::numeric_limits<float>::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<uint32_t> 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<float>::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<const Eigen::Matrix4f>(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<const Eigen::Matrix4f>(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<float>::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();
+14 -15
View File
@@ -23,7 +23,6 @@
#include <QWindow>
#include <QColor>
#include <QElapsedTimer>
#include <QMatrix4x4>
#include <QPoint>
#include <QSet>
#include <QString>
@@ -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<uint32_t> hiz_mip_offset_;
std::vector<uint32_t> hiz_mip_w_;
std::vector<uint32_t> hiz_mip_h_;
QMatrix4x4 hiz_vp_;
Eigen::Matrix4f hiz_vp_;
bool hiz_valid_ = false;
uint32_t hiz_reject_count_ = 0; // per-frame stat