问题描述
我正在使用 JavaScript 画布从头开始创建一个光线投射游戏。
挑战的一部分(对我而言)是用随机图像(图片)装饰墙壁。我已经实现了墙壁、地板、天花板和精灵的绘制。
在绘制墙壁时,我为每个 x
(描绘屏幕坐标)存储到墙壁的距离(Z-BUFFER
)、墙壁的高度(H-BUFFER
)和像素的实际坐标底层二维网格 (GRID_BUFFER
)。
我在墙上绘制贴花(图片)的方法如下(在确定了理论上可见的贴花列表之后):
- 计算到贴花位置的距离(位置被定义为面向观察者的网格顶点的中间)
- 屏幕坐标
decalScreenX
是根据网格坐标到屏幕坐标的变换矩阵计算出来的。这可以正常工作:
let decalScreenX = Math.floor((RAYCAST.SCREEN_WIDTH / 2) * (1 + CAMERA.transformX /CAMERA.transformDepth));
- 然后我检索相关贴花的图像数据并获取其宽度和高度
- 根据距离和观察到的角度,我计算出贴花的感知宽度。这就是真正的问题所在,因为我发现我没有完全准确地计算出这个宽度。
- 有了所有这些信息,就可以轻松计算左右屏幕坐标 - 从哪里开始和在哪里结束绘制贴花,使用
H-BUFFER
计算高度因子并使用GRID_BUFFER
仅在属于此贴花的网格上绘制。
如果玩家方向与贴花面向空间的方向不相反(示例),我看到了贴花从玩家方向向量旋转一个角度的宽度计算:
或者如果玩家方向与贴花方向直接相反,则此角度为 0°(示例):
我的第一种方法是使用反向玩家方向和贴花朝向的点积,从而获得向量之间角度的余弦,并将其用作减少感知宽度的因素:
let CosA = PLAYER.dir.mirror().dot(decal.facingDir);
let widthScale = CosA * (CAMERA.transformDepth / decal.distance);
此解决方案的问题是,当垂直时,因子为 0 并且不绘制贴花,但由于墙壁是透视绘制的,因此不应该是这种情况。于是我开始即兴创作。我定义了 CAMERA.minPerspective
因子,如下所示。视野 (FOV) 为 70°。
CAMERA.minPerspective = Math.cos(Math.radians((90 + this.FOV) / 2));
我的直觉是(因为我缺乏透视和几何知识,唉)对于小角度,因子应该保持为 1。对于接近 90° 的角度,应该有一些最小的因子,以便贴花保持可见。所以我带来了这个“改进”的代码:
let CosA = PLAYER.dir.mirror().dot(decal.facingDir);
let FACTOR = Math.min(1,CosA + CAMERA.minPerspective);
FACTOR = Math.max(FACTOR,CAMERA.minPerspective);
let widthScale = FACTOR * (CAMERA.transformDepth / decal.distance);
这样效果更好,但也有一些缺陷。从视觉上看,对于 0-50° 的角度,减少的因素太大了。如果我使用这种宽度的贴花,它们应该覆盖整个网格表面,就可以观察到这一点。 (见下图;在楼梯的左侧,下方的墙是可见的,贴花应该覆盖整个网格,但它没有,因为 FACTOR
太小了)。
我已经在 Stack Overflow 和网络上搜索了更好的解决方案,因为我的几何知识似乎也阻止了我识别超出此上下文的正确解决方案。
那么,拜托了。可能存在用于计算感知宽度的确定性解决方案,无需再次使用光线投射阶段或使用我能够在光线投射阶段存储的信息。虽然在代码示例中使用了 JavaScript,但我认为这个问题并不特定于任何编程语言。
解决方法
我找到了保留(甚至改进)问题方法的简单性和时间复杂性的解决方案。
- 我在贴花定义中添加了两点 -
leftDrawStart
和rightStartDraw
。这些很容易在贴花点计算 实例化,基于真实的精灵(贴花)宽度和定义 网格(块)大小。在进行此计算时,我会从相机角度(而非网格坐标)考虑leftDrawStart
。 - 渲染贴花时,我使用转换矩阵(如下面的代码示例)从网格坐标计算
leftDrawStart
和rightStartDraw
的屏幕坐标:
transform(spritePos) {
let invDet = 1.0 / (CAMERA.dir.x * PLAYER.dir.y - PLAYER.dir.x * CAMERA.dir.y);
CAMERA.transformX = invDet * (PLAYER.dir.y * spritePos.x - PLAYER.dir.x * spritePos.y);
CAMERA.transformDepth = invDet * (-CAMERA.dir.y * spritePos.x + CAMERA.dir.x * spritePos.y);
}
- 我区分计算出的绝对值
drawStartX
和drawEndX
,以及它们的调整,以便它们适合屏幕边界或在它们完全离开屏幕时从函数返回 - 最后,甚至不需要贴花的感知宽度,因为纹理位置可以通过使用当前绘图之间的差异比率来计算
stripe
- 绝对绘图开始和绝对绘图结束的差异 - 绝对绘图开始:
let texX = (((stripe - drawStartX_abs) / (drawEndX_abs - drawStartX_abs)) * imageData.width) | 0;
与在光线投射步骤中结合贴花投射的方法相比,该方法完全准确且速度要快得多。