1 前言
屏幕深度和法线纹理简介中对深度和法线纹理的来源、使用及推导过程进行了讲解,激光雷达特效中讲述了一种重构屏幕像素点世界坐标的方法,本文将介绍使用深度纹理重构屏幕像素点在相机坐标系下的坐标计算方法,并使用重构后的坐标模拟雾效。
雾效即离观察者越远的点越趋近于雾的颜色,并且雾的浓度越大。本文将使用屏幕后处理技术,计算每个顶点与相机的距离,并根据距离计算雾的浓度,依据浓度给该像素点混合原始颜色与雾效颜色。
本文完整资源见→Unity3D全局雾效。
2 由深度纹理重构相机坐标系下坐标
1)重构像素点在相机坐标系下坐标
对于屏幕上的任意一点,它对应的相机坐标系中的点记为 P,对应的近裁剪平面上的点记为 Q,相机位置记为 O(坐标为 (0, 0, 0)),假设 P 点的深度为 depth(由 LinearEyeDepth 函数获取),相机到近平面的距离为 near,如下图所示。
根据上图,可以列出以下方程组关系。其中,公式 2 由三角形相似原理得到,公式 3 由 O、P、Q 三点共线得到。
化简得:
Q 点在近平面上,可以通过近裁剪平面的四个角插值得到,O 和 near 为定值,因此 (OQ / near) 也可以通过插值得到。假设近裁剪平面的四个角分别为 A、B、C、D,我们将 (OA / near)、(OB / near)、(OC / near)、(OD / near) 输入顶点着色器中,光珊化会自动为我们计算插值后的 (OQ / near)。
如下,我们可以在插值寄存器中定义变量 interpolatedRay,用于存储向量 (OQ / near)。
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间顶点坐标
half2 uv : TEXCOORD0; // 纹理uv坐标,
float4 interpolatedRay : TEXCOORD1; // 插值射线向量(由相机指向近平面上点的向量除以near后的坐标)
};
2)近裁剪平面四角射线向量计算
记近裁剪平面上左下角、右下角、右上角、左上角、中心、右中心、上中心顶点分别为 A、B、C、D、Q、E、F,相机位置为 O 点,如下:
根据几何关系,可以计算向量 OA、OB、OC、OD 如下:
假设像机竖直方向的视野角度为 fov(通过 camera.fieldOfView 获取),屏幕宽高比为 aspect(通过 camera.aspect 获取),相机距离近裁剪平面的距离为 near(通过 camera.nearClipPlane 获取),相机向右、向上、向前方向的单位方向向量分别为 right(坐标为 (1, 0, 0))、up(坐标为 (0, 1, 0)、forward(坐标为 (0, 0, 1),则向量 OQ、QE、QF 的计算如下:
假设摄像机竖直方向的视野角度为 fov(通过 camera.fieldOfView 获取),屏幕宽高比为 aspect(通过 camera.aspect 获取),相机距离近裁剪平面的距离为 near(通过 camera.nearClipPlane 获取),相机向右、向上、向前方向的单位方向向量分别为 right、up、forward(通过 camera.transform 组件获取),则向量 OQ、QE、QF 的计算如下:
2 雾效因子计算
雾效因子的计算一般采样线性衰减、反比衰减、指数衰减等方法。假设顶点与相机间的距离为 dist,雾效因子为 factor,则 factor 的计算如下。
1)线性衰减雾效因子
其中,dist_min 和 dist_max 分别为受雾效影响的最小距离和最大距离。
2)反比衰减雾效因子
其中,r 为衰减速率参数。
3)指数衰减雾效因子
其中,r 为衰减速率参数。
3 雾效实现
FogEffect.cs
using UnityEngine;
[RequireComponent(typeof(Camera))] // 需要相机组件
public class FogEffect : MonoBehaviour {
public bool enableFog = true; // 是否开启雾效
public Color fogColor = Color.red; // 雾的颜色
[Range(0, 20)]
public float minDist = 1f; // 雾的最近距离(线性衰减函数才生效)
[Range(30, 1000)]
public float maxDist = 20; // 雾的最远距离(线性衰减函数才生效)
[Range(0, 1)]
public float r = 0.01f; // 衰减速率参数(反比衰减和指数衰减函数才生效)
private Camera cam; // 相机
private Material material = null; // 材质
private void Awake() {
cam = GetComponent<Camera>();
material = new Material(Shader.Find("MyShader/FogEffect"));
material.hideFlags = HideFlags.DontSave;
}
private void OnValidate() {
if (material != null) {
material.SetFloat("_Enable", enableFog ? 1 : 0);
}
}
private void OnEnable() {
cam.depthTextureMode |= DepthTextureMode.Depth;
material.SetFloat("_Enable", enableFog ? 1 : 0);
}
private void Update() {
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (Mathf.Abs(scroll) > 0)
{ // 缩放场景
cam.transform.position += cam.transform.forward * scroll * 10;
}
}
private void OnRenderImage(RenderTexture src, RenderTexture dest) {
if (enableFog) {
Matrix4x4 frustumCorners = GetFrustumCornersRay();
material.SetMatrix("_FrustumCornersRay", frustumCorners);
material.SetColor("_FogColor", fogColor);
material.SetFloat("_MinDist", minDist);
material.SetFloat("_MaxDist", maxDist);
material.SetFloat("_R", r);
Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
private Matrix4x4 GetFrustumCornersRay() { // 获取插值射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
Matrix4x4 frustumCorners = Matrix4x4.identity;
float fov = cam.fieldOfView;
float near = cam.nearClipPlane;
float aspect = cam.aspect;
float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
Vector3 toRight = Vector3.right * halfHeight * aspect; // 指向右方的向量
Vector3 toTop = Vector3.up * halfHeight; // 指向上方的向量
Vector3 toForward = Vector3.forward * near; // 指向前方的向量
Vector3 bottomLeft = (toForward - toTop - toRight) / near; // 指向左下角的射线
Vector3 bottomRight = (toForward + toRight - toTop) / near; // 指向右下角的射线
Vector3 topRight = (toForward + toRight + toTop) / near; // 指向右上角的射线
Vector3 topLeft = (toForward + toTop - toRight) / near; // 指向左上角的射线
frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);
return frustumCorners;
}
}
FogEffect.shader
Shader "MyShader/FogEffect" { // 雷达波特效
Properties{
_MainTex("Base (RGB)", 2D) = "white" {} // 主纹理
_Enable("Enable", Int) = 0 // 是否启动雾效
_FogColor("FogColor", Color) = (1, 0, 0, 1) // 雾的颜色
_MinDist("MinDist", Float) = 5 // 雾的最近距离(线性衰减函数才生效)
_MaxDist("MaxDist", Float) = 300 // 雾的最远距离(线性衰减函数才生效)
_R("Density", Float) = 0.01 // 衰减速率参数(反比衰减和指数衰减函数才生效)
}
SubShader{
Pass {
// 深度测试始终通过, 关闭深度写入
ZTest Always ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex; // 主纹理
sampler2D _CameraDepthTexture; // 深度纹理
float4x4 _FrustumCornersRay; // 视锥体四角射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
int _Enable; // 是否启动雾效
fixed4 _FogColor; // 雾的颜色
float _MinDist; // 雾的最近距离(线性衰减函数才生效)
float _MaxDist; // 雾的最远距离(线性衰减函数才生效)
float _R; // 衰减速率参数(反比衰减和指数衰减函数才生效)
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间顶点坐标
half2 uv : TEXCOORD0; // 纹理uv坐标,
float4 interpolatedRay : TEXCOORD1; // 插值射线向量(由相机指向近平面上点的向量除以near后的坐标)
};
float4 getInterpolatedRay(half2 uv) { // 获取插值射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
int index = 0;
if (uv.x < 0.5 && uv.y < 0.5) {
index = 0;
} else if (uv.x > 0.5 && uv.y < 0.5) {
index = 1;
} else if (uv.x > 0.5 && uv.y > 0.5) {
index = 2;
} else {
index = 3;
}
return _FrustumCornersRay[index];
}
float getFactory(float len) { // 获取雾效因子
float factor = saturate((_MaxDist - len) / (_MaxDist - _MinDist)); // 线性雾效衰减因子
//float factor = 1 / (_R * len); // 反比雾效衰减因子
//float factor = exp(-_R * len); // 指数雾效衰减因子
return factor;
}
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); // 计算裁剪坐标系中顶点坐标, 等价于: mul(unity_MatrixMVP, v.vertex)
o.uv = v.texcoord;
o.interpolatedRay = getInterpolatedRay(v.texcoord); // 获取插值射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
return o;
}
fixed4 frag(v2f i) : SV_Target{ // v2f_img为内置结构体, 里面只包含pos和uv
if (_Enable == 0) {
return tex2D(_MainTex, i.uv);
}
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // 非线性的深度, tex2D(_CameraDepthTexture, i.uv).r
float linearDepth = LinearEyeDepth(depth); // 线性的深度
//if (linearDepth > _ProjectionParams.z - 2) {
// return tex2D(_MainTex, i.uv); // 天空不参与雾效
//}
float3 viewPos = linearDepth * i.interpolatedRay.xyz; // 顶点在相机坐标系下的坐标(不是观察坐标系, 观察坐标系与相机坐标系z轴方向相反)
float len = length(viewPos);
float factor = getFactory(len); // 获取雾效因子
fixed4 tex = tex2D(_MainTex, i.uv);
fixed4 color = lerp(_FogColor, tex, factor);
return color;
}
ENDCG
}
}
FallBack off
}
运行效果: