从“1”开始3D编程
上QQ阅读APP看书,第一时间看更新

第1章 3D程序分析方法

3D程序通常由两部分组成:非着色器部分和着色器( shader)部分。非着色器部分用C/C++/JavaScript等编写,运行在CPU上,负责给GPU传送数据,以及设置GPU的参数和状态。非着色器部分也称作CPU部分。着色器部分用着色语言(shading language)描述,运行在GPU上,也称作GPU部分。着色语言有多种实现:基于OpenGL的OpenGL着色语言,简称GLSL(OpenGL/OpenGL ES/WebGL/Vulkan都支持GLSL);基于微软公司DirectX的高级着色语言,简称HLSL;苹果公司的Metal着色语言,简称MSL。本书讨论WebGL/Vulkan使用的GLSL。GLSL按照功能分为多种。图形相关的有:处理3D模型的顶点着色器(vertex shader)和几何着色器(geometry shader)、处理光照和纹理的片元着色器(fragment shader)。计算相关的有计算着色器(compute shader),计算着色器目前广泛应用于深度学习。最新的Vulkan标准还增加了任务着色器(task shader)、网格着色器(mesh shader),以及用于光线追踪的多种着色器等。

3D程序的非着色器部分,如果从CPU的角度看,它是普通的程序,可以用程序语言C/C++/JavaScript等来描述。但是如果从GPU的角度看,GPU自身就是一个实现了完整渲染流水线的状态机,非着色器部分只是描述了GPU的输入输出,以及一些GPU参数状态的调整控制命令。所以如果仅从算法和流程的复杂度来看,GPU核心的流程和算法,一部分在GPU流水线本身,一部分在用户提供的顶点着色器和片元着色器里,而不是非着色器部分。

从这一点来说,分析一个3D程序,重点应该分析流水线(本书讨论的投影和纹理映射就属于流水线)和着色器(模型的处理、光照的处理),而不是非着色器部分。基于GL(本书用GL来统称OpenGL/OpenGL ES/WebGL)的程序,其非着色器部分调用的GPU相关接口数量少,分析起来比较容易。但是Vulkan的引入,将原来封装在GL底层的一些功能暴露了出来,同时增加了多线程的支持,因而增加了大量的接口。Vulkan程序的非着色器部分,也比相应的GL实现要复杂很多。针对Vulkan的情况,流水线和着色器依然是理解整个3D程序的重点。不过非着色器部分的代码变得复杂了,也需要做些分析。换句话说,用GL编程的时候,理解GPU的行为就可以了。Vulkan则要求开发者在理解GPU的同时增加一些对CPU编程的理解。

本书重点讨论GPU的流水线和着色器部分。但是针对某些复杂的Vulkan场景的CPU部分(非着色器部分),例如涉及多次渲染的时候,CPU部分的结构会影响到对流水线和着色器的理解,因而也会介绍其结构。本书分析这些示例结构的时候不使用流程图、序列图、类图等常用的分析方法,而是使用了通信专业的输入输出分析方法,即给出程序的输入数据→数据处理过程→输出数据框架,以帮助读者理解3D程序的CPU部分。

为什么选择输入数据输出数据来分析Vulkan程序的CPU部分(当然也可以用于GL的分析)?如果是刚接触3D编程,理解GL、Vulkan是有一定难度的。如果有一定的GL经验,希望通过GL的经验分析Vulkan的CPU部分的源代码,也是有些挑战的。这些挑战来自以下两方面。

(1)与GL实现的接口不一样,Vulkan提供了更多底层资源操作的接口,同时实现了对多线程的支持。因此在接口数量上比GL要多出很多。哪怕是绘制一个最简单的三角形,代码也比同样绘制三角形的GL程序多很多。

(2)另一方面,即使有了初步的GL基础,但是GL到Vulkan的接口很难一一对应起来,虽然两者的主要功能是一样的。

综上两点,从接口层面去理解Vulkan CPU部分的源码并不直观。然而,虽然GL、Vulkan接口差别很大,但是两者的输入输出都是类似的:输入是顶点(以及法线光线等)、MVP矩阵、纹理及坐标;输出则是帧缓冲(或者绑定到帧缓冲的纹理)。数据的处理都是通过绘图渲染来完成的。复杂一些的过程,譬如延迟渲染或者阴影的计算,由于需要两次渲染过程,有些数据是第一个过程的输出,同时作为第二个过程的输入。如果从输入数据、输出数据以及数据的处理过程来理解分析Vulkan,由于将大量的Vulkan接口按照输入、处理过程、输出分为三类,这种分析方法可以简化Vulkan的分析,同时也是一种适用于其他3D编程接口例如GL的分析方法。具体的分析模型如图1-1所示。图中白色方框是输入,灰色方框是输出,圆角框是数据处理过程,灰色虚线方框表示这个模块在作为一个过程的输入的同时还作为另一个过程的输出。

图1-1 3D程序的输入输出模型

对于Vulkan,纹理资源是通过描述符(descriptor)来表示的。如果将着色器也当作一种输入数据,则通常将着色器作为VkPipeline的一部分传递给GPU。在后文讲解Vulkan例子源码结构的时候,会在结构图里面标注描述符和着色器。至于WebGL的例子,因为结构本身就很简单,就没有对其结构做具体分析。

如果Vulkan程序包括多次绘图或者计算(vkCmdDraw*发起绘图,vkCmdDispatch发起计算),而且两个绘图或者计算过程之间还有资源的共享,例如绘图过程1的输出被当作绘图过程2的输入,那么会在结构图里面加一个箭头示意这里会发生资源共享,所以要留意资源读写时的同步与互斥。

本章将从输入输出的角度来分析3D程序的非着色器部分。读者会发现,如果将输入输出模型应用到本书的示例,理解WebGL,以及更加复杂的Vulkan示例,就会简单很多。

本章分析输入顶点数据和纹理时会使用不同的示例。分析顶点数据和MVP数据使用的是输出一个矩形的两个例子:WebGL/projection/projection_perspective_quad.html和Vulkan/examples/projection_perspective_quad。分析纹理使用的两个例子的输出都是纹理图片:WebGL/texturemapping/projection_perspective_texture_mapping.html和Vulkan/examples/projection_perspective_texture。当然,输出纹理的例子仍然需要输入顶点和MVP。

WebGL1.0基于OpenGL ES2.0,WebGL2.0基于OpenGL ES3.0。除了WebGL之外,基于Vulkan、Metal、Direct3D的下一代Web 3D编程标准WebGPU目前正在讨论之中。本书的例子以Vulkan为主,部分章节还同时提供了WebGL1.0的例子,主要是为了对比理解3D模型的通用性。选择WebGL的原因是,做简单的模型验证非常方便。但如果是要设计更加复杂的高性能3D程序,读者需要自己去调查了解WebGL是否满足性能要求。选择Vulkan而不是更接近WebGL的OpenGL,则是因为Vulkan接口更复杂,能够很好地体现基于输入输出的分析方法优势。同时,Vulkan是最新的标准,了解其应用很有必要。