——内容源自唐老狮的shader课程
目录
1.概述
1.1.分别指什么
1.2.如何获取
1.2.1.对摄像机赋值
1.2.2.在Shader中声明
1.2.3.获取深度值
1.2.4.获取法线纹理
1.3.背后的原理
1.3.1.深度纹理中存储的是什么信息
1.3.2.法线纹理中存储的是什么信息
1.3.3.unity是如何得到深度和法线纹理的
1.3.4.深度和法线纹理使用时调用的函数原理
1.3.5.注
2.查看深度和法线纹理
2.1.查看深度信息
2.2.查看法线信息
3.后处理效果——运动模糊
3.1.概述
3.2.基本原理
3.2.1.得到像素上一帧和当前帧在裁剪空间的位置
3.2.2.运动方向
3.2.3.如何模拟运动模糊效果
3.3.实现
3.4.问题
4.后处理效果——全局雾效
4.1.是什么
4.2.unity自带的全局雾效
4.2.1.开启
4.2.2.雾气的计算模式(也是自带雾效的重要参数)
4.3.基于深度纹理实现全局雾效
4.3.1.为什么要自己实现
4.3.2.基本原理(计算像素在世界空间下的位置是为了计算其离摄像机的距离)
4.3.3.获取摄像机指向像素的世界坐标的方向向量
4.4.实现
5.后处理效果——边缘检测
5.1.为什么这么干
5.2.基本原理(不会进行卷积运算)
5.3.关键步骤
5.3.1.得到对角线上的像素
5.3.2.进行深度和法线值的比较
5.3.3.具体的比较
5.4.实现
6.如有疏漏,还请指出
1.概述
1.1.分别指什么
1.深度纹理:屏幕空间的深度纹理,用于存储屏幕图像每个像素深度信息的纹理。可以利用其中存储的每个像素的深度信息
2.法线纹理:屏幕空间的法线纹理,用于存储屏幕图像中每个像素法线信息的纹理。可用来制作屏幕空间环境遮挡,基于屏幕空间的反射等
1.2.如何获取
1.2.1.对摄像机赋值
在c#代码中对主摄像机进行赋值,让其知道我们要使用
Camera.main.depthTextureMode = DepthTextureMode.Depth;
赋值有三种(常用):
1.Depth:获取一张深度纹理
2.DepthNormals:获取一张纹理,其同时包含深度和法线
3.Depth | Normals:获取两张纹理,分别为深度和法线
1.2.2.在Shader中声明
1.深度纹理的声明:
sampler2D _CameraDepthTexture
2.深度 + 法线纹理的声明:
sampler2D _CameraDepthNormalsTexture //一般RG通道存法线,BA通道存深度
1.2.3.获取深度值
1.用SAMPLE_DEPTH_TEXTURE(深度纹理,uv坐标)对深度纹理进行采样,所得结果是非线性的
2.使用LinearEyeDepth将非线性的深度值转换到观察空间下;
或用Linear01Depth将非线性的深度值,转换到01区间内的线性深度值,同样是观察空间
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
float viewDepth = LinearEyeDepth(depth);
float linearDepth = Linear01Depth(depth);
1.2.4.获取法线纹理
1.先使用tex2D对深度法线纹理进行采样得到depthNormal的float4变量。
2.使用DecodeDepthNormal获取01间的深度值和观察空间下的法线信息,想当于一次处理深度和法线。其参数为(depthNormal,depth(float类型),normal(float3类型)),后两个参数需要自己声明,名字自定义
3.也可单独获取某一个量:
DecodeFloatRG(depthNormal.zw):单独获取深度
DecodeViewNormalStereo(depthNormal):单独获取法线
1.3.背后的原理
1.3.1.深度纹理中存储的是什么信息
是进行裁剪空间变换后的z分量再转换到01之后的结果,因为齐次裁剪空间坐标范围为-1 ~ 1,纹理中存储的信息范围需要是0 ~ 1,所以进行如下变换:
深度纹理值 = 0.5 * z + 0.5;
1.3.2.法线纹理中存储的是什么信息
是观察空间下的 法线 再转换到0-1之后的结果。同样的,由于观察空间下范围为 -1 ~ 1(前提它是个单位向量),而纹理中要存储0 ~ 1,所以会进行如下变换:
法线纹理值 = (观察空间下法线 + 1)* 0.5
1.3.3.unity是如何得到深度和法线纹理的
通常分为两种途径(具体使用哪个,取决于使用的渲染路径和设备的硬件限制):
1.从G-buffer几何缓冲区中获取
2.由一个专门的Pass渲染而来
当使用延迟渲染路径时,深度和法线纹理可以直接访问到。因为延迟渲染路径会把信息存储到G-buffer几何缓冲区中(深度和法线等信息都存储在其中)
只有当无法直接获取到深度和法线纹理时,unity才会单独通过一个Pass来渲染,获取深度和法线信息
使用单独的pass渲染获取深度和法线纹理时,二者有所区别。
对于深度纹理:unity内部会使用 着色器替换技术 选择 渲染类型 RenderType = “Opaque”(不透明物体)的物体,然后判度胺它们的渲染队列Queue是否小于等于2500,如果满足这个条件,就会使用物体投射阴影时的Pass(LightMode = ShaderCaster)来得到深度纹理,若没有该Pass。则该物体不会出现在深度纹理中。
即:获取深度纹理需要正确的RenderType标签 和 有阴影投射的Pass
对于法线纹理:unity底层会使用一个单独的Pass把整个场景再渲染一次,从而得到深度和法线纹理。因此,获取法线纹理时,往往就会一块获取深度纹理
1.3.4.深度和法线纹理使用时调用的函数原理
SAMPLE_DEPTH_TEXTURE:它是用来采样的,相较于tex2D,他会帮忙适配平台,他采样的深度值是裁剪空间下的z分量转换到01之间的结果,并且是非线性的,即在透视摄像机的裁剪空间中,深度值分布不均匀。
深度值离近裁减面近时,深度值变化迅速,精度高。反之则缓慢,精度低。
简而言之,远的东西变化不明显。
而将裁剪空间下的深度值转换到观察空间下,才可以得到线性的深度值
LinearEyeDepth:像素到摄像机的实际距离
Linear01Depth:被压缩到01之间的值
1.3.5.注
直接采样出来的深度和法线信息是不会直接使用的,我们需要将其转换。以得到得到我们最终会使用的 观察空间下的深度和法线信息
2.查看深度和法线纹理
2.1.查看深度信息
将深度值作为颜色的RGB显示在屏幕上即可
//Shader部分
Shader "Models_5/WatchDepth"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _CameraDepthTexture;
v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return data ;
}
fixed4 frag (v2f f) : SV_Target
{
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, f.uv);
float linearDepth = Linear01Depth(depth);
return fixed4(linearDepth, linearDepth, linearDepth, 1);
}
ENDCG
}
}
Fallback off
}
//c#部分
using UnityEngine;
public class Leeson77_WatchDepth : Lesson69_Basic
{
/// <summary>
/// 一上来就是纯黑和纯白的原因是:远裁剪面设置的太远了
/// </summary>
private void Start()
{
Camera.main.depthTextureMode = DepthTextureMode.Depth;
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
base.OnRenderImage(source, destination);
}
}
2.2.查看法线信息
同样,将法线作为RGB
//Shader部分
Shader "Models_5/WatchNormal"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _CameraDepthNormalsTexture;
v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return data;
}
fixed4 frag (v2f f) : SV_Target
{
float depth;
float3 normals;
float4 depthNormal = tex2D(_CameraDepthNormalsTexture, f.uv);
DecodeDepthNormal(depthNormal, depth, normals);
depth = DecodeFloatRG(depthNormal.zw); //zw是深度
normals = DecodeViewNormalStereo(depthNormal); //xy是法线
return fixed4(normals, 1);
}
ENDCG
}
}
Fallback off
}
//c#部分
using UnityEngine;
public class Lesson77_WatchNormal : Lesson69_Basic
{
private void Start()
{
Camera.main.depthTextureMode = DepthTextureMode.DepthNormals;
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
base.OnRenderImage(source, destination);
}
}
3.后处理效果——运动模糊
3.1.概述
之前采取的是:将之前的图像不断叠加的方式。而这里我们要使用 速度缓存 的方式来进行,但会对其进行修改。
只需要用当前帧位置和上一帧位置进行计算,得到位置差,从而得到该像素的速度矢量。而想要得到位置差,可以利用深度纹理中的信息来进行计算
需要注意的是:
1.这种方式只适合场景静止,即摄像机快速移动的情况
2.该实现方式并不是基于真实的物理运动规律来计算的,只是一种近似计算
3.2.基本原理
得到像素当前帧和上一帧在裁剪空间下的位置,利用两个位置计算出物体的运动方向,从而模拟出运动模糊的效果。
3.2.1.得到像素上一帧和当前帧在裁剪空间的位置
1.利用uv坐标和深度值组合成一个裁剪空间下的组合坐标 nowClipPos,即:
float4 clipPos = float4(uv.x, ux.y, depth, 1);
而为了让0~1映射到-1~1,还需要对其进行变换:
float4 clipPos = float4(uv.x * 2 - 1, uv.y * 2 - 1, depth * 2 - 1, 1);
2.利用这一帧的世界空间到裁剪空间的变换矩阵 nowM 的逆矩阵 nowM^-1,将刚才所得的裁剪空间下的点转换到世界空间。
可以使用c#中摄像机的一些内置函数来获取世界空间到裁剪空间的变换矩阵:
//相机投影矩阵(观察空间)到裁剪空间的变换矩阵
camera.projectionMatrix;
//世界空间到观察空间的变换矩阵
camera.worldToCameraMatrix;
//世界空间 到 裁剪空间的变换矩阵
Matrix4x4 worldToClipNatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
//裁剪空间 到 世界空间的变换矩阵
worldToClipMatrix.inverse
用上面的矩阵就可实现坐标的相互变换
3.利用上一帧的世界空间到裁剪空间的变换矩阵 oldM 得到上一帧下,nowClipPos所在的位置oldClipPos(使用nowClipPos获取对应世界坐标,因为只有摄像机动,所以其世界坐标不变,然后该世界坐标与oldM计算得到oldClipPos)
3.2.2.运动方向
当前位置.xy - 上一帧位置.xy 便可以得到移动方向
3.2.3.如何模拟运动模糊效果
利用这个方向在纹理中进行多次uv坐标偏移采样,将得到的颜色累加起来,最后进行算数平均值计算即可。
我们会加入一个模糊偏移量来控制模糊程度。只要在每次采样时进行 方向 * 模糊偏移量 的偏移采样即可。
uv += 方向 * 模糊偏移量
还可以加入一个次数变量,来控制uv坐标偏移采样的次数
需要注意的是:ShaderLab没有矩阵类型的变量,所以直接在CG中声明对应属性,然后在c#代码中设置就行
3.3.实现
//Shader部分
Shader "Unlit/Lesson78_MotionBlur2"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_BlurOffset("BlurOffset", Range(0, 1)) = 0.1
_LoopTimes("LoopTimes", Int) = 3
}
SubShader
{
Tags { "RenderType"="Opaque" }
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float2 uv_depth : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
float _BlurOffset;
int _LoopTimes;
sampler2D _CameraDepthTexture;
float4x4 _ClipToWorldMatrix;
float4x4 _FrontWorldToClipMatrix;
v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
data.uv_depth = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
{
data.uv_depth.y = 1 - data.uv_depth.y;
}
#endif
return data;
}
fixed4 frag (v2f f) : SV_Target
{
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, f.uv_depth);
float linearDepth = Linear01Depth(depth);
//当前帧的坐标
float4 nowClipPos = float4(f.uv.x * 2 - 1, f.uv.y * 2 - 1, depth * 2 - 1, 1);
float4 nowWorldPos = mul(_ClipToWorldMatrix, nowClipPos);
//透视除法
nowWorldPos /= nowWorldPos.w;
float4 oldClipPos = mul(_FrontWorldToClipMatrix, nowWorldPos);
//透视除法
oldClipPos /= oldClipPos.w;
float2 moveDir = nowClipPos.xy - oldClipPos.xy; //
//不直接使用f的uv,别的地方可能会用
float2 uv = f.uv;
float4 blurColor = float4(0, 0, 0, 0);
for (int i = 0; i < _LoopTimes; i++)
{
blurColor += tex2D(_MainTex, uv);
uv += moveDir * _BlurOffset;
}
blurColor /= _LoopTimes;
return fixed4(blurColor.rgb, 1);
}
ENDCG
}
}
Fallback off
}
//c#部分
using UnityEngine;
public class Lesson78_MotionBlur2 : Lesson69_Basic
{
[Range(0, 1)]
public float blurOffset;
[Range(1, 10)]
public int loopTimes;
private Matrix4x4 frontWorldToClipMatrix;
private void Start()
{
Camera.main.depthTextureMode = DepthTextureMode.Depth;
}
private void OnEnable()
{
frontWorldToClipMatrix = Camera.main.worldToCameraMatrix * Camera.main.projectionMatrix;
}
protected override void UpdateProperty()
{
Matrix4x4 worldToClipMatrix = Camera.main.worldToCameraMatrix * Camera.main.projectionMatrix;
Mat.SetMatrix("_FrontWorldToClipMatrix", frontWorldToClipMatrix);
Mat.SetMatrix("_ClipToWorldMatrix", worldToClipMatrix.inverse);
frontWorldToClipMatrix = worldToClipMatrix;
Mat.SetFloat("_BlurOffset", blurOffset);
Mat.SetInt("_LoopTimes", loopTimes);
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (Mat != null)
{
UpdateProperty();
Graphics.Blit(source, destination, Mat);
}
else
{
Graphics.Blit(source, destination);
}
}
}
3.4.问题
这个方法有个严重的问题:移动摄像机的位置(不是旋转)时,会出现严重的鬼畜(?)
4.后处理效果——全局雾效
4.1.是什么
是一种视觉效果,用于在3d场景中模拟大气中的雾气对远处物体的遮挡。其会使离摄像机较远的物体看起来逐渐被雾气遮盖。
4.2.unity自带的全局雾效
4.2.1.开启
在Window->Rendering->Lighting窗口中的 Environment 环境页签中开启,勾选fog
然后在shader代码中实现UNITY_FOG_COORDS(),UNITY_TRANSFER_FOG(),UNITY_APPLY_FOG(),
实际上就是一个Unlit着色器被创建出来时候自带的那几句。
4.2.2.雾气的计算模式(也是自带雾效的重要参数)
首先要说明,几种计算模式都是在计算雾的混合因子 f,然后才会根据公式
最终颜色 = (1 - f) * 物体颜色 + f * 雾颜色
来计算雾
1.Linear:f = (end - |d|)/(end - start)->线性
d代表距离摄像机的距离,end和start分别代表雾最强和开始的距离,均是相对摄像机而言
2.Exponential:f = 1 - e^(-density * |d|) ->指数
density代表雾的浓度
3.Exponential Squared:
跟2差不多,就是变成二次方了
4.3.基于深度纹理实现全局雾效
4.3.1.为什么要自己实现
unity自带的很多效果做不了,而且需要对每个物体的shader都实现对应代码
4.3.2.基本原理(计算像素在世界空间下的位置是为了计算其离摄像机的距离)
首先,我们抛弃之前用矩阵来获取像素在世界坐标下的位置。使用另一种会获取像素世界坐标的方式:通过坐标偏移。
像素的世界坐标 = 摄像机位置 + 观察空间线性深度值 * 摄像机指向像素世界坐标的方向向量
4.3.3.获取摄像机指向像素的世界坐标的方向向量
1.屏幕后处理中处理的内容是一张抓取的屏幕图像,相当于是一个具有四个顶点的面片,摄像机近裁减面的四个角相当于是屏幕图像(后处理要处理的图像)四个顶点在世界空间中的位置。
2.我们需要通过c#代码计算四个顶点在世界空间下的射线方向后传递给顶点着色器(因为当数据传递到片元着色器时,每个像素会基于4个顶点的射线插值计算像素出对应的射线方向),四个点记为TL(top left),BL(buttom left),TR,BR 则:
TL = Camera.forward * Near + toTop - toRight;
TR = Camera.forward * Near + toTop + toRight;
BL = Camera.forward * Near - toTop - toRight;
BR = Camera.forward * Near - toTop + toRight;
其中,
Near:摄像机到近裁减面的距离
toTop:近裁减面中心点到顶边的向量 toTop = Camera.up * halfH
toRight:近裁减面中心点到右边的向量 toRight = Camera.right * halfW
halfH:近裁减面高度的一半 halfH = Near * tan(FOV / 2)
halfW:近裁减面宽度的一半 halfW = halfH * aspect
aspect:Game窗口的宽高比(宽:高),其通过 Camera.main.aspect 获取
FOV:摄像机到顶边与摄像机到底边之间夹角(摄像机的竖直夹角),直接点出来即可
需要知道的是:这时不能直接通过公式进行计算得到对应点在世界空间下的坐标,因为这时的深度值只是该点离摄像机z轴方向的距离,而非到摄像机的距离(两点之间的距离称为欧氏距离),所以需要进行处理。
根据相似三角形易得:Depth / Near = dis(两点距离) / |TL|
即:dis = Depth * (|TL| / Near)
对于近裁减面上的四个点,|TL| / Near是通用的(四个点对称,其模长都一样)
所以最后缩减为:Scale = |TL| / Near
RayTL = TL.normalized * Scale
最后使用下面公式计算即可得到世界坐标:
像素的世界坐标 = 摄像机位置 + 观察空间线性深度值 * RayTL
需要注意的是:这里的 "观察空间线性深度值" 是用LinearEyeDepth计算所得
4.4.实现
//Shader部分
Shader "Models_5/GlobalFog"
{
Properties
{
_MainTex("MainTex", 2D) = "white"{}
_FogColor("FogColor", Color) = (1, 1, 1, 1)
_FogDensity("FogDensity", Float) = 1
_FogStart("FogStart", Float) = 0
_FogEnd("FogEnd", Float) = 10
}
SubShader
{
Tags { "RenderType"="Opaque" }
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
float4 _FogColor;
float _FogDensity;
float _FogStart;
float _FogEnd;
float4x4 _RayMatrix;
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
//考虑翻转的深度纹理
float2 uv_depth : TEXCOORD1;
//顶点射线 指向四个角的方向向量 传递到片元时,会自动进行插值运算
float4 ray : TEXCOORD2;
};
//顶点着色器函数 每一个顶点就会执行一次
//对于屏幕后处理来说,就会执行四次,因为就四个点
v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv = v.texcoord.xy;
data.uv_depth = v.texcoord.xy;
//方便赋值
int index = 0;
//因为就四个顶点,所以通过中心点判断
if (v.texcoord.x < 0.5 )
{
//0, 0
if (v.texcoord.y < 0.5)
{
index = 0;
}
//0, 1
else
{
index = 3;
}
}
else
{
//1, 0
if (v.texcoord.y < 0.5)
{
index = 1;
}
//1, 1
else
{
index = 2;
}
}
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
{
data.uv_depth = 1 - data.uv_depth;
//翻转顶点的射线(或许可以通过翻转uv的方式,而不是在这里翻转index)
index = 3 - index;
}
#endif
//赋值
data.ray = _RayMatrix[index];
return data;
}
fixed4 frag (v2f f) : SV_Target
{
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, f.uv_depth);
//此乃z分量
float realDepth = LinearEyeDepth(depth);
float3 fragWorldPos = _WorldSpaceCameraPos + realDepth * f.ray;
float fogFactor = (_FogEnd - fragWorldPos.y) / (_FogEnd - _FogStart);
fogFactor = saturate(fogFactor * _FogDensity);
fixed3 finalColor = lerp(tex2D(_MainTex, f.uv).rgb, _FogColor.rgb, fogFactor);
return fixed4(finalColor, 1);
}
ENDCG
}
}
Fallback off
}
这个计算雾气的方式结合了线性以及指数
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class Lesson79_GlobalFog : Lesson69_Basic
{
public Color fogColor;
public float fogDensity;
public float fogStart;
public float fogEnd;
private void Start()
{
Camera.main.depthTextureMode = DepthTextureMode.Depth;
}
protected override void UpdateProperty()
{
float FOV = Camera.main.fieldOfView;
float aspect = Camera.main.aspect;
float near = Camera.main.nearClipPlane;
//tan用的是弧度,但是fov是角度,所以将其乘以对应变量
float halfH = near * Mathf.Tan((FOV * Mathf.Deg2Rad) / 2f);
float halfW = halfH * aspect;
Vector3 toTop = Camera.main.transform.up * halfH;
Vector3 toRight = Camera.main.transform.right * halfW;
Vector3 TL = Camera.main.transform.forward * near + toTop - toRight;
Vector3 TR = Camera.main.transform.forward * near + toTop + toRight;
Vector3 BL = Camera.main.transform.forward * near - toTop - toRight;
Vector3 BR = Camera.main.transform.forward * near - toTop + toRight;
//w为了让深度值计算出来是两点间距离,所以需要乘以一个缩放值
float scale = TL.magnitude / near;
//这里按照左下开始逆时针的顺序存储
Matrix4x4 RayMatrix = new Matrix4x4();
RayMatrix.SetRow(0, BL.normalized * scale);
RayMatrix.SetRow(1, BR.normalized * scale);
RayMatrix.SetRow(2, TR.normalized * scale);
RayMatrix.SetRow(3, TL.normalized * scale);
Mat.SetMatrix("_RayMatrix", RayMatrix);
Mat.SetColor("_FogColor", fogColor);
Mat.SetFloat("_FogDensity", fogDensity);
Mat.SetFloat("_FogStart", fogStart);
Mat.SetFloat("_FogEnd", fogEnd);
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (Mat != null)
{
UpdateProperty();
Graphics.Blit(source, destination, Mat);
}
else
{
Graphics.Blit(source, destination);
}
}
}
5.后处理效果——边缘检测
5.1.为什么这么干
基于灰度值会很乱,原因是他会将光照,阴影等也算进去,但是基于深度法线不会,只会出现基本的描边
3d推荐深度法线,2d推荐灰度值(因为2d图片深度值和法线都是一样的)
5.2.基本原理(不会进行卷积运算)
简而言之:基于 Roberts(罗伯兹)交叉算子,通过比较对角线上的像素的深度和法线值,判断是否在边缘上(不过我们不用这个交叉算子计算)
Roberts算子:Gx: -1 0 Gy: 0 -1 (虽然不会用这个就是了)
0 1 1 0
5.3.关键步骤
我们是对一个像素的两条对角线上的像素的法线值和深度值进行判断,如果两条对角线有一条的变化过大,那么就说明该像素位于边缘。
5.3.1.得到对角线上的像素
同样是利用纹素进行uv坐标偏移(类似之前的描边)。我们还可以声明一个可控的 采样偏移距离变量 _SampleDistance,他会决定描边的粗细。
其原理是:采样离中心像素越近,检测的变化越细微,深度和法线值变化小,边缘会更细
5.3.2.进行深度和法线值的比较
首先对深度和法线采样获取对应的值,再求出对角线上对角两个像素的 深度值差 和 法线值差
如果其中一个的差值大于自定义的差值(满足其一即可),那么我们认为该像素在物体的边缘上。
5.3.3.具体的比较
一次对一条对角线上的一对点(注意是一对,不算中心点)进行处理。
深度值:深度值求出来之后,直接相减求绝对值,然后乘以自定义深度敏感度变量进行比较(一般同 0.1乘以第一个点的深度 进行比较,小于就不是边缘,因为差异很小)即可
法线值:对两个点的法线进行采样,然后相减取绝对值,用该结果乘以自定义敏感度得到法线差值,最后将该法线插值的三个分量相加与0.1进行比较,小于则证明差异小,不在边缘,即为1
最后用两次比较结果相乘,但凡有一个0,那就处于边缘
5.4.实现
//Shader部分
Shader "Models_5/StrokeWithDepthNormal"
{
Properties
{
_MainTex("MainTex", 2D) = "white"{}
_DepthSensitivity("DepthSensitivity", Float) = 1
_NormalSensitivity("NormalSensitivity", Float) = 1
_EdgeColor("EdgeColor", Color) = (0, 0, 0, 0)
_BackgroundExtent("BackgroundExtent", Range(0, 1)) = 1
_BackgroundColor("BackgroundColor", Color) = (1, 1, 1, 1)
_SampleDistance("SampleDistance", Int) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv[5] : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
float _DepthSensitivity;
float _NormalSensitivity;
float4 _EdgeColor;
float _BackgroundExtent;
float4 _BackgroundColor;
int _SampleDistance;
sampler2D _CameraDepthNormalsTexture;
v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv[0] = v.texcoord.xy;
//右上和左下
data.uv[1] = data.uv[0] + _MainTex_TexelSize.xy * float2(-1, 1) * _SampleDistance;
data.uv[2] = data.uv[0] + _MainTex_TexelSize.xy * float2( 1, -1) * _SampleDistance;
//左上和右下,这个顺序没什么,反正会取绝对值,不在乎正负
data.uv[3] = data.uv[0] + _MainTex_TexelSize.xy * float2( 1, 1) * _SampleDistance;
data.uv[4] = data.uv[0] + _MainTex_TexelSize.xy * float2(-1, -1) * _SampleDistance;
return data;
}
//这个点是对 深度法线纹理 采样所得的点
float CheckSame(float4 depthNormal_1, float4 depthNormal_2)
{
//后俩是深度
float depth_1 = DecodeFloatRG(depthNormal_1.zw);
float depth_2 = DecodeFloatRG(depthNormal_2.zw);
float diffDepth = abs(depth_1 - depth_2) * _DepthSensitivity;
//小于0.1为1,即不是边缘,很接近
int depthSame = diffDepth < 0.1 * depth_1;
//float2 diffNormal = abs(depthNormal_1.xy - depthNormal_2.xy) * _NormalSensitivity;
//int normalSame = (diffNormal.x + diffNormal.y) < 0.1;
float3 normal_1 = DecodeViewNormalStereo(depthNormal_1);
float3 normal_2 = DecodeViewNormalStereo(depthNormal_2);
float3 diffNormal = abs(normal_1 - normal_2) * _NormalSensitivity;
int normalSame = (diffNormal.x + diffNormal.y + diffNormal.z) < 0.1;
int res = depthSame * normalSame;
//0为边缘,1代表这俩点相似
return res;
}
fixed4 frag (v2f f) : SV_Target
{
//这俩有一个为0,那就代表是边缘
float checkSame_1 = CheckSame(tex2D(_CameraDepthNormalsTexture, f.uv[1]), tex2D(_CameraDepthNormalsTexture, f.uv[2]));
float checkSame_2 = CheckSame(tex2D(_CameraDepthNormalsTexture, f.uv[3]), tex2D(_CameraDepthNormalsTexture, f.uv[4]));
float isEdge = 1;
isEdge *= checkSame_1;
isEdge *= checkSame_2;
float4 mainTex = tex2D(_MainTex, f.uv[0]);
fixed4 withEdgeColor = lerp(_EdgeColor, mainTex, isEdge);
//纯色背景控制
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, isEdge);
fixed4 finalColor = lerp(withEdgeColor, onlyEdgeColor, _BackgroundExtent);
return fixed4(finalColor.rgb, 1.0);
}
ENDCG
}
}
Fallback off
}