TL;DR: Is there a way to cache the rasterization of a Node2D (and its children) so that the drawing that occurs every frame is just 1 operation blitting the cached image onto the screen?
Hello everyone!
I'm pretty new to Godot so please bear with me if I understand something wrong :)
While fiddling around with a little platformer project I noticed that the performance of a TileMap drops rapidly when the map grows. In my case, a TileMap with 50x50 tiles uses about 50% of one cpu. At 100x50 tiles I already get below 60 fps. And that is without any processing whatsoever. Just a scene with a TileMap.
To find out where that cpu time goes, I set up a simulation of what I guess TileMap does to draw itself. Now I have a scene with just a Node2D and the following script which draws 100x100 tiles from a png:
extends Node2D
var sprite
func _init():
self.sprite = load("res://tile.png")
._init()
func _ready():
var ci_rid = VisualServer.canvas_item_create()
VisualServer.canvas_item_set_parent(ci_rid, get_canvas_item())
for x in range(100):
for y in range(100):
VisualServer.canvas_item_add_texture_rect(ci_rid, Rect2(Vector2(x, y) * 8, Vector2(8, 8)), self.sprite)
This works but it uses 100% cpu and runs at about 40 fps. I don't fully understand how Godot's drawing works but my guess is that every frame it issues 100x100 drawing commands.
The alternative would be to pre-rasterize all that drawing, cache the result in a texture and then we have only 1 drawing command every frame:
extends Node2D
var sprite
func _init():
self.sprite = load("res://tile.png")
._init()
func _ready():
var full_image = Image.new()
full_image.create(100 * 8, 100 * 8, false, Image.FORMAT_RGBA8)
for x in range(100):
for y in range(100):
full_image.blit_rect(self.sprite, Rect2(0, 0, 8, 8), Vector2(x * 8, y * 8))
var full_image_texture = VisualServer.texture_create_from_image(full_image)
var ci_rid = VisualServer.canvas_item_create()
VisualServer.canvas_item_add_texture_rect(ci_rid, Rect2(0, 0, 8 * 100, 8 * 100), full_image_texture)
VisualServer.canvas_item_set_parent(ci_rid, get_canvas_item())
This uses about 10% cpu and runs at max fps. Speedup of at least 1000% (probably more).
I'm aware that the second version would need a way to manually trigger a redraw as soon as anything changes. But for a pretty static object like a TileMap, this looks like a very reasonable optimization.
So, here comes the question:
Is it somehow possible to generalize this optimization over all CanvasItems? E.g. some kind of Node that pre-rasterizes all its children and draws the result. I tried to use a Viewport with a child TileMap (like mentioned in the docs):
extends Node2D
onready var viewport := Viewport.new()
onready var sprite := Sprite.new()
export var size := Vector2(0, 0)
export var offset := Vector2(0, 0)
func _ready():
# reparent all children into the viewport
for c in self.get_children():
self.remove_child(c)
self.viewport.add_child(c)
# set up dimensions
self.viewport.size = self.size
self.viewport.global_canvas_transform = self.viewport.global_canvas_transform.translated(-self.offset)
self.viewport.render_target_v_flip = true
# the children need to share physics space with everything else. this does not work!
self.viewport.world_2d = self.get_viewport().world_2d
self.add_child(self.viewport)
self.add_child(self.sprite)
self.redraw()
func redraw():
self.viewport.render_target_update_mode = Viewport.UPDATE_ONCE
self.sprite.texture = self.viewport.get_texture()
The problem here is that the viewport will always render directly on-screen if i set its world to the root's world. But I need to share the physics space (I want to use a TileMap, do collisions etc.). I don't know if it is possible for a viewport to use the root's physics space but its own canvas.
Any ideas about that? Thanks for reading this long post :)