翻译14 Fog

应用雾到游戏对象
基于距离或深度的雾
支持deferred fog

1 Forward Fog

在14之前,一直假定着光线在真空中传播,在真空中可能是精确的。但是当光线穿过大气或水就不一样了,光线在击中物体表面时会发生被吸收、散射和反射。

一个精确的大气干扰光线渲染将需要及其昂贵的体积测量方法,那是大多数现代GPU负担不起的。相反,勉强采用一些常量雾参数近似模拟。

1.1 Standard Fog

Unity光照设置包含了场景雾设置选项,默认是不启用。启用后,默认是灰色雾。Unity自带的雾只适用于使用了Forward渲染路径的物体。若激活Deferred path,提示:

image

图1 deferred 提示

image

图2 不明显的雾

1.2 Linear Fog

图2不明显,是因为Fog color灰色雾将散射和反射更多的光线,吸收较少。把Fog Color改为纯黑色试试

image

 image

图3 linear fog

雾的浓度是随视距线性增长的,在视距开头正常显示,超过这个距离就只有雾的颜色可见。

线性雾公式

image

c 是雾坐标;
S 是视距起始距离;
E 是视距终止距离;
f值 被限定在[0, 1]范围,被用在雾和物体着色之间插值。

最终计算在fragment color着色到物体对象上,雾不会影响到skybox

1.3 Exponential Fog

更接近真实感的雾

image 

image

图4 指数雾

指数雾公式

imageimage

d 是fog的密度因子;
c 是距离因子。

1.4 Exponential Squared Fog

image

image

图5 指数平方雾

指数平方雾公式

image image

1.5 Adding Fog

增加Fog到自己的shader中,增加Fog需要使用内置关键字:multi_compile_fog指令。该指令会额外增加:FOG_LINEAR、FOG_EXP、FOG_EXP2变体。

#pragma multi_compile_fog

新增ApplyFog()函数,用于在Fragment计算最终着色:获取当前颜色和插值数据作为参数,返回最终颜色。

计算步骤:
任何雾公式都是基于视距的,首先计算出视距值备用;
然后使用UnityCG.cginc宏UNITY_CALC_FOG_FACTOR_RAW根据具体雾公式计算出雾因子。
最后根据雾因子,在fog_color和当前color取插值返回。

float4 ApplyFOG(float4 color, Interpolators i)
{
    float viewDistance = length(_WorldSpaceCameraPos - i.worldPos);
    UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
    return learp(unity_FogColor, color, unityFogFactor);
}
//宏UNITY_CALC_FOG_FACTOR_RAW
#if defined(FOG_LINEAR)
    // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start))
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = (coord) * unity_FogParams.z + unity_FogParams.w
#elif defined(FOG_EXP)
    // factor = exp(-density*z)
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor)
#elif defined(FOG_EXP2)
    // factor = exp(-(density*z)^2)
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.x * (coord); unityFogFactor = exp2(-unityFogFactor*unityFogFactor)
#else
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = 0.0
#endif

//宏UNITY_CALC_FOG_FACTOR
#define UNITY_CALC_FOG_FACTOR(coord) UNITY_CALC_FOG_FACTOR_RAW(UNITY_Z_0_FAR_FROM_CLIPSPACE(coord))

//unity_FogParams 定义在ShaderVariables
// x = density / sqrt(ln(2)), useful for Exp2 mode
// y = density / ln(2), useful for Exp mode
// z = -1/(end-start), useful for Linear mode
// w = end/(end-start), useful for Linear mode
float4 unity_FogParams;
View Code

注意雾因子必须限定在[0,1]

return learp(unity_FogColor, color, saturate(unityFogFactor));

同时雾也不能影响Alpha值

color.rgb = learp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));

image

图6 linear:standard vs. mine

image

图7 exp:standard vs. mine

image

图8 exp2:standard vs. mine

1.6 Depth-Based Fog

增加深度雾支持。与Standard Shader不同的原因是计算fog坐标方法不同。虽然使用world-space视图距离是有意义的,但标准着色器使用裁剪空间深度值。因此视角不影响雾坐标。此外,在某些情况下,距离是受相机的近裁切面距离的影响,这将把雾推开一点。

image

图9 深度 (三角) vs. 距离(园)

基于深度代替距离的优点是:不必计算平方根,计算速度更快,适用于非真实渲染。缺点是:忽略视角,也即相机以原点旋转会影响雾密度,因为旋转时密度会改变。

image

图10 红到蓝旋转,深度改变密度

支持depth-based深度雾 ,必须把clip-pass裁剪空间深度值传递到片元函数。定义一个关键字:FOG_DEPTH.

#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    #define FOG_DEPTH 1
#endif

由于需要多存储一个z值,但又不能新增一个独立的变量,就把worldPos改为float4

#if defined(FOG_DEPTH)
    float4 worldPos : TEXCOORD4;
#else
    float3 worldPos : TEXCOORD4;
#endif

然后要替换i.worldPos的所有用法为i.worldPos.xyz。将剪贴空间深度值赋给i.worldPos.w,在fragment传递给viewDistance。它只是齐次剪贴空间位置的Z坐标,所以在它被转换为0-1范围内的值之前。

image

图11 incrrect

image

图12 正确

不正确的原因:可能会有反向裁剪空间Z的情况,需要转换。

#if defined(UNITY_REVERSED_Z)
    //D3d with reversed Z =>
    //z clip range is [near, 0] -> remapping to [0, far]
    //max is required to protect ourselves from near plane not being
    //correct/meaningfull in case of oblique matrices.
    #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) 
        max(((1.0-(coord)/_ProjectionParams.y)*_ProjectionParams.z),0)
#elif UNITY_UV_STARTS_AT_TOP
    //D3d without reversed z => z clip range is [0, far] -> nothing to do
    #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
#else
    //Opengl => z clip range is [-near, far] -> should remap in theory
    //but dont do it in practice to save some perf (range is close enought)
    #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
#endif

#define UNITY_CALC_FOG_FACTOR(coord) 
    UNITY_CALC_FOG_FACTOR_RAW(UNITY_Z_0_FAR_FROM_CLIPSPACE(coord))
View Code

1.7 Clip-Space Depth or World-Space Distance

增加双支持!FOG_DISTANCE 和 FOG_DEPTH。用宏代替feature指令,仿照BINORMAL_PER_FRAGMENT定义FOG_DISTANCE,默认就是它。

CGINCLUDE
    #define BINORMAL_PER_FRAGMENT
    #define FOG_DISTANCE
ENDCG
//在shader中,要切换到基于距离的雾,如果FOG_DISTANCE已经被定义,我们要做的就是去掉FOG_DEPTH的定义。
#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    #if !defined(FOG_DISTANCE)
        #define FOG_DEPTH 1
    #endif
#endif

1.8 Disabling Fog

增加支持禁用。只在需要时使用雾,增加FOG_ON宏

#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    #if !defined(FOG_DISTANCE)
        #define FOG_DEPTH 1
    #endif
    #define FOG_ON 1
#endif

float4 ApplyFog (float4 color, Interpolators i) {
    #if FOG_ON
        float viewDistance = length(_WorldSpaceCameraPos - i.worldPos.xyz);
        #if FOG_DEPTH
            viewDistance = UNITY_Z_0_FAR_FROM_CLIPSPACE(i.worldPos.w);
        #endif
        UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
        color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
    #endif
    return color;
}

1.9 Multiple Lights

增加支持多光源。但是变得更亮了,这是因为每个光的颜色都叠加到了雾色之上,所以黑色雾是没问题。

image

图13 太亮了

解决办法就是:对additive pass使用黑色雾,这样就会淡化一部分颜色。

float3 fogColor = 0;
#if defined(FORWARD_BASE_PASS)
    fogColor = unity_FogColor.rgb;
#endif
color.rgb = lerp(fogColor, color.rgb, saturate(unityFogFactor));

image

2 Deferred Fog

deferred路径没有雾,这是因为所有的光照计算完成后,才会计算雾。为了能够在deferred渲染雾,见2.1

2.1 Image Effects

要增加雾渲染,需要等所有光照计算直到它们完成后,在其他pass再次渲染雾。该pass不在shader内部,属于屏幕ImageEffects(后处理)阶段。

[ExecuteInEditMode]
public class DeferredFogRender : MonoBehaviour
{
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
    }
}

这是增加了一个全屏后处理pass。如果有多个这样实现了OnRenderImage脚本,将会按顺序依次执行。

OnRenderImage(RenderTexture source, RenderTexture destination)两个参数:
source        是已计算好最终颜色
destination 输出雾。若为空直接进入帧缓冲区

方法内部必须调用Graphics.Blit函数,它会画一个全屏面片输出到Destination

void OnRenderImage (RenderTexture source, RenderTexture destination) {
    Graphics.Blit(source, destination);
}

image

图14 后处理pass

2.2 Fog Shader

2.1只是做了简单的拷贝,没什么用。必须要新建一个处理sourceTexture的shader来渲染雾。基本框架:

Shader "Custom/MyDeferredFog"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // No culling or depth
        Cull Off
        ZWrite Off
        ZTest Always
        Pass
        {
        }
    }
}

然后用后处理脚本需要引用该Shader

Pass
{
    CGPROGRAM
    #pragma vertex   VertexProgram
    #pragma fragment FragmentProgram
    #pragma multi_compile_fog
    #include "UnityCG.cginc"
    sampler2D _MainTex;
    struct modelData
    {
        float4 vertex : POSITION;
        float2 uv      : TEXCOORD0;
    };
    struct Interpolarters
    {
        float4 position : SV_POSITION;
        float2 uv        : TEXCOORD0;
    };
    Interpolarters VertexProgram(modelData m) {
        Interpolarters i;
        i.position = UnityObjectToClipPos(m.vertex);
        i.uv = m.uv;
        return i;
    }
    float4 FragmentProgram(Interpolarters i) :SV_Target
    {
        float3 sourceColor = tex2D(_MainTex, i.uv).rgb;
        return float4(sourceColor, 1);
    }
    ENDCG
}


2.3 Depth-Based Fog

增加深度雾。Unity自带深度buffer变量_CameraDepthTexture, 然后使用指令SAMPLE_DEPTH_TEXTURE采样深度:

UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

float4 FragmentProgram(Interpolarters i) :SV_Target
{
    float depth = SAMPLE_DEPTHE_TEXTURE(_CameraDepthTexture, i.uv);
    float3 sourceColor  = tex2D(_MainTex, i.uv).rgb;
    return float4(sourceColor, 1);
}

!首先。可以使用在UnityCG中定义的Linear01Depth函数将其转换为一个线性范围。这是因为从深度缓冲区得到原始数据后,需要从齐次坐标转换为[0,1]范围的clip-space坐标。我们必须转换这个值,使它成为世界空间中的一个线性深度值。

float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
depth = Linear01Depth(depth);

Linear01Depth内部实现:

// Z buffer to linear 0..1 depth
inline float Linear01Depth( float z )
{
    return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}

// Values used to linearize the Z buffer
// (http://www.humus.name/temp/Linearize%20depth.txt)
// x = 1-far/near
// y = far/near
// z = x/far
// w = y/far
float4 _ZBufferParams;
View Code

!然后。需要使用far_clip平面距离缩放该depth值,得到真实的深度视距。clip_space裁剪空间可通过float4 _ProjectionParams变量获得, 定义在UnityShaderVariables.cginc中。其中Z分量就是远平面far_clip距离。。

depth = Linear01Depth(depth);
float distance = depth * _ProjectionParams.z;

!最后,计算实际的fog。

float viewDistance = depth * _ProjectionParams.z;
UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
unityFogFactor = saturate(unityFogFactor);
float3 sourceColor  = tex2D(_MainTex, i.uv).rgb;
float3 color = lerp(unity_FogColor.rgb, sourceColor, unityFogFactor);
return float4(color, 1);

image

图15 不太明显的雾

2.4 Fixing the Fog

对比图15,就像把雾蒙在了物体上方。解决办法就是在绘制物体之前,绘制雾。使用ImageEffectOpaque属性绘制

[ImageEffectOpaque]
void OnRenderImage (RenderTexture source, RenderTexture destination) {
    Graphics.Blit(source, destination, fogMate);
}

image

image

处理近平面(非精确处理),near plane存储在Y值中

float viewDistance = depth * _ProjectionParams.z -_ProjectionParams.y;

2.5 Distance-Based Fog

deferred灯光的着色,从depth-buffer中重建世界空间位置,以便计算灯光。我们也可以这样仿照这样计算雾。

透视相机的clip-space空间定义了一个梯形区域,如果忽略near-plane就得到的是一个以相机world-pos为顶点的三角形区域。它的高是far-plaen距离,那么线性化后的depth范围:顶点为0,底边为1。

image

image

图16 金字塔区域

对于渲染的后处理图形Image的每个像素,都能通过从顶点到底边发射一条射线(从屏幕射向3D空间),检测是否击中任何物体,击中渲染,未击中不渲染。

image

图17 Image每个像素发射一条射线

若击中某个物体,那么对应像素的深度就要小于1. 如果,该射线在半道击中了物体,射线对应的像素深度值就是1/2.这就意味着射线对应的Z值 = 射线击中物体时的长度 ➗ 射线总长度。范围[0, 1]。又由于射线的方向都是一致的,X和Y坐标也应该减半。

image

图18 射线的缩放

一旦得到该射线,就能从相机的位置出发,寻找可能会被渲染的物体表面的世界坐标(若击中)。同时,也要得到该射线的长度。

要使用上述方法,必须知道从相机到平面的每一个像素的射线。但实际上,只需要4条射线,金字塔的每个角都需要一条射线。用插值可给出中间所有像素的光线。

2.6 Calculating Rays

基于相机远平面和视角计算光线,同时相机的方向和位置与距离无关,所以可以忽略变换。Camera提供了一个函数:CalculateFrustumCorners,四个参数
    矩形面积(image rect)
    光线投射距离(相机far-plane)
    立体渲染(相机自带)
    4个元素的3D向量组

        deferredCamera.CalculateFrustumCorners
        (
            rectArea,
            deferredCamera.farClipPlane,
            deferredCamera.stereoActiveEye,
            corners
        );

下一步传递该数据至Shader,同时也得改变索引顺序。相机提供的是:左下、左上、右上、右下。shader需要:左下、右下、左上、右上

//corners vectex index: b-l, u-l, u-r, b-r
//shader vectex index : b-l, b-r, u-l, u-r
frustumCorners[0] = corners[0];
frustumCorners[1] = corners[3];
frustumCorners[2] = corners[1];
frustumCorners[3] = corners[2];

fogMate.SetVectorArray("__FustumCorners", frustumCorners);

2.7 Deriving Distances

Shader需要一个接收变量,同时定义一个FOG_DISTANCE宏,当需要使用距离时再计算光线。

#define FOG_DISTANCE

struct Interpolators
{
    float4 position : SV_POSITION;
    float2 uv       : TEXCOORD0;
#if FOG_DISTANCE
    float3 ray : TEXCOORD1;
#endif
};

根据UV坐标计算获取数组中对应的光线,传进shader的数组排列:(0,0) (1,0) (0,1) (1,1),使用U+2V可得

#if FOG_DISTANCE
    i.ray = _FustumCorners[i.uv.x + 2 * i.uv.y];
#endif

最后在Fragment函数替换基于深度计算的雾,使用基于距离计算

float viewDistance = 0;
#if defined(FOG_DISTANCE)
    viewDistance = length(i.ray * depth);
#else
    viewDistance = depth * _ProjectionParams.z - _ProjectionParams.y;
#endif

image

图19 基于深度的雾 standard vs deferrd

image

图20 基于距离的雾 standard vs deferrd

2.8 Fogged Skybox

解放天空盒。两个不同渲染路径渲染的雾会有显著差异。延迟雾也会影响天空盒。它的作用就像far-plane是一个固体屏障,受到雾的影响。当深度值接近1时,表明已经到达了远平面。如果不想给天空盒蒙上雾,可以通过将雾因子设置为1来防止。

if (depth > 0.999)
{
    unityFogFactor = 1;
}

image

2.9 No Fog

最后考虑如何停止渲染雾。解决方案是当没有设置任何雾关键字,通过设置雾因子为1即可。

#if !defined(FOG_LINEAR) || !defined(FOG_EXP) || !defined(FOG_EXP2)
    unityFogFactor = 1;
#endif

3原文

原文地址:https://www.cnblogs.com/baolong-chen/p/12902985.html