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);
}
Live Preview
Tags
2d, perspective, skew
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Hei

guest

21 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Torguen
Torguen
4 years ago

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.

Torguen
Torguen
3 years ago
Reply to  Hei

Thanks! it works very well.

yikescloud
yikescloud
3 years ago

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!

ymh
ymh
2 years ago
Reply to  yikescloud

me too

AndreWharn
3 years ago

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.

mreliptik
2 years ago

Awesome shader, thank you!

John Miller
John Miller
2 years ago

great, but how would one do this toe the entire camera scene?

boko
2 years ago

incredible shader, works flawlessly, exactly what i needed !!!!!

minzojian
minzojian
1 year ago

cool shader but not work fine under tilesprite texture 🙁

ElusiveMoose
1 year ago

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 🙂

idoblenderstuffs
idoblenderstuffs
1 year ago

really doesnt work with text sadly!!! 🙁

AquaBaby
1 year ago

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.

Last edited 1 year ago by AquaBaby
Cstapleslewis
Cstapleslewis
1 year ago

I don’t get it. I’m applying this directly to a Sprite2D and nothing happens. No errors, nothing.

Am I missing something?

Mike Robideau
Mike Robideau
1 year ago
Reply to  Cstapleslewis

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.

caelohm
1 year ago

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

caelohm
1 year ago
Reply to  caelohm

screen reading shaders with vertex is just impossible … whelp

LazyGameDev
LazyGameDev
8 months ago

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

ZYF20220241
ZYF20220241
1 day ago

感谢你分享的着色器,帮了我很大的忙!

但是我在使用过程中遇到了一个小问题,不知道楼主是否有注意到:当项目中 项目设置>常规>渲染>视口>HDR 2D 功能被开启时,调低阴影透明度参数,画面中会出现一块方形异常区域,与阴影颜色值相反;而关闭HDR 2D功能后,该异常就会消失,阴影显示完全正常。

经过排查发现,问题根源在于阴影透明度的计算逻辑:原脚本中使用了减法运算(blurred.a – (1.0 – color.a)),当阴影透明度调低时,会产生负数值的Alpha通道。

普通LDR渲染模式下,引擎会自动将负Alpha截断为0,所以异常被掩盖;但开启HDR 2D后,浮点渲染缓冲不会自动修正负Alpha,负值参与混合后,就会显示出阴影采样范围的方形区块。

这里提供一个解决方案,修改process_texture函数中的阴影透明度计算逻辑,改用乘法叠加并限制Alpha取值范围,兼容HDR/非HDR两种渲染模式:

将原阴影透明度计算代码:

return vec4(color.rgb, (blurred.a - (1.0 - color.a)) * modulate.a * fade);

替换为:

float shadow_alpha = blurred.a * color.a * modulate.a * fade;
return vec4(color.rgb, clamp(shadow_alpha, 0.0, 1.0));

————————————————————————————————————————
Thanks for sharing the shader; it helped me a lot!

However, I encountered a small issue during use. I wonder if you’ve noticed this: when the HDR 2D option in Project Settings > General > Rendering > Viewport is enabled, lowering the shadow transparency parameter causes a square abnormal area with the opposite color value of the shadow to appear on the screen. When HDR 2D is disabled, the abnormality disappears and the shadow displays completely normally.

After troubleshooting, the root cause lies in the shadow transparency calculation logic: the original script uses subtraction (blurred.a – (1.0 – color.a)), which produces negative Alpha channel values when shadow transparency is lowered.

In normal LDR rendering mode, the engine automatically clamps negative Alpha to 0, so the abnormality is concealed. But when HDR 2D is enabled, the floating-point render buffer does not automatically correct negative Alpha, and the negative values result in a square block of the shadow sampling range after blending.

Here is a solution: modify the shadow transparency calculation logic in the rocess_texture function, use multiplicative overlay and limit the Alpha value range to be compatible with both HDR/non-HDR rendering modes:

Replace the original shadow transparency calculation code:

return vec4(color.rgb, (blurred.a - (1.0 - color.a)) * modulate.a * fade);

With:

float shadow_alpha = blurred.a * color.a * modulate.a * fade;
return vec4(color.rgb, clamp(shadow_alpha, 0.0, 1.0));