雾效(fog)
unity 内置的雾效可以产生基于距离的线性或指数雾效。
然而,想要在自己编写的顶点/片元着色器中实现这些雾效,需要在shader中添加【#pragma multi_compile_fog】指令,同时还需要使用相关的内置宏,例如【UNITY_FOG_COORDS】【UNITY_TRANSFER_FOG】【UNITY_APPLY_FOG】等,缺点在于不仅需要为场景中所有物体添加相关的渲染代码,而且能够实现的效果也非常有限,当需要对雾效进行一些个性化操作时,例如使用基于高度的雾效等,仅仅使用Unity内置的雾效就变得不再可行
基于屏幕后处理的全局雾效的关键是:根据深度纹理来重建每个像素在世界空间下的位置
即构建出当前像素的NDC坐标,再通过当前摄像机的视角*投影矩阵的逆矩阵来得到世界空间下的像素坐标,但是,这样的实现需要在片元着色器中进行矩阵乘法的操作,而这通常会影响游戏性能。
本节会学习一个快速从深度纹理中重建世界坐标的方法:
这种方法首先对图像空间下的视椎体射线(从摄像机出发,指向图像上的某点的射线)进行插值,这条射线储存了该图像在世界空间下到摄像机 的方向信息,
然后我们把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置,当得到世界坐标后,就可以轻松使用各个公式来模拟全局雾效了。
如何从深度纹理中重建世界坐标:
坐标系中的一个顶点坐标可以通过它相对于另一个顶点坐标的偏移量来求得,重建像素的世界坐标也是基于这个想法。只需要知道摄像机在世界空间下的位置,以及世界空间下该像素相对于摄像机的偏移量,把他们相加就可以得到该像素到摄像机的世界坐标。整个过程可以用下面的代码来表示
float4 _WorldPos=_WorldSpaceCameraPos+linearDepth*interpolatedRay;
_WorldSpaceCameraPos:摄像机在世界空间下的位置,这个可以由unity内置的变量直接访问得到
linearDepth:由深度纹理得到的线性深度值(13.1.2讲过获取原理)
interpolatedRay:由定点着色器输出并插值后得到的射线,他不仅包含了该像素到摄像机的方向,也包含了距离信息
linearDepth*interpolatedRay:得到相对于摄像机 的偏移量
P278
//全局雾效 13.3
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FogWithDepthTexture : PostEffectsBase{
public Shader fogShader;
private Material fogMaterial = null;
public Material material
{
get
{
fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
return fogMaterial;
}
}
/*
需要获取摄像机的相关参数,如裁剪平面的距离,FOV等
同时还需要获取摄像机在世界空间下的前方,上方和右方等方向
因此用两个变量储存摄像机的Camera组件和Transform组件
*/
private Camera myCamera;
public Camera camera {
get
{
if (myCamera ==null)
{
myCamera = GetComponent();
}
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;//控制雾效的终止高度
/*由于需要获取摄像机的深度纹理,在脚本的OnEnable函数中设置摄像机的相应状态*/
private void OnEnable()
{
camera.depthTextureMode |= DepthTextureMode.Depth;
}
//实现OnRenderImage函数
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{/*首先计算了近似裁剪平面的四个对应角对应的向量,并把他们储存在一个矩阵类型的变量(frustumCorners)
按一定顺序把这四个方向储存到frustumConers不同的行中,这个顺序非常重要,因为决定了我们在顶点着色器中使用哪一行作为该点的待插值向量。
随后,把结果和其他参数传递给材质,并调用Graphics.Blit把渲染结果显示在屏幕上
*/
Matrix4x4 frustumCorners = Matrix4x4.identity;//矩阵类型变量
float fov = camera.fieldOfView;//FOV
float near = camera.nearClipPlane;
float far = camera.farClipPlane;
float aspect = camera.aspect;//方向
/*计算辅助向量
toTop:起点位于裁平面中心,指向摄像机正上方
toRight:起点位于裁平面中心,指向摄像机正右方
Near:近裁剪平面的距离
FOV:是竖直方向的视角范围
camera.up:摄像机的正上方
camera.right:摄像机的正右方
halfHeight=Near*tan[Fov/2]
toTop=camera.up*halfHeight
toRight=camera.right*halfHeight*aspect
*/
float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);//
Vector3 toRight = cameraTransform.right * halfHeight * aspect;
Vector3 toTop = cameraTransform.up * halfHeight;
/*
topLeft【3】---------------topRight【2】
| |
| |
bottomLeft【0】----------bottomRight【1】 */
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.SetMatrix("_ViewProjectionInverseMatrix", (camera.projectionMatrix * camera.worldToCameraMatrix).inverse);
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);
}
}
}
//全局雾效 13.3
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/Chapter13-FogWithDepthTexture"
{
Properties
{
_MainTex ("Base(RGB)", 2D) = "white" {}
_FogDensity("Fog Density",Float) = 1.0
_FogStart("Fog Start",Color) = (1,1,1,1)
_FogEnd("Fog End",Float) = 1.0
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
float4x4 _FrustumCornersRay;//虽然没有在Properies中声明,但仍可以由脚本传递给shader
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;//深度纹理 unity会在背后把深度纹理传递给该值
half _FogDensity;//雾的浓度
fixed4 _FogColor;//雾的颜色
float _FogStart;//雾起始位置
float _FogEnd;//雾结束位置
/*我们定义顶点位置,屏幕图像和深度纹理的纹理坐标,InterpolatedRay变量储存插值之后的像素向量*/
struct v2f {
float4 pos:SV_POSITION;//顶点位置
half2 uv:TEXCOORD0;//屏幕图像
half2 uv_depth :TEXCOORD1;//深度纹理的纹理坐标
float4 interpolatedRay:TEXCOORD2;//插值之后的像素向量
};
/*对深度纹理的采样坐标进行平台差异化处理,
更重要的是,要决定该点对应了4个角中的哪个角,
采用的方法 是判断他的纹理坐标,
unity (0.1)--------(1.1)
| | =>这个对应关系和脚本中的frustumCorners的赋值顺序一致
(0,0)-------(1.0)
DirectX (0.0)--------(1.0)=>但大多数情况下
Metal | |
(0.1)--------(1.1)
但大多数情况下unity会把这些平台下的屏幕图像进行翻转,因此我们仍可以利用这个条件,
但如果在类似DirectX的平台上开启了抗锯齿,unity就不会进行翻转,
为了此时仍然可以得到相应顶点位置索引值,我们对索引值也进行了平台差异化处理,以便在必要时也对索引值进行翻转
最后,我们使用索引值来获取_FrustumCornersRay中对应的行为为该顶点的interPolatedRay值
尽管使用了很多判断语句,但由于屏幕后处理所用的模型是一个四边形网格,只包含4个顶点,因此这些操作不会对性能造成很大影响
*/
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{
/*首先,需要重建该像素在世界空间中的位置
首先使用SAMPLE_DEPTH_TEXTURE对深度纹理进行采样,再用LinearEyeDepth得到视角空间下的线性深度值
之后,与interpolatedRay相乘后和世界空间下的摄像机位置相加,即可得到世界空间下的位置*/
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth));
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i. interpolatedRay.xyz;
/*得到世界坐标后,根据材质属性_FogEnd和_FogStart计算当前的像素高度worldPOS.y对应的雾效系数fogDensity
再和参数_FogDensity相乘后,利用saturate函数截取到[0,1]范围内,作为最后的雾效系数*/
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
{
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}Fallback off
}
本节介绍的使用深度纹理重建像素的世界坐标方法是非常有用的额,但需要注意的是这里的实现是基于摄像机的投影类型是透视的前提下,如果需要在正交投影的情况下重建世界坐标需要使用不同的公式