graphics presentandsync svchost占用cpu过高过高,导致游戏很卡是怎么回事

&img src=&/50/v2-102c4ee0abb790d96422dba8_b.png& data-rawwidth=&717& data-rawheight=&194& class=&origin_image zh-lightbox-thumb& width=&717& data-original=&/50/v2-102c4ee0abb790d96422dba8_r.png&&&p&本文阐述了如下几点:&/p&&ol&&li& 对象序列化,FArchive的设计&/li&&li&uasset文件格式&/li&&li&LinkerLoad & LinkerSave&/li&&li&SavePackage的流程&/li&&/ol&&p&原文链接
简书&a href=&/?target=http%3A///p/9fea500aaa4d& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&UE4对象系统_序列化和uasset文件格式&i class=&icon-external&&&/i&&/a&&/p&&p&&/p&
本文阐述了如下几点: 对象序列化,FArchive的设计uasset文件格式LinkerLoad & LinkerSaveSavePackage的流程原文链接 简书
&p&对于不透明(opaque)的 fragment,最后只需要看到最接近视点的 fragment。在实时渲染一般是通过深度缓冲来取得这个最近的 fragment。&/p&&p&但对于半透明的 fragment,每一个都会影响最终的结果,需要从远至近地渲染。由于深度缓冲不能用于排序多个 fragment,所以只能用于保存最接近视点的不透明 fragment 深度,远于该深度的透明 fragment 不用渲染,所以仍需要深度测试(depth test)。&/p&&p&由于需要手动排序半透明的物体,写入深度已没有意义。如果能完美地手工排序不透明 fragment,写入深度也没有影响;但若物体在深度方向有重叠,有些 fragment 的顺序会有误,如果开启深度写入,会令到渲染错误很明显,而关掉的话影响较小一点。&/p&&p&当然理想的话,应该用顺序无关透明(order independent transparency, OIT)技术,例如用逐像素链表存储多个 fragment,以像素为单位排序渲染。&/p&
对于不透明(opaque)的 fragment,最后只需要看到最接近视点的 fragment。在实时渲染一般是通过深度缓冲来取得这个最近的 fragment。但对于半透明的 fragment,每一个都会影响最终的结果,需要从远至近地渲染。由于深度缓冲不能用于排序多个 fragment,所…
&img src=&/50/v2-29e8ea813b65fea1bd510d_b.jpg& data-rawwidth=&1280& data-rawheight=&913& class=&origin_image zh-lightbox-thumb& width=&1280& data-original=&/50/v2-29e8ea813b65fea1bd510d_r.jpg&&&p&如果我告诉你,计算机图形学早在10年前就开始用机器学习了,你可能会不信。毕竟机器学习这个概念是在2012年左右才火起来的,并藉由深度学习的东风一吹,现在颇有燎原之势;计算机图形学本来就跟机器学习八竿子打不着,怎么可能在10年前就做机器学习?&/p&
&p&&我读paper少,你不要骗我!&&/p&
&p&当然,这也难怪,毕竟在人工智能飞速发展的今天,三个月前的paper可能已经是旧的技术,两三年前的架构都需要“学术考古”才能溯本清源了,自然不会有人去想10年前,甚至是15年前的图形学论文。&b&其实,在图形学中,机器学习的方法早有应用,只是那个时候还不叫机器学习,而是更加淳朴地叫“数据驱动”的方法而已&/b&。&/p&
&p&今天,我们就沿着数据驱动这条主线,从“学术考古”开始,挖一挖早年间的图形学中的机器学习技术。并以微软亚洲研究院网络图形组在数据驱动图形学方向上的几份工作作为例子,讲一讲数据驱动图形学的发展历史。限于篇幅和笔者能力所限,本文仅仅围绕&b&材质属性建模&/b&这一图形学中的子问题进行讨论,其实在图形学很多其他方向,也有大量的数据驱动的做法。&/p&
&p&既然提到所谓的材质属性建模,我们就先来讲讲什么是材质属性建模。在图形学的专业领域,&b&材质属性又称为“表观(Appearance)”&/b&,本意指的是表象上的、外观上的属性,故称之为表观。从科学的角度定义,图形学中的表观(Appearance)是描述光线如何在物体表面和内部进行交互的一种性质,例如,在不透明表面,光线如何进行反射,反射后的能量如何衰减;以及在半透明表面,光的能量如何在物体内部进行传播,这些都是表观要描述的。我们可以看到,这些性质,其实都是和物体的材质属性有关的,比如金属和石膏的表面反射自然就会不同,因此便于大家更好理解,本文就以材质属性来代替所谓
“表观”这一概念。&/p&
&h2&&b&洪荒时代,机器学习,数据当先——早期表观数据采集方法&/b&&/h2&
&p&我们知道,机器学习的方法往往需要有大量的数据作为“知识”,因此为了做好机器学习我们首先需要准备好&b&大量的材质属性数据&/b&。早期的数据采集方法多是基于brute-force sampling。之前,我们对“brute-force”还真是没有什么太优美的翻译,不过现在我们有了一个特别合适的说法——“洪荒之力”。那么就让我们回到那个洪荒年代,看看早期的数据采集是怎么做的。&/p&
&p&早在2003年,数据驱动图形学的先驱者之一Wojciech Matusik等人 [Matuski et al. 2003]在2003年就做出了惊人之举,利用“洪荒之力(brute-force sampling)”的方法一举获得了100个不同材质的表面反射属性。为了能够完整地获取光线在某一特定材质属性表面的反射性质,我们需要测量在不同入射光和出射光方向的组合情况下光线衰减的比率。如果我们把入射和出射方向的角度离散化到1°的话,即使考虑了光线可逆等性质,削减冗余的数据,对于一个材质而言,也要有90x90x360这样上百万个数据点。为了采样这些数据,Matusik等人设计了一个具有两个悬臂的巨大机械设备,自动拍摄一个球形物体在不同光照和视角下的照片,从而获得所需要的数据。这种做法,不仅需要巨大的设备,同时还要手工打造100个均质材料的球体。想想当年Matusik等人还真是下了苦功夫才做出了这100个材质数据。直至今日,大家说起做表面反射属性,总还是会提到这100个材质数据,也就是大名鼎鼎的MIT-MERL BRDF。&/p&
&img src=&/v2-90e2d02a75e54c9a70c2daf65e82d200_b.png& data-rawwidth=&286& data-rawheight=&295& class=&content_image& width=&286&&&img src=&/v2-ea2092249eded1dc74497c1f_b.png& data-rawwidth=&536& data-rawheight=&358& class=&origin_image zh-lightbox-thumb& width=&536& data-original=&/v2-ea2092249eded1dc74497c1f_r.png&&&p&图1 大名鼎鼎的MERL MIT BRDF数据集,及其采集设备&/p&
&p&这种洪荒式的数据采集,可以说是数据驱动建模方法的一个开端。这就好比是现在人工智能中的数据标定,没有ImageNet和MS CoCo这样的大规模数据集,再好的机器学习算法也是白搭。因此,为了能登上数据驱动图形学这条船,洪荒之力的船票还是需要的。微软亚洲研究院的网络图形组也是认定了这一方向,专门配置了数据采集实验室,配置了昂贵的数据材质设备。磨刀不误砍柴功,基于这些采集到的海量数据,网络图形组也做出了很多知名的早期工作,如Modeling and Rendering of Quasi-Homogeneous Materials、SubEdit: a representation for editing measured heterogeneous
subsurface scattering等一系列优秀的SIGGRAPH论文。&/p&
&img src=&/v2-f01a01c6b2ebae4637250_b.png& data-rawwidth=&470& data-rawheight=&364& class=&origin_image zh-lightbox-thumb& width=&470& data-original=&/v2-f01a01c6b2ebae4637250_r.png&&&p&图2 微软亚洲研究院网络图形组的表观数据采集设备&/p&
&p&当年我也有幸亲眼见证了一次利用“洪荒之力”采集半透明材质属性(BSSRDF)的过程,那时对于每一个材料样本,都要拍摄数万张的照片。每次数据采集,相机都会在程序自动控制的情况下进行长达数十个小时的连续拍摄。依稀记得当年为了拍摄所有的BSSRDF数据,组里的单反相机在一个月内换了两次快门。连相机的维修人员都惊诧于我们是怎么能在这么短时间内(保修期之内)用坏相机快门的。现在回想起来,这类方法真的算是用上“洪荒之力”了。当然,值得庆贺的是,藉由分析这些“洪荒”来的大量数据,得出了很多重要结论,并发表在2009年的SIGGRAPH会议上
[Song et al. 2009]。另外,这一数据也多次被其他科研项目所应用,那几个可怜的单反相机做出的牺牲也算是值得了。&/p&
&h2&&b&模型决定效率——基于数据相关性的建模方法&/b&&/h2&
&p&看过前面这种洪荒式的数据采集方法,我想大部分人也能明白,这种做法是不适用于一般应用的。为了科研拼命采集几十个数据尚且可以接受,但是要让一般用户接受,还是需要极大地简化采集数据的规模。因此,&b&如何分析和利用数据本身所具有的特性,对数据进行建模,进而减少所需要采集数据的规模,就成为数据驱动建模研究的精要之处&/b&。用最简单的说法就是“模型决定效率”,有什么样的模型,就决定了所需要采集数据的规模。这里,我们以微软亚洲研究院做得最多的物体表面反射纹理这一工作为例,分析在几篇论文中是如何利用不同的模型来实现高效的数据采集的。&/p&
&p&这里值得注意的是,我们需要采集的是一个具有空间分布的材质属性,对于空间中的每一个点,它的材质属性都是不一样的。因此,在下文中,我们用&b&材质属性(BRDF)&/b&来特指在空间中每一个点的材质性质,表现为一种角度分布(不同的入射光、出射光方向的反射率),而用纹理来特指这些材质属性在空间上的分布(Spatial-distributions,,不同区域的不同材质)。&/p&
&img src=&/v2-ba0bbf625a54d6f05d6dda_b.png& data-rawwidth=&573& data-rawheight=&313& class=&origin_image zh-lightbox-thumb& width=&573& data-original=&/v2-ba0bbf625a54d6f05d6dda_r.png&&&p&图3 物体表面材质是由非常有限的材质构成的&/p&
&p&我们知道,对一般的物体表面而言,虽然其表面材质具有非常复杂的空间纹理变化,但是仔细观察会发现,其实&b&每一个物体表面上所具有的材质数目是非常有限的&/b&。比如上图中这个丝绸的材质,从大类上看,仅有金色和红色两种丝绸材质而已,即使算上细节的变化,整个表面的材质也不会超过几十种。这个数量比起用“洪荒法”独立处理每一个表面位置的材质而言,数目少了多个数量级。因此,如何利用这一性质,并进行建模就成了提高采集效率的关键。这里的一个核心问题就是,如何找到空间中不同位置上具有相同(或相似)材质的点。而且,更重要的是,我们需要利用具有相同材质的不同的点所具有的不同性质(如不同的相对相机、光源位置等)来为我们提供更多的信息。因此,我们就需要寻找表象上并不相近(相机观察到的数值),而本质上确是同一个材质(材质属性)的一组点;由于他们的表象并不相近,想要找出内涵上的相似性并非易事。&/p&
&p&最直接的方法就是,先部分套用“洪荒法”,在一个固定的视角上采集足够多数据,然后利用这些数据,在每个像素点上都重构一些“&b&部分材质属性 partial-BRDF&/b&”,利用这些部分属性作为媒介,在整个材质表面寻找具有相似材质的点 [Wang et al. 2008]。进而综合这些点所具有的信息,统一计算出它们对应的完整的材质属性。这样虽然能够利用数据的相关性减少采集所需要的图像数量,但是最初部分洪荒采集还是需要花费不少功夫,一个高质量的数据依然需要数个小时的数据采集过程。&/p&
&p&前面的方法是先尝试找到不同的“部分数据”而后拼接起来,而另一种直观的思路则是先用一些技巧直接获得对于某一材质所特定的那几种&b&单点材质属性(BRDF)&/b&,而后仅仅需要决定这些材料的纹理(空间分布
Spatial-distributions)就好了 [Dong et al. 2010]。如果有完整的材质属性作为媒介,我们就利用这个已知的材质属性(BRDF),计算出这个材质在不同空间位置应有的反射响应,进而推算出哪些空间中的像素是具有同样材质属性的。因此,我们设计了一个两步走的数据采集方法——
设计并制造了一种专门采集某一单点上材质属性的设备。利用专门设计的光学系统,这一设备可以非常快速地采集单点材质属性;而后,利用采集到的这些单点材质作为媒介,我们仅需要拍摄少数几张照片即可重建整个材质样本的完整材质属性(SVBRDF)。&/p&
&p&我们还进一步发现,对于一个特定材质而言,其表面的材质属性其实可以表示为一些少数具有&b&代表性的材质属性(representative BRDF)&/b&所构成的一个低维流型(Manifold),这个流型模型比传统的全局线性模型具有更少的自由度,可以利用更少的参数刻画一个样本具有空间分布的&b&反射属性(SVBRDF)&/b&。因此,这一模型也进一步减少了所需采集图像的数目。利用数据采集的两步法,加上这个流型模型,我们将采集一个材质样本材质属性的时间减少到了几十分钟。&/p&
&p&虽然这一方法能极大地减少采集数据的成本,但是需要一个专门设计的光学设备还是极大地制约了这一方法的适用范围。为解决这一问题,我们进一步设计了同时求解代表性材质属性(representative BRDF)以及纹理空间分布(spatial
distribution)的方法,一举解决了难以确定哪些空间上的点具有相似材质属性的问题。将“两步走”改进为“一步走”,在减少了采集设备复杂度的同时,依然保持了仅需少量图像的优势。&/p&
&p&在最初的尝试中,我们采用了相对易于优化的&b&全局线性模型&/b&,并利用传统&b&低秩矩阵优化(Low-rank optimization)&/b&的方法进行求解 [Chen et al. 2014]。为了进一步减少采集图像的数目,我们提出了尽可能精简化(Sparse-as-possible)模型,也就是尽可能用精简的方式去解释所观察到的内容,尽量用简单的少数几个代表性材质线性表示观察到的数据,精简模型的复杂度。相比全局线性模型而言,这一精简化模型可以进一步压缩所需要采集图像的数目,对于一个一般的物体,仅需要花几分钟采集几张到几十张照片即可实现高质量的材质重建 [Zhou et al. 2016]。&/p&&br&&img src=&/v2-e25ddffd5ee417dde18c6e_b.png& data-rawwidth=&640& data-rawheight=&161& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&/v2-e25ddffd5ee417dde18c6e_r.png&&&p&图4 利用Sparse-as-possible模型采集的材质进行渲染的结果&/p&
&h2&&b&让计算机变成艺术家&/b&&/h2&
&p&前面介绍了多种数据驱动的纹理材质采集方法,但走的都是传统数据驱动的路子——通过分析和利用数据上的一些特殊结构,对数据进行建模,进而减少数据采集的数目。然而,在计算机图形学中,其实还有另外一种纹理材质建模方法,就是&b&用艺术家的手去绘制出所有的纹理材质属性&/b&。现在电影特效和游戏行业中的大部分纹理材质,其实依然是艺术家手工加工绘制出来的。艺术家根据自己的丰富经验,可以从一张简单的照片出发,对照片进行种种处理,最终绘制出纹理材质对应的不同属性,并可以用于图像渲染。随着人工智能、深度学习等技术的飞速发展,现在能不能通过“训练”计算机,让计算机也变成像艺术家一样,可以根据输入的照片就“绘制”出所对应的材质属性呢?答案当然是可以的。但是,这个目标依然面临着很多的技术挑战。&/p&
&p&我们知道,机器学习需要大量的训练数据,只有计算机“学习”了这些训练数据,并掌握了其中的规律,才能做到举一反三,完成给定的智能任务。但是,对于纹理材质属性这一问题而言,我们能够获得的训练数据少之又少。为了能让计算机学会从普通的照片就能绘制出对应的纹理材质,需要大量的与已知纹理材质对应的照片。这个数据集还需要足够大,换句话说,我们需要这个训练数据能够涵盖绝大多数的纹理材质变化。而现有的纹理材质数据库还远远达不到所需要的规模。&/p&
&p&虽然现有的纹理材质数据库非常有限,但是纹理照片却非常多。这些照片虽然没有对应的纹理材质信息,但却也反应了纹理本身的部分信息,尤其是纹理的空间分布基本都被照片所涵盖。为了利用这些海量的未标定的照片来进行机器学习,我们设计了&b&自增强训练的方法(Self-augmented training)&/b&。与传统的机器学习方法一样,我们依然需要训练一个卷积神经网络(CNN),这个CNN的输入是普通的纹理照片,输出则是对应的纹理材质属性。自增强训练采用了一种特殊的CNN训练方式,区别于传统利用标注的输入输出对作为训练数据的训练方法,自增强训练利用当前尚未训练好的CNN来对未标定的数据进行测试。当然,由于现在CNN还没有训练好,测试结果肯定不能作为正确的标定用来训练。但是,我们对材质属性估计这个问题的逆问题,也就是纹理材质属性的渲染,具有完整的知识。因此,可以用这个中间测试结果得到的材质属性,配合现有的材质渲染程序,生成一个当前中间结果的材质属性和其对应的渲染结果(照片)的数据对,这一数据对是完好保持纹理照片和纹理属性这一对应关系的“正确标定数据对”,因此我们就可以放心地利用这一自增强出来的数据来进行训练了。&/p&
&img src=&/v2-a7dcf7e39acd31df0e2d492be8d415e5_b.png& data-rawwidth=&484& data-rawheight=&540& class=&origin_image zh-lightbox-thumb& width=&484& data-original=&/v2-a7dcf7e39acd31df0e2d492be8d415e5_r.png&&&p&图5 自增强训练的基本流程&/p&
&p&虽然这个中间测试结果可能距离真实的材质属性尚有距离,但是这个自增强学习过程是随着CNN的训练更新的。因此,当CNN被训练得更好的时候,我们就可以得到更高质量的自增强数据,可以进一步去改进CNN,进行进一步训练。&/p&
&p&利用这一自增强训练的方法,我们可以用&b&非常少的标定数据,配合海量的未标定数据来进行训练&/b&。仅需要几十个标定过的纹理数据,配合数千张未标定的照片,即可训练出一个能够有效估计具有空间分布的材质纹理(SVBRDF)的CNN。针对某一种特定材料(如金属、木头或塑料),我们的CNN可以根据一张普通的纹理照片估计出漫反射贴图(diffuse map)、法向贴图(normal map)、均质的光滑度(roughness)和高光反射强度(specular coefficient)。&/p&
&p&另外,我们还在单一材质属性(BRDF)估计这一相对简单的问题上,做出了更详细地分析,讨论了自增强训练的优势,以及如何最有效地进行自增强训练等。[Li et al. 2017]&/p&
&h2&&b&总结&/b&&/h2&
&p&通过回顾传统数据驱动的图形学发展过程,以及对最新的利用深度学习方法进行机器学习的介绍,相信大家能够对数据驱动的图形学有了进一步认识,如果你希望能更深入地了解其中所介绍的具体方法,就去仔细研读一下相关的论文吧,相信你一定会有更多收获的。也欢迎大家在文章下方留言与我们交流讨论。&/p&
&h2&&b&参考文献&/b&&/h2&
&p&Wojciech Matusik, Hanspeter Pfister,
Matthew Brand, and Leonard McMillan. 2003. Efficient isotropic BRDF
measurement. In Proceedings of the 14th Eurographics workshop on Rendering
(EGRW '03). Eurographics Association, Aire-la-Ville, Switzerland, Switzerland,
241-247.&/p&
&a href=&/?target=http%3A//people.csail.mit.edu/wojciech/EfficientMeasurement/index.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Efficient Isotropic BRDF Measurement&i class=&icon-external&&&/i&&/a&
&p&Ying Song, Xin Tong, Fabio Pellacini,
and Pieter Peers. 2009. SubEdit: a representation for editing measured
heterogeneous subsurface scattering. In ACM SIGGRAPH 2009 papers (SIGGRAPH
'09), Hugues Hoppe (Ed.). ACM, New York, NY, USA, , Article 31&/p&
&a href=&/?target=https%3A///en-us/research/wp-content/uploads/2016/12/subedit_final-2.pdf& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&https://www.&/span&&span class=&visible&&/en-us/res&/span&&span class=&invisible&&earch/wp-content/uploads/2016/12/subedit_final-2.pdf&/span&&span class=&ellipsis&&&/span&&i class=&icon-external&&&/i&&/a&
&p&Jiaping Wang, Shuang Zhao, Xin Tong,
John Snyder, and Baining Guo. 2008. Modeling anisotropic surface reflectance
with example-based microfacet synthesis. In ACM SIGGRAPH 2008 papers (SIGGRAPH
'08). ACM, New York, NY, USA, , Article 41&/p&
&a href=&/?target=http%3A///projects/aniso-sg08/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Modeling Anisotropic Surface Reflectance with Example-Based Microfacet Synthesis&i class=&icon-external&&&/i&&/a&
&p&Yue Dong, Jiaping Wang, Xin Tong, John
Snyder, Yanxiang Lan, Moshe Ben-Ezra, and Baining Guo. 2010. Manifold
bootstrapping for SVBRDF capture. ACM Trans. Graph. 29, 4, Article 98&/p&
&a href=&/?target=http%3A//yuedong.shading.me/project/bootstrap/bootstrap.htm& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Manifold Bootstrapping for SVBRDF Capture&i class=&icon-external&&&/i&&/a&
&p&Guojun Chen, Yue Dong, Pieter Peers,
Jiawan Zhang, and Xin Tong. 2014. Reflectance scanning: estimating shading
frame and BRDF with generalized linear light sources. ACM Trans. Graph. 33, 4,
Article 117 (July 2014).&/p&
&a href=&/?target=http%3A//yuedong.shading.me/project/refscan/refscan.htm& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Reflectance Scanning: Estimating Shading Frame and BRDF with Generalized Linear Light Sources&i class=&icon-external&&&/i&&/a&
&p&Zhiming Zhou, Guojun Chen, Yue Dong,
David Wipf, Yong Yu, John Snyder, and Xin Tong. 2016. Sparse-as-possible SVBRDF
acquisition. ACM Trans. Graph. 35, 6, Article 189 (November 2016).&/p&
&a href=&/?target=http%3A//yuedong.shading.me/project/sparsesvbrdf/sparsesvbrdf.htm& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Sparse-as-Possible SVBRDF Acquisitions&i class=&icon-external&&&/i&&/a&
&p&Xiao Li, Yue Dong, Pieter Peers, and Xin
Tong. 2017. Modeling Surface Appearance from a Single Photograph using
Self-augmented Convolutional Neural Networks. ACM Trans. Graph. 36, 4, Article
45 (July 2017)&/p&
&a href=&/?target=http%3A///%7Esanet/sanet.htm& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Modeling Surface Appearance from a Single Photograph using Self-augmented Convolutional Neural Networks&i class=&icon-external&&&/i&&/a&
&h2&&b&作者简介&/b&&/h2&
&img src=&/v2-e2b2e14919d81aec50df35e47240b73b_b.png& data-rawwidth=&387& data-rawheight=&581& class=&content_image& width=&387&&&p&董悦,2006年于清华大学自动化系本科毕业后,被清华大学高等研究院-微软亚洲研究院博士生联合培养计划录取,师从导师沈向洋教授学习计算机图形学。博士期间在微软亚洲研究院网络图形组实习。2011年毕业后加入微软亚洲研究院网络图形组。现任微软亚洲研究院网络图形组主管研究员。&/p&
&p&董悦的主要研究方向是计算机图形学中的表观建模方向,集中研究物体表面材质属性的建模、采集、编辑和制造。在其博士学习和微软研究院工作期间,在计算机图形学顶级会议SIGGRAPH、SIGGRAPH Asia发表多篇论文,并有部分技术应用到微软3D打印项目中。&/p&&br&&img src=&/v2-0a73cb8bc4cd2fdaf011405f_b.png& data-rawwidth=&624& data-rawheight=&12& class=&origin_image zh-lightbox-thumb& width=&624& data-original=&/v2-0a73cb8bc4cd2fdaf011405f_r.png&&&br&&p&&strong&微软研究院AI头条&/strong& &/p&
&p&感谢大家的阅读。&/p&
&p&本账号为微软亚洲研究院的官方知乎账号。本账号立足于计算机领域,特别是人工智能相关的前沿研究,旨在为人工智能的相关研究提供范例,从专业的角度促进公众对人工智能的理解,并为研究人员提供讨论和参与的开放平台,从而共建计算机领域的未来。&/p&
&p&微软亚洲研究院的每一位专家都是我们的智囊团,你在这个账号可以阅读到来自计算机科学领域各个不同方向的专家们的见解。请大家不要吝惜手里的“邀请”,让我们在分享中共同进步。&/p&
&p&也欢迎大家关注我们的&a href=&/?target=http%3A//.cn/msra& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&微博&i class=&icon-external&&&/i&&/a&和微信账号,了解更多我们研究。&/p&
如果我告诉你,计算机图形学早在10年前就开始用机器学习了,你可能会不信。毕竟机器学习这个概念是在2012年左右才火起来的,并藉由深度学习的东风一吹,现在颇有燎原之势;计算机图形学本来就跟机器学习八竿子打不着,怎么可能在10年前就做机器学习?
&img src=&/50/v2-5b94ec22b1ff59e9c9c5f0f7bfccc151_b.png& data-rawwidth=&995& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&995& data-original=&/50/v2-5b94ec22b1ff59e9c9c5f0f7bfccc151_r.png&&&p&Fermi是Nvidia在2010年发布的GPU架构,本篇对&a href=&/?target=https%3A///content/life-triangle-nvidias-logical-pipeline& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Life of a triangle - NVIDIA's logical pipeline&i class=&icon-external&&&/i&&/a&提及的Fermi逻辑架构做一点读后笔记。缺乏量化数据,希望能逐渐完善补充。&/p&&h2&&b&&u&1. Overview&/u&&/b&&/h2&&p&&b&1.1 Rendering流程&/b&&/p&&img src=&/50/v2-0fbef800b49c538cf497d4f_b.png& data-rawwidth=&545& data-rawheight=&605& class=&origin_image zh-lightbox-thumb& width=&545& data-original=&/50/v2-0fbef800b49c538cf497d4f_r.png&&&br&&p&从Nvidia给出架构图可以看出Fermi的大致流程:&/p&&ul&&li&API调用-&Driver验证合法性、编译Shader,生成Command-&通过BUS传送到GPU前端进行解析,得到DrawCall的Context和每个顶点的Index。&/li&&li&由Primitive Distributor把Rendering任务分给4个&b&GPC(Graphics Processing Clusters)&/b&,个人揣测分配粒度至少是DrawCall。比如来4个Draw,那么就给4个GPC依次分一个Draw的任务。因为此处还没有Primitive的概念,分配粒度低于Draw意味着得把同一份Context送给多个GPC,更重要的,这可能导致同一个Triangle的顶点分给了不同GPC。&/li&&li&每个GPC依次做Vertex和Geometry的Shading以及Viewport Transform,输出的Primitive来到了&b&Work Distribution Crossbar&/b&(WDC)做重新的分配。屏幕被分成了许多个Tile(每个Tile是个固定大小的矩形)并由4个GPC分管,WDC会check每个triangle的Bounding BOX,对于所有被Cover的Tile,WDC会把该triangle扔给对应的GPC。&/li&&/ul&&img src=&/50/v2-9e97da174a9b22494ba0_b.png& data-rawwidth=&304& data-rawheight=&235& class=&content_image& width=&304&&&br&&p&&b&重新分配的好处在于保持负载均衡。&/b&每个Draw的任务可能是不均匀的,前面4个GPC的VS/GS可能有些很忙碌,有些却快速完成任务显得IDLE,为了避免这种负载不均衡继续下去,WDC是必要的,它保证了Raster和PS是均衡的。Tile大小是个trade-off问题,取较小的Tile能有更好的平衡,当然Tile太小了会导致一个triangle被分到多个GPC,相当于出现额外的Triangle Setup。Fermi的tile大小是4*2[1]。&/p&&p&个人疑问:&b&Nvidia已经支持了Tile-based Rendering了,那么它是在Work Distribution Crossbar上停顿下来做binning,然后等binning结束后把每个tile的primitive list分发给4个GPC?还是先把primitve分发给4个GPC,在GPC内部各自停顿做binning来完成tile-based rendering?&/b&&/p&&br&&ul&&li&每个triangle经过Raster和Attribute Setup,由来到了一条Crossbar,意味着又要重新分配了,因为4个GPC 是share着6个ROP的。&/li&&/ul&&p&这里有个sort问题,因为ROP前的像素孰前孰后必须按照严格的API order。&b&严格讲,Fermi是Sort Middle(在&/b&&b&Work Distribution Crossbar&/b&&b&做的Sort&/b&&b&)[1]&/b&。Work Distribution Crossbar负责将4个GPC送下来的triangle做Sync,并跟据Bounding Box以及4个GPC对Tile的分属,按顺序把三角形分发给4个GPC做Raster。&/p&&p&Fermi把以往的Sort Last(在PS后ROP前才做sort)改成Sort Middle,个人觉得有2个原因:&/p&&p&1) 在WDC做sync(Sort Middle),FIFO放的是Primitive数据,而在ROP前做sync,FIFO放的是Pixel数据,而一个Primitive通常包含多个Pixel的,意味着ROP的FIFO容量要远大于WDC的FIFO。&/p&&br&&p&2) 如果在PS之后才对Pixel重排序,那就意味着PS之后的Z test必须一直开启&b&(即使PS没修改Z也如此)&/b&——举个简单的例子:&/p&&p&第一个draw含两个triangle,T0和T1,crossbar分配给第1个GPC负责执行,VS、GS速度很慢(比如texture cache一直未命中,画上百个cycle(时钟周期)从memory读数据);&/p&&p&第二个draw含两个triangle,T2和T3,crossbar分配给第2个GPC执行,VS、GS速度很快。&/p&&p&那么T2和T3很可能会提前完成Raster和PS,到ROP等待T0和T1,T0,T1跟T2,T3排个队,并且做深度测试。&/p&&p&而这种情况(PS没修改Z),&b&Sort Middle(在Raster前Sync)在ROP是可以不需要做Z test的&/b&。当然Sort Last在GPC内部是可以做early Z的,但比起Sort Middle完全的early Z,Sort Laast的PS的负载要更重些。&/p&&p&&b&1.2 GPC与流处理器&/b&&/p&&br&&p&Fermi架构有4个GPC,&i&&b&一个GPC其实就是由多个(比如GF100是4个)流处理器(SM,Stream Multiprocessor)以及1个光栅化引擎(Raster Engine)构成的一组渲染资源&/b&&/i&。&/p&&p&其中流处理统一为诸如VS(Vertex Shader),GS(Geometry Shader)和PS(Pixel Shader)等可编程模块提供计算,或者说VS/GS/PS运行在流处理上。而Raster Engine是负责执行光栅化的Fixed Function硬件。&/p&&p&而Fermi的流处理器是长这样的:&/p&&img src=&/50/v2-a318db0487fcd12a8f11bf111bc6ee9e_b.png& data-rawwidth=&226& data-rawheight=&591& class=&content_image& width=&226&&&p&&b&1.2.1 Warp、Warp Scheduler &/b&&/p&&p&Fermi把32个thread作为一个Warp,这里thread即一个Shader示例,负责对一个顶点/像素做shading。作为Unify架构,流处理器32个core是供VS/GS/PS共用,因此流处理器需要决定哪些Warp可以占用Core,这就是Warp Scheduler。&/p&&p&一个流处理器有2个Warp Scheduler,core也被分成2组,每组16个core,因此一个Warp分两个cycle执行完一条指令。从Fermi的文档看,Warp Scheduler的准入容量是48个Warp。&br&&/p&&br&&p&&b&1.2.2&/b& &b&LD/ST&/b&&/p&&p&16个LD/ST辅助模块,可满足一个Warp从Share Memory或Video Memory加载(Load)或存储(Store)数据。&/p&&p&&b&1.2.3 SFU&/b&&/p&&p&Fermi有4个SFU,负责计算特殊的ALU运算,如SIN、COS,平方根等等,另外SFU还负责为每个像素插值。&/p&&p&&b&1.2.4 Texture Unit、Texture Cache&/b&&/p&&p&Fermi有4个texture units,每个texture unit在一个cycle最多可取4个sample,这时刚好喂给一个Warp(的16个车道),每个texture uint有16K的texture cache,并且在往下有L2的支持。&/p&&p&&b&1.2.5 Dispatch Unit&/b&&/p&&p&48个Warp是以SIMT(单指令多线程)的方式同时运行在16个core上的,每个Warp在Register File都有自己的一份。在每个cycle,Dispatch Unit负责决定何个Warp的何条指令来使用这16个core,执行一条指令。&/p&&p&&b&1.2.6 PolyMorph Engine&/b&&br&&/p&&p&除了Shading相关的资源,图中还包括了5个Fixed Function模块——Vertex Fetch, Tessellator,Viewport Transform(Clipping应该也包括在里头),Attribute Setup,Stream Output也含在其中,合称PolyMorph Engine(多形体引擎)。Fermi把这些Fixed Function模块放到一个Pool来Share。&/p&&p&&b&1.3 存储介质&/b&&/p&&p&&b&1.3.1 Register Files&/b&&/p&&p&一块on-chip的SRAM,当一个Warp被Warp Schedule接受时,Warp Schedule将从Register Files中为其分配一组物理寄存器,知道Warp运行结束后才释放回收。每个core(lane,车道)上运行的thread拥有自己的一份寄存器。类似CPU,从寄存器加载或存储到寄存器只需一个或几个cycle。&/p&&p&Fermi有32768个寄存器,按48个Warp算的话,平均每个thread可有=21个。&/p&&p&&b&1.3.2 &/b&&b&L1 Cache和Share Memory&/b&&/p&&p&Fermi有一个64K的on-chip SRAM作为L1 Cache,L1 Cache有两种使用模式&/p&&p&1)48K数据缓存+16K Share Memory&/p&&p&2)16K数据缓存+48K Share Memory&/p&&p&数据缓存pipeline中临时Geometry数据,Share Memory一般作为通用计算中Warp的每条thread共享存储,交流数据。&/p&&p&&img src=&/50/v2-9284aa61bcd51c2e8bfba_b.png& data-rawwidth=&566& data-rawheight=&217& class=&origin_image zh-lightbox-thumb& width=&566& data-original=&/50/v2-9284aa61bcd51c2e8bfba_r.png&&&b&1.3.3 L2 Cache&/b&&br&&/p&&img src=&/50/v2-36df72c0caea_b.png& data-rawwidth=&300& data-rawheight=&219& class=&content_image& width=&300&&&br&&p&Fermi有768K的L2 Cache,作用与CPU类似,到DRAM(Memory)读数据需要几百个cycle,L1 Cache不够存储量不够大的情况下,通过L2提高命中率减少些访问时间。&/p&&p&Fermi的一个特点是砍掉了许多模块间的FIFO,并用L2代替。&br&&/p&&br&&p&&b&Reference&/b&&/p&&p&&b&[1]&/b&&a href=&/?target=https%3A///content/reviews/55/6& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&NVIDIA Fermi GPU and Architecture Analysis&i class=&icon-external&&&/i&&/a&&/p&&p&&b&[2]&/b&计算机体系结构量化研究方法(第5版)&/p&
Fermi是Nvidia在2010年发布的GPU架构,本篇对提及的Fermi逻辑架构做一点读后笔记。缺乏量化数据,希望能逐渐完善补充。1. Overview1.1 Rendering流程 从Nvidia给出架构图可以看出Fermi的大致流程:API调用-&…
&img src=&/50/v2-50f31b17e55acdfd0ee88f_b.png& data-rawwidth=&1600& data-rawheight=&900& class=&origin_image zh-lightbox-thumb& width=&1600& data-original=&/50/v2-50f31b17e55acdfd0ee88f_r.png&&&h2&今天要学习的是游戏开发中,特别是gameplay开发中非常重要的部分
– 骨骼动画。&/h2&
&p&首先会学习一些原理,还有动画制作的pipeline,实践方面会包括动画加载,GPU Skinning,animation blend, Addittive animation等等。&/p&
&p&Tool: VS2015 + blender 2.78 + OpenGL + SDL2&/p&&br&&h2&&b&Keyframe
animation 和Skeletal
animation&/b&&/h2&
&p&Keyframe定义:在关键的几个时间点定义主体的姿态,称为关键帧,&/p&
&img src=&/v2-3ccae08dbb1_b.png& data-rawwidth=&628& data-rawheight=&263& class=&origin_image zh-lightbox-thumb& width=&628& data-original=&/v2-3ccae08dbb1_r.png&&&p&中间的部分用插值(Linear/Spline)得出&/p&
&img src=&/v2-760c5ece0b850e9fff2d7640a4aee9bb_b.png& data-rawwidth=&620& data-rawheight=&282& class=&origin_image zh-lightbox-thumb& width=&620& data-original=&/v2-760c5ece0b850e9fff2d7640a4aee9bb_r.png&&&p&在游戏中,KeyFrame就是指的是一系列Mesh的顶点位置,Mesh的当前位置信息是通过上一个keyframe和下一个keyframe插值而来。在Uncharted4里面,有使用Keyframe animation来做人群动画的。&/p&
&img src=&/v2-3f829d52eca499b90bcb_b.png& data-rawwidth=&1285& data-rawheight=&723& class=&origin_image zh-lightbox-thumb& width=&1285& data-original=&/v2-3f829d52eca499b90bcb_r.png&&&p&相对于Keyframe animation ,Skeletal animation的思想是将有动画的物体分为两个部分:用于渲染的Mesh 和
用与运动的骨骼(通常称为&i&skeleton&/i& 或者 &i&rig&/i&)。Mesh上的顶点和骨骼通常存在着一对多的对应关系,当骨骼发生transform的变化的时候,Mesh上的顶点会根据对应关系得出新的位置。&/p&
&img src=&/v2-348cdbbedadc6d013ac187_b.jpg& data-rawwidth=&1030& data-rawheight=&559& class=&origin_image zh-lightbox-thumb& width=&1030& data-original=&/v2-348cdbbedadc6d013ac187_r.jpg&&&p&在游戏引擎架构这本书中,一个很有用的思想就是将skeletal animation视为一种数据压缩技术。选择动画技术的目的,是能够提供最佳压缩而又不会产生不能接受的视觉瑕疵,KeyFrameAnimation中的动画数据当然是相当巨大的,骨骼动画就能提供最佳的压缩,因为每个关节的移动会扩大至多个顶点的移动。&/p&
&h2&&b&Character
Creation Pipeline&/b&&/h2&
&p&在深入技术之前,我们先来了解Character Creation Pipeline。一下一个常规的Character制作的pipeline大概是这样&/p&
&img src=&/v2-99ee8fa309d03a91ba84d3d27e4eeabc_b.png& data-rawwidth=&1602& data-rawheight=&1238& class=&origin_image zh-lightbox-thumb& width=&1602& data-original=&/v2-99ee8fa309d03a91ba84d3d27e4eeabc_r.png&&&p&这些阶段有些是互相依赖的,有些又是可以并行的,每个阶段的详细说明可以参考&a href=&/?target=https%3A////week-two-research/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&这里&i class=&icon-external&&&/i&&/a&。&/p&
&p&建模就不说了,和动画相关的主要是后面的三个个部分,第一个部分为Rigging,主要是根据模型还有角色可能要做的动作制作出一套对应的骨骼,这套骨骼包含了一些joints和bones,同时还定义了joints的自由度,约束,还包括了一些IK等等。通常在建模的时候,建模师会将角色摆成一个Binding Pos,也叫T pos,目的是方便做Rig的人,因为这样放joints和bones的时候会更方便。&/p&&br&&p&&img src=&/v2-f330a1ac42acb48c2cfb6871_b.png& data-rawwidth=&1080& data-rawheight=&630& class=&origin_image zh-lightbox-thumb& width=&1080& data-original=&/v2-f330a1ac42acb48c2cfb6871_r.png&&之所以叫BingPos,是因为是在这个Pos下进行Mesh和骨骼的Binding。&/p&
&p&Skinng就是就Mesh的顶点绑定到对应的骨骼的上,当骨骼运动的时候,Mesh会根据绑定的骨骼运动到相应的位置。关于Binding Pos更详细的解释可以参考:&a href=&/?target=http%3A//peyman-mass.blogspot.jp/2013/09/what-is-binding-pose-in-character.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&What Is a Binding Pose in Character Animation?&i class=&icon-external&&&/i&&/a&&/p&
&p&在完成Rigging和Skinning之后,动画师就可以去k动画了,也可能通过一些高级手段,比如动作捕捉来制作动画。&/p&
&br&&p&&b&Md5&/b&&b&格式说明&/b&&/p&
&p&Md5是ID software所推出的一种动画格式。一个包含动画的md5资源包含了两个文件:&/p&
&p&.md5mesh 文件:定义了mesh和材质还有骨骼信息.&/p&
&p&.md5anim文件: 定义了一段对应于md5mesh文件的动画.&/p&
&p&注意,两个文件中的骨骼信息必须一致。&/p&
&p&对于这两个格式的详细ie&/p&
&p&一个md5mesh的格式如下&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&MD5Version &int:version&
commandline &string:commandline&
numJoints &int:numJoints&
numMeshes &int:numMeshes&
&string:name& &int:parentIndex& ( &vec3:position& ) ( &vec3:orientation& )
shader &string:texture&
numverts &int:numVerts&
vert &int:vertexIndex& ( &vec2:texCoords& ) &int:startWeight& &int:weightCount&
numtris &int:numTriangles&
tri &int:triangleIndex& &int:vertIndex0& &int:vertIndex1& &int:vertIndex2&
numweights &int:numWeights&
weight &int:weightIndex& &int:jointIndex& &float:weightBias& ( &vec3:weightPosition& )
&/code&&/pre&&/div&&br&&p&有点像obj文件,但是还包含了joint的信息和每个顶点的权重信息。&/p&
&p&一个md5anim文件如下&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&MD5Version &int:version&
commandline &string:commandline&
numFrames &int:numFrames&
numJoints &int:numJoints&
frameRate &int:frameRate&
numAnimatedComponents &int:numAnimatedComponents&
hierarchy {
&string:jointName& &int:parentIndex& &int:flags& &int:startIndex&
( vec3:boundMin ) ( vec3:boundMax )
baseframe {
( vec3:position ) ( vec3:orientation )
frame &int:frameNum& {
&float:frameData& ...
&/code&&/pre&&/div&&p&其中Hierarchy定义了骨骼的父子结构。Bounds定义了每一帧mesh的aabb。Baseframe定义了骨骼的初始位置。Frame定义了每一帧的骨骼信息。&/p&
&p&文件的加载可以自己去写paser,这里为了方便起见就直接用assimp来处理。但是assimp加载还是有很多隐晦的默认规则,比如&/p&
如果load(”soldier.md5mesh “),默认会把加载一个soldier.md5anim的文件,所以如果要加载多个anim文件,就要手动去指定;&/p&
如果shader那一行制定的贴图没有扩展名,则默认为name_d.tga为diffuse贴图。&/p&&br&&h2&&b&骨骼和矩阵数学&/b&&/h2&
&p&一个带骨骼的mesh如下图&/p&&img src=&/v2-ea67ebdfdb4b1dcb04a110_b.png& data-rawwidth=&1075& data-rawheight=&577& class=&origin_image zh-lightbox-thumb& width=&1075& data-original=&/v2-ea67ebdfdb4b1dcb04a110_r.png&&&br&&p&骨骼可理解为一个坐标空间,关节可理解为骨骼坐标空间的原点。关节的位置由它在父骨骼坐标空间中的位置描述。&/p&
&p&一个bone可以定义如下&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&class Bone
Bone(const SkeletonNodeData& node);
unsigned int m_parentID;
vector&unsigned int& m_childID;
glm::mat4 m_
&/code&&/pre&&/div&&br&&p&骨骼就是坐标空间,骨骼层次就是嵌套的坐标空间。关节只是描述骨骼的位置即骨骼自己的坐标空间原点在其父空间中的位置,绕关节旋转是指骨骼坐标空间(包括所有子空间)自身的旋转。&/p&
&p&直接看一下Gpu
skinning 的 vertex shader&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&#version 330 core
//skinningShader.vert
const int MAX_BONES = 100;
uniform mat4 modelM
uniform mat4 projModelViewM // (projection x view x model) matrix
uniform mat3 normalM
uniform mat4 lightProjModelViewM
uniform mat4 boneMatrix[MAX_BONES];
in vec3 in_
in vec3 in_
in vec2 in_textC
in ivec4 in_boneIDs;
vec2 textC
vec4 lightVertexP //position of vertex in light space.
void main()
mat4 boneTransform = boneMatrix[in_boneIDs[0]] * in_weights[0];
boneTransform += boneMatrix[in_boneIDs[1]] * in_weights[1];
boneTransform += boneMatrix[in_boneIDs[2]] * in_weights[2];
boneTransform += boneMatrix[in_boneIDs[3]] * in_weights[3];
//转换到bone 空间下
vec4 vertex4 = boneTransform * vec4(in_position, 1.0);
DataOut.position = vec3(boneTransform * modelMatrix * vertex4);
DataOut.normal = normalize( mat3(boneTransform) * normalMatrix * in_normal);
DataOut.textCoord = in_textC
//转换到世界空间下
DataOut.lightVertexPosition = lightProjModelViewMatrix * vertex4;
gl_Position = projModelViewMatrix * vertex4;
&/code&&/pre&&/div&&br&&p&简直超级简单!就是&/p&
&p&最终位置
= 原始位置 * 求和(矩阵* 权重)&/p&
&p&这个过程就是GPU
Skinning。这里注意到GPU Skinning的一个小小的限制,就是顶点的权重数不超过4,这对于一般的游戏来说已经足够了。(或者传两个vec4来处理多于4个权重数的情况)&/p&
&p&空间变化应当是:&/p&
&p&&b&Model space
-& Bone Space -&World Space&/b&&/p&
&p&Shader中boneMatrix
就是经插值之后得到的matrix矩阵了。&/p&
&p&再看下keyframe,每一个keyframe其实就是一堆骨骼的position和rotation的组合,动画中的每一秒都有24个这样的keyframe&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&class KeyFrame
KeyFrame(double time,
const vector&glm::vec3&& boneTranslation,
const vector&glm::quat&& boneRotation);
inline double getTime() const { return m_ }
inline unsigned int getBoneCount() const { return m_boneTranslation.size(); }
inline const glm::vec3& getBoneTranslation(unsigned int boneID) const
return m_boneTranslation[boneID];
inline const glm::quat& getBoneRotation(unsigned int boneID) const
return m_boneRotation[boneID];
vector&glm::vec3& m_boneT
vector&glm::quat& m_boneR
&/code&&/pre&&/div&&br&&p&在读取的时候MD5anim文件的的时候,每一帧中每个骨骼对应的是六个数字,&/p&
&p&( vec3:position ) ( vec3:orientation
&p&对应的分别是vector3的位置,还有一个四元素表示旋转,其中旋转只给了三个分量,最后一个需要再加载的时候计算出来。&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void ComputeQuatW( glm::quat& quat )
float t = 1.0f - ( quat.x * quat.x ) - ( quat.y * quat.y ) - ( quat.z * quat.z );
if ( t & 0.0f )
quat.w = 0.0f;
quat.w = -sqrtf(t);
&/code&&/pre&&/div&&br&&p&看一下插值计算的过程,注意,这里的计算一般是放在Cpu处理的.为了计算出某个时刻骨骼最终的变换矩阵,我们需要根据当前时间对关键帧之间三个变换数组进行插值,并将这些变换组合成一个矩阵。这些完成之后我们需要在骨骼树种找到对应的骨骼节点并遍历其父节点,之后我们对它的每个父节点都做同样的插值处理,并将这些变换矩阵乘起来即可。对应的函数实现如下&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void AnimatedMeshGL::getBoneTransformation(double timeEllasped, const Skeleton& skeleton, AnimationComponent& animComponent, vector&glm::mat4&& finalTransformList)
//获取当前的动画序列
auto& anim = skeleton.getAnimation(animComponent.m_animationIndex);
unsigned int keyFrameCount = anim.getKeyFrameCount();
double ticksPerSecond = anim.getTicksPerSecond() != 0 ? anim.getTicksPerSecond() : 25.0f;
//总共运行的帧数
double timeInTicks = timeEllasped * ticksPerS
//取余操作
double animationTime = fmod(timeInTicks, anim.getDuration());
//算出当前动画的上一帧和下一帧,后面用
for (unsigned int i = animComponent.m_startFrameI i & keyFrameCount - 1; i++)
if (animationTime & anim.getKeyFrame(i + 1).getTime()) {
animComponent.m_startFrameIndex =
animComponent.m_endFrameIndex = (animComponent.m_startFrameIndex + 1) % keyFrameC
//计算要插值的时间点
double deltaTime = (anim.getKeyFrame(animComponent.m_endFrameIndex).getTime() - anim.getKeyFrame(animComponent.m_startFrameIndex).getTime());
animComponent.m_remainingTime = (animationTime - anim.getKeyFrame(animComponent.m_startFrameIndex).getTime()) / deltaT
glm::mat4 identity(1.0f);
transformBone(0, identity, skeleton, animComponent);
//将计算的结果保存一下
for (unsigned int i = 0, maxBones = m_boneFinalTransform.size(); i & maxB i++)
finalTransformList[i] = m_boneFinalTransform[i];
//最后一帧和第一帧重合
if (animComponent.m_startFrameIndex == keyFrameCount - 2)
animComponent.m_startFrameIndex = 0;
&/code&&/pre&&/div&&br&&p&关键函数transformBone&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void AnimatedMeshGL::transformBone(unsigned int boneID, const glm::mat4& parentTransform, const Skeleton& skeleton, const AnimationComponent& animComponent)
//从0号bone开始递归计算,初始parentTransform为单位阵
const Bone& node = skeleton.getBoneNode(boneID);
//取出上一帧和下一帧的信息
const Animation& animation = skeleton.getAnimation(animComponent.m_animationIndex);
const KeyFrame& startKeyframe = animation.getKeyFrame(animComponent.m_startFrameIndex);
const KeyFrame& endKeyframe = animation.getKeyFrame(animComponent.m_endFrameIndex);
//取出根骨的offset matrix
const glm::mat4& inverseTransform = skeleton.getInverseTransform();
//骨骼的当前位置
glm::mat4 nodeTransform = node.m_
unsigned int boneIndex = animation.getBoneIndex(node.m_name);
if (boneIndex != ~0)
//m_remainingTime作为插值的参数
float factor = float(animComponent.m_remainingTime);
//四元素的slerp对旋转进行插值
auto rotQuat = glm::slerp(startKeyframe.getBoneRotation(boneIndex),
endKeyframe.getBoneRotation(boneIndex), factor);
//Vector3的插值得到位置
auto posVec = BlendVec3(startKeyframe.getBoneTranslation(boneIndex),
endKeyframe.getBoneTranslation(boneIndex), factor);
//组合出boone矩阵
glm::mat4 translation = glm::translate(glm::mat4(), posVec);
nodeTransform = translation * glm::toMat4(rotQuat);
//和父骨骼的矩阵相乘
glm::mat4 localFinalTransform = parentTransform * nodeT
//计算最终的transform
auto iter2 = m_boneNameToIndex.find(node.m_name);
if (iter2 != m_boneNameToIndex.end())
//boneOffset是每个骨骼的offetMatrix 后面有说明
glm::mat4 boneOffset = glm::make_mat4(m_bone[iter2-&second].m_offsetMatrix.m_m16);
m_boneFinalTransform[iter2-&second] = inverseTransform * localFinalTransform * boneO
//递归处理child bone
for (unsigned int i = 0; i & node.m_childID.size(); i++)
transformBone(node.m_childID[i], localFinalTransform, skeleton, animComponent);
&/code&&/pre&&/div&&br&&p&注意其中的插值是分别对position,rotation进行插值。&/p&
&p&由于骨骼的 Transform Matrix (作用是将顶点从骨骼空间变换到上层空间)是基于其父骨骼空间的,只有根骨骼的 Transform 是基于世界空间的,所以要通过自下而上一层层 Transform 变换(如果使用行向量右乘矩阵,这个 Transform 的累积过程就是C=Mbone * &em&Mfather * &/em&Mgrandpar *&em&... *&/em&Mroot
) , 得到该骨骼在世界空间上的变换矩阵 - Combined Transform Matrix ,即通过这个矩阵可将顶点从骨骼空间变换到世界空间。那么这个矩阵的逆矩阵就可以将世界空间中的顶点变换到某块骨骼的骨骼空间。由于 Mesh 实际上就是定义在世界空间了,所以这个逆矩阵就是Bone Offset Matrix 。即Bone OffsetMatrix 就是骨骼在初始位置(没有经过任何动画改变)时将 bone 变换到世界空间的矩阵( CombinedTransformMatrix )的逆矩阵。&/p&
&p&从结构上看, skeletal Animation的输入主要有:动画数据,骨骼数据,包含
Skin info 的 Mesh 数据,以及 Bone Offset Matrix 。&/p&
&p&从过程上看,载入阶段:载入并建立骨骼层次结构,计算或载入
Bone Offset Matrix ,载入
Mesh 数据和 Skin info (具体的实现 不同的引擎中可能都不一样)。运行阶段:根据时间从动画数据中获取骨骼当前时刻的
Transform Matrix ,调用
UpdateBoneMatrix 计算出各骨骼的
CombinedMatrix ,对于每个顶点根据 Skin info 进行 Skinning 计算出顶点的世界坐标,最终进行模型的渲染。&/p&
&p&最终效果(假装在动)&/p&&img src=&/v2-8e61b41bbea53ade370c6d58fb0b25ad_b.png& data-rawwidth=&1616& data-rawheight=&938& class=&origin_image zh-lightbox-thumb& width=&1616& data-original=&/v2-8e61b41bbea53ade370c6d58fb0b25ad_r.png&&&br&&h2&&b&调整播放速度&/b&&/h2&
&p&每个动画片段都有一个局部时间线,里面有动画开始播放的时间,时间缩放比例R。通过调整时间比例R,就可以达到控制动画播放缩率的效果。&/p&
&p&具体来说,对于每个Animation
Clip都有一个Animation Component用于记录播放的信息&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&struct AnimationComponent
AnimationComponent(unsigned int animationIndex, unsigned int startFrameIndex);
unsigned int m_animationI
unsigned int m_startFrameI
unsigned int m_endFrameI
double m_StartTimeM
double m_PlayS
double m_remainingT
&/code&&/pre&&/div&&br&&p&再采样动画的时候,将全局的时间映射到局部的时间&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&double ticksPerSecond = anim.getTicksPerSecond() != 0 ? anim.getTicksPerSecond() : 25.0f;
double timeInTicks = timeEllasped * ticksPerSecond* animComponent.m_PlayS
&/code&&/pre&&/div&&br&&p&以0.25倍的速度播放相同的动画效果如下&/p&
&img src=&/v2-9f4e80dabb37c206da71111_b.jpg& data-rawwidth=&560& data-rawheight=&314& data-thumbnail=&/v2-9f4e80dabb37c206da71111_b.jpg& class=&origin_image zh-lightbox-thumb& width=&560& data-original=&/v2-9f4e80dabb37c206da71111_r.gif&&&h2&&b&Animation
blending&/b&&/h2&
&p&动画混合是把两个或更多的输入姿势结合,产生骨骼的输出姿势,这样就可以再不添加新动画的情况下产生一些新的动画,所需要付出的只是CPU上一些消耗。原理就是插值。对于两个输入姿势Pa和Pb的情况,最终姿势&/p&
&p&P = (1-b)Pa + bPb&/p&
&p&其中b为混合百分比,取值为(0,1).这里所说的姿势插值主要是一个4*4的矩阵进行插值,矩阵当然不能直接插值,所以要对位移和旋转分别进行插值(有些引擎还会有scale插值),位置的插值用的Vector3::Lerp,旋转就用四元素的SLerp。&/p&
&p&下面以两个&b&Animation blending&/b&简单的应用来实践一下。&/p&
&p&&b&Animation Crossfade&/b&&/p&
&p&动画的淡入淡出通常会应用在两个动画的切换,&/p&
&p&关键函数贴一下&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void AnimatedMeshGL::transformBoneBlend(unsigned int boneID, const glm::mat4& parentTransform, const Skeleton& skeleton, const AnimationComponent& animComponent1, const AnimationComponent& animComponent2, float blendFactor)
const Bone& node = skeleton.getBoneNode(boneID);
const Animation& animation1 = skeleton.getAnimation(animComponent1.m_animationIndex);
const KeyFrame& startKeyframe1 = animation1.getKeyFrame(animComponent1.m_startFrameIndex);
const KeyFrame& endKeyframe1 = animation1.getKeyFrame(animComponent1.m_endFrameIndex);
const Animation& animation2 = skeleton.getAnimation(animComponent2.m_animationIndex);
const KeyFrame& startKeyframe2 = animation2.getKeyFrame(animComponent2.m_startFrameIndex);
const KeyFrame& endKeyframe2 = animation2.getKeyFrame(animComponent2.m_endFrameIndex);
const glm::mat4& inverseTransform = skeleton.getInverseTransform();
glm::mat4 nodeTransform = node.m_
unsigned int boneIndex = animation1.getBoneIndex(node.m_name);
if (boneIndex != ~0)
float factor1 = float(animComponent1.m_remainingTime);
auto rotQuat1 = glm::slerp(startKeyframe1.getBoneRotation(boneIndex),
endKeyframe1.getBoneRotation(boneIndex), factor1);
auto posVec1 = BlendVec3(startKeyframe1.getBoneTranslation(boneIndex),
endKeyframe1.getBoneTranslation(boneIndex), factor1);
float factor2 = float(animComponent2.m_remainingTime);
auto rotQuat2 = glm::slerp(startKeyframe2.getBoneRotation(boneIndex),
endKeyframe2.getBoneRotation(boneIndex), factor2);
auto posVec2 = BlendVec3(startKeyframe2.getBoneTranslation(boneIndex),
endKeyframe2.getBoneTranslation(boneIndex), factor2);
auto posVec = BlendVec3(posVec1, posVec2, blendFactor);
auto rotQuat = glm::slerp(rotQuat1, rotQuat2, blendFactor);
glm::mat4 translation = glm::translate(glm::mat4(), posVec);
nodeTransform = translation * glm::toMat4(rotQuat);
glm::mat4 localFinalTransform = parentTransform * nodeT
auto iter2 = m_boneNameToIndex.find(node.m_name);
if (iter2 != m_boneNameToIndex.end())
glm::mat4 boneOffset = glm::make_mat4(m_bone[iter2-&second].m_offsetMatrix.m_m16);
m_boneFinalTransform[iter2-&second] = inverseTransform * localFinalTransform * boneO
for (unsigned int i = 0; i & node.m_childID.size(); i++)
transformBoneBlend(node.m_childID[i], localFinalTransform, skeleton, animComponent1, animComponent2, blendFactor);
&/code&&/pre&&/div&
&p&相比于之前单个动画播放的函数只是再对应的地方添加了插值处理。看一下结果。&/p&
&p&现在要实现行走到下蹲的blend,再没有blend处理的情况下,效果是这样的&/p&&img src=&/v2-b02af95ee41_b.png& data-rawwidth=&1600& data-rawheight=&900& class=&origin_image zh-lightbox-thumb& width=&1600& data-original=&/v2-b02af95ee41_r.png&&&p&可以看在动画切换的时候,右脚被直接掰回来了,显得很不自然。加了blend的动作会通过插值生成中间的动画,比如下面的第二帧图片&/p&
&img src=&/v2-50f31b17e55acdfd0ee88f_b.png& data-rawwidth=&1600& data-rawheight=&900& class=&origin_image zh-lightbox-thumb& width=&1600& data-original=&/v2-50f31b17e55acdfd0ee88f_r.png&&&p&在第三人称角色控制中经常会遇到的一个问题就是走路和跑步的动作blend,假设走路的动画是一个3s的循环,跑步的是一个2s的循环,那么他们两个之间的blend就没那么简单了,需要考虑的问题有1)走路的过程中需要随时可以切换到run的动画 2)blend的时候,要保证出的脚是一致的。要做到这两点就要用到归一化时间的概念。&/p&
&p&对于一个Animation
Clip,无论它的时常T是多长,u = 0代表动画开始,u=1代表动画结束。在blend的时候,将walk的归一化时间和run的归一化时间匹配上,就可以做到完美的匹配。&/p&
&br&&p&&b&Additive
animation&/b&&/p&
&p&首先看下定义,对于两个输入片段S(SourceClip)和参考片段R(ReferenceClip),可以通过减法得到区别片段D(DifferenceClip),有D = S-R.&/p&
&p&得出差别动画之后,就可以将D按一定百分比混合到任意的不相干的动画片段上,而不仅限于原来的参考片段。比如参考片段是角色征程跑步,而来源片段是疲惫下跑步,那么区别片段只含有角色在疲惫的动画,若将此片段应用至步行,结果会是一个疲惫下步行的结果。&/p&
&p&下图是神海2中将两个动画通过Additive blend 的方式生成新的Idle动画。&/p&
&img src=&/v2-73ac2ecc6cfdbfff1747a3_b.jpg& data-rawwidth=&638& data-rawheight=&479& class=&origin_image zh-lightbox-thumb& width=&638& data-original=&/v2-73ac2ecc6cfdbfff1747a3_r.jpg&&&p&在blend中随便制做个前俯后仰的动画,&/p&&img src=&/v2-561be38d4363a7ada02b707fcab7bb5f_b.png& data-rawwidth=&739& data-rawheight=&397& class=&origin_image zh-lightbox-thumb& width=&739& data-original=&/v2-561be38d4363a7ada02b707fcab7bb5f_r.png&&&img src=&/v2-bbdd1a30b8c0c9c8fba1_b.png& data-rawwidth=&769& data-rawheight=&435& class=&origin_image zh-lightbox-thumb& width=&769& data-original=&/v2-bbdd1a30b8c0c9c8fba1_r.png&&&p&这里我们用一个最简单的方法来处理S和R – 导出的动画就是S,R就是动画的第一帧。&/p&
&p&Unity貌似也是这样的处理方法&/p&
&p&&i&Additive animations in Unity are always relative to the
first frame of the animation. This means that you will sometimes need to use
more animations, or do a little more scripting than you would otherwise have
done, but you should always be able to obtain the same results in the end as if
you could have specified any frame as the reference.&/i&&/p&
&p&实现上其实非常简单,
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&float factor1 = float(currentComponent.m_remainingTime);
auto rotQuat1 = glm::slerp(startKeyframe1.getBoneRotation(boneIndex),
endKeyframe1.getBoneRotation(boneIndex), factor1);
auto posVec1 = BlendVec3(startKeyframe1.getBoneTranslation(boneIndex),
endKeyframe1.getBoneTranslation(boneIndex), factor1);
glm::vec3 sourceVec = sourceFrame.getBoneTranslation(boneIndex);
glm::quat sourceRot = sourceFrame.getBoneRotation(boneIndex);
float factor2 = float(addComponent.m_remainingTime);
glm::quat rotQuat2 = glm::slerp(startKeyframe2.getBoneRotation(boneIndex),
endKeyframe2.getBoneRotation(boneIndex), factor2);
auto posVec2 = BlendVec3(startKeyframe2.getBoneTranslation(boneIndex),
endKeyframe2.getBoneTranslation(boneIndex), factor2);
glm::quat rotDiff = rotQuat2 * glm::inverse(sourceRot);
glm::vec3 posDiff = posVec2 - sourceV
auto rotQuat = rotQuat1* rotD
auto posVec = posVec1 + posD
&/code&&/pre&&/div&
&p&效果&/p&
&img src=&/v2-3cc1ebd023d36ced2d2c29e_b.png& data-rawwidth=&1600& data-rawheight=&900& class=&origin_image zh-lightbox-thumb& width=&1600& data-original=&/v2-3cc1ebd023d36ced2d2c29e_r.png&&&p&同样还有使用additive animation的应用有tps游戏里的瞄准混合等等。&/p&
&p&和additive blend类似有一个混合方式是Partial blend,指的是身体的不同部位通过mask标记播放不同的动画,比如各种情况下的挥手动作。Unity中对应的是Animation 中 Layermask 的使用。Partial blend 有两个比较明显的缺点:&/p&&p&1)两个部分的动画因为混合因子改变剧烈(0,1)动画会看上去很突兀。&/p&
&p&2)现实中人体的动作并不是完全独立的,即晃动手臂的同时,其他的骨骼也会有相应的动画。&/p&
&p&所以通常会混合使用多种blend来处理角色动画。&/p&
&p&下面是uncharted2中是动画层&/p&
&img src=&/v2-e008f09d508b3a363ebe5a3d_b.png& data-rawwidth=&634& data-rawheight=&469& class=&origin_image zh-lightbox-thumb& width=&634& data-original=&/v2-e008f09d508b3a363ebe5a3d_r.png&&&p&&b&小结&/b&&/p&
&p&动画作为GamePlay的基石,在国内的游戏开发中通常得不到太大的重视,然后在3A游戏的制作中,通常都会有一个专门的动画团队,甚至有专门负责动画的TA,所以如果想把gameplay这块的东西做好,细节做到位,建议还是认真了解下动画的原理,制作的pipeline。&/p&
&p&写这篇东西花了很长的时间,一方面是最近事情比较多,另一方面,动画这块的东西实在非常之多,上面所写的内容只是动画里面非常小的一些东西,很多内容,比如数据压缩,Procedual Animation 都没有提及,对于想深入了解引擎内部Animation实现的同学,非常建议认真读一下Game Engine Architecture 中关于骨骼动画的内容,书的作者就是做动画系统出身的。最后放个彩蛋&/p&&img src=&/v2-d3d5c913d3a7cafbd935_b.png& data-rawwidth=&1597& data-rawheight=&899& class=&origin_image zh-lightbox-thumb& width=&1597& data-original=&/v2-d3d5c913d3a7cafbd935_r.png&&&br&&p&&b&参考&/b&&/p&
&p&Fast Skinning March 21st 2005 J.M.P. van Waveren ?
2005, Id Software, Inc.ipeLibne&/p&
&p&Fast Skinning March 21st 2005 J.M.P. van Waveren ?
2005, Id Software, Inc.ipeLibne&/p&&p&Game Engine Architecture&/p&&a href=&/?target=https%3A//lwjglgamedev.gitbooks.io/3d-game-development-with-lwjgl/content/chapter19/chapter19.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Animations
in lwjgl&i class=&icon-external&&&/i&&/a&
\\&p&&a href=&/?target=http%3A///archives/opengl/model-md5-format-import-animation-2.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&MD5模型的格式、导入与顶点蒙皮式骨骼动画II&i class=&icon-external&&&/i&&/a& \\&/p&&p&&a href=&/?target=http%3A///archives/opengl/model-md5-format-import-animation-2.html& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&http://www.&/span&&span class=&visible&&/archives/ope&/span&&span class=&invisible&&ngl/model-md5-format-import-animation-2.html&/span&&span class=&ellipsis&&&/span&&i class=&icon-external&&&/i&&/a&&a href=&/?target=https%3A//research.ncl.ac.uk/game/mastersdegree/graphicsforgames/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Graphics
for Games&i class=&icon-external&&&/i&&/a& \\&/p&&p&&a href=&/?target=https%3A///doom3/code.php& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&MAKING
DOOM 3 MODS : THE CODE&i class=&icon-external&&&/i&&/a& \\ &/p&&p&&a href=&/?target=https%3A////doom-md5-model-loader/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Doom
md5 Model Loader&i class=&icon-external&&&/i&&/a& \\&/p&&a href=&/?target=https%3A///loading-and-animating-md5-models-with-opengl/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Loading
and Animating MD5 Models with OpenGL &i class=&icon-external&&&/i&&/a&&p&&a href=&/?target=https%3A//www.khronos.org/opengl/wiki/Keyframe_Animation& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Keyframe Animation&i class=&icon-external&&&/i&&/a&
&/p&&a href=&/?target=https%3A//www.khronos.org/opengl/wiki/Skeletal_Animation& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Skeletal Animation&i class=&icon-external&&&/i&&/a&&a href=&/?target=https%3A///what-is-rigging-2095& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&What
is Rigging?&i class=&icon-external&&&/i&&/a&&a href=&/?target=https%3A///en-us/articles/optimizing-the-rendering-pipeline-of-animated-models-using-the-intel-streaming-simd-extensions& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Optimizing
the Rendering Pipeline of Animated Models Using the Intel Streaming SIMD
Extensions&i class=&icon-external&&&/i&&/a&&p&&b&open source project&/b&&/p&
&a href=&/?target=https%3A///julienr/scalamd5& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&julienr/scalamd5&i class=&icon-external&&&/i&&/a&
今天要学习的是游戏开发中,特别是gameplay开发中非常重要的部分
– 骨骼动画。
首先会学习一些原理,还有动画制作的pipeline,实践方面会包括动画加载,GPU Skinning,animation blend, Addittive animation等等。
Tool: VS2015 + blender 2.78 + OpenG…
&p&1. &b&cpu和gpu同步问题&/b& &/p&&p&1)CPU跟GPU的交流当然得提到Driver,Driver分&b&UMD&/b&(User Mode Driver)和&b&KMD&/b&(Kernel Mode Driver)。&/p&&p&UMD负责把应用API上的状态设置(如MSAA,Blending,Depth Test,Texture等状态设置)、Draw Call等转化为硬件可识别的Command,并写入&b&Memory中的Command Buffer&/b&,写满了或在特殊情况下就提交:&a href=&///?target=https%3A///en-us/windows-hardware/drivers/display/submitting-a-command-buffer& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Submitting a Command Buffer&i class=&icon-external&&&/i&&/a&(包括 Present, Flush, Lock),但这里所谓提交,并非提交给GPU,而是提交给Kernel层的Scheduler。&/p&&p&因为每个应用可以拥有自身的UMD和各自提交的Command,需要Scheduler做调度,当轮到这个应用时,Scheduler就将之提交给KMD。&b&KMD是真正跟GPU打交道的Driver&/b&。&/p&&p&2)注意,KMD并不是直接把Command送给GPU来达成CPU与GPU的交流的,而是通过DMA。具体的讲,KMD把要交给GPU的Command在Memory中的地址放在“DMA Command”,并把DMA Comand写到&b&Memory中一个叫Ring Buffer&/b&的结构中,&b&Ring Buffer好比KMD和GPU沟通的桥梁&/b&,KMD和&b&GPU最前端的Command Processor&/b&通过它构成生产/消费关系。一旦GPU消费得慢,Ring Buffer满了,CPU就那边会被阻塞。&/p&&p&GPU读memory是高延迟的,通过DMA Command去memory读数据会很漫长,Command Processor通常一次会请求一大把数据,读回来的Command会写入一个&b&Command FIFO&/b&,Command Processor按顺序解析每个Command,根据Command的类型(如CS或3D)或者给GPU配置State,或者发起通用计算,或者发起Draw任务,让Pipeline所有模块(Shader,Raster等)一同干活。&/p&&p&3)关于Command Buffer的位置&/p&&p&&b&Command Buffer可以放在System memory或Video memory&/b&,取决于具体硬件。放在的Video memory的话,KMD通过DMA传送把DMA command写到Video memory。&/p&&img src=&/v2-fe4bdaeb78af104d793e_b.png& data-rawwidth=&625& data-rawheight=&424& class=&origin_image zh-lightbox-thumb& width=&625& data-original=&/v2-fe4bdaeb78af104d793e_r.png&&&p&3)对于OpengGL而言,同步问题是Driver维护的,题主提到的程序员维护同步是指Vulkan?&/p&&p&4)至于 &/p&&blockquote&当cpu超过gpu 3帧,gpu还没跑完当前指令,cpu就会阻塞等待 &/blockquote&&p&题主指的是否是下面的问题?这只是特殊情况,应用调用了需要同步的操作,一般不会说CPU等几帧就阻塞。&/p&&img src=&/v2-a558fedccd_b.png& data-rawwidth=&749& data-rawheight=&533& class=&origin_image zh-lightbox-thumb& width=&749& data-original=&/v2-a558fedccd_r.png&&&br&&p&2. &b&帧速率和时间查询&/b&&/p&&p&关于帧速率,似乎以结果导向来看更准确,即单位时间内GPU出了多少张Image。不过个人觉得CPU跟GPU是生产者/消费关系,本身CPU扔一个Present,GPU就会消费之最终给出一张Image,时间稍微有点长时(从硬件角度看)两者误差不大(当然,具体我没试验过)。&/p&&p&关于时间,题主指的是GL_ARB_timer_query吗?(&a href=&///?target=http%3A//developer./opengl/specs/GL_ARB_timer_query.txt& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&http://&/span&&span class=&visible&&developer.&/span&&span class=&invisible&&/opengl/specs/GL_ARB_timer_query.txt&/span&&span class=&ellipsis&&&/span&&i class=&icon-external&&&/i&&/a&)&/p&&p&按NV的说明,测的是GPU的时间。&/p&&p&3. &b&glutSwapBuffers/Present&/b&&/p&&p&应用调用一系列API或多个Draw call,GPU Pipeline最终把所有结果render在Framebuffer上,即framebuffer存储着一张Image,&b&glutSwapBuffers/Present是说——好,就画这些,这一帧图片就到此完成了&/b&。而Framebuffer通常有Back Buffer和Front Buffer两个,&b&避免只有一个Buffer时一边画一边刷新屏幕的&/b& &i&tearing&/i& &b&问题——图片一部分是旧帧,一部分是新帧的内容&/b&。&/p&&p&Back Buffer是pipeline写color的地方,Front Buffer则存着用于显示到屏幕的数据,SwapBuffer时,Back Buffer用做Front Buffer,屏幕刷新后便显示新的Image,而原来的Front Buffer则作为Back Buffer继续给pipeline写Image数据。&/p&
1. cpu和gpu同步问题 1)CPU跟GPU的交流当然得提到Driver,Driver分UMD(User Mode Driver)和KMD(Kernel Mode Driver)。UMD负责把应用API上的状态设置(如MSAA,Blending,Depth Test,Texture等状态设置)、Draw Call等转化为硬件可识别的Command,并写入Me…
&img src=&/50/v2-d3d08b3d196f483c10abe91cd0675db3_b.jpg& data-rawwidth=&1500& data-rawheight=&1000& class=&origin_image zh-lightbox-thumb& width=&1500& data-original=&/50/v2-d3d08b3d196f483c10abe91cd0675db3_r.jpg&&&h2&1. 缘起&/h2&许多时候,学习 C/C++ 语言也只看到黑白的控制台输出,总觉得有点乏味。&p&(题图 &a href=&/?target=https%3A///photos/GLCKtmDbOIY& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Photo by Denis Bayer&i class=&icon-external&&&/i&&/a&)&br&&/p&&p&学习计算机图形,可以尝试用不同算法生成有趣的图形,例如在《&a href=&/question//answer/& class=&internal&&如何用 C 语言画「心形」&/a&》(更新4)中,渲染了一个三维的心形隐函数,并以 portable pixmap format(PPM,一种 &a href=&/?target=https%3A//en.wikipedia.org/wiki/Netpbm_format& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Netpbm&i class=&icon-external&&&/i&&/a&)图片格式。&/p&&img src=&/50/aded2faba_b.jpg& class=&content_image&&&p&PPM 是一个极简单的图片格式,但问题是,很少人知道这个格式,也很少软件可以读取这种格式。&/p&&h2&2. PNG&/h2&&p&相对地,PNG(&a href=&/?target=https%3A//en.wikipedia.org/wiki/Portable_Network_Graphics& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Portable Network Graphics&i class=&icon-external&&&/i&&/a&)就是一个广为人知的图片格式。如果可以把影像直接储存成 PNG,不是更理想么?&/p&&p&然而,在 C/C++ 中写入 PNG 一般需要链接一些程序库,例如 PNG 的标准参考程序库是 &a href=&/?target=http%3A//www.libpng.org/pub/png/libpng.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&libpng&i class=&icon-external&&&/i&&/a&。它很强大,支持 PNG 所有功能,但对于初学者而言,配置、编译并学习如何使用这些程序库,可能已足够打消动手的念头。&/p&&blockquote&可以简单一点么?&/blockquote&&h2&3. svpng&/h2&&p&为此,我在周末尝试写一个极简的 C 函数 &a href=&/?target=https%3A///miloyip/svpng& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Github miloyip/svpng&i class=&icon-external&&&/i&&/a&(save PNG 的缩写),它仅能写入 24-bit RGB 或 32-bit RGBA、无压缩的 PNG。它只有一个 32 行代码的函数。&/p&&p&用法如下:&/p&&div class=&highlight&&&pre&&code class=&language-c&&&span&&/span&&span class=&cp&&#include&/span& &span class=&cpf&&&svpng.inc&&/span&&span class=&cp&&&/span&
&span class=&kt&&void&/span& &span class=&nf&&test_rgb&/span&&span class=&p&&(&/span&&span class=&kt&&void&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&kt&&unsigned&/span& &span class=&kt&&char&/span& &span class=&n&&rgb&/span&&span class=&p&&[&/span&&span class=&mi&&256&/span& &span class=&o&&*&/span& &span class=&mi&&256&/span& &span class=&o&&*&/span& &span class=&mi&&3&/span&&span class=&p&&],&/span& &span class=&o&&*&/span&&span class=&n&&p&/span& &span class=&o&&=&/span& &span class=&n&&rgb&/span&&span class=&p&&;&/span&
&span class=&kt&&unsigned&/span& &span class=&n&&x&/span&&span class=&p&&,&/span& &span class=&n&&y&/span&&span class=&p&&;&/span&
&span class=&kt&&FILE&/span& &span class=&o&&*&/span&&span class=&n&&fp&/span& &span class=&o&&=&/span& &span class=&n&&fopen&/span&&span class=&p&&(&/span&&span class=&s&&&rgb.png&&/span&&span class=&p&&,&/span& &span class=&s&&&wb&&/span&&span class=&p&&);&/span&
&span class=&k&&for&/span& &span class=&p&&(&/span&&span class=&n&&y&/span& &span class=&o&&=&/span& &span class=&mi&&0&/span&&span class=&p&&;&/span& &span class=&n&&y&/span& &span class=&o&&&&/span& &span class=&mi&&256&/span&&span class=&p&&;&/span& &span class=&n&&y&/span&&span class=&o&&++&/span&&span class=&p&&)&/span&
&span class=&k&&for&/span& &span class=&p&&(&/span&&span class=&n&&x&/span& &span class=&o&&=&/span& &span class=&mi&&0&/span&&span class=&p&&;&/span& &span class=&n&&x&/span& &span class=&o&&&&/span& &span class=&mi&&256&/span&&span class=&p&&;&/span& &span class=&n&&x&/span&&span class=&o&&++&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&o&&*&/span&&span class=&n&&p&/span&&span class=&o&&++&/span& &span class=&o&&=&/span& &span class=&p&&(&/span&&span class=&kt&&unsigned&/span& &span class=&kt&&char&/span&&span class=&p&&)&/span&&span class=&n&&x&/span&&span class=&p&&;&/span&
&span class=&cm&&/* R */&/span&
&span class=&o&&*&/span&&span class=&n&&p&/span&&span class=&o&&++&/span& &span class=&o&&=&/span& &span class=&p&&(&/span&&span class=&kt&&unsigned&/span& &span class=&kt&&char&/span&&span class=&p&&)&/span&&span class=&n&&y&/span&&span class=&p&&;&/span&
&span class=&cm&&/* G */&/span&
&span class=&o&&*&/span&&span class=&n&&p&/span&&span class=&o&&++&/span& &span class=&o&&=&/span& &span class=&mi&&128&/span&&span class=&p&&;&/span&
&span class=&cm&&/* B */&/span&
&span class=&p&&}&/span&
&span class=&n&&svpng&/span&&span class=&p&&(&/span&&span class=&n&&fp&/span&&span class=&p&&,&/span& &span class=&mi&&256&/span&&span class=&p&&,&/span& &span class=&mi&&256&/span&&span class=&p&&,&/span& &span class=&n&&rgb&/span&&span class=&p&&,&/span& &span class=&mi&&0&/span&&span class=&p&&);&/span&
&span class=&n&&fclose&/span&&span class=&p&&(&/span&&span class=&n&&fp&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&就会输出这个 &a href=&/?target=https%3A///miloyip/svpng/blob/master/rgb.png& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&rgb.png&i class=&icon-external&&&/i&&/a& 文件:&/p&&img src=&/50/v2-bd2c1cb6c812bcf9c9d63bcc4019f4cd_b.png& data-rawwidth=&256& data-rawheight=&256& class=&content_image& width=&256&&&p&这个函数的声明很简单,缺省配置下是这样的:&/p&&div class=&highlight&&&pre&&code class=&language-c&&&span&&/span&&span class=&cm&&/*!&/span&
&span class=&cm&&
\brief 以 PNG 格式存储 RGB/RGBA 影像&/span&
&span class=&cm&&
\param out 输出流(缺省使用 FILE*)。&/span&
&span class=&cm&&
\param w 影像宽度。(&16383)&/span&
&span class=&cm&&
\param h 影像高度。&/span&
&span class=&cm&&
\param img 影像像素数据,内容为 24 位 RGB 或 32 位 ARGB 格式。&/span&
&span class=&cm&&
\param alpha 影像是否含有 alpha 通道。&/span&
&span class=&cm&&*/&/span&
&span class=&kt&&void&/span& &span class=&nf&&svpng&/span&&span class=&p&&(&/span&&span class=&kt&&FILE&/span&&span class=&o&&*&/span& &span class=&n&&out&/span&&span class=&p&&,&/span& &span class=&kt&&unsigned&/span& &span class=&n&&w&/span&&span class=&p&&,&/span& &span class=&kt&&unsigned&/span& &span class=&n&&h&/span&&span class=&p&&,&/span& &span class=&k&&const&/span& &span class=&kt&&unsigned&/span& &span class=&kt&&char&/span&&span class=&o&&*&/span& &span class=&n&&img&/span&&span class=&p&&,&/span& &span class=&kt&&int&/span& &span class=&n&&alpha&/span&&span class=&p&&);&/span&
&/code&&/pre&&/div&相信这样的函数时使对初学者而言,也极易使用。也不需要另外生成程序库,只要复制到项目便可使用。&h2&4. 实现&/h2&&p&这里简单介绍实现要点,对此没兴趣的读者也可略过。&/p&&p&根据 &a href=&/?target=https%3A//www.w3.org/TR/PNG/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Portable Network Graphics (PNG) Specification (Second Edition)&i class=&icon-external&&&/i&&/a& ,PNG 文件由多个 chunk 组成。每个 chunk 的类型以 4 个字符表示。最基本的 PNG 文件内容是:&br&&/p&&ul&&li&8 字节 magic number:用于识别 PNG 格式&/}

我要回帖

更多关于 物理内存占用过高 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信