17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-25 02:07:37 +02:00

feat(track): rails and ballast blended into those of the tracks joined, where their textures differ

This commit is contained in:
2026-10-21 22:34:57 +02:00
parent 407a555803
commit c5a894a674
19 changed files with 721 additions and 14 deletions

View File

@@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "world/TrackRails.h"
#include "world/TrackBlend.h"
#include "rendering/renderer.h"
namespace track_rails {
@@ -213,7 +214,9 @@ reused_chunks::~reused_chunks() {
reused = m_previous;
}
gfx::geometry_handle insert_chunk( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Bank, int const Type ) {
namespace {
gfx::geometry_handle insert_weighed( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Bank, int const Type ) {
if( reused == nullptr ) {
return GfxRenderer->Insert( Vertices, Userdata, Bank, Type );
@@ -237,6 +240,19 @@ gfx::geometry_handle insert_chunk( gfx::vertex_array &Vertices, gfx::userdata_ar
return handle;
}
} // namespace
gfx::geometry_handle insert_chunk( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Bank, int const Type ) {
if( auto const *blending { track_blend::weighed() }; blending != nullptr ) {
// the weights of the textures the track blends into near its ends; the user data given are the caller's own, often shared
auto weighed { Userdata };
blending->weigh( Vertices, weighed );
return insert_weighed( Vertices, weighed, Bank, Type );
}
return insert_weighed( Vertices, Userdata, Bank, Type );
}
void cut_faces( gfx::vertex_array const &Strip, std::size_t const Ring, bool const Start, bool const End, gfx::geometrybank_handle const &Bank, TTrack::geometryhandle_sequence &Geometry ) {
gfx::vertex_array triangles;

View File

@@ -701,6 +701,18 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin)
m_timber_model_name = modelpath;
m_timber_skin_name = ( skinpath == "none" ? std::string{} : skinpath );
}
else if( str == "blend" ) {
// blend <end> <length>: how far from the end 1 (p1), 2 (p2) or 3 (p4 of a switch) the rails and the ballast blend into those
// of the track joined there, where their textures differ; 0: not at all
auto const end { parser->getToken<int>() };
auto const length { parser->getToken<double>() };
if( end >= 1 && end <= static_cast<int>( m_blend.size() ) ) {
m_blend[ end - 1 ] = static_cast<float>( std::max( 0.0, length ) );
}
else {
ErrorLog( "Bad track: blend at end " + std::to_string( end ) + " of track \"" + m_name + "\" which has no such end" );
}
}
else if( str == "friction" ) {
// memory cell holding friction value modifiers
m_friction.first = parser->getToken<std::string>();
@@ -1546,6 +1558,24 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
if( m_sleeper_enabled && false == m_sleepers3d.active && m_sleeper_local_transforms.empty() ) {
build_sleeper_transforms();
}
// the rails and the ballast blended into those of the tracks joined at the ends, where their textures differ. the weights go with
// the geometry as it's made, so the materials change only with it; not where the chunks are laid again as a movable nose moves
track_blend::blending railsblend;
track_blend::blending ballastblend;
if( Bank != null_handle && false == movable ) {
if( Geometry1.empty() ) {
railsblend = track_blend::setup( *this, track_blend::kind::rails );
m_blendmaterial1 = railsblend.material;
}
if( eType == tt_Switch ) {
ballastblend = track_blend::setup( *this, track_blend::kind::ballast );
m_blendmaterial3 = ballastblend.material;
}
else if( Geometry2.empty() ) {
ballastblend = track_blend::setup( *this, track_blend::kind::ballast );
m_blendmaterial2 = ballastblend.material;
}
}
switch (iCategoryFlag & 15)
{
case 1: // tor
@@ -1583,6 +1613,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
gfx::vertex_array vertices;
Segment->RenderLoft(vertices, m_origin, bpts1, iTrapezoid > 0, texturelength);
if( Bank != 0 && true == Geometry2.empty() ) {
track_blend::weighing const weighing { &ballastblend };
Geometry2.emplace_back(track_rails::insert_chunk(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP));
}
if( Bank == 0 && false == Geometry2.empty() ) {
@@ -1597,6 +1628,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
gfx::vertex_array vertices;
auto const open { eType == tt_Normal && ( trPrev == nullptr || trNext == nullptr ) };
if( Bank != 0 && true == Geometry1.empty() ) {
track_blend::weighing const weighing { &railsblend };
for( auto const side : { 1, -1 } ) {
auto const &profile { side > 0 ? rpts1 : rpts2 };
if( eType == tt_Normal && iTrapezoid == 0 && lay_rail( 0, profile, side, texturelength, track_rails::blade_runon( *this, side ), trPrev == nullptr, trNext == nullptr, Geometry1, GeometryCut1 ) ) {
@@ -1695,6 +1727,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
if( true == Global.CreateSwitchTrackbeds ) {
gfx::vertex_array vertices;
create_switch_trackbed( vertices );
track_blend::weighing const weighing { &ballastblend };
SwitchExtension->Geometry3 = track_rails::insert_chunk(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP);
vertices.clear();
}
@@ -2715,6 +2748,11 @@ TTrack::export_as_text_( std::ostream &Output ) const {
<< m_timber_model_name << ' '
<< ( m_timber_skin_name.empty() ? std::string{ "none" } : m_timber_skin_name ) << ' ';
}
for( std::size_t end = 0; end < m_blend.size(); ++end ) {
if( m_blend[ end ] != track_blend::default_length ) {
Output << "blend " << end + 1 << ' ' << m_blend[ end ] << ' ';
}
}
// footer
Output
<< "endtrack"

View File

@@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/.
#include "world/Segment.h"
#include "world/RailLayout.h"
#include "world/TrackSleepers.h"
#include "world/TrackBlend.h"
#include "model/material.h"
#include "scene/scenenode.h"
#include "utilities/Names.h"
@@ -237,6 +238,12 @@ public:
// timbermodel <model> <skin>
std::string m_timber_model_name;
std::string m_timber_skin_name; // replacable skin, empty for the textures of the model
// how far from its ends p1, p2 (and p4 of a switch) the rails and the ballast blend into those of the tracks joined there (blend)
std::array<float, 3> m_blend { track_blend::default_length, track_blend::default_length, track_blend::default_length };
// the materials the blending is drawn with, null where nothing's blended: the rails of a plain track, its ballast, the trackbed of a switch
material_handle m_blendmaterial1 { null_handle };
material_handle m_blendmaterial2 { null_handle };
material_handle m_blendmaterial3 { null_handle };
TModel3d *m_sleeper_model { nullptr }; // resolved on init; nullptr means no model / failed to load
material_handle m_sleeper_skin { 0 }; // resolved replacable skin handle, 0 = use model defaults
// precomputed local-space transforms (relative to m_origin) for every sleeper instance along the path.

258
world/TrackBlend.cpp Normal file
View File

@@ -0,0 +1,258 @@
/*
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 "world/TrackBlend.h"
#include "world/Track.h"
#include "world/TrackSleepers.h"
#include "rendering/renderer.h"
#include "utilities/Globals.h"
#include <algorithm>
#include <limits>
namespace track_blend {
namespace {
// how far aside of the line of an end its weight reaches, past the widest trackbeds
double constexpr lateral_reach { 6.0 };
blending const *current { nullptr };
int end_count( TTrack const &Track ) {
switch( Track.eType ) {
case tt_Normal: { return 2; }
case tt_Switch: { return ( Track.SwitchExtension != nullptr ? 3 : 0 ); }
default: { return 0; }
}
}
// the segment an end of the track is the start (end 0) or the end of: p1, p2 of the first path, p4 of the second of a switch
TSegment const *end_segment( TTrack const &Track, int const End ) {
if( Track.eType == tt_Switch ) {
return ( End == 2 ? Track.SwitchExtension->Segments[ 1 ].get() : Track.SwitchExtension->Segments[ 0 ].get() );
}
return Track.Segment.get();
}
bool end_frame( TTrack const &Track, int const End, glm::dvec3 &Point, glm::dvec3 &Inward ) {
auto const *segment { end_segment( Track, End ) };
if( segment == nullptr ) {
return false;
}
auto const length { segment->GetLength() };
if( length < 0.01 ) {
return false;
}
auto const step { std::clamp( 0.25 / length, 0.0001, 0.5 ) };
auto const atstart { End == 0 };
Point = ( atstart ? segment->FastGetPoint_0() : segment->FastGetPoint_1() );
auto const way { segment->FastGetPoint( atstart ? step : 1.0 - step ) - Point };
if( glm::length( way ) < 1e-6 ) {
return false;
}
Inward = glm::normalize( way );
return true;
}
TTrack *neighbour( TTrack const &Track, int const End ) {
if( Track.eType == tt_Switch ) {
switch( End ) {
case 0: { return Track.SwitchExtension->pPrevs[ 0 ]; }
case 1: { return Track.SwitchExtension->pNexts[ 0 ]; }
case 2: { return Track.SwitchExtension->pNexts[ 1 ]; }
default: { return nullptr; }
}
}
return ( End == 0 ? Track.trPrev : ( End == 1 ? Track.trNext : nullptr ) );
}
// the end of the track the other one is joined at, the nearest to the point of those joined to it; -1 where it isn't joined
int end_toward( TTrack const &Track, TTrack const *Other, glm::dvec3 const &Point ) {
auto result { -1 };
auto nearest { std::numeric_limits<double>::max() };
for( int end = 0; end < end_count( Track ); ++end ) {
auto joined { neighbour( Track, end ) == Other };
if( Track.eType == tt_Switch && end == 0 ) {
// the second path of a switch starts at p3, its p1 as well
joined = joined || Track.SwitchExtension->pPrevs[ 1 ] == Other;
}
glm::dvec3 point, inward;
if( false == joined || false == end_frame( Track, end, point, inward ) ) {
continue;
}
auto const distance { glm::length( point - Point ) };
if( distance < nearest ) {
nearest = distance;
result = end;
}
}
return result;
}
// the trackbed of a switch, taken from the plain tracks joined to it where it has none of its own, as create_switch_trackbed() does
material_handle switch_trackbed( TTrack const &Switch ) {
auto material { Switch.SwitchExtension->m_material3 };
if( material == null_handle && Switch.trPrev != nullptr && Switch.trPrev->eType == tt_Normal ) {
material = Switch.trPrev->m_material2;
}
if( material == null_handle && Switch.trNext != nullptr && Switch.trNext->eType == tt_Normal ) {
material = Switch.trNext->m_material2;
}
return material;
}
// the material the track draws its rails or its ballast with at the end, null where it has none there
material_handle material_of( TTrack const &Track, kind const Kind, int const End ) {
if( false == Track.m_visible || Track.m_editorremoved || ( Track.iCategoryFlag & 15 ) != 1 ) {
return null_handle;
}
if( Kind == kind::rails ) {
switch( Track.eType ) {
case tt_Normal: { return Track.m_material1; }
case tt_Switch: { return ( End == 2 ? Track.m_material2 : Track.m_material1 ); }
default: { return null_handle; }
}
}
// no ballast is drawn under the 3D sleepers laid on the ground alone
if( false == Global.Sleepers3DBed && track_sleepers::lays( Track ) ) {
return null_handle;
}
switch( Track.eType ) {
case tt_Normal: { return Track.m_material2; }
case tt_Switch: { return ( Global.CreateSwitchTrackbeds && Track.SwitchExtension != nullptr ? switch_trackbed( Track ) : null_handle ); }
default: { return null_handle; }
}
}
// whether the track blends its rails or ballast at the end into those of the track joined there, and what with. the answer is
// the same for that track back, but for the lengths and the kinds of tracks which blend: switches don't blend their rails
bool blends( TTrack const &Track, kind const Kind, int const End, TTrack const **Other = nullptr, int *Otherend = nullptr, material_handle *Othermaterial = nullptr ) {
if( End < 0 || End >= end_count( Track ) ) {
return false;
}
if( Kind == kind::rails && Track.eType != tt_Normal ) {
return false;
}
if( Track.m_blend[ End ] <= 0.f ) {
return false;
}
auto const own { material_of( Track, Kind, End ) };
if( own == null_handle ) {
return false;
}
auto const *other { neighbour( Track, End ) };
glm::dvec3 point, inward;
if( other == nullptr || other == &Track || false == end_frame( Track, End, point, inward ) ) {
return false;
}
auto const otherend { end_toward( *other, &Track, point ) };
if( otherend < 0 ) {
return false;
}
auto const theirs { material_of( *other, Kind, otherend ) };
if( theirs == null_handle || theirs == own ) {
return false;
}
if( false == GfxRenderer->Blendable_Material( own ) || false == GfxRenderer->Blendable_Material( theirs ) ) {
return false;
}
if( Other != nullptr ) { *Other = other; }
if( Otherend != nullptr ) { *Otherend = otherend; }
if( Othermaterial != nullptr ) { *Othermaterial = theirs; }
return true;
}
} // namespace
void blending::weigh( gfx::vertex_array const &Vertices, gfx::userdata_array &Userdata ) const {
Userdata.resize( Vertices.size(), gfx::vertex_userdata { glm::vec4 { 0.f } } );
for( std::size_t index = 0; index < Vertices.size(); ++index ) {
glm::dvec3 const position { Vertices[ index ].position };
auto &data { Userdata[ index ].data };
for( int end = 0; end < 3; ++end ) {
auto const &blend { ends[ end ] };
auto weight { 0.f };
if( blend.active ) {
// measured along the line of the end, so both tracks give the vertices of their joint the same weight
auto const offset { position - blend.point };
auto const along { glm::dot( offset, blend.inward ) };
auto const aside { glm::length( offset - blend.inward * along ) };
if( along < blend.length && aside < lateral_reach ) {
weight = blend.peak * static_cast<float>( 1.0 - std::max( 0.0, along ) / blend.length );
}
}
data[ 1 + end ] = weight;
}
}
}
blending setup( TTrack const &Track, kind const Kind ) {
blending result;
std::array<material_handle, 3> others { null_handle, null_handle, null_handle };
auto found { false };
for( int end = 0; end < end_count( Track ); ++end ) {
TTrack const *other { nullptr };
auto otherend { -1 };
auto theirs { null_handle };
if( false == blends( Track, Kind, end, &other, &otherend, &theirs ) ) {
continue;
}
auto &blend { result.ends[ end ] };
end_frame( Track, end, blend.point, blend.inward );
blend.point -= Track.m_origin;
blend.length = Track.m_blend[ end ];
// where both tracks blend they meet half way at the joint, otherwise this one goes all the way to the other one there
blend.peak = ( blends( *other, Kind, otherend ) ? 0.5f : 1.f );
blend.active = true;
others[ end ] = theirs;
found = true;
}
if( false == found ) {
return {};
}
auto const own { material_of( Track, Kind, 0 ) };
auto const material { GfxRenderer->Blend_Material( own, others, Kind == kind::ballast ) };
if( material == own ) {
// the renderer can't blend them
return {};
}
result.material = material;
return result;
}
weighing::weighing( blending const *Blending ) :
m_previous { current } {
current = ( Blending != nullptr && Blending->any() ? Blending : nullptr );
}
weighing::~weighing() {
current = m_previous;
}
blending const *weighed() {
return current;
}
} // namespace track_blend

70
world/TrackBlend.h Normal file
View File

@@ -0,0 +1,70 @@
/*
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 <array>
#include <glm/glm.hpp>
#include "utilities/Classes.h"
#include "interfaces/ITexture.h"
#include "rendering/geometrybank.h"
class TTrack;
// the rails and the ballast of a track blended into those of the tracks joined at its ends, where their textures differ: near
// an end the vertices carry the weight of the track joined there (y, z, w of their user data for the ends p1, p2 and p4 of a
// switch) and the track is drawn with a material blending its texture with theirs. the ballast comes in as grains, the rails
// fade into each other. a plain track blends its rails and its ballast, a switch the trackbed made for it
namespace track_blend {
// how far from an end the textures blend, by default
float constexpr default_length { 3.f };
enum class kind {
rails,
ballast
};
struct blending {
struct end {
bool active { false };
glm::dvec3 point { 0.0 }; // relative to the origin of the track
glm::dvec3 inward { 0.0 }; // unit vector from the end into the track
double length { 0.0 };
float peak { 0.f }; // the weight at the end: a half where the track joined there blends back, whole where it doesn't
};
std::array<end, 3> ends;
material_handle material { null_handle }; // the material the blending is drawn with, null where nothing's blended
bool any() const {
return material != null_handle; }
// the weights of the ends put in the user data of the vertices (relative to the origin of the track), made as long as the
// vertices where it's shorter; what the user data hold at x is kept
void weigh( gfx::vertex_array const &Vertices, gfx::userdata_array &Userdata ) const;
};
// how the rails or the ballast of the track blend into those of the tracks joined to it, nothing for the tracks which don't
blending setup( TTrack const &Track, kind const Kind );
// while one is alive, the chunks of geometry inserted with track_rails::insert_chunk() get the weights of the blending
class weighing {
public:
explicit weighing( blending const *Blending );
~weighing();
weighing( weighing const & ) = delete;
weighing &operator=( weighing const & ) = delete;
private:
blending const *m_previous { nullptr };
};
// the blending the chunks inserted now are weighed by, null when they aren't
blending const *weighed();
} // namespace track_blend