UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技 - 博物纳新
【博物纳新】专栏是UWA旨在为开发者推荐新颖、易用、有趣的开源项目，帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果，并探索将其应用到自己项目的可行性。很多时候，我们并不知道自己想要什么，直到某一天我们遇到了它。 今天推荐的项目来自UWA开源库： https://lab.uwa4d.com/Lab/62bbf82fa8103dabd0f87c8b 一、简介 自从Unity推出了DOTS（Data-Oriented Technology Stack，面向数据的技术堆栈）之后，关于其应用一直备受关注。虽然DOTS对性能的提升比较明显，但是由于其不算低的门槛，仍然让许多开发者难以上手。以动画为例，截至目前的Entities 1.0版本仍然没有官方的基于Entities的动画解决方案。本文介绍的插件DMotion就提供了基于DOTS的动画框架和状态机，可以帮助开发者更便捷地使用DOTS制作动画。 二、功能概览 DMotion的基础是另一个开源项目Latios-Framework的Kinemation部分。这个项目使用DOTS建立了一些常用模块的框架，包括物理、音频、动画等等，目前仍在频繁更新中，对于希望学习和使用DOTS的开发者也可以作为参考，链接如下： https://lab.uwa4d.com/Lab/5dffdd368bab6aaf026a65c3 由于Latios-Framework使用了另一种动画压缩方案，即使用了插件Animation Compression Library，这个插件虽然理论上可以用于移动端，但是目前还没有进行相应的适配，因此DMotion目前只能在PC平台上使用。作者提到之后会考虑扩展到移动端上。 目前DMotion的主要功能如下： Current Features (V0.3.4)： Fully Bursted Runtime State Machine Visual Editor Transitions: Boolean, Int, Enum And End Time Simple API For Playing Clips Through Code (See Samples) 1D Blend Tree Animation Events Root Motion (With Writegroup Support, If You Need To Override Default Behaviour) Object Attachment Support For Optimized And Non-Optimized Skeletons State Machine Visual Debugging 之后可能会更新的功能： Planned Features： 2D Blend Tree (Cartesian/Freeform) State Machine Override (A.K.A Animator Override Controller) Substates IK Support Multiple Layers Skeleton Masks 三、使用 Latios-Framework最新的版本（目前为0.6.4）已经支持Entities 1.0，但是DMotion还只支持Entities 0.51版本，因此安装DMotion的Package之后需要安装Latios-Framework的旧版本，建议使用0.5.8版本。 DMotion这个插件的使用整体上是比较简单的，可视化程度高，作者提供了Sample和详细的指导，需要使用代码的地方都有例子可以参考。文档链接如下： https://github.com/gamedev-pro/dmotion/wiki/1.1-Getting-Started:-The-Basics 这里只做一些简单的介绍，比如最简单地播放一个Animation Clip。 首先需要创建一个DMotion的Clip。 然后在Inspector界面中设置好要用到的Animation Clip。 再给需要播放动画的对象添加对应的组件，在组件中做一些设置，添加需要使用的Clip，就可以播放动画了。 点击Play即可播放动画。 动画状态机部分可视化程度很高，和Unity本身的动画状态机的使用方法很接近。 首先创建一个动画状态机。 创建一个新的状态。 添加对应的Animation Clip。 两个动画状态之间可以建立Transition。 Transition可以设置一些参数和条件。 这些内容都可以在文档中找到，因此不再赘述。 四、实现原理 DMotion是一个基于DOTS的框架，这里我们从播放动画的代码开始分析。 继承IConvertGameObjectToEntity是为了调用Convert函数把GameObject转化为Entity 。继承IRequestBlobAssets是为了调用RequestBlobAssets，把Animation Clip转化成DOTS支持的格式，这里使用了插件Latios-Framework中的Kinemation部分，最终会转化成一种BlobAsset，这是DOTS当中针对Streaming做了优化的二进制数据结构。 Convert函数的具体内容，把GameObject转化为Entity并添加对应的Component。 使用RequestBlobAssets把Animation Clip转化成BlobAsset的部分。在运行的时候，DMotion使用的代码都是使用Burst编译的。在System文件中的代码是运行时对Entities的Component进行处理的部分，也就是ECS中的“S”，可以看到这些代码都是使用Burst编译的，并且使用了Job System。 可见DMotion充分利用了DOTS系统，可以预测到其性能表现应该是比较优秀的。 五、性能 首先介绍一下DOTS为什么会比传统方式更快。Unity推出的DOTS主要包括三个方面，分别是ECS、Burst Complier和Job System。Entities是游戏中的事物，或者说是一些数据的集合。Components把与Entity相关的数据组织起来，Systems则是把Components的数据从当前状态转换为下一个状态的逻辑。下图展示了ECS的组织架构。 ECS架构在执行逻辑时，只会操作需要操作的数据。System在操作数据的时候只会收集它关心的Component数据，CPU运行时就会将这一整块内存装入高速缓存中，这样就减少了Cache Miss次数，增加了缓存命中率，整体上提高了程序效率。此外现代CPU中使用的SIMD技术与这种数据密集的架构相性极好，可以进一步提高性能。 ECS模式更加适合现代CPU架构，因为它可以做到高效处理数据，而不用把多余的数据字段存入宝贵的缓存从而导致多次Cache Miss。比如操作Unity对象的Position属性，会把GameObject所有相关数据都加入缓存，浪费了宝贵的缓存空间。而如果在ECS架构下，将只会把Position属性放入内存，节省了缓存空间，也一定程度上减少了Cache Miss。 这种数据结构很适合并行处理。Burst Complier是使用LLVM从IL/.NET字节码转换为高度优化的本机代码的编译器，与Job System一起生成多线程并行处理的代码，充分利用SIMD，多线程操作充分发挥ECS的优势。因此Unity的DOTS往往比传统方式速度更快。 由于DOTS本身的特性，DMotion的性能表现比Animator更为优秀。在文档中作者认为性能提升可以达到大约6倍左右，而我们的实际测试结果为大约快3倍左右。 测试环境： 平台：Windows 10 (10.0.19044) 64bit Unity版本：2021.3.9f1c1 GPU：Intel(R) UHD Graphics 750/Direct3D 11.0 CPU：3.6ghz/11th Gen Intel(R) Core(TM) I7-11700K @ 3.60ghz 测试工具：UWA GOT Online 测试用例： 使用的Model如下，拥有5770个顶点和29个骨骼。 使用的动画是行走的动画，分别使用DMotion和Animator进行测试。 测试内容： 加载并播放了2500个带有动画的模型，测试帧率。 测试结果如下： DMotion： 平均帧率为15.33帧 Animator： 平均帧率为5.5帧 可见在性能上DMotion相比Animator有一定提升，可惜的是目前还不适配移动平台。感兴趣的同学可以关注这个项目的更新情况，也可以尝试在其基础上做进一步的改进。
Magica Cloth当前在物理模拟的效果上相比Dynamic Bone插件更加强大，但是随之而来的性能消耗也不容忽视。本文旨在说明通过简单修改Magica Cloth插件中的部分设置项以及调整一些参数，就能够在它的表现效果和性能之间达到一个平衡。 一、Magica Cloth简介 Magica Cloth是一款用于Unity引擎中布料模拟的插件，提供了类似于Unity的Cloth组件的功能。由于效果逼真的布料模拟运算量巨大，耗时较长，每一帧可能需要几十秒的时间来进行模拟；而在游戏中实现的布料模拟因为需要满足帧率的要求，要在几毫秒的时间内进行物理解算才不至于影响其他系统。因此，游戏中的布料模拟往往是极度简化的，以满足实时性的要求。 图片来源Magica Cloth – Magica Soft 二、Magica Cloth使用 关于Magica Cloth的使用，可以参考官方文档，这里就不再赘述。 https://magicasoft.jp/en/magica-cloth-2 三、Magica Cloth的具体效果 Magica Cloth能够让角色的衣服、头发等受到重力、风等外部作用力的影响，表现出比较自然的运动；能够让衣服、头发在角色运动的时候随着角色的动作比较真实地改变自己的位置和形状。 Magica Cloth提供了四种基本的物理组件：Bone Cloth，Bone Spring，Mesh Cloth以及Mesh Spring。 Bone Cloth可以用来模拟有骨骼的头发的晃动，负载比较轻。 Bone Spring可以用来模拟有骨骼的胸部的简谐运动，负载最轻。 Mesh Cloth基于所选的网格的顶点来进行布料模拟，效果最好但是负载最重。 Mesh Spring可以用来模拟没有骨骼的胸部的简谐运动，负载中等。 和Dynamic Bone相比，Magica Cloth无疑功能更加强大，但是如果只是需要移动角色骨骼来实现物理模拟，Dynamic Bone也能做到比较好的结果并且性能更优。 同时，Magica Cloth相比其他物理插件功能更强大的地方在于它有相对而言比较容易调试的防穿模手段。 Magica Cloth还提供了换装系统，可以在运行时更换角色身上的服装，但是这些服装都是要在离线时做好预设，不能够在运行时修改。 四、Magica Cloth的实现原理 首先，它不依赖于Unity的物理系统，所以Unity自带的物理相关的设置不会影响Magica Cloth的表现。Magica Cloth通过Magica Physics Manager从而在运行时控制物理模拟的全部流程，所以需要在场景中存在这个Manager的时候物理模拟才会生效。 其次，它的实时运算使用了Unity的Job System和Burst Compile，能够把一些复杂的布料模拟的计算转移到工作线程中，降低了主线程的CPU占用。 当我们要使用Mesh Cloth或者Mesh Spring的时候，就需要添加额外的Render Deformer和Virtual Deformer。Render Deformer对模型的Mesh进行变形，而Virtual Deformer则是把所有的Render Deformer整合成一个虚拟Mesh并进行简化，也就是合并顶点和三角形，减少顶点和三角形的数量，降低数据量，然后把这个虚拟的Mesh对应的简化结果分配给各个Render Deformer用来进行Mesh的控制和变形。 为了能够尽可能地解决穿模问题，Magica Cloth可以针对性地设置Surface Penetration和Collider Penetration。其中，Surface Penetration在身体骨骼对裙摆等布料的运动产生影响时会比较有效，而当身体骨骼不会影响服饰的运动时，就需要使用Collider Penetration来手动设置碰撞体。Magica Cloth提供了三种Collider：Sphere、Capsule和Plane。因为主要是用于人物角色的Collider，所以常用的是Sphere和Capsule。对于部分布料模拟中分离的骨骼也可以通过设置Connection Mode进行合并，来防止运动时附加了碰撞体的躯干、大腿等从布料模拟骨骼的间隙中穿出去。 同时，除了防穿模设置外，也可以通过限制骨骼的最大位置和旋转偏移来尽可能限制骨骼节点可能的位置，这也有助于减少穿模现象的发生。 五、Magica Cloth的性能表现 在实际应用中可以发现，Magica Cloth虽然使用了Job System和Burst Compile的方法来进行物理计算，但是它造成的CPU耗时依然较高，官方文档中也提到Magica Cloth组件的耗时是稍高于Unity自身提供的Cloth组件的。 但是，Magica Cloth的优点也有很多。它使用方便：针对角色各个部位的布料模拟提供了很多的预设，例如头发的预设就包括刘海、长发、短发以及马尾等不同表现的参数，制作人员可以依据表现效果进行参数的微调，简化了参数调节的过程；为了防止穿模，它提供了一些参数用于限制骨骼节点的最大位移和旋转，同时也设计了适合于游戏中角色的常见的碰撞体，并给出了防穿模的算法。 相比于可以实现类似物理模拟的Dynamica Bone插件，Magica Cloth的缺点还是在于它较重的CPU负载。同样是对Unity-Chan头发的运动进行模拟，Magica Cloth在使用Bone Cloth组件时在整个测试过程中CPU平均占用为0.96ms，而Dynamic Bone做相同的工作只需要0.24ms。（以下测试案例使用机型均为小米8） Magica Physica的优点则是在于提供了更加丰富的物理效果。Magica Cloth不仅能够控制角色的骨骼节点，对于角色的网格，也可以通过网格的变形来模拟出一定的物理效果，比如说能够控制角色身上的各种服饰随着角色的运动的变形，得出比较好的物理效果。 六、Magica Cloth的性能优化 为了对项目中使用的Magica Cloth组件进行针对性的优化，首先需要了解Magica Cloth的性能消耗的主要来源。为此我们同样是使用Unity-Chan的模型进行该插件的性能消耗的分析。 在官方文档中提到，Magica Cloth依赖Job System和Burst Compile进行物理计算，会在游戏运行过程中使用工作线程来完成大部分的物理模拟计算，而通过GOT Online对测试案例进行分析后可以发现，Magica Cloth插件的主要CPU耗时都会被统计到MagicaPhysicsManager这个函数下。下面将结合该函数的源代码和测试结果进行分析。 1. Magica Physics Manager工作流程 在该函数的Update中主要是检查自定义的GameLoop是否在正常工作，如果自定义的GameLoop没有生效，就执行它的初始化操作。 private void Update() { // 对于 Unity2019.3 或更高版本，请在更新期间检查一次自定义循环注册 // 已注册则通过，未注册则重新注册 // 这是在 PlayerLoop 被其他资产重写的情况下的对策 if (updatePlayerLoop == false) { //Debug.Log("Update check!!"); InitCustomGameLoop(); updatePlayerLoop = true; } } 在自定义的GameLoop中注册了afterEarlyUpdate、afterFixedUpdate、afterUpdate、afterLateUpdate、postLateUpdate以及afterRendering这几个回调。在这些回调中，主要是通过使用PhysicsManagerCompute类的方法来安排各种物理计算的Job，这些Job也是Magica Cloth插件最主要的耗时来源。 继续查看Compute中各个Job的调度，可以发现，绝大部分Job需要执行的总任务数和Particle的数量相关。 jobHandle = job.Schedule(Particle.Length, 64, jobHandle); Particle是Magica Cloth中用来表征网格顶点或者骨骼节点的数据结构，在离线生成序列化数据时，可以为网格顶点或者骨骼节点设置Move、Fixed和Invalid三种标记，其中只有被标记为Invalid的网格顶点或者骨骼节点才不会参与物理计算，也就是说不会对上面所说的Particle.Length产生影响。因此，在制作过程中尽可能地减少Move和Fixed节点是最有效的降低物理模拟耗时的方法，当然，这样做也可能会带来一定的表现效果上的损失，需要根据实际情况来决定到底哪些节点需要进行物理模拟。 2. Delay Unscaled Time的使用与否 在Magica Physics Manager的Update选项下，根据Update Mode的选择是否使用了Delay Unscaled Time，Compute的计算方式有所不同。 不使用Delay Unscaled Time时 if (useDelay == false) { // 即时 OnPreUpdate.Invoke(); Compute.UpdateTeamAlways(); Compute.InitJob(); Compute.UpdateReadBone(); Compute.UpdateStartSimulation(updateTime); Compute.UpdateWriteBone(); Compute.UpdateCompleteSimulation(); Compute.UpdateWriteMesh(); OnPostUpdate.Invoke(); } 使用了Delay Unscaled Time后 if (useDelay) { // 延迟执行 OnPreUpdate.Invoke(); Compute.UpdateTeamAlways(); Compute.InitJob(); Compute.UpdateReadWriteBone(); Compute.UpdateStartSimulation(updateTime); Compute.ScheduleJob(); Compute.UpdateWriteMesh(); // 将先前的结果应用于网格 //Debug.Log($"Delay Job! F:{Time.frameCount}"); } 不同的计算方式也导致了最终表现上的差异。使用Delay Unscaled Time后，因为计算的完成发生在渲染之后也就是在afterRendering回调中，所以实际的物理模拟的结果会比画面慢一帧。为了解决画面和物理不同步的问题，Magica Cloth通过上一帧和当前帧的物理模拟结果进行插值，从而得到一个预测的结果同步给下一帧。 // 未来预测 float ratio = unityPhysics ? fixedFutureRatio : normalFutureRatio; // 因骨骼更新模式而异 pos = math.lerp(oldPos, pos, 1.0f + ratio * moveRatio); rot = math.slerp(oldRot, rot, 1.0f + ratio * angRatio); rot = math.normalize(rot); bonePosList[index] = pos; boneRotList[index] = rot; // 记录未来的预测 futurePosList[index] = pos; futureRotList[index] = rot; 显然，这样做会导致在碰撞检测等物理效果上出现一定的问题，但是由于它在性能消耗上更加优秀，所以可以说是非常重要的优化手段。如果对画面表现和物理结果的同步性和防穿模的要求并不是非常高，完全可以选择延迟更新的模式。在测试案例中，我们对同一个场景使用Delay Unscaled Time更新模式和非Delay更新模式进行了对比，结果表明Delay Unscaled Time更新模式下，插件的耗时大幅度降低。 Delay Unscaled Time更新模式 非Delay更新模式 3. Merge Vertex Distance的选取 在使用Mesh Cloth或者Mesh Spring时，在Virtual Deformer组件中可以调节Merge Vertex Distance，该距离越大，生成的虚拟网格的Partcile数量就会越少，从而在物理模拟时需要的计算量就越小。这种优化的思路仍然是尽可能减少参与物理计算的顶点的数量，从而降低耗时。测试结果也表明，改变这个值可以有效降低插件的CPU性能消耗。 Merge Vertex Distance取0.001时 Merge Vertex Distance取0.01时 Merge Vertex Distance取0.1时 4. Use Skinning的使用与否 同样是在Virtual Deformer组件上需要关注的是Use Skinning选项是否需要勾选。 if (use && IsSkinning && IsChangeBoneWeights && vertexCalc) { // 顶点权重变化 //Debug.Log("Change Mesh Weights:" + mesh.name + " buff:" + bufferIndex + " frame:" + Time.frameCount); MagicaPhysicsManager.Instance.Mesh.CopyToRenderMeshBoneWeightData(MeshIndex, mesh, sharedMesh, bufferIndex); IsChangeBoneWeights = false; } 如果勾选了Use Skinning，则需要对所有顶点的骨骼权重进行重新赋值，在官方文档中也提到，取消Use Skinning可以极大地减少物理计算的负担。它的限制在于当取消了Use Skinning选项后，网格只会受到影响程度最大的Virtual Deformer的影响，可能在一定程度上会影响最终的表现，但是如果对于一个网格的Render Deformer只在一个Virtual Deformer中使用的情况下则可以放心地取消该选项的勾选。勾选和取消勾选的测试结果如下。 勾选Use Skinning 取消勾选Use Skinning 5. Normal And Tangent Update Mode的选择 在使用Mesh Cloth或者Mesh Spring时同样需要关注的是Normal And Tangent Update Mode的选择。 public enum RecalculateMode { None = 0, // 每帧更新法线 UpdateNormalPerFrame = 1, // 每帧更新法线和切线 UpdateNormalAndTangentPerFrame = 2, } 如果在渲染中并不要求持续更新网格的切线或者法线数据，可以把该选项设置为None，减少不必要的计算。 if ((use || IsChangePosition || IsChangeNormalTangent) && vertexCalc) { // 重写Mesh // 将meshBuffer设定为mesh（工作量巨大） // ★目前为止除此以外别无他法，经过思考有2个可以避开的办法： // ★（１）等待未来版本的Unity上进行支持 // ★（２）使用computeBuffer对Shader顶点进行合批（很快，但是因为必须使用特殊Shader所以兼容性较低） MagicaPhysicsManager.Instance.Mesh.CopyToRenderMeshLocalPositionData(MeshIndex, mesh, bufferIndex); bool normal = normalAndTangentUpdateMode == RecalculateMode.UpdateNormalPerFrame || normalAndTangentUpdateMode == RecalculateMode.UpdateNormalAndTangentPerFrame; bool tangent = normalAndTangentUpdateMode == RecalculateMode.UpdateNormalAndTangentPerFrame; if (normal || tangent) { MagicaPhysicsManager.Instance.Mesh.CopyToRenderMeshLocalNormalTangentData(MeshIndex, mesh, bufferIndex, normal, tangent); } else if (IsChangeNormalTangent) { // 返回 mesh.normals = sharedMesh.normals; mesh.tangents = sharedMesh.tangents; } IsChangePosition = false; IsChangeNormalTangent = false; } 三种选项下的测试结果如下所示： None UpdateNormalPerFrame UpdateNormalAndTangentPerFrame 七、Magica Cloth总结 Magica Cloth当前在物理模拟的效果上相比Dynamic Bone插件更加强大，但是随之而来的性能消耗也不容忽视。本文旨在说明通过简单修改Magica Cloth插件中的部分设置项以及调整一些参数，就能够在它的表现效果和性能之间达到一个平衡。 除了上文提到的一些性能瓶颈之外，如果在项目中使用了Magica Cloth提供的Collider Penetration，以及设置了各种位置和旋转的限制时也会导致一定的性能消耗，但它们的影响不及上一节中提到的几个重要的优化点。建议在对Magica Cloth进行性能优化时先尝试通过上述的办法进行修改，再去关注穿模和位置旋转限制等相关设置。
【博物纳新】专栏是UWA旨在为开发者推荐新颖、易用、有趣的开源项目，帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果，并探索将其应用到自己项目的可行性。很多时候，我们并不知道自己想要什么，直到某一天我们遇到了它。 随着越来越多的项目使用URP管线，以及更多的项目的特效压力越来越大，因此本篇文章基于GitHub上的一个项目将其从Builtin管线下移植到URP修改版，给大家提供了URP管线下降低特效GPU压力的做法，以供大家参考。 GitHub项目：OffScreenParticleRendering https://github.com/slipster216/OffScreenParticleRendering 这个库做的工作是将粒子系统或者半透明的渲染对象单独渲染到RenderTexture中，然后再将RenderTexture与不透明的渲染进行混合。单独渲染到RenderTexture的好处是可以通过降低RenderTexture的分辨率从而减少GPU的渲染压力。 Builtin管线下最终融合效果 Builtin管线下RenderTexture的渲染效果 为了能将此功能移植到URP下，我们先说明一下原始项目中Builtin管线下功能实现的关键点，具体细节在原GitHub项目的说明文档中有说明，主要步骤如下图1，2，3，4。 1、主相机中渲染不透明对象，且因为渲染半透明时需要做深度比较，因此主相机的depthTextureMode需要设置为DepthTextureMode.Depth。 2、在OnRenderImage中将_CameraDepthTexture进行DownSample。 3、在OnRenderImage中从主相机复制一个相机，此相机渲染到RenderTexture，且RenderTexture可以调整分辨率，在此相机中单独渲染粒子系统和其他半透明对象。调用拷贝的离线渲染相机的Camera.Render，这样可以保证离线相机的渲染顺序是在主相机之后渲染的，否则设置了RenderTarget的相机会优先比没有设置RenderTarget的相机先渲染。半透明的对象的Shader需要做特殊处理，需要将半透明对象和渲染场景生成的深度进行比较，比不透明对象离相机更近的像素才会被“绘制”到RenderTexture上。 PS：在原始项目中渲染Particle的Shader中少了“COMPUTE_EYEDEPTH(o.projPos.z);”，所以效果看上去不太对，在vert函数中添加上这句就OK了。 4、在OnRenderImage的最后通过OffScreenParticle_Upsample Shader将Particle等半透明的RenderTexture融合到不透明场景中。此Shader中将RenderTexture的颜色通过Blend SrcAlpha OneMinusSrcAlpha的方式进行原色混合，从而和主场景进行混合。 以上就是原始Builtin管线下的实现思路，转到URP下，有几个问题需要思考： OnRenderImage在URP下是不支持的。 是否还需要额外的相机来实现半透明对象的渲染。 如何处理最后的融合操作。 在URP下，对于第一个问题，有专门的RenderFeature可以对应OnRenderImage的操作；第二个问题，并不需要额外的相机来渲染半透明对象，只需要模仿URP自带的CopyColor来实现渲染另外RenderTexture的操作即可，只是BlitCopy使用的材质球不一样；第三个问题，同样用另外一个RenderFeature来实现即可。 URP下的实现效果： URP下RenderTexture中渲染OffScreen Particle的效果如下： 在此工程中，主要的点如下： 1、移除额外的Camera，取消了OffScreenParticleCamera脚本的作用。主相机需要开启Copy Depth。额外加了一个Particle的Layer，粒子特效的层改成Particle了。 2、新增了两个RenderFeature，OffScreenParticleFeature和OffScreenMergeFeature。前面的是用来绘制Particle的，后面的是用来做主场景和Particle融合的。在OffScreenParticleFeature中添加了2个Pass，一个绘制Particle到_ParticleRT，一个对Depth进行降采样，绘制到_CameraDepthLowRes，OffScreenMergeFeature中有一个Pass，使用MergeShader进行融合。 3、OffScreenMergeFeature用到的Shader是MergeShader，根据原工程中OffScreenParticle_Upsample Shader进行修改的。 4、为了让Particle层只在OffScreenParticleFeature中进行渲染，需要将两个Shader的LightMode更改为一个自定义的字符串，并且在OffScreenParticleFeature中将这个“OffScreenParticle”加入到ShaderTagList中，ShaderTagList的值是由Pass中的Tags里面的LightMode的值决定的。 最终在FrameDebugger中确定好渲染队列： 渲染场景中不透明物体； Copy Depth到_CameraDepthTexture中； 使用OffScreenParticlePass进行Particle层对象的渲染，可以通过DownSample来调整RT的大小，原始大小或者1/2或者1/4； 对Depth进行降采样，渲染最终Merge中用到的_CameraDepthLowRes； 使用MergePass进行最终的Merge，使用的是MergeShader。 Unity Demo下载链接： https://pan.baidu.com/s/1Zi1avSMAwWkOjCeFj5k4iA 密码：bksb
【博物纳新】专栏是UWA旨在为开发者推荐新颖、易用、有趣的开源项目，帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果，并探索将其应用到自己项目的可行性。很多时候，我们并不知道自己想要什么，直到某一天我们遇到了它。 今天推荐的两个项目来自UWA开源库：雪地脚印和体积云。 雪地脚印 一、概览 https://lab.uwa4d.com/lab/5b66026ed7f10a201ff999c2 该项目实现的功能是在雪地上踩出脚印，只支持PC平台。 效果如下： 二、实现原理 1. 脚印踩在何处？ 我们可以看到dynamic_texture这个父物体下有一个摄像机（暂时叫它副摄像机），并且在运行状态下，每一个实时踩出的脚印都一一对应着框中一个个实例化出的default_sprite预制体，如下图： 图1：dynamic_texture结构 在dynamic_texture附带的DistanceMapPainter中，最主要的工作就是把脚印的基本信息以颜色的形式附给预制体的颜色信息，如下图： 图2：颜色包含的信息（from DistanceMapPainter.cs） 那这些预制Sprite的意义是什么呢？不只是传递以颜色为表现形式的位置信息，也在通过a通道传递脚印的“寿命”信息。 图3：颜色的a通道（from sprite_fading.cs） 最重要的是，还会保存深度信息： 图4：Sprite中的深度信息（from DistanceMapPainter.shader） 2. 脚印信息如何传递？ 副摄像机在这里会专门“监视”这些实时生成的预制体，然后把它们渲染在PainterRT上（如下图）： 图5：副摄像机的渲染目标 那这张“PainterRT”记录着什么呢？直观上来讲，它记录着如图1中花花绿绿的方块图，上文也提到过了这些颜色包含的是各个脚印的位置等信息，但它们重叠怎么办呢？ 请注意我们刚才提到Sprite预制体中的Shader会保存深度信息，然后看PainterRT的信息： 图6：PainterRT基本信息 它是打开深度缓存的，所以这张RT上保存的是通过了深度检测的点的颜色信息。至于哪些点能通过深度检测呢？按照图4对于深度的写法，那就是处于每个Sprite中心位置最“白”的地方，也就是最靠近摄像机的地方，也就是最后能通过深度检测的地方，那最后这张图的中心处肯定能渲染到RT上。 图7：把深度作为颜色输出之后的PainterRT 为了直观地看到深度图的效果，我们暂时把深度作为颜色输出（看完之后记得改回来哦~），可以看到单个的Sprite是一个白色的“山丘”，越靠近中心越白，而我们看到的这些白色其实已经是通过了深度测试的了，可见每个山丘的中心是一定能通过深度测试的，而其他边缘处，即使记载了脚印信息，但是脚印也很可能覆盖不到那儿（当然，这得看你想把脚印画多大了）。 那么会存在两张图完全重合而使得信息读取不到的情况吗？从上图可知只要不是两张图完完全全重合，就不会互相影响，如果真的是两个山丘完全重合，那么的确会丢失其中一个脚印。这就是为什么操控着角色在一个地方反复徘徊，总有几个脚印画不出来的缘故。 而这张RenderTexture将会作为地面Shader的输入，这样就把位置、角度等信息通过一张图传给了地面Shader。 3. 曲面细分着色器 我们现在来看最重要的地面Shader——SnowGroundShader。 打开并浏览这个Shader，可以看到这个Shader中使用了细分曲面。 图8：曲面细分着色器和几何着色器的标志 关于曲面细分的教程并不多，可以参考《Unity Shader：曲面细分着色器》。 我们主要关注细分计算着色器，因为大部分重点在这一部分。 图9：细分计算着色器中获取脚印信息部分 其中_DistanceFiled就是上文中副摄像机渲染的图，centerPos和angle分别读取了其中保存的位置和角度信息。 至于地面的凹凸起伏，就是在顶点的纵向上加上高度，而高度就是从高度图读取的，如下图： 图10：细分计算着色器中顶点的起伏计算 顶点只要变换了，法线就得重构，这里切线空间的三个向量会根据高度图重新计算，存入o.tspace中： 图11：细分计算着色器中法线重构部分 4. 最后的效果 最后的效果在片元着色器中。 首先视觉效果上，上文的o.tspace中保存的因高度改变而重构的法线会与雪地和脚印的法线之和做一个点乘操作，如下图： 图12：片元着色器中法线合并部分 之后的光照，就是用了Unity封装的LightingStandard_Deferred（该项目用了延迟渲染），这里就不赘述了。 总之，整个流程就是确定位置和角度、根据高度图调整顶点高度、重构法线。对于曲面细分的部分，细分的越精细，效果就越精致。 5. Tips 下图是Tessellation hull constant shader，其功能是用来输出细分因子（Tessellation factor）。细分因子用于在Tessellation阶段告诉硬件如何对Patch进行细分。 我们可以看到这里的factor不是写死的，旨在让需要细分的地方细分，也就是只细分脚印周围的地方，不失为一个优化性能的好办法。 图13：细分因子的实时计算部分 图14：细分因子的实时计算效果 体积云 一、概览 https://lab.uwa4d.com/lab/5bc54ec004617c5805d4e8d0 体渲染是从三维标量数据生成图像的渲染技术。通过对自然现象的测量或数值仿真，体渲染可以绘制出逼真的效果。在视觉艺术和计算机游戏领域中，体渲染经常用于模拟云、雾和火等自然现象。 该篇的CloudSkybox项目是Unity默认程序天空盒着色器的扩展，它就是使用了体渲染技术绘制云。 二、原理概述 该项目主要由两部分组成：基于大气散射的天空和基于光线步进的体积云。 在描绘天空时，通过简略地模拟大气散射过程的算法，我们可以渲染出一个带有瑞利和米氏散射现象的天空；而在描绘云时，则采用了噪声采样和光线步进结合的传统方法，来渲染出云层的体积感。 三、具体实现 1. 大气散射 该项目的主要逻辑都在CloudSkybox.shader和ProceduralSky.cginc中，先看一下顶点着色器： 图1：顶点着色器 顶点着色器重点在vert_sky函数，该函数的实现在ProceduralSky.cginc中。它的作用在描绘地面、天空和太阳的颜色，用的方法来自于Nvidia上一篇很经典的文章： https://developer.nvidia.com/gpugems/gpugems2/part-ii-shading-lighting-and-shadows/chapter-16-accurate-atmospheric-scattering 对于大气散射原理的讲解有很多，这里不再细说，只讲一些标志性的代码段： 图2：scale函数 该函数近似于查找表，计算出来的是某个方向的射线上光强的衰减。该方法虽然简化了计算，但前提条件是要求平均大气密度为2.5%。这是一种比较简化的方法，现在大都用预计算和查找表的方法替代该方法。 图3：衰减公式实现 这里对应的公式主要是衰减和光学密度公式： 其中scale查表查到的是以某个角度发射的射线路径上的光学密度，而（kInvWavelength * kKr4PI + kKm4PI）对应的是上式的散射系数β，所以119行的attenuate就是衰减函数的实现。 最终要满足的是上述方程，而121行的depth和scaledLength分别对应上式的密度函数ρR，M(h)和微元ds。至于相位函数F(θ)和散射系数β是在之后的输出时分别乘上的： 图4：散射公式的完善 这里的很多变量是声明在开头处的一些分母的计算，旨在减少除法的运算次数。 上述公式图来源于以下知乎文章： 《[Rendering] 基于物理的大气渲染》 《【译】【大气散射】[Elek09] 实时渲染参数化的、有多次散射的行星大气》 2. 体积云 我们把摄像机想象成一个射线簇，每条射线都从相机坐标出发，对应着穿过近裁剪平面上每个像素，而云层的描绘就是由于这每条射线穿过的云层的密度不同造成的。一般上体积云的渲染，都会采用光线步进法（Ray marching）来进行云层密度的累加计算，这也是核心部分。 不过在该项目中，光线步进法出现在了两个地方。 第一处是单独封装的MarchLight方法： 图5：光线步进方法 该方法描述了从每个pos出发，沿着光源方向步进到穹顶的射线上，累加的云层密度对光所造成的衰减（通过BeerPowder，也就是比尔定律计算）总共有多少，为片元中计算散射做准备。 第二处在片元着色器中，如下图： 图6：片元着色器中的光线步进 在该循环中，acc累加了每个像素发出的射线上，每个步进点处的云层密度和光源在该点处发生的单次散射的乘积（193行），而在循环结束后，又融合了天空被云层衰减之后的颜色（199行）。 值得注意的是在算散射时，用到了Henyey-Greenstein相位函数，该函数可用于计算向内散射，以在面对太阳的云具有高亮效果。 图7：相位函数 至于云层密度的采样，是用了tex3Dlod采样事先计算好的基于Perlin和Worley噪声的Texture3D，并且通过_Time.x来控制采样点的偏移，实现了云层的移动。 图8：噪声采样函数 3. 性能分析 通过UWA的GOT OnLine工具在高端机型OPPO K9（8G RAM）上对该工程进行的检测来看，数据并不乐观，该机型上的性能简报如下图： 帧率尚不足8帧/秒。下图是CPU耗时情况： 从图中可以看到几乎都是Gfx.WaitForPresentOnGfxThread在耗时，该函数表明GPU的运行压力较大，我们来看GPU的耗时： GPU的耗时均值为124.31ms/帧，这个耗时是非常高的，可想而知在中低端机上该耗时将会更加严重。综上所述，该插件并不适合应用在手机项目中，不过可以用来学习借鉴。
【博物纳新】专栏是UWA旨在为开发者推荐新颖、易用、有趣的开源项目，帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果，并探索将其应用到自己项目的可行性。很多时候，我们并不知道自己想要什么，直到某一天我们遇到了它。 今天推荐的两个项目来自UWA开源库： 1）HDRP Water 2）云影 01 HDRP Water ShaderGragh是继2018版之后出现的一种针对Shader的可视化编程工具，通过在ShaderGragh面板中编辑各种效果模块的连接方式，可以实现不同的效果。该工具将编辑Shader代码的操作简单化和可视化，也提高了Shader的编辑效率。 该项目是通过ShaderGragh制作的简单水面着色器。通过对该项目的学习，读者不仅可以对该工具有初步的认识，还可以对水面的实现原理有所了解。 一、原理概述 该项目从原理上可以划分为三部分：顶点波动、法线流动和边缘着色。 顶点波动：该部分主要计算通过3个参数各不相同的波叠加之后，平面上各个顶点的纵向位移规律。 法线流动：顶点的法线信息是通过法线贴图获取的，通过控制法线贴图的UV坐标偏移来实现水面的流动效果。 边缘着色：该部分主要通过深度图检测水面边缘，而后在边缘处加上了颜色渐变效果。 二、具体实现 1. 顶点波动 在渲染管线中，对顶点的编辑通常会放在顶点着色器（下图中的Vertex模块）中： 图中顶点着色器主要就是在计算Position，而从上半部分可知，可控输入包括3个波的波长、波速和振幅。 打开上图的3WaveGenerator，可以发现这三个作为输入的波的处理方式几乎相同，只不过每部分对应的参数稍作变换，目的是使得三种波之后的叠加避免谐振，从而让最终叠加后的波形显得更加自然。拿其中一个波来看，可以看到只有波的x和z值参与了计算（如下图），因为这里要先模拟水波的横向传播部分： 之后，通过乘上时间参数产生实时偏移，来模拟波在xz面上的传播，也就是最终水波的流动效果： 但是我们知道，波的横向传播并不是真的把波上的某个顶点随着波的传播方向“运送”过去，其作用的只是顶点在纵向的振动位置而已。所以这里计算顶点的x和z归根结底是为了算纵坐标，这里通过正弦函数求得： 并且三个波经过计算而叠加之后，作用的也只有模型上各点的纵坐标而已： 但我们要知道该纵坐标是包含了波的传播特性的，因为它是根据x、z坐标和时间算出来的。所以此时如果观看模型，它已经可以波动，且参与叠加的波越多、差别越大，波动就会越自然，不过由于法线信息尚未更新，因此远处的顶点是看不出起伏状态的（如下图）。 2. 法线流动 之所以称顶点为“波动”，法线为“流动”，是因为顶点只是在纵轴上运动，而法线贴图是在水平方向运动。而贴图的“流动”，其实就是对贴图采样点的定时偏移。其主要部分如下图： 值得一提的是该Shader同时输入了两张法线贴图（如下图）。其中第一张法线贴图描绘的比较陡峭、颠簸（下图左），旨在塑造波长较大的水波，而第二张贴图比较轻微、柔和（下图右），旨在塑造细小的波纹： 一般情况下，水面的法线贴图是需要有两张的，因为这样可以塑造出不同波长的水波互相干涉的场景。下图为单张法线贴图输入（左）和两张法线贴图输入（右）的效果图，可以看出来右边的水面描绘了更多细小的波纹。 3. 边缘着色 边缘需要通过深度来判断，下图中，上面的框是在拿当前点的深度，下面的框是在计算当前点对应的像素在深度图中的深度： 这里用两个深度的差将作为边缘色的分母进行除法（下图中Divide部分），那么可以知道只有深度差比较小的时候才能显现出边缘色，也符合现实中“接近边缘才会明显变色”的情况。最后通过Clamp把颜色限制在合理范围： 下图分别是添加了边缘着色（左）与不添加（右）的效果，可见添加该功能会让场景变得更丰富、真实。 02 云影 在以自上而下视角为主的游戏（策略或建设类）中，云影是比较常见的功能。通过控制其形状、移动等各种参数，可以逼真地反映出天气的变化，这对游戏场景的视觉效果及气氛烘托都起到了比较重要的作用。 本篇的Unity Cloud Shadows就是一种动态、高效的云影工具，不仅可以很方便地调节云影的多种参数，也不会对性能造成很大压力。 一、原理概述 该工具的实现主要依靠的就是光源的Cookie功能。官网解释：“如果创建包含Alpha通道的纹理并将其分配给光源的Cookie变量，则会从光源投射剪影。剪影的Alpha遮罩会调制光源亮度，从而在表面上产生亮点和暗点。这是场景增加复杂性或氛围的好方法。” 该功能的位置如下： 二、具体实现 当运用光源的Cookie时，即使场景中没有物体，Frame Debugger中也会出现下图中下的Draw Dynamic，而Draw Dynamic就是在画云影，也就是下图中的噪声图： 图1：Frame Debugger 然后，我们来看一下CloudShadows.cs。其实最核心的就是RenderCloudShadows这个函数，从它的Blit函数中我们可以看到，在每一个云层所属的for循环中，有且只有一个Pass会被执行： 图2：RenderCloudShadows函数（from CloudShadows.cs） 那具体是怎样被渲染的呢，我们看这个材质球的Shader，也就是CloudShadows.shader，几种Pass工整排列（如下图），分别对应上图红框中不同的混合模式，当然这个混合模式是可以从外部直接选择的。 图3：不同混合模式对应的Pass（from CloudShadows.shader） 几个Pass主要是从片元着色器作了区分，下图以FragSubtract类型的片元着色器举例。从图中可以看到，我们只在乎a通道的结果，这是由于Cookie功能只会识别目标Texture的a通道。 图4：FragSubtract函数（from CloudShadows.shader） 这一点也可以在Frame Debugger中得到体现，只有点击下图中的a通道时，Game窗口才会显现出图像，而其他通道不会让Game窗口显出内容： 图5：Frame Debugger中Draw Dynamic的a通道 该工具提供了许多种云影的预制体，不过每种预制体的差别其实就是下图右侧的三种模式的调节方法不同而已。我们可以看到调节方式主要有三种：混合设置（叠加、差值等）、云层设置（速度、方向等）和贴图设置（噪声图选择等）。 图6：该工具的预制体 该工具的用法其实并不局限于噪声图，依照Cookie的特性，任何有透明通道的Texture都可以被附在该工具上，然后进行叠加、移动等，用法比较灵活。效果图如下： 图7：整体效果图 三、Tips 看图2的309行，其实全篇一直都有两个RT：m_RenderTexture1和m_RenderTexture2，并且312行还要交换一下两个RT的引用，为何呢？ 其实只是在配合Blit函数，它需要有两个RT作为输入，所以我们不得不多创建一个作为“替补”，这也就是为何在下图中，每次都只需要重置m_RenderTexture1了，毕竟我们只需要操作一个，因为只需要一个RT传给Cookie。 图8：每次只重置m_RenderTexture1（from CloudShadows.cs） 四、性能分析 通过UWA GOT Online在低端机型荣耀畅玩9A（4GB RAM）上进行了性能测试后，得到了下图的FPS曲线，其中中间凹陷处是开启了云影功能的时刻。 图9：FPS曲线图 可以看出云影开启之后，FPS均值保持在原来水平，而当我们看具体堆栈，代码层面增加了插件中的CloudShadows.Update，不过耗时很低，大约0.7ms： 图10：CPU耗时曲线图 该插件是一个轻便又简单的插件，值得一试。
【博物纳新】专栏是UWA旨在为开发者推荐新颖、易用、有趣的开源项目，帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果，并探索将其应用到自己项目的可行性。很多时候，我们并不知道自己想要什么，直到某一天我们遇到了它。 今天推荐的项目来自UWA开源库：https://lab.uwa4d.com/lab/5b561f86d7f10a201fd8422e 一、概览 AO（Ambient Occlusion）是一种基于全局照明中的环境光（Ambient Light）参数和环境几何信息来计算场景中任何一点的光照强度系数的算法。AO描述了表面上的任何一点所接受到的环境光被周围几何体所遮蔽的百分比，因此使得渲染的结果更加富有层次感，对比度更高。 该项目介绍了一种快速的基于顶点的AO算法，将AO看做模型顶点的一个属性，通过深度相机对模型多位置的采样，结合模型顶点的深度校验来计算对应的AO值。该方法相比较于SSAO，是一种比较小巧快捷的简易方法。通过对该项目的学习，读者可以初步了解AO算法的思想，也可以学到该算法的具体简化策略。 二、原理概述 总体来说，该算法可以分为两个步骤： 1.采样：通过改变深度相机的位置并对目标物体渲染，把各个位置获取到的深度图保存下来作为采样数据。 2.计算AO：将采样数据通过深度校验来识别出需要遮蔽的模型顶点并按照一定权重累加遮蔽值，以达到阴影效果。 该方法并不是一个实时计算的方法，而是一个一步到位的方法，本质上是在运行刚开始时就计算好了一个带有AO的材质并替换掉模型原有的材质。所以它并不会随着光照的改变而改变，通俗来讲就是“把容易变暗的地方的颜色画得暗一点”。 三、具体实现 从geoAO.cs来看，所有的工作都是在Start()中完成的，如下图，而采样和计算AO的主要逻辑大都在DoAO()中。 1. 采样 该步骤需要一个深度摄像机来采样深度图，摄像机的创建在CreateAOCam()中完成： 该摄像机从创建开始就是隐藏状态（148行），因为我们并不需要OnRenderImage()每帧都执行，我们想要它“按需执行”，确切来说是为了让Blit函数“按需执行”（如下图），那么我们可以通过把相机隐藏，用Camera.Render()来手动调用OnRenderImage()。 除此之外，我们也注意到为了模拟平行光效果而把摄像机模式改成了正交（150行），且开启了深度模式（158行），这时一个深度相机就已经准备完成了。 采样环节最核心的环节如下图，该部分在DoAO()函数中： 首先，该for循环的循环次数对应着采样次数，也就是最终渲染出来的深度图样本数量，而样本个数决定了AO的精度高低： 在每一轮采样过程中，AO摄像机（AOCam）作为一个单独的深度摄像机，会到达一个新的已经计算好的位置（256行），然后看向模型（257行），接着把准备好VP矩阵传给Shader，最后渲染。 这里可以注意到276行就是在手动调用OnRenderImage()函数，但其实计算AO这一步就是在276行在Shader（VertexAOCompute.shader）中进行的，相当于每一轮循环都会做一次AO运算，然后每轮得出的AO值最终会累加在一起附给一个新的材质。 2. 计算AO AO的计算在VertexAOCompute.shader中。下图是片元着色器部分： 判断到底哪些点需要有“遮蔽”的部分就是120-121行。从118行的注释我们也可以知道，变量o“决定了遮蔽会有多暗”，而120行告诉我们，对于vertex.z和z相差过大的点，也就是被遮蔽的点，o不会参与累加（121行），因为o越大模型就会越亮。最后的累加会带上权重，只不过每次采样的权重都一样而已（124行）。 而上一步计算出的AO（65行）作为上图中的_AOTex参与了最终图像插值时的权重值（67行）。最终渲染出的图像会附着在一个新材质球上，而该材质球会替换掉模型原有材质（如下图），至此AO效果就添加到了模型上。 3. Tips 该工程计算AO的过程很直接地体现出了AO算法的思想。 概括来讲，环境遮挡被定义为从表面上某一点能够逃离场景的射线的比例。上文提到的每一轮AO采样计算相当于图中每条绿色的射线，而判断深度是在看该射线是否会被遮挡，最后的累加就是在计算比例，而这里通过样本均值来估算积分的过程也是蒙特卡洛积分思想的体现。 四、性能分析 本次测试用的是低端机型OPPO A32（4G RAM），测试分为开启和不开启GeoAO这两种情况。首先，下图显示的是开启情况下的FPS均值和GPU耗时情况： 下图是不开启该插件的情况： 可以看出即使在低端机型上，该工具的开启也没有为GPU和FPS造成压力，相反的是，GPU耗时反而比不开启的情况下要低，这是因为该插件只是在Start()函数中置换了一下物体材质，而新材质比不开启时的材质（Standard）更轻量化的缘故。 提示：并不建议在游戏中途加上该效果，因为它会由于Shader.CreateGPUProgram和AO计算而引起卡顿。其中，Shader.CreateGPUProgram的耗时可以进一步通过收集变体并预热的方式排除，可以参阅《一种Shader变体收集和打包编译优化的思路》；而剩下的AO计算部分放在场景加载阶段完成则是可以接受的开销。 综上所述，该插件是一个比较轻量级且效果尚可的方案。 作者发布项目时的介绍使用的是英文编写，为了方便大家阅读，UWA开源库已将其翻译成中文版本，欢迎大家共同学习。 今天的推荐就到这儿啦，或者它可直接使用，或者它需要您的润色，或者它启发了您的思路......
【博物纳新】专栏回归啦～ 这个专栏是UWA旨在为开发者推荐新颖、易用、有趣的开源项目，帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果，并探索将其应用到自己项目的可行性。很多时候，我们并不知道自己想要什么，直到某一天我们遇到了它。 更多精彩内容请关注：lab.uwa4d.com 一、概览 该示例展示了一种简单的3D网格切割算法。在运行状态下，通过在屏幕上拖拽划线，被线条划过的物体就会被线条分成左右两部分。 如果某些游戏需要包含切割物体之类的玩法，或者项目需要对物体切割的辅助工具，那么该插件可以作为良好的借鉴材料。 开源地址：https://lab.uwa4d.com/lab/5b3e3197d6d8c0171a9469b5 二、原理概述 该工具的原理可以分为划线、切割、补齐三部分。 1、划线部分：通过捕捉鼠标在屏幕上的拖拽轨迹，从而获取到切割平面的基本数据信息（如法向量等）； 2、切割部分：通过添加点和三角面等操作，把原本的Mesh切割成若干个独立的Mesh，再附着给各个被切开的Objects； 3、补齐部分：把每个被切割的物体的切面补上新的面。 三、具体实现 3.1 划线 这一部作为一个直观的操作，逻辑并不多，主要是要清楚这一部在准备什么数据。下图就是该步骤的主要逻辑： 该方法在MouseSlice.cs脚本中，从中我们可以知道切割物体需要的就是三个三维向量start、end和depth。这三个变量的收集来源ScreenLineRenderer.cs的Update()： 从上图可知，start和end对应于上图的startRay和endRay，分别是从摄像机位置出发，指向划线的起点和终点的两条射线，而depth是endRay的归一化向量。 划线切割物体的本质是划了一个平面来切割3D物体，之所以看起来是一条线是因为摄像机恰好在该平面上，该平面如下图所示。这样看来图1中63行的输入（某个点和法向量）就容易理解了，因为一个点和平面的一条法向量可以确定一个平面。 3.2 切割 切割物体时，我们可以注意到每次切割物体其实都是在实例化一个新的物体： 对应代码来自MouseSlice.cs中的SliceObject方法（如下图），从该代码可知，原Mesh会被分开成biggerMesh和smallerMesh两个网格，然后分别更替原物体和新增物体的Mesh（130-131行）。 切割过程是一个添加点的过程。把过程聚焦到每个三角面，如果某个三角面会被切割，那该三角形必定会新增两个点。对应的代码如下： 图中遍历每个三角面，如果会与切割平面相交，那么新增的两个点会加入addedPairs，而关于相交计算的部分在TrianglePlaneIntersect()中。 下图是TrianglePlaneIntersect()中的片段： 这一段代码如果搭配着下图会比较容易理解： longlyPoint指的就是被切平面（红色实线）单独分到一侧的点，而127和128行得到的newPointPrev和newPointNext就是上图中两个红色的点newPrev和newNext。131-135行是在添加由新的点组成的三角形，以上图为例，从上到下分别添加了（longly，newPrev，newNext）、（prev，newPrev，next）和（next，newPrev，newNext）三个三角形，并且以是否与切割平面法线同侧为标准分别加入了不同的Mesh中。 这一步基本已经完成了切割，毕竟“添加点”和“添加三角形”就是切割网格的本质。但是如果只做到这一步，只会得到如下的效果图，所以对于切面的各个三角面仍然需要补齐。 3.3 补齐 该部分是在补齐由切割平面新增的两个截面，对应的代码段就是MeshCutter.cs中的FillBoundaryFace()： 该函数的详细代码如下： 基本思想就是在切割面所新形成的点集中，按序添加三角形。从158行可以看出，这里是双向遍历的。单向和双向所构成的三角面如下示例： 图中黑线是待补齐的平面，红线则是通过循环补的三角面。那么为何要用稍显复杂的双向循环来补齐呢？因为单个顶点的复用会使得大量三角形都比较“狭长”，那么被共用多次的顶点附近的像素在光栅化阶段参与三角形遍历的次数就会增多，也就是Overshading现象会比较严重，这会增大GPU的压力。这就是为何大多数建模软件中，Mesh网格都尽量避免多个三角形共用单个顶点的原因。 相关知识可参考：https://zhuanlan.zhihu.com/p/57661927 而该步骤之后，被切割平面分开的两个物体块就补齐了切割面： 四、性能分析 通过UWA GOT Online在中等配置机型OPPO A72（6G RAM）上做了测试，测试场景为同时切20个圆柱体，测试时间60秒，从第20秒开始切割。 下图是CPU耗时曲线图： 从图中可以看到，从开始切割（约1200帧处）之后，耗时主要集中在Camera.Render和ScreenLineRenderer.Update上。首先我们注意到ScreenLineRenderer.Update的耗时原因是划线操作引起的即时性的峰值耗时，该项可以通过游戏玩法来优化（比如我们可以用发射子弹等其他方式来替代划线切割），这里我们主要研究Camera.Render的耗时。上图中，随着切割出的物体块越来越多，Camera.Render的耗时稳步上升，下图是在物体块很多时的某帧的堆栈耗时： 通过对比测试开始和结尾附近的帧的堆栈耗时，可以发现耗时增加最快的是上图红框中的Render.Mesh。该项的耗时通常由物体中未合批的物体数量决定，从调用次数来看，该项的耗时就是由于分割了过多物体块且未进行合批优化导致的结果。 为了对比说明，下图展示了可以进行动态合批的相同测试案例的堆栈耗时： 从该图可以看出BatchRenderer.Flush的总体占比比未合批时的占比低了大概12%，Render.Mesh调用次数也减少了许多（至于为何主要比较“总体占比”，是因为两次实验很难切出相同数量的物体块）。但其实动态合批的数量不算太多，因为物体块的顶点数量很容易超过300： 所以动态合批提升性能的幅度通常是不确定的，拿该项目为例，如果切割的是一些简单的、顶点数量比较小的物体，那么动态合批的性能提升应该会更大。 除此之外，GPU的耗时增幅也比较可观： 从图中可以看出，除了由ScreenLineRenderer.Update引起的峰值耗时外，均值上升主要是由顶点和三角面数的迅速增加造成的。 综上所述，如果需要使用该插件，那么最好满足：1、物体顶点数不多，且不需要分割出太多物体块；2、支持动态合批。 作者发布项目时的介绍使用的是日文编写，为了方便大家阅读，UWA开源库已将其翻译成中文版本，欢迎大家共同学习。 今天的推荐就到这儿啦，或者它可直接使用，或者它需要您的润色，或者它启发了您的思路......
导读 随着Lua在项目中的大量使用，它所带来的性能问题也逐步成为了项目运行时的重大性能瓶颈之一。特别是内存相关的性能问题，无论是内存分配过大还是内存泄露无法回收，目前都已经在不少研发项目中集中爆发。 UWA推出的GOT Online中的Lua模式已经慢慢成为研发团队对Lua进行日常性能监控的有效手段。因此，也有越来越多的团队反馈，在监控到table数持续上涨，引用Mono对象持续增多等等问题时，应该如何快速地解决？ 本次博物纳新推荐的开源库项目：LuaProfiler-For-Unity，相信可以帮助到大家。 Lua Profiler For Unity支持XLua、SLua、ToLua，该工具是基于远程Socket的Profiler工具，因此它支持Android，iOS的真机Profiler。 小编将结合实际项目中遇到的问题，为大家介绍这款开源库的用法。 开源库链接：https://lab.uwa4d.com/lab/5bf38db072745c25a80c1276 作者Blog：https://www.zhihu.com/people/ElPsyConGree/activities 操作流程 一、部署和安装 参考项目中的Readme文档阐述的详细流程： 1、打开两个Unity项目，一个放进游戏客户端，一个用于展示数据 2、打开LuaProfiler文件夹 LuaProfiler文件目录 3、将 LuaProfilerClient 文件夹复制到游戏项目内，如果您的C＃Lua脚本位于Plugins文件夹中，则将 LuaProfilerClient 复制到插件。此工具必须确保该代码必须位于具有C＃Lua代码的同一DLL中。 4、使用 Unity5.6 or newer version Unity版本将 LuaProfilerServer 作为Unity项目打开。 5、如果Unity版本低于5，请在开始游戏前调用以下代码。 MikuLuaProfiler.HookLuaSetup.OnStartGame(); 注意：不要在static变量声明里面启动Lua流程（比如XLua的Demo），请在Awake或者Start里面进行调用。 二、使用教程 （该小节内容全部来自于项目Readme文档，想要阅读更详细教程的读者可访问项目主页：https://lab.uwa4d.com/lab/5bf38db072745c25a80c1276） 1、配置客户端 Lua Profiler Client界面 LuaProfilerClient插件所在的游戏项目工程，通过Editor界面的Window->Lua Profiler Window打开客户端设置界面。选择想要分析器的代码类型，C＃代码颜色为绿色，Lua代码颜色为蓝色。 2、配置服务器端 LuaProfilerServer插件所在的项目工程，通过Editor界面的Window->Lua Profiler Window打开服务器数据显示界面。 Lua Profiler Server界面 单击OpenService，等待客户端连接。 操作流程示意图 3、相关功能操作 使用示意图 3.1 相关基础数据统计 折线走势图展示了PSS、Mono内存、Lua内存、FPS的走势： 折线走势图 数据列表 数据列表中列举了当前帧下图所示的相关数据： 3.2 监控注册表 注册表引用的Lua对象统计 此处区域会显示当前被注册表引用的类型为Function和table的Lua对象。 3.3 Diff两个不同时期的Lua变量 选取一个适当的时机，比如配置表加载完后，准备打开一个新的UI的时候点击MarkLuaRecord按钮。 操作步骤 打开UI然后关闭并卸载掉UI资源，点击DiffRecord，工具将会对Mark时候的Lua变量与DiffRecord时候的变量进行差异比较。 数据显示界面 点击ShowLog按钮，将会把文件存盘打开之后将把对于变量的类型以及引用路径打印出来。注意_G表示全局表，_R表示被C#引用的对象。 引用链 3.4 Destroy null values统计 该模块展示了Unity已经将资源释放，而Lua仍然引用的变量。 引用链 功能原理详解 一、相关基础数据统计 查看这部分数据时，除了关注排序中开销较大、内存占用较高的函数外还可以关注一些重要的字段，例如： require字段，比较普遍的性能问题在于：加载配置表时产生一个内存占用较大的table，可以通过搜索require字段并进行排序查找内存占用较大的配置表，并对它进行针对性优化。 关于配置表的优化方案可以阅读《Lua配置表存储优化方案》。 [Lua]字段，按照Lua内存的self进行排序，可以定位GC比较严重的Lua函数，进行针对性优化。也可以查看调用次数等。 在搜索框中搜索[Lua] 善用搜索功能，会揪出很多意想不到的性能问题，比如：老生常谈的Vertex3。 二、监控注册表 以SLua框架的示例Circle.cs 为例： //Circle.cs public class Circle : MonoBehaviour { LuaSvr svr; LuaSvr s2; LuaTable self; LuaFunction update; [CustomLuaClass] public delegate void UpdateDelegate(object self); UpdateDelegate ud; void Start () { svr = new LuaSvr(); svr.init(null, () => { self = (LuaTable)svr.start("circle/circle"); update = (LuaFunction)self["update"]; ud = update.cast<UpdateDelegate>(); } ); ...... //Circle.lua local class={} function main() local slider = GameObject.Find("Canvas/Slider"):GetComponent(UI.Slider) local counttxt = GameObject.Find("Canvas/Count"):GetComponent(UI.Text) slider.onValueChanged:AddListener( function(v) class:init(v) counttxt.text=string.format("cube:%d",v) end ) class.root = GameObject("root") class.ftext = GameObject.Find("Canvas/Text"):GetComponent(UI.Text) class.r=10 class.cubes={} class.t=0 class.f=0 class.framet=0 class.max=0 class:init() return class end function class:update() -- gc alloc is zero0 ...... C# 层创建的变量self、svr、update等，引用了Lua层的class、update等。 使用该工具得到的引用关系如图所示： 其中“@circle/circle&line:15"的函数为： function(v) class:init(v) counttxt.text=string.format("cube:%d",v) end 被添加到UI组件Slider的事件监听中。 “@circle/circle&line:65"的函数为：function class:update()，被一个LuaFunction类型对象update引用。 其中具有key为：bgCurrent、init、root、t等值的table，为Lua代码中的： local class={} （其余被引用的table和function是框架初始化时产生的） C#层是一个ref，内存占用较小，但是Lua层会是一个较为复杂的table或者函数调用等，内存占用较大。当不再使用的C#对象没有被完全释放时，由于C#层内存占用较小，并不会及时进行GC，使得Lua层仍然存在引用，无法进行GC，导致大量内存滞留。 三、Destroy null values统计 在任意一种Lua插件中，都存在类似的机制：在C#层维护一个Cache来引用那些被Lua访问过的C#层对象，防止出现以下的问题：当Lua中再次访问该C#对象时，该对象可能已经被C#层的GC回收掉了，从而导致逻辑错误。所以，在Lua中始终保留某个C#层对象的引用，将会导致其无法被释放，当这样的引用越来越多，就会导致C#层的内存泄漏。 其中比较常见的例子便是：应该被申明为local的对象忘记写local。 function main() cube = GameObject.CreatePrimitive(PrimitiveType.Cube) ...... 然后切换场景，是用工具检测得到被引用对象为：Cube，如图： 具体引用链为： 当切换场景时，虽然场景中没有了Cube对象，但对象池中还有，导致仍然有引用而无法GC。此时Cube对象是一个作为UnityEngine.Object为空，而作为System.Object不为空的对象，原因就是Lua对其的引用不为空，会导致泄漏。解决方案也较为简单，将Cube变量申明为local局部变量，解除引用即可： function main() local cube = GameObject.CreatePrimitive(PrimitiveType.Cube) ...... 更多实战例子可以阅读：https://zhuanlan.zhihu.com/p/89912209 四、Diff两个不同时期的Lua变量 对两个时期的Lua State做两次完整的快照，通过比较两次快照的数据，可以得到相关增加与减少的变量。得到疑似泄露的地方。通过快照获取到的引用链定位泄漏的变量。 该工具得到引用链中：_G表示全局表，_R表示被C#引用的对象 引用链 想要更加深入了解该工具、深入了解Lua性能优化方案的读者，推荐阅读： 1、作者书写的工具介绍与性能检测思路的文章，其中详细解释了Lua、Mono双GC系统、以及Mono对象、Lua对象、Unity对象三者的释放流程以及Lua、C#、C++整个调用流程结构：https://www.zhihu.com/question/307064711 2、UWA Blog中对于Lua性能优化的文章： 《Lua性能优化—Lua内存优化》 《Lua的CPU开销性能优化》 快用UWA Lab合辑Mark好项目！ 今天的推荐就到这儿啦，或者它可直接使用，或者它需要您的润色，或者它启发了您的思路...... 请不要吝啬您的点赞和转发，让我们知道我们在做对的事。当然如果您可以留言给出宝贵的意见，我们会越做越好。 【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目，帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果，并探索将其应用到自己项目的可行性。很多时候，我们并不知道自己想要什么，直到某一天我们遇到了它。 更多精彩内容请关注：lab.uwa4d.com
自从在GDC 2019上，Unity分享了名为“连接DOTS：Unity面向数据技术栈”的技术演讲，关于DOTS的讨论和应用一直在业内备受关注。前段时间我们连载的“Unity手游实战”系列中，也有对于DOTS的相关论述。 本文要给大家介绍的是supron在社区中与大家分享的高性能UI解决方案：Pure DOTS UI System[3]。 The current Unity UI solution is very powerful but struggles with performance (especially with many objects instantiation). DOTS seems like a great solution to this problem. DOTS UI开源库：https://lab.uwa4d.com/lab/5d3a18365b9dec79de05d348 一、功能 DOTS UI System可以将Unity的UI系统UGUI的组件转化映射成Entity，利用ECS、JobSystem、Burst的性能优势，明显提升UI运行效率。除此之外，在网格重建部分还使用到了2019.3的新特性：Advanved Mesh API[5]，可以直接写入Mesh数据，运行效率更快。 目前DOTS UI的版本为0.3.0，功能还是非常不完善的。目前已支持的主要功能如下： Canvas Render mode: Screen space camera Screen space overlay Canvas Scaling mode: Constant pixel size Constant physical size Controls: Image TextMeshProUGUI (SDF fonts, not all features are supported) TMP_InputField (very simple implementation) Selectable Button RectMask2D CanvasScaler ScrollRect Input events: Down Up Click Enter Exit Selected Deselected BeginDrag Drag EndDrag Drop Button click InputField OnEndEdit InputField OnReturn 二、使用 在开源库中下载资源后，将com.dotsui.core和com.dotsui.hybrid两个资源包复制到项目工程（Unity 2019.3.0a8以上）的Packages路径下进行导入，则Unity会自动导入其依赖的Entities、Jobs等Package。 打开一个UI预制体，在Canvas节点挂上ConvertToEntity[6]脚本，表示需要将这个UI转换成Entity。默认选择的Conversion Mode为Convert And Destroy，即替换为Entity之后会把原有的UGUI组件销毁。 运行一下，即可看到运行时生效的效果。 由于测试的Prefab中的Text不是使用TextMeshPro做的，所以没有成功转换，但这并不影响其它组件的正常运行。 多做几次测试之后，还会发现RectTransform的Rotation和Scale属性没有被正确显示。这是因为作者在第一个版本中简化了很多属性，其类定义如下： public struct RectTransform : IComponentData { public float2 AnchorMin; public float2 AnchorMax; public float2 Position; public float2 SizeDelta; public float2 Pivot; } 作者还在工程中提供了Sample示例，展示了目前支持的几种效果。 如果需要使用脚本控制UI变化，也要用ECS的方式来编写。可以参考Sample中的简单例子（如：FpsCounter、FPSSystem）进行改写。 三、实现 1、Conversion Conversion的部分相对比较简单，核心在于将Canvas、Image等组件转换为Enity。 以上文提到的RectTransfrom为例，其Convert函数的代码如下： private void Convert(RectTransform transform) { var entity = GetPrimaryEntity(transform); DstEntityManager.AddComponentData(entity, new DotsUI.Core.RectTransform() { AnchorMin = transform.anchorMin, AnchorMax = transform.anchorMax, Pivot = transform.pivot, Position = transform.anchoredPosition, SizeDelta = transform.sizeDelta, }); DstEntityManager.AddComponent(entity, typeof(WorldSpaceRect)); DstEntityManager.AddComponent(entity, typeof(WorldSpaceMask)); DstEntityManager.RemoveComponent(entity, typeof(Translation)); DstEntityManager.RemoveComponent(entity, typeof(Rotation)); DstEntityManager.RemoveComponent(entity, typeof(NonUniformScale)); } 这部分相关的主要相关代码在Dots UI Core Package当中。 2、UI Mesh Batching UI Mesh的合批处理是UI模块非常重要的部分，在这部分DOTS UI将需要渲染的网格信息进行合批并存储起来，为之后的渲染步骤做准备。在DOTS UI中这一步主要分成两步完成。 （1）信息收集 首先定义一个NativeHashMap<Entity，MaterialInfo>，用来记录需要渲染的UI元素（Entity）和Material的对应关系。目前只包含Sprite和Text。 public NativeHashMap<Entity, MaterialInfo> EntityToMaterial; 这里面MaterialInfo包含两个信息，一个是Material类型（Sprite、Text），另一个是MaterialId，在这里指Sprite或Text中记录的NativeMaterialId，实质为Sprite或Text的SCD（SharedComponentData[4]）在Chunk中的Index。 spriteData.NativeMaterialId = chunk.GetSharedComponentIndex(assetType); 到网格更新这一步时，遍历ChunkArray中的Chunk，将SpriteImage和TextRenderer的上述信息记录到HashMap中。 （2）网格合批 在MeshBatching的Job中，将上一步的HashMap作为输入，并递归遍历节点之间的父子关系构建三个DynamicBuffer： private void GoDownRoot(Entity parent, ref DynamicBuffer<MeshVertex> vertices, ref DynamicBuffer<MeshVertexIndex> triangles, ref DynamicBuffer<SubMeshInfo> subMeshes) {...} 如果连续两个Entity的Material信息相同，则记录到一个SubMesh中，完成合批。如果前后两个Entity信息不同，就会创建一个新的SubMesh，也就是一个新的DrawCall。 bool materialAssigned = EntityToMaterial.TryGetValue(entity, out MaterialInfo material); if (!materialAssigned) { material.Type = SubMeshType.SpriteImage; material.Id = -1; } if (m_CurrentMaterialId != material.Id) { subMeshes.Add(new SubMeshInfo() { Offset = triangles.Length, MaterialId = material.Id, MaterialType = material.Type }); m_CurrentMaterialId = material.Id; } int startIndex = vertices.Length; if(VertexPointerFromEntity.Exists(entity)) VertexPointerFromEntity[entity] = new ElementVertexPointerInMesh(){VertexPointer = startIndex}; 3、RenderSystem 完成了UI网格的合批之后，就可以根据已生成的顶点信息、SubMesh等数据生成Mesh，并将这些Buffer信息上传至GPU，最后调用CommandBuffer的DrawMesh进行绘制了。也就是在这一步中使用到了Mesh.SetVertexBufferData等2019.3新支持的Mesh API，可以传递NativeArray参数直接修改Mesh，达到了效率的提升。 但由于这一步的Mesh和CommandBuffer都必须在主线程中完成，所以并不像网格合批可以得益于多线程带来的巨大效率提升。 其主要实现逻辑在HybridRenderSystem.cs中，以下为Build CommandBuffer部分的实现逻辑： private void BuildCommandBuffer(DynamicBuffer<MeshVertex> vertexArray, DynamicBuffer<MeshVertexIndex> indexArray, DynamicBuffer<SubMeshInfo> subMeshArray, Mesh unityMesh, CommandBuffer canvasCommandBuffer) { using (new ProfilerSample("RenderSystem.SetVertexBuffer")) { unityMesh.Clear(true); unityMesh.SetVertexBufferParams(vertexArray.Length, m_MeshDescriptors[0], m_MeshDescriptors[1], m_MeshDescriptors[2], m_MeshDescriptors[3], m_MeshDescriptors[4]); } using (new ProfilerSample("UploadMesh")) { unityMesh.SetVertexBufferData(vertexArray.AsNativeArray(), 0, 0, vertexArray.Length, 0); unityMesh.SetIndexBufferParams(indexArray.Length, IndexFormat.UInt32); unityMesh.SetIndexBufferData(indexArray.AsNativeArray(), 0, 0, indexArray.Length); unityMesh.subMeshCount = subMeshArray.Length; for (int i = 0; i < subMeshArray.Length; i++) { var subMesh = subMeshArray[i]; var descr = new SubMeshDescriptor() { baseVertex = 0, bounds = default, firstVertex = 0, indexCount = i < subMeshArray.Length - 1 ? subMeshArray[i + 1].Offset - subMesh.Offset : indexArray.Length - subMesh.Offset, indexStart = subMesh.Offset, topology = MeshTopology.Triangles, vertexCount = vertexArray.Length }; unityMesh.SetSubMesh(i, descr); } unityMesh.UploadMeshData(false); } using (new ProfilerSample("BuildCommandBuffer")) { canvasCommandBuffer.Clear(); canvasCommandBuffer.SetProjectionMatrix(Matrix4x4.Ortho(0.0f, Screen.width, 0.0f, Screen.height, -100.0f, 100.0f)); canvasCommandBuffer.SetViewMatrix(Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one)); for (int i = 0; i < unityMesh.subMeshCount; i++) { var subMesh = subMeshArray[i]; var renderMaterial = SetMaterial(ref subMesh); canvasCommandBuffer.DrawMesh(unityMesh, float4x4.identity, renderMaterial, i, -1, m_TemporaryBlock); } } } 四、性能 以下为作者给出的性能对比数据： 复杂的UI实例（300 RectTransforms, 30314 characters） Profiler Time性能对比 这里需要说明的是，由于两者的渲染开销几乎相同，所以主要比较的是UI重建开销。 这里也测试了一个简单的1000个字符更新的Demo，在两个中低端设备上运行Demo，通过Timeline记录了两种UI的重建耗时得到数据如下。 Demo运行截图 OPPO K1上的DOTS UI耗时 可见DOTS UI在移动端设备上确实是有明显的性能优势。虽然日后必然会随着功能的扩充，逐渐减小这种优势，但目前的实现方式上也还是有优化空间的。所以DOTS UI的性能表现很值得期待。 相关链接： [1]DOTS UI开源库：https://lab.uwa4d.com/lab/5d3a18365b9dec79de05d348 [2]DOTS UI Github：https://github.com/supron54321/DotsUI [3]DOTS UI介绍：https://forum.unity.com/threads/showcase-pure-dots-ui-system-detailed-description-feedback.688531/ [4]SharedComponentData：https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/shared_component_data.html [5]Mesh API：https://docs.unity3d.com/2019.3/Documentation/ScriptReference/Mesh.html [6]ConvertToEntity：https://docs.unity3d.com/Packages/com.unity.entities@0.0/api/Unity.Entities.ConvertToEntity.html 今天的推荐就到这儿啦，或者它可直接使用，或者它需要您的润色，或者它启发了您的思路...... 请不要吝啬您的点赞和转发，让我们知道我们在做对的事。当然如果您可以留言给出宝贵的意见，我们会越做越好。 【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目，帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果，并探索将其应用到自己项目的可行性。很多时候，我们并不知道自己想要什么，直到某一天我们遇到了它。 更多精彩内容请关注：lab.uwa4d.com
可用于构建树木的工具，不仅能营造出真实的树木和森林效果，还可以方便地嵌入到其它引擎当中。在游戏领域中，很多艺术家青睐于它成熟流水线和真实自然的效果。 图片来源：SpeedTree在游戏场景中的应用 开源库链接：https://lab.uwa4d.com/lab/5c2e78a572745c25a849f614 NVGen介绍 本文将要介绍的开源库是一个适用于Unity SpeedTree的自定义Shader NVGen。 相比于Unity内置的Nature/SpeedTree Shader，NVGen 提供了更丰富的自定义风效。同时支持Occlusion Map环境光遮蔽贴图，并可以自定义调节法线贴图和遮蔽贴图的强度，让渲染效果更逼真。 移动端真机渲染静态截图对比 NVGen比内置Shader多出了20多个可调节参数，以下为参数面板对比： 内置Shader： NVGen Shader： 对于原本就使用了SpeedTree的开发者来说，在使用上，只要将NVGen资源包导入工程，并将导入Unity的.spm格式文件的Shader替换成NVGen即可，不需要再额外处理什么，十分简单方便。 动态效果展示 1、树随风摇摆效果 2、草随风摇摆效果 3、调节法线贴图强度 4、调节遮蔽贴图强度 5、调节Leaf Tumble参数，模拟树叶被吹动摇晃的剧烈程度 6、调节Leaf Ripple参数，模拟树叶波浪摆动的效果大小 性能测评 为了让帧率对比更明显，我们在场景中复制了5棵树，得到不同机型上的帧率对比如下： 从整体帧率对比来看，NVGen Shader确实会有一部分的性能损耗，但可以带给美术人员很灵活、自由的操作空间。研发团队可根据自己的需求来决定是否尝试和使用。 相关链接： 作者Blog：https://nvjob.pro/unity/nvgen-shader-for-unity-speedtree Unity API：https://docs.unity3d.com/Manual/SpeedTree.html SpeedTree官网：https://store.speedtree.com/ 封面图来源：SpeedTree在游戏场景中的应用 快用UWA Lab合辑Mark好项目！ 今天的推荐就到这儿啦，或者它可直接使用，或者它需要您的润色，或者它启发了您的思路...... 请不要吝啬您的点赞和转发，让我们知道我们在做对的事。当然如果您可以留言给出宝贵的意见，我们会越做越好。 【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目，帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果，并探索将其应用到自己项目的可行性。很多时候，我们并不知道自己想要什么，直到某一天我们遇到了它。 更多精彩内容请关注：lab.uwa4d.com