[Omniverse] 180度パノラマのステレオ画像を作る

  • by

Omniverse Create 2021.3.7では、360度パノラマ(Equirectangular)としてレンダリングする機能があります。
これを使用して、180度パノラマのステレオの作成にチャレンジしました。

なお、Omniverse Create 2021.3.7では360度パノラマ(単眼)をレンダリングする機能はありますが、
ステレオ対応はしていません。UIはあるのですが、まだ機能していません。
また、180度をレンダリングする機能はありませんのでいったん360度でレンダリングしてから左右をトリミングして扱うことにします。

いくつかスクリプトで前処理を行うため、まず必要なスクリプトを列挙していきました。

視線方向のワールド座標でのベクトルを取得

Script Editorで以下を実行すると、ワールド座標での視線ベクトル(Gf.Vec3f)が表示されます。

from pxr import Usd, UsdGeom, CameraUtil, UsdShade, Sdf, Gf, Tf
import omni.kit

# Get viewport.
viewport = omni.kit.viewport.get_viewport_interface()
viewportWindow = viewport.get_viewport_window()

# Get active camera path.
cameraPath = viewportWindow.get_active_camera()

# Get stage.
stage = omni.usd.get_context().get_stage()

time_code = omni.timeline.get_timeline_interface().get_current_time() * stage.GetTimeCodesPerSecond()

# Get active camera.
cameraPrim = stage.GetPrimAtPath(cameraPath)
if cameraPrim.IsValid():
    camera  = UsdGeom.Camera(cameraPrim)    # UsdGeom.Camera
    cameraV = camera.GetCamera(time_code)   # Gf.Camera

    # Get view matrix.
    viewMatrix = cameraV.frustum.ComputeViewMatrix()

    # Camera vector(World).
    viewInv = viewMatrix.GetInverse()
    cameraVector = viewInv.TransformDir(Gf.Vec3f(0, 0, -1))
    print("Camera vector(World) : " + str(cameraVector))

コードの意味はここでは解説しませんが、Viewportのカレントカメラを取得し、
カメラのビュー変換行列から視線方向を計算しています。
これを応用して、ステレオ時のIPD(瞳孔間距離 : デフォルトは64(mm)が多い)を考慮した視差をスクリプトで計算します。

カレントカメラの位置からIPD分離れた位置を計算

上記を拡張して、「ipdValue = 6.4」のようにcm単位のIPDが指定されているときの左右のカメラ位置を計算します。
「cameraV」に「Gf.Camera」が入っているとします。

ipdValue = 6.4    #IPD (cm). 

# Get view matrix.
viewMatrix = cameraV.frustum.ComputeViewMatrix()

# Two camera positions in the view.
ipdH = ipdValue * 0.5
leftVPos  = Gf.Vec3f(-ipdH, 0, 0)
rightVPos = Gf.Vec3f( ipdH, 0, 0)

# Camera vector(World).
viewInv = viewMatrix.GetInverse()
vVector = viewInv.TransformDir(Gf.Vec3f(0, 0, -1))

# Convert to camera position in world coordinates.
leftWPos  = viewInv.Transform(leftVPos)
rightWPos = viewInv.Transform(rightVPos)

これで「leftWPos」に左目のカメラのワールド位置、「rightWPos」に右目のカメラのワールド位置が入ります。
「vVector」がワールド座標の視線ベクトルになるため、この情報からカメラを作成すれば視差が作れますね。

左右の目に相当するカメラを新規作成

指定のパス(pathName)のカメラを生成します。
orgCameraは元のカメラ(Gf.Camera)、
positionはワールド座標でのカメラ位置、directionはワールド座標での視線ベクトルです。

def createNewCamera (orgCamera : Gf.Camera, pathName : str, position : Gf.Vec3f, direction : Gf.Vec3f):
    cameraGeom = UsdGeom.Camera.Define(stage, pathName)

    cameraGeom.CreateFocusDistanceAttr(orgCamera.GetFocusDistanceAttr().Get())
    cameraGeom.CreateFocalLengthAttr(orgCamera.GetFocalLengthAttr().Get())
    cameraGeom.CreateFStopAttr(orgCamera.GetFStopAttr().Get())

    # Set position.
    UsdGeom.XformCommonAPI(cameraGeom).SetTranslate((position[0], position[1], position[2]))

    # Set rotation(Y-Up (0, 1, 0)).
    m = Gf.Matrix4f().SetLookAt(Gf.Vec3f(0, 0, 0), direction, Gf.Vec3f(0, 1, 0))
    rV = -m.ExtractRotation().Decompose(Gf.Vec3d(1, 0, 0), Gf.Vec3d(0, 1, 0), Gf.Vec3d(0, 0, 1))
    UsdGeom.XformCommonAPI(cameraGeom).SetRotate((rV[0], rV[1], rV[2]), UsdGeom.XformCommonAPI.RotationOrderXYZ)

    # Set scale.
    UsdGeom.XformCommonAPI(cameraGeom).SetScale((1, 1, 1))

なお、これはY-Upを(0, 1, 0)としています。

以下を呼び出すと、左目用と右目用のカメラを生成できることになります。

pathStr = '/World'
leftPathStr = pathStr + '/camera_left'
createNewCamera(cameraV, leftPathStr, leftWPos, vVector)

rightPathStr = pathStr + '/camera_right'
createNewCamera(cameraV, rightPathStr, rightWPos, vVector)

まとめると以下のようになりました。
これをScript Editorで実行すると、カレントのカメラを中心に「ipdValue」で指定された距離だけ左右に離れたカメラを作成します。

from pxr import Usd, UsdGeom, CameraUtil, UsdShade, Sdf, Gf, Tf
import omni.kit

# IPD (cm).
ipdValue = 6.4

# Get viewport.
viewport = omni.kit.viewport.get_viewport_interface()
viewportWindow = viewport.get_viewport_window()

# Get active camera path.
cameraPath = viewportWindow.get_active_camera()

# Get stage.
stage = omni.usd.get_context().get_stage()

time_code = omni.timeline.get_timeline_interface().get_current_time() * stage.GetTimeCodesPerSecond()

# ---------------------------------.
# Create new camera.
# ---------------------------------.
def createNewCamera (orgCamera : Gf.Camera, pathName : str, position : Gf.Vec3f, direction : Gf.Vec3f):
    cameraGeom = UsdGeom.Camera.Define(stage, pathName)

    cameraGeom.CreateFocusDistanceAttr(orgCamera.GetFocusDistanceAttr().Get())
    cameraGeom.CreateFocalLengthAttr(orgCamera.GetFocalLengthAttr().Get())
    cameraGeom.CreateFStopAttr(orgCamera.GetFStopAttr().Get())

    # Set position.
    UsdGeom.XformCommonAPI(cameraGeom).SetTranslate((position[0], position[1], position[2]))

    # Set rotation(Y-Up (0, 1, 0)).
    m = Gf.Matrix4f().SetLookAt(Gf.Vec3f(0, 0, 0), direction, Gf.Vec3f(0, 1, 0))
    rV = -m.ExtractRotation().Decompose(Gf.Vec3d(1, 0, 0), Gf.Vec3d(0, 1, 0), Gf.Vec3d(0, 0, 1))
    UsdGeom.XformCommonAPI(cameraGeom).SetRotate((rV[0], rV[1], rV[2]), UsdGeom.XformCommonAPI.RotationOrderXYZ)

    # Set scale.
    UsdGeom.XformCommonAPI(cameraGeom).SetScale((1, 1, 1))

# ---------------------------------.
# Get active camera.
cameraPrim = stage.GetPrimAtPath(cameraPath)
if cameraPrim.IsValid():
    camera  = UsdGeom.Camera(cameraPrim)    # UsdGeom.Camera
    cameraV = camera.GetCamera(time_code)   # Gf.Camera

    # Get view matrix.
    viewMatrix = cameraV.frustum.ComputeViewMatrix()

    # Two camera positions in the view.
    ipdH = ipdValue * 0.5
    leftVPos  = Gf.Vec3f(-ipdH, 0, 0)
    rightVPos = Gf.Vec3f( ipdH, 0, 0)

    # Camera vector(World).
    viewInv = viewMatrix.GetInverse()
    vVector = viewInv.TransformDir(Gf.Vec3f(0, 0, -1))

    # Convert to camera position in world coordinates.
    leftWPos  = viewInv.Transform(leftVPos)
    rightWPos = viewInv.Transform(rightVPos)

    # Create camera.
    pathStr = '/World'
    leftPathStr = pathStr + '/camera_left'
    createNewCamera(camera, leftPathStr, leftWPos, vVector)

    rightPathStr = pathStr + '/camera_right'
    createNewCamera(camera, rightPathStr, rightWPos, vVector)

360度のEquirectangularのレンダリングを行う

Viewport左上のカメラを選択するとメニューが表示されます。
Cameraを選択するとシーン上のカメラを選択でき、この中に上記で作成した「camera_left」「camera_right」が存在します。
これを選択すると、左右にずれた状態に切り替わるのを確認できます。

「camera_left」を選択しておきます。

Stageタブで「World」の子として「camera_left」ができているため、それを選択します。
また、Propertyタブにカメラのパラメータが表示されます。

Propertyタブを下にスクロールし「Fisheye Lens」を開きます。
「Projection Type」を「fisheyeSpherical」に変更します。
これは360度パノラマ(Equirectangular)のレンダリングを行います。

Viewportは以下のようになりました。

右目の「camera_right」も同様の処理を行います。
これは360度パノラマなので、水平方向の中央を取り出すと180度になるのが分かりますね。

現状は解像度が低いため、次に最大解像度でレンダリングできるようにしていきます。

高い解像度でレンダリングを行う

Viewportの左上の歯車をクリックして設定を開きます。
「Render Resolution」を最大の「3840×2160」、「Fill Viewport」チェックボックスをOffにします。
これで「3840 x 2160」ピクセルのレンダリングが行われます。

それなりに時間がかかります。といっても、1分は行かないですが。

Viewportのレンダリング画像をファイル出力

メインメニューの「Edit」-「Capture Screenshot」を選択し、レンダリング画像をファイル保存します。
Consoleタブでどこに出力されたかメッセージが出ています。
デフォルトは「C:/Users/UserName/Documents/Kit/shared/screenshots」にpng形式で保存されます。
Viewport以外もキャプチャされてしまう場合は、メインメニューの「Edit」-「Preference」を選択し、
Capture Screenshotの「Capture only the 3D viewport」チェックボックスがOnになっているのを確認してください。

右目のパノラマのレンダリングも同様にファイル保存します。

以上で、Omniverse上の作業は完了です。
2枚の左目と右目のパノラマ画像が出力された状態です。
ファイル名を「camera_left.png」「camera_right.png」と名前変更しました。
以降の画像編集はAffinity Photoで行いました。

Affinity Photo : Affinity Photoで180度を抜き出し

Omniverseでレンダリングしたパノラマ画像をAffinity Photoに読み込んでおきます。
Affinity Photoのメインメニューの「ドキュメント」-「キャンパスのサイズを変更」を選択します。
「キャンパスのサイズを変更」ウィンドウで、
錠のマークをクリックして連動を解除、
アンカーは中央、
サイズで「3840/2」とします(pxはあってもOK)。

OKボタンを押すと、中央だけ切り取られました。
これをpngでエクスポートします。

右目の「camera_right.png」も同じように180度分だけ切り取って保存します。
それぞれ1920 x 2160ピクセルになります。

Affinity Photo : 左右に並べる

左目、右目の180度パノラマを左右に並べます。
これをjpeg画像としてエクスポートしました。3840 x 2160ピクセルになります。

これが最終的な180度ステレオの画像になります(Side by Side)。
これをOculus Quest2などに持って行って立体視してみてくださいませ。

サンプル

以下に実際にOmniverseでレンダリングした180度ステレオを置いています。
WebXR表示になります。

https://ft-lab.github.io/VR180/omniverse.html

Oculus Quest2のブラウザでこのURLにアクセスし、サムネイルをクリックすると立体視として表示されます。

Omniverse Create 2021.3.7ではまだステレオ対応していないですが、
UIとしてすでにステレオの切り替えはありフォーラムでも話題に出ていましたので、おそらく今後対応されるかなと思っています。