/* * 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 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(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(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(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); } } }