1. 緒論
最近想學習一下Unity3d,無奈發現現在大部分教程不僅是視訊形式的,面對的也是美術、設計之類的非程式設計師,更多的時候都是把Unity3d當作PS一樣的工具來用,真正面對程式開發的教程反而非常少,更不用說希望能研究到一些底層圖形技術的技術工作者了。
說一下我看的兩本Unity3d書籍吧。第一本是《Unity 3D遊戲開發(第2版)》(宣雨鬆 著)。這本書算是大部分教程書籍中評價比較好的了,很多人推薦。不過個人感覺作者對Unity3D的知識有了太多的積累,已經忘記了初學者初學Unity3D的心態,知識也顯得比較零散。不過這也是國內大多數書籍的通病了,更像是作者對知識的總結而不是成體系的向讀者介紹知識。建議初學者看這本書一定要實操,喜歡頭腦風暴的同學不適合這本書。
看的第二本書是《Unity Shader入門精要》(馮樂樂 著)。令人佩服的是這本書是位程式媛寫的,可能正是因為如此,這本書寫的確實非常細緻到位。尤其是前面幾章對渲染管線的描述,從Unity3D圖形技出發,已然上升到計算機圖形學的高度上,對學習其他的圖形技術也有非常大的幫助(畢竟很多圖形技術都是通用的)。當時看了覺得確實很不錯,因此還送了同事一本。
最後就是自己也想總結一下Unity3D的相關知識吧,本身是個程式猿,當然更多的會偏向遊戲開發的程式設計師角度,或者圖形技術的程式設計師的角度一點。
2. 概述
圖形渲染技術的第一個HelloWorld當然應該就是繪製一個三角形了。在絕大多數情況下,三角面是渲染物體的基礎圖元。作為高階的渲染引擎,像三角面這樣的幾何體甚至不需要我們去通過程式碼來繪製,但是卻是我們學習的基礎,立足於這個基礎,我們以後能夠渲染更加複雜的圖形。
3. 詳論
3.1. 準備
通過Unity Hub建立一個3D工程:
進入Unity3D環境,通過右鍵選單,在"Hierarchy"檢視中新增一個名為"Root"空的GameObject:
GameObject物件是Unity3D中得一個基礎類,Unity3D中得絕大部分物件都是基於它實現的,比如相機、燈光、或者模型等。所以我們這裡把建立的名為Root的GameObject物件作為場景的根節點。
在Root物件的Inspector皮膚中,可以看到一個"Add Component"按鈕:
也就是說,通過"Add Component"按鈕,我們可以掛接一些元件,這樣,空的GameObject物件就成為了其他型別的物件。例如,我這裡掛接一個C#指令碼,通過C#指令碼來繪製物體,那麼這個GameObject,表示的就是一個渲染的物體。
在"Project"檢視中,通過右鍵選單建立一個C#指令碼:
通過Root物件的Inspector皮膚中的"Add Component"按鈕,將這個指令碼,掛接到Root物件下:
3.2. 實現
通過"Project"檢視的右鍵選單中開啟這個C#工程,可以看到我們新增的指令碼"Main.CS":
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
這個指令碼提供了兩個方法:
- Start()表示初始化(第一幀)的時候需要更新的內容,通常用於初始化之後不再更新的內容。比如我們會在這裡繪製一個物體。
- Update()表示每一幀都需要實時更新的內容,比如相機與滑鼠鍵盤事件的互動。
那麼就在Start()中進行繪製一個三角形的操作:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
GameObject main = GameObject.Find("/Root");
if (main == null)
{
return;
}
GameObject triangleGameObject = GreateTriangle();
triangleGameObject.transform.parent = main.transform;
}
GameObject GreateTriangle()
{
string name = "triangle";
Mesh mesh = new Mesh();
mesh.name = name;
Vector3[] vertices = new Vector3[3]
{
new Vector3(0, 0, 0),
new Vector3(0, 10, 0),
new Vector3(10, 0, 0)
};
mesh.vertices = vertices;
int[] triangles = new int[3] { 0, 1, 2 };
mesh.triangles = triangles;
GameObject triangleGameObject = new GameObject(name);
MeshFilter mf = triangleGameObject.AddComponent<MeshFilter>();
mf.sharedMesh = mesh;
MeshRenderer meshRenderer = triangleGameObject.AddComponent<MeshRenderer>();
return triangleGameObject;
}
// Update is called once per frame
void Update()
{
}
}
3.3. 解析
3.3.1. 場景樹物件
在Start()函式中,首先我們找到了場景根節點Root,然後又通過呼叫GreateTriangle()函式,建立了一個三角形的GameObject物件,最後把這個三角形物件掛接到Root物件下:
void Start()
{
GameObject main = GameObject.Find("/Root");
if (main == null)
{
return;
}
GameObject triangleGameObject = GreateTriangle();
triangleGameObject.transform.parent = main.transform;
}
可以看到子物件掛接到父物件是通過GameObject物件中Transform物件來掛接的,這其實體現了一種思維的體現:Transform其實是表達GameObject物件空間位置的的4X4矩陣,父節點設定Transform會影響到子節點的位置,子節點的初始位置都是基於父節點的Transform開始計算的。通過這種方式,再複雜的場景也可以組織成一個場景樹節點:
3.3.2. 繪製方法
經過圖形技術的多年發展,現在大部分影像渲染引擎都會把渲染的物體封裝成兩種物件:渲染物體的骨架封裝成Mesh(網格),因為絕大多數物體都是通過一個個三角面片渲染出來的;渲染物體的血肉封裝成Material(材質),影響最終渲染的效果,如物體的光感、質地。
所以,為了繪製一個三角形,當然應該先繪製一個Mesh:
GameObject GreateTriangle()
{
string name = "triangle";
Mesh mesh = new Mesh();
mesh.name = name;
Vector3[] vertices = new Vector3[3]
{
new Vector3(0, 0, 0),
new Vector3(0, 10, 0),
new Vector3(10, 0, 0)
};
mesh.vertices = vertices;
int[] triangles = new int[3] { 0, 1, 2 };
mesh.triangles = triangles;
//...
}
這裡,我們給Mesh傳入了三個頂點,以及頂點的三角面索引。三角面索引表示的是按照索引的順序,通過頂點進行繪製,這樣就可以使用較少的頂點進行繪製,節約空間,畢竟Mesh中很多三角面片是共頂點的。
接下來,給GameObject增加一個MeshFilter元件,通過這個元件掛接剛建立的Mesh;給GameObject增加一個MeshRenderer元件,這個元件是用來掛接Material的,不過暫時沒有用上Material(但是必須增加MeshRenderer元件,否則不會顯示物體)。
GameObject GreateTriangle()
{
//...
GameObject triangleGameObject = new GameObject(name);
MeshFilter mf = triangleGameObject.AddComponent<MeshFilter>();
mf.sharedMesh = mesh;
MeshRenderer meshRenderer = triangleGameObject.AddComponent<MeshRenderer>();
return triangleGameObject;
}
4. 結果
點選"Play",執行結果如下: