Unity

三角関数(Sin, Cos, Tan)の値をテクスチャ化してみる

メリークリスマス!!!
いよいよ最終日となりました、グレンジ Advent Calendar 2017の25日目の記事です。
担当の、メッシと申します。
今回はUnityに関する、知っておくといつの日か役立つかも?という小技を紹介したいと思います。

三角関数の計算コスト高い問題

よく言われる話ですが、三角関数は計算コストが高いとよく言われます。
単純なC#コードの場合は、キャッシュするなり、テーブルを用意するなりして回避できますが、
Shaderの場合、どうやってその計算コストを安くすれば良いのでしょうか?

こういう時は、テクスチャ化を検討してみましょう。

要素分解

生成するための必要な情報を整理してみます。

①分解能を決める (精度)
②作成するテクスチャのタイプを指定する

Editor拡張

今回は、ScriptableWizardを使いましょう。

Scriptable Wizardについては
Editor拡張 - ScriptableWizardについて
の記事を参照してください。

詳細

ソースコード

説明するよりも、まずはソースコード

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

/// <summary>
/// Trigonometric texture window
/// 三角関数のテクスチャを生成する
/// </summary>
public class TrigonometricTextureWindow : ScriptableWizard {

    /// <summary>
    /// 生成するテクスチャタイプ
    /// </summary>
    private enum CreateType
    {
        Sin,                // サインテクスチャ
        Cos,                // コサインテクスチャ
        Tan,                // タンジェントテクスチャ
        ArcSin,             // アークサインテクスチャ
        ArcCos,             // アークコサインテクスチャ
        ArcTan,             // アークタンジェントテクスチャ
        SinCosTan,          // サイン、コサイン、タンジェントテクスチャ
        ArcSinCosTan,       // アークサイン、アークコサイン、アークタンジェントテクスチャ
    }

    /// 生成するテクスチャタイプ
    [SerializeField]
    private CreateType _type = CreateType.Sin;

    /// 分解能
    [SerializeField]
    private int Division = 30;

    /// <summary>
    /// Windowを開く
    /// </summary>
    [MenuItem("Window/Trigonometric Texture")]
    private static void Open() 
    {
        DisplayWizard<TrigonometricTextureWindow>("Trigonometric Texture Window");
    }

    /// <summary>
    /// タイプに応じたテクスチャを生成する
    /// </summary>
    private void OnWizardCreate()
    {       
        var directoryPath = EditorUtility.OpenFolderPanel("Select Folder", "Assets", "");
        var fileName = "tex_" + _type.ToString() + "_" + Division + ".png";
        fileName = fileName.ToLower();
        var filePath = directoryPath + "/" + fileName;

        var texture = new Texture2D(Division, 1, TextureFormat.ARGB32, false, true);
        for (int x = 0; x < Division; x++) {
            texture.SetPixel(x, 0, GetColor(x));
        }

        System.IO.File.WriteAllBytes(filePath, texture.EncodeToPNG());
        Debug.Log("Complete: " + filePath);
        AssetDatabase.Refresh();
    }

    /// <summary>
    /// カラーを取得する
    /// マイナス値を格納するために、(値 + 1) / 2fの変換を行い、0 ~ 1の範囲に収める
    /// </summary>
    private Color GetColor(int x) {
        var degree = (360f / Division) * x;
        var radian = Mathf.Deg2Rad * degree;
        var color = Color.red;
        switch (_type) {
        case CreateType.Sin:
            color.r = color.g = color.b = (Mathf.Sin(radian) + 1) / 2f;
            break;
        case CreateType.Cos:
            color.r = color.g = color.b = (Mathf.Cos(radian) + 1) / 2f;
            break;
        case CreateType.Tan:
            color.r = color.g = color.b = (Mathf.Tan(radian) + 1) / 2f;
            break;
        case CreateType.ArcSin:
            color.r = color.g = color.b = (Mathf.Asin(radian) + 1) / 2f;
            break;
        case CreateType.ArcCos:
            color.r = color.g = color.b = (Mathf.Acos(radian) + 1) / 2f;
            break;
        case CreateType.ArcTan:
            color.r = color.g = color.b = (Mathf.Atan(radian) + 1) / 2f;
            break;
        case CreateType.SinCosTan:
            color.r = (Mathf.Sin(radian) + 1) / 2f;
            color.g = (Mathf.Cos(radian) + 1) / 2f;
            color.b = (Mathf.Tan(radian) + 1) / 2f;
            break;
        case CreateType.ArcSinCosTan:
            color.r = (Mathf.Asin(radian) + 1) / 2f;
            color.g = (Mathf.Acos(radian) + 1) / 2f;
            color.b = (Mathf.Atan(radian) + 1) / 2f;
            break;
        }

        return color;
    }
}

ポイント

ポイント①

生成したTexture2DはSystem.IO.WriteAllBytesで書き出す

System.IO.File.WriteAllBytes(filePath, texture.EncodeToPNG());

ポイント②

Refresh関数を呼ばないと、UnityEditor上に出てこない。
(一度、Unityでないウインドウを選択し、再度Unityに戻ってくると出てくるけど)

しかし、この関数を呼ぶことで、Editor上にAssetが追加された事をUnity側に通知するため、即時で見えるようになる

AssetDatabase.Refresh();

ポイント③

Colorは「0 ~ 1」の値しか取り扱えない。
しかし、三角関数の取りうる値は「-1 ~ 1」なため、マイナス値は取り除かれてしまいます。

そのため、値を「+1」して、1/2倍することで三角関数の値を「0 ~ 1」に変換しています。

※ただし、Shader側などで値を取る際はその逆の工程が必要になります。

結果

こんなテクスチャが出来上がります。

サインテクスチャ
4a48eb12-e77c-20f3-f391-617ac69bacfe.png

 
サイン、コサイン、タンジェントテクスチャ
(R:サイン, G: コサイン、B: タンジェント)
87e5596a-9d42-c90c-2ded-2c6908bbf18c.png

まとめ

このテクスチャを用いて処理負荷を節約出来ると良いですね。
使う際は、いちいちShaderにペシペシ貼り付けるよりも、GlobalTextureとして使用するのをオススメします。

最後に

Grengeエンジニアによるアドベントカレンダー2017はこれで終了です。

今年初チャレンジとなるGrengeのアドベントカレンダーでしたが、
業務委託、正社員関係なく、社内のエンジニアの方たちは精力的に取り組んでくれて感謝しております。

そして私たちの発信によって、読者の皆様に対して何かしらの気付きや、知識がストックされたなら良いなと思っております。

また、このアドベントカレンダーを通して、Grengeという会社に対して少しでも興味を持って頂き、
「一緒に働いてみたい!」「ちょっと話だけでも聞いてみたい」と思って頂ける方が増えたら幸いです。

それでは皆様、良いお年を!