75.深度和法线纹理-效果实现-深度纹理实现运动模糊-具体实现
75.1 知识点
知识回顾
利用深度纹理实现运动模糊效果的基本原理是:
通过获取像素当前帧和上一帧在裁剪空间下的位置,利用这两个位置计算物体的运动方向,从而模拟出运动模糊的效果。
补充知识点:C# 设置矩阵变量给 Shader
我们需要通过 C# 代码为 Shader 设置矩阵变量。然而,ShaderLab 语法中的属性并没有直接的矩阵类型变量,因此,我们在 CG 语句中声明矩阵属性,通过材质球指明变量进行设置。
示例:
- 在 CG 代码中声明 4x4 矩阵:
float4x4 _ClipToWorldMatrix;
- 在 C# 代码中声明 4x4 矩阵:
Matrix4x4 frontClipToWorldMatrix;
- 通过材质指明变量设置矩阵:
material.SetMatrix("_ClipToWorldMatrix", frontClipToWorldMatrix);
- 在 CG 代码中声明 4x4 矩阵:
实现 利用深度纹理实现运动模糊屏幕后期处理效果 对应 Shader
主要步骤
新建Shader文件,取名 MotionBlurWithDepthTexture 深度纹理运动模糊效果
声明属性,进行属性映射
- 主纹理 _MainTex
- 模糊偏移量 _BlurSize
- 深度纹理 _CameraDepthTexture
- 当前帧裁剪到世界空间变换矩阵 float4x4 _ClipToWorldMatrix
- 上一帧世界到裁剪空间变换矩阵 float4x4 _FrontWorldToClipMatrix
屏幕后处理标配
- ZTest Always
- Cull Off
- ZWrite Off
结构体
- 顶点和uv坐标
顶点着色器
- 坐标转换 uv坐标赋值
片元着色器
- 得到裁剪空间下的两个点
- 得到点一
- 深度值获取
- 构建裁剪空间下组合坐标 uv 和 深度
- 得到点二
- 裁剪空间坐标转世界空间(注意进行齐次除法)
- 利用上一帧变换矩阵将世界空间坐标转裁剪空间(注意进行齐次除法)
- 得到运动方向
- 用当前帧点 - 上一帧点 得到运动方向
- 进行模糊处理
- 利用模糊偏移量变量进行3次偏移采样颜色后进行平均值计算
- 得到裁剪空间下的两个点
FallBack Off
新建Shader文件,取名 MotionBlurWithDepthTexture 深度纹理运动模糊效果,保留骨架
Shader "Unlit/Lesson75_MotionBlurWithDepthTexture"
{
Properties
{
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
ENDCG
}
}
}
声明属性,进行属性映射。包括主纹理和模糊偏移量、深度纹理、当前帧裁剪到世界空间变换矩阵和上一帧世界到裁剪空间变换矩阵。注意深度纹理的命名规范为_CameraDepthTexture,名字不一致的话无法正确获得深度值。
Properties
{
// 主纹理属性,默认白色
_MainTex ("Texture", 2D) = "white" {}
//用于控制模糊程度的 模糊偏移量
_BlurSize("BlurSize", Float) = 0.5
}
// 主纹理采样器
sampler2D _MainTex;
// 主纹理每个纹素的大小信息,用于一些纹理坐标相关的计算
float4 _MainTex_TexelSize;
// 存储模糊偏移量,用于控制模糊程度
fixed _BlurSize;
// 深度纹理采样器,用于获取场景深度信息
sampler2D _CameraDepthTexture;
// 裁剪空间到世界空间的变换矩阵,用于坐标变换
float4x4 _ClipToWorldMatrix;
// 上一帧世界空间到裁剪空间的变换矩阵,用于获取上一帧相关坐标信息
float4x4 _FrontWorldToClipMatrix;
屏幕后处理标配指令
// 始终进行深度测试
ZTest Always
// 关闭背面剔除
Cull Off
// 关闭深度写入
ZWrite Off
结构体声明顶点和 UV 坐标
struct v2f
{
// 主纹理的纹理坐标,用于在片元着色器中采样主纹理
float2 uv : TEXCOORD0;
// 顶点在裁剪空间中的位置,用于后续渲染等操作
float4 vertex : SV_POSITION;
};
顶点函数对坐标进行转换和对uv坐标赋值
// 顶点着色器函数
v2f vert(appdata_base appdata_base)
{
v2f v2f;
// 将顶点从模型空间转换到裁剪空间
v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
// 直接赋值主纹理的原始纹理坐标
v2f.uv = appdata_base.texcoord;
return v2f;
}
片元函数中,得到裁剪空间下的两个点,作差得到运动方向,最后基于运动方向进行模糊处理
// 片元着色器函数
fixed4 frag(v2f v2f) : SV_Target
{
// 1. 得到裁剪空间下的两个点相关操作
// 获取深度纹理在当前像素位置的深度值 这个深度值是裁剪空间下的
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, v2f.uv);
// 构建裁剪空间下的一个组合坐标,将0~1范围的纹理坐标变换到-1~1范围
// 此为当前帧的第一个点在裁剪空间中的坐标表示
float4 nowClipPos = float4(v2f.uv.x * 2 - 1, v2f.uv.y * 2 - 1, depth * 2 - 1, 1);
// 通过裁剪空间到世界空间的变换矩阵,将当前帧裁剪空间的点转换到世界空间,得到世界空间的坐标
float4 worldPos = mul(_ClipToWorldMatrix, nowClipPos);
// 进行透视的齐次除法,得到正确的世界空间坐标 否则w不是1的话可能不是正确的坐标
worldPos /= worldPos.w;
// 利用上一帧的变换矩阵,将世界空间的点转换回上一帧对应的裁剪空间下的点
// 此为上一帧的第二个点在裁剪空间中的坐标表示
float4 oldClipPos = mul(_FrontWorldToClipMatrix, worldPos);
// 对上一帧的点进行透视的齐次除法
oldClipPos /= oldClipPos.w;
// 2. 计算得到运动方向向量
// 我们只想得到屏幕上的移动方向 所以是2维的 使用uv采样只需要2维即可
float2 moveDir = nowClipPos.xy - oldClipPos.xy;
// 3. 基于运动方向进行模糊处理操作
float2 uv = v2f.uv;
float4 color = float4(0, 0, 0, 0);
for (int it = 0; it < 3; it++)
{
// 循环内进行多次纹理采样并累加颜色
// 第一次采样累加的是当前像素所在位置的颜色
// 第二次采样累加的是当前像素进行了 moveDir * _BlurSize 偏移后的颜色
// 第三次采样累加的是当前像素进行了 2*moveDir * _BlurSize 偏移后的颜色
color += tex2D(_MainTex, uv);
uv += moveDir * _BlurSize;
}
// 计算累加3次后颜色的平均值,实现模糊效果
color /= 3;
// 返回经过模糊处理后的颜色值
return fixed4(color.rgb, 1);
}
不使用回退Shader,即此Shader无法运行时不会切换到其他Shader
// 不使用回退Shader,即此Shader无法运行时不会切换到其他Shader
Fallback Off
实现 利用深度纹理实现运动模糊屏幕后期处理效果 对应 C#
主要步骤
- 创建C#代码,命名和Shader一样
- 继承屏幕后处理基类PostEffectBase
- 声明模糊偏移量变量和用于记录上一次变换矩阵的变量
- 重写OnRenderImage函数,在其中进行属性设置,变换矩阵计算,屏幕后处理
- 在生命周期函数中启用深度纹理,初始化上一帧变换矩阵
创建C#代码,命名和Shader一样,继承屏幕后处理基类PostEffectBase,声明模糊偏移量变量和用于记录上一次变换矩阵的变量
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson75_MotionBlurWithDepthTexture : PostEffectBase
{
//模糊程度
[Range(0, 1)] public float blurSize = 0.5f;
//用于记录上一次的变换矩阵的变量
private Matrix4x4 frontWorldToClipMatrix;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
//设置模糊程度
material.SetFloat("_BlurSize", blurSize);
//设置上一帧世界空间到裁剪空间的矩阵
material.SetMatrix("_FrontWorldToClipMatrix", frontWorldToClipMatrix);
//计算这一帧的变换矩阵
frontWorldToClipMatrix = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix;
//设置这一帧的 裁剪到世界空间的变换矩阵
material.SetMatrix("_ClipToWorldMatrix", frontWorldToClipMatrix.inverse);
//进行屏幕后期处理
Graphics.Blit(source, destination, material);
}
else
{
Graphics.Blit(source, destination);
}
}
}
重写OnRenderImage函数,在其中进行属性设置,变换矩阵计算,屏幕后处理。注意在Start中启用深度纹理,OnEnable时初始化上一帧变换矩阵避免上一变换矩阵为空
private void Start()
{
//开启深度纹理
Camera.main.depthTextureMode = DepthTextureMode.Depth;
//初始化上一次的变换矩阵 用 观察到裁剪变换矩阵(摄像机的透视矩阵) * 世界到观察变换矩阵
//得到的 就是 世界空间到裁剪空间的变换矩阵
//frontWorldToClipMatrix = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix;
}
private void OnEnable()
{
//有时我们会在界面上让脚本失活,每次激活时 可以初始化一次
frontWorldToClipMatrix = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix;
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
//设置模糊程度
material.SetFloat("_BlurSize", blurSize);
//设置上一帧世界空间到裁剪空间的矩阵
material.SetMatrix("_FrontWorldToClipMatrix", frontWorldToClipMatrix);
//计算这一帧的变换矩阵
frontWorldToClipMatrix = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix;
//设置这一帧的 裁剪到世界空间的变换矩阵
material.SetMatrix("_ClipToWorldMatrix", frontWorldToClipMatrix.inverse);
//进行屏幕后期处理
Graphics.Blit(source, destination, material);
}
else
{
Graphics.Blit(source, destination);
}
}
挂载脚本到摄像机并关联 Shader,观察运动模糊效果
其他注意点
考虑不同平台可能存在的垂直翻转问题,使用纹素判断是否要翻转。同时深度采样时用纹素算出来的深度去采样深度纹理,当然主纹理还用原来成uv
Shader "Unlit/Lesson75_MotionBlurWithDepthTexture"
{
Properties
{
//...
}
SubShader
{
Pass
{
//...
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// 主纹理采样器
sampler2D _MainTex;
// 主纹理每个纹素的大小信息,用于一些纹理坐标相关的计算
float4 _MainTex_TexelSize;
//...
struct v2f
{
// 主纹理的纹理坐标,用于在片元着色器中采样主纹理
float2 uv : TEXCOORD0;
// 深度纹理的纹理坐标,用于在片元着色器中采样深度纹理
float2 uv_depth : TEXCOORD1;
// 顶点在裁剪空间中的位置,用于后续渲染等操作
float4 vertex : SV_POSITION;
};
// 顶点着色器函数
v2f vert(appdata_base appdata_base)
{
v2f v2f;
// 将顶点从模型空间转换到裁剪空间
v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
// 直接赋值主纹理的原始纹理坐标
v2f.uv = appdata_base.texcoord;
// 同样赋值深度纹理的原始纹理坐标
v2f.uv_depth = appdata_base.texcoord;
// 在多平台情况下,根据纹理坐标起始位置进行调整,保证深度纹理采样正确
// 当纹理坐标在顶部开始时且主纹理纹素高度小于0,翻转深度纹理的y坐标
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
v2f.uv_depth.y = 1 - v2f.uv_depth.y;
#endif
return v2f;
}
// 片元着色器函数
fixed4 frag(v2f v2f) : SV_Target
{
// 1. 得到裁剪空间下的两个点相关操作
// 获取深度纹理在当前像素位置的深度值 这个深度值是裁剪空间下的
// float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, v2f.uv);
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, v2f.uv_depth);
//...
}
ENDCG
}
}
//...
}
片元函数中让移动方向向量除以2,从而降低运动模糊效果的强度,不要过于强烈
// 2. 计算得到运动方向向量
// 我们只想得到屏幕上的移动方向 所以是2维的 使用uv采样只需要2维即可
// 除以二效果会好点 这是前辈总结出来的经验
float2 moveDir = (nowClipPos.xy - oldClipPos.xy) / 2;
75.2 知识点代码
Lesson75_深度和法线纹理_效果实现_深度纹理实现运动模糊_具体实现.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson75_深度和法线纹理_效果实现_深度纹理实现运动模糊_具体实现 : MonoBehaviour
{
void Start()
{
#region 知识回顾
//利用深度纹理实现运动模糊效果 的基本原理是
//得到像素当前帧和上一帧中在裁剪空间下的位置,
//利用两个位置计算出物体的运动方向,从而模拟出运动模糊的效果
#endregion
#region 补充知识点 C#设置矩阵变量给Shader
//我们需要通过C#代码为Shader设置矩阵变量
//但是ShaderLab语法中的属性中并没有矩阵类型的变量
//因此我们只需要在CG语句中声明矩阵属性即可
//这样C#中通过矩阵的属性名同样可以进行设置
//举例:
//CG代码中声明4x4矩阵 —— float4x4 _ClipToWorldMatrix;
//C#代码中声明4x4矩阵 —— Matrix4x4 frontClipToWorldMatrix;
//通过材质球指明变量进行设置即可
//material.SetMatrix("_ClipToWorldMatrix", frontClipToWorldMatrix);
#endregion
#region 知识点一 实现 利用深度纹理实现运动模糊屏幕后期处理效果 对应 Shader
//1.新建Shader文件,取名 MotionBlurWithDepthTexture 深度纹理运动模糊效果
//2.声明属性,进行属性映射
// 主纹理 _MainTex
// 模糊偏移量 _BlurSize
// 深度纹理 _CameraDepthTexture
// 当前帧裁剪到世界空间变换矩阵 float4x4 _ClipToWorldMatrix
// 上一帧世界到裁剪空间变换矩阵 float4x4 _FrontWorldToClipMatrix
//3.屏幕后处理标配
// ZTest Always
// Cull Off
// ZWrite Off
//4.结构体
// 顶点和uv坐标
//5.顶点着色器
// 坐标转换 uv坐标赋值
//6.片元着色器
// 6-1:得到裁剪空间下的两个点
// 得到点一
// 深度值获取
// 构建裁剪空间下组合坐标 uv 和 深度
// 得到点二
// 裁剪空间坐标转世界空间(注意进行齐次除法)
// 利用上一帧变换矩阵将世界空间坐标转裁剪空间(注意进行齐次除法)
// 6-2:得到运动方向
// 用当前帧点-上一帧点 得到运动方向
// 6-3:进行模糊处理
// 利用模糊偏移量变量进行3次偏移采样颜色后进行平均值计算
//7.FallBack Off
#endregion
#region 知识点二 实现 利用深度纹理实现运动模糊屏幕后期处理效果 对应 C#
//1.创建C#代码,命名和Shader一样
//2.继承屏幕后处理基类PostEffectBase
//3.声明模糊偏移量变量和用于记录上一次变换矩阵的变量
//4.重写OnRenderImage函数
// 在其中进行属性设置,变换矩阵计算,屏幕后处理
//5.在生命周期函数中启用深度纹理,初始化上一帧变换矩阵
#endregion
#region 知识点三 其他注意点
//1.考虑不同平台可能存在的垂直翻转问题、
// #if UNITY_UV_STARTS_AT_TOP
// if (_MainTex_TexelSize.y < 0)
// o.uv_depth.y = 1 - o.uv_depth.y;
// #endif
//2.让移动方向向量除以2
// 从而降低运动模糊效果的强度,不要过于强烈
#endregion
}
}
Lesson75_MotionBlurWithDepthTexture.shader
Shader "Unlit/Lesson75_MotionBlurWithDepthTexture"
{
Properties
{
// 主纹理属性,默认白色
_MainTex ("Texture", 2D) = "white" {}
//用于控制模糊程度的 模糊偏移量
_BlurSize("BlurSize", Float) = 0.5
}
SubShader
{
Pass
{
// 始终进行深度测试
ZTest Always
// 关闭背面剔除
Cull Off
// 关闭深度写入
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// 主纹理采样器
sampler2D _MainTex;
// 主纹理每个纹素的大小信息,用于一些纹理坐标相关的计算
float4 _MainTex_TexelSize;
// 存储模糊偏移量,用于控制模糊程度
fixed _BlurSize;
// 深度纹理采样器,用于获取场景深度信息
sampler2D _CameraDepthTexture;
// 裁剪空间到世界空间的变换矩阵,用于坐标变换
float4x4 _ClipToWorldMatrix;
// 上一帧世界空间到裁剪空间的变换矩阵,用于获取上一帧相关坐标信息
float4x4 _FrontWorldToClipMatrix;
struct v2f
{
// 主纹理的纹理坐标,用于在片元着色器中采样主纹理
float2 uv : TEXCOORD0;
// 深度纹理的纹理坐标,用于在片元着色器中采样深度纹理
float2 uv_depth : TEXCOORD1;
// 顶点在裁剪空间中的位置,用于后续渲染等操作
float4 vertex : SV_POSITION;
};
// 顶点着色器函数
v2f vert(appdata_base appdata_base)
{
v2f v2f;
// 将顶点从模型空间转换到裁剪空间
v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
// 直接赋值主纹理的原始纹理坐标
v2f.uv = appdata_base.texcoord;
// 同样赋值深度纹理的原始纹理坐标
v2f.uv_depth = appdata_base.texcoord;
// 在多平台情况下,根据纹理坐标起始位置进行调整,保证深度纹理采样正确
// 当纹理坐标在顶部开始时且主纹理纹素高度小于0,翻转深度纹理的y坐标
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
v2f.uv_depth.y = 1 - v2f.uv_depth.y;
#endif
return v2f;
}
// 片元着色器函数
fixed4 frag(v2f v2f) : SV_Target
{
// 1. 得到裁剪空间下的两个点相关操作
// 获取深度纹理在当前像素位置的深度值 这个深度值是裁剪空间下的
// float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, v2f.uv);
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, v2f.uv_depth);
// 构建裁剪空间下的一个组合坐标,将0~1范围的纹理坐标变换到-1~1范围
// 此为当前帧的第一个点在裁剪空间中的坐标表示
float4 nowClipPos = float4(v2f.uv.x * 2 - 1, v2f.uv.y * 2 - 1, depth * 2 - 1, 1);
// 通过裁剪空间到世界空间的变换矩阵,将当前帧裁剪空间的点转换到世界空间,得到世界空间的坐标
float4 worldPos = mul(_ClipToWorldMatrix, nowClipPos);
// 进行透视的齐次除法,得到正确的世界空间坐标 否则w不是1的话可能不是正确的坐标
worldPos /= worldPos.w;
// 利用上一帧的变换矩阵,将世界空间的点转换回上一帧对应的裁剪空间下的点
// 此为上一帧的第二个点在裁剪空间中的坐标表示
float4 oldClipPos = mul(_FrontWorldToClipMatrix, worldPos);
// 对上一帧的点进行透视的齐次除法
oldClipPos /= oldClipPos.w;
// 2. 计算得到运动方向向量
// 我们只想得到屏幕上的移动方向 所以是2维的 使用uv采样只需要2维即可
// 除以二效果会好点 这是前辈总结出来的经验
float2 moveDir = (nowClipPos.xy - oldClipPos.xy) / 2;
// 3. 基于运动方向进行模糊处理操作
float2 uv = v2f.uv;
float4 color = float4(0, 0, 0, 0);
for (int it = 0; it < 3; it++)
{
// 循环内进行多次纹理采样并累加颜色
// 第一次采样累加的是当前像素所在位置的颜色
// 第二次采样累加的是当前像素进行了 moveDir * _BlurSize 偏移后的颜色
// 第三次采样累加的是当前像素进行了 2*moveDir * _BlurSize 偏移后的颜色
color += tex2D(_MainTex, uv);
uv += moveDir * _BlurSize;
}
// 计算累加3次后颜色的平均值,实现模糊效果
color /= 3;
// 返回经过模糊处理后的颜色值
return fixed4(color.rgb, 1);
}
ENDCG
}
}
// 不使用回退Shader,即此Shader无法运行时不会切换到其他Shader
Fallback Off
}
Lesson75_MotionBlurWithDepthTexture.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson75_MotionBlurWithDepthTexture : PostEffectBase
{
//模糊程度
[Range(0, 1)] public float blurSize = 0.5f;
//用于记录上一次的变换矩阵的变量
private Matrix4x4 frontWorldToClipMatrix;
private void Start()
{
//开启深度纹理
Camera.main.depthTextureMode = DepthTextureMode.Depth;
//初始化上一次的变换矩阵 用 观察到裁剪变换矩阵(摄像机的透视矩阵) * 世界到观察变换矩阵
//得到的 就是 世界空间到裁剪空间的变换矩阵
//frontWorldToClipMatrix = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix;
}
private void OnEnable()
{
//有时我们会在界面上让脚本失活,每次激活时 可以初始化一次
frontWorldToClipMatrix = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix;
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
//设置模糊程度
material.SetFloat("_BlurSize", blurSize);
//设置上一帧世界空间到裁剪空间的矩阵
material.SetMatrix("_FrontWorldToClipMatrix", frontWorldToClipMatrix);
//计算这一帧的变换矩阵
frontWorldToClipMatrix = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix;
//设置这一帧的 裁剪到世界空间的变换矩阵
material.SetMatrix("_ClipToWorldMatrix", frontWorldToClipMatrix.inverse);
//进行屏幕后期处理
Graphics.Blit(source, destination, material);
}
else
{
Graphics.Blit(source, destination);
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com