【Unity】【エディタ拡張】MatCap用テクスチャを簡単に作るエディタ拡張

f:id:halya_11:20180618232514p:plain:w300

MatCapテクスチャを簡単に作るためのエディタ拡張を作ったので公開します。

やりたいこと

以前MatCapという手法を紹介しました。

light11.hatenadiary.com

この手法はしっかりライティングされた風の描画をお手軽にできますが、そのためにはこのような画像をつくらなければいけません。

f:id:halya_11:20180618231356p:plain

今回はこのテクスチャを、実際にSceneに置いたSphereをキャプチャすることで作ります。

実装

まずはソースコードです。

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;

public class MatCapEditor
{
    private const float SPHERE_RADIUS = 0.5f;
    private const int SPHERE_LONGITUDE = 96;
    private const int SPHERE_LATITUDE = 64;

    // この値(0~1)が大きいほどSphereがカメラからはみ出して描画されるため
    // 精度は落ちるがエッジの部分のサンプリング色が不自然にならなくなる
    private const float SPHERE_SIZE_FACTOR = 0.25f;

    private static Vector3[] _vertices = new Vector3[(SPHERE_LONGITUDE + 1) * SPHERE_LATITUDE + 2];

    [MenuItem("Assets/Create MatCap 128x128")]
    public static void CreateMatCap128()
    {
        CreateMatCap(128);
    }

    [MenuItem("Assets/Create MatCap 256x256")]
    public static void CreateMatCap256()
    {
        CreateMatCap(256);
    }

    [MenuItem("Assets/Create MatCap 512x512")]
    public static void CreateMatCap512()
    {
        CreateMatCap(512);
    }
    
    private static void CreateMatCap(int px)
    {
        var selected = Selection.activeObject;
        if (!(selected is Material)) {
            Debug.LogError("Materialにのみ有効です");
            return;
        }

        // カメラを配置
        var cameraGo = new GameObject("MatCap Capture Camera");
        var camera = cameraGo.AddComponent<Camera>();
        cameraGo.transform.position = new Vector3(0, 0, -10);
        cameraGo.transform.rotation = Quaternion.identity;
        camera.orthographic = true;
        camera.orthographicSize = 0.5f * Mathf.Lerp(0.99f, 1.0f, SPHERE_SIZE_FACTOR);
        camera.backgroundColor = Color.black;
        camera.clearFlags = CameraClearFlags.Color;

        // Sphereを配置
        var targetGo = new GameObject();
        var meshRenderer = targetGo.AddComponent<MeshRenderer>();
        var meshFilter = targetGo.AddComponent<MeshFilter>();
        meshFilter.mesh = CreateSphereMesh();
        meshRenderer.material = selected as Material;
        targetGo.transform.position = Vector3.zero;

        // RenderTextureにレンダリング
        var rt = RenderTexture.GetTemporary(px, px);
        camera.targetTexture = rt;
        camera.Render();

        // RenderTextureの内容をpngに焼きこむ
        var currentRT = RenderTexture.active;
        RenderTexture.active = rt;
        var texture = new Texture2D(px, px, TextureFormat.RGB24, false);
        texture.ReadPixels(new Rect(0, 0, px, px), 0, 0);
        texture.Apply();
        RenderTexture.active = currentRT;

        // pngを保存する
        var filePath = EditorUtility.SaveFilePanel("Save", "Assets", "matcap", "png");
        if (!string.IsNullOrEmpty(filePath)) {
            System.IO.File.WriteAllBytes(filePath, texture.EncodeToPNG());
            AssetDatabase.Refresh();
        }

        // 削除・解放処理
        GameObject.DestroyImmediate(targetGo);
        GameObject.DestroyImmediate(cameraGo);
        RenderTexture.ReleaseTemporary(rt);
    }

    /// <summary>
    /// SphereのMeshを生成する
    /// </summary>
    private static Mesh CreateSphereMesh()
    {
        // Vertices
        _vertices[0] = Vector3.up * SPHERE_RADIUS;
        for (int lat = 0; lat < SPHERE_LATITUDE; lat++) {
            float a1 = Mathf.PI * (float)(lat+1) / (SPHERE_LATITUDE+1);
            float sin1 = Mathf.Sin(a1);
            float cos1 = Mathf.Cos(a1);

            for (int lon = 0; lon <= SPHERE_LONGITUDE; lon++) {
                float a2 = Mathf.PI * 2.0f * (float)(lon == SPHERE_LONGITUDE ? 0 : lon) / SPHERE_LONGITUDE;
                float sin2 = Mathf.Sin(a2);
                float cos2 = Mathf.Cos(a2);

                _vertices[lon + lat * (SPHERE_LONGITUDE + 1) + 1] = new Vector3(sin1 * cos2, cos1, sin1 * sin2) * SPHERE_RADIUS;
            }
        }
        _vertices[_vertices.Length - 1] = Vector3.up * -SPHERE_RADIUS;

        // Normals
        Vector3[] normales = new Vector3[_vertices.Length];
        for (int n = 0; n < _vertices.Length; n++) {
            normales[n] = _vertices[n].normalized;
        }

        // UVs
        Vector2[] uvs = new Vector2[_vertices.Length];
        uvs[0] = Vector2.up;
        uvs[uvs.Length - 1] = Vector2.zero;
        for (int lat = 0; lat < SPHERE_LATITUDE; lat++) {
            for (int lon = 0; lon <= SPHERE_LONGITUDE; lon++) {
                uvs[lon + lat * (SPHERE_LONGITUDE + 1) + 1] = new Vector2((float)lon / SPHERE_LONGITUDE, 1f - (float)(lat + 1) / (SPHERE_LATITUDE + 1));
            }
        }

        // Triangles
        int[] triangles = new int[_vertices.Length * 2 * 3];
        //Top
        int i = 0;
        for (int lon = 0; lon < SPHERE_LONGITUDE; lon++) {
            triangles[i++] = lon + 2;
            triangles[i++] = lon + 1;
            triangles[i++] = 0;
        }
        //Middle
        for (int lat = 0; lat < SPHERE_LATITUDE - 1; lat++) {
            for (int lon = 0; lon < SPHERE_LONGITUDE; lon++) {
                int current = lon + lat * (SPHERE_LONGITUDE + 1) + 1;
                int next = current + SPHERE_LONGITUDE + 1;

                triangles[i++] = current;
                triangles[i++] = current + 1;
                triangles[i++] = next + 1;

                triangles[i++] = current;
                triangles[i++] = next + 1;
                triangles[i++] = next;
            }
        }
        //Bottom
        for (int lon = 0; lon < SPHERE_LONGITUDE; lon++) {
            triangles[i++] = _vertices.Length - 1;
            triangles[i++] = _vertices.Length - (lon + 2) - 1;
            triangles[i++] = _vertices.Length - (lon + 1) - 1;
        }

        // Create Mesh
        var mesh = new Mesh();
        mesh.vertices = _vertices;
        mesh.normals = normales;
        mesh.uv = uvs;
        mesh.triangles = triangles;
        mesh.RecalculateBounds();
        return mesh;
    }
}
#endif

ちょっと長いですがやってることは

  1. OrthographicなCameraを下図のように配置
  2. Sphereを生成&マテリアルをセットして下図のように配置
  3. RenderTextureに描画
  4. png

という感じです。

f:id:halya_11:20180618231340p:plain

ちなみにUnityで用意されているSphereは今回使うにはポリゴン数が少ないのが気になったのでプロシージャルに生成しています。
この部分のソースコードは下記を参考にしています。

ProceduralPrimitives - Unify Community Wiki

使う

まずSceneを開いていい感じにセットアップします。
次にProjectビューから適当なマテリアルを右クリック > Create MatCap > 保存先を選択します。

f:id:halya_11:20180618232514p:plain:w300

このようなテクスチャができました。
Standardシェーダを使って金色の金属っぽくしてみました。

これをMatCapのシェーダを適用したMaterialにアサインすると

f:id:halya_11:20180618233343g:plain

このようになりました。

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com