//-----------------------------------------------------------------------------
// TrackLine.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Text;
using RacingGame.Graphics;
using RacingGame.Helpers;
using RacingGame.Landscapes;
namespace RacingGame.Tracks
{
///
/// Track line
///
public class TrackLine
{
///
/// Number of iterations we will produce from the input points to
/// get our line positions. All data is generated with help of
/// CattmullRom splines.
///
protected const int NumberOfIterationsPer100Meters = 40;
///
/// Curve factor, 1.0 will make all curves the same as the rotations are.
/// Basically we just have to drive ahead. Reduce this value to make
/// the track harder!
///
const float CurveFactor = 0.25f;
///
/// Correct the road to be up each step. This is important to
/// make our road face always up except where we have no choice (loopings).
///
const float UpFactorCorrector = 0.6f;
///
/// Factor for streching the road texture, smaller values will the texture
/// strech more.
///
const float RoadTextureStrechFactor = 0.125f;
///
/// Number of values we put into our second pass up vector smoothing
/// alsorithm below.
///
const int NumberOfUpSmoothValues = 10;
///
/// Minimium distance of the road track to the landscape.
///
const float MinimumLandscapeDistance = 2.0f;
///
/// Looping points for generating smooth loops.
/// See TrackLine constructor for details.
///
static readonly Vector3[] LoopingPoints =
new Vector3[]
{
new Vector3(0, 0, 0),
new Vector3(0, 0.353553f, 0.146447f),
new Vector3(0, 0.5f, 0.5f),
new Vector3(0, 0.353553f, 1.0f-0.146447f),
new Vector3(0, 0, 1.0f),
new Vector3(0, -0.353553f, 1.0f-0.146447f),
new Vector3(0, -0.5f, 0.5f),
new Vector3(0, -0.353553f, 0.146447f),
new Vector3(0, 0, 0),
};
///
/// Points of this track (middle line), generated from the input points
/// in the constructor using .
///
/// Each point has also the following 3 vectors:
/// Right vector: Makes it easier to create the road vertices. Can also be
/// calculated by just crossing up and dir vectors! See Unit Test below.
/// Up vector: This are the calculated up vectors after the second pass.
/// We also work the CurveFactor and UpFactorCorrector in here!
/// Dir vector: Not really used, can be ignored externally.
/// But useful for generation and checking distances.
///
protected List points = new List();
///
/// Road helper position
///
public class RoadHelperPosition
{
///
/// Type
///
public TrackData.RoadHelper.HelperType type;
///
/// Start and end road segment num for our tunnel/palms/laterns.
///
public int startNum, endNum;
///
/// Create road helper position
///
/// Set start number
/// Set end number
public RoadHelperPosition(TrackData.RoadHelper.HelperType setType,
int setStartNum, int setEndNum)
{
type = setType;
startNum = setStartNum;
endNum = setEndNum;
}
}
///
/// Remember tunnel positions, used in the Track class for the
/// tunnel generation. Usually we don't have much tunnels.
///
protected List helperPositions =
new List();
///
/// Create track line
///
/// Input Points, will form a closed curve
///
public TrackLine(Vector3[] inputPoints,
List widthHelpers,
List roadHelpers,
List neutralObjects,
Landscape landscape)
{
Load(inputPoints, widthHelpers, roadHelpers, neutralObjects, landscape);
}
#if DEBUG
///
/// Create track line.
/// This constructor is only used for unit tests.
///
/// Input points
public TrackLine(Vector3[] inputPoints)
: this(inputPoints,
new List(),
new List(),
new List(),
null)
{
}
#endif
///
/// Create track line
///
///
/// Input points from track
public TrackLine(TrackData inputPointsFromTrack,
Landscape landscape)
: this(inputPointsFromTrack.TrackPoints.ToArray(),
inputPointsFromTrack.WidthHelpers,
inputPointsFromTrack.RoadHelpers,
inputPointsFromTrack.NeutralsObjects,
landscape)
{
}
///
/// Load
///
/// Input points
/// Width helpers
/// Road helpers
/// Neutral objects
/// Landscape
protected void Load(Vector3[] inputPoints,
List widthHelpers,
List roadHelpers,
List neutralObjects,
Landscape landscape)
{
points.Clear();
helperPositions.Clear();
// Kill all loaded objects
if (landscape != null)
landscape.KillAllLoadedObjects();
if (inputPoints == null ||
inputPoints.Length < 3)
throw new ArgumentException("inputPoints is invalid, we need at " +
"least 3 valid input points to generate a TrackLine.");
if (landscape != null)
{
// Go through all spline points
for (int num = 0; num < inputPoints.Length; num++)
{
// Get landscape height here
float landscapeHeight = landscape.GetMapHeight(
inputPoints[num].X, inputPoints[num].Y) +
// add little to fix ground errors
MinimumLandscapeDistance * 2.25f;
// And make sure we are always above it!
if (inputPoints[num].Z < landscapeHeight)
inputPoints[num].Z = landscapeHeight;
}
// Second pass, check 24 interpolation points between all inputPoints
for (int num = 0; num < inputPoints.Length; num++)
for (int iter = 1; iter < 25; iter++)
{
float iterPercent = iter / 25.0f;
float iterHeight = inputPoints[num].Z * (1 - iterPercent) +
inputPoints[(num + 1) % inputPoints.Length].Z * iterPercent;
// Check 2x2 points (in all directions) to make sure
// we don't go through the landscape at the sides
for (int x = 0; x < 2; x++)
for (int y = 0; y < 2; y++)
{
// Also get height at middle to next pos
float landscapeHeight = landscape.GetMapHeight(
-5.0f + 10.0f * x +
inputPoints[num].X * (1 - iterPercent) +
inputPoints[(num + 1) % inputPoints.Length].X
* iterPercent, -5.0f + 10.0f * y +
inputPoints[num].Y * (1 - iterPercent) +
inputPoints[(num + 1) % inputPoints.Length].Y
* iterPercent) +
// add little to fix ground errors
MinimumLandscapeDistance * 1.6f;
// Increase both positions if this point
// is under the landscape
if (iterHeight < landscapeHeight)
{
float increaseHeight = landscapeHeight - iterHeight;
inputPoints[num].Z += increaseHeight;
inputPoints[(num + 1) % inputPoints.Length].Z +=
increaseHeight;
}
}
}
}
// Go through all spline points (ignore first and last 3, this
// makes it easier to remove points and add new ones).
for (int num = 1; num < inputPoints.Length - 3; num++)
{
// X/Y distance has to be 4 times smaller than Z distance
Vector3 distVec = inputPoints[num + 1] - inputPoints[num];
float xyDist = (float)Math.Sqrt(
distVec.X * distVec.X + distVec.Y * distVec.Y);
float zDist = Math.Abs(distVec.Z);
// Also check if next point is down again.
Vector3 distVec2 = inputPoints[num + 2] - inputPoints[num + 1];
if (zDist / 2 > xyDist &&
Math.Abs(distVec.Z + distVec2.Z) < zDist / 2)
{
// Find out which direction we are going
Vector3 dir = inputPoints[num] - inputPoints[num - 1];
dir.Normalize();
Vector3 upVec = new Vector3(0, 0, 1);
Vector3 rightVec = Vector3.Cross(dir, upVec);
// Matrix build helper matrix to rotate our looping points
Matrix rotMatrix = new Matrix(
rightVec.X, rightVec.Y, rightVec.Z, 0,
dir.X, dir.Y, dir.Z, 0,
upVec.X, upVec.Y, upVec.Z, 0,
0, 0, 0, 1);
// Ok do a looping with zDist as height.
// Start with the current point, loop around and end with the
// point after the looping. We will remove the current and the
// next 2 points, but add 9 new points instead for our smooth loop.
// See LoopingPoints for the looping itself.
Vector3 startLoopPos = inputPoints[num];
Vector3 endLoopPos = inputPoints[num + 2];
// Insert 7 new points (9 new points, but we reuse
// start, middle and end points which are num, num+1 and num+2,
// plus an additional point after the looping to keep the road
// straight!)
Vector3[] remInputPoints = (Vector3[])inputPoints.Clone();
inputPoints = new Vector3[inputPoints.Length + 7];
// Copy everything over
for (int copyNum = 0; copyNum < remInputPoints.Length; copyNum++)
if (copyNum < num)
inputPoints[copyNum] = remInputPoints[copyNum];
else
inputPoints[copyNum + 7] = remInputPoints[copyNum];
// Ok, now we can add our loop
for (int loopNum = 0; loopNum < LoopingPoints.Length; loopNum++)
{
// Interpolate between start and end pos to land at the end pos!
float loopPercent = loopNum / (float)(LoopingPoints.Length - 1);
inputPoints[num + loopNum] =
startLoopPos * (1 - loopPercent) +
endLoopPos * loopPercent +
zDist * Vector3.Transform(LoopingPoints[loopNum], rotMatrix);
}
// Add extra point to keep the road straight
Vector3 newRoadDir =
inputPoints[num + 10] - inputPoints[num + 8];
// Don't go more than zDist * 2 units away!
if (newRoadDir.Length() > zDist * 2)
{
newRoadDir.Normalize();
newRoadDir = newRoadDir * zDist;
inputPoints[num + 9] = inputPoints[num + 8] + newRoadDir;
}
else
// Just add an interpolation point
inputPoints[num + 9] =
(inputPoints[num + 8] + inputPoints[num + 10]) / 2.0f;
// Advance 10 points until we check for the next loop
num += 10;
// That's it, good work everyone ^^
}
}
// Generate all points with help of catmull rom splines
for (int num = 0; num < inputPoints.Length; num++)
{
// Get the 4 required points for the catmull rom spline
Vector3 p1 = inputPoints[num - 1 < 0 ? inputPoints.Length - 1 : num - 1];
Vector3 p2 = inputPoints[num];
Vector3 p3 = inputPoints[(num + 1) % inputPoints.Length];
Vector3 p4 = inputPoints[(num + 2) % inputPoints.Length];
// Calculate number of iterations we use here based
// on the distance of the 2 points we generate new points from.
float distance = Vector3.Distance(p2, p3);
int numberOfIterations =
(int)(NumberOfIterationsPer100Meters * (distance / 100.0f));
if (numberOfIterations <= 0)
numberOfIterations = 1;
for (int iter = 0; iter < numberOfIterations; iter++)
{
TrackVertex newVertex = new TrackVertex(
Vector3.CatmullRom(p1, p2, p3, p4,
iter / (float)numberOfIterations));
points.Add(newVertex);
}
}
// Pre up vectors are used to first generate all optimal up vectors
// for the track, but this is not useful for driving because we need
// the road to point up always except for loopings.
List preUpVectors = new List();
// Now generate all up vectors, first pass does optimal up vectors.
Vector3 defaultUpVec = new Vector3(0, 0, 1);
Vector3 lastUpVec = defaultUpVec;
for (int num = 0; num < points.Count; num++)
{
// Get direction we are driving in at this point,
// interpolate with help of last and next points.
Vector3 dir = points[(num + 1) % points.Count].pos -
points[num - 1 < 0 ? points.Count - 1 : num - 1].pos;
dir.Normalize();
// Now calculate the optimal up vector for this point
Vector3 middlePoint = (points[(num + 1) % points.Count].pos +
points[num - 1 < 0 ? points.Count - 1 : num - 1].pos) / 2.0f;
Vector3 optimalUpVector = middlePoint - points[num].pos;
if (optimalUpVector.Length() < 0.0001f)
optimalUpVector = lastUpVec;
optimalUpVector.Normalize();
// Store the optimalUpVectors in the preUpVectors list
preUpVectors.Add(optimalUpVector);
// Also save dir vector
points[num].dir = dir;
// And remember the last upVec in case the road is going straight ahead
lastUpVec = optimalUpVector;
}
// Interpolate the first up vector for a smoother road at the start pos
preUpVectors[0] = preUpVectors[preUpVectors.Count - 1] + preUpVectors[1];
preUpVectors[0].Normalize();
// Second pass, interpolated precalced values and apply our logic :)
//preUpVectors[0] =
lastUpVec = Vector3.Lerp(defaultUpVec, preUpVectors[0],
1.5f * CurveFactor * UpFactorCorrector);
//lastUpVec = preUpVectors[0];
Vector3 lastUpVecUnmodified = lastUpVec;// defaultUpVec;
for (int num = 0; num < points.Count; num++)
{
// Grab dir vector (could be calculated here too)
Vector3 dir = points[num].dir;
// First of all interpolate the preUpVectors
Vector3 upVec =
//single input: preUpVectors[num];
Vector3.Zero;
for (int smoothNum = -NumberOfUpSmoothValues / 2;
smoothNum <= NumberOfUpSmoothValues / 2; smoothNum++)
upVec +=
preUpVectors[(num + points.Count + smoothNum) % points.Count];
upVec.Normalize();
// Find out if this road piece is upside down and if we are
// moving up or down. This is VERY important for catching loopings.
bool upsideDown = upVec.Z < -0.25f &&
lastUpVecUnmodified.Z < -0.05f;
bool movingUp = dir.Z > 0.75f;
bool movingDown = dir.Z < -0.75f;
// Mix in the last vector to make curves weaker
upVec = Vector3.Lerp(lastUpVec, upVec, CurveFactor);
upVec.Normalize();
// Store the last value to check for loopings.
lastUpVecUnmodified = upVec;
// Don't mix in default up if we head up or are upside down!
// Its very useful to know if we move up or down to fix the
// problematic areas at loopings by pointing stuff correct right away.
if (movingUp)
lastUpVec = Vector3.Lerp(upVec, -defaultUpVec, UpFactorCorrector);
else if (movingDown)
lastUpVec = Vector3.Lerp(upVec, defaultUpVec, UpFactorCorrector);
else if (upsideDown)
lastUpVec = Vector3.Lerp(upVec, -defaultUpVec, UpFactorCorrector);
else
lastUpVec = Vector3.Lerp(upVec, defaultUpVec, UpFactorCorrector);
// If we are very close to the ground, make the road point up more!
if (//upsideDown == false &&
landscape != null)
{
// Get landscape height here
float landscapeHeight = landscape.GetMapHeight(
points[num].pos.X, points[num].pos.Y);
// If point is close to the landscape, let everything point up more
if (points[num].pos.Z - landscapeHeight <
MinimumLandscapeDistance * 4)
lastUpVec = Vector3.Lerp(upVec, defaultUpVec,
1.75f * UpFactorCorrector);
}
// And finally calculate rightVectors with just a cross product.
// Used to render the track later.
Vector3 rightVec = Vector3.Cross(dir, upVec);
rightVec.Normalize();
points[num].right = rightVec;
// Recalculate up vector with help of right and dir.
// This makes the up vector to always point up 90 degrees.
upVec = Vector3.Cross(rightVec, dir);
upVec.Normalize();
points[num].up = upVec;
}
lastUpVec = points[0].up;
for (int num = 0; num < points.Count; num++)
preUpVectors[num] = points[num].up;
for (int num = 0; num < points.Count; num++)
{
// Interpolate up vectors again
Vector3 upVec = Vector3.Zero;
for (int smoothNum = -NumberOfUpSmoothValues;
smoothNum <= NumberOfUpSmoothValues; smoothNum++)
{
upVec +=
preUpVectors[(num + points.Count + smoothNum) % points.Count];
}
upVec.Normalize();
points[num].up = upVec;
// Also rebuild right vector
Vector3 dir = points[num].dir;
points[num].right = Vector3.Cross(dir, upVec);
}
AdjustRoadWidths(widthHelpers);
GenerateUTextureCoordinates();
GenerateTunnelsAndLandscapeObjects(
roadHelpers, neutralObjects, landscape);
}
///
/// Load
///
/// track
/// Landscape
protected void Load(TrackData trackData, Landscape landscape)
{
Load(trackData.TrackPoints.ToArray(),
trackData.WidthHelpers,
trackData.RoadHelpers,
trackData.NeutralsObjects,
landscape);
}
private void AdjustRoadWidths(
List widthHelpers)
{
// Go through all width helpers along the road,
// everything close enough (<25) is interessting for us.
float currentWidth = TrackVertex.DefaultRoadWidth;
float widthInfluence = currentWidth;
for (int num = 0; num < points.Count; num++)
{
Vector3 pos = points[num].pos;
foreach (TrackData.WidthHelper widthHelper in widthHelpers)
{
float dist = Vector3.Distance(widthHelper.pos, pos);
if (dist < 25.0f)
{
float influence = (1 - (dist / 25.0f));
widthInfluence =
(1 - influence) * widthInfluence +
influence * widthHelper.scale;
}
}
// Use 90% the old width and 10% the new width.
currentWidth =
currentWidth * 0.9f +
widthInfluence * 0.1f;
// At the end of the road, mix in the staring road width (loop)
if (num > points.Count - 7)
{
float influence =
num == (points.Count - 1) ? 0.75f :
num == (points.Count - 2) ? 0.5f :
num == (points.Count - 2) ? 0.25f : 0.175f;
currentWidth =
influence * points[0].roadWidth +
(1 - influence) * currentWidth;
}
if (currentWidth < TrackVertex.MinRoadWidth)
currentWidth = TrackVertex.MinRoadWidth;
if (currentWidth > TrackVertex.MaxRoadWidth)
currentWidth = TrackVertex.MaxRoadWidth;
points[num].roadWidth = currentWidth;
}
}
private void GenerateUTextureCoordinates()
{
float currentRoadUTexValue = 0.0f;
for (int num = 0; num < points.Count; num++)
{
// Assign u texture coordinate
points[num].uv.X = currentRoadUTexValue;
// Uniform calculation of the texture coordinates for the roadway,
// so it doesn't matter if there is a gap of 2 or 200 m
currentRoadUTexValue += RoadTextureStrechFactor *
(points[(num + 1) % points.Count].pos -
points[num % points.Count].pos).Length();
}
// Now we got a problem, for the polygons between the last and the first
// points (last road block) we might have very different texture
// coordinates, which may look very wrong. To fix this we generate a new
// point by just duplicating the first point and applying another set of
// texture coordinates!
points.Add(new TrackVertex(
points[0].pos,
points[0].right,
points[0].up,
points[0].dir,
new Vector2(currentRoadUTexValue, 0),
points[0].roadWidth));
}
private void GenerateTunnelsAndLandscapeObjects(
List roadHelpers,
List neutralObjects,
Landscape landscape)
{
// Go through all tunnel helpers along the road,
// everything close enough (<25) is interessting for us.
int helperStartedNum = -1;
TrackData.RoadHelper.HelperType remType =
TrackData.RoadHelper.HelperType.Reset;
for (int num = 0; num < points.Count; num++)
{
Vector3 pos = points[num].pos;
foreach (TrackData.RoadHelper roadHelper in roadHelpers)
{
float dist = Vector3.Distance(roadHelper.pos, pos);
if (dist < 25.0f)
{
if (helperStartedNum >= 0)
{
helperPositions.Add(new RoadHelperPosition(
remType, helperStartedNum, num));
// Reset?
if (roadHelper.type == TrackData.RoadHelper.HelperType.Reset)
helperStartedNum = -1;
else
{
// Start new part
helperStartedNum = num;
remType = roadHelper.type;
}
}
else
{
helperStartedNum = num;
remType = roadHelper.type;
}
// Remove this roadHelper (don't use it again)!
roadHelpers.Remove(roadHelper);
break;
}
}
}
// Still a helper open? Then close it close before the end!
if (helperStartedNum > 0)
helperPositions.Add(new RoadHelperPosition(
remType, helperStartedNum, points.Count - 3));
if (landscape != null)
{
for (int num = 0; num < neutralObjects.Count; num++)
{
TrackData.NeutralObject obj = neutralObjects[num];
landscape.AddObjectToRender(obj.modelName, obj.matrix, false);
}
}
}
}
}