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