使用 Preetham 天空模型与硬边太阳圆盘实现真实感天空渲染
使用 Preetham 天空模型与硬边太阳圆盘实现真实感天空渲染
在计算机图形学与实时渲染中,想要在天空中重现“中心高亮、边缘暗淡”的太阳盘面,同时呈现真实的天空散射效果,需要结合大气光学理论与贴近物理的数值模型。本篇技术博客将详细介绍如何基于 Preetham 天空亮度分布模型(Preetham Sky Model)以及硬边(Hard-Edge)太阳圆盘方法,在 Cg/HLSL/GLSL 片元着色器中直接展开计算,实现真实感极强的天空渲染效果。
背景与目标
在许多游戏、仿真和虚拟现实场景中,渲染一个逼真的天空是非常重要的。除了模拟天气变化、云层动效外,最直观的要素莫过于太阳与天空的相互映衬。真实的天空应当满足以下两点:
-
天空散射
-
太阳光在大气分子(瑞利散射)和气溶胶(Mie 散射)中被散射后,产生蓝天、晨昏渐变等现象。
-
亮度分布并非线性,需要符合大气辐射传输方程与观测结果。
-
-
太阳小圆盘高亮
-
太阳在天空中视直径极小,约 0.53°。
-
太阳盘面中心比边缘饱和、明亮许多,接近“硬边”效果,且边缘仅有极窄的过渡。
-
本博客要解决的问题是:如何在实时渲染中,将“真实感天空背景(Preetham 模型)”与“硬边小太阳圆盘”结合,得到高质量的天空渲染?
大气散射与天空亮度模型概述
瑞利散射与 Mie 散射
-
瑞利散射(Rayleigh Scattering)
-
由空气分子(直径远小于光波长)引起,满足散射强度与波长的 λ⁻⁴ 关系。
-
负责天空大部分的蓝色成分,从而导致正午蓝天;随着太阳高度变化,散射路径加长,形成红色日出日落。
-
-
Mie 散射(Mie Scattering)
-
由气溶胶、尘埃、水滴等颗粒(直径接近或大于光波长)引起,方向性更强,常表现为“向前散射”(正向)。
-
造成雾霾、泛白,日出日落时天空边缘出现泛红或泛橙的效应。
-
为了高效地在 Shader 中模拟以上散射效果,可使用离线数值模拟结果或观测结果拟合得到的解析模型。Preetham 等人在 1999 年基于 Nishita 方法的数值模拟,提出了一种简洁而准确的参数化天空亮度分布模型——Perez 天空公式,并给出了不同太阳高度下的系数拟合。
Preetham 天空模型原理
Preetham 模型(也被称为 Perez Sky Model)通过以下步骤实现真实感天空亮度:
-
参数化大气浑浊度
-
以常用的“turbidity”(浑浊度)值表示空气中气溶胶含量,范围通常在 1.0 (极纯) 到 10.0 (极浑浊)。
-
浑浊度越高,Mie 散射越明显,天空整体趋于偏灰或偏红。
-
-
计算天顶亮度与天顶颜色(Zenith Luminance / Chromaticity)
-
根据太阳高度角(solarElevation)与浑浊度,给出 Yz(天顶亮度)以及天空在天顶处的色度 (xz, yz)。
-
-
Perez 分布函数
-
将任意视线方向(以视线与天顶的夹角 θ,以及视线与太阳方向的夹角 γ)映射到亮度:
-
其中 A–E 为根据浑浊度与太阳高度拟合出的系数; Iz 为天顶亮度。
-
-
将亮度 Y, x, y 转换为 RGB
-
先将 (x, y) 转换为 XYZ,再到线性 RGB,最后做色调映射(Tone Mapping)或伽马校正。
-
综上,通过预计算或实时 CPU 计算得到 A–E 系数与天顶亮度色度,在 GPU 片元着色器中使用 Perez 函数,就能得到任意方向的天空背景色。
太阳圆盘的物理参数与硬边渲染
太阳视直径与视半径计算
-
太阳半径 R⊙ ≈ 6.96×10⁸ m
-
地日平均距离 d ≈ 1.496×10¹¹ m
-
由几何关系:
-
因此,太阳圆盘视直径 ∼ 0.5334°,视半径 ∼ 0.2667°。
在渲染坐标系中,如果已知一个片元的视线方向 viewDir
(世界空间归一化向量)和太阳中心方向 sunDir
,那么两者的夹角 γ 满足:
要判断视线是否“射中”太阳圆盘:
其中 ψ ≈ 0.2667°,数值非常小,意味着只有与太阳非常接近的一小块区域会被判定为太阳本体。
硬边判断与边缘过渡
-
硬边(Hard Edge):
最简单粗暴的做法,直接用step(cosSunRadius, cosGamma)
来判断。如果cosGamma > cosSunRadius
,则认为该片元属于太阳圆盘内部;否则不属于。 -
边缘过渡(Soft Edge):
为了模拟大气折射、镜头眩光等造成的微弱边缘晕染,可加入一个很窄的过渡带——例如用smoothstep(cosSunRadius, cosSunRadius + ε, cosGamma)
,其中 ε 取 ~0.005 或更小,用于让太阳圆盘边缘呈现 1–0 的渐变。这样能避免突然的锐利断层,同时在 HDR 环境下还可以配合镜头光晕算法进一步叠加泛光(Glow)。
综上,通过判断夹角是否小于视半径,即可在天空上描绘出一个半径仅 0.2667° 的高亮圆盘;结合 smoothstep 或后续光晕函数,就能获得类似“人眼看到的灼热太阳”小圆斑。
完整 Cg 片元着色器示例
以下示例在 Cg/HLSL 片元着色器中直接展开了 Preetham 天空模型与太阳硬边圆盘叠加的计算流程。为了演示方便,部分常量可在应用层预先计算并传入(如 A–E、天顶亮度与色度)。本示例假设 API 级别已具备以下功能:
-
float3 normalize(float3)
、dot(float3, float3)
、pow(x,y)
等基本数学运算。 -
exp(x)
、cos(x)
、sin(x)
、acos(x)
、sqrt(x)
等。 -
radians(deg)
:将角度转为弧度。 -
smoothstep(edge0, edge1, x)
:Hermite 插值函数。
Uniforms 与输入
// ————————————————
// 应用传入的全局 Uniforms
// ————————————————
float3 uSunDirection; // 归一化 |sunDir| = 1,指向太阳中心
float uSunIntensity; // 太阳本体亮度强度(HDR);一般 ≫ 天空亮度,例如 10000–100000
float uTurbidity; // 大气浑浊度,范围[1, 10],1:极清澈;10:极浑浊// Perez 模型拟合系数(根据 uSunElevation 和 uTurbidity 在 CPU 端计算并传入)
float uA, uB, uC, uD, uE; // Perez 分布函数系数
float3 uZenithColor; // 天顶处线性色度(RGB)
float uZenithY; // 天顶处亮度(Y 亮度通道)// 大气散射系数(可选,若需更高精度则使用物理散射进行积分计算;此处留用作扩展)
// float3 uBetaRayleigh;
// float3 uBetaMie;
// float uG; // Mie 相函数参数// ————————————————
// 由顶点着色器传入的片段信息
// ————————————————
float3 vWorldDir; // 世界坐标下的片段方向(归一化),即 (x,y,z) 代表视向(假设在天空球顶点计算)
计算天空背景色(Preetham)
// 1) 计算视线与天顶的夹角 θ:
// 天顶方向假设为 (0,1,0),即世界坐标系中正 Y 轴为“直接正上方”。
float cosTheta = vWorldDir.y;
float theta = acos( clamp(cosTheta, -1.0, 1.0) ); // θ ∈ [0, π]// 2) 计算视线与太阳方向的夹角 γ:
float cosGamma = dot(vWorldDir, uSunDirection);
float gamma = acos( clamp(cosGamma, -1.0, 1.0) ); // γ ∈ [0, π]// 3) Perez 天空亮度函数 F(θ, γ):
// F(θ,γ) = (1 + A·exp(B / cosθ)) · (1 + C·exp(D·γ) + E·cos²γ)
// 注意分母需归一化,以保证 F(0, 90°) = 1。
float denom1 = 1.0 + uA * exp( uB );
float denom2 = 1.0 + uC * exp( uD * 0.0 ) + uE * pow(cos(0.0), 2);
float numerator = (1.0 + uA * exp( uB / max(0.01, cosTheta) )) * (1.0 + uC * exp( uD * gamma ) + uE * pow(cosGamma, 2));
float perezFactor = numerator / (denom1 * denom2);// 4) 计算该方向的亮度 Y_dn:
// Y(θ,γ) = Y_z * F(θ,γ)
float Y_dir = uZenithY * perezFactor;// 5) 计算该方向的色度 (x, y) —— 可选简化:
// Preetham 原论文中,将天顶色度扩展到任意方向需要额外拟合,
// 为了简化可将天顶色度在整个天空做线性混合:
// x_dir ≈ x_z, y_dir ≈ y_z。
// 然而,若需更精确,需根据太阳高度与曦晕颜色拟合系数计算。
// 此处示例直接使用 uZenithColor 作为统一的线性色。
float3 skyRGB = uZenithColor * (Y_dir / uZenithY);
// 由于 skyRGB 已包含 Y_dir,相当于:
// skyRGB = XYZ_from_xyY(x_z, y_z, Y_dir) → 转到线性 RGB// 6) 最终的背景天空色(线性空间)
float3 backgroundColor = skyRGB;
注释
uZenithColor
:一般通过天顶色度 (xz, yz) 与亮度 Yz 从 CIE xyY 转到线性 RGB;可在应用层完成。若需更高保真,可基于 Sun Zenith Angle & Turbidity 在 CPU 上查表或拟合,分别得到 (uA, uB, uC, uD, uE),以及 xz, yz, Yz。
计算并叠加太阳圆盘高亮
// 1) 太阳视半径 ψ ≈ 0.2667°(弧度)
const float sunAngularRadius = radians(0.2667);
const float cosSunRadius = cos(sunAngularRadius);// 2) 片元与太阳中心夹角余弦:cosγ(已在上文计算)
float cosSunGamma = cosGamma;// 3) 太阳圆盘硬边 + 微弱边缘过渡
// smoothstep(edge0, edge1, x) 从 0 → 1 的平滑函数
// 这里 edge0 = cosSunRadius,edge1 = cosSunRadius + ε。
// ε ~ 0.005 可微调;若要更窄,可减小 ε,例如 0.002。
const float edgeSoft = 0.005;
float sunMask = smoothstep(cosSunRadius, cosSunRadius + edgeSoft, cosSunGamma);
// 若不需要过渡,则: float sunMask = (cosSunGamma > cosSunRadius) ? 1.0 : 0.0;// 4) 太阳自发光强度(线性 RGB)
float3 sunColorHDR = float3(uSunIntensity) * sunMask;
// 同时可以考虑光谱分布:800–1700nm,在 NIR 波段太阳远比天空亮。
// 此处直接用单一系数代表太阳光整体亮度。// 5) 可选:叠加更宽的散射晕(Lens Flare/Halo)
// 依据 Mie 散射或镜头点扩散函数 (PSF) 进行额外处理,
// 此处示例忽略。如需,可在 cosSunGamma < cosSunRadius 时额外计算:
/*
if (cosSunGamma < cosSunRadius && cosSunGamma > cosSunRadius - haloWidth)
{float haloFactor = exp(-(γ - sunAngularRadius)^2 / (2σ^2));sunColorHDR += sunHaloIntensity * haloFactor;
}
*/float3 sunContribution = sunColorHDR;
输出最终颜色
// 1) 合成 天空背景 + 太阳圆盘
float3 finalColorLinear = backgroundColor + sunContribution;// 2) 伽马校正(Gamma = 2.2),输出到屏幕
float3 finalColorSRGB = pow(finalColorLinear, float3(1.0/2.2, 1.0/2.2, 1.0/2.2));// 3) 返回片元颜色
return float4(finalColorSRGB, 1.0);
完整片元着色器伪代码结构
float4 FragmentShaderSky(float2 uv : TEXCOORD0) : COLOR {// ---------- 读取 vWorldDir ----------float3 viewDir = normalize(vWorldDir);// ----- 步骤 1:计算天空背景色(Preetham) -----float cosTheta = viewDir.y;float theta = acos( clamp(cosTheta, -1.0, 1.0) );float cosGamma = dot(viewDir, uSunDirection);float gamma = acos( clamp(cosGamma, -1.0, 1.0) );float denom1 = 1.0 + uA * exp(uB);float denom2 = 1.0 + uC * exp(uD * 0.0) + uE * pow(cos(0.0), 2);float numerator = (1.0 + uA * exp(uB / max(0.01, cosTheta)))* (1.0 + uC * exp(uD * gamma) + uE * pow(cosGamma, 2));float perezF = numerator / (denom1 * denom2);float Y_dir = uZenithY * perezF;float3 skyRGB = uZenithColor * (Y_dir / uZenithY);float3 backgroundColor = skyRGB;// ----- 步骤 2:计算太阳圆盘高亮 -----const float sunAngularRadius = radians(0.2667);const float cosSunRadius = cos(sunAngularRadius);const float edgeSoft = 0.005;float sunMask = smoothstep(cosSunRadius, cosSunRadius + edgeSoft, cosGamma);float3 sunContribution = float3(uSunIntensity) * sunMask;// ----- 步骤 3:合成与伽马校正 -----float3 finalLinear = backgroundColor + sunContribution;float3 finalSRGB = pow(finalLinear, float3(1.0/2.2,1.0/2.2,1.0/2.2));return float4(finalSRGB, 1.0); }
参数调节与效果展示
为了在不同场景下获得最优效果,需要对以下参数进行调节:
参数 | 含义 | 推荐范围/说明 |
---|---|---|
uTurbidity | 大气浑浊度 | 1.0 (晴朗) ~ 10.0 (雾霾) |
uA, uB, uC, uD, uE | Perez 模型系数 | 根据 uTurbidity 与 太阳高度(由 uSunDirection.y 决定)在 CPU 端查表或拟合计算 |
uZenithColor | 天顶处线性 RGB | 同样由 uTurbidity 与太阳高度计算或查表得到 |
uZenithY | 天顶处亮度(Y 通道,线性) | 可参考 Preetham 论文或测量值 |
uSunIntensity | 太阳圆盘亮度强度(HDR) | 10000 ~ 50000 ,可根据场景总曝光量调整 |
edgeSoft | 太阳圆盘边缘过渡带宽度(cosγ 方向) | 0.002 ~ 0.01 ,值越小圆盘边缘越锐利 |
sunAngularRadius | 太阳视半径,固定 ≈ 0.2667° | 一般无需更改 ;若想模拟日食或月球遮挡可动态调整 |
效果对比示例
-
标准晴天(Turbidity = 1.0)
-
天空呈现深蓝到浅蓝渐变,近地平线位置偏浅;
-
太阳圆盘清晰锐利,边缘过渡带极窄。
-
-
轻度雾霾(Turbidity = 3.0)
-
天空整体亮度上升,偏淡白;
-
太阳周围出现轻微泛白晕染,同时背景融合更明显。
-
-
重度雾霾(Turbidity = 8.0)
-
天空整体偏灰白,色彩饱和度极低;
-
太阳圆盘较为模糊,边缘过渡带宽度可以适当增大(如 edgeSoft = 0.01)。
-
下图为示例渲染效果(仅示意,不代表真实截图):
┌────────────────────────────────────────────────────┐
│ 晴天 (T=1.0) 雾霾 (T=3.0) 雾霾 (T=8.0) │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ ☀️ │ │ ☀️ │ │ ☀️ │ │
│ │ │ │ │ │ │ │
│ │ 深蓝 → 浅蓝 │ │ 浅蓝 → 淡灰白 │ │ 灰白 → 淡灰白 │ │
│ │ 天空背景 │ │ 天空背景 │ │ 天空背景 │ │
│ │ │ │ │ │ │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└────────────────────────────────────────────────────┘
小结与拓展
本文示范了如何结合 Preetham 天空散射模型 与 硬边太阳圆盘 方法,在片元着色器里直接展开计算,并给出了完整的 Cg 伪代码示例。该方法具有如下优点:
-
科学依据充分:Preetham 模型基于大气辐射传输、观测拟合结果;太阳视直径由天文学几何关系给出。
-
实时性能友好:仅需计算若干指数、乘加、三角函数、一次
dot()
判断,无需进行昂贵的逐像素大气积分。 -
易于拓展:可在太阳圆盘外再叠加镜头光晕(Lens Flare)或光散射(Glare)效果,也可更换为更高级的 Hosek–Wilkie 模型。
若需进一步提升真实感,可考虑:
-
在 CPU 端精细拟合 Perez 系数 与 天顶色度、亮度,并结合实时太阳高度、季节、湿度等参数。
-
引入 大气光散射积分,通过多次采样计算雾化光照与散射,不依赖固定系数。
-
在后期用 光晕贴图(Corona Map) 或 Bloom 技术叠加镜头效果。