引擎设计跟踪(九.14.3) deferred shading 准备
目前做的一些准备工作
1.depth prepass for forward shading.
做depth prepass的原因是为了完善渲染流程, 虽然架构上支持多个pass, 但实际上从来没有测过. 等流程完善的差不多了, 理论上只需要修改xml配置和shader, 不需要修改C++代码就可以实现自定义渲染流程.
2.linear depth
默认情况下, 顶点经过mvp变换, 再单位化到NDC以后, 深度是非线性的, 其函数是一个(-) 1/(a + bx)的曲线, 导致靠近near clip plane的z值精度高, 远处精度低. 使用linear depth 以后深度变化都是均匀的.
linear depth通常使用view space的z 再除以max view distance即far clip distance得到单位化的z,这个z值可以直接写入zbuffer, 用于depth test, 不过需要所有的shader都输出linear depth才能正常使用, 除了那些不需要深度写和深度测试的对象, 可以不用修改.
目前已经将forward shading里面所有的depth都改成linear depth, 来做深度测试.
另外需要注意的是如果是右手坐标系, 那么view 向量朝向-z轴, 所以view space的z都是负值, 如果按负值比较的话, depth test是反的. 所以shader里面需要取反.
还有, 这个linear depth也可以用于线性插值, 所以可以在vertex shader里面计算出结果, 在pixel shader里面输出到深度.
还有一个问题就是depth bias会出问题, 因为硬件中使用bias的公式是基于非线性depth来的. 解决方法: 因为depth在shader里面输出, 所以可以直接在shader里面直接修改depth的偏移.
3.如果不用linear depth, 需要将NDC坐标unproject到world space, 即乘以 invViewProjection, 一次矩阵运算来得到world space.
使用linear depth的话, 单位化的深度z*far clip distance就得到目标点距离视点的距离. 使用linear depth另外的一个好处就是, 可以使用一个ray向量, 乘以深度就可以得到坐标.
通常这个操作习惯于在view space计算, 即
viewPos = [view space origin +] view space ray * viewdepth
viewPos = ray * viewDistance (后面有备注)
由于view space的origin就是(0,0,0) 所以只需要一个ray就可以.
但是由于历史原因, Blade的forward shading使用的全部是世界空间法线和世界空间位置, 为了使原有shader不做太大的修改, 所以想在在deferred shading时不在view space, 而再世界空间计算. 即将view space ray变换到世界空间, 即
world pos = eyePos + worldRay * viewdepth viewDistance
实际上不需要将view space ray变换到world space. 因为在世界空间计算光照, 在vertex shader里面计算world pos, 那么
worldRay = normalize(world pos - view pos);
只需要在vertexshader里多传入一个uniform, 把相机的视点坐标传过来即可, 而后在pixel shader里面, 传入同样的uniform, 还原世界坐标:
float3 worldRay : TEXCOORD1 uniform float4 eye_position : EYE_POS //eye pos in world space ... //expand INTZ float depth = dot(tex2D(depthBuffer, depthUV).arg, float3(0.996093809371817670572857294849, 0.0038909914428586627756752238080039, 1.5199185323666651467481343000015e-5)); float3 worldPos = eye_position.xyz + worldRay * depth;
Directional light的优化
如果方向光很多, 如果对每一个directional light都单独绘制一个quad, 做一个pass, 那么IO量太大. 所以目前使用的方法是一次IO, 迭代所有方向光.
uniform int direcitonal_count; uniform float4 light_drections[MAX_DIR_LIGHT_COUNT]; uniform float4 directional_diffuses[MAX_DIR_LIGHT_COUNT]; uniform float4 directional_speculars[MAX_DIR_LIGHT_COUNT]; for(int i = 0; i < directional_count; ++i) { color += shading(light_drections[i], diretional_diffuses[i], directioanl_speculars[i], normal, depth); }
这样对于多个方向光, 只需要绘制一次quad, 采样一次GBuffer法线, 然后迭代多个光源.
另外, 因为方向光比较特殊, 他的光照方向不需要world pos, 而计算half vector需要的viewdir已经由vertex shader计算好, 并完成插值, 实际上不需要world position, 所以也不需要采样depth buffer:
for(int i = 0; i < light_directional_count; ++i) { float3 light_dir = light_directions[i].xyz; //direcitonal lights doesn't need world position, thus doesn't need depth sampling, //the depthBuffer sampling will be optimized by compiler. float3 half_vec = normalize(light_dir + (-worldRay)); ... // calculate lighting }
这样就能完成和forward shading兼容的, 使用世界空间计算光照的defered shading.
目前deferred shading流程正在整理, deferred的方向光已经可以使用, 最终效果也跟forward shading一样. 因为编辑器里面还没有动态添加光源的功能, 所以后面会慢慢加上point和spot的支持.
问题: forward shading现在vertex shader里面既要计算view space pos (linear depth的需要), 又要计算world space pos(计算光照), 这样矩阵乘法太多. 还是把forward sahding的光照改在view space计算光照比较好, 这样deferred shading也要改在view space.
更新: linear viewdepth = mul(pos, worldViewMatrix).z / farclipDist
实际上shader里面只需要 worldViewMatrix的第三列, 所以shader里面只要传入第三列, 那么
viewDepth = dot(pos, worldViewMatrixCol3) / faclipDist, 而且worldViewMatrixCol3可以在CPU端预乘1/farClipDist, 这样计算view space normalized depth只需要一条指令, 这样的话没有多余的计算, 就不需要考虑在view space计算光照了.
这是在CPU端计算view depth vector的方式: shader 里面一条dot指令就搞定了, 如果使用"mul(pos, worldViewMatrix).z / farclipDist"的话, 优化开到O3指令数为3, 目测为transpose+dot+除法.
////////////////////////////////////////////////////////////////////////// //vector to calculate view space normalized depth //this is an optimization for shader, use dot(v, viewdepthvector) instead of mul(v,world_view_matrix).z / far_clip_distance class ViewDepthVectorUpdater : public InstanceVariableUpdater { public: ViewDepthVectorUpdater(WorldViewMatrixUpdater* worldViewUpdater) :InstanceVariableUpdater(SCT_FLOAT4, 1) ,mWorldViewUpdater(worldViewUpdater) ,mWorldViewMatrixCol3(Vector4::ZERO) { } /** @brief */ virtual const void* updateData() const { const ICamera* camera = IShaderVariableSource::getSingleton().getCamera(); if( camera == NULL ) return &Vector4::ZERO; const Matrix44& worldViewMat = *(const Matrix44*)mWorldViewUpdater->getVariable()->getData(); mWorldViewMatrixCol3[0] = worldViewMat[0][2]; mWorldViewMatrixCol3[1] = worldViewMat[1][2]; mWorldViewMatrixCol3[2] = worldViewMat[2][2]; mWorldViewMatrixCol3[3] = worldViewMat[3][2]; //note: since we're using Right Hand axis, the view direction points at -Z, //view space position's z values are negative. //use this negative value to generate a positive z value into z buffer mWorldViewMatrixCol3 *= -1.0f / camera->getFarClipDistance(); return &mWorldViewMatrixCol3; } protected: WorldViewMatrixUpdater* mWorldViewUpdater; mutable Vector4 mWorldViewMatrixCol3; };
更新2:
如果在pixel shdaer里面输出depth, 那么意味着pipeline必须走完pixel shader才能做depth test, 所以early Z完全失效, 同时也无法真正避免overdraw, 因为所有的pixel必须处理完成才能做depth dest, 这个时候depth pre pass已经毫无意义.
所以尝试在vertex shader里面直接输出深度, 即POSITION.z.
然而结果在某些情况下(靠近near plane的一些mesh, 地形倒没发现)会出现问题.
改用了这个方法: https://www.mvps.org/directx/articles/linear_z/linearz.htm 结果仍然一样(其实思路差不多, 不同的是Blade里面 view.z = 0 对应depth的0, 而文中view.z = znear对应depth的0.
最后发现, 因为虽然单个顶点输出了线性深度, 但相邻顶点的插值会有问题:
http://www.yosoygames.com.ar/wp/2014/01/linear-depth-buffer-my-ass/
文章中提到, 如果一定要在PixelShader写深度的话, 也建议使用高精度的 logarithmic z-buffer, 不过对于defered shading, 需要解出原始深度, 所以需要逆向求解, 目测可以实现, 需要使用pow.
等研究一下有没有更好的办法再继续.
更新3:
由于无法在vertex shader里面输出linear depth, 而又不想在pixel shader输出深度(效率上的问题), 所以目前已经改回常规的depth, 并在deferred shading阶段把zbuffer的z值转换为view space的z, 这个根据projection matrix反向计算就可以得出, 需要注意的是采样zbuffer得出的z是[0,1], 符合d3d的NDC范围, 但是OGL是[-1,1]. 这个转换参数可以在CPU端计算, 不影响shader.
具体的计算这里有个链接, 跟我的思路一样: http://www.derschmale.com/2014/01/26/reconstructing-positions-from-the-depth-buffer/
RAWZ v.s. INTZ
根据G80的doc, INTZ直接采样就得到深度, 只有RAWZ才需要从新构建深度. (http://developer.download.nvidia.com/GPU_Programming_Guide/GPU_Programming_Guide_G80.pdf)
所以前面的INTZ的展开是不需要的, 直接用任意一个通道就可以.