/* 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 "world/Track.h" #include #include #include #include #include #include #include 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 double const outer_reach{30.0}; double const overhang_band{0.15}; double const overhang_depth{0.05}; 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}); } // the gauge changes from 26 m ahead of a curve or of a cant ramp (PKP PLK ST-T2), so the tracks beyond the ends are looked at this far double const lead_reach{26.0}; segment_data turned_piece(segment_data const &Path) { segment_data result{Path}; result.points[segment_data::point::start] = Path.points[segment_data::point::end]; result.points[segment_data::point::end] = Path.points[segment_data::point::start]; result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; result.rolls = {-Path.rolls[1], -Path.rolls[0]}; return result; } bool same_points(segment_data const &A, segment_data const &B) { for (int i = 0; i < 4; ++i) if (glm::distance(A.points[i], B.points[i]) > 1e-3) return false; return true; } glm::dvec3 plan_heading(segment_data const &Piece, double const T) { auto const direction{cubic{Piece}.first(T)}; glm::dvec3 const plan{direction.x, 0.0, direction.z}; return glm::length(plan) > 1e-9 ? glm::normalize(plan) : glm::dvec3{0.0, 0.0, 1.0}; } // paths going on from the end of the last of the pieces, each turned to lead away from it, up to the lead reach std::vector paths_beyond(segment_data Last, std::vector const &Own) { std::vector result; double length{0.0}; while (length < lead_reach && result.size() < 16) { auto const at{Last.points[segment_data::point::end]}; if (simulation::Region == nullptr || false == simulation::Region->point_inside(at)) break; auto const &paths{simulation::Region->section(at).cell(at).m_directories.paths}; std::optional next; for (auto *track : paths) { if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0) continue; for (auto const &path : track->m_paths) for (auto const forward : {true, false}) { auto const candidate{forward ? path : turned_piece(path)}; if (glm::distance(candidate.points[segment_data::point::start], at) > 0.01 || glm::dot(plan_heading(Last, 1.0), plan_heading(candidate, 0.0)) < 0.9) continue; auto const known = [&](segment_data const &Piece) { return same_points(Piece, candidate) || same_points(turned_piece(Piece), candidate); }; if (std::any_of(Own.begin(), Own.end(), known) || std::any_of(result.begin(), result.end(), known)) continue; auto const straight{glm::dot(plan_heading(candidate, 0.0), plan_heading(candidate, 1.0))}; if (next.has_value() && straight <= glm::dot(plan_heading(*next, 0.0), plan_heading(*next, 1.0))) continue; next = candidate; } } if (false == next.has_value()) break; cubic const curve{*next}; for (int k = 1; k <= 8; ++k) length += plan_distance(curve.point((k - 1) / 8.0), curve.point(k / 8.0)); result.push_back(*next); Last = *next; } return result; } // 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 vertices; }; // without the parts lit at night (the glow, the pool of light on the ground), which the instances show only with the lights on void gather_parts(TSubModel *Submodel, glm::dmat4 const &Transform, material_data const &Skins, std::array const &Lit, std::vector &Parts) { for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->Next) { if (std::find(Lit.begin(), Lit.end(), submodel) != Lit.end()) continue; 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::transpose(glm::inverse(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, Lit, 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(T))}; result.normal = glm::length(normal) > 1e-6f ? glm::normalize(normal) : A.normal; result.texture = glm::mix(A.texture, B.texture, static_cast(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 clip(std::vector const &Polygon, std::function const &Along, double const Cut, double const Side) { std::vector result; for (std::size_t i = 0; i < Polygon.size(); ++i) { auto const ¤t{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 slices(std::vector const &Vertices, std::function const &Along, double const Low, double const Width) { std::vector 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(std::floor((std::min({a0, a1, a2}) - Low) / Width))}; auto const last{static_cast(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 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; } struct laid_model { std::vector parts; glm::dvec3 origin{0.0}; glm::dvec3 low{std::numeric_limits::max()}; glm::dvec3 high{-std::numeric_limits::max()}; bool bent{false}; }; struct layout { std::vector models; glm::dvec3 low{std::numeric_limits::max()}; glm::dvec3 high{-std::numeric_limits::max()}; glm::dvec3 shift{0.0}; bool edge_far{false}; }; bool lay_out(sweep_node const &Sweep, layout &Layout) { auto const &definition{Sweep.definition()}; auto const scaling{glm::scale(glm::dmat4(1.0), definition.scale)}; for (auto const &entry : Sweep.items()) { laid_model model; auto const transform{scaling * entry.transform}; gather_parts(entry.model->Root, transform, entry.skins, entry.model->instance_parts().lights_on, model.parts); model.origin = glm::dvec3(transform[3]); if (false == model.parts.empty()) Layout.models.push_back(std::move(model)); } if (Layout.models.empty()) return false; auto &low{Layout.low}; auto &high{Layout.high}; for (auto &model : Layout.models) { for (auto const &part : model.parts) for (auto const &vertex : part.vertices) { auto const local{Sweep.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); } for (auto &model : Layout.models) model.bent = definition.bend && (Layout.models.size() == 1 || model.high.x - model.low.x >= 0.5 * (high.x - low.x)); if (std::any_of(Layout.models.begin(), Layout.models.end(), [](laid_model const &Model) { return Model.bent; })) { low = glm::dvec3{std::numeric_limits::max()}; high = glm::dvec3{-std::numeric_limits::max()}; for (auto const &model : Layout.models) if (model.bent) { low = glm::min(low, model.low); high = glm::max(high, model.high); } } 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::max()}; double nearbottom{std::numeric_limits::max()}, farbottom{std::numeric_limits::max()}; for (auto const &model : Layout.models) if (model.bent || false == definition.bend) for (auto const &part : model.parts) for (auto const &vertex : part.vertices) { auto const local{Sweep.axes(vertex.position)}; if (std::abs(local.y - nearside) <= edge_band) edgetop = std::max(edgetop, local.z); if (std::abs(local.y - nearside) <= overhang_band) nearbottom = std::min(nearbottom, local.z); if (std::abs(local.y - farside) <= overhang_band) farbottom = std::min(farbottom, local.z); } if (edgetop == -std::numeric_limits::max()) edgetop = high.z; Layout.edge_far = farbottom > nearbottom + overhang_depth; auto const point{Sweep.axes(definition.scale * definition.point)}; double const sides[]{0.0, -nearside, -farside, -0.5 * (low.y + high.y), -point.y}; double const heights[]{0.0, -low.z, -high.z, -edgetop, -point.z}; Layout.shift = glm::dvec3{0.0, sides[std::clamp(definition.side_anchor, 0, 4)], heights[std::clamp(definition.height_anchor, 0, 4)]}; return true; } } // 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 || platform != Other.platform || parameters != Other.parameters || side_anchor != Other.side_anchor || height_anchor != Other.height_anchor || point != Other.point || scale != Other.scale || 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(); m_state.skin = Input.getToken(); auto token{Input.getToken()}; while (false == token.empty() && token != "endsweep") { if (token == "mode") { m_state.bend = Input.getToken() != "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() == "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 == "platform") { m_state.platform = 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(false)); } else if (token == "anchor") { auto const side{Input.getToken()}; auto const height{Input.getToken()}; m_state.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : side == "point" ? 4 : 0; m_state.height_anchor = height == "bottom" ? 1 : height == "top" ? 2 : height == "edge" ? 3 : height == "point" ? 4 : 0; } else if (token == "point") { Input.getTokens(3); Input >> m_state.point.x >> m_state.point.y >> m_state.point.z; } else if (token == "scale") { Input.getTokens(3); Input >> m_state.scale.x >> m_state.scale.y >> m_state.scale.z; } 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(); } 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::sample_piece(segment_data const &Piece, std::vector &Samples, double &Station) { cubic const curve{Piece}; auto const count{std::clamp(static_cast(plan_distance(curve.p0, curve.p3) / sample_spacing), 4, 4000)}; for (int k = Samples.empty() ? 0 : 1; k <= count; ++k) { auto const t{static_cast(k) / count}; auto const position{curve.point(t)}; if (false == Samples.empty()) { Station += plan_distance(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}; Samples.push_back({Station, position, tangent, Piece.rolls[0] + (Piece.rolls[1] - Piece.rolls[0]) * t, {}, {}}); } } // the structure gauge widens ahead of the curves and of the cant, the same way the gauge check of the editor has it void sweep_node::gauge_along(std::vector &Samples) { if (Samples.size() < 3) return; std::vector chainage, curvature, cant; for (std::size_t i = 0; i < Samples.size(); ++i) { auto const &before{Samples[i == 0 ? 0 : i - 1]}; auto const &after{Samples[std::min(i + 1, 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(Samples[i].station); curvature.push_back(span > 1e-9 ? turn / span : 0.0); cant.push_back(1.5 * std::abs(std::sin(glm::radians(Samples[i].roll)))); } auto const sections{gauge::sections(gauge::kind::unified, chainage, curvature, cant)}; for (std::size_t i = 0; i < Samples.size() && i < sections.size(); ++i) { Samples[i].widening = sections[i].lower; Samples[i].cant = sections[i].cant; } } void sweep_node::beside_switches(std::vector &Samples) { if (Samples.size() < 3) return; auto const side{m_state.lateral >= 0.0 ? 1 : 0}; auto const sign{m_state.lateral >= 0.0 ? 1.0 : -1.0}; auto const edgeheight{std::max(0.0, m_state.height - gauge::rail_height)}; auto const total = [&](sample const &Sample) { return Sample.widening[side] + std::max(0.0, Sample.cant[side]) * edgeheight / 1.5; }; auto const plan = [](glm::dvec3 const &Vector) { glm::dvec3 const flat{Vector.x, 0.0, Vector.z}; return glm::length(flat) > 1e-9 ? glm::normalize(flat) : glm::dvec3{0.0, 0.0, 1.0}; }; glm::dvec3 low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; for (auto const &sample : Samples) { low = glm::min(low, sample.position); high = glm::max(high, sample.position); } for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2) continue; auto const points{track->m_paths.front().points[segment_data::point::start]}; if (points.x < low.x - 1.0 || points.x > high.x + 1.0 || points.z < low.z - 1.0 || points.z > high.z + 1.0) continue; std::size_t at{0}; auto best{std::numeric_limits::max()}; for (std::size_t i = 0; i < Samples.size(); ++i) if (auto const distance{plan_distance(Samples[i].position, points)}; distance < best) { best = distance; at = i; } if (best > sample_spacing) continue; auto const &closest_sample{Samples[at]}; auto const along{plan(closest_sample.tangent)}; glm::dvec3 const toward{points.x - closest_sample.position.x, 0.0, points.z - closest_sample.position.z}; if (std::abs(glm::dot(toward, glm::dvec3{-along.z, 0.0, along.x})) > 0.05) continue; auto const s0{closest_sample.station + glm::dot(toward, along)}; for (auto const &path : track->m_paths) { auto const heading{glm::dot(plan_heading(path, 0.0), along)}; if (std::abs(heading) < 0.9) continue; auto const beyond{paths_beyond(path, {path})}; std::vector chain; double station{0.0}; if (heading > 0.0) { for (auto const &sample : Samples) if (sample.station >= s0 - outer_reach && sample.station <= s0 + 1e-3) { station = sample.station; chain.push_back(sample); } sample_piece(path, chain, station); for (auto const &piece : beyond) sample_piece(piece, chain, station); } else { for (auto piece = beyond.rbegin(); piece != beyond.rend(); ++piece) sample_piece(turned_piece(*piece), chain, station); sample_piece(turned_piece(path), chain, station); auto const shift{station - s0}; for (auto const &sample : Samples) if (sample.station > s0 + 1e-3 && sample.station <= s0 + outer_reach) { chain.push_back(sample); chain.back().station += shift; } } gauge_along(chain); for (auto &here : Samples) { if (plan_distance(here.position, points) > outer_reach + 2.0 * lead_reach + 100.0) continue; auto const forward{plan(here.tangent)}; sample const *nearest{nullptr}; auto closest{std::numeric_limits::max()}; for (auto const &point : chain) { auto const gap{std::abs(glm::dot(glm::dvec3{point.position.x - here.position.x, 0.0, point.position.z - here.position.z}, forward))}; if (gap < closest) { closest = gap; nearest = &point; } } if (nearest == nullptr || closest > 1.5 * sample_spacing) continue; auto const offset{sign * glm::dot(glm::dvec3{nearest->position.x - here.position.x, 0.0, nearest->position.z - here.position.z}, glm::dvec3{-forward.z, 0.0, forward.x})}; if (offset > 0.05) continue; auto const needed{offset + total(*nearest)}; if (needed > total(here)) here.widening[side] = needed - std::max(0.0, here.cant[side]) * edgeheight / 1.5; } } } } bool sweep_node::bridge() { auto const turned = [](segment_data const &Path) { segment_data result{Path}; result.points[segment_data::point::start] = Path.points[segment_data::point::end]; result.points[segment_data::point::end] = Path.points[segment_data::point::start]; result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; result.rolls = {-Path.rolls[1], -Path.rolls[0]}; return result; }; auto const same = [](segment_data const &A, segment_data const &B) { for (int i = 0; i < 4; ++i) if (glm::distance(A.points[i], B.points[i]) > 1e-3) return false; return true; }; auto const length = [](segment_data const &Piece) { std::vector samples; double station{0.0}; sample_piece(Piece, samples, station); return station; }; auto const heading = [](segment_data const &Piece, double const T) { auto const direction{cubic{Piece}.first(T)}; glm::dvec3 const plan{direction.x, 0.0, direction.z}; return glm::length(plan) > 1e-9 ? glm::normalize(plan) : glm::dvec3{0.0, 0.0, 1.0}; }; auto state{m_state}; auto changed{false}; double station{0.0}; for (std::size_t i = 0; i + 1 < state.pieces.size(); ++i) { station += length(state.pieces[i]); auto const target{state.pieces[i + 1].points[segment_data::point::start]}; auto at{state.pieces[i].points[segment_data::point::end]}; if (glm::distance(at, target) < 0.01) continue; auto const chord{plan_distance(at, target)}; std::vector fill; auto previous{state.pieces[i]}; auto reached{false}; for (int step = 0; step < 20 && false == reached; ++step) { std::optional best; auto bestdistance{std::numeric_limits::max()}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved) continue; for (auto const &path : track->m_paths) for (auto const forward : {true, false}) { auto const candidate{forward ? path : turned(path)}; if (glm::distance(candidate.points[segment_data::point::start], at) > 0.01 || same(candidate, previous) || same(turned(candidate), previous)) continue; if (glm::dot(heading(previous, 1.0), heading(candidate, 0.0)) < 0.5) continue; auto const distance{glm::distance(candidate.points[segment_data::point::end], target)}; if (distance < bestdistance) { bestdistance = distance; best = candidate; } } } if (false == best.has_value()) break; fill.push_back(*best); previous = *best; at = best->points[segment_data::point::end]; reached = glm::distance(at, target) < 0.01; } if (false == reached) continue; double added{0.0}; for (auto const &piece : fill) added += length(piece); auto const shift{added - chord}; if (state.from >= station) state.from += shift; if (state.to >= station) state.to += shift; state.pieces.insert(state.pieces.begin() + i + 1, fill.begin(), fill.end()); station += added; i += fill.size(); changed = true; } if (changed) { WriteLog("Sweep \"" + m_name + "\": the gaps between its pieces filled with the paths which join them"); define(state); } return changed; } void sweep_node::refresh() { auto const current{m_state}; define(current); } void sweep_node::rebuild_samples() { m_boundsvalid = false; m_samples.clear(); std::vector before, after; if ((m_state.widen || m_state.platform) && false == m_state.pieces.empty()) { before = paths_beyond(turned_piece(m_state.pieces.front()), m_state.pieces); after = paths_beyond(m_state.pieces.back(), m_state.pieces); } double station{0.0}; for (auto piece = before.rbegin(); piece != before.rend(); ++piece) sample_piece(turned_piece(*piece), m_samples, station); auto const head{station}; auto const first{m_samples.empty() ? std::size_t{0} : m_samples.size() - 1}; for (auto const &piece : m_state.pieces) sample_piece(piece, m_samples, station); auto const last{m_samples.size()}; for (auto const &piece : after) sample_piece(piece, m_samples, station); gauge_along(m_samples); if (m_state.widen || m_state.platform) beside_switches(m_samples); // the paths beyond the ends only lead the gauge in, the copies stay on the pieces m_samples.erase(m_samples.begin() + last, m_samples.end()); m_samples.erase(m_samples.begin(), m_samples.begin() + first); for (auto &sample : m_samples) sample.station -= head; m_distances.clear(); glm::dvec3 previous{0.0}; for (auto const &sample : m_samples) { frame at; at.widening = sample.widening; at.cant = sample.cant; glm::dvec3 left{-sample.tangent.z, 0.0, sample.tangent.x}; left = glm::length(left) > 1e-9 ? glm::normalize(left) : glm::dvec3{-1.0, 0.0, 0.0}; auto const point{sample.position + left * (m_state.lateral + setback(at))}; m_distances.push_back(m_distances.empty() ? 0.0 : m_distances.back() + std::max(1e-6, plan_distance(previous, point))); previous = point; } 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 m_state.from; } double sweep_node::end() const { return m_state.to < 0.0 ? length() : std::max(m_state.to, start()); } double sweep_node::distance_at(double const Station) const { if (m_samples.size() < 2) return Station; if (Station <= m_samples.front().station) return m_distances.front() + Station - m_samples.front().station; if (Station >= m_samples.back().station) return m_distances.back() + Station - m_samples.back().station; auto const after{static_cast(std::upper_bound(m_samples.begin(), m_samples.end(), Station, [](double const Value, sample const &Sample) { return Value < Sample.station; }) - m_samples.begin())}; auto const before{after - 1}; auto const span{m_samples[after].station - m_samples[before].station}; auto const f{span > 1e-9 ? (Station - m_samples[before].station) / span : 0.0}; return m_distances[before] + (m_distances[after] - m_distances[before]) * f; } double sweep_node::station_at(double const Distance) const { if (m_samples.size() < 2) return Distance; if (Distance <= m_distances.front()) return m_samples.front().station + Distance - m_distances.front(); if (Distance >= m_distances.back()) return m_samples.back().station + Distance - m_distances.back(); auto const after{static_cast(std::upper_bound(m_distances.begin(), m_distances.end(), Distance) - m_distances.begin())}; auto const before{after - 1}; auto const span{m_distances[after] - m_distances[before]}; auto const f{span > 1e-12 ? (Distance - m_distances[before]) / span : 0.0}; return m_samples[before].station + (m_samples[after].station - m_samples[before].station) * f; } double sweep_node::setback(frame const &At) const { if (false == m_state.widen && false == m_state.platform) return 0.0; auto const side{m_state.lateral >= 0.0 ? 1 : 0}; auto const edgeheight{std::max(0.0, m_state.height - gauge::rail_height)}; auto const widening{At.widening[side] + std::max(0.0, At.cant[side]) * edgeheight / 1.5}; return m_state.lateral >= 0.0 ? widening : -widening; } double sweep_node::rise(frame const &At) const { if (m_state.tilt) return rail_head(At.roll, 0.0); if (false == m_state.platform) return rail_head(At.roll, m_state.lateral); // PKP PLK ST-T2 table 3: H = Hi + D/2 -+ D (XB + dbS) / 1500 over the lower rail, with the cant D of the gauge, which // changes from 20 m (inner side) and 26 m (outer side) ahead of the cant ramp, as the platform has to. taken as the // turn of the edge with the plane of the rail heads, as the table has it only to the first order, and on the outer // side, where the edge stays at XB + dbS instead of XB + dbS - dbD, the plane is higher by dbD D / 1500 there auto const side{m_state.lateral >= 0.0 ? 1 : 0}; auto const cant{At.cant[side]}; auto const lower{rail_head(At.roll, 0.0) - 0.75 * std::abs(std::sin(glm::radians(At.roll)))}; auto const edgeheight{std::max(0.0, m_state.height - gauge::rail_height)}; auto const slope{std::clamp(cant / 1.5, -0.5, 0.5)}; auto const level{std::sqrt(1.0 - slope * slope)}; auto const across{std::abs(m_state.lateral) + std::abs(setback(At))}; return lower + 0.5 * std::abs(cant) - (across - edgeheight * slope) * slope / level + edgeheight * (level - 1.0); } glm::dvec3 sweep_node::axes(glm::dvec3 const &Vector) const { auto const &definition{m_state}; auto const needsflip{definition.along_x ? definition.lateral < 0.0 : definition.lateral > 0.0}; auto const flip{definition.flip != (definition.face && needsflip)}; 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; } bool sweep_node::bounds(glm::dvec3 &Low, glm::dvec3 &High, glm::dvec3 &Shift) const { if (false == m_boundsvalid) { layout laid; m_boundsfound = lay_out(*this, laid); m_low = laid.low; m_high = laid.high; m_shift = laid.shift; m_edgefar = laid.edge_far; m_boundsvalid = true; } Low = m_low; High = m_high; Shift = m_shift; return m_boundsfound; } bool sweep_node::edge_far() const { glm::dvec3 low, high, shift; return bounds(low, high, shift) && m_edgefar; } 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::items() const { std::vector 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(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 offsets{glm::dvec3{0.0}}; glm::dvec3 rotation{0.0}; auto token{parser.getToken()}; 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()}; auto const skin{parser.getToken()}; auto rest{parser.getToken()}; while (false == rest.empty() && rest != "endmodel") rest = parser.getToken(); 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()}; while (false == rest.empty() && rest != "end" + type && rest != "end" && rest != "endtri" && rest != "endline") rest = parser.getToken(); } } token = parser.getToken(); } return result; } double sweep_node::project(glm::dvec3 const &Point, double &Lateral) const { double best{std::numeric_limits::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()}; auto const scaling{glm::scale(glm::dmat4(1.0), m_state.scale)}; double low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; for (auto const &entry : pieces) { std::vector parts; gather_parts(entry.model->Root, scaling * entry.transform, entry.skins, entry.model->instance_parts().lights_on, 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 sweep_node::create_shapes() const { std::vector shapes; auto const &definition{m_state}; if (m_samples.size() < 2) return shapes; layout laid; if (false == lay_out(*this, laid)) return shapes; auto &pieces{laid.models}; auto const low{laid.low}; auto const high{laid.high}; auto const anchor_shift{laid.shift}; auto const own{high.x - low.x}; auto const first{distance_at(start())}; auto const stretch{distance_at(end()) - first}; 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(std::round(stretch / copy_length))) : static_cast(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(1, vertex_limit / std::max(1, percopy))}; if (static_cast(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(copies, limit)}; struct batch { material_handle material; lighting_data lighting; bool translucent; std::vector vertices; }; std::vector batches; auto const target = [&](model_part const &Part) -> std::vector & { 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; }; // 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 rise(At); }; 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{first + spacing * static_cast(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(station_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(station_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(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 (sweep.platform) { Output << "platform "; } if (false == sweep.parameters.empty()) { Output << "parameters " << sweep.parameters.size() << ' '; for (auto const ¶meter : 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", "point"}; char const *heights[] = {"origin", "bottom", "top", "edge", "point"}; Output << "anchor " << sides[std::clamp(sweep.side_anchor, 0, 4)] << ' ' << heights[std::clamp(sweep.height_anchor, 0, 4)] << ' '; } if (sweep.side_anchor == 4 || sweep.height_anchor == 4) { Output << "point " << sweep.point.x << ' ' << sweep.point.y << ' ' << sweep.point.z << ' '; } if (sweep.scale != glm::dvec3{1.0}) { Output << "scale " << sweep.scale.x << ' ' << sweep.scale.y << ' ' << sweep.scale.z << ' '; } auto const precision{Output.precision(std::numeric_limits::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 (sweep->bridge()) { if (Global.editor_session) sweep->mark_dirty(); } else if (sweep->definition().widen || sweep->definition().platform) { sweep->refresh(); } 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); } }