1つのナナイトメッシュでMaskedとOpaqueのマテリアルが混在する場合に、PixelProgrammableDistanceを越えるとWPOが無効化されてしまう

■ 発生している問題

表題の通り、MaskedとOpaqueのマテリアルが混在している場合に、PixelProgrammableDistanceを越えたところでOpaqueのWorldPositionOffsetが無効になってしまいます。

<br/>

MaskedマテリアルはPixelProgrammableDistanceを越えたところでMaskedが無効化され、WorldPositionOffsetDisableDistanceを越えたところでWPOが無効化され、期待通りの挙動をしています。

<br/>

Opaqueマテリアルの期待する挙動はPixelProgrammableDistanceを越えたところではMaskedではないので何も起きず、WorldPositionOffsetDisableDistanceを越えたところでWPOが無効化されることです。

<br/>

何か回避方法はありますでしょうか?

<br/>

<br/>

<br/>

<br/>

■ 詳細

以下、弊社で調査してみた内容と考察になります。

おそらく以下の部分でFSceneProxyBaseのbHasPixelProgrammableRasterが各MaterialSectionのorでフラグが有効化されています。そのため、MaskedとOpaqueが混在していた場合にPixelProgrammableDistanceを越えた段階でFallbackが発生しているのだと思います。

<br/>

NaniteResources.cpp

void FSceneProxyBase::OnMaterialsUpdated(bool bOverrideMaterialRelevance)
{
	// 省略
	for (auto& MaterialSection : MaterialSections)
	{
		// 省略
		// Now that the material relevance is updated, determine if any material has programmable raster
		const bool bVertexProgrammableRaster = MaterialSection.IsVertexProgrammableRaster(bEvaluateWorldPositionOffset);
		const bool bPixelProgrammableRaster = MaterialSection.IsPixelProgrammableRaster();
		bHasVertexProgrammableRaster |= bVertexProgrammableRaster;
		bHasPixelProgrammableRaster |= bPixelProgrammableRaster;

<br/>

<br/>

おそらくOpaqueマテリアルだけであればPixelProgrammableDistanceは0が渡り、PixelProgrammableDistanceを越えた所でFallbackRasterが使われてしまうのではないかと思っています。

<br/>

NaniteSceneProxy.h

class FSceneProxyBase : public FPrimitiveSceneProxy
{
  // 省略
  inline float GetPixelProgrammableDistance() const
  {
      return HasPixelProgrammableRaster() ? PixelProgrammableDistance : 0.0f;
  }

NaniteCullingCommon.ush

		BRANCH
		if ( bIsVisible )
		{
			// 省略
			// Determine if we should use fallback raster bins due to disabling pixel programmable raster
			if ( PixelProgrammableDistanceSq > 0.0f && !bSkipPixelProgrammableDistance )
			{
				bFallbackRaster = InstanceDrawDistSq > PixelProgrammableDistanceSq;
			}

<br/>

<br/>

試しに以下のFallback選択の部分でFixedFunctionPipelineを使わないようにしたところOpaqueが正しくWPODisableDistanceを越えた所でWPOが無効になりました。しかし、Fallback先をFixedFunctionPipelineから変えてしまったため、OpaqueマテリアルのRasterBinが変わらなくなっていそうなため問題がありそうです。

<br/>

<br/>

NaniteShading.cpp

bool FNaniteRasterPipeline::GetFallbackPipeline(FNaniteRasterPipeline& OutFallback) const
{
	if ((bPerPixelEval && bHasPixelDistance) || (bDisplacementEnabled && bHasDisplacementFadeOut))
	{
		// 省略
	}
	else if (bHasWPODistance)
	{
		if (bPerPixelEval || bDisplacementEnabled)
		{
			// 省略
		}
		else
		{
			// OutFallback = GetFixedFunctionPipeline(FixedBinMask);
			OutFallback = *this;
		}
	}
}

<br/>

再現手順
この問題はスタティックメッシュでNaniteが有効化され、かつWorldPositionOffset Disable DistanceNanite PixelProgrammableDistanceの両方が使用されている場合に発生します。

1. 2つ以上のマテリアルを使用できるスタティックメッシュを準備します(e.g. 葉と幹で別々のマテリアルを持つ木)

2. レベル内にそのスタティックメッシュアクターを配置し、以下の2つの設定を適用します:

  • WorldPositionOffset Disable Distance を 1000 に設定します
  • PixelProgrammableDistance を 500 に設定します

3. カメラを動かしてテストします:

  • カメラをスタティックメッシュアクターから徐々に遠ざけます。距離が500を超えると問題が発生します。
  • 期待される結果:Pixel Programmable機能のみが無効化されて、1000を過ぎたときにWPOが無効化される
  • 実際の結果:WPOも予期せず無効化される

お世話になっております。

本件返信にお時間を頂いてしまい申し訳ございません。

弊社都合で大変恐縮ですが、イベント等の対応で調査に遅延が発生してしまっており順次対応させていただいている状態となります。

現在ご用意頂いたサンプルプロジェクトを元に本件に関して詳細を確認させていただいておりますので今しばらくお待ちいただけますと幸いです。

よろしくお願いいたします。

本件詳細を調査頂きありがとうございます。

指摘頂いた内容を含めて改めて確認させていただいておりましたが、現状をまとめると以下のような問題があり対応が難しいといった認識です。

・各距離の設定がコンポーネント単位での指定

・bFallbackRasterから駆動するFallbackPipelineは1つ

・bFallbackRasterを制御している箇所がNaniteCullingのシェーダーとなっており各Section毎の情報を取得していない

こちらの挙動に関しては不具合として報告させていただければと考えておりますが、現状の回避方法としましては既に挙げて頂いている固定パイプラインのFallcack箇所を、MaskedかつWPOがある場合のFallbackと同様のRasterPipelineに変更する、といった方法になってしまいそうです。

※WPOはbFallbackRasterが設定される箇所で変更されているbEnableWPOから、NANITE_CULLING_FLAG_ENABLE_WPOを経由して評価がオンオフされているようでした。

bEnableWPO = InstanceDrawDistSq < PrimitiveData.InstanceWPODisableDistanceSquared;

こちらはMaskedかつWPOを持ちDisableDistanceが設定されているBinと同様のコストとなる想定となっており、

お手数ですが、プロジェクトで想定される物量でパフォーマンスが許容できるかご確認いただければと思います。

よろしくお願いいたします。

お忙しいところご回答いただきありがとうございます。

不具合としてご報告いただけるとのことですが、現状では修正が難しく、可能なところとしては前述のFallbackを変更する等の対応が必要な旨、承知いたしました。

今後は、パフォーマンスを確認しつつ該当箇所を変更するか、OpaqueとMaskedが混在しない別のアプローチをとるかを検討して進めたいと思います。

この度はご対応いただきありがとうございました。

MaskedかつWPOがある場合のFallbackと同様のRasterPipelineに変更する方法について、パフォーマンスの確認を行ったため共有させていただきます。

上記の対応によってパフォーマンスが悪化するケースとして考えられるのは、WPO等を使用して個別のRasterBinが割り当てられているものが多く存在している場合かと思われます。本来はDisableDistanceを越えることでFallbackが行われ、同時に存在するRasterBinが​減り負荷が軽減されるかと思います。しかし上記の対応によりDisableDistanceを越えても常に個別のRasterBinが割り当てられて続けてしまい、負荷が下がらずパフォーマンスが悪化してしまうのではないかと予想しています。

上記のケースが確認できるような​テストマップを用意して確認してみました。

  • Maksed/Opaque混合のオブジェクトを複数用意しFoliageで配置
  • WPO/PixelProgrammableDistanceのDisableDistanceを再現手順と同じ値に設定
  • 遠景で同時に多くのオブジェクトが映るようにカメラを配置​

上記の条件で対応の有りと無しを比較しました。結果はDrawスレッドが約5ms前後近く増加し、fpsでは約15%ほど​悪化してしまいました。

そのため現在のプロジェクトでは、OpaqueとMaskedを同時に使用するのを避けることで回避しようと思っています。

ご確認及び情報共有頂きありがとうございます。

固定パイプラインへのフォールバックがない場合パフォーマンス上運用が難しいとのことで、現状は検討頂いているようにMaterialを分けて運用頂く形となってしまいそうです。

また本件に関しましては以下Issueとして報告させて頂いており、こちらは進展があり次第ご連絡させていただければと思います。

UE-355567 Pixel Programmable Distance disables WPO too early on Nanite Meshes with masked and opaque materials

ご不便をおかけしてしまいますが、よろしくお願いいたします。