/images/avatar.png

Papalqi

[译]Low-level Thinking in High-level Shading Languages

很多聪明人写的代码,本可以以更好的方式编写。 偶尔我会听到“这是未优化的”(this is unoptimized)或者这只是个“例子”(educational example)作为借口,但大多数时候这个借口都不成立。因为他们其实不知道如何做到正确。 此外,供应商的SDK示例中的代码也不总是正确的。当最优秀的牛人都做得不好时,这是一个行业的问题。 1 (x – 0.3) * 2.5 = x * 2.5 + (-0.75) 很久之前,我们需要写汇编,从2003年之后的一切都是HLSL或者GLSL。这当然是一种进步,我们使用更高层次的抽象语言进行shading。这没毛病,但是,随着硬件与我们正在使用的抽象内容之间的差距的扩大,人们与硬件失去联系的风险就越来越大了。如果我们只看到HLSL代码,而从来没有不知道GPU运行的是什么,这将是个严重的问题。本文要传达的信息是,在使用高级着色语言时保持低级别(Low-Level)心态对于编写高性能着色器至关重要。 这表现的很清楚,为什么我们应该使用低级别的思考。我们只移动一些东西,加一些括号,就能实现一个更快的着色器。这是通过理解底层硬件并将HLSL构造映射到它从而来实现的。 本文使用的硬件是Radeon HD4870((用于生成最易读的反汇编代码)),但本文中的大部分都是通用的,除非另有说明否则适用于任何GPU。硬件之间具有诸多的不同。 即使您没有观察到特定GPU的性能提高,也有可能对其他的GPU有所提升。GPU有大量的ALU。如果我们将ALU的利用率从50%降低到25%,这却并不一定能够提高性能。因为同时其会受到其他因素的限制(贴图和内存带宽等)因此可能不会提高性能,但可以让你降低功耗,并且为新feature留出更多空间。最后如果你解决了TEX/BW,会给你带来相对于的收益。 “编译器将优化它” 编译器不懂你的思想。编译器只理解着色器中的操作的语义。他们不知道你在努力完成什么。许多可能的优化都“不安全”,因此必须由人来完成。 他们没有整张贴图数据 他们只有有限数据 他们不能打破常规 这可能认为的最简单的代码示例,您可能认为它可以自动优化为使用MAD指令,而不是ADD+MUL,因为这两个常量都是立即数。 然而编译器并不是这么想的。 驱动程序仅限于传递的D3D字节码的语义。最终在GPU上执行的代码就是你写在着色器上的内容!大多数情况下你可以在PS3看到一样的结果。但是上面这种情况例外,可能是因为那里的常数为1.0f。其他的情况都不会变为MAD。 Xbox360着色器编译器很有趣。 它什么都不在乎。 即使你明显破坏了数字,你也会一直做这个优化。 即使它导致常量溢出或下溢为零,也无关紧要。 如果上述式子中1的位置是一个常数,GOGOGO。上车,直接优化 如果上述式子中1的位置是0,这样的话MUL不就好了吗? 太棒了! 所以当然会产生很多小误差。 如果在此转换中丢失了许多浮点数精度,则不会立即注意到为什么会发生这种情况。 我们在这里处理IEEE的浮点数。更改操作顺序并不安全。如果你运气好,你会得到同样的结果。 改变顺序更可能会提高精确度。但是,根据数值的不同,并不总是行得通。 在最坏的情况下,它可能会被上溢或下溢破坏,或者如果不进行优化,它可能会在正确运行的部分返回NaN。一般来说,编译器擅长于:移除死代码,消除未使用的资源,折叠常数,寄存器分配,代码调度;但通常不会:更改代码的含义、中断依赖关系、违反规则。 例如我们的x = 0.2f sqrt(0.1f * (0.2f - x)) ,returns 0 sqrt(0.02f - 0.1f * x) ,returns NaN 这种差异是因为第二个表达式将非常小的负值传递给sqrt。 请记住,0.

蓝图的一些细节

一些基本的Ctrl和Alt类似的基本操作就不说了。 快捷键T可以防止我们选中透明物体,当有大量的雾气或烟雾并且干扰对象选择时,可以使用此功能。 蓝图书签功能 当您按下☆图标时,将显示[新书签]注册框,输入书签名称即可。 这将帮助你快速阅读蓝图和进行跳转。 具有快速跳转功能的蓝图迭代 在[编辑器首选项]-> [常规]-> [键盘快捷键]的“图形编辑器”中指定。 您可以使用Ctrl + N(N:数字键)进行保存,并使用Shift + N(N:数字键)进行移动。 使用快速跳转移动如果已经使用Ctrl + 1,Ctrl + 2,Ctrl + 3保存了蓝图视图,则可以输入Ctrl + 1,Shift + 2,Shift + 3到达你保存时的地方Ctrl +N。。 蓝图宏的开销 宏以与C ++宏相同的方式替换节点。其特征是宏的节点在宏节点所在的位置扩展。与函数不同,除非放置了节点,否则不会对其进行编译。放置的节点像内联函数一样展开,因此调用它的成本应比调用函数的成本低。 仅添加矢量的函数和宏就是这样。 宏为321毫秒,函数结果为392毫秒。宏仍然更快,但是它可能没有您所关心的那么大。由于包括了逻辑的复杂性,函数和宏之间的速度差异变得更加明显。当然,这种加法会产生这样的差异,因此,如果它成为复杂的逻辑,则可能会出现明显的速度差异。 在UE4中,当您双击BP节点时,Visual Studio(或Xcode)将自动启动并打开实现该BP节点的代码。 当您想详细了解如何使用节点和处理内容时,这很方便,但是说实话,有时在不使用BP的情况下意外双击就可能导致Visual Studio在未经许可的情况下启动. 在这种情况下Navigate to Native Functions from Blueprint Call Nodes,让我们禁用编辑器设置!即使双击该节点,Visual Studio也不会在未经许可的情况下启动! 拉直连接和左对齐快捷方式 如果像我一样,希望将蓝图组织得井井有条,您可能已经知道“拉直连接”工具,该工具可让您在选定节点之间建立直连。 不幸的是,默认情况下,此选项没有快捷方式,但是经过深思熟虑的UE4允许您定义一个。我将说明如何进行这些简单的步骤,并在以后节省大量时间。 首先,我们需要进入Editor Preferences,然后进入Keyboard Shortcuts部分。在此处,我们将在搜索栏中输入“straig”以查找与“拉直连接”命令。可以设置两个绑定,个人而言,我只在H键上设置了一个(它既快速又易于使用,并且尚未绑定到其他东西)。 然后,我们可以使用我经常使用的“向左对齐”选项进行相同的操作。就个人而言,我将其绑定到V键。 现在,一切就绪。我们可以在图形编辑器中对其进行测试。只需选择要排列的节点,然后按H(如果要水平对齐节点)或按V(如果要垂直对齐节点)。 编辑器蓝图组件 Asher在他的Inside中就是用这个功能做了自己的高度雾的生成工具。 创建一个小部件。首先,我们需要创建一个类型为Editor Widget的新资产。对于此示例,我们将仅制作一个小的“ Hello World”按钮。编辑器小部件窗口应如下所示: 在小部件的事件图中,我们添加以下简单行为: 运行小部件。这个小部件非常简单,但是它显示了为编辑器创建UMG小部件的主要原理。现在,我们要对其进行测试。 当我们单击按钮Hello World时:在视口中将看到日志行“ Hello World”。假设我们处于编辑器上下文中,则可以调用任何编辑器函数。

关闭双击蓝图跳转VistualStudio

在UE4中,当您双击BP节点时,Visual Studio(或Xcode)将自动启动并打开实现该BP节点的代码。 当您想详细了解如何使用节点和处理内容时,这很方便,但是说实话,有时在不使用BP的情况下意外双击就可能导致Visual Studio在未经许可的情况下启动. 在这种情况下Navigate to Native Functions from Blueprint Call Nodes,让我们禁用编辑器设置!即使双击该节点,Visual Studio也不会在未经许可的情况下启动!

UE4 VirtualTexture 源码解析

一.基础、原理和使用 Unreal最早从2017年就开始进行VirtualTexture功能的研发工作。4.17版本初次的提交,4.23 版本正式放出SVT和RVT,4.26 版本推出Adaptive VT。 按照使用上的不同,在引擎中一共有四种不同的VT类型 UVirtualTexture2D直接是继承于UTexture2D的,是最普遍上的的VT类型。 ULightMapVirtualTexture2D跟 UVirtualTexture2D没有什么太大的区别,主要是使用在LightMap中使用。 URuntimeVirtualTexture,RVT,后面会讲到其最终的用法。 如果RVT勾选了Adaptive,那么RVT则会变成AdaptiveRVT。 大致上我们按照生成的时机不同,主要分为两种VT的格式。UVirtualTexture2D和ULightMapVirtualTexture2D叫做SVT(Streaming Virtual texture)。而URuntimeVirtualTexture和他的衍生类别AdaptiveRVT称之为RVT((Runtime Virtual texture))。 1.1Streaming Virtual Texture VT功能并不是UE4引擎默认开启的,如果我们想要使用Virtual texture ,需要在项目设置中设置Enable Virtual Texture Support。而勾选Enable Virtual texture lightmaps,则会将我们的Lightmap变为VT形式。 1.1.1 普通贴图转换为Virtual Texture 当我们重启编辑器后,我们可以把之前普通的贴图右键点击转换为VirtualTexture。使用这种方式编辑器将检索与之相关的材质使用。会将会改变使用到这张贴图的材质的采样方式。 多选批量转换当然也是支持的。 我们也可以在贴图的设置中去设置这张贴图是否是VirtualTexture。 不过,这样我们就必须手动的去调整材质里对于VT的采样方案。不然材质会报编译错误 1.1.2 导入为VirtualTexture 之前的操作是把项目中已知的贴图进行转换,当我们想要新增贴图时,由于贴图的大小限制,我们可以把一张超大的贴图切割开,然后同时导入多张texture ,如果他的命名规则如下图所示那么他就会按照UDIM多象限UV进行排列并且识别为VirtualTexture。 当然,如果我们只是导入一张贴图的话,会根据项目中的配置来进行判断,分辨率如果大于这个设置,将会转换为VirtualTexture。如果比这个小的话,将视为普通的贴图处理。 1.2 Virtual Texture的目的 在这里我们希望去讨论VT的实现目的。假设我们的游戏画面是静止不动的,并且如果我们能够实时创建这么一张贴图来显示我们的场景,我们假设屏幕分辨率是1920x1080 ,用RGB8来进行存储。 920x1080x3 =6075KB≈5.93MB,(显存利用率) 也就是说,我们显卡的显存只需要用 6MB 就完全够了。这种极端的情况用现有的硬件实现起来恐怕不太现实。这种情况很难在一个动态的场景中实时的去做到,但是即使是有一些cache的冗余数据,就算到了100倍的冗余,其空间使用也小于我们目前的Texture Pool的大小设置。 因此,Virtual Texture的基本思路就是:把能看到的贴图才加载进来,没看到的不加载;进的精度高,远处的精度低。如下图所示,如果我们只把看到的部分(高亮的部分加载进来)那么内存占用上将大大减少。 从中我们可以看出Virtual Texture实则是解决我们空间存储的问题。世间所有的炼金术都遵循等价交换,你既然节省了空间,那么你的时间消耗则会增加,这是一个以时间换空间的技术:在每个Tile动态加载卸载的过程中和VT采样时都将产生额外的性能开销。因此我们是否需要开启,将哪些贴图进行开启,都是一个值得权衡的问题。而不是无脑的去使用。 既然解决的是贴图的内存管理问题,其跟传统的基于Mipmap的贴图和可见性的Texture Streaming管理是不同的。传统的StreamMgr只会根据可见性和对应的Mipmap来进行流送的处理,也就是说我们并不能对单张贴图的某一部分进行更为精确的控制,也就是有了过多的冗余信息。例如我只看到了一个贴图的一个角落,却要把他整个都加载进来。 而VirtualTexture 这种将整个Texture分为Tile 的方案,则考虑了这一冗余数据的处理。VT的劣势也很明显,在镜头剧烈变化的时候,其会不停的进行加载卸载的操作,如果配置不当会导致明显的帧率问题。 1.3 Runtime Virtual Texture 首先我们将解释一下RVT(Runtime Virtual Texure) 和普通的SVT(streaming Virtual Texture)的区别。RVT是一张动态在场景中获取的VT,因此所有的数据并不需要存储在硬盘中,而是在我们需要某一块时(相机看到的时候)直接渲染出来。

ActorPalete

UE4 .26或悄悄添加了曾经是“演员面板(Actor Palete)”插件的东西。顾名思义,这个actor调色板是一个插件,它可以让您加载其他level的角色从它们拖动到当前level。 一旦激活了Actor Palette插件,请从顶部菜单的“窗口”->“ Actor Palette”中打开一个Actor Palette。 当打开一个新窗口时,它将如下所示。 您可以在Actor Palette中打开一个关卡,方法是从Actor Palette窗口中选择“打开关卡”,然后选择您实际要打开的关卡。

Niagara 源码架构解析

从git上的信息其实可以看到,Niagara其实在引擎中存在的时间非常久,在4.0版本就已经存在。但是直到2018年的4.17中公开出来。4.17~4.24 为实验版本,4.25 之后正式版本。但是从目前官方的对于Niagara fix的代码提交上看,还是有相当多的编辑器问题修复。 17年才将Niagara从Engine内部移植到插件中。 随着长久的历史变迁,Niagara已经进行了多次的文件位置上的转移,目前的位置是在引擎的插件中。 Niagara在UE4开始就已经在构思做了,目的就是重构继承于UE3的Cascade。Niagara当时的设计目的就是data driven并且扩展性更高的粒子系统,解锁模拟和渲染,所以有图形化的节点能不需要程序来实现模拟的逻辑功能。所以2014年时引擎实现了向量化(指令并行)的虚拟机。 后续为了进一步提高了并行性,加入了compute shader的GPU支持。 本文主要设计Niagara 的主要代码框架结构,对其内部可能的其他一些知识体系。本接下来文章介绍Niagara 的编译和反射系统。因此本文就不在提及过多关于这方面的内容。 我们照例观察一下整个代码的文件组织。 Niagara:所有的运⾏和逻辑构成 。 NiagaraAnimNotifies:可以在动画的过程中进行事件通知播放Niagara。 NiagaraCore:⼀些通⽤的类 NiagaraEditor:编辑器内的可视化编辑节点。 NiagaraEditorWidgets:Niagara 的UI 。 NiagaraShader:Niagara 的shader 。 NiagaraVertexFactories:组成MeshBatch的顶点⼯⼚。 一.基本概念 1.1system 和Emitter 我们通常在编辑器中使用的是Niagara资源主要有两种,一种是我们的Niagara发射器Emitter,一种是我们的Niagara系统System。并且我们发射器是由我们的Module共同组成,从而完整的形成我们的整个Niagara系统。其设计理念相比与Cascade,拆分的更加的细致,从而能偶比较细致的组合和复用。 我们本次并不关心Module所能代表的发射和编译模块,我们重点关注的是Emitter和System所对应的运行时数据。 所有使用Niagara 的地方都需要我们的UNiagaraComponent来进行,其主要是进行控制和Niagara系统的交互。 我们在编辑器中资源对应的是UNiagaraSystem和UniagaraEmitter。一个UNiagaraComponent会拥有指定的一个UNiagaraSystem对应。但是UNiagaraSystem并不是我们运行时直接使用的数据。我们会在UNiagaraComponent初始化时构建对应UNiagaraSystem的FNiagaraSystemInstance,同样System里面的UNiagaraEmitter会生成对应的FNiagaraEmitterInstance。这如同类和实例的关系一般。 对于每个FSystemInstance,会有⼀个相对应这个System类型的的Simulation,它和Instance并不是⼀对⼀的 关系。如果场景中存在同⼀个UNiagaraSystem的多个实例,其只对应⼀个 FNiagaraSystemSimulation。也就是说,对于数据结构⼀致的实例,会使⽤同⼀个Simulation进⾏模拟。因为相同的System的Spawn和Update与数据存储的大小和类型是相同的。提高缓存一致性和数据的存储效率。 所有的FNiagaraSystemSimulation都由FNiagaraWorldManager统一进行管理。 1.2 运转流程 Niagara将有Initial和Update两个重要阶段。其中Initial只在一开始运行一次,Update则每帧运行。 而根据发生的位置不同分为SystemUpdate,EmitterUpdate和ParticleUpdate,分别代表每个System,每个Emitter,和每个粒子的更新。 例如我们spawn一个NiagaraActor,对应的NiagaraSystem中有两个Emitter,每个Emitter发射1000个粒子。 我们排除一些粒子间事件这种情况。那么当我们在初始化时,会调用一次SystemSpawn,调用各自的Emitter的EmitterSpawn,调用各自粒子的共两千次的ParticleSpawn。之后除非有新的Particle生成会调用particleSpawn,否则不会再调用任何Spawn。 之后,我们每帧都会调用我们的各种Update去更新数据。 1.3 渲染策略 这里忽略Light和component对应的Renderer。我们拥有多种渲染器可供选择。 我们其他MeshDraw一样会拥有⼀个渲染的代理Proxy帮助我们来完成最终的渲染,对于Component来说我们持有的该System 的所有Emiter带有的Renderer都会我们会赋值给Proxy。因此其实我们的最小MeshBatch单位是与Emitter数量相关,其数据被Proxy持有,最终交付渲染,这⾥的渲染和UE4普通的渲染架构并没有什么不同,都是统⼀的框架。 由于Niagara的可编程性,其实现功能的潜力是巨大的。它可以非常复杂,但是我们剖析其本质上是一种空间位置模拟的系统之后,我们就能够抓住其关键就是更新它需要的关键数据。更新FNiagaraDataSet。 二.模拟数据 不论Niagara Emitter有多少的Module,Module里有多少的脚本,脚本中写了多少东西。我们当然要抓住本质数据,其都是更新某个渲染器所需要的必要参数,也就是如下图所示的信息。 也就是说,只有这些信息是每个渲染器所需要的,其会传递给渲染线程并最终提交渲染。任何其他参数仅仅为中间变量。因此我们首先关心的是这个数据是存储在什么地方的。由于一个Emitter可以拥有多个不同的Renderer。因此其本身存储在Emitter级别,而不是在Renderer级别。 因此我们最终传递给渲染线程的数据存储在每个Emitter实例FNiagaraEmitterInstance的FNiagaraDataSet身上。不论是GPU粒子还是CPU粒子。 我们将FNiagaraDataSet的数据作为每帧更新的数据继续传递。FNiagaraDataSet最主要的数据是两个buffer。 其中一个是当前使用的buffer,一个是我们将要写入的buffer。FNiagaraDataBuffer拥有提供CPU粒子或者GPU粒子的数据。如果是CPU粒子,我们将在CPU做计算写入操作,如果是GPU粒子,我们在CPU端只需要把对应的buffer指定就可以了。 从传递的数据来看,如果我们是CPU粒子,将直接把最终的模拟结果对应的buffer传递给renderer。而GPU粒子则没有最终的数据,所以需要传递给渲染线程的数据则更完整一些。 其封装成FNiagaraComputeExecutionContext。 2.1 CPU粒子模拟数据更新 最开始我们有一个全局的管理器FNiagaraWorldManager。每一个System类型对应的唯一FNiagaraSystemSimulation都将在这里进行注册并且收到它的支配和管理。 我们的FNiagaraSystemSimulation的数据更新分成两个部分。 Tick_GameThread。这里主要是更新一下系统时间等参数,然后调用FNiagaraSystemInstance的Tick_GameThread。在FNiagaraSystemInstance主要是更新System的Parameters和DataInterface。 Tick_Concurrent,这个阶段可以不在Game线程完成。 对于那些新生成的SystemInstance调用它的spawn script。 是调用system update script进行System的更新。 将system模拟的结果注入到emitter的绑定数据中。 调用FNiagaraSystemInstance的Tick_Concurrent。 在FNiagaraEmitterInstance::PreTick中,最主要的是执行Emitter的Spawn和Update脚本。

给UE4项目改名

很多时候我们对一个工程随意地起了一个随意的名字,这很常见,如果我们想要后面修改整个项目的名字,似乎应该是一件简单的事情,但是这个过程充满了陷阱。如果做错了,您可能会无意间破坏您的项目。手动的做当然是一件非常困难的事情,尤其是对于C++的项目,其模块的命名其实并不容易进行修改,在这里介绍一个工具帮助我们自动的修改抿成。在进行这样的大更改之前,最好备份您的项目。当然,请确保重命名时未在编辑器中打开您的项目。 使用流程 我们打开二进制文件。 输入对应的目录:E:\UE4\1 需要注意的是,现在他没办法支持中文名称的更改 然后需要我们输入新的名称: 输入完成后就可以了。 其可处理蓝图和C++工程。 工程地址:https://github.com/UnrealisticDev/Renom 直接下载