光线引擎渲染会在屏幕边缘产生轻微的失真

时间:2014-06-11 23:00:15

标签: math canvas 3d rendering raycasting

我正在为HTML5画布开发一个基本的光线投射引擎,用于像Wolfenstein 3D和Doom这样的游戏,作为一个学习练习/爱好项目。我已经达到了在画布上使用纹理贴图进行墙面渲染的要点,经过相当多的努力使交叉点测试功能正确后,这种方法非常有效。

我正在纠正“鱼缸”/“鱼眼”失真效果(由于距离屏幕中心的角度增加而增加到交叉点的距离所引起的),但我仍然有一个非常轻微但明显的弯曲变形在屏幕的边缘。这可以在下面的图像中看到(我画了直线红线以使效果更明显):

example of distortion

任何人都可以了解这种失真的来源是什么吗?这不是一个大问题,但我无法弄清楚原因,所以我显然错过了一些东西,我确信有人必须知道答案。我已经非常广泛地搜索了这个问题,网上的信息不多,但我确实在论坛帖子中找到了以下代码:

“使用恒定角度增量而不是水平投影引起的翘曲完全是另一个问题 - 这就是横向拉伸/聚束效应,虽然它通常只是一个几乎不显眼的效果(对于合理的FOV,尽管你可以定义一个999999度FOV应该响铃),根本没有任何合理的方法可以补偿除了正确的开始之外...角度是错误的,使用固定的增量,那就是那个。“

这听起来可能是指我遇到的相同失真,但除了提示固定角度增量是问题的根源之外它没有提供太多帮助或洞察力(它是一个弯曲的失真,朝向边缘增加屏幕,似乎符合这个建议)。我用来纠正扭曲的功能是:

function m_CorrectRayLengthDistortion( dist, angleFromCentre ){

    return dist * Math.cos( MOD_Maths.degToRad( angleFromCentre ) );
}

MOD_Maths是一个实用程序模块(在这种情况下用于将角度从度数转换为弧度,因此余弦函数可以使用它)。

非常感谢任何有关此问题的帮助,如果有人回答这个问题,它将有希望为将来遇到此问题的任何人提供指导,因为在线提供的主题信息非常缺乏。

谢谢:)

2 个答案:

答案 0 :(得分:4)

我很久以前就解决了这个问题,但直到现在还没有更新答案。我已经删除了我之前的答案,这是错误的(它给出了几乎正确的结果,但是由于我对问题的根本原因缺乏了解而采用间接方法)。

正如Sam先前在评论中提到的那样,问题的根本原因在于,如果要实现等间距列(这是渲染结果看起来不失真所必需的),固定角度增量实际上是不正确的。这在here论坛帖子中提到过,但是虽然我发现了这一点,但我并不完全理解为什么会这样,或者如何解决问题,直到很久以后。

为了在屏幕上实现等间距的列,理所当然地说每条光线必须从视点行进并穿过沿投影表面等距离的像素,这意味着当光线进一步从投影表面移动时屏幕的中心像素,与观察方向的角度增加的增量逐渐变小。下图说明了这一点(道歉,它不是一件艺术品):

diagram

对于小视野,问题不是很明显,但随着视野的增加而变得更加成问题(在我的图中,视野非常大,以清楚地说明问题)。要正确计算光线角度增量,必须使用以下过程:

其中:

ang = ray angle from the look direction, whose ray passes through the central x coordinate of the screen;
opp = opposite side (equivalent to the distance of the screen X coordinate through which the ray passes from the screen X coordinate of the central pixel);
adj = adjacent side (equivalent to the distance from the point of view to the projection surface, which will be predetermined in code somewhere);

我们可以使用以下公式(为了清楚起见,包括派生):

tan( ang ) = opp / adj
ang = atan( opp / adj )
ang = atan( ( pixel x coord - half screen width ) / dist to projection surface )

我的引擎中的Javascript代码示例:

for( var x = 0; x < canvasSizeX; x++ ){

    var xAng = _atan( ( x - canvasSizeHalfX ) / m_DistToProjSurf );
    xRayAngles.push( xAng );
}

由于在线提供的有关光线投影引擎的信息有些稀缺,而且由于这个特定问题并未在任何主要教程中明确涵盖,我想更新这篇文章提供了正确的信息,以防其他人遇到同样的问题但我不明白为什么。希望这会对某人有所帮助。

答案 1 :(得分:3)

花了几个小时试图用我自己的光线投射引擎解决这个确切的问题,我想给出为什么这是正确答案的更详细的数学背景,因为起初我并不完全相信。特别是因为在进行透视投影时,您已经必须校正一些球形变形(鱼缸效应)。这里描述的效果是完全不同的效果。

这就是我使用的引擎:相机位于方形房间内,以大约45°的角度注视着一个角落,其FOV为90°。似乎有轻微的球形变形。红线是在之后添加的,在运动中也更加明显,但是制作GIF就是PITA:

Spherical distortion

这里是同一房间,位置和角度相同,但视野为70°。它并不那么引人注目(同样,在运动中更容易看到):

Same room with FOV=70

我的光线投射引擎的第一个版本从-FOV / 2 + camera_angle到FOV / 2 + camera_angle发出光线,每个角度之间的距离为FOV / SCREEN_WIDTH度(在我的情况下,SCREEN_WIDTH为640)。

这是一个顶视图架构,SCREEN_WIDTH = 9:

Casting rays

您可以在这里看到问题:当我们使用固定角度时,唯一可以保证不变的是两条射线之间的圆弧。但是应该恒定的是投影平面上的线段。通过使用固定角度,我们可以看到这些分段离中心越远越长。

要解决此问题,请记住以下参数:

  • FOV =视场,在此示例中为90°。
  • DIST =相机到投影平面的距离。我最初在引擎中选择了50,但并没有更好的了解,但是实际上需要根据FOV进行调整。
  • SCREEN_WIDTH =屏幕宽度(以像素为单位),在我的示例中为640

知道了这一点,我们可以通过使用三角形ABC中的一些三角函数来计算投影平面上的线段(SEG_LEN)的长度:

  

tan(FOV / 2)= SCREEN_HALFLEN / DIST

     

SCREEN_HALFLEN = DIST * tan(FOV / 2)

SCREEN_HALFLEN是在假想平面上投影的屏幕长度,要获得SEG_LEN,只需执行以下操作:

  

SEG_LEN = SCREEN_HALFLEN /(SCREEN_WIDTH / 2)

知道了片段的长度,我们可以计算出需要发射光线的实际角度:给定x列从0到SCREEN_WIDTH-1,角度应该是:

  

ANGLES [x] = atan((((SEG_LEN * x-SCREEN_HALFLEN)/ DIST)

这与詹姆斯·希尔在他的最后一个例子中给出的公式差不多。将所有这些放到引擎中,确实消除了球形变形:

Spherical distortion corrected

为了好玩,我们可以计算固定角度射线投射和固定长度射线投射之间的区别,在最坏的情况下,射线x = 97,相差9个像素:

固定角度射线投射的角度为= 97 * FOV / SCREEN_WIDTH-FOV / 2 = -31.359375°

使用固定长度的光线投射,角度为:atan(97 * SEG_LEN / DIST)= -34.871676373193203°

因此,使用给定参数(FOV = 90,DIST = 50,SCREEN_WIDTH = 640),误差高达11%。

作为参考,我想添加更多有关如何实现这是我的引擎的细节:无论好坏,我都想使用整数算术(除初始化工作外)进行所有操作。首先,我使用定点算术(示例使用C语言)设置了两个表以预先计算正弦和余弦值:

#define FIXEDSHIFT     13
#define FIXEDPRES      (1<<FIXEDSHIFT)
#define DIST           50
#define FOV            90
#define SCREEN_WIDTH   640
#define SCREEN_HEIGHT  480
#define HALF_WIDTH     (SCREEN_WIDTH/2)

int i;
int size = 360.0 / ((double)FOV / SCREEN_WIDTH)));
int16_t * Cos = malloc(size * sizeof *Cos);
int16_t * Sin = malloc(size * sizeof *Sin);

for (i = 0; i < size; i ++)
{
    double angle = i * (2.0*M_PI / size);
    Cos[i] = (int16_t)(cos(angle) * FIXEDPRES);
    Sin[i] = (int16_t)(sin(angle) * FIXEDPRES);
}

我最初使用这些表来投射光线,从而产生了前2个屏幕截图。所以我添加了ANGLES表,将其拆分为笛卡尔坐标:

int16_t * XRay = malloc(SCREEN_WIDTH * sizeof *XRay);
int16_t * YRay = malloc(SCREEN_WIDTH * sizeof *YRay);
double    dist = (DIST * tan(FOV*M_PI/360)) / (HALF_WIDTH-1);

for (i = 0; i < HALF_WIDTH; i ++)
{
    #if 0
    /* for fun, this re-enables the spherical distortion */
    double angle = i * (2.0*M_PI / (MAX_TAB));
    #else
    double angle = atan((dist * i) / DIST);
    #endif

    XRay[HALF_WIDTH-i-1] =   XRay[HALF_WIDTH+i] = (int16_t)(cos(angle) * FIXEDPRES);
    YRay[HALF_WIDTH-i-1] = -(YRay[HALF_WIDTH+i] = (int16_t)(sin(angle) * FIXEDPRES));
}

然后在光线投射引擎中,为了获得正确的光线,我使用了:

int raycasting(int camera_angle)
{
    int i;
    for (i = 0; i < SCREEN_WIDTH; i ++)
    {
        int dx = Cos[camera_angle];
        int dy = Sin[camera_angle];

        /* simply apply a rotation matrix with dx (cos) and dy (sin) */
        int xray = (XRay[i] * dx - YRay[i] * dy) >> FIXEDSHIFT;
        int yray = (XRay[i] * dy + YRay[i] * dx) >> FIXEDSHIFT;

        /* remember that xray and yray are respectively cosine and sine of the current ray */

        /* you will need those values to do perspective projection */

        /* ... */
    }
}