Skip to content

Instantly share code, notes, and snippets.

@Jellonator
Created January 2, 2020 22:42
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Jellonator/0686c6e74d06745957de5a96fa00ec6c to your computer and use it in GitHub Desktop.
Save Jellonator/0686c6e74d06745957de5a96fa00ec6c to your computer and use it in GitHub Desktop.
Mode 7 shader for Godot
shader_type canvas_item;
uniform mat4 TRANSFORM;
uniform vec2 DEPTH;
uniform bool REPEAT_X;
uniform bool REPEAT_Y;
uniform bool FLIP;
void fragment() {
// Create the matrix. A workaround is used to modify the matrix's W column
// because Godot's transforms are 3x4, not 4x4.
mat4 mat = mat4(1.0);
mat[0].w = DEPTH.x;
mat[1].w = DEPTH.y;
// Transform UV into [-1, 1] range
vec2 uv = UV * 2.0 - vec2(1, 1);
// Turn position into 4d vector
vec4 pos = vec4(uv, 1.0, 1.0);
pos = mat * pos;
pos.xy = pos.xy / pos.w;
// Apply transformation to position
float w = pos.w;
pos.z = 0.0;
pos.w = 1.0;
// Apply depth to position
pos = TRANSFORM * pos;
// divide position by w coordinate; this applies perspective
// Set UV to position; transform its range to [0, 1]
uv = (pos.xy + vec2(1, 1)) * 0.5;
// Determine if uv is in range or repeating pattern
if (((uv.x > 0.0 && uv.x < 1.0) || REPEAT_X) && ((uv.y > 0.0 && uv.y < 1.0) || REPEAT_Y) && (w > 0.0 || FLIP)) {
// Apply texture
uv = mod(uv, 1.0);
COLOR = texture(TEXTURE, uv);
} else {
COLOR = vec4(0, 0, 0, 0);
}
}
@oliviafel
Copy link

Great shader, I was wondering if you would also know how to correctly place sprites on top of a texture that is being transformed by this? I'm trying to create a billboarded sprite appearance and not sure what the correct position transform should be. Mode 7 math and GODOTs weird Basis/Transform objects is mixing me up.

@Jellonator
Copy link
Author

Great shader, I was wondering if you would also know how to correctly place sprites on top of a texture that is being transformed by this? I'm trying to create a billboarded sprite appearance and not sure what the correct position transform should be. Mode 7 math and GODOTs weird Basis/Transform objects is mixing me up.

Yeah I wrote this a while ago, and this gist definitely needs a bit of cleaning up. That being said, I'll try my best and come up with something.
One way I like to look at this is:

  • DEPTH is used to generate a projection matrix
  • TRANSFORM is a view matrix
  • The transform of your Node2D will be the model view matrix

In the original shader, I use the W component of vec4 as the weight. However, we will instead need to use the Z component of a vec3 as a weight instead, as Godot has no Vector4 (although you could implement something like this yourself). This is fine, because you only need three components and a 3x3 matrix type for 2d transforms. I will also be hi-jacking the Basis type for the 3x3 matrix.

# Object.gd
# Assumptions:
#     * a 1920x1080 viewport size
#     * A parent object using the Mode7 shader as a ShaderMatrial, which takes up the entire screen (e.g. Polygon2D),
#        and whose UV is 0,0 in the top left corner and 1,1 in the bottom right corner.
#     * This Node2D has a child named Sprite whose position will be updated
extends Node2D

onready var node_sprite = $Sprite
onready var node_mode7 = get_parent()
const SCREEN_SIZE := Vector2(1920, 1080)

func create_2d_perspective_matrix(depth: Vector2) -> Basis:
    return Basis(
        Vector3(1, 0, depth.x),
        Vector3(0, 1, depth.y),
        Vector3(0, 0, 1))

func mult_position_by_basis(pos: Vector2, mat: Basis) -> Vector2:
    var x = Vector3(pos.x, pos.y, 1.0)
    var res = mat.xform(x)
    return Vector3(res.x / res.z, res.y / res.z, res.z) # Important: dividing here applies perspective
    # Another important factor: the Z component is a depth. The bigger it is, the further
    # away the sprite is. Ideally, the sprite should be hidden if the Z component <= 0

func _process(_delta: float):
    var material = node_mode7.material # should be a ShaderMatrial
    var transform = material.get_shader_param("TRANSFORM")
    var depth = material.get_shader_param("DEPTH")
    # Create perspective matrix
    var perspective_matrix = create_2d_perspective_matrix(depth)
    # Create view matrix from TRANFORM. Node that Z row is skipped, just like in the original shader.
    var view_matrix = Basis(
        transform.basis.x,
        transform.basis.y,
        transform.origin)
    # Transform to range [-1, 1] from world space
    var worldpos = (global_position / (SCREEN_SIZE / 2)) - Vector2.ONE
    var mat = perspective_matrix * view_matrix # May want to reverse order? I don't remember which way is correct
    var pos = mult_position_by_basis(worldpos, mat)
    if (pos.z > 0.0):
        # Transform back to world space
        sprite.global_position = (Vector2(pos.x, pos.y) + Vector2.ONE) * (SCREEN_SIZE / 2)
        sprite.show()
    else:
        sprite.hide()

Disclaimer: I have not tested the above code yet. I'll test this once I get home, and I'll post any revisions.

@oliviafel
Copy link

Disclaimer: I have not tested the above code yet. I'll test this once I get home, and I'll post any revisions.

I'm close to getting this to work, I think? I'm translating it over to C# though and might have fumbled a few things and I'm also doing some questionable things with Viewports where I create a copy of the objects I want to display with the mode 7 and leave the originals to keep game logic completely 2D. I'll put the project link below if you wanted to look, otherwise I'll report back as I attempt to figure out what I messed up.

https://github.com/DTFlip/Mode7_Godot

@BThacker
Copy link

BThacker commented Jan 23, 2022

Sharing here before I forgot. I was just able to get the above code to work in C# with some slight changes. Working on some other touches now but the only way I was able to wrap my head around the entire thing and understand it properly was to use the Systems.Numerics and convert it all to a Matrix4x4.

// good luck with the below
using Godot;
using System;
using System.Numerics;

public class object_test : Node2D
{
    Godot.ShaderMaterial mode7;
    Godot.Node2D parent;
    Godot.Sprite spriteToMove;

    private Matrix4x4 perspectiveMatrix = new Matrix4x4();
    private Matrix4x4 viewMatrix = new Matrix4x4();
    private Godot.Vector2 screenSize = new Godot.Vector2(1024,1024);
    private float halfHeight = 512;

    public override void _Ready()
    {
        parent = GetParent() as Node2D;
        mode7 = parent.Material as ShaderMaterial;
        spriteToMove = GetNode<Sprite>("Sprite");
       
        Godot.Vector2 temp = new Godot.Vector2(0, 20);
        Create2DPerspectiveMatrix(temp);
    }

    private void Create2DPerspectiveMatrix(Godot.Vector2 depth)
    {
        Vector4 _line1 = new Vector4(1f, 0f, 0f, depth.x);
        Vector4 _line2 = new Vector4(0f, 1f, 0f, depth.y);
        Vector4 _line3 = new Vector4(0f, 0f, 1f, 0f);
        Vector4 _line4 = new Vector4(0f, 0f, 0f, 1f);

        perspectiveMatrix.M11 = _line1.X;
        perspectiveMatrix.M12 = _line1.Y;
        perspectiveMatrix.M13 = _line1.Z;
        perspectiveMatrix.M14 = _line1.W;
        perspectiveMatrix.M21 = _line2.X;
        perspectiveMatrix.M22 = _line2.Y;
        perspectiveMatrix.M23 = _line2.Z;
        perspectiveMatrix.M24 = _line2.W;
        perspectiveMatrix.M31 = _line3.X;
        perspectiveMatrix.M32 = _line3.Y;
        perspectiveMatrix.M33 = _line3.Z;
        perspectiveMatrix.M34 = _line3.W;
        perspectiveMatrix.M41 = _line4.X;
        perspectiveMatrix.M42 = _line4.Y;
        perspectiveMatrix.M43 = _line4.Z;
        perspectiveMatrix.M44 = _line4.W;

    }

    private void CreateViewMatrix(Godot.Transform _transform, Godot.Vector2 _depth)
    {
        Vector4 _line1 = new Vector4(_transform.basis.x.x, _transform.basis.x.y, _transform.basis.x.z, 0f);
        Vector4 _line2 = new Vector4(_transform.basis.y.x, _transform.basis.y.y, _transform.basis.y.z, 0f);
        Vector4 _line3 = new Vector4(_transform.basis.z.x, _transform.basis.z.y, _transform.basis.z.z, 0f);
        Vector4 _line4 = new Vector4(_transform.origin.x, _transform.origin.y, 0f, 1f);

        viewMatrix.M11 = _line1.X;
        viewMatrix.M12 = _line1.Y;
        viewMatrix.M13 = _line1.Z;
        viewMatrix.M14 = _line1.W;
        viewMatrix.M21 = _line2.X;
        viewMatrix.M22 = _line2.Y;
        viewMatrix.M23 = _line2.Z;
        viewMatrix.M24 = _line2.W;
        viewMatrix.M31 = _line3.X;
        viewMatrix.M32 = _line3.Y;
        viewMatrix.M33 = _line3.Z;
        viewMatrix.M34 = _line3.W;
        viewMatrix.M41 = _line4.X;
        viewMatrix.M42 = _line4.Y;
        viewMatrix.M43 = _line4.Z;
        viewMatrix.M44 = _line4.W;
        
    }

    public Godot.Vector3 MultPositionByVect4(Godot.Vector2 worldPos, Matrix4x4 mat)
    {
        
        Vector4 temp = new Vector4(worldPos.x, worldPos.y, 0f, 1f);
        Vector4 _transformed = Vector4.Transform(temp, mat);
        Godot.Vector3 _newReturn = new Godot.Vector3(_transformed.X / _transformed.W, _transformed.Y / _transformed.W,  _transformed.W);
        return _newReturn;
        
    }
    
    public override void _Process(float delta)
    {
        Godot.Transform _transform = (Godot.Transform)mode7.GetShaderParam("TRANSFORM");
        GD.Print(_transform);
        Godot.Vector2 _depth = (Godot.Vector2)mode7.GetShaderParam("DEPTH");
        Create2DPerspectiveMatrix(_depth);
        CreateViewMatrix(_transform, _depth);
        Matrix4x4 _mat = perspectiveMatrix * viewMatrix;
        //Matrix4x4 _mat = viewMatrix * perspectiveMatrix;
        Matrix4x4 _inverted = new Matrix4x4();
        Matrix4x4.Invert(_mat, out _inverted);
        //Matrix4x4 _mat = viewMatrix * perspectiveMatrix;
        //Godot.Vector2 worldPos = (this.GlobalPosition / (screenSize / 2)) - Godot.Vector2.One;
        float wX = this.GlobalPosition.x;
        float xY = this.GlobalPosition.y;
        Godot.Vector2 worldpos = (this.GlobalPosition / (screenSize / 2)) - Godot.Vector2.One;
        
        Godot.Vector3 newPos = MultPositionByVect4(worldpos, _inverted);
        Godot.Vector2 final = new Godot.Vector2(newPos.x, newPos.y);

        //Godot.Vector2 finalPos = new Godot.Vector2(newPos.x, newPos.y);
        //Godot.Vector2 finalPos1 = new Godot.Vector2(finalPos + Godot.Vector2.One) * (screenSize / 2);
        spriteToMove.GlobalPosition = (final + Godot.Vector2.One) * (screenSize / 2);
    }    
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment