Learn_Vulkan04_渲染框架实现_材质系统设计
网格(Mesh)用作定义虚拟物体的形状,材质(Material)则是描述这些物体表面的外观,需要借助纹理和材质参数,参数包括光照参数、UV偏移、基础颜色等等。在渲染系统渲染某个网格体时,将对应材质参数传递给着色器,即可完成不同表面的渲染。
如下图,这些网格体利用了不同的材质参数完成渲染。
说明:本文出现的代码,只是为了标明主要流程和设计,大部分只是伪代码,具体实现可查看工程源码。
工程源码:https://gitee.com/xingchen0085/adiosy_engine
渲染循环
以下是该渲染器的主循环伪代码:
伪代码简要描述:
- RenderPass为渲染流程,内部可以包含多个渲染子流程,也是使用RenderPass定义后续需要用的颜色、深度等附件。可以根据RenderPass创建多个RenderTarget。一个Application中可以定义N个RenderPass,所以第一层循环从RenderPass开始。
- RenderTarget为渲染目标,对应的就是不同的FrameBuffer。满足同时预览多个相机的画面的需求,比如对虚拟物体在不同视图下编辑(透视图、正视图、左视图等)。RenderTarget是基于RenderPass创建的,所以第二层循环是RenderTarget,绑定某个RenderTarget,相当于绑定某一套FrameBuffer。
- Material System为材质系统,真正的渲染代码在这里完成。材质系统绑定到了每个RenderTarget上,也就是每个RenderTarget可以自定义处理某些材质系统。所以第三层循环是材质系统。
- Material为材质信息,每个材质系统要渲染绑定了某材质组件的Entity,也就是每个材质系统下会有很多Material。所以第四层循环就是绑定某个材质。
- Mesh为网格体,也就是在这里,正式的提交Draw Call。
以上就是该工程的循环主循环。对于材质系统相关流程,设计主要考虑以下几点:
1/ 不同的RenderTarget绑定不同的材质系统,比如有的RendertTarget只处理非光照材质的Mesh渲染;有的RenderTarget只处理Phong式光照材质的渲染;有的RenderTarget则处理非光照材质、Phong式光照材质、PBR材质等渲染。RenderTarget支持添加或删除某些材质系统。
2/ 材质系统会跟Shader、Pipeline、DescriptorSet图形API对象产生关联,为了渲染和场景树解耦,所以把材质系统作为一种特殊的ECS系统,场景树种的Entity只需要添加某个Material Component即可,相当于标识这个Entity下的Mesh渲染都是使用这个Material System来完成。
3/ 为了性能考虑,减少绑定Descriptor的次数,所以在循环种先循环所有材质,再循环所有绑定该材质下的网格体提交渲染。
材质参数 Material
材质内容包含两个重要信息:材质参数和纹理列表。渲染时,使用Uniform Buffer/Stoage Buffer等将材质参数打包发送给Shader;通过Tetxure/Sampler传递给着色器进行采样。如下图:
材质参数可能是:基础颜色(base color)、漫反射颜色(diffuse)、镜面反射颜色(specular),或者是PBR材质的一些参数等。
不过针纹理列表的话,这里有延申设计了两个概念:
1/ 每个材质内用到的纹理,一般只会读取一次,并将其放入纹理池中以供复用。
2/ 渲染时可以指定一些参数进行采样,额外添加一个TextureView的对象,内部包含纹理指针、采样方式(Sampler,比如过滤、UV寻址方式、Lod等)和UV变换(UV唯一、缩放、旋转)数据。
材质基类定义了基础的数据,然后可以由此派生出其他材质类,比如无光照材质(Unlit)、Phong式光照材质、PBR材质等。
材质基类的基础定义:
struct TextureView{
AdTexture *texture = nullptr; // 纹理
AdSampler *sampler = nullptr; // 采样方式
bool bEnable = true; // 是否启用
glm::vec2 uvTranslation{ 0.f, 0.f }; // UV位移
float uvRotation{ 0.f }; // UV旋转
glm::vec2 uvScale { 1.f, 1.f }; // UV缩放
bool IsValid() const {
return bEnable && texture != nullptr && sampler != nullptr;
}
};
class AdMaterial{
public:
...
...
private:
int32_t mIndex = -1;
std::unordered_map<uint32_t, TextureView> mTextures;
friend class AdMaterialFactory;
};
材质工厂
为了方便创建不同材质,材质的维护统一交给材质工厂来作,不提供外界实例化材质的。重点需要实现的函数就是创建材质。
class AdMaterialFactory{
public:
AdMaterialFactory(const AdMaterialFactory&) = delete;
AdMaterialFactory &operator=(const AdMaterialFactory&) = delete;
static AdMaterialFactory* GetInstance(){
return &s_MaterialFactory;
}
~AdMaterialFactory() {
mMaterials.clear();
}
...
...
template<typename T>
T* CreateMaterial(){
auto mat = std::make_shared<T>();
uint32_t typeId = entt::type_id<T>().hash();
uint32_t index = 0;
if(mMaterials.find(typeId) == mMaterials.end()){
mMaterials.insert({ typeId, { mat }});
} else {
index = mMaterials[typeId].size();
mMaterials[typeId].push_back(mat);
}
mat->mIndex = index;
return mat.get();
}
private:
AdMaterialFactory() = default;
static AdMaterialFactory s_MaterialFactory;
// 所有材质
std::unordered_map<uint32_t, std::vector<std::shared_ptr<AdMaterial>>> mMaterials;
};
材质的创建:
auto matFactory = ade::AdMaterialFactory::GetInstance();
ade::AdUnlitMaterial *unlitMat = matFactory->CreateMaterial<ade::AdUnlitMaterial>();
ade::AdPhongMaterial *phongMat = matFactory->CreateMaterial<ade::AdPhongMaterial>();
ade::AdPBRMaterial *pbrMat = matFactory->CreateMaterial<ade::AdPBRMaterial>();
以上,就是材质数据的维护。材质数据包有了之后,怎么将网格体跟材质扯上关系呢?这就需要材质组件来完成绑定了。
材质组件 Material Component
材质组件由两个重要作用:
1/ 标识某个Entity使用某个材质系统。
2/ 绑定网格体和材质参数。
材质组件的定义:
template<typename T>
class AdMaterialComponent : public AdComponent{
public:
void AddMesh(AdMesh *mesh, T *material = nullptr){
if(!mesh){
return;
}
uint32_t meshIndex = mMeshList.size();
mMeshList.push_back(mesh);
if(mMeshMaterials.find(material) != mMeshMaterials.end()){
mMeshMaterials[material].push_back(meshIndex);
} else {
mMeshMaterials.insert({ material, { meshIndex } });
}
}
uint32_t GetMaterialCount() const {
return mMeshMaterials.size();
}
const std::unordered_map<T*, std::vector<uint32_t>> &GetMeshMaterials() const {
return mMeshMaterials;
}
AdMesh *GetMesh(uint32_t index) const {
if(index < mMeshList.size()){
return mMeshList[index];
}
return nullptr;
}
private:
std::vector<AdMesh*> mMeshList; // 网格体列表
std::unordered_map<T*, std::vector<uint32_t>> mMeshMaterials; // 材质对应的网格体列表
};
// 具体的材质组件
enum UnlitMaterialTexture{
UNLIT_MAT_BASE_COLOR_0,
UNLIT_MAT_BASE_COLOR_1
};
struct UnlitMaterialUbo{
alignas(16) glm::vec3 baseColor0{ 0, 0, 0 };
alignas(16) glm::vec3 baseColor1{ 0, 0, 0 };
alignas(4) float mixValue{ 0.5f };
alignas(16) TextureParam textureParam0;
alignas(16) TextureParam textureParam1;
};
class AdUnlitMaterial : public AdMaterial{
public:
const UnlitMaterialUbo& GetParams() const { return mParams; }
void SetParams(const UnlitMaterialUbo ¶ms){
mParams = params;
bShouldFlushParams = true;
}
private:
UnlitMaterialUbo mParams{};
};
class AdUnlitMaterialComponent : public AdMaterialComponent<AdUnlitMaterial>{
};
alignas 是为了手动内存对齐。该工程中,给Shader传输的数据包全部采用 std140 形式打包。
材质组件的第一个作用如何体现呢?
可以通过下述代码,给Enity添加一个材质组件。
ade::AdEntity *entity = scene->CreateEntity("Cube");
auto &materialComp = entity->AddComponent<ade::AdUnlitMaterialComponent>();
materialComp.AddMesh(mesh1, material0);
materialComp.AddMesh(mesh2, material0);
materialComp.AddMesh(mesh3, material2);
基于此,添加一个MaterialSystem,来专门处理拥有这种MaterialComponent的Entity渲染。
材质系统 Material System
材质系统的重要流程如下:
1/ 初始化渲染管线;包括DescriptorSetLayout、DescriptorPool、Uniform Buffer和PipelineLayout等对象。一个材质系统中可以使用多个Shader、多套渲染管线。
2/ 渲染主循环调用 OnRender() 函数后,根据ECS架构查询出拥有对应材质组件的Entity列表,然后循环所有材质、所有网格执行Draw提交。
3/ 资源销毁。
class AdUnlitMaterialSystem : public AdMaterialSystem{
public:
void OnInit(AdVKRenderPass *renderPass) override;
void OnRender(VkCommandBuffer cmdBuffer, AdRenderTarget *renderTarget) override;
void OnDestroy() override;
...
...
}
材质系统初始化/销毁
void AdUnlitMaterialSystem::OnInit(AdVKRenderPass *renderPass) {
// ---------------------------------------------------------------------------
// 1. Pipeline Layout
ShaderLayout shaderLayout = {
.descriptorSetLayouts = { ... },
.pushConstants = { ... }
};
mPipelineLayout = std::make_shared<AdVKPipelineLayout>(device, shaders, shaderLayout);
// 2. Pipeline
mPipeline = std::make_shared<AdVKPipeline>(device, renderPass, mPipelineLayout.get());
mPipeline->SetVertexInputState(...);
mPipeline->SetInputAssemblyState(...)->EnableDepthTest();
mPipeline->SetDynamicState(...);
mPipeline->Create();
// ---------------------------------------------------------------------------
mDescriptorPool = std::make_shared<ade::AdVKDescriptorPool>(device, 1, poolSizes);
mFrameUboDescSet = mDescriptorPool->AllocateDescriptorSet(...)
// ---------------------------------------------------------------------------
...
...
}
void AdUnlitMaterialSystem::OnDestroy() {
mDescriptorPool.reset();
mPipelineLayout.reset();
mPipeline.reset();
...
...
}
材质系统渲染
void AdUnlitMaterialSystem::OnRender(VkCommandBuffer cmdBuffer, AdRenderTarget *renderTarget) {
// 1. Context params
AdAppContext *appContext = AdApplication::GetAppContext();
AdScene *scene = appContext->scene;
if(!scene){
return;
}
// ---------------------------------------------------------------------------
// 2. !!! 这里是重点,这个使用entt::view函数,筛选出材质组件
entt::registry ® = scene->GetEcsRegistry();
auto view = reg.view<AdTransformComponent, AdUnlitMaterialComponent>();
if(view.begin() == view.end()){
return;
}
// ---------------------------------------------------------------------------
// 3. bind pipeline
mPipeline->Bind(cmdBuffer);
vkCmdSetViewport(cmdBuffer, ...);
vkCmdSetScissor(cmdBuffer, ...);
// 4. Update frame descriptor set
UpdateFrameUboDescSet(renderTarget->GetCamera(), ...);
// 5. Render
view.each([this](const AdTransformComponent &transComp, const AdUnlitMaterialComponent &materialComp){
for (const auto &entry: materialComp.GetMeshMaterials()){
AdUnlitMaterial *material = entry.first;
int32_t materialIndex = material->GetIndex();
VkDescriptorSet paramDescSet = mMaterialDescSets[materialIndex];
VkDescriptorSet resourceDescSet = mMaterialResourceDescSets[materialIndex];
...
// ---------------------------------------------------------------------------
// Update material descriptor set
if(shouldForceUpdateMaterial || material->ShouldFlushParams()){
UpdateMaterialParamsDescSet(paramDescSet, material);
material->FinishFlushParams();
}
if(shouldForceUpdateMaterial || material->ShouldFlushResource()){
UpdateMaterialResourceDescSet(resourceDescSet, material);
material->FinishFlushResource();
}
...
// ---------------------------------------------------------------------------
// Bind Descriptor set and push constant
VkDescriptorSet descriptorSets[] = { mFrameUboDescSet, paramDescSet, resourceDescSet ... };
vkCmdBindDescriptorSets(descriptorSets);
vkCmdPushConstants(...);
// Draw Meshes...
for (const auto &meshId: entry.second){
materialComp.GetMesh(meshId)->Draw(cmdBuffer);
}
}
});
}
设计规则
上文把该工程材质系统设计用到的内容都列了以下,但其实内部还有很多小的细节,又或者有的是困惑了非常之久的问题。所以我将部分再详细描述以下。
1. DescriptorSet规范
上文提到,关于渲染的大部分内容其实是在Material System中实现的。和Shader打交道的就是Material System,我们可以通过Uniform Buffer和Texture等方式将数据传递给Shader。但有一个问题是,VkDescriptorSet是一个透明对象,它和CommandBuffer类似,也是需要从Pool中申请处理,然后执行”录制“,再提交给GPU。只不过这里的”录制“就是将材质参数和纹理绑定到DescriptorSet,调用的是vkUpdateDescriptorSets()
这个API。
但是,当后期渲染场景变大,材质数量变多之后,如果每次都执行一遍vkUpdateDescriptorSets(),会导致性能问题。所以控制vkUpdateDescriptorSets()
的调用次数就很重要了。
那怎么做呢?
一般的方案是:传递给着色器的数据,根据不同的更新频率分类,将其分为若干个DescriptorSet,单独更新。切记不要将不同更新频率的数据放在同一个DescriptorSet中,这样会白白浪费性能。
举个例子:我们可以分为一下几种DescriptorSet。
注:每个Pipeline的DescriptorSet数量也是有限制的,可以通过查询VkPhysicalDeviceProperties::limits::maxBoundDescriptorSets 得到。
Descriptor set | 数据类型 | 更新频率 |
---|---|---|
DS0 | 全局/每帧的数据;比如环境纹理、雾或当前时间、鼠标位置、当前选中物体、风速等数据 | 只更新一次或每帧更新一次 |
DS1 | 每个材质的参数 | 每个材质更新一次 |
DS2 | 每个材质的纹理数据,纹理数据一般更新频率会比材质参数低,所以跟材质参数分开 | 每个材质更新一次 |
DS3 | 每个Entity的参数(最好使用PushConstant) | 每个物体更新一次 |
DS4 | 每个Mesh的参数(最好使用PushConstant) | 每个Mesh更新一次 |
DS0 更新频率稳定,基本上不会有太大性能问题。
DS1/DS2 每个材质参数和纹理数据,则需要在材质中维护两个变量,标识这个材质是否发生变更了,如果发生变更了,更新对应的DescriptorSet后,再将其标识更新为false即可。如下代码:
class AdMaterial{
public:
...
...
// 提供给 Material System 查询
bool ShouldFlushParams() const { return bShouldFlushParams; }
// 提供给 Material System 查询
bool ShouldFlushResource() const { return bShouldFlushResource; }
// 提供给 Material System 设置
void FinishFlushParams() { bShouldFlushParams = false; }
// 提供给 Material System 设置
void FinishFlushResource() { bShouldFlushResource = false; }
void AdMaterial::SetTextureView(uint32_t id, AdTexture *texture, AdSampler *sampler) {
if(HasTexture(id)){
mTextures[id].texture = texture;
mTextures[id].sampler = sampler;
} else {
mTextures[id] = { texture, sampler };
}
bShouldFlushResource = true; // texture替换时更新标识 !!! (1)
}
protected:
bool bShouldFlushParams = false; // params是否发生变更,material内部设置
bool bShouldFlushResource = false; // texture是否发生替换,material内部设置
...
...
};
class AdUnlitMaterial : public AdMaterial{
public:
const UnlitMaterialUbo& GetParams() const { return mParams; }
void SetParams(const UnlitMaterialUbo ¶ms){
mParams = params;
bShouldFlushParams = true; //params发生变更时更新标识 !!! (2)
}
private:
UnlitMaterialUbo mParams{};
};
void AdUnlitMaterialSystem::OnRender(VkCommandBuffer cmdBuffer, AdRenderTarget *renderTarget) {
...
...
...
view.each([](...){
...
int32_t materialIndex = material->GetIndex();
VkDescriptorSet paramDescSet = mMaterialDescSets[materialIndex];
VkDescriptorSet resourceDescSet = mMaterialResourceDescSets[materialIndex];
if(shouldForceUpdateMaterial || material->ShouldFlushParams()){
UpdateMaterialParamsDescSet(paramDescSet, material); //更新params descset !!! (3)
material->FinishFlushParams();
}
if(shouldForceUpdateMaterial || material->ShouldFlushResource()){
UpdateMaterialResourceDescSet(resourceDescSet, material);//更新resource descset !!! (4)
material->FinishFlushResource();
}
...
...
}
}
DS3 每个Enity的数据,比如Enity的状态,模型矩阵等信息,这类数据每个Enitity都需要更新。最好使用Push_Constant常量推送方式,只有在大小超过了Push_Constant范围后再考虑使用DescriptorSet。
DS4 每个Mesh的数据,比如Mesh额外信息,这类数据每个Mesh都需要更新。最好使用Push_Constant常量推送方式,只有在大小超过了Push_Constant范围后再考虑使用DescriptorSet。
举个例子:我设计了一个UnlitMaterial的材质,它是一个无光照材质。片元着色器中接收两个base_color的值,这两个base_color通过mix()函数混合;另外base_color也可以是两张纹理提供颜色数据。所以我的着色器编写如下:
#version 450
layout(location=0) in vec3 a_Pos;
layout(location=1) in vec2 a_Texcoord;
layout(location=2) in vec3 a_Normal;
out gl_PerVertex{
vec4 gl_Position;
};
layout(set=0, binding=0, std140) uniform FrameUbo{
mat4 projMat;
mat4 viewMat;
ivec2 resolution;
uint frameId;
float time;
} frameUbo;
layout(push_constant) uniform PushConstants{
mat4 modelMat;
mat3 normalMat;
} PC;
out layout(location=1) vec2 v_Texcoord;
void main(){
gl_Position = frameUbo.projMat * frameUbo.viewMat * PC.modelMat * vec4(a_Pos.x, a_Pos.y, a_Pos.z, 1.f);
v_Texcoord = a_Texcoord;
}
#version 450
layout(location=1) in vec2 v_Texcoord;
struct TextureParam{
bool enable;
float uvRotation;
vec4 uvTransform; // x,y --> scale, z,w --> translation
};
vec2 getTextureUV(TextureParam param, vec2 inUV){
vec2 retUV = inUV * param.uvTransform.xy; // scale
retUV = vec2( // rotation
retUV.x * sin(param.uvRotation) + retUV.y * cos(param.uvRotation),
retUV.y * sin(param.uvRotation) + retUV.x * cos(param.uvRotation)
);
inUV = retUV + param.uvTransform.zw; // translation
return inUV;
}
layout(set=0, binding=0, std140) uniform FrameUbo{
mat4 projMat;
mat4 viewMat;
ivec2 resolution;
uint frameId;
float time;
} frameUbo;
layout(set=1, binding=0, std140) uniform MaterialUbo{
vec3 baseColor0;
vec3 baseColor1;
float mixValue;
TextureParam textureParam0;
TextureParam textureParam1;
} materialUbo;
layout(set=2, binding=0) uniform sampler2D texture0;
layout(set=2, binding=1) uniform sampler2D texture1;
layout(location=0) out vec4 fragColor;
void main(){
vec3 color0 = materialUbo.baseColor0;
vec3 color1 = materialUbo.baseColor1;
if(materialUbo.textureParam0.enable){
TextureParam param = materialUbo.textureParam0;
param.uvTransform.w = -frameUbo.time;
color0 = texture(texture0, getTextureUV(param, v_Texcoord)).rgb;
}
if(materialUbo.textureParam1.enable){
color1 = texture(texture1, getTextureUV(materialUbo.textureParam1, v_Texcoord)).rgb;
}
fragColor = vec4(mix(color0, color1, materialUbo.mixValue), 1.0);
}
2. DescriptorSet和Material参数隔离
问题:更新Material数据需要DescriptorSet和UniformBuffer等Vulkan对象,但问题是,申请DescriptorSet需要DescriptorPool和DescriptorSetLayout参数,并且DescriptorSetLayout和Shader定义挂钩,也跟Pipeline有关系。所以如果把DescriptorSet和UniformBuffer加到Material中,会导致Material对象和图形API关联过大,无法解耦。
先说一下目前的解决方案:
1/ Material中只维护参数和纹理信息,不维护DescriptorSet信息;
2/ 在Material System中维护N个DescriptorSet,并且DescriptorPool和DescriptorSetLayout等参数都在Material System中维护,Material作为一种”数据包“给到Material System使用;
3/ Material的不提供外部直接创建,而是由Material Factory统一维护,并且在Material中维护一个Material index字段(int32_t类型),并通过该字段和Material System中的Descriptor Set关联,index是两套数据的下标,时间复杂度为O1。
优劣势分析。
优势:
- Material 和 DescriptorSet解耦。
- 不需要将Material System中的Pipeline、DescriptorPool和DescriptorSetLayout等参数对外提供,内部处理即可。
- Material System中可以自由指定DescriptorSet,分配和释放由Material System内部处理。
劣势:
- 过度依赖Material Index字段,且该字段没有实际意义,仅为了程序识别。
- 需要两边数据(Material和DescriptorSet)协调配合,处理不好的话出问题概率较大。
实现:
1/ Material中维护一个Index字段。
class AdMaterial{
public:
...
...
private:
int32_t mIndex = -1;
std::unordered_map<uint32_t, TextureView> mTextures;
friend class AdMaterialFactory;
};
2/ AdMaterialFactory在创建Material时,赋值Index字段。并且支持查询某种类型Material的总数量。
class AdMaterialFactory{
public:
...
...
template<typename T> // 返回某种类型Material的数量
size_t GetMaterialSize(){
uint32_t typeId = entt::type_id<T>().hash();
if(mMaterials.find(typeId) == mMaterials.end()){
return 0;
}
return mMaterials[typeId].size();
}
template<typename T>
T* CreateMaterial(){
auto mat = std::make_shared<T>();
uint32_t typeId = entt::type_id<T>().hash();
uint32_t index = 0;
if(mMaterials.find(typeId) == mMaterials.end()){
mMaterials.insert({ typeId, { mat }});
} else {
index = mMaterials[typeId].size();
mMaterials[typeId].push_back(mat);
}
mat->mIndex = index; // material index
return mat.get();
}
private:
AdMaterialFactory() = default;
static AdMaterialFactory s_MaterialFactory;
std::unordered_map<uint32_t, std::vector<std::shared_ptr<AdMaterial>>> mMaterials;
};
3/ 在Material System中,根据material Index取出对应的Descriptor set使用。
// 维护Material descriptor pool 和 descriptor sets
void AdUnlitMaterialSystem::ReCreateMaterialDescPool(uint32_t materialCount) {
mMaterialDescriptorPool = std::make_shared<ade::AdVKDescriptorPool>(...);
mMaterialDescSets = mMaterialDescriptorPool->AllocateDescriptorSet(...);
mMaterialResourceDescSets = mMaterialDescriptorPool->AllocateDescriptorSet(...);
}
// 渲染
void AdUnlitMaterialSystem::OnRender(VkCommandBuffer cmdBuffer, AdRenderTarget *renderTarget) {
...
...
view.each([](...){
...
int32_t materialIndex = material->GetIndex();
VkDescriptorSet paramDescSet = mMaterialDescSets[materialIndex];
VkDescriptorSet resourceDescSet = mMaterialResourceDescSets[materialIndex];
...
...
}
}
3. DescriptorPool根据Material的数量动态增长
其实这个问题是和【2.DescriptorSet和Material参数隔离】有关系的,我上面列出来的代码有个函数名为ReCreateMaterialDescPool()
,为什么需要重新创建DescriptorSetPool呢?这是因为有这个矛盾点:系统内部的材质总数量是未知的,可能只有10个,也可能有10000个。但DescriptorSetPool是需要指定每种DescriptorSet的数量和总数量(maxSets字段)的。
也就意味着,如果DescriptorSetPool可申请的DescriptorSet数量过少,会报错VK_ERROR_OUT_OF_POOL_MEMORY
,但如果给设置的数量过大,也会浪费内存。
解决这个问题,目前我了解到的有三种解决方案:
1/ 创建DescriptorSetPool时,指定VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT 标识。当指定该标识后,Descriptor的释放需要手动进行,所以可以做到动态的申请和是否DescriptorSet。但这就没用到DescriptorPool原生释放上的一些优化,针对Material,也不应该每一帧都重新申请再释放DescriptorSet,这样意味着每一帧都需要vkUpdateDescriptorSets()。所以弃用该方案。
2/ 手动指定vkResetDescriptorPool(),重置Pool,但也需要每一帧都需要vkUpdateDescriptorSets()。所以弃用该方案。
3/ 申请一定量的DescriptorSet后,将其缓存,然后复用这些DescriptorSet。最终Destory DescriptorPool时自动释放这些DescriptorSet。这个方案可行,但这个”一定量“不好把控。
我目前考虑使用的方案是:采用方案三,然后默认初始化16个DescriptorSet,当发现Material数量超过16时,以2倍的速率扩容,扩容时重新创建一次DescriptorPool和申请DescriptorSet。该方案使用这样的扩容机制:16 -> 32 -> 48 -> 96 -> 128 -> 256 -> 512 -> 1024 -> 2048…
实现:
1/ 定义默认DescriptorSet数量和最大数量。
#define NUM_MATERIAL_BATCH 16
#define NUM_MATERIAL_BATCH_MAX 2048
2/ 扩容机制实现。
void AdUnlitMaterialSystem::ReCreateMaterialDescPool(uint32_t materialCount) {
uint32_t newMaterialCount = mLastMaterialCount;
if(mLastMaterialCount == 0){ // init
newMaterialCount = NUM_MATERIAL_BATCH;
}
while (newMaterialCount < materialCount){
newMaterialCount *= 2; // (1)计算扩容数量
}
if(newMaterialCount > NUM_MATERIAL_BATCH_MAX){ // 最大数量限制
LOG_E("Material max count is : {0}, but request : {1}", NUM_MATERIAL_BATCH_MAX, newMaterialCount);
return;
}
LOG_W("{0}: {1} -> {2} S.", __FUNCTION__, mLastMaterialCount, newMaterialCount);
AdRenderContext *renderCxt = AdApplication::GetAppContext()->renderCxt;
AdVKDevice *device = renderCxt->GetDevice();
mMaterialDescSets.clear(); // (2)销毁之前申请的DescriptorSet
mMaterialResourceDescSets.clear(); // 因为DescriptorPool被销毁后,这些DescSet也不能使用了
if(mMaterialDescriptorPool){
mMaterialDescriptorPool.reset(); // 销毁旧的DescriptorPool
}
mMaterialDescriptorPool = std::make_shared<ade::AdVKDescriptorPool>(device, newMaterialCount * 2, ...);
mMaterialDescSets = mMaterialDescriptorPool->AllocateDescriptorSet(..., newMaterialCount);
mMaterialResourceDescSets = mMaterialDescriptorPool->AllocateDescriptorSet(..., newMaterialCount);
...
...
uint32_t diffCount = newMaterialCount - mLastMaterialCount;
for(int i = 0; i < diffCount; i++){
mMaterialBuffers.push_back(std::make_shared<AdVKBuffer>(...)); // (3)添加新的Uniform Buffer
}
mLastMaterialCount = newMaterialCount;
}
3/ 扩容后,因为旧的DescriptorSet被销毁,所以需要重新更新Material数据到DescriptorSet中。
// 判断是否扩容
bool shouldForceUpdateMaterial = false;
uint32_t materialCount = AdMaterialFactory::GetInstance()->GetMaterialSize<AdUnlitMaterial>();
if(materialCount > mLastMaterialCount){
ReCreateMaterialDescPool(materialCount);
shouldForceUpdateMaterial = true; // 扩容时更新Material数据到DescriptorSet
}
view.each([this, &shouldForceUpdateMaterial](const AdTransformComponent &transComp, const AdUnlitMaterialComponent &materialComp){
for (const auto &entry: materialComp.GetMeshMaterials()){
AdUnlitMaterial *material = entry.first;
int32_t materialIndex = material->GetIndex();
VkDescriptorSet paramDescSet = mMaterialDescSets[materialIndex];
VkDescriptorSet resourceDescSet = mMaterialResourceDescSets[materialIndex];
...
// ---------------------------------------------------------------------------
// Update material descriptor set
if(shouldForceUpdateMaterial || material->ShouldFlushParams()){
UpdateMaterialParamsDescSet(paramDescSet, material);
material->FinishFlushParams();
}
if(shouldForceUpdateMaterial || material->ShouldFlushResource()){
UpdateMaterialResourceDescSet(resourceDescSet, material);
material->FinishFlushResource();
}
...
...
}
});
总结
关于材质系统的设计,受之前OpenGL写过一个版本的影响,还是将其兼容到ECS中去了,发现的确很方便,这是一大幸事。但是,因为OpenGL和Vulkan的差别很大,特别是DescriptorSet这部分,绞尽脑汁设计了特别久,比如下图是我当时考虑的所有设计方案,对比各方案优缺点,然后尝试能不能处理掉缺点。实在不能解决就弃用。
不过,上述方案都被我自己推翻了。最终就是舍弃掉一部分之前的想法,折中设计出当前这个方案。所以该方案在后续还会一点点的去优化。
所以,整个过程还是收获良多的,收获到一些架构设计的经验,当一个架构能做到方便理解、方便维护、方便拓展是非常有成就感的一件事。另外也体会到了Vulkan一些设计的优势之处,对Vulkan的了解又更深入了些。
- Author: xingchen
- Link: http://www.adiosy.com/posts/learn_vulkan/learn_vulkan04_%E6%B8%B2%E6%9F%93%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0_%E6%9D%90%E8%B4%A8%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1.html
- License: This work is under a 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. Kindly fulfill the requirements of the aforementioned License when adapting or creating a derivative of this work.