游戲開發進階Unity網格(Mesh\動態合批\骨骼動畫\蒙皮)

 更新時間:2021年09月03日 17:20:57   作者:林新發  
本篇文章是進階篇文章主要講解游戲開發進階,主要包含的技術有Mesh,動態合批,骨骼動畫,蒙皮下面一起進入Unity網格探險之旅吧

一、前言

嗨,大家好,我是新發。
有同學私信我讓我寫一篇Unity網格相關的教程,

在這里插入圖片描述

那我就帶大家來一次Unity的網格探險之旅吧~

二、Hello Mesh

我背著旅行背包走在Unity的場景中,突然眼前出現了一棵樹,

在這里插入圖片描述

我走近一看,這棵樹身上掛著MeshFilterMeshRenderer組件,根據Unity探險手冊記載,這個MeshFilter是網格過濾器,它會引用一個網格資源,我順騰摸瓜,找到了對應的網格,

在這里插入圖片描述

實在太美了,我久久佇立,這就是網格啊!
正當我欣賞著網格三角形時,突然世界暗了下來,眼前出現了一團火,

在這里插入圖片描述

我又拿出了Unity探險手冊,啊,這一定就是粒子系統了!它可以動態生成網格。

在這里插入圖片描述

天外傳來一陣打字聲,場景中出現了一行看起來像文字的網格,作為一個具有多年Hello World經驗的程序員,我看出了第一個單詞應該是Hello,第二個單詞…我知道了,

在這里插入圖片描述

Hello Mesh

在這里插入圖片描述

(此處為震撼人心的入場音樂)

三、萌新初識Mesh

1、引擎內置的Mesh

網格的英文名是MeshUnity萌新最先接觸的網格應該就是引擎內置的Cube(正方體)、Capsule(膠囊體)、Cylinder(圓柱體)、Plane(平面)、Sphere(球體)、Quad(四邊形),如下

在這里插入圖片描述

事實上,我們在Unity場景中,所有能被渲染出來的物體都會帶有網格,比如3D模型、粒子特效、UI、文字等等。

2、Mesh是什么

從概念上講,網格是圖形硬件用來繪制復雜內容的構造。它至少包含一組定義3D空間中點的頂點,以及一組連接這些點的三角形,實際上還包含法線、頂點顏色紋理坐標等信息,這些三角形構成了網格所代表的任何表面。

我們可以看下UnityMesh類,Mesh的屬性和方法很多,我這里列舉幾個比較常用的,如下

// 頂點坐標數組
public Vector3[] vertices { get; set; }
// 法線向量數組
public Vector3[] normals { get; set; }
// 頂點顏色數組
public Color[] colors { get; set; }
// 三角形序列數組,每三個數字為一組
public int[] triangles { get; set; }
// uv坐標數組
public Vector2[] uv { get; set; }
// 重新計算法線,在修改完頂點后,通常會更新法線來反映新的變化,注意,法線是根據共享的頂點計算出來的。
public void RecalculateNormals();
// 從法線和紋理坐標重新計算網格的切線。修改網格的頂點和法線之后,如果網格使用引用法線貼圖的著色器進行渲染,則切線需要更新。 
public void RecalculateTangents();
// 重新計算從網格包圍體的頂點, 在修改頂點后需要這個函數以確保包圍體是正確的,賦值三角形將自動重新計算這個包圍體。
public void RecalculateBounds();

畫個圖,方便大家有個直觀印象,

在這里插入圖片描述

三、Mesh的創建方式

1、第三方建模軟件

建模本質上就是建網格,我們可以事先通過第三方建模軟件來創建模型網格,

在這里插入圖片描述

常見的建模軟件比如

在這里插入圖片描述

3DS MAX官網:https://www.autodesk.com/products/3ds-max/overview

在這里插入圖片描述

MAYA官網:https://www.autodesk.com/products/maya/overview

在這里插入圖片描述

blender官網:https://www.blender.org/

2、Unity建模插件:ProBuilder

Unity官方提供了一個可以用來創建和自定義幾何體的工具ProBuilder,我們可以在UnityPackage Manager中下載到這個插件,

在這里插入圖片描述

使用ProBuilder我們可以直接在Unity中創建或編輯簡單的幾何體,不用通過第三方建模軟件,提升了效率,方便快速搭建場景原型,

在這里插入圖片描述

3、程序動態生成網格

網格也可以是程序動態生成的,比如粒子系統的網格就是動態生成的,

在這里插入圖片描述

又比如文字,也是程序動態生成網格,

在這里插入圖片描述

文章后面我還會手把手教你如何使用純代碼來構建網格,這里先不急著寫代碼,我們繼續探尋網格的秘密先~

四、Unity中如何顯示網格

Unity中,我們要顯示一個網格,需要用到兩個組件:MeshFilterMeshRenderer

注:你也可以直接使用SkinnedMeshRenderer組件,與MeshFilterMeshRenderer的區別我下文會講。

1、MeshFilter:網格過濾器

MeshFilter是網格過濾器,我們需要通過它設置引用的網格資源,比如這里引用的是一個Cube(正方體)網格。

在這里插入圖片描述

我們可以看下MeshFilter.cs的源碼,

[RequireComponent(typeof(Transform))]
[NativeHeader("Runtime/Graphics/Mesh/MeshFilter.h")]
public sealed partial class MeshFilter : Component
{
    [RequiredByNativeCode]  // MeshFilter is used in the VR Splash screen.
    private void DontStripMeshFilter() {}

    extern public Mesh sharedMesh { get; set; }
    extern public Mesh mesh {[NativeName("GetInstantiatedMeshFromScript")] get; [NativeName("SetInstantiatedMesh")] set; }
}

MeshFilter只有兩個屬性:meshsharedMesh
我們查看Unity的官方手冊,看看meshsharedMesh的區別:https://docs.unity3d.com/ScriptReference/MeshFilter.html

在這里插入圖片描述

我來解讀一下,mesh訪問的是一個Mesh資源的實例(副本),這意味著我們修改這個mesh并不會修改到原始資源本身,改的只是Mesh的實例(副本)。
sharedMesh是原始資源的引用,如果修改了sharedMesh,比如修改頂點坐標,那么原始資源也會被修改。
畫成圖大概是這樣子:

在這里插入圖片描述

這里我順手寫個隨機修改Mesh頂點坐標的腳本,如下,將下面這個RandoMeshmVertices腳本掛到MeshFilter組件所在的物體上即可,

// RandoMeshmVertices.cs
// 隨機修改Mesh頂點坐標
using UnityEngine;
public class RandoMeshmVertices: MonoBehaviour
{
    // Mesh的實例
    MeshFilter meshFilter;
    // 頂點的原始坐標
    Vector3[] originalVertices;
    void Start()
    {
        meshFilter = GetComponent<MeshFilter>();
        originalVertices = meshFilter.mesh.vertices;
    }
    void Update()
    {
        // 隨機修改頂點坐標
        Vector3[] vertices = meshFilter.mesh.vertices;
        for (int i = 0, len = originalVertices.Length; i < len; ++i)
        {
            var v = originalVertices[i];
            vertices[i] = v + Random.Range(-0.1F, 0.1F) * Vector3.one;
        }
        meshFilter.mesh.vertices = vertices;
        meshFilter.mesh.RecalculateNormals();
    }
}

運行效果如下,網格頂點坐標發生了隨機偏移,

在這里插入圖片描述

關于mesh屬性的訪問需要特別注意一下,我們先看看Unity官方手冊的說明,https://docs.unity3d.com/ScriptReference/MeshFilter-mesh.html

在這里插入圖片描述

翻譯一下就是,如果一個Mesh資源已經被分配給MeshFiltermesh屬性,那么當我們在代碼中第一次訪問mesh屬性時才正真創建了Mesh的實例;再次訪問mesh屬性時則直接返回這個實例,并且一旦mesh屬性被訪問,則與原始共享網格的鏈接會丟失,此時sharedMesh變成mesh的別名,如果我們想避免這種自動生成Mesh實例,可以使用sharedMesh代替。
寫成偽代碼的話大致是這樣子:

public class MeshFilter ...
{
	...
	private Mesh _mesh;
	public Mesh mesh
	{
		get
		{
			if (_mesh == null) 
			{
				_mesh = new Mesh();
				Copy(sharedMeh, _mesh);
			}
			return _mesh;
		}
	}
	...
}

還有,如果我們訪問了mesh屬性而導致自動創建了Mesh實例,則需要在代碼中主動調用Resources.UnloadUnusedAssets來銷毀沒有引用的Mesh實例,建議是在場景切換時調用Resources.UnloadUnusedAssets

2、MeshRenderer:網格渲染器

MeshRenderer,顧名思義,網格渲染器。我們依舊先來看看官方手冊的介紹:

https://docs.unity3d.com/Manual/class-MeshRenderer.html

在這里插入圖片描述

翻譯過來就是MeshRenderer會從MeshFilter那里拿到網格數據并在所在物體的位置處將其渲染出來。
如果沒有MeshRenderer,我們就看不見網格了,如下

在這里插入圖片描述

另外,我們還需要在MeshRendererMaterials中指定一個材質球,這樣才能正常顯示,否則模型表面就是紫色的。

在這里插入圖片描述

3、SkinnedMeshRenderer:蒙皮網格渲染器

SkinnedMeshRenderer是蒙皮網格渲染器,可能有小伙伴就會問了,上面使用MeshFilterMeshRenderer已經可以顯示模型網格了,為什么又弄了一個SkinnedMeshRenderer呢?
看下Unity官方手冊的介紹:https://docs.unity3d.com/Manual/class-SkinnedMeshRenderer.html

在這里插入圖片描述

可以看到SkinnedMeshRenderer其實是針對帶 骨骼動畫 的模型的渲染的。

3.1 骨骼動畫

為什么需要做骨骼動畫呢?

就好比我們人一樣,我們的骨骼會隨著我們肌肉的伸縮而動,骨骼又可以帶動它管轄的身體部位發生形變和移動,骨骼還會影響它所連接的其他骨骼一起發生聯動。對應到模型動作上,想想一個簡單的舉手動作要牽涉到多少網格頂點的移動,如果沒有骨骼,那動畫師要每幀挨個網格頂點進行調整,即使動畫做出來了,這個動畫也不能復用到其他模型上,因為不同模型的頂點信息都不一樣,這么低效的動畫制作肯定是不行的,于是,就有了骨骼動畫。

骨骼動畫的原理

就是將模型分為骨骼(Bone)和蒙皮(Mesh)兩個部分,骨骼可分為多層父子骨骼,每個骨骼都附加到周圍網格的一些頂點上,在動畫關鍵幀數據的驅動下,計算出各個父子骨骼的位置,基于骨骼的控制通過頂點混合動態計算出蒙皮網格的頂點。

動畫師可以在MAYA軟件上給模型綁定骨骼,綁定骨骼不是本文的重點,這里就不展講開具體操作了,感興趣的同學可以自行百科學習。

制作好導出為fbx格式,

在這里插入圖片描述

fbx文件導入到Unity中,選中它,

在這里插入圖片描述

Inspector視圖中點擊Rig按鈕,

在這里插入圖片描述

我們可以看到動畫類型Animation TypeNoneLegacyGenericHumanoid四個,

在這里插入圖片描述

具體選項可以參見Unity官方手冊:https://docs.unity3d.com/Manual/FBXImporter-Rig.html

在這里插入圖片描述

我這里演示一下人形骨骼動畫,選擇Humanoid類型,Avatar Definition選擇Create From This Model,然后點擊Configure

在這里插入圖片描述

Inspector視圖中我們就可以看到對應的骨骼綁定信息了,

在這里插入圖片描述

如下,綠色的線段就是一根根骨骼,

在這里插入圖片描述

我們調整一根骨骼,對應的網格也會跟著一起動,如下

在這里插入圖片描述

這樣做出來的人形動畫是可以進行復用了,有請妹子上場,

在這里插入圖片描述

骨骼動畫資源的話,我在之前的文章中也介紹過一個寶藏網站Mixamohttps://www.mixamo.com/,上面有很多做好的人形骨骼動畫,

看,是不是挺好玩的,

我們可以把它的動作直接復用到我們自己的人形模型上,效果如下:



3.2 SkinnedMeshRenderer組件

骨骼動畫可以正常播放,要歸功于SkinnedMeshRenderer組件,制作好骨骼動畫的fbx文件導入Unity中,Unity會自動幫我們掛上SkinnedMeshRenderer組件,

在這里插入圖片描述

其中幾個重要的屬性我講一下,
Bounds:骨骼數據;
Mesh:要渲染的網格;
Root Bone:根骨骼,其他骨骼都是相對根骨骼移動的;
BlendShapes:一般用于制作表情融合,我之前寫過一篇文章講過BlendShapes

Unity通過BlendShape實現面部表情過渡切換Animation教程

我們再來看看SkinnedMeshRenderer腳本的屬性和方法:

在這里插入圖片描述

需要講的應該就是這個BakeMesh方法了,下面我就單獨拎出來講下BakeMesh

3.2 使用BakeMesh進行優化

假設現在場景中有100只皮卡丘,每只皮卡丘的網格、貼圖、動作相同,

在這里插入圖片描述

如果每只皮卡丘身上都掛SkinnedMeshRenderer,那就是100SkinnedMeshRenderer在計算蒙皮,

在這里插入圖片描述

由于SkinnedMeshRenderer是根據骨骼動畫動態計算網格頂點坐標,這個運算開銷還是不小的,有沒有辦法優化呢?

SkinnedMeshRenderer提供了一個BakeMesh方法,可以將一個蒙皮動畫的某個時間點上的動作,Bake成一個不帶蒙皮的Mesh,我們統一使用這個Mesh來顯示其余的皮卡丘,這樣就可以大大減少了SkinnedMeshRenderer的計算了,
畫成圖大概是這樣子:

在這里插入圖片描述

不過,上面這種方案的局限性是每只皮卡丘的動畫是相同的,如果突然某一只皮卡丘要播放與其他皮卡丘不同的動畫,那就不行了。

另一種Bake方案可以是這樣:
對皮卡丘的每個動畫進行遍歷采樣,把采樣到的Mesh存到數組中,因為這里要Bake很多網格,比較耗時,建議在加載場景時時就完成采樣過程;后面要播放某個動畫時直接從這個Mesh數組中獲取Mesh來顯示,此時直接使用MeshFilterMeshRenderer的方式來顯示網格就好了。
貼個BakeMesh的示例腳本:

using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// Bake Mesh 示例
/// </summary>
public class BakeMeshTest : MonoBehaviour
{
    [SerializeField]
    Animation m_animation;
    [SerializeField]
    SkinnedMeshRenderer m_skinnedMeshRenderer; 
    [SerializeField]
    string m_clipToBake = "Idle";
    List<Mesh> m_bakedMeshList = new List<Mesh>();
    /// <summary>
    /// 采樣幀數
    /// </summary>
    [SerializeField]
    int m_numFramesToBake = 30;
    void Start()
    {
        // 獲取要Bake的動畫片段
        AnimationState clipState = m_animation[m_clipToBake];
        if (clipState == null)
        {
            Debug.LogError(string.Format("Unable to get clip '{0}'", m_clipToBake), this);
            return;
        }
        // 開始播放動畫
        m_animation.Play(m_clipToBake, PlayMode.StopAll);
        // 設置動畫初始時間戳
        clipState.time = 0.0f;
        // 采樣幀間隔
        float deltaTime = clipState.length / (float)(m_numFramesToBake - 1);
        for (int frameIndex = 0; frameIndex < m_numFramesToBake; ++frameIndex)
        {
            string frameName = string.Format("BakedFrame{0}", frameIndex);
            // 創建Mesh
            Mesh frameMesh = new Mesh();
            frameMesh.name = frameName;
            // 動畫采樣
            m_animation.Sample();
            // 執行BakeMesh
            m_skinnedMeshRenderer.BakeMesh(frameMesh);
            m_bakedMeshList.Add(frameMesh);
            // 設置動畫時間戳
            clipState.time += deltaTime;
        }
        // 停止播放動畫
        m_animation.Stop();
    }

}

需要提醒的是,這個方案是利用空間換時間,如果模型頂點數據特別多或動畫時長特別長的時候,這時就會遇到內存瓶頸。

五、純代碼動態創建網格

一般情況下,網格是事先制作好的資源,但也有一些特殊的需求需要在代碼中動態創建網格。
比如我之前寫的一篇牙齒碎了的文章:

游戲開發Unity2D圖片任意形狀破碎裂片效果展示

現在我來教大家如何使用代碼從零創建網格并將網格渲染出來,下文我以創建一個正方形網格為例進行講解。

1、創建Mesh對象

第一步最簡單,就是直接new一個Mesh

var mesh = new Mesh();

2、頂點坐標

首先分析一下,一個四邊形有四個頂點,假設正方形邊長為1,四個點的坐標如下,

在這里插入圖片描述

寫成代碼就是這樣:

// 構建頂點坐標
var vertices = new List<Vector3>();
vertices.Add(new Vector3(-0.5f, -0.5f, 0));
vertices.Add(new Vector3(-0.5f, 0.5f, 0));
vertices.Add(new Vector3(0.5f, 0.5f, 0));
vertices.Add(new Vector3(0.5f, -0.5f, 0));
// 將頂點坐標設置給Mesh
mesh.SetVertices(vertices);

3、UV坐標

UV坐標就是紋理貼圖坐標,它將紋理上每一個點精確對應到模型物體的表面上,注意UV的取值范圍是0~1
UV坐標系原點在左下角,U軸是水平軸,V軸是豎直軸,如下:

在這里插入圖片描述

對應到我們的上面那個正方向網格的話,四個點的UV坐標如下:

在這里插入圖片描述

寫成代碼就是這樣:

// 構建UV坐標
var uvs = new List<Vector2>();
uvs.Add(new Vector2(0, 0));
uvs.Add(new Vector2(0, 1));
uvs.Add(new Vector2(1, 1));
uvs.Add(new Vector2(1, 0));
// 將UV坐標設置給Mesh
mesh.SetUVs(0, uvs);

4、三角形序列

網格需要切分成三角形,我們可以這樣切分,

在這里插入圖片描述

當然也可以這樣切分,

在這里插入圖片描述

兩種切分方法對應不同的三角形序列,假設 法線方向 是垂直于屏幕從內指向屏幕外的話,第一種切分方式的三角形序列如下:

注:法線的方向就決定了表面正面,如果你的材質是單面渲染的話,那么只有從正面看才能看到網格被渲染。

在這里插入圖片描述

即三角形序列為:{ 0, 1, 2, 0, 2, 3 },注意序號是從0開始的。
為什么是這樣的順序呢?我教大家一個技巧,伸出你的左手,豎起大拇指,像這樣子,

在這里插入圖片描述

大拇指指向法線的方向,那么此時你的其余四根手指頭環繞的方向就是三角形的序號的順序,三個序號為一組按順序塞入數組中即可,即得到的數組就是:{ 0, 1, 2, 0, 2,3}當然,以下數組最終的效果都是等價的,只要順序一致即可:

{ 0, 1, 2, 0, 2, 3 },
{ 1, 2, 0, 0, 2, 3 },
{ 0, 2, 3, 1, 2, 0 },

我們現在寫成代碼,

// 重新計算法線,注意,法線是根據共享的頂點計算出來的。
mesh.RecalculateNormals();

// 重新計算包圍體,在修改頂點后需要這個函數以確保包圍體是正確的
mesh.RecalculateBounds();

// 從法線和紋理坐標重新計算網格的切線(如果網格使用引用法線貼圖的著色器進行渲染,則切線需要更新)
// 因為我們這里不使用法線貼圖,所以就不調用它了
// mesh.RecalculateTangents();

5、重新計算法線和包圍體

當我們設置或修改了頂點數據后,需要調用MeshRecalculate方法來重新計算一些必要的信息,比如重新計算法線、包圍體,代碼如下

// 重新計算法線,注意,法線是根據共享的頂點計算出來的。
mesh.RecalculateNormals();

// 重新計算包圍體,在修改頂點后需要這個函數以確保包圍體是正確的
mesh.RecalculateBounds();

// 從法線和紋理坐標重新計算網格的切線(如果網格使用引用法線貼圖的著色器進行渲染,則切線需要更新)
// 因為我們這里不使用法線貼圖,所以就不調用它了
// mesh.RecalculateTangents();

6、完整版代碼

以上代碼封裝成GenQuadMesh.cs腳本,完整代碼如下:

// 使用代碼生成四邊形網格
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class GenQuadMesh : MonoBehaviour
{
    public MeshFilter mf;
    private void Start()
    {
        mf.mesh = Build();
    }
   public static Mesh Build()
    {
        var mesh = new Mesh();
        // 構建頂點坐標
        var vertices = new List<Vector3>();
        vertices.Add(new Vector3(-0.5f, -0.5f, 0));
        vertices.Add(new Vector3(-0.5f, 0.5f, 0));
        vertices.Add(new Vector3(0.5f, 0.5f, 0));
        vertices.Add(new Vector3(0.5f, -0.5f, 0));
        // 將頂點坐標設置給Mesh
        mesh.SetVertices(vertices);
        // 構建UV坐標
        var uvs = new List<Vector2>();
        uvs.Add(new Vector2(0, 0));
        uvs.Add(new Vector2(0, 1));
        uvs.Add(new Vector2(1, 1));
        uvs.Add(new Vector2(1, 0));
        // 將UV坐標設置給Mesh
        mesh.SetUVs(0, uvs);
        // 設置三角形序列
        var triangles = new int[] { 0, 1, 2, 0, 2, 3 };
        mesh.SetTriangles(triangles, 0);
        mesh.RecalculateNormals();
        mesh.RecalculateBounds();
        return mesh;
    }
}

7、測試

創建一個空物體,掛上MeshFilterMeshRenderer組件。

在這里插入圖片描述

再掛上我們上面寫的GenQuadMesh腳本,賦值mf變量為MeshFilter對象,如下

在這里插入圖片描述

運行Unity,看到一個紫色快,

在這里插入圖片描述

Scene視圖的模式設置為Wireframe,如下

在這里插入圖片描述

現在我們可以看到我們動態創建的網格啦,

在這里插入圖片描述

上面之所以顯示紫色塊,是因為我們沒有給MeshFilter設置材質球,順手做一個炮姐的材質球吧,

在這里插入圖片描述

MeshRenderer設置材質球對象,

在這里插入圖片描述

重新運行Unity,效果如下,

在這里插入圖片描述

8、項目源碼

要用代碼動態創建一個Mesh,就是new一個Mesh,給它塞入頂點坐標、UV坐標和三角形序列即可。再復雜的網格也可以通過這些步驟創建出來~
下面這些就是使用純代碼創建出來的幾何體網格,感興趣的同學可以下載項目源碼下來學習。
項目源碼:https://codechina.csdn.net/linxinfa/unity-mesh-builder

在這里插入圖片描述

六、網格相關的開源項目

我再推薦一些網格相關的開源項目給大家~

1、2D網格涂鴉

項目地址:https://github.com/mattatz/unity-triangulation2D

在這里插入圖片描述

2、3D網格涂鴉

項目地址:https://github.com/mattatz/unity-teddy

在這里插入圖片描述

3、網格體素化

項目地址:https://github.com/Scrawk/Mesh-Voxelization

在這里插入圖片描述 

在這里插入圖片描述

4、網格平滑算法

項目地址:https://github.com/mattatz/unity-mesh-smoothing

在這里插入圖片描述

5、網格切割

項目地址:https://github.com/hugoscurti/mesh-cutter

在這里插入圖片描述 

在這里插入圖片描述

6、網格合并

項目地址:https://github.com/sanukin39/UniMeshCombiner

在這里插入圖片描述

七、未完的探險

好了,這次探險之旅就暫時到這里吧,還有很多內容需要探索,先保持體力,我們下次再見,更多關于Unity網格(Mesh\動態合批\骨骼動畫\蒙皮)的資料請關注腳本之家其它相關文章!

相關文章

最新評論

精品国内自产拍在线观看