1
\$\begingroup\$

I've been trying to replicate the effect created in this guide (https://www.gdquest.com/tutorial/godot/shaders/silhouette-2d/) in Godot 4, but I haven't had any luck.

enter image description here

The problem seems to be that in Godot 4, Color values are bound from 0 to 1, rather than being unbounded like in Godot 3. The issue is that this makes it very hard to determine when the mask is visible because you cannot give the mask a non-standard color value.

One solution I had tried was checking if the screen color matched the texture color, but this was a no-go as a) my game has 2d lighting that affects color and b) my game has a pretty consistent color palette, leading to "false positives".

I was wondering if there was a different/better method to achieve this effect. Thank you for any help you can give!

EDIT: I'm adding a minimal repro of my code here.

\$\endgroup\$
3
  • \$\begingroup\$ Could you please share your current project, or a minimal reproduction of the problem? It's hard to know exactly what the issue is without seeing your code. (I believe you about the color bounds, but it's hard to suggest improvements without any source code.) \$\endgroup\$
    – Tanaki
    Jul 20, 2023 at 20:27
  • \$\begingroup\$ For sure! I made a minimal version of my attempt and added it to the above post. \$\endgroup\$
    – dahrasz
    Jul 21, 2023 at 18:30
  • \$\begingroup\$ Oops, I think that link is private, could you open up the access? \$\endgroup\$
    – Tanaki
    Jul 21, 2023 at 18:36

1 Answer 1

1
\$\begingroup\$

You've run into the problem that you can't generally share information between shaders - shaders have very limited output functionality. If shading the silhouette in screen space no longer works due to clamping in Godot 4 and unpredictable colors in your scene, I'd suggest shading the silhouette on the foreground nodes themselves (instead of on a character node). It's more annoying, since you have to apply the shader to every single (drawable) foreground node, but it will do exactly what you want.

To achieve this, we'll create a shader that takes the current character sprite and its game location in screen space. Similar to the GDQuest tutorial, we'll combine a solid color with the foreground texture based on the transparency of the character sprite - if it's transparent, we'll use the foreground texture, if not, the solid color. The math for this is a bit gnarly.

shader_type canvas_item;

// vec4(top_left_x, top_left_y, bottom_right_x, bottom_right_y)
uniform vec4 range;
// the sprite texture, which is being used as a mask in this shader
uniform sampler2D mask_texture;

void fragment() {
    vec4 texture_color = texture(TEXTURE, UV);
    // get the alpha channel of the mask based on the screen coordinates
    // (we have no access to game coordinates in shaders)
    // clamp so that we don't access data outside of the mask
    float mask_alpha = texture(mask_texture, vec2(clamp((SCREEN_UV.x - range.x) / (range.z - range.x), 0.0, 1.0), clamp((SCREEN_UV.y - range.y) / (range.w - range.y), 0.0, 1.0))).a;
    // set the alpha of this node's texture opposite to that of the mask
    texture_color.a = 1.0 - mask_alpha;
    // combine the node's texture and the silhouette color (white in this case)
    COLOR = vec4(1.0, 1.0, 1.0, 1.0) * mask_alpha + texture_color;
}

The current character sprite can be a ViewportTexture, just like the one set up in the GDQuest tutorial you linked. But for the character bounds, we need to make sure that the character's screen space location is up-to-date on every frame. We can do that with some custom logic in our game loop. Again, the math isn't pretty.

func _process(_delta):
    // get information on the viewport ("camera") and the character
    var viewport_scale := get_viewport_transform().get_scale()
    var viewport_size := get_viewport_rect().size
    var character_origin := ($Character as Node2D).get_global_transform_with_canvas().get_origin()
    var character_rect := ($Character/Sprite as Sprite2D).get_rect()
    // calculate the bounds of the character sprite
    var rect = Rect2(character_origin + (character_rect.position * viewport_scale), character_rect.size * viewport_scale)
    // normalise the bounds to screen space (a float between 0 and 1)
    var topleft = rect.position / viewport_size
    var bottomright = rect.end / viewport_size
    var range = Vector4(topleft.x, topleft.y, bottomright.x, bottomright.y)
    // update the shader
    (($Foreground as Sprite2D).material as ShaderMaterial).set_shader_parameter('range', range)

This creates an effect like this:

a silhouette shader

Here is a link to a sample project - you can move the character with the arrow keys to see how the silhouette changes.

Be warned that this is a rough implementation and will likely require some cleanup to work with your game. Camera attributes, screen settings, viewport usage, and more could change how the math needs to work to calculate screen space positions.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Hmm... that makes a lot of sense but I'm not sure it will work for my use case, as objects are not consistently in the foreground at any given time and I am not just using this shader for one character but multiple at the same time. I will probably keep looking for alternate implementations or styles of showing the character behind the background. Thank you for the help! \$\endgroup\$
    – dahrasz
    Jul 22, 2023 at 23:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .