|
|
|
|

Adding normals to our Terrain – Intuitive approach |
We’ll be adding normal data to all vertices of our terrain, so our graphics card can perform some lighting calculations on it. As seen in the previous chapter, we will need to add a normal to each of our vertices.
The remainder of this chapter is explained in much more detail in Recipe 5-7
This normal should be perpendicular to the triangle of the vertex. In cases where the vertex is shared among multiple triangles (as in our terrain), you should find the normal of all triangles that use the vertex, and store the average of those normals in the vertex. First we have to reload our code from the ‘Adding colors’ chapter, after which we need to add the struct (which allows normal data to be added to our vectors) to the top of our class:
public struct VertexPositionNormalColored { public Vector3 Position; public Color Color; public Vector3 Normal; public static int SizeInBytes = 7 * 4; public static VertexElement[] VertexElements = new VertexElement[] { new VertexElement( 0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0 ), new VertexElement( 0, sizeof(float) * 3, VertexElementFormat.Color, VertexElementMethod.Default, VertexElementUsage.Color, 0 ), new VertexElement( 0, sizeof(float) * 4, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0 ), }; }
Don’t forget to update our vertices declaration:
VertexPositionNormalColored[] vertices;
As well as our VertexDeclaration in our SetUpVertices method:
myVertexDeclaration = new VertexDeclaration(device, VertexPositionNormalColored.VertexElements);
And the line that actually renders your triangles in our Draw method:
device.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length, indices, 0, indices.Length / 3);
Now we’re ready to calculate the normals, which will be done in the CalculateNormals method. Start by clearing the normals in each of your vertices:
private void CalculateNormals() { for (int i = 0; i < vertices.Length; i++) vertices[i].Normal = new Vector3(0, 0, 0); }
Next, you’re going to scroll through each of your triangles. For each triangle, you will calculate its normal. Finally, you will add this normal to the normal each of the triangle’s 3 vertices:
for (int i = 0; i < indices.Length / 3; i++) { int index1 = indices[i * 3]; int index2 = indices[i * 3 + 1]; int index3 = indices[i * 3 + 2]; Vector3 side1 = vertices[index1].Position - vertices[index3].Position; Vector3 side2 = vertices[index1].Position - vertices[index2].Position; Vector3 normal = Vector3.Cross(side1, side2); vertices[index1].Normal += normal; vertices[index2].Normal += normal; vertices[index3].Normal += normal; }
You look up the indices for the 3 vertices of the triangle. If you know 2 sides of a triangle, you can find its normal by taking the cross product of these 2 sides. Given any 2 vectors, their cross product gives you’re the vector that is perpendicular to both vectors.
So first you find 2 sides of the triangle, by subtracting the position from one corner from the position of another. Once you know the normal for the triangle, you add it to the normal of each of the 3 vertices.
At this point, all vertices contain huge normal vectors, while they need to be of unity length. So end by normalizing all of them:
for (int i = 0; i < vertices.Length; i++) vertices[i].Normal.Normalize();
That’s it for the normals! Don’t forget to call this method at the end of our LoadContent method:
CalculateNormals();
In your Draw method, turn on the light:
Vector3 lightDirection = new Vector3(1.0f, -1.0f, -1.0f); lightDirection.Normalize(); effect.Parameters["xLightDirection"].SetValue(lightDirection); effect.Parameters["xAmbient"].SetValue(0.1f);
The last line turns on ambient lighting, so even the parts of the terrain that are not lit at all by the directional light still receive some lighting. When you run this code, you should see the image below:

Click here to go to the forum on this chapter!
Or click on one of the topics on this chapter to go there: Strange light's movements I followed the series 1 tutorials and I created a ...Error in Terrain Lighting Following the tutorial and when I got to terrain l...Problems after applying updated tutorial Hi reimer,
The normals of my multitextured terr...Problem loading bigger heightmap Hi,
I copy/pasted the code provided in the "Te...Fix @ commented out some effects.fx Hey guys, I had to comment out the following lines...Lighting I can't see the lighting I tried to copy the code...Lighting incompatible on DX8.1 Thanks Reimer. I read somewhere that someone said...Question of Calculate Normal hi, in the 3-picture illustration, i understand t...normals hi i am new to XNA GSE and i have been
following...
As you can see, adding correct lighting dramatically adds more 3D feeling to your scene.
Chapter 6 is completely dedicated to lighting a 3D scene.
The final code of our project:
using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Net; using Microsoft.Xna.Framework.Storage; namespace XNAtutorial { public class Game1 : Microsoft.Xna.Framework.Game {
public struct VertexPositionNormalColored { public Vector3 Position; public Color Color; public Vector3 Normal; public static int SizeInBytes = 7 * 4; public static VertexElement[] VertexElements = new VertexElement[] { new VertexElement( 0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0 ), new VertexElement( 0, sizeof(float) * 3, VertexElementFormat.Color, VertexElementMethod.Default, VertexElementUsage.Color, 0 ), new VertexElement( 0, sizeof(float) * 4, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0 ), }; }
GraphicsDeviceManager graphics; SpriteBatch spriteBatch; GraphicsDevice device; Effect effect;
VertexPositionNormalColored[] vertices;
VertexDeclaration myVertexDeclaration; int[] indices; Matrix viewMatrix; Matrix projectionMatrix; private int terrainWidth; private int terrainHeight; private float[,] heightData; float angle = 0; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { graphics.PreferredBackBufferWidth = 500; graphics.PreferredBackBufferHeight = 500; graphics.IsFullScreen = false; graphics.ApplyChanges(); Window.Title = "Riemer's XNA Tutorials -- Series 1"; base.Initialize(); } protected override void LoadContent() { device = graphics.GraphicsDevice; spriteBatch = new SpriteBatch(GraphicsDevice);
effect = Content.Load<Effect>
("effects");
Texture2D heightMap = Content.Load<Texture2D>
("heightmap"); LoadHeightData(heightMap);
SetUpVertices(); SetUpCamera(); SetUpIndices();
CalculateNormals();
} private void SetUpVertices() { float minHeight = float.MaxValue; float maxHeight = float.MinValue; for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainHeight; y++) { if (heightData[x, y] < minHeight) minHeight = heightData[x, y]; if (heightData[x, y] > maxHeight) maxHeight = heightData[x, y]; } } vertices = new VertexPositionNormalColored[terrainWidth * terrainHeight]; for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainHeight; y++) { vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); if (heightData[x, y] < minHeight + (maxHeight - minHeight) / 4) vertices[x + y * terrainWidth].Color = Color.Blue; else if (heightData[x, y] < minHeight + (maxHeight - minHeight) * 2 / 4) vertices[x + y * terrainWidth].Color = Color.Green; else if (heightData[x, y] < minHeight + (maxHeight - minHeight) * 3 / 4) vertices[x + y * terrainWidth].Color = Color.Brown; else vertices[x + y * terrainWidth].Color = Color.White; } }
myVertexDeclaration = new VertexDeclaration(device, VertexPositionNormalColored.VertexElements);
} private void SetUpIndices() { indices = new int[(terrainWidth - 1) * (terrainHeight - 1) * 6]; int counter = 0; for (int y = 0; y < terrainHeight - 1; y++) { for (int x = 0; x < terrainWidth - 1; x++) { int lowerLeft = x + y*terrainWidth; int lowerRight = (x + 1) + y*terrainWidth; int topLeft = x + (y + 1) * terrainWidth; int topRight = (x + 1) + (y + 1) * terrainWidth; indices[counter++] = topLeft; indices[counter++] = lowerRight; indices[counter++] = lowerLeft; indices[counter++] = topLeft; indices[counter++] = topRight; indices[counter++] = lowerRight; } } }
private void CalculateNormals() { for (int i = 0; i < vertices.Length; i++) vertices[i].Normal = new Vector3(0, 0, 0); for (int i = 0; i < indices.Length / 3; i++) { int index1 = indices[i * 3]; int index2 = indices[i * 3 + 1]; int index3 = indices[i * 3 + 2]; Vector3 side1 = vertices[index1].Position - vertices[index3].Position; Vector3 side2 = vertices[index1].Position - vertices[index2].Position; Vector3 normal = Vector3.Cross(side1, side2); vertices[index1].Normal += normal; vertices[index2].Normal += normal; vertices[index3].Normal += normal; } for (int i = 0; i < vertices.Length; i++) vertices[i].Normal.Normalize(); }
private void SetUpCamera() { viewMatrix = Matrix.CreateLookAt(new Vector3(0, 100, 100), new Vector3(0, 0, 0), new Vector3(0, 1, 0)); projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 1.0f, 300.0f); } private void LoadHeightData(Texture2D heightMap) { terrainWidth = heightMap.Width; terrainHeight = heightMap.Height; Color[] heightMapColors = new Color[terrainWidth * terrainHeight]; heightMap.GetData(heightMapColors); heightData = new float[terrainWidth, terrainHeight]; for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainHeight; y++) heightData[x, y] = heightMapColors[x + y * terrainWidth].R / 5.0f; } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Delete)) angle += 0.05f; if (keyState.IsKeyDown(Keys.PageDown)) angle -= 0.05f; base.Update(gameTime); } protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); device.RenderState.CullMode = CullMode.None; Matrix worldMatrix = Matrix.CreateTranslation(-terrainWidth / 2.0f, 0, terrainHeight / 2.0f) * Matrix.CreateRotationY(angle); effect.CurrentTechnique = effect.Techniques["Colored"]; effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xWorld"].SetValue(worldMatrix);
effect.Parameters["xEnableLighting"].SetValue(true); Vector3 lightDirection = new Vector3(1.0f, -1.0f, -1.0f); lightDirection.Normalize(); effect.Parameters["xLightDirection"].SetValue(lightDirection); effect.Parameters["xAmbient"].SetValue(0.1f);
effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = myVertexDeclaration;
device.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length, indices, 0, indices.Length / 3);
pass.End(); } effect.End(); base.Draw(gameTime); } } }
- Website design & XNA + DirectX code : Riemer Grootjans - ©2003 - 2008 Riemer Grootjans
|
|
|
|
|