大气渲染全攻略
最近在写一个大型一点的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次分割就不错了)。
{
/**********************************
原理:采用数值逼近法求的光学深度的积分值
参数值: 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的那一部分,就像碟状的那种,之所以选这种是因为我感觉更加合理,不过我想就算是个半球也没有什么问题。
我把天空封装成一个完整的类就把代码发上来,希望这篇文章对大家有所帮助。