【计算机外设-第一章:模型从硬盘文件到显示器像素的全过程分析】此文章归类为:["计算机外设"]。
引言
-
艺术家使用Blender、MAYA、3DS MAX等建模软件将设计好的模型导出为硬盘文件,而最终我们看到的模型是显示器上的像素,从硬盘文件到显示器像素,这其中究竟发生了什么?
-
本文主要对 “模型从硬盘文件到显示器像素” 的全过程进行理论分析,硬盘文件即艺术家导出的模型文件,显示器像素即显示器屏幕上显示模型的像素。
-
在第一小节中将对讨论分析硬盘模型文件所包含的数据内容,第二小节阐述第一小节中的内容如何存储到CPU中,第三小节讨论了Shader着色过程并展示了PBR GLSL和HLSL代码。
一、硬盘中的模型文件
-
当艺术家导出模型时需要选择文件格式,常见的模型文件格式有:FBX、GLTF、OBJ等,不同格式存储模型数据的方法大相径庭,同一格式也可能由于版本和形式不同导致存储的数据不同。例如GLTF具有1.0和2.0两个不同版本,FBX具有分离式和嵌入式两种不同形式。分离式和嵌入式是许多模型格式都具有的不同形式,分离式可以将模型的贴图分离到一个文件夹中存储,而嵌入式则会将贴图直接嵌入到模型文件中。下图展示了广泛使用的GLTF格式。
-
使用怎样的模型格式是一个值得考虑的问题,本文考虑到FBX和GLTF格式使用的广泛性,使用FBX和GLTF格式作为研究对象,为了能够直接查看和处理模型贴图,采用分离式FBX和GLTF。
(1)分离式模型文件
- 对于分离式模型文件,其存储的内容如下图所示。模型文件分为主文件和纹理文件夹,这是因为采用了分离式,所以纹理文件并不包含在模型主文件中。
- 纹理文件中存储了模型的一系列贴图,可能是JEPG、PNG和DSS等格式,纹理文件其实就是一个文件夹,打开后可以直接查看和修改其中的纹理图片。而模型主文件往往是代表整个模型的文件,比如分离式GLTF2.0的模型主文件是xxx.gltf,而FBX的模型主文件是xxx.fbx。
- 经典的GLTF2.0分离式模型文件如下图所示,其中textures文件夹保存了模型的纹理文件,而scene.gltf为模型主文件。当你使用建模软件或游戏引擎读取模型文件时,选择模型主文件即可代表整个模型文件。
(2)模型主文件
- 模型主文件存储的内容如下图所示。接下来将介绍顶点的几何数据、顶点几何数据与索引数据如何构成模型表面、如何通过材质或贴图为模型表面着色、如何使用骨骼动画驱动模型。
2.1 顶点几何与索引数据构成模型表面
- 顶点数据存储了一系列顶点,每个顶点具有自己的几何数据即:位置、法向量、纹理坐标、切线向量。
- 有了顶点要如何组成世界中的实体呢?答案是必须要将顶点连接起来,构成模型表面,这样才能作为世界中的实体被人们看到。游戏中常常使用三角形来构成模型表面,那么如何才能知道哪三个顶点构成一个三角形呢?我们可以将顶点按照构成三角形的集合依次排开,这样顶点数组中每三个顶点即对应一个三角形。
- 虽然上述方法能够解决问题,但它并不好,因为一个顶点可能会构成多个三角形,因此这种方法会造成一个顶点数据在数组中保存多次。考虑使用一个索引数组,每个索引可以获取到顶点数组中的对应顶点,每三个索引即可对应到一个三角形,这样使用相同的顶点时我们仅需指定相同的索引即可,而不用保存重复的顶点数据。当然构成模型表面的也可以是四边形,这称为模型的图元拓扑类型。
- 下图展示了如何由顶点构成模型表面。可以看到石头凸起的地方为顶点,多个顶点连接构成一个多边形面,多个面构成了物体表面。采用类似下图的多边形面数建模人物只能建出方块人,易知多边形的面数越多则其细节越丰富,但多边形数的增大会导致GPU计算量的越大,因此许多模型网站提供“低多边形”的模型分类,以此满足较低性能设备的开发需求。
2.2 通过材质或贴图为模型表面着色
- 由2.1中我们即可构建出模型的表面,我们平常在游戏中看到的模型就是通过许多个三角形构成的表面,那么如何才能为模型的表面上色呢?
- 方案一:采用纹理贴图。每个顶点具有一个纹理坐标,如果模型具有纹理贴图,查找顶点对应的纹理贴图在纹理坐标处的颜色值,将此颜色值作为此顶点的颜色,这样每一个顶点都具有了颜色。当显卡渲染物体表面时,每个顶点处的像素可直接使用顶点颜色,而顶点之间的像素使用数学插值的方法计算出一个颜色值,这样物体表面每一个像素都具有颜色了,即完成了模型表面的着色。当然,对于纹理坐标和纹理贴图等数据,由艺术家负责生成,我们只需使用即可。
- 方案二:采用材质数据。考虑不采用贴图,如果我们为每个顶点附带一个颜色值材质数据,以此指定顶点的颜色,那么也可以完成模型表面的着色。
- 虽然上述方案可以完成物体表面的着色,但是它的效果是非常差劲的,请回忆3A游戏的画面,其中模型表面的颜色并不是一成不变的,它会随着灯光和摄像机的视角发生改变,产生高光、阴影等许多效果。因此使用单一的颜色值直接着色是不可取的,除非如果你只是想画个白色方块。高级的着色效果如下图所示。
- Phong模型是一种来源于经验和直觉的着色模型,使用它计算顶点的最终颜色,即可让物体表面产生高光的效果。它的升级版着色模型是Blinn-Phong。目前实时渲染领域最为流行和逼真的着色模型为PBR模型,它是基于物理的渲染模型。后面我们会介绍PBR着色模型,现在你只需要知道PBR模型以基础颜色、金属度、粗糙度、自发光、环境光遮蔽等属性作为输入即可,通过这些属性,PBR着色模型就能计算出顶点的最终颜色。
- 互联网上许多模型都是使用PBR着色的,即模型的创作者在设计模型时就生成了顶点的基础颜色、金属度、粗糙度等信息,我们直接使用即可。类似于上述的两种着色方案,PBR需要的输入信息也可以保存在纹理贴图或材质中。艺术家可能在纹理文件中放置了基础颜色、金属度、粗糙度等贴图,这样在为顶点着色时根据其纹理坐标和各个贴图,即可获取顶点的基础颜色、金属度、粗糙度等属性,以此作为输入,即可计算出PBR模型下顶点的颜色。
- 艺术家也可以不生成模型贴图,而为每个顶点指定所用材质,不同材质有不同的基础颜色、金属度、粗糙度等属性,这时我们就需要直接根据顶点材质数据获取所需属性。
2.3 通过骨骼动画驱动模型
- 使用PBR着色模型我们能够渲染出类似上图球体的酷炫效果,但是如果整个3D场景一动不动,那么还有什么意思呢?简单的模型移动不能满足用户的需求,我们在游戏中常常会试图使用角色进行攻击、跳跃和躲避,只有实现这些功能才能使模型变得生动,那么该如何实现呢?
- 使用骨骼动画即可实现驱动模型的效果。考虑人体骨架,挥动我们的胳膊小臂,我们发现手会跟着运动,抬起我们的二头肌即胳膊大臂,我们发现小臂和手都跟着运动了。易知人体骨架其实是一种层次关联关系,当父对象运动时子对象也需要运动,以此保持父子的相对位置关系不变。
- 我们通常将人体骨架的盆骨作为根骨骼,一个标准的人体骨架如下图所示。
- 假设有一个骨骼数组int bone[BoneNum],其中bone[i]表示第i块骨骼的父骨骼索引,据此我们就可以构建骨架的树状层次结构。每个骨骼都有一个至父矩阵,当骨骼乘以其至父矩阵后便能变换到其父骨骼空间中,因此根据bone和至父矩阵,我们要将一块骨骼如手变换至顶级坐标系时,就要先将其乘以至父矩阵变换到小臂坐标系中,再乘以小臂坐标系变换到大臂坐标系中,这样子对象就受到了父对象的影响,当大臂运动时,此计算机制将导致小臂、手随之运动。
- 当然骨架的根骨骼即盆骨是没有父对象的,因此其至父矩阵为单位矩阵,其他骨骼都需要一路至父变换到根骨骼。弄清楚了骨架之间如何实现父骨骼影响子骨骼,我们考虑如何变换顶点。模型中的骨骼是看不见的,显示器显示的是物体表面即顶点,那么如何让顶点随着骨骼运动呢?考虑胳膊肘处的顶点,它会随着小臂和大臂的移动而被拉伸即移动,因此顶点可能受到多块骨骼的影响(一般最多为四块),模型文件中记录了影响顶点的骨骼索引和权重数组,比如骨骼索引数组可能为:3 5 7 9,而权重数组可能为:0.1 0.3 0.4 0.2。每块骨骼具有一个偏移矩阵,受其影响的顶点乘以偏移矩阵即可变换到骨骼空间。每个顶点将位置以此乘以每个关联骨骼的最终变换矩阵及权重,再对其求和,即可计算出顶点的位置,以此实现骨骼动画。
- 若要使得骨架运动起来,还需要动画数据的配合才行。动画数据存储了每个骨骼的动画信息,每个骨骼的动画信息包括一系列的关键帧。当程序运行至时间t时,根据骨骼的动画信息插值得到t时刻变换矩阵,再将变换矩阵与其原本的至父矩阵相乘,作为其新的变换矩阵,即可实现骨骼动画。
二、CPU中的模型数据
- 根据第一节的内容,我们大概已经知道了模型文件中存储的所有信息,那么在将模型加载到CPU内存中后,应该存储哪些数据呢?CPU中的模型数据组织如下图所示。
- 可以看到CPU与硬盘中模型数据的组织方式大似相同,仅仅在某些地方有所区别。首先是CPU中模型不直接包含顶点数组,而是包含网格数组,每个网格拥有自己的顶点数组。其实无论是CPU中的模型还是硬盘中的模型,都会以网格作为包含顶点的父对象,这是因为同一模型不同部分引用的纹理贴图、材质很可能都不同,比如角色模型的眼睛和嘴巴定然使用不同的贴图,因此将其划分为两个不同的网格,每个网格拥有自己对应的贴图。前文硬盘中的模型内容没有提到网格,也只是为了简化模型文件的组织,方便提及重点。
- CPU中的模型包含多个网格,每个网格具有自己的顶点和索引数组,但CPU中的模型并未包含贴图数据,因为对于分离式模型文件而言,其贴图数据分离在外部纹理文件夹中,因此模型主文件指定网格纹理的方式是记录纹理的相对路径。当我们通过模型主文件时,我们会读取到每个网格使用的纹理的相对路径,因此CPU中的模型并未直接包含纹理数据。
- 由于模型的某一纹理可能被多个网格所使用,因此我们可以直接在模型层面定义一个纹理路径数组,存储所有纹理路径,而网格只需存储其纹理在数组中的索引即可。这就是为什么CPU中的模型包含一个纹理数组,而网格只需包含纹理索引。
- 对于具有骨骼动画的模型而言,模型还必须包含骨骼数据,它阐述了骨架的所有信息。当用户导入模型后,可能还会多次导入模型的动画文件,而这个动画文件也是以模型文件的形式表示的。虽然看似多余,但是动画文件必须包含所描述对象的骨骼信息,它还附带了每个骨骼的动画信息,为此我们可以将其动画信息附加到模型上,然后释放其内存。
三、GPU中的模型数据
- 为了使用GPU的高并行计算能力,我们使用GPU执行着色即Shader代码。对于程序而言,其控制流程是我们在CPU中实现的,可以将GPU视作一个计算器,在程序的每一次循环中,CPU使用3D图形API将Shader的输入绑定到GPU上,然后再调用渲染指令提交给GPU,这样GPU就会根据根据输入执行Shader代码,最终返回一个纹理表示渲染结果。
- 如果想了解PBR渲染,可以看我的这篇文章PBR渲染从理论到实现,其中包含了PBR GLSL着色器代码。
- 不同的3D图形API有不同的绑定和提交流程,介于OpenGL已经停止更新,这里介绍Direct3D12的相关流程。
- 使用PBR的HLSL Shader如下代码所示。
#include "PBR_Auxiliary.hlsl"
struct VSIN
{
float3 PosL : POSITION;
float3 Normal : NORMAL;
float2 TexCoord : TEXCOORD;
float3 Tangent : TANGENT;
float3 BiTangent : BITANGENT;
int4 BoneIds : BONEIDS;
float3 BoneWeights : BONEWEIGHTS;
};
struct VSOUT
{
float4 PosH : SV_POSITION;
float4 PosW : POSITION;
float3 NormalW : NORMAL;
float2 TexCoord : TEXCOORD;
float3 Tangent : TANGENT;
float3 BiTangent : BITANGENT;
};
cbuffer worldMatr : register(b0)
{
float4x4 cWorld;
float4x4 cInvTransWorld;
};
cbuffer commonData : register(b1)
{
float4x4 cView;
float4x4 cInvView;
float4x4 cProj;
更多【计算机外设-第一章:模型从硬盘文件到显示器像素的全过程分析】相关视频教程:www.yxfzedu.com