ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity3D旋转地球实操:网格生成、UV贴图与旋转控制全解析

Unity3D旋转地球实操:网格生成、UV贴图与旋转控制全解析 在Unity3D里做旋转地球绝对算得上最经典的练手项目之一。听过有人用一张全景图贴个球就交差也见过有人为了一个三维地球从网格生成写到纹理映射写了五百行代码还搞不定两极。这篇文章不讲大道理直接复盘我实际做这个效果的全过程从生成球体网格、设置UV、处理贴图方向到旋转控制、光照布景最后再列几个我踩过的坑。如果你是刚接触Unity3D的初学者把它当作一个完整实操案例来看如果你是想在菜单背景、项目展示或作品集里放一个旋转地球的开发者这版方案可以直接抄作业。1. 整体设计思路你的旋转地球处在什么场景里1.1 三种做法对应三种需求层次做旋转地球第一件事不是写代码而是想清楚“这个地球最终出现在哪”。我见过很多方案最后发现不同的应用场景其实对应完全不同的实现路径没必要一上来就搞复杂。第一种最简单直接把Unity自带的基础球体Sphere拖进场景给材质贴上地球纹理再挂一个旋转脚本。这条路适合背景装饰、菜单过渡五秒钟就能出效果。第二种是我个人最推荐的用代码动态生成球体网格再把生成结果赋给MeshFilter。虽然前期要多写几十行代码但你可以精确控制分段数、半径、UV方向和顶点法线想加云层、加大气层、做拖拽旋转都不用依赖外部模型。对很多展示型项目来说路径B比路径A可控得多。第三种是用外部模型。热搜词里提到的SolidWorks模型导入Unity3D就是这么个操作SolidWorks导出为FBX或OBJ格式再拖入Unity工程最后挂上同样的旋转控制脚本。这种方式适合项目本身已经有精细模型的情况但要注意单位换算、坐标系朝向和材质重映射。地球如果用手工建模软件做除非你要非常夸张的浮雕细节否则和程序生成球体比起来并没有优势。1.2 自己生成网格的价值不是“不用美术”有人可能会疑惑Unity自带Sphere为什么我还要自己写网格生成这个问题我当时也问过自己。自带Sphere的问题在于它的UV分布是固定的网格密度也是固定的。你想在高纬度区域获得更多细分时它不会给你更多顶点你想让贴图的某一经线恰好落在模型某个位置时它不能按你的意图调整UV起点。更麻烦的是如果你后续要给同一套Mesh叠加云层、法线扰动或顶点动画自带的Sphere没办法很好地满足。自己生成球体网格本质上是把“球体外观”的控制权拿到自己手里。你可以把经纬度分段设成可调参数在建项目初期用低分段保证运行流畅后期需要特写镜头时再调高分段数。这种做法一旦形成一套代码之后做程序化星球、随机地形、甚至物体变形都能复用同一套逻辑。这也是为什么我在很多项目里都保留一份网格生成工具类的原因。1.3 场景扩展视频流、外部模型和可交互地球标题写的是“旋转的地球”但我建议你在动手前想想它会不会变成“会变化的地球”。如果你期望的最终效果是像一些大屏展示那样地球表面滚动着实时画面或动态数据那么核心思路是在同一个球体网格上换贴图来源。Unity3D里通过视频流或视频纹理把VideoPlayer的输出指向RenderTexture再把这个RenderTexture交给球体材质一个“动态旋转地球“就出现了。你用代码生成的Mesh在这个场景里完全不受影响连UV都不用改。如果项目里已经有其他软件建的模型比如SolidWorks之类导出FBX后同样可以沿用旋转逻辑。这时的旋转脚本只负责操作Transform不关心这个Transform上挂的是什么形状。从代码层面看网格生成、模型导入和视频流贴图其实是互相独立的模块组合起来就能衍生出各种变体。另外还有一个常见场景是“可交互地球”。加一个拖拽旋转逻辑把自动旋转改为鼠标或触摸控制这个球就从展示型变成了交互型。后面的实操章节里我会把自动旋转的脚本写出来拖拽控制其实是在它基础上再做一点点减法。2. 核心原理拆解网格、UV和旋转里的那些坑2.1 球面网格生成的数学起点球体网格的生成听起来很复杂其实就是一个经纬度采样过程。你可以把球体想象成一个西瓜先纵向切出若干瓣再横向切成若干圈交点就是顶点位置。切得越细球面越光滑三角形数量也越多。用数学语言描述球面上任意一点的坐标由两个角度决定纬度Phi和经度Theta。如果把Unity的Y轴作为球心到北极的方向那一个顶点的坐标就是x radius * sin(phi) * sin(theta) y radius * cos(phi) z radius * sin(phi) * cos(theta)Phi从0到π对应从北极到南极Theta从0到2π对应绕Y轴一圈。在代码生成时我先创建两个循环外层循环遍历纬度分段内层循环遍历经度分段每一个交点就是一个顶点。需要注意一点是北极和南极这两个极点位置在不同经度下坐标是重叠的。这不是错误因为每个重叠顶点都带着不同的UV值贴图才能在极点周围正常展开。生成三角形索引时我习惯把网格理解为“一行一行”的长条。每个格子由两个三角形拼成所以每四个顶点会生成两个三角形对应6个索引。这段逻辑不算复杂但很容易写错循环边界所以后面实操部分我会给出完整代码。2.2 UV映射为什么贴图老是反的贴图贴到球体上和把一张世界地图裹到西瓜上是一个道理。世界地图是平面的球面是曲面的要把两者对应起来必须依赖UV。所谓UV就是每个顶点在纹理图片上的二维坐标。常用的地球贴图是等距柱状投影图也就是横向表示经度、纵向表示纬度的那类图片。这种贴图与经纬度网格天然匹配纬度映射到V轴经度映射到U轴。看起来很简单但坑恰恰出在这里。不同引擎和不同图片处理工具对UV原点的定义不一致。Unity的UV坐标原点在左下角而很多图片格式和外部软件的数据起点在左上角这就导致贴图上下颠倒。至于左右方向如果纹理的经度方向和我们生成的网格经度方向相反贴图就会左右镜像。解决办法很粗暴也很实用在给顶点赋UV的时候直接对U轴做一次翻转。也就是uv.x 1f - (float)lon / longitudeSegments;这样做完之后大多数常规地球纹理就能正常显示了。如果你带的贴图左右还是错多半是贴图本身拍摄或绘制方向的问题这时候再把u的计算改成不翻转版本试一下。调整代码比在Photoshop里旋转图片快得多。2.3 贴图资源怎么选怎么避免审核问题贴图是旋转地球的颜值担当但也是最容易出问题的部分。我建议优先使用公开的自然地球纹理。比较常见的是NASA公开的地球卫星合成影像也就是大家常说的“蓝色大理石”整体效果很稳定适合学习演示。但这类高分辨率图尺寸通常很大直接加载会白占内存建议压缩到2048或4096分辨率视觉效果和性能都能兼顾。如果你不想用真实卫星图也可以自己做风格化纹理。比如用程序生成色块、噪点、渐变再配合一些云层图案出来的效果反而比写实图更适合做界面背景。这个思路在游戏项目里尤其受欢迎因为美术风格统一不会出现一张写实地球混搭在低多边形UI里的违和感。还有一个必须提醒的点围绕“地球”这个对象尽量不要使用未经授权的、包含具体区域界线类信息的地图数据尤其不要从各种渠道下载带版权水印或来源不明的商业地图贴到球上去。一方面是版权问题另一方面这类内容很容易让项目在审核环节出现风险。用公开的自然纹理或者自己绘制的风格化贴图是最稳妥的选择画面也干净。2.4 旋转逻辑的正确姿势旋转看起来是整件事里最简单的部分几行代码就能搞定但优化空间其实不小。最容易犯的错误是直接在Update里使用固定数值递增角度。比如写“transform.Rotate(new Vector3(0, 1, 0));”这种代码在60帧和30帧环境下旋转速度差一倍。正确的做法是给旋转角度乘以Time.deltaTime让旋转速度与帧率无关。旋转轴也是一个容易忽略的地方。真实地球自转轴不是垂直向上的而是相对黄道面有大约23.5度的倾斜。如果你的项目只是做个氛围背景垂直旋转也没问题但如果想做得专业一点我建议用一个空物体作为父节点把父节点倾斜23.5度地球作为子物体只绕自身的Y轴旋转。这样处理的好处是你随时可以调整倾角而不会影响旋转逻辑本身。另外要注意Update和FixedUpdate的选择。展示场景里用Update就够了因为Update与渲染同步视觉上更顺滑。FixedUpdate是为物理引擎设计的固定帧率调用如果你只是旋转一个物体却用了FixedUpdate在高刷新率显示器上会感受到微小抖动。2.5 光照与布景怎么让球“透亮”而不是“铁球”一个常见的尴尬是球模型建好了、贴图也贴上了但看起来像一颗被浇了铁水的实心金属球。原因通常是光照设置不对。Unity默认的平行光角度如果直接照着球正面你会发现高光过曝暗部死黑整个球完全没有“星球”的柔和感。建议把平行光设置在斜上方45度左右让它和球体表面形成一定的入射角这样明暗交界线会比较明显球体也会有立体感。其次要调整环境光。如果场景里没有Skybox材质环境光默认亮度可能极低球体背光面会黑得看不清纹理。在Window Rendering Lighting设置里把Environment Lighting改成Gradient模式天空色、赤道色和地面色分别给一点亮度球体暗面就有基础亮度了。如果做了后处理可以加一点Bloom让云层或者贴图里的亮色区域有微弱发光感。记住后处理强度控制得克制一些旋转地球的核心是纹理清晰、轮廓舒服不是赛博朋克发光球。3. 实操手写一个可以跑的旋转地球3.1 场景搭建从零开始打开Unity3D新建一个3D项目。渲染管线选Built-in就能跑通整个流程如果项目是URP环境后面的Shader部分我会额外说明。场景里默认自带相机和灯光。我习惯先把它们的位置重整一下把Main Camera放在(0, 0.5, -5)附近旋转角度设为(0, 0, 0)让镜头正对着原点方向。平行光保持默认存在角度调成大概(50, -30, 0)让光从斜上方照下来。再清空场景里多余的物体只保留相机和灯光。然后在层级面板里新建两个空物体一个命名为“GlobeRoot”一个放在它下面叫“EarthMesh”。为什么要用空物体因为我们要把倾斜放在父级节点上旋转放在子级节点上。3.2 网格生成脚本完整实现接下来创建脚本GlobeGenerator.cs挂到“EarthMesh”这个空物体上。脚本的功能是在运行时生成球体Mesh并赋给MeshFilter。using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class GlobeGenerator : MonoBehaviour { [Range(8, 128)] public int latitudeSegments 30; [Range(8, 128)] public int longitudeSegments 60; public float radius 1f; private void Start() { Generate(); } [ContextMenu(Generate)] public void Generate() { Mesh mesh new Mesh(); mesh.name GlobeMesh; int latCount latitudeSegments 1; int lonCount longitudeSegments 1; Vector3[] vertices new Vector3[latCount * lonCount]; Vector2[] uvs new Vector2[latCount * lonCount]; int[] triangles new int[latitudeSegments * longitudeSegments * 6]; for (int lat 0; lat latCount; lat) { float phi Mathf.PI * (float)lat / latitudeSegments; float sinPhi Mathf.Sin(phi); float cosPhi Mathf.Cos(phi); for (int lon 0; lon lonCount; lon) { float theta 2f * Mathf.PI * (float)lon / longitudeSegments; int index lat * lonCount lon; vertices[index] new Vector3( radius * sinPhi * Mathf.Sin(theta), radius * cosPhi, radius * sinPhi * Mathf.Cos(theta) ); uvs[index] new Vector2( 1f - (float)lon / longitudeSegments, (float)lat / latitudeSegments ); } } int triIndex 0; for (int lat 0; lat latitudeSegments; lat) { for (int lon 0; lon longitudeSegments; lon) { int current lat * lonCount lon; int next current lonCount; triangles[triIndex] current; triangles[triIndex] current 1; triangles[triIndex] next; triangles[triIndex] current 1; triangles[triIndex] next 1; triangles[triIndex] next; } } mesh.vertices vertices; mesh.uv uvs; mesh.triangles triangles; mesh.RecalculateNormals(); mesh.RecalculateBounds(); GetComponentMeshFilter().mesh mesh; } }这段代码的核心逻辑都在前面“网格生成原理”里讲过两个外层循环负责生成顶点两个内层循环负责生成三角面。重点说一下三角形方向的判断如果你把这段代码跑起来发现球体内壁可见、外壁变成黑色那就说明三角形索引顺序反了。这时候把内层循环里每个三角形的顶点顺序调换一下即可例如把“current, current 1, next”改成“next, current 1, current”。我之所以强调这一点是因为Mesh的三角形朝向和相机剔除方向有关。Unity默认逆时针方向为正面如果不小心写反了画面上就会出现“球黑了半边”的诡异效果。很多新手在这里折腾半天不是代码逻辑错误纯粹是索引顺序问题。3.3 旋转控制与地轴倾斜的实现旋转脚本很简单创建一个RotateGlobe.cs挂到“EarthMesh”上。using UnityEngine; public class RotateGlobe : MonoBehaviour { public float rotationSpeed 10f; private void Update() { transform.Rotate(0f, rotationSpeed * Time.deltaTime, 0f, Space.Self); } }这段代码实现了绕本地Y轴匀速自转。rotationSpeed设为10即每秒旋转10度一分钟转一圈多一点。如果你想让地球更接近真实状态在“GlobeRoot”上加上旋转倾角也就是把它的Y轴旋转改成Z轴旋转23.5度。这里要注意的是子物体“EarthMesh”的旋转始终绕自己的Y轴不受父物体旋转影响这样得到的效果就是地球带着一个固定倾角在自转。有人会问为什么旋转要放在子物体而不是直接倾斜整个球体因为如果你直接倾斜球体并让它绕Y轴旋转倾斜方向会随旋转一起转看起来就像陀螺在乱晃。父物体倾斜、子物体自转这个结构才是正确的地轴倾斜做法。3.4 添加云层的进阶姿势一个只带贴图的地球看起来还是有点干加一层云会立刻提升质感。云层的实现思路是复制一个比地球半径稍大的球面使用透明Shader贴上一张带Alpha通道的云纹理。这里提供一种最简单的做法在“EarthMesh”下再创建一个子物体“CloudMesh”给它挂上同一个GlobeGenerator只是把radius调大0.02到0.03。然后创建一个新的材质使用Unlit透明Shader把云纹理的Alpha通道作为透明蒙版。如果你用的是URP渲染管线内置的Shader名略有不同但思路一致。我提供一个适用于Built-in管线的简单Shader参考Shader Custom/CloudLayer { Properties { _MainTex (Cloud Texture, 2D) white {} _Opacity (Opacity, Range(0, 1)) 0.6 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend SrcAlpha OneMinusSrcAlpha Cull Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float _Opacity; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv); col.a * _Opacity; return col; } ENDCG } } }云层Shader的核心就是读云纹理的Alpha通道把它作为透明通道。Cull Off是为了保证从任何角度看云层都不会漏面。ZWrite Off则是让云层不要遮挡后面的物体避免深度排序问题。这层云可以和地球同步旋转也可以不同步。如果你想营造“云层飘动”的感觉单独给CloudMesh挂一个速度略慢或略快的旋转脚本效果会很微妙也算是一个小细节彩蛋。3.5 布光、后处理与导出展示做完网格和材质之后光照设置决定了这个地球最终的长相。把Directional Light的旋转角度设为(50, -30, 0)强度默认1左右就行。再开一个微弱的填充光或者点光源从球的另一侧打过来设置强度0.3左右。这样做能让地球暗部保留一些纹理细节不至于一片死黑。如果你使用的是URP管线可以创建一个Volume Profile勾选Bloom和Color Adjustments。Bloom的Threshold设为0.8左右强度设为0.15到0.3这样贴图里本来就亮的部分会微微泛光但不会过曝。Color Adjustments里把Saturation略微调到1.05画面会更鲜活一点。展示时如果需要录制动图或者视频我建议直接用Unity Recorder插件可以一次导出多分辨率视频或Gif图。要注意的是录制时把Time Scale设为1并且确保场景里没有其他动态物体干扰背景。4. 常见问题排查我踩过的坑直接给你避雷清单4.1 球面黑了一半或完全看不见这个大概率是三角形索引顺序问题。Mesh面片的正面朝向与三角形顶点顺序有关Unity里逆时针方向为正面。如果你生成顶点时顺序写反了正面就会朝内从外部看球体就变成黑色或消失。排查方法很简单把相机移到球体内部如果能在里面看到球面那说明法线确实反了。修复就是把每个三角形的顶点顺序整体调换也就是把“current, current1, next”改为“next, current1, current”。改完再运行一次黑色面就应该消失。如果用的是双面材质这个问题会被掩盖但为了后续加光照和法线效果最好从根本上修正索引顺序。4.2 贴图左右颠倒、上下颠倒这是Unity开发中最容易遇到的问题之一。地球贴图通常在平面软件里以左上角为原点而Unity的UV坐标以左下角为原点导致上下反转。左右方向则取决于生成网格时经度遍历方向。解决办法就在UV计算那一步。上下颠倒时把v改成“1f - (float)lat / latitudeSegments”左右颠倒时把u改成“1f - (float)lon / longitudeSegments”。我建议先在球上贴一张带方向文字的测试图根据文字朝向快速判断是哪个方向出了问题改一次UV就能定位。4.3 极点拉伸到没法看北极和南极附近出现放射状拉伸这是等距柱状投影的固有特征不是Bug。贴图在极点附近本来就堆积了大量像素当这些像素映射到球面极点的一个小区域时拉伸变形就会特别明显。要缓解这个问题最直接的办法是提高latitudeSegments的数值让极点附近的顶点密度增加贴图像素可以更集中地表现出来。如果你希望完全避免极点变形可以换用其他UV映射方案比如立方体映射或者收拢极点UV的球面映射。不过对绝大多数背景展示来说提高分段数已经足够了。latitudeSegments调整到40以上极点区域的拉伸基本就不太看得出。4.4 旋转速度忽快忽慢这个问题的根源几乎都是没有使用Time.deltaTime。如果直接用transform.Rotate(0, 1, 0)这样写旋转量固定为每帧1度在帧率波动时速度就不稳定。改成乘以Time.deltaTime后旋转角度按秒累计帧率变化不影响最终速度。还有一个细节如果你的项目同时存在物理模拟不要用FixedUpdate控制旋转。FixedUpdate的调用频率是固定的但渲染帧不一定和它对齐视觉效果会出现微抖。展示用Update物理模拟单独走FixedUpdate这是原则。4.5 想把地球放进UI如果你想让这个旋转地球显示在UI界面上不能直接把3D物体放进Canvas。正确做法是建一个RenderTexture把一台独立的相机对准地球将相机TargetTexture设为这个RenderTexture再把RenderTexture赋给UI里的RawImage。需要注意几点RenderTexture的分辨率要对应RawImage在屏幕上的大小不要创建超大纹理相机要放在与主场景不重叠的层或者使用独立Layer来避免重复渲染如果有交互需求比如拖拽旋转需要在RawImage上监听鼠标事件再把角度增量发送给旋转脚本。用这个方案一个UI地球的成本并不高效果却非常抓人眼球。4.6 放进URP/HDRP项目后的兼容性如果你是在URP或者HDRP项目里做这个效果项目模板自带的默认Shader和Built-in的Standard Shader不通用。最简单的方法是创建URP材质选择URP/Lit或URP/Unlit着色器再赋上贴图。前面云层Shader那段代码是基于Built-in管线的要迁移到URP需要改写HLSL版本或者直接用URP/Unlit的透明材质配合云纹理的Alpha通道也能达到类似效果。如果你对Shader不太熟悉我建议先用URP/Lit做实体地球用URP/Unlit加透明做云层两个材质都不是自定义Shader兼容性问题会少很多。最后再分享一个实际操作中的经验我第一次做这个项目时花最久时间的是贴图方向。网上能找到的球体生成代码坐标系、UV定义各不相同有的习惯用Z轴朝上有的用Y轴朝上直接拷过来经常出现贴图左右颠倒。后来我养成了一个习惯先在工程资源里放一张带箭头和文字的测试贴图每次生成网格后先看测试贴图的方向确认无误再换成正式地球纹理。这个方法帮我省了很多时间你也可以试一试。
返回列表