mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-10 22:16:41 +00:00
ifcviewer: first-person / fly view — share the desktop fly camera with web
DRY the fly camera into ViewportCore so desktop and web share identical math: - flyMove(fwd,back,right,left,up,down,boost,dt): WASD in the view plane, QE along world +Z, forward = eye→target (no snap after orbiting), Shift = 5x, dt clamped to 0.1s. flyLook(dx,dy): turn in place (yaw/pitch, eye pinned, 0.2 deg/px, pitch ±89.9). flyAdjustSpeed(notches): wheel scales speed x1.25. fly_move_speed_ + orbitEye now live in the core (orbitEye's duplicate removed from ViewportWindow). - Desktop ViewportWindow: fpsIntegrate / mouse-look / wheel-speed call the core methods; behaviour unchanged, BonsaiViewer builds clean. - Web main_web: Shift+F (or the Fly toolbar button) enters and pointer-locks the canvas; W/A/S/D/Q/E + Shift are held-tracked (keydown/keyup) and integrated each RAF frame with wall-clock dt; pointer-lock mouse deltas drive flyLook; wheel tunes speed. Exit on Esc OR a canvas click (matches desktop) — a pointerlockchange handler catches the browser eating the first Esc to release the lock (so a single Esc exits), guarded by fly_locked so a denied lock on entry doesn't insta-exit. Fly button reflects/ syncs active state. Verified: web enter→WASD moves→click/Esc exits, 0 GPU errors; 113/113 desktop core + 6/6 web smoke; desktop app builds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -526,6 +526,66 @@ void ViewportCore::dollyBy(float notches) {
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
void ViewportCore::flyMove(bool fwd, bool back, bool right, bool left,
|
||||
bool up, bool down, bool boost, float dt_seconds) {
|
||||
if (dt_seconds <= 0.0f) return;
|
||||
if (dt_seconds > 0.1f) dt_seconds = 0.1f; // stall clamp
|
||||
// Forward = orbit eye -> target, kept as the view direction in fly mode too
|
||||
// so entering fly right after orbiting doesn't snap to a new heading.
|
||||
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_);
|
||||
Eigen::Vector3f forward = target - eye;
|
||||
if (forward.norm() < 1e-6f) return;
|
||||
forward.normalize();
|
||||
// Looking straight up/down degenerates cross(forward, worldZ); fall back to
|
||||
// worldY so `right` doesn't go NaN.
|
||||
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;
|
||||
Eigen::Vector3f right_vec = forward.cross(right_basis);
|
||||
right_vec.normalize();
|
||||
Eigen::Vector3f move(0.0f, 0.0f, 0.0f);
|
||||
if (fwd) move += forward;
|
||||
if (back) move -= forward;
|
||||
if (right) move += right_vec;
|
||||
if (left) move -= right_vec;
|
||||
if (up) move += world_up;
|
||||
if (down) move -= world_up;
|
||||
if (move.isZero()) return;
|
||||
move.normalize();
|
||||
const float speed = fly_move_speed_ * (boost ? 5.0f : 1.0f); // absolute m/s
|
||||
const Eigen::Vector3f delta = move * (speed * dt_seconds);
|
||||
camera_target_[0] += delta.x();
|
||||
camera_target_[1] += delta.y();
|
||||
camera_target_[2] += delta.z();
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
void ViewportCore::flyLook(float dx_px, float dy_px) {
|
||||
// Turn in place: pin the eye, rotate yaw/pitch, then re-derive the orbit
|
||||
// target so orbitEye(target, dist, new_yaw, new_pitch) == the pinned eye.
|
||||
const Eigen::Vector3f pinned_eye = orbitEye(camera_target_, camera_distance_,
|
||||
camera_yaw_deg_, camera_pitch_deg_);
|
||||
camera_yaw_deg_ -= dx_px * 0.2f;
|
||||
camera_pitch_deg_ += dy_px * 0.2f;
|
||||
camera_pitch_deg_ = std::clamp(camera_pitch_deg_, -89.9f, 89.9f);
|
||||
constexpr float kDeg2Rad = 0.01745329251994329577f;
|
||||
const float yaw = camera_yaw_deg_ * kDeg2Rad;
|
||||
const float pit = camera_pitch_deg_ * kDeg2Rad;
|
||||
const float cp = std::cos(pit), sp = std::sin(pit);
|
||||
const float cy = std::cos(yaw), sy = std::sin(yaw);
|
||||
camera_target_[0] = pinned_eye.x() - camera_distance_ * cp * cy;
|
||||
camera_target_[1] = pinned_eye.y() - camera_distance_ * cp * sy;
|
||||
camera_target_[2] = pinned_eye.z() - camera_distance_ * sp;
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
void ViewportCore::flyAdjustSpeed(float notches) {
|
||||
const float factor = std::pow(1.25f, notches); // up = faster
|
||||
fly_move_speed_ = std::clamp(fly_move_speed_ * factor, 0.05f, 1000.0f);
|
||||
}
|
||||
|
||||
void ViewportCore::toggleProjection() {
|
||||
projection_ortho_ = !projection_ortho_;
|
||||
std::fprintf(stderr, "[info] [wgpu] projection: %s\n",
|
||||
|
||||
@@ -217,6 +217,25 @@ public:
|
||||
void panBy(float dx_px, float dy_px, int viewport_height_px);
|
||||
void dollyBy(float notches);
|
||||
|
||||
// ---- First-person / fly navigation --------------------------------------
|
||||
//
|
||||
// Shared fly-camera math (desktop + web). The HOST owns the fly-mode flag,
|
||||
// held-key tracking, cursor/pointer-lock, and per-frame timing; it calls
|
||||
// these each frame while flying. Behaviour is identical across platforms.
|
||||
//
|
||||
// flyMove: WASD moves in the view plane, QE rises/falls along world +Z;
|
||||
// forward = eye→target so it doesn't snap after orbiting. `boost`
|
||||
// (Shift) is 5x. Speed is metres/second (flyAdjustSpeed tunes it);
|
||||
// `dt` seconds is clamped to 0.1 so a stall can't warp the camera.
|
||||
// flyLook: mouse-look — turn the camera in place (yaw/pitch) with the eye
|
||||
// pinned; pixel deltas, 0.2 deg/px, pitch clamped to ±89.9.
|
||||
// flyAdjustSpeed: wheel scales the move speed (x1.25 per notch, clamped).
|
||||
void flyMove(bool fwd, bool back, bool right, bool left,
|
||||
bool up, bool down, bool boost, float dt_seconds);
|
||||
void flyLook(float dx_px, float dy_px);
|
||||
void flyAdjustSpeed(float notches);
|
||||
float flySpeed() const { return fly_move_speed_; }
|
||||
|
||||
void toggleProjection();
|
||||
bool projectionOrtho() const { return projection_ortho_; }
|
||||
std::string cameraString() const;
|
||||
@@ -1116,6 +1135,9 @@ private:
|
||||
float camera_fov_y_deg_ = 45.0f;
|
||||
float camera_near_ = 0.1f;
|
||||
float camera_far_ = 10000.0f;
|
||||
// Fly-camera move speed (m/s), wheel-adjustable via flyAdjustSpeed. Shared
|
||||
// by desktop + web fly mode; the mode flag itself lives in each host.
|
||||
float fly_move_speed_ = 5.0f;
|
||||
// Perspective by default; toggleProjection (P key) flips this. When
|
||||
// true, buildViewProj uses an orthographic matrix sized by
|
||||
// camera_distance_ × tan(fov/2) so toggling looks like a smooth
|
||||
|
||||
@@ -77,10 +77,6 @@ static inline float srgbToLinear(float s) {
|
||||
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 Eigen::Vector3f orbitEye(const float target[3], float dist,
|
||||
float yaw_deg, float pitch_deg);
|
||||
|
||||
// computeMeshLocalVolumeQuantised moved to ViewportCore.cpp anon
|
||||
// namespace (#84-n).
|
||||
@@ -1467,21 +1463,8 @@ void ViewportWindow::driveStreamingLoads() { core_.driveStreamingLoads(); }
|
||||
// rotation about Z (positive = anticlockwise looking down +Z); pitch is
|
||||
// elevation above the XY plane.
|
||||
|
||||
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.
|
||||
// eye.x = target.x + dist * cos(pitch) * cos(yaw)
|
||||
// eye.y = target.y + dist * cos(pitch) * sin(yaw)
|
||||
// eye.z = target.z + dist * sin(pitch)
|
||||
const float yaw = qDegreesToRadians(yaw_deg);
|
||||
const float pit = qDegreesToRadians(pitch_deg);
|
||||
const float cp = std::cos(pit), sp = std::sin(pit);
|
||||
const float cy = std::cos(yaw), sy = std::sin(yaw);
|
||||
return Eigen::Vector3f(target[0] + dist * cp * cy,
|
||||
target[1] + dist * cp * sy,
|
||||
target[2] + dist * sp);
|
||||
}
|
||||
// orbitEye moved to ViewportCore (its last ViewportWindow uses — fly-mode step
|
||||
// + mouse-look — now go through ViewportCore::flyMove / flyLook).
|
||||
|
||||
// Shared camera-math helper. Every site that needs (view, proj) for cull,
|
||||
// streaming projection, pick, or render uniforms calls this so the
|
||||
@@ -1565,58 +1548,12 @@ void ViewportWindow::fpsIntegrate() {
|
||||
float dt = float(double(elapsed_ns) / 1e9);
|
||||
if (dt > 0.1f) dt = 0.1f;
|
||||
|
||||
// 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 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_);
|
||||
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 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;
|
||||
Eigen::Vector3f right = forward.cross(right_basis);
|
||||
right.normalize();
|
||||
|
||||
Eigen::Vector3f move(0, 0, 0);
|
||||
if (fps_keys_held_.count(Qt::Key_W)) move += forward;
|
||||
if (fps_keys_held_.count(Qt::Key_S)) move -= forward;
|
||||
if (fps_keys_held_.count(Qt::Key_D)) move += right;
|
||||
if (fps_keys_held_.count(Qt::Key_A)) move -= right;
|
||||
if (fps_keys_held_.count(Qt::Key_E)) move += world_up;
|
||||
if (fps_keys_held_.count(Qt::Key_Q)) move -= world_up;
|
||||
if (move.isZero()) return;
|
||||
move.normalize();
|
||||
|
||||
// Absolute m/s, scrollwheel-adjustable (Blender / GL convention).
|
||||
// Scaling with camera_distance_ produced "stuttery" speed on big scenes
|
||||
// because distance varies frame-to-frame (and worse, wheel zoom kept
|
||||
// changing it underneath fly mode).
|
||||
const float speed = fps_move_speed_
|
||||
* (fps_keys_held_.count(Qt::Key_Shift) ? 5.0f : 1.0f);
|
||||
const Eigen::Vector3f delta = move * (speed * dt);
|
||||
|
||||
camera_target_[0] += delta.x();
|
||||
camera_target_[1] += delta.y();
|
||||
camera_target_[2] += delta.z();
|
||||
requestUpdate();
|
||||
|
||||
if (fly_debug_) {
|
||||
// dt timeline: see if values jitter (under/over-integration symptoms).
|
||||
// Show in ms with 2dp so small jumps are visible.
|
||||
const qint64 since_render_ns = fly_render_clock_.isValid()
|
||||
? fly_render_clock_.nsecsElapsed() : 0;
|
||||
fly_render_clock_.restart();
|
||||
Log::info().noquote().nospace()
|
||||
<< "[fly] dt=" << QString::number(dt * 1000.0f, 'f', 2) << "ms"
|
||||
<< " 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.norm(), 'f', 4) << "m";
|
||||
}
|
||||
// Fly-camera math lives in ViewportCore (shared with the web path).
|
||||
core_.flyMove(
|
||||
fps_keys_held_.count(Qt::Key_W) != 0, fps_keys_held_.count(Qt::Key_S) != 0,
|
||||
fps_keys_held_.count(Qt::Key_D) != 0, fps_keys_held_.count(Qt::Key_A) != 0,
|
||||
fps_keys_held_.count(Qt::Key_E) != 0, fps_keys_held_.count(Qt::Key_Q) != 0,
|
||||
fps_keys_held_.count(Qt::Key_Shift) != 0, dt);
|
||||
}
|
||||
|
||||
// chunkScreenAreaPx moved to ViewportCore (#84-h).
|
||||
@@ -1980,32 +1917,9 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) {
|
||||
const int dx = pos.x() - fps_press_center_.x();
|
||||
const int dy = pos.y() - fps_press_center_.y();
|
||||
|
||||
// Save eye BEFORE rotating so we can pin it after.
|
||||
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).
|
||||
// orbitEye stores pitch with sin(pitch) controlling eye.z relative to
|
||||
// target → larger pitch = eye higher = looking down. To make mouse-up
|
||||
// (dy<0) look up (i.e. raise pitch in our stored convention so the
|
||||
// camera tilts down toward the target… wait, with eye pinned in FPS
|
||||
// mode the relationship inverts: increasing pitch pulls *target* up,
|
||||
// which means forward tilts down). Net: dy>0 (down) increases pitch
|
||||
// → forward tilts down → looking down. `+=` is correct here even
|
||||
// though orbit-mode also uses `+=` for the opposite visual reason.
|
||||
camera_yaw_deg_ -= float(dx) * 0.2f;
|
||||
camera_pitch_deg_ += float(dy) * 0.2f;
|
||||
camera_pitch_deg_ = std::clamp(camera_pitch_deg_, -89.9f, 89.9f);
|
||||
|
||||
// Re-derive target so orbitEye(target, dist, new_yaw, new_pitch) ==
|
||||
// pinned_eye. eye = target + dist*(cp*cy, cp*sy, sp) → invert.
|
||||
const float yaw = qDegreesToRadians(camera_yaw_deg_);
|
||||
const float pit = qDegreesToRadians(camera_pitch_deg_);
|
||||
const float cp = std::cos(pit), sp = std::sin(pit);
|
||||
const float cy = std::cos(yaw), sy = std::sin(yaw);
|
||||
camera_target_[0] = pinned_eye.x() - camera_distance_ * cp * cy;
|
||||
camera_target_[1] = pinned_eye.y() - camera_distance_ * cp * sy;
|
||||
camera_target_[2] = pinned_eye.z() - camera_distance_ * sp;
|
||||
// Mouse-look math (turn-in-place) lives in ViewportCore, shared with
|
||||
// the web pointer-lock path.
|
||||
core_.flyLook(float(dx), float(dy));
|
||||
|
||||
fps_ignore_next_mouse_move_ = true;
|
||||
QCursor::setPos(mapToGlobal(QPoint(fps_press_center_.x(), fps_press_center_.y())));
|
||||
@@ -2231,10 +2145,9 @@ void ViewportWindow::wheelEvent(QWheelEvent* event) {
|
||||
// Zooming would re-aim the orbit pivot and yank speed (if it were
|
||||
// distance-scaled) — neither belongs in a free-fly camera.
|
||||
if (fps_mode_) {
|
||||
const float factor = std::pow(1.25f, notches);
|
||||
fps_move_speed_ = std::clamp(fps_move_speed_ * factor, 0.05f, 1000.0f);
|
||||
core_.flyAdjustSpeed(notches); // shared: x1.25/notch, clamped
|
||||
Log::info().noquote().nospace()
|
||||
<< "[wgpu] fly speed: " << QString::number(fps_move_speed_, 'f', 2) << " m/s";
|
||||
<< "[wgpu] fly speed: " << QString::number(core_.flySpeed(), 'f', 2) << " m/s";
|
||||
return;
|
||||
}
|
||||
// Orbit mode: zoom in/out around the pivot (math in ViewportCore).
|
||||
|
||||
@@ -779,12 +779,8 @@ private:
|
||||
Stopwatch fps_last_tick_;
|
||||
Eigen::Vector2i fps_press_center_;
|
||||
bool fps_ignore_next_mouse_move_ = false;
|
||||
// Fly base speed in m/s at no-modifier (Shift gives a 5× boost). Default
|
||||
// 5.0 matches GL fps_move_speed_. Scrollwheel in fly mode adjusts this
|
||||
// by ×1.25 / ×0.8 per notch, Blender-style — wheel does NOT zoom while
|
||||
// in fly mode (which would change camera_distance_ underneath us and
|
||||
// make speed jitter if speed were distance-scaled).
|
||||
float fps_move_speed_ = 5.0f;
|
||||
// Fly base speed (m/s) + wheel adjustment now live in ViewportCore
|
||||
// (fly_move_speed_ / flyAdjustSpeed), shared with the web fly path.
|
||||
// Per-frame [fly] dt log when WGPU_FLY_DEBUG=1. Diagnoses stutter:
|
||||
// print dt of each fpsIntegrate call and the prior render's elapsed
|
||||
// ms. Off by default (env-gated) so the normal log stays clean.
|
||||
|
||||
Reference in New Issue
Block a user