在某些情况下,不同的图形 API 之间图形渲染的行为方式存在差异。大多数情况下,Unity 编辑器会隐藏这些差异,但有些情况下,编辑器无法为您完成此操作。这些情况以及出现这些情况时需要采取的操作列在下面。
两种类型的平台:Direct3D 类和 OpenGL 类之间的垂直纹理坐标约定不同。
这种差异往往不会对您的项目产生任何影响,除非渲染到渲染纹理一种特殊类型的纹理,在运行时创建和更新。要使用它们,首先创建一个新的渲染纹理,然后指定一个相机来渲染到其中。然后,您可以在材质中像使用普通纹理一样使用渲染纹理。 更多信息
参见 术语表。当渲染到 Direct3D 类平台上的纹理时,Unity 在内部将渲染上下翻转。这使得平台之间的约定匹配,OpenGL 类平台约定成为标准。
图像效果和在 UV 空间中的渲染是在着色器在 GPU 上运行的程序。 更多信息
参见 术语表中需要采取措施以确保不同的坐标约定不会在您的项目中造成问题的两个常见情况。
当您使用 图像效果 和抗锯齿时,图像效果的最终源纹理不会被翻转以匹配 OpenGL 类平台约定。在这种情况下,Unity 渲染到屏幕以获得抗锯齿,然后将渲染解析到渲染纹理中,以便与图像效果一起进行进一步处理。
如果您的图像效果是一个简单的效果,每次处理一个渲染纹理,Graphics.Blit 会处理不一致的坐标。但是,如果您在 图像效果 中同时处理多个渲染纹理,则渲染纹理很可能在 Direct3D 类平台上以不同的垂直方向出现,以及当您使用抗锯齿时。为了标准化坐标,您需要在顶点着色器在渲染 3D 模型时,在 3D 模型的每个顶点上运行的程序。 更多信息
参见 术语表中手动将屏幕纹理上下翻转,以使其匹配 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 包含 文件中的 ComputeGrabScreenPos
函数。
当在纹理坐标 (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 将所有浮点类型(float、half 和 fixed)视为相同 - 它们使用完整的 32 位精度进行所有计算,而许多移动设备 GPU 不会这样做。
有关更多信息,请参阅 在着色器中使用 16 位精度。
在 Unity 中编写着色器时,需要注意的是,Unity 将 half
定义为 float
或 min16float
。如果着色器精度模型设置为平台默认,则 half
在 macOS 上为 float
,在 iOS/tvOS 上为 min16float
。如果着色器精度模型设置为统一,则 half
在 macOS/tvOS/iOS 上为 min16float
。
从 Unity 6 开始,min16float
在 Metal 上的大小和对齐方式为任何 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
。
Microsoft HLSL(见 msdn.microsoft.com)和 OpenGL 的 GLSL(见 Wikipedia)着色器语言之间的 const
的使用有所不同。
Microsoft 的 HLSL const
与其在 C# 和 C++ 中的含义基本相同,即声明的变量在其作用域内为只读,但可以用任何方式初始化。
OpenGL 的 GLSL const
表示该变量实际上是一个编译时常量,因此必须使用编译时约束(文字值或对其他 const
的计算)对其进行初始化。
最好遵循 OpenGL 的 GLSL 语义,并且仅在变量真正不变时将其声明为 const
。避免使用其他可变您可以更改可变包的内容。这与不可变相反。只有局部包和嵌入包是可变的。
参见 术语表值(例如,作为函数中的局部变量)初始化 const
变量。这也适用于 Microsoft 的 HLSL,因此以这种方式使用 const
可以避免在某些平台上出现令人困惑的错误。
如果您使用缓冲区在着色器中声明变量,然后使用来自 GPU 计算缓冲区 或 图形缓冲区 的数据设置值,则数据布局可能不匹配,具体取决于图形 API。这意味着您可能会覆盖数据或将变量设置为错误的值。
例如,如果您使用 cbuffer
或 Unity 的 常量缓冲区宏,具体取决于常量缓冲区的数据布局和图形 API,float3
可能会变成 float4
,或者 float
可能会变成 float2
。
您可以执行以下操作以确保所有图形 API 都使用相同的数据布局编译缓冲区
float4
和 float4x4
而不是 float3
和 float3x3
,因为 float4
变量在所有图形 API 上都具有相同的大小,而 float3
变量在某些图形 API 上的大小可能不同。float4
然后 float2
然后 float
,以便所有图形 API 都以相同的方式构造数据。例如
cbuffer myConstantBuffer {
float4x4 matWorld;
float4 vObjectPosition; // Uses a float4 instead of a float3
float arrayIndex;
}
为了让着色器在所有平台上都能正常工作,一些着色器值应该使用这些语义
顶点着色器输出(裁剪空间)位置:SV_POSITION
。有时着色器使用 POSITION 语义来使着色器在所有平台上都能正常工作。请注意,这在 Sony PS4 或使用细分时无效。
片段着色器输出颜色:SV_Target
。有时着色器使用 COLOR
或 COLOR0
来使着色器在所有平台上都能正常工作。请注意,这在 Sony PS4 上无效。
当将网格渲染为点时,从顶点着色器输出 PSIZE
语义(例如,将其设置为 1)。一些平台(例如 OpenGL ES 或 Metal)在未从着色器写入时将点大小视为“未定义”。
有关更多详细信息,请参阅有关 着色器语义 的文档。
Direct3D 平台使用 Microsoft 的 HLSL 着色器编译器。HLSL 编译器在各种细微的着色器错误方面比其他编译器更严格。例如,它不接受未正确初始化的函数输出值。
使用此功能时可能会遇到的最常见情况是
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 的 HLSL(微软的着色器语言)语法。
如果您使用 HLSL 功能,如StructuredBuffers
、RWTextures
和其他非 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.org 上的EXT_shader_framebuffer_fetch
)。
可以在 Unity 中编写使用帧缓冲区获取功能的着色器。为此,在使用 HLSL(微软的着色语言 - 请参阅 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) 方向在不同的着色器平台上有所不同。
DirectX 11、DirectX 12、Metal:反向方向
深度 (Z) 缓冲区在近平面为 1.0,在远平面减小到 0.0。
裁剪空间范围为 [近,0](意味着近平面距离在近平面处,减小到远平面处为 0.0)。
其他平台:传统方向
深度 (Z) 缓冲区值在近平面处为 0.0,在远平面处为 1.0。
请注意,反向方向深度 (Z) 与浮点数 深度缓冲区保存图像中每个像素的 z 值深度的内存存储,其中 z 值是每个渲染像素从投影平面的深度。 更多信息
请参阅 词汇表 结合使用,显着提高了深度缓冲区精度,使其优于传统方向。其优势在于 Z 坐标的冲突更少,阴影效果更好,尤其是在使用较小的近平面和较大的远平面时。
因此,当您使用来自深度 (Z) 反向平台的着色器时
_CameraDepth
纹理纹理范围为 1(近)到 0(远)。但是,以下宏和函数会自动解决深度 (Z) 方向中的任何差异
Linear01Depth(float z)
LinearEyeDepth(float z)
如果您手动获取深度 (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 平台上的裁剪空间,因此在这些平台上它将在“ - 近”1(近)到远(远)之间返回。
GL.GetGPUProjectionMatrix() 如果您在深度 (Z) 反向的平台上,则会返回一个 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;
Unity 会自动处理深度 (Z) 偏差,以确保它与 Unity 的深度 (Z) 方向匹配。但是,如果您使用的是本机代码渲染插件,则需要在您的 C 或 C++ 代码中否定(反转)深度 (Z) 偏差。