måndag 8 april 2013

MultiTexturing XNA - 16 textures

What I've noticed with alot of programming tutorials are that they are very unclear. I've been working on a game project the last half year and some things have been a struggle. One of those things are "Multitexturing" and as XNA is a RAD environment for kickstarting a game that otherwise could take months to complete without a full team - it is a good choice, although the Framework has been deprecated. And due to unclearness I've yet to find a single tutorial that explains the "concept" to use more than 4 textures with a terrain.

However, I've been struggling with ideas that were perhaps overcomplicated to implement and no good information were out there for a simple solution to this problem and no guides had tried to tackle anymore than 4 textures (one for every RGBA = Red Green Blue Alpha color value).

So, as I had an idea on how to implement more than 4 textures I gave up and tried something really basic; to my surprise this worked. So for whoever has been like me, looking for a simple solution on how to texturize something with more than 4 textures for a while. This guide can come in handy.

First of, for those unclear on how a VertexDeclaration works:  it is a simple data structure that contains information on what data is passed on to the Pixel and Vertex Shader. So, to be able to draw a texture's color at Position 0,0,0 it needs to have a VertexDeclaration containing a Vector3 Position. Also, if the shape you're drawing needs colors to be drawn it will also contain that information. And perhaps a Normal and UV position for being able to use lightning and draw a texture at a certain position.

So the basics are, in this case you will be telling the Vertex/Pixel shader that you will be including the following data structures:


(Color) Color = What color are at what position
(Vector3) Position = The position to render the color
(Texture2) TextureCoordinates = The position in the texture to get the color

In this particular case, we need 4 color values. Each for 4 textures. So, Color1 = Texture 1 to Texture 4. Color 2 = Texture 5 = Texture 8 and so on until we have 16 textures. The reason we're only having 16 textures is because XNA only support 16 Texture Samplers. Otherwise, this method could perhaps be extended further but I've yet to do that in DirectX/OpenGL or anything else.


The VertexType is as following:

MultiTextureVertex
namespace GameEngine.Graphics
{
    public struct MultiTextureVertex : IVertexType
    {

        public MultiTextureVertex(Vector3 position, Vector2 textureCoordinate, uint texture)
        {
            Position = position;
            TextureCoordinate = textureCoordinate;

            A = new Color(0, 0, 0, 0);
            B = new Color(0, 0, 0, 0);
            C = new Color(0, 0, 0, 0);
            D = new Color(0, 0, 0, 0);

            switch (texture)
            {
                case 1:
                    A.R = 255;
                    break;
                case 2:
                    A.G = 255;
                    break;
                case 3:
                    A.B = 255;
                    break;
                case 4:
                    A.A = 255;
                    break;
                case 5:
                    B.R = 255;
                    break;
                case 6:
                    B.G = 255;
                    break;
                case 7:
                    B.B = 255;
                    break;
                case 8:
                    B.A = 255;
                    break;
                case 9:
                    C.R = 255;
                    break;
                case 10:
                    C.G = 255;
                    break;
                case 11:
                    C.B = 255;
                    break;
                case 12:
                    C.A = 255;
                    break;
                case 13:
                    D.R = 255;
                    break;
                case 14:
                    D.G = 255;
                    break;
                case 15:
                    D.B = 255;
                    break;
                case 16:
                    D.A = 255;
                    break;
            }
        }

        public MultiTextureVertex(Vector3 position, Vector2 textureCoordinate, Color a, Color b, Color c, Color d)
        {
            Position = position;
            TextureCoordinate = textureCoordinate;

            A = a;
            B = b;
            C = c;
            D = d;

        }

        public void SetTexture(uint texture)
        {
            A = new Color(0, 0, 0, 0);
            B = new Color(0, 0, 0, 0);
            C = new Color(0, 0, 0, 0);
            D = new Color(0, 0, 0, 0);

            switch (texture)
            {
                case 1:
                    A.R = 255;
                    break;
                case 2:
                    A.G = 255;
                    break;
                case 3:
                    A.B = 255;
                    break;
                case 4:
                    A.A = 255;
                    break;
                case 5:
                    B.R = 255;
                    break;
                case 6:
                    B.G = 255;
                    break;
                case 7:
                    B.B = 255;
                    break;
                case 8:
                    B.A = 255;
                    break;
                case 9:
                    C.R = 255;
                    break;
                case 10:
                    C.G = 255;
                    break;
                case 11:
                    C.B = 255;
                    break;
                case 12:
                    C.A = 255;
                    break;
                case 13:
                    D.R = 255;
                    break;
                case 14:
                    D.G = 255;
                    break;
                case 15:
                    D.B = 255;
                    break;
                case 16:
                    D.A = 255;
                    break;
            }
        }

        public Vector3 Position;
        public Vector2 TextureCoordinate;
        public Color A, B, C, D;

        public static readonly VertexDeclaration VertexDeclaration = new VertexDeclaration
        (
            new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
            new VertexElement((sizeof(float) * 3), VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0),
            new VertexElement((sizeof(float) * 5), VertexElementFormat.Color, VertexElementUsage.Color, 0),
            new VertexElement((sizeof(float) * 5) + (sizeof(byte) * 4), VertexElementFormat.Color, VertexElementUsage.Color, 1),
            new VertexElement((sizeof(float) * 5) + (sizeof(byte) * 8), VertexElementFormat.Color, VertexElementUsage.Color, 2),
            new VertexElement((sizeof(float) * 5) + (sizeof(byte) * 12), VertexElementFormat.Color, VertexElementUsage.Color, 3)

        );

        VertexDeclaration IVertexType.VertexDeclaration
        {
            get { return MultiTextureVertex.VertexDeclaration; }
        }
    }
}

Then by using this as my VertexDeclaration, it now contains 4 color values, a Texture coordinate (to map the texture correctly in the Shader) and a Position - where to draw the texture. I also have a shader file, an ".fx" file in XNA that is handling the multitexture. This contains the Pixel and Vertex shader that handles the MultiTexturing / Blending of textures.

HLSL Code
float4x4 View;
float4x4 Projection;
float4x4 World;

texture Texture1; sampler MultiTexture1 = sampler_state { Texture = (Texture1); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture2; sampler MultiTexture2 = sampler_state { Texture = (Texture2); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture3; sampler MultiTexture3 = sampler_state { Texture = (Texture3); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture4; sampler MultiTexture4 = sampler_state { Texture = (Texture4); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture5; sampler MultiTexture5 = sampler_state { Texture = (Texture5); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture6; sampler MultiTexture6 = sampler_state { Texture = (Texture6); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture7; sampler MultiTexture7 = sampler_state { Texture = (Texture7); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture8; sampler MultiTexture8 = sampler_state { Texture = (Texture8); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture9; sampler MultiTexture9 = sampler_state { Texture = (Texture9); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture10; sampler MultiTexture10 = sampler_state { Texture = (Texture10); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture11; sampler MultiTexture11 = sampler_state { Texture = (Texture11); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture12; sampler MultiTexture12 = sampler_state { Texture = (Texture12); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture13; sampler MultiTexture13 = sampler_state { Texture = (Texture13); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture14; sampler MultiTexture14 = sampler_state { Texture = (Texture14); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture15; sampler MultiTexture15 = sampler_state { Texture = (Texture15); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };
texture Texture16; sampler MultiTexture16 = sampler_state { Texture = (Texture16); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = wrap; AddressV = wrap; };

struct VS_INPUT
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
 float4 ColorA : COLOR0;
 float4 ColorB : COLOR1;
 float4 ColorC : COLOR2;
 float4 ColorD : COLOR3;
};

struct VS_OUTPUT
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
 float4 ColorA : COLOR0;
 float4 ColorB : COLOR1;
 float4 ColorC : COLOR2;
 float4 ColorD : COLOR3;
};

VS_OUTPUT MultiTexturing_Vertex(VS_INPUT input)
{
    VS_OUTPUT output;

    float4x4 vp = mul(mul(World, View), Projection);
    output.Position = mul(input.Position, vp);
    output.TexCoord = input.TexCoord;
 output.ColorA = input.ColorA;
 output.ColorB = input.ColorB;
 output.ColorC = input.ColorC;
 output.ColorD = input.ColorD;

    return output;
}

float4 MultiTexturing_Pixel(VS_OUTPUT input) : COLOR
{
 float4 color = float4(0,0,0,0);

 color += tex2D(MultiTexture1, input.TexCoord) * input.ColorA.r;
 color += tex2D(MultiTexture2, input.TexCoord) * input.ColorA.g;
 color += tex2D(MultiTexture3, input.TexCoord) * input.ColorA.b;
 color += tex2D(MultiTexture4, input.TexCoord) * input.ColorA.a;

 color += tex2D(MultiTexture5, input.TexCoord) * input.ColorB.r;
 color += tex2D(MultiTexture6, input.TexCoord) * input.ColorB.g;
 color += tex2D(MultiTexture7, input.TexCoord) * input.ColorB.b;
 color += tex2D(MultiTexture8, input.TexCoord) * input.ColorB.a;

 color += tex2D(MultiTexture9, input.TexCoord) * input.ColorC.r;
 color += tex2D(MultiTexture10, input.TexCoord) * input.ColorC.g;
 color += tex2D(MultiTexture11, input.TexCoord) * input.ColorC.b;
 color += tex2D(MultiTexture12, input.TexCoord) * input.ColorC.a;

 color += tex2D(MultiTexture13, input.TexCoord) * input.ColorD.r;
 color += tex2D(MultiTexture14, input.TexCoord) * input.ColorD.g;
 color += tex2D(MultiTexture15, input.TexCoord) * input.ColorD.b;
 color += tex2D(MultiTexture16, input.TexCoord) * input.ColorD.a;

 return color;
}

technique MultiTexture
{
    pass Pass0
    {
        VertexShader = compile vs_3_0 MultiTexturing_Vertex();
        PixelShader = compile ps_3_0 MultiTexturing_Pixel();
    }
}
So what happened? Well, this is the breakdown:

I supply the the Shader / Effect with my Camera's View, Projection and the World matrix containing any scaling / rotation on the entire entity. In my Game Engine I have components and entites, the Entity is Map and the Component is a TerrainComponent. If the Map entity's world is transformed in any way (scaled, rotated, moved) it will affect the shader this way. It will also affect every component it's handling. If the Map is transformed, so is the Terrain.

First of I calculate the position of where the color should be drawn by Multiplying the World against the View and then against the Projection. This way it will draw correctly so my camera can see it. Then I just pass on the rest of the values to the PixelShader.

The Pixel shader then samples part of the texture by using tex2D, it uses the TextureSampler - MultiTexture1 at the Coordinates of the Texture. So if the current position is 0,0 in the Texture it will take that "color" at that position from the texture, and multiply that with the color. If the texture at this location is texture 1, the Color value for this will be (255, 0, 0, 0) and it will 100% take that color. If a blending occurs between texture point 0,0 and 1,1 - right in the middle there will be a mix of the colors; it will take 50% of first texture and 50% of the second texture - unless the other corners of (say a plane) is two different textures completly. However, if a plane contains 4 points and 3 of those points have the same texture it will blend with the 4th corner of that plane, it will mix the right amount of colors from both textures and thus create a blending effect so that there's a smooth transition between the two textures, or any number of textures if that were the case.

Because although I only put whole 255, 0, 0, 0 like values for texture 1, 5, 9, 13 when we're looking at a different position as I stated earlier, like inbetween two positions - this "red" value might have decreased. It creates a kind of a gradient between the colors. Although if you're using Texture 1 and Texture 5 which both have a full 255, 0, 0, 0 (RED) value at those position it will still create a blending effect. This is what I though was weird, but weridly it worked. If I had rendered this with say only a VertexPositionColor Vertex those two corners would not blend, but instead look completely red. But in this case it works. Because at 0,0 it's Color1 = 255, 0, 0, 0 but at 1,1 Color1 = 0, 0, 0, 0 while Color 2 = 255, 0, 0, 0 and that way it works. This principle should work with more than 16 textures but as I stated earlier - XNA only supports a maximum of 16 textures.


Finally, for this to be a "complete" tutorial on this subject I also have to supply a sample where I create a terrain. The basics of my Terrain Component is as following, although it's been slimmed to a single method instead of an entire class.

TerrainComponent / Create Terrain (Reduced)
namespace GameEngine.Components
{

    // Rest of component reduced for smaller size

        public void CreateTerrain(uint TextureID, float Size, float Cell, out VertexBuffer vertexBuffer, out IndexBuffer indexBuffer)
        {
            if ((Size % Cell) != 0)
                throw new Exception("(Size % Spacing) must be zero");

            // VertexBUffer, IndexBuffer, Size, Cell, Vertices and Data are stored in the Component class
            //
            // This is stored in the class rather than this method
            // I create a dictionary of positions simply because of quick access unless I wanted to change the texture at say any position
            // Then I just access it with Data[Vector3.Zero] and change it. The int value contains the index of that position
            // In the Vertices array, then I simply update the VertexBuffer and I can redraw the entire terrain very quickly

            // Data = Dictionary with Key = Vector3, Value = int
            // But blogger keeps changing this to a HTML value

            float endX = (Size / 2);
            float endZ = (Size / 2);
            float startX = -(Size / 2);
            float startZ = -(Size / 2);

            int width = (int)((Math.Abs(endX - startX)) / Cell) + 1;
            int height = (int)((Math.Abs(endZ - startZ)) / Cell) + 1;

            Vector2 cellsize = new Vector2(Cell, Cell);

            // This is also in the "Component" class so I can alter those values
            MultiTextureVertex[] Vertices = new MultiTextureVertex[width * height];

            for (int z = 0; z < height; z++)
            {
                for (int x = 0; x < width; x++)
                {
                    Vector3 position = new Vector3(startX + (x * Cell), 0, startZ + (z * Cell));

                    Vertices[z * width + x] = new MultiTextureVertex(position, new Vector2(x, z) * cellsize, TextureID);
                    Data.Add(position, z * width + x);

                }
            }

            vertexBuffer = new VertexBuffer(GraphicsDevice, MultiTextureVertex.VertexDeclaration, Vertices.Length, BufferUsage.None);
            vertexBuffer.SetData(Vertices);

            int[] indexs = new int[((width - 1) * (height - 1) * 6)];

            int i = 0;

            for (int x = 0; x < width - 1; x++)
            {
                for (int z = 0; z < height - 1; z++)
                {
                    // 0, 1, 3, 3, 1, 2
                    int upperLeft = z * width + x;
                    int upperRight = upperLeft + 1;
                    int lowerLeft = upperLeft + width;
                    int lowerRight = lowerLeft + 1;

                    indexs[i] = upperLeft;
                    i++;

                    indexs[i] = upperRight;
                    i++;

                    indexs[i] = lowerLeft;
                    i++;

                    indexs[i] = lowerLeft;
                    i++;

                    indexs[i] = upperRight;
                    i++;

                    indexs[i] = lowerRight;
                    i++;
                }
            }

            indexBuffer = new IndexBuffer(GraphicsDevice, typeof(int), indexs.Length, BufferUsage.None);
            indexBuffer.SetData(indexs);
        }
}
As you can see in my comments I point out which data types are normally included with my Component and not just created during the method. I want to have access (at least if I have an editor) to the Positions and the Buffer so they can be altered and perhaps so that I can change the Texture at any Position later.

Finally the draw call on how to draw the Terrain / Set the textures:

TerrainComponent / Draw Terrain (Reduced)
namespace GameEngine
{
      // Reduced for size
 
      public void DrawTerrain(Effect Effect, IndexBuffer indexBuffer, VertexBuffer vertexBuffer, Matrix projection, Matrix view, Matrix world, params Texture2D[] textures)
        {
            try
            {
                Effect.CurrentTechnique = Effect.Techniques["MultiTexture"];

                for (int i = 0; i < textures.Length && i < 16; i++)
                {
                    Effect.Parameters["Texture" + (i + 1)].SetValue(textures[i]);
                }

                Effect.Parameters["Projection"].SetValue(projection);
                Effect.Parameters["View"].SetValue(view);
                Effect.Parameters["World"].SetValue(world);

                foreach (EffectPass pass in Effect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                }

                GraphicsDevice.Indices = indexBuffer;
                GraphicsDevice.SetVertexBuffer(vertexBuffer);
                GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertexBuffer.VertexCount, 0, indexBuffer.IndexCount / 3);

            }
            catch { }

        }
}
I hope that wraps up everything. If I missed anything or anything was unclear (If I were lazy) I'd be happy to revise and further explain. 

onsdag 3 april 2013

Lazy

This is a first post. Hope to get back here and write something useful that is not short, half-baked and overall lazy. This will be my only lazy post.