GLM 数学库是否与 Apple 的金属着色语言兼容?
Is the GLM math library compatible with Apple's metal shading language?
我即将移植一个 iOS 应用程序,该应用程序利用以 C++ 编写的 OpenGL 到 Apple 的 Metal。目标是彻底摆脱OpenGL,用Metal取而代之。
OpenGL 代码是分层的,我试图只替换渲染器,即实际调用 OpenGL 函数的 class。但是,整个代码库利用 GLM 数学库来表示向量和矩阵。
例如,有一个提供视图和投影矩阵的相机class。它们都是 glm::mat4
类型,并简单地传递给 GLSL 顶点着色器,在那里它们与 GLSL 给定的 mat4
数据类型兼容。我想利用相机 class 将这些矩阵发送到金属顶点着色器。现在,我不确定 glm::mat4
是否与 Metal 的 float4x4
.
兼容
我没有可以测试它的工作示例,因为我刚开始使用 Metal,在网上找不到任何有用的东西。
所以我的问题如下:
glm::mat4
和 glm::vec4
等 GLM 类型是否与 Metal 的 float4x4
/ float4
兼容?
- 如果问题 1. 的答案是肯定的,如果我在 Metal 着色器中直接使用 GLM 类型是否有任何缺点?
关于问题 2 的背景是,我遇到了 Apple 的 SIMD 库,它提供了另一组数据类型,在这种情况下我将无法使用,对吗?
该应用仅 iOS,我根本不关心 运行 macOS 上的 Metal。
代码片段(最好是 Objective-C(是的,不是开玩笑))非常受欢迎。
总的来说,答案是是,GLM 非常适合使用 Apple Metal 的应用程序。但是,有几件事需要考虑。其中一些内容已经在评论中有所暗示。
Metal defines its Normalized Device Coordinate (NDC) system as a 2x2x1 cube with its center at (0, 0, 0.5)
这意味着Metal NDC坐标不同于OpenGL NDC坐标,因为OpenGL将NDC坐标系定义为2x2x2立方体,其中心位于(0, 0, 0)
,即有效的OpenGL NDC坐标必须在[=35=内]
// Valid OpenGL NDC coordinates
-1 <= x <= 1
-1 <= y <= 1
-1 <= z <= 1
因为 GLM 最初是为 OpenGL 量身定制的,它的 glm::ortho
和 glm::perspective
函数创建投影矩阵,将坐标转换为 OpenGL NDC 坐标。因此,有必要将那些坐标调整到 Metal。 this 博客 post.
中概述了如何实现这一点
但是,有一种更优雅的方法来固定这些坐标。有趣的是,Vulkan utilizes the same NDC coordinate system as Metal and GLM has already been adapted to work with Vulkan (hint for this found here).
通过定义 C/C++ 预处理器宏 GLM_FORCE_DEPTH_ZERO_TO_ONE
,提到的 GLM 投影矩阵函数将转换坐标以与 Metal 的/Vulkan 的 NDC 坐标系一起工作。 #define
将因此解决不同 NDC 坐标系的问题。
接下来,在 Metal 着色器和客户端 (CPU) 代码之间交换数据时,重要的是要考虑 GLM 和 Metal 数据类型的大小和对齐方式。 Apple 的 Metal Shading Language Specification 列出了其数据类型 一些 的大小和对齐方式。
对于其中未列出的数据类型,可以使用 C/C++ 的 sizeof
和 alignof
运算符确定大小和对齐方式。有趣的是,Metal 着色器支持这两种运算符。以下是 GLM 和 Metal 的几个示例:
// Size and alignment of some GLM example data types
glm::vec2 : size: 8, alignment: 4
glm::vec3 : size: 12, alignment: 4
glm::vec4 : size: 16, alignment: 4
glm::mat4 : size: 64, alignment: 4
// Size and alignment of some of Metal example data types
float2 : size: 8, alignment: 8
float3 : size: 16, alignment: 16
float4 : size: 16, alignment: 16
float4x4 : size: 64, alignment: 16
packed_float2 : size: 8, alignment: 4
packed_float3 : size: 12, alignment: 4
packed_float4 : size: 16, alignment: 4
从上面可以看出 table GLM 矢量数据类型在大小和对齐方面与 Metal 的打包矢量数据类型非常匹配。但是请注意,4x4 矩阵数据类型在对齐方面不匹配。
根据 this 对另一个 SO 问题的回答,对齐方式如下:
Alignment is a restriction on which memory positions a value's first byte can be stored. (It is needed to improve performance on processors and to permit use of certain instructions that works only on data with particular alignment, for example SSE need to be aligned to 16 bytes, while AVX to 32 bytes.)
Alignment of 16 means that memory addresses that are a multiple of 16 are the only valid addresses.
因此,在将 4x4 矩阵发送到 Metal 着色器时,我们需要小心考虑不同的对齐方式。让我们看一个例子:
以下 Objective-C 结构用作缓冲区来存储要发送到金属顶点着色器的统一值:
typedef struct
{
glm::mat4 modelViewProjectionMatrix;
glm::vec2 windowScale;
glm::vec4 edgeColor;
glm::vec4 selectionColor;
} SolidWireframeUniforms;
此结构在头文件中定义,该文件包含在客户端(即 CPU 端)代码中需要的任何地方。为了能够在金属顶点着色器端使用这些值,我们需要一个相应的数据结构。在本例中,金属顶点着色器部分如下所示:
#include <metal_matrix>
#include <metal_stdlib>
using namespace metal;
struct SolidWireframeUniforms
{
float4x4 modelViewProjectionMatrix;
packed_float2 windowScale;
packed_float4 edgeColor;
packed_float4 selectionColor;
};
// VertexShaderInput struct defined here...
// VertexShaderOutput struct defined here...
vertex VertexShaderOutput solidWireframeVertexShader(VertexShaderInput input [[stage_in]], constant SolidWireframeUniforms &uniforms [[buffer(1)]])
{
VertexShaderOutput output;
// vertex shader code
}
为了将数据从客户端代码传输到 Metal 着色器,统一结构被打包到缓冲区中。下面的代码显示了如何创建和更新该缓冲区:
- (void)createUniformBuffer
{
_uniformBuffer = [self.device newBufferWithBytes:(void*)&_uniformData length:sizeof(SolidWireframeUniforms) options:MTLResourceCPUCacheModeDefaultCache];
}
- (void)updateUniforms
{
dispatch_semaphore_wait(_bufferAccessSemaphore, DISPATCH_TIME_FOREVER);
SolidWireframeUniforms* uniformBufferContent = (SolidWireframeUniforms*)[_uniformBuffer contents];
memcpy(uniformBufferContent, &_uniformData, sizeof(SolidWireframeUniforms));
dispatch_semaphore_signal(_bufferAccessSemaphore);
}
注意用于更新缓冲区的 memcpy
调用。如果 GLM 和 Metal 数据类型的大小和对齐方式不匹配,这就是问题所在。由于我们只是将 Objective-C 结构的每个字节复制到缓冲区,然后在金属着色器端再次解释该数据,如果数据结构不匹配,数据将在金属着色器端被误解。
在该示例中,内存布局如下所示:
104 bytes
|<--------------------------------------------------------------------------->|
| |
| 64 bytes 8 bytes 16 bytes 16 bytes |
| modelViewProjectionMatrix windowScale edgeColor selectionColor |
|<------------------------->|<----------->|<--------------->|<--------------->|
| | | | |
+--+--+--+------------+--+--+--+-------+--+--+-----------+--+--+----------+---+
Byte index | 0| 1| 2| ... |62|63|64| ... |71|72| ... |87|88| ... |103|
+--+--+--+------------+--+--+--+-------+--+--+-----------+--+--+----------+---+
^ ^ ^
| | |
| | +-- Is a multiple of 4, aligns with glm::vec4 / packed_float4
| |
| +-- Is a multiple of 4, aligns with glm::vec4 / packed_float4
|
+-- Is a multiple of 4, aligns with glm::vec2 / packed_float2
除了 4x4 矩阵对齐外,一切都匹配得很好。 4x4 矩阵的未对齐在上面的内存布局中可见,这里没有问题。但是,如果统一结构被修改,对齐或大小可能会成为问题,并且可能需要填充才能使其正常工作。
最后,还有一点需要注意。数据类型的对齐会影响需要为统一缓冲区分配的大小。因为在SolidWireframeUniforms
结构中出现的最大对齐是16,看来uniform buffer的长度也必须是16的倍数。
在上面的例子中不是这种情况,缓冲区长度为 104 字节,不是 16 的倍数。当 运行 直接从 Xcode 应用程序时,内置断言打印以下消息:
validateFunctionArguments:3478: failed assertion `Vertex Function(solidWireframeVertexShader): argument uniforms[0] from buffer(1) with offset(0) and length(104) has space for 104 bytes, but argument has a length(112).'
为了解决这个问题,我们需要将缓冲区的大小设置为 16 字节的倍数。为此,我们只需根据我们需要的实际长度计算下一个 16 的倍数。对于 104 那将是 112,这就是上面的断言也告诉我们的。
以下函数计算指定整数的下一个 16 的倍数:
- (NSUInteger)roundUpToNextMultipleOf16:(NSUInteger)number
{
NSUInteger remainder = number % 16;
if(remainder == 0)
{
return number;
}
return number + 16 - remainder;
}
现在我们使用上述改变缓冲区创建方法(posted)的函数计算统一缓冲区的长度,如下所示:
- (void)createUniformBuffer
{
NSUInteger bufferLength = [self roundUpToNextMultipleOf16:sizeof(SolidWireframeUniforms)];
_uniformBuffer = [self.device newBufferWithBytes:(void*)&_uniformData length:bufferLength options:MTLResourceCPUCacheModeDefaultCache];
}
这应该可以解决上述断言检测到的问题。
我即将移植一个 iOS 应用程序,该应用程序利用以 C++ 编写的 OpenGL 到 Apple 的 Metal。目标是彻底摆脱OpenGL,用Metal取而代之。
OpenGL 代码是分层的,我试图只替换渲染器,即实际调用 OpenGL 函数的 class。但是,整个代码库利用 GLM 数学库来表示向量和矩阵。
例如,有一个提供视图和投影矩阵的相机class。它们都是 glm::mat4
类型,并简单地传递给 GLSL 顶点着色器,在那里它们与 GLSL 给定的 mat4
数据类型兼容。我想利用相机 class 将这些矩阵发送到金属顶点着色器。现在,我不确定 glm::mat4
是否与 Metal 的 float4x4
.
我没有可以测试它的工作示例,因为我刚开始使用 Metal,在网上找不到任何有用的东西。
所以我的问题如下:
glm::mat4
和glm::vec4
等 GLM 类型是否与 Metal 的float4x4
/float4
兼容?- 如果问题 1. 的答案是肯定的,如果我在 Metal 着色器中直接使用 GLM 类型是否有任何缺点?
关于问题 2 的背景是,我遇到了 Apple 的 SIMD 库,它提供了另一组数据类型,在这种情况下我将无法使用,对吗?
该应用仅 iOS,我根本不关心 运行 macOS 上的 Metal。
代码片段(最好是 Objective-C(是的,不是开玩笑))非常受欢迎。
总的来说,答案是是,GLM 非常适合使用 Apple Metal 的应用程序。但是,有几件事需要考虑。其中一些内容已经在评论中有所暗示。
Metal defines its Normalized Device Coordinate (NDC) system as a 2x2x1 cube with its center at (0, 0, 0.5)
这意味着Metal NDC坐标不同于OpenGL NDC坐标,因为OpenGL将NDC坐标系定义为2x2x2立方体,其中心位于(0, 0, 0)
,即有效的OpenGL NDC坐标必须在[=35=内]
// Valid OpenGL NDC coordinates
-1 <= x <= 1
-1 <= y <= 1
-1 <= z <= 1
因为 GLM 最初是为 OpenGL 量身定制的,它的 glm::ortho
和 glm::perspective
函数创建投影矩阵,将坐标转换为 OpenGL NDC 坐标。因此,有必要将那些坐标调整到 Metal。 this 博客 post.
但是,有一种更优雅的方法来固定这些坐标。有趣的是,Vulkan utilizes the same NDC coordinate system as Metal and GLM has already been adapted to work with Vulkan (hint for this found here).
通过定义 C/C++ 预处理器宏 GLM_FORCE_DEPTH_ZERO_TO_ONE
,提到的 GLM 投影矩阵函数将转换坐标以与 Metal 的/Vulkan 的 NDC 坐标系一起工作。 #define
将因此解决不同 NDC 坐标系的问题。
接下来,在 Metal 着色器和客户端 (CPU) 代码之间交换数据时,重要的是要考虑 GLM 和 Metal 数据类型的大小和对齐方式。 Apple 的 Metal Shading Language Specification 列出了其数据类型 一些 的大小和对齐方式。
对于其中未列出的数据类型,可以使用 C/C++ 的 sizeof
和 alignof
运算符确定大小和对齐方式。有趣的是,Metal 着色器支持这两种运算符。以下是 GLM 和 Metal 的几个示例:
// Size and alignment of some GLM example data types
glm::vec2 : size: 8, alignment: 4
glm::vec3 : size: 12, alignment: 4
glm::vec4 : size: 16, alignment: 4
glm::mat4 : size: 64, alignment: 4
// Size and alignment of some of Metal example data types
float2 : size: 8, alignment: 8
float3 : size: 16, alignment: 16
float4 : size: 16, alignment: 16
float4x4 : size: 64, alignment: 16
packed_float2 : size: 8, alignment: 4
packed_float3 : size: 12, alignment: 4
packed_float4 : size: 16, alignment: 4
从上面可以看出 table GLM 矢量数据类型在大小和对齐方面与 Metal 的打包矢量数据类型非常匹配。但是请注意,4x4 矩阵数据类型在对齐方面不匹配。
根据 this 对另一个 SO 问题的回答,对齐方式如下:
Alignment is a restriction on which memory positions a value's first byte can be stored. (It is needed to improve performance on processors and to permit use of certain instructions that works only on data with particular alignment, for example SSE need to be aligned to 16 bytes, while AVX to 32 bytes.)
Alignment of 16 means that memory addresses that are a multiple of 16 are the only valid addresses.
因此,在将 4x4 矩阵发送到 Metal 着色器时,我们需要小心考虑不同的对齐方式。让我们看一个例子:
以下 Objective-C 结构用作缓冲区来存储要发送到金属顶点着色器的统一值:
typedef struct
{
glm::mat4 modelViewProjectionMatrix;
glm::vec2 windowScale;
glm::vec4 edgeColor;
glm::vec4 selectionColor;
} SolidWireframeUniforms;
此结构在头文件中定义,该文件包含在客户端(即 CPU 端)代码中需要的任何地方。为了能够在金属顶点着色器端使用这些值,我们需要一个相应的数据结构。在本例中,金属顶点着色器部分如下所示:
#include <metal_matrix>
#include <metal_stdlib>
using namespace metal;
struct SolidWireframeUniforms
{
float4x4 modelViewProjectionMatrix;
packed_float2 windowScale;
packed_float4 edgeColor;
packed_float4 selectionColor;
};
// VertexShaderInput struct defined here...
// VertexShaderOutput struct defined here...
vertex VertexShaderOutput solidWireframeVertexShader(VertexShaderInput input [[stage_in]], constant SolidWireframeUniforms &uniforms [[buffer(1)]])
{
VertexShaderOutput output;
// vertex shader code
}
为了将数据从客户端代码传输到 Metal 着色器,统一结构被打包到缓冲区中。下面的代码显示了如何创建和更新该缓冲区:
- (void)createUniformBuffer
{
_uniformBuffer = [self.device newBufferWithBytes:(void*)&_uniformData length:sizeof(SolidWireframeUniforms) options:MTLResourceCPUCacheModeDefaultCache];
}
- (void)updateUniforms
{
dispatch_semaphore_wait(_bufferAccessSemaphore, DISPATCH_TIME_FOREVER);
SolidWireframeUniforms* uniformBufferContent = (SolidWireframeUniforms*)[_uniformBuffer contents];
memcpy(uniformBufferContent, &_uniformData, sizeof(SolidWireframeUniforms));
dispatch_semaphore_signal(_bufferAccessSemaphore);
}
注意用于更新缓冲区的 memcpy
调用。如果 GLM 和 Metal 数据类型的大小和对齐方式不匹配,这就是问题所在。由于我们只是将 Objective-C 结构的每个字节复制到缓冲区,然后在金属着色器端再次解释该数据,如果数据结构不匹配,数据将在金属着色器端被误解。
在该示例中,内存布局如下所示:
104 bytes
|<--------------------------------------------------------------------------->|
| |
| 64 bytes 8 bytes 16 bytes 16 bytes |
| modelViewProjectionMatrix windowScale edgeColor selectionColor |
|<------------------------->|<----------->|<--------------->|<--------------->|
| | | | |
+--+--+--+------------+--+--+--+-------+--+--+-----------+--+--+----------+---+
Byte index | 0| 1| 2| ... |62|63|64| ... |71|72| ... |87|88| ... |103|
+--+--+--+------------+--+--+--+-------+--+--+-----------+--+--+----------+---+
^ ^ ^
| | |
| | +-- Is a multiple of 4, aligns with glm::vec4 / packed_float4
| |
| +-- Is a multiple of 4, aligns with glm::vec4 / packed_float4
|
+-- Is a multiple of 4, aligns with glm::vec2 / packed_float2
除了 4x4 矩阵对齐外,一切都匹配得很好。 4x4 矩阵的未对齐在上面的内存布局中可见,这里没有问题。但是,如果统一结构被修改,对齐或大小可能会成为问题,并且可能需要填充才能使其正常工作。
最后,还有一点需要注意。数据类型的对齐会影响需要为统一缓冲区分配的大小。因为在SolidWireframeUniforms
结构中出现的最大对齐是16,看来uniform buffer的长度也必须是16的倍数。
在上面的例子中不是这种情况,缓冲区长度为 104 字节,不是 16 的倍数。当 运行 直接从 Xcode 应用程序时,内置断言打印以下消息:
validateFunctionArguments:3478: failed assertion `Vertex Function(solidWireframeVertexShader): argument uniforms[0] from buffer(1) with offset(0) and length(104) has space for 104 bytes, but argument has a length(112).'
为了解决这个问题,我们需要将缓冲区的大小设置为 16 字节的倍数。为此,我们只需根据我们需要的实际长度计算下一个 16 的倍数。对于 104 那将是 112,这就是上面的断言也告诉我们的。
以下函数计算指定整数的下一个 16 的倍数:
- (NSUInteger)roundUpToNextMultipleOf16:(NSUInteger)number
{
NSUInteger remainder = number % 16;
if(remainder == 0)
{
return number;
}
return number + 16 - remainder;
}
现在我们使用上述改变缓冲区创建方法(posted)的函数计算统一缓冲区的长度,如下所示:
- (void)createUniformBuffer
{
NSUInteger bufferLength = [self roundUpToNextMultipleOf16:sizeof(SolidWireframeUniforms)];
_uniformBuffer = [self.device newBufferWithBytes:(void*)&_uniformData length:bufferLength options:MTLResourceCPUCacheModeDefaultCache];
}
这应该可以解决上述断言检测到的问题。