1. 程式人生 > >愛麗絲的髮絲──《愛麗絲驚魂記:瘋狂再臨》製作點滴

愛麗絲的髮絲──《愛麗絲驚魂記:瘋狂再臨》製作點滴

今天(2011年6月14日)是《愛麗絲驚魂記:瘋狂再臨 (Alice: Madness Returns) Xbox360/PlayStation3/PC》(下簡稱《愛》)正式發售日,身為其開發程式設計師之一,特撰此文以作紀念。

簡介

《愛》(圖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開發,並使用了ScaleformKynapseBink中介軟體。在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),即進行多個迭代,每次執行所有約束一次,那麼其結果就會就趨近合乎所有約束的解。當天寫的測試程式,其資料結構和偽程式碼表示如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // 節點(粒子) struct Node { Vector3 p0, p1; // 前幀/本幀的位置 float length; // 和上一節點的止動長度 } // 髮束 struct Strand { size_t nodeStart, nodeEnd; // 此髮束中,節點陣列的起始和結束索引 Vector3 rootP; // 髮根的區域性座標(相對於頭的變換) }; SimulateHair(nodes, strands, sphere, damping, dt, headToWorld) // 對每個節點進行Verlet積分 for each n in nodes a = Accumulating force for n, divided by mass // 現時只是引力加速度常量 p2 = Verlet(n.p0, n.p1, damping, a, dt) n.p0 = n.p1, n.p1 = p2 // 以新狀態取代舊狀態 // 對每束髮絲以鬆弛法進行約束求解 for each s in strands for a number of iterations for index = s.nodeStart to s.nodeEnd - 1 na = 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)的情況。從實驗得知,後者其實不太顯眼,只要不做成彈簧伸縮的感覺,視覺上很難察覺出問題。

效能測試

至2009年8月30日(星期日),在Windows上實現的基本頭髮模擬和渲染實驗已經完成,程式碼亦使用了XNA Math做SIMD向量優化。很興奮地把螢幕截圖發給團隊成員,分享成果。

在往後的技術會議基本上滿意這個研發進度及結果,但還有一點憂慮,就是此技術在遊戲機平臺的效能。

因此,之後趕緊把程式碼移植至Xbox 360上測試(圖8)。實驗證明此技術的效能是可以的,瓶頸主要出現在特寫鏡頭時的GPU填充率。

圖8: Xbox360上的測試截圖

除了效能,另一讓我糾結的問題是髮絲排序。使用半透明alpha混合(alpha blending)的渲染效果,比alpha測試(alpha testing)好得多。因為前者能表現出很柔滑的感覺,而後者則較粗糙。然而,實驗裡的排序是基於一個啟發(heuristic)──按每根髮束的第i個節點在觀察空間(view space)的深度進行排序。當設i=0就是用髮根來排序,以實驗測試找出各種情況下較好的值。但以髮束為單位的排序不能完美解決問題,只是一個折衷方案。

當時還考慮過一些次序無關透明(order independent transparency, OIT)技術,例如實現過screen-door transparency,但效果都不如理想。或許用硬體的OIT方案,如alpha-to-coverage(需要較高的MSAA)、Direct3D 11中在渲染目標的緩衝區做OIT。

整合至Unreal引擎

效能測試大約花了一星期,之後就開始把此頭髮方案整合至Unreal引擎。

這個整合比我預期中困難許多,主要原因可能是我不熟悉Unreal渲染架構,而Unreal也缺乏這類文件,只能靠閱讀原始碼。由於所需的頂點資訊和Unreal內建的不一樣,所以要加入新的頂點格式、頂點著色器程式碼等。為了渲染第一個三角形,記得大概要增加、修改數十個原始碼檔案,並且由於變換矩陣的一些特別設定,以及多執行緒的問題,最終花了一星期時間才能渲染一個用自定頂點格式的三角形。

然而,之後的程式碼整合就變得容易,很快就可以為愛麗絲加上飄逸的長髮。在戰鬥中猛烈搖頭的效果,尤其令我鼓舞。之後再配合Unreal的工具,設定碰撞用的球體,最基本的整合就完成了。接著是要移植程式碼至Xbox360和PS3,我的同事Jake幫忙做了PS3的部分,把程式碼改寫成SPU的方式。而專門做圖形方面的楊同學也幫忙解決了不少問題,例如是景深效果(depth of field, DoF)時的問題(因為半透明的關係,頭髮本來並沒有寫入深度)。接下來一年的時間,也不斷作出調整和新功能,例如愛麗絲縮小、跳躍、滑翔和風吹效果等等。

有一次EA來訪,我們展示這個新技術時,對我印象最深刻的評語是:

不如讓愛麗絲做洗髮水廣告吧!

新的需求

因為頭髮系統研發的成果,便希望愛麗絲的衣飾都能捨棄手工動畫,採用程式式的動畫。之前我們嘗試過用現成的物理引擎的布料模擬,但效果不如理想。主要原因是,愛麗絲的裙子並不是輕薄柔滑的,而是裡面有許多層布,形成一個較固定的形狀。而且,設定中愛麗絲往下滑翔時,希望把裙子變成像降