我正在为HTML5画布开发一个基本的光线投射引擎,用于像Wolfenstein 3D和Doom这样的游戏,作为一个学习练习/爱好项目。我已经达到了在画布上使用纹理映射进行墙壁渲染的程度,经过相当多的努力才能使交叉点测试功能正常运行。
我正在修正“鱼缸”/“鱼眼”失真效果(由于距离屏幕中心的角度增加而增加到交叉点的距离所引起的),但是我仍然在边缘处有一个非常轻微但明显的弯曲变形的屏幕。这可以在下面的图像中看到(我绘制了直线红线以使效果更明显):
任何人都可以了解这种失真的来源是什么吗?这不是一个大问题,但我无法弄清楚原因,所以我显然错过了一些东西,我确信有人必须知道答案。我已经非常广泛地搜索了这个问题并且网上的信息不多,但我确实在论坛帖子中找到了以下代码:
“使用恒定角度增量代替水平投影引起的翘曲完全是另一个问题 - 这就是横向拉伸/聚束效应,虽然它通常只是一个几乎不显眼的效果(对于合理的FOV,尽管你可以定义一个999999度的FOV应该响铃),根本没有任何合理的方式来补偿,除了做正确的开始..角度是错误的使用固定的增量,这就是那个。“
这听起来可能是指我遇到的相同失真,但除了提示固定角度增量是问题的根源之外它没有提供太多帮助或见解(它是一个弯曲的失真,朝向屏幕边缘增加,这似乎符合这个建议)。我用来纠正扭曲的功能是:
function m_CorrectRayLengthDistortion( dist, angleFromCentre ){
return dist * Math.cos( MOD_Maths.degToRad( angleFromCentre ) );
}
MOD_Maths是一个实用程序模块(在这种情况下用于将角度从度数转换为弧度,因此余弦函数可以使用它)。
非常感谢任何有关此问题的帮助,如果有人回答这个问题,它将有希望为将来遇到此问题的任何人提供指导,因为在线提供的主题信息很少。
谢谢 :)
我刚刚解决了这个问题,但直到现在才更新答案。我已经删除了我以前的答案,这是不正确的(它给出了几乎正确的结果,但是通过间接方法,由于我对问题的根本原因缺乏了解)。
正如Sam先前在其评论中提到的那样,问题的根本原因在于,如果要实现等间距列(这是渲染结果看起来不失真所必需的),固定角度增量实际上是不正确的。这在here的论坛帖子中提到过,但是虽然我发现了这个但我并不完全理解为什么会这样,或者如何解决这个问题,直到很久以后。
为了在屏幕上实现等间隔的列,理所当然地说每条光线必须从视点行进并穿过沿投影表面等距离的像素,这意味着当光线进一步从中心像素移动时屏幕,与观察方向的角度增加的增量逐渐变小。下图说明了这一点(道歉,这不完全是一件艺术品):
对于小视野,问题不是很明显,但随着视野的增加而变得更成问题(在我的图中,视野非常大,以清楚地说明问题)。要正确计算光线角度增量,必须使用以下过程:
哪里:
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 );
}
由于在线提供的光线投射引擎信息有些稀缺,而且由于这个特定问题没有在任何主要教程中明确涵盖,我想用正确的更新这篇文章信息,以防其他人有同样的问题我做了,不明白为什么。希望这会对某人有所帮助。
花了几个小时试图在我自己的光线投射引擎上解决这个确切的问题,我想提供更详细的数学背景,为什么这是正确答案,因为我一开始并不完全相信。特别是在进行透视投影时,您必须纠正一些球形失真(鱼缸效应)。这里描述的效果是完全不同的效果。
这就是我在我的发动机中得到的结果:相机在一个方形房间里,以大约45°的角度观察角落,视野为90°。它似乎有轻微的球形失真。之后添加了红线,在运动中看起来也更加明显,但制作GIF是PITA:
这是相同的房间,相同的位置和角度,但FOV为70°。它并不明显(并且再次,更容易在运动中看到):
我的光线投射引擎的第一个版本从-FOV / 2 + camera_angle向FOV / 2 + camera_angle发射光线,每个角度间隔FOV / SCREEN_WIDTH度(在我的情况下SCREEN_WIDTH为640)。
这是一个顶视图架构,SCREEN_WIDTH = 9:
你可以在这里看到问题:当我们使用固定角度时,唯一保证恒定的是两条射线之间的圆弧。但是应该保持不变的是投影平面上的线段。我们可以通过使用一个固定的角度看到,段距离中心越远越长。
要解决此问题,请记住以下参数:
知道了这一点,我们可以通过在三角形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);
}
我最初使用这些表来投射光线,这导致前两个截图。所以我添加了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 */
/* ... */
}
}