情報は力ではない

UE4 とか Blender とか。

UE4 + ARCore でデプスシャドウを実装してみた。

概要

今、UE4 + ARCore でアプリケーションを作ってます。 現実世界に Gray ちゃんを召喚して好きなポーズをつけることができるアプリを目指してます。

キャラクターを Spawn したときに、現実世界に落とす影を、丸影じゃなくてキャストシャドウを使いたいなぁと思ったので、やってみました。 少し C++ を使ってます。

まだ荒いですが、こんな感じです。

f:id:masahiro8080:20181125192750p:plain

環境

Version
Unreal Engine 4.21.0
Android 9 (Pixel 3 XL)

方針

影を落とすのにどういう方法があるのかわからなかったので調べてたのですが @ruyo_h さんの MToon の記事をたまたま見つけました。

qiita.com

この記事の方法が使えそうだったので、実装してみることにしました。

ざっくり説明すると、光源方向に配置した SceneCaptureComponent2D を使ってシャドウマップを作成し、影を落とす地面のマテリアルで事前に作成したシャドウマップを見ながら地面メッシュの各ピクセルが影なのかどうかを決めていくという方法です。

実装

設定

Mobile HDR にチェックを入れています。 また、Build は ES 3.1 を使用しています。

f:id:masahiro8080:20181202220504p:plain f:id:masahiro8080:20181202220416p:plain

シャドウマップの準備

まずはじめにシャドウマップの準備をします。 シャドウマップには Render Target を使用します。Render Target を作成し、名前は RT_ShadowMap としておきます。 作成した Render Target の設定は次のようにしました。

f:id:masahiro8080:20181129204303p:plain

後で説明する SceneCaptureComponent2D の設定で、Render Target のアルファチャンネルに深度値を格納するために Render Target Format を RTF RGBA16f にしています。

次に SceneCaptureComponent2D の設定をしていきます。 Pawn を作成し、SceneCaptureComponent2D を追加します。 この Pawn には、影を落とすための地面メッシュも同時に追加しておきます。

f:id:masahiro8080:20181129205249p:plain

SceneCaptureComponent2D は次の画像のように設定しておきます。 SceneCaptureComponent2D の X 軸正方向が影を落としたいメッシュを向くようにします。

f:id:masahiro8080:20181129225831p:plain

入力した箇所は具体的には以下の項目です。

Property Value
Projection Type Orhographic
Ortho Width 256
Texture Target RT_ShadowMap
Capture Source Scene Color (HDR) in RGB, Scene Depth in A

Capture Source を Scene Color (HDR) in RGB, Scene Depth in A に設定することで、RT_ShadowMap のアルファチャンネルに SceneCaptureComponent2D からの深度値が入っているはずです。

以上で、シャドウマップの設定ができました。

シャドウマップの確認

確認のために次のようなマテリアルを作って確認してみました。

f:id:masahiro8080:20181129231001p:plain

RT_ShadowMap を設定した Texture Sample ノードのアルファ値を 1000 で割った値を Base Color につないでいるだけです。 RT_ShadowMap のアルファに格納されている深度値は cm 単位で入っていると思われます。 Base Color に入れる値を 0 ~ 1 の間の値にするために、ある程度大きめの値で割っています。

作成した Pawn と、上で作成したマテリアルを設定した Plane Static Mesh をマップに配置しました。 Plane Static Mesh にシャドウマップが表示されていることが確認できました。

f:id:masahiro8080:20181129232119p:plain

影を落とす地面メッシュのマテリアル

次に、作成したシャドウマップを影を落とすための地面メッシュにマッピングさせるためのマテリアルを作成します。 そのためには、影を落とすための地面メッシュの各ピクセルが、作成したシャドウマップのどこに対応するかを求める必要があります。 その変換を行うために、影を落とすための地面メッシュの各ピクセルのワールド座標から SceneCaptureComponent2D のローカル座標への射影変換行列が必要です。 この射影変換行列を取得するために、UBPFL_GrayAR という Blueprint Function Library を C++ で作成し、次のような C++ のコードを準備しました。

UBPFL_GrayAR.h

public:
    UFUNCTION(BlueprintCallable, Category = "GrayAR")
    static TArray<FLinearColor> getInverseMatrix(const FTransform& transform);

UBPFL_GrayAR.cpp

TArray<FLinearColor> UBPFL_GrayAR::getInverseMatrix(const FTransform& transform)
{
    TArray<FLinearColor> Ret;
    Ret.SetNum(4);

    FMatrix InverseMatrix = transform.ToMatrixWithScale().Inverse();
    for (int i = 0; i < 4; i++) {
        FLinearColor color;
        color.R = InverseMatrix.M[i][0];
        color.G = InverseMatrix.M[i][1];
        color.B = InverseMatrix.M[i][2];
        color.A = InverseMatrix.M[i][3];
        Ret[i] = color;
    }
    return Ret;
}

これは、取得した Transform から、射影変換行列の逆行列を返す関数です。 射影変換行列の逆行列は、 TArray として返却しています。

この関数から返された値を次の Blueprint のようにマテリアルのパラメータに渡して使用します。

f:id:masahiro8080:20181202182109p:plain

影を落とす地面メッシュのマテリアルは次のようにしています。

このマテリアルは @ruyo_h さんの記事のマテリアルをほぼそのまま使用しています。

f:id:masahiro8080:20181202181607p:plain

使用している custom ノードも次のような感じです。

入力したパラメータから射影変換行列を作り、メッシュの各ピクセルの絶対座標を 射影変換行列と掛け算することで、メッシュの各ピクセルの絶対座標を SceneCaptureComponent2D のローカル座標に変換しています。

float4x4 m;

m = float4x4(
  float4(r1.x, r1.y, r1.z, r1.w),
  float4(r2.x, r2.y, r2.z, r2.w),
  float4(r3.x, r3.y, r3.z, r3.w),
  float4(r4.x, r4.y, r4.z, r4.w));

float4 t = mul(position, m);

return t / t.w;

最後に地面メッシュに作成したマテリアルを設定し、確認を影が落ちていることを確認します。

f:id:masahiro8080:20181202213118p:plain

Android 上でもデプスシャドウが落ちていることが確認できました。

f:id:masahiro8080:20181202220255p:plain

補足

当初は @ruyo_h さんの記事にあるように RTF R32f を使用し、R チャンネルに深度値を設定するようにしていたのですが Android 上でシャドウマップが表示されませんでした。

次の画像は Render Target Format: RTF R32f で Capture Source: Scene Depth in R を使用して Android 上で実行したものです。

f:id:masahiro8080:20181202215134p:plain

次の画像は Render Target Format: RTF RGBA16f で Capture Source: Scene Color (HDR) in RGB, Scene Depth in A を使用して Android 上で実行したものです。 (Plane Static Mesh の角度がおかしいですが)

f:id:masahiro8080:20181202215106p:plain

実装に自分の間違いもあるかもしれないのですが、RTF R32f だとシャドウマップが表示されなかったので、今回は RTF RGBA16f で実装しました。

さいごに

モバイル上で実行するには負荷が高いのかもしれませんが、AR 上で影ができたのでとりあえずは満足です。

雑な説明になってしまいましたが、以上です。