Created
January 11, 2019 14:30
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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