Upload
others
View
1
Download
0
Embed Size (px)
Citation preview
1
IWKS 3400 LAB 81
JK Bennett
This lab will build upon what we learned in Lab 7, and will present more advanced terrain techniques.
We will create terrain that is very much like what you could use in a real game. You should complete
Lab 7 before starting Lab 8. In this Lab we will cover:
First-person mouse camera
Multitexturing with high resolution textures close to the camera
Realistic, moving water with reflections-refractions
Bump mapping
Specular reflections
Perlin noise
Shape-changing clouds
HLSL cylindrical billboarding
Region growing
You will need to load several files to complete this lab. Download and unzip this file to obtain them. If
this link does not work for some reason, here is the hard link:
http://inworks.ucdenver.edu/jkb/IWKS3400/labs/Lab8_Mono_Files.zip.
We will start with code that is almost identical to the final version of the code of Lab 7 (cleaned up a bit,
and with a few bells and whistles removed, but that is all). This file is named
Lab8_Mono_Starting_Game1.cs. It is worth reading this file to see what has changed (and why), and to
learn, for example, how to change to a HiDef grapgics profile in a way that handles a current (Version
3.7) bug in MonoGame.
Create a new Windows 4.0 game project, and replace the contents of Game1.cs with the contents of
Lab8_Mono_Starting_Game1.cs (located in the directory that you just downloaded). Make sure this file
is named Game1.cs in your project. If necessary, change the namespace declaration in Program.cs to
Lab8_Mono.
Add the following files to the MonoGame Content Pipeline from the Lab8_Mono_Files folder:
effects.fx (after renaming from Lab8_Mono_Starting_effects.fx)
heightmap2.bmp
Make sure the result compiles, and produces a result similar to that of Lab 7 (albeit with a different
terrain). You should be able to rotate the terrain (although the code that makes the rotation axis the center
of the terrain has been removed so as not to cause problems later) with the PageUp and PageDown keys.
Creating a Movable Camera
1 This lab is derived substantially from a series of excellent on-line XNA tutorials created by Riemer Grootjans. You can find his
web site at http://www.riemers.net/. The code and content in Riemer’s tutorials have been modified for MonoGame (Verion 3.7)
– jkb, Oct. 2018.
2
Let’s first add a moveable camera to our scene, so later on we can easily move it around our terrain.
Generally, we will want a first-person camera to rotate around the up vector (turning) and around the side
vector (looking up-down). Therefore, instead of storing a rotation matrix, we will simply store the rotation
values around the side and up vector, next to the position of the camera. To accomplish this, add the
following constants and instance variables to the relevant Game1 class regions. Regions are a mechanism
provided in C# to organize source code into sections that contain related code. These regions can then be
collapsed or expanded when desired. Regions are created by defining the beginning and end of each
section, as shown below.
#region Constants
// Camera control constants public const float ROTATION_SPEED = 0.3f; public const float MOVE_SPEED = 30.0f; #endregion #region Game1 Instance Vars //...existing variables Vector3 cameraPosition = new Vector3(130, 30, -50); float leftrightRot = MathHelper.PiOver2; float updownRot = -MathHelper.Pi / 10.0f; #endregion
The two constant values will indicate how fast we want our camera to respond to mouse and keyboard
input.
We will start by creating the UpdateViewMatrix method, which creates a view matrix based upon the
current camera position and rotation values. As you recall, to create the view matrix, we need the camera
position, a point where the camera is pointed, and the vector that’s considered to be ‘up’ by the camera.
These two last vectors depend on the rotation of the camera, so we first need to construct the camera
rotation matrix based on the current rotation values. Put this code in the Camera Methods region below
SetUpCamera():
private void UpdateViewMatrix()
{ Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);
3
viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector); }
The first line creates the camera rotation matrix, by combining the rotation around the X axis (looking up-
down) with the rotation around the Y axis (looking left-right).
As the target point for our camera, we take the position of our camera, plus the (0,0,-1) ‘forward’ vector.
We need to transform this forward vector with the rotation of the camera, so it becomes the forward
vector of the camera. We find the ‘Up’ vector the same way: by transforming it with the camera rotation.
With these vectors known, it’s easy to construct the viewMatrix. Do this by replacing the code in
SetUpCamera() with the following code:
UpdateViewMatrix(); projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 0.3f, 1000.0f);
Next, we will make our camera react to user input. We want our camera to go forward/backward or pan
left/right when we press the corresponding arrow buttons on the keyboard. Camera rotation will be
directed by mouse input. We will create a method for this that updates at the frame rate, so the camera
will turn at a speed not dependent upon the computer speed. Put this code in the Camera Methods region
as well:
private void ProcessInput(float amount)
{ Vector3 moveVector = new Vector3(0, 0, 0); KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W)) moveVector += new Vector3(0, 0, -1); if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S)) moveVector += new Vector3(0, 0, 1); if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D)) moveVector += new Vector3(1, 0, 0); if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A)) moveVector += new Vector3(-1, 0, 0); if (keyState.IsKeyDown(Keys.Q)) moveVector += new Vector3(0, 1, 0); if (keyState.IsKeyDown(Keys.Z)) moveVector += new Vector3(0, -1, 0); AddToCameraPosition(moveVector * amount); }
This method reads the keyboard, and sets a moveVector accordingly. This moveVector is multiplied by
the amount variable, which will indicate the amount of time passed since the last call. The result is passed
to the AddToCameraPosition method, which is defined below (place this code in the Camera Methods
region).
private void AddToCameraPosition(Vector3 vectorToAdd)
{ Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot);
4
Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation); cameraPosition += MOVE_SPEED * rotatedVector; UpdateViewMatrix(); }
This method again creates the rotation matrix of the camera. Once we have this matrix, we use it
transform the moveDirection. This is needed, because if we want the camera to move into the Forward
direction, you don’t want it to move in the (0,0,-1) direction, but rather in the direction that is actually
Forward for the camera at the current time. The last line transforms this vector by the rotation of our
camera, so ‘Forward’ will actually be ‘Forward’ relative to our camera.
We still need to call the ProcessInput method from within the Update method (place this code above the
worldMatrix definition):
float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f;
ProcessInput(timeDifference);
Run this code; you should be able to move the camera using the arrow buttons.
Now let’s use mouse input to control the camera. We can read out the mouse just like we did with the
keyboard: by reading the MouseState. The MouseState structure contains (among other things) the
absolute X and Y position of the mouse cursor, but not the amount of movement since the last call. So we
will need to reposition the mouse cursor to the middle of the screen at the end of each update. By
comparing the current position of the mouse to the middle position of the screen, we can check every
frame how much the mouse has moved. To do this, add the following Game1 class instance variable:
MouseState originalMouseState;
During the startup of our project, we will want to position the mouse in the middle and store this state; do
this by putting this code in our LoadContent method (before SetUpCamera()):
Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2);
originalMouseState = Mouse.GetState();
Finally, add this code to top of our ProcessInput method (in Camera Methods):
MouseState currentMouseState = Mouse.GetState(); if (currentMouseState != originalMouseState) { float xDifference = currentMouseState.X - originalMouseState.X; float yDifference = currentMouseState.Y - originalMouseState.Y; leftrightRot -= ROTATION_SPEED * xDifference * amount; updownRot -= ROTATION_SPEED * yDifference * amount; Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); UpdateViewMatrix(); }
5
First, we retrieve the current MouseState. Next, we check whether this state differs from the original
mouse position, in the middle of the screen. If there is a difference, the corresponding rotation values are
updated according to the amount of mouse movement, multiplied by the time elapsed since the last frame.
NOTE: Since we are continuously setting the mouse cursor to the middle of our window, there will
be no way to click on the X to close the window. Use Alt+F4 to close the running program.
Run this code. You should be able to move over your terrain as in any other first person game.
Experiment with the various ways to move both the camera and the terrain.
Although we did not change a lot to the screenshot, we now have a moving camera that does what we tell
it to do.
Adding Texture to Terrain
We learned how to apply textures to triangles in Lab 6. In this section, we will apply a grass texture to
our terrain by copying it a few times over our terrain in mirrored mode. We will use the grass.dds texture
that we downloaded earlier with other content files. Import this texture into the content pipeline as we
have done before (if you wish, you can go ahead and import all of the other content for use later), and add
the following Game1 instance variable:
Texture2D grassTexture;
Let’s create a small separate method to load all our textures:
#region Texture Loads private void LoadTextures() { grassTexture = Content.Load<Texture2D>("grass"); } #endregion
And call this method from the end of our LoadContent method:
LoadTextures();
Now, instead of using our user-defined VertexPositionColorNormal format, we will use the MonoGame-
provided VertexPositionNormalTexture format. Don’t delete the VertexPositionColorNormal struct
though, as we will use it in the next section.
Replace all instances of VertexPositionColorNormal in the code with VertexPositionNormalTexture
(you can use Ctrl+H for this), but do not rename the VertexPositionColorNormal struct, since this
would override MonoGame’s VertexPositionNormalTexture struct!
6
We also need to change the way we specify our VertexBuffer in CopyToBuffers() (in the Terrain Methods
region), as follows:
myVertexBuffer = new VertexBuffer(device, typeof(VertexPositionNormalTexture), vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices);
Now, instead of specifying the color of each vertex, we’ll need to pass in the texture coordinate. So
replace the existing SetUpVertices method with the following code:
private void SetUpVertices() { vertices = new VertexPositionNormalTexture[terrainWidth * terrainLength]; for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainLength; y++) { vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / MAX_HT; vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / MAX_HT; } } }
We need to add a constant to indicate the maximum normalized height. This will reduce the range of
heights displayed, and will make things look a little better. #region Constants
// Terrain texture mapping constants public const float MAX_HT = 30.0f; #endregion
And also clean up LoadHeightData (in Terrain Methods) to use this constant, and to compute the
maximum and minimum heights found in our height bitmap:
private void LoadHeightData(Texture2D heightMap) { terrainWidth = heightMap.Width; terrainLength = heightMap.Height; float minimumHeight = float.MaxValue; float maximumHeight = float.MinValue; Color[] heightMapColors = new Color[terrainWidth * terrainLength]; heightMap.GetData(heightMapColors); heightData = new float[terrainWidth, terrainLength]; for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++)
7
{ heightData[x, y] = heightMapColors[x + y * terrainWidth].R; if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y]; if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y]; } for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * MAX_HT; }
In the Draw method, we need to indicate that we will be using the Textured technique to draw our terrain,
instead of the Colored technique, and pass our grass texture to the shader code (leave the other
parameters):
effect.CurrentTechnique = effect.Techniques["Textured"]; effect.Parameters["xTexture"].SetValue(grassTexture);
Run your code, and you should see some terrain covered with grass, as shown below.
8
Here is our code so far: using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace Lab8_Mono { /// <summary> /// This is the main type for your game. /// </summary> public class Game1 : Game { #region Constants //Lighting Constants Vector3 LIGHT_DIRECTION = new Vector3(1.0f, -1.0f, 1.0f); //use (1.0f, -1.0f, 1.0f) to flip light public const float AMBIENT_LIGHT_LEVEL = 0.7f; // Camera control constants public const float ROTATION_SPEED = 0.3f; public const float MOVE_SPEED = 30.0f; // Terrain texture mapping constants public const float MAX_HT = 30.0f; #endregion #region Vertex Structs public struct VertexPositionColorNormal { public Vector3 Position; public Color Color; public Vector3 Normal; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Color, VertexElementUsage.Color, 0), new VertexElement(sizeof(float) * 3 + 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0) ); } #endregion #region Game1 Instance Vars GraphicsDeviceManager graphics; GraphicsDevice device; Effect effect; VertexPositionNormalTexture[] vertices; int[] indices; Matrix viewMatrix; Matrix projectionMatrix;
9
VertexBuffer myVertexBuffer; IndexBuffer myIndexBuffer; private int terrainWidth; private int terrainLength; private float[,] heightData; Matrix worldMatrix = Matrix.Identity; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity; Vector3 cameraPosition = new Vector3(130, 30, -50); float leftrightRot = MathHelper.PiOver2; float updownRot = -MathHelper.Pi / 10.0f; MouseState originalMouseState; Texture2D grassTexture; #endregion #region Class Game1 Constructor public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } #endregion #region Terrain Methods private void SetUpVertices() { vertices = new VertexPositionNormalTexture[terrainWidth * terrainLength]; for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainLength; y++) { vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / MAX_HT; vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / MAX_HT; } } } private void SetUpIndices() { indices = new int[(terrainWidth - 1) * (terrainLength - 1) * 6]; int counter = 0; for (int y = 0; y < terrainLength - 1; y++) { for (int x = 0; x < terrainWidth - 1; x++)
10
{ 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 CopyToBuffers() { myVertexBuffer = new VertexBuffer(device, typeof(VertexPositionNormalTexture), vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices); myIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly); myIndexBuffer.SetData(indices); } private void LoadHeightData(Texture2D heightMap) { terrainWidth = heightMap.Width; terrainLength = heightMap.Height; float minimumHeight = float.MaxValue;
11
float maximumHeight = float.MinValue; Color[] heightMapColors = new Color[terrainWidth * terrainLength]; heightMap.GetData(heightMapColors); heightData = new float[terrainWidth, terrainLength]; for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) { heightData[x, y] = heightMapColors[x + y * terrainWidth].R; if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y]; if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y]; } for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * MAX_HT; } #endregion #region Texture Loads private void LoadTextures() { grassTexture = Content.Load<Texture2D>("grass"); } #endregion #region Camera Methods private void SetUpCamera() { UpdateViewMatrix(); projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 0.3f, 1000.0f); } private void UpdateViewMatrix() { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);
12
viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector); } private void ProcessInput(float amount) { MouseState currentMouseState = Mouse.GetState(); if (currentMouseState != originalMouseState) { float xDifference = currentMouseState.X - originalMouseState.X; float yDifference = currentMouseState.Y - originalMouseState.Y; leftrightRot -= ROTATION_SPEED * xDifference * amount; updownRot -= ROTATION_SPEED * yDifference * amount; Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); UpdateViewMatrix(); } Vector3 moveVector = new Vector3(0, 0, 0); KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W)) moveVector += new Vector3(0, 0, -1); if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S)) moveVector += new Vector3(0, 0, 1); if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D)) moveVector += new Vector3(1, 0, 0); if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A)) moveVector += new Vector3(-1, 0, 0); if (keyState.IsKeyDown(Keys.Q)) moveVector += new Vector3(0, 1, 0); if (keyState.IsKeyDown(Keys.Z)) moveVector += new Vector3(0, -1, 0); AddToCameraPosition(moveVector * amount); } private void AddToCameraPosition(Vector3 vectorToAdd) { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation); cameraPosition += MOVE_SPEED * rotatedVector; UpdateViewMatrix(); } #endregion #region Initialize /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() {
13
// JKB Note: This ordering and repetition of graphics profile commands addresses a // current bug in Monogame 3.7. If we don't do it this way the window defaults to // a (small) fixed size. Also, Monogame says it defaults to Reach, but it doesn't, so // we have to set HiDef explicitily here in order to do 32-bit index buffers. graphics.GraphicsProfile = GraphicsProfile.HiDef; graphics.IsFullScreen = false; graphics.ApplyChanges(); graphics.PreferredBackBufferWidth = 800; graphics.PreferredBackBufferHeight = 800; graphics.ApplyChanges(); Window.Title = "Lab8_Mono - Terrain Tutorial II"; base.Initialize(); } #endregion #region LoadContent /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { device = graphics.GraphicsDevice; effect = Content.Load<Effect>("effects"); Texture2D heightMap = Content.Load<Texture2D>("heightmap2"); LoadHeightData(heightMap); Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); originalMouseState = Mouse.GetState(); SetUpCamera(); SetUpVertices(); SetUpIndices(); CalculateNormals(); CopyToBuffers(); LoadTextures(); } #endregion #region UnloadContent /// <summary> /// UnloadContent will be called once per game and is the place to unload /// game-specific content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } #endregion
14
#region Update /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyState = Keyboard.GetState(); //Rotation if (keyState.IsKeyDown(Keys.PageUp)) { worldRotation = Matrix.CreateRotationY(0.01f); } else if (keyState.IsKeyDown(Keys.PageDown)) { worldRotation = Matrix.CreateRotationY(-0.01f); } else { worldRotation = Matrix.CreateRotationY(0); } float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f; ProcessInput(timeDifference); worldMatrix *= worldTranslation * worldRotation; base.Update(gameTime); } #endregion #region Draw /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); LIGHT_DIRECTION.Normalize(); effect.Parameters["xLightDirection"].SetValue(LIGHT_DIRECTION); effect.Parameters["xAmbient"].SetValue(AMBIENT_LIGHT_LEVEL); effect.Parameters["xEnableLighting"].SetValue(true); effect.CurrentTechnique = effect.Techniques["Textured"]; effect.Parameters["xTexture"].SetValue(grassTexture); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xWorld"].SetValue(worldMatrix);
15
foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.Indices = myIndexBuffer; device.SetVertexBuffer(myVertexBuffer); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, indices.Length / 3); // JKB Note: Originally, this was: // device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Length, 0, indices.Length / 3); // using the obsolete method: // DrawIndexedPrimitives(PrimitiveType primitiveType, int baseVertex, int minVertexIndex, int numVertices, int startIndex, int primitiveCount) // In Monogame, minVertexIndex and numVertices are unused and ignored, so we use: // DrawIndexedPrimitives(PrimitiveType primitiveType, int baseVertex, int startIndex, int primitiveCount) // instead. } base.Draw(gameTime); } #endregion } }
Creating Multi-textured Terrain
Our textured terrain already looks a lot nicer than a simply color-coded terrain, but it could still be
improved by, for example, using different textures according to the height level of each vertex. In this
section, we will divide our terrain into sand, grass, rocky and snow-covered areas. If you have not already
done so, add the three textures sand.dds, rock.dds and snow.dds to your content project, and declare the
following Game1 instance variables:
Texture2D sandTexture;
Texture2D rockTexture; Texture2D snowTexture;
Now load them in the LoadTextures method (Texture Loads region):
sandTexture = Content.Load<Texture2D>("sand"); rockTexture = Content.Load<Texture2D>("rock"); snowTexture = Content.Load<Texture2D>("snow");
We could take the same approach as we did with the four colors, but we would have the same problem: at
the boundaries between textures, we would clearly see the edges between the two different textures. What
we want is a smooth transition between the four textures. We can accomplish this by giving each vertex a
combination of multiple textures, as follows. Each vertex will store four weights, one for each texture. For
example: the highest vertex of the terrain would have weight 1 for the snow texture, and weight 0 for the
other three textures, so that vertex would get its color entirely from the snow texture. A vertex in the
middle between the snowy and the rocky region will have weight 0.5 for both the snow and rock texture
and weight 0 for the other two textures, which would result in a color taken for 50% from the snow
16
texture and 50% from the rock texture.
To do this, we first have to define a new custom vertex format, which will enable us to store the four
weights for each vertex, along with the position, normal and texture coordinates. We will call this format
VertexMultitextured, defined as follows:
#region Vertex Structs
public struct VertexMultitextured { public Vector3 Position; public Vector3 Normal; public Vector4 TextureCoordinate; public Vector4 TexWeights; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0 ), new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0 ), new VertexElement(sizeof(float) * 6, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 0 ), new VertexElement(sizeof(float) * 10, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1 ) ); } #endregion
The last entry is new: for each vertex we’ll be storing an additional Vector4, which will contain the four
weights. Because there’s no standard part of a VertexElement called “textureweights” or something, we
will pass this information as an additional TextureCoordinate. Because we are already passing another
TextureCoordinate, we have to give this one an index of 1, instead of 0.
Replace all instances in your code of VertexPositionNormalTexture with VertexMultitextured (using
Ctrl+H).
Now that each vertex has an additional Vector4 to store the weights, let’s fill them. For this, we need a
scheme that maps the height of each vertex into texture weights. To make things easy, we usually want
weights to have a value between 0 and 1, where 0 means ‘nothing’ and 1 means ‘all’. This means in each
vertex, we need to find four weights between 0 and 1. Consider the left image below, which shows the
weight we ideally want to find. Note that the horizontal X axis indicates the weight values.
17
In the LoadHeightData method, the heights of the terrain vertices are set to values between 0 and MAX_HT
(30 in this example). These heights are represented by the vertical arrow. You can see the vertices with
height 0 should have weight=1 for the sand texture, while the vertices with height 30 should have
weight=1 for the snow texture. A vertex with height=25 should get a weight between 0 and 1 for both the
snow and the rock texture. Thus sand will range from 0 to 8, grass will range from 6 to 18, rock will
range from 14 to 26, and snow will range from 24 to 30. To implement this approach, we have to
compute texture weights that will produce this effect.
Consider how to find the weight for the grass texture, TexWeights.Y. We see in the left image that a vertex
with height 12 should have weight 1. The weight should become 0 for heights 6 and 18, which are (12-6)
and (12+6), respectively. In other words, all heights that are within 6 meters from the 12 meter mark
should have some weight factor for the grass texture. This explains the ‘abs(height-12)/6’: it will be 0 for
height = 12, and become 1 as the height approaches 6 or 18. But we need the opposite: at height 12 we
need weight = 1, and at heights 6 and 18 we need weight = 0. So we subtract ‘abs(height-12)/6’ from 1 to
get ‘1- abs(height-12)/6’. This value become smaller than 0 for height lower than 6 and larger than 18, so
we clamp this value between 0 and 1. Finally, since imbedded constants are a pain, let’s make this work
for any MAX_HT.
First add some additional Terrain texture mapping constants:
public const float MIN_HT = 0.0f; public const float SAND_UPPER = 0.266f * MAX_HT; // 8 public const float GRASS_MID = 0.4f * MAX_HT; // 12 public const float GRASS_RANGE = 0.2f * MAX_HT; // 12 +/- 6 public const float ROCK_MID = 0.666f * MAX_HT; // 20 public const float ROCK_RANGE = 0.2f * MAX_HT; // 20 +/- 6 public const float SNOW_LOWER = 0.8f * MAX_HT; // 24
18
And now replace the inner for loop (DO NOT overwrite the outer for loop) of SetUpVertices (In Terrain
Methods) as follows:
for (int y = 0; y < terrainLength; y++)
{ vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / MAX_HT; vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / MAX_HT; vertices[x + y * terrainWidth].TexWeights.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MIN_HT) / SAND_UPPER, 0, 1); vertices[x + y * terrainWidth].TexWeights.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - GRASS_MID) / GRASS_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - ROCK_MID) / ROCK_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MAX_HT) / SNOW_LOWER, 0, 1); }
Now we can assign any MAX_HT we like, and the code will compute texture weights correctly. In order
for us to use the absolute value method (part of System.Math), we need to add the following using
statement (at the top of Game1.cs) to tell Monogame what we are up to:
using System;
Although our progress so far is a step in the right direction, it isn’t perfect yet. For example: since snow
and rock weights are 0.2, the pixels corresponding to height 25 will get 20% of their color from the snow
texture, and 20% from the rock texture. The remaining 60% will remain black, so these pixels will look
very dark. If you like, run the code now to see this (you will have to jump ahead and add the
MultiTextured effect to effects.fx, as described below). In some cases we might like this effect (in a
Twilight game, perhaps?) but we will usually want to ensure that for every vertex, the sum of all weights
is exactly 1. To do this, for each vertex we will make the sum of all weights, and divide all weights by
this sum. In case of the previous example, the sum would be 0.2 + 0.2 = 0.4. Next, 0.2 divided by 0.4
gives 0.5 for both the new snow and rock weights. And of course, 0.5 + 0.5 equals 1. This is what is
shown in the right part of the image above. You’ll notice that for each height, the summed weight value is
1.
This normalization is accomplished by the following code, which must be performed for each vertex, so it
also must be placed inside the inner for loop (below the code we just added):
float total = vertices[x + y * terrainWidth].TexWeights.X;
total += vertices[x + y * terrainWidth].TexWeights.Y; total += vertices[x + y * terrainWidth].TexWeights.Z;
19
total += vertices[x + y * terrainWidth].TexWeights.W; vertices[x + y * terrainWidth].TexWeights.X /= total; vertices[x + y * terrainWidth].TexWeights.Y /= total; vertices[x + y * terrainWidth].TexWeights.Z /= total; vertices[x + y * terrainWidth].TexWeights.W /= total;
Now that we have texture weights for each vertex of our terrain and a vertex structure that allows
MonoGame to pass these values to the HLSL effect, it’s time to actually code this effect. Open effects.fx
(if necessary, first add it to the Content folder “Add->Existing”, and double click on it), and modify the
code as follows:
First, add the new texture samplers (near the top of the effects file) that will process our four textures (you
should leave the xTexture sampler alone): Texture xTexture0; sampler TextureSampler0 = sampler_state { texture = <xTexture0>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture1; sampler TextureSampler1 = sampler_state { texture = <xTexture1>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture2; sampler TextureSampler2 = sampler_state { texture = <xTexture2>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xTexture3; sampler TextureSampler3 = sampler_state { texture = <xTexture3>; magfilter = LINEAR;
minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; };
Now add the actual technique code at the end of the file:
//------- Technique: Multitextured -------- struct MTVertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float3 Normal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 LightDirection : TEXCOORD2; float4 TextureWeights : TEXCOORD3; }; struct MTPixelToFrame { float4 Color : COLOR0; }; MTVertexToPixel MultiTexturedVS(float4 inPos : POSITION, float3 inNormal : NORMAL, float2 inTexCoords : TEXCOORD0, float4 inTexWeights : TEXCOORD1) { MTVertexToPixel Output = (MTVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection);
20
float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Normal = (float3) mul(normalize(float4(inNormal, 0.0)), xWorld); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -xLightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; return Output; } MTPixelToFrame MultiTexturedPS(MTVertexToPixel PSIn) { MTPixelToFrame Output = (MTPixelToFrame)0; float lightingFactor = 1; if (xEnableLighting) lightingFactor = saturate(saturate(dot(float4(PSIn.Normal, 0.0), PSIn.LightDirection)) + xAmbient); Output.Color = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; Output.Color += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; Output.Color += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; Output.Color += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w; Output.Color *= lightingFactor; return Output; } technique MultiTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 MultiTexturedVS(); PixelShader = compile ps_4_0_level_9_1 MultiTexturedPS(); } }
Note how the data enters our vertex shader: the usual texture coordinates are passed as TEXCOORD0,
while our newly added texture weights are using TEXCOORD1. Check our vertex definition structure to
remember why. This data is simply passed on to the output. The pixel shader only needs to calculate the
color of each pixel, so we use the default output structure.
The most interesting code of this technique is the pixel shader. We define the color of the pixel by
sampling all four textures, and multiplying these four colors with their corresponding weight. The four
results are all summed together, and we multiply the final result with the lighting factor.
Finally, fix the way we create the vertex buffer to deal with our custom vertices:
In the CopyToBuffers() method of Game1.cs, replace:
21
myVertexBuffer = new VertexBuffer(device, typeof(VertexPositionNormalTexture), vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices);
with:
myVertexBuffer = new VertexBuffer(device, VertexMultitextured.vertexDeclaration, vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices);
Back in the Draw method of Game1.cs, we need to specify that we will be using this new technique to
draw our terrain, and we have to pass in the four textures. Replace:
effect.CurrentTechnique = effect.Techniques["Textured"]; effect.Parameters["xTexture"].SetValue(grassTexture);
with: effect.CurrentTechnique = effect.Techniques["MultiTextured"]; effect.Parameters["xTexture0"].SetValue(sandTexture); effect.Parameters["xTexture1"].SetValue(grassTexture); effect.Parameters["xTexture2"].SetValue(rockTexture); effect.Parameters["xTexture3"].SetValue(snowTexture);
Run this code, you should see a multitextured terrain, similar to that shown below. Use the camera to
have a closer look at the transitions between our textures. How cool is that! Although this multitextured
terrain looks pretty good, it still can be improved. Try moving the camera very close to the sand, and
notice the pixels in the texture. In the next section, we will discover how we can have higher detail closer
to the camera.
22
Optional - test your knowledge:
Try different MAX_HT values with different height maps.
Play around with the weight mapping values. For each texture, there are two parameters to
change: the central height value of the peak, as well as the width of the peak.
Open up the heightmap2.bmp file in Microsoft Paint (or other image editing program that can
handle .bmp files), and find the rivers. In the middle of a river, add a few completely white pixels.
Run the program, and try to explain what you see.
Here is our code so far:
Game1.cs
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; namespace Lab8_Mono { /// <summary> /// This is the main type for your game. /// </summary> public class Game1 : Game { #region Constants //Lighting Constants Vector3 LIGHT_DIRECTION = new Vector3(1.0f, -1.0f, 1.0f); //use (1.0f, -1.0f, 1.0f) to flip light public const float AMBIENT_LIGHT_LEVEL = 0.7f; // Camera control constants public const float ROTATION_SPEED = 0.3f; public const float MOVE_SPEED = 30.0f; // Terrain texture mapping constants public const float MAX_HT = 30.0f; public const float MIN_HT = 0.0f; public const float SAND_UPPER = 0.266f * MAX_HT; // 8 public const float GRASS_MID = 0.4f * MAX_HT; // 12 public const float GRASS_RANGE = 0.2f * MAX_HT; // 12 +/- 6 public const float ROCK_MID = 0.666f * MAX_HT; // 20 public const float ROCK_RANGE = 0.2f * MAX_HT; // 20 +/- 6 public const float SNOW_LOWER = 0.8f * MAX_HT; // 24 #endregion #region Vertex Structs public struct VertexPositionColorNormal { public Vector3 Position; public Color Color; public Vector3 Normal;
23
public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Color, VertexElementUsage.Color, 0), new VertexElement(sizeof(float) * 3 + 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0) ); } #endregion #region Vertex Structs public struct VertexMultitextured { public Vector3 Position; public Vector3 Normal; public Vector4 TextureCoordinate; public Vector4 TexWeights; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0), new VertexElement(sizeof(float) * 6, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 0), new VertexElement(sizeof(float) * 10, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1) ); } #endregion #region Game1 Instance Vars GraphicsDeviceManager graphics; GraphicsDevice device; Effect effect; VertexMultitextured[] vertices; int[] indices; Matrix viewMatrix; Matrix projectionMatrix; VertexBuffer myVertexBuffer; IndexBuffer myIndexBuffer; private int terrainWidth; private int terrainLength; private float[,] heightData; Matrix worldMatrix = Matrix.Identity; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity;
24
Vector3 cameraPosition = new Vector3(130, 30, -50); float leftrightRot = MathHelper.PiOver2; float updownRot = -MathHelper.Pi / 10.0f; MouseState originalMouseState; Texture2D grassTexture; Texture2D sandTexture; Texture2D rockTexture; Texture2D snowTexture; #endregion #region Class Game1 Constructor public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } #endregion #region Terrain Methods private void SetUpVertices() { vertices = new VertexMultitextured[terrainWidth * terrainLength]; for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainLength; y++) { vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / MAX_HT; vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / MAX_HT; vertices[x + y * terrainWidth].TexWeights.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MIN_HT) / SAND_UPPER, 0, 1); vertices[x + y * terrainWidth].TexWeights.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - GRASS_MID) / GRASS_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - ROCK_MID) / ROCK_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MAX_HT) / SNOW_LOWER, 0, 1); float total = vertices[x + y * terrainWidth].TexWeights.X; total += vertices[x + y * terrainWidth].TexWeights.Y; total += vertices[x + y * terrainWidth].TexWeights.Z; total += vertices[x + y * terrainWidth].TexWeights.W; vertices[x + y * terrainWidth].TexWeights.X /= total; vertices[x + y * terrainWidth].TexWeights.Y /= total; vertices[x + y * terrainWidth].TexWeights.Z /= total; vertices[x + y * terrainWidth].TexWeights.W /= total;
25
} } } private void SetUpIndices() { indices = new int[(terrainWidth - 1) * (terrainLength - 1) * 6]; int counter = 0; for (int y = 0; y < terrainLength - 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 CopyToBuffers() { myVertexBuffer = new VertexBuffer(device, VertexMultitextured.vertexDeclaration, vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices);
26
myIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly); myIndexBuffer.SetData(indices); } private void LoadHeightData(Texture2D heightMap) { terrainWidth = heightMap.Width; terrainLength = heightMap.Height; float minimumHeight = float.MaxValue; float maximumHeight = float.MinValue; Color[] heightMapColors = new Color[terrainWidth * terrainLength]; heightMap.GetData(heightMapColors); heightData = new float[terrainWidth, terrainLength]; for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) { heightData[x, y] = heightMapColors[x + y * terrainWidth].R; if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y]; if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y]; } for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * MAX_HT; } #endregion #region Texture Loads private void LoadTextures() { grassTexture = Content.Load<Texture2D>("grass"); sandTexture = Content.Load<Texture2D>("sand"); rockTexture = Content.Load<Texture2D>("rock"); snowTexture = Content.Load<Texture2D>("snow"); } #endregion #region Camera Methods private void SetUpCamera() { UpdateViewMatrix(); projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 0.3f, 1000.0f); }
27
private void UpdateViewMatrix() { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation); viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector); } private void ProcessInput(float amount) { MouseState currentMouseState = Mouse.GetState(); if (currentMouseState != originalMouseState) { float xDifference = currentMouseState.X - originalMouseState.X; float yDifference = currentMouseState.Y - originalMouseState.Y; leftrightRot -= ROTATION_SPEED * xDifference * amount; updownRot -= ROTATION_SPEED * yDifference * amount; Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); UpdateViewMatrix(); } Vector3 moveVector = new Vector3(0, 0, 0); KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W)) moveVector += new Vector3(0, 0, -1); if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S)) moveVector += new Vector3(0, 0, 1); if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D)) moveVector += new Vector3(1, 0, 0); if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A)) moveVector += new Vector3(-1, 0, 0); if (keyState.IsKeyDown(Keys.Q)) moveVector += new Vector3(0, 1, 0); if (keyState.IsKeyDown(Keys.Z)) moveVector += new Vector3(0, -1, 0); AddToCameraPosition(moveVector * amount); } private void AddToCameraPosition(Vector3 vectorToAdd) { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation); cameraPosition += MOVE_SPEED * rotatedVector; UpdateViewMatrix(); } #endregion
28
#region Initialize /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // JKB Note: This ordering and repetition of graphics profile commands addresses a // current bug in MonoGame 3.7. If we don't do it this way the window defaults to // a (small) fixed size. Also, MonoGame says it defaults to Reach, but it doesn't, so // we have to set HiDef explicitily here in order to use 32-bit index buffers. graphics.GraphicsProfile = GraphicsProfile.HiDef; graphics.IsFullScreen = false; graphics.ApplyChanges(); graphics.PreferredBackBufferWidth = 800; graphics.PreferredBackBufferHeight = 800; graphics.ApplyChanges(); Window.Title = "Lab8_Mono - Terrain Tutorial II"; base.Initialize(); } #endregion #region LoadContent /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { device = graphics.GraphicsDevice; effect = Content.Load<Effect>("effects"); Texture2D heightMap = Content.Load<Texture2D>("heightmap2"); LoadHeightData(heightMap); Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); originalMouseState = Mouse.GetState(); SetUpCamera(); SetUpVertices(); SetUpIndices(); CalculateNormals(); CopyToBuffers();
29
LoadTextures(); } #endregion #region UnloadContent /// <summary> /// UnloadContent will be called once per game and is the place to unload /// game-specific content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } #endregion #region Update /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyState = Keyboard.GetState(); //Rotation if (keyState.IsKeyDown(Keys.PageUp)) { worldRotation = Matrix.CreateRotationY(0.01f); } else if (keyState.IsKeyDown(Keys.PageDown)) { worldRotation = Matrix.CreateRotationY(-0.01f); } else { worldRotation = Matrix.CreateRotationY(0); } float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f; ProcessInput(timeDifference); worldMatrix *= worldTranslation * worldRotation; base.Update(gameTime); } #endregion #region Draw /// <summary>
30
/// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); LIGHT_DIRECTION.Normalize(); effect.Parameters["xLightDirection"].SetValue(LIGHT_DIRECTION); effect.Parameters["xAmbient"].SetValue(AMBIENT_LIGHT_LEVEL); effect.Parameters["xEnableLighting"].SetValue(true); effect.CurrentTechnique = effect.Techniques["MultiTextured"]; effect.Parameters["xTexture0"].SetValue(sandTexture); effect.Parameters["xTexture1"].SetValue(grassTexture); effect.Parameters["xTexture2"].SetValue(rockTexture); effect.Parameters["xTexture3"].SetValue(snowTexture); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xWorld"].SetValue(worldMatrix); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.Indices = myIndexBuffer; device.SetVertexBuffer(myVertexBuffer); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, indices.Length / 3); } base.Draw(gameTime); } #endregion } }
effects.fx
//---------------------------------------------------- //-- This effect file derived from: -- //-- www.riemers.net -- //-- Basic shaders -- //-- -- //-- Modified for MonoGame by John K. Bennett -- //-- -- //-- Use/modify as you like -- //-- -- //---------------------------------------------------- struct VertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float LightingFactor: TEXCOORD0;
31
float2 TextureCoords: TEXCOORD1; }; struct PixelToFrame { float4 Color : COLOR0; }; //------- Constants -------- float4x4 xView; float4x4 xProjection; float4x4 xWorld; float3 xLightDirection; float xAmbient; bool xEnableLighting; bool xShowNormals; //------- Texture Samplers -------- Texture xTexture; sampler TextureSampler = sampler_state { texture = <xTexture>; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;}; Texture xTexture0; sampler TextureSampler0 = sampler_state { texture = <xTexture0>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture1; sampler TextureSampler1 = sampler_state { texture = <xTexture1>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture2; sampler TextureSampler2 = sampler_state { texture = <xTexture2>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xTexture3; sampler TextureSampler3 = sampler_state { texture = <xTexture3>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; //------- Technique: Pretransformed -------- VertexToPixel PretransformedVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; Output.Position = inPos; Output.Color = inColor; return Output; } PixelToFrame PretransformedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color;
32
return Output; } technique Pretransformed { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 PretransformedVS(); PixelShader = compile ps_4_0_level_9_1 PretransformedPS(); } } //------- Technique: Colored -------- VertexToPixel ColoredVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame ColoredPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Colored { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredVS(); PixelShader = compile ps_4_0_level_9_1 ColoredPS(); } } //------- Technique: ColoredNoShading -------- // No lighting or shading, so no normal info passed in VertexToPixel ColoredNoShadingVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0;
33
float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; return Output; } PixelToFrame ColoredNoShadingPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique ColoredNoShading { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredNoShadingVS(); PixelShader = compile ps_4_0_level_9_1 ColoredNoShadingPS(); } } //------- Technique: Textured -------- VertexToPixel TexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld));; Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame TexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; }
34
technique Textured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 TexturedVS(); PixelShader = compile ps_4_0_level_9_1 TexturedPS(); } } //------- Technique: Multitextured -------- struct MTVertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float3 Normal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 LightDirection : TEXCOORD2; float4 TextureWeights : TEXCOORD3; }; struct MTPixelToFrame { float4 Color : COLOR0; }; MTVertexToPixel MultiTexturedVS(float4 inPos : POSITION, float3 inNormal : NORMAL, float2 inTexCoords : TEXCOORD0, float4 inTexWeights : TEXCOORD1) { MTVertexToPixel Output = (MTVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Normal = (float3) mul(normalize(float4(inNormal, 0.0)), xWorld); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -xLightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; return Output; } MTPixelToFrame MultiTexturedPS(MTVertexToPixel PSIn) { MTPixelToFrame Output = (MTPixelToFrame)0; float lightingFactor = 1; if (xEnableLighting) lightingFactor = saturate(saturate(dot(float4(PSIn.Normal, 0.0), PSIn.LightDirection)) + xAmbient); Output.Color = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; Output.Color += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; Output.Color += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; Output.Color += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w; Output.Color *= lightingFactor;
35
return Output; } technique MultiTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 MultiTexturedVS(); PixelShader = compile ps_4_0_level_9_1 MultiTexturedPS(); } }
Adding More Detail Close to the Camera
When we look at the terrain from a distance, it looks pretty good. But when we move the camera very
close to the terrain, we see the individual pixels of each texture are spread out over a large area. This is
not good, because in a game, the camera will usually be placed close to the terrain. How might we fix
this? One approach would be to enlarge our texture coordinates, so that more of the texture would be put
on each terrain grid. This would give good results when the camera was close to the terrain, but as we
moved the camera further away, the textures will be so small that we would see the repeating pattern. So
the problem is now the other way around.
Instead of choosing one of these approaches, we will combine both. This will be done in the pixel shader:
when the pixel is far away from our camera, we will use the big textures. When the pixel is very close to
the camera, we will use the smaller, more detailed textures. For the region in between, we’ll blend
between these two. This whole operation can be done in the pixel shader, so the load on our CPU will
remain the same. How cool is that?
All of what we need to do involves editing the effects.fx file. First, we will need to know the distance to
our camera for each pixel. In the MultiTextured shader (in effects.fx), add the following variable to the
MTVertexToPixel struct to keep track of this:
float Depth : TEXCOORD4;
This distance is just the z coordinate of the position in camera space. Remember, because this is the result
of a 4x4 matrix multiplication, we first need to divide it by the w component before we can use it. Add
this code at the end of the MultiTexturedVS routine:
Output.Depth = Output.Position.z/Output.Position.w;
Now that we can access this value in the pixel shader, we know the distance between each pixel and the
camera. Now, in the pixel shader (MultiTexturedPS), define the values that will govern the blending: the
distance to the camera at which blending will begin, and the width of the blending border (place this code
below float lightingFactor = 1;):
float blendDistance = 0.99f; float blendWidth = 0.005f;
36
float blendFactor = clamp((PSIn.Depth-blendDistance)/blendWidth, 0, 1);
The last line represents the blending function. All pixels will have a Depth value between 0 and 1, where
0 corresponds to the near clipping plane distance, and 1 to the far clipping plane distance (as set in our
projectionMatrix). Using this function, all pixels closer to the camera than 0.99 will get a blendFactor of
0; all pixels further than 0.99+0.005=0.995 will get a blendFactor of 1, and all pixels in between will get
a linearly interpolated blendFactor. This idea is visualized in the image below:
In this figure, solid blue connotes blendFactor 0, and solid red blendFactor 1. Pixels with blendFactor 0
(blue) will use the highly detailed textures, and pixels with blendFactor 1 (red) will use the standard
textures.
First, we will calculate both the high-detail color and the standard color for each pixel.
Replace:
Output.Color = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; Output.Color += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; Output.Color += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; Output.Color += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w;
with:
float4 farColor; farColor = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; farColor += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; farColor += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; farColor += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w; float4 nearColor; float2 nearTextureCoords = PSIn.TextureCoords*3; nearColor = tex2D(TextureSampler0, nearTextureCoords)*PSIn.TextureWeights.x; nearColor += tex2D(TextureSampler1, nearTextureCoords)*PSIn.TextureWeights.y; nearColor += tex2D(TextureSampler2, nearTextureCoords)*PSIn.TextureWeights.z; nearColor += tex2D(TextureSampler3, nearTextureCoords)*PSIn.TextureWeights.w;
37
For the near color, we multiply our TextureCoords by 3, so the texture is three times smaller, and thus three
times more detailed.
Now we have both our near and far colors for the pixel, we need to mix them according to the
blendFactor.
Replace:
Output.Color *= lightingFactor;
with:
Output.Color = lerp(nearColor, farColor, blendFactor); Output.Color *= lightingFactor;
This is simple linear interpolation (the “lerp” function linearly interpolates between two values), useful
when the blendFactor is between 0 and 1. When we multiply this with the lighting factor, and run the
resulting code, we should get the result shown below:
Here is the final version of effects.fx:
38
//---------------------------------------------------- //-- This effect file derived from: -- //-- www.riemers.net -- //-- Basic shaders -- //-- -- //-- Modified for MonoGame by John K. Bennett -- //-- -- //-- Use/modify as you like -- //-- -- //---------------------------------------------------- struct VertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float LightingFactor: TEXCOORD0; float2 TextureCoords: TEXCOORD1; }; struct PixelToFrame { float4 Color : COLOR0; }; //------- Constants -------- float4x4 xView; float4x4 xProjection; float4x4 xWorld; float3 xLightDirection; float xAmbient; bool xEnableLighting; bool xShowNormals; //------- Texture Samplers -------- Texture xTexture; sampler TextureSampler = sampler_state { texture = <xTexture>; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;}; Texture xTexture0; sampler TextureSampler0 = sampler_state { texture = <xTexture0>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture1; sampler TextureSampler1 = sampler_state { texture = <xTexture1>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture2; sampler TextureSampler2 = sampler_state { texture = <xTexture2>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xTexture3; sampler TextureSampler3 = sampler_state { texture = <xTexture3>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; //------- Technique: Pretransformed --------
39
VertexToPixel PretransformedVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; Output.Position = inPos; Output.Color = inColor; return Output; } PixelToFrame PretransformedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique Pretransformed { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 PretransformedVS(); PixelShader = compile ps_4_0_level_9_1 PretransformedPS(); } } //------- Technique: Colored -------- VertexToPixel ColoredVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame ColoredPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; }
40
technique Colored { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredVS(); PixelShader = compile ps_4_0_level_9_1 ColoredPS(); } } //------- Technique: ColoredNoShading -------- // No lighting or shading, so no normal info passed in VertexToPixel ColoredNoShadingVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; return Output; } PixelToFrame ColoredNoShadingPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique ColoredNoShading { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredNoShadingVS(); PixelShader = compile ps_4_0_level_9_1 ColoredNoShadingPS(); } } //------- Technique: Textured -------- VertexToPixel TexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld));; Output.LightingFactor = 1;
41
if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame TexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Textured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 TexturedVS(); PixelShader = compile ps_4_0_level_9_1 TexturedPS(); } } //------- Technique: Multitextured -------- struct MTVertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float3 Normal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 LightDirection : TEXCOORD2; float4 TextureWeights : TEXCOORD3; float Depth : TEXCOORD4; }; struct MTPixelToFrame { float4 Color : COLOR0; }; MTVertexToPixel MultiTexturedVS(float4 inPos : POSITION, float3 inNormal : NORMAL, float2 inTexCoords : TEXCOORD0, float4 inTexWeights : TEXCOORD1) { MTVertexToPixel Output = (MTVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Normal = (float3) mul(normalize(float4(inNormal, 0.0)), xWorld); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -xLightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; Output.Depth = Output.Position.z / Output.Position.w;
42
return Output; } MTPixelToFrame MultiTexturedPS(MTVertexToPixel PSIn) { MTPixelToFrame Output = (MTPixelToFrame)0; float lightingFactor = 1; float blendDistance = 0.99f; float blendWidth = 0.005f; float blendFactor = clamp((PSIn.Depth - blendDistance) / blendWidth, 0, 1); if (xEnableLighting) lightingFactor = saturate(saturate(dot(float4(PSIn.Normal, 0.0), PSIn.LightDirection)) + xAmbient); float4 farColor; farColor = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; farColor += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; farColor += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; farColor += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w; float4 nearColor; float2 nearTextureCoords = PSIn.TextureCoords * 3; nearColor = tex2D(TextureSampler0, nearTextureCoords)*PSIn.TextureWeights.x; nearColor += tex2D(TextureSampler1, nearTextureCoords)*PSIn.TextureWeights.y; nearColor += tex2D(TextureSampler2, nearTextureCoords)*PSIn.TextureWeights.z; nearColor += tex2D(TextureSampler3, nearTextureCoords)*PSIn.TextureWeights.w; Output.Color = lerp(nearColor, farColor, blendFactor); Output.Color *= lightingFactor; return Output; } technique MultiTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 MultiTexturedVS(); PixelShader = compile ps_4_0_level_9_1 MultiTexturedPS(); } }
Building a Skydome
Before we start drawing water, let’s first draw something that can be reflected by the water. We already
have a nice terrain; let’s do something about the solid black sky. We will use a “skydome” for this
purpose. A dome is a section of a sphere. Load dome.x and cloudMap.jpg into your content project (if
you have not already done so. Below you can see a wireframe of the dome model.
43
Next we will put a cloud texture on top of our skydome. Add two new Game1 instance variables: Texture2D cloudMap; Model skyDome;
Note that we are not using an array of textures, because we know the model only has one texture. Add
code to our LoadContent method to load the Model:
skyDome = Content.Load<Model>("dome"); skyDome.Meshes[0].MeshParts[0].Effect = effect.Clone();
We load the Model from file, and replace its effect with our own. Also load the cloudmap into the correct
variable by adding this code in the LoadTextures method: cloudMap = Content.Load<Texture2D>("cloudMap");
Now we have to draw our dome. We will create a new method to do this. Since our model ranges from -1
to +1, we need to scale it up by a factor of 500 so the near clipping plane does not create problems. We
will always reposition the skydome around our camera, and we will disable Z buffer writing when
rendering the skydome.
#region Skydome Methods private void DrawSkyDome(Matrix currentViewMatrix) { GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead; Matrix[] modelTransforms = new Matrix[skyDome.Bones.Count]; skyDome.CopyAbsoluteBoneTransformsTo(modelTransforms); Matrix wMatrix = Matrix.CreateTranslation(0, -0.3f, 0) * Matrix.CreateScale(500) * Matrix.CreateTranslation(cameraPosition); foreach (ModelMesh mesh in skyDome.Meshes)
44
{ foreach (Effect currentEffect in mesh.Effects) { Matrix mworldMatrix = modelTransforms[mesh.ParentBone.Index] * wMatrix;
currentEffect.CurrentTechnique = currentEffect.Techniques["SimpleTextured"];
currentEffect.Parameters["xAmbient"].SetValue(1.0f); currentEffect.Parameters["xWorld"].SetValue(mworldMatrix); currentEffect.Parameters["xView"].SetValue(currentViewMatrix); currentEffect.Parameters["xProjection"].SetValue(projectionMatrix); currentEffect.Parameters["xTexture"].SetValue(cloudMap); currentEffect.Parameters["xEnableLighting"].SetValue(false); } mesh.Draw(); } GraphicsDevice.DepthStencilState = DepthStencilState.Default; } #endregion
Besides scaling up the dome by factor 500 and positioning it over the camera, we also move it slightly
downward, so its edges are below the camera. This makes sure the largest part of the camera’s view
frustum is covered by the dome. Next, we pass the cloudMap texture and render the dome using the
Textured technique. Finally, we need to call this method from within our Draw method. However, we
need to make this call BEFORE we draw the terrain. Put this code in the Draw method:
DrawSkyDome(viewMatrix);
Finally, we need to add a new technique to our effects.fx file. This is because the MonoGame shader
compiler is more picky than XNA about unspecified inputs. In our case, the dome.x model file only
contains position and texture coordinate information, but our “Textured” technique expects lighting
normal information for every vertex. The XNA shader compiler was happy to ignore this omission, but
Monogame is more eager to protect us from ourselves. So, add the following new technique to
effects.fx:
//------- Technique: SimpleTextured -------- VertexToPixel SimpleTexturedVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; return Output; } PixelToFrame SimpleTexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient;
45
return Output; } technique SimpleTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 SimpleTexturedVS(); PixelShader = compile ps_4_0_level_9_1 SimpleTexturedPS(); } }
Now run this code. You should see something like the following image:
Introduction to Water
Now that we have removed every single pixel of solid background color on our screen, we move will
move on to the most complex terrain technique: water. Is’t not that realistic water is incredibly
complicated or difficult to implement; it just takes several steps to get it right. However, you will be
pleased with the result - the addition of realistic water to a 3D scene will greatly enhance its quality.
We will review the entire process before we dive in to the details.
46
For 3D games, you generally have two kinds of water: ocean water and lake water. The difference is the
height and length of the waves: for ocean water, we need a lot of vertices for which the height is adjusted
in the vertex shader. For lake water, as in our case, we can manage with a completely flat surface, upon
which we will create the illusion of ripples. Because of this, to draw the flat surface of our water we will
only need two triangles! The effect of water is completely created in the pixel shader, as we will see
below.
Recall that in the pixel shader we are primarily concerned with a single question: for each pixel of the
water, what will its color be? We could simply take a texture of some waves, and put it over the surface.
But in reality, the color of water depends completely on its surroundings: it is partly a mirror, so we see
the reflections of the surrounding scene in the water (this is called the reflective color). But water is also
partly transparent, so we also see a bit of the underlying sand shining through (this is called the refractive
color). So, the final color of the water is a clever combination of the reflective and the refractive color.
The flowchart below depicts all of the steps we will go through in the process of creating realistic water:
47
We will first need to know the reflective and refractive color for each pixel, so we will begin by rendering
these maps into two textures. Imagine that for each pixel we would only use the reflection color. This
would give a perfect mirror. But real water never looks like a real mirror; it is always deformed by
ripples. So before our pixel shader will sample the color from both maps, we will add a small deformation
to the texture coordinates of each pixel, which will simulate these ripples.
Next, both colors are blended together in such a way that when we look straight into the water, all we see
is the refractive color (the underlying sand). When we look out over the water from a shallow angle, all
we will see is the reflective color. The blendFactor that handles this difference is called the “Fresnel
term,” named for the French physicist Augustin-Jean Fresnel who studied optics and is most famously
known for the lenses he creased for lighthouses, which turned out to be useful in many other applications
(including solar ovens!).
The water color at this point would be the color of perfectly pure and clean water. To make our water a bit
more realistic, at the end we will blend in a bit of grayish-green-blue. (As a small extension, we could
also sample the heightmap of the terrain in the pixel shader, so deeper patches of water could have a
deeper as their refractive color.)
After these basic computations, we will add some bump mapping, so we can add specular reflections
(where the sunlight is directly reflected in the water). Also, reflections of the mountain will be deformed
by the ripples. And as a last note: the water very close to the border is a bit more blue-ish, which is due to
the last step we will apply.
Rendering the Refractive Map
As a first step towards drawing our water, we will render the refractive map into a texture. We will need
this map for each pixel of our water, to determine the color of what’s underneath that pixel.
In fact, this almost comes down to rendering the scene, as we see it through the camera, into a texture.
However, there are cases where some hills could obstruct our view to the bottom of the river behind them.
This would mean that for the pixels of that river, we would sample the color of the hill, instead of the
bottom of the river.
As we’ll be rendering to a texture, we’ll need these Game1 constants and variables (by now you know
where to put them):
// Water Constants
const float WATER_HEIGHT = 5.0f; #region Game1 Instance Vars
… RenderTarget2D refractionRenderTarget; Texture2D refractionMap;
48
The constant WATER_HEIGHT indicates how high we want our water to be positioned.
We need to initialize the refractionRenderTarget, so add this code to our LoadContent method:
PresentationParameters pp = device.PresentationParameters; refractionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat);
We only want to draw the things that are BELOW the water, and clip everything above the water away.
To do this, we use a mechanism called a “clip plane”. The name explains what it does: it is a plane, and
the part of the scene on one side of that plane is clipped away. In our case, we will define a horizontal
plane at the height of the water, so everything above that plane will not be drawn. We will also have to
modify our pixel shader to actually do the clipping for us; that step is discussed below.
Defining a plane generally in 3D space can be complicated. The easiest way to define a plane is to specify
the normal direction of the plane, and its shortest distance to the (0,0,0) point of the scene. For each
combination of normal and distance, there is only one plane. In our case, this is pretty easy because our
plane is completely horizontal: in that case, the normal is simply the (0,1,0) Up-vector, and the distance to
the (0,0,0) point is the height level of our water. Because the lowest points of our terrain have Y height
coordinate 0, we cannot define the water at Y coordinate 0, as the water would be lower than all vertices
in our terrain. Instead, we will say the water is at Y coordinate WATER_HEIGHT (5 in this case), which we
will use as a clip plane, so everything above this plane will not be drawn.
We will start by creating the plane; we need the height and the normal direction. Then we need to specify
whether we want to clip everything away below, or above, the plane. We will pass this information to the
method as the Boolean value clipSide. If we specify clipSide=false, we want to clip everything away
below the plane. If we want the clip everything away above the plane, we will specify clipSide=true,
which results in a sign change of the four coefficients.
We first normalize to ensure that our normal is of unity length. Next, we create the plane coefficients,
from which MonoGame can create the plane. As the clipping will occur in hardware after the vertices
have passed the vertex shader, the vertices that will be compared to the plane will already be in camera
space coordinates. This means we need to transform our plane coefficients with the inverse of the camera
matrix, before creating the plane from these coefficients. Recall that the camera matrix is the combination
of the world, view and projection matrices. We get the inverse of a matrix by inverting it and taking the
transpose of the inverted matrix. Note that the vertices of the terrain and water are already defined in
absolute World space, so we use Matrix.Identity as the World matrix, or just leave the World matrix out
of the computation. Now we can transform our plane coefficients into the correct space, and create our
plane, as follows:
#region Water Methods
private Plane CreatePlane(float height, Vector3 planeNormalDirection, Matrix currentViewMatrix, bool clipSide) { planeNormalDirection.Normalize(); Vector4 planeCoeffs = new Vector4(planeNormalDirection, height);
49
if (clipSide) planeCoeffs *= -1; Matrix worldViewProjection = currentViewMatrix * projectionMatrix; Matrix inverseWorldViewProjection = Matrix.Invert(worldViewProjection); inverseWorldViewProjection = Matrix.Transpose(inverseWorldViewProjection); planeCoeffs = Vector4.Transform(planeCoeffs, inverseWorldViewProjection); Plane finalPlane = new Plane(planeCoeffs); return finalPlane; }
#endregion
With our plane ready, we are ready to define a method DrawRefractionMap, which, well, draws the
refraction map. Add this code to our new Water Methods region:
private void DrawRefractionMap()
{ Plane refractionPlane = CreatePlane(WATER_HEIGHT + 1.5f, new Vector3(0, 1, 0), viewMatrix, false); effect.Parameters["ClipPlane0"].SetValue(new Vector4(refractionPlane.Normal, refractionPlane.D)); // Enable clipping for the purpose of creating a refraction map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(refractionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawTerrain(viewMatrix); refractionMap = refractionRenderTarget; // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); }
Don’t worry that Visual Studio is flagging an error (the absence of a DrawTerrain() method); we will fix
this below).
The first line creates a horizontal plane, a bit above the value of the WATER_HEIGHT variable. The last
argument indicates that we only want the things under the plane to be rendered. The next lines
communicate with our HLSL shader code (which we will modify in a moment) to enable clipping, and to
tell the graphics card how to create the clipping plane.
Once the clip plane is enabled, we create a render target (a place for the graphics card to send the output
of the shader computations in lieu of the display) named refractionRenderTarget, and we clean it (set all
pixels to black). Then we render the terrain onto this render target (only the part below the clip plane will
50
be rendered), after which we store the contents of the render target into the refractionMap texture and
disable clipping.
Now we have to modify the shader code associated with drawing the terrain. Open the effects.fx file for
editing and make the following changes to the Multitextured technique (additions are shown in yellow;
“//...” means other code that you leave alone.):
//... //------- Global Constants Passed Into the Effects File-------- float4x4 xView; float4x4 xProjection; float4x4 xWorld; float3 xLightDirection; float xAmbient; bool xEnableLighting; bool xShowNormals; bool Clipping; float4 ClipPlane0; //...
struct MTVertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float3 Normal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 LightDirection : TEXCOORD2; float4 TextureWeights : TEXCOORD3; float Depth : TEXCOORD4; float4 clipDistances : TEXCOORD5; }; //... MTVertexToPixel MultiTexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0, float4 inTexWeights: TEXCOORD1) { MTVertexToPixel Output = (MTVertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Normal = mul(normalize(inNormal), xWorld); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -xLightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; Output.Depth = Output.Position.z/Output.Position.w; Output.clipDistances = dot(inPos, ClipPlane0); return Output; } MTPixelToFrame MultiTexturedPS(MTVertexToPixel PSIn)
51
{ MTPixelToFrame Output = (MTPixelToFrame)0; if (Clipping) clip(PSIn.clipDistances); //...
So what have we just done? First, we created two global constants (Clipping and ClipPlane0) to receive
the information passed in from our Game1.cs code. Then we created an MTVertexToPixel struct member
(clipDistances) to hold the MTVertexToPixel output. The clipDistances are computed by taking the dot
product of the input positions and the clip plane. This passes all of the information that the pixel shader
needs to clip the correct pixels in 3D space. Finally, we use the HLSL intrinsic function clip (which will
discard any pixels whose value is 0 or less) to perform the actual clipping.
Finally, we need to encapsulate our code that draws the terrain into a callable function, so that we can call
it from our water code. Create a new method in the Terrain Methods region called DrawTerrain, and
populate that method with the existing code from the current Draw method that is highlighted below.
Here is what your new method should look like:
private void DrawTerrain(Matrix currentViewMatrix) { LIGHT_DIRECTION.Normalize(); effect.Parameters["xLightDirection"].SetValue(LIGHT_DIRECTION); effect.Parameters["xAmbient"].SetValue(AMBIENT_LIGHT_LEVEL); effect.Parameters["xEnableLighting"].SetValue(true); effect.CurrentTechnique = effect.Techniques["MultiTextured"]; effect.Parameters["xTexture0"].SetValue(sandTexture); effect.Parameters["xTexture1"].SetValue(grassTexture); effect.Parameters["xTexture2"].SetValue(rockTexture); effect.Parameters["xTexture3"].SetValue(snowTexture); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xWorld"].SetValue(worldMatrix); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.Indices = myIndexBuffer; device.SetVertexBuffer(myVertexBuffer); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, indices.Length / 3); } }
Then, replace all of the code that you just copied from the Draw method with a call to DrawTerrain (after
the DrawSkyDome () call):
DrawTerrain(viewMatrix);
52
Now we are ready to call DrawRefractionMap at the beginning of the Draw method (BEFORE the
device.Clear):
DrawRefractionMap();
When you run this code, you should get the same result as last section, but in the background, the
refraction map is stored into an image to be used later.
Rendering the Reflection Map
Now that we have created a clip plane, rendered the part of the scene that is under this clip plane, and
created the refraction map, we will use the same technique to render the reflection map into a texture.
This texture is needed, because for each pixel of our water, we need to know the reflective color.
We will need another texture to render into, so let’s define these variables:
RenderTarget2D reflectionRenderTarget;
Texture2D reflectionMap;
And initialize them in our LoadContent method:
reflectionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth,
pp.BackBufferHeight, false, pp.BackBufferFormat,
pp.DepthStencilFormat);
Now let’s think about how we can render the reflections onto a texture. We will need to reposition our
camera, as we need to see the scene as seen by the water. We just need to know where exactly to position
that camera, and where it needs to point.
Consider the figure below. Camera A represents where the actual camera (our eyes) is located. To see the
mountain correctly reflected on the water, Camera B needs to be positioned at the same X and Z
coordinates as Camera A, but with the Y coordinate mirrored over the water plane.
53
Thus, for each pixel of the water, the reflective color as seen by Camera A is the same color that Camera
B would see. So, in order to know the reflective colors of each pixel of the water, we have to render the
scene as seen by Camera B onto a texture, and then place that texture on the surface of the water.
However, there is one additional problem: part of the terrain that is under the water may obstruct Camera
B’s view below the water. This means that Camera B would only render the bottom of the terrain onto the
texture, which is not what we want. To handle this potential problem, we will clip away all parts of the
terrain that are below the water, as we did (in the reverse) for the refractive image
We begin our implementation by defining the matrix corresponding to Camera B. Add this Game1
instance variable:
Matrix reflectionViewMatrix;
The UpdateViewMatrix method (In Camera Methods) is where we will populate this matrix. Recall that
to define a camera matrix, we need a position, target and up vector for the camera.
We already know the position: the X and Z coordinates of Camera B are the same as those of Camera A,
and the Y coordinate of Camera B is the negative of the distance between the water and Camera A. That
distance is equal to the negative of Camera A’s height, plus two time the water height (work this out for
yourself). Add this code to UpdateViewMatrix:
Vector3 reflCameraPosition = cameraPosition; reflCameraPosition.Y = -cameraPosition.Y + WATER_HEIGHT * 2;
To obtain the target of Camera B, we can apply the same reasoning, so add this code as well:
Vector3 reflTargetPos = cameraFinalTarget; reflTargetPos.Y = -cameraFinalTarget.Y + WATER_HEIGHT * 2;
Now we still need to define the Up vector. For this we need a little math. Recall that the cross product of
two vectors is the vector that is perpendicular to both vectors. So, if we take the cross product of the
forward and side vector of camera B, we get the upvector of camera B. This is the code that does exactly
this:
54
Vector3 cameraRight = Vector3.Transform(new Vector3(1, 0, 0), cameraRotation); Vector3 invUpVector = Vector3.Cross(cameraRight, reflTargetPos - reflCameraPosition);
Now we have the position, target and up vector for Camera B, we’re ready to define its View matrix:
reflectionViewMatrix = Matrix.CreateLookAt(reflCameraPosition, reflTargetPos, invUpVector);
With the View matrix of Camera B defined, we can create a DrawReflectionMap method in the Water
Methods region. This will look a lot like the DrawRefractionMap code:
private void DrawReflectionMap()
{
Plane reflectionPlane = CreatePlane(WATER_HEIGHT - 0.5f, new Vector3(0, -1, 0), reflectionViewMatrix, true); effect.Parameters["ClipPlane0"].SetValue(new Vector4(-reflectionPlane.Normal, -reflectionPlane.D)); // Enable clipping for the purpose of creating a reflection map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(reflectionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawSkyDome(reflectionViewMatrix); DrawTerrain(reflectionViewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); reflectionMap = reflectionRenderTarget; }
The first line creates a plane as before, except that we need to flip the sign of the Y parameter, and we
need to pass in the view matrix of Camera B (instead of Camera A), and we need to indicate that we only
want to render things that are ABOVE the clipping plane. Next, we activate the clipping plane and define
the correct render target for our graphics card, and clean the render target. Then the Terrain and Skydome
are rendered. Note that in this method, we pass in the reflectionViewMatrix, as we need the terrain and
skybox as seen by camera B.
Finally, we need to call this method from the Draw method (after the call to DrawRefractionMap):
DrawReflectionMap();
At this point we have created textures for the refractive and reflective images, but we have not used them.
Let’s fix that now, beginning with reflection.
Perfect Mirror Reflection
55
Now that we have rendered a reflective texture we will paste the texture over our mirror: the water. That
way our water will look like a perfect mirror. This is done through projective texturing. We will begin by
creating the water (our mirror). We use two large triangles to do this, and paste the reflective texture onto
the rectangle created by these triangles. Begin by defining the buffer that will hold the triangle vertices
used to define this rectangle in Game1 Instance Vars:
VertexBuffer waterVertexBuffer;
Now create a method (in our Water Methods region) to define the water triangle vertices (that will create
a rectangle that spans the whole terrain):
private void SetUpWaterVertices()
{ VertexPositionTexture[] waterVertices = new VertexPositionTexture[6]; waterVertices[0] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[2] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, - terrainLength), new Vector2(1, 0)); waterVertices[1] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, -terrainLength), new Vector2(0, 0)); waterVertices[3] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[5] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, 0), new Vector2(1, 1)); waterVertices[4] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, - terrainLength), new Vector2(1, 0)); waterVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, waterVertices.Length, BufferUsage.WriteOnly); waterVertexBuffer.SetData(waterVertices); }
Although strictly speaking we don’t need to pass texture coordinates with the vertices to do projective
texturing (as the texture coordinate will be calculated in the vertex shader), later on we will need to be
able to specify texture coordinates here. So we are using VertexPositionTexture vertices to prepare for
that eventuality.
We need to call SetUpWaterVertices at the end of the LoadContent method:
SetUpWaterVertices();
Now add the method that draws the two triangles, using the Water technique which we will create in a
minute:
private void DrawWater(float time)
{ effect.CurrentTechnique = effect.Techniques["Water"];
56
Matrix worldMatrix = Matrix.Identity; effect.Parameters["xWorld"].SetValue(worldMatrix); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xReflectionView"].SetValue(reflectionViewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); //effect.Parameters["xRefractionMap"].SetValue(refractionMap); effect.Parameters["xReflectionMap"].SetValue(reflectionMap); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.SetVertexBuffer(waterVertexBuffer); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2); } }
Notice that we pass in the reflection map, but we comment out the line that passes the refraction map.
This is because the Monogame HLSL compiler complains if pass in a texture and then do not use it. We
will uncomment this line after we update our shader code to use the refraction map.
We need to call the DrawWater method at the end of our main Draw method (but before Base.Draw), and
not from the DrawRefectionMap or DrawRefractionMap (why?):
DrawWater(time);
And we need to create the time variable for the DrawWater method to use (put this at the top of Draw):
float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 100.0f;
Let’s move over to the HLSL code (effects.fx), First, we need new global constant:
float4x4 xReflectionView;
and texture samplers for the refraction and reflection textures:
Texture xReflectionMap; sampler ReflectionSampler = sampler_state { texture = <xReflectionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xRefractionMap; sampler RefractionSampler = sampler_state { texture = <xRefractionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; };
Now define the actual Water technique (at the end of effects.fx):
//------- Technique: Water -------- struct WVertexToPixel { float4 Position : POSITION; float4 ReflectionMapSamplingPos : TEXCOORD1; }; struct WPixelToFrame
57
{ float4 Color : COLOR0; }; WVertexToPixel WaterVS(float4 inPos : POSITION, float2 inTex : TEXCOORD) { WVertexToPixel Output = (WVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); float4x4 preReflectionViewProjection = mul(xReflectionView, xProjection); float4x4 preWorldReflectionViewProjection = mul(xWorld, preReflectionViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.ReflectionMapSamplingPos = mul(inPos, preWorldReflectionViewProjection); return Output; } WPixelToFrame WaterPS(WVertexToPixel PSIn) { WPixelToFrame Output = (WPixelToFrame)0; float2 ProjectedTexCoords; ProjectedTexCoords.x = PSIn.ReflectionMapSamplingPos.x / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; ProjectedTexCoords.y = -PSIn.ReflectionMapSamplingPos.y / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; Output.Color = tex2D(ReflectionSampler, ProjectedTexCoords); return Output; } technique Water { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 WaterVS(); PixelShader = compile ps_4_0_level_9_1 WaterPS(); } }
Since the two triangles of the water are completely flat, we do not need to pass any normal data; if we
need the normal, it’s always pointing upward. We only need to calculate the sampling coordinates for the
reflection map. Notice that we only calculate two output values in WVertexToPixel: the 2D screen
position of the current vertex and the corresponding 2D position as it would be seen by Camera B. We use
this second 2D position as the sampling coordinate for our reflective texture in the pixel shader.
Run this code, and you should see a perfect mirror of the sky at the water level. In the interest of space, I
won’t list the code here. If you have a problem, look below for the next code sample.
58
Rippling Water – Basic Bump Mapping
Right now our water looks like a mirror; we need to add a rippling effect to make it look more realistic.
To do this, we use a technique called “bump mapping,” which works follows: for each pixel of the water,
instead of sampling the reflection map at the correct position, we will change the sampling position a little
bit differently for each pixel. This technique is sometimes called “texture coordinate perturbation”. All the
needed texture coordinate calculations are performed in the pixel shader, which makes this technique fast.
Of course, we still need to figure out how to go about adjusting the texture coordinates of each pixel.
Consider how real water looks: if there is absolutely no wind, the water is flat, and looks like a mirror. If
there is wind, there are waves, whose height varies with the wind speed. At the top and bottom of the
waves, the water is still mostly flat; thus the reflections at the top and bottom the reflections are still
mostly perfect. As a result, we need the smallest perturbations at the bottom and top of each wave.
However, the sides of the waves are not horizontal, so there we will have the largest perturbations.
What we need is a measurement of the ‘flatness’ of each pixel in the water. For this, we will use an image
of real waves. We are not interested in the wave image itself, but in the flatness of each pixel. This is
exactly what a gradient map is: if you have a 256x256 wave image A, its gradient map B is also a
256x256 image where the color of each pixel indicates the difference in color compared to the same pixel
in image A.
A gradient map is the mathematical term; in games this usually is called the bump map. Below you can
see such a bump map:
Each pixel of this image contains three values, one for each color. These three values correspond to the
three components of the normal vector in the map. For example, if the surface would be completely flat in
a certain pixel, the normal vector would be pointing straight upward. So the X and Z components would
be 0, while the Y component would be 1. Using colors, the Red and Green components would be 0, the
Blue would be 1, so the whole map would be completely blue (this is not exactly what we will find in a
59
bump map, as we will see in a moment).
However, since the water isn’t flat, the normal is different for each pixel, so the Red and Green
components contain exactly how much the normal is pointing in the X and Z directions. So the larger the
red and green components, the less the normal vector will point upwards, and the less flat the water will
be. The blue color component is only provided so we can create the normal vector, for which we need
three components. However, in this case we are only interested in how flat/rippled the water is at a certain
pixel, so the red and green components are all we need.
Add the file waterbump.dds to your content project, as we have done before.
There is not a lot we need to do in Game1.cs. First, define some new water constants that we will use to
characterize our water waves:
public const float WAVE_LENGTH = 0.2f; public const float WAVE_HEIGHT= 0.3f;
Now, declare the bumpmap variable:
Texture2D waterBumpMap;
Fill it in the LoadTextures method:
waterBumpMap = Content.Load<Texture2D>("waterbump");
And pass all three new parameters to the shader in the DrawWater method:
effect.Parameters["xWaterBumpMap"].SetValue(waterBumpMap); effect.Parameters["xWaveLength"].SetValue(WAVE_LENGTH); effect.Parameters["xWaveHeight"].SetValue(WAVE_HEIGHT);
That’s it for the Game1 code; let’s move on to the HLSL part. Declare two new global constants to accept
the wave length and height:
float xWaveLength;
float xWaveHeight;
And provide a sampler for the water bump map:
Texture xWaterBumpMap;
sampler WaterBumpMapSampler = sampler_state { texture = <xWaterBumpMap> ; magfilter =
LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;};
Now we need to update our shaders to sample the water bump map. Add a new member to the
WVertexToPixel struct: struct WVertexToPixel { float4 Position : POSITION;
60
float4 ReflectionMapSamplingPos : TEXCOORD1; float2 BumpMapSamplingPos : TEXCOORD2; };
And use this term in WaterVS: WVertexToPixel WaterVS(float4 inPos : POSITION, float2 inTex: TEXCOORD) { //... Output.ReflectionMapSamplingPos = mul(inPos, preWorldReflectionViewProjection); Output.BumpMapSamplingPos = inTex/xWaveLength; //...
Note that larger values of xWaveLength will make the texture coordinates smaller, and thus the bump map
will be stretched over a larger area.
On to the pixel shader. There, we first sample the bump map. At this moment, the red and green color
values of the bumpColor indicate how much the sampling coordinates of the reflection map should be
perturbated. So we need these values to range between -1 and +1. However, colors can only contain value
between 0 and 1. To fix this, values in the range [-1,1] are mapped to the range [0,1] before saving them
as a color. This is done by dividing by 2 and adding 0.5. As an example, if the normal is unadjusted, the X
and Z component are 0, and the Y component would be 1. When this value is converted to a color, we get
(0.5, 0.5, 1), which is the primary color found in most bump maps.
When we use the bump map, we need to remap the bump map values in the range [0,1] back to values in
the range [-1,1]. This conversion is just the opposite of the first conversion: we subtract .5 and multiply
by 2. When we are done, we compute the final texture coordinates we use to sample our reflection map.
To accomplish all of this, make the changes shown below:
WPixelToFrame WaterPS(WVertexToPixel PSIn)
{ WPixelToFrame Output = (WPixelToFrame)0; float4 bumpColor = tex2D(WaterBumpMapSampler, PSIn.BumpMapSamplingPos); float2 perturbation = xWaveHeight*(bumpColor.rg - 0.5f)*2.0f; float2 ProjectedTexCoords; ProjectedTexCoords.x = PSIn.ReflectionMapSamplingPos.x/PSIn.ReflectionMapSamplingPos.w/2.0f + 0.5f; ProjectedTexCoords.y = -PSIn.ReflectionMapSamplingPos.y/PSIn.ReflectionMapSamplingPos.w/2.0f + 0.5f; float2 perturbatedTexCoords = ProjectedTexCoords + perturbation; Output.Color = tex2D(ReflectionSampler, ProjectedTexCoords); Output.Color = tex2D(ReflectionSampler, perturbatedTexCoords); return Output; }
61
Run this code. You should finally give us something that looks more like water. However, we are not
done. Here is the code so far:
Game1.cs
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; namespace Lab8_Mono { /// <summary> /// This is the main type for your game. /// </summary> public class Game1 : Game { #region Constants //Lighting Constants Vector3 LIGHT_DIRECTION = new Vector3(1.0f, -1.0f, 1.0f); //use (1.0f, -1.0f, 1.0f) to flip light public const float AMBIENT_LIGHT_LEVEL = 0.7f; // Camera control constants public const float ROTATION_SPEED = 0.3f; public const float MOVE_SPEED = 30.0f; // Terrain texture mapping constants public const float MAX_HT = 30.0f; public const float MIN_HT = 0.0f; public const float SAND_UPPER = 0.266f * MAX_HT; // 8 public const float GRASS_MID = 0.4f * MAX_HT; // 12 public const float GRASS_RANGE = 0.2f * MAX_HT; // 12 +/- 6 public const float ROCK_MID = 0.666f * MAX_HT; // 20 public const float ROCK_RANGE = 0.2f * MAX_HT; // 20 +/- 6 public const float SNOW_LOWER = 0.8f * MAX_HT; // 24 // Water Constants const float WATER_HEIGHT = 5.0f; public const float WAVE_LENGTH = 0.2f; public const float WAVE_HEIGHT = 0.3f; #endregion #region Vertex Structs public struct VertexPositionColorNormal { public Vector3 Position; public Color Color; public Vector3 Normal; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration (
62
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Color, VertexElementUsage.Color, 0), new VertexElement(sizeof(float) * 3 + 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0) ); } #endregion #region Vertex Structs public struct VertexMultitextured { public Vector3 Position; public Vector3 Normal; public Vector4 TextureCoordinate; public Vector4 TexWeights; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0), new VertexElement(sizeof(float) * 6, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 0), new VertexElement(sizeof(float) * 10, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1) ); } #endregion #region Game1 Instance Vars GraphicsDeviceManager graphics; GraphicsDevice device; Effect effect; VertexMultitextured[] vertices; int[] indices; Matrix viewMatrix; Matrix projectionMatrix; VertexBuffer myVertexBuffer; IndexBuffer myIndexBuffer; VertexBuffer waterVertexBuffer; private int terrainWidth; private int terrainLength; private float[,] heightData; Matrix worldMatrix = Matrix.Identity; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity; Vector3 cameraPosition = new Vector3(130, 30, -50);
63
float leftrightRot = MathHelper.PiOver2; float updownRot = -MathHelper.Pi / 10.0f; MouseState originalMouseState; Texture2D grassTexture; Texture2D sandTexture; Texture2D rockTexture; Texture2D snowTexture; Texture2D cloudMap; Model skyDome; RenderTarget2D refractionRenderTarget; Texture2D refractionMap; RenderTarget2D reflectionRenderTarget; Texture2D reflectionMap; Matrix reflectionViewMatrix; Texture2D waterBumpMap; #endregion #region Class Game1 Constructor public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } #endregion #region Terrain Methods private void SetUpVertices() { vertices = new VertexMultitextured[terrainWidth * terrainLength]; for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainLength; y++) { vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / MAX_HT; vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / MAX_HT; vertices[x + y * terrainWidth].TexWeights.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MIN_HT) / SAND_UPPER, 0, 1); vertices[x + y * terrainWidth].TexWeights.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - GRASS_MID) / GRASS_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - ROCK_MID) / ROCK_RANGE, 0, 1);
64
vertices[x + y * terrainWidth].TexWeights.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MAX_HT) / SNOW_LOWER, 0, 1); float total = vertices[x + y * terrainWidth].TexWeights.X; total += vertices[x + y * terrainWidth].TexWeights.Y; total += vertices[x + y * terrainWidth].TexWeights.Z; total += vertices[x + y * terrainWidth].TexWeights.W; vertices[x + y * terrainWidth].TexWeights.X /= total; vertices[x + y * terrainWidth].TexWeights.Y /= total; vertices[x + y * terrainWidth].TexWeights.Z /= total; vertices[x + y * terrainWidth].TexWeights.W /= total; } } } private void SetUpIndices() { indices = new int[(terrainWidth - 1) * (terrainLength - 1) * 6]; int counter = 0; for (int y = 0; y < terrainLength - 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;
65
vertices[index3].Normal += normal; } for (int i = 0; i < vertices.Length; i++) vertices[i].Normal.Normalize(); } private void CopyToBuffers() { myVertexBuffer = new VertexBuffer(device, VertexMultitextured.vertexDeclaration, vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices); myIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly); myIndexBuffer.SetData(indices);
}
private void LoadHeightData(Texture2D heightMap) { terrainWidth = heightMap.Width; terrainLength = heightMap.Height; float minimumHeight = float.MaxValue; float maximumHeight = float.MinValue; Color[] heightMapColors = new Color[terrainWidth * terrainLength]; heightMap.GetData(heightMapColors); heightData = new float[terrainWidth, terrainLength]; for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) { heightData[x, y] = heightMapColors[x + y * terrainWidth].R; if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y]; if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y]; } for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * MAX_HT; } private void DrawTerrain(Matrix currentViewMatrix) { LIGHT_DIRECTION.Normalize(); effect.Parameters["xLightDirection"].SetValue(LIGHT_DIRECTION); effect.Parameters["xAmbient"].SetValue(AMBIENT_LIGHT_LEVEL); effect.Parameters["xEnableLighting"].SetValue(true); effect.CurrentTechnique = effect.Techniques["MultiTextured"]; effect.Parameters["xTexture0"].SetValue(sandTexture);
66
effect.Parameters["xTexture1"].SetValue(grassTexture); effect.Parameters["xTexture2"].SetValue(rockTexture); effect.Parameters["xTexture3"].SetValue(snowTexture); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xWorld"].SetValue(worldMatrix); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.Indices = myIndexBuffer; device.SetVertexBuffer(myVertexBuffer); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, indices.Length / 3); } } #endregion #region Texture Loads private void LoadTextures() { grassTexture = Content.Load<Texture2D>("grass"); sandTexture = Content.Load<Texture2D>("sand"); rockTexture = Content.Load<Texture2D>("rock"); snowTexture = Content.Load<Texture2D>("snow"); cloudMap = Content.Load<Texture2D>("cloudMap"); waterBumpMap = Content.Load<Texture2D>("waterbump"); } #endregion #region Camera Methods private void SetUpCamera() { UpdateViewMatrix(); projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 0.3f, 1000.0f); } private void UpdateViewMatrix() { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);
67
viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector); Vector3 reflCameraPosition = cameraPosition; reflCameraPosition.Y = -cameraPosition.Y + WATER_HEIGHT * 2; Vector3 reflTargetPos = cameraFinalTarget; reflTargetPos.Y = -cameraFinalTarget.Y + WATER_HEIGHT * 2; Vector3 cameraRight = Vector3.Transform(new Vector3(1, 0, 0), cameraRotation); Vector3 invUpVector = Vector3.Cross(cameraRight, reflTargetPos - reflCameraPosition); reflectionViewMatrix = Matrix.CreateLookAt(reflCameraPosition, reflTargetPos, invUpVector); } private void ProcessInput(float amount) { MouseState currentMouseState = Mouse.GetState(); if (currentMouseState != originalMouseState) { float xDifference = currentMouseState.X - originalMouseState.X; float yDifference = currentMouseState.Y - originalMouseState.Y; leftrightRot -= ROTATION_SPEED * xDifference * amount; updownRot -= ROTATION_SPEED * yDifference * amount; Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); UpdateViewMatrix(); } Vector3 moveVector = new Vector3(0, 0, 0); KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W)) moveVector += new Vector3(0, 0, -1); if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S)) moveVector += new Vector3(0, 0, 1); if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D)) moveVector += new Vector3(1, 0, 0); if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A)) moveVector += new Vector3(-1, 0, 0); if (keyState.IsKeyDown(Keys.Q)) moveVector += new Vector3(0, 1, 0); if (keyState.IsKeyDown(Keys.Z)) moveVector += new Vector3(0, -1, 0); AddToCameraPosition(moveVector * amount); } private void AddToCameraPosition(Vector3 vectorToAdd) { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation); cameraPosition += MOVE_SPEED * rotatedVector; UpdateViewMatrix(); } #endregion
68
#region Skydome Methods private void DrawSkyDome(Matrix currentViewMatrix) { GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead; Matrix[] modelTransforms = new Matrix[skyDome.Bones.Count]; skyDome.CopyAbsoluteBoneTransformsTo(modelTransforms); Matrix wMatrix = Matrix.CreateTranslation(0, -0.3f, 0) * Matrix.CreateScale(500) * Matrix.CreateTranslation(cameraPosition); foreach (ModelMesh mesh in skyDome.Meshes) { foreach (Effect currentEffect in mesh.Effects) { Matrix mworldMatrix = modelTransforms[mesh.ParentBone.Index] * wMatrix; currentEffect.CurrentTechnique = currentEffect.Techniques["SimpleTextured"]; currentEffect.Parameters["xAmbient"].SetValue(1.0f); currentEffect.Parameters["xWorld"].SetValue(mworldMatrix); currentEffect.Parameters["xView"].SetValue(currentViewMatrix); currentEffect.Parameters["xProjection"].SetValue(projectionMatrix); currentEffect.Parameters["xTexture"].SetValue(cloudMap); currentEffect.Parameters["xEnableLighting"].SetValue(false); } mesh.Draw(); } GraphicsDevice.DepthStencilState = DepthStencilState.Default; } #endregion #region Water Methods private Plane CreatePlane(float height, Vector3 planeNormalDirection, Matrix currentViewMatrix, bool clipSide) { planeNormalDirection.Normalize(); Vector4 planeCoeffs = new Vector4(planeNormalDirection, height); if (clipSide) planeCoeffs *= -1; Matrix worldViewProjection = currentViewMatrix * projectionMatrix; Matrix inverseWorldViewProjection = Matrix.Invert(worldViewProjection); inverseWorldViewProjection = Matrix.Transpose(inverseWorldViewProjection); planeCoeffs = Vector4.Transform(planeCoeffs, inverseWorldViewProjection); Plane finalPlane = new Plane(planeCoeffs); return finalPlane; } private void DrawRefractionMap() { Plane refractionPlane = CreatePlane(WATER_HEIGHT + 1.5f, new Vector3(0, 1, 0), viewMatrix, false);
69
effect.Parameters["ClipPlane0"].SetValue(new Vector4(refractionPlane.Normal, refractionPlane.D)); // Enable clipping for the purpose of creating a refraction map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(refractionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawTerrain(viewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); refractionMap = refractionRenderTarget;
}
private void DrawReflectionMap() { Plane reflectionPlane = CreatePlane(WATER_HEIGHT - 0.5f, new Vector3(0, -1, 0), reflectionViewMatrix, true); effect.Parameters["ClipPlane0"].SetValue(new Vector4(-reflectionPlane.Normal, -reflectionPlane.D)); // Enable clipping for the purpose of creating a reflection map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(reflectionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawSkyDome(reflectionViewMatrix); DrawTerrain(reflectionViewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); reflectionMap = reflectionRenderTarget; } private void SetUpWaterVertices() { VertexPositionTexture[] waterVertices = new VertexPositionTexture[6]; waterVertices[0] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[2] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertices[1] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, -terrainLength), new Vector2(0, 0)); waterVertices[3] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[5] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, 0), new Vector2(1, 1));
70
waterVertices[4] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, waterVertices.Length, BufferUsage.WriteOnly); waterVertexBuffer.SetData(waterVertices); } private void DrawWater(float time) { effect.CurrentTechnique = effect.Techniques["Water"]; Matrix worldMatrix = Matrix.Identity; effect.Parameters["xWorld"].SetValue(worldMatrix); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xReflectionView"].SetValue(reflectionViewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); //effect.Parameters["xRefractionMap"].SetValue(refractionMap); effect.Parameters["xReflectionMap"].SetValue(reflectionMap); effect.Parameters["xWaterBumpMap"].SetValue(waterBumpMap); effect.Parameters["xWaveLength"].SetValue(WAVE_LENGTH); effect.Parameters["xWaveHeight"].SetValue(WAVE_HEIGHT); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.SetVertexBuffer(waterVertexBuffer); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2); } } #endregion #region Initialize /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // JKB Note: This ordering and repetition of graphics profile commands addresses a // current bug in MonoGame 3.7. If we don't do it this way the window defaults to // a (small) fixed size. Also, MonoGame says it defaults to Reach, but it doesn't, so // we have to set HiDef explicitily here in order to use 32-bit index buffers. graphics.GraphicsProfile = GraphicsProfile.HiDef; graphics.IsFullScreen = false; graphics.ApplyChanges();
71
graphics.PreferredBackBufferWidth = 800; graphics.PreferredBackBufferHeight = 800; graphics.ApplyChanges(); Window.Title = "Lab8_Mono - Terrain Tutorial II"; base.Initialize(); } #endregion #region LoadContent /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { device = graphics.GraphicsDevice; effect = Content.Load<Effect>("effects"); skyDome = Content.Load<Model>("dome"); skyDome.Meshes[0].MeshParts[0].Effect = effect.Clone(); Texture2D heightMap = Content.Load<Texture2D>("heightmap2"); LoadHeightData(heightMap); Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); originalMouseState = Mouse.GetState(); SetUpCamera(); SetUpVertices(); SetUpIndices(); CalculateNormals(); CopyToBuffers(); LoadTextures(); PresentationParameters pp = device.PresentationParameters; refractionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); reflectionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); SetUpWaterVertices(); } #endregion #region UnloadContent /// <summary> /// UnloadContent will be called once per game and is the place to unload /// game-specific content. /// </summary>
72
protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } #endregion #region Update /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyState = Keyboard.GetState(); //Rotation if (keyState.IsKeyDown(Keys.PageUp)) { worldRotation = Matrix.CreateRotationY(0.01f); } else if (keyState.IsKeyDown(Keys.PageDown)) { worldRotation = Matrix.CreateRotationY(-0.01f); } else { worldRotation = Matrix.CreateRotationY(0); } float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f; ProcessInput(timeDifference); worldMatrix *= worldTranslation * worldRotation; base.Update(gameTime); } #endregion #region Draw /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 100.0f; DrawRefractionMap(); DrawReflectionMap();
73
device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawSkyDome(viewMatrix); DrawTerrain(viewMatrix); DrawWater(time); base.Draw(gameTime); } #endregion }
}
effects.fx
//---------------------------------------------------- //-- This effect file derived from: -- //-- www.riemers.net -- //-- Basic shaders -- //-- -- //-- Modified for MonoGame by John K. Bennett -- //-- -- //-- Use/modify as you like -- //-- -- //---------------------------------------------------- struct VertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float LightingFactor: TEXCOORD0; float2 TextureCoords: TEXCOORD1; }; struct PixelToFrame { float4 Color : COLOR0; }; //------- Constants -------- float4x4 xView; float4x4 xProjection; float4x4 xWorld; float3 xLightDirection; float xAmbient; bool xEnableLighting; bool xShowNormals; bool Clipping; float4 ClipPlane0; float4x4 xReflectionView; float xWaveLength; float xWaveHeight;
74
//------- Texture Samplers -------- Texture xTexture; sampler TextureSampler = sampler_state { texture = <xTexture>; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;}; Texture xTexture0; sampler TextureSampler0 = sampler_state { texture = <xTexture0>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture1; sampler TextureSampler1 = sampler_state { texture = <xTexture1>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture2; sampler TextureSampler2 = sampler_state { texture = <xTexture2>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xTexture3; sampler TextureSampler3 = sampler_state { texture = <xTexture3>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xRefractionMap; sampler RefractionSampler = sampler_state { texture = <xRefractionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xReflectionMap; sampler ReflectionSampler = sampler_state { texture = <xReflectionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xWaterBumpMap; sampler WaterBumpMapSampler = sampler_state { texture = <xWaterBumpMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; //------- Technique: Pretransformed -------- VertexToPixel PretransformedVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; Output.Position = inPos; Output.Color = inColor; return Output; } PixelToFrame PretransformedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique Pretransformed { pass Pass0
75
{ VertexShader = compile vs_4_0_level_9_1 PretransformedVS(); PixelShader = compile ps_4_0_level_9_1 PretransformedPS(); } } //------- Technique: Colored -------- VertexToPixel ColoredVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame ColoredPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Colored { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredVS(); PixelShader = compile ps_4_0_level_9_1 ColoredPS(); } } //------- Technique: ColoredNoShading -------- // No lighting or shading, so no normal info passed in VertexToPixel ColoredNoShadingVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; return Output;
76
} PixelToFrame ColoredNoShadingPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique ColoredNoShading { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredNoShadingVS(); PixelShader = compile ps_4_0_level_9_1 ColoredNoShadingPS(); } } //------- Technique: Textured -------- VertexToPixel TexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame TexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Textured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 TexturedVS(); PixelShader = compile ps_4_0_level_9_1 TexturedPS(); } }
77
//------- Technique: SimpleTextured -------- VertexToPixel SimpleTexturedVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; return Output; } PixelToFrame SimpleTexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; }
technique SimpleTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 SimpleTexturedVS(); PixelShader = compile ps_4_0_level_9_1 SimpleTexturedPS(); } } //------- Technique: Multitextured -------- struct MTVertexToPixel { float4 Position : POSITION0; float4 Color : COLOR0; float3 Normal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 LightDirection : TEXCOORD2; float4 TextureWeights : TEXCOORD3; float Depth : TEXCOORD4; float4 clipDistances : TEXCOORD5; }; struct MTPixelToFrame { float4 Color : COLOR0; }; MTVertexToPixel MultiTexturedVS(float4 inPos : POSITION, float3 inNormal : NORMAL, float2 inTexCoords : TEXCOORD0, float4 inTexWeights : TEXCOORD1) { MTVertexToPixel Output = (MTVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection);
78
Output.Normal = (float3) mul(normalize(float4(inNormal, 0.0)), xWorld); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -xLightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; Output.Depth = Output.Position.z / Output.Position.w; Output.clipDistances = dot(inPos, ClipPlane0); return Output; } MTPixelToFrame MultiTexturedPS(MTVertexToPixel PSIn) { MTPixelToFrame Output = (MTPixelToFrame)0; if (Clipping) clip(PSIn.clipDistances); float lightingFactor = 1; float blendDistance = 0.99f; float blendWidth = 0.005f; float blendFactor = clamp((PSIn.Depth - blendDistance) / blendWidth, 0, 1); if (xEnableLighting) lightingFactor = saturate(saturate(dot(float4(PSIn.Normal, 0.0), PSIn.LightDirection)) + xAmbient); float4 farColor; farColor = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; farColor += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; farColor += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; farColor += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w; float4 nearColor; float2 nearTextureCoords = PSIn.TextureCoords * 3; nearColor = tex2D(TextureSampler0, nearTextureCoords)*PSIn.TextureWeights.x; nearColor += tex2D(TextureSampler1, nearTextureCoords)*PSIn.TextureWeights.y; nearColor += tex2D(TextureSampler2, nearTextureCoords)*PSIn.TextureWeights.z; nearColor += tex2D(TextureSampler3, nearTextureCoords)*PSIn.TextureWeights.w; Output.Color = lerp(nearColor, farColor, blendFactor); Output.Color *= lightingFactor; return Output; } technique MultiTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 MultiTexturedVS(); PixelShader = compile ps_4_0_level_9_1 MultiTexturedPS(); } } //------- Technique: Water -------- struct WVertexToPixel
79
{ float4 Position : POSITION; float4 ReflectionMapSamplingPos : TEXCOORD1; float2 BumpMapSamplingPos : TEXCOORD2; }; struct WPixelToFrame { float4 Color : COLOR0; }; WVertexToPixel WaterVS(float4 inPos : POSITION, float2 inTex : TEXCOORD) { WVertexToPixel Output = (WVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); float4x4 preReflectionViewProjection = mul(xReflectionView, xProjection); float4x4 preWorldReflectionViewProjection = mul(xWorld, preReflectionViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.ReflectionMapSamplingPos = mul(inPos, preWorldReflectionViewProjection); Output.BumpMapSamplingPos = inTex / xWaveLength; return Output; } WPixelToFrame WaterPS(WVertexToPixel PSIn) { WPixelToFrame Output = (WPixelToFrame)0; float4 bumpColor = tex2D(WaterBumpMapSampler, PSIn.BumpMapSamplingPos); float2 perturbation = xWaveHeight * (bumpColor.rg - 0.5f)*2.0f; float2 ProjectedTexCoords; ProjectedTexCoords.x = PSIn.ReflectionMapSamplingPos.x / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; ProjectedTexCoords.y = -PSIn.ReflectionMapSamplingPos.y / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; float2 perturbatedTexCoords = ProjectedTexCoords + perturbation; Output.Color = tex2D(ReflectionSampler, perturbatedTexCoords); return Output; } technique Water { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 WaterVS(); PixelShader = compile ps_4_0_level_9_1 WaterPS(); } }
80
Adding Refraction and the Fresnel Term
Now that we have added some ripples to our water, it’s time we use our refraction map to blend in the
color of the bottoms of the water channels. We will use the rippling effect of last section. Since we
already know the reflective and refractive color, the only remaining question is: for each pixel, how much
of each color should we use? The answer to this question can be deduced from the figure below. The
horizontal flat line represents our water. The vector pointing upward is the normal vector of the pixel. The
other vector, called the “eyevector”, is the vector going from the camera to the pixel. The length of green
on the normal vector indicates the amount of reflection the current pixel should have, and the length of
red represents the amount of refraction. If you think about it, this makes since. If we look straight down
in clear water, we will see mostly the bottom (if the water is sufficiently shallow). If we look out across
the water, we will see mostly reflection. Photographers use this effect to capture stunning photographs of
mountain lakes.
So how do we find the lengths of the green and red bars? We need to project the eyevector onto the
normal vector. Conveniently, this is exactly what a dot product does: when you dot product the eyevector
and the normal vector, you get the length of the red bar. The green bar then equals (1- length of red bar).
How cool is that?
We will achieve this effect almost entirely in shader code, so we will do that first. Since we will need the
camera position in the pixel shader, we need to add a global constant to hold this information when we
pass it into the shader code. We will also add two other global constants whose purpose will be explained
later: float3 xCamPos; float xDirtyWaterFactor; float4 xDullColor;
Then we will calculate the refractive color for our water. This is done in exactly the same as we did for
the reflective water: we compute the projective textures, add some ripple perturbation to them, and
sample the refraction map. To know the projective textures, we again need the 2D screen coordinates for
that pixel, as seen by the camera that created the refraction map. This camera was our normal camera, so
add variables to hold this information this in the vertex shader output struct: struct WVertexToPixel { float4 Position : POSITION; float4 ReflectionMapSamplingPos : TEXCOORD1;
81
float2 BumpMapSamplingPos : TEXCOORD2; float4 RefractionMapSamplingPos : TEXCOORD3; float4 Position3D : TEXCOORD4; };
And compute the refraction map sampling position in the output of the vertex shader:
Output.RefractionMapSamplingPos = mul(inPos, preWorldViewProjection); Output.Position3D = mul(inPos, xWorld);
For each pixel, we will also need its 3D position to calculate the eyevector, thus the last line to our vertex
shader. The vertices of the water have already been defined in absolute World space, but to keep our
shader code generally useful we multiply the water vertex positions by the World matrix.
Next, in our pixel shader, we first save the reflective color we previously obtained: float4 reflectiveColor = tex2D(ReflectionSampler, perturbatedTexCoords);
Now we do exactly the same as we did in the last section, but this time for the refraction map: float2 ProjectedRefrTexCoords; ProjectedRefrTexCoords.x = PSIn.RefractionMapSamplingPos.x/PSIn.RefractionMapSamplingPos.w/2.0f + 0.5f; ProjectedRefrTexCoords.y = -PSIn.RefractionMapSamplingPos.y/PSIn.RefractionMapSamplingPos.w/2.0f + 0.5f; float2 perturbatedRefrTexCoords = ProjectedRefrTexCoords + perturbation; float4 refractiveColor = tex2D(RefractionSampler, perturbatedRefrTexCoords);
Now we know both the reflective and the refractive color, it’s time to blend them together according to
the Fresnel term. To obtain the Fresnel term we first need to find the eyevector: float3 eyeVector = (float3) normalize(float4(xCamPos,0.0) - PSIn.Position3D);
Now use our bump map to compute the normal for each pixel of our water: float3 normalVector = (bumpColor.rbg-0.5f)*2.0f;
And compute the Fresnel term: float fresnelTerm = dot(eyeVector, normalVector);
And finally we can blend the refraction and reflection colors according to the Fresnel term, using linear
interpolation. Save this color at this point: float4 combinedColor = lerp(reflectiveColor, refractiveColor, fresnelTerm);
The last thing we want to do in the pixel shader is to blend in a little bit of dull water color, since real
water is rarely devoid of dissolved solids that give it a greenish-grayish-blue tinge. To do this, we will
use two values passed in from the Game1 code: a color and a linear interpolation factor that tell us how
much of this color to blend in.
82
Now blend it in: Output.Color = tex2D(ReflectionSampler, perturbatedTexCoords); Output.Color = lerp(combinedColor, xDullColor, xDirtyWaterFactor);
That’s it for the HLSL code. In our Game1 code, we only need to define two new water constants, and
pass in the camera position and water constants in the DrawWater method:
//Water Constants const float WATER_HEIGHT = 5.0f; public const float WAVE_LENGTH = 0.2f; public const float WAVE_HEIGHT = 0.3f; public Vector4 DULL_COLOR = new Vector4(0.3f, 0.4f, 0.5f, 1.0f); public const float WATER_DIRTINESS = 0.2f; // Increase to make the water "dirtier"
And in DrawWater: effect.Parameters["xCamPos"].SetValue(cameraPosition); effect.Parameters["xDirtyWaterFactor"].SetValue(WATER_DIRTINESS); effect.Parameters["xDullColor"].SetValue(DULL_COLOR);
Finally, uncomment this line (since we are now using the Refraction map).
effect.Parameters["xRefractionMap"].SetValue(refractionMap);
Run this code. You should get water that has both a reflective and refractive color, with realistic a dull
color blended in. Your water should now look something like the image below. Try some other dull
water colors and blending factors.
83
Here is our code so far.
Game1.cs:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; namespace Lab8_Mono { /// <summary> /// This is the main type for your game. /// </summary> public class Game1 : Game { #region Constants //Lighting Constants Vector3 LIGHT_DIRECTION = new Vector3(1.0f, -1.0f, 1.0f); //use (1.0f, -1.0f, 1.0f) to flip light public const float AMBIENT_LIGHT_LEVEL = 0.7f; // Camera control constants public const float ROTATION_SPEED = 0.3f; public const float MOVE_SPEED = 30.0f; // Terrain texture mapping constants public const float MAX_HT = 30.0f; public const float MIN_HT = 0.0f; public const float SAND_UPPER = 0.266f * MAX_HT; // 8 public const float GRASS_MID = 0.4f * MAX_HT; // 12 public const float GRASS_RANGE = 0.2f * MAX_HT; // 12 +/- 6 public const float ROCK_MID = 0.666f * MAX_HT; // 20 public const float ROCK_RANGE = 0.2f * MAX_HT; // 20 +/- 6 public const float SNOW_LOWER = 0.8f * MAX_HT; // 24 // Water Constants const float WATER_HEIGHT = 5.0f; public const float WAVE_LENGTH = 0.2f; public const float WAVE_HEIGHT = 0.3f; public Vector4 DULL_COLOR = new Vector4(0.3f, 0.4f, 0.5f, 1.0f); public const float WATER_DIRTINESS = 0.2f; // Increase to make the water "dirtier" #endregion #region Vertex Structs public struct VertexPositionColorNormal { public Vector3 Position; public Color Color; public Vector3 Normal; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration (
84
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Color, VertexElementUsage.Color, 0), new VertexElement(sizeof(float) * 3 + 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0) ); } #endregion #region Vertex Structs public struct VertexMultitextured { public Vector3 Position; public Vector3 Normal; public Vector4 TextureCoordinate; public Vector4 TexWeights; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0), new VertexElement(sizeof(float) * 6, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 0), new VertexElement(sizeof(float) * 10, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1) ); } #endregion #region Game1 Instance Vars GraphicsDeviceManager graphics; GraphicsDevice device; Effect effect; VertexMultitextured[] vertices; int[] indices; Matrix viewMatrix; Matrix projectionMatrix; VertexBuffer myVertexBuffer; IndexBuffer myIndexBuffer; VertexBuffer waterVertexBuffer; private int terrainWidth; private int terrainLength; private float[,] heightData; Matrix worldMatrix = Matrix.Identity; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity; Vector3 cameraPosition = new Vector3(130, 30, -50);
85
float leftrightRot = MathHelper.PiOver2; float updownRot = -MathHelper.Pi / 10.0f; MouseState originalMouseState; Texture2D grassTexture; Texture2D sandTexture; Texture2D rockTexture; Texture2D snowTexture; Texture2D cloudMap; Model skyDome; RenderTarget2D refractionRenderTarget; Texture2D refractionMap; RenderTarget2D reflectionRenderTarget; Texture2D reflectionMap; Matrix reflectionViewMatrix; Texture2D waterBumpMap; #endregion #region Class Game1 Constructor public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } #endregion #region Terrain Methods private void SetUpVertices() { vertices = new VertexMultitextured[terrainWidth * terrainLength]; for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainLength; y++) { vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / MAX_HT; vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / MAX_HT; vertices[x + y * terrainWidth].TexWeights.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MIN_HT) / SAND_UPPER, 0, 1); vertices[x + y * terrainWidth].TexWeights.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - GRASS_MID) / GRASS_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - ROCK_MID) / ROCK_RANGE, 0, 1);
86
vertices[x + y * terrainWidth].TexWeights.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MAX_HT) / SNOW_LOWER, 0, 1); float total = vertices[x + y * terrainWidth].TexWeights.X; total += vertices[x + y * terrainWidth].TexWeights.Y; total += vertices[x + y * terrainWidth].TexWeights.Z; total += vertices[x + y * terrainWidth].TexWeights.W; vertices[x + y * terrainWidth].TexWeights.X /= total; vertices[x + y * terrainWidth].TexWeights.Y /= total; vertices[x + y * terrainWidth].TexWeights.Z /= total; vertices[x + y * terrainWidth].TexWeights.W /= total; } } } private void SetUpIndices() { indices = new int[(terrainWidth - 1) * (terrainLength - 1) * 6]; int counter = 0; for (int y = 0; y < terrainLength - 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;
87
vertices[index3].Normal += normal; } for (int i = 0; i < vertices.Length; i++) vertices[i].Normal.Normalize(); }
private void CopyToBuffers() { myVertexBuffer = new VertexBuffer(device, VertexMultitextured.vertexDeclaration, vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices); myIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly); myIndexBuffer.SetData(indices); } private void LoadHeightData(Texture2D heightMap) { terrainWidth = heightMap.Width; terrainLength = heightMap.Height; float minimumHeight = float.MaxValue; float maximumHeight = float.MinValue; Color[] heightMapColors = new Color[terrainWidth * terrainLength]; heightMap.GetData(heightMapColors); heightData = new float[terrainWidth, terrainLength]; for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) { heightData[x, y] = heightMapColors[x + y * terrainWidth].R; if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y]; if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y]; } for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * MAX_HT; } private void DrawTerrain(Matrix currentViewMatrix) { LIGHT_DIRECTION.Normalize(); effect.Parameters["xLightDirection"].SetValue(LIGHT_DIRECTION); effect.Parameters["xAmbient"].SetValue(AMBIENT_LIGHT_LEVEL); effect.Parameters["xEnableLighting"].SetValue(true); effect.CurrentTechnique = effect.Techniques["MultiTextured"]; effect.Parameters["xTexture0"].SetValue(sandTexture); effect.Parameters["xTexture1"].SetValue(grassTexture); effect.Parameters["xTexture2"].SetValue(rockTexture);
88
effect.Parameters["xTexture3"].SetValue(snowTexture); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xWorld"].SetValue(worldMatrix); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.Indices = myIndexBuffer; device.SetVertexBuffer(myVertexBuffer); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, indices.Length / 3); } } #endregion #region Texture Loads private void LoadTextures() { grassTexture = Content.Load<Texture2D>("grass"); sandTexture = Content.Load<Texture2D>("sand"); rockTexture = Content.Load<Texture2D>("rock"); snowTexture = Content.Load<Texture2D>("snow"); cloudMap = Content.Load<Texture2D>("cloudMap"); waterBumpMap = Content.Load<Texture2D>("waterbump"); } #endregion #region Camera Methods private void SetUpCamera() { UpdateViewMatrix(); projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 0.3f, 1000.0f); } private void UpdateViewMatrix() { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation); viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector);
89
Vector3 reflCameraPosition = cameraPosition; reflCameraPosition.Y = -cameraPosition.Y + WATER_HEIGHT * 2; Vector3 reflTargetPos = cameraFinalTarget; reflTargetPos.Y = -cameraFinalTarget.Y + WATER_HEIGHT * 2; Vector3 cameraRight = Vector3.Transform(new Vector3(1, 0, 0), cameraRotation); Vector3 invUpVector = Vector3.Cross(cameraRight, reflTargetPos - reflCameraPosition); reflectionViewMatrix = Matrix.CreateLookAt(reflCameraPosition, reflTargetPos, invUpVector); } private void ProcessInput(float amount) { MouseState currentMouseState = Mouse.GetState(); if (currentMouseState != originalMouseState) { float xDifference = currentMouseState.X - originalMouseState.X; float yDifference = currentMouseState.Y - originalMouseState.Y; leftrightRot -= ROTATION_SPEED * xDifference * amount; updownRot -= ROTATION_SPEED * yDifference * amount; Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); UpdateViewMatrix(); } Vector3 moveVector = new Vector3(0, 0, 0); KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W)) moveVector += new Vector3(0, 0, -1); if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S)) moveVector += new Vector3(0, 0, 1); if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D)) moveVector += new Vector3(1, 0, 0); if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A)) moveVector += new Vector3(-1, 0, 0); if (keyState.IsKeyDown(Keys.Q)) moveVector += new Vector3(0, 1, 0); if (keyState.IsKeyDown(Keys.Z)) moveVector += new Vector3(0, -1, 0); AddToCameraPosition(moveVector * amount); } private void AddToCameraPosition(Vector3 vectorToAdd) { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation); cameraPosition += MOVE_SPEED * rotatedVector; UpdateViewMatrix(); } #endregion #region Skydome Methods
90
private void DrawSkyDome(Matrix currentViewMatrix) { GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead; Matrix[] modelTransforms = new Matrix[skyDome.Bones.Count]; skyDome.CopyAbsoluteBoneTransformsTo(modelTransforms); Matrix wMatrix = Matrix.CreateTranslation(0, -0.3f, 0) * Matrix.CreateScale(500) * Matrix.CreateTranslation(cameraPosition); foreach (ModelMesh mesh in skyDome.Meshes) { foreach (Effect currentEffect in mesh.Effects) { Matrix mworldMatrix = modelTransforms[mesh.ParentBone.Index] * wMatrix; currentEffect.CurrentTechnique = currentEffect.Techniques["SimpleTextured"]; currentEffect.Parameters["xAmbient"].SetValue(1.0f); currentEffect.Parameters["xWorld"].SetValue(mworldMatrix); currentEffect.Parameters["xView"].SetValue(currentViewMatrix); currentEffect.Parameters["xProjection"].SetValue(projectionMatrix); currentEffect.Parameters["xTexture"].SetValue(cloudMap); currentEffect.Parameters["xEnableLighting"].SetValue(false); } mesh.Draw(); } GraphicsDevice.DepthStencilState = DepthStencilState.Default; } #endregion #region Water Methods private Plane CreatePlane(float height, Vector3 planeNormalDirection, Matrix currentViewMatrix, bool clipSide) { planeNormalDirection.Normalize(); Vector4 planeCoeffs = new Vector4(planeNormalDirection, height); if (clipSide) planeCoeffs *= -1; Matrix worldViewProjection = currentViewMatrix * projectionMatrix; Matrix inverseWorldViewProjection = Matrix.Invert(worldViewProjection); inverseWorldViewProjection = Matrix.Transpose(inverseWorldViewProjection); planeCoeffs = Vector4.Transform(planeCoeffs, inverseWorldViewProjection); Plane finalPlane = new Plane(planeCoeffs); return finalPlane; } private void DrawRefractionMap() { Plane refractionPlane = CreatePlane(WATER_HEIGHT + 1.5f, new Vector3(0, 1, 0), viewMatrix, false); effect.Parameters["ClipPlane0"].SetValue(new Vector4(refractionPlane.Normal, refractionPlane.D)); // Enable clipping for the purpose of creating a refraction map
91
effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(refractionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawTerrain(viewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); refractionMap = refractionRenderTarget; }
private void DrawReflectionMap() { Plane reflectionPlane = CreatePlane(WATER_HEIGHT - 0.5f, new Vector3(0, -1, 0), reflectionViewMatrix, true); effect.Parameters["ClipPlane0"].SetValue(new Vector4(-reflectionPlane.Normal, -reflectionPlane.D)); // Enable clipping for the purpose of creating a reflection map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(reflectionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawSkyDome(reflectionViewMatrix); DrawTerrain(reflectionViewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); reflectionMap = reflectionRenderTarget; } private void SetUpWaterVertices() { VertexPositionTexture[] waterVertices = new VertexPositionTexture[6]; waterVertices[0] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[2] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertices[1] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, -terrainLength), new Vector2(0, 0)); waterVertices[3] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[5] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, 0), new Vector2(1, 1)); waterVertices[4] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, waterVertices.Length, BufferUsage.WriteOnly);
92
waterVertexBuffer.SetData(waterVertices); } private void DrawWater(float time) { effect.CurrentTechnique = effect.Techniques["Water"]; Matrix worldMatrix = Matrix.Identity; effect.Parameters["xWorld"].SetValue(worldMatrix); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xReflectionView"].SetValue(reflectionViewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xRefractionMap"].SetValue(refractionMap); effect.Parameters["xReflectionMap"].SetValue(reflectionMap); effect.Parameters["xWaterBumpMap"].SetValue(waterBumpMap); effect.Parameters["xWaveLength"].SetValue(WAVE_LENGTH); effect.Parameters["xWaveHeight"].SetValue(WAVE_HEIGHT); effect.Parameters["xCamPos"].SetValue(cameraPosition); effect.Parameters["xDirtyWaterFactor"].SetValue(WATER_DIRTINESS); effect.Parameters["xDullColor"].SetValue(DULL_COLOR); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.SetVertexBuffer(waterVertexBuffer); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2); } } #endregion #region Initialize /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // JKB Note: This ordering and repetition of graphics profile commands addresses a // current bug in MonoGame 3.7. If we don't do it this way the window defaults to // a (small) fixed size. Also, MonoGame says it defaults to Reach, but it doesn't, so // we have to set HiDef explicitily here in order to use 32-bit index buffers. graphics.GraphicsProfile = GraphicsProfile.HiDef; graphics.IsFullScreen = false; graphics.ApplyChanges(); graphics.PreferredBackBufferWidth = 800; graphics.PreferredBackBufferHeight = 800; graphics.ApplyChanges(); Window.Title = "Lab8_Mono - Terrain Tutorial II";
93
base.Initialize(); } #endregion #region LoadContent /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { device = graphics.GraphicsDevice; effect = Content.Load<Effect>("effects"); skyDome = Content.Load<Model>("dome"); skyDome.Meshes[0].MeshParts[0].Effect = effect.Clone(); Texture2D heightMap = Content.Load<Texture2D>("heightmap2"); LoadHeightData(heightMap); Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); originalMouseState = Mouse.GetState(); SetUpCamera(); SetUpVertices(); SetUpIndices(); CalculateNormals(); CopyToBuffers(); LoadTextures(); PresentationParameters pp = device.PresentationParameters; refractionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); reflectionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); SetUpWaterVertices(); } #endregion #region UnloadContent /// <summary> /// UnloadContent will be called once per game and is the place to unload /// game-specific content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here
94
} #endregion #region Update /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyState = Keyboard.GetState(); //Rotation if (keyState.IsKeyDown(Keys.PageUp)) { worldRotation = Matrix.CreateRotationY(0.01f); } else if (keyState.IsKeyDown(Keys.PageDown)) { worldRotation = Matrix.CreateRotationY(-0.01f); } else { worldRotation = Matrix.CreateRotationY(0); } float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f; ProcessInput(timeDifference); worldMatrix *= worldTranslation * worldRotation; base.Update(gameTime); } #endregion #region Draw /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 100.0f; DrawRefractionMap(); DrawReflectionMap(); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);
95
DrawSkyDome(viewMatrix); DrawTerrain(viewMatrix); DrawWater(time); base.Draw(gameTime); } #endregion } }
Effects.fx:
//---------------------------------------------------- //-- This effect file derived from: -- //-- www.riemers.net -- //-- Basic shaders -- //-- -- //-- Modified for MonoGame by John K. Bennett -- //-- -- //-- Use/modify as you like -- //-- -- //---------------------------------------------------- struct VertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float LightingFactor: TEXCOORD0; float2 TextureCoords: TEXCOORD1; }; struct PixelToFrame { float4 Color : COLOR0; }; //------- Constants -------- float4x4 xView; float4x4 xProjection; float4x4 xWorld; float3 xLightDirection; float xAmbient; bool xEnableLighting; bool xShowNormals; bool Clipping; float4 ClipPlane0; float4x4 xReflectionView; float xWaveLength; float xWaveHeight; float3 xCamPos; float xDirtyWaterFactor; float4 xDullColor; //------- Texture Samplers --------
96
Texture xTexture; sampler TextureSampler = sampler_state { texture = <xTexture>; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;}; Texture xTexture0; sampler TextureSampler0 = sampler_state { texture = <xTexture0>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture1; sampler TextureSampler1 = sampler_state { texture = <xTexture1>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture2; sampler TextureSampler2 = sampler_state { texture = <xTexture2>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xTexture3; sampler TextureSampler3 = sampler_state { texture = <xTexture3>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xRefractionMap; sampler RefractionSampler = sampler_state { texture = <xRefractionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xReflectionMap; sampler ReflectionSampler = sampler_state { texture = <xReflectionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xWaterBumpMap; sampler WaterBumpMapSampler = sampler_state { texture = <xWaterBumpMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; //------- Technique: Pretransformed -------- VertexToPixel PretransformedVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; Output.Position = inPos; Output.Color = inColor; return Output; } PixelToFrame PretransformedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique Pretransformed { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 PretransformedVS();
97
PixelShader = compile ps_4_0_level_9_1 PretransformedPS(); } } //------- Technique: Colored -------- VertexToPixel ColoredVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame ColoredPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Colored { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredVS(); PixelShader = compile ps_4_0_level_9_1 ColoredPS(); } } //------- Technique: ColoredNoShading -------- // No lighting or shading, so no normal info passed in VertexToPixel ColoredNoShadingVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; return Output; }
98
PixelToFrame ColoredNoShadingPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique ColoredNoShading { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredNoShadingVS(); PixelShader = compile ps_4_0_level_9_1 ColoredNoShadingPS(); } } //------- Technique: Textured -------- VertexToPixel TexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame TexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Textured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 TexturedVS(); PixelShader = compile ps_4_0_level_9_1 TexturedPS(); } } //------- Technique: SimpleTextured --------
99
VertexToPixel SimpleTexturedVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; return Output; } PixelToFrame SimpleTexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; }
technique SimpleTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 SimpleTexturedVS(); PixelShader = compile ps_4_0_level_9_1 SimpleTexturedPS(); } } //------- Technique: Multitextured -------- struct MTVertexToPixel { float4 Position : POSITION0; float4 Color : COLOR0; float3 Normal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 LightDirection : TEXCOORD2; float4 TextureWeights : TEXCOORD3; float Depth : TEXCOORD4; float4 clipDistances : TEXCOORD5; }; struct MTPixelToFrame { float4 Color : COLOR0; }; MTVertexToPixel MultiTexturedVS(float4 inPos : POSITION, float3 inNormal : NORMAL, float2 inTexCoords : TEXCOORD0, float4 inTexWeights : TEXCOORD1) { MTVertexToPixel Output = (MTVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Normal = (float3) mul(normalize(float4(inNormal, 0.0)), xWorld); Output.TextureCoords = inTexCoords;
100
Output.LightDirection.xyz = -xLightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; Output.Depth = Output.Position.z / Output.Position.w; Output.clipDistances = dot(inPos, ClipPlane0); return Output; } MTPixelToFrame MultiTexturedPS(MTVertexToPixel PSIn) { MTPixelToFrame Output = (MTPixelToFrame)0; if (Clipping) clip(PSIn.clipDistances); float lightingFactor = 1; float blendDistance = 0.99f; float blendWidth = 0.005f; float blendFactor = clamp((PSIn.Depth - blendDistance) / blendWidth, 0, 1); if (xEnableLighting) lightingFactor = saturate(saturate(dot(float4(PSIn.Normal, 0.0), PSIn.LightDirection)) + xAmbient); float4 farColor; farColor = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; farColor += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; farColor += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; farColor += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w; float4 nearColor; float2 nearTextureCoords = PSIn.TextureCoords * 3; nearColor = tex2D(TextureSampler0, nearTextureCoords)*PSIn.TextureWeights.x; nearColor += tex2D(TextureSampler1, nearTextureCoords)*PSIn.TextureWeights.y; nearColor += tex2D(TextureSampler2, nearTextureCoords)*PSIn.TextureWeights.z; nearColor += tex2D(TextureSampler3, nearTextureCoords)*PSIn.TextureWeights.w; Output.Color = lerp(nearColor, farColor, blendFactor); Output.Color *= lightingFactor; return Output; } technique MultiTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 MultiTexturedVS(); PixelShader = compile ps_4_0_level_9_1 MultiTexturedPS(); } } //------- Technique: Water -------- struct WVertexToPixel { float4 Position : POSITION;
101
float4 ReflectionMapSamplingPos : TEXCOORD1; float2 BumpMapSamplingPos : TEXCOORD2; float4 RefractionMapSamplingPos : TEXCOORD3; float4 Position3D : TEXCOORD4; }; struct WPixelToFrame { float4 Color : COLOR0; }; WVertexToPixel WaterVS(float4 inPos : POSITION, float2 inTex : TEXCOORD) { WVertexToPixel Output = (WVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); float4x4 preReflectionViewProjection = mul(xReflectionView, xProjection); float4x4 preWorldReflectionViewProjection = mul(xWorld, preReflectionViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.ReflectionMapSamplingPos = mul(inPos, preWorldReflectionViewProjection); Output.BumpMapSamplingPos = inTex / xWaveLength; Output.RefractionMapSamplingPos = mul(inPos, preWorldViewProjection); Output.Position3D = mul(inPos, xWorld); return Output; } WPixelToFrame WaterPS(WVertexToPixel PSIn) { WPixelToFrame Output = (WPixelToFrame)0; float4 bumpColor = tex2D(WaterBumpMapSampler, PSIn.BumpMapSamplingPos); float2 perturbation = xWaveHeight * (bumpColor.rg - 0.5f)*2.0f; float2 ProjectedTexCoords; ProjectedTexCoords.x = PSIn.ReflectionMapSamplingPos.x / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; ProjectedTexCoords.y = -PSIn.ReflectionMapSamplingPos.y / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; float2 perturbatedTexCoords = ProjectedTexCoords + perturbation; float4 reflectiveColor = tex2D(ReflectionSampler, perturbatedTexCoords); float2 ProjectedRefrTexCoords; ProjectedRefrTexCoords.x = PSIn.RefractionMapSamplingPos.x / PSIn.RefractionMapSamplingPos.w / 2.0f + 0.5f; ProjectedRefrTexCoords.y = -PSIn.RefractionMapSamplingPos.y / PSIn.RefractionMapSamplingPos.w / 2.0f + 0.5f; float2 perturbatedRefrTexCoords = ProjectedRefrTexCoords + perturbation; float4 refractiveColor = tex2D(RefractionSampler, perturbatedRefrTexCoords); float3 eyeVector = (float3) normalize(float4(xCamPos,0.0) - PSIn.Position3D);
102
float3 normalVector = (bumpColor.rbg - 0.5f)*2.0f; float fresnelTerm = dot(eyeVector, normalVector); float4 combinedColor = lerp(reflectiveColor, refractiveColor, fresnelTerm); Output.Color = lerp(combinedColor, xDullColor, xDirtyWaterFactor); return Output; } technique Water { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 WaterVS(); PixelShader = compile ps_4_0_level_9_1 WaterPS(); } }
Moving Water
Our water is looking pretty good, but a critical element of realism is missing: movement. In this section,
we will learn how to create the illusion of moving water. To do that, we need to remember the origin of
the ripples in our water. The water bump map indicates for each pixel exactly how much to perturbate the
sampling coordinates of the reflection and refraction map. Therefore, we can make our water move in
one direction by simply translating the texture coordinates of the bump map. Recall that these texture
coordinates were attached to the six vertices of the two large triangles that make up our water. So one way
to change the vertices would be in the Game1 code, but a MUCH better place would be to change them in
the vertex shader. In general, it is always better to offload graphical computations to the GPU. In this
case, we will simply pass a time (xTime) variable to our shader, which the vertex shader will use to update
the bump map texture coordinates. We will also pass in variables that govern the wind direction and
force. In effects.fx add the following global constants:
float xTime;
float3 xWindDirection; float xWindForce;
The xWindForce variable will control how fast the ripples scroll through our water; the xWindDirection
variable will control the direction of our ripples. Add the following code to the water vertex shader:
Output.BumpMapSamplingPos = inTex/xWaveLength; Output.RefractionMapSamplingPos = mul(inPos, preWorldViewProjection); Output.Position3D = mul(inPos, xWorld); float2 moveVector = float2(0, xTime*xWindForce); Output.BumpMapSamplingPos = (inTex + moveVector)/xWaveLength;
We will scroll our bump map along the Y direction; since the waves in the image are horizontal, we need
to scroll them vertically. The scrolling speed depends on the xWindForce value.
103
We are done with the shader code. Now we need to modify Game1.cs. First add two new water
constants:
public const float WIND_FORCE = 0.0005f; // It doesn't take much public Vector3 WIND_DIRECTION = new Vector3(0, 0, 1);
Vectors cannot be constants in C#, so we just make them public. Now edit our DrawWater method:
effect.Parameters["xTime"].SetValue(time); effect.Parameters["xWindForce"].SetValue(WIND_FORCE); effect.Parameters["xWindDirection"].SetValue(WIND_DIRECTION);
Run your code at this point. You should see realistic moving water. Woo hoo!
The discerning reader will note that we have not yet used the WIND_DIRECTION vector. We set it, and
pass it to the shader, but the shader does not use this information to control the direction of the waves.
Let’s fix that. However, there is one factor we still cannot control: the direction of the wind, and thus the
direction of the waves. To understand how to do this, consider the images below.
The squares indicate our original texture coordinates; think of them as representing our complete water
plane. The red arrows indicate the direction we want the water to move (defined by the wind direction).
The green arrows represent the direction perpendicular to the red arrows. Look more closely at the right
image (the one with off-axis wind direction), and in particular, the small rectangle outlined by a dotted
black line in the upper left. For each pixel in this rectangle, we need to find a new X and Y texture
coordinate. The constraints on these coordinates are as follows:
1. All pixels on the red arrow must have the same X texture coordinate.
2. All pixels on the green arrow must have the same Y texture coordinate.
As stated, these constraints apply only to the pixels that are located exactly on the arrows. However we
can generalize these constraints, as follows:
1. For any line parallel to the red arrow, all pixels on that line must have the same X texture
coordinate.
104
2. For any line parallel to the green arrow, all pixels on that line must have the same Y texture
coordinate.
For each pixel in the small rectangle, these constraints apply to the X and Y texture coordinates. Now we
need to determine the texture coordinates for all pixels. Here is how: In the right image, take a look at the
upper-left pixel of the black square. The Y texture coordinate is found by taking the corresponding
portion of the red arrow, as displayed by the thicker red line. The length of this thick red line will be the Y
texture coordinate for this pixel. In fact, all pixels on the long dotted black line have the same thick red
line, and thus the same Y texture coordinate, thus fulfilling the second constraint. In the same manner, the
X texture coordinate is found by projecting the pixel on the green arrow, and finding the length of the
thicker green line. All pixels on the short dotted black line will have the same X texture coordinate, which
is represented by thick green line.
We find the length of the thick red and green lines by taking the dot product between the pixel vector (the
thin black line), and the red and green vectors, respectively.
In order to obtain the X and Y texture coordinates, we should first find the red and green arrows. The red
one is easy, as it is the normalized version of the xWindDirection vector. The green arrow is perpendicular
to the red arrow and the Up direction, so it can be found by taking their cross product. Then, we find the
X and Y texture coordinates by taking the dot product of the pixel vector and both arrows. We now have
the fixed texture coordinates, rotated so the waves are perpendicular to the xWindDirection, exactly as
they should be. All we need to do now is to add the resulting change in the Y texture coordinate to make
the texture scroll correctly, and pass the result to the pixel shader. These changes are reflected in the
following vertex shader code:
float2 moveVector = float2(0, xTime*xWindForce); Output.BumpMapSamplingPos = (inTex + moveVector)/xWaveLength; float3 windDir = normalize(xWindDirection); float3 perpDir = cross(xWindDirection, float3(0,1,0)); float ydot = dot(inTex, xWindDirection.xz); float xdot = dot(inTex, perpDir.xz); float2 moveVector = float2(xdot, ydot); moveVector.y += xTime*xWindForce; Output.BumpMapSamplingPos = moveVector/xWaveLength;
Run this code. You should now be able to control the direction of your waves. It just keeps getting
better! With our water moving, there is one last effect we need to add: realistic lighting. That is the
subject of the next section.
Specular Highlights
Specular highlights are small spots of high reflectivity on the water, near the region where the sun (or
another light source) is reflected in the water. This idea is represented in the image below:
105
If the water surface were a mirror, then the camera would see the light source reflected directly if the
camera is located along the reflection vector created by the light source. So, to compute specular
highlights, we mirror the direction of the light (the left arrow in the image) over the normal in the water
pixel, and compare this vector to the eye vector (the right arrow in the image). If both are almost the
same, the pixel is situated in a specular highlighted area.
The actual code to implement this idea in the water pixel shader is simple, thanks to the use of the HLSL
reflect intrinsic function (reflect returns a reflection vector given an incident ray and a surface normal),
and because we have already determined the normalVector and calculated and eyeVector in our pixel
shader. First we calculate the direction of the light, reflected over the normal vector:
float3 reflectionVector = -reflect(xLightDirection, normalVector);
Next, we need to find out how nearly this reflected vector is the same as the eyeVector. This can be done
easily by taking their dot product: the dot product will be 1 of they are 100% the same, and will be 0 if
they are perpendicular:
float specular = abs(dot(normalize(reflectionVector), normalize(eyeVector)));
Since we are interested only in those vectors that are very close to the same, we only want to consider dot
values that are higher than 0.95. By taking this number to a very high power, only those numbers very
close to 1 will remain (this is because 1256 is still 1; but 0.9256 is a very small number).
specular = pow(specular, 256); Output.Color.rgb += specular;
The last line adds this amount as white to our final color of the pixel. Run this code. You should see
beautiful water, similar to the image below.
106
That’s it for water. We will now move on to make the sky look more realistic, which will indirectly make
our water look even better. Note that if you watch the water move for a while, you might be able to
discern the bump map pattern. This can be solved by scrolling the same bumpmap multiple times over
itself, each time with a different scaling, scroll speed, and possibly with the X and Y texture coordinates
swapped. Try this if you feel brave.
Here is our code at this point:
Game1.cs
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; namespace Lab8_Mono { /// <summary> /// This is the main type for your game. /// </summary> public class Game1 : Game { #region Constants //Lighting Constants
107
Vector3 LIGHT_DIRECTION = new Vector3(1.0f, -1.0f, 1.0f); //use (1.0f, -1.0f, 1.0f) to flip light public const float AMBIENT_LIGHT_LEVEL = 0.7f; // Camera control constants public const float ROTATION_SPEED = 0.3f; public const float MOVE_SPEED = 30.0f; // Terrain texture mapping constants public const float MAX_HT = 30.0f; public const float MIN_HT = 0.0f; public const float SAND_UPPER = 0.266f * MAX_HT; // 8 public const float GRASS_MID = 0.4f * MAX_HT; // 12 public const float GRASS_RANGE = 0.2f * MAX_HT; // 12 +/- 6 public const float ROCK_MID = 0.666f * MAX_HT; // 20 public const float ROCK_RANGE = 0.2f * MAX_HT; // 20 +/- 6 public const float SNOW_LOWER = 0.8f * MAX_HT; // 24 // Water Constants const float WATER_HEIGHT = 5.0f; public const float WAVE_LENGTH = 0.2f; public const float WAVE_HEIGHT = 0.3f; public Vector4 DULL_COLOR = new Vector4(0.3f, 0.4f, 0.5f, 1.0f); public const float WATER_DIRTINESS = 0.2f; // Increase to make the water "dirtier" public const float WIND_FORCE = 0.0005f; // It doesn't take much public Vector3 WIND_DIRECTION = new Vector3(0, 0, 1); #endregion #region Vertex Structs public struct VertexPositionColorNormal { public Vector3 Position; public Color Color; public Vector3 Normal; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Color, VertexElementUsage.Color, 0), new VertexElement(sizeof(float) * 3 + 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0) ); } #endregion #region Vertex Structs public struct VertexMultitextured { public Vector3 Position; public Vector3 Normal; public Vector4 TextureCoordinate;
108
public Vector4 TexWeights; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0), new VertexElement(sizeof(float) * 6, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 0), new VertexElement(sizeof(float) * 10, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1) ); } #endregion #region Game1 Instance Vars GraphicsDeviceManager graphics; GraphicsDevice device; Effect effect; VertexMultitextured[] vertices; int[] indices; Matrix viewMatrix; Matrix projectionMatrix; VertexBuffer myVertexBuffer; IndexBuffer myIndexBuffer; VertexBuffer waterVertexBuffer; private int terrainWidth; private int terrainLength; private float[,] heightData; Matrix worldMatrix = Matrix.Identity; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity; Vector3 cameraPosition = new Vector3(130, 30, -50); float leftrightRot = MathHelper.PiOver2; float updownRot = -MathHelper.Pi / 10.0f; MouseState originalMouseState; Texture2D grassTexture; Texture2D sandTexture; Texture2D rockTexture; Texture2D snowTexture; Texture2D cloudMap; Model skyDome; RenderTarget2D refractionRenderTarget; Texture2D refractionMap; RenderTarget2D reflectionRenderTarget;
109
Texture2D reflectionMap; Matrix reflectionViewMatrix; Texture2D waterBumpMap; #endregion #region Class Game1 Constructor public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } #endregion #region Terrain Methods private void SetUpVertices() { vertices = new VertexMultitextured[terrainWidth * terrainLength]; for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainLength; y++) { vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / MAX_HT; vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / MAX_HT; vertices[x + y * terrainWidth].TexWeights.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MIN_HT) / SAND_UPPER, 0, 1); vertices[x + y * terrainWidth].TexWeights.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - GRASS_MID) / GRASS_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - ROCK_MID) / ROCK_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MAX_HT) / SNOW_LOWER, 0, 1); float total = vertices[x + y * terrainWidth].TexWeights.X; total += vertices[x + y * terrainWidth].TexWeights.Y; total += vertices[x + y * terrainWidth].TexWeights.Z; total += vertices[x + y * terrainWidth].TexWeights.W; vertices[x + y * terrainWidth].TexWeights.X /= total; vertices[x + y * terrainWidth].TexWeights.Y /= total; vertices[x + y * terrainWidth].TexWeights.Z /= total; vertices[x + y * terrainWidth].TexWeights.W /= total; } } } private void SetUpIndices()
110
{ indices = new int[(terrainWidth - 1) * (terrainLength - 1) * 6]; int counter = 0; for (int y = 0; y < terrainLength - 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 CopyToBuffers() { myVertexBuffer = new VertexBuffer(device, VertexMultitextured.vertexDeclaration, vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices); myIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly); myIndexBuffer.SetData(indices);
111
}
private void LoadHeightData(Texture2D heightMap) { terrainWidth = heightMap.Width; terrainLength = heightMap.Height; float minimumHeight = float.MaxValue; float maximumHeight = float.MinValue; Color[] heightMapColors = new Color[terrainWidth * terrainLength]; heightMap.GetData(heightMapColors); heightData = new float[terrainWidth, terrainLength]; for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) { heightData[x, y] = heightMapColors[x + y * terrainWidth].R; if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y]; if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y]; } for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * MAX_HT; } private void DrawTerrain(Matrix currentViewMatrix) { LIGHT_DIRECTION.Normalize(); effect.Parameters["xLightDirection"].SetValue(LIGHT_DIRECTION); effect.Parameters["xAmbient"].SetValue(AMBIENT_LIGHT_LEVEL); effect.Parameters["xEnableLighting"].SetValue(true); effect.CurrentTechnique = effect.Techniques["MultiTextured"]; effect.Parameters["xTexture0"].SetValue(sandTexture); effect.Parameters["xTexture1"].SetValue(grassTexture); effect.Parameters["xTexture2"].SetValue(rockTexture); effect.Parameters["xTexture3"].SetValue(snowTexture); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xWorld"].SetValue(worldMatrix); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.Indices = myIndexBuffer; device.SetVertexBuffer(myVertexBuffer); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, indices.Length / 3); } }
112
#endregion #region Texture Loads private void LoadTextures() { grassTexture = Content.Load<Texture2D>("grass"); sandTexture = Content.Load<Texture2D>("sand"); rockTexture = Content.Load<Texture2D>("rock"); snowTexture = Content.Load<Texture2D>("snow"); cloudMap = Content.Load<Texture2D>("cloudMap"); waterBumpMap = Content.Load<Texture2D>("waterbump"); } #endregion #region Camera Methods private void SetUpCamera() { UpdateViewMatrix(); projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 0.3f, 1000.0f); } private void UpdateViewMatrix() { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation); viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector); Vector3 reflCameraPosition = cameraPosition; reflCameraPosition.Y = -cameraPosition.Y + WATER_HEIGHT * 2; Vector3 reflTargetPos = cameraFinalTarget; reflTargetPos.Y = -cameraFinalTarget.Y + WATER_HEIGHT * 2; Vector3 cameraRight = Vector3.Transform(new Vector3(1, 0, 0), cameraRotation); Vector3 invUpVector = Vector3.Cross(cameraRight, reflTargetPos - reflCameraPosition); reflectionViewMatrix = Matrix.CreateLookAt(reflCameraPosition, reflTargetPos, invUpVector); }
113
private void ProcessInput(float amount) { MouseState currentMouseState = Mouse.GetState(); if (currentMouseState != originalMouseState) { float xDifference = currentMouseState.X - originalMouseState.X; float yDifference = currentMouseState.Y - originalMouseState.Y; leftrightRot -= ROTATION_SPEED * xDifference * amount; updownRot -= ROTATION_SPEED * yDifference * amount; Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); UpdateViewMatrix(); } Vector3 moveVector = new Vector3(0, 0, 0); KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W)) moveVector += new Vector3(0, 0, -1); if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S)) moveVector += new Vector3(0, 0, 1); if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D)) moveVector += new Vector3(1, 0, 0); if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A)) moveVector += new Vector3(-1, 0, 0); if (keyState.IsKeyDown(Keys.Q)) moveVector += new Vector3(0, 1, 0); if (keyState.IsKeyDown(Keys.Z)) moveVector += new Vector3(0, -1, 0); AddToCameraPosition(moveVector * amount); } private void AddToCameraPosition(Vector3 vectorToAdd) { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation); cameraPosition += MOVE_SPEED * rotatedVector; UpdateViewMatrix(); } #endregion #region Skydome Methods private void DrawSkyDome(Matrix currentViewMatrix) { GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead; Matrix[] modelTransforms = new Matrix[skyDome.Bones.Count]; skyDome.CopyAbsoluteBoneTransformsTo(modelTransforms); Matrix wMatrix = Matrix.CreateTranslation(0, -0.3f, 0) * Matrix.CreateScale(500) * Matrix.CreateTranslation(cameraPosition); foreach (ModelMesh mesh in skyDome.Meshes) { foreach (Effect currentEffect in mesh.Effects) { Matrix mworldMatrix = modelTransforms[mesh.ParentBone.Index] * wMatrix;
114
currentEffect.CurrentTechnique = currentEffect.Techniques["SimpleTextured"]; currentEffect.Parameters["xAmbient"].SetValue(1.0f); currentEffect.Parameters["xWorld"].SetValue(mworldMatrix); currentEffect.Parameters["xView"].SetValue(currentViewMatrix); currentEffect.Parameters["xProjection"].SetValue(projectionMatrix); currentEffect.Parameters["xTexture"].SetValue(cloudMap); currentEffect.Parameters["xEnableLighting"].SetValue(false); } mesh.Draw(); } GraphicsDevice.DepthStencilState = DepthStencilState.Default; } #endregion #region Water Methods private Plane CreatePlane(float height, Vector3 planeNormalDirection, Matrix currentViewMatrix, bool clipSide) { planeNormalDirection.Normalize(); Vector4 planeCoeffs = new Vector4(planeNormalDirection, height); if (clipSide) planeCoeffs *= -1; Matrix worldViewProjection = currentViewMatrix * projectionMatrix; Matrix inverseWorldViewProjection = Matrix.Invert(worldViewProjection); inverseWorldViewProjection = Matrix.Transpose(inverseWorldViewProjection); planeCoeffs = Vector4.Transform(planeCoeffs, inverseWorldViewProjection); Plane finalPlane = new Plane(planeCoeffs); return finalPlane; } private void DrawRefractionMap() { Plane refractionPlane = CreatePlane(WATER_HEIGHT + 1.5f, new Vector3(0, 1, 0), viewMatrix, false); effect.Parameters["ClipPlane0"].SetValue(new Vector4(refractionPlane.Normal, refractionPlane.D)); // Enable clipping for the purpose of creating a refraction map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(refractionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawTerrain(viewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); refractionMap = refractionRenderTarget;
115
}
private void DrawReflectionMap() { Plane reflectionPlane = CreatePlane(WATER_HEIGHT - 0.5f, new Vector3(0, -1, 0), reflectionViewMatrix, true); effect.Parameters["ClipPlane0"].SetValue(new Vector4(-reflectionPlane.Normal, -reflectionPlane.D)); // Enable clipping for the purpose of creating a reflection map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(reflectionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawSkyDome(reflectionViewMatrix); DrawTerrain(reflectionViewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); reflectionMap = reflectionRenderTarget; } private void SetUpWaterVertices() { VertexPositionTexture[] waterVertices = new VertexPositionTexture[6]; waterVertices[0] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[2] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertices[1] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, -terrainLength), new Vector2(0, 0)); waterVertices[3] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[5] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, 0), new Vector2(1, 1)); waterVertices[4] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, waterVertices.Length, BufferUsage.WriteOnly); waterVertexBuffer.SetData(waterVertices); } private void DrawWater(float time) { effect.CurrentTechnique = effect.Techniques["Water"]; Matrix worldMatrix = Matrix.Identity; effect.Parameters["xWorld"].SetValue(worldMatrix); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xReflectionView"].SetValue(reflectionViewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xRefractionMap"].SetValue(refractionMap);
116
effect.Parameters["xReflectionMap"].SetValue(reflectionMap); effect.Parameters["xWaterBumpMap"].SetValue(waterBumpMap); effect.Parameters["xWaveLength"].SetValue(WAVE_LENGTH); effect.Parameters["xWaveHeight"].SetValue(WAVE_HEIGHT); effect.Parameters["xCamPos"].SetValue(cameraPosition); effect.Parameters["xDirtyWaterFactor"].SetValue(WATER_DIRTINESS); effect.Parameters["xDullColor"].SetValue(DULL_COLOR); effect.Parameters["xTime"].SetValue(time); effect.Parameters["xWindForce"].SetValue(WIND_FORCE); effect.Parameters["xWindDirection"].SetValue(WIND_DIRECTION); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.SetVertexBuffer(waterVertexBuffer); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2); } } #endregion #region Initialize /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // JKB Note: This ordering and repetition of graphics profile commands addresses a // current bug in MonoGame 3.7. If we don't do it this way the window defaults to // a (small) fixed size. Also, MonoGame says it defaults to Reach, but it doesn't, so // we have to set HiDef explicitily here in order to use 32-bit index buffers. graphics.GraphicsProfile = GraphicsProfile.HiDef; graphics.IsFullScreen = false; graphics.ApplyChanges(); graphics.PreferredBackBufferWidth = 800; graphics.PreferredBackBufferHeight = 800; graphics.ApplyChanges(); Window.Title = "Lab8_Mono - Terrain Tutorial II"; base.Initialize(); } #endregion #region LoadContent /// <summary> /// LoadContent will be called once per game and is the place to load
117
/// all of your content. /// </summary> protected override void LoadContent() { device = graphics.GraphicsDevice; effect = Content.Load<Effect>("effects"); skyDome = Content.Load<Model>("dome"); skyDome.Meshes[0].MeshParts[0].Effect = effect.Clone(); Texture2D heightMap = Content.Load<Texture2D>("heightmap2"); LoadHeightData(heightMap); Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); originalMouseState = Mouse.GetState(); SetUpCamera(); SetUpVertices(); SetUpIndices(); CalculateNormals(); CopyToBuffers(); LoadTextures(); PresentationParameters pp = device.PresentationParameters; refractionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); reflectionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); SetUpWaterVertices(); } #endregion #region UnloadContent /// <summary> /// UnloadContent will be called once per game and is the place to unload /// game-specific content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } #endregion #region Update /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary>
118
/// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyState = Keyboard.GetState(); //Rotation if (keyState.IsKeyDown(Keys.PageUp)) { worldRotation = Matrix.CreateRotationY(0.01f); } else if (keyState.IsKeyDown(Keys.PageDown)) { worldRotation = Matrix.CreateRotationY(-0.01f); } else { worldRotation = Matrix.CreateRotationY(0); } float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f; ProcessInput(timeDifference); worldMatrix *= worldTranslation * worldRotation; base.Update(gameTime); } #endregion #region Draw /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 100.0f; DrawRefractionMap(); DrawReflectionMap(); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawSkyDome(viewMatrix); DrawTerrain(viewMatrix); DrawWater(time); base.Draw(gameTime); }
119
#endregion }
}
effects.fx
//---------------------------------------------------- //-- This effect file derived from: -- //-- www.riemers.net -- //-- Basic shaders -- //-- -- //-- Modified for MonoGame by John K. Bennett -- //-- -- //-- Use/modify as you like -- //-- -- //---------------------------------------------------- struct VertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float LightingFactor: TEXCOORD0; float2 TextureCoords: TEXCOORD1; }; struct PixelToFrame { float4 Color : COLOR0; }; //------- Constants -------- float4x4 xView; float4x4 xProjection; float4x4 xWorld; float3 xLightDirection; float xAmbient; bool xEnableLighting; bool xShowNormals; bool Clipping; float4 ClipPlane0; float4x4 xReflectionView; float xWaveLength; float xWaveHeight; float3 xCamPos; float xDirtyWaterFactor; float4 xDullColor; float xTime; float3 xWindDirection; float xWindForce; //------- Texture Samplers -------- Texture xTexture; sampler TextureSampler = sampler_state { texture = <xTexture>; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;};
120
Texture xTexture0; sampler TextureSampler0 = sampler_state { texture = <xTexture0>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture1; sampler TextureSampler1 = sampler_state { texture = <xTexture1>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture2; sampler TextureSampler2 = sampler_state { texture = <xTexture2>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xTexture3; sampler TextureSampler3 = sampler_state { texture = <xTexture3>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xRefractionMap; sampler RefractionSampler = sampler_state { texture = <xRefractionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xReflectionMap; sampler ReflectionSampler = sampler_state { texture = <xReflectionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xWaterBumpMap; sampler WaterBumpMapSampler = sampler_state { texture = <xWaterBumpMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; //------- Technique: Pretransformed -------- VertexToPixel PretransformedVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; Output.Position = inPos; Output.Color = inColor; return Output; } PixelToFrame PretransformedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique Pretransformed { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 PretransformedVS(); PixelShader = compile ps_4_0_level_9_1 PretransformedPS(); } }
121
//------- Technique: Colored -------- VertexToPixel ColoredVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame ColoredPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Colored { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredVS(); PixelShader = compile ps_4_0_level_9_1 ColoredPS(); } } //------- Technique: ColoredNoShading -------- // No lighting or shading, so no normal info passed in VertexToPixel ColoredNoShadingVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; return Output; } PixelToFrame ColoredNoShadingPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0;
122
Output.Color = PSIn.Color; return Output; } technique ColoredNoShading { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredNoShadingVS(); PixelShader = compile ps_4_0_level_9_1 ColoredNoShadingPS(); } } //------- Technique: Textured -------- VertexToPixel TexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame TexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Textured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 TexturedVS(); PixelShader = compile ps_4_0_level_9_1 TexturedPS(); } } //------- Technique: SimpleTextured -------- VertexToPixel SimpleTexturedVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD0) {
123
VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; return Output; } PixelToFrame SimpleTexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; }
technique SimpleTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 SimpleTexturedVS(); PixelShader = compile ps_4_0_level_9_1 SimpleTexturedPS(); } } //------- Technique: Multitextured -------- struct MTVertexToPixel { float4 Position : POSITION0; float4 Color : COLOR0; float3 Normal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 LightDirection : TEXCOORD2; float4 TextureWeights : TEXCOORD3; float Depth : TEXCOORD4; float4 clipDistances : TEXCOORD5; }; struct MTPixelToFrame { float4 Color : COLOR0; }; MTVertexToPixel MultiTexturedVS(float4 inPos : POSITION, float3 inNormal : NORMAL, float2 inTexCoords : TEXCOORD0, float4 inTexWeights : TEXCOORD1) { MTVertexToPixel Output = (MTVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Normal = (float3) mul(normalize(float4(inNormal, 0.0)), xWorld); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -xLightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights;
124
Output.Depth = Output.Position.z / Output.Position.w; Output.clipDistances = dot(inPos, ClipPlane0); return Output; } MTPixelToFrame MultiTexturedPS(MTVertexToPixel PSIn) { MTPixelToFrame Output = (MTPixelToFrame)0; if (Clipping) clip(PSIn.clipDistances); float lightingFactor = 1; float blendDistance = 0.99f; float blendWidth = 0.005f; float blendFactor = clamp((PSIn.Depth - blendDistance) / blendWidth, 0, 1); if (xEnableLighting) lightingFactor = saturate(saturate(dot(float4(PSIn.Normal, 0.0), PSIn.LightDirection)) + xAmbient); float4 farColor; farColor = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; farColor += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; farColor += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; farColor += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w; float4 nearColor; float2 nearTextureCoords = PSIn.TextureCoords * 3; nearColor = tex2D(TextureSampler0, nearTextureCoords)*PSIn.TextureWeights.x; nearColor += tex2D(TextureSampler1, nearTextureCoords)*PSIn.TextureWeights.y; nearColor += tex2D(TextureSampler2, nearTextureCoords)*PSIn.TextureWeights.z; nearColor += tex2D(TextureSampler3, nearTextureCoords)*PSIn.TextureWeights.w; Output.Color = lerp(nearColor, farColor, blendFactor); Output.Color *= lightingFactor; return Output; } technique MultiTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 MultiTexturedVS(); PixelShader = compile ps_4_0_level_9_1 MultiTexturedPS(); } } //------- Technique: Water -------- struct WVertexToPixel { float4 Position : POSITION; float4 ReflectionMapSamplingPos : TEXCOORD1; float2 BumpMapSamplingPos : TEXCOORD2; float4 RefractionMapSamplingPos : TEXCOORD3;
125
float4 Position3D : TEXCOORD4; }; struct WPixelToFrame { float4 Color : COLOR0; }; WVertexToPixel WaterVS(float4 inPos : POSITION, float2 inTex : TEXCOORD) { WVertexToPixel Output = (WVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); float4x4 preReflectionViewProjection = mul(xReflectionView, xProjection); float4x4 preWorldReflectionViewProjection = mul(xWorld, preReflectionViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.ReflectionMapSamplingPos = mul(inPos, preWorldReflectionViewProjection); Output.RefractionMapSamplingPos = mul(inPos, preWorldViewProjection); Output.Position3D = mul(inPos, xWorld); float3 windDir = normalize(xWindDirection); float3 perpDir = cross(xWindDirection, float3(0, 1, 0)); float ydot = dot(inTex, xWindDirection.xz); float xdot = dot(inTex, perpDir.xz); float2 moveVector = float2(xdot, ydot); moveVector.y += xTime * xWindForce; Output.BumpMapSamplingPos = moveVector / xWaveLength; return Output; } WPixelToFrame WaterPS(WVertexToPixel PSIn) { WPixelToFrame Output = (WPixelToFrame)0; float4 bumpColor = tex2D(WaterBumpMapSampler, PSIn.BumpMapSamplingPos); float2 perturbation = xWaveHeight * (bumpColor.rg - 0.5f)*2.0f; float2 ProjectedTexCoords; ProjectedTexCoords.x = PSIn.ReflectionMapSamplingPos.x / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; ProjectedTexCoords.y = -PSIn.ReflectionMapSamplingPos.y / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; float2 perturbatedTexCoords = ProjectedTexCoords + perturbation; float4 reflectiveColor = tex2D(ReflectionSampler, perturbatedTexCoords); float2 ProjectedRefrTexCoords; ProjectedRefrTexCoords.x = PSIn.RefractionMapSamplingPos.x / PSIn.RefractionMapSamplingPos.w / 2.0f + 0.5f; ProjectedRefrTexCoords.y = -PSIn.RefractionMapSamplingPos.y / PSIn.RefractionMapSamplingPos.w / 2.0f + 0.5f;
126
float2 perturbatedRefrTexCoords = ProjectedRefrTexCoords + perturbation; float4 refractiveColor = tex2D(RefractionSampler, perturbatedRefrTexCoords); float3 eyeVector = (float3) normalize(float4(xCamPos,0.0) - PSIn.Position3D); float3 normalVector = (bumpColor.rbg - 0.5f)*2.0f; float fresnelTerm = dot(eyeVector, normalVector); float4 combinedColor = lerp(reflectiveColor, refractiveColor, fresnelTerm); Output.Color = lerp(combinedColor, xDullColor, xDirtyWaterFactor); // add specular highlights float3 reflectionVector = -reflect(xLightDirection, normalVector); float specular = abs(dot(normalize(reflectionVector), normalize(eyeVector))); specular = pow(specular, 256); Output.Color.rgb += specular; return Output; } technique Water { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 WaterVS(); PixelShader = compile ps_4_0_level_9_1 WaterPS(); } }
Making Better Clouds - Perlin Noise
Noise is an important aspect of realism in game programming. Noise (sometimes called “coherent noise”)
is not the same as static. Static (or incoherent noise) is represented by a set of totally random set of data
points, where each point has nothing to do with its neighboring points. An example of a 2D static map is
shown on the left side of the image below.
In a noise map, the value of each point smoothly changes from point to point, there are no discontinuities..
So although the value in each point is different, the value of a point is highly likely to be close to the
values of its neighboring points. An example of a noise map is shown on the right side of the image
below.
127
Noise is routinely used by visual effects artists to increase the appearance of realism in computer-
generated graphical images. The cloudmap and heightmap are examples of noise maps that we have
already used.
Perlin noise is named for its inventor, Ken Perlin, who developed the idea while working at Mathematical
Applications Group, Inc. The use of Perlin noise in the 1982 movie “Tron” led to an Academy Award for
Technical Achievement. The Perlin noise function has a pseudo-random appearance, but all of its visual
details are the same size. This property allows it to be readily controllable; multiple scaled copies of
Perlin noise can be inserted into mathematical expressions to create a variety of textures. Perlin noise is
widely used in computer graphics for visual effects like fire, smoke, and clouds. It is also frequently used
to generate textures when memory is limited. Synthetic texture using Perlin noise is often used to imitate
the apparently random textures of nature. For an understandable explanation of the details of Perlin noise
generation, see https://en.wikipedia.org/wiki/Perlin_noise.
In this section we will generate an modified version of Perlin Noise to learn how to generate a noise map.
We will then use this noise map to make our clouds more realistic. We will begin with multiple static
maps, which are easy to generate as they only contain random numbers. The resolution of the static maps
has to be the same, but the level of detail will increase by a factor of two from map to map, as shown in
these images:
If we sum these maps together, we will get an image that looks like this:
So far so good, but we can do better. First instead of using six different maps, we will use the same map
six times, with differently scaled texture coordinates. Second, we need to linearly interpolate between the
small number of values used to create the map. As it happens, the texture interpolator on our graphics
128
card will do this job automatically for us. How cool is that?
Let’s begin by coding a small method that generates a low-resolution static map. Add the following
method to the skydome region:
private Texture2D CreateStaticMap(int resolution)
{ Random rand = new Random(); Color[] noisyColors = new Color[resolution * resolution]; for (int x = 0; x < resolution; x++) for (int y = 0; y < resolution; y++) noisyColors[x + y * resolution] = new Color(new Vector3((float)rand.Next(1000) / 1000.0f, 0, 0)); Texture2D noiseImage = new Texture2D(device, resolution, resolution, true, SurfaceFormat.Color); noiseImage.SetData(noisyColors); return noiseImage; }
Simply specify the resolution, and this method will return a square texture with completely random values
stored as the red color component in all of its pixels.
Since we are going to render the noise map as a HLSL effect, we will need two triangles covering the
whole screen. Two triangles need six vertices as a TriangleList, or four vertices as a TriangleStrip:
private void SetUpFullscreenVertices()
{ VertexPositionTexture[] vertices = new VertexPositionTexture[4]; vertices[0] = new VertexPositionTexture(new Vector3(-1, 1, 0f), new Vector2(0, 1)); vertices[1] = new VertexPositionTexture(new Vector3(1, 1, 0f), new Vector2(1, 1)); vertices[2] = new VertexPositionTexture(new Vector3(-1, -1, 0f), new Vector2(0, 0)); vertices[3] = new VertexPositionTexture(new Vector3(1, -1, 0f), new Vector2(1, 0)); fullScreenVertices = vertices; }
And we need to add some new instance variables to our Game1 code:
RenderTarget2D cloudsRenderTarget; Texture2D cloudStaticMap; VertexPositionTexture[] fullScreenVertices;
We will use the cloudStaticMap to hold our basic static map. Initialize the render target in our
LoadContent method (after reflectionRenderTarget is initialized:
129
cloudsRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight,
false, pp.BackBufferFormat,
pp.DepthStencilFormat);
Generate the static map in the LoadTextures method:
cloudStaticMap = CreateStaticMap(CLOUD_SIZE);
Which will generate a CLOUD_SIZE x CLOUD_SIZE static map. We need to define this constant on our
constant region (Larger CLOUD_SIZE values make for smaller clouds): //cloud constants public const int CLOUD_SIZE = 32; //use 64 to make smaller clouds
Finally, we need to call SetUpFullscreenVertices at the end of the LoadContent method:
SetUpFullscreenVertices();
Now we need to create a new HLSL technique to do the majority of the work for us. The PerlinNoise
vertex shader simply to pass the texture coordinates to the pixel shader. In the pixel shader, we will take
the static image, and sample it six times at different resolutions. Since we want the Perlin value to remain
between 0 and 1, we need to scale the result to do this. The first sampling, which has the lowest
resolution, has the largest influence: 50% of the final result. The next one has 25%, then 12.5% and so on
to the highest resolution sampling. We need to make sure that when we sum everything up, we end with a
total of exactly 100%.
The last thing we need to do is to make our clouds move. We do this by moving the texture over the
skybox. However, real clouds do not just move, they also change shape. We can simulate this effect by
having the images of different resolution scroll over each other at different speeds.
Finally, since the Perlin value is between 0 and 1, we can sharpen up the image by taking it to a power
larger than 1. The smaller the xOvercast value, the smaller and sharper our Perlin clouds will be (since we
subtract this value from 1.0f, we get the inverse). Here is the resulting PerlinNoise shader, which you
should place at the end of effects.fx:
//------- Technique: PerlinNoise -------- struct PNVertexToPixel { float4 Position : POSITION; float2 TextureCoords : TEXCOORD0; }; struct PNPixelToFrame { float4 Color : COLOR0; }; PNVertexToPixel PerlinVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD) {
130
PNVertexToPixel Output = (PNVertexToPixel)0; Output.Position = inPos; Output.TextureCoords = inTexCoords; return Output; } PNPixelToFrame PerlinPS(PNVertexToPixel PSIn) { PNPixelToFrame Output = (PNPixelToFrame)0; float2 move = float2(0, 1); float4 perlin = tex2D(TextureSampler, (PSIn.TextureCoords) + xTime * move) / 2; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 2 + xTime * move) / 4; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 4 + xTime * move) / 8; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 8 + xTime * move) / 16; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 16 + xTime * move) / 32; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 32 + xTime * move) / 32; Output.Color.rgb = 1.0f - pow(abs(perlin.r), xOvercast)*2.0f; Output.Color.a = 1; return Output; } technique PerlinNoise { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 PerlinVS(); PixelShader = compile ps_4_0_level_9_1 PerlinPS(); } }
The last thing we need to do in our shader code is to add the xOvercast global constant:
float xOvercast;
That’s it for our shader code. Now in Game1.cs, we need to add a simple method that runs the
PerlinNoise effect on our skydome region:
private void GeneratePerlinNoise(float time) { device.SetRenderTarget(cloudsRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); effect.CurrentTechnique = effect.Techniques["PerlinNoise"]; effect.Parameters["xTexture"].SetValue(cloudStaticMap); effect.Parameters["xOvercast"].SetValue(OVERCAST_FACTOR); effect.Parameters["xTime"].SetValue(time / TIME_DIV); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply();
131
device.DrawUserPrimitives(PrimitiveType.TriangleStrip, fullScreenVertices, 0, 2); } device.SetRenderTarget(null); cloudMap = cloudsRenderTarget; }
And we need to add three new cloud constants to parameterize the effect (we will use Sky_Top_Color
soon):
public const float OVERCAST_FACTOR = 1.2f; // Increase to make more cloudy public const float TIME_DIV = 4000.0f; // Increase to make clouds move more slowly public Vector4 SKY_TOP_COLOR = new Vector4(0.3f, 0.3f, 0.8f, 1);
Finally, we need to call this method from the our Draw method (after DrawReflectionMap is a good
spot):
GeneratePerlinNoise(time);
If you save the cloudMap texture to file, you would get something like this:
If we ran our code at this point (feel free to do this) we would have a sky with great clouds and a black
background. Let’s fix this by creating a skydome technique worthy of displaying this map on our
skydome. This will result in a gradient skydome.
If we take a look outside, we notice that the top of the atmosphere has a deeper color than the horizon. We
can imitate that effect in HLSL. Let’s create a new Skydome shader technique to accomplish this.
The vertex shader of our new technique will need to pass the texture coordinates to the pixel shader, but
also the object position of each vertex. The object position of a vertex is the position of the vertex inside
the object, in this case, the skydome itself. This position remains the same all the time, no matter whether
the object is rotated, moved or scaled in the 3D world. This will allow us to define a fixed gradient in the
pixel shader.
132
This is our technique so far (through the vertex shader):
//------- Technique: SkyDome --------
struct SDVertexToPixel { float4 Position : POSITION; float2 TextureCoords : TEXCOORD0; float4 ObjectPosition : TEXCOORD1; }; struct SDPixelToFrame { float4 Color : COLOR0; }; SDVertexToPixel SkyDomeVS( float4 inPos : POSITION, float2 inTexCoords: TEXCOORD0) { SDVertexToPixel Output = (SDVertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; Output.ObjectPosition = inPos; return Output; }
You can see that the original position and texture coordinates are routed immediately to the
ObjectPosition and TextureCoords outputs, respectively. Because the skydome is a 3D model that needs
to be transformed to 2D screen space, we as usual need to transform the 3D position by the
WorldViewProjection matrix and pass the result to the mandatory Output.Position.
The pixel shader is straightforward. We define two colors: one blue-ish color for the top of the skydome
(passed in as float4 xSkyTopColor; in “Constants”, which you need to add at this point), and white for
the horizons. Add this to our global constants:
float4 xSkyTopColor;
Next, we interpolate between both colors, based on how high the current pixel is in the skydome. The
highest point in the skydome has height Y=0.5, so all pixels above 0.4 will get the xSkyTopColor color.
The pixels lower than 0.4 will get a gradient color between the xSkyTopColor color and white. Finally, we
look up the value in the cloud map, corresponding to the current pixel. We use this value to interpolate
between our sky color and white, which adds the clouds to our skydome. The rest of our SkyDome
techniques looks like this:
SDPixelToFrame SkyDomePS(SDVertexToPixel PSIn) { SDPixelToFrame Output = (SDPixelToFrame)0; float4 bottomColor = 1; float4 baseColor = lerp(bottomColor, xSkyTopColor, saturate((PSIn.ObjectPosition.y) / 0.4f));
133
float4 cloudValue = tex2D(TextureSampler, PSIn.TextureCoords).r; Output.Color = lerp(baseColor, 1, cloudValue); return Output; } technique SkyDome { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 SkyDomeVS(); PixelShader = compile ps_4_0_level_9_1 SkyDomePS(); } }
Now, back in Game1.cs, we need to change the DrawSkydome method, to use our new SkyDome
technique, as follows:
currentEffect.CurrentTechnique = currentEffect.Techniques["SimpleTextured"]; currentEffect.CurrentTechnique = currentEffect.Techniques["SkyDome"]; currentEffect.Parameters["xSkyTopColor"].SetValue(SKY_TOP_COLOR);
Run this code. We should see a blue sky with moving, changing clouds. The only things we do not have
at this point are trees, the subject of the next section. Here is our code so far:
Game1.cs
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; namespace Lab8_Mono { /// <summary> /// This is the main type for your game. /// </summary> public class Game1 : Game { #region Constants //Lighting Constants Vector3 LIGHT_DIRECTION = new Vector3(1.0f, -1.0f, 1.0f); //use (1.0f, -1.0f, 1.0f) to flip light public const float AMBIENT_LIGHT_LEVEL = 0.7f; // Camera control constants public const float ROTATION_SPEED = 0.3f; public const float MOVE_SPEED = 30.0f; // Terrain texture mapping constants public const float MAX_HT = 30.0f; public const float MIN_HT = 0.0f;
134
public const float SAND_UPPER = 0.266f * MAX_HT; // 8 public const float GRASS_MID = 0.4f * MAX_HT; // 12 public const float GRASS_RANGE = 0.2f * MAX_HT; // 12 +/- 6 public const float ROCK_MID = 0.666f * MAX_HT; // 20 public const float ROCK_RANGE = 0.2f * MAX_HT; // 20 +/- 6 public const float SNOW_LOWER = 0.8f * MAX_HT; // 24 // Water Constants const float WATER_HEIGHT = 5.0f; public const float WAVE_LENGTH = 0.2f; public const float WAVE_HEIGHT = 0.3f; public Vector4 DULL_COLOR = new Vector4(0.3f, 0.4f, 0.5f, 1.0f); public const float WATER_DIRTINESS = 0.2f; // Increase to make the water "dirtier" public const float WIND_FORCE = 0.0005f; // It doesn't take much public Vector3 WIND_DIRECTION = new Vector3(0, 0, 1); //cloud constants public const int CLOUD_SIZE = 32; //use 64 to make smaller clouds public const float OVERCAST_FACTOR = 1.2f; // Increase to make more cloudy public const float TIME_DIV = 4000.0f; // Increase to make clouds move more slowly public Vector4 SKY_TOP_COLOR = new Vector4(0.3f, 0.3f, 0.8f, 1); #endregion #region Vertex Structs public struct VertexPositionColorNormal { public Vector3 Position; public Color Color; public Vector3 Normal; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Color, VertexElementUsage.Color, 0), new VertexElement(sizeof(float) * 3 + 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0) ); } #endregion #region Vertex Structs public struct VertexMultitextured { public Vector3 Position; public Vector3 Normal; public Vector4 TextureCoordinate; public Vector4 TexWeights; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration (
135
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0), new VertexElement(sizeof(float) * 6, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 0), new VertexElement(sizeof(float) * 10, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1) ); } #endregion #region Game1 Instance Vars GraphicsDeviceManager graphics; GraphicsDevice device; Effect effect; VertexMultitextured[] vertices; int[] indices; Matrix viewMatrix; Matrix projectionMatrix; VertexBuffer myVertexBuffer; IndexBuffer myIndexBuffer; VertexBuffer waterVertexBuffer; private int terrainWidth; private int terrainLength; private float[,] heightData; Matrix worldMatrix = Matrix.Identity; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity; Vector3 cameraPosition = new Vector3(130, 30, -50); float leftrightRot = MathHelper.PiOver2; float updownRot = -MathHelper.Pi / 10.0f; MouseState originalMouseState; Texture2D grassTexture; Texture2D sandTexture; Texture2D rockTexture; Texture2D snowTexture; Texture2D cloudMap; Model skyDome; RenderTarget2D refractionRenderTarget; Texture2D refractionMap; RenderTarget2D reflectionRenderTarget; Texture2D reflectionMap; Matrix reflectionViewMatrix; Texture2D waterBumpMap;
136
RenderTarget2D cloudsRenderTarget; Texture2D cloudStaticMap; VertexPositionTexture[] fullScreenVertices; #endregion #region Class Game1 Constructor public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } #endregion #region Terrain Methods private void SetUpVertices() { vertices = new VertexMultitextured[terrainWidth * terrainLength]; for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainLength; y++) { vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / MAX_HT; vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / MAX_HT; vertices[x + y * terrainWidth].TexWeights.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MIN_HT) / SAND_UPPER, 0, 1); vertices[x + y * terrainWidth].TexWeights.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - GRASS_MID) / GRASS_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - ROCK_MID) / ROCK_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MAX_HT) / SNOW_LOWER, 0, 1); float total = vertices[x + y * terrainWidth].TexWeights.X; total += vertices[x + y * terrainWidth].TexWeights.Y; total += vertices[x + y * terrainWidth].TexWeights.Z; total += vertices[x + y * terrainWidth].TexWeights.W; vertices[x + y * terrainWidth].TexWeights.X /= total; vertices[x + y * terrainWidth].TexWeights.Y /= total; vertices[x + y * terrainWidth].TexWeights.Z /= total; vertices[x + y * terrainWidth].TexWeights.W /= total; } } } private void SetUpIndices() { indices = new int[(terrainWidth - 1) * (terrainLength - 1) * 6];
137
int counter = 0; for (int y = 0; y < terrainLength - 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 CopyToBuffers() { myVertexBuffer = new VertexBuffer(device, VertexMultitextured.vertexDeclaration, vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices); myIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly); myIndexBuffer.SetData(indices); } private void LoadHeightData(Texture2D heightMap) {
138
terrainWidth = heightMap.Width; terrainLength = heightMap.Height; float minimumHeight = float.MaxValue; float maximumHeight = float.MinValue; Color[] heightMapColors = new Color[terrainWidth * terrainLength]; heightMap.GetData(heightMapColors); heightData = new float[terrainWidth, terrainLength]; for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) { heightData[x, y] = heightMapColors[x + y * terrainWidth].R; if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y]; if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y]; } for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * MAX_HT; } private void DrawTerrain(Matrix currentViewMatrix) { LIGHT_DIRECTION.Normalize(); effect.Parameters["xLightDirection"].SetValue(LIGHT_DIRECTION); effect.Parameters["xAmbient"].SetValue(AMBIENT_LIGHT_LEVEL); effect.Parameters["xEnableLighting"].SetValue(true); effect.CurrentTechnique = effect.Techniques["MultiTextured"]; effect.Parameters["xTexture0"].SetValue(sandTexture); effect.Parameters["xTexture1"].SetValue(grassTexture); effect.Parameters["xTexture2"].SetValue(rockTexture); effect.Parameters["xTexture3"].SetValue(snowTexture); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xWorld"].SetValue(worldMatrix); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.Indices = myIndexBuffer; device.SetVertexBuffer(myVertexBuffer); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, indices.Length / 3); } } #endregion #region Texture Loads
139
private void LoadTextures() { grassTexture = Content.Load<Texture2D>("grass"); sandTexture = Content.Load<Texture2D>("sand"); rockTexture = Content.Load<Texture2D>("rock"); snowTexture = Content.Load<Texture2D>("snow"); cloudMap = Content.Load<Texture2D>("cloudMap"); waterBumpMap = Content.Load<Texture2D>("waterbump"); cloudStaticMap = CreateStaticMap(CLOUD_SIZE); } #endregion #region Camera Methods private void SetUpCamera() { UpdateViewMatrix(); projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 0.3f, 1000.0f); } private void UpdateViewMatrix() { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation); viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector); Vector3 reflCameraPosition = cameraPosition; reflCameraPosition.Y = -cameraPosition.Y + WATER_HEIGHT * 2; Vector3 reflTargetPos = cameraFinalTarget; reflTargetPos.Y = -cameraFinalTarget.Y + WATER_HEIGHT * 2; Vector3 cameraRight = Vector3.Transform(new Vector3(1, 0, 0), cameraRotation); Vector3 invUpVector = Vector3.Cross(cameraRight, reflTargetPos - reflCameraPosition); reflectionViewMatrix = Matrix.CreateLookAt(reflCameraPosition, reflTargetPos, invUpVector); } private void ProcessInput(float amount) { MouseState currentMouseState = Mouse.GetState(); if (currentMouseState != originalMouseState)
140
{ float xDifference = currentMouseState.X - originalMouseState.X; float yDifference = currentMouseState.Y - originalMouseState.Y; leftrightRot -= ROTATION_SPEED * xDifference * amount; updownRot -= ROTATION_SPEED * yDifference * amount; Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); UpdateViewMatrix(); } Vector3 moveVector = new Vector3(0, 0, 0); KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W)) moveVector += new Vector3(0, 0, -1); if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S)) moveVector += new Vector3(0, 0, 1); if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D)) moveVector += new Vector3(1, 0, 0); if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A)) moveVector += new Vector3(-1, 0, 0); if (keyState.IsKeyDown(Keys.Q)) moveVector += new Vector3(0, 1, 0); if (keyState.IsKeyDown(Keys.Z)) moveVector += new Vector3(0, -1, 0); AddToCameraPosition(moveVector * amount); } private void AddToCameraPosition(Vector3 vectorToAdd) { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation); cameraPosition += MOVE_SPEED * rotatedVector; UpdateViewMatrix(); } #endregion #region Skydome Methods private void DrawSkyDome(Matrix currentViewMatrix) { GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead; Matrix[] modelTransforms = new Matrix[skyDome.Bones.Count]; skyDome.CopyAbsoluteBoneTransformsTo(modelTransforms); Matrix wMatrix = Matrix.CreateTranslation(0, -0.3f, 0) * Matrix.CreateScale(500) * Matrix.CreateTranslation(cameraPosition); foreach (ModelMesh mesh in skyDome.Meshes) { foreach (Effect currentEffect in mesh.Effects) { Matrix mworldMatrix = modelTransforms[mesh.ParentBone.Index] * wMatrix; //currentEffect.CurrentTechnique = currentEffect.Techniques["SimpleTextured"]; currentEffect.CurrentTechnique = currentEffect.Techniques["SkyDome"];
141
currentEffect.Parameters["xSkyTopColor"].SetValue(SKY_TOP_COLOR); currentEffect.Parameters["xAmbient"].SetValue(1.0f); currentEffect.Parameters["xWorld"].SetValue(mworldMatrix); currentEffect.Parameters["xView"].SetValue(currentViewMatrix); currentEffect.Parameters["xProjection"].SetValue(projectionMatrix); currentEffect.Parameters["xTexture"].SetValue(cloudMap); currentEffect.Parameters["xEnableLighting"].SetValue(false); } mesh.Draw(); } GraphicsDevice.DepthStencilState = DepthStencilState.Default; } private Texture2D CreateStaticMap(int resolution) { Random rand = new Random(); Color[] noisyColors = new Color[resolution * resolution]; for (int x = 0; x < resolution; x++) for (int y = 0; y < resolution; y++) noisyColors[x + y * resolution] = new Color(new Vector3((float)rand.Next(1000) / 1000.0f, 0, 0)); Texture2D noiseImage = new Texture2D(device, resolution, resolution, true, SurfaceFormat.Color); noiseImage.SetData(noisyColors); return noiseImage; } private void SetUpFullscreenVertices() { VertexPositionTexture[] vertices = new VertexPositionTexture[4]; vertices[0] = new VertexPositionTexture(new Vector3(-1, 1, 0f), new Vector2(0, 1)); vertices[1] = new VertexPositionTexture(new Vector3(1, 1, 0f), new Vector2(1, 1)); vertices[2] = new VertexPositionTexture(new Vector3(-1, -1, 0f), new Vector2(0, 0)); vertices[3] = new VertexPositionTexture(new Vector3(1, -1, 0f), new Vector2(1, 0)); fullScreenVertices = vertices; } private void GeneratePerlinNoise(float time) { device.SetRenderTarget(cloudsRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); effect.CurrentTechnique = effect.Techniques["PerlinNoise"]; effect.Parameters["xTexture"].SetValue(cloudStaticMap); effect.Parameters["xOvercast"].SetValue(OVERCAST_FACTOR); effect.Parameters["xTime"].SetValue(time / TIME_DIV); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply();
142
device.DrawUserPrimitives(PrimitiveType.TriangleStrip, fullScreenVertices, 0, 2); } device.SetRenderTarget(null); cloudMap = cloudsRenderTarget; } #endregion #region Water Methods private Plane CreatePlane(float height, Vector3 planeNormalDirection, Matrix currentViewMatrix, bool clipSide) { planeNormalDirection.Normalize(); Vector4 planeCoeffs = new Vector4(planeNormalDirection, height); if (clipSide) planeCoeffs *= -1; Matrix worldViewProjection = currentViewMatrix * projectionMatrix; Matrix inverseWorldViewProjection = Matrix.Invert(worldViewProjection); inverseWorldViewProjection = Matrix.Transpose(inverseWorldViewProjection); planeCoeffs = Vector4.Transform(planeCoeffs, inverseWorldViewProjection); Plane finalPlane = new Plane(planeCoeffs); return finalPlane; } private void DrawRefractionMap() { Plane refractionPlane = CreatePlane(WATER_HEIGHT + 1.5f, new Vector3(0, 1, 0), viewMatrix, false); effect.Parameters["ClipPlane0"].SetValue(new Vector4(refractionPlane.Normal, refractionPlane.D)); // Enable clipping for the purpose of creating a refraction map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(refractionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawTerrain(viewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); refractionMap = refractionRenderTarget; } private void DrawReflectionMap() { Plane reflectionPlane = CreatePlane(WATER_HEIGHT - 0.5f, new Vector3(0, -1, 0), reflectionViewMatrix, true); effect.Parameters["ClipPlane0"].SetValue(new Vector4(-reflectionPlane.Normal, -reflectionPlane.D));
143
// Enable clipping for the purpose of creating a reflection map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(reflectionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawSkyDome(reflectionViewMatrix); DrawTerrain(reflectionViewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); reflectionMap = reflectionRenderTarget; } private void SetUpWaterVertices() { VertexPositionTexture[] waterVertices = new VertexPositionTexture[6]; waterVertices[0] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[2] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertices[1] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, -terrainLength), new Vector2(0, 0)); waterVertices[3] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[5] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, 0), new Vector2(1, 1)); waterVertices[4] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, waterVertices.Length, BufferUsage.WriteOnly); waterVertexBuffer.SetData(waterVertices); } private void DrawWater(float time) { effect.CurrentTechnique = effect.Techniques["Water"]; Matrix worldMatrix = Matrix.Identity; effect.Parameters["xWorld"].SetValue(worldMatrix); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xReflectionView"].SetValue(reflectionViewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xRefractionMap"].SetValue(refractionMap); effect.Parameters["xReflectionMap"].SetValue(reflectionMap); effect.Parameters["xWaterBumpMap"].SetValue(waterBumpMap); effect.Parameters["xWaveLength"].SetValue(WAVE_LENGTH); effect.Parameters["xWaveHeight"].SetValue(WAVE_HEIGHT); effect.Parameters["xCamPos"].SetValue(cameraPosition); effect.Parameters["xDirtyWaterFactor"].SetValue(WATER_DIRTINESS); effect.Parameters["xDullColor"].SetValue(DULL_COLOR); effect.Parameters["xTime"].SetValue(time); effect.Parameters["xWindForce"].SetValue(WIND_FORCE);
144
effect.Parameters["xWindDirection"].SetValue(WIND_DIRECTION); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.SetVertexBuffer(waterVertexBuffer); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2); } } #endregion #region Initialize /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // JKB Note: This ordering and repetition of graphics profile commands addresses a // current bug in MonoGame 3.7. If we don't do it this way the window defaults to // a (small) fixed size. Also, MonoGame says it defaults to Reach, but it doesn't, so // we have to set HiDef explicitily here in order to use 32-bit index buffers. graphics.GraphicsProfile = GraphicsProfile.HiDef; graphics.IsFullScreen = false; graphics.ApplyChanges(); graphics.PreferredBackBufferWidth = 800; graphics.PreferredBackBufferHeight = 800; graphics.ApplyChanges(); Window.Title = "Lab8_Mono - Terrain Tutorial II"; base.Initialize(); } #endregion #region LoadContent /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { device = graphics.GraphicsDevice; effect = Content.Load<Effect>("effects");
145
skyDome = Content.Load<Model>("dome"); skyDome.Meshes[0].MeshParts[0].Effect = effect.Clone(); Texture2D heightMap = Content.Load<Texture2D>("heightmap2"); LoadHeightData(heightMap); Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); originalMouseState = Mouse.GetState(); SetUpCamera(); SetUpVertices(); SetUpIndices(); CalculateNormals(); CopyToBuffers(); LoadTextures(); PresentationParameters pp = device.PresentationParameters; refractionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); reflectionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); cloudsRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); SetUpWaterVertices(); SetUpFullscreenVertices(); } #endregion #region UnloadContent /// <summary> /// UnloadContent will be called once per game and is the place to unload /// game-specific content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } #endregion #region Update /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
146
this.Exit(); KeyboardState keyState = Keyboard.GetState(); //Rotation if (keyState.IsKeyDown(Keys.PageUp)) { worldRotation = Matrix.CreateRotationY(0.01f); } else if (keyState.IsKeyDown(Keys.PageDown)) { worldRotation = Matrix.CreateRotationY(-0.01f); } else { worldRotation = Matrix.CreateRotationY(0); } float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f; ProcessInput(timeDifference); worldMatrix *= worldTranslation * worldRotation; base.Update(gameTime); } #endregion #region Draw /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 100.0f; DrawRefractionMap(); DrawReflectionMap(); GeneratePerlinNoise(time); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawSkyDome(viewMatrix); DrawTerrain(viewMatrix); DrawWater(time); base.Draw(gameTime); } #endregion } }
147
effects.fx
//---------------------------------------------------- //-- This effect file derived from: -- //-- www.riemers.net -- //-- Basic shaders -- //-- -- //-- Modified for MonoGame by John K. Bennett -- //-- -- //-- Use/modify as you like -- //-- -- //---------------------------------------------------- struct VertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float LightingFactor: TEXCOORD0; float2 TextureCoords: TEXCOORD1; }; struct PixelToFrame { float4 Color : COLOR0; }; //------- Constants -------- float4x4 xView; float4x4 xProjection; float4x4 xWorld; float3 xLightDirection; float xAmbient; bool xEnableLighting; bool xShowNormals; bool Clipping; float4 ClipPlane0; float4x4 xReflectionView; float xWaveLength; float xWaveHeight; float3 xCamPos; float xDirtyWaterFactor; float4 xDullColor; float xTime; float3 xWindDirection; float xWindForce; float xOvercast; float4 xSkyTopColor; //------- Texture Samplers -------- Texture xTexture; sampler TextureSampler = sampler_state { texture = <xTexture>; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;}; Texture xTexture0;
148
sampler TextureSampler0 = sampler_state { texture = <xTexture0>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture1; sampler TextureSampler1 = sampler_state { texture = <xTexture1>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture2; sampler TextureSampler2 = sampler_state { texture = <xTexture2>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xTexture3; sampler TextureSampler3 = sampler_state { texture = <xTexture3>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xRefractionMap; sampler RefractionSampler = sampler_state { texture = <xRefractionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xReflectionMap; sampler ReflectionSampler = sampler_state { texture = <xReflectionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xWaterBumpMap; sampler WaterBumpMapSampler = sampler_state { texture = <xWaterBumpMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; //------- Technique: Pretransformed -------- VertexToPixel PretransformedVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; Output.Position = inPos; Output.Color = inColor; return Output; } PixelToFrame PretransformedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique Pretransformed { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 PretransformedVS(); PixelShader = compile ps_4_0_level_9_1 PretransformedPS(); } } //------- Technique: Colored --------
149
VertexToPixel ColoredVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame ColoredPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Colored { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredVS(); PixelShader = compile ps_4_0_level_9_1 ColoredPS(); } } //------- Technique: ColoredNoShading -------- // No lighting or shading, so no normal info passed in VertexToPixel ColoredNoShadingVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; return Output; } PixelToFrame ColoredNoShadingPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color;
150
return Output; } technique ColoredNoShading { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredNoShadingVS(); PixelShader = compile ps_4_0_level_9_1 ColoredNoShadingPS(); } } //------- Technique: Textured -------- VertexToPixel TexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame TexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Textured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 TexturedVS(); PixelShader = compile ps_4_0_level_9_1 TexturedPS(); } } //------- Technique: SimpleTextured -------- VertexToPixel SimpleTexturedVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection);
151
float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; return Output; }
PixelToFrame SimpleTexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique SimpleTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 SimpleTexturedVS(); PixelShader = compile ps_4_0_level_9_1 SimpleTexturedPS(); } } //------- Technique: Multitextured -------- struct MTVertexToPixel { float4 Position : POSITION0; float4 Color : COLOR0; float3 Normal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 LightDirection : TEXCOORD2; float4 TextureWeights : TEXCOORD3; float Depth : TEXCOORD4; float4 clipDistances : TEXCOORD5; }; struct MTPixelToFrame { float4 Color : COLOR0; }; MTVertexToPixel MultiTexturedVS(float4 inPos : POSITION, float3 inNormal : NORMAL, float2 inTexCoords : TEXCOORD0, float4 inTexWeights : TEXCOORD1) { MTVertexToPixel Output = (MTVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Normal = (float3) mul(normalize(float4(inNormal, 0.0)), xWorld); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -xLightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; Output.Depth = Output.Position.z / Output.Position.w; Output.clipDistances = dot(inPos, ClipPlane0);
152
return Output; } MTPixelToFrame MultiTexturedPS(MTVertexToPixel PSIn) { MTPixelToFrame Output = (MTPixelToFrame)0; if (Clipping) clip(PSIn.clipDistances); float lightingFactor = 1; float blendDistance = 0.99f; float blendWidth = 0.005f; float blendFactor = clamp((PSIn.Depth - blendDistance) / blendWidth, 0, 1); if (xEnableLighting) lightingFactor = saturate(saturate(dot(float4(PSIn.Normal, 0.0), PSIn.LightDirection)) + xAmbient); float4 farColor; farColor = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; farColor += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; farColor += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; farColor += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w; float4 nearColor; float2 nearTextureCoords = PSIn.TextureCoords * 3; nearColor = tex2D(TextureSampler0, nearTextureCoords)*PSIn.TextureWeights.x; nearColor += tex2D(TextureSampler1, nearTextureCoords)*PSIn.TextureWeights.y; nearColor += tex2D(TextureSampler2, nearTextureCoords)*PSIn.TextureWeights.z; nearColor += tex2D(TextureSampler3, nearTextureCoords)*PSIn.TextureWeights.w; Output.Color = lerp(nearColor, farColor, blendFactor); Output.Color *= lightingFactor; return Output; } technique MultiTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 MultiTexturedVS(); PixelShader = compile ps_4_0_level_9_1 MultiTexturedPS(); } } //------- Technique: Water -------- struct WVertexToPixel { float4 Position : POSITION; float4 ReflectionMapSamplingPos : TEXCOORD1; float2 BumpMapSamplingPos : TEXCOORD2; float4 RefractionMapSamplingPos : TEXCOORD3; float4 Position3D : TEXCOORD4; };
153
struct WPixelToFrame { float4 Color : COLOR0; }; WVertexToPixel WaterVS(float4 inPos : POSITION, float2 inTex : TEXCOORD) { WVertexToPixel Output = (WVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); float4x4 preReflectionViewProjection = mul(xReflectionView, xProjection); float4x4 preWorldReflectionViewProjection = mul(xWorld, preReflectionViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.ReflectionMapSamplingPos = mul(inPos, preWorldReflectionViewProjection); Output.RefractionMapSamplingPos = mul(inPos, preWorldViewProjection); Output.Position3D = mul(inPos, xWorld); float3 windDir = normalize(xWindDirection); float3 perpDir = cross(xWindDirection, float3(0, 1, 0)); float ydot = dot(inTex, xWindDirection.xz); float xdot = dot(inTex, perpDir.xz); float2 moveVector = float2(xdot, ydot); moveVector.y += xTime * xWindForce; Output.BumpMapSamplingPos = moveVector / xWaveLength; return Output; } WPixelToFrame WaterPS(WVertexToPixel PSIn) { WPixelToFrame Output = (WPixelToFrame)0; float4 bumpColor = tex2D(WaterBumpMapSampler, PSIn.BumpMapSamplingPos); float2 perturbation = xWaveHeight * (bumpColor.rg - 0.5f)*2.0f; float2 ProjectedTexCoords; ProjectedTexCoords.x = PSIn.ReflectionMapSamplingPos.x / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; ProjectedTexCoords.y = -PSIn.ReflectionMapSamplingPos.y / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; float2 perturbatedTexCoords = ProjectedTexCoords + perturbation; float4 reflectiveColor = tex2D(ReflectionSampler, perturbatedTexCoords); float2 ProjectedRefrTexCoords; ProjectedRefrTexCoords.x = PSIn.RefractionMapSamplingPos.x / PSIn.RefractionMapSamplingPos.w / 2.0f + 0.5f; ProjectedRefrTexCoords.y = -PSIn.RefractionMapSamplingPos.y / PSIn.RefractionMapSamplingPos.w / 2.0f + 0.5f; float2 perturbatedRefrTexCoords = ProjectedRefrTexCoords + perturbation; float4 refractiveColor = tex2D(RefractionSampler, perturbatedRefrTexCoords); float3 eyeVector = (float3) normalize(float4(xCamPos,0.0) - PSIn.Position3D);
154
float3 normalVector = (bumpColor.rbg - 0.5f)*2.0f; float fresnelTerm = dot(eyeVector, normalVector); float4 combinedColor = lerp(reflectiveColor, refractiveColor, fresnelTerm); Output.Color = lerp(combinedColor, xDullColor, xDirtyWaterFactor); // add specular highlights float3 reflectionVector = -reflect(xLightDirection, normalVector); float specular = abs(dot(normalize(reflectionVector), normalize(eyeVector))); specular = pow(specular, 256); Output.Color.rgb += specular; return Output; } technique Water { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 WaterVS(); PixelShader = compile ps_4_0_level_9_1 WaterPS(); } }
//------- Technique: PerlinNoise -------- struct PNVertexToPixel { float4 Position : POSITION; float2 TextureCoords : TEXCOORD0; }; struct PNPixelToFrame { float4 Color : COLOR0; }; PNVertexToPixel PerlinVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD) { PNVertexToPixel Output = (PNVertexToPixel)0; Output.Position = inPos; Output.TextureCoords = inTexCoords; return Output; } PNPixelToFrame PerlinPS(PNVertexToPixel PSIn) { PNPixelToFrame Output = (PNPixelToFrame)0; float2 move = float2(0, 1); float4 perlin = tex2D(TextureSampler, (PSIn.TextureCoords) + xTime * move) / 2; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 2 + xTime * move) / 4; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 4 + xTime * move) / 8; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 8 + xTime * move) / 16;
155
perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 16 + xTime * move) / 32; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 32 + xTime * move) / 32; Output.Color.rgb = 1.0f - pow(abs(perlin.r), xOvercast)*2.0f; Output.Color.a = 1; return Output; } technique PerlinNoise { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 PerlinVS(); PixelShader = compile ps_4_0_level_9_1 PerlinPS(); } } //------- Technique: SkyDome -------- struct SDVertexToPixel { float4 Position : POSITION; float2 TextureCoords : TEXCOORD0; float4 ObjectPosition : TEXCOORD1; }; struct SDPixelToFrame { float4 Color : COLOR0; }; SDVertexToPixel SkyDomeVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD0) { SDVertexToPixel Output = (SDVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; Output.ObjectPosition = inPos; return Output; } SDPixelToFrame SkyDomePS(SDVertexToPixel PSIn) { SDPixelToFrame Output = (SDPixelToFrame)0; float4 bottomColor = 1; float4 baseColor = lerp(bottomColor, xSkyTopColor, saturate((PSIn.ObjectPosition.y) / 0.4f)); float4 cloudValue = tex2D(TextureSampler, PSIn.TextureCoords).r; Output.Color = lerp(baseColor, 1, cloudValue); return Output; }
156
technique SkyDome { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 SkyDomeVS(); PixelShader = compile ps_4_0_level_9_1 SkyDomePS(); } }
Adding Trees - Billboarding
With our multitextured terrain and water finished, it’s time to add some trees to our terrain.
If you have not already done so, import tree.dds into your content project.
One way to create trees would be to load a 3D Model of a tree, and rendering it several hundred times on
our terrain. Unfortunately, this would likely result in an unacceptably slow frame rate. The solution is a
technique called “billboarding”. The idea behind billboarding is that most of the trees in a landscape are
there for scenery, not to be an important part of game play. Billboarding therefore replaces distant 3D
objects with simple 2D images. So in the case of a tree, we would replace a 3D tree model by a simple 2D
tree image, positioned at the same position in our 3D world. To make the tree realistic, we always keep it
facing toward the camera. “Spherical” billboarding keeps the 2D image facing toward the camera
regardless of position. “Cylindrical” billboarding keeps the image upright, and rotates the image only
about the Y axis. If you think about this, it is easy to see that cylindrical billboarding is what we need for
trees (since trees generally grow up, not sideways). Billboarding is a widely used technique in game
programming. And, if we ever really need a detailed 3D tree, we can always draw one when the camera
gets close enough to tell the difference.
The figures below illustrates how billboarding works. The left image shows five 2D images that are
positioned in a 3D world. The right image shows exactly the same five 2D images, rotated so that they
face the camera. If the images were of a tree, the right image would give a nice result, provided we were
not too close.
Billboards consist of two triangles of three vertices each, but two vertices of each triangle are shared, so
there are only four unique vertices. As we did with terrain, we will use indices to make the drawing of
billboards faster. There are six indices per billboard. We will thus render each billboard using two
triangles, identified by six indices that reference four vertices. For each billboard, the four vertices need to
157
contain the correct texture coordinates, but they will all contain the same position: the central bottom
position of the billboard, as shown in the image below. That’s easy to specify, as it’s the position where
the trunk will hit the terrain. The graphics process will take care of the math to correctly locate the four
vertices for each billboard, since it would have to do that anyway to locate the billboard in its 3D space.
In the figure below, each of the three vertices are given unique coordinates: (0,0), (0,1), (1,0) and (1,1).
Notice that vertices (0,0) and (1,1) are shared by the two triangles, and that the blue dot denotes the initial
position of the four vertices in object space.
In a moment, we will create a list of positions where we want a billboard in our 3D world. First let’s see
how we define vertices and indices once we have the list. The method below uses a list (treeList in this
case), and for each position in the list it will generate four vertices, and the corresponding six indices. It
then places this information in a vertex buffer and an index buffer, respectively. Create a new region
(Tree Methods), and add the following code:
#region Tree Methods
private void CreateBBVertsAndIntsFromList() { VertexPositionTexture[] billboardVertices = new VertexPositionTexture[treeList.Count * 4]; int [] billboardIndices = new int [treeList.Count * 6]; int j = 0; int i = 0; foreach (Vector3 currentV3 in treeList) { // Create vertices billboardVertices[i + 0] = new VertexPositionTexture(currentV3, new Vector2(0, 0)); billboardVertices[i + 1] = new VertexPositionTexture(currentV3, new Vector2(0, 1)); billboardVertices[i + 2] = new VertexPositionTexture(currentV3, new Vector2(1, 1)); billboardVertices[i + 3] = new VertexPositionTexture(currentV3, new Vector2(1, 0));
158
//Create indices billboardIndices[j++] = i + 0; billboardIndices[j++] = i + 3; billboardIndices[j++] = i + 2; billboardIndices[j++] = i + 2; billboardIndices[j++] = i + 1; billboardIndices[j++] = i + 0; i += 4; } treeVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, billboardVertices.Length, BufferUsage.WriteOnly); treeVertexBuffer.SetData(billboardVertices); treeIndexBuffer = new IndexBuffer(device, IndexElementSize.ThirtyTwoBits, treeList.Count * 6, BufferUsage.WriteOnly); treeIndexBuffer.SetData(billboardIndices); } #endregion
Now we need to define the VertexBuffer and IndexBuffer, and the treeList in our Game1 instance
variables, as follows:
VertexBuffer treeVertexBuffer; IndexBuffer treeIndexBuffer; List<Vector3> treeList;
In order to use the List type, we need to add a using statement to the top of Game1.cs: using System.Collections.Generic;
The billboard vertices will be transformed to the correct location by our vertex shader, so that the
resulting billboards will always be facing the camera. We will create this shader code in a moment, but
first we will complete the Game1 code. If you forgot to do so earlier, import tree.dds into your content
project.
Let’s also add a texture declaration:
Texture2D treeTexture;
And load this texture in the LoadTextures method:
treeTexture = Content.Load<Texture2D>("tree");
Now, let’s create a simple method in our Trees region that will generate a list with four trees:
private void GenerateTreePositions(VertexMultitextured[] terrainVertices) { treeList = new List<Vector3>();
159
treeList.Add(terrainVertices[3310].Position); treeList.Add(terrainVertices[3315].Position); treeList.Add(terrainVertices[3320].Position); treeList.Add(terrainVertices[3325].Position); }
This method creates a list, using four terrain positions chosen randomly. In the next section, we will
extend this method, for now we will just try to render these four trees to the screen. We need to call both
of these new methods from within LoadContent (place this code after the call to SetUpWaterVertices:
GenerateTreePositions(vertices);
CreateBBVertsAndIntsFromList();
Finally, we need a method to draw our billboards. Add a new method in the Tree Methods region to do
this:
private void DrawTrees(Matrix currentViewMatrix)
{ effect.CurrentTechnique = effect.Techniques["CylBillboard"]; effect.Parameters["xWorld"].SetValue(Matrix.Identity); effect.Parameters["xView"].SetValue(currentViewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xCamPos"].SetValue(cameraPosition); effect.Parameters["xAllowedRotDir"].SetValue(new Vector3(0, 1, 0)); effect.Parameters["xBillboardTexture"].SetValue(treeTexture); int numVertices = treeList.Count * 4; int numTriangles = treeList.Count * 2; device.SetVertexBuffer(treeVertexBuffer); device.Indices = treeIndexBuffer; // Turn on alpha blending GraphicsDevice.BlendState = BlendState.AlphaBlend; GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead; effect.CurrentTechnique.Passes[0].Apply(); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, numTriangles); // Reset graphics adapter to default state GraphicsDevice.BlendState = BlendState.Opaque; GraphicsDevice.DepthStencilState = DepthStencilState.Default; }
We select the cylindrical billboarding technique (which we will create soon), and set the necessary
MonoGame-to-HLSL variables. The only one that needs explanation is the xAllowedRotDir variable,
which specifies the axis around which the 2D images are allowed to rotate. This is the difference between
spherical and cylindrical billboarding: in spherical billboarding, the 2D image is allowed to rotate about
any axis to rotate the image to face the camera. Since we are dealing with trees, we want the tree only to
be rotated along its trunk, which is around the (0,1,0) Up axis.
Now call this method at the very end of our Draw method (after DrawWater):
160
DrawTrees(viewMatrix);
Finally, let’s create the new shader code. Add the following code to effects.fx. First, declare a new
global constant:
float3 xAllowedRotDir;
And add a texture sampler for the billboard texture:
Texture xBillboardTexture; sampler textureSampler = sampler_state { texture = <xBillboardTexture> ; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = CLAMP; AddressV = CLAMP;};
Then add the actual technique to the end of the file:
//------- Technique: CylBillboard -------- struct BBVertexToPixel { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; }; struct BBPixelToFrame { float4 Color : COLOR0; }; BBVertexToPixel CylBillboardVS(float3 inPos: POSITION0, float2 inTexCoord : TEXCOORD0) { BBVertexToPixel Output = (BBVertexToPixel)0; float3 center = (float3) mul(float4(inPos,0.0), xWorld); float3 eyeVector = center - xCamPos; float3 upVector = xAllowedRotDir; upVector = normalize(upVector); float3 sideVector = cross(eyeVector, upVector); sideVector = normalize(sideVector); float3 finalPosition = center; finalPosition += (inTexCoord.x - 0.5f)*sideVector; finalPosition += (1.5f - inTexCoord.y*1.5f)*upVector; float4 finalPosition4 = float4(finalPosition, 1); float4x4 preViewProjection = mul(xView, xProjection); Output.Position = mul(finalPosition4, preViewProjection); Output.TexCoord = inTexCoord; return Output; } BBPixelToFrame BillboardPS(BBVertexToPixel PSIn)
161
{ BBPixelToFrame Output = (BBPixelToFrame)0; Output.Color = tex2D(textureSampler, PSIn.TexCoord); return Output; } technique CylBillboard { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 CylBillboardVS(); PixelShader = compile ps_4_0_level_9_1 BillboardPS(); } }
Most of this code is stuff we have seen before. A few words of explanation are in order. In the vertex
shader, we have to account for the fact that all of the vertices are in the middle-bottom position. The
finalPosition computation deals with this. The upVector computation endures that we will only rotate
about the Y axis.
Run this code. You should see the four pathetic-looking trees shown below:
Move your camera around the trees, and your graphics card will render them so they are always facing the
camera. How cool is that? However, if you position the camera directly above the trees, you will see that
they are 2D images. If you place the camera above the trees and a little bit off center you can watch the
trees rotate as the camera moves up and down.
162
Adding More Trees
In this section, we will populate our world with forests. To get the most realistic result, instead of simply
putting trees randomly on our terrain, we are going to selectively add small patches of forest.
How should we create the boundaries of such a forest? The solution is to use a noisemap. We already
know how to create noisemaps, so to speed things up we will use a ready-made one (feel free to create
your own, just as we did for clouds). The image below depicts our ready-made noise map.
Our basic approach to using this noise map is as follows: for each pixel in the noisemap where the white
value is above a certain threshold, we will add a tree to our terrain. We will also take into account the
height of the terrain (forests are not likely in the middle of a river, or on top of a mountain), and the slope
of the terrain (forests are less likely on a steep mountain side). If you have not already done so, begin by
importing treeMap.jpg into your content project.
We have a lot of changes to make to the GenerateTreePositions method, so let’s look at the revised code,
and then work through it. Replace the existing GenerateTreePositions code with this code:
private void GenerateTreePositions(VertexMultitextured[] terrainVertices) { Color[] treeMapColors = new Color[treeMap.Width * treeMap.Height]; treeMap.GetData(treeMapColors); int[,] noiseData = new int[treeMap.Width, treeMap.Height]; for (int x = 0; x < treeMap.Width; x++) for (int y = 0; y < treeMap.Height; y++) noiseData[x, y] = treeMapColors[y + x * treeMap.Height].R; treeList = new List<Vector3>(); Random random = new Random(); for (int x = 0; x < terrainWidth; x++)
163
{ for (int y = 0; y < terrainLength; y++) { float terrainHeight = heightData[x, y]; if ((terrainHeight > TREE_MIN_HT) && (terrainHeight < TREE_MAX_HT)) { float flatness = Vector3.Dot(terrainVertices[x + y * terrainWidth].Normal, new Vector3(0, 1, 0)); float minFlatness = (float)Math.Cos(MathHelper.ToRadians(TREE_MAX_SLOPE)); if (flatness > minFlatness) { float relx = (float)x / (float)terrainWidth; float rely = (float)y / (float)terrainLength; float noiseValueAtCurrentPosition = noiseData[(int)(relx * treeMap.Width), (int)(rely * treeMap.Height)]; float treeDensity; if (noiseValueAtCurrentPosition > TREEMAP_HI_THOLD) treeDensity = HI_NUM_TREES; else if (noiseValueAtCurrentPosition > TREEMAP_MED_THOLD) treeDensity = MED_NUM_TREES; else if (noiseValueAtCurrentPosition > TREEMAP_LOW_THOLD) treeDensity = LOW_NUM_TREES; else treeDensity = 0; for (int currDetail = 0; currDetail < treeDensity; currDetail++) { float rand1 = (float)random.Next(1000) / 1000.0f; float rand2 = (float)random.Next(1000) / 1000.0f; Vector3 treePos = new Vector3((float)x - rand1, 0, -(float)y - rand2); treePos.Y = heightData[x, y]; treeList.Add(treePos); } } } } } }
We begin by converting the treeMap image into a 1D array of colors: first create the 1D array, large
enough to store all data and then copy the data from the texture into the array using the GetData method.
Next, the 1D array is reshaped into a 2D array of integers. For each pixel, the red component (which is a
value between 0 and 255) is extracted. Because the current noisemap is a greyscale map, the red
component will be the same as the blue and green components.
With our 2D array ready, we create the list of Vector3s to hold the final output of the method, plus a
random number generator (to give some realism to our tree location choices).
164
The final double for loop scans our terrain grid, and at each vertex we will decide whether we need to add
trees. For each vertex, we ask:
1. Is the height acceptable? We don’t want to put some trees in the middle of the river, or on top of
our mountains.
2. Is the terrain flat? We don’t want trees on steep hillsides.
3. What should be the density of trees at this location? We want many trees at the center of the
forests, while at the borders of the forest the density should be lower.
Let’s tackle these issues one by one. The first one is easy: we will simply look up the height of our terrain
at the current position, and check if it’s inside an acceptable region. This makes sure that trees only get
planted when the height of the terrain is between TREE_MIN_HT and TREE_MAX_HT. We need to add
some constants to define these boundaries:
//Tree Constants public const int TREE_MIN_HT = 8; // Min terrain height for trees public const int TREE_MAX_HT = 14; // Max terrain height for trees
Next, we address terrain steepness. Here we can use the normals defined for each vertex. The normal
indicates the direction perpendicular to the terrain at each vertex. So on a flat part of the terrain, the
normal will point upward. So we simply need to check how close the normal is to the Up vector. As we
have seen before, a good way to measure this is by taking the dot product of both vectors. If the vertex
and the (0,1,) Up vector are the same, the dot product will be 1. If they are perpendicular to each other, it
will be 0. The discerning reader will notice that the dot product is nothing more than the cosine of the
angle between the two vectors, so we can check to see that the inclination of the terrain is no more than
TREE_MAX_SLOPE degrees:
public const int TREE_MAX_SLOPE = 15; // Max terrain slope on which to put trees
Finally, we need to check whether the noise map indicates there should be a tree at the current position.
Therefore, we will sample the noisemap at the position corresponding to the current location on the
terrain. Since we want the resolutions of our terrain and noisemap to be independent of each other, we
work with relative coordinates. The relx and rely values will be between 0 and 1, allowing us to sample
the noisemap at the correct location. The noise value at the current location is stored in the
noiseValueAtCurrentPosition variable, which contains a value between 0 and 255. From this value, we
decide how many trees should be added at the current location. If the value is really high (above
TREEMAP_HI_THOLD), we’re in the middle of a forest and we want to add HI_NUM_TREES trees around
the current terrain vertex. This number decreases corresponding to the current noise value, and drops to
zero below a certain threshold. Lets define the constants that characterize this behavior:
public const int TREEMAP_HI_THOLD = 200; // Noise map value for HI_NUM_TREES trees public const int TREEMAP_MED_THOLD = 150; // Noise map value for MED_NUM_TREES trees public const int TREEMAP_LOW_THOLD = 100; // Noise map value for LOW_NUM_TREES trees public const int HI_NUM_TREES = 5; // Num trees at TREEMAP_HI_THOLD noise value
165
public const int MED_NUM_TREES = 4; // Num trees at TREEMAP_MED_THOLD noise value public const int LOW_NUM_TREES = 3; // Num trees at TREEMAP_LOW_THOLD noise value
At this point, we know exactly how many trees we should add at the current terrain vertex. The next bit of
code actually generates the locations for the new trees. First we generate two random numbers, which we
scale to the range [0,1]. We use these to add to the X and Z coordinates of the current vertex, and we use
the height of the terrain as Y coordinate. Once these values are computed, we add the new position to the
treeList, and we’re done.
The last thing we need to do is to load the treeMap.jpg texture. First declare the necessary Game1
instance variable:
Texture2D treeMap;
Then, in LoadTextures, load it.
treeMap = Content.Load<Texture2D>("treeMap");
Run the code at this point. You should see forests of trees. Experiment with different values for noise
and height thresholds. When you move your camera around the world, there are two flaws that you might
notice:
1. The most critical flaw is apparent when you move the camera above the highest mountain and
look at the trees. Trees behind other trees on certain terrain and for certain camera positions may
be occluded in odd ways. We will address this problem in the next section.
2. The second problem is that the trees are not reflected in the water. This is because we are not
rendering any trees in our DrawReflectionMap method, so let’s fix this. Add the following call
after the call to the DrawSkyDome method (in DrawReflectionMap).
DrawTrees(reflectionViewMatrix);
Now we should see at some points you will see trees reflected in the water (the camera angle has to be
right, just like in real life).
A Few Last Details
Our trees look good from most angles, but sometimes trees behind other trees look like they have been cut
in half, as depicted in the image below.
166
Why is this happening? To answer this question, consider what the graphics card is doing:
1. The skydome and terrain are rendered, saving their color in the backbuffer and their distance in
the Z buffer.
2. Our program enters the DrawTrees method, and asks the graphics card to render the trees.
3. The first tree is rendered to the screen. Let’s say this is the tree shown in front in the image above.
The whole rectangle of the 2D image is rendered. The transparent pixels are also rendered; in this
case the color already present in the backbuffer remains. However, since these pixels are actually
rendered, they change the value in the Z buffer for these pixels.
4. The second billboard is rendered. Let’s say this is the tree to the right of the front tree. For all
pixels of this rectangle, the graphics card will check whether the distance to the camera is closer
than the value stored in the Z buffer. This is where the problem arises: the first tree has already
written its distance in the Z buffer, so the pixels of the left part of the second tree have a larger
distance to the camera than the value already stored in the Z buffer. The graphics card, thinking it
is helping us, does not render pixels of objects that are behind other objects.
There are two solutions to this problem. The first is 100% accurate, but impractical for large numbers of
billboards, unless you have the computer resources available to George Lucas: render the trees, starting
from back to front. This way, all pixels of all trees will be rendered, allowing perfect blending. The only
drawback, however, is that we would need to order the billboards from back to front before rendering
them. This order depends entirely on the position of your camera, so we would need to do this reordering
each time the camera (or the position of a tree) moves. Since this reordering would be done by the CPU,
we cannot employ this approach.
167
The second approach is not 100% accurate, but it is close, and it is very fast. This is how we will render
our trees:
1. Start by rendering all billboards, but only the pixels of the trees that are not transparent. This will
not render the full rectangle of the images, but only those pixels of the core of trees and leaves.
Also, the Z buffer will be updated only for those pixels. You can see the result of this first step in
the left image below: only the core of the trunks and trees are rendered, but the order of the trees
is correct.
2. In the second step, we will render our billboards again, but this time only the (partly) transparent
pixels. While doing this, we will disable updating the Z buffer, because otherwise we will run
into the same trouble. We can disable the updating of the Z buffer, because the cores of the trees
have already been drawn correctly, and thus the most important pixels already have the correct Z
value. The result after the second step is shown in the right image below.
Step 1 Step 2
To implement this approach we need to change the DrawTrees method, and make a slight modification to
our pixel shader. Here is the new DrawTrees code, which we will explain below:
private void DrawTrees(Matrix currentViewMatrix) { effect.CurrentTechnique = effect.Techniques["CylBillboard"]; effect.Parameters["xWorld"].SetValue(Matrix.Identity); effect.Parameters["xView"].SetValue(currentViewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xCamPos"].SetValue(cameraPosition);
168
effect.Parameters["xAllowedRotDir"].SetValue(new Vector3(0, 1, 0)); effect.Parameters["xBillboardTexture"].SetValue(treeTexture); effect.Parameters["xBBAlphaTestValue"].SetValue(BB_ALPHA_TEST_VALUE); int numVertices = treeList.Count * 4; int numTriangles = treeList.Count * 2; device.SetVertexBuffer(treeVertexBuffer); device.Indices = treeIndexBuffer; GraphicsDevice.BlendState = BlendState.AlphaBlend; // First draw the "solid" part of the tree GraphicsDevice.DepthStencilState = DepthStencilState.Default; effect.Parameters["xAlphaTestGreater"].SetValue(true); effect.CurrentTechnique.Passes[0].Apply(); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, numTriangles); // Now draw the rest of the tree GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead; effect.Parameters["xAlphaTestGreater"].SetValue(false); effect.CurrentTechnique.Passes[0].Apply(); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, numTriangles); // Reset graphics adapter to default state GraphicsDevice.BlendState = BlendState.Opaque; GraphicsDevice.DepthStencilState = DepthStencilState.Default; }
The effect parameters are the same as before, except for new parameter xBBAlphaTestValue. This is the
value that we pass to the pixel shader to represent the alpha threshold of pixels of the core of trees and
leaves. We need to define a new constant to hold this value:
public const float BB_ALPHA_TEST_VALUE = 0.6f; // useful values are .5 to .9
After setting all but one of the effect parameters, we pre-compute the number of triangles and the number
of vertices, so we only have to do this once, and set the appropriate vertex and index buffers. Then we
turn on alpha blending (so transparent pixels will not be rendered). The next four lines are where we
draw the core trees and leaves. To do this, we have to set the DepthStencilState to allow depth buffer
writes. The xAlphaTestGreater parameter will be used to tell the pixel shader whether we are drawing the
core (alpha values greater than xBBAlphaTestValue), or the (mostly) transparent pixels (alpha values less
than or equal to xBBAlphaTestValue). After drawing the core trees and leaves, we draw the (mostly)
transparent pixels by setting xAlphaTestGreater to “false” and DepthStencilState to read-only before
rendering. Finally, we restore the default graphics card settings.
We are almost done, but we still need to update our pixel shader in effects.fx. First, declare two new
global constants to hold the new passed-in values: float xBBAlphaTestValue; bool xAlphaTestGreater;
Now we need to add a single line to our pixel shader, shown in yellow below:
169
BBPixelToFrame BillboardPS(BBVertexToPixel PSIn) : COLOR0 { BBPixelToFrame Output = (BBPixelToFrame)0; Output.Color = tex2D(textureSampler, PSIn.TexCoord); clip((Output.Color.a - xBBAlphaTestValue) * (xAlphaTestGreater ? 1 : -1)); return Output; }
This line uses the HLSL intrinsic function clip, which we have used before. The clip function discards
pixels for which the argument is zero. By subtracting xBBAlphaTestValue from the alpha value of the
current pixel, and multiplying the result by either 1 (if we want to keep pixels whose alpha value is
greater than xBBAlphaTestValue) , or -1 (if we want to keep pixels whose alpha value is less than or equal
to xBBAlphaTestValue), the clip function does exactly what we need.
Running this code should give you something like the right image above, but this time, the trees always
look good, no matter where the camera is positioned. How cool is that? That’s it for billboards, and in
fact for Lab 8. To those of you who stuck it out this far, congratulations! I hope you think it was time
well spent. If you have comments, or if you find errors, please contact me at [email protected]. Finally,
I want to again thank Riemer Grootjans for creating and sharing the XNA 3.1 on-line tutorials upon which
this lab was originally based.
Here is our final bucolic scene:
170
Optional: Two other things that you might want to do at this point:
1. Incorporate Lab 9 (which will measure your performance in frames per second), and
2. Use a much larger heightmap (e.g., heightmap5.png, available in the Lab8-Mono files), which
will have an interesting effect on performance.
Here is our final Game1.cs and effects.fx code (without the optional items above):
Game1.cs
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; namespace Lab8_Mono { /// <summary> /// This is the main type for your game. /// </summary> public class Game1 : Game {
171
#region Constants //Lighting Constants Vector3 LIGHT_DIRECTION = new Vector3(1.0f, -1.0f, 1.0f); //use (1.0f, -1.0f, 1.0f) to flip light public const float AMBIENT_LIGHT_LEVEL = 0.7f; // Camera control constants public const float ROTATION_SPEED = 0.3f; public const float MOVE_SPEED = 30.0f; // Terrain texture mapping constants public const float MAX_HT = 30.0f; public const float MIN_HT = 0.0f; public const float SAND_UPPER = 0.266f * MAX_HT; // 8 public const float GRASS_MID = 0.4f * MAX_HT; // 12 public const float GRASS_RANGE = 0.2f * MAX_HT; // 12 +/- 6 public const float ROCK_MID = 0.666f * MAX_HT; // 20 public const float ROCK_RANGE = 0.2f * MAX_HT; // 20 +/- 6 public const float SNOW_LOWER = 0.8f * MAX_HT; // 24 // Water Constants const float WATER_HEIGHT = 5.0f; public const float WAVE_LENGTH = 0.2f; public const float WAVE_HEIGHT = 0.3f; public Vector4 DULL_COLOR = new Vector4(0.3f, 0.4f, 0.5f, 1.0f); public const float WATER_DIRTINESS = 0.2f; // Increase to make the water "dirtier" public const float WIND_FORCE = 0.0005f; // It doesn't take much public Vector3 WIND_DIRECTION = new Vector3(0, 0, 1); //Cloud constants public const int CLOUD_SIZE = 32; //use 64 to make smaller clouds public const float OVERCAST_FACTOR = 1.2f; // Increase to make more cloudy public const float TIME_DIV = 4000.0f; // Increase to make clouds move more slowly public Vector4 SKY_TOP_COLOR = new Vector4(0.3f, 0.3f, 0.8f, 1); //Tree Constants public const int TREE_MIN_HT = 8; // Min terrain height for trees public const int TREE_MAX_HT = 14; // Max terrain height for trees public const int TREE_MAX_SLOPE = 15; // Max terrain slope on which to put trees public const int TREEMAP_HI_THOLD = 200; // Noise map value for HI_NUM_TREES trees public const int TREEMAP_MED_THOLD = 150; // Noise map value for MED_NUM_TREES trees public const int TREEMAP_LOW_THOLD = 100; // Noise map value for LOW_NUM_TREES trees public const int HI_NUM_TREES = 5; // Num trees at TREEMAP_HI_THOLD noise value public const int MED_NUM_TREES = 4; // Num trees at TREEMAP_MED_THOLD noise value public const int LOW_NUM_TREES = 3; // Num trees at TREEMAP_LOW_THOLD noise value public const float BB_ALPHA_TEST_VALUE = 0.6f; // useful values are .5 to .9 #endregion #region Vertex Structs public struct VertexPositionColorNormal {
172
public Vector3 Position; public Color Color; public Vector3 Normal; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Color, VertexElementUsage.Color, 0), new VertexElement(sizeof(float) * 3 + 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0) ); } #endregion #region Vertex Structs public struct VertexMultitextured { public Vector3 Position; public Vector3 Normal; public Vector4 TextureCoordinate; public Vector4 TexWeights; public readonly static VertexDeclaration vertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0), new VertexElement(sizeof(float) * 6, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 0), new VertexElement(sizeof(float) * 10, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1) ); } #endregion #region Game1 Instance Vars GraphicsDeviceManager graphics; GraphicsDevice device; Effect effect; VertexMultitextured[] vertices; int[] indices; Matrix viewMatrix; Matrix projectionMatrix; VertexBuffer myVertexBuffer; IndexBuffer myIndexBuffer; VertexBuffer waterVertexBuffer; private int terrainWidth; private int terrainLength;
173
private float[,] heightData; Matrix worldMatrix = Matrix.Identity; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity; Vector3 cameraPosition = new Vector3(130, 30, -50); float leftrightRot = MathHelper.PiOver2; float updownRot = -MathHelper.Pi / 10.0f; MouseState originalMouseState; Texture2D grassTexture; Texture2D sandTexture; Texture2D rockTexture; Texture2D snowTexture; Texture2D treeTexture; Texture2D cloudMap; Texture2D treeMap; Model skyDome; RenderTarget2D refractionRenderTarget; Texture2D refractionMap; RenderTarget2D reflectionRenderTarget; Texture2D reflectionMap; Matrix reflectionViewMatrix; Texture2D waterBumpMap; RenderTarget2D cloudsRenderTarget; Texture2D cloudStaticMap; VertexPositionTexture[] fullScreenVertices; VertexBuffer treeVertexBuffer; IndexBuffer treeIndexBuffer; List<Vector3> treeList; #endregion #region Class Game1 Constructor public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } #endregion #region Terrain Methods private void SetUpVertices() { vertices = new VertexMultitextured[terrainWidth * terrainLength];
174
for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainLength; y++) { vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y); vertices[x + y * terrainWidth].TextureCoordinate.X = (float)x / MAX_HT; vertices[x + y * terrainWidth].TextureCoordinate.Y = (float)y / MAX_HT; vertices[x + y * terrainWidth].TexWeights.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MIN_HT) / SAND_UPPER, 0, 1); vertices[x + y * terrainWidth].TexWeights.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - GRASS_MID) / GRASS_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - ROCK_MID) / ROCK_RANGE, 0, 1); vertices[x + y * terrainWidth].TexWeights.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y] - MAX_HT) / SNOW_LOWER, 0, 1); float total = vertices[x + y * terrainWidth].TexWeights.X; total += vertices[x + y * terrainWidth].TexWeights.Y; total += vertices[x + y * terrainWidth].TexWeights.Z; total += vertices[x + y * terrainWidth].TexWeights.W; vertices[x + y * terrainWidth].TexWeights.X /= total; vertices[x + y * terrainWidth].TexWeights.Y /= total; vertices[x + y * terrainWidth].TexWeights.Z /= total; vertices[x + y * terrainWidth].TexWeights.W /= total; } } } private void SetUpIndices() { indices = new int[(terrainWidth - 1) * (terrainLength - 1) * 6]; int counter = 0; for (int y = 0; y < terrainLength - 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()
175
{ 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 CopyToBuffers() { myVertexBuffer = new VertexBuffer(device, VertexMultitextured.vertexDeclaration, vertices.Length, BufferUsage.WriteOnly); myVertexBuffer.SetData(vertices); myIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly); myIndexBuffer.SetData(indices); } private void LoadHeightData(Texture2D heightMap) { terrainWidth = heightMap.Width; terrainLength = heightMap.Height; float minimumHeight = float.MaxValue; float maximumHeight = float.MinValue; Color[] heightMapColors = new Color[terrainWidth * terrainLength]; heightMap.GetData(heightMapColors); heightData = new float[terrainWidth, terrainLength]; for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++) { heightData[x, y] = heightMapColors[x + y * terrainWidth].R; if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y]; if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y]; } for (int x = 0; x < terrainWidth; x++) for (int y = 0; y < terrainLength; y++)
176
heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * MAX_HT; } private void DrawTerrain(Matrix currentViewMatrix) { LIGHT_DIRECTION.Normalize(); effect.Parameters["xLightDirection"].SetValue(LIGHT_DIRECTION); effect.Parameters["xAmbient"].SetValue(AMBIENT_LIGHT_LEVEL); effect.Parameters["xEnableLighting"].SetValue(true); effect.CurrentTechnique = effect.Techniques["MultiTextured"]; effect.Parameters["xTexture0"].SetValue(sandTexture); effect.Parameters["xTexture1"].SetValue(grassTexture); effect.Parameters["xTexture2"].SetValue(rockTexture); effect.Parameters["xTexture3"].SetValue(snowTexture); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xWorld"].SetValue(worldMatrix); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.Indices = myIndexBuffer; device.SetVertexBuffer(myVertexBuffer); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, indices.Length / 3); } } #endregion #region Texture Loads private void LoadTextures() { grassTexture = Content.Load<Texture2D>("grass"); sandTexture = Content.Load<Texture2D>("sand"); rockTexture = Content.Load<Texture2D>("rock"); snowTexture = Content.Load<Texture2D>("snow"); cloudMap = Content.Load<Texture2D>("cloudMap"); waterBumpMap = Content.Load<Texture2D>("waterbump"); cloudStaticMap = CreateStaticMap(CLOUD_SIZE); treeTexture = Content.Load<Texture2D>("tree"); treeMap = Content.Load<Texture2D>("treeMap"); } #endregion #region Camera Methods private void SetUpCamera() { UpdateViewMatrix(); projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
177
device.Viewport.AspectRatio, 0.3f, 1000.0f); } private void UpdateViewMatrix() { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation); viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector); Vector3 reflCameraPosition = cameraPosition; reflCameraPosition.Y = -cameraPosition.Y + WATER_HEIGHT * 2; Vector3 reflTargetPos = cameraFinalTarget; reflTargetPos.Y = -cameraFinalTarget.Y + WATER_HEIGHT * 2; Vector3 cameraRight = Vector3.Transform(new Vector3(1, 0, 0), cameraRotation); Vector3 invUpVector = Vector3.Cross(cameraRight, reflTargetPos - reflCameraPosition); reflectionViewMatrix = Matrix.CreateLookAt(reflCameraPosition, reflTargetPos, invUpVector); } private void ProcessInput(float amount) { MouseState currentMouseState = Mouse.GetState(); if (currentMouseState != originalMouseState) { float xDifference = currentMouseState.X - originalMouseState.X; float yDifference = currentMouseState.Y - originalMouseState.Y; leftrightRot -= ROTATION_SPEED * xDifference * amount; updownRot -= ROTATION_SPEED * yDifference * amount; Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); UpdateViewMatrix(); } Vector3 moveVector = new Vector3(0, 0, 0); KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W)) moveVector += new Vector3(0, 0, -1); if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S)) moveVector += new Vector3(0, 0, 1); if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D)) moveVector += new Vector3(1, 0, 0); if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A)) moveVector += new Vector3(-1, 0, 0);
178
if (keyState.IsKeyDown(Keys.Q)) moveVector += new Vector3(0, 1, 0); if (keyState.IsKeyDown(Keys.Z)) moveVector += new Vector3(0, -1, 0); AddToCameraPosition(moveVector * amount); } private void AddToCameraPosition(Vector3 vectorToAdd) { Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot); Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation); cameraPosition += MOVE_SPEED * rotatedVector; UpdateViewMatrix(); } #endregion #region Skydome Methods private void DrawSkyDome(Matrix currentViewMatrix) { GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead; Matrix[] modelTransforms = new Matrix[skyDome.Bones.Count]; skyDome.CopyAbsoluteBoneTransformsTo(modelTransforms); Matrix wMatrix = Matrix.CreateTranslation(0, -0.3f, 0) * Matrix.CreateScale(500) * Matrix.CreateTranslation(cameraPosition); foreach (ModelMesh mesh in skyDome.Meshes) { foreach (Effect currentEffect in mesh.Effects) { Matrix mworldMatrix = modelTransforms[mesh.ParentBone.Index] * wMatrix; //currentEffect.CurrentTechnique = currentEffect.Techniques["SimpleTextured"]; currentEffect.CurrentTechnique = currentEffect.Techniques["SkyDome"]; currentEffect.Parameters["xSkyTopColor"].SetValue(SKY_TOP_COLOR); currentEffect.Parameters["xAmbient"].SetValue(1.0f); currentEffect.Parameters["xWorld"].SetValue(mworldMatrix); currentEffect.Parameters["xView"].SetValue(currentViewMatrix); currentEffect.Parameters["xProjection"].SetValue(projectionMatrix); currentEffect.Parameters["xTexture"].SetValue(cloudMap); currentEffect.Parameters["xEnableLighting"].SetValue(false); } mesh.Draw(); } GraphicsDevice.DepthStencilState = DepthStencilState.Default; } private Texture2D CreateStaticMap(int resolution) { Random rand = new Random(); Color[] noisyColors = new Color[resolution * resolution]; for (int x = 0; x < resolution; x++) for (int y = 0; y < resolution; y++)
179
noisyColors[x + y * resolution] = new Color(new Vector3((float)rand.Next(1000) / 1000.0f, 0, 0)); Texture2D noiseImage = new Texture2D(device, resolution, resolution, true, SurfaceFormat.Color); noiseImage.SetData(noisyColors); return noiseImage; } private void SetUpFullscreenVertices() { VertexPositionTexture[] vertices = new VertexPositionTexture[4]; vertices[0] = new VertexPositionTexture(new Vector3(-1, 1, 0f), new Vector2(0, 1)); vertices[1] = new VertexPositionTexture(new Vector3(1, 1, 0f), new Vector2(1, 1)); vertices[2] = new VertexPositionTexture(new Vector3(-1, -1, 0f), new Vector2(0, 0)); vertices[3] = new VertexPositionTexture(new Vector3(1, -1, 0f), new Vector2(1, 0)); fullScreenVertices = vertices; } private void GeneratePerlinNoise(float time) { device.SetRenderTarget(cloudsRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); effect.CurrentTechnique = effect.Techniques["PerlinNoise"]; effect.Parameters["xTexture"].SetValue(cloudStaticMap); effect.Parameters["xOvercast"].SetValue(OVERCAST_FACTOR); effect.Parameters["xTime"].SetValue(time / TIME_DIV); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.DrawUserPrimitives(PrimitiveType.TriangleStrip, fullScreenVertices, 0, 2); } device.SetRenderTarget(null); cloudMap = cloudsRenderTarget; } #endregion #region Water Methods private Plane CreatePlane(float height, Vector3 planeNormalDirection, Matrix currentViewMatrix, bool clipSide) { planeNormalDirection.Normalize(); Vector4 planeCoeffs = new Vector4(planeNormalDirection, height); if (clipSide) planeCoeffs *= -1;
180
Matrix worldViewProjection = currentViewMatrix * projectionMatrix; Matrix inverseWorldViewProjection = Matrix.Invert(worldViewProjection); inverseWorldViewProjection = Matrix.Transpose(inverseWorldViewProjection); planeCoeffs = Vector4.Transform(planeCoeffs, inverseWorldViewProjection); Plane finalPlane = new Plane(planeCoeffs); return finalPlane; } private void DrawRefractionMap() { Plane refractionPlane = CreatePlane(WATER_HEIGHT + 1.5f, new Vector3(0, 1, 0), viewMatrix, false); effect.Parameters["ClipPlane0"].SetValue(new Vector4(refractionPlane.Normal, refractionPlane.D)); // Enable clipping for the purpose of creating a refraction map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(refractionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawTerrain(viewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); refractionMap = refractionRenderTarget;
// write out our refraction map to see what it looks like // DO NOT leave this code active /* using (Stream stream = File.OpenWrite("refractionmap.png")) { { refractionMap.SaveAsPng(stream, refractionMap.Width, refractionMap.Height); } stream.close(); } */ } private void DrawReflectionMap() { Plane reflectionPlane = CreatePlane(WATER_HEIGHT - 0.5f, new Vector3(0, -1, 0), reflectionViewMatrix, true); effect.Parameters["ClipPlane0"].SetValue(new Vector4(-reflectionPlane.Normal, -reflectionPlane.D)); // Enable clipping for the purpose of creating a reflection map effect.Parameters["Clipping"].SetValue(true); device.SetRenderTarget(reflectionRenderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);
181
DrawSkyDome(reflectionViewMatrix); DrawTrees(reflectionViewMatrix); DrawTerrain(reflectionViewMatrix); // Turn clipping off effect.Parameters["Clipping"].SetValue(false); device.SetRenderTarget(null); reflectionMap = reflectionRenderTarget;
// write out our reflection map to see what it looks like // DO NOT leave this code active /* using (Stream stream = File.OpenWrite("reflectionmap.png")) { { reflectionMap.SaveAsPng(stream, reflectionMap.Width, reflectionMap.Height); } stream.close(); } */ } private void SetUpWaterVertices() { VertexPositionTexture[] waterVertices = new VertexPositionTexture[6]; waterVertices[0] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[2] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertices[1] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, -terrainLength), new Vector2(0, 0)); waterVertices[3] = new VertexPositionTexture(new Vector3(0, WATER_HEIGHT, 0), new Vector2(0, 1)); waterVertices[5] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, 0), new Vector2(1, 1)); waterVertices[4] = new VertexPositionTexture(new Vector3(terrainWidth, WATER_HEIGHT, -terrainLength), new Vector2(1, 0)); waterVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, waterVertices.Length, BufferUsage.WriteOnly); waterVertexBuffer.SetData(waterVertices); } private void DrawWater(float time) { effect.CurrentTechnique = effect.Techniques["Water"]; Matrix worldMatrix = Matrix.Identity; effect.Parameters["xWorld"].SetValue(worldMatrix); effect.Parameters["xView"].SetValue(viewMatrix); effect.Parameters["xReflectionView"].SetValue(reflectionViewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xRefractionMap"].SetValue(refractionMap); effect.Parameters["xReflectionMap"].SetValue(reflectionMap); effect.Parameters["xWaterBumpMap"].SetValue(waterBumpMap); effect.Parameters["xWaveLength"].SetValue(WAVE_LENGTH);
182
effect.Parameters["xWaveHeight"].SetValue(WAVE_HEIGHT); effect.Parameters["xCamPos"].SetValue(cameraPosition); effect.Parameters["xDirtyWaterFactor"].SetValue(WATER_DIRTINESS); effect.Parameters["xDullColor"].SetValue(DULL_COLOR); effect.Parameters["xTime"].SetValue(time); effect.Parameters["xWindForce"].SetValue(WIND_FORCE); effect.Parameters["xWindDirection"].SetValue(WIND_DIRECTION); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); device.SetVertexBuffer(waterVertexBuffer); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2); } } #region Tree Methods private void CreateBBVertsAndIntsFromList() { VertexPositionTexture[] billboardVertices = new VertexPositionTexture[treeList.Count * 4]; int[] billboardIndices = new int[treeList.Count * 6]; int j = 0; int i = 0; foreach (Vector3 currentV3 in treeList) { // Create vertices billboardVertices[i + 0] = new VertexPositionTexture(currentV3, new Vector2(0, 0)); billboardVertices[i + 1] = new VertexPositionTexture(currentV3, new Vector2(0, 1)); billboardVertices[i + 2] = new VertexPositionTexture(currentV3, new Vector2(1, 1)); billboardVertices[i + 3] = new VertexPositionTexture(currentV3, new Vector2(1, 0)); //Create indices billboardIndices[j++] = i + 0; billboardIndices[j++] = i + 3; billboardIndices[j++] = i + 2; billboardIndices[j++] = i + 2; billboardIndices[j++] = i + 1; billboardIndices[j++] = i + 0; i += 4; } treeVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, billboardVertices.Length, BufferUsage.WriteOnly); treeVertexBuffer.SetData(billboardVertices); treeIndexBuffer = new IndexBuffer(device, IndexElementSize.ThirtyTwoBits, treeList.Count * 6, BufferUsage.WriteOnly); treeIndexBuffer.SetData(billboardIndices);
183
} private void DrawTrees(Matrix currentViewMatrix) { effect.CurrentTechnique = effect.Techniques["CylBillboard"]; effect.Parameters["xWorld"].SetValue(Matrix.Identity); effect.Parameters["xView"].SetValue(currentViewMatrix); effect.Parameters["xProjection"].SetValue(projectionMatrix); effect.Parameters["xCamPos"].SetValue(cameraPosition); effect.Parameters["xAllowedRotDir"].SetValue(new Vector3(0, 1, 0)); effect.Parameters["xBillboardTexture"].SetValue(treeTexture); effect.Parameters["xBBAlphaTestValue"].SetValue(BB_ALPHA_TEST_VALUE); int numVertices = treeList.Count * 4; int numTriangles = treeList.Count * 2; device.SetVertexBuffer(treeVertexBuffer); device.Indices = treeIndexBuffer; GraphicsDevice.BlendState = BlendState.AlphaBlend; // First draw the "solid" part of the tree GraphicsDevice.DepthStencilState = DepthStencilState.Default; effect.Parameters["xAlphaTestGreater"].SetValue(true); effect.CurrentTechnique.Passes[0].Apply(); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, numTriangles); // Now draw the rest of the tree GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead; effect.Parameters["xAlphaTestGreater"].SetValue(false); effect.CurrentTechnique.Passes[0].Apply(); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, numTriangles); // Reset graphics adapter to default state GraphicsDevice.BlendState = BlendState.Opaque; GraphicsDevice.DepthStencilState = DepthStencilState.Default; } private void GenerateTreePositions(VertexMultitextured[] terrainVertices) { Color[] treeMapColors = new Color[treeMap.Width * treeMap.Height]; treeMap.GetData(treeMapColors); int[,] noiseData = new int[treeMap.Width, treeMap.Height]; for (int x = 0; x < treeMap.Width; x++) for (int y = 0; y < treeMap.Height; y++) noiseData[x, y] = treeMapColors[y + x * treeMap.Height].R; treeList = new List<Vector3>(); Random random = new Random(); for (int x = 0; x < terrainWidth; x++) { for (int y = 0; y < terrainLength; y++) { float terrainHeight = heightData[x, y]; if ((terrainHeight > TREE_MIN_HT) && (terrainHeight < TREE_MAX_HT))
184
{ float flatness = Vector3.Dot(terrainVertices[x + y * terrainWidth].Normal, new Vector3(0, 1, 0)); float minFlatness = (float)Math.Cos(MathHelper.ToRadians(TREE_MAX_SLOPE)); if (flatness > minFlatness) { float relx = (float)x / (float)terrainWidth; float rely = (float)y / (float)terrainLength; float noiseValueAtCurrentPosition = noiseData[(int)(relx * treeMap.Width), (int)(rely * treeMap.Height)]; float treeDensity; if (noiseValueAtCurrentPosition > TREEMAP_HI_THOLD) treeDensity = HI_NUM_TREES; else if (noiseValueAtCurrentPosition > TREEMAP_MED_THOLD) treeDensity = MED_NUM_TREES; else if (noiseValueAtCurrentPosition > TREEMAP_LOW_THOLD) treeDensity = LOW_NUM_TREES; else treeDensity = 0; for (int currDetail = 0; currDetail < treeDensity; currDetail++) { float rand1 = (float)random.Next(1000) / 1000.0f; float rand2 = (float)random.Next(1000) / 1000.0f; Vector3 treePos = new Vector3((float)x - rand1, 0, -(float)y - rand2); treePos.Y = heightData[x, y]; treeList.Add(treePos); } } } } } } #endregion #endregion #region Initialize /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // JKB Note: This ordering and repetition of graphics profile commands addresses a
185
// current bug in MonoGame 3.7. If we don't do it this way the window defaults to // a (small) fixed size. Also, MonoGame says it defaults to Reach, but it doesn't, so // we have to set HiDef explicitily here in order to use 32-bit index buffers. graphics.GraphicsProfile = GraphicsProfile.HiDef; graphics.IsFullScreen = false; graphics.ApplyChanges(); graphics.PreferredBackBufferWidth = 800; graphics.PreferredBackBufferHeight = 800; graphics.ApplyChanges(); Window.Title = "Lab8_Mono - Terrain Tutorial II"; base.Initialize(); } #endregion #region LoadContent /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { device = graphics.GraphicsDevice; effect = Content.Load<Effect>("effects"); skyDome = Content.Load<Model>("dome"); skyDome.Meshes[0].MeshParts[0].Effect = effect.Clone(); Texture2D heightMap = Content.Load<Texture2D>("heightmap2"); LoadHeightData(heightMap); Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); originalMouseState = Mouse.GetState(); SetUpCamera(); SetUpVertices(); SetUpIndices(); CalculateNormals(); CopyToBuffers(); LoadTextures(); PresentationParameters pp = device.PresentationParameters; refractionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); reflectionRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat);
186
cloudsRenderTarget = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); SetUpWaterVertices(); GenerateTreePositions(vertices); CreateBBVertsAndIntsFromList(); SetUpFullscreenVertices(); } #endregion #region UnloadContent /// <summary> /// UnloadContent will be called once per game and is the place to unload /// game-specific content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } #endregion #region Update /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyState = Keyboard.GetState(); //Rotation if (keyState.IsKeyDown(Keys.PageUp)) { worldRotation = Matrix.CreateRotationY(0.01f); } else if (keyState.IsKeyDown(Keys.PageDown)) { worldRotation = Matrix.CreateRotationY(-0.01f); } else { worldRotation = Matrix.CreateRotationY(0); } float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f; ProcessInput(timeDifference);
187
worldMatrix *= worldTranslation * worldRotation; base.Update(gameTime); } #endregion #region Draw /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 100.0f; DrawRefractionMap(); DrawReflectionMap(); GeneratePerlinNoise(time); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0); DrawSkyDome(viewMatrix); DrawTerrain(viewMatrix); DrawWater(time); DrawTrees(viewMatrix); base.Draw(gameTime); } #endregion } }
effects.fx
//---------------------------------------------------- //-- This effect file derived from: -- //-- www.riemers.net -- //-- Basic shaders -- //-- -- //-- Modified for MonoGame by John K. Bennett -- //-- -- //-- Use/modify as you like -- //-- -- //---------------------------------------------------- struct VertexToPixel { float4 Position : POSITION; float4 Color : COLOR0; float LightingFactor: TEXCOORD0;
188
float2 TextureCoords: TEXCOORD1; }; struct PixelToFrame { float4 Color : COLOR0; }; //------- Constants -------- float4x4 xView; float4x4 xProjection; float4x4 xWorld; float3 xLightDirection; float xAmbient; bool xEnableLighting; bool xShowNormals; bool Clipping; float4 ClipPlane0; float4x4 xReflectionView; float xWaveLength; float xWaveHeight; float3 xCamPos; float xDirtyWaterFactor; float4 xDullColor; float xTime; float3 xWindDirection; float xWindForce; float xOvercast; float4 xSkyTopColor; float3 xAllowedRotDir; float xBBAlphaTestValue; bool xAlphaTestGreater; //------- Texture Samplers -------- Texture xTexture; sampler TextureSampler = sampler_state { texture = <xTexture>; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;}; Texture xTexture0; sampler TextureSampler0 = sampler_state { texture = <xTexture0>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture1; sampler TextureSampler1 = sampler_state { texture = <xTexture1>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = wrap; AddressV = wrap; }; Texture xTexture2; sampler TextureSampler2 = sampler_state { texture = <xTexture2>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xTexture3; sampler TextureSampler3 = sampler_state { texture = <xTexture3>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xRefractionMap;
189
sampler RefractionSampler = sampler_state { texture = <xRefractionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xReflectionMap; sampler ReflectionSampler = sampler_state { texture = <xReflectionMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xWaterBumpMap; sampler WaterBumpMapSampler = sampler_state { texture = <xWaterBumpMap>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; Texture xBillboardTexture; sampler textureSampler = sampler_state { texture = <xBillboardTexture>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = CLAMP; AddressV = CLAMP; }; //------- Technique: Pretransformed -------- VertexToPixel PretransformedVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; Output.Position = inPos; Output.Color = inColor; return Output; } PixelToFrame PretransformedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique Pretransformed { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 PretransformedVS(); PixelShader = compile ps_4_0_level_9_1 PretransformedPS(); } } //------- Technique: Colored -------- VertexToPixel ColoredVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld));
190
Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame ColoredPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Colored { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredVS(); PixelShader = compile ps_4_0_level_9_1 ColoredPS(); } } //------- Technique: ColoredNoShading -------- // No lighting or shading, so no normal info passed in VertexToPixel ColoredNoShadingVS( float4 inPos : POSITION, float4 inColor: COLOR) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Color = inColor; return Output; } PixelToFrame ColoredNoShadingPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = PSIn.Color; return Output; } technique ColoredNoShading { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 ColoredNoShadingVS(); PixelShader = compile ps_4_0_level_9_1 ColoredNoShadingPS(); } }
191
//------- Technique: Textured -------- VertexToPixel TexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul (xView, xProjection); float4x4 preWorldViewProjection = mul (xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; float3 Normal = (float3) normalize(mul(normalize(float4(inNormal, 0.0)), xWorld)); Output.LightingFactor = 1; if (xEnableLighting) Output.LightingFactor = dot(Normal, -xLightDirection); return Output; } PixelToFrame TexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient; return Output; } technique Textured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 TexturedVS(); PixelShader = compile ps_4_0_level_9_1 TexturedPS(); } }
//------- Technique: SimpleTextured -------- VertexToPixel SimpleTexturedVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD0) { VertexToPixel Output = (VertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; return Output; } PixelToFrame SimpleTexturedPS(VertexToPixel PSIn) { PixelToFrame Output = (PixelToFrame)0; Output.Color = tex2D(TextureSampler, PSIn.TextureCoords); Output.Color.rgb *= saturate(PSIn.LightingFactor) + xAmbient;
192
return Output; } technique SimpleTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 SimpleTexturedVS(); PixelShader = compile ps_4_0_level_9_1 SimpleTexturedPS(); } } //------- Technique: Multitextured -------- struct MTVertexToPixel { float4 Position : POSITION0; float4 Color : COLOR0; float3 Normal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 LightDirection : TEXCOORD2; float4 TextureWeights : TEXCOORD3; float Depth : TEXCOORD4; float4 clipDistances : TEXCOORD5; }; struct MTPixelToFrame { float4 Color : COLOR0; }; MTVertexToPixel MultiTexturedVS(float4 inPos : POSITION, float3 inNormal : NORMAL, float2 inTexCoords : TEXCOORD0, float4 inTexWeights : TEXCOORD1) { MTVertexToPixel Output = (MTVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.Normal = (float3) mul(normalize(float4(inNormal, 0.0)), xWorld); Output.TextureCoords = inTexCoords; Output.LightDirection.xyz = -xLightDirection; Output.LightDirection.w = 1; Output.TextureWeights = inTexWeights; Output.Depth = Output.Position.z / Output.Position.w; Output.clipDistances = dot(inPos, ClipPlane0); return Output; } MTPixelToFrame MultiTexturedPS(MTVertexToPixel PSIn) { MTPixelToFrame Output = (MTPixelToFrame)0; if (Clipping) clip(PSIn.clipDistances); float lightingFactor = 1; float blendDistance = 0.99f;
193
float blendWidth = 0.005f; float blendFactor = clamp((PSIn.Depth - blendDistance) / blendWidth, 0, 1); if (xEnableLighting) lightingFactor = saturate(saturate(dot(float4(PSIn.Normal, 0.0), PSIn.LightDirection)) + xAmbient); float4 farColor; farColor = tex2D(TextureSampler0, PSIn.TextureCoords)*PSIn.TextureWeights.x; farColor += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y; farColor += tex2D(TextureSampler2, PSIn.TextureCoords)*PSIn.TextureWeights.z; farColor += tex2D(TextureSampler3, PSIn.TextureCoords)*PSIn.TextureWeights.w; float4 nearColor; float2 nearTextureCoords = PSIn.TextureCoords * 3; nearColor = tex2D(TextureSampler0, nearTextureCoords)*PSIn.TextureWeights.x; nearColor += tex2D(TextureSampler1, nearTextureCoords)*PSIn.TextureWeights.y; nearColor += tex2D(TextureSampler2, nearTextureCoords)*PSIn.TextureWeights.z; nearColor += tex2D(TextureSampler3, nearTextureCoords)*PSIn.TextureWeights.w; Output.Color = lerp(nearColor, farColor, blendFactor); Output.Color *= lightingFactor; return Output; } technique MultiTextured { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 MultiTexturedVS(); PixelShader = compile ps_4_0_level_9_1 MultiTexturedPS(); } } //------- Technique: Water -------- struct WVertexToPixel { float4 Position : POSITION; float4 ReflectionMapSamplingPos : TEXCOORD1; float2 BumpMapSamplingPos : TEXCOORD2; float4 RefractionMapSamplingPos : TEXCOORD3; float4 Position3D : TEXCOORD4; }; struct WPixelToFrame { float4 Color : COLOR0; }; WVertexToPixel WaterVS(float4 inPos : POSITION, float2 inTex : TEXCOORD) { WVertexToPixel Output = (WVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection);
194
float4x4 preReflectionViewProjection = mul(xReflectionView, xProjection); float4x4 preWorldReflectionViewProjection = mul(xWorld, preReflectionViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.ReflectionMapSamplingPos = mul(inPos, preWorldReflectionViewProjection); Output.RefractionMapSamplingPos = mul(inPos, preWorldViewProjection); Output.Position3D = mul(inPos, xWorld); float3 windDir = normalize(xWindDirection); float3 perpDir = cross(xWindDirection, float3(0, 1, 0)); float ydot = dot(inTex, xWindDirection.xz); float xdot = dot(inTex, perpDir.xz); float2 moveVector = float2(xdot, ydot); moveVector.y += xTime * xWindForce; Output.BumpMapSamplingPos = moveVector / xWaveLength; Output.RefractionMapSamplingPos = mul(inPos, preWorldViewProjection); Output.Position3D = mul(inPos, xWorld); return Output; } WPixelToFrame WaterPS(WVertexToPixel PSIn) { WPixelToFrame Output = (WPixelToFrame)0; float4 bumpColor = tex2D(WaterBumpMapSampler, PSIn.BumpMapSamplingPos); float2 perturbation = xWaveHeight * (bumpColor.rg - 0.5f)*2.0f; float2 ProjectedTexCoords; ProjectedTexCoords.x = PSIn.ReflectionMapSamplingPos.x / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; ProjectedTexCoords.y = -PSIn.ReflectionMapSamplingPos.y / PSIn.ReflectionMapSamplingPos.w / 2.0f + 0.5f; float2 perturbatedTexCoords = ProjectedTexCoords + perturbation; float4 reflectiveColor = tex2D(ReflectionSampler, perturbatedTexCoords); float2 ProjectedRefrTexCoords; ProjectedRefrTexCoords.x = PSIn.RefractionMapSamplingPos.x / PSIn.RefractionMapSamplingPos.w / 2.0f + 0.5f; ProjectedRefrTexCoords.y = -PSIn.RefractionMapSamplingPos.y / PSIn.RefractionMapSamplingPos.w / 2.0f + 0.5f; float2 perturbatedRefrTexCoords = ProjectedRefrTexCoords + perturbation; float4 refractiveColor = tex2D(RefractionSampler, perturbatedRefrTexCoords); float3 eyeVector = (float3) normalize(float4(xCamPos,0.0) - PSIn.Position3D); float3 normalVector = (bumpColor.rbg - 0.5f)*2.0f; float fresnelTerm = dot(eyeVector, normalVector); float4 combinedColor = lerp(reflectiveColor, refractiveColor, fresnelTerm); Output.Color = lerp(combinedColor, xDullColor, xDirtyWaterFactor);
195
// add specular highlights float3 reflectionVector = -reflect(xLightDirection, normalVector); float specular = abs(dot(normalize(reflectionVector), normalize(eyeVector))); specular = pow(specular, 256); Output.Color.rgb += specular; return Output; } technique Water { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 WaterVS(); PixelShader = compile ps_4_0_level_9_1 WaterPS(); } } //------- Technique: PerlinNoise -------- struct PNVertexToPixel { float4 Position : POSITION; float2 TextureCoords : TEXCOORD0; }; struct PNPixelToFrame { float4 Color : COLOR0; }; PNVertexToPixel PerlinVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD) { PNVertexToPixel Output = (PNVertexToPixel)0; Output.Position = inPos; Output.TextureCoords = inTexCoords; return Output; }
PNPixelToFrame PerlinPS(PNVertexToPixel PSIn) { PNPixelToFrame Output = (PNPixelToFrame)0; float2 move = float2(0, 1); float4 perlin = tex2D(TextureSampler, (PSIn.TextureCoords) + xTime * move) / 2; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 2 + xTime * move) / 4; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 4 + xTime * move) / 8; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 8 + xTime * move) / 16; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 16 + xTime * move) / 32; perlin += tex2D(TextureSampler, (PSIn.TextureCoords) * 32 + xTime * move) / 32; Output.Color.rgb = 1.0f - pow(abs(perlin.r), xOvercast)*2.0f; Output.Color.a = 1; return Output; }
196
technique PerlinNoise { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 PerlinVS(); PixelShader = compile ps_4_0_level_9_1 PerlinPS(); } } //------- Technique: SkyDome -------- struct SDVertexToPixel { float4 Position : POSITION; float2 TextureCoords : TEXCOORD0; float4 ObjectPosition : TEXCOORD1; }; struct SDPixelToFrame { float4 Color : COLOR0; }; SDVertexToPixel SkyDomeVS(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD0) { SDVertexToPixel Output = (SDVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); Output.TextureCoords = inTexCoords; Output.ObjectPosition = inPos; return Output; } SDPixelToFrame SkyDomePS(SDVertexToPixel PSIn) { SDPixelToFrame Output = (SDPixelToFrame)0; float4 bottomColor = 1; float4 baseColor = lerp(bottomColor, xSkyTopColor, saturate((PSIn.ObjectPosition.y) / 0.4f)); float4 cloudValue = tex2D(TextureSampler, PSIn.TextureCoords).r; Output.Color = lerp(baseColor, 1, cloudValue); return Output; } technique SkyDome { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 SkyDomeVS(); PixelShader = compile ps_4_0_level_9_1 SkyDomePS(); } }
197
//------- Technique: CylBillboard -------- struct BBVertexToPixel { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; }; struct BBPixelToFrame { float4 Color : COLOR0; }; BBVertexToPixel CylBillboardVS(float3 inPos: POSITION0, float2 inTexCoord : TEXCOORD0) { BBVertexToPixel Output = (BBVertexToPixel)0; float3 center = (float3) mul(float4(inPos,0.0), xWorld); float3 eyeVector = center - xCamPos; float3 upVector = xAllowedRotDir; upVector = normalize(upVector); float3 sideVector = cross(eyeVector, upVector); sideVector = normalize(sideVector); float3 finalPosition = center; finalPosition += (inTexCoord.x - 0.5f)*sideVector; finalPosition += (1.5f - inTexCoord.y*1.5f)*upVector; float4 finalPosition4 = float4(finalPosition, 1); float4x4 preViewProjection = mul(xView, xProjection); Output.Position = mul(finalPosition4, preViewProjection); Output.TexCoord = inTexCoord; return Output; } BBPixelToFrame BillboardPS(BBVertexToPixel PSIn) { BBPixelToFrame Output = (BBPixelToFrame)0; Output.Color = tex2D(textureSampler, PSIn.TexCoord); clip((Output.Color.a - xBBAlphaTestValue) * (xAlphaTestGreater ? 1 : -1)); return Output; } technique CylBillboard { pass Pass0 { VertexShader = compile vs_4_0_level_9_1 CylBillboardVS(); PixelShader = compile ps_4_0_level_9_1 BillboardPS(); } }
198