mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-03-22 15:05:03 +01:00
basic particle system implementation
This commit is contained in:
432
particles.cpp
Normal file
432
particles.cpp
Normal file
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "particles.h"
|
||||
|
||||
#include "Timer.h"
|
||||
#include "Globals.h"
|
||||
#include "AnimModel.h"
|
||||
#include "simulationenvironment.h"
|
||||
|
||||
|
||||
void
|
||||
smoke_source::particle_emitter::deserialize( cParser &Input ) {
|
||||
|
||||
if( Input.getToken<std::string>() != "{" ) { return; }
|
||||
|
||||
std::unordered_map<std::string, float &> const variablemap{
|
||||
{ "min_inclination:", inclination[ value_limit::min ] },
|
||||
{ "max_inclination:", inclination[ value_limit::max ] },
|
||||
{ "min_velocity:", velocity[ value_limit::min ] },
|
||||
{ "max_velocity:", velocity[ value_limit::max ] },
|
||||
{ "min_size:", size[ value_limit::min ] },
|
||||
{ "max_size:", size[ value_limit::max ] },
|
||||
{ "min_opacity:", opacity[ value_limit::min ] },
|
||||
{ "max_opacity:", opacity[ value_limit::max ] } };
|
||||
std::string key;
|
||||
|
||||
while( ( false == ( ( key = Input.getToken<std::string>( true, "\n\r\t ,;[]" ) ).empty() ) )
|
||||
&& ( key != "}" ) ) {
|
||||
|
||||
auto const lookup { variablemap.find( key ) };
|
||||
if( lookup == variablemap.end() ) { continue; }
|
||||
|
||||
lookup->second = Input.getToken<float>( true, "\n\r\t ,;[]" );
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
smoke_source::particle_emitter::initialize( smoke_particle &Particle ) {
|
||||
|
||||
auto const polarangle { glm::radians( Random( inclination[ value_limit::min ], inclination[ value_limit::max ] ) ) }; // theta
|
||||
auto const azimuthalangle { glm::radians( Random( -180, 180 ) ) }; // phi
|
||||
// convert spherical coordinates to opengl coordinates
|
||||
auto const launchvector { glm::vec3(
|
||||
std::sin( polarangle ) * std::sin( azimuthalangle ),
|
||||
std::cos( polarangle ),
|
||||
std::sin( polarangle ) * std::cos( azimuthalangle ) * -1 ) };
|
||||
auto const launchvelocity { static_cast<float>( Random( velocity[ value_limit::min ], velocity[ value_limit::max ] ) ) };
|
||||
|
||||
Particle.velocity = launchvector * launchvelocity;
|
||||
|
||||
Particle.rotation = glm::radians( Random( 0, 360 ) );
|
||||
Particle.size = Random( size[ value_limit::min ], size[ value_limit::max ] );
|
||||
Particle.opacity = Random( opacity[ value_limit::min ], opacity[ value_limit::max ] );
|
||||
Particle.age = 0;
|
||||
}
|
||||
|
||||
bool
|
||||
smoke_source::deserialize( cParser &Input ) {
|
||||
|
||||
if( false == Input.ok() ) { return false; }
|
||||
|
||||
while( true == deserialize_mapping( Input ) ) {
|
||||
; // all work done by while()
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// imports member data pair from the config file
|
||||
bool
|
||||
smoke_source::deserialize_mapping( cParser &Input ) {
|
||||
|
||||
// token can be a key or block end
|
||||
std::string const key { Input.getToken<std::string>( true, "\n\r\t ,;[]" ) };
|
||||
|
||||
if( ( true == key.empty() ) || ( key == "}" ) ) { return false; }
|
||||
|
||||
// if not block end then the key is followed by assigned value or sub-block
|
||||
if( key == "spawn_rate:" ) {
|
||||
Input.getTokens();
|
||||
Input >> m_spawnrate;
|
||||
}
|
||||
else if( key == "initializer:" ) {
|
||||
m_emitter.deserialize( Input );
|
||||
}
|
||||
/*
|
||||
else if( key == "velocity_change:" ) {
|
||||
m_velocitymodifier.deserialize( Input );
|
||||
}
|
||||
*/
|
||||
else if( key == "size_change:" ) {
|
||||
m_sizemodifier.deserialize( Input );
|
||||
}
|
||||
else if( key == "opacity_change:" ) {
|
||||
m_opacitymodifier.deserialize( Input );
|
||||
}
|
||||
|
||||
return true; // return value marks a [ key: value ] pair was extracted, nothing about whether it's recognized
|
||||
}
|
||||
|
||||
void
|
||||
smoke_source::initialize() {
|
||||
|
||||
m_particles.reserve(
|
||||
// put a cap on number of particles in a single source. TBD, TODO: make it part of he source configuration?
|
||||
std::min(
|
||||
500,
|
||||
// NOTE: given nature of the smoke we're presuming opacity decreases over time and the particle is killed when it reaches 0
|
||||
// this gives us estimate of longest potential lifespan of single particle, and how many particles total can there be at any given time
|
||||
// TBD, TODO: explicit lifespan variable as part of the source configuration?
|
||||
static_cast<int>( m_spawnrate / std::abs( m_opacitymodifier.value_change() ) ) ) );
|
||||
/*
|
||||
m_particlehead =
|
||||
m_particletail =
|
||||
std::begin( m_particles );
|
||||
*/
|
||||
}
|
||||
|
||||
void
|
||||
smoke_source::bind( TDynamicObject const *Vehicle ) {
|
||||
|
||||
m_owner.vehicle = Vehicle;
|
||||
m_ownertype = (
|
||||
m_owner.vehicle != nullptr ?
|
||||
owner_type::vehicle :
|
||||
owner_type::none );
|
||||
}
|
||||
|
||||
// updates state of owned particles
|
||||
void
|
||||
smoke_source::update( double const Timedelta, bool const Onlydespawn ) {
|
||||
|
||||
// prepare bounding box for new pass
|
||||
// TODO: include bounding box in the bounding_area class
|
||||
bounding_box boundingbox {
|
||||
glm::dvec3{ std::numeric_limits<double>::max() },
|
||||
glm::dvec3{ std::numeric_limits<double>::lowest() } };
|
||||
|
||||
m_spawncount = (
|
||||
Onlydespawn ?
|
||||
0.f :
|
||||
std::min<float>(
|
||||
m_spawncount + ( m_spawnrate * Timedelta ),
|
||||
m_particles.capacity() ) );
|
||||
// update spawned particles
|
||||
/*
|
||||
while( m_particlehead != m_particletail ) {
|
||||
|
||||
auto &particle { *m_particlehead };
|
||||
bool particleisalive;
|
||||
|
||||
while( ( false == ( particleisalive = update( particle, Timedelta ) ) )
|
||||
&& ( m_spawncount >= 1.f ) ) {
|
||||
// replace dead particle with a new one
|
||||
m_spawncount -= 1.f;
|
||||
initialize( particle );
|
||||
}
|
||||
if( false == particleisalive ) {
|
||||
// we have a dead particle and no pending spawn requests, (try to) move the last particle here
|
||||
do {
|
||||
--m_particletail;
|
||||
if( m_particlehead == m_particletail ) { break; }
|
||||
particle = *m_particletail;
|
||||
} while( false == ( particleisalive = update( particle, Timedelta ) ) );
|
||||
}
|
||||
if( true == particleisalive ) {
|
||||
// ensure at the end of the pass the head iterator is placed after the last alive particle
|
||||
++m_particlehead;
|
||||
}
|
||||
}
|
||||
*/
|
||||
for( auto particleiterator { std::begin( m_particles ) }; particleiterator != std::end( m_particles ); ++particleiterator ) {
|
||||
|
||||
auto &particle { *particleiterator };
|
||||
bool particleisalive;
|
||||
|
||||
while( ( false == ( particleisalive = update( particle, boundingbox, Timedelta ) ) )
|
||||
&& ( m_spawncount >= 1.f ) ) {
|
||||
// replace dead particle with a new one
|
||||
m_spawncount -= 1.f;
|
||||
initialize( particle );
|
||||
}
|
||||
if( false == particleisalive ) {
|
||||
// we have a dead particle and no pending spawn requests, (try to) move the last particle here
|
||||
do {
|
||||
if( std::next( particleiterator ) == std::end( m_particles ) ) { break; } // already at last particle
|
||||
particle = m_particles.back();
|
||||
m_particles.pop_back();
|
||||
} while( false == ( particleisalive = update( particle, boundingbox, Timedelta ) ) );
|
||||
}
|
||||
if( false == particleisalive ) {
|
||||
// NOTE: if we're here it means the iterator is at last container slot which holds a dead particle about to be eliminated...
|
||||
m_particles.pop_back();
|
||||
// ...since this effectively makes the iterator now point at end() and the advancement at the end of the loop will move it past end()
|
||||
// we have to break the loop manually (could use < comparison but with both ways being ugly, this is
|
||||
break;
|
||||
}
|
||||
}
|
||||
// spawn pending particles in remaining container slots
|
||||
/*
|
||||
while( ( m_spawncount >= 1.f )
|
||||
&& ( m_particlehead != std::end( m_particles ) ) ) {
|
||||
|
||||
m_spawncount -= 1.f;
|
||||
auto &particle { *m_particlehead };
|
||||
initialize( particle );
|
||||
if( true == update( particle, Timedelta ) ) {
|
||||
++m_particlehead;
|
||||
}
|
||||
}
|
||||
*/
|
||||
while( ( m_spawncount >= 1.f )
|
||||
&& ( m_particles.size() < m_particles.capacity() ) ) {
|
||||
|
||||
m_spawncount -= 1.f;
|
||||
// work with a temporary copy in case initial update renders the particle dead
|
||||
smoke_particle newparticle;
|
||||
initialize( newparticle );
|
||||
if( true == update( newparticle, boundingbox, Timedelta ) ) {
|
||||
// if the new particle didn't die immediately place it in the container...
|
||||
m_particles.emplace_back( newparticle );
|
||||
}
|
||||
}
|
||||
// if we still have pending requests after filling entire container replace older particles
|
||||
if( m_spawncount >= 1.f ) {
|
||||
// sort all particles from most to least transparent, oldest to youngest if it's a tie
|
||||
std::sort(
|
||||
std::begin( m_particles ),
|
||||
std::end( m_particles ),
|
||||
[]( smoke_particle const &Left, smoke_particle const &Right ) {
|
||||
return ( Left.opacity != Right.opacity ?
|
||||
Left.opacity < Right.opacity :
|
||||
Left.age > Right.age ); } );
|
||||
// replace old particles with new ones until we run out of either requests or room
|
||||
for( auto &particle : m_particles ) {
|
||||
|
||||
while( m_spawncount >= 1.f ) {
|
||||
m_spawncount -= 1.f;
|
||||
// work with a temporary copy so we don't wind up with replacing a good particle with a dead on arrival one
|
||||
smoke_particle newparticle;
|
||||
initialize( newparticle );
|
||||
if( true == update( newparticle, boundingbox, Timedelta ) ) {
|
||||
// if the new particle didn't die immediately place it in the container...
|
||||
particle = newparticle;
|
||||
// ...and move on to the next slot
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// discard pending spawn requests our container couldn't fit
|
||||
m_spawncount -= std::floor( m_spawncount );
|
||||
}
|
||||
/*
|
||||
// after the pass the head iterator is left last alive particle
|
||||
m_particletail = m_particlehead;
|
||||
m_particlehead = std::begin( m_particles );
|
||||
*/
|
||||
|
||||
// determine bounding area from calculated bounding box
|
||||
if( false == m_particles.empty() ) {
|
||||
m_area.center = interpolate( boundingbox[ value_limit::min ], boundingbox[ value_limit::max ], 0.5 );
|
||||
m_area.radius = 0.5 * ( glm::length( boundingbox[ value_limit::max ] - boundingbox[ value_limit::min ] ) );
|
||||
}
|
||||
else {
|
||||
m_area.center = location();
|
||||
m_area.radius = 0;
|
||||
}
|
||||
}
|
||||
|
||||
glm::dvec3
|
||||
smoke_source::location() const {
|
||||
|
||||
glm::dvec3 location;
|
||||
|
||||
switch( m_ownertype ) {
|
||||
case owner_type::vehicle: {
|
||||
location = glm::dvec3 {
|
||||
m_offset.x * m_owner.vehicle->VectorLeft()
|
||||
+ m_offset.y * m_owner.vehicle->VectorUp()
|
||||
+ m_offset.z * m_owner.vehicle->VectorFront() };
|
||||
location += glm::dvec3{ m_owner.vehicle->GetPosition() };
|
||||
break;
|
||||
}
|
||||
case owner_type::node: {
|
||||
// TODO: take into account node rotation
|
||||
location = m_offset;
|
||||
location += m_owner.node->location();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
location = m_offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
// sets particle state to fresh values
|
||||
void
|
||||
smoke_source::initialize( smoke_particle &Particle ) {
|
||||
|
||||
m_emitter.initialize( Particle );
|
||||
|
||||
Particle.position = location();
|
||||
|
||||
if( m_ownertype == owner_type::vehicle ) {
|
||||
Particle.opacity *= m_owner.vehicle->MoverParameters->dizel_fill;
|
||||
switch( m_owner.vehicle->MoverParameters->EngineType ) {
|
||||
case TEngineType::DieselElectric: {
|
||||
Particle.velocity *= 1.0 + m_owner.vehicle->MoverParameters->enrot / ( m_owner.vehicle->MoverParameters->DElist[ m_owner.vehicle->MoverParameters->MainCtrlPosNo ].RPM / 60.0 );
|
||||
break;
|
||||
}
|
||||
case TEngineType::DieselEngine: {
|
||||
Particle.velocity *= 1.0 + m_owner.vehicle->MoverParameters->enrot / m_owner.vehicle->MoverParameters->nmax;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updates state of provided particle and bounding box. returns: true if particle is still alive afterwards, false otherwise
|
||||
bool
|
||||
smoke_source::update( smoke_particle &Particle, bounding_box &Boundingbox, double const Timedelta ) {
|
||||
|
||||
m_opacitymodifier.update( Particle.opacity, Timedelta );
|
||||
// if the particle is dead we can bail out early...
|
||||
if( Particle.opacity <= 0.f ) { return false; }
|
||||
// ... otherwise proceed with full update
|
||||
m_sizemodifier.update( Particle.size, Timedelta );
|
||||
|
||||
// crude smoke dispersion simulation
|
||||
// http://www.auburn.edu/academic/forestry_wildlife/fire/smoke_guide/smoke_dispersion.htm
|
||||
Particle.velocity.y = std::max<float>( 0.25 * ( 1.f - Global.Overcast ), Particle.velocity.y - 0.25 * Global.Overcast * Timedelta );
|
||||
|
||||
Particle.position += Particle.velocity * static_cast<float>( Timedelta );
|
||||
Particle.position += 0.35f * simulation::Environment.wind() * static_cast<float>( Timedelta );
|
||||
// m_velocitymodifier.update( Particle.velocity, Timedelta );
|
||||
|
||||
Particle.age += Timedelta;
|
||||
|
||||
// update bounding box
|
||||
Boundingbox[ value_limit::min ] = glm::min( Boundingbox[ value_limit::min ], Particle.position - glm::dvec3{ Particle.size } );
|
||||
Boundingbox[ value_limit::max ] = glm::max( Boundingbox[ value_limit::max ], Particle.position + glm::dvec3{ Particle.size } );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// adds a new particle source of specified type, placing it in specified world location
|
||||
// returns: true on success, false if the specified type definition couldn't be located
|
||||
bool
|
||||
particle_manager::insert( std::string const &Sourcetemplate, glm::dvec3 const Location ) {
|
||||
|
||||
auto const *sourcetemplate { find( Sourcetemplate ) };
|
||||
|
||||
if( sourcetemplate == nullptr ) { return false; }
|
||||
|
||||
// ...if template lookup didn't fail put template clone on the source list and initialize it
|
||||
m_sources.emplace_back( *sourcetemplate );
|
||||
auto &source { m_sources.back() };
|
||||
source.initialize();
|
||||
source.m_offset = Location;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
particle_manager::insert( std::string const &Sourcetemplate, TDynamicObject const *Vehicle, glm::dvec3 const Location ) {
|
||||
|
||||
if( false == insert( Sourcetemplate, Location ) ) { return false; }
|
||||
|
||||
// attach the source to specified vehicle
|
||||
auto &source { m_sources.back() };
|
||||
source.bind( Vehicle );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// updates state of all owned emitters
|
||||
void
|
||||
particle_manager::update() {
|
||||
|
||||
auto const timedelta { Timer::GetDeltaTime() };
|
||||
|
||||
if( timedelta == 0.0 ) { return; }
|
||||
|
||||
auto const distancethreshold { 2 * Global.BaseDrawRange * Global.fDistanceFactor }; // to reduce workload distant enough sources won't spawn new particles
|
||||
|
||||
for( auto &source : m_sources ) {
|
||||
|
||||
auto const viewerdistance { glm::length( source.area().center - glm::dvec3{ Global.pCamera.Pos } ) - source.area().radius };
|
||||
|
||||
source.update( timedelta, viewerdistance > distancethreshold );
|
||||
}
|
||||
}
|
||||
|
||||
smoke_source *
|
||||
particle_manager::find( std::string const &Template ) {
|
||||
|
||||
auto const templatepath { "data/" };
|
||||
auto const templatename { ToLower( Template ) };
|
||||
|
||||
// try to locate specified rail profile...
|
||||
auto const lookup { m_sourcetemplates.find( templatename ) };
|
||||
if( lookup != m_sourcetemplates.end() ) {
|
||||
// ...if it works, we're done...
|
||||
return &(lookup->second);
|
||||
}
|
||||
// ... and if it fails try to add the template to the database from a data file
|
||||
smoke_source source;
|
||||
if( source.deserialize( cParser( templatepath + templatename + ".txt", cParser::buffer_FILE ) ) ) {
|
||||
// if deserialization didn't fail cache the source as template for future instances
|
||||
m_sourcetemplates.emplace( templatename, source );
|
||||
// should be 'safe enough' to return lookup result directly afterwards
|
||||
return &( m_sourcetemplates.find( templatename )->second );
|
||||
}
|
||||
// if fetching data from the file fails too, give up
|
||||
return nullptr;
|
||||
}
|
||||
Reference in New Issue
Block a user