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 = 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
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?
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:
Download Build:
// 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);
}