Silverlight Terrain Tutorial Part 2 – Using Transform Matrices to Create 3D Looking Terrain.
For this tutorial we will make use of matrix transforms to skew and scale an image in a terrain tile while moving the individual vertices of the terrain tiles. This will give the terrain a 3D look and feel. Make certain to read Part 1 of before reading this tutorial.
For a preview of the final app from this tutorial, see: http://silverlight.services.live.com/invoke/66033/Terrain%20Transform/iframe.html. Grab any terrain vertex (black dot) by holding down the left mouse button so that the vertex turns red. Move the mouse and you will see the vertex is also moved.
Here is an example of something you can make from this site above:
To illustrate what we are doing, take a look at these 3 steps. I have put some numbers on a grass tile so you can better see the expected result in skewing/scaling.
Step 1: One terrain tile before the vertices are changed. Notice the seam.
Step 2: Upper right vertex moved to the upper right but the image is not transformed/skewed to fit change.
Step 3: Matrix scale and skew applied to the terrain tile. Notice the seam is gone due to the scaling and the numbers follow the border on the top of the image due to the transform.
So how did we do this? First, let’s change the template we declared in Part 1 of these terrain tutorials. We are adding both a ScaleTransform and and a MatrixTransform to our Path. To our ImageBrush we are adding just a MatrixTransform. More on these soon.
private const string _imageTriangleContentTemplate
= "<ControlTemplate xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"" +
" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">" +
"<Canvas><Path Stroke=\"Blue\" x:Name=\"PathElementUpperLeft\" " +
" Width=\"100\" Height=\"100\"" +
" Data=\"M 0,0 100,0 0,100\">" +
" <Path.RenderTransform>" +
" <TransformGroup>" +
" <ScaleTransform x:Name=\"PathScaleUpperLeft\" ScaleX=\"1.01\" CenterX=\"25\" CenterY=\"25\" />" +
" <MatrixTransform x:Name=\"PathTransformUpperLeft\" />" +
" </TransformGroup>" +
" </Path.RenderTransform>" +
" <Path.Fill>" +
" <ImageBrush x:Name=\"TileImageUpperLeft\" " +
" AlignmentX=\"Left\" AlignmentY=\"Top\" Stretch=\"Fill\">" +
" <ImageBrush.RelativeTransform>" +
" <MatrixTransform x:Name=\"BrushTransformUpperLeft\"/>" +
" </ImageBrush.RelativeTransform>" +
" </ImageBrush>" +
" </Path.Fill>" +
"</Path>" +
"<Path x:Name=\"PathElementLowerRight\" " +
" Width=\"100\" Height=\"100\"" +
" Data=\"M 0,0 100,0 0,100\">" +
" <Path.RenderTransform>" +
" <TransformGroup>" +
" <ScaleTransform x:Name=\"PathScaleLowerRight\" ScaleX=\"1.01\" CenterX=\"25\" CenterY=\"25\" />" +
" <MatrixTransform x:Name=\"PathTransformLowerRight\" />" +
" </TransformGroup>" +
" </Path.RenderTransform>" +
" <Path.Fill>" +
" <ImageBrush x:Name=\"TileImageLowerRight\" " +
" AlignmentX=\"Left\" AlignmentY=\"Top\" Stretch=\"Fill\">" +
" <ImageBrush.RelativeTransform>" +
" <MatrixTransform x:Name=\"BrushTransformLowerRight\"/>" +
" </ImageBrush.RelativeTransform>" +
" </ImageBrush>" +
" </Path.Fill>" +
"</Path></Canvas>" +
"</ControlTemplate>";
In our OnApplyTemplate() function we can now get a references to these transform objects:
public override void OnApplyTemplate()
{
_pathScaleUpperLeft = (ScaleTransform)GetTemplateChild("PathScaleUpperLeft");
_pathTransformUpperLeft = (MatrixTransform)GetTemplateChild("PathTransformUpperLeft");
_brushTransformUpperLeft = (MatrixTransform)GetTemplateChild("BrushTransformUpperLeft");
_imageBrushUpperLeft = (ImageBrush)GetTemplateChild("TileImageUpperLeft");
_tilePathUpperLeft = (Path)GetTemplateChild("PathElementUpperLeft");
_pathScaleLowerRight = (ScaleTransform)GetTemplateChild("PathScaleLowerRight");
_pathTransformLowerRight = (MatrixTransform)GetTemplateChild("PathTransformLowerRight");
_brushTransformLowerRight = (MatrixTransform)GetTemplateChild("BrushTransformLowerRight");
_imageBrushLowerRight = (ImageBrush)GetTemplateChild("TileImageLowerRight");
_tilePathLowerRight = (Path)GetTemplateChild("PathElementLowerRight");
UpdateTextureCoordinates(new Point(0, 0), new Point(1, 0), new Point(0, 1), _brushTransformUpperLeft);
UpdateTextureCoordinates(new Point(1, 0), new Point(1, 1), new Point(0, 1), _brushTransformLowerRight);
}
Our references are declared as such:
private Path _tilePathUpperLeft;
private ScaleTransform _pathScaleUpperLeft;
private MatrixTransform _pathTransformUpperLeft;
private MatrixTransform _brushTransformUpperLeft;
private ImageBrush _imageBrushUpperLeft;
private ScaleTransform _pathScaleLowerRight;
private MatrixTransform _pathTransformLowerRight;
private MatrixTransform _brushTransformLowerRight;
private ImageBrush _imageBrushLowerRight;
private Path _tilePathLowerRight;
You will notice above in OnApplyTemplate() we call UpdateTextureCoordinates(). This is required to ensure the image maps in the right direction within the triangle. We pass our ImageBrush transform matrix to this function:
private void UpdateTextureCoordinates(Point p1, Point p2, Point p3, MatrixTransform transform)
{
Double m11 = (p2.X - p1.X);
Double m12 = (p2.Y - p1.Y);
Double m21 = (p3.X - p1.X);
Double m22 = (p3.Y - p1.Y);
Double ox = p1.X;
Double oy = p1.Y;
transform.Matrix = new Matrix(m11, m12, m21, m22, ox, oy).Invert(); ;
}
For the upper left triangle we would set the following texture coordinates:
- UpdateTextureCoordinates(new Point(0, 0), new Point(1, 0), new Point(0, 1), _brushTransformUpperLeft);
For the lower right triangle we would set the following texture coordinates:
- UpdateTextureCoordinates(new Point(1, 0), new Point(1, 1), new Point(0, 1), _brushTransformLowerRight);
Each triangle by default is set with coordinates 0,0, 100,0, 0,100 and a scale of 100x100. We then transform these coordinates to whatever coordinates we want using this scale. Anytime we want to change a triangle we call UpdateCorners() with the transform and scale matrices for that triangle. The first section of code performs the transformation on the new coordinates. The second part of the code scales the image slightly to get rid of the seam. I have found setting _antiSeamOverScale = 2 gets rid of the seam.
private void UpdateCorners(Point p1, Point p2, Point p3, MatrixTransform transform, ScaleTransform scale)
{
Double m11 = (p2.X - p1.X) * 0.01d;
Double m12 = (p2.Y - p1.Y) * 0.01d;
Double m21 = (p3.X - p1.X) * 0.01d;
Double m22 = (p3.Y - p1.Y) * 0.01d;
Double ox = p1.X;
Double oy = p1.Y;
transform.Matrix = new Matrix(m11, m12, m21, m22, ox, oy);
Rect rect = new Rect(p1, p2);
rect.Union(p3);
Double w = rect.Width, h = rect.Height;
scale.ScaleX = w > 0 ? (w + _antiSeamOverscale) / w : 0;
scale.ScaleY = h > 0 ? (h + _antiSeamOverscale) / h : 0;
}
With that, here is a complete loop to create a map of tiles.
public partial class Page : UserControl
{
TerrainTile[,] _tiles = new TerrainTile[10, 10];
public Page()
{
InitializeComponent();
for (int y = 0; y < 10; y++)
{
for (int x = 0; x < 10; x++)
{
TerrainTile tile = new TerrainTile();
tile.SetUpperLeftPoints(new Point(x * 100, y * 100), new Point(x * 100 + 100, y * 100), new Point(x * 100, y * 100 + 100));
tile.SetLowerRightPoints(new Point(x * 100 + 100, y * 100), new Point(x * 100 + 100, y * 100 + 100), new Point(x * 100, y * 100 + 100));
tile.SetImage("earth2.png");
MyCanvas.Children.Add(tile);
_tiles[y, x] = tile;
}
}
}
}
For more info on matrix transforms see: http://www.senocular.com/flash/tutorials/transformmatrix/
Other resources: http://www.pacem.it/CMerighi/Posts/71,en-US/Triangle_Texturing,_Theory_and_Silverlight_App.aspx
Special thanks to Florian Kruesch for answering some questions. See his sample here: http://www.codeproject.com/KB/silverlight/silverlight_triangle.aspx
Thank you,
--Mike Snow
Subscribe in a reader