Ландшафт с Геометрией Clipmaps в XNA (Перевод) Часть 4

Оригинал: http://www.ziggyware.com/readarticle.php?article_id=220

Вот так. Теперь мы должны использовать класс, чтобы создать множество clipmaps и нарисовать его. Это сделано в классе TerrainComponent, который является очень маленьким и говорит сам за себя.

using System; 
using System.Collections.Generic; 
using System.Text; 
using Microsoft.Xna.Framework; 
using Microsoft.Xna.Framework.Graphics; 
using GeoClipmapping; 
namespace GeoClipmapping.Components 
{ 
 public class TerrainComponent : DrawableGameComponent 
 { 
 // The used camera. 
 private Camera camera; 
 // Heightmap and heightdata to store the terrainvalues. 
 private Texture2D heightmap; 
 private float[] heightdata; 
 // Clipmap parameters and a list of clipmaps that are used. 
 private int L = 5; 
 private int N = 127; 
 private float S = 64; 
 private int mapsize; 
 private List<Clipmap> clips; 
 // Graphical stuff. 
 private GraphicsDevice device; 
 private Effect effect; 
 /// <summary> 
 /// Creates a terraincomponent that can update and draw terrain. 
 /// </summary> 
 /// <param name="game"></param> 
 public TerrainComponent(Game game) 
 : base(game) 
 { 
 } 
 /// <summary> 
 /// Initializes the component 
 /// </summary> 
 public override void Initialize() 
 { 
 // get camera that is a registered service. 
 camera = Game.Services.GetService(typeof(Camera)) as Camera; 
 if (camera == null) 
 { 
 throw new InvalidOperationException("Cameraservice not found."); 
 } 
 base.Initialize(); 
 } 
 /// <summary> 
 /// Loads needed graphical content like effect and the heightmap. 
 /// </summary> 
 protected override void LoadContent() 
 { 
 // load effect and heightmap 
 effect = Game.Content.Load<Effect>("Effects//Terraineffect"); 
 heightmap = Game.Content.Load<Texture2D>("Heightmaps//Heightmap00"); 
 // create a dataarray to hold the values from heightmaptexture 
 heightdata = new float[heightmap.Width * heightmap.Height]; 
 // put heightmapvalues to the created array 
 heightmap.GetData<float>(heightdata); 
 mapsize = heightmap.Width; 
 device = Game.GraphicsDevice; 
 // Create and initialize the clipmaplevels. 
 clips = new List<Clipmap>(); 
 for (int i = 0; i < L; i++) 
 { 
 clips.Add(new Clipmap(i, N, S, mapsize, ref heightdata, device)); 
 } 
 base.LoadContent(); 
 } 
 /// <summary> 
 /// Updates all clipmaps. 
 /// </summary> 
 /// <param name="gameTime"></param> 
 public override void Update(GameTime gameTime) 
 { 
 // Update vertices of all clipmaps first. 
 foreach (Clipmap clip in clips) 
 { 
 // the clipmapcenter is always the cameras position. 
 clip.UpdateVertices(camera.Position); 
 } 
 // Update indices now. 
 for (int i = 0; i < clips.Count; i++) 
 { 
 if (i == 0) 
 { 
 // Level 0 has no nested level, so pass null as parameter. 
 clips[i].UpdateIndices(null, camera.ViewFrustum); 
 } 
 else 
 { 
 // All other levels i have the level i-1 nested in. 
 clips[i].UpdateIndices(clips[i - 1], camera.ViewFrustum); 
 } 
 } 
 base.Update(gameTime); 
 } 

Самое важное в этом классе – метод прорисовки. Я использовал метод DrawUSERIndexedPrimitives для прорисовки уровней. Этот метод медленнее, чем прорисовка примитивов с помощью DrawIndexedPrimitves, потому что в пользовательском режиме мы не используем буферы вершин и индексов, которые бы распределяли память на графической карте, мы передаем только те массивы, которые хранятся в оперативной памяти. Так как наши вершина и массив индексов постоянно изменяются, мы должны использовать метод DrawUSERIndexedPrimitive.

Другим вариантом может быть использование DynamicVertex и IndexBuffers из XNA, где возможно менять буферы и использовать более быстрый метод DrawIndexed. Но изменение данных, которые уже находятся в GPU, может вызвать остановы, в то время как GPU занят на том буфере. Есть некоторые решения, как избежать этого, я рекомендую прочитать блог Shawns особенно этот раздел, но у меня не получилось достичь большей эффективности с использованием динамических буферов, поэтому я продолжаю использовать метод DrawUSERIndexed. Если кто-то из вас знает способ сделать более эффективным команду перерисовки с динамическими буферами, сообщите мне.

/// <summary> 
 /// Draws the terrain to screen. 
 /// </summary> 
 /// <param name="gameTime"></param> 
 public override void Draw(GameTime gameTime) 
 { 
 // prepare device 
 device.RenderState.DepthBufferEnable = true; 
 device.RenderState.DepthBufferFunction = CompareFunction.Less; 
 device.VertexDeclaration = clips[0].VertexDeclaration; 
 device.RenderState.FillMode = FillMode.WireFrame; 
 // preapare effect 
 effect.Parameters["World"].SetValue(Matrix.Identity); 
 effect.Parameters["View"].SetValue(camera.View); 
 effect.Parameters["Projection"].SetValue(camera.Projection); 
 // draw levels from last to first. 
 for (int i = clips.Count - 1; i >= 0; i--) 
 { 
 Clipmap clip = clips[i]; 
 if (clip.Triangles > 0) 
 { 
 effect.Begin(); 
 foreach (EffectPass pass in effect.CurrentTechnique.Passes) 
 { 
 pass.Begin(); 
 device.DrawUserIndexedPrimitives( 
 clip.PrimitiveType, 
 clip.Vertices, 
 0, 
 clip.Vertices.Length, 
 clip.Indices, 
 0, 
 clip.Triangles); 
 pass.End(); 
 } 
 effect.End(); 
 } 
 } 
 base.Draw(gameTime); 
 } 
 } 
} 

Во всяком случае, нам необходим файл эффектов. Здесь представлены только его основы, потому что мы рисуем на тот раз ландшафт в каркасном виде. В следующей главе мы заполним этот файл.

Поместите это в папку «Effect» в содержимое проекта.

float4x4 World; 
float4x4 View; 
float4x4 Projection; 
struct VertexShaderInput 
{ 
 float4 Position : POSITION0; 
}; 
struct VertexShaderOutput 
{ 
 float4 Position : POSITION0; 
}; 
VertexShaderOutput VertexShaderFunction(VertexShaderInput input) 
{ 
 VertexShaderOutput output; 
 // the w value is the second height of a vertex. In this effect we dont use that 
 // but we must set the w value to 1.0f to get correct matrixresults. 
 input.Position.w = 1.0f; 
 float4 worldPosition = mul(input.Position, World); 
 float4 viewPosition = mul(worldPosition, View); 
 output.Position = mul(viewPosition, Projection); 
 return output; 
} 
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0 
{ 
 return float4(1, 1, 1, 1); 
} 
technique Technique1 
{ 
 pass Pass1 
 { 
 VertexShader = compile vs_1_1 VertexShaderFunction(); 
 PixelShader = compile ps_1_1 PixelShaderFunction(); 
 } 
} 
Карты высот - Heightmaps 
Чтобы нарисовать ландшафт, нам нужна карта heightmap. Я говорил раньше, что я предпочитаю формат R32F, поэтому я оставил его в проекте. Этот формат очень полезен, потому что использование чисел с плавающей запятой очень удобно. Мы только должны взять значение и взвесить его с желаемой высотой. Если вы используете этот формат изображения, не забывайте переключать значение "Texture Format" содержимого процессора на "NoChange". 
Другие форматы, такие как Color или Byte и т.д. нуждаются в дополнительных операциях. Но я знаю, что большинство из вас использует BMP файлы, и не у всех есть photoshop с плагином DDS и есть возможность создать файлы R32F, поэтому вот очень короткое решение: добавьте свои битовые карты к содержимому проекта и удостоверьтесь, что в значении "Texture Format" содержимого процессора стоит "Color". Добавьте метод к TerrainComponent, который преобразовывает данные вашей битового карты в массив со числами с плавающей запятой. Тем самым, нам не нужно менять реализацию Clipmap. Тогда вам нужно будет только вызвать тот метод с загруженной структурой вместо "texture.GetData". 
private float[] GetData(Texture2D texture) 
{ 
 float[] result = new float[texture.Width * texture.Height]; 
 Color[] data = new Color[result.Length]; 
 texture.GetData<Color>(data); 
 for (int i = 0; i < data.Length; i++) 
 { 
 result[i] = data[i].ToVector3().Length(); 
 } 
 return result; 
} 

Если вы запустите код сейчас, то вы увидите каркасный ландшафт. Вы также заметите различные уровни.

Добавление теней

Следующий шаг это добавление теней, чтобы нарисовать текстурированный ландшафт и, наконец, выполнить геоморфинг, чтобы избежать трещин между уровнями. Мы также хотим выполнить основное освещение, и некоторые из вас могут удивиться этому, ведь мы еще не создали нормали. И вы правы. Мы передаем только положение и дополнительное значение высоты графической карте. И прежде, чем мы напишем шейдер с освещением, нам необходимо передать некоторые значения нормалей для каждого положения на карте высот heightmap. Я предпочитаю предварительно вычислять всю карту нормалей normalmap и сохранять ее как структуру на графической карте. Таким образом, мы заканчиваем с картой высот heightmap, а карта нормалей normalmap определяет наш ландшафт. Все, что мы должны сделать — это сказать шейдеру, какую карту нормалей использовать, и затем только искать нормаль в определенном положении.

Вот код, как карта нормалей может быть создана. Он представляет собой итерации по всем пикселям карты высот heightmap и создает нормали, используя очень простой поперечный фильтр для каждого пикселя. Вы можете заменить этот фильтр более сложным алгоритмом, чтобы добиться лучшего результата.

/// <summary> 
/// Gets the normal map from an existing heightmap. This method uses a simple cross 
/// filter which is correct on horizontal and vertical, but not on diagonal. 
/// </summary> 
/// <param name="device">The graphicsdevice to use for the normalmap.</param> 
/// <param name="heightfield">Heightvalue to generate normals from.</param> 
/// <param name="maxHeight">The maximum height that the heightfield can reach.</param> 
/// <param name="width">Width of the heightfield</param> 
/// <param name="length">Length of the heightfield</param> 
/// <returns></returns> 
public static Texture2D GetNormalMap(GraphicsDevice device, float[] heightfield, 
 float maxHeight, int width, int length) 
{ 
 // Allocate space for the normals in Rgba32 format where every entry has 
 // a Red Green Blue and Alpha channel. We wont use the Alpha channel so 
 // that space is wasted. We dont care because that image is created only once. 
 Rgba32[] normalfield = new Rgba32[heightfield.Length]; 
 // Iterate through all positions in the heightfield and create a normal at that position. 
 int index = 0; 
 for (int y = 0; y < length; y++) 
 { 
 for (int x = 0; x < width; x++) 
 { 
 Vector4 normal = SimpleCrossFilter(x, y, ref heightfield, maxHeight, width, length); 
 normalfield[index++] = new Rgba32(normal); 
 } 
 } 
 // Create a new image and fill that with the values we just created. 
 Texture2D result = new Texture2D(device, width, length); 
 result.SetData<Rgba32>(normalfield); 
 return result; 
} 
/// <summary> 
/// Simple cross filter that calculates a normal at a specified position. 
/// This algorithm is correct on horizontal and vertical but not correct 
/// on diagonal. 
/// </summary> 
/// <param name="x">x-coordinate</param> 
/// <param name="y">y-coordinate</param> 
/// <param name="heightfield">Heightfield to create normalp from.</param> 
/// <param name="normalStrength">The maximum height the heightfield can reach.</param> 
/// <param name="width">Width of the heightfield.</param> 
/// <param name="length">Length of the heightfield.</param> 
/// <returns></returns> 
public static Vector4 SimpleCrossFilter(int x, int y, ref float[] heightfield, 
 float normalStrength, int width, int length) 
{ 
 // Create four positions around the specified position 
 Point[] pos = new Point[] 
 { 
 new Point(x - 1, y), // left 
 new Point(x + 1, y), // right 
 new Point(x, y - 1), // higher 
 new Point(x, y + 1), // lower 
 }; 
 // Get the heightvalues at the four positions we just created 
 float[] heights = new float[4]; 
 for (int i = 0; i < 4; i++) 
 { 
 // Check if we can access the array with the current coordinates 
 if (pos[i].X >= 0 && pos[i].X < width && 
 pos[i].Y >= 0 && pos[i].Y < length) 
 { 
 int j = pos[i].X + pos[i].Y * width; 
 heights[i] = heightfield[j]; 
 } 
 else 
 { 
 // If not, then set value to zero. 
 heights[i] = 0; 
 } 
 } 
 // Perform simple cross filter. 
 float dx = heights[0] - heights[1]; 
 float dz = heights[2] - heights[3]; 
 float hy = 1.0f / normalStrength; 
 // Create and normalize the final normal 
 Vector3 normal = new Vector3(dx, hy, dz); 
 normal.Normalize(); 
 return new Vector4(normal, 0.0f); 
} 
Реклама
Запись опубликована в рубрике Uncategorized. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s