最近在写一个大型一点的Demo,就相当于毕业设计了吧,目前正在把以前学的东西逐个集成,昨天终于完成了大气渲染的这一部分,虽然以前也做个一个的,但是那只是一个实验性质的,不过还是相当有效,这次很快就完成了,我想做个比较完善的天空,加入云,云也应该加入光照计算,夜晚星空这些我也正在考虑,我把目前大气渲染的一些细节写出来,方便大家以后如果要使用的话可以用作参考。
先贴张效果图。
从视觉的角度来说应该是正确的,不过右图的太阳之中感觉太明亮了,这可能和Mie散射的参数设置有关,具体的参数设置,大家可以自己尝试。
技术细节:
光学深度(Optical Depth): 描述了光强度通过某一种介质的衰减程度。
描述方程:t(Pa, Pb, waveLength) = 4*PI*K(waveLength)[exp( -h/H0 )]在Pa到Pb间的积分(ps:公式不好输入,具体可以参考GPU Gems2或相关论文)
方程解释:h 是一个相对高度, H0表示大气层中平均密度所在的考度(因为大气层越往上越稀薄)大气层中最高位置被压缩至1.一般我们大气层的平均密度所在高度为0.25, 也就是大气层厚度X0.25所在的高度。Pa, Pb表示大气层中任意两点,当然要是被光照的一面。
其实呢,我们可以讲这个光学深度值预计算出来,想想地球,内层是地壳,外层是大气层,我们以高度h代表一张查找表的U值,以弧度theta代表V值(球体是圆的,我们也可以假设是太阳围绕地球旋转),具体如何计算大家画画图用初中的几何知识就可以得出结果了,这里要注意,我们的h的增量不是线性增长的,和星球半径和大气层厚度有关。(积分用数值逼近法计算,因为是预计算的,因此可以比较精确,一般采用100次分割就不错了)。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
/* *********************************
原理:采用数值逼近法求的光学深度的积分值
参数值: u 高度,压缩量(0,1)区间
v cos角度, 通过0.5*(1-cos(theta)) 压缩至(0,1)
********************************* */
float sARadius = R / (R - r); // Scale Atmosphere Radius
float sPRadius = r / (R - r); // Scale Planet Radius
for ( int u = 0 ; u < uSize; u ++ )
{
for ( int v = 0 ; v < vSize; v ++ )
{
p[u][v].a = 0 ;
// 高度
float h = 1.0f / (uSize - 1 ) * u;
// 方向角度(弧度制)
float theta = 3.1415926535 / (vSize - 1 ) * v;
// 可以简化
float A = sPRadius + h;
float L = - A * cos(theta) + sqrt(sARadius * sARadius - A * A * sin(theta) * sin(theta));
float ds = L / samples;
for ( int i = 0 ; i < samples; i ++ )
{
float L = ds * (i + 0.5 );
float hi = sqrt(A * A + L * L + 2 * A * L * cos(theta)) - sPRadius;
if (hi < 0 )
{
break ;
}
p[u][v].a += exp( - hi / havg);
}
p[u][v].a *= ds;
// Rayleigh散射
p[u][v].a *= 0.012 ;
p[u][v].r = p[u][v].a * 4 * 3.1415926535 ; // * Kr;
p[u][v].g = p[u][v].a * 4 * 3.1415926535 ; // * Kg;
p[u][v].b = p[u][v].a * 4 * 3.1415926535 ; // * Kb;
// Mie散射
p[u][v].a *= 4 * 3.1415926535 ;
}
}
}
渲染图:
大小是512x512的,这种图可以直接使用。
分析一下,相同高度下角度越大我们的像素越亮,基本到中央90度的位置是最亮的,随着高度的增加,我们的最亮的地方也逐渐向下移动,因为光学深度和光线在大气层中通过的距离有关,角度越大距离则越大(因为我们不是在球体的中心,而是非常靠近顶层,我们只是在球体表面,其实可以使用数学软件计算公式并画出曲线图,这对程序问题的发现有很重大的意义,也可以便于分析计算结果(可惜,以前貌似大一学过Mathmatics,不过全部忘完了)。
RayLeigh散射和Mie散射:RayLeigh散射导致天空颜色的改变,Mie散射导致天空有时看上去朦朦胧胧的,RayLeigh散射和光线波长有关,和光线波长的四次方成反比,这也是出现大气颜色的一个关键,记住了,光学深度出来后不要忘记乘以1/pow(waveLength, 4);
第二个方程就是外向散射方程(GPU Gems2中文版如是说)这个方程的推导过程可以看NishiTa的论文,有个假定条件:光线是平行的!!!这样就可以大大简化方程的复杂度。推导我不写了(在电脑上输入公式就是一种杯具...)具体在《Display of the Earth Taking into Account Atmosphereric Scattering》上。
Shader代码
float3 eyePos;
#define PI 3.1415926535858
float Krr = 1.0f / pow( 0.625 , 4 );
float Krg = 1.0f / pow( 0.525 , 4 );
float Krb = 1.0f / pow( 0.470 , 4 );
float Km = 1.0f ;
float Luminance;
float OpticalScale;
float RayLeighDensity;
float MieDensity;
float3 lightDir = float3( 0 , 0 , 1 );
float scale = 0.005 ;
float Viewheight = 0.10 ;
float Exposure = 1.5 ;
#define SAMPLES 5
sampler s0 : register(s0);
float PhaseFunction( float g, float costheta)
{
return 1.5 * ( 1 - g * g) / ( 2 + g * g) * ( 1 + costheta * costheta) / pow( 1 + g * g - 2 * g * costheta, 1.5 );
}
void vs_main(float4 inPos : POSITION,
out float4 outPos : POSITION,
out float4 outColor : TEXCOORD0,
out float3 eyeVec : TEXCOORD1)
{
float4 Pos = float4(inPos.xyz + eyePos, 1.0f );
outPos = mul(Pos, matWorldViewProj);
float3 rayVec = inPos / SAMPLES;
float rayVecLength = length(rayVec);
float3 StartPos = eyePos;
float cosAngle = dot(normalize(rayVec), normalize(float3(inPos.x, 0 , inPos.z)));
float sinAngle = sqrt( 1 - sqrt(cosAngle * cosAngle));
float4 OutScatter = 0 ;
float radEye = acos(dot(normalize(rayVec), float3( 0 , 1 , 0 ))) / PI;
float radSun = acos(dot(normalize(lightDir), float3( 0 , 1 , 0 ))) / PI;
for ( int i = 0 ; i < SAMPLES; i ++ )
{
float3 currentPos = StartPos + (i + 0.5 ) * rayVec;
float currentPosHeight = (currentPos.y - eyePos.y) * scale; // (i+0.5)*rayVecLength*(sinAngle)*scale + Viewheight;
float4 opticalDepthCP = tex2Dlod(s0, float4( currentPosHeight, radEye, 0 , 0 ) );
float4 opticalDepthEP = tex2Dlod(s0, float4( Viewheight, radEye, 0 , 0 ) );
float4 opticalDepthCS = tex2Dlod(s0, float4( currentPosHeight, radSun, 0 , 0 ) );
float4 opticalDepthCE = opticalDepthEP - opticalDepthCP;
float4 Attenuation = exp( - (opticalDepthCE + opticalDepthCS) * OpticalScale * float4(Krr, Krg, Krb, 1.0f ));
OutScatter += Attenuation * exp( - 4 * currentPosHeight) ;
}
OutScatter *= rayVecLength;
outColor = OutScatter * Luminance * scale;
eyeVec = normalize(rayVec);
}
void ps_main(float4 inColor : TEXCOORD0,
float3 eyeVec : TEXCOORD1,
out float4 outColor : COLOR)
{
float costheta = dot(normalize(eyeVec), - normalize(lightDir));
float Fr = 0.75 * ( 1 + costheta * costheta);
float Fm = PhaseFunction( - 0.9922f , costheta);
float4 RColor = inColor * float4(Krr, Krg, Krb, 1 );
float4 MColor = inColor.a * Km;
float4 color = MColor * Fm * MieDensity + RColor * RayLeighDensity ;
outColor = color;
}
Technique T0
{
pass p0
{
VertexShader = compile vs_3_0 vs_main();
PixelShader = compile ps_3_0 ps_main();
}
}
这里用了顶点纹理,以前没用过,还不知道顶点纹理只能用tex2Dlod()。注意参数,Mie散射的phase Function的g值影响到了太阳光斑的大小(没说耀斑),也就是日晕的大小,我设置成-0.9922f的样子感觉还不错,最麻烦的其实还是如何调节参数,我整整调了两天才感觉比较合理,当然,还加入了HDR效果,不过是个简单的ToneMap。加了之后感觉颜色更加协调,如果只是一个简单的Explosure的话你会发现日落时黄色和红色不太分明。
天空模型的问题:不是严格意义上的半球,而只是一个半径是8000的球的最顶端的0.025的那一部分,就像碟状的那种,之所以选这种是因为我感觉更加合理,不过我想就算是个半球也没有什么问题。
我把天空封装成一个完整的类就把代码发上来,希望这篇文章对大家有所帮助。