Mode-7 in Lite-C




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:
mode7_precompiled.rar




// Pseudo-Mode-7!
// Programm still need some enhancement
	
// written in August 2012 by Tommy - tommy@fenixfox-studios.com
// modified for A8 compatibility by Christian Behrenberg (HeelX) - christian@behrenberg.de
	
#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);
}