Hi! I am making a simple tiled, pixel art 2D isometric game and getting fairly low FPS (~10 FPS at 320x240) because of all the overdrawing I end up doing. Profiling my code revealed that the biggest culprit is also the biggest cause of overdrawing (which btw is the player's shadow, currently causing redraws of a lot of tiles around it), and indeed removing that single cause gets me back to mostly solid 60 FPS. (still not solid 60 FPS because I have a few other things causing lots of redraws. Stopping those too gives rock solid 60 FPS)
Not doing so much overdrawing would be the obvious solution to all my problems, but I am completely out of ideas on how to further reduce it by now (and that would probably deserve a whole different thread), and the golden answer to most Löve graphics performance problems seems to be sprite batches, so I thought I'd try to learn a little more about them before reworking all my code to use them.
Considering I am drawing each tile individually (because the player can get behind most), should I expect considerable performance boost from switching to sprite batches? I am currently drawing everything to a buffer canvas before drawing that to the screen.
Also, from what I've read, performance-wise there seems to be no reason to not simply put EVERY SINGLE THING that ever goes on screen in one single gargantuan sprite atlas (well, other than the absolute hell that would be to manage. Speaking from a strictly performance only point of view). Is that what all the cool kids do, or am I missing something?
Thanks in advance!
Are sprite batches the solution to my low FPS?
Forum rules
Before you make a thread asking for help, read this.
Before you make a thread asking for help, read this.
-
- Party member
- Posts: 548
- Joined: Wed Oct 05, 2016 11:53 am
Re: Are sprite batches the solution to my low FPS?
What do you do to draw the player's shadow, and how does this incur more draw calls? How is an individual tile drawn, let alone how does the game step through drawing the map? Also because you do not make a mention of it, are you making sure to draw only the stuff that's visible on the screen at any given time? Without seeing any code, I can not tell how much sprite batches would help your use case.
It's worth noting that since 11.0 or so, draw calls automatically get batched if you do not alter the draw state (eq. draw different textures between calls, do not change shaders or shader uniforms, change canvases, etc). So for example if you're drawing your tiles from different textures, and the placement of tiles makes it alternate the currently drawing texture a lot, you'll start seeing more draw calls piling up.
It's worth noting that since 11.0 or so, draw calls automatically get batched if you do not alter the draw state (eq. draw different textures between calls, do not change shaders or shader uniforms, change canvases, etc). So for example if you're drawing your tiles from different textures, and the placement of tiles makes it alternate the currently drawing texture a lot, you'll start seeing more draw calls piling up.
Re: Are sprite batches the solution to my low FPS?
Overdraw doesn't by itself causes slowdowns, it's just usually every pixel is only rendered once (ideally) so in 1080p monitor you only have to process 2 million pixels per frame, but if you overdraw then this goes up by a multiple of overdraw passes, so with just a handful of fullscreen overdraws you're looking to render in excess of 10 million pixels per frame. And do note that you will also need to fetch in excess of 10 million texture pixels in the process, so memory bandwidth will also be impacted (shader complexity needs to be very very high for there to be a computational bottleneck and not a memory bottleneck).
Sprite batches obviously don't do anything about overdraw, but they reduce number of GPU draw calls - those are very slow to make and you have very limited number of them (in Vulkan not so much but that's not currently supported and it's not a silver bullet solution either). If your models consist of grand total two triangles and a plain texture, actually rendering them takes way less time than does sending them for render. Autobatching exist but it's significantly slower than manual batching, so don't take it as anything more substantial than a stopgap solution to bad framerate during unoptimized phase of development. Note that having to change render parameters - current texture, current shader or shader uniforms, current render target, et cetera, will force batch breakup. With that in mind, make your sprites render in a way that no change of these parameters is necessary: use a universal shader (make it work for any sprite, don't just paste all shaders into the same file using if-switches to use different subsections, that's the same as switching the entire shader), use a common texture atlas, group together rendering operations that can use the same render settings (and put them in a batch).
Drawing all tiles into a canvas before rendering that as a background for final render just incurs a bit of performance penalty because you're doing a few extra GPU calls and an extra fullscreen render of said canvas for no visual difference whatsoever. Regarding managing atlases, you don't manage them manually, you use software tools for that. Player shadow should simply be a second player sprite rendered directly underneath main player sprite.
Sprite batches obviously don't do anything about overdraw, but they reduce number of GPU draw calls - those are very slow to make and you have very limited number of them (in Vulkan not so much but that's not currently supported and it's not a silver bullet solution either). If your models consist of grand total two triangles and a plain texture, actually rendering them takes way less time than does sending them for render. Autobatching exist but it's significantly slower than manual batching, so don't take it as anything more substantial than a stopgap solution to bad framerate during unoptimized phase of development. Note that having to change render parameters - current texture, current shader or shader uniforms, current render target, et cetera, will force batch breakup. With that in mind, make your sprites render in a way that no change of these parameters is necessary: use a universal shader (make it work for any sprite, don't just paste all shaders into the same file using if-switches to use different subsections, that's the same as switching the entire shader), use a common texture atlas, group together rendering operations that can use the same render settings (and put them in a batch).
Drawing all tiles into a canvas before rendering that as a background for final render just incurs a bit of performance penalty because you're doing a few extra GPU calls and an extra fullscreen render of said canvas for no visual difference whatsoever. Regarding managing atlases, you don't manage them manually, you use software tools for that. Player shadow should simply be a second player sprite rendered directly underneath main player sprite.
Re: Are sprite batches the solution to my low FPS?
Every call of love.graphics.draw it's two-part process:
1. Send data to GPU (image + verticies + transform info, it's slow)
2. Draw data by GPU (it's mostly fast, if you don't use heavy shaders)
Love2d have autobatching, so if you send one image a lot of times in a row, it may be transformed to one batch and this batch will be sended to GPU once. But if your map is really large and you trying to draw all map tiles to gpu (5000x2000 for example), it will waste a lot of time for sending invisible things regardless of screen resolution. Also sending too much verticies (one rectangular tile is two triangles or six verticies) is may be slow.
So you need to clip tiles by camera view, and draw them sequentially (for autobatching), or just create one spritebatch, clear it every frame and refill it by tiles currently in camera view.
For isometric games, you can separate drawing stuff by order.
1. Combine all sprites (include floors/walls/items/player/NPCs etc) into one image (it may be 8192x8192px if here too much images, it's ok);
2. Cut this image into quad set;
3. Create spritebatch based on this image;
4. Clear batch;
5. Add floor to batch, clipped by camera view;
6. Add foreground (walls, player etc, clipped too);
7. Draw batch;
8. Goto 4.
Yay! A lot of FPS!
Simple example:
1. Send data to GPU (image + verticies + transform info, it's slow)
2. Draw data by GPU (it's mostly fast, if you don't use heavy shaders)
Love2d have autobatching, so if you send one image a lot of times in a row, it may be transformed to one batch and this batch will be sended to GPU once. But if your map is really large and you trying to draw all map tiles to gpu (5000x2000 for example), it will waste a lot of time for sending invisible things regardless of screen resolution. Also sending too much verticies (one rectangular tile is two triangles or six verticies) is may be slow.
So you need to clip tiles by camera view, and draw them sequentially (for autobatching), or just create one spritebatch, clear it every frame and refill it by tiles currently in camera view.
For isometric games, you can separate drawing stuff by order.
1. Combine all sprites (include floors/walls/items/player/NPCs etc) into one image (it may be 8192x8192px if here too much images, it's ok);
2. Cut this image into quad set;
3. Create spritebatch based on this image;
4. Clear batch;
5. Add floor to batch, clipped by camera view;
6. Add foreground (walls, player etc, clipped too);
7. Draw batch;
8. Goto 4.
Yay! A lot of FPS!
Simple example:
Code: Select all
local image = love.graphics.newImage(...)
local batch = love.graphics.newSpriteBatch(image, ...)
-- Quads instead of images with the same interface as images.
local images = {
floor = love.graphics.newQuad(...),
wall = love.graphics.newQuad(...),
player = love.graphics.newQuad(...),
enemy = love.graphics.newQuad(...),
}
local map = {}
for i = 1, ... do
map[i] = {}
for j = 1, ... do
map[i][j] = {
bakgr = images.floor, -- floor tiles here
foregr = images.wall -- wall/player/enemy etc
}
end
end
function love.draw()
local floor, ceil = math.floor, math.ceil
batch:clear()
local x, y, w, h = camera:getDimensions()
x, y = floor(x / tilewidth), floor(y / tileheight)
w, h = ceil(w / tilewidth), ceil(h / tileheight)
-- add boundaries checks
...
-- add floor layer
for i = x, x + w do
for j = y, y + h do
batch:add(map[i][j].backgr, x * tilewidth, y * tileheight, ...)
end
end
-- add foreground layer
for i = x, x + w do
for j = y, y + h do
batch:add(map[i][j].foregr, x * tilewidth, y * tileheight, ...)
end
end
-- also we can add clouds/weather stuff etc, a lot of layers.
-- And finally, we send only visible data once.
love.graphics.draw(batch)
end
Science and violence
Who is online
Users browsing this forum: Bing [Bot] and 4 guests