#include #include #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 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); } void Decal::markForDeletion() { orig_obj_type = OBJ_NONE; } 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 == OBJ_NONE) { //Decal should be cleared 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 { UNREACHABLE("Only ships are currently supported for decals!"); return false; } return true; } SCP_vector 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 == GraphicsAPI::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(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(); } bool decalSystemActive() { return Decal_system_active && Decal_option_active && gr_is_capable(gr_capability::CAPABILITY_INSTANCED_RENDERING); } 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(decal.definition_handle)) { int definition_handle = std::get(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>(decal.definition_handle); } draw_list.add_decal(diffuse_bm, glow_bm, normal_bm, decal_time, getDecalTransform(decal, alpha)); } void renderAll() { if (!decalSystemActive()) { 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; } for (auto iter = active_single_frame_decals.begin(); iter != active_single_frame_decals.end();) { if (!iter->isValid()) { // if we're sitting on the very last element, popping-back will invalidate the iterator! if (iter + 1 == active_single_frame_decals.end()) { active_single_frame_decals.pop_back(); break; } *iter = active_single_frame_decals.back(); active_single_frame_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(std::move(info)); } void invalidateForShip(const ship* shipp) { int objnum = shipp->objnum; for (Decal& decal : active_decals) { if (decal.object.objnum == objnum) { decal.markForDeletion(); } } } }