Mode-7 in Lite-C
Breakdown of the pseudo 3D-Effect
March 13, 2016 - Tommy Dräger
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 (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.
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
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.
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.
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 = 200;
}
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
function mode_7_level (BMAP* screen, BMAP* source, BMAP* distfog, BMAP* backdrop);
function mode_7_sprite (PANEL* sprite, int px, int py);
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, -200);
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?
the farther away, the lesser is the value of the screen Y.
function mode_7_sprite(PANEL *sprite, int worldX, int worldY)
{
float rot_x = (worldX - xOffset) / m7.scale;
float rot_y = (worldY - yOffset) / m7.scale;
float space_x = rot_x * cos(angle) + rot_y * sin(angle);
float space_y = -rot_x * sin(angle) + rot_y * cos(angle);
float y = (-m7.foclength - space_y * m7.horizon) / (space_y + 1);
float pz = y + m7.horizon;
float dx_screen = space_x*pz + xres/2;
float dy_screen = y + yres/2;
// COMPUTE SPRITE SCALE ON DISTANCE
// EQUATIONS STANDS IN EVERY FORMULARY
float distance = sqrt((worldX - xOffset) * (worldX - xOffset) + (worldY - yOffset) * (worldY - yOffset));
float scale = m7.obj_scale / distance;
sprite.scale_x = scale;
sprite.scale_y = scale;
sprite.pos_x = dx_screen - (sprite.size_x * scale / 2);
sprite.pos_y = dy_screen - (sprite.size_y * scale / 2);
// JUST A QUICKFIX SOLUTION FOR CULLING
if (distance > 0 && pz > 0) sprite.flags |= VISIBLE;
else sprite.flags &= ~VISIBLE;
}
until here the Function is pretty much the same.
float distance = sqrt((worldX - xOffset) * (worldX - xOffset) + (worldY - yOffset) * (worldY - yOffset));
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.
Don't forget to call the function at the bottomline of the mainfunction
mode_7_level(blank_16, displace, blank_32, background);
complete Code
Download A8-Engine: server.conitec.net/down/gstudio8_setup.exe
Github Repository: https://github.com/MilesTails01/Mode-7
Download Textures:
Download Build:
// Pseudo-Mode-7!
/* Programm still need some enhancement */
// updated in August 2012 by Tommy - [email protected]
// modified for A8 compatibility by Christian Behrenberg (HeelX) - [email protected]
#include <acknex.h>
#include <litec.h>
#include <default.c>
#define xres 320
#define yres 240
BMAP* blank_16;
BMAP* blank_32;
BMAP* sphere = "../public/BBSphere.bmp";
BMAP* displace = "../public/o1.bmp";
BMAP* background = "../public/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);
typedef struct /* MODE 7 SETTINGS */
{
int foclength; // FOCALLENGTH
int horizon;
int scale; // SCALEFACTOR FOR LEVELGROUND
int obj_scale; // SCALEFACTOR FOR SPRITES
} M7_OPT;
// THESE ARE VALUES THAT WORKS FINE FOR
// ME AT A RESOLUTION OF 320 X 240
M7_OPT *m7 =
{
foclength = 400;
horizon = 80;
scale = 60;
obj_scale = 150;
}
float angle = 0;
float xOffset = 0;
float yOffset = 0;
// SCREEN AND DISTFOG ARE BMAPS TO PAINT ON
// THE OTHERS ARE USES FOR LEVELGROUND AND
// SKYKBACKDROP
function mode_7_level(BMAP *screen, BMAP *source, BMAP *distfog, BMAP *backdrop)
{
fixed count;
DWORD blend;
int x, y;
int frm;
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)
{
angle += (key_cur - key_cul) * time_step / 15; // STEERING
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 format_screen = bmap_lock(screen, 0);
var format_source = bmap_lock(source, 0);
var format_distfog = bmap_lock(distfog, 0);
var format_backdrop = bmap_lock(backdrop, 0);
for (y = -yres / 2; y < yres / 2; y++)
{
dy = y + yres / 2;
// BLEND HOLDS THE ALPHAVALUE
// FOR THE DISTANTSFOGGING
blend = ((dy)-60) * -100 / 110 - 10;
blend = clamp(blend, -100, 0);
// JUST TO CREATE A NON-LINEAR EFFECT
// 505 WAS DETERMINE BY ME
blend &= 505;
// MAKE A SLICE AT 70
// TO RENDER THE BACKGROUND
if (y >= (-yres / 2) + 70)
{
for (x = -xres / 2; x < xres / 2; x++)
{
dx = x + xres / 2;
/******* ACTUAL MODE 7 ********/
// ASIGN SOME TEMPVARS FOR
// A BETTER OVERVIEW
px = x;
py = y + m7.foclength;
pz = y + m7.horizon;
// MATHIMATICAL PROJECTION
// FOR TURNING A 3D POINT TO
// A 2D SCREENPIXEL
// Y STANDS FOR Z RESP. THE DEPTH
space_x = px / pz;
space_y = py / pz * -1;
// INVERT THE Y DIRECTION
// SO THAT EVERTHING POINT
// IN A CORRECT WAY
// A TRIGONOMIC SOLUTION TO BE ABLE
// TO ROTATE THE BMAP SO YOU CAN
// LOOK AROUND 360DEGREE
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 INFINTIY-PATTERN
VECTOR color;
pixel_to_vec(&color, NULL, format_source, pixel_for_bmap(source, (int)sx & mask_x, (int)sy & mask_y) + 0x12);
pixel_to_bmap(screen, dx, dy, pixel_for_vec(&color, 100, format_screen));
pixel_to_bmap(distfog, dx, dy, pixel_for_vec(vector(240, 254, 160), blend, format_distfog));
// pixel_to_bmap(screen,dx,dy,pixel_for_bmap(source,(int)sx & mask_x,(int)sy & mask_y)+0x001200);
// pixel_to_bmap(distfog,dx,dy,pixel_for_vec(vector(240,254,160),blend,0));
/*******************************/
}
}
else
{
for (x = 0; x < xres; x++)
{
COLOR color;
pixel_to_vec(&color, NULL, format_backdrop, pixel_for_bmap(backdrop, (int)(x + angle * 200) & mask_x_backdrop, dy));
pixel_to_bmap(screen, x, dy, pixel_for_vec(&color, 100, format_screen));
}
}
}
// EVERYTIME YOU WANT TO ADD A OBJECT YOU'VE
// TO PLACE A NEW MODE_7_SPRITE FUNCTION HERE
// IT'S PLACED HERE BECAUSE OF THE PERFORMANCE
mode_7_sprite(obj_01, 0, -200);
bmap_unlock(backdrop);
bmap_unlock(distfog);
bmap_unlock(source);
bmap_unlock(screen);
// THE NEXT LINES AREN'T NECESSARY
// THESE JUST CREATING AN ANIMATED
// WATERSURFACE, BUT YOU CAN DELETE
// THESE IF YOU LIKE
count += (int)4*time_step;
if(count > 10){count %= 10;frm++;}
if(frm >= 7)frm = 0;
switch (frm)
{
case 0: bmap_purge(source);
case 1: bmap_load(source, "../public/o2.bmp", 0);
case 2: bmap_load(source, "../public/o3.bmp", 0);
case 3: bmap_load(source, "../public/o4.bmp", 0);
case 4: bmap_load(source, "../public/o5.bmp", 0);
case 5: bmap_load(source, "../public/o6.bmp", 0);
case 6: bmap_load(source, "../public/o7.bmp", 0);
case 7: bmap_load(source, "../public/o8.bmp", 0);
}
wait(1);
}
}
function mode_7_sprite(PANEL *sprite, int worldX, int worldY)
{
float rot_x = (worldX - xOffset) / m7.scale;
float rot_y = (worldY - yOffset) / m7.scale;
float space_x = rot_x * cos(angle) + rot_y * sin(angle);
float space_y = -rot_x * sin(angle) + rot_y * cos(angle);
float y = (-m7.foclength - space_y * m7.horizon) / (space_y + 1);
float pz = y + m7.horizon;
float dx_screen = space_x*pz + xres/2;
float dy_screen = y + yres/2;
// COMPUTE SPRITE SCALE ON DISTANCE
// EQUATIONS STANDS IN EVERY FORMULARY
float distance = sqrt((worldX - xOffset) * (worldX - xOffset) + (worldY - yOffset) * (worldY - yOffset));
float scale = m7.obj_scale / distance;
sprite.scale_x = scale;
sprite.scale_y = scale;
sprite.pos_x = dx_screen - (sprite.size_x * scale / 2);
sprite.pos_y = dy_screen - (sprite.size_y * scale / 2);
// JUST A QUICKFIX SOLUTION FOR CULLING
if (distance > 0 && pz > 0) sprite.flags |= VISIBLE;
else sprite.flags &= ~VISIBLE;
}
function main()
{
level_load("");
wait(2);
video_set(xres, yres, 16, 0);
// CREATE TWO BLANK BMAPS FOR THE SCREEN
// AND THE DISTANCE FOGGING (BECAUSE OF
// OF THE TRANSPERANTY OF THE LAYER)
blank_16 = bmap_createblack(xres, yres, 16);
blank_32 = bmap_createblack(xres, yres, 32);
framebuffer.bmap = blank_16;
framedepth.bmap = blank_32;
// fps_max = 20;
mode_7_level(blank_16, displace, blank_32, background);
}