【转载】Unity3D Shader 系列之护盾效果

446 阅读5分钟

原文链接

Unity3D Shader系列之护盾效果 | 老王

1 引言

在《Unity3D Shader系列之深度纹理》这篇文章中,我们详细讨论了深度纹理相关的知识点,这里面留了一个坑,说用深度纹理来实现一些效果。今天咱们就用深度纹理来实现一下护盾的效果。注意,看这篇文章之前一定要将深度纹理的知识弄清楚,这样才能看懂 Shader 中的代码。效果如下。
护盾效果

2 代码实现

2.1 原理分析

护盾效果的 重点 在于护盾与其他物体相交时,需要在相交边缘增加额外的相交光,就像下图箭头所示。
护盾效果与其他物体交互
所以护盾效果的 难点 在于,如何在 Shader 中知道护盾与其他物体相交了。这里直接就说答案了,不知道答案的话可能也很难想到。
其具体步骤如下:

  • 我们先获取相机的深度图(这里面包含了所有距离相机 最近不透明 物体的深度信息),
  • 在护盾 Shader 中获取当前像素的 观察空间 深度值 Zview
  • 片元着色器中,使用像素对应的 视口坐标 对深度纹理进行采样,并转换为 观察空间 中的深度值 Zcamera
  • 两者相减(Zview – Zcamera),如果差值在某一范围内,就认为护盾与相机中的其他物体相交了
  • 然后相交部分增加额外的颜色(如上图中的白色)

2.2 代码分析

2.2.1 获取深度纹理

我们在《Unity3D Shader系列之深度纹理》中已经讲过如何在 Shader 中获取相交的深度图,这里再重复一遍:

  • 相机的 depthTextureMode 设置为 DepthTextureMode.Depth
    (当然设置为 DepthNormals 也可以,此时在对深度+法线纹理采样那儿需要用 DecodeDepthNormal 来解码)
  • Shader中添加名为 _CameraDepthTexturesampler2D 变量,Unity 会自动将相机的深度纹理赋值到此变量中
sampler2D _CameraDepthTexture;

2.2.2 使用视口坐标对深度纹理采样

按上面这两步骤操作完,我们在 Shader 中即可通过 _CameraDepthTexture 访问到相机的深度纹理了。但是新问题出现了,如何得到像素的视口坐标?这一点我们在《Unity3D Shader系列之全息投影》进行过详细讨论。这里也直接拿过来了:

  1. 在顶点着色器中使用 ComputeScreenPos 方法即可得到该顶点对应的 “视口坐标”(这里打引号是其实它 还不是真正的 视口坐标,我们实际使用时需要进行 透视除法),该方法的参数为顶点在 裁剪空间 的坐标值,返回值为 float4 类型的变量
o.screenPos = ComputeScreenPos(o.vertex);

ComputeScreenPos 位于 UnityCG.cginc 中。

inline float4 ComputeScreenPos(float4 pos) {
    float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    return o;
}

inline float4 ComputeNonStereoScreenPos(float4 pos) {
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}

#if defined(UNITY_SINGLE_PASS_STEREO)
float2 TransformStereoScreenSpaceTex(float2 uv, float w)
{
    float4 scaleOffset = unity_StereoScaleOffset[unity_StereoEyeIndex];
    return uv.xy * scaleOffset.xy + scaleOffset.zw * w;
}
  1. 在片元着色器中,先对 screenPosxy 分量进行 透视除法 (即除以w),即可得到该像素对应的视口坐标
  2. 然后使用 SAMPLE_DEPTH_TEXTURE 方法对深度纹理 _CameraDepthTexture 进行采样得到 NDC 坐标系 中的深度值
float2 wcoord = i.screenPos.xy / i.screenPos.w;
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, wcoord);

解释SAMPLE_DEPTH_TEXTURE 内部其实就是使用 tex2D 对深度纹理进行采样,只不过它对 PS2 平台进行了 兼容性处理
当然,上面两行代码也可以使用 SAMPLE_DEPTH_TEXTURE_PROJ 方法 简化为一行代码

  • SAMPLE_DEPTH_TEXTURE_PROJ 内部使用 tex2Dproj 对纹理采样,
  • SAMPLE_DEPTH_TEXTURE 使用tex2D 对纹理采样。

tex2Dproj 会对输入的 uv 坐标进行 透视除法,然后再进行采样。后缀是 _PROJ 或者 proj 嘛,自然表面传进来的 uv 坐标是 裁剪空间 下的,所以内部会对其进行透视除法。

float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.screenPos);

这一点从 SAMPLE_DEPTH_TEXTURESAMPLE_DEPTH_TEXTURE_PROJ 的定义可以看出,位于 HLSLSupport.cginc

// Depth texture sampling helpers.
// On most platforms you can just sample them, but some (e.g. PSP2) need special handling.
//
// SAMPLE_DEPTH_TEXTURE(sampler,uv): returns scalar depth
// SAMPLE_DEPTH_TEXTURE_PROJ(sampler,uv): projected sample
// SAMPLE_DEPTH_TEXTURE_LOD(sampler,uv): sample with LOD level

#if defined(SHADER_API_PSP2) && !defined(SHADER_API_PSM)
#   define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D<float>(sampler, uv))
#   define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2DprojShadow(sampler, uv))
#   define SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod<float>(sampler, uv))
#   define SAMPLE_RAW_DEPTH_TEXTURE(sampler, uv) SAMPLE_DEPTH_TEXTURE(sampler, uv)
#   define SAMPLE_RAW_DEPTH_TEXTURE_PROJ(sampler, uv) SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv)
#   define SAMPLE_RAW_DEPTH_TEXTURE_LOD(sampler, uv) SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv)
#else
    // Sample depth, just the red component.
#   define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv).r)
#   define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)
#   define SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod(sampler, uv).r)
    // Sample depth, all components.
#   define SAMPLE_RAW_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv))
#   define SAMPLE_RAW_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv))
#   define SAMPLE_RAW_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod(sampler, uv))
#endif

// Deprecated; use SAMPLE_DEPTH_TEXTURE & SAMPLE_DEPTH_TEXTURE_PROJ instead
#if defined(SHADER_API_PSP2)
#   define UNITY_SAMPLE_DEPTH(value) (value).r
#else
#   define UNITY_SAMPLE_DEPTH(value) (value).r
#endif
  1. 最后使用 LinearEyeDepthNDC 坐标系 中的深度值转换为 观察空间 中的深度值
float eyeDepth = LinearEyeDepth(depth);

LinearEyeDepth 方法的定义位于 UnityCG.cginc 中,具体如下。

// Z buffer to linear 0..1 depth
inline float Linear01Depth( float z )
{
    return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{
    return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

2.2.3 获取当前像素的深度值

在 2.1 节的步骤 ② 中,我们需要在护盾 Shader 中获取当前像素的 观察空间 深度值 Zview 。这怎么获取呢?其实也很简单,我们在顶点着色器中,对顶点的 局部 坐标进行 MV 变换并将 z 值乘以 -1 即可得到该 顶点 对应的 观察空间 深度值 Zview ,然后经过 GPU 从顶点着色器到片元着色器的 插值,即可得到当前 像素 对应的 观察空间 深度值 Zview

有个问题,为什么要乘以 -1 呢?

因为 Unity 中局部坐标系、世界坐标系都是 左手坐标系,而观察空间是 右手坐标系,如果不乘 -1 得到的 Zview 将是个负值,而 LinearEyeDepth 方法得到的观察空间深度值永远是正值(其值范围为 Near ~ Far),所以我们这里需要乘个 -1
当然 Unity 已经帮我们将上述步骤封装成了 COMPUTE_EYEDEPTH 方法,我们直接使用就好。

COMPUTE_EYEDEPTH(o.screenPos.z);

COMPUTE_EYEDEPTH 的定义位于 UnityCG.cginc 中,具体如下。

#define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z
#define COMPUTE_DEPTH_01 -(UnityObjectToViewPos( v.vertex ).z * _ProjectionParams.w)

3 完整代码

DepthTexCamera.cs

using UnityEngine;

[RequireComponent(typeof(Camera))]
public class DepthTexCamera : MonoBehaviour
{
    private Camera m_Camera;

    private void Awake()
    {
        m_Camera = GetComponent<Camera>();
    }

    private void OnEnable()
    {
        m_Camera.depthTextureMode |= DepthTextureMode.Depth;
    }

    private void OnDisable()
    {
        m_Camera.depthTextureMode &= ~DepthTextureMode.Depth;
    }
}

护盾 Shader

Shader "LaoWang/Shield"
{
    Properties
    {
        _Color ("Color", Color) = (0, 0, 0.5, 0.5)
        _IntersectColor ("Intersect Color", Color) = (1, 0, 0, 1)
        _IntersectPower ("Intesect Power", Range(0, 8)) = 0.2
        _RimIntensity ("Rim Intensity", Range(0, 4)) = 2.0
    }
    SubShader
    {
        Tags {  "RenderType"="Transparent" 
                "Queue"="Transparent" 
                "IgnoreProjector"="true" 
        }

        Pass
        {
            Cull Off
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            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 screenPos : TEXCOORD1;
                float3 worldViewDir : TEXCOORD2;
                float3 worldNormal : NORMAL;
                float4 vertex : SV_POSITION;
            };

            sampler2D _CameraDepthTexture;
            fixed4 _Color;
            fixed4 _IntersectColor;
            fixed _IntersectPower;
            float _RimIntensity;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.screenPos = ComputeScreenPos(o.vertex);
                // #define COMPUTE_EYEDEPTH(o) o = -mul( UNITY_MATRIX_MV, v.vertex ).z
                COMPUTE_EYEDEPTH(o.screenPos.z); ///< 同上

                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldViewDir = UnityWorldSpaceViewDir(mul(unity_ObjectToWorld, v.vertex));
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                /// @note 相交光
                //float2 wcoord = i.screenPos.xy / i.screenPos.w;
                //float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, wcoord);
                float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.screenPos); ///< 同上
                float eyeDepth = LinearEyeDepth(depth);
                float distance = eyeDepth - i.screenPos.z;
                float intersect = (1 - distance) * _IntersectPower;

                /// @note 边缘光
                float rim = 1.0 - abs(dot(i.worldNormal, normalize(i.worldViewDir)));
                rim *= _RimIntensity;

                float glow = max(intersect, rim);
                return _Color * glow;
            }
            ENDCG
        }
    }
}

完整项目

链接:pan.baidu.com/s/1I-HoVsOF…
提取码:4h2o

4 参考文章

  1. 神奇的深度图:复杂的效果,不复杂的原理
  2. UNITY SHADER 之 简单 护盾SHIELD 效果的实现