本Blog的体积雾散射算法借鉴自Miles Macklin Simulation and computer graphics,如需原文参照,可转至链接。
球形体积雾,即通过一个球体,配备一个雾效Shader,从而模拟出球状雾效。
主要包括:
#if UNITY_REVERSED_Z
positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#else
positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#endif
o.uv = ComputeScreenPos(o.vertex);
)float4 GetShadowCoord(VertexPositionInputs vertexInput)
{
#if defined(_MAIN_LIGHT_SHADOWS_SCREEN) && !defined(_SURFACE_TYPE_TRANSPARENT)
return ComputeScreenPos(vertexInput.positionCS);
#else
return TransformWorldToShadowCoord(vertexInput.positionWS);
#endif
}
// 通过深度计算得到直接坐标
real depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.texcoord);
float3 worldPos = ComputeWorldSpacePosition(i.texcoord, depth, UNITY_MATRIX_I_VP);
ComputeWorldSpacePosition定义在Common.hlsl
float3 ComputeWorldSpacePosition(float2 positionNDC, float deviceDepth, float4x4 invViewProjMatrix)
{
float4 positionCS = ComputeClipSpacePosition(positionNDC, deviceDepth);
float4 hpositionWS = mul(invViewProjMatrix, positionCS);
return hpositionWS.xyz / hpositionWS.w;
}
float3 ComputeWorldSpacePosition(float4 positionCS, float4x4 invViewProjMatrix)
{
float4 hpositionWS = mul(invViewProjMatrix, positionCS);
return hpositionWS.xyz / hpositionWS.w;
}
ComputeClipSpacePosition(float2 positionNDC, float deviceDepth)将NDC空间转移到Clip空间
float4 ComputeClipSpacePosition(float2 positionNDC, float deviceDepth)
{
float4 positionCS = float4(positionNDC * 2.0 - 1.0, deviceDepth, 1.0);
#if UNITY_UV_STARTS_AT_TOP
// Our world space, view space, screen space and NDC space are Y-up.
// Our clip space is flipped upside-down due to poor legacy Unity design.
// The flip is baked into the projection matrix, so we only have to flip
// manually when going from CS to NDC and back.
positionCS.y = -positionCS.y;
#endif
return positionCS;
}
// Use case examples:
// (position = positionCS) => (clipSpaceTransform = use default)
// (position = positionVS) => (clipSpaceTransform = UNITY_MATRIX_P)
// (position = positionWS) => (clipSpaceTransform = UNITY_MATRIX_VP)
float4 ComputeClipSpacePosition(float3 position, float4x4 clipSpaceTransform = k_identity4x4)
{
return mul(clipSpaceTransform, float4(position, 1.0));
}
之后,我们计算球形体积雾雾效因子。
由此,我们可以计算远处片元反射的光线,要穿过多厚的雾,才能到达我们的眼睛。因为雾效是局部的而不是全局的,因此我们需要计算穿过的雾效厚度L:
根据上图有:
d i r → = ( P − E ) . n o r m a l i z e d ∣ E X ∣ = d o t ( d i r → , E C → ) ∣ C X ∣ 2 = ∣ E C ∣ 2 − ∣ E X ∣ 2 \overrightarrow{dir} = (P-E).normalized\\ |EX| = dot(\overrightarrow{dir}, \overrightarrow{EC})\\ |CX|^2 = |EC|^2 - |EX|^2 dir=(P−E).normalized∣EX∣=dot(dir,EC)∣CX∣2=∣EC∣2−∣EX∣2
射线与圆的相交的判定条件为:
∣ C X ∣ 2 < R 2 ( 直线穿过球内 ) |CX|^2
当条件符合后,再进行雾效计算,有:
L 2 = R 2 − ∣ C X ∣ 2 \frac{L}{2} = \sqrt{R^2-|CX|^2} 2L=R2−∣CX∣2
当摄像机在球形范围外,即 ∣ E X ∣ > L 2 ∣ ∣ ∣ E X ∣ < − L 2 |EX|>\frac{L}{2} \quad||\quad |EX|<-\frac{L}{2} ∣EX∣>2L∣∣∣EX∣<−2L
当相机在球形范围内,雾球内距离为 L/2 + |EX|
综上:有代码如下
///
/// describe:
/// BallFogFactor的另一种算法
/// params:
/// depthPosWS: 深度图中记录深度点的世界坐标
/// fogBallCenterRadiusWS: 世界空间中球形体积雾的信息(位置+半径)
/// fogDensity: 雾效强度
/// fogMaxFactor: 最大雾效因子
/// falloffDist: 衰减系数 * 半径 * 0.5f
///
half CalculateBallFogFactor2(
float3 depthPosWS, float4 fogBallCenterRadiusWS,
float fogDensity, float fogMaxFactor, float falloffDist
)
{
// 数据提取
float R = fogBallCenterRadiusWS.w;//R
float3 C = fogBallCenterRadiusWS.xyz;//C
float3 E = GetCameraPositionWS();//E
// 数据计算
float3 cameraToDepthPoint = depthPosWS - E;
float depthT = length(cameraToDepthPoint);// 深度距离
float3 dir = normalize(cameraToDepthPoint);// dir
float3 EC = center - camPosWS;
float EX = dot(dir,EC);
float CX_2 = dot(EC,EC) - EX * EX;
//判定
float R_2 = R * R;
if(CX_2 >= R_2) return 0;//直线不穿过球内,没有雾效
float LDiv2 = sqrt(R_2 - CX_2);//sqrt{L}{2}
float L = 2 * LDiv2;
if(EX < - LDiv2)return 0;//射线不穿过球内,没有雾效
// 判定通过,计算雾效距离
float FogDistance;
if(EX > LDiv2) FogDistance = L; //光线经过整个雾球
else FogDistance = LDiv2 + EX; //摄像机在雾效范围内
// 如果雾是均匀雾,我们就可以返回雾效因子为
float FogFactor = clamp(0, fogMaxFactor, FogDistance * fogDensity);
return FogFactor;
}
运行后发现,我们忘记处理了一种情况,当深度点在雾效球内,或雾效之前,我们需要删掉被遮挡的雾效。
增加代码:// 如果最大深度小于雾效深度,需要减去被遮挡的雾距离。
///
/// describe:
/// BallFogFactor的另一种算法,该方法使用几何信息递推求解
/// 但这种方法暂时只支持均匀分布的雾效
/// 具体推导过程见Blog《光在雾效中的散射》
/// params:
/// depthPosWS: 深度图中记录深度点的世界坐标
/// fogBallCenterRadiusWS: 世界空间中球形体积雾的信息(位置+半径)
/// fogDensity: 雾效强度
/// fogMaxFactor: 最大雾效因子
/// falloffDist: 衰减系数 * 半径 * 0.5f
///
half CalculateBallFogFactor2(
float3 depthPosWS, float4 fogBallCenterRadiusWS,
float fogDensity, float fogMaxFactor, float falloffDist
)
{
// 设:
// float R: 球的半径
// float3 C:球的中心世界坐标
// float3 E:摄像机的世界坐标
// float3 X:光线所在的直线上距离球心最近的点
// float3 dir:光线的方向(摄像机到像素点的方向)
// 设定:
// XX_2:表示XX的平方,如果XX为向量,则表示距离的平方。
// 数据提取
float R = fogBallCenterRadiusWS.w;
float3 C = fogBallCenterRadiusWS.xyz;
float3 E = GetCameraPositionWS();
// 数据计算
float3 cameraToDepthPoint = depthPosWS - E;
float depthT = length(cameraToDepthPoint);// 深度距离
float3 dir = normalize(cameraToDepthPoint);// dir
float3 EC = C - E;
float EX = dot(dir,EC);
float CX_2 = dot(EC,EC) - EX * EX;
//判定
float R_2 = R * R;
if(CX_2 >= R_2) return 1;//直线不穿过球内,没有雾效
float LDiv2 = sqrt(R_2 - CX_2);//sqrt{L}{2}
float L = 2 * LDiv2;
if(EX < - LDiv2)return 1;//射线不穿过球内,没有雾效
// 判定通过,计算雾效距离
float RayDistance = EX + LDiv2;//光线从摄像机发出,到穿过雾时移动的距离
float FogDistance = 1;
if(EX > LDiv2) FogDistance = L; //光线经过整个雾球
else FogDistance = LDiv2 + EX; //摄像机在雾效范围内
// 如果最大深度小于雾效深度,需要减去被遮挡的雾距离。
float DivDistance = 0;
if(depthT < RayDistance){
DivDistance = RayDistance - depthT;
}
FogDistance -= DivDistance;
if(FogDistance<0)FogDistance = 0;
// 如果雾是均匀雾,我们就可以返回雾效因子为
half FogFactor = clamp(0,1-fogMaxFactor,FogDistance * fogDensity);
return 1-FogFactor;
}
然而,再增加条件:如果雾效并不是均匀分布的,那我们如何处理。
首先我们知道进入点距离球心为R,退出点也距离球心为R。
如果衰减函数为 y = − k x + 1 ( k > 0 ) y = -kx + 1(k>0) y=−kx+1(k>0);
球心边缘y为0,球心中心y为1,则进入点雾效距离x = R,中心点雾效距离为x = CX。
中间任意一点雾效距离为:
x = t 2 + C X 2 x = \sqrt{t^2 + CX^2} x=t2+CX2
故整体雾效强度为
2 ∫ t = 0 t = L 2 − k t 2 + C X 2 + 1 ( d t ) 2\int_{t=0}^{t=\frac{L}{2}} -k \sqrt{t^2 + CX^2} + 1 (dt) 2∫t=0t=2L−kt2+CX2+1(dt)
但是这样求解的是全部雾效的强度,但是片元有可能在雾效内,雾效前。所以不能全部积分。
根据t的取值,对积分区间求解,最终得到最后的结果。
那既然我们需要一个参数t得知光线在其中的位置,何不直接在计算时,得到光线在雾内传播的起始t值,和结束t值。
// 设:
// float r: 球的半径
// float3 C:球的中心世界坐标
// float3 E:摄像机的世界坐标
// float3 X:光线所在的直线上的点
// float3 dir:光线的方向(摄像机到像素点的方向)
设:光线函数为
X ( t ) = E + D i r ∗ t ( D i r 为单位向量 ) X(t) = E + Dir * t(Dir为单位向量) X(t)=E+Dir∗t(Dir为单位向量)
当光线和球面相交,公式为:
∣ X ( t ) − C ∣ 2 = r 2 |X(t) - C|^2 = r^2 ∣X(t)−C∣2=r2
代入公式,得:
∣ E + D i r ∗ t − C ∣ 2 = r 2 |E + Dir * t - C|^2 = r^2 ∣E+Dir∗t−C∣2=r2
注意:因为这里 r 为float,而 E、Dir、C 为向量,故不能将 r 放入平方内。
我们展开公式,并整理得:
∣ D i r ∣ 2 t 2 + 2 ( ∣ E − C ∣ ⋅ D i r ) ∗ t + ( ∣ E − C ∣ 2 − r 2 ) = 0 |Dir|^2t^2 + 2(|E-C| \cdot Dir) * t + (|E-C|^2-r^2) = 0 ∣Dir∣2t2+2(∣E−C∣⋅Dir)∗t+(∣E−C∣2−r2)=0
求解2次方程:公式我居然忘了!!!!
得到 t m i n t_{min} tmin, t m a x t_{max} tmax
继续计算得到场景中实际的t值。
0 < = t m i n < = d e p t h T 0<=t_{min}<=depthT 0<=tmin<=depthT
t m i n < = t m a x < = d e p t h T t_{min}<=t_{max}<=depthT tmin<=tmax<=depthT
最后得到传播距离为
f o g D i s t = t m a x − t m i n ; fogDist = t_{max} - t_{min}; fogDist=tmax−tmin;
同样我们需要考虑雾效衰减的问题。
雾效衰减该部分内容属于公司文件,这里就不再阐述。(怕收到律师函)