天空和大气的渲染
单次散射积分实现
实现代码 Alpha3D v0.1.6
实时大气单次散射 ray-marching 实现,包含 rayleigh 散射、mie 散射与臭氧吸收
参考论文 Nishita 1993
Nishita et al. Display of the Earth Taking into Account Atmospheric Scattering. SIGGRAPH, 1993
vec3 CalculateScattering(vec3 rayOrigin, vec3 rayDirection, float maxDistance,
vec3 background, vec3 lightDirection) {
...
for (int i = 0; i < PRIMARY_STEPS; ++i) {
vec3 samplePosition = rayOrigin + rayDirection * rayPosition;
float height = max(length(samplePosition) - planetRadius, 0.0);
vec3 localDensity = AtmosphereDensity(height) * stepSize;
opticalDepth += localDensity;
vec2 lightHit = RaySphereIntersect(samplePosition, lightDirection, atmosphereRadius);
float lightStepSize = max(lightHit.y, 0.0) / float(LIGHT_STEPS);
float lightPosition = lightStepSize * 0.5;
vec3 lightOpticalDepth = vec3(0.0);
for (int j = 0; j < LIGHT_STEPS; ++j) {
vec3 lightSample = samplePosition + lightDirection * lightPosition;
float lightHeight = max(length(lightSample) - planetRadius, 0.0);
lightOpticalDepth += AtmosphereDensity(lightHeight) * lightStepSize;
lightPosition += lightStepSize;
}
vec3 combinedDepth = opticalDepth + lightOpticalDepth;
vec3 attenuation = exp(-rayleighBeta * combinedDepth.x
- mieBeta * combinedDepth.y
- absorptionBeta * combinedDepth.z);
totalRayleigh += localDensity.x * attenuation;
totalMie += localDensity.y * attenuation;
rayPosition += stepSize;
}
vec3 transmittance = exp(-rayleighBeta * opticalDepth.x
- mieBeta * opticalDepth.y
- absorptionBeta * opticalDepth.z);
vec3 scatteredLight = (rayleighPhase * rayleighBeta * totalRayleigh
+ miePhase * mieBeta * totalMie) * lightIntensity;
return scatteredLight + background * transmittance;
}rayleigh 散射
rayleigh 散射由空气中小分子实现,蓝光比红光更容易被散射。
白天,天空蓝光被更多地散射到观察者方向,使天空呈蓝色:
日出和日落时,太阳光经过更长的大气路径,蓝光被大量散射出去,直射太阳光偏橙红色:
mie 散射
米氏散射通常具有明显的前向散射特征,朝太阳方向观察时会看到更强的亮度和光晕:
臭氧吸收
臭氧层对绿光吸收最强,对红光次之,对蓝光吸收最弱。
日出、日落时,光在大气中的传播路径变长,臭氧使部分区域呈现更明显的蓝紫色或粉紫色过渡:
从太空观察时,大气分子产生的 rayleigh 散射使行星边缘呈现蓝色光环,臭氧吸收进一步削弱部分红光和绿光:
查找表加速实现
实现代码 Alpha3D v0.1.9
实时大气散射快速查找实现,报刊 Sky-View LUT 和 Transmittance LUT 两张查找表
参考论文 Hillaire 2020
Hillaire et al. A Scalable and Production Ready Sky and Atmosphere Rendering Technique, 2020
使用查找表加速结果如下(单位 毫秒),其中 primary steps 和 light steps 均设置为 64:
- Bar1:由双重积分暴力实现;
- Bar2:在 Bar1 的基础上加入 64 light-steps Transmittance LUT;
- Bar3:在 Bar2 的基础上加上了 Sky-View LUT。
Transmittance LUT
采样 LUT 的主要流程如下:
vec3 integrateAtmosphereScattering(vec3 rayOrigin, vec3 rayDirection, float maxDistance, vec3 background, vec3 lightDirection) {
...
for (int i = 0; i < viewSampleCount; ++i) {
vec3 samplePosition = rayOrigin + rayDirection * rayPosition;
float height = max(length(samplePosition) - planetRadius, 0.0);
vec3 localDensity = AtmosphereDensity(height) * stepSize;
opticalDepth += localDensity;
vec2 lightHit = RaySphereIntersect(samplePosition, lightDirection, atmosphereRadius);
vec3 lightOpticalDepth = vec3(0.0);
vec3 attenuation = vec3(0.0);
vec3 viewTransmittance = exp(-rayleighBeta * opticalDepth.x - mieBeta * opticalDepth.y - absorptionBeta * opticalDepth.z);
if (useTransmittanceLUT) {
attenuation = viewTransmittance * SampleTransmittanceLUT(samplePosition, lightDirection);
}
else {
attenuation = viewTransmittance * computeTransmittance(samplePosition, lightDirection, lightSampleCount);
}
totalRayleigh += localDensity.x * attenuation;
totalMie += localDensity.y * attenuation;
rayPosition += stepSize;
}
}计算 LUT 的主要流程如下:
vec4 computeTransmittanceLUT() {
int sampleCount = max(transmittanceLUTSteps, 1);
float viewHeight;
float viewZenithCos;
uvToTransmittanceParams(clamp(screenUV, vec2(0.0), vec2(1.0)), viewHeight, viewZenithCos);
vec3 assumptionPosition = vec3(0, viewHeight, 0);
vec3 assumptionDirection = zenithCosToDirection(viewZenithCos, 0);
return vec4(computeTransmittance(assumptionPosition, assumptionDirection, sampleCount), 1.0);
}Transmittance LUT 记录光线积分部分,后续计算 Sky-View LUT 等直接查询,LUT如下:

处理行星遮挡
增加地面遮挡判断,保证日落时地球背对太阳的那一侧天空是黑的,否则会有奇怪的漏光。
for (int i = 0; i < viewSampleCount; ++i) {
float rayPosition = rayStart + (float(i) + 0.5) * stepSize;
vec3 samplePosition = rayOrigin + rayDirection * rayPosition;
float radius = length(samplePosition);
vec3 localDensity = AtmosphereDensity(max(radius - planetRadius, 0.0)) * stepSize;
opticalDepth += localDensity;
float zenithCos = dot(samplePosition / radius, lightDirection);
vec3 viewTransmittance = exp(-Extinction(opticalDepth));
vec2 groundHit = RaySphereIntersect(samplePosition, lightDirection, planetRadius);
vec3 lightTransmittance = vec3(0.0);
if (groundHit.y <= 0.0) {
lightTransmittance = useTransmittanceLUT
? SampleTransmittanceLUT(radius, zenithCos)
: transmittance(radius, zenithCos, lightSampleCount);
}
vec3 attenuation = viewTransmittance * lightTransmittance;
totalRayleigh += localDensity.x * attenuation;
totalMie += localDensity.y * attenuation;
}效果如下:
Sky-View LUT
采样 LUT 的主要流程如下:
vec3 integrateAtmosphereScattering(vec3 rayOrigin, vec3 rayDirection, float maxDistance, vec3 background, vec3 lightDirection) {
vec2 atmosphereHit = RaySphereIntersect(rayOrigin, rayDirection, atmosphereRadius);
if (atmosphereHit.x > atmosphereHit.y) return background;
float viewHeight = length(rayOrigin);
if (MODE == 0 && useSkyViewLUT && viewHeight < atmosphereRadius && maxDistance >= 1e20) {
vec3 upVector = normalize(rayOrigin);
float viewZenithCosAngle = dot(rayDirection, upVector);
vec3 sideVector = normalize(cross(upVector, rayDirection));
vec3 forwardVector = normalize(cross(sideVector, upVector));
vec2 lightOnPlane = vec2(dot(lightDirection, forwardVector), dot(lightDirection, sideVector));
lightOnPlane = normalize(lightOnPlane);
float lightViewCosAngle = lightOnPlane.x;
vec2 skyViewUV = skyViewLutParamsToUv(viewZenithCosAngle, lightViewCosAngle, viewHeight, false);
vec3 viewTransmittance = useTransmittanceLUT
? SampleTransmittanceLUT(viewHeight, viewZenithCosAngle)
: transmittance(viewHeight, viewZenithCosAngle, max(lightSteps, 1));
return texture(skyViewLUT, skyViewUV).rgb + background * viewTransmittance;
}
...
for (int i = 0; i < viewSampleCount; ++i) {
...
}
}
计算 LUT 的主要流程如下:
vec4 computeSkyViewLUT()
{
float viewHeight = clamp(
planetRadius + cameraHeight + cameraPos.y,
planetRadius + 1.0,
atmosphereRadius - 1.0);
float viewZenithCos;
float lightViewCos;
uvToSkyViewParams(viewZenithCos, lightViewCos, viewHeight, screenUV);
float sunZenithCos = clamp(normalize(lightDirection).y, -1.0, 1.0);
vec3 sunDirection = normalize(vec3(
sqrt(max(1.0 - sunZenithCos * sunZenithCos, 0.0)),
0.0,
sunZenithCos));
float viewZenithSin = sqrt(max(1.0 - viewZenithCos * viewZenithCos, 0.0));
vec3 viewDirection = normalize(vec3(
viewZenithSin * lightViewCos,
viewZenithSin * sqrt(max(1.0 - lightViewCos * lightViewCos, 0.0)),
viewZenithCos));
vec3 atmosphereCamera = vec3(0.0, 0.0, viewHeight);
vec2 planetHit = RaySphereIntersect(atmosphereCamera, viewDirection, planetRadius);
float sceneDepth = 1e20;
if (planetHit.y > 0.0)
sceneDepth = max(planetHit.x, 0.0);
vec3 radiance = integrateAtmosphereScattering(atmosphereCamera, viewDirection, sceneDepth, vec3(0.0), sunDirection);
return vec4(radiance, 1.0);
}横轴非线性编码视线与太阳的夹角,纵轴以地平线为中心编码视线天顶角,利用大气对称性只覆盖半球,LUT如下:
