简介《爱》(图1a)是一款由上海独立游戏工作室麻辣马(Spicy Horse)制作、美商电艺(Electronic Arts)发行的惊悚动作冒险游戏。此全乃2000年发行的《爱丽丝惊魂记(American McGee’s Alice) PC》(图1b)的续作。图1(a): 《Alice: Madness Returns》Xbox360封面 (b): 《American McGee’s Alice》PC封面在为期超过两年的制作期间《爱》的制作团队最高达75人另外有50人左右的美术外包团队。《爱》的制作团队有许多不同国籍的成员但当中主要为华人。从制作地点及人员来说《爱》可以说是一个国产游戏。但从目前的环境来说《爱》应该不会在国内发行。《爱》使用Unreal Engine 3开发并使用了Scaleform、Kynapse和Bink中间件。在PC平台上合作伙伴nVidia加入了使用GPU加速PhysX效果。但游戏主角爱丽丝的头发和衣饰模拟并非使用PhysX而是一个自定义解决方案这也是本文将谈及的主要内容。有时候需求和技术就像是鸡和蛋的关系──因某需求而开发新技术或因某技术而产生新的需求。本人在《爱》的开发过程清楚体会到这个关系。让我细细回想当天的事……研究之始2009年8月23日(星期日)刚入职满三个星期了。这段时间的工作主要是按游戏策画的需求做了一些简单的游戏性编程(gameplay programming)例如是一些关卡内的机关这正好让我学习一下UnrealScript(Unreal引擎的脚本语言)。上周看到新版本的主角模型虽然比旧版本更精细但我看上去觉得还有改善空间于是分别和美术总监和动画总监讨论大家也认为现时对头发和衣饰以手工关键帧动画(keyframe)方法表现效果不够理想而且用Phong反射模型来渲染头发有点像塑料玩偶的感觉。从动画的工作来说头发和衣饰的关键帧动画要做得自然并不容易尤其动画间的混合(blending)更为困难不是动画不自然加减速就是会穿过身体。传统上许多游戏会避免把角色设计为长发也会避免穿着长裙。但爱丽丝无可避免要触犯此二禁忌。既然如此何不尝试进行突破并以此为游戏特色呢当天虽是周日我在上海孤单一人在炎夏就不外出了。脑里不断思考着上周工作上的事情。不过单单在想也没有用就直接敲键盘实验一些方案。最先想到的是常用于模拟绳子和布的弹簧质点系统(mass-spring system)记得以前看过相关的入门文章[1]就以该文的基础。弹簧质点系统所谓弹簧质点系统其实就是仿真一些有质量的粒子(质点)再在粒子之间加入一些无质量的虚拟弹簧。例如要模拟一条绳子最简单的方法是建立n个粒子再在每两个连续的粒子之间加入弹簧即有n-1个弹簧如图2。图2: 用5个粒子和4个弹簧模拟的绳子要模拟粒子运动可使用《用JavaScript玩转游戏物理(一)运动学模拟与粒子系统》一文中谈及的欧拉方法(Euler method)但[1]里介绍的Verlet数值积分在很多情况下是更好的选择。我使用了含简单阻尼效果的Verlet数值积分方程:当中是时间在t时粒子的位置为时步(timestep)的阻尼系数为时间在时作用于粒子的加速度(即当时作用于粒子的力除以其质量例如引力加速度)。Verlet方法分的计算简单不需保留或计算速度(velocity)也比欧拉稳定但缺点是时步()必须是固定的。Verlet积分的另一特点是可以简单地加入各种约束(constraint)例如某粒子在仿真之后其位置位于地面以下只需把粒子移至最近地面的点。对于绳子另一约束就是相邻粒子的距离在Verlet积分下此距离约束可以模拟弹簧。假设两个相邻粒子的位置为、两者间的止动长度(rest length)为则可以这样调节两粒子的位置此外要模拟头发必须避免头发移动至头颅及其他身体部分之内此仍碰撞检测(collision detection)和碰撞决议(collision resolution)。如前所述这部分也可以用约束来表示。由于头颅较接近球体在此简单测试中只加入一个球体去进行检测。此约束把球体内的粒子推至最近的球面上。要同时满足多个约束最简单的方法是松弛法(relaxation method)即进行多个迭代每次执行所有约束一次那么其结果就会就趋近合乎所有约束的解。当天写的测试程序其数据结构和伪代码表示如下:123456789101112131415161718192021222324252627282930313233// 节点(粒子)structNode {Vector3 p0, p1;// 前帧/本帧的位置floatlength;// 和上一节点的止动长度}// 发束structStrand {size_tnodeStart, nodeEnd;// 此发束中节点数组的起始和结束索引Vector3 rootP;// 发根的局部坐标(相对于头的变换)};SimulateHair(nodes, strands, sphere, damping, dt, headToWorld)// 对每个节点进行Verlet积分foreach n in nodesa Accumulating forceforn, divided by mass// 现时只是引力加速度常量p2 Verlet(n.p0, n.p1, damping, a, dt)n.p0 n.p1, n.p1 p2// 以新状态取代旧状态// 对每束发丝以松弛法进行约束求解foreach s in strandsfora number of iterationsforindex s.nodeStart to s.nodeEnd - 1na nodes[index]nb nodes[index 1]// 碰撞检测和决议nb.p1 collideSphere(sphere, nb.p1)// 长度约束na.p1, nb.p1 lengthConstraint(na.p1, nb.p1, nb.length)// 固定发根nodes[s.nodeStart].p1 transform(headToWorld, s.rootP)用程序产生一些发束并把模疑结果用直线线段渲染出来就做成图3的效果图3: 最初的头发实验程序中能使用鼠标旋转头颅表现暮然回首的飘逸也可改变引力方向表现风吹秀发的感觉。这个花了一天时间写的程序实验其实并不复杂至少比写这篇博文容易。把研究放进日程次日把成果带到公司向程序同事和动画同事演示决定把这个初步构思带到周三的定期技术会议。除了继续完成之前的工作也花了些时间搜集、阅读关于实时头发模拟及渲染的文献并把一些想法写到项目的wiki里。终于到了周三的定期技术会议把程序向项目组的主要决策者(包括制作人、创意总监、美术总监、技术总监等)演示并展示一些文献里的最终渲染效果。基本上反馈是正面的一些主要讨论重点大约如下:问使用程序化的头发相比手工动画有何好处答节省工作量而且效果应该会比手工更好。另外《爱》中有海底场景可使用阻尼等参数模拟水里的头发飘动效果在室外、天空上的场景也可以加入风的效果。问此技术会否很耗CPU/GPU时间答具体开销暂时未能确定。由于我们游戏在Xbox360/PS3上GPU应该会成为瓶颈所以可以考虑在Xbox360使用空闲的CPU核在PS3上使用SPU去进行头发模拟。若假设发束之间无互动关系还可以使用这种并行性作多核/多SPU并行加速。问三维美术方面如何去设定发型答最简单的方法是使用额外的骨头(bone)去设定发型那么就不用更改导出工具或编写特别的工具。模拟中可以加入额外的约束使发束自然回复至预设的发型。问为甚么其他市面上的游戏不用这种技术答……(当时真答不出来但也许这个问题也是我的重要得着详见后文)然后也讨论了预计所需的研发时间。最后决定可以继续第一阶段的研发以成果决定是否继续下一阶段。头发渲染获准研究除了我感到兴奋动画组同事也非常希望此技术能成功应用到游戏因为此技术会大大节省他们的工作量。因此他们也热心准备爱丽丝的测试用发型数据。为了更容易设定发型只要求建立一个头皮(sculp)的三角形网格并在网格上每个顶点上加入一串骨架以代表引导发束(guided strands)程序中按LOD细分(subdivide)头皮网格并以插值方式产生引导发束之间的发束。我更新了测试程序使用D3DX库去导入爱丽丝的白模及发型再把发型数据转化为仿真用的节点和发束数据结构。接着是先尝试渲染部分然后再改进模拟部分。我尝试过几种渲染方法。[2]中以线表(line list)去渲染发丝要表现稠密发丝所需的像素数目(primitive count)很大在目标平台上需要占用很多GPU时间。而另一种方法是把发束线段向屏幕空间展开形成固定宽度的三角形表(triangle list)或四边形表(quad list)。为了使用较少的节点而得到圆滑的发束我采用了均匀三次B样条(uniform cubic B-splines)把原来的发束插值为曲线(图4)之后才把该曲线展开为三角形表。插值的数量可以成为运行期动态LOD的参数。图4: 绿色线段为模拟结果橙色为三次B样条至于着色方面采用了较简单的Kajiya-Kay模型[3]。此模型基于切线(tangent)而非法线(normal)能表现出头发的高光(图5)。图5: 早期基于Kajiya-Kay反射模型的着色改进模拟之前的做法虽然能模拟出一条绳子但它的行为更像一条锁链因为它是完全柔软的而真实的绳子在止动时通常是直线的弯曲绳子需要施力。一个简单的实现方式是再加入弹簧(长度约束)连接相隔一个粒子的每对粒子(图6)。图6: 加入防止弯曲的长度约束(红色)要把发束回复至原来的发型方法是把目前节点位置向该节点的引导动位置(guided position即发型中设置的位置)施以归还力(restitution force)。经过实验测试发觉可以把接近发根的节点设定较大的归还力越接近发梢则归还力越弱。我简单地使用一个衰变的关系当中为发根的归还力为自发根起计的节点索引是衰变的速度。在碰撞方面只是从一个球体扩展至多个球体模拟更准确的头形以及对脖子、胸、肩、手臂的碰撞。Verlet方法使碰撞计算简单之时又真实可表现出发丝在肩上顺滑地流动。研发通常都不是一帆风顺的。当在程序中加入了移动模型的操控后发现在少量迭代的情况下发丝像弹簧般弹来弹去换句话说长度约束的收敛不够快。此问题是技术关键当时没找到好的现成办法苦恼多时。我试过不同的方法例如把约束的执行乱序化或是以不同分组方法进行长度约束但效果都不如理想。最后灵机一触想到既然碰撞这么简单、效果又好可以想象每个节点都被限制在一个球体之内球体中心为发根半径则是发根至该节点的止动长度之和如图7a所示。图7(a): 每个粒子限制在一个球体之内 (b): 不能满足长度约束的情形但仍然保持每个节点和发根的直线距离此法能有效地避免头发超出半径范围但不能控制如图7(b)的情况。从实验得知后者其实不太显眼只要不做成弹簧伸缩的感觉视觉上很难察觉出问题。效能测试