tinyraycaster关键技术分析

简介:

        本文章旨在分析我在学习和制作github上面的tinyraycaster时遇到的一些问题,以及我理解的技术原理。注:本文章并非中文教程,只是解释了一些代码的原理

        文章阅读方法:进入以下github地址阅读原版教程,遇到看不懂的地方转到 此文章查阅

        源地址:https://github.com/ssloy/tinyraycaster

1.图片处理

       首先图像中每一个像素可以用4个值来展示,即rgba,分别代表红绿蓝和alpha通道,给与不同的分量便可以展示不同的颜色。

  pack_color

uint32_t pack_color(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a=255) {
    return (a<<24) + (b<<16) + (g<<8) + r;
}

这段代码输入的是rgba分量的值,其中每一个分量都是一个八位无符号数,将a左移24位,b左移16位,g左移8位再加上r,返回一个32位无符号整数代表颜色。

例如 a = 255,b =254, g = 253,r = 252

return = 11111111 11111110 11111101 11111100

unpack_color

void unpack_color(const uint32_t &color, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) {
    r = (color >>  0) & 255;
    g = (color >>  8) & 255;
    b = (color >> 16) & 255;
    a = (color >> 24) & 255;
}

输入一个32位颜色值,将其分解成r,g,b,a分量储存。

取g来看,g = (color >>  8) & 255;

表示将color右移 8 位再和255(11111111)进行按位与,将255看作32位整数

假设颜色值color = 11111111 11111110 11111101 11111100

color >> 8  =00000000 11111111 11111110 11111101

(255)D = 00000000 00000000 00000000 11111111

进行按位与之后,前面24位全部归零,得到的结果自然就是最后8位 11111101,即g的值

drop_ppm_image:将颜色输出到图片,不解释

绘制背景图

    std::vector<uint32_t> framebuffer(win_w*win_h, 255); // the image itself, initialized to red

    for (size_t j = 0; j<win_h; j++) { // fill the screen with color gradients
        for (size_t i = 0; i<win_w; i++) {
            uint8_t r = 255*j/float(win_h); // varies between 0 and 255 as j sweeps the vertical
            uint8_t g = 255*i/float(win_w); // varies between 0 and 255 as i sweeps the horizontal
            uint8_t b = 0;
            framebuffer[i+j*win_w] = pack_color(r, g, b);
        }
    }

    drop_ppm_image("./out.ppm", framebuffer, win_w, win_h);

首先定义了一个一维数组来存储每一个像素的信息,数组大小为win_w * win_h

接下来遍历每一行,每一个像素,对其颜色计算之后输出到图像中

需要实现颜色的渐变效果,r = 255 * j / float(win_h)

更好理解的写法是 r = j * (255 / float(win_h))        

j 从 0 到 win_h 进行递增,乘以对应颜色递增的量,即可得到当前像素的颜色

因为 r 的值从上到下递增,即红色分量从上到下递增

g 从左到右递增,即绿色分量。蓝色分量b始终为0

最终输出的图像

2.绘制地图

作者使用简单粗暴的方式定义了一张16*16的地图

    const size_t map_w = 16; // map width
    const size_t map_h = 16; // map height
    const char map[] = "0000222222220000"\
                       "1              0"\
                       "1      11111   0"\
                       "1     0        0"\
                       "0     0  1110000"\
                       "0     3        0"\
                       "0   10000      0"\
                       "0   0   11100  0"\
                       "0   0   0      0"\
                       "0   0   1  00000"\
                       "0       1      0"\
                       "2       1      0"\
                       "0       0      0"\
                       "0 0000000      0"\
                       "0              0"\
                       "0002222222200000"; // our game map
    assert(sizeof(map) == map_w*map_h+1); // +1 for the null terminated string

其中有数字的地方代表墙壁,其余地方代表空。

1.draw rectangle

作者直接将地图分成不同的长方形,有点类似我的世界,通过在特定的位置放置长方形以进行墙壁的绘制,地图中每一个数字单元都是一个方块

void draw_rectangle(std::vector<uint32_t> &img, const size_t img_w, const size_t img_h, const size_t x, const size_t y, const size_t w, const size_t h, const uint32_t color) {
    assert(img.size()==img_w*img_h);
    for (size_t i=0; i<w; i++) {
        for (size_t j=0; j<h; j++) {
            size_t cx = x+i;
            size_t cy = y+j;
            assert(cx<img_w && cy<img_h);
            img[cx + cy*img_w] = color;
        }
    }
}

此代码用于绘制长方形,传入场景(即上面定义的framebuffer),传入场景的长宽(像素单位)

长方形左上角在场景的位置(x,y),长方形的宽和高(w,h),以及长方形的颜色

遍历长方形中的每一个像素,则长方形中的像素某个像素位置为(x+i,y+j),

在场景中将此位置的像素颜色设置为color

如下图在小格子的像素图中绘制绿色的正方形

长方形的宽和高通过 如下方法计算:场景的大小 / 地图大小  (512 / 16)

  const size_t rect_w = win_w/map_w;
  const size_t rect_h = win_h/map_h;

绘制一整张地图

    for (size_t j=0; j<map_h; j++) { // draw the map
        for (size_t i=0; i<map_w; i++) {
            if (map[i+j*map_w]==' ') continue; // skip empty spaces
            size_t rect_x = i*rect_w;
            size_t rect_y = j*rect_h;
            draw_rectangle(framebuffer, win_w, win_h, rect_x, rect_y, rect_w, rect_h, pack_color(0, 255, 255));
        }
    }

遍历整个地图(16*16),如果遍历到墙壁,即地图中非空格位置,则计算长方形左上角在场景(512*512)中的位置,因为整个场景是被地图平均分成512/16个大格子,所以长方形在左上角的位置直接通过如下方式计算

rect_x = i * rect_w;  rect_y = j * rect_h

绘制出来的图片如下:

2.添加玩家

在地图(16 * 16)中非墙壁的位置放置一个玩家,调用draw tectangle绘制一个小的正方形

    float player_x = 3.456; // player x position
    float player_y = 2.345; // player y position
draw_rectangle(framebuffer, win_w, win_h, player_x*rect_w, player_y*rect_h, 5, 5, pack_color(255, 255, 255));

3.射线!

1.绘制一根射线

 一条射线通过以上方式计算,t 为射线的长度,通过遍历 t ,寻找当射线长度为 t 时射线在该点的像素位置

当超过射线总长度(20)或者撞到墙壁时停止前进

float player_a = 1.523; // 玩家视线方向
    
for (float t=0; t<20; t+=.05) {
//计算射线位置
        float cx = player_x + t*cos(player_a);
        float cy = player_y + t*sin(player_a);
//撞到墙壁则停止前进
        if (map[int(cx)+int(cy)*map_w]!=' ') break;

//通过地图坐标(16*16)计算场景像素坐标(512*512)
        size_t pix_x = cx*rect_w;
        size_t pix_y = cy*rect_h;
//对应位置绘制
        framebuffer[pix_x + pix_y*win_w] = pack_color(255, 255, 255);
    }

绘制出来的射线如图

2.绘制一组射线

定义一个可视角度fov,角度angle

angle方向由玩家视线方向player_a决定

从 player_a - fov/2 开始发射射线,一直到player_a + fov/2

一共需要发射512根射线,以此来对应即将绘制的3d场景(512*512)中,每一列将使用一根射线来绘制

所以代码如下

 for (size_t i=0; i<win_w; i++) { // 发射一组(512根)射线

        float angle = player_a-fov/2 + fov*i/float(win_w);
        //每一根射线跨越的角度大小为 fov / win_w(512)
        绘制一根射线
}

 

 4.3D化

1.初步3D

首先扩展图片到原先的两倍大小(1024 * 512)

左边用于绘制地图,右边用于绘制3D图像

将右边图像为512根射线分配像素列,每一根射线对应一列像素

按照透视图像近大远小的规律,当射线撞到墙壁时

对于射线长度 t 大的一列像素,绘制高度小

对于射线长度 t 小的一列像素,绘制高度大

所以绘制的高度直接取 win_h / t;绘制的长方体宽度为1

if (map[int(cx)+int(cy)*map_w]!=' ') { // our ray touches a wall, so draw the vertical column to create an illusion of 3D
                size_t column_height = win_h/t;
                draw_rectangle(framebuffer, win_w, win_h, win_w/2+i, win_h/2-column_height/2, 1, column_height, pack_color(0, 255, 255));
                break;

注意:因为图片已经扩展到两倍大小,所以原来的一些坐标需要变换

 通过改变玩家位置player_x,player_y,以及改变玩家视线方向player_a可以实现运动效果

2.处理鱼眼畸变

当你渲染出3D图像时你会发现,在观察平直的墙壁,或者近处的物体会发生很严重的扭曲,即鱼眼畸变

 产生鱼眼畸变的原因:column_height = win_h / t;

        这一行代码计算了3D场景中绘制的像素高度,但是其计算方法是一个反函数

查看反函数在正数范围内的图像,随着 x 增大,其 y 减小,但是随着 x 增大,y减小的速率越来越小,在 x 很小的时候 y 减小的速率非常大。其递减规律并不是线性的

所以在我们观察近处物体时,t 值很小,则通过 t 计算出来的高度变换会非常剧烈,于是产生了鱼眼现象

如何解决鱼眼畸变,作者将反函数处理成这样一个函数

column_height = win_h/(t*cos(angle-player_a))

 试着分析一下,

假设当前玩家旁边有一面墙,玩家的视线方向与x轴夹角为a,玩家与墙面垂直距离为d

射线从 angle = 0 扫描到 angle = a的过程中有

 此时 t = (d / cos(angle))

将式子修改一下得到

column_height = win_h/(d/cos(angle)*cos(angle-player_a))

假设当前玩家视线方向a = 1rad,d = 3,将 angle 当成自变量 x

查看函数图像有

 可以看到从0到1.5之间的图像过度都非常平缓了。

而且我们只需要angle < a + fov/2 && angle > a-fov/2 的图像

于是在玩家正对着墙壁的时候,鱼眼畸变问题被完美解决

 但也带来了新的问题

 绘制出来的图像解决了鱼眼畸变问题,但是同样,墙壁边缘还是有和函数图像类似的变形。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值