實現原理揭祕:如何用Unity製作逼真的自然場景?
《Book of the Dead》是由Unity官方製作的一個演示短片,其中有大量的植物,不僅渲染上有著照片級的真實感,而且風與植物的互動也非常的自然。其所有的自然資源都是來自照片掃描技術,而且使用了HDRP高清渲染管線,場景在Unity Asset Store上可以下載得到。本文主要分析短片中風與植物互動的原理。
場景中,對於風與植物互動的模擬,支援三種結構:
- Hierachy Pivot:層次巢狀Pivot,用於模擬樹或者其他有多重層次結構的植物
- Single Pivot Color:單Pivot,用於模擬草
- Procedural Animation:程式動畫,用於模擬浮萍等無pivot的植物
對於樹的模擬最為複雜,它屬於Hierachy Pivot結構,最多支援3個層次巢狀:
- 主幹,連線地面
- Level 0分支,連線著主幹
- Level 1分支,連線著Leval 0分支
本文重點分析Hierachy Pivot結構的實現原理。風與植物的互動一般用程式頂點動畫實現,隨意找到一棵樹的shader,順藤摸瓜可以在VS中找到如下程式碼:
可以看到,每個頂點的uv3通道中存的是pivot資訊,即該頂點受哪些pivot影響。
- #if USE_VEGETATION_ANIM
- float3 positionWS = GetAbsolutePositionWS(positionRWS);
- APPLY_VEGETATION_ANIM_TIMENUDGE(positionWS, normalWS, input.uv3/*pivotData*/, input.color.rgb/*pivotColor*/, GetObjectAbsolutePositionWS(), time.x);
- positionRWS = GetCameraRelativePositionWS(positionWS);
- #endif
注意的是,這裡的uv3是float3型別。
- struct AttributesMesh
- {
- //...
- //forest-begin: Added vertex animation
- #if defined(_ANIM_SINGLE_PIVOT_COLOR) || defined(_ANIM_HIERARCHY_PIVOT)
- float3 uv3 : TEXCOORD3;
- //...
- };
Hierachy Pivot結構的植物,最終呼叫的是AnimateVegetationHierarchyPivot。
- #if defined(USE_VEGETATION_ANIM) && defined(_ANIM_SINGLE_PIVOT_COLOR)
- #define APPLY_VEGETATION_ANIM_TIMENUDGE(worldPos, normalWorld, pivotData, pivotColor, objectRoot, timeNudge) { AnimateVegetationSinglePivot(worldPos, normalWorld, pivotData, pivotColor, timeNudge); }
- #elif defined(USE_VEGETATION_ANIM) && defined(_ANIM_HIERARCHY_PIVOT)
- #define APPLY_VEGETATION_ANIM_TIMENUDGE(worldPos, normalWorld, pivotData, pivotColor, objectRoot, timeNudge) { AnimateVegetationHierarchyPivot(worldPos, normalWorld, pivotData, pivotColor, objectRoot, timeNudge); }
- #elif defined(USE_VEGETATION_ANIM) && defined(_ANIM_PROCEDURAL_BRANCH)
- #define APPLY_VEGETATION_ANIM_TIMENUDGE(worldPos, normalWorld, pivotData, piv
pivotData是float3型別,先用asuint轉成uint3,一共是32x3=96個bit,分成兩段前後48bit,分別存Pivot0和Pivot1的資訊,分別用UnpackPivot0和UnpackPivot1解出來。
- uint3 packedData =asuint(pivotData);
- float3 pivotPos0, pivotPos1, pivotFwd0, pivotFwd1;
- bool pivotEnabled0 =UnpackPivot0(packedData, pivotPos0, pivotFwd0);
- bool pivotEnabled1 =UnpackPivot1(packedData, pivotPos1, pivotFwd1);
Pivot0和Pivot1剛好是對稱排列。
接下來分析Pivot0是如何解碼出來的,48bit裡面,高32bit存Pivot Pos,低16位存Pivot Fwd,細節如下圖所示。最終解出來的Pos是模型空間的座標,樹的建模應該是樹幹的根在模型空間的原點,Pos.x和Pos.z是有正負的,而樹只能向上長,於是Pos.y必然是大於0。對於一般的樹而言垂直方向範圍一般大於水平方向的範圍,於是用12bit儲存Pos.y的值,稍微比x和z多2個bit的精度。
滿足packedData.y & 0xFFFF0000時,即高16位有值時,代表有Pivot0的資訊,才需要解析。
- // Needs to match shader packing in baking tool
- bool UnpackPivot0(uint3 packedData, inout float3 pivotPos0, inout float3 pivotFwd0) {
- if(packedData.y & 0xFFFF0000) {
- pivotPos0.x = UnpackFixedToSFloat(packedData.x, 8.f, 10, 22);
- pivotPos0.y = UnpackFixedToUFloat(packedData.x, 32.f, 12, 10);
- pivotPos0.z = UnpackFixedToSFloat(packedData.x, 8.f, 10, 0);
- pivotFwd0.x = UnpackFixedToSFloat(packedData.y, 1.f, 8, 24);
- pivotFwd0.z = UnpackFixedToSFloat(packedData.y, 1.f, 7, 17);
- pivotFwd0.y = sqrt(1.f - saturate(dot(pivotFwd0.xz, pivotFwd0.xz))) * (((packedData.y >> 16) & 1) ? 1.f : -1.f);
- pivotFwd0 = normalize(pivotFwd0);
- return true;
- }
- return false;
- }
其中Pos.x用UnpackFixedToSFloat解出來,10bit實際存的是百分比[0, 1],由於x可能是負數,編碼時把[-1, 1]對映到[0, 1],於是這裡把[0, 1]反對映回[-1, 1],再乘以傳入的range,可以看出Pos.x的範圍是[-8f, 8f]。從其他硬編碼的引數可以看出,樹的建模尺寸是長寬16x16,高是32。
- float UnpackFixedToSFloat(uint val, float range, uint bits, uint shift) {
- const uint BitMask = (1 << bits) - 1;
- val = (val >> shift) & BitMask;
- float fval = val / (float)BitMask;
- return (fval * 2.f - 1.f) * range;
- }
Fwd是分支(樹幹或者樹枝)的方向,由於是單位向量,所以只存了x和z分量,y分量可以通過公式反算出來,開方後丟失了符號資訊,於是用1位存符號。
- pivotFwd0.x = UnpackFixedToSFloat(packedData.y, 1.f, 8, 24);
- pivotFwd0.z = UnpackFixedToSFloat(packedData.y, 1.f, 7, 17);
- pivotFwd0.y = sqrt(1.f - saturate(dot(pivotFwd0.xz, pivotFwd0.xz))) * (((packedData.y >> 16) & 1) ? 1.f : -1.f);
- pivotFwd0 = normalize(pivotFwd0);
虛擬碼如下所示,有點跟骨骼動畫類似,頂點受骨骼的變換影響,而每個骨骼會受其父骨骼的變換影響,最終頂點受骨骼的級聯變換影響。樹的頂點至少受主幹的影響,因為任何頂點肯定要麼是屬於主幹或者屬於其他分支,而其他分支必然直接或間接連著主幹,最複雜的情況是頂點在level1分支上,level1分支連著level0分支,level分支連著主幹,需要計算累計變換。
- //任何頂點肯定是在主幹上或連線著主幹
- 計算主幹受風力影響導致的旋轉;
- 旋轉作用於頂點pos和normal;
- if (有pivot0資訊)//主幹連線著level0分支
- {
- 計算level0分支受風力影響導致的旋轉;
- 旋轉作用於頂點pos和normal;
- if (有pivot1資訊)//level0分支連線著level1分支
- {
- 計算level1分支受風力影響導致的旋轉;
- 旋轉作用於頂點pos和normal;
- }
- }
4.1 主幹受風影響
對於每個枝幹受風吹後彎曲程度,由如下變數控制,整體的彎曲程度可以由Wind Elasticity Lvl x系列變數控制,其中Lvl B是主幹。
樹被風吹有個特點,離地面越遠部分,被吹彎曲的越厲害,所以會有個縮放係數來控制旋轉量,地面處為0(樹根),離地面越遠的部分這個縮放係數越大。有種預烘培做法,是用頂點模型空間的y除以整個樹的高度,計算出縮放係數並把它烘到點色或者其他通道上,這裡的做法是執行時計算,用變數_WindRangeLvlB調節受風的範圍,它是模型空間的量,其實跟預烘培的效果差不多。lvBElasticity是最終的彈性縮放係數。
- //主幹風力影響程式碼
- float lvBRelativeObjectScale = mul(GetActualObject2World(), float4(0, _WindRangeLvlB, 0, 0)).y;
- float3 windFwd = GetWindDirection(objectRoot);
- float3 lvBBaseGustWind = GetTreeBaseGustWind(objectRoot, timeNudge);
- float3 lvBPos = objectRoot;
- //主幹fwd直接取模型空間y軸方向
- float3 lvBFwd = float3(0, 1, 0); //TODO: grab from rotation matrix
- float lvBElasticity = _WindElasticityLvlB;
- float lvBDistScale = saturate((worldPos.y - objectRoot.y) / lvBRelativeObjectScale);
- lvBElasticity *= lvBDistScale;
對於風吹草的模擬一般在頂點加上風力方向的偏移就可以得到比較好的效果,因為一般草都比較矮小,但是對於樹這種比較高的複雜結構,用草的方式模擬會有種樹被拉扯變長的感覺,所以一般的方案是用旋轉代替頂點偏移。
lvBWindAxis為旋轉軸,windFwd與lvBFwd如果同向或者反向時候,主幹應該是不會旋轉,後面的枝幹level 0做了這種情況的修正,主幹這裡可能從設計上就不會有垂直於地面的風向吧。
然後就是把旋轉作用到頂點的pos和normal上,旋轉的錨點是世界空間下的objectRoot,應該是模型空間的原點。
- float lvBWindRotAngle = lvBBaseGustWind.x * lvBElasticity;
- //對旋轉角度進行log2衰減
- lvBWindRotAngle = log2(1.f + abs(lvBWindRotAngle)) * sign(lvBWindRotAngle);
- float3 lvBWindAxis = cross(lvBFwd, windFwd);
- float4 lvBWindQuat = QuaternionFromAxisAngle(lvBWindAxis, lvBWindRotAngle);
- worldPos = QuaternionRotatePointAbout(worldPos, lvBPos, lvBWindQuat);
- worldNrm = QuaternionRotateVector(worldNrm, lvBWindQuat);
4.2 支幹Level0受風影響
邏輯基本與主幹差不多,不同的地方是lv0Fwd的方向用lv0BaseGustWind和lvBDistScale做了調整,猜測是為了讓彎曲旋轉更自然。
旋轉角度lv0WindRotAngle根據windFwd和lv0Fwd的是否平行,進行了相應的衰減。
- //當lv0BaseGustWind.y為0時,平行風,旋轉軸為y軸模擬更自然
- lv0Fwd.y *= lv0BaseGustWind.y * lvBDistScale;
- lv0Fwd = normalize(lv0Fwd);
- float3 lv0WindAxis = cross(windFwd, lv0Fwd);
- float3 lv0WindRight = cross(windFwd, lv0WindAxis);
- float lv0PerpendicularFactor = dot(lv0Fwd, lv0WindRight);
- float lv0AngleFactor = lv0PerpendicularFactor * lv0PerpendicularFactor;
- lv0AngleFactor *= sign(lv0PerpendicularFactor);
- lv0WindRotAngle *= lv0AngleFactor;
4.3 支幹Level1受風影響
Level1是樹最外層的部分了,整體流程與LevelB和Level0差不多,另外多了一些撲動的處理,樹的外端末枝(樹葉或者小樹枝)被風吹的時候往往是有較劇烈的搖晃,而且呈一定的隨機週期性運動,這部分計算出來的是頂點偏移,並在最後的旋轉前先加到頂點座標上。
- float vertexFlutterPhase = dot(worldPos, _WindFlutterPhase);
- float windFlutterCos = cos(WIND_PI2 * (_WindTime + timeNudge + vertexFlutterPhase) / (_WindTreeFlutterGustVariancePeriod * _WindFlutterPeriodScale));
- float windFlutterStrength = lv1Elasticity * _WindFlutterElasticity * _WindFlutterScale * (_WindTreeFlutterStrength + saturate((max(0.f, lv0BaseGustWind.z) - _WindTreeFlutterGustStrengthOffset) / _WindTreeFlutterGustStrengthScale) * _WindTreeFlutterGustStrength);
- float3 lv1WindAxis = cross(windFwd, lv1Fwd);
- worldPos += lv1WindAxis * windFlutterCos * windFlutterStrength;
Reference:
1. https://unity3d.com/book-of-the-dead
2. https://docs.unrealengine.com/en-US/Engine/Content/Tools/PivotPainter/PivotPainter2/index.html
作者:Kirk 騰訊互動娛樂 工程師
來源:騰訊GWB遊戲無界
原地址:https://mp.weixin.qq.com/s/j1_yZeEctRMcSfaMRRwr_w
相關文章
- 揭祕《Sherman》:使用Unity製作影視級光照效果Unity
- unity實現場景跳轉Unity
- 揭祕電子遊戲背後音效製作的故事遊戲
- Maya模型製作與場景建模模型
- php陣列原理遍歷原理揭祕PHP陣列
- 場景製作環節總是效率低?詳解場景製作初期的規劃思路
- 莉莉絲《劍與遠征》概念PV製作幕後揭祕
- 非同步神器:CompletableFuture實現原理和使用場景非同步
- 「揭祕GP」Greenplum 的人工智慧應用場景:MADlib、GPText、GPU人工智慧GPTGPU
- 【揭祕】高薪,單身,苦逼,這就是中國程式設計師生存現狀?高薪程式設計師
- 記憶體池原理大揭祕記憶體
- 揭祕Flutter Hot Reload(原理篇)Flutter
- Calico 網路通訊原理揭祕
- 揭祕webpack外掛的工作原理Web
- 徹底揭祕keep-alive原理Keep-Alive
- FastTunnel-內網穿透原理揭祕AST內網穿透
- Unity GameFramework丨(八)場景UnityGAMFramework
- 揭祕高仿包包市場
- KVM場景製作qcow2檔案
- widget state重新整理介面原理揭祕
- unity場景匯入iOSAPP求助UnityiOSAPP
- 如何用Unity實現超多面渲染?其實只需這三招Unity
- 使用Unity製作遊戲AIUnity遊戲AI
- Unity製作特寫鏡頭Unity
- Unity製作一個小星球Unity
- 真實場景再現
- 快照是什麼?揭祕儲存快照的實現
- 指哪打哪的遊戲是如何實現的? 揭祕光槍背後的原理遊戲
- 大資料揭祕:學歷真的能改變命運?大資料
- 【謊言大揭祕】Modin真的比pandas執行更快嗎?
- 揭祕定製高仿包包淘寶代理
- Unity《Boat Attack》Demo幕後揭祕(內附原始碼下載)Unity原始碼
- 揭祕高仿包包市場批發
- Unity C# 打包AssetBundle與場景UnityC#
- 如何繪製三維動畫設計和製作場景更好動畫
- 「Python實用祕技07」在pandas中實現自然順序排序Python排序
- 重製遊戲還要重現 Bug?!揭祕遊戲重製的幕後故事遊戲
- 觸底反彈,市場回暖?投資人揭祕真實的遊戲市場殘酷現狀遊戲