URP でビルボードシェーダ [ゲーム開発ログ 2020-11-07]

2020-11-07   (Updated : 2020-11-15)

URP シェーダにビルボード機能を組み込む

  • ビルボードとは 3D ゲームの開発で昔から使われている、常に画面の正面を向くようなオブジェクトのこと
  • キャラの上に出すアイコンなんかにビルボードを使いたかったので用意することにした
  • GPU レベルで(シェーダで)ビルボードを描画する方法って Unity 標準では用意されてなさそうなので、自分で実装する必要がある

実装

  • Unity の URP 7.3.1 のシェーダの以下のところを置き換える:
input.positionCS = TransformWorldToHClip(input.positionWS);
※ URP 7.3.1 のカスタムシェーダを手で書く際のテンプレートを以前作ったので、
Unity 組み込みの SimpleLit シェーダをいじったシェーダを書きたくなった時は参考にされたい:


  • 自分の実装:
UNITY_BRANCH
if (_BillboardOn > 0)
{
    float2 scale = float2(
        length(float3(UNITY_MATRIX_M[0].x, UNITY_MATRIX_M[1].x, UNITY_MATRIX_M[2].x)),
        length(float3(UNITY_MATRIX_M[0].y, UNITY_MATRIX_M[1].y, UNITY_MATRIX_M[2].y))
    );
    input.positionCS = mul(
        UNITY_MATRIX_P,
        mul(UNITY_MATRIX_MV, float4(0, 0, 0, 1))
            + float4(positionOS.xy, 0, 0) * float4(scale.x, scale.y, 1, 1)
    );
}
else
{
    input.positionCS = TransformWorldToHClip(input.positionWS);
}

説明

座標変換はオブジェクトのローカル座標に 3 つの行列をかけることで行われる。

まずの元のコードの TransformObjectToHClip()、 これは旧来の Builtin-RP では UnityObjectToClipPos() に相当する処理だが、実装は以下のようになっている:

// Transforms position from object space to homogenous space
float4 TransformObjectToHClip(float3 positionOS)
{
    // More efficient than computing M*VP matrix product
    return mul(GetWorldToHClipMatrix(), mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)));
}

これは噛み砕くと次のようなコードになる:

return mul(
    UNITY_MATRIX_V, mul(
        UNITY_MATRIX_P, mul(
            UNITY_MATRIX_M, float4(positionOS, 1.0)
        )
    )
);

positionOS は Object Space における位置、つまりオブジェクトのローカル座標。 これに M と V と P の行列をかけて座標変換しているわけだ。

M / V / P の行列の意味は以下:

定数 内容 説明
UNITY_MATRIX_M モデル変換の行列 オブジェクトのローカル座標からワールド座標への変換
UNITY_MATRIX_V ビュー変換の行列 ワールド座標からカメラ基準の座標系へ変換
UNITY_MATRIX_P プロジェクション変換の行列 カメラ基準の座標からスクリーン座標に変換

で、ビルボード変換のコードだが

    input.positionCS = mul(
        UNITY_MATRIX_P,
        mul(UNITY_MATRIX_MV, float4(0, 0, 0, 1))
            + float4(positionOS.xy, 0, 0)
    );

本来オブジェクトの頂点座標に対して MVP 変換をかけるところを、 原点座標 (0, 0, 0) にかけることで回転を無効化している。 これだけだと原点座標のままなのでその後別途 xy 座標を足してやる。

この時点でビルボードらしい見た目にはなるのだが、これだと GameObject の Transform で指定した Scale 値が反映されない。 原点座標でモデル変換を行っている関係で、スケールの変換も無視されているからだ。

そこで、xy 座標を足すときにスケール値も考慮してやる。 Transform の Scale 値は(自分の知る限り)直接シェーダには渡っていないと思うが、 モデル行列の要素から計算できるので計算してやる。

計算した scale をかけてやって、最終的には以下のようなコードとなった:

    float2 scale = float2(
        length(float3(UNITY_MATRIX_M[0].x, UNITY_MATRIX_M[1].x, UNITY_MATRIX_M[2].x)),
        length(float3(UNITY_MATRIX_M[0].y, UNITY_MATRIX_M[1].y, UNITY_MATRIX_M[2].y))
    );
    input.positionCS = mul(
        UNITY_MATRIX_P,
        mul(UNITY_MATRIX_MV, float4(0, 0, 0, 1))
            + float4(positionOS.xy, 0, 0) * float4(scale.x, scale.y, 1, 1)
    );

参考リンク