Skip to content

Commit 4765d1e

Browse files
committed
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
1 parent 219f692 commit 4765d1e

13 files changed

Lines changed: 347 additions & 225 deletions

File tree

code/graphics/render.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,20 @@ void gr_string(float sx, float sy, const char* s, int resize_mode, float scaleMu
934934
}
935935
}
936936

937+
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)
938+
{
939+
// draw outline by rendering text at all surrounding offsets
940+
gr_set_color_fast(outline);
941+
for (int dx = -offset; dx <= offset; dx++)
942+
for (int dy = -offset; dy <= offset; dy++)
943+
if (dx || dy)
944+
gr_string(x + dx, y + dy, text, resize_mode, scaleMultiplier, length);
945+
946+
// draw foreground text on top
947+
gr_set_color_fast(foreground);
948+
gr_string(x, y, text, resize_mode, scaleMultiplier, length);
949+
}
950+
937951
static void gr_line(float x1, float y1, float x2, float y2, int resize_mode) {
938952
auto path = beginDrawing(resize_mode);
939953

code/graphics/render.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,25 @@ inline void gr_string(int x, int y, const char* string, int resize_mode = GR_RES
7979
gr_string(i2fl(x), i2fl(y), string, resize_mode, scaleMultiplier, length);
8080
}
8181

82+
/**
83+
* @brief Draws outlined text at the given position
84+
*
85+
* @details Renders the text string with an outline by drawing the text
86+
* at surrounding offsets in the outline color, then drawing the main text on top
87+
* in the foreground color.
88+
*
89+
* @param x The x-coordinate
90+
* @param y The y-coordinate
91+
* @param text The text to draw
92+
* @param foreground Color for the main text
93+
* @param outline Color for the outline
94+
* @param offset Pixel offset for the outline (default 1)
95+
* @param resize_mode The mode for translating the screen positions
96+
* @param scaleMultiplier The scale to use to apply scaling in addition to user settings
97+
* @param length The number of bytes in the string to render. -1 will render the whole string.
98+
*/
99+
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);
100+
82101
/**
83102
* @brief Draws a single line segment to the screen.
84103
* @param x1 The starting x-coordinate

fred2/fred.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ BOOL CFREDApp::InitInstance() {
246246
Draw_outlines_on_selected_ships = GetProfileInt("Preferences", "Draw outlines on selected ships", 1) != 0;
247247
Point_using_uvec = GetProfileInt("Preferences", "Point using uvec", Point_using_uvec);
248248
Draw_outline_at_warpin_position = GetProfileInt("Preferences", "Draw outline at warpin position", 0) != 0;
249+
Outline_lod = GetProfileInt("Preferences", "Outline LOD", 1);
249250
Always_save_display_names = GetProfileInt("Preferences", "Always save display names", 0) != 0;
250251
Error_checker_checks_potential_issues = GetProfileInt("Preferences", "Error checker checks potential issues", 1) != 0;
251252

@@ -546,6 +547,7 @@ void CFREDApp::write_ini_file(int degree) {
546547
WriteProfileInt("Preferences", "Draw outlines on selected ships", Draw_outlines_on_selected_ships ? 1 : 0);
547548
WriteProfileInt("Preferences", "Point using uvec", Point_using_uvec);
548549
WriteProfileInt("Preferences", "Draw outline at warpin position", Draw_outline_at_warpin_position ? 1 : 0);
550+
WriteProfileInt("Preferences", "Outline LOD", Outline_lod);
549551
WriteProfileInt("Preferences", "Always save display names", Always_save_display_names ? 1 : 0);
550552
WriteProfileInt("Preferences", "Error checker checks potential issues", Error_checker_checks_potential_issues ? 1 : 0);
551553

fred2/fred.rc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,14 @@ BEGIN
264264
MENUITEM "Show IFF 8", 33089
265265
MENUITEM "Show IFF 9", 32988
266266
END
267+
POPUP "Outline LOD"
268+
BEGIN
269+
MENUITEM "LOD 0 (highest detail)", 33107
270+
MENUITEM "LOD 1", 33108, CHECKED
271+
MENUITEM "LOD 2", 33109
272+
MENUITEM "LOD 3", 33110
273+
MENUITEM "LOD 4 (lowest detail)", 33111
274+
END
267275
MENUITEM SEPARATOR
268276
MENUITEM "Hide Marked Objects", 33002
269277
MENUITEM "Show Hidden Objects", 33003

fred2/fredrender.cpp

Lines changed: 87 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ int Show_horizon = 0;
103103
int Show_outlines = 0;
104104
bool Draw_outlines_on_selected_ships = true;
105105
bool Draw_outline_at_warpin_position = false;
106+
int Outline_lod = 1;
106107
bool Always_save_display_names = false;
107108
bool Error_checker_checks_potential_issues = true;
108109
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
211212
*/
212213
void render_models(void);
213214

215+
enum class subsystem_highlight { BOUNDING_BOX, LABEL };
214216
/**
215217
* @brief Renders the bounding box for the given subsystem with HTL
216218
*
217219
* @param[in] s2r Subsystem to render a box for
218220
*/
219-
void fredhtl_render_subsystem_bounding_box(subsys_to_render * s2r);
221+
void fredhtl_render_subsystem_highlight(subsys_to_render *s2r, subsystem_highlight highlight);
220222

221223
/**
222224
* @brief Draws the X from a elevation line on the grid
@@ -414,10 +416,18 @@ void display_active_ship_subsystem() {
414416
if (Highlight_selectable_subsys) {
415417
auto shipp = &Ships[objp->instance];
416418

417-
for (auto ss = GET_FIRST(&shipp->subsys_list); ss != END_OF_LIST(&shipp->subsys_list); ss = GET_NEXT(ss)) {
419+
// first pass: draw all bounding boxes
420+
for (auto ss : list_range(&shipp->subsys_list)) {
418421
if (ss->system_info->subobj_num != -1) {
419422
subsys_to_render s2r = { true, objp, ss };
420-
fredhtl_render_subsystem_bounding_box(&s2r);
423+
fredhtl_render_subsystem_highlight(&s2r, subsystem_highlight::BOUNDING_BOX);
424+
}
425+
}
426+
// second pass: draw all labels
427+
for (auto ss : list_range(&shipp->subsys_list)) {
428+
if (ss->system_info->subobj_num != -1) {
429+
subsys_to_render s2r = { true, objp, ss };
430+
fredhtl_render_subsystem_highlight(&s2r, subsystem_highlight::LABEL);
421431
}
422432
}
423433
}
@@ -430,7 +440,8 @@ void display_active_ship_subsystem() {
430440
}
431441

432442
if (Render_subsys.do_render) {
433-
fredhtl_render_subsystem_bounding_box(&Render_subsys);
443+
fredhtl_render_subsystem_highlight(&Render_subsys, subsystem_highlight::BOUNDING_BOX);
444+
fredhtl_render_subsystem_highlight(&Render_subsys, subsystem_highlight::LABEL);
434445
} else {
435446
cancel_display_active_ship_subsystem();
436447
}
@@ -726,7 +737,7 @@ void draw_orient_sphere2(int col, object *obj, int r, int g, int b) {
726737
}
727738
}
728739

729-
void fredhtl_render_subsystem_bounding_box(subsys_to_render *s2r)
740+
void fredhtl_render_subsystem_highlight(subsys_to_render *s2r, subsystem_highlight highlight)
730741
{
731742
vertex text_center;
732743
SCP_string buf;
@@ -740,122 +751,75 @@ void fredhtl_render_subsystem_bounding_box(subsys_to_render *s2r)
740751

741752
auto bsp = &pm->submodel[subobj_num];
742753

743-
vec3d front_top_left = bsp->bounding_box[7];
744-
vec3d front_top_right = bsp->bounding_box[6];
745-
vec3d front_bot_left = bsp->bounding_box[4];
746-
vec3d front_bot_right = bsp->bounding_box[5];
747-
vec3d back_top_left = bsp->bounding_box[3];
748-
vec3d back_top_right = bsp->bounding_box[2];
749-
vec3d back_bot_left = bsp->bounding_box[0];
750-
vec3d back_bot_right = bsp->bounding_box[1];
751-
752-
gr_set_color(255, 32, 32);
753-
754-
fred_enable_htl();
755-
756-
// get into the frame of reference of the submodel
757-
int g3_count = 1;
758-
g3_start_instance_matrix(&objp->pos, &objp->orient, true);
759-
int mn = subobj_num;
760-
while ((mn >= 0) && (pm->submodel[mn].parent >= 0))
761-
{
762-
vec3d offset = pm->submodel[mn].offset;
763-
vm_vec_add2(&offset, &pmi->submodel[mn].canonical_offset);
764-
765-
g3_start_instance_matrix(&offset, &pmi->submodel[mn].canonical_orient, true);
766-
g3_count++;
767-
mn = pm->submodel[mn].parent;
768-
}
769-
770-
771-
//draw a cube around the subsystem
772-
g3_draw_htl_line(&front_top_left, &front_top_right);
773-
g3_draw_htl_line(&front_top_right, &front_bot_right);
774-
g3_draw_htl_line(&front_bot_right, &front_bot_left);
775-
g3_draw_htl_line(&front_bot_left, &front_top_left);
776-
777-
g3_draw_htl_line(&back_top_left, &back_top_right);
778-
g3_draw_htl_line(&back_top_right, &back_bot_right);
779-
g3_draw_htl_line(&back_bot_right, &back_bot_left);
780-
g3_draw_htl_line(&back_bot_left, &back_top_left);
781-
782-
g3_draw_htl_line(&front_top_left, &back_top_left);
783-
g3_draw_htl_line(&front_top_right, &back_top_right);
784-
g3_draw_htl_line(&front_bot_left, &back_bot_left);
785-
g3_draw_htl_line(&front_bot_right, &back_bot_right);
786-
754+
// transform bounding box corners from submodel-local space to world space
755+
// and draw edges as thick camera-facing quads via g3_render_rod
756+
color clr_red;
757+
gr_init_color(&clr_red, 255, 32, 32);
758+
float rod_width = 2.0f;
759+
760+
auto transform_and_draw_box = [&](const vec3d *bbox, int sobj_num) {
761+
vec3d corners[8];
762+
for (int i = 0; i < 8; i++)
763+
model_instance_local_to_global_point(&corners[i], &bbox[i], pm, pmi, sobj_num, &objp->orient, &objp->pos);
764+
765+
// 12 edges of a box: front face, back face, connecting edges
766+
// bounding_box indices: 0=BBL 1=BBR 2=BTR 3=BTL 4=FBL 5=FBR 6=FTR 7=FTL
767+
static const int edges[12][2] = {
768+
{7, 6}, {6, 5}, {5, 4}, {4, 7}, // front face
769+
{3, 2}, {2, 1}, {1, 0}, {0, 3}, // back face
770+
{7, 3}, {6, 2}, {4, 0}, {5, 1}, // connecting edges
771+
};
772+
773+
for (const auto& edge : edges) {
774+
vec3d pts[2] = { corners[edge[0]], corners[edge[1]] };
775+
g3_render_rod(&clr_red, 2, pts, rod_width);
776+
}
777+
};
787778

788-
//draw another cube around a gun for a two-part turret
789-
if ((ss->system_info->turret_gun_sobj >= 0) && (ss->system_info->turret_gun_sobj != ss->system_info->subobj_num))
790-
{
791-
bsp_info *bsp_turret = &pm->submodel[ss->system_info->turret_gun_sobj];
792-
793-
front_top_left = bsp_turret->bounding_box[7];
794-
front_top_right = bsp_turret->bounding_box[6];
795-
front_bot_left = bsp_turret->bounding_box[4];
796-
front_bot_right = bsp_turret->bounding_box[5];
797-
back_top_left = bsp_turret->bounding_box[3];
798-
back_top_right = bsp_turret->bounding_box[2];
799-
back_bot_left = bsp_turret->bounding_box[0];
800-
back_bot_right = bsp_turret->bounding_box[1];
801-
802-
g3_start_instance_matrix(&bsp_turret->offset, &pmi->submodel[ss->system_info->turret_gun_sobj].canonical_orient, true);
803-
804-
g3_draw_htl_line(&front_top_left, &front_top_right);
805-
g3_draw_htl_line(&front_top_right, &front_bot_right);
806-
g3_draw_htl_line(&front_bot_right, &front_bot_left);
807-
g3_draw_htl_line(&front_bot_left, &front_top_left);
808-
809-
g3_draw_htl_line(&back_top_left, &back_top_right);
810-
g3_draw_htl_line(&back_top_right, &back_bot_right);
811-
g3_draw_htl_line(&back_bot_right, &back_bot_left);
812-
g3_draw_htl_line(&back_bot_left, &back_top_left);
813-
814-
g3_draw_htl_line(&front_top_left, &back_top_left);
815-
g3_draw_htl_line(&front_top_right, &back_top_right);
816-
g3_draw_htl_line(&front_bot_left, &back_bot_left);
817-
g3_draw_htl_line(&front_bot_right, &back_bot_right);
818-
819-
g3_done_instance(true);
820-
}
779+
if (highlight == subsystem_highlight::BOUNDING_BOX) {
780+
fred_enable_htl();
821781

822-
for (int i = 0; i < g3_count; i++)
823-
g3_done_instance(true);
782+
// draw a box around the subsystem
783+
transform_and_draw_box(bsp->bounding_box, subobj_num);
824784

825-
fred_disable_htl();
785+
// draw another box around a gun for a two-part turret
786+
if ((ss->system_info->turret_gun_sobj >= 0) && (ss->system_info->turret_gun_sobj != ss->system_info->subobj_num))
787+
transform_and_draw_box(pm->submodel[ss->system_info->turret_gun_sobj].bounding_box, ss->system_info->turret_gun_sobj);
826788

827-
// get text
828-
buf = ss->system_info->subobj_name;
789+
fred_disable_htl();
790+
} else {
791+
// get text
792+
buf = ss->system_info->subobj_name;
829793

830-
// add weapons if present
831-
for (int i = 0; i < ss->weapons.num_primary_banks; ++i)
832-
{
833-
int wi = ss->weapons.primary_bank_weapons[i];
834-
if (wi >= 0)
794+
// add weapons if present
795+
for (int i = 0; i < ss->weapons.num_primary_banks; ++i)
835796
{
836-
buf += "\n";
837-
buf += Weapon_info[wi].name;
797+
int wi = ss->weapons.primary_bank_weapons[i];
798+
if (wi >= 0)
799+
{
800+
buf += "\n";
801+
buf += Weapon_info[wi].name;
802+
}
838803
}
839-
}
840-
for (int i = 0; i < ss->weapons.num_secondary_banks; ++i)
841-
{
842-
int wi = ss->weapons.secondary_bank_weapons[i];
843-
if (wi >= 0)
804+
for (int i = 0; i < ss->weapons.num_secondary_banks; ++i)
844805
{
845-
buf += "\n";
846-
buf += Weapon_info[wi].name;
806+
int wi = ss->weapons.secondary_bank_weapons[i];
807+
if (wi >= 0)
808+
{
809+
buf += "\n";
810+
buf += Weapon_info[wi].name;
811+
}
847812
}
848-
}
849813

850-
//draw the text. rotate the center of the subsystem into place before finding out where to put the text
851-
vec3d center_pt;
852-
vm_vec_unrotate(&center_pt, &bsp->offset, &objp->orient);
853-
vm_vec_add2(&center_pt, &objp->pos);
854-
g3_rotate_vertex(&text_center, &center_pt);
855-
g3_project_vertex(&text_center);
856-
if (!(text_center.flags & PF_OVERFLOW)) {
857-
gr_set_color_fast(&colour_white);
858-
gr_string((int)text_center.screen.xyw.x, (int)text_center.screen.xyw.y, buf.c_str());
814+
//draw the text. rotate the center of the subsystem into place before finding out where to put the text
815+
vec3d center_pt;
816+
vm_vec_unrotate(&center_pt, &bsp->offset, &objp->orient);
817+
vm_vec_add2(&center_pt, &objp->pos);
818+
g3_rotate_vertex(&text_center, &center_pt);
819+
g3_project_vertex(&text_center);
820+
if (!(text_center.flags & PF_OVERFLOW)) {
821+
gr_string_outlined((int)text_center.screen.xyw.x, (int)text_center.screen.xyw.y, buf.c_str(), &colour_white, &colour_black, 2);
822+
}
859823
}
860824
}
861825

@@ -1889,9 +1853,14 @@ void render_one_model_htl(object *objp) {
18891853
prop* propp = prop_id_lookup(z);
18901854

18911855
if (Fred_outline) {
1856+
// use a different LOD for the wireframe to reduce visual clutter on high-poly models
1857+
int prop_model_num = Prop_info[propp->prop_info_index].model_num;
1858+
int outline_lod = std::min(Outline_lod, model_get(prop_model_num)->n_detail_levels - 1);
1859+
render_info.set_detail_level_lock(outline_lod);
18921860
render_info.set_color(Fred_outline >> 16, (Fred_outline >> 8) & 0xff, Fred_outline & 0xff);
18931861
render_info.set_flags(flags | MR_SHOW_OUTLINE_HTL | MR_NO_LIGHTING | MR_NO_POLYS | MR_NO_TEXTURING);
1894-
model_render_immediate(&render_info, Prop_info[propp->prop_info_index].model_num, propp->model_instance_num, &objp->orient, &objp->pos);
1862+
model_render_immediate(&render_info, prop_model_num, propp->model_instance_num, &objp->orient, &objp->pos);
1863+
render_info.set_detail_level_lock(-1);
18951864
}
18961865
//
18971866
render_info.set_flags(flags);
@@ -1930,9 +1899,14 @@ void render_one_model_htl(object *objp) {
19301899
render_info.set_replacement_textures(model_get_instance(Ships[z].model_instance_num)->texture_replace);
19311900

19321901
if (Fred_outline) {
1902+
// use a different LOD for the wireframe to reduce visual clutter on high-poly models
1903+
int ship_model_num = Ship_info[Ships[z].ship_info_index].model_num;
1904+
int outline_lod = std::min(Outline_lod, model_get(ship_model_num)->n_detail_levels - 1);
1905+
render_info.set_detail_level_lock(outline_lod);
19331906
render_info.set_color(Fred_outline >> 16, (Fred_outline >> 8) & 0xff, Fred_outline & 0xff);
19341907
render_info.set_flags(flags | MR_SHOW_OUTLINE_HTL | MR_NO_LIGHTING | MR_NO_POLYS | MR_NO_TEXTURING);
1935-
model_render_immediate(&render_info, Ship_info[Ships[z].ship_info_index].model_num, Ships[z].model_instance_num, &objp->orient, &objp->pos);
1908+
model_render_immediate(&render_info, ship_model_num, Ships[z].model_instance_num, &objp->orient, &objp->pos);
1909+
render_info.set_detail_level_lock(-1);
19361910
}
19371911

19381912
if (Draw_outline_at_warpin_position

fred2/fredrender.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ extern int Show_coordinates; //!< Bool. If nonzero, draw the coordinates
2222
extern int Show_outlines; //!< Bool. If nonzero, draw each object's mesh. If models are shown, highlight them in white.
2323
extern bool Draw_outlines_on_selected_ships; // If a ship is selected, draw mesh lines
2424
extern bool Draw_outline_at_warpin_position; // Project an outline at the place where the ship will arrive after warping in
25+
extern int Outline_lod; // The LOD to use for wireframe outlines (0 = highest detail, default 1)
2526
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.
2627
// But ships in wings are excepted, because a display name will cause a ship to have the same name in every wave.
2728
// In the future, a display name feature could be added to the wing dialog to handle this case.

0 commit comments

Comments
 (0)