Creating a outline shader: I am confused about datatypes
Asked Answered
G

4

0

I am following the GDQuest shader secrets course and want to create an outline shader but I am confused about the datatypes.

The code so far looks like this:

shader_type canvas_item;

uniform vec4 line_color: source_color = vec4(1.0);
uniform float line_thickness: hint_range(0.0, 1.0) = 1.0;

void fragment() {
	vec2 size = TEXTURE_PIXEL_SIZE * line_thickness;

	float left = texture(TEXTURE, UV + vec2(-size.x, 0)).a;
	float right = texture(TEXTURE, UV + vec2(size.x, 0)).a;
	float up = texture(TEXTURE, UV + vec2(0, size.y)).a;
	float down = texture(TEXTURE, UV + vec2(0, -size.y)).a;
	
	float sum = left + right + up + down;
	// This value represents the black-and-white mask we'll use to outline the sprite.
	float outline = min(sum, 1.0);
	
	vec4 color = texture(TEXTURE, UV);
	// We apply the outline color by calculating a mask surrounding the sprite.
	COLOR = mix(color, line_color, outline - color.a);
}

And the one thing I do not get is the value that is stored inside of the floats inside the fragment function (left, right, up, down) and why they are added together.

As far as I understand the shader looks at a pixel and then samples the pixels to the left, right, top and bottom, then we store the alpha value in the variables. But wouldn't that just return the alpha value for those pixels, i.e. a value between 0 and 1? And then we add them together with a maximum of 1, I just don't get how this works and the explanations in the tutorial don't cover it. Can someone help?

Sorry for the vague question, basically what I am looking for:

what value is stored inside of the floats
why are the values added together

Gogetter answered 17/4, 2023 at 20:56 Comment(0)
S
0

Gogetter
Floats are preferred type in shaders because GPUs are highly optimized to work with them. So the shader implementations of various algorithms are typically adapted to floats even if integers would fit better.

Pixel color values in sprites/textures are commonly stored as integers in 0-255 range per channel. However when a shader reads that information it is returned as a floating point number in 0-1 range. We say that color information is normalized. It's done so because many math operations become easier to do when dealing with numbers in 0-1 range.

As for that line of code that baffled you; it needs to be considered with the line that follows. And it's basically shader's way of doing this:

float outline = 0.0;
if( left == 1.0 || right == 1.0 || up == 1.0 || down == 1.0 ){
	outline = 1.0;
}

To put it in words; if any of the sampled pixels is opaque then we set outline value to 1.0 (drawn), otherwise to 0.0 (not drawn). Note that the shader assumes that sprites's alpha is either 0 or 255 (so either 0 or 1 when sampled in the shader)

You'll se a lot of stuff like that in shaders, where conditional statements are cleverly replaced by some simple math, yielding the same result as conditional statement would. This is done to avoid branching in code. Since shader code runs in parallel for many pixels at the same time it's best to run the same code for all pixels. Running different branches of code for different pixels can diminish shader's performance.

Shippy answered 17/4, 2023 at 21:54 Comment(0)
G
0

Shippy thank you for the answer, that is super helpful.

I am still a bit confused, I think my main issue is that I don't get how the outline float is turned into a position. I have simplified the code:

shader_type canvas_item;

void fragment() {
	// gets current pixel size
	vec2 size = TEXTURE_PIXEL_SIZE * 5.0;

	// samples the left & right point of current pixel -> gets alpha value between 0.0 and 1.0 for each
	float left = texture(TEXTURE, UV + vec2(-size.x, 0)).a;
	float right = texture(TEXTURE, UV + vec2(size.x, 0)).a;
	
	// create outline 
	float outline = 0.0;
	
	// check if left or right pixel is transparent
	// sets the outline to 1.0 if either is not
	if (left == 1.0 || right == 1.0){
		outline = 1.0;
	}
	
	// get current pixel color
	vec4 color = texture(TEXTURE, UV);
	
	// interpolate between pixel color and line color 
	COLOR = mix(color, vec4(1.0), outline - color.a);
}

To go through it:

  1. we get the pixel size, that one is easy
  2. then we check if the left and right side of the current pixel is transparent or not. This returns a float between 0.0 and 1.0.
  3. we create the outline, also a float.
  4. we check if either the left or right side of the pixel is not transparent, then we set the outline value to 1.0
  5. we get the current pixel color
  6. We set a new COLOR using mix by mixing the current color with the target color (white in this case). outline - color.a makes sure that we only color in values that are transparent.

What I don't get is where pixels around the texture are being drawn. The code checks if the values are transparent and sets an outline to 1 or 0 if they are or aren't but I don't get how a float of 1.0 (the outline) draws around the texture.

Hope that makes sense.

Gogetter answered 18/4, 2023 at 8:46 Comment(0)
G
0

oh wait, I am an idiot! I just got it 😃
My mistake was that I forgot that we look at every pixel individually and was expecting some kind of vector but that is the wrong approach.
Thanks again for the help!

Gogetter answered 18/4, 2023 at 8:51 Comment(0)
S
0

Gogetter Yeah, dealing with shader code takes some getting used to. Best way to think about it is from the perspective of a single pixel. Shader code is effectively an implicit for loop that runs for every pixel.

Shippy answered 18/4, 2023 at 9:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.