Unity Shader 跨平台兼容性:处理纹理坐标翻转与精度差异

张开发
2026/4/10 19:40:41 15 分钟阅读

分享文章

Unity Shader 跨平台兼容性:处理纹理坐标翻转与精度差异
OpenGL、DirectX、Metal、Vulkan 在 UV 坐标方向与浮点精度上的行为并不一致。 本文深入分析各 API 的根本差异并给出在 Unity URP Shader 中可直接使用的兼容性解决方案。1图形 API 总览平台与后端的映射关系Unity URP 通过Unity 渲染后端抽象层将同一份 HLSL Shader 编译到不同的底层 API。理解平台映射关系是解决兼容性问题的第一步。2纹理坐标系差异UV 翻转的根源坐标系方向对比历史原因造成了两套完全相反的纹理坐标约定OpenGL 派系以左下角为原点DirectX 派系以左上角为原点。何时会触发翻转并非所有 UV 操作都会受影响翻转仅在以下场景出现场景是否翻转说明从普通纹理采样不翻转Unity 在导入时已自动处理无需手动修正渲染到纹理RenderTexture / Camera target可能翻转OpenGL 平台上 y 轴与 DirectX 相反后处理 Blit全屏三角形 / Quad可能翻转最常见的踩坑场景需用内置宏修正深度/法线缓冲区采样可能翻转与 RenderTexture 规则相同顶点数据中的 UV0/UV1不翻转由美术工具链保证Shader 无需干预Compute Shader 写入 / 读取可能翻转写入 UAV 时需手动补偿 y 轴3Unity URP 内置宏与翻转处理Unity 在Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl中提供了若干专用宏封装了平台差异。理解这些宏是写出可移植 Shader 的核心。核心宏速查// ── 翻转相关宏 ────────────────────────────────────────── // 返回 1 表示当前平台需要翻转 RenderTexture 的 UV.y // OpenGL / WebGL返回 1V轴向上需翻转 // DirectX / Metal / Vulkan返回 0 #define UNITY_UV_STARTS_AT_TOP // 编译期布尔DX/Metal/Vulkan 平台为真 // 将标准 UV 修正为当前平台正确方向 // 仅在 UNITY_UV_STARTS_AT_TOP 且图像已被垂直翻转时执行翻转 #define UnityStereoTransformScreenSpaceTex(uv) ... // VR 专用 // ── 最常用全屏 Blit UV 修正 ──────────────────────── // 在 Vertex Shader 中对屏幕空间 UV 进行平台修正 #define GetFullScreenTriangleVertexPosition(vertexID, z) #define GetFullScreenTriangleTexCoord(vertexID) // ── 深度/GBuffer 采样前的 UV 修正 ──────────────────── float2 FlipUV(float2 uv) { // UNITY_UV_STARTS_AT_TOP 为编译器常量OpenGL 下为 0 #if UNITY_UV_STARTS_AT_TOP uv.y 1.0 - uv.y; #endif return uv; } // ── 更完整版本同时处理 render target 翻转标志 ─────── // _ProjectionParams.x 1正向或 -1翻转 float2 FlipUVIfNeeded(float2 uv) { #if UNITY_UV_STARTS_AT_TOP if (_ProjectionParams.x 0) uv.y 1.0 - uv.y; #endif return uv; }⚠️Vulkan 的特殊性Vulkan 的 NDC标准化设备坐标clip spaceY 轴向下与 DirectX 一致但 framebuffer 存储仍与 OpenGL 相同。Unity 的抽象层已处理这一差异但手写 Native RenderPass 时需格外留意。_ProjectionParams.x 的含义Unity 将当前帧的投影翻转状态编码到内置变量_ProjectionParams中// float4 _ProjectionParams // .x 1.0 投影矩阵未翻转OpenGL 风格V 向上 // .x -1.0 投影矩阵已翻转DirectX 风格V 向下 // .y Near clip plane // .z Far clip plane // .w 1/Far // 用法示例在 Vertex Shader 中修正裁剪空间 y float4 clipPos TransformObjectToHClip(positionOS); clipPos.y * _ProjectionParams.x; // 统一翻转方向4实践后处理 Pass 中的完整 UV 修正后处理 Shader 是 UV 翻转问题的重灾区。下面是一个使用全屏三角形Procedural Full-Screen Triangle的完整示例可直接在 URP ScriptableRenderPass 中使用。4 实践后处理 Pass 中的完整 UV 修正 后处理 Shader 是 UV 翻转问题的重灾区。下面是一个使用 全屏三角形Procedural Full-Screen Triangle 的完整示例可直接在 URP ScriptableRenderPass 中使用。 HLSL PostProcessBlit.shader — Vertex Shader Shader Custom/URP/PostProcessBlit { SubShader { Pass { HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl TEXTURE2D(_BlitTexture); // URP Blit 传入的源纹理 SAMPLER(sampler_BlitTexture); float4 _BlitTexture_TexelSize; // xy1/wh, zwwh struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings Vert(uint vertexID : SV_VertexID) { Varyings o; // ▶ 关键使用 URP 内置函数生成全屏三角形位置 // 它内部已处理 _ProjectionParams.x 翻转 o.positionCS GetFullScreenTriangleVertexPosition(vertexID); // ▶ 关键用对应宏获取 UV而非手动计算 // OpenGL 平台下此宏会自动执行 uv.y 1 - uv.y o.uv GetFullScreenTriangleTexCoord(vertexID); return o; } half4 Frag(Varyings i) : SV_Target { // UV 已由 Vert 修正此处直接采样即可 half4 color SAMPLE_TEXTURE2D(_BlitTexture, sampler_BlitTexture, i.uv); return color; } ENDHLSL } } }✅Unity 6 推荐方式使用Blitter.BlitCameraTexture(cmd, src, dst, material, passIndex)替代传统的cmd.Blit()。前者内置了所有平台 UV 修正无需在 Shader 中手动处理。旧版 cmd.Blit 的陷阱// ✗ 错误cmd.Blit 在某些平台会自动翻转 UV // 导致与手写全屏三角形 Shader 行为不一致 cmd.Blit(source, dest, material, passIndex); // ✓ 正确Unity 6 / URP 17使用新 Blitter API Blitter.BlitCameraTexture(cmd, source, dest, material, passIndex); // ✓ 如果不需要自定义 Shader仅复制 Blitter.BlitCameraTexture(cmd, source, dest); // ✓ 兼容旧版 URP14 之前的手动处理方式 // 在 Shader 中使用 _BlitScaleBias 对 UV 进行偏移缩放 float4 uv i.uv.xyxy * _BlitScaleBias.xyxy _BlitScaleBias.zwzw;5浮点精度half / float 在各平台的行为精度问题是跨平台 Shader 中另一个主要坑点。half16 位在移动端 GPU 有巨大的性能优势但在某些平台上会悄悄被提升为float32 位而在其他平台上则会引发明显的精度丢失。精度映射关系HLSL / GLSL / Metal Shading LanguageHLSL (Unity)GLSLMetal Shading实际位宽推荐用途half/min16floatmediump floathalf16 bit颜色、法线、UV移动端floathighp floatfloat32 bit世界空间坐标、深度、矩阵变换min10floatlowp float—10–11 bit0–1 范围颜色分量慎用realURP 宏——平台自适应URP 推荐移动halfPCfloatURP 的real类型Unity URP 在Core.hlsl中定义了real作为平台自适应精度类型。在移动平台上它解析为half在桌面/主机平台上解析为float。对于通用光照计算优先使用real。6精度陷阱与最佳实践陷阱 1世界空间坐标用 half这是移动端最常见的精度 Bug用half存储世界空间坐标时由于 half 的范围仅为 ±65504在大场景中会出现顶点抖动vertex shimmer和阴影条纹shadow striping。// ✗ 错误世界坐标用 half大世界场景会抖动 half3 worldPos TransformObjectToWorld(positionOS).xyz; // ✓ 正确世界坐标必须用 float float3 worldPos TransformObjectToWorld(positionOS).xyz; // ✓ 如果之后只用于颜色计算可以在此转换为 half half3 viewDir (half3)normalize(_WorldSpaceCameraPos - worldPos);陷阱 2OpenGL ES 的 mediump 截断Android OpenGL ES 的mediump对应 HLSLhalf精度约为 10–11 位有效尾数比标准 IEEE 754 float16 更低。以下场景会出现明显的色阶断层// ✗ 问题在 GLES 上half 的 1/255 ≈ 0.004 超出精度 // 导致 8bit 颜色空间出现色阶断层banding half4 albedo SAMPLE_TEXTURE2D(_Albedo, sampler_Albedo, uv); half luminance dot(albedo.rgb, half3(0.299, 0.587, 0.114)); // ✓ 修复采样用 half临界计算升级为 float half4 albedo SAMPLE_TEXTURE2D(_Albedo, sampler_Albedo, uv); float luminance dot((float3)albedo.rgb, float3(0.299, 0.587, 0.114)); // ✓ 或使用 URP 的 real 类型自动适配 real luminance dot((real3)albedo.rgb, real3(0.299, 0.587, 0.114));陷阱 3深度缓冲区精度差异// 深度值 [0,1] 范围但反转深度Reversed-Z在 DX/Metal/Vulkan 上为默认 // OpenGL 深度范围是 [-1,1]Unity 会自动映射但精度不同 // ✗ 不跨平台直接读取深度并用 half 存储 half depth SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv); // ✓ 跨平台用 float 读取深度再用 URP 宏转换为线性深度 float rawDepth SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv); // Linear01Depth 内部已处理 UNITY_REVERSED_Z 宏 float linearDepth Linear01Depth(rawDepth, _ZBufferParams); // 只有转换后的线性深度才可以安全地降精度 half depthHalf (half)linearDepth;精度选择速查表数据类型推荐精度原因世界空间位置 / 法线float范围大半精度会抖动裁剪空间位置float4SV_POSITION 要求全精度原始深度值float精度直接影响 Z-fighting纹理 UV 坐标half20–1 范围half 足够颜色值HDRhalf4移动端省寄存器带宽颜色值LDRhalf48bit 颜色 half 完全够用光照方向向量real3URP 自适应移动/PC 均优时间 / 动画参数float累积误差问题不能用 half矩阵变换float4x4绝对不能降精度7综合案例跨平台兼容 Blit Shader下面是一个综合了所有兼容性处理的 URP 后处理 Shader可作为实际项目的生产级模板。Shader Custom/URP/CrossPlatformPostProcess { Properties { _Intensity (Effect Intensity, Range(0,1)) 1.0 } SubShader { // 关闭深度写入和裁剪——后处理 Pass 标准设置 Tags { RenderType Opaque RenderPipeline UniversalPipeline } ZWrite Off ZTest Always Cull Off Pass { Name CrossPlatformPost HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag // ── 必须包含的头文件 ────────────────────────────── #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl // ── 纹理与参数声明 ──────────────────────────────── TEXTURE2D(_BlitTexture); // URP Blit 源纹理自动绑定 SAMPLER(sampler_LinearClamp); // URP 内置采样器 // CBUFFER 包装——移动端 uniform 合批优化 CBUFFER_START(UnityPerMaterial) float _Intensity; // 强度参数用 float避免 half 精度截断 CBUFFER_END // ── 顶点到片元结构体 ────────────────────────────── struct Varyings { float4 positionCS : SV_POSITION; // 必须 float4 float2 uv : TEXCOORD0; // 保持 float 直到片元 }; // ── Vertex Shader ──────────────────────────────── Varyings Vert(uint vertexID : SV_VertexID) { Varyings o; // ▶ 使用 URP 内置宏自动处理平台 UV 翻转 o.positionCS GetFullScreenTriangleVertexPosition(vertexID, UNITY_NEAR_CLIP_VALUE); o.uv GetFullScreenTriangleTexCoord(vertexID); return o; } // ── Fragment Shader ────────────────────────────── half4 Frag(Varyings i) : SV_Target { float2 uv i.uv; // ── 颜色采样half 足够节省移动带宽 ────────── half4 color SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv); // ── 深度采样必须 float ──────────────────────── float rawDepth SampleSceneDepth(uv); // URP 宏已处理 UV 翻转 float linearDepth Linear01Depth(rawDepth, _ZBufferParams); // 处理 Reversed-Z // ── 光照计算使用 real 类型自适应精度 ───────── real3 luminanceWeights real3(0.2126, 0.7152, 0.0722); real luma dot((real3)color.rgb, luminanceWeights); // ── 效果合成 ─────────────────────────────────── half3 grayscale (half3)(luma * luminanceWeights * 3.0); half3 finalColor lerp(grayscale, color.rgb, (half)_Intensity); return half4(finalColor, 1.0); } ENDHLSL } } }对应的 C# ScriptableRenderPassusing UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.Rendering.RenderGraphModule; public class CrossPlatformPostPass : ScriptableRenderPass { private Material m_Material; public CrossPlatformPostPass(Material mat) { m_Material mat; renderPassEvent RenderPassEvent.AfterRenderingPostProcessing; requiresIntermediateTexture true; // 避免 read/write 同一 RT } // ── Unity 6 推荐使用 RenderGraph API ───────────── public override void RecordRenderGraph(RenderGraph graph, ContextContainer ctx) { var cameraData ctx.GetUniversalCameraData(); var resourceData ctx.GetUniversalResourceData(); // ▶ Blitter 自动处理 UV 翻转无需在 Shader 手写 using (var builder graph.AddRasterRenderPassPassData(CrossPlatformPost, out var passData)) { passData.source resourceData.activeColorTexture; passData.material m_Material; builder.UseTexture(passData.source); builder.SetRenderAttachment(resourceData.backBufferColor, 0); builder.SetRenderFunc((PassData d, RasterGraphContext c) Blitter.BlitTexture(c.cmd, d.source, new Vector4(1,1,0,0), d.material, 0)); } } private class PassData { public TextureHandle source; public Material material; } }✅跨平台兼容检查清单① 后处理 UV 使用GetFullScreenTriangleTexCoord或BlitterAPI② 世界空间坐标、深度值、矩阵运算 → 坚持使用float③ 颜色、法线、UV → 移动端用half通用逻辑用real④ 深度采样后用Linear01Depth转换处理 Reversed-Z⑤ 用cmd.Blit的地方统一升级为Blitter.BlitCameraTexture

更多文章