Files
fs2open.github.com/code/decals/decals.cpp
T
BMagnu 9b82daeefc Proper Ambient Light (#6849)
* Remove ambient handling from amin / decal pass

* Add ambient light to deferred pass

* Make decals work and fix double insignia rendering

* Fix decal vertex shader

* render insignias as decals

* Cleanup of old insignia stuff

* Move insignia post-processing to model load

* perform envmap lighting in deferred shader

* Remove Envmapping from main shader

* Fix MSVC warning

* exclude def_files from clang tidy
2025-07-22 00:17:22 +02:00

540 lines
15 KiB
C++

#include <nebula/neb.h>
#include <utility>
#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 decals {
DecalDefinition::DecalDefinition(SCP_string name) : _name(std::move(name)) {
}
DecalDefinition::~DecalDefinition() {
if (_diffuseBitmap >= 0) {
bm_release(_diffuseBitmap);
}
if (_glowBitmap >= 0) {
bm_release(_glowBitmap);
}
if (_normalBitmap >= 0) {
bm_release(_normalBitmap);
}
}
void DecalDefinition::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(&_loopDiffuse);
}
}
if (optional_string("+Glow:")) {
stuff_string(_glowFilename, F_FILESPEC);
if (!bm_validate_filename(_glowFilename, true, true)) {
error_display(0, "Animation '%s' is not valid!", _glowFilename.c_str());
_glowFilename = "";
}
if (optional_string("+Loop:")) {
stuff_boolean(&_loopGlow);
}
}
if (optional_string("+Normal:")) {
stuff_string(_normalMapFilename, F_FILESPEC);
if (!bm_validate_filename(_normalMapFilename, true, true)) {
error_display(0, "Animation '%s' is not valid!", _normalMapFilename.c_str());
_normalMapFilename = "";
}
if (optional_string("+Loop:")) {
stuff_boolean(&_loopNormal);
}
}
}
void DecalDefinition::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 (_glowBitmap == -1 && VALID_FNAME(_glowFilename)) {
_glowBitmap = bm_load_either(_glowFilename.c_str());
if (_glowBitmap == -1) {
Warning(LOCATION,
"Bitmap '%s' failed to load for decal definition %s!",
_glowFilename.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 DecalDefinition::pageIn() {
if (_diffuseBitmap >= 0) {
bm_page_in_texture(_diffuseBitmap);
}
if (_glowBitmap >= 0) {
bm_page_in_texture(_glowBitmap);
}
if (_normalBitmap >= 0) {
bm_page_in_texture(_normalBitmap);
}
}
bool DecalDefinition::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 || _glowBitmap >= 0 || _normalBitmap >= 0;
}
const SCP_string& DecalDefinition::getName() const {
return _name;
}
int DecalDefinition::getDiffuseBitmap() const {
return _diffuseBitmap;
}
int DecalDefinition::getGlowBitmap() const {
return _glowBitmap;
}
int DecalDefinition::getNormalBitmap() const {
return _normalBitmap;
}
bool DecalDefinition::isDiffuseLooping() const {
return _loopDiffuse;
}
bool DecalDefinition::isGlowLooping() const {
return _loopGlow;
}
bool DecalDefinition::isNormalLooping() const {
return _loopNormal;
}
SCP_vector<DecalDefinition> DecalDefinitions;
// Variable to indicate if the system is able to work correctly on the current system
bool Decal_system_active = true;
bool Decal_option_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(std::move(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(gr_capability::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"));
}
if (!gr_is_capable(gr_capability::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"));
}
if (!gr_is_capable(gr_capability::CAPABILITY_SEPARATE_BLEND_FUNCTIONS)) {
// We need separate blending functions for different color buffers
Decal_system_active = false;
mprintf(("Note: Decal system has been disabled due to lack of separate color buffer blend functions.\n"));
}
}
Decal::Decal() {
vm_vec_make(&scale, 1.f, 1.f, 1.f);
}
bool Decal::isValid() const {
if (!object.isValid()) {
return false;
}
if (object.objp()->flags[Object::Object_Flags::Should_be_dead]) {
return false;
}
if (orig_obj_type != object.objp()->type) {
mprintf(("Decal object type for object %d has changed from %s to %s. Please let m!m know about this\n",
object.objnum, Object_type_names[orig_obj_type], Object_type_names[object.objp()->type]));
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 < object_get_model(objp)->n_models,
"Invalid submodel number detected!");
auto smi = &model_instance->submodel[submodel];
if (smi->blown_off) {
return false;
}
} else {
Assertion(false, "Only ships are currently supported for decals!");
return false;
}
return true;
}
SCP_vector<Decal> active_decals, active_single_frame_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() {
if (gr_screen.mode == GR_STUB) {
Decal_system_active = false;
return;
}
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 findDecalDefinition(const SCP_string& name) {
auto ii = std::find_if(DecalDefinitions.begin(), DecalDefinitions.end(), [&](const DecalDefinition& def)
{
return def.getName() == name;
});
if (ii == DecalDefinitions.end())
return -1;
return static_cast<int>(std::distance(DecalDefinitions.begin(), ii));
}
void parseDecalReference(creation_info& dest_info, bool is_new_entry) {
SCP_string name;
stuff_string(name, F_NAME);
// decals are not initialized in FRED, so no decal definitions are known
if (!Fred_running) {
auto decalRef = findDecalDefinition(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)) {
dest_info.radius = util::ParsedRandomFloatRange::parseRandomRange(0.0001f);
}
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::ParsedRandomFloatRange::parseRandomRange(0.0001f);
}
}
if (optional_string("+Use Random Rotation:")) {
stuff_boolean(&dest_info.random_rotation);
}
}
void loadBitmaps(const creation_info& info) {
if (!Decal_system_active || !Decal_option_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 || !Decal_option_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();
}
// Discard any fragments where the angle to the direction to greater than 45°
const float DECAL_ANGLE_CUTOFF = fl_radians(45.f);
const float DECAL_ANGLE_FADE_START = fl_radians(30.f);
static matrix4 getDecalTransform(const Decal& decal, float alpha) {
Assertion(decal.object.objp()->type == OBJ_SHIP, "Only ships are currently supported for decals!");
auto objp = decal.object.objp();
auto ship = &Ships[objp->instance];
auto pmi = model_get_instance(ship->model_instance_num);
auto pm = model_get(pmi->model_num);
vec3d worldPos;
matrix worldOrient;
model_instance_local_to_global_point_orient(&worldPos,
&worldOrient,
&decal.position,
&decal.orientation,
pm,
pmi,
decal.submodel,
&objp->orient,
&objp->pos);
// The decal API sees the "direction" of a decal to be along the normal of the surface it is attached to. However,
// this will lead to a situation where we would look at the decal texture "from behind" causing the texture to
// appear flipped. We fix that here for the graphics transform by inverting the Z scaling.
// ALSO for some reason the uvec needs to be flipped as well, otherwise the decals render upside-down. Without
// being sure of the root cause, this at least makes it appear correct for modders.
// Apply scaling
vm_vec_scale(&worldOrient.vec.rvec, decal.scale.xyz.x);
vm_vec_scale(&worldOrient.vec.uvec, -decal.scale.xyz.y);
vm_vec_scale(&worldOrient.vec.fvec, -decal.scale.xyz.z);
matrix4 mat4;
vm_matrix4_set_transform(&mat4, &worldOrient, &worldPos);
// This is currently a constant but in the future this may be configurable by the decals table
mat4.a2d[0][3] = DECAL_ANGLE_CUTOFF;
mat4.a2d[1][3] = DECAL_ANGLE_FADE_START;
mat4.a2d[2][3] = alpha;
return mat4;
}
inline static void renderDecal(graphics::decal_draw_list& draw_list, const Decal& decal) {
auto mission_time = f2fl(Missiontime);
int diffuse_bm = -1;
int glow_bm = -1;
int normal_bm = -1;
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 (std::holds_alternative<int>(decal.definition_handle)) {
int definition_handle = std::get<int>(decal.definition_handle);
Assertion(definition_handle >= 0 && definition_handle < (int) DecalDefinitions.size(),
"Invalid decal handle detected!");
auto &decalDef = DecalDefinitions[definition_handle];
if (decalDef.getDiffuseBitmap() >= 0) {
diffuse_bm = decalDef.getDiffuseBitmap()
+
bm_get_anim_frame(decalDef.getDiffuseBitmap(), decal_time, 0.0f, decalDef.isDiffuseLooping());
}
if (decalDef.getGlowBitmap() >= 0) {
glow_bm = decalDef.getGlowBitmap()
+ bm_get_anim_frame(decalDef.getGlowBitmap(), decal_time, 0.0f, decalDef.isGlowLooping());
}
if (decalDef.getNormalBitmap() >= 0) {
normal_bm = decalDef.getNormalBitmap()
+ bm_get_anim_frame(decalDef.getNormalBitmap(), decal_time, 0.0f, decalDef.isNormalLooping());
}
}
else {
std::tie(diffuse_bm, glow_bm, normal_bm) = std::get<std::tuple<int, int, int>>(decal.definition_handle);
}
draw_list.add_decal(diffuse_bm, glow_bm, normal_bm, decal_time, getDecalTransform(decal, alpha));
}
void renderAll() {
if (!Decal_system_active || !Decal_option_active || !gr_is_capable(gr_capability::CAPABILITY_INSTANCED_RENDERING)) {
return;
}
// Clear out any invalid decals
for (auto iter = active_decals.begin(); iter != active_decals.end();) {
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;
}
// next decal, only increment the iterator if we found a valid value so nothing gets skipped
// otherwise we may skip a decal which can then get through to the draw_list loop while being invalid
++iter;
}
if (active_decals.empty() && active_single_frame_decals.empty()) {
return;
}
graphics::decal_draw_list draw_list;
for (auto& decal : active_decals)
renderDecal(draw_list, decal);
for (auto& decal : active_single_frame_decals)
renderDecal(draw_list, decal);
draw_list.render();
active_single_frame_decals.clear();
}
void addDecal(creation_info& info, const object* host, int submodel, const vec3d& local_pos, const matrix& local_orient) {
if (!Decal_system_active || !Decal_option_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.orig_obj_type = host->type;
newDecal.submodel = submodel;
newDecal.creation_time = f2fl(Missiontime);
newDecal.lifetime = info.lifetime.next();
newDecal.position = local_pos;
newDecal.orientation = local_orient;
if (info.width < 0.0f || info.height < 0.0f) {
float radius = info.radius.next();
newDecal.scale.xyz.x = radius;
newDecal.scale.xyz.y = radius;
newDecal.scale.xyz.z = radius;
} else {
newDecal.scale.xyz.x = info.width;
newDecal.scale.xyz.y = info.height;
newDecal.scale.xyz.z = MIN(info.width, info.height);
}
active_decals.push_back(newDecal);
}
void addSingleFrameDecal(Decal&& info) {
active_single_frame_decals.push_back(info);
}
}