在去年夏天Nvidia RTX图形卡出现之后,光线追踪已恢复了以前的流行。 在过去的几个月中,我的Twitter feed中充斥着无休止的图形比较流,其中包括启用和禁用RTX。
在欣赏了许多精美的图像之后,我想尝试将经典的前向渲染器与光线追踪器结合起来。
结果,我患上
了拒绝他人开发的综合症,因此我创建了自己的基于WebGL1的混合渲染引擎。 您可以
在此处使用Wolfenstein 3D的球体(由于光线跟踪而使用)进行演示级别渲染。
样机
我通过创建一个原型开始了这个项目,尝试
通过Metro Exodus的光线跟踪来重新创建
全局照明 。
第一个展示漫反射全局照明的原型(Diffuse GI)该原型基于前向渲染器,该渲染器渲染场景的整个几何形状。 用于光栅化几何体的着色器不仅可以计算直接照明,而且还可以使用光线跟踪器间接反射从非发光表面产生的光(漫反射GI),从渲染的几何体表面发出随机光线以进行累积。
在上面的图像中,您可以看到仅通过间接照明(光线从相机后面的墙反射)正确地照亮了所有球体。 光源本身被图像左侧的棕色墙壁覆盖。
德军总部3D
原型使用一个非常简单的场景。 它只有一个光源,并且仅渲染了几个球体和立方体。 因此,着色器中的光线跟踪代码非常简单。 测试光束与场景中所有多维数据集和球体的相交的相交检查蛮力循环仍然足够快,程序可以实时执行。
创建此原型后,我想通过向场景添加更多的几何图形和大量光源来做一些更复杂的事情。
更复杂的环境的问题在于,我仍然需要能够实时跟踪场景中的光线。 通常,将使用
边界体积层次结构 (BVH)结构来加快光线跟踪过程,但是我在WebGL1上创建此项目的决定不允许这样做:无法将16位数据加载到WebGL1中的纹理中,并且二进制操作不能在着色器中使用。 这使BVH在WebGL1着色器中的初步计算和应用变得复杂。
这就是为什么我决定为此使用Wolfenstein 3D演示级别的原因。 2013年,我在
Shadertoy中创建了一个
片段化的WebGL着色器 ,它不仅可以渲染类似Wolfenstein的关卡,还可以在程序上创建所有必要的纹理。 根据我在此着色器上的工作经验,我知道Wolfenstein基于网格的关卡设计也可以用作快速简便的加速结构,并且在此结构上的光线追踪将非常快。
下面是该演示的屏幕截图,在全屏模式下,您可以在此处播放它:
https :
//reindernijhoff.net/wolfrt 。
简短说明
该演示使用混合渲染引擎。 为了渲染框架中的所有多边形,它使用传统的栅格化方法,然后将结果与阴影,漫反射GI和由射线跟踪创建的反射合并。
暗影弥漫性感言主动渲染
Wolfenstein卡可以完全编码为64×64二维网格。 该演示中使用的地图基于Wolfenstein 3D
第1集的
第一级 。
在启动时,将创建通过主动渲染所需的所有几何图形。 根据地图数据生成墙的网格。 它还会创建地板和天花板平面,用于灯光,门和随机球体的单独网格。
用于墙壁和门的所有纹理都包装在一个纹理图集中,因此可以在一次绘制调用中绘制所有墙壁。
阴影和灯光
在用于前向渲染过程的着色器中计算直接照明。 每个片段可以由四个不同的光源照亮(最大)。 要知道哪些源可以影响着色器中的片段,请在演示开始时预先计算搜索纹理。 此搜索纹理的大小为64 x 128,并对地图网格中每个位置的4个最近光源的位置进行编码。
varying vec3 vWorldPos; varying vec3 vNormal; void main(void) { vec3 ro = vWorldPos; vec3 normal = normalize(vNormal); vec3 light = vec3(0); for (int i=0; i<LIGHTS_ENCODED_IN_MAP; i++) { light += sampleLight(i, ro, normal); }
为了获得每个片段和光源的柔和阴影,需要对光源中的随机位置进行采样。 使用着色器中的光线跟踪代码(请参见下面的“光线跟踪”部分),阴影光线会发射到采样点,以确定光源的可见性。
添加(辅助)反射后(请参见下面的“反射”部分),通过在“漫反射GI渲染目标”中进行搜索(请参见下文),将漫反射GI添加到片段的计算颜色中。
光线追踪
尽管用于漫反射GI的原型光线跟踪代码与抢先式着色器组合在一起,但在演示中,我还是决定将它们分开。
我通过使用另一个仅发出随机光线以收集漫反射GI的着色器对所有几何图形进行第二次渲染到单独的渲染目标(漫反射GI渲染目标)中来分离它们(请参见下面的“漫反射GI”部分)。 在此渲染目标中收集的光照将添加到在前向渲染通道中计算的直接光照。
通过将主动通道和漫反射GI分开,我们每个屏幕像素可以发射少于一个漫射GI光束。 这可以通过减小缓冲区比例(通过移动屏幕右上角选项中的滑块)来完成。
例如,如果“缓冲区比例”为0.5,则每四个屏幕像素仅发射一条光线。 这极大地提高了生产率。 使用屏幕右上角的相同UI,您还可以更改渲染目标(SPP)中每个像素的采样数和光束反射数。
发出光束
为了能够将光线发射到场景中,所有关卡几何必须具有着色器中的光线跟踪器可以使用的格式。 Wolfenstein层对64×64的网格进行了编码,因此将所有数据编码为单个64×64的纹理非常容易:
- 在纹理颜色的红色通道中,对位于地图网格的相应像元x,y中的所有对象进行了编码。 如果红色通道的值为零,则该单元格中没有对象,否则,它会被墙(值从1到64),门,光源或球体所占据,需要检查相交。
- 如果球体占据了水平网格单元,则使用绿色,蓝色和alpha通道对网格单元内球体的半径以及相对x和y坐标进行编码。
通过使用以下代码遍历纹理,可以在场景中发出光线:
bool worldHit(n vec3 ro,in vec3 rd,in float t_min, in float t_max, inout vec3 recPos, inout vec3 recNormal, inout vec3 recColor) { vec3 pos = floor(ro); vec3 ri = 1.0/rd; vec3 rs = sign(rd); vec3 dis = (pos-ro + 0.5 + rs*0.5) * ri; for( int i=0; i<MAXSTEPS; i++ ) { vec3 mm = step(dis.xyz, dis.zyx); dis += mm * rs * ri; pos += mm * rs; vec4 mapType = texture2D(_MapTexture, pos.xz * (1. / 64.)); if (isWall(mapType)) { ... return true; } } return false; }
在Shadertoy的
Wolfenstein着色器中可以找到类似的网格光线跟踪代码。
在计算出与墙壁或门的相交点(使用
带有平行四边形的相交测试 )之后,在用于通过主动渲染的相同纹理图集中进行搜索,即可得到反照率相交点。 球体的颜色是根据网格中其
x,y坐标和
颜色梯度函数根据程序确定的。
门要移动一些,因此要复杂一些。 为了使CPU中的场景表示(用于在前向渲染过程中渲染网格)与GPU中的场景表示(用于光线跟踪)相同,所有门均根据从摄像机到门的距离自动确定地移动。
弥漫性
散射全局照明(漫反射GI)是通过在着色器中发出光线来计算的,该着色器用于绘制“漫反射GI”渲染目标中的所有几何图形。 这些射线的方向取决于表面的法线,该法线是通过采样余弦加权的半球而确定的。
具有光束方向
rd和起点
ro ,可以使用以下周期来计算反射照明:
vec3 getBounceCol(in vec3 ro, in vec3 rd, in vec3 col) { vec3 emitted = vec3(0); vec3 recPos, recNormal, recColor; for (int i=0; i<MAX_RECURSION; i++) { if (worldHit(ro, rd, 0.001, 20., recPos, recNormal, recColor)) {
为了减少噪声,直接光采样被添加到环路中。 这与我在Shadertoy上的
Cornell Box着色器中使用的技术类似。
倒影
由于能够在着色器中使用光线跟踪场景,因此添加反射非常容易。 在我的演示中,通过使用相机的反射光束调用与
上面所示相同的
getBounceCol方法来添加反射:
#ifdef REFLECTION col = mix(col, getReflectionCol(ro, reflect(normalize(vWorldPos - _CamPos), normal), albedo), .15); #endif
在前向渲染过程中添加了反射,因此,一束反射射线将始终发出一束反射光束。
临时抗锯齿
由于主动渲染过程中的软阴影和漫反射GI逼近每个像素大约使用一个样本,因此最终结果非常嘈杂。 为了减少噪声量,基于Playdead的TAA使用了临时抗锯齿(TAA):
INSIDE中的时间重投影抗锯齿 。
重新投影
TAA背后的想法非常简单:TAA每帧计算一个子像素,然后将其值与前一帧的相关像素取平均值。
为了知道当前像素在前一帧中的位置,使用前一帧的模型-视图-投影矩阵重新投影片段位置。
放下样本并限制邻域
在某些情况下,从过去保存的样本是无效的,例如,当摄影机以几何形状关闭前一帧中当前帧的片段的方式移动时。 为了丢弃这样的无效样本,使用邻域限制。 我选择了最简单的限制类型:
vec3 history = texture2D(_History, uvOld ).rgb; for (float x = -1.; x <= 1.; x+=1.) { for (float y = -1.; y <= 1.; y+=1.) { vec3 n = texture2D(_New, vUV + vec2(x,y) / _Resolution).rgb; mx = max(n, mx); mn = min(n, mn); } } vec3 history_clamped = clamp(history, mn, mx);
我还尝试使用基于有界平行四边形的限制方法,但与我的解决方案没有太大区别。 这可能是由于演示场景中存在许多相同的深色且几乎没有移动物体而发生的。
相机震动
为了获得抗锯齿效果,由于使用了(伪)随机子像素偏移,因此每帧相机都会振动。 这是通过更改投影矩阵来实现的:
this._projectionMatrix[2 * 4 + 0] += (this.getHaltonSequence(frame % 51, 2) - .5) / renderWidth; this._projectionMatrix[2 * 4 + 1] += (this.getHaltonSequence(frame % 41, 3) - .5) / renderHeight;
噪音
噪声是用于计算漫反射GI和软阴影的算法的基础。 使用
良好的噪点会极大地影响图像质量,而不良的噪点会造成伪影或减慢图像收敛。
恐怕此演示中使用的白噪声不是很好。
在本演示中,使用良好的噪声可能是提高图像质量的最重要方面。 例如,您可以使用
blue noise 。
我根据黄金比率进行了噪音实验,但没有成功。 到目前为止,使用了臭名昭著的
没有戴夫·霍斯金斯
正弦的哈希 :
vec2 hash2() { vec3 p3 = fract(vec3(g_seed += 0.1) * HASHSCALE3); p3 += dot(p3, p3.yzx + 19.19); return fract((p3.xx+p3.yz)*p3.zy); }
降噪
即使启用了TAA,该演示仍会显示很多噪音。 渲染天花板特别困难,因为它只能通过间接照明进行照明。 天花板是一个大的平坦表面并填充有纯色的情况并不能简化这种情况:如果天花板具有纹理或几何细节,则噪音将变得不太明显。
我不想在演示的这一部分上花费很多时间,所以我尝试仅应用一种降噪滤波器:
Morgan McGuire和Kyle Witson的Median3x3 。 不幸的是,此滤镜无法与墙纹理的“像素艺术”图形配合使用:它会删除远处的所有细节,并对附近墙的像素的角进行倒圆角处理。
在另一个实验中,我将相同的滤镜应用于“漫反射GI渲染目标”。 尽管他稍微降低了噪音,但几乎同时不改变墙体纹理的细节,但我认为这种改进不值得花费额外的毫秒时间。
演示版
您可以在此处播放演示 。