获取深度和法线纹理
深度纹理:深度值来源于顶点变换得到的归一化的设备坐标(NDC),unity中针对观察空间为右手坐标系,使用列矩阵右乘,变换到NDC后z分量将再[-1,1]。DX中,变换后的分量z在[0,1]中。z分量通过乘0.5加0.5的方式映射到纹理坐标中。
unity中得到:真正的深度缓存或一个单独的pass渲染——取决于使用的渲染路径和硬件。
使用延迟渲染时,深度纹理可以访问。
无法直接获取深度缓存时,深度和法线纹理通过一个单独的pass渲染:unity使用着色器替换技术选择那些渲染类型为opaque的物体,判断他们使用的渲染队列是否小于等于2500(内置的background、geometry、alphatest渲染队列均在此范围内),如果满足条件,就把它渲染到深度和法线纹理中。
因此如果希望物体能够出现在深度和法线纹理中,就必须在shader中设置正确的渲染类型标签。
在unity中,摄像机可以生成一张深度纹理或一张深度+法线纹理。
选择生成一张深度纹理时:unity直接获取深度缓存或按之前讲到的着色器替换技术,选取不透明的物体,并使用它投射阴影时使用的pass(lightmode设置为shadowcaster)来得到深度纹理,如果shader中不包含这样一个pass,那么这个物体就不会出现在深度纹理中。
选择生成一张深度+法线纹理:unity会创建一张和屏幕分辨率相同,精度为32(每个通道8位)的纹理,观察空间下的法线信息编码进RG通道,深度信息编码进BA通道。法线信息在延迟渲染中非常容易获得,unity只需要合并深度和法线缓存即可,但是在前向渲染中,默认情况下是不会创建法线缓存的,因此unity底层使用一个单独的pass把整个场景再次渲染一遍来完成。
如何获取:
获取深度纹理:相机脚本中设置depthTextureMode:
camera.depthTextureMode=DepthTextureMode.Depth;
设置好以后在shader中可以通过声明变量:_CameraDepthTexture来访问。
获取深度+法线纹理:相机脚本中设置depthTextureMode:
camera.depthTextureMode=DepthTextureMode.DepthNormals;
设置好以后在shader中可以通过声明变量:_CameraDepthNormalsTexture来访问。
还可以组合模式,以同时产生一张深度和深度+法线纹理:
camera.depthTextureMode |= DepthTextureMode.Depth; camera.depthTextureMode |= DepthTextureMode.DepthNormals;
unity中还可以看到当前摄像机是否需要渲染深度或深度+法线纹理。
采样使用统一的宏(平台差异的处理):
float d=SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture , i.uv)
采样得到的值是非线性的(透视投影使用的裁剪矩阵),所以还需要把投影后的深度值变换到线性空间下(例如视角空间下的深度值)。unity提供辅助函数来进行上述的计算——LinearEyeDepth(负责把深度纹理的采样结果转换到视角空间下的深度值)和Linear01Depth(返回一个范围在01的线性深度值)
如果需要获取深度+法线纹理,可以直接用tex2D对_CameraDepthNormalsTexture进行采样,用DecodeDepthNormal对采样结果解嘛,从而得到深度值(01的线性深度值,与单独的深度纹理中存储的深度值不同)和法线方向(视角空间下的)
查看深度和法线纹理:使用帧调试器,看到的深度纹理是非线性空间的深度值,而深度+法线纹理都是由unity编码后的结果:
显示出线性空间下的深度信息或解码后的法线方向会更有用,可以自行在片元着色器中输出转换或解码后的深度和法线值。
输出线性深度值:
float depth=SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); float linearDepth=Linear01Depth(depth); return fixed4(linearDepth,linearDepth,linearDepth,1.0);
输出法线方向:
fixed3 normal=DecodeViewNormalStereo(tex2D(_CameraDepthNormalsTexture, i.uv).xy); return fixed4(normal*0.5+0.5,1.0);
出现全黑全白的原因:覆盖了从近裁剪平面到远裁剪平面的所有深度区域。
深度图:
再谈运动模糊
前面生成的运动模糊效果是混合多张屏幕图像来模拟的,还可以使用速度映射图(存储每个像素的速度,使用这个速度来决定模糊的方向和大小)。
速度缓冲的生成有多种办法:
- 一种方法是把场景中所有物体的速度渲染到一张纹理中,但这种方法的缺点在于需要修改场景中所有物体的shader代码,使其添加计算速度的代码并输出到一个渲染纹理中。
- 一种方法是利用深度纹理在片元着色器中位每个像素计算其在世界空间下的位置,这是通过使用当前视角*投影矩阵的逆矩阵对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<Camera>();
}
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 "Unity Shaders Book/Chapter 13/Motion Blur With Depth Texture" {
Properties {
_MainTex ("Base (RGB)", 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);//当前帧在NDC下的坐标
// 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);//前一帧在NDC下的坐标
// 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
}
}
FallBack Off
}
使场景运动的代码和之前使用的一样。
效果:
全局雾效
unity内置的雾效(Fog)可以产生基于距离的线性或指数雾效。
要在自己编写的着色器中实现雾效,需要添加
#pragma multi_compile_fog
还需要使用相关的内置宏,缺点在于要为所有物体添加相关的渲染代码,而且实现效果有限,例如基于高度的雾效,采用屏幕后处理的全局雾效,自由性很高。
基于屏幕后处理的全局雾效关键:根据深度纹理来重建每个像素在世界空间下的坐标:之前运动模糊中先构建NDC再通过摄像机的视角*投影矩阵的逆矩阵来变换到世界空间下的像素坐标。这种方法需要在片元着色器中进行矩阵乘法运算,影响游戏性能。还可以采用一种快速重建的方法:首先对图像空间下的视锥体射线(摄像机出发,指向图像上某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息,然后把该射线的线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。
过程:
1、重建世界坐标:知道摄像机在世界空间下的位置,以及世界空间下该像素相对于摄像机的偏移量,把他们相加就可以得到该像素的世界坐标:
float4 worldPos=_WorldSpaceCameraPos + linearDepth * interpolateRay
linearDepth是深度纹理得到的线性深度值
2、interpolateRay是由顶点着色器输出并插值后得到的射线,来源于对近裁剪平面的四个角的某个特定向量的插值,这四个向量包含了他们到摄像机的方向和距离信息。
计算:
halfHeight=Near×tan ( FOV / 2 ) toTop=camera.up × halfHeight toRight=camera.right × halfHeight · aspect TL=camera.forward · Near + toTop - toRight TR=camera.forward · Near + toTop + toRight BL=camera.forward · Near - toTop - toRight BR=camera.forward · Near - toTop + toRight
由于得到的线性深度值不是点到摄像机的欧氏距离,而是在z方向上的距离
所以还需要把深度值转换成到摄像机的欧氏距离,以TL为例:
计算:depth/dist=Near/|TL|
dist=( |TL| / Near )× depth
屏幕后处理的原理是使用特定的材质去渲染一个刚好填充整个屏幕的四边形面片,这个四边形面片的四个顶点就对应了近剪裁平面的四个角,因此将上面的值传递给顶点着色器,顶点着色器根据当前的位置选择它所对应的向量,然后再将其输出,经过插值后传递给片元着色器得到imterpolateRay。
3、雾的计算:
简单雾效实现中,需要计算一个雾效系数f,作为混合原始颜色和雾颜色的混合系数。
float3 afterFog= f*fogColor + (1-f)*origColor
雾效系数f的计算有很多种,unity内置的雾效实现中,支持三种:线性+指数+指数的平方。
在给定距离z后,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<Camera>();
}
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 "Unity Shaders Book/Chapter 13/Fog With Depth Texture" {
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
}
}
FallBack Off
}
效果:
这里的实现是基于投影类型为透视投影的前提下,如果需要在正交投影的情况下重建世界坐标,需要不同的公式。
再谈边缘检测
之前使用sobel算子对屏幕图像进行边缘检测,但是直接利用颜色信息进行边缘检测会产生很多不希望得到的边缘线。在深度和法线纹理上进行边缘检测,这些图像不会受到纹理和光照的影响,而仅仅保存了当前渲染物体的模型信息,检测出来的边缘更可靠。
本次采用roberts算子进行边缘检测,使用的卷积核为:
该算子的本质是计算左上角和右下角的差值,乘以右上角和左下角的差值,作为评估边缘的依据。取对角方向的深度或法线值,比较他们之间的差值,如果超过某个阈值,就认为他们之间存在一条边。
相机脚本:
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<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
}
//默认情况下,该函数会在所有不透明和透明的pass执行完毕后被调用。
//为了不对透明的有影响,添加ImageEffectOpaque属性。
[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);
}
}
}
shader:
Shader "Unity Shaders Book/Chapter 13/Edge Detection Normals And Depth" {
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;
}
//计算对角线上两个纹理值得差值,返回要么0——存在边界,要么1
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
}
}
FallBack Off
}
效果:
场景中得所有物体都会被添加描边效果,如果希望只对特定物体描边,可以使用unity提供得graphics.DrawMesh或Graphics.DrawMeshNow函数把需要描边的物体再次渲染一遍(在所有不透明的物体渲染完毕后),然后再使用前面提到的边缘检测算法计算深度或法线纹理中每个像素的梯度值,判断他们是否小于某个阈值,如果是,就在shader中使用clip函数剔除掉该像素,从而显示出原来的物体颜色。
扩展
使用深度纹理还可以实现特定物体的描边、角色护盾、相交线的高光模拟等效果、屏幕空间的环境遮挡。