【问题标题】:Efficient floor/ceiling rendering in RaycasterRaycaster 中的高效地板/天花板渲染
【发布时间】:2021-08-10 23:57:48
【问题描述】:

我在我的 Raycaster 引擎上工作了一段时间,我在较慢的机器上运行。 我遇到的最具挑战性的问题是/是高效的地板和天花板铸造。

我的问题是: 我还可以使用什么其他更快的方法? (我不确定 Doom 地板和天花板是如何渲染的)

到目前为止,我尝试了两种典型的解决方案:

水平方法当然要快得多,但我还用定点变量对其进行了优化。

不幸的是,即使是这种方法也是一种性能杀手 - 即使在更快的 CPU 上也有相当大的 fps 下降,而更慢的 CPU 则是一个瓶颈。

我的其他想法:

  • 我想出了一种算法,它可以将可见的地板/天花板地图图块转换为四边形,然后将其分成两个三角形 - 并像在常规扫描线光栅化器中一样对它们进行光栅化。它要快得多——我也可以按纹理 ID 对图块进行排序,以使缓存更加友好。不幸的是,在这种情况下,我进入了“透视校正纹理映射”——为了解决这个问题,我必须添加一些分区,这会降低性能。但也有一些可以做的优化..

  • 每 2 条射线(在列、行或两者中)使用水平投射 - 我将用平均纹理坐标填充空白空间

  • 我也可以尝试将我的算法从 1 点与水平投射相结合 - 我可以按 ID 对纹理进行排序,例如,我认为不会有纹理扭曲

  • 模式 7 ?

到目前为止我的进步: https://www.youtube.com/watch?v=u3zA2Wh0NB4

编辑(1):

Floor and Ceiling rednering 代码(基于 od lodev 教程的水平方法) 但用定点优化。 Ceil 计算反映到地板。

https://lodev.org/cgtutor/raycasting2.html

这种方法比垂直方法快,但大量计算是内循环,随机访问纹理像素会影响性能..

inline void RC_Raycast_Floor_Ceiling()
{
    // get local copy of RC_level_textures
    sBM_Bitmap* level_textures = RC_level_textures;

    // ray direction for leftmost ray (x = 0) and rightmost ray (x = width)
    float32 r_dx0 = RC_pp_dx - RC_pp_nsize_x_d2;
    float32 r_dy0 = RC_pp_dy - RC_pp_nsize_y_d2;

    //float32 r_dx1 = RC_pp_dx + RC_pp_nsize_x_d2;
    //float32 r_dy1 = RC_pp_dy + RC_pp_nsize_y_d2;

    // precalculated helpers for performance
    float32 r_dx1_m_dx0_div_width = (RC_pp_nsize_x_d2 + RC_pp_nsize_x_d2) * RC_render_width__1div__f;
    float32 r_dy1_m_dy0_div_width = (RC_pp_nsize_y_d2 + RC_pp_nsize_y_d2) * RC_render_width__1div__f;

    int16 ray_y = -1;
    u_int16 ray_y_counter = RC_render_height__i;

    u_int8* walls_buffer__ptr = RC_walls_buffer;

    // casting floor and ceiling - horizontal line by line - from left to right
    while(ray_y_counter)
    {
        ray_y++;
        ray_y_counter--;

        // dont go further if the current floor/ceil scanline line won't be visible
        if (ray_y >= RC_walls_start)
        {
            break;
            if (ray_y < RC_walls_end)
            {
                ray_y = RC_walls_end;
                ray_y_counter = RC_walls_start - 1;
                walls_buffer__ptr += ((RC_walls_end - RC_walls_start) * RC_render_width__i);
                continue;
            }
        }
      
        // whether this section is floor or ceiling
        u_int8 is_floor = ray_y > RC_render_height__div2__i;

        // current ray y position compared to the center of the screen (the horizon)
        float32 ry_pos = (float32)(is_floor ? (ray_y - RC_render_height__div2__i) : (RC_render_height__div2__i - ray_y));

        // vertical position of projection plane, 0.5 is between floor and ceiling
        float32 pp_z = (float32)(is_floor ? (RC_render_height__div2__i) : (RC_render_height__div2__i));

        float32 straight_distance_to_point = pp_z / ry_pos;

        // calculate the real world step vector we have to add for each x (parallel to camera plane)
        // adding step by step avoids multiplications with a weight in the inner loop
        float32 floor_step_x = straight_distance_to_point * r_dx1_m_dx0_div_width;
        float32 floor_step_y = straight_distance_to_point * r_dy1_m_dy0_div_width;

        float32 floor_x = RC_player_x + straight_distance_to_point * r_dx0;
        float32 floor_y = RC_player_y + straight_distance_to_point * r_dy0;

        // convert that values to fixed point
        int32 floor_x__fp = (int32)(floor_x * 65536.0f);
        int32 floor_y__fp = (int32)(floor_y * 65536.0f);

        int32 floor_step_x__fp = (int32)(floor_step_x * 65536.0f);
        int32 floor_step_y__fp = (int32)(floor_step_y * 65536.0f);

        u_int32 ry_m_render_width = ray_y * RC_render_width__i;
        u_int32 ry_m_render_width_i_mirror = (RC_render_height__i- ray_y-1) * RC_render_width__i;

        int16 ray_x = -1;
        u_int16 ray_x_counter = RC_render_width__i;

        sLV_Cell* level_map = RC_level->map;

        // drawing floor and ceiling from left to right
        while(ray_x_counter)
        {
            ray_x++;
            ray_x_counter--;

            floor_x__fp += floor_step_x__fp;
            floor_y__fp += floor_step_y__fp;

            if (*walls_buffer__ptr != 0)
            {
                walls_buffer__ptr++;
                continue;
            }

            walls_buffer__ptr++;

            u_int32 output_pixel_index = ray_x + ry_m_render_width;
            u_int32 output_pixel_index_mirror = ray_x + ry_m_render_width_i_mirror;

            // the cell coord is simply got from the integer parts of floorX and floorY

            u_int32 curr_cell_x = (floor_x__fp & FP_INTEGER_MASK_16) >> 16;
            u_int32 curr_cell_y = (floor_y__fp & FP_INTEGER_MASK_16) >> 16;

            // prevent overflow
           // curr_cell_x &= LV_MAP_SIZE_m1;
           // curr_cell_y &= LV_MAP_SIZE_m1;

            u_int32 texture_pixel_x = (floor_x__fp & FP_FRACTION_MASK_16) >> 9;
            u_int32 texture_pixel_y = (floor_y__fp & FP_FRACTION_MASK_16) >> 9;

            u_int32 cell_index = curr_cell_x + (curr_cell_y << LV_MAP_SIZE_BITSHIFT);

            // get the texture index depending on the cell
            u_int32 texture_index;

        /*    if (is_floor)
            {
               
                texture_index = level_map[cell_index].floor_id;
            }
            else
            {
                texture_index = level_map[cell_index].ceil_id;
            }*/

            texture_index = level_map[cell_index].ceil_id;

            // get pixel coords in texture
            u_int32 tex_index = texture_pixel_x + (texture_pixel_y << 7);
            u_int32 texture_current_pixel = level_textures[0].pixels[tex_index];

            RC_output_buffer_32[output_pixel_index] = texture_current_pixel;

            texture_index = level_map[cell_index].floor_id;
            texture_current_pixel = level_textures[texture_index].pixels[tex_index];

            RC_output_buffer_32[output_pixel_index_mirror] = texture_current_pixel;
        }
    }
}

【问题讨论】:

  • 顺便说一句,因为我在这里的 cmets 中被纠正了 What techniques were used to reduce the required re-rendering in 3D programs? Doom 没有使用光线投射......还可以在这里查看答案 Perspective Vision on Canvas 以获得一些额外的想法
  • 嗨@Spektre,“问题”是我正在使用复古硬件,我的意思是 Amiga,在 32 位模式下使用 320x240 或 640x480 的现代 68k CPU。我得到了很好的性能,但是地板/天花板算法太重了,这会造成瓶颈——可能是因为内部循环中有很多计算,没有排序的纹理访问等等。所以我只是在寻找不同的方法。我也可以使用 ammx 限制函数,但我不知道汇编程序,也许会有一些可用的宏允许我在 C 中使用它。
  • 我用 C 编码,没有额外的库。我的代码是可移植的,我同时在我的 PC 和 Amiga 上检查它。在 PC 上,我在 640x480x32 中获得了约 100fps 的平滑效果,带有纹理和阴影。 Amiga 当然更慢 - 我的目标是在 320x200x32 下获得 ~50fps ......但正如我所说,lodev 的地板/天花板水平算法效率不高。我使用定点和将地板计算镜像到天花板对其进行了一些优化。主要问题是内部循环中的纹理访问。
  • @Spektre 感谢您的评论。我编辑了 1 篇文章 - 添加渲染代码。我没有添加它,因为正如我所说,该方法基于 lodev 教程 - 水平方法。 lodev.org/cgtutor/raycasting2.html 这种方法在内部循环中进行了一些计算,这会影响性能,而且对纹理的访问是随机的(未排序),这也会影响性能 - 我正在寻找更优化的方法..
  • @Spektre 嗯,你说的这似乎很有趣——但我不明白。您能否更准确地向我解释一下该算法。提前谢谢你:)

标签: graphics 3d rendering raycasting


【解决方案1】:

我将参考我的光线投射引擎,所以这里有一些可以帮助您理解它的东西。让我们从类声明开始:

const int   _Doom3D_mipmaps=8;          // number of mipmaps for texture
const DWORD _Doom3D_edit_cell_size=4;   // [pixel] 2D map cell size
const DWORD _Doom3D_cell_size=100;      // [units] cell cube size
//                                              heigh     ceil   floor  wall
const DWORD _Doom3D_cell_floor=(                0<<24)|( 0<<16)|(18<<8)|(39);
const DWORD _Doom3D_cell_wall =(_Doom3D_cell_size<<24)|( 0<<16)|( 0<<8)|(39);
class Texture2D // 2D textures
    {
public:
    Graphics::TBitmap *bmp;
    int xs,ys; DWORD **pyx;

    Texture2D() { bmp=new Graphics::TBitmap; xs=0; ys=0; pyx=NULL; resize(1,1); }
    Texture2D(Texture2D& a) { *this=a; }
    ~Texture2D() { free(); if (bmp) delete bmp; bmp=NULL; }
    Texture2D* operator = (const Texture2D *a) { *this=*a; return this; }
    Texture2D* operator = (const Texture2D &a) { bmp->Assign(a.bmp); resize(); return this; }

    void free() { if (pyx) delete[] pyx; xs=0; ys=0; pyx=NULL; }
    void resize(int _xs=-1,int _ys=-1) { free(); if ((_xs<0)||(_ys<0)) { _xs=bmp->Width; _ys=bmp->Height; } else bmp->SetSize(_xs,_ys); bmp->HandleType=bmDIB; bmp->PixelFormat=pf32bit; pyx=new DWORD* [_ys]; for (int y=0;y<_ys;y++) pyx[y]=(DWORD*)bmp->ScanLine[y]; xs=bmp->Width; ys=bmp->Height; }
    void load(AnsiString name) { AnsiString ext=ExtractFileExt(name).LowerCase(); for(;;) { if (ext==".bmp") { bmp->LoadFromFile(name); break; } if (ext==".jpg") { TJPEGImage *jpg=new TJPEGImage; if (jpg==NULL) return; jpg->LoadFromFile(name); bmp->Assign(jpg); delete jpg; break; } return; } resize(); }
    };
class Doom3D
    {
public:
    Texture2D map,map2,scr,sky;         // map, map preview, screen, sky texture
    Texture2D txr[_Doom3D_mipmaps],*ptxr;// texture atlas with mipmas, actual mipmap for rendering scan lines
    DWORD sxs2,sys2;                    // screen half resolution
    DWORD tn,tm;                        // number of textures in texture atlas, number of mipmaps used
    BYTE liH[256],liV[256],liF[256];    // shading LUT for H,V,floor/ceiling (light scaled shades of gray)

    int scale_x;
    bool _no_mipmap;

    struct _player
        {
        double  a;                      // player view direction [rad]
        double x0,y0,z0;                // old player position [cell]
        double  x, y, z;                // player position [cell]
        double vx,vy,vz;                // player speed [cell/s]
        double ax,ay,az;                // player acceleration [cell/s]
        void update(double dt)
            {
            x0=x; vx+=ax*dt; x+=vx*dt;
            y0=y; vy+=ay*dt; y+=vy*dt;
            z0=z; vz+=az*dt; z+=vz*dt;
            }
        _player() { a=x=y=z=vx=vy=vz=ax=ay=az=0.0; }
        _player(_player& a) { *this=a; }
        ~_player() {};
        _player* operator = (const _player *a) { *this=*a; return this; }
        //_player* operator = (const _player &a) { ..copy... return this; }
        } plr;
    double view_ang;                    // [rad] view angle
    double focus;                       // [cells] view focal length
    double wall;                        // [px] projected wall size ratio size = height*wall/distance
    struct _ray
        {
        int    x,y;                     // cell map position
        double ang;                     // ray angle
        double x0,y0,l0;                // cell first hit
        double x1,y1,l1;                // cell second hit
        int    sx;                      // screen x coordinate
        int    sy0,sy1;                 // screen y coordinates of V scanline to render
        char tp0,tp1;                   // H/V hit type
        DWORD map;                      // map cell of hit or 0xFFFFFFFF
        _ray() {};
        _ray(_ray& a)   { *this=a; }
        ~_ray() {};
        _ray* operator = (const _ray *a) { *this=*a; return this; }
        //_ray* operator = (const _ray &a) { ..copy... return this; }
        };

    keytab keys;                        // keyboard handler
    DWORD txr_wall;                     // map editor
    DWORD txr_floor;
    DWORD txr_ceil;
    DWORD cell_h;
    DWORD *txr_mode; AnsiString txr_mode_txt;

    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_clear();                   // clear whole map
    void map_save(AnsiString name);     // save map to file
    void map_load(AnsiString name);     // load map from file
    void scr_resize(DWORD xs,DWORD ys); // resize view
    void txr_mipmap();                  // generate mipmaps for txr

    bool cell2screen(int &sx,int &sy,double x,double y,double z);   // [pixel] <- [cell] return tru if in front of player
    void draw_scanline(int sx,int sy0,int sy1,                int symin,int tx0,int ty0,int tx1,int ty1,BYTE *li); // render screen y-scan line from texture (sub routine)
    void draw_scanline(int sx,int sy0,int sy1,int sz0,int sz1,int symin,int tx0,int ty0,int tx1,int ty1,BYTE *li); // render screen y-scan line from texture (sub routine)
    void draw_cell(_ray &p);            // render actual cell hit by ray p (sub routine)
    void draw();                        // render view
    void update(double dt);             // update game logic (call in timer with interval dt [s])
    void mouse(double x,double y,TShiftState sh)    // editor mouse handler
        {
        keys.setm(x,y,sh);  // mx,my mouse screen pos [pixels]
        x=floor(x/_Doom3D_edit_cell_size); //if (x>=map.xs) x=map.xs-1; if (x<0) x=0;
        y=floor(y/_Doom3D_edit_cell_size); //if (y>=map.ys) y=map.ys-1; if (y<0) y=0;
        DWORD xx=x,yy=y;
        if ((xx>=0)&&(xx<map.xs)&&(yy>=0)&&(yy<map.ys)) keys.setk(xx,yy,sh); // kx,ky mouse map pos [cells]
        xx=keys.kx; yy=keys.ky;
        if (keys.Shift.Contains(ssLeft  )) map.pyx[yy][xx]=(txr_wall)|(txr_floor<<8)|(txr_ceil<<16)|(cell_h<<24);
        if (keys.Shift.Contains(ssRight )) map.pyx[yy][xx]=0xFFFFFFFF;
        if (keys.Shift.Contains(ssMiddle))
            {
            DWORD c=map.pyx[yy][xx];
            txr_wall  =c     &0xFF;
            txr_floor=(c>> 8)&0xFF;
            txr_ceil =(c>>16)&0xFF;
            cell_h   =(c>>24)&0xFF;
            }
        keys.rfsmouse();
        }
    void wheel(int delta,TShiftState sh)    // editor mouse wheel handler
        {
        if (sh.Contains(ssShift))
            {
            if (delta<0) { cell_h-=10; if (cell_h>_Doom3D_cell_size) cell_h=0; }
            if (delta>0) { cell_h+=10; if (cell_h>_Doom3D_cell_size) cell_h=_Doom3D_cell_size; }
            }
        else{
            if (delta<0) { (*txr_mode)--; if (*txr_mode>=tn) *txr_mode=tn-1; }
            if (delta>0) { (*txr_mode)++; if (*txr_mode==tn) *txr_mode=   0; }
            }
        }
    };

完整代码(没有任何帮助文件)约为 27.7 KB,因此不适合答案。它的旧版本(没有地板/天花板/墙顶)在这里:

它比你的更复杂,因为它允许具有不同高度的墙壁以及墙壁、地板和天花板的单独纹理(但是天花板尚未实现,但我使用云纹理代替)并且还可以跳跃(z 玩家的位置)。

地图和投影的几何形状与上面的链接相同。这是一个在 2.5D 地图位置和屏幕之间转换的辅助函数(因此您知道使用了什么数学):

bool Doom3D::cell2screen(int &sx,int &sy,double x,double y,double z)
    {
    double a,l;
    // x,y relative to player
    x-=plr.x;
    y-=plr.y;
    // convert z from [cell] to units
    z*=_Doom3D_cell_size;
    // angle -> sx
    a=atanxy(x,y)-plr.a;
    if (a<-pi) a+=pi2;
    if (a>+pi) a-=pi2;
    sx=double(sxs2)*(1.0+(2.0*a/view_ang));
    // perpendicular distance -> sy
    l=sqrt((x*x)+(y*y))*cos(a);
    sy=sys2+divide((double((2.0*plr.z+1.0)*_Doom3D_cell_size)-z-z)*wall,l);
    // in front of player?
    return (fabs(a)<=0.5*pi);
    }

该功能本身仅用于编辑器的HUD(在2.5D视图中突出显示选定的地图单元格)sxs2,sys2是屏幕中心(分辨率的一半sxs,sys)和wall用于管理透视图投影计算如下:

void Doom3D::scr_resize(DWORD xs,DWORD ys)
    {
    scr.resize(xs,ys);
    sxs2=scr.xs>>1;
    sys2=scr.ys>>1;
    // aspect ratio,view angle corrections
    double a=90.0*deg-view_ang;
    wall=double(scr.xs)*(1.25+(0.288*a)+(2.04*a*a))*focus/double(_Doom3D_cell_size);
    }

在我的引擎中,光线以更像光线行进的方式进行测试,因为它不会在第一次命中时停止,而是在第一次命中时停止,覆盖整个墙壁大小(因此光线可以穿过不会阻挡整个视野的较小墙壁,这也沿途渲染地砖)。

由于您的引擎不执行此操作并且只有全尺寸的墙,您将只需单次命中。

现在终于回到你的问题。我看到了两种改进方法:

  1. 使用来自墙壁的光线测试结果而不是投射地板/天花板光线

    由于您想在天花板和地板上有单独的瓷砖,因此您应该使用像射线投射一样的射线行进。意思是每列屏幕投射一条射线,并通过所有地图单元格交叉点对其进行迭代,直到撞到墙壁。但是,您必须在每个单元格命中时渲染,而不是仅在墙壁命中时渲染。像这张图片上的东西:

    所以红线是投射光线(橙色只是一面镜子)。地图的每个渲染单元都被 2 个点的光线击中。您应该知道每个命中的地图单元位置以及来自光线投射的屏幕坐标。因此,您只需要添加到相机的垂直距离并将线段渲染为地板和天花板的透视正确插值纹理线。墙壁总是只是垂直的非透视纹理线。纹理坐标取自命中的地图位置(坐标的小数部分)。在代码中它有点乱,但它是:

     void Doom3D::draw_scanline(int sx,int sy0,int sy1,int symin,int tx0,int ty0,int tx1,int ty1,BYTE *li)
         {
         // affine texture mapping (front side of walls) sy0>sy1
         union { DWORD dd; BYTE db[4]; } cc;
         int sy,tx,ty,ktx,kty,dtx,dty,ctx,cty,dsy;
                dsy=sy1-sy0; if (dsy<0)                        dsy=-dsy;
         ktx=0; dtx=tx1-tx0; if (dtx>0) ktx=+1; else { ktx=-1; dtx=-dtx; } tx=tx0; ctx=0;
         kty=0; dty=ty1-ty0; if (dty>0) kty=+1; else { kty=-1; dty=-dty; } ty=ty0; cty=0;
         if (dsy) for (sy=sy0;sy>=sy1;sy--)
             {
             if ((sy>=0)&&(sy<scr.ys)&&(sy<=symin))
              if ((tx>=0)&&(tx<ptxr->xs)&&(ty>=0)&&(ty<ptxr->ys))
                 {
                 cc.dd=ptxr->pyx[ty][tx];
                 cc.db[0]=li[cc.db[0]];
                 cc.db[1]=li[cc.db[1]];
                 cc.db[2]=li[cc.db[2]];
                 scr.pyx[sy][sx]=cc.dd;
                 }
             for (ctx+=dtx;ctx>=dsy;) { ctx-=dsy; tx+=ktx; }
             for (cty+=dty;cty>=dsy;) { cty-=dsy; ty+=kty; }
             }
         }
     void Doom3D::draw_scanline(int sx,int sy0,int sy1,int sz0,int sz1,int symin,int tx0,int ty0,int tx1,int ty1,BYTE *li)
         {
         // perspective correct mapping (floor, top side of walls, ceiling) sy0>sy1
         union { DWORD dd; BYTE db[4]; } cc;
         int sy,tx,ty,dsy,dtx,dty,n,dn;
         int a,_z0,_z1,_tx;
         const int acc0=16;
         const int acc1=8;
         _tx=tx0-(tx0%ptxr->ys);
         tx0-=_tx;
         tx1-=_tx;
         dsy=sy1-sy0; dn=abs(dsy);
         dtx=tx1-tx0;
         dty=ty1-ty0;
         if (sz0==0) return; _z0=(1<<acc0)/sz0;
         if (sz1==0) return; _z1=(1<<acc0)/sz1;
         if (dn) for (n=0;n<=dn;n++)
             {
             sy=sy0+((n*dsy)/dn);
             a=((n<<acc1)*_z1)/(((dn-n)*_z0)+(n*_z1)); // perspective correction a=<0,1<<acc1> (https://en.wikipedia.org/wiki/Texture_mapping)
             tx=tx0+((a*dtx)>>acc1)+_tx;
             ty=ty0+((a*dty)>>acc1);
             if ((sy>=0)&&(sy<scr.ys)&&(sy<=symin))
              if ((tx>=0)&&(tx<ptxr->xs)&&(ty>=0)&&(ty<ptxr->ys))
                 {
                 cc.dd=ptxr->pyx[ty][tx];
                 cc.db[0]=li[cc.db[0]];
                 cc.db[1]=li[cc.db[1]];
                 cc.db[2]=li[cc.db[2]];
                 scr.pyx[sy][sx]=cc.dd;
                 }
             }
         }
     void Doom3D::draw_cell(_ray &p)
         {
         BYTE *li;
         DWORD m;
         int tx0,tx1,ty0,ty1,sy,sy0,sy1,sy2,sy3,sz0,sz1,q;
         int sy4,sy5;
         //sy0>=sy1
         sy0=sys2+divide(double((1.0+2.0*plr.z)*_Doom3D_cell_size)*wall,p.l0);
         sy1=sy0 -divide(double((p.map>>24)<<1                   )*wall,p.l0);
         sy2=sys2+divide(double((1.0+2.0*plr.z)*_Doom3D_cell_size)*wall,p.l1);
         sy3=sy2 -divide(double((p.map>>24)<<1                   )*wall,p.l1);
         sy4=sys2-divide(double((1.0-2.0*plr.z)*_Doom3D_cell_size)*wall,p.l1);
         sy5=sys2-divide(double((1.0-2.0*plr.z)*_Doom3D_cell_size)*wall,p.l0);
         sz0=double(p.l0*_Doom3D_cell_size);
         sz1=double(p.l1*_Doom3D_cell_size);
         // select mipmap resolution
         ty0=divide(double(_Doom3D_cell_size<<1)*wall,p.l0);
         for (q=tm-1;q>=0;q--)
             {
             ptxr=txr+q;
             if (ty0<=ptxr->ys) break;
             }
         if (_no_mipmap) ptxr=txr;
         // mouse select
         if (p.sx==round(keys.mx))
          if (keys.my>=sy3)
           if (keys.my<=sy0)
            if ((keys.my>=map2.ys)||(keys.mx>=map2.xs))
             {
             keys.kx=p.x;
             keys.ky=p.y;
             }
         if ((p.map&0xFF)==0xFF) { sy1=sy0; sy3=sy2; }
         // wall
         if ((sy1<p.sy1)&&((p.map&0xFF)!=0xFF))
             {
             tx0=ptxr->ys*(p.map&0xFF);
             if (p.tp0=='H') { li=liH; tx0+=double(double(ptxr->ys-1)*(p.x0-floor(p.x0))); }
             if (p.tp0=='V') { li=liV; tx0+=double(double(ptxr->ys-1)*(p.y0-floor(p.y0))); }
             draw_scanline(p.sx,sy0,sy1,p.sy1,tx0,0,tx0,((p.map>>24)*(ptxr->ys-1))/_Doom3D_cell_size,li);
             p.sy1=sy1;
             }
         // ceiling
         if ((p.map&0xFF0000)!=0xFF0000)
             {
             q=ptxr->ys*((p.map>>16)&0xFF);
             tx0=double(double(ptxr->ys-1)*(p.x0-double(p.x)))+q;
             ty0=double(double(ptxr->ys-1)*(p.y0-double(p.y)));
             tx1=double(double(ptxr->ys-1)*(p.x1-double(p.x)))+q;
             ty1=double(double(ptxr->ys-1)*(p.y1-double(p.y)));
             draw_scanline(p.sx,sy5,sy4,sz0,sz1,p.sy1,tx0,ty0,tx1,ty1,liF);
             }
         // floor/top side
         if ((sy3<p.sy1)&&((p.map&0xFF00)!=0xFF00))
             {
             q=ptxr->ys*((p.map>>8)&0xFF);
             tx0=double(double(ptxr->ys-1)*(p.x0-double(p.x)))+q;
             ty0=double(double(ptxr->ys-1)*(p.y0-double(p.y)));
             tx1=double(double(ptxr->ys-1)*(p.x1-double(p.x)))+q;
             ty1=double(double(ptxr->ys-1)*(p.y1-double(p.y)));
             draw_scanline(p.sx,sy1,sy3,sz0,sz1,p.sy1,tx0,ty0,tx1,ty1,liF);
             p.sy1=sy3;
             }
         if (sy3<p.sy1) p.sy1=sy3;
         }
     void Doom3D::draw()
         {
         tbeg();
         _ray p;
         DWORD x,y,c,m;
         DWORD mx,mx0,mx1;
         DWORD my,my0,my1;
         double a,a0,da,dx,dy,l;
         double xx0,yy0,dx0,dy0,ll0,dl0;
         double xx1,yy1,dx1,dy1,ll1,dl1;
    
         // compute diffuse + ambient lighting LUT (light scaled shades of gray)
         c=155.0+fabs(100.0*sin(   plr.a)); for (x=0;x<256;x++) liH[x]=(x*c)>>8; // H wall
         c=155.0+fabs(100.0*cos(   plr.a)); for (x=0;x<256;x++) liV[x]=(x*c)>>8; // V wall
         c=155.0+fabs(100.0*cos(30.0*deg)); for (x=0;x<256;x++) liF[x]=(x*c)>>8; // floor, wall top side
    
         // [2D map]
         m=_Doom3D_edit_cell_size;
         for (my0=0,my1=m,y=0;y<map.ys;y++,my0=my1,my1+=m)   // map.pyx[][]
          for (mx0=0,mx1=m,x=0;x<map.xs;x++,mx0=mx1,mx1+=m)
             {
             c=0x00010101*((0x40+(0x40*(map.pyx[y][x]>>24)))/_Doom3D_cell_size);
             for (my=my0;my<my1;my++)
              for (mx=mx0;mx<mx1;mx++)
               map2.pyx[my][mx]=c;
             }
         c=0x00202020;                                       // map grid
         for (y=0;y<map2.ys;y+=m) for (x=0;x<map2.xs;x++) map2.pyx[y][x]=c;
         for (x=0;x<map2.xs;x+=m) for (y=0;y<map2.ys;y++) map2.pyx[y][x]=c;
         x=keys.kx*m;                                        // selected cell
         y=keys.ky*m;
         map2.bmp->Canvas->Pen->Color=0x0020FFFF;
         map2.bmp->Canvas->MoveTo(x  ,y  );
         map2.bmp->Canvas->LineTo(x+m,y  );
         map2.bmp->Canvas->LineTo(x+m,y+m);
         map2.bmp->Canvas->LineTo(x  ,y+m);
         map2.bmp->Canvas->LineTo(x  ,y  );
         map2.bmp->Canvas->Pen->Mode=pmMerge;
    
         // [cast rays]
         a0=plr.a-(0.5*view_ang);
         da=divide(view_ang,scr.xs-1);
         da*=scale_x;
         for (a=a0,x=0;x<scr.xs;x+=scale_x,a+=da)
             {
             // grid V-line hits
             ll0=1.0e20; dl0=0.0; dx0=cos(a); const char tp0='V';
             if (dx0<0.0) { xx0=floor(plr.x); dx0=-1.0; }
             if (dx0>0.0) { xx0=ceil (plr.x); dx0=+1.0; }
             if (fabs(dx0)>1e-6) { dy0=tan(a); yy0=plr.y+((xx0-plr.x)*dy0);             dy0*=dx0; dx=xx0-plr.x; dy=yy0-plr.y; ll0=sqrt((dx*dx)+(dy*dy)); dl0=sqrt((dx0*dx0)+(dy0*dy0)); }
             // grid H-line hits
             ll1=1.0e20; dl1=0.0; dy1=sin(a); const char tp1='H';
             if (dy1<0.0) { yy1=floor(plr.y); dy1=-1.0; }
             if (dy1>0.0) { yy1=ceil (plr.y); dy1=+1.0; }
             if (fabs(dy1)>1e-6) { dx1=divide(1.0,tan(a)); xx1=plr.x+((yy1-plr.y)*dx1); dx1*=dy1; dx=xx1-plr.x; dy=yy1-plr.y; ll1=sqrt((dx*dx)+(dy*dy)); dl1=sqrt((dx1*dx1)+(dy1*dy1)); }
             p.ang=a;
             p.sx =x;
             p.sy0=scr.ys;
             p.sy1=scr.ys;
             // first hit
             if (ll0<ll1){ p.tp0=tp0; p.x0=xx0; p.y0=yy0; p.l0=ll0; xx0+=dx0; yy0+=dy0; ll0+=dl0; }
              else       { p.tp0=tp1; p.x0=xx1; p.y0=yy1; p.l0=ll1; xx1+=dx1; yy1+=dy1; ll1+=dl1; }
             p.l0*=cos(p.ang-plr.a); // anti fish eye
             p.map=0xFFFFFFFF; p.x=p.x0; p.y=p.y0;
             for (;;)
                 {
                 // closest hit
                 if (ll0<ll1) { p.tp1=tp0; p.x1=xx0; p.y1=yy0; p.l1=ll0; xx0+=dx0; yy0+=dy0; ll0+=dl0; }
                  else        { p.tp1=tp1; p.x1=xx1; p.y1=yy1; p.l1=ll1; xx1+=dx1; yy1+=dy1; ll1+=dl1; }
                 p.x=floor(0.5*(p.x0+p.x1)); // actaul cell position
                 p.y=floor(0.5*(p.y0+p.y1));
                 p.l1*=cos(p.ang-plr.a);     // anti fish eye
                 // edge of map crossed?
                 if ((p.x>=0)&&(p.x<map.xs)&&(p.y>=0)&&(p.y<map.ys)) p.map=map.pyx[p.y][p.x]; else break;
                 // render
                 draw_cell(p);               // scan line
                 if (p.sy1<=0) break;        // scan line reached top of screen
                 // prepare next cell position
                 p.tp0=p.tp1; p.x0=p.x1; p.y0=p.y1; p.l0=p.l1;
                 }
             // copy skiped scan lines
             for (mx=1;mx<scale_x;mx++)
              if (x+mx<scr.xs)
               for (y=0;y<scr.ys;y++)
                scr.pyx[y][x+mx]=scr.pyx[y][x];
             // render map ray
             if (x==sxs2)                   map2.bmp->Canvas->Pen->Color=0x000000FF;
             if ((x==0)||(x==sxs2+scale_x)) map2.bmp->Canvas->Pen->Color=0x00002020;
             map2.bmp->Canvas->MoveTo(plr.x*m,plr.y*m);
             map2.bmp->Canvas->LineTo(p.x1*m,p.y1*m);
             }
         map2.bmp->Canvas->Pen->Mode=pmCopy;
         map2.bmp->Canvas->Pen->Color=0x000000FF;
         map2.bmp->Canvas->Brush->Color=0x000000FF;
         c=focus*m;
         map2.bmp->Canvas->Ellipse(plr.x*m-c,plr.y*m-c,plr.x*m+c,plr.y*m+c);
         scr.bmp->Canvas->Draw(0,0,map2.bmp);
         // ... here HUD and info texts continues I skipped it to keep this simple
         }
    
  2. 渲染地板和天花板,没有每行/列的像素投射

    简单的光线投射器确实使用无纹理的地板/天花板,这使得这很简单,只需在渲染墙壁之前(或者如果记住渲染墙壁的开始和结束,则在它之后)用天空渲染一半屏幕,用地面颜色渲染另一个屏幕:

    代码中的类似内容:

     int x,y,sxs=sxs2<<1,sys=sys2<<1;
     // simple color sky/ceiling
     for (y=0;y<sys2;y++)
      for (x=0;x<sxs;x++)
       scr.pyx[y][x]=0x000080FF;
      for (y=sys2;y<sys;y++)
       for (x=0;x<sxs;x++)
        scr.pyx[y][x]=0x00404040;
    

    为了使它更漂亮,通常添加了覆盖天花板的地图天空纹理的户外部分。它不随玩家移动,只是旋转。因此,您可以将带有天空纹理的四边形映射到仅使用 -plr.a 旋转的视图的上半部分,这里是几何图形的概述:

    较大的半径R 是纹理分辨率的一半,而较小的半径我根据经验通过r=R*sin(0.5*view_ang) 计算,因为它对我来说看起来最好(但是真实值应该根据屏幕纵横比和透视焦距和view_ang 计算)。

    这里有一些代码:

         const int x0=0,x1=sxs2<<1,y0=0,y1=sys2,y2=sys2<<1;
         int sx[4]={x0,x0,x1,x1},
             sy[4]={y0,y1,y1,y0},
             tx[4],ty[4],dx,dy;
         float a,r,R;
         R=sky.xs>>1;            // sky texture inscribed circle radius
         r=R*sin(0.5*view_ang);  // smaller radius (visible portion of sky)
         dx=sky.xs>>1;           // mid of sky texture
         dy=sky.ys>>1;
         a=plr.a-(0.5*view_ang);
         tx[0]=float(R*cos(a))+dx;
         ty[0]=float(R*sin(a))+dy;
         a=plr.a-(0.5*view_ang);
         tx[1]=float(r*cos(a))+dx;
         ty[1]=float(r*sin(a))+dy;
         a=plr.a+(0.5*view_ang);
         tx[2]=float(r*cos(a))+dx;
         ty[2]=float(r*sin(a))+dy;
         a=plr.a+(0.5*view_ang);
         tx[3]=float(R*cos(a))+dx;
         ty[3]=float(R*sin(a))+dy;
         polygon2D(scr,sky,sx,sy,tx,ty,4);
    

    地面(和室内天花板)可以类似地完成,但半径R 必须是整个纹理的一小部分。玩家在地图中的位置必须缩放到纹理half size - R 并添加到纹理坐标中。然而,纹理分辨率必须足够大,否则看起来不会那么好(理想情况下,空白空间应该与地图大小的分辨率*墙壁纹理分辨率相匹配......所以如果R 是一半空白空间也将是@987654350 @ 然后它是这样完成的:

         const int x0=0,x1=sxs2<<1,y0=0,y1=sys2,y2=sys2<<1;
         int sx[4]={x0,x0,x1,x1},
             sy[4]={y1,y2,y2,y1},
             tx[4],ty[4],i,dx,dy,dr;
         float a,r,R;
         R=sky.xs>>2;            // sky texture inscribed circle radius /2 so empty space is also R
         r=R*sin(0.5*view_ang);  // smaller radius (visible portion of sky)
         dx=sky.xs>>1;           // mid of sky texture
         dy=sky.ys>>1;
         a=float(R)/float(map.xs);   // add player position skaled to empty space
         dx+=float(plr.x*a);
         dy+=float(plr.y*a);
         a=plr.a-(0.5*view_ang);
         tx[0]=float(R*cos(a))+dx;
         ty[0]=float(R*sin(a))+dy;
         a=plr.a-(0.5*view_ang);
         tx[1]=float(r*cos(a))+dx;
         ty[1]=float(r*sin(a))+dy;
         a=plr.a+(0.5*view_ang);
         tx[2]=float(r*cos(a))+dx;
         ty[2]=float(r*sin(a))+dy;
         a=plr.a+(0.5*view_ang);
         tx[3]=float(R*cos(a))+dx;
         ty[3]=float(R*sin(a))+dy;
         polygon2D(scr,sky,sx,sy,tx,ty,4);
    

    如果您想使用较小的纹理,那么您的多边形渲染必须能够处理纹理坐标,例如 OpenGL 中的GL_REPEATpolygon2D(scr,sky,sx,sy,tx,ty,4) 函数只是简单/丑陋/慢/未优化的 2D 纹理多边形渲染我昨天忙着测试这个(因为我不想弄乱我的优化渲染例程#1 方法只支持扫描线而不是多边形无论如何) 其中sx,sy 是屏幕坐标数组,tx,ty 是纹理坐标数组,4 是顶点数,scr,txr 是目标和源纹理。代码只是这个fill_quad 的一个端口,没有阴影和SSD1306 相关的东西。完整代码如下:

     const int ys_max=1024;
     int bufl_vx[ys_max],bufr_vx[ys_max];
     int bufl_tx[ys_max],bufr_tx[ys_max];
     int bufl_ty[ys_max],bufr_ty[ys_max];
     void _fill2D_line(Texture2D &scr,Texture2D &txr,int vx0,int vy0,int tx0,int ty0,int vx1,int vy1,int tx1,int ty1)
         {
         int *bvx,*btx,*bty;
         int i,n,cvx,cvy,ctx,cty,svx,svy,stx,sty;
         // target buffer depend on y direction (before point ordering)
         if (vy0<vy1){ bvx=bufl_vx; btx=bufl_tx; bty=bufl_ty; }
          else       { bvx=bufr_vx; btx=bufr_tx; bty=bufr_ty; }
         // order points so joined edges are interpolated the same way
         if (vx0>vx1)
             {
             i=vx0; vx0=vx1; vx1=i;
             i=vy0; vy0=vy1; vy1=i;
             i=tx0; tx0=tx1; tx1=i;
             i=ty0; ty0=ty1; ty1=i;
             }
         // line DDA parameters
         vx1-=vx0; svx=0; if (vx1>0) svx=+1; if (vx1<0) { svx=-1; vx1=-vx1; } if (vx1) vx1++;            n=vx1;
         vy1-=vy0; svy=0; if (vy1>0) svy=+1; if (vy1<0) { svy=-1; vy1=-vy1; } if (vy1) vy1++; if (n<vy1) n=vy1;
         tx1-=tx0; stx=0; if (tx1>0) stx=+1; if (tx1<0) { stx=-1; tx1=-tx1; } if (tx1) tx1++; if (n<tx1) n=tx1;
         ty1-=ty0; sty=0; if (ty1>0) sty=+1; if (ty1<0) { sty=-1; ty1=-ty1; } if (ty1) ty1++; if (n<ty1) n=ty1;
         // single pixel (not a line)
         if (!n)
             {
             if ((vy0>=0)&&(vy0<scr.ys))
                 {
                 bufl_vx[vy0]=vx0; bufl_tx[vy0]=tx0; bufl_ty[vy0]=ty0;
                 bufr_vx[vy0]=vx0; bufr_tx[vy0]=tx0; bufr_ty[vy0]=ty0;
                 }
             return;
             }
         // horizontal line
         if (svy==0) return;
         // ND DDA algo i is parameter
         for (cvx=cvy=ctx=cty=n,i=0;;)
             {
             if ((vy0>=0)&&(vy0<scr.ys)){ bvx[vy0]=vx0; btx[vy0]=tx0; bty[vy0]=ty0; }
             i++; if (i>=n) break;
             cvx-=vx1; if (cvx<=0){ cvx+=n; vx0+=svx; }
             cvy-=vy1; if (cvy<=0){ cvy+=n; vy0+=svy; }
             ctx-=tx1; if (ctx<=0){ ctx+=n; tx0+=stx; }
             cty-=ty1; if (cty<=0){ cty+=n; ty0+=sty; }
             }
         }
     void _fill2D(Texture2D &scr,Texture2D &txr,int Y0,int Y1)
         {
         int vx0,vx1,tx0,tx1,ty0,ty1;
         int vy,i,n,cvx,ctx,cty,svx,stx,sty;
         // fill horizontal lines
         for (vy=Y0;vy<=Y1;vy++)
             {
             // horizontal line to render
             vx0=bufl_vx[vy]; tx0=bufl_tx[vy]; ty0=bufl_ty[vy];
             vx1=bufr_vx[vy]; tx1=bufr_tx[vy]; ty1=bufr_ty[vy];
             if ((vx0<      0)||(vx1<      0)) continue;
             if ((vx0<      0)&&(vx1<      0)) continue;
             if ((vx0>=scr.xs)&&(vx1>=scr.xs)) continue;
             // line DDA parameters
             vx1-=vx0; svx=0; if (vx1>0) svx=+1; if (vx1<0) { svx=-1; vx1=-vx1; } if (vx1) vx1++;            n=vx1;
             tx1-=tx0; stx=0; if (tx1>0) stx=+1; if (tx1<0) { stx=-1; tx1=-tx1; } if (tx1) tx1++; if (n<tx1) n=tx1;
             ty1-=ty0; sty=0; if (ty1>0) sty=+1; if (ty1<0) { sty=-1; ty1=-ty1; } if (ty1) ty1++; if (n<ty1) n=ty1;
             // single pixel (not a line)
             if (!n)
                 {
                 if ((vx0>=0)&&(vx0<scr.xs)) scr.pyx[vy][vx0]=txr.pyx[ty0][tx0];
                 continue;
                 }
             // ND DDA algo i is parameter
             for (cvx=ctx=cty=n,i=0;;)
                 {
                 while (tx0<0) tx0+=txr.xs;
                 while (ty0<0) ty0+=txr.ys;
                 while (tx0>=txr.xs) tx0-=txr.xs;
                 while (ty0>=txr.ys) ty0-=txr.ys;
                 if ((vx0>=0)&&(vx0<scr.xs)) scr.pyx[vy][vx0]=txr.pyx[ty0][tx0];
                 i++; if (i>=n) break;
                 cvx-=vx1; if (cvx<=0){ cvx+=n; vx0+=svx; }
                 ctx-=tx1; if (ctx<=0){ ctx+=n; tx0+=stx; }
                 cty-=ty1; if (cty<=0){ cty+=n; ty0+=sty; }
                 }
             }
         }
     void polygon2D(Texture2D &scr,Texture2D &txr,int *vx,int *vy,int *tx,int *ty,int n)
         {
         int i,j,y,Y0,Y1;
         // y range to render
         Y0=Y1=vy[0];
         for (i=1;i<n;i++)
             {
             if (Y0>vy[i]) Y0=vy[i];
             if (Y1<vy[i]) Y1=vy[i];
             }
         // clip to screen in y axis
         if ((Y1<0)||(Y0>=scr.ys)) return;
         if (Y0<      0) Y0=       0;
         if (Y1>=scr.ys) Y1=scr.ys-1;
         // clear buffers
         for (y=Y0;y<=Y1;y++)
             {
             bufl_vx[y]=-1;
             bufr_vx[y]=-1;
             }
         // render circumference
         for (j=n-1,i=0;i<n;j=i,i++)
          _fill2D_line(scr,txr,vx[i],vy[i],tx[i],ty[i],vx[j],vy[j],tx[j],ty[j]);
         // fill horizontal lines
         _fill2D(scr,txr,Y0,Y1);
         }
    

    最后预览(使用天空和地面的天空纹理):

    渲染没有透视正确的插值,但对于单个大纹理它不是一个大问题。如果您还想跳跃,那么您需要使用玩家的z 位置重新计算R,r,或者使用另一种选项通过简单地投射4条光线(半屏矩形的每个角一个)来计算纹理坐标并检查位置它击中了地图边缘。

    后面的数学可以在这里找到:

    但请注意,您的视角必须与光线投射相匹配,否则可能会出现对齐伪影(或地面移动的速度与墙壁略有不同)。

【讨论】:

  • 谢谢@Spectre,我要去研究它,我的 Raycaster 也可以在墙壁、地板和天花板上的每个瓷砖上拥有不同的纹理(就像在我的 youtube 视频中一样)——这也会影响性能,因为我需要检查每个像素上的瓷砖并选择正确的纹理..但是是的。地板将保持只有一个高度 - 不打算做不同的高度尺寸
  • @Mateusz 我更新了我的答案。 #2 完成。稍后将继续使用#1,但今天我的盘子上没有很多,因为最后天气好转了,所以我可以正常工作。昨天因为它转了几个小时。今天这是在早餐前完成的 :) 年纪越大,我就越依赖天气。
  • 你好@Spektre,再次感谢!大量的知识!是的,天空盒子本身和无纹理的地板(甚至是距离阴影)是一个很大的加速。有一个非常有趣的项目,称为标准 Amiga 500 的“Dread”(Doom 克隆),它使用类似的方法,我的意思是 - 天空+ 非纹理地板:youtube.com/watch?v=c00B3uE5FV8 我期待看到您在该纹理版本上的解决方案 - 这是我的目标 :) 我花了一些时间寻找和测试不同的方法,但现在我缺乏想法。 :)
  • @Mateusz 我添加了带有工作代码(或多或少优化)的 #1 并实现了上限。我已经达到了 30K 的限制,所以我不能添加更多(必须删除一些格式行并重新编写文本以适应)我的引擎唯一剩下的就是渲染天空纹理,以防天花板纹理设置为空白......
  • 您好,感谢您提供的文件,我会检查一下。同时,我想出了一个全局优化“技巧”。由于我的纹理是 128x128,它们不需要是真正的颜色索引颜色就足够了,例如。 256 种颜色。然后我可以预先计算 256 个强度值 0-255 的颜色表,并将其用于距离着色。现在我将我的 4byte 颜色拆分为单独的 r、g、b - 为每个颜色取强度并将其放回 4byte 值。它的 7 个操作 + 3 个数组调用。使用索引颜色,我只有 0 个操作,只有 1 个数组调用..
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-21
  • 1970-01-01
  • 2015-02-19
  • 2023-03-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多