How do I mask a Node2D using dynamically drawn shapes?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By MickaelH

I need to dynamically draw some circles and then use those circles to mask a texture underneath.

So, first thing I did was create a shader which masks the alpha channel of a CanvasItem using the red and alpha channels of a texture passed as parameter.

shader_type canvas_item;
uniform sampler2D mask;
void fragment() {
    vec4 color = texture(TEXTURE, UV);
    vec4 vmask = texture(mask, UV);
    color.a = vmask.r * vmask.a;
    COLOR = color;

Then I started looking for a way to turn the result of a draw function into a texture, and that’s where things started going south.

First thing I tried was to use the get_texture() from Viewport to get an image. This method has 4 major issues:

  1. You have to visibly draw on screen
  2. The screenshot takes EVERYTHING in the Viewport, not just what you draw
  3. You have to wait for the screen to be rendered to take your screenshot
  4. The screenshot takes the WHOLE screen, not just the part that you draw

To solve problems 1 and 2, I created a separated Viewport with its own World in which to draw my circles:

var _world:World2D =
var _viewport:Viewport =

func _ready():

Then I tried sending the texture generated from Viewport.get_texture() to the main scene using signals. Problem: the texture is sent but I can’t display it. Like this code:

func _my_signal_listener(my_viewport_texture:ImageTexture):
    $mask_ctn.texture = preload("res://assets/some_test_image.png")

Displays an image on screen. But this code:

func _my_signal_listener(my_viewport_texture:ImageTexture):
    $mask_ctn.texture = my_viewport_texture

Displays absolutely nothing…
Now this is where it gets weird. This is the scene in which I drew my circles:

extends Node2D

signal texture_generated(texture)

var _blobs_pos:Array = [Vector2(200, 200), Vector2(200, 270)]
var _texture:ImageTexture

func _ready():
    yield(get_tree(), "idle_frame")
    yield(get_tree(), "idle_frame")

    var img = get_viewport().get_texture().get_data()

    _texture =

    $capture.texture = _texture
    emit_signal("texture_generated", _texture)

    # remove the draw shapes, only the sprite should remain
    _blobs_pos = []

func _draw():
    for pos in _blobs_pos:
        draw_circle(pos, 100, Color8(255, 0, 0))

    for pos in _blobs_pos:
        draw_circle(pos, 80, Color8(0, 255, 0))

When I play this scene directly, I can see my circles drawn on the scene. I can see my texture, so I know that what is being sent along with the “texture_generated” signal is consistent… So why do I receive an empty texture in the listener?

I also tried to ignore the problems 1 and 2 and see if I could at least make it work on an empty scene, and then it works. But the conditions in which it works makes it completely unusable…

And… that’s it. That’s where I’m at. Every solution that I’ve tried has created more problems than it solved, and I’m completely short on ideas…

It’s even more nerve-racking for me considering that I used to be a Flash developer, and in Flash, this problem would be solved in 2 lines of code:

my_mask_circles.cache_as_bitmap = true;
my_image.mask = my_mask_circles;

Thanks in advance to anyone who can help me understand and/or fix this mess :slight_smile:

Here’s a test project showing the issue:

Not need to use signals to connect the viewport texture to your sprite. So I assume you have a viewport with all the circle drawing stuff as children. As a first test, I’d create a fresh sprite and set its texture to “New Viewport Texture” and select the viewport. Does that display the circles as desired? If so, you can move on to the original sprite with the shader. In this case you don’t set the actual sprite texture, but under “Shader params” you should see your uniform sampler2D mask and that’s where you can select “New Viewport Texture” now. If the shape of your mask and the texture are the same, you can use UV directly to lookup the fragment in the mask. Otherwise you need the work-around mentioned here.

bluenote | 2019-12-30 13:58