当前位置: 首页 > news >正文

使用 Preetham 天空模型与硬边太阳圆盘实现真实感天空渲染

使用 Preetham 天空模型与硬边太阳圆盘实现真实感天空渲染

在计算机图形学与实时渲染中,想要在天空中重现“中心高亮、边缘暗淡”的太阳盘面,同时呈现真实的天空散射效果,需要结合大气光学理论与贴近物理的数值模型。本篇技术博客将详细介绍如何基于 Preetham 天空亮度分布模型(Preetham Sky Model)以及硬边(Hard-Edge)太阳圆盘方法,在 Cg/HLSL/GLSL 片元着色器中直接展开计算,实现真实感极强的天空渲染效果。


背景与目标

在许多游戏、仿真和虚拟现实场景中,渲染一个逼真的天空是非常重要的。除了模拟天气变化、云层动效外,最直观的要素莫过于太阳与天空的相互映衬。真实的天空应当满足以下两点:

  1. 天空散射

    • 太阳光在大气分子(瑞利散射)和气溶胶(Mie 散射)中被散射后,产生蓝天、晨昏渐变等现象。

    • 亮度分布并非线性,需要符合大气辐射传输方程与观测结果。

  2. 太阳小圆盘高亮

    • 太阳在天空中视直径极小,约 0.53°。

    • 太阳盘面中心比边缘饱和、明亮许多,接近“硬边”效果,且边缘仅有极窄的过渡。

本博客要解决的问题是:如何在实时渲染中,将“真实感天空背景(Preetham 模型)”与“硬边小太阳圆盘”结合,得到高质量的天空渲染?


大气散射与天空亮度模型概述

瑞利散射与 Mie 散射

  • 瑞利散射(Rayleigh Scattering)

    • 由空气分子(直径远小于光波长)引起,满足散射强度与波长的 λ⁻⁴ 关系。

    • 负责天空大部分的蓝色成分,从而导致正午蓝天;随着太阳高度变化,散射路径加长,形成红色日出日落。

  • Mie 散射(Mie Scattering)

    • 由气溶胶、尘埃、水滴等颗粒(直径接近或大于光波长)引起,方向性更强,常表现为“向前散射”(正向)。

    • 造成雾霾、泛白,日出日落时天空边缘出现泛红或泛橙的效应。

为了高效地在 Shader 中模拟以上散射效果,可使用离线数值模拟结果或观测结果拟合得到的解析模型。Preetham 等人在 1999 年基于 Nishita 方法的数值模拟,提出了一种简洁而准确的参数化天空亮度分布模型——Perez 天空公式,并给出了不同太阳高度下的系数拟合。

Preetham 天空模型原理

Preetham 模型(也被称为 Perez Sky Model)通过以下步骤实现真实感天空亮度:

  1. 参数化大气浑浊度

    • 以常用的“turbidity”(浑浊度)值表示空气中气溶胶含量,范围通常在 1.0 (极纯) 到 10.0 (极浑浊)。

    • 浑浊度越高,Mie 散射越明显,天空整体趋于偏灰或偏红。

  2. 计算天顶亮度与天顶颜色(Zenith Luminance / Chromaticity)

    • 根据太阳高度角(solarElevation)与浑浊度,给出 Yz(天顶亮度)以及天空在天顶处的色度 (xz, yz)。

  3. Perez 分布函数

    • 将任意视线方向(以视线与天顶的夹角 θ,以及视线与太阳方向的夹角 γ)映射到亮度:

    • 其中 A–E 为根据浑浊度与太阳高度拟合出的系数; Iz 为天顶亮度。

  4. 将亮度 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, uEPerez 模型系数根据 uTurbidity 与 太阳高度(由 uSunDirection.y 决定)在 CPU 端查表或拟合计算
uZenithColor天顶处线性 RGB同样由 uTurbidity 与太阳高度计算或查表得到
uZenithY天顶处亮度(Y 通道,线性)可参考 Preetham 论文或测量值
uSunIntensity太阳圆盘亮度强度(HDR)10000 ~ 50000 ,可根据场景总曝光量调整
edgeSoft太阳圆盘边缘过渡带宽度(cosγ 方向)0.002 ~ 0.01 ,值越小圆盘边缘越锐利
sunAngularRadius太阳视半径,固定 ≈ 0.2667°一般无需更改 ;若想模拟日食或月球遮挡可动态调整

效果对比示例

  1. 标准晴天(Turbidity = 1.0)

    • 天空呈现深蓝到浅蓝渐变,近地平线位置偏浅;

    • 太阳圆盘清晰锐利,边缘过渡带极窄。

  2. 轻度雾霾(Turbidity = 3.0)

    • 天空整体亮度上升,偏淡白;

    • 太阳周围出现轻微泛白晕染,同时背景融合更明显。

  3. 重度雾霾(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 技术叠加镜头效果。

相关文章:

  • Day 40训练
  • Unknown key: ‘auto_activate_base‘解决
  • AI变革思考2:当小众需求遇上人工智能,催生长尾应用的春天
  • 【AAOS】【源码分析】用户管理(三)-- 用户启动
  • 045-代码味道-数据泥团
  • ObservableRecipient与ObservableObject
  • 深度学习习题2
  • Python爬虫与Java爬虫深度对比:从原理到实战案例解析
  • Java 中比较两个 long 类型变量大小的方法
  • Linux网桥实战手册:从基础配置到虚拟化网络深度优化
  • N2语法 強調、限定
  • RK3588 InsightFace人脸识别移植及精度测试全解析
  • 合并表格(按行合并)
  • 汇川变频器MD600S-4T-5R5为什么要搭配GRJ9000S-10-T滤波器?
  • Unity基础-Mathf相关
  • latex画表格
  • 深度学习习题3
  • c# :this() 和 :base()区别
  • Axure 与 Cursor 集成实现方案
  • 【iOS】cache_t分析
  • 网站报错 500/成都网站seo诊断
  • 企业宣传片制作拍摄电话/seo主要做什么工作
  • 做网站要好多钱/app拉新接单平台
  • 网站建设 文库/中公教育培训机构官网
  • 一般通过什么意思/网站优化包括对什么优化
  • 优质的南昌网站建设/北京百度推广优化排名