Jam*_*ill 2 math 3d rendering canvas raycasting
我正在开发用于HTML5画布的基本光线投射引擎,作为学习练习/爱好项目,该引擎用于诸如Wolfenstein 3D和Doom之类的游戏。我已经到了要在画布上使用纹理贴图渲染墙壁的位置,经过大量的工作才能正确完成交点测试功能,该效果很好。
我正在校正“鱼缸” /“鱼眼”变形效果(由于与屏幕中心的夹角增加而导致到相交点的距离增加),但是边缘仍然有非常轻微但明显的弯曲变形屏幕的 可以在下图中看到(我画了红色的直线,使效果更明显):

谁能弄清楚这种失真的根源是什么?这不是一个大问题,但我无法找出原因,因此我显然缺少了一些东西,并且我敢肯定有人必须知道答案。我已经广泛搜索了该问题,并且在线上没有太多信息,但是我确实在论坛帖子中找到了以下代码段:
“使用恒定角度增量而不是地平线投影而引起的翘曲完全是另一回事-那是横向拉伸/聚束效果,尽管通常情况下,这只是勉强可以察觉的效果(对于合理的FOV,尽管您可以定义999999度FOV的事实应该敲响钟声),根本没有任何合理的方法可以补偿,除了正确的开始。使用固定的增量,角度是错误的,仅此而已。”
听起来好像是我遇到的相同失真,但是除了提示固定角度增量是问题的根源(它是弯曲失真,向屏幕边缘逐渐增加)以外,它并没有提供太多帮助或见解。 ,这似乎符合此建议)。我用来纠正失真的函数是:
function m_CorrectRayLengthDistortion( dist, angleFromCentre ){
return dist * Math.cos( MOD_Maths.degToRad( angleFromCentre ) );
}
Run Code Online (Sandbox Code Playgroud)
MOD_Maths是一个实用程序模块(在这种情况下,用于将角度从度转换为弧度,以便余弦函数可以使用它)。
非常感谢您提供任何帮助,并且如果有人回答此问题,则由于在线上缺少有关该主题的信息,因此希望将来对遇到此问题的任何人提供指导。
谢谢 :)
我很早以前就正确地解决了这个问题,但是直到现在还没有办法更新答案。我删除了之前的错误答案(由于我对问题的根本原因缺乏理解,它给出了几乎正确的结果,但是采用了间接方法)。
正如Sam在先前的评论中提到的那样,问题的根本原因在于,如果您想获得等距的列(使渲染结果看起来不失真,则固定角度增量实际上实际上是不正确的)。这是在这里的论坛帖子中提到的,但是,尽管我发现了这一点,但直到很久以后,我才完全不明白为什么会这样,或者如何解决该问题。
为了在屏幕上获得等距的列,可以合理地认为,每条光线必须从视点传播并穿过沿投影面等距分布的像素,这意味着随着光线从屏幕的中心像素移远在屏幕上,与观看方向的夹角增加的增量逐渐变小。下图说明了这一点(抱歉,这不是一件艺术品):
在较小的视场中,问题不是很明显,但是随着视场的增加,问题变得更加严重(在我的图中,视场很大,可以清楚地说明问题)。若要正确计算射线角度增量,必须使用以下过程:
哪里:
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);
Run Code Online (Sandbox Code Playgroud)
我们可以使用以下公式(为清楚起见,包括派生公式):
tan( ang ) = opp / adj
ang = atan( opp / adj )
ang = atan( ( pixel x coord - half screen width ) / dist to projection surface )
Run Code Online (Sandbox Code Playgroud)
我的引擎中的JavaScript代码示例:
for( var x = 0; x < canvasSizeX; x++ ){
var xAng = _atan( ( x - canvasSizeHalfX ) / m_DistToProjSurf );
xRayAngles.push( xAng );
}
Run Code Online (Sandbox Code Playgroud)
由于在线上提供的有关射线广播引擎的信息的本质有点稀缺,并且由于该特定问题并未在任何主要教程中明确涵盖,因此,我想用正确的信息来更新本文。信息,以防其他人遇到我遇到的相同问题并且不了解原因。希望这会帮助某人。
花了几个小时试图在我自己的光线投射引擎上解决这个确切的问题后,我想提供更详细的数学背景,说明为什么这是正确答案,因为一开始我并不完全相信。尤其是在进行透视投影时,您已经必须校正一些球面失真(鱼缸效应)。这里描述的效果是完全不同的效果。
这就是我在我的引擎中得到的:相机在一个方形房间里,以大约 45° 的角度看一个角落,FOV 为 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)
知道线段长度,我们可以计算出需要发射光线的真实角度:给定从 0 到 SCREEN_WIDTH-1 的列 x,角度应该是:
角度[x] = atan(((SEG_LEN * x - SCREEN_HALFLEN) / DIST)
这或多或少与 James Hill 在他的最后一个例子中给出的公式相同。将所有这些放在引擎中,它确实消除了球形失真:
为了好玩,我们可以计算固定角度光线投射和固定长度光线投射之间的差异,在最坏的情况下,光线 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);
}
Run Code Online (Sandbox Code Playgroud)
我最初使用这些表也投射光线,这导致了前 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));
}
Run Code Online (Sandbox Code Playgroud)
然后在光线投射引擎中,为了获得正确的光线,我使用了:
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 */
/* ... */
}
}
Run Code Online (Sandbox Code Playgroud)