0

I have made a simple Ray casting Engine using SDL2 complete with player movement, and doing some research noticed from Wikipedia: "While the world appears 3D, the player cannot look up or down or only in limited angles with shearing distortion".

My understanding of Ray casting is essentially that a ray is shot from a player, and according to the distance from a ray to a wall, that wall is rendered to a certain size, allowing a pseudo-3d effect on a 2d surface. However, theoretically, would it not be possible to do the same thing while looking up and down, not just left and right?

Also, how does one go about including multiple heights/levels in Ray casting, as for now I only seem to be stuck with 1 level.

Could you please shine some light on looking up and down in a Ray casting Engine, and including multiple levels?

xingharvey
  • 131
  • 6
  • I don't know correctly about definition of the your word "Raycasting", what you want to do may be "ray casting" the word in CG rendering. – fana Jun 16 '23 at 02:11
  • Yes, I fixed that. – xingharvey Jun 16 '23 at 02:20
  • Regarding the camera pitch, if the angle is narrow to some extent, is it possible to deceive it with image processing? (deforming the normal rendering result with Homography matrix etc) – fana Jun 16 '23 at 02:56
  • @fana You can deform it in image space, but you won't get a realistic result. Why? Imagine you are in a cityscape, hovering and looking straight down, above an intersection between 4 tall buildings. These buildings nearest to you obscure nearly everything else; and behind them, the next-nearest buildings obscure nearly everything else around them, and so on; and this happens in both screen axes, x and y. The kind of obscuring seen in a 2D-raycast-to-horizon perspective is inaccurate, since it works mainly in the direction of the horizon (screen y). See Spektre's answer for more detail. – Engineer Aug 24 '23 at 11:23

2 Answers2

1

2D raycast shoots single ray per x position of view. If you want to look up/down freely through multiple floors that would require to shoot ray on per pixel manner converting from 2D raycast to 3D raytrace which would be hugely slower.

However of you want just look up/down inside single floor that is doable up to a point (straight up/down has singularity in goniometrics used and would require additional ifs slowing stuff down due brunching)

having more levels/floors is simple you just add lifts or portals ... that way you can still have simple raycast where you never see anything else but current level/floor.

take a look at this:

with it you can add usable stairs so you could move through levels also by stairs you just have to make sure its properly shielded so you will not have holes in view while traversing between levels.

you also need floor/ceiling tiles in your map something like I have in here:

without knowing more about your project its hard to be specific. In some cases you might want to use Voxel space ray casting instead of levels ...

Spektre
  • 49,595
  • 11
  • 110
  • 380
1

multiple heights

I considered about very simple situation "If all wall are simply vertical but have several height".

In this situation, I think, we can simply draw all the walls intersecting the ray (with Painter Algorithm like way).

Following code is I tried. (not beautiful code!).
And result image is this:

enter image description here

//
//* I used OpenCV to drawing(cv::Mat and drawing func) and 2D-vector operation(cv::Vec2d)
//

//3D Data : as Height Field.
constexpr int HF_SX = 11;
constexpr int HF_SY = 8;
const int HF[HF_SY][HF_SX] = {
    2,2,2,3,5,2,3,0,1,3,3,
    2,2,2,3,5,2,1,0,1,4,3,
    2,2,1,3,5,2,1,0,1,3,3,
    3,0,0,1,2,0,0,0,1,1,2,
    2,1,1,1,0,0,0,2,0,1,1,
    3,0,0,0,0,1,0,1,0,0,1,
    2,0,1,0,1,1,0,0,0,0,0,
    0,0,0,0,1,1,1,0,0,0,0,
};

//Camera Location and Direction
const double CamH = 1.75;
const cv::Vec2d CamPos( 5.65, 7.6 );
const cv::Vec2d CamFront( 0, -1 );
const cv::Vec2d CamRight( -CamFront[1], CamFront[0] );

//Rendering Image Size
constexpr int ScrnW =240*2;
constexpr int ScrnH = 180*2;

//Camera Intrinsic Parameters (Pinhole Camera Model)
constexpr double PC_x0 = ScrnW * 0.5;
constexpr double PC_y0 = ScrnH * 0.5;
constexpr double PC_f = 0.75 * ScrnW * 0.5;

//Return_data_type for the function EnumCheckPoint below
struct CheckPoint
{
    CheckPoint( const cv::Vec2d &P, bool IsHorizontal ) : Pos(P), IsHorizontalEdge( IsHorizontal ) {}

    cv::Vec2d Pos;
    bool IsHorizontalEdge;
};

//Enumerate points along a line segment(From - To) where x or y are integers.
//The result is stored in the vector rDst.
void EnumCheckPoint( const cv::Vec2d &From, const cv::Vec2d &To, std::vector<CheckPoint> &rDst )
{
    rDst.clear();
    const cv::Vec2d d = To - From;

    const int MinX = (int)ceil( (std::min)(From[0],To[0]) );
    const int MaxX = (int)floor( (std::max)(From[0],To[0]) );
    if( MinX<=MaxX )
    {
        const double dy_per_x = d[1]/d[0];
        for( int x=MinX; x<=MaxX; ++x )
        {   rDst.emplace_back( cv::Vec2d{ (double)x, From[1]+(x-From[0])*dy_per_x }, false );   }
    }

    const int MinY = (int)ceil( (std::min)(From[1],To[1]) );
    const int MaxY = (int)floor( (std::max)(From[1],To[1]) );
    if( MinY <= MaxY )
    {
        const double dx_per_y = d[0]/d[1];
        for( int y=MinY; y<=MaxY; ++y )
        {   rDst.emplace_back( cv::Vec2d{ From[0]+(y-From[1])*dx_per_y, (double)y }, true );    }
    }

    auto Dist = [&From]( const CheckPoint &P )->double
        {
            auto D = P.Pos - From;
            return fabs(D[0]) + fabs(D[1]);
        };

    std::sort(
        rDst.begin(), rDst.end(),
        [&Dist]( const CheckPoint &lhs, const CheckPoint &rhs )->bool{  return ( Dist(lhs) < Dist(rhs) );   }
    );
}

//Return_data_type for the function Cvt_CPs_to_Walls below
struct Wall
{
    Wall( const cv::Vec2d &P, const cv::Vec2d &N, int th, int bh )
        : LayHitPos(P), UnitNormal(N), TopHeight(th), BottomHeight(bh) {}

    cv::Vec2d LayHitPos;
    cv::Vec2d UnitNormal;
    int TopHeight;
    int BottomHeight;
};

//Create Wall(where the height changes) Data
void Cvt_CPs_to_Walls( const std::vector<CheckPoint> &CPs, std::vector<Wall> &rDstWalls )
{
    rDstWalls.clear();
    rDstWalls.reserve( CPs.size() );

    for( const auto &CP : CPs )
    {
        int x = (int)CP.Pos[0];
        int y = (int)CP.Pos[1];
        if( CP.IsHorizontalEdge )
        {
            if( x<0 || x>=HF_SX )continue;
            if( y<=0 || y>=HF_SY )continue;

            int Hu = HF[y-1][x];
            int Hd = HF[y][x];
            if( Hu == Hd )continue;
            if( Hu > Hd ){  rDstWalls.emplace_back( CP.Pos, cv::Vec2d{ 0,1 }, Hu, Hd ); }
            else{   rDstWalls.emplace_back( CP.Pos, cv::Vec2d{ 0,-1 }, Hd, Hu );    }
        }
        else
        {
            if( y<0 || y>=HF_SY )continue;
            if( x<=0 || x>=HF_SX )continue;

            int Hl = HF[y][x-1];
            int Hr = HF[y][x];
            if( Hl == Hr )continue;
            if( Hl > Hr ){  rDstWalls.emplace_back( CP.Pos, cv::Vec2d{ 1,0 }, Hl, Hr ); }
            else{   rDstWalls.emplace_back( CP.Pos, cv::Vec2d{ -1,0 }, Hr, Hl );    }
        }
    }
}

//Draw the range (x,ybegin) - (x,yend) as Ground Plane which height is GndH
void DrawGndPlane( cv::Mat &Scrn, int x, int ybegin, int yend, double GndH )
{
    for( int y=ybegin; y<yend; ++y )
    {
        double tan = (y - PC_y0) / PC_f;
        double depth_of_gnd = (CamH - GndH) / tan;
        Scrn.at< cv::Vec3b >( y,x ) = cv::Vec3b( 0, (unsigned char)std::min(255.0, 255/depth_of_gnd), 0);
    }
}

//main
int main()
{
    //Rendering Image Buffer
    cv::Mat Scrn( ScrnH, ScrnW, CV_8UC3 );
    Scrn = cv::Scalar( 192, 32, 32 );   //Fill with Bkgnd Color (as Blue Sky)

    //------------------------------------
    //Ray Casting

    std::vector< CheckPoint > CPs;  //Working buffer
    std::vector<Wall> Walls;    //Working buffer

    //for all column(x)
    for( int x=0; x<Scrn.cols; ++x )
    {
        //Calculate ray (unit directional vector)
        double theta = atan( (x - PC_x0)/PC_f );
        cv::Vec2d ray = cos(theta)*CamFront + sin(theta)*CamRight;
        //Get the terrain information needed to draw this column(x)
        EnumCheckPoint( CamPos, CamPos + ray*15, CPs );
        Cvt_CPs_to_Walls( CPs, Walls );

        //Rendering
        int RenderedTop = Scrn.rows;    //(indicate the range y>=RenderedTop is already rendered)
        int GndPlaneH = HF[ (int)CamPos[1] ][ (int)CamPos[0] ];

        for( const auto &W : Walls )
        {
            //Calculate this Wall's render y range (y0 - y1)
            auto depth = abs( W.LayHitPos[1] - CamPos[1] );
            double ry0 = PC_f * (W.TopHeight - CamH) / depth;
            double ry1 = PC_f * (CamH - W.BottomHeight) / depth;
            int y0 = cvRound( PC_y0 - ry0 );
            int y1 = cvRound( PC_y0 + ry1 );

            if( y0 >= RenderedTop )continue;

            if( W.UnitNormal.dot( ray ) <= 0 )
            {//When the wall faces the camera
                GndPlaneH = W.BottomHeight;
                DrawGndPlane( Scrn, x, y1, RenderedTop, GndPlaneH );
                //Draw Wall
                y1 = std::min( y1, RenderedTop-1 );
                cv::line( Scrn, cv::Point(x,y0), cv::Point(x,y1), cv::Scalar(0,0, std::min(255.0, 1.5*255/depth)) );
            }
            else
            {//Else( when the wall faces away )
                GndPlaneH = W.TopHeight;
                DrawGndPlane( Scrn, x, std::max(0,y0), RenderedTop, GndPlaneH );
            }
            RenderedTop = y0;
        }
        //Draws the ground plane up to the y-coordinate of the horizon if needed.
        DrawGndPlane( Scrn, x, cvRound(PC_y0), RenderedTop, GndPlaneH );
    }
    //Show Rednering Result
    cv::imshow( "Result", Scrn );
    cv::waitKey();
    return 0;
}
fana
  • 1,370
  • 2
  • 7