mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-10 17:46:53 -10:00
This adds a mechanism for running generic operations at specific points of the frame execution. This can be used by any part of the code an not just Lua since it simply executes std::functions. It also adds an "execution context" concept that specifies in which situation an operation should be executed. This is then used by the Lua Coroutines code for resuming coroutines at specific points instead of always when the awaited promise resolves.
549 lines
14 KiB
C++
549 lines
14 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 {
|
|
|
|
class DecalDefinition {
|
|
SCP_string _name;
|
|
|
|
SCP_string _diffuseFilename;
|
|
bool _loopDiffuse = true;
|
|
|
|
SCP_string _glowFilename;
|
|
bool _loopGlow = true;
|
|
|
|
SCP_string _normalMapFilename;
|
|
bool _loopNormal = true;
|
|
|
|
int _diffuseBitmap = -1;
|
|
int _glowBitmap = -1;
|
|
int _normalBitmap = -1;
|
|
|
|
public:
|
|
explicit DecalDefinition(SCP_string name) : _name(std::move(name)) {
|
|
}
|
|
|
|
~DecalDefinition() {
|
|
if (_diffuseBitmap >= 0) {
|
|
bm_release(_diffuseBitmap);
|
|
}
|
|
if (_glowBitmap >= 0) {
|
|
bm_release(_glowBitmap);
|
|
}
|
|
if (_normalBitmap >= 0) {
|
|
bm_release(_normalBitmap);
|
|
}
|
|
}
|
|
|
|
// Disallow copying
|
|
DecalDefinition(const DecalDefinition&) = delete;
|
|
DecalDefinition& operator=(const DecalDefinition&) = delete;
|
|
|
|
// Move constructor and operator
|
|
DecalDefinition(DecalDefinition&& other) noexcept {
|
|
*this = std::move(other); // Use operator implementation
|
|
}
|
|
DecalDefinition& operator=(DecalDefinition&& other) noexcept {
|
|
std::swap(_name, other._name);
|
|
|
|
std::swap(_diffuseFilename, other._diffuseFilename);
|
|
std::swap(_glowFilename, other._glowFilename);
|
|
std::swap(_normalMapFilename, other._normalMapFilename);
|
|
|
|
std::swap(_diffuseBitmap, other._diffuseBitmap);
|
|
std::swap(_glowBitmap, other._glowBitmap);
|
|
std::swap(_normalBitmap, other._normalBitmap);
|
|
|
|
std::swap(_loopDiffuse, other._loopDiffuse);
|
|
std::swap(_loopGlow, other._loopGlow);
|
|
std::swap(_loopNormal, other._loopNormal);
|
|
|
|
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(&_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 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 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 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& getName() const {
|
|
return _name;
|
|
}
|
|
int getDiffuseBitmap() const {
|
|
return _diffuseBitmap;
|
|
}
|
|
int getGlowBitmap() const {
|
|
return _glowBitmap;
|
|
}
|
|
int getNormalBitmap() const {
|
|
return _normalBitmap;
|
|
}
|
|
bool isDiffuseLooping() const {
|
|
return _loopDiffuse;
|
|
}
|
|
bool isGlowLooping() const {
|
|
return _loopGlow;
|
|
}
|
|
bool 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;
|
|
|
|
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"));
|
|
}
|
|
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"));
|
|
}
|
|
if (!gr_is_capable(CAPABILITY_SEPARATE_BLEND_FUNCTIONS)) {
|
|
// WWe 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"));
|
|
}
|
|
}
|
|
|
|
struct Decal {
|
|
int definition_handle = -1;
|
|
object_h object;
|
|
int orig_obj_type = OBJ_NONE;
|
|
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 (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",
|
|
OBJ_INDEX(object.objp), 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 < 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;
|
|
}
|
|
|
|
auto mission_time = f2fl(Missiontime);
|
|
|
|
graphics::decal_draw_list draw_list(active_decals.size());
|
|
for (auto& decal : active_decals) {
|
|
int diffuse_bm = -1;
|
|
int glow_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.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());
|
|
}
|
|
|
|
draw_list.add_decal(diffuse_bm, glow_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.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;
|
|
newDecal.scale.xyz.x = info.radius;
|
|
newDecal.scale.xyz.y = info.radius;
|
|
newDecal.scale.xyz.z = info.radius;
|
|
|
|
active_decals.push_back(newDecal);
|
|
}
|
|
|
|
}
|