【Unity】【シェーダ】スクリプトからVP行列を渡してシェーダで座標変換に使う

UnityのシェーダでスクリプトからVP行列を渡してシェーダで座標変換に使う方法をまとめました。

Unity2019.2.18

MVP行列をばらしたシェーダを書く

Unityのシェーダでは頂点シェーダでUnityObjectToClipPos()関数を使うことで簡単に座標変換が行えます。
しかし今回はV行列とP行列をスクリプトから渡して使いたいので、まずMVP行列をばらすところから始めます。

シェーダは以下のように記述します。

Shader "Example"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(mul(UNITY_MATRIX_P, mul(UNITY_MATRIX_V, unity_ObjectToWorld)), v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return 1;
            }
            ENDCG
        }
    }
}

Unityでは上記のように行列は右からMVPの順に掛けます。
mul関数の第一引数を行列にすることにも注意しましょう。

スクリプトからVP行列を渡す

次にV行列とP行列をスクリプトからシェーダに渡します。

using UnityEngine;

[RequireComponent(typeof(Camera))]
[ExecuteAlways]
public class Example : MonoBehaviour
{
    private void OnPreRender()
    {
        SetMatrix(GetComponent<Camera>());
    }

    private static void SetMatrix(Camera camera)
    {
        var viewMatrix = camera.worldToCameraMatrix;
        var projectionMatrix = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
        Shader.SetGlobalMatrix("_MatrixV", viewMatrix);
        Shader.SetGlobalMatrix("_MatrixP", projectionMatrix);
    }
}

ビュー行列はCamera.worldToCameraMatrixを使います。
シェーダにビュー行列を渡す場合にはTransform.worldToLocalMatrixではなくこちらを使うことに注意してください。

一方プロジェクション行列はCamera.projectionMatrixで取得しますが、
GPUに渡す場合にはこれをさらにGL.GetGPUProjectionMatrix()で変換します。

これらの理由については本記事では触れませんが、下記の記事に詳しく書かれていますので必要に応じて参照してください。

tech.drecom.co.jp

シェーダは次のように修正します。

Shader "Example"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
            
            float4x4 _MatrixV;
            float4x4 _MatrixP;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(mul(_MatrixP, mul(_MatrixV, unity_ObjectToWorld)), v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return 1;
            }
            ENDCG
        }
    }
}

UnityのV行列、P行列を使っていた部分をスクリプトから受け取るように変更しました。

スクリプトで行列を合成してから渡す

次にV行列とP行列をスクリプトで合成してからシェーダに渡すように変更します。
まずスクリプトです。変更点だけ記載しています。

private static void SetMatrix(Camera camera)
{
    var viewMatrix = camera.worldToCameraMatrix;
    var projectionMatrix = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
    Shader.SetGlobalMatrix("_MatrixVP", projectionMatrix * viewMatrix);
}

単純にV行列とP行列を掛けているだけです。
右側から順に掛けている点に注意してください。

頂点シェーダは以下のように変更します。

float4x4 _MatrixVP;

v2f vert (appdata v)
{
    v2f o;
    o.vertex = mul(mul(_MatrixVP, unity_ObjectToWorld), v.vertex);
    return o;
}

Sceneビューに対応する

さてここまでの作業でGameビューではモデルが正常に描画されるようになりました。
ただし、Sceneビューではうまく表示されません。

f:id:halya_11:20200123185628p:plain

これはSceneビューのカメラでオブジェクトをレンダリングするときにもGameビューのカメラの行列を使ってしまっているためです。
そこで、シーンビューをレンダリングする前にシーンビューのカメラの行列を渡すようにスクリプトを修正します。

using UnityEngine;

[RequireComponent(typeof(Camera))]
[ExecuteAlways]
public class Example : MonoBehaviour
{
    private static bool _didRegisterOnPreRender = false;

    private void OnEnable(){
#if UNITY_EDITOR
        if (!_didRegisterOnPreRender)
        {
            Camera.onPreRender += OnPreRender;
            _didRegisterOnPreRender = true;
        }
#endif
    }

    private void OnDisable()
    {
#if UNITY_EDITOR
        Camera.onPreRender -= OnPreRender;
        _didRegisterOnPreRender = false;
#endif
    }

    private static void OnPreRender(Camera camera)
    {
        if (camera == null)
        {
            return;
        }
        if (camera.name == "SceneCamera" || camera.name == "Preview Camera")
        {
            SetMatrix(camera);
        }
    }

    private void OnPreRender()
    {
        SetMatrix(GetComponent<Camera>());
    }

    private static void SetMatrix(Camera camera)
    {
        var viewMatrix = camera.worldToCameraMatrix;
        var projectionMatrix = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
        Shader.SetGlobalMatrix("_MatrixVP", projectionMatrix * viewMatrix);
    }
}

シーンビューのレンダリング前処理はCamera.onPreRenderをフックすることで行えます。
ちなみにPreview Cameraはカメラを選択したときにシーンビューの右下に表示されるプレビューを描画する用のカメラです。

結果

このスクリプトとシェーダを使ってレンダリングした結果は以下のようになります。

f:id:halya_11:20200123185513p:plain

正常に座標変換行列が受け渡せていることが確認できました。

参考

tech.drecom.co.jp