読者です 読者をやめる 読者になる 読者になる

しゅみぷろ

プログラミングとか

WorldSpaceからUV算出するののパフォーマンスチューニング

UnityTexturePaint

はじめに

esprog.hatenablog.com

以前紹介した記事で、任意のワールド座標上の点からオブジェクト表面のUVを算出する手順を紹介しました。 このまま使ったのではあまりパフォーマンスがよくないため、今回はこれを高速化しよう、というお話。

以前は遅い原因を「メッシュの持つすべての三角形平面について調査している」ためだと書きました。勿論、頂点数が増えれば増えるほど、単純で線形的な探索ではボロが出ます。

これを解決するために空間分割を行うべきだと思っています。

ですが、プロファイラーで色々調べてみると、CG.Collectが頻発しているため、処理が重いということも発覚しました。

なので、今回はこの部分を修正してパフォーマンスを計測しました。正直そこまで複雑なモデルじゃなければ空間分割するまでもないくらいにはパフォーマンスが改善されました。

GC.Collectが頻発する原因

原因はツイートの通り。 Unityのフォーラムでも書かれていました。

http://forum.unity3d.com/threads/mesh-get_vertices-gc-collection-very-heavy.334882/forum.unity3d.com

要するに、プロパティのgetterで配列を生成して返しているため、頻繁にgetterを呼び出すとその度に配列作られるぞ、と。 なら話は簡単で、頻繁に呼び出さない。それだけです。

実装

以前と比べて、ただ変数を使いまわすようにしたのと、Meshの一部プロパティ呼び出し回数を減らしただけです。

using System.Collections;
using UnityEngine;

/// <summary>
/// サーフェス上の点pからuvを算出する
/// </summary>
public class CalcUV : MonoBehaviour
{
    private MeshFilter meshFilter;
    private Mesh mesh;

    private int index0;
    private int index1;
    private int index2;

    private Vector3 p1;
    private Vector3 p2;
    private Vector3 p3;
    private Vector3 p;

    private Vector3 v1;
    private Vector3 v2;
    private Vector3 vp;

    private Vector3 nv;

    private Vector3 a;
    private Vector3 b;
    private Vector3 c;

    private float d_ab;
    private float d_bc;

    private void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;
            if(Physics.Raycast(ray, out hitInfo))
            {
                meshFilter = hitInfo.transform.GetComponent<MeshFilter>();
                mesh = meshFilter.sharedMesh;
                var meshTriangles = mesh.triangles;
                var meshVertices = mesh.vertices;
                var meshUv = mesh.uv;

                for(var i = 0; i < meshTriangles.Length; i += 3)
                {
                   #region 1.ある点pが与えられた3点において平面上に存在するか

                    index0 = i + 0;
                    index1 = i + 1;
                    index2 = i + 2;

                    p1 = meshVertices[meshTriangles[index0]];
                    p2 = meshVertices[meshTriangles[index1]];
                    p3 = meshVertices[meshTriangles[index2]];
                    p = hitInfo.transform.InverseTransformPoint(hitInfo.point);

                    v1 = p2 - p1;
                    v2 = p3 - p1;
                    vp = p - p1;

                    nv = Vector3.Cross(v1, v2);
                    var val = Vector3.Dot(nv, vp);
                    //適当に小さい少数値で誤差をカバー
                    if(!(-0.000001f < val && val < 0.000001f))
                        continue;

                   #endregion 1.ある点pが与えられた3点において平面上に存在するか

                   #region 2.同一平面上に存在する点pが三角形内部に存在するか

                    a = Vector3.Cross(p1 - p3, p - p1).normalized;
                    b = Vector3.Cross(p2 - p1, p - p2).normalized;
                    c = Vector3.Cross(p3 - p2, p - p3).normalized;

                    d_ab = Vector3.Dot(a, b);
                    d_bc = Vector3.Dot(b, c);

                    if(!(0.999f < d_ab && 0.999f < d_bc))
                        continue;

                   #endregion 2.同一平面上に存在する点pが三角形内部に存在するか

                   #region 3.点pのUV座標を求める

                    var uv1 = meshUv[meshTriangles[index0]];
                    var uv2 = meshUv[meshTriangles[index1]];
                    var uv3 = meshUv[meshTriangles[index2]];

                    //PerspectiveCollect(透視投影を考慮したUV補間)
                    Matrix4x4 mvp = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix * hitInfo.transform.localToWorldMatrix;
                    //各点をProjectionSpaceへの変換
                    Vector4 p1_p = mvp * p1;
                    Vector4 p2_p = mvp * p2;
                    Vector4 p3_p = mvp * p3;
                    Vector4 p_p = mvp * p;
                    //通常座標への変換(ProjectionSpace)
                    Vector2 p1_n = new Vector2(p1_p.x, p1_p.y) / p1_p.w;
                    Vector2 p2_n = new Vector2(p2_p.x, p2_p.y) / p2_p.w;
                    Vector2 p3_n = new Vector2(p3_p.x, p3_p.y) / p3_p.w;
                    Vector2 p_n = new Vector2(p_p.x, p_p.y) / p_p.w;
                    //頂点のなす三角形を点pにより3分割し、必要になる面積を計算
                    var s = 0.5f * ((p2_n.x - p1_n.x) * (p3_n.y - p1_n.y) - (p2_n.y - p1_n.y) * (p3_n.x - p1_n.x));
                    var s1 = 0.5f * ((p3_n.x - p_n.x) * (p1_n.y - p_n.y) - (p3_n.y - p_n.y) * (p1_n.x - p_n.x));
                    var s2 = 0.5f * ((p1_n.x - p_n.x) * (p2_n.y - p_n.y) - (p1_n.y - p_n.y) * (p2_n.x - p_n.x));
                    //面積比からuvを補間
                    var u = s1 / s;
                    var v = s2 / s;
                    var w = 1 / ((1 - u - v) * 1 / p1_p.w + u * 1 / p2_p.w + v * 1 / p3_p.w);
                    var uv = w * ((1 - u - v) * uv1 / p1_p.w + u * uv2 / p2_p.w + v * uv3 / p3_p.w);

                    //uvが求まったよ!!!!
                    Debug.Log(uv + ":" + hitInfo.textureCoord);
                    return;

                   #endregion 3.点pのUV座標を求める
                }
            }
        }
    }
}

パフォーマンスの変化

Unity標準のSphere(頂点数526)での比較。

f:id:es_program:20160922162229p:plain:w800

改善前はこんな感じ

f:id:es_program:20160922162256p:plain:w800

改善後です。

f:id:es_program:20160922163527p:plain:w800

並べると一目瞭然です。

原因調べたらかなり楽にパフォーマンス改善できてよかった。。。

f:id:es_program:20160922172245p:plain:w800

約27000頂点で試したところ、そこまでパフォーマンスは悪くないような気がしました。 空間分割にかかる初期コストを考えたらあまり必要なさそうな気がしてきました。 あと出来るのは並列処理にして少しでも速くする・・・とか?

追記

バグ見つけました。 詳細は以下で。

esprog.hatenablog.com

esprog.hatenablog.com