Unity3D Shader系列之护盾效果


1 引言

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

2 代码实现

2.1 原理分析

护盾效果的重点在于护盾与其他物体相交时,需要在相交边缘增加额外的相交光,就像下图箭头所示。
护盾效果与其他物体交互
所以护盾效果的难点在于,如何在Shaer中知道护盾与其他物体相交了。这里直接就说答案了,不知道答案的话可能也很难想到。
其具体步骤如下:
①我们先获取相机的深度图(这里面包含了所有距离相机最近的不透明物体的深度信息),
②在护盾Shader中获取当前像素的观察空间深度值Zview
③片元着色器中,使用像素对应的视口坐标对深度纹理进行采样,并转换为观察空间中的深度值Zcamera
④两者相减(Zview - Zcamera),如果差值在某一范围内,就认为护盾与相机中的其他物体相交了
⑤然后相交部分增加额外的颜色(如上图中的白色)

2.2 代码分析

2.2.1 获取深度纹理

我们在《Unity3D Shader系列之深度纹理》中已经讲过如何在Shader中获取相交的深度图,这里再重复一遍:
①相机的depthTextureMode设置为DepthTextureMode.Depth
(当然设置为DepthNormals也可以,此时在对深度+法线纹理采样那儿需要用DecodeDepthNormal来解码)
②Shader中添加名为_CameraDepthTexture的sampler2D变量,Unity会自动将相机的深度纹理赋值到此变量中

sampler2D _CameraDepthTexture;

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

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

②在片元着色器中,先对screenPos的xy分量进行透视除法(即除以w),即可得到该像素对应的视口坐标
③然后使用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_TEXTURE与SAMPLE_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

④最后使用LinearEyeDepth将NDC坐标系中的深度值转换为观察空间中的深度值

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
            {
    
    
				// 相交光
				//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;

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

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

博主本文博客链接。
完整项目。
链接:https://pan.baidu.com/s/1I-HoVsOFTYmyLdotgYArQQ
提取码:4h2o

4 参考文章

猜你喜欢

转载自blog.csdn.net/sinat_25415095/article/details/124369921
今日推荐