mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 17:52:08 -10:00
* 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]>
406 lines
11 KiB
C++
406 lines
11 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 <algorithm>
|
|
#include <list>
|
|
#include <vector>
|
|
|
|
#include "asteroid/asteroid.h"
|
|
#include "cmdline/cmdline.h"
|
|
#include "debris/debris.h"
|
|
#include "graphics/light.h"
|
|
#include "jumpnode/jumpnode.h"
|
|
#include "mission/missionparse.h"
|
|
#include "model/modelrender.h"
|
|
#include "nebula/neb.h"
|
|
#include "object/object.h"
|
|
#include "prop/prop.h"
|
|
#include "scripting/scripting.h"
|
|
#include "render/3d.h"
|
|
#include "render/batching.h"
|
|
#include "ship/ship.h"
|
|
#include "tracing/tracing.h"
|
|
#include "weapon/weapon.h"
|
|
#include "decals/decals.h"
|
|
#include "freespace.h"
|
|
#include "utils/modular_curves.h"
|
|
|
|
class sorted_obj
|
|
{
|
|
public:
|
|
object *obj; // a pointer to the original object
|
|
float z, min_z, max_z; // The object's z values relative to viewer
|
|
|
|
sorted_obj() :
|
|
obj(NULL), z(0.0f), min_z(0.0f), max_z(1.0f)
|
|
{
|
|
}
|
|
|
|
bool operator < (const sorted_obj &other) const;
|
|
};
|
|
|
|
inline bool sorted_obj::operator < (const sorted_obj &other) const
|
|
{
|
|
int model_num_a = -1;
|
|
int model_num_b = -1;
|
|
|
|
if ( obj->type == OBJ_SHIP ) {
|
|
ship_info *sip = &Ship_info[Ships[obj->instance].ship_info_index];
|
|
|
|
model_num_a = sip->model_num;
|
|
} else if ( obj->type == OBJ_WEAPON ){
|
|
weapon_info *wip;
|
|
|
|
wip = &Weapon_info[Weapons[obj->instance].weapon_info_index];
|
|
|
|
if ( wip->render_type == WRT_POF ) {
|
|
model_num_a = wip->model_num;
|
|
}
|
|
} else if ( obj->type == OBJ_DEBRIS ) {
|
|
debris *db;
|
|
|
|
db = &Debris[obj->instance];
|
|
model_num_a = db->model_num;
|
|
} else if ( obj->type == OBJ_ASTEROID ) {
|
|
asteroid *asp;
|
|
|
|
asp = &Asteroids[obj->instance];
|
|
model_num_a = Asteroid_info[asp->asteroid_type].subtypes[asp->asteroid_subtype].model_number;
|
|
} else if (obj->type == OBJ_RAW_POF) {
|
|
model_num_a = Pof_objects[obj->instance].model_num;
|
|
} else if (obj->type == OBJ_PROP) {
|
|
prop_info *pip = &Prop_info[Props[obj->instance]->prop_info_index];
|
|
model_num_a = pip->model_num;
|
|
}
|
|
|
|
if ( other.obj->type == OBJ_SHIP ) {
|
|
ship_info *sip = &Ship_info[Ships[other.obj->instance].ship_info_index];
|
|
|
|
model_num_b = sip->model_num;
|
|
} else if ( other.obj->type == OBJ_WEAPON ){
|
|
weapon_info *wip;
|
|
|
|
wip = &Weapon_info[Weapons[other.obj->instance].weapon_info_index];
|
|
|
|
if ( wip->render_type == WRT_POF ) {
|
|
model_num_b = wip->model_num;
|
|
}
|
|
} else if ( other.obj->type == OBJ_DEBRIS ) {
|
|
debris *db;
|
|
|
|
db = &Debris[other.obj->instance];
|
|
model_num_b = db->model_num;
|
|
} else if ( other.obj->type == OBJ_ASTEROID ) {
|
|
asteroid *asp;
|
|
|
|
asp = &Asteroids[other.obj->instance];
|
|
model_num_b = Asteroid_info[asp->asteroid_type].subtypes[asp->asteroid_subtype].model_number;
|
|
} else if (other.obj->type == OBJ_RAW_POF) {
|
|
model_num_b = Pof_objects[other.obj->instance].model_num;
|
|
} else if (other.obj->type == OBJ_PROP) {
|
|
prop_info* pip = &Prop_info[Props[other.obj->instance]->prop_info_index];
|
|
model_num_b = pip->model_num;
|
|
}
|
|
|
|
if ( model_num_a == model_num_b ) {
|
|
return (max_z > other.max_z);
|
|
}
|
|
|
|
return model_num_a < model_num_b;
|
|
}
|
|
|
|
|
|
SCP_vector<sorted_obj> Sorted_objects;
|
|
SCP_vector<object*> effect_ships;
|
|
SCP_vector<object*> transparent_objects;
|
|
bool object_had_transparency = false;
|
|
// Used to (fairly) quicky find the 8 extreme
|
|
// points around an object.
|
|
vec3d check_offsets[8] = {
|
|
{ { { -1.0f, -1.0f, -1.0f } } },
|
|
{ { { -1.0f, -1.0f, 1.0f } } },
|
|
{ { { -1.0f, 1.0f, -1.0f } } },
|
|
{ { { -1.0f, 1.0f, 1.0f } } },
|
|
{ { { 1.0f, -1.0f, -1.0f } } },
|
|
{ { { 1.0f, -1.0f, 1.0f } } },
|
|
{ { { 1.0f, 1.0f, -1.0f } } },
|
|
{ { { 1.0f, 1.0f, 1.0f } } }
|
|
};
|
|
|
|
// See if an object is in the view cone.
|
|
// Returns:
|
|
// 0 if object isn't in the view cone
|
|
// 1 if object is in cone
|
|
// This routine could possibly be optimized. Right now, for an
|
|
// offscreen object, it has to rotate 8 points to determine it's
|
|
// offscreen. Not the best considering we're looking at a sphere.
|
|
int obj_in_view_cone( object * objp )
|
|
{
|
|
int i;
|
|
vec3d tmp,pt;
|
|
ubyte codes;
|
|
|
|
// Center isn't in... are other points?
|
|
ubyte and_codes = 0xff;
|
|
|
|
float obj_size;
|
|
|
|
if (objp->type == OBJ_WEAPON && Weapon_info[Weapons[objp->instance].weapon_info_index].render_type == WRT_LASER) {
|
|
auto wp = &Weapons[objp->instance];
|
|
auto wip = &Weapon_info[wp->weapon_info_index];
|
|
float length_scalar = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_LENGTH_MULT, *wp, &wp->modular_curves_instance);
|
|
if (wip->laser_length_by_frametime) {
|
|
length_scalar *= flFrametime;
|
|
}
|
|
float radius_scalar = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_RADIUS_MULT, *wp, &wp->modular_curves_instance);
|
|
obj_size = (wip->laser_length * length_scalar) + (wip->laser_head_radius * radius_scalar);
|
|
} else {
|
|
obj_size = objp->radius;
|
|
}
|
|
|
|
for (i=0; i<8; i++ ) {
|
|
vm_vec_scale_add( &pt, &objp->pos, &check_offsets[i], obj_size );
|
|
codes=g3_rotate_vector(&tmp,&pt);
|
|
if ( !codes ) {
|
|
//mprintf(( "A point is inside, so render it.\n" ));
|
|
return 1; // this point is in, so return 1
|
|
}
|
|
and_codes &= codes;
|
|
}
|
|
|
|
if (and_codes) {
|
|
//mprintf(( "All points offscreen, so don't render it.\n" ));
|
|
return 0; //all points off screen
|
|
}
|
|
|
|
//mprintf(( "All points inside, so render it, but doesn't need clipping.\n" ));
|
|
return 1;
|
|
}
|
|
|
|
inline bool obj_render_is_model(object *obj)
|
|
{
|
|
return obj->type == OBJ_SHIP
|
|
|| (obj->type == OBJ_WEAPON
|
|
&& Weapon_info[Weapons[obj->instance].weapon_info_index].render_type == WRT_POF)
|
|
|| obj->type == OBJ_ASTEROID
|
|
|| obj->type == OBJ_DEBRIS
|
|
|| obj->type == OBJ_JUMP_NODE
|
|
|| obj->type == OBJ_RAW_POF
|
|
|| obj->type == OBJ_PROP;
|
|
}
|
|
|
|
// Are there reasons to hide objects base on distance?
|
|
bool is_full_nebula()
|
|
{
|
|
return (The_mission.flags[Mission::Mission_Flags::Fullneb]) && (Neb2_render_mode != NEB2_RENDER_NONE) && !Fred_running;
|
|
}
|
|
|
|
// Sorts all the objects by Z and renders them
|
|
void obj_render_all(const std::function<void(object*)>& render_function, bool *draw_viewer_last )
|
|
{
|
|
object *objp;
|
|
int i;
|
|
|
|
objp = Objects;
|
|
|
|
for (i=0;i<=Highest_object_index;i++,objp++) {
|
|
if ( (objp->type != OBJ_NONE) && (objp->flags[Object::Object_Flags::Renders]) ) {
|
|
objp->flags.remove(Object::Object_Flags::Was_rendered);
|
|
|
|
if ( obj_in_view_cone(objp) ) {
|
|
sorted_obj osp;
|
|
|
|
osp.obj = objp;
|
|
|
|
vec3d to_obj;
|
|
vm_vec_sub( &to_obj, &objp->pos, &Eye_position );
|
|
osp.z = vm_vec_dot( &Eye_matrix.vec.fvec, &to_obj );
|
|
/*
|
|
if ( objp->type == OBJ_SHOCKWAVE )
|
|
osp.z -= 2*objp->radius;
|
|
*/
|
|
// Make warp in effect draw after any ship in it
|
|
if ( objp->type == OBJ_FIREBALL ) {
|
|
//if ( fireball_is_warp(objp) ) {
|
|
osp.z -= 2*objp->radius;
|
|
//}
|
|
}
|
|
|
|
osp.min_z = osp.z - objp->radius;
|
|
osp.max_z = osp.z + objp->radius;
|
|
|
|
Sorted_objects.push_back(osp);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( Sorted_objects.empty() )
|
|
return;
|
|
|
|
std::sort(Sorted_objects.begin(), Sorted_objects.end());
|
|
|
|
gr_zbuffer_set( GR_ZBUFF_FULL );
|
|
|
|
bool full_neb = is_full_nebula();
|
|
bool c_viewer = (!Viewer_mode || (Viewer_mode & VM_PADLOCK_ANY) || (Viewer_mode & VM_OTHER_SHIP) || (Viewer_mode & VM_TRACK));
|
|
|
|
// now draw them
|
|
// only render models in this loop in order to minimize state changes
|
|
SCP_vector<sorted_obj>::iterator os;
|
|
for (os = Sorted_objects.begin(); os != Sorted_objects.end(); ++os) {
|
|
object *obj = os->obj;
|
|
|
|
obj->flags.set(Object::Object_Flags::Was_rendered);
|
|
|
|
//This is for ship cockpits. Bobb, feel free to optimize this any way you see fit
|
|
if ( (obj == Viewer_obj)
|
|
&& (obj->type == OBJ_SHIP)
|
|
&& c_viewer
|
|
&& (Ship_info[Ships[obj->instance].ship_info_index].flags[Ship::Info_Flags::Show_ship_model])
|
|
&& (!Show_ship_only_if_cockpits_enabled || Cockpit_active) )
|
|
{
|
|
(*draw_viewer_last) = true;
|
|
continue;
|
|
}
|
|
|
|
// if we're fullneb, fire up the fog - this also generates a fog table
|
|
if (full_neb) {
|
|
// maybe skip rendering an object because its obscured by the nebula
|
|
if(neb2_skip_render(obj, os->z)){
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( obj_render_is_model(obj) ) {
|
|
if( ((obj->type == OBJ_SHIP) && Ships[obj->instance].shader_effect_timestamp.isValid()) || (obj->type == OBJ_FIREBALL) )
|
|
effect_ships.push_back(obj);
|
|
else
|
|
render_function(obj);
|
|
}
|
|
if(object_had_transparency)
|
|
{
|
|
object_had_transparency = false;
|
|
transparent_objects.push_back(obj);
|
|
}
|
|
}
|
|
gr_deferred_lighting_msaa();
|
|
gr_deferred_lighting_end();
|
|
|
|
// we're done rendering models so flush render states
|
|
gr_clear_states();
|
|
|
|
// render everything else that isn't a model
|
|
for (os = Sorted_objects.begin(); os != Sorted_objects.end(); ++os) {
|
|
object *obj = os->obj;
|
|
|
|
obj->flags.set(Object::Object_Flags::Was_rendered);
|
|
|
|
if ( obj_render_is_model(obj) )
|
|
continue;
|
|
|
|
// maybe skip rendering an object because its obscured by the nebula
|
|
if(full_neb && neb2_skip_render(obj, os->z)){
|
|
continue;
|
|
}
|
|
|
|
render_function(obj);
|
|
}
|
|
|
|
Sorted_objects.clear();
|
|
|
|
batching_render_all();
|
|
batching_render_all(true);
|
|
}
|
|
|
|
void obj_render_queue_all()
|
|
{
|
|
GR_DEBUG_SCOPE("Render all objects");
|
|
TRACE_SCOPE(tracing::RenderScene);
|
|
|
|
object *objp;
|
|
int i;
|
|
model_draw_list scene;
|
|
|
|
objp = Objects;
|
|
|
|
gr_deferred_lighting_begin(false);
|
|
|
|
scene.init();
|
|
|
|
bool full_neb = is_full_nebula();
|
|
|
|
for ( i = 0; i <= Highest_object_index; i++,objp++ ) {
|
|
if ( (objp->type != OBJ_NONE) && ( objp->flags [Object::Object_Flags::Renders] ) ) {
|
|
objp->flags.remove(Object::Object_Flags::Was_rendered);
|
|
|
|
if ( !obj_in_view_cone(objp) ) {
|
|
continue;
|
|
}
|
|
|
|
if ( full_neb ) {
|
|
vec3d to_obj;
|
|
vm_vec_sub( &to_obj, &objp->pos, &Eye_position );
|
|
float z = vm_vec_dot( &Eye_matrix.vec.fvec, &to_obj );
|
|
|
|
if ( neb2_skip_render(objp, z) ){
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
if ( (objp->type == OBJ_SHIP) && Ships[objp->instance].shader_effect_timestamp.isValid() ) {
|
|
effect_ships.push_back(objp);
|
|
continue;
|
|
}
|
|
|
|
objp->flags.set(Object::Object_Flags::Was_rendered);
|
|
obj_queue_render(objp, &scene);
|
|
}
|
|
}
|
|
|
|
scene.init_render();
|
|
|
|
scene.render_all(ZBUFFER_TYPE_FULL);
|
|
gr_zbuffer_set(ZBUFFER_TYPE_READ);
|
|
gr_zbias(0);
|
|
gr_set_cull(0);
|
|
|
|
gr_clear_states();
|
|
gr_set_fill_mode(GR_FILL_MODE_SOLID);
|
|
gr_deferred_lighting_msaa();
|
|
|
|
decals::renderAll();
|
|
|
|
gr_deferred_lighting_end();
|
|
gr_deferred_lighting_finish();
|
|
|
|
gr_zbuffer_set(ZBUFFER_TYPE_READ);
|
|
|
|
gr_reset_lighting();
|
|
|
|
// now render transparent meshes
|
|
scene.render_all(ZBUFFER_TYPE_READ);
|
|
scene.render_all(ZBUFFER_TYPE_NONE);
|
|
|
|
// render electricity effects and insignias
|
|
scene.render_outlines();
|
|
scene.render_insignias();
|
|
scene.render_arcs();
|
|
|
|
gr_zbuffer_set(ZBUFFER_TYPE_READ);
|
|
gr_zbias(0);
|
|
gr_set_cull(0);
|
|
gr_set_fill_mode(GR_FILL_MODE_SOLID);
|
|
|
|
gr_clear_states();
|
|
|
|
}
|