// A constant-width track.
//
// Copyright (C) 2001-2019 Sam Varner
//
// This file is part of Vamos Automotive Simulator.
//
// Vamos is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Vamos is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Vamos. If not, see <http://www.gnu.org/licenses/>.
#include "Strip_Track.hpp"
#include "../geometry/Numeric.hpp"
#include "../geometry/Parameter.hpp"
#include "../geometry/Spline.hpp"
#include "../media/Texture_Image.hpp"
#include <GL/glu.h>
#include <cassert>
#include <cmath>
#include <memory>
using namespace Vamos_Geometry;
using namespace Vamos_Media;
using namespace Vamos_Track;
namespace
{
Camera default_camera(0, Three_Vector(100.0, -20.0, 10.0), 0.0);
}
namespace Vamos_Track
{
} // namespace Vamos_Track
Sky_Box::Sky_Box(double side_length, std::string sides_image, std::string top_image,
std::string bottom_image, bool smooth)
// Clamp the textures to aviod showing seams.
: mp_sides(new Texture_Image(sides_image, smooth, true, GL_CLAMP_TO_EDGE)),
mp_top(new Texture_Image(top_image, smooth, true, GL_CLAMP_TO_EDGE)),
mp_bottom(new Texture_Image(bottom_image, smooth, true, GL_CLAMP_TO_EDGE)),
m_list_id(glGenLists(1))
{
double height = side_length;
double length = side_length;
double width = side_length;
double x = -length / 2.0;
double y = -width / 2.0;
double z = -height / 2.0;
glNewList(m_list_id, GL_COMPILE);
glColor3f(1.0, 1.0, 1.0);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
mp_sides->activate();
// front
glBegin(GL_QUAD_STRIP);
glTexCoord2d(0.0, 0.0);
glVertex3d(x + length, y + width, z + height);
glTexCoord2d(0.0, 1.0);
glVertex3d(x + length, y + width, z);
glTexCoord2d(0.25, 0.0);
glVertex3d(x + length, y, z + height);
glTexCoord2d(0.25, 1.0);
glVertex3d(x + length, y, z);
// right
glTexCoord2d(0.25, 0.0);
glVertex3d(x + length, y, z + height);
glTexCoord2d(0.25, 1.0);
glVertex3d(x + length, y, z);
glTexCoord2d(0.5, 0.0);
glVertex3d(x, y, z + height);
glTexCoord2d(0.5, 1.0);
glVertex3d(x, y, z);
// back
glTexCoord2d(0.5, 0.0);
glVertex3d(x, y, z + height);
glTexCoord2d(0.5, 1.0);
glVertex3d(x, y, z);
glTexCoord2d(0.75, 0.0);
glVertex3d(x, y + width, z + height);
glTexCoord2d(0.75, 1.0);
glVertex3d(x, y + width, z);
// left
glTexCoord2d(0.75, 0.0);
glVertex3d(x, y + width, z + height);
glTexCoord2d(0.75, 1.0);
glVertex3d(x, y + width, z);
glTexCoord2d(1.0, 0.0);
glVertex3d(x + length, y + width, z + height);
glTexCoord2d(1.0, 1.0);
glVertex3d(x + length, y + width, z);
glEnd();
// top
mp_top->activate();
glBegin(GL_QUADS);
glTexCoord2d(0.0, 0.0);
glVertex3d(x, y + width, z + height);
glTexCoord2d(0.0, 1.0);
glVertex3d(x + length, y + width, z + height);
glTexCoord2d(1.0, 1.0);
glVertex3d(x + length, y, z + height);
glTexCoord2d(1.0, 0.0);
glVertex3d(x, y, z + height);
glEnd();
// bottom
mp_bottom->activate();
glBegin(GL_QUADS);
glTexCoord2d(0.0, 0.0);
glVertex3d(x + length, y + width, z);
glTexCoord2d(0.0, 1.0);
glVertex3d(x, y + width, z);
glTexCoord2d(1.0, 1.0);
glVertex3d(x, y, z);
glTexCoord2d(1.0, 0.0);
glVertex3d(x + length, y, z);
glEnd();
glFlush();
glEndList();
}
Sky_Box::~Sky_Box()
{
glDeleteLists(m_list_id, 1);
}
void Sky_Box::draw(const Three_Vector& view) const
{
glLoadIdentity();
glTranslatef(view.x, view.y, view.z);
glCallList(m_list_id);
// Clear the depth buffer to keep the sky behind everything else.
glClear(GL_DEPTH_BUFFER_BIT);
}
//----------------------------------------------------------------------------------------
Map_Background::Map_Background(std::string image_file_name, double x_offset, double y_offset,
double x_size, double y_size)
: mp_image(new Texture_Image(image_file_name, true)),
m_x_offset(x_offset),
m_y_offset(y_offset),
m_x_size(x_size),
m_y_size(y_size)
{
}
void Map_Background::draw() const
{
glColor3f(1.0, 1.0, 1.0);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
mp_image->activate();
glLoadIdentity();
glTranslatef(m_x_offset, m_y_offset, 0.0);
glBegin(GL_QUADS);
glTexCoord2d(0.0, 1.0);
glVertex3d(m_x_offset, m_y_offset, 0.0);
glTexCoord2d(0.0, 0.0);
glVertex3d(m_x_offset, m_y_offset + m_y_size, 0.0);
glTexCoord2d(1.0, 0.0);
glVertex3d(m_x_offset + m_x_size, m_y_offset + m_y_size, 0.0);
glTexCoord2d(1.0, 1.0);
glVertex3d(m_x_offset + m_x_size, m_y_offset, 0.0);
glEnd();
// Clear the depth buffer to keep the background behind the track.
glClear(GL_DEPTH_BUFFER_BIT);
}
//----------------------------------------------------------------------------------------
Camera::Camera(size_t segment_index_in, const Three_Vector& position_in, double range_in)
: segment_index(segment_index_in),
position(position_in),
range(range_in)
{}
Camera::Camera()
: Camera(0, Three_Vector(), 0.0)
{}
//----------------------------------------------------------------------------------------
Racing_Line::Racing_Line()
: m_iterations(1500),
m_stiffness(0.5),
m_damping(0.01),
m_margin(1.6)
{}
Racing_Line::~Racing_Line()
{
glDeleteLists(m_list_id, 1);
}
Two_Vector Racing_Line::position(double along) const
{
assert(mp_line);
return mp_line->interpolate(wrap(along, m_length));
}
Three_Vector Racing_Line::curvature(double along, double offline_fraction) const
{
along = wrap(along, m_length);
const Three_Vector c1 = m_curvature.interpolate(along);
const Three_Vector c2 = (offline_fraction > 0.0) ? m_left_curvature.interpolate(along)
: m_right_curvature.interpolate(along);
// Linearly interpolate from line to edge.
double f = std::abs(offline_fraction);
return Three_Vector(Vamos_Geometry::interpolate(f, 0.0, c1.x, 1.0, c2.x),
Vamos_Geometry::interpolate(f, 0.0, c1.y, 1.0, c2.y),
Vamos_Geometry::interpolate(f, 0.0, c1.z, 1.0, c2.z));
}
Three_Vector Racing_Line::tangent(double along) const
{
return m_tangent.interpolate(wrap(along, m_length));
}
Three_Vector Racing_Line::normal_curvature(const Three_Vector& p1, const Three_Vector& p2,
const Three_Vector& p3) const
{
auto r21(p1 - p2);
auto r23(p3 - p2);
auto up(r23.cross(r21));
double length_23 = r23.magnitude();
// Assume the angle is small so that sin x = x.
return up / (r21.dot(r21) * length_23);
}
Three_Vector Racing_Line::planar_curvature(const Three_Vector& p1, const Three_Vector& p2,
const Three_Vector& p3) const
{
return normal_curvature(p1, p2, p3).magnitude() * (p2 - (p1 + p3) / 2.0).unit();
}
void Racing_Line::force(const Three_Vector& p1, const Three_Vector& p2, const Three_Vector& p3,
Three_Vector& f1, Three_Vector& f2, Three_Vector& f3)
{
auto r21(p1 - p2);
auto r23(p3 - p2);
auto curvature = normal_curvature(p1, p2, p3);
auto df1 = m_stiffness * curvature.cross(r21);
auto df3 = -m_stiffness * curvature.cross(r23);
f1 += df1;
f2 -= (df1 + df3);
f3 += df3;
}
double Racing_Line::right_width(const Road& road, double along) const
{
return road.right_racing_line_width(along) - m_margin;
}
double Racing_Line::left_width(const Road& road, double along) const
{
return road.left_racing_line_width(along) - m_margin;
}
void Racing_Line::propagate(const Road& road, std::vector<Three_Vector>& positions,
std::vector<Three_Vector>& velocites, double interval, bool close)
{
size_t points = positions.size();
auto forces = std::vector<Three_Vector>(points);
force(positions[points - 1], positions[0], positions[1], forces[points - 1], forces[0],
forces[1]);
for (size_t i = 1; i < points - 1; i++)
force(positions[i - 1], positions[i], positions[i + 1], forces[i - 1], forces[i],
forces[i + 1]);
force(positions[points - 2], positions[points - 1], positions[0], forces[points - 2],
forces[points - 1], forces[0]);
size_t index = 0;
for (size_t i = 0; i < points; i++)
{
velocites[i] += (forces[i] - velocites[i] * m_damping);
positions[i] += velocites[i];
// Constrain the racing line to the track.
double along = i * interval;
double across = clip(road.track_coordinates(positions[i], index).y,
-right_width(road, along), left_width(road, along));
positions[i] = road.position(along, across);
}
}
void Racing_Line::build(const Road& road, bool close)
{
m_length = road.length();
if (m_length <= 0.0)
throw Bad_Racing_Line_Length(m_length);
mp_line.reset(new Parametric_Spline());
// Divide the track into the smallest number of equal intervals not longer than
// 'interval'
double interval = m_resolution ? *m_resolution
: 0.5 * (left_width(road, 0.0) + right_width(road, 0.0));
const int divisions = std::ceil(m_length / interval);
if (divisions <= 0)
throw No_Racing_Line_Segments(divisions);
interval = m_length / divisions;
// Use the center of the track as the initial guess.
std::vector<Three_Vector> positions;
for (int node = 0; node < divisions; node++)
positions.push_back(road.position(node * interval, 0.0));
std::vector<Three_Vector> velocities(positions.size());
for (size_t i = 0; i < m_iterations; i++)
propagate(road, positions, velocities, interval, close);
m_curvature.clear();
m_left_curvature.clear();
m_right_curvature.clear();
m_tangent.clear();
for (size_t i = 1; i < positions.size() - 1; ++i)
load_curvature(i * interval, positions[i - 1], positions[i], positions[i + 1], road);
if (close)
{
mp_line->set_periodic(m_length);
m_curvature.set_periodic(m_length);
m_left_curvature.set_periodic(m_length);
m_right_curvature.set_periodic(m_length);
m_tangent.set_periodic(m_length);
}
build_list(road);
}
void Racing_Line::load_curvature(double along, const Three_Vector& p1, const Three_Vector& p2,
const Three_Vector& p3, const Road& road)
{
const auto& segment = *road.segment_at(along);
mp_line->load(along, p2.x, p2.y);
m_tangent.load(along, (p3 - p1).unit());
const double factor = segment.racing_line_curvature_factor();
m_curvature.load(along, factor * planar_curvature(p1, p2, p3));
if (segment.radius() == 0.0)
{
m_left_curvature.load(along, Three_Vector::ZERO);
m_right_curvature.load(along, Three_Vector::ZERO);
}
else
{
double across = segment.left_racing_line_width(along);
auto p1 = road.position(along - 10, across);
auto p2 = road.position(along, across);
auto p3 = road.position(along + 10, across);
m_left_curvature.load(along, planar_curvature(p1, p2, p3));
across = segment.right_racing_line_width(along);
p1 = road.position(along - 10, across);
p2 = road.position(along, across);
p3 = road.position(along + 10, across);
m_right_curvature.load(along, planar_curvature(p1, p2, p3));
}
}
void Racing_Line::build_list(const Road& road)
{
if (m_list_id != 0)
glDeleteLists(m_list_id, 1);
m_list_id = glGenLists(1);
glNewList(m_list_id, GL_COMPILE);
glDisable(GL_TEXTURE_2D);
glLineWidth(2.0);
glBegin(GL_LINE_STRIP);
auto last_world = position(0.0);
for (double along = 0.0; along < m_length; along += 0.1)
{
auto world = position(along);
auto forward = (world - last_world).unit();
auto curve = curvature(along, 0.0);
double color = 100.0 * curve.magnitude();
if (curve.cross(forward).z < 0.0)
color *= -1.0;
glColor4f(1.0 - color, 1.0 + color, 1.0, 0.5);
glVertex3d(world.x, world.y, road.segment_at(along)->world_elevation(world) + 0.05);
last_world = world;
}
glEnd();
glPointSize(4.0);
glColor4f(0.8, 0.0, 0.0, 0.5);
glBegin(GL_POINTS);
for (size_t i = 0; i < mp_line->size(); i++)
{
auto world = (*mp_line)[i];
glVertex3d(world.x, world.y,
road.segment_at(mp_line->parameter(i))->world_elevation(world) + 0.04);
}
glEnd();
glEnable(GL_TEXTURE_2D);
glEndList();
}
void Racing_Line::draw() const
{
glCallList(m_list_id);
}
//----------------------------------------------------------------------------------------
Road::Road()
{
m_elevation.load(0.0, 0.0);
}
void Road::clear()
{
m_elevation.clear();
m_elevation.load(0.0, 0.0);
m_length = 0.0;
m_bounds = Rectangle();
m_segments.clear();
}
size_t Road::add_segment(Gl_Road_Segment *segment)
{
if (!m_segments.empty())
{
const auto& last = *m_segments.back();
segment->set_start(last.end_coords(), last.end_distance(), last.end_angle(), 0.0,
last.texture_offsets());
}
m_segments.emplace_back(segment);
return m_segments.size();
}
double Road::build_elevation(bool periodic)
{
double length = 0.0;
for (auto& segment : m_segments)
{
segment->build_elevation(&m_elevation, length);
length += segment->length();
}
if (periodic)
m_elevation.set_periodic(length);
return length;
}
void Road::build_segments(Three_Vector start_coords, double start_angle, double start_bank)
{
//!! number of strips
// Start at zero. Updated after building each segment.
std::array<double, 7> texture_offsets{0.0};
m_length = 0.0;
for (auto& segment : m_segments)
{
segment->set_start(start_coords, m_length, start_angle, start_bank, texture_offsets);
segment->build();
// Update the bounding dimensions.
m_bounds.enclose(segment->bounds());
m_length += segment->length();
start_coords = segment->end_coords();
start_angle = segment->end_angle();
start_bank = segment->banking().end_angle();
texture_offsets = segment->texture_offsets();
}
}
Three_Vector Road::position(double along, double from_center, const Gl_Road_Segment& segment) const
{
return segment.position(along - segment.start_distance(), from_center);
}
Three_Vector Road::position(double along, double from_center) const
{
along = wrap(along, length());
const Gl_Road_Segment *segment = segment_at(along);
return position(along, from_center, *segment);
}
Three_Vector Road::track_coordinates(const Three_Vector& world_pos, size_t& segment_index,
bool forward_only) const
{
auto done = [&segment_index, this](const Three_Vector& pos, size_t index) {
assert(index < m_segments.size());
segment_index = index;
return pos + Three_Vector::X * m_segments[index]->start_distance();
};
// Find the distance along the track, distance from center, and elevation for the
// world coordinates `world_pos.x' and `world_pos.y'.
assert(segment_index < m_segments.size());
for (size_t i = 0; i < m_segments.size() + 1; ++i)
{
Three_Vector track_pos;
double off_end = m_segments[segment_index]->coordinates(world_pos, track_pos);
if (off_end == 0.0)
return done(track_pos, segment_index);
if (forward_only || off_end > 0.0)
{
// We're off the end of the current segment. Find a new candidate segment.
if (++segment_index == m_segments.size())
{
if (!m_is_closed)
return done(track_pos, segment_index - 1);
segment_index = 0;
}
continue;
}
// Try the previous segment.
if (segment_index == 0)
{
if (!m_is_closed)
return done(track_pos, segment_index);
segment_index = m_segments.size();
}
--segment_index;
}
throw Segment_Not_Found(world_pos, segment_index);
}
double Road::distance(double along1, double along2) const
{
const double limit = 0.5 * m_length;
return wrap(along1 - along2, -limit, limit);
}
double Road::left_road_width(double along) const
{
return segment_at(along)->left_road_width(along);
}
double Road::right_road_width(double along) const
{
return segment_at(along)->right_road_width(along);
}
double Road::right_racing_line_width(double along) const
{
return segment_at(along)->right_racing_line_width(along);
}
double Road::left_racing_line_width(double along) const
{
return segment_at(along)->left_racing_line_width(along);
}
const Gl_Road_Segment *Road::segment_at(double along) const
{
assert(!m_segments.empty());
double distance = 0.0;
for (const auto& segment : m_segments)
{
if (distance + segment->length() >= along)
return segment.get();
distance += segment->length();
}
return m_segments.front().get();
}
void Road::build_racing_line()
{
m_racing_line.build(*this, m_is_closed);
}
void Road::narrow_pit_segments()
{
Gl_Road_Segment* last_from_out = nullptr;
Gl_Road_Segment* last_from_in = nullptr;
for (auto it = m_segments.begin(); it != m_segments.end(); it++)
{
const auto& pit = (*it)->pit();
if (!pit.active())
continue;
if (pit.direction() == OUT)
{
for (Segment_List::reverse_iterator rit(it);
rit != m_segments.rend() && rit->get() != last_from_in && !(*rit)->pit().active();
++rit)
{
(*rit)->narrow(pit.side(), (*it)->pit_width());
last_from_out = rit->get();
}
}
else
{
for (auto it2 = it + 1;
it2 != m_segments.end() && it2->get() != last_from_out && !(*it2)->pit().active();
++it2)
{
(*it2)->narrow(pit.side(), (*it)->pit_width());
last_from_in = it2->get();
}
}
}
}
void Road::build(bool close, int adjusted_segments, double length)
{
narrow_pit_segments();
set_skews();
Gl_Road_Segment& first = **m_segments.begin();
Gl_Road_Segment& last = **(m_segments.end() - 1);
if (close)
{
join(first.start_coords(), first.start_angle(), first.start_coords(), first.start_angle(),
adjusted_segments);
// Force the segment to end at 0, 0.
last.last_segment(true);
}
if (length > 0.0)
set_length(length);
// Make sure the walls join.
last.set_left_width(last.length(), first.left_width(0.0));
last.set_right_width(last.length(), first.right_width(0.0));
build_elevation(m_is_closed);
build_segments(Three_Vector(), start_direction(), close ? last.banking().end_angle() : 0.0);
}
double perpendicular_distance(const Three_Vector& p1, const Three_Vector& p2, double angle)
{
return (p2 - p1).magnitude() * sin(atan2(p1.y - p2.y, p1.x - p2.x) - angle);
}
void Road::join(const Three_Vector& start_coords, double start_angle,
const Three_Vector& end_coords, double end_angle, int adjusted_segments)
{
m_is_closed = true;
if (adjusted_segments < 0 || adjusted_segments > 3)
{
std::ostringstream message;
message << "The number of segments to be adjusted (" << adjusted_segments
<< ") is not in the range [0, 3]";
throw Can_Not_Close(message.str());
}
if (m_segments.size() < static_cast<size_t>(adjusted_segments))
{
std::ostringstream message;
message << "Track has fewer segments (" << m_segments.size()
<< ") than the number of segments to be adjusted (" << adjusted_segments << ")";
throw Can_Not_Close(message.str());
}
if (adjusted_segments == 0)
{
// Call it closed without adjusting anything.
return;
}
auto* last_segment = (m_segments.end() - 1)->get();
auto* last_curve = adjusted_segments > 1
? (m_segments.end() - 2)->get()
: last_segment->is_straight() ? nullptr : last_segment;
auto* other_straight = adjusted_segments == 3 ? (m_segments.end() - 3)->get() : nullptr;
if (adjusted_segments > 1 && (last_curve->is_straight() || !last_segment->is_straight()))
{
throw Can_Not_Close("Track must end with a curve followed by a straight when "
"more than one segment is to be adjusted.");
}
if (adjusted_segments == 3 && !other_straight->is_straight())
{
throw Can_Not_Close("Track must end with a straight, a curve and a "
"straight when three segments are to be adjusted.");
}
// Make the last segment parallel to the first by changing the length of the last
// curve.
double last_arc = 0.0;
if (last_curve)
{
last_arc = last_curve->arc() + branch(end_angle - last_curve->end_angle(), -pi);
last_curve->set_arc(last_arc);
// If we're only adjusting the last curve, we're done.
if (last_segment == last_curve)
return;
}
if (adjusted_segments > 1)
{
// Make the last segment collinear with the first.
const double perp = perpendicular_distance(last_curve->end_coords(), end_coords, end_angle);
switch (adjusted_segments)
{
case 2:
{
// Change the radius of the curve.
// last_curve->set_radius (last_curve->radius () + perp / cos (last_arc));
last_curve->set_radius(last_curve->radius() + perp / (1.0 - cos(last_arc)));
break;
}
case 3:
{
// Change the length of segment[-3].
double new_length = other_straight->length() + perp / sin(last_arc);
if (new_length <= 0.0)
throw Can_Not_Close("Closing would require a non-positive length for the"
"next-to-last straight.");
other_straight->set_length(new_length);
break;
}
default:
// 2 and 3 should be the only possibilities in this branch.
assert(false);
}
// Propagate any adjustments to the end of the track.
connect(m_segments.end() - 2);
}
// Extend the last segment to meet the first. Assume they are collinear.
// This is guaranteed for 'adjusted_segments' == 2 or 3 but not 1.
double new_length = (last_segment->start_coords() - end_coords).magnitude();
if (new_length <= 0.0)
throw Can_Not_Close("Closing would require a non-positive length for the"
"last straight.");
last_segment->set_length(new_length);
}
void Road::set_skews()
{
for (auto it = m_segments.begin() + 1; it != m_segments.end(); it++)
{
double skew = (*it)->start_skew();
if (skew != 0.0 && (*it)->arc() != 0.0)
{
if ((*(it - 1))->arc() == 0.0)
(*(it - 1))->set_end_skew(skew);
if ((*(it + 1))->arc() == 0.0)
(*(it + 1))->set_start_skew(-skew);
}
}
}
void Road::set_length(double length)
{
assert(m_segments.size() != 0 && length > 0.0);
// Find the current length.
double old_length = 0.0;
for (const auto& segment : m_segments)
old_length += segment->length();
assert(old_length > 0.0);
double factor = length / old_length;
// Adjust the segments.
for (auto& segment : m_segments)
segment->scale(factor);
}
void Road::set_start_direction(double degrees)
{
m_start_direction = branch(deg_to_rad(degrees), 0.0);
if (m_segments.empty())
return;
m_segments.front()->set_start_angle(m_start_direction);
connect(m_segments.begin() + 1);
}
void Road::connect(Segment_List::iterator it)
{
// There's nothing to do to the first segment.
if (it == m_segments.begin())
it++;
const auto* last = (it - 1)->get();
for (; it != m_segments.end(); it++)
{
(*it)->set_start_angle(last->end_angle());
(*it)->set_start_coords(last->end_coords());
last = it->get();
}
}
void Road::draw()
{
for (const auto& segment : m_segments)
segment->draw();
if (m_draw_racing_line)
m_racing_line.draw();
}
Three_Vector pit_offset(const Gl_Road_Segment& pit, const Gl_Road_Segment& segment,
double distance, double direction)
{
assert(cos(direction) != 0.0);
double pit_width = (pit.pit().side() == LEFT
? segment.left_width(distance) : segment.right_width(distance))
/ cos(direction);
double along = pit.pit().split_or_join();
double offset = pit.pit().side() == LEFT
? pit.left_width(along) - pit_width : -pit.right_width(along) + pit_width;
return pit.radius() == 0.0
? Three_Vector(along, offset, 0.0).rotate(pit.angle(along) * Three_Vector::Z)
: pit.center_of_curve() - pit.start_coords()
+ Three_Vector(pit.radius() - offset, pit.angle(along) - half_pi);
}
Three_Vector Pit_Lane::pit_in_offset(const Gl_Road_Segment& pit_in) const
{
return pit_offset(pit_in, *segments().front(), 0.0, start_direction());
}
Three_Vector Pit_Lane::pit_out_offset(const Gl_Road_Segment& pit_out) const
{
return pit_offset(pit_out, *segments().back(), segments().back()->length(), start_direction());
}
void Pit_Lane::build(bool join_to_track, int adjusted_segments, Gl_Road_Segment& pit_in,
Gl_Road_Segment& pit_out, const Spline& track_elevation)
{
if (m_segments.size() == 0)
return;
// Skew the ends of the pit lane to meet the track.
set_skews();
m_segments.front()->set_start_skew(tan(start_direction()));
m_segments.back()->set_end_skew(tan(end_direction()));
// The ends of the pit lane attach to points on the track. Call
// build_segments() to transform the pit lane's coordinates to the track's
// orientation and pit-in position.
build_elevation(false);
build_segments(pit_in.start_coords() + pit_in_offset(pit_in),
pit_in.pit_angle() + start_direction(),
pit_in.banking().end_angle());
// Make the end of the pit lane meet the track's pit-out position.
if (join_to_track)
join(pit_in.start_coords() + pit_in_offset(pit_in),
pit_in.pit_angle() + start_direction(),
pit_out.start_coords() + pit_out_offset(pit_out),
pit_out.pit_angle() + end_direction(),
adjusted_segments);
// Load the pit lane with elevations from the track.
{
m_length = build_elevation(false);
m_elevation.clear();
double in_distance = pit_in.start_distance() + pit_in.pit().split_or_join();
double out_distance = pit_out.start_distance() + pit_out.pit().split_or_join();
double track_length = track_elevation.back().x;
double delta = wrap(out_distance - in_distance, track_length);
static const int elevations = 10;
for (int i = 0; i < elevations; i++)
{
double along_pit = i * m_length / elevations;
double along_track = wrap(in_distance + i * delta / elevations, track_length);
double z = track_elevation.interpolate(along_track);
m_elevation.load(along_pit, z);
}
m_elevation.load(m_length, track_elevation.interpolate(out_distance));
}
// Finalize the elevations and segments.
build_elevation(false);
build_segments(pit_in.start_coords() + pit_in_offset(pit_in),
pit_in.pit_angle() + start_direction(),
pit_in.banking().end_angle());
}
//----------------------------------------------------------------------------------------
Strip_Track::Strip_Track()
: mp_track(new Road),
mp_pit_lane(new Pit_Lane)
{
m_timing_lines.clear();
m_cameras.clear();
}
void Strip_Track::show_racing_line(bool show)
{
if (mp_track)
mp_track->show_racing_line(show);
}
void Strip_Track::read(std::string data_dir, std::string track_file)
{
// Remember the file name for re-reading.
if (!data_dir.empty() && !track_file.empty())
{
m_data_dir = data_dir;
m_track_file = track_file;
}
mp_track->clear();
mp_pit_lane->clear();
m_timing_lines.clear();
Strip_Track_Reader reader(m_data_dir, m_track_file, this);
}
size_t Strip_Track::add_segment(Gl_Road_Segment *segment)
{
return mp_track->add_segment(segment);
}
size_t Strip_Track::add_pit_segment(Gl_Road_Segment *segment)
{
bool start = mp_pit_lane->segments().empty();
double distance = start ? 0.0 : segment->length();
double width = segment->left_width(distance) + segment->right_width(distance);
double left_shoulder = segment->left_width(distance) - segment->left_road_width(distance);
double right_shoulder = segment->right_width(distance) - segment->right_road_width(distance);
auto pit_segment = mp_track->segments()[start ? m_pit_in_index : m_pit_out_index].get();
pit_segment->set_pit_width(width, left_shoulder, right_shoulder);
return mp_pit_lane->add_segment(segment);
}
void Strip_Track::set_pit_in(size_t index, double angle)
{
m_pit_in_index = index;
mp_pit_lane->set_start_direction(angle);
}
void Strip_Track::set_pit_out(size_t index, double angle)
{
m_pit_out_index = index;
mp_pit_lane->set_end_direction(angle);
}
void Strip_Track::build(bool close, int adjusted_road_segments, double track_length,
bool join_pit_lane, int adjusted_pit_segments)
{
mp_track->build(close, adjusted_road_segments, track_length);
if (m_pit_in_index == -1 || m_pit_out_index == -1)
return;
auto& in = *mp_track->segments()[m_pit_in_index];
auto& out = *mp_track->segments()[m_pit_out_index];
mp_pit_lane->build(join_pit_lane, adjusted_pit_segments, in, out, mp_track->elevation());
double along = in.pit().split_or_join() + 1.0e-6;
double from_center = in.pit().side() == RIGHT
? -in.right_width(along) : in.left_width(along);
m_objects.push_back(Track_Object(position(along + in.start_distance(), from_center),
in.pit().side() == RIGHT ? in.right_material(0.0)
: in.left_material(0.0)));
along = out.pit().split_or_join() - 1.0e-6;
from_center = out.pit().side() == RIGHT
? -out.right_width(along) : out.left_width(along);
m_objects.push_back(Track_Object(position(along + out.start_distance(), from_center),
in.pit().side() == RIGHT ? out.right_material(0.0)
: out.left_material(0.0)));
}
void Strip_Track::set_sky_box(std::string sides_image, std::string top_image,
std::string bottom_image, bool smooth)
{
mp_sky_box.reset(new Sky_Box(100.0, sides_image, top_image, bottom_image, smooth));
}
void Strip_Track::set_map_background(std::string background_image, double x_offset, double y_offset,
double x_size, double y_size)
{
mp_map_background.reset(new Map_Background(
background_image, x_offset, y_offset, x_size, y_size));
}
void Strip_Track::draw_sky(const Three_Vector& view) const
{
mp_sky_box->draw(view);
}
void Strip_Track::draw_map_background() const
{
if (mp_map_background)
mp_map_background->draw();
}
void Strip_Track::draw() const
{
glLoadIdentity();
mp_track->draw();
mp_pit_lane->draw();
}
Three_Vector Strip_Track::reset_position(const Three_Vector& pos, size_t& road_index,
size_t& segment_index) const
{
return track_coordinates(pos, road_index, segment_index).x * Three_Vector::X;
}
Three_Matrix Strip_Track::reset_orientation(const Three_Vector& pos, size_t& road_index,
size_t& segment_index) const
{
// Align the car's up direction with the normal.
const auto& track_pos = track_coordinates(pos, road_index, segment_index);
const auto* segment = get_road(road_index).segments()[segment_index].get();
double along = track_pos.x - segment->start_distance();
auto normal = segment->normal(along, track_pos.y);
Three_Matrix orientation;
orientation.rotate(Three_Vector(-asin(normal.y), asin(normal.x), segment->angle(along)));
return orientation;
}
Three_Vector Strip_Track::track_coordinates(const Three_Vector& world_pos, size_t& road_index,
size_t& segment_index) const
{
// Find the distance along the track, distance from center, and elevation
// for the world coordinates `world_pos.x' and `world_pos.y'.
Three_Vector track_pos;
// Don't use a reference, it may be reassigned.
const auto* segments = &(get_road(road_index).segments());
if (segment_index >= segments->size())
{
std::cerr << segment_index << ' ' << segments->size() << ' ' << road_index << std::endl;
assert(false);
}
const auto* segment = (*segments)[segment_index].get();
int direction = 0;
size_t i = 0;
bool found = false;
while (i < segments->size() + 1)
{
double off_end = segment->coordinates(world_pos, track_pos);
if (off_end == 0.0)
{
found = true;
break;
}
if (direction == 1 || (direction == 0 && off_end > 0.0))
{
direction = 1;
// We're off the end of the current segment. Find a new candidate segment.
if (road_index == 0 && segment_index == size_t(m_pit_in_index)
&& segment->on_pit_merge(track_pos.x, track_pos.y))
{
// We've moved onto the pit lane. Try the first segment on the pit lane.
road_index = 1;
segment_index = 0;
}
else if (road_index == 1 && segment_index == mp_pit_lane->segments().size() - 1)
{
// We've moved off of the pit lane. Try the segment where the pit lane
// joins the track.
road_index = 0;
segment_index = m_pit_out_index;
}
else
{
// Try the next segment.
++segment_index;
if (road_index == 0 && segment_index == segments->size())
segment_index = 0;
}
}
else
{
direction = -1;
// We're off the beginning of the current segment. Find a new candidate
// segment.
if (road_index == 0 && segment_index == size_t(m_pit_out_index)
&& segment->on_pit_merge(track_pos.x, track_pos.y))
{
// We've moved onto the end of the pit lane. Try the
// last segment on the pit lane.
road_index = 1;
segment_index = mp_pit_lane->segments().size() - 1;
}
else if (road_index == 1 && segment_index == 0)
{
// We've moved off of the beginning of the pit lane.
// Try the segment where the pit lane splits from the
// track.
road_index = 0;
segment_index = m_pit_in_index;
}
else
{
// Try the previous segment.
if (road_index == 0 && segment_index == 0)
segment_index = segments->size();
--segment_index;
}
}
segments = &(get_road(road_index).segments());
segment = (*segments)[segment_index].get();
++i;
}
// Throw an exception if a segment could not be found.
if (!found)
throw Segment_Not_Found(world_pos, segment_index);
assert(segment_index < segments->size());
auto material = segment->material_at(track_pos.x, track_pos.y);
auto bump = material.bump(track_pos.x, track_pos.y);
return track_pos + Three_Vector(segment->start_distance(), 0.0, bump.z);
}
const Road& Strip_Track::get_road(size_t road_index) const
{
assert(road_index < 2 && mp_track && mp_pit_lane);
return road_index == 0 ? *mp_track : *mp_pit_lane;
}
Contact_Info Strip_Track::test_for_contact(const Three_Vector& pos, double bump_parameter,
size_t& road_index, size_t& segment_index) const
{
const auto track_pos = track_coordinates(pos, road_index, segment_index);
const auto* segment = get_road(road_index).segments()[segment_index].get();
const double segment_dist = track_pos.x - segment->start_distance();
// Test for contact with the road.
double diff = track_pos.z - pos.z;
if (diff >= 0.0)
{
auto material = segment->material_at(track_pos.x, track_pos.y);
auto bump = material.bump(track_pos.x, track_pos.y);
auto normal = segment->normal(segment_dist, track_pos.y, bump);
return Contact_Info{true, diff, normal, material};
}
auto test_wall = [track_pos, segment, segment_dist](const Material& material, double y) {
auto bump = material.bump(track_pos.x, track_pos.y);
double diff = y + bump.z;
if (diff >= 0.0)
{
auto normal = segment->barrier_normal(segment_dist, track_pos.y, bump);
return Contact_Info{true, diff, normal, material};
}
return Contact_Info{0};
};
auto contact = test_wall(segment->left_material(pos.z),
track_pos.y - segment->left_width(segment_dist));
if (contact.contact)
return contact;
return test_wall(segment->right_material(pos.z),
-track_pos.y - segment->right_width(segment_dist));
}
Three_Vector Strip_Track::position(double along, double from_center) const
{
return mp_track->position(along, from_center);
}
int Strip_Track::sector(double distance) const
{
for (size_t i = 0; i < m_timing_lines.size(); i++)
if (m_timing_lines[i] > distance)
return i;
return m_timing_lines.size();
}
void Strip_Track::set_start_direction(double degrees)
{
m_start_direction = deg_to_rad(degrees);
mp_track->set_start_direction(degrees);
}
void Strip_Track::add_camera(const Camera& camera)
{
m_cameras.push_back(camera);
}
double Strip_Track::camera_range(const Camera& camera) const
{
double range = mp_track->segments()[camera.segment_index]->start_distance() + camera.position.x
- camera.range;
return wrap(range, mp_track->length());
}
Three_Vector Strip_Track::camera_position(const Camera& camera) const
{
const auto& segment = *mp_track->segments()[camera.segment_index];
return segment.position(camera.position.x, camera.position.y)
+ Three_Vector(0.0, 0.0, camera.position.z);
}
const Camera& Strip_Track::get_camera(double distance) const
{
if (m_cameras.empty())
return default_camera;
// See if we're near the end of the track and should be picked up by the first camera.
double first = m_cameras.begin()->position.x - m_cameras.begin()->range;
if (mp_track->is_closed() && first < 0.0 && distance > wrap(first, mp_track->length()))
return *m_cameras.begin();
for (auto rit = m_cameras.crbegin(); rit != m_cameras.crend(); ++rit)
if (distance > camera_range(*rit))
return *rit;
return *m_cameras.begin();
}
Three_Vector Strip_Track::camera_target(const Camera& camera) const
{
double angle = mp_track->segments()[camera.segment_index]->angle(camera.position.x);
return camera_position(camera)
+ Three_Vector(-sin(deg_to_rad(camera.direction.x) + angle),
cos(deg_to_rad(camera.direction.x) + angle),
sin(deg_to_rad(camera.direction.y)));
}
const Rectangle& Strip_Track::bounds() const
{
return Rectangle(mp_track->bounds()).enclose(mp_pit_lane->bounds());
}
Three_Vector Strip_Track::grid_position(int place, int total, bool pit) const
{
assert(place > 0); // 1-based
assert(place <= total);
static const double grid_interval = pit ? 12.0 : 8.0;
// Put the 1st car 1 interval from the beginning of the 1st segment to avoid
// putting off the end.
double across = pit ? 1.5 * mp_track->left_road_width(0.0) : 3.0 * std::pow(-1, place);
return Three_Vector(grid_interval * (total - place + 1), across, 0.0);
}