InstantNeRFで任意画像から3D再構成をするまでの流れメモ

NVIDIA InstantNeRF (InstantNGP) の環境セットアップと,
自身で用意した画像群を使った3D再構成をするフローのメモ.

基本的に公式リポジトリのセットアップ手順そのまま.
github.com

セットアップ

実行環境

WIndows10 64bit
RTX 3070

Requirementsのセットアップ

CUDA

"v10.2 or higher"
11.7 をインストール.
developer.nvidia.com

CMAKE

"v3.21 or higher"
3.24.0-rc5 をインストール.
インストーラによる system Path へのCMAKEパス追加 は true とした.
自分で追加する場合は環境変数Pathに "C:\Program Files\CMake\bin" あたりを追加する.
cmake.org

Python

"3.7 or higher"
自身の環境のAnacondaに 3.9 が含まれていたので改めてインストールはせず.

Python Modules

公式リポジトリの requirements.txt をローカルにダウンロード.
このtxtに必要なモジュールが記述されているのでpipでPython環境にインストール.
https://github.com/NVlabs/instant-ngp/blob/master/requirements.txt

pip install -r requirements.txt
OptiX

"7.3 or higher"
7.5 をインストール.
インストール後に以下の環境変数が設定されていなければ手動で追加する.
変数名 "OptiX_INSTALL_DIR"
値 "C:\ProgramData\NVIDIA Corporation\OptiX SDK バージョン番号"
developer.nvidia.com

InstantNgpのビルド

リポジトリの取得

適当なディレクトリにcloneする.

git clone --recursive https://github.com/nvlabs/instant-ngp
ビルド

cloneしたinstant-ngpディレクトリでCMAKEによるビルドを実行.

cmake . -B build
cmake --build build --config RelWithDebInfo -j
サンプルを実行

ビルドが成功したらサンプルが実行できるか確認する.
instant-ngpディレクトリでコマンドラインからnerf/foxサンプルを実行.

.\build\testbed.exe --scene .\data\nerf\fox
NeRF_Fox_Sample

狐のNeRFシーンサンプルのリアルタイム学習が確認できる.

3D再構成の準備

NeRFの学習には画像毎のカメラ姿勢情報が必要.
COLMAPを利用することで画像群からカメラ姿勢を推定することができ, NeRFの学習に利用できるようになる.
InstantNgpでは colmap2nerf.py というスクリプトが提供されており, COLMAPで推定したカメラ姿勢情報をNeRF向けに変換できる.
詳細は以下の公式tipsの「Preparing new NeRF datasets」を参考.
github.com

COLMAP

ダウンロード

COLMAP公式から pre-build binaries リンクを辿ってビルド済みバイナリを取得.
今回は COLMAP-3.7-windows-cuda.zip をダウンロードして適当なディレクトリに展開.
colmap.github.io

環境変数Pathへの追加

環境変数PathにCOLMAPディレクトリを追加する.
(colmap2nerf.pyでCOLMAP.batを参照するため)

3D再構成

新規に用意した画像群でInstatnNeRFを学習する.
簡単のために画像データは instant-ngp ディレクトリの data/nerf 下に新規にディレクトリを作成するものとする.

画像ファイルの準備

instant-ngp/data/nerf/new_data ディレクトリを作成.
instant-ngp/data/nerf/new_data/images ディレクトリを作成.
instant-ngp/data/nerf/new_data/images に画像群をコピー.

COLMAPによる画像群カメラ姿勢推定

instant-ngp/data/nerf/new_data ディレクトリでコマンドラインからcolmap2nerf.pyを実行してカメラ姿勢情報を生成する.

python [path-to-instant-ngp]/scripts/colmap2nerf.py --colmap_matcher exhaustive --run_colmap --aabb_scale 16

これで画像毎のカメラ姿勢情報を格納したtransforms.jsonなどが生成される.

InstantNeRFを実行

instant-ngpディレクトリでコマンドラインからnew_dataでNeRFを実行する.

.\build\testbed.exe --mode nerf --scene .\data\nerf\new_data

以上で新たに用意した画像群からカメラ姿勢推定情報を計算してInstantNeRFで再構成することができる.
現実で手頃な被写体がなかったので、ゲームのスクリーンショット画像群で試した例がこちら(ELDEN RING)
スクリーンショットからでもカメラ姿勢推定ができるCOLMAPの威力を見た...

3要素ソートのメモ (3element pattern sort)

3要素をパターンでソート.
安定ソートではないことに注意.

久しぶりの記事がこれですよ

// パターン.
constexpr int k_sort_pattern_3[8][3]
{
    {2, 1, 0},  // 000-> 0
    {0, 2, 1},  // 001-> 1
    {1, 0, 2},  // 010-> 2
    {0 ,1, 2},  // 011-> 3
    {2, 1, 0},  // 100-> 4
    {2, 0, 1},  // 101-> 5
    {1, 2, 0},  // 110-> 6
    {0 ,1, 2},  // 111-> 7
};
// 3要素からパターンID計算.
constexpr auto get_sort_pattern_3 = [](const float (&ar)[3])
{
    return ((ar[0] <= ar[1]) ? 1 : 0) + ((ar[1] <= ar[2]) ? 2 : 0) + ((ar[2] <= ar[0]) ? 4 : 0);
};

// ソート対象の3要素配列.
constexpr float src[3] = { -3.0f,3.0f,-2.3f };
// パターンID取得.
constexpr int sort_pattern = get_sort_pattern_3(src);

// パターンでソート順に取り出し.
constexpr float sort_v0 = src[k_sort_pattern_3[sort_pattern][0]];
constexpr float sort_v1 = src[k_sort_pattern_3[sort_pattern][1]];
constexpr float sort_v2 = src[k_sort_pattern_3[sort_pattern][2]];

// チェック.
static_assert(sort_v0 <= sort_v1 && sort_v1 <= sort_v2);

TextureSampleGradのCustomNode

UEのマテリアルでTextureからSampleGradでサンプリングするメモ.

これが2021年最後の記事. 良いお年を!

ノードとしてSampleGradが提供されていないようなので、CustomNodeでSampleGrad関数を直接呼ぶこととする.
(提供されているようならこの記事は見なかったことにしてください.)

CustomNodeの内容

TextureとUV, ddx, ddy を引数にとって内部で TextureSampleGrad関数を利用する.

/*
    Input
            InTexture  ( and InTextureSampler)
            InUv
            InDdx
            InDdy
    
    Output
            float4
*/

float4 tex = Texture2DSampleGrad(InTexture, InTextureSampler, InUv, InDdx, InDdy);
return tex;

CustomNodeコピペ用

コピペで利用したい場合は以下のテキストをマテリアルエディタに貼り付ければCustomNodeが生成される.

Begin Object Class=/Script/UnrealEd.MaterialGraphNode Name="MaterialGraphNode_21"
   Begin Object Class=/Script/Engine.MaterialExpressionCustom Name="MaterialExpressionCustom_2"
   End Object
   Begin Object Name="MaterialExpressionCustom_2"
      Code="\r\n/*\r\n    Input\r\n            InTexture  ( and InTextureSampler)\r\n            InUv\r\n            InDdx\r\n            InDdy\r\n    \r\n    Output\r\n            float4\r\n*/\r\n\r\nfloat4 tex = Texture2DSampleGrad(InTexture, InTextureSampler, InUv, InDdx, InDdy);\r\nreturn tex;\r\n\r\n"
      OutputType=CMOT_Float4
      Inputs(0)=(InputName="InTexture",Input=(Expression=MaterialExpressionTextureObjectParameter'"MaterialExpressionTextureObjectParameter_1"'))
      Inputs(1)=(InputName="InUv",Input=(Expression=MaterialExpressionReroute'"MaterialExpressionReroute_0"'))
      Inputs(2)=(InputName="InDdx",Input=(Expression=MaterialExpressionDDX'"MaterialExpressionDDX_0"'))
      Inputs(3)=(InputName="InDdy",Input=(Expression=MaterialExpressionDDY'"MaterialExpressionDDY_0"'))
      MaterialExpressionEditorX=-352
      MaterialExpressionEditorY=240
      MaterialExpressionGuid=A6C202464ABAD8F6E9734E9B54C6742D
      Material=PreviewMaterial'"/Engine/Transient.M_TexturePreview"'
   End Object
   MaterialExpression=MaterialExpressionCustom'"MaterialExpressionCustom_2"'
   NodePosX=-352
   NodePosY=240
   ErrorType=1
   ErrorMsg="Custom material Custom missing input 1 (InTexture)"
   NodeGuid=98660A2E479B2DED7843C29DD7E144AB
   CustomProperties Pin (PinId=3FB004F94791B89E090EB9B99B713C0F,PinName="InTexture",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,LinkedTo=(MaterialGraphNode_83 FF18694A45E6BEAA46EA3F8C083A2CBC,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=3E114E314D9BAB61118B329013A0C05A,PinName="InUv",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,LinkedTo=(MaterialGraphNode_Knot_7 18FE939F4A97C00C67CC16A085BED853,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=B5C056784D5BAD800E83398A08AD56B8,PinName="InDdx",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,LinkedTo=(MaterialGraphNode_85 80EC1CDF4B3A1C9667D72EB37399E8C0,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=905889CC49E11D9C88DB2792EF254574,PinName="InDdy",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,LinkedTo=(MaterialGraphNode_86 BBAD4B0443CF6057D92E17A5A0BB730A,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=8EFEA7514110CC30D32E41B0B1689991,PinName="Output",PinFriendlyName=NSLOCTEXT("MaterialGraphNode", "Space", " "),Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,LinkedTo=(MaterialGraphNode_Root_0 1653EE4D4D08EED8A95A3CA74981CFA2,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
End Object

利用例

TextureObjectとUV, ddx, ddy をCustomNodeに入力する.

f:id:nagakagachi:20211231190816p:plain
当記事のCustomNodeの利用例

何に使うの

確率的テクスチャブレンディング等ではタイリング境界でUVギャップが大きくなり, 自動的なMipLevel選択ではアーティファクトが発生してしまう.
そのため自前で適切な勾配(Grad)を計算して明示的に指定する必要がある.

左画像は通常のTextureSampleノードを使った場合, 右がTextureSampleGradを利用した場合.
TextureSampleではライン上のアーティファクトが発生しているが, TextureSampleGradでは自前で勾配を指定することで改善している.

f:id:nagakagachi:20211231192444p:plain
TextureSampleとTextureSampleGrad

RGB信号の輝度について

そういえば と思って調べたこと.

RGBの3次元ベクトルから輝度を計算するときに出てくるあの係数.

sRGB色空間の場合
 Luminance = 
dot(
\begin{pmatrix}
R_{sRGB}\\G_{sRGB}\\B_{sRGB}\\ 
\end{pmatrix}
, 

\begin{pmatrix}
0.2126\\0.7152\\0.0722\\ 
\end{pmatrix}
)


 
\begin{pmatrix}
0.2126\\0.7152\\0.0722\\ 
\end{pmatrix}
↑これ


これはRGB値をその色空間(↑の場合はsRGB)からXYZ色空間へ変換する変換行列のY座標に関する成分.
XYZ色空間のYは輝度と定義されているため, XYZ色空間のY座標のみ計算しているということなんですね.


 
\begin{align}
\begin{pmatrix}
X\\Y\\Z\\ 
\end{pmatrix} &=
\begin{pmatrix}
0.4123908&0.35758434&0.18048079\\
0.21263901&0.71516868&0.07219232\\
0.01933082&0.11919478&0.95053215\\
\end{pmatrix}
\begin{pmatrix}
R_{sRGB}\\G_{sRGB}\\B_{sRGB}\\ 
\end{pmatrix} \\
&=
\begin{pmatrix}
0.4123908R_{sRGB}+0.35758434G_{sRGB}+0.18048079B_{sRGB}\\
0.21263901R_{sRGB}+0.71516868G_{sRGB}+0.07219232B_{sRGB}\\
0.01933082R_{sRGB}+0.11919478G_{sRGB}+0.95053215B_{sRGB}\\
\end{pmatrix}
\end{align}


↑の式では四捨五入していないので最初の式と少し違うけれど、Yはそういうこと.
別の色空間のRGBなら別の係数になるのも当然. 注意重点.


色空間の変換行列がたくさん乗っているページ.
本当に正しい値かはチェックが必要だけれどとても参考になる.
Welcome to Bruce Lindbloom's Web Site

f:id:nagakagachi:20211215001404p:plain
http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html

縮小バッファによる半透明パーティクル描画についてのメモ

縮小バッファに描いたものを合成するというフローによって様々な制限がかかるのだ.

Lost Planet

西川善司の3Dゲームファンのための「ロスト プラネット」グラフィックス講座

  1. シーンカラーとシーンデプスの縮小版生成, 縮小解像度のアルファバッファも生成
  2. パーティクル群を縮小バッファへ描画,この時縮小アルファには最終合成時用のマスクを書き込む
  3. 縮小アルファを元にフル解像度シーンカラーへ合成

半透明の奥側は縮小シーンであるため, フル解像度との合成で解像度のズレが見える.
加算合成等の場合は縮小アルファにどのような値を書き込むのかわからない.
縮小アルファへの書き込みさえなんとかなれば加算減算乗算も可能に思える.

METAL GEAR SOLID 4

西川善司の3Dゲームファンのためのゲームグラフィックス講座

  1. 専用のRGBAバッファ(ブレンドバッファと呼称)を生成(縮小解像度も可)
  2. ブレンドバッファを(0,0,0,1)でクリア
  3. パーティクル群を書き込み
    • アルファブレンド
      • RGB : Dst*(1-SrcAlpha) + Src*SrcAlpha
      • Alpha: DstAlpha * (1 - SrcAlpha)
    • 加算合成時
      • RGB : Dst + Src*SrcAlpha
      • Alpha: DstAlpha
  4. ブレンドバッファをフル解像度シーンに合成
    • RGB : Dst * SrcAlpha + Src

Src*SrcAlpha の部分をシェーダ側で計算すれば乗算済アルファとして考えられる.
シーンとの合成がフル解像度なのでLost Plantの方法で問題であった半透明部の透過解像度の齟齬が無い.
アルファブレンドと加算合成(減算)の両方に自然に対応 (乗算は不可.


パーティクル描画と最終合成を分離しても大丈夫なのか不安だったので計算してみた.
c0, a0が(0,0,0,1)でクリア直後のブレンドバッファとRGB, Alphaとして, 3回のパーティクル描画をした場合を考える.
ここで c1, c2, c3 はアルファを乗算済のRGBとする.
最初の式はシーンバッファに直接描画した場合, 2つ目の式はブレンドバッファを利用した場合.
直感的には本当に同じ結果になるのか確信が持てなかったが式で一致が確認できたのでヨシ.



Tales of ARISE

CEDEC2019: 『Tales of ARISE』におけるレンダリング技術と高速化
UE4改造して複数解像度による多段階の半透明描画をしている

  1. 低解像度で半透明描画 (MGS4と同様で, 縮小シーンではなさそう?)
  2. 低解像度バッファの勾配情報等から低解像度高周波マスク生成
  3. シーンへの低解像度バッファ合成と, フル解像度高周波ステンシルバッファ生成
    • ステンシル出力有効にして高周波マスク値からステンシル値出力している?
  4. ステンシルバッファを利用してフル解像度で部分的に半透明再描画
    • 詳細部分のみパーティクル描画をフル解像度で実行する

低解像度で描画して, 詳細が必要な部分だけステンシルマスク利用のフル解像度描画.
資料の情報からはMGS4ブレンドバッファと同様にシーンの縮小バッファではなくクリアされた縮小バッファに描いている?.
ブレンドバッファ方式の場合は乗算等は不可能そうに思えるがどうか?
earlydepthstencilでピクセル処理を早期棄却しつつ部分的にフル解像度描画できる.
高周波判定する閾値を変化させることで負荷のコントロールができそうな点も面白そう.

UE4マテリアルによるDistanceField生成(Jump Floodingアルゴリズム)

サンプルプロジェクト有 (UE4.27).

Jump Flooding アルゴリズムによるDistanceField生成をUEマテリアルで実装したメモ.
ついでに Editor Utility Widget でSeedテクスチャの生成とDistanceFieldの生成をトリガーするテスト.

Jump Flooding Algorithm (JFA)の詳細については各種資料参照.
https://www.comp.nus.edu.sg/~tants/jfa/i3d06.pdf
Jump flooding algorithm - Wikipedia
GPU Voronoi Diagrams using the Jump Flooding Algorithm / Ricky Reusser / Observable

サンプルプロジェクト

DistanceFIeldByJFA00.zip - Google ドライブ


プロジェクトの[ThirdPersonBP/DF]に関連BPやマテリアル等がある.

f:id:nagakagachi:20211107224956p:plain
関連コンテンツフォルダ

DistanceFieldテクスチャ生成手順

EUW_JfaTestアセット を右クリック->Run Editor Utility WidgetWidgetを表示する.
Generate Seed Texture ボタン を押すとマテリアルによってSeedテクスチャが更新される.

f:id:nagakagachi:20211107225514p:plain
Generate SeedTexture

Run JFA ボタン を押すとSeedテクスチャを元にDistanceFieldテクスチャが生成される.

f:id:nagakagachi:20211107230005p:plain
Generate DistanceField

アセット説明

  • RT_Seed
    • DistanceFieldの元になるSeedテクスチャ. 黒テクセルは無効で, それ以外の色のテクセルがSeedとして評価される.
    • どんなテクスチャでもよいが,今回はマテリアルM_DistanceSeedでその場で生成している.
    • サイズはデフォルト512x512だが変更可能. ただし動作確認しているのは2の冪のみ.
  • RT_Jfa0
    • DistanceFieldの出力先テクスチャ.
  • M_DistanceSeed
    • Seedテクスチャの生成用マテリアル. 適当な計算で適当な位置に黒(0,0,0)ではないSeedを書き込む.
  • MF_JumpFlood
    • JFAのメイン処理をCustomNodeで記述したMaterialFunction. M_JumpFlood_SetupとM_JumpFlood_Itrはこれを呼び出す.
  • M_JumpFlood_Setup
    • JFAの初回パス用マテリアル. Seedテクスチャを元に黒(0,0,0)ではないテクセルをSeedとしてJFAの初回パスを実行する.
  • M_JumpFlood_Itr
    • JFAの反復パス用マテリアル. 基本的には初回パスと同じだが,JFA入力が前回のJFAパスの結果テクスチャになる.
  • M_JumpFlood_Copy
    • JFA結果テクスチャを元に出力先テクスチャRT_Jfa0 へ出力するマテリアル.
  • EUW_JfaTest
    • JFA処理のMaterialDrawなどをするWidget. 今回のJFA関連の処理はほぼここに書いてある.
    • JFAのワークテクスチャ(PingPong用に2枚)を内部で生成して利用する.
    • JFA自体はテクスチャサイズXに対して (log2(X)-1)回のマテリアルDrawで完了する.

Jump Flooding ステップ

JFAの1ステップの処理は MF_JumpFlood のCustomで行っている.
初回のSeedテクスチャの処理だけ少し特殊対応しているがそれ以外は素直な実装.
テクスチャサイズは2の冪でのみ動作確認.

//  MF_JumpFlood
//  入力テクスチャの非ゼロ(0,0,0)テクセルをSeedとして最近接Seedテクセル位置をJFAで計算する.
//
// inTexture : 入力テクスチャ.
//        初回パスは黒(0,0,0)が無効値扱いである任意のテクスチャ.
//        反復パスではRG=(0,0)が無効値であるような最近接Seedテクセル位置テクスチャ.
//        テクセル位置は必ずテクセル中心を指すものとする. 具体的には半テクセルサイズ分のオフセットが付加されたもの.
// texelPosition : ハーフテクセルサイズ込のテクセル中心位置.
// textureSize : テクスチャサイズ.
// stepLength : この反復でのステップのテクセル数.
//        入力サイズ/2 から開始し, 反復的に 1/2 としてサイズが 1 になるまで実行することでJFAが完了する.
// isSetupPass : 初回パスの場合非0.
//
float bestDistance = 65535.0;
float2 bestSeed = float2(0.0, 0.0);
for(int j = -1; j <= 1; ++j)
{
    for(int i = -1; i <= 1; ++i)
    {
        float2 samplePosition = texelPosition + float2(i, j) * stepLength;
        
        const float2 sampleUv = samplePosition / textureSize;
        const float4 col = Texture2DSample(inTexture, inTextureSampler, sampleUv);

        if(any(float2(0.0,0.0) > samplePosition) || any(textureSize <= samplePosition))
            continue;

        float2 seedPosition = samplePosition;
        if(0.0 != isSetupPass.x)
        {
            // 最初のパスは黒テクセルを無視.
            if(all(float3(0.0, 0.0, 0.0) == col.xyz))
                continue;   
        }
        else
        {
            // 有効であればハーフピクセルオフセット付きのテクセル位置が格納されている.
            seedPosition = col.xy;
            // 0,0 は有効なシードを格納していない.
            if(all(float2(0.0, 0.0) == seedPosition))
                continue;
        }

        float curDist = distance(seedPosition, texelPosition);
        if(bestDistance > curDist)
        {
            bestSeed = seedPosition;
            bestDistance = curDist;
        }
    }
}

return float4(bestSeed.x, bestSeed.y, 0.0, 0.0);


以下の疑似コードのように反復をすることで最終的に各テクセルの最近接Seedテクセル座標が計算される.
それを元にDistanceFieldテクスチャを生成する.

// Pseudo Code.
int stepCount = log2(TextureSize) - 1;
for(int i = 0; i < stepCount; ++i)
{
    JumpFlood(stepLength = pow(2, stepCount - i));
}

その他

今回の実装ではSeedテクスチャの黒(0,0,0)は無効値として扱う.
同様にJFAワークテクスチャは最近接Seedテクセル座標float2を格納するが, (0,0)が格納されている場合は無効値扱いとする.
テクセル座標は必ずテクセル中心位置であり, 必ず半テクセルサイズ分がオフセットされているため (0,0)という値にはなりえないことから.

MainViewのViewportSizeとカメラ位置,カメラ姿勢の取得

UE4.27で確認

// Get Current View Info (EditorMode, PlayMode).
auto GetCurrentViewportInfo = [](const UWorld* world, FVector2D& out_viewport_size, FVector& out_view_location, FQuat& out_view_quat)
{
    if (!world)
        return;

#if WITH_EDITOR
    if (world->WorldType == EWorldType::Editor || world->WorldType == EWorldType::EditorPreview)
    {
        // EditorMode.
        // Use First Editor Viewport.
        for (FLevelEditorViewportClient* level_viewport_clients : GEditor->GetLevelViewportClients())
        {
            if (level_viewport_clients && level_viewport_clients->IsPerspective())
            {
                out_viewport_size = level_viewport_clients->Viewport->GetSizeXY();
                out_view_location = level_viewport_clients->GetViewLocation();
                out_view_quat = level_viewport_clients->GetViewRotation().Quaternion();
                break;
            }
        }
    }
    else
#endif
    {
        // Non EditorMode.
        // Use First PlayerCamera.
        if (auto* camera_manager = UGameplayStatics::GetPlayerCameraManager(world, 0))
        {
            GEngine->GameViewport->GetViewportSize(out_viewport_size);
            out_view_location = camera_manager->GetCameraLocation();
            out_view_quat = camera_manager->GetCameraRotation().Quaternion();
        }
    }

    return;
};

const UWorld* world = [ワールド];
FVector2D out_viewport_size;
FVector out_view_location;
FQuat out_view_quat;
// Get Info.
GetCurrentViewportInfo(world, out_viewport_size, out_view_location, out_view_quat);