UE5.6で追加されたファーストパーソンレンダリングとは？

# はじめに
　UE5.6でファーストパーソンレンダリングというものが追加されました。
どんなものかは[リリースノート](https://dev.epicgames.com/documentation/ja-jp/unreal-engine/unreal-engine-5-6-release-notes)を読むと良いですが、抜粋すると

>ファースト パーソン レンダリング メソッドは、プレイヤーの手/武器がワールド内オブジェクトに入らないようにするものであり、デベロッパーがファースト パーソン アセットに対して他の場所とは異なる視野角 (FOV) を指定できる個別の FOV が含まれています。

　ということです。
　これ、実はファーストパーソン・シューターではけっこう昔からやられていて、筆者も十数年くらい前にUE3で実装したことがあります。そのときはけっこう無理やりで、セルフシャドウとかうまく処理できませんでしたが。
　なぜこんな描画をやりたいかというと、ゲームにもよりますが

 - プレイヤーの腕や武器が壁に張り付くとめり込んだりして不自然になるのを防ぎたい
 - プレイヤーの腕や武器を大きく見せたり画面いっぱいに見せたい
 - カメラに近いと画角によっては歪むのを緩和したい
 - 画角を変更できるゲームでプレイヤーの表示はある程度固定したい

　などの事情があるんじゃないかと思います。

　といった感じです。噛み砕いていくとファーストパーソンでより臨場感を増したり自然に見せるための表現と捉えると良いかと思います。

# 解析してみよう
　早速ですが、何をやってるのか解析したいと思います。
　まずはどんなパラメーターで制御されているのか

```SceneVies.h
struct FFirstPersonParameters
{
	/** FOV correction factor applied to the first person transform used on primitives tagged as "IsFirstPerson". This should be computed as tan(SceneFOVRadians * 0.5) / tan(FirstPersonFOVRadians * 0.5). */
	float FOVCorrectionFactor = 1.0f;

	/** The scale to apply to primitives tagged as "IsFirstPerson". This is used to scale down primitives towards the camera such that they are small enough not to intersect with the scene. */
	float Scale = 1.0f;

	/** If bUseParameters is true, FOV and Scale should be applied to primitives tagged as "IsFirstPerson". */
	bool bUseParameters = false;

	FFirstPersonParameters() = default;
	FFirstPersonParameters(float InFOVCorrectionFactor, float InScale, bool bInUseParameters) : FOVCorrectionFactor(InFOVCorrectionFactor), Scale(InScale), bUseParameters(bInUseParameters) {}
};
```

　画角とスケールを操作できるようですね。どこに設定するのかというと、CameraComponentの様です。

 ```CameraComponent.h
	/**
	 * The horizontal field of view (in degrees) used for primitives tagged as "IsFirstPerson".
	 */
    UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = CameraSettings, meta = (UIMin = "5.0", UIMax = "170", ClampMin = "0.001", ClampMax = "360.0", Units = deg, EditCondition = "bEnableFirstPersonFieldOfView"))
	float FirstPersonFieldOfView;
 
	/**
	 * The scale to apply to primitives tagged as "IsFirstPerson". This is used to scale down primitives towards the camera such that they are small enough not to intersect with the scene.
	 */
	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = CameraSettings, meta = (UIMin = "0.001", UIMax = "1.0", ClampMin = "0.001", ClampMax = "1.0", EditCondition = "bEnableFirstPersonScale"))
	float FirstPersonScale;
```

## 実際に設定してみよう
　ということでFirstPersonTemplateで実際に試してみましょう。
　なんですけど、UE5.6からけっこうテンプレートが変わってて、FirstPersonTemplateで銃を持たなくなっちゃいました。うーん、やりにくい。Variant_Shooterフォルダのレベルとかを開けば銃持って撃てるんですが、デフォルトのレベルは手を降って走り回るだけになったみたいです。
　あと、諸々設定してもエディタ上では確認できません。実行時にのみ適用されるのでプレイを押して確認しましょう。

 手っ取り早く試します。この円柱の右側のアクターを選択してプロパティを確認
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/6d7856e1-02f5-4e2a-94e6-a317640beec1.png)

First Person Primitive TypeでFirst Personに設定したアクターが対象になるみたいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/e45a6a16-d972-4d44-a7f3-618bd534f59c.png)

　設定したら影が消えちゃいましたね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/6af76a92-7eed-439c-905a-345945defa8e.png)

　影以外は特に変わらないみたいです。ここでプレイしてみると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/f4733e35-3541-4626-bf4c-2769560df522.png)
 こんな感じに手前に飛び出します。
 F8押してCameraComponentを選択すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/a28057be-027a-4ef6-bd22-b11b28ee6201.png)
　ここでFOVとスケールが変更できます。
　実際の描画パスがどうなっているかをRenderDocで見てみると、BasePassで２回に分けて描画されているのが確認できます。
　まずFirstPersonRendering対象のアクターと床が描画され
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/532f3c7b-c1a0-448e-892f-266963101a07.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/240c6fd5-87f4-422e-a871-39fa0dddc5e2.png)

　次にそれ以外のアクターが描画されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/b9778059-3dd5-421b-b22d-130e48c92dae.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/2697e808-8564-46f4-a10c-107190064127.png)

## 実際にはどういうもの？
　ここまでの結果でだいたいわかったことをまとめます。
- 指定したアクターをシーンのカメラとは違う画角、違うスケールで別に描画する
- ファーストパーソンレンダリング対象は先に描画される
- ファーストパーソンレンダリング対象はシャドウをキャストしない

　ドキュメントにも

シャドウ マップ レンダリング (VSM)
レイ トレーシング反射およびシャドウ
ファースト パーソン セルフ シャドウ (スクリーン空間)

　とあるので、通常のデプスシャドウでのシャドウの事は考慮されていないみたいです。まあ、異なるView空間のシャドウの辻褄をあわせるのは困難なので仕方ないでしょう。
　実際のコードを見てみますが、その前に、ActorのFirst Person Primitive Typeに設定できる項目を見てみると
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/49ca6561-bd12-4e79-ab71-266ee29cedab.png)
　First PersonがFirst Person Renderingの指定ですが、もうひとつWorld Space Representationというのがあります。ツールチップにはこう書いてあります
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/a5bd062b-e6b5-4429-9cbb-f85db1a538ae.png)

　コードも見てみましょう 
```PrimitiveSceneProxy.cpp
	// FirstPerson primitives won't properly work with most shadow solutions, so just disable them here to give a smoother user experience out of the box.
	// Such primitives are intended to be used in conjunction with FirstPersonWorldSpaceRepresentation primitives, which are used to cast shadow onto the scene.
	// Self shadowing for FirstPerson primitives is solved with First Person Self Shadows, which are currently implemented with screen space ray marching.
	// Note that bCastContactShadow and a few of the legacy shadow cast flags are not overridden here. This is intentional as there might be valid use cases for them in this context.
	if (bIsFirstPerson)
	{
		bCastDynamicShadow = false;
		bCastStaticShadow = false;
		bCastVolumetricTranslucentShadow = false;
		bCastFarShadow = false;
	}
	// FirstPersonWorldSpaceRepresentation primitives must have bOwnerNoSee=true and bCastHiddenShadow=false.
	// This way, they are skipped by regular shadow rendering and can be picked up by the special first person
	// shadow path in VSM. Raytraced shadows ignore these flags anyways, so this works for all cases.
	else if (bIsFirstPersonWorldSpaceRepresentation)
	{
		bOwnerNoSee = true;
		bCastHiddenShadow = false;
	}

```
　First Personはキャストシャドウが全てオフにされています。レシーブシャドウやスクリーンスペースシャドウは適用されるはず。World Space Reprentationは他のプレイヤーから見た影を描画するためのアクターということのようです。オンラインマルチなゲームでは、自キャラを両方のタイプで２回描画する必要があるのでしょうか。

```NaniteResouces.cpp
	if (IsFirstPerson())
	{
		// First person primitives are currently not supported in raytracing as this kind of geometry only makes sense from the camera's point of view.
		return ERayTracingPrimitiveFlags::Exclude;
	}
```

```SkeletalMeshSceneProxy.cpp
	bCastCapsuleDirectShadow = InMeshDesc.bCastDynamicShadow && InMeshDesc.CastShadow && InMeshDesc.bCastCapsuleDirectShadow && !InMeshDesc.bIsFirstPerson;
	bCastsDynamicIndirectShadow = InMeshDesc.bCastDynamicShadow && InMeshDesc.CastShadow && InMeshDesc.bCastCapsuleIndirectShadow && !InMeshDesc.bIsFirstPerson```
```

```NaniteResourcesHelper.h
		// Force disable Nanite on first person primitives as they cause issues when used together with bOwnerNoSee/bOnlyOwnerSee, which is often the case for FirstPerson.
		const bool bIsFirstPersonRelevant = Component.IsFirstPersonRelevant();
		if (bForceFallback || Component.IsDisallowNanite() || Component.IsForceDisableNanite() || bIsFirstPersonRelevant)
		{
			// Regardless of the static mesh asset supporting Nanite, this component does not want Nanite to be used
			return false;
		}
```

```PrimitiveSceneInfo.cpp
		// First person primitives potentially deform the geometry outside of its bounds in a view dependent way. They are very unlikely to be occluded anyways,
		// so to avoid falsely culling them, it is better to simply don't occlusion cull them at all.
		if (Proxy->CanBeOccluded() && !Proxy->IsFirstPerson())
		{
			OcclusionFlags |= EOcclusionFlags::CanBeOccluded;
		}
```
　レイトレから外されたりNaniteから外されたりオクルージョンクエリから外されたり、いろいろ制限あるみたいですね。でもレイトレースシャドウは描画されますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/216e954a-e571-46f1-a5f9-7f751910878a.png)
　ファーストパーソンレンダリング用のパスでFirst Person指定をしていない床も描画されているので、どういう処理なのかとちょっと調べましたが、どうも地面に影を描くためにライトの影響範囲を調べてパスに投入してるっぽい処理がありましたが、まだ詳しく追えてません。
　自分が過去に似たようなものを実装したときはPixel Depth Offsetみたいな感じで深度にオフセットかけて背景にめり込みにくくしたりしましたが、そういう機能は無いみたいです。スケールで小さくして画角を狭めることで元の背景に対しては小さく描画されるのでめり込み難くなる効果はあるのかな。
 
## 関連機能
　ポストプロセスマテリアルのSceneTextureにIsFirstPersonが追加されて、First Person Renderingで描画された領域を識別できます。こんなマテリアルを組んでみると
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/6fdfe537-bdb6-442f-afe5-cb87867c8e8d.png)
　こんなふうになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/b6cafb51-0fc9-4e6e-a304-7a3fa64a765e.png)

　マテリアルノード用には First Person Outputというノードが追加されています。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/6521a886-216c-470d-94d8-057c6cd3e32a.png)
　0を入力すると通常の位置に表示されます。1だとFirst Person Renderingで設定した位置ですね。カメラから遠くなると通常の位置に描画みたいに使えるのかなと思います。
　IsFristPersonはFirst Person Renderingの対象かどうかの判定です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/1a9a9d1a-7b39-499c-8b60-10d215744840.png)

　TransformPositionノードの出力にFirst Person Space(Camera Relative World Space)が追加されました。WorldPostionノードをCamera Relative World Postionを設定して入力すると、First Person Rendererでの座標系に変換してくれます。このように計算してWorld Postion Offsetに入力してやればFirst Person Rendererと同様の座標変換が可能になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/592799b7-33cc-4d45-82ef-84de661d4149.png)
　First Person指定じゃないアクターをFirest Person Rendererで変換される位置に描画することができます。
 
# まとめ
　UE5.6で追加されたファーストパーソンレンダリングをざっくり調べてました。鮮度の良いうちに記事にまとめたので参考にしてください。まだベータですし、今後も変わって行く可能性あります。マルチプレイでの見え方とかレイトレの挙動とかは時間があったら調べてみたいですが、まずは速報として。
　ぶっちゃけ何に使えるかというと、ちょっと微妙で、どうしてもシャドウとの相性の悪さが目立ちます。カメラに近い自キャラなのでシャドウが無くてもそれほど気にならないという場合には有効かな。あるいはレイトレを有効にできる環境ならいい感じに使えるかもしれません。
　対象がNanite描画にならないし、パスが２段になるので当然描画負荷も若干上がります。FPSなら腕と武器だけなのでそれほど問題にはならないと思いますが。
　応用としてはよくあるステータス画面にキャラを3D描画とかに使えるかもしれません。ポストプロセスでファーストパーソンレンダリングを判別できるので、キャラだけ残して別背景描画するとか。プレイ中のキャラがシームレスにステータス画面に表示されるとかなんか面白い使い方を考えてみたいところです。
　自分としては改造して別用途の２パスレンダリングを実装する起点にするとかかな。

　ということで、UE5.6の新機能、ファーストパーソンレンダリングをざっくり調べてみました。即興間違い勘違いあるかもしれませんが、ご容赦を