17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 19:52:47 +01:00
Files
maszyna/model/AnimModel.cpp
pawelw bb009b0499 Text displays: draw opaque characters in the opaque pass (#201)
* Text displays: accept characters above 127, in windows-1250 or utf-8

A text display (sub-model of type "text") looks up the sub-model of each character
in a 256 entry table, and indexed it with a plain char. With char being signed, any
character above 127 gave a negative index: reading the text went outside the table,
and so did filling the table if a character sub-model had such a name. In effect the
displays could only show ASCII, and a text with national characters could crash.

The lookup is now keyed by unicode code point. Both the displayed text and the names
of the character sub-models are decoded as utf-8 if they are valid utf-8, and as
windows-1250 otherwise, so legacy (ansi) sceneries and models work next to utf-8
ones, and the two can be mixed. A short ansi text can be valid utf-8 by accident
(two upper case accented letters in a row will do); when both readings are possible
the one which finds more of its characters in the model is used.

The rest is unchanged: the first character of the text still isn't displayed, the
characters are still drawn in the translucent pass only. A missing text pointer no
longer gets dereferenced.

* Text displays: draw opaque characters in the opaque pass

The characters of a text display were only laid out in the translucent pass, so a
display had to be made of translucent sub-models to show anything. Those don't write
depth: overhead wires and other translucent geometry drawn later showed through the
display, and translucent geometry in front of it could end up painted over.

The opaque pass now lays the characters out the same way, so a display built from
opaque sub-models is drawn with depth like any other opaque geometry (and casts
shadows). Translucent characters keep working as before.

A model with a text display shows the text of its own instance, so such models are
excluded from the GPU-instanced opaque path.

* Text displays: prepare the characters once for each text

The sequence of character sub-models was cached for the last text only, so
displays of several instances drawn in turn decoded their texts again on
every draw. It is now kept for each text the display shows; the texts of the
instances don't change, so a draw costs a single lookup.

Also the comments added in the renderers are now in English.

* Text displays: move the text decoding to utilities

utf8_to_utf32() and win1250_to_utf32(), with the windows-1250 table, now sit
next to win1250_to_utf8(). The existing function maps only the Polish letters
and passes other bytes above 127 through, so it can't serve the displays.

* Text displays: address the SonarCloud findings

std::array instead of the C-style arrays, the utf-8 sequence length worked
out in a function of its own instead of nested conditional operators,
std::byte for the bit operations, std::ranges::count_if and contains().
The character loop of the renderers moved to Render_Alpha_text(), which
takes the nesting back to the limit, and the static pasText is accessed
through the class.

* Text displays: move the opaque character loop to Render_text()

The same as for the translucent pass: the loop sits in a function of its
own and the static pasText is accessed through the class, which keeps the
nesting within the limit.

* Text displays: access the static iAlpha through the class in the opaque pass

* Text displays: transparent lookup of the text layouts, init-statement for the lookup result

Two SonarCloud findings left in the code of #198: the map of the text layouts
gets a transparent hasher with std::equal_to<>, and the result of the lookup
is declared in the if statement.
2026-11-07 12:36:17 +01:00

932 lines
37 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
/*
MaSzyna EU07 locomotive simulator
Copyright (C) 2001-2004 Marcin Wozniak and others
*/
#include "stdafx.h"
#include "model/AnimModel.h"
#include "rendering/renderer.h"
#include "model/MdlMngr.h"
#include "simulation/simulation.h"
#include "simulation/simulationtime.h"
#include "world/Event.h"
#include "utilities/Globals.h"
#include "utilities/Timer.h"
#include "utilities/Logs.h"
#include "rendering/renderer.h"
std::list<std::weak_ptr<TAnimContainer>> TAnimModel::acAnimList;
TAnimContainer::TAnimContainer()
{
vRotateAngles = glm::vec3(0.0f, 0.0f, 0.0f); // aktualne kąty obrotu
vDesiredAngles = glm::vec3(0.0f, 0.0f, 0.0f); // docelowe kąty obrotu
fRotateSpeed = 0.0;
vTranslation = glm::dvec3(0.0, 0.0, 0.0); // aktualne przesunięcie
vTranslateTo = glm::dvec3(0.0, 0.0, 0.0); // docelowe przesunięcie
fTranslateSpeed = 0.0;
fAngleSpeed = 0.0;
pSubModel = nullptr;
iAnim = 0; // położenie początkowe
evDone = nullptr; // powiadamianie o zakończeniu animacji
}
bool TAnimContainer::Init(TSubModel *pNewSubModel)
{
fRotateSpeed = 0.0f;
pSubModel = pNewSubModel;
return pSubModel != nullptr;
}
void TAnimContainer::SetRotateAnim(glm::vec3 vNewRotateAngles, double fNewRotateSpeed)
{
vDesiredAngles = vNewRotateAngles;
fRotateSpeed = fNewRotateSpeed;
iAnim |= 1;
/* //Ra 2014-07: jeśli model nie jest renderowany, to obliczyć czas animacji i dodać event
wewnętrzny
//można by też ustawić czas początku animacji zamiast pobierać czas ramki i liczyć różnicę
*/
if (evDone)
{ // dołączyć model do listy aniomowania, żeby animacje były przeliczane również bez
// wyświetlania
if (iAnim >= 0)
{ // jeśli nie jest jeszcze na liście animacyjnej
TAnimModel::acAnimList.push_back(shared_from_this());
iAnim |= 0x80000000; // dodany do listy
}
}
}
void TAnimContainer::SetTranslateAnim(glm::dvec3 vNewTranslate, double fNewSpeed)
{
vTranslateTo = vNewTranslate;
fTranslateSpeed = fNewSpeed;
iAnim |= 2;
/* //Ra 2014-07: jeśli model nie jest renderowany, to obliczyć czas animacji i dodać event
wewnętrzny
//można by też ustawić czas początku animacji zamiast pobierać czas ramki i liczyć różnicę
*/
if (evDone)
{ // dołączyć model do listy aniomowania, żeby animacje były przeliczane również bez
// wyświetlania
if (iAnim >= 0)
{ // jeśli nie jest jeszcze na liście animacyjnej
TAnimModel::acAnimList.push_back(shared_from_this());
iAnim |= 0x80000000; // dodany do listy
}
}
}
// przeliczanie animacji wykonać tylko raz na model
void TAnimContainer::UpdateModel() {
if (pSubModel) // pozbyć się tego - sprawdzać wcześniej
{
if (fTranslateSpeed != 0.0)
{
auto dif = vTranslateTo - vTranslation; // wektor w kierunku docelowym
double l2 = glm::length2(dif); // długość wektora potrzebnego przemieszczenia
if (l2 >= sq(0.01))
{ // jeśli do przemieszczenia jest ponad 1cm
auto s = glm::normalize(dif); // jednostkowy wektor kierunku // Długość wektora nie jest równa 0, sprawdzane wcześniej więc wektor normalny będzie zawsze prawidłowy.
s = s *
(fTranslateSpeed *
Timer::GetDeltaTime()); // przemieszczenie w podanym czasie z daną prędkością
if (glm::length2(s) < l2) //żeby nie jechało na drugą stronę
vTranslation += s;
else
vTranslation = vTranslateTo; // koniec animacji, "koniec animowania" uruchomi
// się w następnej klatce
}
else
{ // koniec animowania
vTranslation = vTranslateTo;
fTranslateSpeed = 0.0; // wyłączenie przeliczania wektora
if (glm::length2(vTranslation) <= sq(0.01)) // jeśli jest w punkcie początkowym
iAnim &= ~2; // wyłączyć zmianę pozycji submodelu
if( evDone ) {
// wykonanie eventu informującego o zakończeniu
simulation::Events.AddToQuery( evDone, nullptr );
}
}
}
if (fRotateSpeed != 0.0)
{
bool anim = false;
auto dif = vDesiredAngles - vRotateAngles;
double s;
s = std::abs( fRotateSpeed ) * sign(dif.x) * Timer::GetDeltaTime();
if (fabs(s) >= fabs(dif.x))
vRotateAngles.x = vDesiredAngles.x;
else
{
vRotateAngles.x += s;
anim = true;
}
s = std::abs( fRotateSpeed ) * sign(dif.y) * Timer::GetDeltaTime();
if (fabs(s) >= fabs(dif.y))
vRotateAngles.y = vDesiredAngles.y;
else
{
vRotateAngles.y += s;
anim = true;
}
s = std::abs( fRotateSpeed ) * sign(dif.z) * Timer::GetDeltaTime();
if (fabs(s) >= fabs(dif.z))
vRotateAngles.z = vDesiredAngles.z;
else
{
vRotateAngles.z += s;
anim = true;
}
// HACK: negative speed allows to work around legacy behaviour, where desired angle > 360 meant permanent rotation
if( fRotateSpeed > 0.0 ) {
while( vRotateAngles.x >= 360 )
vRotateAngles.x -= 360;
while( vRotateAngles.x <= -360 )
vRotateAngles.x += 360;
while( vRotateAngles.y >= 360 )
vRotateAngles.y -= 360;
while( vRotateAngles.y <= -360 )
vRotateAngles.y += 360;
while( vRotateAngles.z >= 360 )
vRotateAngles.z -= 360;
while( vRotateAngles.z <= -360 )
vRotateAngles.z += 360;
}
if( vRotateAngles.x == 0.0
&& vRotateAngles.y == 0.0
&& vRotateAngles.z == 0.0 ) {
iAnim &= ~1; // kąty są zerowe
}
if (!anim)
{ // nie potrzeba przeliczać już
fRotateSpeed = 0.0;
if( evDone ) {
// wykonanie eventu informującego o zakończeniu
simulation::Events.AddToQuery( evDone, nullptr );
}
}
}
if( fAngleSpeed != 0.f ) {
// NOTE: this is angle- not quaternion-based rotation TBD, TODO: switch to quaternion rotations?
fAngleCurrent += fAngleSpeed * Timer::GetDeltaTime(); // aktualny parametr interpolacji
}
}
};
void TAnimContainer::PrepareModel()
{ // tutaj zostawić tylko ustawienie submodelu, przeliczanie ma być w UpdateModel()
if (pSubModel) // pozbyć się tego - sprawdzać wcześniej
{
// nanoszenie animacji na wzorzec
if (iAnim & 1) // zmieniona pozycja względem początkowej
pSubModel->SetRotateXYZ(vRotateAngles); // ustawia typ animacji
if (iAnim & 2) // zmieniona pozycja względem początkowej
pSubModel->SetTranslate(vTranslation);
if (iAnim & 4) // zmieniona pozycja względem początkowej
{
if (fAngleSpeed > 0.0f)
{
if (fAngleCurrent >= 1.0f)
{ // interpolacja zakończona, ustawienie na pozycję końcową
qCurrent = qDesired;
fAngleSpeed = 0.0; // wyłączenie przeliczania wektora
if( evDone ) {
// wykonanie eventu informującego o zakończeniu
simulation::Events.AddToQuery( evDone, nullptr );
}
}
else
{ // obliczanie pozycji pośredniej
// normalizacja jest wymagana do interpolacji w następnej animacji
qCurrent = Normalize(
Slerp(qStart, qDesired, fAngleCurrent)); // interpolacja sferyczna kąta
// qCurrent=Slerp(qStart,qDesired,fAngleCurrent); //interpolacja sferyczna kąta
if (qCurrent.w ==
1.0) // rozpoznać brak obrotu i wyłączyć w iAnim w takim przypadku
iAnim &= ~4; // kąty są zerowe
}
}
mAnim->Quaternion(&qCurrent); // wypełnienie macierzy (wymaga normalizacji?)
pSubModel->mAnimMatrix = mAnim; // użyczenie do submodelu (na czas renderowania!)
}
}
// if (!strcmp(pSubModel->pName,"?Z?“?^?[")) //jak główna kość
// WriteLog(AnsiString(pMovementData->iFrame)+": "+AnsiString(iAnim)+"
// "+AnsiString(vTranslation.x)+" "+AnsiString(vTranslation.y)+" "+AnsiString(vTranslation.z));
}
bool TAnimContainer::InMovement()
{ // czy trwa animacja - informacja dla obrotnicy
return fRotateSpeed != 0.0 || fTranslateSpeed != 0.0;
}
void TAnimContainer::EventAssign(basic_event *ev)
{ // przypisanie eventu wykonywanego po zakończeniu animacji
evDone = ev;
};
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
TAnimModel::TAnimModel( scene::node_data const &Nodedata ) : basic_node( Nodedata ) {
m_lightcolors.fill( glm::vec3{ -1.f } );
m_lightopacities.fill( 1.f );
}
bool TAnimModel::Init(std::string const &asName, std::string const &asReplacableTexture)
{
m_skintoken = asReplacableTexture;
if( asReplacableTexture.substr( 0, 1 ) == "*" ) {
// od gwiazdki zaczynają się teksty na wyświetlaczach
asText = asReplacableTexture.substr( 1, asReplacableTexture.length() - 1 ); // zapamiętanie tekstu
}
else if( asReplacableTexture != "none" ) {
// the same skin is typically shared by lots of instances, and locating its files takes a couple dozen disk lookups.
// NOTE: only located skins are kept, so a missing one is still searched for each time, the way it used to be
static std::unordered_map<std::string, material_data> skins;
auto const skinkey { Global.asCurrentTexturePath + '|' + asReplacableTexture };
auto const lookup { skins.find( skinkey ) };
if( lookup != skins.end() ) {
m_materialdata = lookup->second;
}
else {
m_materialdata.assign( asReplacableTexture );
if( m_materialdata.replacable_skins[ 1 ] != null_handle ) {
skins.emplace( skinkey, m_materialdata );
}
}
}
// TODO: redo the random timer initialization
// fBlinkTimer = Random() * ( fOnTime + fOffTime );
pModel = TModelsManager::GetModel( asName );
return pModel != nullptr;
}
bool
TAnimModel::is_keyword( std::string const &Token ) const {
return Token == "endmodel"
|| Token == "lights"
|| Token == "lightcolors"
|| Token == "angles"
|| Token == "scale"
|| Token == "notransition";
}
// assigns the model and the replacable skin to the instance, and binds light and variant submodels of the model
void TAnimModel::assign_model( std::string &name, std::string const &texture, bool const ter )
{
if (!Init( name, texture ))
{
if (name != "notload")
{ // gdy brak modelu
if (ter) // jeśli teren
{
if( name.ends_with(".t3d") ) {
name[ name.length() - 3 ] = 'e';
}
#ifdef EU07_USE_OLD_TERRAINCODE
Global.asTerrainModel = name;
WriteLog("Terrain model \"" + name + "\" will be created.");
#endif
}
else
ErrorLog("Missed file: " + name);
}
}
else
{ // wiązanie świateł, o ile model wczytany
// the submodels are located once for the model and shared by its instances
auto const &submodels { pModel->instance_parts() };
LightsOn = submodels.lights_on;
LightsOff = submodels.lights_off;
sm_winter_variant = submodels.variants[ 0 ];
sm_spring_variant = submodels.variants[ 1 ];
sm_summer_variant = submodels.variants[ 2 ];
sm_autumn_variant = submodels.variants[ 3 ];
}
for (int i = 0; i < iMaxNumLights; ++i)
if (LightsOn[i] || LightsOff[i]) // Ra: zlikwidowałem wymóg istnienia obu
iNumLights = i + 1;
}
bool TAnimModel::Load(cParser *parser, bool ter)
{ // rozpoznanie wpisu modelu i ustawienie świateł
std::string name = parser->getToken<std::string>();
std::string texture = parser->getToken<std::string>(false);
replace_slashes( name );
replace_slashes( texture );
assign_model( name, texture, ter );
std::string token;
do {
token = parser->getToken<std::string>();
if( token == "lights" ) {
auto i{ 0 };
while( false == (token = parser->getToken<std::string>()).empty()
&& false == is_keyword(token) ) {
if( i < iNumLights ) {
// stan światła jest liczbą z ułamkiem
LightSet( i, std::stof( token ) );
}
++i;
}
}
if( token == "lightcolors" ) {
auto i{ 0 };
while( false == (token = parser->getToken<std::string>()).empty()
&& false == is_keyword(token) ) {
if( i < iNumLights
&& token != "-1" ) { // -1 leaves the default color intact
auto const lightcolor { std::stoi( token, 0, 16 ) };
m_lightcolors[i] = {
( lightcolor >> 16 & 0xff ) / 255.f,
( lightcolor >> 8 & 0xff ) / 255.f,
( lightcolor & 0xff ) / 255.f };
}
++i;
}
}
if( token == "angles" ) {
parser->getTokens( 3 );
*parser
>> vAngle[ 0 ]
>> vAngle[ 1 ]
>> vAngle[ 2 ];
}
if( token == "scale" ) {
// Per-node scale: `scale <x> <y> <z>` (always three tokens, mirroring
// the `angles` syntax). For uniform scaling, write the same value
// three times (e.g. `scale 2 2 2`). Combines multiplicatively with
// any active scale block from the scenariostateserializer (which is
// applied at deserialize_model time before Load() is called, so
// m_scale already reflects the outer block when we arrive here).
parser->getTokens( 3 );
glm::vec3 factor;
*parser >> factor.x >> factor.y >> factor.z;
if( factor.x > 0.0f && factor.y > 0.0f && factor.z > 0.0f ) {
m_scale.x *= factor.x;
m_scale.y *= factor.y;
m_scale.z *= factor.z;
}
}
if( token == "notransition" ) {
m_transition = false;
}
} while( false == token.empty()
&& token != "endmodel" );
update_instanceable_flag();
return true;
}
// sets up the instance from the values of a definition which was already taken apart, the way Load() does it from the text
void TAnimModel::Load( std::string const &Name, std::string const &Texture, TAnimModel const *Twin, glm::vec3 const *Angles, glm::vec3 const *Scale, bool const Transition )
{
if( Twin != nullptr
&& Twin->pModel != nullptr
&& ( Twin->m_materialdata.replacable_skins[ 1 ] != null_handle || Texture == "none" ) ) {
// the same model with the same skin was located for the twin already.
// NOTE: a model or a skin which couldn't be located is searched for and reported for each instance, like it always was
m_skintoken = Twin->m_skintoken;
m_materialdata = Twin->m_materialdata;
pModel = Twin->pModel;
LightsOn = Twin->LightsOn;
LightsOff = Twin->LightsOff;
sm_winter_variant = Twin->sm_winter_variant;
sm_spring_variant = Twin->sm_spring_variant;
sm_summer_variant = Twin->sm_summer_variant;
sm_autumn_variant = Twin->sm_autumn_variant;
iNumLights = Twin->iNumLights;
}
else {
auto name { Name };
assign_model( name, Texture, false );
}
// the optional blocks, each with the effect it has in Load()
if( Angles != nullptr ) {
vAngle = *Angles;
}
if( Scale != nullptr
&& Scale->x > 0.0f && Scale->y > 0.0f && Scale->z > 0.0f ) {
m_scale.x *= Scale->x;
m_scale.y *= Scale->y;
m_scale.z *= Scale->z;
}
if( false == Transition ) {
m_transition = false;
}
update_instanceable_flag();
}
namespace {
// returns true if this animation type mutates per-instance state on the shared
// TSubModel tree, which would make batched rendering unsafe. Most animations
// loaded from .t3d files are global functions of time (clocks, wind, sky) and
// only transform the local modelview matrix — those are safe to share. Camera-
// relative billboards are not: they're computed on the cpu from the modelview of
// the drawn instance, which in a batch is only the submodel-local chain.
// The runtime SetRotate/SetTranslate animations (at_Rotate / at_RotateXYZ /
// at_Translate) are tied to per-instance iAnimOwner and are unsafe to share.
// at_Undefined is the type assigned to .t3d submodels declared with `anim: true`
// (a generic "this submodel is animatable" hint) and to anything else that
// doesn't match a recognised animation keyword — these are driven by event-
// triggered SetRotate/SetTranslate at runtime, which would silently break if
// the model were batched.
bool anim_type_unsafe_for_instancing( TAnimType a ) {
switch( a ) {
case TAnimType::at_Rotate:
case TAnimType::at_RotateXYZ:
case TAnimType::at_Translate:
case TAnimType::at_DigiClk: // mutates child submodels via SetRotate
case TAnimType::at_Undefined: // `anim: true` / unknown — driven by events
return true;
default:
return false;
}
}
// returns true if this animation type depends only on global state (simulation
// time, wind, sky) and so gives every instance of the model the same local
// transform. The batch evaluates it once and the per-instance placement comes
// from the instance matrix, the result is the same as with per-instance drawing.
bool anim_type_shared_by_instances( TAnimType a ) {
switch( a ) {
case TAnimType::at_SecondsJump:
case TAnimType::at_MinutesJump:
case TAnimType::at_HoursJump:
case TAnimType::at_Hours24Jump:
case TAnimType::at_Seconds:
case TAnimType::at_Minutes:
case TAnimType::at_Hours:
case TAnimType::at_Hours24:
case TAnimType::at_Wind:
case TAnimType::at_Sky:
return true;
default:
return false;
}
}
// recursively walks a submodel tree and returns true if any submodel declares
// an animation type that's unsafe to batch, OR carries the runtime "needs
// animation matrix" flag (iFlags bit 0x4000) for anything else than an animation
// shared by all instances. The flag is set whenever the submodel was tagged as
// animatable in the .t3d file (any `anim:` other than `false`) or had
// WillBeAnimated() called on it during model load. Such a submodel may receive
// per-instance event-driven animation commands at runtime, which the GPU-
// instanced path (one shared submodel tree across all instances) cannot serve.
bool submodel_tree_blocks_instancing( TSubModel const *Sub ) {
if( Sub == nullptr ) { return false; }
// a text display shows the text of its instance, a tree shared by all instances can't do that
if( Sub->eType == TP_TEXT ) { return true; }
if( anim_type_unsafe_for_instancing( Sub->b_Anim ) ) { return true; }
if( ( ( Sub->iFlags & 0x4000 ) != 0 ) && ( false == anim_type_shared_by_instances( Sub->b_Anim ) ) ) { return true; }
if( submodel_tree_blocks_instancing( Sub->Child ) ) { return true; }
if( submodel_tree_blocks_instancing( Sub->Next ) ) { return true; }
return false;
}
} // anonymous namespace
int TAnimModel::s_instanceable_total = 0;
unsigned int TAnimModel::s_instanceable_revoked = 0;
int TAnimModel::s_classified_total = 0;
int TAnimModel::s_rejected_no_pmodel = 0;
int TAnimModel::s_rejected_lights = 0;
int TAnimModel::s_rejected_animlist = 0;
int TAnimModel::s_rejected_animated_submodel = 0;
void TAnimModel::update_instanceable_flag() {
// The instanceable path skips RaAnimate() entirely and only calls RaPrepare()
// once per bucket. The conditions below ensure that's safe:
// - no lights: per-instance light state machines must not exist
// - no anim list: per-instance submodel animations must not exist
// - no animated submodels in the shared TModel3d
// Replacable skins, seasonal variants, and submodel replacable-skin material
// refs are intentionally allowed — they're either passed per-instance via
// Material() or share global state (season) across all instances.
m_instanceable = false;
++s_classified_total;
if( pModel == nullptr ) { ++s_rejected_no_pmodel; return; }
if( iNumLights != 0 ) { ++s_rejected_lights; return; }
if( !m_animlist.empty() ) { ++s_rejected_animlist; return; }
if( submodel_tree_blocks_instancing( pModel->Root ) ) { ++s_rejected_animated_submodel; return; }
m_instanceable = true;
++s_instanceable_total;
}
std::shared_ptr<TAnimContainer> TAnimModel::AddContainer(std::string const &Name)
{ // dodanie sterowania submodelem dla egzemplarza
if (!pModel)
return nullptr;
TSubModel *tsb = pModel->GetFromName(Name);
if (tsb)
{
auto tmp = std::make_shared<TAnimContainer>();
tmp->Init(tsb);
m_animlist.push_back(tmp);
// containers are added by events after the instance was loaded and bucketed;
// the instanced path skips RaAnimate(), so this instance has to be drawn on its own
if( m_instanceable ) { ++s_instanceable_revoked; }
m_instanceable = false;
return tmp;
}
return nullptr;
}
std::shared_ptr<TAnimContainer> TAnimModel::GetContainer(std::string const &Name)
{ // szukanie/dodanie sterowania submodelem dla egzemplarza
if (true == Name.empty())
return !m_animlist.empty() ? m_animlist.front() : nullptr; // pobranie pierwszego (dla obrotnicy)
for (auto entry : m_animlist) {
if (entry->NameGet() == Name)
return entry;
}
return AddContainer(Name);
}
// przeliczenie animacji - jednorazowo na klatkę
void TAnimModel::RaAnimate( unsigned int const Framestamp ) {
if( Framestamp == m_framestamp ) { return; }
auto const timedelta { Timer::GetDeltaTime() };
// interpretacja ułamka zależnie od typu
// case ls_Off: ustalenie czasu migotania, t<1s (f>1Hz), np. 0.1 => t=0.1 (f=10Hz)
// case ls_On: ustalenie wypełnienia ułamkiem, np. 1.25 => zapalony przez 1/4 okresu
// case ls_Blink: ustalenie częstotliwości migotania, f<1Hz (t>1s), np. 2.2 => f=0.2Hz (t=5s)
for (int idx = 0; idx < iNumLights; ++idx) {
float modeintegral;
const float modefractional = std::modf(std::abs(lsLights[idx]), &modeintegral);
const auto mode = static_cast<TLightState>(modeintegral);
if (mode == ls_Dark || mode == ls_Home)
continue; // light threshold modes don't use timers
float &opacity { m_lightopacities[ idx ] };
float &timer { m_lighttimers[ idx ] };
// Phase logic
float ontime =
modefractional < 0.01f ? fOnTime :
mode == ls_Off ? modefractional * 0.5f :
mode == ls_On ? modefractional * (fOnTime + fOffTime) :
mode == ls_Blink ? 0.5f / modefractional :
fOnTime; // fallback
float offtime =
modefractional < 0.01f ? fOffTime :
mode == ls_Off ? modefractional * 0.5f :
mode == ls_On ? (1 - modefractional) * (fOnTime + fOffTime) :
mode == ls_Blink ? 0.5f / modefractional :
fOffTime; // fallback
// Determine whether the light is currently on and update the timers
bool on = false;
if ((mode == ls_Off || mode == ls_On) && modefractional < 0.01f)
on = mode == ls_On;
else {
timer = fmod(timer + timedelta, ontime + offtime); // 0..(ontime+offtime)
const float time = fmod(timer + ( lsLights[ idx ] > 0.f ? 0.f : 0.5f ), ontime + offtime); // time with correction for phase shift for negative light values
on = time < ontime;
}
// Update the light brightness
const float transitionontime = std::min(0.25f, std::min(ontime, offtime) * 0.95f);
const float transitionofftime = std::min(0.45f, std::min(ontime, offtime) * 0.95f);
if (on)
opacity += m_transition ? timedelta / transitionontime : 1.f; // increase to max value
else
opacity -= m_transition ? timedelta / transitionofftime : 1.f; // reduce to zero
// Clamp the opacity
opacity = std::clamp(opacity, 0.f, 1.f);
}
// Ra 2F1I: to by można pomijać dla modeli bez animacji, których jest większość
for (const auto entry : m_animlist) {
if (!entry->evDone) // jeśli jest bez eventu
entry->UpdateModel(); // przeliczenie animacji każdego submodelu
}
m_framestamp = Framestamp;
}
// aktualizujemy submodele w zaleznosci od aktualnej porty roku
void TAnimModel::on_season_update() {
if (this->sm_winter_variant != nullptr) // pokazujemy wariant zimowy
this->sm_winter_variant->SetVisibilityLevel(Global.Season == "winter:" ? 1 : 0, true, false);
if (this->sm_spring_variant != nullptr) // pokazujemy wariant wiosenny
this->sm_spring_variant->SetVisibilityLevel(Global.Season == "spring:" ? 1 : 0, true, false);
if (this->sm_summer_variant != nullptr) // pokazujemy wariant letni
this->sm_summer_variant->SetVisibilityLevel(Global.Season == "summer:" ? 1 : 0, true, false);
if (this->sm_autumn_variant != nullptr) // pokazujemy wariant jesienny
this->sm_autumn_variant->SetVisibilityLevel(Global.Season == "autumn:" ? 1 : 0, true, false);
}
void TAnimModel::RaPrepare()
{ // ustawia światła i animacje we wzorcu modelu przed renderowaniem egzemplarza
if (Global.UpdateMaterials)
on_season_update();
for (int i = 0; i < iNumLights; ++i)
{
bool state{ false }; // stan światła
auto const lightmode { static_cast<int>( std::abs( lsLights[ i ] ) ) };
switch( lightmode ) {
case ls_On:
case ls_Off:
case ls_Blink: {
if (LightsOn[i]) {
LightsOn[i]->iVisible = m_lightopacities[i] > 0.f;
LightsOn[i]->SetVisibilityLevel( m_lightopacities[i], true, false );
}
if (LightsOff[i]) {
LightsOff[i]->iVisible = m_lightopacities[i] < 1.f;
LightsOff[i]->SetVisibilityLevel( 1.f, true, false );
}
break;
}
case ls_Dark: {
// zapalone, gdy ciemno
state =
Global.fLuminance - std::max(0.f, Global.Overcast - 1.f) <= (lsLights[i] == static_cast<float>(ls_Dark) ? DefaultDarkThresholdLevel : lsLights[i] - static_cast<float>(ls_Dark));
break;
}
case ls_Home: {
// like ls_dark but off late at night
auto const simulationhour { simulation::Time.data().wHour };
state =
Global.fLuminance - std::max(0.f, Global.Overcast - 1.f) <= (lsLights[i] == static_cast<float>(ls_Home) ? DefaultDarkThresholdLevel : lsLights[i] - static_cast<float>(ls_Home));
// force the lights off between 1-5am
state = state && (simulationhour < 1 || simulationhour >= 5);
break;
}
default: {
break;
}
}
if( lightmode >= ls_Dark ) {
// crude as hell but for test will do :x
if (LightsOn[i]) {
LightsOn[i]->iVisible = state;
// TODO: set visibility for the entire submodel's children as well
LightsOn[i]->fVisible = m_lightopacities[i];
}
if (LightsOff[i])
LightsOff[i]->iVisible = !state;
}
// potentially modify freespot colors
if( LightsOn[i] ) {
LightsOn[i]->SetDiffuseOverride( m_lightcolors[i], true);
}
}
TSubModel::iInstance = reinterpret_cast<std::uintptr_t>( this ); //żeby nie robić cudzych animacji
// NOTE: scenery models don't pick texture variants of their own, so we clear the seed left by the
// last rendered vehicle. instanced draws share a single material bind anyway, which would make
// per instance variants inconsistent between batched and individually drawn models
TSubModel::iVariantSeed = 0;
TSubModel::pasText = &asText; // przekazanie tekstu do wyświetlacza (!!!! do przemyślenia)
for (auto entry : m_animlist) {
entry->PrepareModel();
}
}
glm::mat4 const &TAnimModel::rotation_scale() {
if( ( false == m_rotationscalevalid ) || ( vAngle != m_rotationscaleangles ) || ( m_scale != m_rotationscalescale ) ) {
// same order as the per-instance render path
glm::mat4 transform( 1.0f );
if( vAngle.y != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.y ), glm::vec3( 0.f, 1.f, 0.f ) ); }
if( vAngle.x != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.x ), glm::vec3( 1.f, 0.f, 0.f ) ); }
if( vAngle.z != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.z ), glm::vec3( 0.f, 0.f, 1.f ) ); }
if( m_scale != glm::vec3( 1.0f ) ) { transform = glm::scale( transform, m_scale ); }
m_rotationscale = transform;
m_rotationscaleangles = vAngle;
m_rotationscalescale = m_scale;
m_rotationscalevalid = true;
}
return m_rotationscale;
}
int TAnimModel::Flags()
{ // informacja dla TGround, czy ma być w Render, RenderAlpha, czy RenderMixed
int i = pModel ? pModel->Flags() : 0; // pobranie flag całego modelu
if( m_materialdata.replacable_skins[ 1 ] > 0 ) // jeśli ma wymienną teksturę 0
i |= (i & 0x01010001) * (m_materialdata.textures_alpha & 1 ? 0x20 : 0x10);
return i;
}
//---------------------------------------------------------------------------
int TAnimModel::TerrainCount()
{ // zliczanie kwadratów kilometrowych (główna linia po Next) do tworznia tablicy
return pModel ? pModel->TerrainCount() : 0;
}
TSubModel * TAnimModel::TerrainSquare(int n)
{ // pobieranie wskaźników do pierwszego submodelu
return pModel ? pModel->TerrainSquare(n) : 0;
}
//---------------------------------------------------------------------------
void TAnimModel::LightSet(int const n, float const v)
{ // ustawienie światła (n) na wartość (v)
if( n >= iMaxNumLights ) {
return; // przekroczony zakres
}
lsLights[ n ] = v;
}
std::optional<std::tuple<float, float, std::optional<glm::vec3>> > TAnimModel::LightGet(const int n)
{
if (n >= iMaxNumLights)
return std::nullopt;
if (!LightsOn[n] && !LightsOff[n])
return std::nullopt;
std::optional<glm::vec3> color;
if (m_lightcolors[n].r >= 0.0f)
color.emplace(m_lightcolors[n]);
if (!color && LightsOn[n])
color = LightsOn[n]->GetDiffuse();
return std::make_tuple(lsLights[n], m_lightopacities[n], color);
}
void TAnimModel::SkinSet( int const Index, material_handle const Material ) {
m_materialdata.replacable_skins[ std::clamp( Index, 1, 4 ) ] = Material;
// the instance stays in the bucket of its old skin set, where it would get the textures of the batch
if( m_instanceable ) { ++s_instanceable_revoked; }
m_instanceable = false;
}
void TAnimModel::AnimUpdate(double dt)
{ // wykonanie zakolejkowanych animacji, nawet gdy modele nie są aktualnie wyświetlane
acAnimList.remove_if([](std::weak_ptr<TAnimContainer> ptr)
{
std::shared_ptr<TAnimContainer> container = ptr.lock();
if (!container)
return true;
container->UpdateModel(); // na razie bez usuwania z listy, bo głównie obrotnica na nią wchodzi
return false;
});
}
// radius() subclass details, calculates node's bounding radius.
// For non-uniform scale we use the largest axis factor so the bounding sphere
// fully contains the scaled model — undersizing would cause incorrect culling.
float
TAnimModel::radius_() {
if( pModel == nullptr ) { return 0.f; }
float const max_scale = std::max( { m_scale.x, m_scale.y, m_scale.z } );
return pModel->bounding_radius() * max_scale;
}
// serialize() subclass details, sends content of the subclass to provided stream
void
TAnimModel::serialize_( std::ostream &Output ) const {
// TODO: implement
}
// deserialize() subclass details, restores content of the subclass from provided stream
void
TAnimModel::deserialize_( std::istream &Input ) {
// TODO: implement
}
// export() subclass details, sends basic content of the class in legacy (text) format to provided stream.
// Smart export: omit fields that match defaults so reloaded scenarios stay clean.
// - if X and Z rotation are zero, fold Y rotation into the 4th token slot
// (the legacy `node ... model X Y Z <rotation.y> ...` format) and skip the
// `angles` block entirely
// - if any axis of rotation needs all three components, emit `angles X Y Z`
// - emit `scale X Y Z` only when m_scale isn't (1,1,1)
void
TAnimModel::export_as_text_( std::ostream &Output ) const {
// header
Output << "model ";
// location and rotation. The 4th token after location is a legacy
// shorthand for the Y rotation. We use it (and skip the angles block)
// whenever the rotation is purely around Y, which is the common case.
bool const xz_rotation_zero = vAngle.x == 0.0f && vAngle.z == 0.0f;
Output << std::fixed << std::setprecision( 3 )
<< location().x << ' '
<< location().y << ' '
<< location().z << ' '
<< ( xz_rotation_zero ? vAngle.y : 0.0f ) << ' ';
// 3d shape
auto modelfile { (
pModel ?
pModel->NameGet() + ".t3d" : // rainsted requires model file names to include an extension
"none" ) };
if( modelfile.find( paths::models ) == 0 ) {
// don't include 'models/' in the path
modelfile.erase( 0, std::string{ paths::models }.size() );
}
Output << modelfile << ' ';
// texture
// the skin goes out the way it came in. the materials it was turned into can't give it back: they don't tell
// a set of skins from a single one, a text for a display from no skin at all, or what a texture was to be generated from
auto texturefile { m_skintoken };
if( texturefile.empty() ) {
texturefile = (
m_materialdata.replacable_skins[ 1 ] != null_handle ?
GfxRenderer->Material( m_materialdata.replacable_skins[ 1 ] )->GetName() :
"none" );
if( texturefile.find( paths::textures ) == 0 ) {
// don't include 'textures/' in the path
texturefile.erase( 0, std::string{ paths::textures }.size() );
}
}
if( contains( texturefile, ' ' ) ) {
Output << "\"" << texturefile << "\"" << ' ';
}
else {
Output << texturefile << ' ';
}
// light submodels activation configuration
if( iNumLights > 0 ) {
Output << "lights ";
for( int lightidx = 0; lightidx < iNumLights; ++lightidx ) {
Output << lsLights[ lightidx ] << ' ';
}
}
// colors given to the lights; goes right after the lights, the way it's read
if( std::any_of( std::begin( m_lightcolors ), std::begin( m_lightcolors ) + iNumLights, []( glm::vec3 const &Color ) { return Color.r >= 0.f; } ) ) {
Output << "lightcolors ";
for( int lightidx = 0; lightidx < iNumLights; ++lightidx ) {
auto const &color { m_lightcolors[ lightidx ] };
if( color.r < 0.f ) {
// the light was left with the color the model gives it
Output << "-1 ";
continue;
}
auto const component = []( float const Value ) { return std::clamp( static_cast<int>( std::lround( Value * 255.f ) ), 0, 255 ); };
Output
<< std::hex << std::setfill( '0' ) << std::setw( 6 )
<< ( component( color.r ) << 16 | component( color.g ) << 8 | component( color.b ) )
<< std::dec << std::setfill( ' ' ) << ' ';
}
}
// potential light transition switch
if( false == m_transition ) {
Output << "notransition" << ' ';
}
// angles directive only when X or Z are rotated — otherwise the Y angle
// already lives in the 4th token slot above.
if( false == xz_rotation_zero ) {
Output << "angles "
<< vAngle.x << ' '
<< vAngle.y << ' '
<< vAngle.z << ' ';
}
// scale directive only when actually scaled — keeps default-scale models
// from being polluted with redundant `scale 1 1 1` entries on every save.
if( m_scale.x != 1.0f || m_scale.y != 1.0f || m_scale.z != 1.0f ) {
Output << "scale "
<< m_scale.x << ' '
<< m_scale.y << ' '
<< m_scale.z << ' ';
}
// footer
Output
<< "endmodel"
<< "\n";
}
//---------------------------------------------------------------------------