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

Hello!

I have a pretty extensive tile engine going. I have parallaxing layers, each with a single large tile-grid (2d array of “Tiles”) which extends the full size of the layer… So background layers, which are scaled to look far away, have many tiles visible at any given time, many of which are just empty space for player traversal, ie have nothing to draw.

My problem is, I feel like it taxes the system too hard to run draw-calls on every tile (in a nested for-loop style setup)
even if there is nothing to draw (each tile would get an if statement on whether or not to draw, or simply draw an empty texture or using alpha=0)

Many of my systems use these layer sized grids, even if they contain no elements to draw… Lighting, collision detection, path-finding, etc… So I feel they must persist in their current form.

I am looking for a way to cull empty tiles from my draw method, which right now just draw the whole visible/on-screen segment of the layer grid, no matter the content… Any good ideas out there?

ONE idea I have, is adding smaller 2d-arrays to each layer, containing just the coords of tile-segments in the big grid that need drawing, and then running my draw for-loops on each of these… But this sounds like a chore to implement, and I would appreciate feed-back before I get started… Thoughts?

1 Like

Idea 1: Only consider tiles that intersect with your camera’s frustum. It seems you have already tried this.

Idea 2: Upload your tiles as vertex data to a GPU buffer once or infrequently and just don’t even try to figure out what tiles need to be rendered on the CPU every tick.

Thank you for your answer, but I still need help.

Yes to number one… I already do that. It’s more the empty tiles within my camera scope that bother me… Some tile-grids may be mostly empty space (tons of empty tiles), but the nested for-loops still push every tile in the grid through my draw-method.

Could you elaborate on number 2 a little? I have near zero experience with these concepts, I only use something similar it to subtract some black shadow-casting geometry from my lights…

-If you could explain the concept you are getting at for dummies, that would be great… Or if you can link to something relevant, I will read.
-Right now, I don’t even know enough about “Upload your tiles as vertex data to a GPU buffer” to understand how that would help me.

1 Like

Hi @Mando_Palaise, welcome to the forums.

Assuming you have a draw flag in your method?

Though now dwindling from the internet, you can try looking up old XNA resources in the waybackmachine website, and search in general for XNA Tile Rendering tutorials, have you come across Reimers? unsure if I spelt it correctly but he has many tutorials.

What level of C# are you at?

Happy Coding!

Hi, and thank you!

I’ve actually used this forum in the way-back, but lost my sign-in credentials years ago. Life got in the way for a while there. Reimers tuts were a good resource back then :slight_smile:

-But I have never used a “draw flag”… Unless you mean a simple bool for each tile to draw or not, I think I’m currently relying on alpha value for that, anything above zero draws… Of coarse this relies on a check for each tile, which (I think?) is cumbersome with many layers of tiles…

I’m at a level of C# where I have completed several projects over the years, and can do so without help, from start to finish. One or two 3d projects included, but 2d is my thing. Though, like now, whenever I push to do something new, I will seek help/tips, there is always more to learn.

I think I am leaning towards high-lighting rectangular areas of my master tile-map, and storing these high-lights min/max X-Y values (top left and bottom right corners), and using those X-Y values as start/end points for my nested for-loops ie draw loops.

pseudo code:
for each highlighted_draw_area
{
for (y = highlighted_draw_area.minY; y < highlighted_draw_area.maxY; y++)
{
for (x = highlighted_draw_area.minX; x < highlighted_draw_area.maxX; x++)
{
draw tile x,y;
}
}
}

What do you think? Sounds decent? -The idea being not having any checks in draw, and not drawing too many empty tiles.

1 Like

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.