Unity Shader实现动态日夜循环:从原理到实战的完整指南

📅 2026/7/28 7:47:05 👁️ 阅读次数 📝 编程学习
Unity Shader实现动态日夜循环:从原理到实战的完整指南

1. 项目概述:从静态到动态的视觉叙事

在游戏和交互式应用开发中,环境氛围的营造是沉浸感的关键。一个静态的场景,无论光影多么精致,总感觉缺少一丝“呼吸感”。而“日夜变化”恰恰是赋予虚拟世界生命力的最直观、最有效的手段之一。想象一下,你精心搭建的城镇,从清晨薄雾中的第一缕阳光,到正午烈日下的清晰轮廓,再到黄昏时分被染成金红色的天际线,最后沉入静谧深邃的星空。这种动态的视觉叙事,能极大地增强玩家的代入感和情感共鸣。

实现这种变化,最核心、最高效的技术路径就是编写自定义的 Unity Shader。为什么是 Shader?因为它是 GPU 上运行的程序,直接控制着每个像素的最终颜色,绕开了 CPU 逐物体计算的性能瓶颈。通过 Shader,我们可以将时间作为一个变量,动态地驱动天空颜色、太阳/月亮位置、环境光照乃至雾效等一系列视觉参数的平滑过渡。这不仅仅是“换天”那么简单,而是一套完整的、基于物理(或风格化)规律的视觉系统联动。

这个项目,就是带你从零开始,构建一个支持平滑日夜循环的 Unity Shader。它不依赖于复杂的天气系统插件,而是深入图形渲染管线,理解光照模型、向量运算和插值原理,最终实现一个性能高效、效果可控的解决方案。无论你是想为你的独立游戏增添一抹动态色彩,还是希望深入理解 Shader 如何驱动视觉变化,这篇内容都将提供一条清晰的实践路径。

2. 核心思路与方案设计:构建一个基于时间的驱动引擎

要实现一个看起来自然且高效的日夜变化,我们不能简单地让天空盒在两个贴图之间切换,那样会显得非常生硬。我们的核心思路是:构建一个以时间为核心驱动参数的视觉计算模型。这个模型将时间映射到一系列视觉属性上,并通过 Shader 在 GPU 端实时计算每一帧的效果。

2.1 核心驱动:模拟太阳轨迹

日夜变化的本质是太阳(或等效光源)相对于观察者的位置变化。在 Shader 中,我们通常用一个方向向量来代表太阳光的方向。一个简单而有效的模拟方法是使用球面坐标。

我们定义一个循环周期(例如 24 秒模拟 24 小时),用_Time.y(自场景加载以来的时间,以秒为单位)结合周期来计算当前的时间进度。然后,将这个进度映射到球面的经度(方位角)和纬度(高度角)上。例如,我们可以让太阳在正午时处于头顶正上方(高度角最大),在清晨和黄昏时处于地平线附近(高度角较小)。

// 在Shader的Properties或变量声明区 _TimeOfDay(“Time of Day”, Range(0, 1)) = 0.25 // 默认清晨 _CycleDuration(“Cycle Duration (Seconds)”, Float) = 60.0 // 在顶点或片元着色器中计算 float timeProgress = frac(_Time.y / _CycleDuration); // 取小数部分,实现循环 // 或者使用自定义的_TimeOfDay变量,由脚本控制 float currentTime = _TimeOfDay; // 将时间进度映射到球面坐标(简化模型,假设太阳在赤道平面运动) float sunAzimuth = currentTime * TWO_PI; // 方位角,0到2π float sunAltitude = sin(currentTime * PI) * PI / 3.0; // 高度角,在0到π/3之间变化 // 将球面坐标转换为笛卡尔坐标系下的方向向量(世界空间) float3 sunDirection; sunDirection.x = cos(sunAltitude) * sin(sunAzimuth); sunDirection.y = sin(sunAltitude); sunDirection.z = cos(sunAltitude) * cos(sunAzimuth);

这个sunDirection向量将成为我们后续所有光照计算的基础。通过调整映射函数,你可以轻松模拟不同季节、不同纬度的太阳轨迹。

2.2 视觉属性联动设计

有了太阳方向,接下来就需要定义哪些视觉属性会随之变化,以及如何变化。一个完整的系统通常包括:

  1. 天空颜色/渐变:这是最显著的变化。我们需要一个从深蓝(夜晚)到亮蓝(白天)再到橙红(黄昏)的平滑渐变。可以使用多个颜色关键帧(_SkyColorNight,_SkyColorDay,_SkyColorSunset)并根据太阳高度角进行插值。
  2. 太阳光颜色与强度:正午的阳光是明亮的白色,黄昏时则偏橙红色,夜晚月光是冷调的淡蓝色。光的强度也随太阳高度角变化,正午最强,夜晚最弱(或为零)。
  3. 环境光:Unity 的环境光(Ambient Light)也应随之调整,以匹配天空的整体亮度。这通常需要在脚本中配合修改RenderSettings.ambientLight或使用环境探针。
  4. 雾效:雾的颜色和密度在日夜不同时段差异很大。白天的雾可能更透明、偏白,夜晚的雾可能更浓、偏蓝或灰。
  5. 星月显示:当太阳高度角低于某个阈值(夜晚),可以淡入星星和月亮的纹理。

注意:直接在 Shader 中修改全局环境光和雾效比较困难,通常的实践是:Shader 负责计算当前“应该”的颜色和强度,然后通过一个 C# 脚本(挂载在摄像机或全局管理器上)读取 Shader 的中间计算结果(例如通过一个不可见的 Renderer 或 MaterialPropertyBlock),再去设置RenderSettings中的全局属性。这样实现了 Shader 计算与 Unity 渲染管线的联动。

2.3 Shader 类型选择:Surface Shader vs. Unlit Shader

Unity 提供了多种 Shader 编写方式。对于这个项目,主要有两个选择:

  • Surface Shader:这是 Unity 对光照模型的封装,写起来简单,能自动处理前向渲染中的多光源。如果你想让你受日夜变化影响的物体(如地形、建筑)仍然接受场景中其他点光源的影响,Surface Shader 是个好选择。你需要在其光照模型函数中,用我们计算的sunDirectionsunColor来覆盖或影响默认的主光源。
  • Unlit Shader:完全不参与 Unity 的光照管线,所有颜色自己计算。这是实现全屏天空盒、后期处理效果或风格化渲染的常用选择。对于核心的日夜天空渲染,Unlit Shader 更直接、更高效,因为天空本身通常不需要复杂的光照计算。

我的选择与理由:为了更清晰地展示日夜变化的核心原理,并实现最高效的天空渲染,本方案将采用Unlit Shader来构建天空盒。对于场景中需要受日夜主光源影响的物体,我们可以额外编写一个简化的 Surface Shader,或者通过脚本将计算好的主光方向与颜色赋给场景的Directional Light。这种“天空与物体分离处理”的方式,结构清晰,也便于分别优化。

3. 核心 Shader 实现详解:编写天空盒着色器

我们将创建一个自定义的天空盒 Shader。在 Unity 中,天空盒本质上是一个包裹整个场景的巨大球体或立方体,其 Shader 在每个像素上根据观察方向计算颜色。

3.1 基础框架与属性定义

首先,创建一个新的 Unlit Shader,并定义驱动日夜变化所需的属性。

Shader “Custom/SkyboxDynamicDayNight” { Properties { // 时间控制 [Header(Time Control)] _TimeOfDay (“Time of Day”, Range(0, 1)) = 0.25 _CycleSpeed (“Cycle Speed Multiplier”, Float) = 1.0 // 天空颜色关键帧 [Header(Sky Colors)] _SkyColorNight (“Night Sky Color”, Color) = (0.05, 0.05, 0.1, 1) _SkyColorDay (“Day Sky Color”, Color) = (0.4, 0.7, 1.0, 1) _SkyColorSunset (“Sunset Color”, Color) = (1.0, 0.4, 0.1, 1) _HorizonColor (“Horizon Color”, Color) = (0.7, 0.8, 0.9, 1) _HorizonSharpness (“Horizon Sharpness”, Range(0, 10)) = 3.0 // 太阳属性 [Header(Sun)] _SunSize (“Sun Size”, Range(0, 0.5)) = 0.05 _SunColor (“Sun Color”, Color) = (1, 0.95, 0.8, 1) _SunIntensity (“Sun Intensity”, Float) = 2.0 // 星星属性 [Header(Stars)] _StarsTex (“Stars Cubemap”, Cube) = “black” {} _StarsIntensity (“Stars Intensity”, Float) = 1.0 _StarsTwinkleSpeed (“Twinkle Speed”, Float) = 0.5 } SubShader { Tags { “Queue”=“Background” “RenderType”=“Background” “PreviewType”=“Skybox” } Cull Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include “UnityCG.cginc” // 属性变量声明(与Properties对应) uniform float _TimeOfDay; uniform float _CycleSpeed; uniform float4 _SkyColorNight; uniform float4 _SkyColorDay; uniform float4 _SkyColorSunset; uniform float4 _HorizonColor; uniform float _HorizonSharpness; uniform float _SunSize; uniform float4 _SunColor; uniform float _SunIntensity; uniform samplerCUBE _StarsTex; uniform float _StarsIntensity; uniform float _StarsTwinkleSpeed; // … 结构体和函数将在下面展开 ENDCG } } FallBack Off }

3.2 顶点与片元着色器结构

这是一个标准的天空盒着色器结构,顶点着色器主要负责传递观察方向。

struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; float3 viewDir : TEXCOORD0; // 世界空间观察方向 }; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); // 计算从摄像机指向天空盒顶点的方向(世界空间) o.viewDir = mul(unity_ObjectToWorld, v.vertex).xyz - _WorldSpaceCameraPos; return o; }

核心逻辑全部在片元着色器frag中。

3.3 计算太阳方向与高度

首先,我们需要一个根据_TimeOfDay计算太阳方向和高度的函数。这里采用一个简化的模型。

// 将时间(0-1)映射到太阳高度角(-90度到90度,但通常我们只用到-10到90度) // 0.0: 午夜 (最低点) -> 0.25: 日出 (0度附近) -> 0.5: 正午 (最高点) -> 0.75: 日落 (0度附近) -> 1.0: 午夜 float GetSunAltitude(float time) { // 使用正弦函数模拟平滑的升降,将值域映射到[-PI/2, PI/2]再转换到[-1,1]范围 float rad = (time - 0.25) * UNITY_TWO_PI; // 让日出在time=0.25时高度为0 return sin(rad) * 0.5 + 0.5; // 映射到[0,1],1为最高 } // 根据时间和高度计算归一化的太阳方向向量(假设太阳在XZ平面做圆周运动) float3 CalculateSunDirection(float time, float altitude) { float azimuth = time * UNITY_TWO_PI; // 方位角 float radAltitude = altitude * UNITY_HALF_PI; // 将[0,1]映射到[0, 90]度弧度 float3 dir; dir.x = sin(azimuth) * cos(radAltitude); dir.y = sin(radAltitude); // Y轴向上 dir.z = cos(azimuth) * cos(radAltitude); return normalize(dir); }

在片元着色器中调用:

float sunAltitude = GetSunAltitude(_TimeOfDay); float3 sunDir = CalculateSunDirection(_TimeOfDay, sunAltitude);

3.4 混合天空颜色

天空颜色不是简单的两种颜色切换,而是根据太阳高度和观察点的垂直分量(viewDir.y)进行多层混合。

float4 CalculateSkyColor(float3 viewDir, float3 sunDir, float sunAltitude) { // 1. 基础日夜混合:根据太阳高度在夜晚色和白天色之间插值 float dayNightFactor = smoothstep(-0.1, 0.2, sunAltitude); // 太阳在地平线以下时为夜晚 float4 baseSkyColor = lerp(_SkyColorNight, _SkyColorDay, dayNightFactor); // 2. 添加黄昏色调:当太阳高度在特定范围时,混合日落色 float sunsetFactor = smoothstep(0.15, 0.25, sunAltitude) * (1.0 - smoothstep(0.25, 0.35, sunAltitude)); baseSkyColor = lerp(baseSkyColor, _SkyColorSunset, sunsetFactor * 0.7); // 0.7是黄昏色强度 // 3. 地平线增强:观察方向越接近水平(viewDir.y接近0),颜色越接近_HorizonColor float horizon = 1.0 - saturate(abs(viewDir.y) * _HorizonSharpness); // saturate将值限制在0-1 float4 horizonColor = lerp(baseSkyColor, _HorizonColor, horizon); return horizonColor; }

3.5 绘制太阳与星星

太阳被绘制成一个高光圆盘,星星则通过立方体贴图在夜晚淡入。

// 绘制太阳 float4 DrawSun(float3 viewDir, float3 sunDir, float sunAltitude) { // 只有当太阳在地平线以上时才绘制 if (sunAltitude > 0.01) { // 计算观察方向与太阳方向的夹角余弦 float sunDot = dot(normalize(viewDir), sunDir); // 如果夹角很小,说明视线正对太阳 float sunMask = smoothstep(1.0 - _SunSize, 1.0, sunDot); return _SunColor * sunMask * _SunIntensity * sunAltitude; // 太阳高度也影响亮度 } return float4(0,0,0,0); } // 绘制星星 float4 DrawStars(float3 viewDir, float sunAltitude) { // 仅在夜晚显示星星,且随太阳高度降低而增强 float nightFactor = 1.0 - smoothstep(-0.05, 0.05, sunAltitude); if (nightFactor > 0.01) { // 可以添加简单的闪烁效果 float twinkle = sin(_Time.y * _StarsTwinkleSpeed + viewDir.x * 100) * 0.5 + 0.5; float4 stars = texCUBE(_StarsTex, viewDir); return stars * _StarsIntensity * nightFactor * twinkle; } return float4(0,0,0,0); }

3.6 片元着色器主函数

最后,将所有部分组合起来。

fixed4 frag (v2f i) : SV_Target { float3 viewDir = normalize(i.viewDir); // 1. 计算太阳数据 float sunAltitude = GetSunAltitude(_TimeOfDay); float3 sunDir = CalculateSunDirection(_TimeOfDay, sunAltitude); // 2. 计算基础天空色 float4 skyColor = CalculateSkyColor(viewDir, sunDir, sunAltitude); // 3. 叠加太阳 float4 sunColor = DrawSun(viewDir, sunDir, sunAltitude); skyColor.rgb += sunColor.rgb; // 4. 叠加星星(采用加法混合,因为星星是亮斑) float4 starsColor = DrawStars(viewDir, sunAltitude); skyColor.rgb += starsColor.rgb; // 5. 简单的基于高度的雾效模拟(可选) float heightFog = saturate(viewDir.y * 2.0); // 越往下(y值小),雾感越强 skyColor.rgb = lerp(skyColor.rgb * 0.8, skyColor.rgb, heightFog); // 地平线附近变暗模拟雾 return skyColor; }

至此,一个核心的、支持平滑日夜变化的天空盒 Shader 就完成了。你可以将其材质赋给Window -> Rendering -> Lighting -> Environment中的 Skybox Material 槽位。

4. 与场景光照联动:驱动全局光照

仅有天空盒变化是不够的,场景中的物体必须受到相应光照的影响。我们通过一个 C# 脚本来桥接 Shader 的计算结果和 Unity 的场景光照。

创建一个名为DayNightCycleController的脚本。

using UnityEngine; public class DayNightCycleController : MonoBehaviour { public Material skyboxMaterial; // 引用我们写的天空盒材质 public Light directionalLight; // 场景中的方向光(模拟太阳/月亮) [Header(“Time Settings”)] public float timeOfDay = 0.25f; // 0-1, 0为午夜,0.25为日出,0.5为正午,0.75为日落 public float cycleDurationInSeconds = 60f; // 一个完整周期对应的真实秒数 public bool isAutoCycle = true; [Header(“Lighting Settings”)] public Color daylightColor = Color.white; public Color sunsetColor = new Color(1f, 0.6f, 0.4f); public Color nightlightColor = new Color(0.4f, 0.5f, 1f); public float maxLightIntensity = 1.2f; public float minLightIntensity = 0.1f; private float sunIntensity; private Color sunColor; void Start() { if (skyboxMaterial == null) skyboxMaterial = RenderSettings.skybox; if (directionalLight == null) directionalLight = GameObject.FindObjectOfType<Light>(); // 建议手动赋值 } void Update() { // 1. 更新时间 if (isAutoCycle) { timeOfDay += Time.deltaTime / cycleDurationInSeconds; timeOfDay %= 1.0f; // 保持在0-1循环 } // 2. 将时间传递给Shader skyboxMaterial.SetFloat(“_TimeOfDay”, timeOfDay); // 3. 根据时间计算太阳高度(这里复制了Shader中的简单逻辑,确保一致) float sunAltitude = Mathf.Sin((timeOfDay - 0.25f) * Mathf.PI * 2) * 0.5f + 0.5f; // 4. 更新方向光(太阳/月亮) UpdateDirectionalLight(sunAltitude); // 5. 更新全局环境光(可选,更复杂的效果需要烘焙或实时GI) UpdateAmbientLight(sunAltitude); } void UpdateDirectionalLight(float sunAltitude) { if (directionalLight == null) return; // 计算光强度:太阳越高越亮 float lightIntensity = Mathf.Lerp(minLightIntensity, maxLightIntensity, sunAltitude); // 在日出日落时,强度曲线可以加一个平滑过渡 float dawnDuskFactor = Mathf.SmoothStep(0, 1, sunAltitude / 0.1f) * Mathf.SmoothStep(1, 0, (sunAltitude - 0.9f) / 0.1f); lightIntensity *= dawnDuskFactor; directionalLight.intensity = lightIntensity; // 计算光颜色:根据高度混合白天、黄昏、夜晚的颜色 if (sunAltitude > 0.5f) sunColor = daylightColor; else if (sunAltitude > 0.25f) sunColor = Color.Lerp(sunsetColor, daylightColor, (sunAltitude - 0.25f) / 0.25f); else if (sunAltitude > 0.1f) sunColor = Color.Lerp(nightlightColor, sunsetColor, (sunAltitude - 0.1f) / 0.15f); else sunColor = nightlightColor; directionalLight.color = sunColor; // 计算并设置光的方向(与Shader中计算保持一致) float azimuth = timeOfDay * Mathf.PI * 2; float radAltitude = sunAltitude * Mathf.PI * 0.5f; Vector3 dir = new Vector3( Mathf.Sin(azimuth) * Mathf.Cos(radAltitude), Mathf.Sin(radAltitude), Mathf.Cos(azimuth) * Mathf.Cos(radAltitude) ); directionalLight.transform.rotation = Quaternion.LookRotation(-dir, Vector3.up); // 方向光照射方向是transform.forward的负方向 } void UpdateAmbientLight(float sunAltitude) { // 简单示例:根据时间调整环境光的亮度和颜色 Color ambientColor = Color.Lerp(nightlightColor * 0.2f, daylightColor * 0.6f, sunAltitude); RenderSettings.ambientLight = ambientColor; // 对于更复杂项目,可能需要更新环境探针或使用Unity的GI系统 } // 提供一个方法供其他脚本(如UI滑块)修改时间 public void SetTimeOfDay(float newTime) { timeOfDay = Mathf.Clamp01(newTime); isAutoCycle = false; // 手动设置时,通常暂停自动循环 } }

将这个脚本挂载到场景中的一个空物体上,并拖拽赋值天空盒材质和方向光。运行后,你将看到天空盒与场景光照同步变化,物体投射的阴影方向和颜色也会随之改变,从而获得完整的日夜循环体验。

5. 性能优化与效果增强技巧

一个基础的日夜系统完成后,接下来要考虑的是性能和效果的提升。

5.1 Shader 性能优化点

  1. 减少纹理采样:我们的星星贴图是 Cubemap,采样开销相对较大。可以通过在片元着色器早期判断nightFactor是否接近于零,如果是则直接跳过texCUBE采样。
  2. 简化数学运算sin,cos,pow等函数在 Shader 中开销较大。可以预先在 C# 脚本中计算好一些关键参数(如sunDir,sunAltitude),通过Material.SetVector等传递给 Shader,减少片元着色器的计算量。
  3. 使用 LOD:为天空盒材质设置不同的 LOD 级别,在远处或移动设备上使用简化版本的 Shader(例如去掉星星的闪烁效果、简化颜色渐变计算)。
  4. 避免分支语句:GPU 不擅长处理if-else分支。尽管我们示例中用了,但在性能敏感处,可以尝试用step()smoothstep()函数来模拟条件判断。

5.2 视觉效果增强方案

  1. 大气散射模拟:实现更逼真的天空颜色,需要考虑瑞利散射(Rayleigh scattering,导致蓝天和红日落)和米氏散射(Mie scattering,导致光晕和雾蒙蒙的效果)。这需要更复杂的数学模型,如将太阳方向、观察方向、大气厚度等参数纳入计算。
  2. 云层系统:静态的云层贴图会穿帮。可以尝试:
    • 体积云:使用 3D 噪声纹理在片元着色器中实时生成,效果最好但性能要求高。
    • 平移多层 2D 云图:使用两张或多张云层贴图,以不同速度和方向滚动,配合日夜颜色着色,性价比很高。
  3. 动态天气系统扩展:以日夜系统为基础,可以引入“湿度”、“云量”等参数。通过 Shader 的lerp函数,在晴天、多云、雨天的天空颜色、雾效、光照强度之间平滑过渡。
  4. 后处理集成:在后期处理栈中,可以根据当前时间调整色调(白平衡)、对比度、 Bloom 阈值。例如,夜晚可以整体偏蓝、对比度降低,并增强光源的 Bloom 效果。

5.3 美术资源准备建议

  • 星星 Cubemap:可以在 3D 软件中生成,或使用专门的星空生成工具。确保是 HDR 格式,以获得更好的亮度范围。
  • 云层纹理:使用无缝平铺的、带有 Alpha 通道的噪声纹理或真实的云照片。建议准备多张不同尺度的纹理,混合使用以增加细节。
  • 颜色参数调校:不要只靠想象设置颜色。多参考现实世界的照片或优秀的游戏作品(如《荒野大镖客 2》、《塞尔达传说:旷野之息》),使用取色工具获取关键时间点的天空 RGB 值,作为你 Shader 中颜色属性的初始值。

6. 常见问题与调试实录

在实际实现过程中,你几乎一定会遇到下面这些问题。这里记录了我的排查过程和解决方案。

6.1 天空盒接缝或扭曲

问题描述:在天空盒的某些边缘,颜色出现不连续的接缝,或者太阳/星星在靠近边缘时被拉伸扭曲。

原因与解决

  1. 使用错误类型的天空盒:Unity 支持 6 面体、全景图和程序化天空盒。我们的 Shader 是程序化的,应确保在 Lighting 窗口中选择的是材质(Material)模式,并正确赋值。如果错误地选用了 6 面体模式,就会导致接缝。
  2. 观察方向计算错误:在顶点着色器中,我们计算的是从相机到顶点的向量。如果天空盒模型的缩放不是均匀的(1,1,1),或者原点不在相机位置,就会导致viewDir不正确,从而引起扭曲。确保用于渲染天空盒的 Mesh(通常是球体或巨大立方体)缩放为 1,且其位置与相机无关(我们通过_WorldSpaceCameraPos做了补偿,这是正确的)。
  3. Cubemap 采样问题:如果星星使用 Cubemap,确保贴图导入设置中,“Wrap Mode”设置为“Clamp”,而不是“Repeat”,可以避免边缘采样错误。

6.2 太阳/月亮位置与阴影方向不匹配

问题描述:天空中的太阳位置和场景中方向光(主光源)投射的阴影方向不一致,严重出戏。

原因与解决: 这是脚本与 Shader 计算不同步的典型问题。

  • 确保公式一致DayNightCycleController脚本中计算sunDir的数学公式,必须与 Shader 中CalculateSunDirection函数的逻辑完全一致。任何细微差别(如弧度与角度的转换、坐标系朝向)都会导致偏差。建议将计算函数单独写在一个工具类中,供脚本和 Shader(通过预计算参数传递)共同调用。
  • 检查坐标系:Unity 的世界空间是 Y 轴向上。确保你的方向计算也遵循(x, y, z)对应(东, 上, 北)或你定义的约定,并在脚本设置光源旋转时注意Quaternion.LookRotation(-dir, Vector3.up)的用法(光源照射方向是其 transform.forward 的负方向)。

6.3 日夜过渡生硬,颜色跳变

问题描述:从白天切换到夜晚时,颜色不是平滑渐变,而是在某个时间点突然“跳”了一下。

原因与解决: 问题出在颜色插值的逻辑上。我们使用了lerpsmoothstep

  • 检查插值因子:确保驱动插值的关键参数(如sunAltitude)本身是连续平滑变化的。打印出这个值随时间的变化曲线,看是否有阶跃。
  • 使用smoothstep替代硬切换:避免使用if语句进行颜色选择。就像示例中计算sunsetFactor一样,使用smoothstep来创建平滑的过渡区间。smoothstep(a, b, x)x<a时返回 0,在x>b时返回 1,在中间时返回平滑的 Hermite 插值。
  • 引入时间平滑:在脚本中,可以对timeOfDay的变化率进行限制,或者对计算出的sunColorlightIntensity进行帧间插值(Lerp),避免一帧内的剧烈变化。

6.4 移动设备上性能不佳

问题描述:在手机或平板电脑上运行,帧率明显下降。

原因与解决: 天空盒 Shader 在每个像素上执行,分辨率越高,计算量越大。

  • 简化片元着色器:这是最有效的方法。移除或简化昂贵的操作:
    • 去掉星星的闪烁计算(sin(_Time.y * ...))。
    • 降低颜色渐变的复杂度,减少lerp次数。
    • 用纹理查找(LUT)替代部分实时计算。例如,预先烘焙一张从(sunAltitude, viewDir.y)skyColor的 2D 纹理,在 Shader 中只需一次纹理采样,代价是牺牲一些动态自由度。
  • 降低精度:将float改为halffixed(在支持的平台)。注意,方向、坐标等需要高精度的变量仍需用float
  • 检查后处理:有时不是天空盒本身的问题,而是 Bloom、体积光等后处理效果在移动端开销大。可以为移动平台提供简化的后处理配置。

6.5 构建后效果丢失或变黑

问题描述:在编辑器里运行正常,但打包成可执行文件后,天空盒全黑或显示异常。

原因与解决: 这是 Shader 变体或资源包含问题。

  • 检查 Shader 的编译与包含:确保你的自定义 Shader 被正确包含在构建中。在Project Settings -> Graphics -> Always Included Shaders列表中,添加你的Custom/SkyboxDynamicDayNightShader。或者,确保场景中使用了该材质的对象被正确引用和打包。
  • 检查 Cubemap 纹理设置:如果使用了星星 Cubemap,在其导入设置中,检查“Platform Settings”,确保为目标构建平台选择了正确的纹理压缩格式,并且“Alpha Source”等设置无误。有时需要将纹理的“Texture Type”明确设置为“Cubemap”。
  • 查看播放器日志:构建后运行,查看输出日志中是否有关于 Shader 编译错误或属性丢失的警告信息。Unity 在构建时会剥离未使用的 Shader 变体,如果脚本通过Material.SetFloat设置了一个 Shader 中不存在的属性,在编辑器下可能不报错,但构建后会导致问题。确保脚本设置的属性名与 Shader 中的变量名完全一致。

实现一个令人信服的日夜变化系统,是游戏视觉开发中一项极具成就感的工作。它不仅仅是技术的堆砌,更是对自然规律的观察和艺术化的表达。从最基础的太阳轨迹模拟,到天空颜色的多层混合,再到与场景光照的联动,每一步都需要仔细调试和审美判断。我个人的体会是,参数调校的时间往往比写代码的时间更长。不要满足于“能跑”,多花时间在不同时间点观察对比现实世界或参考作品,微调那些颜色、过渡曲线和强度参数,直到循环看起来自然而舒适。这个系统还可以作为基石,扩展出天气系统、季节变化,让你的虚拟世界真正“活”起来。