Nested for-loop to draw 2d grid. Minus the empty tiles?

What we did in MonoGame.Extended for drawing tile maps is to ditch SpriteBatch and do our own rendering. The great thing and the problem with SpriteBatch is that it’s a priority queue of textured quads (two triangles) that gets streamed to the GPU from the CPU every game tick. This is a simple and powerful API for rendering textured quads. However, it’s not so great for performance to stream geometry that doesn’t change very often every game tick. If you search online, there are tons of tutorials on how to use vertex buffers and index buffers.

Another option is to draw the tilemap in a shader as a single Quad.
See: Shader+Example here
This is still not merger in my Master cause I wanted to add an importer to read .tmx and export the tilemap texture, I’ll see into it as soon as I get some free time.

I think I’m starting to get what you are saying…
I have done something like this in the past for 3d flight sim terrain, I think?..

you mean, for each tile that HAS image content, I would add a flat square geometry-piece to a collection, and then render those in a scene? -That way I wouldn’t have to worry about empty spaces and whether or not to draw each tile, like how 3d models you don’t have to account for empty space…

Maybe the computer can even handle culling for me, so it will only render relevant (in-frame) geometry, and not the whole layer?
-Like in my sim, it knew to only draw terrain FACING the player, and not the back-side of hills and mountains, and I just had to specify a max draw distance.

I realize my terminology is missing here, I’m just trying to grasp the concepts… Am I on to something here?

No, what we did is just upload all the triangles to the GPU once. Then we tell the GPU to render all these triangles from GPU memory every game tick.

oh, but in the case of a big map, I think that would be a problem, seeing as I have any number of layers of tiles on top of each other…?
There could be thousands or millions of tiles in a map… So I would need a way to cull the ones not in frame?

-Or is that the case for you too, and it just goes without saying…?

To answer your question we need to review some basics of how the GPU works. It’s too long to answer here. You are better off taking a dive into https://simonschreibt.de/gat/renderhell/

still cheaper to upload them to the gpu

You can use the offsets in DrawPrimitives to draw a bunch of line segments of tiles that are within the visible range just like you would with a for loop.

        public void DrawQuads(Rectangle TileRegionToDraw)
        {
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                SetBuffers();
                for (int y = TileRegionToDraw.Y; y < TileRegionToDraw.Bottom; y++)
                {
                    int i = y * mapWidth + TileRegionToDraw.X;
                    // Each tile represents a quad. There are 6 verts per quad and 2 triangle primitives per quad.
                    GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, i * 6, TileRegionToDraw.Width *2);
                }
            }
        }

Probably not the greatest example as far as the scrolling part or how to do it with layers. But i don’t have time to fix it up you might see how to do it from the example anyways.

If i get time ill make a proper example and post it.

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace HowToQuadDrawWithVertices
{
    public class Example04QuadCustomBufferedDraw : Game
    {
        public static GraphicsDeviceManager graphics;
        public static SpriteBatch spriteBatch;
        public static Effect effect;
        public static Texture2D generatedTexture;

        public static Vector3 camera2DScrollPosition = new Vector3(0, 0, -1);
        public static Vector3 camera2DScrollLookAt = new Vector3(0, 0, 0);
        public static float camera2dRotationZ = 0f;

        public int mapWidth = 300;
        public int mapHeight = 300;
        public int tileWidth = 64;
        public int tileHeight = 64;

        // for our buffer
        public List<VertexPositionColorTexture> vertexList = new List<VertexPositionColorTexture>();
        VertexBuffer vertexBuffer;

        public Example04QuadCustomBufferedDraw()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            graphics.GraphicsProfile = GraphicsProfile.HiDef;
            IsMouseVisible = true;
            Window.AllowUserResizing = true;
            graphics.PreferredBackBufferWidth = 800;
            graphics.PreferredBackBufferHeight = 500;
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            effect = Content.Load<Effect>("TestingEffect");
            generatedTexture = GenerateTexture2DWithTopLeftDiscoloration(tileWidth, tileHeight);
            CreateTileMap(mapWidth, mapHeight, tileWidth, tileHeight);
            CreateBuffers();
        }

        public Texture2D GenerateTexture2DWithTopLeftDiscoloration(int w, int h)
        {
            Texture2D t = new Texture2D(this.GraphicsDevice, w, h);
            Color[] cdata = new Color[w * h];
            for (int i = 0; i < w; i++)
            {
                for (int j = 0; j < h; j++)
                {
                    if (i < 20 && j < 20)
                        cdata[i * w + j] = new Color(120, 120, 120, 250);
                    else
                        cdata[i * w + j] = Color.White;
                }
            }
            t.SetData(cdata);
            return t;
        }

        public void CreateTileMap(int width, int height, int tileWidth, int tileHeight)
        {
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    CreateQuad(generatedTexture, new Rectangle(x * tileWidth, y * tileHeight, tileWidth, tileHeight), new Rectangle(0, 0, generatedTexture.Width, generatedTexture.Height));
                }
            }
        }

        protected override void UnloadContent()
        {
            generatedTexture.Dispose();
        }

        Vector2 pos = new Vector2();
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            if (Keyboard.GetState().IsKeyDown(Keys.Left))
                pos.X += 1f;
            if (Keyboard.GetState().IsKeyDown(Keys.Right))
                pos.X -= 1f;

            SetCameraPosition2D((int)pos.X, (int)pos.Y);

            base.Update(gameTime);
        }

        public void SetCameraPosition2D(int x, int y)
        {
            camera2DScrollPosition.X = x;
            camera2DScrollPosition.Y = y;
            camera2DScrollPosition.Z = -1;
            camera2DScrollLookAt.X = x;
            camera2DScrollLookAt.Y = y;
            camera2DScrollLookAt.Z = 0;
        }

        public void SetStates()
        {
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.SamplerStates[0] = SamplerState.LinearClamp;
            GraphicsDevice.RasterizerState = RasterizerState.CullClockwise;//RasterizerState.CullClockwise; //RasterizerState.CullCounterClockwise; // RasterizerState.CullNone;
        }

        public void SetUpEffect()
        {
            effect.CurrentTechnique = effect.Techniques["QuadDraw"]; //["BasicDrawing"];

            Viewport viewport = GraphicsDevice.Viewport;
            Vector3 cameraUp = Vector3.TransformNormal(new Vector3(0, -1, 0), Matrix.CreateRotationZ(camera2dRotationZ)) * 10f;
            Matrix World = Matrix.Identity;
            Matrix View = Matrix.CreateLookAt(camera2DScrollPosition, camera2DScrollLookAt, cameraUp);
            Matrix halfPixelOffset = Matrix.CreateTranslation(-0.5f, -0.5f, 0);
            Matrix Projection = Matrix.CreateScale(1, -1, 1) * Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, 1); // nans

            Matrix wvp = World * View * Projection;
            effect.Parameters["World"].SetValue(World);
            effect.Parameters["View"].SetValue(View);
            effect.Parameters["Projection"].SetValue(Projection);
            effect.Parameters["TextureA"].SetValue(generatedTexture);
        }


        protected override void Draw(GameTime gameTime)
        {
            this.GraphicsDevice.Clear(Color.Beige );

            // were are we on the tile map and how many tiles do we draw.
            var r = new Rectangle(3, 5, 15, 15);

            // we view the above position in pixels at.
            var rp = r.Location * new Point(tileWidth, tileHeight);
            SetCameraPosition2D(rp.X, rp.Y);

            // set states and effect up.
            SetStates();
            SetUpEffect();

            // draw tiles that are valid.
            DrawQuads(r);

            //DrawQuads(); // just draw everything.

            base.Draw(gameTime);
        }

        public void CreateBuffers()
        {
            var quads = vertexList.ToArray();
            vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColorTexture), quads.Length, BufferUsage.WriteOnly);
            vertexBuffer.SetData(quads);
        }

        private void CreateQuad(Texture2D texture, Rectangle destination, Rectangle source)
        {
            Vector2 tl = source.Location.ToVector2() / texture.Bounds.Size.ToVector2();
            Vector2 tr = new Vector2(source.Right, source.Top) / texture.Bounds.Size.ToVector2();
            Vector2 br = new Vector2(source.Right, source.Bottom) / texture.Bounds.Size.ToVector2();
            Vector2 bl = new Vector2(source.Left, source.Bottom) / texture.Bounds.Size.ToVector2();
            // t1
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Left, destination.Top, 0f), Color.White, tl));
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Left, destination.Bottom, 0f), Color.Red, bl));
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Right, destination.Bottom, 0f), Color.Green, br));
            // t2
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Right, destination.Bottom, 0f), Color.Green, br));
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Right, destination.Top, 0f), Color.Blue, tr));
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Left, destination.Top, 0f), Color.White, tl));
        }

        public void SetBuffers()
        {
            GraphicsDevice.SetVertexBuffer(vertexBuffer);
        }

        public void DrawQuads()
        {
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                SetBuffers();
                GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, vertexList.Count / 3);
            }
        }

        public void DrawQuads(Rectangle TileRegionToDraw)
        {
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                SetBuffers();
                for (int y = TileRegionToDraw.Y; y < TileRegionToDraw.Bottom; y++)
                {
                    int i = y * mapWidth + TileRegionToDraw.X;
                    // Each tile represents a quad. There are 6 verts per quad and 2 triangle primitives per quad.
                    GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, i * 6, TileRegionToDraw.Width *2);
                }
            }
        }

    }
}

You can draw layers over top the other ones of course. As well as alter the index further to be 3 dimensional for layers in the draw by

index or i = (y* mapWidth + x) + layer * (mapWidth* mapHeight);
Then when you make the map just add more layers to the map.

shader.

// TestingEffect.fx
#if OPENGL
#define SV_POSITION POSITION
#define VS_SHADERMODEL vs_3_0
#define PS_SHADERMODEL ps_3_0
#else
#define VS_SHADERMODEL vs_4_0     //_level_9_1
#define PS_SHADERMODEL ps_4_0     //_level_9_1
#endif

matrix World;
matrix View;
matrix Projection;

Texture2D TextureA; // primary texture.
sampler TextureSamplerA = sampler_state
{
    texture = <TextureA>;
    //magfilter = LINEAR; //minfilter = LINEAR; //mipfilter = LINEAR; //AddressU = mirror; //AddressV = mirror; 
};
//_______________________________________________________________
// techniques 
// Quad Draw  Position Color Texture
//_______________________________________________________________
struct VsInputQuad
{
    float4 Position : POSITION0;
    float4 Color : COLOR0;
    float2 TexureCoordinateA : TEXCOORD0;
};
struct VsOutputQuad
{
    float4 Position : SV_Position;
    float4 Color : COLOR0;
    float2 TexureCoordinateA : TEXCOORD0;
};
struct PsOutputQuad
{
    float4 Color : COLOR0;
};
// ____________________________
VsOutputQuad VertexShaderQuadDraw(VsInputQuad input)
{
    VsOutputQuad output;
    float4x4 vp = mul(View, Projection);
    float4x4 wvp = mul(World, vp);
    output.Position = mul(input.Position, wvp); // Transform by WorldViewProjection
    output.Color = input.Color;
    output.TexureCoordinateA = input.TexureCoordinateA;
    return output;
}
PsOutputQuad PixelShaderQuadDraw(VsOutputQuad input)
{
    PsOutputQuad output;
    output.Color = tex2D(TextureSamplerA, input.TexureCoordinateA) * input.Color;
    return output;
}

technique QuadDraw
{
    pass
    {
        VertexShader = compile VS_SHADERMODEL VertexShaderQuadDraw();
        PixelShader = compile PS_SHADERMODEL PixelShaderQuadDraw();
    }
}

Output.

1 Like

Alright, lots to absorb here.

So you can add any amount of quads to represent tiles, and it only draws inside whatever bounds I set?..
And for gaps in the tilemap, you just don’t add quads, and there would be a “hole” at those coords?

I am trying to decrease the cost of drawing many tiles. Checking each tile for a draw-flag and drawing sprites for each one is slow at high numbers…

What you proposed would be much faster, with no flag checks, is that correct?

And what about map size limitations? How will that be effected? I don’t know this, but I get a feeling like I am pushing more onto v-ram, and I of coarse have much more normal ram to work with.

you just don’t add quads, and there would be a “hole” at those coords?

No you do then yes there will still be a hole.
While you could do it that way by not adding tiles in some spots your world map would end up disjointed.
This example is a bit better as it compromises with blank tiles the shader can remove as well as only drawing a region of the entire map…

What you proposed would be much faster, with no flag checks, is that correct?

Yes

The area you draw is the area out of your entire map you wish to draw as a rectangle of it i.e that portion that is on screen.

For empty tiles within that area you can make it more complex to skip them however that means more draw calls which even though you wont be drawing a tile here and there still means doing more overall cpu to gpu accesses and it also would mean flaging your map on the cpu which will be slow in both respects.

So its cheaper to compromise in the following ways.
Do not draw the entire map on the gpu that would be a waste just to clip 90% of the entire map.
Do draw the rectangular areas of your tilemap that will be on your screen or visible using gpu memory.
Do not skip empty tiles in these regions but… instead add empty tiles within that area on your entire map that can be ignored by the gpu itself … set to them a (fully transparent tile) that can be alpha blended out completely or cliped out on the shader. Which means they are effectively not drawn either way and don’t affect other layers at all.

In other words have a blank tile in your spritesheet a tile that has a rgb alpha value for all its pixels set to zero.

And what about map size limitations? How will that be effected?

Maximum vertices the gpu can hold / by 6 will be equal to the maximum map tiles.
The maximum map tiles in this context will be equal to [mapWidth * mapHeight] * layers.

I think for reach dx 9 that was 65,000 vertices at worst give or take so bout 10,800 tiles… but pretty sure with hi def set on its much higher.

You can alternately make a separate set of buffers per layer with a different spritesheet i believe that would also be satisfactory. Or just change the texture per layer draw if needed.
Also be aware this is a example as such there is a lot more optimizing you could do for example the projection matrix needs only be set once ect… only the drawing part is emphasized here.

Edit
I should have also mention you need to add to the pixel shader clip(myFinalOutputColor.a - 0.02f) some small value.
or
Use a transparent tile and change the opaque blendstate in the below line, sorry.

GraphicsDevice.BlendState = BlendState.Opaque; // change that line opaque to alpha blend.

@willmotil Your example is having one draw call per quad; this is going to kill performance with DX9 era MonoGame. The whole idea of SpriteBatch was to batch draw calls.

@nkast Drawing the whole tiled map as one quad is one option we looked at for MonoGame.Extended but people were hitting GPU memory limits for large maps because the entire map is stored as pixel information.

@Mando_Palaise Take a look at the work we did for Tiled maps in MonoGame.Extended for an idea of what I’m talking about: https://github.com/craftworkgames/MonoGame.Extended/blob/develop/Source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs

Your example is having one draw call per quad; this is going to kill performance with DX9 era MonoGame. The whole idea of SpriteBatch was to batch draw calls.

It is batched and culling by drawing a row of tiles at once and skipping thru the 1d array as it moves down a column so moving down is a draw but a full row is drawn all at once and culled on the sides before it is sent to the gpu.

I cleaned up the example a bit more and altered it so it can draw layers so basically 9 layers all alpha blended together + background, So bout 2 and a half to 3k tiles per frame at 400+ fps seems ok. Most people wont use 9 alpha blended full map layers for a map, but the trick here is that every tileset has a blank tile in it.

Id say just use the monogame extended or nkasts but i think he is trying to learn the basics to roll his own.

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace HowToTileMap
{
    /// <summary>
    /// GL desktop project. has minor differences in positioning from dx.
    /// </summary>
    public class Example05TileLayeredCustomBufferedDraw : Game
    {
        public const int VERTICES_PER_QUAD = 6;
        public const int TRIANGLES_PER_QUAD = 2;

        SpriteFont font;
        public static GraphicsDeviceManager graphics;
        public static SpriteBatch spriteBatch;
        public static Effect effect;
        Matrix Projection;

        public List<Texture2D> layerTextures = new List<Texture2D>();

        public Texture2D t2d_height_mountain_mono;
        public int mountainSourceTileWidth;
        public int mountainSourceTileHeight;

        public Texture2D t2d_rivers;
        public int riverSourceTileWidth;
        public int riverSourceTileHeight;

        Vector2 mapViewedScrollPos = new Vector2();
        private Vector3 camera2DScrollPosition = new Vector3(0, 0, -1);
        private Vector3 camera2DScrollLookAt = new Vector3(0, 0, 0);
        public float camera2dRotationZ = 0f;

        public int mapWidth = 500;
        public int mapHeight = 30;
        public int tileSourceWidth;
        public int tileSourceHeight;
        public int tileDestinationWidth = 64;
        public int tileDestinationHeight = 64;
        public Point tilesThatFitToScreenSize; 

        // for our buffer
        public List<VertexPositionColorTexture> vertexList = new List<VertexPositionColorTexture>();
        VertexBuffer vertexBuffer;

        public Example05TileLayeredCustomBufferedDraw()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            graphics.GraphicsProfile = GraphicsProfile.HiDef;
            IsMouseVisible = true;
            Window.AllowUserResizing = true;
            graphics.PreferredBackBufferWidth = 1200;
            graphics.PreferredBackBufferHeight = 900;
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // layer 0 tiles
            t2d_height_mountain_mono = Content.Load<Texture2D>("height_mountain_mono");
            mountainSourceTileWidth = t2d_height_mountain_mono.Width / 8;
            mountainSourceTileHeight = t2d_height_mountain_mono.Height / 8;

            // layer 1 tiles
            t2d_rivers = Content.Load<Texture2D>("rivers");
            riverSourceTileWidth = t2d_rivers.Width / 8;
            riverSourceTileHeight = t2d_rivers.Height / 8;

            // We set the default destination tile size to something constant as the source texture tile dimensions may change but the destination we want to stay the same.
            // its better if the destination is larger then the source to prevent downsampling linear or if mip mapping is used.
            // If it is smaller and you are not mip mapping use point instead of linear for the sampler state when you set states. 
            tileDestinationWidth = riverSourceTileWidth;
            tileDestinationHeight = riverSourceTileWidth;

            // this is dependant on the initial size of the screen at start up and is tied to the create of the vertices in the buffer so im not really handling resizing well here just enough.
            tilesThatFitToScreenSize = new Point(GraphicsDevice.Viewport.Width / tileDestinationWidth + 2, GraphicsDevice.Viewport.Height / tileDestinationHeight + 2);

            // ok so basically bunch of sheets bunch of tiles.

            // Set layer 0 mountain to the primitive array.
            CreateTileMapFromSheet(0, t2d_height_mountain_mono, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, mountainSourceTileWidth, mountainSourceTileHeight);
            // Set layer 1 rivers to the primitive array.
            CreateTileMapFromSheet(1, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
            // Set layer 2 rivers to the primitive array.
            CreateTileMapFromSheet(2, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
            // Set layer 3 rivers to the primitive array.
            CreateTileMapFromSheet(3, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
            // Set layer 4 rivers to the primitive array.
            CreateTileMapFromSheet(4, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
            // Set layer 5 rivers to the primitive array.
            CreateTileMapFromSheet(5, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
            // Set layer 6 rivers to the primitive array.
            CreateTileMapFromSheet(6, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
            // Set layer 7 rivers to the primitive array.
            CreateTileMapFromSheet(7, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
            // Set layer 8 rivers to the primitive array.
            CreateTileMapFromSheet(8, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);

            // so this is 9 layers and the clear color.

            CreateBuffers();
            SetBuffers();

            effect = Content.Load<Effect>("TestingEffect");
            effect.CurrentTechnique = effect.Techniques["QuadDraw"];
            SetProjectionMatrix();
            SetTexture(t2d_height_mountain_mono);
        }

        public void CreateTileMapFromSheet(int seed, Texture2D texture ,int mapWidth, int mapHeight, int tileDestinationWidth, int tileDestinationHeight, int tileWidth, int tileHeight)
        {
            // put the textures into a layer list to keep things orderly later in the draw loop.
            layerTextures.Add(texture);

            // we pass in a seed to randomize the map im not going to design a actual map for the example.
            Random rnd = new Random(seed);
            for (int y = 0; y < mapHeight; y++)
            {
                for (int x = 0; x < mapWidth; x++)
                {
                    var rx = rnd.Next(0, 8);
                    var ry = rnd.Next(0, 8);
                    var source = new Rectangle(rx * tileWidth, ry * tileHeight, tileWidth, tileHeight);
                    var destination = new Rectangle(x * tileDestinationWidth, y * tileDestinationHeight, tileDestinationWidth, tileDestinationHeight);
                    CreateQuad(texture, destination, source);
                }
            }
        }

        private void CreateQuad(Texture2D texture, Rectangle destination, Rectangle source)
        {
            Vector2 tl = source.Location.ToVector2() / texture.Bounds.Size.ToVector2();
            Vector2 tr = new Vector2(source.Right, source.Top) / texture.Bounds.Size.ToVector2();
            Vector2 br = new Vector2(source.Right, source.Bottom) / texture.Bounds.Size.ToVector2();
            Vector2 bl = new Vector2(source.Left, source.Bottom) / texture.Bounds.Size.ToVector2();
            // t1
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Left, destination.Top, 0f), Color.White, tl));
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Left, destination.Bottom, 0f), Color.White, bl));
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Right, destination.Bottom, 0f), Color.White, br));
            // t2
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Right, destination.Bottom, 0f), Color.White, br));
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Right, destination.Top, 0f), Color.White, tr));
            vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Left, destination.Top, 0f), Color.White, tl));
        }

        public void CreateBuffers()
        {
            var quads = vertexList.ToArray();
            vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColorTexture), quads.Length, BufferUsage.WriteOnly);
            vertexBuffer.SetData(quads);
        }

        public void SetCameraTileMapPixelPosition2D(Vector2 tilePosition, int currentTileMapDestinationWidth, int currentTileMapDestinationHeight)
        {
            var x = tilePosition.X * (float)currentTileMapDestinationWidth;
            var y = tilePosition.Y * (float)currentTileMapDestinationHeight;
            SetCameraPixelPosition2D( x, y);
        }
        private void SetCameraPixelPosition2D(float x, float y)
        {
            camera2DScrollPosition.X = x;
            camera2DScrollPosition.Y = y;
            camera2DScrollPosition.Z = -1;
            camera2DScrollLookAt.X = x;
            camera2DScrollLookAt.Y = y;
            camera2DScrollLookAt.Z = 0;
        }

        public void SetStates()
        {
            GraphicsDevice.BlendState = BlendState.AlphaBlend; // AlphaBlend Opaque NonPremultiplied
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
            GraphicsDevice.RasterizerState = RasterizerState.CullClockwise;
        }

        public void SetUpEffect()
        {
            //effect.CurrentTechnique = effect.Techniques["QuadDraw"];
            Viewport viewport = GraphicsDevice.Viewport;
            Vector3 cameraUp = Vector3.TransformNormal(new Vector3(0, -1, 0), Matrix.CreateRotationZ(camera2dRotationZ)) * 10f;
            Matrix World = Matrix.Identity;
            Matrix View = Matrix.CreateLookAt(camera2DScrollPosition, camera2DScrollLookAt, cameraUp);

            effect.Parameters["World"].SetValue(World);
            effect.Parameters["View"].SetValue(View);
        }

        public void SetProjectionMatrix()
        {
            Viewport viewport = GraphicsDevice.Viewport;
            Projection = Matrix.CreateScale(1, -1, 1) * Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, 1);
            effect.Parameters["Projection"].SetValue(Projection);
        }

        public void SetTexture(Texture2D texture)
        {
            effect.Parameters["TextureA"].SetValue(texture);
        }

        public void SetBuffers()
        {
            GraphicsDevice.SetVertexBuffer(vertexBuffer);
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            float scrollSpeedInTilesPerSecond = 10 * elapsed; // ten tiles per second so its kinda fast.

            if (Keyboard.GetState().IsKeyDown(Keys.Left))
                mapViewedScrollPos.X -= scrollSpeedInTilesPerSecond; 
            if (Keyboard.GetState().IsKeyDown(Keys.Right))
                mapViewedScrollPos.X += scrollSpeedInTilesPerSecond;
            if (Keyboard.GetState().IsKeyDown(Keys.Up))
                mapViewedScrollPos.Y -= scrollSpeedInTilesPerSecond;
            if (Keyboard.GetState().IsKeyDown(Keys.Down))
                mapViewedScrollPos.Y += scrollSpeedInTilesPerSecond;

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear( Color.CornflowerBlue  );

            // We view the current scrolled position in pixels at.
            SetCameraTileMapPixelPosition2D(mapViewedScrollPos, tileDestinationWidth, tileDestinationHeight);

            // Were are we on the tile map and how many tiles do we draw in relation to the scrolled world position.
            var mapVisibleDrawRect = new Rectangle((int)mapViewedScrollPos.X, (int)mapViewedScrollPos.Y, tilesThatFitToScreenSize.X, tilesThatFitToScreenSize.Y);

            // set states and effect up.
            SetStates();
            SetUpEffect();

            // Draw all tiles that are valid from start to end layers.
            DrawTileMap(mapVisibleDrawRect, 0 , layerTextures.Count);

            base.Draw(gameTime);
        }

        public void DrawTileMap(Rectangle TileRegionToDraw, int StartingLayer, int LayerLength)
        {
            // The total map tiles
            int mapTilesLength = mapWidth * mapHeight;

            // Ensure the buffer is set we could have different buffers for each layer but instead i put it all in one buffer.
            SetBuffers();

            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                // Loop thru the layers we are going to draw.
                for (int layer = StartingLayer; layer < (StartingLayer + LayerLength); layer++)
                {
                    // Set the texture that this layer uses on the card.
                    SetTexture(layerTextures[layer]);

                    // Call apply the set the texture.
                    pass.Apply();

                    // Calculate the layer offset.
                    var layerVerticeOffset = mapTilesLength * layer * VERTICES_PER_QUAD;

                    // Increment down a column as we draw each row of tiles that is in view.
                    for (int y = TileRegionToDraw.Y; y < TileRegionToDraw.Bottom; y++)
                    {
                        // Calculate the starting index position for this row of tiles at the targeted layer within the 1 dimensional triangle buffer and the span of primitives to draw.
                        int startingVerticeIndex = ((y * mapWidth + TileRegionToDraw.X)  * VERTICES_PER_QUAD) + layerVerticeOffset;
                        int primitiveSpanLength = TileRegionToDraw.Width * TRIANGLES_PER_QUAD;
                        // Tell the gpu to execute the drawing of this visbile row span of tiles in the shader.
                        GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, startingVerticeIndex, primitiveSpanLength);
                    }
                }
            }
        }
    }
}

same shader as above.

Images used.


1 Like

I think you misunderstood. I assume you are talking about baking the entire map in a big texture.
In my approach I save the tilemap as a texture, one pixel (4 bytes) per tile. The shader does the lookup into the tileAtlas.
That’s taking less space than rendering a VertexBuffer/IndexBuffer where you allocate 4 VertexPositionTexture (4*20 bytes) per tile.

@willmotil Very nice. This is pretty much what I had in mind originally.

@nkast Interesting. Yes I misunderstood at first glance; it’s just a little hard to parse with all the verbose stuff you have in there for DirectX. I would appreciate cleaner example code if you have some.

This is pretty much how I feel about everything you guys are talking about. :slight_smile:

I am pretty good at making things work with “real world” relatable concepts like grids, 2d maps, sprites, and trigonometry, but as soon as I hear quads, API’s, directX, I hit a wall… These things don’t have parallels to the real world, and are harder to grasp… I have used some “VertexPositionColor” to draw dynamic shadows on my lights, but that is very simple and limited in scope, it has very few parts. -Also, the concepts here are right out of real life.

I guess I would have a lot of reading to do before I can even understand most of what you guys are trying to tell me… It’s just above my head.

I have an artist working with me though, so I don’t have the luxury of unlimited research / dev time…
So we talked it over, and I did a quick test of my own original suggestion, which took only hours to code, and it actually works very well for what I intended. -So that is what we are running with this time around. I think our game will be more than adequately smooth, it certainly seems that way in prototyping.

So in conclusion I want to thank you guys again for your efforts, it’s always nice to reach ones limits, to have the borders defined more clearly…

One thing I am taking away from this is “extended”, which I might have to look into.

Why not add a fog to reduce the number of tiles that must be drawn?

Or simply a vignette?

Just throwing in my 2 cents based on what I’ve done:
Assuming using a spanning technique: (start_x to end_x) & (start_y to end_y) relative to a player map coordinate:
It’s possible to preprocess a map to mark tile with a “next_tile”, to skip empties - and to improve further (altho it may be overkill) you could use “frame coherence” and add/remove on a render-target (with coordinate reset) as needed (ie: moving the target’s offset back 64 and adding to the edges each time the draw coords move beyond 64 pixels). This way in the initial draw - all empties are skipped (instead x++, going x=next_index). This works for static layers. [also always using sprite-sheet/batching for all world gfx]
Some of this could get tricky for layers that contain a lot of dynamic elements (in which case I’d separate dynamic from static if needed).
Personally - I’d go simple with span skips and add speed tricks as necessity arises. Disagreements & criticisms are welcome. :slight_smile:

2 Likes

That’s a alternative if the map is static enough you could just render the parts of the fully drawn static map to textures and scroll thru them that would be really fast.
You could probably also instance it dunno if that would really be better.
But i suppose a mixed approach that was really refined would be the best for a particular situation were you knew exactly what was needed.
I do think in any case that this is a solid starting point to refine things further from.

Since this is up i might as well add how to manually set tiles via index.
Below i wanted to also demonstrate that the source destination and map tile indexs can be decoupled.
For example the second SetTile call below adds a map tile to be 3x as large as the other tiles so that it basically overlaps other tiles so that one tile drawn is really big on the map.

Additionally drawing a hexmap from this example instead is also straight forward and trivial.
Every odd numbered tile is displaced by 1/2 the destination tile height typically upwards and the placement position is multiplied by the destination tileWidth * by say .75 which is just some percentage of overlap

in load after the tile map is initially set up.

            var source = new Rectangle(7 * mountainSourceTileWidth, 7 * mountainSourceTileHeight, mountainSourceTileWidth, mountainSourceTileHeight);
            SetTile(t2d_height_mountain_mono, source, 1, 2);

            var destination = new Rectangle(2 * tileDestinationWidth, 2 * tileDestinationHeight, tileDestinationWidth*3, tileDestinationHeight*3);
            SetTile(t2d_height_mountain_mono, destination, source, 2, 2);

The above and below could be further automated depending on how you want them to behave and the expectations,

As well you could set them dynamically at runtime and then set the buffers again though im not sure you would want to do it that way for a dynamic layer in that case it might be better to have some layers that are in there own smaller buffer or just straight up drawn directly.

        public void SetTile(Texture2D texture, Rectangle destination, Rectangle source, int mapTilePosX, int mapTilePosY)
        {
            int layer = -1;
            for (int i = 0; i < layerTextures.Count; i++)
                if (layerTextures[i] == texture)
                {
                    layer = i;
                }
            if (layer < 0)
            {
                throw new Exception("Error This function doesn't explicitly handle adding a entirely new layer eg a new texture.");
            }
            int mapTilesLength = mapWidth * mapHeight;
            var layerVerticeOffset = mapTilesLength * layer * VERTICES_PER_QUAD;
            int startingVerticeIndex = ((mapTilePosY * mapWidth + mapTilePosX) * VERTICES_PER_QUAD) + layerVerticeOffset;
            //
            Vector2 tl = source.Location.ToVector2() / texture.Bounds.Size.ToVector2();
            Vector2 tr = new Vector2(source.Right, source.Top) / texture.Bounds.Size.ToVector2();
            Vector2 br = new Vector2(source.Right, source.Bottom) / texture.Bounds.Size.ToVector2();
            Vector2 bl = new Vector2(source.Left, source.Bottom) / texture.Bounds.Size.ToVector2();

            // t1
            vertexList[startingVerticeIndex + 0] = new VertexPositionColorTexture(new Vector3(destination.Left, destination.Top, 0f), vertexList[startingVerticeIndex + 0].Color, tl);
            vertexList[startingVerticeIndex + 1] = new VertexPositionColorTexture(new Vector3(destination.Left, destination.Bottom, 0f), vertexList[startingVerticeIndex + 1].Color, bl);
            vertexList[startingVerticeIndex + 2] = new VertexPositionColorTexture(new Vector3(destination.Right, destination.Bottom, 0f), vertexList[startingVerticeIndex + 2].Color, br);
            // t2
            vertexList[startingVerticeIndex + 3] = new VertexPositionColorTexture(new Vector3(destination.Right, destination.Bottom, 0f), vertexList[startingVerticeIndex + 3].Color, br);
            vertexList[startingVerticeIndex + 4] = new VertexPositionColorTexture(new Vector3(destination.Right, destination.Top, 0f), vertexList[startingVerticeIndex + 4].Color, tr);
            vertexList[startingVerticeIndex + 5] = new VertexPositionColorTexture(new Vector3(destination.Left, destination.Top, 0f), vertexList[startingVerticeIndex + 5].Color, tl);
        }


        public void SetTile(Texture2D texture, Rectangle source, int mapTilePosX, int mapTilePosY)
        {
            int layer = -1;
            for(int i =0; i < layerTextures.Count;i++)
                if(layerTextures[i] == texture)
                {
                    layer = i;
                }
            if(layer < 0)
            {
                throw new Exception("Error This function doesn't explicitly handle adding a entirely new layer eg a new texture.");
            }
            int mapTilesLength = mapWidth * mapHeight;
            var layerVerticeOffset = mapTilesLength * layer * VERTICES_PER_QUAD;
            int startingVerticeIndex = ((mapTilePosY * mapWidth + mapTilePosX) * VERTICES_PER_QUAD) + layerVerticeOffset;
            //
            Vector2 tl = source.Location.ToVector2() / texture.Bounds.Size.ToVector2();
            Vector2 tr = new Vector2(source.Right, source.Top) / texture.Bounds.Size.ToVector2();
            Vector2 br = new Vector2(source.Right, source.Bottom) / texture.Bounds.Size.ToVector2();
            Vector2 bl = new Vector2(source.Left, source.Bottom) / texture.Bounds.Size.ToVector2();

            // t1
            vertexList[startingVerticeIndex + 0] = new VertexPositionColorTexture(vertexList[startingVerticeIndex + 0].Position, vertexList[startingVerticeIndex + 0].Color, tl);
            vertexList[startingVerticeIndex + 1] = new VertexPositionColorTexture(vertexList[startingVerticeIndex + 1].Position, vertexList[startingVerticeIndex + 1].Color, bl);
            vertexList[startingVerticeIndex + 2] = new VertexPositionColorTexture(vertexList[startingVerticeIndex + 2].Position, vertexList[startingVerticeIndex + 2].Color, br);
            // t2
            vertexList[startingVerticeIndex + 3] = new VertexPositionColorTexture(vertexList[startingVerticeIndex + 3].Position, vertexList[startingVerticeIndex + 3].Color, br);
            vertexList[startingVerticeIndex + 4] = new VertexPositionColorTexture(vertexList[startingVerticeIndex + 4].Position, vertexList[startingVerticeIndex + 4].Color, tr);
            vertexList[startingVerticeIndex + 5] = new VertexPositionColorTexture(vertexList[startingVerticeIndex + 5].Position, vertexList[startingVerticeIndex + 5].Color, tl);
        }
1 Like

I like this idea, of a “next tile”… Will think about it!