《戀與深空》作為一款超現實3D沉浸戀愛互動手游,自2024年1月18日上線以來,全球玩家數量突破7000萬,榮獲科隆游戲展2025最佳移動游戲。
在今天Unite Shanghai 2025活動中,來自《戀與深空》的引擎工程師們首次進行深度技術分享,揭秘《戀與深空》的創作幕后,如何通過影視級的渲染管線、真實細膩的物理效果,為玩家創造真實可感世界!
![]()
△分享現場座無虛席
成為今年Unity大會最火爆的場次
以下為《戀與深空》技術分享完整內容回顧:
![]()
《戀與深空》渲染底層框架的分享內容主要涵蓋【場景渲染優化、光照方案與管線設計、陰影優化】三部分。在開發過程中,我們基于Unity 2019對引擎源碼進行深度修改,開發了一套自定義的SRP管線。目前Android線上版本為GLES 3.1,未來也將上線Vulkan版本,持續提升性能,滿足玩家對高品質游戲的需求。
1. 場景渲染優化
在場景渲染優化中,我們開發了一套名【RendererGroupRenderer】的場景渲染系統,將每個渲染批次稱之為一個RenderGroup。通過這套系統我們實現了以下功能:
- 自定義靜態場景描述:我們去除GameObject,避免了更新大量GameObject時帶來的性能損耗。
- 優化CPU→GPU Upload 頻率:主要包括InstanceData和ConstantBuffer的Upload。關于InstanceData的Upload優化,我們在項目初期針對室內小規模場景,采用的是靜態生成InstanceDataBuffer,配合BVH分割裁剪的形式。隨著項目推進,場景精度要求不斷提高,后期便轉向在GPU端完成裁剪與instance填充。ConstantBuffer的Upload優化將在后面【單DrawCall性能優化】部分進行詳細說明。
- CPU側Burst+Job System并行粗裁剪:對靜態物件我們通過Burst+Job System實現了一套高度并發的裁剪系統,同時只在CPU側進行粗略的裁剪,將細粒度的裁剪任務交由GPU完成。
InstanceData數據形式
業界常用Constant Buffer形式的InstanceData存在一些缺點,比如64KB 尺寸限制、常量緩存小、動態索引時容易發生緩存擊穿導致性能下降等。另一種常用形式是用SSBO來傳遞InstanceData,但這種方法讀取性能通常不如緩存未擊穿情況下的ConstantBuffer,并且部分安卓設備在GLES下不支持在vertex shader中讀取SSBO,這也限制了它的兼容性。同時這兩個方案都有共同的問題:依賴動態索引,對低端手機性能不友好。
針對以上問題,我們提出了一種“新瓶裝舊酒”的方案——Vertex Stream based Instance Data。
- 使用PerInstance Step的Vertex Stream作為Instance Buffer;
- 走Vertex Fetch緩存,無需動態索引,Cache命中率高,無兼容性問題;
- 通過ComputeShader向Instance VertexBuffer輸出,實現GPU Driven。
使用PerInstance Step的Vertex Stream作為Instance Buffer——這是一種在GPU Instancing誕生之初就被支持的Instance方法,既可以避免動態索引帶來的性能問題,也避免了SSBO的兼容性問題。我們還可以通過ComputeShader向Instance VertexBuffer輸出來實現GLES下兼容性較高的GPU Driven。
最后,由于Unity引擎在底層沒有支持PerInstance Step的Vertex Stream,我們對引擎也做了相應的定制,最終暴露給上層的是CommandBuffer中添加的一個DrawMeshInstancedTraditional接口,它需要將另一個mesh作為instance data傳進來。我們也加了相應的接口來配置instance mesh中各個數據段對應的頂點semantic。
GPU Driven
我們會依據Group數量與Instance數量,提前分配IndirectParameter Buffer與Instance Data Buffer(這里Instance Data Buffer只是提前分配了空間,實際的數據為GPU Cull時填入)。
同時,我們會預計算每個Group的Instance Offset,并將其存儲到Parameter的InstanceStart項,全程只綁定一份Instance Buffer。
![]()
此外,我們還需要生成逐物件信息Buffe(包含GroupID、LOD Distance Range、Bounds、Transform等信息),用于在GPU裁剪時獲取每個物件的屬性。
- CPU剪裁:在GPU裁剪之前,我們會先執行一次CPU粗裁剪,以判斷Group整體是否可見。從一個根包圍盒開始,比較物件包圍盒體積總和與合并后包圍盒的體積比值,低于閾值就遞歸分裂包圍盒(主要目的為避免兩個物件距離過遠,拉出一個超大總包圍盒的情況發生)。同時結合PVS進一步判斷Group的可見性,因為我們沒有類似DX12的IndirectExecute,我們的GPU裁剪只能減少instance數,并不能消除Group整體的drawcall,因此需要,通過CPU裁剪盡可能準確地剔除掉完全不可見的Group。
- GPU剪裁:GPU裁剪則通過一次dispatch對所有Group進行逐物件3段裁剪,包含視錐裁剪、LOD裁剪、Hiz遮擋剔除,通過裁剪將Parameter的Instance Count加1,并輸出InstanceData。
- 陰影剔除:我們參考了龍之教條分享的方法,將畫面深度重投影到陰影空間作為Shadow Reveiver Mask,若Shadow Caster投出的Volume與Mask不相交,就可剔除避免多余陰影渲染。
此外關于我們“為什么沒有實現Cluster/Meshlet”部分,首先它在移動端存在較大基礎開銷,其次在GLES下實現Cluster也存在兼容性問題。綜合考慮下,我們認為優先優化單DrawCall的性能更能為我們帶來免費且直接的性能提升。
單DrawCall性能優化
在過往的觀察中,我們發現許多對于渲染的CPU耗時優化往往過于關注DrawCall數量,而忽視了每個DrawCall本身的耗時。我們認為降低DrawCall數量只是一種優化方法,最終的CPU耗時才是唯一的衡量指標。
現代移動設備與圖形標準其實早就可以勝任大量drawcall,這部分在HypeHype引擎團隊在Siggraph 2023中也有過分享——他們在iphone 6s上測試了一萬個不同Mesh與材質的DrawCall,耗時僅有11.27ms。其他同等的安卓設備也都基本能維持在60幀以上。而在2014年Metal剛剛誕生時,也提出過比GLES多畫10倍DrawCall的口號。
11年后的今天,我們仍為DrawCall過多而苦惱的原因,主要來自多方面的開銷,包括PSO切換過多、Buffer提交與拷貝、引擎渲染邏輯以及過多RHI接口調用,都會增加CPU負擔。因此我們認為性能優化不能只盯著DrawCall數量,而要綜合考量這些因素。
PSO切換優化:主要取決于每個項目對shader變體數量和shader復雜度的權衡。RenderGroup渲染隊列會根據shader,material,mesh的優先級排序,同時我們對陰影進行特殊處理:無AlphaTest的材質統一用相同shader渲染Shadow Depth,減少陰影渲染時的PSO切換頻率。
Buffer提交優化:在GLES下,Map/Unmap buffer會帶來顯著開銷,現代RHI支持的persistent map雖能顯著減少upload耗時,但仍無法避免數據從主線程到渲染線程,再到buffer內存的多次拷貝以及memcmp。因此我們采用了以下三種針對性的策略,顯著減少了Buffer Upload:
- PerRendererBuffer將逐Renderer的參數(如物體所受的環境光SH),存放在由Renderer對象維護的Uniform Buffer中,渲染時直接綁定;
- PerShaderBuffer針對不需要逐材質變化的uniform buffer,只在shader切換時提交一次,相比PerRendererBuffer來說,PerShaderBuffer更加靈活,可以支持不同的shader變體;
- 針對PerMaterialBuffer,我們借用了SRP Batcher代碼預生成逐材質buffer并直接綁定。
渲染邏輯優化:商業游戲引擎為保證靈活性與穩定性,渲染時會進行復雜的邏輯判斷。比如在Unity引擎內部,每次調用Draw時會先調用一個ApplyMaterial函數,它會在渲染之前更新所有的渲染狀態與參數,當DrawCall數量較多時存在可觀的耗時。因此我們進行了以下優化:
- 對ApplyMaterial接口進行了單獨拆分,僅在材質或參數需要切換時才由上層主動調用;
- 只需改變PerMaterialBuffer時,改用簡化后的專用接口。
優化后,我們的CPU在在相同DrawCall下耗時減少1/3。
RHI調用優化:RHI調用優化主要的目標是減少除了Draw Primitive以外的其他圖形API調用,具體優化包括:
- 合并相同stride的Vertex&Index Buffer,避免逐Draw Call bind VB/IB,耗時減少15%;
- Resource未發生變化時,跳過DescriptorSet設置,耗時進一步減少30%;SetDescriptors本身耗時較高時候,而且切換Descriptor還會增加下一次draw的耗時,這個在Arm的Best Practice Guide里有過介紹。
我們在低端安卓設備上測試了5000個DrawCall的耗時。使用引擎原生的渲染時,渲染線程的耗時是34.79ms。當我們對Buffer提交與渲染邏輯進行優化后,耗時降低到22.97ms。在進一步優化RHI調用次數后,耗時進一步大幅降至了11.8ms。最終我們在DrawCall數量不變的前提下,讓CPU耗時減少到了原來的1/3以下。
![]()
其他優化嘗試Benchmark場景測試結果
我們還嘗試了一些新的RHI特性,包括:
- Multi-Draw Indirect(MDI):在支持的設備上能夠帶來明顯優化,一定程度上改善GPU遮擋剔除可能會提交空DrawCall的問題(CPU端提交減少);
- Bindless:然而,Bindless的表現卻不盡如人意,即便在最新的安卓設備上也出現了神秘的負優化。結合MDI與Bindless,我們可以實現幾乎用一個DrawCall渲染所有物件,但是CPU耗時卻比不合批時還更高。這也是一個過度關注DrawCall數量的反面案例。當然,我們期待以后的移動芯片對bindless能有更好的支持。現階段的話,我們嘗試基于Unity Texture Streaming擴展出了一套無Feedback SVT系統作為替代方案,這個方案也還在驗證階段。
從Benchmark場景測試結果來看,RenderGroupRenderer對比原始無instancing渲染,DrawCall減少了1/3,渲染線程耗時大幅減少3/4,主線程耗時也減少了2/3(雖然C#耗時增加,但引擎原生裁剪與GameObject更新耗時減少,整體仍然帶來了大幅的優化)。
2. 光照方案
光照方案
前向渲染管線:
我們在項目中選擇使用前向渲染管線,包含以下多方面考慮:首先,前向管線在應對美術復雜且多變的需求方面有其優勢,我們不需要擔心一些材質屬性的添加是否會導致GBuffer膨脹。
其次,傳統的延遲管線對于移動平臺而言帶寬不太友好。OnePassDeferred則在靈活性方面存在一些局限,比如無法在RenderPass中間改變RT的尺寸,也不能fetch當前位置以外的像素內容。
在GLES下,FrameBufferFetch的兼容性也存在問題,不同芯片支持的fetch RT數量不同,有的只支持1張RT,需要改成通過PLS實現,但是我們測試PLS的性能并不理想。
另外,引擎自帶的逐物件4盞光源對于較大的物件來說不太夠用,因此我們嘗試了Forward+。但是Forward+在早期設備上耗時太高,若限制逐tile最大光源數,鏡頭變化時,tile內光源數量不可控,超上限會帶來表現bug。
為解決這些問題,我們采用了水平世界空間Tile劃分——默認2米一格,分布于相機前方,逐Tile最多4盞光源,128*128 Index Map。這種劃分方式使Tile光源重疊狀態穩定,便于在制作時及時發現超限問題。
![]()
Vulkan版本管線改進
我們在未來的Vulkan版本的管線中增加了基于Subpass的Light Pre-Pass。
在Pre-Z Pass中,我們會輸出一張簡易的GBuffer RT并且store下來。由于我們的local light光照使用了無fresnel的簡化PBR模型,所以我們不需要在GBuffer中輸出specular或者Albedo,只將normal,roughness和一些特殊的材質id或屬性信息pack到一張RGBA8的Gbuffer上,然后就可以跑一遍類似Deferred Shading的光源Volume渲染流程,將幾何光照結果保存到Tile Memory上。
![]()
之后在Shading Pass中,我們會把物件再畫一遍并fetch這些光照信息,再結合渲染時獲得的albedo等材質屬性,得到最終的光照結果。
我們將TAA所需的MotionVector Encode為RGBA8,R + G == 0代表無有效速度,這樣某些不輸出速度的材質可在BA通道存其他信息。
比如我們針對一些簡易且大量的植被,會在MotionVector的BA通道上保存他們的UV信息,這樣在Shading Pass時,我們只需要后處理獲取gbuffer中的幾何信息與MotionVector中的UV信息,即可還原出植被的材質表現。
Vulkan版本的管線流程大致如下:首先由PreZ Pass輸出Depth,GBuffer與MotionVector,然后計算陰影的遮擋剔除,接著執行陰影的深度渲染,再然后是一些AO和屏幕空間SSS之類的計算然后我們就進入NativeRenderPass,在SubPass中計算ShadowMask,Light Pre-Pass,以及執行正常的Shading Pass。最后退出RenderPass,再執行其他后處理Pass。
![]()
Vulkan版本管線改進也存在一定局限,比如Light Pre-Pass只能替換默認Lighting Model,對于需要更多Gbuffer通道的Lighting Model,還是需要采用Forward+。
不過我們提供了一個逐光源可選參數,可以針對某個光源強行使用Standard Lit Model,對所有材質統一處理,這樣可以在犧牲Lighting Model準確性的條件下實現讓同Tile內的像素受4盞以上燈的影響。
GI
Diffuse GI部分,我們采用了較為傳統的Lightmap+Light Probe的方式,Lightmap只保存間接光信息,Light Probe除了正常的逐物件單個采樣點的模式以外,我們還提供了一種多采樣點模式,能為每個物體設置多個采樣點,依據線段、三角形或四面體的重心坐標進行插值。
在以下兩張對比圖中,左圖為單采樣點的效果,box的底部為統一的環境光照;右圖則為使用兩個采樣點的結果,可以發現左右兩邊受到了不同的間接光照。
![]()
Specular GI方面,我們主要是基于使用了AABB校正的Reflection Probe。另外對于一些特定的地板或水面,我們還會使用平面反射代理。大致可以看成一種專門用來畫反射的HLOD。
此外我們還參考了戰神的做法,對Reflection Probe的CubeMap做了歸一化。具體來說就是根據CubeMap的像素生成一份環境光照的SH系數,將CubeMap中的像素顏色與該方向的環境光照相除,得到歸一化的CubeMap。在實際渲染時,再用每個像素在反射方向上所受的實際環境光照與CubeMap像素相乘,還原出反射顏色。
這種做法的好處是,即使大量物件采樣同一個Reflection Probe,不同區域的反射也能產生不同的明暗差別。
3. 陰影優化
功能設計
我們陰影系統的基本設計為:
- 三級CSM+角色特寫陰影/多角色POSM:3級Cascade的CSM+1級角色專屬的特寫陰影,在某些多角色場景時會使用POSM(Per-Object Shadow Map);
- 可支持兩盞錐燈投影;
- ScreenSpaceShadowMask:將以上陰影的結果都將輸出到了一張RGBA8的ScreenSpaceShadowMask上;
- R:Directional Shadow, G: Local Shadow 1, B: Local Shadow 2, A: AO:R通道保存主光陰影,G和B保存了錐燈陰影,A通道保存了AO信息。
距離剔除
我們首先做了一個簡單的距離剔除,根據陰影距離修改ScreenSpaceShadow后處理三角形頂點的深度值,之后再用ZTest Greater渲染,剔除陰影距離外的Shadow計算。
因為在計算陰影時要采樣depth,我們需要兩份depth分別用于Test與Sample,我們會在NativeRenderPass中拷貝一份Memoryless的Depth Buffer用于Test,盡量避免額外的讀寫帶寬。
半影區域檢測
我們增加了半影區域檢測功能,先在1/4分辨率下計算一次PCF,隨后在全分辨率Shadow Pass里采樣1/4 mask,僅對shadow值處于中間區域的像素執行全分辨率PCF,在保證效果的同時降低計算量。
為了避免這樣做之后存在某些細節像素檢測不準確的問題,我們會分別依據1/4 Buffer中Position的偏導與全分辨率Gather的4個深度值計算兩組法線。若法線夾角大于閾值,則判定低分辨率像素不可靠,強行執行全分辨率PCF。
以下為場景的Debug視圖,紅色區域被我們判定為半影區間,只有這些像素才會執行全分辨率的PCF。
![]()
逐像素bias
我們利用Receiver Plane Depth Bias算法實現了逐像素的Shadow Bias。它的原理也比較簡單,首先對屏幕空間shadow coordinates偏導應用二維鏈式法則,求出陰影空間偏導。
![]()
利用偏導與PCF采樣偏移我們可以求出bias值。對于中心點來說,我們增加了1個像素偏移的bias結果作為起始bias。
下圖為固定bias與逐像素bias的對比結果:
![]()
左圖使用固定bias值,可以看到box的底部有一段漏光區域,并且與光照方向接近垂直的表面存在部分自陰影走樣;使用逐像素bias之后(右圖),我們只會在偏導較大的區域增加bias,可以在保持細節投影的同時解決自陰影的走樣問題。
不過,當屏幕深度不連續時,逐像素bias可能算出錯誤結果,導致一些漏光現象。為了解決這一問題,需要美術手動指定bias的最大最小范圍。
Scrolling Cached Shadow Map
針對DrawCall較多的場景,我們還嘗試了Scrolling Cached Shadow Map,具體包括:
- 緩存CSM深度,對于前后兩幀都被陰影視錐完全包含的對象,將上一幀的CSM滾動到當前幀投影位置直接得到陰影深度,避免直接渲染對象;
- 只對最后一級cascade應用Scrolling,當cascade范圍比較小時,大量物體與會與視錐相交,優化效果就會受限;
- 間隔多幀更新緩存,減緩帶寬壓力。
在未來,我們還準備支持Local ShadowMap Atlas以及緩存機制。我們將會支持兩盞以上的局部燈投影,并且根據光源的屏占比動態調整ShadowDepth精度了,對于遠距離的局部光源,也會引入靜態緩存支持。
![]()
1. 角色光照方案
在角色光照方案中,相信大家多多少少都會遇到以下幾類問題:
![]()
對這些問題進行拆解,則可以總結為以下3個需求:
![]()
基于以上需求,我們進行了具體角色光照方案設計。
光照是由【直接光】和【間接光】組成的,一般情況下我們只會有一個平行光——我們習慣稱之為主光。主光正常照亮場景,但在照亮角色的時候我們保留它的方向,用一個類似后處理盒子的方式覆寫主光的顏色和亮度。具體實現方式為:
- 給Shader多傳一份角色主光顏色,角色的Shader在獲取主光時獲取到的顏色為角色主光顏色;
- 給角色提供了一盞額外的不投影的平行光用來做輪廓光;
- 同時預留了兩個額外光給角色,額外光可以是任意的點光和射燈組合,可以正常照亮范圍內的角色和場景物件( 因為一個2米的格子最多四盞額外光,所以將2個燈光劃分給角色)。
間接光我們使用Unity的LightProbe系統來創建探針,自己實現了保存間接光到探針里的部分,把場景的探針和角色的探針分開兩套,分別存儲和使用;
環境光高光我們使用同一個反射探針,但對于一些特殊的材質,我們提供了材質上輸入CubeMap覆蓋環境的反射探針的選項。
![]()
我們把這些影響角色的光照信息存到一個Scriptableobject里,由燈光師調整好之后保存為一個模板;下方右圖為角色燈光方案保存的信息,包含了上面提到的兩盞平行光,兩個額外光,還有探針保存下來的sh,以及一些后處理盒子上可以額外調整的信息和是否使用自定義的反射探針。
最后用一個manager使用類似棧的方式去管理,這里選用棧的管理方式跟具體使用強相關——通常情況下除了加載新的燈光方案之外,最常用的一個功能就是還原上一個燈光方案效果,因此我們采用了棧的管理方式。
![]()
到這里,這個方案已經具備了角色/場景分開、可實時切換、支持定制保持模板這些功能。最后我們把切換燈光方案定義成劇情編輯器上的一個事件行為,支持了可銜接光照動畫。
可銜接光效果如下所示:

下圖為項目專用劇情編輯工具,基本上所有的燈光和陰影相關的參數及部分后處理、物理效果都可以在這個劇情編輯器控制。

2. 特寫陰影
光和影一直都是密不可分的。如前文所提到,我們的陰影方案為三級CSM加特寫陰影,實現原理就使用角色身上的一根可指定的骨骼做球心,構成一個指定半徑的球,用這個球來構建和生成這張陰影圖,在屏幕空間陰影的時候會進行精度比較,使用這張陰影圖和級聯陰影中精度較高的一張作為這個像素的Shadow Map。
![]()
通過以下動圖可以看到,角色原本整個都在主光陰影里,打開特寫陰影的時候變成了可以被主光正常照亮,就是因為特寫陰影修改了近裁切平面;也就是說我們的特寫陰影是一張單獨可調參數的陰影圖,具體參數包括遠近裁切平面,最遠距離,還有使用哪一盞光和往往最讓人頭疼的bias。

3. 皮膚細節
皮膚上我們聚焦一些細節表現,具體以臉紅效果和流汗效果為例。
臉紅效果
通常來講,臉紅的過程是一個逐漸變化并且不同區域變紅程度不一樣的過程,比如大部分人在臉紅的時候會先從耳朵開始紅,然后是臉頰,偶爾會有整張臉變紅的表現。

為了模擬這個過程,我們采取了以下方式,使畫面更加生動和真實:
- 手繪遮罩:基于遮罩紋理控制臉紅區域、顏色梯度與強度;
- 多通道獨立:可分別調節面部、耳朵、鼻子等不同區域的紅暈效果;
- 預存變化過程:臉紅的過渡過程分通道記錄在對應曲線上,實現自然的情緒表達。
流汗效果
我們游戲里提供了運動陪伴功能,男主會進行一些運動訓練的陪伴,因此也就需要提供相應的流汗效果。具體實現主要通過以下三個方面:
- 材質與粒子結合:材質著色器模擬皮膚表面光澤與濕潤度,汗珠效果提供附著在皮膚上的材質實現和vfx實現可供選擇;
- 遮罩控制流汗區域:使用遮罩圖確定材質流汗區域,增強流汗效果的真實性和藝術性;
- 數據自動化傳遞:主控參數變化自動驅動材質與粒子參數。
下圖為一些具體的計算方式與最終效果示意。
![]()
△計算汗滴生成位置并修改汗滴位置粗糙度
![]()
△通過uv格子id生成隨機數
![]()
△模擬汗滴下落的軌跡

△運動陪伴系統流汗效果
![]()
物理效果的分享主要圍繞四個方面,包括布料模擬實現、實時表演控制、基于Unity DOTS的開發、碰撞檢測模塊。
1. 布料模擬實現
為了解決項目中的一些針對性的問題,我們內部自研了一套布料模擬的系統。
基于骨骼的布料模擬系統:StrayCloth
StrayCloth采用XPBD結合sub step的模擬方式。相比PBD,XPBD的優點是擺脫了迭代次數和時間步長的依賴,結合Substep可以顯著提升解算的收斂效果。
比較特殊的地方在于,我們使用骨骼作為模擬粒子,也就說每個粒子除了位置以外還帶旋轉信息。
在具體的substep實現中,我們針對不同性能壓力場景采用動態的子步幅時間,在1/200 -1/300之間。并且對場景中的運動對象進行運行插值,這樣碰撞的效果會更加穩定。事實上運動插值雖然性能開銷不是很高,但是由于類型眾多,比如有靜態粒子,碰撞體,風場等,實踐起來還是非常麻煩的。
![]()
為什么使用骨骼而不是代理網格?主要出于以下三個原因:
- 戀與深空在劇情、戰斗、換裝中的表現需求復雜,骨骼方案可以很好的過渡動畫和解算;
- 在可控性需求和移動端性能限制下,骨骼方案給美術的自由調節空間更大;
- 使用骨骼+約束可以構建類似Mesh的結構來達到相近的效果。
骨骼約束方案
在已有的骨骼布料方案里,骨骼約束實現常采用基于Local和Global形狀約束的實現方式,雖然簡單快速,但是也有明顯的缺點——在用來做布料模擬時,效果偏向卡通風格,不符合《戀與深空》追求的3D寫實風格;而且它的參數調整不直觀,因為它有gloabl和local兩個彎曲強度參數,不利于美術調整以及在不同場景下的效果匹配。
![]()
因此,我們在骨骼約束方案上,選擇了基于Cosserat Rod的骨骼約束。它的優點包括:
- 效果上更加自然,貼近戀與深空整體的寫實美術表現風格
- 參數調整上更加直觀,并且三個軸向強度分離,在一些場合比如模擬裙子的時候,可以通過各向異性的彎曲強度來近似裙撐的效果。
- 頭發模擬中可以直接復用,所以我們頭發和衣服也可以共用一套約束。
具體效果可以參考最新日卡的表現:

布料與角色連接
布料和角色的連接主要通過兩種方式:
- 層級:靜態骨骼直接受角色的骨骼動畫影響,根據層級關系進行移動。這種方式比較簡單,在一些偏向于剛性的連接部位時表現良好。但是對于一些骨骼交界有多個骨骼影響或者存在一定幅度拉伸和收縮的較為復雜的位置,例如手肘、肩部、腰部,表現上容易出現布料和角色分離。
- 吸附:靜態粒子受角色模型的錨定三角形控制。并行bake mesh,通過重心坐標每幀計算更新。
對于三角形存在的退化的特殊情況,我們使用三角形頂點的蒙皮骨骼的變換,進行加權平權來更新靜態粒子的transform。

碰撞方案
碰撞方案上,我們使用一個dynamic Bvh來作為場景碰撞的broad phase管理,每個角色作為sub tree包含其內部的碰撞體作為sub tree node。
同時,我們通過角色id,分享可見性還有部件類型,這個三個規則來實現不同角色、不同部件的碰撞規則的共享規則管理。
在narrow phase 當中,我們不直接生成contact,而是緩存碰撞體對,在substep中再具體的解決,因為我們采用的sub step的優點,大多數情況下直接使用DCD就可以避免一些快速運動下造成的穿透問題,不需要引入ccd或者predictive contact等一些操作。
![]()
Mesh Collider實現
對于參數化的幾何碰撞體,例如plane、capsule、box,可以比較簡單的解決它們和粒子以及edge的碰撞。在肩頸和胸背部等復雜部位,參數化的幾何體難以準確的表達角色模型形態,表現上容易發生穿透,所以在這些部位我們大量的使用Mesh collider。
但是mesh collider作為不規則的凹體,有時也可能是非閉合的,想達到精準的碰撞效果相對參數化幾何體就比較困難,特別是在移動設備下,因此我們采用散列哈希來作為三角形的粗略查找方式,結合緩存的鄰近三角形結果,在迭代開始前生成一次粒子-三角形碰撞對,后續的迭代中判讀粒子是否在三角形的范圍,如果超出三角形的范圍,通過模型的三角形鄰接關系進行限制步幅的三角形查找,來獲取最近的三角形,并且緩存結果作為下一次使用。
下方的動圖是項目中的一些具體表現示例,可以看到表現上是比較穩定的。

Face Collider
面部碰撞體可以看作是特殊的Mesh collider,相對于基本的mesh collider,它形態較為固定,也較為平滑,從模型中心出發基本上沒有三角形重疊,所以我們使用16x16的CubeMap來預計算各個方向上的三角形,這樣碰撞計算時可以快速查找到鄰近的三角形。
![]()
層間碰撞
游戲當中布料模擬的自碰撞是最難處理的部分,出于性能上的考慮,我們給出的方案如下:
- 使用spatial hashing作為查找加速結構
- 由美術預先分層,只考慮層之間粒子和三角形碰撞
- 避免層之間卡住的情況,只計算粒子和三角形單法線方向的碰撞
由美術預先對布料進行分層,只考慮這些層之間的碰撞。使用散列哈希作為查找的加速結構,并且為了避免層之間卡住的情況,我們只考慮單法線方向的碰撞,如果已經穿透了則略過,交給后面的步驟來修復。
實際實踐中,我們使用上一次substep的粒子位置來和當前的粒子位置進行碰撞,這樣可以很簡單的就解耦數據避免依賴。

層間穿透分離
對于層碰撞已經穿透的部分,我們參考了untanging cloth的方式,使用了一個輕量的解決辦法,通過布料分層,從布料的固定點出發,計算不同層級的邊和三角形的交點,因為我們的資產結構必定為一個uniform的網格,因此可以通過網格交點比較簡單的推測出其它粒子的推出三角形,最后對穿透的粒子-三角形對施加彈簧約束來解決穿透。在實踐中由于substep的關系,穿透的概率相對不大,因此我們采用分幀分塊執行來減輕性能壓力。
2. 實時表演控制
戀與深空劇情表現中大部分的物理表現,都是依托于cutscene來實現的各種物理效果的控制和調節。我們的工具同學開發和維護了一套非常強大的cutscene工具,在他們的基礎上我們開發了多種的功能軌道來具體調控物理效果。
這邊是我們一個動卡的Cutscene Physcs Track的例子,因為我們美術同學對于畫面表現扣的非常細,所以可以看到整個物理軌道的配置還是非常復雜的。

△Cutscene Physcs Track 示例
SmoothBlendPose Track
在表現當中,一個非常常見的問題就是動作瞬切切換帶來的物理抖動,無論是在劇情表演中還是換裝中,都經常出現。
我們開發了一個較為通用的辦法,通過記錄初始物理姿態,在切換的時候在初始姿態和當前姿態進行姿態插值計算,這樣就可以大幅度的緩解抖動,當然這個會帶來一些時間開銷,一般會在幾十毫秒左右,在大多數情況下都可以接受,提供一些參數例如插值次數,插值的步幅大小來讓美術可以根據實際需要來去調整。

Pose Track
當然,SmoothBlendPose存在局限性,不能保證的完全順暢,特別是在一些劇情表演的復雜鏡頭切鏡下。我們還提供了一個比較直接的方案——離線直接保存某個時間幀的物理狀態,在播放時,將保存的物理狀態直接應用到布料上,這樣就可以完美避免切鏡帶來的問題。

Edit Param Track
單一的物理資產是很難滿足劇情當中的各種不同場景下的表現的,比如有的時候希望布料軟一些硬一些,阻尼大一些小一些。我們提供編輯參數的軌道,通過這個軌道來實時的編輯修改參數,絕大部分的參數都可以覆蓋大,可以非常方便的針對一小段時間幀進行修改。這個參數修改還可以用來做一些特殊的效果,比如動圖當中的利用編輯約束參數來實現的斷裂的效果。

Animation Track
完全的物理效果實際上不足以支持起整個畫面方方面面的表現的,很多時候表現上需要動畫和物理的結合來做一些互動。我們通過動畫軌道來實現動畫和物理的銜接和融合,精細的控制不同時間幀范圍下的表現。在實際制作流程當中,動作在dcc里和最終進引擎的表現差異是比較大的,包括一些引擎的實時rig系統修改后,動畫可能和其它地方有穿透,所以我們在動畫融合的基礎上,可以疊加上物理的碰撞效果,來避免一些穿插。
動圖當中展示是項鏈在物理和動畫的交互效果,包括從物理到動畫的狀態切換以及在不同動畫之間的切換。

Collider Track & Wind Track
Collider Track與Wind Track可以在cutscene中動態的創建、銷毀碰撞體和風場。根據不同畫面需求,靈活改變碰撞體和風場的狀態。通過角色、部件類型、還有布料的層分組來細節控制所要影響的對象范圍。
并且,碰撞體和風場軌道的絕大部分參數可以添加動畫幀控制,包括碰撞體的形態大小、風場的方向、范圍、強度、湍流等,方便美術把控物理效果,精準控制變化。
動圖當中是軌道膠囊體和風場的一些表現例子。

3. 基于Unity DOTS的開發
Jobs + Burst + Mathematics
DOTS這套工具非常強大,在C#層就可以實現高性能的多線程開發。我們的物理系統使用DOTS完全構建在C#層上,功能迭代和debug都非常便利。目前來說我們最高可以支持2000+骨骼粒子的模擬。
當然,我們也針對性的在項目中,做了一些優化進一步提升性能。

Cache Job
模擬中的job數量和依賴關系確定,job data并不頻繁變化,幀內一般為相同數量和依賴關系的job組多次循環執行,Unity Jobs 在發起任務時每次都需要重新創建job,雖然可以提前發起任務緩解,但是依然會卡主線程。并且在執行完成job還需要clear。基于以上的觀察,我們開發了Cache Job的方案,預先創建好job data,然后每次執行時復用,避免每次重新創建job帶來的性能開銷。
![]()
實現上比較簡單,因為是一個專用的結構,只考慮一些固定的使用場景。額外添加了一個Atomic Queue用來存cache job,使用fetch and add array 來存具體的job。右邊是worker執行cache job的流程示意圖。
![]()
Neon Intrinsics
Burst會針對不同平臺生成高性能的simd code,在Burst Inspector中可以非常方便的查看。經過檢查Burst Inspector和實機測試,在某些場合下也可以通過手寫Arm Neon Intrinsics來進一步提升性能。
這里給出例子是判斷向量是否存在大于0的元素的實現。
![]()
![]()
Dot(float4)
對于點乘,我這里列出了3種方式,使用neon intrinsics相比于mathematics在測試用例中可以獲得約30%的性能提升。如果目標機型支持armv8.2的話,可以使用新增的規約加法指令,來進一步的提升性能。一般來說現在市面上的大部分流行機型都是支持armv8.2的。
![]()
Transpose(float4x4)
對于轉置計算,可以看到mathematics生成的assembly code看起來性能是非常低的,通過手寫neon intrinsics, 可以得到一個巨大的性能提升。
如果只是純粹的需要轉置,可以直接使用交錯讀,這里這樣實現因為在我一般的實際使用中是通過對4個float4轉置來將點乘變成矢量乘。
這里只是給出這兩個項目里比較常用的例子。因為mathematics的代碼一般被內聯,在具體優化時還需要根據代碼的上下文進行具體的優化,可以結合burst inspector和真機測試來進行具體的性能測試。
![]()
4. 碰撞檢測模塊
為什么要脫離Unity成熟的物理模塊重新開發?
Unity 本身具有基于physx的一套成熟的物理模塊,而脫離Unity成熟的物理模塊重新開發,主要基于以下考慮:
- 戀與深空有相當多的不同種類的玩法,玩法間的layer設置相對獨立,非常希望能夠各自維護一套layer設置。
- 有些模塊例如戰斗需要特殊的Trigger觸發和退出機制希望在底層就可以支持,對于執行流程也希望有更靈活的控制。
- 最后是在性能探索上我們也有一些想法,就是在僅需要碰撞檢測的情況下,利用DOTS能否提升性能?

《戀與深空》中的實現包括:
- 基本實現了所有原生的碰撞查詢功能
- 定制化的Update和Trigger邏輯
- 線程安全的查詢接口,上層可以無負擔調用
- 結合DOTS的輕量化結構實現,在性能測試中,最高可獲得——15%的提升
查詢流程示例
由于真機上,我們實際的線程數量是固定的為4,所以對于memory allocator可以預先按照線程數量分配好,在分配時可以直接根據當前線程索引來獲取。
使用基于SAH的dynamic bvh作為broadphase加速結構,在插入、刪除以及超出范圍的移動時,對當前操作節點的鄰近的幾個層級節點進行旋轉平衡。
因為碰撞檢測的功能目標相對概括,對于精度要求沒有那么高,所以我們也適當的犧牲一些精度簡化了一些碰撞檢測算法來提升性能。
![]()
觸發流程示例
為了滿足戰斗模塊的需求,我們設計了特殊的trigger觸發邏輯,Trigger的 Enter 和 Exit必須要成對出現,可以看到以下流程示意圖中,在a觸發b的函數中移除b后,會觸發所有和b存在overlap的collider,這里和unity原生的有所不同——原生的unity中在trigger邏輯中刪除掉b是不會觸發其它碰撞體的trigger的。最后,我們通過History計數來標記collider的版本,解決復用邏輯可能會導致的一些潛在問題。
![]()
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.