在写这篇文章时,我特意去Wiki上搜了一下ScreenSpace, 可能是由于太过直白的缘故,并没有找到标准的定义。
不过他的定义是显而易见的,屏幕空间的所有的信息都是与屏幕上的像素有关的,而不是和场景中的几何有关的信息都叫屏幕空间,这一点其实很像是Pixel和Fragment的区别。pixel是定义在屏幕空间上的,而Fragment是定义在三维空间上的。
举个最简单的例子,我们从相机原点射出一条射线,然后穿过两个不透明物体。这两个交点,在进行光栅化时就是2个Fragment, 但是最终渲染到屏幕上最终只会有一个Fragment被采用,而屏幕空间就是最终被采用的Fragment的集合。
显然深度纹理属于屏幕空间。
以前,我一直觉得深度缓存只能用来做ZTest。然而大神们分分钟教我做人。
在光线追踪算法下,我们可以这样生成深度图,将深度图放在相机的近平面。
然后相机原点对深度图上所有像素都发出射线和场景中物体相交,并把首次相交的物体的Fragment,在相机空间下的Z坐标写入深度图。
下面这张图片更详细的展示了光线追踪的细节。
在光栅化算法下,生成的深度图结果和光线追踪一模一样,但显然光线追踪的算法更清晰并易于理解。
从光线追踪算法来看,给我们一个指定分辨率的深度图和相机近平面距离,我们就可以完全还原出来生成深度图用到的所有射线。
我们还知道,深度图上每个像素上的深度值,都是从相机原点到这个像素发出的射线与场景物体相交的点产生的。
反过来说就是,影响像素A的深度值Z_A所在的Fragment_A(3D坐标下的点),一定在从相机原点出发到像素A的射线Ray_A上。
我们来看看,有了这些信息,我们都能求出哪些额外的信息。
RayA*Z_A = Fragment_A在相机空间下的位置ViewPosition_A
mul(inverse(V), ViewPosition_A) = Fragment_A在世界坐标下的位置WorldPosition_A
我们甚至可以通过mul(inverse(M), WorldPosition_A)将Fragment_A变换到任意模型ModelPosition_A。
来看看我们能利用这些还原的信息做什么吧。
深度贴花:
在很久以前, 我曾经执迷于贴花,最终学会了使用Mesh来贴花,但是利用深度信息,我们可以更便捷的做到。
我们假设要贴的花是一张平面贴纸,我们有这个纸片的MVP矩阵。
根据上面的推导,我们可以将深度图上的任意一个像素的Fragment转换到贴纸的模型空间中来。
然后我们根据这个Fragment在贴纸的模型空间中的坐标X,计算出需要采样的uv。
假设整个贴花的大小是(x,z) = (-0.5,-0.5)~(0.5,0.5), 那么uv=X.xy + (0.5,0.5)。当然需要clip掉uv.xy < (0,0)和uv.xy >(1,1)的坐标。
最后根据计算出的uv直接采样纹理,整个贴花就完成了。
全局雾效:
有了深度图,我们可以重新计算出每一个像素的Fragment在相机空间下的位置ViewPosition。
记受雾影响的最大和最小距离为 d_{\tiny min}
和 d_{\tiny max}
。
假设雾的浓度和高度有关的,一种简单的雾效计算公式为。
f = \frac{d_{\tiny max} - ViewPosition.y}{d_{\tiny max}-d_{\tiny min}}
float3 afterFog = f fogColor + (1 – f) oriColor;
SSAO:
在SSAO的实现里, 我们甚至都不需要去重新计算坐标信息。
仅通过采样一个像素周围的平均深度,就可以来近似计算一个遮蔽关系。
虽然深度图可以计算出来很多信息,但是还是有很多信息是计算不出来,比如法线信息。
在我们写光栅化时,都会有这样一个经历,在有多光源的情况下,我们的代码和下面代码很相似。
for l in lights do
for m in meshes do
render(l,m)
end
end
没错,这就是传说中的ForwardBase Lighting。在上述代码中,每多增加一个光源,我们就需要把所有Mesh重新渲染一遍,如果光源非常多的话,这种开销几乎是不可承受的。
聪明的大神们发现了一个现象,还是光线追踪的思路。当我从相机原点到成像纹理的像素发出射线时,只有第一个与射线相交的场景中的Fragment才会被采用,后面的Fragment在后来做ZTest时都会被丢弃, 即然这样,我只对屏幕空间中的Fragment计算光照就可以了。
这种思路在有复杂遮挡关系的场景中,优化效果是惊人的。那具体应该怎么做呢?
一般是通过两个Pass来做的。
在第一趟Pass中,Shader会计算所有Fragment的采样颜色和法线。并将颜色和法线分别写入两张Texture中。由于在Fragment Shader在输出结果时,会做ZTest, 因此只有离相机最近的Fragment才有资格写入纹理。这一步的目的就是为了减少计算光照的Fragment。
在第二趟Pass中,Shader会根据纹理中的每一个像素所包含颜色和法线信息来对所有光源进行计算。
整个计算流程大概如下:
--First Pass
for f in fragments do
if depth_test(f) do
write_diffuse(f, texture2D(f.uv, tex))
write_normal(f, f.normal)
end
end
--Second Pass
for f in fragments_in_screen_space do
c = float3(0)
for l in lights do
c += render(l, f)
end
write(f, c)
end
这就是大鼎鼎的Deferred Lighting。
你以为这就是大神极限了么,大神会立马再教你做人。
即然光照可以通过两趟Pass的方式来优化,那我可不可以用来加速光线追踪呢?
答案是肯定的, 由于屏幕空间上的信息量相比整个场景来讲少之又少,在计算光线相交时,可以更快的判定。当然同样由于信息量过少, 所以会有这样或那样的瑕疵,不过相比效果来讲,瑕疵反而不是微不足道的。
没错,这就是传说中的SSR(ScreenSpaceReflection)。其实就是在屏幕空间下进行RayTracing, 是不是叫ScreenSpaceRayTracing更好一些:D。