こんにちは、エンジニアのオオバです。

この記事ではカプセルメッシュをUnityプログラミングで作ってみます。

【Unity】プログラミングでカプセルメッシュの作り方_0

3Dプログラミング初心者にとってはハードルが高く感じるかもしません。正直最初からカプセルメッシュは難しいです。最初は基礎にあたる三角形メッシュから入ることをオススメします。 三角形は3頂点しかなく理解しやすい からです。次の記事で詳しく三角形メッシュをUnityで作る方法を紹介しています。ぜひこちらを読んでみてください。

話を戻してカプセルメッシュは球メッシュの応用編です。 カプセルは球メッシュの側面を頂点を伸ばしただけです。
つまり球メッシュの作り方がわかっていれば、カプセルメッシュは簡単です。

球メッシュの作り方は次の記事で詳しく解説しています。本記事も球メッシュの作り方がわかっている前提で話が進みますので、ぜひ読んでみてください。

カプセルメッシュは球メッシュと作り方はほぼ同じ

結論から話すと、カプセルメッシュは球メッシュとほぼソースコードが同じです。カプセルメッシュは球を元にして考えると理解しやすいです。ポイントは 頂点座標をカプセルの高さ分移動(オフセット)させること です。

分割数を減らした球メッシュ

【Unity】プログラミングでカプセルメッシュの作り方_1

カプセルと球の違いは、赤枠で囲った頂点を2回描画して分割することです。この分割した「隙間」を 高さ(Height) としましょう。球を真ん中で分割するため、上下に球は分離します。

【Unity】プログラミングでカプセルメッシュの作り方_2

上半球 は高さを2で割った数値を加算、 下半球 は高さを2で割った数値を減算することで、頂点座標は算出できるのです。この考え方を基本にすることで、 球メッシュのソースコードをほとんど再利用できます。

では、少し長いですがソースコード全文を見てみましょう。

ソースコード全文の共有

using System.Collections;  
using System.Collections.Generic;  
using UnityEngine;  

[RequireComponent(typeof(MeshFilter))]  
[RequireComponent(typeof(MeshRenderer))]  
public class Capsule : MonoBehaviour  
{
    private MeshRenderer _renderer;  
    private MeshRenderer Renderer => _renderer != null ? _renderer : (_renderer = GetComponent<MeshRenderer>());  

    private MeshFilter _filter;  
    private MeshFilter Filter => _filter != null ? _filter : (_filter = GetComponent<MeshFilter>());  


    [SerializeField] private Vector2Int _divide = new Vector2Int(20, 20);  

    /// <summary>高さ</summary>  
    [SerializeField] public float _height = 1f;  

    /// <summary>半径</summary>  
    [SerializeField] float _radius = 0.5f;  

    void Start() => Create();  

    void Create()  
    {
        int divideH = _divide.x;  
        int divideV = _divide.y;  

        var data = CreateCapsule(divideH, divideV, _height, _radius);  
        var mesh.SetVertices(data.vertices);  
        mesh.SetIndices(data.indices, MeshTopology.Triangles, 0);  
        Filter.mesh = mesh;  
        mesh.RecalculateNormals();  
    }

    struct MeshData  
    {
        public Vector3[] vertices;  
        public int[] indices;  
    }

    /// <summary>  
    /// カプセルメッシュデータを作成  
    /// </summary>  
    MeshData CreateCapsule(int divideH, int divideV, float height = 1f, float radius = 0.5f)  
    {
        divideH = divideH < 4 ? 4 : divideH;  
        divideV = divideV < 4 ? 4 : divideV;  
        radius = radius <= 0 ? 0.001f : radius;  

        // 偶数のみ有効  
        if (divideV % 2 != 0)  
        {
            divideV++;  
        }

        int cnt = 0;  

        // =============================  
        // 頂点座標作成  
        // =============================  

        int vertCount = divideH * divideV + 2;  
        var vertices = new Vector3[vertCount];  

        // 中心角  
        float centerEulerRadianH = 2f * Mathf.PI / (float) divideH;  
        float centerEulerRadianV = 2f * Mathf.PI / (float) divideV;  

        float offsetHeight = height * 0.5f;  

        // 天面  
        vertices[cnt++] = new Vector3(0, radius + offsetHeight, 0);  

        // カプセル上部  
        for (int vv = 0; vv < divideV / 2; vv++)  
        {
            var vRadian = (float) (vv + 1) * centerEulerRadianV / 2f;  

            // 1辺の長さ  
            var tmpLen = Mathf.Abs(Mathf.Sin(vRadian) * radius);  

            var y = Mathf.Cos(vRadian) * radius;  
            for (int vh = 0; vh < divideH; vh++)  
            {
                var pos = new Vector3(  
                    tmpLen * Mathf.Sin((float) vh * centerEulerRadianH),  
                    y + offsetHeight,  
                    tmpLen * Mathf.Cos((float) vh * centerEulerRadianH)  
                );  
                // サイズ反映  
                vertices[cnt++] = pos;  
            }
        }

        // カプセル下部  
        int offset = divideV / 2;  
        for (int vv = 0; vv < divideV / 2; vv++)  
        {
            var yRadian = (float) (vv + offset) * centerEulerRadianV / 2f;  

            // 1辺の長さ  
            var tmpLen = Mathf.Abs(Mathf.Sin(yRadian) * radius);  

            var y = Mathf.Cos(yRadian) * radius;  
            for (int vh = 0; vh < divideH; vh++)  
            {
                var pos = new Vector3(  
                    tmpLen * Mathf.Sin((float) vh * centerEulerRadianH),  
                    y - offsetHeight,  
                    tmpLen * Mathf.Cos((float) vh * centerEulerRadianH)  
                );  
                // サイズ反映  
                vertices[cnt++] = pos;  
            }
        }

        // 底面  
        vertices[cnt] = new Vector3(0, -radius - offsetHeight, 0);  

        // =============================  
        // インデックス配列作成  
        // =============================  

        int topAndBottomTriCount = divideH * 2;  
        // 側面三角形の数  
        int aspectTriCount = divideH * (divideV - 2 + 1) * 2;  

        int[] indices = new int[(topAndBottomTriCount + aspectTriCount) * 3];  

        //天面  
        int offsetIndex = 0;  
        cnt = 0;  
        for (int i = 0; i < divideH * 3; i++)  
        {
            if (i % 3 == 0)  
            {
                indices[cnt++] = 0;  
            }
            else if (i % 3 == 1)  
            {
                indices[cnt++] = 1 + offsetIndex;  
            }
            else if (i % 3 == 2)  
            {
                var index = 2 + offsetIndex++;  
                // 蓋をする  
                index = index > divideH ? indices[1] : index;  
                indices[cnt++] = index;  
            }
        }

        // 側面Index  

        /* 頂点を繋ぐイメージ  
         * 1 - 2  
         * |   |  
         * 0 - 3  
         *  
         * 0, 1, 2  
         * 0, 2, 3  
         *  
         * 注意 : 1周した時にClampするのを忘れないように。  
         */  

        // 開始Index番号  
        int startIndex = indices[1];  

        // 天面、底面を除いたカプセルIndex要素数  
        int sideIndexLen = aspectTriCount * 3;  

        int lap1stIndex = 0;  

        int lap2ndIndex = 0;  

        // 一周したときのindex数  
        int lapDiv = divideH * 2 * 3;  

        int createSquareFaceCount = 0;  

        for (int i = 0; i < sideIndexLen; i++)  
        {
            // 一周の頂点数を超えたら更新(初回も含む)  
            if (i % lapDiv == 0)  
            {
                lap1stIndex = startIndex;  
                lap2ndIndex = startIndex + divideH;  
                createSquareFaceCount++;  
            }

            if (i % 6 == 0 || i % 6 == 3)  
            {
                indices[cnt++] = startIndex;  
            }
            else if (i % 6 == 1)  
            {
                indices[cnt++] = startIndex + divideH;  
            }
            else if (i % 6 == 2 || i % 6 == 4)  
            {
                if (i > 0 &&  
                    (i % (lapDiv * createSquareFaceCount - 2) == 0 ||  
                     i % (lapDiv * createSquareFaceCount - 4) == 0)  
                )  
                {
                    // 1周したときのClamp処理  
                    // 周回ポリゴンの最後から2番目のIndex  
                    indices[cnt++] = lap2ndIndex;  
                }
                else  
                {
                    indices[cnt++] = startIndex + divideH + 1;  
                }
            }
            else if (i % 6 == 5)  
            {
                if (i > 0 && i % (lapDiv * createSquareFaceCount - 1) == 0)  
                {
                    // 1周したときのClamp処理  
                    // 周回ポリゴンの最後のIndex  
                    indices[cnt++] = lap1stIndex;  
                }
                else  
                {
                    indices[cnt++] = startIndex + 1;  
                }

                // 開始Indexの更新  
                startIndex++;  
            }
            else  
            {
                Debug.LogError("Invalid : " + i);  
            }
        }


        // 底面Index  
        offsetIndex = vertices.Length - 1 - divideH;  
        lap1stIndex = offsetIndex;  
        var finalIndex = vertices.Length - 1;  
        int len = divideH * 3;  

        for (int i = len - 1; i >= 0; i--)  
        {
            if (i % 3 == 0)  
            {
                // 底面の先頂点  
                indices[cnt++] = finalIndex;  
                offsetIndex++;  
            }
            else if (i % 3 == 1)  
            {
                indices[cnt++] = offsetIndex;  
            }
            else if (i % 3 == 2)  
            {
                var value = 1 + offsetIndex;  
                if (value >= vertices.Length - 1)  
                {
                    value = lap1stIndex;  
                }

                indices[cnt++] = value;  
            }
        }


        return new MeshData()  
        {
            indices = indices,  
            vertices = vertices  
        };  
    }
}

こちらのコードをGameObjectにアタッチすることでカプセルは表示されます。今回はほとんど解説を割愛しました。ほぼ球メッシュを作る記事で解説してしまったからです。

カプセルメッシュの作り方がわからなかった方は、こちらの球メッシュの作り方をぜひ参考にしてみてください。

そもそも3Dプログラミング初心者の方は、基礎にあたる三角形メッシュの作り方から始めましょう。三角形が分かれば球もカプセルもただの応用です。次の記事は三角形メッシュの作り方を初心者向けに分かりやすく解説しています。ぜひ読んでみてください。

この記事があなたのゲーム開発に少しでもお役に立てたら嬉しいです。

「Unity初心者大学」というUnity初心者向けのYouTube始めました!!
ぜひチャンネル登録をお願いします!

最後まで読んでいただきありがとうございました!
すばらしい3Dプログラミングライフをお過ごしください。

オススメ記事
検証環境