要说Unity3D里最容易让新手做出成就感的效果,旋转地球绝对排得上号。它不涉及复杂的物理模拟,也不依赖美术资源,只需要一个球体、一张贴图、几行脚本,就能在屏幕上看到一个像模像样的星球。但你别小看这个小玩意儿,它背后牵扯到的知识点,几乎覆盖了Unity开发的全部底层逻辑:坐标变换、组件生命周期、帧循环与时间管理、资源管线,甚至光照和材质原理。我最早接触Unity时,就是靠这个项目理解了很多文档上看不懂的概念。这篇文章我把完整的实现过程拆开揉碎讲清楚,从建场景到写代码,从转速计算到坑点排查,争取让零基础的朋友照着做也能一次跑起来。
1. 从零搭场景:球体、材质与光照的正确姿势
1.1 创建球体与贴图:最容易被小瞧的一步
新建Unity工程时建议选3D模板,版本用Unity 2020 LTS或者更新的2021/2022都可以,核心API差别不大。工程建好后的第一步是在Hierarchy窗口里右键,选择3D Object -> Sphere,场景里就出现了一个默认的白色球体。这一步看起来简单,但它同时干了三件事:创建了一个带有Transform组件的GameObject,给它挂了一个MeshRenderer,还附带了一个SphereCollider。MeshRenderer负责把球体网格渲染出来,Transform记录位置旋转缩放,缺一不可。
接下来要给球换皮肤。先下载一张地球纹理图,推荐搜“earth texture map”或者直接去NASA的可见地球项目里找公开素材,分辨率选2048x2048的版本就够了,再高的4096对新手阶段的加载速度和包体都不友好。在Project窗口右键Create -> Material,新建材质球命名为EarthMat。选中它,在Inspector面板里把Shader从默认的Legacy Shaders换到Standard(也就是Unity标准着色器)。Albedo贴图那一栏拖入地球纹理,然后把Metallic调到0,Smoothness调到0。这一步很多人会忽略,但它是决定“地球看起来像塑料还是像星球”的关键:默认的Smoothness值会让高光区域异常油亮,真实的地球表面在大气层下是漫反射占主导,粗糙度很高,所以平滑度必须归零。
1.2 摄像机、光源与背景:为什么我的球是乌漆嘛黑的
如果你现在直接点运行,大概率看到的画面是球体出现在视野中央但颜色发灰,甚至某一面全黑。原因有两层:第一,Unity场景默认自带一个方向光Directional Light,但它的角度是从斜上方照下来的,如果球正好没有受光面,渲染出来就偏暗;第二,Standard Shader在没有任何光照的情况下,是不会显示Albedo贴图原本颜色的,它需要光的输入才能计算最终像素颜色。
解决办法是先选中主摄像机和方向光,在Transform面板里确认它们的朝向。最简单稳妥的方案是:摄像机放世界坐标(0, 0, -5)附近,看向原点;灯光放在(2, 4, 3)附近,Rotation大约是(50, -30, 0),这样球体的受光面和相机视角能对上。背景色如果不想用默认天空盒,可以在Window -> Rendering -> Lighting 环境设置里把天空盒换成纯色Solid Color,这样地球轮廓会更突出,更像一张“星球渲染图”。
这里我额外提一个经验:做这类单物体演示项目,建议先把场景里自带的平行光角度固定在能让球体产生“侧光”的位置,也就是光源和摄像机呈大约90度到120度夹角。全正面光会把贴图的凹凸细节全都打平,纯侧光又能看出球体轮廓的立体感。实操下来一百二十度的夹角观感最好。
2. 旋转的核心逻辑:Transform、Time.deltaTime与引擎帧循环
2.1 认识Transform、Start和Update三个老朋友
在这个项目里,你只需要写一个几十行的C#脚本就能完成所有逻辑,但脚本里涉及的几个概念值得掰开讲透。
Transform组件代表的是物体在三维空间中的姿态,包含了Position(位置)、Rotation(旋转)、Scale(缩放)。你在代码里访问transform.Rotate,本质上是在修改这个组件维护的旋转四元数和角度数据。这里很多人会困惑:为什么我不直接改transform.rotation这个四元数?因为四元数直接赋值对新手来说是灾难,你根本没法直观地写“让它绕Y轴转30度”,而Rotate方法把坐标旋转转换成了人类能理解的角度加法,这才是它存在的意义。
Start函数在脚本生命周期里只会执行一次,在挂载物体的那一帧之后触发。Update函数在每一帧渲染之前执行,只要你点击了Play,它就不停地跑。这个项目里旋转变量的初始化和速度定义放Start里做,旋转动作本身放Update里做。理由很简单:速度和方向只初始化一次,但转动是持续性的,每一帧都需要让角度发生微小变化。
2.2 为什么Time.deltaTime是旋转的灵魂
很多第一次写旋转脚本的新手都会写出这样的代码:transform.Rotate(Vector3.up * 10f);然后发现球转得飞快,或者在不同电脑上转速还不一样。这就要回到Unity帧循环的本质:Update的调用频率取决于当前帧率。在60Hz的显示器上,Update一秒钟调用60次,但在低配手机或者复杂场景里可能只有30次甚至更低。如果不乘以deltaTime,转速就是“每帧固定转10度”,帧率高的设备转得就快,帧率低的设备转得就慢,完全不可控。
Time.deltaTime指的是上一帧到当前帧所经过的秒数。把它乘以速度值,所有设备上的角速度就统一了:每秒固定的旋转角度。这是Unity里一切移动、旋转、计时逻辑的基础。我习惯把它类比成开车时的里程表:帧率是发动机转速,deltaTime是挡位齿比,只有乘以齿比,车速才是稳定可控的。写代码时永远记住这句话:所有跟时间相关的数值,在Update里都要乘以Time.deltaTime。旋转、位移、倒计时、缩放统统如此,例外只有一个:你刻意想做“暂停时仍继续无视时间的动画”,但那需要用别的方式绕过。
2.3 转轴与转速:从真实地球的数据反推
地球自转一圈的时间是23小时56分4秒,也就是86164秒。一圈360度,因此地球的真实角速度是360除以86164,约等于每秒0.00418度。这个数据在数学上很优美,但你把它直接填进Unity,球转一圈要一天,观众看五分钟还以为画面卡了。
所以实操时要做一个“加速倍数”。我的做法是在脚本里定义一个rotationSpeed变量,取值为真实角速度乘以一个加速系数。演示场景我习惯用80倍到100倍,刚好能看出明显自转,又不会转到头晕。算一下:0.00418乘以100就是每秒0.418度,转完一圈大约需要861秒,约14分钟,视觉上刚刚好。如果你只想做十秒左右的快速演示,把倍数拉到1000,每秒转4.18度,一圈86秒,也挺舒服。这个系数后续可以做成公开变量在Inspector里调节,免得每次改代码重新编译。
还有一个细节是自转轴倾斜。真实地球的自转轴和黄道面的法线方向夹角大约是23.4度。如果只是让球体绕默认的Y轴转,效果就是乒乓球转体,不够写实。在Unity里处理这个问题的标准做法是把球体放进一个空物体,调整空物体的Rotation为X轴倾斜23.4度(也就是transform.rotation = Quaternion.Euler(23.4f, 0, 0)),然后让脚本控制球体在空物体内部绕着本地Y轴转。这样你得到的是“一根倾斜的轴在带动球体旋转”,视觉效果和真实地球的自转姿态完全对齐。脚本旋转时记得用Space.Self,让它绕自身本地坐标转,而不是绕世界坐标轴转,否则倾斜会被覆盖掉。
3. 完整脚本实现:从最小运行到进阶扩展
3.1 最小可运行脚本:五分钟从零到转
下面这段代码是整篇文章的核心。新建一个C#脚本,命名为EarthRotation,挂到Sphere物体上。代码如下:
using UnityEngine; public class EarthRotation : MonoBehaviour { [Header("旋转速度控制")] public float rotateSpeed = 10f; // 每秒旋转角度,做演示时可以改到30~60 [Header("自转轴倾斜角度(度)")] public float axialTilt = 23.4f; private Transform planetTransform; void Start() { planetTransform = transform; // 将球体自身绕X轴倾斜,模拟真实地球的轴倾角 transform.rotation = Quaternion.Euler(axialTilt, 0f, 0f); } void Update() { // 使用本地坐标轴旋转:绕物体自身的向上方向轴(Y轴)转动 planetTransform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime, Space.Self); } }运行一下,你会看到一个倾斜的地球在持续自转。就这么简单。如果你把Space.Self改写成Space.World,旋转轴会立刻变回世界坐标的竖直轴,之前设置的23.4度倾斜相当于被无视了。这个坑我踩过不止一次,每次都是从“哇好真实”变成“怎么又竖直了”,原因都是在潜意识里把Rotate的默认空间当成世界坐标。
关于旋转速度,我再补充一个参数计算思路。假如你希望球体转一圈正好用20秒,那么角速度就是360/20 = 18度/秒。想让它转一圈用60秒,就把速度设置成6。公式是:speed = 360 / 目标秒数。这个换算在你做任何旋转类动画时都能用上,比如时钟指针、雷达扫射、风扇转动、开门动画,同一套逻辑。
3.2 增加公转:一个空物体同时搞定自转和轨迹
自转搞定之后,总忍不住想让它动得再丰富一点。给地球加一个绕太阳公转的效果,是很多人的第一个扩展需求。实现方式也很经典:建一个空物体,命名为EarthOrbitCenter,放在世界原点当父物体,把Sphere拖进去成为它的子物体。子物体相对于父物体的偏移量就是公转半径,比如把Sphere的Position设为(5, 0, 0)。然后给Sphere挂上自转脚本,再给EarthOrbitCenter挂一个新的公转脚本:
using UnityEngine; public class OrbitRotation : MonoBehaviour { public float orbitSpeed = 20f; // 公转角速度,单位:度/秒 void Update() { // 让父物体绕世界Y轴旋转,子物体自然会跟着做圆周运动 transform.Rotate(Vector3.up, orbitSpeed * Time.deltaTime, Space.World); } }父物体绕世界Y轴转,子物体就在水平面画圈。公转半径等于子物体Position的模长,想调整就改父物体下Sphere的坐标。这里的细节是:如果子物体坐标不是(5, 0, 0)而是(5, 2, 0),公转轨道就变成了倾角轨道,视觉效果更像行星运动。这个例子里,orbitSpeed的计算方式和转速完全一样,360除以目标公转秒数。
公转和自转同时运动时,你会发现一个很容易被忽视的现象:地球自身的旋转方向会因为公转叠加而变得混乱,特别是在相机位置不变的情况下。如果想让观感更干净,可以把公转脚本的Space.World也改成Space.Self,但这样一来,父物体的本地坐标会随着旋转而偏转,轨道就不再是正圆形。所以这里我必须提醒你:太空场景演示里的自转和公转必须分开挂在两个物体上,并且公转用World空间,自转用Self空间,两者才互不干扰。
3.3 让球体更真实的进阶扩展:法线贴图、云层、视频动态纹理
基础版本做出来之后,你肯定不满足于一张平贴图。想让地球看起来有地形凹凸感,需要用到法线贴图。网上搜索“earth normal map”能找到现成的资源,把法线贴图拖到材质的Normal Map槽位,然后把Smoothness从0调回0.5左右,可以让凹凸细节和光照互动时产生微弱的镜面高光,海洋和陆地交界处会呈现出类似真实卫星图的光泽感,观感提升非常明显。
再进一步,可以加一层云层效果。做法是再复制一个球体,略微放大,比如Scale设为球体的1.03倍,挂上一个独立的材质,使用透明Shader(比如Universal Rendering Pipeline下的Unlit Transparent或者Legacy的Transparent/Diffuse),把一张云层纹理作为主贴图,颜色设为白色,透明度控制为半透状态。这样对着光源时,云层会像真的云层一样飘在地球表面,而不会写穿到背面。这个思路本质上是利用Unity的Render Queue排序:半透明物体在相机渲染时后绘制,所以会呈现覆盖在实体球体上的效果。
关于云层是否旋转:如果你想让云层跟着地球一起自转,就把它也挂到同一个倾斜轴的空物体下,并复用旋转脚本。如果想模拟大气流动,可以给云层单独调整一个稍微不同的转速,比如比地表快2度每秒,动态感立马就出来了。
还有一个很多人感兴趣的方向是动态纹理。热搜词里出现了“unity3d视频流”,在这个项目里就能用上:视频文件可以作为VideoClip直接赋给一个VideoPlayer组件,然后把视频渲染到RenderTexture上,再把RenderTexture赋给材质的Albedo贴图。这样地球表面显示的就是一段动态影像,比如卫星云图动画或者电影片段的球面映射。这个做法的原理就是VideoPlayer->RenderTexture->Material的管线,本质上和静态贴图没有区别,但能玩出很多花样。Unity 2021之后的VideoPlayer已经原生支持URL播放,直接从网络流拉视频纹理也不是不行,不过需要留意视频编码格式和内存占用。
至于“solidworks模型导入unity3d”这个方向,跟旋转地球其实也有关联:你可以把SolidWorks导出的球体或卫星模型以FBX格式导入Unity,替换掉默认Sphere。导入时注意SolidWorks的模型通常是CAD坐标系(Z轴向上),导入Unity后可能需要旋转90度修正轴向,才能让模型与自转轴对齐。这一点我曾经在导入一个卫星模型时踩过坑,FBX里的模型默认朝向和Unity的坐标系完全不同,解决办法是在导入设置面板的Model选项卡里,调整X轴旋转量到-90度,或者建一个空物体把模型包一层并手动修正朝向。
4. 常见问题与排查技巧实录
4.1 现象速查表
下面这些问题是过去我身边所有人做这个项目时都会遇到的,我整理成一个速查表:
| 现象 | 根本原因 | 解决办法 |
|---|---|---|
| 球体全黑或没有颜色 | 没有光照,或材质Shader用错 | 确认场景有Directional Light,材质Shader选Standard |
| 旋转反了或转歪 | Space.World和Space.Self搞混 | 自转用Space.Self,公转用Space.World |
| 球体转得时快时慢 | 旋转代码没乘Time.deltaTime | 给角度增量乘以Time.deltaTime |
| 材质像塑料一样反光 | Smoothness过高 | Albedo保持完整贴图,Smoothness改为0 |
| 球体看起来被压扁 | Scale轴向不一致 | 球体Scale保持三轴相同 |
| 贴图接缝处错位 | UV映射接缝不可见 | 使用2:1等距柱状投影的地球贴图 |
| 摄像机里看不到完整球体 | 相机FOV太小或距离太近 | 摄像机放到Z轴-5以上,或调大FOV |
| 子物体公转不变 | 旋转了子物体本身而不是父物体 | 公转逻辑必须放在父物体上 |
| 云层写穿球体 | 半透明球体没有背面剔除 | 用Transparent Dffuse着色器并启用双面渲染或调整RenderQueue |
| 模型轴向倾斜很奇怪 | 导入文件的坐标系不匹配 | 导入后查看FBX的旋转导入设置,修正-90度X轴 |
4.2 几个实际调试经验
第一个经验是:旋转不生效,不要先怀疑代码,先看Transform面板。运行游戏时选中球体,看Rotation数值是否有变化。如果数值在变但视觉不变,多半是摄像机没动,或者你创建了一个空物体挂脚本,但球是子物体之外的另一个独立物体。这种时候从层级结构里拉一条线,理清父子关系,比白苍苍地翻代码快得多。
第二个经验关于贴图方向:地球纹理务必选择标准的等距柱状投影图,长宽比是2:1。如果用了其它比例或者从搜索引擎里随便抠出来的平面地图,贴到球面上后地图线条会扭曲,经线和纬线交叉处割裂感特别明显。我见过很多人觉得是Unity的UV映射有bug,其实单纯是素材问题。检测方法很原始:在资料文件夹里双击贴图预览,如果左边的边缘和右边边缘在视觉上能自然接上,大概率没问题。
第三个经验是性能相关。我的场景里既要球体旋转云层,又加了VideoPlayer播放视频流,手机端发热严重,帧率掉到二十以下。排查后发现罪魁祸首是RenderTexture的实时刷新加上半透明云层的overdraw。解决办法是缩小RenderTexture分辨率到1024,并把云层纹理做成静态贴图配合轻微旋转,不透明物体和半透明物体的绘制开销立刻降下来了。如果你要真做移动端的心情球球屏保,记住:模型面数不是瓶颈,纹理采样频率和透明片数量才是。
第四个经验和光照烘焙有关。演示项目默认没做烘焙,但如果加上了静态的Spacebox或场景装饰,很容易出现球体表面的间接光变化,导致材质看起来忽亮忽暗。要解决这个问题,可以在Lighting设置里把Ambient Mode从Realtime改成Baked,灯光模式设置为Mixed。同时确保球体是动态物体(Dynamic),否则烘焙好的光照不会在旋转时实时更新,会产生“球面亮了半圈”的诡异效果。关于动态物体和静态物体的光照交互,我建议新手直接记住:任何旋转、移动的物体都要保持Dynamic标识。
5. 写在这个小项目之后
这个旋转地球项目做完,我的最大收获不是说会了Unity里某个按钮的用法,而是第一次把“组件、脚本、帧循环”这三层逻辑串到了一起。组件描述了世界是什么,脚本定义了世界怎么变,帧循环决定了世界什么时候变。这套思维模型一旦建立,之后再学移动、碰撞、动画、网络同步,都是在同一张地图上添砖加瓦。我建议你做完基础版本后,试着把转速改成Inspector可调的公开变量,再把球体的Material复制一份做成夜晚灯光版本(Emissive槽位加一张城市夜光贴图),观察一下同一颗星球在不同光照条件下的观感差异——这个小实验能帮你理解PBR材质系统里的自发光、反射率、光源强度三者是怎么协同工作的。如果后面你对地球数据可视化感兴趣,还可以在球体表面动态生成节点或标记,用脚本按照经纬度坐标把UI元素投影到三维表面上,那就是下一个人气项目“Unity3D地球信息可视化”的起点了。