1. 程式人生 > >Unity中用Mesh畫一個圓環

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;
    }
}

未完待續。。