Skip to content

Commit 1240d2e

Browse files
Goober5000claude
andauthored
add orbit camera support to FRED2 and qtFRED (scp-fs2open#7310)
Implement a spherical-coordinate orbit camera for both the MFC (FRED2) and Qt (qtFRED) editors, providing intuitive 3D viewport navigation similar to other 3D editing tools. Controls: - Middle mouse drag: orbit rotate around pivot point - Shift + middle mouse drag: pan the pivot point - Right mouse drag: orbit rotate (alternative to middle mouse) - Shift + right mouse drag: pan (alternative to shift + middle mouse) - Mouse wheel: zoom in/out - Right click without drag: context menu (preserving existing feature) Camera pivot selection: - If an object is selected, orbit around that object - Otherwise, intersect the camera forward ray with the current grid plane to find a natural pivot point - Falls back to grid center if the camera is parallel to the grid Grid-plane awareness: - Orbit axes are derived from The_grid->gmatrix using vm_vec_rotate and vm_vec_unrotate, so the camera orbits correctly on all three grid planes (XZ, XY, YZ), not just the default XZ plane - The look-at matrix uses the grid's up vector (uvec) rather than a hardcoded world-up direction Robust zoom/pan scaling: - Zoom uses an exponential formula (powf) so the distance multiplier is always positive regardless of physics_speed setting (1-500) - Pan uses a clamped speed factor to avoid excessive movement at high physics_speed values State management: - Keyboard camera movement (process_controls) invalidates orbit state, so the next mouse drag re-initializes from the current view - Orbit state is per-viewport in qtFRED (EditorViewport members) and global in FRED2 (matching existing conventions in each codebase) Implementation: - FRED2: orbit math in fredrender.cpp, mouse handlers in fredview.cpp - qtFRED: orbit math in EditorViewport.cpp, mouse/wheel handlers in renderwidget.cpp; RenderWindow::event() updated to forward middle button and wheel events to RenderWidget Also fix an AND vs OR bug in FRED's process_controls(). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 343bc17 commit 1240d2e

10 files changed

Lines changed: 555 additions & 29 deletions

File tree

fred2/fredrender.cpp

Lines changed: 147 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
#include "starfield/starfield.h"
5454
#include "weapon/weapon.h"
5555

56+
#include <algorithm>
57+
#include <cmath>
58+
5659
#include <stdio.h>
5760
#include <stdlib.h>
5861
#include <string.h>
@@ -92,6 +95,14 @@ int Last_cursor_over = -1;
9295
int last_x = 0;
9396
int last_y = 0;
9497
int Lookat_mode = 0;
98+
99+
// Orbit camera state
100+
vec3d Orbit_pivot = vmd_zero_vector;
101+
matrix Orbit_grid_orient = vmd_identity_matrix;
102+
float Orbit_distance = 200.0f;
103+
float Orbit_phi = 1.24f;
104+
float Orbit_theta = 2.25f;
105+
bool Orbit_active = false;
95106
int rendering_order[MAX_SHIPS];
96107
int render_count = 0;
97108
int Show_asteroid_field = 1;
@@ -1275,6 +1286,124 @@ void move_mouse(int btn, int mdx, int mdy) {
12751286
}
12761287
}
12771288

1289+
// ---------- Orbit camera functions ----------
1290+
1291+
vec3d orbit_camera_get_pivot()
1292+
{
1293+
vec3d pivot;
1294+
1295+
if (query_valid_object()) {
1296+
// Pivot on current object
1297+
pivot = Objects[cur_object_index].pos;
1298+
} else if (!The_grid) {
1299+
// Pivot on the origin, if no grid
1300+
pivot = ZERO_VECTOR;
1301+
} else {
1302+
// Intersect camera forward ray with the grid plane
1303+
vec3d *grid_normal = &The_grid->gmatrix.vec.uvec;
1304+
float denom = vm_vec_dot(grid_normal, &view_orient.vec.fvec);
1305+
1306+
if (fl_abs(denom) > 0.0001f) {
1307+
// t = -(dot(normal, view_pos) + planeD) / dot(normal, fvec)
1308+
float t = -(vm_vec_dot(grid_normal, &view_pos) + The_grid->planeD) / denom;
1309+
if (t > 0.0f) {
1310+
vm_vec_scale_add(&pivot, &view_pos, &view_orient.vec.fvec, t);
1311+
} else {
1312+
pivot = The_grid->center;
1313+
}
1314+
} else {
1315+
// Camera is parallel to grid plane; fall back to grid center
1316+
pivot = The_grid->center;
1317+
}
1318+
}
1319+
return pivot;
1320+
}
1321+
1322+
void orbit_camera_init_from_current_view(const vec3d *pivot, const matrix *grid_orient)
1323+
{
1324+
Orbit_pivot = pivot ? *pivot : vmd_zero_vector;
1325+
Orbit_grid_orient = grid_orient ? *grid_orient : vmd_identity_matrix;
1326+
1327+
vec3d offset;
1328+
vm_vec_sub(&offset, &view_pos, &Orbit_pivot);
1329+
1330+
Orbit_distance = vm_vec_mag(&offset);
1331+
if (Orbit_distance < 1.0f)
1332+
Orbit_distance = 100.0f;
1333+
1334+
// Transform offset into grid-local frame for angle decomposition
1335+
// In local frame, Y is always "up" (the grid plane normal)
1336+
vec3d local_offset;
1337+
vm_vec_rotate(&local_offset, &offset, &Orbit_grid_orient);
1338+
1339+
Orbit_phi = acosf(std::clamp(local_offset.xyz.y / Orbit_distance, -1.0f, 1.0f));
1340+
Orbit_theta = atan2f(local_offset.xyz.z, local_offset.xyz.x);
1341+
Orbit_active = true;
1342+
}
1343+
1344+
void orbit_camera_apply()
1345+
{
1346+
float sp = sinf(Orbit_phi);
1347+
1348+
// Build offset in grid-local coordinates (Y = up/normal)
1349+
vec3d local_pos;
1350+
local_pos.xyz.x = sp * cosf(Orbit_theta);
1351+
local_pos.xyz.y = cosf(Orbit_phi);
1352+
local_pos.xyz.z = sp * sinf(Orbit_theta);
1353+
1354+
// Transform back to world space
1355+
vec3d world_offset;
1356+
vm_vec_unrotate(&world_offset, &local_pos, &Orbit_grid_orient);
1357+
1358+
vm_vec_scale(&world_offset, Orbit_distance);
1359+
vm_vec_add(&view_pos, &Orbit_pivot, &world_offset);
1360+
1361+
// Point camera at pivot, using grid's up vector
1362+
vec3d look_dir;
1363+
vm_vec_sub(&look_dir, &Orbit_pivot, &view_pos);
1364+
if (vm_vec_mag(&look_dir) > 0.001f) {
1365+
vec3d grid_up = Orbit_grid_orient.vec.uvec;
1366+
vm_vector_2_matrix(&view_orient, &look_dir, &grid_up, nullptr);
1367+
}
1368+
}
1369+
1370+
void orbit_camera_rotate(int dx, int dy)
1371+
{
1372+
float rot_scale = physics_rot / 300.0f;
1373+
Orbit_theta += dx / 100.0f * rot_scale;
1374+
Orbit_phi += dy / 100.0f * rot_scale;
1375+
1376+
CLAMP(Orbit_phi, 0.01f, PI - 0.01f);
1377+
1378+
orbit_camera_apply();
1379+
}
1380+
1381+
void orbit_camera_pan(int dx, int dy)
1382+
{
1383+
float speed_factor = 1.0f + (physics_speed - 1) / 499.0f * 9.0f;
1384+
float pan_scale = Orbit_distance * 0.002f * speed_factor;
1385+
1386+
vec3d pan_delta;
1387+
vm_vec_copy_scale(&pan_delta, &view_orient.vec.rvec, -(float)dx * pan_scale);
1388+
vm_vec_scale_add2(&pan_delta, &view_orient.vec.uvec, (float)dy * pan_scale);
1389+
1390+
vm_vec_add2(&Orbit_pivot, &pan_delta);
1391+
1392+
orbit_camera_apply();
1393+
}
1394+
1395+
void orbit_camera_zoom(float delta)
1396+
{
1397+
float zoom_speed = 1.0f + (physics_speed - 1) / 499.0f * 4.0f;
1398+
Orbit_distance *= powf(2.0f, delta * zoom_speed);
1399+
1400+
CLAMP(Orbit_distance, 1.0f, 10000000.0f);
1401+
1402+
orbit_camera_apply();
1403+
}
1404+
1405+
// ---------- End orbit camera functions ----------
1406+
12781407
int object_check_collision(object *objp, vec3d *p0, vec3d *p1, vec3d *hitpos) {
12791408
mc_info mc;
12801409

@@ -1333,6 +1462,7 @@ int object_check_collision(object *objp, vec3d *p0, vec3d *p1, vec3d *hitpos) {
13331462

13341463
void process_controls(vec3d *pos, matrix *orient, float frametime, int key, int mode) {
13351464
static std::unique_ptr<io::spacemouse::SpaceMouse> spacemouse = io::spacemouse::SpaceMouse::searchSpaceMice(0);
1465+
bool wantsUpdate = false;
13361466

13371467
if (Flying_controls_mode) {
13381468
grid_read_camera_controls(&view_controls, frametime);
@@ -1350,12 +1480,14 @@ void process_controls(vec3d *pos, matrix *orient, float frametime, int key, int
13501480
if (key_get_shift_status())
13511481
memset(&view_controls, 0, sizeof(control_info));
13521482

1353-
if ((fabs(view_controls.pitch) > (frametime / 100)) &&
1354-
(fabs(view_controls.vertical) > (frametime / 100)) &&
1355-
(fabs(view_controls.heading) > (frametime / 100)) &&
1356-
(fabs(view_controls.sideways) > (frametime / 100)) &&
1357-
(fabs(view_controls.bank) > (frametime / 100)) &&
1358-
(fabs(view_controls.forward) > (frametime / 100)))
1483+
wantsUpdate = (fabs(view_controls.pitch) > (frametime / 100))
1484+
|| (fabs(view_controls.vertical) > (frametime / 100))
1485+
|| (fabs(view_controls.heading) > (frametime / 100))
1486+
|| (fabs(view_controls.sideways) > (frametime / 100))
1487+
|| (fabs(view_controls.bank) > (frametime / 100))
1488+
|| (fabs(view_controls.forward) > (frametime / 100));
1489+
1490+
if (wantsUpdate)
13591491
Update_window = 1;
13601492

13611493
flFrametime = frametime;
@@ -1388,7 +1520,16 @@ void process_controls(vec3d *pos, matrix *orient, float frametime, int key, int
13881520
*orient = newmat;
13891521
if (rotangs.h && Universal_heading)
13901522
vm_transpose(orient);
1523+
1524+
wantsUpdate = !IS_VEC_NULL(&movement_vec)
1525+
|| rotangs.p != 0.0f
1526+
|| rotangs.h != 0.0f
1527+
|| rotangs.b != 0.0f;
13911528
}
1529+
1530+
// Invalidate orbit camera state when the user moves the camera another way
1531+
if (wantsUpdate)
1532+
Orbit_active = false;
13921533
}
13931534

13941535
void process_movement_keys(int key, vec3d *mvec, angles *angs) {

fred2/fredrender.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ extern int Flying_controls_mode; //!< Bool. Unknown.
3636
extern int Group_rotate; //!< Bool. If nonzero, each object rotates around the leader. If zero. rotate
3737
extern int Show_horizon; //!< Bool. If nonzero, draw a line representing the horizon (XZ plane)
3838
extern int Lookat_mode; //!< Bool. Unknown.
39+
40+
// Orbit camera state
41+
extern vec3d Orbit_pivot;
42+
extern matrix Orbit_grid_orient;
43+
extern float Orbit_distance;
44+
extern float Orbit_phi;
45+
extern float Orbit_theta;
46+
extern bool Orbit_active;
3947
extern int True_rw; //!< Unsigned. The width of the render area
4048
extern int True_rh; //!< Unsigned. The height of the render area
4149
extern int Fixed_briefing_size; //!< Bool. If nonzero then expand the briefing preview as much as we can, maintaining the aspect ratio.
@@ -94,3 +102,11 @@ void verticalize_controlled();
94102
* @return -1 if no object found
95103
*/
96104
int select_object(int cx, int cy);
105+
106+
// Orbit camera functions
107+
vec3d orbit_camera_get_pivot();
108+
void orbit_camera_init_from_current_view(const vec3d *pivot, const matrix *grid_orient);
109+
void orbit_camera_apply();
110+
void orbit_camera_rotate(int dx, int dy);
111+
void orbit_camera_pan(int dx, int dy);
112+
void orbit_camera_zoom(float delta);

fred2/fredview.cpp

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ BEGIN_MESSAGE_MAP(CFREDView, CView)
158158
ON_WM_SIZE()
159159
ON_WM_MOUSEMOVE()
160160
ON_WM_LBUTTONUP()
161+
ON_WM_MBUTTONDOWN()
162+
ON_WM_MBUTTONUP()
163+
ON_WM_RBUTTONDOWN()
164+
ON_WM_RBUTTONUP()
165+
ON_WM_MOUSEWHEEL()
161166
ON_COMMAND(ID_MISCSTUFF_SHOWSHIPSASICONS, OnMiscstuffShowshipsasicons)
162167
ON_WM_CONTEXTMENU()
163168
ON_COMMAND(ID_EDIT_POPUP_SHOW_SHIP_ICONS, OnEditPopupShowShipIcons)
@@ -1048,7 +1053,7 @@ void CFREDView::OnLButtonDown(UINT nFlags, CPoint point)
10481053
CView::OnLButtonDown(nFlags, point);
10491054
}
10501055

1051-
void CFREDView::OnMouseMove(UINT nFlags, CPoint point)
1056+
void CFREDView::OnMouseMove(UINT nFlags, CPoint point)
10521057
{
10531058
// RT point
10541059

@@ -1058,6 +1063,26 @@ void CFREDView::OnMouseMove(UINT nFlags, CPoint point)
10581063
last_mouse_y = marking_box.y2 = point.y;
10591064
Cursor_over = select_object(point.x, point.y);
10601065

1066+
// Orbit camera: middle button drag
1067+
if (m_orbit_dragging && (nFlags & MK_MBUTTON)) {
1068+
handle_orbit_drag(point, nFlags);
1069+
CView::OnMouseMove(nFlags, point);
1070+
return;
1071+
}
1072+
1073+
// Orbit camera: right button drag
1074+
if (m_rbutton_down && (nFlags & MK_RBUTTON) && viewpoint == 0 && Control_mode == 0) {
1075+
if (!m_rbutton_moved) {
1076+
if (abs(point.x - m_rbutton_down_point.x) > 2 || abs(point.y - m_rbutton_down_point.y) > 2)
1077+
m_rbutton_moved = true;
1078+
}
1079+
if (m_rbutton_moved) {
1080+
handle_orbit_drag(point, nFlags);
1081+
CView::OnMouseMove(nFlags, point);
1082+
return;
1083+
}
1084+
}
1085+
10611086
if (!(nFlags & MK_LBUTTON))
10621087
button_down = 0;
10631088

@@ -1066,7 +1091,7 @@ void CFREDView::OnMouseMove(UINT nFlags, CPoint point)
10661091
if (button_down && GetCapture() != this)
10671092
cancel_drag();
10681093

1069-
if (!button_down && GetCapture() == this)
1094+
if (!button_down && !m_orbit_dragging && !m_rbutton_down && GetCapture() == this)
10701095
ReleaseCapture();
10711096

10721097
if (button_down) {
@@ -1099,7 +1124,7 @@ void CFREDView::OnLButtonUp(UINT nFlags, CPoint point)
10991124
if (button_down && GetCapture() != this)
11001125
cancel_drag();
11011126

1102-
if (GetCapture() == this)
1127+
if (!m_orbit_dragging && !m_rbutton_down && GetCapture() == this)
11031128
ReleaseCapture();
11041129

11051130
if (button_down) {
@@ -1182,6 +1207,94 @@ void CFREDView::OnLButtonUp(UINT nFlags, CPoint point)
11821207
CView::OnLButtonUp(nFlags, point);
11831208
}
11841209

1210+
// ---------- Orbit camera mouse handlers ----------
1211+
1212+
void CFREDView::handle_orbit_drag(CPoint point, UINT nFlags)
1213+
{
1214+
int dx = point.x - m_orbit_last_mouse.x;
1215+
int dy = point.y - m_orbit_last_mouse.y;
1216+
m_orbit_last_mouse = point;
1217+
1218+
if (nFlags & MK_SHIFT)
1219+
orbit_camera_pan(dx, dy);
1220+
else
1221+
orbit_camera_rotate(dx, dy);
1222+
Update_window = 1;
1223+
}
1224+
1225+
void CFREDView::OnMButtonDown(UINT nFlags, CPoint point)
1226+
{
1227+
if (viewpoint != 0 || Control_mode != 0)
1228+
return;
1229+
1230+
vec3d pivot = orbit_camera_get_pivot();
1231+
auto grid_orient = The_grid ? &The_grid->gmatrix : nullptr;
1232+
orbit_camera_init_from_current_view(&pivot, grid_orient);
1233+
1234+
m_orbit_dragging = true;
1235+
m_orbit_last_mouse = point;
1236+
SetCapture();
1237+
}
1238+
1239+
void CFREDView::OnMButtonUp(UINT nFlags, CPoint point)
1240+
{
1241+
if (m_orbit_dragging) {
1242+
m_orbit_dragging = false;
1243+
if (GetCapture() == this && !m_rbutton_down)
1244+
ReleaseCapture();
1245+
}
1246+
}
1247+
1248+
void CFREDView::OnRButtonDown(UINT nFlags, CPoint point)
1249+
{
1250+
m_rbutton_down = true;
1251+
m_rbutton_moved = false;
1252+
m_rbutton_down_point = point;
1253+
m_orbit_last_mouse = point;
1254+
1255+
if (viewpoint == 0 && Control_mode == 0) {
1256+
vec3d pivot = orbit_camera_get_pivot();
1257+
auto grid_orient = The_grid ? &The_grid->gmatrix : nullptr;
1258+
orbit_camera_init_from_current_view(&pivot, grid_orient);
1259+
SetCapture();
1260+
}
1261+
}
1262+
1263+
void CFREDView::OnRButtonUp(UINT nFlags, CPoint point)
1264+
{
1265+
bool was_dragging = m_rbutton_moved;
1266+
m_rbutton_down = false;
1267+
m_rbutton_moved = false;
1268+
1269+
if (GetCapture() == this && !m_orbit_dragging)
1270+
ReleaseCapture();
1271+
1272+
if (!was_dragging) {
1273+
// No drag occurred — show context menu as normal
1274+
CPoint screen_point = point;
1275+
ClientToScreen(&screen_point);
1276+
OnContextMenu(this, screen_point);
1277+
}
1278+
}
1279+
1280+
BOOL CFREDView::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
1281+
{
1282+
if (viewpoint != 0 || Control_mode != 0)
1283+
return CView::OnMouseWheel(nFlags, zDelta, pt);
1284+
1285+
if (!Orbit_active) {
1286+
vec3d pivot = orbit_camera_get_pivot();
1287+
auto grid_orient = The_grid ? &The_grid->gmatrix : nullptr;
1288+
orbit_camera_init_from_current_view(&pivot, grid_orient);
1289+
}
1290+
1291+
orbit_camera_zoom(zDelta / -200.0f);
1292+
Update_window = 1;
1293+
return TRUE;
1294+
}
1295+
1296+
// ---------- End orbit camera mouse handlers ----------
1297+
11851298
// This function never gets called because nothing causes
11861299
// the WM_GOODBYE event to occur.
11871300
// False! When you close the Ship Dialog, this function is called! --MK, 8/30/96

0 commit comments

Comments
 (0)