1. 项目概述为什么天空盒是新手绕不开的第一课如果你是刚接触Unity3D的新手在捣鼓场景搭建时大概率会遇到一个共同的困惑为什么我的场景看起来这么“假”明明模型摆好了灯光也打上了但整个环境就是缺乏沉浸感像一个悬浮在虚空中的孤岛。这个问题的答案往往就出在“天空盒”上。天空盒你可以把它想象成一个包裹住整个游戏世界的巨大立方体盒子盒子的内壁贴上了天空、远山、云层等环境图像。它决定了玩家视野中除了你亲手放置的物体之外所有背景区域呈现什么样子。一个合适的天空盒能瞬间将你的场景从“白模测试”状态拉入到一个有氛围、有故事感的世界里。很多新手教程会直接告诉你“拖一个天空盒材质球到渲染设置里”但知其然更要知其所以然。为什么Unity默认的天空盒选项那么少网上找到的漂亮全景图为什么拖进去不是一片黑就是严重变形这背后涉及的核心技术点就是“Cubemap”。简单来说Cubemap是一套由六张正方形图片组成的资源分别对应立方体的前、后、左、右、上、下六个面。Unity的天空盒系统正是基于Cubemap来工作的。理解了这一点你就掌握了自定义天空盒的钥匙。今天我们不只讲怎么“拖拽设置”更要带你从零理解原理并亲手写一个C#脚本将任何一张普通的全景图那种360°环绕的“长条图”自动转换成Unity能直接使用的Cubemap。整个过程争取让你在5分钟内看到效果并真正搞懂背后的逻辑。2. 核心原理拆解从全景图到Cubemap的魔法在动手之前我们得先搞清楚要解决的核心问题是什么。你从网上下载的或者自己渲染的漂亮天空背景通常是一种叫做“等距柱状投影”的全景图它看起来像一张被严重拉伸的世界地图长宽比是2:1。这种格式虽然能完整记录360°x180°的环境信息但Unity的天空盒材质球并不能直接使用它。2.1 为什么Unity需要CubemapUnity的渲染引擎在处理天空这种无限远的环境时采用了一种高效且视觉上更自然的方法Cubemap。想象一下你站在一个绝对光滑的立方体房间正中央房间的六面墙上画着无缝衔接的风景。无论你朝哪个方向看看到的都是对应墙上的画面。这就是Cubemap的视觉原理。它对GPU非常友好因为根据摄像机的观察方向可以快速计算出应该采样立方体的哪个面以及面上的哪个像素。而2:1的全景图可以理解为将这个立方体“剥开并压平”。将立方体的六个面以特定的布局展开成一张平面图。我们的目标就是通过脚本逆向完成这个“压平”的过程把全景图重新“折叠”回立方体的六个面。2.2 转换算法的思路解析转换的核心是坐标映射。我们有一张全景图输入需要生成六张正方形图片输出即Cubemap的六个面。定义立方体面我们为立方体的六个面右、左、上、下、前、后分别创建一张空白的纹理。遍历输出像素对于输出Cubemap的每一个面比如“右”面上的每一个像素点x, y我们将其归一化到[-1, 1]的立方体空间坐标。例如“右”面的中心点坐标可能是(1, 0, 0)。方向向量计算这个归一化坐标代表了一个从立方体中心指向该像素点的方向向量。球面坐标转换将这个三维方向向量转换为球面坐标经度和纬度。采样输入全景图利用计算出的经度和纬度换算成输入全景图上的UV坐标横纵坐标。因为全景图是等距柱状投影经度直接对应U横向坐标纬度对应V纵向坐标。像素填充从输入全景图的对应UV坐标处取出颜色值填充到输出Cubemap当前面的当前像素上。这个过程由CPU来完成计算量会很大但幸运的是我们可以利用Unity的Texture2D.SetPixel和GetPixel方法或者更高效的Graphics.CopyTexture配合Shader来实现。为了兼顾理解性和便捷性我们先实现一个基于CPU的、易于理解的版本它足以处理新手遇到的大部分非实时生成的需求。3. 实战手把手编写全景图转Cubemap脚本理论可能有点绕我们直接上代码在写的过程中理解。在Unity项目中创建一个新的C#脚本命名为PanoramaToCubemap。3.1 脚本基础结构与参数首先我们需要定义脚本的输入和输出参数。这个脚本将作为一个编辑器工具来使用。using UnityEngine; using UnityEditor; // 因为要用到Editor相关功能 using System.IO; public class PanoramaToCubemap : EditorWindow { // 输入全景图纹理 private Texture2D panoramaTexture; // 输出Cubemap每条边的像素尺寸 private int faceSize 1024; // 输出路径 private string outputPath Assets/CubemapOutput; // 添加一个菜单项到Unity编辑器的“Window”菜单下 [MenuItem(Window/工具/全景图转Cubemap)] static void Init() { // 获取或创建一个编辑器窗口实例 PanoramaToCubemap window (PanoramaToCubemap)EditorWindow.GetWindow(typeof(PanoramaToCubemap)); window.Show(); } // 绘制编辑器窗口的GUI void OnGUI() { GUILayout.Label(全景图转Cubemap工具, EditorStyles.boldLabel); panoramaTexture (Texture2D)EditorGUILayout.ObjectField(全景图, panoramaTexture, typeof(Texture2D), false); faceSize EditorGUILayout.IntField(每个面尺寸, faceSize); outputPath EditorGUILayout.TextField(输出路径, outputPath); if (GUILayout.Button(生成Cubemap)) { if (panoramaTexture ! null) { ConvertPanoramaToCubemap(); } else { EditorUtility.DisplayDialog(错误, 请先选择一张全景图纹理, 确定); } } } }这段代码创建了一个简单的编辑器窗口让你可以选择一张全景图设置生成的面片尺寸和输出路径。faceSize我默认设为1024这是一个比较通用的尺寸清晰度足够性能开销也可接受。你可以根据源全景图的分辨率进行调整通常不超过源图宽度的一半。3.2 核心转换函数的实现接下来是重头戏ConvertPanoramaToCubemap函数。我们将按照之前讲的思路一步步实现。private void ConvertPanoramaToCubemap() { // 1. 检查并创建输出目录 if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } // 2. 定义立方体六个面的方向向量右、左、上、下、前、后 Vector3[] faceDirections { new Vector3(1, 0, 0), // 右 (Right, X) new Vector3(-1, 0, 0), // 左 (Left, -X) new Vector3(0, 1, 0), // 上 (Up, Y) new Vector3(0, -1, 0), // 下 (Down, -Y) new Vector3(0, 0, 1), // 前 (Front, Z) new Vector3(0, 0, -1) // 后 (Back, -Z) }; string[] faceNames { right, left, up, down, front, back }; // 3. 为了计算方便预计算一些值 float invFaceSize 1.0f / faceSize; float panoramaWidth panoramaTexture.width; float panoramaHeight panoramaTexture.height; // 4. 循环处理每一个面 for (int face 0; face 6; face) { // 为当前面创建一张新的纹理 Texture2D faceTexture new Texture2D(faceSize, faceSize, TextureFormat.RGB24, false); // 5. 遍历这个面上的每一个像素 for (int y 0; y faceSize; y) { for (int x 0; x faceSize; x) { // 5.1 将像素坐标归一化到[-1, 1]范围以面中心为原点 float normX (x * invFaceSize) * 2.0f - 1.0f; float normY (y * invFaceSize) * 2.0f - 1.0f; // 5.2 根据当前是哪个面结合归一化坐标构造方向向量 Vector3 direction Vector3.zero; switch (face) { case 0: // 右 (X) direction new Vector3(1.0f, -normY, -normX).normalized; break; case 1: // 左 (-X) direction new Vector3(-1.0f, -normY, normX).normalized; break; case 2: // 上 (Y) direction new Vector3(normX, 1.0f, normY).normalized; break; case 3: // 下 (-Y) direction new Vector3(normX, -1.0f, -normY).normalized; break; case 4: // 前 (Z) direction new Vector3(normX, -normY, 1.0f).normalized; break; case 5: // 后 (-Z) direction new Vector3(-normX, -normY, -1.0f).normalized; break; } // 5.3 将方向向量转换为球面坐标经度、纬度 // 计算经度0到2π和纬度-π/2到π/2 float longitude Mathf.Atan2(direction.z, direction.x); // 注意这里用z和x计算经度对应全景图横向 float latitude Mathf.Asin(direction.y); // 5.4 将球面坐标映射到全景图的UV坐标 // 经度从[-π, π]映射到[0, 1] float u 0.5f longitude / (2.0f * Mathf.PI); // 纬度从[-π/2, π/2]映射到[1, 0]因为纹理V坐标原点在顶部 float v 0.5f - latitude / Mathf.PI; // 5.5 根据UV坐标采样全景图颜色 int srcX Mathf.FloorToInt(u * panoramaWidth); int srcY Mathf.FloorToInt(v * panoramaHeight); // 防止数组越界 srcX Mathf.Clamp(srcX, 0, panoramaTexture.width - 1); srcY Mathf.Clamp(srcY, 0, panoramaTexture.height - 1); Color pixelColor panoramaTexture.GetPixel(srcX, srcY); // 5.6 将颜色设置到Cubemap当前面的纹理上 // 注意Texture2D的坐标系原点在左下角我们循环的y是从下到上的所以直接设置 faceTexture.SetPixel(x, y, pixelColor); } } // 6. 应用纹理更改并保存为PNG文件 faceTexture.Apply(); byte[] bytes faceTexture.EncodeToPNG(); string facePath Path.Combine(outputPath, $cubemap_{faceNames[face]}.png); File.WriteAllBytes(facePath, bytes); Debug.Log($已生成面: {facePath}); // 销毁临时纹理对象避免内存泄漏 DestroyImmediate(faceTexture); } // 7. 所有面生成完成后刷新资源管理器让Unity识别新文件 AssetDatabase.Refresh(); EditorUtility.DisplayDialog(完成, $Cubemap六张面图已生成至{outputPath}, 确定); }注意这个算法是一个基础实现。在switch语句中构造方向向量时对不同的面进行了不同的坐标交换和取反操作这是为了匹配立方体面的朝向与Unity的坐标系。你可以把这部分理解为一个“映射表”确保从立方体面上某一点发出的射线能正确地对接到全景图上的对应点。3.3 使用脚本生成你的第一个Cubemap将脚本放在项目的Assets/Editor文件夹下如果没有就创建一个。这是Unity的约定放在此文件夹下的脚本会被识别为编辑器脚本。在Unity编辑器顶部菜单栏点击Window - 工具 - 全景图转Cubemap打开工具窗口。从Project视图拖拽一张2:1比例的全景图纹理到“全景图”字段。点击“生成Cubemap”按钮。等待片刻在控制台看到6条生成日志后去你设置的输出路径默认是Assets/CubemapOutput就能看到生成的6张正方形图片分别以cubemap_right/left/up/down/front/back.png命名。4. 在Unity中创建并使用天空盒材质生成了六张图我们只是完成了原料的准备。接下来需要在Unity中把它们组装成一个真正的Cubemap资源并应用到天空盒上。4.1 创建Cubemap资源在Project视图的Assets/CubemapOutput文件夹内全选生成的六张图片。在Inspector面板中将“Texture Shape”从默认的2D改为Cube。这是最关键的一步告诉Unity这是一组立方体贴图。点击“Apply”按钮。现在你可以将这六张图中的任意一张拖到场景中Unity会自动将其识别为一个Cubemap资源。或者你也可以在选中状态时在Inspector顶部看到它已经是一个Cubemap了。实操心得有时候直接修改Texture Shape并Apply后预览图可能还是2D的样子。不用担心只要Inspector显示Texture Shape: Cube就是成功了。更稳妥的做法是在修改前先在导入设置里把“Wrap Mode”设为Clamp避免接缝处出现采样错误然后一次性修改Texture Shape并Apply。4.2 创建天空盒材质并应用在Project视图右键 -Create - Material创建一个新材质球命名为MySkybox。选中这个材质球在Inspector面板顶部将Shader从Standard改为Skybox - Cubemap。你会发现材质球的属性变成了一个Cubemap的插槽。将我们刚才创建好的Cubemap资源就是那六张图里的任意一张拖拽到材质球的Cubemap插槽中。Tint Color和Exposure属性可以保持默认或者根据画面效果微调。最后一步将这个材质球应用到场景的天空盒上。有两种方法方法一全局设置点击菜单栏Window - Rendering - Lighting打开Lighting面板。切换到Environment标签页将Skybox Material拖拽成我们刚创建的MySkybox材质。这是最常用、影响范围最广的方法。方法二摄像机覆盖选中主摄像机在Inspector中找到Camera组件有一个Clear Flags选项确保其为Skybox。然后下方会出现Background Type选择Skybox并将MySkybox材质拖入。这种方法只影响该摄像机渲染的画面适合用于画中画、UI摄像机或者特殊效果。现在运行你的游戏或是在Scene视图里旋转视角应该就能看到自定义的天空环境了。如果天空是纯色或错乱请回到第4.1步检查Cubemap资源是否正确创建。5. 常见问题、优化与高级技巧第一次尝试难免会遇到各种问题。这里我整理了几个最常见的坑和对应的解决方案。5.1 问题排查速查表问题现象可能原因解决方案天空一片纯色如粉色、蓝色1. Cubemap资源未正确创建Texture Shape不是Cube。2. 天空盒材质Shader选错。1. 确认六张图导入设置中Texture Shape为Cube并已Apply。2. 确认材质球Shader为Skybox/Cubemap。天空显示错乱、撕裂1. 六张图方向或命名不对应。2. 全景图本身非标准2:1等距柱状投影。3. 脚本中方向向量映射有误。1. 检查生成图片的命名与脚本中faceNames顺序是否一致。2. 尝试使用另一张确认可用的全景图测试。3. 尝试交换脚本中faceDirections的顺序或调整switch内的向量计算。天空有黑色接缝Cubemap的“Wrap Mode”未设置为Clamp。选中Cubemap资源在Inspector中将Wrap Mode从Repeat改为Clamp然后Apply。转换脚本运行报错或卡死1. 全景图尺寸过大。2. 输出面尺寸faceSize设置过大。1. 尝试缩小全景图尺寸如2048x1024。2. 将faceSize设为512或256试试。CPU逐像素计算很耗时。天空盒边缘有模糊或锯齿1. 源全景图分辨率太低。2. 生成的Cubemap面尺寸太小。1. 寻找更高分辨率的源图片。2. 适当提高faceSize如2048但会增大内存和转换时间。5.2 性能与质量优化我们上面写的脚本是CPU逐像素计算的当faceSize较大时如2048转换一张图可能需要几十秒并且会阻塞主线程。对于需要频繁转换或处理大图的情况有更优的方案使用Compute Shader或Graphics.Blit这是推荐给进阶用户的高性能方法。原理是将坐标映射的计算放到GPU上并行执行速度极快。你需要编写一个Shader接收全景图作为输入渲染到一个RenderTexture上而这个RenderTexture被配置为包含6个面的Cubemap。网上有大量开源实现搜索“Panorama to Cubemap Shader”可以找到。使用现成的插件或在线工具如果你不想折腾代码Asset Store上有不少成熟的转换插件如“Cubemap Generator”。也有一些免费的在线转换网站上传全景图可以直接下载六面图。预处理是关键对于项目中的静态天空盒最好的实践是在项目外部预处理。使用专业的图像处理软件如Photoshop的插件或开源的libvips命令行工具提前生成好Cubemap六面图然后直接导入Unity使用。这能保证最好的质量和零运行时开销。5.3 让天空盒“活”起来动态天空与混合掌握了静态天空盒后你可以尝试更酷的效果动态天空盒通过脚本在运行时修改天空盒材质的Cubemap资源可以实现昼夜交替、天气变化晴空万里到乌云密布。你需要准备多套不同时间/天气的Cubemap然后用RenderSettings.skybox.SetTexture属性进行切换并配合Color.Lerp来平滑过渡Tint Color。天空盒混合Unity的Skybox/Cubemap BlendedShader允许你混合两个Cubemap并通过一个Blend参数控制混合度。这非常适合做平滑的天气过渡效果。程序化天空盒完全通过Shader代码实时生成天空、云层、日月星辰。这需要较强的图形学知识和Shader编程能力但效果也最灵活、最震撼。Unity的Asset Store里也有相关的插件可供学习。从一张全景图到最终渲染在屏幕上的沉浸天空这个过程打通了资源准备、数据转换、引擎配置的完整链条。理解Cubemap的原理能让你在未来处理环境反射、镜面效果等需要环境贴图的场合时更加得心应手。这个5分钟的小工具希望不只是帮你换了个天空更是打开了一扇理解3D图形渲染中环境表达的大门。
Unity天空盒制作:从全景图到Cubemap的完整实现
1. 项目概述为什么天空盒是新手绕不开的第一课如果你是刚接触Unity3D的新手在捣鼓场景搭建时大概率会遇到一个共同的困惑为什么我的场景看起来这么“假”明明模型摆好了灯光也打上了但整个环境就是缺乏沉浸感像一个悬浮在虚空中的孤岛。这个问题的答案往往就出在“天空盒”上。天空盒你可以把它想象成一个包裹住整个游戏世界的巨大立方体盒子盒子的内壁贴上了天空、远山、云层等环境图像。它决定了玩家视野中除了你亲手放置的物体之外所有背景区域呈现什么样子。一个合适的天空盒能瞬间将你的场景从“白模测试”状态拉入到一个有氛围、有故事感的世界里。很多新手教程会直接告诉你“拖一个天空盒材质球到渲染设置里”但知其然更要知其所以然。为什么Unity默认的天空盒选项那么少网上找到的漂亮全景图为什么拖进去不是一片黑就是严重变形这背后涉及的核心技术点就是“Cubemap”。简单来说Cubemap是一套由六张正方形图片组成的资源分别对应立方体的前、后、左、右、上、下六个面。Unity的天空盒系统正是基于Cubemap来工作的。理解了这一点你就掌握了自定义天空盒的钥匙。今天我们不只讲怎么“拖拽设置”更要带你从零理解原理并亲手写一个C#脚本将任何一张普通的全景图那种360°环绕的“长条图”自动转换成Unity能直接使用的Cubemap。整个过程争取让你在5分钟内看到效果并真正搞懂背后的逻辑。2. 核心原理拆解从全景图到Cubemap的魔法在动手之前我们得先搞清楚要解决的核心问题是什么。你从网上下载的或者自己渲染的漂亮天空背景通常是一种叫做“等距柱状投影”的全景图它看起来像一张被严重拉伸的世界地图长宽比是2:1。这种格式虽然能完整记录360°x180°的环境信息但Unity的天空盒材质球并不能直接使用它。2.1 为什么Unity需要CubemapUnity的渲染引擎在处理天空这种无限远的环境时采用了一种高效且视觉上更自然的方法Cubemap。想象一下你站在一个绝对光滑的立方体房间正中央房间的六面墙上画着无缝衔接的风景。无论你朝哪个方向看看到的都是对应墙上的画面。这就是Cubemap的视觉原理。它对GPU非常友好因为根据摄像机的观察方向可以快速计算出应该采样立方体的哪个面以及面上的哪个像素。而2:1的全景图可以理解为将这个立方体“剥开并压平”。将立方体的六个面以特定的布局展开成一张平面图。我们的目标就是通过脚本逆向完成这个“压平”的过程把全景图重新“折叠”回立方体的六个面。2.2 转换算法的思路解析转换的核心是坐标映射。我们有一张全景图输入需要生成六张正方形图片输出即Cubemap的六个面。定义立方体面我们为立方体的六个面右、左、上、下、前、后分别创建一张空白的纹理。遍历输出像素对于输出Cubemap的每一个面比如“右”面上的每一个像素点x, y我们将其归一化到[-1, 1]的立方体空间坐标。例如“右”面的中心点坐标可能是(1, 0, 0)。方向向量计算这个归一化坐标代表了一个从立方体中心指向该像素点的方向向量。球面坐标转换将这个三维方向向量转换为球面坐标经度和纬度。采样输入全景图利用计算出的经度和纬度换算成输入全景图上的UV坐标横纵坐标。因为全景图是等距柱状投影经度直接对应U横向坐标纬度对应V纵向坐标。像素填充从输入全景图的对应UV坐标处取出颜色值填充到输出Cubemap当前面的当前像素上。这个过程由CPU来完成计算量会很大但幸运的是我们可以利用Unity的Texture2D.SetPixel和GetPixel方法或者更高效的Graphics.CopyTexture配合Shader来实现。为了兼顾理解性和便捷性我们先实现一个基于CPU的、易于理解的版本它足以处理新手遇到的大部分非实时生成的需求。3. 实战手把手编写全景图转Cubemap脚本理论可能有点绕我们直接上代码在写的过程中理解。在Unity项目中创建一个新的C#脚本命名为PanoramaToCubemap。3.1 脚本基础结构与参数首先我们需要定义脚本的输入和输出参数。这个脚本将作为一个编辑器工具来使用。using UnityEngine; using UnityEditor; // 因为要用到Editor相关功能 using System.IO; public class PanoramaToCubemap : EditorWindow { // 输入全景图纹理 private Texture2D panoramaTexture; // 输出Cubemap每条边的像素尺寸 private int faceSize 1024; // 输出路径 private string outputPath Assets/CubemapOutput; // 添加一个菜单项到Unity编辑器的“Window”菜单下 [MenuItem(Window/工具/全景图转Cubemap)] static void Init() { // 获取或创建一个编辑器窗口实例 PanoramaToCubemap window (PanoramaToCubemap)EditorWindow.GetWindow(typeof(PanoramaToCubemap)); window.Show(); } // 绘制编辑器窗口的GUI void OnGUI() { GUILayout.Label(全景图转Cubemap工具, EditorStyles.boldLabel); panoramaTexture (Texture2D)EditorGUILayout.ObjectField(全景图, panoramaTexture, typeof(Texture2D), false); faceSize EditorGUILayout.IntField(每个面尺寸, faceSize); outputPath EditorGUILayout.TextField(输出路径, outputPath); if (GUILayout.Button(生成Cubemap)) { if (panoramaTexture ! null) { ConvertPanoramaToCubemap(); } else { EditorUtility.DisplayDialog(错误, 请先选择一张全景图纹理, 确定); } } } }这段代码创建了一个简单的编辑器窗口让你可以选择一张全景图设置生成的面片尺寸和输出路径。faceSize我默认设为1024这是一个比较通用的尺寸清晰度足够性能开销也可接受。你可以根据源全景图的分辨率进行调整通常不超过源图宽度的一半。3.2 核心转换函数的实现接下来是重头戏ConvertPanoramaToCubemap函数。我们将按照之前讲的思路一步步实现。private void ConvertPanoramaToCubemap() { // 1. 检查并创建输出目录 if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } // 2. 定义立方体六个面的方向向量右、左、上、下、前、后 Vector3[] faceDirections { new Vector3(1, 0, 0), // 右 (Right, X) new Vector3(-1, 0, 0), // 左 (Left, -X) new Vector3(0, 1, 0), // 上 (Up, Y) new Vector3(0, -1, 0), // 下 (Down, -Y) new Vector3(0, 0, 1), // 前 (Front, Z) new Vector3(0, 0, -1) // 后 (Back, -Z) }; string[] faceNames { right, left, up, down, front, back }; // 3. 为了计算方便预计算一些值 float invFaceSize 1.0f / faceSize; float panoramaWidth panoramaTexture.width; float panoramaHeight panoramaTexture.height; // 4. 循环处理每一个面 for (int face 0; face 6; face) { // 为当前面创建一张新的纹理 Texture2D faceTexture new Texture2D(faceSize, faceSize, TextureFormat.RGB24, false); // 5. 遍历这个面上的每一个像素 for (int y 0; y faceSize; y) { for (int x 0; x faceSize; x) { // 5.1 将像素坐标归一化到[-1, 1]范围以面中心为原点 float normX (x * invFaceSize) * 2.0f - 1.0f; float normY (y * invFaceSize) * 2.0f - 1.0f; // 5.2 根据当前是哪个面结合归一化坐标构造方向向量 Vector3 direction Vector3.zero; switch (face) { case 0: // 右 (X) direction new Vector3(1.0f, -normY, -normX).normalized; break; case 1: // 左 (-X) direction new Vector3(-1.0f, -normY, normX).normalized; break; case 2: // 上 (Y) direction new Vector3(normX, 1.0f, normY).normalized; break; case 3: // 下 (-Y) direction new Vector3(normX, -1.0f, -normY).normalized; break; case 4: // 前 (Z) direction new Vector3(normX, -normY, 1.0f).normalized; break; case 5: // 后 (-Z) direction new Vector3(-normX, -normY, -1.0f).normalized; break; } // 5.3 将方向向量转换为球面坐标经度、纬度 // 计算经度0到2π和纬度-π/2到π/2 float longitude Mathf.Atan2(direction.z, direction.x); // 注意这里用z和x计算经度对应全景图横向 float latitude Mathf.Asin(direction.y); // 5.4 将球面坐标映射到全景图的UV坐标 // 经度从[-π, π]映射到[0, 1] float u 0.5f longitude / (2.0f * Mathf.PI); // 纬度从[-π/2, π/2]映射到[1, 0]因为纹理V坐标原点在顶部 float v 0.5f - latitude / Mathf.PI; // 5.5 根据UV坐标采样全景图颜色 int srcX Mathf.FloorToInt(u * panoramaWidth); int srcY Mathf.FloorToInt(v * panoramaHeight); // 防止数组越界 srcX Mathf.Clamp(srcX, 0, panoramaTexture.width - 1); srcY Mathf.Clamp(srcY, 0, panoramaTexture.height - 1); Color pixelColor panoramaTexture.GetPixel(srcX, srcY); // 5.6 将颜色设置到Cubemap当前面的纹理上 // 注意Texture2D的坐标系原点在左下角我们循环的y是从下到上的所以直接设置 faceTexture.SetPixel(x, y, pixelColor); } } // 6. 应用纹理更改并保存为PNG文件 faceTexture.Apply(); byte[] bytes faceTexture.EncodeToPNG(); string facePath Path.Combine(outputPath, $cubemap_{faceNames[face]}.png); File.WriteAllBytes(facePath, bytes); Debug.Log($已生成面: {facePath}); // 销毁临时纹理对象避免内存泄漏 DestroyImmediate(faceTexture); } // 7. 所有面生成完成后刷新资源管理器让Unity识别新文件 AssetDatabase.Refresh(); EditorUtility.DisplayDialog(完成, $Cubemap六张面图已生成至{outputPath}, 确定); }注意这个算法是一个基础实现。在switch语句中构造方向向量时对不同的面进行了不同的坐标交换和取反操作这是为了匹配立方体面的朝向与Unity的坐标系。你可以把这部分理解为一个“映射表”确保从立方体面上某一点发出的射线能正确地对接到全景图上的对应点。3.3 使用脚本生成你的第一个Cubemap将脚本放在项目的Assets/Editor文件夹下如果没有就创建一个。这是Unity的约定放在此文件夹下的脚本会被识别为编辑器脚本。在Unity编辑器顶部菜单栏点击Window - 工具 - 全景图转Cubemap打开工具窗口。从Project视图拖拽一张2:1比例的全景图纹理到“全景图”字段。点击“生成Cubemap”按钮。等待片刻在控制台看到6条生成日志后去你设置的输出路径默认是Assets/CubemapOutput就能看到生成的6张正方形图片分别以cubemap_right/left/up/down/front/back.png命名。4. 在Unity中创建并使用天空盒材质生成了六张图我们只是完成了原料的准备。接下来需要在Unity中把它们组装成一个真正的Cubemap资源并应用到天空盒上。4.1 创建Cubemap资源在Project视图的Assets/CubemapOutput文件夹内全选生成的六张图片。在Inspector面板中将“Texture Shape”从默认的2D改为Cube。这是最关键的一步告诉Unity这是一组立方体贴图。点击“Apply”按钮。现在你可以将这六张图中的任意一张拖到场景中Unity会自动将其识别为一个Cubemap资源。或者你也可以在选中状态时在Inspector顶部看到它已经是一个Cubemap了。实操心得有时候直接修改Texture Shape并Apply后预览图可能还是2D的样子。不用担心只要Inspector显示Texture Shape: Cube就是成功了。更稳妥的做法是在修改前先在导入设置里把“Wrap Mode”设为Clamp避免接缝处出现采样错误然后一次性修改Texture Shape并Apply。4.2 创建天空盒材质并应用在Project视图右键 -Create - Material创建一个新材质球命名为MySkybox。选中这个材质球在Inspector面板顶部将Shader从Standard改为Skybox - Cubemap。你会发现材质球的属性变成了一个Cubemap的插槽。将我们刚才创建好的Cubemap资源就是那六张图里的任意一张拖拽到材质球的Cubemap插槽中。Tint Color和Exposure属性可以保持默认或者根据画面效果微调。最后一步将这个材质球应用到场景的天空盒上。有两种方法方法一全局设置点击菜单栏Window - Rendering - Lighting打开Lighting面板。切换到Environment标签页将Skybox Material拖拽成我们刚创建的MySkybox材质。这是最常用、影响范围最广的方法。方法二摄像机覆盖选中主摄像机在Inspector中找到Camera组件有一个Clear Flags选项确保其为Skybox。然后下方会出现Background Type选择Skybox并将MySkybox材质拖入。这种方法只影响该摄像机渲染的画面适合用于画中画、UI摄像机或者特殊效果。现在运行你的游戏或是在Scene视图里旋转视角应该就能看到自定义的天空环境了。如果天空是纯色或错乱请回到第4.1步检查Cubemap资源是否正确创建。5. 常见问题、优化与高级技巧第一次尝试难免会遇到各种问题。这里我整理了几个最常见的坑和对应的解决方案。5.1 问题排查速查表问题现象可能原因解决方案天空一片纯色如粉色、蓝色1. Cubemap资源未正确创建Texture Shape不是Cube。2. 天空盒材质Shader选错。1. 确认六张图导入设置中Texture Shape为Cube并已Apply。2. 确认材质球Shader为Skybox/Cubemap。天空显示错乱、撕裂1. 六张图方向或命名不对应。2. 全景图本身非标准2:1等距柱状投影。3. 脚本中方向向量映射有误。1. 检查生成图片的命名与脚本中faceNames顺序是否一致。2. 尝试使用另一张确认可用的全景图测试。3. 尝试交换脚本中faceDirections的顺序或调整switch内的向量计算。天空有黑色接缝Cubemap的“Wrap Mode”未设置为Clamp。选中Cubemap资源在Inspector中将Wrap Mode从Repeat改为Clamp然后Apply。转换脚本运行报错或卡死1. 全景图尺寸过大。2. 输出面尺寸faceSize设置过大。1. 尝试缩小全景图尺寸如2048x1024。2. 将faceSize设为512或256试试。CPU逐像素计算很耗时。天空盒边缘有模糊或锯齿1. 源全景图分辨率太低。2. 生成的Cubemap面尺寸太小。1. 寻找更高分辨率的源图片。2. 适当提高faceSize如2048但会增大内存和转换时间。5.2 性能与质量优化我们上面写的脚本是CPU逐像素计算的当faceSize较大时如2048转换一张图可能需要几十秒并且会阻塞主线程。对于需要频繁转换或处理大图的情况有更优的方案使用Compute Shader或Graphics.Blit这是推荐给进阶用户的高性能方法。原理是将坐标映射的计算放到GPU上并行执行速度极快。你需要编写一个Shader接收全景图作为输入渲染到一个RenderTexture上而这个RenderTexture被配置为包含6个面的Cubemap。网上有大量开源实现搜索“Panorama to Cubemap Shader”可以找到。使用现成的插件或在线工具如果你不想折腾代码Asset Store上有不少成熟的转换插件如“Cubemap Generator”。也有一些免费的在线转换网站上传全景图可以直接下载六面图。预处理是关键对于项目中的静态天空盒最好的实践是在项目外部预处理。使用专业的图像处理软件如Photoshop的插件或开源的libvips命令行工具提前生成好Cubemap六面图然后直接导入Unity使用。这能保证最好的质量和零运行时开销。5.3 让天空盒“活”起来动态天空与混合掌握了静态天空盒后你可以尝试更酷的效果动态天空盒通过脚本在运行时修改天空盒材质的Cubemap资源可以实现昼夜交替、天气变化晴空万里到乌云密布。你需要准备多套不同时间/天气的Cubemap然后用RenderSettings.skybox.SetTexture属性进行切换并配合Color.Lerp来平滑过渡Tint Color。天空盒混合Unity的Skybox/Cubemap BlendedShader允许你混合两个Cubemap并通过一个Blend参数控制混合度。这非常适合做平滑的天气过渡效果。程序化天空盒完全通过Shader代码实时生成天空、云层、日月星辰。这需要较强的图形学知识和Shader编程能力但效果也最灵活、最震撼。Unity的Asset Store里也有相关的插件可供学习。从一张全景图到最终渲染在屏幕上的沉浸天空这个过程打通了资源准备、数据转换、引擎配置的完整链条。理解Cubemap的原理能让你在未来处理环境反射、镜面效果等需要环境贴图的场合时更加得心应手。这个5分钟的小工具希望不只是帮你换了个天空更是打开了一扇理解3D图形渲染中环境表达的大门。