Shader编写指南(五十九): 跨图形 API 的着色器开发指南

不同图形 API(如 Direct3D、OpenGL、Metal)在渲染行为上存在底层差异,Unity 编辑器虽会隐藏大部分差异,但在特定场景下(如渲染纹理坐标、深度缓冲处理)仍需手动适配。以下是关键差异点及解决方案:

一、纹理坐标(UV)的垂直方向差异

核心问题
  • Direct3D 类平台(Direct3D、Metal、主机):纹理坐标 Y 轴顶部为 0,向下递增。
  • OpenGL 类平台(OpenGL、OpenGL ES):纹理坐标 Y 轴底部为 0,向上递增。
影响场景
  1. 渲染到 RenderTexture

    • Direct3D 类平台需内部翻转纹理以匹配 OpenGL 约定,但开启抗锯齿(AA)时可能导致多个纹理方向不一致。
    • 解决方案:在顶点着色器中手动翻转 Y 轴坐标。

      hlsl

      // 在顶点着色器中处理UV坐标  
      float4 vert(appdata v) {  
          float4 pos = UnityObjectToClipPos(v.vertex);  
          float2 uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
          #if UNITY_UV_STARTS_AT_TOP  // Direct3D类平台标记  
              if (_MainTex_TexelSize.y < 0) uv.y = 1 - uv.y;  
          #endif  
          // 传递uv到片元着色器  
          o.uv = uv;  
          return pos;  
      }  
      
  2. GrabPass 采样

    • Direct3D 类平台的 GrabPass 纹理可能未自动翻转,需使用ComputeGrabScreenPos计算正确坐标。

      hlsl

      // 片元着色器中采样GrabPass纹理  
      float4 frag(v2f i) : SV_Target {  
          float4 screenPos = ComputeGrabScreenPos(i.pos);  
          float4 grab = tex2Dproj(_GrabTexture, screenPos);  
          // 处理纹理数据  
          return grab;  
      }  
      
  3. UV 空间渲染

    • 若直接在 UV 空间渲染(如 UI 特效),需根据_ProjectionParams.x判断是否翻转 Y 轴。

      hlsl

      float4 vert(float2 uv : TEXCOORD0) : SV_POSITION {  
          if (_ProjectionParams.x < 0) uv.y = 1 - uv.y; // Direct3D类平台翻转  
          return float4(uv * 2 - 1, 0, 1); // 转换为NDC坐标  
      }  
      

二、裁剪空间深度(Z)的差异

核心问题
  • Direct3D/Metal:裁剪空间深度范围为[近平面值, 0],深度缓冲1.0表示近平面,0.0表示远平面(反向深度)。
  • OpenGL 类:裁剪空间深度范围为[-近平面值, 远平面值],深度缓冲0.0表示近平面,1.0表示远平面(正向深度)。
适配方案
  1. 深度缓冲采样

    • 使用Linear01DepthLinearEyeDepth宏自动处理深度方向。

      hlsl

      float depth = tex2D(_CameraDepthTexture, uv).r;  
      #if defined(UNITY_REVERSED_Z) // Direct3D类平台标记  
          depth = 1 - depth; // 转换为正向深度  
      #endif  
      float linearDepth = Linear01Depth(depth); // 转换为0-1范围  
      
  2. 自定义深度计算

    • 通过UNITY_Z_0_FAR_FROM_CLIPSPACE宏将裁剪空间 Z 转换为 0-1 范围。

      hlsl

      float clipZ = i.pos.z;  
      float normalizedZ = UNITY_Z_0_FAR_FROM_CLIPSPACE(clipZ);  
      
  3. 投影矩阵处理

    • 在脚本中手动翻转 Direct3D 类平台的投影矩阵 Z 分量。

      csharp

      // C#脚本中处理投影矩阵  
      if (SystemInfo.usesReversedZBuffer) {  
          projectionMatrix.m20 *= -1;  
          projectionMatrix.m21 *= -1;  
          projectionMatrix.m22 *= -1;  
          projectionMatrix.m23 *= -1;  
      }  
      

三、着色器数据精度与语法差异

1. 浮点精度
  • PC GPUfloathalffixed均为 32 位精度。
  • 移动 GPUhalf(16 位)和fixed(12 位)可能存在精度损失,需显式声明精度。

    hlsl

    // 移动平台推荐精度声明  
    #pragma target 3.0  
    half4 frag() : SV_Target {  
        fixed3 color = fixed3(0.5, 0.5, 0.5); // 使用fixed类型  
        return half4(color, 1);  
    }  
    
2. const 关键字
  • HLSLconst变量为运行时常量,可初始化任意值。
  • GLSLconst变量必须为编译时常量(字面量或常量表达式)。
    最佳实践:仅用const声明真正的不变量,避免使用变量初始化。

    hlsl

    const float PI = 3.1415926; // 推荐写法  
    float angle = 90.0;  
    // const float rad = angle * PI / 180; // 不推荐(angle为变量)  
    
3. 语义(Semantic)差异
  • 顶点着色器位置:统一使用SV_POSITION,避免POSITION在 PS4 或曲面细分时失效。
  • 片元着色器颜色:使用SV_Target,避免COLOR在 PS4 不兼容。

    hlsl

    // 正确写法  
    float4 frag() : SV_Target { return float4(1,0,0,1); }  
    
4. HLSL 特定语法
  • Direct3D 平台需注意 HLSL 编译器的严格性(如未初始化变量报错)。
    • 顶点修改函数中初始化输出结构体:

      hlsl

      void vert(inout appdata_full v, out Input o) {  
          UNITY_INITIALIZE_OUTPUT(Input, o); // 初始化输出  
          o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
      }  
      
    • 避免在顶点着色器中使用tex2D,改用tex2Dlod并声明#pragma target 3.0

四、缓冲区与内存布局

1. 常量缓冲区(Constant Buffer)
  • 不同 API 对结构体对齐方式不同,推荐使用float4float4x4避免布局错误。

    hlsl

    // 推荐写法(float3用float4存储)  
    cbuffer MyBuffer {  
        float4x4 WorldMatrix;  
        float4 Position; // 替代float3,确保对齐  
        float Scale;  
    };  
    
2. 计算缓冲区(Compute Buffer)
  • 确保结构体在 C# 和 HLSL 中布局一致,使用[StructLayout(LayoutKind.Sequential)]标记。

    csharp

    // C#结构体定义  
    [StructLayout(LayoutKind.Sequential)]  
    public struct MyData {  
        public Vector4 Position;  
        public float Scale;  
    }  
    
     

    hlsl

    // HLSL结构体定义  
    struct MyData {  
        float4 Position;  
        float Scale;  
    };  
    

五、特定 API 功能适配

1. 帧缓冲取指(Framebuffer Fetch)
  • 支持平台(如 iOS 的 PowerVR GPU)可通过inout half4 color访问当前像素颜色。

    hlsl

    #pragma only_renderers framebufferfetch // 限制支持平台  
    void frag(v2f i, inout half4 color : SV_Target) {  
        color.rgb = max(color.rgb, 0.5); // 提亮现有颜色  
    }  
    
2. DirectX 11 特定语法
  • 使用#ifdef SHADER_API_D3D11包裹 DX11 专属代码(如 StructuredBuffer)。

    hlsl

    #ifdef SHADER_API_D3D11  
        StructuredBuffer Points;  
    #else  
        // 其他API的替代实现  
    #endif  
    

六、性能与兼容性最佳实践

  1. 平台宏判断
    使用 Unity 内置平台宏(如UNITY_UV_STARTS_AT_TOPUNITY_REVERSED_Z)条件编译。

    hlsl

    #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN // Direct3D类平台  
        // 特定代码  
    #elif UNITY_ANDROID || UNITY_IOS // OpenGL ES类平台  
        // 特定代码  
    #endif  
    

  2. 避免依赖 API 特性

    • 不使用 GLSL 的centroid等非通用修饰符。
    • 统一使用tex2Dlod替代tex2D以兼容顶点着色器。
  3. 测试与调试

    • 在目标平台真机测试,尤其注意移动设备的精度问题。
    • 使用 Unity 的 Frame Debugger 查看不同 API 的渲染结果差异。

总结:跨平台着色器开发 Checklist

场景 操作指南
纹理坐标垂直方向 使用UNITY_UV_STARTS_AT_TOP宏判断,手动翻转 Y 轴或依赖ComputeGrabScreenPos
深度缓冲方向 UNITY_REVERSED_Z宏判断,通过Linear01Depth/LinearEyeDepth转换深度值。
常量缓冲区对齐 float4存储向量,按float4float2float顺序声明变量。
语义兼容性 顶点位置用SV_POSITION,片元颜色用SV_Target
平台特定代码 #ifdef SHADER_API_*包裹 DirectX/Metal 等专属语法。
精度与变量初始化 移动平台显式声明精度,HLSL 中初始化所有输出变量。

通过系统性处理上述差异,可确保着色器在不同图形 API 和平台间保持一致表现,同时兼顾性能与兼容性。

你可能感兴趣的:(Shader,着色器,shader,unity)