【Unity / URP10.8.1】よく使うシェーダーのテクニックについてメモ (座標まわり)

2023/12/30に公開

概要

シェーダーの実装を行う上で、個人的に使うことが多いシェーダーのテクニックについてまとめてみようと思います。

環境

Unity 2020.3.32f1
Universal RP 10.8.1

紹介する手法

  1. Transform の position を求める
  2. Transform の forward, right, up を求める
  3. カメラの前方向ベクトルを求める
  4. ビューベクトルを求める
  5. 頂点座標から深度値を求める
  6. スクリーン座標を求める
  7. 頂点を画面奥へずらす
  8. 一定の太さのアウトラインを作る
  9. 深度値からワールド座標を復元する

手法1. Transform.positionを求める

以下のようなコードで、transform.position (オブジェクトの位置) を取得できます。

float3 worldPosition;
worldPosition.x = UNITY_MATRIX_M[0].w; // ワールド空間X座標 (上から1番目の列のW成分)
worldPosition.y = UNITY_MATRIX_M[1].w; // ワールド空間Y座標 (上から2番目の列のW成分)
worldPosition.z = UNITY_MATRIX_M[2].w; // ワールド空間Z座標 (上から3番目の列のW成分)
M = \begin{pmatrix} m_{00} & m_{01} & m_{02} & \red {T_X} \\ m_{10} & m_{11} & m_{12} & \green {T_Y} \\ m_{20} & m_{21} & m_{22} & \blue {T_Z} \\ \gray{m_{30}} & \gray{m_{31}} & \gray{m_{32}} & \gray{m_{33}} \\\end{pmatrix}

transposeを利用して行列を転置させることで、座標取得を以下のように書くことができます。

float3 worldPosition = transpose(UNITY_MATRIX_M)[3].xyz; // ワールド空間X座標
M^T = \begin{pmatrix} m_{00} & m_{10} & m_{20} & \gray{m_{30}} \\ m_{01} & m_{11} & m_{21} & \gray{m_{31}} \\ m_{02} & m_{12} & m_{22} & \gray{m_{32}} \\ \red {T_X} & \green {T_Y} & \blue {T_Z} & \gray{m_{33}} \\\end{pmatrix}
取得できる理由

取得できる理由について

Unityシェーダーのfloat4x4は行優先な行列です。

UNITY_MATRIX_M は、オブジェクト空間をワールド空間の座標に変換する行列で、
モデル行列 と呼びます。

モデル行列 M は、以下のように表記できます。

M = \begin{pmatrix} m_{00} & m_{01} & m_{02} & \red {T_X} \\ m_{10} & m_{11} & m_{12} & \green {T_Y} \\ m_{20} & m_{21} & m_{22} & \blue {T_Z} \\ \gray{m_{30}} & \gray{m_{31}} & \gray{m_{32}} & \gray{m_{33}} \\\end{pmatrix}


モデル行列M を用いた座標変換の演算は、以下のような式で計算できます。
(X, Y, Z) はワールド空間の座標で、(x, y, z) はオブジェクト空間の座標です。

\begin{aligned} \begin{pmatrix} X \\ Y \\ Z \\ 1 \end{pmatrix} &= \begin{pmatrix} m_{00} & m_{01} & m_{02} & \red {T_X} \\ m_{10} & m_{11} & m_{12} & \green {T_Y} \\ m_{20} & m_{21} & m_{22} & \blue {T_Z} \\ \gray{m_{30}} & \gray{m_{31}} & \gray{m_{32}} & \gray{m_{33}} \\\end{pmatrix} \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} \end{aligned}


これを計算すると、以下のようになります。

\begin{aligned} X &= m_{00} \cdot x + m_{01} \cdot y + m_{02} \cdot z + \red {T_X} \\ Y &= m_{10} \cdot x + m_{11} \cdot y + m_{12} \cdot z + \green {T_Y} \\ Z &= m_{20} \cdot x + m_{21} \cdot y + m_{22} \cdot z + \blue {T_Z} \end{aligned}

\red {T_X}, \green {T_Y}, \blue {T_Z} が平行移動成分となっています。


transpose(行列の転置)を利用する

モデル行列Mの転置M^Tを計算してみると、以下のように式変形できます。

M^{T} = \begin{pmatrix} m_{00} & m_{01} & m_{02} & \red {T_X} \\ m_{10} & m_{11} & m_{12} & \green {T_Y} \\ m_{20} & m_{21} & m_{22} & \blue {T_Z} \\ \gray{m_{30}} & \gray{m_{31}} & \gray{m_{32}} & \gray{m_{33}} \\\end{pmatrix}^T = \begin{pmatrix} m_{00} & m_{10} & m_{20} & \gray{m_{30}} \\ m_{01} & m_{11} & m_{21} & \gray{m_{31}} \\ m_{02} & m_{12} & m_{22} & \gray{m_{32}} \\ \red {T_X} & \green {T_Y} & \blue {T_Z} & \gray{m_{33}} \\\end{pmatrix}

UNITY_MATRIX_M の転置を取ると、行[3]に平行移動成分が入ります。
transpose(UNITY_MATRIX_M)[3].xyz で平行移動成分が取ることができます。

参考記事

https://qiita.com/yuji_yasuhara/items/8d63455d1d277af4c270
https://pdwslmr.netlify.app/posts/3d-prog/column-row-major/

手法 2. Transform の forward, right, up を求める

3DオブジェクトのX,Y,Z軸は、以下のようなコードで取得できます。

float3 axisX = transpose(UNITY_MATRIX_M)[0].xyz; // X軸 : transform.right  
float3 axisY = transpose(UNITY_MATRIX_M)[1].xyz; // Y軸 : transform.up     
float3 axisZ = transpose(UNITY_MATRIX_M)[2].xyz; // Z軸 : transform.forward

取得できる理由

取得できる理由について

ローカル空間の X軸 (1, 0, 0), X軸 (0, 1, 0) , Z軸 (0, 0, 1) それぞれに行列M の回転成分を乗じた結果が、
ワールド空間における軸となります。

\begin{pmatrix} \red{m_{00}} & \green{m_{01}} & \blue{m_{02}} \\ \red{m_{10}} & \green{m_{11}} & \blue{m_{12}} \\ \red{m_{20}} & \green{m_{21}} & \blue{m_{22}} \\\end{pmatrix} \begin{pmatrix} 1 \\ 0 \\ 0 \end{pmatrix} = \begin{pmatrix} \red{m_{00}} \\ \red{m_{10}} \\ \red{m_{20}} \end{pmatrix}
\begin{pmatrix} \red{m_{00}} & \green{m_{01}} & \blue{m_{02}} \\ \red{m_{10}} & \green{m_{11}} & \blue{m_{12}} \\ \red{m_{20}} & \green{m_{21}} & \blue{m_{22}} \\\end{pmatrix} \begin{pmatrix} 0 \\ 1 \\ 0 \end{pmatrix} = \begin{pmatrix} \green{m_{01}} \\ \green{m_{11}} \\ \green{m_{21}} \end{pmatrix}
\begin{pmatrix} \red{m_{00}} & \green{m_{01}} & \blue{m_{02}} \\ \red{m_{10}} & \green{m_{11}} & \blue{m_{12}} \\ \red{m_{20}} & \green{m_{21}} & \blue{m_{22}} \\\end{pmatrix} \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix} = \begin{pmatrix} \blue{m_{02}} \\ \blue{m_{12}} \\ \blue{m_{22}} \end{pmatrix}

行列の転置を取ることで、軸を簡単に取り出すことができます。

M^{T} = \begin{pmatrix} \red{m_{00}} & \green{m_{01}} & \blue{m_{02}} & \gray{m_{03}} \\ \red{m_{10}} & \green{m_{11}} & \blue{m_{12}} & \gray{m_{13}} \\ \red{m_{20}} & \green{m_{21}} & \blue{m_{22}} & \gray{m_{23}} \\ \gray{m_{30}} & \gray{m_{31}} & \gray{m_{32}} & \gray{m_{33}} \\\end{pmatrix} ^{T} = \begin{pmatrix} \red{m_{00}} & \red{m_{10}} & \red{m_{20}} & \gray{m_{30}} \\ \green{m_{01}} & \green{m_{11}} & \green{m_{21}} & \gray{m_{31}} \\ \blue{m_{02}} & \blue{m_{12}} & \blue{m_{22}} & \gray{m_{32}} \\ \gray{m_{03}} & \gray{m_{13}} & \gray{m_{23}} & \gray{m_{33}} \\\end{pmatrix}

参考記事

https://www.klab.com/jp/blog/tech/2021/cedec-kyushu-2021-online-3d.html

参考スライド(53ページ)
https://docs.google.com/presentation/d/e/2PACX-1vR39XBvlbJsgXU0JauxGISW9XyyRqfJHg1b65rMZhUlY3x0M5deiGsD2YWf_3QylFHHINigD3TGOYsN/pub?start=false&loop=false&delayms=3000&slide=id.g1026d1beedb_1_2725

手法 3. カメラの前方向のベクトルを取得する

以下のコードで、カメラの前方向ベクトルを取ることができます。

float3 cameraForwardDir  = -UNITY_MATRIX_V[2].xyz;
V = \begin{pmatrix} v_{00} & v_{01} & v_{02} & v_{03} \\ v_{10} & v_{11} & v_{12} & v_{13} \\ \red{v_{20}} & \red{v_{21}} & \red{v_{22}} & v_{23} \\ v_{30} & v_{31} & v_{32} & v_{33} \\\end{pmatrix}

URP内部には、ビューベクトルを取得できる関数GetViewForwardDirが用意されているので、これを利用しても良いでしょう。

Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderVariablesFunctions.hlsl
float3 GetViewForwardDir()
{
    float4x4 viewMat = GetWorldToViewMatrix();
    return -viewMat[2].xyz;
}
取得できる理由

取得できる理由について

UNITY_MATRIX_V は、ワールド空間の座標をカメラ空間の座標に変換するような行列で、
ビュー行列と呼ばれます。

ビュー行列V の回転成分V_Rは、以下のように書くことができます

V_R = \begin{pmatrix} v_{00} & v_{01} & v_{02} \\ v_{10} & v_{11} & v_{12} \\ \red{v_{20}} & \red{v_{21}} & \red{v_{22}} \\\end{pmatrix}

赤く色を付けた要素が、カメラの前方向ベクトルとなります。


カメラの前方向ベクトルを計算する

カメラ空間では、カメラの前方向ベクトルは (0, 0, -1) を向いています。

これは、ワールド空間のカメラ前方向ベクトル (C_x, C_y, C_z)に、
ビュー行列を適用するとカメラ空間の前方向ベクトル (0, 0, -1) が得られるということを意味しています。

数式で書くと、以下のようになります。

\begin{pmatrix} 0 \\ 0 \\ -1 \end{pmatrix} = \begin{pmatrix} v_{00} & v_{01} & v_{02} \\ v_{10} & v_{11} & v_{12} \\ \red{v_{20}} & \red{v_{21}} & \red{v_{22}} \\\end{pmatrix} \begin{pmatrix} C_x \\ C_y \\ C_z \\\end{pmatrix}

ビュー行列の逆行列を用いることで、カメラの前方向ベクトルが求まります。
(補足: 回転行列の転置は、回転行列の逆行列になります。)

\begin{aligned} \begin{pmatrix} C_x \\ C_y \\ C_z \end{pmatrix} &= \begin{pmatrix} v_{00} & v_{01} & v_{02} \\ v_{10} & v_{11} & v_{12} \\ \red {v_{20}} & \red {v_{21}} & \red {v_{22}} \\\end{pmatrix} ^ {-1} \begin{pmatrix} 0 \\ 0 \\ -1 \\\end{pmatrix} \\\\ &= \begin{pmatrix} v_{00} & v_{01} & v_{02} \\ v_{10} & v_{11} & v_{12} \\ \red {v_{20}} & \red {v_{21}} & \red {v_{22}} \\\end{pmatrix} ^ {T} \begin{pmatrix} 0 \\ 0 \\ -1 \\\end{pmatrix} \\\\ &= \begin{pmatrix} v_{00} & v_{10} & \red{v_{20}} \\ v_{01} & v_{11} & \red{v_{21}} \\ v_{02} & v_{12} & \red{v_{22}} \\\end{pmatrix} \begin{pmatrix} 0 \\ 0 \\ -1 \\\end{pmatrix} \\\\ &= \begin{pmatrix} -\red{v_{20}} \\ -\red{v_{21}} \\ -\red{v_{22}} \\\end{pmatrix} \end{aligned}

ビュー行列の2列目にカメラの前方向ベクトルの要素が格納されているので、以下のシェーダーコードでカメラの前方向ベクトルを取ることができます。

float3 axisZ = -UNITY_MATRIX_V[2].xyz;

参考記事

https://qiita.com/yuji_yasuhara/items/8d63455d1d277af4c270

手法 4.ビューベクトルを求める

描画点からカメラまでの向きベクトル(ビューベクトル)を取得するには、URPの GetWorldSpaceViewDir 関数を使うと良いでしょう。

float3 positionWS = TransformObjectToWorld(v.vertex);
float3 viewDir = GetWorldSpaceViewDir(positionWS);

GetWorldSpaceViewDir は、 OrthographicとPerspective の両方で動作するように実装されています。

GetWorldSpaceViewDirの実装
com.unity.render-pipelines.universal@10.8.1/ShaderLibrary/ShaderVariablesFunctions.hlsl
// Computes the world space view direction (pointing towards the viewer).
float3 GetWorldSpaceViewDir(float3 positionWS)
{
    if (IsPerspectiveProjection())
    {
        // Perspective
        return GetCurrentViewPosition() - positionWS;
    }
    else
    {
        // Orthographic
        return -GetViewForwardDir();
    }
}

手法 5. 頂点座標から深度値を求める

Orthographic

Linear01DepthやLinearEyeDepthを用いることで、深度値を求めることができます。

// ニアクリップ面:0 ファークリップ面:1
float depth01 = Linear01Depth(clipZ, _ZBufferParams); 

// ニアクリップ面:0 ファークリップ面:f
float eyeDepth = LinearEyeDepth(clipZ, _ZBufferParams); 

Perspective

Perspective では、clipZ が 0 ~ 1 の深度値となっていますが、グラフィックスAPIによって向きが異なっています。

#if UNITY_REVERSED_Z
float depth = 1.0 - v.z;  // ニアクリップ面:0 ファークリップ面:1
#else
float depth = v.z;  // ニアクリップ面:0 ファークリップ面:1
#endif

Orthographic と Perspective の両方で動作させる

URPの、IsPerspectiveProjection() という関数を使うことで、PerspectiveかOrthogprahicsかを判断できるので、
これを利用します。

half4 frag (v2f input) : SV_Target
{
    // クリップ空間の頂点座標
    float clipZ = input.positionCS.z;

    if (IsPerspectiveProjection())
    {
        // Perspective
        // ニアクリップ面:0 ファークリップ面:1
        float depth01 = Linear01Depth(clipZ, _ZBufferParams); 
        // ニアクリップ面:0 ファークリップ面:f
        float eyeDepth = LinearEyeDepth(clipZ, _ZBufferParams); 

        return depth01;
    }
    else
    {
        // Orthographic
        #if UNITY_REVERSED_Z
        // ニアクリップ面:0 ファークリップ面:1
        float depth01 = 1.0 - clipZ;  
        #else
        // ニアクリップ面:0 ファークリップ面:1
        float depth01 = clipZ;  
        #endif

        return depth01;
    }
}

手法 6. スクリーン座標を求める

スクリーン座標を取得したい場合、以下のようなシェーダーを書きます。

  1. vertシェーダー側で、ComputeScreenPosでクリップ空間座標からスクリーン座標を計算
  2. fragシェーダーで、スクリーン座標をwで除算
v2f vert (appdata v)
{
    v2f o;
    o.positionCS = TransformObjectToHClip(v.vertex);
    o.screenPos = ComputeScreenPos(o.positionCS);
    return o;
}

half4 frag (v2f input) : SV_Target
{
    float2 screenPos = input.screenPos.xy / input.screenPos.w;
    return float4(screenPos, 0, 1);
}

https://light11.hatenadiary.com/entry/2018/06/13/235543

手法 7. 頂点を画面奥へずらす

画面上の頂点の見た目を変えず、頂点の深度値だけをずらす変えるテクニックになります。
グラフィックスAPIによって、クリップ空間のz値の向きが異なるため注意が必要です。

#if UNITY_REVERSED_Z
o.positionCS.z -= 0.01; // ニアクリップ面:z=1 , ファークリップ面:z=0
#else
o.positionCS.z += 0.01; // ニアクリップ面:z=0 , ファークリップ面:z=1
#endif

// クリップ座標のz値が0~1の範囲外に出ると、表示されなくなるためclampをかける
o.positionCS.z = clamp(o.positionCS.z, 0.001, 0.999);

関連

https://nagakagachi.hatenablog.com/entry/2018/09/08/203717
https://media.colorfulpalette.co.jp/n/n927a776b6b35#81c0b51e-3648-4e60-92fe-f5da8fc5def3

手法 8. 一定の太さのアウトラインを作る

距離の影響を受けないアウトライン

カメラから離れるほど、W除算によってクリップ空間上の頂点の移動量が小さくなります。
クリップ座標のW値を乗算することで、W除算の影響を打ち消すことができるため、
距離の影響を受けないアウトラインを作ることができます。

v2f vert (appdata v)
{
    v2f o;
    o.positionCS = TransformObjectToHClip(v.vertex);

    float3 normal = TransformObjectToWorldDir(v.normal);
    normal = TransformWorldToHClipDir(normal);
    
    #define WIDTH 0.005

    // アウトライン用の頂点移動 (クリップ座標のWで乗算)
    o.positionCS.xy += normal.xy * WIDTH * o.positionCS.w; 

https://zenn.dev/r_ngtm/articles/shaderlab-outline

FOVの影響を受けないアウトライン

FOVが大きくなるほど、クリップ空間上の頂点座標の移動量が小さくなります。
unity_CameraProjection._m11 で割ることで、FOVの影響を打ち消すことができるため、
FOVの影響を受けないアウトラインを作ることができます。

v2f vert (appdata v)
{
    v2f o;
    o.positionCS = TransformObjectToHClip(v.vertex);

    float3 normal = TransformObjectToWorldDir(v.normal);
    normal = TransformWorldToHClipDir(normal);
    
    #define WIDTH 0.005

    // アウトライン用の頂点移動 (unity_CameraProjection._m11で除算)
    o.positionCS.xy += normal.xy * WIDTH / unity_CameraProjection._m11; 

https://zenn.dev/r_ngtm/articles/shaderlab-outline-2

手法 9. 深度値からワールド座標を復元する

スクリーン座標と深度値が決まると、そのワールド座標を求めることができます。

void ReconstructWorldFromDepth(float2 ScreenPosition, float Depth, float4x4 unity_MatrixInvVP, out float3 Out)
{
    // スクリーン座標とDepthからクリップ座標を作成
    float4 positionCS = float4(ScreenPosition * 2.0 - 1.0, Depth, 1.0);

    #if UNITY_UV_STARTS_AT_TOP
        positionCS.y = -positionCS.y;
    #endif

    // クリップ座標にView Projection変換を適用し、ワールド座標にする    
    float4 hpositionWS = mul(unity_MatrixInvVP, positionCS);

    // 同次座標系を標準座標系に戻す
    Out = hpositionWS.xyz / hpositionWS.w;
}

https://zenn.dev/r_ngtm/articles/shadergraph-reconstruct-wpos-depth

その他 : 空間変換系の便利関数

座標変換系

URPには、座標を変換するための関数が多く用意されています。

float3 TransformObjectToWorld(float3 positionOS)
float3 TransformWorldToObject(float3 positionWS)
float3 TransformWorldToView(float3 positionWS)
float4 TransformObjectToHClip(float3 positionOS)
float4 TransformWorldToHClip(float3 positionWS)
float4 TransformWViewToHClip(float3 positionVS)
float3 TransformTangentToWorld(float3 dirTS, float3x3 tangentToWorld)
float3 TransformWorldToTangent(float3 dirWS, float3x3 tangentToWorld)
float3 TransformTangentToObject(float3 dirTS, float3x3 tangentToWorld)
float3 TransformObjectToTangent(float3 dirOS, float3x3 tangentToWorld)

GetVertexPositionInputs

GetVertexPositionInputs()を使うと、各種空間の座標をまとめて取れます。

VertexPositionInputs positionInputs = GetVertexPositionInputs()
struct VertexPositionInputs
{
    float3 positionWS; // World space position
    float3 positionVS; // View space position
    float4 positionCS; // Homogeneous clip space position
    float4 positionNDC;// Homogeneous normalized device coordinates
};

向き変換系

向きを変換するための関数も用意されています。

float3 TransformObjectToWorldDir(float3 dirOS, bool doNormalize)
float3 TransformWorldToObjectDir(float3 dirWS, bool doNormalize)
float3 TransformWorldToViewDir(float3 dirWS, bool doNormalize)
float3 TransformWorldToHClipDir(float3 directionWS, bool doNormalize)
float3 TransformObjectToWorldNormal(float3 normalOS, bool doNormalize)
float3 TransformWorldToObjectNormal(float3 normalWS, bool doNormalize)

Discussion