Easiest way to use TileMapLayers as Sprite3D Nodes?

Godot Version

Godot 4.6 (or any future version, I can update if needed)

Question

Hello all! I’m pretty new to Godot, and I’ve been making a really simple prototype for a Hollow Knight-esque environment. My current setup uses a SubViewport + a 2D scene to make the main gameplay layer, which uses a TileMapLayer to build out the 2D layer of the world. The 3D elements (foreground + background layers) of the scene use Sprite3D assets. I opted for this 2D/3D hybrid to take advantage of 3D lighting techniques + because parallax code was not as specific as I was looking for.

When making a background, I was wondering if there was any non-SubViewport way to use TileMapLayers as the texture of a Sprite3D, or some equivalent. I would like to avoid creating a new scene + SubViewport with an additional TileMapLayer just for background decor, especially when I could see myself easily using 25+ layers, and I could see that many SubViewports impacting performance. (That being said, as I’m typing this, I guess setting those SubViewports to ‘Update Once’ mode could negate some of those issues, as they likely would remain static)

I ultimately am asking this because I:

A) feel like there is definitely a better way to manage my scene tree rather than having 25+ subviewports just to display a simple tilemap

B) would definitely having a hard time coordinating these TileMapLayers (lining up elements between scenes, in-editor scene management, etc). Ideally I could paint multiple TileMapLayers on top of each other (as one would do in a regular and/or Parallax2D setting), so that they line up perfectly, and then use them separately as textures for Sprite3D nodes.

C) feel as if there should be/probably is some quick plugin/built-in function/addon (not super familiar with these at all yet) that simply lets me drag + drop the ‘TileMapLayer’ node into the Sprite3D texture space.

Any suggestions, plugins, tutorials, organization methods, etc., are welcome! I just don’t want to make fundamental organization decisions on something that could potentially be super hard on performance without weighing my options first! Thanks!

No, there’s no way of using a TilemapLayer as the texture of a Sprite3D without a SubViewport.

You could build your Sprite3D layers from a 2D scene where you have all your tilemaps lined up at runtime. Imagine you have a scene tilemaps.tscn like:

Node
    TilemapLayer1
    TilemapLayer2
    TilemapLayer3

Then, you’d do the following (untested code):

func create_background() -> void:
    # Instantiate the tilemaps
    var tilemaps = load("res://tilemaps.tscn").instantiate()
    
    # For each tilemap
    for tilemap in tilemaps.get_children():
        # Create the subviewport
        var subviewport = SubViewport.new()
        
        # configure the subviewport as needed
        
        # reparent the tilemap to the new subviewport
        tilemap.reparent(subviewport)
        
        # Create the Sprite3D, add the subviewport as a child of it, and assign the subviewport's texture to it
        var sprite_3d = Sprite3D.new()
        sprite_3d.add_child(subviewport)
        sprite_3d.texture = subviewport.get_texture()
        
        # Add the Sprite3D to the scene
        add_child(sprite_3d)
        
    # Clean up the tilemaps
    tilemaps.queue_free()

Another option, if the size of the final background isn’t too big and they are static and won’t update, that avoids subviewports would be to blit the TilemapLayer into an Image and ImageTexture. For example (untested code):

func create_background() -> void:
    # Instantiate the tilemaps
    var tilemaps = load("res://tilemaps.tscn").instantiate()
    
    # For each tilemap
    for tilemap in tilemaps.get_children():
    
        var tileset = tilemap.tile_set
    
        # Get the image size
        var image_size = tilemap.get_used_rect()
        # And create it empty
        var image = Image.create_empty(image_size.x, image_size.y, true, Image.FORMAT_RGB8)
        
        # For each cell used in the tilemap
        for cell_id in tilemap.get_used_cells():
            # Get its TileSetSource (assumes that it will be an TileSetAtlasSource)
            var source = tilemap.get_cell_source_id(cell_id)
            var atlas = tileset.get_atlas_source(source)
            
            # Find the region in the texture atlas tile cell uses
            var atlas_coords = tilemap.get_cell_atlas_coords(cell_id)
            var region = atlas.get_tile_texture_region(atlas_coords)
            var atlas_image = atlas.texture.get_image()
            
            # Blits that region into our final image
            image.blit_rect(atlas_image, region, cell_id * tileset.tile_size)
            
        # Create the Sprite3D, assign a ImageTexture, and add the node to the scene
        var sprite_3d = Sprite3D.new()
        sprite_3d.texture = ImageTexture.create_from_image(image)
        add_child(sprite_3d)
        
    tilemaps.queue_free()

Again, this option has some limitations. Using subviewports is more flexible.

Thank you for the help! I’ll have to test this out later in the day when I get the chance, but these definitely are helpful! That first solution 100% makes the in-editor side of things much less daunting! Also, as of writing the original post, hadn’t tested out an implementation with SubViewports, so my worries of performance issues may have been exaggerated :sweat_smile::sweat_smile:.

I’ll try to implement this soon, and I’ll label this post as resolved when it works! Thanks again!