From 3feca56c0861cf7be4b2a6d4e8c9322e8a9a77b5 Mon Sep 17 00:00:00 2001 From: tmj-fstate Date: Tue, 16 Jul 2019 10:32:57 +0200 Subject: [PATCH 1/5] build 190712. heating generator state cab controls, engine state ui readout fix, minor ai logic tweaks --- Driver.cpp | 41 +++++++++++++++++++++++------------------ McZapkie/Mover.cpp | 2 ++ Train.cpp | 12 ++++++++++++ driveruipanels.cpp | 14 ++++++++------ version.h | 2 +- 5 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Driver.cpp b/Driver.cpp index a3b15065..9f54a144 100644 --- a/Driver.cpp +++ b/Driver.cpp @@ -3440,30 +3440,35 @@ void TController::SpeedSet() break; case TEngineType::DieselEngine: // Ra 2014-06: "automatyczna" skrzynia biegów... - if (!mvControlling->MotorParam[mvControlling->ScndCtrlPos].AutoSwitch) // gdy biegi ręczne - if ((mvControlling->ShuntMode ? mvControlling->AnPos : 1.0) * mvControlling->Vel > - 0.75 * mvControlling->MotorParam[mvControlling->ScndCtrlPos].mfi) + if( false == mvControlling->MotorParam[ mvControlling->ScndCtrlPos ].AutoSwitch ) { + // gdy biegi ręczne + if( ( mvControlling->ShuntMode ? mvControlling->AnPos : 1.0 ) * mvControlling->Vel > + 0.75 * mvControlling->MotorParam[ mvControlling->ScndCtrlPos ].mfi ) // if (mvControlling->enrot>0.95*mvControlling->dizel_nMmax) //youBy: jeśli obroty > // 0,95 nmax, wrzuć wyższy bieg - Ra: to nie działa { // jak prędkość większa niż 0.6 maksymalnej na danym biegu, wrzucić wyższy - mvControlling->DecMainCtrl(2); - if (mvControlling->IncScndCtrl(1)) - if (mvControlling->MotorParam[mvControlling->ScndCtrlPos].mIsat == - 0.0) // jeśli bieg jałowy - mvControlling->IncScndCtrl(1); // to kolejny + if( mvControlling->ScndCtrlPos < mvControlling->ScndCtrlPosNo ) { + // ...presuming there is a higher gear + mvControlling->DecMainCtrl( 2 ); + if( mvControlling->IncScndCtrl( 1 ) ) { + while( ( mvControlling->MotorParam[ mvControlling->ScndCtrlPos ].mIsat == 0.0 ) // jeśli bieg jałowy + && ( mvControlling->IncScndCtrl( 1 ) ) ) { // to kolejny + ; + } + } + } } - else if ((mvControlling->ShuntMode ? mvControlling->AnPos : 1.0) * mvControlling->Vel < - mvControlling->MotorParam[mvControlling->ScndCtrlPos].fi) - { // jak prędkość mniejsza niż minimalna na danym biegu, wrzucić niższy - mvControlling->DecMainCtrl(2); - mvControlling->DecScndCtrl(1); - if (mvControlling->MotorParam[mvControlling->ScndCtrlPos].mIsat == - 0.0) // jeśli bieg jałowy - if (mvControlling->ScndCtrlPos) // a jeszcze zera nie osiągnięto - mvControlling->DecScndCtrl(1); // to kolejny wcześniejszy + else if( ( mvControlling->ShuntMode ? mvControlling->AnPos : 1.0 ) * mvControlling->Vel < + mvControlling->MotorParam[ mvControlling->ScndCtrlPos ].fi ) { // jak prędkość mniejsza niż minimalna na danym biegu, wrzucić niższy + mvControlling->DecMainCtrl( 2 ); + mvControlling->DecScndCtrl( 1 ); + if( mvControlling->MotorParam[ mvControlling->ScndCtrlPos ].mIsat == 0.0 ) // jeśli bieg jałowy + if( mvControlling->ScndCtrlPos ) // a jeszcze zera nie osiągnięto + mvControlling->DecScndCtrl( 1 ); // to kolejny wcześniejszy else - mvControlling->IncScndCtrl(1); // a jak zeszło na zero, to powrót + mvControlling->IncScndCtrl( 1 ); // a jak zeszło na zero, to powrót } + } break; } }; diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 6b3efdae..6f35b399 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -1572,6 +1572,8 @@ void TMoverParameters::HeatingCheck( double const Timestep ) { auto const absrevolutions { std::abs( generator.revolutions ) }; generator.voltage = ( + false == HeatingAllow ? 0.0 : + // TODO: add support for desired voltage selector absrevolutions < generator.revolutions_min ? generator.voltage_min * absrevolutions / generator.revolutions_min : // absrevolutions > generator.revolutions_max ? generator.voltage_max * absrevolutions / generator.revolutions_max : interpolate( diff --git a/Train.cpp b/Train.cpp index 49bb59c0..08dbf68b 100644 --- a/Train.cpp +++ b/Train.cpp @@ -8377,6 +8377,18 @@ bool TTrain::initialize_gauge(cParser &Parser, std::string const &Label, int con gauge.AssignDouble(&mvControlled->AnPos); m_controlmapper.insert( gauge, "shuntmodepower:" ); } + else if( Label == "heatingvoltage:" ) { + if( mvControlled->HeatingPowerSource.SourceType == TPowerSource::Generator ) { + auto &gauge = Cabine[ Cabindex ].Gauge( -1 ); // pierwsza wolna gałka + gauge.Load( Parser, DynamicObject ); + gauge.AssignDouble( &(mvControlled->HeatingPowerSource.EngineGenerator.voltage) ); + } + } + else if( Label == "heatingcurrent:" ) { + auto &gauge = Cabine[ Cabindex ].Gauge( -1 ); // pierwsza wolna gałka + gauge.Load( Parser, DynamicObject ); + gauge.AssignDouble( &( mvControlled->TotalCurrent ) ); + } else { // failed to match the label diff --git a/driveruipanels.cpp b/driveruipanels.cpp index 9047f561..4845d6d6 100644 --- a/driveruipanels.cpp +++ b/driveruipanels.cpp @@ -750,17 +750,14 @@ debug_panel::update_vehicle_brake() const { void debug_panel::update_section_engine( std::vector &Output ) { - - if( m_input.train == nullptr ) { return; } + // engine data if( m_input.vehicle == nullptr ) { return; } if( m_input.mover == nullptr ) { return; } - auto const &train { *m_input.train }; auto const &vehicle{ *m_input.vehicle }; auto const &mover{ *m_input.mover }; - // engine data - // induction motor data + // induction motor data if( mover.EngineType == TEngineType::ElectricInductionMotor ) { Output.emplace_back( " eimc: eimv: press:", Global.UITextColor ); @@ -772,7 +769,11 @@ debug_panel::update_section_engine( std::vector &Output ) { + mover.eimv_labels[ i ] + to_string( mover.eimv[ i ], 2, 9 ); if( i < 10 ) { - parameters += " | " + train.fPress_labels[ i ] + to_string( train.fPress[ i ][ 0 ], 2, 9 ); + // NOTE: we pull consist data from the train structure, so show this data only if we're viewing controlled vehicle + parameters += + ( ( m_input.train != nullptr ) && ( m_input.train->Dynamic() == m_input.vehicle ) ? + " | " + TTrain::fPress_labels[ i ] + to_string( m_input.train->fPress[ i ][ 0 ], 2, 9 ) : + "" ); } else if( i == 12 ) { parameters += " med:"; @@ -784,6 +785,7 @@ debug_panel::update_section_engine( std::vector &Output ) { Output.emplace_back( parameters, Global.UITextColor ); } } + // diesel engine data if( mover.EngineType == TEngineType::DieselEngine ) { std::string parameterstext = "param value"; diff --git a/version.h b/version.h index 67a81d3e..9f573cd6 100644 --- a/version.h +++ b/version.h @@ -1,5 +1,5 @@ #pragma once #define VERSION_MAJOR 19 -#define VERSION_MINOR 701 +#define VERSION_MINOR 712 #define VERSION_REVISION 0 From 95aab4b629e39f1aa127e0e638b98d4f5413627b Mon Sep 17 00:00:00 2001 From: tmj-fstate Date: Sun, 21 Jul 2019 17:15:34 +0200 Subject: [PATCH 2/5] shp activation fix --- McZapkie/Mover.cpp | 50 +++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 6f35b399..bfc76504 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -2373,8 +2373,9 @@ void TMoverParameters::SecuritySystemCheck(double dt) if ((!Radio)) RadiostopSwitch(false); - if ((SecuritySystem.SystemType > 0) && (SecuritySystem.Status > 0) && - (Battery)) // Ra: EZT ma teraz czuwak w rozrządczym + if ((SecuritySystem.SystemType > 0) + && (SecuritySystem.Status > 0) + && (Battery)) // Ra: EZT ma teraz czuwak w rozrządczym { // CA if( ( SecuritySystem.AwareMinSpeed > 0.0 ? @@ -2402,30 +2403,29 @@ void TMoverParameters::SecuritySystemCheck(double dt) SecuritySystem.EmergencyBrakeDelay) && (SecuritySystem.EmergencyBrakeDelay >= 0)) SetFlag(SecuritySystem.Status, s_CAebrake); - - // SHP - if (TestFlag(SecuritySystem.SystemType, 2) && - TestFlag(SecuritySystem.Status, s_active)) // jeśli świeci albo miga - SecuritySystem.SystemSoundSHPTimer += dt; - if (TestFlag(SecuritySystem.SystemType, 2) && - TestFlag(SecuritySystem.Status, s_SHPalarm)) // jeśli buczy + } + // SHP + if( TestFlag( SecuritySystem.SystemType, 2 ) ) { + if( TestFlag( SecuritySystem.Status, s_SHPalarm ) ) { + // jeśli buczy SecuritySystem.SystemBrakeSHPTimer += dt; - if (TestFlag(SecuritySystem.SystemType, 2) && TestFlag(SecuritySystem.Status, s_active)) - if ((Vel > SecuritySystem.VelocityAllowed) && (SecuritySystem.VelocityAllowed >= 0)) - SetFlag(SecuritySystem.Status, s_SHPebrake); - else if (((SecuritySystem.SystemSoundSHPTimer > SecuritySystem.SoundSignalDelay) && - (SecuritySystem.SoundSignalDelay >= 0)) || - ((Vel > SecuritySystem.NextVelocityAllowed) && - (SecuritySystem.NextVelocityAllowed >= 0))) - if (!SetFlag(SecuritySystem.Status, - s_SHPalarm)) // juz wlaczony sygnal dzwiekowy} - if ((SecuritySystem.SystemBrakeSHPTimer > - SecuritySystem.EmergencyBrakeDelay) && - (SecuritySystem.EmergencyBrakeDelay >= 0)) - SetFlag(SecuritySystem.Status, s_SHPebrake); - - } // else SystemTimer:=0; + } + if( TestFlag( SecuritySystem.Status, s_active ) ) { + // jeśli świeci albo miga + SecuritySystem.SystemSoundSHPTimer += dt; + if( ( SecuritySystem.VelocityAllowed >= 0 ) && ( Vel > SecuritySystem.VelocityAllowed ) ) { + SetFlag( SecuritySystem.Status, s_SHPebrake ); + } + else if( ( ( SecuritySystem.SoundSignalDelay >= 0 ) && ( SecuritySystem.SystemSoundSHPTimer > SecuritySystem.SoundSignalDelay ) ) + || ( ( SecuritySystem.NextVelocityAllowed >= 0 ) && ( Vel > SecuritySystem.NextVelocityAllowed ) ) ) { + SetFlag( SecuritySystem.Status, s_SHPalarm ); + if( ( SecuritySystem.EmergencyBrakeDelay >= 0 ) && ( SecuritySystem.SystemBrakeSHPTimer > SecuritySystem.EmergencyBrakeDelay ) ) { + SetFlag( SecuritySystem.Status, s_SHPebrake ); + } + } + } + } // TEST CA if (TestFlag(SecuritySystem.Status, s_CAtest)) // jeśli świeci albo miga SecuritySystem.SystemBrakeCATestTimer += dt; @@ -10308,7 +10308,7 @@ bool TMoverParameters::RunCommand( std::string Command, double CValue1, double C else if ((CValue1 == 0)) Battery = false; if ((Battery) && (ActiveCab != 0) /*or (TrainType=dt_EZT)*/) - SecuritySystem.Status = SecuritySystem.Status || s_waiting; // aktywacja czuwaka + SecuritySystem.Status = SecuritySystem.Status | s_waiting; // aktywacja czuwaka else SecuritySystem.Status = 0; // wyłączenie czuwaka OK = SendCtrlToNext( Command, CValue1, CValue2, Couplertype ); From fbd2ff85fad24a95600561cf6c51eb4fa6bf7679 Mon Sep 17 00:00:00 2001 From: tmj-fstate Date: Tue, 6 Aug 2019 14:06:46 +0200 Subject: [PATCH 3/5] basic particle system implementation --- AnimModel.cpp | 12 + Classes.h | 1 + McZapkie/Mover.cpp | 3 + Model3d.cpp | 53 ++++- Model3d.h | 10 + drivermode.cpp | 3 + parser.cpp | 13 + parser.h | 8 +- particles.cpp | 432 ++++++++++++++++++++++++++++++++++ particles.h | 229 ++++++++++++++++++ renderer.cpp | 169 ++++++++++++- renderer.h | 50 ++++ simulation.cpp | 2 + simulation.h | 1 + simulationenvironment.cpp | 38 ++- simulationenvironment.h | 19 +- simulationstateserializer.cpp | 12 + 17 files changed, 1045 insertions(+), 10 deletions(-) create mode 100644 particles.cpp create mode 100644 particles.h diff --git a/AnimModel.cpp b/AnimModel.cpp index 8d90ba0b..71900216 100644 --- a/AnimModel.cpp +++ b/AnimModel.cpp @@ -422,7 +422,19 @@ bool TAnimModel::Init(std::string const &asName, std::string const &asReplacable asText = asReplacableTexture.substr( 1, asReplacableTexture.length() - 1 ); // zapamiętanie tekstu } else if( asReplacableTexture != "none" ) { +/* + auto const texturepath { substr_path( asReplacableTexture ) }; + if( false == texturepath.empty() ) { + Global.asCurrentTexturePath = texturepath; + } +*/ m_materialdata.replacable_skins[ 1 ] = GfxRenderer.Fetch_Material( asReplacableTexture ); +/* + if( false == texturepath.empty() ) { + // z powrotem defaultowa sciezka do tekstur + Global.asCurrentTexturePath = std::string( szTexturePath ); + } +*/ } if( ( m_materialdata.replacable_skins[ 1 ] != null_handle ) && ( GfxRenderer.Material( m_materialdata.replacable_skins[ 1 ] ).has_alpha ) ) { diff --git a/Classes.h b/Classes.h index 8c4209cc..a01470cc 100644 --- a/Classes.h +++ b/Classes.h @@ -40,6 +40,7 @@ class powergridsource_table; class instance_table; class vehicle_table; struct light_array; +class particle_manager; struct dictionary_source; namespace scene { diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index bfc76504..3794c78a 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -4553,9 +4553,12 @@ double TMoverParameters::TractionForce( double dt ) { EngineHeatingRPM ) / 60.0 ); } + // NOTE: fake dizel_fill calculation for the sake of smoke emitter which uses this parameter to determine smoke opacity + dizel_fill = clamp( 0.2 + 0.35 * ( tmp - enrot ), 0.0, 1.0 ); } else { tmp = 0.0; + dizel_fill = 0.0; } if( enrot != tmp ) { diff --git a/Model3d.cpp b/Model3d.cpp index 52a24399..e03111b1 100644 --- a/Model3d.cpp +++ b/Model3d.cpp @@ -703,7 +703,8 @@ void TSubModel::InitialRotate(bool doit) } else if (Global.iConvertModels & 2) { // optymalizacja jest opcjonalna - if ((iFlags & 0xC000) == 0x8000) // o ile nie ma animacji + if ( ((iFlags & 0xC000) == 0x8000) // o ile nie ma animacji + && ( false == is_emitter() ) ) // don't optimize smoke emitter attachment points { // jak nie ma potomnych, można wymnożyć przez transform i wyjedynkować go float4x4 *mat = GetMatrix(); // transform submodelu if( false == Vertices.empty() ) { @@ -810,6 +811,26 @@ TSubModel::find_replacable4() { return std::make_tuple( nullptr, false ); } +// locates particle emitter submodels and adds them to provided list +void +TSubModel::find_smoke_sources( nameoffset_sequence &Sourcelist ) const { + + auto const name { ToLower( pName ) }; + + if( ( eType == TP_ROTATOR ) + && ( pName.find( "smokesource_" ) == 0 ) ) { + Sourcelist.emplace_back( pName, offset() ); + } + + if( Next != nullptr ) { + Next->find_smoke_sources( Sourcelist ); + } + + if( Child != nullptr ) { + Child->find_smoke_sources( Sourcelist ); + } +} + int TSubModel::FlagsCheck() { // analiza koniecznych zmian pomiędzy submodelami // samo pomijanie glBindTexture() nie poprawi wydajności @@ -1047,7 +1068,7 @@ void TSubModel::RaAnimation(TAnimType a) } }; - //--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- void TSubModel::serialize_geometry( std::ostream &Output ) const { @@ -1132,6 +1153,14 @@ void TSubModel::ColorsSet( glm::vec3 const &Ambient, glm::vec3 const &Diffuse, g */ }; +bool +TSubModel::is_emitter() const { + + return ( + ( eType == TP_ROTATOR ) + && ( ToLower( pName ).find( "smokesource_" ) == 0 ) ); +} + // pobranie transformacji względem wstawienia modelu void TSubModel::ParentMatrix( float4x4 *m ) const { @@ -1230,8 +1259,9 @@ TModel3d::~TModel3d() { } }; -TSubModel *TModel3d::AddToNamed(const char *Name, TSubModel *SubModel) -{ +TSubModel * +TModel3d::AddToNamed(const char *Name, TSubModel *SubModel) { + TSubModel *sm = Name ? GetFromName(Name) : nullptr; if( ( sm == nullptr ) && ( Name != nullptr ) && ( std::strcmp( Name, "none" ) != 0 ) ) { @@ -1243,6 +1273,7 @@ TSubModel *TModel3d::AddToNamed(const char *Name, TSubModel *SubModel) // jedyny poprawny sposób dodawania submodeli, inaczej mogą zginąć przy zapisie E3D void TModel3d::AddTo(TSubModel *tmp, TSubModel *SubModel) { + if (tmp) { // jeśli znaleziony, podłączamy mu jako potomny tmp->ChildAdd(SubModel); @@ -1269,6 +1300,18 @@ TSubModel *TModel3d::GetFromName(std::string const &Name) const } }; +// locates particle source submodels and stores them on internal list +nameoffset_sequence const & +TModel3d::find_smoke_sources() { + + m_smokesources.clear(); + if( Root != nullptr ) { + Root->find_smoke_sources( m_smokesources ); + } + + return smoke_sources(); +} + // returns offset vector from root glm::vec3 TSubModel::offset( float const Geometrytestoffsetthreshold ) const { @@ -1888,6 +1931,8 @@ void TModel3d::Init() asBinary = ""; // zablokowanie powtórnego zapisu } } + // check if the model contains particle emitters + find_smoke_sources(); }; //----------------------------------------------------------------------------- diff --git a/Model3d.h b/Model3d.h index 412c1f2b..ca479e19 100644 --- a/Model3d.h +++ b/Model3d.h @@ -51,6 +51,8 @@ namespace scene { class shape_node; } +using nameoffset_sequence = std::vector>; + class TSubModel { // klasa submodelu - pojedyncza siatka, punkt świetlny albo grupa punktów //m7todo: zrobić normalną serializację @@ -149,6 +151,8 @@ public: // chwilowo private: int SeekFaceNormal( std::vector const &Masks, int const Startface, unsigned int const Mask, glm::vec3 const &Position, gfx::vertex_array const &Vertices ); void RaAnimation(TAnimType a); + // returns true if the submodel is a smoke emitter attachment point, false otherwise + bool is_emitter() const; public: static size_t iInstance; // identyfikator egzemplarza, który aktualnie renderuje model @@ -168,6 +172,8 @@ public: int count_children(); // locates submodel mapped with replacable -4 std::tuple find_replacable4(); + // locates particle emitter submodels and adds them to provided list + void find_smoke_sources( nameoffset_sequence &Sourcelist ) const; int TriangleAdd(TModel3d *m, material_handle tex, int tri); void SetRotate(float3 vNewRotateAxis, float fNewAngle); void SetRotateXYZ( Math3D::vector3 vNewAngles); @@ -240,6 +246,7 @@ private: int iSubModelsCount; // Ra: używane do tworzenia binarnych std::string asBinary; // nazwa pod którą zapisać model binarny std::string m_filename; + nameoffset_sequence m_smokesources; // list of particle sources defined in the model public: TModel3d(); @@ -252,6 +259,7 @@ public: inline TSubModel * GetSMRoot() { return (Root); }; TSubModel * GetFromName(std::string const &Name) const; TSubModel * AddToNamed(const char *Name, TSubModel *SubModel); + nameoffset_sequence const & find_smoke_sources(); void AddTo(TSubModel *tmp, TSubModel *SubModel); void LoadFromTextFile(std::string const &FileName, bool dynamic); void LoadFromBinFile(std::string const &FileName, bool dynamic); @@ -260,6 +268,8 @@ public: int Flags() const { return iFlags; }; void Init(); std::string NameGet() const { return m_filename; }; + nameoffset_sequence const & smoke_sources() const { + return m_smokesources; } int TerrainCount() const; TSubModel * TerrainSquare(int n); void deserialize(std::istream &s, size_t size, bool dynamic); diff --git a/drivermode.cpp b/drivermode.cpp index ccbd053b..0f152e61 100644 --- a/drivermode.cpp +++ b/drivermode.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "simulationtime.h" #include "simulationenvironment.h" #include "lightarray.h" +#include "particles.h" #include "Train.h" #include "Driver.h" #include "DynObj.h" @@ -235,6 +236,8 @@ driver_mode::update() { simulation::Region->update_sounds(); audio::renderer.update( deltarealtime ); + // NOTE: particle system runs on simulation time, but needs actual camera position to determine how to update each particle source + simulation::Particles.update(); GfxRenderer.Update( deltarealtime ); simulation::is_ready = true; diff --git a/parser.cpp b/parser.cpp index 25bd9c3a..1ba87a48 100644 --- a/parser.cpp +++ b/parser.cpp @@ -79,6 +79,19 @@ cParser::~cParser() { } } +template <> +glm::vec3 +cParser::getToken( bool const ToLower, char const *Break ) { + // NOTE: this specialization ignores default arguments + getTokens( 3, false, "\n\r\t ,;[]" ); + glm::vec3 output; + *this + >> output.x + >> output.y + >> output.z; + return output; +}; + template<> cParser& cParser::operator>>( std::string &Right ) { diff --git a/parser.h b/parser.h index 9dd96657..f47f349b 100644 --- a/parser.h +++ b/parser.h @@ -37,7 +37,7 @@ class cParser //: public std::stringstream operator>>( Type_ &Right ); template Output_ - getToken( bool const ToLower = true, const char *Break = "\n\r\t ;" ) { + getToken( bool const ToLower = true, char const *Break = "\n\r\t ;" ) { getTokens( 1, ToLower, Break ); Output_ output; *this >> output; @@ -108,6 +108,12 @@ class cParser //: public std::stringstream std::deque tokens; }; + +template <> +glm::vec3 +cParser::getToken( bool const ToLower, const char *Break ); + + template cParser& cParser::operator>>( Type_ &Right ) { diff --git a/particles.cpp b/particles.cpp new file mode 100644 index 00000000..9f9dbc94 --- /dev/null +++ b/particles.cpp @@ -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() != "{" ) { return; } + + std::unordered_map 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( true, "\n\r\t ,;[]" ) ).empty() ) ) + && ( key != "}" ) ) { + + auto const lookup { variablemap.find( key ) }; + if( lookup == variablemap.end() ) { continue; } + + lookup->second = Input.getToken( 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( 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( 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( 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::max() }, + glm::dvec3{ std::numeric_limits::lowest() } }; + + m_spawncount = ( + Onlydespawn ? + 0.f : + std::min( + 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( 0.25 * ( 1.f - Global.Overcast ), Particle.velocity.y - 0.25 * Global.Overcast * Timedelta ); + + Particle.position += Particle.velocity * static_cast( Timedelta ); + Particle.position += 0.35f * simulation::Environment.wind() * static_cast( 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; +} diff --git a/particles.h b/particles.h new file mode 100644 index 00000000..4e03bf63 --- /dev/null +++ b/particles.h @@ -0,0 +1,229 @@ +/* +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/. +*/ + +#pragma once + +#include "Classes.h" +#include "scene.h" + +// particle specialized for drawing smoke +// given smoke features we can take certain shortcuts +// -- there's no need to sort the particles, can be drawn in any order with depth write turned off +// -- the colour remains consistent throughout, only opacity changes +// -- randomized particle rotation +// -- initial velocity reduced over time to slow drift upwards (drift speed depends on particle and air temperature difference) +// -- size increased over time +struct smoke_particle { + + glm::dvec3 position; // meters, 3d space; + float rotation; // radians; local z axis angle + glm::vec3 velocity; // meters per second, 3d space; current velocity + float size; // multiplier, billboard size +// glm::vec4 color; // 0-1 range, rgba; geometry color and opacity + float opacity; // 0-1 range +// glm::vec2 uv_offset; // 0-1 range, uv space; for texture animation + float age; // seconds; time elapsed since creation +// double distance; // meters; distance between particle and camera +}; + +enum value_limit { + min = 0, + max = 1 +}; + +// helper, adjusts provided variable by fixed amount, keeping resulting value between limits +template +class fixedstep_modifier { + +public: +// methods + void + deserialize( cParser &Input ); + // updates state of provided variable + void + update( Type_ &Variable, double const Timedelta ) const; + Type_ const & + value_change() const { + return m_valuechange; } + +private: +//types +// methods +// members +// Type_ m_intialvalue { Type_( 0 ) }; // meters per second; velocity applied to freshly spawned particles + Type_ m_valuechange { Type_( 0 ) }; // meters per second; change applied to initial velocity + Type_ m_valuelimits[ 2 ] { Type_( std::numeric_limits::lowest() ), Type_( std::numeric_limits::max() ) }; +}; + + +// particle emitter +class smoke_source { +// located in scenery +// new particles emitted if distance of source < double view range +// existing particles are updated until dead no matter the range (presumed to have certain lifespan) +// during update pass dead particle slots are filled with new instances, if there's no particles queued the slot is swapped with the last particle in the list +// bounding box/sphere calculated based on position of all owned particles, used by the renderer to include/discard data in a draw pass + friend class particle_manager; + +public: +// types + using particle_sequence = std::vector; +// methods + bool + deserialize( cParser &Input ); + void + initialize(); + void + bind( TDynamicObject const *Vehicle ); + // updates state of owned particles + void + update( double const Timedelta, bool const Onlydespawn ); + glm::dvec3 + location() const; + // provides access to bounding area data + scene::bounding_area const & + area() const { + return m_area; } + particle_sequence const & + sequence() const { + return m_particles; } + +private: +// types + enum class owner_type { + none = 0, + vehicle, + node + }; + + struct particle_emitter { + float inclination[ 2 ] { 0.f, 0.f }; + float velocity[ 2 ] { 1.f, 1.f }; + float size[ 2 ] { 1.f, 1.f }; + float opacity[ 2 ] { 1.f, 1.f }; + + void deserialize( cParser &Input ); + void initialize( smoke_particle &Particle ); + }; + + using bounding_box = glm::dvec3[ 2 ]; // bounding box of owned particles + +// methods + // imports member data pair from the config file + bool + deserialize_mapping( cParser &Input ); + void + initialize( smoke_particle &Particle ); + // updates state of provided particle and bounding box. returns: true if particle is still alive afterwards, false otherwise + bool + update( smoke_particle &Particle, bounding_box &Boundingbox, double const Timedelta ); +// members + // config/inputs + // TBD: union and indicator, or just plain owner variables? + owner_type m_ownertype { owner_type::none }; + union { + TDynamicObject const * vehicle; + TAnimModel const * node; + } m_owner { nullptr }; // optional, scene item carrying this source + glm::dvec3 m_offset; // meters, 3d space; relative position of the source, either from the owner or the region centre + float m_spawnrate { 0.f }; // number of particles to spawn per second + particle_emitter m_emitter; +// bool m_inheritvelocity { false }; // whether spawned particle should receive velocity of its owner + // TODO: replace modifiers with configurable interpolator item allowing keyframe-based changes over time +// fixedstep_modifier m_velocitymodifier; // particle velocity + fixedstep_modifier m_sizemodifier; // particle billboard size +// fixedstep_modifier m_colormodifier; // particle billboard color and opacity + fixedstep_modifier m_opacitymodifier; +// texture_handle m_texture { -1 }; // texture assigned to particle billboards + // current state + float m_spawncount { 0.f }; // number of particles to spawn during next update + particle_sequence m_particles; // collection of spawned particles +/* + smoke_sequence::iterator // helpers, iterators marking currently used part of the particle container + m_particlehead, + m_particletail; +*/ + scene::bounding_area m_area; // bounding sphere of owned particles +}; + + +// holds all particle emitters defined in the scene and updates their state +class particle_manager { + + friend opengl_renderer; + +public: +// types + using source_sequence = std::vector; +// constructors + particle_manager() = default; +// destructor +// ~particle_manager(); +// methods + // 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 + insert( std::string const &Sourcetemplate, glm::dvec3 const Location ); + bool + insert( std::string const &Sourcetemplate, TDynamicObject const *Vehicle, glm::dvec3 const Location ); + // updates state of all owned emitters + void + update(); + // data access + source_sequence & + sequence() { + return m_sources; } + +// members + +private: +// types + using source_map = std::unordered_map; +// methods + smoke_source * + find( std::string const &Template ); +// members + source_map m_sourcetemplates; // cached particle emitter configurations + source_sequence m_sources; // all owned particle emitters +}; + + + +template +void +fixedstep_modifier::update( Type_ &Variable, double const Timedelta ) const { + // HACK: float cast to avoid vec3 and double mismatch + // TBD, TODO: replace with vector types specialization + Variable += ( m_valuechange * static_cast( Timedelta ) ); + // clamp down to allowed value range + Variable = glm::max( Variable, m_valuelimits[ value_limit::min ] ); + Variable = glm::min( Variable, m_valuelimits[ value_limit::max ] ); +} + +template +void +fixedstep_modifier::deserialize( cParser &Input ) { + + if( Input.getToken() != "{" ) { return; } + + std::unordered_map const variablemap { + { "step:", m_valuechange }, + { "min:", m_valuelimits[ value_limit::min ] }, + { "max:", m_valuelimits[ value_limit::max ] } }; + + std::string key; + + while( ( false == ( ( key = Input.getToken( true, "\n\r\t ,;[]" ) ).empty() ) ) + && ( key != "}" ) ) { + + auto const lookup { variablemap.find( key ) }; + if( lookup == variablemap.end() ) { continue; } + + lookup->second = Input.getToken( true, "\n\r\t ,;[]" ); + } +} diff --git a/renderer.cpp b/renderer.cpp index c8cd7c10..dd0f53c6 100644 --- a/renderer.cpp +++ b/renderer.cpp @@ -106,6 +106,134 @@ opengl_camera::draw( glm::vec3 const &Offset ) const { ::glEnd(); } + + +std::vector> const billboard_vertices { + + { { -0.5f, -0.5f, 0.f }, { 0.f, 0.f } }, + { { 0.5f, -0.5f, 0.f }, { 1.f, 0.f } }, + { { 0.5f, 0.5f, 0.f }, { 1.f, 1.f } }, + { { -0.5f, 0.5f, 0.f }, { 0.f, 1.f } } +}; + +void +opengl_particles::update( opengl_camera const &Camera ) { + + m_particlevertices.clear(); + // build a list of visible smoke sources + // NOTE: arranged by distance to camera, if we ever need sorting and/or total amount cap-based culling + std::multimap sources; + + for( auto const &source : simulation::Particles.sequence() ) { + if( false == Camera.visible( source.area() ) ) { continue; } + // NOTE: the distance is negative when the camera is inside the source's bounding area + sources.emplace( + static_cast( glm::length( Camera.position() - source.area().center ) - source.area().radius ), + source ); + } + + if( true == sources.empty() ) { return; } + + // build billboard data for particles from visible sources + auto const camerarotation { glm::mat3( Camera.modelview() ) }; + particle_vertex vertex; + for( auto const &source : sources ) { + + auto const &particles { source.second.sequence() }; + // TODO: put sanity cap on the overall amount of particles that can be drawn + auto const sizestep { 256.0 * billboard_vertices.size() }; + m_particlevertices.reserve( + sizestep * std::ceil( m_particlevertices.size() + ( particles.size() * billboard_vertices.size() ) / sizestep ) ); + for( auto const &particle : particles ) { + // TODO: particle color support + vertex.color[ 0 ] = + vertex.color[ 1 ] = + vertex.color[ 2 ] = static_cast( Global.fLuminance * 32 ); + vertex.color[ 3 ] = clamp( particle.opacity * 255, 0, 255 ); + + auto const offset { glm::vec3{ particle.position - Camera.position() } }; + auto const rotation { glm::angleAxis( particle.rotation, glm::vec3{ 0.f, 0.f, 1.f } ) }; + + for( auto const &billboardvertex : billboard_vertices ) { + vertex.position = offset + ( rotation * billboardvertex.first * particle.size ) * camerarotation; + vertex.texture = billboardvertex.second; + + m_particlevertices.emplace_back( vertex ); + } + } + } + + // ship the billboard data to the gpu: + // setup... + ::glPushClientAttrib( GL_CLIENT_VERTEX_ARRAY_BIT ); + // ...make sure we have enough room... + if( m_buffercapacity < m_particlevertices.size() ) { + // allocate gpu side buffer big enough to hold the data + m_buffercapacity = 0; + if( m_buffer != -1 ) { + // get rid of the old buffer + ::glDeleteBuffers( 1, &m_buffer ); + } + ::glGenBuffers( 1, &m_buffer ); + ::glBindBuffer( GL_ARRAY_BUFFER, m_buffer ); + if( m_buffer > 0 ) { + // if we didn't get a buffer we'll try again during the next draw call + // NOTE: we match capacity instead of current size to reduce number of re-allocations + auto const particlecount { m_particlevertices.capacity() }; + ::glBufferData( + GL_ARRAY_BUFFER, + particlecount * sizeof( particle_vertex ), + nullptr, + GL_DYNAMIC_DRAW ); + if( ::glGetError() == GL_OUT_OF_MEMORY ) { + // TBD: throw a bad_alloc? + ErrorLog( "openGL error: out of memory; failed to create a geometry buffer" ); + ::glDeleteBuffers( 1, &m_buffer ); + m_buffer = -1; + } + else { + m_buffercapacity = particlecount; + } + } + } + // ...send the data... + if( m_buffer > 0 ) { + // if the buffer exists at this point it's guaranteed to be big enough to hold our data + ::glBindBuffer( GL_ARRAY_BUFFER, m_buffer ); + ::glBufferSubData( + GL_ARRAY_BUFFER, + 0, + m_particlevertices.size() * sizeof( particle_vertex ), + m_particlevertices.data() ); + } + // ...and cleanup + ::glPopClientAttrib(); +} + +void +opengl_particles::render( int const Textureunit ) { + + if( m_buffercapacity == 0 ) { return; } + if( m_particlevertices.empty() ) { return; } + + // setup... + ::glPushClientAttrib( GL_CLIENT_VERTEX_ARRAY_BIT ); + ::glBindBuffer( GL_ARRAY_BUFFER, m_buffer ); + ::glVertexPointer( 3, GL_FLOAT, sizeof( particle_vertex ), static_cast( nullptr ) ); + ::glEnableClientState( GL_VERTEX_ARRAY ); + ::glColorPointer( 4, GL_UNSIGNED_BYTE, sizeof( particle_vertex ), static_cast( nullptr ) + sizeof( float ) * 3 ); + ::glEnableClientState( GL_COLOR_ARRAY ); + ::glClientActiveTexture( Textureunit ); + ::glTexCoordPointer( 2, GL_FLOAT, sizeof( particle_vertex ), static_cast( nullptr ) + sizeof( float ) * 3 + sizeof( std::uint8_t ) * 4 ); + ::glEnableClientState( GL_TEXTURE_COORD_ARRAY ); + // ...draw... + ::glDrawArrays( GL_QUADS, 0, m_particlevertices.size() ); + // ...and cleanup + ::glPopClientAttrib(); +} + + + bool opengl_renderer::Init( GLFWwindow *Window ) { @@ -194,6 +322,7 @@ opengl_renderer::Init( GLFWwindow *Window ) { if( m_helpertextureunit >= 0 ) { m_reflectiontexture = Fetch_Texture( "fx/reflections" ); } + m_smoketexture = Fetch_Texture( "fx/smoke" ); WriteLog( "...gfx data pre-loading done" ); #ifdef EU07_USE_PICKING_FRAMEBUFFER @@ -576,6 +705,8 @@ opengl_renderer::Render_pass( rendermode const Mode ) { // ...translucent parts setup_drawing( true ); Render_Alpha( simulation::Region ); + // particles + Render_particles(); // precipitation; done at the end, only before cab render Render_precipitation(); // cab render @@ -1489,7 +1620,7 @@ opengl_renderer::Render( world_environment *Environment ) { auto const &modelview = OpenGLMatrices.data( GL_MODELVIEW ); - auto const fogfactor { clamp( Global.fFogEnd / 2000.f, 0.f, 1.f ) }; // stronger fog reduces opacity of the celestial bodies + auto const fogfactor { clamp( Global.fFogEnd / 2000.f, 0.f, 1.f ) }; // closer/denser fog reduces opacity of the celestial bodies float const duskfactor = 1.0f - clamp( std::abs( Environment->m_sun.getAngle() ), 0.0f, 12.0f ) / 12.0f; glm::vec3 suncolor = interpolate( glm::vec3( 255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f ), @@ -3008,6 +3139,25 @@ opengl_renderer::Render( TMemCell *Memcell ) { ::glPopMatrix(); } +void +opengl_renderer::Render_particles() { + + switch_units( true, false, false ); + + Bind_Material( null_handle ); // TODO: bind smoke texture + + // TBD: leave lighting on to allow vehicle lights to affect it? + ::glDisable( GL_LIGHTING ); + // momentarily disable depth write, to allow vehicle cab drawn afterwards to mask it instead of leaving it 'inside' + ::glDepthMask( GL_FALSE ); + + Bind_Texture( m_smoketexture ); + m_particlerenderer.render( m_diffusetextureunit ); + + ::glDepthMask( GL_TRUE ); + ::glEnable( GL_LIGHTING ); +} + void opengl_renderer::Render_precipitation() { @@ -3023,8 +3173,11 @@ opengl_renderer::Render_precipitation() { colors::white, 0.5f * clamp( Global.fLuminance, 0.f, 1.f ) ) ) ); ::glPushMatrix(); - // tilt the precipitation cone against the velocity vector for crude motion blur - auto const velocity { simulation::Environment.m_precipitation.m_cameramove * -1.0 }; + // tilt the precipitation cone against the camera movement vector for crude motion blur + // include current wind vector while at it + auto const velocity { + simulation::Environment.m_precipitation.m_cameramove * -1.0 + + glm::dvec3{ simulation::Environment.wind() } * 0.5 }; if( glm::length2( velocity ) > 0.0 ) { auto const forward{ glm::normalize( velocity ) }; auto left { glm::cross( forward, {0.0,1.0,0.0} ) }; @@ -3801,6 +3954,16 @@ opengl_renderer::Update_Mouse_Position() { void opengl_renderer::Update( double const Deltatime ) { + + // per frame updates + if( simulation::is_ready ) { + // update particle subsystem + renderpass_config renderpass; + setup_pass( renderpass, rendermode::color ); + m_particlerenderer.update( renderpass.camera ); + } + + // fixed step updates /* m_pickupdateaccumulator += Deltatime; diff --git a/renderer.h b/renderer.h index f1388c71..ec0bb785 100644 --- a/renderer.h +++ b/renderer.h @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "frustum.h" #include "scene.h" #include "simulationenvironment.h" +#include "particles.h" #include "MemCell.h" #define EU07_USE_PICKING_FRAMEBUFFER @@ -43,6 +44,8 @@ struct opengl_light : public basic_light { return *this; } }; + + // encapsulates basic rendering setup. // for modern opengl this translates to a specific collection of glsl shaders, // for legacy opengl this is combination of blending modes, active texture units etc @@ -50,6 +53,8 @@ struct opengl_technique { }; + + // simple camera object. paired with 'virtual camera' in the scene class opengl_camera { @@ -112,6 +117,46 @@ private: glm::mat4 m_inversetransformation; // cached transformation to world space }; + +// particle data visualizer +class opengl_particles { +public: +// constructors + opengl_particles() = default; +// destructor + ~opengl_particles() { + if( m_buffer != 0 ) { + ::glDeleteBuffers( 1, &m_buffer ); } } +// methods + void + update( opengl_camera const &Camera ); + void + render( int const Textureunit ); +private: +// types + struct particle_vertex { + glm::vec3 position; // 3d space + std::uint8_t color[ 4 ]; // rgba, unsigned byte format + glm::vec2 texture; // uv space + float padding[ 2 ]; // experimental, some gfx hardware allegedly works better with 32-bit aligned data blocks + }; +/* + using sourcedistance_pair = std::pair; + using source_sequence = std::vector; +*/ + using particlevertex_sequence = std::vector; +// methods +// members +/* + source_sequence m_sources; // list of particle sources visible in current render pass, with their respective distances to the camera +*/ + particlevertex_sequence m_particlevertices; // geometry data of visible particles, generated on the cpu end + GLuint m_buffer{ (GLuint)-1 }; // id of the buffer holding geometry data on the opengl end + std::size_t m_buffercapacity{ 0 }; // total capacity of the last established buffer +}; + + + // bare-bones render controller, in lack of anything better yet class opengl_renderer { @@ -296,6 +341,8 @@ private: Render_cab( TDynamicObject const *Dynamic, float const Lightlevel, bool const Alpha = false ); void Render( TMemCell *Memcell ); + void + Render_particles(); void Render_precipitation(); void @@ -338,6 +385,7 @@ private: texture_handle m_suntexture { -1 }; texture_handle m_moontexture { -1 }; texture_handle m_reflectiontexture { -1 }; + texture_handle m_smoketexture { -1 }; GLUquadricObj *m_quadric { nullptr }; // helper object for drawing debug mode scene elements // TODO: refactor framebuffer stuff into an object bool m_framebuffersupport { false }; @@ -369,6 +417,8 @@ private: int m_environmentcubetextureface { 0 }; // helper, currently processed cube map face int m_environmentupdatetime { 0 }; // time of the most recent environment map update glm::dvec3 m_environmentupdatelocation; // coordinates of most recent environment map update + // particle visualization subsystem + opengl_particles m_particlerenderer; int m_helpertextureunit { GL_TEXTURE0 }; int m_shadowtextureunit { GL_TEXTURE1 }; diff --git a/simulation.cpp b/simulation.cpp index 262cf5f9..8faca902 100644 --- a/simulation.cpp +++ b/simulation.cpp @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "AnimModel.h" #include "DynObj.h" #include "lightarray.h" +#include "particles.h" #include "scene.h" #include "Train.h" @@ -36,6 +37,7 @@ sound_table Sounds; instance_table Instances; vehicle_table Vehicles; light_array Lights; +particle_manager Particles; scene::basic_region *Region { nullptr }; TTrain *Train { nullptr }; diff --git a/simulation.h b/simulation.h index 4a37c683..b2e55dd1 100644 --- a/simulation.h +++ b/simulation.h @@ -48,6 +48,7 @@ extern sound_table Sounds; extern instance_table Instances; extern vehicle_table Vehicles; extern light_array Lights; +extern particle_manager Particles; extern scene::basic_region *Region; extern TTrain *Train; diff --git a/simulationenvironment.cpp b/simulationenvironment.cpp index f105faa2..48e45ba4 100644 --- a/simulationenvironment.cpp +++ b/simulationenvironment.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "simulationenvironment.h" #include "Globals.h" +#include "Timer.h" namespace simulation { @@ -77,8 +78,14 @@ world_environment::init() { m_stars.init(); m_clouds.Init(); m_precipitation.init(); - m_precipitationsound.deserialize( "rain-sound-loop", sound_type::single ); + m_wind = basic_wind{ + static_cast( Random( 0, 360 ) ), + static_cast( Random( -5, 5 ) ), + static_cast( Random( -2, 4 ) ), + static_cast( Random( -1, 1 ) ), + static_cast( Random( 5, 20 ) ), + {} }; } void @@ -170,6 +177,8 @@ world_environment::update() { // reduce friction due to snow Global.FrictionWeatherFactor = 0.75f; } + + update_wind(); } void @@ -178,6 +187,33 @@ world_environment::update_precipitation() { m_precipitation.update(); } +void +world_environment::update_wind() { + + auto const timedelta{ static_cast( Timer::GetDeltaTime() ) }; + + m_wind.change_time -= timedelta; + if( m_wind.change_time < 0 ) { + m_wind.change_time = Random( 5, 30 ); + m_wind.azimuth_change = Random( -5, 5 ); + m_wind.velocity_change = Random( -1, 1 ); + } + // TBD, TODO: wind configuration + m_wind.azimuth = clamp_circular( m_wind.azimuth + m_wind.azimuth_change * timedelta ); + // HACK: negative part of range allows for some quiet periods, without active wind + m_wind.velocity = clamp( m_wind.velocity + m_wind.velocity_change * timedelta, -2, 4 ); + // convert to force vector + auto const polarangle { glm::radians( 90.f ) }; // theta + auto const azimuthalangle{ glm::radians( m_wind.azimuth ) }; // phi + // convert spherical coordinates to opengl coordinates + m_wind.vector = + std::max( 0.f, m_wind.velocity ) + * glm::vec3( + std::sin( polarangle ) * std::sin( azimuthalangle ), + std::cos( polarangle ), + std::sin( polarangle ) * std::cos( azimuthalangle ) * -1 ); +} + void world_environment::time( int const Hour, int const Minute, int const Second ) { diff --git a/simulationenvironment.h b/simulationenvironment.h index 2f69f631..09834c99 100644 --- a/simulationenvironment.h +++ b/simulationenvironment.h @@ -36,8 +36,25 @@ public: void compute_season( int const Yearday ) const; // calculates current weather void compute_weather() const; + // data access + glm::vec3 const & + wind() const { + return m_wind.vector; } private: +// types + struct basic_wind { + // internal state data + float azimuth; + float azimuth_change; + float velocity; + float velocity_change; + float change_time; + // output + glm::vec3 vector; + }; +// methods + void update_wind(); // members CSkyDome m_skydome; cStars m_stars; @@ -45,8 +62,8 @@ private: cMoon m_moon; TSky m_clouds; basic_precipitation m_precipitation; - sound_source m_precipitationsound { sound_placement::external, -1 }; + basic_wind m_wind; }; namespace simulation { diff --git a/simulationstateserializer.cpp b/simulationstateserializer.cpp index a4d2790a..ee4424ed 100644 --- a/simulationstateserializer.cpp +++ b/simulationstateserializer.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation.h" #include "simulationtime.h" #include "scenenodegroups.h" +#include "particles.h" #include "Event.h" #include "Driver.h" #include "DynObj.h" @@ -55,6 +56,7 @@ state_serializer::deserialize( std::string const &Scenariofile ) { // as long as the scenario file wasn't rainsted-created base file override Region->serialize( Scenariofile ); } + return true; } @@ -354,6 +356,16 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch // vehicle import can potentially fail if( vehicle == nullptr ) { return; } + // + if( vehicle->mdModel != nullptr ) { + for( auto const &smokesource : vehicle->mdModel->smoke_sources() ) { + Particles.insert( + smokesource.first, + vehicle, + smokesource.second ); + } + } + if( false == simulation::Vehicles.insert( vehicle ) ) { ErrorLog( "Bad scenario: duplicate vehicle name \"" + vehicle->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); From d317dc2296acee1fe884be27aa21a94b4aba1c71 Mon Sep 17 00:00:00 2001 From: tmj-fstate Date: Tue, 6 Aug 2019 14:08:22 +0200 Subject: [PATCH 4/5] cab change logic tweak, event launcher execution fix --- Train.cpp | 38 ++++++++++++++++++++++++++++++-------- scene.cpp | 3 +++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/Train.cpp b/Train.cpp index 08dbf68b..ba49a5bf 100644 --- a/Train.cpp +++ b/Train.cpp @@ -5049,12 +5049,23 @@ void TTrain::OnCommand_radiocall3send( TTrain *Train, command_data const &Comman void TTrain::OnCommand_cabchangeforward( TTrain *Train, command_data const &Command ) { if( Command.action == GLFW_PRESS ) { - if( false == Train->CabChange( 1 ) ) { - if( TestFlag( Train->DynamicObject->MoverParameters->Couplers[ end::front ].CouplingFlag, coupling::gangway ) ) { + auto const movedirection { + 1 * ( Train->DynamicObject->ctOwner->Vehicle( end::front )->DirectionGet() == Train->DynamicObject->DirectionGet() ? + 1 : + -1 ) }; + if( false == Train->CabChange( movedirection ) ) { + auto const exitdirection { ( + movedirection > 0 ? + end::front : + end::rear ) }; + if( TestFlag( Train->DynamicObject->MoverParameters->Couplers[ exitdirection ].CouplingFlag, coupling::gangway ) ) { // przejscie do nastepnego pojazdu - Global.changeDynObj = Train->DynamicObject->PrevConnected(); + Global.changeDynObj = ( + exitdirection == end::front ? + Train->DynamicObject->PrevConnected() : + Train->DynamicObject->NextConnected() ); Global.changeDynObj->MoverParameters->ActiveCab = ( - Train->DynamicObject->MoverParameters->Neighbours[end::front].vehicle_end ? + Train->DynamicObject->MoverParameters->Neighbours[ exitdirection ].vehicle_end ? -1 : 1 ); } @@ -5069,12 +5080,23 @@ void TTrain::OnCommand_cabchangeforward( TTrain *Train, command_data const &Comm void TTrain::OnCommand_cabchangebackward( TTrain *Train, command_data const &Command ) { if( Command.action == GLFW_PRESS ) { - if( false == Train->CabChange( -1 ) ) { - if( TestFlag( Train->DynamicObject->MoverParameters->Couplers[ end::rear ].CouplingFlag, coupling::gangway ) ) { + auto const movedirection { + -1 * ( Train->DynamicObject->ctOwner->Vehicle( end::front )->DirectionGet() == Train->DynamicObject->DirectionGet() ? + 1 : + -1 ) }; + if( false == Train->CabChange( movedirection ) ) { + auto const exitdirection { ( + movedirection > 0 ? + end::front : + end::rear ) }; + if( TestFlag( Train->DynamicObject->MoverParameters->Couplers[ exitdirection ].CouplingFlag, coupling::gangway ) ) { // przejscie do nastepnego pojazdu - Global.changeDynObj = Train->DynamicObject->NextConnected(); + Global.changeDynObj = ( + exitdirection == end::front ? + Train->DynamicObject->PrevConnected() : + Train->DynamicObject->NextConnected() ); Global.changeDynObj->MoverParameters->ActiveCab = ( - Train->DynamicObject->MoverParameters->Neighbours[end::rear].vehicle_end ? + Train->DynamicObject->MoverParameters->Neighbours[ exitdirection ].vehicle_end ? -1 : 1 ); } diff --git a/scene.cpp b/scene.cpp index f09ca8dd..cc4b241e 100644 --- a/scene.cpp +++ b/scene.cpp @@ -950,6 +950,9 @@ basic_region::on_click( TAnimModel const *Instance ) { // legacy method, polls event launchers around camera void basic_region::update_events() { + + if( false == simulation::is_ready ) { return; } + // render events and sounds from sectors near enough to the viewer auto const range = EU07_SECTIONSIZE; // arbitrary range auto const §ionlist = sections( Global.pCamera.Pos, range ); From 69130d54a0e9b6a3dd6798ee25cda6ebcf08289d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=B3lik=20Uszasty?= Date: Sun, 21 Jul 2019 20:36:02 +0200 Subject: [PATCH 5/5] Additional Zeros don't work for MHZ_EN57 --- McZapkie/Mover.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 3794c78a..3ae9012a 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -6163,7 +6163,7 @@ void TMoverParameters::CheckEIMIC(double dt) { if (eimic > 0.001) eimic = std::max(0.002, eimic * (double)MainCtrlPosNo / ((double)MainCtrlPosNo - 1.0) - 1.0 / ((double)MainCtrlPosNo - 1.0)); - if (eimic < -0.001) + if ((eimic < -0.001) && (BrakeHandle != TBrakeHandle::MHZ_EN57)) eimic = std::min(-0.002, eimic * (double)LocalBrakePosNo / ((double)LocalBrakePosNo - 1.0) + 1.0 / ((double)LocalBrakePosNo - 1.0)); } break;