9

I have a java project who makes the "windows' maze" and use the ray casting algorithm. Here's a screenshot :

Maze made with ray casting

Like you can see all the walls have the same height size. I would like to do the same but with different height size

private void castRay(int xOnScreen,double angle,double direction) {
    R rx = castRayInX(angle,direction);
    R ry = castRayInY(angle,direction);
    // In case of out-of-space rays
    if (rx.getDistance()==Double.MAX_VALUE && ry.getDistance()==Double.MAX_VALUE) {
        graphics.setColor(BACKGROUND);
        graphics.drawLine(xOnScreen,0,xOnScreen,this.image.getHeight());
        return;
    }
    double distance = rx.getDistance();
    double normal = rx.getNormal();
    Color c = rx.getColor();
    double coef = Math.cos((angle+direction+Math.PI)-normal);
    Plot collision = rx.getPlot();

    if (ry.getDistance()<rx.getDistance()) {
        distance = ry.getDistance();
        normal = ry.getNormal();
        c = ry.getColor();
        coef = Math.cos((angle+direction+Math.PI)-normal);
        collision = ry.getPlot();
    }

    coef = Math.abs(coef);
    int factor = map.length*SQUARE_SIZE;
    double d = (double)(distance+factor)/factor;
    coef *= 1/(d*d);
    Color c2 = new Color((int)(c.getRed()*coef),(int)(c.getGreen()*coef),(int)(c.getBlue()*coef));
    graphics.setColor(c2);
//  graphics.setColor(c);  // no illumination
    distance *= Math.cos(angle); // lens correction
    int h = (int)(this.screenDistance/distance*WALL_HEIGHT); // perspective height
    int vh = this.image.getHeight();
    graphics.drawLine(xOnScreen,(vh-h)/2,xOnScreen,(vh+h)/2);
    drawEye(direction,collision);
}

private R castRayInX(double angleRay,double direction) {
    double angle = angleRay+direction;
    double x1 = eye.getX()+SQUARE_SIZE*Math.cos(angle);
    double y1 = eye.getY()+SQUARE_SIZE*Math.sin(angle);
    double slope = (y1-eye.getY())/(x1-eye.getX());
    if (Math.cos(angle)==0) {
        if (Math.sin(angle)>0)
            return new R(Double.MAX_VALUE,3*Math.PI/2,BACKGROUND,null);
        else
            return new R(Double.MAX_VALUE,Math.PI/2,BACKGROUND,null);
    }
    if (Math.cos(angle)>0) {
        int firstX = ((eye.getX()/SQUARE_SIZE)+1)*SQUARE_SIZE;
        R r = new R(Double.MAX_VALUE,angle+Math.PI,BACKGROUND,null);
        for (int x = firstX; x<map[0].length*SQUARE_SIZE; x += SQUARE_SIZE) {
            int y = (int)(slope*(x-eye.getX())+eye.getY());
            if (isOutside(x,y,Color.MAGENTA,this.showRayCastingX)) break;
            Color c = colorAt(x,y);
            if (c==null) c = colorAt(x,y-1);
            if (c==null) c = colorAt(x-1,y);
            if (c==null) c = colorAt(x-1,y-1);
            if (c!=null) {
                int DX = x-eye.getX();
                double DY = y-eye.getY();
                return new R(Math.sqrt(DX*DX+DY*DY),Math.PI,c,new Plot((int)x,(int)y, WALL_HEIGHT));
            }
        }
        return r;
    } else {
        int firstX = ((eye.getX()/SQUARE_SIZE))*SQUARE_SIZE;
        R r = new R(Double.MAX_VALUE,angle+Math.PI,BACKGROUND,null);
        for (int x = firstX; x>=0; x -= SQUARE_SIZE) {
            int y = (int)(slope*(x-eye.getX())+eye.getY());
            if (isOutside(x,y,Color.MAGENTA,this.showRayCastingX)) break;
            Color c = colorAt(x,y);
            if (c==null) c = colorAt(x,y-1);
            if (c==null) c = colorAt(x-1,y);
            if (c==null) c = colorAt(x-1,y-1);
            if (c!=null) {
                int DX = x-eye.getX();
                double DY = y-eye.getY();
                return new R(Math.sqrt(DX*DX+DY*DY),0,c,new Plot((int)x,(int)y, WALL_HEIGHT));
            }
        }
        return r;           
    }
}
private R castRayInY(double angleRay,double direction) {
//  System.out.println("cast ray 2 Y "+angleRay+" "+direction);
    double angle = angleRay+direction;
    double x1 = eye.getX()+SQUARE_SIZE*Math.cos(angle);
    double y1 = eye.getY()+SQUARE_SIZE*Math.sin(angle);
//  System.out.println(eye+" "+x1+" "+y1);
    double slope = (y1-eye.getY())/(x1-eye.getX());
    if (Math.sin(angle)==0) {
        if (Math.cos(angle)>0)
            return new R(Double.MAX_VALUE,Math.PI,BACKGROUND,null);
        else
            return new R(Double.MAX_VALUE,0,BACKGROUND,null);
    }
    if (Math.sin(angle)>0) {
        int firstY = ((eye.getY()/SQUARE_SIZE)+1)*SQUARE_SIZE;
        R r = new R(Double.MAX_VALUE,angle+Math.PI,BACKGROUND,null);
        for (int y = firstY; y<map.length*SQUARE_SIZE; y += SQUARE_SIZE) {
            int x = (int)((y-eye.getY())/slope)+eye.getX();
            if (isOutside(x,y,Color.CYAN,this.showRayCastingY)) break;
            Color c = colorAt(x,y);
            if (c==null) c = colorAt(x,y-1);
            if (c==null) c = colorAt(x-1,y);
            if (c==null) c = colorAt(x-1,y-1);
            if (c!=null) {
                double DX = x-eye.getX();
                int DY = y-eye.getY();
                return new R(Math.sqrt(DX*DX+DY*DY),3*Math.PI/2,c,new Plot((int)x,(int)y, WALL_HEIGHT));
                }
            }
            return r;
        } else {
            int firstY = ((eye.getY()/SQUARE_SIZE))*SQUARE_SIZE;
            R r = new R(Double.MAX_VALUE,angle+Math.PI,BACKGROUND,null);
            for (int y = firstY; y>=0; y -= SQUARE_SIZE) {
                int x = (int)((y-eye.getY())/slope)+eye.getX();
                if (isOutside(x,y,Color.CYAN,this.showRayCastingY)) break;
                Color c = colorAt(x,y);
                if (c==null) c = colorAt(x,y-1);
                if (c==null) c = colorAt(x-1,y);
                if (c==null) c = colorAt(x-1,y-1);
                if (c!=null) {
                    double DX = x-eye.getX();
                    int DY = y-eye.getY();
                    return new R(Math.sqrt(DX*DX+DY*DY),Math.PI/2,c,new Plot((int)x,(int)y, WALL_HEIGHT));
                }
            }
            return r;           
        }
    }

My Rclass has a Plot (x, y, z) for now I use WALL_HEIGHT a color, a distance and a normal for the light. For now this works but I would like to add a new function like castRayInZ but I don't have all the mathematics theory behind. My maze is made from a map like that :

private String [][]map = {  // each: SQUARE_SIZE x SQUARE_SIZE
        { "Y300", "Z500", "X230", "Y112", "Z321", "X120", "X354" },
        { "X89", " ", " ", " ", "Y120", " ", "X232" },
        { "Z124", " ", "X276", " ", "X123", " ", "X" },
        { "Y290", " ", " ", " ", " ", " ", "X100" },
        { "X32", "Z430", " ", "Y500", "X120", " ", "X123" },
        { "X222", " ", " ", " ", " ", " ", "X210" },
        { "X12", "Y98", "Y763", "X146", "Y111", "Y333", "X321" }

where X Y Z is for the color (X for Red, Y for Green and Z for Blue just testing my light function) and I add a height for each square of my map. I set all of length to SQUARE_LENGTH for now maybe later I will reduce the size of each square to a pixel and enlarge my map by generating it. But I really want to know how can I change the height of each square. I'm working on it since 4 days now and I don't have any clues...

EDIT

I have some news, I changed the size of my walls but I have some strange stuff, here's a screenshot :

Strange stuff image

Like you can see I there's some strange things appear here. Here's my code :

private void castRay(int xOnScreen,double angle,double direction) {
    R rx = castRayInX(angle,direction);
    R ry = castRayInY(angle,direction);
    // In case of out-of-space rays
    if (rx.getDistance()==Double.MAX_VALUE && ry.getDistance()==Double.MAX_VALUE) {
        graphics.setColor(BACKGROUND);
        graphics.drawLine(xOnScreen,0,xOnScreen,this.image.getHeight());
        return;
    }
    double distance = rx.getDistance();
    double normal = rx.getNormal();
    Color c = rx.getColor();
    double coef = Math.cos((angle+direction+Math.PI)-normal);
    Plot collision = rx.getPlot();

    if (ry.getDistance()<rx.getDistance()) {
        distance = ry.getDistance();
        normal = ry.getNormal();
        c = ry.getColor();
        coef = Math.cos((angle+direction+Math.PI)-normal);
        collision = ry.getPlot();
    }

    coef = Math.abs(coef);
    int factor = map.length*SQUARE_SIZE;
    double d = (double)(distance+factor)/factor;
    coef *= 1/(d*d);
    Color c2 = new Color((int)(c.getRed()*coef),(int)(c.getGreen()*coef),(int)(c.getBlue()*coef));
graphics.setColor(c);
    distance *= Math.cos(angle); // lens correction
    int h;
    int hw = (int)(this.screenDistance/distance*WALL_HEIGHT); //WALL_HEIGHT value is 300px at default
    if(rx.getPlot() != null)
        h = (int)(this.screenDistance/distance*rx.getPlot().getZ()); // perspective height
    else
        h = (int)(this.screenDistance/distance*WALL_HEIGHT);
    int vh = this.image.getHeight();
    int y0 = (hw+vh)/2;
    int y1 = (vh-h)/2;
    graphics.drawLine(xOnScreen,y0,xOnScreen,y1);
    drawEye(direction,collision);

My problem should be from castRayInXfunction :

private R castRayInX(double angleRay,double direction) {
    double angle = angleRay+direction;
    double x1 = eye.getX()+SQUARE_SIZE*Math.cos(angle);
    double y1 = eye.getY()+SQUARE_SIZE*Math.sin(angle);
    double slope = (y1-eye.getY())/(x1-eye.getX());
    if (Math.cos(angle)==0) {
        if (Math.sin(angle)>0)
            return new R(Double.MAX_VALUE,3*Math.PI/2,BACKGROUND,null);
        else
            return new R(Double.MAX_VALUE,Math.PI/2,BACKGROUND,null);
    }
    if (Math.cos(angle)>0) {
        int firstX = ((eye.getX()/SQUARE_SIZE)+1)*SQUARE_SIZE;
        R r = new R(Double.MAX_VALUE,angle+Math.PI,BACKGROUND,null);
        for (int x = firstX; x<map[0].length*SQUARE_SIZE; x += SQUARE_SIZE) {
            int y = (int)(slope*(x-eye.getX())+eye.getY());
            if (isOutside(x,y,Color.MAGENTA,this.showRayCastingX)) break;
            Color c = colorAt(x,y);
            int z = heightAt(x,y);
            if (c==null) c = colorAt(x,y-1);
            if (c==null) c = colorAt(x-1,y);
            if (c==null) c = colorAt(x-1,y-1);
            if (z == 0) z = heightAt(x,y-1);
            if (z == 0) z = heightAt(x-1,y);
            if (z == 0) z = heightAt(x-1,y-1);
            if (c!=null) {
                int DX = x-eye.getX();
                double DY = y-eye.getY();
                return new R(Math.sqrt(DX*DX+DY*DY),Math.PI,c,new Plot((int)x,(int)y,(int)z));
            }
        }
        return r;
    } else {
        int firstX = ((eye.getX()/SQUARE_SIZE))*SQUARE_SIZE;
        R r = new R(Double.MAX_VALUE,angle+Math.PI,BACKGROUND,null);
        for (int x = firstX; x>=0; x -= SQUARE_SIZE) {
            int y = (int)(slope*(x-eye.getX())+eye.getY());
            if (isOutside(x,y,Color.MAGENTA,this.showRayCastingX)) break;
            Color c = colorAt(x,y);
            int z = heightAt(x,y);
            if (c==null) c = colorAt(x,y-1);
            if (c==null) c = colorAt(x-1,y);
            if (c==null) c = colorAt(x-1,y-1);
            if (z == 0) z = heightAt(x,y-1);
            if (z == 0) z = heightAt(x-1,y);
            if (z == 0) z = heightAt(x-1,y-1);
            if (c!=null) {
                int DX = x-eye.getX();
                double DY = y-eye.getY();
                return new R(Math.sqrt(DX*DX+DY*DY),0,c,new Plot((int)x,(int)y,(int)z));
            }
        }
        return r;           
    }
}

Should I make a castRayInZfunction ? Or should I get my z value somewhere else ?

Jack
  • 385
  • 2
  • 10
  • 1
    Didn't you already answer the question yourself? You need different height values per patch instead of `WALL_HEIHGT`. What are you having trouble with? – Nico Schertler Nov 12 '17 at 07:15
  • It's easy by using a constant value, but I don't know how to get all my different heights. I have some strange things – Jack Nov 12 '17 at 15:58

1 Answers1

15

so you obviously know the basics of Wolfenstein raycasting techniques. To add variable height you need to do this:

  1. add height info on per cell basis

so simply add another value to your cell info in your map table map[][]. You code the stuff as strings that is odd ...

  1. update scan line render

Somewhere in the code (after hit was detected) you render vertical line per each ray. There you should compute the scan line size something like (assuming y=0 is top of the screen):

    y0 = center_of_view_y + projected_half_size
    y1 = center_of_view_y - projected_half_size

And should change to:

    y0 = center_of_view_y + projected_size
    y1 = y0 - 2*projected_half_size*wall_size

Where projected_half_size is the line size computed for constant cell height as you got now, wall_size=<0,1> is scale and center_of_view_y is the y coordinate of the horizon line in your view. This will put your wall on the ground.

  1. update ray-casting

now when you hit the first wall you stop. With variable wall height you can stop only when you hit wall of full size (wall_size=1) or run out of map. You got 2 option to implement this.

  1. remember all hits and render in reverse order
  2. render immediately but only from last rendered height instead of from ground.

The first option is easy to implement but requires more memory and it is slower. The second one is fast and does not need any lists or stacks. But it involves a little bit more math for the scan line render (O(1) if coded right)

I played a bit with my demo from the link at the top. Now the result should look like this:

variable height

As you can see the highlighted cells on the map are pass through for rays above their height (so you can see bigger cells behind them).

Beware once you add movement altitude direction (jumps, stairs etc) then the end condition must be different (the rendered scan line hit the top of the view). Also projection part of y coordinate will be different and need to include the actual player altitude.

  1. add topside

You need to add the rendering of top side. It is similar to rendering ceiling and floor. IIRC original Wolfenstein did not have this capability but the latter pseudo 3D games like DOOM did.

There are more possible approaches like Perspective Vision on Canvas but I think the easiest to implement (as we already got enough info) is to compute the top side part of vertical scanline coordinates in texture and just copy pixels. As we already know where the ray hit the cell and angle of player/camera is also known. For more info see: PCGPE 1.0 Doom techniques

So as a first step add hits for back faces too. That should look like this:

back faces

That is done by checking hits of the last hit cell first. Now if you remember the last rendered y coordinate from previous hit (of the same scanline) then if back face is hit instead of rendering the face render top side color from last y to actual y (or copy pixels from floor/ceiling texture instead). Here the green color used for this:

top side

If it helps here is mine C++ (GDI/VCL based) code for this:

//---------------------------------------------------------------------------
//--- Doom 3D engine ver: 1.000 --------------------------------------
//---------------------------------------------------------------------------
#ifndef _Doom3D_h
#define _Doom3D_h
//---------------------------------------------------------------------------
#include <math.h>
#include <jpeg.hpp>
#include "performance.h"
#include "OpenGLrep4d_double.h"
//---------------------------------------------------------------------------
const DWORD _Doom3D_cell_size=10;   // 2D map cell size
const DWORD _Doom3D_wall_size=100;  // full height of wall in map
#define _Doom3D_filter_txr
//---------------------------------------------------------------------------
class Doom3D
    {
public:
    DWORD mxs,mys,**pmap;           // 2D map   // txr + height<<16
    DWORD sxs,sys,**pscr;           // pseudo 3D screen
    Graphics::TBitmap *scr;
    DWORD txs,tys,**ptxr,tn;        // 2D textures
    Graphics::TBitmap *txr,*txr2;   // textures, texture mipmaps resolution: /2 and /4
    double plrx,plry,plrz,plra;     // player position [x,y,z,angle]
    double view_ang;                // [rad] view angle
    double focus;                   // [cells] view focal length
    struct _ray
        {
        double x,y,l;               // hit or end of map position
        DWORD hit;                  // map cell of hit or 0xFFFFFFFF
        char typ;                   // H/V
        _ray() {};
        _ray(_ray& a)   { *this=a; }
        ~_ray() {};
        _ray* operator = (const _ray *a) { *this=*a; return this; }
        //_ray* operator = (const _ray &a) { ..copy... return this; }
        };
    _ray *ray;                      // ray[sxs]

    keytab keys;
    DWORD txr_sel;
    DWORD cell_h;

    Doom3D();
    Doom3D(Doom3D& a)   { *this=a; }
    ~Doom3D();
    Doom3D* operator = (const Doom3D *a) { *this=*a; return this; }
    //Doom3D* operator = (const Doom3D &a) { ..copy... return this; }

    void map_resize(DWORD xs,DWORD ys); // change map resolution
    void map_height(DWORD height);      // set height for whole map to convert maps from Wolfenstein3D demo
    void map_clear();                   // clear whole map
    void map_save(AnsiString name);
    void map_load(AnsiString name);
    void scr_resize(DWORD xs,DWORD ys);
    void txr_load(AnsiString name);

    void draw();
    void update(double dt);
    void mouse(double x,double y,TShiftState sh)
        {
        x=floor(x/_Doom3D_cell_size); if (x>=mxs) x=mxs-1; if (x<0) x=0;
        y=floor(y/_Doom3D_cell_size); if (y>=mys) y=mys-1; if (y<0) y=0;
        DWORD xx=x,yy=y;
        keys.setm(x,y,sh);
        if (keys.Shift.Contains(ssLeft )) pmap[yy][xx]=(txr_sel)|(cell_h<<16);
        if (keys.Shift.Contains(ssRight)) pmap[yy][xx]=0xFFFFFFFF;
        keys.rfsmouse();
        }
    void wheel(int delta,TShiftState sh)
        {
        if (sh.Contains(ssShift))
            {
            if (delta<0) { cell_h-=10; if (cell_h<10) cell_h=10; }
            if (delta>0) { cell_h+=10; if (cell_h>_Doom3D_wall_size) cell_h=_Doom3D_wall_size; }
            }
        else{
            if (delta<0) { txr_sel--; if (txr_sel==0xFFFFFFFF) txr_sel=tn-1; }
            if (delta>0) { txr_sel++; if (txr_sel==        tn) txr_sel=   0; }
            }
        }
    };
//---------------------------------------------------------------------------
Doom3D::Doom3D()
    {
    mxs=0; mys=0;                            pmap=NULL;
    sxs=0; sys=0; scr=new Graphics::TBitmap; pscr=NULL; ray=NULL;
    txs=0; tys=0; txr=new Graphics::TBitmap; ptxr=NULL; tn=0;
                  txr2=new Graphics::TBitmap;
    plrx=0.0; plry=0.0; plrz=0.0; plra=0.0;
    view_ang=60.0*deg;
    focus=0.25;
    txr_sel=0;
    cell_h=_Doom3D_wall_size;

    txr_load("textures128x128.jpg");
    map_resize(16,16);
    map_load("Doom3D_map.dat");
    }
//---------------------------------------------------------------------------
Doom3D::~Doom3D()
    {
    DWORD y;
    map_save("Doom3D_map.dat");
    if (pmap) { for (y=0;y<mys;y++) delete[] pmap[y]; delete[] pmap; pmap=NULL; } if (ray) delete[] ray; ray=NULL;
    if (pscr) {                                       delete[] pscr; pscr=NULL; } if (scr) delete scr; scr=NULL;
    if (ptxr) {                                       delete[] ptxr; ptxr=NULL; } if (txr) delete txr; txr=NULL;
                                                                                  if (txr2) delete txr2; txr2=NULL;
    }
//---------------------------------------------------------------------------
void Doom3D::map_resize(DWORD xs,DWORD ys)
    {
    DWORD y;
    if (pmap) { for (y=0;y<mys;y++) delete[] pmap[y]; delete[] pmap; pmap=NULL; }
    mys=ys; mxs=xs; pmap=new DWORD*[mys]; for (y=0;y<mys;y++) pmap[y]=new DWORD[mxs];
    map_clear();
    plrx=(mxs-1)*0.5; plry=(mys-1)*0.5; plrz=0.0; plra=0.0*deg;
    }
//---------------------------------------------------------------------------
void Doom3D::map_height(DWORD h)
    {
    DWORD x,y,c;
    for (y=0;y<mys;y++)
     for (x=0;x<mxs;x++)
        {
        c=pmap[y][x];
        c&=0xFFFF;
        c|=h<<16;
        pmap[y][x]=c;
        }
    }
//---------------------------------------------------------------------------
void Doom3D::map_clear()
    {
    DWORD x,y,c;
    for (y=0;y<mys;y++)
     for (x=0;x<mxs;x++)
        {
        c=0xFFFFFFFF;
        if ((x==0)||(x==mxs-1)) c=0;
        if ((y==0)||(y==mys-1)) c=0;
        pmap[y][x]=c;
        }
    }
//---------------------------------------------------------------------------
void Doom3D::map_save(AnsiString name)
    {
    int hnd=FileCreate(name); if (hnd<0) return;
    DWORD y;
    y=' PAM';
    FileWrite(hnd,&y  ,4);  // id
    FileWrite(hnd,&mxs,4);  // x resolution
    FileWrite(hnd,&mys,4);  // y resolution
    for (y=0;y<mys;y++)     // map
     FileWrite(hnd,pmap[y],mxs<<2);
    y=' RLP';
    FileWrite(hnd,&y  ,4);  // id
    FileWrite(hnd,&plrx,8);
    FileWrite(hnd,&plry,8);
    FileWrite(hnd,&plrz,8);
    FileWrite(hnd,&plra,8);
    FileClose(hnd);
    }
//---------------------------------------------------------------------------
void Doom3D::map_load(AnsiString name)
    {
    int hnd=FileOpen(name,fmOpenRead); if (hnd<0) return;
    DWORD x,y;
    y=' PAM'; FileRead(hnd,&x  ,4); // id
    if (x==y)
        {
        FileRead(hnd,&x,4); // x resolution
        FileRead(hnd,&y,4); // y resolution
        map_resize(x,y);
        for (y=0;y<mys;y++) // map
         FileRead(hnd,pmap[y],mxs<<2);
        }
    y=' RLP'; FileRead(hnd,&x  ,4); // id
    if (x==y)
        {
        FileRead(hnd,&plrx,8);
        FileRead(hnd,&plry,8);
        FileRead(hnd,&plrz,8);
        FileRead(hnd,&plra,8);
        }
    FileClose(hnd);
    }
//---------------------------------------------------------------------------
void Doom3D::scr_resize(DWORD xs,DWORD ys)
    {
    scr->HandleType=bmDIB;
    scr->PixelFormat=pf32bit;
    scr->SetSize(xs,ys);
    sxs=scr->Width;
    sys=scr->Height;
    delete[] pscr; pscr=new DWORD*[sys];
    for (DWORD y=0;y<sys;y++) pscr[y]=(DWORD*)scr->ScanLine[y];
    if (ray) delete[] ray; ray=new _ray[sxs];
    }
//---------------------------------------------------------------------------
void Doom3D::txr_load(AnsiString name)
    {
    AnsiString ext=ExtractFileExt(name).LowerCase();
    for(;;)
        {
        if (ext==".bmp")
            {
            txr->LoadFromFile(name);
            break;
            }
        if (ext==".jpg")
            {
            TJPEGImage *jpg=new TJPEGImage;
            if (jpg==NULL) return;
            jpg->LoadFromFile(name);
            txr->Assign(jpg);
            delete jpg;
            break;
            }
        return;
        }
    DWORD y=tys;
    txr->HandleType=bmDIB;
    txr->PixelFormat=pf32bit;
    txs=txr->Width;
    tys=txr->Height;
    // mip map
    txr2->SetSize(txs>>1,(tys>>1)+(tys>>2));
    txr2->Canvas->StretchDraw(TRect(0,     0,txs>>1,tys>>1),txr);
    txr2->Canvas->StretchDraw(TRect(0,tys>>1,txs>>2,(tys>>1)+(tys>>2)),txr);
    tn=txs/tys; txs=tys;
    delete[] ptxr; ptxr=new DWORD*[tys];
    for (y=0;y<tys;y++) ptxr[y]=(DWORD*)txr->ScanLine[y];
    }
//---------------------------------------------------------------------------
void Doom3D::draw()
    {
    // total time measurement
    tbeg(); double tperf0=performance_tms;

    AnsiString tcls,tray,tmap,ttotal;
    double a,a0,da,dx,dy,l,mx,my;
    DWORD x,y,xs2,ys2,c,m;
    double xx0,yy0,dx0,dy0,ll0; DWORD c0,d0;
    double xx1,yy1,dx1,dy1,ll1; DWORD c1,d1;
    _ray *p;
    xs2=sxs>>1;
    ys2=sys>>1;

    // aspect ratio,view angle corrections
    a=90.0*deg-view_ang;
    double wall=double(sxs)*(1.25+(0.288*a)+(2.04*a*a)); // [px]

    // floor,ceilling/sky
    tbeg();
    for (y=0;y<ys2;y++) for (x=0;x<sxs;x++) pscr[y][x]=0x000080FF;
    for (   ;y<sys;y++) for (x=0;x<sxs;x++) pscr[y][x]=0x00404040;
    tend(); tcls=tstr(1)+" cls";

    // [cast rays]
    tbeg();
    // diffuse + ambient lighting
    DWORD ch=155.0+fabs(100.0*sin(plra));
    DWORD cv=155.0+fabs(100.0*cos(plra));
    a0=plra-(0.5*view_ang);
    da=divide(view_ang,sxs-1);
    mx=mxs; my=mys;
    for (p=ray,a=a0,x=0;x<sxs;x++,a+=da,p++)
        {
        p->x=plrx;
        p->y=plry;
        p->hit=0xFFFFFFFF;
        p->typ=' ';
        p->l=1.0e20;
        ll0=ll1=p->l;
        // grid V-line hits
        c0=0; dx0=cos(a);
        if (dx0<0.0) { c0=1; xx0=floor(plrx)-0.001; dx0=-1.0; }
        if (dx0>0.0) { c0=1; xx0=ceil (plrx)+0.001; dx0=+1.0; }
        if (c0) { dy0=tan(a); yy0=plry+((xx0-plrx)*dy0);             dy0*=dx0; dx=xx0-plrx; dy=yy0-plry; ll0=(dx*dx)+(dy*dy); }
        // grid H-line hits
        c1=0; dy1=sin(a);
        if (dy1<0.0) { c1=1; yy1=floor(plry)-0.001; dy1=-1.0; }
        if (dy1>0.0) { c1=1; yy1=ceil (plry)+0.001; dy1=+1.0; }
        if (c1) { dx1=divide(1.0,tan(a)); xx1=plrx+((yy1-plry)*dx1); dx1*=dy1; dx=xx1-plrx; dy=yy1-plry; ll1=(dx*dx)+(dy*dy); }
        int height0=sys; // already rendered height [pixels]
        bool _hit,_back=false,_bck=true;
        if (!c0) ll0=1e20;
        if (!c1) ll1=1e20;
        for (;c0||c1;)
            {
            _hit=false;
            // grid V-line hits
            if (c0)
                {
                if (xx0<0.0) { c0=0; ll0=1e20; }
                if (xx0>=mx) { c0=0; ll0=1e20; }
                if (yy0<0.0) { c0=0; ll0=1e20; }
                if (yy0>=my) { c0=0; ll0=1e20; }
                }
            if ((c0)&&(ll0<ll1))
                {
                m=DWORD(xx0-dx0);
                if ((m>=0.0)&&(m<mxs)&&(!_bck)){ c=pmap[DWORD(yy0)][      m   ]; if ((c&0xFFFF)!=0xFFFF) { p->hit=c; p->typ='V'; p->l=ll0; p->x=xx0; p->y=yy0; _hit=true; _back=true;  _bck=true;  }}
                if (!_hit)                     { c=pmap[DWORD(yy0)][DWORD(xx0)]; if ((c&0xFFFF)!=0xFFFF) { p->hit=c; p->typ='V'; p->l=ll0; p->x=xx0; p->y=yy0; _hit=true; _back=false; _bck=false; } xx0+=dx0; dx=xx0-plrx; yy0+=dy0; dy=yy0-plry; ll0=(dx*dx)+(dy*dy); }
                }
            // grid H-line hits
            if (c1)
                {
                if (xx1<0.0) { c1=0; ll1=1e20; }
                if (xx1>=mx) { c1=0; ll1=1e20; }
                if (yy1<0.0) { c1=0; ll1=1e20; }
                if (yy1>=my) { c1=0; ll1=1e20; }
                }
            if ((c1)&&(ll0>ll1)&&(!_hit))
                {
                m=DWORD(yy1-dy1);
                if ((m>=0.0)&&(m<mys)&&(!_bck)){ c=pmap[      m   ][DWORD(xx1)]; if ((c&0xFFFF)!=0xFFFF) { p->hit=c; p->typ='H'; p->l=ll1; p->x=xx1; p->y=yy1; _hit=true; _back=true;  _bck=true;  }}
                if (!_hit)                     { c=pmap[DWORD(yy1)][DWORD(xx1)]; if ((c&0xFFFF)!=0xFFFF) { p->hit=c; p->typ='H'; p->l=ll1; p->x=xx1; p->y=yy1; _hit=true; _back=false; _bck=false; } xx1+=dx1; dx=xx1-plrx; yy1+=dy1; dy=yy1-plry; ll1=(dx*dx)+(dy*dy); }
                }
            // render scan line
            if (_hit)
                {
                union { DWORD dd; BYTE db[4]; } cc;
                int tx,ty,sy,sy0,sy1,cnt,dsy,dty;
                p->l=sqrt(p->l)*cos(a-plra);// anti fish eye
                m=divide(wall*focus,p->l);  // projected wall half size
                c=0;
                if (p->typ=='H') { c=ch; tx=double(double(txs)*(p->x-floor(p->x))); }
                if (p->typ=='V') { c=cv; tx=double(double(txs)*(p->y-floor(p->y))); }
                tx+=txs*(p->hit&0xFFFF);

                // prepare interpolation
                sy1=ys2+m;
//              sy0=ys2-m;                                          // constant wall height
                sy0=sy1-(((m+m)*(p->hit>>16))/_Doom3D_wall_size);   // variable wall height
                dty=tys-1;
                dsy=sy1-sy0+1;
                // skip sy>=sys
                if (sy1>=sys) sy1=sys-1;
                // skip sy<0
                for (cnt=dsy,sy=sy0,ty=0;sy<0;sy++) { cnt-=dty; while (cnt<=0) { cnt+=dsy; ty++; }}

                #ifdef _Doom3D_filter_txr
                DWORD r=0,g=0,b=0,n=0;
                #else
                cc.dd=ptxr[ty][tx];
                cc.db[0]=DWORD((DWORD(cc.db[0])*c)>>8);
                cc.db[1]=DWORD((DWORD(cc.db[1])*c)>>8);
                cc.db[2]=DWORD((DWORD(cc.db[2])*c)>>8);
                #endif
                // continue sy>=0
                y=height0;
                if (sy1>height0) sy1=height0;
                if (sy0<height0) height0=sy0;
                if (_back){ for (sy=sy0;sy<=y;sy++){ if ((sy>0)&&(sy<sys)) pscr[sy][x]=0x0000FF00; }}
                 else for (;sy<=sy1;sy++)
                    {
                    #ifdef _Doom3D_filter_txr
                    if (!n)
                        {
                        cc.dd=ptxr[ty][tx];
                        b+=DWORD(cc.db[0]);
                        g+=DWORD(cc.db[1]);
                        r+=DWORD(cc.db[2]); n+=256;
                        }
                    if ((sy>0)&&(sy<sys))
                        {
                        cc.db[0]=DWORD(c*b/n); b=0;
                        cc.db[1]=DWORD(c*g/n); g=0;
                        cc.db[2]=DWORD(c*r/n); r=0; n=0;
                        pscr[sy][x]=cc.dd;
                        }
                    cnt-=dty; while (cnt<=0)
                        {
                        cnt+=dsy; ty++;
                        cc.dd=ptxr[ty][tx];
                        b+=DWORD(cc.db[0]);
                        g+=DWORD(cc.db[1]);
                        r+=DWORD(cc.db[2]); n+=256;
                        }
                    #else
                    if ((sy>0)&&(sy<sys)) pscr[sy][x]=cc.dd;
                    cnt-=dty; while (cnt<=0)
                        {
                        cnt+=dsy; ty++;
                        cc.dd=ptxr[ty][tx];
                        cc.db[0]=DWORD((DWORD(cc.db[0])*c)>>8);
                        cc.db[1]=DWORD((DWORD(cc.db[1])*c)>>8);
                        cc.db[2]=DWORD((DWORD(cc.db[2])*c)>>8);
                        }
                    #endif
                    }
                if (height0<0) break;
                }
            }
        }
    tend(); tray=tstr(1)+" ray";

    // [2D map]
    tbeg();
    m=_Doom3D_cell_size;
    mx=_Doom3D_cell_size;
    if ((sxs>=mxs*m)&&(sys>=mys*m))
        {
        for (y=0;y<mys*m;y++)       // pmap[][]
         for (x=0;x<mxs*m;x++)
            {
            if ((pmap[y/m][x/m]&0xFFFF)!=0xFFFF) c=0x00808080; else c=0x00000000;
            pscr[y][x]=c;
            }
        x=double(plrx*mx);          // view rays
        y=double(plry*mx);
        scr->Canvas->Pen->Color=0x00005050;
        scr->Canvas->Pen->Mode=pmMerge;
        for (c=0;c<sxs;c++)
            {
            scr->Canvas->MoveTo(x,y);
            scr->Canvas->LineTo(DWORD(ray[c].x*mx),DWORD(ray[c].y*mx));
            }
        scr->Canvas->Pen->Mode=pmCopy;
        c=focus*m;                  // player and view direction
        scr->Canvas->Pen->Color=0x000000FF;
        scr->Canvas->Brush->Color=0x000000FF;
        scr->Canvas->MoveTo(x,y);
        scr->Canvas->LineTo(DWORD(ray[xs2].x*mx),DWORD(ray[xs2].y*mx));
        scr->Canvas->Ellipse(x-c,y-c,x+c,y+c);
        scr->Canvas->Pen->Color=0x00202020;
        for (y=0;y<=mys;y++)        // map grid
         for (x=0;x<=mxs;x++)
            {
            scr->Canvas->MoveTo(0    ,y*m);
            scr->Canvas->LineTo(mxs*m,y*m);
            scr->Canvas->MoveTo(x*m,    0);
            scr->Canvas->LineTo(x*m,mys*m);
            }
        x=keys.mx*m;                // selected cell
        y=keys.my*m;
        scr->Canvas->Pen->Color=0x0020FFFF;
        scr->Canvas->MoveTo(x  ,y  );
        scr->Canvas->LineTo(x+m,y  );
        scr->Canvas->LineTo(x+m,y+m);
        scr->Canvas->LineTo(x  ,y+m);
        scr->Canvas->LineTo(x  ,y  );
        }
    tend(); tmap=tstr(1)+" map";

    // [editor]
    if (txr_sel!=0xFFFFFFFF)
        {
        int x=sxs,y=5,s0,s1,s2,i,j;
        s0=txs>>1;
        s1=txs>>2;
        s2=(s0*cell_h)/_Doom3D_wall_size;

        for (i=-3;i<=3;i++)
            {
            j=txr_sel+i;
            while (j<  0) j+=tn;
            while (j>=tn) j-=tn;
            if (i) { scr->Canvas->CopyRect(TRect(x-s1,y+(s1>>1),x,s1+(s1>>1)),txr2->Canvas,TRect(s1*j,s0,s1*j+s1,s0+s1)); x-=s1+5; }
            else   { scr->Canvas->CopyRect(TRect(x-s0,y+s0-s2  ,x,s0        ),txr2->Canvas,TRect(s0*j, 0,s0*j+s0,s2   )); x-=s0+5; }
            }
        }

    // total time measurement
    performance_tms=tperf0;
    tend(); ttotal=tstr(1)+" total";

    x=m*mxs+m;
    c=16; y=-c;
    scr->Canvas->Font->Color=clYellow;
    scr->Canvas->Brush->Style=bsClear;
    scr->Canvas->TextOutA(x,y+=c,AnsiString().sprintf("player: %.2lf x %.2lf x %.2lf",plrx,plry,plrz));
    scr->Canvas->TextOutA(x,y+=c,AnsiString().sprintf(" mouse: %.2lf x %.2lf",keys.mx,keys.my));
    scr->Canvas->TextOutA(x,y+=c,tray);
    scr->Canvas->TextOutA(x,y+=c,tcls);
    scr->Canvas->TextOutA(x,y+=c,tmap);
    scr->Canvas->TextOutA(x,y+=c,ttotal);
    scr->Canvas->TextOutA(x,y+=c,AnsiString().sprintf("   key: %d",keys.Key));

    // aspect ratio test
/*
    c=ys2*7/10;
    scr->Canvas->Rectangle(xs2-c,ys2-c,xs2+c,ys2+c);
*/
    // cross
    c=4,m=32;
    scr->Canvas->Pen->Color=clRed;
    scr->Canvas->MoveTo(xs2-c,ys2-m);
    scr->Canvas->LineTo(xs2-c,ys2-c);
    scr->Canvas->LineTo(xs2-m,ys2-c);
    scr->Canvas->MoveTo(xs2+c,ys2-m);
    scr->Canvas->LineTo(xs2+c,ys2-c);
    scr->Canvas->LineTo(xs2+m,ys2-c);
    scr->Canvas->MoveTo(xs2-c,ys2+m);
    scr->Canvas->LineTo(xs2-c,ys2+c);
    scr->Canvas->LineTo(xs2-m,ys2+c);
    scr->Canvas->MoveTo(xs2+c,ys2+m);
    scr->Canvas->LineTo(xs2+c,ys2+c);
    scr->Canvas->LineTo(xs2+m,ys2+c);

    scr->Canvas->Brush->Style=bsSolid;
    }
//---------------------------------------------------------------------------
void Doom3D::update(double dt)
    {
    int move=0;
    double da=120.0*deg*dt;
    double dl=  5.0    *dt;
    double dx=0.0,dy=0.0,dz=0.0;
    if (keys.get(104)) { plra-=da; if (plra< 0.0) plra+=pi2; }                      // turn l/r
    if (keys.get(105)) { plra+=da; if (plra>=pi2) plra-=pi2; }
    if (keys.get(101)) { move=1; dx=+dl*cos(plra); dy=+dl*sin(plra); }              // move f/b
    if (keys.get( 98)) { move=1; dx=-dl*cos(plra); dy=-dl*sin(plra); }
    if (keys.get(102)) { move=1; dx= dl*cos(plra-90*deg); dy=dl*sin(plra-90*deg); } // strafe l/r
    if (keys.get( 99)) { move=1; dx= dl*cos(plra+90*deg); dy=dl*sin(plra+90*deg); }
    if (keys.get(100)) { move=1; dz=+dl; }  // strafe u/d
    if (keys.get( 97)) { move=1; dz=-dl; }
    if (move)   // update/test plr position
        {
        double x,y,z,mx,my;
        x=plrx+dx; mx=mxs-focus;
        y=plry+dy; my=mys-focus;
        z=plrz+dz; if ((z>=0.0)&&(z<=_Doom3D_wall_size)) plrz=z;;
        if (x<focus) x=focus; if (x>mx) x=mx;
        if (y<focus) y=focus; if (y>my) y=my;
        dx*=divide(focus,dl);
        dy*=divide(focus,dl);
             if ((pmap[DWORD(y+dy)][DWORD(x+dx)]&0xFFFF)==0xFFFF) { plrx=x; plry=y; }
        else if ((pmap[DWORD(y+dy)][DWORD(x   )]&0xFFFF)==0xFFFF)           plry=y;
        else if ((pmap[DWORD(y   )][DWORD(x+dx)]&0xFFFF)==0xFFFF)   plrx=x;
        }
    keys.rfskey();
    }
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
#endif
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------

Just ignore the performance.h time measurement tbeg,tend,tstr, OpenGLrep4d_double.h keyboard and mouse handler keytab and port VCL related stuff (Canvas,AnsiString,File access, JPEG...).

If you need help with understand the gfx stuff see

The usage of this class is simple declare an object of this class, and add the event to your window (mouse,keyboard,repaint ...). My VCL Window (single form with one timer on it) code looks like this:

//$$---- Form CPP ----
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "win_main.h"
#include "Doom3D.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMain *Main;
Doom3D game;
//---------------------------------------------------------------------------
void TMain::draw()
    {
    game.draw();
    Canvas->Draw(0,0,game.scr);
    }
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner)
    {
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormResize(TObject *Sender)
    {
    game.scr_resize(ClientWidth,ClientHeight);
    }
//---------------------------------------------------------------------------
void __fastcall TMain::tim_redrawTimer(TObject *Sender)
    {
    game.update(tim_redraw->Interval*0.001);
    draw();
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormKeyDown(TObject *Sender, WORD &Key,TShiftState Shift){ game.keys.set(Key,Shift); }
void __fastcall TMain::FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift) { game.keys.rst(Key,Shift); }
void __fastcall TMain::FormActivate(TObject *Sender)                            { game.keys.reset_keys(); }
//---------------------------------------------------------------------------
void __fastcall TMain::FormMouseMove(TObject *Sender,                      TShiftState Shift, int X, int Y) { game.mouse(X,Y,Shift); }
void __fastcall TMain::FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { game.mouse(X,Y,Shift); }
void __fastcall TMain::FormMouseUp  (TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { game.mouse(X,Y,Shift); }
void __fastcall TMain::FormMouseWheel(TObject *Sender, TShiftState Shift, int WheelDelta, TPoint &MousePos, bool &Handled) { game.wheel(WheelDelta,Shift); Handled=true; }
//---------------------------------------------------------------------------

And here the main iteration variables explanation:

variables

And here the texture file:

textures

Here is how it looks like after some more code tweaking and perspective correct texture mapping:

jump

Here related QA to this one:

And more recent demo version (with map editor):

Spektre
  • 41,942
  • 8
  • 91
  • 312
  • Is it works when I add `heightAt(double x, double y)` function that returns the height of the wall at the x,y coordinate ? Because I did that and I called it in my `castRayInX` function but I have strange stuff appear because sometimes it returns a null Plot object but there's a wall. – Jack Nov 12 '17 at 17:35
  • @Jack what do you mean by strange stuff (do you got an image)? I edited my answer added some more info and image of how it should look (I implemented first 3 steps into my wolfenstein ray cast engine) and it works as expected. The last part will have to wait as I do not have enough time and mood for it right now ... – Spektre Nov 12 '17 at 20:55
  • I made a screenshot, and my `WALL_HEIGHT`value is at `300px`I will edit my post with more information. – Jack Nov 13 '17 at 17:32
  • and your formula doesn't work for me, because y = 0 isn't the top of my screen for now so my compute is `int y0 = (hw+vh)/2`and `int y1 = (vh-h)/2` with `int vh = this.image.getHeight()`and `h = (int)(this.screenDistance/distance*rx.getPlot().getZ())` or `h = (int)(this.screenDistance/distance*ry.getPlot().getZ())` and `int hw = (int)(this.screenDistance/distance*WALL_HEIGHT)` – Jack Nov 13 '17 at 17:55
  • @Jack that code/equation tells nothing without context (as I can only guess what the variables mean). What is your screen geometry ? (middle is (0,0) and (+1,+1) is the top right corner? or what ... also you need to know the projection angle (FOV usually 60 or 90 degrees) – Spektre Nov 13 '17 at 20:07
  • (0,0) is the top left corner but like you see in my last screen my image is in the middle of my window. My screen distance is `this.screenDistance = this.view.getPreferredSize().getWidth()/2/Math.tan(aperture/2)` where `aperture` is `2*Math.PI/3` and my `width`is '500' and `height` is `300`. `view`is only the middle screen (with the grey background) – Jack Nov 13 '17 at 23:25
  • @Jack the coordinate system you got is the same as mine ... you just need to use midle point of screen instead of half screen coordinate. The image of yours suggest that you do not have appropriate cell height coming into the equations Are you sure you do not have any bug in that part of code? Try to ignore your colors and set color as a function of cell height to check for that. If the walls will have different scan line colors then you know you wrongly store/obtain the heights. – Spektre Nov 15 '17 at 07:13
  • @Jack added new stuff, images and code. It is a bit of a mess as I did this from my old Wolfenstein3D demo and needed to change the internal architecture quite a bit to make this work. – Spektre Nov 15 '17 at 15:32
  • @Jack added animated preview with textures after some more code tweaking and adding perspective correct texture mapping and Newton d'Lambert movement. Sadly new code is ~25Kb and the whole answer does not fit 30KB limit .... – Spektre Nov 24 '17 at 16:03
  • @Jack added 2 new links at the end of answer I think you will like them both. – Spektre May 28 '21 at 09:09