书名:Vulkan开发实战详解
ISBN:978-7-115-50939-0
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 吴亚峰
责任编辑 张 涛
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书共分为19章,介绍了Vulkan的诞生、特点、开发环境的搭建以及运行机制、渲染管线和调试技术,着色器编程语言——GLSL、投影及各种变换、光照、纹理映射、3D模型的加载、混合与雾、两种测试及片元丢弃、顶点着色器的妙用、片元着色器的妙用、真实光学环境的模拟、阴影及高级光照、几种高级着色器特效、骨骼动画、Vulkan的性能优化等,最后以一个休闲游戏——方块历险记的案例来展示Vulkan的功能与技术。本书按照必知必会的基础知识、基于Vulkan实现基本特效以及高级特效、完整游戏案例的顺序,循序渐进地进行详细讲解,适合不同需求、不同水平层次的读者。为了便于读者学习,随书提供了书中所有案例的完整源代码(书中所有案例都给出了安卓版和Windows版,最后的大案例还进一步给出了macOS、iOS和Linux版),最大限度地帮助读者快速掌握各方面的开发技术。
本书适合游戏开发者、程序员学习,也可以作为大专院校相关专业的师生学习用书和培训学校的教材。
作为一种跨平台的2D和3D图形应用程序接口,Vulkan因为高性能和低开销而大受欢迎,虽然面市不久,但市面上目前已有不少支持Vulkan的游戏和应用,如《Doom》《Dota2》《极品飞车——无极限》等。同时由于3D应用程序开发比较复杂,而Vulkan比传统的OpenGL更加复杂,造成入门门槛较高,初学者无从下手。根据这种情况,作者结合多年从事3D游戏及应用开发的经验编写此书。
Vulkan起初被称为GLNext,了解一些3D领域的技术人员都知道,当下多种平台上的3D应用开发很多是基于OpenGL的,但面向单线程任务设计的OpenGL在当下的处境比较尴尬,单线程的问题严重制约了新一代GPU渲染能力的发挥。
Vulkan则良好地解决了这一问题,其原生支持多线程并发渲染,留给了开发人员充分的发挥空间。Vulkan的许多新特性也使渲染的3D场景光影效果更加真实,实现过程更加迅速。本书在给出实际的案例时涉及了安卓、Windows、macOS、iOS、Linux等主流平台,充分考虑到了各个不同主流目标平台读者的需求。
经过两年多见缝插针式的奋战,本书终于交稿了。回顾写书的这两年时间,不禁为自己能最终完成这个耗时费力的“大制作”而感到欣慰。同时也为自己能将从事游戏与应用开发近15年来积累的宝贵经验以及编程感悟,分享给正在开发阵线上埋头苦干的广大开发人员而感到高兴。
贾岛的《剑客》一诗有言:“十年磨一剑,霜刃未曾试,今日把示君,谁有不平事?”从1998年首次接触Java与OpenGL算起,到现在已经20多年了。作者希望用20多年的知识和经验打磨成的“利剑”,能够帮助广大读者在实际工作中披荆斩棘、奋勇向前。
本着“起点低,终点高”的原则,本书涵盖从Vulkan必知必会的基础知识到基于Vulkan实现各种高级特效,最后还给出了一个完整的3D游戏案例。这样的内容组织使得3D应用开发初学者可以一步一步成长为3D开发的资深人员,符合绝大部分想学习3D应用开发的学生与程序开发人员以及相关技术人员的需求。
本书配合每个需要讲解的知识点都给出了丰富的插图与完整的案例,使得初学者易于上手,有一定基础的读者便于深入。书中所有的案例均是根据作者多年的开发心得进行设计的,结构清晰明朗,便于读者学习。同时书中还给出了很多作者多年来积累的编程技巧与心得,具有很高的参考价值。
为了便于读者学习,随书提供了书中所有案例的完整源代码(书中所有案例都给出了安卓版和Windows版,最后的大案例还进一步给出了macOS、iOS和Linux版),最大限度地帮助读者快速地掌握各方面的开发技术。
本书共分为19章,内容按照必知必会的基础知识、基于Vulkan实现基本特效以及高级特效、完整游戏案例的顺序循序渐进地进行详细讲解。
章 名 |
主要内容 |
---|---|
第1章 初识Vulkan |
本章简要介绍了Vulkan的诞生、特点、开发环境的搭建以及运行机制 |
第2章 渲染管线和调试技术 |
本章主要介绍了渲染管线、着色器预编译和Vulkan调试技术等,为以后的Vulkan项目开发打下良好的基础 |
第3章 着色器编程语言——GLSL |
本章对可以用于实现Vulkan可编程渲染管线着色器的GLSL进行了系统地介绍,为后面各方面的深入学习打下了基础 |
第4章 投影与各种变换 |
本章介绍了3D开发中投影、各种变换的原理与实现,同时还介绍了几种不同的绘制方式 |
第5章 光照 |
本章介绍了Vulkan中光照的基本原理与实现、点法向量与面法向量的区别以及光照的顶点计算与片元计算的差别等 |
第6章 纹理映射 |
本章介绍了纹理映射的基本原理与使用,同时还介绍了不同的纹理拉伸与采样方式、多重过程纹理技术以及压缩纹理等 |
第7章 更逼真的场景——3D模型的加载 |
本章介绍了如何使用自定义的加载工具类直接加载通过3D Max创建的3D立体物体模型 |
第8章 独特的场景渲染技术——混合与雾 |
本章主要介绍了混合以及雾的基本原理与使用 |
第9章 常用3D开发小技巧 |
本章主要介绍了一些常用的3D开发技巧,包括标志板、灰度图地形、高真实感地形、天空盒与天空穹、简单镜像技术以及非真实感绘制等 |
第10章 两种测试及片元丢弃 |
本章主要介绍了Vulkan中经常使用的两种测试及片元丢弃,包括剪裁测试、模板测试、片元丢弃操作以及任意剪裁平面等 |
第11章 顶点着色器的妙用 |
本章主要介绍如何通过顶点着色器实现几种酷炫效果,包括飘扬的旗帜、扭动的软糖、展翅飞翔的雄鹰、吹气特效等 |
第12章 片元着色器的妙用 |
本章介绍了如何通过片元着色器实现几种酷炫效果,包括程序纹理、数字图像处理技术、分形着色器、3D纹理的妙用、体积雾以及粒子系统火焰特效等 |
第13章 真实光学环境的模拟 |
本章介绍如何通过Vulkan模拟现实环境中的一些光学效果,如反射、折射、凹凸映射、镜头光晕等。同时本章还介绍了常用的投影贴图、绘制到纹理、高级镜像以及高真实感水面倒影等 |
第14章 阴影及高级光照 |
本章介绍如何通过Vulkan模拟现实世界的阴影及高级光照,主要包括平面映射、阴影映射、阴影贴图等几个方面。同时本章还介绍了几种常见的技术,即多重渲染目标、聚光灯高级光源、延迟渲染以及环境光遮蔽等 |
第15章 几种高级着色器特效 |
本章主要介绍了一些常用的高级着色器特效,如运动模糊、遮挡透视效果、积雪效果、背景虚化、泛光以及体绘制等 |
第16章 骨骼动画 |
本章介绍了3D游戏开发中常用的骨骼动画技术,包括自主开发的骨骼动画、ms3d骨骼动画文件的加载以及自定义格式骨骼动画的加载等 |
第17章 让应用运行得更流畅——性能优化 |
本章讨论了一些在使用Vulkan开发3D游戏、应用过程中的性能优化问题,包括着色器代码的优化、纹理使用过程中的优化以及3D图形绘制过程中的优化等。同时本章还介绍了几种常见的技术,即图元重启、几何体实例渲染、遮挡查询、计算着色器的使用以及多线程并发渲染等 |
第18章 杂项 |
本章主要介绍了一些与Vulkan应用开发相关的不太容易分类的知识与技术,主要包括四元数旋转、3D拾取、多重采样抗锯齿、保存屏幕图像、Windows系统窗口缩放、曲面细分着色器与几何着色器,以及苹果与Linux平台下Vulkan应用的开发等 |
第19章 基于Vulkan的3D休闲游戏——方块历险记 |
本章将通过介绍“方块历险记”游戏在Android平台以及Windows、macOS、iOS、Linux等平台上的设计与实现,对使用Vulkan 技术开发3D休闲类游戏的步骤做详细讲解 |
本书内容丰富,从基本知识到高级特效,从简单的应用程序到完整的3D游戏案例,适合不同需求、不同水平层次的各类读者。
本书包括在各个主流平台下进行3D应用开发的知识,内容由浅入深,配合详细的案例,非常适合3D游戏、应用开发的初学者循序渐进地进行学习,最终成为3D游戏、应用开发的资深人员。
本书不仅包括了Vulkan开发的基础知识,同时也包括了基于Vulkan实现高级特效的内容以及完整的游戏案例,有利于有一定基础的开发人员进一步提高开发水平与能力。
本书内容组织不单聚焦于Vulkan技术本身,还介绍了很多图形学方面的知识与技术,做到了理论联系实际,也非常适合想基于Vulkan学习图形学的读者。
吴亚峰,毕业于北京邮电大学,后留学澳大利亚卧龙岗大学取得硕士学位。1998 年开始从事Java应用的开发,有多年的Java开发与培训经验。主要的研究方向为Vulkan、OpenGL ES、手机游戏、以及VR/AR。曾任3D游戏、VR/AR独立软件工程师,并兼任百纳科技软件培训中心首席培训师。近十年来为数十家著名企业培养了上千名高级软件开发人员,曾编写过《OpenGL ES 3x游戏开发(上下卷)》《Unity 案例开发大全》(第1版、第2版)、《VR与AR开发高级教程——基于Unity》《H5和WebGL 3D开发实战详解》《Android应用案例开发大全》(第1版~第4版)、《Android游戏开发大全》(第1版~第4版)等多本畅销技术书。2008年初开始关注Android平台下的3D应用开发,并开发出一系列优秀的Android应用程序与3D游戏。
本书在编写过程中得到了百纳科技软件培训中心的大力支持,同时刘易周、宋润坤、张杰义、毛煜、尹豆、官端亮、李昀阳、刘聪颖、梁超以及作者的家人为本书的编写提供了很多帮助,在此表示衷心地感谢!
由于作者的水平和学识有限,且书中涉及的知识较多,难免有错误疏漏之处,敬请广大读者批评指正,并多提宝贵意见。本书责任编辑联系邮箱为zhangtao@ptpress.com.cn。
作者
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。
如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
Vulkan是一种跨平台的2D和3D图形应用程序接口,最早由Khronos组织在2015年GDC上发布。其本质上是AMD Mantle的后续版本,继承了前者强大的低开销架构,使开发人员能够方便全面地获取GPU与多核CPU的性能、功能和提升效率。
相比于OpenGL,Vulkan支持深入硬件底层进行控制,并能大幅度降低CPU在高负载绘制任务中的开销。同时其对多核心CPU的支持也更加完善,更加适应当下从高端工作站到PC平台到移动平台的多核战略。
介绍具体的开发技术之前,本节将首先介绍Vulkan的历史传承以及一些技术特点,同时将Vulkan与其他的图形应用程序接口(OpenGL、DirectX、Metal等)进行简要的比较,最后还会介绍一下当下支持Vulkan的游戏,具体内容如下。
了解Vulkan的具体知识之前,我们有必要首先了解一下市面上主流的各3D图形应用程序接口。目前各平台下主流的3D图形API有OpenGL、OpenGL ES、DirectX、Metal以及Vulkan,其各自的应用领域及特点如下。
Vulkan最早被称为下一代OpenGL,项目名称为GLNext。其设计考虑到了统一各个平台的开发,因此不像OpenGL与OpenGL ES那样,根据硬件性能、供电区分不同版本,而是工作站、PC、移动嵌入式等平台完全一致。这对广大开发人员来说,是一个极大的利好。
2016年2月16日,Khronos组织发布了Vulkan的首个正式版本。从此,数字图形技术产业诞生了一个真正意义上能与DirectX 12、Metal分庭抗礼的全新图形应用程序接口。到2016年4月,Google在第二个Android N的开发预览版中也正式加入了对Vulkan的支持。Vulkan的主要特点如下。
Vulkan本身博大精深,其革命性的设计远远不止上述这些,读者可以跟随本书的脚步逐渐深入地学习Vulkan的方方面面。
通过前面简单的介绍,读者已基本了解到Vulkan相比于传统图形应用程序接口的多项优势。正因为Vulkan这些突出的特性,目前市面上已有几款知名游戏开始使用Vulkan。但由于Vulkan诞生的时间不长,故使用Vulkan的游戏数量还不是很多。接下来,我们将对使用Vulkan的几款游戏进行简单的介绍。
作为一款广受玩家欢迎的巨作,早在2016年Dota 2便推出官方补丁使其支持Vulkan。如图1-1所示为原版Dota 2的游戏场景图,图1-2所示为在Vulkan支持下运行的Dota 2游戏场景图。
▲图1-1 原版Dota 2游戏场景
▲图1-2 Vulkan支持下的Dota 2游戏场景
说明
通过对比图1-1、图1-2可以看出,在游戏画面方面,Vulkan支持下的Dota 2较原版Dota 2场景更加逼真、细腻。在游戏的实际对比测试中,可以感觉到Vulkan支持下的Dota 2运行更加流畅,并且可以观察到CPU使用率更低,这正体现了Vulkan降低CPU开销的特点。
通过对比Dota 2在使用Vulkan前后的场景画面,我们已经观察到了Vulkan在3D图形处理方面的进步。接下来将通过展示Electronic Arts开发的赛车竞技类游戏“极品飞车:无极限”,进一步感受Vulkan的3D图形处理能力,具体情况如图1-3和图1-4所示。
▲图1-3 极品飞车:无极限场景1
▲图1-4 极品飞车:无极限场景2
说明
可以看出上述两幅使用Vulkan API渲染出的“极品飞车:无极限”游戏场景画面光影效果极其逼真,烟雾、运动模糊效果都很真实。
介绍完上述两款支持Vulkan的游戏Dota 2和极品飞车之后,不得不介绍First Touch开发的体育类游戏——Dream League Soccer。该游戏自发布以来一直广受玩家的好评,现在更是推出了Vulkan版本,其效果分别如图1-5和图1-6所示。
▲图1-5 Dream League Soccer场景1
▲图1-6 Dream League Soccer场景2
通过对上述几款游戏画面的观察,我们可以领略到Vulkan在3D图形处理方面的能力提升。前面的内容中,多次提到Vulkan的一大优势是能够大幅度降低渲染时的CPU开销,这将直接影响游戏运行及画面的流畅度,有关权威组织对Vulkan这方面的测试也不少。
比如早在2016年Bethesda和Nvidia就进行了相关测试,测试结果表明使用DirectX 11在1080P分辨率下运行《毁灭战士4》,平均帧率在55~60之间。之后,使用Vulkan进行同样的渲染工作,整个游戏帧率提升到了震撼的120以上,可见Vulkan在降低CPU开销及图形渲染等方面均效果显著。
前面介绍过,Vulkan是跨平台的2D和3D图形应用程序接口。因此,为了方便不同目标平台读者的学习,书中所有的案例都给出了基于Windows平台的PC版项目以及Android移动平台下的Android Studio版项目。本节将依次介绍如何配置Vulkan的Android开发环境和Windows开发环境。
首先介绍的是Android平台开发环境的配置,需要使用的开发工具包括Oracle的JDK、Android的SDK及NDK、Android Studio等,具体内容如下。
JDK是Java Development Kit的缩写,是开发Java程序必备的工具包。其中包含了Java运行环境、Java开发工具和Java的基础类库等。本节主要介绍JDK的下载和安装以及相关环境变量的配置,具体步骤如下。
(1)首先登录Oracle网站下载最新的适合自己开发的PC或工作站操作系统版本的JDK安装程序。单击如图1-7所示的按钮进入如图1-8所示的下载页面。
▲图1-7 JDK下载页面1
▲图1-8 JDK下载页面2
提示
由于新版的Android Studio仅仅支持64位版本的JDK,因此本书中的开发要求安装64位版本的JDK,这一点请读者注意。
(2)接着双击下载的JDK安装包(如jdk-9.0.1_windows-x64_bin.exe),开始JDK的安装。安装过程中,系统会弹出如图1-9所示的安装设置界面,若没有特殊需要,单击下一步按钮安装到默认目录即可。当然,也可以单击“更改”按钮设置JDK的安装路径。
(3)安装完成后将转到如图1-10所示界面,单击“关闭”按钮结束安装。
(4)接着需要在操作系统的Path环境变量中加入JDK的bin路径,用鼠标右键单击“我的电脑”图标,单击属性→高级→环境变量,如图1-11所示。在Path变量中添加JDK的bin路径,如“C:\Program Files\Java\jdk-9.0.1\bin”,并且与前面原有的环境变量用“;”分开。
▲图1-9 JDK安装页面
▲图1-10 安装完成
▲图1-11 设置JDK环境变量
(5)最后在环境变量中新增JAVA_HOME项。具体方法为,在环境变量下的系统变量中添加JAVA_HOME项,将变量值设置为JDK的安装路径,如“C:\Program Files\Java\jdk-9.0.1”,整个操作过程如图1-12所示。
▲图1-12 新建JAVA_HOME项
前面介绍了JDK的安装及相关环境变量的配置,接下来要介绍的是Android Studio的下载与配置。Android Studio是一款用于开发Android应用程序的集成开发工具,其中提供了一套完整的开发工具集,用于开发和调试Android应用程序。相关具体步骤如下。
(1)首先打开Android Studio的官方下载网站,如图1-13所示。然后将页面下拉至图1-14所示处,单击“android-studio-ide-171.4408382-windows.exe”进行下载,此时浏览器会弹出下载对话框,提示下载并保存。
▲图1-13 Android Studio官方下载首页
▲图1-14 Android Studio官网下载处
(2)将Android Studio下载成功以后,会得到一个可执行文件。双击打开该文件,根据安装向导的指示安装 Android Studio 和所有所需的 SDK 工具。SDK的安装路径可以自由设置,作者采用的路径为“D:\Android\”,建议读者也使用该路径。
提示
关于Android Studio及SDK具体的安装、设置步骤,读者如果不是很熟悉的话可以参考官方安装指导,也可以参看本书的随书相关视频。要注意的是:安装的过程中还包含了很多文件的下载,因此需要联网。另外,这可能要耗费数小时的时间,读者朋友可以去做其他事情了。
经过前面的步骤,读者应该已经完成了Android Studio及SDK的安装与配置,此时已经可以基于Java或Kotlin语言进行Android应用程序的开发了。但目前Android平台下Vulkan应用程序的开发仅仅可以使用C/C++语言,因此还需要下载与配置专门用于这方面开发的NDK,具体步骤如下。
(1)首先打开Android NDK的官方下载网站,如图1-15所示。单击“android-ndk-r13b- windows-x86_64.zip”进行下载,如图1-16所示。此时浏览器会弹出下载对话框,提示下载并保存。
▲图1-15 Android NDK下载界面1
▲图1-16 Android NDK下载界面2
(2)将Android NDK下载成功以后,会得到一个名称为“android-ndk-r13b-windows-x86_64.zip”的压缩包,将其解压缩到“D:\Android\” 路径下。
(3)接着打开Windows的命令行窗口(cmd),依次输入“path=D:\Android\android-ndk-r13b”“d:”“cd D:\Android\android-ndk-r13b\sources\third_party\shaderc”“ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=Android.mk APP_STL:=gnustl_static APP_ABI=all libshaderc_combined”等命令进行NDK的构建,如图1-17所示。经过构建操作NDK才能支持基于其进行Android平台下的Vulkan应用程序开发。构建过程需要比较长的时间,随机器性能不同所需时间为5~30分钟,构建成功后命令行窗口如图1-18所示。
▲图1-17 构建界面1
▲图1-18 构建界面2
提示
作者使用的NDK路径为“D:\Android\android-ndk-r13b”,如果读者使用的不是该路径,需要对输入命令行窗口的内容进行适当修改才可以进行正常构建。虽然理论上说SDK、NDK等可以安装到任何合理的路径下,但这里作者建议读者采用与作者相同的路径,这便于学习、运行书中附带的Android案例项目。当然,如果读者很熟悉项目各方面的配置与修改就无所谓了。
Android的开发环境搭建基本完成后,还有一项重要的工作。那就是运行一下本书第一个Vulkan案例项目——3色三角形。通过运行这个项目,读者就可以掌握如何将本书中的Android平台项目导入Android Studio中运行,具体步骤如下。
(1)Android Studio安装成功后出现打开项目界面,如图1-19所示。将随书项目“Sample1_1”的压缩包复制到桌面,并解压缩。接着单击“Import project”打开桌面上解压后的项目,路径如图1-20所示(桌面路径因PC设置的不同而不同),单击“OK”打开项目。
▲图1-19 Android Studio打开项目界面1
▲图1-20 Android Studio打开项目界面2
(2)项目打开完成后,若希望成功运行,还需要安装Cmake插件。单击Tools→Android→SDK Manager,打开SDK管理窗口,如图1-21所示。单击“SDK Tools”,勾选Cmake,单击“Apply”进行安装(请注意这期间需要连接互联网),如图1-22所示。
▲图1-21 环境配置界面1
▲图1-22 环境配置界面2
(3)本书中Android项目的SDK和NDK的默认搜索路径都为“D:\Android\”,如果读者PC中SDK和NDK路径不是“D:\Android\”,项目可能无法正常运行。此时可选择Android Studio的“File”菜单下的“Project Structure”,系统将弹出相关设置界面,如图1-23所示。在界面中设置自己的SDK和NDK的所在路径,单击“OK”完成修改,如图1-24所示。
▲图1-23 路径配置界面1
▲图1-24 路径配置界面2
至此,用于开发Android平台下 Vulkan应用程序的Android Studio集成开发环境的搭建就完成了,读者此时可以正式开始Android 平台下的Vulkan应用开发之旅了。
前面已经介绍过,Vulkan是跨平台的图形应用程序接口,自然也可以用于开发Windows下的图形应用程序。下面将介绍Windows平台下的开发环境配置,主要包括Visual Studio、Cmake、Git、VulkanSDK和Python等,具体内容如下。
Visual Studio是 Windows下强大易用的开发工具集,所写的目标代码几乎适用于微软旗下的所有平台。它也是目前最流行的Windows平台应用程序集成开发环境,具体的下载、安装与配置步骤如下。
(1)首先打开Visual Studio的官方下载网站,如图1-25所示。在其中“Visual Studio 2015 和其他产品”板块后点击“下载”按钮,将跳转到下载列表界面,如图1-26所示。
(2)在如图1-26所示的界面中选择所需的版本(这里建议选择作者使用的Visual Studio Community 2015 with Update 3),单击“Download”按钮进行下载。此时浏览器会弹出下载对话框,提示给出下载后文件的保存路径。
▲图1-25 Visual Studio官方下载首页
▲图1-26 Visual Studio官网下载处
提示
上述步骤的操作需要首先用微软的账号登录,如果没有登录,系统会提示读者进行登录后再操作。如果读者没有微软的账号,可以先免费申请一个。
(3)下载成功后,将得到一个名称较长的可执行文件(如en_visual_studio_community_2015_with_update_3_x86_x64_web_installer_8922963.exe)。双击此可执行文件以打开安装程序,然后选择自定义安装(这是因为Visual Studio的默认安装不支持C++开发),如图1-27所示。接着单击“Next”按钮,程序将跳转到如图1-28所示的界面。
(4)接着在如图1-28所示的界面中勾选“Visual C++”选项,再单击“Next”按钮正式开始安装。
▲图1-27 Visual Studio安装界面1
▲图1-28 Visual Studio安装界面2
提示
安装的过程中需要联网下载的文件非常大,大约有13GB,可能要耗费数小时的时间。
Vulkan SDK提供了构建,运行和调试Vulkan应用程序所需的开发和运行时组件。开发Windows平台下的Vulkan程序之前,都必须首先安装Vulkan SDK。本节主要介绍Vulkan SDK的下载与安装,这个过程并不复杂,具体内容如下。
(1)首先打开Vulkan SDK的官方下载网站,如图1-29所示。然后将页面下拉至如图1-30所示处,单击“VulkanSDK-1.0.68.0-Installer.exe”进行下载。此时浏览器会弹出下载对话框,提示下载并保存。
▲图1-29 VulkanSDK官方下载首页
▲图1-30 VulkanSDK官网下载处
(2)下载成功后,将得到一个可执行文件(如VulkanSDK-1.0.68.0-Installer.exe)。双击此可执行文件以打开安装程序,如图1-31所示。
(3)在如图1-31所示的界面中单击“I Agree”按钮,将弹出路径选择界面,如图1-32所示。若没有特殊需要,单击“Install”按钮安装到默认路径即可。
▲图1-31 Vulkan SDK安装页面
▲图1-32 Vulkan SDK路径选择界面
提示
本书中所有Windows平台的案例都是基于VulkanSDK-1.0.68.0进行开发和调试的,此版本也是VulkanSDK-1.0的最后一个版本,再后面就是VulkanSDK-1.1了。截至作者完稿时,Android等设备还没有广泛支持Vulkan 1.1,因此本书中没有介绍Vulkan 1.1的新特性。不过读者不用觉得很遗憾,Vulkan 1.1中增加的新特性并不多,不属于革命性的变化。读者只要全面掌握了Vulkan 1.0,未来有需要时再进一步学习Vulkan 1.1是非常容易的。
Vulkan应用程序的构建还需要Python的支撑,Python具有丰富而强大的库,能够把用其他语言开发的各种模块(尤其是C/C++)很轻松地连接在一起。下面将介绍Python的下载和安装,这个过程并不复杂,具体步骤如下。
(1)首先打开Python的官方下载网站,如图1-33所示。然后将页面下拉至如图1-34所示处,单击“Download Windows x86-64 executable installer”进行下载。此时浏览器会弹出下载对话框,提示下载并保存。
▲图1-33 Python的下载界面1
▲图1-34 Python的下载界面2
(2)下载成功后,将得到一个可执行文件(如python-3.6.1-amd64.exe)。双击此可执行文件以打开安装程序,如图1-35所示。
(3)在如图1-35所示的界面中选中“Add Python 3.6 to Path”选项,如图1-36所示。 若没有特殊需要,接着点击“Install New”执行默认的安装即可。
▲图1-35 Python的安装界面1
▲图1-36 Python的安装界面2
CMake是一款跨平台的项目构建工具,可以用简单的语句来描述所有主流平台的项目构建过程,还能测试编译器所支持的C++特性。下面将介绍CMake的下载、安装与配置,这个过程很简单,具体步骤如下。
(1)首先打开CMake的官方下载网站,如图1-37所示。然后将页面下拉至如图1-38所示处,单击“cmake-3.9.6-win64-x64.msi”进行下载,此时浏览器会弹出下载对话框,提示下载并保存。
▲图1-37 CMake的下载界面1
▲图1-38 CMake的下载界面2
(2)下载成功后,将得到一个可执行文件(如cmake-3.9.6-win64-x64.msi)。双击此可执行文件以打开安装程序,如图1-39所示。接着单击界面中的“Next”按钮,程序将跳转到安装设置选项界面,如图1-40所示。
(3)在如图1-40所示的界面中勾选“Add CMake to the system PATH for all users”选项。若没有特殊需要,接着单击“Next”按钮继续完成安装即可。
▲图1-39 CMake的安装界面1
▲图1-40 CMake的安装界面2
Git是一个开源的分布式版本控制系统,可以有效、高速地处理从非常小到非常大的项目版本管理。下面将介绍Git的下载和安装,这个过程同样很简单,详细步骤如下。
(1)首先打开Git的官方下载网站,如图1-41所示。然后将页面下拉至如图1-42所示处,单击 “Git-2.13.0-64-bit.exe”进行下载,此时浏览器会弹出下载对话框,提示下载并保存。
▲图1-41 Git的下载界面1
▲图1-42 Git的下载界面2
(2)下载成功后,将得到一个可执行文件(如Git-2.13.0-64-bit.exe)。双击此可执行文件以打开安装程序,如图1-43所示。接着单击“Next”按钮,程序将跳转到如图1-44所示的界面。若没有特殊需要,接着单击“Next”按钮继续完成安装即可。
▲图1-43 Git的安装界面1
▲图1-44 Git的安装界面2
开发Windows下的Vulkan图形应用程序之前,还需要对安装完成的Vulkan SDK进行构建。构建后的SDK才能用于实际项目的开发,具体步骤如下。
(1)首先打开Windows的命令行窗口(cmd),依次输入“echo %VULKAN_SDK%”“cd C:\VulkanSDK\1.0.68.0\Bin”和“vulkaninfo”以检查Vulkan SDK是否安装成功。若显示如图1-45所示的内容则说明Vulkan SDK安装成功。
(2)接着依次输入“git --version”“python --version”“cmake --version”来检查Git、Python和CMake是否安装成功,若显示出如图1-46所示的内容则说明git、Python、CMake安装成功。
▲图1-45 cmd界面1
▲图1-46 cmd界面2
(3)当Vulkan SDK、Git、Python和CMake都安装成功后,就可以进行Vulkan SDK的构建了。从“开始→所有程序”打开Visual Studio自带的命令行工具“MSBuild Command Prompt for VS2015”。该工具在开始菜单中的“Visual Studio 2015”项目下,如图1-47所示。
(4)接着在弹出的命令行窗口中依次输入“cd C:\VulkanSDK\1.0.68.0\Samples\”和“build_windows_samples.bat”,如图1-48所示。此时就开始了Vulkan SDK的构建,这需要比较长的时间,随机器性能不同可能10~30分钟。
▲图1-47 MSBuild Command Prompt所在位置
▲图1-48 MSBuild运行界面
提示
作者使用的Vulkan SDK版本为“1.0.68.0”,如果读者下载的不是该版本的Vulkan SDK,还需要对输入命令行窗口的内容进行适当修改才可以进行正常构建。
完成了上述工作后,Windows平台下的Vulkan图形应用程序开发环境就搭建完毕了。下面将介绍如何导入、运行本书附带的Windows平台案例项目,这里还是以3色三角形案例项目为例进行介绍,具体步骤如下。
(1)将本书所带项目PCSample1_1的压缩包复制到桌面,并进行解压。打开Visual Studio,单击File→Open→Project,如图1-49所示。找到桌面上解压完的项目文件夹PCSample1_1并打开,继续打开build子文件夹,选择名称为“PCSample1_1”、后缀为“sln”的文件打开,如图1-50所示。
▲图1-49 打开项目界面1
▲图1-50 打开项目界面2
(2)运行项目之前,还需要确认一下桌面路径位置。打开项目中的“PathData.h”文件,该文件位置如图1-51所示。打开后的内容如图1-52所示,这里有一个宏“PathPre”,内容为桌面路径的字符串,如果读者的桌面路径与该路径不同,应进行适当修改,否则可能会造成项目无法正常运行。
▲图1-51 文件所在位置
▲图1-52 文件内容
(3)桌面路径字符串修改完成后,单击运行按钮就可以运行案例了。运行按钮所在位置如图1-53所示,运行效果如图1-54所示。
▲图1-53 运行按钮
▲图1-54 运行效果
至此,用于开发Windows Vulkan应用程序的Visual Studio集成开发环境的搭建及相关环境的配置就完成了,读者此时可以正式开始Windows 平台下的Vulkan图形应用程序开发之旅了。
通过前面的内容,读者对Vulkan应该有了一个简单的了解。本节将给出一个使用Vulkan绘制3色三角形的案例,带领读者真正进入Vulkan图形应用程序开发的世界。本节主要内容包括:案例运行效果概览、Vulkan应用程序的基本架构、具体的代码开发等方面。
学习正式的代码开发之前,我们有必要先了解一下案例的具体运行效果。本节案例Sample1_1是一个绕轴旋转的3色三角形场景,具体情况如图1-55和图1-56所示。
▲图1-55 运行效果图1
▲图1-56 运行效果图2
提示
为了方便读者学习,本书所有的案例都配置了Windows下的PC版的项目以及Android移动平台下的Android Studio版项目。两种平台项目的核心源代码是基本一致的,读者可以根据自己的学习目标需要选用某个平台的案例进行学习。若读者需要运行的是Android平台版本的案例,则需要一部支持Vulkan的设备。具体要求是Android系统版本7.0或更高,且设备的GPU硬件支持Vulkan。关于GPU硬件的信息可以参考本章后面介绍支持Vulkan不同型号GPU的相关内容。
1.3.1节介绍了案例的运行效果,本节将开始介绍Vulkan应用程序的开发。由于Vulkan比较复杂,一个简单的三角形案例代码也多达1500行,故在学习具体的代码之前,我们需要首先学习一下编程中经常遇到的一些基本类型以及应用程序的基本架构。
首先来熟悉一些常用的Vulkan基本类型,主要包含设备、队列、命令缓冲、队列家族、渲染通道、管线等,具体内容如表1-1所示。
表1-1 常用的Vulkan基本类型
名称 |
Vulkan类型 |
说明 |
---|---|---|
实例 |
VkInstance |
用于存储Vulkan程序相关状态的软件结构,可以在逻辑上区分不同的Vulkan应用程序或者同一应用程序内部不同的Vulkan上下文 |
物理设备 |
VkPhysicalDevice |
对系统中GPU硬件的抽象,每个GPU对应于一个物理设备。另外,每个实例下可以有多个物理设备 |
设备 |
VkDevice |
基于物理设备创建的逻辑设备,本质上是存储信息的软件结构,其中主要保留了与对应物理设备相关的资源。每个物理设备可以对应多个逻辑设备 |
命令池 |
VkCommandPool |
服务于高效分配命令缓冲 |
命令缓冲 |
VkCommandBuffer |
用于记录组成绘制或计算任务的各个命令,在命令池中分配。若执行的是不变的绘制命令,可以对记录了命令的命令缓冲进行重用 |
命令缓冲启动信息 |
VkCommandBufferBeginInfo |
携带了命令缓冲启动时必要信息的对象 |
命令缓冲提交信息 |
VkSubmitInfo |
携带了命令缓冲提交给队列执行时必要信息的对象,包括需要等待的信号量数量、等待的信号量列表、命令缓冲数量、命令缓冲列表、触发的信号量数量、触发的信号量列表等 |
队列家族属性 |
VkQueueFamilyProperties |
携带了特定队列家族属性信息的软件结构,包括家族中队列的数量、能力标志等。每一个队列家族中可能含有多个能力相近的队列,常用的队列家族主要有支持图形任务和计算任务的两大类 |
队列 |
VkQueue |
功能为接收提交的任务,将任务按序由所属GPU硬件依次执行 |
格式 |
VkFormat |
一个枚举类型,包含了Vulkan开发中用到的各种内存组织格式,如VK_FORMAT_R8G8B8A8_UNORM就表示支持RGBA四个色彩通道,每个通道8个数据比特 |
2D尺寸 |
VkExtent2D |
用于记录2D尺寸的结构体,有width和height两个属性 |
图像 |
VkImage |
设备内存的一种使用模式,这种模式下对应的内存用于存储图像像素数据。其中存储的像素数据可能是来自于纹理图也可能是来自于绘制任务的结果等 |
图像视图 |
VkImageView |
配合图像对象使用,其中携带了对应图像对象的类型、格式、色彩通道交换设置等方面的信息 |
交换链 |
VkSwapchainKHR |
将画面呈现到特定目标平台(如Windows、Android、Linux等)窗体或表面的机制,通过它可以提供多个用于呈现的图像。这些图像与目标平台相关,可以看作目标平台呈现用KHR表面的抽象接口。持续换帧呈现时交替使用其中的多个图像执行,避免用户看到绘制过程中的画面引起画面撕裂。一般情况下,交换链中至少有两个用于呈现的图像,有些设备中数量会更多 |
帧缓冲 |
VkFrameBuffer |
为绘制服务,其中可以包含颜色附件(用于记录一帧画面中各个像素的颜色值)、深度附件(用于记录一帧画面中各个像素的深度值)、模板附件(用于记录一帧画面中各个像素的模板值)等 |
缓冲 |
VkBuffer |
设备内存的一种使用模式,这种模式下对应的内存用于存储各种数据。比如:绘制用顶点信息数据、绘制用一致变量数据等 |
缓冲描述信息 |
VkDescriptorBufferInfo |
携带了描述缓冲信息的结构体,包含对应缓冲、内存偏移量、范围等 |
渲染通道 |
VkRenderPass |
其中包含了一次绘制任务需要的多方面信息,诸如颜色附件、深度附件情况,子通道列表、子通道相互依赖信息等,用于向驱动描述绘制工作的结构、过程。一般来说,每个渲染通道从开始到结束将产生一帧完成的画面 |
清除内容 |
VkClearValue |
包含了每次绘制前清除帧缓冲所用数据的相关值,主要有清除用颜色值、深度值、模板值等 |
渲染通道启动信息 |
VkRenderPassBeginInfo |
携带了启动渲染通道时所需的信息,包括对应的渲染通道、渲染区域的位置及尺寸、绘制前的清除数据值等 |
渲染子通道描述 |
VkSubpassDescription |
一个渲染通道由多个子通道组成,至少需要一个子通道。每个子通道用一个VkSubpassDescription实例描述,其中包含了此子通道的输入附件、颜色附件、深度附件等方面的信息 |
描述集布局 |
VkDescriptorSetLayout |
服务于描述集,给出布局接口。通俗讲就是给出着色器中包含了哪些一致变量、分别是什么类型、绑定编号是什么、对应于哪个管线阶段(比如顶点着色器、片元着色器)等 |
描述集 |
VkDescriptorSet |
通过布局接口将所需资源和着色器连接起来,帮助着色器读入并理解资源中的数据,比如着色器中的采样器类型、一致变量缓冲等 |
写入描述集 |
VkWriteDescriptorSet |
用于绘制前更新着色器所需的一致变量等 |
描述集池 |
VkDescriptorPool |
用于高效地分配描述集 |
管线布局 |
VkPipelineLayout |
描述管线整体布局,包括有哪些推送常量、有哪些描述集等 |
管线 |
VkPipeline |
包含了执行指定绘制工作对应管线的各方面信息,诸如管线布局、顶点数据输入情况、图元组装设置、光栅化设置、混合设置、视口与剪裁设置、深度及模板测试设置、多重采样设置等 |
着色器阶段创建信息 |
VkPipelineShaderStageCreateInfo |
携带了单个着色器阶段信息的对象,包括着色器的SPIR-V模块、着色器主方法名称、着色器对应阶段(比如顶点着色器、片元着色器、几何着色器、曲面细分着色器)等 |
顶点输入绑定描述 |
VkVertexInputBindingDescription |
用于描述管线的顶点数据输入情况,包括绑定点编号、数据输入频率(比如每顶点一套数据)、数据间隔等 |
顶点输入属性描述 |
VkVertexInputAttributeDescription |
描述顶点输入的某项数据信息(比如顶点位置、顶点颜色),包括绑定点编号、位置编号、数据格式、偏移量等 |
管线缓冲 |
VkPipelineCache |
为高效地创建管线提供支持 |
格式属性 |
VkFormatProperties |
用于存储指定格式类型(比如VK_FORMAT_D16_UNORM)的格式属性,包括线性瓦片特性标志、最优化瓦片特性标志、缓冲特性标志等 |
物理设备内存属性 |
VkPhysicalDeviceMemoryProperties |
用于存储获取的基于指定GPU的设备内存属性,包括内存类型数量、内存类型列表、内存堆数量、内存堆列表等 |
设备内存 |
VkDeviceMemory |
设备内存的逻辑抽象,前面提到的缓冲(VkBuffer)、图像(VkImage)都需要绑定设备内存才能正常工作 |
信号量 |
VkSemaphore |
用于一个设备(GPU)内部相同或不同队列并发执行任务时的同步工作,一般与方法VkQueueSubmit配合使用,以确保通过VkQueueSubmit方法提交的任务在指定信号量未触发前阻塞直至信号量触发后才执行。要特别注意的是,若有多个提交的任务同时等待同一个信号量触发,则此信号量的触发仅仅会被一个等待的任务接收到,其他等待的任务还将继续等待。这里的“同步”指的是并发执行任务时解决冲突的一种策略,有兴趣的读者可以进一步查阅相关资料 |
栅栏 |
VkFence |
用于主机和设备之间的同步,通俗地讲就是用于CPU和GPU并发执行任务时的同步 |
KHR表面 |
VkSurfaceKHR |
此类对象服务于帧画面的呈现 |
KHR表面能力 |
VkSurfaceCapabilitiesKHR |
携带了用于呈现画面的表面相关呈现能力的信息,比如画面尺寸范围、交换链中的图像数量、是否支持屏幕变换等 |
呈现信息 |
VkPresentInfoKHR |
携带了执行呈现时所需的一些信息,包括需要等待的信号量数量、信号量列表、交换链的数量、交换链列表、此次呈现的图像在交换链中的索引等 |
提示
从表1-1中可以看出,Vulkan开发中需要涉及的类型非常多。读者看完表1-1后可能对有些概念还不是很清楚。不用担心,随着后面逐步的学习读者一定可以掌握上述所有的内容,这里有一个简单的了解即可。
了解了一些常用的Vulkan基本类型后,下面来介绍Vulkan应用程序的基本架构。一般来说,完整的Vulkan图形应用程序包含创建Vulkan实例、获取物理设备列表创建逻辑设备、创建命令缓冲、获取设备中支持图形工作的队列、初始化交换链、创建深度缓冲、创建渲染通道、创建帧缓冲、创建绘制用的物体、初始化渲染管线、创建栅栏和初始化呈现信息、初始化基本变换矩阵、摄像机矩阵和投影矩阵、执行绘制、销毁相关对象等模块,具体内容如下。
提示
下面介绍这些模块的工作时采用的顺序与案例程序中的顺序一致。但实际开发中,随项目不同、开发人员习惯不同,有些模块的顺序是可以调整的,读者可以在后续学习中慢慢领会。
这部分首先初始化所需扩展列表,然后构建应用程序信息结构体实例,接着构建Vulkan实例创建信息结构体实例,最后创建所需的Vulkan实例。
这部分首先获取指定Vulkan实例下的物理设备数量,接着获取指定索引物理设备的内存属性。这里的物理设备指的是机器上安装的GPU(俗称显卡),若机器中不止一个GPU,那么物理设备的数量将大于一。这也是Vulkan的特点之一,支持多GPU协同工作。
这部分首先获取指定索引的物理设备的队列家族数量和属性,接着遍历此物理设备的队列家族列表,并记录支持图形工作的队列家族索引。随后构建设备队列创建信息结构体实例,接着设置逻辑设备所需的设备扩展,最后构建设备创建信息结构体实例并基于其创建所需的逻辑设备。
创建命令缓冲时首先构建命令缓冲池创建信息结构体实例并创建所需的命令缓冲池,接着构建命令缓冲分配信息结构体实例并在缓冲池中分配所需的命令缓冲。随后构建命令缓冲启动信息结构体实例,最后构建提交信息结构体实例。
基于命令缓冲池分配所需的命令缓冲是Vulkan提高效率的一项设计,同时这里构建的命令缓冲、命令缓冲启动信息结构体实例和提交信息结构体实例将在后面实际执行绘制工作时使用。
该部分根据给定的逻辑设备及指定的队列家族索引与队列索引获取了设备中支持图形工作的队列。此队列将用于后面接收绘制任务的提交,并依次将任务分配给GPU执行。
交换链(SwapChain)是Vulkan中执行呈现工作的重要策略,需要呈现模块的Vulkan应用程序都应该初始化所需的交换链。首先需要构建对应目标系统的KHR表面创建信息结构体实例,接着创建所需的表面。然后遍历所用GPU所有的队列家族,找到其中既支持显示工作又支持图形绘制工作的队列家族,记录其索引。
如果没有既支持图形工作又支持显示工作的队列家族,就单独记录支持显示工作的队列家族索引,随后获取前面创建的表面所支持的格式数量和列表。接着获取表面的能力、表面支持的显示模式数量及显示模式列表,并进一步确定交换链使用的显示模式,同时确定交换链执行呈现时的宽度和高度。
接着构建交换链创建信息结构体实例,并基于其创建交换链。然后获取交换链中的图像数量和图像列表,为图像列表中的每个图像创建了图像视图,以备后面创建帧缓冲等工作时使用。交换链中的多幅图像在绘制时将轮流使用,一幅图像绘制完成后,将其呈现到屏幕。同时可以绘制其他图像,确保绘制过程中的画面不会呈现于屏幕被用户看到,避免画面撕裂。
首先给定深度缓冲的图像格式,接着构建用于创建深度缓冲图像的图像创建信息结构体实例,并创建深度缓冲图像。随后构建内存分配信息结构体实例与图像视图创建信息结构体实例,接着获取深度缓冲图像对应的内存类型索引,为深度缓冲图像分配内存,并将分配的内存与深度缓冲图像绑定,最后为深度缓冲图像创建图像视图以备后面创建帧缓冲等工作时使用。
首先需要构建创建渲染通道所需的附件描述结构体实例数组,数组中的第一个元素用于描述颜色附件,第二个元素用于描述深度附件。然后构建用于描述子渲染通道的结构体实例,接着构建渲染通道创建信息结构体实例,最后创建了渲染通道。
要注意的是,根据绘制任务需求的不同,附件数量会有较大变化,本节案例比较简单,因此只有两个附件(一个颜色附件,一个深度附件)。此外,一个渲染通道中包含的子渲染也可能是多个,本节案例绘制任务简单,故仅包含一个子渲染。
该部分首先创建作为帧缓冲附件的图像视图数组,其中的两个元素分别为颜色附件与深度附件。然后根据交换链中的图像数量创建对应数量的帧缓冲。此处有一个要点请读者注意,为呈现服务的帧缓冲应该是一组,其中帧缓冲的数量与交换链中的图像数量对应。
为了方便复杂3D场景的开发,本书将绘制用的物体独立出来,每种绘制物体单独一个类。本节案例中绘制用物体类只有一个,那就是DrawableObjectCommonLight类。此类对象中携带了绘制用物体的顶点数据,诸如顶点位置坐标、顶点颜色等。
同时,此类对象中还提供了绘制对应物体时被调用的方法(本书案例中一般方法名为drawSelf)。该方法中包含了一系列绘制时所需Vulkan方法的调用,一般包含管线的绑定、描述集的绑定、顶点数据缓冲的绑定、绘制等相关方法的调用。
Vulkan中的渲染管线与传统OpenGL中的不同,需要开发人员根据具体的绘制需求创建,这大大提高了开发的灵活性。本书中为了方便复杂场景下多种不同渲染管线的开发与管理,将每种渲染管线的相关操作独立为一个类。本节案例中情况较为简单,仅有一种渲染管线,对应的类为ShaderQueueSuit_Common。
ShaderQueueSuit_Common类中首先对管线的各方面信息进行了设置,主要包括一致变量缓冲信息、描述集信息、着色器信息、管线动态信息、数据输入阶段信息、图元组装阶段信息、光栅化阶段信息、颜色混合阶段信息、视口及剪裁信息、深度测试与模板测试信息、多重采样信息等。各方面信息准备完毕后,最终创建了所需的渲染管线。
Vulkan中CPU与GPU是协同并发工作的,为确保这二者并发工作的同步,需要创建服务于此目标的栅栏对象。另外,每一帧画面绘制完成后,需要将帧缓冲中的画面呈现到屏幕,此时需要用到呈现信息。这里一并创建了需要的呈现信息结构体,以备后面执行呈现时使用。
将给定的顶点信息绘制为所需的画面需要经历一系列的数学运算,主要包括基本变换(平移、旋转、缩放)、摄像机观察、投影等。这里对支撑这些变换的多种矩阵进行了相关的初始化工作,相关的数学知识会在后续的章节详细介绍,这里读者简单了解即可。
执行绘制时首先需要获取交换链中的当前帧索引,然后为渲染通道设置当前的帧缓冲并清除命令缓冲,随后启动命令缓冲并刷新此帧画面绘制时所需的描述集,并启动渲染通道。接着在命令缓冲中记录用于执行相关绘制任务的各个命令。
完成绘制命令的记录后,结束渲染通道,结束命令缓冲,设置命令缓冲提交信息相关属性,将命令缓冲提交指定的队列执行。执行完毕后,将画面呈现到屏幕。
前面的一系列步骤中创建了很多的对象、实例,在绘制任务结束后,不再使用的对象、实例应该销毁,以释放占用的资源。
学习完基本架构的相关内容后,相信读者朋友对Vulkan图形应用程序的各个部分有了一定的认识。但由于各部分是分别介绍的,这可能导致没有一个全局观,对各部分相互之间的关系不够明晰。下面将给出一幅基本架构图,其中包含了前面介绍的各个部分,具体情况如图1-57所示。
▲图1-57 Vulkan图形应用程序基本架构图
Vulkan图形应用程序的基本架构已经介绍完毕,现在正式进入代码的学习。由于Vulkan程序比较复杂,整体代码较长。因此在介绍具体的代码之前还需要对3色三角形案例中各个类或模块的功能有一个简单的了解,具体内容如下。
该模块包含事件处理回调方法、命令回调方法和程序入口函数方法。事件处理回调方法涉及触控点的按下、移动、弹起等触控响应。命令回调方法涉及窗口初始化、窗口终止等窗口响应。入口函数主要功能是为应用程序设置命令回调方法、事件处理回调方法并启动事件循环。
该类是案例中最重要的类之一,完成了Vulkan图形应用程序所需各类实例的创建和销毁以及绘制画面的工作,例如创建Vulkan实例、初始化物理设备、销毁实例、销毁逻辑设备、绘制画面等。因此,本质上此类是案例中的统筹管理者。
前面提到过Vulkan改变了一贯OpenGL着色器程序的编译方式,引入一种被称为SPIR-V的中间语言,该中间语言可以方便地在各种适配Vulkan的GPU上被处理运行。这既提高了开发的灵活性,又可以提高程序运行时的效率。ShaderCompileUtil类就是用于将GLSL着色器脚本编译为SPIR-V格式的工具类,本书几乎每个案例中都有这个类的身影。
此类对管线的各方面信息进行了设置,主要包括一致变量缓冲信息、描述集信息、着色器信息、管线动态信息等相关内容。当程序中存在多种不同的渲染管线时,会有多个与ShaderQueueSuit_Common相似的类。一般来说,每套着色器程序对应一个渲染管线实例,这一点在后面章节较为复杂的案例中会有体现。
此类中最重要的就是doTask方法了,此方法中按照顺序调用了Vulkan图形应用程序运行过程中各个阶段的相关方法,供指定的线程来执行。这也是Vulkan程序区别于一般OpenGL程序的一个特点,Vulkan中并没有像OpenGL那样提供一个特殊的GL线程,而是由用户创建线程来执行相关绘制任务。这也为多线程并发执行绘制任务铺平了道路,本书后面会有专门讨论多线程并发绘制的章节。
此类用于存储3色三角形的顶点位置数据和对应的顶点颜色数据,并提供了数据的生成方法。
此类对象代表绘制用的物体,对于本案例而言就是3色三角形。一般开发中为了方便,每种绘制用物体单独一个对象。DrawableObjectCommon类对象中携带了绘制用物体的顶点相关数据缓冲,同时还提供了供绘制时调用的drawSelf方法。
此类用于加载各种资源文件,诸如着色器脚本字符串、纹理数据文件等。对于本案例而言此类中提供了用于加载顶点着色器和片元着色器脚本字符串的相关方法。
该类中封装了计算FPS和限制FPS最大值的相关方法。对于移动应用而言,过高的FPS对用户的体验提升不大,但耗电量会急剧增加。因此考虑到项目跨平台的问题,作者开发了这个工具类。
此类中封装了用于多次重复调用的工具方法,随项目的不同此类中包含的方法不尽相同。对于3色三角形案例而言,其中只封装了确定内存类型索引的memoryTypeFromProperties方法。
此类中包含了实现矩阵各种数学运算的方法,主要包括:矩阵与矩阵相乘、矩阵乘以向量、设置单位矩阵、生成平移矩阵、生成旋转矩阵、生成缩放矩阵、生成投影矩阵、生成摄像机观察矩阵、矩阵的转置等。
将给定的顶点信息绘制为所需的画面需要经历一系列的数学运算,为了开发的方便,作者开发了用于管理矩阵状态的MatrixState3D类,本书中的每个案例都将包含此类。
提示
前面已经提到过,本书中所有案例项目都将给出Android和PC两种版本。这里主要给出了PC和Android项目中共有的类,没有详细介绍PC和Android项目中不同的类,读者朋友如果感兴趣,请自行查看不同版本的项目。另外,本书后面的代码讲解主要是以Android版项目代码为主,PC版项目的大部分代码与Android版相同,读者可以自行参照学习。
实际开发中需要用到很多Vulkan提供的功能方法,在介绍具体的案例代码之前有必要先了解这些常用的功能方法。本小节将详细介绍3色三角形案例中用到的Vulkan提供的许多功能方法,具体内容如下所列。
(1)首先介绍的是Vulkan中用于执行基本操作的相关功能方法,主要包括创建Vulkan实例、创建逻辑设备、创建命令池、创建帧缓冲等,具体内容如表1-2所示。
表1-2 Vulkan基本操作的功能方法
方法签名 |
说明 |
---|---|
VkResult vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance) |
此方法功能为创建Vulkan实例。第一个参数为指向创建信息结构体实例的指针;第二个参数为指向自定义内存分配器的指针(若不使用自定义内存分配器则调用时此指针为空);第三个参数为指向创建的Vulkan实例的指针;返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkEnumeratePhysicalDevices(VkInstance instance, uint32_t* pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices) |
此方法功能为枚举已安装的Vulkan物理设备。第一个参数为Vulkan实例;第二个参数为指向Vulkan物理设备数量变量的指针;第三个参数为指向枚举的Vulkan物理设备列表首地址的指针。若第三个参数值为NULL,则此方法获取物理设备数量值送入第二个参数指向的变量;若第三个参数值不为NULL,则获取第二个参数值所给出数量的物理设备列表。返回值为VkResult枚举类型,若值为VK_SUCCESS 、VK_INCOMPLETE表示枚举操作执行成功,否则表示操作失败 |
void vkGetPhysicalDeviceMemoryProperties( VkPhysical Device physicalDevice, VkPhysicalDeviceMemory Properties* pMemoryProperties) |
此方法的功能为获取指定物理设备的内存属性。第一个参数为指定的物理设备;第二个参数为指向获取的物理设备内存属性列表首地址的指针 |
void vkGetPhysicalDeviceQueueFamilyProperties( |
此方法功能为获取指定物理设备的队列家族数量和属性列表。第一个参数为指定的物理设备;第二个参数为指向队列家族数量变量的指针;第三个参数为指向获取的队列家族属性列表首地址的指针。若第三个参数值为NULL,则此方法获取指定物理设备队列家族数量送入第二个参数所指向的变量;若第三个参数值不为NULL,则获取第二个参数值所给出数量的队列家族属性列表 |
VkResult vkCreateDevice(VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDevice* pDevice) |
此方法功能为创建逻辑设备。第一个参数为对应的物理设备;第二个参数为指向逻辑设备创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针(若不使用自定义内存分配器则调用时此指针为空);第四个参数为指向创建的逻辑设备的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkCreateCommandPool(VkDevice device, const VkCommandPoolCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkCommandPool* pCommandPool) |
此方法功能为创建命令池。第一个参数为指定的逻辑设备;第二个参数为指向命令池创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针(若不使用自定义内存分配器则调用时此指针为空);第四个参数为指向创建的命令池的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkAllocateCommandBuffers(VkDevice device, const VkCommandBufferAllocateInfo* pAllocateInfo, VkCommandBuffer* pCommandBuffers) |
此方法功能为分配命令缓冲。第一个参数为指定的逻辑设备;第二个参数为指向命令缓冲分配信息结构体实例的指针;第三个参数为指向分配的命令缓冲的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
void vkGetDeviceQueue(VkDevice device, uint32_t queueFamilyIndex, uint32_t queueIndex,VkQueue* pQueue); |
此方法的功能为获取给定队列家族中指定索引的队列。第一个参数为指定的逻辑设备;第二个参数为指定队列家族的索引;第三个参数为要获取队列的索引;第四个参数为指向获取队列的指针 |
VkResult vkCreateRenderPass(VkDevice device, const VkRenderPassCreateInfo* pCreateInfo, const VkAllocation Callbacks* pAllocator, VkRenderPass* pRenderPass) |
此方法的功能为创建渲染通道。第一个参数为指定的逻辑设备;第二个参数为指向渲染通道创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的渲染通道的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkCreateFramebuffer(VkDevice device,const VkFramebufferCreateInfo* pCreateInfo,const VkAllocation Callbacks* pAllocator,VkFramebuffer* pFramebuffer) |
此方法的功能为创建帧缓冲。第一个参数为指定的逻辑设备;第二个参数为指向帧缓冲创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的帧缓冲的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkCreateFence(VkDevice device, const VkFenceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkFence* pFence) |
此方法的功能为创建栅栏。第一个参数为指定的逻辑设备;第二个参数为指向栅栏创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的栅栏的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
void vkDestroyFramebuffer(VkDevice device, VkFramebuffer framebuffer,const VkAllocationCallbacks* pAllocator) |
此方法功能为销毁帧缓冲。第一个参数为指定的逻辑设备;第二个参数为要销毁的帧缓冲;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkDestroyRenderPass(VkDevice device, VkRender Pass renderPass, const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁渲染通道。第一个参数为指定的逻辑设备,第二个参数为要销毁的渲染通道;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkDestroyCommandPool(VkDevice device,VkCommand Pool commandPool,const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁命令池。第一个参数为指定的逻辑设备;第二个参数为需要被销毁的命令池;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkFreeCommandBuffers(VkDevice device, VkCommand Pool commandPool, uint32_t commandBufferCount, const VkCommandBuffer* pCommandBuffers) |
此方法的功能为释放命令缓冲。第一个参数为指定的逻辑设备;第二个参数为命令缓冲对应的命令池;第三个参数为要释放命令缓冲的数量;第四个参数为指向要释放的命令缓冲列表首地址的指针 |
void vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁逻辑设备。第一个参数为要销毁的逻辑设备;第二个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkDestroyInstance(VkInstance instance,const VkAllocationCallbacks* pAllocator); |
此方法的功能为销毁Vulkan实例。第一个参数为要销毁的Vulkan实例;第二个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
(2)接着介绍的是与Vulkan显示工作相关的功能方法,主要包括创建KHR表面、初始化交换链、获取表面显示信息、获取表面支持的格式数量等,具体内容如表1-3所示。
表1-3 Vulkan显示工作相关的功能方法
方法签名 |
说明 |
---|---|
VkResult vkCreateAndroidSurfaceKHR(VkInstance instance, const VkAndroidSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface) |
此方法的功能为创建Android平台用KHR表面。第一个参数为Vulkan实例;第二个参数为指向Android表面创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的表面的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkGetPhysicalDeviceSurfaceSupportKHR (VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex, VkSurfaceKHR surface, VkBool32* pSupported) |
此方法功能为判断物理设备中指定的队列家族是否支持KHR表面呈现。第一个参数为指定的物理设备;第二个参数为指定的队列家族索引;第三个参数为给定的KHR表面;第四个参数为指向存储判断结果布尔值变量的指针。若第四个参数指向的变量值为VK_TRUE表示支持,为VK_FALSE表示不支持。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkGetPhysicalDeviceSurfaceFormatsKHR( |
此方法的功能为获取物理设备支持的KHR表面格式。第一个参数为指定的物理设备;第二个参数为指定的KHR表面;第三个参数为指向存储支持的表面格式数量变量的指针;第四个参数为指向支持的格式信息列表首地址的指针。若第四个参数为NULL,则该方法获取支持的表面格式数量送入第三个参数所指向的变量;若第四个参数不为NULL,则获取第二个参数所给出数量的支持的表面格式的信息。返回值为VkResult枚举类型,若值为VK_SUCCESS、VK_INCOMPLETE表示操作执行成功,否则表示操作失败 |
VkResult vkGetPhysicalDeviceSurfaceCapabilitiesKHR (VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, VkSurfaceCapabilitiesKHR* pSurfaceCapabilities) |
此方法的功能为获取物理设备中KHR表面的能力。第一个参数为指定的物理设备;第二个参数为给定的KHR表面;第三个参数为指向获取的表面能力的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示获取成功,否则表示获取失败 |
VkResult vkGetPhysicalDeviceSurfacePresentModes KHR(VkPhysicalDevice physicalDevice, VkSurface KHR surface, uint32_t* pPresentModeCount, VkPresent ModeKHR* pPresentModes) |
此方法的功能为获取物理设备中KHR表面的呈现模式。第一个参数为指定的物理设备;第二个参数为给定的KHR表面;第三个参数为指向呈现模式数量变量的指针;第四个参数为指向呈现模式列表首地址的指针。若第四个参数为NULL,则该方法获取呈现模式的数量送入第三个参数所指向的变量;若第四个参数不为NULL,则获取第二个参数所给出数量的呈现模式列表。返回值为VkResult枚举类型,若值为VK_SUCCESS、VK_INCOMPLETE表示操作执行成功,否则表示操作失败 |
VkResult vkCreateSwapchainKHR( VkDevice device, const VkSwapchainCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSwapchain KHR* pSwapchain) |
此方法的功能为创建KHR交换链。第一个参数为指定的逻辑设备;第二个参数为指向交换链创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的交换链的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkGetSwapchainImagesKHR( VkDevice device, VkSwapchainKHR swapchain, uint32_t* pSwapchainImage Count, VkImage* pSwapchainImages) |
此方法的功能为获取交换链中的图像数量和图像列表。第一个参数为指定的逻辑设备;第二个参数为给定的交换链;第三个参数为指向交换链中图像数量变量的指针;第四个参数为指向交换链中图像列表首地址的指针。若第四个参数为NULL,则获取交换链中图像的数量送入第三个参数所指向的变量;若第四个参数不为NULL,则获取第二个参数所给出数量的图像列表。返回值为VkResult枚举类型,若值为VK_SUCCESS、VK_INCOMPLETE表示操作执行成功,否则表示操作失败 |
void vkGetPhysicalDeviceFormatProperties(VkPhysical Device physicalDevice, VkFormat format, VkFormat Properties* pFormatProperties) |
此方法的功能为获取物理设备中给定格式的属性。第一个参数为指定的物理设备;第二个参数为给定的格式;第三个参数为指向获取的格式属性的指针 |
(3)其次介绍的是与Vulkan管线相关的功能方法,包括创建描述集布局、创建管线布局、创建描述集池、创建着色器模块、创建管线缓冲、创建图形管线等,具体内容如表1-4所示。
表1-4 Vulkan管线相关的功能方法
方法签名 |
说明 |
---|---|
VkResult vkCreateDescriptorSetLayout(VkDevice device, const VkDescriptorSetLayoutCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator,VkDescriptor SetLayout* pSetLayout) |
此方法的功能为创建描述集布局。第一个参数为指定的逻辑设备;第二个参数为指向描述集布局创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的描述集布局的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkCreatePipelineLayout(VkDevice device, const VkPipelineLayoutCreateInfo* pCreateInfo,const VkAllocationCallbacks* pAllocator,VkPipelineLayout* pPipelineLayout) |
此方法的功能为创建管线布局。第一个参数为指定的逻辑设备;第二个参数为指向管线布局创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的管线布局的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkCreateDescriptorPool(VkDevice device, const VkDescriptorPoolCreateInfo* pCreateInfo,const VkAllocationCallbacks* pAllocator,VkDescriptorPool* pDescriptorPool) |
此方法的功能为创建描述集池。第一个参数为指定的逻辑设备;第二个参数指向描述集池创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的描述集池的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkAllocateDescriptorSets(VkDevice device, const VkDescriptorSetAllocateInfo* pAllocateInfo, VkDescriptorSet* pDescriptorSets) |
此方法的功能为分配描述集。第一个参数为指定的逻辑设备;第二个参数为指向描述集分配信息结构体实例的指针;第三个参数为指向分配的描述集的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkCreateShaderModule(VkDevice device, const VkShaderModuleCreateInfo* pCreateInfo,const VkAllocationCallbacks* pAllocator,VkShaderModule* pShaderModule) |
此方法功能为创建着色器模块。第一个参数为指定的逻辑设备;第二个参数为指向着色器模块创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的着色器模块的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkCreatePipelineCache(VkDevice device, const VkPipelineCacheCreateInfo* pCreateInfo,const VkAllocationCallbacks* pAllocator,VkPipelineCache* pPipelineCache) |
此方法的功能为创建管线缓冲。第一个参数为指定的逻辑设备;第二个参数为指向管线缓冲创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的管线缓冲的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkCreateGraphicsPipelines(VkDevice device, VkPipelineCache pipelineCache,uint32_t createInfo Count,const VkGraphicsPipelineCreateInfo* pCreateInfos, const VkAllocationCallbacks* pAllocator,VkPipeline* pPipelines) |
此方法功能为创建图形管线。第一个参数为指定的逻辑设备;第二个参数为管线缓冲;第三个参数为需要创建的管线数量;第四个参数为指向图形管线创建信息结构体实例的指针;第五个参数为指向自定义内存分配器的指针;第六个参数为指向创建的管线的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
void vkDestroyPipeline(VkDevice device,VkPipeline pipeline,const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁管线。第一个参数为指定的逻辑设备;第二个参数为要销毁的管线;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkDestroyPipelineCache(VkDevice device,VkPipeline Cache pipelineCache,const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁管线缓冲。第一个参数为指定的逻辑设备;第二个参数为要删除的管线缓冲;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkDestroyShaderModule(VkDevice device,VkShader Module shaderModule,const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁着色器模块。第一个参数为指定的逻辑设备;第二个参数为要删除的着色器模块;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkDestroyDescriptorSetLayout(VkDevice device, VkDescriptorSetLayout descriptorSetLayout, const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁描述集布局。第一个参数为指定的逻辑设备;第二个参数为要销毁的描述集布局;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkDestroyPipelineLayout(VkDevice device, |
此方法的功能为销毁管线布局。第一个参数为指定的逻辑设备;第二个参数为要销毁的管线布局;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
(4)接下来介绍的是与Vulkan绘制阶段相关的功能方法,包括创建信号量、获取当前交换链中当前帧的索引、启动命令缓冲、启动渲染通道等,具体内容如表1-5所示。
表1-5 Vulkan显示工作相关的功能方法
方法签名 |
说明 |
---|---|
VkResult vkCreateSemaphore(VkDevice device,const VkSemaphoreCreateInfo* pCreateInfo,const VkAllocation Callbacks* pAllocator,VkSemaphore* pSemaphore) |
此方法的功能为创建信号量。第一个参数为指定的逻辑设备;第二个参数为指向信号量创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的信号量的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkAcquireNextImageKHR(VkDevice device, VkSwapchainKHR swapchain,uint64_t timeout, VkSemaphore semaphore,VkFence fence,uint32_t* pImageIndex) |
此方法的功能为获取交换链中当前帧的索引。第一个参数为指定的逻辑设备;第二个参数为给定的交换链;第三个参数为超时时间(即若没有可以使用的图像对象时等待的最大时间。若此参数为0,表示无论有没有可用的图像对象方法立即返回。);第四个参数为指定的信号量(当成功获取可以使用的图像对象时触发此信号量,以便和其他工作进行同步,若不需要可将此参数设置为空);第五个参数为指定的栅栏(当成功获取可以使用的图像对象时设置此栅栏的状态为完成态,以便和其他工作进行同步,若不需要可将此参数设置为空);第六个参数为获取的当前帧索引。返回值为VkResult枚举类型,若值为VK_SUCCESS、VK_TIMEOUT、VK_NOT_READY、VK_SUBOPTIMAL_KHR表示操作成功,否则表示操作失败。要特别注意的是,调用此方法时semaphore、fence两个参数不能同时为空,至少其中一个应该传入有效值 |
VkResult vkResetCommandBuffer(VkCommandBuffer commandBuffer,VkCommandBufferResetFlags flags) |
此方法的功能为恢复命令缓冲到初始状态。第一个参数为需要恢复的命令缓冲;第二个参数为恢复操作特定工作标志位。若第二个参数设置成0,则表示恢复命令缓冲时没有特定工作需要执行。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkBeginCommandBuffer(VkCommandBuffer commandBuffer,const VkCommandBufferBeginInfo* pBeginInfo) |
此方法的功能为启动命令缓冲,开始记录命令。第一个参数为需要启动的命令缓冲;第二个参数为指向命令缓冲启动信息结构体实例的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
void vkCmdBeginRenderPass(VkCommandBuffer command Buffer,const VkRenderPassBeginInfo* pRenderPass Begin,VkSubpassContents contents) |
此方法的功能为启动渲染通道。第一个参数为给定的命令缓冲;第二个参数为指向渲染通道启动信息结构体实例的指针;第三个参数用于指定第一个子渲染通道中命令的提供方式 |
void vkCmdBindPipeline(VkCommandBuffer command Buffer,VkPipelineBindPoint pipelineBindPoint, VkPipeline pipeline) |
此方法的功能为绑定命令缓冲与管线。第一个参数为需要绑定的命令缓冲;第二个参数用于指定绑定点;第三个参数为需要绑定的管线。若第二个参数为VK_PIPELINE_BIND_POINT_GRAPHICS,表示为绑定图形渲染管线,若第二个参数为VK_PIPELINE_BIND_POINT_COMPUTE表示绑定计算管线 |
void vkCmdBindDescriptorSets(VkCommandBuffer commandBuffer,VkPipelineBindPoint pipelineBindPoint, VkPipelineLayout layout,uint32_t firstSet,uint32_t descriptorSetCount,const VkDescriptorSet* pDescriptor Sets,uint32_t dynamicOffsetCount,const uint32_t* pDynamicOffsets) |
此方法的功能为将命令缓冲与一个或者多个描述集进行绑定。第一个参数为需要绑定的命令缓冲;第二个参数为指定绑定点;第三个参数为管线布局;第四个参数为第一个需要绑定的描述集的索引;第五个参数为绑定的描述集数量;第六个参数为指向需要绑定的描述集列表的指针;第七个参数为动态偏移量的数量;第八个参数为指向动态偏移量列表的指针 |
void vkCmdBindVertexBuffers(VkCommandBuffer commandBuffer,uint32_t firstBinding,uint32_t bindingCount,const VkBuffer* pBuffers,const VkDeviceSize* pOffsets) |
此方法的功能为将顶点缓冲与命令缓冲绑定,以便在执行绘制任务时向管线提供顶点数据。第一个参数为指定的命令缓冲;第二个参数为绑定的多个顶点数据缓冲在列表中的首索引;第三个参数为绑定的顶点缓冲数量;第四个参数为指向顶点缓冲列表首地址的指针;第五个参数为指向各个顶点缓冲内部偏移量构成的数组的首地址的指针 |
void vkCmdDraw(VkCommandBuffer commandBuffer, uint32_t vertexCount,uint32_t instanceCount,uint32_t firstVertex,uint32_t firstInstance) |
此方法的功能为执行绘制。第一个参数为指定的命令缓冲;第二个参数为需要绘制的顶点数量;第三个参数为需要绘制的实例的数量;第四个参数为第一个绘制用顶点的索引;第五个参数为第一个绘制的实例序号 |
void vkCmdEndRenderPass(VkCommandBuffer |
此方法的功能为结束渲染通道。方法参数为指定的命令缓冲 |
VkResult vkEndCommandBuffer(VkCommandBuffer commandBuffer); |
此方法的功能为结束命令缓冲,完成所有命令的记录。方法参数为指定的命令缓冲。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkQueueSubmit(VkQueue queue,uint32_t submitCount,const VkSubmitInfo* pSubmits,VkFence fence) |
此方法的功能为将命令缓冲提交给指定队列执行。第一个参数为指定的队列;第二个参数为提交信息结构体实例的数量;第三个参数为指向提交信息结构体实例列表首地址的指针;第四个参数为对应的栅栏(当命令缓冲被提交后,程序可以通过此栅栏的状态判断提交的任务是否执行完毕)。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkWaitForFences(VkDevice device,uint32_t fenceCount,const VkFence* pFences,VkBool32 waitAll, uint64_t timeout) |
此方法的功能为等待指定的栅栏完成。第一个参数为指定的逻辑设备;第二个参数为等待的栅栏数量;第三个参数为指向栅栏列表首地址的指针;第四个参数为是否等待所有栅栏完成标志;第五个参数为等待的超时时间。若第四个参数为VK_TRUE,那么此方法将等待所有的栅栏完成,否则只需要等到某一个栅栏完成即可。返回值为VkResult枚举类型,若值为VK_SUCCESS、VK_INCOMPLETE表示操作执行成功,否则表示操作失败 |
VkResult vkResetFences(VkDevice device,uint32_t fenceCount,const VkFence* pFences) |
此方法的功能为将栅栏重置到未触发状态。第一个参数为指定的逻辑设备;第二个参数为栅栏的数量;第三个参数为指向需要重置的栅栏列表首地址的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkQueuePresentKHR(VkQueue queue,const VkPresentInfoKHR* pPresentInfo) |
此方法的功能为执行呈现。第一个参数为用于呈现的队列;第二个参数为指向呈现描述信息结构体实例的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作执行成功,否则表示操作失败(如VK_ERROR_SURFACE_LOST_KHR、VK_SUBOPTIMAL _KHR、VK_ERROR_OUT_OF_DATE_KHR等) |
void vkDestroySemaphore(VkDevice device, VkSemaphore semaphore, const VkAllocationCallbacks* pAllocator) |
此方法功能为销毁信号量。第一个参数为指定的逻辑设备;第二个参数为需要被销毁的信号量;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
(5)最后介绍的是与Vulkan设备内存使用相关的功能方法,包括创建图像、创建图像视图、创建缓冲、获取缓冲的内存需求、映射指定设备内存为CPU可访问等,具体内容如表1-6所示。
表1-6 Vulkan内存使用相关的功能方法
方法签名 |
说明 |
---|---|
VkResult vkCreateImage(VkDevice device, const VkImage CreateInfo* pCreateInfo, const VkAllocation Callbacks* pAllocator, VkImage* pImage) |
此方法的功能为创建图像。第一个参数为指定的逻辑设备;第二个参数为指向图像创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的图像的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示创建成功,否则表示创建失败 |
VkResult vkCreateImageView(VkDevice device,const VkImageViewCreateInfo* pCreateInfo,const VkAllocation Callbacks* pAllocator,VkImageView* pView) |
此方法的功能为创建图像视图。第一个参数为指定的逻辑设备;第二个参数为指向图像视图创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的图像视图的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
void vkGetImageMemoryRequirements(VkDevice device, VkImage image,VkMemoryRequirements* pMemory Requirements) |
此方法的功能为获取图像内存需求。第一个参数为指定的逻辑设备;第二个参数为需要获取内存需求的图像;第三个参数为指向获取的图像内存需求的指针 |
VkResult vkAllocateMemory(VkDevice device,const VkMemoryAllocateInfo* pAllocateInfo,const VkAllocation Callbacks* pAllocator,VkDeviceMemory* pMemory) |
此方法的功能为分配设备内存。第一个参数为指定的逻辑设备;第二个参数为指向内存分配信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向分配的设备内存的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkBindImageMemory(VkDevice device, VkImage image,VkDeviceMemory memory, VkDeviceSize memoryOffset) |
此方法功能为将设备内存与图像进行绑定。第一个参数为指定的逻辑设备;第二个参数为需要绑定的图像;第三个参数为需要绑定的设备内存;第四个参数为绑定的设备内存起始偏移量。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
VkResult vkCreateBuffer(VkDevice device,const VkBuffer CreateInfo* pCreateInfo,const VkAllocationCallbacks* pAllocator,VkBuffer* pBuffer) |
此方法的功能为创建缓冲。第一个参数为指定的逻辑设备;第二个参数为指向缓冲创建信息结构体实例的指针;第三个参数为指向自定义内存分配器的指针;第四个参数为指向创建的缓冲的指针。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
void vkGetBufferMemoryRequirements(VkDevice device, VkBuffer buffer,VkMemoryRequirements* pMemory Requirements) |
此方法的功能为获取缓冲的内存需求。第一个参数为指定的逻辑设备;第二个参数为需要获取内存需求的缓冲;第三个参数为指向获取的内存需求的指针 |
VkResult vkMapMemory(VkDevice device,VkDevice Memory memory,VkDeviceSize offset,VkDeviceSize size,VkMemoryMapFlags flags,void** ppData) |
此方法的功能为将设备内存映射为CPU可以访问的内存。第一个参数为指定的逻辑设备;第二个参数为要映射的设备内存对象;第三个参数为设备内存的起始偏移量;第四个参数为映射的设备内存范围大小;第五个参数为保留的参数,供未来使用;第六个参数为供CPU访问时指向映射内存首地址的指针。若第四个参数值为VK_WHOLE_SIZE则表示映射范围从起始偏移量到要映射的设备内存的最后。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
void vkUnmapMemory(VkDevice device,VkDevice Memory memory) |
此方法的功能为解除设备内存的映射。第一个参数为指定的逻辑设备;第二个参数为需要解除映射的设备内存 |
VkResult vkBindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize memoryOffset) |
此方法的功能为将设备内存与缓冲进行绑定。第一个参数为指定的逻辑设备;第二个参数为需要绑定的缓冲;第三个参数为需要绑定的设备内存;第四个参数为绑定设备内存的区域起始偏移量。返回值为VkResult枚举类型,若值为VK_SUCCESS表示操作成功,否则表示操作失败 |
void vkDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁缓冲。第一个参数为指定的逻辑设备;第二个参数为要销毁的缓冲;第三个参数为指向自定义内存分配器的指针 |
void vkFreeMemory(VkDevice device,VkDeviceMemory memory,const VkAllocationCallbacks* pAllocator) |
此方法功能为释放指定的设备内存。第一个参数为指定的逻辑设备;第二个参数为需要被释放的设备内存;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkDestroyImageView(VkDevice device, VkImage View imageView, const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁图像视图。第一个参数为指定的逻辑设备;第二个参数为需要销毁的图像视图;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
void vkDestroyImage(VkDevice device,VkImage image,const VkAllocationCallbacks* pAllocator) |
此方法的功能为销毁图像。第一个参数为指定的逻辑设备;第二个参数为需要销毁的图像;第三个参数为指向自定义内存分配器的指针,若创建时没有使用自定义内存分配器则这里值为NULL |
提示
前面的表1-2至表1-6中的很多Vulkan创建方法都用到了某种类型的创建信息结构体实例。这些不同类型的创建信息结构体实例用于向对应的创建方法提供执行时的必要信息,在后面的代码介绍部分将对使用到的一些进行详细的介绍,这里读者简单了解即可。另外,前面多个表中都提到了设备内存,其指的是能够被GPU直接访问的内存,通俗讲就是显存。与其对应的是主机内存,其指的是能够被CPU直接访问的内存,这种内存读者肯定非常熟悉了。
通过前面的几节介绍,读者对3色三角形案例应该有了一个总体的了解。下面正式开始介绍案例项目代码的开发,首先需要了解的是程序统筹管理者类——MyVulkanManager的基本结构,具体内容如下。
(1)首先介绍的是MyVulkanManager类的基本结构,此类中声明了程序运行过程中需要用到的各种成员变量以及功能方法,诸如Vulkan实例、逻辑设备、命令缓冲、交换链、深度缓冲图像等,具体代码如下。
提示
本书中每个案例的Android版本项目均以“SampleX_X”的形式来命名,对应的PC版本项目均以“PCSampleX_X”的形式来命名,读者根据自己的需要选用相应版本的案例项目即可。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.h。
1 //此处省略了相关头文件的导入,感兴趣的读者请自行查看随书源代码
2 #define FENCE_TIMEOUT 100000000 //栅栏的超时时间
3 class MyVulkanManager{
4 public:
5 static android_app* Android_application; //Android应用指针
6 static bool loopDrawFlag; //绘制的循环工作标志
7 static std::vector<const char *> instanceExtensionNames;//需要使用的实例扩展名称列表
8 static VkInstance instance; //Vulkan实例
9 static uint32_t gpuCount; //物理设备数量
10 static std::vector<VkPhysicalDevice> gpus; //物理设备列表
11 static uint32_t queueFamilyCount; //物理设备对应的队列家族数量
12 static std::vector<VkQueueFamilyProperties> queueFamilyprops;
//物理设备对应的队列家族属性列表
13 static uint32_t queueGraphicsFamilyIndex; //支持图形工作的队列家族索引
14 static VkQueue queueGraphics; //支持图形工作的队列
15 static uint32_t queuePresentFamilyIndex; //支持显示工作的队列家族索引
16 static std::vector<const char *> deviceExtensionNames;//所需的设备扩展名称列表
17 static VkDevice device; //逻辑设备
18 static VkCommandPool cmdPool; //命令池
19 static VkCommandBuffer cmdBuffer; //命令缓冲
20 static VkCommandBufferBeginInfo cmd_buf_info; //命令缓冲启动信息
21 static VkCommandBuffer cmd_bufs[1]; //供提交执行的命令缓冲数组
22 static VkSubmitInfo submit_info[1]; //命令缓冲提交执行信息数组
23 static uint32_t screenWidth; //屏幕宽度
24 static uint32_t screenHeight; //屏幕高度
25 static VkSurfaceKHR surface; //KHR表面
26 static std::vector<VkFormat> formats; //KHR表面支持的格式
27 static VkSurfaceCapabilitiesKHR surfCapabilities; //表面的能力
28 static uint32_t presentModeCount; //显示模式数量
29 static std::vector<VkPresentModeKHR> presentModes;//显示模式列表
30 static VkExtent2D swapchainExtent; //交换链尺寸
31 static VkSwapchainKHR swapChain; //交换链
32 static uint32_t swapchainImageCount; //交换链中的图像数量
33 static std::vector<VkImage> swapchainImages; //交换链中的图像列表
34 static std::vector<VkImageView> swapchainImageViews;//交换链对应的图像视图列表
35 static VkFormat depthFormat; //深度图像格式
36 static VkFormatProperties depthFormatProps; //物理设备支持的深度格式属性
37 static VkImage depthImage; //深度缓冲图像
38 static VkPhysicalDeviceMemoryProperties memoryroperties;//物理设备内存属性
39 static VkDeviceMemory memDepth; //深度缓冲图像对应的内存
40 static VkImageView depthImageView; //深度缓冲图像视图
41 static VkSemaphore imageAcquiredSemaphore; //渲染目标图像获取完成信号量
42 static uint32_t currentBuffer; //从交换链中获取的当前渲染用图像对应的缓冲编号
43 static VkRenderPass renderPass; //渲染通道
44 static VkClearValue clear_values[2];//渲染通道用清除帧缓冲深度、颜色附件的数据
45 static VkRenderPassBeginInfo rp_begin; //渲染通道启动信息
46 static VkFence taskFinishFence; //等待任务完毕的栅栏
47 static VkPresentInfoKHR present; //呈现信息
48 static VkFramebuffer* framebuffers; //帧缓冲序列首指针
49 static ShaderQueueSuit_Common* sqsCL; //着色器管线指针
50 static DrawableObjectCommonLight* triForDraw; //绘制用3色三角形物体对象指针
51 static float xAngle; //三角形旋转角度
52 //此处省略了多个功能方法的声明,下文将会详细介绍
53 };
说明
上述头文件中定义了很多的成员变量,涉及不少Vulkan提供的基本类型。这些类型在Vulkan应用程序的开发中都是经常使用的,如果读者还没有掌握Vulkan基本类型的相关知识,请参考本书1.3.2节中的表1-1学习一下。
(2)接着给出的是步骤(1)中省略的多个功能方法的声明,主要包括创建Vulkan实例、初始化物理设备、创建逻辑设备、创建命令缓冲、初始化队列、初始化渲染管线等,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.h。
1 static void init_vulkan_instance(); //创建Vulkan实例
2 static void enumerate_vulkan_phy_devices(); //初始化物理设备
3 static void create_vulkan_devices(); //创建逻辑设备
4 static void create_vulkan_CommandBuffer(); //创建命令缓冲
5 static void create_vulkan_swapChain(); //初始化交换链
6 static void create_vulkan_DepthBuffer(); //创建深度缓冲相关
7 static void create_render_pass(); //创建渲染通道
8 static void init_queue(); //获取设备中支持图形工作的队列
9 static void create_frame_buffer(); //创建帧缓冲
10 static void createDrawableObject(); //创建绘制用物体
11 static void drawObject(); //执行场景中的物体绘制
12 static void doVulkan(); //启动线程执行Vulkan任务
13 static void initPipeline(); //初始化管线
14 static void createFence(); //创建栅栏
15 static void initPresentInfo(); //初始化显示信息
16 static void initMatrix(); //初始化矩阵
17 static void flushUniformBuffer(); //将一致变量数据送入缓冲
18 static void flushTexToDesSet(); //将纹理等数据与描述集关联
19 static void destroyFence(); //销毁栅栏
20 static void destroyPipeline(); //销毁管线
21 static void destroyDrawableObject(); //销毁绘制用物体
22 static void destroy_frame_buffer(); //销毁帧缓冲
23 static void destroy_render_pass(); //销毁渲染通道
24 static void destroy_vulkan_DepthBuffer(); //销毁深度缓冲相关
25 static void destroy_vulkan_swapChain(); //销毁交换链
26 static void destroy_vulkan_CommandBuffer(); //销毁命令缓冲
27 static void destroy_vulkan_devices(); //销毁逻辑设备
28 static void destroy_vulkan_instance(); //销毁实例
说明
上述代码中声明了很多Vulkan应用程序执行过程中需要用到的功能方法,后面的部分将对这些功能方法进行详细的介绍。
了解了MyVulkanManager类的基本结构后,下面依次对其中的功能方法进行介绍。首先介绍的是用于创建Vulkan实例的方法——init_vulkan_instance,其中包含了加载Vulkan动态库、初始化所需实例的扩展名称列表、构建应用信息结构体实例等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::init_vulkan_instance(){ //创建Vulkan实例的方法
2 AAssetManager* aam=MyVulkanManager::Android_application->
3 activity->assetManager; //获取资源管理器指针
4 FileUtil::setAAssetManager(aam); //将资源管理器传给文件I/O工具类
5 if (!vk::loadVulkan()){ //加载Vulkan动态库
6 LOGI("加载Vulkan图形应用程序接口失败!");
7 return ;
8 }
9 instanceExtensionNames.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
10 instanceExtensionNames
11 .push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);//初始化所需实例扩展名称列表
12 VkApplicationInfo app_info = {}; //构建应用信息结构体实例
13 app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; //结构体的类型
14 app_info.pNext = NULL; //自定义数据的指针
15 app_info.pApplicationName = "HelloVulkan"; //应用的名称
16 app_info.applicationVersion = 1; //应用的版本号
17 app_info.pEngineName = "HelloVulkan"; //应用的引擎名称
18 app_info.engineVersion = 1; //应用的引擎版本号
19 app_info.apiVersion = VK_API_VERSION_1_0;//使用的Vulkan图形应用程序API版本
20 VkInstanceCreateInfo inst_info = {}; //构建实例创建信息结构体实例
21 inst_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; //结构体的类型
22 inst_info.pNext = NULL; //自定义数据的指针
23 inst_info.flags = 0; //供将来使用的标志
24 inst_info.pApplicationInfo = &app_info; //绑定应用信息结构体
25 inst_info.enabledExtensionCount = instanceExtensionNames.size();//扩展的数量
26 inst_info.ppEnabledExtensionNames = instanceExtensionNames.data();//扩展名称列表数据
27 inst_info.enabledLayerCount = 0; //启动的层数量
28 inst_info.ppEnabledLayerNames = NULL; //启动的层名称列表
29 VkResult result; //存储运行结果的辅助变量
30 result = vk::vkCreateInstance(&inst_info, NULL, &instance);//创建Vulkan实例
31 if(result== VK_SUCCESS){ //检查实例是否创建成功
32 LOGE("Vulkan实例创建成功!"); //打印创建成功信息
33 }else{
34 LOGE("Vulkan实例创建失败!"); //打印创建失败信息
35 }}
Vulkan实例创建完成后,接下来需要做的工作是获取物理设备列表。完成此项工作的功能方法为enumerate_vulkan_phy_devices,其中包含获取物理设备数量、得到物理设备列表、获取物理设备的内存属性等步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::enumerate_vulkan_phy_devices(){//获取物理设备列表的方法
2 gpuCount=0; //存储物理设备数量的变量
3 VkResult result = vk::vkEnumeratePhysicalDevices(instance, &gpuCount, NULL);
//获取物理设备数量
4 assert(result==VK_SUCCESS);
5 LOGE("[Vulkan硬件设备数量为%d个]",gpuCount);
6 gpus.resize(gpuCount); //设置物理设备列表尺寸
7 result = vk::vkEnumeratePhysicalDevices(instance, &gpuCount, gpus.data());
//填充物理设备列表
8 assert(result==VK_SUCCESS);
9 vk::vkGetPhysicalDeviceMemoryProperties(gpus[0],&memoryroperties);
//获取第一物理设备的内存属性
10 }
获取了物理设备列表后,就可以基于其中指定的物理设备创建逻辑设备了。创建逻辑设备的方法是create_vulkan_devices,其中主要包括获取物理设备队列家族列表和队列家族属性、遍历队列家族列表及创建逻辑设备等步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::create_vulkan_devices(){ //创建逻辑设备的方法
2 vk::vkGetPhysicalDeviceQueueFamilyProperties(gpus[0],//获取物理设备0中队列家族的数量
3 &queueFamilyCount, NULL);
4 LOGE("[Vulkan硬件设备0支持的队列家族数量为%d]",queueFamilyCount);
5 queueFamilyprops.resize(queueFamilyCount);//随队列家族数量改变vector长度
6 vk::vkGetPhysicalDeviceQueueFamilyProperties(gpus[0],//填充物理设备0队列家族属性列表
7 &queueFamilyCount, queueFamilyprops.data());
8 LOGE("[成功获取Vulkan硬件设备0支持的队列家族属性列表]");
9 VkDeviceQueueCreateInfo queueInfo = {}; //构建设备队列创建信息结构体实例
10 bool found = false; //辅助标志
11 for (unsigned int i = 0; i < queueFamilyCount; i++){ //遍历所有队列家族
12 if (queueFamilyprops[i].queueFlags & VK_QUEUE_GRAPHICS_BIT){//若当前队列家族
支持图形工作
13 queueInfo.queueFamilyIndex = i; //绑定此队列家族索引
14 queueGraphicsFamilyIndex=i; //记录支持图形工作的队列家族索引
15 LOGE("[支持GRAPHICS工作的一个队列家族的索引为%d]",i);
16 LOGE("[此家族中的实际队列数量是%d]",queueFamilyprops[i].queueCount);
17 found = true;
18 break;
19 }}
20 float queue_priorities[1] = {0.0}; //创建队列优先级数组
21 queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;//给出结构体类型
22 queueInfo.pNext = NULL; //自定义数据的指针
23 queueInfo.queueCount = 1; //指定队列数量
24 queueInfo.pQueuePriorities = queue_priorities;//给出每个队列的优先级
25 queueInfo.queueFamilyIndex = queueGraphicsFamilyIndex; //绑定队列家族索引
26 deviceExtensionNames.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);//设置所需扩展
27 VkDeviceCreateInfo deviceInfo = {}; //构建逻辑设备创建信息结构体实例
28 deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; //给出结构体类型
29 deviceInfo.pNext = NULL; //自定义数据的指针
30 deviceInfo.queueCreateInfoCount = 1; //指定设备队列创建信息结构体数量
31 deviceInfo.pQueueCreateInfos = &queueInfo; //给定设备队列创建信息结构体列表
32 deviceInfo.enabledExtensionCount = deviceExtensionNames.size();//所需扩展数量
33 deviceInfo.ppEnabledExtensionNames = deviceExtensionNames.data();//所需扩展列表
34 deviceInfo.enabledLayerCount = 0; //需启动Layer的数量
35 deviceInfo.ppEnabledLayerNames = NULL; //需启动Layer的名称列表
36 deviceInfo.pEnabledFeatures = NULL; //启用的设备特性
37 VkResult result = vk::vkCreateDevice(gpus[0], &deviceInfo, NULL, &device);
//创建逻辑设备
38 assert(result==VK_SUCCESS); //检查逻辑设备是否创建成功
39 }
说明
上述代码中创建逻辑设备时涉及了Layer,其是Vulkan中用于调试的一项技术,本书后面会有专门的章节进行介绍,这里读者简单了解即可。
接着介绍的是用于创建命令缓冲的方法——create_vulkan_CommandBuffer,其中主要包含构建命令池创建信息结构体实例、构建命令缓冲分配信息结构体实例、创建命令池、基于命令池分配命令缓冲、设置命令缓冲启动信息、设置命令缓冲提交信息等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::create_vulkan_CommandBuffer(){ //创建命令缓冲的方法
2 VkCommandPoolCreateInfo cmd_pool_info = {}; //构建命令池创建信息结构体实例
3 cmd_pool_info.sType =
4 VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; //给定结构体类型
5 cmd_pool_info.pNext = NULL; //自定义数据的指针
6 cmd_pool_info.queueFamilyIndex = queueGraphicsFamilyIndex;//绑定所需队列家族索引
7 cmd_pool_info.flags =
8 VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; //执行控制标志
9 VkResult result = vk::vkCreateCommandPool(device,//创建命令池
10 &cmd_pool_info, NULL, &cmdPool);
11 assert(result==VK_SUCCESS); //检查命令池创建是否成功
12 VkCommandBufferAllocateInfo cmdBAI = {}; //构建命令缓冲分配信息结构体实例
13 cmdBAI.sType =
14 VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;//给定结构体类型
15 cmdBAI.pNext = NULL; //自定义数据的指针
16 cmdBAI.commandPool = cmdPool; //指定命令池
17 cmdBAI.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; //分配的命令缓冲级别
18 cmdBAI.commandBufferCount = 1; //分配的命令缓冲数量
19 result = vk::vkAllocateCommandBuffers(device,
20 &cmdBAI, &cmdBuffer); //分配命令缓冲
21 assert(result==VK_SUCCESS); //检查分配是否成功
22 cmd_buf_info.sType =
23 VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; //给定结构体类型
24 cmd_buf_info.pNext = NULL; //自定义数据的指针
25 cmd_buf_info.flags = 0; //描述使用标志
26 cmd_buf_info.pInheritanceInfo = NULL; //命令缓冲继承信息
27 cmd_bufs[0] = cmdBuffer; //要提交到队列执行的命令缓冲数组
28 VkPipelineStageFlags* pipe_stage_flags = new VkPipelineStageFlags();//目标管线阶段
29 *pipe_stage_flags=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
30 submit_info[0].pNext = NULL; //自定义数据的指针
31 submit_info[0].sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;//给定结构体类型
32 submit_info[0].pWaitDstStageMask = pipe_stage_flags; //给定目标管线阶段
33 submit_info[0].commandBufferCount = 1; //命令缓冲数量
34 submit_info[0].pCommandBuffers = cmd_bufs; //提交的命令缓冲数组
35 submit_info[0].signalSemaphoreCount = 0; //任务完毕后设置的信号量数量
36 submit_info[0].pSignalSemaphores = NULL; //任务完毕后设置的信号量数组
37 }
上述命令池的执行控制标志有两种可以设置的值进行组合,具体情况如下所列。
命令缓冲的级别包括两种,VK_COMMAND_BUFFER_LEVEL_PRIMARY和VK_COMMAND_BUFFER_LEVEL_SECONDARY,分别表示一级命令缓冲(或主命令缓冲)和二级命令缓冲(或子命令缓冲)。
主命令缓冲直接提交给队列执行,子命令缓冲通过所属的主命令缓冲执行,不能直接提交给队列执行。另外,只有子命令缓冲分配时需要提供继承信息。本节案例中仅仅用到了主命令缓冲,因此没有提供继承信息。关于子命令缓冲的使用在后面的章节会进行介绍,这里读者简单了解即可。
从前面介绍创建逻辑设备的1.3.8节中可以看出,创建逻辑设备时还指定了所需的队列。接下来介绍获取逻辑设备中支持图形工作队列的方法——init_queue,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::init_queue(){ //获取设备中支持图形工作队列的方法
2 vk::vkGetDeviceQueue(device,
3 queueGraphicsFamilyIndex, 0,&queueGraphics);//获取指定队列家族中索引为0的队列
4 }
说明
上述代码调用vkGetDeviceQueue方法从指定的逻辑设备中按照指定的队列家族索引获取了索引为0的队列,此队列在后面提交绘制任务时使用。指定的队列家族索引是前面小节中获取并记录下来的支持图形工作的队列家族索引。
接着介绍的是用于初始化交换链的方法——create_vulkan_swapChain。由于该方法代码较多,故将其分成多个部分依次进行详细介绍,具体内容如下。
(1)首先介绍的是用于创建KHR表面的相关代码,主要包括遍历队列家族、寻找同时支持图形和显示工作的队列家族索引等,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::create_vulkan_swapChain(){ //初始化交换链的方法
2 screenWidth = ANativeWindow_getWidth(Android_application->window);//获取屏幕宽度
3 screenHeight = ANativeWindow_getHeight(Android_application->window);//获取屏幕高度
4 LOGE("窗体宽度%d 窗体高度%d",screenWidth,screenHeight);
5 VkAndroidSurfaceCreateInfoKHR createInfo; //构建KHR表面创建信息结构体实例
6 createInfo.sType =
7 VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;//给定结构体类型
8 createInfo.pNext = nullptr; //自定义数据的指针
9 createInfo.flags = 0; //供未来使用的标志
10 createInfo.window = Android_application->window; //给定窗体
11 PFN_vkCreateAndroidSurfaceKHR fpCreateAndroidSurfaceKHR=//动态加载创建KHR表面的方法
12 (PFN_vkCreateAndroidSurfaceKHR)vk::vkGetInstanceProcAddr(instance
13 , "vkCreateAndroidSurfaceKHR"); //加载Android平台所需方法
14 if (fpCreateAndroidSurfaceKHR == NULL){ //判断方法是否加载成功
15 LOGE( "找不到vkCreateAndroidSurfaceKHR扩展函数!" );
16 }
17 VkResult result = fpCreateAndroidSurfaceKHR(instance,
18 &createInfo, nullptr, &surface); //创建Android平台用KHR表面
19 assert(result==VK_SUCCESS); //检查是否创建成功
20 VkBool32 *pSupportsPresent = (VkBool32 *)malloc(queueFamilyCount * sizeof(VkBool32));
21 for (uint32_t i = 0; i < queueFamilyCount; i++){ //遍历设备对应的队列家族列表
22 vk::vkGetPhysicalDeviceSurfaceSupportKHR(gpus[0], i, surface, &pSupportsPresent[i]);
23 LOGE("队列家族索引=%d %s显示",i,(pSupportsPresent[i]==1?"支持":"不支持"));
24 }
25 queueGraphicsFamilyIndex = UINT32_MAX; //支持图形工作的队列家族索引
26 queuePresentFamilyIndex = UINT32_MAX; //支持显示(呈现)工作的队列家族索引
27 for (uint32_t i = 0; i <queueFamilyCount; ++i){//遍历设备对应的队列家族列表
28 if ((queueFamilyprops[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0)
//若此队列家族支持图形工作
29 if (queueGraphicsFamilyIndex== UINT32_MAX) queueGraphicsFamilyIndex = i;
30 if (pSupportsPresent[i] == VK_TRUE){ //如果当前队列家族支持显示工作
31 queueGraphicsFamilyIndex = i; //记录此队列家族索引为支持图形工作的
32 queuePresentFamilyIndex = i; //记录此队列家族索引为支持显示工作的
33 LOGE("队列家族索引=%d 同时支持Graphis(图形)和Present(呈现)工作",i);
34 break;
35 }}}
36 if (queuePresentFamilyIndex == UINT32_MAX){//若没有找到同时支持两项工作的队列家族
37 for (size_t i = 0; i < queueFamilyCount; ++i){//遍历设备对应的队列家族列表
38 if (pSupportsPresent[i] == VK_TRUE){ //判断是否支持显示工作
39 queuePresentFamilyIndex= i; //记录此队列家族索引为支持显示工作的
40 break;
41 }}}
42 free(pSupportsPresent); //释放存储是否支持呈现工作的布尔值列表
43 if (queueGraphicsFamilyIndex == UINT32_MAX || queuePresentFamilyIndex == UINT32_MAX){
44 LOGE("没有找到支持Graphis(图形)或Present(呈现或显示)工作的队列家族");
45 assert(false); } //若没有支持图形或显示操作的队列家族则程序终止
46 //此处省略了create_vulkan_swapChain方法中执行其他工作的代码,后面的步骤中进行介绍
47 }
(2)接下来介绍的是确定支持的格式数量、分配对应数量的空间、获取支持的格式信息和支持的格式、获取支持的显示模式数量和显示模式列表、调整空间尺寸以及确定交换链的显示模式等关键步骤对应的代码。具体内容如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 uint32_t formatCount; //支持的格式数量
2 result = vk::vkGetPhysicalDeviceSurfaceFormatsKHR(gpus[0],
3 surface, &formatCount, NULL); //获取支持的格式数量
4 LOGE("支持的格式数量为 %d",formatCount);
5 VkSurfaceFormatKHR *surfFormats = //分配对应数量的空间
6 (VkSurfaceFormatKHR *)malloc(formatCount * sizeof(VkSurfaceFormatKHR));
7 formats.resize(formatCount); //调整对应Vector尺寸
8 result = vk::vkGetPhysicalDeviceSurfaceFormatsKHR(gpus[0],
9 surface, &formatCount, surfFormats); //获取支持的格式信息
10 for(int i=0;i<formatCount;i++){ //记录支持的格式信息
11 formats[i]=surfFormats[i].format;
12 LOGE("[%d]支持的格式为%d",i,formats[i]);
13 }
14 if (formatCount == 1 && surfFormats[0].format //特殊情况处理
15 == VK_FORMAT_UNDEFINED){
16 formats[0] = VK_FORMAT_B8G8R8A8_UNORM;
17 }
18 free(surfFormats); //释放辅助内存
19 result = vk::vkGetPhysicalDeviceSurfaceCapabilitiesKHR(gpus[0],
20 surface, &surfCapabilities); //获取KHR表面的能力
21 assert(result == VK_SUCCESS);
22 result = vk::vkGetPhysicalDeviceSurfacePresentModesKHR(gpus[0],
23 surface, &presentModeCount, NULL); //获取支持的显示模式数量
24 assert(result == VK_SUCCESS);
25 LOGE("显示模式数量为%d",presentModeCount);
26 presentModes.resize(presentModeCount); //调整对应Vector尺寸
27 result = vk::vkGetPhysicalDeviceSurfacePresentModesKHR(gpus[0],
28 surface, &presentModeCount, presentModes.data());//获取支持的显示模式列表
29 for(int i=0;i<presentModeCount;i++){ //遍历打印所有显示模式的信息
30 LOGE("显示模式[%d]编号为%d",i,presentModes[i]);
31 }
32 VkPresentModeKHR swapchainPresentMode =
33 VK_PRESENT_MODE_FIFO_KHR; //确定交换链显示模式
34 for (size_t i = 0; i < presentModeCount; i++){ //遍历显示模式列表
35 if (presentModes[i] == VK_PRESENT_MODE_MAILBOX_KHR){//若支持MAILBOX模式
36 swapchainPresentMode = VK_PRESENT_MODE_MAILBOX_KHR;
37 break;
38 }
39 if ((swapchainPresentMode != VK_PRESENT_MODE_MAILBOX_KHR)
40 &&(presentModes[i] == VK_PRESENT_MODE_IMMEDIATE_KHR)){//若支持IMMEDIATE模式
41 swapchainPresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR;
42 }}
说明
显示模式主要有4种,名称和含义分别为:VK_PRESENT_MODE_IMMEDIATE_KHR表示系统立即响应呈现请求,从不考虑垂直同步问题,也没有内部队列来管理呈现请求,使用此模式比较容易引起画面撕裂;VK_PRESENT_MODE_MAILBOX_KHR表示有内部队列管理呈现请求,仅在垂直同步的恰当时机响应呈现请求,不会引起画面撕裂,队列中请求的处理顺序就像老式的邮筒一样,先处理最上面(最后)的请求,其他前面的在最后一个请求处理后都被回收以备后用;VK_PRESENT_MODE_FIFO_KHR与VK_PRESENT_MODE_MAILBOX_KHR类似,只不过队列中请求的处理顺序变为FIFO(先进先出依次处理);VK_PRESENT_MODE_FIFO_RELAXED_KHR类似于VK_PRESENT_MODE_FIFO_KHR,只不过可能不考虑垂直同步而响应呈现请求,当然也就可能引起画面撕裂。综上可以看出,本案例中倾向于采用的VK_PRESENT_MODE_MAILBOX_KHR选项的原因是保证画面不撕裂情况下效率是最高的。
(3)然后介绍的是用于确定交换链中图像尺寸并进一步构建交换链创建信息结构体实例的相关代码,具体内容如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 if (surfCapabilities.currentExtent.width == 0xFFFFFFFF){//若表面没有确定尺寸
2 swapchainExtent.width = screenWidth; //设置宽度为窗体宽度
3 swapchainExtent.height = screenHeight; //设置高度为窗体高度
4 if (swapchainExtent.width < surfCapabilities.minImageExtent.width){//限制宽度在范围内
5 swapchainExtent.width = surfCapabilities.minImageExtent.width;
6 }else if (swapchainExtent.width > surfCapabilities.maxImageExtent.width){
7 swapchainExtent.width = surfCapabilities.maxImageExtent.width;
8 }if (swapchainExtent.height < surfCapabilities.minImageExtent.height){
//限制高度在范围内
9 swapchainExtent.height = surfCapabilities.minImageExtent.height;
10 }else if (swapchainExtent.height > surfCapabilities.maxImageExtent.height){
11 swapchainExtent.height = surfCapabilities.maxImageExtent.height;
12 }
13 LOGE("使用自己设置的宽度%d高度%d",swapchainExtent.width,swapchainExtent.height);
14 }else{swapchainExtent = surfCapabilities.currentExtent; //若表面有确定尺寸
15 LOGE("使用获取的surface能力中的宽度%d高度%d",
16 swapchainExtent.width,swapchainExtent.height);
17 }
18 screenWidth=swapchainExtent.width; //记录实际采用的宽度
19 screenHeight=swapchainExtent.height; //记录实际采用的高度
20 uint32_t desiredMinNumberOfSwapChainImages =
21 surfCapabilities.minImageCount+1; //期望交换链中的最少图像数量
22 if ((surfCapabilities.maxImageCount > 0) && //将图像数量限制到范围内
23 (desiredMinNumberOfSwapChainImages > surfCapabilities.maxImageCount)){
24 desiredMinNumberOfSwapChainImages = surfCapabilities.maxImageCount;
25 }
26 VkSurfaceTransformFlagBitsKHR preTransform; //KHR表面变换标志
27 if (surfCapabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR){
28 preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;//若支持所需的变换
29 }else{ //若不支持所需的变换
30 preTransform = surfCapabilities.currentTransform;
31 }
32 VkSwapchainCreateInfoKHR swapchain_ci = {}; //构建交换链创建信息结构体实例
33 swapchain_ci.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;//结构体类型
34 swapchain_ci.pNext = NULL; //自定义数据的指针
35 swapchain_ci.surface = surface; //指定KHR表面
36 swapchain_ci.minImageCount = desiredMinNumberOfSwapChainImages;//最少图像数量
37 swapchain_ci.imageFormat = formats[0]; //图像格式
38 swapchain_ci.imageExtent.width = swapchainExtent.width; //交换链图像宽度
39 swapchain_ci.imageExtent.height = swapchainExtent.height;//交换链图像高度
40 swapchain_ci.preTransform = preTransform; //指定变换标志
41 swapchain_ci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;//混合Alpha值
42 swapchain_ci.imageArrayLayers = 1; //图像数组层数
43 swapchain_ci.presentMode = swapchainPresentMode; //交换链的显示模式
44 swapchain_ci.oldSwapchain = VK_NULL_HANDLE; //前导交换链
45 swapchain_ci.clipped = true; //开启剪裁
46 swapchain_ci.imageColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR;//色彩空间
47 swapchain_ci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;//图像用途
48 swapchain_ci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;//图像共享模式
49 swapchain_ci.queueFamilyIndexCount = 0; //队列家族数量
50 swapchain_ci.pQueueFamilyIndices = NULL; //队列家族索引列表
说明
图像的用途对于交换链中的图像而言一般只有VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT一种选择,表示将作为帧缓冲的颜色附件使用。但对于普通图像而言则有多种选择,如:VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT表示作为帧缓冲的深度模板附件使用、VK_IMAGE_USAGE_TRANSFER_SRC_BIT表示作为传输的源图像、VK_IMAGE_USAGE _TRANSFER_DST_BIT表示作为传输的目标图像等。
(4)最后介绍用于创建交换链、获取交换链中的图像数量和图像列表、交换链中的每一幅图像创建对应图像视图等工作的相关代码,具体内容如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 if (queueGraphicsFamilyIndex !
2 = queuePresentFamilyIndex){ //若支持图形和显示工作的队列家族不相同
3 swapchain_ci.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
4 swapchain_ci.queueFamilyIndexCount = 2; //交换链所需的队列家族索引数量为2
5 uint32_t queueFamilyIndices[2] = {queueGraphicsFamilyIndex,queuePresentFamilyIndex};
6 swapchain_ci.pQueueFamilyIndices = queueFamilyIndices;//交换链所需的队列家族索引列表
7 }
8 result = vk::vkCreateSwapchainKHR(device, //创建交换链
9 &swapchain_ci, NULL, &swapChain);
10 assert(result == VK_SUCCESS); //检查交换链是否创建成功
11 result = vk::vkGetSwapchainImagesKHR(device, //获取交换链中的图像数量
12 swapChain, &swapchainImageCount, NULL);
13 assert(result == VK_SUCCESS); //检查是否获取成功
14 LOGE("[SwapChain中的Image数量为%d]",swapchainImageCount);
15 swapchainImages.resize(swapchainImageCount); //调整图像列表尺寸
16 result = vk::vkGetSwapchainImagesKHR(device, //获取交换链中的图像列表
17 swapChain, &swapchainImageCount, swapchainImages.data());
18 assert(result == VK_SUCCESS); //检查是否获取成功
19 swapchainImageViews.resize(swapchainImageCount); //调整图像视图列表尺寸
20 for (uint32_t i = 0; i < swapchainImageCount; i++){//为交换链中的各幅图像创建图像视图
21 VkImageViewCreateInfo color_image_view = {};//构建图像视图创建信息结构体实例
22 color_image_view.sType =
23 VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; //设置结构体类型
24 color_image_view.pNext = NULL; //自定义数据的指针
25 color_image_view.flags = 0; //供将来使用的标志
26 color_image_view.image = swapchainImages[i];//对应交换链图像
27 color_image_view.viewType = VK_IMAGE_VIEW_TYPE_2D; //图像视图的类型
28 color_image_view.format = formats[0]; //图像视图格式
29 color_image_view.components.r = VK_COMPONENT_SWIZZLE_R; //设置R通道调和
30 color_image_view.components.g = VK_COMPONENT_SWIZZLE_G; //设置G通道调和
31 color_image_view.components.b = VK_COMPONENT_SWIZZLE_B; //设置B通道调和
32 color_image_view.components.a = VK_COMPONENT_SWIZZLE_A; //设置A通道调和
33 color_image_view.subresourceRange.aspectMask
34 = VK_IMAGE_ASPECT_COLOR_BIT; //图像视图使用方面
35 color_image_view.subresourceRange.baseMipLevel = 0; //基础Mipmap级别
36 color_image_view.subresourceRange.levelCount = 1; //Mipmap级别的数量
37 color_image_view.subresourceRange.baseArrayLayer = 0;//基础数组层
38 color_image_view.subresourceRange.layerCount = 1; //数组层的数量
39 result = vk::vkCreateImageView(device, //创建图像视图
40 &color_image_view, NULL, &swapchainImageViews[i]);
41 assert(result == VK_SUCCESS); //检查是否创建成功
42 }}
说明
VK_IMAGE_ASPECT_COLOR_BIT表示图像视图将作为颜色附件使用,其对应的图像中的每个像素用于存储颜色值。RGBA的4个色彩通道的调和是Vulkan提供的一种灵活编织输出色彩通道与图像色彩通道对应关系的技术手段,本案例中是让输出RGBA色彩通道依次对应于图像RGBA色彩通道。如果开发人员有特殊需要,也可以编织其他的色彩通道对应关系,例如让输出R(红色)通道对应到图像G(绿色)通道。只不过这样输出的图像颜色就会发生变化,一般情况下这是不期望的效果。
本节介绍的是用于创建深度缓冲的方法——create_vulkan_DepthBuffer。由于该方法代码较多,故将其分成多个部分依次进行详细介绍,具体内容如下。
(1)首先介绍的是用于获取物理设备支持的指定格式的属性、确定图像的瓦片组织方式以及构建深度图像创建信息结构体实例和构建内存分配信息结构体实例等工作的代码,具体内容如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::create_vulkan_DepthBuffer(){ //创建深度缓冲的方法
2 depthFormat = VK_FORMAT_D16_UNORM; //指定深度图像的格式
3 VkImageCreateInfo image_info = {}; //构建深度图像创建信息结构体实例
4 vk::vkGetPhysicalDeviceFormatProperties(gpus[0],//获取物理设备支持的指定格式的属性
5 depthFormat, &depthFormatProps);
6 if (depthFormatProps.linearTilingFeatures & //是否支持线性瓦片组织方式
7 VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT){
8 image_info.tiling = VK_IMAGE_TILING_LINEAR;//采用线性瓦片组织方式
9 LOGE("tiling为VK_IMAGE_TILING_LINEAR!");
10 }else if (depthFormatProps.optimalTilingFeatures//是否支持最优瓦片组织方式
11 & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT){
12 image_info.tiling = VK_IMAGE_TILING_OPTIMAL;//采用最优瓦片组织方式
13 LOGE("tiling为VK_IMAGE_TILING_OPTIMAL!");
14 }else{
15 LOGE("不支持VK_FORMAT_D16_UNORM!"); //打印不支持指定格式的提示信息
16 }
17 image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; //指定结构体类型
18 image_info.pNext = NULL; //自定义数据的指针
19 image_info.imageType = VK_IMAGE_TYPE_2D; //图像类型
20 image_info.format = depthFormat; //图像格式
21 image_info.extent.width = screenWidth; //图像宽度
22 image_info.extent.height =screenHeight; //图像高度
23 image_info.extent.depth = 1; //图像深度
24 image_info.mipLevels = 1; //图像Mipmap级数
25 image_info.arrayLayers = 1; //图像数组层数量
26 image_info.samples = VK_SAMPLE_COUNT_1_BIT; //采样模式
27 image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;//初始布局
28 image_info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;//图像用途
29 image_info.queueFamilyIndexCount = 0; //队列家族数量
30 image_info.pQueueFamilyIndices = NULL; //队列家族索引列表
31 image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; //共享模式
32 image_info.flags = 0; //标志
33 VkMemoryAllocateInfo mem_alloc = {}; //构建内存分配信息结构体实例
34 mem_alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;//结构体类型
35 mem_alloc.pNext = NULL; //自定义数据的指针
36 mem_alloc.allocationSize = 0; //分配的内存字节数
37 mem_alloc.memoryTypeIndex = 0; //内存的类型索引
38 ……//此处省略了create_vulkan_DepthBuffer方法中执行其他工作的代码,后面的步骤中进行介绍
39 }
说明
图像的共享模式(sharingMode)有两个选择:VK_SHARING_MODE_EXCLUSIVE或VK_SHARING_MODE_CONCURRENT。VK_SHARING_MODE_EXCLUSIVE表示图像不允许被多个队列家族的队列访问,VK_SHARING_MODE_CONCURRENT则表示图像允许被多个队列家族的队列访问。若设置图像的共享模式为VK_SHARING_MODE_EXCLUSIVE,则图像对应的队列家族数量应该为0,队列家族索引列表应设置为空并将被系统忽略。
(2)接下来介绍构建图像视图创建信息结构体实例、创建深度图像、获取图像内存需求、分配并绑定内存等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 VkImageViewCreateInfo view_info = {}; //构建深度图像视图创建信息结构体实例
2 view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;//设置结构体类型
3 view_info.pNext = NULL; //自定义数据的指针
4 view_info.image = VK_NULL_HANDLE; //对应的图像
5 view_info.format = depthFormat ; //图像视图的格式
6 view_info.components.r = VK_COMPONENT_SWIZZLE_R; //设置R通道调和
7 view_info.components.g = VK_COMPONENT_SWIZZLE_G; //设置G通道调和
8 view_info.components.b = VK_COMPONENT_SWIZZLE_B; //设置B通道调和
9 view_info.components.a = VK_COMPONENT_SWIZZLE_A; //设置A通道调和
10 view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;//图像视图使用方面
11 view_info.subresourceRange.baseMipLevel = 0; //基础Mipmap级别
12 view_info.subresourceRange.levelCount = 1; //Mipmap级别的数量
13 view_info.subresourceRange.baseArrayLayer = 0; //基础数组层
14 view_info.subresourceRange.layerCount = 1; //数组层的数量
15 view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; //图像视图的类型
16 view_info.flags = 0; //标志
17 VkResult result = vk::vkCreateImage(device, //创建深度图像
18 &image_info, NULL, &depthImage);
19 assert(result == VK_SUCCESS);
20 VkMemoryRequirements mem_reqs; //获取图像内存需求
21 vk::vkGetImageMemoryRequirements(device, depthImage, &mem_reqs);
22 mem_alloc.allocationSize = mem_reqs.size; //获取所需内存字节数
23 VkFlags requirements_mask=0; //需要的内存类型掩码
24 bool flag=memoryTypeFromProperties(memoryroperties, //获取所需内存类型索引
25 mem_reqs.memoryTypeBits,requirements_mask,&mem_alloc.memoryTypeIndex);
26 assert(flag); //检查获取是否成功
27 LOGE("确定内存类型成功 类型索引为%d",mem_alloc.memoryTypeIndex);
28 result = vk::vkAllocateMemory(device, &mem_alloc, NULL, &memDepth);//分配内存
29 assert(result == VK_SUCCESS);
30 result = vk::vkBindImageMemory(device, depthImage, memDepth, 0);//绑定图像和内存
31 assert(result == VK_SUCCESS);
32 view_info.image = depthImage; //指定图像视图对应图像
33 result = vk::vkCreateImageView(device, &view_info, NULL, &depthImageView);
//创建深度图像视图
34 assert(result == VK_SUCCESS);
提示
Vulkan中将特定格式(如VK_FORMAT_D16_UNORM)的图像作为深度缓冲使用,这样的图像可以称之为深度图像。深度图像中的每个像素用于记录对应位置片元的深度值,在管线进行深度测试时使用。关于片元、深度测试的具体内容,在后面介绍管线的部分将进行详细介绍,这里读者简单了解即可。
完成了深度缓冲的创建后,下面就可以创建渲染通道了。渲染通道包含了一次绘制任务需要的多方面信息,案例中对应的创建方法是create_render_pass。由于该方法代码较多,故将其分成多个部分依次进行详细介绍,具体内容如下。
(1)首先介绍的是构建信号量创建信息结构体实例并创建信号量、准备颜色和深度附件描述信息、定义颜色附件和深度附件的引用等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::create_render_pass(){ //创建渲染通道的方法
2 VkSemaphoreCreateInfo imageAcquiredSemaphoreCreateInfo; //构建信号量创建信息结构体实例
3 imageAcquiredSemaphoreCreateInfo.sType
4 = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; //结构体类型
5 imageAcquiredSemaphoreCreateInfo.pNext = NULL; //自定义数据的指针
6 imageAcquiredSemaphoreCreateInfo.flags = 0; //供将来使用的标志
7 VkResult result = vk::vkCreateSemaphore(device, //创建信号量
8 &imageAcquiredSemaphoreCreateInfo, NULL, &imageAcquiredSemaphore);
9 assert(result == VK_SUCCESS); //检测信号量是否创建成功
10 VkAttachmentDescription attachments[2]; //附件描述信息数组
11 attachments[0].format = formats[0]; //设置颜色附件的格式
12 attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; //设置采样模式
13 attachments[0].loadOp = //子渲染通道开始时的操作(针对颜色附件)
14 VK_ATTACHMENT_LOAD_OP_CLEAR;
15 attachments[0].storeOp= //子渲染通道结束时的操作(针对颜色附件)
16 VK_ATTACHMENT_STORE_OP_STORE;
17 attachments[0].stencilLoadOp = //子渲染通道开始时的操作(针对模板附件)
18 VK_ATTACHMENT_LOAD_OP_DONT_CARE;
19 attachments[0].stencilStoreOp = //子渲染通道结束时的操作(针对模板附件)
20 VK_ATTACHMENT_STORE_OP_DONT_CARE;
21 attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; //开始时的布局
22 attachments[0].finalLayout = //结束时的最终布局
23 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
24 attachments[0].flags = 0; //设置位掩码
25 attachments[1].format = depthFormat; //设置深度附件的格式
26 attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; //设置采样模式
27 attachments[1].loadOp = //子渲染通道开始时的操作(针对深度附件)
28 VK_ATTACHMENT_LOAD_OP_CLEAR;
29 attachments[1].storeOp = //子渲染通道结束时的操作(针对深度附件)
30 VK_ATTACHMENT_STORE_OP_DONT_CARE;
31 attachments[1].stencilLoadOp = //子渲染通道开始时的操作(针对模板附件)
32 VK_ATTACHMENT_LOAD_OP_DONT_CARE;
33 attachments[1].stencilStoreOp = //子渲染通道结束时的操作(针对模板附件)
34 VK_ATTACHMENT_STORE_OP_DONT_CARE;
35 attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; //开始时的布局
36 attachments[1].finalLayout = //结束时的布局
37 VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
38 attachments[1].flags = 0; //设置位掩码
39 VkAttachmentReference color_reference = {}; //颜色附件引用
40 color_reference.attachment = 0; //对应附件描述信息数组下标
41 color_reference.layout = //设置附件布局
42 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
43 VkAttachmentReference depth_reference = {}; //深度附件引用
44 depth_reference.attachment = 1; //对应附件描述信息数组下标
45 depth_reference.layout = //设置附件布局
46 VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
47 //此处省略了create_render_pass方法中执行其他工作的代码,后面的步骤中进行介绍
48 }
说明
第21行设置颜色附件的最终布局为VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,这是为了最后将画面进行呈现,若不需要呈现则一般不会采用此布局。另外带“_OP_CLEAR”后缀的操作表示清除,带“_DONT_CARE”后缀的表示不关心具体操作。本案例中没有使用模板测试,因此两个附件模板相关的操作都是带“_DONT_CARE”后缀的。模板测试相对比较复杂,后面会有专门的章节进行介绍,这里简单了解即可。
(2)接着介绍的是构建渲染子通道描述信息结构体实例、构建渲染通道创建信息结构体实例、创建渲染通道、设定清除帧缓冲颜色深度和模板各分量等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 VkSubpassDescription subpass = {}; //构建渲染子通道描述结构体实例
2 subpass.pipelineBindPoint =
3 VK_PIPELINE_BIND_POINT_GRAPHICS; //设置管线绑定点
4 subpass.flags = 0; //设置掩码
5 subpass.inputAttachmentCount = 0; //输入附件数量
6 subpass.pInputAttachments = NULL; //输入附件列表
7 subpass.colorAttachmentCount = 1; //颜色附件数量
8 subpass.pColorAttachments = &color_reference;//颜色附件列表
9 subpass.pResolveAttachments = NULL; //Resolve附件
10 subpass.pDepthStencilAttachment = &depth_reference;//深度模板附件
11 subpass.preserveAttachmentCount = 0; //preserve附件数量
12 subpass.pPreserveAttachments = NULL; //pPreserve附件列表
13 VkRenderPassCreateInfo rp_info = {}; //构建渲染通道创建信息结构体实例
14 rp_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;//结构体类型
15 rp_info.pNext = NULL; //自定义数据的指针
16 rp_info.attachmentCount = 2; //附件的数量
17 rp_info.pAttachments = attachments; //附件列表
18 rp_info.subpassCount = 1; //渲染子通道数量
19 rp_info.pSubpasses = &subpass; //渲染子通道列表
20 rp_info.dependencyCount = 0; //子通道依赖数量
21 rp_info.pDependencies = NULL; //子通道依赖列表
22 result = vk::vkCreateRenderPass(device, &rp_info, NULL, &renderPass);//创建渲染通道
23 assert(result == VK_SUCCESS); //检查是否创建成功
24 clear_values[0].color.float32[0] = 0.2f; //帧缓冲清除用R分量值
25 clear_values[0].color.float32[1] = 0.2f; //帧缓冲清除用G分量值
26 clear_values[0].color.float32[2] = 0.2f; //帧缓冲清除用B分量值
27 clear_values[0].color.float32[3] = 0.2f; //帧缓冲清除用A分量值
28 clear_values[1].depthStencil.depth = 1.0f; //帧缓冲清除用深度值
29 clear_values[1].depthStencil.stencil = 0; //帧缓冲清除用模板值
30 rp_begin.sType =
31 VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;//渲染通道启动信息结构体类型
32 rp_begin.pNext = NULL; //自定义数据的指针
33 rp_begin.renderPass = renderPass; //指定要启动的渲染通道
34 rp_begin.renderArea.offset.x = 0; //渲染区域起始x坐标
35 rp_begin.renderArea.offset.y = 0; //渲染区域起始y坐标
36 rp_begin.renderArea.extent.width = screenWidth;//渲染区域宽度
37 rp_begin.renderArea.extent.height = screenHeight;//渲染区域高度
38 rp_begin.clearValueCount = 2; //帧缓冲清除值数量
39 rp_begin.pClearValues = clear_values; //帧缓冲清除值数组
提示
本节案例比较简单,渲染通道中仅仅包含了一个渲染子通道。在复杂的情况下(例如实施延迟渲染),一个渲染通道可能包含一系列渲染子通道,这些渲染子通道之间还有特定的依赖关系。这一点本书后面会有专门的章节进行介绍,这里简单了解即可。
接下来介绍创建帧缓冲的方法——create_frame_buffer,其中包含创建帧缓冲附件数组、构建帧缓冲创建信息结构体实例、动态分配帧缓冲所需内存以及为交换链中的所有图像创建对应帧缓冲等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::create_frame_buffer(){ //创建帧缓冲的方法
2 VkImageView attachments[2]; //附件图像视图数组
3 attachments[1]=depthImageView; //给定深度图像视图
4 VkFramebufferCreateInfo fb_info = {}; //构建帧缓冲创建信息结构体实例
5 fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;//结构体类型
6 fb_info.pNext = NULL; //自定义数据的指针
7 fb_info.renderPass = renderPass; //指定渲染通道
8 fb_info.attachmentCount = 2; //附件数量
9 fb_info.pAttachments = attachments; //附件图像视图数组
10 fb_info.width = screenWidth; //宽度
11 fb_info.height = screenHeight; //高度
12 fb_info.layers = 1; //层数
13 uint32_t i; //循环控制变量
14 framebuffers = (VkFramebuffer *)malloc( //为帧缓冲序列动态分配内存
15 swapchainImageCount * sizeof(VkFramebuffer));
16 assert(framebuffers); //检查内存分配是否成功
17 for (i = 0; i < swapchainImageCount; i++){ //遍历交换链中的各个图像
18 attachments[0] = swapchainImageViews[i];//给定颜色附件对应图像视图
19 VkResult result = vk::vkCreateFramebuffer(device, //创建帧缓冲
20 &fb_info, NULL, &framebuffers[i]);
21 assert(result == VK_SUCCESS); //检查是否创建成功
22 LOGE("[创建帧缓冲%d成功!]",i);
23 }}
接下来介绍创建绘制用物体对象的方法——createDrawableObject,该方法包含了两大步骤。首先生成绘制用3色三角形的顶点坐标数据和颜色数据,然后基于生成的数据创建绘制用3色三角形物体对象,具体内容如下。
(1)首先介绍的是createDrawableObject方法本身,其代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::createDrawableObject(){ //创建绘制用物体的方法
2 TriangleData::genVertexData(); //生成3色三角形顶点数据和颜色数据
3 triForDraw=new DrawableObjectCommonLight(TriangleData::vdata,//创建绘制用3色三角形对象
4 TriangleData::dataByteCount,TriangleData::vCount,device,memoryroperties);
5 }
提示
将绘制用物体的相关代码独立到其他类中分开来写并不是Vulkan本身的要求,这是为了方便开发与维护。可以想象,复杂场景中同时会有很多不同物体,如果这些物体的代码都写到一起会大大增加代码的复杂度和维护成本。因此,各个物体独立开来是非常好的选择,本书中的所有案例都将采用类似的思路。
(2)介绍TriangleData类时,首先需要了解其基本结构。该类中声明了数据数组的首地址指针、所占字节数量、顶点数量和生成3色三角形数据的genVertexData方法,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的TriangleData.h。
1 class TriangleData{
2 public:
3 static float* vdata; //数据数组首地址指针
4 static int dataByteCount; //数据所占总字节数量
5 static int vCount; //顶点数量
6 static void genVertexData(); //生成数据的方法
7 };
(3)接着介绍TriangleData类的实现代码,具体内容如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的TriangleData.cpp。
1 //此处省略了相关头文件的导入,感兴趣的读者自行查看随书源代码
2 float* TriangleData::vdata; //数据数组首地址指针
3 int TriangleData::dataByteCount; //数据所占总字节数量
4 int TriangleData::vCount; //顶点数量
5 void TriangleData::genVertexData(){ //顶点数据生成方法
6 vCount = 3; //顶点数量
7 dataByteCount=vCount*6* sizeof(float); //数据所占内存总字节数
8 vdata=new float[vCount*6]{ //数据数组
9 0,75,0, 1,0,0, //每一行前3个是顶点坐标
10 -45,0,0, 0,1,0, //每一行后3个是颜色RGB值
11 45,0,0, 0,0,1
12 };}
说明
从上述代码中可以看出3色三角形的顶点坐标和颜色数据是存储在同一数组中的,3个顶点的颜色分别是红、绿、蓝。这里再多说一点,如果读者有兴趣可以修改数据生成方法的代码,改动顶点数量与顶点坐标、颜色数据等,可以方便地得到立方体、四棱锥等简单几何体。
(4)了解了3色三角形顶点相关数据的生成后,接下来介绍的是与3色三角形绘制工作相关的DrawableObjectCommonLight类。首先给出此类的声明,其中声明了顶点的数据缓冲、顶点数据所需的设备内存、绘制方法等,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/util目录下的DrawableObjectCommon.h。
1 //此处省略了相关头文件的导入,感兴趣的读者自行查看随书源代码
2 class DrawableObjectCommonLight{
3 public:
4 VkDevice* devicePointer; //指向逻辑设备的指针
5 float* vdata; //顶点数据数组首地址指针
6 int vCount; //顶点数量
7 VkBuffer vertexDatabuf; //顶点数据缓冲
8 VkDeviceMemory vertexDataMem; //顶点数据所需设备内存
9 VkDescriptorBufferInfo vertexDataBufferInfo; //顶点数据缓冲描述信息
10 DrawableObjectCommonLight(float* vdataIn,int dataByteCount,int vCountIn,//构造函数
11 VkDevice& device,VkPhysicalDeviceMemoryProperties& memoryroperties);
12 ~DrawableObjectCommonLight(); //析构函数
13 void drawSelf(VkCommandBuffer& secondary_cmd, //绘制方法
14 VkPipelineLayout& pipelineLayout,VkPipeline& pipeline,VkDescriptorSet* desSetPointer);
15 };
(5)了解了DrawableObjectCommonLight类的头文件后,下面将介绍此类的具体实现代码。由于此类的实现代码较长,故分为两部分进行介绍。首先介绍的是此类的构造函数与析构函数,代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/util目录下的DrawableObjectCommon.cpp。
1 //此处省略了相关头文件的导入,感兴趣的读者自行查看随书源代码
2 DrawableObjectCommonLight::DrawableObjectCommonLight(float* vdataIn,int dataByteCount,
3 int vCountIn,VkDevice& device,VkPhysicalDeviceMemoryProperties& memoryroperties){
4 this->devicePointer=&device; //接收逻辑设备指针并保存
5 this->vdata=vdataIn; //接收顶点数据数组首地址指针并保存
6 this->vCount=vCountIn; //接收顶点数量并保存
7 VkBufferCreateInfo buf_info = {}; //构建缓冲创建信息结构体实例
8 buf_info.sType =
9 VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;//设置结构体类型
10 buf_info.pNext = NULL; //自定义数据的指针
11 buf_info.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;//缓冲的用途为顶点数据
12 buf_info.size = dataByteCount; //设置数据总字节数
13 buf_info.queueFamilyIndexCount = 0; //队列家族数量
14 buf_info.pQueueFamilyIndices = NULL; //队列家族索引列表
15 buf_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; //共享模式
16 buf_info.flags = 0; //标志
17 VkResult result = vk::vkCreateBuffer(device,
18 &buf_info, NULL, &vertexDatabuf); //创建缓冲
19 assert(result == VK_SUCCESS); //检查缓冲创建是否成功
20 VkMemoryRequirements mem_reqs; //缓冲内存需求
21 vk::vkGetBufferMemoryRequirements(device, vertexDatabuf, &mem_reqs);//获取缓冲内存需求
22 assert(dataByteCount<=mem_reqs.size); //检查内存需求获取是否正确
23 VkMemoryAllocateInfo alloc_info = {}; //构建内存分配信息结构体实例
24 alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;//结构体类型
25 alloc_info.pNext = NULL; //自定义数据的指针
26 alloc_info.memoryTypeIndex = 0; //内存类型索引
27 alloc_info.allocationSize = mem_reqs.size;//内存总字节数
28 VkFlags requirements_mask=VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
29 | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; //需要的内存类型掩码
30 bool flag=memoryTypeFromProperties(memoryroperties, //获取所需内存类型索引
31 mem_reqs.memoryTypeBits,requirements_mask,&alloc_info.memoryTypeIndex);
32 if(flag){
33 LOGE("确定内存类型成功,类型索引为%d",alloc_info.memoryTypeIndex);
34 }else{
35 LOGE("确定内存类型失败!");
36 }
37 result = vk::vkAllocateMemory(device, //为顶点数据缓冲分配内存
38 &alloc_info, NULL, &vertexDataMem);
39 assert(result == VK_SUCCESS); //检查内存分配是否成功
40 uint8_t *pData; //CPU访问时的辅助指针
41 result = vk::vkMapMemory(device, vertexDataMem,//将设备内存映射为CPU可访问
42 0, mem_reqs.size, 0, (void **)&pData);
43 assert(result == VK_SUCCESS); //检查映射是否成功
44 memcpy(pData, vdata, dataByteCount); //将顶点数据复制进设备内存
45 vk::vkUnmapMemory(device, vertexDataMem); //解除内存映射
46 result = vk::vkBindBufferMemory(device,
47 vertexDatabuf, vertexDataMem, 0); //绑定内存与缓冲
48 assert(result == VK_SUCCESS);
49 vertexDataBufferInfo.buffer = vertexDatabuf; //指定数据缓冲
50 vertexDataBufferInfo.offset = 0; //数据缓冲起始偏移量
51 vertexDataBufferInfo.range = mem_reqs.size; //数据缓冲所占字节数
52 }
53 DrawableObjectCommonLight::~DrawableObjectCommonLight(){ //析构函数
54 delete vdata; //释放指针内存
55 vk::vkDestroyBuffer(*devicePointer, vertexDatabuf, NULL);//销毁顶点缓冲
56 vk::vkFreeMemory(*devicePointer, vertexDataMem, NULL); //释放设备内存
57 }
说明
缓冲的共享模式(sharingMode)有两种选择,VK_SHARING_MODE_EXCLUSIVE或VK_SHARING_MODE_CONCURRENT。VK_SHARING_MODE_EXCLUSIVE表示缓冲不允许被多个队列家族的队列访问,VK_SHARING_MODE_CONCURRENT则表示缓冲允许被多个队列家族的队列访问。若设置缓冲的共享模式为VK_SHARING_MODE_EXCLUSIVE,则缓冲对应的队列家族数量应该为0,队列家族索引列表应设置为空并将被系统忽略。
(6)接下来介绍物体的绘制方法——drawSelf,其将命令缓冲与管线、管线布局、描述集、顶点数据进行绑定并执行绘制,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/util目录下的DrawableObjectCommon.cpp。
1 void DrawableObjectCommonLight::drawSelf(VkCommandBuffer& cmd, //绘制的方法
2 VkPipelineLayout& pipelineLayout,VkPipeline& pipeline,VkDescriptorSet* desSetPointer){
3 vk::vkCmdBindPipeline(cmd, //将当前使用的命令缓冲与指定管线绑定
4 VK_PIPELINE_BIND_POINT_GRAPHICS,pipeline);
5 vk::vkCmdBindDescriptorSets(cmd, //将命令缓冲、管线布局、描述集绑定
6 VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1,desSetPointer, 0, NULL);
7 const VkDeviceSize offsetsVertex[1] = {0}; //顶点数据偏移量数组
8 vk::vkCmdBindVertexBuffers( //将顶点数据与当前使用的命令缓冲绑定
9 cmd, //当前使用的命令缓冲
10 0, //顶点数据缓冲在列表中的首索引
11 1, //绑定顶点缓冲的数量
12 &(vertexDatabuf), //绑定的顶点数据缓冲列表
13 offsetsVertex //各个顶点数据缓冲的内部偏移量
14 );
15 vk::vkCmdDraw(cmd, vCount, 1, 0, 0); //执行绘制
16 }
接下来介绍的是初始化渲染管线的方法——initPipeline,此方法代码很简单,只是在其中创建了封装渲染管线相关的ShaderQueueSuit_Common类对象,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::initPipeline(){ //初始化渲染管线的方法
2 sqsCL=new ShaderQueueSuit_Common(&device, //创建封装了渲染管线相关的对象
3 renderPass,memoryroperties);
4 }
上述代码中用到了ShaderQueueSuit_Common类,这是作者开发的用于封装渲染管线相关的工具类。这样设计是为了未来在开发包含多种不同渲染管线程序时的方便,下面对ShaderQueueSuit_Common类进行详细的介绍,具体内容如下。
(1)首先介绍的是ShaderQueueSuit_Common类的基本结构,其中声明了所需的成员变量以及相关的功能方法,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.h。
1 //此处省略了相关头文件的导入,感兴趣的读者自行查看随书源代码
2 class ShaderQueueSuit_Common{
3 private:
4 VkBuffer uniformBuf; //一致变量缓冲
5 VkDescriptorBufferInfo uniformBufferInfo; //一致变量缓冲描述信息
6 int NUM_DESCRIPTOR_SETS; //描述集数量
7 std::vector<VkDescriptorSetLayout> descLayouts; //描述集布局列表
8 VkPipelineShaderStageCreateInfo shaderStages[2];//着色器阶段数组
9 VkVertexInputBindingDescription vertexBinding; //管线的顶点输入数据绑定描述
10 VkVertexInputAttributeDescription vertexAttribs[2]; //管线的顶点输入属性描述
11 VkPipelineCache pipelineCache; //管线缓冲
12 VkDevice* devicePointer; //逻辑设备指针
13 VkDescriptorPool descPool; //描述池
14 void create_uniform_buffer(VkDevice& device,
15 VkPhysicalDeviceMemoryProperties& memoryroperties);//创建一致变量缓冲
16 void destroy_uniform_buffer(VkDevice& device); //销毁一致变量缓冲
17 void create_pipeline_layout(VkDevice& device); //创建管线布局
18 void destroy_pipeline_layout(VkDevice& device); //销毁管线布局
19 void init_descriptor_set(VkDevice& device); //初始化描述集
20 void create_shader(VkDevice& device); //创建着色器
21 void destroy_shader(VkDevice& device); //销毁着色器
22 void initVertexAttributeInfo(); //初始化顶点输入属性信息
23 void create_pipe_line(VkDevice& device,VkRenderPass& renderPass); //创建管线
24 void destroy_pipe_line(VkDevice& device); //销毁管线
25 public:
26 int bufferByteCount; //一致缓冲总字节数
27 VkDeviceMemory memUniformBuf; //一致变量缓冲内存
28 VkWriteDescriptorSet writes[1]; //一致变量写入描述集实例数组
29 std::vector<VkDescriptorSet> descSet; //描述集列表
30 VkPipelineLayout pipelineLayout; //管线布局
31 VkPipeline pipeline; //管线
32 ShaderQueueSuit_Common(VkDevice* deviceIn,VkRenderPass& //构造函数
33 renderPass,VkPhysicalDeviceMemoryProperties& memoryroperties);
34 ~ShaderQueueSuit_Common(); //析构函数
35 };
说明
上述头文件中定义了很多的成员变量,涉及不少Vulkan提供的基本类型。如果读者还没有掌握Vulkan基本类型的相关知识,请参考本书1.3.2节中的表1-1进行学习。
(2)对ShaderQueueSuit_Common类头文件有了一定的了解后,下面开始介绍ShaderQueueSuit_Common类的实现代码。首先介绍的是ShaderQueueSuit_Common类的构造函数,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 ShaderQueueSuit_Common::ShaderQueueSuit_Common(VkDevice* deviceIn,
2 VkRenderPass& renderPass,VkPhysicalDeviceMemoryProperties& memoryroperties){
3 this->devicePointer=deviceIn;
4 create_uniform_buffer(*devicePointer,memoryroperties); //创建一致变量缓冲
5 create_pipeline_layout(*devicePointer); //创建管线布局
6 init_descriptor_set(*devicePointer); //初始化描述集
7 create_shader(*devicePointer); //创建着色器
8 initVertexAttributeInfo(); //初始化顶点属性信息
9 create_pipe_line(*devicePointer,renderPass); //创建管线
10 }
说明
从上述代码中可以看出,此类的构造函数很简单,依次调用了初始化管线时所需的各个功能方法,这些功能方法将在下面进行具体的介绍。
(3)首先介绍的是用于创建一致变量缓冲的方法create_uniform_buffer,该方法中包含构建一致变量缓冲创建信息结构体实例、构建内存分配信息结构体实例、确定需要的内存类型掩码并获取所需内存类型索引等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 void ShaderQueueSuit_Common::create_uniform_buffer(VkDevice& device,
//创建一致变量缓冲的方法
2 VkPhysicalDeviceMemoryProperties& memoryroperties){
3 bufferByteCount=sizeof(float)*16; //一致变量缓冲的总字节数
4 VkBufferCreateInfo buf_info = {}; //构建一致变量缓冲创建信息结构体实例
5 buf_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; //结构体的类型
6 buf_info.pNext = NULL; //自定义数据的指针
7 buf_info.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;//缓冲的用途
8 buf_info.size = bufferByteCount; //缓冲总字节数
9 buf_info.queueFamilyIndexCount = 0; //队列家族数量
10 buf_info.pQueueFamilyIndices = NULL; //队列家族索引列表
11 buf_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; //共享模式
12 buf_info.flags = 0; //标志
13 VkResult result = vk::vkCreateBuffer(device, &buf_info, NULL, &uniformBuf);
//创建一致变量缓冲
14 assert(result == VK_SUCCESS); //检查创建是否成功
15 VkMemoryRequirements mem_reqs; //内存需求变量
16 vk::vkGetBufferMemoryRequirements(device, uniformBuf, &mem_reqs);
//获取此缓冲的内存需求
17 VkMemoryAllocateInfo alloc_info = {}; //构建内存分配信息结构体实例
18 alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;//结构体类型
19 alloc_info.pNext = NULL; //自定义数据的指针
20 alloc_info.memoryTypeIndex = 0; //内存类型索引
21 alloc_info.allocationSize = mem_reqs.size; //缓冲内存分配字节数
22 VkFlags requirements_mask=VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
23 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; //需要的内存类型掩码
24 bool flag=memoryTypeFromProperties(memoryroperties, //获取所需内存类型索引
25 mem_reqs.memoryTypeBits,requirements_mask, &alloc_info.memoryTypeIndex);
26 if(flag){LOGE("确定内存类型成功 类型索引为%d",alloc_info.memoryTypeIndex);}
27 else{LOGE("确定内存类型失败!");}
28 result = vk::vkAllocateMemory(device, //分配内存
29 &alloc_info, NULL, &memUniformBuf);
30 assert(result == VK_SUCCESS); //检查内存分配是否成功
31 result = vk::vkBindBufferMemory(device, //将内存和对应缓冲绑定
32 uniformBuf, memUniformBuf, 0);
33 assert(result == VK_SUCCESS); //检查绑定操作是否成功
34 uniformBufferInfo.buffer = uniformBuf; //指定一致变量缓冲
35 uniformBufferInfo.offset = 0; //起始偏移量
36 uniformBufferInfo.range = bufferByteCount; //一致变量缓冲总字节数
37 }
提示
管线这部分与着色器配套的相关代码较多,这里读者可以先学习一下,待后文介绍了着色器之后再对照学习一遍,理解应该就没有问题了。后面步骤中有关着色器的代码也请读者采用这样的方式进行学习。
(4)接着介绍的是创建管线布局的方法create_pipeline_layout,其中包括构建描述集布局绑定信息结构体实例、构建描述集布局创建信息结构体实例和构建管线布局创建信息结构体实例等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 void ShaderQueueSuit_Common::create_pipeline_layout(VkDevice& device){
//创建管线布局的方法
2 NUM_DESCRIPTOR_SETS=1; //设置描述集数量
3 VkDescriptorSetLayoutBinding layout_bindings[1]; //描述集布局绑定数组
4 layout_bindings[0].binding = 0; //此绑定的绑定点编号
5 layout_bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;//描述类型
6 layout_bindings[0].descriptorCount = 1 //描述数量
7 layout_bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;//目标着色器阶段
8 layout_bindings[0].pImmutableSamplers = NULL;
9 VkDescriptorSetLayoutCreateInfo descriptor_layout = {};//构建描述集布局创建信息
结构体实例
10 descriptor_layout.sType =
11 VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;//结构体类型
12 descriptor_layout.pNext = NULL; //自定义数据的指针
13 descriptor_layout.bindingCount = 1; //描述集布局绑定的数量
14 descriptor_layout.pBindings = layout_bindings; //描述集布局绑定数组
15 descLayouts.resize(NUM_DESCRIPTOR_SETS); //调整描述集布局列表尺寸
16 VkResult result = vk::vkCreateDescriptorSetLayout(device,
17 &descriptor_layout, NULL, descLayouts.data()); //创建描述集布局
18 assert(result == VK_SUCCESS); //检查描述集布局创建是否成功
19 VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = {};//构建管线布局创建
信息结构体实例
20 pPipelineLayoutCreateInfo.sType =
21 VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; //结构体类型
22 pPipelineLayoutCreateInfo.pNext = NULL; //自定义数据的指针
23 pPipelineLayoutCreateInfo.pushConstantRangeCount = 0;//推送常量范围的数量
24 pPipelineLayoutCreateInfo.pPushConstantRanges = NULL;//推送常量范围的列表
25 pPipelineLayoutCreateInfo.setLayoutCount = NUM_DESCRIPTOR_SETS;//描述集布局的数量
26 pPipelineLayoutCreateInfo.pSetLayouts = descLayouts.data();//描述集布局列表
27 result = vk::vkCreatePipelineLayout(device,
28 &pPipelineLayoutCreateInfo, NULL, &pipelineLayout); //创建管线布局
29 assert(result == VK_SUCCESS); //检查创建是否成功
30 }
提示
本节案例比较简单,没有使用到推送常量,因此推送常量范围的数量为0。本书后面有多个不同位置物体的案例中基本都使用了推送常量,到那时再对推送常量进行详细介绍。另外从前面的几处相关代码中可以看出,管线布局主要是管理相关的各个描述集,而描述集负责将所需的一致数据、纹理等资源与管线关联,以备特定着色器进行访问。每个描述集布局关联多个描述集布局绑定,每个描述集布局绑定关联到某个着色器阶段着色器中的某项一致数据或纹理采样器等。Vulkan图形应用程序中描述集布局绑定与着色器中接收的一致变量情况应当是匹配的,这一点读者可以对照后面着色器的相关代码进行学习。
(5)完成了管线布局的创建后,下面介绍的是初始化描述集的方法init_descriptor_set。该方法中包括构建描述集分配信息结构体实例、创建描述集池、分配描述集、完善一致变量写入描述集实例数组等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 void ShaderQueueSuit_Common::init_descriptor_set(VkDevice& device){//初始化描述集的方法
2 VkDescriptorPoolSize type_count[1]; //描述集池尺寸实例数组
3 type_count[0].type = //描述类型
4 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
5 type_count[0].descriptorCount = 1; //描述数量
6 VkDescriptorPoolCreateInfo descriptor_pool = {};//构建描述集池创建信息结构体实例
7 descriptor_pool.sType =
8 VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; //结构体类型
9 descriptor_pool.pNext = NULL; //自定义数据的指针
10 descriptor_pool.maxSets = 1; //描述集最大数量
11 descriptor_pool.poolSizeCount = 1; //描述集池尺寸实例数量
12 descriptor_pool.pPoolSizes = type_count; //描述集池尺寸实例数组
13 VkResult result = vk::vkCreateDescriptorPool(device, //创建描述集池
14 &descriptor_pool, NULL, &descPool);
15 assert(result == VK_SUCCESS); //检查描述集池创建是否成功
16 std::vector<VkDescriptorSetLayout> layouts; //描述集布局列表
17 layouts.push_back(descLayouts[0]); //向列表中添加指定描述集布局
18 VkDescriptorSetAllocateInfo alloc_info[1]; //构建描述集分配信息结构体实例数组
19 alloc_info[0].sType = //结构体类型
20 VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
21 alloc_info[0].pNext = NULL; //自定义数据的指针
22 alloc_info[0].descriptorPool = descPool; //指定描述集池
23 alloc_info[0].descriptorSetCount = 1; //描述集数量
24 alloc_info[0].pSetLayouts = layouts.data(); //描述集布局列表
25 descSet.resize(1); //调整描述集列表尺寸
26 result = vk::vkAllocateDescriptorSets(device, //分配描述集
27 alloc_info, descSet.data());
28 assert(result == VK_SUCCESS); //检查描述集分配是否成功
29 writes[0] = {};//完善一致变量写入描述集实例数组元素0
30 writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;//结构体类型
31 writes[0].pNext = NULL; //自定义数据的指针
32 writes[0].descriptorCount = 1; //描述数量
33 writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;//描述类型
34 writes[0].pBufferInfo = &uniformBufferInfo; //对应一致变量缓冲的信息
35 writes[0].dstArrayElement = 0; //目标数组起始元素
36 writes[0].dstBinding = 0;//目标绑定编号(与着色器中绑定编号对应)
37 }
(6)完成了描述集的初始化后,下面介绍的是创建着色器的方法——create_shader,该方法包括准备两种着色器阶段信息、将两种着色器脚本编译成SPV格式、创建两种着色器模块等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 void ShaderQueueSuit_Common::create_shader(VkDevice& device){ //创建着色器的方法
2 std::string vertStr= FileUtil::
3 loadAssetStr("shader/commonTexLight.vert");//加载顶点着色器脚本
4 std::string fragStr= FileUtil::
5 loadAssetStr("shader/commonTexLight.frag");//加载片元着色器脚本
6 shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
7 shaderStages[0].pNext = NULL; //自定义数据的指针
8 shaderStages[0].pSpecializationInfo = NULL; //特殊信息
9 shaderStages[0].flags = 0; //供将来使用的标志
10 shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; //着色器阶段为顶点
11 shaderStages[0].pName = "main"; //入口函数为main
12 std::vector<unsigned int> vtx_spv; //将顶点着色器脚本编译为SPV
13 bool retVal = GLSLtoSPV(VK_SHADER_STAGE_VERTEX_BIT, vertStr.c_str(), vtx_spv);
14 assert(retVal); //检查编译是否成功
15 LOGE("顶点着色器脚本编译SPV成功!");
16 VkShaderModuleCreateInfo moduleCreateInfo; //准备顶点着色器模块创建信息
17 moduleCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
18 moduleCreateInfo.pNext = NULL; //自定义数据的指针
19 moduleCreateInfo.flags = 0; //供将来使用的标志
20 moduleCreateInfo.codeSize = vtx_spv.size() * sizeof(unsigned int);
//顶点着色器SPV数据总字节数
21 moduleCreateInfo.pCode = vtx_spv.data(); //顶点着色器SPV数据
22 VkResult result = vk::vkCreateShaderModule(device, //创建顶点着色器模块
23 &moduleCreateInfo, NULL, &shaderStages[0].module);
24 assert(result == VK_SUCCESS); //检查顶点着色器模块创建是否成功
25 shaderStages[1].sType =
26 VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;//结构体类型
27 shaderStages[1].pNext = NULL; //自定义数据的指针
28 shaderStages[1].pSpecializationInfo = NULL; //特殊信息
29 shaderStages[1].flags = 0; //供将来使用的标志
30 shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;//着色器阶段为片元
31 shaderStages[1].pName = "main"; //入口函数为main
32 std::vector<unsigned int> frag_spv;
33 retVal = GLSLtoSPV(VK_SHADER_STAGE_FRAGMENT_BIT, //将片元着色器脚本编译为SPV
34 fragStr.c_str(), frag_spv);
35 assert(retVal); //检查编译是否成功
36 LOGE("片元着色器脚本编译SPV成功!");
37 moduleCreateInfo.sType = //准备片元着色器模块创建信息
38 VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; //设置结构体类型
39 moduleCreateInfo.pNext = NULL; //自定义数据的指针
40 moduleCreateInfo.flags = 0; //供将来使用的标志
41 moduleCreateInfo.codeSize = frag_spv.size() * sizeof(unsigned int);
//片元着色器SPV数据总字节数
42 moduleCreateInfo.pCode = frag_spv.data(); //片元着色器SPV数据
43 result = vk::vkCreateShaderModule(device, //创建片元着色器模块
44 &moduleCreateInfo, NULL, &shaderStages[1].module);
45 assert(result == VK_SUCCESS); //检查片元着色器模块创建是否成功
46 }
(7)接下来介绍的是设置顶点着色器输入属性信息的initVertexAttributeInfo方法,其中包括设置数据输入的频率、每组数据的跨度字节数等,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 void ShaderQueueSuit_Common::initVertexAttributeInfo(){ //设置顶点着色器输入属性信息
2 vertexBinding.binding = 0; //对应绑定点
3 vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; //数据输入频率为每顶点
4 vertexBinding.stride = sizeof(float)*6; //每组数据的跨度字节数
5 vertexAttribs[0].binding = 0; //第1个顶点输入属性的绑定点
6 vertexAttribs[0].location = 0; //第1个顶点输入属性的位置索引
7 vertexAttribs[0].format = VK_FORMAT_R32G32B32_SFLOAT;//第1个顶点输入属性的数据格式
8 vertexAttribs[0].offset = 0; //第1个顶点输入属性的偏移量
9 vertexAttribs[1].binding = 0; //第2个顶点输入属性的绑定点
10 vertexAttribs[1].location = 1; //第2个顶点输入属性的位置索引
11 vertexAttribs[1].format = VK_FORMAT_R32G32B32_SFLOAT;//第2个顶点输入属性的数据格式
12 vertexAttribs[1].offset = 12; //第2个顶点输入属性的偏移量
13 }
提示
本案例中顶点着色器(可以与后面顶点着色器的代码进行对照)有两个输入参数,第1个为顶点位置坐标(包含x、y、z分量),第2个为顶点RGB颜色值。因此每组顶点数据包含6个float分量(占“sizeof(float)*6”个字节)。由于有两个输入参数,故上述代码中共设置了两个顶点输入属性描述结构体实例的几项属性值。要特别注意的是偏移量以字节计,而第1个顶点输入属性包含3个float分量,每个float分量4个字节,因此第2个顶点输入属性的起始偏移量为12。
(8)完成了顶点着色器输入属性信息的设置后,就应该介绍用于创建管线的方法create_pipe_line了。由于该方法代码较多,故将其分成多部分详细介绍。首先介绍的是设置管线动态状态信息、设置管线顶点数据输入阶段信息、设置管线图元组装阶段信息和设置管线光栅化阶段信息等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 void ShaderQueueSuit_Common::create_pipe_line(VkDevice& device,VkRenderPass& renderPass){
2 VkDynamicState dynamicStateEnables[VK_DYNAMIC_STATE_RANGE_SIZE];//动态状态启用标志
3 memset(dynamicStateEnables, 0, sizeof dynamicStateEnables);//设置所有标志为false
4 VkPipelineDynamicStateCreateInfo dynamicState = {}; //管线动态状态创建信息
5 dynamicState.sType =
6 VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;//结构体类型
7 dynamicState.pNext = NULL; //自定义数据的指针
8 dynamicState.pDynamicStates = dynamicStateEnables;//动态状态启用标志数组
9 dynamicState.dynamicStateCount = 0; //启用的动态状态项数量
10 VkPipelineVertexInputStateCreateInfo vi; //管线顶点数据输入状态创建信息
11 vi.sType =
12 VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
13 vi.pNext = NULL; //自定义数据的指针
14 vi.flags = 0; //供将来使用的标志
15 vi.vertexBindingDescriptionCount = 1; //顶点输入绑定描述数量
16 vi.pVertexBindingDescriptions = &vertexBinding; //顶点输入绑定描述列表
17 vi.vertexAttributeDescriptionCount = 2; //顶点输入属性描述数量
18 vi.pVertexAttributeDescriptions =vertexAttribs; //顶点输入属性描述列表
19 VkPipelineInputAssemblyStateCreateInfo ia; //管线图元组装状态创建信息
20 ia.sType =
21 VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
22 ia.pNext = NULL; //自定义数据的指针
23 ia.flags = 0; //供将来使用的标志
24 ia.primitiveRestartEnable = VK_FALSE; //关闭图元重启
25 ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;//采用三角形图元列表模式
26 VkPipelineRasterizationStateCreateInfo rs; //管线光栅化状态创建信息
27 rs.sType =
28 VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
29 rs.pNext = NULL; //自定义数据的指针
30 rs.flags = 0; //供将来使用的标志
31 rs.polygonMode = VK_POLYGON_MODE_FILL; //绘制方式为填充
32 rs.cullMode = VK_CULL_MODE_NONE; //不使用背面剪裁
33 rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; //卷绕方向为逆时针
34 rs.depthClampEnable = VK_TRUE; //深度截取
35 rs.rasterizerDiscardEnable = VK_FALSE;//启用光栅化操作(若为TRUE则光栅化不产生任何片元)
36 rs.depthBiasEnable = VK_FALSE; //不启用深度偏移
37 rs.depthBiasConstantFactor = 0; //深度偏移常量因子
38 rs.depthBiasClamp = 0; //深度偏移值上下限(若为正作为上限,为负作为下限)
39 rs.depthBiasSlopeFactor = 0; //深度偏移斜率因子
40 rs.lineWidth = 1.0f; //线宽度(仅在线绘制模式起作用)
41 VkPipelineColorBlendAttachmentState att_state[1];//管线颜色混合附件状态数组
42 att_state[0].colorWriteMask = 0xf; //设置写入掩码
43 att_state[0].blendEnable = VK_FALSE; //关闭混合
44 att_state[0].alphaBlendOp = VK_BLEND_OP_ADD; //设置Alpha通道混合方式
45 att_state[0].colorBlendOp = VK_BLEND_OP_ADD; //设置RGB通道混合方式
46 att_state[0].srcColorBlendFactor = VK_BLEND_FACTOR_ZERO;//设置源颜色混合因子
47 att_state[0].dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;//设置目标颜色混合因子
48 att_state[0].srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;//设置源Alpha混合因子
49 att_state[0].dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;//设置目标Alpha混合因子
50 //此处省略了一些完成其他工作的代码,后面步骤中详细介绍
51 }
说明
上述代码中涉及了很多读者可能不熟悉的概念,诸如剪裁窗口、视口、图元重启、图元组装、光栅化、背面剪裁、卷绕、深度偏移、混合方式、源混合因子、目标混合因子等。这些概念在后面的章节中会单独进行详细地介绍,这里读者简单了解一下即可。
(9)接着介绍的是构建管线颜色混合状态创建信息结构体实例、设置管线视口信息和剪裁信息、构建管线视口状态创建信息结构体实例、构建管线深度及模板状态创建信息结构体实例的相关代码,具体内容如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 VkPipelineColorBlendStateCreateInfo cb; //管线的颜色混合状态创建信息
2 cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
3 cb.pNext = NULL; //自定义数据的指针
4 cb.flags = 0; //供未来使用的标志
5 cb.attachmentCount = 1; //颜色混合附件数量
6 cb.pAttachments = att_state; //颜色混合附件列表
7 cb.logicOpEnable = VK_FALSE; //不启用逻辑操作
8 cb.logicOp = VK_LOGIC_OP_NO_OP; //逻辑操作类型为无
9 cb.blendConstants[0] = 1.0f; //混合常量R分量
10 cb.blendConstants[1] = 1.0f; //混合常量G分量
11 cb.blendConstants[2] = 1.0f; //混合常量B分量
12 cb.blendConstants[3] = 1.0f; //混合常量A分量
13 VkViewport viewports; //视口信息
14 viewports.minDepth = 0.0f; //视口最小深度
15 viewports.maxDepth = 1.0f; //视口最大深度
16 viewports.x = 0; //视口<em>x</em>坐标
17 viewports.y = 0; //视口<em>y</em>坐标
18 viewports.width = MyVulkanManager::screenWidth; //视口宽度
19 viewports.height = MyVulkanManager::screenHeight; //视口高度
20 VkRect2D scissor; //剪裁窗口信息
21 scissor.extent.width = MyVulkanManager::screenWidth;//剪裁窗口的宽度
22 scissor.extent.height = MyVulkanManager::screenHeight;//剪裁窗口的高度
23 scissor.offset.x = 0; //剪裁窗口的<em>x</em>坐标
24 scissor.offset.y = 0; //剪裁窗口的<em>y</em>坐标
25 VkPipelineViewportStateCreateInfo vp = {}; //管线视口状态创建信息
26 vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
27 vp.pNext = NULL; //自定义数据的指针
28 vp.flags = 0; //供将来使用的标志
29 vp.viewportCount = 1; //视口的数量
30 vp.scissorCount = 1; //剪裁窗口的数量
31 vp.pScissors = &scissor; //剪裁窗口信息列表
32 vp.pViewports = &viewports; //视口信息列表
33 VkPipelineDepthStencilStateCreateInfo ds; //管线深度及模板状态创建信息
34 ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
35 ds.pNext = NULL; //自定义数据的指针
36 ds.flags = 0; //供将来使用的标志
37 ds.depthTestEnable = VK_TRUE; //开启深度测试
38 ds.depthWriteEnable = VK_TRUE; //开启深度值写入
39 ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; //深度检测比较操作
40 ds.depthBoundsTestEnable = VK_FALSE; //关闭深度边界测试
41 ds.minDepthBounds = 0; //最小深度边界
42 ds.maxDepthBounds = 0; //最大深度边界
43 ds.stencilTestEnable = VK_FALSE; //关闭模板测试
44 ds.back.failOp = VK_STENCIL_OP_KEEP; //未通过模板测试时的操作
45 ds.back.passOp = VK_STENCIL_OP_KEEP; //模板测试、深度测试都通过时的操作
46 ds.back.compareOp = VK_COMPARE_OP_ALWAYS; //模板测试的比较操作
47 ds.back.compareMask = 0; //模板测试比较掩码
48 ds.back.reference = 0; //模板测试参考值
49 ds.back.depthFailOp = VK_STENCIL_OP_KEEP; //未通过深度测试时的操作
50 ds.back.writeMask = 0; //写入掩码
51 ds.front = ds.back;
说明
VK_COMPARE_OP_LESS_OR_EQUAL表示深度测试在小于等于原有值的情况下通过,这是一种常规的选择。关于深度测试的具体内容会在后面章节进行详细的介绍,同时模板测试的详细内容也是如此,读者这里可以先放一放这些比较深入的细节问题。
(10)下面介绍的是构建管线多重采样状态的创建信息结构体实例、构建图形管线的创建信息结构体实例、创建管线缓冲、创建管线的相关代码,具体内容如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 VkPipelineMultisampleStateCreateInfo ms; //管线多重采样状态创建信息
2 ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
3 ms.pNext = NULL; //自定义数据的指针
4 ms.flags = 0; //供将来使用的标志位
5 ms.pSampleMask = NULL; //采样掩码
6 ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; //光栅化阶段采样数量
7 ms.sampleShadingEnable = VK_FALSE; //关闭采样着色
8 ms.alphaToCoverageEnable = VK_FALSE; //不启用alphaToCoverage
9 ms.alphaToOneEnable = VK_FALSE; //不启用alphaToOne
10 ms.minSampleShading = 0.0; //最小采样着色
11 VkGraphicsPipelineCreateInfo pipelineInfo; //图形管线创建信息
12 pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
13 pipelineInfo.pNext = NULL; //自定义数据的指针
14 pipelineInfo.layout = pipelineLayout; //指定管线布局
15 pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;//基管线句柄
16 pipelineInfo.basePipelineIndex = 0; //基管线索引
17 pipelineInfo.flags = 0; //标志
18 pipelineInfo.pVertexInputState = &vi; //管线的顶点数据输入状态信息
19 pipelineInfo.pInputAssemblyState = &ia; //管线的图元组装状态信息
20 pipelineInfo.pRasterizationState = &rs; //管线的光栅化状态信息
21 pipelineInfo.pColorBlendState = &cb; //管线的颜色混合状态信息
22 pipelineInfo.pTessellationState = NULL; //管线的曲面细分状态信息
23 pipelineInfo.pMultisampleState = &ms; //管线的多重采样状态信息
24 pipelineInfo.pDynamicState = &dynamicState; //管线的动态状态信息
25 pipelineInfo.pViewportState = &vp; //管线的视口状态信息
26 pipelineInfo.pDepthStencilState = &ds; //管线的深度模板测试状态信息
27 pipelineInfo.stageCount = 2; //管线的着色阶段数量
28 pipelineInfo.pStages = shaderStages; //管线的着色阶段列表
29 pipelineInfo.renderPass = renderPass; //指定的渲染通道
30 pipelineInfo.subpass = 0; //设置管线执行对应的渲染子通道
31 VkPipelineCacheCreateInfo pipelineCacheInfo; //管线缓冲创建信息
32 pipelineCacheInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
33 pipelineCacheInfo.pNext = NULL; //自定义数据的指针
34 pipelineCacheInfo.initialDataSize = 0; //初始数据尺寸
35 pipelineCacheInfo.pInitialData = NULL; //初始数据内容,此处为NULL
36 pipelineCacheInfo.flags = 0; //供将来使用的标志位
37 VkResult result = vk::vkCreatePipelineCache(device, &pipelineCacheInfo, NULL, &pipelineCache);
38 assert(result == VK_SUCCESS); //检查管线缓冲创建是否成功
39 result = vk::vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineInfo, NULL, &pipeline);
40 assert(result == VK_SUCCESS); //检查管线创建是否成功
(11)最后介绍的是ShaderQueueSuit_Common类的析构函数和销毁管线相关实例的几个方法,主要包括销毁管线的方法、销毁着色器模块的方法、销毁管线布局的方法、销毁一致变量缓冲的方法等,具体内容如下。
代码位置:见随书中源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 ShaderQueueSuit_Common::~ShaderQueueSuit_Common(){ //析构函数
2 destroy_pipe_line(*devicePointer); //销毁管线
3 destroy_shader(*devicePointer); //销毁着色器模块
4 destroy_pipeline_layout(*devicePointer); //销毁管线布局
5 destroy_uniform_buffer(*devicePointer); //销毁一致变量缓冲
6 }
7 void ShaderQueueSuit_Common::destroy_pipe_line(VkDevice& device){//销毁管线的方法
8 vk::vkDestroyPipeline(device, pipeline, NULL); //销毁管线
9 vk::vkDestroyPipelineCache(device, pipelineCache, NULL); //销毁管线缓冲
10 }
11 void ShaderQueueSuit_Common::destroy_shader(VkDevice& device){//销毁着色器模块的方法
12 vk::vkDestroyShaderModule(device,shaderStages[0].module,NULL);//销毁顶点着色器模块
13 vk::vkDestroyShaderModule(device,shaderStages[1].module,NULL);//销毁片元着色器模块
14 }
15 void ShaderQueueSuit_Common::destroy_pipeline_layout(VkDevice& device){//销毁管线布局的方法
16 for (int i=0;i<NUM_DESCRIPTOR_SETS;i++){ //遍历描述集列表
17 vk::vkDestroyDescriptorSetLayout(device,descLayouts[i],NULL);//销毁对应描述集布局
18 }
19 vk::vkDestroyPipelineLayout(device,pipelineLayout,NULL); //销毁管线布局
20 }
21 void ShaderQueueSuit_Common::destroy_uniform_buffer(VkDevice& device){//销毁一致变量缓冲相关
22 vk::vkDestroyBuffer(device, uniformBuf, NULL); //销毁一致变量缓冲
23 vk::vkFreeMemory(device, memUniformBuf, NULL); //释放一致变量缓冲对应设备内存
24 }
说明
从上述代码中可以看出销毁和释放各种相关实例的方法很简单,往往是调用一个Vulkan API方法即可完成。这里希望提醒读者的是,开发中应当在使用完毕后适时销毁不需要的实例,养成良好的开发习惯。
了解了管线的初始化之后,下面介绍的是创建栅栏和初始化呈现的相关方法。其中创建栅栏的方法是createFence,初始化呈现信息的方法是initPresentInfo,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::createFence(){ //创建用于等待指定任务执行完毕的栅栏
2 VkFenceCreateInfo fenceInfo; //栅栏创建信息结构体实例
3 fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;//结构体类型
4 fenceInfo.pNext = NULL; //自定义数据的指针
5 fenceInfo.flags = 0; //栅栏的初始状态标志
6 vk::vkCreateFence(device, &fenceInfo, NULL, &taskFinishFence); //创建栅栏
7 }
8 void MyVulkanManager::initPresentInfo(){ //初始化呈现信息
9 present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;//结构体类型
10 present.pNext = NULL; //自定义数据的指针
11 present.swapchainCount = 1; //交换链的数量
12 present.pSwapchains = &swapChain; //交换链列表
13 present.waitSemaphoreCount = 0; //等待的信号量数量
14 present.pWaitSemaphores = NULL; //等待的信号量列表
15 present.pResults = NULL; //呈现操作结果标志列表
16 }
接下来介绍的是初始化基本变换矩阵、摄像机矩阵和投影矩阵的initMatrix方法,该方法包含初始化摄像机位置、初始化基本变换矩阵和设置投影参数等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::initMatrix(){
2 MatrixState3D::setCamera(0,0,200,0,0,0,0,1,0); //初始化摄像机
3 MatrixState3D::setInitStack(); //初始化基本变换矩阵
4 float ratio=(float)screenWidth/(float)screenHeight; //求屏幕长宽比
5 MatrixState3D::setProjectFrustum(-ratio,ratio,-1,1,1.5f,1000);//设置投影参数
6 }
说明
按照屏幕长宽比设置投影参数是为了保证绘制的画面不变形,关于摄像机、基本变换(平移、旋转、缩放)、投影参数等内容将会在后面的章节中详细介绍,这里读者简单了解即可。
完成了几种矩阵的初始化之后,接着介绍的是执行绘制的方法——drawObject,其中建立了渲染循环以持续绘制各帧画面,具体代码如下。
(1)首先介绍的是drawObject绘制方法本身,其包括初始化FPS计算、计算FPS、绘制3色三角形、结束渲染通道、结束命令缓冲等关键步骤,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::drawObject(){
2 FPSUtil::init(); //初始化FPS计算
3 while(MyVulkanManager::loopDrawFlag){ //每循环一次绘制一帧画面
4 FPSUtil::calFPS(); //计算FPS
5 FPSUtil::before(); //一帧开始
6 VkResult result = vk::vkAcquireNextImageKHR(device, swapChain,
//获取交换链中的当前帧索引
7 UINT64_MAX, imageAcquiredSemaphore, VK_NULL_HANDLE,¤tBuffer);
8 rp_begin.framebuffer = framebuffers[currentBuffer];//为渲染通道设置当前帧缓冲
9 vk::vkResetCommandBuffer(cmdBuffer, 0); //恢复命令缓冲到初始状态
10 result = vk::vkBeginCommandBuffer(cmdBuffer, &cmd_buf_info);//启动命令缓冲
11 MyVulkanManager::flushUniformBuffer(); //将当前帧相关数据送入一致变量缓冲
12 MyVulkanManager::flushTexToDesSet(); //更新绘制用描述集
13 vk::vkCmdBeginRenderPass(cmdBuffer, &rp_begin, VK_SUBPASS_CONTENTS_INLINE);
14 triForDraw->drawSelf(cmdBuffer,sqsCL->pipelineLayout, //绘制三色三角形
15 sqsCL->pipeline,&(sqsCL->descSet[0]));
16 vk::vkCmdEndRenderPass(cmdBuffer); //结束渲染通道
17 result = vk::vkEndCommandBuffer(cmdBuffer);//结束命令缓冲
18 submit_info[0].waitSemaphoreCount = 1; //等待的信号量数量
19 submit_info[0].pWaitSemaphores = &imageAcquiredSemaphore;//等待的信号量列表
20 result = vk::vkQueueSubmit(queueGraphics, 1, submit_info, taskFinishFence);
//提交命令缓冲
21 do{ //等待渲染完毕
22 result = vk::vkWaitForFences(device, 1, &taskFinishFence, VK_TRUE, FENCE_TIMEOUT);
23 }
24 while (result == VK_TIMEOUT);
25 vk::vkResetFences(device,1,&taskFinishFence);//重置栅栏
26 present.pImageIndices = ¤tBuffer; //指定此次呈现的交换链图像索引
27 result = vk::vkQueuePresentKHR(queueGraphics, &present);//执行呈现
28 FPSUtil::after(60); //限制FPS不超过指定的值
29 }}
提示
上述代码中多处使用了FPSUtil类的相关方法,此类是笔者开发的用于帮助计算FPS(帧速率)、限制FPS最大值的工具类。此类与Vulkan并没有必然联系,这里不再赘述,有兴趣的读者可以参考随书源代码。另外,对于手机应用而言过高的FPS对用户体验的提升有限,但急剧增加耗电,因此手机应用往往会采用限制FPS最大值的策略来平衡用户体验和续航需求。
(2)接下来介绍的是步骤(1)中第11行调用的将当前帧相关数据送入一致变量缓冲的方法——flushUniformBuffer,该方法中包括更改3色三角形的旋转角度、将最终变换矩阵数据送进渲染管线等关键步骤,具体代码如下。
代码位置:见随书中源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::flushUniformBuffer(){ //将当前帧的一致数据送入一致变量缓冲的方法
2 xAngle=xAngle+1.0f; //改变3色三角形的旋转角
3 if(xAngle>=360){xAngle=0;} //限制3色三角形旋转角范围
4 MatrixState3D::pushMatrix(); //保护现场
5 MatrixState3D::rotate(xAngle,1,0,0); //旋转变换
6 float* vertexUniformData=MatrixState3D::getFinalMatrix();//获取最终变换矩阵
7 MatrixState3D::popMatrix(); //恢复现场
8 uint8_t *pData; //CPU访问设备内存时的辅助指针
9 VkResult result = vk::vkMapMemory(device, sqsCL->memUniformBuf,
//将设备内存映射为CPU可访问
10 0, sqsCL->bufferByteCount, 0, (void **)&pData);
11 assert(result==VK_SUCCESS); //检查映射是否成功
12 memcpy(pData, vertexUniformData, sqsCL->bufferByteCount);//将最终矩阵数据复制进设备内存
13 vk::vkUnmapMemory(device,sqsCL->memUniformBuf); //解除内存映射
14 }
(3)最后介绍的是步骤(1)中第12行调用的更新绘制用描述集的方法——flushTexToDesSet,该方法通过使用vkUpdateDescriptorSets方法进行描述集的更新,具体代码如下。
1 void MyVulkanManager::flushTexToDesSet(){ //更新绘制用描述集的方法
2 sqsCL->writes[0].dstSet = sqsCL->descSet[0]; //更新描述集对应的写入属性
3 vk::vkUpdateDescriptorSets(device, 1, sqsCL->writes, 0, NULL);//更新描述集
4 }
提示
从上述代码中可以看出,通过调用vkUpdateDescriptorSets方法可以将描述集与对应的资源进行关联。本案例比较简单,描述集相关的资源也只有一个一致变量缓冲。因此上述更新绘制用描述集的方法中代码很少,当程序中的相关资源增加后,此方法的代码也会相应增加,读者可以在学习到后面的章节时再进行比对。
前面的多个步骤中创建了很多不同类型的Vulkan对象,当程序执行完毕时这些对象应该被恰当的销毁或释放。本节将介绍销毁、释放上述对象的相关方法,具体代码如下。
代码位置:见随书源代码/第1章/Sample1_1/src/main/cpp/bndevp目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::destroyPipeline(){ //销毁管线
2 delete sqsCL;
3 }
4 void MyVulkanManager::destroyDrawableObject(){ //销毁绘制用物体
5 delete triForDraw;
6 }
7 void MyVulkanManager::destroy_frame_buffer(){ //销毁帧缓冲
8 for (int i = 0; i < swapchainImageCount; i++){//循环销毁交换链中各个图像对应的帧缓冲
9 vk::vkDestroyFramebuffer(device, framebuffers[i], NULL);
10 }
11 free(framebuffers);
12 LOGE("销毁帧缓冲成功!");
13 }
14 void MyVulkanManager::destroy_render_pass(){ //销毁渲染通道相关
15 vk::vkDestroyRenderPass(device, renderPass, NULL);
16 vk::vkDestroySemaphore(device, imageAcquiredSemaphore, NULL);
17 }
18 void MyVulkanManager::destroy_vulkan_DepthBuffer(){ //销毁深度缓冲相关
19 vk::vkDestroyImageView(device, depthImageView, NULL);
20 vk::vkDestroyImage(device, depthImage, NULL);
21 vk::vkFreeMemory(device, memDepth, NULL);
22 LOGE("销毁深度缓冲相关成功!");
23 }
24 void MyVulkanManager::destroy_vulkan_swapChain(){ //销毁交换链相关
25 for (uint32_t i = 0; i < swapchainImageCount; i++){
26 vk::vkDestroyImageView(device, swapchainImageViews[i], NULL);
27 LOGE("[销毁SwapChain ImageView %d 成功]",i);
28 }
29 vk::vkDestroySwapchainKHR(device, swapChain, NULL);
30 LOGE("销毁SwapChain成功!");
31 }
32 void MyVulkanManager::destroy_vulkan_CommandBuffer(){ //销毁命令缓冲
33 VkCommandBuffer cmdBufferArray[1] = {cmdBuffer}; //创建要释放的命令缓冲数组
34 vk::vkFreeCommandBuffers( //释放命令缓冲
35 device, //所属逻辑设备
36 cmdPool, //所属命令池
37 1, //要销毁的命令缓冲数量
38 cmdBufferArray //要销毁的命令缓冲数组
39 );
40 vk::vkDestroyCommandPool(device, cmdPool, NULL); //销毁命令池
41 }
42 void MyVulkanManager::destroy_vulkan_devices(){ //销毁逻辑设备
43 vk::vkDestroyDevice(device, NULL);
44 LOGE("逻辑设备销毁完毕!");
45 }
46 void MyVulkanManager::destroy_vulkan_instance(){ //销毁Vulkan实例
47 vk::vkDestroyInstance(instance, NULL);
48 LOGE("Vulkan实例销毁完毕!");
49 }
说明
从上述代码中可以看出销毁和释放各种Vulkan对象的操作比创建对应对象简单很多,往往是调用一个方法即可完成。不过初学者很容易忘记进行这些销毁的操作,可能会带来程序的潜在问题,请读者多加注意。
前面已经提到过,Vulkan的绘制相关任务一般是由自定义的线程来执行的。下面就介绍一下启动自定义线程执行Vulkan绘制相关任务的doVulkan方法,其也在MyVulkanManager类中,具体代码如下。
1 void MyVulkanManager::doVulkan(){
2 ThreadTask* tt=new ThreadTask(); //创建执行Vulkan绘制相关任务的对象
3 thread t1(&ThreadTask::doTask,tt); //创建线程执行任务方法doTask
4 t1.detach(); //将子线程与主线程分离
5 }
从上述代码中可以看出,doVulkan方法是通过让自定义线程执行ThreadTask类对象的doTask方法来完成Vulkan绘制相关任务的。下面就详细介绍一下ThreadTask类,具体内容如下。
(1)首先给出的是ThreadTask类的头文件,具体代码如下。
1 class ThreadTask{
2 public:
3 ThreadTask(); //构造函数
4 ~ThreadTask(); //析构函数
5 void doTask(); //执行Vulkan绘制相关任务的方法
6 };
说明
上述代码并不复杂,主要是声明了ThreadTask类的构造函数、析构函数以及执行Vulkan绘制相关任务的doTask方法。
(2)接着介绍的是ThreadTask类的实现代码,主要是doTask方法的实现。doTask方法依次调用了本节前面多个小节介绍的用于完成Vulkan绘制各项相关任务的方法,具体代码如下。
1 void ThreadTask::doTask(){
2 MyVulkanManager::init_vulkan_instance(); //创建Vulkan实例
3 MyVulkanManager::enumerate_vulkan_phy_devices(); //获取物理设备列表
4 MyVulkanManager::create_vulkan_devices(); //创建逻辑设备
5 MyVulkanManager::create_vulkan_CommandBuffer(); //创建命令缓冲
6 MyVulkanManager::init_queue(); //获取支持图形工作的队列
7 MyVulkanManager::create_vulkan_swapChain(); //初始化交换链
8 MyVulkanManager::create_vulkan_DepthBuffer(); //创建深度缓冲
9 MyVulkanManager::create_render_pass(); //创建渲染通道
10 MyVulkanManager::create_frame_buffer(); //创建帧缓冲
11 MyVulkanManager::createDrawableObject(); //创建绘制用物体
12 MyVulkanManager::initPipeline(); //初始化渲染管线
13 MyVulkanManager::createFence(); //创建栅栏
14 MyVulkanManager::initPresentInfo(); //初始化呈现信息
15 MyVulkanManager::initMatrix(); //初始化矩阵
16 MyVulkanManager::drawObject(); //执行绘制
17 MyVulkanManager::destroyFence(); //销毁栅栏
18 MyVulkanManager::destroyPipeline(); //销毁管线
19 MyVulkanManager::destroyDrawableObject(); //销毁绘制用物体
20 MyVulkanManager::destroy_frame_buffer(); //销毁帧缓冲
21 MyVulkanManager::destroy_render_pass(); //销毁渲染通道相关
22 MyVulkanManager::destroy_vulkan_DepthBuffer(); //销毁深度缓冲相关
23 MyVulkanManager::destroy_vulkan_swapChain(); //销毁交换链相关
24 MyVulkanManager::destroy_vulkan_CommandBuffer(); //销毁命令缓冲
25 MyVulkanManager::destroy_vulkan_devices(); //销毁逻辑设备
26 MyVulkanManager::destroy_vulkan_instance(); //销毁Vulkan 实例
27 }
说明
上述代码可以说是一般Vulkan图形应用程序的“故事情节概要”了,其中依次调用了前面介绍的各个功能方法,按照必要的顺序完成了Vulkan图形应用程序的各项操作。另外,开辟独立线程执行绘制任务的部分本书只是给出了一种参考实现。这部分解决方案并不唯一,读者也可以开发自定义的其他实现。
前面多处对代码的讲解都提到了着色器,下面来简单介绍一下本案例中用到的两种着色器,这两种着色器是顶点着色器和片元着色器,具体内容如下。
提示
1.1.1节介绍过,Vulkan没有指定官方的着色器编程语言,而是采用SPIR-V二进制中间格式来进行表示。但对于开发人员来说,不大可能直接使用SPIR-V进行开发,一般都需要基于某种着色器编程语言开发着色器然后再编译为SPIR-V格式。本书案例中的着色器都选用了GLSL着色器编程语言进行开发,这对于熟悉或了解一些OpenGL的开发人员来说应该是最好的选择了。
(1)首先介绍的是顶点着色器,其每顶点执行一次。本节案例的顶点着色器主要包括计算顶点的最终绘制位置以及将顶点颜色传递给片元着色器,具体代码如下。
代码位置:见随书中源代码/第1章/Sample1_1/src/main/assets/shader目录下的commonTexLight.vert。
1 #version 400 //着色器版本号
2 #extension GL_ARB_separate_shader_objects : enable//开启GL_ARB_separate_shader_objects
3 #extension GL_ARB_shading_language_420pack : enable//开启GL_ARB_shading_language_420pack
4 layout (std140,set = 0, binding = 0) uniform bufferVals { //一致块
5 mat4 mvp; //总变换矩阵
6 } myBufferVals;
7 layout (location = 0) in vec3 pos; //传入的物体坐标系顶点坐标
8 layout (location = 1) in vec3 color; //传入的顶点颜色
9 layout (location = 0) out vec3 vcolor; //传到片元着色器的顶点颜色
10 out gl_PerVertex { //输出接口块
11 vec4 gl_Position; //顶点最终位置
12 };
13 void main() { //主函数
14 gl_Position = myBufferVals.mvp * vec4(pos,1.0); //计算顶点最终位置
15 vcolor=color; //传递顶点颜色给片元着色器
16 }
提示
这里读者可能会感到疑惑,为什么第14行在顶点变换时表示一个点的位置需要4个分量?这涉及齐次坐标的使用,后面会有专门的章节进行介绍。另外,上述顶点着色器代码读者学习后可能还是比较糊涂。不用担心,这是正常的!着色器的开发本身学习曲线就比较陡峭,不像有些类型程序的代码比较容易直接理解,后面笔者会带领读者逐步学习,慢慢读者就能够顺利掌握了。
(2)接下来介绍的是片元着色器,片元着色器每片元执行一次。本节案例中的片元着色器仅仅是将顶点着色器传递过来的颜色值输出,传递给渲染管线的后继阶段进行处理,具体代码如下。
代码位置:见随书中源代码/第1章/Sample1_1/src/main/assets/shader目录下的commonTexLight.frag。
1 #version 400 //着色器版本号
2 #extension GL_ARB_separate_shader_objects : enable//开启GL_ARB_separate_shader_objects
3 #extension GL_ARB_shading_language_420pack : enable//开启GL_ARB_shading_language_420pack
4 layout (location = 0) in vec3 vcolor; //顶点着色器传入的顶点颜色数据
5 layout (location = 0) out vec4 outColor; //输出到渲染管线的片元颜色值
6 void main() {
7 outColor=vec4(vcolor.rgb,1.0); //将顶点着色器传递过来的颜色值输出
8 }
提示
第7行最后一个分量代表4个色彩通道(RGBA)中的A通道,宿主语言(C++)在传输数据时没有传入A通道值(可以对照1.3.15节中步骤3下TriangleData类的相关代码),而管线需要的片元颜色输出值是RGBA的第4个通道,因此这里需要自己增加一个1.0作为A通道值。
前面介绍初始化渲染管线的1.3.16节中讲解了管线布局、描述集布局、描述集等相关知识,当时提到这些都是与管线对应的着色器密切相关的,这里再进行一下梳理,具体内容如下。
提示
上述梳理出的几点请读者将本节的着色器与前面相关内容进行对照学习,以便加深理解,建立这些知识点之间必要的逻辑联系。
前面几节介绍了Vulkan图形应用程序的基本架构,同时给出了一个3色三角形案例的代码。到目前为止读者可能还是不太清楚虚拟3D世界中的立体物体是如何搭建出来的。其实这与现实世界搭建建筑物并没有本质区别,请读者考察图1-58和图1-59中国家大剧院远景和近景的照片。
▲图1-58 国家大剧院远景
▲图1-59 国家大剧院近景
从两幅照片中可以对比出,现实世界的某些建筑物远看是平滑的曲面,其实近看是由一个一个的小平面组成的。3D虚拟世界中也是如此,任何立体物体都是由多个小平面搭建而成的。这些小平面切分得越小,越细致,搭建出来的物体就越平滑。
当然Vulkan的虚拟世界与现实世界还是有区别的,现实世界中可以用任意形状的多边形来搭建建筑物,例如图1-58中的国家大剧院就是用四边形搭建的,而Vulkan中仅仅允许采用三角形来搭建物体。其实这从构造能力上来说并没有区别,因为任何多边形都可以拆分为多个三角形,只需开发时稍微注意一下即可。
提示
Vulkan中之所以仅仅支持三角形而不支持任意多边形是出于性能的考虑,就目前移动设备的硬件性能情况来看,这是必然的选择了。
图1-60更加具体地说明了在Vulkan中如何采用三角形来构建立体物体。
▲图1-60 用三角形搭建立体物体
说明
从图1-60中可以看出用三角形可以搭建出任意形状的立体物体,这里仅仅是给出了几个简单的例子,本书的后继章节中还有很多其他形状的立体物体。
了解了Vulkan中立体物体的搭建方式后,下面就需要了解Vulkan中的坐标系统了。Vulkan中采用的是三维笛卡尔坐标系,具体情况如图1-61所示。
▲图1-61 Vulkan中的坐标系
从图1-61中可以看出,Vulkan中采用的是左手标架坐标系。一般来说,初始情况下y轴平行于屏幕的竖边,x轴平行于屏幕的横边,z轴垂直于屏幕平面。
提示
空间解析几何中有两种坐标系标架,左手标架和右手标架。本书非讨论空间解析几何的专门书籍,关于标架的问题不予详述,需要的读者可以参考空间解析几何的相关书籍或资料。另外,了解了立体物体搭建的基本原理后,有兴趣的读者可以根据自己的理解修改1.3.15节中介绍的TriangleData类中顶点数据相关的代码,得到其他的几何形状,比如立方体。
通过本章的学习,读者应该体会到Vulkan的学习门槛相对较高,上手有一定困难,一个简单的旋转三角形场景就用去了1500行左右的代码。但不要灰心,随着后面章节的不断深入,读者应该会慢慢体会到Vulkan的强大。
第1章介绍了Vulkan的基本知识,同时详细介绍了3色三角形案例各部分的开发。本章将进一步介绍Vulkan的渲染管线、着色器外编译以及调试Layer的使用,这些知识与技术都是开发良好Vulkan图形应用程序所必知必会的。
经过第1章3色三角形案例的学习后,读者应该对Vulkan图形应用程序的基本架构有了一定的了解。但此时应该还有一个疑问:绘制命令送入设备队列执行后,Vulkan是如何将原始的物体顶点坐标数据、顶点颜色数据最终转化为屏幕中画面的?这就需要再进一步学习Vulkan渲染管线方面的知识了。
Vulkan的渲染管线可以看作一条流水线,物体绘制相关数据进入管线后在多个阶段中被依次处理。每一阶段执行某种处理,并输出下一阶段需要的内容。在管线的末端,一开始输入的数据已经被转换为携带画面中每个像素颜色数据的片元,最后被呈现到屏幕。图2-1给出了Vulkan完整的渲染管线结构。
从图2-1中可以看出,Vulkan的渲染管线包含了很多处理步骤和涉及的支撑对象。图中左右两侧各有一个应用程序入口,左侧进入的是3色三角形案例中用到的图形渲染管线,右侧进入的是利用GPU进行高性能通用计算时用到的计算管线。
计算管线用途特殊,本书后面会有专门的章节进行介绍。本节主要是详细介绍执行绘制任务所需的图形渲染管线,具体内容如下。
这是命令进入Vulkan图形渲染管线的位置。通常,Vulkan设备内部某一部分可以解释命令缓冲中的命令,并直接和硬件交互以引导工作。当完成了绘制前的准备工作后命令便进入图形渲染管线,以便为图像的渲染做进一步的工作。
该阶段读取顶点缓冲和索引缓冲中的数据,其中包含了程序将要绘制物体的顶点信息数据(如顶点位置坐标、顶点颜色等、顶点法向量等),然后对数据分组并进行组装,以供管线后续部分使用。
例如输入的顶点缓冲中包含3个顶点的数据,每个顶点包含x、y、z位置坐标和RGB色彩通道颜色值(3色三角形案例中就是如此),则输入装配阶段将顶点缓冲中的数据每6个分为一组,作为一个顶点的数据,以备后继顶点着色器进行处理。
▲图2-1 Vulkan完整的渲染管线结构
顶点着色器是一个可编程的处理单元,功能为执行顶点的变换、完成光照与材质的运用及计算等相关操作,其操作对象为每个顶点。一般工作流程为首先将原始的顶点坐标数据及其他属性值传送到顶点着色器中,再经由自己开发的顶点着色器处理后产生顶点纹理坐标、颜色、位置等后继流程需要的各项顶点属性值,并将这些结果数据传递给下一阶段。
通过可编程的顶点着色器,开发人员可以根据实际需求自行开发顶点变换、光照等功能。下面给出了顶点着色器的工作原理,如图2-2所示。
▲图2-2 顶点着色器工作原理
这里的out变量在顶点着色器内赋值后并不一定是直接将赋的值传递到后继着色器的in变量中,在此有两种可能的情况:如果后继着色器是细分控制着色器或几何着色器,则顶点着色器输出的out变量值直接传入后继着色器对应的in变量中;如果后继着色器是片元着色器,则有两种可能。
后继着色器为片元着色器时的两种可能情况如下。
▲图2-3 顶点着色器中out变量的工作原理
提示
上面介绍的两种情况是大部分渲染管线的选择,管线中仅仅包含顶点着色器与片元着色器。顶点着色器输出的数据由管线插值后传递到片元着色器,前面的3色三角形案例中采用的渲染管线就是这样。回顾一下3色三角形案例的画面,三角形中各个片元的颜色是平滑渐变的。但代码中仅仅给出了3个顶点的颜色值,这就是如图2-3中插值产生的效果了。
曲面细分是近代GPU提供的一项高级特性,通过其可以在采用较少原始顶点数据的情况下绘制出如同采用海量数据描述的光滑曲面。曲面细分工作由细分控制着色器与细分求值着色器协同完成,具体工作过程如下。
提示
从图2-1中可以看出,细分阶段并不一定在所有的情况下都选用,例如前面的3色三角形案例中就没有细分阶段。另外,这里读者简单了解一下即可,后面会有专门的章节介绍曲面细分。
几何着色器也是近代GPU提供的一项高级特性,通过其可以对图元进行处理。其输入为一个图元,输出为一个或多个图元。同时,输入与输出图元的类型可以不同。例如输入图元为三角形,输出图元为4根线段(比如三角形的3条边以及三角形的法向量)。
这就使得在不重新组织绘制用原始数据的情况下,可以用各种不同的模式进行绘制呈现,大大提高了开发的灵活性和效率。
提示
从图2-1中同样可以看出,几何着色器并不一定在所有的情况下都选用,例如前面的3色三角形案例中就没有几何着色器。同样,这里读者简单了解一下即可,后面会有专门的章节介绍几何着色器的使用。
该阶段的第一个任务是把顶点着色器、细分求值着色器或几何着色器产生的结果顶点分组,根据指定的绘制方式(如点绘制、线段绘制、三角形绘制等)和顶点连接关系(连接关系信息可能来自索引数据)将顶点组成图元以供光栅化。
图元组装完成后的任务就是进行剪裁,因为随着观察位置、角度的不同,并不总能看到(这里可以简单地理解为显示到设备屏幕上)特定物体某个图元的全部。例如,当观察一个正四面体并离某个三角形面很近时,可能只能看到此面的一部分,这时在屏幕上显示的就不再是三角形了,而是经过裁剪后形成的多边形,如图2-4所示。
▲图2-4 从不同角度、距离观察正四面体
剪裁时,如果图元完全位于视景体以及自定义剪裁平面的内部,则将完整图元传递到后面的步骤进行处理;如果其完全位于视景体或者自定义剪裁平面的外部,则丢弃该图元;如果其有一部分位于内部,另一部分位于外部,则需要剪裁该图元。
虽然3D虚拟世界中的几何信息是三维的,但由于目前用于显示的设备都是二维的,因此在真正执行光栅化工作之前,首先需要将3D虚拟世界中的物体投影到视平面上。需要注意的是,由于观察者位置的不同,同一个3D场景中的物体投影到视平面可能会产生不同的效果,如图2-5所示。
▲图2-5 3D场景投影到视平面示意图
另外,由于虚拟3D世界中物体的信息一般采用连续的数学量来表示,因此投影的结果平面也是用连续数学量表示的。但目前的显示设备屏幕都是离散化的(由一个一个的独立像素组成),因此还需要对投影的结果进行离散化,将其分解为一个一个离散化的小单元。
将投影后的图元分解为一个一个离散化小单元的操作就称之为光栅化,这些小单元一般被称为片元,具体效果如图2-6所示。
每个片元都对应于帧缓冲中的一个像素,之所以不能直接称之为像素是因为3D空间中的物体是可以相互遮挡的,并且每个3D物体的每个图元是独立处理的。因此距离观察点不同距离但在同一条视线上的不同图元将会对应到帧缓冲中的同一个位置上,这时距离远的片元就被覆盖(如何覆盖的检测将在深度检测阶段完成)。因此,某片元不一定能成为最终呈现在屏幕上的像素,称之为像素就不准确了。
▲图2-6 投影后图元离散化示意图
片元前操作,顾名思义就是在片元着色器执行前进行一些预处理的工作。这些预处理的工作主要是根据程序的设置情况,剔除一些不需要处理的片元,以提高后继片元着色器处理阶段的工作效率。随设备厂商、驱动的不同,此阶段执行的操作不尽相同。
例如,若程序打开了剪裁测试,一般在此阶段会对前面光栅化得到的各个片元进行过滤,只有位于剪裁窗口内部的片元会进入下一个阶段,其他片元则会被丢弃。
片元着色器是用于处理光栅化阶段生成并经过片元前操作处理的片元值及其相关数据的可编程处理单元,其可以执行纹理的采样、颜色的汇总等操作,每片元执行一次。其主要功能就是通过自己编写的着色方法计算每个输入片元的颜色等属性并送入后继阶段进行处理,如图2-7所示。
▲图2-7 片元着色器工作原理
提示
经过对顶点着色器、光栅化、片元着色器的介绍,可以看出顶点着色器每顶点一执行,而片元着色器每片元一执行,片元着色器的执行次数明显大于顶点着色器的执行次数。因此在开发中,应尽量减少片元着色器的运算量,可以将一些复杂运算尽量放在顶点着色器中执行。
片元着色器完成了对所有输入片元的处理后,还需要对片元进行一些特定的片元后操作。主要包含深度测试与模板测试,这两种测试的具体作用如下。
深度测试是指将输入片元的深度值与帧缓冲中存储的对应位置的深度值进行比较,若输入片元的深度值小,则将输入片元送入下一阶段准备覆盖帧缓冲中原有片元或与帧缓冲中的原有片元混合,否则将丢弃输入片元。
模板测试的主要功能是将绘制区域限定在任意形状的指定范围内,一般用于镜像、水面倒影绘制等场合。若能灵活运用,可以实现很多相关高真实感特效。
颜色混合操作接收片元着色器和片元后操作的结果,对每一个片元执行一次。如果程序开启了混合,则根据源混合因子、目标混合因子将片元颜色值与帧缓冲中对应位置的片元颜色值进行混合,否则送入的片元颜色值将覆盖帧缓冲中对应位置片元的颜色值。
Vulkan中的物体绘制并不是直接在屏幕上进行的,而是预先在帧缓冲中进行绘制,每绘制完一帧再将绘制的结果呈现到屏幕上。因此,一般在每次绘制新的一帧时都需要清除帧缓冲中的相关数据,否则有可能产生不正确的绘制效果。
同时需要了解的是为了应对不同方面的需要,帧缓冲是由一套附件组成的,主要包括颜色附件、深度附件、模板附件、输入附件,各附件的具体用途如下所列。
提示
本节只是对渲染管线中的每一个模块进行了简单的介绍,更为具体的情况会在本书中的后继章节进行更为详细的讨论,这里读者只要在概念上有个整体的把握即可。另外读者也可以回头复习一下第1章3色三角形案例的相关代码,相信理解肯定会加深不少。
第1章给出的3色三角形案例中,着色器是在程序运行时加载源代码字符串并编译为SPIR-V格式再使用的。这种策略在小型程序中使用并无不妥,但对于大型游戏或应用而言效率就不够高了。因此大型游戏或应用一般都会预先将着色器编译为SPIR-V格式并保存在文件中,程序运行时直接加载SPIR-V数据即可,效率会显著提高。
本节将基于改造3色三角形案例为着色器预编译版介绍如何对着色器进行预编译以得到存储SPIR-V数据的文件,以及如何在程序中加载SPIR-V数据使用,具体内容如下。
(1)首先应该开发出案例中需要的着色器源代码,并保存在文件中。由于本节案例改造自3色三角形案例,故其中的顶点着色器与片元着色器源文件分别为“commonTexLight.vert”与“commonTexLight.frag”。接着使用安装的Windows版Vulkan SDK中的glslangvalidator命令对两个着色器源代码进行编译,具体命令如下。
1 glslangvalidator -V commonTexLight.vert -o commonTexLight.vert.spv
2 glslangvalidator -V commonTexLight.frag -o commonTexLight.frag.spv
说明
上述命令中第1行编译了顶点着色器,第2行编译了片元着色器。命令中的“V”参数表示编译得到SPIR-V格式数据,“o”参数用于指定输出文件的路径。另外,此命令要求顶点着色器、片元着色器、细分控制着色器、细分执行着色器、几何着色器、计算着色器源代码文件的后缀分别为“.vert”“.frag”“.tesc”“.tese”“.geom”“.comp”。
(2)了解了着色器的预编译之后,下面介绍的是案例中用于加载着色器SPIR-V数据的FileUtil类的头文件。其改造自原3色三角形案例中的FileUtil类头文件,在其中增加了用于加载SPIR-V数据的loadSPV方法以及用于存储SPIR-V数据的结构体,具体代码如下。
代码位置:见随书中源代码/第2章/Sample2_1/src/main/cpp/util目录下的FileUtil.h。
1 //此处省略了相关头文件的导入,感兴趣的读者请自行查看随书源代码
2 typedef struct SpvDataStruct{ //存储SPIR-V数据的结构体
3 int size; //SPIR-V数据总字节数
4 uint32_t* data; //指向SPIR-V数据内存块首地址的指针
5 } SpvData;
6 class FileUtil{
7 public:
8 //此处省略了原有FileUtil类头文件中的成员方法声明
9 static SpvData& loadSPV(string fname); //加载Assets文件夹下的SPIR-V数据
10 };
(3)FileUtil类头文件的变化已经介绍完毕,接着介绍具体的实现代码。主要就是新增loadSPV方法的实现代码,具体内容如下。
代码位置:见随书中源代码/第2章/Sample2_1/src/main/cpp/util目录下的FileUtil.cpp。
1 SpvData& FileUtil::loadSPV(string fname){ //加载Assets文件夹下的SPIR-V数据文件
2 AAsset* asset =AAssetManager_open(aam,fname.c_str(),AASSET_MODE_STREAMING);
3 assert(asset);
4 size_t size = AAsset_getLength(asset); //获取SPIR-V数据文件的总字节数
5 assert(size > 0); //检查总字节数是否大于0
6 SpvData spvData; //构建SpvData结构体实例
7 spvData.size=size; //设置SPIR-V数据总字节数
8 spvData.data=(uint32_t*)(malloc(size)); //分配相应字节数的内存
9 AAsset_read(asset, spvData.data, size); //从文件中加载数据进入内存
10 AAsset_close(asset); //关闭AAsset对象
11 return spvData; //返回SpvData结构体实例
12 }
说明
上述loadSPV方法首先通过AAssetManager_open方法获得了一个对应于项目中Assets文件夹下指定名称文件的AAsset对象,接着通过AAsset_getLength方法获取了SPIR-V数据的总字节数,然后构建了用于存储SPIR-V数据的结构体实例,随后加载了SPIR-V数据。
(4)接着修改的是ShaderQueueSuit_Common类中用于创建着色器模块的create_shader方法,将其中加载着色器源代码并编译的部分替换为直接加载SPIR-V数据,具体代码如下。
代码位置:见随书中源代码/第2章/Sample2_1/src/main/cpp/bndevp目录下的ShaderQueueSuit_Common.cpp。
1 void ShaderQueueSuit_Common::create_shader(VkDevice& device){
2 SpvData spvVertData=FileUtil::loadSPV("shader/commonTexLight.vert.spv");
//加载顶点着色器数据
3 SpvData spvFragData=FileUtil::loadSPV("shader/commonTexLight.frag.spv");
//加载片元着色器数据
4 //此处省略了部分源代码,感兴趣的读者请自行查看随书源代码
5 VkShaderModuleCreateInfo moduleCreateInfo; //准备顶点着色器模块创建信息
6 moduleCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
7 moduleCreateInfo.pNext = NULL; //自定义数据的指针
8 moduleCreateInfo.flags = 0; //供将来使用的标志
9 moduleCreateInfo.codeSize = spvVertData.size; //顶点着色器SPV数据总字节数
10 moduleCreateInfo.pCode = spvVertData.data; //顶点着色器SPV数据
11 //此处省略了部分源代码,感兴趣的读者请自行查看随书源代码
12 VkShaderModuleCreateInfo moduleCreateInfo; //准备片元着色器模块创建信息
13 moduleCreateInfo.sType =VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
14 moduleCreateInfo.pNext = NULL; //自定义数据的指针
15 moduleCreateInfo.flags = 0; //供将来使用的标志
16 moduleCreateInfo.codeSize = spvFragData.size; //片元着色器SPV数据总字节数
17 moduleCreateInfo.pCode = spvFragData.data; //片元着色器SPV数据
18 //此处省略了部分源代码,感兴趣的读者请自行查看随书源代码
19 }
说明
上述代码基本与第1章3色三角形案例中的相同,只是修改了加载和编译顶点着色器与片元着色器的部分代码。首先将原来加载着色器脚本的方法替换为加载SPIR-V数据的loadSPV方法,接着修改了第9行与第10行以及第16行与第17行的代码,直接使用加载的SPIR-V数据。
前面章节的案例中,程序都无法对Vulkan相关调用的问题给出提示性信息,这在实际开发中将大大降低开发的效率。第1章也曾经提到过,Vulkan提供了可插拔的错误检查机制,可以在需要时启用,发布应用时关闭。这样既可以方便开发,又不影响发布程序的运行效率。
本节将对Vulkan中的可插拔错误检查机制——验证Layer进行详细的介绍,主要包括常用的验证Layer、验证消息的类型以及一个实际的案例。首先需要了解的是各种常用验证Layer的功能,具体情况如表2-1所示。
提示
不同型号的GPU随厂商、驱动版本的不同会提供不同的验证Layer组合,表2-1中列出的是LunarG及谷歌建议的几个常用验证Layer。这些验证Layer仅仅在一些厂商的某些版本的驱动中包含,可能读者自己所选用的GPU及驱动组合并不支持。
表2-1 常用的验证Layer
名称 |
说明 |
---|---|
VK_LAYER_GOOGLE_unique_objects |
由于非分派的Vulkan对象句柄不要求具有唯一性,因此只要这些不同的对象被认为是等价的,Vulkan驱动就有可能返回相同的句柄。这就使得调试时的对象追踪变得比较困难。激活此验证Layer后,每个Vulkan对象都会被分配唯一的标识,这就使得调试时的对象追踪变得容易很多。另外要注意的是,这一验证Layer必须被放到所有激活验证Layer序列的最后,也就是离驱动最近的位置 |
VK_LAYER_LUNARG_api_dump |
打开此验证Layer后,程序运行过程中将打印所有被调用Vulkan功能方法中所有的相关参数值,便于开发人员调试 |
VK_LAYER_LUNARG_core_validation |
此验证Layer激活后程序运行过程中将验证并打印描述集、管线状态等方面的重要信息。同时此验证Layer激活后还会追踪与验证显存、对象绑定、命令缓冲等,也会对图形管线和计算管线进行验证 |
VK_LAYER_LUNARG_image |
此验证Layer用于验证纹理格式、渲染目标格式等。例如可以验证请求使用的格式在对应的设备中是否支持,还可以验证图像视图创建参数与对应的图像是否匹配等 |
VK_LAYER_LUNARG_object_tracker |
此验证Layer用于追踪对象从创建到使用再到销毁的全过程,可以帮助开发人员避免内存泄露,还可以用于验证关注的对象是否恰当地被创建以及目前是否有效等 |
VK_LAYER_LUNARG_parameter_validation |
此验证Layer用于验证传递给Vulkan功能方法的参数是否正确 |
VK_LAYER_LUNARG_swapchain |
此验证Layer用于验证WSI交换链扩展的使用。例如,其可以在使用WSI交换链扩展相关功能方法前验证此扩展是否可用,可用于验证给出的图像索引是否在交换链允许的范围内等 |
VK_LAYER_GOOGLE_threading |
此验证Layer主要用于帮助检查Vulkan上下文的线程安全性。其可以检查多线程相关的API是否被正确地使用,其还可以报告违反多线程互斥访问规则的对象访问。同时,其还可以使应用程序在报告了线程问题的情况下持续正常运行而不崩溃 |
VK_LAYER_LUNARG_standard_validation |
此验证Layer用于确认所有的验证Layer以正确的顺序组织 |
表2-1中提到了非分派的Vulkan对象句柄,这里简单介绍一下这方面的知识。Vulkan中的对象句柄分为分派的和非分派的,具体含义如下。
启用了指定的验证Layer后,开发人员一般还需要提供相应的回调方法来打印相关的验证信息。不同类型的验证信息有不同的标志位,主要分为错误信息、警告信息、消息信息、性能警告信息和调试信息等5大类,具体情况如表2-2所示。
表2-2 不同类型的验证信息
名称 |
信息类型标志 |
说明 |
---|---|---|
错误信息 |
VK_DEBUG_REPORT_ERROR_BIT_EXT |
一般指错误的API使用。这类问题可能导致不可预知的程序运行结果,比如程序崩溃 |
警告信息 |
VK_DEBUG_REPORT_WARNING_BIT_EXT |
一般是指有潜在错误的API使用或危险的API使用 |
消息信息 |
VK_DEBUG_REPORT_INFORMATION_BIT_EXT |
用于显示用户友好的提示信息。一般这些信息用于描述程序后台的活动,比如资源的明细等对调试工作很有帮助的信息 |
性能警告信息 |
VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT |
一般用于提醒潜在的非最优Vulkan调用,这些调用可能导致程序的性能变差 |
调试信息 |
VK_DEBUG_REPORT_DEBUG_BIT_EXT |
用于给出来自加载器或验证Layer的诊断信息 |
了解了常用的验证Layer以及不同类型的验证信息后,下面给出一个具体的案例——PCSample2_2。实际上本案例仅仅是将第1章的案例PCSample1_1复制了一份并增加了相应的模块和调用的代码,具体内容如下。
提示
请读者注意,截止到作者交稿时Android平台下仅仅支持0个验证Layer,也就是暂时不支持Vulkan的验证Layer。因此,本节所介绍的案例代码都来自PC平台下。当然,作者也提供了Android平台下的对应案例(Sample2_2),只是目前运行后不会有任何验证Layer工作。
(1)首先需要增加的是 BNValidateUtil类,其中封装了启用指定验证Layer所需的功能方法、结构体实例、列表等。下面先给出BNValidateUtil类的声明,具体代码如下。
代码位置:见随书中源代码/第2章/PCSample2_2/BNVulkanEx/main_task目录下的BNValidateUtil.h。
1 //此处省略了相关头文件的导入,感兴趣的读者请自行查看随书源代码
2 typedef struct BNDeviceLayerAndExtensionType{
3 std::vector<std::string*> layerNames; //支持的验证Layer名称列表
4 std::vector<std::string*> extensionNames; //支持的验证Layer所需扩展的名称列表
5 }BNDeviceLayerAndExtension;
6 class BNValidateUtil{
7 public:
8 static std::vector<VkLayerProperties> layerList; //获取的验证Layer属性列表
9 static PFN_vkCreateDebugReportCallbackEXT dbgCreateDebugReportCallback;
10 static PFN_vkDestroyDebugReportCallbackEXT dbgDestroyDebugReportCallback;
11 static VkDebugReportCallbackEXT debugReportCallback; //调试报告回调
12 static BNDeviceLayerAndExtension getLayerProperties
13 (std::vector<const char*> exceptedLayerNames);
14 static std::vector<std::string*> getLayerDeviceExtension(VkPhysicalDevice& gpu,
15 std::vector<std::string*> supportedlayerNames);
16 static void createDebugReportCallbackSelf(VkInstance& instance);
//创建调试报告回调的方法
17 static void destroyDebugReportCallbackSelf(VkInstance& instance);
//销毁调试报告回调的方法
18 static VKAPI_ATTR VkBool32 VKAPI_CALL debugFunction(//用于被回调以打印验证信息的方法
19 VkFlags msgFlags, //触发此回调执行的调试事件类型标志
20 VkDebugReportObjectTypeEXT objType, //由此回调处理的对象类型
21 uint64_t srcObject, //此回调创建或处理的对象的句柄
22 size_t location, //描述对应调试事件代码的位置
23 int32_t msgCode, //消息代码
24 const char *layerPrefix, //触发此回调的验证Layer
25 const char *msg, //消息字符串
26 void *pUserData //用户自定义数据
27 );};
(2)接下来逐步给出BNValidateUtil类的具体实现代码,首先是getLayerProperties方法的实现代码,具体内容如下。
代码位置:见随书中源代码/第2章/PCSample2_2/BNVulkanEx/main_task目录下的BNValidateUtil.cpp。
1 //此处省略了部分相关文件的调用,感兴趣的读者请自行查看随书源代码
2 bool isContain(std::vector<const char*> inNames, char* inName){
//判断字符串是否在列表中的方法
3 for (auto s : inNames){ //遍历字符串列表
4 if (strcmp(s, inName) == 0){ //若给定字符串与当前字符串相同
5 return true; //返回true,表示指定字符串在列表中
6 }}
7 return false; //返回false,表示指定字符串不在列表中
8 }
9 bool isContain(std::vector<std::string*> inNames, char* inName){//判断字符串是否在列表中的方法
10 for (auto s : inNames){ //遍历字符串列表
11 if (strcmp((*s).c_str(), inName) == 0){//若给定字符串与当前字符串相同
12 return true; //返回true,表示指定字符串在列表中
13 }}
14 return false; //返回false,表示指定字符串不在列表中
15 }
16 BNDeviceLayerAndExtension BNValidateUtil::getLayerProperties
17 (std::vector<const char*> exceptedLayerNames){
18 BNDeviceLayerAndExtension result; //返回结果结构体实例
19 uint32_t layerCount; //总的验证Layer的数量
20 vkEnumerateInstanceLayerProperties(&layerCount, NULL);//获取总的验证Layer数量
21 LOGE("Layer的数量为 %d\n", layerCount); //打印总的验证Layer数量
22 layerList.resize(layerCount) ; //更改列表长度
23 vkEnumerateInstanceLayerProperties(&layerCount, layerList.data());
//获取总的验证Layer属性列表
24 for (int i = 0; i < layerList.size(); i++){//遍历验证Layer属性列表
25 VkLayerProperties lp = layerList[i]; //获取当前验证Layer属性
26 LOGE("----------------Layer %d----------------\n", i);//打印验证Layer序号
27 LOGE("layer名称 %s\n", lp.layerName); //打印验证Layer名称
28 LOGE("layer描述 %s\n", lp.description);//打印验证Layer描述信息
29 bool flag = isContain(exceptedLayerNames, lp.layerName);//当前验证Layer是否需要
30 if (flag){ //若需要,则将当前验证Layer名称记录到验证Layer名称结果列表
31 result.layerNames.push_back(new std::string(lp.layerName));
32 }
33 uint32_t propertyCount; //此验证Layer对应的扩展属性数量
34 vkEnumerateInstanceExtensionProperties(lp.layerName, &propertyCount, NULL);
35 std::vector<VkExtensionProperties> propertiesList; //扩展属性列表
36 propertiesList.resize(propertyCount); //调整列表长度
37 vkEnumerateInstanceExtensionProperties(lp.layerName, &propertyCount,
propertiesList.data());
38 for (auto ep : propertiesList){ //遍历此验证Layer对应的扩展属性列表
39 LOGE(" 所需扩展:%s\n", ep.extensionName); //打印扩展名称
40 if (flag){ //若当前验证Layer是需要的
41 if (!isContain(result.extensionNames, ep.extensionName)){
42 result.extensionNames.push_back(new std::string(ep. extensionName));
43 }}}}
44 return result; //返回结果
45 }
46 //此处省略了其他功能方法的实现,接下来将会详细介绍
(3)继续给出的是获取指定验证Layer名称列表所需的逻辑设备扩展名称列表的方法——getLayerDeviceExtension,其具体代码如下。
代码位置:见随书中源代码/第2章/PCSample2_2/BNVulkanEx/main_task目录下的BNValidateUtil.cpp。
1 std::vector<std::string*> BNValidateUtil::getLayerDeviceExtension
2 (VkPhysicalDevice& gpu, std::vector<std::string*> supportedlayerNames){
3 std::vector<std::string*> result; //所需设备扩展名称结果列表
4 for (int i = 0; i < layerList.size(); i++){ //遍历所有验证Layer的属性列表
5 VkLayerProperties lp = layerList[i]; //获取当前验证Layer属性
6 LOGE("----------------Layer %d----------------\n", i);//打印验证Layer序号
7 LOGE("layer名称 %s\n", lp.layerName); //打印验证Layer名称
8 LOGE("layer描述 %s\n", lp.description);//打印验证Layer描述信息
9 uint32_t propertyCount; //设备扩展属性数量
10 vkEnumerateDeviceExtensionProperties(gpu, //获取当前验证Layer对应设备扩展属性数量
11 lp.layerName, &propertyCount, NULL);
12 std::vector<VkExtensionProperties> propertiesList; //设备扩展属性列表
13 propertiesList.resize(propertyCount); //调整列表长度
14 vkEnumerateDeviceExtensionProperties(gpu,//填充当前验证Layer对应设备扩展属性列表
15 lp.layerName, &propertyCount, propertiesList.data());
16 for (auto ep : propertiesList){ //遍历设备扩展属性列表
17 LOGE(" 所需设备扩展:%s\n", ep.extensionName);
18 if (isContain(supportedlayerNames, lp.layerName)){//判断当前验证Layer是否需要
19 if (!isContain(result, ep.extensionName)){//判断当前设备扩展是否已在列表中
20 result.push_back(new std::string(ep.extensionName));
//将当前设备扩展名称添加进列表
21 }}}}
22 return result; //返回所需设备扩展名称结果列表
23 }
说明
上述getLayerDeviceExtension方法接收指定的物理设备和需要支持的验证Layer名称列表,然后遍历目前所有的验证Layer属性列表,获取每个验证Layer所需的设备扩展属性列表。若遍历到的当前验证Layer名称在需要支持的验证Layer名称列表中,则将对应的设备扩展名称添加到结果列表中。这里要注意的是,不同的验证Layer可能需要同样的设备扩展,因此组织所需设备扩展名称结果列表时要注意避免重复的问题。
(4)接着介绍的是用于被回调以打印验证信息的方法——debugFunction。若此方法的返回值为VK_SUCCESS表示此回调结束后,后继的验证Layer继续执行,若返回值为VK_FALSE表示后面的验证Layer不再继续执行。
代码位置:见随书中源代码/第2章/PCSample2_2/BNVulkanEx/main_task目录下的BNValidateUtil.cpp。
1 VKAPI_ATTR VkBool32 VKAPI_CALL BNValidateUtil::debugFunction(
2 VkFlags msgFlags, //触发此回调执行的调试事件类型标志
3 VkDebugReportObjectTypeEXT objType, //由此回调处理的对象类型
4 uint64_t srcObject, //此回调创建或处理的对象的句柄
5 size_t location, //描述对应调试事件代码的位置
6 int32_t msgCode, //消息代码
7 const char *layerPrefix, //触发此回调的验证Layer,比如是加载器还是验证Layer
8 const char *msg, //消息字符串
9 void *pUserData){ //用户自定义数据
10 if (msgFlags & VK_DEBUG_REPORT_ERROR_BIT_EXT){ //错误信息
11 LOGE("[VK_DEBUG_REPORT] ERROR: [%s]Code%d:%s\n", layerPrefix, msgCode, msg);
12 }else if (msgFlags & VK_DEBUG_REPORT_WARNING_BIT_EXT){ //警告信息
13 LOGE("[VK_DEBUG_REPORT] WARNING: [%s]Code%d:%s\n", layerPrefix, msgCode, msg);
14 }else if (msgFlags & VK_DEBUG_REPORT_INFORMATION_BIT_EXT){ //消息信息
15 LOGE("[VK_DEBUG_REPORT] INFORMATION:[%s]Code%d:%s\n",layerPrefix,msgCode, msg);
16 }else if (msgFlags& VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT){//性能警告信息
17 LOGE("[VK_DEBUG_REPORT] PERFORMANCE: [%s]Code%d:%s\n", layerPrefix, msgCode, msg);
18 }else if (msgFlags & VK_DEBUG_REPORT_DEBUG_BIT_EXT){ //调试信息
19 LOGE("[VK_DEBUG_REPORT] DEBUG: [%s]Code%d:%s\n", layerPrefix, msgCode, msg);
20 }else{
21 return VK_FALSE; //其他未知情况
22 }
23 return VK_SUCCESS;
24 }
说明
上述debugFunction方法并不复杂,其根据消息代码将消息分为错误信息、警告信息、消息信息、性能警告信息、调试信息,并分别进行打印。实际开发中若读者还有其他特殊需要,还可以进一步自定义输出的调试信息。另外,此回调方法的名称可以自定义,但其入口参数序列是Vulkan中规定的,不能进行随意改动。
(5)经过前面的步骤后,BNValidateUtil类的实现代码就剩下创建调试报告回调的方法createDebugReportCallbackSelf和销毁调试报告回调的方法destroyDebugReportCallbackSelf还没有介绍了,这两个方法的代码如下。
代码位置:见随书中源代码/第2章/PCSample2_2/BNVulkanEx/main_task目录下的BNValidateUtil.cpp。
1 void BNValidateUtil::createDebugReportCallbackSelf(VkInstance& instance){ //创建调试报告回调相关
2 dbgCreateDebugReportCallback = (PFN_vkCreateDebugReportCallbackEXT)
3 vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT");
4 VkDebugReportCallbackCreateInfoEXT //构建调试报告回调创建用信息结构体实例
5 dbgReportCreateInfo = {};
6 dbgReportCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT;
7 dbgReportCreateInfo.pfnCallback = debugFunction; //指定回调方法
8 dbgReportCreateInfo.pUserData = NULL; //传递给回调的用户自定义数据
9 dbgReportCreateInfo.pNext = NULL; //指向自定义数据的指针
10 dbgReportCreateInfo.flags = //所需的触发消息回调的事件类型
11 VK_DEBUG_REPORT_WARNING_BIT_EXT |
12 VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT |
13 VK_DEBUG_REPORT_ERROR_BIT_EXT |
14 VK_DEBUG_REPORT_DEBUG_BIT_EXT;
15 VkResult result = dbgCreateDebugReportCallback(instance,//创建调试报告回调实例
16 &dbgReportCreateInfo, NULL, &debugReportCallback);
17 if (result == VK_SUCCESS){
18 LOGE("调试报告回调对象创建成功!\n");
19 }}
20 void BNValidateUtil::destroyDebugReportCallbackSelf(VkInstance& instance){
//销毁调试报告回调相关
21 dbgDestroyDebugReportCallback = (PFN_vkDestroyDebugReportCallbackEXT)
22 vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT");
23 dbgDestroyDebugReportCallback(instance, debugReportCallback, NULL);
24 }
(6)了解了BNValidateUtil类后,接下来就可以基于其在项目中启动期望的验证Layer了。首先需要在MyVulkanManager类的头文件中增加相关的声明,具体代码如下。
代码位置:见随书中源代码/第2章/PCSample2_2/BNVulkanEx/main_task目录下的MyVulkanManager.h。
1 class MyVulkanManager
2 {
3 //此处省略了其他成员的声明
4 static std::vector<const char *> exceptedLayerNames;//期望启动的验证Layer名称列表
5 static BNDeviceLayerAndExtension bdlae; //支持的验证Layer和所需实例扩展
6 //此处省略了其他方法的声明
7 }
说明
从上述代码中可以看出,为了使用验证Layer,增加了两个成员。一个是期望启动的验证Layer名称列表,另一个是能够支持的验证Layer与所需实例扩展名称列表的组合结构体。
(7)在头文件中增加了所需成员的声明后,下面的工作为在MyVulkanManager类的init_vulkan_instance方法、create_vulkan_devices方法和destroy_vulkan_instance方法中添加相关的调用代码,具体内容如下。
代码位置:见随书中源代码/第2章/PCSample2_2/BNVulkanEx/main_task目录下的MyVulkanManager.cpp。
1 void MyVulkanManager::init_vulkan_instance(){ //创建Vulkan实例的方法
2 //此处省略了部分代码,感兴趣的读者自行查看随书源代码
3 instanceExtensionNames.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
4 instanceExtensionNames.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
5 instanceExtensionNames.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
6 //此处省略了部分代码,感兴趣的读者自行查看随书源代码
7 exceptedLayerNames.push_back("VK_LAYER_LUNARG_core_validation");
8 exceptedLayerNames.push_back("VK_LAYER_LUNARG_parameter_validation");
9 exceptedLayerNames.push_back("VK_LAYER_LUNARG_standard_validation");
10 bdlae = BNValidateUtil::getLayerProperties(exceptedLayerNames);//获取支持情况
11 for (auto s : bdlae.extensionNames){ //将所需的扩展加入扩展名称列表
12 instanceExtensionNames.push_back((*s).c_str());
13 }
14 exceptedLayerNames.clear(); //清空验证Layer名称列表
15 for (auto s : bdlae.layerNames){ //将能支持的验证Layer名称加入Layer名称列表
16 exceptedLayerNames.push_back((*s).c_str());
17 }
18 //此处省略了部分代码,感兴趣的读者自行查看随书源代码
19 inst_info.enabledExtensionCount = instanceExtensionNames.size();//扩展的数量
20 inst_info.ppEnabledExtensionNames = instanceExtensionNames.data();//扩展名称列表数据
21 inst_info.enabledLayerCount = exceptedLayerNames.size();//启动的验证Layer数量
22 inst_info.ppEnabledLayerNames = exceptedLayerNames.data();//启动的验证Layer名称列表
23 //此处省略了部分代码,感兴趣的读者自行查看随书源代码
24 if (exceptedLayerNames.size()>0){ //若能够启动的验证Layer数量大于0
25 BNValidateUtil::createDebugReportCallbackSelf(instance);//创建调试报告回调
26 }}
27 void MyVulkanManager::create_vulkan_devices(){ //创建逻辑设备的方法
28 //此处省略了部分代码,感兴趣的读者自行查看随书源代码
29 std::vector<std::string*> needsDeviceExtensions = //获取验证Layer所需设备扩展
30 BNValidateUtil::getLayerDeviceExtension(gpus[USED_GPU_INDEX], bdlae);
31 for (auto s : needsDeviceExtensions){ //将所需设备扩展加入列表
32 deviceExtensionNames.push_back((*s).c_str());
33 }
34 //此处省略了部分代码,感兴趣的读者自行查看随书源代码
35 }
36 void MyVulkanManager::destroy_vulkan_instance(){ //销毁Vulkan实例
37 if (exceptedLayerNames.size()>0){ //销毁调试报告回调
38 BNValidateUtil::destroyDebugReportCallbackSelf(instance);
39 }
40 //此处省略了部分代码,感兴趣的读者自行查看随书源代码
41 }
提示
通过以上代码读者应该发现,在基于作者开发的工具类BNValidateUtil使用Vulkan提供的验证Layer进行调试时,首先需要组织开发者自己所需的验证Layer名称列表。接着调用BNValidateUtil类的getLayerProperties方法获取能够支持的验证Layer及其所需的设备扩展以备创建Vulkan实例时使用,然后调用BNValidateUtil类的createDebugReportCallbackSelf方法创建调试报告回调。随后,在创建逻辑设备前调用BNValidateUtil类的getLayerDeviceExtension方法获取能启动的验证Layer所需的设备扩展名称列表以备创建逻辑设备时使用。最后,在销毁Vulkan实例的同时销毁一开始创建的调试报告回调实例。
完成了代码的开发后,就可以运行程序观察打印的各种调试信息了,具体内容如下。
(1)运行案例PCSample2_2,在getLayerProperties方法执行后,控制台窗口中会打印所有当前环境中支持的验证Layer名称、描述信息及所需实例扩展名称,如图2-8所示。
(2)getLayerDeviceExtension方法执行后,控制台窗口中会打印所有验证Layer的名称、描述信息及所需设备扩展名称,如图2-9所示。
(3)本节案例PCSample2_2打开的验证Layer为VK_LAYER_LUNARG_core_validation、VK_LAYER_LUNARG_parameter_validation和VK_LAYER_LUNARG_standard_validation。打开VK_LAYER_LUNARG_standard_validation验证Layer后,打印的调试信息如图2-10所示。当Vulkan图形应用程序出现不合理情况时,控制台窗口会打印相应的错误(ERROR)信息,如图2-11所示。
▲图2-8 所有验证Layer列表及所需扩展
▲图2-9 所有验证Layer列表及所需设备扩展
▲图2-10 调试信息1
▲图2-11 调试信息2
提示
从上述程序的运行中可以看出,有了验证Layer的帮助,程序开发中遇到问题进行调试时就不再是在黑暗中摸索了,能大大提高开发效率,同时避免隐藏的问题未被发现。另外,由于硬件及驱动版本的不同,读者运行时的输出信息可能和图2-8、图2-9、图2-10以及图2-11所示的内容大相径庭。
GPU一词想必读者已经很熟悉,其最早由NVIDIA于1999年提出,指的是专为执行图形渲染所需的复杂计算而设计的专用处理器,其在图形渲染工作中的效率要远高于通用设计目标的CPU。对于3D图形相关开发人员及游戏爱好者而言,GPU能力是衡量PC或移动设备性能的重要指标。
由于Vulkan诞生的时间不长,因此当下市面上的GPU并不都能很好地支持。为了使读者更好地了解这方面的情况,本节将简要介绍目前能够很好地支持Vulkan的GPU。介绍将分为移动端及PC端两部分进行,具体内容如下。
Android平台下,由于没有统一的硬件标准,导致各厂家各型号智能手机、平板电脑的硬件配置大相径庭。目前应用在Android移动平台的GPU主要由4家公司提供,分别为Imagination、ARM、高通和NVIDIA。下面将对这4家公司提供的支持Vulkan的GPU进行简要介绍,具体内容如下。
PowerVR Rogue是由Imagination于2010年发布的PowerVR架构,支持Vulkan需要PowerVR 6及更新的系列,具体情况如下。
Mail系列GPU是ARM设计出品的,其中Midgard1-4可以全平台支持Vulkan API。目前主要型号为Mali-G71、Mali-G72、Mali-T760、Mali-T820、Mali-T830、Mali-T860、Mali-T880等,具体情况如下。
▲图2-12 Galaxy S8
▲图2-13 Galaxy A7
Adreno系列由高通推出,被广泛应用于高通的Snapdragon平台上。其中,高通 Adreno 400和Adreno 500系列全平台支持Vulkan API。目前应用较为广泛3款Adreno系列GPU分别是Adreno 430、Adreno 530、Adreno 540。
▲图2-14 小米5
▲图2-15 小米6
GeForce ULV系列由NVIDIA推出,被广泛应用于Tegra平台上。目前支持Vulkan API的型号主要为Tegra x1等。从性能上来看,英伟达的GeForce系列图形芯片在整体上非常优秀,特别是在高清视频录制和播放方面以及大型3D游戏方面有着卓越的表现。
Tegra X1是英伟达目前最先进的移动处理器,其拥有 256 个 NVIDIA Maxwell GPU 核心和一颗64位CPU、具备优异的 4K 视频功能和超越上一代产品的节能性与性能。例如NVIDIA推出的Shield系列游戏机及平板产品都是搭载的Tegra X1,而大名鼎鼎的任天堂Switch也是采用的Tegra X1改进版。
与移动端GPU相比,PC端GPU最核心的差别在于性能设计不同。为了满足PC端计算需求更高的游戏或图形处理软件的要求,PC端GPU的性能较移动GPU要高出一大截。目前市场上的PC端GPU主要由NVIDIA、AMD、Inter提供,具体情况如下。
NIVDIA是PC端领域GPU提供商中的翘楚,目前市面上很多高性能游戏PC、图形处理工作站都采用了其提供的GPU。NIVDIA GPU对Vulkan的支持较为广泛,Kepler、Maxwell、Pascal的3代GPU中的大部分型号都可以良好地兼容Vulkan API。下面将介绍两个较新系列的产品,具体情况如下。
AMD是一家专门为计算机、通信和消费电子行业设计和制造各种创新的微处理器(CPU、GPU、APU、主板芯片组、电视卡芯片等),以及提供闪存和低功率处理器解决方案的公司。产品中基于次世代图形核心(GCN)架构的任何AMD APU 或 Radeon™ GPU 现在均能良好适配Vulkan API。下面介绍其产品中较新的几个系列,具体内容如下。
Intel是美国一家主要研制CPU处理器的公司,是全球最大的个人计算机零件和CPU制造商。目前市面上的Sky Lake和Kaby Lake系列处理器搭载的核显基本都可以适配Vulkan API,诸如常见的HD Graphics 510/515/520/530、Iris Graphics 540/550/580、HD610/630等。
提示
核显毕竟处理能力有限,读者如果希望运行大规模的Vulkan图形应用程序,还是建议采用恰当的独立显卡,诸如NVIDIA的GTX1060性价比就很不错。
通过本章的学习,读者应该基本掌握了渲染管线、着色器预编译和Vulkan调试技术,并对目前支持Vulkan的主流GPU有了简单的了解。尤其是着色器预编译和Vulkan调试技术,为以后读者进行实际的Vulkan项目开发打下了良好的基础。