实时体积云渲染的光照细节

去年十月份我打算在引擎里加入体积云,折腾两个月后弄了一个大概的效果,基本看得过去,匆忙录了一个视频:

https://www.bilibili.com/video/BV18e4y1L7bv

Image

但把体积云作为天空的默认启动效果,放在场景中,360°频繁观看时,总会觉得辣眼睛。我又花了好多时间来细调这个效果,大概是3个月的闲暇时间吧 :D,快魔怔了之后终于得到了一个还算自然的效果(再也不改动体积云相关的Shader了)。

体积云渲染使用Volumetric Raymarching技术,渲染前我们确定星球的半径,体积云层的开始高度和终止高度,使用一些简单的数学运算,很容易就能得到RayMarching的起始点和终止点 a 和 b :

Image

大家都知道光线在经过水/烟/雾等体积时会被吸收掉一部分能量,因此,在a到b的路上,我们也要知道天空的光线有多少被吸收掉了?

Image

使用transmittance来表示天空被云遮挡的程度(透射率),那么体积云的渲染公式如下:

vec3 finalColor = transmittance * skyBakgroundColor; // skyBakgroundColor为天空的颜色

然后云本身也会被照亮,用scattering表示被照亮后的亮度,那么光线 b-> a的过程中,也要加入这个scattering, 此时为:

vec3 finalColor = transmittance * skyBakgroundColor + scattering; 

我们仅需计算transmittance和scattering就可以实现体积云的渲染了,XD。

transmittance可以使用Beer定律来计算得到,由于体积云位于大气中,可以使用大气散射的简化Beer公式。

// density为云的密度, a/b为大气层两个交点,距离单位为米
float transmittance = exp(-cloudDensity * distance(a, b)); 

从上面的示意图就能看出来,b->a之间的云是随机散布的,这里的cloudDensity很难用一个函数表示,所以我们要在b->a之间切段离散化来求解(Ray marching),

Image

此时,transmittance的计算伪代码如下:

float transmittance = 1.0f;
vec3 scattering = vec3(0.0f);

vec3 stepPosition = a;
while(a->b)
{
    float stepCloudDensity = sampleCloud(stepPosition); // 采样体积云的密度
    float stepTransmittance = exp(-stepCloudDensity * dt);
    
    stepPosition += dt * dir;
    transmittance *= stepTransmittance;
}

vec3 finalColor = transmittance * skyBakgroundColor + scattering; 

此时忽略云的scattering, 直接输出会得到如下结果:

Image

接下来计算云的scattering,刚刚求解transmittance时,已经切段RayMarching了,我们只需在Marching途中,计算一下步进点的Scattering并累加起来即可。

Image

float transmittance = 1.0f;
vec3 scattering = vec3(0.0f);

vec3 stepPosition = a;
while(a->b)
{
    float stepCloudDensity = sampleCloud(stepPosition); // 采样体积云的密度
    float stepTransmittance = exp(-stepCloudDensity * dt);
    
    vec3 stepScattering = ???
    
    vec3 sigmaS = vec3(stepCloudDensity);

    // b->P transmittance` in step.
    // NOTE: need * (sigmaS * dt) optical depth.
    scattering += stepScattering * transmittance * (sigmaS * dt);
    
    stepPosition += dt * dir;
    transmittance *= stepTransmittance;
}

// transmittance b->a

vec3 finalColor = transmittance * skyBakgroundColor + scattering; 

寒霜引擎在2017年的slices中给出了更好看的scattering积分公式,如下:

vec3 sigmaS = vec3(stepCloudDensity);
const float sigmaA = 0.0;
vec3 sigmaE = max(vec3(1e-8f), sigmaA + sigmaS);

vec3 sactterLitStep = stepScattering * sigmaS;
sactterLitStep = transmittance * (sactterLitStep - sactterLitStep * stepTransmittance);
sactterLitStep /= sigmaE;
scattering += sactterLitStep; 

积分公式有了之后, 只需搞定步进点的光照即可。

体积云位于天空中,会受到太阳的直接光照,很直接的,光照公式可表示为:

vec3 stepScattering = sunColor * sunAtmosphereTransmittance * sunPhase * sunVisibility;

sun atmosphere transmittance为天空大气模型中计算得到的步进点太阳透射率。

sun phase一般使用两次hg公式拟合,可以得到体积云的"银边效果":

// See http://www.pbr-book.org/3ed-2018/Volume_Scattering/Phase_Functions.html
float hgPhase(float g, float cosTheta)
{
	float numer = 1.0f - g * g;
	float denom = 1.0f + g * g + 2.0f * g * cosTheta;
	return numer / (4.0f * kPI * denom * sqrt(denom));
}

float dualLobPhase(float g0, float g1, float w, float cosTheta)
{
	return mix(hgPhase(g0, cosTheta), hgPhase(g1, cosTheta), w);
}

 sunPhase = dualLobPhase(0.5, -0.5, 0.2, -VoL);

Sun Visibility项则需要从步进点开始,向太阳方向做RayMarching,计算透射率作为SunVisibility:

vec3 sunVisStep = stepPosition;

float sunVisibility = 1.0f;
for(uint j = 0; j < kStepLight; j++)
{
	float stepLitDensity = sampleDensity(sunVisStep);
	sunVisibility *= exp(-stepLitDensity * dStepLight);
	sunVisStep += dStepLight * sunDirection;
}

完成这个之后,我们的云效果如下:

Image

我们需要为体积云增加大气透视效果。

在前面Marching时,我们同时增加一个Transmittance加权的坐标,便于得到云实体在天空中的位置:

float transmittance = 1.0f;

vec3 rayHitPos = vec3(0.0);
float rayHitPosWeight = 0.0;

vec3 stepPosition = a;
while(a->b)
{
    float stepCloudDensity = sampleCloud(stepPosition); // 采样体积云的密度
    float stepTransmittance = exp(-stepCloudDensity * dt);
    
    rayHitPos += samplePos * transmittance;
    rayHitPosWeight += transmittance;
    
    stepPosition += dt * dir;
    transmittance *= stepTransmittance;
}

rayHitPos /= rayHitPosWeight;

接下来,用这个位置来计算大气透视并叠加即可:

Image

体积云背光现在一片黑色,这是因为我们仅计算了太阳的直接光照。这里我们可以用天空球计算一个球谐光,作为着色时的环境光:

Image

此时如果我们调高云的密度,会发现背光处没有任何细节:

Image

此时,可以增加一个额外向上方向的Ambient Trace来增加背光部细节:

Image

寒霜引擎在17年的slice中还提到了多重散射近似,基本就是额外再计算一次折半衰减的Scattering Light,感觉不是特别的符合物理规律。

使用多重散射近似时,对于没有Visibility Trace的Ambient Light, 寒霜仅会在第一次散射时叠加,多次叠加将会抹平体积云的细节。

地平线提出过一个Powder Effect,可以为体积云增加自定义着色细节的功能,我修改调整后如下:

float powderEffectNew(float depth, float height, float VoL)
{
    float r = VoL * 0.5 + 0.5;
    r = r * r;
    height = height * (1.0 - r) + r;
    return depth * height;
}

float powderEffect;
{
    float depthProbability = pow(
        clamp(stepCloudDensity * 10.0, 0.0, 1.0), 
        remap(normalizeHeight, 0.3, 0.85, 0.5, 2.0));
    depthProbability += 0.05;
    
    float verticalProbability = pow(remap(normalizeHeight, 0.07, 0.22, 0.1, 1.0), 0.8);
    powderEffect =  powderEffectNew(depthProbability, verticalProbability, VoL);
}

我们可以用这个强调银边效果,得到一个火烧云的图:

Image

如果把VoL输入修改成-abs(VoL),则可以得到卡通风格的体积云:

Image

Image

直接在云海中也是效果不错的:

Image

把密度调低后我们可以得到一个软绵绵的体积云https://www.bilibili.com/video/BV1HA411C7DG:

Image

全部的代码开源:

https://github.com/qiutang98/flower

© - 2024 · 月光下的旅行。 禁止转载