Unity中用Mesh畫一個圓環
Probuider
前幾天在做一個小專案的時候,用到了Unity自帶的一個包ProBuilder其中的Arch生成1/4圓。
挺好玩的,可以在直接Unity中根據需要用Mesh定製生成圖形,而不用建模軟體。
但是存在一個小問題,就是在使用的時候他的中心點是在生成圖形的左下角。
旋轉的時候不符合我的需求,我想要的是生成的時候旋轉中心在圓心的位置,所以準備自己定製一個。
目標
關於Mesh生成圖形的原理可以參考這篇文章,講得雖然不算很詳細,但足夠了解基本概念了。
目標是生成下面圖中的一個1/4空心圓柱體
我們切換到Wireframe模式下,可以看出它是有一個一個的頂點,並通過一條條的直線連線起來。那麼我們如何確定這些頂點和線的位置呢?
小目標-生成一個面
其實很簡單的,我們一步一步慢慢來。一次生成一整個會有點麻煩,我們可以一面一面來。只要生成了第一個面,其他的面也是類似的方法生成就好。
在前面我們提到了我們要的是生成一個圓柱體,圓柱體一個的重要性質就是可以由一個圓形疊加產生,也就是隻要我們生成一個圓形,就完成了大部分的工作。
我們知道3D建模就是由一個一個的三角形組合成的,所以我們要用三角形來模擬來一個空心的圓。
在Probuilder中生成這樣一個空心圓柱體用的是Arch,它有幾個引數,分別是
\(\color{#1E90FF}{Radius}\) 半徑,圓心到最外圈的距離
\(\color{#1E90FF}{Thickness}\) 厚度,圓心到最外圈的距離-圓心到最內圈的距離
\(\color{#1E90FF}{Depth}\) 深度
\(\color{#1E90FF}{NumberOfSides}\) 由多少個面組成,面越多越平滑,效能也越差
\(\color{#1E90FF}{DrawArchDegrees}\) 總共繪製的角度
\(\color{#1E90FF}{NumberOfSides}\)中的面是指由兩個三角形一頭一尾拼成的梯形,多個頭大腳小的梯形拼在一起便成了我們需要的圓形。
原理已經知道了,那下一步只要確定三角形頂點的位置就OK了。至於如何確定三角形頂點的位置,我們可以再看下這張圖。
是不是瞬間清晰明瞭,紅線的交匯處就是圓心的位置,數字則是每個頂點的編號。
我們假設圓心在原點,數字0-1所在的線為180度線。\(\color{#1E90FF}{Increment}\) = \(\color{#1E90FF}{DrawArchDegrees}\)/\(\color{#1E90FF}{NumberOfSides}\)就是線與線之間的角度。每條線的角度可以由\(\color{#1E90FF}{180-Increment*i}\)得到。i為第幾條線。
線上的點可以由\(\color{#1E90FF}{y = r* sinθ, y = r* cosθ}\)得到。
//頂點座標
vertexList.Clear();
float incrementAngle = DrawArchDegrees / NumberOfSides;
//小於等於是因為n+1條線才能組成n個面
for (int i = 0; i <= NumberOfSides; i++)
{
float angle = 180 - i * incrementAngle;
float innerX = (Radius - Thickness) * Mathf.Cos(angle * Mathf.Deg2Rad);
float innerY = (Radius - Thickness) * Mathf.Sin(angle * Mathf.Deg2Rad);
vertexList.Add(new Vector3(innerX, innerY, 0));
float outsideX = Radius * Mathf.Cos(angle * Mathf.Deg2Rad);
float outsideY = Radius * Mathf.Sin(angle * Mathf.Deg2Rad);
vertexList.Add(new Vector3(outsideX, outsideY, 0));
}
在上面的程式碼中我們已經計算出了頂點的位置,下一步我們要做的是按順序插入三角形頂點的位置。從Mesh這篇文章中我們可以知道,只有是三角形是正面的情況下才會被渲染。
而正反面可以通過法線的朝向進行判斷,向外的面就是正面,相反的就是背面。
在Unity中,法線的朝向可以由左手法則得到。拿出左手,伸直,拇指與其他四個指頭垂直,然後四指彎曲,指尖朝向迴圈的方向,拇指就指向法線的方向。
也就是說在上圖中,我們想渲染三角形,順序應該是類似這樣的012,321, 234, 543。
//三角形索引
triangleList.Clear();
int direction = 1;
for (int i = 0; i < NumberOfSides * 2; i++)
{
int[] triangleIndexs = getTriangleIndexs(i, direction);
direction *= -1;
for (int j = 0; j < triangleIndexs.Length; j++)
{
triangleList.Add(triangleIndexs[j]);
}
}
\(\color{#F08080}{getTriangleIndexs}\)程式碼如下
int[] getTriangleIndexs(int index, int direction)
{
int[] triangleIndexs = new int[3] { 0,1,2};
for (int i = 0; i < triangleIndexs.Length; i++)
{
triangleIndexs[i] += index;
}
if (direction == -1)
{
int temp = triangleIndexs[0];
triangleIndexs[0] = triangleIndexs[2];
triangleIndexs[2] = temp;
}
return triangleIndexs;
}
至於uv座標就更簡單了,把內圈頂點uv座標中的Y固定為0,外圈頂點uv座標中的Y固定為1,而x座標由\(\color{#1E90FF}{1/NumberOfSides}\)得到:
//UV索引
uvList.Clear();
for (int i = 0; i <= NumberOfSides; i++)
{
float angle = 180 - i * incrementAngle;
float littleX = (1.0f / NumberOfSides) * i;
uvList.Add(new Vector2(littleX, 0));
float bigX = (1.0f / NumberOfSides) * i;
uvList.Add(new Vector2(bigX, 1));
}
完整程式碼如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//[RequireComponent(typeof(MeshFilter))]
//[RequireComponent(typeof(MeshRenderer))]
//[ExecuteInEditMode]
public class DrawArch : MonoBehaviour
{
public float Radius = 20.0f; //外圈的半徑
public float Thickness = 10.0f; //厚度,外圈半徑減去內圈半徑
public float Depth = 1.0f; //厚度
public float NumberOfSides = 30.0f; //由多少個面組成
public float DrawArchDegrees = 90.0f; //要繪畫多長
public Material archMaterial = null;
private List<Vector3> vertexList = new List<Vector3>();
private List<int> triangleList = new List<int>();
private List<Vector2> uvList = new List<Vector2>();
// Start is called before the first frame update
void Start()
{
GenerateVertex();
}
void GenerateVertex()
{
//頂點座標
vertexList.Clear();
float incrementAngle = DrawArchDegrees / NumberOfSides;
//小於等於是因為n+1條線才能組成n個面
for (int i = 0; i <= NumberOfSides; i++)
{
float angle = 180 - i * incrementAngle;
float innerX = (Radius - Thickness) * Mathf.Cos(angle * Mathf.Deg2Rad);
float innerY = (Radius - Thickness) * Mathf.Sin(angle * Mathf.Deg2Rad);
vertexList.Add(new Vector3(innerX, innerY, 0));
float outsideX = Radius * Mathf.Cos(angle * Mathf.Deg2Rad);
float outsideY = Radius * Mathf.Sin(angle * Mathf.Deg2Rad);
vertexList.Add(new Vector3(outsideX, outsideY, 0));
}
//三角形索引
triangleList.Clear();
int direction = 1;
for (int i = 0; i < NumberOfSides * 2; i++)
{
int[] triangleIndexs = getTriangleIndexs(i, direction);
direction *= -1;
for (int j = 0; j < triangleIndexs.Length; j++)
{
triangleList.Add(triangleIndexs[j]);
}
}
//UV索引
uvList.Clear();
for (int i = 0; i <= NumberOfSides; i++)
{
float angle = 180 - i * incrementAngle;
float littleX = (1.0f / NumberOfSides) * i;
uvList.Add(new Vector2(littleX, 0));
float bigX = (1.0f / NumberOfSides) * i;
uvList.Add(new Vector2(bigX, 1));
}
Mesh mesh = new Mesh()
{
vertices = vertexList.ToArray(),
uv = uvList.ToArray(),
triangles = triangleList.ToArray(),
};
mesh.RecalculateNormals();
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = archMaterial;
}
int[] getTriangleIndexs(int index, int direction)
{
int[] triangleIndexs = new int[3] { 0,1,2};
for (int i = 0; i < triangleIndexs.Length; i++)
{
triangleIndexs[i] += index;
}
if (direction == -1)
{
int temp = triangleIndexs[0];
triangleIndexs[0] = triangleIndexs[2];
triangleIndexs[2] = temp;
}
return triangleIndexs;
}
}
未完待續。。