• 2D
  • How do you erase during a _draw() call? (Blend modes?)

I've been beating my head against this for a few days now. How do you erase pixels during a draw call?

My first idea was to add a CanvasItemMaterial to the node doing the drawing and then switch blend modes. But switching blend modes seems to have no effect at all. (I've been testing with a simple large black circle and large white circle.) And it doesn't seem to matter what I set the blend mode to, it just draws the circle on top exactly the same as it does by default, with no material.

So, I guess this is conditional question. If blend modes are the proper way to erase parts of a draw call, then how are you actually meant to implement blend modes? What do you have to do to make blend modes have an effect during a draw call?

Or, if there's a better or different way to set the pixels in an area to 0.0 alpha, what is it and how do you do it?

I'm thinking the issue is that you need to do your data processing before you draw. Once you have already drawn something to the buffer it is in there, unless you clear the buffer and start over again. Makes sense?

Now when you say:

@Ephemeral said:

Or, if there's a better or different way to set the pixels in an area to 0.0 alpha, what is it and how do you do it?

What exactly do you mean, what are we talking about here? What node are you using here and what exactly are you trying to make transparent on it?

That doesn't really make sense, sorry. I don't follow at all what you mean by data processing.

As for what node is drawing... should that even matter? I don't see why it would, but in this case it is a Node2D drawing to the viewport of which it is a child.

All I want to do is draw transparency.

The first thing I tried was

_draw():
    # several draw_ functions that behave as expected
    material.blend_mode = material.BLEND_MODE_SUB
    draw_circle(...)
    material.blend_mode = material.BLEND_MODE_MIX

but this has zero effect. Why doesn't changing the blend mode... actually change the blend mode?

anything you feed to the draw call would be it's input data. draw_circle would be an example of a draw call.

Logically the material.blend_mode = material.BLEND_MODE_SUB would apply to the following draw_circle() in your example above. The last line in your example has yet to apply to anything since you haven't actually drawn anything with it.

Um, yes. That is exactly my logic as well.

MIX is the default, correct? So that line is just to reset it for subsequent drawing.

The important part is that draw_circle() is not affected. I get the same result regardless of what I set the blend mode to on the line before draw_circle()

So what are you doing within the draw_circle( > here < )?

What color are you using for it, and can you provide a screenshot of it's output, with possibly a image mock-up of what you expect it to output?

I was testing with both white and black.

I have a bunch of draw functions that draw a whole bunch of stuff. Then at the end, I change the blend mode and then draw the two circles large and covering most of the drawn area for testing purposes. Both the white circle and the black circle appear as solid opaque color, the same solid opaque color.

The goal is to be able to take pixels that had previously had something drawn on them, and set the alpha of those pixels back to 0.0, erasing from the area drawn to. Drawing a circle with (0.0, 0.0, 0.0. 1.0) in SUBtract mode, should logically produce a blank area cut out of the middle of the image, but it doesn't, it just produces a solid opaque black circle.

@Megalomaniak said: can you provide a screenshot of it's output

You're not really drawing your circles over each other. Might want to start off by just keeping you background 50% gray and drawing your circles with overlap. See what results you get with that.

I wonder if setting it back to BLEND_MODE_MIX is causing the issue. It could be that the material's blend mode is not necessarily applied when the draw_circle function is called, but rather at a later point (maybe it is queued or something). That might explain why all three images have the same result, as the material blend mode used for drawing the circles on the screen is not changing.

Maybe try removing the line that changes it back to BLEND_MODE_MIX and see if that makes a difference?

@Megalomaniak said: You're not really drawing your circles over each other. Might want to start off by just keeping you background 50% gray and drawing your circles with overlap. See what results you get with that.

Could you rephrase this? I have no idea what you're talking about. Of course I'm not drawing the circles over each other, and the background has to be transparent or there's no point. I'm trying to erase. (Specifically, I want to cull the blurred edge that is drawn outside the tile.)

@TwistedTwigleg said: Maybe try removing the line that changes it back to BLEND_MODE_MIX and see if that makes a difference?

Alright, this actually just causes the entire _draw() to draw nothing at all. This is a clue, though, so I tried a couple of other things. It turns out it doesn't matter where you set the blend mode. I tried putting SUB at the very top of the draw function and MIX at the very bottom, and the entire image was still drawn in MIX mode.

It seems like the entire draw function is invisibly cached, and only then rendered to the viewport in whatever blend mode is active after the _draw() call is complete.

So that means the blend mode approach fundamentally doesn't work, and the fundamental question changes. I can think of a work around for this already but it is horrifically convoluted. So, I guess now my other question remains. Forget about swapping blend modes on the fly, what other ways are there to erase? Is the _draw() cache image data accessible somehow?

Hmm, interesting. I wonder why that is. I knew the _draw function is cached, as in the documentation it mentions you need to call the update function so the node will be redrawn. I didn't know the caching applies to materials though.

I dunno a solution right off, but I'll think about it. Right now the only thing I can think of is maybe create a Node2D child that has the BLEND_MODE_MIX and another that has BLEND_MODE_SUBTRACT and somehow send data from one to another. That said, I'm wondering if there is an easier way to achieve the same effect...

@Ephemeral said:

@Megalomaniak said: You're not really drawing your circles over each other. Might want to start off by just keeping you background 50% gray and drawing your circles with overlap. See what results you get with that.

Could you rephrase this? I have no idea what you're talking about. Of course I'm not drawing the circles over each other, and the background has to be transparent or there's no point. I'm trying to erase. (Specifically, I want to cull the blurred edge that is drawn outside the tile.)

The assumption I made was that if each of the draw_circle acts as a draw call in itself then they might only work within the draw function and their combined effect might be what gets to influence outside the draw func. In that case and towards testing that I recommended to try drawing the circles overlapping each other partially to see if there is an effect.

We've established that what's happening is because of the way that Godot caches all of the code in a draw() call without applying the material, and then only afterwards, when rendering that cached data to the viewport in one go, applies the material. Materials cannot be applied inside draw().

Now that I know this, I know I was taking the wrong approach.

I still don't know what the right approach would be, though. I have a couple ideas for hacky work-arounds, but is there really no cleaner way? I refuse to believe GMS2 actually has Godot beat at literally anything, and this was straightforward in GML.

Well, I'm still not clear on what you want to achieve with it. Do you wish to clear pixels on a tile level in what looks like a tile-map, or do you wish to do that for the whole display buffer, or what?

I don't know how else to explain that I want to be able to erase part of what I draw. If I didn't need a transparent background, it would be simple. I could just draw the background color over what I wanted to 'erase' with draw_colored_polygon(), but to actually erase from a transparent surface I need a way to make pixels have 0.0 alpha.

More importantly, I need a way to do it inside the _draw() call, because the place in the code where I'd want to use this happens to be inside a loop. (I'd need to erase from the first row before I draw the second, erase from the second row before I draw the third, etc.)

So using a shader material would be out of the question?

edit: wait, earlier when you mentioned CanvasItem Material, did you mean CanvasItem Shader? If so, this changes everything. You want to keep it to the MIX blendmode and write your black and white values to the fourth component of the fragment shader COLOR output which happens to be (RGBA) alpha.

If in a CanvasItem _draw() however, you could try draw_colored_polygon with a viewport/screen texture fed to it.

I did not mean a Shader, I indeed meant a Material. I did not confuse them.

How would feeding an empty viewport texture to the draw_cirle(...) be different than just feeding it a Color with 0 alpha? The cache it draws to seems to operate in MIX mode with no way of changing that, so it just wouldn't draw anything.

From what I can tell from my tests, it appears the alpha channel of the material/shader doesn't apply with the mix/sub/etc modes. I'm not totally sure why, but my guess is that the alpha channel is processed differently than the color.

You might need to write a custom shader and use a Viewport texture as the input. I wrote a tutorial on RandomMomentania that might help as a reference.

@Ephemeral said: How would feeding an empty viewport texture to the draw_cirle(...) be different than just feeding it a Color with 0 alpha? The cache it draws to seems to operate in MIX mode with no way of changing that, so it just wouldn't draw anything.

You wouldn't be feeding an empty Viewport node the texture, you'd instead render what the Viewport sees and use that as an input for the Shader. All of the shapes/things you want to use for removing pixels would be in the Viewport node. That way you could selectively mask/remove pixels in the main scene through a simple shader. That said, you'd need to separate the code that draws the scene and the code that uses draw_circle(...) into separate nodes so the node that uses draw_circle can be a child of the Viewport node.

The tutorial I linked above shows how this works in 3D. The same principles could be applied to 2D as well, you'd just need to make some minor modifications. I could probably make a quick example if you need.

That said, if you need layered rendering, where you draw one layer and remove pixels before drawing the next layer that (potentially) overlaps the first, then I'm not totally sure on what the best way to go about it would be. The easiest way I can think to work around it is to either draw each layer using separate Node2D nodes, or find a way to not draw the pixels you are wanting to remove altogether.


You mentioned that Game Maker Studio 2 can do what you are wanting to achieve, right? Do you mind linking to the Game Maker Studio 2 feature so we can look at it? It might help us understand what you are trying to achieve, which may help us find a potential solution.

@TwistedTwigleg said: You mentioned that Game Maker Studio 2 can do what you are wanting to achieve, right? Do you mind linking to the Game Maker Studio 2 feature so we can look at it? It might help us understand what you are trying to achieve, which may help us find a potential solution.

Yeah, that is something I with I had thought to ask, TBH. :sweat_smile: