mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-11 04:31:35 -10:00
This allows to render screen space decals into the deferred lighting buffers no matter how complex the underlying geometry is. Currently there is only limited support for using this in the engine. Only weapon impact decals are supported at the moment for testing purposes.
486 lines
12 KiB
C++
486 lines
12 KiB
C++
#include <nebula/neb.h>
|
|
#include "decals/decals.h"
|
|
|
|
#include "graphics/2d.h"
|
|
#include "graphics/decal_draw_list.h"
|
|
#include "graphics/util/uniform_structs.h"
|
|
#include "parse/parselo.h"
|
|
#include "tracing/tracing.h"
|
|
#include "ship/ship.h"
|
|
|
|
namespace {
|
|
|
|
class DecalDefinition {
|
|
SCP_string _name;
|
|
|
|
SCP_string _diffuseFilename;
|
|
SCP_string _normalMapFilename;
|
|
|
|
bool _loop = true;
|
|
|
|
int _diffuseBitmap = -1;
|
|
int _normalBitmap = -1;
|
|
|
|
public:
|
|
DecalDefinition(const SCP_string& name) : _name(name) {
|
|
}
|
|
|
|
~DecalDefinition() {
|
|
if (_diffuseBitmap >= 0) {
|
|
bm_release(_diffuseBitmap);
|
|
}
|
|
if (_normalBitmap >= 0) {
|
|
bm_release(_normalBitmap);
|
|
}
|
|
}
|
|
|
|
// Disallow copying
|
|
DecalDefinition(const DecalDefinition&) = delete;
|
|
DecalDefinition& operator=(const DecalDefinition&) = delete;
|
|
|
|
// Move constructor and operator
|
|
DecalDefinition(DecalDefinition&& other) {
|
|
*this = std::move(other); // Use operator implementation
|
|
}
|
|
DecalDefinition& operator=(DecalDefinition&& other) {
|
|
std::swap(_name, other._name);
|
|
|
|
std::swap(_diffuseFilename, other._diffuseFilename);
|
|
std::swap(_normalMapFilename, other._normalMapFilename);
|
|
|
|
std::swap(_diffuseBitmap, other._diffuseBitmap);
|
|
std::swap(_normalBitmap, other._normalBitmap);
|
|
|
|
std::swap(_loop, other._loop);
|
|
|
|
return *this;
|
|
}
|
|
|
|
void parse() {
|
|
if (optional_string("+Diffuse:")) {
|
|
stuff_string(_diffuseFilename, F_FILESPEC);
|
|
|
|
if (!bm_validate_filename(_diffuseFilename, true, true)) {
|
|
error_display(0, "Animation '%s' is not valid!", _diffuseFilename.c_str());
|
|
_diffuseFilename = "";
|
|
}
|
|
}
|
|
|
|
if (optional_string("+Loop:")) {
|
|
stuff_boolean(&_loop);
|
|
}
|
|
}
|
|
|
|
void loadBitmaps() {
|
|
if (_diffuseBitmap == -1 && VALID_FNAME(_diffuseFilename)) {
|
|
_diffuseBitmap = bm_load_either(_diffuseFilename.c_str());
|
|
if (_diffuseBitmap == -1) {
|
|
Warning(LOCATION,
|
|
"Bitmap '%s' failed to load for decal definition %s!",
|
|
_diffuseFilename.c_str(),
|
|
_name.c_str());
|
|
}
|
|
}
|
|
if (_normalBitmap == -1 && VALID_FNAME(_normalMapFilename)) {
|
|
_normalBitmap = bm_load_either(_normalMapFilename.c_str());
|
|
if (_normalBitmap == -1) {
|
|
Warning(LOCATION,
|
|
"Bitmap '%s' failed to load for decal definition %s!",
|
|
_normalMapFilename.c_str(),
|
|
_name.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
void pageIn() {
|
|
if (_diffuseBitmap >= 0) {
|
|
bm_page_in_texture(_diffuseBitmap);
|
|
}
|
|
if (_normalBitmap >= 0) {
|
|
bm_page_in_texture(_normalBitmap);
|
|
}
|
|
}
|
|
|
|
bool bitmapsLoaded() {
|
|
// Since both bitmap types are optional we need to check if either is loaded to determine if any bitmap is loaded
|
|
return _diffuseBitmap >= 0 || _normalBitmap >= 0;
|
|
}
|
|
|
|
const SCP_string& getName() const {
|
|
return _name;
|
|
}
|
|
int getDiffuseBitmap() const {
|
|
return _diffuseBitmap;
|
|
}
|
|
int getNormalBitmap() const {
|
|
return _normalBitmap;
|
|
}
|
|
bool isLooping() const {
|
|
return _loop;
|
|
}
|
|
};
|
|
|
|
SCP_vector<DecalDefinition> decalDefinitions;
|
|
|
|
// Variable to indicate if the system is able to work correctly on the current system
|
|
bool decal_system_active = true;
|
|
|
|
void parse_decals_table(const char* filename) {
|
|
try {
|
|
read_file_text(filename, CF_TYPE_TABLES);
|
|
reset_parse();
|
|
|
|
required_string("#Decals");
|
|
|
|
while (optional_string("$Decal:")) {
|
|
SCP_string name;
|
|
stuff_string(name, F_NAME);
|
|
|
|
DecalDefinition def(name);
|
|
def.parse();
|
|
|
|
decalDefinitions.push_back(std::move(def));
|
|
}
|
|
|
|
required_string("#End");
|
|
} catch (const parse::ParseException& e) {
|
|
mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", filename, e.what()));
|
|
return;
|
|
}
|
|
|
|
if (!gr_is_capable(CAPABILITY_DEFERRED_LIGHTING)) {
|
|
// we need deferred lighting
|
|
decal_system_active = false;
|
|
mprintf(("Note: Decal system has been disabled due to lack of deferred lighting.\n"));
|
|
}
|
|
/*
|
|
Normal mapping isn't used at the moment. This check needs to be reenabled once support for that is added
|
|
if (!gr_is_capable(CAPABILITY_NORMAL_MAP)) {
|
|
// We need normal mapping for the full feature range
|
|
decal_system_active = false;
|
|
mprintf(("Note: Decal system has been disabled due to lack of normal mapping.\n"));
|
|
}
|
|
*/
|
|
}
|
|
|
|
struct Decal {
|
|
int definition_handle = -1;
|
|
object_h object;
|
|
int submodel = -1;
|
|
|
|
float creation_time = -1.0f; //!< The mission time at which this decal was created
|
|
float lifetime = -1.0f; //!< The time this decal is active. When negative it never expires
|
|
|
|
vec3d position = vmd_zero_vector;
|
|
vec3d scale;
|
|
matrix orientation = vmd_identity_matrix;
|
|
|
|
Decal() {
|
|
vm_vec_make(&scale, 1.f, 1.f, 1.f);
|
|
}
|
|
|
|
bool isValid() {
|
|
if (!object.IsValid()) {
|
|
return false;
|
|
}
|
|
|
|
if (lifetime > 0.0f) {
|
|
if (f2fl(Missiontime) >= creation_time + lifetime) {
|
|
// Decal has expired
|
|
return false;
|
|
}
|
|
}
|
|
|
|
auto objp = object.objp;
|
|
if (objp->type == OBJ_SHIP) {
|
|
auto shipp = &Ships[objp->instance];
|
|
auto model_instance = model_get_instance(shipp->model_instance_num);
|
|
|
|
Assertion(submodel >= 0 && submodel < model_get(object_get_model(objp))->n_models,
|
|
"Invalid submodel number detected!");
|
|
auto smi = &model_instance->submodel[submodel];
|
|
|
|
if (smi->blown_off) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
SCP_vector<Decal> active_decals;
|
|
|
|
bool required_string_if_new(const char* token, bool new_entry) {
|
|
if (!new_entry) {
|
|
return optional_string(token) == 1;
|
|
}
|
|
|
|
required_string(token);
|
|
return true;
|
|
}
|
|
|
|
float clamp(float x, float min, float max) {
|
|
return std::min(std::max(x, min), max);
|
|
}
|
|
|
|
/**
|
|
* @brief Produces a smoothstep value as specified by the GLSL specification
|
|
* @param edge0
|
|
* @param edge1
|
|
* @param x
|
|
* @return
|
|
*/
|
|
float smoothstep(float edge0, float edge1, float x) {
|
|
auto t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
|
|
return t * t * (3.0f - 2.0f * t);
|
|
}
|
|
|
|
}
|
|
|
|
namespace decals {
|
|
|
|
void initialize() {
|
|
decalDefinitions.clear();
|
|
graphics::decal_draw_list::globalInit();
|
|
|
|
parse_modular_table(NOX("*-dcl.tbm"), parse_decals_table);
|
|
}
|
|
void shutdown() {
|
|
graphics::decal_draw_list::globalShutdown();
|
|
|
|
// Free allocated resources
|
|
decalDefinitions.clear();
|
|
}
|
|
|
|
int getDecalConfig(const SCP_string& name) {
|
|
int index = 0;
|
|
for (auto& def : decalDefinitions) {
|
|
if (!stricmp(def.getName().c_str(), name.c_str())) {
|
|
return index;
|
|
}
|
|
++index;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
void parseDecalReference(creation_info& dest_info, bool is_new_entry) {
|
|
SCP_string name;
|
|
stuff_string(name, F_NAME);
|
|
|
|
auto decalRef = getDecalConfig(name);
|
|
|
|
if (decalRef < 0) {
|
|
error_display(0, "Decal definition '%s' is unknown!", name.c_str());
|
|
}
|
|
dest_info.definition_handle = decalRef;
|
|
|
|
if (required_string_if_new("+Radius:", is_new_entry)) {
|
|
stuff_float(&dest_info.radius);
|
|
|
|
if (dest_info.radius <= 0.0f) {
|
|
error_display(0, "Invalid radius of %f! Must be a positive number.", dest_info.radius);
|
|
dest_info.radius = 1.0f;
|
|
}
|
|
}
|
|
|
|
if (required_string_if_new("+Lifetime:", is_new_entry)) {
|
|
if (optional_string("Eternal")) {
|
|
dest_info.lifetime = util::UniformFloatRange(-1.0f);
|
|
} else {
|
|
// Require at least a small lifetime so that the calculations don't have to deal with div-by-zero
|
|
dest_info.lifetime = util::parseUniformRange(0.0001f);
|
|
}
|
|
}
|
|
}
|
|
void loadBitmaps(const creation_info& info) {
|
|
if (!decal_system_active) {
|
|
return;
|
|
}
|
|
// Silently ignore invalid definition handle since weapons use the default values if the decal option is not present
|
|
if (info.definition_handle < 0) {
|
|
return;
|
|
}
|
|
|
|
Assertion(info.definition_handle >= 0 && info.definition_handle < (int) decalDefinitions.size(),
|
|
"Invalid decal handle detected!");
|
|
|
|
auto& def = decalDefinitions[info.definition_handle];
|
|
|
|
def.loadBitmaps();
|
|
}
|
|
void pageInDecal(const creation_info& info) {
|
|
if (!decal_system_active) {
|
|
return;
|
|
}
|
|
// Silently ignore invalid definition handle since weapons use the default values if the decal option is not present
|
|
if (info.definition_handle < 0) {
|
|
return;
|
|
}
|
|
|
|
Assertion(info.definition_handle >= 0 && info.definition_handle < (int) decalDefinitions.size(),
|
|
"Invalid decal handle detected!");
|
|
|
|
decalDefinitions[info.definition_handle].pageIn();
|
|
}
|
|
|
|
void initializeMission() {
|
|
active_decals.clear();
|
|
}
|
|
|
|
matrix4 getDecalTransform(Decal& decal) {
|
|
Assertion(decal.object.objp->type == OBJ_SHIP, "Only ships are currently supported for decals!");
|
|
|
|
auto objp = decal.object.objp;
|
|
auto ship = &Ships[objp->instance];
|
|
|
|
vec3d worldPos;
|
|
model_instance_find_world_point(&worldPos,
|
|
&decal.position,
|
|
ship->model_instance_num,
|
|
decal.submodel,
|
|
&objp->orient,
|
|
&objp->pos);
|
|
|
|
vec3d worldDir;
|
|
model_instance_find_world_dir(&worldDir,
|
|
&decal.orientation.vec.fvec,
|
|
ship->model_instance_num,
|
|
decal.submodel,
|
|
&objp->orient);
|
|
|
|
vec3d worldUp;
|
|
model_instance_find_world_dir(&worldUp,
|
|
&decal.orientation.vec.fvec,
|
|
ship->model_instance_num,
|
|
decal.submodel,
|
|
&objp->orient);
|
|
|
|
matrix worldOrient;
|
|
vm_vector_2_matrix(&worldOrient, &worldDir, &worldUp);
|
|
|
|
// Apply scaling
|
|
worldOrient.a2d[0][0] *= decal.scale.xyz.x;
|
|
worldOrient.a2d[0][1] *= decal.scale.xyz.x;
|
|
worldOrient.a2d[0][2] *= decal.scale.xyz.x;
|
|
worldOrient.a2d[1][0] *= decal.scale.xyz.y;
|
|
worldOrient.a2d[1][1] *= decal.scale.xyz.y;
|
|
worldOrient.a2d[1][2] *= decal.scale.xyz.y;
|
|
worldOrient.a2d[2][0] *= decal.scale.xyz.z;
|
|
worldOrient.a2d[2][1] *= decal.scale.xyz.z;
|
|
worldOrient.a2d[2][2] *= decal.scale.xyz.z;
|
|
|
|
matrix4 mat4;
|
|
vm_matrix4_set_transform(&mat4, &worldOrient, &worldPos);
|
|
|
|
return mat4;
|
|
}
|
|
|
|
void renderAll() {
|
|
if (!decal_system_active) {
|
|
return;
|
|
}
|
|
|
|
// Clear out any invalid decals
|
|
for (auto iter = active_decals.begin(); iter != active_decals.end(); ++iter) {
|
|
if (!iter->isValid()) {
|
|
// if we're sitting on the very last element, popping-back will invalidate the iterator!
|
|
if (iter + 1 == active_decals.end()) {
|
|
active_decals.pop_back();
|
|
break;
|
|
}
|
|
|
|
*iter = active_decals.back();
|
|
active_decals.pop_back();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (active_decals.empty()) {
|
|
return;
|
|
}
|
|
|
|
|
|
graphics::decal_draw_list draw_list;
|
|
auto mission_time = f2fl(Missiontime);
|
|
|
|
bool full_neb = ((The_mission.flags[Mission::Mission_Flags::Fullneb]) && (Neb2_render_mode != NEB2_RENDER_NONE)
|
|
&& !Fred_running);
|
|
|
|
if (full_neb) {
|
|
// Rendering decals is not supported when the fog effect is active (since the two effects do not work well
|
|
// together) so we skip rendering the decals in this case
|
|
// The active decals are still processed as usual though in case the nebula type of the mission is switched
|
|
return;
|
|
}
|
|
|
|
for (auto& decal : active_decals) {
|
|
int diffuse_bm = -1;
|
|
int normal_bm = -1;
|
|
|
|
Assertion(decal.definition_handle >= 0 && decal.definition_handle < (int) decalDefinitions.size(),
|
|
"Invalid decal handle detected!");
|
|
auto& decalDef = decalDefinitions[decal.definition_handle];
|
|
|
|
auto decal_time = mission_time - decal.creation_time;
|
|
auto progress = decal_time / decal.lifetime;
|
|
|
|
float alpha = 1.0f;
|
|
if (progress > 0.8) {
|
|
// Fade the decal out for the last 20% of its lifetime
|
|
alpha = 1.0f - smoothstep(0.8f, 1.0f, progress);
|
|
}
|
|
|
|
if (decalDef.getDiffuseBitmap() >= 0) {
|
|
diffuse_bm = decalDef.getDiffuseBitmap()
|
|
+ bm_get_anim_frame(decalDef.getDiffuseBitmap(), decal_time, 0.0f, decalDef.isLooping());
|
|
}
|
|
|
|
if (decalDef.getNormalBitmap() >= 0) {
|
|
normal_bm = decalDef.getNormalBitmap()
|
|
+ bm_get_anim_frame(decalDef.getNormalBitmap(), decal_time, 0.0f, decalDef.isLooping());
|
|
}
|
|
|
|
draw_list.add_decal(diffuse_bm, normal_bm, decal_time, getDecalTransform(decal), alpha);
|
|
}
|
|
|
|
draw_list.render();
|
|
}
|
|
void
|
|
addDecal(creation_info& info, object* host, int submodel, const vec3d& local_pos, const matrix& local_orient) {
|
|
if (!decal_system_active) {
|
|
return;
|
|
}
|
|
// Silently ignore invalid definition handle since weapons use the default values if the decal option is not present
|
|
if (info.definition_handle < 0) {
|
|
return;
|
|
}
|
|
|
|
Assertion(info.definition_handle >= 0 && info.definition_handle < (int) decalDefinitions.size(),
|
|
"Invalid decal handle detected!");
|
|
auto& def = decalDefinitions[info.definition_handle];
|
|
|
|
if (!def.bitmapsLoaded()) {
|
|
// If this decal was never used before then the bitmaps are not loaded so we need to do that here.
|
|
def.loadBitmaps();
|
|
}
|
|
|
|
Decal newDecal;
|
|
newDecal.definition_handle = info.definition_handle;
|
|
newDecal.object = object_h(host);
|
|
newDecal.submodel = submodel;
|
|
newDecal.creation_time = f2fl(Missiontime);
|
|
newDecal.lifetime = info.lifetime.next();
|
|
|
|
newDecal.position = local_pos;
|
|
newDecal.orientation = local_orient;
|
|
newDecal.scale.xyz.x = info.radius;
|
|
newDecal.scale.xyz.y = info.radius;
|
|
newDecal.scale.xyz.z = info.radius;
|
|
|
|
active_decals.push_back(newDecal);
|
|
}
|
|
|
|
}
|