From d01cf9187effa458e7a2241bce8929839ae62bfd Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 17 Mar 2026 21:51:25 -0400 Subject: [PATCH] FRED and qtFRED quality-of-life improvements for highlighted models A few visual improvements requested on Discord: 1. Use a configurable LOD to draw the outline wireframe, defaulting to LOD 1 2. Draw subsystem names with a dark outline to make them more visible 3. Draw the subsystem boundary boxes with thicker lines --- code/graphics/render.cpp | 14 ++ code/graphics/render.h | 19 +++ fred2/fred.cpp | 2 + fred2/fred.rc | 8 ++ fred2/fredrender.cpp | 200 ++++++++++++--------------- fred2/fredrender.h | 1 + fred2/fredview.cpp | 32 +++++ fred2/fredview.h | 12 ++ fred2/resource.h | 7 +- qtfred/src/mission/EditorViewport.h | 1 + qtfred/src/mission/FredRenderer.cpp | 203 +++++++++++++--------------- qtfred/src/ui/FredView.cpp | 16 +++ qtfred/ui/FredView.ui | 51 +++++++ 13 files changed, 341 insertions(+), 225 deletions(-) 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 + + + Outline LOD + + + + + + + &Viewpoint @@ -94,6 +104,7 @@ + @@ -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