Voxel Space 体素空间引擎解析 (2017)
- #体素
- #渲染引擎
- #游戏开发
- #Hacker News
- #s-macke.github.io
Voxel Space 体素空间引擎 Web 演示
历史回顾
让我们回到1992年。当时的CPU比现在慢1000倍,GPU加速尚未普及或价格高昂。3D游戏完全由CPU计算,渲染引擎仅以单一颜色绘制填充多边形。
就在那一年,NovaLogic发布了游戏《Comanche》。当时的画面令人叹为观止,在我看来领先时代3年。你可以在山脉和山谷上看到更多细节,比如纹理,并且首次出现了平滑的着色甚至阴影。当然,画面是像素化的,但那个年代的所有游戏都是像素化的。
渲染算法
《Comanche》使用了一种称为Voxel Space的技术,它基于与光线投射类似的思想。因此Voxel Space引擎是一个2.5D引擎,它不具备常规3D引擎的所有自由级别。
高度图和颜色图
表示地形的最简单方式是通过高度图和颜色图。对于游戏《Comanche》,使用了10241024的单字节高度图和10241024的单字节颜色图,你可以在本网站下载。这些地图是周期性的:这种地图将地形限制为“地图上每个位置只有一个高度”——无法表示建筑物或树木等复杂几何体。然而,颜色图的一个巨大优势是它已经包含了着色和阴影。Voxel Space引擎直接使用颜色,无需在渲染过程中计算光照。
基本算法
对于3D引擎来说,渲染算法出奇地简单。Voxel Space引擎对高度图和颜色图进行光栅化,并绘制垂直线。下图演示了这种技术。
- 清屏。为了保证遮挡正确,从后往前渲染,这称为画家算法。
- 确定地图上对应于观察者相同光学距离的线。考虑视场和透视投影(物体越远越小)。
- 对线进行光栅化,使其匹配屏幕的列数。
- 从2D地图中获取线段对应的高度和颜色。
- 对高度坐标执行透视投影。
- 使用从透视投影获得的高度,以相应颜色绘制垂直线。
核心算法的最简单形式只有几行代码(Python语法):
def Render(p, height, horizon, scale_height, distance, screen_width, screen_height):
# 从后往前绘制(从高z坐标到低z坐标)
for z in range(distance, 1, -1):
# 在地图上找到对应的线。此计算对应90°视场
pleft = Point(-z + p.x, -z + p.y)
pright = Point( z + p.x, -z + p.y)
# 分割线段
dx = (pright.x - pleft.x) / screen_width
# 光栅化线段并为每个分段绘制垂直线
for i in range(0, screen_width):
height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height + horizon
DrawVerticalLine(i, height_on_screen, screen_height, colormap[pleft.x, pleft.y])
pleft.x += dx
# 使用相机参数调用渲染函数:位置、高度、地平线位置、高度缩放因子、最大距离、屏幕宽度和高度
Render(Point(0, 0), 50, 120, 120, 300, 800, 600)
添加旋转
使用上述算法我们只能朝北看。不同的角度需要多几行代码来旋转坐标。
def Render(p, phi, height, horizon, scale_height, distance, screen_width, screen_height):
# 预计算视角参数
sinphi = math.sin(phi)
cosphi = math.cos(phi)
# 从后往前绘制(从高z坐标到低z坐标)
for z in range(distance, 1, -1):
# 在地图上找到对应的线。此计算对应90°视场
pleft = Point( (-cosphi*z - sinphi*z) + p.x, ( sinphi*z - cosphi*z) + p.y)
pright = Point( ( cosphi*z - sinphi*z) + p.x, (-sinphi*z - cosphi*z) + p.y)
# 分割线段
dx = (pright.x - pleft.x) / screen_width
dy = (pright.y - pleft.y) / screen_width
# 光栅化线段并为每个分段绘制垂直线
for i in range(0, screen_width):
height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height + horizon
DrawVerticalLine(i, height_on_screen, screen_height, colormap[pleft.x, pleft.y])
pleft.x += dx
pleft.y += dy
# 使用相机参数调用渲染函数:位置、视角、高度、地平线位置、高度缩放因子、最大距离、屏幕宽度和高度
Render(Point(0, 0), 0, 50, 120, 120, 300, 800, 600)
性能提升
当然有很多技巧可以实现更高的性能。我们可以从前往后绘制,而不是从后往前。这样做的好处是,由于遮挡,我们不必每次都将线绘制到屏幕底部。然而,为了保证遮挡,我们需要一个额外的y-buffer。对于每一列,存储最高的y位置。因为我们是从前向后绘制,下一条线的可见部分只能大于之前绘制的最高线。
细节层次。近处渲染更多细节,远处渲染更少细节。
def Render(p, phi, height, horizon, scale_height, distance, screen_width, screen_height):
# 预计算视角参数
sinphi = math.sin(phi)
cosphi = math.cos(phi)
# 初始化可见性数组。屏幕上每列的Y位置
ybuffer = np.zeros(screen_width)
for i in range(0, screen_width):
ybuffer[i] = screen_height
# 从前往后绘制(从低z坐标到高z坐标)
dz = 1.
z = 1.
while z < distance:
# 在地图上找到对应的线。此计算对应90°视场
pleft = Point( (-cosphi*z - sinphi*z) + p.x, ( sinphi*z - cosphi*z) + p.y)
pright = Point( ( cosphi*z - sinphi*z) + p.x, (-sinphi*z - cosphi*z) + p.y)
# 分割线段
dx = (pright.x - pleft.x) / screen_width
dy = (pright.y - pleft.y) / screen_width
# 光栅化线段并为每个分段绘制垂直线
for i in range(0, screen_width):
height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height + horizon
DrawVerticalLine(i, height_on_screen, ybuffer[i], colormap[pleft.x, pleft.y])
if height_on_screen < ybuffer[i]:
ybuffer[i] = height_on_screen
pleft.x += dx
pleft.y += dy
# 移到下一行,远处时增加步长
z += dz
dz += 0.2
# 使用相机参数调用渲染函数:位置、视角、高度、地平线位置、高度缩放因子、最大距离、屏幕宽度和高度
Render(Point(0, 0), 0, 50, 120, 120, 300, 800, 600)
链接
- Web项目演示页面
- Voxel地形引擎介绍
- 个人网站
- 地图(颜色、高度)
许可证
本仓库的软件部分采用MIT许可证。请阅读许可证文件以获取更多信息。请注意,Voxel Space技术在某些国家可能仍受专利保护。颜色和高度图是从游戏《Comanche》中逆向工程得到的,因此不包含在许可证范围内。
本站开源。改进此页面。
评论