(从深度缓冲里重建位置信息)
首先我必须让大家知道,我们现在讨论的是什么问题?我们研究的是延时渲染中一个非常有用的应用:从一个深度值中还原前一帧渲染像素的3D位置信息(可以是在视图空间也可以是在世界空间的)。在实践中,其实并不复杂。对任意像素进行渲染的时候,你本来就知道它2D的位置 ,采样一个深度值中便可以得到它完整的3D位置信息。不过重建信息时候仍然很容易陷入困境中,虽然你有很多种方式可以达到目的,但是很多初学者并不是很擅长Debug自己的着色器。
方法一:把投影矩阵的z/w存起来,再和x / w和y / w进行组合运算,通过投影矩阵的逆的进行变换,最后除以w。以下为HLSL代码:
Depth pass: //顶点着色 output.vPositionCS = mul(input.vPositionOS, g_matWorldViewProj); output.vDepthCS.xy = output.vPositionCS.zw; //像素着色(输出 z/w) return input.vDepthCS.x / input.vDepthVS.y; 获取位置信息: // 这个方法在延时的像素着色器把深度信息转换到视图空间去 //vTexCoord 是全屏纹理的坐标,x=0是屏幕的左边,Y=0是屏幕的上面 float3 VSPositionFromDepth(float2 vTexCoord) { // 获取深度信息 float z = tex2D(DepthSampler, vTexCoord); // 从视口坐标中获取 x/w 和 y/w float x = vTexCoord.x * 2 - 1; float y = (1 - vTexCoord.y) * 2 - 1; float4 vProjectedPos = float4(x, y, z, 1.0f); // 通过转置的投影矩阵进行转换到视图空间 float4 vPositionVS = mul(vProjectedPos, g_matInvProjection); // Divide by w to get the view-space position return vPositionVS.xyz / vPositionVS.w; }
对许多人来说,这种是首选的方法,因为它使用的是硬件深度缓冲工作。它有些东西看起来很自然很简单:我们通过投影得到深度值,我们又通过逆投影矩阵得到的位置信息。如果我们无法访问硬件深度缓冲怎么办?如果你在PC和D3D9上开发,像采样一张纹理一样采样深度缓冲是不可能的,除非你通过驱动Hacks钩子的方法(译者:打破常规的作法,D3D是不允许的)。如果你使用的是XNA,所有的框架跨平台兼容PC和360完成这件事情是不可能的。在这样情况下,我们可以直接使用顶点和像素着色器渲染出一张深度缓冲。这是一个好的方法么?z / w是非线性的,而大多数精度需要非常接近近裁剪平面。
一种不同的方法是,将归一化的视图空间的Z作为我们的深度。因为它是视图空间的是线性的,这就意味着我们可以得到一致的精度分布,这也意味着我们不必操心使用投影或投影的逆去重建位置。相反,我们可以采取类似于CryTek的方法,把这个深度值与一条从摄像机指向远平面的射线相乘。HLSL代码如下:
// Shaders渲染线性的深度 void DepthVS( in float4 in_vPositionOS : POSITION, out float4 out_vPositionCS : POSITION, out float out_fDepthVS : TEXCOORD0 ) { // Figure out the position of the vertex in // view space and clip space float4x4 matWorldView = mul(g_matWorld, g_matView); float4 vPositionVS = mul(in_vPositionOS, matWorldView); out_vPositionCS = mul(vPositionVS, g_matProj); out_fDepthVS = vPositionVS.z; } float4 DepthPS(in float in_fDepthVS : TEXCOORD0) : COLOR0 { // 取反并除以与远平面的距离,这样深度值的范围就在【0,1】之间 // 这是在右手坐标系下做的,左手坐标系深度值不需要取反 float fDepth = -in_fDepthVS/g_fFarClip; return float4(fDepth, 1.0f, 1.0f, 1.0f); } // Shaders 延时渲染重建位置信息 // Vertex shader for rendering a full-screen quad void QuadVS ( in float3 in_vPositionOS : POSITION, in float3 in_vTexCoordAndCornerIndex : TEXCOORD0, out float4 out_vPositionCS : POSITION, out float2 out_vTexCoord : TEXCOORD0, out float3 out_vFrustumCornerVS : TEXCOORD1 ) { / /偏移0.5个像素的位置使得纹理的像素和屏幕的像素对齐。XNA和D3D9都必须这样做 out_vPositionCS.x = in_vPositionOS.x - (1.0f/g_vOcclusionTextureSize.x); out_vPositionCS.y = in_vPositionOS.y + (1.0f/g_vOcclusionTextureSize.y); out_vPositionCS.z = in_vPositionOS.z; out_vPositionCS.w = 1.0f; // 利用了纹理坐标和平截头体的角在视图空间的位置。 // out_vTexCoord = in_vTexCoordAndCornerIndex.xy; out_vFrustumCornerVS = g_vFrustumCornersVS[in_vTexCoordAndCornerIndex.z]; } // PS重建位置信息 float3 VSPositionFromDepth(float2 vTexCoord, float3 vFrustumRayVS) { float fPixelDepth = tex2D(DepthSampler, vTexCoord).r; return fPixelDepth * vFrustumRayVS; }
就像你看到的一样,使用线性的深度信息重建出来的位置相当的不错。我们只需要使用一个简单的乘法,就替代了4次的矩阵运算 和一次与投影的除法运算。如果你好奇得到我们所使用平截头体的角位置的方法,这个网站有介绍。
http://www.lighthouse3d.com/opengl/viewfrustum/index.php?defvf
如果你正在使用的是XNA,将会更方便,有一个super-convient BoundingFrustum类,里面可以直接得到。我获取位置的方法类似于(XNA):
Matrix viewProjMatrix = viewMatrix * projMatrix; BoundingFrustum frustum = new BoundingFrustum(viewProjMatrix); frustum.GetCorners(frustumCornersWS); Vector3.Transform(frustumCornersWS, ref viewMatrix, frustumCornersVS); for (int i = 0; i < 4; i++) farFrustumCornersVS[i] = frustumCornersVS[i + 4];
数组farFrustumCornersVS作为Shader Constants传给顶点着色器。那么你在正方形顶点上需要有一个index来确定那个顶点属于哪个角。另一种方法你可以把角的位置信息直接存在顶点纹理坐标里面。