从零开始搞Unity天空球:渲染原理、内置工作流与手写Shader笔记
但凡做过一段时间Unity,你就迟早会碰天空球这个东西。不管是做个室外场景、搭个数字孪生的城市底座,还是要给微信小游戏项目省Draw Call,天空球都是跨不过去的一环。我最早对它不屑一顾,觉得不就是个背景图嘛,直到被一个甲方要求在动态天气系统里做场景日夜交替,才发现这玩意儿的坑比想象中多得多。今天我就把从原理到实操的经验完整梳理一遍,包括怎么用内置Skybox做环境映射、怎么用Shader Graph和手写代码控制昼夜变化,以及我在项目里踩过的那些天花板级的坑。
先说结论:Unity天空球不是一张贴图那么简单,它是整个场景环境光照的来源,是反射探针和实时阴影的基准,也是决定画面氛围的第一块拼图。如果只是把天空当背景,后续做PBR材质、做Post Processing、做urtp光照模式,你会处处受制于人。所以这篇博文,从打包代码层面的实现细节,到美术层面的效果调优,适配从新手到老手的阅读需求。
1. 天空球到底做了什么:不只是画个背景
1.1 什么是天空球,Unity为什么用它
天空球这个概念来自传统3D图形学:用一个包围整个场景的球体网格,把环境贴图渲染到球体内表面,摄像机位于球心,不管视角转到哪里,看到的都是天空。Unity的做法也沿用了这个思路,只不过默认情况下它不是一个场景里的真实物体,而是通过渲染管线特殊处理的。
这里有一个新手常见的误解:以为天空球是个普通的Mesh Renderer,需要自己拉一个Sphere模型进来。实际上,Unity管这个叫Skybox,是渲染设置中独立的一层,需要绑定材质,相机的Clear Flags(或者URP里的背景类型)设置为Skybox才能渲染出来。
Unity内置默认的天空盒材质叫Default-Skybox,它本质上是一个程序化天空盒生成器,可以通过参数控制太阳位置、大气散射强度、地面颜色等,不需要任何贴图。如果你做了HDRP项目,它的Volume组件里还有更高级的Physically Based Sky,那个是带物理模型的光照模拟,性能开销大很多,但效果确实炸裂。
1.2 天空球承担的双重职责:背景渲染与全局光照
很多人只把天空盒当作视觉背景,实际上它同时承担环境光照的来源。Shader里采样环境反射时,天空盒就是那个被采样的Cubemap,你的金属材质、光滑地面、汽车漆面,之所以有环境反射效果,全靠天空盒在撑腰。
如果场景里没有额外布反射探针(Reflection Probe),Unity默认就用天空盒做反射源。哪怕你场景里放了几面镜子、放了个金属质感很强的雕塑,它们反射出来的内容其实仍是天空盒的内容,而不是周围物体。这是一个很容易被忽略的性能与效果权衡点。
另外,法线贴图、环境遮蔽等效果也会间接受到天空盒颜色明暗的影响。比如,天空盒很亮时,场景整体会显得曝光值偏大,这时如果不配合后期曝光参数调整,材质颜色会失真。理性看待天空球,它不是背景布,是整个场景的光环境基础。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 内置天空球功能的工作流:参数、遮罩与后处理
2.1 在Built-in与URP里分别怎么设置天空盒
先说明白,Unity分为内置渲染管线(Built-in Render Pipeline)和SRP(Scriptable Render Pipeline),后者又分为URP和HDRP。这三种管线下,天空球的设置入口不一样,坑也不一样。
在Built-in管线中,进入Window > Rendering > Lighting Settings,找到Environment区域,Skybox Material指定材质即可。这里还要同步设置Sun Source,也就是把太阳光Directional Light拖进去,这样程序化天空盒会用这盏灯的方向计算太阳位置和光晕方向。
在URP管线中,用法要分两种。如果只是静态设置,去Window > Rendering > Lighting - Environment里绑定天空盒材质。如果想做动态环境(比如昼夜变化、天气变化),更推荐用Volume组件里的Skybox Override:场景里放一个Global Volume,把Skybox勾选并覆盖材质参数,脚本控制这些参数的插值变化,就能实现平滑过渡。
HDRP则完全不一样,它直接在Volume里提供Procedural Sky和Physically Based Sky,HDRP的天空盒效果与光照、雾效深度耦合,参数特别多。打开HDRP项目先看默认Volume里的Sky类型设置,否则你会发现白天也能看着像阴天,因为曝光、雾浓度、云层高度等都需要联动调整。
2.2 相机Clear Flags与天空盒的配合:别让背景穿帮
很多人设置好天空盒,却发现相机里看不到天空,或者看到的是纯色背景。这通常是因为相机的Clear Flags设置错了。在Built-in管线里,Camera组件上的Clear Flags选项有三个:Skybox、Solid Color、Depth Only。
选Skybox时,相机每帧先清屏为天空盒内容,再渲染其他物体。选Solid Color时,会用纯色刷掉屏幕,天空盒当然不会显示。选Depth Only时,深度缓冲被清掉而颜色缓冲不清,适合多相机叠层。
一个重要细节:如果你的天空盒材质是程序化的,相机Clear Flags是Skybox,但天空盒里亮度为0(例如夜晚参数),屏幕会看起来是纯黑的。这不代表出bug了,是天空盒本身的颜色太暗。这时候就要检查后处理的曝光设置,以免误判。
2.3 定制天空盒材质参数:从清晨到夜晚的调整思路
用官方Default-Skybox调试时,最核心的参数就四个:Sun Size(太阳圆盘大小)、Sun Size Convergence(太阳边缘的模糊锐利度)、Atmosphere Thickness(大气层的视觉厚度,影响天顶到地平线的色彩差异)、Exposure(整体曝光)。
如果要做场景的氛围切换,我建议这样调:
清晨时段,Atmosphere Thickness拉到0.7-0.9,Sun Size保持0.03左右,Exposure给0.9,太阳光暖色调;到正午,Exposure给1.2,大气厚度控制在0.55-0.65,Sun Size稍微收小到0.02;傍晚想呈现橙红晚霞感,把Atmosphere Thickness加回0.85以上,同时把Exposure压到0.7-0.8,通过后期加暖色;夜晚,把Skybox材质换掉,用Cubemap贴图或程序化星空,Exposure直接给0.1以下。
这样一轮下来你就能感受到,同样一个天空盒材质,只改参数不动Shader,已经能撑起一个昼夜系统的雏形。
3. 手写天空球Shader:从原理到完整实现
3.1 为什么还要手写Shader,内置版不够用吗
内置天空盒适用于大多数静态和简单动态场景。但有一个关键短板:它不能很好地表达云层、星空、渐变贴图上的细节分层。尤其是做数字孪生项目时,甲方动不动就要求天空要有体积云,白天的云要有厚度,夜晚要有星空旋转和星座线条,这些内置天空盒的参数完全支撑不起来。
我个人的习惯是:基础场景用内置天空盒,定制化天气或写实艺术风格场景就手写Shader,或者用Shader Graph搭一个可调参数的天空盒。手写Shader虽然学习成本高,但它给了性能、内存、参数可控性的最优解。
3.2 Shader基础概念速补:渲染队列、Cull Front与ZTest
在写天空球Shader之前,有几个基础概念先厘清。天空球的核心是:一个放在场景中心的球体,它的面法线朝向相机,且它在所有不透明物体之前渲染。这需要几个关键控制参数。
第一,渲染队列要设为Background(值为1000),这样天空球会在所有不透明物体之前被绘制,处于整个场景渲染的最开始阶段。第二,剔除模式要设为Cull Front,因为我们要渲染的是球体内表面,相机的视线落在球体内部,只有内表面朝向观察者,所以需要剔除正面、渲染背面。第三,ZWrite要设为Off,因为天空球是场景的背景层,不应该写入深度缓冲,否则后面物体的深度对比会错乱。
下面是一种典型的不使用贴图的程序化天空Shader的核心逻辑。代码结构上分两层:第一层是处理日照方向的渐变球体,第二层是叠加一个圆盘表示太阳轮廓。
3.3 程序化渐变天空Shader核心代码
这里给出一个可用的程序化天空Shader,主要实现天顶到地平线的两个颜色插值,以及太阳圆盘的高亮模拟。
shaderlab复制Shader "Custom/ProceduralGradientSky" {
Properties {
_TopColor ("Top Color", Color) = (0.2, 0.5, 1.0, 1.0)
_HorizonColor ("Horizon Color", Color) = (0.8, 0.9, 1.0, 1.0)
_BottomColor ("Bottom Color", Color) = (0.4, 0.3, 0.2, 1.0)
_SunDir ("Sun Direction", Vector) = (0.5, 0.8, 0.2, 0.0)
_SunColor ("Sun Color", Color) = (1.0, 0.95, 0.8, 1.0)
_SunSize ("Sun Size", Range(0.001, 0.1)) = 0.02
_Exposure ("Exposure", Float) = 1.0
}
SubShader {
Tags { "Queue"="Background" "RenderType"="Background" "IgnoreProjector"="True" }
Cull Front
ZWrite Off
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldDir : TEXCOORD0;
};
float3 _TopColor;
float3 _HorizonColor;
float3 _BottomColor;
float3 _SunDir;
float3 _SunColor;
float _SunSize;
float _Exposure;
v2f vert (appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// 用物体坐标的法线方向作为观察方向,天空球位于世界原点时可以这样简化
o.worldDir = normalize(v.normal);
return o;
}
float4 frag (v2f i) : SV_Target {
float h = i.worldDir.y * 0.5 + 0.5; // 把y轴映射到0-1区间
// 上方和地平线之间插值,下方和地平线之间插值
float3 col = h > 0.5
? lerp(_HorizonColor, _TopColor, (h - 0.5) * 2.0)
: lerp(_BottomColor, _HorizonColor, h * 2.0);
// 加入太阳光晕点
float sunDot = saturate(dot(normalize(i.worldDir), normalize(_SunDir)));
float sunDisk = step(1.0 - _SunSize, sunDot);
col += _SunColor * sunDisk * _Exposure;
col *= _Exposure;
return float4(col, 1.0);
}
ENDCG
}
}
}
这段代码的关键点在于,我们不需要任何UV坐标,直接用球体顶点的法线方向作为方向向量,去计算该方向应该显示的天空颜色。这是因为天空球是纯程序化的,它的颜色只取决于观察方向,跟表面纹理无关。
实际项目里如果把天空球放在非世界原点,直接用normal当作方向向量就会出现偏差。稳妥做法是把顶点的物体坐标转换到世界空间,再减掉天空球的世界坐标得到方向向量,这样即使场景远处有其他环境物体,天空球也能正确渲染。
3.4 加入星空与动态云层的扩展思路
有了基础渐变Shader,扩展现有功能就非常方便。加星空理论上可以在frag中叠加一个噪声函数,用世界方向作为采样坐标,对星空小球位置打点。这里有一个技术要点:直接用世界坐标采样,相机会因视角不同产生明显的扭曲和拉伸,要使用世界方向归一化后再映射到常说的UV球体坐标去采样噪声,才能保证星点看起来稳定。
云层就更麻烦了。如果想要动态飘动感的体积云,常规做法是做一张平铺噪声图,利用世界方向和时间节点做两个图层的偏移混合,模拟云的流动。考虑到性能,在移动端或者微信小游戏运行时,建议用两张低分辨率噪声叠加即可,别用全屏实时噪声计算,手机GPU扛不住。
我帮人调微信小游戏项目时,就是把天空Shader的云层做了变体宏控制,微信平台用简易的LayerBlend噪声云,PC端跑完整版的三层Perlin,一个Colorbuffer画布上要稳住30帧,性能开销完全不同。
4. 天空球与场景光照、Post Processing的联动玩法
4.1 反射探针与天空盒的联动:让金属材质有真实环境反射
天空盒同时也是反射环境。在Unity里,如果你没有放Reflection Probe,默认反射源就是天空盒。Unity在启运时会自动生成一张天空盒的Cubemap存下来。
这就引出一个常见问题:我改了天空盒颜色,为什么金属材质的反射没有立刻更新?这是因为默认Cubemap是烘焙在内存里的,需要重新生成。在Lighting窗口里,点Generate Lighting,或者手动设置DynamicGI.UpdateEnvironment(),反射Cubemap才会更新。
但高性能做法是放Reflection Probe。Probe可以指定源为Skybox或自定义Cubemap,开启实时刷新。注意实时反射性能开销很大,在场景中放超过两个实时Probe,手机端帧率会肉眼可见下滑。建议场景里只放一个实时Probe用于主视角,其他位置用烘焙的静态Probe。
4.2 天空盒曝光与后期色调映射的匹配
做URP项目时,Post Processing里一般都会挂Color Adjustments和Tonemapping。这两者与天空盒曝光参数的相互影响,是我调项目时最容易反复扯皮的地方。
Tonemapping的ACES模式会把高光压暗,如果你天空盒本身Exposure给了1.4,在经过ACES后天空会出现明显的灰白蔓延感,甚至有点HDR爆掉的感觉。我的调色顺序经验是:先定Tonemapping模式,再把天空盒Exposure放到0.8,看高光区域有无过曝,再回来调Color Adjustments的PostExposure。
记住一个检查方法:把场景里所有物体临时隐藏,只看天空,此时天空的明暗应比最终画面稍亮一些,因为最终有后期压在环境上。若隐藏物体后天空看起来刚好,加上Tonemapping后通常就会偏暗,此时把PostExposure拉到0.2-0.5即可平衡。
4.3 动态改变天空球材质的做法:脚本控制示例
动态切换天空球场景中,最常见的需求是做日夜循环。我常用如下脚本控制两个天空球材质之间的Lerp插值,而不是直接材质切换,这样过渡更自然。
csharp复制using UnityEngine;
public class SkyboxDayNightController : MonoBehaviour
{
public Material daySky;
public Material nightSky;
[Range(0f, 1f)]
public float time01 = 0f;
void Update()
{
// 利用材质Lerp,可将日夜两个天空盒互相混色
RenderSettings.skybox.Lerp(daySky, nightSky, time01);
DynamicGI.UpdateEnvironment(); // 更新反射环境
}
}
这里注意RenderSettings.skybox.Lerp是Material的实例方法,它修改的是当前场景天空盒材质到临时变量的混合结果,不会改到原始材质资产。每帧调用DynamicGI.UpdateEnvironment()开销不小,适合在玩法和性能要求不高的场景,或者只在time01变化超过阈值时才调用。
4.4 与雾效、大气散射的协同:做一个视觉闭环
天空球做得再漂亮,如果场景雾效不匹配也会垮掉。雾效的颜色通常取地平线附近的天空色,才显得自然。Unity的Linear Fog与Exponential Squared Fog用在室外场景中,Fog Color可以手动指定匹配地平线附近颜色。
HDRP里带物理天空盒大气散射后,雾效受大气参数联动控制,颜色与密度不再需要手动匹配。在现代渲染体系中,天空球已经和大雾、大气散射深度绑定,单独把天空球调好了是不够的,全局的视觉建模要从物理上统一。
5. 各类项目场景中的天空球适配经验
5.1 移动端、微信小游戏与数字孪生项目的性能取舍
具体问题要具体分析。如果你做的是移动端游戏,后处理、实时反射、体积云都是吃帧率的大户,天空球的Shader就必须做严格瘦身:建议用带LOD的Cubemap静态贴图,尽量规避运行时Shader计算;程序化天空的片元计算量很小,但如果在frag里叠加太多噪声函数,单片GPU的负载还是会涨得很快。
微信小游戏环境的API支持相对WebGL 2.0子集,一些PC端的Shader语法和API可能无法直接使用。做微信小游戏项目时,天空球Shader不宜用到曲面细分、几何着色器这种桌面级功能,需要保证编译目标为GLES3或WebGL2,去掉所有Geometry阶段代码。另外,微信小游戏的工程包体有大小限制,如果一个天空球材质包含了高分辨率Cubemap,体积会猛增,所以云端下载与压缩格式的选择要特别用心。
我见过不少团队用Cesium for Unity做三维地球底座,把天空球换成太空黑背景,电子地图和数字孪生中的天空球反而降级成一个基础的渐变层,重点全放在地面模型的呈现上。这说明,天空球方案最终要由项目类型和玩法重点来定,不存在万能方案。
5.2 静态Cubemap做天空:什么时候选它
程序化天空球灵活、轻量,可以调色和动态变化,但不是所有场景都适合。如果你的场景环境是固定不变的,或者美术团队已经有一套成熟的环境贴图资源,直接用Cubemap当天空盒是更稳妥、更高效的做法。
Cubemap天空盒只需要把材质Shader的Panorama贴图换成一张HDR纹理,通过Import Settings将它设为Cubemap,赋给天空盒材质即可。要注意的是,Cubemap分辨率直接决定天空清晰度:512太糊,1024基本可用,2048纹理在PC上效果良好,但移动端需要看内存预算。
有一个隐含坑:Cubemap格式在HDR和LDR之间的选择,影响天空高亮区域的表现。如果天空过曝区域想保留完整层次,务必使用HDR格式的Cubemap,然后通过曝光参数来控制亮度。如果在LDR下压曝光,高光区域会呈现难看的色阶断层。
5.3 当天空球被场景物体遮挡或穿透的奇怪现象
场景里偶尔会出现,天空球与山体、云层等模型穿插,画面中天空从模型边缘透出来,或者模型遮挡了天空结果出现闪烁。这个一般是两个原因引起的:
第一,天空球的包围盒没有正确覆盖整个场景范围,摄像机在某些位置越出了天空球的外壳,导致看穿到球外的空洞。第二,天空球的深度写入设置错误,导致它和场景物体的深度比较出现异常,尤其当玩家靠近一些半透明物体时,半透明物体与天空球的混合顺序会错乱。
解决第一种情况,最简单的方案是把天空球网格放大到远超场景半径,然后确保Shader的Cull Front使其仍然只渲染内表面。也可以用官方Skybox方案,由渲染管线自动处理,不会出现球体穿透问题。
我的实战建议是:手写天空球时,scale设置成场景半径的50-100倍,确保相机在场景内走动时,永远处于球心附近的小范围区域,不会接近球壳而穿帮。
6. 常见问题与排查技巧实录
6.1 天空球不显示,或显示为纯色
这是一个最高频的问题。排查顺序建议如下:看Camera Clear Flags是否为Skybox;检查Renderer Settings(URP里Lighting Environment里的Skybox Material)是否赋值;再看材质Shader是否编译报错,错误提示经常出现在Console面板。
有一种特殊情况:你改了场景里的Skybox,但运行时的相机是多相机配置,子相机Clear Flags被设置成Depth Only或Solid Color,画面里天空盒就被子相机的透明通道覆盖掉了。此时需要检查主相机与UI相机的关系,一般UI相机的Clear Flags要选Depth Only且Culling Mask只包含UI层。
6.2 天空盒颜色偏灰、过曝或有色带
先说颜色偏灰。多数原因是Tonemapping与泛光Bloom叠加导致,过曝部分会向灰白色过渡,看起来不干净。解决方法是把天空盒Exposure降到0.6-0.8,并把Bloom的Threshold上调到1.2以上。
色带问题集中在渐变较长的天色上,例如黄昏到夜晚的过渡。这种情况多是因为最终渲染颜色位深不够,8位RGBA下渐变不平滑。要么在天空球Shader里手动增加dithering(抖动)来打散色带,要么调整后期加微噪声把色带破坏掉,后一种方案在移动端比较有效。
6.3 天空盒反射不更新
反射不更新,通常不是Shader问题,而是反射环境数据缓存机制导致的。前面提过,RenderSettings.skybox动态变了,但很多老旧项目的反射探针烘焙是基于旧Cubemap的,没有自动跟随天空球变化。
碰到这种问题时,逐一检查场景中是否有Reflection Probe,且其Refresh Mode是否设为Every Frame或Via Scripting。如果项目以静态环境为主,建议Unity版本在4.x时代就用Baked Environment做反射,一劳永逸。动态环境则要综合考虑反射探针的重建开销。
6.4 天空球Shader在URP下编译报错
很多写好的Built-in管线Shader直接切换到URP下会报错,因为URP里CGPROGRAM的有些内置变量不再适用,比如UNITY_MATRIX_MVP的行为已经有所变化,UnityCG.cginc里的UnityObjectToClipPos在不同管线中仍然可用,但雾效宏、光照变量需要改成URP专用。
在URP中编写自定义天空球Shader,基础使用HLSLPROGRAM+CBuffer形式,加入#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"。我的方案是同时维护Built-in与URP两个版本,在正式项目的SRP Batcher兼容性上有更好的保障。
7. 个人经验总结与扩展建议
7.1 做天空球最容易忽略的三件事
回头看我做过的一堆项目,最容易被新手忽略的三件事其实是:包围盒设置的位置与scale、反射探针与该天空球的匹配、Shader的变体白白占用的内存。
包围盒scale不做大,相机跑出球体区域后一切都会炸掉,这个在前面提过;反射探针与天空球材质不匹配,产物就是地面很假,环境反射与天空完全两层皮;Shader变体控制不好,一个天空球Shader也能编译出十几个变体,每次打包体积白白多出几MB。
7.2 从天空球到整套环境光照方案
天空球是环境光照大环节的一部分,如果只为做好天空球而做,没法和场景的材质反馈联动,产出的最终画面一定是不和谐的。当你调完天空球后,不妨再做几件事:用天空球的色调去统一场景中灯的Color;调整整个场景的Ambient Color与Ambient Mode;在反射探针中放低配天空贴图,使反射与天空一致。
这一步会让你体会到,从一个天空球出发,逐渐把场景里的一切视觉串联起来,才算真正理解环境光照。
7.3 后续还能怎么扩展玩出新意
做一个可视化的昼夜循环系统是最好的练手项目。不再局限手动滑参数,而是让天空球根据真实时间24小时自动变化:日出前是深蓝渐变,日出时加入暖色光晕,正午天空发白,日落转为橙红再进入夜间,同时地面灯光与雾效也跟着联动。这个系统需要用到曲线动画与Shader参数插值,做出来之后拿到哪个项目里都极其实用。
更进一步,可以结合天气系统,把天空球的云层参数、雾浓度、光照方向做成一个WeatherManager的控制中心。我在一个项目中已经把晴天、阴天、小雨、大雾四种天气都接入到同一套天空Shader中,只要切换天气枚举,天空球、灯光、粒子效果、反射探针自动联动,整个场景氛围一键切换,这才把天空球的潜力真正榨了出来。最终你会发现,天空球这套机制,远比一个背景图值得好好钻研。
