3

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?

  • 1. Godot can do 3D, why not do 3D? Baring a 3D version of TileMap (other than GridMap), you can make a "2D" game in Godot with 3D tools, and then a 3D matrix transform is the given. 2. You can render to texture (a viewport texture, see the [3d in 2d](https://github.com/godotengine/godot-demo-projects/tree/master/viewport/3d_in_2d) and [2d in 3d](https://github.com/godotengine/godot-demo-projects/tree/master/viewport/2d_in_3d) demos). 3. We have shaders now. – Theraot Apr 26 '21 at 17:56
  • @Theraot My main goal is to learn how to write this kind of script. I am using Godot, but would like to be able to use my knowledge in many places, not just Godot. Also, to get an authentic feel for my game, I think the best method is to replicate the original. – TheWorldSpins Apr 26 '21 at 18:40
  • If replicate the original does not mean using the actual hardware, or an emulator, it got to mean one of the things I mentioned. By the way, questions of the form "how to do this or that effect" are welcome on gamedev.stackexchange.com. However, you mention a script, do you a particular code you want to replicate? – Theraot Apr 26 '21 at 19:02
  • I'm thinking that a shader is probably easier to do. I'll try to get it working. – Theraot Apr 26 '21 at 19:08
  • @Theraot The reason I specified "Mode 7" is because SNES Mode 7 was the mode that let you manipulate backgrounds in specific ways. A famous example of this being used for 3d was the original Super Mario Kart. The effect can create what is essentially a floor for your characters to walk around on. – TheWorldSpins Apr 26 '21 at 21:18
  • I have been trying to set it up, with only partial success. My shader code: `shader_type canvas_item;uniform mat3 matrix;vec2 processUV(vec2 input){vec3 position = matrix * vec3(input, 1.0);return ((input - 0.5) / position.z) + 0.5;}void fragment(){ COLOR = texture(TEXTURE, processUV(UV));}`. The first issue was that Godot in 2D is doing affine texture mapping, so I had to process the UV coords in the fragment shader. However, I have been unable to make it work for tilemaps, only sprites. I tried the technique from 2D in 3D with that (so 2D in 2D but with a 3D transform) still not perfect. – Theraot Apr 26 '21 at 21:48

1 Answers1

4

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:

Robot with no effect and Robot with perspective effect

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:

TileMap with perspective effect and Robot ontop with no effect

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.

Theraot
  • 31,890
  • 5
  • 57
  • 86