Unity3D几个点击屏幕自动点击助手获执行相关操作的方法

&figure&&img src=&https://pic2.zhimg.com/v2-0f63f75fb73d9956198a_b.jpg& data-rawwidth=&1024& data-rawheight=&512& class=&origin_image zh-lightbox-thumb& width=&1024& data-original=&https://pic2.zhimg.com/v2-0f63f75fb73d9956198a_r.jpg&&&/figure&&blockquote&译注:Unity项目往往稍微大点就会有性能瓶颈。Unity 2018提出了面向数据编程模式的ECS系统,结合新的安全的多线程机制Job System来解决这个问题,来取代过去的单线程、面向对象的GameObject/MonoBehaviour模式。&/blockquote&&p&&br&&/p&&h2&&b&一、前言&/b&&/h2&&p&&br&&/p&&p&&b&1,我们试图解决什么问题?&/b&&/p&&p&&br&&/p&&p&当用 &b&GameObject&/b&/&b&MonoBehaviour&/b&模式做应用时,很容易编写代码,但结果却是难以阅读、难以维护、难以优化。这是多种因素综合导致的,包括:面向对象模式、由Mono编译的非机器码、垃圾回收和单线程编程。&/p&&p&&br&&/p&&p&&b&2,使用Entity-Component-System(ECS)来拯救你的工程&/b&&/p&&p&&br&&/p&&p&&a href=&https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Entity%25E2%component%25E2%system& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&ECS&/a&是一种编写代码的新方式,着重于你真正该解决的问题:构成你应用的数据(data)和行为(behavior)。&/p&&p&&br&&/p&&blockquote&译注:所谓的行为,具体来说就是方法。&/blockquote&&p&&br&&/p&&p&除了从设计角度讲这是种更好地编程方式之外,使用ECS还可让你发挥Unity Job System和Burst编译器的功力,充分利用当今的多核处理器。&/p&&p&我们发布了Unity原生的Job System,用户可以使用它并结合ECS C#脚本来获得多线程批处理的性能优势。这套Job System内置了用于检测&a href=&https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Race_condition& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&线程竞争条件&/a&(race condition)的安全功能。&/p&&p&所以,我们需要引入一种新的思维和编码方式,以充分发挥Job System的优势。&/p&&p&&br&&/p&&h2&&b&二、什么是ESC?&/b&&/h2&&p&&br&&/p&&p&&b&1,MonoBehavior —— 亲切的老朋友&/b&&/p&&p&&br&&/p&&p&MonoBehaviours既包含数据也包含行为。一个进行旋转的简单例子:&/p&&p&&br&&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&k&&class&/span& &span class=&nc&&Rotator&/span& &span class=&p&&:&/span& &span class=&n&&MonoBehaviour&/span&
&span class=&p&&{&/span&
&span class=&c1&&//数据-可以在编辑器面板里编辑值&/span&
&span class=&k&&public&/span& &span class=&kt&&float&/span& &span class=&n&&speed&/span&&span class=&p&&;&/span&
&span class=&c1&&//行为-从这个Coponent中读取speed,然后根据它改变Transform的rotation&/span&
&span class=&k&&void&/span& &span class=&nf&&Update&/span&&span class=&p&&()&/span&
&span class=&p&&{&/span&
&span class=&n&&transform&/span&&span class=&p&&.&/span&&span class=&n&&rotation&/span& &span class=&p&&*=&/span& &span class=&n&&Quaternion&/span&&span class=&p&&.&/span&&span class=&n&&AxisAngle&/span&&span class=&p&&(&/span&&span class=&n&&Time&/span&&span class=&p&&.&/span&&span class=&n&&deltaTime&/span& &span class=&p&&*&/span& &span class=&n&&speed&/span&&span class=&p&&,&/span& &span class=&n&&Vector3&/span&&span class=&p&&.&/span&&span class=&n&&up&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&&br&&/p&&p&但是,MonoBehaviour继承了一大堆类;每个都包含了它自己的一大套数据——不管我们用不用得到。因此,我们无缘无故浪费了许多内存。所以,我们得想一想到底需要哪些数据,然后好好优化一下我们的代码。&/p&&p&&br&&/p&&blockquote&译注:继承是面向对象编程的三大特征之一,同时也是一大缺点,它导致我们继承了一些无用的数据,浪费了太多内存,内存命中率低下。因此,我们不得不放弃过去的面向对象编程方式,用面向数据编程来进行连续紧凑的内存布局,以提高内存命中率。&/blockquote&&p&&br&&/p&&p&&b&2,ComponentSystem——迈入新领域的第一步&/b&&/p&&p&&br&&/p&&p&在我们的新模式中,一个Component只包含数据,而不包含行为。ComponentSystem才会包含行为,它负责用一组匹配的Component来更新所有GameObject(这些Component也不同于过去继承自MonoBehaviour的组件,它们是用结构struct体定义的,而非类class)。&br&用ComponentSystem实现上文MonoBehavior 相同的功能:&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&k&&private&/span& &span class=&k&&class&/span& &span class=&nc&&Rotator&/span& &span class=&p&&:&/span& &span class=&n&&MonoBehaviour&/span&
&span class=&p&&{&/span&
&span class=&c1&&//数据 - 可以在编辑器面板里编辑值&/span&
&span class=&k&&public&/span& &span class=&kt&&float&/span& &span class=&n&&Speed&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&span class=&k&&class&/span& &span class=&nc&&RotatorSystem&/span& &span class=&p&&:&/span& &span class=&n&&ComponentSystem&/span&
&span class=&p&&{&/span&
&span class=&k&&struct&/span& &span class=&nc&&Group&/span&
&span class=&p&&{&/span&
&span class=&c1&&//定义这个ComponentSystem需要处理哪些Component&/span&
&span class=&n&&Transform&/span& &span class=&n&&Transform&/span&&span class=&p&&;&/span&
&span class=&n&&Rotator&/span&
&span class=&n&&Rotator&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&span class=&k&&override&/span& &span class=&k&&protected&/span& &span class=&nf&&OnUpdate&/span&&span class=&p&&()&/span&
&span class=&p&&{&/span&
&span class=&c1&&// 我们马上看到了第一个优化&/span&
&span class=&c1&&// 我们知道所有rotator的deltaTime都是一样的,&/span&
&span class=&c1&&// 我们就把它存成个本地变量,以便获得根本更好的性能。&/span&
&span class=&kt&&float&/span& &span class=&n&&deltaTime&/span& &span class=&p&&=&/span& &span class=&n&&Time&/span&&span class=&p&&.&/span&&span class=&n&&deltaTime&/span&&span class=&p&&;&/span&
&span class=&c1&&// ComponentSystem.GetEntities&Group&&/span&
&span class=&c1&&// 让我们可以高效地遍历每个有Transform & Rotator的GameObject&/span&
&span class=&c1&&// (因为它们都被定义到上文的Group结构体里了)。&/span&
&span class=&k&&foreach&/span& &span class=&p&&(&/span&&span class=&kt&&var&/span& &span class=&n&&e&/span& &span class=&k&&in&/span& &span class=&n&&GetEntities&/span&&span class=&p&&&&/span&&span class=&n&&Group&/span&&span class=&p&&&())&/span&
&span class=&p&&{&/span&
&span class=&n&&e&/span&&span class=&p&&.&/span&&span class=&n&&Transform&/span&&span class=&p&&.&/span&&span class=&n&&rotation&/span& &span class=&p&&*=&/span& &span class=&n&&Quaternion&/span&&span class=&p&&.&/span&&span class=&n&&AxisAngle&/span&&span class=&p&&(&/span&&span class=&n&&e&/span&&span class=&p&&.&/span&&span class=&n&&Rotator&/span&&span class=&p&&.&/span&&span class=&n&&Speed&/span& &span class=&p&&*&/span& &span class=&n&&deltaTime&/span&&span class=&p&&,&/span& &span class=&n&&Vector3&/span&&span class=&p&&.&/span&&span class=&n&&up&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&&br&&/p&&h2&&b&三、混合ECS: 用ComponentSystem配合现存的GameObject/Component模式&/b&&/h2&&p&目前还存在大量基于MonoBehaviour/GameObject编写的代码,我们或许想要不费力地让ComponentSystem配合现存的GameObject/Component一起工作。其实一次性把一个项目转换成ComponentSystem风格其实也不难的。&/p&&p&从上述例子你就能看出来,我们很轻易地用ComponentSystem配合GameObject/Component遍历了所有的Rotator和Transformt。&/p&&p&&br&&/p&&p&&b&1,ComponentSystem是怎么知道GameObject身上的Rotator和Transform的?&/b&&/p&&p&&b&EntityManager&/b&需要事先知道那些对应的Entity,然后才能像上面的例子那样去遍历所有的Component。&/p&&p&ECS附带一种&b&GameObjectEntity&/b&组件。 当 &b&OnEnable&/b&时,GameObjectEntity会创建一个包含GameObject身上所有Component的Entity。这样,整个GameObject和它的全部Component就都能被ComponentSystem遍历到了。&/p&&p&&br&&/p&&blockquote&注意:所以,目前你必须要在你想让ComponentSystem能遍历得到或看得到的GameObject上添加GameObjectEntity组件。&/blockquote&&p&&br&&/p&&p&&b&2,这对我们程序来说意味着什么呢?&/b&&/p&&p&这意味着你可以一个接一个地,把&b&MonoBehaviour.Update&/b&模式转变成ComponentSystem模式。&br&你可以继续使用&b&&a href=&https://link.zhihu.com/?target=https%3A//docs.unity3d.com/ScriptReference/Object.Instantiate.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&GameObject.Instantiate&/a&&/b& 来创建实例等。&/p&&p&你只是简单地把MonoBehaviour.Update的内容移到了 &b&ComponentSystem.OnUpdate&/b&里面去。 数据仍然保存在那个MonoBehaviour或别的类型的Component中。&/p&&p&&br&&/p&&p&&b&你这么做能够:&/b&&/p&&ul&&li&用更简洁地方式把数据和方法剥离;&/li&&li&System对物体的操作都是批处理的, 避免了逐物体的虚拟调用(virtual call)。在批处理中进行优化就很简单了 (参见上述的deltaTime优化);&/li&&li&你还能继续使用当前的编辑器面板及其他编辑器工具等;&/li&&/ul&&p&&br&&/p&&p&&b&你这么做不能够:&/b&&/p&&ul&&li&实例化耗时得不到优化;&/li&&li&加载化耗时得不到优化;&/li&&li&数据是随机访问的,线性内存布局得不到保证;&/li&&li&没有多线程;&/li&&li&没有单指令流多数据流&a href=&https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/SIMD& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&SIMD&/a&;&/li&&/ul&&p&&br&&/p&&p&所以结合使用ComponentSystem、GameObject和MonoBehaviour是写ECS代码的良好开头,它能给你立即的性能提升,但是它并没有发挥出所有的性能潜力!&/p&&p&&br&&/p&&h2&四、纯粹ECS:IComponentData 和 Job&/h2&&p&&br&&/p&&p&使用ECS的动机之一就是你想让程序有最佳性能。所谓最佳性能是说,你写一些简单的ECS 代码就能得到跟你完全手写SIMD指令集代码差不多的性能。&/p&&p&&br&C# Job System不支持托管的类(class),只支持结构体(struct)类型和原生容器(NativeContainer)。所以,只有&b&IComponentData&/b&才能被安全地用于C# Job System。&br&EntityManager 为组件数据的线性内存布局做出了有力的保证。这是使用IComponentData可以实现的C# Job System的重要组成部分。&/p&&p&&br&&/p&&blockquote&译注:为什么要进行线性内存布局,EntityManager 怎样为组件数据的线性内存布局做出了有力的保证的,后面的&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&ECS In Detail(1)&/a&文章会有阐述。&/blockquote&&p&&br&&/p&&p&下面是使用纯粹ECS方式实现上述相同功能的例子:&/p&&p&1,使用IComponentData储存数据:&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&c1&&// 这个RotationSpeed就是简单地储存一下旋转速度&/span&
&span class=&na&&[Serializable]&/span&
&span class=&k&&public&/span& &span class=&k&&struct&/span& &span class=&nc&&RotationSpeed&/span& &span class=&p&&:&/span& &span class=&n&&IComponentData&/span&
&span class=&p&&{&/span&
&span class=&k&&public&/span& &span class=&kt&&float&/span& &span class=&n&&Value&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&span class=&c1&&// 目前而言,你要想添加或移除Component,就必须要使用这个ComponentDataWrapper,&/span&
&span class=&c1&&// 将来我们想把这个ComponentDataWrapper搞成自动的。&/span&
&span class=&k&&public&/span& &span class=&k&&class&/span& &span class=&nc&&RotationSpeedComponent&/span& &span class=&p&&:&/span& &span class=&n&&ComponentDataWrapper&/span&&span class=&p&&&&/span&&span class=&n&&RotationSpeed&/span&&span class=&p&&&&/span& &span class=&p&&{&/span& &span class=&p&&}&/span&
&/code&&/pre&&/div&&p&2,使用JobComponentSystem实现对数据的多线程批处理:&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&c1&&// 使用IJobProcessComponentData去遍历所有符合这个组件类型的Entity&/span&
&span class=&c1&&// Entity的处理时并行的。主线程只负责安排Job。&/span&
&span class=&k&&public&/span& &span class=&k&&class&/span& &span class=&nc&&RotationSpeedSystem&/span& &span class=&p&&:&/span& &span class=&n&&JobComponentSystem&/span&
&span class=&p&&{&/span&
&span class=&c1&&//IJobProcessComponentData是用来遍历所有带有所需Compoenent类型Enity的简单方法&/span&
&span class=&c1&&//它也比IJobParallelFor更高效更便捷。&/span&
&span class=&na&&
[ComputeJobOptimization]&/span&
&span class=&k&&struct&/span& &span class=&nc&&RotationSpeedRotation&/span& &span class=&p&&:&/span& &span class=&n&&IJobProcessComponentData&/span&&span class=&p&&&&/span&&span class=&n&&Rotation&/span&&span class=&p&&,&/span& &span class=&n&&RotationSpeed&/span&&span class=&p&&&&/span&
&span class=&p&&{&/span&
&span class=&k&&public&/span& &span class=&kt&&float&/span& &span class=&n&&dt&/span&&span class=&p&&;&/span&
&span class=&k&&public&/span& &span class=&k&&void&/span& &span class=&nf&&Execute&/span&&span class=&p&&(&/span&&span class=&k&&ref&/span& &span class=&n&&Rotation&/span& &span class=&n&&rotation&/span&&span class=&p&&,&/span& &span class=&p&&[&/span&&span class=&n&&ReadOnly&/span&&span class=&p&&]&/span&&span class=&k&&ref&/span& &span class=&n&&RotationSpeed&/span& &span class=&n&&speed&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&n&&rotation&/span&&span class=&p&&.&/span&&span class=&n&&Value&/span& &span class=&p&&=&/span& &span class=&n&&math&/span&&span class=&p&&.&/span&&span class=&n&&mul&/span&&span class=&p&&(&/span&&span class=&n&&math&/span&&span class=&p&&.&/span&&span class=&n&&normalize&/span&&span class=&p&&(&/span&&span class=&n&&rotation&/span&&span class=&p&&.&/span&&span class=&n&&Value&/span&&span class=&p&&),&/span& &span class=&n&&math&/span&&span class=&p&&.&/span&&span class=&n&&axisAngle&/span&&span class=&p&&(&/span&&span class=&n&&math&/span&&span class=&p&&.&/span&&span class=&n&&up&/span&&span class=&p&&(),&/span& &span class=&n&&speed&/span&&span class=&p&&.&/span&&span class=&n&&Value&/span& &span class=&p&&*&/span& &span class=&n&&dt&/span&&span class=&p&&));&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&span class=&c1&&// 我们继承JobComponentSystem,这样System就可以自动提供给我们所需Job之间的依赖关系了。&/span&
&span class=&c1&&// IJobProcessComponentData声明了它要对RotationSpeed读操作,并且对Rotation写操作。&/span&
&span class=&c1&&// 这样声明以后,JobComponentSystem就连可以给我们Job之间的依赖关系了,包括之前已经安排好的要写Rotation或RotationSpeed的那些Job。&/span&
&span class=&c1&&// 我们要把这个依赖关系renturn出来,这样,依据类型我们已经安排好的Job就能注册到下一个可能会运行的System里去了。 &/span&
&span class=&c1&&// 这么做意味着:&/span&
&span class=&c1&&// * 主线程不发生等待, 主线程只需要根据依赖关系去安排Job (只有依赖关系被确定以后,Job才会被启动)。&/span&
&span class=&c1&&// * 依赖关系为我们自动计算出来了, 这样我们就只写一些模块化的多线程代码就可以了。&/span&
&span class=&k&&protected&/span& &span class=&k&&override&/span& &span class=&n&&JobHandle&/span& &span class=&nf&&OnUpdate&/span&&span class=&p&&(&/span&&span class=&n&&JobHandle&/span& &span class=&n&&inputDeps&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&kt&&var&/span& &span class=&n&&job&/span& &span class=&p&&=&/span& &span class=&k&&new&/span& &span class=&n&&RotationSpeedRotation&/span&&span class=&p&&()&/span& &span class=&p&&{&/span& &span class=&n&&dt&/span& &span class=&p&&=&/span& &span class=&n&&Time&/span&&span class=&p&&.&/span&&span class=&n&&deltaTime&/span& &span class=&p&&};&/span&
&span class=&k&&return&/span& &span class=&n&&job&/span&&span class=&p&&.&/span&&span class=&n&&Schedule&/span&&span class=&p&&(&/span&&span class=&k&&this&/span&&span class=&p&&,&/span& &span class=&m&&64&/span&&span class=&p&&,&/span& &span class=&n&&inputDeps&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&&br&&/p&&blockquote&译注:进一步内容可以参阅&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&ECS In Detail(1)&/a&&/blockquote&
译注:Unity项目往往稍微大点就会有性能瓶颈。Unity 2018提出了面向数据编程模式的ECS系统,结合新的安全的多线程机制Job System来解决这个问题,来取代过去的单线程、面向对象的GameObject/MonoBehaviour模式。 一、前言 1,我们试图解决什么问题? 当用 G…
&figure&&img src=&https://pic1.zhimg.com/v2-c2a4a0eaa49f5fcb6da973a_b.jpg& data-rawwidth=&1490& data-rawheight=&574& class=&origin_image zh-lightbox-thumb& width=&1490& data-original=&https://pic1.zhimg.com/v2-c2a4a0eaa49f5fcb6da973a_r.jpg&&&/figure&&p&&/p&&p&&b&摘自Unite Beijing 2018 —— iOS底层内存解析&/b&&/p&&p&有兴趣可以踩踩我的小站:&/p&&p&&a href=&https://link.zhihu.com/?target=http%3A//www.resetoter.cn/%3Fp%3D626& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&理解IOS内存构成(Understanding iOS Memory)&/a&&/p&&h2&内存的种类(Kind of Memory)&/h2&&h2&System Memory&/h2&&p&Physical Memory:物理内存&/p&&p&就是实际的物理限制,现在的操作系统都不会直接去操作物理内存&/p&&p&Virtual Memory:虚拟内存&/p&&p&每当启动一个进程的时候都会创建一个logical address space,和物理内存或者其他应用程序的虚拟地址都不对称。&/p&&p&&br&&/p&&p&系统将地址空间分成想呕吐那个大小的块,称作页(page)。进程和内存管理单元包含了一个分页表(page table)来管理分页,在程序运行的时候就通过这个分页表转换到实际的硬件内存地址。&/p&&p&&br&&/p&&p&早版本的iOS页的大小,页的尺寸是4kb,在最新的iOS,A7和A8的系统开放了16kb的64位用户空间对应了4kb的物理页,A9的时候开放了16kb的页并且对应了16kb的物理页。&/p&&p&&br&&/p&&p&虚拟内存包含很多区域,包括代码部分(Code Segments),动态库(Dynamic Libraries),GPU驱动内存(GPU Driver Memory),malloc堆(malloc heap)和其他的。&/p&&p&&br&&/p&&h2&GPU 驱动内存(gpu driver memory)&/h2&&p&由虚拟内存组成,用于驱动,本质上就是IOS的显存。&/p&&p&iOS中所谓统一架构(unified architecture),CPU和GPU共享相同的内存(虽然现代硬件的GPU有更大的传输带宽)大多数内存申请都在驱动中完成,并且大多数都是贴图和网格信息。&/p&&h2&Malloc堆(malloc heap)&/h2&&p&堆内存是虚拟内存中应用能够申请的部分(通过malloc和calloc函数)。&/p&&p&也就是内存申请允许访问的地方。&/p&&p&苹果没有最大程度上地开放堆内存,理论上虚拟地址只被指针大小限制(比如64位那就有2的64次方的bytes),这是进程架构决定的。实际上这个限制被ios的版本限制了,远远小于我们所认为的大小。一个普通的app能申请到的最大内存如下:&/p&&p&&br&&/p&&figure&&img src=&https://pic3.zhimg.com/v2-8c59355abcc8df94a764f3a37ec88220_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&1384& data-rawheight=&616& class=&origin_image zh-lightbox-thumb& width=&1384& data-original=&https://pic3.zhimg.com/v2-8c59355abcc8df94a764f3a37ec88220_r.jpg&&&/figure&&h2&常驻内存&/h2&&p&常驻内存是游戏实际使用的物理内存数量。&/p&&p&一个进程能够申请一个虚拟内存块,但是系统实际上是给了一个相符的物理内存块然后进行写入。这种情况,这个申请的物理内存块就是这个程序的常驻内存。&/p&&h2&分页&/h2&&p&分页是移动物理内存页从内存中放到后台储存中。&/p&&p&进程申请内存的时候会将空闲的内存块申请出来并且标志为常驻内存。&/p&&p&当一个进程申请了块虚拟内存,系统会寻找在物理内存中的空闲的内存页并且将它们映射到已申请的虚拟内存页上(因此将这些内存页作为程序的常驻内存)&/p&&p&如果在物理内存中已经没有可使用的部分的话,系统将根据平台尝试释放已经存在的页,以保证有足够的空间申请新的页。通常情况下,一些使用比较少的页会被移动到后备储存中,并且像一般的文件一样进行储存下来这被称作 paging out.&/p&&p&但在iOS上没没有后台储存,所以页不会page out。但是只读也依旧可以被从内存中移除并且在需要的情况下从磁盘中重载,进程的这种行为被称为page in&/p&&p&&br&&/p&&p&如果当前请求的应用程序申请的地址并不在当前的物理内存上,会产生一个页错误。当这种事情发生时,虚拟内存系统调用一个特殊的也错误处理器来应对这种情况,定位一个空闲物理内存,从后备储存中加载包含所需数据的页,更新page table,然后归还代码的控制权。&/p&&h2&Clean Memory&/h2&&p&Clean Memory&/p&&p&是一个应用常驻内存的只读内存页集,iOS能够安全地从磁盘中移除或重载。&/p&&p&内存申请时将以下的这些看做是Clean的:&/p&&ul&&li&系统framework&/li&&li&程序的二进制可执行文件&/li&&li&内存映射文件&/li&&/ul&&p&当一个应用程序链接到framework上,Clean Memory集合会增加二进制framework文件的尺寸。但大多时候,只有一部分二进制文件被加载到物理内存中。&/p&&p&因为Clean Memory是只读的所以,应用程序可以共享framework以及library,就像其他只读或者写时拷贝的页一样。&/p&&p&&br&&/p&&h2&Dirty Memory&/h2&&p&DirtyMemory是无法被系统主动移除的常驻内存部分。&/p&&p&(因为是脏的数据……&/p&&p&&…&&/p&&p&&br&&/p&&h2&交换压缩内存(Swapped Compressed Memory)&/h2&&p&swapped(Compressed Memory)是Dirty Memory的一部分,是被系统认为用的比较少并且放在一个被压缩的区域。&/p&&p&&br&&/p&&p&用于计算移动和压缩这些内存块的算法并没有被开放出来,但是测试显示iOS经常频繁调用这个算法,以此来降低Dirty Memory的数量。&/p&&h2&Unity内存&/h2&&p&Unity是一个带了.Net脚本虚拟机的C++引擎。Unity为C++object以及虚拟机所需的object申请内存。另一方面,第三方的插件也能从虚拟机内存池中申请内存。&/p&&h2&Native Memory&/h2&&p&游戏虚拟内存中的Native内存是由native(C++)部分进行申请的——在这里Unity申请了它所需要的所有页,包括了Mono堆的。&/p&&p&&br&&/p&&p&在内部,Unity有一系列专门的内存申请器来管理虚拟内存的申请,包括短期用途和长期用途的。所有游戏当中的资源都在Native Memory中进行储存,并且在.Net虚拟机中开放出轻量级接口。换句话说,当一张Texture2D在C#中被创建出来,最大的那部分,实际上是贴图信息,在Native内存中被申请了,而并非在Mono堆中(虽然大多时间他会被上传到GPU然后被丢弃)。&/p&&h2&Mono Heap&/h2&&p&Mono堆是Native内存的申请的一部分,用于.Net虚拟机。它包括了所有托管C#申请的内存,并且由垃圾回收器管理。&/p&&p&Mono 堆由大小相似的储存着各种对象的内存块中进行申请。每一个块能储存一定数量的object如果它在几轮的GC中保持为空(在iOS中为8次GC),这个Block会从内存中被释放(它的物理内存被归还给系统)。但是被GC所使用的虚拟内存地址空间永远不会被释放,并且也不能被任何游戏内存申请器使用。&/p&&p&现在存在的问题是,很多情况下申请的内存块是分散的,也许很大的一块尺寸仅仅使用了很小的一部分。这些块被认为是正在被使用的,所以他们引用的物理内存就无法被正常释放。不幸的是,这种情况经常在实际使用中遇到,也很容易人为地就产生Mono堆常驻内存快速增长的情况。&/p&&h2&iOS内存管理&/h2&&p&iOS是多任务操作系统,它允许多个应用程序在同一环境中共存。每个应用程序都有它自己的虚拟地址空间映射到物理内存的一些部分。&/p&&p&当物理内存不足的时候(或者是过多的应用程序被加载,或者是前台程序消耗了太多的物理内存),iOS开始尝试降低内存压力。&/p&&p&1. 首先,iOS尝试卸载部分Clean Memory页&/p&&p&2. 如果应用程序使用了过多的Dirty Memory,iOS会发送一个内存过低的预警给应用程序,让它自己释放一些内存。&/p&&p&3. 在若干次警告之后,如果应用程序依旧占用了过多的内存,iOS将会终止这个应用程序。&/p&&p&不幸的是,杀死进程的决定并不是透明的。但是它看上去就是由内存压力、内核内存管理器的内部状态以及操作系统已经尝试了多少次减少内存压力的操作决定的。只有当所有的储存空间使用完毕之后,它会决定杀死当前进程。这就是为什么有时候应用程序在申请了多于30%的内存的时候很快就退出了。&/p&&p&尝试调查闪退的最重要的部分就是Dirty Memory的控制,因为iOS无法主动移除脏页来提供更多空间给新的申请需求。这就意味着,修复内存释放问题,开发者必须做到以下几点:&/p&&p&1. 找出在游戏中有多少Dirty Memory,并且是否还随着时间增长。&/p&&p&2. 算出什么对象在贡献游戏的dirty memory 并且无法被压缩&/p&&p&不同的iOS设备上的Dirty Memory尺寸有相应合理的限制(从我们能广泛看到的结果):&/p&&ul&&li&512MB设备中180MB&/li&&li&1GB设备中360MB&/li&&li&2GB设备中1.2GB&/li&&/ul&&p&记下这些推荐限制值,被iOS关闭依旧是有可能的但是可以大大减少被iOS关闭的可能。&/p&&p&&br&&/p&&p&&br&&/p&&h2&各种Profile工具&/h2&&p&1. Unity Memory Profiler&/p&&p&2. MemoryProfiler (on BitBucket)&/p&&p&3. MemoryProfiler Extension (on Github)&/p&&p&4. Xcode memory gauge in Debug Navigator view&/p&&p&5. VM Tracker Instrument&/p&&p&6. Allocations Instrument&/p&&p&&br&&/p&&p&(后面都是工具的使用部分了,大家有兴趣可以自己进行尝试&/p&
摘自Unite Beijing 2018 —— iOS底层内存解析有兴趣可以踩踩我的小站:内存的种类(Kind of Memory)System MemoryPhysical Memory:物理内存就是实际的物理限制,现在的操作系统都不会直接去操作物理内存Virt…
&figure&&img src=&https://pic3.zhimg.com/v2-4b40fb399fd_b.jpg& data-rawwidth=&1640& data-rawheight=&903& class=&origin_image zh-lightbox-thumb& width=&1640& data-original=&https://pic3.zhimg.com/v2-4b40fb399fd_r.jpg&&&/figure&&p&&a href=&https://link.zhihu.com/?target=https%3A//forum.unity3d.com/threads/terraincomposer-to-create-aaa-realistc-terrains-released.171928/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&题图链接&/a&&/p&&h2&写在前面&/h2&&p&开了个专栏,搬一篇之前写的文章过来看看。以下是之前写的&a href=&https://link.zhihu.com/?target=http%3A//candycat1992.github.io//blend-terrain-textures/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&博客&/a&正文。&/p&&p&前一段时间有个朋友找我分析一个地形的shader,代码很长就主要看了下里面的纹理合并的部分。Unity目前常见的地形应该是T4M的做法,据说是只支持打包4张纹理,也就是说可以在地形上刷4种纹理,多了的话就不太方便了。而这个shader中的方法可以打包9张纹理,然后靠一张混合纹理来控制混合,感觉还挺巧妙的。&/p&&h2&方法&/h2&&p&这个shader主要会利用两张纹理。一张自然是包含了9种地形纹理的atlas纹理,就称为BlockMainTex吧:&/p&&p&&br&&/p&&figure&&img src=&https://pic2.zhimg.com/v2-46ee2e4b3e46c16a63aef5cbbeec63d7_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&355& data-rawheight=&356& class=&content_image& width=&355&&&/figure&&p&&br&&/p&&p&以及一张负责混合纹理的BlendTex:&/p&&p&&br&&/p&&figure&&img src=&https://pic3.zhimg.com/v2-fdc7c38f1741691bdd292_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&315& data-rawheight=&314& class=&content_image& width=&315&&&/figure&&p&&br&&/p&&p&这张纹理是关键所在,它的RG通道存储了该位置处需要混合的&b&两种&/b&地形纹理的&b&索引值&/b&,它的B通道存储了这两种纹理的混合系数。下面是上图的RGB通道图:&/p&&p&&br&&/p&&figure&&img src=&https://pic2.zhimg.com/v2-a5d2c5731d0cbb20e5012b_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&632& data-rawheight=&218& class=&origin_image zh-lightbox-thumb& width=&632& data-original=&https://pic2.zhimg.com/v2-a5d2c5731d0cbb20e5012b_r.jpg&&&/figure&&p&&br&&/p&&p&我们最终可以得到类似下面的效果:&/p&&p&&br&&/p&&figure&&img src=&https://pic3.zhimg.com/v2-7ab7a08ab88cb0c386c48b1b41d2d448_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&652& data-rawheight=&390& class=&origin_image zh-lightbox-thumb& width=&652& data-original=&https://pic3.zhimg.com/v2-7ab7a08ab88cb0c386c48b1b41d2d448_r.jpg&&&/figure&&p&&br&&/p&&p&可以看出来,我们可以用一个draw call+两张纹理刷出至多9种不同的地形纹理。&/p&&h2&地形纹理的索引&/h2&&p&都可以看出来关键在于混合纹理BlendTex,它的RG通道存储了该位置处需要混合的两种地形纹理的索引值,即每个通道存储了一个索引值。实际上,由于BlockMainTex是按照九宫格来打包了9种纹理,所以这个索引是一个二维的向量(x,y),也就是说把这个二维(x,y)索引值打包进一个0~1的8 bits小数内(通道值的范围)。这主要是靠下面的公式:&/p&&p&&img src=&https://www.zhihu.com/equation?tex=f+%3D+%5Cfrac%7Bx%7D%7B16%7D+%2B+%5Cfrac%7By%7D%7B256%7D+%5C%5C& alt=&f = \frac{x}{16} + \frac{y}{256} \\& eeimg=&1&&&/p&&p&其中,x和y分别表示在索引对应的行列值(我总是把上面的公式理解成把x编码进了前4个bits,把y编码进了后4个bits)。&/p&&p&上面是编码的过程,解码的相关公式就是:&/p&&p&&img src=&https://www.zhihu.com/equation?tex=x+%3D+floor%C%5C+y+%3D+floor%28256f%29+-+16x& alt=&x = floor(16f) \\ y = floor(256f) - 16x& eeimg=&1&&&/p&&p&Shader代码对应:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&float2 encodedIndices = tex2D(_BlendTex, i.uv).
float2 twoVerticalIndices = floor((encodedIndices * 16.0));
float2 twoHorizontalIndices = (floor((encodedIndices * 256.0)) - (16.0 * twoVerticalIndices));
float4 decodedI
decodedIndices.x = twoHorizontalIndices.x;
decodedIndices.y = twoVerticalIndices.x;
decodedIndices.z = twoHorizontalIndices.y;
decodedIndices.w = twoVerticalIndices.y;
decodedIndices = floor(decodedIndices/4)/4;
&/code&&/pre&&/div&&p&decodedIndices就是0~3的整数索引值除以4的结果,即该种纹理在BlockMainTex中的起始值。拿图中樱花那个block举例,它对应的xy值是(0,8)(由于xy的范围是0~15,而图片索引范围是0~3,所以要乘以4),所以在BlendTex中的颜色就是8/256。&/p&&h2&纹理采样&/h2&&p&知道了两张地形纹理的索引,就该对它们进行采样了。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&float2 worldScale = (worldPos.xz * _BlockScale);
float2 worldUv = 0.234375 * frac(worldScale) + 0.0078125; // 0.0078125 ~ 0.2421875, the range of a block
float2 uv0 = worldUv.xy + decodedIndices.
float2 uv1 = worldUv.xy + decodedIndices.
&/code&&/pre&&/div&&p&整个地形使用xz平面的世界坐标的小数部分作为采样坐标进行平铺。由于每个block其实只占了1/4的长宽值,所以要进行缩放。为了防止接缝处出现问题,还在两边稍微拉伸了下,即每边拉伸了0.0078125个单位(即1/128个单位):&/p&&p&&br&&/p&&figure&&img src=&https://pic3.zhimg.com/v2-34f22f8e64fb9d8c1988320ade32e05a_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&654& data-rawheight=&461& class=&origin_image zh-lightbox-thumb& width=&654& data-original=&https://pic3.zhimg.com/v2-34f22f8e64fb9d8c1988320ade32e05a_r.jpg&&&/figure&&p&&br&&/p&&h2&处理接缝&/h2&&p&如果直接使用上面的uv0和uv1对纹理采样,那么在地形接缝处会出现明显的问题:&/p&&p&&br&&/p&&figure&&img src=&https://pic1.zhimg.com/v2-347b9ca2cea2b5b24658_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&653& data-rawheight=&353& class=&origin_image zh-lightbox-thumb& width=&653& data-original=&https://pic1.zhimg.com/v2-347b9ca2cea2b5b24658_r.jpg&&&/figure&&p&&br&&/p&&p&这主要是因为这里的纹理tiling是我们手动对worldScale取frac得到的,这样纹理采样坐标的偏导其实是不连续的,而通常我们使用单张纹理的tiling是连续的,是由图形API和硬件帮我们处理平铺类型的。&/p&&p&解决方法也很简单,我们只需要保证在接缝处的偏导连续不突变即可,这可以靠支持4个参数的&a href=&https://link.zhihu.com/?target=http%3A//http.developer.nvidia.com/Cg/tex2D.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&tex2D函数&/a&来解决。完整的代码如下:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&float blendRatio = tex2D(_BlendTex, i.uv).z;
float2 worldScale = (worldPos.xz * _BlockScale);
float2 worldUv = 0.234375 * frac(worldScale) + 0.0078125;
float2 dx = clamp(0.234375 * ddx(worldScale), -0..0078125);
float2 dy = clamp(0.234375 * ddy(worldScale), -0..0078125);
float2 uv0 = worldUv.xy + decodedIndices.
float2 uv1 = worldUv.xy + decodedIndices.
// Sample the two texture
float4 col0 = tex2D(_BlockMainTex, uv0, dx, dy);
float4 col1 = tex2D(_BlockMainTex, uv1, dx, dy);
// Blend the two textures
float4 col = lerp(col0, col1, blendRatio);
&/code&&/pre&&/div&&p&其实就是手动算了下采样坐标worldScale的ddx和ddy,这也是为什么之前每个block要向每边拉伸了0.0078125个单位,这样才不会采样越境。上面在算ddx和ddy的时候,还把结果截取到(-0..0078125)即(1/128,-1/128)之间,我猜想这是为了在摄像机距地面非常的远的时候(此时ddx和ddy的绝对值会比较大,纹素密度很大),如果ddx或ddy的绝对值超过了拉伸值0./128),就会在接缝处采样到隔壁的block,所以要在这里使用clamp截取一下范围,下图显示了截取范围前后的区别。在此需要感谢评论区的jim童鞋,我之前只考虑到了正数的情况,没有考虑负值,这是不正确的(额这么说来其实某个上线游戏里也是不对的…)。&/p&&p&&br&&/p&&figure&&img src=&https://pic3.zhimg.com/v2-141eeb80efbe18fdd4aec_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&728& data-rawheight=&180& class=&origin_image zh-lightbox-thumb& width=&728& data-original=&https://pic3.zhimg.com/v2-141eeb80efbe18fdd4aec_r.jpg&&&/figure&&p&&br&&/p&&h2&写在最后&/h2&&p&这里只是主纹理采样,当然还可以加上法线的采样,比如:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// Sample the two normal
fixed3 norm0 = UnpackNormal(tex2D(_BlockNormalTex, uv0, dx, dy));
fixed3 norm1 = UnpackNormal(tex2D(_BlockNormalTex, uv1, dx, dy));
// Blend the two normals
fixed3 norm = lerp(norm0, norm1, blendRatio);
norm = normalize(norm);
&/code&&/pre&&/div&&p&还有很多自定义的纹理可以靠这种方法来类推。另外,这种方法显然要实现一套自定义的刷地形工具给美术用(这应该是最麻烦的部分……)。总结来看,这种方法需要的基本采样次数是:一次对BlendTex的采样,两次BlockMainTex的采样,共3次来完成9种地形纹理的混合(其实每次只能同时混合两张)。&/p&&p&&/p&&p&&/p&
写在前面开了个专栏,搬一篇之前写的文章过来看看。以下是之前写的正文。前一段时间有个朋友找我分析一个地形的shader,代码很长就主要看了下里面的纹理合并的部分。Unity目前常见的地形应该是T4M的做法,据说是只支持打包4张纹理,也就是说可…
&figure&&img src=&https://pic1.zhimg.com/v2-6ddb27d2b9_b.jpg& data-rawwidth=&2281& data-rawheight=&1421& class=&origin_image zh-lightbox-thumb& width=&2281& data-original=&https://pic1.zhimg.com/v2-6ddb27d2b9_r.jpg&&&/figure&&p&大家好,我是吉祥,目前是数字媒体专业的大二学生。我从2年前开始接触计算机图形学相关的技术,目前也正在学习研究中。这篇文章是我最近几天研究Unity 可编程渲染管线的一些结果总结,希望对具有相同需求的朋友们有所帮助。文章内容不一定对,如果错误请不吝赐教。作者也是刚刚开始学习Unity,之前研究的一直是CRYENGINE的渲染管线,所以对一些功能和C#的API还不是很熟悉,也请大家多多包涵。&/p&&p&Retro3D是在GDC 2018上官方展示的一个可编程渲染管线的应用案例,由Unity东京分部的Keijiro Takahashi制作。您可以在&a href=&http://link.zhihu.com/?target=https%3A//github.com/keijiro/Retro3DPipeline& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Github&/a&上找到这个项目的源代码。如果需要正确运行源代码,您需要一个使用Unity 2018.1及以上的版本。&/p&&p&首先简单介绍一下这篇文章的主要内容:&/p&&ul&&li&简要解释什么是&b&渲染管线&/b&,其和着色器代码的关系。就作者的经验分析Unity SRP的优缺点所在。&/li&&li&直接入手开始分析Retro3D的实现代码,并且以此介绍SRP的基本架构。&/li&&/ul&&p&在开始之前,我建议读者先至少阅读Unity官方博客中关于SRP的介绍:&a href=&http://link.zhihu.com/?target=https%3A//blogs.unity3d.com/cn//srp-overview/%3F_ga%3D2.5528305& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&链接&/a&。&/p&&h2&渲染管线&/h2&&p&渲染管线(Rendering Pipeline)可以指很多东西,但是在现在的语境下,我们将渲染管线形容为CPU为了&b&渲染一帧所需要的全部渲染相关的代码&/b&。游戏中图像的渲染需要CPU和GPU共同完成,其中CPU负责处理一些“全局”的任务,比如决定每一个绘制指令(Drawcall)中需要提供给GPU的物体、灯光,设置显卡的渲染管线(这是另一个东西,用来表示一个Drawcall中绘制的整个流程,不要弄混了~),然后同时显卡去进行渲染。而显卡负责“细节”任务,我们可以使用着色器(Shader)代码来给显卡编程,指定如何让显卡去真正绘制我们想要的图案。&/p&&p&一些使用Unity进行游戏开发的读者可能已经有过编写着色器的经验了,Unity提供的ShaderLab可以很好地自定义自己的着色器。但是其实渲染管线应该和着色器成为一个整体考虑,着色器代码决定如何绘制具体的效果,而渲染管线则控制“应该绘制什么”以及“使用哪些着色器来进行绘制”等问题。在以往的Unity版本中,渲染管线虽然可以配置,但是仍然只能使用Unity自带的多个渲染管线中的一个,因此在编写着色器代码的时候难免会有些拘束,有很多功能使用内置渲染管线无法实现。而在2018添加了可编程渲染管线的功能以后,我们&b&几乎&/b&就可以完全自己决定整个画面的表现了(虽然仍然没有直接使用诸如Dx11的API来得灵活,但是Unity已经帮忙处理了所有的物体、光源、环境探针等复杂的结构,其总体效率肯定要高得多)。&/p&&p&Unity的可编程渲染管线优点是什么?作者觉得,其最大的优点在于将渲染的灵活性大幅度提升的同时又保持最低的复杂度增长。Unity的渲染管线是通过向C#暴露脚本的形式提供的,相比于虚幻和CE直接开源的方式,使用脚本虽然仍然有一些功能和性能上的限制,但是确实降低了学习的复杂度和出错的几率(直接改引擎源代码十分困难,而且不容易维护和升级)。而Unity可编程渲染管线的瓶颈也正好是在使用脚本编程上,渲染管线作为每一帧都要执行,并且很可能是消耗帧时间大头的功能,使用脚本编程一定会造成更大的性能开销。目前还没有针对SRP的性能压力测试,但是根据colormath在其&a href=&http://link.zhihu.com/?target=http%3A//colourmath.com/2018/tutorials/exploring-scriptable-render-pipelines-in-unity-2018-1/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&博客文章&/a&里所述,光是因为GC造成的性能开销就已经不可忽略。&/p&&p&但是总之,可编程渲染管线仍是很多项目的可选方案之一,它十分灵活,易于实现很出色的效果,而对于各位图形学爱好者来说,这也是一个&b&十分有趣可玩&/b&的功能,并且可以用一种最简单的方式入门渲染管线的编程。&/p&&h2&Retro3D源代码分析&/h2&&p&Retro3D是一个十分简单的像素化场景案例,其技术本身没有什么值得分析的内容,因此我们主要通过Retro3D分析如何编写一个简单的渲染管线。&/p&&p&整个项目里比较核心的代码文件是以下三个,都放在Assets/Retro3D目录下。&/p&&ul&&li&Retro3DPipeline.cs 定义自己的渲染管线。&/li&&li&Retro3DPipelineAsset.cs 定义渲染管线的资产,会在下面简单的解释。&/li&&li&Retro3DSurface.shader 定义着色器代码。&/li&&/ul&&p&其中比较重要的是第一个文件,因此我们先从第一个文件讲起。&/p&&p&首先关注到以下这行代码&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&k&&public&/span& &span class=&k&&class&/span& &span class=&nc&&Retro3DPipeline&/span& &span class=&p&&:&/span& &span class=&n&&RenderPipeline&/span&
&/code&&/pre&&/div&&p&Unity使用RenderPipeline作为所有自定义管线类的基类,这个类主要需要重载两个方法:&/p&&ul&&li&Render方法,用于执行一帧的所有绘制指令。&/li&&li&Dispose方法,用于在卸载管线时做清理工作。&/li&&/ul&&p&除此之外,您也可以添加任何您想要的方法和成员变量。&/p&&h2&Command Buffer和Render Context&/h2&&p&为了组织渲染流程,Unity提供了Command Buffer(使用RenderBuffer类表示)和Render Context(使用ScriptableRenderContext类表示)两个概念,这两个类都是延迟执行的:用户需要往这两个里先添加各种指令(清空渲染目标、设置各种参数、执行渲染等),然后通知其一次性执行。&/p&&p&Command Buffer可以理解为一个记载各种命令的清单,每一个Command Buffer可以有自己的名字(name属性),用户可以往Command Buffer里添加命令,也可以清空Command Buffer(Clear方法),一旦命令都添加完毕以后,就可以复用Command Buffer进行一系列特定操作。&/p&&p&Render Context可以理解为当前渲染的总控Command Buffer,其也是一个清单,所有的Command Buffer最终都要将命令按顺序“倒入”Render Context中(通过ExecuteCommandBuffer方法),然后Render Context通过Submit方法一次性执行整个渲染管线。&/p&&p&这一种组织方式显然是在帮助开发者最低限度地减少脚本编程带来的额外开销,因为理论上,Command Buffer只需要在初始化的时候填充完,然后就可以在整个管线中复用了(目前还不知道能不能在帧间复用,作者觉得应该是不行)。避免了不必要的指令执行开销。&/p&&h2&Render方法&/h2&&p&所有的渲染秘密都在Render方法里,我们看一下其函数签名:&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&k&&public&/span& &span class=&k&&override&/span& &span class=&k&&void&/span& &span class=&nf&&Render&/span&&span class=&p&&(&/span&&span class=&n&&ScriptableRenderContext&/span& &span class=&n&&context&/span&&span class=&p&&,&/span& &span class=&n&&Camera&/span&&span class=&p&&[]&/span& &span class=&n&&cameras&/span&&span class=&p&&)&/span&
&/code&&/pre&&/div&&ul&&li&context:使用的渲染环境,在上一段已经解释过了。渲染环境是全局唯一的。&/li&&li&cameras:一个需要渲染的相机数组,大部分情况下只有一个相机需要渲染(视口),但是Unity也允许渲染多个相机。&/li&&/ul&&p&然后是初始化CommandBuffer的代码,很直观:&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&c1&&// Lazy initialization of the temporary command buffer.&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&_cb&/span& &span class=&p&&==&/span& &span class=&k&&null&/span&&span class=&p&&)&/span& &span class=&n&&_cb&/span& &span class=&p&&=&/span& &span class=&k&&new&/span& &span class=&n&&CommandBuffer&/span&&span class=&p&&();&/span&
&/code&&/pre&&/div&&p&下面的部分比较难理解,我们会在后面用到这两个变量的时候解释,简单来说,这两个变量用于创建一个新的渲染目标:&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&c1&&// Constants used in the camera render loop. &/span&
&span class=&kt&&var&/span& &span class=&n&&rtDesc&/span& &span class=&p&&=&/span& &span class=&k&&new&/span& &span class=&n&&RenderTextureDescriptor&/span&&span class=&p&&(&/span&&span class=&m&&256&/span&&span class=&p&&,&/span& &span class=&m&&224&/span&&span class=&p&&,&/span& &span class=&n&&RenderTextureFormat&/span&&span class=&p&&.&/span&&span class=&n&&RGB565&/span&&span class=&p&&,&/span& &span class=&m&&16&/span&&span class=&p&&);&/span&
&span class=&kt&&var&/span& &span class=&n&&rtID&/span& &span class=&p&&=&/span& &span class=&n&&Shader&/span&&span class=&p&&.&/span&&span class=&n&&PropertyToID&/span&&span class=&p&&(&/span&&span class=&s&&&_LowResScreen&&/span&&span class=&p&&);&/span&
&/code&&/pre&&/div&&p&然后就是一个foreach循环,说明对于每一个相机,管线都要运行一次。大部分的渲染代码都是per camera的,并且一个相机一定对应一个渲染目标(Render Target)。&/p&&p&在进入相机循环以后,我们要做的第一件事是使用当前的相机来设置绘制环境。这样一来之后的绘制才能正确地使用相机的变换矩阵。这一步通过以下代码实现:&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&n&&context&/span&&span class=&p&&.&/span&&span class=&n&&SetupCameraProperties&/span&&span class=&p&&(&/span&&span class=&n&&camera&/span&&span class=&p&&);&/span&
&/code&&/pre&&/div&&p&这一步会实现以下功能(参考LightweightPipeline.cs第344行的注释):&/p&&ol&&li&设置相机的RenderTarget和Viewport参数,会自动将相机设置为当前环境的渲染目标。&/li&&li&设置VR相机参数(如果是VR相机)。&/li&&li&设置相机的view和proj矩阵,以及它们的逆矩阵。&/li&&li&设置_WorldSpaceCameraPos, _ProjectionParams, _ScreenParams, _ZBufferParams, unity_OrthoParams这几个shader全局变量。&/li&&li&设置相机的世界裁剪平面参数。&/li&&li&设置HDR关键字。&/li&&li&设置全局着色器时间变量(_Time, _SinTime, _CosTime)。&/li&&/ol&&p&可以看出,Unity仍然为我们提供了很多快捷的功能来帮助我们配置自己的可编程管线。比如如果使用SetupCameraProperties方法的话,我们仍然可以和以前一样在着色器里使用UnityObjectToClipPos函数进行顶点变换。当然读者也可以完全不用Unity提供的方法,自己手动设置所有这些参数,不过大部分情况下,还是应该让Unity帮忙处理这些问题会比较好。(别忘了我们还可以在设置完以后通过SetGlobalVector等方法覆盖Unity帮我们&/p&&h2&创建渲染目标&/h2&&p&我们先将以下几行代码一起贴上来。它们的作用是新建一个渲染目标(Render Target),并将其设置为当前环境的渲染目标.&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&c1&&// Setup commands: Initialize the temporary render texture.&/span&
&span class=&n&&_cb&/span&&span class=&p&&.&/span&&span class=&n&&name&/span& &span class=&p&&=&/span& &span class=&s&&&Setup&&/span&&span class=&p&&;&/span&
&span class=&n&&_cb&/span&&span class=&p&&.&/span&&span class=&n&&GetTemporaryRT&/span&&span class=&p&&(&/span&&span class=&n&&rtID&/span&&span class=&p&&,&/span& &span class=&n&&rtDesc&/span&&span class=&p&&);&/span&
&span class=&c1&&//创建一个新的临时渲染目标&/span&
&span class=&n&&_cb&/span&&span class=&p&&.&/span&&span class=&n&&SetRenderTarget&/span&&span class=&p&&(&/span&&span class=&n&&rtID&/span&&span class=&p&&);&/span&
&span class=&c1&&//将新的临时渲染目标作为当前的渲染目标&/span&
&span class=&n&&_cb&/span&&span class=&p&&.&/span&&span class=&n&&ClearRenderTarget&/span&&span class=&p&&(&/span&&span class=&k&&true&/span&&span class=&p&&,&/span& &span class=&k&&true&/span&&span class=&p&&,&/span& &span class=&n&&camera&/span&&span class=&p&&.&/span&&span class=&n&&backgroundColor&/span&&span class=&p&&);&/span&
&span class=&c1&&//使用相机背景色清空临时渲染目标&/span&
&span class=&n&&context&/span&&span class=&p&&.&/span&&span class=&n&&ExecuteCommandBuffer&/span&&span class=&p&&(&/span&&span class=&n&&_cb&/span&&span class=&p&&);&/span&
&span class=&c1&&//执行提交的所有命令。&/span&
&span class=&n&&_cb&/span&&span class=&p&&.&/span&&span class=&n&&Clear&/span&&span class=&p&&();&/span&
&span class=&c1&&//清空命令缓存。&/span&
&/code&&/pre&&/div&&p&我们首先简单介绍一下什么是渲染目标(Render Target)。如果读者之前没有进行过图形管线的研究,可能对这个名词不是很熟悉。在一般情况下,显卡执行一次绘制,会把最终的数据绘制在一块显存区域里。这块显存区域代表了一张纹理(Texture),其有自己的宽(Width)、高(Height)和格式(Format),并且每一个像素点以单独的一位数据表示,整个纹理可以看成是像素点组成的数组。&/p&&p&我们最终绘制在屏幕上的颜色信息也是保存在一张纹理中,被称为Backbuffer,任何被绘制到Backbuffer上的颜色信息最终都会被呈现给用户。Backbuffer的长宽和窗口保持一致,且它是一个&b&渲染目标&/b&(Render Target)。&/p&&p&一张纹理在整个渲染流程中可以作为贴图被采样(编写过shader的读者肯定很熟悉了),也可以指定为让这一次管线将最终颜色输出给纹理。同一张纹理在不同的drawcall中可以重复利用,比如上一个drawcall为这个纹理绘制一些信息,而在下一个drawcall中需要用到这些信息进行进一步的计算。如果一张纹理在某一个drawcall中被指定为输出的目标,那么其就被称为渲染目标。&/p&&p&于是在这段代码中,使用GetTemporaryRT函数创建了一个新的可用于接受绘制结果的纹理,这一个函数需要用到两个参数:一个是新的纹理需要的ID(rtID),以全局定位它,另一个就是新的纹理需要的格式,一般包括宽、高和格式信息,使用rtDesc结构体指定。&/p&&p&在创建了新的渲染目标纹理以后,我们将其设置为当前激活的渲染目标,&b&这样之后的所有绘制指令都不是直接绘制到屏幕上,而是绘制到一个临时的渲染目标上&/b&。在真正的代码中,这种技术会被反复使用。&/p&&p&接下来就到了真正处理渲染的部分了。Retro3D的代码虽然很简短,但是其包含了一个渲染管线所必需的所有内容,因此理解了Retro3D的渲染,您就可以开始着手研究自己的渲染管线了(但是要玩好还有很长的路要走)。&/p&&p&基本上,为了执行一次绘制(drawcall),用户需要在渲染管线里设置以下三个东西:&/p&&ul&&li&Cull Results 裁剪结果,对整个场景的物体进行裁剪,决定需要渲染的物体。&/li&&li&Filtering Rules 过滤规则,过滤出本次pass需要处理的物体属性。&/li&&li&Drawing Rules 绘制规则,指定真正执行本次绘制使用的pass。&/li&&/ul&&h2&裁剪规则设置和执行裁剪&/h2&&p&在SRP中,可以使用两种裁剪:相机视锥裁剪和遮挡裁剪。在本案例中我们只使用相机视锥裁剪。&/p&&p&相机视锥裁剪的意思是,将所有完全不在相机可见范围内的物体从裁剪中剔除。哪怕物体和相机视锥有一点点相交,其都会被保留下来,在光栅化之前使用GPU进行裁剪。&/p&&p&在SRP中,一般情况下通过相机(Camera)视角进行裁剪。这个Camera对象和Unity内置的相机对象是同一个。裁剪算法是Unity内置的不可编程,但是可以进行一些配置。&/p&&p&如果需要配置视锥裁剪,需要使用ScriptableCullingParameters这个结构体,可以通过CullResults.GetCullingParameters静态方法从指定的相机里提取。在提取出这个结构体以后,就可以根据需要对里面参数进行调整。具体参数和用法在官方文档中已经有了比较详细的介绍,在此就不多展开了。&/p&&p&在设置好裁剪参数以后,调用CullResults.Cull静态方法执行最终的裁剪,其中第一个camera参数可以使用ScriptableCullingParameters代替,第二个是context,第三个使用一个CullResults实例保存裁剪结果。&/p&&p&详细代码如下:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&var culled = new CullResults();
CullResults.Cull(camera, context, out culled);
&/code&&/pre&&/div&&p&在本案例中,我们并没有需要修改裁剪参数,因此可以直接将相机传入裁剪。&/p&&p&需要注意的是,裁剪并不仅仅针对需要渲染的物体。事实上,裁剪会对以下三种物体起作用:&/p&&ul&&li&灯光(平行光除外,因为其影响是全局的)&/li&&li&被渲染物体&/li&&li&反射球(Reflection Probe)&/li&&/ul&&p&使用CullResults.visibleRenderers成员变量可以获取指向可见渲染目标的句柄,提供给context就可以渲染。&/p&&h2&过滤规则和绘制规则&/h2&&p&裁剪规则执行的是初步的裁剪,但是我们还有更细节的问题:一个场景中可能有的物体材质是多种多样的,有不透明的物体,有透明的物体,有不受光照的物体,还有天空球等物体。我们需要使用一个额外的规则来指定我们当前的绘制需要关注哪一种物体。&/p&&p&在Untiy中,使用渲染队列(Render Queue)来描述物体的绘制次序,每一种类型的物体都占了特定的渲染区间。以下是一份渲染队列的参考:&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&c1&&// Background : 1000
0 - 1499 = Background&/span&
&span class=&c1&&// Geometry
1500 - 2399 = Geometry&/span&
&span class=&c1&&// AlphaTest
2400 - 2699 = AlphaTest&/span&
&span class=&c1&&// Transparent: 3000
2700 - 3599 = Transparent&/span&
&span class=&c1&&// Overlay
3600 - 5000 = Overlay&/span&
&/code&&/pre&&/div&&p&在标准Unity管线中,Unity会按照渲染队列的顺序从小到大绘制物体,但是在自定义管线中,Unity并不会帮我们做这个工作。事实上,通过裁剪获取的所有物体,其排列顺序是随机的。这允许我们使用自己的算法对物体进行排列,而在这种情况下,渲染队列其实只是帮助我们进行排序的工具之一。&/p&&p&补充:使用DrawRendererSettings.sorting.flags可以设置绘制时的排序规则。默认设置为0则不进行排序。从SortFlags里可以选择几种内置的排序算法。&/p&&p&但是在这个demo里,我们并不需要排序,因为深度缓冲会保证物体以任意顺序绘制的都是正确的。我们确实需要关注的是绘制物体的类型。因此我们可以使用&b&过滤规则&/b&FilterRenderersSettings指定我们需要绘制的渲染队列区间,比如这个代码中以下语句:&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&kt&&var&/span& &span class=&n&&filter&/span& &span class=&p&&=&/span& &span class=&k&&new&/span& &span class=&n&&FilterRenderersSettings&/span&&span class=&p&&(&/span&&span class=&k&&true&/span&&span class=&p&&);&/span&
&span class=&n&&filter&/span&&span class=&p&&.&/span&&span class=&n&&renderQueueRange&/span& &span class=&p&&=&/span& &span class=&n&&RenderQueueRange&/span&&span class=&p&&.&/span&&span class=&n&&opaque&/span&&span class=&p&&;&/span&
&span class=&c1&&//过滤出所有opaque的物体&/span&
&/code&&/pre&&/div&&p&指定了只有队列范围落在opaque(不透明)物体区间的物体才会被绘制。&/p&&p&事实上,除了指定渲染区间以外,还可以使用过滤规则指定需要使用的Unity层(Layer)等参数,可以参考官方文档和博客了解更多内容。&/p&&p&在将物体进行裁剪和过滤以后,我们终于得到了一张需要绘制的物体的列表。现在我们需要决定如何绘制这些物体。这通过&b&绘制规则&/b&DrawRendererSettings来设置。&/p&&p&绘制规则需要指定使用的相机和使用的着色器pass,但是我们在这里并不是直接指定pass的名字,而是指定光照模式(LightMode)的名字。这样所有需要绘制,且材质使用的shader中有对应光照模式的pass的物体都会被绘制。在这个案例中,指定了“Base”作为光照模式名称,但是这个名称可以自定,只要和着色器保持一致就好。&/p&&div class=&highlight&&&pre&&code class=&language-csharp&&&span&&/span&&span class=&c1&&// Render visible objects that has &Base& light mode tag.&/span&
&span class=&kt&&var&/span& &span class=&n&&settings&/span& &span class=&p&&=&/span& &span class=&k&&new&/span& &span class=&n&&DrawRendererSettings&/span&&span class=&p&&(&/span&&span class=&n&&camera&/span&&span class=&p&&,&/span& &span class=&k&&new&/span& &span class=&n&&ShaderPassName&/span&&span class=&p&&(&/span&&span class=&s&&&Base&&/span&&span class=&p&&));&/span&
&/code&&/pre&&/div&&p&最终,我们终于可以执行一个drawcall来绘制物体了。这通过以下方法调用:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&context.DrawRenderers(culled.visibleRenderers, ref settings, filter);
&/code&&/pre&&/div&&p&这个调用需要输入裁剪完的物体列表、绘制规则和过滤规则。&/p&&p&但是还没完,还记得前文所说的吗?我们并没有将内容直接绘制到场景中,而是绘制到了一个临时的渲染目标上。我们最终还是需要将渲染目标的颜色信息拷贝到场景中。Blit函数指定这个功能:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&_cb.name = &Blit&;
_cb.Blit(rtID, BuiltinRenderTextureType.CameraTarget);
context.ExecuteCommandBuffer(_cb);
_cb.Clear();
&/code&&/pre&&/div&&p&Blit并不是简单地进行数据拷贝,事实上它会将源渲染目标的尺寸缩放拉伸到新的渲染目标尺寸并且将像素映射上去。这一步是显卡直接完成的,所以速度很快。&/p&&p&在Blit完成以后,使用ExecuteCommandBuffer就可以向context“倒入”在命令缓存中的所有命令。之后别忘了使用Clear清空命令缓存,以准备好接受下一批命令!&/p&&p&最后的最后,需要绘制相机对应的场景时,我们使用Submit函数一次性执行之前声明的所有内容。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&context.Submit();
&/code&&/pre&&/div&&h2&风格化原理&/h2&&p&在这个小节中,作者会简要分析以下这个风格化的(像素化的)场景的制作原理。作者假设读者具有一定的shader编程能力,因此不会在这里讲解shader的语法和编程(事实上也说不完),而是分析一下实现这个步骤的几个关键。&/p&&p&有趣的是,这个案例很好地反应了shader和管线配合的效果,如果单纯使用shader制作的话,有一些效果还实现不出来。&/p&&p&请读者注意以下这些代码:&/p&&p&在Retro3DSurface.shader中第33行:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&vp = floor(vp * 64) / 64;
&/code&&/pre&&/div&&p&第44行:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&uv = floor(uv * 256) / 256;
&/code&&/pre&&/div&&p&第46行:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&c = floor(c * 8) / 8;
&/code&&/pre&&/div&&p&读者可以试着将这三行注释掉,shader不会出错,看一下屏幕上的图像有何变化。&/p&&p&事实上,这三步都在做同一件事:将一个连续的量离散化,只能按照某一个步进值取颜色或者信息。这三步使得画面出现明显的颗粒感。&/p&&p&但是整个效果的关键其实不在着色器中,而是在渲染管线里。请注意Retro3DPipeline.cs的这行代码:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&var rtDesc = new RenderTextureDescriptor(256, 224, RenderTextureFormat.RGB565, 16);
&/code&&/pre&&/div&&p&刚刚我们了解到,rtDesc设置新的临时渲染目标的参数,注意新的目标的尺寸:&/p&&p&256x224&/p&&p&看出来了吗,我们只将图像绘制到一个十分小的渲染目标上,然后将其Blit到一个非常大的(在作者电脑上超过)的Backbuffer之上,因此每一个像素都被拉伸了好多倍,产生了这种像素化的效果。&/p&&p&读者可以尝试修改这个尺寸大小至一个比较大的值,看一下效果有什么变化。&/p&&h2&总结&/h2&&p&本文主要分析了Retro3D的实现原理,并且以此简单介绍了Untiy可编程渲染管线的基本用法。本文的内容还远远不够覆盖大部分的可编程管线有趣内容,但是希望能给读者指明一个研究的起点,然后读者就可以自己钻进代码里开始折腾自己的渲染管线。&/p&&p&本文尚未提及Retro3DPipelineAsset.cs文件,这个文件主要是为渲染管线创建在编辑器里的替身,允许我们通过可视化的设置来选择需要使用的渲染管线。由于作者对Unity的资源系统还没有过多的研究,因此对于很多用法还不甚了解,因此在本文里没有进行分析。在大部分情况下,只要按照官方给的样例编写这个文件,都不会出太大问题。&/p&&p&欢迎对这块内容感兴趣的读者前来交流和探讨,我也会继续研究这个好玩的新功能。&/p&
大家好,我是吉祥,目前是数字媒体专业的大二学生。我从2年前开始接触计算机图形学相关的技术,目前也正在学习研究中。这篇文章是我最近几天研究Unity 可编程渲染管线的一些结果总结,希望对具有相同需求的朋友们有所帮助。文章内容不一定对,如果错误请不…
&figure&&img src=&https://pic4.zhimg.com/v2-a47ace124bc890fe607c_b.jpg& data-rawwidth=&630& data-rawheight=&210& class=&origin_image zh-lightbox-thumb& width=&630& data-original=&https://pic4.zhimg.com/v2-a47ace124bc890fe607c_r.jpg&&&/figure&&h2&0x00 前言&/h2&&figure&&img src=&https://pic4.zhimg.com/v2-a47ace124bc890fe607c_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&630& data-rawheight=&210& class=&origin_image zh-lightbox-thumb& width=&630& data-original=&https://pic4.zhimg.com/v2-a47ace124bc890fe607c_r.jpg&&&/figure&&p&今天写篇博客,和大家聊一聊Android平台上的性能调试神器——snapdragon profiler。和之前大家熟悉的adreno profiler相比,snapdragon profiler是前者的替代品,并且增加了对vulkan的支持。因此建议大家可以使用该工具调试高通设备。&br&&/p&&p&下文使用sdp代表snapdragon profiler。&/p&&h2&0x01 安装&/h2&&p&首先是sdp的下载地址:&/p&&a href=&https://link.zhihu.com/?target=https%3A//developer.qualcomm.com/software/snapdragon-profiler& data-draft-node=&block& data-draft-type=&link-card& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&https://&/span&&span class=&visible&&developer.qualcomm.com/&/span&&span class=&invisible&&software/snapdragon-profiler&/span&&span class=&ellipsis&&&/span&&/a&&p&其次,在mac上运行sdp需要有mono的环境,因此还需要下载最新的mono framework:&/p&&a href=&https://link.zhihu.com/?target=http%3A//www.mono-project.com/download/%23download-mac& data-draft-node=&block& data-draft-type=&link-card& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Download | Mono&/a&&p&同时系统要安装adb。&/p&&p&之后按照安装正常mac应用的流程安装sdp即可。&/p&&h2&0x02 启动&/h2&&p&安装完毕之后,即可以点击图标运行,也可以在命令行中运行。&/p&&p&这里建议在命令行中运行,因为会有一些console输出,因此如果有一些异常例如sdp一打开就闪退或者运行期无响应,基本都可以在console找到原因。&/p&&figure&&img src=&https://pic2.zhimg.com/v2-dc1eee3eb787_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&1654& data-rawheight=&840& class=&origin_image zh-lightbox-thumb& width=&1654& data-original=&https://pic2.zhimg.com/v2-dc1eee3eb787_r.jpg&&&/figure&&p&另外一个原因是在我点击图标启动sdp后,发现sdp无法检测到已经链接到电脑的测试设备,重启adb服务也无效。换成命令行启动sdp后,一切正常。 &/p&&p&&br&&/p&&h2&0x03 链接设备&/h2&&p&接下来将安卓设备的开发者模式开启,链接电脑。此时可以使用adb来查看一下设备是否上线。之后在sdp的start page即可选择链接设备。点击“Connect to a Device”即可弹出当前在线的设备,选择目标设备再点击Connect即可建立连接。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-67a955d64ed70dac817a41_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&2040& data-rawheight=&1362& class=&origin_image zh-lightbox-thumb& width=&2040& data-original=&https://pic1.zhimg.com/v2-67a955d64ed70dac817a41_r.jpg&&&/figure&&p&此时可以看到,在页面左侧之前灰色的3个选项被激活,分别对应的是实时性能追踪、时间线抓取分析以及图形库的抓振分析(暂时貌似只支持es3),同时左下角还可以看到当前和设备的连接状态。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-074eeaa54f60_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&2400& data-rawheight=&1544& class=&origin_image zh-lightbox-thumb& width=&2400& data-original=&https://pic4.zhimg.com/v2-074eeaa54f60_r.jpg&&&/figure&&p&&br&&/p&&h2&0x04 实时性能数据(RealTime) &/h2&&p&RealTime模式提供了设备的实时数据,我们即可以点击具体的app来获取app的性能指标,也可以直接获取整个系统的性能指标。&/p&&p&我们可以很方便的分别在Process和System条目下分别选择app和系统要测试的性能指标。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-1edf8eaae8e3cf_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&1926& data-rawheight=&1928& class=&origin_image zh-lightbox-thumb& width=&1926& data-original=&https://pic3.zhimg.com/v2-1edf8eaae8e3cf_r.jpg&&&/figure&&p&这里我们可以很方便的获取app的cpu、gpu方面的开销。以及系统cpu、gpu、网络、电量、温度方面的数据。&/p&&p&不过这里要说明一下,app的fps数据目前只支持es的app,而不支持vulkan。(v2.1.0.),而fps这个测试指标在EGL条目下找到。&/p&&p&如果我们想对数据进行进一步的分析,可以将当前profiler的数据进行导出。在左上角有一个数据导出图标,可以将当前的profiler数据导出为csv文件。&/p&&p&还有需要说明的一点是,sdp同样也有logcat的输出。只是默认没有在页面内显示,如果需要开启的话,可以在Tool菜单内打开logcat。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-a7ecb61e718ba777a7826_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&1600& data-rawheight=&1244& class=&origin_image zh-lightbox-thumb& width=&1600& data-original=&https://pic1.zhimg.com/v2-a7ecb61e718ba777a7826_r.jpg&&&/figure&&p&&br&&/p&&h2&0x05 时间线抓取分析(Trace Capture) &/h2&&p&除了可以查看实时数据之外,我们还可以抓取一段时间内的调用追踪,以时间线的形式展示。&/p&&p&开启一个Trace Capture同样很简单,只需要在StartPage点击New Trace Capture或者使用Capture菜单内的New Trace即可创建一个新的Trace。&/p&&p&和RealTime类似的,我们也可以分别为app和系统选择我们感兴趣的性能指标进行检测,勾选相应的指标后,点击左上角的“Start Capture”开始抓取。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-0c353b8a5b3f83a12f39ef_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&1938& data-rawheight=&1642& class=&origin_image zh-lightbox-thumb& width=&1938& data-original=&https://pic3.zhimg.com/v2-0c353b8a5b3f83a12f39ef_r.jpg&&&/figure&&p&1,2秒种后再点击Stop Capture结束这次抓取,那么我们就获取了这1~2秒内的调用追踪,并且以时间线的形式展示。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-624b6b9fdec27eab0d8b5aa_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&2980& data-rawheight=&1698& class=&origin_image zh-lightbox-thumb& width=&2980& data-original=&https://pic1.zhimg.com/v2-624b6b9fdec27eab0d8b5aa_r.jpg&&&/figure&&p&和Unity的Profiler类似,我们同样可以缩放、点击具体的调用信息。而且在右侧还有inspector窗口,可以查看该调用的具体信息。&/p&&p&&br&&/p&&h2&0x06 抓帧分析(Snapshot Capture)&/h2&&p&Snapshot Capture是很强大的分析渲染流程的工具,但是很可惜的是目前抓帧分析只支持es,vulkan目前还不能使用Snapshot Capture功能,这也是我觉得很遗憾的事情。因为Snapshot Capture是我最喜欢的一个功能——它甚至能提供每一个像素的渲染历史。&/p&&p&开启一个Snapshot Capture和Trace Capture十分类似,即可以在StartPage内开启也可以在Capture菜单内开启。&/p&&p&只需要点击一下Take Snapshot按钮,等待数据传送完毕后,当前帧的图形库渲染的各种数据就展现在眼前了。&/p&&figure&&img src=&https://pic2.zhimg.com/v2-e963a989fbfd4_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&2980& data-rawheight=&1948& class=&origin_image zh-lightbox-thumb& width=&2980& data-original=&https://pic2.zhimg.com/v2-e963a989fbfd4_r.jpg&&&/figure&&p&最中间的当然是截取的画面,点击某个像素之后,左上角的Pixel History窗口便展示出了该像素的历史。 &/p&&p&下面是gl的指令,点击某一条draw就可以查看这次draw的各种数据,例如顶点数据,索引数据等等。&/p&&p&右上角则提供了当前的Resource以及当前帧的分析数据。&/p&&p&其中Resource内包括各种texture、各种buffer甚至是shader program,点击program条目下对应的shader,就可以直接查看shader的内容了。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-c63c3abc4685e5eee92a6fe_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&2458& data-rawheight=&1952& class=&origin_image zh-lightbox-thumb& width=&2458& data-original=&https://pic3.zhimg.com/v2-c63c3abc4685e5eee92a6fe_r.jpg&&&/figure&&p&而当前帧的分析数据同样很有价值,这里我们可以查看当前帧底层的各种数据。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-cacb76f9b_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&800& data-rawheight=&562& class=&origin_image zh-lightbox-thumb& width=&800& data-original=&https://pic4.zhimg.com/v2-cacb76f9b_r.jpg&&&/figure&&p&&br&&/p&&h2&0x07 vulkan相关 &/h2&&p&最后,简单聊一聊sdp和vulkan。&/p&&p&首先,作为高通adreno profiler的替代品,sdp是支持vulkan的。但是目前的sdp版本需要设备进行root才能进行vulkan的测试,所以一些不方便root的设备会比较麻烦。&/p&&p&具体原因在高通的开发者论坛上高通的工作人员提到过:&/p&&blockquote&Unfortunately loading the Vulkan layer requires setting Android’s SELinux module to permissive mode, which requires root permissions on your device. We hope to provide a path for Vulkan profiling without root permissions in in the future, but for now you will need a rooted device.&/blockquote&&p&所以,希望以后sdp能为vulkan提供更方便的测试方式,同时,也是最重要的,Snapshot Capture一定要增加对vulkan的支持啊。&/p&&p&&br&&/p&&p&-EOF-&br&最后打个广告,欢迎支持我的书《&a href=&https://link.zhihu.com/?target=https%3A//item.jd.com/.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Unity 3D脚本编程&/a&》&br&&/p&&figure&&img src=&https://pic4.zhimg.com/v2-8a61da560d13f2d74f8fbd3_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&522& data-rawheight=&668& class=&origin_image zh-lightbox-thumb& width=&522& data-original=&https://pic4.zhimg.com/v2-8a61da560d13f2d74f8fbd3_r.jpg&&&/figure&&p&&br&&/p&&p&欢迎大家关注我的公众号慕容的游戏编程:chenjd01&/p&&p&&br&&/p&&figure&&img src=&https://pic3.zhimg.com/v2-67fae8ae8f7755_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&400& data-rawheight=&400& class=&content_image& width=&400&&&/figure&
0x00 前言今天写篇博客,和大家聊一聊Android平台上的性能调试神器——snapdragon profiler。和之前大家熟悉的adreno profiler相比,snapdragon profiler是前者的替代品,并且增加了对vulkan的支持。因此建议大家可以使用该工具调试高通设备。 下文使用sdp…
&figure&&img src=&https://pic1.zhimg.com/v2-6e4c57bfab2f25a7a31fc22360cd0fda_b.jpg& data-rawwidth=&673& data-rawheight=&249& class=&origin_image zh-lightbox-thumb& width=&673& data-original=&https://pic1.zhimg.com/v2-6e4c57bfab2f25a7a31fc22360cd0fda_r.jpg&&&/figure&&p&升级到2017的团队应该都注意到了,打开原来的Sprite Packer会有这样一个提示&/p&&figure&&img src=&https://pic2.zhimg.com/v2-174c9d3cbab2f2d16dd1f5_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&627& data-rawheight=&85& class=&origin_image zh-lightbox-thumb& width=&627& data-original=&https://pic2.zhimg.com/v2-174c9d3cbab2f2d16dd1f5_r.jpg&&&/figure&&p&只有重新选择Legacy Sprite Packer才能继续使用原来的图集功能&/p&&figure&&img src=&https://pic3.zhimg.com/v2-4e791cd17c9f850d21dce_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&793& data-rawheight=&198& class=&origin_image zh-lightbox-thumb& width=&793& data-original=&https://pic3.zhimg.com/v2-4e791cd17c9f850d21dce_r.jpg&&&/figure&&p&那么,它新增的“不是Legacy”的图集功能在哪里呢?&/p&&figure&&img src=&https://pic3.zhimg.com/v2-ddfbd9ffa0f1bbab25b9ae_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&684& data-rawheight=&529& class=&origin_image zh-lightbox-thumb& width=&684& data-original=&https://pic3.zhimg.com/v2-ddfbd9ffa0f1bbab25b9ae_r.jpg&&&/figure&&figure&&img src=&https://pic1.zhimg.com/v2-a74d3f44bf36bea38d31150_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&950& data-rawheight=&829& class=&origin_image zh-lightbox-thumb& width=&950& data-original=&https://pic1.zhimg.com/v2-a74d3f44bf36bea38d31150_r.jpg&&&/figure&&p&&b&在2017,图集的概念被恢复了。&/b&准确的说,是把虚拟的图集配置变成了实际存在的Asset。&/p&&p&&br&&/p&&p&实际上,整个图集系统并没有太大的变化,更改的只是用户界面。你还是可以使用(也只能使用)原来分散的Sprite文件,在没有图集的情况下拼合UI或者2D内容。图集还是可以在项目的末期再考虑,更改一个Sprite的图集所属并不需要修改使用过它的预制。&/p&&p&&b&这个图集Asset其实只是一个打包配置文件&/b&,在打包之前磁盘里并不会存在这样一个图集纹理,打包时才会生成图集纹理并替换所有相关Sprite的内容。一切都和以前一样。这次只是项目配置文件的Asset化。&/p&&p&&br&&/p&&p&不过经过这样的更改,操作上则稍微有了一些便利:&/p&&p&&a href=&http://link.zhihu.com/?target=http%3A//1.ni/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&1.&/a&你不需要再写编辑器脚本给目录下的文件设置sprite tag,新的Sprite Atlas设置包含内容的时候支持目录,你只需要把目录设置成Sprite Atlas的项目即可。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-581eabcfe0e_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&668& data-rawheight=&123& class=&origin_image zh-lightbox-thumb& width=&668& data-original=&https://pic3.zhimg.com/v2-581eabcfe0e_r.jpg&&&/figure&&p&2.可以统一地设置整个图集的压缩纹理格式,而不是到每个Sprite上设置并保证他们一致。&/p&&p&3.Allow Rotation(允许旋转)和Tight Packing(按OutLine紧密打包)被拆分成了两个选项,所以你可以选择不“紧密打包”但是“允许旋转”。另外,现在的这个设置也比以前直观很多,我打赌很多人之前根本不知道Unity有这两个功能。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-bcefb17d709dd63f2e9530cebffabaee_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&666& data-rawheight=&104& class=&origin_image zh-lightbox-thumb& width=&666& data-original=&https://pic3.zhimg.com/v2-bcefb17d709dd63f2e9530cebffabaee_r.jpg&&&/figure&&p&4.Sprite Atlas提供了GetSprites的API,所以可以从Atlas直接取到它全部Sprite的实例了。做Sprite内容的切换(不同颜色的框,VIP等级图片)就不用自己保存一个Sprite列表了,直接引用图集Asset用GetSprite(name)就可以。是的,Sprite Name的概念也恢复了。&/p&&p&5.AB分配的时候只需要设置图集的AssetBoundle所属,Sprite不需要设置。具体的机理我也没弄懂,总之设不设Sprite,结果都一样的。把图集加入一个Ab,等于把其包含的所有Sprite都加入这个Ab。&/p&&p&&br&&/p&&p&值得一提的是,由于图集设置变成了“图集包含特定Sprite”,所以,一个Sprite现在可以被放置到多

我要回帖

更多关于 屏幕自动点击助手 的文章

 

随机推荐