Skip to content

Instantly share code, notes, and snippets.

@TomGroner
Created January 11, 2019 14:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TomGroner/508077dc7620738ad3f2c3e28c8a7890 to your computer and use it in GitHub Desktop.
Save TomGroner/508077dc7620738ad3f2c3e28c8a7890 to your computer and use it in GitHub Desktop.
An example of generating terrain using a height map in Xenko, salvaged from a project on a dead hard drive.
using Xenko.Core.Mathematics;
using Xenko.Engine;
using Xenko.Extensions;
using Xenko.Graphics;
using Xenko.Graphics.GeometricPrimitives;
using Xenko.Rendering;
using XenkoTerrain.Extensions;
using System.Collections.Generic;
namespace XenkoTerrain.Graphics
{
/// <summary>
/// An example of how to use the TerrainGeometryBuilder.
/// </summary>
/// <remarks>
/// I am posting this as a gist because of a hard drive crash resulting in the full project being
/// lost. A handful of pieces are missing that I no longer have the source for. I plan to re-make the
/// project using the code I was able to salvage, but the following needs to be re-coded / re-researched:
///
/// * The Texture used to obtain the Image needs to have properties set in Xenko Studio for the
/// PixelBuffer to have values that the builder expects. I believe they are:
/// - RGB sampling = true
/// - Generate mipmaps = false
/// - Compress = false
///
/// However, this is from memory and may be entirely wrong :( In-fact, it may not even be a color
/// image, it may need to be greyscale. Tinkering is required on the texture's settings for this to
/// work as intended.
/// </remarks>
public class ExampleUsage
{
/// <summary>
/// Create the height map terrain and adds it to the provided entity as a ModelComponent.
/// </summary>
/// <param name="entity">Entity to add the terrain to.</param>
/// <param name="settings">A placeholder for something that has the settings in the game</param>
/// <remarks>This is a usage that might be called within a StartupScript or SyncScript.</remarks>
public void AddTerrainToEntity(Entity entity, HeightMapSettings settings)
{
var terrainBuilder = new TerrainGeometryBuilder(settings.Device, settings.Size, settings.HeightMap, settings.MaxHeight);
var terrainModel = terrainBuilder.BuildTerrainModel();
entity.Components.Add(terrainModel);
}
/// <summary>
/// Create the height map geometry for various usages.
/// </summary>
/// <param name="settings">A placeholder for something that has the settings in the game</param>
/// <remarks>This is a useage that might be called by a custom RootRenderFeature when it builds a RenderObject.</remarks>
public GeometricPrimitive GetTerrainGeometry(HeightMapSettings settings)
{
var terrainBuilder = new TerrainGeometryBuilder(settings.Device, settings.Size, settings.HeightMap, settings.MaxHeight);
return terrainBuilder.BuildTerrain();
}
/// <summary>
/// An example of how to get the image that is passed to the TerrainGeometryBuilder, obtained
/// from a Texture, such as a property on a component.
/// </summary>
/// <param name="heightMapTexture">Texture to make the terrain from</param>
/// <param name="heightMapImage">Image from the texture needed by the builder.</param>
/// <returns>True if the image was obtained, otherwise false.</returns>
public bool TryGetHeightMapImageData(Texture heightMapTexture, out Image heightMapImage)
{
try // TODO: When used from a SyncScript this always errors on first attempt.
// See if there is something that can be checked and awaited instead of this sloppy hack.
{
heightMapImage = heightMapTexture.GetDataAsImage(Game.GraphicsContext.CommandList);
}
catch (Exception)
{
heightMapImage = null;
}
return heightMapImage != null;
}
}
public class TerrainGeometryBuilder
{
public const float MaxPixelColor = 256 * 256 * 256;
private PixelBuffer heightMapPixelBuffer;
public TerrainGeometryBuilder(GraphicsDevice graphicsDevice, float size, Image heightMapImage, float maxHeight)
{
heightMapPixelBuffer = heightMapImage.PixelBuffer[0];
GraphicsDevice = graphicsDevice;
Size = size;
HeightMapImage = heightMapImage;
MaxHeight = maxHeight;
}
public GraphicsDevice GraphicsDevice { get; }
public float Size { get; }
public Image HeightMapImage { get; }
public float MaxHeight { get; }
public ModelComponent BuildTerrainModel()
{
return BuildModel(BuildTerrain());
}
private ModelComponent BuildModel(GeometricPrimitive primitive)
{
var mesh = new Mesh(primitive.ToMeshDraw(), new ParameterCollection());
var model = new Model() { Meshes = new List<Mesh>(new[] { mesh }) };
return new ModelComponent(model);
}
public GeometricPrimitive BuildTerrain()
{
var data = GenerateTerrainGeometry(HeightMapImage.Description.Width, HeightMapImage.Description.Height, Size, false);
return new GeometricPrimitive(GraphicsDevice, data);
}
/// <summary>
/// Uses the height of neighboring points to get a pretty-close-to-realistic normal for a given point.
/// </summary>
private Vector3 GetNormal(int x, int y)
{
// TODO: Pre-calculate these or cache calls to GetHeight() to re-use values
var heightL = GetHeight(x - 1, y);
var heightR = GetHeight(x + 1, y);
var heightD = GetHeight(x, y - 1);
var heightU = GetHeight(x, y + 1);
var normal = new Vector3(heightL - heightR, 2f, heightD - heightU);
normal.Normalize();
return normal;
}
/// <summary>
/// Gets the height of a given point by looking in the height map pixel buffer
/// </summary>
private float GetHeight(int x, int y)
{
if (x < 0 || x >= HeightMapImage.Description.Height || y < 0 || y >= HeightMapImage.Description.Width)
{
// In-case we're trying to calculate a normal for a point on the edge and there is no neighbor
return 0;
}
float height = heightMapPixelBuffer.GetPixel<Color>(x, y).ToRgb();
height += MaxPixelColor / 2f;
height /= MaxPixelColor / 2f;
return height * MaxHeight - MaxHeight;
}
/// <summary>
/// Creates the height map terrain.
/// </summary>
/// <param name="tessellationX">The number of quads to split the mesh along the X-axis. </param>
/// <param name="tessellationY">The number of quads to split the mesh along the Y-axis (or really, Z)</param>
/// <param name="size">The size (squared) of the terrain in Xenko units.</param>
/// <param name="generateBackFace">Generate a backface (true) or cull the backface (false).</param>
/// <returns>Geometric mesh data for the terrain</returns>
/// <remarks>
/// Nearly all of this was take from the engine code at:
/// https://github.com/xenko3d/xenko/blob/master/sources/engine/Xenko.Graphics/GeometricPrimitives/GeometricPrimitive.Plane.cs
///
/// Modifications to the height and normals were then added using the height map.
/// </remarks>
private GeometricMeshData<VertexPositionNormalTexture> GenerateTerrainGeometry(int tessellationX, int tessellationY, float size, bool generateBackFace)
{
var lineWidth = (tessellationX + 1);
var lineHeight = (tessellationY + 1);
var vertices = new VertexPositionNormalTexture[lineWidth * lineHeight * (generateBackFace ? 2 : 1)];
var indices = new int[tessellationX * tessellationY * 6 * (generateBackFace ? 2 : 1)];
var deltaX = size / tessellationX;
var deltaY = size / tessellationY;
size /= 2.0f;
var vertexCount = 0;
var indexCount = 0;
// Create vertices
var uv = new Vector2(1f, 1f);
for (var y = 0; y < (tessellationY + 1); y++)
{
for (var x = 0; x < (tessellationX + 1); x++)
{
var height = x == 0 || y == 0 ? 0.1f : GetHeight(x, y);
var position = new Vector3(-size + deltaX * x, height, -size + deltaY * y);
var normal = GetNormal(x, y);
var texCoord = new Vector2(uv.X * x / tessellationX, uv.Y * y / tessellationY);
vertices[vertexCount++] = new VertexPositionNormalTexture(position, normal, texCoord);
}
}
// Create indices
for (var y = 0; y < tessellationY; y++)
{
for (var x = 0; x < tessellationX; x++)
{
// Six indices (two triangles) per face.
var vbase = lineWidth * y + x;
indices[indexCount++] = (vbase + 1);
indices[indexCount++] = (vbase + 1 + lineWidth);
indices[indexCount++] = (vbase + lineWidth);
indices[indexCount++] = (vbase + 1);
indices[indexCount++] = (vbase + lineWidth);
indices[indexCount++] = (vbase);
}
}
if (generateBackFace)
{
var numVertices = lineWidth * lineHeight;
for (var y = 0; y < (tessellationY + 1); y++)
{
for (var x = 0; x < (tessellationX + 1); x++)
{
var baseVertex = vertices[vertexCount - numVertices];
var position = new Vector3(baseVertex.Position.X, baseVertex.Position.Y, baseVertex.Position.Z);
var texCoord = new Vector2(uv.X * x / tessellationX, uv.Y * y / tessellationY);
var normal = baseVertex.Normal;
vertices[vertexCount++] = new VertexPositionNormalTexture(position, -normal, texCoord);
}
}
for (var y = 0; y < tessellationY; y++)
{
for (var x = 0; x < tessellationX; x++)
{
var vbase = lineWidth * (y + tessellationY + 1) + x;
indices[indexCount++] = (vbase + 1);
indices[indexCount++] = (vbase + lineWidth);
indices[indexCount++] = (vbase + 1 + lineWidth);
indices[indexCount++] = (vbase + 1);
indices[indexCount++] = (vbase);
indices[indexCount++] = (vbase + lineWidth);
}
}
}
return new GeometricMeshData<VertexPositionNormalTexture>(vertices, indices, false) { Name = "Terrain" };
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment