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

feat(scenery): models laid along curves (sweep nodes), bent to follow the paths or repeated along them, loaded and saved with the scenery; trainsets put on a path while the simulation runs; trainset turned around by reversed after its header: the first vehicle stays at the offset and leads towards point 1, the rest stands behind it; launcher keeps reversed trainsets and can turn one around; memory cells saved with the new name of the path they name; events tell their targets and the events of a multiple event; renderer tells whether the cursor is over a surface; vehicle placed in the editor entered when the driving goes on

This commit is contained in:
maj00r
2026-10-06 22:19:32 +02:00
parent 084a705452
commit 978b99b7e5
21 changed files with 1329 additions and 15 deletions

View File

@@ -96,6 +96,7 @@ set(SOURCES
"utilities/Timer.cpp"
"world/Track.cpp"
"world/Road.cpp"
"world/Sweep.cpp"
"world/RoadPoint.cpp"
"world/Traction.cpp"
"world/TractionPower.cpp"

View File

@@ -255,6 +255,19 @@ bool driver_mode::update()
// variable step simulation time routines
if (false == Global.editor_enter_vehicle.empty())
{
auto const name{std::move(Global.editor_enter_vehicle)};
Global.editor_enter_vehicle.clear();
if (auto *dynamic{simulation::Vehicles.find(name)})
{
if (false == FreeFlyModeFlag)
InOutKey();
m_relay.post(user_command::entervehicle, 0.0, simulation::Train ? simulation::Train->id() : 0, GLFW_PRESS, 0, dynamic->GetPosition(), &dynamic->name());
change_train = dynamic->name();
}
}
if (!change_train.empty())
{
TTrain *train = simulation::Trains.find(change_train);

View File

@@ -119,6 +119,10 @@ void ui::scenerylist_panel::draw_launch_box()
ImGui::TextWrapped(selected_trainset->description.c_str());
ImGui::Checkbox(STR_C("Trainset turned around"), &selected_trainset->reversed);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The first vehicle stays where the trainset starts and leads the other way,\nthe rest of the trainset stands behind it"));
if (ImGui::Button(STR_C("Launch"), ImVec2(-1, 0))) {
if (!launch_simulation())
ImGui::OpenPopup("missing_driver");
@@ -163,7 +167,7 @@ void ui::scenerylist_panel::add_replace_entry(const trainset_desc &trainset)
set += trainset.name + " ";
set += trainset.track + " ";
set += std::to_string(trainset.offset) + " ";
set += std::to_string(trainset.velocity) + "\n";
set += std::to_string(trainset.velocity) + (trainset.reversed ? " reversed\n" : "\n");
for (const auto &veh : trainset.vehicles) {
if (!veh.skin) {
ErrorLog("trainset contains invalid vehicle " + veh.name);

View File

@@ -95,6 +95,10 @@ void scenery_scanner::parse_trainset(cParser &parser)
parser >> trainset.name >> trainset.track >> trainset.offset >> trainset.velocity;
parser.getTokens();
if (parser.peek() == "reversed") {
trainset.reversed = true;
parser.getTokens();
}
while (parser.peek() == "node") {
trainset.vehicles.emplace_back();
dynamic_desc &dyn = trainset.vehicles.back();

View File

@@ -30,6 +30,7 @@ struct trainset_desc {
float offset { 0.f };
float velocity { 0.f };
bool reversed { false };
std::vector<dynamic_desc> vehicles;
};

View File

@@ -5154,6 +5154,7 @@ glm::dvec3 opengl33_renderer::Update_Mouse_Position()
if (pointdepth != std::numeric_limits<float>::max())
{
m_mousehit = (GLAD_GL_ARB_clip_control || GLAD_GL_EXT_clip_control) ? pointdepth > 0.0f : pointdepth < 1.0f;
if (GLAD_GL_ARB_clip_control || GLAD_GL_EXT_clip_control) {
if (pointdepth > 0.0f)
m_worldmousecoordinates = glm::unProjectZO(glm::vec3(bufferpos, pointdepth), glm::mat4(glm::mat3(m_colorpass.pass_camera.modelview())), m_colorpass.pass_camera.projection(),

View File

@@ -105,6 +105,8 @@ class opengl33_renderer : public gfx_renderer {
Pick_Node() const override { return m_picksceneryitem; }
glm::dvec3
Mouse_Position() const override { return m_worldmousecoordinates; }
bool
Mouse_Hit() const override { return m_mousehit; }
glm::mat4
Camera_View_Matrix() const override { return glm::mat4( glm::mat3( m_colorpass.pass_camera.modelview() ) ); }
glm::mat4
@@ -387,6 +389,7 @@ class opengl33_renderer : public gfx_renderer {
std::vector<scene::basic_node *> m_picksceneryitems;
scene::basic_node *m_picksceneryitem{nullptr};
glm::vec3 m_worldmousecoordinates { 0.f };
bool m_mousehit { false };
#ifdef EU07_USE_DEBUG_CAMERA
renderpass_config m_worldcamera; // debug item
#endif

View File

@@ -73,6 +73,7 @@ public:
virtual auto Pick_Node() const -> scene::basic_node const * = 0;
virtual auto Mouse_Position() const -> glm::dvec3 = 0;
virtual auto Mouse_Hit() const -> bool { return true; }
// editor helpers: matrices/position of the most recent color pass camera.
// the view matrix is camera-relative (rotation only, camera at origin), matching the
// camera-relative rendering used by the engine; build object matrices relative to Camera_Position().

View File

@@ -69,7 +69,9 @@ struct scratch_data {
std::vector<TDynamicObject *> vehicles;
std::vector<int> couplings;
TDynamicObject * driver { nullptr };
TTrack * path { nullptr }; // given directly, for a trainset placed in the editor on a path which may have no name
bool is_open { false };
bool reversed { false };
std::unordered_map<std::string, std::string> assignment;
} trainset;

View File

@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
#include "scene/scenelayers.h"
#include "world/Track.h"
#include "world/Road.h"
#include "world/Sweep.h"
#include "world/RoadPoint.h"
#include "simulation/simulation.h"
@@ -231,7 +232,7 @@ bool patch_model(std::string &Text, model_placement const &Placement, std::strin
}
// definition layout: node <max> <min> <name> memcell <x> <y> <z> <text> <value 1> <value 2> <track> endmemcell
bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &Error)
bool patch_memcell(std::string &Text, std::optional<glm::dvec3> const &Location, std::string const &Track, std::string &Error)
{
auto const tokens{tokenize(Text)};
if (tokens.size() < 13 || tokens[0].text != "node" || tokens[4].text != "memcell" || tokens.back().text != "endmemcell")
@@ -239,7 +240,13 @@ bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &E
Error = "unexpected layout of memory cell definition";
return false;
}
apply(Text, {{tokens[5].begin, tokens[5].end, number(Location.x)}, {tokens[6].begin, tokens[6].end, number(Location.y)}, {tokens[7].begin, tokens[7].end, number(Location.z)}});
std::vector<text_change> changes;
if (Location)
changes.insert(changes.end(), {{tokens[5].begin, tokens[5].end, number(Location->x)}, {tokens[6].begin, tokens[6].end, number(Location->y)}, {tokens[7].begin, tokens[7].end, number(Location->z)}});
auto const &track{tokens[tokens.size() - 2]};
if (false == Track.empty() && track.text != Track)
changes.push_back({track.begin, track.end, Track});
apply(Text, changes);
return true;
}
@@ -699,7 +706,9 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
auto const rotated{false == glm::all(glm::epsilonEqual(angles, source.angles, 1e-3f))};
auto const scaled{false == glm::all(glm::epsilonEqual(scale, source.scale, 1e-4f))};
auto const renamed{Model != nullptr && m_renamed.find(Node) != m_renamed.end()};
if (false == (moved || rotated || scaled || renamed))
auto const *cell{Model == nullptr ? static_cast<TMemCell const *>(Node) : nullptr};
auto const track{cell != nullptr && cell->Track != nullptr ? cell->Track->name() : std::string{}};
if (false == (moved || rotated || scaled || renamed || (false == track.empty() && Node->dirty())))
{
return true;
}
@@ -739,13 +748,17 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
}
else
{
patched = patch_memcell(text, source.context.to_local(location), error);
patched = patch_memcell(text, moved ? std::optional<glm::dvec3>{source.context.to_local(location)} : std::nullopt, track, error);
}
if (false == patched)
{
state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\"";
return false;
}
if (text.size() == static_cast<std::size_t>(source.span.end - source.span.begin) && content.compare(static_cast<std::size_t>(source.span.begin), text.size(), text) == 0)
{
return true;
}
file_patch::edit edit;
edit.begin = source.span.begin;
edit.end = source.span.end;
@@ -988,6 +1001,72 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
state.patches[source.layer].edits.emplace_back(std::move(edit));
rewritten.emplace(road);
}
// models laid along curves, likewise
std::vector<sweep_node *> savedsweeps;
std::vector<sweep_node const *> droppedsweeps;
auto const sweep_text = [](sweep_node &Sweep, glm::dvec3 const &Offset) {
auto const definition{Sweep.definition()};
auto local{definition};
for (auto &piece : local.pieces)
{
piece.points[segment_data::point::start] -= Offset;
piece.points[segment_data::point::end] -= Offset;
}
Sweep.define(local);
std::string text;
Sweep.export_as_text(text);
Sweep.define(definition);
text.erase(text.find_last_not_of(" \t\r\n") + 1);
return text;
};
for (auto *sweep : simulation::Sweeps.sequence())
{
if (sweep == nullptr || sweep->from_template())
{
continue;
}
auto const nodelayer{resolve(sweep->layer())};
if (false == is_output(nodelayer))
{
continue;
}
auto const lookup{m_sources.find(sweep)};
if (lookup == m_sources.end())
{
if (sweep->m_editorremoved || false == sweep->dirty())
{
continue;
}
created[nodelayer].emplace_back(sweep, sweep_text(*sweep, layer(nodelayer).context_insert().offset));
savedsweeps.push_back(sweep);
continue;
}
auto const &source{lookup->second};
if ((false == sweep->m_editorremoved && false == sweep->dirty()) || false == writable(source.layer))
{
continue;
}
if (false == load(source.layer))
{
return fail(state.error);
}
file_patch::edit edit;
edit.begin = source.span.begin;
edit.end = source.span.end;
if (sweep->m_editorremoved)
{
droppedsweeps.push_back(sweep);
}
else
{
edit.text = sweep_text(*sweep, source.context.offset);
edit.length = edit.text.size();
edit.node = sweep;
savedsweeps.push_back(sweep);
}
state.patches[source.layer].edits.emplace_back(std::move(edit));
rewritten.emplace(sweep);
}
// road junctions, likewise
std::vector<junction_node *> savedjunctions;
std::vector<junction_node const *> droppedjunctions;
@@ -1647,6 +1726,14 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
{
m_sources.erase(road);
}
for (auto *sweep : savedsweeps)
{
sweep->m_dirty = false;
}
for (auto const *sweep : droppedsweeps)
{
m_sources.erase(sweep);
}
for (auto *junction : savedjunctions)
{
junction->m_dirty = false;

View File

@@ -149,6 +149,9 @@ public:
// set lighting (used by generated geometry painted with plain colour)
void
lighting( lighting_data const &Lighting );
// sets whether the shape is drawn with the translucent geometry (used by generated geometry)
void
translucent( bool const Translucent );
// data access
shapenode_data const &
data() const;
@@ -186,6 +189,11 @@ void
shape_node::lighting( lighting_data const &Lighting ) {
m_data.lighting = Lighting;
}
inline
void
shape_node::translucent( bool const Translucent ) {
m_data.translucent = Translucent;
}
// data access
inline
shape_node::shapenode_data const &

View File

@@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/.
#include "world/MemCell.h"
#include "world/Track.h"
#include "world/Road.h"
#include "world/Sweep.h"
#include "world/RoadPoint.h"
#include "world/Traction.h"
#include "world/TractionPower.h"
@@ -39,6 +40,7 @@ event_manager Events;
memory_table Memory;
path_table Paths;
road_table Roads;
sweep_table Sweeps;
junction_table Junctions;
roadpoint_table Roadpoints;
traction_table Traction;
@@ -497,6 +499,10 @@ TAnimModel * state_manager::create_model(const std::string &src, const std::stri
return m_serializer.create_model(src, name, position);
}
std::vector<TDynamicObject *> state_manager::insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed) {
return m_serializer.insert_trainset(Name, Path, Offset, Vehicles, Reversed);
}
TEventLauncher * state_manager::create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position) {
return m_serializer.create_eventlauncher(src, name, position);
}

View File

@@ -40,6 +40,9 @@ public:
// create model from node string
TAnimModel *
create_model(const std::string &src, const std::string &name, const glm::dvec3 &position);
// places the vehicles, given as their node statements, as a trainset on the path. returns: the vehicles made
std::vector<TDynamicObject *>
insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed = false);
// create eventlauncher from node string
TEventLauncher *
create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position);
@@ -86,6 +89,7 @@ extern event_manager Events;
extern memory_table Memory;
extern path_table Paths;
extern road_table Roads;
extern sweep_table Sweeps;
extern junction_table Junctions;
extern roadpoint_table Roadpoints;
extern traction_table Traction;

View File

@@ -28,6 +28,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/lightarray.h"
#include "world/TractionPower.h"
#include "world/Road.h"
#include "world/Sweep.h"
#include "world/RoadPoint.h"
#include "application/application.h"
#include "rendering/renderer.h"
@@ -35,8 +36,19 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/utilities.h"
#include "editor/editorTerrainStreamer.hpp"
namespace simulation {
namespace {
double vehicle_length( std::string const &Folder, std::string const &Type ) {
TMoverParameters probe( 0.0, Type, Type, 0 );
return probe.LoadFIZ( paths::dynamic + Folder + "/" ) ? probe.Dim.L : 0.0;
}
} // namespace
std::shared_ptr<deserializer_state>
state_serializer::deserialize_begin( std::string const &Scenariofile ) {
@@ -123,7 +135,8 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) {
{ "trainset", &state_serializer::deserialize_trainset },
{ "terrain", &state_serializer::deserialize_terrain },
{ "editorterrain", &state_serializer::deserialize_editorterrain },
{ "endtrainset", &state_serializer::deserialize_endtrainset } };
{ "endtrainset", &state_serializer::deserialize_endtrainset },
{ "reversed", &state_serializer::deserialize_reversed } };
for( auto &function : functionlist ) {
state->functionmap.emplace( function.first, std::bind( function.second, this, std::ref( state->input ), std::ref( state->scratchpad ) ) );
@@ -191,6 +204,7 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
simulation::Roads.create_geometry( Scratchpad );
simulation::Junctions.create_geometry( Scratchpad );
simulation::Roadpoints.create_geometry();
simulation::Sweeps.create_geometry( Scratchpad );
return false;
}
@@ -554,6 +568,20 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
road->create_lanes();
scene::Layers.track( road, { sourcebegin, Input.TokenEnd() } );
}
else if( nodedata.type == "sweep" ) {
auto *sweep { new sweep_node( nodedata ) };
sweep->import(
Input,
( Scratchpad.location.offset.empty() ?
glm::dvec3 { 0.0 } :
glm::dvec3 { Scratchpad.location.offset.top() } ) );
if( false == simulation::Sweeps.insert( sweep ) ) {
ErrorLog( "Bad scenario: duplicate sweep name \"" + sweep->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" );
}
scene::Groups.insert( scene::Groups.handle(), sweep );
scene::Layers.track( sweep, { sourcebegin, Input.TokenEnd() } );
}
else if( nodedata.type == "junction" ) {
auto *junction { new junction_node( nodedata ) };
@@ -1224,6 +1252,17 @@ include_terrain_file( std::string File, std::string &Includes, scene::scratch_da
} // namespace
void
state_serializer::deserialize_reversed( cParser &Input, scene::scratch_data &Scratchpad ) {
if( false == Scratchpad.trainset.is_open
|| false == Scratchpad.trainset.vehicles.empty() ) {
ErrorLog( "Bad trainset: \"reversed\" has to follow the trainset header, ahead of its vehicles, in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" );
return;
}
Scratchpad.trainset.reversed = true;
}
void
state_serializer::deserialize_terrain(cParser &Input, scene::scratch_data &Scratchpad)
{
@@ -1312,9 +1351,16 @@ state_serializer::deserialize_endtrainset( cParser &Input, scene::scratch_data &
}
if( vehicleindex > 0 ) {
// from second vehicle on couple it with the previous one
Scratchpad.trainset.vehicles[ vehicleindex - 1 ]->AttachNext(
vehicle,
Scratchpad.trainset.couplings[ vehicleindex - 1 ] );
if( Scratchpad.trainset.reversed ) {
vehicle->AttachNext(
Scratchpad.trainset.vehicles[ vehicleindex - 1 ],
Scratchpad.trainset.couplings[ vehicleindex - 1 ] );
}
else {
Scratchpad.trainset.vehicles[ vehicleindex - 1 ]->AttachNext(
vehicle,
Scratchpad.trainset.couplings[ vehicleindex - 1 ] );
}
}
++vehicleindex;
}
@@ -1494,7 +1540,7 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra
loadtype = "";
}
auto *path = simulation::Paths.find( pathname );
auto *path = Scratchpad.trainset.path != nullptr ? Scratchpad.trainset.path : simulation::Paths.find( pathname );
if( path == nullptr ) {
ErrorLog( "Bad scenario: vehicle \"" + Nodedata.name + "\" placed on nonexistent path \"" + pathname + "\" in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" );
@@ -1502,7 +1548,9 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra
return nullptr;
}
if( true == Scratchpad.trainset.vehicles.empty() // jeśli pierwszy pojazd,
auto const reversedset { Scratchpad.trainset.is_open && Scratchpad.trainset.reversed };
if( false == reversedset
&& true == Scratchpad.trainset.vehicles.empty() // jeśli pierwszy pojazd,
&& false == path->m_events0.empty() // tor ma Event0
&& std::abs(velocity) <= 1.f // a skład stoi
&& Scratchpad.trainset.offset >= 0.0 // ale może nie sięgać na owy tor
@@ -1512,23 +1560,34 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra
}
auto *vehicle = new TDynamicObject();
auto const gap { offset == -1.0 ? 0.0 : offset };
auto const turned { ( offset == -1.0 ) != reversedset };
auto const expected { reversedset ? vehicle_length( datafolder, mmdfile ) : 0.0 };
auto const length =
vehicle->Init(
Nodedata.name,
datafolder, skinfile, mmdfile,
path,
offset == -1.0 ? Scratchpad.trainset.offset : Scratchpad.trainset.offset - offset,
reversedset ? Scratchpad.trainset.offset + gap + expected : Scratchpad.trainset.offset - gap,
drivertype,
velocity,
Scratchpad.trainset.name,
loadcount, loadtype,
offset == -1.0,
turned,
params );
if( length != 0.0 && reversedset ) {
if( std::abs( length - expected ) > 0.01 ) {
vehicle->place_on_track( path, Scratchpad.trainset.offset + gap + length, turned );
}
Scratchpad.trainset.offset += length;
}
if( length != 0.0 ) { // zero oznacza błąd
// przesunięcie dla kolejnego, minus bo idziemy w stronę punktu 1
Scratchpad.trainset.offset -= length;
if( false == reversedset ) {
Scratchpad.trainset.offset -= length;
}
// automatically establish permanent connections for couplers which specify them in their definitions
if( coupling != 0
&& vehicle->MoverParameters->Couplers[(offset == -1.0 ? end::front : end::rear)].AllowedFlag & coupling::permanent ) {
@@ -1774,6 +1833,11 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons
point->export_as_text( scmfile );
}
}
for( auto const *sweep : Sweeps.sequence() ) {
if( sweep != nullptr && false == sweep->m_editorremoved && sweep->dirty() == Dirty && sweep->group() == null_handle ) {
sweep->export_as_text( scmfile );
}
}
// traction
scmfile << "// traction\n";
for( auto const *traction : Traction.sequence() ) {
@@ -1826,6 +1890,25 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st
return cloned;
}
std::vector<TDynamicObject *> state_serializer::insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed) {
scene::scratch_data scratch;
scratch.trainset.is_open = true;
scratch.trainset.name = Name;
scratch.trainset.track = Path != nullptr ? Path->name() : std::string{};
scratch.trainset.path = Path;
scratch.trainset.offset = static_cast<float>(Offset);
scratch.trainset.reversed = Reversed;
cParser parser(Vehicles, cParser::buffer_TEXT, Global.asCurrentSceneryPath, Global.bLoadTraction);
auto token { parser.getToken<std::string>() };
while (false == token.empty()) {
if (token == "node") { deserialize_node(parser, scratch); }
token = parser.getToken<std::string>();
}
auto const vehicles { scratch.trainset.vehicles };
if (false == vehicles.empty()) { deserialize_endtrainset(parser, scratch); }
return vehicles;
}
std::pair<int, int> state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) {
// statements which take more than a single token, with the tokens ending them
static std::unordered_map<std::string, std::string> const nodeends {

View File

@@ -44,6 +44,8 @@ public:
export_as_text( std::string const &Scenariofile ) const;
// create new model from node stirng
TAnimModel * create_model(std::string const &src, std::string const &name, const glm::dvec3 &position);
// places the vehicles, given as their node statements, as a trainset on the path, while the simulation runs. returns: the vehicles made
std::vector<TDynamicObject *> insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed = false);
// create new eventlauncher from node stirng
TEventLauncher * create_eventlauncher(std::string const &src, std::string const &name, const glm::dvec3 &position);
// creates model instances defined by an include directive placed or changed in the scenery editor, to show the outcome.
@@ -80,6 +82,7 @@ private:
void deserialize_terrain( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_editorterrain( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_endtrainset( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_reversed( cParser &Input, scene::scratch_data &Scratchpad );
TTrack * deserialize_path( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata );
TTraction * deserialize_traction( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata );
TTractionPowerSource * deserialize_tractionpowersource( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata );

View File

@@ -45,6 +45,8 @@ struct light_array;
class particle_manager;
class road_node; // kawałek drogi, z którego powstają pasy ruchu
class road_table;
class sweep_node; // model ułożony wzdłuż krzywej: wygięty albo powtarzany
class sweep_table;
class junction_node;
class junction_table;
class roadpoint_node; // przejazd kolejowy albo miejsce, w którym pojazdy drogowe się pojawiają lub znikają

View File

@@ -84,6 +84,7 @@ struct global_settings {
std::string szDefaultExt{ szTexturesDDS };
std::string SceneryFile;
std::string local_start_vehicle{ "EU07-424" };
std::string editor_enter_vehicle; // vehicle placed in the editor, to be entered when the driving goes on
bool editor_session{ false }; // scenery was opened for editing (-edit): loader keeps track of scenery layers and hands over to the editor
int iConvertModels{ 0 }; // tworzenie plików binarnych
int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan

View File

@@ -1243,6 +1243,26 @@ multi_event::init() {
}
}
std::vector<basic_event *> multi_event::children() const {
std::vector<basic_event *> result;
for (auto const &childevent : m_children) {
if (std::get<basic_event *>(childevent) != nullptr) {
result.push_back(std::get<basic_event *>(childevent));
}
}
return result;
}
std::vector<scene::basic_node *> basic_event::target_nodes() const {
std::vector<scene::basic_node *> result;
for (auto const &target : m_targets) {
if (std::get<scene::basic_node *>(target) != nullptr) {
result.push_back(std::get<scene::basic_node *>(target));
}
}
return result;
}
std::vector<std::string> multi_event::dump_children_names() const {
std::vector<std::string> result;
for (auto const &childevent : m_children) {

View File

@@ -47,6 +47,7 @@ public:
// destructor
virtual ~basic_event();
// methods
std::vector<scene::basic_node *> target_nodes() const;
// restores event data from provided stream
virtual
void
@@ -334,6 +335,7 @@ public:
void init() override;
std::vector<std::string> dump_children_names() const;
std::vector<basic_event *> children() const;
private:
// types
@@ -699,6 +701,9 @@ public:
// returns all eventlaunchers in radius ignoring height
std::vector<TEventLauncher *>
find_eventlaunchers(glm::vec2 center, float radius) const;
std::deque<basic_event *> const &
sequence() const {
return m_events; }
// all event launchers, global ones included
std::vector<TEventLauncher *>
launchers() const {

907
world/Sweep.cpp Normal file
View File

@@ -0,0 +1,907 @@
/*
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/Sweep.h"
#include "editor/editorGauge.hpp"
#include "model/MdlMngr.h"
#include "model/Model3d.h"
#include "rendering/renderer.h"
#include "scene/scene.h"
#include "simulation/simulation.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "utilities/parser.h"
#include "vehicle/DynObj.h"
#include <glm/gtx/rotate_vector.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>
#include <cmath>
#include <functional>
namespace
{
double const sample_spacing{0.5}; // m along the curve between the samples
std::size_t const vertex_limit{4000000}; // copies beyond this many vertices are left out
double const slice_width{1.0}; // m of the model, the faces of a bent copy are cut into
double const edge_band{0.3}; // m from the side facing the curve, where the top of the edge is looked for
struct cubic
{
glm::dvec3 p0, p1, p2, p3;
explicit cubic(segment_data const &Piece)
{
p0 = Piece.points[segment_data::point::start];
p3 = Piece.points[segment_data::point::end];
auto const &control1{Piece.points[segment_data::point::control1]};
auto const &control2{Piece.points[segment_data::point::control2]};
if (control1 == glm::dvec3{} && control2 == glm::dvec3{})
{
p1 = glm::mix(p0, p3, 1.0 / 3.0);
p2 = glm::mix(p0, p3, 2.0 / 3.0);
}
else
{
p1 = p0 + control1;
p2 = p3 + control2;
}
}
glm::dvec3 point(double const T) const
{
auto const u{1.0 - T};
return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3;
}
glm::dvec3 first(double const T) const
{
auto const u{1.0 - T};
return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2);
}
};
double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B)
{
return glm::length(glm::dvec2{B.x - A.x, B.z - A.z});
}
// triangles of the model which share the look, in the space of the model
struct model_part
{
material_handle material{null_handle};
lighting_data lighting;
bool translucent{false};
std::vector<world_vertex> vertices;
};
void gather_parts(TSubModel *Submodel, glm::dmat4 const &Transform, material_data const &Skins, std::vector<model_part> &Parts)
{
for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->Next)
{
auto transform{Transform};
if ((submodel->iFlags & 0xC000) != 0 && submodel->GetMatrix() != nullptr)
{
transform = Transform * glm::dmat4(glm::make_mat4(submodel->GetMatrix()->readArray()));
}
// the nearest level of detail only, the others would be drawn over it
if (submodel->eType == GL_TRIANGLES && submodel->fSquareMinDist <= 0.f && (submodel->m_geometry.handle.bank != 0 || submodel->m_geometry.handle.chunk != 0))
{
auto material{submodel->m_material};
if (material < 0)
{
material = Skins.replacable_skins[std::clamp(-material, 1, 4)];
}
model_part part;
part.material = material;
part.lighting.ambient = submodel->f4Ambient;
part.lighting.diffuse = submodel->f4Diffuse;
part.lighting.specular = submodel->f4Specular;
part.translucent = material != null_handle && GfxRenderer->Material(material)->get_or_guess_opacity() == 0.0f;
auto const normals{glm::dmat3(transform)};
auto const &vertices{GfxRenderer->Vertices(submodel->m_geometry.handle)};
auto const &indices{GfxRenderer->Indices(submodel->m_geometry.handle)};
auto const take = [&](gfx::basic_vertex const &Vertex) {
auto vertex{Vertex.to_world()};
vertex.position = glm::dvec3(transform * glm::dvec4(vertex.position, 1.0));
auto const normal{normals * glm::dvec3(vertex.normal)};
vertex.normal = glm::length(normal) > 1e-9 ? glm::vec3(glm::normalize(normal)) : vertex.normal;
part.vertices.push_back(vertex);
};
if (false == indices.empty())
{
for (std::size_t i = 0; i + 2 < indices.size(); i += 3)
{
take(vertices[indices[i]]);
take(vertices[indices[i + 1]]);
take(vertices[indices[i + 2]]);
}
}
else
{
for (std::size_t i = 0; i + 2 < vertices.size(); i += 3)
{
take(vertices[i]);
take(vertices[i + 1]);
take(vertices[i + 2]);
}
}
if (false == part.vertices.empty())
{
Parts.push_back(std::move(part));
}
}
if (submodel->Child != nullptr)
{
gather_parts(submodel->Child, transform, Skins, Parts);
}
}
}
world_vertex blend(world_vertex const &A, world_vertex const &B, double const T)
{
world_vertex result;
result.position = glm::mix(A.position, B.position, T);
auto const normal{glm::mix(A.normal, B.normal, static_cast<float>(T))};
result.normal = glm::length(normal) > 1e-6f ? glm::normalize(normal) : A.normal;
result.texture = glm::mix(A.texture, B.texture, static_cast<float>(T));
return result;
}
// the part of the polygon on one side of the plane Along = Cut: Side 1 keeps what's beyond it, -1 what's before it
std::vector<world_vertex> clip(std::vector<world_vertex> const &Polygon, std::function<double(glm::dvec3 const &)> const &Along, double const Cut, double const Side)
{
std::vector<world_vertex> result;
for (std::size_t i = 0; i < Polygon.size(); ++i)
{
auto const &current{Polygon[i]};
auto const &next{Polygon[(i + 1) % Polygon.size()]};
auto const a{Side * (Along(current.position) - Cut)};
auto const b{Side * (Along(next.position) - Cut)};
if (a >= 0.0)
result.push_back(current);
if ((a >= 0.0) != (b >= 0.0))
result.push_back(blend(current, next, a / (a - b)));
}
return result;
}
// triangles cut across the axis of the model into slices, so a long face bends with the curve instead of spanning it straight
std::vector<world_vertex> slices(std::vector<world_vertex> const &Vertices, std::function<double(glm::dvec3 const &)> const &Along, double const Low, double const Width)
{
std::vector<world_vertex> result;
result.reserve(Vertices.size());
for (std::size_t i = 0; i + 2 < Vertices.size(); i += 3)
{
auto const a0{Along(Vertices[i].position)};
auto const a1{Along(Vertices[i + 1].position)};
auto const a2{Along(Vertices[i + 2].position)};
auto const first{static_cast<int>(std::floor((std::min({a0, a1, a2}) - Low) / Width))};
auto const last{static_cast<int>(std::ceil((std::max({a0, a1, a2}) - Low) / Width)) - 1};
if (last <= first)
{
result.insert(result.end(), Vertices.begin() + i, Vertices.begin() + i + 3);
continue;
}
std::vector<world_vertex> const triangle{Vertices[i], Vertices[i + 1], Vertices[i + 2]};
for (int slice = first; slice <= last; ++slice)
{
auto const polygon{clip(clip(triangle, Along, Low + slice * Width, 1.0), Along, Low + (slice + 1) * Width, -1.0)};
for (std::size_t k = 1; k + 1 < polygon.size(); ++k)
{
result.push_back(polygon[0]);
result.push_back(polygon[k]);
result.push_back(polygon[k + 1]);
}
}
}
return result;
}
} // namespace
// height of the head of the rail on the side of the offset over the points of the track, less the height of the rail; 0 for no side
double sweep_node::rail_head(double const Roll, double const Lateral)
{
auto const roll{glm::radians(Roll)};
auto const raise{Global.bRollFix ? 0.75 * std::abs(std::sin(roll)) : 0.0};
if (Lateral == 0.0)
return raise;
return raise - (Lateral > 0.0 ? 1.0 : -1.0) * 0.75 * std::sin(roll);
}
bool sweep_node::state::operator==(state const &Other) const
{
if (model != Other.model || skin != Other.skin || bend != Other.bend || step != Other.step || from != Other.from || to != Other.to || lateral != Other.lateral || height != Other.height || along_x != Other.along_x ||
flip != Other.flip || mirror != Other.mirror || tilt != Other.tilt || face != Other.face || widen != Other.widen || parameters != Other.parameters || side_anchor != Other.side_anchor || height_anchor != Other.height_anchor || pieces.size() != Other.pieces.size())
{
return false;
}
for (std::size_t i = 0; i < pieces.size(); ++i)
{
if (pieces[i].points != Other.pieces[i].points || pieces[i].rolls != Other.pieces[i].rolls)
{
return false;
}
}
return true;
}
sweep_node::sweep_node(scene::node_data const &Nodedata) : basic_node(Nodedata) {}
void sweep_node::import(cParser &Input, glm::dvec3 const &Offset)
{
m_state = state{};
m_state.model = Input.getToken<std::string>();
m_state.skin = Input.getToken<std::string>();
auto token{Input.getToken<std::string>()};
while (false == token.empty() && token != "endsweep")
{
if (token == "mode")
{
m_state.bend = Input.getToken<std::string>() != "repeat";
}
else if (token == "step")
{
Input.getTokens();
Input >> m_state.step;
}
else if (token == "range")
{
Input.getTokens(2);
Input >> m_state.from >> m_state.to;
}
else if (token == "offset")
{
Input.getTokens(2);
Input >> m_state.lateral >> m_state.height;
}
else if (token == "axis")
{
m_state.along_x = Input.getToken<std::string>() == "x";
}
else if (token == "flip")
{
m_state.flip = true;
}
else if (token == "mirror")
{
m_state.mirror = true;
}
else if (token == "tilt")
{
m_state.tilt = true;
}
else if (token == "widen")
{
m_state.widen = true;
}
else if (token == "noface")
{
m_state.face = false;
}
else if (token == "parameters")
{
int count{0};
Input.getTokens();
Input >> count;
m_state.parameters.clear();
for (int i = 0; i < count; ++i)
m_state.parameters.push_back(Input.getToken<std::string>(false));
}
else if (token == "anchor")
{
auto const side{Input.getToken<std::string>()};
auto const height{Input.getToken<std::string>()};
m_state.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0;
m_state.height_anchor = height == "bottom" ? 1 : height == "top" ? 2 : height == "edge" ? 3 : 0;
}
else if (token == "piece")
{
segment_data piece;
piece.deserialize(Input, Offset);
m_state.pieces.push_back(piece);
}
else
{
ErrorLog("Bad sweep: unknown property \"" + token + "\" of \"" + m_name + "\" in file \"" + Input.Name() + "\"");
}
token = Input.getToken<std::string>();
}
rebuild_samples();
}
void sweep_node::define(state const &State)
{
auto const shown{m_shapes.shown()};
if (shown)
{
hide();
}
m_state = State;
rebuild_samples();
if (shown)
{
show();
}
}
void sweep_node::rebuild_samples()
{
m_samples.clear();
double station{0.0};
for (auto const &piece : m_state.pieces)
{
cubic const curve{piece};
auto const count{std::clamp(static_cast<int>(plan_distance(curve.p0, curve.p3) / sample_spacing), 4, 4000)};
for (int k = m_samples.empty() ? 0 : 1; k <= count; ++k)
{
auto const t{static_cast<double>(k) / count};
auto const position{curve.point(t)};
if (false == m_samples.empty())
{
station += plan_distance(m_samples.back().position, position);
}
auto tangent{curve.first(t)};
tangent = glm::length(tangent) > 1e-9 ? glm::normalize(tangent) : glm::dvec3{0.0, 0.0, 1.0};
m_samples.push_back({station, position, tangent, piece.rolls[0] + (piece.rolls[1] - piece.rolls[0]) * t, {}, {}});
}
}
if (m_samples.size() > 2)
{
// the structure gauge widens ahead of the curves and of the cant, the same way the gauge check of the editor has it
std::vector<double> chainage, curvature, cant;
for (std::size_t i = 0; i < m_samples.size(); ++i)
{
auto const &before{m_samples[i == 0 ? 0 : i - 1]};
auto const &after{m_samples[std::min(i + 1, m_samples.size() - 1)]};
glm::dvec2 const a{before.tangent.x, before.tangent.z};
glm::dvec2 const b{after.tangent.x, after.tangent.z};
auto const span{after.station - before.station};
auto const turn{glm::length(a) > 1e-9 && glm::length(b) > 1e-9 ? std::atan2(a.x * b.y - a.y * b.x, glm::dot(a, b)) : 0.0};
chainage.push_back(m_samples[i].station);
curvature.push_back(span > 1e-9 ? turn / span : 0.0);
cant.push_back(1.5 * std::abs(std::sin(glm::radians(m_samples[i].roll))));
}
auto const sections{gauge::sections(gauge::kind::unified, chainage, curvature, cant)};
for (std::size_t i = 0; i < m_samples.size() && i < sections.size(); ++i)
{
m_samples[i].widening = sections[i].lower;
m_samples[i].cant = sections[i].cant;
}
}
if (false == m_samples.empty())
{
location(frame_at(0.5 * length()).position);
}
m_area.radius = -1.f;
}
double sweep_node::length() const
{
return m_samples.empty() ? 0.0 : m_samples.back().station;
}
double sweep_node::start() const
{
return std::clamp(m_state.from, 0.0, length());
}
double sweep_node::end() const
{
return m_state.to < 0.0 ? length() : std::clamp(m_state.to, start(), length());
}
sweep_node::frame sweep_node::frame_at(double const Station) const
{
frame result;
if (m_samples.empty())
{
return result;
}
auto const found{std::upper_bound(m_samples.begin(), m_samples.end(), Station, [](double const Value, sample const &Sample) { return Value < Sample.station; })};
auto const after{found == m_samples.end() ? std::prev(found) : found};
auto const before{after == m_samples.begin() ? after : std::prev(after)};
auto const span{after->station - before->station};
auto const f{span > 1e-9 ? std::clamp((Station - before->station) / span, 0.0, 1.0) : 0.0};
result.position = glm::mix(before->position, after->position, f);
// beyond the ends the curve goes on straight
if (Station < m_samples.front().station)
{
result.position = m_samples.front().position + m_samples.front().tangent * (Station - m_samples.front().station);
}
else if (Station > m_samples.back().station)
{
result.position = m_samples.back().position + m_samples.back().tangent * (Station - m_samples.back().station);
}
auto forward{glm::mix(before->tangent, after->tangent, f)};
forward = glm::length(forward) > 1e-9 ? glm::normalize(forward) : glm::dvec3{0.0, 0.0, 1.0};
glm::dvec3 left{-forward.z, 0.0, forward.x};
left = glm::length(left) > 1e-9 ? glm::normalize(left) : glm::dvec3{-1.0, 0.0, 0.0};
glm::dvec3 up{glm::normalize(glm::cross(left, forward))};
result.roll = before->roll + (after->roll - before->roll) * f;
for (int side = 0; side < 2; ++side)
{
result.widening[side] = before->widening[side] + (after->widening[side] - before->widening[side]) * f;
result.cant[side] = before->cant[side] + (after->cant[side] - before->cant[side]) * f;
}
if (m_state.tilt)
{
auto const roll{glm::radians(result.roll)};
auto const turnedleft{left * std::cos(roll) - up * std::sin(roll)};
up = up * std::cos(roll) + left * std::sin(roll);
left = turnedleft;
}
result.forward = forward;
result.left = left;
result.up = up;
return result;
}
// models a template is made of, with their placement in the space of the template
std::vector<sweep_node::item> sweep_node::items() const
{
std::vector<item> result;
auto const &definition{m_state};
auto const ends = [](std::string const &Name, std::string const &Extension) { return Name.size() >= Extension.size() && Name.compare(Name.size() - Extension.size(), Extension.size(), Extension) == 0; };
auto name{definition.model};
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
if (false == ends(name, ".inc"))
{
item single;
single.model = TModelsManager::GetModel(definition.model, false, false);
if (false == definition.skin.empty() && definition.skin != "none")
single.skins.assign(definition.skin);
if (single.model != nullptr)
result.push_back(single);
return result;
}
cParser parser(definition.model, cParser::buffer_FILE, Global.asCurrentSceneryPath, Global.bLoadTraction, definition.parameters);
std::vector<glm::dvec3> offsets{glm::dvec3{0.0}};
glm::dvec3 rotation{0.0};
auto token{parser.getToken<std::string>()};
while (false == token.empty())
{
if (token == "origin")
{
glm::dvec3 offset;
parser.getTokens(3);
parser >> offset.x >> offset.y >> offset.z;
offsets.push_back(offsets.back() + offset);
}
else if (token == "endorigin")
{
if (offsets.size() > 1)
offsets.pop_back();
}
else if (token == "rotate")
{
parser.getTokens(3);
parser >> rotation.x >> rotation.y >> rotation.z;
}
else if (token == "node")
{
double rangemax, rangemin;
std::string nodename, type;
parser.getTokens(4);
parser >> rangemax >> rangemin >> nodename >> type;
if (type == "model")
{
glm::dvec3 location;
double yaw;
parser.getTokens(4);
parser >> location.x >> location.y >> location.z >> yaw;
auto const model{parser.getToken<std::string>()};
auto const skin{parser.getToken<std::string>()};
auto rest{parser.getToken<std::string>()};
while (false == rest.empty() && rest != "endmodel")
rest = parser.getToken<std::string>();
item entry;
entry.model = TModelsManager::GetModel(model, false, false);
if (skin != "none" && false == skin.empty() && skin.front() != '*')
entry.skins.assign(skin);
// placed the way the scenery places a model in an origin turned by a rotate statement
auto const place{glm::rotateY(location, glm::radians(rotation.y)) + offsets.back()};
glm::dmat4 transform{glm::translate(glm::dmat4(1.0), place)};
transform = glm::rotate(transform, glm::radians(rotation.y + yaw), glm::dvec3{0.0, 1.0, 0.0});
transform = glm::rotate(transform, glm::radians(rotation.x), glm::dvec3{1.0, 0.0, 0.0});
transform = glm::rotate(transform, glm::radians(rotation.z), glm::dvec3{0.0, 0.0, 1.0});
entry.transform = transform;
if (entry.model != nullptr && rangemin >= 0.0)
result.push_back(entry);
}
else
{
// whatever else the template defines isn't laid along the curve
auto rest{parser.getToken<std::string>()};
while (false == rest.empty() && rest != "end" + type && rest != "end" && rest != "endtri" && rest != "endline")
rest = parser.getToken<std::string>();
}
}
token = parser.getToken<std::string>();
}
return result;
}
double sweep_node::project(glm::dvec3 const &Point, double &Lateral) const
{
double best{std::numeric_limits<double>::max()};
double station{0.0};
Lateral = 0.0;
glm::dvec2 const point{Point.x, Point.z};
for (std::size_t i = 1; i < m_samples.size(); ++i)
{
glm::dvec2 const a{m_samples[i - 1].position.x, m_samples[i - 1].position.z};
glm::dvec2 const b{m_samples[i].position.x, m_samples[i].position.z};
auto const ab{b - a};
auto const length2{glm::dot(ab, ab)};
if (length2 < 1e-12)
continue;
auto const t{std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0)};
auto const distance{glm::length(point - (a + ab * t))};
if (distance < best)
{
best = distance;
station = m_samples[i - 1].station + (m_samples[i].station - m_samples[i - 1].station) * t;
auto const side{ab.x * (point - a).y - ab.y * (point - a).x};
Lateral = side >= 0.0 ? distance : -distance;
}
}
return station;
}
double sweep_node::model_length() const
{
auto const pieces{items()};
double low{std::numeric_limits<double>::max()}, high{-std::numeric_limits<double>::max()};
for (auto const &entry : pieces)
{
std::vector<model_part> parts;
gather_parts(entry.model->Root, entry.transform, entry.skins, parts);
for (auto const &part : parts)
for (auto const &vertex : part.vertices)
{
auto const along{m_state.along_x ? vertex.position.x : vertex.position.z};
low = std::min(low, along);
high = std::max(high, along);
}
}
return high > low ? high - low : 0.0;
}
std::vector<scene::shape_node> sweep_node::create_shapes() const
{
std::vector<scene::shape_node> shapes;
auto const &definition{m_state};
if (m_samples.size() < 2)
return shapes;
// the parts of each model of the template, and where the model stands in the template
struct piece
{
std::vector<model_part> parts;
glm::dvec3 origin{0.0};
glm::dvec3 low{std::numeric_limits<double>::max()};
glm::dvec3 high{-std::numeric_limits<double>::max()};
bool bent{false};
};
std::vector<piece> pieces;
for (auto const &entry : items())
{
piece model;
gather_parts(entry.model->Root, entry.transform, entry.skins, model.parts);
model.origin = glm::dvec3(entry.transform[3]);
if (false == model.parts.empty())
pieces.push_back(std::move(model));
}
if (pieces.empty())
return shapes;
// the side of the model facing the curve is the one its lateral axis points away from: -x, or -z for a model laid along x
auto const needsflip{definition.along_x ? definition.lateral < 0.0 : definition.lateral > 0.0};
auto const flip{definition.flip != (definition.face && needsflip)};
// the model's own axes: along the curve, sideways (+ to the right of it), and up
auto const axes = [&](glm::dvec3 const &Vector) {
glm::dvec3 result{definition.along_x ? Vector.x : Vector.z, definition.along_x ? Vector.z : -Vector.x, Vector.y};
if (flip)
{
result.x = -result.x;
result.y = -result.y;
}
if (definition.mirror)
{
result.y = -result.y;
}
return result;
};
glm::dvec3 low{std::numeric_limits<double>::max()}, high{-std::numeric_limits<double>::max()};
for (auto &model : pieces)
{
for (auto const &part : model.parts)
for (auto const &vertex : part.vertices)
{
auto const local{axes(vertex.position)};
model.low = glm::min(model.low, local);
model.high = glm::max(model.high, local);
}
low = glm::min(low, model.low);
high = glm::max(high, model.high);
}
// the long models of a template follow the curve, the others stand at their places along it
for (auto &model : pieces)
model.bent = definition.bend && (pieces.size() == 1 || model.high.x - model.low.x >= 0.5 * (high.x - low.x));
// what's bent sets the length of a copy, its sides and its top; the rest only goes along with it
if (std::any_of(pieces.begin(), pieces.end(), [](piece const &Model) { return Model.bent; }))
{
low = glm::dvec3{std::numeric_limits<double>::max()};
high = glm::dvec3{-std::numeric_limits<double>::max()};
for (auto const &model : pieces)
if (model.bent)
{
low = glm::min(low, model.low);
high = glm::max(high, model.high);
}
}
auto const own{high.x - low.x};
// the point of the model the offset puts at its place, sideways and up
auto const nearside{definition.lateral >= 0.0 ? low.y : high.y};
auto const farside{definition.lateral >= 0.0 ? high.y : low.y};
double edgetop{-std::numeric_limits<double>::max()};
for (auto const &model : pieces)
if (model.bent || false == definition.bend)
for (auto const &part : model.parts)
for (auto const &vertex : part.vertices)
{
auto const local{axes(vertex.position)};
if (std::abs(local.y - nearside) <= edge_band)
edgetop = std::max(edgetop, local.z);
}
if (edgetop == -std::numeric_limits<double>::max())
edgetop = high.z;
glm::dvec3 const anchor_shift{0.0,
definition.side_anchor == 1 ? -nearside : definition.side_anchor == 2 ? -farside : definition.side_anchor == 3 ? -0.5 * (low.y + high.y) : 0.0,
definition.height_anchor == 1 ? -low.z : definition.height_anchor == 2 ? -high.z : definition.height_anchor == 3 ? -edgetop : 0.0};
auto const from{start()};
auto const to{end()};
auto const stretch{to - from};
if (own < 1e-3 || stretch < 1e-3)
return shapes;
if (definition.bend)
{
auto const along = [&](glm::dvec3 const &Position) { return axes(Position).x; };
for (auto &model : pieces)
if (model.bent)
for (auto &part : model.parts)
part.vertices = slices(part.vertices, along, low.x, slice_width);
}
auto const copy_length{definition.step > 0.01 ? definition.step : own};
// copies end to end fill the stretch exactly, each one squeezed or stretched a bit; repeated ones go at even steps
auto const copies{definition.bend ? std::max(1, static_cast<int>(std::round(stretch / copy_length))) : static_cast<int>(std::floor(stretch / copy_length + 1e-6)) + 1};
auto const spacing{definition.bend ? stretch / copies : copy_length};
auto const scale{definition.bend ? spacing / own : 1.0};
std::size_t percopy{0};
for (auto const &model : pieces)
for (auto const &part : model.parts)
percopy += part.vertices.size();
auto const limit{std::max<std::size_t>(1, vertex_limit / std::max<std::size_t>(1, percopy))};
if (static_cast<std::size_t>(copies) > limit)
ErrorLog("Bad sweep: \"" + m_name + "\" would make " + std::to_string(copies) + " copies of \"" + definition.model + "\", only " + std::to_string(limit) + " are made");
auto const count{std::min<std::size_t>(copies, limit)};
struct batch
{
material_handle material;
lighting_data lighting;
bool translucent;
std::vector<world_vertex> vertices;
};
std::vector<batch> batches;
auto const target = [&](model_part const &Part) -> std::vector<world_vertex> & {
for (auto &entry : batches)
if (entry.material == Part.material && entry.lighting == Part.lighting && entry.translucent == Part.translucent)
return entry.vertices;
batches.push_back({Part.material, Part.lighting, Part.translucent, {}});
return batches.back().vertices;
};
// set back by the widening of the gauge: from the radius, and on the inner side of a canted curve by the lean of the cars
// at the height of the edge
auto const side{definition.lateral >= 0.0 ? 1 : 0};
auto const edgeheight{std::max(0.0, definition.height - gauge::rail_height)};
auto const setback = [&](frame const &At) {
if (false == definition.widen)
return 0.0;
auto const widening{At.widening[side] + std::max(0.0, At.cant[side]) * edgeheight / 1.5};
return definition.lateral >= 0.0 ? widening : -widening;
};
// heights are given from the head of the rail on the side of the copies. the cant turns the track about its inner rail when the
// simulation raises it for the cant, about the axis otherwise; a copy which leans with the cant goes up with the axis
auto const cone = [&](frame const &At) { return rail_head(At.roll, definition.tilt ? 0.0 : definition.lateral); };
auto const upright = [](frame At) {
glm::dvec3 forward{At.forward.x, 0.0, At.forward.z};
forward = glm::length(forward) > 1e-9 ? glm::normalize(forward) : glm::dvec3{0.0, 0.0, 1.0};
At.forward = forward;
At.left = glm::dvec3{-forward.z, 0.0, forward.x};
At.up = glm::dvec3{0.0, 1.0, 0.0};
return At;
};
glm::dvec3 const offset{0.0, definition.height, 0.0};
for (std::size_t copy = 0; copy < count; ++copy)
{
auto const base{from + spacing * static_cast<double>(copy)};
for (auto const &model : pieces)
{
// a model which doesn't bend stands upright at its point: the place of its origin along the curve
auto const anchor{axes(model.origin) + anchor_shift};
auto const stand{upright(frame_at(definition.bend ? base + (anchor.x - low.x) * scale : base))};
for (auto const &part : model.parts)
{
auto &vertices{target(part)};
auto const first{vertices.size()};
for (auto const &vertex : part.vertices)
{
auto const local{axes(vertex.position) + anchor_shift};
auto const direction{axes(glm::dvec3(vertex.normal))};
world_vertex placed{vertex};
if (model.bent)
{
auto const at{frame_at(base + (local.x - low.x) * scale)};
placed.position = at.position + at.left * (local.y + definition.lateral + setback(at)) + at.up * local.z + offset + glm::dvec3{0.0, cone(at), 0.0};
placed.normal = glm::vec3(glm::normalize(at.forward * direction.x + at.left * direction.y + at.up * direction.z));
}
else
{
auto const along{definition.bend ? local.x - anchor.x : local.x};
placed.position = stand.position + stand.forward * along + stand.left * (local.y + definition.lateral + setback(stand)) + stand.up * local.z + offset + glm::dvec3{0.0, cone(stand), 0.0};
placed.normal = glm::vec3(glm::normalize(stand.forward * direction.x + stand.left * direction.y + stand.up * direction.z));
}
vertices.push_back(placed);
}
if (definition.mirror)
{
// a mirror image turns the triangles inside out
for (auto i = first; i + 2 < vertices.size(); i += 3)
std::swap(vertices[i + 1], vertices[i + 2]);
}
}
}
}
for (auto &entry : batches)
{
if (entry.vertices.empty())
continue;
scene::shape_node shape;
shape.make_terrain(entry.material, std::move(entry.vertices), glm::dvec3{0.0});
shape.lighting(entry.lighting);
shape.translucent(entry.translucent);
shapes.emplace_back(std::move(shape));
}
return shapes;
}
void sweep_node::show()
{
if (m_merged || m_shapes.shown() || m_editorremoved)
{
return;
}
m_shapes.show(create_shapes(), location());
}
void sweep_node::hide()
{
m_shapes.hide();
}
float sweep_node::radius_()
{
return static_cast<float>(0.5 * length() + std::abs(m_state.lateral) + 25.0);
}
void sweep_node::serialize_(std::ostream &Output) const
{
// TODO: implement
}
void sweep_node::deserialize_(std::istream &Input)
{
// TODO: implement
}
void sweep_node::export_as_text_(std::ostream &Output) const
{
auto const &sweep{m_state};
Output << "sweep " << sweep.model << ' ' << (sweep.skin.empty() ? std::string{"none"} : sweep.skin) << ' ';
Output << "mode " << (sweep.bend ? "bend" : "repeat") << ' ';
if (sweep.step > 0.0)
{
Output << "step " << sweep.step << ' ';
}
if (sweep.from > 0.0 || sweep.to >= 0.0)
{
Output << "range " << sweep.from << ' ' << sweep.to << ' ';
}
if (sweep.lateral != 0.0 || sweep.height != 0.0)
{
Output << "offset " << sweep.lateral << ' ' << sweep.height << ' ';
}
if (sweep.along_x)
{
Output << "axis x ";
}
if (sweep.flip)
{
Output << "flip ";
}
if (sweep.mirror)
{
Output << "mirror ";
}
if (sweep.tilt)
{
Output << "tilt ";
}
if (false == sweep.face)
{
Output << "noface ";
}
if (sweep.widen)
{
Output << "widen ";
}
if (false == sweep.parameters.empty())
{
Output << "parameters " << sweep.parameters.size() << ' ';
for (auto const &parameter : sweep.parameters)
Output << (parameter.empty() ? std::string{"none"} : parameter) << ' ';
}
if (sweep.side_anchor != 0 || sweep.height_anchor != 0)
{
char const *sides[] = {"origin", "near", "far", "centre"};
char const *heights[] = {"origin", "bottom", "top", "edge"};
Output << "anchor " << sides[std::clamp(sweep.side_anchor, 0, 3)] << ' ' << heights[std::clamp(sweep.height_anchor, 0, 3)] << ' ';
}
auto const precision{Output.precision(std::numeric_limits<double>::digits10)};
for (auto const &piece : sweep.pieces)
{
auto const &start{piece.points[segment_data::point::start]};
auto const &control1{piece.points[segment_data::point::control1]};
auto const &control2{piece.points[segment_data::point::control2]};
auto const &end{piece.points[segment_data::point::end]};
Output << "\npiece " << start.x << ' ' << start.y << ' ' << start.z << ' ' << piece.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' '
<< end.x << ' ' << end.y << ' ' << end.z << ' ' << piece.rolls[1] << ' ' << piece.radius;
}
Output.precision(precision);
Output << "\nendsweep\n";
}
void sweep_table::create_geometry(scene::scratch_data &Scratchpad)
{
if (simulation::Region == nullptr)
{
return;
}
for (auto *sweep : m_items)
{
if (sweep == nullptr)
{
continue;
}
if (Global.editor_session && false == Global.NvRenderer)
{
// scenery opened for editing: the copies are kept apart, so they can be changed later
sweep->show();
continue;
}
for (auto &shape : sweep->create_shapes())
{
simulation::Region->insert(shape, Scratchpad, false);
}
sweep->merged(true);
}
}

158
world/Sweep.h Normal file
View File

@@ -0,0 +1,158 @@
/*
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 <string>
#include <vector>
#include <glm/glm.hpp>
#include "utilities/Classes.h"
#include "utilities/Names.h"
#include "scene/scenenode.h"
#include "world/Road.h"
#include "world/Segment.h"
#include "vehicle/DynObj.h"
namespace scene
{
struct scratch_data;
}
// a model laid along a curve: copies of it bent to follow the curve end to end, or set along it at even steps.
// the node isn't drawn itself, it makes ordinary shapes of the copies, so the renderers deal with it as with the rest
// of the scenery. animations, lights and other submodels which aren't plain triangles are left out of the copies.
// scenery entry:
// node <max> <min> <name> sweep <model> <skin> [<property> <values>]... piece <laid out like a path of a track>... endsweep
// properties: mode <bend|repeat>, step <length of a copy along the curve, 0: the length of the model>,
// range <from> <to> (m along the curve, to below 0: up to its end), offset <sideways, + to the right> <up>,
// anchor <origin|near|far|centre> <origin|bottom|top> (what of the model the offset puts there: sideways its origin, its side
// facing the curve, the other side or its middle; up its origin, its bottom or its top),
// axis <x|z> (axis of the model laid along the curve), flip (the model goes the other way), mirror (its sides swapped),
// widen (the offset grows by the widening of the structure gauge in the curves, the way the platforms are set back),
// noface (the model isn't turned to have its -x side, or -z for one laid along x, facing the curve),
// parameters <count> <values>... (of a template: the model can be an *.inc file, its long models are bent, the others stand
// upright at their places along the curve),
// anchor sides: origin, near, far, centre; heights: origin, bottom, top, edge (the top of the side facing the curve),
// tilt (bent copies lean with the cant of the curve; otherwise they stay upright, as the platforms and the walls by a track do,
// at the height the plane of the cant has at their sideways offset)
class sweep_node : public scene::basic_node
{
public:
struct state
{
std::string model;
std::string skin{"none"};
bool bend{true};
double step{0.0};
double from{0.0};
double to{-1.0};
double lateral{0.0};
double height{0.0};
bool along_x{false};
bool flip{false};
bool mirror{false};
bool tilt{false};
bool face{true}; // the model is turned so its -x side (-z for a model laid along x) faces the curve
bool widen{false}; // moved away from the curve by the widening of the structure gauge in the curves
std::vector<std::string> parameters; // of a template (*.inc) laid along the curve
int side_anchor{0}; // 0: origin, 1: the side facing the curve, 2: the far side, 3: the middle
int height_anchor{0}; // 0: origin, 1: bottom, 2: top, 3: top of the side facing the curve
std::vector<segment_data> pieces;
bool operator==(state const &Other) const;
};
// point of the curve, with the directions the copies are laid out by
struct frame
{
glm::dvec3 position{0.0};
glm::dvec3 forward{0.0, 0.0, 1.0};
glm::dvec3 left{-1.0, 0.0, 0.0};
glm::dvec3 up{0.0, 1.0, 0.0};
double roll{0.0}; // degrees, of the curve there
std::array<double, 2> widening{}; // of the structure gauge, below the platforms: right and left side, metres
std::array<double, 2> cant{}; // metres, positive on the inner side of the curve
};
explicit sweep_node(scene::node_data const &Nodedata);
void import(cParser &Input, glm::dvec3 const &Offset);
void define(state const &State);
state const &definition() const
{
return m_state;
}
// length of the curve, in the plan
double length() const;
frame frame_at(double const Station) const;
// station of the point of the curve nearest to the point in the plan, and how far to the right of the curve it is
double project(glm::dvec3 const &Point, double &Lateral) const;
// stretch of the curve the copies take
double start() const;
double end() const;
// length of a copy along the curve, as the model has it
double model_length() const;
std::vector<scene::shape_node> create_shapes() const;
// a model of what's laid along the curve, placed in its space
struct item
{
TModel3d *model{nullptr};
material_data skins;
glm::dmat4 transform{1.0};
};
std::vector<item> items() const;
// height of the head of the rail on the side of the offset over the points of the track, less the height of the rail
static double rail_head(double const Roll, double const Lateral);
// puts the copies in the scene as shapes of their own, which can be taken back
void show();
void hide();
void merged(bool const Merged)
{
m_merged = Merged;
}
bool merged() const
{
return m_merged;
}
bool m_editorremoved{false};
private:
void rebuild_samples();
float radius_() override;
void serialize_(std::ostream &Output) const override;
void deserialize_(std::istream &Input) override;
void export_as_text_(std::ostream &Output) const override;
struct sample
{
double station;
glm::dvec3 position;
glm::dvec3 tangent;
double roll; // degrees
std::array<double, 2> widening;
std::array<double, 2> cant;
};
state m_state;
std::vector<sample> m_samples;
bool m_merged{false};
owned_shapes m_shapes;
};
// collection of the models laid along curves present in the scene
class sweep_table : public basic_table<sweep_node>
{
public:
// generates geometry of the copies and puts it in the scene
void create_geometry(scene::scratch_data &Scratchpad);
};