Erasing Pixels in 2D Light

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

I’m looking for a way to erase pixels when they’re exposed to light in the 2d shader processor. The idea is an “outline” on sprites that fades out in light, giving the sprite a more defined shadowy affect. This isn’t to replace an occluder, but to give sprites a bit more definition.

I’ve figured out how to get it to draw on the opposite side the light is shining and that looked nice at first but that breaks with multiple lights. You can see this affect working as intended in this tweet(I was listening to music, may or may not wanna mute, isn’t loud either way):
https://twitter.com/TyoTheCreator/status/1361212230105264128?s=19

Here that second light was an experiment in making a backlight - if you happen to have an elegant solution for that as well, I’m listening!

The funkyness can be seen in this image with two proper lights:

I can see why this is happening. My shader simply flips the light vector, so instead of drawing where the light shines, it draws in the opposite vector… meaning, if two lights shine, both vectors get drawn. However, I don’t know what to do about this. The Shadow properties of the Light processor didn’t seem to do anything for me, and I can’t think of a way to tell the shader to erase the pixels facing the light instead of drawing the pixels away from the light.

The shader itself is super simple:

shader_type canvas_item;

void fragment() {
    // Get the vertex color or the color from the texture if set
    vec4 finalColor = min(texture(TEXTURE, UV), COLOR);
    COLOR = finalColor;
    if(!AT_LIGHT_PASS) {
        COLOR = vec4(0, 0, 0, 0);
    }
}

void light()
{
    LIGHT_VEC.x = -LIGHT_VEC.x;
    LIGHT = COLOR + vec4(0,0,.5,2); //just makes it slightly bluer and defined
}

I appreciate any help anyone offers for this!

So far, all I’ve found is an alternative way to do my initial implementation without the need of a shader. It’s the same logic, though. I can use an “inverse” light texture and the “mask” light mode to make a light which “shines” from the edges of the texture and dims inwards. This leads to the same outcome, though. Since the pixels are being drawn and not erased, if two lights are made on either end of the sprite an almost full outline will show, rather than the desired effect of having almost no outline.

tyo | 2021-02-20 05:12

:bust_in_silhouette: Reply From: tyo

SO I finally figured out a way to do this. It is quite a bit hacky, however because of what I can only think are certain limitations of the light processor this is the only way I managed.

I had to send my light positions to my fragment shader, and from there do some custom logic for the lights.

shader_type canvas_item;

uniform vec2 light_position0;
uniform vec2 light_position1;
uniform vec2 light_position2;

vec4 calc_light(vec2 light_pos, sampler2D tex, vec2 uv, vec4 col, vec2 point_coord, vec3 norm)
{
	vec2 temp_pos = vec2(light_pos.x / 100.0, light_pos.y / -100.0);
	vec4 fin_color = min(texture(tex, uv), col);
	col = fin_color;
	vec2 light_vec = normalize(temp_pos - point_coord);
	float brightness = dot(norm.xy, light_vec) * 2.0;
	float fade = max(distance(temp_pos, point_coord), 1.0);
	brightness = max(fin_color.a - brightness / fade, 0.0);
	return vec4(fin_color.rgb, brightness);
}

void fragment() {
	vec2 light_positions[3] = {light_position0, light_position1, light_position2};
	int j = 0;
	for (int i = 0; i < light_positions.length(); i++)
	{
		if(light_positions[i] != vec2(0.0))
		{
			COLOR = COLOR * calc_light(light_positions[i], TEXTURE, UV, COLOR, POINT_COORD, NORMAL);
		}
		else
		{
			j++;
		}
	}
	if(j == light_positions.length())
	{
		COLOR = min(texture(TEXTURE, UV), COLOR)
	}
}

the j variable is for drawing the outline in the absence of all lights. With this implementation, I have to manually add the light positions to the shader. I think this is acceptable given I probably won’t use more than a handful of lights affecting any one object, so I can just define some maximum number of lights the shader can handle and feed it as many lights are “in range” at any given time. Of course this shader will completely ignore the light’s other parameters, but I also see this as acceptable as outlines shouldn’t really need to get anymore complicated. The most I’ll do from here is perhaps also feed the shader each light’s Energy in order to affect the brightness as well.

The shader in action below(as before, light audio):
video