17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 22:07:20 +01:00
Files
maszyna/rendering/particles.cpp
jerrrrycho 4489dd7469 test(gfx): unit tests of the smoke of a vehicle taken off the tracks (#90)
The spawn count of the smoke source of a vehicle moves out of
smoke_source::update() into smoke::vehicle_spawn_count() (vehicle/smoke.h),
a free function on the mover state, which joins the core library so the
unit tests can call it the way the source does. The rates are the same as
before, the steady rate of the source still counted in before the share of
the engine.

Tests: tests/unit/test_smoke.cpp. A vehicle taken off the tracks spawns no
smoke and drops the pending particles, with the engine starting, idling or
under load; a road vehicle back from the pool puffs again; the engine adds
to the steady rate; the pending count stays within the room of the source.
Two of the four fail without the fix.

Checked again on td.scn with the SM42 driven into a portal: the smoke
source keeps puffing on the run and drops to 0 particles once the vehicle
is taken off the tracks.
2026-11-07 11:38:49 +01:00

459 lines
18 KiB
C++

/*
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 "rendering/particles.h"
#include "utilities/Timer.h"
#include "utilities/Globals.h"
#include "model/AnimModel.h"
#include "simulation/simulationenvironment.h"
#include "utilities/Logs.h"
#include "vehicle/smoke.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 != "}" ) ) {
if( key == "color:" ) {
// special case, vec3 attribute type
// TODO: variable table, if amount of vector attributes increases
color = Input.getToken<glm::vec3>( true, "\n\r\t ,;[]" );
color =
glm::clamp(
color / 255.f,
glm::vec3{ 0.f }, glm::vec3{ 1.f } );
}
else {
// float type attributes
auto const lookup { variablemap.find( key ) };
if( lookup != variablemap.end() ) {
lookup->second = Input.getToken<float>( true, "\n\r\t ,;[]" );
}
}
}
}
void
smoke_source::particle_emitter::initialize( smoke_particle &Particle ) const {
auto const polarangle { glm::radians( LocalRandom( inclination[ value_limit::min ], inclination[ value_limit::max ] ) ) }; // theta
auto const azimuthalangle { glm::radians( LocalRandom( -180, 180 ) ) }; // phi
// convert spherical coordinates to opengl coordinates
auto const launchvector { glm::vec3(
std::sin( polarangle ) * std::sin( azimuthalangle ) * -1,
std::cos( polarangle ),
std::sin( polarangle ) * std::cos( azimuthalangle ) ) };
auto const launchvelocity { static_cast<float>( LocalRandom( velocity[ value_limit::min ], velocity[ value_limit::max ] ) ) };
Particle.velocity = launchvector * launchvelocity;
Particle.rotation = static_cast<float>(glm::radians( LocalRandom( 0, 360 ) ));
Particle.size = static_cast<float>(LocalRandom( size[ value_limit::min ], size[ value_limit::max ] ));
Particle.opacity = static_cast<float>(LocalRandom( opacity[ value_limit::min ], opacity[ value_limit::max ] ) / Global.SmokeFidelity);
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_max_particles =
// put a cap on number of particles in a single source. TBD, TODO: make it part of he source configuration?
std::min(
static_cast<int>( 500 * Global.SmokeFidelity ),
// 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() ) ) );
}
void
smoke_source::bind( TDynamicObject const *Vehicle ) {
m_owner.vehicle = Vehicle;
m_ownertype = (
m_owner.vehicle != nullptr ?
owner_type::vehicle :
owner_type::none );
}
void
smoke_source::bind( TAnimModel const *Node ) {
m_owner.node = Node;
m_ownertype = (
m_owner.node != nullptr ?
owner_type::node :
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() } };
auto const spawning { ( true == Global.Smoke ) && ( false == Onlydespawn ) };
if( m_ownertype == owner_type::vehicle ) {
auto const &mover { *m_owner.vehicle->MoverParameters };
if( Global.AirTemperature <= 5.f || mover.dizel_heat.Ts < 45.f ) {
m_emitter.color = glm::vec3{ 128, 128, 128 };
}
// a vehicle taken off the tracks (gone through a portal, or waiting in the pool of the road traffic) puffs no more,
// the particles it already let out fade away as usual
m_spawncount = (
( true == spawning ) ?
smoke::vehicle_spawn_count(
mover, m_owner.vehicle->bEnabled,
{ m_spawncount, m_spawnrate, Global.SmokeFidelity, static_cast<float>( m_max_particles ) },
Timedelta ) :
0.f );
}
else {
m_spawncount = (
( true == spawning ) ?
std::min<float>(
static_cast<float>(m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity )),
static_cast<float>(m_max_particles) ) :
0.f );
}
// update spawned particles
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_particles.size() < m_max_particles ) ) {
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::ranges::sort(
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 );
}
// determine bounding area from calculated bounding box
if( false == m_particles.empty() ) {
m_area.center = glm::mix(boundingbox[value_limit::min], boundingbox[value_limit::max], 0.5);
m_area.radius = static_cast<float>(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 += m_owner.vehicle->GetPosition();
break;
}
case owner_type::node: {
auto const rotationx { glm::angleAxis( glm::radians( m_owner.node->Angles().x ), glm::vec3{ 1.f, 0.f, 0.f } ) };
auto const rotationy { glm::angleAxis( glm::radians( m_owner.node->Angles().y ), glm::vec3{ 0.f, 1.f, 0.f } ) };
auto const rotationz { glm::angleAxis( glm::radians( m_owner.node->Angles().z ), glm::vec3{ 0.f, 0.f, 1.f } ) };
location = rotationy * rotationx * rotationz * glm::vec3{ 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 *= static_cast<float>(m_owner.vehicle->MoverParameters->dizel_fill);
if (m_owner.vehicle->MoverParameters->EngineType == TEngineType::DieselElectric)
{
if (m_owner.vehicle->MoverParameters->dizel_spinup == true)
{
Particle.velocity *= 0.38*(((m_owner.vehicle->MoverParameters->enrot)/2)*0.5); // / m_owner.vehicle->MoverParameters->dizel_fill *0.01)) ;
}
else
{
Particle.velocity *= ((((m_owner.vehicle->MoverParameters->enrot)/60)*0.6) * (m_owner.vehicle->MoverParameters->EnginePower *0.03)); // / m_owner.vehicle->MoverParameters->dizel_fill *0.01)) ;
//Particle.velocity *= m_owner.vehicle->GetVelocity(); // / m_owner.vehicle->MoverParameters->dizel_fill *0.01)) ;
}
}
}
}
// 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
if (m_ownertype == owner_type::vehicle)
{
Particle.velocity.y += static_cast<float>(( 0.025 * Particle.velocity.y ) * std::min( 0.f, Global.AirTemperature - 90 ) * Timedelta); // decelerate faster in cold weather
Particle.velocity.y -= static_cast<float>(( (0.05 * (pow(m_owner.vehicle->GetVelocity()*1,0.4))) * Particle.velocity.y ) * Global.Overcast * Timedelta); // decelerate faster with high air humidity and/or precipitation
}
else
{
Particle.velocity.y += static_cast<float>(( 0.005 * Particle.velocity.y ) * std::min( 0.f, Global.AirTemperature - 10 ) * Timedelta); // decelerate faster in cold weather
Particle.velocity.y -= static_cast<float>(( 0.050 * Particle.velocity.y ) * Global.Overcast * Timedelta); // decelerate faster with high air humidity and/or precipitation
Particle.velocity.y = std::max<float>( static_cast<float>(0.25 * ( 2.f - Global.Overcast )), Particle.velocity.y ); // put a cap on deceleration
}
Particle.position += Particle.velocity * static_cast<float>( Timedelta );
Particle.position += 0.1f * Particle.age * simulation::Environment.wind() * static_cast<float>( Timedelta );
// m_velocitymodifier.update( Particle.velocity, Timedelta );
Particle.age += static_cast<float>(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;
}
bool
particle_manager::insert( std::string const &Sourcetemplate, TAnimModel const *Node, glm::dvec3 const Location ) {
if( false == insert( Sourcetemplate, Location ) ) { return false; }
// attach the source to specified node
auto &source { m_sources.back() };
source.bind( Node );
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...
if( auto const lookup { m_sourcetemplates.find( templatename ) }; 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 (cParser parser(templatepath + templatename + ".txt", cParser::buffer_FILE); source.deserialize(parser))
{
// if deserialization didn't fail finish source setup...
source.m_opacitymodifier.bind( &Global.SmokeFidelity );
// ...then cache the source as template for future instances
m_sourcetemplates.try_emplace( templatename, source );
// should be 'safe enough' to return lookup result directly afterwards
return &( m_sourcetemplates.find( templatename )->second );
}
else {
ErrorLog( "Bad file: failed to locate particle source configuration file \"" + std::string( templatepath + templatename + ".txt" ) + "\"", logtype::file );
}
// if fetching data from the file fails too, give up
return nullptr;
}