I recently found out about the pseudo-3d effect that utilized SNES mode 7, and want to try to replicate it in the Godot Engine. I tried looking around online, but everything was either explained in a way i couldn't understand, or in a programming language I didn't know. I also need to learn how to rotate the area, and put sprites in as characters or enemies, but I didn't find anything on those. Can someone explain the formula, as well as how I could implement it?
Ok, I figured this out. There are two kinds of setup I'll explain.
Before we get to that, let me explain the shader code we will be using:
shader_type canvas_item;
uniform mat3 matrix;
void fragment()
{
vec3 uv = matrix * vec3(UV, 1.0);
COLOR = texture(TEXTURE, uv.xy / uv.z);
}
This is a canvas_item
shader, as such it is intended to work in 2D. What we are doing is applying a transformation matrix (passed as uniform
) to the texture coordinates (UV
). The result we are storing in the uv
variable. We are going to use it to sample the texture of whatever node is using this shader… However we need to use the z
of uv
to do a perspective effect. To do that we divide uv.xy
by uv.z
.
However, I want to apply it centered to the texture. So, let me subtract 0.5
at the start, and add the 0.5
back at the end:
shader_type canvas_item;
uniform mat3 matrix;
void fragment()
{
vec3 uv = matrix * vec3(UV - 0.5, 1.0);
COLOR = texture(TEXTURE, (uv.xy / uv.z) + 0.5);
}
One more thing. I don't like that on extreme values we see a reversed image. Thus, I'll handle that like this:
shader_type canvas_item;
uniform mat3 matrix;
void fragment()
{
vec3 uv = matrix * vec3(UV - 0.5, 1.0);
if (uv.z < 0.0) discard;
COLOR = texture(TEXTURE, (uv.xy / uv.z) + 0.5);
}
Here is an alternative for the branchless enthusiasts (I don't know if it is better):
shader_type canvas_item;
uniform mat3 matrix;
void fragment()
{
vec3 uv = matrix * vec3(UV - 0.5, 1.0);
COLOR = texture(TEXTURE, (uv.xy / uv.z) + 0.5);
COLOR.a *= sign(sign(uv.z) + 1.0);
}
Here sign(uv.z)
will be either -1.0
, 0.0
or 1.0
.
Then sign(uv.z) + 1.0
it will be either 0.0
, 1.0
or 2.0
.
Finally sign(sign(uv.z) + 1.0)
will be either 0.0
or 1.0
(you could use clamp(sign(uv.z), 0.0, 1.0)
instead if you prefer). And thus COLOR.a *= sign(sign(uv.z) + 1.0)
is multiplying alpha by 0.0
anywhere uv.z
is negative.
Note: The reason why I manipulate the UV coordinates in the fragment shader instead of doing it in the vertex shader is because Godot is doing affine texture mapping for 2D. Which would result in a distortion. This is a workaround.
The first setup is simply a sprite. Set a Sprite
with whatever texture you want, and set the material to a new shader material, and in the shader use the code I shown at the start.
Godot will give you the option to edit the uniform mat3 matrix
under Shader Param
in the material resource. By default it will be the identity matrix, which looks like this in the editor:
x 1 y 0 z 0
x 0 y 1 z 0
x 0 y 0 z 1
You can use it to apply a rotation, scaling, shearing or perspective transformations. *I suggest to start by changing the zeros of the z column (the rightmost one), the control the perspective:
x 1 y 0 z 3d_rotate_horizontal
x 0 y 1 z 3d_rotate_vertical
x 0 y 0 z scale
Example result:
A recommendation: Do not use a texture that goes all the way to the edge. When you apply perspective, the shader will read beyond the edge, but by default it is clamped, which result in stretching any pixels at the edge of the texture.
By the way, if you import images as Image
(instead of Texture
which is the default), you can set the Sprite texture as ImageTexture
which will give you some additional control on how the texture shows, including enabling mipmap, antialias filter, and repeating the texture beyond its edge (both mirrored and not mirrored).
The second, more complex setup, is for multiple objects. This is also the setup that works for a TileMap
. You are going to need this tree structure:
- Sprite2D
+- Viewport
+- Camera2D
+- target
Position the Sprite2D
where we want to see this, give it a shader material with the shader code I shown at the start. By the way, this should also work with a TextureRect
in case you need it in the UI.
Do not set a texture for the Sprite2D
(or TextureRect
). You are going to attach an script that looks like this:
extends Sprite
func _ready():
var viewport = $Viewport
yield(get_tree(), "idle_frame")
yield(get_tree(), "idle_frame")
texture = viewport.get_texture()
Change Sprite
to TextureRect
if needed.
This code is taking a reference to the Viewport
node, waiting two frames (to make sure the Viewport
texture is available) and then taking the texture and assigning it to itself.
You need to give the Viewport the size you want. Also I suggest to set Transparent Bg
and V Flip
. The Camera2D
can keep its defaults.
Finally "target" is whatever you want to show. It can be one or multiple 2D nodes. I suggest to make it another scene, that way it will be easy to edit it independently of this setup (whatever is child of the Viewport
will not be visible in the editor).
Example result:
Yes, we could have archived this same effect with actual 3D in Godot, no problem. But we didn't. We choose to implement this effect with 2D tools and do the other things, not because they are easy, but because they are hard.
The textures used in this answer are public domain (CC0), from Kenney.