游戏人生

不积跬步,无以至千里;不积小流,无以成江海。

My Links

Blog Stats

News

引擎设计跟踪(九.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的展开是不需要的, 直接用任意一个通道就可以.

 

posted on 2015-12-25 17:16 crazii 阅读(...) 评论(...) 编辑 收藏