Unity的立體幾何問題
今天我們來講一點有趣的立體幾何問題。
在可視化與可視化分析中,基礎的幾何圖形繪製是一切的基礎,比如常見的餅狀圖由扇形組成,柱狀圖由長方形組成,玫瑰圖由弧形組成。在2D的世界裡,這些都是SVG和Canvas繪圖的基礎。在3D的世界中,前端庫three.js也提供了與之對應的3D繪製方法,但是在Unity中對於3D基礎模型的提供支持卻相對較少,需要專門編寫代碼來生成。今天我們就講一講在Unity裡面通過繪製這些基礎3D結構的方法,同時也聊一聊這些基本3D結構的數學繪製邏輯。
在Unity繪圖中,有Mesh和Render兩部分組成,Mesh用於描述物體的結構,Render描述這個物體該繪製成什麼顏色。今天主要講Mesh。
Mesh是由頂點和這些頂點構成的基礎三角形組成。
頂點à三角形。
正X面體
對於我們來說,最基礎的3D結構應該是正方體,也就是正6面體。由8個頂點,6個正方形組成。這6個正方形又可以被拆分成12個等腰直角三角形。那麼描述一個正方體可以表示為:
Vector3 p0 = new Vector3(0.5f, 0.5f, 0.5f).normalized;Vector3 p1 = new Vector3(-0.5f, 0.5f, 0.5f).normalized;Vector3 p2 = new Vector3(-0.5f, -0.5f, 0.5f).normalized;Vector3 p3 = new Vector3(0.5f, -0.5f, 0.5f).normalized;Vector3 p4 = new Vector3(0.5f, 0.5f, -0.5f).normalized;Vector3 p5 = new Vector3(-0.5f, 0.5f, -0.5f).normalized;Vector3 p6 = new Vector3(-0.5f, -0.5f, -0.5f).normalized;Vector3 p7 = new Vector3(0.5f, -0.5f, -0.5f).normalized;mesh.vertices = new Vector3[] { p0, p1, p2, p3, p4, p5 };mesh.triangles = new int[]{ 0,1,2, 0,2,3, 1,5,6 1,6,2, 5,4,7, 5,7,6, 4,0,3 4,3,7, 0,4,1, 4,5,1, 3,2,6, 3,6,7};
繪製效果:
最基礎的3D物體是正4面體,也就是每個面都是正三角形的三稜錐。由4個頂點,4個三角形組成。那麼描述一個正4面體可以表示為:
Vector3 p0 = new Vector3(0, 0, 0);Vector3 p1 = new Vector3(1, 0, 0);Vector3 p2 = new Vector3(0.5f, 0, Mathf.Sqrt(0.75f));Vector3 p3 = new Vector3(0.5f, Mathf.Sqrt(0.75f), Mathf.Sqrt(0.75f) / 3);Vector3 center = (p0 + p1 + p2 + p3) / 4;p0 = (p0 - center).normalized;p1 = (p1 - center).normalized;p2 = (p2 - center).normalized;p3 = (p3 - center).normalized;mesh.vertices = new Vector3[] { p0, p1, p2, p3 };mesh.triangles = new int[]{ 0,1,2, 0,2,3, 2,1,3, 0,3,1};
繪製效果:
正8面體,由8個正三角形組成,其中有6個頂點。那麼一個正8面體可以描述為:
Vector3 p0 = new Vector3(1, 0, 0);Vector3 p1 = new Vector3(0, 1, 0);Vector3 p2 = new Vector3(0, 0, 1);Vector3 p3 = new Vector3(-1, 0, 0);Vector3 p4 = new Vector3(0, -1, 0);Vector3 p5 = new Vector3(0, 0, -1);mesh.vertices = new Vector3[] { p0, p1, p2, p3, p4, p5 };mesh.triangles = new int[]{ 2,0,1, 3,2,1, 5,3,1, 0,5,1, 3,5,4, 5,0,4, 0,2,4, 2,3,4};
繪製效果:
前面都還比較簡單,正20面體,由20個正三角形組成,其中有12個頂點。對於一般人而言,這個時候想在頭腦中想像出來正20面體的樣子還是有點難的,這裡我教大家怎麼繪製正20面體。
首先,準備3塊一樣大小的長方形紙板,其長寬比為(√5 + 1):2,按照如圖的方式進行組合:
其頂點連接組成的形狀就是一個正20面體,單獨對正20面體的任意一個角進行觀察,這個角周圍貼合了5個正三角形。就像這個樣子:
根據上面的三個紙板參考的方式,我們可以容易的描述一個正20面體:
Vector3 p0 = new Vector3(Mathf.Sqrt(5f) + 1f, 2f, 0).normalized;Vector3 p1 = new Vector3(Mathf.Sqrt(5f) + 1f, -2f, 0).normalized;Vector3 p2 = new Vector3(-Mathf.Sqrt(5f) - 1f, -2f, 0).normalized;Vector3 p3 = new Vector3(-Mathf.Sqrt(5f) - 1f, 2f, 0).normalized;Vector3 p4 = new Vector3(2f, 0, Mathf.Sqrt(5f) + 1f).normalized;Vector3 p5 = new Vector3(-2f, 0, Mathf.Sqrt(5f) + 1f).normalized;Vector3 p6 = new Vector3(-2f, 0, -Mathf.Sqrt(5f) - 1f).normalized;Vector3 p7 = new Vector3(2f, 0, -Mathf.Sqrt(5f) - 1f).normalized;Vector3 p8 = new Vector3(0, Mathf.Sqrt(5f) + 1f, 2f).normalized;Vector3 p9 = new Vector3(0, Mathf.Sqrt(5f) + 1f, -2f).normalized;Vector3 p10 = new Vector3(0, -Mathf.Sqrt(5f) - 1f, -2f).normalized;Vector3 p11 = new Vector3(0, -Mathf.Sqrt(5f) - 1f, 2f).normalized;mesh.vertices = new Vector3[] { p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11 };mesh.triangles = new int[]{ 0,8,4, 0,4,1, 0,1,7, 0,7,9, 0,9,8, 2,6,10, 2,3,6, 2,5,3, 2,11,5, 2,10,11, 1,11,10, 1,4,11, 11,4,5, 5,4,8, 3,5,8, 3,8,9, 3,9,6, 6,9,7, 6,7,10, 10,7,1};
繪製出來效果如下:
好了,除了正12面體以外,基礎的正X面體的繪製方法已經講完了。
柱體
基礎的3D圖形還有哪些呢?
長方體,長方體可以通過正方體的Scale變換而輕易得到,這裡不多說。
圓柱,怎麼通過點和線來描述一個圓柱體呢?
首先我們知道一個圓可以拆分成很多個同心小扇形,在扇形角度很小的時候,可以用三角形來代替扇形。通過將圓形拉長就可以得到一個圓柱。描述代碼如下:
mesh.vertices = new Vector3[(SectorNumber + 1) * 2];mesh.triangles = new int[SectorNumber * 12];Vector3[] vertices = mesh.vertices;int[] triangles = mesh.triangles;vertices[0].x = 0;vertices[0].y = Height / 2;vertices[0].z = 0;float tmpArc;int tmpCount = 0;for (int i = 0; i < SectorNumber; ++i){ tmpArc = 2 * Mathf.PI * i / SectorNumber; vertices[i + 1].x = Mathf.Sin(tmpArc) * Radius; vertices[i + 1].y = Height / 2; vertices[i + 1].z = Mathf.Cos(tmpArc) * Radius; triangles[tmpCount++] = 0; triangles[tmpCount++] = i + 1; triangles[tmpCount++] = i != (SectorNumber - 1) ? i + 2 : 1;}for(int i = SectorNumber; i < SectorNumber*2; ++i){ tmpArc = 2 * Mathf.PI * i / SectorNumber; vertices[i + 1].x = Mathf.Sin(tmpArc) * Radius; vertices[i + 1].y = -Height / 2; vertices[i + 1].z = Mathf.Cos(tmpArc) * Radius; triangles[tmpCount++] = SectorNumber * 2 + 1; triangles[tmpCount++] = i != (SectorNumber * 2 - 1) ? i + 2 : SectorNumber + 1; triangles[tmpCount++] = i + 1;}vertices[SectorNumber * 2 + 1].x = 0;vertices[SectorNumber * 2 + 1].y = -Height / 2;vertices[SectorNumber * 2 + 1].z = 0;for(int i = 1; i < SectorNumber; ++i){ triangles[tmpCount++] = i; triangles[tmpCount++] = i + SectorNumber; triangles[tmpCount++] = i + 1; triangles[tmpCount++] = i + 1; triangles[tmpCount++] = i + SectorNumber; triangles[tmpCount++] = i + SectorNumber + 1;}triangles[tmpCount++] = SectorNumber;triangles[tmpCount++] = 2 * SectorNumber;triangles[tmpCount++] = 1;triangles[tmpCount++] = 1;triangles[tmpCount++] = 2 * SectorNumber;triangles[tmpCount++] = SectorNumber + 1;mesh.vertices = vertices;mesh.triangles = triangles;
繪製效果如下(可以通過控制SectorNumber參數來控制圓柱的精細程度,左邊24個扇區,右邊12個扇區):
同樣,弧形柱的繪製方式也和圓柱相似,代碼:
mesh.vertices = new Vector3[(pieces + 1) * 4];Vector3[] vertices = mesh.vertices;mesh.triangles = new int[(pieces*8 + 4)*3];float CurrentArc = 0;for (int i = 0; i <= pieces; ++i){ CurrentArc = i * Arc / pieces + StartArc; vertices[i * 4].x = outterRadius * Mathf.Sin(CurrentArc * Mathf.PI / 180); vertices[i * 4].z = outterRadius * Mathf.Cos(CurrentArc * Mathf.PI / 180); vertices[i * 4].y = - Height / 2; vertices[i * 4 + 1].x = outterRadius * Mathf.Sin(CurrentArc * Mathf.PI / 180); vertices[i * 4 + 1].z = outterRadius * Mathf.Cos(CurrentArc * Mathf.PI / 180); vertices[i * 4 + 1].y = Height / 2; vertices[i * 4 + 2].x = innerRadius * Mathf.Sin(CurrentArc * Mathf.PI / 180); vertices[i * 4 + 2].z = innerRadius * Mathf.Cos(CurrentArc * Mathf.PI / 180); vertices[i * 4 + 2].y = Height / 2; vertices[i * 4 + 3].x = innerRadius * Mathf.Sin(CurrentArc * Mathf.PI / 180); vertices[i * 4 + 3].z = innerRadius * Mathf.Cos(CurrentArc * Mathf.PI / 180); vertices[i * 4 + 3].y = - Height / 2;}mesh.vertices = vertices;int[] triangles = mesh.triangles;for (int i = 0; i < pieces; ++i){ triangles[i * 24] = i * 4; triangles[i * 24 + 1] = (i + 1) * 4; triangles[i * 24 + 2] = (i + 1) * 4 + 1; triangles[i * 24 + 3] = i * 4; triangles[i * 24 + 4] = (i + 1) * 4 + 1; triangles[i * 24 + 5] = i * 4 + 1; triangles[i * 24 + 6] = i * 4 + 1; triangles[i * 24 + 7] = (i + 1) * 4 + 1; triangles[i * 24 + 8] = (i + 1) * 4 + 2; triangles[i * 24 + 9] = i * 4 + 1; triangles[i * 24 + 10] = (i + 1) * 4 + 2; triangles[i * 24 + 11] = i * 4 + 2; triangles[i * 24 + 12] = i * 4 + 2; triangles[i * 24 + 13] = (i + 1) * 4 + 2; triangles[i * 24 + 14] = (i + 1) * 4 + 3; triangles[i * 24 + 15] = i * 4 + 2; triangles[i * 24 + 16] = (i + 1) * 4 + 3; triangles[i * 24 + 17] = i * 4 + 3; triangles[i * 24 + 18] = i * 4 + 3; triangles[i * 24 + 19] = (i + 1) * 4 + 3; triangles[i * 24 + 20] = (i + 1) * 4; triangles[i * 24 + 21] = i * 4 + 3; triangles[i * 24 + 22] = (i + 1) * 4; triangles[i * 24 + 23] = i * 4;}triangles[pieces * 24] = 0;triangles[pieces * 24 + 1] = 1;triangles[pieces * 24 + 2] = 3;triangles[pieces * 24 + 3] = 1;triangles[pieces * 24 + 4] = 2;triangles[pieces * 24 + 5] = 3;triangles[pieces * 24 + 6] = 4 * pieces;triangles[pieces * 24 + 7] = 4 * pieces + 2;triangles[pieces * 24 + 8] = 4 * pieces + 1;triangles[pieces * 24 + 9] = 4 * pieces;triangles[pieces * 24 + 10] = 4 * pieces + 3;triangles[pieces * 24 + 11] = 4 * pieces + 2;mesh.triangles = triangles;
繪製效果:
梯形柱的繪製邏輯就是只有一個扇面的弧形柱,因此在此就不再多加贅述。
球體
在Unity3D世界中,球體也是通過一個一個小三角形來表示的。怎麼去生成一個由小三角形組成的球呢,常見的對於地球的劃分是經緯度方法,如圖:
這種分割球形表面的方法有個弊端,球靠近赤道兩邊的節點會很稀疏,靠近兩極的節點會很密集,因此在這裡不多做介紹。
比較好的做法是通過將正X面體每個面進行細分來擬近圓,對於正X面體的每個小三角面,按照每條邊的中點將其拆分成四個三角形,拆分方式如圖:
將拆分的新的四個三角形,貼合到球面上。然後依次迭代,就可以得到一個擬合不錯的「球」。
其拆分邏輯如下:
Vector3[] vectices = mesh.vertices;int[] triangles = mesh.triangles;int size1 = vectices.Length;int size2 = triangles.Length / 3;Vector3[] vectices2 = new Vector3[size1 + size2 * 3];int[] triangles2 = new int[size2 * 12];for (int i = 0; i < size1; ++i){ vectices2[i] = vectices[i];}for (int i = 0; i < size2; ++i){ Vector3 center1 = Mid(vectices[triangles[i * 3 + 1]], vectices[triangles[i * 3 + 2]]).normalized; Vector3 center2 = Mid(vectices[triangles[i * 3]], vectices[triangles[i * 3 + 2]]).normalized; Vector3 center3 = Mid(vectices[triangles[i * 3]], vectices[triangles[i * 3 + 1]]).normalized; vectices2[size1 + i * 3] = center1; vectices2[size1 + i * 3 + 1] = center2; vectices2[size1 + i * 3 + 2] = center3; triangles2[i * 12] = triangles[i * 3]; triangles2[i * 12 + 1] = size1 + i * 3 + 2; triangles2[i * 12 + 2] = size1 + i * 3 + 1; triangles2[i * 12 + 3] = size1 + i * 3 + 2; triangles2[i * 12 + 4] = triangles[i * 3 + 1]; triangles2[i * 12 + 5] = size1 + i * 3; triangles2[i * 12 + 6] = size1 + i * 3 + 1; triangles2[i * 12 + 7] = size1 + i * 3; triangles2[i * 12 + 8] = triangles[i * 3 + 2]; triangles2[i * 12 + 9] = size1 + i * 3; triangles2[i * 12 + 10] = size1 + i * 3 + 1; triangles2[i * 12 + 11] = size1 + i * 3 + 2;}mesh.Clear();mesh.vertices = vectices2;mesh.triangles = triangles2;
各種類型的正面體擬合效果:
正4面體(4個基礎三角面):
正8面體(8個基礎三角面):
正6面體(正方體)(12個基礎三角面):
正20面體:
看到這裡有人可能就會問了,為什麼沒有講正12面體,也就下面這位:
但是正12面體每個基本面是正五邊形,再拆分成三角形的話至少會有36個基礎三角形,因此不拿它來擬合球。
看到這,可能有人會問,小編講了這麼多到底要幹嘛?
其實我就是突然有一天,想畫一個海膽,就是下面這位:
但是Unity自帶的幾何結構很少,沒法畫。因此,自己動手寫代碼畫。
要畫這麼一個海膽,首先要能畫一個球,再將用於模擬球面的每個三角面通過下圖中的變化來突出刺來。
代碼邏輯如下:
Vector3[] vectices = mesh.vertices;int[] triangles = mesh.triangles;int size1 = vectices.Length;int size2 = triangles.Length / 3;Vector3[] vectices2 = new Vector3[size1 + size2];int[] triangles2 = new int[size2 * 9];for (int i = 0; i < size1; ++i){ vectices2[i] = vectices[i];}for (int i = 0; i < size2; ++i){ Vector3 center = Mid(vectices[triangles[i * 3]], vectices[triangles[i * 3 + 1]], vectices[triangles[i * 3 + 2]]).normalized; //vectices2[size1 + i] = center * (Random.Range(0f,1f)<0.6? 1: outterRadius); vectices2[size1 + i] = center * outterRadius; triangles2[i * 9] = triangles[i * 3]; triangles2[i * 9 + 1] = triangles[i * 3 + 1]; triangles2[i * 9 + 2] = size1 + i; triangles2[i * 9 + 3] = triangles[i * 3]; triangles2[i * 9 + 4] = size1 + i; triangles2[i * 9 + 5] = triangles[i * 3 + 2]; triangles2[i * 9 + 6] = size1 + i; triangles2[i * 9 + 7] = triangles[i * 3 + 1]; triangles2[i * 9 + 8] = triangles[i * 3 + 2];}mesh.Clear();mesh.vertices = vectices2;mesh.triangles = triangles2;
效果如下:
最後,可能有人想問,為什麼要畫這些?
怎麼說呢,因為好玩,就是這麼任性,有機會再跟大家分享一些其他的好玩的東西。
推薦閱讀:
※詩豪:劉禹錫寫詩用字分析(唐代詩人寫詩用字分析之三)
※一張圖告訴你全球葡萄酒都產自哪些國家
※Matplotlib畫三維曲線
※星巴克點亮全球:看看哪些城市星巴克最多
※AI演算法眼中的世界是什麼樣子?這些圖像或許能幫你更好地理解