Creating a 2D sprite skin based on a UV map

Godot Version

4.2.1.stable.mono

Question

I will preface this by saying I am completely new to shaders. I have been trying to implement a shader for my 2D player sprite that maps a color from a “skin texture” to the player sprite, based on the sprite’s RGBA value. This is based on Aarthificial’s popular youtube video, so check it out and credit to him:

Check his pinned comment for his more detailed explanation. He probably does a better job than me.


I have been trying to port his shader, which he made in Unity’s visual shader, to a .gdshader. I think I have made it most of the way to a functional shader, but I seem to have reached the end of my ability.

I hope to be fairly descriptive so that those more skilled with shaders can help me easier, but also so that people can potentially use this thread as a resource in the future. I’ll first share some screenshots with descriptions:


Here is an example of a frame from the player sprite. I used the color picker on his nose to show the RGBA values of that pixel, because I will use it as an example throughout this post. They are (10, 60, 0, 255). These will be used as UV coordinates for the “skin texture”.



Here is an example of a skin. I have added a selection box from the bottom left corner of the texture to the bottom left corner of the nose pixel. The box is 10 x 60, which are the ‘r’ and ‘g’ values of nose pixel on the player sprite. Based on my understanding, this is how UV coordinates work in Godot. From the bottom left.




These are the gdshader I have written, the inspector, and the output. As you can see, the output is an all black silhouette. The shader is pretty self explanatory, but I will at least describe the UV scaling.

Shader:

shader_type canvas_item;

uniform sampler2D Skin;

void fragment()
{
    // collect RGBA of pixel from sprite texture
	vec4 map = texture(TEXTURE, UV); 
	
	// scale the UV to the 64 x 64 "skin" sprite
    vec2 scaledUV = (map.rg * 255.0 + 0.5) / 64.0;
	
	// debug to check if any given scaleUV is being calculated correctly
	//if (abs(scaledUV.r - 0.164) < 0.001 && abs(scaledUV.g - 0.945) < 0.001)
    //	COLOR = vec4(1.0, 0.0, 0.0, map.a);
	
	// collect the rgba from the 'skin' sprite based on the scaledUV
    vec4 color = texture(Skin, scaledUV);
	
	// assign original sprite transparency
    color.a = map.a;
	
	// set new color
    COLOR = color;
}

All the map.rg values will be in a range of 0 - 0.251, because my skin sprite is 64 x 64 and therefor the biggest possible RGBA pixel values would be (64, 64, 0, 255). Godot normalizes all the values to a scale of 0 - 1 (divide all numbers by 255), so the largest RGBA values possible on the player sprite would be (0.251, 0.251, 0, 1).

For the nose pixel example, the RGBA would be (0.039, 0.235, 0, 1). The ‘r’ and ‘g’ values are each multiplied by 255 to get them back up to 10 and 60. 0.5 is added, so we deal with the middle of the pixel rather than its edge. And then each value is divide by the length and width of the skin texture, in my case 64 for each.

The result is that the ‘r’ value for the nose pixel is equal to 0.164 (~10.5/64) and the ‘g’ value for the nose pixel is 0.945 (~60.5/64).



I have confirmed that these numbers are in fact being calculated, as shown by the nose turning red when I run this debug version of the script, so I assume my issue is to do with a step following the scaledUV calculation.

My best guess is that I am misunderstanding the following line:
vec4 color = texture(Skin, scaledUV)
I would assume that it would return the RGBA value of a pixel from the skin at the scaledUV coordinate, but as I said I am new to shaders.

Any insight would be greatly appreciated and let me know if I can provide any additional details.
Thanks!
stion here! Try to give as many details as possible →

2 Likes

UVs in Godot are from the top left, so your map texture has the wrong values in it. i.e. you want to be counting pixels from the top instead of pixels from the bottom. 10,4,0 instead of 10,60,0.

4 Likes

That was it! Made the change and went through the long process of switching my sprite sheet to the new RGBA values, and it now works as intended.

Just in case anyone is looking for how to do this and finds this post. Here is the new sprite sheet with the nose pixel RGBA being (10, 3, 0, 255).


You start at the top left corner and go to the top left corner of the pixel to get its ‘r’ and ‘g’ values.


Here’s the shader working. One sprite sheet for the animation and switching between a few different skins.
shader_showcase

Thanks again Timberjaw!

2 Likes

Here’s the finished shader:

shader_type canvas_item;

uniform sampler2D Skin; // assign skin sprite in inspector
uniform vec2 SkinDimension;  // assign length and width of skin sprite in editor

void fragment()
{
	vec4 map = texture(TEXTURE, UV);  // collect RGBA of pixel from sprite texture
	vec2 scaledUV = (map.rg * 255.0 + 0.5) / SkinDimension;  // scale the UV to the 64 x 64 "skin" sprite
	vec4 color = texture(Skin, scaledUV);  // collect the rgba from the 'skin' sprite based on the scaledUV
	color.a = map.a;  // set original sprite transparency
	COLOR = color;  // set new color
}
5 Likes

Glad it works! That’s a neat technique.

1 Like

Here’s a link to my follow-up post:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.