每次都能让人头大的 Shader -- 从一次简单的功能说起

  最近有个功能, 要渲染从主相机视角看到的另一个相机的可视范围和不可见范围, 大概如下图 : 

每次都能让人头大的 Shader -- 从一次简单的功能说起

  简单来说就是主相机视野和观察者相机视野重合的地方, 能标记出观察者相机的可见和不可见, 实现原理就跟 ShadowMap 一样, 就是有关深度图, 世界坐标转换之类的, 每次有此类的功能都会很悲催, 虽然它的逻辑很简单, 可是用Unity3D做起来很麻烦...

  原理 : 在观察者相机中获取深度贴图, 储存在某张 RenderTexture 中,  然后在主相机中每个片元都获取对应的世界坐标位置, 将世界坐标转换到观察者相机viewPort中, 如果在观察者相机视野内, 就继续把视口坐标转换为UV坐标然后取出储存在 RenderTexture 中的对应深度, 把深度转换为实际深度值后做比较, 如果深度小于等于深度图的深度, 就是观察者可见 否则就是不可见了.

 

  先来看怎样获取深度图吧, 不管哪种渲染方式, 给相机添加获取深度图标记即可:

        _cam = GetComponent<Camera>();
        _cam.depthTextureMode |= DepthTextureMode.Depth;

  

  然后深度图要精度高点的, 单通道图即可:

        renderTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 0, RenderTextureFormat.RFloat);
        renderTexture.hideFlags = HideFlags.DontSave;

  

  之后就直接把相机的贴图经过后处理把深度图渲染出来即可 : 

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (_material && _cam)
        {
            Shader.SetGlobalFloat("_cameraNear", _cam.nearClipPlane);
            Shader.SetGlobalFloat("_cameraFar", _cam.farClipPlane);

            Graphics.Blit(source, renderTexture, _material);
        }
        Graphics.Blit(source, destination);
    }

  材质就是一个简单的获取深度的shader : 

    sampler2D _CameraDepthTexture;    
    uniform float _cameraFar;
    uniform float _cameraNear;
    
    float DistanceToLinearDepth(float d, float near, float far)
    {
        float z = (d - near) / (far - near);
        return z;
    }
    
    fixed4 frag(v2f i) : SV_Target
    {
        float depth  = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
        depth = LinearEyeDepth(depth);
        depth = DistanceToLinearDepth(depth, _cameraNear, _cameraFar);
        return float4(depth, depth, depth, 1);
    }
    

 

  这里有点奇怪吧, 为什么返回的不是深度贴图的深度, 也不是 Linear01Depth(depth) 的归一化深度, 而是自己计算出来的一个深度?

  这是因为缺少官方文档........其实看API里面有直接提供 RenderBuffer 给相机的方法 : 

        _cam.SetTargetBuffers(renderTexture.colorBuffer, renderTexture.depthBuffer);

  可是没有文档也没有例子啊, 鬼知道你渲染出来我怎么用啊, 还有 renderTexture.depthBuffer 到底怎样作为 Texture 传给 shader 啊... 我之前尝试过它们之间通过 IntPtr 作的转换操作, 都是失败...

  于是就只能用最稳妥的方法, 通过 Shader 渲染一张深度图出来吧, 然后通过 SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); 获取到的深度值, 应该是 NDC 坐标, 深度值的范围应该是[0, 1]之间吧(非线性), 如果在其它相机过程中获取实际深度的话, 就需要自己实现 LinearEyeDepth(float z) 这个方法:

inline float LinearEyeDepth( float z )
{
    return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

  而在不同平台, _ZBufferParams 里面的值是不一样的, 所以如果实现它的话会很麻烦, 并且经过我的测试, 结果是不对的......

double x, y;

OpenGL would be this:
x = (1.0 - m_FarClip / m_NearClip) / 2.0;
y = (1.0 + m_FarClip / m_NearClip) / 2.0;

D3D is this:
x = 1.0 - m_FarClip / m_NearClip;
y = m_FarClip / m_NearClip;

_ZBufferParams = float4(x, y, x/m_FarClip, y/m_FarClip);

  

 

  最后做出来的只是基本功能, 并没有对距离深度做样本估计, 所以会像硬阴影一样有锯齿边, 还有像没开各项异性的贴图一样有撕扯感, 这些都需要进行处理...

上一篇:Unity填坑之Video与RenderTexure


下一篇:图形裁切