尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity动态天空与昼夜系统实现:从天空盒到程序化渲染

Unity动态天空与昼夜系统实现:从天空盒到程序化渲染 1. 项目概述从游戏到引擎的动态天空实现最近在玩《原神》的时候除了被它的开放世界和剧情吸引我总会不自觉地停下来看看天。游戏里的天空不是一张静止的贴图而是会随着时间流逝从朝霞满天到烈日当空再到暮色四合、星河璀璨整个过程流畅自然极大地增强了世界的沉浸感。这种动态的天空效果在Unity引擎里其实并不神秘其核心实现手段就是Skybox天空盒和Cubemap立方体贴图这两项技术的结合与动态控制。简单来说这个项目的目标就是在Unity中不依赖任何付费插件仅使用引擎内置功能复现一个类似《原神》中那样平滑、真实的动态天空与昼夜更替系统。它不仅仅是让天空的颜色渐变更要模拟太阳或月亮的轨迹移动、云层的缓慢飘动、以及不同时间点下环境光照如方向光颜色和强度的联动变化。这不仅是提升场景美术表现力的关键更是构建一个“活”的虚拟世界的基础。无论你是独立游戏开发者、技术美术还是对图形渲染感兴趣的学习者掌握这套方法都能让你对实时渲染有更深刻的理解。2. 核心原理与方案设计2.1 天空盒与立方体贴图世界的“全景画布”要理解动态天空必须先搞懂它的静态基础——天空盒。天空盒本质上是一个将玩家摄像机包裹在中心的巨大立方体。这个立方体的六个内表面分别贴上了六张纹理共同构成一个无缝的360度全景背景。因为它的尺寸远大于场景中的其他物体并且随着摄像机旋转而旋转通常只旋转不平移所以能营造出一种无限深远的天穹假象。而立方体贴图就是为这个立方体的六个面提供纹理数据的一种特殊纹理资源类型。在Unity中一个Cubemap资产包含了六张正方形图片分别对应X右、-X左、Y上、-Y下、Z前、-Z后六个方向。你可以把它想象成一个拆开平铺的盒子表面。传统的静态天空盒就是将一个绘制好的、固定的Cubemap比如一个晴朗的白天或一个星空直接赋给场景的Skybox Material。但我们要实现动态效果关键在于“动态”二字。这意味着我们不能只用一张Cubemap而是需要一个Cubemap序列或者通过程序实时混合、修改Cubemap的采样结果。2.2 动态天空的核心设计思路基于上述原理实现动态天空主要有两种主流思路我将对它们进行对比分析并说明本项目选择的方向。思路一基于多张Cubemap的插值混合这是最直观的方法。美术同学需要预先制作多个代表不同时间点的天空盒Cubemap例如黎明、正午、黄昏、午夜。在运行时我们根据游戏内时间一个从0到1循环的_Time变量计算当前时间介于哪两个关键Cubemap之间然后通过lerp线性插值函数对这两张Cubemap的采样颜色进行混合。优点效果质量上限高完全由美术资源决定可以做出风格化或写实度极高的天空。缺点资源消耗大需要制作多张高质量的Cubemap每张6个面混合阶段如果关键帧较少过渡可能不够平滑难以实现太阳/月亮等发光体的动态位置变化。思路二基于程序化噪声与数学模型的实时生成这种方法不依赖或仅依赖少量基础Cubemap主要通过在Shader中使用程序化噪声如Perlin Noise和复杂的数学函数来实时计算天空每个像素的颜色。太阳、月亮的位置可以通过球面坐标计算云层由多层滚动的噪声图叠加而成。优点极度灵活可以无缝循环参数可动态调整如云量、大气散射强度资源占用极小。缺点实现复杂度高Shader编写难度大要达到AAA级游戏的视觉质量需要极其精巧的算法和调参。本项目的折中与优选方案 为了在效果、性能和实现难度之间取得平衡我选择了一种混合方案使用一张基础的“中性”天空盒Cubemap作为底色结合一个强大的程序化太阳/月亮系统并动态控制天空颜色渐变、云层移动以及环境光照。底色采用一张包含基本蓝天、地平线辉光和远处云层细节的Cubemap。它不包含强烈的光源如正午的太阳只是一个“背景板”。动态光源在Shader中完全通过数学计算来模拟太阳和月亮这两个发光体。我们可以精确控制它们的仰角高度角和方位角模拟日出日落、月升月落。颜色与雾效根据动态光源的位置和强度实时计算并叠加天空的颜色梯度从头顶的天顶色到地平线附近的地平线色并影响场景的雾效颜色实现清晨的冷蓝色雾霭和黄昏的暖黄色暮光。云层在基础Cubemap的云层之上可以再叠加一层或两层使用简单UV偏移制作的2D云层纹理增加近处云朵的流动感。这个方案既保留了美术对整体色调和远景云层的控制力又通过程序化手段实现了最关键的动态元素光源运动、颜色变化灵活且高效。2.3 工具选型与资源准备在开始动手前我们需要准备好“武器”和“材料”。核心工具Shader Graph 与 URP/HDRPUnity的Shader Graph可视化着色器编辑工具是我们的首选。它允许我们通过连节点的方式构建Shader无需直接编写HLSL代码当然必要时可以插入自定义节点大大降低了图形编程的门槛。本项目将基于Universal Render Pipeline (URP)进行。URP是Unity官方推出的轻量级、可扩展的渲染管线对移动端和PC端都有良好支持且内置了对Skybox Shader Graph的良好兼容。如果你使用的是Built-in管线思路相通但部分节点和设置需要调整。关键资源准备一张高质量的Cubemap天空盒纹理你可以在Unity Asset Store搜索“Skybox Cubemap”有很多免费或付费资源。建议选择一张云层分布均匀、地平线过渡柔和、且没有明显太阳光斑的图片。也可以使用像“HDRi Haven”这样的网站下载免费的HDR环境贴图在Photoshop或在线工具中将其转换为Cubemap格式。云层纹理准备1-2张Tileable可平铺的云朵黑白噪波图或细节图。黑白图用于作为云层密度遮罩。创建Skybox Material在Project窗口中右键 - Create - Material将其Shader类型改为Shader Graph - Skybox。我们将在这个材质球上连接我们的Shader Graph。3. 构建动态天空着色器从节点到视觉接下来我们进入核心环节在Shader Graph中一步步搭建我们的动态天空。3.1 搭建基础框架与Cubemap采样首先创建一个新的Shader Graph并选择Skybox Shader模板。模板会默认提供天空盒所需的基本输出结构。引入时间变量添加一个Time节点获取游戏的运行时间。我们通常使用其Time输出端口它提供一个稳定增长的浮点数。为了便于控制昼夜循环速度我们将其乘以一个Cycle Speed浮点参数然后取正弦Sine或使用Fraction节点取小数部分得到一个在0-1之间循环的_DayTime变量。采样基础Cubemap添加一个Cubemap Asset节点并关联你准备好的那张基础天空盒纹理。然后使用Sample Cubemap节点对其进行采样。采样所需的向量Vector通常来自Skybox模板自带的Position节点即片元在世界空间中的方向向量。将采样结果作为天空颜色的基础。3.2 程序化太阳与月亮系统的实现这是动态天空的灵魂。我们将在Shader中“画”出太阳和月亮。计算光源方向太阳和月亮的位置可以用球面坐标来描述。我们需要两个参数_SunElevation太阳高度角0度在地平线90度在天顶和_SunAzimuth太阳方位角0-360度。我们可以根据_DayTime来驱动这两个参数。例如设定_SunElevation sin(_DayTime * PI) * 80让太阳在-80到80度之间运动模拟东升西落。 根据这两个角度可以计算出一个标准化的世界空间方向向量sunDirection。// 伪代码逻辑在Shader Graph中用数学节点实现 float radElevation _SunElevation * PI / 180; float radAzimuth _SunAzimuth * PI / 180; float3 sunDirection; sunDirection.y sin(radElevation); float horizonDistance cos(radElevation); sunDirection.x horizonDistance * cos(radAzimuth); sunDirection.z horizonDistance * sin(radAzimuth);绘制太阳圆盘如何判断当前像素是否在太阳上我们计算当前像素方向向量viewDir与sunDirection的点积Dot Product。如果点积结果大于一个非常接近1的阈值例如0.9995我们就认为这个像素在太阳的视圆盘内。float sunDot dot(normalize(viewDir), sunDirection); float sunMask step(0.9995, sunDot); // 大于阈值则为1否则为0将sunMask与一个代表太阳颜色的参数如亮黄色相乘叠加到最终输出颜色上。为了边缘柔和可以将step换成smoothstep。添加太阳光晕在太阳圆盘周围通常有光晕。我们可以利用sunDot当它的值在稍大的一个范围内如0.99到0.9995时用一个渐变函数计算出光晕的强度并叠加一个更弥散的颜色。月亮系统月亮的逻辑与太阳完全一致只是它的方向向量moonDirection可以根据_DayTime偏移半个周期来计算颜色设为冷白色亮度低于太阳。需要注意处理太阳和月亮同时可见黄昏/黎明的情况。3.3 动态天空颜色与雾效的联动天空的颜色绝不是一成不变的。我们需要根据太阳的高度角来动态混合天顶色和地平线色。计算高度因子使用像素方向向量的Y分量viewDir.y经过简单的重映射Remap得到一个从地平线Y值小到天顶Y值大的渐变因子heightFactor。时间驱动的颜色梯度创建两套颜色参数_DaytimeZenithColor,_DaytimeHorizonColor和_NightZenithColor,_NightHorizonColor。根据_DayTime或太阳高度角对这两套颜色进行插值得到当前时刻的目标天顶色和地平线色。float3 currentZenith lerp(_NightZenithColor, _DaytimeZenithColor, sunLightFactor); float3 currentHorizon lerp(_NightHorizonColor, _DaytimeHorizonColor, sunLightFactor);这里的sunLightFactor可以是(_SunElevation 10) / 100的饱和值用来表示白天的强度。混合颜色使用heightFactor对currentHorizon和currentZenith进行插值得到基于像素高度的基础天空色。然后将这个颜色与之前采样的Cubemap基础色、太阳/月亮颜色进行混合通常是相加混合。驱动雾效与环境光这是让整个场景“活”起来的关键。在Shader中我们可以将计算出的currentHorizon它最能反映时间氛围输出到一个Color类型的Shader全局属性例如_GlobalSkyColor。然后在Unity的渲染设置或一个单独的全局控制脚本中读取这个颜色并动态赋值给RenderSettings.fogColor雾效颜色和RenderSettings.ambientLight环境光。这样当天空变成橘红色时你的整个场景也会沐浴在夕阳的余晖中。3.4 流动的云层细节为了让近处的云层有动感我们需要让云层纹理动起来。云层纹理采样使用Sample Texture 2D节点采样你准备的云层噪波图。将纹理的UV坐标与时间变量和风速参数结合实现滚动。float2 cloudUV viewDir.xz / (viewDir.y 0.5); // 一个简单的平面投影确保云在地平线附近 cloudUV _Time.y * _WindSpeed * float2(1.0, 0.3); // 随时间偏移模拟风 float cloudDensity tex2D(_CloudTex, cloudUV).r;多层混合可以使用两张不同尺度、不同滚动速度的云图进行叠加创造出更丰富的细节。用Multiply或Screen混合模式将它们叠加在一起。高度衰减云层不应该在整个天空均匀分布。通常越靠近天顶越稀薄。我们可以用viewDir.y来调制云层的透明度让地平线附近的云更浓天顶附近的云更淡。最终合成将计算好的云层颜色通常是白色或灰白色与透明度以Lerp或Add的方式叠加到已有的天空颜色上。注意控制叠加强度避免云层过于突兀。4. 在Unity场景中集成与调试Shader写好了接下来就是把它用起来并让整个系统运转起来。4.1 材质与场景配置应用材质将编写好的Shader Graph保存并生成一个材质球。在Unity顶部菜单栏中选择Window - Rendering - Lighting打开光照设置窗口。在Environment标签页下将Skybox Material拖拽为你刚创建的动态天空材质。配置场景光照创建一个Directional Light方向光作为太阳。我们需要编写一个简单的C#脚本DynamicSunController挂在这个灯光上。using UnityEngine; public class DynamicSunController : MonoBehaviour { public Material skyboxMaterial; // 动态天空材质 public Light sunLight; // 方向光组件 public float dayDurationInSeconds 600f; // 一天的游戏时长 private float _currentTime 0.5f; // 0-10为午夜0.5为正午 void Update() { // 更新时间 _currentTime Time.deltaTime / dayDurationInSeconds; _currentTime % 1.0f; // 计算太阳高度角假设正弦运动-90到90度 float sunElevation Mathf.Sin(_currentTime * Mathf.PI * 2) * 90f; // 计算太阳方位角简单的线性变化 float sunAzimuth _currentTime * 360f; // 将角度传递给Shader skyboxMaterial.SetFloat(_SunElevation, sunElevation); skyboxMaterial.SetFloat(_SunAzimuth, sunAzimuth); // 根据太阳高度角更新方向光的位置、强度和颜色 // 1. 计算方向向量与Shader中逻辑一致 Vector3 sunDirection CalculateSunDirection(sunElevation, sunAzimuth); sunLight.transform.rotation Quaternion.LookRotation(-sunDirection); // 光的方向与太阳方向相反 // 2. 更新光强和颜色 float lightIntensity Mathf.Clamp01((sunElevation 10) / 20); // 日出日落时减弱 sunLight.intensity lightIntensity; sunLight.color Color.Lerp(Color.white, new Color(1f, 0.6f, 0.4f), 1 - lightIntensity); // 日落时偏红 // 3. 将计算出的天空主色传递给环境光或雾效可选也可在Shader中设置全局属性 Color skyColor skyboxMaterial.GetColor(_GlobalSkyColor); RenderSettings.fogColor Color.Lerp(RenderSettings.fogColor, skyColor, Time.deltaTime); } Vector3 CalculateSunDirection(float elevation, float azimuth) { /* 实现同上文Shader逻辑 */ } }这个脚本是连接游戏逻辑时间与渲染表现Shader参数、灯光的桥梁。4.2 参数调节与视觉打磨现在运行游戏你应该能看到天空和太阳在运动了。但很可能看起来“很假”。接下来的调参才是真正出效果的关键。太阳/月亮大小与光晕调整判断太阳圆盘的阈值和光晕的渐变范围。现实中太阳的视直径很小在Shader中可能需要比0.9995更大的阈值如0.9999才能看起来不那么大。光晕的范围和衰减曲线需要反复调试以达到柔和自然的效果。颜色曲线不要简单地在白天色和夜晚色之间线性插值。日出前半小时的“鱼肚白”、日落后的“蓝色时刻”Blue Hour都有独特的色彩。你可以通过一个AnimationCurve类型的Shader属性或脚本变量来控制颜色插值的权重制造出非线性的、更有艺术感的色彩变化。云层运动与密度调整云层纹理的平铺Tiling值避免重复感过于明显。风速参数不宜过大应是缓慢的漂移。通过曲线控制云层密度随太阳高度的变化例如正午云层更显白亮夜晚云层几乎不可见。性能考量在Shader Graph中检查节点的复杂度。Sample Cubemap和复杂的噪声计算是性能消耗的大头。确保移动端项目中使用经过压缩的Cubemap纹理并考虑在低端设备上关闭或简化云层效果。可以使用Shader LOD细节级别或不同的Quality Setting来配置不同质量等级的天空盒Shader。5. 常见问题、优化与扩展思路在实际操作中你肯定会遇到一些棘手的情况。这里记录了我踩过的一些坑和解决方案。5.1 常见问题排查速查表问题现象可能原因排查与解决思路天空盒有接缝或扭曲Cubemap纹理六张图接缝处对不齐或采样向量未归一化。确保使用的Cubemap资源本身是无缝的。在Shader中对用于采样Cubemap的方向向量使用Normalize节点。太阳/月亮位置不对或跳动方向向量计算错误或传递给Shader的参数在Update中波动。在脚本中打印计算的sunDirection向量检查其值是否平滑变化。确保所有角度到弧度的转换正确。天空颜色在特定角度突变天顶色与地平线色的插值因子高度因子计算不连续。检查用于计算heightFactor的viewDir.y的重映射函数确保其输出在[0,1]范围内平滑变化。尝试使用smoothstep代替lerp进行插值。云层纹理拉伸严重使用的UV投影方式不适合当前视角。简单的平面投影viewDir.xz / viewDir.y在看向天顶时viewDir.y接近1UV会很小看向地平线时viewDir.y接近0UV会极大导致拉伸。可以改用球面投影或使用更复杂的映射或者接受这种特性作为风格化表现。移动端帧率下降明显Shader过于复杂特别是云层使用了多次纹理采样或复杂噪声。简化Shader减少纹理采样次数将云层从Shader中移至一个简单的、面向摄像机的粒子系统或面片降低Cubemap分辨率。使用Unity的Frame Debugger或GPU Profiler定位瓶颈。雾效/环境光更新延迟在Update中直接设置RenderSettings但帧率波动导致变化不跟手。将颜色更新放在LateUpdate中。或者更好的做法是在Shader中直接将颜色写入一个全局Shader属性Shader.SetGlobalColor让雾效Shader自己去读取保证同步。5.2 进阶优化技巧预计算与烘焙对于完全由程序化生成的、且参数固定的复杂天空可以考虑将一天中不同时刻的天空状态预计算并烘焙成一组Cubemap或一张纹理阵列Texture2DArray运行时根据时间进行采样混合。这能极大降低运行时Shader计算量适合移动端或风格化项目。使用物理大气散射模型追求极致写实可以研究并实现类似Bruneton等人的预计算大气散射模型。这类实现非常复杂但效果惊人能模拟出瑞利散射Rayleigh导致蓝天和红霞和米氏散射Mie导致光晕和雾蒙蒙的效果。社区已有一些开源项目可供参考。与天气系统结合将天空系统参数化。例如增加一个“云量”参数控制云层密度和覆盖范围增加一个“湿度”参数影响雾效浓度和天空饱和度。将这些参数暴露给游戏逻辑就可以轻松实现晴、阴、雨、雾等不同天气状态的切换和过渡。5.3 从动态天空到完整时间系统一个逼真的动态天空绝不是一个孤立的视觉效果。它应该是一个完整的时间与环境系统的视觉出口。时间管理建立一个全局的GameTimeManager单例统一管理游戏内时间包括日、时、分、秒并发出“日出”、“正午”、“日落”、“午夜”等事件。DynamicSunController和任何其他依赖于时间的系统如NPC作息、植物生长都监听这个管理器。间接光照更新在URP/HDRP中天空盒直接影响场景的反射探针Reflection Probe和光照探针Light Probe。当天空剧烈变化时需要安排反射探针的定时烘焙或实时更新以确保物体表面的反射内容与天空匹配。后处理联动调整后处理堆栈Post-processing Stack中的参数。例如在夜晚自动增加一点胶片颗粒Grain和暗角Vignette在正午适当提高对比度和饱和度。可以通过DayTime参数来驱动这些后处理效果的强度曲线。实现一个像《原神》那样令人信服的动态天空是一项融合了美术审美、数学原理和编程技巧的工作。它没有唯一的正确答案本文提供的混合方案是一个坚实的起点。最关键的是理解各个环节是如何串联起来的时间驱动参数参数改变Shader计算Shader输出颜色和全局属性全局属性再反过来影响场景中的灯光、雾效和后处理。当你打通了这个闭环并愿意花时间去耐心调试每一个颜色、每一个参数曲线时你创造的世界也就真正拥有了呼吸和生命。
返回列表