Version: Unity 6.0 (6000.0)
语言 : 中文
纹理采样器
在着色器中使用 16 位精度

为不同的图形 API 编写着色器

在某些情况下,不同图形 API 之间的图形渲染行为方式存在差异。大多数情况下,Unity 编辑器会隐藏这些差异,但在某些情况下,编辑器无法为您执行此操作。这些情况以及发生这些情况时需要采取的操作如下所列。

渲染纹理坐标

垂直纹理坐标规范在两种平台类型之间有所不同:即 Direct3D 类和 OpenGL 类。

  • Direct3D 类:顶部坐标为 0 并向下增加。适用于 Direct3D、Metal 和游戏主机。
  • OpenGL 类:底部坐标为 0 并向上增加。适用于 OpenGL 和 OpenGL ES。

这种差异一般不会对项目产生任何影响,除了渲染到渲染纹理的情况。在 Direct3D 类平台上渲染到纹理时,Unity 会在内部上下翻转渲染。这样就会匹配不同平台之间的坐标规范,并以 OpenGL 类平台规范作为标准。

在着色器中,有两种常见场景需要您采取操作以确保不同的坐标规范不会在项目中产生问题,即图像效果和 UV 空间中的渲染。

图像效果

使用图像效果和抗锯齿时,生成的源纹理不会为匹配 OpenGL 类平台的规范而被翻转。这时,Unity 会渲染到屏幕以获得抗锯齿效果,然后将渲染解析为渲染纹理,以便通过图像效果进行进一步处理。

如果您的图像效果是一次处理一个渲染纹理的简单图像效果,则 Graphics.Blit 会处理不一致的坐标。但如果您在图像效果中同时处理多个渲染纹理,则在 Direct3D 类平台中以及在您使用抗锯齿时,渲染纹理很可能以不同的垂直方向出现。要让坐标标准化,您需要在顶点着色器中手动上下“翻转”屏幕纹理,使其与 OpenGL 类坐标标准匹配。

以下代码示例演示了如何执行此操作:

// Flip sampling of the Texture:
// The main Texture
// texel size will have negative Y).

#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
        uv.y = 1-uv.y;
#endif

GrabPass 也会出现类似的情况。生成的渲染纹理实际上可能不会在 Direct3D 类(非 OpenGL 类)平台上进行上下翻转。如果着色器代码对 GrabPass 纹理进行采样,请使用 UnityCG include 文件中的 ComputeGrabScreenPos 函数。

在 UV 空间中渲染

在 UV 空间中针对特殊效果或工具进行渲染时,您可能需要调整着色器,以便在 Direct3D 类和 OpenGL 类系统之间进行一致的渲染。您还可能需要在渲染到屏幕和渲染到纹理之间进行渲染调整。为进行此类调整,请上下翻转 Direct3D 类的投影,使其坐标与 OpenGL 类投影坐标相匹配。

内置变量 ProjectionParams.x 包含一个 +1 –1 值。-1 表示投影已上下翻转以匹配 OpenGL 类投影坐标,而 +1 表示尚未翻转。您可以在着色器中检查此值,然后执行不同的操作。下方示例会检查是否已翻转投影,如果已翻转,则再次进行翻转,然后返回 UV 坐标以便匹配。

float4 vert(float2 uv : TEXCOORD0) : SV_POSITION
{
    float4 pos;
    pos.xy = uv;
    // This example is rendering with upside-down flipped projection,
    // so flip the vertical UV coordinate too
    if (_ProjectionParams.x < 0)
        pos.y = 1 - pos.y;
    pos.z = 0;
    pos.w = 1;
    return pos;
}

裁剪空间坐标

与纹理坐标类似,裁剪空间坐标(也称为投影后空间坐标)在 Direct3D 类和 OpenGL 类平台之间也有所不同:

  • Direct3D 类:裁剪空间深度从近平面的 +1.0 到远平面的 0.0 不等。适用于 Direct3D、Metal 和游戏主机。

  • OpenGL 类:裁剪空间深度从近平面的 –1.0 到远平面的 +1.0 不等。适用于 OpenGL 和 OpenGL ES。

在着色器代码内,可使用内置宏 UNITY_NEAR_CLIP_VALUE 来获取基于平台的近平面值。

在脚本代码内,使用 GL.GetGPUProjectionMatrix 将 Unity 的坐标系(遵循 OpenGL 类规范)转换为 Direct3D 类坐标(如果这是平台期望如此)。

着色器计算的精度

为避免精度问题,请确保在目标平台上测试着色器。移动设备和 PC 中的 GPU 在处理浮点类型方面有所不同。PC 的 GPU 会将所有浮点类型(浮点精度、半精度和固定精度)视为相同,并使用完整的 32 位精度进行所有计算,而许多移动设备 GPU 并不会这样做。

详情请参阅在着色器中使用 16 位精度

着色器数据类型支持

在 Unity 中编写着色器时,必须注意 Unity 会将 half 定义为 floatmin16float。如果 着色器精度模型 (Shader Precision Model) 被设为 平台默认项 (Platform Default),则 half 在 macOS 上为 float,在 iOS/tvOS 上为 min16float。如果 着色器精度模型 被设为 统一 (Unified),则 half 在 macOS/tvOS/iOS 上为 min16float

从 Unity 6 开始,Metal 上 min16float 的大小和对齐方式在任何 CPU 可见缓冲区(例如顶点着色器输入、常量缓冲区和结构化缓冲区)上均为 4 字节。因此,无论平台或项目设置如何,half 的大小和对齐方式始终相同。在 Unity 之前的版本中,由于 min16float 的大小和对齐方式为 2 字节,因此包含 half 的缓冲区的布局会因平台和所选的 着色器精度模型 设置而变化。由于此问题,iOS 和 tvOS 用户在 iOS/tvOS 上将数据上传到 GPU 缓冲区时,必须添加 C# 代码作为变通方法,而这不再适用于 Unity 6。要在 Unity 6 中使用 FXC 进行编译时启用这一旧版行为,可以在着色器中包含 #pragma metal_fxc_allow_float16_in_cpu_visible_buffers

着色器中的 Const 声明

const 的使用在 Microsoft HLSL(见 msdn.microsoft.com)和 OpenGL 的 GLSL(见 Wikipedia)着色器语言之间有所不同。

  • Microsoft 的 HLSL const 和其在 C# 和 C++ 中的含义大致相同:声明的变量在其作用域内是只读的,但可按任何方式初始化。

  • OpenGL 的 GLSL const 表示变量实际上是编译时常量,因此必须使用编译时约束(文字值或其他对于 const 的计算)进行初始化。

最好遵循 OpenGL 的 GLSL 语义,并且只有当变量真正不变时才将变量声明为 const。请避免使用其他可变值初始化 const 变量(例如,作为函数中的局部变量)。这一原则也适用于 Microsoft 的 HLSL,因此以这种方式使用 const 可以避免某些平台上的混淆错误。

将缓冲区与 GPU 缓冲区结合使用

如果您使用缓冲区在着色器中声明变量,然后使用 GPU 计算缓冲区图形缓冲区中的数据来设置值,则数据布局可能不匹配,这具体取决于图形 API。这意味着您可能会覆盖数据或将变量设置为错误的值。

例如,如果使用 cbuffer 或 Unity 的常量缓冲区宏,则根据常量缓冲区的数据布局和图形 API,float3 可能成为float4float 可能成为 float2

您可以执行以下操作以确保所有图形 API 都编译数据布局相同的缓冲区:

  • 使用 float4float4x4 而不是 float3float3x3,因为 float4 变量在所有图形 API 上的大小都相同,而 float3 变量在某些图形 API 上可能变为不同的大小。
  • 按大小递减顺序声明变量,例如先 float4float2float,从而让所有图形 API 都以相同的方式构建数据。

例如:

cbuffer myConstantBuffer {
    float4x4 matWorld;
    float4 vObjectPosition; // Uses a float4 instead of a float3
    float arrayIndex;
}

着色器使用的语义

要让着色器在所有平台上运行,部分着色器值应该使用以下语义:

  • __顶点着色器输出(裁剪空间)位置__:SV_POSITION。有时着色器会使用 POSITION 语义来使着色器在所有平台上运行。请注意,这对 Sony PS4 或有曲面细分的情况不生效。

  • __片元着色器输出颜色__:SV_Target。有时着色器会使用 COLORCOLOR0 来使着色器在所有平台上运行。请注意,这对 Sony PS4 不生效。

将网格渲染为点时,从顶点着色器输出 PSIZE 语义(例如将其设置为 1)。某些平台(如 OpenGL ES 或 Metal)在未从着色器写入点大小时会将点大小视为“未定义”。

详情请参阅关于着色器语义的文档。

Direct3D 着色器编译器语义

Direct3D 平台使用 Microsoft 的 HLSL 着色器编译器。对于各种细微的着色器错误,HLSL 编译器比其他编译器更严格。例如,它不接受未正确初始化的函数输出值。

使用此编译器时,您可能遇到的最常见情况如下:

  • 具有 out 参数的表面着色器顶点修饰符。按如下方式初始化输出:
  void vert (inout appdata_full v, out Input o)
      {
        **UNITY_INITIALIZE_OUTPUT(Input,o);**
        // ...
      }
  • 部分初始化的值。例如,函数返回 float4,但代码只设置它的 .xyz 值。如果只需要三个值,请设置所有值或更改为 float3

  • 在顶点着色器中使用 tex2D。这是无效的,因为顶点着色器中不存在 UV 导数。这时您需要采样明确的 Mip 级别;例如,使用 tex2Dlod (tex, float4(uv,0,0))。此外,还需要添加 #pragma target 3.0,因为 tex2Dlod 是着色器模型 3.0 的功能。

着色器中的 DirectX 11 (DX11) HLSL 语法

表面着色器编译管线的某些部分不能理解 DirectX 11 特定的 HLSL(Microsoft 着色器语言)语法。

如果您正在使用 HLSL 功能(比如 StructuredBuffersRWTextures 和其他非 DirectX 9 语法),请将它们包裹在 DirectX X11 专用的预处理器宏中,如下例所示。

#ifdef SHADER_API_D3D11
// DirectX11-specific code, for example
StructuredBuffer<float4> myColors;
RWTexture2D<float4> myRandomWriteTexture;
#endif

使用着色器帧缓冲区提取

某些 GPU(以 iOS 上基于 PowerVR 的 GPU 为代表)允许您通过提供以当前片元颜色为片元着色器的输入来进行某种可编程的混合(见 khronos.orgEXT_shader_framebuffer_fetch)。

您可在 Unity 中编写使用帧缓冲区提取功能的着色器。为此,请在使用 HLSL(Microsoft 的着色语言,见 msdn.microsoft.com)或 Cg(Nvidia 的着色语言,见 nvidia.co.uk)编写片元着色器时使用 inout 颜色参数。

以下示例采用的是 Cg 语言。

CGPROGRAM
// only compile Shader for platforms that can potentially
// do it (currently gles,gles3,metal)
#pragma only_renderers framebufferfetch

void frag (v2f i, inout half4 ocol : SV_Target)
{
    // ocol can be read (current framebuffer color)
    // and written into (will change color to that one)
    // ...
}
ENDCG

着色器中的深度 (Z) 方向

深度 (Z) 方向在不同的着色器平台上不同。

DirectX 11、DirectX 12、Metal:反转方向

  • 深度 (Z) 缓冲区在近平面处为 1.0,在远平面处减小到 0.0。

  • 裁剪空间范围是 [near,0](即近平面处的近平面距离,在远平面处减小到 0.0)。

其他平台:传统方向

  • 深度 (Z) 缓冲区值在近平面处为 0.0,在远平面处为 1.0。

  • 裁剪空间取决于具体的平台:
    • 在 Direct3D 类平台上,范围是 [0,far](即在近平面处为 0.0,在远平面处增加到远平面距离)。
    • 在 OpenGL 类平台上,范围是 [-near,far](即在近平面处为负的近平面距离,在远平面处增加到远平面距离)。

请注意,让反转方向深度 (Z) 与浮点深度缓冲区相结合,可显著提高相对于传统方向的深度缓冲区精度。这样做的优点是减少 Z 坐标的冲突并改善阴影,特别是在使用小的近平面和大的远平面时。

因此,在使用反转的深度 (Z) 平台的着色器时:

  • UNITY_REVERSED_Z 会被定义。
  • _CameraDepth 纹理的纹理范围是 1(近平面)到 0(远平面)。
  • 裁剪空间范围是“near”(近平面)到 0(远平面)。

但下列宏和函数会自动计算出深度 (Z) 方向的任何差异:

  • Linear01Depth(float z)
  • LinearEyeDepth(float z)
  • UNITY_CALC_FOG_FACTOR(coord)

提取深度缓冲区

如果要手动提取深度 (Z) 缓冲区值,则可能需要检查缓冲区方向。以下是执行此操作的示例:

float z = tex2D(_CameraDepthTexture, uv);
#if defined(UNITY_REVERSED_Z)
    z = 1.0f - z;
#endif

使用裁剪空间

如果要手动使用裁剪空间 (Z) 深度,则可能还需要使用以下宏来抽象化平台差异:

float clipSpaceRange01 = UNITY_Z_0_FAR_FROM_CLIPSPACE(rawClipSpace);

注意:此宏不会改变 OpenGL 或 OpenGL ES 平台上的裁剪空间,因此在这些平台上,此宏会返回“-near”1(近平面)到 far(远平面)之间的值。

投影矩阵

如果处于深度 (Z) 发生反转的平台上,则 GL.GetGPUProjectionMatrix() 会返回一个还原了 z 的矩阵。但如果要手动通过投影矩阵进行合成(例如针对自定义阴影或深度渲染),那么您需要通过脚本按需自行还原深度 (Z) 方向。

以下是执行此操作的示例:

var shadowProjection = Matrix4x4.Ortho(...); //shadow camera projection matrix
var shadowViewMat = ...     //shadow camera view matrix
var shadowSpaceMatrix = ... //from clip to shadowMap texture space

//'m_shadowCamera.projectionMatrix' is implicitly reversed
//when the engine calculates device projection matrix from the camera projection
m_shadowCamera.projectionMatrix = shadowProjection;

//'shadowProjection' is manually flipped before being concatenated to 'm_shadowMatrix'
//because it is seen as any other matrix to a Shader.
if(SystemInfo.usesReversedZBuffer)
{
    shadowProjection[2, 0] = -shadowProjection[2, 0];
    shadowProjection[2, 1] = -shadowProjection[2, 1];
    shadowProjection[2, 2] = -shadowProjection[2, 2];
    shadowProjection[2, 3] = -shadowProjection[2, 3];
}
    m_shadowMatrix = shadowSpaceMatrix * shadowProjection * shadowViewMat;

深度 (Z) 偏差

Unity 会自动处理深度 (Z) 偏差,以确保其与 Unity 的深度 (Z) 方向匹配。但如果要使用原生代码渲染插件,则需要在 C 或 C++ 代码中消除(反转)深度 (Z) 偏差。

深度 (Z) 方向检查工具

纹理采样器
在着色器中使用 16 位精度