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标记而意外引发一些不期望的负面效果。它有一个显著的优势,可以用于不同形状和巨大尺寸的网格,这是动态批处理无法实现的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/210811.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】cat 命令使用

cat 命令 cat(英文全拼:concatenate)命令用于连接文件并打印到标准输出设备上。 可以使用cat连接多个文件、创建新文件、将内容附加到现有文件、查看文件内容以及重定向终端或文件中的输出。 cat可用于在不同选项的帮助下格式化文件的输出…

LV.13 D1 嵌入式系统移植导学 学习笔记

一、嵌入式系统分层 操作系统:向下管理硬件、向上提供接口 操作系统为我们提供了: 1.进程管理 2.内存管理 3.网络接口 4.文件系统 5.设备管理 那系统移植是干什么呢? 就是将Linux操作系统移植到基于ARM处理器的开发板中。 那为什么要移植系…

【calcitonin ; 降钙素 ;降钙素原】

Parathyroid_Hormone -甲状旁腺激素 PTH ; 特立帕肽;

『OPEN3D』1.8.2 全局ICP配准

前文提到的多种icp方式均需要初始的变换函数作为配准过程的初始值,并在该初始值上进行迭代优化得到结果;那么global icp为前面这些精配准的icp提供了初始变换函数。因此global ICP配准后可视化的点云结果可能没有完全配准,需要再进行一次精配准操作。 global icp需要对点云提…

lightdb plorasql集合类型新增可变数组

文章目录 背景集合类型可变数组可变数组示例 背景 在信创适配中,从Oracle迁移过来的存储过程使用到可变数组。因此在LightDB-X 23.4版本中对现有的集合类型进行了增强,添加了可变数组类型。 集合类型 在LightDB-X 23.4版本开始plorasql支持的集合类型…

【SQL开发实战技巧】系列(四十八):Oracle12C常用新特性☞多分区操作和管理

系列文章目录 【SQL开发实战技巧】系列(一):关于SQL不得不说的那些事 【SQL开发实战技巧】系列(二):简单单表查询 【SQL开发实战技巧】系列(三):SQL排序的那些事 【SQL开发实战技巧…

K8s构建的mysql无法远程连接

最近在写一个老师布置的大作业,都是老师写好的yaml文件,都是没问题的,但是构建的mysql无法远程连接。 尝试了网上的很多方法,都失败了,我的构建过程应该是没什么错误的,所以网上的方法并不奏效&#xff0c…

【小白专用】Sql Server 连接Mysql 更新23.12.09

目标 已知mysql连接参数(地址和用户),期望通过Microsoft Sql Server Management Studio (以下简称MSSSMS)连接Mysql,在MSSSMS中直接查询或修改Mysql中的数据。 一般是选最新的版本下载。 选64位还是32位&a…

C++ 对象的初始化和清理:构造函数和析构函数

目录 构造函数和析构函数 构造函数 析构函数 构造函数的分类及调用 括号法 显示法 隐式转换法 拷贝构造函数的调用时机 使用一个已经创建完毕的对象来初始化一个新对象 值传递的方式给函数参数传值 以值方式返回局部对象 构造函数调用规则 初始化列表 类对象作…

【Java 基础】27 XML 解析

文章目录 1.SAX 解析器1)什么是 SAX2)SAX 工作流程初始化实现事件处理类解析 3)示例代码 2.DOM 解析器1)什么是 DOM2)DOM 工作流程初始化解析 XML 文档操作 DOM 树 3)示例代码 总结 在项目开发中&#xff0…

Jupyter notebook修改背景主题

打开Anaconda Prompt,输入以下内容 1. pip install --upgrade jupyterthemes 下载对应背景主题包 出现Successfully installed jupyterthemes-0.20.0 lesscpy-0.15.1时,说明已经下载安装完成 2. jt -l 查看背景主题列表 3. jt -t 主题名称(…

【LeeCode】18.四数之和

给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] &#xff08;若两个四元组元素一一对应&#xff0c;则认为两个四元组重复&#xff09;&#xff1a; 0 < a, …

mysql的BIT数值类型

MySQL :: MySQL 8.2 Reference Manual :: 11.1.5 Bit-Value Type - BIT MySQL :: MySQL 8.2 Reference Manual :: 9.1.5 Bit-Value Literals BIT类型用来存放bit值&#xff0c;每一位是0或者1&#xff0c;允许1-64位。 例如&#xff0c;下面表定义了new这列的类型为8位的BIT…

NestJS的微服务实现

1.1 基本概念 微服务基本概念&#xff1a;微服务就是将一个项目拆分成多个服务。举个简单的例子&#xff1a;将网站的登录功能可以拆分出来做成一个服务。 微服务分为提供者和消费者&#xff0c;如上“登录服务”就是一个服务提供者&#xff0c;“网站服务器”就是一个服务消…

Python如何实现数据驱动的接口自动化测试

大家在接口测试的过程中&#xff0c;很多时候会用到对CSV的读取操作&#xff0c;本文主要说明Python3对CSV的写入和读取。下面话不多说了&#xff0c;来一起看看详细的介绍吧。 1、需求 某API&#xff0c;GET方法&#xff0c;token,mobile,email三个参数 token为必填项mobil…

python在线读取传奇列表,并解析为需要的JSON格式

python在线读取传奇列表,并解析为需要的JSON格式,以下为传奇中使用的TXT列表格式, [Server] ; 使用“/”字符分开颜色,也可以不使用颜色,支持以前的旧格式,只有标题和服务器标题支持颜色 ; 标题/颜色代码(0-255)|服务器标题/颜色代码(0-255)|服务器名称|服务器IP|服务器端…

探索人工智能领域——每日20个名词详解【day13】

目录 前言 正文 总结 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助。 &#x1f4a1;本文由Filotimo__✍️原创&#xff0c;首发于CSDN&#x1f4da;。 &#x1f4e3;如需转载&#xff0c;请事先与我联系以…

Axure网页端高交互组件库, 下拉菜单文件上传穿梭框日期城市选择器

作品说明 组件数量&#xff1a;共 11 套 兼容软件&#xff1a;Axure RP 9/10&#xff0c;不支持低版本 应用领域&#xff1a;web端原型设计、桌面端原型设计 作品特色 本作品为「web端组件库」&#xff0c;高保真高交互 (带仿真功能效果)&#xff1b;运用了动态面板、中继…

UniGui使用CSSUniTreeMenu滚动条

有些人反应UniTreeMenu当菜单项目比较多的时候会超出但是没有出滚动条&#xff0c;只需要添加如下CSS 老规矩&#xff0c;unitreemeu的layout的componentcls里添加bbtreemenu&#xff0c;然后在css里添加 .bbtreemenu .x-box-item{ overflow-y: auto; } 然后当内容超出后就会…

【数据结构第 6 章 ②】- 用 C 语言实现邻接矩阵

目录 一、邻接矩阵表示法 二、AMGraph.h 三、AMGraph.c 四、Test.c 【数据结构第 6 章 ① 】- 图的定义和基本术语-CSDN博客 由于图的结构比较复杂&#xff0c;任意两个顶点之间都可能存在联系&#xff0c;因此无法以数据元素在存储区中的物理位置来表示元素之间的关系&…