《Unity Shader入门精要》笔记(二十五)
本文为《Unity Shader入门精要》第十三章《使用深度和法线纹理》的第三节内容《全局雾效》。本文相关代码,详见:
原书代码,详见原作者github:
<hr/>1. 概念原理
1.1 Unity内置的雾效
在Shader中添加#pragma multi_compile_fog指令,需要用到相关的内置宏:UNITY_FOG_COORDS、UNITY_TRANSFER_FOG、UNITY_APPLY_FOG。
缺点:
[*]需要所有物体添加相关的代码
[*]实现的效果有限
1.2 全局雾效
本节将学习基于屏幕后处理的全局无效的实现,无需修改场景内物体的Shader代码,仅需要一次后处理。
优点:
[*]自由度高
1.3 全局雾效实现原理
关键:根据深度纹理来重建每个像素在世界空间下的位置。
(上一节运动模糊中实现过,但运算是在片元着色器中实现的,影响性能)
快速从深度纹理中重构世界坐标的方法:
[*]对图像空间下的视椎体射线进行插值
[*](视椎体射线:从相机触发,指向图像上的某点的射线,它存储了该像素在世界空间下到摄像机的方向信息)
[*]把射线和线性化后的视角空间下的深度值相乘,加上相机的世界位置,得到该像素在世界空间下的位置
1.3.1 重建世界坐标
思想:坐标系的一个顶点可以通过它相对的另一个坐标的偏移量求得。
像素的世界坐标 = 相机的世界坐标 + 像素相对于相机的偏移量:
// _WorldSpaceCameraPos:内置变量,相机在世界空间下的位置
// linearDepth:由深度纹理得到的线性深度值
// interpolatedRay:由顶点着色器输出并插值后得到的射线,包含方向和距离信息
float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;
interpolatedRay的求法:
源于对近裁剪平面的4个角的某个特定向量的插值(包含他们到相机的方向和距离信息),可利用相机的近裁剪平面、FOV、横纵比计算得到。
计算toTop和toRight:
// Near近裁剪平面的距离
// FOV 竖直方向的视角范围
halfHeight = Near X tan(FOV / 2)
// camera.up:相机正上方
toTop = camera.up X halfHeight
// camera.right:相机正右方
toRight = camera.right X halfHeight · aspect
计算4个角相对相机的方向:
TL = camera.forward · Near + toTop - toRight
TR = camera.forward · Near + toTop + toRight
BL = camera.forward · Near - toTop - toRight
BR = camera.forward · Near - toTop + toRight
注意:线性深度值并非点到相机欧式距离,而是z方向上的距离,不能直接使用深度值和4个角的单位方向的乘积来计算它们到相机的偏移量。
线性深度实现思路:
相似三角形原理,相机的深度值 / 它到相机的实际距离 = 近裁剪平面的距离 / TL(或TR、BL、BR)向量的模。
depth / dist = Near / |TL|
因此得到TL距离摄像机的欧氏距离dist:
dist = |TL| / Near X depth
因4个点相互对称,其他3个向量的模和TL相等,所以它们可共用同一个因子和单位向量相乘,得到对应的向量值:
scale = |TL| / Near
RayTL = TL / |TL| X scale
RayTR = TR / |TR| X scale
RayBL = BL / |BL| X scale
RayBR = BR / |BR| X scale
屏幕后处理原理:
使用特定的材质去渲染一个刚好填充整个屏幕的四边形面片。
[*]这个四边形的4个订单对应了近裁剪平面的4个角,因此可以把上面的计算结果传递给顶点着色器
[*]顶点着色器根据当前的位置选择它对应的向量,然后将其输出
[*]经插值后传递给片元着色器,得到interpolatedRay
[*]利用本小节最开始的公式重建该像素在世界空间下的位置
1.3.2 雾的计算
一个无效系数f:作为混合原始颜色和雾的颜色的混合系数。
float3 afterFog = f * fogColor + (1 - f) * originColor;
Unity内置的雾效实现支持3种计算方式:线性、指数、指数的平方。
雾效系数f的计算公式对应分别是:
// -- 线性 --
// dMin、dMax分别表示受雾影响的最小距离、最大距离
f = (dMax - |z|) / (dMax - dMin);
// -- 指数 --
// d:控制雾的浓度的参数
f = e ^ (-d · |z|)
// -- 指数的平方 --
// d:控制雾的浓度的参数
f = e ^ ((-d · |z|) ^ 2)
接下来使用类似线性雾的计算方式,计算基于高度的雾效:
// hEnd、hStart分别表示受雾影响的起始高度和终止高度
f = (hEnd - y) / (hEnd - hStart)
2. 案例:基于高度的全局雾效
2.1 效果预览
本案例将实现如下效果:
2.2 准备工作
完成如下准备工作:
[*]新建名为Scene_13_3的场景,并去掉天空盒子;
[*]搭建一个雾效场景,构建包含3面墙的房间,并放置几个立方体,详细可参考Git工程里对应的场景布置;
[*]拖拽一个控制相机不断围绕一个中心运动的脚本Translating.cs到相机组件,详细可从工程中找;
[*]新建一个名为FogWithDepthTexture的C#脚本,并拖拽给相机;
[*]新建一个名为Chapter13-FogWithDepthTexture的Unity Shader,并拖拽到上一步创建的脚本里的Fog Shader属性;
[*]保存场景。
2.3 编写C#脚本:FogWithDepthTexture
编写脚本如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FogWithDepthTexture : PostEffectsBase
{
// 雾效Shader
public Shader fogShader;
// 雾效材质
private Material fogMaterial = null;
public Material material
{
get
{
fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
return fogMaterial;
}
}
// 需要借助相机获取相关参数,如:近裁剪平面、FOV等
private Camera myCamera;
public Camera camera
{
get
{
if (myCamera == null) myCamera = GetComponent<Camera>();
return myCamera;
}
}
// 相机Transform
private Transform myCameraTransform;
public Transform cameraTransform
{
get
{
if (myCameraTransform == null) myCameraTransform = camera.transform;
return myCameraTransform;
}
}
// 雾效密度
public float fogDensity = 1.0f;
// 雾效颜色
public Color fogColor = Color.white;
// 雾效起始高度
public float fogStart = 0.0f;
// 雾效终止高度
public float fogEnd = 2.0f;
private void OnEnable()
{
// 为了在Shader中获取深度纹理
camera.depthTextureMode |= DepthTextureMode.Depth;
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material == null)
{
Graphics.Blit(source, destination);
return;
}
// 用4X4的矩阵记录4个角的位置
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 - toRight - toTop;
bottomLeft.Normalize();
bottomLeft *= scale;
Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
bottomRight.Normalize();
bottomRight *= scale;
// 将四个角的向量存储到矩阵中
frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);
// 往Shader中传入属性的值
material.SetMatrix(&#34;_FrustumCornersRay&#34;, frustumCorners);
material.SetMatrix(&#34;_ViewProjectionInverseMatrix&#34;, (camera.projectionMatrix * camera.worldToCameraMatrix).inverse);
material.SetFloat(&#34;_FogDensity&#34;, fogDensity);
material.SetColor(&#34;_FogColor&#34;, fogColor);
material.SetFloat(&#34;_FogStart&#34;, fogStart);
material.SetFloat(&#34;_FogEnd&#34;, fogEnd);
Graphics.Blit(source, destination, material);
}
}
2.4 编写Shader:Chapter13-FogWithDepthTexture
编写代码如下:
Shader &#34;Unity Shaders Book/Chapter 13/Fog With Depth Texture&#34;
{
Properties
{
_MainTex (&#34;Base (RGB)&#34;, 2D) = &#34;white&#34; {}
// 雾效最大密度
_FogDensity (&#34;Fog Density&#34;, Float) = 1.0
// 雾效颜色
_FogColor (&#34;Fog Color&#34;, Color) = (1, 1, 1, 1)
// 雾效起点高度
_FogStart (&#34;Fog Start&#34;, Float) = 0.0
// 雾效终点高度
_FogEnd (&#34;Fog End&#34;, Float) = 1.0
}
SubShader
{
CGINCLUDE
#include &#34;UnityCG.cginc&#34;
// 记录相机到画面四个角的向量
// (没有在Properties里声明,但是可以通过脚本传递到Shader)
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;
// 深度纹理UV
o.uv_depth = v.texcoord;
// 兼容不同平台
// 通常Unity会对DirectX、Metal这样的平台(他们的(0,0)在左上角)的图像进行翻转,
// 但如果这些平台开启了抗锯齿,Unity不会进行翻转
// 所以我们需要根据纹素的正负来判定图像是否需要翻转
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif
// 使用索引值,来获取_FrustCornersRay中对应的行作为该顶点的interpolatedRay值
int index = 0;
float x = v.texcoord.x;
float y = v.texcoord.y;
// 一般使用if会造成比较大的性能问题,但本案例中用到的模型是一个四边形网格,质保函4个顶点,所以影响不大
if (x < 0.5 && y < 0.5)
index = 0;
else if (x > 0.5 && y < 0.5)
index = 1;
else if (x > 0.5 && 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;
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;
// 基于y分量计算得到归一化的雾效密度
float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
// 与雾效最大密度相乘,得到实际的雾效密度
fogDensity = saturate(fogDensity * _FogDensity);
// 主纹理采样
fixed4 finalColor = tex2D(_MainTex, i.uv);
// 根据雾效密度,在实际颜色和雾效颜色之间进行差值
finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
return finalColor;
}
ENDCG
// 雾效Pass
Pass
{
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
Fallback Off
}
2.5 参数配置
配置好脚本必要的参数,即可得到最终的效果:
3. 注意
本案例基于透视投影的相机进行实现,若是正交投影的相机,计算方式会有不同。原作者给出了文章链接,但已经失效,感兴趣的小伙伴可自行查阅资料。
以上是本次笔记的所有内容,下一篇笔记,我们将学习《再谈边缘检测》的相关知识。
<hr/>
写在最后
本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder)
页:
[1]