Add render path to dispatch Particles as Decals (#7507)

* Add path to dispatch particles as decals

* Testing and bugfixing

* Address feedback

* Fix Typo

* Fix obj attachment
This commit is contained in:
BMagnu authored and GitHub committed 2026-07-17 13:38:45 +09:00
1 parent 446e8942ca
commit 856e676e79
6 files changed
+142 -30

No files matched your search

+24 -2
View File
@@ -361,6 +361,10 @@ void pageInDecal(const creation_info& info) {
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();
}
@@ -459,7 +463,7 @@ inline static void renderDecal(graphics::decal_draw_list& draw_list, const Decal
}
void renderAll() {
if (!Decal_system_active || !Decal_option_active || !gr_is_capable(gr_capability::CAPABILITY_INSTANCED_RENDERING)) {
if (!decalSystemActive()) {
return;
}
@@ -482,6 +486,24 @@ void renderAll() {
++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;
}
@@ -542,7 +564,7 @@ void addDecal(creation_info& info, const object* host, int submodel, const vec3d
}
void addSingleFrameDecal(Decal&& info) {
active_single_frame_decals.push_back(info);
active_single_frame_decals.push_back(std::move(info));
}
void invalidateForShip(const ship* shipp) {
+2
View File
@@ -133,6 +133,8 @@ void loadBitmaps(const creation_info& info);
*/
void pageInDecal(const creation_info& info);
bool decalSystemActive();
/**
* @brief Shut down the decal system. Must be called from game_shutdown
*/
+6
View File
@@ -16,6 +16,7 @@ ParticleEffect::ParticleEffect(SCP_string name)
m_rotation_type(RotationType::DEFAULT),
m_direction(ShapeDirection::ALIGNED),
m_velocity_directional_scaling(VelocityScaling::NONE),
m_decalOrientationMode(DecalOrientationMode::TOWARDS_CENTER),
m_affectedByDetail(false),
m_parentLifetime(false),
m_parentScale(false),
@@ -26,6 +27,8 @@ ParticleEffect::ParticleEffect(SCP_string name)
m_vel_inherit_from_position_absolute(false),
m_reverseAnimation(false),
m_ignore_velocity_inherit_if_has_parent(false),
m_renderAsDecal(false),
m_decalEmissive(false),
m_parent_is_transitive(true),
m_bitmap_list({}),
m_bitmap_range(::util::UniformRange<size_t>(0)),
@@ -88,6 +91,7 @@ ParticleEffect::ParticleEffect(SCP_string name,
m_rotation_type(RotationType::DEFAULT),
m_direction(direction),
m_velocity_directional_scaling(velocity_directional_scaling),
m_decalOrientationMode(DecalOrientationMode::TOWARDS_CENTER),
m_affectedByDetail(affectedByDetail),
m_parentLifetime(false),
m_parentScale(false),
@@ -98,6 +102,8 @@ ParticleEffect::ParticleEffect(SCP_string name,
m_vel_inherit_from_position_absolute(velInheritFromPositionAbsolute),
m_reverseAnimation(reverseAnimation),
m_ignore_velocity_inherit_if_has_parent(ignoreVelocityInheritIfParented),
m_renderAsDecal(false),
m_decalEmissive(false),
m_parent_is_transitive(true),
m_bitmap_list({bitmap}),
m_bitmap_range(::util::UniformRange<size_t>(0)),
+8
View File
@@ -69,6 +69,10 @@ public:
SCREEN_ALIGNED
};
enum class DecalOrientationMode : uint8_t {
TOWARDS_CENTER
};
enum class ParticleCurvesOutput : uint8_t {
PARTICLE_NUM_MULT,
PARTICLE_FREQ_MULT,
@@ -131,6 +135,7 @@ public:
friend int ::parse_weapon(int, bool, const char*);
friend ParticleEffectHandle scripting::api::getLegacyScriptingParticleEffect(int bitmap, bool reversed);
friend bool move_particle(float frametime, particle* part);
friend bool render_particle(particle* part);
SCP_string m_name; //!< The name of this effect
@@ -140,6 +145,7 @@ public:
RotationType m_rotation_type;
ShapeDirection m_direction;
VelocityScaling m_velocity_directional_scaling;
DecalOrientationMode m_decalOrientationMode;
bool m_affectedByDetail; //Kinda deprecated. Only used by the oldest of legacy effects.
bool m_parentLifetime;
@@ -151,6 +157,8 @@ public:
bool m_vel_inherit_from_position_absolute;
bool m_reverseAnimation;
bool m_ignore_velocity_inherit_if_has_parent;
bool m_renderAsDecal;
bool m_decalEmissive;
bool m_parent_is_transitive;
SCP_vector<int> m_bitmap_list;
+30
View File
@@ -348,6 +348,35 @@ namespace particle {
}
}
static void parseRenderAsDecal(ParticleEffect &effect) {
if (optional_string("$Render As Decal:")) {
stuff_boolean(&effect.m_renderAsDecal);
if (effect.m_renderAsDecal) {
if (!effect.m_parent_local) {
error_display(0, "Particle must be local to parent for decal rendering. Disabling decal rendering.");
effect.m_renderAsDecal = false;
//We still parse the rest to allow parsing to properly complete for the other effects.
}
if (optional_string("+Orientation Mode:")) {
switch (required_string_one_of(1, "Towards Center")) {
case 0:
required_string("Towards Center");
effect.m_decalOrientationMode = ParticleEffect::DecalOrientationMode::TOWARDS_CENTER;
break;
default:
UNREACHABLE("Unexpected orientation mode");
}
}
if (optional_string("+Emissive:")) {
stuff_boolean(&effect.m_decalEmissive);
}
}
}
}
static void parseModularCurvesLifetime(ParticleEffect& effect) {
effect.m_lifetime_curves.parse("$Particle Lifetime Curve:");
}
@@ -410,6 +439,7 @@ namespace particle {
parseLifetime(effect);
parseLocalPositionScaling(effect);
parseParentLocal(effect);
parseRenderAsDecal(effect);
parseLightEmissionSettings(effect);
//Spawner Settings
+72 -28
View File
@@ -10,6 +10,7 @@
#include "bmpman/bmpman.h"
#include "decals/decals.h"
#include "particle/particle.h"
#include "freespace.h"
@@ -387,26 +388,90 @@ namespace particle
/**
* @brief Renders a single particle
* @param part The particle to render
* @return @c true if the particle has been added to the rendering batch, @c false otherwise
* @return @c true if the particle has been added to the rendering batch (notably, this only includes main-render pass, alternative dispatch through decals is not true), @c false otherwise
*/
static bool render_particle(particle* part) {
bool render_particle(particle* part) {
// skip back-facing particles (ripped from fullneb code)
// Wanderer - add support for attached particles
vec3d p_pos = part->attachment.local_pos_to_global(part->pos);
bool part_has_length = part->length != 0.0f;
if (!part_has_length && vm_vec_dot_to_point(&Eye_matrix.vec.fvec, &Eye_position, &p_pos) <= 0.0f)
const auto& source_effect = part->parent_effect.getParticleEffect();
if (!source_effect.m_renderAsDecal && !part_has_length && vm_vec_dot_to_point(&Eye_matrix.vec.fvec, &Eye_position, &p_pos) <= 0.0f)
{
return false;
}
const auto& source_effect = part->parent_effect.getParticleEffect();
//For anything apart from the velocity curve, "Post-Curves Velocity" is well defined. This is needed to facilitate complex but common particle scaling and appearance curves.
const auto& curve_input = std::forward_as_tuple(*part,
vm_vec_mag_quick(&part->velocity) * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::VELOCITY_MULT, std::forward_as_tuple(*part, vm_vec_mag_quick(&part->velocity))));
// figure out which frame we should be using
int framenum;
int cur_frame;
if (part->nframes > 1) {
if (source_effect.m_lifetime_curves.has_curve(ParticleEffect::ParticleLifetimeCurvesOutput::ANIM_STATE)) {
cur_frame = fl2i(i2fl(part->nframes - 1) * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::ANIM_STATE, curve_input));
}
else {
framenum = bm_get_anim_frame(part->bitmap, part->age, part->max_life, part->looping);
cur_frame = part->reverse ? (part->nframes - framenum - 1) : framenum;
}
}
else
{
cur_frame = 0;
}
framenum = part->bitmap;
Assert( (cur_frame < part->nframes) || (part->nframes == 0 && cur_frame == 0) );
int actual_frame = cur_frame + framenum;
if (source_effect.m_renderAsDecal) {
if (!decals::decalSystemActive()) {
return false;
}
const auto& obj = part->attachment.extract_object();
if (!obj || obj->objnum < 0 || Objects[obj->objnum].signature != obj->sig || Objects[obj->objnum].type != OBJ_SHIP) {
return false;
}
float radius = part->radius * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::RADIUS_MULT, curve_input);
decals::Decal decalInfo;
if (source_effect.m_decalEmissive) {
decalInfo.definition_handle = std::tuple(-1, actual_frame, -1);
} else {
decalInfo.definition_handle = std::tuple(actual_frame, -1, -1);
}
decalInfo.object = &Objects[obj->objnum];
decalInfo.submodel = -1;
decalInfo.creation_time = f2fl(Missiontime);
decalInfo.lifetime = 1.0f;
decalInfo.position = part->pos;
decalInfo.scale = {{{ radius, radius, radius }}};
decalInfo.orig_obj_type = OBJ_SHIP;
switch (source_effect.m_decalOrientationMode) {
case ParticleEffect::DecalOrientationMode::TOWARDS_CENTER:
vm_vector_2_matrix(&decalInfo.orientation, &part->pos, nullptr, nullptr);
break;
default:
decalInfo.orientation = vmd_identity_matrix;
break;
}
decals::addSingleFrameDecal(std::move(decalInfo));
return false;
}
vec3d p1 = vmd_x_vector;
if (part_has_length) {
@@ -450,36 +515,15 @@ namespace particle
g3_transfer_vertex(&pos, &p_pos);
// figure out which frame we should be using
int framenum;
int cur_frame;
if (part->nframes > 1) {
if (source_effect.m_lifetime_curves.has_curve(ParticleEffect::ParticleLifetimeCurvesOutput::ANIM_STATE)) {
cur_frame = fl2i(i2fl(part->nframes - 1) * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::ANIM_STATE, curve_input));
}
else {
framenum = bm_get_anim_frame(part->bitmap, part->age, part->max_life, part->looping);
cur_frame = part->reverse ? (part->nframes - framenum - 1) : framenum;
}
}
else
{
cur_frame = 0;
}
framenum = part->bitmap;
Assert( (cur_frame < part->nframes) || (part->nframes == 0 && cur_frame == 0) );
float radius = part->radius * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::RADIUS_MULT, curve_input);
if (part_has_length) {
vec3d p0 = p_pos;
batching_add_laser(framenum + cur_frame, &p0, radius, &p1, radius);
batching_add_laser(actual_frame, &p0, radius, &p1, radius);
}
else {
// it will subtract Physics_viewer_bank, so without the flag we counter that and make it screen-aligned again
batching_add_volume_bitmap_rotated(framenum + cur_frame, &pos, part->use_angle ? part->angle : Physics_viewer_bank, radius, alpha);
batching_add_volume_bitmap_rotated(actual_frame, &pos, part->use_angle ? part->angle : Physics_viewer_bank, radius, alpha);
}
return true;