當前位置:首頁 >教程首頁 > 遊戲程序 > Unity3D遊戲開發工程師班 >如何用Unity製作逼真的自然場景

如何用Unity製作逼真的自然場景

發布時間:2020-03-25 12:36:41

遊戲中的風景越真實、自然,玩家的代入感也就越強。那如何用Unity製作逼真的自然場景?本文將分析Unity官方製作的演示短片——《Book of the Dead》中,風與植物的交互原理,幫助大家製作出更自然的風景。

0-1.jpg

《Book of the Dead》是由Unity官方製作的一個演示短片,其中有大量的植物,不僅渲染上有著照片級的真實感,而且風與植物的交互也非常的自然。其所有的自然資源都是來自照片掃描技術,而且使用了HDRP高清渲染管線,場景在Unity Asset Store上可以下載得到。本文主要分析短片中風與植物交互的原理。

0-2.gif

一、支持的植物結構

場景中,對於風與植物交互的模擬,支持三種結構:

Hierachy Pivot:層次嵌套Pivot,用於模擬樹或者其他有多重層次結構的植物

Single Pivot Color:單Pivot,用於模擬草

Procedural Animation:程序動畫,用於模擬浮萍等無pivot的植物

1-1.jpg

對於樹的模擬最為複雜,它屬於Hierachy Pivot結構,最多支持3個層次嵌套:

主幹,連接地麵

Level 0分支,連接著主幹

Level 1分支,連接著Leval 0分支

1-2.jpg

二、代碼入口

本文重點分析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, pivotColor, objectRoot, timeNudge)  { AnimateVegetationProceduralBranch(worldPos, normalWorld, objectRoot, timeNudge); }

三、PivotData解碼

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剛好是對稱排列。

3-1.png

接下來分析Pivot0是如何解碼出來的,48bit裏麵,高32bit存Pivot Pos,低16位存Pivot Fwd,細節如下圖所示。最終解出來的Pos是模型空間的坐標,樹的建模應該是樹幹的根在模型空間的原點,Pos.x和Pos.z是有正負的,而樹隻能向上長,於是Pos.y必然是大於0。對於一般的樹而言垂直方向範圍一般大於水平方向的範圍,於是用12bit保存Pos.y的值,稍微比x和z多2個bit的精度。

3-2.png

滿足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是主幹。

4-1.png

樹被風吹有個特點,離地麵越遠部分,被吹彎曲的越厲害,所以會有個縮放係數來控製旋轉量,地麵處為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;

文 | Kirk

騰訊互動娛樂 工程師

华体会hth体育网 賞析
  • 2101期學員李思庭作品

    2101期學員李思庭作品

  • 2104期學員林雪茹作品

    2104期學員林雪茹作品

  • 2107期學員趙淩作品

    2107期學員趙淩作品

  • 2107期學員趙燃作品

    2107期學員趙燃作品

  • 2106期學員徐正浩作品

    2106期學員徐正浩作品

  • 2106期學員弓莉作品

    2106期學員弓莉作品

  • 2105期學員白羽新作品

    2105期學員白羽新作品

  • 2107期學員王佳蕊作品

    2107期學員王佳蕊作品

專業問題谘詢

你擔心的問題,火星幫你解答
  • 杭州室內設計師培訓班有哪些

    杭州室內設計師培訓班有哪些已成為許多設計愛好者進入創意領域的理想起點。在杭州這座設計之都,室內設計行業蓬勃發展,帶動了培訓需求......

  • 石家莊視頻剪輯培訓價格性價比分析

    在視覺內容主導傳播的當下,掌握專業的視頻剪輯技能,已成為眾多求職者和職場人士提升競爭力的關鍵路徑。麵對日益增長的市場需求,石家......

  • 湛江影視後期培訓零基礎入門

    在數字媒體蓬勃發展的當下,影視後期製作是創意產業的核心驅動力。湛江,這座濱海城市,文化底蘊獨特,區位優勢明顯,影視行業正快速崛......

  • 濟南UI設計培訓哪家機構靠譜

    濟南UI設計培訓正成為越來越多人的職業起點。在這個數字時代,界麵設計需求激增,濟南作為一座文化底蘊深厚的城市,為學習者提供了豐......

  • 柳州UI設計培訓班避坑指南

    柳州UI設計培訓避坑指南:開啟你的創意職業之旅

  • 荊州平麵設計培訓班避坑指南

    荊州平麵設計培訓在當今數字化浪潮中扮演著關鍵角色。隨著品牌營銷和視覺傳達需求的激增,專業設計師成為各行各業的熱門人才。然而,選......

×

同學您好!

您已成功報名0元試學活動,老師會在第一時間與您取得聯係,請保持電話暢通!
確定