Search code examples
godotgdscriptgodot4

How to capture viewport image in tool mode?


I'm trying to capture images of MeshInstance3D via SubViewport and assigning them as textures to sprites as such:

enter image description here

@tool
extends Node2D

const PIXEL_PER_METER = 100.0
@export var btn := false : set=set_btn

func set_btn(new_val):
    if(new_val):
        assign_images()

func assign_images():
    var sub_viewport=$SubViewport
    var camera=$SubViewport/Camera3D
    var mesh_instances=[ $SubViewport/A, $SubViewport/B, $SubViewport/C ]
    var sprites=[ $A, $B, $C ]
    
    sub_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
    camera.projection = Camera3D.PROJECTION_ORTHOGONAL
    
    for i in len(mesh_instances):
        var sprite=sprites[i]
        var mesh=mesh_instances[i]
        var mesh_aabb=mesh.get_aabb()
        camera.size = max(abs(mesh_aabb.size.x), abs(mesh_aabb.size.y))
        camera.global_position = Vector3(mesh.global_position.x, mesh.global_position.y, camera.global_position.z)
        sprite.position = Vector2(mesh.position.x * PIXEL_PER_METER, mesh.position.y * -PIXEL_PER_METER)
        #await get_tree().process_frame  # this doesn't work either
        sprite.texture=ImageTexture.create_from_image(sub_viewport.get_texture().get_image())

How ever this doesn't work, it doesn't capture the right image, nor does it place the sprite at right position relative to it's mesh counterpart:

enter image description here

For the image capture part, I suspect SubViewport & Camera3D isn't being updated quickly enough hence it captures the wrong image.

As for the position part, I believe 1 m in 3D translates to 100 pixels in 2D, maybe due to the unit differences the size & position of image is wrong.

So how do I implement this properly?

Note: I'm open to an alternate approach as long as it gives the same result, and no MeshInstance2D does not work

Minimal reproduction project (MRP)


Solution

  • I figured it out, just needed to add await RenderingServer.frame_post_draw like this:

    @tool
    extends Node2D
    
    const PIXEL_PER_METER = 100.0
    @export var btn := false : set=set_btn
    
    func set_btn(new_val):
        if(new_val):
            assign_images()
    
    func assign_images():
        var sub_viewport=$SubViewport
        var camera=$SubViewport/Camera3D
        var mesh_instances=[ $SubViewport/A, $SubViewport/B, $SubViewport/C ]
        var sprites=[ $A, $B, $C ]
        
        sub_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
        camera.projection = Camera3D.PROJECTION_ORTHOGONAL
        
        for i in len(mesh_instances):
            var sprite=sprites[i]
            var mesh=mesh_instances[i]
            var mesh_aabb=mesh.get_aabb()
            camera.size = max(abs(mesh_aabb.size.x), abs(mesh_aabb.size.y))
            camera.global_position = Vector3(mesh.global_position.x, mesh.global_position.y, camera.global_position.z)
            sprite.position = Vector2(mesh.position.x * PIXEL_PER_METER, mesh.position.y * -PIXEL_PER_METER)
        
            await RenderingServer.frame_post_draw  # added this line
        
            sprite.texture=ImageTexture.create_from_image(sub_viewport.get_texture().get_image())