球面上の座標を20区間に等分(正二十面体に投影)し、区間IDを得る

整理しておかないと自分が忘れそうなので記事にします。

# 経緯

2Dプログラミングしかまともにやったことのなかった自分が恐る恐る始めてみたUnityいじりですが、意外と緩ーく書けて楽しくなってきたので、塊っぽい魂を再現してみることにしました（以前から一度やってみたかった）。こちらがその途中経過。

![塊っぽい魂](https://v.cdn.vine.co/r/videos/35B7B3AE181206808071775588352_3f2c2b355b9.3.0.17904409353761855010.mp4.jpg?versionId=ymLePYNblqO4bIe3uQ3I8L2rjk_P9Iyb)
[動画](https://vine.co/v/eZWD6ujWJXa)

ここまでいろいろ試してみて具合がよかった設計：

- 当たり判定には SphereCollider しか使わないようにする（棒状のものなどはそれを複数配置する）
- 巻き込んだモノを Joint で塊に結合して行くと破たんするので、吸着対象のコライダ自体を外して塊の方に移し、重量を加える

速度面で合理的な手法がそのままゲーム的にも具合のいい感じになってるのがミソっぽいです。しかし、まだ巻き込み物は数十個というオーダーまでしか試しておらず、それというのも　**当たり判定が緩すぎるとだんだん節足動物のようになる** という問題があるため（上の動画参照）。

- 突出した方向にばかりモノがくっついて球体からかけ離れた形状になり、うまく回せなくなる
- ほんの少し突出した部分があるだけでスムーズに回転しなくなり、操作感が悪い（しかし「棒状のものをつっかえ棒にする」技は有効にしておく必要がある）

オリジナルの塊魂をやってみても、同じ方向にばかりは決して伸びないようになっているみたいだし、どうやら突出部のコライダを選択的に無効化する必要がありそうです。

では、何を判断基準にコライダを無効とすれば良いのか？ …仮に、塊表面にタマネギのような層が重なっている空間を想定すると、**ある半径の球面層にモノがどれだけまんべんなく付着しているかを測る** ことで以下の処理ができるだろう、と考えました。

- まんべんなくモノが付着している半径までの層を１つの球として再近似（その内側のすべてのコライダを1つにまとめる）
- ほとんどモノが付着していない層よりも外側の吸着用コライダを無効化（地形衝突用コライダとしては一部有効にしておく）

![たまねぎ](https://qiita-image-store.s3.amazonaws.com/0/34010/1591322a-95d5-ec66-226f-1b4906b79fdc.jpeg)

まずはその足掛かりとして、**球面上の座標を均等に区間分けする** 関数を書いてみました。モノを巻き込んだら、各コライダの座標が属する層における球面区間のマス目を塗りつぶし、その層の塗りつぶし率が閾値を超えたら「その層より内側は全部埋まってることにする」、みたいな処理を想定しているわけです。

もしかしたらこんなことしなくても、すごい数学の知識があればもっと合理的に計算できるのかも知れないですが、少なくとも自分にはこれが精いっぱいなので、この方法でやってみることにします。


# 球面を正二十面体に投影する

まずはこちらのエレガントな説明を読んで正二十面体の特性を理解します。

- [正二十面体の対角線・体積を座標でエレガントに導出](http://mathtrain.jp/nizyu)

どうやら8つの象限は鏡像になっているようなので、+X+Y+Z象限にだけ注目して下図のように整理してみます。 ※上記サイトの説明とは向きがちょっと違いましたがまぁ問題ないです

![正二十面体の+X+Y+Z象限部分](https://qiita-image-store.s3.amazonaws.com/0/34010/19d80351-dbbb-95fa-d515-c518e8fee20b.jpeg)

図中の T はこの象限に完全に含まれ、分断されていない面（正三角形）。U, V, W はそれぞれ ZX平面, XY平面, YZ平面 に分断されて隣の象限と生き別れになっている半分の面（直角三角形）です。

ここで、Tの3辺それぞれについて、**その辺と原点とを通る平面** を考えます。たとえば、下図の緑の平面はその3つのうちの1つです。

![正二十面体の+X+Y+Z象限](https://qiita-image-store.s3.amazonaws.com/0/34010/2a402952-7b1f-4280-75d7-dd54b34b53b2.jpeg)

**この緑の平面の向こう側にある点は、Vに属する** と言えそうです。
同じ方法で U, W に属する点も判定でき、残った点は T に属することになります。

この方法で、ある任意の点を +X+Y+Z 象限に移したときに4つの面のどれに属するのかが分かれば、あとは他の象限も含む全20個の面との対応付けを考えて、本来の面を特定してやれば良いわけです。

# ソースコード

```csharp
using UnityEngine;
using System.Collections;

/**
 *  球面上の座標を20個の区間に等分(正二十面体に投影)し、区間IDを得るサンプル
 *  (適当にCreateEmptyしたオブジェクトにアタッチして実行)
 *  https://twitter.com/bucchigiri/status/595572141325594625
 * 
 *  [説明]
 *  正二十面体を XY平面, YZ平面, ZX平面 を境として8つに分断すると、各象限は
 *  すべて鏡像の関係にあり、たとえば +X+Y+Z 象限では以下の計4つの三角形が現れる。
 *  ・ +X+Y平面, +Y+Z平面, +Z+X 平面内に1つずつ頂点を持つ正三角形 (T)
 *  ・ Tの1辺を斜辺とし、その斜辺の一端から +X軸, +Y軸, +Z軸 に降ろした
 *  　 垂線を底辺とする直角三角形 (U, V, W)
 *  　 U, V, W は隣り合う象限から分かたれた正三角形の半面である。
 *  
 *  原点 および Tの1辺 を含む平面で空間を分断すると、
 *  ある点がそのどちら側に属すかを調べることにより、
 *  どの面に投影されるかを判定することができる。
 */
public class IcosaherdaMapper : MonoBehaviour {

    private static readonly Vector3 nX; // 原点, pXY, pZX を含む平面の法線ベクトル
    private static readonly Vector3 nY; // 原点, pYZ, pXY を含む平面の法線ベクトル
    private static readonly Vector3 nZ; // 原点, pZX, pYZ を含む平面の法線ベクトル

    // 初期化
    static IcosaherdaMapper() {
        float phi = (1f + Mathf.Sqrt(5f)) / 2f;
        Vector3 pXY = new Vector3(phi, 1f, 0f); // T の +X+Y 平面上の頂点
        Vector3 pYZ = new Vector3(0f, phi, 1f); // T の +Y+Z 平面上の頂点
        Vector3 pZX = new Vector3(1f, 0f, phi); // T の +Z+X 平面上の頂点
        nX = Vector3.Cross(pXY, pZX);
        nY = Vector3.Cross(pYZ, pXY);
        nZ = Vector3.Cross(pZX, pYZ);
    }

    // 面を特定するID
    enum Plane : int {
        Skew_XnYnZn = 0x00,
        Skew_XpYnZn = 0x01,
        Skew_XnYpZn = 0x02,
        Skew_XpYpZn = 0x03,
        Skew_XnYnZp = 0x04,
        Skew_XpYnZp = 0x05,
        Skew_XnYpZp = 0x06,
        Skew_XpYpZp = 0x07,
        ContactAxis_Xn_Zn = 0x08,
        ContactAxis_Xn_Zp = 0x09,
        ContactAxis_Xp_Zn = 0x0A,
        ContactAxis_Xp_Zp = 0x0B,
        ContactAxis_Yn_Xn = 0x0C,
        ContactAxis_Yn_Xp = 0x0D,
        ContactAxis_Yp_Xn = 0x0E,
        ContactAxis_Yp_Xp = 0x0F,
        ContactAxis_Zn_Yn = 0x10,
        ContactAxis_Zn_Yp = 0x11,
        ContactAxis_Zp_Yn = 0x12,
        ContactAxis_Zp_Yp = 0x13
    }

    // 任意の座標から面IDを求める
    public static int Map(Vector3 p) {
        int xs = p.x < 0 ? 0 : 1;
        int ys = p.y < 0 ? 0 : 1;
        int zs = p.z < 0 ? 0 : 1;
        p.Set(p.x*(xs-.5f), p.y*(ys-.5f), p.z*(zs-.5f));
        p.Normalize();
        if (0 < Vector3.Dot(p, nX)) return (int)Plane.ContactAxis_Xn_Zn | zs | xs<<1;
        if (0 < Vector3.Dot(p, nY)) return (int)Plane.ContactAxis_Yn_Xn | xs | ys<<1;
        if (0 < Vector3.Dot(p, nZ)) return (int)Plane.ContactAxis_Zn_Yn | ys | zs<<1;
        return (int)Plane.Skew_XnYnZn | xs | ys<<1 | zs<<2;
    }
    


    /////////////////////////////////// demo ///////////////////////////////////

    int plotMaxCount = 20000;
    int plotSpeed = 20;

    static readonly int[] colors = new int[20] {
        0x000, 0xF00, 0x0F0, 0xFF0, 0x00F, 0xF0F, 0x0FF, 0xFFF,
        0x080, 0x08F, 0xF80, 0xF8F,
        0x008, 0xF08, 0x0F8, 0xFF8,
        0x800, 0x8F0, 0x80F, 0x8FF
    };

    void Update() {
        if (plotMaxCount <= 0) return;
        for (int i=0; i<plotSpeed; i++) {
            Vector3 p = Random.onUnitSphere;
            int j = Map(p);
            GameObject obj = new GameObject("plot-" + i.ToString());
            obj.transform.LookAt(-p);
            obj.transform.position = p;
            obj.transform.localScale = Vector3.one * .025f;
            TextMesh text = obj.AddComponent<TextMesh>();
            text.text = "■" + new string(new char[]{ (char)(j + 'A') });
            text.alignment = TextAlignment.Left;
            text.anchor = TextAnchor.MiddleCenter;
            text.richText = false;
            text.fontSize = 9;
            int c = colors[j];
            text.color = new Color((c>>8&15)/15f, (c>>4&15)/15f, (c&15)/15f);
        }
        plotMaxCount -= plotSpeed;
    }

}
```

# 実行結果

![実行結果](https://qiita-image-store.s3.amazonaws.com/0/34010/780fb053-ecc8-64df-53d1-c89c16b46415.jpeg)

という具合に、球面上の座標を20個のエリアに等分することができました。

これを使って、塊表面の各層におけるモノの付着状況を整理する処理を書いて行けば良さそうです。
され、これで本当にうまくいくのか…？ 続きはまた今度