Any way to get canvases to be more efficient?

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Sky_Render
Prole
Posts: 48
Joined: Fri Jul 05, 2019 2:59 am

Re: Any way to get canvases to be more efficient?

Post by Sky_Render »

I'd have to make some fixes to my code right now to get it working again; the SpriteBatch experiment really messed it up (good thing I didn't delete my original render code!). I have been working on that while we've been talking, though, and once I've got it fixed I will be posting what I've got so far.

For a decent example of similar visual effect, just about any modern 2D top-down-Zelda-style game with lots of layering would suffice. Graveyard Keeper, Stardew Valley, and Undermine all come to mind as decent examples of games of the style in question.
User avatar
raidho36
Party member
Posts: 2063
Joined: Mon Jun 17, 2013 12:00 pm

Re: Any way to get canvases to be more efficient?

Post by raidho36 »

Sky_Render wrote: Sat Feb 22, 2020 6:07 pm For a decent example of similar visual effect, just about any modern 2D top-down-Zelda-style game with lots of layering would suffice. Graveyard Keeper, Stardew Valley, and Undermine all come to mind as decent examples of games of the style in question.
There's absolutely nothing in there that warrants using more than one layer.
Sky_Render
Prole
Posts: 48
Joined: Fri Jul 05, 2019 2:59 am

Re: Any way to get canvases to be more efficient?

Post by Sky_Render »

raidho36 wrote: Sat Feb 22, 2020 6:12 pm
Sky_Render wrote: Sat Feb 22, 2020 6:07 pm For a decent example of similar visual effect, just about any modern 2D top-down-Zelda-style game with lots of layering would suffice. Graveyard Keeper, Stardew Valley, and Undermine all come to mind as decent examples of games of the style in question.
There's absolutely nothing in there that warrants using more than one layer.
Oh really? So how exactly would one recognize foreground? Or background that doesn't have shadows rendered over it? And I guess lighting effects are also not a thing? Your statement does not make sense to me.
Sky_Render
Prole
Posts: 48
Joined: Fri Jul 05, 2019 2:59 am

Re: Any way to get canvases to be more efficient?

Post by Sky_Render »

As promised, now that I've managed to resurrect my working code, here's what it's doing. This is called once when the game loads:

Code: Select all

function render_layers()
  -- Renders all layers of the selected map

  local i = 1
  -- Draw background
  while i <= map_lastshadow[current_map] do
   if i ~= map_collisionlayer[current_map] then
    draw_layer(current_map, i)
    draw_layer(current_map, i, 1)
    if current_season == 0 then draw_layer(current_map, i, 2) end
   end
   i = i + 1
  end

  -- Draw above-shadow-below-entity background layers
  i = map_lastshadow[current_map] + 1
  while i < map_foreground[current_map] do
   if i ~= map_collisionlayer[current_map] then
    draw_layer(current_map, i)
    draw_layer(current_map, i, 1)
    if current_season == 0 then draw_layer(current_map, i, 2) end
   end
   i = i + 1
  end

  -- Draw foreground
  i = map_foreground[current_map]
  while i <= map_layer_count[current_map] do
  if i ~= map_collisionlayer[current_map] then
   draw_layer(current_map, i)
   draw_layer(current_map, i, 1)
   if current_season == 0 then draw_layer(current_map, i, 2) end
  end
   i = i + 1
  end
end
And draw_layer looks like this:

Code: Select all

function draw_layer(index, layer, layertype)
  -- Draws a layer from the specified map
  local hold_r = hold_r
  local hold_g = hold_g
  local hold_b = hold_b
  local tile_xsize = tile_xsize[index]
  local tile_ysize = tile_ysize[index]
  local map_xsize = map_xsize[index]
  local map_ysize = map_ysize[index]

  local i = 1
  local j = 1
  local l = 1
  local rendertile
  local selecttileset = 1
  local pickset = "tileset_tiles"

if setting_map == false then
 if layertype == 1 then g.setCanvas(snow_layer[layer])
 elseif layertype == 2 then g.setCanvas(transition_layer[layer])
 else g.setCanvas(map_layer[layer]) end
 g.clear()
end
  while j <= map_ysize do
    while l <= map_xsize do
      pickset = "tileset_tiles"
      if tile[index][layer] then if tile[index][layer][l] then if tile[index][layer][l][j] then
      if tile[index][layer][l][j] ~= 0 then
        rendertile = tile[index][layer][l][j]

        -- Check to see if the tile's value exceeds the currently-checked tileset offset
        selecttileset = 1
        while map_tileset[index][i] do
          if tile[index][layer][l][j] >= map_tileset_offset[index][i] then
            -- Check if an offset exists higher than this
            if not map_tileset_offset[index][i + 1] then
              -- No higher offset that meet this criteria, so use this one
              rendertile = tile[index][layer][l][j] - map_tileset_offset[index][i] + 1
              selecttileset = i
            elseif i > 1 then -- This one is too big, use the previous one (but only if not first index!)
              rendertile = tile[index][layer][l][j] - map_tileset_offset[index][i] + 1
              selecttileset = i
            end
          end
          i = i + 1
        end
        i = 1
        
        selecttileset = map_tileset[index][selecttileset]
        if tileset_variants[selecttileset] then
         if not layertype then
          if current_season == 2 or (current_season == 0 and current_subseason == 3) then
           pickset = "tileset_summertiles"
          elseif current_season == 3 or (current_season == 0 and current_subseason == 4) then
           pickset = "tileset_autumntiles"
          elseif current_season == 4 or (current_season == 0 and current_subseason == 1) then
           pickset = "tileset_wintertiles"
          else
           pickset = "tileset_tiles"
          end
         else
          if current_subseason == 1 then pickset = "tileset_tiles"
          elseif current_subseason == 2 then pickset = "tileset_summertiles"
          elseif current_subseason == 3 then pickset = "tileset_autumntiles"
          elseif current_subseason == 4 then pickset = "tileset_wintertiles" end
         end
        end

        if layertype == 1 then pickset = "tileset_snowtiles" end

        -- Store diggable and water info; this is the only place it made sense to set this...
        if tileset_diggable[selecttileset][rendertile] == true and not tile_diggable[index][l][j] then
         tile_diggable[index][l][j] = true
         tile_digimage[index][l][j] = ""
         tile_waterimage[index][l][j] = ""
         tile_state[index][l][j] = "normal"
         tile_tilled[index][l][j] = false
         tile_ploughed[index][l][j] = false
         tile_trenched[index][l][j] = false
         tile_trenched_filled[index][l][j] = false
        end
        if tileset_water[selecttileset][rendertile] == true and not tile_water[index][l][j] then
         tile_water[index][l][j] = true
        end

        if tileset_animation[selecttileset][rendertile - 1] then rendertile = tileset_animation_frame[selecttileset][rendertile - 1][tileset_animation[selecttileset][rendertile - 1]] + 1 end

        -- Render the tile; if this is a blockmap render, we do it a little differently!
        if map_layer_opacity[index][layer] then g.setColor(hold_r, hold_g, hold_b, map_layer_opacity[index][layer]) end
        if setting_map then gdraw(tileset_tiles[selecttileset][rendertile], ((l * tile_xsize) - tile_xsize), ((j * tile_ysize) - tile_ysize))
        else if _G[pickset][selecttileset][rendertile] then gdraw(_G[pickset][selecttileset][rendertile], ((l * tile_xsize) - tile_xsize), ((j * tile_ysize) - tile_ysize)) end
        end end end end end
      g.setColor(hold_r, hold_g, hold_b, base_a)
    l = l + 1
    end
  l = 1
  j = j + 1
  end
 if setting_map == false then g.setCanvas() end
end
In love.draw it's called thusly in 3 different variants depending on background, non-shadow background, and foreground:

Code: Select all

  while i <= map_lastshadow[current_map] do
    if i ~= map_collisionlayer[current_map] then
     gdraw(map_layer[i], screen_x, screen_y, base_rot, scale_x, scale_y)
    g.setColor(base_r, base_g, base_b, snow_level / 100)
    gdraw(snow_layer[i], screen_x, screen_y, base_rot, scale_x, scale_y)
    g.setColor(base_r, base_g, base_b, base_a)
    end
    i = i + 1
  end
User avatar
raidho36
Party member
Posts: 2063
Joined: Mon Jun 17, 2013 12:00 pm

Re: Any way to get canvases to be more efficient?

Post by raidho36 »

This code looks like you took Durgasoft courses a little too unironically.

You can start by not using globals and not using nondescript numerical indices. Not grinding through a bunch of static data for every single tile would also help. Finally you can organize render order of your tiles by GPU draw groups and not by coordinate axis. That's as far as I'd put it because the entire thing needs a complete rewrite, but given that you made this in the first place that might actually be a counter-productive advice. Without changing the way you handle effects, it should look something like this:

Code: Select all

for x = x_visible_min, x_visible_max do
	for y = y_visible_min, y_visible_max do
		table.insert ( rendertiles, tiles[layer][x][y] )
	end
end
table.sort ( rendertiles, function ( a, b ) return a.priority < b.priority end )
for i = 1, #rendertiles do
	spritebatch:add ( rendertiles[i].sprite, rendertiles[i].x, rendertiles[i].y )
end
love.graphics.draw ( spritebatch, camera_x, camera_y )
The tile sprite isn't needed to be dynamically selected in real time for every single tile, when a trigger condition occurs that requires a tile to change its sprite, do so in update routine.
Sky_Render
Prole
Posts: 48
Joined: Fri Jul 05, 2019 2:59 am

Re: Any way to get canvases to be more efficient?

Post by Sky_Render »

raidho36 wrote: Sat Feb 22, 2020 7:16 pm This code looks like you took Durgasoft courses a little too unironically.

You can start by not using globals and not using nondescript numerical indices. Not grinding through a bunch of static data for every single tile would also help. Finally you can organize render order of your tiles by GPU draw groups and not by coordinate axis. That's as far as I'd put it because the entire thing needs a complete rewrite, but given that you made this in the first place that might actually be a counter-productive advice. Without changing the way you handle effects, it should look something like this:

Code: Select all

for x = x_visible_min, x_visible_max do
	for y = y_visible_min, y_visible_max do
		table.insert ( rendertiles, tiles[layer][x][y] )
	end
end
table.sort ( rendertiles, function ( a, b ) return a.priority < b.priority end )
for i = 1, #rendertiles do
	spritebatch:add ( rendertiles[i].sprite, rendertiles[i].x, rendertiles[i].y )
end
love.graphics.draw ( spritebatch, camera_x, camera_y )
The tile sprite isn't needed to be dynamically selected in real time for every single tile, when a trigger condition occurs that requires a tile to change its sprite, do so in update routine.
I don't really get most of what you just said, I'll be perfectly honest with you. I've only just realized that Canvases just store the instructions given to them and parse them when called via the draw command, which explains a lot about the issues I'm running into. Now that I get that, however, I think I have some ideas of how to improve things.
User avatar
raidho36
Party member
Posts: 2063
Joined: Mon Jun 17, 2013 12:00 pm

Re: Any way to get canvases to be more efficient?

Post by raidho36 »

Sky_Render wrote: Sat Feb 22, 2020 7:30 pmI've only just realized that Canvases just store the instructions given to them and parse them when called via the draw command
Uh, no? That would be spritebatch (and technically it just combines sprite geometry into a single megasprite). Canvas is the same as the screen, except not rigidly tied to the actual screen.

There are three ways you optimize things:
1) don't do it
2) do it less often
3) do it using less resources

I'm pretty sure at no point you need to check if tiles are within offsets, because you can simply generate tiles that would never violate this requirement, so don't do it. I'm pretty sure your seasons don't change every frame for every tile individually and you can just loop over the tiles and enable different graphics once seasons actually change, so do it less often. Finally you just brute force render all your tiles left to right ,top to bottom. Autobatching will give you some mileage but randomly dropped tiles from different tilesets gonna break it. One way to go about it is not to have different tilesets, to have one megaset that covers for the entire level (possibly the entire game). Another way is to sort the tiles by the tileset such that it doesn't need to switch back and forth for no good reason. It's not difficult to reduce the amount of draw calls, do it using less resources.
User avatar
zorg
Party member
Posts: 3436
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

Re: Any way to get canvases to be more efficient?

Post by zorg »

Sky_Render wrote: Sat Feb 22, 2020 6:17 pm
raidho36 wrote: Sat Feb 22, 2020 6:12 pm
Sky_Render wrote: Sat Feb 22, 2020 6:07 pm For a decent example of similar visual effect, just about any modern 2D top-down-Zelda-style game with lots of layering would suffice. Graveyard Keeper, Stardew Valley, and Undermine all come to mind as decent examples of games of the style in question.
There's absolutely nothing in there that warrants using more than one layer.
Oh really? So how exactly would one recognize foreground? Or background that doesn't have shadows rendered over it? And I guess lighting effects are also not a thing? Your statement does not make sense to me.
Stencil masks to separate foreground and background, and shaders for shadows and lighting effects utilizing the stencil masks?
By my count, that's still one layer, two at most if you really want entities to be on a separate layer, but then the shader stuff might get a bit more complicated... oh, and maybe add another layer for the GUI. :3
Me and my stuff :3True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.
User avatar
raidho36
Party member
Posts: 2063
Joined: Mon Jun 17, 2013 12:00 pm

Re: Any way to get canvases to be more efficient?

Post by raidho36 »

GUI just goes on top of everything so it doesn't needs to be a layer. Well actually since everything is rendered in specific order and layers' only purpose is to impose a specific render order - which already exists - there is never any need for layers. I decided not to answer any of that because it's a backwards question. It's like saying that santa claus has to exist because otherwise how are you supposed to get christmas presents.
Sky_Render
Prole
Posts: 48
Joined: Fri Jul 05, 2019 2:59 am

Re: Any way to get canvases to be more efficient?

Post by Sky_Render »

I've been doing some testing on various rendering in a dedicated program, and have found the following thus far:

- Rendering to a SpriteBatch is limited to about 16 layers for efficiency, and holds many other limitations as well.
- Rendering to an ImageData is limited to about 19 layers for efficiency, and will clip anything pasted outside of the size limitations.
- Rendering to a Canvas is limited to about 19 layers for efficiency, and will not clip edges.

I can't get ArrayImages to test correctly, however; it declares anything I send it besides a filename to be nil, even if it is ImageData, but I suspect it would be no more efficient than ImageData and Canvas are. I cannot seem to find a method that exceeds 19 layers, however. From what I've read on the Wiki, Shaders could potentially help with this, but I'm finding the documentation on them to be hard to comprehend.

EDIT: Adding the basic shader listed on the newShader page upped efficiency to 19, 21, and 21 respectively. Not exactly impressive gains, I feel like I'm still missing something here.
Last edited by Sky_Render on Mon Feb 24, 2020 7:40 pm, edited 1 time in total.
Post Reply

Who is online

Users browsing this forum: Bing [Bot], Google [Bot] and 36 guests