漫谈图形计算中的材质系统
Posted 华为移动服务
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了漫谈图形计算中的材质系统相关的知识,希望对你有一定的参考价值。
通用材质系统介绍
材质系统是一个实时渲染引擎非常重要的部分,它使得开发者能够非常便捷地设计出具有真实感的场景和角色。一个好的材质系统可以提高引擎的易用性,并可以方便的扩展渲染效果,来提升渲染质量和效率。
材质系统需求
图形引擎通常需要支持不同的渲染效果,一个优秀的材质系统通常要支持多Pass渲染管线和自定义Shader模板,由于渲染效果的复杂多样性会导致Shader数量大幅增加,这样会造成Shader文件冗余,因此材质系统要提供一套Shader复用的机制。同时,市面上各硬件厂商对图形API的支持程度不同,受限于硬件水平的差异,材质系统也要兼容中低高端硬件。综上所述,通用材质系统需要满足以下需求:
多Technique:材质中包含多个实现方案,这样在进行高中低端机适配或实现不同材质效果时,我们可以方便进行材质更替。
多Pass:对于复杂绘制效果,单次绘制无法实现,常常包含多个Pass的渲染。
自定义Shader:减少Shader数量,提供Shader复用机制。
模板 + 实例:材质是一个模板,通过对某一个材质进行实例化,指定不同的数据和贴图,就可以让物体表现出不同的显示效果。
材质数据
材质描述了场景中物体与光照进行交互的过程,本质上它是指能够描述一个物体显示外观的一系列数据,它包括几个方面:
- 着色模型(Shading Model):着色器的组合,决定了材质的参数与光照参数如何被处理,从而合成最终的颜色。比如最基本的着色模式为:Surface Color = Diffuse + Specular + Emissive + Ambient。
- 渲染状态:比如剔除模式(正面、背面、不剔除),混合模式(开启,关闭),混合因子,深度测试,模板测试等。
- 混合模式(Blend Mode):决定了几何对象渲染完成后如何与场景中的其他物体进行叠加,混合模式会影响对象的绘制顺序,混合模式的渲染次序从先到后是:不透明(Opaque) > 蒙版(Masked) > 半透明(Translucent) > 叠加(Additive) > 相乘(Modulate)。
- 参数: Shader中使用到的Uniform参数,比如纹理贴图,采样器,颜色因子,相机参数,光源参数,Pass间的混合参数等。
材质模板
材质文件就是将上述的材质数据进行合理的组织,方便应用开发者使用,通常材质文件被划分为三个重要的模块:
- Defines:宏列表,定义Shader宏有什么值。
- Properties:定义Shader中参数的值。
- Technique:Pass列表,定义渲染用的状态和Shader文件。
总结下来,一个材质模板文件应该是类似这样的一个结构:
Material “ForwardPbr” {
Properties {
Color(“Color”,Color)=(1,1,1,1)
SpecularColor(“SpecularColor”,Color)=(1,1,1,1)
Gloss(“Gloss”,Range(8.0,256))=20
}
Technique {
Pass {
Blend One One
CullMode None
SkinningEnable true
Shader “ForwardPbr.vert”
Shader “ForwardPbr.frag”
}
Pass{}
}
Technique {
Pass{}
Pass{}
}
}
CGKit的材质系统
图形引擎中提到的材质贴图和物体颜色,高光计算,ALPHA混合、纹理过滤、裁剪模式等,在Vulkan中大多数由渲染管线控制。
Vulkan图形渲染管线介绍
Vulkan中的图形管线决定了顶点数据如何被程序加工与处理,以及几何对象的渲染顺序,提交到设备的状态控制值,着色器模型,是图形引擎的核心模块,Vulkan的图形管线包括以下几个部分(其中Vulkan通过Pipeline State Object进行状态管理):
在图形引擎或游戏引擎中,我们通常用材质文件来描述上述PSO状态数据,Shader数据和贴图数据,通过各种变换操作,最终将网格使用到的顶点数据转化为屏幕空间像素。材质决定了应用最后的展示效果和图像质量。
CGKit材质系统介绍
CGKit的材质系统同样也要满足通用材质系统的需求,支持多Technique、多Pass和自定义Shader。
CGKit材质系统架构图和类
CGKit的材质系统主要由以下类组成,我们简短介绍下它们的功能:
Material:包含了创建一个材质需要的所有资源,包括属性定义,Shader文件定义,纹理贴图,渲染状态设置,由多个Technique组成。
PipelineState:Vulkan的PSO的封装,包括了管线中的所有状态。
ShaderProgram:表示渲染一个模型用到的所有Shader,负责把glsl编译成SPIRV,并反射出所有的ShaderResource。
ShaderResource:通过Shader反射系统获取的Shader资源,可以获取到资源的名字,位置等信息。
DescriptorSetLayout:定义了Shader中的资源与DescriptorSet的映射。
PipelineLayout:管理一组描述符集合布局。
DescriptorSet:管理一组Shader资源。
RenderPipeline:用于渲染的Vulkan管线。
MaterialInstance:根据Material文件创建材质实例,会根据Material文件创建DescriptorSetLayout,DescriptorSet和RenderPipeline。
它对应的架构图如下所示:
CGKit材质模板
CGKit使用json文件格式定义材质模板,材质文件描述伪代码如下:
“Material” : {
“basePath” : “material/ ForwardPbr.cgmat”,
“Properties” : [{
“name” : “Color”,
“type” : “vec4”,
“value” : “1,1,1,1”
},
{
“name” : “albedo”,
“type” : “texture”,
“value” : “models/chip/chip_albedo.png”
},
{
“name” : “normal”,
“type” : “texture”,
“value” : “models/chip/chip_normal.png”
}]
“Techniques” : [{
“Pass”:[{
"rasterizationState": {
"cullMode": " NONE"
},
"depthStencilState": {
"depthTestEnable": true,
"depthWriteEnable": true
},
“SkinningEnable” : true,
"shader": [{
"type": "SHADER_STAGE_TYPE_FRAGMENT"
"uri": "shaders/basic_pbr.frag",
}],
}]
}]
}
CGKit自定义Shader
材质系统中最重要的一块就是Shader文件的配置,实现Shader的自定义需要完成以下功能:
- Shader编译;
- Shader代码复用;
- Shader拼接;
- Shader反射;
- Shader参数更新;
Shader编译
Shader只是一段可执行的汇编代码,我们无论是使用GLSL、HLSL、CG,或者使用Unity的Unity Shader,最终提交给GPU时,都需要将这些高层实现语言编译成二进制的汇编语言。
CGKit的图形API是Vulkan,而Vulkan使用的是SPIRV格式的Shader,我们通过KhronosGroup提供的Glslang可以将GLSL、HLSL编写的Shader代码编译成SPIRV中间代码。CGKit使用Glslang将GLSL转换称为SPIRV:
External/`uname -s`/bin/GlslangValidator -H -o Asset/Shaders/Vulkan/pbr_ps.spv Asset/Shaders/Vulkan/pbr.frag
Shader代码复用
不同的渲染效果需要不同的Shader实现,每个Shader完全独立输入的方式会造成Shader文件大量的冗余,CGKit提供了一套Shader代码复用的机制,通过将Shader进行模块划分并增加预处理宏来减少Shader数量。
鉴于Shader存在大量通用的数据结构及函数,通过对Shader进行合理模块划分,可以达到Shader代码复用的功能。比如我们对不同的材质效果进行整理,找出它们数据结构之间的共性,抽取通用部分放在独立的glsl文件里,然后将剩余独有的部分保留在各自的文件里。
通常我们会将一些常量数据,如灯光,MVP矩阵,相机参数,材质贴图(如阴影图,PBR材质模型贴图)放在cbuffer.glsl文件中。同样的会将一些通用算法,如求交函数,伪随机函数,插值函数,光照阴影计算,PBR中的各种GGX计算函数放在一个functions.glsl文件中。
为了复用Shader的数据结构和算法,CGKit在Shader中定义了预处理宏,通过在材质文件中开启或关闭这些宏来动态启用或关闭Shader代码,达到了减少Shader文件数量的目的。例如我们可以动态开启和关闭一些渲染效果,如光照,阴影,雾效等等。
因为要动态开启和关闭宏,CGKit通过Glslang对Shader实时编译,为避免实时编译增加Shader的加载时间,CGKit同时也提供了Shader缓存机制。
Shader拼接
CGKit使用GLSL Shader,由于GLSL语言不支持#include预编译命令,我们需要用命令行工具把不同模块的Shader文件重新组合起来,形成一个完整的GLSL Shader:
cat Asset/Shaders/cbuffer.glsl Asset/Shaders/functions.glsl pbr_ps.glsl > Asset/Shaders/Vulkan/pbr.frag
Shader反射
对于Shader里面的符号变量,如uniform buffer,texture sampler,push constant,specialization constant,CGKit需要与这些符号变量进行交互,通过材质系统设置或更新它们的值,因此,我们需要通过一套反射机制获取到对应变量的name,set,bind,location等信息。
SPIRV-Cross提供了一套Shader的反射机制,CGKit首先通过Glslang将指定的GLSL格式的Shader代码编译成SPIRV,再通过SPIRV Reflection将SPIRV code里面的符号变量全部反射出来。
Shader参数更新
Shader中的数据流主要包括两部分:
- vertex、index buffer等mesh提供的数据:这部分属于Shader固定输入,在创建管线的时候指定好顶点格式声明,在渲染的时候绑定相应的顶点,索引buffer即可。
- uniform buffer,texture sampler:这部分输入需要CGKit通过Descriptor Set进行设置和更新。通过SPIRV-Cross的Shader反射,我们可以获取到对应资源的名字,位置信息。因为我们是通过材质文件来更新这些Shader资源的,所以我们在材质文件里面指定了这些参数,通过严格按名字匹配来更新Shader资源。因此我们建议用户尽量统一Shader里面的参数名字,并定义在公共头文件中。
CGKit材质创建
CGKit根据材质模板生成材质实例,生成材质实例的过程其实是自动化创建Vulkan纹理贴图,描述符集合布局,管线布局,描述符集合,渲染管线的过程。
CGKit加载材质的时候根据Shader反射填充好描述符集合,在更新Shader的uniform buffer,texture sampler时,会相应地更新DescriptorSet,在提交绘制命令时,只需要绑定不同的DescriptorSet就能切换不同的资源。
创建DescriptorSetLayout
创建描述符集合布局分两步:
1. 通过Shader反射机制获取ShaderResource:材质文件里面定义了一个渲染对象需要用到的所有Shader,我们通过Shader的反射机制将Shader文件里面的符号变量资源反射出来,作为一个Shader资源存放在ShaderProgram类,Shader资源包含了资源的名字以及所属的描述符集合的索引和绑定槽,类似下面的结构体:
struct ShaderResource {
String name = “”;
ShaderStageFlag stageFlag = SHADER_STAGE_VERT;
ShaderResourceType type; // 资源类型
u32 set = 0;
u32 binding = 0; // binding
u32 arraySize = 0; // 对应VkDescriptorSetLayoutBinding的descriptorCount
u32 offset = 0; // for push constants
u32 size = 0; // for push constants
u32 constantID = 0; // for specialization constants
u32 location = 0;
u32 inputAttachmentIndex = 0;
u32 vecSize = 0;
u32 columns = 0;
};
其中Shader中资源的类型如下:
enum ShaderResourceType {
SHADER_RESOURCE_TYPE_INPUT,
SHADER_RESOURCE_TYPE_OUTPUT,
SHADER_RESOURCE_TYPE_BUFFER_UNIFORM,
SHADER_RESOURCE_TYPE_BUFFER_STORAGE,
SHADER_RESOURCE_TYPE_INPUTATTACHMENT,
SHADER_RESOURCE_TYPE_IMAGE,
SHADER_RESOURCE_TYPE_IMAGE_SAMPLERR,
SHADER_RESOURCE_TYPE_IMAGE_STORAGE,
SHADER_RESOURCE_TYPE_SAMPLER,
SHADER_RESOURCE_TYPE_PUSH_CONSTANT, // for pipeline layout creating
SHADER_RESOURCE_TYPE_SPECIALIZATION, // for Shader stage creating
SHADER_RESOURCE_TYPE_All
};
2. 根据ShaderResource创建描述符集合布局:通过Shader反射后ShaderProgram类最终拥有不同的描述符集编号,及其对应的ShaderResource。我们根据ShaderResource生成DescriptorSetLayoutBinding,当然,要去掉四种没有绑定槽的资源(Input,Output,PushConstant,SpecializationConstant)。然后根据DescriptorSetLayoutBinding信息生成DescriptorSetLayout。在DescriptorSetLayout类中,我们可以根据资源的名字得到它的绑定槽,以及对应的描述符布局绑定信息。
创建DescriptorSet
创建描述符集合分两步。
1. 创建DescriptorPool:规定好每个描述符池能够分配的最大描述符集合个数,假定为16,从DescriptorSetLayout中获取所有的Bindings,统计描述符的数量,用这个数与最大描述符集合个数相乘,得到描述符池的大小,依据这个大小创建描述符池。描述符池会容许创建16个描述符集合,如果描述符集合的数量超过了16,则重新分配一个描述符池。
2.根据DescriptorSetLayout和DescriptorPool生成描述符集合:同类型的描述符集合会对应多个描述符池。
创建PipelineLayout
根据DescriptorSetLayout和Shader中的push constant资源创建管线布局。
创建RenderPipeline
从PipelineState中获取管线的状态信息和Shader信息,从mesh中拿到顶点布局信息,创建管线。
CGKit材质应用
材质资源一旦被创建,就可以添加到各种渲染组件中进行渲染。如果要修改材质表现效果,我们只需要在运行时动态修改材质参数,包括渲染状态,纹理参数,Shader参数,Shader文件,就可以达到目的,不需要关注材质系统底层做了什么事情。
CGKit材质系统优化
材质排序
我们都知道,像Vulkan这样的图形接口每设置一次GPU状态的时候,都会有一定的开销。为保证渲染流畅,我们要尽量减少状态切换来降低开销。
在CGKit中,通过对几何对象的材质进行分组排序,将相似的材质排在一起可以减少渲染流程中的状态切换,从而达到提高渲染效率的目的,分组的顺序如下:
- 先按混合模式分组,顺序为:不透明 > 蒙版 > 半透明 > 叠加 > 相乘;
- 混合模式分组后,每组中的对象再按着色模型分组;
- 着色模型分组后,每组对象再按纹理分组;
- 纹理分组后,再按其他参数分组。
即分组的优先级为:混合模式 > 着色模型 > 纹理对象 > 其他参数。
调整资源更新频率
Shader资源在渲染时需要不断更新,而且每个资源的更新频率会不一样。应用需要指定每个资源的更新频率,按照资源的更新频率可以把Shader资源分为三种类型:
- Static:只要绑定了就不会改变的资源,例如相机属性(包括相机位置,视图矩阵,投影矩阵),光照属性(光源类型,光源位置,光源方向,光源颜色,光照强度,光源衰减因子),屏幕宽高,阴影Shadowmap等全局常量。
- Mutable:相当于材质的更新频率,例如漫反射贴图、法线贴图,自发光贴图,切换一个材质就会更新一次。
- Dynamic:随时都可能更新的资源,如模型的世界矩阵。
预先创建管线
Vulkan中的图形渲染管线几乎不可改变,如果需要更改Shader,混合,光栅化等状态的设置,则必须重新创建管线。因此我们可以预先创建好所有的管线,这样管线的操作都是提前知道的,则可以通过驱动程序更好地优化它。
缓存机制
随着场景复杂度的增加,材质文件数量会变多,与材质创建相关的资源会大量重复,我们可以将这些资源缓存起来,避免资源的重复创建并加快资源的加载和创建。与材质创建相关的资源主要有Texture,Shader,DescriptorSetLayout,PipilineLayout,DescriptorSet,enderPipiline,我们可以将这些资源都缓存起来,加载资源的时候,先从缓存里面查找,找不到,再从磁盘中加载和创建。
>>访问华为图形计算服务官网,了解更多相关内容
>>获取华为图形计算服务开发指导文档
>>华为HMS Core官方论坛
>>华为图形计算服务开源仓库地址:GitHub、Gitee
点击右上角头像右方的关注,第一时间了解华为移动服务最新技术~
以上是关于漫谈图形计算中的材质系统的主要内容,如果未能解决你的问题,请参考以下文章