跳转至

刚接触Unity没多久,对Unity的ShaderLab一直很感兴趣,感觉它可以快速地实现各种各样的显示效果,很有意思。嘛,作为一个门都还没入的人,我就来搞一搞深度图和边缘检测吧。

小地图设置

因为我只是做了一个小雏形,所以我不打算详细地讲如何去在场景上画小地图,大致上说我做了以下一些事情:

  1. 获取场景的 bounding box,这个在设置相机的参数和位置时有用
  2. 把小地图相机配置成正交投影,根据 bounding box 设置相机的近平面和远平面
  3. 为该相机增加一个人物目标,目标会显示在地图的中心
  4. 每次更新相机的位置,根据目标的位置,还有场景的最大 y 值

具体的配置可以参考后面给出的代码。

获取深度图

depthTextureMode 来获取深度图

相机自己可以保存DepthBuffer或者一个DepthNormalBuffer(可用来做边缘检测),只需要设置

Camera.depthTextureMode = DepthTextureMode.DepthNormals;

然后在Shader里面引用

sampler2D _CameraDepthNormalsTexture;

就可以了,具体的做法可以参考我后面给出的代码。关于在Z-Buffer里面保存的深度值跟真实世界的深度的关系可以参考这两篇文章: Learning to Love your Z-buffer,Linearize depth。另外 Unity 也提供了一些函数来计算深度: Linear01Depth, LinearEyeDepth 等。

这不是我这里讨论的重点,我想说的是,本来我的相机设置为正交投影,深度应该是线性的,但我测试出来却不是线性。然后我用上面链接介绍的方法来计算真实世界的深度,也一直都不正确,以至于一直计算不出真实的线性深度,不知道是Unity的Z_Buffer的问题还是什么,那位朋友知道的请教教我。当然,如果不需要真实的深度值,单单是比较深度的大小之类的,用上面的方法就足够了,而且很简单。但是对于我这里来说,我想要把真实深度映射为颜色值,需要获得真实的线性的深度值(虽然也是[0, 1]),我只好用另外一种用 RenderWithShader 方法了。

RenderWithShader 来获取深度图

这种方法其实就是用Unity Reference里面的一个例子:Rendering with Replaced Shaders。需要理解的是,RenderWithShader会把场景中的相应的Mesh画一遍。

创建一个 Shader :

Shader "Custom/DepthByReplaceShader" 
{
SubShader 
{
    Tags { "RenderType"="Opaque" }
    Pass {
        Fog { Mode Off }
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"

        struct v2f {
            float4 pos : SV_POSITION;
            float2 depth : TEXCOORD0;
        };

        v2f vert (appdata_base v) {
            v2f o;
            o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
            UNITY_TRANSFER_DEPTH(o.depth);
            return o;
        }

        float4 frag(v2f i) : COLOR {
            //UNITY_OUTPUT_DEPTH(i.depth);
            float d = i.depth.x/i.depth.y;
            return float4(d, d, d, 1);
        }
        ENDCG
    }
}
}

为你的小地图相机(没有的话创建之)添加一个脚本,把相机配置成正交投影等,并且在 Update() 里面使用这个 Shader 来渲染场景:

camera.targetTexture = depthTexture;
camera.RenderWithShader(depthShader, "");

渲染的结果就会保存在 depthTexture里面,很简单吧。

把深度映射成颜色

要完成这个工作,首先需要一张颜色图,这张图可以用 Matlab 很简单地生成,例如我用的是 Matlab 里面的 jet 图:

把这张图放到项目目录 Assets\Resources 里面,就可以在程序中读取:

colorMap = Resources.Load<Texture2D>("colormap");

需要注意的是,这张图片的 Wrap Mode 应该是 Clamp,防止在两边缘的颜色值之间进行插值。

之后就需要使用 OnRenderImageGraphics.Blit 函数,函数的原型为:

void OnRenderImage(RenderTexture src, RenderTexture dst);
static void Blit(Texture source, RenderTexture dest, Material mat, int pass = -1);

这个函数的 src 是相机渲染的结果,dst 是处理后传回给相机的结果,因此这个函数通常是用来在相机渲染完成后做图片的一些效果,例如我们这里的对深度做颜色映射,还有边缘检测。做法就是在OnRenderImage中调用Graphics.Blit,传入特定的Material

depthEdgeMaterial.SetTexture("_DepthTex", src);
Graphics.Blit(src, dst, depthEdgeMaterial);
return;

需要注意的是,Graphics.Blit实际上做了这样一件事情:在相机前面画一个跟屏幕大小一样的平面,把src作为这个平面的_MainTex传进Shader中,然后把结果放到dst里面,而不是把实际场景中的Mesh重新画一遍。

对颜色映射其实就是把深度 [0, 1] 看成图片的 uv,因为我想距离相机近的为红色,所以我对深度取了反:

half4 color = tex2D(_ColorMap, float2(saturate(1-depth), 0.5));

边缘检测

边缘检测需要用到了相机自己的 _CameraDepthNormalsTexture,主要是用 Normal 的值,深度还是用之前计算出来的。在 _CameraDepthNormalsTexture 的每个像素 (x, y, z, w) 中,(x, y) 是法向,(z, w)是深度,法向是用了一种方法来存放的,有兴趣可以自己搜索。

代码是参考了 Unity 自带的 Image Effect 里面的边缘检测,需要做的事情就是,比较当前像素的法向深度和邻近像素的差别,足够大我们就认为存在边缘:

inline half CheckSame (half2 centerNormal, half2 sampleNormal, float centerDepth, float sampleDepth)
{
    // difference in normals
    // do not bother decoding normals - there's no need here
    half2 diff = abs(centerNormal - sampleNormal);
    half isSameNormal = (diff.x + diff.y) < 0.5;

    // difference in depth
    float zdiff = abs(centerDepth-sampleDepth);
    // scale the required threshold by the distance
    half isSameDepth = (zdiff < 0.09 * centerDepth) || (centerDepth < 0.1);

    // return:
    // 1 - if normals and depth are similar enough
    // 0 - otherwise
    return isSameNormal * isSameDepth;
}

完整的 Shader 如下:

Shader "Custom/DepthColorEdge" {

Properties 
{
    _DepthTex ("Depth Tex", 2D) = "white" {}
    _ColorMap ("Color Map", 2D) = "white" {}
}
    SubShader 
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            sampler2D _CameraDepthNormalsTexture;
            sampler2D _DepthTex;
            uniform float4 _DepthTex_TexelSize;
            sampler2D _ColorMap;
            float _ZNear;
            float _ZFar;

            struct v2f 
            {
                float4 pos : SV_POSITION;
                float2 uv[3] : TEXCOORD0;
            };

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
                o.uv[0] = MultiplyUV( UNITY_MATRIX_TEXTURE0, v.texcoord );
                o.uv[1] = o.uv[0] + float2(-_DepthTex_TexelSize.x, -_DepthTex_TexelSize.y);
                o.uv[2] = o.uv[0] + float2(+_DepthTex_TexelSize.x, -_DepthTex_TexelSize.y);
                return o;
            }


            inline half CheckSame (half2 centerNormal, half2 sampleNormal, float centerDepth, float sampleDepth)
            {
                // difference in normals
                // do not bother decoding normals - there's no need here
                half2 diff = abs(centerNormal - sampleNormal);
                half isSameNormal = (diff.x + diff.y) < 0.5;

                // difference in depth
                float zdiff = abs(centerDepth-sampleDepth);
                // scale the required threshold by the distance
                half isSameDepth = (zdiff < 0.09 * centerDepth) || (centerDepth < 0.1);

                // return:
                // 1 - if normals and depth are similar enough
                // 0 - otherwise
                return isSameNormal * isSameDepth;
            }

            half4 frag(v2f i) : COLOR 
            {
                // get color based on depth
                float depth = tex2D (_DepthTex, i.uv[0]).r;
                half4 color = tex2D(_ColorMap, float2(saturate(1-depth), 0.5));

                // detect normal diff
                half2 centerNormal = tex2D(_CameraDepthNormalsTexture, i.uv[0]).xy;
                half2 sampleNormal1 = tex2D (_CameraDepthNormalsTexture, i.uv[1]).xy;
                half2 sampleNormal2 = tex2D (_CameraDepthNormalsTexture, i.uv[2]).xy;
                float sampleDepth1 = tex2D (_DepthTex, i.uv[1]).r;
                float sampleDepth2 = tex2D (_DepthTex, i.uv[2]).r;
                color *= CheckSame(centerNormal, sampleNormal1, depth, sampleDepth1);
                color *= CheckSame(centerNormal, sampleNormal2, depth, sampleDepth2);

                return color;
            }
            ENDCG
        }

    }
    FallBack "Diffuse"
}

结果类似于这个:

混合真实世界图像

单单是深度的颜色图可能有点无趣,那么我们可以混合上真实场景的颜色图,只需要再建一个 Shader,传入前面的图像和相机的真实图像,在 OnRenderImage 中进行混合:

Shader "Custom/ColorMixDepth" {
    Properties {
        _MainTex ("Base (RGBA)", 2D) = "white" {}
        _DepthTex ("Depth (RGBA)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Lambert

        sampler2D _MainTex;
        sampler2D _DepthTex;

        struct Input {
            float2 uv_MainTex;
            float2 uv_DepthTex;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            half4 c = tex2D (_MainTex, IN.uv_MainTex);
            half4 d = tex2D (_DepthTex, IN.uv_DepthTex);
            //d = d.x == 1? 0 : d;
            o.Albedo = c.rgb*0.1 + d.rgb*0.9;
            o.Alpha = 1;
        }
        ENDCG
    } 
    FallBack "Diffuse"
}

void OnRenderImage(RenderTexture src, RenderTexture dst)
{
    // if now rendering depth map
    if (isRenderDepth)
    {
        depthEdgeMaterial.SetTexture("_DepthTex", src);
        if(isUseColorMap)
            Graphics.Blit(src, dst, depthEdgeMaterial);
        else
            Graphics.Blit(src, dst);
        return;
    }
    // else rendering real color scene, mix the real color with depth map
    else
    {
        mixMaterial.SetTexture("_MainTex", src);
        mixMaterial.SetTexture("_DepthTex", depthTexture);
        Graphics.Blit(src, dst, mixMaterial);
        ReleaseTexture();
    }
}
上面的代码就是完成这个工作,需要理解的是,我们在调用 RenderWithShader 的时候,OnRenderImage 也会被调用,也就是这个函数被调用了两次,而两次调用需要完成的功能是不同的,所以我这里用一个变量来指示当前的渲染状态是做深度图还是混合。

完整的代码

代码文件有点多,就放到这里了depth-minimap

原文地址:https://wiki.disenone.site

本篇文章受 CC BY-NC-SA 4.0 协议保护,转载请注明出处。