一轮整理:2021年6月25日09:52:09
二轮整理:2021年11月4日17:26:23
作者:聪头
笔记主题和书本主题并非一一对应
Ch12.屏幕后处理效果
12.1 后处理脚本系统
重要接口 OnRenderImage
要实现屏幕后处理效果的基础在于得到渲染后的屏幕图像,即抓取屏幕,而Unity提供了这样一个方便的接口:OnRenderImage函数。声明如下:
MonoBehaviour.OnRenderImage(RenderTexture src, RenderTexture dest)
当我们在脚本中声明此函数后,Unity会把当前渲染得到的图像存储在第一个参数对应的源渲染纹理中,通过函数中的一系列操作后,再渲染到目标纹理,即第二个参数对应的渲染纹理显示到屏幕上。在OnRenderImage函数中,我们通常是利用Graphics.Blit函数来完成对渲染纹理的处理。它有3种函数声明:
参数解析:
- src:源纹理,在屏幕后处理技术中,这个参数通常就是当前屏幕的渲染纹理或上一步处理后得到的渲染纹理
- dest:目标渲染纹理,如果它的值为null,就会直接将结果显示在屏幕上
- mat:使用的材质,这个材质使用Unity Shader将会进行各种屏幕后处理操作,而src纹理将会被传递给Shader中名为_MainTex的纹理属性
- pass:默认为-1,表示将会一次调用Shader内所有Pass。否则,只会调用给定索引的Pass(第一个Pass下标从0开始)
调用时机
- OnRenderImage函数会在所有不透明和透明的Pass执行完毕后被调用,以便对场景中所有游戏对象都产生影响
- 若我们希望在不透明的Pass执行完毕后立即调用OnRenderImage函数,可以在OnRenderImage函数前添加ImageEffectOpaque属性来实现这样的目的
处理过程
- 需要在摄像机中添加一个用于屏幕后处理的脚本。在这个脚本中,我们会实现OnRenderImage函数来获取当前屏幕的渲染纹理
- 检查一系列条件是否满足,如平台是否支持渲染纹理和屏幕特效(Unity2019必支持),是否支持当前使用的Unity Shader等
- 调用Graphics.Blit函数使用特定的Unity Shader 来对当前图像进行处理,再把返回的渲染纹理显示到屏幕上
- 对于一些复杂的屏幕特效,我们可能需要多次重复3
脚本:后处理系统基类
- 截止2019.4版本,许多书中的判断已经过时,下面给出自己修改后的最新代码
/****************************************************
文件:CTPostEffectsBase.cs
作者:聪头
邮箱: 1322080797@qq.com
日期:2021/11/02 15:02
功能:屏幕后处理系统基类
*****************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode] //编辑器有效
[RequireComponent(typeof(Camera))] //需要Camera组件
public class CTPostEffectsBase : MonoBehaviour
{
//根据Shader生成material
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material)
{
//如果shader为空,返回null
if (shader == null) return null;
//如果shader支持,材质存在且材质的shader为该shader,返回该材质
if (shader.isSupported && material && material.shader == shader)
return material;
//如果shader不支持,返回null
if (!shader.isSupported)
{
return null;
}
else
{
//shader支持,但没有相应材质,就创建材质
material = new Material(shader);
if (material)//成功创建,则返回该材质
return material;
else return null;
}
}
}
Shader:共同点
Pass设置
Pass {
ZTest Always
Cull Off
//关闭深度写入,防止它挡住其后面被渲染的物体
//如仅对不透明物体做后处理,关闭深度写入可以避免挡住透明物体
Zwrite Off
......
}
屏幕后处理实际上是在场景中绘制了一个与屏幕同宽高的四边形面片
顶点着色器
- 屏幕特效使用的顶点着色器代码通常比较简单,只需要进行必须的顶点变换。重要是传递正确纹理坐标
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
实战12-1:亮度、饱和度和对比度
脚本
/****************************************************
文件:CTBrightnessSaturationAndContrast.cs
作者:聪头
邮箱: 1322080797@qq.com
日期:2021/11/02 15:13
功能:修改屏幕亮度、饱和度和对比度
*****************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CTBrightnessSaturationAndContrast : CTPostEffectsBase
{
public Shader briSatConShader;
private Material briSatConMaterial;
public Material material
{
get
{
briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
return briSatConMaterial;
}
}
[Range(0.0f, 3.0f)]
public float brightness = 1.0f;//亮度
[Range(0.0f, 3.0f)]
public float saturation = 1.0f;//饱和度
[Range(0.0f, 3.0f)]
public float contrast = 1.0f;//对比度
//每帧调用,用于屏幕渲染的事件
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
//给Shader赋值
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
//将原纹理通过material修改后输出到屏幕
Graphics.Blit(src, dest, material);
}
else
{
//材质为空,不做处理
Graphics.Blit(src, dest);
}
}
}
Shader
Shader "Unity Shaders Book/Chapter 12/CT_BrightnessSaturationAndContrast"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Brightness ("Brightness", Float) = 1
_Saturation ("Saturation", Float) = 1
_Contrast ("Contrast", Float) = 1
}
SubShader
{
Pass {
//后处理标配
ZTest Always
Cull Off
//关闭深度写入,防止它挡住其后面被渲染的物体
//如仅对不透明物体做后处理,关闭深度写入可以避免挡住透明物体
Zwrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
half _Brightness;
half _Saturation;
half _Contrast;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
//采样屏幕图像
fixed4 renderTex = tex2D(_MainTex, i.uv);
//亮度
fixed3 finalColor = renderTex.rgb * _Brightness;
//饱和度
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b; //线性空间
//fixed luminance = 0.3 * renderTex.r + 0.59 * renderTex.g + 0.11 * renderTex.b; //sRGB空间
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
finalColor = lerp(luminanceColor, finalColor, _Saturation);
//对比度
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor, renderTex.a);
//return fixed4(luminanceColor, 1.0);
}
ENDCG
}
}
FallBack Off
}
lerp的本质!!!
参考:https://zhuanlan.zhihu.com/p/73487722
等价于 float3 lerp(float3 a, float3 b, float w) { return a + w*(b-a); }
最终效果
实战12-2:边缘检测
原理:使用卷积核(通常包含两个,分别为水平和竖直),对每个像素分别进行一次卷积计算,得到两个方向上的梯度值Gx和Gy,从而得到整体梯度来判断是否为边缘(梯度越大,越是边缘)
使用场景:一个系统,输入不稳定,输出稳定,用卷积求系统存量
(一维)卷积公式:
应用:二维图像卷积
- 视频20min处
G函数旋转180度后才是卷积核,卷积核是能够直接扣在图像上的,即图像坐标的(-1,-1)对应卷积核的(-1,-1)位置的值
脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyEdgeDetection : MyPostEffectsBase {
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;//控制边缘与背景的混合方式,0则边缘与原图混合,1则边缘与背景色混合
public Color edgeColor = Color.black;//边缘颜色
public Color backgroundColor = Color.white;//背景色
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
material.SetFloat("_EdgeOnly", edgesOnly);//值为0,边缘会叠加在原渲染图上;值为1,只显示边缘,不显示原渲染图像
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(source, destination, material);
}
else
{
Graphics.Blit(source, destination);
}
}
}
Shader
Shader "Unity Shaders Book/Chapter 12/CT_EdgeDetection"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}//处理前的屏幕纹理,unity会自动赋值
_EdgeOnly ("Edge Only", Float) = 1//边缘和背景的混合方式,1为边缘和原图混合,0为边缘与背景混合
_EdgeColor ("Edge Color", Color) = (0,0,0,1)//边缘颜色
_BackgroundColor ("Background Color", Color) = (1,1,1,1)//背景颜色
}
SubShader
{
Pass
{
//屏幕后处理标配:开启深度测试,双面渲染,关闭深度写入
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;//Unity内定义,计算纹素,即1/纹理尺寸
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
struct v2f
{
float4 pos : SV_POSITION;
half2 uv[9] : TEXCOORD0; //9个相邻位置的纹理坐标
};
///顶点着色器
v2f vert (appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
//获得该顶点周围9个相邻采样坐标
//以uv坐标为轴,顶点为中心赋值(从左到右,从下到上)
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
//得到光亮度,该颜色的黑白灰信息
fixed luminance(fixed4 color)
{
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
//Sobel算法
half Sobel(v2f i)
{
//构造Sobel算子
//水平卷积核
const half Gx[9] = {
-1, -2, -1,
0, 0, 0,
1, 2, 1
};
//竖直卷积核
const half Gy[9] = {
-1, 0, 1,
-2, 0, 2,
-1, 0, 1
};
half texColor;
half edgeX = 0;
half edgeY = 0;
//使用两个卷积核计算梯度
for(int it = 0; it < 9; it++)
{
texColor = luminance(tex2D(_MainTex, i.uv[it]));
edgeX += texColor * Gx[it];//计算水平梯度
edgeY += texColor * Gy[it];//计算竖直梯度
}
//总梯度 = 水平梯度绝对值 + 竖直梯度绝对值
half edge = 1 - abs(edgeX) - abs(edgeY);//1 - 总梯度
return edge;//edge值越小,代表梯度越大,越是边缘
}
///片元着色器
fixed4 frag (v2f i) : SV_Target
{
half edge = Sobel(i);
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);//边缘和原图混合的结果
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);//边缘和背景色混合的结果
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);//控制边缘与背景的混合权重
}
ENDCG
}
}
}
最终效果
实战12-3:高斯模糊
- 图片降低分辨率,再使用卷积核,分别对水平和竖直进行卷积操作,迭代几次,最终起到高斯模糊的效果
脚本
/****************************************************
文件:CTGaussianBlur.cs
作者:聪头
邮箱: 1322080797@qq.com
日期:2021/11/02 17:13
功能:高斯模糊
*****************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CTGaussianBlur : CTPostEffectsBase
{
public Shader gaussianBlur;
private Material gaussianBlurMaterial = null;
public Material material
{
get
{
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlur, gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}
[Range(0, 4)] public int iterations = 3;//模糊迭代次数,次数越多,图像越模糊
[Range(0.2f, 3.0f)] public float blurSpread = 0.6f;//模糊扩散,越大越模糊
[Range(1, 8)] public int downSample = 2;//降采样系数,越大采样图像越小,性能越好,越模糊
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
//缩放宽和高
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//生成降采样后大小的临时纹理
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;//过滤模式设为双线性
//将源屏幕纹理渲染至buffer0
Graphics.Blit(source, buffer0);
//高斯模糊的迭代,次数越多越模糊,越耗性能
for (int i = 0; i < iterations; i++)
{
//_BlurSize越大越模糊,过大可能导致虚影,不真实
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
//再生成一张临时纹理buffer1
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//使用第一个Pass,即使用竖直方向的一维高斯核进行滤波,将处理后的结果存在buffer1
Graphics.Blit(buffer0, buffer1, material, 0);
//将处理后的图像给buffer0,buffer1作为下次目标
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//使用第二个Pass,即使用水平方向的一维高斯核进行滤波
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
//将结果输出
Graphics.Blit(buffer0, destination);
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(source, destination);
}
}
}
Shader
Shader "Unity Shaders Book/Chapter 12/CT_GaussianBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader
{
CGINCLUDE //把Pass中的公共的字段,结构和方法等声明在CGINCLUDE代码块中
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5] : TEXCOORD0;
};
//竖直卷积 顶点着色器
v2f vertBlurVertical(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
//卷积核的uv采样,对称采样
o.uv[0] = uv;
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
//水平卷积 顶点着色器
v2f vertBlurHorizontal(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
//片元着色器,实现高斯模糊
fixed4 fragBlur(v2f i) : SV_Target
{
float weight[3] = {0.4026, 0.2442, 0.0545};//卷积核,因为对称,故只需3个
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];//中心点卷积
//其余各点卷积
for(int it = 1; it < 3; it++)
{
//根据对称性,该核的对称步长为2,故*2
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1.0);
}
ENDCG
ZTest Always Cull Off ZWrite Off
//处理竖直方向
Pass {
NAME "GAUSSIAN_BLUR_VERTICAL" //后后面的Bloom效果作铺垫
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
//处理水平方向
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
CGINCLUDE用法!!!
- 类似C++中头文件的功能,上面例子下可以避免我们编写两个完全一样的frag函数
SubShader {
CGINCLUDE
...
ENDCG
...
}
最终效果
实战12-4:bloom效果
实现思路:首先根据一个阈值提取出图像中的较亮区域,把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散效果,最后再将其和原图像进行混合,得到最终效果
脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyBloom : MyPostEffectsBase
{
public Shader bloomShader;
private Material bloomMaterial;
public Material material
{
get
{
bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
return bloomMaterial;
}
}
[Range(0, 4)] public int iterations = 3; //迭代次数
[Range(0.2f, 3.0f)] public float blurSpread = 0.6f; //模糊扩散
[Range(1, 8)] public int downSample = 2; //降采样系数
[Range(0.0f, 4.0f)] public float luminanceThreshold = 0.6f; //Bloom阈值
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
int rtW = source.width / downSample;
int rtH = source.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
//使用一个Pass提取图像中较亮的区域,只模糊该区域
Graphics.Blit(source, buffer0, material, 0);
//和高斯模糊部分一样
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
//将亮部模糊后的贴图赋值给Shader
material.SetTexture("_Bloom", buffer0);
//将source和_Bloom贴图混合,输出
Graphics.Blit(source, destination, material, 3);
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(source, destination);
}
}
}
Shader
Shader "Unity Shaders Book/Chapter 12/CT_Bloom"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} //原屏幕纹理
_Bloom ("Bloom (RGB)", 2D) = "black" {} //Bloom处理后的纹理
_LuminanceThreshold ("Luminance Threshold", Float) = 0.5
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
//计算光亮度 用于获取亮部贴图
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
//顶点着色器
v2f vertExtractBright(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
//片元着色器 用于获取亮部贴图
fixed4 fragExtractBright(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);//只提取超过阈值的部分
return c * val;
}
struct v2fBloom {
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};
//顶点着色器 用于Bloom
v2fBloom vertBloom(appdata_img v) {
v2fBloom o;
o.pos = UnityObjectToClipPos (v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
//是否为DX平台
#if UNITY_UV_STARTS_AT_TOP
//是否开启了抗锯齿
if (_MainTex_TexelSize.y < 0.0)
//Bloom为多重渲染纹理,需要对纹理(提取亮部并模糊处理)的v坐标进行特殊处理
o.uv.w = 1.0 - o.uv.w;
#endif
return o;
}
//顶点着色器 用于Bloom 混合贴图
fixed4 fragBloom(v2fBloom i) : SV_Target {
//颜色叠加
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}
ENDCG
ZTest Always Cull Off ZWrite Off
//第一个Pass:提取亮部
Pass {
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
//第二个Pass:计算竖直模糊
UsePass "Learning/Sampler12_4/GAUSSIAN_BLUR_VERTICAL"
//第二个Pass:计算水平模糊
UsePass "Learning/Sampler12_4/GAUSSIAN_BLUR_HORIZONTAL"
//第四个Pass:效果叠加
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off
}
最终效果
实战12-5:运动模糊
两种主要实现方式:
方式1:利用一块累积缓存(accumulation buffer)来混合多张连续图像
- 当物体快速移动产生多张图像后,我们取它们之间的平均值作为最后的运动模糊图像
- 这种暴力的方法对性能消耗很大,因为获取多张帧图像往往意味着我们需要在同一帧里渲染多次场景,或者将前几帧图像保存起来
方式2(推荐):创建和使用速度缓存(velocity buffer),这个缓存中存储了各个像素当前的运动速度,然后利用该值来决定模糊的方向和大小
本节使用第一种方法实现运动模糊
脚本
/****************************************************
文件:CTMotionBlur.cs
作者:聪头
邮箱: 1322080797@qq.com
日期:2021/11/02 18:35
功能:运动模糊
*****************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CTMotionBlur : CTPostEffectsBase
{
public Shader motionBlurShader;
private Material motionBlurMaterial = null;
public Material material
{
get
{
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
return motionBlurMaterial;
}
}
[Range(0.0f, 0.9f)]
public float blurAmount = 0.5f; //混合参数。值越大,运动拖尾效果就越明显
private RenderTexture accumulationTexture;//用于混合的贴图
void OnDisable()
{
if (accumulationTexture != null)
DestroyImmediate(accumulationTexture);//禁用就释放贴图
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
// Create the accumulation texture
//null或修改分辨率,就重新生成运动纹理
if (accumulationTexture == null || accumulationTexture.width != src.width || accumulationTexture.height != src.height)
{
DestroyImmediate(accumulationTexture);
accumulationTexture = new RenderTexture(src.width, src.height, 0);
Graphics.Blit(src, accumulationTexture);
}
//恢复操作:发生在渲染到纹理而该纹理又没有被提前清空或销毁的情况下
//accumulationTexture纹理不需要提前被清空,因为它保存了我们之前的混合结果
accumulationTexture.MarkRestoreExpected();
material.SetFloat("_BlurAmount", 1.0f - blurAmount);
//将当前屏幕图像和上一帧图像进行混合
Graphics.Blit(src, accumulationTexture, material);
//通常这里显示在屏幕后就把缓存给释放了(或无法访问),需要上面的恢复操作还原
Graphics.Blit(accumulationTexture, dest);
}
else
{
Graphics.Blit(src, dest);
}
}
}
Shader
Shader "Unity Shaders Book/Chapter 12/CT_MotionBlur"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
//脚本赋值越大,该值就越小,叠加时原图就更不明显
_BlurAmount ("Blur Amount", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed _BlurAmount;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
//RGB和A分别处理
//RGB通道版本将其A值设为_BlurAmount,以便在后面混合时可以使用它的透明通道进行混合
fixed4 fragRGB (v2f i) : SV_Target {
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}
//A通道版本只返回采样结果(后面会遮住RGB,相当于返回A通道值)
//这个版本只是为了维护渲染纹理的透明通道值,不让其受到混合时使用的透明度值的影响
half4 fragA (v2f i) : SV_Target {
return tex2D(_MainTex, i.uv);
}
ENDCG
ZTest Always Cull Off ZWrite Off
//第一个Pass:只渲染RGB通道,做图像混合
Pass {
//开启透明度混合,此时源图像是屏幕图像,渲染目标是上一帧图像(在脚本中执行了恢复操作)
Blend SrcAlpha OneMinusSrcAlpha
//打开RGB通道写掩码
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}
//第二个Pass:只渲染A通道,还原A通道值(为源颜色的a通道)
Pass {
Blend One Zero
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
FallBack Off
}
最终效果
Ch13.使用深度和法线纹理
13.1 深度和法线纹理
1.规定:Unity中观察空间使用右手坐标系,NDC的z分量∈[-1,1]
2.获取深度和法线纹理的两种途径:
- 来自于真正的缓存,一般是使用延迟渲染时,通过G-buffer获取
- 单独的Pass渲染而得。会使用着色器替换(Shader Replacement)技术选择那些渲染类型为Opaque的物体,判断它们的渲染队列是否小于等于2500(内置的Background、Geometry和AlphaTest渲染队列均在此范围内),如果满足条件,就把它们渲染到深度和法线纹理中
RenderType:让物体能够出现在深度和法线纹理中
单独的Pass:想让物体出现在深度纹理中,需要投射阴影时使用的Pass(即LightMode被设置为ShadowCaster的Pass)
3.精度:
- 深度纹理:精度通常是24位或16位,这取决于使用的深度缓存的精度
- 深度+法线纹理:和屏幕分辨率相同,精度为32位(每个通道8位)的纹理,其中观察空间下的法线信息会被编码进纹理的R和G通道,而深度信息会被编码进B和A通道(16位)
4.获取:
camera.depthTextureMode = DepthTextureMode.Depth;//深度纹理
//深度和法线纹理 或运算
camera.depthTextureMode |= DepthTextureMode.Depth;//深度
camera.depthTextureMode |= DepthTextureMode.DepthNormals;//法线
5.采样
- 通常情况,直接使用tex2D函数
- 特殊情况(如PS3和PS2),使用统一宏
SAMPLE_DEPTH_TEXTURE
对深度纹理采样float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- i.uv是一个float2类型变量,对应了当前像素的纹理坐标
- 类似的宏还有(了解):
SAMPLE_DEPTH_TEXTURE_PROJ
和SAMPLE_DEPTH_TEXTURE_LOD
6.深度纹理的处理
- 获取线性深度值
通过纹理采样得到的深度值,通常是非线性的(正交投影除外),所以我们需要通过采样深度反推出观察空间下的线性深度值。推导见P270
思路:采样深度纹理d∈(0,1) -> NDC∈(-1,1) -> 乘以w分量,得到 (- zviewzclip) —> 通过投影矩阵回推 zview—>观察空间正向对应z为负值,故取反得最终结果z’view
- 两个辅助函数
- LinearEyeDepth:负责把深度纹理的采样结果转换到视角空间下的深度值
- Linear01Depth:返回一个范围在[0,1]的线性深度值,也就是上式除以far的结果
- 这两个函数都使用_ZBufferParams变量来得到远近裁剪平面的距离
7.深度+法线纹理的处理
- 可以直接使用tex2D函数对_CameraDepthNormalsTexture进行采样,得到深度和法线信息
Unity提供辅助函数DecodeDepthNormal对采样结果进行解码,得到深度值和法线方向
inline void DecodeDepthNormal(float4 enc, out float depth, out float3 normal) { depth = DecodeFloatRG(enc.zw); normal = DecodeViewNormalStereo(enc); }
参数①:对深度+法线纹理的采样结果,即Unity对深度和法线进行编码后的结果。xy分量存储的是视角空间下的法线信息,而深度信息被编码进了zw分量
- 参数②:范围[0,1]的线性深度值(观察空间)
- 参数③:观察空间下的法线方向
- 同样,我们可以通过调用DecodeFloatRG和DecodeViewNormalStereo来解码深度+法线纹理中的深度和法线信息
补充:RenderType深度解析(了解)
CSDN:https://blog.csdn.net/mobilebbki399/article/details/50512059
13.2 再谈运动模糊
使用速度映射图:
- 这种方法利用深度纹理在片元着色器中为每个像素计算其在世界空间下的位置,这是通过使用当前的视角*投影矩阵的逆矩阵对NDC下的顶点坐标进行变换得到的
- 得到世界空间中的顶点坐标后,使用前一帧的视角*投影矩阵对其进行变换,得到该位置在前一帧中的NDC坐标
- 利用前一帧和当前帧的位置差,生成该像素的速度
优点:
- 可以在一个屏幕后处理步骤中完成整个效果的模拟
缺点:
- 需要在片元着色器中进行两次矩阵乘法的操作,对性能有所影响
- 只适用于场景静止,摄像机快速运动的情况
脚本
/****************************************************
文件:CTMotionBlurWithDepthTexture.cs
作者:聪头
邮箱: 1322080797@qq.com
日期:2021/11/03 11:23
功能:运动模糊之速度映射图
*****************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CTMotionBlurWithDepthTexture : CTPostEffectsBase
{
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;
//前一个VP矩阵(投影 * 观察)
previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
material.SetFloat("_BlurSize", blurSize);
//将上一帧摄像机的VP矩阵传入Shader
material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
//计算当前帧的VP矩阵的逆矩阵,用于将像素坐标还原成世界空间顶点
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/CT_MotionBlurWithDepthTexture"
{
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; //当前帧VP矩阵的逆矩阵
float4x4 _PreviousViewProjectionMatrix; //上一帧的VP矩阵
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 {
//得到深度纹理 SAMPLE_DEPTH_TEXTURE会根据平台差异采样深度纹理(可以当成跨平台的tex2D)
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
// 根据uv和深度信息,将该顶点反映射回NDC (各分量∈(-1,1))
float4 ndcPos = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
// 当前帧世界空间坐标中间值(得到的值为(x/-z, y/-z, z/-z, 1/-z)
float4 worldPos = mul(_CurrentViewProjectionInverseMatrix, ndcPos);
//当前帧世界空间坐标(x,y,z,1)
worldPos = worldPos / worldPos.w;
//上一帧的该点在裁剪空间下的坐标
float4 previousNdcPos = mul(_PreviousViewProjectionMatrix, worldPos);
//上一帧该点NDC坐标
previousNdcPos /= previousNdcPos.w;
// 得到像素速度
float2 velocity = (ndcPos.xy - previousNdcPos.xy) / 2.0f;
float2 uv = i.uv;
float4 c = tex2D(_MainTex, uv);//当前帧采样
uv += velocity * _BlurSize;//根据速度和Blur系数 对uv进行偏移
for (int it = 0; it < 2; it++, uv += velocity * _BlurSize) {//迭代,同时偏移uv
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
}
最终效果
13.3 全局雾效*
关键:根据深度纹理来重建每个像素在世界空间下的位置,本节采用另一种更省性能的方法
- 对图像空间下的视锥体射线(从摄像机出发,指向图像上的某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息(重点就是求射线)
- 把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置,进而模拟全局雾效
float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;
关键步骤推导
1.摄像机位置可以直接获取
2.像素对应时间空间的深度信息(linearDepth)可以通过采样深度纹理(SAMPLE_DEPTH_TEXTURE)和调用Unity内置函数(LinearEyeDepth)获取
3.难点在于interpolatedRay的获取,下面作简要分析:
理解interpolatedRay
的核心(书本原话):
interpolatedRay是由顶点着色器输出并插值后得到的射线,它包含了该像素到摄像机的方向,也包含了距离信息
- 首先求出近平面的toTop和toRight两个向量,分别指向相机的向上方向和向右方向,模长分别为高、宽的一半
- ```c 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; //近平面向上方向向量
- 分别求出近平面四个顶点的位置坐标:BL,BR,TR,TL,归一化后可以得到对应射线的方向向量(如下图)

- 根据相似,可以推出像素对应的世界空间下的点(如下图),到摄像机的实际距离(不要单纯以为是z轴深度值,投影变换会压缩z轴的)

> 已知量:TL,Near,depth(采样深度纹理获取)
>
> 推出:

> 令 
>
> 得 
>
> 这个Ray不仅包含了方向信息,也包含了不完整的深度信息(需要再通过depth求最终深度),其他位置的Ray同理可以求得
>
> 细心的读者(我)可能会问:这不就是求了四个顶点方向上的向量吗?怎么就能还原像素的世界位置呢?
>
> 答:顶点着色器输出是会插值的!还不懂请参考代码和验证理解!
```c
Vector3 topLeft = cameraTransform.forward * near + toTop - toRight; //近平面左上坐标点
//欧式距离dist = |TL| / |Near| * depth = scale * depth (现在只有depth不知道)
//世界空间位置 = dist * interpolatedRay(单位向量)
float scale = topLeft.magnitude / near; //scale = |TL| / |Near|
topLeft.Normalize();
topLeft *= scale;
- 通过顶点着色器输出并插值,可以得到该像素位置的射线(包含距离信息,不可归一化),此时得到的向量为interpolatedRay,即插值的射线向量
- 根据摄像机位置和深度,最终可以求得该像素对应的世界空间坐标
脚本
/****************************************************
文件:CTFogWithDepthTexture.cs
作者:聪头
邮箱: 1322080797@qq.com
日期:2021/11/03 22:59
功能:全局雾效
*****************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CTFogWithDepthTexture : CTPostEffectsBase
{
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; //近平面左上坐标点
//欧式距离dist = |TL| / |Near| * depth = scale * depth (现在只有depth不知道)
//世界空间位置 = dist * interpolatedRay(单位向量)
float scale = topLeft.magnitude / near; //scale = |TL| / |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/CT_FogWithDepthTexture"
{
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
//顶点着色器中赋值interpolatedRay,进行插值
//具体插值过程:一张图像分成4个区域,在4个区域之间相互插值,得到该像素对应的Ray向量
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 * normalize(i.interpolatedRay.xyz);
//基于高度的雾效
float fogDensity = saturate((_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
}
最终效果
插值结果验证
remap原理:https://zhuanlan.zhihu.com/p/158039963
修改Shader文件
- 主要就是修改了片元着色器代码,将各分量映射到0~1,其他不变
half remap(half x, half old1, half old2, half new1, half new2)
{
return (x - old1) / (old2 - old1) * (new2 - new1) + new1;
}
fixed4 frag(v2f i) : SV_Target {
//重映射 Ray分量
i.interpolatedRay.x = remap(i.interpolatedRay.x, _FrustumCornersRay[0].x, _FrustumCornersRay[1].x, 0, 1);
i.interpolatedRay.y = remap(i.interpolatedRay.y, _FrustumCornersRay[0].y, _FrustumCornersRay[2].y, 0, 1);
return half4(i.interpolatedRay.xy, 0, 1);
}
不难发现,确实插值得很完美
13.4 再谈边缘检测
原理:取对角方向的深度和法线值,比较它们的差值,如果超过某个阈值,就认为它们之间存在一条边
好处:在深度和法线纹理上进行边缘检测,这些图像不会受纹理和光照的影响,而仅仅保存了当前渲染物体的模型信息,通过这样的方式检测出来的边缘更加可靠
脚本
/****************************************************
文件:CTEdgeDetectNormalsAndDepth.cs
作者:聪头
邮箱: 1322080797@qq.com
日期:2021/11/04 11:20
功能:深度法线纹理的边缘检测
*****************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CTEdgeDetectNormalsAndDepth : CTPostEffectsBase
{
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);
}
}
}
Shader
Shader "Unity Shaders Book/Chapter 13/CT_EdgeDetectNormalsAndDepth"
{
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;
//uv[0]:存储了屏幕颜色图像的采样纹理坐标,利于对深度纹理的坐标进行平台差异化处理
//其余4个为Roberts算子(本质:计算左上角和右下角的差值,乘以右上角和左下角的差值)
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;
}
//返回值:1表示无描边,0表示描边
half CheckSame(half4 center, half4 sample) {
//获取对角线上采样像素的深度和法线(法线只比较差异,故无需解码)
half2 centerNormal = center.xy;
float centerDepth = DecodeFloatRG(center.zw);
half2 sampleNormal = sample.xy;
float sampleDepth = DecodeFloatRG(sample.zw);
//比较法线差异
half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
//如果二者在x,y上的差异之和小于0.1
//返回1,表示不是描边;否则返回0,表示是描边
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
//比较深度差异
float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
// 缩放阈值后比较
//返回1,表示不是描边;否则返回0,表示是描边
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;
//返回值: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
}
最终效果
Ch14.非真实感渲染
NRP:Non-Photorealistic Rendering
14.1 卡通风格的渲染
1.渲染轮廓线
本节使用过程式几何轮廓线渲染(方法二),简要介绍实现思路
第一个Pass:描边。我们会使用轮廓线颜色渲染整个背面的面片,渲染时会在视角空间下把模型顶点沿着法线方向向外扩张一段距离,以此来让背部轮廓线可见
viewPos = viewPos + viewNormal * _Outline
考虑内凹的模型,可能发生背面面片遮挡正面面片的情况。为避免此情况发生,在扩张背面顶点之前,我们首先对顶点法线的z分量进行处理,使它们等于一个定值,然后把法线归一化后再对顶点进行扩张。这样的好处在于,扩展后的背面更加扁平化,从而降低了遮挡正面面片的可能性(不太好理解,记着吧)
viewNormal.z = -0.5;
viewNormal = normalize(viewNormal);
viewPos = viewPos + viewNormal * _Outline
第二个Pass:渲染模型
2.添加高光
float spec = dot(worldNormal, worldHalfDir);
spec = lerp(0, 1, smoothstep(-w, w, spec - threshold));
这样的效果是,我们可以在[-w, w]区间内,即高光区域的边界处,得到一个从0到1平滑变化的spec值,从而实现抗锯齿目的。本例中,我们使用邻域像素之间的近似导数值,这可以通过CG的 fwidth函数 来得到
Shader
Shader "Unity Shaders Book/Chapter 14/CT_ToonShading"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color Tint", Color) = (1,1,1,1)
_Ramp ("RampTexture", 2D) = "white"{} //控制漫反射色调的渐变纹理
_Outline ("Outline", Range(0,1)) = 0.1 //轮廓线宽度
_OutlineColor ("Outline Color", Color) = (0,0,0,1)
_Specular ("Specular", Color) = (1,1,1,1)
_SpecularScale ("Specular Scale", Range(0, 0.1)) = 0.01 //控制计算高光反射时使用的阈值
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
//渲染背面,得到轮廓颜色
Pass
{
NAME "OUTLINE"
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
fixed4 _OutlineColor;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
};
v2f vert (a2v v)
{
v2f o;
float4 pos = mul(UNITY_MATRIX_MV, v.vertex);//模型->观察
float3 normal = mul(UNITY_MATRIX_IT_MV, v.normal);//将法线从模型->观察
normal.z -= 0.5;
//沿法线方向外扩
pos = pos + float4(normalize(normal), 0) * _Outline;
o.pos = mul(UNITY_MATRIX_P, pos);
return o;
}
float4 frag(v2f i) : SV_Target {
return float4(_OutlineColor.rgb, 1);
}
ENDCG
}
//渲染正面
Pass {
Tags {"LightMode"="ForwardBase"}
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _Ramp;
fixed4 _Specular;
fixed _SpecularScale;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};
struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
SHADOW_COORDS(3)
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
TRANSFER_SHADOW(o);
return o;
}
float4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);
fixed4 c = tex2D(_MainTex, i.uv);
fixed3 albedo = c * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
//漫反射
fixed diff = dot(worldNormal, worldLightDir);
diff = (diff * 0.5 + 0.5) * atten;
fixed3 diffuse = _LightColor0 * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;//用衰减的半兰伯特采样渐变纹理
//高光
fixed spec = dot(worldNormal, worldHalfDir);
fixed w = fwidth(spec) * 2.0;
//使用fwidth进行抗锯齿处理,最后使用step确保在_SpecularScale为0时,可以完全消除高光反射
fixed3 specular = _Specular * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
最终效果
14.2 素描风格的渲染
步骤:
- 首先,在顶点着色器阶段计算逐顶点光照,根据光照来决定6张纹理的混合权重,并传递给片元着色器
- 然后,在片元着色器中根据这些权重来混合6张纹理的采样结果
Shader
Shader "Unity Shaders Book/Chapter 14/CT_Hatching"
{
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_TileFactor ("Tile Factor", Float) = 1 //纹理的平铺系数,越大则模型上的素描线条越密
_Outline ("Outline", Range(0, 1)) = 0.1
//6张素描贴图,依次从疏->密,越亮的地方越稀疏
_Hatch0 ("Hatch 0", 2D) = "white" {}
_Hatch1 ("Hatch 1", 2D) = "white" {}
_Hatch2 ("Hatch 2", 2D) = "white" {}
_Hatch3 ("Hatch 3", 2D) = "white" {}
_Hatch4 ("Hatch 4", 2D) = "white" {}
_Hatch5 ("Hatch 5", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
//渲染背面,并沿法线外扩,画出轮廓线
UsePass "Unity Shaders Book/Chapter 14/CT_ToonShading/OUTLINE"
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
fixed4 _Color;
float _TileFactor;
sampler2D _Hatch0;
sampler2D _Hatch1;
sampler2D _Hatch2;
sampler2D _Hatch3;
sampler2D _Hatch4;
sampler2D _Hatch5;
struct a2v {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
fixed3 hatchWeights0 : TEXCOORD1;
fixed3 hatchWeights1 : TEXCOORD2;
float3 worldPos : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord.xy * _TileFactor;
fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex));
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed diff = max(0, dot(worldLightDir, worldNormal));
//存储6张图的权重
o.hatchWeights0 = fixed3(0, 0, 0);
o.hatchWeights1 = fixed3(0, 0, 0);
float hatchFactor = diff * 7.0;//限制在0~7
//可以推出,越亮的地方采样越稀疏的纹理图
if (hatchFactor > 6.0) {
// Pure white, do nothing
} else if (hatchFactor > 5.0) {
o.hatchWeights0.x = hatchFactor - 5.0;
} else if (hatchFactor > 4.0) {
o.hatchWeights0.x = hatchFactor - 4.0;
o.hatchWeights0.y = 1.0 - o.hatchWeights0.x;
} else if (hatchFactor > 3.0) {
o.hatchWeights0.y = hatchFactor - 3.0;
o.hatchWeights0.z = 1.0 - o.hatchWeights0.y;
} else if (hatchFactor > 2.0) {
o.hatchWeights0.z = hatchFactor - 2.0;
o.hatchWeights1.x = 1.0 - o.hatchWeights0.z;
} else if (hatchFactor > 1.0) {
o.hatchWeights1.x = hatchFactor - 1.0;
o.hatchWeights1.y = 1.0 - o.hatchWeights1.x;
} else {
o.hatchWeights1.y = hatchFactor;
o.hatchWeights1.z = 1.0 - o.hatchWeights1.y;
}
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
//根据权重采样
fixed4 hatchTex0 = tex2D(_Hatch0, i.uv) * i.hatchWeights0.x;
fixed4 hatchTex1 = tex2D(_Hatch1, i.uv) * i.hatchWeights0.y;
fixed4 hatchTex2 = tex2D(_Hatch2, i.uv) * i.hatchWeights0.z;
fixed4 hatchTex3 = tex2D(_Hatch3, i.uv) * i.hatchWeights1.x;
fixed4 hatchTex4 = tex2D(_Hatch4, i.uv) * i.hatchWeights1.y;
fixed4 hatchTex5 = tex2D(_Hatch5, i.uv) * i.hatchWeights1.z;
fixed4 whiteColor = fixed4(1, 1, 1, 1) * (1 - i.hatchWeights0.x - i.hatchWeights0.y - i.hatchWeights0.z -
i.hatchWeights1.x - i.hatchWeights1.y - i.hatchWeights1.z);
fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(hatchColor.rgb * _Color.rgb * atten, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
最终效果
Ch15.使用噪声
15.1 消融效果
原理:概括来说就是噪声纹理+透明度测试
- 我们使用对噪声纹理的采样结果和某个控制消融程度的阈值比较,如果小于阈值,就使用clip函数把它对应的像素裁减掉,这些部分就对应了图中被“烧毁”的区域
- 而镂空区域边缘的烧焦效果则将两种颜色混合,再用pow函数处理后,与原纹理颜色混合后的结果
Shader
Shader "Unity Shaders Book/Chapter 15/CT_Dissolve"
{
Properties {
_BurnAmount ("Burn Amount", Range(0.0, 1.0)) = 0.0 //控制消融程度,0正常,1完全消融
_LineWidth("Burn Line Width", Range(0.0, 0.2)) = 0.1 //控制模拟烧焦效果时的线宽,值越大,火焰边缘的蔓延范围越广
_MainTex ("Base (RGB)", 2D) = "white" {} //物体原本的漫反射贴图
_BumpMap ("Normal Map", 2D) = "bump" {} //物体原本法线贴图
//火焰边缘的两种颜色
_BurnFirstColor("Burn First Color", Color) = (1, 0, 0, 1) //第一种为物体本身的颜色
_BurnSecondColor("Burn Second Color", Color) = (1, 0, 0, 1) //第二种为燃烧时的颜色
_BurnMap("Burn Map", 2D) = "white"{} //关键的噪声贴图!
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass {
Tags { "LightMode"="ForwardBase" }
Cull Off
CGPROGRAM
#include "Lighting.cginc"
#include "AutoLight.cginc"
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
fixed _BurnAmount;
fixed _LineWidth;
sampler2D _MainTex;
sampler2D _BumpMap;
fixed4 _BurnFirstColor;
fixed4 _BurnSecondColor;
sampler2D _BurnMap;
float4 _MainTex_ST;
float4 _BumpMap_ST;
float4 _BurnMap_ST;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uvMainTex : TEXCOORD0;
float2 uvBumpMap : TEXCOORD1;
float2 uvBurnMap : TEXCOORD2;
float3 lightDir : TEXCOORD3;
float3 worldPos : TEXCOORD4;
SHADOW_COORDS(5)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uvMainTex = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uvBumpMap = TRANSFORM_TEX(v.texcoord, _BumpMap);
o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap);
//模型 -> 切线的变换矩阵
TANGENT_SPACE_ROTATION;
//将光源方向变换到切线空间
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
//世界空间顶点位置,用于计算衰减
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb;
//根据噪声贴图的r通道裁剪片元
clip(burn.r - _BurnAmount);
//采样切线空间法线,正常计算光照
float3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uvBumpMap));
fixed3 albedo = tex2D(_MainTex, i.uvMainTex).rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
//边缘处,burn.r - _BurnAmount会很小,t=1-很小值 ≈ 1。故1为边缘,0为物体本身
fixed t = 1 - smoothstep(0.0, _LineWidth, burn.r - _BurnAmount);
//这里挺矛盾的,你t很小,取FirstColor,但是在求finalColor根本不考虑burnColor!
fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t);
burnColor = pow(burnColor, 5);
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount));
return fixed4(finalColor, 1);
}
ENDCG
}
// Pass to render object as a shadow caster
Pass {
Tags { "LightMode" = "ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
fixed _BurnAmount;
sampler2D _BurnMap;
float4 _BurnMap_ST;
struct v2f {
V2F_SHADOW_CASTER;
float2 uvBurnMap : TEXCOORD1;
};
v2f vert(appdata_base v) {
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb;
clip(burn.r - _BurnAmount);
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
FallBack "Diffuse"
}
最终效果
15.2 水波效果
原理:
- 我们使用一张立方体纹理(Cubemap)作为环境映射纹理,模拟反射
- 为了模拟折射效果,我们使用GrabPass来获取当前屏幕的渲染纹理,并使用切线空间下的法线方向(x,y)对像素的屏幕坐标进行偏移,再使用该坐标对渲染纹理进行屏幕采样,从而模拟近似的折射效果
- 本节与中级篇第10章玻璃效果的不同之处在于,水波的法线纹理是由一张噪声纹理生成而得,而且会随着时间变换不断平移,模拟波光粼粼的效果。除此之外,我们没有使用一个定值来混合反射和折射颜色,而是使用之前提到的菲涅尔系数来动态决定混合系数
- 菲涅尔系数计算公式:
fresnel=pow(1-max(0, v·n), 4)
- 其中,v和n分别对应了视角方向和法线方向,它们之间夹角越小,fresnel值越小,反射越弱,折射越强
- 菲涅尔系数还经常会用于边缘光的计算中
- 菲涅尔系数计算公式:
折射:使用切线空间的法线,偏扰屏幕uv坐标,进而采样屏幕纹理即可
反射:使用反射向量采样立方体贴图
Shader
Shader "Unity Shaders Book/Chapter 15/CT_WaterWave"
{
Properties {
_Color ("Main Color", Color) = (0, 0.15, 0.115, 1) //控制水面颜色
_MainTex ("Base (RGB)", 2D) = "white" {} //水面波纹材质纹理
_WaveMap ("Wave Map", 2D) = "bump" {} //噪声纹理生成的法线纹理
_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {} //模拟反射的立方体纹理
//法线在X和Y的平移速度
_WaveXSpeed ("Wave Horizontal Speed", Range(-0.1, 0.1)) = 0.01
_WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01
_Distortion ("Distortion", Range(0, 100)) = 10
}
SubShader {
// We must be transparent, so other objects are drawn before this one.
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
// This pass grabs the screen behind the object into a texture.
// We can access the result in the next pass as _RefractionTex
GrabPass { "_RefractionTex" }
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#include "UnityCG.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _WaveMap;
float4 _WaveMap_ST;
samplerCUBE _Cubemap;
fixed _WaveXSpeed;
fixed _WaveYSpeed;
float _Distortion;
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 scrPos : TEXCOORD0;
float4 uv : TEXCOORD1;
float4 TtoW0 : TEXCOORD2;
float4 TtoW1 : TEXCOORD3;
float4 TtoW2 : TEXCOORD4;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = ComputeGrabScreenPos(o.pos);
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _WaveMap);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag(v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float2 speed = _Time.y * float2(_WaveXSpeed, _WaveYSpeed); //根据时间产生的采样偏移量
//将噪声贴图当成法线贴图解析(这里挺难理解的。。。),且uv会随时间动态变换
fixed3 bump1 = UnpackNormal(tex2D(_WaveMap, i.uv.zw + speed)).rgb;
fixed3 bump2 = UnpackNormal(tex2D(_WaveMap, i.uv.zw - speed)).rgb;
//为了模拟两层交叉的水面波动效果,取两次结果的平均值作为切线空间法线值
fixed3 bump = normalize(bump1 + bump2);
// Compute the offset in tangent space
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
//我们把偏移量和屏幕坐标的z分量相差,这是为了模拟深度越大、折射程度越大的效果;也可以直接叠加offset
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
fixed3 refrCol = tex2D( _RefractionTex, i.scrPos.xy/i.scrPos.w).rgb; //获得折射颜色
// Convert the normal to world space
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
fixed4 texColor = tex2D(_MainTex, i.uv.xy + speed);
fixed3 reflDir = reflect(-viewDir, bump);
fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb * _Color.rgb; //获得反射颜色
fixed fresnel = pow(1 - saturate(dot(viewDir, bump)), 4); //获得菲涅尔系数
fixed3 finalColor = reflCol * fresnel + refrCol * (1 - fresnel);
return fixed4(finalColor, 1);
}
ENDCG
}
}
// Do not cast shadow
FallBack Off
}
最终效果
15.3 动态雾效
回顾全局雾效:
- 我们由深度纹理重建每个像素在世界空间下的位置,再使用一个基于高度的公式来计算雾效的混合系数,最终使用该系数来混合雾的颜色和屏幕颜色
本节要求:
- 之前是基于高度的均匀雾效,即在同一个高度上,雾的浓度是相同的。这节我们要使用一张噪声纹理,让雾不断飘动,看起来更加缥缈
实现思路:
- 本节实现非常简单,绝大多数代码和全局雾效中的完全一样,只是添加噪声相关的参数和属性,并在Shader片元着色器中对高度的计算添加了噪声的影响
脚本
/****************************************************
文件:CTFogWithNoise.cs
作者:聪头
邮箱: 1322080797@qq.com
日期:2021/11/04 15:35
功能:动态全局雾效
*****************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CTFogWithNoise : CTPostEffectsBase
{
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.1f, 3.0f)]
public float fogDensity = 1.0f;
public Color fogColor = Color.white;
public float fogStart = 0.0f;
public float fogEnd = 2.0f;
public Texture noiseTexture;
[Range(-0.5f, 0.5f)]
public float fogXSpeed = 0.1f;
[Range(-0.5f, 0.5f)]
public float fogYSpeed = 0.1f;
[Range(0.0f, 3.0f)]
public float noiseAmount = 1.0f;
void OnEnable()
{
GetComponent<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);
material.SetTexture("_NoiseTex", noiseTexture);
material.SetFloat("_FogXSpeed", fogXSpeed);
material.SetFloat("_FogYSpeed", fogYSpeed);
material.SetFloat("_NoiseAmount", noiseAmount);
Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}
}
Shader
Shader "Unity Shaders Book/Chapter 15/CT_FogWithNoise"
{
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
_NoiseTex ("Noise Texture", 2D) = "white" {}
_FogXSpeed ("Fog Horizontal Speed", Float) = 0.1
_FogYSpeed ("Fog Vertical Speed", Float) = 0.1
_NoiseAmount ("Noise Amount", Float) = 1
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
float4x4 _FrustumCornersRay;
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
half _FogDensity;
fixed4 _FogColor;
float _FogStart;
float _FogEnd;
sampler2D _NoiseTex;
half _FogXSpeed;
half _FogYSpeed;
half _NoiseAmount;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float2 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;
//根据时间和速度产生偏移量
float2 speed = _Time.y * float2(_FogXSpeed, _FogYSpeed);
//根据偏移量偏移采样uv,采样噪声贴图,根据r通道获得噪声值
float noise = (tex2D(_NoiseTex, i.uv + speed).r - 0.5) * _NoiseAmount;
float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
//雾的密度加上噪声值的影响
fogDensity = saturate(fogDensity * _FogDensity * (1 + noise));
fixed4 finalColor = tex2D(_MainTex, i.uv);
finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
return finalColor;
}
ENDCG
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack Off
}
最终效果
Ch16.Unity中的渲染优化技术*
本章涉及的优化技术:
CPU优化
- 使用批处理技术减少draw call数目
GPU优化
- 优化几何体
- 使用模型的LOD(Level of Detail)技术
- 使用遮挡剔除(Occlusion Culling)技术
- 减少需要处理的片元数目
- 控制绘制顺序
- 警惕透明物体
- 减少实时光照
- 减少计算复杂度
- 使用Shader的LOD(Level of Detail)技术
- 代码方面的优化
节省内存带宽
- 减少纹理大小
- 利用分辨率缩放
16.1 Unity中的渲染分析工具
渲染统计窗口
Rendering Statistics Window
性能分析器
Profiler
打开(2019.4):Window->Analysis->Profiler
帧调试器
Frame Debugger
打开:Window->Analysis->Frame Debugger
16.2 减少DrawCall数目
使用批处理(batching)技术,前提是共享同一个材质且材质属性相同,Unity中支持两种:
- 动态批处理:Unity自动完成,优点是仍可移动;缺点是限制多,容易破坏这种机制
- 静态批处理:自由度很高,限制很少;缺点是可能会占用更多的内存,而且经过静态批处理后的所有物体都不可以再移动了(包括脚本也无法改变其位置)
这节总结起来就是两句话:
- 模型之间能共享材质就共享
- 将模型设为Batching Static,可以用空间换时间
动态批处理
原理:每一帧把可以进行批处理的模型网格进行合并,再把合并后模型数据传递给GPU,然后使用同一个材质对其渲染
主要的条件限制(基于Unity5.2.1):
- 模型共享同一材质
- 能够进行动态批处理的网格的顶点属性规模要小于900(现在肯定不止这个数)
- 例如,shader中需要使用顶点位置、法线和纹理坐标,那么想让其能被动态批处理,顶点数不能超过300
- 使用光照纹理(lightmap)的物体需要小心处理。这些物体需要额外的渲染参数,例如,在光照纹理上的索引、偏移量和缩放信息等。因此,为了让这些物体可以被动态批处理,我们需要保证它们指向光照纹理中的同一位置(怎么保证???)
- 多Pass的shader会中断批处理
静态批处理
原理:只在运行开始阶段,把需要进行静态批处理的模型合并到一个新的网格结构中,这意味着这些模型不可以在运行时刻被移动。但由于它只需要进行一次合并操作,因此,比动态批处理更加高效
缺点及产生原因:需要占用更多的内存来存储合并后的几何结构。如果在静态批处理前一些物体共享了相同的网格,那么内存中每个物体都会对应一个该网格的复制品,即一个网格会变成多个网格再发给CPU(程序上,体现在VBO total的变化,VBO数目更大了)
共享材质
无论是静态批处理还是动态批处理,都要求模型之间需要共享同一材质且属性相同
- 两个材质纹理不同,可以把这些纹理合并到一张更大的纹理中,再使用不同的采样坐标对纹理采样即可
- 两个材质参数不同,可以使用网格的顶点数据(最常见的是顶点颜色)来存储这些参数
注意点:
- 在脚本中访问共享材质,应该使用Renderer.sharedMaterial来保证修改的是和其他物体共享的材质。若使用Renderer.material来修改材质,Unity会创建一个该材质的复制品,从而破坏批处理在该物体上的应用
小结之小建议
- 尽可能选择静态批处理,但时刻小心内存的消耗,牢记经过静态批处理的物体不可以再被移动
- 如果无法进行静态批处理,而要使用动态批处理的话,那么请小心上面提到的各种条件限制
- 尽可能让这样的物体少并且包含少量顶点属性和顶点数目
- 对于游戏中的小道具,例如可以捡拾的金币等,可以使用动态批处理
- 对于包含动画的这类物体,我们无法全部使用静态批处理,但其中如果有不动的部分,可以把这部分标识成“Static”
- DisableBatching标签的使用。批处理需要把多个模型变换到世界空间下再合并它们,因此,如果shader中存在基于模型空间下的坐标运算,往往会得到错误的结果。需要使用DisableBatching标签强制shader的材质不会被批处理
Unity5.2中渲染摄像机的深度纹理等部分没有实现批处理
16.3 减少需要处理的顶点数目
3个常用顶点优化策略
- 优化几何体:建模时,移除不必要的硬边以及纹理衔接,避免边界平滑和纹理分离
- 模型的LOD技术
- 原理:物体根据离摄像机远近,动态改变模型上面片的数量,从而提高性能
- Unity中使用
LOD Gropu
组件来为一个物体构建一个LOD,我们需要为同一个对象准备多个包含不同细节程度的模型,然后赋值给不同等级
- 遮挡剔除技术
- 原理:视锥体剔除只会剔除那些不在摄像机视野范围内的对象,不会判断视野中是否有物体被其他物体挡住。而遮挡剔除会使用一个虚拟摄像机来遍历场景,从而构建一个潜在可见的对象集合的层级结构。运行时刻,每个相机将会使用这个数据来识别哪些物体可见,哪些不可见
- 参考:https://www.bilibili.com/video/BV1qt411i7sC?from=search&seid=14051031881898920412&spm_id_from=333.337.0.0
16.4 减少需要处理的片元数目
这一部分优化的中点在于减少overdraw,即减少同一个像素被绘制了多次
- 控制渲染顺序。由于深度测试的存在,如果我们可以保证物体都是从前往后绘制的,那么就可以很大程度上减少overdraw。理由显而易见
- 在Unity中,渲染队列小于2500(如”Background”,”Geometry”和”AlphaTest”)的对象都被认为是不透明(opaque)的物体,这些物体总体上是从前往后绘制的。所以尽量把物体的渲染队列设为不透明的
- 时刻警惕透明物体。由于它们没有开启深度写入,因此,如果要得到正确的渲染效果,就必须从后往前渲染。这意味着半透明物体会从后往前渲染,如果不注意,在一些机器会严重影响性能
- GUI通常是半透明的,我们可以尽量减少窗口中GUI所占面积。或者,将GUI的绘制和三维场景的绘制交给不同的摄像机,二者尽量不要重叠(不实用)
- 减少实时光照和阴影
- 烘焙光照纹理
16.5 节省带宽
大量使用未经压缩的纹理以及使用过大的分辨率都会造成由于带宽而引发的性能瓶颈
- 节省纹理大小
- 长宽值最好为2的整数幂
- 尽可能使用多级渐远纹理技术和纹理压缩
- 利用分辨率缩放
- 对特定机器进行分辨率的缩放
16.6 减少计算复杂度
两个方面减少计算复杂度:
Shader的LOD技术
- 原理:只有Shader的LOD值小于某个设定的值,这个Shader才会被使用,而使用了那些超过设定值的Shader的物体将不会被渲染
SubShader {
Tags {"RenderType"="Opaque"}
LOD 200
......
}
- 我们可以在Untiy Shader的导入面板上看到该Shader使用的LOD值。默认情况下,允许的LOD等级是无限大,意味着任何被当前显卡支持的Shader都可以被使用。若想去掉一些复杂计算的Shader渲染,可以使用Shader.maximumLOD或Shader.globalMaximumLOD来设置允许的最大LOD值
- Unity内置的Shader实验了不同的LOD值,例如Diffuse的LOD为200,而Bumped Specular的LOD为400
代码方面的优化
实现游戏效果时,我们可以选择在哪里进行某些特定的运算。通常来讲,游戏需要计算的对象、顶点、像素的数目排序为:对象数 < 顶点数 < 像素数
- 我们应尽可能把计算放在每个对象和顶点上
- 尽可能使用低精度的浮点值进行运算
上图参考CSDN:https://www.cnblogs.com/hiker-online/p/14228555.html
- 顶点着色器输出给下一阶段时,我们应该使用尽可能少的插值变量
- 通常,如果需要对两个纹理坐标进行插值,我们会打包在同一个float4变量中(VR平台除外)
- 尽可能不要使用全屏的屏幕后处理效果
- 代码优化规则
- 尽可能不要使用分支和循环语句
- 尽可能避免使用类似sin、tan、pow、log等较为复杂的数学运算。可以使用查找表来替代
- 尽可能不要使用discard,因为这会影响硬件的某些优化