2D-perspective
This shader “fakes” a 3D-camera perspective on CanvasItems.
The shader works out-of-the-box with nodes Sprite and TextureRect, as long as the rect_size equals the size of the texture. If this isn’t the case, you can do the following change:
//VERTEX += (UV - 0.5) / TEXTURE_PIXEL_SIZE * tang * (1.0 - inset);
// to (rect_size is a uniform):
VERTEX += (UV - 0.5) * rect_size * tang * (1.0 - inset);
Also, remember to enable mipmaps and anisotropic for the texture to retain quality with harsh angles.
Shader code
// Hey this is Hei! This shader "fakes" a 3D-camera perspective on CanvasItems.
// License: MIT
shader_type canvas_item;
// Camera FOV
uniform float fov : hint_range(1, 179) = 90;
uniform bool cull_back = true;
uniform float y_rot : hint_range(-180, 180) = 0.0;
uniform float x_rot : hint_range(-180, 180) = 0.0;
// At 0, the image retains its size when unrotated.
// At 1, the image is resized so that it can do a full
// rotation without clipping inside its rect.
uniform float inset : hint_range(0, 1) = 0.0;
// Consider changing this to a uniform and changing it from code
varying flat vec2 o;
varying vec3 p;
const float PI = 3.14159;
// Creates rotation matrix
void vertex(){
float sin_b = sin(y_rot / 180.0 * PI);
float cos_b = cos(y_rot / 180.0 * PI);
float sin_c = sin(x_rot / 180.0 * PI);
float cos_c = cos(x_rot / 180.0 * PI);
mat3 inv_rot_mat;
inv_rot_mat[0][0] = cos_b;
inv_rot_mat[0][1] = 0.0;
inv_rot_mat[0][2] = -sin_b;
inv_rot_mat[1][0] = sin_b * sin_c;
inv_rot_mat[1][1] = cos_c;
inv_rot_mat[1][2] = cos_b * sin_c;
inv_rot_mat[2][0] = sin_b * cos_c;
inv_rot_mat[2][1] = -sin_c;
inv_rot_mat[2][2] = cos_b * cos_c;
float t = tan(fov / 360.0 * PI);
p = inv_rot_mat * vec3((UV - 0.5), 0.5 / t);
float v = (0.5 / t) + 0.5;
p.xy *= v * inv_rot_mat[2].z;
o = v * inv_rot_mat[2].xy;
VERTEX += (UV - 0.5) / TEXTURE_PIXEL_SIZE * t * (1.0 - inset);
}
void fragment(){
if (cull_back && p.z <= 0.0) discard;
vec2 uv = (p.xy / p.z).xy - o;
COLOR = texture(TEXTURE, uv + 0.5);
COLOR.a *= step(max(abs(uv.x), abs(uv.y)), 0.5);
}

Very good shader, I was looking for something like this.
Just one question, everything moves taking the center of the sprite as the center point, can that be changed?
Thank you for this.
I’m not very active here so didn’t see your comment sooner, sorry about that!
I’m not sure if this is still relevant for you, but the short answer is, yes! The long answer is… it’s not worth it to do it “properly”, as it would make the shader inefficient to the point you might as well use a real 3d-object and -camera.
That said, here’s a simple “improper”, version:
shader_type canvas_item; // Camera FOV uniform vec2 rotation_pivot = vec2(0.5); uniform float fov : hint_range(1, 179) = 90; uniform float y_rot : hint_range(-180, 180) = 0.0; uniform float x_rot : hint_range(-180, 180) = 0.0; // At 0, the image retains its size when unrotated. // At 1, the image is resized so that it can do a full // rotation without clipping inside its rect. uniform float inset : hint_range(0, 1) = 0.0; // Consider changing this to a uniform and change it from code varying mat3 rotmat; varying float tang; const float PI = 3.14159; // Creates rotation matrix void vertex(){ float sin_b = sin(y_rot / 180.0 * PI); float cos_b = cos(y_rot / 180.0 * PI); float sin_c = sin(x_rot / 180.0 * PI); float cos_c = cos(x_rot / 180.0 * PI); rotmat[0][0] = cos_b; rotmat[0][1] = sin_b * sin_c; rotmat[0][2] = sin_b * cos_c; rotmat[1][0] = 0.0; rotmat[1][1] = cos_c; rotmat[1][2] = -sin_c; rotmat[2][0] = -sin_b; rotmat[2][1] = cos_b * sin_c; rotmat[2][2] = cos_b * cos_c; tang = tan(fov / 360.0 * PI); VERTEX += (UV - 0.5) / TEXTURE_PIXEL_SIZE * tang * (1.0 - inset) * 2.0; // Since we can, why not precalculate this too. tang = 0.5 / tang; } void fragment(){ // p is a vector from camera origo to camera nearplane. vec3 p = vec3(UV - 0.5, tang); vec3 plane_offset = vec3(vec2(0.5) - rotation_pivot, tang + 1.); vec3 plane_normal = rotmat[2]; float dot_prod = dot(plane_normal, p); if (dot_prod <= 0.0) discard; float fac = dot(plane_normal, plane_offset) / dot_prod; vec2 uv = (inverse(rotmat) * (p * fac - plane_offset)).xy; uv += vec2(0.5) - rotation_pivot; if (any(greaterThan(abs(uv), vec2(0.5)))) discard; COLOR = texture(TEXTURE, uv + 0.5); }It can manage itself up to a certain level of rotation, otherwise, it will begin clipping. (though you could manually scale the vertex size even bigger). Once again if you want to render proper 3D-2D objects, it is better to, well, render proper 3D objects.
Thanks! it works very well.
Thanks for your shader! But I’m not very understand about how the shader works, would you like to explain it a little bit? And I came up some issue about this shader, very appreciate for any advice. Thanks!
me too
This shader is amazing, I was looking for something exactly like this, thank you so much for making it!!
One small request though, is it possible to make it compatible with a NinePatchRect node? I would really need it for a node like that, but the texture looks and behaves weirdly with it.
Awesome shader, thank you!
great, but how would one do this toe the entire camera scene?
incredible shader, works flawlessly, exactly what i needed !!!!!
cool shader but not work fine under tilesprite texture 🙁
Hey, just for anybody running into the issue I was –
If you are having problems with dynamically sized textures getting scaled improperly by the shader… Just set the FOV property on the shader to 1. Maybe this was obvious to some but I lost a full day of work to this, due to my inexperience with shaders! Hope this can help somebody avoid my fate. Love the shader by the way! Happy shading 🙂
really doesnt work with text sadly!!! 🙁
You can try to have a structure like so:
SubViewportContainer
-SubViewPort
—-RichTextLabel
applying the shader to the subviewportcontainer. This has its own issues but works somewhat successfully.
I don’t get it. I’m applying this directly to a Sprite2D and nothing happens. No errors, nothing.
Am I missing something?
Assuming you’ve already set this as a shader on the Sprite2D under Inspector > Material, you can next go to Material > Shader Params and then start sliding the x_rot/y_rot up and down. You should see your sprite rotate in the scene preview.
To get it working in practice, you need to go into this sprite’s script and call $Sprite2D.material.set_shader_parameter(“x_rot”, some_value). What that value is depends on your use case. E.g., if you want it to move on mouseover, you need to call it from _on_gui_input and have some logic based on the mouse event. Hope that helps.
I hate to have a weird use case for this, but is there any way this can be done as a screen-reading shader? https://docs.godotengine.org/en/stable/tutorials/shaders/screen-reading_shaders.html I am trying to apply it to text in my game, but the only way to apply shaders to text is to use screen reading shaders. I can’t replace UV with SCREEN_UV in the vertex shader
maybe this would help somehow? https://www.reddit.com/r/godot/comments/15jvra2/how_to_apply_perletter_shaders_to_a_richtextlabel/
screen reading shaders with vertex is just impossible … whelp
workaround https://www.reddit.com/r/godot/comments/1ardm63/2d_perspective_shader_not_working_for_label_node/
Unfortunately i can’t really get it to work for a sprite node that as its texture has a portion of a sprite atlas such as a signle frame of a sprite animation… even with the modification pointed out it rotates as if it where the whoel spritesheet but only showing one cell of it