Search code examples
3d2dgodotgdscript

Simple mode 7 formula / example?


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?


Solution

  • 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.