Ch4-1 着色器模组
着色器是在管线的可编程阶段运行的GPU程序。
通常使用适合人类阅读和书写的编程语言GLSL编写着色器(也可以用微软的HLSL),然后将着色器编译到SPIR-V这一中间语言,后缀名为.spv。
之后再由Vulkan程序读取.spv文件,由显卡驱动提供的Vulkan实现将其编译为着色器模组(VkShaderModule)。
为什么不事先一步到位地编译?因为不同的显卡驱动所提供的Vulkan实现的编译结果可能不同。
本节先讲述Vulkan API中与着色器模组相关的接口,再说明编写用于Vulkan的GLSL着色器时的一些通用常识。
着色器模组
创建着色器模组
用vkCreateShaderModule(...)创建着色器模组:
VkResult VKAPI_CALL vkCreateShaderModule(...) 的参数说明 |
|
---|---|
VkDevice device |
逻辑设备的handle |
const VkShaderModuleCreateInfo* pCreateInfo |
指向VkFramebuffer的创建信息 |
const VkAllocationCallbacks* pAllocator |
|
VkShaderModule* pShaderModule |
若创建成功,将着色器模组的handle写入*pShaderModule |
struct VkShaderModuleCreateInfo 的成员说明 |
|
---|---|
VkStructureType sType |
结构体的类型,本处必须是VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO |
const void* pNext |
如有必要,指向一个用于扩展该结构体的结构体 |
VkShaderModuleCreateFlags flags |
|
size_t codeSize |
SPIR-V代码的大小,单位是字节,必须是4的倍数 |
const uint32_t* pCode |
指向SPIR-V代码 |
VkShaderModuleCreateInfo中没有任何有必要特地说明的地方。关于如何从.spv文件读取SPIR-V代码到内存,这里给出C++代码:
std::ifstream file(filepath, std::ios::ate | std::ios::binary); if (!file) { /*错误信息,此处略*/ } size_t fileSize = size_t(file.tellg()); std::vector<uint32_t> binaries(fileSize / 4); file.seekg(0); file.read(reinterpret_cast<char*>(binaries.data()), fileSize); file.close();
管线着色器阶段的创建信息
创建渲染管线时需要着色器阶段的创建信息(VkPipelineShaderStageCreateInfo):
struct VkPipelineShaderStageCreateInfo 的成员说明 |
|
---|---|
VkStructureType sType |
结构体的类型,本处必须是VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO |
const void* pNext |
如有必要,指向一个用于扩展该结构体的结构体 |
VkPipelineShaderStageCreateFlags flags |
|
VkShaderStageFlagBits stage |
着色器对应的可编程管线阶段 |
VkShaderModule module |
着色器模组的handle |
const char* pName |
接入点函数名称,比如通常的主函数为"main" |
const VkSpecializationInfo* pSpecializationInfo |
常量的特化信息,若不需要特化常量,则为nullptr |
-
自Vulkan1.3起可以将flags指定为与计算着色器相关的VK_PIPELINE_SHADER_STAGE_CREATE_ALLOW_VARYING_SUBGROUP_SIZE_BIT或VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT,本套教程中不作讲解。
可以用VkSpecializationInfo对管线中着色器的特定常量进行独有的特化:
struct VkSpecializationInfo 的成员说明 |
|
---|---|
uint32_t mapEntryCount |
需被特化的常量的个数 |
const VkSpecializationMapEntry* pMapEntries |
各个常量的特化信息 |
size_t dataSize |
特化数据的总大小,单位为字节 |
const void* pData |
所有特化数据所在的内存地址 |
struct VkSpecializationMapEntry 的成员说明 |
|
---|---|
uint32_t constantID |
被特化常量的ID |
uint32_t offset |
对应的特化数据在VkSpecializationInfo::pData中的起始位置 |
size_t size |
对应的特化数据的大小,单位为字节 |
-
如果对应的常量是布尔类型,size为4(一个VkBool32的大小)。
这里以具体代码为例,说明常量特化的具体写法:
//GLSL代码 layout(constant_id = 0) const uint maxLightCount = 32; layout(constant_id = 1) const uint shininess = 32;
-
对于需要被特化的常量,在声明前加上
layout(constant_id = ID编号)
以上代码声明了两个常量,其默认值皆指定为32。
试着在创建管线时对它们进行特化,将maxLightCount变为48,shininess变为64:
//函数体内部的C++代码 static constexpr struct { int32_t maxLightCount = 48; int32_t shininess = 64; } constants; VkSpecializationMapEntry specializationMapEntries[] = { { 0, 0, 4 },//开头的0对应constant_id = 0,之后的0对应结构体变量constants中成员maxLightCount的起始位置 { 1, 4, 4 } //开头的1对应constant_id = 1,之后的4对应结构体变量constants中成员shininess的起始位置 }; VkSpecializationInfo specializationInfo = { .mapEntryCount = 2, .pMapEntries = specializationMapEntries, .dataSize = sizeof constants, .pData = &constants };
封装为shaderModule类
向VKBase.h,vulkan命名空间中添加以下代码:
class shaderModule { VkShaderModule handle = VK_NULL_HANDLE; public: shaderModule() = default; shaderModule(VkShaderModuleCreateInfo& createInfo) { Create(createInfo); } shaderModule(const char* filepath /*VkShaderModuleCreateFlags flags*/) { Create(filepath); } shaderModule(size_t codeSize, const uint32_t* pCode /*VkShaderModuleCreateFlags flags*/) { Create(codeSize, pCode); } shaderModule(shaderModule&& other) noexcept { MoveHandle; } ~shaderModule() { DestroyHandleBy(vkDestroyShaderModule); } //Getter DefineHandleTypeOperator; DefineAddressFunction; //Const Function VkPipelineShaderStageCreateInfo StageCreateInfo(VkShaderStageFlagBits stage, const char* entry = "main") const { return { VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,//sType nullptr, //pNext 0, //flags stage, //stage handle, //module entry, //pName nullptr //pSpecializationInfo }; } //Non-const Function result_t Create(VkShaderModuleCreateInfo& createInfo) { createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; VkResult result = vkCreateShaderModule(graphicsBase::Base().Device(), &createInfo, nullptr, &handle); if (result) outStream << std::format("[ shader ] ERROR\nFailed to create a shader module!\nError code: {}\n", int32_t(result)); return result; } result_t Create(const char* filepath /*VkShaderModuleCreateFlags flags*/) { std::ifstream file(filepath, std::ios::ate | std::ios::binary); if (!file) { outStream << std::format("[ shader ] ERROR\nFailed to open the file: {}\n", filepath); return VK_RESULT_MAX_ENUM;//没有合适的错误代码,别用VK_ERROR_UNKNOWN } size_t fileSize = size_t(file.tellg()); std::vector<uint32_t> binaries(fileSize / 4); file.seekg(0); file.read(reinterpret_cast<char*>(binaries.data()), fileSize); file.close(); return Create(fileSize, binaries.data()); } result_t Create(size_t codeSize, const uint32_t* pCode /*VkShaderModuleCreateFlags flags*/) { VkShaderModuleCreateInfo createInfo = { .codeSize = codeSize, .pCode = pCode }; return Create(createInfo); } };
标准化设备坐标系
编写Vulkan用的GLSL着色器时,首先需要注意的是其NDC(标准化设备坐标系,normalized device coordinates)。
相机参考系中的三维坐标加上齐次坐标w,经投影矩阵变换后,被转换到齐次剪裁空间(homogeneous clip space)坐标。将该四维矢量输出到gl_Position后,在栅格化阶段前,该四维矢量的xyz三个分量被除以w分量,由此得到的三维矢量即是NDC坐标(二维渲染中gl_Position.w应总是指定为1,那么gl_Position.xyz的数值就等同于NDC坐标)。
最终片段在图像中的具体坐标及深度值,是根据创建管线时指定的视口(viewport),将NDC坐标映射到屏幕空间(screen space)后得到的。
对比Vulkan与OpenGL的NDC:
Vulkan |
OpenGL |
|
---|---|---|
可被渲染的x坐标的范围 |
[-1, 1] |
[-1, 1] |
+x方向 |
向右 |
向右 |
可被渲染的y坐标的范围 |
[-1, 1] |
[-1, 1] |
+y方向 |
向下 |
向上 |
可被渲染的z坐标的范围 |
[ 0, 1] |
[-1, 1] |
+z方向 |
因人而异 |
因人而异 |
-
+z方向无关紧要,远近的定义取决于如何进行深度测试。
之前在Ch1-1 创建GLFW窗口中已经定义了GLM_FORCE_DEPTH_ZERO_TO_ONE来指定深度范围为[ 0, 1]。GLM生成的投影矩阵适用于OpenGL,但对于Vulkan程序还需要解决+y方向问题,通过以下函数对投影矩阵略作修改即可:
inline glm::mat4 FlipVertical(const glm::mat4& projection) { glm::mat4 _projection = projection; for (uint32_t i = 0; i < 4; i++) _projection[i][1] *= -1; return _projection; }
从GLSL编译到SPIR-V
你所下载到的Vulkan SDK中应该附带Bin/glslc.exe,这是谷歌提供的工具(官方Github文档见此),用于将由GLSL书写的着色器编译到.spv文件。
着色器类型判别
编译GLSL着色器到SPIR-V时,需要告诉glslc.exe该着色器对应的可编程阶段。
以下三种方式之一即可:
1.通过文件扩展名表示着色器类型
2.通过着色器中的预编译指令表示着色器类型,语法为#pragma shader_stage(着色器对应的阶段名称)
3.编译时显式指定,语法为-fshader-stage=着色器对应的阶段名称
以上三条按优先级从低到高排序,即显式指定的优先级最高,预编译指令其次。
管线阶段 |
文件扩展名 |
预编译指令/显式指定的阶段名称 |
---|---|---|
顶点 |
.vert |
vertex |
片段 |
.frag |
fragment |
细分控制 |
.tesc |
tesscontrol |
细分求值 |
.tese |
tesseval |
几何 |
.geom |
geometry |
计算 |
.comp |
compute |
基础命令行语法
指定文件名:
glslc.exe路径 着色器文件路径 -o 输出文件路径
//若显式指定着色器对应的阶段名称
glslc.exe路径 着色器文件路径 -fshader-stage=阶段名称 -o 输出文件路径
自动命名:
glslc.exe路径 着色器文件路径 -c
//若显式指定着色器对应的阶段名称
glslc.exe路径 着色器文件路径 -fshader-stage=阶段名称 -c
-
其实
-fshader-stage=阶段名称
和-c
或-o 输出文件路径
之间不分先后。 -
若着色器文件的后缀为着色器类型(.vert等),自动命名的方式是在其后添加.spv,比如foo.vert变为foo.vert.spv。否则,自动命名的方式是替代扩展名为.spv,比如foo.vert.shader变为foo.vert.spv。
GLSL着色器的基本语法
版本声明
在着色器的开头须声明其版本,语法为:
#version 三位数版本号
截至我撰写这套教程为止,最新的GLSL版本为4.60,如下所示:
#version 460
接入点函数
同C++程序需要主函数一样,着色器需要一个主函数作为程序接入点,通常我会把它也称作main()。
注意着色器的接入点函数的返回值只能是void类型,即不能返回值,但你能在函数体中使用return关键字显式返回。
除了主函数外,你当然还能声明其他的函数。
数据类型
着色器中可用以下标量类型:
类型名 |
说明 |
---|---|
bool |
4字节,对应VkBool32 |
int |
32位有符号整形 |
uint |
32位无符号整形 |
float |
32位浮点数 |
double |
64位浮点数 |
着色器中可用以下矢量类型:
类型名 |
说明 |
---|---|
bvec2 |
分量是bool类型,每个分量4字节 |
bvec3 |
同上 |
bvec4 |
同上 |
ivec2 |
分量是int类型 |
ivec3 |
同上 |
ivec4 |
同上 |
uvec2 |
分量是uint类型 |
uvec3 |
同上 |
uvec4 |
同上 |
vec2 |
分量是float类型 |
vec3 |
同上 |
vec4 |
同上 |
dvec2 |
分量是double类型 |
dvec3 |
同上 |
dvec4 |
同上 |
-
以四维矢量为例,各个分量元素分别为x、y、z、w(通常用于表示坐标),或r、g、b、a(通常用于表示颜色),或者s、t、p、q(通常用于表示贴图坐标),这三种写法可以根据用途以示语义差别(写给人看),但语法上不存在差别。若要访问
vec4 vector
的第一个元素,应为vector.x
或vector.r
或vector.s
。 -
通过在
.
后面连写分量名称,可以构成新的矢量。再以vec4 vector
为例,vector.yxy
表示一个vec3类型的矢量,其x和z分量的值等于vector的y分量,其y分量的值等于vector的x分量。注意类似vector.xrs
的表达式不合法,连写的分量名称必须对应单种语义的写法。 -
通过在
.
后面连写分量名称,标量也可以构成新的矢量。以float scalar
为例,scalar.xx
表示一个vec2类型的矢量,每个分量都等于scalar。
着色器中可用以下单精度浮点数矩阵类型:
类型名 |
说明 |
---|---|
mat2 |
2列2行,等价于mat2x2 |
mat2x3 |
2列3行 |
mat2x4 |
2列4行 |
mat3x2 |
3列2行 |
mat3 |
3列3行,等价于mat3x3 |
mat3x4 |
3列4行 |
mat4x2 |
4列2行 |
mat4x3 |
4列3行 |
mat4 |
4列4行,等价于mat4x4 |
-
GLSL中的矩阵默认是列主矩阵(但可以通过在类型前添加
layout(row_major)
来使用行主矩阵),GLM生成的变换矩阵也是列主矩阵。 -
在类型名前加d,即为相应双精度浮点数矩阵类型。
访问矩阵元素时的格式为:矩阵变量名[列索引][行索引],比如:
mat3 matrix; matrix[1] = vec3(3.0, 3.0, 3.0);//第二列元素全赋值为3.0 matrix[2][0] = 16.0; //第三列第一个元素赋值为16.0
着色器中可以定义数组类型,比如int a[2][3]
,同C语言中类似。
此外,着色器中也可以定义结构体类型,同C语言中类似。
图形着色器中通用的输入输出声明方式
用以下语法在着色器中声明输入输出:
//输入 layout(location = 索引) in 插值修饰符 类型 名称; //或 layout(location = 索引) 插值修饰符 in 类型 名称; //输出,若为片段着色器的输出,location为颜色附件索引 layout(location = 索引) out 插值修饰符 类型 名称; //或 layout(location = 索引) 插值修饰符 out 类型 名称;
-
可对特定阶段的输入输出使用插值修饰符,若省略插值修饰符,则默认为smooth。
-
输入来源于先前的阶段,输出则是输出到之后的阶段。当前阶段的输入要与先前阶段的输出匹配,需要匹配索引、插值修饰符、类型,名称可以不同。
-
相应索引的颜色附件对应创建渲染通道时,VkSubpassDescription::pColorAttachments所指数组中相应VkAttachmentReference元素代表的颜色附件。
插值修饰符
可以对以下阶段的输入使用插值修饰符:
1.细分控制着色器(从顶点着色器到细分控制着色器本就不会插值,写插值修饰符为了跟顶点着色器中的修饰符匹配)
2.几何着色器(同样,写插值修饰符为了跟先前着色器中的修饰符匹配)
3.片段着色器
着色器的代码可能适用于某个阶段的着色器可有可无的情况,比如你可以构造出一组着色器,既能走“顶点→几何→片段”的流程,也能走“顶点→片段”的流程,如果片段着色器中需要特定插值修饰符的输入,那么顶点着色器中就得使用相应插值修饰符的输出,而为了匹配,尽管“顶点→几何”不需要插值,这里头几何着色器也得有同样插值修饰符的输入。
可以对以下阶段的输出使用插值修饰符:
1.顶点着色器
2.细分求值着色器
3.几何着色器
插值修饰符有以下三种:
1.smooth是默认的插值方式,它进行双曲插值,会根据gl_Position的w分量考虑透视的影响(经三维相机效果的投影矩阵变换后生成的顶点,透视的影响会反映在其w值上)。
2.flat不进行插值,对某一片段调用片段着色器时,flat的输入取得先前阶段中,该片段对应图元的激发顶点(provoking vertex)的相应值。
3.noperspective进行简单粗暴的线性插值,不考虑透视的影响。
如果输出的gl_Position.w总是为1(通常为渲染平面图形时),smooth和noperspective的效果没有差别。
关于激发顶点,请自行参阅Vulkan官方标准中的图示。
下图左侧为通过noperspective插值的uv坐标绘制的正方体的两个面,右侧则是通过smooth插值的uv坐标绘制:
Push Constant的声明方式
Push constant是在着色器中使用可变更(由CPU侧)常量的两种方式之一。
Push constant适用于少量数据,Vulkan的实现通常会确保你能在push constant块中使用128个字节。
这里介绍如何在着色器中声明push constant,关于更新push constant,参见Ch7-4 初识Push Constant。
layout(内存布局修饰符, push_constant) uniform 块名称 { 成员声明 } 实例名称;
-
若省略
内存布局修饰符,
,则默认为std430,详见后文块成员的内存布局。 -
可以没有实例名称,如此一来能直接用成员名称访问块成员。否则用
实例名称.成员名称
来访问块成员。
块当中的成员可以前缀layout(offset = 距离整块数据起始位置的字节数)
修饰,其具体用例:
//顶点着色器中 layout(push_constant) uniform pushConstants { mat4 proj; vec2 view; vec2 scale; }; //片段着色器中 layout(push_constant) uniform pushConstants { layout(offset = 80) vec4 color; };
以上代码摘自一组很简单的用于2D渲染的着色器,其中,顶点着色器中需要proj矩阵、view和scale两个矢量,而片段着色器只需要color,它们加在一起一共96个字节,可以全部放进push constant中,片段着色器中只需要声明color,但它和proj、view和scale在同一整块数据中,若不想在片段着色器中声明proj、view和scale,则必须写明color的offset。
Uniform缓冲区的声明方式
Uniform缓冲区(uniform buffer)是在着色器中使用可变更(由CPU侧)常量的两种方式之一,类似于HLSL中的常量缓冲区。
相比push constant,uniform缓冲区适用于大量数据。
这里介绍如何在着色器中声明uniform缓冲区,关于如何绑定uniform缓冲区,参见Ch7-5 初识Uniform缓冲区。
layout(set = 描述符集索引, binding = 绑定索引) uniform 块名称 { 成员声明 } 实例名称;
-
若省略
set = 描述符集索引,
,则默认为0号描述符集。 -
实例可以为数组,对应的描述符亦构成数组(创建相应描述符布局时VkDescriptorSetLayoutBinding::descriptorCount大于1)。
-
同push constant一样,可以没有实例名称,如此一来能直接用成员名称访问块成员。
-
同push constant一样,块当中的成员可以前缀
layout(offset = 距离缓冲区起始位置的字节数)
修饰。
其他Uniform对象的声明
注意到上文中声明push constant和uniform缓冲区时皆用到了uniform这个关键字。
Uniform对象指的是着色器中的运行期常量(所谓运行期,指其并非编译期或装配管线时指定),只读不写,且在单次绘制命令的调用中不会改变其数据。
以下几种uniform对象能以类似的方式声明:
-
各类贴图:以texture、itexture、utexture开头的一系列类型,如texture2D。无前缀、i、u前缀分别对应浮点、有符号整形和无符号整形(所涉及注意事项见Ch7-7 使用贴图)。
-
采样器:有sampler和samplerShadow两种。
-
带采样器的贴图:以sampler、isampler、usampler开头的一系列类型,如sampler2D和sampler2DShadow。带Shadow后缀的无i或u前缀版本。
-
Uniform纹素缓冲区:有textureBuffer、itextureBuffer、utextureBuffer三种(写成OpenGL中定义的samplerBuffer、isamplerBuffer、usamplerBuffer也没差,反正都跟采样器没关系)。
Uniform对象的声明方式非常简单,与uniform缓冲区很相似,只是不需要块:
layout(set = 描述符集索引, binding = 绑定索引) uniform 类型 实例名称;
-
类似uniform缓冲区,若省略
set = 描述符集索引,
,则默认为0号描述符集。 -
类似uniform缓冲区,实例可以为数组,对应的描述符构成数组。
输入附件的的声明
最后还剩一种uniform对象:子通道输入(subpass input),即输入附件(input attachment)在GLSL中的概念,有subpssInput、subpssInputMS、isubpssInput、isubpssInputMS、usubpssInput、usubpssInputMS六种类型,MS后缀说明是多重采样附件。
输入附件的声明方式为:
layout(set = 描述符集索引, binding = 绑定索引, input_attachment_index = 输入附件索引) uniform 类型 实例名称;
-
类似uniform缓冲区,若省略
set = 描述符集索引,
,则默认为0号描述符集。
输入附件索引对应VkSubpassDescription::pInputAttachments所指代的相应输入附件。
实例可以为数组,对应的描述符构成数组。啊?你问这要怎么构成数组?以下式为例:
layout(binding = 0, input_attachment_index = 基础索引) uniform subpassInput u_GBuffers[3];
通过表达式实例名称[N]
进行访问时,访问到VkSubpassDescription::pInputAttachments[基础索引 + N]指代的输入附件。
这是Vulkan的GLSL方言中规定的。在将输入附件对应的image view写入描述符时应当注意顺序。
关于输入附件的具体用例见:Ch8-3 延迟渲染
Storage缓冲区的声明
//TODO 等写计算着色器的示例再写。
其他Storage对象的声明
//TODO 等写计算着色器的示例再写。
块成员的内存布局
Vulkan的GLSL着色器中,对于块,允许两种内存布局:std140和std430。不同的内存布局遵循不同的对齐规则。
Uniform缓冲区的内存布局只能为std140。Storage缓冲区和push constant的默认内存布局为std430,可以指定为std140。
内存布局涉及到的对齐规则十分重要,关系到如何在C++中定义相应的结构体。
先来看一个简单的例子:
layout(push_constant) uniform pushConstants { mat4 proj; //offset为0 vec2 view; //offset为64 vec2 scale; //offset为72 float width; //offset为80 float cornerRadius;//offset为84 vec4 color0; //offset为96 vec4 color1; //offset为112 };
-
cornerRadius的大小是4,但color0的offset一下子从88变成了96,这是因为vec4的对齐是16,它距离缓冲区开头的位置必须为16的整数倍。
std430的对齐遵循以下规则:
1.大小为N的标量,对齐为N。
2.二维矢量,每个分量大小为N,则对齐为2N。
3.三维及四维矢量,每个分量大小为N,则对齐为4N(注意,没有12字节对齐)。
4.C列R行的列主矩阵的对齐,相当于R维矢量的对齐。
5.结构体的对齐,取其成员大小中最大的对齐。
6.数组的对齐,取其单个元素大小的对齐。
std140的对齐比std430更严格:
1.结构体的对齐,取其成员大小中最大的对齐并凑整到16的倍数。
2.数组的对齐,取其单个元素大小的对齐并凑整到16的倍数,且数组的每个元素之间的步长等于数组的对齐(即步长也凑整到16的倍数,这会导致由16个int构成的数组的大小是256字节而不是64字节)。
在C++中,可以通过前缀alignas(...)来指定对齐:
struct { alignas(16) mat4 proj; //offset为0 alignas( 8) vec2 view; //offset为64 alignas( 8) vec2 scale; //offset为72 alignas( 4) float width; //offset为80 alignas( 4) float cornerRadius;//offset为84 alignas(16) vec4 color0; //offset为96 alignas(16) vec4 color1; //offset为112 } constant;
GLM中提供了的aligned type来帮你在一定程度上省事:
#define GLM_FORCE_ALIGNED_GENTYPES #include <gtc/type_aligned.hpp> //通过using来简化太长的名称 using vec2a = glm::aligned_vec2; //对齐为 8 using vec3a = glm::aligned_vec3; //对齐为16 using vec4a = glm::aligned_vec4; //对齐为16 using dvec2a = glm::aligned_dvec2;//对齐为16 using dvec3a = glm::aligned_dvec3;//对齐为32 using dvec4a = glm::aligned_dvec4;//对齐为32 using ivec2a = glm::aligned_ivec2;//对齐为 8 using ivec3a = glm::aligned_ivec3;//对齐为16 using ivec4a = glm::aligned_ivec4;//对齐为16 using uvec2a = glm::aligned_uvec2;//对齐为 8 using uvec3a = glm::aligned_uvec3;//对齐为16 using uvec4a = glm::aligned_uvec4;//对齐为16 using mat3a = glm::aligned_mat3; //对齐为16 using mat4a = glm::aligned_mat4; //对齐为16
注意GLM提供的aligned type也存在问题:
//GLSL代码,块大小为16 layout(push_constant) uniform pushConstants { vec3 color; float alpha; }; //C++代码,结构体大小为32 struct { glm::aligned_vec3 color; float alpha; } constant;
因为glm::aligned_vec3的大小和对齐都是16(理想的情况是,大小为12,但对齐为16),所以C++代码中alpha没有填入12~16字节间的空位上。
我建议始终手动alignas(...)而非使用GLM的aligned type。
可特化常量的声明方式和使用
如前文所述,用以下语法声明可特化的常量:
layout(constant_id = ID编号) const 类型 常量名称;
需要注意的是,若在uniform块中声明数组时,应当只有一个数组的大小被可特化常量指定,且该数组应该位于块的最后:
//声明一个可以被特化的常量 layout(constant_id = 0) const uint maxLightCount = 32; //Case 1: 这么做可以 layout(binding = 0) uniform descriptorConstants { vec3 cameraPosition; int lightCount; light lights[maxLightCount];//light是自定义结构体类型,定义略 }; //Case 2: 不要这么做 layout(binding = 0) uniform descriptorConstants { light lights[maxLightCount]; vec3 cameraPosition; int lightCount; };
-
各个块成员的offset是被静态计算的,也就是说,Case 2这种情况,cameraPosition在缓冲区中的位置是根据maxLightCount的默认值进行计算的。假设可以在创建管线时改变maxLightCount(所谓假设,是因为这种情况下创建管线时Vulkan的验证层会报错),cameraPosition也总是在32个light的大小之后的位置。