diff --git a/code/graphics/render.cpp b/code/graphics/render.cpp
index b05d44dcbd8..a64e70cf23c 100644
--- a/code/graphics/render.cpp
+++ b/code/graphics/render.cpp
@@ -934,6 +934,20 @@ void gr_string(float sx, float sy, const char* s, int resize_mode, float scaleMu
}
}
+void gr_string_outlined(int x, int y, const char* text, const color* foreground, const color* outline, int offset, int resize_mode, float scaleMultiplier, size_t length)
+{
+ // draw outline by rendering text at all surrounding offsets
+ gr_set_color_fast(outline);
+ for (int dx = -offset; dx <= offset; dx++)
+ for (int dy = -offset; dy <= offset; dy++)
+ if (dx || dy)
+ gr_string(x + dx, y + dy, text, resize_mode, scaleMultiplier, length);
+
+ // draw foreground text on top
+ gr_set_color_fast(foreground);
+ gr_string(x, y, text, resize_mode, scaleMultiplier, length);
+}
+
static void gr_line(float x1, float y1, float x2, float y2, int resize_mode) {
auto path = beginDrawing(resize_mode);
diff --git a/code/graphics/render.h b/code/graphics/render.h
index b31058d2ef9..88da9014d89 100644
--- a/code/graphics/render.h
+++ b/code/graphics/render.h
@@ -79,6 +79,25 @@ inline void gr_string(int x, int y, const char* string, int resize_mode = GR_RES
gr_string(i2fl(x), i2fl(y), string, resize_mode, scaleMultiplier, length);
}
+/**
+ * @brief Draws outlined text at the given position
+ *
+ * @details Renders the text string with an outline by drawing the text
+ * at surrounding offsets in the outline color, then drawing the main text on top
+ * in the foreground color.
+ *
+ * @param x The x-coordinate
+ * @param y The y-coordinate
+ * @param text The text to draw
+ * @param foreground Color for the main text
+ * @param outline Color for the outline
+ * @param offset Pixel offset for the outline (default 1)
+ * @param resize_mode The mode for translating the screen positions
+ * @param scaleMultiplier The scale to use to apply scaling in addition to user settings
+ * @param length The number of bytes in the string to render. -1 will render the whole string.
+ */
+void gr_string_outlined(int x, int y, const char* text, const color* foreground, const color* outline, int offset = 1, int resize_mode = GR_RESIZE_FULL, float scaleMultiplier = 1.0f, size_t length = std::string::npos);
+
/**
* @brief Draws a single line segment to the screen.
* @param x1 The starting x-coordinate
diff --git a/fred2/fred.cpp b/fred2/fred.cpp
index 9b2d906928b..2fc042245d3 100644
--- a/fred2/fred.cpp
+++ b/fred2/fred.cpp
@@ -246,6 +246,7 @@ BOOL CFREDApp::InitInstance() {
Draw_outlines_on_selected_ships = GetProfileInt("Preferences", "Draw outlines on selected ships", 1) != 0;
Point_using_uvec = GetProfileInt("Preferences", "Point using uvec", Point_using_uvec);
Draw_outline_at_warpin_position = GetProfileInt("Preferences", "Draw outline at warpin position", 0) != 0;
+ Outline_lod = GetProfileInt("Preferences", "Outline LOD", 1);
Always_save_display_names = GetProfileInt("Preferences", "Always save display names", 0) != 0;
Error_checker_checks_potential_issues = GetProfileInt("Preferences", "Error checker checks potential issues", 1) != 0;
@@ -546,6 +547,7 @@ void CFREDApp::write_ini_file(int degree) {
WriteProfileInt("Preferences", "Draw outlines on selected ships", Draw_outlines_on_selected_ships ? 1 : 0);
WriteProfileInt("Preferences", "Point using uvec", Point_using_uvec);
WriteProfileInt("Preferences", "Draw outline at warpin position", Draw_outline_at_warpin_position ? 1 : 0);
+ WriteProfileInt("Preferences", "Outline LOD", Outline_lod);
WriteProfileInt("Preferences", "Always save display names", Always_save_display_names ? 1 : 0);
WriteProfileInt("Preferences", "Error checker checks potential issues", Error_checker_checks_potential_issues ? 1 : 0);
diff --git a/fred2/fred.rc b/fred2/fred.rc
index bbfefe7442b..9ba9c004181 100644
--- a/fred2/fred.rc
+++ b/fred2/fred.rc
@@ -264,6 +264,14 @@ BEGIN
MENUITEM "Show IFF 8", 33089
MENUITEM "Show IFF 9", 32988
END
+ POPUP "Outline LOD"
+ BEGIN
+ MENUITEM "LOD 0 (highest detail)", 33107
+ MENUITEM "LOD 1", 33108, CHECKED
+ MENUITEM "LOD 2", 33109
+ MENUITEM "LOD 3", 33110
+ MENUITEM "LOD 4 (lowest detail)", 33111
+ END
MENUITEM SEPARATOR
MENUITEM "Hide Marked Objects", 33002
MENUITEM "Show Hidden Objects", 33003
diff --git a/fred2/fredrender.cpp b/fred2/fredrender.cpp
index a92ba794201..dba32c95ad1 100644
--- a/fred2/fredrender.cpp
+++ b/fred2/fredrender.cpp
@@ -103,6 +103,7 @@ int Show_horizon = 0;
int Show_outlines = 0;
bool Draw_outlines_on_selected_ships = true;
bool Draw_outline_at_warpin_position = false;
+int Outline_lod = 1;
bool Always_save_display_names = false;
bool Error_checker_checks_potential_issues = true;
bool Error_checker_checks_potential_issues_once = false;
@@ -211,12 +212,13 @@ int get_subsys_bounding_rect(object *ship_obj, ship_subsys *subsys, int *x1, int
*/
void render_models(void);
+enum class subsystem_highlight { BOUNDING_BOX, LABEL };
/**
* @brief Renders the bounding box for the given subsystem with HTL
*
* @param[in] s2r Subsystem to render a box for
*/
-void fredhtl_render_subsystem_bounding_box(subsys_to_render * s2r);
+void fredhtl_render_subsystem_highlight(subsys_to_render *s2r, subsystem_highlight highlight);
/**
* @brief Draws the X from a elevation line on the grid
@@ -414,10 +416,18 @@ void display_active_ship_subsystem() {
if (Highlight_selectable_subsys) {
auto shipp = &Ships[objp->instance];
- for (auto ss = GET_FIRST(&shipp->subsys_list); ss != END_OF_LIST(&shipp->subsys_list); ss = GET_NEXT(ss)) {
+ // first pass: draw all bounding boxes
+ for (auto ss : list_range(&shipp->subsys_list)) {
if (ss->system_info->subobj_num != -1) {
subsys_to_render s2r = { true, objp, ss };
- fredhtl_render_subsystem_bounding_box(&s2r);
+ fredhtl_render_subsystem_highlight(&s2r, subsystem_highlight::BOUNDING_BOX);
+ }
+ }
+ // second pass: draw all labels
+ for (auto ss : list_range(&shipp->subsys_list)) {
+ if (ss->system_info->subobj_num != -1) {
+ subsys_to_render s2r = { true, objp, ss };
+ fredhtl_render_subsystem_highlight(&s2r, subsystem_highlight::LABEL);
}
}
}
@@ -430,7 +440,8 @@ void display_active_ship_subsystem() {
}
if (Render_subsys.do_render) {
- fredhtl_render_subsystem_bounding_box(&Render_subsys);
+ fredhtl_render_subsystem_highlight(&Render_subsys, subsystem_highlight::BOUNDING_BOX);
+ fredhtl_render_subsystem_highlight(&Render_subsys, subsystem_highlight::LABEL);
} else {
cancel_display_active_ship_subsystem();
}
@@ -726,7 +737,7 @@ void draw_orient_sphere2(int col, object *obj, int r, int g, int b) {
}
}
-void fredhtl_render_subsystem_bounding_box(subsys_to_render *s2r)
+void fredhtl_render_subsystem_highlight(subsys_to_render *s2r, subsystem_highlight highlight)
{
vertex text_center;
SCP_string buf;
@@ -740,122 +751,75 @@ void fredhtl_render_subsystem_bounding_box(subsys_to_render *s2r)
auto bsp = &pm->submodel[subobj_num];
- vec3d front_top_left = bsp->bounding_box[7];
- vec3d front_top_right = bsp->bounding_box[6];
- vec3d front_bot_left = bsp->bounding_box[4];
- vec3d front_bot_right = bsp->bounding_box[5];
- vec3d back_top_left = bsp->bounding_box[3];
- vec3d back_top_right = bsp->bounding_box[2];
- vec3d back_bot_left = bsp->bounding_box[0];
- vec3d back_bot_right = bsp->bounding_box[1];
-
- gr_set_color(255, 32, 32);
-
- fred_enable_htl();
-
- // get into the frame of reference of the submodel
- int g3_count = 1;
- g3_start_instance_matrix(&objp->pos, &objp->orient, true);
- int mn = subobj_num;
- while ((mn >= 0) && (pm->submodel[mn].parent >= 0))
- {
- vec3d offset = pm->submodel[mn].offset;
- vm_vec_add2(&offset, &pmi->submodel[mn].canonical_offset);
-
- g3_start_instance_matrix(&offset, &pmi->submodel[mn].canonical_orient, true);
- g3_count++;
- mn = pm->submodel[mn].parent;
- }
-
-
- //draw a cube around the subsystem
- g3_draw_htl_line(&front_top_left, &front_top_right);
- g3_draw_htl_line(&front_top_right, &front_bot_right);
- g3_draw_htl_line(&front_bot_right, &front_bot_left);
- g3_draw_htl_line(&front_bot_left, &front_top_left);
-
- g3_draw_htl_line(&back_top_left, &back_top_right);
- g3_draw_htl_line(&back_top_right, &back_bot_right);
- g3_draw_htl_line(&back_bot_right, &back_bot_left);
- g3_draw_htl_line(&back_bot_left, &back_top_left);
-
- g3_draw_htl_line(&front_top_left, &back_top_left);
- g3_draw_htl_line(&front_top_right, &back_top_right);
- g3_draw_htl_line(&front_bot_left, &back_bot_left);
- g3_draw_htl_line(&front_bot_right, &back_bot_right);
-
+ // transform bounding box corners from submodel-local space to world space
+ // and draw edges as thick camera-facing quads via g3_render_rod
+ color clr_red;
+ gr_init_color(&clr_red, 255, 32, 32);
+ float rod_width = 2.0f;
+
+ auto transform_and_draw_box = [&](const vec3d *bbox, int sobj_num) {
+ vec3d corners[8];
+ for (int i = 0; i < 8; i++)
+ model_instance_local_to_global_point(&corners[i], &bbox[i], pm, pmi, sobj_num, &objp->orient, &objp->pos);
+
+ // 12 edges of a box: front face, back face, connecting edges
+ // bounding_box indices: 0=BBL 1=BBR 2=BTR 3=BTL 4=FBL 5=FBR 6=FTR 7=FTL
+ static const int edges[12][2] = {
+ {7, 6}, {6, 5}, {5, 4}, {4, 7}, // front face
+ {3, 2}, {2, 1}, {1, 0}, {0, 3}, // back face
+ {7, 3}, {6, 2}, {4, 0}, {5, 1}, // connecting edges
+ };
+
+ for (const auto& edge : edges) {
+ vec3d pts[2] = { corners[edge[0]], corners[edge[1]] };
+ g3_render_rod(&clr_red, 2, pts, rod_width);
+ }
+ };
- //draw another cube around a gun for a two-part turret
- if ((ss->system_info->turret_gun_sobj >= 0) && (ss->system_info->turret_gun_sobj != ss->system_info->subobj_num))
- {
- bsp_info *bsp_turret = &pm->submodel[ss->system_info->turret_gun_sobj];
-
- front_top_left = bsp_turret->bounding_box[7];
- front_top_right = bsp_turret->bounding_box[6];
- front_bot_left = bsp_turret->bounding_box[4];
- front_bot_right = bsp_turret->bounding_box[5];
- back_top_left = bsp_turret->bounding_box[3];
- back_top_right = bsp_turret->bounding_box[2];
- back_bot_left = bsp_turret->bounding_box[0];
- back_bot_right = bsp_turret->bounding_box[1];
-
- g3_start_instance_matrix(&bsp_turret->offset, &pmi->submodel[ss->system_info->turret_gun_sobj].canonical_orient, true);
-
- g3_draw_htl_line(&front_top_left, &front_top_right);
- g3_draw_htl_line(&front_top_right, &front_bot_right);
- g3_draw_htl_line(&front_bot_right, &front_bot_left);
- g3_draw_htl_line(&front_bot_left, &front_top_left);
-
- g3_draw_htl_line(&back_top_left, &back_top_right);
- g3_draw_htl_line(&back_top_right, &back_bot_right);
- g3_draw_htl_line(&back_bot_right, &back_bot_left);
- g3_draw_htl_line(&back_bot_left, &back_top_left);
-
- g3_draw_htl_line(&front_top_left, &back_top_left);
- g3_draw_htl_line(&front_top_right, &back_top_right);
- g3_draw_htl_line(&front_bot_left, &back_bot_left);
- g3_draw_htl_line(&front_bot_right, &back_bot_right);
-
- g3_done_instance(true);
- }
+ if (highlight == subsystem_highlight::BOUNDING_BOX) {
+ fred_enable_htl();
- for (int i = 0; i < g3_count; i++)
- g3_done_instance(true);
+ // draw a box around the subsystem
+ transform_and_draw_box(bsp->bounding_box, subobj_num);
- fred_disable_htl();
+ // draw another box around a gun for a two-part turret
+ if ((ss->system_info->turret_gun_sobj >= 0) && (ss->system_info->turret_gun_sobj != ss->system_info->subobj_num))
+ transform_and_draw_box(pm->submodel[ss->system_info->turret_gun_sobj].bounding_box, ss->system_info->turret_gun_sobj);
- // get text
- buf = ss->system_info->subobj_name;
+ fred_disable_htl();
+ } else {
+ // get text
+ buf = ss->system_info->subobj_name;
- // add weapons if present
- for (int i = 0; i < ss->weapons.num_primary_banks; ++i)
- {
- int wi = ss->weapons.primary_bank_weapons[i];
- if (wi >= 0)
+ // add weapons if present
+ for (int i = 0; i < ss->weapons.num_primary_banks; ++i)
{
- buf += "\n";
- buf += Weapon_info[wi].name;
+ int wi = ss->weapons.primary_bank_weapons[i];
+ if (wi >= 0)
+ {
+ buf += "\n";
+ buf += Weapon_info[wi].name;
+ }
}
- }
- for (int i = 0; i < ss->weapons.num_secondary_banks; ++i)
- {
- int wi = ss->weapons.secondary_bank_weapons[i];
- if (wi >= 0)
+ for (int i = 0; i < ss->weapons.num_secondary_banks; ++i)
{
- buf += "\n";
- buf += Weapon_info[wi].name;
+ int wi = ss->weapons.secondary_bank_weapons[i];
+ if (wi >= 0)
+ {
+ buf += "\n";
+ buf += Weapon_info[wi].name;
+ }
}
- }
- //draw the text. rotate the center of the subsystem into place before finding out where to put the text
- vec3d center_pt;
- vm_vec_unrotate(¢er_pt, &bsp->offset, &objp->orient);
- vm_vec_add2(¢er_pt, &objp->pos);
- g3_rotate_vertex(&text_center, ¢er_pt);
- g3_project_vertex(&text_center);
- if (!(text_center.flags & PF_OVERFLOW)) {
- gr_set_color_fast(&colour_white);
- gr_string((int)text_center.screen.xyw.x, (int)text_center.screen.xyw.y, buf.c_str());
+ //draw the text. rotate the center of the subsystem into place before finding out where to put the text
+ vec3d center_pt;
+ vm_vec_unrotate(¢er_pt, &bsp->offset, &objp->orient);
+ vm_vec_add2(¢er_pt, &objp->pos);
+ g3_rotate_vertex(&text_center, ¢er_pt);
+ g3_project_vertex(&text_center);
+ if (!(text_center.flags & PF_OVERFLOW)) {
+ gr_string_outlined((int)text_center.screen.xyw.x, (int)text_center.screen.xyw.y, buf.c_str(), &colour_white, &colour_black, 2);
+ }
}
}
@@ -1889,9 +1853,14 @@ void render_one_model_htl(object *objp) {
prop* propp = prop_id_lookup(z);
if (Fred_outline) {
+ // use a different LOD for the wireframe to reduce visual clutter on high-poly models
+ int prop_model_num = Prop_info[propp->prop_info_index].model_num;
+ int outline_lod = std::min(Outline_lod, model_get(prop_model_num)->n_detail_levels - 1);
+ render_info.set_detail_level_lock(outline_lod);
render_info.set_color(Fred_outline >> 16, (Fred_outline >> 8) & 0xff, Fred_outline & 0xff);
render_info.set_flags(flags | MR_SHOW_OUTLINE_HTL | MR_NO_LIGHTING | MR_NO_POLYS | MR_NO_TEXTURING);
- model_render_immediate(&render_info, Prop_info[propp->prop_info_index].model_num, propp->model_instance_num, &objp->orient, &objp->pos);
+ model_render_immediate(&render_info, prop_model_num, propp->model_instance_num, &objp->orient, &objp->pos);
+ render_info.set_detail_level_lock(-1);
}
//
render_info.set_flags(flags);
@@ -1930,9 +1899,14 @@ void render_one_model_htl(object *objp) {
render_info.set_replacement_textures(model_get_instance(Ships[z].model_instance_num)->texture_replace);
if (Fred_outline) {
+ // use a different LOD for the wireframe to reduce visual clutter on high-poly models
+ int ship_model_num = Ship_info[Ships[z].ship_info_index].model_num;
+ int outline_lod = std::min(Outline_lod, model_get(ship_model_num)->n_detail_levels - 1);
+ render_info.set_detail_level_lock(outline_lod);
render_info.set_color(Fred_outline >> 16, (Fred_outline >> 8) & 0xff, Fred_outline & 0xff);
render_info.set_flags(flags | MR_SHOW_OUTLINE_HTL | MR_NO_LIGHTING | MR_NO_POLYS | MR_NO_TEXTURING);
- model_render_immediate(&render_info, Ship_info[Ships[z].ship_info_index].model_num, Ships[z].model_instance_num, &objp->orient, &objp->pos);
+ model_render_immediate(&render_info, ship_model_num, Ships[z].model_instance_num, &objp->orient, &objp->pos);
+ render_info.set_detail_level_lock(-1);
}
if (Draw_outline_at_warpin_position
diff --git a/fred2/fredrender.h b/fred2/fredrender.h
index ffabc5d6e3c..22e8c1aa3f1 100644
--- a/fred2/fredrender.h
+++ b/fred2/fredrender.h
@@ -22,6 +22,7 @@ extern int Show_coordinates; //!< Bool. If nonzero, draw the coordinates
extern int Show_outlines; //!< Bool. If nonzero, draw each object's mesh. If models are shown, highlight them in white.
extern bool Draw_outlines_on_selected_ships; // If a ship is selected, draw mesh lines
extern bool Draw_outline_at_warpin_position; // Project an outline at the place where the ship will arrive after warping in
+extern int Outline_lod; // The LOD to use for wireframe outlines (0 = highest detail, default 1)
extern bool Always_save_display_names; // When saving a mission, always write display names to the mission file even if the display name is not set.
// But ships in wings are excepted, because a display name will cause a ship to have the same name in every wave.
// In the future, a display name feature could be added to the wing dialog to handle this case.
diff --git a/fred2/fredview.cpp b/fred2/fredview.cpp
index 62a135fd166..b0d64d1e377 100644
--- a/fred2/fredview.cpp
+++ b/fred2/fredview.cpp
@@ -171,6 +171,16 @@ BEGIN_MESSAGE_MAP(CFREDView, CView)
ON_COMMAND(ID_CHANGE_VIEWPOINT_EXTERNAL, OnChangeViewpointExternal)
ON_UPDATE_COMMAND_UI(ID_CHANGE_VIEWPOINT_FOLLOW, OnUpdateChangeViewpointFollow)
ON_COMMAND(ID_CHANGE_VIEWPOINT_FOLLOW, OnChangeViewpointFollow)
+ ON_COMMAND(ID_OUTLINE_LOD_0, OnOutlineLod0)
+ ON_UPDATE_COMMAND_UI(ID_OUTLINE_LOD_0, OnUpdateOutlineLod0)
+ ON_COMMAND(ID_OUTLINE_LOD_1, OnOutlineLod1)
+ ON_UPDATE_COMMAND_UI(ID_OUTLINE_LOD_1, OnUpdateOutlineLod1)
+ ON_COMMAND(ID_OUTLINE_LOD_2, OnOutlineLod2)
+ ON_UPDATE_COMMAND_UI(ID_OUTLINE_LOD_2, OnUpdateOutlineLod2)
+ ON_COMMAND(ID_OUTLINE_LOD_3, OnOutlineLod3)
+ ON_UPDATE_COMMAND_UI(ID_OUTLINE_LOD_3, OnUpdateOutlineLod3)
+ ON_COMMAND(ID_OUTLINE_LOD_4, OnOutlineLod4)
+ ON_UPDATE_COMMAND_UI(ID_OUTLINE_LOD_4, OnUpdateOutlineLod4)
ON_COMMAND(ID_EDITORS_GOALS, OnEditorsGoals)
ON_COMMAND(ID_EDITORS_CUTSCENES, OnEditorsCutscenes)
ON_COMMAND(ID_SPEED1, OnSpeed1)
@@ -1672,6 +1682,28 @@ void CFREDView::OnChangeViewpointFollow()
Update_window = 1;
}
+void CFREDView::OnOutlineLod(int lod)
+{
+ Outline_lod = lod;
+ Update_window = 1;
+}
+
+void CFREDView::OnUpdateOutlineLod(int lod, CCmdUI* pCmdUI)
+{
+ pCmdUI->SetCheck(Outline_lod == lod);
+}
+
+void CFREDView::OnOutlineLod0() { OnOutlineLod(0); }
+void CFREDView::OnOutlineLod1() { OnOutlineLod(1); }
+void CFREDView::OnOutlineLod2() { OnOutlineLod(2); }
+void CFREDView::OnOutlineLod3() { OnOutlineLod(3); }
+void CFREDView::OnOutlineLod4() { OnOutlineLod(4); }
+void CFREDView::OnUpdateOutlineLod0(CCmdUI* pCmdUI) { OnUpdateOutlineLod(0, pCmdUI); }
+void CFREDView::OnUpdateOutlineLod1(CCmdUI* pCmdUI) { OnUpdateOutlineLod(1, pCmdUI); }
+void CFREDView::OnUpdateOutlineLod2(CCmdUI* pCmdUI) { OnUpdateOutlineLod(2, pCmdUI); }
+void CFREDView::OnUpdateOutlineLod3(CCmdUI* pCmdUI) { OnUpdateOutlineLod(3, pCmdUI); }
+void CFREDView::OnUpdateOutlineLod4(CCmdUI* pCmdUI) { OnUpdateOutlineLod(4, pCmdUI); }
+
void CFREDView::OnEditorsGoals()
{
CMissionGoalsDlg dlg;
diff --git a/fred2/fredview.h b/fred2/fredview.h
index ac1cd9f0242..975f7cddbbc 100644
--- a/fred2/fredview.h
+++ b/fred2/fredview.h
@@ -126,6 +126,18 @@ class CFREDView : public CView
afx_msg void OnChangeViewpointExternal();
afx_msg void OnUpdateChangeViewpointFollow(CCmdUI* pCmdUI);
afx_msg void OnChangeViewpointFollow();
+ afx_msg void OnOutlineLod0();
+ afx_msg void OnOutlineLod1();
+ afx_msg void OnOutlineLod2();
+ afx_msg void OnOutlineLod3();
+ afx_msg void OnOutlineLod4();
+ afx_msg void OnUpdateOutlineLod0(CCmdUI* pCmdUI);
+ afx_msg void OnUpdateOutlineLod1(CCmdUI* pCmdUI);
+ afx_msg void OnUpdateOutlineLod2(CCmdUI* pCmdUI);
+ afx_msg void OnUpdateOutlineLod3(CCmdUI* pCmdUI);
+ afx_msg void OnUpdateOutlineLod4(CCmdUI* pCmdUI);
+ void OnOutlineLod(int lod);
+ void OnUpdateOutlineLod(int lod, CCmdUI* pCmdUI);
afx_msg void OnEditorsGoals();
afx_msg void OnEditorsCutscenes();
afx_msg void OnSpeed1();
diff --git a/fred2/resource.h b/fred2/resource.h
index 0ba948f0bd9..01a9268e51a 100644
--- a/fred2/resource.h
+++ b/fred2/resource.h
@@ -1455,6 +1455,11 @@
#define ID_NEW_PROP_TYPE 33104
#define ID_STATIC_SHIP_LABEL 33105
#define ID_STATIC_PROP_LABEL 33106
+#define ID_OUTLINE_LOD_0 33107
+#define ID_OUTLINE_LOD_1 33108
+#define ID_OUTLINE_LOD_2 33109
+#define ID_OUTLINE_LOD_3 33110
+#define ID_OUTLINE_LOD_4 33111
#define ID_VIEW_OUTLINES_ON_SELECTED 32982
#define ID_SHOW_STARFIELD 32983
#define ID_ASTEROID_EDITOR 32984
@@ -1580,7 +1585,7 @@
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_3D_CONTROLS 1
#define _APS_NEXT_RESOURCE_VALUE 335
-#define _APS_NEXT_COMMAND_VALUE 33104
+#define _APS_NEXT_COMMAND_VALUE 33112
#define _APS_NEXT_CONTROL_VALUE 1705
#define _APS_NEXT_SYMED_VALUE 105
#endif
diff --git a/qtfred/src/mission/EditorViewport.h b/qtfred/src/mission/EditorViewport.h
index 35e93deef87..5026e70d751 100644
--- a/qtfred/src/mission/EditorViewport.h
+++ b/qtfred/src/mission/EditorViewport.h
@@ -34,6 +34,7 @@ struct ViewSettings {
bool Show_waypoints = true;
bool Show_compass = true;
bool Highlight_selectable_subsys = false;
+ int Outline_lod = 1;
ViewSettings();
};
diff --git a/qtfred/src/mission/FredRenderer.cpp b/qtfred/src/mission/FredRenderer.cpp
index 1739f0435b1..b5e4b48d3cd 100644
--- a/qtfred/src/mission/FredRenderer.cpp
+++ b/qtfred/src/mission/FredRenderer.cpp
@@ -144,7 +144,9 @@ void draw_asteroid_field() {
}
}
-void fredhtl_render_subsystem_bounding_box(subsys_to_render *s2r)
+enum class subsystem_highlight { BOUNDING_BOX, LABEL };
+
+void fredhtl_render_subsystem_highlight(subsys_to_render *s2r, subsystem_highlight highlight)
{
vertex text_center;
SCP_string buf;
@@ -158,119 +160,75 @@ void fredhtl_render_subsystem_bounding_box(subsys_to_render *s2r)
auto bsp = &pm->submodel[subobj_num];
- vec3d front_top_left = bsp->bounding_box[7];
- vec3d front_top_right = bsp->bounding_box[6];
- vec3d front_bot_left = bsp->bounding_box[4];
- vec3d front_bot_right = bsp->bounding_box[5];
- vec3d back_top_left = bsp->bounding_box[3];
- vec3d back_top_right = bsp->bounding_box[2];
- vec3d back_bot_left = bsp->bounding_box[0];
- vec3d back_bot_right = bsp->bounding_box[1];
-
- gr_set_color(255, 32, 32);
-
- enable_htl();
-
- // get into the frame of reference of the submodel
- int g3_count = 1;
- g3_start_instance_matrix(&objp->pos, &objp->orient, true);
- int mn = subobj_num;
- while ((mn >= 0) && (pm->submodel[mn].parent >= 0))
- {
- g3_start_instance_matrix(&pm->submodel[mn].offset, &pmi->submodel[mn].canonical_orient, true);
- g3_count++;
- mn = pm->submodel[mn].parent;
- }
-
-
- //draw a cube around the subsystem
- g3_draw_htl_line(&front_top_left, &front_top_right);
- g3_draw_htl_line(&front_top_right, &front_bot_right);
- g3_draw_htl_line(&front_bot_right, &front_bot_left);
- g3_draw_htl_line(&front_bot_left, &front_top_left);
-
- g3_draw_htl_line(&back_top_left, &back_top_right);
- g3_draw_htl_line(&back_top_right, &back_bot_right);
- g3_draw_htl_line(&back_bot_right, &back_bot_left);
- g3_draw_htl_line(&back_bot_left, &back_top_left);
-
- g3_draw_htl_line(&front_top_left, &back_top_left);
- g3_draw_htl_line(&front_top_right, &back_top_right);
- g3_draw_htl_line(&front_bot_left, &back_bot_left);
- g3_draw_htl_line(&front_bot_right, &back_bot_right);
-
+ // transform bounding box corners from submodel-local space to world space
+ // and draw edges as thick camera-facing quads via g3_render_rod
+ color clr_red;
+ gr_init_color(&clr_red, 255, 32, 32);
+ float rod_width = 2.0f;
+
+ auto transform_and_draw_box = [&](const vec3d *bbox, int sobj_num) {
+ vec3d corners[8];
+ for (int i = 0; i < 8; i++)
+ model_instance_local_to_global_point(&corners[i], &bbox[i], pm, pmi, sobj_num, &objp->orient, &objp->pos);
+
+ // 12 edges of a box: front face, back face, connecting edges
+ // bounding_box indices: 0=BBL 1=BBR 2=BTR 3=BTL 4=FBL 5=FBR 6=FTR 7=FTL
+ static const int edges[12][2] = {
+ {7, 6}, {6, 5}, {5, 4}, {4, 7}, // front face
+ {3, 2}, {2, 1}, {1, 0}, {0, 3}, // back face
+ {7, 3}, {6, 2}, {4, 0}, {5, 1}, // connecting edges
+ };
+
+ for (const auto& edge : edges) {
+ vec3d pts[2] = { corners[edge[0]], corners[edge[1]] };
+ g3_render_rod(&clr_red, 2, pts, rod_width);
+ }
+ };
- //draw another cube around a gun for a two-part turret
- if ((ss->system_info->turret_gun_sobj >= 0) && (ss->system_info->turret_gun_sobj != ss->system_info->subobj_num))
- {
- bsp_info *bsp_turret = &pm->submodel[ss->system_info->turret_gun_sobj];
-
- front_top_left = bsp_turret->bounding_box[7];
- front_top_right = bsp_turret->bounding_box[6];
- front_bot_left = bsp_turret->bounding_box[4];
- front_bot_right = bsp_turret->bounding_box[5];
- back_top_left = bsp_turret->bounding_box[3];
- back_top_right = bsp_turret->bounding_box[2];
- back_bot_left = bsp_turret->bounding_box[0];
- back_bot_right = bsp_turret->bounding_box[1];
-
- g3_start_instance_matrix(&bsp_turret->offset, &pmi->submodel[ss->system_info->turret_gun_sobj].canonical_orient, true);
-
- g3_draw_htl_line(&front_top_left, &front_top_right);
- g3_draw_htl_line(&front_top_right, &front_bot_right);
- g3_draw_htl_line(&front_bot_right, &front_bot_left);
- g3_draw_htl_line(&front_bot_left, &front_top_left);
-
- g3_draw_htl_line(&back_top_left, &back_top_right);
- g3_draw_htl_line(&back_top_right, &back_bot_right);
- g3_draw_htl_line(&back_bot_right, &back_bot_left);
- g3_draw_htl_line(&back_bot_left, &back_top_left);
-
- g3_draw_htl_line(&front_top_left, &back_top_left);
- g3_draw_htl_line(&front_top_right, &back_top_right);
- g3_draw_htl_line(&front_bot_left, &back_bot_left);
- g3_draw_htl_line(&front_bot_right, &back_bot_right);
-
- g3_done_instance(true);
- }
+ if (highlight == subsystem_highlight::BOUNDING_BOX) {
+ enable_htl();
- for (int i = 0; i < g3_count; i++)
- g3_done_instance(true);
+ // draw a box around the subsystem
+ transform_and_draw_box(bsp->bounding_box, subobj_num);
- disable_htl();
+ // draw another box around a gun for a two-part turret
+ if ((ss->system_info->turret_gun_sobj >= 0) && (ss->system_info->turret_gun_sobj != ss->system_info->subobj_num))
+ transform_and_draw_box(pm->submodel[ss->system_info->turret_gun_sobj].bounding_box, ss->system_info->turret_gun_sobj);
- // get text
- buf = ss->system_info->subobj_name;
+ disable_htl();
+ } else {
+ // get text
+ buf = ss->system_info->subobj_name;
- // add weapons if present
- for (int i = 0; i < ss->weapons.num_primary_banks; ++i)
- {
- int wi = ss->weapons.primary_bank_weapons[i];
- if (wi >= 0)
+ // add weapons if present
+ for (int i = 0; i < ss->weapons.num_primary_banks; ++i)
{
- buf += "\n";
- buf += Weapon_info[wi].name;
+ int wi = ss->weapons.primary_bank_weapons[i];
+ if (wi >= 0)
+ {
+ buf += "\n";
+ buf += Weapon_info[wi].name;
+ }
}
- }
- for (int i = 0; i < ss->weapons.num_secondary_banks; ++i)
- {
- int wi = ss->weapons.secondary_bank_weapons[i];
- if (wi >= 0)
+ for (int i = 0; i < ss->weapons.num_secondary_banks; ++i)
{
- buf += "\n";
- buf += Weapon_info[wi].name;
+ int wi = ss->weapons.secondary_bank_weapons[i];
+ if (wi >= 0)
+ {
+ buf += "\n";
+ buf += Weapon_info[wi].name;
+ }
}
- }
- //draw the text. rotate the center of the subsystem into place before finding out where to put the text
- vec3d center_pt;
- vm_vec_unrotate(¢er_pt, &bsp->offset, &objp->orient);
- vm_vec_add2(¢er_pt, &objp->pos);
- g3_rotate_vertex(&text_center, ¢er_pt);
- g3_project_vertex(&text_center);
- if (!(text_center.flags & PF_OVERFLOW)) {
- gr_set_color_fast(&colour_white);
- gr_string((int)text_center.screen.xyw.x, (int)text_center.screen.xyw.y, buf.c_str());
+ //draw the text. rotate the center of the subsystem into place before finding out where to put the text
+ vec3d center_pt;
+ vm_vec_unrotate(¢er_pt, &bsp->offset, &objp->orient);
+ vm_vec_add2(¢er_pt, &objp->pos);
+ g3_rotate_vertex(&text_center, ¢er_pt);
+ g3_project_vertex(&text_center);
+ if (!(text_center.flags & PF_OVERFLOW)) {
+ gr_string_outlined((int)text_center.screen.xyw.x, (int)text_center.screen.xyw.y, buf.c_str(), &colour_white, &colour_black, 2);
+ }
}
}
@@ -549,10 +507,18 @@ void FredRenderer::display_active_ship_subsystem(subsys_to_render& Render_subsys
if (view().Highlight_selectable_subsys) {
auto shipp = &Ships[objp->instance];
- for (auto ss = GET_FIRST(&shipp->subsys_list); ss != END_OF_LIST(&shipp->subsys_list); ss = GET_NEXT(ss)) {
+ // first pass: draw all bounding boxes
+ for (auto ss : list_range(&shipp->subsys_list)) {
+ if (ss->system_info->subobj_num != -1) {
+ subsys_to_render s2r = { true, objp, ss };
+ fredhtl_render_subsystem_highlight(&s2r, subsystem_highlight::BOUNDING_BOX);
+ }
+ }
+ // second pass: draw all labels
+ for (auto ss : list_range(&shipp->subsys_list)) {
if (ss->system_info->subobj_num != -1) {
subsys_to_render s2r = { true, objp, ss };
- fredhtl_render_subsystem_bounding_box(&s2r);
+ fredhtl_render_subsystem_highlight(&s2r, subsystem_highlight::LABEL);
}
}
}
@@ -565,7 +531,8 @@ void FredRenderer::display_active_ship_subsystem(subsys_to_render& Render_subsys
}
if (Render_subsys.do_render) {
- fredhtl_render_subsystem_bounding_box(&Render_subsys);
+ fredhtl_render_subsystem_highlight(&Render_subsys, subsystem_highlight::BOUNDING_BOX);
+ fredhtl_render_subsystem_highlight(&Render_subsys, subsystem_highlight::LABEL);
} else {
cancel_display_active_ship_subsystem(Render_subsys);
}
@@ -861,13 +828,18 @@ void FredRenderer::render_one_model_htl(object* objp,
render_info.set_debug_flags(0);
if (Fred_outline) {
+ // use a different LOD for the wireframe to reduce visual clutter on high-poly models
+ int prop_model_num = Prop_info[propp->prop_info_index].model_num;
+ int outline_lod = std::min(view().Outline_lod, model_get(prop_model_num)->n_detail_levels - 1);
+ render_info.set_detail_level_lock(outline_lod);
render_info.set_color(Fred_outline >> 16, (Fred_outline >> 8) & 0xff, Fred_outline & 0xff);
render_info.set_flags(flags | MR_SHOW_OUTLINE_HTL | MR_NO_LIGHTING | MR_NO_POLYS | MR_NO_TEXTURING);
model_render_immediate(&render_info,
- Prop_info[propp->prop_info_index].model_num,
+ prop_model_num,
propp->model_instance_num,
&objp->orient,
&objp->pos);
+ render_info.set_detail_level_lock(-1);
}
render_info.set_flags(flags);
@@ -913,22 +885,31 @@ void FredRenderer::render_one_model_htl(object* objp,
g3_done_instance(false);
+ int ship_model_num = Ship_info[Ships[z].ship_info_index].model_num;
+ int ship_model_instance_num = Ships[z].model_instance_num;
+
// Outline pass: use a dedicated pass with MR_NO_POLYS so is_outlines_only_htl fires
// in the renderer. Modern HTL models don't have outline_buffer, so relying on
// MR_SHOW_OUTLINE_HTL alone (without MR_NO_POLYS) silently does nothing.
if (Fred_outline) {
model_render_params outline_info;
+ // use a different LOD for the wireframe to reduce visual clutter on high-poly models
+ int outline_lod = std::min(view().Outline_lod, model_get(ship_model_num)->n_detail_levels - 1);
+ outline_info.set_detail_level_lock(outline_lod);
+
outline_info.set_color(Fred_outline >> 16, (Fred_outline >> 8) & 0xff, Fred_outline & 0xff);
outline_info.set_flags(flags | MR_SHOW_OUTLINE_HTL | MR_NO_POLYS | MR_NO_LIGHTING | MR_NO_TEXTURING);
- model_render_immediate(&outline_info, Ship_info[Ships[z].ship_info_index].model_num, Ships[z].model_instance_num, &objp->orient, &objp->pos);
+ model_render_immediate(&outline_info, ship_model_num, ship_model_instance_num, &objp->orient, &objp->pos);
+
+ outline_info.set_detail_level_lock(-1);
}
if (view().Show_ship_models) {
model_render_params render_info;
render_info.set_debug_flags(debug_flags);
- render_info.set_replacement_textures(model_get_instance(Ships[z].model_instance_num)->texture_replace);
+ render_info.set_replacement_textures(model_get_instance(ship_model_instance_num)->texture_replace);
render_info.set_flags(flags);
- model_render_immediate(&render_info, Ship_info[Ships[z].ship_info_index].model_num, Ships[z].model_instance_num, &objp->orient, &objp->pos);
+ model_render_immediate(&render_info, ship_model_num, ship_model_instance_num, &objp->orient, &objp->pos);
}
if (view().Draw_outline_at_warpin_position
diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp
index 7510dffaa58..42fde42ec0d 100644
--- a/qtfred/src/ui/FredView.cpp
+++ b/qtfred/src/ui/FredView.cpp
@@ -655,6 +655,22 @@ void FredView::syncViewOptions() {
connectActionToViewSetting(ui->actionShowDistances, &_viewport->view.Show_distances);
connect(ui->actionVisibility_Layers, &QAction::triggered, this, [this]() { openLayerManagerDialog(); });
+
+ // Outline LOD submenu — mutually exclusive options
+ QAction* outlineLodActions[] = {
+ ui->actionOutline_LOD_0, ui->actionOutline_LOD_1, ui->actionOutline_LOD_2,
+ ui->actionOutline_LOD_3, ui->actionOutline_LOD_4
+ };
+ for (int i = 0; i < 5; i++) {
+ auto action = outlineLodActions[i];
+ connect(this, &FredView::viewIdle, this, [action, this, i]() {
+ action->setChecked(_viewport->view.Outline_lod == i);
+ });
+ connect(action, &QAction::triggered, this, [this, i]() {
+ _viewport->view.Outline_lod = i;
+ _viewport->needsUpdate();
+ });
+ }
}
void FredView::initializeStatusBar() {
_statusBarViewmode = new QLabel();
diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui
index 4b9a3c736dc..42e223e8b41 100644
--- a/qtfred/ui/FredView.ui
+++ b/qtfred/ui/FredView.ui
@@ -86,6 +86,16 @@
&View
+
+
@@ -928,6 +939,46 @@
Current &Ship
+
+
+ true
+
+
+ LOD 0 (highest detail)
+
+
+
+
+ true
+
+
+ LOD 1
+
+
+
+
+ true
+
+
+ LOD 2
+
+
+
+
+ true
+
+
+ LOD 3
+
+
+
+
+ true
+
+
+ LOD 4 (lowest detail)
+
+
true