private void CreateRoad() { //RandomHelper.Random = new Random(1234567); // Set isClosed to true join the start and the end of the road. bool isClosed = false; // Create a new TerrainRoadLayer which paints a road onto the terrain. // The road itself is defined by a mesh which is set later. _roadLayer = new TerrainRoadLayer(GraphicsService) { DiffuseColor = new Vector3F(0.5f), SpecularColor = new Vector3F(1), DiffuseTexture = ContentManager.Load<Texture2D>("Terrain/Road-Asphalt-Diffuse"), NormalTexture = ContentManager.Load<Texture2D>("Terrain/Road-Asphalt-Normal"), SpecularTexture = ContentManager.Load<Texture2D>("Terrain/Road-Asphalt-Specular"), HeightTexture = ContentManager.Load<Texture2D>("Terrain/Road-Asphalt-Height"), // The size of the tileable detail textures in world space units. TileSize = 5, // The border blend range controls how the border of the road fades out. // We fade out 5% of the texture on each side of the road. BorderBlendRange = new Vector4F(0.05f, 0.05f, 0.05f, 0.05f), }; // Create 3D spline path with some semi-random control points. _roadPath = new Path3F { PreLoop = isClosed ? CurveLoopType.Cycle : CurveLoopType.Linear, PostLoop = isClosed ? CurveLoopType.Cycle : CurveLoopType.Linear, SmoothEnds = isClosed, }; // The position of the next path key. Vector3F position = new Vector3F( RandomHelper.Random.NextFloat(-20, 20), 0, RandomHelper.Random.NextFloat(-20, 20)); // The direction to the next path key. Vector3F direction = QuaternionF.CreateRotationY(RandomHelper.Random.NextFloat(0, 10)).Rotate(Vector3F.Forward); // Add path keys. for (int j = 0; j < 10; j++) { // Instead of a normal PathKey3F, we use a TerrainRoadPathKey which allows to control // the road with and the side falloff. var key = new TerrainRoadPathKey { Interpolation = SplineInterpolation.CatmullRom, Parameter = j, Point = position, // The width of the road at the path key. Width = RandomHelper.Random.NextFloat(6, 10), // The side falloff (which is used in ClampTerrainToRoad to blend the height values with // the road). SideFalloff = RandomHelper.Random.NextFloat(20, 40), }; _roadPath.Add(key); // Get next random position and direction. position += direction * RandomHelper.Random.NextFloat(20, 40); position.Y += RandomHelper.Random.NextFloat(-2, 2); direction = QuaternionF.CreateRotationY(RandomHelper.Random.NextFloat(-1, 1)) .Rotate(direction); } if (isClosed) { // To create a closed path the first and the last key should be identical. _roadPath[_roadPath.Count - 1].Point = _roadPath[0].Point; ((TerrainRoadPathKey)_roadPath[_roadPath.Count - 1]).Width = ((TerrainRoadPathKey)_roadPath[0]).Width; ((TerrainRoadPathKey)_roadPath[_roadPath.Count - 1]).SideFalloff = ((TerrainRoadPathKey)_roadPath[0]).SideFalloff; // Since the path is closed we do not have to fade out the start and the end of the road. _roadLayer.BorderBlendRange *= new Vector4F(1, 0, 1, 0); } // Convert the path to a mesh. Submesh roadSubmesh; Aabb roadAabb; float roadLength; TerrainRoadLayer.CreateMesh( GraphicsService.GraphicsDevice, _roadPath, 4, 10, 0.1f, out roadSubmesh, out roadAabb, out roadLength); // Set the mesh in the road layer. _roadLayer.SetMesh(roadSubmesh, roadAabb, roadLength, true); if (isClosed) { // When the path is closed, the end texture and the start texture coordinates should // match. This is the case if (roadLength / tileSize) is an integer. var numberOfTiles = (int)(roadLength / _roadLayer.TileSize); _roadLayer.TileSize = roadLength / numberOfTiles; } // The road layer is added to the layers of a tile. We have to add the road to each terrain // tile which it overlaps. foreach (var tile in _terrainObject.TerrainNode.Terrain.Tiles) if (GeometryHelper.HaveContact(tile.Aabb, _roadLayer.Aabb.Value)) tile.Layers.Add(_roadLayer); }
public static void ClampTerrainToRoad(HeightField terrain, Path3F road, float defaultWidth, float defaultSideFalloff, int maxNumberOfIterations, float tolerance) { if (terrain == null) { throw new ArgumentNullException("terrain"); } if (road == null) { throw new ArgumentNullException("road"); } // Compute list of line segments. (2 points per line segment!) var flattenedPoints = new List <Vector3F>(); road.Flatten(flattenedPoints, maxNumberOfIterations, tolerance); // Abort if path is empty. int numberOfLineSegments = flattenedPoints.Count / 2; if (numberOfLineSegments <= 0) { return; } // Compute accumulated lengths. (One entry for each entry in flattenedPoints.) float[] accumulatedLengths = new float[flattenedPoints.Count]; accumulatedLengths[0] = 0; for (int i = 1; i < flattenedPoints.Count; i += 2) { Vector3F previous = flattenedPoints[i - 1]; Vector3F current = flattenedPoints[i]; float length = (current - previous).Length; accumulatedLengths[i] = accumulatedLengths[i - 1] + length; if (i + 1 < flattenedPoints.Count) { accumulatedLengths[i + 1] = accumulatedLengths[i]; } } // Create a mapping between accumulatedLength and the path keys. // (accumulatedLength --> key) var pathLengthsAndKeys = new List <Pair <float, TerrainRoadPathKey> >(); { int index = 0; foreach (var key in road) { Vector3F position = key.Point; var roadKey = key as TerrainRoadPathKey; if (roadKey == null) { roadKey = new TerrainRoadPathKey { Point = key.Point, Width = defaultWidth, SideFalloff = defaultSideFalloff, }; } for (; index < flattenedPoints.Count; index++) { if (Vector3F.AreNumericallyEqual(position, flattenedPoints[index])) { pathLengthsAndKeys.Add(new Pair <float, TerrainRoadPathKey>(accumulatedLengths[index], roadKey)); break; } bool isLastLineSegment = (index + 2 == flattenedPoints.Count); if (!isLastLineSegment) { index++; } } index++; } } // Create a list of interpolated road widths and side falloffs. (One entry for each entry in flattenedPoints.) var halfWidths = new float[flattenedPoints.Count]; var sideFalloffs = new float[flattenedPoints.Count]; int previousKeyIndex = 0; var previousKey = pathLengthsAndKeys[0]; var nextKey = pathLengthsAndKeys[1]; halfWidths[0] = 0.5f * pathLengthsAndKeys[0].Second.Width; sideFalloffs[0] = pathLengthsAndKeys[0].Second.SideFalloff; for (int i = 1; i < flattenedPoints.Count; i += 2) { if (accumulatedLengths[i] > nextKey.First) { previousKey = nextKey; previousKeyIndex++; nextKey = pathLengthsAndKeys[previousKeyIndex + 1]; } float p = (accumulatedLengths[i] - previousKey.First) / (nextKey.First - previousKey.First); halfWidths[i] = 0.5f * InterpolationHelper.Lerp(previousKey.Second.Width, nextKey.Second.Width, p); sideFalloffs[i] = InterpolationHelper.Lerp(previousKey.Second.SideFalloff, nextKey.Second.SideFalloff, p); if (i + 1 < flattenedPoints.Count) { halfWidths[i + 1] = halfWidths[i]; sideFalloffs[i + 1] = sideFalloffs[i]; } } // Get AABB of road with the side falloff. Aabb aabbWithSideFalloffs; { Vector3F p = flattenedPoints[0]; float r = halfWidths[0] + sideFalloffs[0]; aabbWithSideFalloffs = new Aabb(new Vector3F(p.X - r, 0, p.Z - r), new Vector3F(p.X + r, 0, p.Z + r)); for (int i = 1; i < flattenedPoints.Count; i += 2) { p = flattenedPoints[i]; r = halfWidths[i] + sideFalloffs[i]; aabbWithSideFalloffs.Grow(new Vector3F(p.X - r, 0, p.Z - r)); aabbWithSideFalloffs.Grow(new Vector3F(p.X + r, 0, p.Z + r)); } } // Terrain properties. int numberOfSamplesX = terrain.NumberOfSamplesX; int numberOfSamplesZ = terrain.NumberOfSamplesZ; int numberOfCellsX = numberOfSamplesX - 1; int numberOfCellsZ = numberOfSamplesZ - 1; float widthX = terrain.WidthX; float cellSizeX = widthX / numberOfCellsX; float widthZ = terrain.WidthZ; float cellSizeZ = widthZ / numberOfCellsZ; float cellSizeDiagonal = (float)Math.Sqrt(cellSizeX * cellSizeX + cellSizeZ * cellSizeZ); bool isClosed = Vector3F.AreNumericallyEqual(flattenedPoints[0], flattenedPoints[flattenedPoints.Count - 1]); { // Get the line segments which of the road border. List <Vector4F> segments = new List <Vector4F>(); // 2 points per segment. Vector3F lastOrthonormal = Vector3F.Right; Vector4F previousV1 = Vector4F.Zero; Vector4F previousV2 = Vector4F.Zero; for (int i = 0; i < flattenedPoints.Count; i++) { Vector3F start = flattenedPoints[i]; Vector3F previous; bool isFirstPoint = (i == 0); if (!isFirstPoint) { previous = flattenedPoints[i - 1]; } else if (isClosed && road.SmoothEnds) { previous = flattenedPoints[flattenedPoints.Count - 2]; } else { previous = start; } Vector3F next; bool isLastPoint = (i + 1 == flattenedPoints.Count); if (!isLastPoint) { next = flattenedPoints[i + 1]; } else if (isClosed && road.SmoothEnds) { next = flattenedPoints[1]; } else { next = start; } Vector3F tangent = next - previous; Vector3F orthonormal = new Vector3F(tangent.Z, 0, -tangent.X); if (!orthonormal.TryNormalize()) { orthonormal = lastOrthonormal; } // Add 2 vertices two segments for the road side border. // // pV1 pV2 (previous vertices) // x x // | | // x x // v1 v2 (current vertices) // // We store the side falloff with the vertex: // Vectors are 4D. Height is y. Side falloff is w. Vector4F v1 = new Vector4F(start - orthonormal * (halfWidths[i] + 0), sideFalloffs[i]); Vector4F v2 = new Vector4F(start + orthonormal * (halfWidths[i] + 0), sideFalloffs[i]); if (i > 0) { segments.Add(previousV1); segments.Add(v1); segments.Add(previousV2); segments.Add(v2); if (isLastPoint && !isClosed) { // A segment for the end of the road. segments.Add(v1); segments.Add(v2); } } else { if (!isClosed) { // A segment for the start of the road. segments.Add(v1); segments.Add(v2); } } previousV1 = v1; previousV2 = v2; lastOrthonormal = orthonormal; // The flattened points list contains 2 points per line segment, which means that there // are duplicate intermediate points, which we skip. bool isLastLineSegment = (i + 2 == flattenedPoints.Count); if (!isLastLineSegment) { i++; } } // Apply the side falloff to the terrain heights. // We use a padding where the road influence is 100% because we want road width to be flat // but that means that we also have to set some triangle vertices outside the road width to // full 100% road height. float padding = cellSizeDiagonal; ClampHeightsToLineSegments(terrain, aabbWithSideFalloffs, segments, padding); } // Clamp the terrain heights to the inner part of the road. // We create quads for the road mesh and clamp the heights to the quad triangles. { Vector3F previousV1 = Vector3F.Zero; Vector3F previousV2 = Vector3F.Zero; Vector3F lastOrthonormal = Vector3F.Right; for (int i = 0; i < flattenedPoints.Count; i++) { Vector3F start = flattenedPoints[i]; Vector3F previous; bool isFirstPoint = (i == 0); if (!isFirstPoint) { previous = flattenedPoints[i - 1]; } else if (isClosed && road.SmoothEnds) { previous = flattenedPoints[flattenedPoints.Count - 2]; } else { previous = start; } Vector3F next; bool isLastPoint = (i + 1 == flattenedPoints.Count); if (!isLastPoint) { next = flattenedPoints[i + 1]; } else if (isClosed && road.SmoothEnds) { next = flattenedPoints[1]; } else { next = start; } Vector3F tangent = next - previous; Vector3F orthonormal = new Vector3F(tangent.Z, 0, -tangent.X); if (!orthonormal.TryNormalize()) { orthonormal = lastOrthonormal; } // Add 2 vertices to create a mesh like this: // // pV1 pV2 (previous vertices) // x---------------x // | | // x---------------x // v1 v2 (current vertices) // // Then we check all height samples against these triangles. // Vectors are 4D. Height is y. Influence is w. Vector3F v1 = start - orthonormal * halfWidths[i]; Vector3F v2 = start + orthonormal * halfWidths[i]; if (i > 0) { ClampHeightsToQuad(terrain, previousV1, previousV2, v1, v2); } previousV1 = v1; previousV2 = v2; lastOrthonormal = orthonormal; // The flattened points list contains 2 points per line segment, which means that there // are duplicate intermediate points, which we skip. bool isLastLineSegment = (i + 2 == flattenedPoints.Count); if (!isLastLineSegment) { i++; } } } terrain.Invalidate(); }
public static void ClampTerrainToRoad(HeightField terrain, Path3F road, float defaultWidth, float defaultSideFalloff, int maxNumberOfIterations, float tolerance) { if (terrain == null) throw new ArgumentNullException("terrain"); if (road == null) throw new ArgumentNullException("road"); // Compute list of line segments. (2 points per line segment!) var flattenedPoints = new List<Vector3F>(); road.Flatten(flattenedPoints, maxNumberOfIterations, tolerance); // Abort if path is empty. int numberOfLineSegments = flattenedPoints.Count / 2; if (numberOfLineSegments <= 0) return; // Compute accumulated lengths. (One entry for each entry in flattenedPoints.) float[] accumulatedLengths = new float[flattenedPoints.Count]; accumulatedLengths[0] = 0; for (int i = 1; i < flattenedPoints.Count; i += 2) { Vector3F previous = flattenedPoints[i - 1]; Vector3F current = flattenedPoints[i]; float length = (current - previous).Length; accumulatedLengths[i] = accumulatedLengths[i - 1] + length; if (i + 1 < flattenedPoints.Count) accumulatedLengths[i + 1] = accumulatedLengths[i]; } // Create a mapping between accumulatedLength and the path keys. // (accumulatedLength --> key) var pathLengthsAndKeys = new List<Pair<float, TerrainRoadPathKey>>(); { int index = 0; foreach (var key in road) { Vector3F position = key.Point; var roadKey = key as TerrainRoadPathKey; if (roadKey == null) { roadKey = new TerrainRoadPathKey { Point = key.Point, Width = defaultWidth, SideFalloff = defaultSideFalloff, }; } for (; index < flattenedPoints.Count; index++) { if (Vector3F.AreNumericallyEqual(position, flattenedPoints[index])) { pathLengthsAndKeys.Add(new Pair<float, TerrainRoadPathKey>(accumulatedLengths[index], roadKey)); break; } bool isLastLineSegment = (index + 2 == flattenedPoints.Count); if (!isLastLineSegment) index++; } index++; } } // Create a list of interpolated road widths and side falloffs. (One entry for each entry in flattenedPoints.) var halfWidths = new float[flattenedPoints.Count]; var sideFalloffs = new float[flattenedPoints.Count]; int previousKeyIndex = 0; var previousKey = pathLengthsAndKeys[0]; var nextKey = pathLengthsAndKeys[1]; halfWidths[0] = 0.5f * pathLengthsAndKeys[0].Second.Width; sideFalloffs[0] = pathLengthsAndKeys[0].Second.SideFalloff; for (int i = 1; i < flattenedPoints.Count; i += 2) { if (accumulatedLengths[i] > nextKey.First) { previousKey = nextKey; previousKeyIndex++; nextKey = pathLengthsAndKeys[previousKeyIndex + 1]; } float p = (accumulatedLengths[i] - previousKey.First) / (nextKey.First - previousKey.First); halfWidths[i] = 0.5f * InterpolationHelper.Lerp(previousKey.Second.Width, nextKey.Second.Width, p); sideFalloffs[i] = InterpolationHelper.Lerp(previousKey.Second.SideFalloff, nextKey.Second.SideFalloff, p); if (i + 1 < flattenedPoints.Count) { halfWidths[i + 1] = halfWidths[i]; sideFalloffs[i + 1] = sideFalloffs[i]; } } // Get AABB of road with the side falloff. Aabb aabbWithSideFalloffs; { Vector3F p = flattenedPoints[0]; float r = halfWidths[0] + sideFalloffs[0]; aabbWithSideFalloffs = new Aabb(new Vector3F(p.X - r, 0, p.Z - r), new Vector3F(p.X + r, 0, p.Z + r)); for (int i = 1; i < flattenedPoints.Count; i += 2) { p = flattenedPoints[i]; r = halfWidths[i] + sideFalloffs[i]; aabbWithSideFalloffs.Grow(new Vector3F(p.X - r, 0, p.Z - r)); aabbWithSideFalloffs.Grow(new Vector3F(p.X + r, 0, p.Z + r)); } } // Terrain properties. int numberOfSamplesX = terrain.NumberOfSamplesX; int numberOfSamplesZ = terrain.NumberOfSamplesZ; int numberOfCellsX = numberOfSamplesX - 1; int numberOfCellsZ = numberOfSamplesZ - 1; float widthX = terrain.WidthX; float cellSizeX = widthX / numberOfCellsX; float widthZ = terrain.WidthZ; float cellSizeZ = widthZ / numberOfCellsZ; float cellSizeDiagonal = (float)Math.Sqrt(cellSizeX * cellSizeX + cellSizeZ * cellSizeZ); bool isClosed = Vector3F.AreNumericallyEqual(flattenedPoints[0], flattenedPoints[flattenedPoints.Count - 1]); { // Get the line segments which of the road border. List<Vector4F> segments = new List<Vector4F>(); // 2 points per segment. Vector3F lastOrthonormal = Vector3F.Right; Vector4F previousV1 = Vector4F.Zero; Vector4F previousV2 = Vector4F.Zero; for (int i = 0; i < flattenedPoints.Count; i++) { Vector3F start = flattenedPoints[i]; Vector3F previous; bool isFirstPoint = (i == 0); if (!isFirstPoint) previous = flattenedPoints[i - 1]; else if (isClosed && road.SmoothEnds) previous = flattenedPoints[flattenedPoints.Count - 2]; else previous = start; Vector3F next; bool isLastPoint = (i + 1 == flattenedPoints.Count); if (!isLastPoint) next = flattenedPoints[i + 1]; else if (isClosed && road.SmoothEnds) next = flattenedPoints[1]; else next = start; Vector3F tangent = next - previous; Vector3F orthonormal = new Vector3F(tangent.Z, 0, -tangent.X); if (!orthonormal.TryNormalize()) orthonormal = lastOrthonormal; // Add 2 vertices two segments for the road side border. // // pV1 pV2 (previous vertices) // x x // | | // x x // v1 v2 (current vertices) // // We store the side falloff with the vertex: // Vectors are 4D. Height is y. Side falloff is w. Vector4F v1 = new Vector4F(start - orthonormal * (halfWidths[i] + 0), sideFalloffs[i]); Vector4F v2 = new Vector4F(start + orthonormal * (halfWidths[i] + 0), sideFalloffs[i]); if (i > 0) { segments.Add(previousV1); segments.Add(v1); segments.Add(previousV2); segments.Add(v2); if (isLastPoint && !isClosed) { // A segment for the end of the road. segments.Add(v1); segments.Add(v2); } } else { if (!isClosed) { // A segment for the start of the road. segments.Add(v1); segments.Add(v2); } } previousV1 = v1; previousV2 = v2; lastOrthonormal = orthonormal; // The flattened points list contains 2 points per line segment, which means that there // are duplicate intermediate points, which we skip. bool isLastLineSegment = (i + 2 == flattenedPoints.Count); if (!isLastLineSegment) i++; } // Apply the side falloff to the terrain heights. // We use a padding where the road influence is 100% because we want road width to be flat // but that means that we also have to set some triangle vertices outside the road width to // full 100% road height. float padding = cellSizeDiagonal; ClampHeightsToLineSegments(terrain, aabbWithSideFalloffs, segments, padding); } // Clamp the terrain heights to the inner part of the road. // We create quads for the road mesh and clamp the heights to the quad triangles. { Vector3F previousV1 = Vector3F.Zero; Vector3F previousV2 = Vector3F.Zero; Vector3F lastOrthonormal = Vector3F.Right; for (int i = 0; i < flattenedPoints.Count; i++) { Vector3F start = flattenedPoints[i]; Vector3F previous; bool isFirstPoint = (i == 0); if (!isFirstPoint) previous = flattenedPoints[i - 1]; else if (isClosed && road.SmoothEnds) previous = flattenedPoints[flattenedPoints.Count - 2]; else previous = start; Vector3F next; bool isLastPoint = (i + 1 == flattenedPoints.Count); if (!isLastPoint) next = flattenedPoints[i + 1]; else if (isClosed && road.SmoothEnds) next = flattenedPoints[1]; else next = start; Vector3F tangent = next - previous; Vector3F orthonormal = new Vector3F(tangent.Z, 0, -tangent.X); if (!orthonormal.TryNormalize()) orthonormal = lastOrthonormal; // Add 2 vertices to create a mesh like this: // // pV1 pV2 (previous vertices) // x---------------x // | | // x---------------x // v1 v2 (current vertices) // // Then we check all height samples against these triangles. // Vectors are 4D. Height is y. Influence is w. Vector3F v1 = start - orthonormal * halfWidths[i]; Vector3F v2 = start + orthonormal * halfWidths[i]; if (i > 0) ClampHeightsToQuad(terrain, previousV1, previousV2, v1, v2); previousV1 = v1; previousV2 = v2; lastOrthonormal = orthonormal; // The flattened points list contains 2 points per line segment, which means that there // are duplicate intermediate points, which we skip. bool isLastLineSegment = (i + 2 == flattenedPoints.Count); if (!isLastLineSegment) i++; } } terrain.Invalidate(); }