mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-13 06:21:29 -10:00
#5328 added a very useful culling for particles created by specific particle effects (ie weapons, now beams), though in an effort to simplify the culling process, that PR removed the necessary culling of default smoke on ships. This PR restores the culling of default smoke while also keeping the new culling of particle specific effects. Tested and works as expected, including fixing the slowdowns caused by the aforementioned removal of smoke effect culls.
621 lines
15 KiB
C++
621 lines
15 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 "particle/particle.h"
|
|
#include "particle/ParticleManager.h"
|
|
#include "debugconsole/console.h"
|
|
#include "globalincs/systemvars.h"
|
|
#include "graphics/2d.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;
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
inline int get_percent(int count)
|
|
{
|
|
if (count == 0)
|
|
return 0;
|
|
|
|
// this should basically return a scale like:
|
|
// 50, 75, 100, 125, 150, ...
|
|
// based on value of 'count' (detail level)
|
|
return (50 + (25 * (count - 1)));
|
|
}
|
|
}
|
|
|
|
namespace particle
|
|
{
|
|
// Reset everything between levels
|
|
void init()
|
|
{
|
|
if (Is_standalone)
|
|
{
|
|
Particles_enabled = 0;
|
|
return;
|
|
}
|
|
|
|
// FIRE!!!
|
|
if (Anim_bitmap_id_fire == -1)
|
|
{
|
|
Anim_bitmap_id_fire = bm_load_animation("particleexp01", &Anim_num_frames_fire, nullptr, NULL, 0);
|
|
}
|
|
|
|
// Cough, cough
|
|
if (Anim_bitmap_id_smoke == -1)
|
|
{
|
|
Anim_bitmap_id_smoke = bm_load_animation("particlesmoke01", &Anim_num_frames_smoke, nullptr, NULL, 0);
|
|
}
|
|
|
|
// wheeze
|
|
if (Anim_bitmap_id_smoke2 == -1)
|
|
{
|
|
Anim_bitmap_id_smoke2 = bm_load_animation("particlesmoke02", &Anim_num_frames_smoke2, nullptr, NULL, 0);
|
|
}
|
|
}
|
|
|
|
// only call from game_shutdown()!!!
|
|
void close()
|
|
{
|
|
Persistent_particles.clear();
|
|
Particles.clear();
|
|
}
|
|
|
|
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");
|
|
|
|
bool init_particle(particle* part, particle_info* info) {
|
|
if (!Particles_enabled)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// treat particles on lower detail levels as 'further away' for the purposes of culling
|
|
float adjusted_dist = vm_vec_dist(&Eye_position, &info->pos) * powf(2.5f, (float)(NUM_DEFAULT_DETAIL_LEVELS - Detail.num_particles));
|
|
// treat bigger particles as 'closer'
|
|
adjusted_dist /= info->rad;
|
|
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 false;
|
|
}
|
|
|
|
int fps = 1;
|
|
|
|
part->pos = info->pos;
|
|
part->velocity = info->vel;
|
|
part->age = 0.0f;
|
|
part->max_life = info->lifetime;
|
|
part->radius = info->rad;
|
|
part->type = info->type;
|
|
part->optional_data = info->optional_data;
|
|
part->attached_objnum = info->attached_objnum;
|
|
part->attached_sig = info->attached_sig;
|
|
part->reverse = info->reverse;
|
|
part->looping = false;
|
|
part->length = info->length;
|
|
part->angle = frand_range(0.0f, PI2);
|
|
part->size_lifetime_curve = info->size_lifetime_curve;
|
|
part->vel_lifetime_curve = info->vel_lifetime_curve;
|
|
|
|
switch (info->type)
|
|
{
|
|
case PARTICLE_BITMAP:
|
|
case PARTICLE_BITMAP_PERSISTENT:
|
|
{
|
|
Assertion(bm_is_valid(info->optional_data), "Invalid bitmap handle passed to particle create.");
|
|
|
|
bm_get_info(info->optional_data, NULL, NULL, NULL, &part->nframes, &fps);
|
|
|
|
if (part->nframes > 1 && info->lifetime_from_animation)
|
|
{
|
|
// Recalculate max life for ani's
|
|
part->max_life = i2fl(part->nframes) / i2fl(fps);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case PARTICLE_FIRE:
|
|
{
|
|
if (Anim_bitmap_id_fire < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
part->optional_data = Anim_bitmap_id_fire;
|
|
part->nframes = Anim_num_frames_fire;
|
|
|
|
break;
|
|
}
|
|
|
|
case PARTICLE_SMOKE:
|
|
{
|
|
if (Anim_bitmap_id_smoke < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
part->optional_data = Anim_bitmap_id_smoke;
|
|
part->nframes = Anim_num_frames_smoke;
|
|
|
|
break;
|
|
}
|
|
|
|
case PARTICLE_SMOKE2:
|
|
{
|
|
if (Anim_bitmap_id_smoke2 < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
part->optional_data = Anim_bitmap_id_smoke2;
|
|
part->nframes = Anim_num_frames_smoke2;
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
part->nframes = 1;
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void create(particle_info* pinfo) {
|
|
particle part;
|
|
if (!init_particle(&part, pinfo)) {
|
|
return;
|
|
}
|
|
|
|
Particles.push_back(part);
|
|
}
|
|
|
|
// Creates a single particle. See the PARTICLE_?? defines for types.
|
|
WeakParticlePtr createPersistent(particle_info* pinfo)
|
|
{
|
|
ParticlePtr new_particle = std::make_shared<particle>();
|
|
|
|
if (!init_particle(new_particle.get(), pinfo)) {
|
|
return WeakParticlePtr();
|
|
}
|
|
|
|
Persistent_particles.push_back(new_particle);
|
|
|
|
return WeakParticlePtr(new_particle);
|
|
}
|
|
|
|
void create(vec3d* pos,
|
|
vec3d* vel,
|
|
float lifetime,
|
|
float rad,
|
|
ParticleType type,
|
|
int optional_data,
|
|
object* objp,
|
|
bool reverse) {
|
|
particle_info pinfo;
|
|
|
|
Assertion((type >= 0) && (type < NUM_PARTICLE_TYPES), "Invalid particle type %d specified!", type);
|
|
|
|
// setup old data
|
|
pinfo.pos = *pos;
|
|
pinfo.vel = *vel;
|
|
pinfo.lifetime = lifetime;
|
|
pinfo.rad = rad;
|
|
pinfo.type = type;
|
|
pinfo.optional_data = optional_data;
|
|
|
|
// setup new data
|
|
if (objp == NULL) {
|
|
pinfo.attached_objnum = -1;
|
|
pinfo.attached_sig = -1;
|
|
} else {
|
|
pinfo.attached_objnum = OBJ_INDEX(objp);
|
|
pinfo.attached_sig = objp->signature;
|
|
}
|
|
pinfo.reverse = reverse;
|
|
|
|
// lower level function
|
|
create(&pinfo);
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
static 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->attached_objnum >= 0)
|
|
{
|
|
// if the signature has changed, or it's bogus, kill it
|
|
if ((part->attached_objnum >= MAX_OBJECTS) ||
|
|
(part->attached_sig != Objects[part->attached_objnum].signature))
|
|
{
|
|
remove_particle = true;
|
|
}
|
|
}
|
|
|
|
if (remove_particle)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
float vel_scalar = 1.0f;
|
|
if (part->vel_lifetime_curve >= 0) {
|
|
vel_scalar = Curves[part->vel_lifetime_curve].GetValue(part->age / part->max_life);
|
|
}
|
|
|
|
// move as a regular particle
|
|
part->pos += (part->velocity * vel_scalar) * frametime;
|
|
|
|
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, @c false otherwise
|
|
*/
|
|
static bool render_particle(particle* part) {
|
|
// skip back-facing particles (ripped from fullneb code)
|
|
// Wanderer - add support for attached particles
|
|
vec3d p_pos;
|
|
if (part->attached_objnum >= 0)
|
|
{
|
|
vm_vec_unrotate(&p_pos, &part->pos, &Objects[part->attached_objnum].orient);
|
|
vm_vec_add2(&p_pos, &Objects[part->attached_objnum].pos);
|
|
}
|
|
else
|
|
{
|
|
p_pos = part->pos;
|
|
}
|
|
|
|
if (vm_vec_dot_to_point(&Eye_matrix.vec.fvec, &Eye_position, &p_pos) <= 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)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
g3_transfer_vertex(&pos, &p_pos);
|
|
|
|
// figure out which frame we should be using
|
|
int framenum;
|
|
int cur_frame;
|
|
if (part->nframes > 1) {
|
|
framenum = bm_get_anim_frame(part->optional_data, part->age, part->max_life, part->looping);
|
|
cur_frame = part->reverse ? (part->nframes - framenum - 1) : framenum;
|
|
}
|
|
else
|
|
{
|
|
cur_frame = 0;
|
|
}
|
|
|
|
if (part->type == PARTICLE_DEBUG)
|
|
{
|
|
if (part->optional_data >= 0)
|
|
gr_set_color((part->optional_data >> 16) & 0xff, (part->optional_data >> 8) & 0xff, part->optional_data & 0xff);
|
|
else
|
|
gr_set_color(255, 0, 0);
|
|
g3_draw_sphere_ez(&p_pos, part->radius);
|
|
}
|
|
else
|
|
{
|
|
framenum = part->optional_data;
|
|
|
|
Assert( cur_frame < part->nframes );
|
|
|
|
float radius = part->radius;
|
|
if (part->size_lifetime_curve >= 0) {
|
|
radius *= Curves[part->size_lifetime_curve].GetValue(part->age / part->max_life);
|
|
}
|
|
|
|
if (part->length != 0.0f) {
|
|
vec3d p0 = part->pos;
|
|
|
|
vec3d p1;
|
|
vm_vec_copy_normalize(&p1, &part->velocity);
|
|
p1 *= part->length;
|
|
p1 += part->pos;
|
|
|
|
batching_add_laser(framenum + cur_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, Randomize_particle_rotation ? part->angle : Physics_viewer_bank, radius, alpha);
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void render_all()
|
|
{
|
|
GR_DEBUG_SCOPE("Render Particles");
|
|
TRACE_SCOPE(tracing::ParticlesRenderAll);
|
|
|
|
bool render_batch = false;
|
|
|
|
if (!Particles_enabled)
|
|
return;
|
|
|
|
if (Persistent_particles.empty() && Particles.empty())
|
|
return;
|
|
|
|
for (auto& part : Persistent_particles) {
|
|
if (render_particle(part.get())) {
|
|
render_batch = true;
|
|
}
|
|
}
|
|
|
|
for (auto& part : Particles) {
|
|
if (render_particle(&part)) {
|
|
render_batch = true;
|
|
}
|
|
}
|
|
|
|
if (render_batch)
|
|
{
|
|
batching_render_all();
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//============== HIGH-LEVEL PARTICLE SYSTEM CREATION CODE ====================
|
|
//============================================================================
|
|
|
|
// Use a structure rather than pass a ton of parameters to particle_emit
|
|
/*
|
|
typedef struct particle_emitter {
|
|
int num_low; // Lowest number of particles to create
|
|
int num_high; // Highest number of particles to create
|
|
vec3d pos; // Where the particles emit from
|
|
vec3d vel; // Initial velocity of all the particles
|
|
float lifetime; // How long the particles live
|
|
vec3d normal; // What normal the particle emit arond
|
|
float normal_variance; // How close they stick to that normal 0=good, 1=360 degree
|
|
float min_vel; // How fast the slowest particle can move
|
|
float max_vel; // How fast the fastest particle can move
|
|
float min_rad; // Min radius
|
|
float max_rad; // Max radius
|
|
} particle_emitter;
|
|
*/
|
|
|
|
// Creates a bunch of particles. You pass a structure
|
|
// rather than a bunch of parameters.
|
|
void emit(particle_emitter* pe, ParticleType type, int optional_data, float range)
|
|
{
|
|
int i, n;
|
|
|
|
if (!Particles_enabled)
|
|
return;
|
|
|
|
int n1, n2;
|
|
|
|
// Account for detail
|
|
int percent = get_percent(Detail.num_particles);
|
|
|
|
//Particle rendering drops out too soon. Seems to be around 150 m. Is it detail level controllable? I'd like it to be 500-1000
|
|
float min_dist = 125.0f;
|
|
float dist = vm_vec_dist_quick(&pe->pos, &Eye_position) / range;
|
|
if (dist > min_dist)
|
|
{
|
|
percent = fl2i(i2fl(percent) * min_dist / dist);
|
|
if (percent < 1)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
//mprintf(( "Dist = %.1f, percent = %d%%\n", dist, percent ));
|
|
|
|
n1 = (pe->num_low * percent) / 100;
|
|
n2 = (pe->num_high * percent) / 100;
|
|
|
|
// How many to emit?
|
|
n = Random::next(n1, n2);
|
|
|
|
if (n < 1) return;
|
|
|
|
|
|
for (i = 0; i < n; i++)
|
|
{
|
|
// Create a particle
|
|
vec3d tmp_vel;
|
|
vec3d normal; // What normal the particle emit arond
|
|
|
|
float radius = ((pe->max_rad - pe->min_rad) * frand()) + pe->min_rad;
|
|
|
|
float speed = ((pe->max_vel - pe->min_vel) * frand()) + pe->min_vel;
|
|
|
|
float life = ((pe->max_life - pe->min_life) * frand()) + pe->min_life;
|
|
|
|
normal.xyz.x = pe->normal.xyz.x + (frand() * 2.0f - 1.0f) * pe->normal_variance;
|
|
normal.xyz.y = pe->normal.xyz.y + (frand() * 2.0f - 1.0f) * pe->normal_variance;
|
|
normal.xyz.z = pe->normal.xyz.z + (frand() * 2.0f - 1.0f) * pe->normal_variance;
|
|
vm_vec_normalize_safe(&normal);
|
|
vm_vec_scale_add(&tmp_vel, &pe->vel, &normal, speed);
|
|
|
|
create(&pe->pos, &tmp_vel, life, radius, type, optional_data);
|
|
}
|
|
}
|
|
}
|