UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技 - usparkle
type: channel
link: https://blog.uwa4d.com/tag/usparkle/
items: 《三弦》技术分享—波纹效果及其实现
items: Unity移动端动态阴影总结
items: Lua性能优化—Lua内存优化
items: 用好Lua+Unity，让性能飞起来—LuaJIT性能坑详解
items: 动画重定向技术分析及其在Unity中的应用
items: VR交互动画短片《拾梦老人》的开发经历
items: GPU Skinning 加速骨骼动画
items: Unity 5.x AssetBundle零冗余解决方案
items: Unity手游iOS内存分析和测试
items: Lua配置表存储优化方案

《三弦》技术分享—波纹效果及其实现
type: item
date: 2018-05-31T04:58:00+03:00
description: 这篇文章主要和大家聊一聊带波浪的地面的实现，以及场景特效的实现方法。如下图所示，地板随时间以波浪状变化，角色在地板上走的时候会随着它的波浪高低起伏。 视觉实现 关于波浪效果的实现，作者最开始的时候走了些弯路。一开始，我想的是试试用直接修改地板Mesh的方式，达到视觉和物理上的同步实现。事实上这被证明是非常不科学的，因为修改Mesh会严重影响性能。在涉及物理的情况下，我测试的结果是游戏帧率调到了个位数。所以最终选择了利用Shader实现视觉效果+利用脚本伪造行走物理效果的办法。 首先是地板的模型，这里使用了一个Plane： 波浪函数的实现，这里是函数实现，用的是两个方向的波进行叠加，波纹随时间变化。即构造一个全局函数，因为c#代码和Shader代码里都要使用，所以这是一个以World-Space的坐标x,z，以及游戏时间Time.time为变量的，以波浪高度y为结果的函数。使用的波纹函数为三角函数： 两路波形线性叠加，具体实现如下： [SerializeField] float height1 = 1f; [SerializeField] float frequency1 = 1f; [SerializeField] float wavelength1 = 1f; [SerializeField] Vector3 wave1Direction; [SerializeField] float height2 = 1f; [SerializeField] float frequency2 = 1f; [SerializeField] float wavelength2 = 1f; [SerializeField] Vector3 wave2Direction; [SerializeField][Range(0,10f)] float overallHeight = 1f; [SerializeField][Range(0,10f)] float overallFrequency = 1f; [SerializeField][Range(0,10f)] float overallWaveLength = 1f; [SerializeField] float offset; public float GetHeight( float x , float z ) { float x1 = x * wave1Direction.normalized.x + z * wave1Direction.normalized.z; float y1 = Mathf.Sin (x1 / (wavelength1 * overallWaveLength) + Time.time * frequency1 * overallFrequency) * height1 * overallHeight; float x2 = x * wave2Direction.normalized.x + z * wave2Direction.normalized.z; float y2 = Mathf.Sin (x2 / (wavelength2 * overallWaveLength) + Time.time * frequency2 * overallFrequency) * height2 * overallHeight; return y1 + y2 + offset; } 当然这是在C#里的实现，其实和在Shader里的实现类似。不过注意，需要把函数的参数传给Material。同时，需要每帧对时间参数进行同步(C#代码）： public void SetMaterial( Material material ) // called at start { material.SetFloat ("_height1", height1 * overallHeight); material.SetFloat ("_frequency1", frequency1 * overallFrequency); material.SetFloat ("_waveLength1", wavelength1* overallWaveLength); material.SetFloat ("_height2", height2 * overallHeight); material.SetFloat ("_frequency2", frequency2 * overallFrequency); material.SetFloat ("_waveLength2", wavelength2* overallWaveLength); material.SetVector ("_wave1Dire…

Unity移动端动态阴影总结
type: item
date: 2017-05-03T09:01:00+03:00
description: 此文将Unity移动端的大量美术阴影作了归纳总结，如Cubemap的动态软阴影、地面云阴影、角色脚下的阴影面片等阴影效果，无论是对项目的制作开发还是平日的学习积累，都是极具参考价值的宝贵素材。 壹 · 基于Cubemap的动态软阴影 ARM公司曾利用Unity开发过两款技术Demo（Ice Cave 和 Chess Room），里面充分发挥了Cubemap的强大威力—既用来做地面反射、冰块折射，还用来做动态软阴影，利用简单的技术做出了高品质的画面。下面是Ice Cave的效果： 其中反射、折射部分参考：Reflections Based on Local Cubemaps in Unity、ARM Guide for Unity Developers，下面主要介绍下软阴影部分原理。 以此国际象棋屋为例，屋子中间放置一个Reflect probe来拍摄周围环境，只用了Cubemap的RGB通道，而周围环境的Alpha其实也代表了光是穿透了窗户还是被墙壁遮挡，那就可以利用Cubemap剩余的Alpha通道就可以来存储光和周围环境的遮挡情况，Alpha通道图如下： 生成Cubemap细节可以参考AssetStore中的源码。 利用生成的Cubemap渲染阴影主要分为两步，一是向量L（vertex-to-light）转换为Lp（校准过的vertex-to-light，用来采样Cubemap用），二是软阴影处理。 1. L到Lp向量校准： 输入参数： _EnviCubeMapPos >> Cubemap 中心坐标 _BBoxMax >> 包围盒最大坐标，生成Cubemap时自动生成 _BBoxMin >> 包围盒最小坐标，生成Cubemap时自动生成 V： >> 顶点坐标 L： >> vertex-to-light向量，已normalized 输出参数： Lp >> 校准后的vertex-to-light向量，作为UV去采样Cubemap 校准过程： // Working in World Coordinate System. vec3 intersectMaxPointPlanes = (_BBoxMax - V) / L; vec3 intersectMinPointPlanes = (_BBoxMin - V) / L; // Looking only for intersections in the forward direction of the ray. vec3 largestRayParams = max(intersectMaxPointPlanes, intersectMinPointPlanes); // Smallest value of the ray parameters gives us the intersection. float dist = min(min(largestRayParams.x, largestRayParams.y), largestRayParams.z); // Find the position of the intersection point. vec3 intersectPositionWS = V + L * dist; // Get the local corrected vector. Lp = intersectPositionWS - _EnviCubeMapPos; 先利用线和包围盒求交点，从包围盒位置到交点的向量就是Lp，然后利用Lp去采样Cubemap用于着色。 float shadow = texCUBE(cubemap, Lp).a; 另外背面要特殊处理下，防止阴影穿透问题。 if (dot(L,N) < 0) shadow = 0.0; shadow *= max(dot(L, N), 0.0); 2. 软阴影： 阴影平滑的过程比较有趣，首先Cubemap过滤方式选择tri-linear filtering，然后计算vertex-to-intersection-point（顶点到交点）向量的长度，然后乘以外部传入系数： float texLod = length(IntersectPositionWS - V); texLod *= distanceCoefficient; 为了平滑阴影，我们用texCUBElod 去采样Cubemap，其中UV的XYZ来自Lp，W来自vertex-to-intersection-point（顶点到交点）的距离。 Lp.w = texLod; shadow = texCUBElod(cubemap, Lp).a; 下图也可以看到离窗户越远处的阴影越模糊。 这种阴影比较适合室内环境、点光源位置不变、内部有移动物体的情…

Lua性能优化—Lua内存优化
type: item
date: 2017-04-27T12:06:00+03:00
description: 笔者在心动网络负责《仙境传说RO：守护永恒的爱》的性能优化，今天他将为大家分享Lua性能优化方面的一些经验，主要为拿到原始数据后，如何处理过滤数据、信息的经验，从而更快更准确地定位问题。 导语：大家好，我是舒航，现任职于心动网络，主要负责《仙境传说RO：守护永恒的爱》（下简称"RO"）的优化工作。今天主要想和大家分享一下，我这段时间在Lua性能优化方面的一些经验。 现在市面上大部分Unity游戏都能支持热更，主要热更Lua和一些资源。而Lua主要实现一些UI界面之类的非核心逻辑，这样虽然Lua这部分代码非常灵活，增加功能、修复BUG都非常方便。但是由于Lua实现的大部分都是UI，那么Lua热更出去的功能最多就是一些新UI界面罢了，或者用资源配合配置表来扩充下游戏既有内容。如果想做个特殊的玩法势必受到掣肘，一般最后都会发现导出的接口不全，非得打整包才行。为了使得RO的玩法更灵活，热更的游戏内容更丰富，其实就是为了满足策划们的各种需求啦，RO战斗逻辑的主体都是在Lua中完成。所以RO相对于其他的游戏，对Lua代码的性能要求会更高一些。 在我对Lua代码进行性能优化的时候，主要分为两部分： 1.内存优化 常驻内存优化 内存分配优化 内存泄露优化 2.CPU优化 这篇文章主要是和大家分享第一点，在内存优化上的一些经验和方法。在我进行Lua优化之前，我们已经请过UWA团队进行过深度的优化了，而且收效很好，在此给UWA团队赞一个。那么，面对一个已经进行过一次彻底优化的项目，要进行更深入的优化之前首先要问自己一个问题： Q： 我为什么能比前人优化得更彻底，做到前人没有做到得事情呢？ 总的来说就是两点：1）工具更先进，2）项目更熟悉。 所以在这样的策略下，我主要是针对我们的项目编写了两个工具：LuaMemoryMonitor和LuaProfiler。一个用于优化内存泄露和内存分布，一个用于优化内存分配。有了更好的工具之后，再针对性地优化自己项目的代码，无论是效率还是结果都非常好。LuaMemoryMonitor能在十几分钟内就定位到泄露的代码，用这个工具我们一下午就查清了战斗中的内存泄露，并且能清晰地列出Lua的内存分布。而LuaProfiler帮助我们把Lua内存分配速度降低到了原来的40-50%。 LuaMemoryMonitor LuaMemoryMonitor主要由两部分组成： C库Snapshot 和 UnityEditor ，下面我们分别说明。 一、C库的实现细节 C库的主要工作有： 快照_G 计算Lua对象内存大小 储存快照 1.快照_G Lua中可回收的对象递归关系如图： 遍历_G时按这个结构关系进行遍历，只需要编写5个函数traverse_object、traverse_table、traverse_function、traverse_userdata、traverse_thread，然后从traverse_table(_G)开始递归即可。在统计内存大小时，即要统计对象本身占用的内存，也要递归地遍历它所引用的所有对象占用的内存。其中string是Lua内部管理的，统计时要注意同一字符串的不同引用不能重复统计内存。 2.计算Lua对象内存大小 对Lua当前内存进行快照时，我主要是快照_G，如果有需要的话可以快照Registry。如果仅仅是查内存泄露，那么不统计_G内每一Entry的内存大小也可以。但是我为了分析内存分布，那么必须得统计每一Entry的内存大小。统计Lua对象的内存大小是没有现成的接口的，我这里给大家提供一种思路，也是Lua源码中统计对象内存大小的办法。例如一个Table占用的内存大小为： 3.储存快照 由于要对Lua内存进行分析，那么就不能把快照又存到Lua内，这样很容易干扰自己的数据采集。这也是把快照的代码写在C里的一个重要原因，如果是在Lua里写的话，就必须要小心翼翼地处理这些数据了。而写在C里就很好解决了，我在C里用一颗多节点树来储存每一个快照，多个快照组成一个链表。 二、Unity Editor 1.Editor特性 Snapshot库已经为编辑器准备好了所有数据了，现在只需要想一个好主意、好方法来利用这些数据。这里我做了一个特别的功能，可以很高效地利用这些数据。在UnityEditor里可以对每个快照进行逻辑操作。 求交集：即两个或多个快照中都存在的对象，即这些快照中的常驻内存。 求补集：A在B中的补集指在A中但不在B中的对象，即A相对于B的增量。 简单例子：在刚进入战斗时采样得到A快照，战斗一段时间后采样得到B快照，离开战斗场景回到主城，手动GC后采样得到C快照。求AB的补集得到一个新的快照D，D即是战斗期间新增的内存。求AC的补集得到快照E，E即是战斗期间新增并且离开战斗GC后没有释放掉的内存 。…

用好Lua+Unity，让性能飞起来—LuaJIT性能坑详解
type: item
date: 2017-04-23T17:48:00+03:00
description: 导语：大家都知道LuaJIT比原生Lua快，快在JIT这三个字上。但实际情况是，LuaJIT的行为十分复杂。尤其JIT并不是一个简单的把代码翻译成机器码的机制，背后有很多会影响性能的因素存在，下面笔者将带大家一一说明。 这是侑虎科技第231篇原创文章，感谢作者招文勇供稿，欢迎转发分享，未经作者授权请勿转载。当然，如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者博客：http://www.cnblogs.com/zwywilliam。同时，作者也是U Sparkle活动参与者哦，UWA欢迎更多开发朋友加入 U Sparkle开发者计划，这个舞台有你更精彩！ 一、LuaJIT分为JIT模式和Interpreter模式，首先要弄清楚你使用的模式 同样的代码，在PC下可能以不足1ms的速度完成，而到了iOS却需要几十ms，是因为PC的CPU更好？是，但要知道顶级iOS设备的CPU单核性能已经是PC级，几十甚至百倍的差距显然不在这里。 这里要了解LuaJIT的两种运行模式：JIT、Interpreter JIT模式：这是LuaJIT高效所在，简单地说就是直接将代码编译成机器码级别执行，效率大大提升（事实上这个机制没有说的那么简单，下面会提到）。然而不幸的是这个模式在iOS下是无法开启的，因为iOS为了安全，从系统设计上禁止了用户进程自行申请有执行权限的内存空间，因此你没有办法在运行时编译出一段代码到内存然后执行，所以JIT模式在iOS以及其他有权限管制的平台（例如PS4，XBox）都不能使用。 Interpreter模式：那么没有JIT的时候怎么办呢？还有一个Interpreter模式。事实上这个模式跟原生Lua的原理是一样的，就是并不直接编译成机器码，而是编译成中间态的字节码（bytecode），然后每执行下一条字节码指令，都相当于switch到一个对应的function中执行，相比之下当然比JIT慢。但好处是这个模式不需要运行时生成可执行机器码（字节码是不需要申请可执行内存空间的），所以任何平台任何时候都能用，跟原生Lua一样。这个模式可以运行在任何LuaJIT已经支持的平台，而且你可以手动关闭JIT，强制运行在Interpreter模式下。 我们经常说的将Lua编译成bytecode可以防止破解，这个bytecode是Interpreter模式的bytecode，并不是JIT编译出的机器码（事实上还有一个在bytecode向机器码转换过程中的中间码SSA IR，有兴趣可以看LuaJIT官方WIKI），比较坑的是可供32位版本和64位版本执行的bytecode还不一样，这样才有了著名的2.0.x版本在iOS加密不能的坑。 二、JIT模式一定更快？不一定！ iOS不能用JIT，那么安卓下应该就可以飞起来用了吧？用脚本语言获得飞一般的性能，让我大红米也能对杠iPhone！然而，并不是安卓不能开启JIT，而是JIT的行为极其复杂，对平台高度依赖，导致它在以arm为主的安卓平台下，未必能发挥出在PC上的威力，要知道LuaJIT最初只是考虑PC平台的。 首先我们要知道，JIT到底怎么运作的。LuaJIT使用了一个很特殊的机制（也是其大坑），叫做trace compiler的方式，来将代码进行JIT编译的。什么意思呢？它不是简单的像C++编译器那样直接把整套代码翻译成机器码就完事了，因为这么做有三个问题： 编译时间长，这点比较好理解。 更关键的是，作为动态语言，难以优化。例如对于一个function foo(a)，这个a到底是什么类型，并不知道，对这个a的任何操作，都要检查类型，然后根据类型做相应处理，哪怕就是一个简单的a+b都必须这样（a和b完全有可能是两个表，实现的__add元方法），实际上跟Interpreter模式就没什么区别了，根本起不到高效运行的作用； 很多动态类型无法提前知道类型信息，也就很难做链接（知道某个function的地址、知道某个成员变量的地址）。 那怎么办呢？这个解决方案可以另写一篇文章了。这里只是简单说一下LuaJIT采用的trace compiler方案：首先所有的Lua都会被编译成bytecode，在Interpreter模式下执行，当Interpreter发现某段代码经常被执行，比如for循环代码（是的，大部分性能瓶颈其实都跟循环有关），那么LuaJIT会开启一个记录模式，记录这段代码实际运行每一步的细节（比如里头的变量是什么类型，猜测是数值还是table）。有了这些信息，LuaJIT就可以做优化了：如果a+b发现就是两个数字相加，那就可以优化成数值相加；如果a.xxx就是访问a下面某个固定的字段，那就可以优化成固定的内存访问，不用再走表查询。最后就可以将这段经常执行…

动画重定向技术分析及其在Unity中的应用
type: item
date: 2017-04-13T04:46:00+03:00
description: 动画重定向技术是一种时间换空间的思路，在运行时将一套动画根据预计算好的骨骼差异信息，得到目标模型骨架上可以适用的动画数据，就可以生成目标角色的受击动画效果。这种方式既可以减少美术的工作量，又可以增加游戏对策划需求和修改的灵活度。 前言 笔者新的手游项目使用Unity引擎，动画部分要使用重定向技术来实现动画复用。笔者之前在大公司工作的时候对这块了解比较深入，读过Havok引擎在这部分的实现源码，并基于自己的理解，在公司自研的手游引擎中实现了一个简化的版本。本文将从原理层面分析Animation Retargeting技术的实现方式，然后落回到Unity引擎中，记录笔者使用Retargeting时遇到的问题和解决方法。 壹 · 什么是动画重定向 动画重定向，即Animation Retargeting，是一种动画复用的技术。直观上，你可以把它的作用想象成周星驰电影《西游-降魔篇》里的“听话符”——“我做什么，你就跟我做什么”。 听话符 这里给出一个非官方的定义： Retargeting is just the process of “copying” the animation from one skeleton to the other. 简单来说，重定向技术就是一种将动画数据从一个骨架拷贝到另外一个骨架的过程，只是这个所谓的“拷贝”过程，没有Ctrl+C、Ctrl+V这么简单。其实现的方式，也会影响到动画应用到另外一个骨架之后的效果。 贰 · 为什么要使用动画重定向 动画重定向技术在很多地方已经得到很广泛的应用，比如目前3A级主机游戏常用的动作捕捉技术就是基于这个原理来实现的——将真实人物的动作通过图像识别等技术生成动画信息应用到虚拟的角色身上，保存成动画数据。还有Max和Maya这样的建模工具中，也都已经集成了这一技术，用来方便美术把一个动作文件复用到其他骨架上。 那什么类型的游戏开发需要这样的技术呢？我们可以假想，如果要开发类似《街霸》这样的3D格斗游戏，其中有几十个不同体型、姿态各异的角色，战斗过程中会有很多抓技和投技的动作表现，对于这一类型的技能，不同角色的受击动作应该是一致的。 如果使用最常规的制作方法，需要对每一个角色制作攻击动作对应的受击动画，或者需要限制策划设计的受击表现在使用固定姿势、挂接、特效等方式可以实现的范围内。而不甘平庸的策划同学往往需要更加复杂的受击表现，常规的制作方法带来的问题可能有如下三种： 美术工作量较大。 如果有n个不同角色的骨架，有m个需要特殊表现的技能，那么只考虑受击动画就需要m*n个，这个数量级通常会在几千个动作的级别。美术需要制作、导出这些动画文件，需要非常大的工作量。 动画维护困难。 想象下，策划设计了一个需要特殊动作表现的技能，美术加班加点制作出了所有英雄的受击动画，第二天黑着眼圈来上班，策划同学满脸讪笑地走过来说——“哎呀，你看，这个受击动作的第15帧有点不自然，力量感不够强，要你不优化一下？”……每一处修改都需要美术手动同步到其他动作文件上，重新调整导出，这一过程太费时费力。 游戏发布打包文件过大。 动画文件多必然带来这一问题，尤其是在手游中，包体大小通常是安卓渠道发行的一个需要考虑的重要指标。 动画重定向技术就是一种时间换空间的思路，在运行时将一套动画根据预计算好的骨骼差异信息，得到目标模型骨架上可以适用的动画数据，就可以生成目标角色的受击动画效果。这种方式既可以减少美术的工作量，又可以增加游戏对策划需求和修改的灵活度，将需要维护和打包的动画数量从m*n这样的平方级别降低到m个。当然，软件工程中“没有银弹”的定律在这里依旧生效，重定向之后的动画可能由于体型差异等问题在某些情况下无法完全满足策划或者美术的要求，这就需要一些额外的方法或者重新制作部分动画来进行弥补，这部分放在后面进行详述。 叁 · 基本原理 写到这里，其实之前有一点没有说明的是动画重定向技术主要是针对骨骼动画的方案，由骨骼来描述动作信息，用蒙皮来表示模型网格与骨骼之间的关系，从而得到模型最终的样子。对于这块不熟悉的读者可以自己查阅一下相关的资料。 骨骼动画 动画重定向的过程主要是针对骨骼信息，对于蒙皮过程没有任何影响。而动画信息，可以理解为每一帧中所有骨骼数据信息的集合。我们可以这样更加形象地理解：动画就是每一帧为模型制作一个Pose（姿势），在每帧之间的姿势可以通过差值获得。 为了更加清楚地描述动画重定向地原理，我们截取某一帧的姿势来进行分析——在解决了每一帧的姿势重定向之后，整个动画的重定向，也就是在每一帧都进行姿势的重定向即可。 假设有两个模型，它们的骨架分别是A和B，我们拥有A骨架对应的动画数据，想把这个动画数据应用在B骨架上。直接应用是否可以？当A骨架和B骨架完全一样的时候，直接应用是可以的，但通常动…

VR交互动画短片《拾梦老人》的开发经历
type: item
date: 2017-04-10T10:59:00+03:00
description: 作为VR交互动画短片《拾梦老人》的开发者，笔者从引擎选取、技术创新、填坑之路以及后期展望等多个角度，为我们细细道来了他们的开发故事。 笔者去年写了一篇关于如何打造高逼格耀斑效果的文章，很荣幸得到大家的关注。本人平日乐于分享自己工作中的点滴技术心得，欢迎大家联系我进行技术相关的交流学习。 今年笔者在PintaStudio，这是我在中科院的同学创立的一家VR电影工作室，我们希望用游戏引擎实时渲染的方式来表达艺术家的思想。同时，我们制作的VR沉浸式感受也是一种全新的观影体验。接下来，我将简述整个开发的历程。 VR交互动画短片《拾梦老人》剧照 一、引擎的选择 （Unity VS UE） 立项阶段，我们遇到了引擎选择上的分歧。我们想做的是重量级大作，所以最初的想法是采用一个重量级引擎。早期做端游时，我们了解得最多的是CRY ENGINE（下称“CE”）和UNREAL ENGINE（下称“UE”）了，这两个引擎就是引擎界的“西门吹雪”和“叶孤城”，长期霸占着国外的大作市场。CE这些年在国内推广不利，所以在最初阶段，更多的小伙伴希望用UE来做我们这个电影。我的加入，给团队带来了新的选择—Unity。相比前两个引擎，Unity可以说是后起之秀，尤其近3年手游的兴起，才把Unity引入大众的视线。我们主要对UE和Unity两个引擎进行了比较： 在视觉效果上，UE的完成度更高。引擎自带了比较多的效果如Light Propagation、各种Volume效果；UE的实时光照用的是经典PBR光照模型，更加接近艺术家们要求的Maya效果；UE的ShaderTree，更是给人一种“屌丝图形码工可以退休”的感觉。反观Unity，自带的效果比较有限(不考虑庞大逆天的AssetStore情况下)，而且引擎中增加了一些无端端的优化，造成了一些视觉上的Bug，遭到一些艺术家的诟病。 性能上，UE没有对所有模块做全部测试，但是Unity经历了90%以上手游的考验，相信可以保证影片的流畅性；同时因为有更多的移动开发案例，所以小胜UE。 操作上，UE更强大的功能必然会导致编译时间较长；对于喜欢改装引擎的老司机，修改UE代码需要对它所有的模块都熟知；加上是基于C++的开发，程序需要格外小心内存、指针等各种问题。再看Unity，Unity把一些固定代码做了封装，改装引擎最多只能是对某些模块进行重写(当然，这样改装的结果可能导致性能略低，但是对于我们项目而言，不涉及过多硬件交互)，不太容易出现问题；Unity是基于C#的开发，程序也相对耐操一些，对于支持VR的PC硬件，内存不是最致命的问题，但怕GC太多。 综上所述，我建议团队采用Unity引擎。尽管后来被几个大坑坑得鼻青脸肿，但是从项目进度以及结果来看，这个引擎基本上达到了我们想要的效果。 二、技术创新 Unity引擎本身的画质相对来说是较为简朴的，但是对于电影来说，艺术家希望能有一些更丰富的画面表现。我之前在公司运营团队写的一篇文章里也简单提过，大家可以参考： https://mp.weixin.qq.com/s/TozfSpORF8mb89pao-t4rg 在这里我们举个简单小例子: 如上图这个光影效果，上面窗户衍射效果就有很多种实现方法。先对Depthmap做边缘的blur，然后计算光照是一种办法；直接对Render Target做局部的bloom也是一种方法；OffScreenRenderTarget，渲染一张低分辨率的光照贴图，然后合成回MainTarget，也不失为一种好方法。 对于地上阴影的边缘效果，怎么实现呢？CookieTexture？错误，你没有办法用CookeTexture去解决不规则的阴影衍射，更何况阴影的边缘颜色是艺术家幻想的。那怎么办？其实不难，它是软阴影算法的变种，所以，利用概率采样ShadowMap的方法得到阴影边缘，然后在边缘地区做颜色叠加即可实现。 我们再举一个小例子： 这狗的耳朵是不是有点透明？其实这就是个SSS效果，实现方法是：对于一个表面，沿着视线方向做一个延伸，取到次表面的光照，再叠加到前表面的颜色上。仔细看Unity树叶的Shader（Unity内置Terrain里的Shader），有个变量backcontrib，是把树叶背面的颜色叠到前面来，其实也是sss的变种。 三、重重阻碍 我们项目的开发进程充满艰辛，在此列举部分，给可能要入坑的同学提个醒： 1. 普通物件Standard材质 因为进度原因，我们仍然采用了Unity自带Standard里的PBR光照模型，这个光照模型是经典模型的简化版，对于大多数渲染对象来说还是可行的。但Unity自带的StandardShader在支持半透物件时，就会出现瘫痪的情况。如下图： 左图为正确效果，右图为UnityStandard的错误效果。这…

GPU Skinning 加速骨骼动画
type: item
date: 2017-04-02T05:51:00+03:00
description: 场景中有很多人物动画模型的时候，性能会产生大量开销，其中很大一部分来自于骨骼动画。此文推荐的方法是将CPU中的蒙皮工作转移到GPU中进行，从而能够较大地提升多角色场景的运行效率，在大规模群体动画模拟如MMO、RTS等游戏中有较大的应用性。 一、起因 我们知道，场景中有很多人物动画模型的时候，性能会产生大量开销。这些开销除了 Draw Call 外，很大一部分来自于骨骼动画。Unity 内置了 GPU Skinning 功能，但笔者测试下来并没有对整体性能有任何提升，反而增加了不少。有很多种方法来减小骨骼动画的开销，每一种方法都有其利弊，都不是万金油，这里介绍的方法同样如此。其实本质还是由我们自己来实现 GPU Skinning，只是和 Unity 内置的 GPU Skinning 有所区别。 使用了 ShadowGun中的角色模型 开启 Unity 内置的 GPU Skinning 从上图中可以看到，Unity 调用到了OpenGL ES 的 Transform Feedback 接口，这个接口至少要在 OpenGL ES 3.0 中才有。笔者理解的 Transform Feedback，就是将大批的数据传递给 Vertex Shader，将 GPU 计算过后的结果通过一个 Buffer Object 返回到 CPU 中，CPU 再从 Buffer Object 读取数据（或直接将 Buffer Object 传递给下一步），在随后步骤中使用。显然，在骨骼动画中，TransformFeedback 负责骨骼变换，Unity 将变换后的结果拿来再进行 GPU 蒙皮操作。 这次我们要动手实现的就是这个过程，但是不使用 Transform Feedback，因为要保证在 OpenGL ES 2.0 上也能良好运行，况且Unity引擎也没有提供这么底层的接口。 大致的步骤如下： 将骨骼动画数据序列化到自定义的数据结构中。这么做是因为这样能完全摆脱 Animation 的束缚，并且可以做到 Optimize GameObjects（Unity 中一个功能，在不丢失绑点的情况下将骨骼的层级结构 GameObjects 完全去掉，减少开销）； 在 CPU 中进行骨骼变换； 将骨骼变换的结果传递给 GPU，进行蒙皮。 很简单的三大步骤，对于传统的骨骼动画来说没有任何特殊情况，下面我会对其中的每一步展开说明，并将其中的细节描述清楚。 二、实现 提取骨骼动画数据 Unity 中的 Animation 数据 这个步骤的目的就是将这些数据提取出来，存储到自定义的数据结构中。代码大致如下： 其中有两个注意点。第一，要清楚 AnimationCurve 中提取出来的旋转量是欧拉角还是四元数。这里我一开始就弄错了，想当然认为是欧拉角，所以随后计算得到的结果也就错了。第二，用来旋转的四元数，必须是单位四元数（模是1），否则你会得到 Unity 的一个报错信息。 以上的代码中，我将每一帧的数据以 30fps 的频率直接采样了出来，其实也可以不采样出来，而是等需要的时候再从 AnimationCurve 中采样，这样会更平滑但是运行时的计算量也更多了。 骨骼变换 骨骼变换是所有代码的核心部分了，看似挺复杂，其实想清楚后代码量是最少的: 简单来说骨骼变换就是一个矩阵乘法，比如 bone0（简写为b0） 是 bone1（简写为b1）的父骨骼： 注意这里是矩阵左乘（从右往左读），trs 是 Matrix4x4.TRS，也就是从 AnimationCurve 采样到的数据。 Bindpose 的作用是将模型空间中的顶点坐标变换到骨骼空间中（是骨骼矩阵的逆矩阵），然后应用当前骨骼的变换，沿着层级关系一层层地变换下去。 蒙皮 蒙皮CPU部分的代码如下： 由于骨骼数量固定为 24，所以图中的 96 = 24 x 4 使用 SetMatrixArray 其实有点浪费了，因为对于一个 4x4 的矩阵（四个 float4 ）来说，最后一维永远是 （0, 0, 0, 1），所以可以使用 3x4 的矩阵（三个float4）代替，这样就减少了数据传递的压力。 现在所有的骨骼变换矩阵已经传递到 Shader 中了，就可以使用这些数据来进行蒙皮（变换顶点坐标）。 三、改进 此时所有角色的动作都是同步的。接下来进行改进，不再使用 uniform array 的方式来传递数据，而是将骨骼动画数据存储到纹理中，并加以一定的差异化，避免所有角色的动作完全同步的问题。在运行的最开始，将所有帧的动画数据存储到纹理中，代码如下： Shader中的蒙皮代码相应变为： 以上就是笔者实现 GPU Skinning 的细节。但没有一种方法是完美的，作为能够减少骨骼动画开销的备选方案之一，在恰当的情况下使用会大大地…

Unity 5.x AssetBundle零冗余解决方案
type: item
date: 2017-03-01T09:21:00+02:00
description: 最近笔者参考Unity官方介绍资源管理机制和Unity序列化的一些文章做了下AssetBundle打包相关的优化，成功实现了零冗余打包，下面和大家分享一下实现的过程。 一、问题概述 相比Unity 4.x版本里AssetBundle（以下简称ab）打包复杂的API，Unity 5.x版本里提供了更加人性化的依赖自动管理机制——对指定打包的资源，Unity会自动收集并分析其依赖的资源，如果该资源依赖的某个资源没有被显式指定打包到ab中，就将其依赖的这个资源打包进该资源所在的ab里；如果已经被指定打包进其他ab里，那么这两个ab之间就会构成依赖关系，加载ab时，先加载其依赖的ab即可。 这一套依赖管理机制使用方便的同时也会带来一个问题：如果两个ab A和B中的一些资源都依赖了一个没有被指定要打包的资源C，那么C就会同时被打进ab A和B中，造成资源的冗余，增大ab和安装包的体积。而这个被A，B依赖的资源C又可以分为两种类型，一种是Assets下外部导入的资源，即开发者导入或创建的资源；另一种则是Unity内置的资源，例如内置的Shader，Default-Material和UGUI一些组件如Image用的一些纹理资源等等。因此要解决资源冗余的问题，就要分别对这两种被依赖的资源进行处理。下图是我们的一个项目优化前使用UWA提供的资源检测工具检测到的资源冗余情况，可以发现问题较为严重： 二、处理被依赖的外部资源 对于没有被指定打包的外部资源，如果多个ab包依赖了它，打包时该资源就会被多次打包进依赖它的ab包中，造成冗余。解决方案就是将这些被多个ab包依赖的资源打包到一个公共ab包中。处理过程如下： 使用EditorUtility.CollectDependencies()得到ab依赖的所有资源的路径； 统计资源被所有ab引用的次数，将被多个ab引用的资源打包为公共ab包。 需要注意的地方： EditorUtility.CollectDependencies()收集到的资源包含了脚本，dll和编辑器资源，这些资源无需打进ab中。 更详细的步骤可以参考这篇文章：如何优化AssetBundle资源。 至此，再次进行检测我们可以发现冗余的资源数大大减少，冗余的外部资源已经被消灭，只剩下一些冗余的Unity内置资源，接下来我们再来处理这些冗余的内置资源。 三、处理被依赖的内部资源 经过上面的处理，ab中为什么还会有冗余的内置资源？我们来看上面获得ab依赖资源的过程，对于外部资源，可以使用AssetDataBase.GetAssetPath()获得该资源在Assets下的路径；但是对于内置资源，获取的路径却始终是"Resources/unity_builtin_extra"（后面会提到另外一种情况），unity_builtin_extra文件在Assets下并不可见，但会出现在安装包Data/Resources下，可以猜测unity_builtin_extra是Unity内部使用的一个资源库，类似于ab，但无法像对待外置资源那样对其进行打包。因此解决问题的关键就是修改这些资源对内部资源的引用，让其引用外部的资源。 第一种方案是不使用内置资源，该方案理论上可行实际上几乎不可行，例如一些Shader的FallBack是Leagcy VertexLit，使用了该Shader的资源会依赖Leagcy VertexLit（Shader也会依赖FallBack Shader）；Unity自带的3D Object默认使用了Default-Material材质，粒子系统则默认使用了Default-Particle，UGUI一些则组件会用到Background这些内置Sprite等等。 第二种方案则是提取出Unity内置的资源，在打ab前进行预处理，修改引用了内置资源的资源使其引用提取出的那一份资源，接着按处理外部资源的流程进行打包，这样被多次引用的“内置资源”（这里指的是提取出的那一份内置资源）可以被正确的打入ab包，打包完成后再将之前步骤修改过的引用还原即可。这样即不影响项目的正常开发，也从根本上避免了内置资源的冗余。该方案地实现步骤如下： 1. 提取出Unity内置的资源 使用AssetDataBase.LoadAllAssetsAtPath() 可以加载出"Resources/unity_builtin_extra"下所有的Object，可以发现共有4种类型的资源：Shader，Material，Texture以及Sprite。对于内置Shader，我们可以直接从Unity官方网站下载；而对于后三种，可以通过AssetDataBase提供的相关API来进行创建： Object[] UnityAssets = AssetDatabase.LoadAllAss…

Unity手游iOS内存分析和测试
type: item
date: 2017-02-09T05:06:00+02:00
description: 内存是Unity手游的硬伤，如果没有做好内存的相关管理和详细的测试，游戏极有可能出现卡顿、闪退等影响用户体验的现象。在此，笔者为我们介绍了一些Unity手游内存分析和测试过程中比较实用的测试场景案例、分析方法和解决方案等。 内存是Unity手游的硬伤，如果没有做好内存的相关管理和详细的测试，游戏极有可能出现卡顿、闪退等影响用户体验的现象。在此，笔者为我们介绍了一些Unity手游内存分析和测试过程中比较实用的测试场景案例、分析方法和解决方案等。由于Android和iOS分析思路比较类似，在这里我们仅拿iOS内存分析和测试为例，供大家参考。 测试目标 保证1G内存的iOS设备（iPhone 6、iPhone 6 Plus等）可在长时间持续游戏不闪退，同时优化已知的内存占用过高问题。 测试分析 由于已知游戏内存占用过高，针对这一问题，要搭建测试环境，模拟玩家的实际操作以及外网的实际情况，测试内存的使用情况。一般分为基础测试、压力测试和其他测试。 基础测试，即各种基本操作测试，比如场景切换操作、UI界面操作等； 压力测试，即多角色操作测试，比如角色的加载、角色的复杂程度、技能和动作的释放、上线下线操作等等，比如模拟多角色野外场景压力测试等； 其他测试，比如Xcode的相关工具使用分析等； 在测试中统计各操作对内存增长的贡献，分析可能的原因，并基于测试结果提出合理的优化方案。 测试思路 ◆◆◆ 基础测试 对于基础测试这一部分，可做的测试较多，我们仅拿UI界面操作测试和场景切换测试为例。 1. UI界面操作测试 主角进入指定场景的固定位置，尽量重复操作所有能操作的UI界面，采样对比前后的内存增长。 测试结果： 对比两次采样结果，单纯的界面操作行为，内存总共增长了140M，其中增长最多的是UITexture，剩余的主要是AnimationClip、AnimatorController和其他Texture等。 测试结果分析： 为何UITexture界面贴图会占用这么多的内存呢？ 经分析，为了解决Android平台下打开界面响应过慢的问题，我们对UI界面采用了缓存机制。但是在iOS平台下，首先iPhone的反应速度要优于Android；其次iOS平台（如iPhone 6内存只有1G的设备）对应用程序的内存使用占比控制比Android平台更为严格，超过一定阀值系统会自动结束该进程。因此在iOS平台下，速度与内存要兼顾，尽可能节约对内存的使用。 针对这个问题可以修改界面缓存策略： 根据界面优先级、重要性进行划分，择优缓存； 部分大图集拆小、允许少量小贴图元素冗余； 在后续的优化中，程序修改了界面缓存策略，单纯UITexture方面就减少了约80M！如果你的项目也有这方面的需求，非常值得一试哦！ 2. 场景切换测试 指定A场景的一个固定位置，主角从A场景切换到B场景，然后从B场景切换到A场景，重复切换多次后，最终回到A场景的固定位置，采样对比前后的内存增长。 测试结果： 对比场景切换前后的内存使用情况，发现无明显的变化。 测试结果分析： 场景切换前后内存无增长，基本保证了切换场景逻辑无内存泄露。反之，在测试中如果内存增加较多，则需要查看代码逻辑修复问题。 ◆◆◆ 压力测试 对于压力测试这一部分，由于情况比较多，操作比较复杂，所以要仔细测试。 为何要模拟外网真实环境进行测试呢？ 首先就是可能无外网真实环境可用，这个没有办法，只能模拟；其次是外网真实环境过于复杂不利于有效的分析；再次就是外网真实环境不能随时重现利用，并长时间供测试人员分析，所以要尽量去模拟外网环境。 我们就以模拟多角色野外场景压力测试为例，进行迭代优化。由于需要模拟多人PK的情景，所以我们自己制作了机器人来方便定位并分析多人情况下的性能和内存问题。 1. 模拟多角色野外场景压力测试 模拟测试场景一： 选中一个野外场景A，主角和大量随机门派、随机外观的机器人进入场景，随机释放技能，然后机器人全部下线。分不同情况采集数据对比内存增长，在机器人全部下线后，主角切换到场景B，再切回测试场景A的原始位置，采集数据对比内存增长。 测试结果： 机器人上线后各种不同场景下的内容变化情况如下： 机器人静止时，内存对比空场景，增长97.5M； 机器人释放技能平稳后，内存对比空场景，增长119.4M； 机器人全部下线，内存对比空场景，依然增长98.3M； 机器人全部下线后，主角切换场景，再切回测试场景，内存对比空场景，基本无增长。 测试结果分析： 通过上述的测试及结果,结果分析及可获得的信息如下： 机器人全部下线后，由于大部分资源进入缓存池，故内存有增长是正常的； 机器人全部下线并切换场景后，对比空场景基本无增长，说明缓存池已全部卸载，故切换场景的缓存池释放逻辑正确、无泄漏； 角色相关的绝大部分资源保存至缓存…

Lua配置表存储优化方案
type: item
date: 2017-01-24T15:37:00+02:00
description: 近几年手游大热，Unity + Lua更是手游行业的标配技术。如何有效利用有限的硬件条件，实现当下高品质、高规格的游戏产品是持久不变的话题。本文将介绍一种消除配置文件中冗余数据，达到压缩和优化数据存储的方法。 ◆◆◆ 简单介绍 Lua作为一个极为精简的嵌入型脚本语言，已经广泛地用在了游戏业。Lua的存在一般是两种场合，一种用于实现游戏上层业务逻辑，一种则利用了Lua语言本身灵活简单的数据表达能力而被广大程序员用于数据的存储，也就是常说的配置文件。 一般来说配置文件的初始来源是策划维护着的、有着一定格式约束的Excel表格，经由程序员提供的导出工具，把Excel的表格数据导出成为游戏能直接读取使用的Lua源码。 这些源码文件以Lua table的形式存储与Excel等价的数据，通常可以简单把这个配置表看成是一组2维数组，转换成配置就是一个key（Excel第一列）对应一组子数据（Excel中一行），那么整个配置数据就是一个大表包含着若干小表，如下： 原始配置表： 转换成Lua后大致是这样： local mapsetting = { [1] = { name = "map1", useminimap = "map1000", show_name = "十里桃林", cam_pitch = 40, cam_yaw = 230, cam_dis = 10, pkflag_support = 1, pkflag_punish = 1, maxnum = 50, scheduleConf = {}, scriptId = {}, ... }, [2] = { name = "map2", useminimap = "map1000", show_name = "碧波潭", cam_pitch = 40, cam_yaw = 230, cam_dis = 10, pkflag_support = 1, pkflag_punish = 1, maxnum = 50, scheduleConf = {}, scriptId = {10002}, ... }, [3] = { name = "map3", useminimap = "map1000", show_name = "梵音谷", cam_pitch = 40, cam_yaw = 230, cam_dis = 10, pkflag_support = 1, pkflag_punish = 1, maxnum = 50, scheduleConf = {}, scriptId = {}, ... } ... } return mapsetting 如果配置文件中的数据过大，或着是有冗余的无用数据，那么势必会导致输出的Lua文件过大，这将严重影响加载速度和增大内存占用量。本文将介绍一种消除配置文件中冗余数据，达到压缩和优化数据存储的方法。 ◆◆◆ 优化思路 介绍该方法前，大家先来看看一般数据在哪些地方会有冗余。由上面的示例图我们可以清晰地看到数据的冗余点： 大量的数据是重复的，或着是代表没有意义的空值（比如0，[]等）； 大量的中文字符串是需要游戏后期做本地化处理的； 很多复合型数据（子表，数组）内容是一样的； 搞清楚了数据冗余的原因，我们就可以制定优化方案： 对于Excel中的一列，出现次数最多的值认定为默认值，然后把它从Lua表中剔除掉，然后利用metatable机制实现全局默认值存储； 对于中文字符串，替换为一个唯一的id标识，写回到Lua表中，读取的时候加上相应的查找替换； 对于一些复杂的子表或着数组，做唯一化替换处理，替换后写回到原始数据中； 能看出来，上面的操作其实做的都是唯一化处理，所以有个要求就是整个Lua表的数据必须是只读的，如果不满足以上条件，一切优化都是错误的。所以在最后我们需要把整个表改为只读，以保护数据不被错误篡改。 优化后变成了下面这样： local __rt_1 = { 0 } local __rt_2 = { } local __rt_3 = { 10004 } ... local __rt_34 = { desc = "@40313", drop_show = __rt_33, scriptId = { 10001, 10011 }, shield = 6, show_name = "@470882", subtype = 4 } ... --超出Lua局部变量个数，则需要另外单独存储和引用 --createtable是把c中创建表的函数导入到lua中，这样通过创建一个预分配 --的表，能有效的避免table re-hash，有效利用内存 --下面创建一个预分配919个数组array-part、0个元素的空hash-part local __rt = cr…