Ugui上でスパイク形状を簡単に作成するためのコンポーネント

https://x.com/ScreenPocket/status/1744045427010514966?s=20

Ugui上で多角形や、スパイクの形状を作成したい時が有ります。
そういうテクスチャを張り付けてしまえば完成！…なのですが、**形状の精細さを担保するにはテクスチャの解像度が必要**だったり、**形状に合わせた画像ファイルが複数必要**だったりと、画像で対応しようとすると結構な手間が生じてしまいます。

そんな問題に対応するために、パラメトリックに形状を作成するためのコンポーネントを用意しました。

# コード

早速コードを張り付けます

```cs:SpikeBase.cs
using UnityEngine;
using UnityEngine.UI;

namespace ScreenPocket
{
    [RequireComponent(typeof(RectTransform))]
    [RequireComponent(typeof(CanvasRenderer))]
    public class SpikeBase : MaskableGraphic
    {
        /// <summary>
        /// テクスチャ（一応）
        /// </summary>
        [SerializeField]
        private Texture2D _texture;
        
        [SerializeField, Range(0f,1f)]
        private float _outsideRadius = 1f;
        [SerializeField, Range(0f,1f)]
        private float _insideRadius = 1f;

        [SerializeField, Range(3,64)]
        private int _vertexCount = 3;


        [SerializeField]
        private Color _outsideColor = Color.white;
        [SerializeField]
        private Color _insideColor = Color.white;
        [SerializeField]
        private Color _centerColor = Color.white;
        
        public override Texture mainTexture => _texture != null ? _texture : Texture2D.whiteTexture;
        
        protected override void OnPopulateMesh(VertexHelper vh)
        {
            var r = GetPixelAdjustedRect();

            var divDegree = 360f / _vertexCount;
            var degree = 0f;
            
            //スパイク外周側の頂点並びを作成する
            var outsidePositions = new Vector3[_vertexCount];
            var outsideUVs = new Vector2[_vertexCount];
            for (var i = 0 ; i < _vertexCount ; ++i)
            {
                //半径1で回す
                outsidePositions[i] = Quaternion.AngleAxis(degree, Vector3.forward) * Vector3.up * _outsideRadius;
                //UVは0.5倍して-0.5~0.5の範囲にしておく
                outsideUVs[i] = outsidePositions[i] * 0.5f;
                //位置は矩形サイズを加味しておく
                outsidePositions[i].x *= r.width * 0.5f;
                outsidePositions[i].y *= r.height * 0.5f;
                degree += divDegree;
            }

            //スパイク内周側の頂点並びを作成する
            var insidePositions = new Vector3[_vertexCount];
            var insideUVs = new Vector2[_vertexCount];
            for (var i = 0 ; i < _vertexCount-1 ; ++i)
            {
                //外周2頂点の中心点を内周基準点とする
                insidePositions[i] = (outsidePositions[i] + outsidePositions[i+1]) * 0.5f * _insideRadius;
                insideUVs[i] = (outsideUVs[i] + outsideUVs[i+1]) * 0.5f * _insideRadius;
            }
            //最後の中点は、末尾と最初の頂点座標から
            insidePositions[_vertexCount - 1] = (outsidePositions[_vertexCount - 1] + outsidePositions[0]) * 0.5f * _insideRadius;
            insideUVs[_vertexCount - 1] = (outsideUVs[_vertexCount - 1] + outsideUVs[0]) * 0.5f * _insideRadius;

            //頂点組み立てここから
            vh.Clear();
            //中心の点
            vh.AddVert(r.center, _centerColor * color, new Vector4(0.5f,0.5f));
            //外周と内周を交互に並べる
            for (var i = 0; i < _vertexCount; ++i)
            {
                //UVは-0.5~0.5の範囲になっているのでずらす
                var outsideUV = outsideUVs[i] + new Vector2(0.5f, 0.5f);
                var insideUV = insideUVs[i] + new Vector2(0.5f, 0.5f);
                vh.AddVert(outsidePositions[i], _outsideColor * color, outsideUV);
                vh.AddVert(insidePositions[i], _insideColor * color, insideUV);
            }
            
            //ファンのトポロジで面を貼る
            for (var i = 0; i < _vertexCount; ++i)
            {
                vh.AddTriangle(0, 1+i*2, 2+i*2);
                if (i < _vertexCount - 1)
                {
                    vh.AddTriangle(0, 2 + i * 2, 3 + i * 2);
                }
            }
            //ファンを閉じる
            vh.AddTriangle(0, _vertexCount*2, 1);
        }
    }
}

```
MaskableGraphicから派生して、OnPopulateMesh()で面を張る事で、UGUI上でメッシュ描画を行うことが出来ます。
過去の記事

https://qiita.com/ScreenPocket/items/1741c1c60df69022f000

でも扱った通りですね。

SpikeBase.csというファイルを作成し、上記のコピペで動くようになるかと思います。
>※「何でBase？」かと言うと、ココから派生する事も有るかもしれないのでとりあえずBaseと付けましたが、正直**ただのSpike**でも**SpikeUgui**でも良いかと思います。名前はご自由にリネームしてください。

# コードの説明
一応コードの説明も軽くしておきます。
今回のコードは大体4つの部分に分けられますね。

* 外周円を作る
* 内周円を作る
* 頂点を並べる
* 面を貼る

図を交えて解説していきましょう
## 外周円を作る

先ずはRectTransformの内接円を基準にした多角形を構成します。
頂点数が3なら3角形、4なら4角形という単純な頂点位置の配列です。
真上に頂点を配置したいので、Vector3.upを回転させながら位置を求めましょう

```cs:外周円を作っている部分
            var r = GetPixelAdjustedRect();

            var divDegree = 360f / _vertexCount;
            var degree = 0f;
            
            //スパイク外周側の頂点並びを作成する
            var outsidePositions = new Vector3[_vertexCount];
            var outsideUVs = new Vector2[_vertexCount];
            for (var i = 0 ; i < _vertexCount ; ++i)
            {
                //半径1で回す
                outsidePositions[i] = Quaternion.AngleAxis(degree, Vector3.forward) * Vector3.up * _outsideRadius;
                //UVは0.5倍して-0.5~0.5の範囲にしておく
                outsideUVs[i] = outsidePositions[i] * 0.5f;
                //位置は矩形サイズを加味しておく
                outsidePositions[i].x *= r.width * 0.5f;
                outsidePositions[i].y *= r.height * 0.5f;
                degree += divDegree;
            }
```
Vector3.upをdivDegreeずつ回して、outsidePositionを求めていきます。
3角形で考えるとこんな感じ↓
![20240108a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/129e3df8-7ece-73cd-e366-2c7fed6d10a7.png)
_outsideRadiusで長さの調整も行います。
UVについては後述しますが、位置座標とテクスチャマッピング座標に差が出来てしまうので、値を0.5倍した上で別バッファに置いておきます。
最後にRectTransformの矩形の幅(r.width,r.height)で補正した位置にして完成です。

## 内周円を作る

スパイクのトゲを作成するために凹ませる必要が有りますので、先ほど求めた位置の2点毎の辺の真ん中の位置を確保しておきます。

```cs:内周円を作っている部分
            //スパイク内周側の頂点並びを作成する
            var insidePositions = new Vector3[_vertexCount];
            var insideUVs = new Vector2[_vertexCount];
            for (var i = 0 ; i < _vertexCount-1 ; ++i)
            {
                //外周2頂点の中心点を内周基準点とする
                insidePositions[i] = (outsidePositions[i] + outsidePositions[i+1]) * 0.5f * _insideRadius;
                insideUVs[i] = (outsideUVs[i] + outsideUVs[i+1]) * 0.5f * _insideRadius;
            }
            //最後の中点は、末尾と最初の頂点座標から
            insidePositions[_vertexCount - 1] = (outsidePositions[_vertexCount - 1] + outsidePositions[0]) * 0.5f * _insideRadius;
            insideUVs[_vertexCount - 1] = (outsideUVs[_vertexCount - 1] + outsideUVs[0]) * 0.5f * _insideRadius;
```
外周円の2つの頂点の真ん中を、内周円の基準点とします。
![20240108b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/2a415da6-8d52-3d14-e1b1-d5226835ac46.png)

_insideRadiusで凹み具合も調整できるように。
UVも位置座標と全く同じような感じで確保しておきましょう。

## 頂点を並べる

ここまでで求まった位置座標、UV座標を使って頂点を並べましょう。
```cs:頂点を並べている部分
            //頂点組み立てここから
            vh.Clear();
            //中心の点
            vh.AddVert(r.center, _centerColor * color, new Vector4(0.5f,0.5f));
            //外周と内周を交互に並べる
            for (var i = 0; i < _vertexCount; ++i)
            {
                //UVは-0.5~0.5の範囲になっているのでずらす
                var outsideUV = outsideUVs[i] + new Vector2(0.5f, 0.5f);
                var insideUV = insideUVs[i] + new Vector2(0.5f, 0.5f);
                vh.AddVert(outsidePositions[i], _outsideColor * color, outsideUV);
                vh.AddVert(insidePositions[i], _insideColor * color, insideUV);
            }
```
後で「扇状に面を貼って円を構成する」つもりなので、「中心に頂点を1つ置いた上で、外周、内周を交互に頂点として追加」していきます。
![20240108c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/71a72910-4903-43c3-467b-eb8aecc0a929.png)
こんな感じで。

UV座標については矩形上0～1の値にマッピングする事で正しく張り付けられるようになりますので、-0.5~0.5だったものに0.5を加えて、0~1に補正しましょう。
![20240108d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/bdca60d2-11dd-eb35-6bd6-096ee1d62795.png)

※テクスチャマッピング座標については過去の記事

https://qiita.com/ScreenPocket/items/dab8d785859faa05ed18

でも触れています。

色も乗算するのもお忘れなく。

## 面を貼る
最後に扇状に面を貼っていきます。
```cs:面を張っている部分
            //ファンのトポロジで面を貼る
            for (var i = 0; i < _vertexCount; ++i)
            {
                vh.AddTriangle(0, 1+i*2, 2+i*2);
                if (i < _vertexCount - 1)
                {
                    vh.AddTriangle(0, 2 + i * 2, 3 + i * 2);
                }
            }
            //ファンを閉じる
            vh.AddTriangle(0, _vertexCount*2, 1);
```
扇状、いわゆるファンのトポロジのように面を張りましょう。


DirectXで言う所のコチラ↓

https://learn.microsoft.com/ja-jp/windows/win32/direct3d9/triangle-fans

> と思ったのですが、DirectX10ではTRIANGLE_FANがサポートされなくなったってマジですか。
>どおりで「トポロジー ファン」で検索しても引っかからないわけだ…。
>ただ、もうコメントにトポロジと記載してしまったので、トポロジの誤用だったとしてもそのまま進めさせて頂きます。

で、話を戻しますが、シンプルに順番にクルクルと3角形を構成していっています。
![20240108e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/00d62faf-328c-c673-bdf9-ae0a7c6997ed.png)

最後の1面だけは、最初の外周円の座標(1番目)とくっつけて円を閉じるのを忘れずに。

# おわりに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/bf3d6e85-1641-23d3-e58f-313bf6ef5327.png)
↑RectTransformの形状を基準にしているので、楕円形状にしたり傾けたりできるぞ！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/eff03e86-7aa4-79f2-4c15-5493b1680cf7.png)
↑テクスチャも貼れるぞ！

という事で、UGUI上でスパイク模様を構成するコンポーネントでした。
**色を複数指定できるようにした** のと、**RectTransformの矩形に依存した形状に出来た**ので、中々使い所が有るのではないでしょうか？
単純に多角形のプリミティブとしても使用できるかもしれませんね。

個人的にはアルファ値のグラデーションがあまり綺麗になっていないので、その部分だけ要修正と考えています。
まぁ当初の目的のスパイク模様は達成できたので、とりあえず今回はここまでという事で。