Mode-7 in Lite-C


Breakdown of the pseudo 3D-Effect


March 13, 2016 - Tommy Dräger


battle corp FZero SNES
Do you remember these times? Almost no one reminds himself back to the beginning of 3d, where everthing starts with a stretched plane that simulated a 3d-space mixed with some nice painted sprites. That time leaves its mark and created a whole generation of nostalgic mode7 fans. The SNES pursued a new approach at this time, with which a representation of pseudo-like 3D graphics was used to implement a fake impression of 3D called Mode7. Titles like Super Mario Kart, FZero, Battle Corps, Pilotwings and pole position powers for the implementation of a pseudo-realistic representation to use.

Super Mario Kart SNES Pole Position SNES
Super Mario Kart (Left) & Pole Position (Right)



Mode 7


"The SNES was the first console to have special hardware for graphic tricks that allowed linear transformations (like rotation and scaling) on backgrounds and sprites. Mode7 took this one step further: it not only rotated and scaled a background, but added a step for perspective to create a 3D look."

The Super NES console has eight graphics modes, numbered from 0 to 7, for displaying background layers. The last one (background mode 7) has a single layer that can be scaled and rotated. This graphical method is suited to racing games, and is used extensively for the overworld sections of role-playing games such as Square's popular 1994 game Final Fantasy VI. The effect enables developers to create the impression of sprawling worlds that continue toward the horizon.

Zelda A Link To The Past Final Fantasy IV
A Link To The Past (Left) & Final Fantasy IV (Right)



Mathematical Background


Mode 7 is a very simple effect. It projects a 2D x/y texture (or tiles) to some floor/ceiling. Old SNES use hardware to do this, but modern computers are so powerful that you can do this realtime (and no need of ASM as you mention).

Basic 3D math formula to project a 3D point (x, y, z) to a 2D point (x, y) is :
x_screen = x_point / Z_point;
y_screen = y_point / z_point;

x' = x / z;
y' = y / z; 

Mathematical Derivation

ScreenLayer PointLayer
Depicting are the vector of the observer E, the screen surface with a resolution of 320px by 240px which is away from the viewer at a distance of 640px. The screen surface is the point from S which was projected from point P of the dot area of the screen surface. Point C represents the screen center. Q is the focal point of the area.

ScreenLayer PointerLayer Top

The crucial point is that the triangles ECS and EQP are "similar triangles".
(In case you don't know what similar triangles are, they are triangles with the same shape but of different sizes**. They have the property that their corresponding sides are proportional, meaning they are magnified by the same amount, and thus the ratio between the corresponding sides is the same.)

Arises the following relationship:
 EC     CS
---- = ----
 EQ     QP

With set values following equation arises: "160" is half the screen length, "640" describes the distance from the viewer

       640          (xs - 160)
---------------- = ------------
(zp - zs) + 640     (xp - 160)

under the defenition that the projection has the Z value "0"

    640         (xs - 160)
----------- = ------------
 zp + 640      (xp - 160)

we solve the term by XS, on this we get:

                640 * (xp - 160)
 (xs - 160) = ------------------ | + 160
                   zp + 640


         640 * (xp - 160)
 xs  = ------------------ + 160
            zp + 640

Now consider the critical variables we get our presumption of access

 x' = x / z

If we replace the static values by native constants we get

         EyeDist * (xp - (ScrX/2))
 xs  = --------------------------- + (ScrX/2)
              zp + EyeDist

The same derivation works analogously to YS.

ScreenLayer PointerLayer Side

Step by Step

#include < acknex.h >
#include < litec.h >

#define xres 320
#define yres 240

Include those 2 Libraries from include folder of gstudio7 and define some macros for the screenresolution

BMAP* blank_16;
BMAP* blank_32;

Declare Framebuffer and Framedepth (for Distantsfog);

BMAP* sphere        = "object.bmp";
BMAP* displace      = "ground.bmp";
BMAP* background    = "backdrop.bmp";

Textures in the Section below.

PANEL* framebuffer =
{
    layer = 10;
    flags = VISIBLE;
}

PANEL* framedepth =
{
    layer = 11;
    flags = VISIBLE;
}

PANEL* obj_01 =
{
    layer = 12;
    flags = VISIBLE | OVERLAY;
    bmap = sphere;
}

Define some PANEL* structs for the backdrop, distantsfog and a spriteobject

// mode 7 settings
typedef struct
{
    int foclength; // focal length
    int horizon;
    int scale; // level ground scale factor
    int obj_scale; // scale factor for sprites
} M7_OPT;


// these are values that work fine at a resolution of 320 x 240
M7_OPT* m7 =
{
    foclength = 400;
    horizon = 80;
    scale = 60;
    obj_scale = 15;
}

add some Option Structs for a better Overview

function main()
{
    level_load("");
    wait(2);
    // load an empty 3d-space   
    // wait for 2 frames

    video_set(xres, yres, 16, 0);
    // set video resolution to xres, yres, 
    // bitmode to 16-bit and fullscreen

    // create two blank bmaps for the screen and the distance fogging
    blank_16 = bmap_createblack(xres, yres, 16);
    blank_32 = bmap_createblack(xres, yres, 32);

    framebuffer.bmap = blank_16;
    framedepth.bmap = blank_32;
}

Define the startup-Function and assign the BMAP to respective PANEL

Lite-C Panel
function mode_7_level (BMAP* screen, BMAP* source, BMAP* distfog, BMAP* backdrop);
function mode_7_sprite (PANEL* sprite, int px, int py, float obj_dir, float obj_org);

float angle     = 0;
float xOffset   = 0;
float yOffset   = 0;

Define some functionprototypes and some global variables for later interaction

function mode_7_level (BMAP* screen, BMAP* source, BMAP* distfog, BMAP* backdrop)
{
    // fixed count;
    DWORD blend;
    // alphavalue

    int x,y;
    // countervars

    int frm; // frame

    int dx, dy;
    int mask_x, mask_y;
    int mask_x_backdrop;

    float space_x, space_y, space_z;
    float screen_x, screen_y;
    float px, py, pz;
    float sx, sy;

    mask_x = bmap_width(source) - 1;
    mask_y = bmap_height(source) - 1;
    mask_x_backdrop = bmap_width(backdrop) - 1;

    while (1)
    {
        var formatScreen    = bmap_lock(screen, 0);
        var formatSource    = bmap_lock(source, 0);
        var formatDistFog   = bmap_lock(distfog, 0);
        var formatBackdrop  = bmap_lock(backdrop, 0);

        for(y = -yres/2; y < yres/2; y++)
        {
            dy = y+yres/2;

            // blend is the alpha of the fog
            blend = ((dy)-60) * -100/110 - 10;
            blend = clamp(blend, -100, 0);

            // just to create a non-linear effect
            blend &= 505;

            // make a slice at 70 to render the background:
            // render ground
            if (y >= (-yres/2)+70) //70 hardcoded
            {
                for (x = -xres/2; x < xres/2; x++)
                {
                    dx = x+xres/2;

                    // actual mode 7 code
                    // assign some tempvars for a better overview
                    px = x; 
                    py = y + m7.foclength;
                    pz = y + m7.horizon;

                    // projection from a 3d point to a 2d screenpixel; y stands for z (depth)
                    space_x = px / pz;
                    space_y = py / pz * -1;

                    // invert the y direction so that everthing points in a correct way
                    // a trigonomic solution to be able to rotate the bmap so you can look around 360°
                    // Equation stands in every formulary
                    screen_x = space_x * cos(angle) - space_y * sin(angle);
                    screen_y = space_x * sin(angle) + space_y * cos(angle);

                    // final transformation and scaling
                    sx = screen_x * m7.scale + xOffset;
                    sy = screen_y * m7.scale + yOffset;

                    // use the and-operator to create an infintife pattern

                    var pixel = pixel_for_bmap(source, (int)sx & mask_x, (int)sy & mask_y);

                    // transfer source pixel to target pixel format                 
                    COLOR color;
                    pixel_to_vec(&color, NULL, formatSource, pixel);
                    pixel = pixel_for_vec(&color, 100, formatScreen);           

                    // write source to screen render target
                    pixel_to_bmap(screen,dx,dy, pixel);

                    // write fog
                    pixel = pixel_for_vec(vector(255,255,255) , blend, formatDistFog);
                    pixel_to_bmap(distfog, dx, dy, pixel);
                }
            }
            else // render sky
            {
                for(x = 0; x < xres; x++)
                {
                    // read sky pixel
                    var pixel = pixel_for_bmap(backdrop, (int)(x+angle*200) & mask_x_backdrop, dy);

                    // transfer sky pixel to screen format
                    COLOR color;
                    pixel_to_vec(&color, NULL, formatBackdrop, pixel);
                    pixel = pixel_for_vec(&color, 100, formatScreen);

                    // draw sky to screen render target
                    pixel_to_bmap(screen, x, dy, pixel);
                }
            }
        }

        // everytime you want to add a object you've to place a new mode_7_sprite function here
        mode_7_sprite(obj_01, 10, -100, 0, 0);

        bmap_unlock(backdrop);
        bmap_unlock(distfog);
        bmap_unlock(source);
        bmap_unlock(screen);

        wait(1);
    }
}


px = x; 
py = y + m7.foclength;
pz = y + m7.horizon;

space_x = px / pz;
space_y = py / pz * -1;

Actual Mode7. See Equation Above. x' = x / z. Why pz depends on y?

Perspective 3D To 2D

the farther away, the lesser is the value of the screen Y.

function mode_7_sprite (PANEL* sprite, int px, int py, float obj_dir, float obj_org)
{
    float width, height;
    float space_x, space_y;
    float screen_x, screen_y;

    float obj_x = px + xOffset; 
    float obj_y = py - yOffset; 

    // equations stands in every formula
    float distance = sqrt(pow(obj_x,2)+pow(obj_y,2));

    space_x = obj_x * cos(angle) - obj_y * sin(angle);
    space_y = obj_x * sin(angle) + obj_y * cos(angle);

    // space_y is the depth; if you want to reproduce these equation just look for
    // "projection" resp. "similar triangles"
    screen_x = xres/2 + (space_x * m7.foclength) / space_y;
    screen_y = (m7.foclength / space_y) + m7.horizon - 10;

    // calculate the new height depend indirect proportional to the distance
    height = (m7.scale / distance);
    width = (m7.scale / distance);

    sprite.scale_x = width;
    sprite.scale_y = height;
...

until here the Function is pretty much the same.

float distance = sqrt(pow(obj_x,2)+pow(obj_y,2));

This line is kinda interessting. You've to determine the distance, between the camera and the object to resize objects. This later ensures that objects shrinks as they move farther away and grow as they come closer.

sprite.pos_x = screen_x - (width*sprite.size_x/2);
sprite.pos_y = screen_y + (height*sprite.size_y/2); 
// define the center of the object


// just a small solution for culling so you doesn't see the sprite twice
// convert vector to +-180 angle
vec_to_angle(obj_dir,vector(space_x,space_y,0));
vec_to_angle(obj_org,vector(px,py,0));

// convert +-180 angle to 0...360 angle
obj_dir = cycle(obj_dir,0,360);
obj_org = cycle(obj_org,0,360);

// determine wheter the object is in eyesight
if (obj_dir > (obj_org - 90) && obj_dir < (obj_org + 90))
    sprite.flags |= VISIBLE;
else
    sprite.flags &= ~VISIBLE;
// and simple switch the renderstate to on and off

return;
}

Don't forget to call the function at the bottomline of the mainfunction

mode_7_level(blank_16, displace, blank_32, background);

kompletter Code

Download A8-Engine: server.conitec.net/down/gstudio8_setup.exe

Download Textures:

GroundTexture SpriteTexture BackgroundTexture

Download Build:

mode7_precompiled.rar

Mode-7 Example Mode-7 Ocean
// Pseudo-Mode-7!
// Programm still need some enhancement

// written in August 2012 by Tommy - [email protected]
// modified for A8 compatibility by Christian Behrenberg (HeelX) - [email protected]

#include < acknex.h>
#include < default.c>

#define xres 320
#define yres 240

BMAP* blank_16;
BMAP* blank_32;

BMAP* sphere        = "object.bmp";
BMAP* displace      = "ground.bmp";
BMAP* background    = "backdrop.bmp";

PANEL* framebuffer =
{
    layer = 10;
    flags = VISIBLE;
}

PANEL* framedepth =
{
    layer = 11;
    flags = VISIBLE;
}

PANEL* obj_01 =
{
    layer   = 12;
    flags   = VISIBLE | OVERLAY;
    bmap    = sphere;
}

function mode_7_level (BMAP* screen, BMAP* source, BMAP* distfog, BMAP* backdrop);
function mode_7_sprite (PANEL* sprite, int px, int py, float obj_dir, float obj_org);

// mode 7 settings
typedef struct
{
    int foclength; // focal length
    int horizon;
    int scale; // level ground scale factor
    int obj_scale; // scale factor for sprites
} M7_OPT;


// these are values that work fine at a resolution of 320 x 240
M7_OPT* m7 =
{
    foclength = 400;
    horizon = 80;
    scale = 60;
    obj_scale = 15;
}

float angle = 0;

float xOffset = 0;
float yOffset = 0;

// renders the mode 7 screen image; screen and distfog are the (cpu) target bitmaps,
// distfog and backdrop are the source images
function mode_7_level (BMAP* screen, BMAP* source, BMAP* distfog, BMAP* backdrop)
{
    fixed count;
    DWORD blend;

    int x,y;

    int frm; // frame

    int dx, dy;
    int mask_x, mask_y;
    int mask_x_backdrop;

    float space_x, space_y, space_z;
    float screen_x, screen_y;
    float px, py, pz;
    float sx, sy;

    mask_x = bmap_width(source) - 1;
    mask_y = bmap_height(source) - 1;
    mask_x_backdrop = bmap_width(backdrop) - 1;

    while (1)
    {
        // steering
        angle += (key_cur - key_cul) * time_step / 15;

        xOffset     += (key_d - key_a)*time_step * 4;//SHIFTING
        m7.scale += (key_w - key_s)*time_step * 4;//SHIFTING

        xOffset += (key_cuu - key_cud) * sin(angle) * time_step * 8;
        yOffset -= (key_cuu - key_cud) * cos(angle) * time_step * 8;

        var formatScreen        = bmap_lock(screen, 0);
        var formatSource        = bmap_lock(source, 0);
        var formatDistFog   = bmap_lock(distfog, 0);
        var formatBackdrop  = bmap_lock(backdrop, 0);

        for(y = -yres/2; y < yres/2; y++)
        {
            dy = y+yres/2;

            // blend is the alpha of the fog
            blend = ((dy)-60) * -100/110 - 10;
            blend = clamp(blend, -100, 0);

            // just to create a non-linear effect (magic number)
            blend &= 505;

            // make a slice at 70 to render the background:

            // render ground
            if (y >= (-yres/2)+70)
            {
                for (x = -xres/2; x < xres/2; x++)
                {
                    dx = x+xres/2;

                    // actual mode 7 code

                    // assign some tempvars for a better overview
                    px = x; 
                    py = y + m7.foclength;
                    pz = y + m7.horizon;

                    // projection from a 3d point to a 2d screenpixel; y stands for z (depth)
                    space_x = px / pz;
                    space_y = py / pz * -1;

                    // invert the y direction so that everthing points in a correct way

                    // a trigonomic solution to be able to rotate the bmap so you can look around 360°
                    screen_x = space_x * cos(angle) - space_y * sin(angle);
                    screen_y = space_x * sin(angle) + space_y * cos(angle);

                    // final transformation and scaling
                    sx = screen_x * m7.scale + xOffset;
                    sy = screen_y * m7.scale + yOffset;

                    // use the and-operator to create an infintife pattern

                    var pixel = pixel_for_bmap(source, (int)sx & mask_x, (int)sy & mask_y);

                    // transfer source pixel to target pixel format                 
                    COLOR color;
                    pixel_to_vec(&color, NULL, formatSource, pixel);
                    pixel = pixel_for_vec(&color, 100, formatScreen);           

                    // write source to screen render target
                    pixel_to_bmap(screen,dx,dy, pixel);

                    // write fog
                    pixel = pixel_for_vec(vector(255,255,255) , blend, formatDistFog);
                    pixel_to_bmap(distfog, dx, dy, pixel);
                }
            }
            else // render sky
            {
                for(x = 0; x < xres; x++)
                {
                    // read sky pixel
                    var pixel = pixel_for_bmap(backdrop, (int)(x+angle*200) & mask_x_backdrop, dy);

                    // transfer sky pixel to screen format
                    COLOR color;
                    pixel_to_vec(&color, NULL, formatBackdrop, pixel);
                    pixel = pixel_for_vec(&color, 100, formatScreen);

                    // draw sky to screen render target
                    pixel_to_bmap(screen, x, dy, pixel);
                }
            }
        }

        // everytime you want to add a object you've to place a new mode_7_sprite function here
        mode_7_sprite(obj_01, 10, -100, 0, 0);

        bmap_unlock(backdrop);
        bmap_unlock(distfog);
        bmap_unlock(source);
        bmap_unlock(screen);

        wait(1);
    }
}

// draws an object
function mode_7_sprite (PANEL* sprite, int px, int py, float obj_dir, float obj_org)
{
    float width, height;
    float space_x, space_y;
    float screen_x, screen_y;

    float obj_x = px + xOffset; 
    float obj_y = py - yOffset; 

    // equations stands in every formula
    float distance = sqrt(pow(obj_x,2)+pow(obj_y,2));

    space_x = obj_x * cos(angle) - obj_y * sin(angle);
    space_y = obj_x * sin(angle) + obj_y * cos(angle);

    // space_y is the depth; if you want to reproduce these equation just look for
    // "projection" resp. "similar triangles"
    screen_x = xres/2 + (space_x * m7.foclength) / space_y;
    screen_y = (m7.foclength / space_y) + m7.horizon - 10;

    // calculate the new height depend indirect proportional to the distance
    height = (m7.scale / distance);
    width = (m7.scale / distance);

    sprite.scale_x = width;
    sprite.scale_y = height;

    sprite.pos_x = screen_x - (width*sprite.size_x/2);
    sprite.pos_y = screen_y + (height*sprite.size_y/2); 

    // just a small solution for culling so you doesn't see the sprite twice

    // convert vector to +-180 angle
    vec_to_angle(obj_dir,vector(space_x,space_y,0));
    vec_to_angle(obj_org,vector(px,py,0));

    // convert +-180 angle to 0...360 angle
    obj_dir = cycle(obj_dir,0,360);
    obj_org = cycle(obj_org,0,360);

    // determine wheter the object is in eyesight
    if (obj_dir > (obj_org - 90) && obj_dir < (obj_org + 90))
        sprite.flags |= VISIBLE;
    else
        sprite.flags &= ~VISIBLE;

    return;
}

function main()
{
    level_load("");

    wait(2);

    video_set(xres, yres, 16, 0);

    // create two blank bmaps for the screen and the distance fogging
    blank_16 = bmap_createblack(xres, yres, 16);
    blank_32 = bmap_createblack(xres, yres, 32);

    framebuffer.bmap = blank_16;
    framedepth.bmap = blank_32;

    mode_7_level(blank_16, displace, blank_32, background);
}