3

华为移动服务的个人空间

 2 years ago
source link: https://my.oschina.net/u/4956408/blog/5067171
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

通用材质系统介绍

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

材质系统需求

图形引擎通常需要支持不同的渲染效果,一个优秀的材质系统通常要支持多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官方论坛
>>华为图形计算服务开源仓库地址:GitHubGitee

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK