I’ve been looking at efficient ways to render a large number of tiles. I adapted my code from here: capitalgstudios.wordpress.com/2019/01/03/spritebatch-vs-vertexbuffer/.
This can render 250k tiles at 60fps. You can zoom out and have all tiles on the screen at once with no impact on the performance.
However, if I render the same number of tiles in a tighter formation, sprites overlaid on top of each other, then render performance decreases a lot, FPS went down to ~14. You can try out and toggle with F1 to see the difference.
Why is this happening?
screenshots:
graphic used:
font used:
https://github.com/bo3b/3Dmigoto/blob/master/arial.spritefont
test code:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
namespace SampleTemplate
{
public class Examples : Game
{
public static SpriteBatch SpriteBatch = null;
public static SpriteFont SpriteFont = null;
// Constants
private const int QuadPixelSize = 32;
private const int QuadTotalVertices = 6;
// Quad Data
private int m_iWidth = 500;
private int m_iHeight = 500;
private int m_iQuadTotal = 0; // number of quads IE = m_iWidth * m_iHeight
// misc
public Random Random = new Random();
private GraphicsDeviceManager m_oGraphicDevice = null;
private string sMode;
public bool tileLayout_grid = true;
// Textures
private Texture2D m_oTextureQuad = null;
// Effects
private BasicEffect m_oBasicEffect = null;
// Vertex Buffers
private VertexBuffer m_oVertexBuffer = null;
// Input
private KeyboardState m_oPreviousKeyboardState = Keyboard.GetState();
// Components
private FrameRateCounter m_oFrameRateCounter = null;
private Camera m_oCamera = null;
public Examples()
{
m_oGraphicDevice = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
m_oFrameRateCounter = new FrameRateCounter(this); // add basic FPS counter
m_oCamera = new Camera(this);
Components.Add(m_oFrameRateCounter);
Components.Add(m_oCamera);
base.Initialize();
}
protected override void LoadContent()
{
SpriteBatch = new SpriteBatch(GraphicsDevice);
// Load Textures
m_oTextureQuad = Content.Load<Texture2D>("earth");
// Load fonts
SpriteFont = Content.Load<SpriteFont>("arial");
// Effects
m_oBasicEffect = new BasicEffect(GraphicsDevice);
m_oBasicEffect.TextureEnabled = true;
m_oBasicEffect.Texture = m_oTextureQuad;
GraphicsDevice.BlendState = BlendState.NonPremultiplied;
GraphicsDevice.RasterizerState = RasterizerState.CullNone;
m_oGraphicDevice.ApplyChanges();
//m_oBasicEffect.VertexColorEnabled = true; // if enabled I won't be able to use VertexPositionTexture
Vector3 oCameraPosition = new Vector3(0.0f, 0.0f, 1); // 100000.0f
Vector3 oCameraTarget = new Vector3(0.0f, 0.0f, 0.0f); // Look back at the origin
float fFovAngle = MathHelper.ToRadians(45); // convert 45 degrees to radians
float fAspectRatio = GraphicsDevice.Viewport.Width / GraphicsDevice.Viewport.Height;
float fNear = 0.01f; // the near clipping plane distance
float fFar = 1500000f; // the far clipping plane distance
Matrix oWorld = Matrix.CreateTranslation(0.0f, 0.0f, 0.0f);
Matrix oView = Matrix.CreateLookAt(oCameraPosition, oCameraTarget, Vector3.Up);
Matrix oProjection = Matrix.CreatePerspectiveFieldOfView(fFovAngle, fAspectRatio, fNear, fFar);
m_oBasicEffect.World = oWorld;
m_oBasicEffect.View = oView;
m_oBasicEffect.Projection = oProjection;
CreateTiles();
}
protected override void Update(GameTime gameTime)
{
KeyboardState oCurrentKeyboardState = Keyboard.GetState();
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
{
Exit();
}
if (m_oPreviousKeyboardState.IsKeyDown(Keys.F1) && oCurrentKeyboardState.IsKeyUp(Keys.F1))
{
tileLayout_grid = !tileLayout_grid;
CreateTiles();
}
if (m_oPreviousKeyboardState.IsKeyDown(Keys.Up) && oCurrentKeyboardState.IsKeyUp(Keys.Up))
{
m_iWidth += 5;
m_iHeight += 5;
CreateTiles();
}
if (m_oPreviousKeyboardState.IsKeyDown(Keys.Down) && oCurrentKeyboardState.IsKeyUp(Keys.Down))
{
if (m_iWidth - 5 > 0)
{
m_iWidth -= 5;
}
if (m_iHeight - 5 > 0)
{
m_iHeight -= 5;
}
CreateTiles();
}
m_oPreviousKeyboardState = oCurrentKeyboardState;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
m_oBasicEffect.View = m_oCamera.View;
GraphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
GraphicsDevice.SetVertexBuffer(m_oVertexBuffer);
foreach (EffectPass oEffectPass in m_oBasicEffect.CurrentTechnique.Passes)
{
oEffectPass.Apply();
GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, m_iQuadTotal * 2); // 2* as it is two triangles
}
// text output
SpriteBatch.Begin();
SpriteBatch.DrawString(SpriteFont, $"Rendering {m_iQuadTotal} tiles, layout: {sMode} (Cam X: {m_oCamera.m_oPosition.X}, Y: {m_oCamera.m_oPosition.Y}, Z: {m_oCamera.m_oPosition.Z})", new Vector2(100, 5), Color.Black); // drop shadow
SpriteBatch.DrawString(SpriteFont, $"Rendering {m_iQuadTotal} tiles, layout: {sMode} (Cam X: {m_oCamera.m_oPosition.X}, Y: {m_oCamera.m_oPosition.Y}, Z: {m_oCamera.m_oPosition.Z})", new Vector2(99, 4), Color.White);
SpriteBatch.End();
base.Draw(gameTime);
}
#region TILE MAP
private void CreateTiles()
{
if (tileLayout_grid) // depending on the tile layout mode, we recreate the tiles and move the camera there
{
CreateTiles_grid();
sMode = "Grid";
m_oCamera.m_oPosition = new Vector3(-8000, 8100, 19701);
}
else
{
CreateTiles_tight();
sMode = "Tight";
m_oCamera.m_oPosition = new Vector3(-1300, 800, 1601);
}
}
private void CreateTiles_tight()
{
m_iQuadTotal = m_iWidth * m_iHeight;
// Generate vertex buffer
VertexPositionTexture[] oVertices = new VertexPositionTexture[m_iQuadTotal * QuadTotalVertices];
m_oVertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), oVertices.Length, BufferUsage.WriteOnly);
int iX = 0;
int iY = 0;
Random random = new Random();
for (int iCell = 0; iCell < m_iQuadTotal; iCell++)
{
Vector2 randomPos = new Vector2((float)(random.NextDouble() * GraphicsDevice.Viewport.Width/10), (float)(random.NextDouble() * GraphicsDevice.Viewport.Height / 10));
int iCellIndex = iCell * 6;
// Vertex Position
// - Triangle 1
oVertices[iCellIndex + 0].Position = new Vector3((randomPos.X * 32), (randomPos.Y * 32), 0);
oVertices[iCellIndex + 1].Position = new Vector3((randomPos.X * 32), 32 + (randomPos.Y * 32), 0);
oVertices[iCellIndex + 2].Position = new Vector3(32 + (randomPos.X * 32), (randomPos.Y * 32), 0);
// - Triangle 2
oVertices[iCellIndex + 3].Position = oVertices[iCellIndex + 1].Position;
oVertices[iCellIndex + 4].Position = new Vector3(32 + (randomPos.X * 32), 32 + (randomPos.Y * 32), 0);
oVertices[iCellIndex + 5].Position = oVertices[iCellIndex + 2].Position;
// Vertex Texture
// - Triangle 1
oVertices[iCellIndex + 0].TextureCoordinate = new Vector2(0, 1); // bottom left
oVertices[iCellIndex + 1].TextureCoordinate = new Vector2(0, 0); // Top Left
oVertices[iCellIndex + 2].TextureCoordinate = new Vector2(1, 1); // Bottom Right
// - Triangle 2
oVertices[iCellIndex + 3].TextureCoordinate = oVertices[iCellIndex + 1].TextureCoordinate; // Top Left again
oVertices[iCellIndex + 4].TextureCoordinate = new Vector2(1, 0); // Top Right
oVertices[iCellIndex + 5].TextureCoordinate = oVertices[iCellIndex + 2].TextureCoordinate; // Bottom Right again
if (iX < (m_iWidth - 1))
{
iX++;
}
else
{
iX = 0;
iY++;
}
}
m_oVertexBuffer.SetData(oVertices);
}
private void CreateTiles_grid()
{
m_iQuadTotal = m_iWidth * m_iHeight;
// Generate vertex buffer
VertexPositionTexture[] oVertices = new VertexPositionTexture[m_iQuadTotal * QuadTotalVertices];
m_oVertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), oVertices.Length, BufferUsage.WriteOnly);
int iX = 0;
int iY = 0;
for (int iCell = 0; iCell < m_iQuadTotal; iCell++)
{
int iCellIndex = iCell * 6;
//Color oColor = new Color(Random.Next(0, 255), Random.Next(0, 255), Random.Next(0, 255));
// Vertex Position
// - Triangle 1
oVertices[iCellIndex + 0].Position = new Vector3((iX * 32), (iY * 32), 0);
oVertices[iCellIndex + 1].Position = new Vector3((iX * 32), 32 + (iY * 32), 0);
oVertices[iCellIndex + 2].Position = new Vector3(32 + (iX * 32), (iY * 32), 0);
// - Triangle 2
oVertices[iCellIndex + 3].Position = oVertices[iCellIndex + 1].Position;
oVertices[iCellIndex + 4].Position = new Vector3(32 + (iX * 32), 32 + (iY * 32), 0);
oVertices[iCellIndex + 5].Position = oVertices[iCellIndex + 2].Position;
// Vertex Texture
// - Triangle 1
oVertices[iCellIndex + 0].TextureCoordinate = new Vector2(0, 1); // bottom left
oVertices[iCellIndex + 1].TextureCoordinate = new Vector2(0, 0); // Top Left
oVertices[iCellIndex + 2].TextureCoordinate = new Vector2(1, 1); // Bottom Right
// - Triangle 2
oVertices[iCellIndex + 3].TextureCoordinate = oVertices[iCellIndex + 1].TextureCoordinate; // Top Left again
oVertices[iCellIndex + 4].TextureCoordinate = new Vector2(1, 0); // Top Right
oVertices[iCellIndex + 5].TextureCoordinate = oVertices[iCellIndex + 2].TextureCoordinate; // Bottom Right again
if (iX < (m_iWidth - 1))
{
iX++;
}
else
{
iX = 0;
iY++;
}
}
m_oVertexBuffer.SetData(oVertices);
}
#endregion
#region CAMERA / FRAME RATE COUNTER
public class Camera : Microsoft.Xna.Framework.GameComponent
{
public Vector3 m_oPosition = new Vector3(0, 0, 1.0f);
private float m_fZoom = 1.0f;
// Input
private KeyboardState m_oPreviousKeyboardState = Keyboard.GetState();
private KeyboardState m_oCurrentKeyboardState = Keyboard.GetState();
private MouseState m_oPreviousMouseState = Mouse.GetState();
private MouseState m_oCurrentMouseState = Mouse.GetState();
public Camera(Game oGame) : base(oGame)
{
}
#region MonoGame Pipeline
public override void Initialize()
{
base.Initialize();
}
public override void Update(GameTime oGameTime)
{
m_oCurrentKeyboardState = Keyboard.GetState();
m_oCurrentMouseState = Mouse.GetState();
// Horizontal Panning
if (m_oCurrentKeyboardState.IsKeyDown(Keys.D))
{
m_oPosition.X -= 100;
}
if (m_oCurrentKeyboardState.IsKeyDown(Keys.A))
{
m_oPosition.X += 100;
}
// Vertical Panning
if (m_oCurrentKeyboardState.IsKeyDown(Keys.W))
{
m_oPosition.Y += 100;
}
if (m_oCurrentKeyboardState.IsKeyDown(Keys.S))
{
m_oPosition.Y -= 100;
}
// Zooming
if (m_oCurrentKeyboardState.IsKeyDown(Keys.Q))
{
m_oPosition.Z += 1000;
}
if (m_oCurrentKeyboardState.IsKeyDown(Keys.E))
{
m_oPosition.Z -= 100;
}
m_oPreviousKeyboardState = m_oCurrentKeyboardState;
m_oPreviousMouseState = m_oCurrentMouseState;
base.Update(oGameTime);
}
#endregion
#region Public Properties
public Matrix View
{
get { return Matrix.CreateLookAt(new Vector3(-m_oPosition.X, m_oPosition.Y, m_oPosition.Z), new Vector3(-m_oPosition.X, m_oPosition.Y, 0), Vector3.Up) * Matrix.CreateScale(m_fZoom); }
}
#endregion
}
public class FrameRateCounter : DrawableGameComponent
{
private int m_iFrameRate = 0;
private int m_iFrameCounter = 0;
private string m_sMessage = string.Empty;
private TimeSpan m_oElapsedTime = TimeSpan.Zero;
public FrameRateCounter(Game oGame) : base(oGame)
{
}
protected override void LoadContent()
{
}
public override void Update(GameTime oGameTime)
{
m_oElapsedTime += oGameTime.ElapsedGameTime;
if (m_oElapsedTime > TimeSpan.FromSeconds(1))
{
m_oElapsedTime -= TimeSpan.FromSeconds(1);
m_iFrameRate = m_iFrameCounter;
m_iFrameCounter = 0;
}
m_sMessage = $"FPS: {m_iFrameRate}";
}
public override void Draw(GameTime oGameTime)
{
m_iFrameCounter++;
Examples.SpriteBatch.Begin();
Examples.SpriteBatch.DrawString(Examples.SpriteFont, m_sMessage, new Vector2(6, 5), Color.Black);
Examples.SpriteBatch.DrawString(Examples.SpriteFont, m_sMessage, new Vector2(5, 4), Color.White);
Examples.SpriteBatch.End();
}
}
#endregion
}
}