UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技 - 动画
在上一期《动画优化：关于AnimationClip的三两事》中，我们针对动画优化中的“AnimationClip的压缩方式”以及“AnimatorController挂载的AnimationState”做了解释。这些看似细小的知识点，很容易在大家的开发和学习过程中被疏忽，而长期的问题积累最终都会反映到项目的性能表现上。为此，我们将这些规则列出，并且以一个个知识点的形式向大家逐一解读。 本期，我们将面向粒子系统，讲解UWA本地资源检测中的规则：“网格发射数超过5的粒子系统”、“检测粒子系统使用的网格Read/Write选项”、“引用网格面片数超过500的粒子系统”和“引用纹理数超过5的粒子系统”。我们将力图以浅显易懂的表达，让职场萌新或优化萌新深入理解。 1、网格发射数超过5的粒子系统 关于粒子系统的定义，我们在此不再赘述。我们重点来看一下粒子系统的渲染模式。 粒子的渲染模式可分为两大类：2D的Billboard（公告牌）图形模式和Mesh模式。Billboard是3D游戏中用的非常多的一种技术，使用该模式，粒子会以一个平面的形式存在，并始终以一定的角度对着我们的镜头。举个例子来讲，场景中有一棵树，是实体存在的；但在各种Billboard模式下，你只需要看到这棵树的一个“面”。在展示效果上表现其实一样，但渲染一个“面”和渲染一整个实体，前者在消耗的内存和运算量上会更有优势。 Billboard模式，粒子会始终面向摄像机； Stretched Billboard模式下，粒子在面向摄像机的同时，会在速度的方向上进行拉伸，可以设置粒子的各种缩放和拉伸效果； Horizontal Billboard模式下，粒子平面会和XZ面平行； Vertical Billboard模式下，粒子平面在面向摄像机的同时，会在Y轴方向上直立（垂直于XZ平面）； 而“Mesh”模式就是我们今天的主题。这个模式允许粒子系统发射3D网格而非2D的Billboard，以此来实现更为复杂和贴合需求的粒子效果。 3D网格性能开销显然是高于2D的Billboard的。如果粒子系统的发射的网格数量过大，会带来较高的CPU计算开销、GPU渲染压力甚至较高的堆内存分配。所以我们对使用Mesh进行渲染的粒子系统的“最大粒子数”要求更为严苛。一般的粒子系统中，粒子发射数上限建议不超过30（我们在之前的文章中有进行讨论，详见《【性能黑榜】掌握了这些规则，你已经战胜了80%的对手！》）。经由UWA的测试结果表明：渲染模式为Mesh、网格发射数在不超过5的情况下，能够维持粒子效果和性能消耗上的一个相对平衡。 所以开发团队在本条规则执行后，需要对筛选出来的粒子系统进行显示效果和性能消耗上的平衡考量，并对相应的粒子系统进行修改。 2、所使用的网格未开启Read/Write选项的粒子系统 由于粒子系统的渲染对象从传统的粒子变成了3D网格，所以引用的网格对象本身的属性也会对整个粒子系统产生影响。在这种情况下，很多针对网格的优化建议也同样适用于Mesh模式下的粒子系统，只不过针对特定应用环境，部分优化建议需要“反着看”。 在之前的文章《网格优化：溃堤之穴，一个也不能放过》中，我们对于网格的Read/Write选项进行过一定的说明。仅仅针对网格而言，我们是不建议开启相关的读写权限。 但是在粒子系统应用中，往往需要去实现各种各样的动态效果。所以当渲染模式为Mesh时，网格就需要开启Read/Write选项以实现对3D网格的动态修改，来更好地实现相应的渲染展示效果。 Unity官方文档告诉我们，网格必须设置为Read/Write Enabled才能在粒子系统上工作。若不开启，在某些设备上运行时，可能会出现模型丢失的情况。可见问答：https://answer.uwa4d.com/question/5cf732fed27511377098284e。因此我们将该类异常的资源检测出来，希望研发团队进行规范，防止出现意外的显示结果。 3、引用网格面片数超过500的粒子系统 和上一条规则不同，这条便是网格优化在粒子系统中的正向应用。在以前文章《那些年给性能埋过的坑，你跳了吗？》中，我们对于网格的面片数有过相关的简单讲解。对3D网格而言，过多的面片数会带来内存，加载和渲染上的额外消耗。在粒子系统中也是同样如此。 在Mesh模式下，发射的3D网格对面片数的要求也是同样严苛的。所以开发团队需要在本条规则下，去检查相关粒子系统引用的网格是否存在面片数过多的情况，并视实际的表现效果和性能消耗去决定是否需要做出相关的修改。 4、引用纹理数超过5的粒子系统 在之前的文章《材质优化：如何正确处理纹理和材质的关系》中，我们简单讲了下材质的Shader中引用了过多纹理会在内存和计算上带来的影响。 同样的，在粒子系统中，如果在材质内包含了过多的引用纹理，也同样会导致存储和运算上的额外消耗，从而对整个的粒子系统带来不必要的负担。 所以开发团队在为纹理系统引入材质时，需要关注相关材质内引用的纹理数量。在满足使用的前提下，从材质做瘦身，从而为整个粒子系统调优。 以上，大家其实可以发现，看似各个独立的部分：网格、粒子系统、材质和纹理等，其实在优化上都有着互相的关联。所以面对项目优化，开发团队要以一个整体的高度去看待项目的各个部分。 希望以上这些知识点能在实际的开发过程中为大家带来帮助。需要说明的是，每一项检测规则的阈值都可以由开发团队依据自身项目的实际需求去设置合适的阈值范围，这也是本地资源检测的一大特点。同时，也欢迎大家来使用UWA推出的本地资源检测服务，可帮助大家尽早对项目建立科学的美术规范。 我们已支持Luacheck功能啦~ 我们已支持粒子特效检测~ 下周会有重磅新功能推出喔 这么多实用贴心的功能 还不走你~ 万行代码屹立不倒，全靠基础掌握得好！ 性能黑榜相关阅读 《那些年给性能埋过的坑，你跳了吗？》 《那些年给性能埋过的坑，你跳了吗？（第二弹）》 《掌握了这些规则，你已经战胜了80%的对手！》
Unity引擎模块分析—注重效率的你不妨这么做！
UWA GOT是UWA推出的本地性能优化工具，其Online功能限时免费中，并受到了大量开发者的试用和积极反馈。其中，它的总体性能分析模式主要便于研发团队直观地定位、验证和解决逻辑代码中的效率瓶颈。 然而我们知道，引擎模块的性能同样至关重要，仅仅逻辑代码的检测还无法实现系统、完整地定位瓶颈的目的。所以今天，我们在该工具中推出Unity引擎模块的检测。您只需要拥有一个UWA GOT，在本地真机上测试后即可把数据同步到官网，这些详细的数据就能立马呈现。 详细的引擎模块参数 目前，UWA GOT Online报告支持渲染、UI、粒子、动画、加载和物理这六大主流引擎模块的重要参数检测。如下图，在渲染模块页面中，我们可以查看不透明、半透明、视域裁剪CPU和相机后处理等CPU耗时的走势以及某一帧上的耗时。 我们也可以缩放某段测试区域，结合运行截图更直观地定位某些峰值处的表现。 对于引擎模块的重要参数，我们提供了UWA推荐值和相关的优化建议，便于研发团队进行有的放矢的排查和优化。 精准的逻辑代码堆栈 堆栈信息是我们定位性能瓶颈的鹰眼，这次版本的更新不仅丰富了引擎模块的重要参数检测，在逻辑代码中也可以查看更细致的引擎函数堆栈信息，并且通过总体、指定场景和指定帧来定位具体的调用详情。 特别的，在堆栈信息列表中，我们也提供了历史数据对比功能，针对于研发团队定制的性能或功能回归测试，我们可以直接在报告中查看指定重要参数的变化趋势，便于开发团队可以在第一时间验证测试结果。 这次的新功能小编就安利到这里啦！目前，该功能也在限免使用中，研发团队只需要拥有一个UWA GOT，就可以在总体性能分析模式中体验到该功能！还不快打开你的黑科技试一试呀^_^！ 关于UWA GOT Online UWA GOT 是UWA推出的本地性能测评工具，它方便研发团队在本地真机测试，并能在编辑器中搭建本地服务器查看性能数据。现在，在UWA GOT现有的本地测评模式上，我们增加了线上同步和分析功能，研发团队可以将本地测评的数据快速同步到UWA官网，生成在线测评报告，提供更高效的性能分析数据及有针对性的优化建议。同时，贴心的版本对比功能，方便研发团队轻松监测每次测试中的重要性能参数变化趋势，从质量管控上为项目的研发保驾护航。
本期聚集了这些话题：ShaderLab内存优化、Static Batch使用注意事项、foreach的堆内存分配问题修复、Unity 2017场景光照烘焙信息过大...... 我们将从日常技术交流中精选若干个开发相关的问题，建议阅读时间15分钟，认真读完必有收获。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。 UWA QQ群：793972859 UWA 问答社区：answer.uwa4d.com 渲染 Q1：我们的版本是Unity 2017，我们的美术烘焙出来的场景光照贴图中，还带了一个Lighting Data Asset这样的文件，查询相关资料显示是包含了贴图采样信息等内容，按理说应该不会很大，可是现在项目里，这个文件达到了90MB一个场景很不正常。有人建议是把这个东西删掉索引，不要打包。但是不知道这样场景里的灯光贴图还可以正常加载么？ 建议题主可以看下文档（https://docs.unity3d.com/Manual/LightmapSnapshot.html）里的两条描述： The asset is an Editor only construct so far, so you can’t access it in the player. Currently, this file is a bit bloated as it contains data for multiple platforms - we will fix this. Also we are considering adding some compression for this data. 所以这90MB并不是全都打到包里去的，但里面所保存的必要信息，打进包里后，具体有多大确实不好评估。一般来说，不开启Precomputed GI的话，应该是不大的。“把这个东西删掉索引”再打包的话，理论上Lightmap是会丢失的，得自己来动态加载和设置的。 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/5a4f4367e041ef3c50c9ce64 内存管理 Q2：真机测试环境下，在Profiler里查看内存使用情况，发现ShaderLab一项占用内存很大，里面主要是什么？减少了Shader的变体后这部分内存有所下降，没有使用Unity内置的Standard Shader。能否在不减少变体的情况下，减少这一部分占用的内存？ 另外，也发现同一项下的Objects占用也很大，这是为什么呢？ ShaderLab是项目在编译Shader时产生的解析Buffer，其内存增长的特点就是Key words越多、变体越多，其内存占用越大。目前，还没有看到有不减少变体的情况下，可以减少该内存占用的有效方法。如后续有找到，我们也会来及时更新这块的内容。 至于Objects，建议搜索题库，来查找之前相应的回答，比如这里： https://answer.uwa4d.com/search/objects 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/5a4d8df42b7c402cc97294a3 动画 Q3：我们导入anim后缀的文件进行精度处理后某些信息丢失，如下图所示： 修改的代码如下： AnimationClip theAnimation = AssetDatabase.LoadAssetAtPath<AnimationClip> (path); Keyframe key; Keyframe[] keyFrames; foreach (EditorCurveBinding binding in (AnimationUtility.GetCurveBindings(theAnimation))) { AnimationCurve curve = AnimationUtility.GetEditorCurve(theAnimation, binding); if (curve == null || curve.keys == null) { continue; } keyFrames = curve.keys; for (int i = 0; i < keyFrames.Length; i++) { key = keyFrames[i]; key.value = float.Parse(key.value.ToString(“f3”)); key.inTangent = float.Parse(key.inTangent.ToString(“f3”)); key.outTangent = float.Parse(key.outTangent.ToString(“f3”)); keyFrames[i] = key; } curve.keys = keyFrames; theAnimation.SetCurve(binding.path, binding.type, binding.propertyName, curve); 修改前后的例子已经上传到UWA问答社区。 丢失的信息是mEditorcurves, 该字段只对anim在Editor中的内存占用产生影响，对其在真机运行时的动画效果与内存占用并无影响。因此该字段列表为空与否不会影响其在真机中的表现，可以用提问中的方式对其进行精度处理。如果想保留该字段（虽然对真机内存无影响），可以通过文本处理的方法对anim文件进行精度处理，处理时跳过mEdtorcurves字段。 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/5a4254efebf015c04f5ed54e 资源管理 Q4：场景使用Static Batch似乎会将所有用到的模型合并成一个大的网格并保存成文件，导致场景的AssetBundle很大，而且会导致场景的模型文件和场景文件资源似乎有重复。 如果场景不标记Static Batch，而是加载完成之后动态去设置Static Batch，尺寸会减少吗？ 是的，标记Static Batch会生成额外的Combined Mesh数据，这个往往占据较大的包体和内存。不标记Static Batch，加载之后通过StaticBatchingUtility API来动态设置也是一个很好的选择，唯一的不同是拿场景加载的时间来换空间。 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/5a40eafdb319bedd3bec6d91 资源管理 Q5：请问在Unity 5.6.4中解决了foreach导致的堆内存分配问题了吗？ 在Unity 5.6中，foreach确实不产生堆内存分配了，另外感谢王亮的补充，在Unity 5.5.5p1中也已经修复了该问题。 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/5a4c575c2adbd673cb602a33 今天的分享就到这里。当然，生有涯而知无涯。在漫漫的开发周期中，您看到的这些问题也许都只是冰山一角，我们早已在UWA问答网站（answer.uwa4d.com）上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入，也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。 官网：www.uwa4d.com 官方技术博客：blog.uwa4d.com 官方问答社区：answer.uwa4d.com 官方技术QQ群：793972859（仅限技术交流）
虚幻引擎学习之路：动画模块进阶篇之动画融合
上周UWA介绍了Unreal 4引擎动画模块中的基本功能，接下来我们将进一步介绍Unreal 4引擎动画模块中提供的进阶功能。其中主要包括：动画融合、动画重定向、反向运动学、顶点动画。鉴于篇幅原因，今天我们先来讲解动画融合。 在此，特别感谢Unreal中国团队对于本篇文章中Unreal引擎相关内容的审核，并在UWA团队学习其引擎的道路上提供的大力支持。 动画融合 动画融合是游戏引擎动画模块中的重要功能。本节将介绍Unreal 4引擎中提供的动画融合功能，主要包括：Blend节点、Blend Space、Animation Montage、Animation Composite以及Post Process Animation Blueprint。 1、Blend节点 Unreal 4引擎的Anim Graph中提供了一系列的Blend节点可以实现不同的动画融合功能，其类似于Unity引擎中的Blend Tree功能。Blend节点的输入是来自多个不同动画文件的Pose数据以及Alpha权重参数，它会根据权重参数计算出最后融合的Pose，如下图所示： 其中，A和B为两个输入Pose，Alpha为[0,1]的浮点数表示A和B的权重。当Alpha为0时输出Pose全部来自A，当Alpha为1时输出Pose全部来自B，当Alpha为0~1之间时输出Pose为A和B的融合结果。类似功能的节点还有：Apply Additive、Blend Multi等。 Time Based Blend类型节点可用于连接两个或多个动画，并在动画切换时进行融合。Blend Poses by bool节点是其中一种，该节点根据输入的Bool值（红框中的Active Value）来控制True Pose和False Pose的切换，并且可分别设置两个Pose的融合时间（蓝框所示），其分别表示切换到True Pose的融合时间和切换到False Pose的融合时间，如下图所示： 类似功能的节点还有：Blend Posed by int，支持多个动画Pose的切换融合。 Layered Blend per bone节点可以将多个动画的不同骨骼融合在一起，例如：将Locomotion的下半身体和射击以及近战的上半部分身体进行融合，可以实现一边行走一边射击等动作。该节点需要配合Animation Montage的Slot使用，本文将在Animation Montage小节中进行介绍。 2、Blend Space Blend Space是Unreal 4引擎动画模块中用于实现复杂动画融合功能的资源。它最多可提供两个控制参数，并支持在二维平面上摆放动画采样点对多个动画进行融合。当变量取值在二维平面中的某一坐标位置时，Blend Space会根据采样点处的动画和坐标插值计算出当前坐标的动画。下图显示了1D和2D的Blend Space示意图： 其中，左图为1D的Blend Space，包含一个参数Speed。右图为2D的Blend Space，包含两个参数Speed和Yaw。 Blend Space的创建与其他资源创建过程类似，点击Add New按钮或在Content Browser中点击右键，在弹出菜单中选择Blend Space即可。Blend Space资源分为1D和2D，分别支持一个和两个控制参数的动画融合。创建好Blend Space资源后双击即可打开编辑界面，如下图所示： 上图例子使用了1D的Blend Space，实现了根据Speed参数对Idle、Walk以及Run三个动画进行采样融合效果。其中，区域1显示了资源的参数设置，区域2显示了控制参数坐标轴以及动画采样点，区域3显示了动画文件列表。通常在使用Blend Space时首先需要设置坐标轴，如下图所示： 其中，在Interpolation Type选项中可以设置动画插值计算方式，默认设置是Averaged Interpolation。 设置好坐标轴即可从区域3动画列表中选取采样动画拖动到区域2中。上述例子中设置了三个动画采样点：Idle、Walk和Run。在区域1中可查看其参数设置，如下图所示： 上图参数设置表示Idle动画位于Speed取值为0处，Walk动画位于Speed取值为93.75处，Run动画位于Speed取值为375.0处。在区域3中可查看采样动画分布，如下图所示： 其中，三个白色菱形点表示三个采样动画在Speed坐标轴上的位置，蓝色菱形点表示当前Speed取值。点击播放动画，并拖动蓝色菱形点在Speed坐标轴上移动，即可看到不同的动画插值结果。上述例子为1D的Blend Space，2D的Blend Space比1D多了纵坐标，使用方式与1D类似。 Blend Space资源设置完成后，即可拖入Animation Blueprint的Anim Graph中作为节点使用，如下图红框所示： 3、Animation Montage Unreal 4引擎中的Animation Montage是一个多功能的动画编辑工具。它可以将多个动画进行连接，并且支持自定义切分动画段、循环播放、任意跳转以及配合Layered Animation实现不同骨骼播放不同动画的功能。它支持如同普通Animation Sequence一样的Notification和Curve的编辑。同时，Animation Montage还支持重用。本节将对上述功能和使用方法进行介绍。 Animation Montage创建方式与其他资源相同，在Content Browser中双击即可打开编辑界面，如下图所示： 其中，区域1显示了资源的参数设置，区域2显示了Animation Montage中Timeline相关的设置，区域3显示了资源列表和Slot信息。 - 自定义切分动画 Animation Montage中重要概念是Slot和Section。Slot类似于一个容器，可以存放多个动画文件，并将它们按时序排列，其顺序可通过在Slot的Timeline上拖动进行设置。Slot的管理支持分组，每组可包含多个Slot，如下图所示： 上图显示了Slot的Timeline。其中的Slot名字为Jump，属于Default Group，并且包含了三个动画：ThirdPersonJump_Start、ThirdPersonJump_Loop和ThirdPersonJump_End。从区域3中动画文件列表中可以添加更多的动画文件到Slot中。 Section则是将整个Slot进行切分后的片段，主要作用是让开发者可以在每个片段上进行编辑，从而达到不同的效果。默认情况，Slot只有一个Section，通过新建Section可将Slot分成多段，切分位置可通过在Section的Timeline上拖动进行设置，如下图所示： 上图显示了Section的Timeline，其中包含了三个Section：Start、Loop和End，绿色竖线为其起始位置。添加新的Section可通过右键菜单创建。 通过在Section Track上添加Section并重新组合排列就可生成新的动画序列。该新生成的动画序列就是Animation Montage的输出结果，如下图所示： 上图红框中显示了Section Track，其中Start、Loop和End被顺序连接成一个动画序列。 - Branch Notify 同时，Animation Montage也具有普通Animation Sequence一样的Notification和Curve设置功能。并且，Animation Montage还具有新的Notification—Branch Notify。它支持在动画播放的任意时刻跳转到任意Section，如下图所示： 上图显示了Animation Montage中的Notify和Curve的Timeline，其功能和编辑方式与Animation Sequence中相同。其中，Element Timing的Timeline显示了当前所有Notify以及Section起始位置的可视化信息。比如，上图中绿框0、1、3分别表示动画Start、Loop和End动画的起始位置，而蓝框2表示TestNotify事件的起始位置。方框中序号显示了事件触发的时序，方便编辑的同时查看。图中的Animation Montage的Branch Notify事件响应逻辑可以在Animation Blueprint的Event Graph中实现，如下图所示： 上图红框中显示了在Event Graph中通过添加Montage Set Next Section节点实现Branch Notify的过程。其中，触发事件为TestNotify，即上图Animation Montage的Notify Timeline中添加的事件。当该事件被触发时，Montage Set Next Section节点会从Loop Section跳转到End Section，从而实现了Section的跳转功能。利用Section跳转，可以实现更加丰富的动画逻辑。 - 循环播放Section 此外，Section支持重复循环播放的功能，如下图所示： 上图中Loop Section变成蓝灰色（红框所示），表示其设置为循环播放。该功能可以用于制作有吟唱的技能，吟唱Section循环次数可根据吟唱时间确定，当吟唱结束后可利用Branch Notify跳转到技能后摇的Section。 Animation Montage的播放可以利用Anim Graph中的Slot节点指定播放的Slot，如下图所示： 其中，Slot节点的输入是动画状态机的Pose数据，输出则作为角色最终的Pose。然后在Event Graph中用Montage Play节点播放，如下图所示： Montage Play节点中可设置需要播放的Animation Montage资源名字，然后根据游戏中的逻辑连接到相应的流程中。 - Layered Animation Animation Montage配合Layered Animation能够实现不同骨骼播放不同动画的功能，例如：上半身骨骼播放攻击动画，下半身骨骼播放行走或是下蹲等动画。该功能可以在Anim Graph中利用Layered blend per bone节点配合Animation Montage实现，如下图所示。 其中，上图节点图中红框表示获取Pose的Cache信息，主要分别用于蓝框和红框操作来进行使用；蓝框为直接将Cache Pose进行索引，而红框则是将Cache Pose通过UpperBody的Animation Montage操作输出新的Pose，并将新的Pose索引到混合结点中。最终，通过融合结点，则可以计算出最终的动作融合效果。在Layered blend per bone节点的属性中可以对bone layer进行设置，如下图所示： 上图的设置使得Animation Montage的输出Pose被融合到spine_01骨骼以及其子节点骨骼中，从而实现了角色上半身骨骼播放Animation Montage的动画，下半身依旧播放Locomotion状态机动画的功能。 从大方向上来看，上述功能类似于Unity引擎Mecanim动画系统的Layer动画层融合功能，而从细微处来看， Layered blend per bone功能更像是Unity引擎中Layer动画层融合功能中又加入了Mask的效果，大家可以体会一下：） - Animation Montage重用 Unreal 4引擎提供了Animation Montage的重用功能。对于已经配置好的Animation Montage可以通过创建Child Animation Montage继承其父类的所有设置。在Child Animation Montage除了 Slot中的动画可以替换之外，其他的设置都无法修改，如下图所示： 其中，Slot的Timeline中黄色的部分（红框中）表示被替换掉的动画，绿色表示继承自父类的动画。从上图可见，其他的设置选项都被置为灰色，并且无法编辑。 4、Animation Composite Unreal 4引擎中的Animation Composite提供了将多个动画连接成一个动画的功能，并且支持与普通动画相同的Notify和Curve的功能。其创建方式与其他资源相同，在Content Browser中双击可打开编辑器，如下图所示： Animation Composite编辑界面与Animation Montage类似，上图红色区域中显示了Composite、Notify以及Curve的Timeline。其操作方式只需将右侧列表中的动画拖入Timeline即可。其播放方式是在Anim Graph中当做普通动画即可，如下图所示： Animation Composite的功能与Animation Montage有相似之处，它们都能将多个动画拼接成一个动画。但是，Animation Composite只提供了简单的拼接以及Notify和Curve的功能，而Animation Montage则提供了更加复杂的Section循环以及跳转功能。Animation Composite比Animation Montage使用更简单可直接当做一个Animation Sequence使用，而Animation Montage还需指定Slot等信息。 5、Post Process Animation Blueprint Unreal 4引擎的Skeletal Mesh资源参数中有一项Post Process Animation Blueprint，它为角色提供同时运行两个Animation Blueprint的功能。通常情况下，角色的Animation Blueprint可以在Character Blueprint中的Skeletal Mesh组件的Anim Class参数中进行设置，如下图所示： Post Process Animation Blueprint不仅能替代主Animation Blueprint，更重要的功能是它可以允许开发者访问和改变相关的骨骼动画信息。在Unity引擎中，我们可以在LateUpdate中访问和改变骨骼动画信息，而在Unreal 4引擎中，则是通过Post Process Animation Blueprint来处理这件事。 同样，Unreal 4引擎提供了AnimDynamic功能来与之配合使用，从而实现角色基于物理的动画，其功能类似于Unity Asset Store中的Dynamic Bone，而Dynamic Bone功能更像是AnimDynamic的简化版。 AnimDynamic节点支持对角色身上物件实现类似Ragdoll的动画，例如：头发、项链、衣服等。该效果能够在角色移动时，更加真实地模拟项链等物件的被施加外力从而产生摆动的动画。AnimDynamic的底层实现并不会计算物理碰撞，而是利用小方盒对物体的形状进行近似，然后通过限制方盒运动的角度范围近似模拟物理碰撞产生的自然摆动，如下图所示： Unreal官方网站【1】中介绍了利用AnimDynamic和Post Process Animation Blueprint对怪物身上的项链施加动画的教程，有兴趣的读者可进一步阅读。 参考文献 【1】 https://docs.unrealengine.com/latest/INT/Engine/Animation/AnimHowTo/AnimDynamics/index.html 以上则为Unreal 4引擎动画模块关于动画融合的介绍，希望通过上述内容，可以让您对Unreal引擎的动画模块产生明晰的感性认识。下周，UWA将为您带来动画模块中动画重定向、反向运动学等进阶功能，欢迎关注！
虚幻引擎学习之路：动画模块之基础篇
在之前的微信文章推送中，我们陆续为大家详细介绍了Unreal 4引擎的渲染模块（包括光照系统、材质系统、相机后处理和全局光照明），接下来我们将讲解动画模块的基础功能，并且将在后续文章中陆续推出该模块中进阶功能和特殊用途的介绍。 在此，特别感谢Unreal中国团队对于本篇文章中Unreal引擎相关内容的审核，并在UWA团队学习其引擎的道路上提供的大力支持。 前言 动画模块是游戏引擎中的重要部分，是驱动游戏中场景、角色能够活灵活现地做出动作的基础。UWA将分三块内容介绍Unreal 4引擎中的动画模块功能，其中包括：基础功能、进阶功能以及特殊功能。在介绍基础和进阶功能时，UWA不仅会对其功能进行重点讲解，同时也将结合Unity引擎中的相似功能进行类比，以求让大家可以更明晰地掌握Unreal引擎动画模块的功能使用。在特殊功能中，UWA将对Unreal 4引擎中针对特殊用途提供的功能进行重点解读。 本节主要介绍Unreal 4引擎动画模块中提供的基础功能。其中主要包括以下几个部分： 蒙皮网格 骨骼文件 动画文件 加粗文字动画逻辑 Unreal 4引擎的动画模块提供了四个编辑器：蒙皮网格编辑器、骨骼编辑器、动画文件编辑器和动画蓝图编辑器。上述基础功能可以分别在这四个编辑器中完成。在接下来的内容中，本文将在介绍基础功能的同时对相应编辑器的使用进行介绍。 一、 蒙皮网格 Unreal 4引擎支持从外部FBX文件中导入蒙皮网格和动画。导入操作可通过Content Browser->Import按钮完成。导入界面如下图所示： 其中，包含了网格、动画以及材质相关的导入设置。默认情况下Unreal 4引擎会为网格创建材质、物理以及骨骼文件。若选择导入动画，也会同时创建相应动画文件。 蒙皮网格在Unreal 4引擎中被称为Skeletal Mesh，其相当于Unity引擎中的Skinned Mesh，表示了带有蒙皮信息的网格。双击Content Browser中的蒙皮网格文件，即可打开蒙皮网格编辑器，如下图所示： 其中，左边区域（1区）是Skeletal Mesh的参数设置，右边区域（2区）是Morph Target（Blend Shape）以及预览场景设置。在Skeletal Mesh参数设置中，可以设置模型的材质、LOD、Clothing、骨骼、物理（Rigidbody）、Capsule Shadow等相关参数。其中，Clothing支持Nvidia的apex文件。骨骼文件即为下文中将介绍的Skeleton Asset。Capsule Shadow是Unreal 4引擎中Skeletal Mesh特有的软阴影。它使用形状不同的Capsule对Mesh进行近似表示，在渲染阴影时则使用该近似表示进行渲染，从而节省了渲染开销。右上角区域（3区）显示了与当面Skeletal Mesh相关的文件，点击即可快速打开相关文件。 二、骨骼文件 Unreal 4引擎中的骨骼文件被称为Skeleton Asset，其相当于Unity引擎中的Avatar文件。双击Content Browser中的Skeleton Asset文件即可打开该文件，如下图所示： 其中，左边区域（1区）显示了当前Skeletal Mesh的骨骼信息，可以选中其中某一骨骼进行编辑。同时，还可进行Retargeting的设置。右上角区域（2区）显示了选中骨骼的详细信息以及预览场景设置。在骨骼详细信息中可以设置其Transform和Name参数。右下角区域（3区）显示了动画事件和Animation Curve。其中，Animation Curve可以用于在动画过程中修改模型材质以及修改Morph Target曲线。Animation Curve的具体修改过程需要在动画编辑器中完成，此处只提供了预览。 在Unity引擎中，查看骨骼信息的方式是将其拖入场景，即可在左边Hierarchy页面中看到骨骼信息。需要注意的是，FBX模型不要开启Optimize GameObject选项。 在Unreal 4引擎中，共用Skeleton Asset的网格可以共享动画文件，即动画重定向（Animation Retargeting）。但要共享Skeleton Asset需要满足一定条件，即两个网格中的骨骼命名顺序要保持一致，如下图所示： 其中，骨骼(1,2,3)与骨骼(1,2,3,4)可以共用Skeleton Asset，但是与骨骼(1,3,2)无法共用。 在Unity引擎中，通过对Humanoid类型动画网格的骨骼进行映射也可以支持动画重定向。但是Unity中的动画重定向只能支持骨骼长度相近的网格，对于骨骼相差较大的网格会出现穿插或者分离的现象。Unreal 4引擎提供了该问题的解决方案，我们将在后续文章的动画重定向一节中进行介绍。 三、动画文件 Unreal 4引擎中的动画文件被称为Animation Sequence，其相当于Unity引擎中的Animation Clip。双击Content Browser中的Animation Sequence文件即可打开该文件，如下图所示： 其中，左边区域（1区）显示了动画文件的相关参数和骨骼结构信息。右边区域（2区）的上半部分显示了选中骨骼的参数以及预览场景设置，下半部分显示了该模型的动画文件列表。下部区域（3区）显示了动画事件、Animation Curve以及Animation Layers Track。其中，Animation Layers Track支持对动画中单个骨骼的Transform进行修改，从而生成新的动画序列。 Animation Curve以及Animation Layers Track的功能类似于Unity中创建Animation Clip的功能。在Unity中也可以通过对蒙皮网格创建Animation Clip来修改其材质以及单个骨骼的Transform，如下图所示： 四、动画逻辑 在引擎中播放动画需要处理动画逻辑。Unreal 4引擎提供了动画专用的蓝图资源：Animation Blueprint。在Animation Blueprint中包含两种节点图：Event Graph和Anim Graph。两者分别用于提供动画数值计算逻辑以及动画状态机的功能。本节将介绍Unreal 4引擎中动画逻辑的功能，主要包括：动画数值计算，动画状态机，动画事件以及动画逻辑重用。 4.1 动画数值计算 Event Graph节点图用于计算播放动画的逻辑中所需要的数值变量，并提供给后续状态机使用。其中，Event Blueprint Update Animation节点会在每帧被调用。通过该节点可以在动画播放期间每帧更新逻辑数值，如下图所示： 其中，该节点图每帧计算了两个数值：IsInAir和Speed。IsInAir用于表示角色目前是否在空中，Speed表示当前角色的速度。IsInAir可以用于在空中切换角色动画，而Speed则可用于切换角色Idle，Walk以及Run等动画。 4.2 动画状态机 Anim Graph节点图用于计算角色最后输出的动作，其本质就是动画状态机，类似于Unity引擎中的Animator Controller资源，如下图所示： 其中，该节点图包含了一个状态名字为Default的状态机，并且将状态机计算的角色Pose作为最终结果。双击State Machine节点，可查看其内容，如下图所示： 其中，该节点图包含了默认起始节点Entry以及角色的移动以及跳跃等状态，在不同状态之间根据不同的条件进行跳转。这与Unity引擎中的Animator Controller动画状态机过渡颇为相似。由上图可知从移动节点跳转到起跳状态的判定条件即为IsInAir变量值，该数值由Event Graph每帧更新。 双击Idle/Run节点可以看到其内容，如下图所示： 该节点图根据Speed变量的数值输出不同的移动动作。其最终结果是通过Blend Space将Idle、Walk和Run三个动作进行插值的计算结果。Blend Space动画融合将在本文接下来内容中进行介绍。 Unreal 4引擎动画状态机不仅提供了动画状态以及跳转的功能，也在动画跳转过程中提供了状态过渡时的动画融合的功能，其融合时间和融合方式可设置。并且，其还提供了Conduits节点作为分支节点，支持对同一类型多个状态的管理，如下图所示： 上图取自Unreal 4官方网站 其中，Killed为Conduits节点，连接了不同死亡方式的动画节点。如果同一节点有多个跳转条件可能同时满足，通过设置跳转条件的优先级解决。 4.3 动画事件 动画逻辑中一个重要的功能就是动画事件，在Unreal 4引擎中被称为Animation Notification，其相当于Unity引擎中的Animation Event。其中一种动画事件是在动画状态跳转中触发，可在转移条件的详细属性中设置，如下图所示： 其中，Custom Blueprint Event可以在Event Graph中定义。 另一种动画事件是在动画播放过程中触发，可在动画编辑器中设置。在动画编辑器的Notifies轨道上点击右键即可添加动画该种事件，如下图所示： 其中，这种动画事件支持三种类型：Play Particle Effect，Play Sound以及Custom。其中，Play Particle Effect和Play Sound分别用于在动画播放过程中触发粒子系统和音频的播放。Custom则可自定义功能。 此外，Unreal 4引擎还提供了Notify State功能。它分别在动画播放的开始、播放中、结束触发事件。在开始和结束时各触发一次，在播放中每帧触发一次。其添加方式如同Notification一样，如下图所示： 其中，Notify State包含三种类型：Timed Particle Effect、Trail和Custom。Timed Particle Effect支持在动画播放中添加粒子特效，并且粒子特效的播放时长可设置。Trail支持在蒙皮网格上实现拖尾特效。Custom类型需要通过AnimNotifyState Blueprint实现。 4.4 动画逻辑重用 对于行为相似的角色，有的时候只需要更换不同的动画文件，而不需要重新制作类似的逻辑，或者可重用部分逻辑。Unreal 4引擎提供了动画逻辑重用功能提高开发效率。接下来，本节将介绍两种动画逻辑重用的方式：Child Animation Blueprint和Sub Animation Blueprint Instance。 - Child Animation Blueprint 主要用于重用所有动画逻辑，仅替换不同的动画文件。其使用方式是通过原始Animation Blueprint创建Child Animation Blueprint，如下图所示： 然后，双击打开Child Animation Blueprint，并打开Asset Override Editor窗口，即可在Asset一列对应原动画位置的下拉菜单中选择新的动画文件，下图则显示了用BS_Jog替换原始Jog动画文件的设置过程。 - Sub Animation Blueprint Instance 主要用于将动画逻辑分解成模块，然后方便重用。其使用方式是：首先将通用功能动画逻辑编辑保存成Animation Blueprint，然后在需要引用的节点图中添加Sub Anim Instance节点，并指向通用功能Animation Blueprint，如下图所示： 其中，上图显示了在Animation Blueprint中引用Sub Anim Instance的过程。Sub Anim Instance的内部参数可以通过设置暴露给外部Animation Blueprint作为输入，如上图中的Effect Alpha参数。其设置在Sub Anim Instance节点的详细信息页面，如下图所示： 外部的Pose数据可通过在Sub Anim Instance节点图中添加Sub-Graph Input节点传入，如上图中的In Pose参数。其设置如下图所示： 其中，上图为上述例子中Sub Anim Instance的节点图。Sub Graph Input节点为其提供了外部输入Pose数据。 以上则为Unreal 4引擎动画模块的基础篇介绍，我们重点讲解了动画模块的主要编辑和视图模块，希望通过上述内容，可以让您对Unreal引擎的动画模块产生明晰的感性认识。下周，UWA将为您带来Unreal 4引擎更为高级的进阶功能，欢迎关注！
本期聚集话题：LightingDataAsset Delete后Lightmap展开问题、Gamma和Linear颜色空间问题、AlphaTest和Alpha Blend的性能差异... 我们将从日常技术交流中精选若干个开发相关的问题，建议阅读时间15分钟，认真读完必有收获。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。 UWA QQ群：793972859 UWA 问答社区：answer.uwa4d.com 制作 Q1：我这边做了个测试，把场景中的物件进行StaticBatch，用Lightmap烘焙，并且在烘焙完记录每个物件Renderer的lightmapindex and offset，然后在运行中，我删除了生成的LightingData.Asset，自己设置了Lightmapsetting，并且恢复了每个物体Renderer的Lightmapindex and offset, 结果我发现烘焙出来的场景Lightmap还是不对的。我怀疑是不是LightingData.Asset 还记录了别的东西，想问问看UWA有没有什么经验呢？ 我在answer.uwa4d.com上打包一个测试用例，打开Scene/LightMapTest.unity 这个场景即可。 这个问题还是出在Static Batching上。经过测试我们发现如果去掉场景中物件的“Static Batching”标记，换成手动Batching，即采用StaticBatchingUtility.Combine的方式结果就正确了。 同时，我们发现使用LightingData.Asset时，开启Static Batching与不开启都正确，因此我们认为LightingData.Asset可能记录了与CombinedMesh有关的数据，导致如果仅仅使用gameobject的LightMap参数无法正确读取CombinedMesh的LightMap UV。另外一种可能是Unity的一个Bug。 最后，我们建议采用上述手动Batching的方式，即可解决此问题。 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/59c0d16c5cd7219d3bbfb3b3 制作 Q2：我们场景中有两种Unlit物体，一种是在脚本中通过Mesh顶点色（Mesh.colors）赋颜色值，一种是用Shader属性传递颜色值，在Gamma颜色空间时两种物体的渲染颜色是一致的： 但是切换到Linear空间发现不一样了，如下图，左边是顶点色，右边是Shader属性颜色： 顶点颜色和属性都没改过，只是切换了颜色空间，请问是什么情况？ 我能想到的有两种可能性： 1）顶点色的颜色值在Shader中没有被正确处理； 2）右边使用贴图的导入设置中sRGB的选项有错误。 看上去左边的在线性空间下变亮了，而右侧的方块颜色在两种Space下差别不大，因此推测第一种的可能性比较大，顶点色在很多情况下并不被当作颜色处理，而作为值来存储，所以有可能这里处理有问题。至于为什么变亮了，感觉是值经过了伽马校正变大了。 Unity有接口在两种颜色空间下转换，建议题主可以查一下，在Shader中修改代码来排查下是哪里的问题。 该问题来自UWA 社区，感谢贾伟昊提供了回答，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/59c387de79a00b1e16da7414 UI 制作 Q3：当使用Linear设置时，UI上图片透明度等也跟着变化，出来的效果与美术人员的需求不一样。请问有什么办法可以让UI不使用Linear或者让UI可以在Linear影响下也正常呢？ 推荐题主可以看下这个帖子 https://forum.unity.com/threads/bug-with-bypass-srgb-sampling.282469 “When switching to linear mode it (greatly) changes how blending works, this is because when sampling only the rgb channels are linearised as it is assumed the alpha is a linear scale representation already.” 简单说，这是正常现象，里面有三种解决方法： 1）改资源的 alpha 通道： Reauthor the content for linear blending (this is supported in photoshop). 2）用 Shader 提前处理下颜色空间的转换： Add a workaround in the shader if in linear space to ‘pretend’ the alpha from the source texture is in the correct format. 3）用屏幕后处理来绕过 UI 的 linear 处理。 可以直接搜 topsekret 的回答。 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/59c1e5c66c8b329c76a5f66e 制作 Q4：我们项目在实现显示部分模型像素的时候用了Builtin的Cutout Shader，还有一种方式是用Blend，也就是把Alpha设成0。我想请教下这两种方式有什么区别？性能上哪个好？在什么情况下使用比较高效？ 答案一：功能上，Alpha Test无法实现半透的效果，只能让物体的某些部分按照Alpha的值“透”或者“不透”，Alpha Blend可以做到半透，但是在多个Alpha Blend的物体重叠的情况下，由于不写Z会导致先后顺序可能存在问题。 由于Alpha Test的实现是基于discard的方式，即在PS里发现Alpha的阈值不符合要求，就将该像素丢弃掉不进行渲染。这种方式会导致使用了类似early-z技术的GPU优化失效，导致性能下降。 另外，知乎上有讨论——“对于PowerVR系列而言会影响HSR（隐藏面消除），打断ISP处理覆盖同一像素的几何体（更具体一点，ISP要得到PS执行后的结果才能正确进行HSR，在这一过程中，所有覆盖了带有discard操作像素的几何体全部都要等待）”。 参考：https://www.zhihu.com/question/27875435 所以整体来说，在移动设备上，Blend具有更高的性能，但是有些效果未必能够达到，结果可能不是美术设计想要的效果，特效中用得较多。 题主也可以参考乐乐的这篇：http://blog.csdn.net/candycat1992/article/details/41599167 感谢贾伟昊提供了以上回答 答案二：不考虑硬件去讨论这个问题有点形而上学，另外，实际使用中还要考虑Overdraw的影响，而不是单纯的一层像素，再有就是，网上很多资料其实已经落后了。所以在知乎上搜这个问题，会有大牛告诉你Blend更快，但是会马上有人把测试截图贴出来说看我们的实测数据“test明显更快”。 建议题主分别用Mali，PowerVR，Adreno的移动设备来测一下就知道差异了。 另外，PowerVR的HSR，楼主可以试试看用PowerVR Tool，看看Blend和Test，是不是像传说的那样，都不能开启HSR，可能会有意向不到的结果。目前我们测试最后的结论是，尽量还是用test。 感谢 kylda提供了以上回答 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/59c09367a3d42c411c37a0cd/ 制作 Q5：我们有一个需求，是在不同的动画播放的时候切换不同的NormalMap，但是动画的“Add Property”没有NormalMap等变量，请问有什么好的办法可以解决吗？ 可以自己写一个脚本，挂载需要切换NormalMap的节点上面，public NormalMap等变量，然后在脚本里面写段代码，当发现NormalMap修改了，就去改模型的NormalMap。而动画是可以修改这个脚本变量了。 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/59bf72d0ca564ff3748d4f27 今天的分享就到这里。当然，生有涯而知无涯。在漫漫的开发周期中，您看到的这些问题也许都只是冰山一角，我们早已在UWA问答网站（answer.uwa4d.com）上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入，也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。 官网：www.uwa4d.com 官方技术博客：blog.uwa4d.com 官方问答社区：answer.uwa4d.com 官方技术QQ群：793972859（仅限技术交流）
为了优化Unity动画文件，作者从两个角度出发：1）压缩浮点数精度和 2）去除scale曲线，通过多次实验观察，得出了通过精度优化降低内存的本质，文末附工具代码和使用说明，值得大家探究和借鉴。 前不久笔者在UWA群里和一些朋友们讨论动画文件精度优化的问题，参考了UWA问答上王亮的代码后，我也尝试优化了动画文件，发现如下的一个很奇怪的现象。下图中Inspector中显示的大小并没有任何变化，但是文件大小和Profiler中的内存大小确实是减小了。那么Inspector中显示的Size是什么含义呢？ 下图中的动画文件不包含Scale曲线，所以这次优化中只压缩了浮点数精度。 我分别对比了动画文件优化前和优化后的大小。 FileSize FileInfo.Length取得的文件大小 可以在操作系统的文件系统中看到 MemorySize Profiler.GetRuntimeMemorySize取得的内存大小 可以在Profiler中通过采样看到 分别在真机和Editor下进行了采样 BlobSize 反射取得的AnimationClipStats.size二进制大小 显示在AnimationClip的Inspector的面板上 红色框内即是BlobSize，在我的理解，FileSize是指文件在硬盘中占的大小，BlobSize是从文件反序列化出来的对象的二进制大小。Editor下的MemorySize不仅有序列化后的内存大小，还维护了一份原文件的内存。就像我们在Editor下加载一张Texture内存是双份一样，而真机下就约等于BlobSize。真机下的MemorySize和Inspector里的BlobSize非常接近，BlobSize可以认为是真机上的内存大小，这个大小更有参考意义。 同时，我也对去除Scale曲线的方法进行了实验。下图这个动画文件原来Inspector中Scale的值为4，即有Scale曲线，原始文件BlobSize为10.2KB，去除Scale曲线后，Blob Size变为7.4KB，所以BlobSize减小了27%。 Curve减少导致内存减小 从上面的实验可以看出来，只裁剪动画文件的压缩精度，没有引起Curve减少。BlobSize是不会有任何变化的，因为每个浮点数固定占32bit。而文件大小、AB大小、Editor下的内存大小，压缩精度后不管有没有引起Curve的变化，都会变小。 裁剪动画文件的精度，意味着点的位置发生了变化，所以Constant Curve和Dense Curve的数量也有可能发生变化。由于是裁剪精度所以动画的点更稀疏了，而连续相同的点更多了。所以Dense Curve是减少了，Constant Curve是增多了，总的内存是减小了。 Constant Curve只需要最左边的点就可以描述一个曲线段。 只裁剪精度使BlobSize减小的实例 裁剪精度前，大小为2.2kb，ScaleCurve为0， ConstantCurve为4(57.1%)，Stream(使用Optimal模式这部分数据将储存为Dense)为3(42.9%)。 裁剪精度后，大小为2.1kb，ConstantCurve为7(100%)，Stream为0(0%)。裁剪完精度后导致ConstantCurve增加了3，Stream(Optimal模式下即为Dense)减少了3，BlobSize减小了0.1kb。 因此，可以看出，通过精度优化降低内存的方式，其实质是将曲线上过于接近的数值（例如相差数值出现在小数点4位以后）直接变为一致，从而使部分曲线变为constant曲线来降低内存消耗。 总结 隔壁项目组对他们项目中所有的动画文件都进行了优化。其中文件大小从820MB->225MB, ab大小从72MB->64MB, 内存大小从50MB->40MB。总的来说动画文件的scale越多优化越明显。 取BlobSize代码 AnimationClip aniClip = AssetDatabase.LoadAssetAtPath<AnimationClip> (path); var fileInfo = new System.IO.FileInfo(path); Debug.Log(fileInfo.Length);//FileSize Debug.Log(Profiler.GetRuntimeMemorySize (aniClip));//MemorySize Assembly asm = Assembly.GetAssembly(typeof(Editor)); MethodInfo getAnimationClipStats = typeof(AnimationUtility).GetMethod("GetAnimationClipStats", BindingFlags.Static | BindingFlags.NonPublic); Type aniclipstats = asm.GetType("UnityEditor.AnimationClipStats"); FieldInfo sizeInfo = aniclipstats.GetField ("size", BindingFlags.Public | BindingFlags.Instance); var stats = getAnimationClipStats.Invoke(null, new object[]{aniClip}); Debug.Log(EditorUtility.FormatBytes((int)sizeInfo.GetValue(stats)));//BlobSize 工具代码 最后附上工具的代码和简要使用说明，选中想要优化的文件夹或文件，右键Animation->裁剪浮点数去除Scale。 //**************************************************************************** // // File: OptimizeAnimationClipTool.cs // // Copyright (c) SuiJiaBin // // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A // PARTICULAR PURPOSE. // //**************************************************************************** using System; using System.Collections.Generic; using UnityEngine; using System.Reflection; using UnityEditor; using System.IO; namespace EditorTool { class AnimationOpt { static Dictionary<uint,string> _FLOAT_FORMAT; static MethodInfo getAnimationClipStats; static FieldInfo sizeInfo; static object[] _param = new object[1]; static AnimationOpt () { _FLOAT_FORMAT = new Dictionary<uint, string> (); for (uint i = 1; i < 6; i++) { _FLOAT_FORMAT.Add (i, "f" + i.ToString ()); } Assembly asm = Assembly.GetAssembly (typeof(Editor)); getAnimationClipStats = typeof(AnimationUtility).GetMethod ("GetAnimationClipStats", BindingFlags.Static | BindingFlags.NonPublic); Type aniclipstats = asm.GetType ("UnityEditor.AnimationClipStats"); sizeInfo = aniclipstats.GetField ("size", BindingFlags.Public | BindingFlags.Instance); } AnimationClip _clip; string _path; public string path { get{ return _path;} } public long originFileSize { get; private set; } public int originMemorySize { get; private set; } public int originInspectorSize { get; private set; } public long optFileSize { get; private set; } public int optMemorySize { get; private set; } public int optInspectorSize { get; private set; } public AnimationOpt (string path, AnimationClip clip) { _path = path; _clip = clip; _GetOriginSize (); } void _GetOriginSize () { originFileSize = _GetFileZie (); originMemorySize = _GetMemSize (); originInspectorSize = _GetInspectorSize (); } void _GetOptSize () { optFileSize = _GetFileZie (); optMemorySize = _GetMemSize (); optInspectorSize = _GetInspectorSize (); } long _GetFileZie () { FileInfo fi = new FileInfo (_path); return fi.Length; } int _GetMemSize () { return Profiler.GetRuntimeMemorySize (_clip); } int _GetInspectorSize () { _param [0] = _clip; var stats = getAnimationClipStats.Invoke (null, _param); return (int)sizeInfo.GetValue (stats); } void _OptmizeAnimationScaleCurve () { if (_clip != null) { //去除scale曲线 foreach (EditorCurveBinding theCurveBinding in AnimationUtility.GetCurveBindings(_clip)) { string name = theCurveBinding.propertyName.ToLower (); if (name.Contains ("scale")) { AnimationUtility.SetEditorCurve (_clip, theCurveBinding, null); Debug.LogFormat ("关闭{0}的scale curve", _clip.name); } } } } void _OptmizeAnimationFloat_X (uint x) { if (_clip != null && x > 0) { //浮点数精度压缩到f3 AnimationClipCurveData[] curves = null; curves = AnimationUtility.GetAllCurves (_clip); Keyframe key; Keyframe[] keyFrames; string floatFormat; if (_FLOAT_FORMAT.TryGetValue (x, out floatFormat)) { if (curves != null && curves.Length > 0) { for (int ii = 0; ii < curves.Length; ++ii) { AnimationClipCurveData curveDate = curves [ii]; if (curveDate.curve == null || curveDate.curve.keys == null) { //Debug.LogWarning(string.Format("AnimationClipCurveData {0} don't have curve; Animation name {1} ", curveDate, animationPath)); continue; } keyFrames = curveDate.curve.keys; for (int i = 0; i < keyFrames.Length; i++) { key = keyFrames [i]; key.value = float.Parse (key.value.ToString (floatFormat)); key.inTangent = float.Parse (key.inTangent.ToString (floatFormat)); key.outTangent = float.Parse (key.outTangent.ToString (floatFormat)); keyFrames [i] = key; } curveDate.curve.keys = keyFrames; _clip.SetCurve (curveDate.path, curveDate.type, curveDate.propertyName, curveDate.curve); } } } else { Debug.LogErrorFormat ("目前不支持{0}位浮点", x); } } } public void Optimize (bool scaleOpt, uint floatSize) { if (scaleOpt) { _OptmizeAnimationScaleCurve (); } _OptmizeAnimationFloat_X (floatSize); _GetOptSize (); } public void Optimize_Scale_Float3 () { Optimize (true, 3); } public void LogOrigin () { _logSize (originFileSize, originMemorySize, originInspectorSize); } public void LogOpt () { _logSize (optFileSize, optMemorySize, optInspectorSize); } public void LogDelta () { } void _logSize (long fileSize, int memSize, int inspectorSize) { Debug.LogFormat ("{0} \nSize=[ {1} ]", _path, string.Format ("FSize={0} ; Mem->{1} ; inspector->{2}", EditorUtility.FormatBytes (fileSize), EditorUtility.FormatBytes (memSize), EditorUtility.FormatBytes (inspectorSize))); } } public class OptimizeAnimationClipTool { static List<AnimationOpt> _AnimOptList = new List<AnimationOpt> (); static List<string> _Errors = new List<string>(); static int _Index = 0; [MenuItem("Assets/Animation/裁剪浮点数去除Scale")] public static void Optimize() { _AnimOptList = FindAnims (); if (_AnimOptList.Count > 0) { _Index = 0; _Errors.Clear (); EditorApplication.update = ScanAnimationClip; } } private static void ScanAnimationClip() { AnimationOpt _AnimOpt = _AnimOptList[_Index]; bool isCancel = EditorUtility.DisplayCancelableProgressBar("优化AnimationClip", _AnimOpt.path, (float)_Index / (float)_AnimOptList.Count); _AnimOpt.Optimize_Scale_Float3(); _Index++; if (isCancel || _Index >= _AnimOptList.Count) { EditorUtility.ClearProgressBar(); Debug.Log(string.Format("--优化完成-- 错误数量: {0} 总数量: {1}/{2} 错误信息↓:\n{3}\n----------输出完毕----------", _Errors.Count, _Index, _AnimOptList.Count, string.Join(string.Empty, _Errors.ToArray()))); Resources.UnloadUnusedAssets(); GC.Collect(); AssetDatabase.SaveAssets(); EditorApplication.update = null; _AnimOptList.Clear(); _cachedOpts.Clear (); _Index = 0; } } static Dictionary<string,AnimationOpt> _cachedOpts = new Dictionary<string, AnimationOpt> (); static AnimationOpt _GetNewAOpt (string path) { AnimationOpt opt = null; if (!_cachedOpts.ContainsKey(path)) { AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip> (path); if (clip != null) { opt = new AnimationOpt (path, clip); _cachedOpts [path] = opt; } } return opt; } static List<AnimationOpt> FindAnims() { string[] guids = null; List<string> path = new List<string>(); List<AnimationOpt> assets = new List<AnimationOpt> (); UnityEngine.Object[] objs = Selection.GetFiltered(typeof(object), SelectionMode.Assets); if (objs.Length > 0) { for(int i = 0; i < objs.Length; i++) { if (objs [i].GetType () == typeof(AnimationClip)) { string p = AssetDatabase.GetAssetPath (objs [i]); AnimationOpt animopt = _GetNewAOpt (p); if (animopt != null) assets.Add (animopt); } else path.Add(AssetDatabase.GetAssetPath (objs [i])); } if(path.Count > 0) guids = AssetDatabase.FindAssets (string.Format ("t:{0}", typeof(AnimationClip).ToString().Replace("UnityEngine.", "")), path.ToArray()); else guids = new string[]{}; } for(int i = 0; i < guids.Length; i++) { string assetPath = AssetDatabase.GUIDToAssetPath (guids [i]); AnimationOpt animopt = _GetNewAOpt (assetPath); if (animopt != null) assets.Add (animopt); } return assets; } } } 这是侑虎科技第246篇原创文章，感谢作者舒航供稿，欢迎转发分享，未经作者授权请勿转载。当然，如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者博客：http://www.codershu.com. 同时，作者也是U Sparkle活动参与者哦，UWA欢迎更多开发朋友加入U Sparkle开发者计划，这个舞台有你更精彩！
本期我们聚集了这些话题：资源泄漏、ETC压缩后图片质量模糊、降低动画文件的浮点数精度、iOS内存的提升... 我们将从日常技术交流中精选若干个开发相关的问题，建议阅读时间15分钟，认真读完必有收获。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。 UWA QQ群：465082844 UWA 问答社区：answer.uwa4d.com 资源管理 Q1：使用AssetBundle.Unload(true)释放AnimationClip , 但发现AnimationClip在Profiler看到还存在，请问要怎样才释放干净呢？ 如果这个AnimaitonClip是从AssetBundle中加载过来的，那么当这个AssetBundle调用unload(true)时，该AnimationClip理论上是会被强行卸载的。如果Profiler中查看还有，那么只有两种情况： （1）仍然存在的AnimationClip是从其他地方加载来的，而不是来自于该卸载的AssetBundle； （2）Unity引擎的Bug，这种概率不是没有，但很小。 此问答来自于UWA 问答社区： https://answer.uwa4d.com/question/5937adc70ded23f1039c67ff 如您对该问题仍有疑问，可以转至社区进行进一步交流。 资源管理 Q2：如何处理ETC压缩完图片质量模糊和ARGB32内存占用太大这种关系？ 研发团队可以从参考以下几个方法： 1）使用ETC压缩的图片尽量减少过渡，这是美术制作时需要注意的，很多情况下并不需要明显的过渡，只是需要半透明而已； 2）当然，有部分过渡色是必须的，这时候就要把相关小图整合到一张RGBA32的图片上； 3）在资源管理上入手。一般情况下，我们是允许部分资源使用RGBA32，但是要注意，及时卸载掉不使用的资源，以保证内存峰值在可控范围内。 感谢UWA 问答社区的郑骁提供了以上回答。 在我们团队里面有一个意识，品质高和性能好有些时候是一对矛盾，有些情况要无损压缩是很难做到的。我们目前如果ETC压> 缩效果不好的，会使用RGBA32并降一个尺寸规格来压缩，这样虽然图片会模糊点，但是过渡还是平滑的。 感谢UWA 问答社区的邓永健提供了以上回答。 此问答来自于UWA 问答社区： https://answer.uwa4d.com/question/593629f27780cfc82fa5f867 资源管理 Q3：如何降低动画文件的浮点数精度？之前UWA给过思路，但具体怎么个执行方法？ 动画文件后处理可以做两件事，1）精度压缩，2）scale曲线剔除。比起用工具修改原始fbx文件，这样比较灵活。 实际测试，在开启Optimal压缩的情况下，加上这个后处理，能再节省40%左右。 void OnPostprocessModel(GameObject g) { // for skeleton animations. List<AnimationClip> animationClipList = new List<AnimationClip>(AnimationUtility.GetAnimationClips(g)); if (animationClipList.Count == 0) { AnimationClip[] objectList = UnityEngine.Object.FindObjectsOfType (typeof(AnimationClip)) as AnimationClip[]; animationClipList.AddRange(objectList); } foreach (AnimationClip theAnimation in animationClipList) { try { //去除scale曲线 foreach (EditorCurveBinding theCurveBinding in AnimationUtility.GetCurveBindings(theAnimation)) { string name = theCurveBinding.propertyName.ToLower(); if (name.Contains("scale")) { AnimationUtility.SetEditorCurve(theAnimation, theCurveBinding, null); } } //浮点数精度压缩到f3 AnimationClipCurveData[] curves = null; curves = AnimationUtility.GetAllCurves(theAnimation); Keyframe key; Keyframe[] keyFrames; for (int ii = 0; ii < curves.Length; ++ii) { AnimationClipCurveData curveDate = curves[ii]; if (curveDate.curve == null || curveDate.curve.keys == null) { //Debug.LogWarning(string.Format("AnimationClipCurveData {0} don't have curve; Animation name {1} ", curveDate, animationPath)); continue; } keyFrames = curveDate.curve.keys; for (int i = 0; i < keyFrames.Length; i++) { key = keyFrames[i]; key.value = float.Parse(key.value.ToString("f3")); key.inTangent = float.Parse(key.inTangent.ToString("f3")); key.outTangent = float.Parse(key.outTangent.ToString("f3")); keyFrames[i] = key; } curveDate.curve.keys = keyFrames; theAnimation.SetCurve(curveDate.path, curveDate.type, curveDate.propertyName, curveDate.curve); } } catch (System.Exception e) { Debug.LogError(string.Format("CompressAnimationClip Failed !!! animationPath : {0} error: {1}", assetPath, e)); } } } 此问答来自于UWA 问答社区： https://answer.uwa4d.com/question/593955b6c42dc04f4d8f7341 感谢UWA 问答社区的WangLiang提供了以上回答。 内存管理 Q4：请问一下iOS平台打出来的带Profiler的包，在XCode里看内存运行期间涨了近60MB，Profile里面看ReservedTotal只涨了10MB，这个是为什么呢？测了很多次，都是xcode内存涨很多，但是资源看不到太大的变化，最后游戏可能会崩溃，求解？ 这说明iOS内存的提升绝大部分并不是由Unity引擎所造成的，一般来说，系统库会占用一定的内存，但如果内存持续上升直至崩溃，那么建议研发团队从所用第三方库（比如Lua、Native插件等）入手，检测其是否存在内存泄露风险。 资源管理 Q5：UWA报告中的资源是否是实时的？我看给的是原始格式，不是压缩后的内存数值。 ASTC格式并不是所有机型都支持，在Android端只有配有高端Mali芯片的机器才支持，对于不支持的，引擎会将其解压成RGBA32进行处理，所以图片中的内存值过大，很可能是项目在不支持ASTC格式的设备上所看到的结果。 今天的分享就到这里。当然，生有涯而知无涯。在漫漫的开发周期中，您看到的这些问题也许都只是冰山一角，我们早已在UWA问答网站（answer.uwa4d.com）上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入，也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。 官网：www.uwa4d.com 官方技术博客：blog.uwa4d.com 官方问答社区：answer.uwa4d.com 官方技术QQ群：465082844（仅限技术交流）
本期话题：频繁GC是否能真正提升手游流畅度？如何优化UI中的动静元素？关于UI Panel重绘，你需要知道的一些Tips、动画文件定格位移不一致，如何切换？ 精选5个性能优化问题，建议阅读时间15分钟，认真读完必有收获。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。 UWA QQ群：793972859 动画 Q1：我有一个带位移动画A，有位置有旋转的变化，需要播完这个动画后切换到一个原地的动画B。现在我发现动画A没播放就会切换到B了，导致一些位移数据并没有作用到模型上，这种情况怎么处理？ Mecanim动画系统提供了“Apply Root Motion”功能来满足两个动画文件顶点位移不一致的切换的需求。当开启“Apply Root Motion”功能后，角色的GameObject位置会随着动画的更新而更新，因此，问题中的A切到B后，GameObject的位移将不会改变。建议该研发团队检测GameObject Animator组件的“Apply Root Motion”功能是否开启。 性能优化 Q2：关于GC优化，Unity官方文档中有如下两个推荐的方法，请问在手游上是否适用？（1）频繁GC，隔几秒主动调用一次，保证不需要的堆内存能够及时释放。适用于对内存总量较小的情况。（2）尽量避免GC，可以考虑先撑大内存。 上述两种方法在目前的移动游戏中并不适用。 （1）中频繁地调用GC是否会降低GC的CPU开销吗？理论上会，但实际上，即便是降低后的GC开销也会导致频繁的卡顿。下图为UWA上一款实际测评项目在红米Note2上频繁调用GC的开销。可以看到在红框处，其GC调用非常频繁，但是其CPU占用并没有因频繁调用而降低。 （2）中尽可能避免GC是非常推荐的，但撑大堆内存的方式是欠妥当的。最好的方式是通过降低不必要的堆内存分配来尽可能避免的GC的到来。UWA建议GC的调用频率应该在1000帧/次以上。具体的做法建议查看我们之前的技术推送：Unity内存的深度剖析。 性能优化 Q：如下图所示，什么情况下会触发MaskableGraphic.Enable() ？现在我得到的结论是只要用到了Image组件，然后禁用启用带有这个组件的物体，都会触发进而产生GC。大家都怎么显示隐藏图片这种需求的，SetActive这种方式容易产生GC。 在UGUI中，Image组件并没有重写其父类的OnEnable函数，所以在激活时会出现MaskableGraphic.OnEnable。其中出现堆内存开销的话，通常是因为其父类函数Graphic.OnEnable中，UGUI在进行全局容器的Add等类似的操作时，遇到了扩容等产生堆内存的操作。 总之，在UGUI中，UI元素的激活和禁用所导致的堆内存分配，通常是不会持续出现的，其实不需要特别地处理。但对于其CPU开销，在UI元素数量较大时，依然是可观的，所以我们依然建议，对于激活禁用操作较为频繁的UI元素，可以尝试通过移出屏幕，缩放为0等方式来避免SetActive的调用。 UI 性能 Q：UWA建议“将较多的动态UI元素分组放在不同的UI Panel中”，那么请问如果是ScrollView里面多个item的话，是否意味着每个Item都加一个Panel会更好一些？ “动态元素”其实是相对的。在 ScrollView 中，一般情况下，如背包，其中的Item在滑动过程中是相对静止的，因此这种情况下只需要将这些Item放在一个UIPanel中即可。但在类似于聊天界面中，存在一些UI元素是有持续的动画的，那么就需要考虑对这类元素进行特殊处理，可以尝试将这部分有动画的UI放在独立的UIPanel中，或者在个数不多的情况下，各自变为一个UIPanel等。 UI 性能 Q：如果我在UIPanel下面放的是Sprite Renderer而不是NGUI的Sprite，是否会引起整个UIPanel的重绘？ 在NGUI中使用Unity2D的Sprite有两种情况，一种是直接使用Unity2D的SpriteRenderer组件，这种情况下，NGUI和Unity2D之间是互不影响的，只是在深度的设置上相对会比较麻烦一些。另一种是使用NGUI的UI2DSprite组件，而该组件是NGUI对Unity2D的SpriteRenderer组件上进行封装的，方便使其深度与其他UI元素进行穿插，因此其行为和其他的UI元素一致，在某些情况下是有可能引起UIPanel的重绘。 今天的分享就到这里。也欢迎热爱进步的你加入UWA的QQ群（793972859），也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。 比起闭门造车，我们更乐意与大家各抒己见，畅所欲言；比起形而上的泛泛而谈，我们更乐意与大家直击痛点，对症下药。