mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* Add path to dispatch particles as decals * Testing and bugfixing * Address feedback * Fix Typo * Fix obj attachment
553 lines
17 KiB
C++
553 lines
17 KiB
C++
/*
|
|
* Copyright (C) Volition, Inc. 1999. All rights reserved.
|
|
*
|
|
* All source code herein is the property of Volition, Inc. You may not sell
|
|
* or otherwise commercially exploit the source or things you created based on the
|
|
* source.
|
|
*
|
|
*/
|
|
|
|
|
|
|
|
#include "bmpman/bmpman.h"
|
|
#include "decals/decals.h"
|
|
#include "particle/particle.h"
|
|
|
|
#include "freespace.h"
|
|
#include "particle/ParticleManager.h"
|
|
#include "particle/ParticleEffect.h"
|
|
#include "particle/hosts/EffectHostVector.h"
|
|
#include "debugconsole/console.h"
|
|
#include "globalincs/systemvars.h"
|
|
#include "graphics/2d.h"
|
|
#include "lighting/lighting.h"
|
|
#include "math/curve.h"
|
|
#include "render/3d.h"
|
|
#include "render/batching.h"
|
|
#include "tracing/tracing.h"
|
|
#include "tracing/Monitor.h"
|
|
#include "utils/Random.h"
|
|
#include "nebula/neb.h"
|
|
#include "mission/missionparse.h"
|
|
#include "mod_table/mod_table.h"
|
|
|
|
using namespace particle;
|
|
|
|
namespace
|
|
{
|
|
SCP_vector<::particle::particle> Particles;
|
|
SCP_vector<ParticlePtr> Persistent_particles;
|
|
|
|
static int Particles_enabled = 1;
|
|
|
|
float get_current_alpha(vec3d* pos, float rad)
|
|
{
|
|
float dist;
|
|
float alpha = 0.99999f;
|
|
|
|
const float inner_radius = MIN(30.0f, rad);
|
|
const float magic_num = MIN(2.75f, rad / 10.0f);
|
|
|
|
// determine what alpha to draw this bitmap with
|
|
// higher alpha the closer the bitmap gets to the eye
|
|
dist = vm_vec_dist_quick(&Eye_position, pos);
|
|
|
|
// if the point is inside the inner radius, alpha is based on distance to the player's eye,
|
|
// becoming more transparent as it gets close
|
|
if (dist <= inner_radius)
|
|
{
|
|
// alpha per meter between the magic # and the inner radius
|
|
alpha /= (inner_radius - magic_num);
|
|
|
|
// above value times the # of meters away we are
|
|
alpha *= (dist - magic_num);
|
|
if (alpha < 0.05f)
|
|
return 0.0f;
|
|
}
|
|
|
|
if (Neb_affects_particles)
|
|
nebula_handle_alpha(alpha, pos,
|
|
Neb2_fog_visibility_particle_const + (rad * Neb2_fog_visibility_particle_scaled_factor));
|
|
|
|
return alpha;
|
|
}
|
|
}
|
|
|
|
namespace particle
|
|
{
|
|
int Anim_bitmap_id_fire = -1;
|
|
int Anim_num_frames_fire = -1;
|
|
|
|
int Anim_bitmap_id_smoke = -1;
|
|
int Anim_num_frames_smoke = -1;
|
|
|
|
int Anim_bitmap_id_smoke2 = -1;
|
|
int Anim_num_frames_smoke2 = -1;
|
|
|
|
// Reset everything between levels
|
|
void init()
|
|
{
|
|
if (Is_standalone)
|
|
{
|
|
Particles_enabled = 0;
|
|
return;
|
|
}
|
|
|
|
// FIRE!!!
|
|
if (Anim_bitmap_id_fire < 0)
|
|
{
|
|
Anim_bitmap_id_fire = bm_load_animation("particleexp01", &Anim_num_frames_fire, nullptr, NULL, 0);
|
|
if (Anim_bitmap_id_fire < 0)
|
|
Warning(LOCATION, "Could not load legacy fire particle bitmap (particleexp01)!");
|
|
}
|
|
|
|
// Cough, cough
|
|
if (Anim_bitmap_id_smoke < 0)
|
|
{
|
|
Anim_bitmap_id_smoke = bm_load_animation("particlesmoke01", &Anim_num_frames_smoke, nullptr, NULL, 0);
|
|
if (Anim_bitmap_id_smoke < 0)
|
|
Warning(LOCATION, "Could not load legacy smoke particle bitmap (particlesmoke01)!");
|
|
}
|
|
|
|
// wheeze
|
|
if (Anim_bitmap_id_smoke2 < 0)
|
|
{
|
|
Anim_bitmap_id_smoke2 = bm_load_animation("particlesmoke02", &Anim_num_frames_smoke2, nullptr, NULL, 0);
|
|
if (Anim_bitmap_id_smoke2 < 0)
|
|
Warning(LOCATION, "Could not load legacy smoke2 particle bitmap (particlesmoke02)!");
|
|
}
|
|
}
|
|
|
|
const ParticleEffect& ParticleSubeffectHandle::getParticleEffect() const {
|
|
//TODO possibly cache this!
|
|
return ParticleManager::get()->getEffect(handle)[subeffect];
|
|
}
|
|
|
|
// only call from game_shutdown()!!!
|
|
void close()
|
|
{
|
|
Persistent_particles.clear();
|
|
Particles.clear();
|
|
}
|
|
|
|
size_t get_particle_count() {
|
|
return Particles.size() + Persistent_particles.size();
|
|
}
|
|
|
|
void page_in()
|
|
{
|
|
if (!Particles_enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bm_page_in_texture(Anim_bitmap_id_fire);
|
|
bm_page_in_texture(Anim_bitmap_id_smoke);
|
|
bm_page_in_texture(Anim_bitmap_id_smoke2);
|
|
|
|
ParticleManager::get()->pageIn();
|
|
}
|
|
|
|
DCF_BOOL2(particles, Particles_enabled, "Turns particles on/off",
|
|
"Usage: particles [bool]\nTurns particle system on/off. If nothing passed, then toggles it.\n");
|
|
|
|
|
|
static bool maybe_cull_particle(const particle& new_particle) {
|
|
if (!Particles_enabled)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
vec3d world_pos = new_particle.attachment.local_pos_to_global(new_particle.pos);
|
|
|
|
// treat particles on lower detail levels as 'further away' for the purposes of culling
|
|
float adjusted_dist = vm_vec_dist(&Eye_position, &world_pos) * powf(2.5f, (float)(static_cast<int>(DefaultDetailPreset::Num_detail_presets) - Detail.num_particles));
|
|
// treat bigger particles as 'closer'
|
|
adjusted_dist /= new_particle.radius;
|
|
float cull_start_dist = 1000.f;
|
|
if (adjusted_dist > cull_start_dist) {
|
|
if (frand() > 1.0f / (log2(adjusted_dist / cull_start_dist) + 1.0f))
|
|
return true;
|
|
}
|
|
|
|
if (new_particle.nframes >= 0 && new_particle.bitmap < 0)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void create(particle&& new_particle) {
|
|
if (maybe_cull_particle(new_particle))
|
|
return;
|
|
|
|
Particles.push_back(new_particle);
|
|
}
|
|
|
|
// Creates a single particle. See the PARTICLE_?? defines for types.
|
|
WeakParticlePtr createPersistent(particle&& new_particle)
|
|
{
|
|
if (maybe_cull_particle(new_particle))
|
|
return {};
|
|
|
|
ParticlePtr new_particle_ptr = std::make_shared<particle>(new_particle);
|
|
|
|
Persistent_particles.push_back(new_particle_ptr);
|
|
|
|
return {new_particle_ptr};
|
|
}
|
|
|
|
float getPixelSize(const particle& subject_particle) {
|
|
vec3d world_pos = subject_particle.attachment.local_pos_to_global(subject_particle.pos);
|
|
|
|
float distance_to_eye = vm_vec_dist(&Eye_position, &world_pos);
|
|
|
|
return convert_distance_and_diameter_to_pixel_size(
|
|
distance_to_eye,
|
|
subject_particle.radius * 2.f,
|
|
g3_get_hfov(Eye_fov),
|
|
gr_screen.max_w);
|
|
}
|
|
|
|
/**
|
|
* @brief Moves a single particle
|
|
* @param frametime The length of the current frame
|
|
* @param part The particle to process for movement
|
|
* @return @c true if the particle has expired and should be removed, @c false otherwise
|
|
*/
|
|
bool move_particle(float frametime, particle* part) {
|
|
if (part->age == 0.0f)
|
|
{
|
|
part->age = 0.00001f;
|
|
}
|
|
else
|
|
{
|
|
part->age += frametime;
|
|
}
|
|
|
|
bool remove_particle = false;
|
|
|
|
// if its time expired, remove it. If the particle is looping then it will never be removed due to age
|
|
if (part->age > part->max_life && !part->looping)
|
|
{
|
|
// special case, if max_life is 0 then we want it to render at least once
|
|
if ((part->age > frametime) || (part->max_life > 0.0f))
|
|
{
|
|
remove_particle = true;
|
|
}
|
|
}
|
|
|
|
// if the particle is attached to an object which has become invalid, kill it
|
|
if (!part->attachment.is_valid())
|
|
{
|
|
remove_particle = true;
|
|
}
|
|
|
|
const auto& source_effect = part->parent_effect.getParticleEffect();
|
|
|
|
if (remove_particle)
|
|
{
|
|
if (source_effect.m_deathEffect.isValid()) {
|
|
vec3d world_pos = part->attachment.local_pos_to_global(part->pos);
|
|
vec3d world_vel = part->attachment.local_vel_to_global(part->velocity);
|
|
|
|
matrix orient = vmd_identity_matrix;
|
|
if (vm_vec_mag_squared(&world_vel) > 0.0f) {
|
|
vm_vector_2_matrix(&orient, &world_vel);
|
|
}
|
|
|
|
auto deathSource = ParticleManager::get()->createSource(source_effect.m_deathEffect);
|
|
deathSource->setHost(std::make_unique<EffectHostVector>(world_pos, orient, world_vel));
|
|
deathSource->finishCreation();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
float part_velocity = vm_vec_mag_quick(&part->velocity);
|
|
float vel_scalar = source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::VELOCITY_MULT, std::forward_as_tuple(*part, part_velocity) );
|
|
|
|
// move as a regular particle
|
|
vec3d prev_pos = part->pos;
|
|
part->pos += (part->velocity * vel_scalar) * frametime;
|
|
|
|
const auto& curve_input = std::forward_as_tuple(*part, part_velocity * vel_scalar);
|
|
|
|
if (Detail.lighting > 3 && source_effect.m_light_source) {
|
|
const auto& light_source = *source_effect.m_light_source;
|
|
|
|
vec3d p_pos = part->attachment.local_pos_to_global(part->pos);
|
|
|
|
float light_radius = light_source.light_radius * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LIGHT_RADIUS_MULT, curve_input);
|
|
float source_radius = light_source.source_radius * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LIGHT_SOURCE_RADIUS_MULT, curve_input);
|
|
float intensity = light_source.intensity * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LIGHT_INTENSITY_MULT, curve_input);
|
|
float r = light_source.r * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LIGHT_R_MULT, curve_input);
|
|
float g = light_source.g * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LIGHT_G_MULT, curve_input);
|
|
float b = light_source.b * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LIGHT_B_MULT, curve_input);
|
|
|
|
// after this point it is only lighting code, so we can early return
|
|
if (light_radius <= 0.0f || intensity <= 0.0f) {
|
|
return false;
|
|
}
|
|
|
|
switch (light_source.light_source_mode) {
|
|
case ParticleEffect::LightInformation::LightSourceMode::POINT:
|
|
light_add_point(&p_pos, light_radius, light_radius, intensity, r, g, b, source_radius);
|
|
break;
|
|
case ParticleEffect::LightInformation::LightSourceMode::TO_LAST_POS: {
|
|
vec3d p_prev_pos = part->attachment.local_last_pos_to_global(prev_pos);
|
|
light_add_tube(&p_prev_pos, &p_pos, light_radius, light_radius, intensity, r, g, b, source_radius);
|
|
}
|
|
break;
|
|
case ParticleEffect::LightInformation::LightSourceMode::AS_PARTICLE:
|
|
if (part->length != 0.0f) {
|
|
vec3d p1 = part->attachment.local_vel_to_global(part->velocity);
|
|
vm_vec_normalize_safe(&p1);
|
|
p1 *= part->length * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LENGTH_MULT, curve_input);
|
|
p1 += p_pos;
|
|
light_add_tube(&p_pos, &p1, light_radius, light_radius, intensity, r, g, b, source_radius);
|
|
}
|
|
else {
|
|
light_add_point(&p_pos, light_radius, light_radius, intensity, r, g, b, source_radius);
|
|
}
|
|
break;
|
|
case ParticleEffect::LightInformation::LightSourceMode::CONE: {
|
|
float cone_angle = light_source.cone_angle * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LIGHT_CONE_ANGLE_MULT, curve_input);
|
|
float cone_inner_angle = light_source.cone_inner_angle * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LIGHT_CONE_INNER_ANGLE_MULT, curve_input);
|
|
vec3d p1 = part->attachment.local_vel_to_global(part->velocity);
|
|
vm_vec_normalize_safe(&p1);
|
|
|
|
light_add_cone(&p_pos, &p1, cone_angle, cone_inner_angle, false, light_radius, light_radius, intensity, r, g, b, source_radius);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void move_all(float frametime)
|
|
{
|
|
TRACE_SCOPE(tracing::ParticlesMoveAll);
|
|
|
|
if (!Particles_enabled)
|
|
return;
|
|
|
|
if (Persistent_particles.empty() && Particles.empty())
|
|
return;
|
|
|
|
for (auto p = Persistent_particles.begin(); p != Persistent_particles.end();)
|
|
{
|
|
ParticlePtr part = *p;
|
|
if (move_particle(frametime, part.get()))
|
|
{
|
|
// if we're sitting on the very last particle, popping-back will invalidate the iterator!
|
|
if (p + 1 == Persistent_particles.end())
|
|
{
|
|
Persistent_particles.pop_back();
|
|
break;
|
|
}
|
|
|
|
*p = Persistent_particles.back();
|
|
Persistent_particles.pop_back();
|
|
continue;
|
|
}
|
|
|
|
// next particle
|
|
++p;
|
|
}
|
|
|
|
for (auto p = Particles.begin(); p != Particles.end();)
|
|
{
|
|
if (move_particle(frametime, &(*p)))
|
|
{
|
|
// if we're sitting on the very last particle, popping-back will invalidate the iterator!
|
|
if (p + 1 == Particles.end())
|
|
{
|
|
Particles.pop_back();
|
|
break;
|
|
}
|
|
|
|
*p = Particles.back();
|
|
Particles.pop_back();
|
|
continue;
|
|
}
|
|
|
|
// next particle
|
|
++p;
|
|
}
|
|
}
|
|
|
|
// kill all active particles
|
|
void kill_all()
|
|
{
|
|
// kill all active particles
|
|
Particles.clear();
|
|
Persistent_particles.clear();
|
|
}
|
|
|
|
/**
|
|
* @brief Renders a single particle
|
|
* @param part The particle to render
|
|
* @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
|
|
*/
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
//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) {
|
|
p1 = part->attachment.local_vel_to_global(part->velocity);
|
|
vm_vec_normalize_safe(&p1);
|
|
p1 *= part->length * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LENGTH_MULT, curve_input);
|
|
p1 += p_pos;
|
|
|
|
float dot0 = vm_vec_dot_to_point(&Eye_matrix.vec.fvec, &Eye_position, &p_pos);
|
|
float dot1 = vm_vec_dot_to_point(&Eye_matrix.vec.fvec, &Eye_position, &p1);
|
|
|
|
if (dot0 <= 0.0f && dot1 <= 0.0f) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// calculate the alpha to draw at
|
|
auto alpha = get_current_alpha(&p_pos, part->radius);
|
|
|
|
// if it's transparent then just skip it
|
|
if (alpha <= 0.0f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
vertex pos;
|
|
auto flags = g3_rotate_vertex(&pos, &p_pos);
|
|
|
|
if (flags)
|
|
{
|
|
if (part_has_length) {
|
|
vertex pos2;
|
|
auto flags2 = g3_rotate_vertex(&pos2, &p1);
|
|
if (flags & flags2) {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
g3_transfer_vertex(&pos, &p_pos);
|
|
|
|
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(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(actual_frame, &pos, part->use_angle ? part->angle : Physics_viewer_bank, radius, alpha);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void render_all()
|
|
{
|
|
GR_DEBUG_SCOPE("Render Particles");
|
|
TRACE_SCOPE(tracing::ParticlesRenderAll);
|
|
|
|
if (!Particles_enabled)
|
|
return;
|
|
|
|
if (Persistent_particles.empty() && Particles.empty())
|
|
return;
|
|
|
|
for (auto& part : Persistent_particles) {
|
|
render_particle(part.get());
|
|
}
|
|
|
|
for (auto& part : Particles) {
|
|
render_particle(&part);
|
|
}
|
|
|
|
}
|
|
}
|