Page view counter

Silverlight Tips of the Day - Blog by Mike Snow

Game Programming with Silverlight

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:

image 

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.

image

Step 2: Upper right vertex moved to the upper right but the image is  not transformed/skewed to fit change.

image

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.

image

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);
image

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

Comments

Microsoft Weblogs said:

For this tutorial we will make use of matrix transforms to skew and scale an image in a terrain tile

# August 18, 2008 3:24 PM

Silverlight news for August 19, 2008 said:

Pingback from  Silverlight news for August 19, 2008

# August 19, 2008 3:45 AM

Dew Drop - August 19, 2008 | Alvin Ashcraft's Morning Dew said:

Pingback from  Dew Drop - August 19, 2008 | Alvin Ashcraft's Morning Dew

# August 19, 2008 9:04 AM

Community Blogs said:

Martin Mihaylov on Resizing the SL Object, Dan Wahlin on the Grid and a Flyout StackPanel, Robby Ingebretsen

# August 19, 2008 5:17 PM

Silverlight Tips of the Day - Blog by Mike Snow said:

The purpose of this post is to create an outline summary all the blogs from my Silverlight tips of the

# January 2, 2009 5:56 PM

o UAU nosso de cada dia said:

essa lista eu copiei desse blog bárbaro (acompanhe por RSS você também): uma lista de dicas super úteis

# January 3, 2009 6:25 AM