Skip to content

Commit 9399519

Browse files
Goober5000claude
andcommitted
add orbit camera support to FRED2 and qtFRED
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 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 05054f8 commit 9399519

8 files changed

Lines changed: 511 additions & 15 deletions

File tree

fred2/fredrender.cpp

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ int Last_cursor_over = -1;
9292
int last_x = 0;
9393
int last_y = 0;
9494
int Lookat_mode = 0;
95+
96+
// Orbit camera state
97+
vec3d Orbit_pivot = ZERO_VECTOR;
98+
float Orbit_distance = 200.0f;
99+
float Orbit_phi = 1.24f;
100+
float Orbit_theta = 2.25f;
101+
bool Orbit_active = false;
95102
int rendering_order[MAX_SHIPS];
96103
int render_count = 0;
97104
int Show_asteroid_field = 1;
@@ -1311,6 +1318,124 @@ void move_mouse(int btn, int mdx, int mdy) {
13111318
}
13121319
}
13131320

1321+
// ---------- Orbit camera functions ----------
1322+
1323+
vec3d orbit_camera_get_pivot()
1324+
{
1325+
vec3d pivot;
1326+
1327+
if (query_valid_object()) {
1328+
pivot = Objects[cur_object_index].pos;
1329+
} else if (!The_grid) {
1330+
pivot = ZERO_VECTOR;
1331+
} else {
1332+
// Intersect camera forward ray with the grid plane
1333+
vec3d *grid_normal = &The_grid->gmatrix.vec.uvec;
1334+
float denom = vm_vec_dot(grid_normal, &view_orient.vec.fvec);
1335+
1336+
if (fl_abs(denom) > 0.0001f) {
1337+
// t = -(dot(normal, view_pos) + planeD) / dot(normal, fvec)
1338+
float t = -(vm_vec_dot(grid_normal, &view_pos) + The_grid->planeD) / denom;
1339+
if (t > 0.0f) {
1340+
vm_vec_scale_add(&pivot, &view_pos, &view_orient.vec.fvec, t);
1341+
} else {
1342+
pivot = The_grid->center;
1343+
}
1344+
} else {
1345+
// Camera is parallel to grid plane; fall back to grid center
1346+
pivot = The_grid->center;
1347+
}
1348+
}
1349+
return pivot;
1350+
}
1351+
1352+
void orbit_camera_init_from_current_view(const vec3d *pivot)
1353+
{
1354+
vec3d offset;
1355+
vm_vec_sub(&offset, &view_pos, pivot);
1356+
1357+
Orbit_distance = vm_vec_mag(&offset);
1358+
if (Orbit_distance < 1.0f)
1359+
Orbit_distance = 100.0f;
1360+
1361+
// Transform offset into grid-local frame for angle decomposition
1362+
// In local frame, Y is always "up" (the grid plane normal)
1363+
const matrix &grid_mat = The_grid ? The_grid->gmatrix : vmd_identity_matrix;
1364+
vec3d local_offset;
1365+
vm_vec_rotate(&local_offset, &offset, &grid_mat);
1366+
1367+
Orbit_phi = acosf(std::clamp(local_offset.xyz.y / Orbit_distance, -1.0f, 1.0f));
1368+
Orbit_theta = atan2f(local_offset.xyz.z, local_offset.xyz.x);
1369+
Orbit_pivot = *pivot;
1370+
Orbit_active = true;
1371+
}
1372+
1373+
void orbit_camera_apply()
1374+
{
1375+
float sp = sinf(Orbit_phi);
1376+
1377+
// Build offset in grid-local coordinates (Y = up/normal)
1378+
vec3d local_pos;
1379+
local_pos.xyz.x = sp * cosf(Orbit_theta);
1380+
local_pos.xyz.y = cosf(Orbit_phi);
1381+
local_pos.xyz.z = sp * sinf(Orbit_theta);
1382+
1383+
// Transform back to world space
1384+
const matrix &grid_mat = The_grid ? The_grid->gmatrix : vmd_identity_matrix;
1385+
vec3d world_offset;
1386+
vm_vec_unrotate(&world_offset, &local_pos, &grid_mat);
1387+
1388+
vm_vec_scale(&world_offset, Orbit_distance);
1389+
vm_vec_add(&view_pos, &Orbit_pivot, &world_offset);
1390+
1391+
// Point camera at pivot, using grid's up vector
1392+
vec3d look_dir;
1393+
vm_vec_sub(&look_dir, &Orbit_pivot, &view_pos);
1394+
if (vm_vec_mag(&look_dir) > 0.001f) {
1395+
vec3d grid_up = grid_mat.vec.uvec;
1396+
vm_vector_2_matrix(&view_orient, &look_dir, &grid_up, nullptr);
1397+
}
1398+
1399+
Update_window = 1;
1400+
}
1401+
1402+
void orbit_camera_rotate(int dx, int dy)
1403+
{
1404+
float rot_scale = physics_rot / 300.0f;
1405+
Orbit_theta += dx / 100.0f * rot_scale;
1406+
Orbit_phi += dy / 100.0f * rot_scale;
1407+
1408+
CLAMP(Orbit_phi, 0.01f, PI - 0.01f);
1409+
1410+
orbit_camera_apply();
1411+
}
1412+
1413+
void orbit_camera_pan(int dx, int dy)
1414+
{
1415+
float speed_factor = 1.0f + (physics_speed - 1) / 499.0f * 9.0f;
1416+
float pan_scale = Orbit_distance * 0.002f * speed_factor;
1417+
1418+
vec3d pan_delta;
1419+
vm_vec_copy_scale(&pan_delta, &view_orient.vec.rvec, -(float)dx * pan_scale);
1420+
vm_vec_scale_add2(&pan_delta, &view_orient.vec.uvec, (float)dy * pan_scale);
1421+
1422+
vm_vec_add2(&Orbit_pivot, &pan_delta);
1423+
1424+
orbit_camera_apply();
1425+
}
1426+
1427+
void orbit_camera_zoom(float delta)
1428+
{
1429+
float zoom_speed = 1.0f + (physics_speed - 1) / 499.0f * 4.0f;
1430+
Orbit_distance *= powf(2.0f, delta * zoom_speed);
1431+
1432+
CLAMP(Orbit_distance, 1.0f, 10000000.0f);
1433+
1434+
orbit_camera_apply();
1435+
}
1436+
1437+
// ---------- End orbit camera functions ----------
1438+
13141439
int object_check_collision(object *objp, vec3d *p0, vec3d *p1, vec3d *hitpos) {
13151440
mc_info mc;
13161441

@@ -1425,6 +1550,9 @@ void process_controls(vec3d *pos, matrix *orient, float frametime, int key, int
14251550
if (rotangs.h && Universal_heading)
14261551
vm_transpose(orient);
14271552
}
1553+
1554+
// Invalidate orbit camera state so it re-initializes on next mouse drag
1555+
Orbit_active = false;
14281556
}
14291557

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

fred2/fredrender.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ extern int Flying_controls_mode; //!< Bool. Unknown.
3535
extern int Group_rotate; //!< Bool. If nonzero, each object rotates around the leader. If zero. rotate
3636
extern int Show_horizon; //!< Bool. If nonzero, draw a line representing the horizon (XZ plane)
3737
extern int Lookat_mode; //!< Bool. Unknown.
38+
39+
// Orbit camera state
40+
extern vec3d Orbit_pivot;
41+
extern float Orbit_distance;
42+
extern float Orbit_phi;
43+
extern float Orbit_theta;
44+
extern bool Orbit_active;
3845
extern int True_rw; //!< Unsigned. The width of the render area
3946
extern int True_rh; //!< Unsigned. The height of the render area
4047
extern int Fixed_briefing_size; //!< Bool. If nonzero then expand the briefing preview as much as we can, maintaining the aspect ratio.
@@ -93,3 +100,11 @@ void verticalize_controlled();
93100
* @return -1 if no object found
94101
*/
95102
int select_object(int cx, int cy);
103+
104+
// Orbit camera functions
105+
vec3d orbit_camera_get_pivot();
106+
void orbit_camera_init_from_current_view(const vec3d *pivot);
107+
void orbit_camera_apply();
108+
void orbit_camera_rotate(int dx, int dy);
109+
void orbit_camera_pan(int dx, int dy);
110+
void orbit_camera_zoom(float delta);

fred2/fredview.cpp

Lines changed: 111 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)
@@ -1038,7 +1043,7 @@ void CFREDView::OnLButtonDown(UINT nFlags, CPoint point)
10381043
CView::OnLButtonDown(nFlags, point);
10391044
}
10401045

1041-
void CFREDView::OnMouseMove(UINT nFlags, CPoint point)
1046+
void CFREDView::OnMouseMove(UINT nFlags, CPoint point)
10421047
{
10431048
// RT point
10441049

@@ -1048,6 +1053,26 @@ void CFREDView::OnMouseMove(UINT nFlags, CPoint point)
10481053
last_mouse_y = marking_box.y2 = point.y;
10491054
Cursor_over = select_object(point.x, point.y);
10501055

1056+
// Orbit camera: middle button drag
1057+
if (m_orbit_dragging && (nFlags & MK_MBUTTON)) {
1058+
handle_orbit_drag(point, nFlags);
1059+
CView::OnMouseMove(nFlags, point);
1060+
return;
1061+
}
1062+
1063+
// Orbit camera: right button drag
1064+
if (m_rbutton_down && (nFlags & MK_RBUTTON) && viewpoint == 0 && Control_mode == 0) {
1065+
if (!m_rbutton_moved) {
1066+
if (abs(point.x - m_rbutton_down_point.x) > 2 || abs(point.y - m_rbutton_down_point.y) > 2)
1067+
m_rbutton_moved = true;
1068+
}
1069+
if (m_rbutton_moved) {
1070+
handle_orbit_drag(point, nFlags);
1071+
CView::OnMouseMove(nFlags, point);
1072+
return;
1073+
}
1074+
}
1075+
10511076
if (!(nFlags & MK_LBUTTON))
10521077
button_down = 0;
10531078

@@ -1056,7 +1081,7 @@ void CFREDView::OnMouseMove(UINT nFlags, CPoint point)
10561081
if (button_down && GetCapture() != this)
10571082
cancel_drag();
10581083

1059-
if (!button_down && GetCapture() == this)
1084+
if (!button_down && !m_orbit_dragging && !m_rbutton_down && GetCapture() == this)
10601085
ReleaseCapture();
10611086

10621087
if (button_down) {
@@ -1089,7 +1114,7 @@ void CFREDView::OnLButtonUp(UINT nFlags, CPoint point)
10891114
if (button_down && GetCapture() != this)
10901115
cancel_drag();
10911116

1092-
if (GetCapture() == this)
1117+
if (!m_orbit_dragging && !m_rbutton_down && GetCapture() == this)
10931118
ReleaseCapture();
10941119

10951120
if (button_down) {
@@ -1174,6 +1199,89 @@ void CFREDView::OnLButtonUp(UINT nFlags, CPoint point)
11741199
CView::OnLButtonUp(nFlags, point);
11751200
}
11761201

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

fred2/fredview.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ class CFREDView : public CView
3636
CGrid* m_pGDlg;
3737
int global_error_check_player_wings(int multi);
3838

39+
// Orbit camera drag state
40+
bool m_orbit_dragging = false;
41+
bool m_rbutton_down = false;
42+
bool m_rbutton_moved = false;
43+
CPoint m_orbit_last_mouse;
44+
CPoint m_rbutton_down_point;
45+
void handle_orbit_drag(CPoint point, UINT nFlags);
46+
3947
protected: // create from serialization only
4048
CFREDView();
4149
DECLARE_DYNCREATE(CFREDView)
@@ -331,6 +339,11 @@ class CFREDView : public CView
331339
afx_msg void OnUpdateViewLighting(CCmdUI* pCmdUI);
332340
afx_msg void OnViewFullDetail();
333341
afx_msg void OnUpdateViewFullDetail(CCmdUI *pCmdUI);
342+
afx_msg void OnMButtonDown(UINT nFlags, CPoint point);
343+
afx_msg void OnMButtonUp(UINT nFlags, CPoint point);
344+
afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
345+
afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
346+
afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
334347
//}}AFX_MSG
335348
afx_msg void OnGroup(UINT nID);
336349
afx_msg void OnSetGroup(UINT nID);

0 commit comments

Comments
 (0)