深度纹理
深度纹理实际就是一张渲染纹理,只不过它里边存储的是像素值不是颜色值,而是一个高精度的深度值。
顶点座标转化到NDC(归一化的设备座标)下的座标的z分量就是顶点的深度值。NDC中,z分量范围在[-1,1],为了让z分量可以存到一张纹理中,需要使用公式将z分量映射:
d = 0.5 * + 0.5
Unity获取深度纹理方式分为两种方式:
在Unity中,可以让摄像机生成一张深度纹理或是深度+法线纹理。
1.当只生成一张深度纹理时,Unity在使用延迟渲染路径时(渲染路径决定了光照如何应用到Shader中,决定了把光源信息和处理后的光照信息放到那些数据中。),直接从G-buffer中获取深度缓存。
2.在使用前向渲染路劲时,渲染深度纹理通过使用着色器替换技术,渲染需要的不透明物体,并使用它投射阴影时使用的Pass(即LightMode 设置为ShadowCaster 的Pass)来得到深度纹理。具体实现是,Unity会使用着色器替换技术渲染那些渲染类型(即SubShader的RenderType标签)为Opaque的物体,判断它们使用的渲染队列是否 <= 2500,如果满足,就把它渲染到深度和法线纹理中。
深度纹理的精度通常是24位活16位。如果选择生成一张深度+法线纹理,Unity会创建一张和屏幕分辨率相同,精度为32位(每个通道8位)的纹理。法线信息存到R和G通道。深度信息存到B和A通道。
- 使用延迟渲染路径时:Unity只需要合并深度和法线缓存即可。
- 使用前向渲染路劲时:默认不会创建法线缓存。因此Unity底层使用了一个单独的Pass把整个场景再次渲染一遍。这个Pass被放在一个内置Shader中。
代码具体获取深度纹理步骤:
- camera.depehtTextureMode = DepthTextureMode.Depth; //获取深度纹理
- Shader中直接访问特定纹理名称(_CameraDepthTexture)即可。
camera.depehtTextureMode = DepthTextureMode.DepthNormals;//获取深度纹理 + 法线纹理
camera.depehtTextureMode |= DepthTextureMode.Depth;
camera.depehtTextureMode = DepthTextureMode.DepthNormals; 以上两句获取一张深度纹理 + 一张深度+法线纹理
采样深度纹理,非线性变线性推导。。。以后补上。。
当摄像机远裁剪平面的距离过大,会导致距离摄像机较近的物体被映射的深度值非常小,生成的深度纹理变"黑"。相反,远裁剪平面过大,会导致距离摄像机较近的物体被映射的深度值偏大。生成的深度纹理变"白"。
实践:
1.运动模糊
速度映射图模拟运动模糊。速度映射图中存储了每个像素的速度,然后使用这个速度决定模糊的方向和大小。
生成速度映射纹理有两种方式:
- 把场景中所有物体都渲染到一张纹理中,不过这种方法需要修改场景所有的Shader。
- 使用深度纹理在片元着色器中为每个像素计算世界空间下的位置(使用当前帧的视角*投影矩阵的逆矩阵计算)。当得到世界空间下的顶点座标后,然后使用前一帧视角*投影矩阵对其进行变换,得到该顶点位置在前一帧的NDC座标。当前帧顶点的NDC座标 - 前一帧的NDC位置得到该像素的速度。优点是屏幕后统一处理计算,缺点是两次矩阵变换,效率低。
Shader "Chan/Chapter 13/Motion Blur With Depth Texture" {
Properties
{
_MainTex("Base Tex",2D) = "white"{}
_BlurSize("Blur Size",float) = 1.0
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
//通过_CameraDepthTexture 直接就能访问到当前摄像机的深度纹理
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;
//多张渲染纹理,可能uv采样座标会出问题 需要根据平台判断
# 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
{
//不同平台要通过uv采样深度纹理得到深度值,可能有差异。使用宏定义 内部处理了。SAMPLE_DEPTH_TEXTURE
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth);
//归一化设备座标系下的座标[-1,1],纹理通道值[0,1] 所以存储到深度纹理时,进行了映射。此处为反映射
//归一化设备座标系下的座标座标(x,y值由uv映射而来,z值由深度值映射而来)
float4 h = float4(i.uv.x * 2 - 1,i.uv.y * 2 - 1,d * 2 - 1,1);
//归一化设备座标系下的座标 乘以当前摄像机 * 投影矩阵的逆矩阵 得到世界空间座标系的座标
float4 D = mul(_CurrentViewProjectionInverseMatrix,h);
//世界空间座标系的座标
float4 worldPos = D / D.w;
//当前世界座标乘以 当前摄像机视角 * 投影矩阵 得到上一帧时候在归一化设备座标系下的座标
float4 currentPos = h;
float4 previousPos = mul(_PreviousViewProjectionMatrix,worldPos);
previousPos /= previousPos.w;
//得到速度
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
}
配合使用的C#脚本:
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;
//1.当前帧的(视角 * 投影矩阵)的逆矩阵,用于将顶点座标从归一化的设备座标变换到世界空间下
//2.世界空间下的座标,经过上一帧的 摄像机视角 * 投影矩阵转换,得到上一帧的归一化设备座标
//3.当前帧的归一化设备空间座标 - 上一帧的归一化设备空间座标 = 当前顶点的速度
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
previousViewProjectionMatrix = currentViewProjectionMatrix;
Graphics.Blit (src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
注解:
归一化设备座标系(NDC)下的座标(x,y值由uv映射而来,z值由深度值映射而来)
NDC(归一化的设备座标)下的座标的z分量就是顶点的深度值。NDC中,z分量范围在[-1,1],为了让z分量可以存到一张纹理中,需要使用公式将z分量映射:
d = 0.5 * + 0.5
因此采样深度纹理后,需要进行反映射: = d * 2 - 1
2.全局雾效
基于屏幕后处理。根据深度纹理来重建每个像素在世界空间中的位置。
2.1重建世界座标
float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;首先获取世界空间下的摄像机位置WorldSpaceCameraPos 。linearDepth * interpolatedRay 该像素相对于摄像机位置的偏移量。
- 摄像机位置WorldSpaceCameraPos 。
- linearDepth = 深度纹理得到的线性深度值。 = d * 2 - 1
- interpolatedRay 顶点着色器输出并插值后得到的射线,它不仅包含了该像素到摄像机的方向,也包含了距离信息。
scale = |TL|/Near
2.2雾的计算
使用一个雾效系数 f ,混合原始颜色和雾的颜色。
float3 afterFog = f * fogColor + ( 1 - f ) * origColor;
Unity内置雾效,有三种方式计算次数f:
- Linear(线性):
dmax 和 dmin表示受雾影响的最大最小距离。
- Exponential(指数):
d 控制雾的浓度。 b
- Exponential Squared(指数):
d 控制雾的浓度。
本文采样类似线性雾效计算方式。
Hend 和 Hsart分辨代表受雾影响的起始高度和终止高度。
Shader "Chan/Chapter 13/Fog With Depth Texture" {
Properties
{
_MainTex("Main Tex",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 interpolateRay: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
//屏幕后全局所用的模型是一个四边形网格,只包含四个顶点
//判断当前顶点是哪个顶点,此处使用uv纹理座标判断。左下->右下->右上->左上
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;
}
//平台,设置差异 可能导致左下->右下->右上->左上错乱,跟c#脚本对不上,差异化处理
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
{
index = 3 - index;
}
#endif
o.interpolateRay = _FrustumCornersRay[index];
return o;
}
fixed4 frag(v2f i) :SV_Target
{
//不同平台要通过uv采样深度纹理得到深度值,可能有差异。使用宏定义 内部处理了。SAMPLE_DEPTH_TEXTURE
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth));
//计算得到某个像素点的世界座标
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolateRay.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
}
注解:
//不同平台要通过uv采样深度纹理得到深度值,可能有差异。使用宏定义 内部处理了。SAMPLE_DEPTH_TEXTURE
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth));
//计算得到某个像素点的世界座标
//摄像机世界座标 + 像素点相对于摄像机的偏移量(linearDepth * i.interpolateRay.xyz) = 像素的世界座标
//linearDepth * i.interpolateRay.xyz = dist = depth * scale
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolateRay.xyz;
配合使用的C#脚本:
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);
//将摄像机近裁剪平面的 左下->右下->右上->左上 计算出来后,传入shader中
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);
}
}
}
3.边缘检测
使用Sobel卷积算子进行边缘,物体的纹理,阴影可以能被描边。使用Roberts算子来替代。
Roberts算子本质是计算左上角-右下角的差值,乘以右上角-左下角的差值,作为评估边缘的依据。
具体代码操作:
通过uv采样深度+法线纹理的某点的相邻上下左右四点信息,然后比较左上-右下,右上-左下的深度值和 "法线"值,当深度值和法线值小于某个阈值,证明此点为边缘点。
Shader "Chan/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
//xy分量 分别对应了法线和深度的检测灵敏度
_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) {
//因为值比较两点的法线差值,因此不需要吧xy分量转化为整整的法线
half2 centerNormal = center.xy;
//将存在纹理信息zw分量中的深度值转化为float
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;
//传入的两个参数的“法线”差值小于0.1,判定法线有差异
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
// difference in depth
float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
//传入的两个参数的深度差值小于0.1,判定深度有差异
int isSameDepth = diffDepth < 0.1 * centerDepth;
// return:
// 1 - 差异比较小,不判定为边缘点
// 0 - 差异比较大,判定为边缘点
return isSameNormal * isSameDepth ? 1.0 : 0.0;
}
fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target {
//通过uv 采样四个点
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
}
配合使用的C#脚本:
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;
}
[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);
}
}
}