Unity URP动态天空盒实战从零实现昼夜交替与大气散射效果含完整Shader代码在构建一个令人信服的开放世界或写实风格场景时天空往往是最容易被忽视却又最能奠定整体氛围的基石。一张静态的、精美的天空盒贴图在十年前或许足够但对于今天的玩家和体验者来说一个能够动态响应时间流逝、光源变化甚至模拟真实大气光学现象的天空才是沉浸感的关键所在。很多开发者尝试实现动态天空时常常陷入效果生硬、性能开销巨大或代码难以维护的困境。本文将带你从零开始在Unity的URP管线中构建一套不仅视觉效果惊艳而且结构清晰、性能可控的动态天空系统。我们会深入探讨从基础的日夜颜色过渡到复杂的日月位置计算再到基于物理的大气散射模拟并提供可直接集成到你项目中的完整Shader代码。无论你是想为你的独立游戏增添一抹真实的晨昏还是为你的数字孪生应用模拟精确的天象这里都有你需要的答案。1. 构建动态天空的基石核心架构与日夜状态管理在动手写第一行Shader代码之前我们需要建立一个清晰、可扩展的架构。一个优秀的动态天空系统不应该是一堆难以维护的魔法数字和硬编码逻辑的集合。我们的目标是创建一个数据驱动的框架将时间、光照状态与视觉表现解耦。首先我们需要一个可靠的时间与状态管理系统。这个系统不直接处理渲染而是为Shader提供统一的输入参数。我通常会创建一个名为SkyController的MonoBehaviour脚本它挂在场景中的一个空物体上作为整个天空系统的“大脑”。using UnityEngine; using UnityEngine.Rendering; public class SkyController : MonoBehaviour { [Header(时间控制)] [Range(0f, 24f)] public float timeOfDay 12.0f; // 24小时制 public float timeScale 60.0f; // 游戏内1秒对应现实多少秒 [Header(太阳/月亮光源)] public Light directionalLight; // URP主方向光 [Header(天空材质)] public Material skyboxMaterial; private void Update() { // 更新时间 timeOfDay Time.deltaTime / 3600f * timeScale; // 将秒转换为小时增量 timeOfDay % 24.0f; // 计算太阳/月亮位置基于时间 UpdateCelestialPositions(); // 将关键参数传递给Shader UpdateShaderProperties(); } private void UpdateCelestialPositions() { // 将一天24小时映射到360度 float sunAngle (timeOfDay / 24.0f) * 360.0f - 90.0f; // -90度让0点在东方地平线 float sunRad sunAngle * Mathf.Deg2Rad; // 计算太阳方向向量Y-up坐标系 Vector3 sunDirection new Vector3( Mathf.Cos(sunRad), Mathf.Sin(sunRad), 0.0f ).normalized; // 更新主光源方向 if (directionalLight ! null) { directionalLight.transform.forward -sunDirection; } // 月亮方向与太阳相反简化模型 Vector3 moonDirection -sunDirection; } private void UpdateShaderProperties() { if (skyboxMaterial null) return; // 传递时间参数 skyboxMaterial.SetFloat(_TimeOfDay, timeOfDay); // 计算并传递日夜状态因子 float sunHeight directionalLight.transform.forward.y; // 一个更平滑的日夜过渡曲线 float dayNightFactor Mathf.Clamp01((sunHeight 0.1f) / 0.2f); skyboxMaterial.SetFloat(_DayNightFactor, dayNightFactor); // 传递太阳/月亮方向 skyboxMaterial.SetVector(_SunDirection, -directionalLight.transform.forward); skyboxMaterial.SetVector(_MoonDirection, directionalLight.transform.forward); } }这个控制器脚本解决了几个关键问题时间流逝的逻辑、天体位置的计算以及Shader参数的统一管理。注意我们将太阳高度sunHeight映射到一个0到1的_DayNightFactor这个因子将成为Shader中几乎所有颜色和效果混合的“总开关”。提示在实际项目中你可能希望将timeOfDay与游戏存档系统关联或者允许玩家通过UI滑块直接调整时间。SkyController的设计使其很容易扩展这些功能。接下来我们进入Shader部分首先定义核心属性。一个好的属性组织能让美术或策划人员在Inspector窗口中直观地调整效果。// 在Properties块中定义 Properties { // 日夜基础色 [Header(Day Colors)] _DayHorizonColor(Day Horizon, Color) (0.4, 0.7, 1.0, 1.0) _DayZenithColor(Day Zenith, Color) (0.1, 0.3, 0.8, 1.0) [Header(Night Colors)] _NightHorizonColor(Night Horizon, Color) (0.05, 0.05, 0.1, 1.0) _NightZenithColor(Night Zenith, Color) (0.01, 0.01, 0.02, 1.0) // 过渡控制 [Header(Transitions)] _SunriseStartHeight(Sunrise Start, Range(-0.5, 0.5)) -0.1 _SunriseEndHeight(Sunrise End, Range(-0.5, 0.5)) 0.1 _SunsetStartHeight(Sunset Start, Range(-0.5, 0.5)) 0.1 _SunsetEndHeight(Sunset End, Range(-0.5, 0.5)) -0.1 // 光晕效果 [Header(Horizon Glow)] _HorizonGlowWidth(Glow Width, Range(0.01, 1.0)) 0.15 _HorizonGlowIntensity(Glow Intensity, Range(0.0, 5.0)) 2.0 _HorizonGlowColorDay(Day Glow Color, Color) (1.0, 0.6, 0.3, 1.0) _HorizonGlowColorNight(Night Glow Color, Color) (0.3, 0.4, 0.8, 1.0) }有了这些基础我们就可以在片元着色器中开始构建天空的底色了。核心思路是根据观察方向与世界空间垂直方向Y轴的夹角以及我们计算出的_DayNightFactor来混合日夜两套颜色。2. 天空底色与地平线光晕从渐变到自然过渡天空的颜色从来不是均匀的。从头顶的天顶到远方的地平线存在着微妙而重要的渐变。同时在日出日落时分地平线附近会出现温暖或清冷的光晕。实现这些效果关键在于对观察方向向量的巧妙运用。在URP的天空盒Shader中顶点着色器输出的世界位置方向worldPos是从相机指向天空球面上某点的向量。这个向量的Y分量worldDir.y是我们判断“高度”的关键。// 在片元着色器中 float3 CalculateSkyGradient(float3 worldDir, float dayNightFactor) { // 1. 计算垂直位置将worldDir.y从[-1, 1]映射到[0, 1] // 0代表正对地平线1代表正对天顶 float verticalPos saturate(worldDir.y * 0.5 0.5); // 2. 分别计算白天和夜晚的渐变 // 白天地平线到天顶的渐变 float3 dayGradient lerp(_DayHorizonColor.rgb, _DayZenithColor.rgb, verticalPos); // 夜晚使用不同的颜色曲线夜晚的天空顶部可能更暗 // 这里使用pow函数让夜晚的渐变更陡峭 float nightVertical pow(verticalPos, 1.5); float3 nightGradient lerp(_NightHorizonColor.rgb, _NightZenithColor.rgb, nightVertical); // 3. 根据日夜因子平滑混合 float3 finalGradient lerp(nightGradient, dayGradient, dayNightFactor); return finalGradient; }但这只是基础。真实天空在地平线处的颜色变化更为复杂尤其是在清晨和黄昏。我们需要引入一个地平线光晕效果。这个效果本质上是一个以地平线为中心、向外平滑衰减的“光带”。我更喜欢使用一个基于高斯函数的衰减而不是简单的线性或指数衰减因为它能产生更柔和、更自然的光晕边缘。float3 CalculateHorizonGlow(float3 worldDir, float dayNightFactor) { // 计算当前视线方向与地平线的“距离” // abs(worldDir.y)越小说明视线越接近地平线 float horizonDistance abs(worldDir.y); // 高斯衰减函数exp(-(x^2) / (width^2)) // 当x0正对地平线时值为1随着x增大值快速衰减到0 float glowMask exp(-(horizonDistance * horizonDistance) / (_HorizonGlowWidth * _HorizonGlowWidth)); // 根据日夜状态混合光晕颜色 float3 glowColor lerp(_HorizonGlowColorNight.rgb, _HorizonGlowColorDay.rgb, dayNightFactor); // 调整光晕强度在日出日落时最强正午和深夜最弱 // 这里假设太阳高度信息通过_SunDirection.y传递 float sunHeight _SunDirection.y; float timeIntensity smoothstep(-0.2, 0.0, sunHeight) * (1.0 - smoothstep(0.0, 0.2, sunHeight)); timeIntensity saturate(timeIntensity * 2.0); // 增强效果 float3 finalGlow glowColor * glowMask * _HorizonGlowIntensity * timeIntensity; return finalGlow; }将底色与光晕结合我们就能得到一个初步的动态天空float3 baseSkyColor CalculateSkyGradient(worldDir, dayNightFactor); float3 horizonGlow CalculateHorizonGlow(worldDir, dayNightFactor); float3 finalSkyColor baseSkyColor horizonGlow;然而一个常见的陷阱是颜色值可能超过1.0HDR范围导致过曝。在移动平台或某些色彩空间下这会导致问题。一个简单的修复方法是使用min函数或tonemapping。// 简单的颜色钳位 finalSkyColor min(finalSkyColor, float3(10.0, 10.0, 10.0)); // 设置一个合理的上限 // 或者使用ACES色调映射近似 float3 ACESToneMapping(float3 color) { float A 2.51; float B 0.03; float C 2.43; float D 0.59; float E 0.14; color (color * (A * color B)) / (color * (C * color D) E); return saturate(color); }下表总结了这一阶段的关键参数及其视觉影响参数类型默认值视觉影响调整建议_DayHorizonColorColor(0.4, 0.7, 1.0)白天地平线附近的颜色偏向暖色调如浅蓝、淡紫_DayZenithColorColor(0.1, 0.3, 0.8)白天头顶天空的颜色通常比地平线颜色更深、更饱和_NightHorizonColorColor(0.05, 0.05, 0.1)夜晚地平线颜色非常暗略带环境光色_NightZenithColorColor(0.01, 0.01, 0.02)夜晚天顶颜色接近纯黑但非完全黑_HorizonGlowWidthRange0.15地平线光晕的宽度值越大光晕覆盖的天空范围越广_HorizonGlowIntensityRange2.0光晕的亮度日出日落时可调高至3-4verticalPos幂指数代码常量1.5夜晚渐变曲线的陡峭度增大使天顶更快变黑注意这些颜色值在线性颜色空间Linear Color Space下工作效果最佳。如果你的项目使用的是Gamma空间可能需要适当提高饱和度因为Gamma空间下的颜色混合会显得更“灰”。3. 日月星辰的绘制天体位置、光晕与动态细节有了背景天空接下来我们需要在上面“放置”太阳、月亮和星星。这不仅仅是画两个发光的圆盘那么简单我们需要考虑它们的运动轨迹、透视大小、光晕效果以及与背景天空的交互如太阳被云层部分遮挡的视觉效果。3.1 太阳的绘制与光晕太阳的绘制核心是判断当前片元像素是否在太阳的圆盘范围内。我们通过比较观察方向worldDir与太阳方向_SunDirection之间的夹角来实现。float3 DrawSun(float3 worldDir, float3 sunDir, float3 skyColor) { // 计算观察方向与太阳方向的点积值越接近1说明方向越一致 float sunDot dot(normalize(worldDir), normalize(sunDir)); // 将点积转换为一个圆盘遮罩 float sunDisk smoothstep(0.9985, 0.9995, sunDot); // 调整阈值控制太阳大小 // 基础太阳颜色 float3 sunColor _SunColor.rgb * sunDisk; // **太阳光晕**一个比太阳圆盘大得多的柔和发光区域 // 使用更宽松的阈值和不同的衰减曲线 float sunGlowDot saturate(dot(worldDir, sunDir)); // 使用pow函数创造非线性的衰减让光晕中心亮边缘柔和 float sunGlow pow(sunGlowDot, _SunGlowFalloff) * _SunGlowIntensity; // 确保光晕只在太阳附近可见 sunGlow * (1.0 - sunDisk); // 避免与太阳圆盘本身叠加过亮 // 将光晕颜色通常比太阳本身更偏暖、更散射叠加到天空颜色上 float3 glowColor _SunGlowColor.rgb * sunGlow; skyColor glowColor; // 最后将太阳圆盘本身叠加在最上层 skyColor lerp(skyColor, sunColor, sunDisk); return skyColor; }这里有一个重要的细节太阳的视觉大小应该随着高度角变化吗在现实中当太阳接近地平线时由于大气折射和人类的心理感知蓬佐错觉它看起来会更大。我们可以模拟这个效果// 动态太阳大小地平线附近更大 float sunHeightFactor saturate(-sunDir.y * 2.0 0.5); // 太阳越低值越大 float adaptiveSunSize lerp(_SunBaseSize, _SunBaseSize * _HorizonSizeMultiplier, sunHeightFactor); // 在计算sunDot之前使用adaptiveSunSize调整平滑步进的阈值3.2 月亮的绘制与纹理映射月亮的绘制比太阳复杂因为我们通常希望月亮表面有纹理月海、环形山。简单地用方向判断画一个圆盘再叠加纹理会导致纹理在天空球上“滑动”因为天空球随着相机旋转。我们需要将观察方向转换到一个与月亮相对稳定的空间。解决方案是使用一个变换矩阵。我们在C#脚本中计算这个矩阵并将其传递给Shader。// 在SkyController的UpdateShaderProperties方法中添加 if (directionalLight ! null) { // 获取代表月亮方向的变换这里用主光源的逆方向作为月亮方向 Transform moonTransform directionalLight.transform; // 实际项目中可能是一个单独的月亮空物体 Matrix4x4 moonLocalToWorld moonTransform.localToWorldMatrix; // 我们需要世界到月亮的矩阵所以取逆 Matrix4x4 worldToMoon moonLocalToWorld.inverse; skyboxMaterial.SetMatrix(_WorldToMoonMatrix, worldToMoon); }在Shader中我们使用这个矩阵将世界空间的观察方向转换到月亮的“局部空间”在这个空间里月亮纹理的UV坐标才是稳定的。float3 DrawMoon(float3 worldDir, float3 moonDir, float3 skyColor) { // 将世界空间方向转换到月亮局部空间 float3 moonLocalDir mul((float3x3)_WorldToMoonMatrix, worldDir).xyz; // 使用转换后的方向生成UV坐标 // 假设月亮纹理是球面投影我们可以使用正交投影 float2 moonUV moonLocalDir.xy * 0.5 0.5; moonUV moonUV * _MoonTex_ST.xy _MoonTex_ST.zw; // 应用纹理的缩放和偏移 // 采样月亮纹理 float4 moonTex SAMPLE_TEXTURE2D(_MoonTex, sampler_MoonTex, moonUV); // 判断当前片元是否在月亮圆盘内基于角度 float moonDot dot(normalize(worldDir), normalize(moonDir)); float moonDisk smoothstep(0.998, 0.999, moonDot); // 结合纹理和圆盘遮罩 float3 moonColor moonTex.rgb * _MoonColor.rgb * moonTex.a; // 使用纹理的alpha通道 moonColor * moonDisk; // 月亮光晕比太阳光晕更冷、更柔和 float moonGlowDot saturate(dot(worldDir, moonDir)); float moonGlow pow(moonGlowDot, _MoonGlowFalloff) * _MoonGlowIntensity; moonGlow * (1.0 - moonDisk); float3 moonGlowColor _MoonGlowColor.rgb * moonGlow; skyColor moonGlowColor; skyColor lerp(skyColor, moonColor, moonDisk); return skyColor; }3.3 星星与银河的程序化生成星星和银河不能是简单的静态贴图否则当相机旋转时它们会显得不自然。一种常见且高效的方法是使用立方体贴图Cubemap。但为了更动态的效果如星星闪烁我们可以采用程序化生成结合纹理采样的方法。星星闪烁可以通过一个基于时间和空间位置变化的噪声函数来实现避免所有星星同步闪烁的不自然感。float3 DrawStars(float3 worldDir, float dayNightFactor, float3 skyColor) { // 只在夜晚显示星星 float nightVisibility 1.0 - smoothstep(0.0, 0.15, dayNightFactor); if (nightVisibility 0.01) return skyColor; // 方法1使用立方体贴图简单性能好但缺乏动态细节 // float3 stars SAMPLE_TEXTURECUBE(_StarsCubemap, sampler_StarsCubemap, worldDir).rgb; // 方法2程序化投影 噪声更动态可控性高 // 将球面坐标映射到2D平面用于采样星星密度图 float2 starsUV; // 避免在极点附近UV拉伸严重 if (abs(worldDir.y) 0.999) { starsUV float2(worldDir.x * 0.5 0.5, worldDir.z * 0.5 0.5); } else { float scale 1.0 / sqrt(1.0 - worldDir.y * worldDir.y); starsUV worldDir.xz * scale * 0.1; // 缩放系数控制星星密度 } starsUV _StarsOffset; // 可以随时间缓慢偏移模拟星空旋转 // 采样星星密度/亮度图 float4 starTex SAMPLE_TEXTURE2D(_StarDensityTex, sampler_StarDensityTex, starsUV); // 添加闪烁效果使用柏林噪声生成每颗星星独立的闪烁周期 float2 noiseUV starsUV * _TwinkleNoiseScale _Time.y * _TwinkleSpeed; float twinkleNoise (GenerateNoise(noiseUV) * 2.0 - 1.0) * _TwinkleIntensity; float twinkleFactor 1.0 twinkleNoise; // 计算最终星星颜色 float3 starsColor starTex.rgb * _StarsColor.rgb * _StarsIntensity * twinkleFactor; starsColor * nightVisibility; // 受日夜因子影响 // 将星星叠加到天空上使用加法混合 skyColor starsColor; return skyColor; }银河可以看作是一片密度更高的星星区域我们可以用另一张纹理来定义其形状并叠加一些缓慢流动的噪声来模拟其微弱的动态。float3 DrawGalaxy(float3 worldDir, float dayNightFactor, float3 skyColor) { float nightVisibility 1.0 - smoothstep(0.0, 0.1, dayNightFactor); if (nightVisibility 0.01) return skyColor; // 银河UV使用不同的投影缩放使其在天空中的比例合适 float2 galaxyUV worldDir.xz * 0.05; // 采样银河主纹理 float4 galaxyTex SAMPLE_TEXTURE2D(_GalaxyTex, sampler_GalaxyTex, galaxyUV); // 采样流动噪声纹理用于扭曲银河UV产生缓慢的“飘动”感 float2 flowUV galaxyUV _Time.x * _GalaxyFlowSpeed; float2 flowNoise SAMPLE_TEXTURE2D(_FlowNoiseTex, sampler_FlowNoiseTex, flowUV).rg * 2.0 - 1.0; flowNoise * _GalaxyFlowStrength; // 应用扭曲 float2 distortedUV galaxyUV flowNoise; galaxyTex SAMPLE_TEXTURE2D(_GalaxyTex, sampler_GalaxyTex, distortedUV); // 增强对比度让银河中心更亮 galaxyTex.rgb pow(galaxyTex.rgb, _GalaxyContrast); float3 galaxyColor galaxyTex.rgb * _GalaxyColor.rgb * _GalaxyIntensity; galaxyColor * nightVisibility; // 银河通常在地平线附近会因大气消光而减弱 float horizonFade smoothstep(0.1, 0.4, abs(worldDir.y)); galaxyColor * horizonFade; skyColor galaxyColor; return skyColor; }将所有这些天体绘制函数按正确的顺序组合通常是先背景、再光晕、最后是前景天体你就能得到一个拥有动态日月星辰的生动天空了。4. 大气散射的物理模拟从瑞利散射到米氏散射至此我们的天空看起来已经不错了但它还缺少真实感中最关键的一环大气散射。正是大气散射让白天的天空呈现蓝色让日出日落时的天空染上绚烂的红色与橙色。忽略这一物理过程天空看起来就会像一块单纯的渐变背景板。大气散射主要分为两种类型它们共同作用形成了我们看到的天空颜色瑞利散射Rayleigh Scattering由远小于光波长的空气分子如氮气、氧气引起。它对短波长的光蓝色、紫色散射更强这就是为什么晴朗的天空是蓝色的。瑞利散射的强度与波长的四次方成反比~1/λ⁴。米氏散射Mie Scattering由大气中较大的颗粒物如尘埃、水蒸气、污染物引起。它对所有波长的光散射强度相近但具有强烈的方向性前向散射。米氏散射导致了日出日落时的红色霞光、太阳周围的光晕以及雾蒙蒙的天空。在Shader中完全精确地模拟这些物理过程计算量巨大。对于实时渲染我们采用一种简化的、基于**光线步进Raymarching**的近似方法。4.1 基础光线步进框架我们首先需要定义大气层模型。通常我们将地球和大气层简化为两个同心球体。// 在Shader顶部定义大气参数 float _PlanetRadius; // 行星半径单位米地球约6371km float _AtmosphereHeight; // 大气层高度单位米地球约100km float3 _PlanetCenter; // 行星中心通常为世界原点下方 // 光线与球体求交函数核心 float2 RaySphereIntersection(float3 rayOrigin, float3 rayDir, float3 sphereCenter, float sphereRadius) { float3 oc rayOrigin - sphereCenter; float a dot(rayDir, rayDir); float b 2.0 * dot(oc, rayDir); float c dot(oc, oc) - sphereRadius * sphereRadius; float discriminant b * b - 4.0 * a * c; // 无交点 if (discriminant 0.0) return float2(-1.0, -1.0); float sqrtDisc sqrt(discriminant); float t1 (-b - sqrtDisc) / (2.0 * a); // 近交点 float t2 (-b sqrtDisc) / (2.0 * a); // 远交点 return float2(t1, t2); }这个函数返回两个距离值分别代表光线进入和离开球体的位置。对于天空渲染rayOrigin是相机位置rayDir是观察方向。4.2 简化散射积分完整的散射积分需要计算光线路径上每一点到太阳的光学深度这非常昂贵。我们采用一个高度依赖的密度函数和简化的积分来近似。float3 CalculateAtmosphericScattering(float3 rayOrigin, float3 rayDir, float3 sunDir) { // 1. 计算视线与大气层的交点 float2 atmIntersect RaySphereIntersection(rayOrigin, rayDir, _PlanetCenter, _PlanetRadius _AtmosphereHeight); // 如果视线没有指向天空与大气层无交点返回黑色 if (atmIntersect.x 0.0 atmIntersect.y 0.0) return float3(0,0,0); float rayStart max(0.0, atmIntersect.x); // 进入大气的点 float rayEnd atmIntersect.y; // 离开大气的点或无穷远 float rayLength rayEnd - rayStart; // 2. 光线步进 int numSteps 8; // 步数越多越精确性能开销越大 float stepSize rayLength / float(numSteps); float3 step rayDir * stepSize; float3 currentPos rayOrigin rayDir * rayStart; float3 totalLight float3(0,0,0); for (int i 0; i numSteps; i) { // 计算当前点的高度 float height length(currentPos - _PlanetCenter) - _PlanetRadius; // 高度归一化 float normalizedHeight saturate(height / _AtmosphereHeight); // **瑞利散射密度**随高度指数衰减 float rayleighDensity exp(-normalizedHeight / _RayleighScaleHeight) * _RayleighScatteringCoeff; // **米氏散射密度**衰减更快集中在低空 float mieDensity exp(-normalizedHeight / _MieScaleHeight) * _MieScatteringCoeff; // 计算从当前点到太阳的光学深度简化假设为常数或使用预计算 // 这里简化使用高度相关的密度近似 float sunRayDensity exp(-normalizedHeight / (_AtmosphereHeight * 0.3)); // 组合散射贡献 // 瑞利散射贡献蓝色通道更强 float3 rayleighContrib _RayleighScatteringCoeff * float3(0.65, 0.57, 0.475) * rayleighDensity * sunRayDensity; // 米氏散射贡献各波长均匀产生白色/暖色光晕 float3 mieContrib _MieScatteringCoeff * float3(1.0, 1.0, 1.0) * mieDensity * sunRayDensity; // 相位函数描述散射的方向性 float cosTheta dot(rayDir, sunDir); float rayleighPhase (3.0 / (16.0 * PI)) * (1.0 cosTheta * cosTheta); float miePhase _MiePhaseG / (4.0 * PI) * pow((1.0 _MiePhaseG * _MiePhaseG - 2.0 * _MiePhaseG * cosTheta), -1.5); totalLight (rayleighContrib * rayleighPhase mieContrib * miePhase) * stepSize; currentPos step; } // 3. 应用曝光和色调映射 totalLight * _ScatteringIntensity; // 简单的曝光控制 totalLight 1.0 - exp(-totalLight * _Exposure); return totalLight; }4.3 整合到天空着色器最后我们需要将大气散射的颜色与之前计算的天空颜色进行混合。散射光应该以加法混合的方式叠加到天空底色上。// 在片元着色器主函数中 float3 scatteringColor CalculateAtmosphericScattering(_WorldSpaceCameraPos, worldDir, _SunDirection); // 散射强度受太阳高度影响太阳在地平线时光线穿过的大气更厚散射更强 float sunHeightFactor saturate(_SunDirection.y * 10.0 1.0); // 简化计算 scatteringColor * sunHeightFactor; // 最终颜色混合 float3 finalColor baseSkyColor horizonGlow sunAndMoonColor starsColor scatteringColor;下表概述了大气散射的关键参数参数描述典型值视觉影响_RayleighScatteringCoeff瑞利散射系数(5.8e-6, 1.35e-5, 3.31e-5)控制天空的蓝色程度。增大使天空更蓝。_MieScatteringCoeff米氏散射系数4e-5控制雾霾、霞光和太阳光晕的强度。_RayleighScaleHeight瑞利散射尺度高度8000瑞利散射密度随高度衰减的速度。值越大高空蓝色越明显。_MieScaleHeight米氏散射尺度高度1200米氏散射雾霾主要集中在低空。_MiePhaseG米氏散射相位函数不对称因子0.76控制前向散射的尖锐程度。值越接近1太阳光晕越集中。_Exposure散射结果曝光度2.0整体调整散射亮度。注意这些物理参数的值是基于地球大气模型的近似。你可以调整它们来创造外星世界的大气效果例如一个拥有浓厚二氧化碳大气层散射更强的火星风格天空。实现到这里你已经拥有了一个功能完整、视觉效果丰富的动态天空系统。它包含了从日夜循环、天体运动到基于物理的大气散射等一系列高级特性。将所有这些代码模块整合到一个精心组织的URP Shader Graph或手写Shader中你就能为你的项目注入一片充满生机的天空。记住参数调整需要耐心和艺术感最好的效果往往来自于对现实世界的细致观察与适度的艺术夸张之间的平衡。