漫谈CG Kit中的材质系统

通用材质系统介绍

材质系统是一个实时渲染引擎很是重要的部分,它使得开发者可以很是便捷地设计出具备真实感的场景和角色。一个好的材质系统能够提升引擎的易用性,并能够方便的扩展渲染效果,来提高渲染质量和效率。java

材质系统需求

图形引擎一般须要支持不一样的渲染效果,一个优秀的材质系统一般要支持多Pass渲染管线和自定义Shader模板,因为渲染效果的复杂多样性会致使Shader数量大幅增长,这样会形成Shader文件冗余,所以材质系统要提供一套Shader复用的机制。同时,市面上各硬件厂商对图形API的支持程度不一样,受限于硬件水平的差别,材质系统也要兼容中低高端硬件。综上所述,通用材质系统须要知足如下需求:
多Technique:材质中包含多个实现方案,这样在进行高中低端机适配或实现不一样材质效果时,咱们能够方便进行材质更替。
多Pass:对于复杂绘制效果,单次绘制没法实现,经常包含多个Pass的渲染。
自定义Shader:减小Shader数量,提供Shader复用机制。
模板 + 实例:材质是一个模板,经过对某一个材质进行实例化,指定不一样的数据和贴图,就可让物体表现出不一样的显示效果。git

材质数据

材质描述了场景中物体与光照进行交互的过程,本质上它是指可以描述一个物体显示外观的一系列数据,它包括几个方面:github

  • 着色模型(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文件。

总结下来,一个材质模板文件应该是相似这样的一个结构:json

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材质系统架构图和类ide

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官方论坛
>>华为图形计算服务开源仓库地址:GitHubGitee

点击右上角头像右方的关注,第一时间了解华为移动服务最新技术~

相关文章
相关标签/搜索