diff --git a/AnimModel.cpp b/AnimModel.cpp index 5c23c0a0..63af27e5 100644 --- a/AnimModel.cpp +++ b/AnimModel.cpp @@ -276,7 +276,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 ] ).get_or_guess_opacity() == 0.0f ) ) { diff --git a/CMakeLists.txt b/CMakeLists.txt index d3636652..4176e73f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,6 +124,7 @@ set(SOURCES "precipitation.cpp" "pythonscreenviewer.cpp" "dictionary.cpp" +"particles.cpp" "network/network.cpp" "network/message.cpp" diff --git a/Classes.h b/Classes.h index e5f98ad4..65bf942c 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/Driver.cpp b/Driver.cpp index 3f510769..a05cd068 100644 --- a/Driver.cpp +++ b/Driver.cpp @@ -3434,30 +3434,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 20751efd..85cb7588 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( @@ -2381,8 +2383,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 ? @@ -2410,30 +2413,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; @@ -4561,9 +4563,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 ) { @@ -6168,7 +6173,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; @@ -10321,7 +10326,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 ); diff --git a/Model3d.cpp b/Model3d.cpp index 318ec1de..9baca1ef 100644 --- a/Model3d.cpp +++ b/Model3d.cpp @@ -740,7 +740,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() ) { @@ -847,6 +848,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 ); + } +} + uint32_t TSubModel::FlagsCheck() { // analiza koniecznych zmian pomiędzy submodelami // samo pomijanie glBindTexture() nie poprawi wydajności @@ -1086,7 +1107,7 @@ void TSubModel::RaAnimation(glm::mat4 &m, TAnimType a) } }; - //--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- void TSubModel::serialize_geometry( std::ostream &Output ) const { @@ -1171,6 +1192,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 { @@ -1269,8 +1298,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 ) ) { @@ -1282,6 +1312,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); @@ -1308,6 +1339,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 { @@ -1954,6 +1997,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 f13e998b..9322277e 100644 --- a/Model3d.h +++ b/Model3d.h @@ -48,6 +48,7 @@ class shape_node; } class TModel3d; +using nameoffset_sequence = std::vector>; class TSubModel { // klasa submodelu - pojedyncza siatka, punkt świetlny albo grupa punktów @@ -148,6 +149,8 @@ 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); void RaAnimation(glm::mat4 &m, 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 @@ -167,6 +170,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); @@ -242,6 +247,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(); @@ -254,6 +260,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); @@ -262,6 +269,8 @@ public: uint32_t 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/Train.cpp b/Train.cpp index a274598e..e3c48a38 100644 --- a/Train.cpp +++ b/Train.cpp @@ -5052,12 +5052,22 @@ 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 - TDynamicObject *dynobj = Train->DynamicObject->PrevConnected(); + TDynamicObject *dynobj = ( exitdirection == end::front ? + Train->DynamicObject->PrevConnected() : + Train->DynamicObject->NextConnected() ); dynobj->MoverParameters->ActiveCab = ( - Train->DynamicObject->MoverParameters->Neighbours[end::front].vehicle_end ? + Train->DynamicObject->MoverParameters->Neighbours[exitdirection].vehicle_end ? -1 : 1 ); Train->MoveToVehicle(dynobj); @@ -5073,12 +5083,22 @@ 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 - TDynamicObject *dynobj = Train->DynamicObject->NextConnected(); + TDynamicObject *dynobj = ( exitdirection == end::front ? + Train->DynamicObject->PrevConnected() : + Train->DynamicObject->NextConnected() ); dynobj->MoverParameters->ActiveCab = ( - Train->DynamicObject->MoverParameters->Neighbours[end::rear].vehicle_end ? + Train->DynamicObject->MoverParameters->Neighbours[exitdirection].vehicle_end ? -1 : 1 ); Train->MoveToVehicle(dynobj); @@ -8532,6 +8552,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/drivermode.cpp b/drivermode.cpp index ed15e5cc..d17decba 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" @@ -256,6 +257,8 @@ driver_mode::update() { simulation::Region->update_sounds(); audio::renderer.update( Global.iPause ? 0.0 : 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/driveruipanels.cpp b/driveruipanels.cpp index 5befece4..e4a5d610 100644 --- a/driveruipanels.cpp +++ b/driveruipanels.cpp @@ -750,16 +750,13 @@ debug_panel::update_vehicle_brake() const { void debug_panel::update_section_engine( std::vector &Output ) { - if( m_input.train == nullptr ) { return; } 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 ); @@ -771,7 +768,10 @@ 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 ); + 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:"; diff --git a/parser.cpp b/parser.cpp index df050158..c4e64547 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..d3fc089d --- /dev/null +++ b/particles.cpp @@ -0,0 +1,433 @@ +/* +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( + 2000, + // 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; + cParser parser( templatepath + templatename + ".txt", cParser::buffer_FILE ); + if( source.deserialize( parser ) ) { + // 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 00953bee..75db899f 100644 --- a/renderer.cpp +++ b/renderer.cpp @@ -66,6 +66,117 @@ void opengl_camera::draw(glm::vec3 const &Offset) const // m7t port to core gl } +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, 0.f } }, + { { -0.5f, 0.5f, 0.f }, { 0.f, 1.f } }, + { { 0.5f, 0.5f, 0.f }, { 1.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.r = + vertex.color.g = + vertex.color.b = Global.fLuminance * 0.125f; + vertex.color.a = std::clamp(particle.opacity, 0.0f, 1.0f); + + 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: + // make sure we have enough room... + if( m_buffercapacity < m_particlevertices.size() ) { + m_buffercapacity = m_particlevertices.size(); + if (!m_buffer) + m_buffer.emplace(); + + m_buffer->allocate(gl::buffer::ARRAY_BUFFER, + m_buffercapacity * sizeof(particle_vertex), GL_STREAM_DRAW); + } + + if (m_buffer) { + // ...send the data... + m_buffer->upload(gl::buffer::ARRAY_BUFFER, + m_particlevertices.data(), 0, m_particlevertices.size() * sizeof(particle_vertex)); + } +} + +void +opengl_particles::render() { + + if( m_buffercapacity == 0 ) { return; } + if( m_particlevertices.empty() ) { return; } + + if (!m_vao) { + m_vao.emplace(); + + m_vao->setup_attrib(*m_buffer, 0, 3, GL_FLOAT, sizeof(particle_vertex), 0); + m_vao->setup_attrib(*m_buffer, 1, 4, GL_FLOAT, sizeof(particle_vertex), 12); + m_vao->setup_attrib(*m_buffer, 2, 2, GL_FLOAT, sizeof(particle_vertex), 28); + + m_buffer->unbind(gl::buffer::ARRAY_BUFFER); + m_vao->unbind(); + } + + if (!m_shader) { + gl::shader vert("smoke.vert"); + gl::shader frag("smoke.frag"); + gl::program *prog = new gl::program({vert, frag}); + m_shader = std::unique_ptr(prog); + } + + m_buffer->bind(gl::buffer::ARRAY_BUFFER); + m_shader->bind(); + m_vao->bind(); + + glDrawArrays(GL_TRIANGLES, 0, m_particlevertices.size()); + + m_shader->unbind(); + m_vao->unbind(); + m_buffer->unbind(gl::buffer::ARRAY_BUFFER); +} + bool opengl_renderer::Init(GLFWwindow *Window) { if (!Init_caps()) @@ -105,6 +216,7 @@ bool opengl_renderer::Init(GLFWwindow *Window) m_glaretexture = Fetch_Texture("fx/lightglare"); m_suntexture = Fetch_Texture("fx/sun"); m_moontexture = Fetch_Texture("fx/moon"); + m_smoketexture = Fetch_Texture("fx/smoke"); WriteLog("...gfx data pre-loading done"); // prepare basic geometry chunks @@ -659,6 +771,9 @@ void opengl_renderer::Render_pass(viewport_config &vp, rendermode const Mode) setup_drawing(true); Render_Alpha(simulation::Region); + // particles + Render_particles(); + // precipitation; done at end, only before cab render Render_precipitation(); @@ -2909,9 +3024,23 @@ void opengl_renderer::Render(TMemCell *Memcell) ::glPopMatrix(); } +void opengl_renderer::Render_particles() +{ + // momentarily disable depth write, to allow vehicle cab drawn afterwards to mask it instead of leaving it 'inside' + glDepthMask(GL_FALSE); + + model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + model_ubo->update(model_ubs); + + Bind_Texture(0, m_smoketexture); + m_particlerenderer.update(m_renderpass.pass_camera); + m_particlerenderer.render(); + + glDepthMask(GL_TRUE); +} + void opengl_renderer::Render_precipitation() { - if (Global.Overcast <= 1.f) { return; @@ -2919,7 +3048,10 @@ void opengl_renderer::Render_precipitation() ::glPushMatrix(); // tilt the precipitation cone against the velocity vector for crude motion blur - auto const velocity{simulation::Environment.m_precipitation.m_cameramove * -1.0}; + // 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)}; diff --git a/renderer.h b/renderer.h index 0e86feac..9f946aba 100644 --- a/renderer.h +++ b/renderer.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "MemCell.h" #include "scene.h" #include "light.h" +#include "particles.h" #include "gl/ubo.h" #include "gl/framebuffer.h" #include "gl/renderbuffer.h" @@ -116,6 +117,42 @@ class opengl_camera glm::mat4 m_inversetransformation; // cached transformation to world space }; +// particle data visualizer +class opengl_particles { +public: +// constructors + opengl_particles() = default; + +// methods + void + update( opengl_camera const &Camera ); + void + render( ); +private: +// types + struct particle_vertex { + glm::vec3 position; // 3d space + glm::vec4 color; // rgba, unsigned byte format + glm::vec2 texture; // uv space + }; +/* + 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 + std::optional m_buffer; + std::optional m_vao; + std::unique_ptr m_shader; + + 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 { @@ -289,6 +326,7 @@ class opengl_renderer void Render(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last); bool Render_cab(TDynamicObject const *Dynamic, float const Lightlevel, bool const Alpha = false); void Render(TMemCell *Memcell); + void Render_particles(); void Render_precipitation(); void Render_Alpha(scene::basic_region *Region); void Render_Alpha(cell_sequence::reverse_iterator First, cell_sequence::reverse_iterator Last); @@ -324,6 +362,7 @@ class opengl_renderer texture_handle m_glaretexture{-1}; texture_handle m_suntexture{-1}; texture_handle m_moontexture{-1}; + texture_handle m_smoketexture{-1}; // main shadowmap resources int m_shadowbuffersize{2048}; @@ -333,6 +372,7 @@ class opengl_renderer 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 + opengl_particles m_particlerenderer; // particle visualization subsystem unsigned int m_framestamp; // id of currently rendered gfx frame float m_framerate; diff --git a/scene.cpp b/scene.cpp index 5b7d6fca..c0ef1fc7 100644 --- a/scene.cpp +++ b/scene.cpp @@ -1003,6 +1003,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 ); diff --git a/shaders/billboard.frag b/shaders/billboard.frag index f5265270..7ef0756f 100644 --- a/shaders/billboard.frag +++ b/shaders/billboard.frag @@ -1,4 +1,3 @@ -in vec3 f_normal; in vec2 f_coord; #texture (tex1, 0, sRGB_A) diff --git a/shaders/smoke.frag b/shaders/smoke.frag new file mode 100644 index 00000000..aa9a8c2d --- /dev/null +++ b/shaders/smoke.frag @@ -0,0 +1,34 @@ +in vec4 f_color; +in vec2 f_coord; + +in vec4 f_clip_pos; +in vec4 f_clip_future_pos; + +#texture (tex1, 0, sRGB_A) +uniform sampler2D tex1; + +#include +#include + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +void main() +{ + vec4 tex_color = texture(tex1, f_coord); +#if POSTFX_ENABLED + out_color = tex_color * f_color; +#else + out_color = tonemap(tex_color * f_color); +#endif +#if MOTIONBLUR_ENABLED + { + vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5;; + vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5;; + + out_motion = vec4(a - b, 0.0f, tex_color.a * alpha_mult); + } +#endif +} diff --git a/shaders/smoke.vert b/shaders/smoke.vert new file mode 100644 index 00000000..4ae3a45a --- /dev/null +++ b/shaders/smoke.vert @@ -0,0 +1,21 @@ +layout(location = 0) in vec3 v_vert; +layout(location = 1) in vec4 v_color; +layout(location = 2) in vec2 v_coord; + +out vec4 f_color; +out vec2 f_coord; + +out vec4 f_clip_pos; +out vec4 f_clip_future_pos; + +#include + +void main() +{ + f_clip_pos = (projection * modelview) * vec4(v_vert, 1.0f); + f_clip_future_pos = (projection * future * modelview) * vec4(v_vert, 1.0f); + + gl_Position = f_clip_pos; + f_coord = v_coord; + f_color = v_color; +} diff --git a/simulation.cpp b/simulation.cpp index c712bbe6..d2c6946a 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" @@ -38,6 +39,7 @@ train_table Trains; light_array Lights; sound_table Sounds; lua Lua; +particle_manager Particles; scene::basic_region *Region { nullptr }; TTrain *Train { nullptr }; diff --git a/simulation.h b/simulation.h index 2a36c5b4..1dceb1bd 100644 --- a/simulation.h +++ b/simulation.h @@ -59,6 +59,7 @@ extern train_table Trains; extern light_array Lights; extern sound_table Sounds; extern lua Lua; +extern particle_manager Particles; extern scene::basic_region *Region; extern TTrain *Train; diff --git a/simulationenvironment.cpp b/simulationenvironment.cpp index 28dcff7c..17bc36e2 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 { @@ -79,8 +80,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 @@ -176,6 +183,8 @@ world_environment::update() { else { m_precipitationsound.stop(); } + + update_wind(); } void @@ -184,6 +193,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 55ed0406..b241aae7 100644 --- a/simulationenvironment.h +++ b/simulationenvironment.h @@ -36,8 +36,25 @@ public: void compute_season( int const Yearday ); // calculates current weather void compute_weather(); + // 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 5cc7fc49..7109e477 100644 --- a/simulationstateserializer.cpp +++ b/simulationstateserializer.cpp @@ -14,6 +14,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" @@ -369,6 +370,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 ) + ")" ); diff --git a/version.h b/version.h index ce296b32..19def794 100644 --- a/version.h +++ b/version.h @@ -1 +1 @@ -#define VERSION_INFO "M7 (gfx-work) 13.07.2019" +#define VERSION_INFO "M7 (gfx-work) 8.08.2019"