2

today I'm bringing a subject about Pseudo 3D and perspective.

I was checking the video #1 Java Classical 3D Rendering Tutorial : Creating 3D World where he used a method for render pseudo-3D ceil and floor. I tried to find some tutorial or the name of the method that he used but I didn't find. I saw the algorithm but it is not clear to understand. I started to search about perspective graphics (vanishing points, horizon...) but the unique thing that I got was static drawing. I wanna apply an illusion moving putting the camera inside the plan and moving it. Under follow an example about the perspective floor and ceiling that I wanna make.

Static Perspective Image Static Perspective Image

This is just an image, but my first question is: "I realy can make a movement of the camera in this ambient, like rotation and move x, and y axis?". I tried to make 2 vanishing point in a canvas, creating lines for each degree of 15º, and I got a perspective illusion, but i couldn't find a way to make the rotation, or the movement. In that video I saw the pixels creating 2 dimensions using just the colors green and blue, but I wanna make this using lines, to understand how it works.

enter image description here

There isn't a place that is teaching step by step how to make the perspective with movements. I didn't find. I checked the videos of 3D game maker in Java and the Markus Person creating the game called by "Prelude of the chambered" using the method of the video, but I didn't find an explanation for this king of rendering.

enter image description here

Lets supose I have to create a plan using a grid. how is the logic that I have to apply in the lines to create the movement? I realy wanna understand the logic to make this kind of pseudo-3D, without using frameworks or thing like that. Thanks for help me! I will wait for your answer.

I checked something about MODE 7 of SNES. This is a good way for make it I think. I have just to understand how it works, and how to make the rotation.

enter image description here

** Note: I don't what to use raycasting for it. Raycasting I'll use to create the walls.

Spektre
  • 41,942
  • 8
  • 91
  • 312

2 Answers2

2

Interesting problem. I did not resist and code it for fun so here some insights... Well there are 2 basic approaches for this. One is raster fake and second is Vector based. I will describe the latter as you can do much more with it.

Vector approach

This approach is not faking anything it really is 3D. The rest depends on the rendering you want to use this for... For now I assume you can render 2D lines. All the code chunks are in C++.

  1. Transformations

    You need vector math to transform points between world and camera space and back again. In 3D graphics are usually 4x4 homogenuous transform matrices used for this and many programing APIs support them natively. I will base my math on OpenGL matrix layout which determine the order of multiplication used. For more info I strongly recommend to read this:

    As I use a lot from it. The linked answers there are also useful especially the 3D graphics pipeline and Full pseudo inverse matrix. The Answer itself is basic knowledge needed for 3D rendering in a nutshell (low level without the need for any lib apart of the rendering stuff).

    There are also libs for this like GLM so if you want you can use any linear algebra supporting 4x4 matrices and 4D vectors instead of my code.

    So lets have two 4x4 matrices one (camera) representing our camera coordinate system and second (icamera) which is its inverse. Now if we want to transform between world and screen space we simply do this:

    P = camera*Q
    Q = icamera*P
    

    where P(x,y,z,1) is point in camera coordinate system and Q(x,y,z,1) is the same point in global world coordinate system.

  2. Perspective

    This is done simply by dividing P by its z coordinate. That will scale objects around (0,0) so the more far object is the smaller will be. If we add some screen resolution and axis correction we can use this:

    void perspective(double *P) // apply perspective transform on P
        {
        // perspectve division
        P[0]*=znear/P[2];
        P[1]*=znear/P[2];
        // screen coordinate system
        P[0]=xs2+P[0];          // move (0,0) to screen center
        P[1]=ys2-P[1];          // axises: x=right, y=up
        }
    

    so point 0,0 is center of screen. The xs2,ys2 is half of resolution of the screen and znear is focal length of the projection. So XY plane rectangle with screen resolution and center at (0,0,znear) will cover the screen exactly.

  3. Rendering 3D line

    We can use any primitives for rendering. I chose line as it is very simple and can achieve much. So what we want is to render 3D line using 2D line rendering API (of any kind). I am VCL based so I chose VCL/GDI Canvas which should be very similar to your Canvas.

    So as input we got two 3D points in global world coordinate system. In order to render it with 2D line we need to convert the 3D position to 2D screen space. That is done by matrix*vector multiplication.

    From that we obtain two 3D points but in camera coordinate system. Now we need to clip the line by our view area (Frustrum). We can ignore x,y axises as 2D line api usually does that for us anyway. So the only thing left is clip z axis. Frustrum in z axis is defined by znear and zfar. Where zfar is our max visibility distance from camera focal point. So if our line is fully before or after our z-range we ignore it and do not render. If it is inside we render it. If it crosses znear or zfar we cut the outside part off (by linear interpolation of the x,y coordinates).

    Now we just apply perspective on both points and render 2D line using their x,y coordinates.

    My code for this looks like this:

    void draw_line(TCanvas *can,double *pA,double *pB)  // draw 3D line
        {
        int i;
        double D[3],A[3],B[3],t;
        // transform to camera coordinate system
        matrix_mul_vector(A,icamera,pA);
        matrix_mul_vector(B,icamera,pB);
        // sort points so A.z<B.z
        if (A[2]>B[2]) for (i=0;i<3;i++) { D[i]=A[i]; A[i]=B[i]; B[i]=D[i]; }
        // D = B-A
        for (i=0;i<3;i++) D[i]=B[i]-A[i];
        // ignore out of Z view lines
        if (A[2]>zfar) return;
        if (B[2]<znear) return;
        // cut line to view if needed
        if (A[2]<znear)
            {
            t=(znear-A[2])/D[2];
            A[0]+=D[0]*t;
            A[1]+=D[1]*t;
            A[2]=znear;
            }
        if (B[2]>zfar)
            {
            t=(zfar-B[2])/D[2];
            B[0]+=D[0]*t;
            B[1]+=D[1]*t;
            B[2]=zfar;
            }
        // apply perspective
        perspective(A);
        perspective(B);
        // render
        can->MoveTo(A[0],A[1]);
        can->LineTo(B[0],B[1]);
        }
    
  4. Rendering XZ plane

    We can visualize the ground and sky planes using our 3D line as grid of squares. So we just create for loops rendering the x-axis aligned lines and y-axis aligned lines covering some square of some size around some origin position O. The lines should be some step far between each other equal to grid cell size.

    The origin position O should be near our frustrun center. If it would be constant then we could walk out of the plane edges so it woul dnot cover the whole (half)screen. We can use our camera position and add 0.5*(zfar+znear)*camera_z_axis to it. To maintain the illusion of movement we need to align the O to step size. We can exploit floor,round or integer cast for this.

    The resulting plane code looks like this:

    void draw_plane_xz(TCanvas *can,double y,double step) // draw 3D plane
        {
        int i;
        double A[3],B[3],t,size;
        double U[3]={1.0,0.0,0.0};  // U = X
        double V[3]={0.0,0.0,1.0};  // V = Z
        double O[3]={0.0,0.0,0.0};  // Origin
        // compute origin near view center but align to step
        i=0; O[i]=floor(camera[12+i]/step)*step;
        i=2; O[i]=floor(camera[12+i]/step)*step;
        O[1]=y;
        // set size so plane safely covers whole view
        t=xs2*zfar/znear;               size=t; // x that will convert to xs2 at zfar
        t=0.5*(zfar+znear); if (size<t) size=t; // half of depth range
        t+=step;                                // + one grid cell beacuse O is off up to 1 grid cell
        t*=sqrt(2);                             // diagonal so no matter how are we rotate in Yaw
        // U lines
        for (i=0;i<3;i++)
            {
            A[i]=O[i]+(size*U[i])-((step+size)*V[i]);
            B[i]=O[i]-(size*U[i])-((step+size)*V[i]);
            }
        for (t=-size;t<=size;t+=step)
            {
            for (i=0;i<3;i++)
                {
                A[i]+=step*V[i];
                B[i]+=step*V[i];
                }
            draw_line(can,A,B);
            }
        // V lines
        for (i=0;i<3;i++)
            {
            A[i]=O[i]-((step+size)*U[i])+(size*V[i]);
            B[i]=O[i]-((step+size)*U[i])-(size*V[i]);
            }
        for (t=-size;t<=size;t+=step)
            {
            for (i=0;i<3;i++)
                {
                A[i]+=step*U[i];
                B[i]+=step*U[i];
                }
            draw_line(can,A,B);
            }
        matrix_mul_vector(A,icamera,A);
        }
    

Now if I put all this together in small VCL/GDI/Canvas application I got this:

//---------------------------------------------------------------------------
#include <vcl.h> // you can ignore these lines
#include <math.h>
#pragma hdrstop
#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm" // up to here.
TMain *Main; // this is pointer to my VCL window (you do not need it)
//--- Here starts the important stuff: --------------------------------------
// perspective
double znear= 100.0;    // focal length for perspective
double zfar = 2100.0;   // visibility
// view
double xs2=0.0;         // screen half resolution
double ys2=0.0;
// camera
double yaw=0.0;         // euler yaw angle [rad]
double camera[16];      // camera direct transform matrix
double icamera[16];     // camera inverse transform matrix
// keyboard bools
bool _forw=false,_back=false,_right=false,_left=false;
//---------------------------------------------------------------------------
void matrix_inv(double *a,double *b) // a[16] = Inverse(b[16])
    {
    double x,y,z;
    // transpose of rotation matrix
    a[ 0]=b[ 0];
    a[ 5]=b[ 5];
    a[10]=b[10];
    x=b[1]; a[1]=b[4]; a[4]=x;
    x=b[2]; a[2]=b[8]; a[8]=x;
    x=b[6]; a[6]=b[9]; a[9]=x;
    // copy projection part
    a[ 3]=b[ 3];
    a[ 7]=b[ 7];
    a[11]=b[11];
    a[15]=b[15];
    // convert origin: new_pos = - new_rotation_matrix * old_pos
    x=(a[ 0]*b[12])+(a[ 4]*b[13])+(a[ 8]*b[14]);
    y=(a[ 1]*b[12])+(a[ 5]*b[13])+(a[ 9]*b[14]);
    z=(a[ 2]*b[12])+(a[ 6]*b[13])+(a[10]*b[14]);
    a[12]=-x;
    a[13]=-y;
    a[14]=-z;
    }
//---------------------------------------------------------------------------
void  matrix_mul_vector(double *c,double *a,double *b) // c[3] = a[16]*b[3]
    {
    double q[3];
    q[0]=(a[ 0]*b[0])+(a[ 4]*b[1])+(a[ 8]*b[2])+(a[12]);
    q[1]=(a[ 1]*b[0])+(a[ 5]*b[1])+(a[ 9]*b[2])+(a[13]);
    q[2]=(a[ 2]*b[0])+(a[ 6]*b[1])+(a[10]*b[2])+(a[14]);
    for(int i=0;i<3;i++) c[i]=q[i];
    }
//---------------------------------------------------------------------------
void compute_matrices() // recompute camera,icamera after camera position or yaw change
    {
    // bound angle
    while (yaw>2.0*M_PI) yaw-=2.0*M_PI;
    while (yaw<0.0     ) yaw+=2.0*M_PI;
    // X = right
    camera[ 0]= cos(yaw);
    camera[ 1]=     0.0 ;
    camera[ 2]= sin(yaw);
    // Y = up
    camera[ 4]=     0.0 ;
    camera[ 5]=     1.0 ;
    camera[ 6]=     0.0 ;
    // Z = forward
    camera[ 8]=-sin(yaw);
    camera[ 9]=     0.0 ;
    camera[10]= cos(yaw);
    // no projection
    camera[ 3]=     0.0 ;
    camera[ 7]=     0.0 ;
    camera[11]=     0.0 ;
    camera[15]=     1.0 ;
    // compute the inverse matrix
    matrix_inv(icamera,camera);
    }
//---------------------------------------------------------------------------
void perspective(double *P) // apply perspective transform
    {
    // perspectve division
    P[0]*=znear/P[2];
    P[1]*=znear/P[2];
    // screen coordinate system
    P[0]=xs2+P[0];          // move (0,0) to screen center
    P[1]=ys2-P[1];          // axises: x=right, y=up
    }
//---------------------------------------------------------------------------
void draw_line(TCanvas *can,double *pA,double *pB)  // draw 3D line
    {
    int i;
    double D[3],A[3],B[3],t;
    // transform to camera coordinate system
    matrix_mul_vector(A,icamera,pA);
    matrix_mul_vector(B,icamera,pB);
    // sort points so A.z<B.z
    if (A[2]>B[2]) for (i=0;i<3;i++) { D[i]=A[i]; A[i]=B[i]; B[i]=D[i]; }
    // D = B-A
    for (i=0;i<3;i++) D[i]=B[i]-A[i];
    // ignore out of Z view lines
    if (A[2]>zfar) return;
    if (B[2]<znear) return;
    // cut line to view if needed
    if (A[2]<znear)
        {
        t=(znear-A[2])/D[2];
        A[0]+=D[0]*t;
        A[1]+=D[1]*t;
        A[2]=znear;
        }
    if (B[2]>zfar)
        {
        t=(zfar-B[2])/D[2];
        B[0]+=D[0]*t;
        B[1]+=D[1]*t;
        B[2]=zfar;
        }
    // apply perspective
    perspective(A);
    perspective(B);
    // render
    can->MoveTo(A[0],A[1]);
    can->LineTo(B[0],B[1]);
    }
//---------------------------------------------------------------------------
void draw_plane_xz(TCanvas *can,double y,double step) // draw 3D plane
    {
    int i;
    double A[3],B[3],t,size;
    double U[3]={1.0,0.0,0.0};  // U = X
    double V[3]={0.0,0.0,1.0};  // V = Z
    double O[3]={0.0,0.0,0.0};  // Origin
    // compute origin near view center but align to step
    i=0; O[i]=floor(camera[12+i]/step)*step;
    i=2; O[i]=floor(camera[12+i]/step)*step;
    O[1]=y;
    // set size so plane safely covers whole view
    t=xs2*zfar/znear;               size=t; // x that will convert to xs2 at zfar
    t=0.5*(zfar+znear); if (size<t) size=t; // half of depth range
    t+=step;                                // + one grid cell beacuse O is off up to 1 grid cell
    t*=sqrt(2);                             // diagonal so no matter how are we rotate in Yaw
    // U lines
    for (i=0;i<3;i++)
        {
        A[i]=O[i]+(size*U[i])-((step+size)*V[i]);
        B[i]=O[i]-(size*U[i])-((step+size)*V[i]);
        }
    for (t=-size;t<=size;t+=step)
        {
        for (i=0;i<3;i++)
            {
            A[i]+=step*V[i];
            B[i]+=step*V[i];
            }
        draw_line(can,A,B);
        }
    // V lines
    for (i=0;i<3;i++)
        {
        A[i]=O[i]-((step+size)*U[i])+(size*V[i]);
        B[i]=O[i]-((step+size)*U[i])-(size*V[i]);
        }
    for (t=-size;t<=size;t+=step)
        {
        for (i=0;i<3;i++)
            {
            A[i]+=step*U[i];
            B[i]+=step*U[i];
            }
        draw_line(can,A,B);
        }
    matrix_mul_vector(A,icamera,A);
    }
//---------------------------------------------------------------------------
void TMain::draw() // this is my main rendering routine
    {
    // clear buffer
    bmp->Canvas->Brush->Color=clWhite;
    bmp->Canvas->FillRect(TRect(0,0,xs,ys));
    // init/update variables
    double step= 50.0;                              // plane grid size
    ::xs2=Main->xs2;                                // update actual screen half resolution
    ::ys2=Main->ys2;
    // sky
    bmp->Canvas->Pen->Color=clBlue;
    draw_plane_xz(bmp->Canvas,+200.0,step);
    // terrain
    bmp->Canvas->Pen->Color=clGreen;
    draw_plane_xz(bmp->Canvas,-200.0,step);
    // render backbuffer
    Main->Canvas->Draw(0,0,bmp);
    _redraw=false;
    }
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner) // this is initialization
    {
    bmp=new Graphics::TBitmap;
    bmp->HandleType=bmDIB;
    bmp->PixelFormat=pf32bit;
    pyx=NULL;
    _redraw=true;


    // camera start position
    camera[12]=0.0;
    camera[13]=0.0;
    camera[14]=0.0;
    compute_matrices();
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormDestroy(TObject *Sender) // this is exit
    {
    if (pyx) delete[] pyx;
    delete bmp;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormResize(TObject *Sender) // this is called on resize
    {
    xs=ClientWidth;  xs2=xs>>1;
    ys=ClientHeight; ys2=ys>>1;
    bmp->Width=xs;
    bmp->Height=ys;
    if (pyx) delete[] pyx;
    pyx=new int*[ys];
    for (int y=0;y<ys;y++) pyx[y]=(int*) bmp->ScanLine[y];
    _redraw=true;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormPaint(TObject *Sender) // this is called on forced repaint
    {
    _redraw=true;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::tim_redrawTimer(TObject *Sender) // this is called periodically by my timer
    {
    double da=5.0*M_PI/180.0;   // turn speed
    double dl=15.0;             // movement speed
    bool _recompute=false;
    if (_left ) { _redraw=true; _recompute=true; yaw+=da; }
    if (_right) { _redraw=true; _recompute=true; yaw-=da; }
    if (_forw ) { _redraw=true; _recompute=true; for (int i=0;i<3;i++) camera[12+i]+=dl*camera[8+i]; }
    if (_back ) { _redraw=true; _recompute=true; for (int i=0;i<3;i++) camera[12+i]-=dl*camera[8+i]; }
    if (_recompute) compute_matrices();
    if (_redraw) draw();
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormKeyDown(TObject *Sender, WORD &Key,TShiftState Shift) // this is called when key is pushed
    {
    //Caption=Key;
    if (Key==104) _left=true;
    if (Key==105) _right=true;
    if (Key==100) _forw=true;
    if (Key== 97) _back=true;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift) // this is called when key is released
    {
    if (Key==104) _left=false;
    if (Key==105) _right=false;
    if (Key==100) _forw=false;
    if (Key== 97) _back=false;
    }
//---------------------------------------------------------------------------

Here the Form header file (you do not really need it unless you are reconstructin my VCL app)

//---------------------------------------------------------------------------

#ifndef win_mainH
#define win_mainH
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ComCtrls.hpp>
#include <ExtCtrls.hpp>
//---------------------------------------------------------------------------
class TMain : public TForm
{
__published:    // IDE-managed Components
    TTimer *tim_redraw;
    void __fastcall FormResize(TObject *Sender);
    void __fastcall FormPaint(TObject *Sender);
    void __fastcall FormDestroy(TObject *Sender);
    void __fastcall tim_redrawTimer(TObject *Sender);
    void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift);
    void __fastcall FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift);
private:    // User declarations
public:     // User declarations
    __fastcall TMain(TComponent* Owner);
    void draw();

    int xs,ys,xs2,ys2,**pyx;
    Graphics::TBitmap *bmp;
    bool _redraw;
};
//---------------------------------------------------------------------------
extern PACKAGE TMain *Main;
//---------------------------------------------------------------------------
#endif

The VCL app is just single Form with single timer (100ms) on it and no other VCL components. The bmp is just my backbuffer bitmap to avoid flickering. The keyboard events are just to enable turning and movement (with numpad 8,9,4,1).

Here preview of above code:

preview

Now if you want to add the whiteout visibility limiter that is done by Fog or Volumetric fog. You simply interpolate between rendered color and White based on parameter t:

t = (z-znear)/(zfar-znear); // t = <0,1>

where z is pixel coordinate in camera space so:

color = color*(1.0-t) + White*t;

But to apply this in here we would need to encode the 2D line rasterizer or have 2D line api with per vertex color (like OpenGL). Another option is to fake it by blending in a Fog image which is fully solid near center line and fully transparent on the top and bottom edges.

Spektre
  • 41,942
  • 8
  • 91
  • 312
  • Good! You help me a lot. I found another explanation that is called by Mode 7, the same method that SNES uses to create thperspective view mapping some texture. I will put my answer for it here. – Vinícius Biavatti Sep 17 '17 at 18:41
  • 1
    @ViníciusBiavatti the raster techniques (texture pixel mappings) are usually based on [Wolfenstein like raycasting techniques](https://stackoverflow.com/a/32733650/2521214) ... for both walls and floor/ceiling – Spektre Sep 17 '17 at 19:16
2

I found the method that was used to create the perspective view in old games. Check my tutorial here: http://programandocoisas.blogspot.com.br/2017/09/mode-7.html. The method's name is MODE 7. I made a tutorial to help wo wants to implement and understand it. The formula to make the mode 7 on a texture is:

_X = X / Z
_Y = Y / Z

Z you can use to create the depth. This variable is just an incremented variable on the Y coord. After get the _X and _Y new coords, just use these coords to get the pixel in the texture that will be mapped, and insert this pixel in X Y coord in the render view.

Here is the pseudo-code: Basically, the pseudo code is it:

//This is the pseudo-code to generate the basic mode7

for each y in the view do
    y' <- y / z
    for each x in the view do
        x' <- x / z
        put x',y' texture pixel value in x,y view pixel
    end for
    z <- z + 1
end for

Here is the code:

package mode7;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;

/**
 * Mode 7 - Basic Implementation
 * This code will map a texture to create a pseudo-3d perspective.
 * This is an infinite render mode. The texture will be repeated without bounds.
 * @author VINICIUS
 */
public class BasicModeSeven {

    //Sizes
    public static final int WIDTH = 800;
    public static final int WIDTH_CENTER = WIDTH/2;
    public static final int HEIGHT = 600;
    public static final int HEIGHT_CENTER = HEIGHT/2;

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws IOException {

        //Create Frame
        JFrame frame = new JFrame("Mode 7");
        frame.setSize(WIDTH, HEIGHT);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

        //Create Buffered Images:
        //image - This is the image that will be printed in the render view
        //texture - This is the image that will be mapped to the render view
        BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
        BufferedImage texture = ImageIO.read(new File("src/mode7/texture.png"));

        //The new coords that will be used to get the pixel on the texture
        double _x, _y;

        //z - the incrementable variable that beggins at -300 and go to 300, because 
        //the depth will be in the center of the HEIGHT
        double z =  HEIGHT_CENTER * -1;

        //Scales just to control de scale of the printed pixel. It is not necessary
        double scaleX = 16.0;
        double scaleY = 16.0; 

        //Mode 7 - loop (Left Top to Down)
        for(int y = 0; y < HEIGHT; y++){

            _y = y / z; //The new _y coord generated
            if(_y < 0)_y *= -1; //Control the _y because the z starting with a negative number
            _y *= scaleY; //Increase the size using scale
            _y %= texture.getHeight(); //Repeat the pixel avoiding get texture out of bounds 

            for(int x = 0; x < WIDTH; x++){

                _x = (WIDTH_CENTER - x) / z; //The new _x coord generated
                if(_x < 0)_x *= -1; //Control the _x to dont be negative
                _x *= scaleX; //Increase the size using scale
                _x %= texture.getWidth(); //Repeat the pixel avoiding get texture out of bounds 

                //Set x,y of the view image with the _x,_y pixel in the texture
                image.setRGB(x, y, texture.getRGB((int)_x, (int)_y));
            }

            //Increment depth
            z++;
        }

        //Loop to render the generated image
        while(true){
            frame.getGraphics().drawImage(image, 0, 0, null);
        }
    }
}

This is the result:

enter image description here