/* * 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 "globalincs/systemvars.h" #include "graphics/2d.h" #include "render/3d.h" #include "bmpman/bmpman.h" #include "particle/particle.h" #include "object/object.h" #include "cmdline/cmdline.h" #include "graphics/grbatch.h" #include "debugconsole/console.h" #include "graphics/gropenglextension.h" #ifndef NDEBUG #include "io/timer.h" #endif int Num_particles = 0; static SCP_vector 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; uint lastSignature = 0; // 0 is an invalid signature! int Particle_buffer_object = -1; int Geometry_shader_buffer_object = -1; // Reset everything between levels void particle_init() { int fps; Particles_enabled = (Detail.num_particles > 0); Num_particles = 0; Particles.clear(); // FIRE!!! if ( Anim_bitmap_id_fire == -1 ) { Anim_bitmap_id_fire = bm_load_animation( "particleexp01", &Anim_num_frames_fire, &fps, NULL, 0 ); } // Cough, cough if ( Anim_bitmap_id_smoke == -1 ) { Anim_bitmap_id_smoke = bm_load_animation( "particlesmoke01", &Anim_num_frames_smoke, &fps, NULL, 0 ); } // wheeze if ( Anim_bitmap_id_smoke2 == -1 ) { Anim_bitmap_id_smoke2 = bm_load_animation( "particlesmoke02", &Anim_num_frames_smoke2, &fps, NULL, 0 ); } // grab a vertex buffer object if ( Particle_buffer_object < 0 ) { Particle_buffer_object = gr_create_stream_buffer(); } if ( Geometry_shader_buffer_object < 0 && !Cmdline_no_geo_sdr_effects && Is_Extension_Enabled(OGL_EXT_GEOMETRY_SHADER4) ) { Geometry_shader_buffer_object = gr_create_stream_buffer(); } } // only call from game_shutdown()!!! void particle_close() { for (SCP_vector::iterator p = Particles.begin(); p != Particles.end(); ++p) { (*p)->signature = 0; delete *p; } Particles.clear(); } void particle_page_in() { bm_page_in_texture( Anim_bitmap_id_fire ); bm_page_in_texture( Anim_bitmap_id_smoke ); bm_page_in_texture( Anim_bitmap_id_smoke2 ); } DCF_BOOL2(particles, Particles_enabled, "Turns particles on/off", "Usage: particles [bool]\nTurns particle system on/off. If nothing passed, then toggles it.\n"); int Num_particles_hwm = 0; // Creates a single particle. See the PARTICLE_?? defines for types. particle *particle_create( particle_info *pinfo ) { if ( !Particles_enabled ) { return NULL; } particle* new_particle = new particle(); int fps = 1; new_particle->pos = pinfo->pos; new_particle->velocity = pinfo->vel; new_particle->age = 0.0f; new_particle->max_life = pinfo->lifetime; new_particle->radius = pinfo->rad; new_particle->type = pinfo->type; new_particle->optional_data = pinfo->optional_data; new_particle->tracer_length = pinfo->tracer_length; new_particle->attached_objnum = pinfo->attached_objnum; new_particle->attached_sig = pinfo->attached_sig; new_particle->reverse = pinfo->reverse; new_particle->particle_index = (int)Particles.size(); switch (pinfo->type) { case PARTICLE_BITMAP: case PARTICLE_BITMAP_PERSISTENT: { if (pinfo->optional_data < 0) { Int3(); delete new_particle; return NULL; } bm_get_info( pinfo->optional_data, NULL, NULL, NULL, &new_particle->nframes, &fps ); if ( new_particle->nframes > 1 ) { // Recalculate max life for ani's new_particle->max_life = i2fl(new_particle->nframes) / i2fl(fps); } break; } case PARTICLE_FIRE: { if (Anim_bitmap_id_fire < 0) { delete new_particle; return NULL; } new_particle->optional_data = Anim_bitmap_id_fire; new_particle->nframes = Anim_num_frames_fire; break; } case PARTICLE_SMOKE: { if (Anim_bitmap_id_smoke < 0) { delete new_particle; return NULL; } new_particle->optional_data = Anim_bitmap_id_smoke; new_particle->nframes = Anim_num_frames_smoke; break; } case PARTICLE_SMOKE2: { if (Anim_bitmap_id_smoke2 < 0) { delete new_particle; return NULL; } new_particle->optional_data = Anim_bitmap_id_smoke2; new_particle->nframes = Anim_num_frames_smoke2; break; } default: new_particle->nframes = 1; break; } new_particle->signature = ++lastSignature; Particles.push_back( new_particle ); #ifndef NDEBUG if (Particles.size() > (uint)Num_particles_hwm) { Num_particles_hwm = (int)Particles.size(); nprintf(("Particles", "Num_particles high water mark = %i\n", Num_particles_hwm)); } #endif return Particles.back(); } particle *particle_create( vec3d *pos, vec3d *vel, float lifetime, float rad, int type, int optional_data, float tracer_length, object *objp, bool reverse ) { particle_info pinfo; if ( (type < 0) || (type >= NUM_PARTICLE_TYPES) ) { Int3(); return NULL; } // 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 pinfo.tracer_length = -1.0f; 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? 1 : 0; // lower level function return particle_create(&pinfo); } MONITOR( NumParticles ) void particle_move_all(float frametime) { MONITOR_INC( NumParticles, Num_particles ); if ( !Particles_enabled ) return; if ( Particles.empty() ) return; for (SCP_vector::iterator p = Particles.begin(); p != Particles.end(); ) { particle* part = *p; if (part->age == 0.0f) { part->age = 0.00001f; } else { part->age += frametime; } bool remove_particle = false; // if its time expired, remove it if (part->age > part->max_life) { // 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) { part->signature = 0; delete part; // if we're sitting on the very last particle, popping-back will invalidate the iterator! if (p + 1 == Particles.end()) { Particles.pop_back(); break; } else { *p = Particles.back(); Particles.pop_back(); continue; } } // move as a regular particle vm_vec_scale_add2( &part->pos, &part->velocity, frametime ); // next particle ++p; } } // kill all active particles void particle_kill_all() { // kill all active particles Num_particles = 0; Num_particles_hwm = 0; for (SCP_vector::iterator p = Particles.begin(); p != Particles.end(); ++p) { (*p)->signature = 0; delete *p; } Particles.clear(); } MONITOR( NumParticlesRend ) static float get_current_alpha(vec3d *pos) { float dist; float alpha; const float inner_radius = 30.0f; const float magic_num = 2.75f; // 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 = 0.99999f / (inner_radius - magic_num); // above value times the # of meters away we are alpha *= (dist - magic_num); return (alpha < 0.05f) ? 0.0f : alpha; } return 0.99999f; } void particle_render_all() { ubyte flags; float pct_complete; float alpha; vertex pos; vec3d ts, te, temp; int rotate = 1; int framenum, cur_frame; bool render_batch = false; int tmap_flags = TMAP_FLAG_TEXTURED | TMAP_HTL_3D_UNLIT | TMAP_FLAG_SOFT_QUAD; if ( !Particles_enabled ) return; MONITOR_INC( NumParticlesRend, Num_particles ); if ( Particles.empty() ) return; for (SCP_vector::iterator p = Particles.begin(); p != Particles.end(); ++p) { particle* part = *p; // 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 ) { continue; } // calculate the alpha to draw at alpha = get_current_alpha(&p_pos); // if it's transparent then just skip it if (alpha <= 0.0f) { continue; } // make sure "rotate" is enabled for this particle rotate = 1; // if this is a tracer style particle, calculate tracer vectors if (part->tracer_length > 0.0f) { ts = p_pos; temp = part->velocity; vm_vec_normalize_quick(&temp); vm_vec_scale_add(&te, &ts, &temp, part->tracer_length); // don't bother rotating rotate = 0; } // rotate the vertex if (rotate) { flags = g3_rotate_vertex( &pos, &p_pos ); if ( flags ) { continue; } if (!Cmdline_nohtl) g3_transfer_vertex(&pos, &p_pos); } // pct complete for the particle pct_complete = part->age / part->max_life; // figure out which frame we should be using if (part->nframes > 1) { framenum = fl2i(pct_complete * part->nframes + 0.5); CLAMP(framenum, 0, part->nframes-1); cur_frame = part->reverse ? (part->nframes - framenum - 1) : framenum; } else { cur_frame = 0; } if (part->type == PARTICLE_DEBUG) { gr_set_color( 255, 0, 0 ); g3_draw_sphere_ez( &p_pos, part->radius ); } else { framenum = part->optional_data; Assert( cur_frame < part->nframes ); // if this is a tracer style particle if (part->tracer_length > 0.0f) { batch_add_laser( framenum + cur_frame, &ts, part->radius, &te, part->radius ); } // draw as a regular bitmap else { batch_add_bitmap( framenum + cur_frame, tmap_flags | TMAP_FLAG_VERTEX_GEN, &pos, part->particle_index % 8, part->radius, alpha ); } render_batch = true; } } profile_begin("Batch Render"); if (render_batch) { geometry_batch_render(Geometry_shader_buffer_object); batch_render_all(Particle_buffer_object); } profile_end("Batch Render"); } //============================================================================ //============== 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; */ static 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)) ); } // Creates a bunch of particles. You pass a structure // rather than a bunch of parameters. void particle_emit( particle_emitter *pe, int 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 = (rand() % (n2-n1+1)) + n1; if ( n < 1 ) return; for (i=0; imax_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 ); particle_create( &pe->pos, &tmp_vel, life, radius, type, optional_data ); } }