Unity优化——批处理的优势

简介: Unity优化——批处理的优势

介绍


在3D图形和游戏中,批处理是一个非常通用的术语,它描述了将大量任意数据块组合在一起并将它们作为单个大数据块进行处理的过程。这对于CPU,特别是GPU来说是理想的,因为它可以使用多个内核同时粗粝多个任务。在内存中不同位置来回切换内核是需要时间的,因此切换内核所花的时间越少越好。


在某些情况下,批处理的对象指的是网格、顶点、边、UV坐标和其他用于描述3D对象的不同数据类型的大集合。然而,该术语也可以简单代表批处理音频文件、精灵、纹理文件和其他大数据集的行为。


因此,为了避免混淆,本专题提到的Unity中的批处理,通常指的是两种用于批处理网格数据的主要机制:动态批处理和静态批处理。这两种方法本质上是几何体合并的两种不同形式,用于将多个对象的网格数据合并到一起,并在单一指令中渲染它们,而不是单独准备和绘制每个几何体。


将多个网格批处理为单个网格是可以实现的,因为没有规定网格对象必须是3D空间中连续的几何体。Rendering Pipeline(管线渲染)可以接受一系列没有共同变的顶点,因此可以将本来需要多个渲染指令的独立网格合并为单个网格,用单一指令渲染它。


多年来,关于动态批处理和静态批处理系统的触发条件,以及批处理在什么地方能够带来性能提升,一直存在许多困惑。毕竟,在某些情况下,如果没有正确使用批处理,它的确会恶化性能。正确理解这些系统将有助于我们掌握显著提升应用程序图形性能所需的知识。


本文涵盖如下专题:


  • 管线渲染和Draw Call概念的简单介绍
  • Unity的材质和着色器如何一起工作,以渲染对象
  • 使用Frame Debugger可视化渲染行为
  • 动/静态批处理的工作原理及优化方式


一、Draw Call


在单独讨论动态批处理和静态批处理之前,首先要明白它们在管线渲染中试图解决的问题。


这些批处理方法的主要目标是减少在当前视图中渲染所有对象所需的Draw Call数量。就最基本的形式而言,Draw Call只是一个从CPU发送到GPU中用于绘制对象的请求。


注意:Draw Call是这一过程的通用行业术语,但在Unity中有时也称为SetPass Call,因为一些底层方法也命名为SetPass Call。可以将Draw Call理解为初始化当前渲染过程之前的配置选项。本文剩余部分将其统称为Draw Call。


在请求Draw Call之前,需要完成一些任务。首先,网格和纹理数据必须从CPU内存(RAM)送到GPU内存(VRAM)中,这通常发生在场景初始化期间,但仅限于场景文件知道的纹理和网格。如果使用非场景中的纹理和网格数据在运行时动态实例化对象,那么必须在它们实例化时完成加载。接着,CPU必须配置处理对象(这些对象就是Draw Call的目标)所需的选项和渲染特性,为GPU做好准备。


这些CPU和GPU间的通信任务是通过Graphics API进行的,这可以是DirectX、OpenGL等,取决于针对的平台和指定的图形设置。这些API调用通过一个称为"驱动"的类库来执行,该类库包含一系列错综复杂的设置、状态变量以及可以在应用程序中配置和执行的数据集(只是"驱动"库旨在同时服务多个程序,以及来自多个线程的渲染器调用)。可用的特性会根据我们的显卡和所针对的Graphics API发生巨大的变化;更高级的显卡支持更高级的特性,但这需要由更新版本的API支持,因此需要更新的驱动程序来启动它们。多年来创建的各种设置、支持的特性和版本之间的兼容性级别(特别是诸如OpenGL这样的旧API),其数量简直令人难以置信。幸运的事,在某种抽象级别上,所有这些API都倾向于以类似的方式运行,因此Unity可以通过一个公共接口支持很多不同的Graphics API。


在渲染对象之前,必须为准备管线渲染而配置的大量设置常常统称为渲染状态(Render State)。除非这些渲染状态选项发生了变化,GPU将为所有传入的对象保持相同的渲染状态,并以类似的方式渲染它们。


更改渲染状态是一个耗时的过程。例如,如果将渲染状态设置为使用一个蓝色纹理文件,然后要求它渲染一个巨大的网格,那么渲染会非常快,整个网格都显示为蓝色。然后,可以再渲染9个完全不同的网格,它们都显示为蓝色,因为没有改变所使用的问题。然而,如果先用10种不同的纹理渲染10个网格,就将花费更长的时间。这是因为在每个网格发送Draw Call指令之前,需要使用新的纹理来准备渲染状态。


用于渲染当前对象的纹理在Graphics API种实际上是一个全局变量,而在并行系统内修改全局变量说起来容易做起来难。在诸如GPU这样的大规模并行系统中,实际上必须在修改渲染状态之前一直等待,直到所有当前的作业打到同一个同步点为止(换句话说,最快的内核需要停下,等待最慢的内核赶上,这浪费了它们可以用于其他任务的时间),到达同步点后,需要重新启动所有的并行作业。这会浪费很多时间,因此请求改变渲染状态的次数越少,Graphics API越能更快递处理请求。


可以出发渲染状态同步的操作包括但不限于:立即推送一张新纹理到GPU,修改着色器、照明信息、阴影、透明度和其他任何图形设置。


一旦配置了渲染状态,CPU就必须决定绘制哪个网格,用什么纹理和着色器,以及基于对象的位置、旋转和缩放(这些都在一个名为变换的4✖️4矩阵中表示,这正是Transform组件名字的由来)决定在何处绘制对象,然后发送指令到GPU以绘制它。为了使CPU和GPU之间的通信保持活跃,新指令被推入一个名为Command Buffer的队列中。这个队列包含CPU创建的指令,以及GPU每次执行完前面的指令后从中提取的指令。


批处理提升此过程性能的诀窍在于,新的Draw Call不一定意味着必须配置新的渲染状态。如果两个对象共享完全相同的渲染状态信息,那么GPU可以立即开始渲染新对象,因为在最后一个对象完成渲染之后,还维护着相同的渲染状态,这消除了由于同步渲染状态而浪费的时间,也减少了需要推入Command Buffer中的指令数,减少了CPU和GPU上的工作负载。


二、材质和着色器


在Unity中,渲染状态本质上是通过材质呈现给开发者的。材质是着色器的容器,着色器是一种用于定义GPU应该如何渲染输入的顶点和纹理数据的简短程序。着色器本身没有必要的状态信息来完成任何有价值的工作。着色器需要诸如漫反射纹理、法线影响和光照信息之类的输入,并有效地规定了为了呈现传入的数据需要设置哪些渲染状态变量。


提示:着色器之所以如此命名是因为多年前,它们原本仅实现为处理对象的光照和着色(应用阴影,原本是没有阴影的)。现在它们的功能已经有了巨大的增长,现在更通用的功能是作为访问各种不同并行任务的可编程接口,但依旧使用之前的名字。


每个着色器都需要一个材质,而每个材质必须有一个着色器。甚至新导入场景中的网格,如果没有赋予材质,就会自动被赋予默认(隐藏的)材质,为它们提供基本的漫反射着色器和白色色彩。因此,无法绕过这一关系。


提示:注意一个材质只支持一个着色器。要对一个网格使用多个着色器,需要将多个材质赋予该网格的不同部位。


所以,如果想要最小化渲染状态修改的频率,可以减少场景中使用的材质数量。这将同时提升两个性能:CPU每帧花费更少的时间生成指令,并传输给GPU;而GPU不需要经常停止,重新同步状态的变更。


为了理解材质和批处理的行为,先介绍一个简单的场景。然而,在开始之前,应该仅用一些渲染选项,因为它们会产生一些额外的Draw Call,这可能会令人分散注意力:


1、导航到Edit|Project Settings|Quality,并设置Shadows为Disable Shadows(或者选择默认的Fastest品质级别)。


2、导航到Edit|Project Settings|Player,打开Other Settings选项卡,并禁用Static Batching和Dynamic Batching(如果它们是开启的)。


下一步创建一个场景,其中包含一个方向光、4个立方体和4个球体,每个对象都有独特的材质、位置、旋转和缩放。


然后看Game窗口的Stats弹出框中的Batching值共有9个批处理。该值严格等于渲染场景中使用的Draw Call数量。当前视图将消耗其中一个批处理来渲染场景的背景,场景的背景可以设置为Skybox或Solid Clor,这取决于摄像机对象的Clear Flags设置。


剩余8个批处理用于绘制8个对象。对于每个对象,Draw Call需要使用材质的属性准备管线渲染,并请求GPU根据对象当前的变换设置渲染给定的网格。给每个对象提供不同的纹理文件用于渲染,来确保材质是唯一的。因此,每个网格需要不同的渲染状态,所以这8个不同网格都需要各不相同的Draw Call。


如前所述,理论上可以通过减少系统修改渲染状态信息的频率,来最小化Draw Call的数量。因此,一部分目标是减少使用的材质数。然而,如果所有对象都设置为使用相同的材质,则性能依旧没有任何提升,批处理数量依然是9。这是因为渲染状态变更的数量没有真正减少,也没有高效低合并网格信息。遗憾的是,管线渲染不够智能,意识不到在重复写入完全相同的渲染信息,并要求它一次又一次地渲染相同的风格。


三、Frame Debugger


在深入讨论批处理之前,先研究一个有用的工具Frame Debugger,它有助于确定批处理是如何影响场景的。


要打开Frame Debugger,在主窗口中选择Window|Frame Debugger或者在Profiler的Rendering区域中单击BreakDown View Options中的Frame Debugger按钮,这两个操作都可以打开Frame Debug窗口。


单击Frame Debug窗口的Enable按钮,可以观察场景是如何创建的,每次执行一个Draw Call。一般左侧显示GPU指令列表,右侧显示选中的Draw Call的详细信息。


Frame Debugger窗口提供了很多有用的信息,可以用于调整单一Draw Call的行为,但最有用的区域是左边面板的Drawing部分,其中列出了场景中的所有Draw Call,其中的每一项表示一个唯一的Draw Call和它渲染的对象。该工具的一个非常有用的特性是单击其中任一项,就能立刻在Game窗口中看到场景渲染到所单击记得那一项所需的Draw Call。这样就可以快速、直观地区分两个连续的Draw Call,也很容易准确地指出给定的Draw Call渲染了哪些对象。可以通过查看在Draw Call期间出现了多少个对象,来帮助确定是否对一组对象进行批处理。


四、动态批处理


动态批处理有下面3个重要优势:


  • 在运行时生成
  • 批处理中包含的对象在不同帧之间可能有所不同,这取决于哪些网格在主相机视图中是可见的
  • 能在场景中运动的对象也可以批处理


如果返回Player settings页面并开启Dynamic Batching,将看到批处理数量从9降到6。动态批处理自动识别共享材质和网格信息的对象,因此,将它们合并到一个大的批次中以供处理。还应该看到Frame Debugger中有一列不同的项,展示了正在进行动态批处理的网络。


结果是,4个立方体合并到一个名为Dynamic Batch的Draw Call中,但4个球体依然通过4个独立的Draw Call渲染,这是因为4个球体不满足动态批处理的要求。尽管它们使用的材质相同,但还必须满足很多其他条件。


对网格进行成功的动态批处理所需满足的需求列表可以在Unity文档中找到。


为给定网格执行动态批处理的要求如下:


  • 所有网格实例必须使用相同的材质引用。
  • 只有ParticleSystem和MeshRenderer组件进行动态批处理。SkinnedMeshRenderer组件(用于角色动画)和所有其他可渲染的组件类型不能进行批处理。
  • 每个网格至多有300个顶点,但是着色器使用的顶点属性数不能大于900.这意味着对于复杂的着色器,每个网格的最大顶点数可能小于300。
  • 对象不能在变换中包含镜像(也就是说,一个具有正比例的游戏对象A和一个具有付比例的游戏对象B不能放在一起批处理)。
  • 网格实例应该引用相同的光照纹理文件
  • 材质的着色器不能依赖多个进程
  • 网格实例不能接受实时投影
  • 整个批处理中网格索引的总数有上线,这与所用的Graphics API和平台有关,一般索引值为32~64。
  • 重点关注术语"材质引用",因为如果使用两个不同的材质,但他们的设置相同,则渲染管线的智能并不足以发现这一点,会把它们当成不同的材质,进而不执行动态批处理。其他要求已经解释过了,然而,有几个要求的描述并不直观,需要额外解释。


4.1顶点属性


顶点属性只是网格文件中基于每个顶点的一段信息,通常表示为一组浮点数。顶点属性包括但不限于顶点位置(相对于网络的根)、法线向量(一个从对抗表面指向外面的向量,通常用于光照计算)、一套或多套纹理UV坐标(用于定义一张或多张纹理如何包裹网格),甚至可能包括每个顶点的颜色信息(通常用于自定义光照或扁平化着色、低多边形风格的对象)。只有着色器使用的顶点属性总数小于900的网格才会进行动态批处理。


注意:查看网格的原始数据文件,其中包含的顶点属性信息比Unity载入内存的少,这是由于引擎会将网格数据从原始数据格式转化为内部格式。因此,不要假设3D建模工具提供的顶点属性数量是最终的数量。验证属性数量的最好方式是将网格对象拖到场景中,在Project窗口中找到MeshFilter组件,在Inspector窗口的Preview子区域中查看verts值


在伴随的着色器中,每个顶点使用的属性数据越多,900个属性预算就消耗得越多,从而减少网格允许拥有的顶点数量,这些顶点不能再用于动态批处理。例如,简单的漫反射着色器只能给每个顶点使用3个属性:位置、法线和一组UV坐标,因此,动态批处理可以使用这个着色器来支持总共有300个顶点的网格。然而,在更复杂的着色器中,每个顶点需要5个属性,只能支持不超过180个顶点的网格的动态批处理。另外,请注意,即使咋着色器中每个顶点使用不到3个顶点属性,动态批处理仍然只支持最多300个顶点的网格,因此只有相对简单的对象才适合动态批处理。


这些限制证实场景开启动态批处理之后,尽管所有对象共享相同的材质引用,也仅节省3个Draw Call的原因。Unity自动生成的立方体网格仅包含8个顶点,每个顶点都带有位置、法线和UV数据,总共24个属性,远低于300个顶点和900个顶点属性的上限。然而,自动生成的球体包含515个顶点,因此总共有1545个顶点属性,明显超过300个顶点和900个顶点属性的限制,所以不能动态批处理。


如果单击Frame Debuger中的一个Draw Call选项,就会显示标签为"Why this draw call can't be batched with previous one"的部分。大多数情况下,下方的解释文本说明了哪个条件没有被满足(至少是它检测到的首个条件),以及调试批处理行为的有用方法有什么。


4.2网格缩放


文档清楚地建议,使用负数缩放会对动态批处理产生奇怪的效果。负数缩放通常是镜像场景中网格的快速方式,可以避免创建和导入完全不同的网格,来生成仅沿着某个轴翻转的对象。这个技巧通常用于创建一对门,或只是为了使场景看起来不同。然而,如果与没有缩放或对两个轴进行负数缩放的网格相比,只对1个轴或3个轴进行负数缩放的网格,会放到一个不同的动态批处理中。这与3个值(x,y,z)中哪个是负数无关,仅和负数值的数量是奇数或偶数有关。


在后台,批处理分离行为的另一个奇怪的副产品是,对象的渲染顺序可以决定什么网格能进行批处理。如果先前的对象出现在与当前对象不同的批处理组中,则无法对其进行批处理。同样,最好举例说明。再次假设5个对象:V以(1,1,1)缩放,W以(-1,1,1)缩放,X以(-1,-1,1)缩放,Y以(-1,-1,-1)缩放,Z和V均以(1,1,1)缩放。对象V和Z使用相同的等比缩放,因此它们会被批处理到一起。然而,如果以上述顺序渲染所有对象到场景中,那么V会被渲染,接着Unity测试对象W和V是否可以批处理到一起。由于W有及数个附属缩放,因此不能和V进行批处理。Unity接着比较X和W,检查它们是否可以批处理到一起,依然不行,因为W有及数个附属缩放,而X有偶数个。然后比较对象W-Y和Y-Z,失败的原因都相同,最终结果是5个对象用5个Draw Call渲染,没有机会进行V和Z的批处理合并。注意,只有使用负数缩放,才会产生这个奇怪的效果。


据推测,这是用于检测有效可批处理组的算法的唯一副产品,由于在两个维度上镜像网格,在数学上等价于网格绕相同的轴旋转180°,而没有哪种旋转等价于网格沿着1个轴或3个轴进行镜像,因此所观察到的行为可能只是动态批处理系统自动转换了对象,尽管这并不完全清楚。无论如何,希望这能为生成动态批处理时可能遇到的许多奇怪情况做好准备。


4.3动态批处理总结


渲染大量的简单网格时,动态批处理是非常有用的工具。使用大量外观几乎相同的简单物体时,该系统的设计是非常完美的。应用动态批处理的可能情况如下:


  • 到处都是石头和树木的森林
  • 有很多简单而常见的元素(计算机、走廊、管道等)的建筑、工厂或空间站
  • 一个游戏包含很多动态的非动画对象,还包含简单的几何体和粒子特效(如几何战争等游戏)。


阻止两个对象动态批处理的唯一条件是,它们使用了不同的纹理,就应该花点时间和精力合并纹理(通常称为图集),并重新生成网格UV,以便进行动态批处理。这可能会牺牲纹理的质量,或者纹理文件会变大,但这是值得的。


动态批处理可能对性能造成损害的唯一情况是,设置一个场景,其中有数百个简单对象,而每个批处理中只有几个对象。在这种情况下,检测和生成这么多小批处理组的开销成本可能比为每个网格单独执行Draw Call所节省的时间还要多。即便如此,一般也不会发生这种情况。


简单地假设正在进行动态批处理,则更有可能给应用程序带来性能损失,而实际上我们忘记了其中一个必要条件。推送一个新的网格版本,可以意外地突破定点限制,在Unity将一个原始对象(扩展名为.obj)转换成它自己的内部格式的过程中,生成的顶点属性比预期的要多。要突破定点限制,还可以调整一些着色器代码,或添加额外的过程,但不会取消对象进行动态批处理的资格,甚至可以设置对象来启用阴影或光线探测,但这会破坏另一个条件。


当这些意外发生时,并没有发出警告,只是指出修改后Draw Call的数量在增加,性能也进一步下降了。为了使场景中动态批处理的数量保持合适的水平,需要连续不断地检查Draw Call的数量,并观察Frame Debugger的数据,以确保最新的修改不会意外取消对象的动态批处理资格。然而,与往常一样,如果证实这会造成性能瓶颈,那么仅需关心Draw Call的性能。


总之,每种情况都是各不相同的,需要使用网格数据、材质和着色器进行实验,以确定能动态批处理什么,不能动态批处理什么,并对场景不时地执行一些测试,以确保使用数量合理的Draw Call。


五、静态批处理


Unity通过静态批处理提供了第二种批处理机制。静态批处理功能在几个方面类似于动态批处理,例如对哪些对象进行批处理,取决于运行时它对摄像机是否可见,批处理的内容每帧都不同。然而,静态批处理只处理标记为Static的对象,因此命名为静态批处理。


静态批处理系统有自己的要求:


  • 网格必须标记为Static(具体来说是Batching Static)
  • 每个被静态批处理的网格都需要额外的内存
  • 合并到静态批处理中的顶点数量是有上限的,并随着Graphics API和平台的不同而不同,一般为32~64K个顶点
  • 网格实例可以来自任何网格数据源,但它们必须使用相同的材质引用。


接下来细述这些要求


5.1Static标记


静态批处理只能应用于开启Static标记的对象,具体而言是Batching Static子标记(这些子标记称为StaticEditorFlags)。单击GameObject的Static选项旁边的下三角按钮,会出现一个StaticEditorFlags下拉列表框,该框可以为不同的Static处理过程修改对象的行为。


Static标记的一个明显的副作用是不能修改对象的变换。因此,任何想要使用静态批处理的对象都不能通过任何方式移动、旋转和缩放。


5.2内存需求


静态批处理的额外内存需求取决于批处理的网格中复制的次数。静态批处理在工作时,将所有标记为Static的可见网格数据复制到一个更大的网格数据缓冲中,并通过一个Draw Call传到管线渲染中,同时忽略原始网格。如果所有进行静态批处理的网格都各不相同,那么与正常渲染对象相比,这不会增加内存使用量,因为存储网格需要的内存空间量是相同的。


然而,由于数据是高效复制过来的,因此这些静态批处理的副本会消耗额外的内存,其数量等于网格的数量乘以原始网格的大小。通常,渲染1个、10个或100万个相同的对象,消耗的内存是相同的,因为它们都引用相同的网格数据。在这种情况下,对象之间的唯一区别就是每个对象的变换。然而,因为静态批处理需要把数据复制到一个大的缓冲区,所以这个引用会丢失,原因是原始网格的每个副本都会复制到缓冲区中,每个副本都带着各不相同的数据集,以及附着到顶点位置的硬编码变换。


因此,使用静态批处理渲染1000个相同的树对象,消耗的内存是不使用静态批处理渲染相同树的1000倍。如果没有正确地使用静态批处理,将导致一些严重的内存消耗和性能问题。


5.3材质引用


如前所述,共享材质引用时间少渲染状态变更的一种方式,因此该要求显而易见。另外,有时静态批处理需要更多材质的网格。在这种情况下,所有网格会根据所使用的材质划分到各自的静态批处理组,每个组使用不同的材质。


该要求的缺点是,静态批处理渲染所有静态网格时,使用的Draw Call数量最多只能等于所需的材质数量。


5.4静态批处理的警告


静态批处理有几个缺点。它实现批处理的方式是将网格合并到一个更大的网格中,所以静态批处理系统有一些需要注意的警告。这些警告包括较小的不便和明显的缺点,这取决于场景:


  • Draw Call减少了,但不能直接在Stats窗口中看到,要在运行时才能看到
  • 在运行时向场景中引入标记为Batching Static的对象,不能自动包含到静态批处理中


下面深入讨论这些问题


5.4.1静态批处理的Edit模式调试


试图确定静态批处理在场景中的整体效果有一些困难,因为在Edit模式下,静态批处理没有生效,因此在手动测试之前,难以确定静态批处理提供了什么优势。应该用Frame Debugger来验证静态批处理是否正确生成,以及是否包含了预期的对象。


如果在项目后期才开始启用该特性,可能会有问题,因为此时需要花费大量时间启动、调整、重启场景,以确保节省了期待节省的Draw Call。所以,最好在构建新场景的早起开始进行静态批处理优化。


不言而喻,静态批处理创建工作并不完全是琐碎的,如果有许多批处理要创建,或有许多大型对象要批处理,那么场景初始化时间可能会显著增加。


5.4.2在运行时实例化静态网格


在运行时添加到场景中的任何新对象,即使它们标记为Batching Static对象,也不会由静态批处理系统自动合并到任何现有批处理中。自动合并会导致重新计算网格和与管线渲染同步时造成巨大的运行时开销,所以Unity甚至不会尝试自动合并。


大多数情况下,应该尝试让任何期望被静态批处理的网格出现在场景的原始文件中。然而,如果需要动态实例化,或者使用叠加方式加载场景,就可以使用StaticBatchUtility.Combine()方法控制静态批处理。该工具方法有两个重载形式:一个重载形式需要提供根GameObject,该对象中所有带网格的子GameObject对象都会转换到新的静态批处理组中(如果使用了多个材质,就会创建多个组);另一种重载形式需要提供GameObject列表和一个根GameObject,该重载形式会自动将列表中的对象作为根对象的子节点,以相同的方式生成新的静态批处理组。


应该分析一下StaticBatchUtility.Combine()的用法,因为如果有许多顶点要合并,那么该操作的开销将非常大。它也不会将给定的网格与任何预先存在的静态批处理组合并在一起,即使它们使用相同的材质。因此,无法通过实例化或叠加加载的静态网格来减少Draw Call,这些静态网格使用的材质与场景中已经存在的其他静态批处理组相同(它只能与在Combine()调用中分组的网格合并)。


提示:如果调用StaticBatchUtility.Combine()方法进行批处理之前,GameObject没有被标记为Static,GameObject就一直是非Static,但网格自身是Static。这意味着GameObject、它的Colider组件和其他任何重要对象可能被意外移动,但网格依然留在原处。对于在静态批处理的对象中意外混合Static和非Static状态,要特别小心


5.5静态批处理总结


静态批处理是一种强大但危险的工具,如果使用不当,就很容易因为呢诶村小号和应用程序的渲染成本造成巨大的性能损失。它还需要大量的手动调整和配置,以确保正确生成批处理,不会由于使用各种Static标记而意外引发一些不期望的负面效果。它有一个显著的优势,可以用于不同形状和巨大尺寸的网格,这是动态批处理无法实现的。


相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
2月前
|
存储 人工智能 Java
Unity优化——脚本优化策略4
Unity优化——脚本优化策略4
|
3月前
|
安全 Java 图形学
Unity3D 导出的apk进行混淆加固、保护与优化原理(防止反编译)
Unity3D 导出的apk进行混淆加固、保护与优化原理(防止反编译)
30 0
|
2月前
|
人工智能 安全 API
Unity优化——加速物理引擎1
Unity优化——加速物理引擎1
|
2月前
|
存储 人工智能 缓存
Unity优化——脚本优化策略3
Unity优化——脚本优化策略3
|
2月前
|
存储 缓存 Java
Unity优化——脚本优化策略2
Unity优化——脚本优化策略2
|
2月前
|
存储 XML 缓存
Unity优化——脚本优化策略1
Unity优化——脚本优化策略1
|
8月前
|
开发框架 Java .NET
《unity游戏优化》第8章内存优化
《unity游戏优化》第8章内存优化
|
4月前
|
安全 Java 图形学
Unity3D 导出的apk进行混淆加固、保护与优化原理(防止反编译)
对于辛辛苦苦完成的apk程序被人轻易的反编译了,那就得不偿失了,这篇文章就是解决Unity打包出来的包进行代码加固和混淆。
|
5月前
|
Java 定位技术 图形学
Unity客户端开发优化要点
Unity客户端开发优化要点
|
8月前
|
前端开发 图形学 异构计算
Unity优化之Drawcall
Unity优化之Drawcall
266 0