找回密码
 立即注册
查看: 445|回复: 0

unity深度及应用

[复制链接]
发表于 2022-7-11 14:11 | 显示全部楼层 |阅读模式
获得深度

我们知道,如果想要获取名为MainCamera的主相机且Tag为MainCamera的深度图


只需要在脚本里:
private void Start()
    {
        Camera cam = GameObject.Find("Main Camera").GetComponent<Camera>() ;
        //申请深度图,然后unity会给一个全局变量传值,这个全局变量叫_CameraDepthTexture
        cam.depthTextureMode = DepthTextureMode.Depth;//|=也行,因为unity除了生成深度之外,还有法线
        //或者直接:
        //Camera.main.depthTextureMode = DepthTextureMode.Depth;
    }
这样Camera在渲染时会增加一个前置pass,将所有的opaque物体渲染一次,将深度保存到内置的一张深度图中。在渲染物体时,shader内通过sampler2D _CameraDepthTexture可以访问这张深度贴图。将脚本挂载到相机,点击运行在TargetEye下会有提示:


全局变量_CameraDepthTexture,shader中,不在properties中声明,在下面CGPROGRAM中声明则表明该变量为全局变量。
sampler2D _CameraDepthTexture;
_CameraDepthTexture在不同平台有不同的存储方式,用UNITY_REVERSED_Z宏能够判断,如果是1,通常是Direct3D平台,深度范围是[1, 0];否则是OpenGL平台,深度范围是[0,1]。透视投影时,如果是DX平台,深度的范围是[0,1],near plane为1,far plane为0,OpenGL平台下,_CameraDepthTexture中保存越接近near plane,depth越接近0,越接近far plane,越接近1

  • Direct3D平台下,投影矩阵之后z∈[w, 0] -> 透视除法后z∈[1, 0]
  • OpenGL平台下,投影矩阵之后z∈[-w, w] -> 透视除法后z∈[-1,1] -> glDepthRange(0,1)后z∈[0, 1]
不同渲染平台下投影矩阵不同。 Camera.projectionMatrix保存的是OpenGL规范的投影矩阵(无论哪个平台),传到shader前要调用GL.GetGPUProjectionMatrix()处理平台差异。
//读取_CameraDepthTexture
//1.如果是后处理,可以直接用uv访问
//vertex
//当有多个RenderTarget时,需要自己处理UV翻转问题
#if UNITY_UV_STARTS_AT_TOP //DirectX之类的
    if(_MainTex_TexelSize.y < 0) //开启了抗锯齿
        o.uv.y = 1 - o.uv.y; //满足上面两个条件时uv会翻转,因此需要转回来
#endif
//fragment
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv));
//depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos))
//depth = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)).r


//2.其他:利用投影纹理采样
//fragment 里面
float depth = SAMLPLET_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);

float linear01Depth = Linear01Depth(depth); //转换成[0,1]内的线性变化深度值
float linearEyeDepth = LinearEyeDepth(depth); //转换到摄像机空间访问_CameraDepthTexture后就可以对纹理坐标进行采样。绝大多数情况下使用tex2D即可。
SAMPLE_DEPTH_TEXTURE用来处理平台差异问题。
SAMPEL_DEPTH_TEXTURE_PROJ:接受两个参数深度纹理和一个float3和float4类型的纹理坐标,内部使用了tex2Dproj进行投影纹理采样,通常用于阴影的实现。
SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,UNITY_PROJ_COORD(i.screenPos ));
//vertex 里面
o.screenPos = ComputeScreenPos(o.vertex);
//ComputeScreenPos用来计算线性的屏幕坐标(实际上名不副实,其实是将[-w,w]范围内的齐次坐标xy缩放到[0, w]范围),
//并保留zw值不变。这里的齐次坐标是不能做透视除法的,因为重心插值只能对线性变量插值,做了除法以后就不是线性值了,
//要保证插值永远在线性空间内进行(物体、世界、相机、齐次四个空间)。LinearEyeDepth:负责把深度纹理的采样转到视角空间下的深度值。
Linear01Depth:返回一个0~1的线性深度值 。
如果需要得到深度+法线纹理,可以直接使用tex2D对_CameraDepthNormalsTexture进行采样。
DecodeDepthNormal:的第一个参数是对深度+法线采样的结果0~1;
也可以通过DecodeFloatRG和DecodeViewNormalStereo来解码深度+法线纹理中的深度法线信息。
可视化

把深度给显示出来,思路借用后处理的方式,OnRenderImage传入的material用的shader直接返回_CameraDepthTexture查到的值转到线性空间(Linear01Depth)且是[0,1],为方便,这里不再直接把shader传给脚本在脚本创建材质传入OnRenderImage,而是直接创材质传给脚本:


//脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//保证非运行状态也能执行看到效果,不过建议挂载一个摄像机移动脚本,当运行状态下时,方便移动查看
//摄像机移动脚本: https://blog.csdn.net/pikefish/article/details/96424505
[ExecuteInEditMode]
public class DepthVisiable : MonoBehaviour
{
    public Material mat;
   
        private Camera cam;

        void Start()
        {
            cam = GetComponent<Camera>();   //获取当前绑定到脚本的相机
            cam.farClipPlane = 10;//设置相机远屏幕,太大会导致近的物体深度值过小,即黑
            cam.depthTextureMode = DepthTextureMode.Depth;
        }
   
        void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            Graphics.Blit(source, destination, mat);
        }
}
shader返回转化的深度值。
Shader "Custom/DepthVisiable" {
        SubShader{
        Tags { "RenderType" = "Opaque" }

        Pass{
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #include "UnityCG.cginc"

                sampler2D _CameraDepthTexture;

                struct v2f {
                   float4 pos : SV_POSITION;
                   float4 scrPos:TEXCOORD1;
                };

                //Vertex Shader
                v2f vert(appdata_base v) {
                   v2f o;
                   o.pos = UnityObjectToClipPos(v.vertex);
                   o.scrPos = ComputeScreenPos(o.pos);
                   return o;
                }

                //Fragment Shader
                float4 frag(v2f i) : COLOR{
                   //Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
                   float depthValue = Linear01Depth(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos)).r);
                   return float4(depthValue.xxx,1.0);
                }
                ENDCG
                }
        }
                FallBack "Diffuse"
}如果选择生成深度+法线纹理,Unity会创建一张与屏幕分辨率相同,32位精度四通道的纹理,其中ViewSpace下的法线会被编码到R与G通道,深度信息为B和A通道。
其次可以通过Frame Debugger查看深度图:UpdateDepth Texture这一步





红0,黑1,离相机近点更容易看到

其他相机的深度图

使用上面的方法,是unity内部完成的操作,unity内部设置好了一个全局变量_CameraDepthTexture提供使用,但是这样只能得到MainCamera的深度纹理,所以思路就是,模仿unity的操作,为其他的相机也设置一个全局变量提供使用,简而言之,需要通过SetTargetBuffers拿到相机深度缓存,然后放到纹理里,这个纹理就是全局变量,即sampler2D _DepthTex
//脚本
using System;
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
public class OtherCamDepth : MonoBehaviour
{
    /// 另一个摄像机
    private Camera m_Camera;
   
    /// 用于存储深度
    public RenderTexture m_depthBufferTex;

    /// 深度图
    public RenderTexture m_DepthTex;

    /// 当前摄像机渲染最终的图片
    private RenderTexture m_CameraRenderTex;
   
    /// 存储处理深度图的命令,在指定某个时期执行其中存储的命令
    private CommandBuffer m_DepthBuffer;

    public void Start()
    {
       //另一个相机
        m_Camera = GameObject.Find("otherCamera").GetComponent<Camera>() ;
        Init();

        //主相机
        Camera cam = GameObject.Find("Main Camera").GetComponent<Camera>() ;
        cam.depthTextureMode = DepthTextureMode.Depth;
    }

    private void Init()
    {
        //屏幕渲染图
        m_CameraRenderTex = RenderTexture.GetTemporary(Screen.width, Screen.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Default);

        //存储深度
        m_depthBufferTex = new RenderTexture(Screen.width, Screen.height, 24, RenderTextureFormat.Depth);
        m_depthBufferTex.name = "DepthBuffer";

        //深度图
        m_DepthTex = new RenderTexture(Screen.width, Screen.height, 0, RenderTextureFormat.RHalf);
        m_DepthTex.name = "DepthTex";

        //添加处理深度图commandbuffer
        m_DepthBuffer = new CommandBuffer();
        m_DepthBuffer.name = "CommandBuffer_DepthBuffer";
        //把depthbuffer写入m_DepthTex的colorbuffer
        //把depthbuffer合成一张rt和自带的是重新渲染一张rt效果一样
        //我这里定义rt为_DepthTex,shader直接获取这个就可以使用自定义深度图
        //向命令缓冲区写入命令
        m_DepthBuffer.Blit(m_depthBufferTex.depthBuffer, m_DepthTex.colorBuffer);
        //设置命令缓冲区中的命令何时执行
        m_Camera.AddCommandBuffer(CameraEvent.AfterForwardOpaque, m_DepthBuffer);
        //设置shader全局深度图
        Shader.SetGlobalTexture("_DepthTex", m_DepthTex);
        
    }

    private void OnPreRender()
    {  
      
        m_Camera.SetTargetBuffers(m_CameraRenderTex.colorBuffer, m_depthBufferTex.depthBuffer);
    }
}
把脚本挂载到Main Camera上,创建另外一个相机命名为otherCamera(脚本里需要根据名字获取相机),然后就可以创建一个shader,在里面声明你定义的全局变量名称,这里是_DepthTex,用法与_CameraDepthTexture一致。
同样,想要可视化深度的话,可以采取后处理的办法,输出到屏幕上,向脚本里添加:
public Material mat;
...
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
    Graphics.Blit(src , dest ,mat);
}
...创建两个材质
//这里为方便不再创建两个shader,用一个int取0或1来控制使用那张深度纹理,_DepthTexture或_CameraDepthTexture
Shader "Unlit/OtherCamDepth"
{
    Properties
    {
        _main2side ( "main2side" , int ) = 0
    }
    SubShader
    {
        ZTest Off
        Tags { "RenderType"="transparent" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _DepthTex;
            sampler2D _CameraDepthTexture;
            int _main2side;

            struct appdata{
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };


            struct v2f{
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 screenPos : TEXCOORD1;
            };

            v2f vert (appdata v){
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.screenPos = ComputeScreenPos(o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target{
                return float4(Linear01Depth((SAMPLE_DEPTH_TEXTURE(_DepthTex, i.uv))).xxx,1.0) * _main2side + (1-_main2side)*float4(Linear01Depth((SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv))).xxx,1.0);
            }
            ENDCG
        }
    }

}



左:正视,右:俯视,两个相机,far plane均为10

应用

水面

拿到深度之后,可以做的事情就很多了,比如水面,水面一般来说





Creater Lake







morskie oko

明显可以感知到,水深的地方颜色就深,所以我们需要找到一种近似水深的量,联想到深度,假设我们是俯视看着水面,我们需要知道水面到水底的距离,很简单,我们只要把水底写入深度图,水面不写入,渲染水面时,能拿到当前shading Point的深度,也就是水面的深度,同时unity中通过LinearEyeDepth函数我们可以传入深度图上的深度,获得ViewSpace下的水底的z坐标,直接减去当前shading Point的ViewSpace下的z坐标:


float existingDepth01 = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)).r;
//float existingDepth01 = (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)))

//LinearEyeDepth可以拿到深度图上记录的点的ViewSpace下的深度也就是此时该点的z值            
float existingDepthLinear = LinearEyeDepth(existingDepth01);

//view space 下水面与水底Z坐标差值,因为水面不写入深度,所以深度图 里记的是水底的深度,
//i.vertex.w是clip space下的坐标,它的w分量正好是经过
//mv变换后,处在view sapce下的顶点经过透视投影矩阵P,P的最后一行是0,0,1,0,
//所以作用完后得到clip space下顶点的w坐标正好为P矩阵作用的坐标的Z分量
//其次 unity相机看向-Z,所以i.viewSpace_pos.z全是负值,但是LinearEyeDepth算出的Z是正数
float viewSpace_pos_Z = -1 * i.viewSpace_pos.z;
float depthDifference  = existingDepthLinear - i.viewSpace_pos.z;//i.vertex.w , i.screenPos.w
return float4( depthDifference.xxx,1.0 );


返回z坐标差值,左隐藏水面,右显示水面



使用插值lerp几种颜色,比如两种

Shader "Unlit/water"
{
    Properties
    {      
        _DepthGradientShallow("Depth Gradient Shallow", Color) = (0.0, 0.7, 1.0, 0.0)
        
        _DepthGradientDeep("Depth Gradient Deep", Color) = (0.0, 0.26, 1, 0.98)
        
        // Maximum distance the surface below the water will affect the color gradient.
        _DepthMaxDistance("Depth Maximum Distance", Float) = 1

    }
    SubShader
    {
        Tags
        {
                "Queue" = "Transparent"
        }

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal :NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 screenPos : TEXCOORD1;
                    float3 normal :TEXCOORD2;
                    float4 viewSpace_pos : TEXCOORD3;
            };

            //sampler2D _MainTex;
            //float4 _MainTex_ST;
            float4 _DepthGradientShallow;
            float4 _DepthGradientDeep;
            sampler2D _CameraDepthTexture;
            float _DepthMaxDistance;   
      
            v2f vert (appdata v)
            {
                v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.screenPos = ComputeScreenPos(o.vertex);
                    o.viewSpace_pos = mul(UNITY_MATRIX_MV , v.vertex);
                   
                return o;
            }
   

            fixed4 frag (v2f i) : SV_Target
            {


                float existingDepth01 = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)).r;
               
                float existingDepthLinear = LinearEyeDepth(existingDepth01);

                    //view space 下水面与水底Z坐标差值,因为水面不写入深度,所以深度图 里记的是水底的深度,
                    //LinearEyeDepth可以拿到深度图上记录的点的ViewSpace下的深度也就是此时该点的z值
                    //i.vertex.w是clip space下的坐标,它的w分量正好是经过
                    //mv变换后,处在view sapce下的顶点经过透视投影矩阵P,P的最后一行是0,0,1,0,
                    //所以作用完后得到clip space下顶点的w坐标正好为P矩阵作用的坐标的Z分量
                    //其次 unity相机看向-Z,所以i.viewSpace_pos.z全是负值,但是LinearEyeDepth算出的Z是正数
                float viewSpace_pos_Z = -1 * i.viewSpace_pos.z;
                float depthDifference = existingDepthLinear - viewSpace_pos_Z;//i.vertex.w , i.screenPos.w
                //return float4(depthDifference.xxx,1.0);

                float waterDepthDifference01 = saturate(depthDifference / _DepthMaxDistance);
                float4 waterColor = lerp(_DepthGradientShallow, _DepthGradientDeep, waterDepthDifference01);
                return waterColor;
            }
            ENDCG
        }
    }

}
这里只用到unity的_CameraDepthTexture一张深度图,脚本只需启用相机深度图。
扫描

扫描这里的实现思路很简单的,依旧是后处理的思路,为了方便直接在脚本里用材质,为了实现效果需要找到一定区域内的深度值,比如[a,a+c],比如按下c键,a就从0逐渐增长,扫描宽度c,判断深度是否处于这一区域,符合直接在shader里返回扫描线的颜色:
using UnityEngine;

[ExecuteInEditMode]
public class Scan : MonoBehaviour {

    public Material mat;
    public float velocity = 0.4f;  //速度
    public float scanWidth = 0.01f;//扫描线宽度
    [Range(0.0f,1.0f)]public float slowNear = 0.22f;//降低近处扫描线速度,提升远处速度
    [Range(0.0f,1.0f)]public float decreaseNearWidth = 0.27f;//减小近处扫描线宽度,增大远处扫描线宽度
    [Range(0.0f,1.0f)]public float scanHeightPos = 0.11f;//扫描线最亮的部位处最前最后还是中间
    private float dis;
   
    void Start(){
        Camera.main.depthTextureMode = DepthTextureMode.Depth;
    }

    void Update(){
        this.dis += Time.deltaTime * this.velocity;
        
        if (Input.GetKeyDown(KeyCode.C)){
            this.dis = 0;
        }
    }
    void OnRenderImage(RenderTexture src, RenderTexture dst) {
        mat.SetFloat("_ScanDistance", dis);
        mat.SetFloat("_ScanWidth", scanWidth);
        mat.SetFloat("_SlowNear", slowNear);
        mat.SetFloat("_ScanHeightPos", scanHeightPos);
        mat.SetFloat("_DecreaseNearWidth", decreaseNearWidth);
        Graphics.Blit(src, dst, mat);
    }
}

Shader "Unlit/Scan"
{
    Properties{
        _MainTex ("Texture", 2D) = "white" {}
        _ScanColor("Scan Color" , Color) = (1,0,0,1)
        
    }
    SubShader{
        Tags {
            "RenderType" = "Transparent"
            "Queue" = "Transparent"
            }
        Blend SrcAlpha OneMinusSrcAlpha
        Pass{
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct v2f{
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 scrPos : TEXCOORD1;
            };

            sampler2D _MainTex;
            sampler2D _CameraDepthTexture;
            float _ScanDistance,_ScanWidth,_SlowNear, _ScanHeightPos,_DecreaseNearWidth;
            float4 _ScanColor;
            
         
            v2f vert (appdata_base v){
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                return o;
            }
            //算[a,b]中va长度占总长度的比例系数, 1-Inverse()即为vb长度占总长度
            float InverseLerp( float a , float b , float v ){
                return ( v - a ) / ( b - a )   ;
            }

            fixed4 frag (v2f i) : SV_Target{
                float4 col = tex2D( _MainTex,i.uv );
                float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
                    depth = Linear01Depth(depth);
                //优化问题,为什么不推荐用if在shader里 https://blog.csdn.net/liu_if_else/article/details/77455639
                _ScanWidth += depth*_DecreaseNearWidth; //近处的扫描范围小远处的扫描范围大
                _ScanDistance *= _SlowNear+depth;//distance从零均匀上涨(c#),近处的涨慢点,远处的涨快点
                if (depth < _ScanDistance && depth > _ScanDistance - _ScanWidth && depth < 1)
                {
                    //return float4(1,0,0,1);
                    //根据Depth到扫描线离相机远的位置距离,插值拖尾,头亮尾巴淡化
                    //return lerp( col , _ScanColor   ,(InverseLerp( _ScanDistance - _ScanWidth ,_ScanDistance ,  depth )) );
                    //根据Depth到扫描线中间的位置的距离,两边淡化
                    //return lerp( _ScanColor,col   ,abs(depth - (_ScanDistance - _ScanWidth*0.5))/(_ScanWidth*0.5) );
                    //调参来控制尾巴淡化
                    float a = _ScanDistance -_ScanWidth;
                    float v = _ScanDistance -_ScanHeightPos * _ScanWidth;
                    float b = _ScanDistance;
                    //[a,b],有[a,v)[v,b],depth在其中一个区间,需要根据depth到v的距离除以所在区间总长度得到系数来插值
                    if( depth < v ){
                         return lerp( col , _ScanColor   ,InverseLerp(a,v,depth));
                    }else{
                        return lerp( col , _ScanColor   ,InverseLerp(b,v,depth));
                    }
                }
                //return 0;
                return col;
            }
            ENDCG
        }
    }
}



return float(1,0,0,0)



CHAI'S BLOG " Unity深度和深度贴图
Unity Toon Water Shader Tutorial at Roystan

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-15 16:16 , Processed in 0.147660 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表