本文同时发布在我的个人博客上:https://dragon_boy.gitee.io
获取深度和法线纹理
原理
深度纹理实际是一张渲染纹理,存储的是深度值,范围是[0,1],通常是非线性分布。
在顶点空间变换时,变换到裁剪空间的坐标为NDC,即范围在[-1,1],所以需要映射一下到[0,1]。
在Unity中,会使用着色器替换技术选择那些渲染类型为Opaque
的物体,判断它们使用的渲染队列是否小于等于2500,如果满足条件就把它渲染到深度和法线纹理中。
在Unity中,我们可以选择让一个摄像机生成一张深度纹理或是一张深度+法线纹理。选择前者,Unity会直接获取深度缓冲或上述的着色器替换技术,选择需要的不透明物体,并对使用它投射阴影时使用的Pass来得到深度纹理。如果选择后者,Unity会创建一张和屏幕分辨率相同、精度为32为的纹理,其中观察空间下的发现信息在RG通道,深度信息在BA通道。法线信息在延迟测试中可以非常容易得到,Unity只需合并深度和发现缓存。在前向渲染中,默认情况下不会创建法线缓存,因此Unity底层使用一个单独的Pass把整个场景再次渲染一遍来完成。
如何获取
获取深度纹理很简单,只需在脚本中设置深度纹理模式,然后可以在Shader中使用_CameraDepthTexture
访问深度纹理:
camera.depthTextureMode = DepthTextureMode.Depth;
深度法线纹理同理:
camera.depthTextureMode = DepthTextureMode.DepthNormals;
在Shader中使用_CameraDepthNormalsTexture
访问。
Unity中提供了一个宏定义SAMPLE_DEPTH_TEXTURE
来对深度纹理采样,主要是为了处理平台差异。
当通过纹理采样获得深度值后,这些深度值往往是非线性的,这种非线性来自于透视投影使用的裁剪矩阵。实际计算中我们需要线性的深度值,所以我们需要将深度值变换到线性空间下,例如视角空间下的深度值,推导过程如下:
当我们使用透视投影的裁剪矩阵对视角空间下的顶点变换后,裁剪空间下的顶点的z和w分量是:
通过透视除法,就可以得到NDC下的z分量:
深度纹理中的深度值是通过上面的NDC分量计算得到的:
根据上述推导的表达式:
由于Unity中视图空间正对的z值为负数,将上式取反:
上式取值范围是[Near,Far],为得到[0,1]之间的深度值,将上式结果除以Far,结果如下:
针对上述的推导过程,Unity提供了两个函数。LinearEyeDepth
将深度纹理的采样结果转换到视角空间下的深度值,即。Linear01Depth
将返回范围在[0,1]的线性深度值,即。这两个函数使用Unity内置的_ZBuffferParams
变量来得到远近裁剪平面的距离。
我们可以使用tex2D
直接对深度法线纹理采样,然后使用Unity的DecodeDepthNormal
函数来对采样结果解码。
运动模糊
这里我们使用速度映射图来模拟运动模糊。我们利用深度纹理在片元着色器中为每个像素计算其在世界空间下的位置,这是通过使用当前视角投影矩阵的逆矩阵对NDC下的顶点坐标进行变换得到的。接着我们使用前一帧的视角投影矩阵对其进行变换,得到该位置在前一帧中的NDC坐标。然后,我们计算前一帧和当前帧的位置差,生成该像素的速度。
下面编写脚本:
using UnityEngine;
using System.Collections;
public class MotionBlurWithDepthTexture : PostEffectsBase
{
public Shader motionBlurShader;
private Material motionBlurMaterial = null;
public Material material
{
get
{
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
return motionBlurMaterial;
}
}
private Camera myCamera;
public Camera camera
{
get
{
if (myCamera == null)
{
myCamera = GetComponent();
}
return myCamera;
}
}
[Range(0.0f, 1.0f)]
public float blurSize = 0.5f;
private Matrix4x4 previousViewProjectionMatrix;
void OnEnable()
{
camera.depthTextureMode |= DepthTextureMode.Depth;
previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
material.SetFloat("_BlurSize", blurSize);
material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
previousViewProjectionMatrix = currentViewProjectionMatrix;
Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}
}
Shader代码如下:
Shader "Unlit/MotionBlurWIthDepthTexture"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
float4x4 _CurrentViewProjectionInverseMatrix;
float4x4 _PreviousViewProjectionMatrix;
half _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv_depth : TEXCOORD1;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif
return o;
}
fixed4 frag(v2f i) : SV_Target {
// Get the depth buffer value at this pixel.
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
// H is the viewport position at this pixel in the range -1 to 1.
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
// Transform by the view-projection inverse.
float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
// Divide by w to get the world position.
float4 worldPos = D / D.w;
// Current viewport position
float4 currentPos = H;
// Use the world position, and transform by the previous view-projection matrix.
float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
// Convert to nonhomogeneous points [-1,1] by dividing by w.
previousPos /= previousPos.w;
// Use this frame's position and last frame's to compute the pixel velocity.
float2 velocity = (currentPos.xy - previousPos.xy) / 2.0f;
float2 uv = i.uv;
float4 c = tex2D(_MainTex, uv);
uv += velocity * _BlurSize;
for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
float4 currentColor = tex2D(_MainTex, uv);
c += currentColor;
}
c /= 3;
return fixed4(c.rgb, 1.0);
}
ENDCG
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
当得到像素速度后,我们就根据这个速度来对它的邻域像素进行采样,接着平均。
全局雾效
这里介绍一种快速从深度纹理重建世界坐标的方法。这种方法首先对图像空间下的视锥体射线(从摄像机出发,指向图像上的某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息。然后,我们把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素再世界空间下的位置。
重建世界坐标
重建世界坐标的代码如下:
float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;
其中,_WorldSpaceCameraPos
是摄像机在世界空间下的位置,这可以右Unity的内置变量直接访问得到。而linearDepth * interpolatedRay
则可以计算得到该像素相对于摄像机的偏移量,linearDepth
是由深度纹理得到的线性深度值,interpolatedRay
是由顶点着色器输出并插值后得到的射线,它不仅包含该像素到摄像机的方向,也包含了距离信息。
interpolatedRay
来源于对近裁剪平面的4个角的某个特定向量的插值,这4个向量包含了它们到摄像机的方向和距离信息。下面进行推导:
首先计算两个向量,toTop,toRight,它们是起点位于近裁剪平面中心、分别指向摄像机正上方和正右方的向量,计算公式如下:
得到这两个矢量后,就可以计算近裁剪平面的四个角相对于摄像机的方向。以左上角TL为例:
同理,其它三个角:
上面求得的四个向量不仅包含方向信息,它们的模对应了4个点到摄像机的空间距离。由于我们得到的线性深度值并非是颠倒摄像机的欧式距离,而是z方向的距离,因此,不能直接使用深度值和4个角的单位方向的乘积来计算它们到摄像机的偏移量。下面进行线性深度值到欧式距离的转化。
TL所在的射线上,像素的深度值和它到摄像机的实际距离的比等于近裁剪平面的距离和TL向量的模的比,即:
那么TL点距离摄像机的欧式距离dist:
由于其它三个向量的模和TL相等,那么我们可以提取一个缩放因子:
我们可以使用这个缩放因子和单位向量相乘来得到对应的向量值,如:
屏幕后处理的原理就是使用特定的材质去渲染一个刚好填充整个屏幕的四边形面片。这个四边形面片的4个顶点对应了近裁剪平面的4个角。我们将上述的计算结果传递给顶点着色器,顶点着色器根据当前的位置选择他所对应的相应向量,然后将其输出,经插值后传递给片元着色器得到interpolatedRay
。
雾的计算
在简单的雾效实现中,我们需要计算一个雾效系数f,作为混合原始颜色和雾的颜色的混合系数:
float3 afterFog = f * fogColor + (1 - f) * origColor;
f的计算方法很多,Unity内置的雾效实现支持三种:线性、指数和指数的平方。当给定距离z后,f的计算公式如下:
- Linear:
,和分别是受雾影响的最小距离和最大距离。 - Exponential:
,是控制雾的浓度的参数。 - Exponential Squared:
,是控制雾的浓度的参数。
在这里使用类似线性雾的计算方式,计算基于高度的雾效,具体方法是,当给定一点在世界空间下的高度y后,f的计算公式为:
,和分别表示受雾影响的起始高度和终止高度。
首先实现脚本:
using UnityEngine;
using System.Collections;
public class FogWithDepthTexture : PostEffectsBase
{
public Shader fogShader;
private Material fogMaterial = null;
public Material material
{
get
{
fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
return fogMaterial;
}
}
private Camera myCamera;
public Camera camera
{
get
{
if (myCamera == null)
{
myCamera = GetComponent();
}
return myCamera;
}
}
private Transform myCameraTransform;
public Transform cameraTransform
{
get
{
if (myCameraTransform == null)
{
myCameraTransform = camera.transform;
}
return myCameraTransform;
}
}
[Range(0.0f, 3.0f)]
public float fogDensity = 1.0f;
public Color fogColor = Color.white;
public float fogStart = 0.0f;
public float fogEnd = 2.0f;
void OnEnable()
{
camera.depthTextureMode |= DepthTextureMode.Depth;
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
Matrix4x4 frustumCorners = Matrix4x4.identity;
float fov = camera.fieldOfView;
float near = camera.nearClipPlane;
float aspect = camera.aspect;
float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
Vector3 toRight = cameraTransform.right * halfHeight * aspect;
Vector3 toTop = cameraTransform.up * halfHeight;
Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
float scale = topLeft.magnitude / near;
topLeft.Normalize();
topLeft *= scale;
Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
topRight.Normalize();
topRight *= scale;
Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
bottomLeft.Normalize();
bottomLeft *= scale;
Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
bottomRight.Normalize();
bottomRight *= scale;
frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);
material.SetMatrix("_FrustumCornersRay", frustumCorners);
material.SetFloat("_FogDensity", fogDensity);
material.SetColor("_FogColor", fogColor);
material.SetFloat("_FogStart", fogStart);
material.SetFloat("_FogEnd", fogEnd);
Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}
}
按照之前的理论计算四个射线向量,然后按照以左下角为原点,逆时针按行构建矩阵。这个顺序非常重要,因为这决定了我们在顶点着色器中使用哪一行作为该点的待插值向量。
Shader代码如下:
Shader "Unlit/Fog"
{
Properties{
_MainTex("Base (RGB)", 2D) = "white" {}
_FogDensity("Fog Density", Float) = 1.0
_FogColor("Fog Color", Color) = (1, 1, 1, 1)
_FogStart("Fog Start", Float) = 0.0
_FogEnd("Fog End", Float) = 1.0
}
SubShader{
CGINCLUDE
#include "UnityCG.cginc"
float4x4 _FrustumCornersRay;
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
half _FogDensity;
fixed4 _FogColor;
float _FogStart;
float _FogEnd;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv_depth : TEXCOORD1;
float4 interpolatedRay : TEXCOORD2;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif
int index = 0;
if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
index = 0;
}
else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
index = 1;
}
else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
index = 2;
}
else {
index = 3;
}
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
index = 3 - index;
#endif
o.interpolatedRay = _FrustumCornersRay[index];
return o;
}
fixed4 frag(v2f i) : SV_Target {
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
fogDensity = saturate(fogDensity * _FogDensity);
fixed4 finalColor = tex2D(_MainTex, i.uv);
finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
return finalColor;
}
ENDCG
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
边缘检测
这里使用深度和法线纹理进行边缘检测。
脚本实现如下:
using UnityEngine;
using System.Collections;
public class EdgeDetectNormalsAndDepth : PostEffectsBase
{
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null;
public Material material
{
get
{
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f;
public Color edgeColor = Color.black;
public Color backgroundColor = Color.white;
public float sampleDistance = 1.0f;
public float sensitivityDepth = 1.0f;
public float sensitivityNormals = 1.0f;
void OnEnable()
{
GetComponent().depthTextureMode |= DepthTextureMode.DepthNormals;
}
[ImageEffectOpaque]
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
material.SetFloat("_SampleDistance", sampleDistance);
material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));
Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}
}
注意我们为OnRenderImage
函数添加了[ImageEffectOpaque]
属性,不对透明物体产生影响。
这里使用Roberts算子进行边缘检测,Shader代码如下:
Shader "Unlit/EdgeDetect"
{
Properties{
_MainTex("Base (RGB)", 2D) = "white" {}
_EdgeOnly("Edge Only", Float) = 1.0
_EdgeColor("Edge Color", Color) = (0, 0, 0, 1)
_BackgroundColor("Background Color", Color) = (1, 1, 1, 1)
_SampleDistance("Sample Distance", Float) = 1.0
_Sensitivity("Sensitivity", Vector) = (1, 1, 1, 1)
}
SubShader{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
float _SampleDistance;
half4 _Sensitivity;
sampler2D _CameraDepthNormalsTexture;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
uv.y = 1 - uv.y;
#endif
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1,1) * _SampleDistance;
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance;
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,1) * _SampleDistance;
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1,-1) * _SampleDistance;
return o;
}
half CheckSame(half4 center, half4 sample) {
half2 centerNormal = center.xy;
float centerDepth = DecodeFloatRG(center.zw);
half2 sampleNormal = sample.xy;
float sampleDepth = DecodeFloatRG(sample.zw);
// difference in normals
// do not bother decoding normals - there's no need here
half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
// difference in depth
float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
// scale the required threshold by the distance
int isSameDepth = diffDepth < 0.1 * centerDepth;
// return:
// 1 - if normals and depth are similar enough
// 0 - otherwise
return isSameNormal * isSameDepth ? 1.0 : 0.0;
}
fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target {
half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
half edge = 1.0;
edge *= CheckSame(sample1, sample2);
edge *= CheckSame(sample3, sample4);
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRobertsCrossDepthAndNormal
ENDCG
}
}
}
我们调用CheckSame
来计算算子的对角线上的两个纹理值的插值,返回0表明存在边界。
在CheckSame
函数中,我们首先得到两个采样点法线和深度值,我们计算两个采样点的法线和深度值的插值,并称一对应的敏感系数,将差异值得每个分量相加再与阈值比较,如果小于阈值则表明不存在边界,反之存在边界。最后将法线和深度得检查结果相乘,作为组合值返回。