diff --git a/code/def_files/data/tables/generated_nebula.tbl b/code/def_files/data/tables/generated_nebula.tbl new file mode 100644 index 00000000000..5ed7412194e --- /dev/null +++ b/code/def_files/data/tables/generated_nebula.tbl @@ -0,0 +1,165 @@ +;; Generated background nebula (the modern FS1-style nebula a mission picks with +Nebula: and +;; +Color:). Mods replace this whole table with their own generated_nebula.tbl, or add to and +;; override it by name with *-gneb.tbm files. + +#Generated Nebula Colors + +;; +RGB: the color of the brightest parts; dimmer parts fade evenly toward black. +;; +Palette Suffix: with a pattern's +Palette Prefix: names an FS1 palette file (-.pcx); +;; when that file exists its first 20 entries are the ramp instead of the RGB fade. Retail FS2 ships +;; only gamepalette1-01 (Nebula01 Red); the others are FS1 data a mod can ship. +;; The built-in colors are FS1's: each FS1 mission palette held a 20-step ramp from black +;; to one color, and +RGB is that ramp's brightest entry. + +$Name: Red ++RGB: 151 0 0 ++Palette Suffix: 01 + +$Name: Blue ++RGB: 0 55 183 ++Palette Suffix: 02 + +$Name: Gold ++RGB: 107 91 27 ++Palette Suffix: 03 + +$Name: Purple ++RGB: 107 0 95 ++Palette Suffix: 04 + +$Name: Maroon ++RGB: 107 0 39 ++Palette Suffix: 05 + +$Name: Green ++RGB: 0 95 0 ++Palette Suffix: 06 + +$Name: Grey blue ++RGB: 0 67 91 ++Palette Suffix: 07 + +$Name: Violet ++RGB: 63 51 79 ++Palette Suffix: 08 + +$Name: Grey Green ++RGB: 0 87 63 ++Palette Suffix: 09 + +#End + +#Generated Nebula Patterns + +;; A pattern has two layers, combined so the clouds brighten the background without clipping: +;; - a procedural background made from noise (+Density through +Seed), unless +Clouds Only +;; - placed clouds (+Cloud), broken up by the same noise according to +Cloud Detail +;; +;; Fields, all optional, in this order: +;; +Mesh: an FS1 .neb mesh (nebula01-03 ship in sparky_fs2.vp); drawn through the color ramp +;; instead of the generated layers when the file exists, else the layers below are used +;; +Palette Prefix: see +Palette Suffix: under the colors +;; +Clouds Only: YES for no procedural background, only the placed clouds +;; +Cloud Detail: how much the noise breaks up the clouds, 0 (smooth) to 1 (ragged) +;; +Cloud: lon lat width height angle brightness, repeated for each cloud +;; lon/lat: center in degrees (lon 0..360 wraps, lat -90..90) +;; width/height: size in degrees of longitude, across and along the cloud +;; (full width at half brightness); angle: rotation in degrees +;; brightness: peak, 0..1 (overlapping clouds add up, capped at 1) +;; +Density: background: fraction of the sky with bright patches +;; +Frequency: noise frequency around and along the sphere (unequal values give streaks) +;; +Octaves: noise detail layers; fewer gives broader, smoother masses +;; +Warp: how much the noise is swirled, stretching it into wisps +;; +Contrast: background falloff curve for the bright patches +;; +Intensity: overall brightness multiplier (through a palette file, colors stop at its last entry) +;; +Seed: noise seed, giving each pattern its own variation +;; +Resolution: sphere mesh resolution (around, along) +;; +Band: range of the sphere covered, 0..1 from pole to pole +;; +;; The built-in patterns are clouds only, laid out to match the FS1 originals. + +$Name: Nebula01 ++Mesh: nebula01 ++Palette Prefix: gamepalette1 ++Clouds Only: YES ++Cloud Detail: 0.35 ++Cloud: 222.7 -14.5 70.8 11.0 164 0.73 ++Cloud: 136.6 -48.5 41.1 8.7 4 0.55 ++Cloud: 217.5 36.7 60.6 10.4 6 0.46 ++Cloud: 255.5 -4.0 44.3 22.1 155 0.87 ++Cloud: 291.1 4.2 56.7 9.3 2 0.53 ++Cloud: 160.1 -2.3 36.2 18.5 176 0.75 ++Cloud: 93.4 2.4 53.0 18.7 8 0.42 ++Cloud: 313.9 -8.7 46.5 8.7 13 0.39 ++Cloud: 288.0 42.2 33.6 12.4 175 0.54 ++Cloud: 188.6 -13.5 37.5 8.7 159 0.53 ++Cloud: 110.0 -9.7 48.1 13.5 167 0.72 ++Cloud: 109.6 -47.1 22.5 11.3 14 0.35 ++Cloud: 274.3 -23.7 35.8 17.3 13 0.26 ++Cloud: 152.6 -42.3 9.1 10.7 126 0.27 ++Cloud: 228.5 2.6 36.6 14.0 10 0.29 ++Cloud: 153.5 -9.2 34.0 10.4 4 0.37 ++Cloud: 83.0 -11.5 33.6 10.0 8 0.31 ++Cloud: 163.0 -47.2 34.9 6.0 14 0.24 ++Frequency: 5 6 ++Warp: 0.40 ++Seed: 1 ++Resolution: 48 20 + +$Name: Nebula02 ++Mesh: nebula02 ++Palette Prefix: gamepalette2 ++Clouds Only: YES ++Cloud Detail: 0.35 ++Cloud: 319.0 16.8 37.5 21.3 16 0.41 ++Cloud: 85.4 -29.5 70.4 15.3 18 0.74 ++Cloud: 54.1 11.0 36.4 8.0 5 0.36 ++Cloud: 282.6 35.4 87.2 13.1 7 0.63 ++Cloud: 253.5 23.9 34.4 19.1 167 0.88 ++Cloud: 106.4 -15.1 46.4 12.7 1 0.61 ++Cloud: 253.3 -37.9 101.6 9.1 3 0.44 ++Cloud: 104.9 13.7 43.3 8.2 15 0.55 ++Cloud: 212.1 3.1 43.0 19.9 14 0.50 ++Cloud: 215.3 32.6 40.2 7.8 170 0.44 ++Cloud: 67.4 16.9 24.7 18.8 7 0.47 ++Cloud: 173.1 -2.7 40.2 11.8 34 0.64 ++Cloud: 316.3 -30.5 27.2 14.9 15 0.28 ++Cloud: 260.4 5.1 70.9 12.7 170 0.59 ++Cloud: 284.0 12.2 41.7 8.5 171 0.61 ++Cloud: 176.9 -7.1 51.9 4.7 27 0.39 ++Cloud: 50.3 24.9 25.2 7.8 127 0.33 ++Cloud: 98.1 -32.5 11.7 20.8 85 0.29 ++Frequency: 6 5 ++Warp: 0.50 ++Seed: 2 ++Resolution: 48 20 + +$Name: Nebula03 ++Mesh: nebula03 ++Palette Prefix: gamepalette3 ++Clouds Only: YES ++Cloud Detail: 0.35 ++Cloud: 257.6 31.6 33.8 18.2 14 0.85 ++Cloud: 123.8 -11.5 62.7 10.9 13 0.67 ++Cloud: 106.1 14.6 42.9 9.0 1 0.63 ++Cloud: 323.7 13.7 34.7 19.6 9 0.54 ++Cloud: 121.3 39.9 45.5 12.7 1 0.47 ++Cloud: 72.6 -46.4 55.4 12.2 177 0.86 ++Cloud: 100.6 -17.8 67.1 15.0 173 0.48 ++Cloud: 239.8 -38.1 64.7 9.3 2 0.38 ++Cloud: 301.4 35.2 56.2 11.4 5 0.73 ++Cloud: 98.4 -37.4 32.0 18.2 145 0.57 ++Cloud: 126.0 -53.5 37.8 6.9 4 0.31 ++Cloud: 52.9 17.8 37.1 7.4 174 0.52 ++Cloud: 259.1 22.1 42.8 10.0 1 0.65 ++Cloud: 67.1 17.5 33.8 8.5 16 0.37 ++Cloud: 172.0 40.6 39.5 12.9 6 0.64 ++Cloud: 126.4 -24.3 57.2 10.9 9 0.31 ++Cloud: 306.4 6.7 35.8 10.7 0 0.43 ++Cloud: 301.1 -33.1 38.6 8.9 1 0.28 ++Frequency: 5 7 ++Warp: 0.60 ++Seed: 3 ++Resolution: 48 20 + +#End diff --git a/code/lab/renderer/lab_renderer.cpp b/code/lab/renderer/lab_renderer.cpp index eec2c2d1813..a3218877145 100644 --- a/code/lab/renderer/lab_renderer.cpp +++ b/code/lab/renderer/lab_renderer.cpp @@ -380,13 +380,15 @@ void LabRenderer::useBackground(const SCP_string& mission_name) { int skybox_flags; int ambient_light_level; - extern const char* Neb2_filenames[]; char envmap_name[MAX_FILENAME_LEN] = {0}; SCP_string ltp_name; currentMissionBackground = mission_name; stars_pre_level_init(true); + nebula_close(); + Nebula_index = -1; + Nebula_mission_switch = -1; vm_set_identity(&skybox_orientation); // (DahBlount) - Remember to load the debris anims @@ -507,43 +509,26 @@ void LabRenderer::useBackground(const SCP_string& mission_name) { } else { Nebula_index = -1; + Mission_palette = std::max(generated_nebula_default_color(), 0); + Nebula_mission_switch = -1; if (optional_string("+Nebula:")) { char str[MAX_FILENAME_LEN]; - int z; stuff_string(str, F_NAME, MAX_FILENAME_LEN); - // parse the proper nebula type (full or not) - for (z = 0; z < NUM_NEBULAS; z++) { - if (flags[Mission::Mission_Flags::Fullneb]) { - if (!stricmp(str, Neb2_filenames[z])) { - Nebula_index = z; - break; - } - } - else { - if (!stricmp(str, Nebula_filenames[z])) { - Nebula_index = z; - break; - } - } - } - - if (z == NUM_NEBULAS) + // look up the generated nebula pattern in the registry + Nebula_index = generated_nebula_pattern_lookup(str); + if (Nebula_index < 0) WarningEx(LOCATION, "Unknown nebula %s!", str); if (optional_string("+Color:")) { stuff_string(str, F_NAME, MAX_FILENAME_LEN); - for (z = 0; z < NUM_NEBULA_COLORS; z++) { - if (!stricmp(str, Nebula_colors[z])) { - Mission_palette = z; - break; - } - } + int color_idx = generated_nebula_color_lookup(str); + if (color_idx < 0) + WarningEx(LOCATION, "Unknown nebula color %s!", str); + else + Mission_palette = color_idx; } - if (z == NUM_NEBULA_COLORS) - WarningEx(LOCATION, "Unknown nebula color %s!", str); - if (optional_string("+Pitch:")) { stuff_int(&Nebula_pitch); } @@ -564,6 +549,7 @@ void LabRenderer::useBackground(const SCP_string& mission_name) { else { Nebula_heading = 0; } + } nebula_init(Nebula_index, Nebula_pitch, Nebula_bank, Nebula_heading); @@ -584,6 +570,12 @@ void LabRenderer::useBackground(const SCP_string& mission_name) { stuff_string(envmap_name, F_NAME, MAX_FILENAME_LEN); } + if (optional_string("$FS1 Nebula:")) { + bool on = false; + stuff_boolean(&on); + Nebula_mission_switch = on ? 1 : 0; + } + const int size = 512; int gen_flags = (BMP_FLAG_RENDER_TARGET_STATIC | BMP_FLAG_CUBEMAP | BMP_FLAG_RENDER_TARGET_MIPMAP); diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index cccd93941ea..7c17569a5aa 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -105,8 +105,8 @@ int Total_initially_docked; mission The_mission; char Mission_filename[80]; -int Mission_palette; // index into Nebula_palette_filenames[] of palette file to use for mission -int Nebula_index; // index into Nebula_filenames[] of nebula to use in mission. +int Mission_palette; // index into Generated_nebula_colors of the generated nebula tint color +int Nebula_index; // index into Generated_nebula_patterns of the generated nebula to use in mission (-1 = none) int Num_ai_behaviors = MAX_AI_BEHAVIORS; int Num_cargo = 0; int Num_arrival_names = MAX_ARRIVAL_NAMES; @@ -222,31 +222,6 @@ static bool mission_has_layer_name(const mission* pm, const SCP_string& layerNam //XSTR:OFF -const char *Nebula_filenames[NUM_NEBULAS] = { - "Nebula01", - "Nebula02", - "Nebula03" -}; - -const char *Neb2_filenames[NUM_NEBULAS] = { - "Nebfull01", - "Nebfull02", - "Nebfull03" -}; - -// Note: Nebula_colors[] and Nebula_palette_filenames are linked via index numbers -const char *Nebula_colors[NUM_NEBULA_COLORS] = { - "Red", - "Blue", - "Gold", - "Purple", - "Maroon", - "Green", - "Grey blue", - "Violet", - "Grey Green", -}; - const char *Ai_behavior_names[MAX_AI_BEHAVIORS] = { "Chase", "Evade", @@ -6337,7 +6312,6 @@ void parse_one_background(background_t *background) void parse_bitmaps(mission *pm) { char str[MAX_FILENAME_LEN]; - int z; required_string("#Background bitmaps"); @@ -6403,38 +6377,26 @@ void parse_bitmaps(mission *pm) } else { if (optional_string("+Nebula:")) { stuff_string(str, F_NAME, MAX_FILENAME_LEN); - - // parse the proper nebula type (full or not) - for (z=0; zflags[Mission::Mission_Flags::Fullneb]){ - if (!stricmp(str, Neb2_filenames[z])) { - Nebula_index = z; - break; - } - } else { - if (!stricmp(str, Nebula_filenames[z])) { - Nebula_index = z; - break; - } - } - } - if (z == NUM_NEBULAS) + // look up the generated nebula pattern in the registry; an unknown name is kept so a + // save writes it back unchanged + Nebula_index = generated_nebula_pattern_lookup(str); + if (Nebula_index < 0) { WarningEx(LOCATION, "Mission %s\nUnknown nebula %s!", pm->name.c_str(), str); + Nebula_unknown_pattern = str; + } if (optional_string("+Color:")) { stuff_string(str, F_NAME, MAX_FILENAME_LEN); - for (z=0; zname.c_str(), str); + Nebula_unknown_color = str; + } else { + Mission_palette = color_idx; } } - if (z == NUM_NEBULA_COLORS) - WarningEx(LOCATION, "Mission %s\nUnknown nebula color %s!", pm->name.c_str(), str); - if (optional_string("+Pitch:")){ stuff_int(&Nebula_pitch); } else { @@ -6451,7 +6413,7 @@ void parse_bitmaps(mission *pm) stuff_int(&Nebula_heading); } else { Nebula_heading = 0; - } + } } nebula_init(Nebula_index, Nebula_pitch, Nebula_bank, Nebula_heading); @@ -6468,6 +6430,12 @@ void parse_bitmaps(mission *pm) stuff_string(pm->envmap_name, F_NAME, MAX_FILENAME_LEN); } + if (optional_string("$FS1 Nebula:")) { + bool on = false; + stuff_boolean(&on); + Nebula_mission_switch = on ? 1 : 0; + } + // bypass spurious stuff from e.g. FS1 missions skip_to_start_of_string("#"); } @@ -7501,7 +7469,10 @@ void mission_init(mission *pm, bool quick_init) Nebula_bank = (int)((float)(Random::next() & 0x0fff) * 360.0f / 4096.0f); Nebula_heading = (int)((float)(Random::next() & 0x0fff) * 360.0f / 4096.0f); Nebula_index = -1; - Mission_palette = 1; + Mission_palette = std::max(generated_nebula_default_color(), 0); + Nebula_mission_switch = -1; + Nebula_unknown_pattern.clear(); + Nebula_unknown_color.clear(); } // Main parse routine for parsing a mission. The default parameter flags tells us which information diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index bc6765b6b4f..3af67725337 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -38,9 +38,6 @@ struct p_dock_instance; enum class ArrivalLocation; enum class DepartureLocation; -#define NUM_NEBULAS 3 // how many background nebulas we have altogether -#define NUM_NEBULA_COLORS 9 - #define DEFAULT_AMBIENT_LIGHT_LEVEL 0x00787878 #define MIN_TARGET_ARRIVAL_DISTANCE 500.0f // float because that's how FRED does the math @@ -605,10 +602,8 @@ extern char Player_start_shipname[NAME_LENGTH]; extern int Player_start_shipnum; extern p_object *Player_start_pobject; -extern int Mission_palette; // index of palette file to use for mission -extern int Nebula_index; // index into Nebula_filenames[] of nebula to use in mission. -extern const char *Nebula_filenames[NUM_NEBULAS]; -extern const char *Nebula_colors[NUM_NEBULA_COLORS]; +extern int Mission_palette; // index into Generated_nebula_colors of the generated nebula tint color +extern int Nebula_index; // index into Generated_nebula_patterns of the generated nebula to use in mission (-1 = none) extern p_object *Arriving_support_ship; extern char Neb2_texture_name[MAX_FILENAME_LEN]; diff --git a/code/missioneditor/missionsave.cpp b/code/missioneditor/missionsave.cpp index 6f57eba347f..774486be63b 100644 --- a/code/missioneditor/missionsave.cpp +++ b/code/missioneditor/missionsave.cpp @@ -954,17 +954,27 @@ int Fred_mission_save::save_bitmaps() } // neb 1 stuff else { - if (Nebula_index >= 0) { + // a name the tables don't know is written back as it was loaded + if (Nebula_index >= 0 || !Nebula_unknown_pattern.empty()) { if (optional_string_fred("+Nebula:")) { parse_comments(); } else { fout("\n+Nebula:"); } - fout(" %s", Nebula_filenames[Nebula_index]); + fout(" %s", Nebula_index >= 0 ? generated_nebula_pattern_name(Nebula_index) : Nebula_unknown_pattern.c_str()); + + // +Color: is required, so never write it empty + const char* color_name = generated_nebula_color_name(Mission_palette); + if (!Nebula_unknown_color.empty()) + color_name = Nebula_unknown_color.c_str(); + else if (*color_name == '\0') + color_name = generated_nebula_color_name(generated_nebula_default_color()); + if (*color_name == '\0') + color_name = "Blue"; required_string_fred("+Color:"); parse_comments(); - fout(" %s", Nebula_colors[Mission_palette]); + fout(" %s", color_name); required_string_fred("+Pitch:"); parse_comments(); @@ -977,6 +987,7 @@ int Fred_mission_save::save_bitmaps() required_string_fred("+Heading:"); parse_comments(); fout(" %d", Nebula_heading); + } } @@ -1089,6 +1100,21 @@ int Fred_mission_save::save_bitmaps() bypass_comment(";;FSO 3.6.9;; $Environment Map:"); } + if (Nebula_mission_switch >= 0 && (Nebula_index >= 0 || !Nebula_unknown_pattern.empty()) && + !The_mission.flags[Mission::Mission_Flags::Fullneb]) { + fso_comment_push(";;FSO 26.1.0;;"); + // bypass_comment() edits only the raw text, so the parsed text can still hold a line the raw text lost + if (strstr(raw_ptr, "$FS1 Nebula:") != nullptr && optional_string_fred("$FS1 Nebula:")) { + parse_comments(2); + fout(" %s", Nebula_mission_switch == 1 ? "YES" : "NO"); + } else { + fout_version("\n\n$FS1 Nebula: %s", Nebula_mission_switch == 1 ? "YES" : "NO"); + } + fso_comment_pop(); + } else { + bypass_comment(";;FSO 26.1.0;; $FS1 Nebula:", "#Asteroid Fields"); + } + fso_comment_pop(true); return err; diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index 2ca750af08c..73ed0b83a7c 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -29,6 +29,7 @@ int Directive_wait_time; bool True_loop_argument_sexps; bool Skybox_internal_depth_consistency; +bool Fs1_nebula_backgrounds; bool Fixed_turret_collisions; bool Fixed_missile_detonation; bool Damage_impacted_subsystem_first; @@ -979,6 +980,10 @@ void parse_mod_table(const char *filename) stuff_boolean(&Render_insignias_as_decals); } + if (optional_string("$FS1 nebula backgrounds:")) { + stuff_boolean(&Fs1_nebula_backgrounds); + } + optional_string("#NETWORK SETTINGS"); if (optional_string("$FS2NetD port:")) { @@ -1677,6 +1682,9 @@ static void mod_table_log_settings() else mprintf(("Game Settings Table: Skybox submodels will not use internal depth consistency (retail behavior)\n")); + if (Fs1_nebula_backgrounds) + mprintf(("Game Settings Table: FS1 nebula backgrounds will be drawn for missions that name one\n")); + if (Fixed_turret_collisions) mprintf(("Game Settings Table: Using fixed turret collisions (shooting a turret barrel will always register)\n")); else @@ -1865,6 +1873,7 @@ void mod_table_reset() Directive_wait_time = 3000; True_loop_argument_sexps = false; Skybox_internal_depth_consistency = false; + Fs1_nebula_backgrounds = false; Fixed_turret_collisions = false; Fixed_missile_detonation = false; Damage_impacted_subsystem_first = false; diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index d502f96098b..2bcc8b3f47b 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -49,6 +49,7 @@ struct splash_screen { extern int Directive_wait_time; extern bool True_loop_argument_sexps; extern bool Skybox_internal_depth_consistency; +extern bool Fs1_nebula_backgrounds; extern bool Fixed_turret_collisions; extern bool Fixed_missile_detonation; extern bool Damage_impacted_subsystem_first; diff --git a/code/nebula/neb.cpp b/code/nebula/neb.cpp index b7df5beff0c..0a455eb0147 100644 --- a/code/nebula/neb.cpp +++ b/code/nebula/neb.cpp @@ -25,6 +25,7 @@ #include "render/3d.h" #include "render/batching.h" #include "ship/ship.h" +#include "starfield/nebula.h" #include "starfield/starfield.h" #include "tgautils/tgautils.h" #include "tracing/tracing.h" @@ -348,6 +349,9 @@ void parse_nebula_table(const char* filename) // initialize neb2 stuff at game startup void neb2_init() { + // the generated (FS1-style) background nebula's patterns and colors: generated_nebula.tbl + generated_nebula_init(); + // first parse the default table parse_nebula_table("nebula.tbl"); diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 04734bca905..20b7459f5b5 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -314,6 +314,7 @@ add_file_folder("Default files\\\\data\\\\tables" def_files/data/tables/post_processing.tbl def_files/data/tables/species_defs.tbl def_files/data/tables/cheats.tbl + def_files/data/tables/generated_nebula.tbl ) # These files will be included in the executable but not in CFile diff --git a/code/starfield/nebula.cpp b/code/starfield/nebula.cpp index c2718799fd8..ffb5ef06103 100644 --- a/code/starfield/nebula.cpp +++ b/code/starfield/nebula.cpp @@ -1,65 +1,329 @@ /* * Copyright (C) Volition, Inc. 1999. All rights reserved. * - * All source code herein is the property of Volition, Inc. You may not sell - * or otherwise commercially exploit the source or things you created based on the + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the * source. * */ +#include +#include + +#include "bmpman/bmpman.h" #include "cfile/cfile.h" -#include "debugconsole/console.h" +#include "def_files/def_files.h" +#include "globalincs/systemvars.h" +#include "graphics/2d.h" +#include "graphics/material.h" +#include "graphics/matrix.h" +#include "lighting/lighting_profiles.h" #include "math/vecmat.h" #include "mission/missionparse.h" -#include "nebula/neb.h" -#include "graphics/material.h" +#include "mod_table/mod_table.h" +#include "parse/parselo.h" +#include "pcxutils/pcxutils.h" #include "render/3d.h" #include "starfield/nebula.h" -#define MAX_TRIS 200 -#define MAX_POINTS 300 +// distance the background nebula sphere sits at; large so it reads as a faraway backdrop +#define NEBULA_RADIUS 1000.0f + +// ---------------------------------------------------------------------------------------------------- +// data-driven pattern / color registries +// ---------------------------------------------------------------------------------------------------- + +SCP_vector Generated_nebula_patterns; +SCP_vector Generated_nebula_colors; + +int generated_nebula_pattern_lookup(const char *name) +{ + for (int i = 0; i < static_cast(Generated_nebula_patterns.size()); i++) { + if (!stricmp(Generated_nebula_patterns[i].name.c_str(), name)) + return i; + } + return -1; +} + +int generated_nebula_color_lookup(const char *name) +{ + for (int i = 0; i < static_cast(Generated_nebula_colors.size()); i++) { + if (!stricmp(Generated_nebula_colors[i].name.c_str(), name)) + return i; + } + return -1; +} + +const char *generated_nebula_pattern_name(int index) +{ + if (index < 0 || index >= static_cast(Generated_nebula_patterns.size())) + return ""; + return Generated_nebula_patterns[index].name.c_str(); +} + +int generated_nebula_default_color() +{ + const int blue = generated_nebula_color_lookup("Blue"); + if (blue >= 0) + return blue; + return Generated_nebula_colors.empty() ? -1 : 0; +} + +const char *generated_nebula_color_name(int index) +{ + if (index < 0 || index >= static_cast(Generated_nebula_colors.size())) + return ""; + return Generated_nebula_colors[index].name.c_str(); +} -static int num_pts = 0; +namespace { -static vec3d nebula_vecs[MAX_POINTS]; -static vertex nebula_verts[MAX_POINTS]; +constexpr const char *GENERATED_NEBULA_TABLE = "generated_nebula.tbl"; +constexpr const char *GENERATED_NEBULA_MODULAR_GLOB = "*-gneb.tbm"; -static float scale_factor = 1.0f; +void parse_generated_nebula_colors() +{ + while (optional_string("$Name:")) { + SCP_string nm; + stuff_string(nm, F_NAME); + + // a name already defined (by the base table or an earlier .tbm) is overridden in place + int idx = generated_nebula_color_lookup(nm.c_str()); + generated_nebula_color *c; + if (idx < 0) { + Generated_nebula_colors.emplace_back(); + c = &Generated_nebula_colors.back(); + c->name = nm; + } else { + c = &Generated_nebula_colors[idx]; + } + + if (optional_string("+RGB:")) { + int rgb[3]; + parse_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + c->r = static_cast(rgb[0]); + c->g = static_cast(rgb[1]); + c->b = static_cast(rgb[2]); + } + if (optional_string("+Palette Suffix:")) + stuff_string(c->palette_suffix, F_NAME); + } +} -static int num_tris = 0; -static int tri[MAX_TRIS][3]; +void parse_generated_nebula_patterns() +{ + while (optional_string("$Name:")) { + SCP_string nm; + stuff_string(nm, F_NAME); + + // a name already defined (by the base table or an earlier .tbm) is overridden in place + int idx = generated_nebula_pattern_lookup(nm.c_str()); + generated_nebula_pattern *p; + if (idx < 0) { + Generated_nebula_patterns.emplace_back(); + p = &Generated_nebula_patterns.back(); + p->name = nm; + } else { + p = &Generated_nebula_patterns[idx]; + } + + if (optional_string("+Mesh:")) + stuff_string(p->mesh, F_NAME); + if (optional_string("+Palette Prefix:")) + stuff_string(p->palette_prefix, F_NAME); + if (optional_string("+Clouds Only:")) + stuff_boolean(&p->clouds_only); + if (optional_string("+Cloud Detail:")) { + stuff_float(&p->cloud_detail); + CLAMP(p->cloud_detail, 0.0f, 1.0f); + } + // any +Cloud entries replace the pattern's whole cloud list + bool first_cloud = true; + while (optional_string("+Cloud:")) { + float c[6]; + parse_float_list(c, 6); + if (first_cloud) { + p->clouds.clear(); + first_cloud = false; + } + generated_nebula_cloud cloud; + cloud.lon = c[0]; + cloud.lat = c[1]; + CLAMP(cloud.lat, -90.0f, 90.0f); + cloud.width = std::max(c[2], 0.1f); + cloud.height = std::max(c[3], 0.1f); + cloud.angle = c[4]; + cloud.brightness = std::max(c[5], 0.0f); + p->clouds.push_back(cloud); + } + if (optional_string("+Density:")) + stuff_float(&p->density); + if (optional_string("+Frequency:")) { + float f[2]; + parse_float_list(f, 2); + p->freq_u = f[0]; + p->freq_v = f[1]; + CLAMP(p->freq_u, 0.1f, 64.0f); + CLAMP(p->freq_v, 0.1f, 64.0f); + } + if (optional_string("+Octaves:")) { + stuff_int(&p->octaves); + CLAMP(p->octaves, 1, 8); + } + if (optional_string("+Warp:")) { + stuff_float(&p->warp); + CLAMP(p->warp, 0.0f, 8.0f); + } + if (optional_string("+Contrast:")) { + stuff_float(&p->contrast); + CLAMP(p->contrast, 0.05f, 10.0f); + } + if (optional_string("+Intensity:")) { + stuff_float(&p->intensity); + p->intensity = std::max(p->intensity, 0.0f); + } + if (optional_string("+Seed:")) { + stuff_int(&p->seed); + CLAMP(p->seed, -1000000, 1000000); + } + if (optional_string("+Resolution:")) { + int r[2]; + parse_int_list(r, 2); + p->res_lon = r[0]; + p->res_lat = r[1]; + } + if (optional_string("+Band:")) { + float b[2]; + parse_float_list(b, 2); + p->band_min = b[0]; + p->band_max = b[1]; + } + } +} + +// generated_nebula.tbl or a *-gneb.tbm (filename), or the built-in default (nullptr). Both +// sections are optional, so a .tbm can carry just colors or just patterns. +void parse_generated_nebula_table(const char *filename) +{ + try { + if (filename != nullptr) + read_file_text(filename, CF_TYPE_TABLES); + else + read_file_text_from_default(defaults_get_file(GENERATED_NEBULA_TABLE)); + reset_parse(); + + // either section may come first, and either may be left out + while (true) { + if (optional_string("#Generated Nebula Colors")) { + parse_generated_nebula_colors(); + required_string("#End"); + } else if (optional_string("#Generated Nebula Patterns")) { + parse_generated_nebula_patterns(); + required_string("#End"); + } else { + break; + } + } + if (!check_for_eof()) + error_display(0, "Unexpected text in %s; expected #Generated Nebula Colors or #Generated Nebula Patterns.", + filename != nullptr ? filename : GENERATED_NEBULA_TABLE); + } catch (const parse::ParseException &e) { + mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", + filename != nullptr ? filename : GENERATED_NEBULA_TABLE, e.what())); + } +} + +} // namespace + +void generated_nebula_init() +{ + nebula_close(); + Generated_nebula_patterns.clear(); + Generated_nebula_colors.clear(); + + // The base table holds the built-in FS1 patterns and colors (a mod's own generated_nebula.tbl + // replaces it), then *-gneb.tbm files add to it and override entries by name + if (cf_exists_full(GENERATED_NEBULA_TABLE, CF_TYPE_TABLES)) + parse_generated_nebula_table(GENERATED_NEBULA_TABLE); + else + parse_generated_nebula_table(nullptr); + + parse_modular_table(GENERATED_NEBULA_MODULAR_GLOB, parse_generated_nebula_table); +} + +// ---------------------------------------------------------------------------------------------------- +// procedural mesh generation + rendering +// ---------------------------------------------------------------------------------------------------- + +static vertex *Nebula_verts = nullptr; +static int Nebula_n_verts = 0; + +// the generated path's equirectangular bake, or the mesh path's 20-entry ramp (which the vertex colors +// are read from). Nebula_tex_data is our own buffer; bm_create() keeps the pointer without copying, +// so we hold it until bm_release + delete[]. +static ubyte *Nebula_tex_data = nullptr; +static int Nebula_bitmap = -1; +// the pattern and color index the texture was baked from, so an orientation change can keep it +static int Nebula_baked_pattern = -1; +static int Nebula_baked_color = -1; +static bool Nebula_baked_mesh = false; +// the mesh path's palette color per vertex (BGR), and the post-processing state the vertex colors were +// made for +static SCP_vector Nebula_mesh_colors; +static int Nebula_mesh_colors_tonemapper = -2; +static float Nebula_mesh_colors_exposure = 0.0f; +static int Nebula_mesh_warned_tonemapper = -1; -static int Nebula_loaded = 0; -static angles Nebula_pbh; static matrix Nebula_orient; int Nebula_pitch; int Nebula_bank; int Nebula_heading; -void nebula_close() -{ - if (!Nebula_loaded) return; +int Nebula_mission_switch = -1; - Nebula_loaded = 0; +bool nebula_backgrounds_enabled() +{ + if (Nebula_mission_switch >= 0) + return Nebula_mission_switch == 1; + return Fs1_nebula_backgrounds; } +SCP_string Nebula_unknown_pattern; +SCP_string Nebula_unknown_color; -#define NEBULA_FILE_ID NOX("NEBU") -#define NEBULA_MAJOR_VERSION 1 // Can be 1-? -#define NEBULA_MINOR_VERSION 0 // Can be 0-99 +void nebula_close() +{ + delete[] Nebula_verts; + Nebula_verts = nullptr; + Nebula_n_verts = 0; -// given: -// u,v in range 0-1 + if (Nebula_bitmap >= 0) { + bm_release(Nebula_bitmap); + Nebula_bitmap = -1; + } + delete[] Nebula_tex_data; + Nebula_tex_data = nullptr; + Nebula_baked_pattern = -1; + Nebula_baked_color = -1; + Nebula_baked_mesh = false; + Nebula_mesh_colors.clear(); + Nebula_mesh_colors_tonemapper = -2; + Nebula_mesh_warned_tonemapper = -1; +} -void project_2d_onto_sphere( vec3d *pnt, float u, float v ) +// classic 2d -> sphere projection. u,v in range 0..1. +static void project_2d_onto_sphere(vec3d *pnt, float u, float v) { - float a,x,y,z,s; + float a, x, y, z, s; - a = PI * (2.0f * u - 1.0f ); - z = 2.0f * v - 1.0f; - s = scale_factor * fl_sqrt( 1.0f - z*z ); + a = PI * (2.0f * u - 1.0f); + z = 2.0f * v - 1.0f; + s = fl_sqrt(1.0f - z * z); x = s * cosf(a); y = s * sinf(a); pnt->xyz.x = x; @@ -67,122 +331,617 @@ void project_2d_onto_sphere( vec3d *pnt, float u, float v ) pnt->xyz.z = z; } -// Version 199 mean major version=1, minor=99. -// Changing major means no longer compatible. -// Revision history: -// 1.00 - initial version +// value noise of its own rather than ANL (nebula/volumetrics.cpp): the bake wants an exact period across the longitude +// seam and seed-stable output, which this gives in a few lines without building a kernel -// returns 0 if failed -int load_nebula_sub(const char *filename) +static uint32_t neb_hash(uint32_t x) { - CFILE *fp; - char id[16]; - int version, major; + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x; +} - fp = cfopen(filename, "rb"); +static float neb_hash2(int ix, int iy, int seed) +{ + uint32_t h = neb_hash((static_cast(ix) * 73856093U) ^ (static_cast(iy) * 19349663U) ^ + (static_cast(seed) * 83492791U)); + return (h & 0xffffffU) / static_cast(0x1000000); +} - if ( !fp ) { - return 0; - } +static float neb_smooth(float t) +{ + return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); +} + +// value noise that is periodic in x with period xperiod (so the longitude seam matches) +static float neb_vnoise(float x, float y, int seed, int xperiod) +{ + int ix = static_cast(floorf(x)); + int iy = static_cast(floorf(y)); + float fx = x - ix; + float fy = y - iy; + + auto wrap = [xperiod](int i) { + if (xperiod > 0) { + i %= xperiod; + if (i < 0) + i += xperiod; + } + return i; + }; + + float h00 = neb_hash2(wrap(ix), iy, seed); + float h10 = neb_hash2(wrap(ix + 1), iy, seed); + float h01 = neb_hash2(wrap(ix), iy + 1, seed); + float h11 = neb_hash2(wrap(ix + 1), iy + 1, seed); + + float u = neb_smooth(fx); + float v = neb_smooth(fy); + + return (((h00 * (1.0f - u)) + (h10 * u)) * (1.0f - v)) + (((h01 * (1.0f - u)) + (h11 * u)) * v); +} + +// multi-octave, anisotropic, longitude-seamless fbm in 0..1 +static float neb_fbm(float lon, float lat, const generated_nebula_pattern &p) +{ + const int base = std::max(static_cast(std::lround(p.freq_u)), 1); - // ID of NEBU - cfread( id, 4, 1, fp ); - if ( strncmp( id, NEBULA_FILE_ID, 4) != 0) { - mprintf(( "Not a valid nebula file.\n" )); - return 0; - } - cfread( &version, sizeof(int), 1, fp ); - major = version / 100; + // low-frequency directional warp to stretch the wisps + float w = (neb_vnoise(lon * base, lat * p.freq_v, p.seed + 777, base) - 0.5f) * p.warp; - if ( major != NEBULA_MAJOR_VERSION ) { - mprintf(( "An out of date nebula file.\n" )); - return 0; - } + int octaves = p.octaves; + CLAMP(octaves, 1, 8); - cfread( &num_pts, sizeof(int), 1, fp ); - Assert( num_pts < MAX_POINTS ); - cfread( &num_tris, sizeof(int), 1, fp ); - Assert( num_tris < MAX_TRIS ); + float sum = 0.0f, amp = 0.5f, norm = 0.0f; + for (int o = 0; o < octaves; o++) { + int per = base << o; + float n = neb_vnoise((lon + w) * per, lat * p.freq_v * static_cast(1 << o), p.seed + o, per); + sum += amp * n; + norm += amp; + amp *= 0.5f; + } - // Cyborg - can't load nonsense. - if (num_pts <= 0 || num_pts >= MAX_POINTS || num_tris <= 0 || num_tris >= MAX_TRIS) - return 0; + return (norm > 0.0f) ? (sum / norm) : 0.0f; +} - int i; - for (i=0; i nebula_prepare_clouds(const generated_nebula_pattern &p) +{ + SCP_vector out; + out.reserve(p.clouds.size()); + // width/height are full widths at half brightness: sigma = width / (2 * sqrt(2 * ln 2)) + constexpr float fwhm_to_sigma = 1.0f / 2.3548f; + for (const auto &c : p.clouds) { + nebula_cloud_eval e; + e.x = fmodf(c.lon, 360.0f); + if (e.x < 0.0f) + e.x += 360.0f; + // the map's other axis is linear in sin(latitude), like the FS1 meshes + e.y = (sinf(fl_radians(c.lat)) + 1.0f) * 90.0f; + const float a = fl_radians(c.angle); + e.cos_a = cosf(a); + e.sin_a = sinf(a); + const float sx = c.width * fwhm_to_sigma; + const float sy = c.height * fwhm_to_sigma; + e.inv_wx2 = 0.5f / (sx * sx); + e.inv_wy2 = 0.5f / (sy * sy); + e.brightness = c.brightness; + e.reach = 3.5f * std::max(sx, sy); + out.push_back(e); + } + return out; +} - // throw in some randomness to the nebula vertices depth +// brightness 0..1 of the whole pattern for a texel: the procedural background (unless clouds +// only) screened with the placed clouds, which the noise breaks up per +Cloud Detail +static float nebula_brightness(float lon, float lat, const generated_nebula_pattern &p, + const SCP_vector &clouds) +{ + // the noise is shared by the background and the cloud breakup, so compute it at most once + float noise = -1.0f; + auto get_noise = [&]() { + if (noise < 0.0f) + noise = neb_fbm(lon, lat, p); + return noise; + }; + + const float bg = p.clouds_only ? 0.0f : nebula_background_brightness(get_noise(), lat, p); + if (clouds.empty()) + return bg; + + const float x = lon * 360.0f; + const float y = lat * 180.0f; + float c = 0.0f; + for (const auto &e : clouds) { + const float dy = y - e.y; + if (fabsf(dy) > e.reach) + continue; + float dx = x - e.x; + if (dx > 180.0f) + dx -= 360.0f; + else if (dx < -180.0f) + dx += 360.0f; + if (fabsf(dx) > e.reach) + continue; + const float xr = dx * e.cos_a + dy * e.sin_a; + const float yr = -dx * e.sin_a + dy * e.cos_a; + c += e.brightness * expf(-(xr * xr * e.inv_wx2 + yr * yr * e.inv_wy2)); } - for (i=0; i 0.0f && p.cloud_detail > 0.0f) { + // the noise averages about 0.5, so this keeps the clouds' overall brightness while + // carving them into brighter and darker wisps + c *= std::max(0.0f, 1.0f + (p.cloud_detail * ((2.0f * get_noise()) - 1.0f) * 2.0f)); } + c = std::min(c, 1.0f); - cfclose(fp); + return 1.0f - (1.0f - bg) * (1.0f - c); +} - return 1; +// dimensions of the baked equirectangular brightness texture +constexpr int NEBULA_TEX_W = 1024; +constexpr int NEBULA_TEX_H = 512; + +constexpr int NEBULA_RAMP_SIZE = 20; + +static ubyte nebula_chan(float c, float b) +{ + int v = static_cast(c * b); + CLAMP(v, 0, 255); + return static_cast(v); } -void nebula_init( int index, int pitch, int bank, int heading ) +// the FS1 palette named by the pattern's prefix and the color's suffix, if both are set and it loads +static bool nebula_read_palette(const generated_nebula_pattern &p, const generated_nebula_color &col, + ubyte palette[768]) { - angles a; + if (p.palette_prefix.empty() || col.palette_suffix.empty()) + return false; + SCP_string filename = p.palette_prefix + "-" + col.palette_suffix; + if (pcx_read_header(filename.c_str(), nullptr, nullptr, nullptr, nullptr, palette) == PCX_ERROR_NONE) + return true; + mprintf(("Background nebula palette %s.pcx could not be read; fading %s to black instead.\n", filename.c_str(), col.name.c_str())); + return false; +} - a.p = fl_radians(pitch); - a.b = fl_radians(bank); - a.h = fl_radians(heading); +// bake the procedural pattern into an equirectangular RGB texture, colored through the palette ramp +// when one is named, else tinted by the mission color. +// sampling per-texel here (rather than per-vertex on a coarse mesh) is what removes the triangle +// facets; the smooth brightness field + bilinear filtering give the soft, feathered FS1 look. +static void nebula_bake_texture(const generated_nebula_pattern &p, const generated_nebula_color &col) +{ + // nebula_init() closed the previous texture before baking, so these start fresh + Nebula_tex_data = new ubyte[NEBULA_TEX_W * NEBULA_TEX_H * 3]; + + const float intensity = std::max(p.intensity, 0.0f); + const auto clouds = nebula_prepare_clouds(p); + ubyte palette[768] = {}; + const bool from_file = nebula_read_palette(p, col, palette); + + ubyte *px = Nebula_tex_data; + for (int j = 0; j < NEBULA_TEX_H; j++) { + float v = (static_cast(j) + 0.5f) / static_cast(NEBULA_TEX_H); // latitude 0..1 + for (int i = 0; i < NEBULA_TEX_W; i++) { + float u = (static_cast(i) + 0.5f) / static_cast(NEBULA_TEX_W); // longitude 0..1 + float b = nebula_brightness(u, v, p, clouds) * intensity; + // 24-bit user bitmaps upload as GL_BGR, so store blue-green-red + if (from_file) { + // blended between ramp entries rather than rounded down as on the mesh path, which would band + const float pos = (b > 0.0f ? std::min(b, 1.0f) : 0.0f) * static_cast(NEBULA_RAMP_SIZE - 1); + const int lo = std::min(static_cast(pos), NEBULA_RAMP_SIZE - 2); + const float f = pos - static_cast(lo); + for (int c = 2; c >= 0; c--) { + *px++ = static_cast( + palette[lo * 3 + c] + (palette[(lo + 1) * 3 + c] - palette[lo * 3 + c]) * f + 0.5f); + } + } else { + *px++ = nebula_chan(col.b, b); + *px++ = nebula_chan(col.g, b); + *px++ = nebula_chan(col.r, b); + } + } + } - nebula_init(index < 0 ? nullptr : Nebula_filenames[index], &a); + // 24-bit (no alpha) so material_set_unlit() picks additive blending + Nebula_bitmap = bm_create(24, NEBULA_TEX_W, NEBULA_TEX_H, Nebula_tex_data, 0); } -void nebula_init( const char *filename, angles * pbh ) +// build the background sphere (positions + equirectangular UVs) into Nebula_verts. brightness and +// color live in the baked texture now, so the geometry only needs to be a smooth-enough sphere. +static void nebula_generate_sphere(const generated_nebula_pattern &p, const matrix *orient) { - if ( Nebula_loaded ) { - nebula_close(); + delete[] Nebula_verts; + Nebula_verts = nullptr; + Nebula_n_verts = 0; + + int rlon = p.res_lon; + int rlat = p.res_lat; + CLAMP(rlon, 2, 200); + CLAMP(rlat, 1, 100); + + float band_min = p.band_min; + float band_max = p.band_max; + CLAMP(band_min, 0.0f, 1.0f); + CLAMP(band_max, 0.0f, 1.0f); + if (band_max < band_min) + std::swap(band_max, band_min); + + int n_grid = (rlon + 1) * (rlat + 1); + SCP_vector gpt(n_grid); + SCP_vector guv(n_grid); + + for (int i = 0; i <= rlon; i++) { + float u = static_cast(i) / static_cast(rlon); // 0..1 longitude + for (int j = 0; j <= rlat; j++) { + float t = static_cast(j) / static_cast(rlat); + float v = band_min + (band_max - band_min) * t; // sphere v inside the band + + vec3d pt; + project_2d_onto_sphere(&pt, u, v); + vm_vec_scale(&pt, NEBULA_RADIUS); + vm_vec_unrotate(&pt, &pt, orient); + + int gi = i * (rlat + 1) + j; + gpt[gi] = pt; + guv[gi].u = u; + guv[gi].v = v; + } } - if ( filename && load_nebula_sub( cf_add_ext(filename, NOX(".neb")) ) ) { - Nebula_loaded = 1; + Nebula_n_verts = rlon * rlat * 6; + Nebula_verts = new vertex[Nebula_n_verts]; + + auto set_vert = [&](vertex *vt, int gi) { + g3_transfer_vertex(vt, &gpt[gi]); + vt->texture_position = guv[gi]; + vt->r = vt->g = vt->b = vt->a = 255; + }; + + int k = 0; + for (int i = 0; i < rlon; i++) { + for (int j = 0; j < rlat; j++) { + int g00 = i * (rlat + 1) + j; + int g10 = (i + 1) * (rlat + 1) + j; + int g11 = (i + 1) * (rlat + 1) + (j + 1); + int g01 = i * (rlat + 1) + (j + 1); + + set_vert(&Nebula_verts[k++], g00); + set_vert(&Nebula_verts[k++], g10); + set_vert(&Nebula_verts[k++], g11); + + set_vert(&Nebula_verts[k++], g00); + set_vert(&Nebula_verts[k++], g11); + set_vert(&Nebula_verts[k++], g01); + } } - if ( pbh ) { - Nebula_pbh = *pbh; - vm_angles_2_matrix(&Nebula_orient, &Nebula_pbh ); + Assertion(k == Nebula_n_verts, "generated nebula sphere vertex count mismatch (%d != %d)", k, Nebula_n_verts); +} - } else { - Nebula_pbh.p = 0.0f; - Nebula_pbh.b = 0.0f; - Nebula_pbh.h = 0.0f; - Nebula_orient = vmd_identity_matrix; +// The FS1 mesh path. A .neb file (NEBU, version 1xx) holds points on the same flat map the +// generated clouds use, each with a brightness level, and a triangle list; nebula01-03.neb ship in +// sparky_fs2.vp. Each corner takes the ramp entry its brightness rounds down to and those colors blend +// across the triangle (drawn in pieces, see NEBULA_MESH_SUBDIV), as the retail Glide and Direct3D +// renderers did (they blended in screen space; this blends with perspective). +constexpr int NEBULA_MESH_MAX_POINTS = 300; +constexpr int NEBULA_MESH_MAX_TRIS = 200; + +static bool nebula_load_mesh(const char *name, SCP_vector &pts, SCP_vector &levels, SCP_vector &tris) +{ + CFILE *fp = cfopen(cf_add_ext(name, NOX(".neb")), "rb"); + if (fp == nullptr) { + mprintf(("Background nebula mesh %s.neb could not be opened; drawing the generated pattern instead.\n", name)); + return false; + } + char id[4] = {}; + int version = 0, num_pts = 0, num_tris = 0; + const int header_reads = cfread(id, 4, 1, fp) + cfread(&version, sizeof(int), 1, fp) + + cfread(&num_pts, sizeof(int), 1, fp) + cfread(&num_tris, sizeof(int), 1, fp); + if (header_reads != 4 || strncmp(id, NOX("NEBU"), 4) != 0 || version / 100 != 1 || num_pts <= 0 || + num_pts >= NEBULA_MESH_MAX_POINTS || num_tris <= 0 || num_tris >= NEBULA_MESH_MAX_TRIS) { + mprintf(("Background nebula mesh %s.neb is not a valid version 1 nebula file.\n", name)); + cfclose(fp); + return false; + } + pts.resize(num_pts); + levels.resize(num_pts); + for (int i = 0; i < num_pts; i++) { + float xf = 0.0f, yf = 0.0f; + int l = 0; + const int point_reads = + cfread(&xf, sizeof(float), 1, fp) + cfread(&yf, sizeof(float), 1, fp) + cfread(&l, sizeof(int), 1, fp); + if (point_reads != 3 || fl_is_nan(xf) || fl_is_nan(yf) || xf < 0.0f || xf > 1.0f || yf < 0.0f || yf > 1.0f) { + mprintf(("Background nebula mesh %s.neb is truncated or has a point off the map.\n", name)); + cfclose(fp); + return false; + } + project_2d_onto_sphere(&pts[i], 1.0f - xf, yf); + CLAMP(l, 0, 31); + levels[i] = static_cast((l * 255) / 31); + } + tris.resize(num_tris * 3); + for (int i = 0; i < num_tris * 3; i++) { + if (cfread(&tris[i], sizeof(int), 1, fp) != 1 || tris[i] < 0 || tris[i] >= num_pts) { + mprintf(("Background nebula mesh %s.neb is truncated or has a triangle index out of range.\n", name)); + cfclose(fp); + return false; + } } + cfclose(fp); + return true; } -DCF(nebula,"Loads a different nebula") +// one row of NEBULA_RAMP_SIZE texels: the FS1 palette's first entries when the file exists, else an +// even fade from black to the color. Stored BGRA, as 32-bit user bitmaps upload. +static void nebula_make_ramp(const generated_nebula_pattern &p, const generated_nebula_color &col) { - SCP_string filename; + Nebula_tex_data = new ubyte[NEBULA_RAMP_SIZE * 4]; + ubyte palette[768] = {}; + const bool from_file = nebula_read_palette(p, col, palette); + for (int i = 0; i < NEBULA_RAMP_SIZE; i++) { + ubyte *px = &Nebula_tex_data[i * 4]; + if (from_file) { + px[0] = palette[i * 3 + 2]; + px[1] = palette[i * 3 + 1]; + px[2] = palette[i * 3 + 0]; + } else { + float f = static_cast(i) / static_cast(NEBULA_RAMP_SIZE - 1); + px[0] = nebula_chan(col.b, f); + px[1] = nebula_chan(col.g, f); + px[2] = nebula_chan(col.r, f); + } + px[3] = 255; + } + Nebula_bitmap = bm_create(32, NEBULA_RAMP_SIZE, 1, Nebula_tex_data, 0); +} - if (dc_optional_string_either("help", "--help")) { - dc_printf("Usage: nebula [filename]\n"); - dc_printf("Loads the nebula file (without filename extension). No filename takes away nebula\n" ); +// Each mesh triangle is drawn as NEBULA_MESH_SUBDIV^2 flat pieces whose corners carry the blend of the +// triangle's corner colors at that point. Without post-processing that draws the same picture to +// within byte rounding; with it, the compensation below holds at every piece's corners, and the +// curve's bend between them stays within about one step (it reached 15 across a whole triangle). +constexpr int NEBULA_MESH_SUBDIV = 8; + +// build Nebula_verts from the mesh, keeping each vertex's palette color for nebula_update_mesh_colors() +static void nebula_build_mesh_verts(const SCP_vector &pts, const SCP_vector &levels, const SCP_vector &tris, const matrix *orient) +{ + Assertion(Nebula_baked_mesh && Nebula_tex_data != nullptr, "the mesh path needs the palette ramp"); + constexpr int N = NEBULA_MESH_SUBDIV; + const int num_tris = static_cast(tris.size()) / 3; + delete[] Nebula_verts; + Nebula_n_verts = num_tris * N * N * 3; + Nebula_verts = new vertex[Nebula_n_verts]; + Nebula_mesh_colors.resize(Nebula_n_verts * 3); + + int k = 0; + for (int t = 0; t < num_tris; t++) { + vec3d corner[3]; + const ubyte *rgb[3]; + for (int c = 0; c < 3; c++) { + const int p = tris[t * 3 + c]; + corner[c] = pts[p]; + vm_vec_scale(&corner[c], NEBULA_RADIUS); + vm_vec_unrotate(&corner[c], &corner[c], orient); + rgb[c] = &Nebula_tex_data[((levels[p] * (NEBULA_RAMP_SIZE - 1)) / 255) * 4]; + } + // grid point (i, j) is corner 0 plus i/N of the way to corner 1 and j/N of the way to corner 2 + auto emit = [&](int i, int j) { + const float a = static_cast(i) / N; + const float b = static_cast(j) / N; + const float w = 1.0f - a - b; + vec3d pt; + pt.xyz.x = w * corner[0].xyz.x + a * corner[1].xyz.x + b * corner[2].xyz.x; + pt.xyz.y = w * corner[0].xyz.y + a * corner[1].xyz.y + b * corner[2].xyz.y; + pt.xyz.z = w * corner[0].xyz.z + a * corner[1].xyz.z + b * corner[2].xyz.z; + vertex *vt = &Nebula_verts[k]; + g3_transfer_vertex(vt, &pt); + vt->a = 255; + for (int ch = 0; ch < 3; ch++) + Nebula_mesh_colors[k * 3 + ch] = + static_cast(w * rgb[0][ch] + a * rgb[1][ch] + b * rgb[2][ch] + 0.5f); + k++; + }; + for (int i = 0; i < N; i++) { + for (int j = 0; j < N - i; j++) { + emit(i, j); + emit(i + 1, j); + emit(i, j + 1); + if (i + j < N - 1) { + emit(i + 1, j); + emit(i + 1, j + 1); + emit(i, j + 1); + } + } + } + } + Assertion(k == Nebula_n_verts, "background nebula mesh vertex count mismatch (%d != %d)", k, Nebula_n_verts); + Nebula_mesh_colors_tonemapper = -2; +} + +// How bright the scene must be, before the exposure, for post-processing to show `target`: the scene +// pass linearizes colors (gamma 2.2, gamma.sdr); the tonemap pass applies the exposure, the curve +// (tonemapping-f.sdr) and gamma 1/2.2. Uncharted inverts as a quadratic; Linear is only a clamp. +static float nebula_scene_value(ubyte target, lighting_profiles::TonemapperAlgorithm tonemapper) +{ + const float out = std::pow(target / 255.0f, 2.2f); + if (tonemapper != lighting_profiles::TonemapperAlgorithm::Uncharted) + return out; + constexpr float A = 0.15f, B = 0.50f, C = 0.10f, D = 0.20f, E = 0.02f, F = 0.30f, W = 11.2f; + const float white = ((W * (A * W + C * B) + D * E) / (W * (A * W + B) + D * F)) - E / F; + const float k = out * white + E / F; + const float qa = A * (1.0f - k); + const float qb = B * (C - k); + const float qc = D * (E - k * F); + return (-qb + std::sqrt(qb * qb - 4.0f * qa * qc)) / (2.0f * qa); +} + +// Color the mesh vertices for the current post-processing, so the palette colors reach the screen as +// FS1 drew them over a black sky (bloom and the other effects still apply on top, and over a lit +// background the sum is tonemapped together); only redone when post-processing is switched or its +// curve or exposure changes. A color brighter than the curve can show clips, as it would on any +// other background; scene values above 1 would also bloom where the rest of the background cannot. +static void nebula_update_mesh_colors() +{ + Assertion(Nebula_tex_data != nullptr && Nebula_mesh_colors.size() == static_cast(Nebula_n_verts) * 3, + "the background nebula mesh colors do not match its vertices"); + namespace ltp = lighting_profiles; + const auto tn = ltp::current_tonemapper(); + const float exposure = ltp::current_exposure(); + const bool exact = (tn == ltp::TonemapperAlgorithm::Uncharted || tn == ltp::TonemapperAlgorithm::Linear) && + exposure > 0.0f && std::isfinite(exposure); + if (High_dynamic_range && !exact && static_cast(tn) != Nebula_mesh_warned_tonemapper) { + mprintf(("Background nebula: no color compensation for the %s tonemapper at exposure %f.\n", + ltp::tonemapper_to_name(tn).c_str(), exposure)); + Nebula_mesh_warned_tonemapper = static_cast(tn); + } + const int tonemapper = (High_dynamic_range && exact) ? static_cast(tn) : -1; + const float key_exposure = (tonemapper >= 0) ? exposure : 1.0f; + if (tonemapper == Nebula_mesh_colors_tonemapper && key_exposure == Nebula_mesh_colors_exposure) return; + Nebula_mesh_colors_tonemapper = tonemapper; + Nebula_mesh_colors_exposure = key_exposure; + + ubyte lut[256]; + for (int v = 0; v < 256; v++) { + if (tonemapper < 0) { + lut[v] = static_cast(v); + continue; + } + const float scene = nebula_scene_value(static_cast(v), tn) / exposure; + float b = std::pow(std::min(scene, 1.0f), 1.0f / 2.2f) * 255.0f + 0.5f; + CLAMP(b, 0.0f, 255.0f); + lut[v] = static_cast(b); + } + for (int i = 0; i < Nebula_n_verts; i++) { + vertex *vt = &Nebula_verts[i]; + vt->b = lut[Nebula_mesh_colors[i * 3 + 0]]; + vt->g = lut[Nebula_mesh_colors[i * 3 + 1]]; + vt->r = lut[Nebula_mesh_colors[i * 3 + 2]]; } +} - if (dc_maybe_stuff_string_white(filename)) { - nebula_init(filename.c_str()); +void nebula_init(int index, int pitch, int bank, int heading) +{ + const bool valid = index >= 0 && index < static_cast(Generated_nebula_patterns.size()) && + !Is_standalone; + + // Only the sphere depends on the orientation, so a change of pitch, bank or heading alone (the + // editor's spinboxes, or undoing one) keeps the baked texture instead of baking it again. + const bool same_texture = valid && Nebula_bitmap >= 0 && Nebula_baked_pattern == index && + Nebula_baked_color == Mission_palette; + if (same_texture) { + delete[] Nebula_verts; + Nebula_verts = nullptr; + Nebula_n_verts = 0; } else { nebula_close(); } -} + angles pbh; + pbh.p = fl_radians(pitch); + pbh.b = fl_radians(bank); + pbh.h = fl_radians(heading); + vm_angles_2_matrix(&Nebula_orient, &pbh); + if (!valid) + return; + + const generated_nebula_pattern &pattern = Generated_nebula_patterns[index]; + // pick the tint color (fall back to white if the palette index is out of range) + generated_nebula_color col; + if (Mission_palette >= 0 && Mission_palette < static_cast(Generated_nebula_colors.size())) + col = Generated_nebula_colors[Mission_palette]; + + SCP_vector mesh_pts; + SCP_vector mesh_levels; + SCP_vector mesh_tris; + // the ramp and the equirect bake differ, so a change of path needs a fresh texture + if (!pattern.mesh.empty() && nebula_load_mesh(pattern.mesh.c_str(), mesh_pts, mesh_levels, mesh_tris)) { + if (!same_texture || !Nebula_baked_mesh) { + nebula_close(); + nebula_make_ramp(pattern, col); + Nebula_baked_pattern = index; + Nebula_baked_color = Mission_palette; + Nebula_baked_mesh = true; + } + nebula_build_mesh_verts(mesh_pts, mesh_levels, mesh_tris, &Nebula_orient); + return; + } + if (!same_texture || Nebula_baked_mesh) { + nebula_close(); + nebula_bake_texture(pattern, col); + Nebula_baked_pattern = index; + Nebula_baked_color = Mission_palette; + Nebula_baked_mesh = false; + } + nebula_generate_sphere(pattern, &Nebula_orient); +} + +void nebula_render() +{ + // the mesh path does not sample Nebula_bitmap, but it still marks a finished bake on both paths + if (!nebula_backgrounds_enabled() || Nebula_verts == nullptr || Nebula_n_verts <= 0 || Nebula_bitmap < 0) + return; + if (!Detail.planets_suns) + return; + + // additive sphere (no depth), instanced around the eye + material mat; + material_set_unlit(&mat, Nebula_baked_mesh ? -1 : Nebula_bitmap, 1.0f, true, false); + // the ramp's dark end is black, so adding leaves the stars and any skybox where the nebula is faint + mat.set_blend_mode(ALPHA_BLEND_ADDITIVE); + + if (Nebula_baked_mesh) + nebula_update_mesh_colors(); + + gr_start_instance_matrix(&Eye_position, &vmd_identity_matrix); + if (Nebula_baked_mesh) + g3_render_primitives_colored(&mat, Nebula_verts, Nebula_n_verts, PRIM_TYPE_TRIS, false); + else + g3_render_primitives_textured(&mat, Nebula_verts, Nebula_n_verts, PRIM_TYPE_TRIS, false); + gr_end_instance_matrix(); +} diff --git a/code/starfield/nebula.h b/code/starfield/nebula.h index 2d6f86add9d..cffbff28b51 100644 --- a/code/starfield/nebula.h +++ b/code/starfield/nebula.h @@ -1,8 +1,8 @@ /* * Copyright (C) Volition, Inc. 1999. All rights reserved. * - * All source code herein is the property of Volition, Inc. You may not sell - * or otherwise commercially exploit the source or things you created based on the + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the * source. * */ @@ -12,17 +12,95 @@ #ifndef _NEBULA_H #define _NEBULA_H +#include "globalincs/pstypes.h" + // mainly only needed by Fred extern int Nebula_pitch; extern int Nebula_bank; extern int Nebula_heading; -struct angles; +// A placed cloud in a generated nebula pattern: a soft ellipse on the nebula's flat map, the same +// map the FS1 nebula meshes were drawn on. Longitude wraps, so a cloud can cross the seam. +// +Cloud: lon lat width height angle brightness +struct generated_nebula_cloud { + float lon = 0.0f; // center longitude in degrees, 0..360 + float lat = 0.0f; // center latitude in degrees, -90..90 + float width = 30.0f; // size across, in degrees of longitude (full width at half brightness) + float height = 10.0f; // size along the map's other axis, on the same scale as width + float angle = 0.0f; // rotation of the ellipse on the map, in degrees + float brightness = 0.5f; // peak brightness, 0..1 (overlapping clouds add up, capped at 1) +}; + +// A generated (FS1-style) background nebula pattern, made of two layers: +// - a procedural background from noise (the density..seed fields), unless +Clouds Only is set +// - placed clouds (+Cloud), broken up by the same noise according to +Cloud Detail +// The layers combine with a screen blend, so clouds brighten the background without clipping. +// Patterns and colors come from generated_nebula.tbl (#Generated Nebula Patterns / #Generated +// Nebula Colors); the built-in one holds the FS1 set (Nebula01..03 as clouds-only patterns laid out +// like the FS1 originals, and FS1's nine colors). *-gneb.tbm files add entries or override them by +// name; an entry that lists any +Cloud replaces the pattern's whole cloud list. +struct generated_nebula_pattern { + SCP_string name; + SCP_string mesh; // +Mesh: an FS1 .neb mesh drawn instead of the generated clouds when the file exists + SCP_string palette_prefix; // +Palette Prefix: with a color's +Palette Suffix: names the FS1 palette file (-.pcx) + SCP_vector clouds; + bool clouds_only = false; // no procedural background, only the placed clouds + float cloud_detail = 0.35f; // how much the noise breaks up the clouds: 0 = smooth, 1 = ragged + float density = 0.30f; // fraction of bright "knots" (mostly-black otherwise) + float freq_u = 2.0f; // noise frequency along longitude (unequal u/v => streaky) + float freq_v = 5.0f; // noise frequency along latitude + int octaves = 4; // fbm octaves; fewer = smoother/broader masses (1-2 = FS1-like), more = busier + float warp = 0.4f; // directional warp strength (stretches the wisps) + float contrast = 1.5f; // falloff curve applied to bright knots + float intensity = 1.0f; // overall brightness multiplier (channels are clamped to max) + int seed = 0; // noise seed (gives each pattern a distinct look) + int res_lon = 24; // longitude grid resolution (coarse = retro chunky gouraud) + int res_lat = 12; // latitude grid resolution + float band_min = 0.0f; // latitude extent in 0..1; full sphere by default (poles closed) + float band_max = 1.0f; +}; + +// A named tint color for the generated nebula (generated_nebula.tbl #Generated Nebula Colors). +struct generated_nebula_color { + SCP_string name; + SCP_string palette_suffix; // +Palette Suffix: see generated_nebula_pattern::palette_prefix; the file's 20-entry ramp replaces the RGB fade when it exists + ubyte r = 255; + ubyte g = 255; + ubyte b = 255; +}; + +extern SCP_vector Generated_nebula_patterns; +extern SCP_vector Generated_nebula_colors; + +// Clear the registries above and fill them from generated_nebula.tbl and any *-gneb.tbm. Called +// once from neb2_init(). +void generated_nebula_init(); + +// Look up a pattern/color by name; returns the registry index or -1 if not found. +int generated_nebula_pattern_lookup(const char *name); +int generated_nebula_color_lookup(const char *name); + +// Safe name accessors for the registries (return "" when the index is out of range). +const char *generated_nebula_pattern_name(int index); +const char *generated_nebula_color_name(int index); + +extern int Nebula_mission_switch; +// true when the background nebula should be drawn: the mission line when present, else the mod switch +bool nebula_backgrounds_enabled(); + +// +Nebula: / +Color: names that matched no table entry, kept so saving writes them back unchanged +// instead of dropping the pattern or switching the color +extern SCP_string Nebula_unknown_pattern; +extern SCP_string Nebula_unknown_color; + +// The default color index: "Blue" (FS1's default) if the tables define it, else the first color, +// else -1 when there are no colors at all +int generated_nebula_default_color(); -// You shouldn't pass the extension for filename. -// PBH = Pitch, Bank, Heading. Pass NULL for default orientation. +// PBH = Pitch, Bank, Heading (in degrees). index < 0 disables the nebula. void nebula_init( int index, int pitch, int bank, int heading ); -void nebula_init( const char *filename, angles *pbh = /*NULL*/ 0 ); void nebula_close(); +// Renders the background nebula (no-op if none is active). +void nebula_render(); #endif //_NEBULA_H diff --git a/code/starfield/starfield.cpp b/code/starfield/starfield.cpp index a90ede83880..ac004c14b39 100644 --- a/code/starfield/starfield.cpp +++ b/code/starfield/starfield.cpp @@ -1079,6 +1079,8 @@ void stars_level_close() { Mission_env_map = -1; } + nebula_close(); + ENVMAP = Default_env_map; } @@ -1928,7 +1930,7 @@ void stars_draw_motion_debris() refresh_motion_debris = false; } -void stars_draw(int show_stars, int show_suns, int /*show_nebulas*/, int show_subspace, int env, bool in_mission) +void stars_draw(int show_stars, int show_suns, int show_nebulas, int show_subspace, int env, bool in_mission) { GR_DEBUG_SCOPE("Draw Stars"); TRACE_SCOPE(tracing::DrawStars); @@ -1966,6 +1968,14 @@ void stars_draw(int show_stars, int show_suns, int /*show_nebulas*/, int show_s stars_draw_background(); } + // generated (FS1-style) background nebula -- a skybox-level backdrop that sits in front of + // the background pof and behind the stars. Gate it like stars_draw_background() above (render + // whenever the background does, including into the env map for reflections) but never under neb2. + if (show_nebulas && (Game_detail_flags & DETAIL_FLAG_NEBULAS) && (show_stars || !show_subspace) && + !(The_mission.flags[Mission::Mission_Flags::Fullneb])) { + nebula_render(); + } + if ( !env && show_stars && (Nmodel_num < 0) && (Game_detail_flags & DETAIL_FLAG_STARS) && !(The_mission.flags[Mission::Mission_Flags::Fullneb]) && (supernova_stage() < SUPERNOVA_STAGE::TOOLTIME) ) { stars_draw_stars(); } diff --git a/fred2/bgbitmapdlg.cpp b/fred2/bgbitmapdlg.cpp index 74e34e88bb1..e97f82abddd 100644 --- a/fred2/bgbitmapdlg.cpp +++ b/fred2/bgbitmapdlg.cpp @@ -45,6 +45,7 @@ bg_bitmap_dlg::bg_bitmap_dlg(CWnd* pParent) : CDialog(bg_bitmap_dlg::IDD, pParen m_envmap = _T(""); m_nebula_color = -1; m_nebula_index = -1; + m_fs1_neb_mission = 0; m_bank = 0; m_heading = 0; m_pitch = 0; @@ -95,6 +96,7 @@ void bg_bitmap_dlg::DoDataExchange(CDataExchange* pDX) DDX_Text(pDX, IDC_NEB2_INTENSITY, m_neb_intensity); DDX_CBIndex(pDX, IDC_NEBCOLOR, m_nebula_color); DDX_CBIndex(pDX, IDC_NEBPATTERN, m_nebula_index); + DDX_CBIndex(pDX, IDC_FS1_NEB_MISSION, m_fs1_neb_mission); DDX_Text(pDX, IDC_BANK, m_bank); DDX_Text(pDX, IDC_HEADING, m_heading); DDX_Text(pDX, IDC_PITCH, m_pitch); @@ -164,6 +166,7 @@ BEGIN_MESSAGE_MAP(bg_bitmap_dlg, CDialog) ON_WM_CLOSE() ON_CBN_SELCHANGE(IDC_NEBCOLOR, OnSelchangeNebcolor) ON_CBN_SELCHANGE(IDC_NEBPATTERN, OnSelchangeNebpattern) + ON_CBN_SELCHANGE(IDC_FS1_NEB_MISSION, OnSelchangeFs1NebMission) ON_BN_CLICKED(IDC_FULLNEB, OnFullNeb) ON_BN_CLICKED(IDC_NEB2_PALETTE_OVERRIDE, OnNeb2PaletteOverride) ON_CBN_SELCHANGE(IDC_NEB2_TEXTURE, OnSelchangeNeb2Texture) @@ -260,9 +263,14 @@ void bg_bitmap_dlg::create() GetDlgItem(IDC_SUN1_B_SPIN)->EnableWindow(FALSE); box = (CComboBox *) GetDlgItem(IDC_NEBCOLOR); - for (int i=0; iAddString(Nebula_colors[i]); - } + for (int i=0; i<(int)Generated_nebula_colors.size(); i++){ + box->AddString(Generated_nebula_colors[i].name.c_str()); + } + + box = (CComboBox *) GetDlgItem(IDC_FS1_NEB_MISSION); + box->AddString("Default"); + box->AddString("On"); + box->AddString("Off"); m_slider.SetRange(0, MAX_STARS); m_slider.SetPos(Num_stars); @@ -339,8 +347,10 @@ void bg_bitmap_dlg::create() m_nebula_index = Nebula_index + 1; m_nebula_color = Mission_palette; - if (Nebula_index < 0){ + m_fs1_neb_mission = Nebula_mission_switch < 0 ? 0 : (Nebula_mission_switch == 1 ? 1 : 2); + if (Nebula_index < 0 && Nebula_unknown_pattern.empty()){ GetDlgItem(IDC_NEBCOLOR)->EnableWindow(FALSE); + GetDlgItem(IDC_FS1_NEB_MISSION)->EnableWindow(FALSE); } m_pitch = Nebula_pitch; @@ -452,6 +462,7 @@ void bg_bitmap_dlg::OnClose() { UpdateData(TRUE); Mission_palette = m_nebula_color; + Nebula_mission_switch = m_fs1_neb_mission == 0 ? -1 : (m_fs1_neb_mission == 1 ? 1 : 0); if(m_fullneb){ The_mission.flags.set(Mission::Mission_Flags::Fullneb); @@ -601,7 +612,9 @@ void bg_bitmap_dlg::OnSelchangeNebcolor() UpdateData(TRUE); Mission_palette = m_nebula_color; - + Nebula_unknown_color.clear(); + nebula_init(Nebula_index, m_pitch, m_bank, m_heading); + Update_window = 1; } @@ -612,14 +625,22 @@ void bg_bitmap_dlg::OnSelchangeNebpattern() UpdateData(TRUE); // fullneb indexes differently - Nebula_index = m_nebula_index - 1; + Nebula_index = m_nebula_index - 1; + Nebula_unknown_pattern.clear(); GetDlgItem(IDC_NEBCOLOR)->EnableWindow(m_nebula_index ? TRUE : FALSE); + GetDlgItem(IDC_FS1_NEB_MISSION)->EnableWindow(m_nebula_index ? TRUE : FALSE); nebula_init(Nebula_index, m_pitch, m_bank, m_heading); Update_window = 1; } +void bg_bitmap_dlg::OnSelchangeFs1NebMission() +{ + UpdateData(TRUE); + Nebula_mission_switch = m_fs1_neb_mission == 0 ? -1 : (m_fs1_neb_mission == 1 ? 1 : 0); +} + void bg_bitmap_dlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar) { char buf[40]; @@ -678,6 +699,7 @@ void bg_bitmap_dlg::OnFullNeb() // disable non-fullneb controls GetDlgItem(IDC_NEBPATTERN)->EnableWindow(FALSE); GetDlgItem(IDC_NEBCOLOR)->EnableWindow(FALSE); + GetDlgItem(IDC_FS1_NEB_MISSION)->EnableWindow(FALSE); GetDlgItem(IDC_PITCH)->EnableWindow(FALSE); GetDlgItem(IDC_BANK)->EnableWindow(FALSE); GetDlgItem(IDC_HEADING)->EnableWindow(FALSE); @@ -685,6 +707,7 @@ void bg_bitmap_dlg::OnFullNeb() // enable all non-fullneb controls GetDlgItem(IDC_NEBPATTERN)->EnableWindow(TRUE); GetDlgItem(IDC_NEBCOLOR)->EnableWindow(TRUE); + GetDlgItem(IDC_FS1_NEB_MISSION)->EnableWindow(m_nebula_index ? TRUE : FALSE); GetDlgItem(IDC_PITCH)->EnableWindow(TRUE); GetDlgItem(IDC_BANK)->EnableWindow(TRUE); GetDlgItem(IDC_HEADING)->EnableWindow(TRUE); @@ -754,11 +777,11 @@ void bg_bitmap_dlg::build_nebfile_list() // clear the box box->ResetContent(); - // add all necessary strings + // add all necessary strings box->AddString("None"); - for (i=0; iAddString(Nebula_filenames[i]); - } + for (i=0; i<(int)Generated_nebula_patterns.size(); i++){ + box->AddString(Generated_nebula_patterns[i].name.c_str()); + } // select the first elementccombobox box->SetCurSel(0); diff --git a/fred2/bgbitmapdlg.h b/fred2/bgbitmapdlg.h index 96c8d61b8f7..751ffc20c34 100644 --- a/fred2/bgbitmapdlg.h +++ b/fred2/bgbitmapdlg.h @@ -50,6 +50,7 @@ class bg_bitmap_dlg : public CDialog CString m_neb_intensity; int m_nebula_color; int m_nebula_index; + int m_fs1_neb_mission; int m_bank; int m_heading; int m_pitch; @@ -133,6 +134,7 @@ class bg_bitmap_dlg : public CDialog afx_msg void OnOK(); afx_msg void OnSelchangeNebcolor(); afx_msg void OnSelchangeNebpattern(); + afx_msg void OnSelchangeFs1NebMission(); afx_msg void OnFullNeb(); afx_msg void OnNeb2PaletteOverride(); afx_msg void OnSelchangeNeb2Texture(); diff --git a/fred2/dumpstats.cpp b/fred2/dumpstats.cpp index fbb4f227280..138f676226f 100644 --- a/fred2/dumpstats.cpp +++ b/fred2/dumpstats.cpp @@ -24,6 +24,7 @@ #include "object/object.h" #include "object/waypoint.h" #include "species_defs/species_defs.h" +#include "starfield/nebula.h" #include "starfield/starfield.h" #include "weapon/weapon.h" @@ -285,7 +286,7 @@ void DumpStats::get_background_stats(CString &buffer) } else { // FS! nebula pattern if (Nebula_index >= 0) { - temp.Format("\tOld style FS1 nebula filename: %s\r\n", Nebula_filenames[Nebula_index]); + temp.Format("\tGenerated nebula pattern: %s\r\n", generated_nebula_pattern_name(Nebula_index)); buffer += temp; } } diff --git a/fred2/fred.rc b/fred2/fred.rc index d69261b8939..ae9728d9845 100644 --- a/fred2/fred.rc +++ b/fred2/fred.rc @@ -1813,6 +1813,8 @@ BEGIN CONTROL "Toggle ship trails",IDC_NEB2_TOGGLE_TRAILS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,250,65,10 COMBOBOX IDC_NEBPATTERN,42,405,61,108,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP COMBOBOX IDC_NEBCOLOR,42,421,61,108,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "In mission",IDC_STATIC,14,391,36,8 + COMBOBOX IDC_FS1_NEB_MISSION,54,388,49,60,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP EDITTEXT IDC_PITCH,166,388,40,14,ES_AUTOHSCROLL | ES_NUMBER EDITTEXT IDC_BANK,166,405,40,14,ES_AUTOHSCROLL | ES_NUMBER EDITTEXT IDC_HEADING,166,422,40,14,ES_AUTOHSCROLL | ES_NUMBER @@ -1883,7 +1885,7 @@ BEGIN LTEXT "P",IDC_STATIC,227,256,8,8 LTEXT "B",IDC_STATIC,293,256,8,8 LTEXT "H",IDC_STATIC,357,255,8,8 - GROUPBOX "Old Nebula",IDC_STATIC,5,378,210,64 + GROUPBOX "Generated BG Nebula",IDC_STATIC,5,378,210,64 LISTBOX IDC_NEB2_POOF_LIST,142,190,67,90,LBS_EXTENDEDSEL | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP CONTROL "Display bg bitmaps in nebula",IDC_NEB2_BACKGROUND_BITMAPS, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,260,128,10 diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index 0e7e2d25b61..2d6587255ed 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -370,8 +370,6 @@ bool CFREDDoc::load_mission(const char *pathname, int flags) { } } - Assert(Mission_palette >= 0); - Assert(Mission_palette <= 98); // go through all ships and translate their callsign and alternate name indices objp = GET_FIRST(&obj_used_list); diff --git a/fred2/resource.h b/fred2/resource.h index f03b894bf21..28ccf0da8ab 100644 --- a/fred2/resource.h +++ b/fred2/resource.h @@ -563,6 +563,7 @@ #define IDC_SET_FROM_SHIP_CLASS 1203 #define IDC_CURRENT_CONTAINER_NAME 1203 #define IDC_NEBCOLOR 1204 +#define IDC_FS1_NEB_MISSION 1747 #define IDC_SUBSTITUTE_EVENT_MUSIC 1205 #define IDC_OPEN_CUSTOM_DATA 1206 #define IDC_AI_PROFILE 1207 @@ -1638,7 +1639,7 @@ #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_3D_CONTROLS 1 #define _APS_NEXT_RESOURCE_VALUE 340 -#define _APS_NEXT_CONTROL_VALUE 1747 +#define _APS_NEXT_CONTROL_VALUE 1748 #define _APS_NEXT_COMMAND_VALUE 33124 #define _APS_NEXT_SYMED_VALUE 105 #endif diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index b07243013f5..33083f8a68a 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -366,7 +366,6 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { } Assert(Mission_palette >= 0); - Assert(Mission_palette <= 98); // go through all ships and translate their callsign and alternate name indices objp = GET_FIRST(&obj_used_list); diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 5c1e47c175f..2794bf0059e 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -987,8 +987,12 @@ SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() { SCP_vector out; out.emplace_back(""); - for (auto& neb : Nebula_filenames) { - out.emplace_back(neb); + for (const auto& p : Generated_nebula_patterns) { + out.emplace_back(p.name); + } + // a name the table does not know is kept so the mission saves it back unchanged + if (!Nebula_unknown_pattern.empty()) { + out.emplace_back(Nebula_unknown_pattern); } return out; } @@ -996,59 +1000,73 @@ SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() SCP_vector BackgroundEditorDialogModel::getOldNebulaColorOptions() { SCP_vector out; - out.reserve(NUM_NEBULA_COLORS); - for (auto& color : Nebula_colors) { - out.emplace_back(color); + out.reserve(Generated_nebula_colors.size() + 1); + for (const auto& c : Generated_nebula_colors) { + out.emplace_back(c.name); + } + if (!Nebula_unknown_color.empty()) { + out.emplace_back(Nebula_unknown_color); } return out; } SCP_string BackgroundEditorDialogModel::getOldNebulaPattern() { + if (!Nebula_unknown_pattern.empty()) + return Nebula_unknown_pattern; + if (Nebula_index < 0) return ""; - if (Nebula_index >= 0 && Nebula_index < NUM_NEBULAS) { - return Nebula_filenames[Nebula_index]; - } - - return SCP_string{}; + return generated_nebula_pattern_name(Nebula_index); } void BackgroundEditorDialogModel::setOldNebulaPattern(const SCP_string& name) { int newIndex = -1; if (!name.empty() && stricmp(name.c_str(), "") != 0) { - for (int i = 0; i < NUM_NEBULAS; ++i) { - if (!stricmp(Nebula_filenames[i], name.c_str())) { - newIndex = i; - break; - } - } + newIndex = generated_nebula_pattern_lookup(name.c_str()); } + if (newIndex >= 0 || name.empty() || stricmp(name.c_str(), "") == 0) { + Nebula_unknown_pattern.clear(); + } else { + Nebula_unknown_pattern = name; + } modify(Nebula_index, newIndex); + nebula_init(Nebula_index, Nebula_pitch, Nebula_bank, Nebula_heading); } SCP_string BackgroundEditorDialogModel::getOldNebulaColorName() { - if (Mission_palette >= 0 && Mission_palette < NUM_NEBULA_COLORS) { - return Nebula_colors[Mission_palette]; - } - return SCP_string{}; + if (!Nebula_unknown_color.empty()) + return Nebula_unknown_color; + + return generated_nebula_color_name(Mission_palette); } void BackgroundEditorDialogModel::setOldNebulaColorName(const SCP_string& name) { if (name.empty()) return; - for (int i = 0; i < NUM_NEBULA_COLORS; ++i) { - if (!stricmp(Nebula_colors[i], name.c_str())) { - modify(Mission_palette, i); - return; - } + const int idx = generated_nebula_color_lookup(name.c_str()); + if (idx < 0) { + Nebula_unknown_color = name; + return; } - // name not found: ignore + Nebula_unknown_color.clear(); + modify(Mission_palette, idx); + nebula_init(Nebula_index, Nebula_pitch, Nebula_bank, Nebula_heading); +} + +int BackgroundEditorDialogModel::getFs1NebulaMissionSwitch() +{ + return Nebula_mission_switch; +} + +void BackgroundEditorDialogModel::setFs1NebulaMissionSwitch(int value) +{ + modify(Nebula_mission_switch, value); } int BackgroundEditorDialogModel::getOldNebulaPitch() @@ -1062,6 +1080,7 @@ void BackgroundEditorDialogModel::setOldNebulaPitch(int deg) if (Nebula_pitch != deg) { Nebula_pitch = deg; modify(Nebula_pitch, deg); + nebula_init(Nebula_index, Nebula_pitch, Nebula_bank, Nebula_heading); } } @@ -1076,6 +1095,7 @@ void BackgroundEditorDialogModel::setOldNebulaBank(int deg) if (Nebula_bank != deg) { Nebula_bank = deg; modify(Nebula_bank, deg); + nebula_init(Nebula_index, Nebula_pitch, Nebula_bank, Nebula_heading); } } @@ -1090,6 +1110,7 @@ void BackgroundEditorDialogModel::setOldNebulaHeading(int deg) if (Nebula_heading != deg) { Nebula_heading = deg; modify(Nebula_heading, deg); + nebula_init(Nebula_index, Nebula_pitch, Nebula_bank, Nebula_heading); } } diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 36118b3aa8d..f4121724867 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -133,6 +133,9 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { void setOldNebulaBank(int deg); static int getOldNebulaHeading(); void setOldNebulaHeading(int deg); + // the mission's $FS1 Nebula: line: -1 follows the game_settings.tbl switch, 1 draws, 0 does not + static int getFs1NebulaMissionSwitch(); + void setFs1NebulaMissionSwitch(int value); // ambient light group static int getAmbientR(); diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index 075274477ed..4c6fbac3682 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -106,6 +106,10 @@ void BackgroundEditorDialog::initializeUi() ui->oldNebulaColorCombo->addItem(QString::fromStdString(s)); } + ui->fs1NebulaMissionCombo->addItem("Default (game_settings.tbl)"); + ui->fs1NebulaMissionCombo->addItem("On"); + ui->fs1NebulaMissionCombo->addItem("Off"); + updateOldNebulaControls(); // Ambient light @@ -337,12 +341,16 @@ void BackgroundEditorDialog::updateOldNebulaControls() ui->oldNebulaPitchSpinBox->setEnabled(enabled && old_enabled); ui->oldNebulaBankSpinBox->setEnabled(enabled && old_enabled); ui->oldNebulaHeadingSpinBox->setEnabled(enabled && old_enabled); + ui->fs1NebulaMissionCombo->setEnabled(enabled && old_enabled); ui->oldNebulaPatternCombo->setCurrentIndex(ui->oldNebulaPatternCombo->findText(QString::fromStdString(_model->getOldNebulaPattern()))); ui->oldNebulaColorCombo->setCurrentIndex(ui->oldNebulaColorCombo->findText(QString::fromStdString(_model->getOldNebulaColorName()))); ui->oldNebulaPitchSpinBox->setValue(_model->getOldNebulaPitch()); ui->oldNebulaBankSpinBox->setValue(_model->getOldNebulaBank()); ui->oldNebulaHeadingSpinBox->setValue(_model->getOldNebulaHeading()); + // combo rows: 0 default, 1 on, 2 off; the model's -1/1/0 + const int sw = _model->getFs1NebulaMissionSwitch(); + ui->fs1NebulaMissionCombo->setCurrentIndex(sw < 0 ? 0 : (sw == 1 ? 1 : 2)); } void BackgroundEditorDialog::updateAmbientLightControls() @@ -813,6 +821,13 @@ void BackgroundEditorDialog::on_oldNebulaColorCombo_currentIndexChanged(int inde _model->setOldNebulaColorName(text.toUtf8().constData()); } +void BackgroundEditorDialog::on_fs1NebulaMissionCombo_currentIndexChanged(int index) +{ + if (index < 0) + return; + _model->setFs1NebulaMissionSwitch(index == 0 ? -1 : (index == 1 ? 1 : 0)); +} + void BackgroundEditorDialog::on_oldNebulaPitchSpinBox_valueChanged(int arg1) { _model->setOldNebulaPitch(arg1); diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index 42ea46fb77f..302be26f7a8 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -76,6 +76,7 @@ private slots: void on_oldNebulaPitchSpinBox_valueChanged(int arg1); void on_oldNebulaBankSpinBox_valueChanged(int arg1); void on_oldNebulaHeadingSpinBox_valueChanged(int arg1); + void on_fs1NebulaMissionCombo_currentIndexChanged(int index); // Ambient Light void on_ambientLightRedSlider_valueChanged(int value); diff --git a/qtfred/ui/BackgroundEditor.ui b/qtfred/ui/BackgroundEditor.ui index 779d8ab0311..cb6bb8b36a1 100644 --- a/qtfred/ui/BackgroundEditor.ui +++ b/qtfred/ui/BackgroundEditor.ui @@ -706,7 +706,7 @@ - Old Nebula + Background Nebula (FS1 style) @@ -731,6 +731,20 @@ + + + + In this mission + + + + + + + Default follows the mod's $FS1 nebula backgrounds: switch in game_settings.tbl; On or Off writes $FS1 Nebula: into the mission. + + + @@ -1190,6 +1204,7 @@ lightingProfileCombo oldNebulaPatternCombo oldNebulaColorCombo + fs1NebulaMissionCombo oldNebulaPitchSpinBox oldNebulaBankSpinBox oldNebulaHeadingSpinBox