Files
BMagnuandGoober5000 20c8417e8b Fullneb Revamp (#7352)
* change to new fog system

* Fix fog conversion issues

* remove unused old neb values

* Update scripting API

* temporary old-value round trip

* Rewrite nebula fog with four explicit parameters

Replace the legacy `+Fog Near Mult` / `+Fog Far Mult` model — which
depended on fixed engine constants — with four parameters that map
directly to renderer behavior:

  +Fog 1000m Visibility    fraction of light surviving 1000m of fog
  +Fog Near Distance       meters from camera where fog begins
  +Fog Skybox Clip Distance  max render distance for the skybox in fog
  +Fog Clip Distance       max render distance for the scene in fog

The deferred-lighting fog shader, material system, model renderer,
object sort, and the Lua mission API all consume the new parameters
directly. The legacy `Neb2_fog_near_mult` / `Neb2_fog_far_mult`
globals are removed.

Mission parsing accepts either the legacy keys or the modern keys.
Legacy values are converted to the modern set at parse time, and a
per-mission flag (`Neb2_fog_use_legacy_values`) records which keys to
write back on save. The flag is cleared only when something actually
changes, so opening and OK'ing an unmodified legacy mission preserves
its on-disk format. Saving uses `optional_string_fred` to preserve
user comments through edit cycles.

FRED2 and QtFRED background editors both expose the four parameters
in place of the old multipliers, with friendly labels and tooltips.
Both editors validate input (visibility clamped to [0, 1]; near
distance must be > 0; skybox and scene clip distances may be 0 to
disable fog at that range) and use a snapshot-and-compare at close
time so that a no-op open-then-OK never spuriously rewrites a legacy
mission in the modern format.

FRED2's background dialog is also reorganized so that "Display bg
bitmaps in nebula", "Override nebula fog palette", the RGB row, and
the four fog parameters all sit inside the Nebula group box; the
"Old Nebula" group box is restored below it with its Pattern, Color,
Pitch, Bank, and Heading controls back inside the frame. The dialog
now overrides PreTranslateMessage so that the new fog tooltips —
and the previously broken "Save sun/bitmap angles in correct format"
tooltip — actually fire.

QtFRED's BackgroundEditor.ui adds four QDoubleSpinBox controls in
the existing nebula form layout, with per-field decimals chosen to
avoid scientific notation, and per-field min/max declaring the
validation rules. The QtFRED help page is updated to describe the
new fields.

* fix tooltips

* restore forward-renderer fog

* Clang Tidy

* address feedback

* Refine nebula fog rewrite

- Rename Neb2_fog_use_legacy_values to Neb2_fog_save_legacy_values
  to reflect that the flag governs save format, not runtime
  behavior.
- Decide both the save flag and the legacy-to-modern conversion
  purely from whether the file contains modern fog keys, dropping
  the prior heuristic based on required_fso_version. A mission
  without modern keys (legacy- or retail-format) converts on load
  and saves back in legacy format; missions with modern keys are
  saved as modern.
- Snapshot the four fog values as floats at editor open and use
  fl_equal for the no-change comparison in both FRED2 and QtFRED,
  replacing the FRED2 CString comparison and the QtFRED strict
  inequality.
- Document the on-disk invariant in missionsave: when not saving
  legacy keys, the four modern keys must always be written (never
  gated on "only if non-default"), since the parser uses their
  presence to decide whether to run the conversion on next load.

---------

Co-authored-by: Goober5000 <[email protected]>
2026-05-28 14:43:48 +09:00

1284 lines
37 KiB
C++

/*
* 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
* source.
*
*/
#include "bmpman/bmpman.h"
#include "cmdline/cmdline.h"
#include "ddsutils/ddsutils.h"
#include "debugconsole/console.h"
#include "freespace.h"
#include "jpgutils/jpgutils.h"
#include "math/bitarray.h"
#include "mission/missionparse.h"
#include "nebula/neb.h"
#include "object/object.h"
#include "options/Option.h"
#include "parse/parselo.h"
#include "pcxutils/pcxutils.h"
#include "render/3d.h"
#include "render/batching.h"
#include "ship/ship.h"
#include "starfield/starfield.h"
#include "tgautils/tgautils.h"
#include "tracing/tracing.h"
#include "graphics/light.h"
// --------------------------------------------------------------------------------------------------------
// NEBULA DEFINES/VARS
//
bool Nebula_sexp_used = false;
ubyte Neb2_fog_color[3] = { 0,0,0 };
// #define NEB2_THUMBNAIL
/*
3D CARDS THAT FOG PROPERLY
Voodoo1
Voodoo2
G200
TNT
3D CARDS THAT DON'T FOG PROPERLY
Permedia2
AccelStar II
*/
// if nebula rendering is active (DCF stuff - not mission specific)
int Neb2_render_mode = NEB2_RENDER_NONE;
SCP_vector<poof_info> Poof_info;
float Poof_dist_threshold;
vec3d Poof_last_gen_pos;
SCP_vector<float> Poof_accum;
float Poof_density_multiplier;
const float UPKEEP_DIST_MULT = 1.2f;
const float PROBABLY_TOO_MANY_POOFS = 100000.0f;
// bit array of neb2 poofs
std::unique_ptr<ubyte[]> Neb2_poof_flags;
// array of neb2 bitmaps
SCP_vector<SCP_string> Neb2_bitmap_filenames;
// texture to use for this level
char Neb2_texture_name[MAX_FILENAME_LEN] = "";
float max_rotation = 3.75f;
float neb2_flash_fade = 0.3f;
// stats
int pneb_tried = 0; // total pnebs tried to render
int pneb_tossed_alpha = 0; // pnebs tossed because of alpha optimization
int pneb_tossed_dot = 0; // pnebs tossed because of dot product
int pneb_tossed_off = 0; // pnebs tossed because of being offscree
int neb_tried = 0; // total nebs tried
int neb_tossed_alpha = 0; // nebs tossed because of alpha
int neb_tossed_dot = 0; // nebs tossed because of dot product
int neb_tossed_count = 0; // nebs tossed because of max render count
// the AWACS suppression level for the nebula
float Neb2_awacs = -1.0f;
// Deprecated! Only used for compatibility
bool Neb2_fog_save_legacy_values = false;
float Neb2_fog_legacy_near_mult = 1.0f;
float Neb2_fog_legacy_far_mult = 1.0f;
// The visual render distance multipliers for the nebula
float Neb2_fog_near_distance = 10.0f;
float Neb2_fog_1000m_visibility = 0.00464158883f;
float Neb2_fog_skybox_clip_distance = Default_max_draw_distance;
float Neb2_fog_clip_distance = Default_max_draw_distance;
float Neb2_fog_visibility_trail = 1.0f;
float Neb2_fog_visibility_thruster = 1.5f;
float Neb2_fog_visibility_weapon = 1.3f;
float Neb2_fog_visibility_shield = 1.2f;
float Neb2_fog_visibility_glowpoint = 1.2f;
float Neb2_fog_visibility_beam_const = 4.0f;
float Neb2_fog_visibility_beam_scaled_factor = 0.1f;
float Neb2_fog_visibility_particle_const = 1.0f;
float Neb2_fog_visibility_particle_scaled_factor = 1.0f / 12.0f;
float Neb2_fog_visibility_shockwave = 2.5f;
float Neb2_fog_visibility_fireball_const = 1.2f;
float Neb2_fog_visibility_fireball_scaled_factor = 1.0f / 12.0f;
SCP_vector<poof> Neb2_poofs;
int Neb2_background_color[3] = {0, 0, 255}; // rgb background color (used for lame rendering)
const SCP_vector<std::pair<int, std::pair<const char*, int>>> DetailLevelValues = {{ 0, {"Minimum", 1680}},
{ 1, {"Low", 1160}},
{ 2, {"Medium", 1161}},
{ 3, {"High", 1162}},
{ 4, {"Ultra", 1721}}};
static void parse_nebula_detail_func()
{
constexpr int num_detail_presets = static_cast<int>(DefaultDetailPreset::Num_detail_presets);
int value[num_detail_presets];
stuff_int_list(value, num_detail_presets, ParseLookupType::RAW_INTEGER_TYPE);
for (int i = 0; i < num_detail_presets; i++) {
if (value[i] < 0 || value[i] > MAX_DETAIL_VALUE) {
error_display(0, "%i is an invalid detail level value!", value[i]);
} else {
change_default_detail_level(static_cast<DefaultDetailPreset>(i), DetailSetting::NebulaDetail, value[i]);
}
}
}
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
const auto NebulaDetailOption __UNUSED = options::OptionBuilder<int>("Graphics.NebulaDetail",
std::pair<const char*, int>{"Nebula Detail", 1361},
std::pair<const char*, int>{"Detail level of nebulas", 1697})
.category(std::make_pair("Graphics", 1825))
.values(DetailLevelValues)
.default_func([]() { return Detail.nebula_detail; })
.importance(7)
.change_listener([](int val, bool) {
Detail.nebula_detail = val;
return true;
})
.flags({options::OptionFlags::RetailBuiltinOption})
.parser(parse_nebula_detail_func)
.finish();
// --------------------------------------------------------------------------------------------------------
// NEBULA FORWARD DECLARATIONS
//
// do a pre-render of the background nebula
void neb2_pre_render(camid cid);
// fill in the position of the eye for this frame
void neb2_get_eye_pos(vec3d *eye_vector);
// fill in the eye orient for this frame
void neb2_get_eye_orient(matrix *eye_matrix);
// --------------------------------------------------------------------------------------------------------
// NEBULA FUNCTIONS
//
static poof_info* get_nebula_poof_pointer(char* nebula_name)
{
for (size_t i = 0; i < Poof_info.size(); i++) {
if (!stricmp(nebula_name, Poof_info[i].name)) {
return &Poof_info[i];
}
}
// Didn't find anything.
return nullptr;
}
void parse_nebula_table(const char* filename)
{
char name[MAX_FILENAME_LEN];
try
{
// read in the nebula.tbl
read_file_text(filename, CF_TYPE_TABLES);
reset_parse();
// allow modular tables to not define bitmaps
bool skip_background_bitmaps = false;
if (Parsing_modular_table && (check_for_string("+Poof:") || check_for_string("$Name:")))
skip_background_bitmaps = true;
// background bitmaps
while (!skip_background_bitmaps && !optional_string("#end")) {
// nebula
optional_string("+Nebula:");
stuff_string(name, F_NAME, MAX_FILENAME_LEN);
if (!generic_bitmap_exists(name) && !generic_anim_exists(name)) {
error_display(0, "Nebula bitmap %s was not found!, skipping!", name);
continue;
}
Neb2_bitmap_filenames.push_back(name);
}
// allow modular tables to not define poofs
if (Parsing_modular_table && check_for_eof())
return;
// poofs (see also check_for_string above)
while (required_string_one_of(3, "#end", "+Poof:", "$Name:")) {
bool create_if_not_found = true;
poof_info pooft;
poof_info* poofp;
bool poof_new = true;
if (optional_string("+Poof:")) { // retail style
stuff_string(name, F_NAME, MAX_FILENAME_LEN);
strcpy_s(pooft.bitmap_filename, name);
strcpy_s(pooft.name, name);
if (!generic_bitmap_exists(pooft.bitmap_filename) && !generic_anim_exists(pooft.bitmap_filename)) {
error_display(0, "Bitmap defined for nebula poof %s was not found. Skipping!", pooft.name);
continue;
}
generic_anim_init(&pooft.bitmap, name);
Poof_info.push_back(pooft);
} else if (optional_string("$Name:")) { // new style
stuff_string(pooft.name, F_NAME, NAME_LENGTH);
if (optional_string("+nocreate")) {
if (!Parsing_modular_table) {
Warning(LOCATION, "+nocreate flag used for nebula poof in non-modular table\n");
}
create_if_not_found = false;
}
// Does this poof exist already?
// If so, load this new info into it
poofp = get_nebula_poof_pointer(pooft.name);
if (poofp != nullptr) {
if (!Parsing_modular_table) {
error_display(1,
"Error: Nebula Poof %s already exists. All nebula poof names must be unique.",
pooft.name);
}
poof_new = false;
} else {
// Don't create poof if it has +nocreate and is in a modular table.
if (!create_if_not_found && Parsing_modular_table) {
if (!skip_to_start_of_string_either("$Name:", "#end")) {
error_display(1, "Missing [#end] or [$Name] after nebula poof %s", pooft.name);
}
continue;
}
Poof_info.push_back(pooft);
poofp = &Poof_info[Poof_info.size() - 1];
}
if (poof_new) {
required_string("$Bitmap:");
stuff_string(name, F_NAME, MAX_FILENAME_LEN);
strcpy_s(poofp->bitmap_filename, name);
generic_anim_init(&poofp->bitmap, name);
} else {
if (optional_string("$Bitmap:")) {
stuff_string(name, F_NAME, MAX_FILENAME_LEN);
strcpy_s(poofp->bitmap_filename, name);
generic_anim_init(&poofp->bitmap, name);
}
}
//We can't skip here because we'd have to back out the entire poof from the vector-Mjn
if (!generic_bitmap_exists(poofp->bitmap_filename) && !generic_anim_exists(poofp->bitmap_filename))
error_display(0, "Bitmap defined for nebula poof %s was not found!", poofp->name);
if (optional_string("$Scale:"))
poofp->scale = ::util::ParsedRandomFloatRange::parseRandomRange(0.01f, 100000.0f);
if (optional_string("$Density:")) {
stuff_float(&poofp->density);
if (poofp->density <= 0.0f) {
Warning(LOCATION, "Poof %s must have a density greater than 0.", poofp->name);
poofp->density = 150.0f;
}
poofp->density = 1 / (poofp->density * poofp->density * poofp->density);
}
if (optional_string("$Alignment:")) {
SCP_string type;
stuff_string(type, F_NAME);
if (!stricmp(type.c_str(), "VERTICAL"))
poofp->alignment = vmd_y_vector;
else
Warning(LOCATION, "Unrecognized alignment type '%s' for nebula poof %s", type.c_str(), poofp->name);
}
if (optional_string("$Rotation:"))
poofp->rotation = util::ParsedRandomFloatRange::parseRandomRange(-1000.0f, 1000.0f);
if (optional_string("$View Distance:")) {
stuff_float(&poofp->view_dist);
if (poofp->view_dist < 0.0f) {
Warning(LOCATION, "Poof %s must have a positive view distance.", poofp->name);
poofp->view_dist = 360.f;
}
float volume = PI * 4 / 3 * (poofp->view_dist * poofp->view_dist * poofp->view_dist);
if (volume * poofp->density > PROBABLY_TOO_MANY_POOFS) {
Warning(LOCATION, "Poof %s will have over 100,000 poofs on the field at once, and could cause serious performance issues. "
"Remember that as $Density decreases and $View Distance increases, the total number of "
"poofs increases exponentially.",
poofp->name);
}
}
if (optional_string("$Alpha:")) {
poofp->alpha = util::ParsedRandomFloatRange::parseRandomRange(0.0f, 1.0f);
}
}
}
}
catch (const parse::ParseException& e)
{
mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", "nebula.tbl", e.what()));
return;
}
}
// initialize neb2 stuff at game startup
void neb2_init()
{
// first parse the default table
parse_nebula_table("nebula.tbl");
// parse any modular tables
parse_modular_table("*-neb.tbm", parse_nebula_table);
// align Poof_accum with Poof_info
Poof_accum.resize(Poof_info.size());
// set up bit string
Neb2_poof_flags = std::make_unique<ubyte[]>(calculate_num_bytes(Poof_info.size()));
clear_all_bits(Neb2_poof_flags.get(), Poof_info.size());
}
// set the bits for poofs from a list of poof names
void neb2_set_poof_bits(const SCP_vector<SCP_string>& list)
{
clear_all_bits(Neb2_poof_flags.get(), Poof_info.size()); //Make absolutely sure flags are zero'd before we start adding to it-Mjn
for (const SCP_string& thisPoof : list) {
for (size_t i = 0; i < Poof_info.size(); i++) {
if (lcase_equal(Poof_info[i].name, thisPoof)) {
set_bit(Neb2_poof_flags.get(), i);
}
}
}
}
bool poof_is_used(size_t idx) {
return get_bit(Neb2_poof_flags.get(), idx) != 0;
}
void neb2_get_fog_color(ubyte *r, ubyte *g, ubyte *b)
{
if (r) *r = Neb2_fog_color[0];
if (g) *g = Neb2_fog_color[1];
if (b) *b = Neb2_fog_color[2];
}
void neb2_pre_level_init()
{
Neb2_awacs = -1.0f;
Neb2_fog_save_legacy_values = false;
Neb2_fog_legacy_near_mult = 1.0f;
Neb2_fog_legacy_far_mult = 1.0f;
Neb2_fog_near_distance = 10.0f;
Neb2_fog_1000m_visibility = 0.00464158883f;
Neb2_fog_skybox_clip_distance = Max_draw_distance;
Neb2_fog_clip_distance = Max_draw_distance;
strcpy_s(Neb2_texture_name, "");
clear_all_bits(Neb2_poof_flags.get(), Poof_info.size());
strcpy_s(Mission_parse_storm_name, "none");
}
void neb2_level_init()
{
Nebula_sexp_used = false;
}
float nNf_near, nNf_density;
void neb2_poof_setup()
{
if (!any_bits_set(Neb2_poof_flags.get(), Poof_info.size()))
return;
// make the total density of poofs be the average of all poofs, and each poofs density is its relative proportion compared to others
// this way we maintain the retail way of not affecting total density by having more poof types
float Poof_density_sum_square = 0.0f;
float Poof_density_sum = 0.0f;
// also determine the minimum distance before re-triggering a poof upkeep
Poof_dist_threshold = 9999.0f;
for (size_t i = 0; i < Poof_info.size(); i++) {
if (poof_is_used(i)) {
Poof_density_sum_square += Poof_info[i].density * Poof_info[i].density;
Poof_density_sum += Poof_info[i].density;
float dist_threshold = Poof_info[i].view_dist * (UPKEEP_DIST_MULT - 1.0f);
if (dist_threshold < Poof_dist_threshold)
Poof_dist_threshold = dist_threshold;
}
}
Poof_density_multiplier = Poof_density_sum_square / (Poof_density_sum * Poof_density_sum);
Poof_density_multiplier *= (Detail.nebula_detail + 0.5f) / (MAX_DETAIL_VALUE + 0.5f); // scale the poofs down based on detail level
}
void neb2_generate_fog_color(const char *fog_color_palette, ubyte fog_color[])
{
// Set a default colour just in case something goes wrong
fog_color[0] = 30;
fog_color[1] = 52;
fog_color[2] = 157;
if (!fog_color_palette || !strlen(fog_color_palette))
return;
auto fog_data = new ubyte[768];
if (pcx_read_header(fog_color_palette, nullptr, nullptr, nullptr, nullptr, fog_data) == PCX_ERROR_NONE) {
// based on the palette, get an average color value (this doesn't really account for actual pixel usage though)
ushort r = 0, g = 0, b = 0, pcount = 0;
for (int idx = 0; idx < 768; idx += 3) {
if (fog_data[idx] || fog_data[idx+1] || fog_data[idx+2]) {
r = r + fog_data[idx];
g = g + fog_data[idx+1];
b = b + fog_data[idx+2];
pcount++;
}
}
if (pcount > 0) {
fog_color[0] = (ubyte)(r / pcount);
fog_color[1] = (ubyte)(g / pcount);
fog_color[2] = (ubyte)(b / pcount);
} else {
// it's just black
fog_color[0] = fog_color[1] = fog_color[2] = 0;
}
}
// done, now free up the palette data
delete[] fog_data;
}
// initialize nebula stuff - call from game_post_level_init(), so the mission has been loaded
void neb2_post_level_init(bool fog_color_override)
{
// standalone servers can bail here
if (Game_mode & GM_STANDALONE_SERVER) {
return;
}
// Skip actual rendering if we're in FRED.
if (Fred_running)
{
Neb2_render_mode = NEB2_RENDER_NONE;
return;
}
// if the mission is not a fullneb mission, skip
if ( !((The_mission.flags[Mission::Mission_Flags::Fullneb]) || Nebula_sexp_used) ) {
Neb2_render_mode = NEB2_RENDER_NONE;
Neb2_awacs = -1.0f;
return;
}
// OK, lets try something a bit more interesting
if (!fog_color_override && strlen(Neb2_texture_name)) {
neb2_generate_fog_color(Neb2_texture_name, Neb2_fog_color);
}
Neb2_render_mode = NEB2_RENDER_HTL;
// load in all nebula bitmaps
for (poof_info &pinfo : Poof_info) {
if (pinfo.bitmap.first_frame < 0) {
if (generic_anim_load(&pinfo.bitmap)) {
// fall back to non-animated type
pinfo.bitmap.first_frame = bm_load(pinfo.bitmap_filename);
if (pinfo.bitmap.first_frame >= 0) {
pinfo.bitmap.num_frames = 1;
pinfo.bitmap.total_time = 1.0f;
} else {
mprintf(("Could not find a usable bitmap for nebula poof '%s'!\n", pinfo.name));
Warning(LOCATION, "Could not find a usable bitmap (%s) for nebula poof '%s'!\n", pinfo.bitmap_filename, pinfo.name);
}
}
}
}
pneb_tried = 0;
pneb_tossed_alpha = 0;
pneb_tossed_dot = 0;
neb_tried = 0;
neb_tossed_alpha = 0;
neb_tossed_dot = 0;
neb_tossed_count = 0;
// if we are going to use fullneb, but aren't fullneb yet, then be sure to reset our mode
if ( !(The_mission.flags[Mission::Mission_Flags::Fullneb]) ) {
Neb2_render_mode = NEB2_RENDER_NONE;
Neb2_awacs = -1.0f;
}
// set the mission fog near dist and density
neb2_get_adjusted_fog_values(&nNf_near, &nNf_density);
for (float& accum : Poof_accum)
accum = 0.0f;
// a bit awkward but this will force a full sphere gen
vm_vec_make(&Poof_last_gen_pos, 999999.0f, 999999.0f, 999999.0f);
neb2_poof_setup();
}
// shutdown nebula stuff
void neb2_level_close()
{
// standalone servers can bail here
if (Game_mode & GM_STANDALONE_SERVER) {
return;
}
// if the mission is not a fullneb mission, skip
if ( !((The_mission.flags[Mission::Mission_Flags::Fullneb]) || Nebula_sexp_used) ) {
return;
}
// unload all nebula bitmaps
for (poof_info& pinfo : Poof_info) {
if (pinfo.bitmap.first_frame >= 0) {
bm_release(pinfo.bitmap.first_frame);
pinfo.bitmap.first_frame = -1;
}
}
// clear da poofs
Neb2_poofs.clear();
// unflag the mission as being fullneb so stuff doesn't fog in the techdata room :D
The_mission.flags.remove(Mission::Mission_Flags::Fullneb);
}
// call before beginning all rendering
void neb2_render_setup(camid cid)
{
GR_DEBUG_SCOPE("Nebula Setup");
TRACE_SCOPE(tracing::SetupNebula);
// standalone servers can bail here
if (Game_mode & GM_STANDALONE_SERVER) {
return;
}
// if the mission is not a fullneb mission, skip
if ( !(The_mission.flags[Mission::Mission_Flags::Fullneb]) ) {
return;
}
if (Neb2_render_mode == NEB2_RENDER_HTL) {
// RT The background needs to be the same colour as the fog and this seems
// to be the ideal place to do it
ubyte tr = gr_screen.current_clear_color.red;
ubyte tg = gr_screen.current_clear_color.green;
ubyte tb = gr_screen.current_clear_color.blue;
neb2_get_fog_color(
&gr_screen.current_clear_color.red,
&gr_screen.current_clear_color.green,
&gr_screen.current_clear_color.blue);
gr_clear();
gr_screen.current_clear_color.red = tr;
gr_screen.current_clear_color.green = tg;
gr_screen.current_clear_color.blue = tb;
return;
}
// pre-render the real background nebula
neb2_pre_render(cid);
}
// level paging code
void neb2_page_in()
{
// load in all nebula bitmaps
if ( (The_mission.flags[Mission::Mission_Flags::Fullneb]) || Nebula_sexp_used ) {
for (size_t idx = 0; idx < Poof_info.size(); idx++) {
if (Poof_info[idx].bitmap.first_frame >= 0 && poof_is_used(idx)) {
bm_page_in_texture(Poof_info[idx].bitmap.first_frame);
}
}
}
}
// should we not render this object because its obscured by the nebula?
int neb_skip_opt = 0;
DCF(neb_skip, "Toggles culling of objects obscured by nebula")
{
neb_skip_opt = !neb_skip_opt;
if (neb_skip_opt) {
dc_printf("Using neb object skipping!\n");
} else {
dc_printf("Not using neb object skipping!\n");
}
}
int neb2_skip_render(object *objp, float z_depth)
{
float fog_near, fog_density;
// if we're never skipping
if (!neb_skip_opt) {
return 0;
}
// get near and far fog values based upon object type and rendering mode
neb2_get_adjusted_fog_values(&fog_near, &fog_density);
float fog = pow(fog_density, z_depth - fog_near + objp->radius);
// by object type
switch( objp->type ) {
// some objects we always render
case OBJ_SHOCKWAVE:
case OBJ_JUMP_NODE:
case OBJ_NONE:
case OBJ_GHOST:
case OBJ_BEAM:
case OBJ_WAYPOINT:
return 0;
// any weapon over 500 meters away
// Use the "far" distance multiplier here
case OBJ_WEAPON:
if (fog < 0.05f) {
return 1;
}
break;
// any ship or raw pof less than 3% visible at their closest point
case OBJ_RAW_POF:
case OBJ_PROP:
case OBJ_SHIP:
if (fog < 0.03f)
return 1;
break;
// any fireball over the fog limit for small ships
case OBJ_FIREBALL:
return 0;
break;
// any debris over the fog limit for small ships
case OBJ_DEBRIS:
return 0;
break;
// any asteroid less than 3% visible at their closest point
case OBJ_ASTEROID:
if (fog < 0.03f)
return 1;
break;
// hmmm. unknown object type - should probably let it through
default:
Int3();
return 0;
}
return 0;
}
// extend LOD
float neb2_get_lod_scale(int objnum)
{
ship *shipp;
ship_info *sip;
// bogus
if ( (objnum < 0)
|| (objnum >= MAX_OBJECTS)
|| (Objects[objnum].type != OBJ_SHIP)
|| (Objects[objnum].instance < 0)
|| (Objects[objnum].instance >= MAX_SHIPS)) {
return 1.0f;
}
shipp = &Ships[Objects[objnum].instance];
sip = &Ship_info[shipp->ship_info_index];
// small ship?
if (sip->is_small_ship()) {
return 1.8f;
} else if (sip->is_big_ship()) {
return 1.4f;
}
// hmm
return 1.0f;
}
// --------------------------------------------------------------------------------------------------------
// NEBULA FORWARD DEFINITIONS
//
// return the alpha the passed poof should be rendered with, for a 2 shell nebula
float neb2_get_alpha_2shell(float alpha, float inner_radius, float outer_radius, float magic_num, vec3d *v)
{
float dist;
vec3d eye_pos;
// get the eye position
neb2_get_eye_pos(&eye_pos);
// determine what alpha to draw this bitmap with
// higher alpha the closer the bitmap gets to the eye
dist = vm_vec_dist_quick(&eye_pos, v);
// if the point is inside the inner radius, alpha is based on distance to the player's eye,
// becoming more transparent as it gets close
if (dist <= inner_radius) {
// alpha per meter between the magic # and the inner radius
alpha = alpha / (inner_radius - magic_num);
// above value times the # of meters away we are
alpha *= (dist - magic_num);
return alpha < 0.0f ? 0.0f : alpha;
}
// if the point is outside the inner radius, it starts out as completely transparent at max
// outer radius, and becomes more opaque as it moves towards inner radius
else if (dist <= outer_radius) {
// alpha per meter between the outer radius and the inner radius
alpha = alpha / (outer_radius - inner_radius);
// above value times the range between the outer radius and the poof
return alpha < 0.0f ? 0.0f : alpha * (outer_radius - dist);
}
// otherwise transparent
return 0.0f;
}
// Calculate the alpha multiplier for a poof type. This is used for fading in and out
void neb2_calc_poof_fades() {
for (size_t i = 0; i < Poof_info.size(); i++) {
poof_info* pinfo = &Poof_info[i];
if (pinfo->fade_duration > -1) {
// Initialize variables
if (pinfo->fade_start == TIMESTAMP::invalid()) {
pinfo->fade_start = _timestamp();
}
// Calculate the elapsed time in milliseconds
auto elapsedTime = timestamp_since(pinfo->fade_start);
// fade duration of 0 can be reasonably set to 1 to prevent divide by 0
if (pinfo->fade_duration == 0) {
pinfo->fade_duration = 1;
}
if (pinfo->fade_in) {
// Make sure to enable this poof type if we're fading it in
if (pinfo->fade_multiplier < 0) {
set_bit(Neb2_poof_flags.get(), i);
neb2_poof_setup();
}
pinfo->fade_multiplier = 0.0f + ((float)elapsedTime / pinfo->fade_duration);
} else {
pinfo->fade_multiplier = 1.0f - ((float)elapsedTime / pinfo->fade_duration);
}
// if we're finished then reset pf_info
if ((pinfo->fade_multiplier >= 1.0f && pinfo->fade_in) || (pinfo->fade_multiplier <= 0.0f && !pinfo->fade_in)) {
// turn off any faded out poof types
if (!pinfo->fade_in) {
clear_bit(Neb2_poof_flags.get(), i);
neb2_poof_setup();
}
// reset the values to default
pinfo->fade_duration = -1;
pinfo->fade_start = TIMESTAMP::invalid();
pinfo->fade_in = true;
pinfo->fade_multiplier = -1.0f;
}
}
}
}
// -------------------------------------------------------------------------------------------------
// WACKY LOCAL PLAYER NEBULA STUFF
//
void neb2_toggle_poof(int poof_idx, bool enabling)
{
if (enabling)
set_bit(Neb2_poof_flags.get(), poof_idx);
else
clear_bit(Neb2_poof_flags.get(), poof_idx);
}
void neb2_toggle_poof_finalize()
{
Neb2_poofs.clear();
// a bit awkward but this will force a full sphere gen
vm_vec_make(&Poof_last_gen_pos, 999999.0f, 999999.0f, 999999.0f);
neb2_poof_setup();
}
void neb2_fade_poof(int poof_idx, int time, bool type)
{
poof_info* pinfo = &Poof_info[poof_idx];
pinfo->fade_duration = time;
pinfo->fade_in = type;
}
void new_poof(size_t poof_info_idx, vec3d* pos) {
poof new_poof;
poof_info* pinfo = &Poof_info[poof_info_idx];
new_poof.poof_info_index = poof_info_idx;
new_poof.flash = 0;
new_poof.radius = pinfo->scale.next();
new_poof.pt = *pos;
new_poof.rot_speed = fl_radians(pinfo->rotation.next());
new_poof.alpha = pinfo->alpha.next();
new_poof.anim_time = frand_range(0.0f, pinfo->bitmap.total_time);
if (pinfo->alignment == vmd_zero_vector)
vm_vec_rand_vec(&new_poof.up_vec);
else
new_poof.up_vec = pinfo->alignment;
Neb2_poofs.push_back(new_poof);
}
static uint neb_rand_seed = 0;
void upkeep_poofs()
{
vec3d eye_pos;
neb2_get_eye_pos(&eye_pos);
// cull distant poofs
if (!Neb2_poofs.empty()) {
for (size_t i = 0; i < Neb2_poofs.size();) {
// If the poof type is not used or if the poof is too far then cull it
if (!poof_is_used(Neb2_poofs[i].poof_info_index) || (
vm_vec_dist(&Neb2_poofs[i].pt, &eye_pos) >
Poof_info[Neb2_poofs[i].poof_info_index].view_dist * UPKEEP_DIST_MULT)) {
Neb2_poofs[i] = Neb2_poofs.back();
Neb2_poofs.pop_back();
}
else // if we needed to cull we should not advance because we just moved a new poof into this spot
i++;
}
}
neb_rand_seed = 0;
// make new poofs
for (size_t i = 0; i < Poof_info.size(); i++) {
if (!poof_is_used(i))
continue;
poof_info* pinfo = &Poof_info[i];
float gen_side_length = (pinfo->view_dist * UPKEEP_DIST_MULT) * 2;
float gen_density = pinfo->density * Poof_density_multiplier;
float poofs_to_gen = gen_side_length * gen_side_length * gen_side_length * gen_density;
// store the fractional part, take the integer part
Poof_accum[i] = modff(Poof_accum[i] + (poofs_to_gen), &poofs_to_gen);
for (int j = 0; j < poofs_to_gen; j++) {
vec3d pos = eye_pos;
vec3d offset = eye_pos / gen_side_length;
vec3d rand_pos = vm_well_distributed_rand_vec(neb_rand_seed, &offset);
neb_rand_seed++;
rand_pos.xyz.x *= gen_side_length / 2;
rand_pos.xyz.y *= gen_side_length / 2;
rand_pos.xyz.z *= gen_side_length / 2;
pos += rand_pos;
// we generated poofs in a cube, now keep only those that are within the view sphere, and weren't within the last view sphere
// not terribly efficient but very simple
if (vm_vec_dist(&eye_pos, &pos) <= (gen_side_length / 2) &&
vm_vec_dist(&Poof_last_gen_pos, &pos) > (gen_side_length / 2))
new_poof(i, &pos);
}
}
}
void neb2_render_poofs()
{
GR_DEBUG_SCOPE("Nebula render player");
TRACE_SCOPE(tracing::DrawPoofs);
vertex p, ptemp;
float alpha;
vec3d eye_pos;
matrix eye_orient;
// standalone servers can bail here
if (Game_mode & GM_STANDALONE_SERVER) {
return;
}
// if the mission is not a fullneb mission, skip
if (!(The_mission.flags[Mission::Mission_Flags::Fullneb])) {
return;
}
memset(&p, 0, sizeof(p));
memset(&ptemp, 0, sizeof(ptemp));
// upkeep poof fade stuff
neb2_calc_poof_fades();
// get eye position and orientation
neb2_get_eye_pos(&eye_pos);
neb2_get_eye_orient(&eye_orient);
// maybe swap stuff around if the player crossed the dist threshold
if (vm_vec_dist(&eye_pos, &Poof_last_gen_pos) > Poof_dist_threshold) {
upkeep_poofs();
Poof_last_gen_pos = eye_pos;
}
// if we've switched nebula rendering off
if (Neb2_render_mode == NEB2_RENDER_NONE) {
return;
}
// render the nebula
for (poof &pf : Neb2_poofs) {
poof_info* pinfo = &Poof_info[pf.poof_info_index];
// Miss this one out if the id is -1
if (pinfo->bitmap.first_frame < 0)
continue;
// It's possible for some faded-out poofs to get rendered after a poof type has finished a fade out
// and reset it's multiplier but before the poof is culled from the poof vector. So this small safety
// prevents them from flickering.
if (!poof_is_used(pf.poof_info_index)) {
continue;
}
// do animation upkeep
int framenum = 0;
if (pinfo->bitmap.num_frames > 1) {
pf.anim_time += flFrametime;
framenum = bm_get_anim_frame(pinfo->bitmap.first_frame, pf.anim_time, pinfo->bitmap.total_time, true);
}
// generate the bitmap orient
// If the bitmap is large and distant and points in the view fvec direction (like retail poofs do) this looks good when moving,
// but bad when you rotate (since the bitmaps rotate in place with you, which looks weird).
// Conversely, if the bitmap is close and small (like retail size-ish) and points at the view position, it looks good
// when rotating (since the bitmaps remain static) but bad when moving (as they rotate in place to continue pointing at you)
//
// So blend between the two styles based on promixity and size, since distant (relative to their size) bitmaps
// are more affected by rotating than moving and close bitmaps are vice versa
// We will scale the bitmap direction from the view position to "off to infinity" in the negative view fvec direction - Asteroth
matrix orient;
vec3d view_pos;
{
float scalar = -1 / powf((vm_vec_dist(&eye_pos, &pf.pt) / (10 * pf.radius)), 3.f);
if (pinfo->alignment != vmd_zero_vector)
scalar = 0.0f;
vm_vec_scale_add(&view_pos, &eye_pos, &eye_orient.vec.fvec, scalar);
view_pos -= pf.pt;
if (pinfo->alignment != vmd_zero_vector)
vm_project_point_onto_plane(&view_pos, &view_pos, &pinfo->alignment, &vmd_zero_vector);
vm_vec_normalize(&view_pos);
vm_vector_2_matrix_norm(&orient, &view_pos, &pf.up_vec, nullptr);
}
// update the poof's up vector to be perpindicular to the camera and also rotated by however much its rotating
vec3d poof_direction;
vm_vec_normalized_dir(&poof_direction, &pf.pt, &eye_pos);
vm_project_point_onto_plane(&pf.up_vec, &pf.up_vec, &view_pos, &vmd_zero_vector);
vm_vec_normalize(&pf.up_vec);
vm_rot_point_around_line(&pf.up_vec, &pf.up_vec, pf.rot_speed * flFrametime, &vmd_zero_vector, &view_pos);
// optimization 1 - don't draw backfacing poly's
if (vm_vec_dot_to_point(&eye_orient.vec.fvec, &eye_pos, &pf.pt) <= 0.0f)
continue;
// get the proper alpha value
alpha = neb2_get_alpha_2shell(pf.alpha, pf.radius, pinfo->view_dist, pf.radius/4, &pf.pt);
if (pinfo->fade_duration > -1) {
alpha = alpha * pinfo->fade_multiplier;
}
// optimization 2 - don't draw 0.0f or less poly's
// this amounts to big savings
if (alpha <= 0.0f)
continue;
// render!
batching_add_volume_polygon(pinfo->bitmap.first_frame + framenum, &pf.pt, &orient, pf.radius, pf.radius, alpha);
}
// gr_set_color_fast(&Color_bright_red);
// gr_printf(30, 100, "Area %.3f", total_area);
#ifdef NEB2_THUMBNAIL
extern int tbmap;
if (tbmap != -1) {
gr_set_bitmap(tbmap);
gr_bitmap(0, 0);
}
#endif
}
// This version of the function allows for global adjustment to fog values
void neb2_get_adjusted_fog_values(float *fnear, float *fdensity)
{
// There was code here that adjusted distanced based on object type, but at the time of reworking this, the code was effectively dead and never used.
*fnear = Neb2_fog_near_distance;
if (fdensity != nullptr)
*fdensity = powf(Neb2_fog_1000m_visibility, 1.f / 1000.f);
}
// given a position, returns 0 - 1 the fog visibility of that position, 0 = completely obscured
// distance_mult will multiply the result, use for things that can be obscured but can 'shine through' the nebula more than normal
float neb2_get_fog_visibility(const vec3d *pos, float distance_mult)
{
float pct;
// get the fog pct
pct = powf(nNf_density, (vm_vec_dist(&Eye_position, pos) - nNf_near) / distance_mult);
CLAMP(pct, 0.0f, 1.0f);
return pct;
}
bool nebula_handle_alpha(float& alpha, const vec3d* pos, float distance_mult) {
bool bHasNebula = false;
float fAlphaMult = 1.0f;
if (The_mission.flags[Mission::Mission_Flags::Fullneb])
{
fAlphaMult *= neb2_get_fog_visibility(pos, distance_mult);
bHasNebula = true;
}
if (The_mission.volumetrics)
{
fAlphaMult *= The_mission.volumetrics->getAlphaToPos(*pos, distance_mult);
bHasNebula = true;
}
alpha *= fAlphaMult;
return bHasNebula;
}
// fogging stuff --------------------------------------------------------------------
// do a pre-render of the background nebula
#define ESIZE 32
ubyte tpixels[ESIZE * ESIZE * 4]; // for 32 bits
int last_esize = -1;
int this_esize = ESIZE;
float ex_scale, ey_scale;
int tbmap = -1;
// UnknownPlayer : Contained herein, the origins of the nebula rendering bug!
// I am really not entirely sure what this code achieves, but the old
// D3D calls were the cause of the nebula bug - they have been commented out.
// If you want to save some rendering time, I would suggest maybe kill this off.
// It doesn't use much, but it APPEARS to be fairly useless unless someone wants
// to enlighten me.
//
void neb2_pre_render(camid cid)
{
// if the mission is not a fullneb mission, skip
if (!(The_mission.flags[Mission::Mission_Flags::Fullneb])) {
return;
}
// bail early in lame and poly modes
if (Neb2_render_mode != NEB2_RENDER_POF) {
return;
}
// set the view clip
gr_screen.clip_width = this_esize;
gr_screen.clip_height = this_esize;
g3_start_frame(1); // Turn on zbuffering
g3_set_view(cid.getCamera());
gr_set_clip(0, 0, this_esize, this_esize);
// render the background properly
// hack - turn off nebula stuff
int neb_save = Neb2_render_mode;
Neb2_render_mode = NEB2_RENDER_NONE;
// draw background stuff nebula
stars_draw_background();
Neb2_render_mode = neb_save;
// grab the region
gr_get_region(0, this_esize, this_esize, (ubyte*)tpixels);
#ifdef NEB2_THUMBNAIL
if (tbmap == -1) {
tbmap = bm_create(16, this_esize, this_esize, tpixels, 0);
bm_lock(tbmap, 16, 0);
bm_unlock(tbmap);
}
#endif
// maybe do some swizzling
// end the frame
g3_end_frame();
gr_clear();
// if the size has changed between frames, make a new bitmap
if (this_esize != last_esize) {
last_esize = this_esize;
// recalculate ex_scale and ey_scale values for looking up color values
ex_scale = (float)this_esize / (float)gr_screen.max_w;
ey_scale = (float)this_esize / (float)gr_screen.max_h;
}
}
// fill in the position of the eye for this frame
void neb2_get_eye_pos(vec3d *eye_vector)
{
*eye_vector = Eye_position;
}
// fill in the eye orient for this frame
void neb2_get_eye_orient(matrix *eye_matrix)
{
*eye_matrix = Eye_matrix;
}
// nebula DCF functions ------------------------------------------------------
// TODO: With the new debug parser in place, most of these sub-commands can now be handled by neb2. This should clear up the DCF list a bit
DCF(neb2, "list nebula console commands")
{
// dc_printf("neb2_fog <X> <float> <float> : set near and far fog planes for ship type X\n");
// dc_printf("where X is an integer from 1 - 11\n");
// dc_printf("1 = cargo containers, 2 = fighters/bombers, 3 = cruisers\n");
// dc_printf("4 = freighters, 5 = capital ships, 6 = transports, 7 = support ships\n");
// dc_printf("8 = navbuoys, 9 = sentryguns, 10 = escape pods, 11 = background nebula polygons\n\n");
dc_printf("neb2_select : <int> <int> where the first # is the bitmap to be adjusting (0 through 5), and the second int is a 0 or 1, to turn off and on\n");
dc_printf("neb2_mode : switch between no nebula, polygon background, pof background, lame, or HTL rendering (0, 1, 2, 3 and 4 respectively)\n\n");
dc_printf("neb2_ff : flash fade/sec\n");
dc_printf("neb2_background : rgb background color\n");
dc_printf("neb2_fog_color : rgb fog color\n");
// dc_printf("neb2_fog_vals : display all the current settings for all above values\n");
}
DCF(neb2_select, "Enables/disables a poof bitmap")
{
int bmap;
bool val_b;
dc_stuff_int(&bmap);
if ( (bmap >= 0) && (bmap < static_cast<int>(Poof_info.size())) ) {
dc_stuff_boolean(&val_b);
val_b ? (set_bit(Neb2_poof_flags.get(), bmap)) : (clear_bit(Neb2_poof_flags.get(), bmap));
}
}
DCF(neb2_ff, "flash fade/sec")
{
dc_stuff_float(&neb2_flash_fade);
}
DCF(neb2_mode, "Switches nebula render modes")
{
int mode;
dc_stuff_int(&mode);
switch (mode) {
case NEB2_RENDER_NONE:
Neb2_render_mode = NEB2_RENDER_NONE;
break;
case NEB2_RENDER_POF:
Neb2_render_mode = NEB2_RENDER_POF;
stars_set_background_model(BACKGROUND_MODEL_FILENAME, "Eraseme3");
stars_set_background_orientation();
break;
case NEB2_RENDER_HTL:
Neb2_render_mode = NEB2_RENDER_HTL;
break;
}
}
DCF(neb2_background, "Sets the RGB background color (lame rendering)")
{
int r, g, b;
dc_stuff_int(&r);
dc_stuff_int(&g);
dc_stuff_int(&b);
Neb2_background_color[0] = r;
Neb2_background_color[1] = g;
Neb2_background_color[2] = b;
}
DCF(neb2_fog_color, "Sets the RGB fog color (HTL)")
{
ubyte r, g, b;
dc_stuff_ubyte(&r);
dc_stuff_ubyte(&g);
dc_stuff_ubyte(&b);
Neb2_fog_color[0] = r;
Neb2_fog_color[1] = g;
Neb2_fog_color[2] = b;
}