公司动态

Unity全景图渲染实战:从天空盒到自定义着色器的完整实现

📅 2026/7/26 22:22:53
Unity全景图渲染实战:从天空盒到自定义着色器的完整实现
1. 项目概述从天空盒子到全景图预览的思维跃迁在Unity开发中尤其是涉及虚拟漫游、数字孪生、VR/AR内容展示时环境背景的呈现是沉浸感的第一道门槛。长久以来很多开发者包括我自己在早期项目里都习惯性地将“天空盒子”作为环境背景的唯一解。一个立方体贴图拖到渲染设置里简单粗暴效果也还过得去。但当你需要展示一个真实的室内空间、一个博物馆的360度展厅或者一个由单张全景照片构建的虚拟场景时天空盒子的局限性就暴露无遗它无法处理由单张等距柱状投影全景图Equirectangular Panorama所描述的、真正“包裹”住观察者的球面环境。这个项目要解决的正是这个痛点。我将手把手带你实现两种在Unity 2022.3 LTS中预览标准全景图的方案。这不仅仅是替换一个天空盒材质那么简单它涉及到对UV映射、球面坐标、着色器编程等核心图形学概念的深入理解。第一种方案我们将改造内置的“Skybox/Panoramic”着色器让它更灵活、更高效第二种方案则从零开始编写一个自定义的Unlit Shader让你彻底掌握全景图渲染的底层逻辑。我会提供完整的、可即用的Shader代码并解释每一行关键代码背后的“为什么”。无论你是想为你的VR应用快速集成全景导览还是希望深入理解Unity的渲染管线这篇文章都能给你带来直接的、可复现的价值。2. 全景图渲染的核心原理与方案选型在动手写代码之前我们必须搞清楚我们要渲染的对象是什么以及Unity是如何“看”世界的。这决定了我们方案的技术路径。2.1 全景图格式与观察者视角我们通常所说的“全景图”绝大多数情况下指的是等距柱状投影全景图。你可以把它想象成一张世界地图将整个球面环境“剥开”并拉伸成一张2:1比例的矩形图片。水平方向U轴代表经度0°到360°垂直方向V轴代表纬度-90°到90°。当我们身处这个球体的中心时看向任何一个方向看到的都应该是这张矩形图上对应坐标点的颜色。在Unity中默认的摄像机渲染的是一个视锥体Frustum内的场景。天空盒子Skybox是一种特殊的渲染它在所有不透明物体之后、在所有透明物体之前以一个巨大的立方体或球体的形式将纹理渲染到整个屏幕的背景上。标准的“Skybox/Cubemap”使用六张图构成一个立方体贴图而“Skybox/Panoramic”则尝试使用一张2:1的全景图来模拟这个立方体。2.2 方案一优化内置全景天空盒的利弊Unity内置的Skybox/Panoramic着色器是一个很好的起点。它的原理是在顶点着色器阶段将模型顶点转换到世界空间然后通过方向向量采样一张Cubemap。而它的“Panoramic”变体则通过一个数学函数将方向向量转换为等距柱状投影的UV坐标从而采样2:1的全景纹理。选择此方案的理由快速集成无需编写任何C#脚本只需创建一个材质球选择该着色器拖入全景图即可。对于原型验证或简单展示需求效率极高。兼容性好作为内置着色器它与Unity的渲染管线无论是内置管线还是URP/HDRP通常有对应版本有良好的兼容性减少了因版本升级导致的适配问题。基础功能完善提供了曝光Exposure、旋转Rotation、色调映射Tint等基本控制参数。其局限性及我们的优化方向性能黑盒内置着色器可能包含一些我们不需要的特性分支对于移动端或WebGL平台可能存在可优化的空间。灵活性不足难以添加自定义效果比如边缘模糊、动态过渡、与场景物体的特殊混合模式等。调试困难当渲染出现问题时如接缝、扭曲由于无法看到其内部计算过程排查问题较为困难。因此我们的“优化”不仅仅是使用而是理解其原理后创建一个更透明、更可控的版本。我们将创建一个新的着色器保留其核心算法但代码结构完全对我们开放便于定制和优化。2.3 方案二自研Unlit全景着色器的优势从零编写一个Unlit Shader来渲染全景图是彻底掌握该技术的必经之路。Unlit意味着它不受场景光照影响只负责输出纹理颜色这正符合背景环境的定位。选择此方案的核心优势绝对控制权从坐标变换到最终采样每一步都清晰可见。你可以精确控制性能消耗例如将计算从片段着色器移至顶点着色器进行插值以换取更高的帧率。深度定制化可以轻松集成多张全景图混合、动态视频全景、根据视角显示不同细节简单版LOD等高级功能。卓越的学习价值通过实现它你将深刻理解视图方向向量、球面坐标、UV映射以及Shader编程的基本范式这是提升图形学能力的绝佳练习。在接下来的部分我们将首先深入方案一的优化实现然后彻底解剖方案二的自研过程。两种方案的完整代码都会提供你可以根据项目需求自由选择或结合使用。3. 方案一实现剖析与优化内置全景天空盒我们首先复现并优化内置着色器的功能。在Unity中创建一个新的Shader文件命名为CustomPanoramicSkybox.shader。3.1 着色器基础结构与属性定义Shader Custom/Skybox/Panoramic { Properties { _MainTex (Panoramic Texture (HDR), 2D) grey {} _Rotation (Rotation, Range(0, 360)) 0 _Exposure (Exposure, Range(0, 8)) 1.0 _Tint (Tint, Color) (.5, .5, .5, 1) [Toggle] _GammaSpace (Gamma Space Sampling, Float) 0 } SubShader { Tags { QueueBackground RenderTypeBackground PreviewTypeSkybox } Cull Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; // 注意对于天空盒ST通常不使用但声明以避免编译警告 float _Rotation; float _Exposure; half4 _Tint; float _GammaSpace; // 顶点着色器输入结构 struct appdata { float4 vertex : POSITION; }; // 顶点着色器输出/片段着色器输入结构 struct v2f { float4 pos : SV_POSITION; float3 viewDir : TEXCOORD0; };代码解析与注意事项Tags中的QueueBackground确保了此物体在所有不透明物体之前渲染ZWrite Off关闭深度写入因为它作为背景不应遮挡任何物体但也不应被写入深度缓冲区干扰后续物体的深度测试。Cull Off关闭背面剔除因为无论摄像机在球体内部看向哪一面我们都希望看到纹理。_MainTex_ST是Unity自动处理纹理缩放偏移的变量虽然天空盒通常不需要但定义它可以避免一些平台上的着色器编译警告。_GammaSpace属性是一个优化点。大部分全景图是sRGB颜色空间的。在Gamma颜色空间下如移动端直接采样即可。在Linear空间下如PC端HDRP我们需要在着色器中手动进行sRGB到Linear的转换或者依赖纹理导入设置。这里我们提供一个开关让开发者根据项目颜色空间设置和纹理导入设置来灵活控制。3.2 核心算法从视图方向到UV坐标这是全景渲染最核心的数学部分发生在顶点着色器或片段着色器中。v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); // 计算从摄像机指向模型顶点的方向世界空间。 // 对于天空盒摄像机在球心模型是一个巨大球体这个方向就是视图方向。 float3 viewDir mul((float3x3)unity_ObjectToWorld, v.vertex.xyz); o.viewDir viewDir; return o; } // 关键函数将标准化后的3D方向向量转换为2D等距柱状投影UV。 float2 DirectionToEquirectUV(float3 dir) { // 1. 计算水平角度经度atan2(z, x)结果范围[-π, π]映射到[0, 1] float u atan2(dir.z, dir.x); u u * (0.5 / UNITY_PI) 0.5; // 从[-π, π]映射到[0, 1] // 2. 计算垂直角度纬度asin(y)结果范围[-π/2, π/2]映射到[1, 0] // 注意纹理V坐标0在上1在下而asin(y)在y1顶部时值为π/2所以需要1-... float v asin(dir.y); v v * (1.0 / UNITY_PI) 0.5; // 从[-π/2, π/2]映射到[0, 1] v 1.0 - v; // 翻转V坐标以匹配纹理坐标系 return float2(u, v); }计算过程详解水平分量uatan2(z, x)函数返回的是从正X轴到向量(x, z)的夹角。这完美对应了经度。将其从弧度制范围[-π, π]线性映射到[0, 1]就得到了UV的U坐标。垂直分量vasin(dir.y)返回的是方向向量与XZ平面的夹角纬度。因为dir是归一化的dir.y范围是[-1, 1]asin结果范围是[-π/2, π/2]。同样线性映射到[0, 1]。关键的翻转操作v 1.0 - v是因为纹理坐标的V轴原点0在顶部而我们在球体顶部y1时期望的V坐标应该是0顶部像素。实操心得很多自实现全景着色器出现“天地颠倒”的问题根源就在于忘记了这一步V坐标翻转。这是一个非常经典的坑。3.3 片段着色器与旋转支持在片段着色器中我们对插值后的视图方向进行归一化应用旋转然后计算UV并采样。half4 frag (v2f i) : SV_Target { // 归一化视图方向 float3 viewDir normalize(i.viewDir); // 应用旋转绕Y轴旋转视图方向 float rad _Rotation * UNITY_PI / 180.0; // 角度转弧度 float sinRot, cosRot; sincos(rad, sinRot, cosRot); // 绕Y轴旋转矩阵 viewDir.xz float2( viewDir.x * cosRot - viewDir.z * sinRot, viewDir.x * sinRot viewDir.z * cosRot ); // 计算UV float2 uv DirectionToEquirectUV(viewDir); // 采样纹理 half4 col tex2D(_MainTex, uv); // 处理颜色空间简化示例实际需根据项目管线调整 #ifndef UNITY_COLORSPACE_GAMMA if (_GammaSpace 0.5) { // 如果纹理在Gamma空间但项目是Linear需转换 col.rgb GammaToLinearSpace(col.rgb); } #endif // 应用曝光和色调 col.rgb * _Exposure * _Tint.rgb * _Tint.a; // 通常Tint.a为1这里乘以是习惯 return col; } ENDCG } } FallBack Off }旋转实现的原理我们不是在旋转纹理而是旋转“观察方向”。这比在UV空间做旋转更直观且能避免纹理边缘的接缝问题。绕Y轴的旋转矩阵应用于方向向量的XZ分量从而实现在水平方向上环视全景图。注意事项颜色空间处理是一个复杂话题。在Unity的线性渲染管线中默认假设所有输入纹理是线性值。如果你的全景图是sRGB格式如JPEG并且导入设置未勾选“sRGB (Color Texture)”那么你需要在着色器中手动转换。上述代码提供了一个开关但最佳实践是在纹理导入设置中正确设置颜色空间让Unity引擎在加载时自动处理这样着色器代码可以更简洁高效。4. 方案二实现从零构建高性能Unlit全景着色器现在我们抛弃天空盒的思维定式创建一个直接渲染到网格上的Unlit全景着色器。我们将使用一个细分程度足够的球体或立方体作为载体网格。4.1 着色器设计思路与属性创建一个新ShaderUnlitPanoramic.shader。这次我们面向可能附着在场景中某个物体上的情况。Shader Custom/Unlit/Panoramic { Properties { _MainTex (Panoramic Texture, 2D) white {} [Enum(Equirectangular, 0, Cubemap, 1)] _MappingMode (Mapping Mode, Float) 0 _CubeMap (Cubemap (if mode1), Cube) grey {} _Rotation (Rotation, Vector) (0, 0, 0, 0) // Euler angles (X,Y,Z) _MipLevel (Mip Level (Cubemap), Range(0, 10)) 0 [HDR] _Color (Color Tint, Color) (1,1,1,1) _BlendFactor (Blend Factor (for transitions), Range(0, 1)) 1.0 }设计解析_MappingMode增加了映射模式枚举不仅支持等距柱状投影也支持直接采样立方体贴图让着色器更通用。_Rotation使用Vector类型表示欧拉角允许绕三个轴旋转提供更灵活的方向控制。_MipLevel专门为立方体贴图采样设计可以用于实现粗糙反射或简单的模糊效果。_BlendFactor这是一个实用功能用于在多张全景图之间做动态淡入淡出过渡。4.2 顶点与片段着色器的协同优化在方案一中方向计算在顶点着色器进行在片段着色器归一化。这里我们做一个重要的性能优化将方向向量的计算完全放在顶点着色器并利用插值器传递给片段着色器。由于方向向量在球体或立方体表面变化是平滑的线性插值产生的误差对于全景图这种低频内容视觉上几乎不可察觉但可以节省大量片段着色器中的归一化计算。struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; // 使用法线作为方向向量基础更直观 }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); // 直接将模型空间法线转换到世界空间作为视图方向的基础。 // 注意这里假设观察者在网格中心且网格是球体或立方体。 // 对于非中心观察者需要计算从摄像机到顶点的世界空间方向。 o.worldNormal normalize(mul((float3x3)unity_ObjectToWorld, v.normal)); return o; }为什么用法线对于位于原点的球体其表面任意一点的法线方向正是从球心指向该点的方向这与我们在球心观察所需的方向向量一致。计算比用顶点坐标更直接。对于立方体其法线在每一个面上是恒定的这会导致在面上方向不变采样结果也是一片均匀的颜色这正是立方体贴图天空盒的原理。因此使用一个高细分的球体网格作为载体效果最好。4.3 支持多种映射模式的片段着色器片段着色器需要处理两种映射模式并实现旋转。half4 frag (v2f i) : SV_Target { float3 viewDir i.worldNormal; // 已从顶点着色器归一化并插值 // 应用欧拉角旋转 (绕Z, X, Y轴顺序常见顺序) float3 rot _Rotation.xyz * UNITY_PI / 180.0; // 构建旋转矩阵 (简化表示实际应使用矩阵乘法或四元数) // 这里为了清晰使用连续的轴角旋转函数伪代码需实现 viewDir RotateAroundAxis(viewDir, float3(0,0,1), rot.z); // Z viewDir RotateAroundAxis(viewDir, float3(1,0,0), rot.x); // X viewDir RotateAroundAxis(viewDir, float3(0,1,0), rot.y); // Y half4 col; if (_MappingMode 0.5) // Equirectangular Mode { float2 uv DirectionToEquirectUV(viewDir); col tex2D(_MainTex, uv); } else // Cubemap Mode { col texCUBElod(_CubeMap, float4(viewDir, _MipLevel)); } col.rgb * _Color.rgb * _BlendFactor; col.a 1.0; // 确保不透明 return col; }关于旋转的实现在实际代码中你需要实现RotateAroundAxis函数或者更高效地在C#脚本中计算好旋转矩阵或四元数通过Material.SetMatrix或Material.SetVector传递到着色器。在片段着色器中进行多次向量旋转计算开销较大对于性能敏感的场景建议在CPU端计算好最终方向或旋转矩阵。texCUBElod的用途使用lod版本可以在着色器中指定mipmap级别_MipLevel为0时采样最精细的级别。这可以用来实现性能分级远处用低mip或特定的模糊效果。4.4 在场景中的使用与材质配置创建载体在场景中创建一个Sphere球体将其Scale设置为一个较大的值如100确保它包裹住整个场景。或者使用一个细分级别很高的球体预制体以获得更好的视觉效果。创建材质使用我们编写的Custom/Unlit/Panoramic着色器创建新材质。配置材质将你的全景图拖入_MainTex。确保纹理的Wrap Mode设置为Clamp或Repeat根据需求通常Repeat用于水平无缝衔接的全景图。将Mapping Mode设为Equirectangular。赋予物体将该材质赋予给场景中的球体。调整位置确保摄像机位于球体的中心位置。你可以将摄像机设为球体的子物体或者通过脚本保证其相对位置。实操心得解决接缝问题有时在UV的0和1边界即经度360°处会出现细微的颜色接缝。这是因为浮点数精度问题atan2在边界处可能产生跳跃。解决方法是在片段着色器采样时对U坐标进行一个微小的偏移和钳制或者更推荐的是确保你的全景图在Photoshop等工具中处理过使其左右边缘在色彩和亮度上是连续无缝的。5. 两种方案的性能对比与适用场景分析实现之后我们需要知道在什么情况下选择哪种方案。方案一优化天空盒的优势渲染顺序作为Skybox它在所有几何体之后渲染不会产生overdraw除非全屏理论上效率更高。使用便捷直接拖入Lighting窗口的Skybox Material即可无需在场景中放置实体网格。与光照系统集成可以作为环境光照的来源影响场景物体的反射和漫反射如果开启了全局光照。方案二Unlit网格的优势灵活性可以将其附着在任何物体上不一定是背景。你可以创建一个漂浮的“全景球”让玩家观察或者用于画中画显示。深度交互由于它是一个场景中的实体网格你可以为其添加Collider实现点击交互如点击全景图某个区域触发事件。高级效果更容易与后期处理、自定义混合如与场景几何体做特殊混合结合。性能可控网格的顶点数量完全由你控制。一个低精度的球体用于远景背景可以极大降低顶点处理开销。性能数据参考粗略估算方案一天空盒通常渲染1个全屏四边形顶点数固定为4片段数等于屏幕像素数。计算开销主要在片段着色器的方向-UV转换和纹理采样。方案二球体网格一个细分级别为100的球体可能有上万个顶点。虽然顶点着色器计算简单但顶点处理开销增大。片段数取决于球体在屏幕上的覆盖面积。如果球体足够大覆盖整个屏幕则片段数与方案一相同但多了顶点处理开销。结论与选型建议纯背景展示追求极致简便和标准环境光照选择方案一天空盒。需要交互、动态切换、与非背景物体混合或作为场景内可观察物体选择方案二Unlit网格。移动端或WebGL等性能敏感平台如果使用方案二务必使用低面数网格如一个立方体或低细分球体并考虑将部分计算如旋转移至顶点着色器。6. 常见问题、调试技巧与进阶扩展在实际使用中你肯定会遇到各种问题。这里记录了我踩过的一些坑和解决方法。6.1 全景图扭曲或拉伸不正确症状画面看起来被压扁或拉长或者南北极区域扭曲严重。排查步骤检查纹理比例确保你的全景图是标准的2:1比例宽度是高度的两倍。如果不是UV映射公式将失效。检查UV计算函数重点检查DirectionToEquirectUV函数中的asin和atan2参数顺序以及V坐标的翻转1.0 - v是否正确。检查方向向量在片段着色器中将计算出的viewDir或最终的uv输出为颜色可视化检查。例如return float4(viewDir, 1.0);可以看到一个彩色的方向球。如果方向球看起来不对称或有问题说明顶点着色器传递的方向错了。6.2 画面出现接缝症状在水平方向0°和360°交界处有一条明显的颜色或亮度差异线。解决方案源头解决使用专业的全景图缝合软件如PTGui输出并确保启用“混合边缘”选项。着色器容错在采样前对U坐标进行微调。例如float seamWidth 0.001; // 一个很小的值 if (uv.x seamWidth) uv.x 1.0 - (seamWidth - uv.x); else if (uv.x 1.0 - seamWidth) uv.x uv.x - (1.0 - seamWidth); col tex2D(_MainTex, uv);这会在边界处做一个微小的采样偏移混合边缘像素。6.3 在VR中效果异常症状在VR头盔中双眼看到的全景背景不一致或有畸变。原因与解决VR渲染是双摄像机、双通道。我们的着色器默认基于单个摄像机视图方向。在VR下需要确保每个眼睛的渲染都独立计算其视图方向。Unity XR系统会自动处理这一点只要你的Shader支持多pass stereo渲染。确保在Shader的Pass块中或通过#pragma multi_compile_instancing等指令支持实例化渲染Unity XR会为每只眼睛传入正确的摄像机矩阵。6.4 进阶扩展思路掌握了基础之后你可以尝试以下扩展让全景展示更具吸引力动态视频全景将_MainTex的类型从2D改为MovieTexture或使用VideoPlayer组件配合RenderTexture即可播放360度全景视频。注意视频解码性能。多图混合过渡定义两个纹理属性_MainTex和_SecondTex以及一个混合进度_Blend。在片段着色器中同时采样两张图并使用lerp根据_Blend值混合。通过脚本动画控制_Blend可以实现平滑的全景图切换效果。热点与交互在方案二的网格渲染基础上为球体添加Collider和Event Trigger。通过射线检测将点击的屏幕坐标转换为世界空间方向再反向映射回全景图的UV坐标即可知道用户点击了全景图中的哪个位置从而触发信息弹出、场景跳转等交互。与3D场景融合这不是简单的覆盖。一种方法是使用“门户”Portal技术在3D场景中开一个“窗口”这个窗口渲染的是另一个摄像机捕捉的全景图。这需要用到渲染纹理Render Texture和额外的摄像机实现画中画的效果。从依赖现成的天空盒子到亲手掌控全景渲染的每一个像素这个过程不仅解决了一个具体的技术需求更重要的是打通了从3D方向到2D纹理映射的图形学通路。两种方案没有绝对的好坏只有是否适合你的项目场景。理解其原理后你甚至可以创造出第三种、第四种更适合自己需求的方案。最后别忘了在真机上测试特别是VR/AR设备移动端和PC端的性能表现和精度要求可能完全不同根据实际表现回头来调整你的Shader比如降低计算精度、查找表优化等这才是技术落地的完整闭环。