先给一句话结论:
球面展开就是在“球面上的方向”和“一张平面图片上的坐标”之间建立换算关系。因为球面无法不变形地摊平,所以不同的展开方式各有代价,游戏会根据用途选择不同的方案。
一、通俗理解:地球仪和世界地图
地球仪(球面) 世界地图(平面) ___ ┌──────────────────────┐ / 北 \ │▓▓▓▓▓▓ 北极被拉成一整条 ▓▓▓│ │ 极 │ 展开 ─▶ │ │ │ │ │ 赤道附近基本正常 │ \_南__/ │ │ │▓▓▓▓▓▓ 南极被拉成一整条 ▓▓▓│ └──────────────────────┘常见的世界地图上,格陵兰岛看起来几乎和非洲一样大,实际上非洲大约是它的 14 倍。原因是:
球面是弯曲的,平面是平的 就像橘子皮,怎么压都无法完全平整地铺在桌上,一定会被拉伸或撕开数学上,这由高斯的“绝妙定理”保证:球面和平面的曲率不同,不存在既不拉伸、又不撕裂的展开方式。
所以所有球面展开方案都是在做取舍:
保角度? 保面积? 接缝少? 计算简单? 采样均匀? → 不可能全部满足,只能根据用途挑选游戏中需要处理球面的地方很多:天空、环境反射、星球、全景视频、光照探针等,本质都是用一张或几张平面贴图,存储“各个方向”的信息。
二、核心原理:方向 ↔ 球面坐标 ↔ 贴图 UV
1. 用两个角度描述球面上的一个点
球面上任意一点,都可以用“经度 + 纬度”两个角度表示:
Y(上) │ ● 点 P │ /│ │ / │ θ:纬度(离赤道多高,-90°~+90°) │/ │ ─────────●───┼────── X / ╲ │ / ╲ │ Z φ:经度(绕 Y 轴转了多少,-180°~+180°)在游戏中,球面上的一个点通常等价于从球心出发的一个单位方向向量d = (x, y, z),例如:
天空:从摄像机看向某个方向,能看到什么颜色 反射:反射光线朝某个方向射出,会照到什么2. 方向 → UV(查贴图时用)
以最常见的**经纬度展开(Equirectangular)**为例,Y 轴朝上:
经度 φ = atan2(z, x) 范围 -π ~ π 纬度 θ = asin(y) 范围 -π/2 ~ π/2 u = φ / (2π) + 0.5 范围 0 ~ 1 v = θ / π + 0.5 范围 0 ~ 1相当于:经度横向摊开成 u,纬度纵向摊开成 v,得到一张宽高比为 2:1 的图。
3. UV → 方向(生成球体模型、烘焙贴图时用)
φ = (u - 0.5) × 2π θ = (v - 0.5) × π x = cos θ · cos φ y = sin θ z = cos θ · sin φ两个方向的换算都有了,就能在“球面”和“图片”之间来回转换。
4. Shader 示例:用全景图画天空
Texture2D _PanoramaTex; SamplerState sampler_PanoramaTex; float2 DirToEquirectUV(float3 d) { d = normalize(d); float u = atan2(d.z, d.x) / (2.0 * PI) + 0.5; float v = asin(d.y) / PI + 0.5; return float2(u, v); } float4 frag(float3 viewDir : TEXCOORD0) : SV_Target { float2 uv = DirToEquirectUV(viewDir); return _PanoramaTex.Sample(sampler_PanoramaTex, uv); }每个像素只需要知道“摄像机朝哪看”,换算成 UV 后查表即可。
三、经纬度展开的三个典型问题
问题 1:两极严重拉伸
赤道一圈周长:2πR 北纬 60° 一圈周长:πR(只有一半) 北极:周长为 0,只是一个点 但在贴图里,每一行的像素数都一样多 → 越靠近两极,越多像素挤在越小的区域里 → 极点处,整整一行像素都对应同一个点结果是:
- 两极浪费大量像素,赤道附近反而分辨率不足 - 在球体模型上,极点附近的贴图呈放射状扭曲(“菊花纹”)每个像素覆盖的实际面积,大致和cos(纬度)成正比,越靠近两极越小。
问题 2:左右边缘的接缝
u = 0和u = 1在球面上是同一条经线,但在贴图上分别位于最左和最右:
贴图: │ u=0 …………………………………… u=1 │ └──────── 球面上其实是同一条线 ────────┘如果贴图左右两边内容没有对齐,会出现一条明显的竖线。
问题 3:接缝处的 Mipmap 错误(Shader 中很常见)
即使贴图左右两边完全对齐,实时渲染时也常看到一条细线。原因是:
GPU 选择 Mipmap 层级时,会比较相邻像素之间 UV 的变化量 正常情况:相邻像素 u 相差 0.001 → 选清晰的层级 接缝处: 左边像素 u = 0.999, 右边像素 u = 0.001 → GPU 以为 u 变化了近 1 → 选最模糊的 Mipmap → 出现一条模糊或异色的细线常见解决方法:
// 方法 1:同时计算两套 u,选变化量更小的那套来计算 Mipmap float u1 = frac(uv.x); float u2 = frac(uv.x + 0.5) - 0.5; float u = (fwidth(u1) < fwidth(u2)) ? u1 : u2; // 方法 2:不用自动 Mipmap,自己用连续的方向向量计算梯度,再调用 SampleGrad // 方法 3:天空只用 Mip 0 采样(SampleLevel(..., 0)),简单直接四、常见的球面展开方式对比
| 方式 | 形状 | 优点 | 缺点 | 典型用途 |
|---|---|---|---|---|
| 经纬度(Equirectangular) | 一张 2:1 的图 | 直观,易编辑,摄影设备直接输出 | 两极拉伸,有接缝 | HDRI 全景、360 视频、天空素材 |
| 立方体贴图(Cube Map) | 6 张正方形图 | 变形小,GPU 硬件原生支持,无极点 | 6 个面有边界,占用较多 | 天空盒、反射探针 |
| 球形贴图(Sphere Map) | 一张圆形图,像拍镜面球 | 只要一张图 | 依赖视角,背面严重压缩 | 早期环境反射、MatCap |
| 八面体映射(Octahedral) | 一张正方形图 | 一张图覆盖全方向,分布较均匀,编码简单 | 有折叠接缝 | 光照探针、法线压缩、探针图集 |
| 双抛物面(Dual Paraboloid) | 前、后两张圆形图 | 只需渲染两次 | 两半交界处有误差 | 早期动态反射、点光源阴影 |
| 立方体球(Cube Sphere) | 6 块网格 | 网格分布均匀,无极点 | UV 分成 6 块 | 星球地形 |
几种展开方式的形状示意:
经纬度 立方体贴图(十字展开) 八面体 ┌──────────┐ ┌──┐ ┌───────┐ │ │ ┌──┼──┼──┬──┐ │ ╲ ╱ │ │ 2 : 1 │ │ │ │ │ │ │ ╲ ╱ │ │ │ └──┼──┼──┴──┘ │ ╱ ╲ │ └──────────┘ └──┘ │ ╱ ╲ │ └───────┘五、在游戏中的应用
应用 1:天空盒与 HDRI 全景
美术和摄影师一般提供经纬度全景图,因为相机、渲染软件可以直接输出,Photoshop 也能编辑:
拍摄或渲染 360° 全景 → 得到 2:1 经纬度图(HDR 格式) │ ├── 直接在 Shader 里按方向采样,作为天空(Unity 的 Panoramic Skybox) │ └── 导入引擎时转换成 Cube Map → 运行时采样更快,没有极点问题经纬度图适合制作、传输和存储,Cube Map 适合运行时采样。
应用 2:环境反射
金属、水面、车漆等材质,需要知道“反射方向上能看到什么”:
反射方向 R = reflect(视线方向, 法线) 颜色 = 环境贴图.Sample(R)技术演进大致是:
早期(固定管线时代):Sphere Map 一张“拍镜面球”的照片,只在某个视角下正确 之后:Cube Map 反射 视角无关,GPU 原生支持 现代:Reflection Probe(反射探针) 在场景中多处放置探针,各自烘焙一张 Cube Map 物体使用离自己最近的探针,并按粗糙度选择不同的 Mip 层级 (粗糙表面反射模糊,就用更模糊的 Mip)应用 3:MatCap(材质捕获)
MatCap 是球形贴图在今天最常见的用法:用一张“画好的光照球”代替光照计算。
美术画一张圆形的球体光照图: ┌─────────┐ │ ◜高光◝ │ │ ( 球 ) │ 这张图已经包含了光照、高光、反射、材质质感 │ ◟暗部◞ │ └─────────┘原理:把视角空间下的法线直接当作 UV:
float3 nView = normalize(mul((float3x3)UNITY_MATRIX_V, worldNormal)); float2 matcapUV = nView.xy * 0.5 + 0.5; float3 color = _MatCapTex.Sample(sampler_MatCapTex, matcapUV).rgb;法线朝向屏幕正前方 → 采样图片中心 法线朝向左边 → 采样图片左侧 法线朝向上方 → 采样图片上侧特点:
优点:只需一次采样,极其便宜,效果风格化、可控 适合手游角色、雕刻软件预览(ZBrush)、卡通渲染、UI 中的 3D 物件 缺点:光照跟着摄像机走,物体转动时高光位置不变,无法对应真实光源应用 4:星球和球体模型贴图
给一个球体模型贴图,就是在做“球面展开”。常见的球体网格有两种:
UV 球(经纬线球) 立方体球(Cube Sphere) ___ ┌──────┐ /│││\ 极点处三角形挤成一团 │╲ ╱│ │─┼┼┼─│ UV 就是经纬度展开 │ ┌──┐ │ 把立方体每个顶点归一化 │─┼┼┼─│ → 贴图在两极扭曲 │ └──┘ │ 推到球面上 \│││/ │╱ ╲│ 网格均匀,没有极点 └──────┘开放世界中的星球(如太空探索类游戏)通常采用立方体球 + 分块 LOD:
- 6 个面各自是一块四叉树地形,靠近时细分 - 每个面有独立的高度图、贴图,没有极点问题 - 也可以避开 UV,直接用三维坐标采样三维噪声生成地形,完全无接缝对于岩石、地形等不想处理 UV 的情况,也常用三平面映射(Triplanar Mapping):从 X、Y、Z 三个方向投影贴图,按法线混合,和球面展开思路互补。
应用 5:360° 全景视频与 VR
全景视频通常以经纬度格式编码(视频编码器只认矩形画面) │ 播放时:在一个内表面朝向观众的大球上贴视频,或者在 Shader 中按视线方向采样 │ 优化:两极浪费了大量码率 → 一些平台改用立方体格式或等角立方体格式(EAC) → 同样的码率,画面中更多像素用在了观众真正关心的区域应用 6:存储“各个方向”的数据(八面体映射)
现代引擎中有大量数据是“每个方向一个值”:
- 光照探针(Light Probe):每个方向的入射光是多少 - 全局光照探针(如 DDGI):每个方向的光照和距离 - G-Buffer 中的法线:把三维单位向量压缩成两个数八面体映射特别适合这些用途:
1. 把方向投影到八面体 |x| + |y| + |z| = 1 上 2. 上半部分直接压平成菱形 3. 下半部分沿边折到四个角上 4. 得到一张正方形图,覆盖全部方向float2 OctEncode(float3 n) { n /= (abs(n.x) + abs(n.y) + abs(n.z)); if (n.z < 0) n.xy = (1.0 - abs(n.yx)) * (n.xy >= 0 ? 1.0 : -1.0); return n.xy * 0.5 + 0.5; }优势:
- 只需一张正方形贴图,方便把成百上千个探针打包到一张大图集里 - 分布比经纬度均匀,没有严重的极点问题 - 法线从 3 个分量压到 2 个分量,节省 G-Buffer 带宽六、怎么选择
| 需求 | 推荐方式 |
|---|---|
| 美术制作、交换全景素材 | 经纬度 |
| 运行时天空盒、反射 | Cube Map |
| 手游低成本风格化材质 | MatCap(球形贴图) |
| 大量探针打包、法线压缩 | 八面体映射 |
| 星球地形 | 立方体球 + 分块 LOD,或三维噪声 |
| 不想处理 UV 的球形物体 | 三平面映射 |
| 360 视频 | 经纬度(兼容性好)或立方体格式(画质更好) |
七、总结
1. 本质:建立“球面方向 ↔ 平面贴图坐标”的换算关系 方向向量 → 经纬度角 → UV,以及反向换算 2. 根本限制:球面无法无变形地展开 所有方案都在拉伸、接缝、均匀度、计算成本之间取舍 3. 经纬度展开最直观,但有极点拉伸、左右接缝、Mipmap 细线问题 4. 游戏中的主要用途: 天空与全景 → 经纬度素材,运行时多转为 Cube Map 环境反射 → Cube Map / 反射探针 风格化材质 → MatCap 星球 → 立方体球 方向数据 → 八面体映射一句话:球面展开就是把“四面八方的信息”塞进平面贴图;没有完美的塞法,经纬度方便制作,Cube Map 方便实时采样,MatCap 便宜好看,八面体适合大量存储,游戏开发要做的就是按场景选择最合适的那一种。