VRゲームで自身をアバターとして表示して録画する方法と、その撮影するカメラを自動で動かす手段を考えてみた話

（2020/2/14追記)
現在はバーチャルモーションキャプチャーと[EVMC4U](https://qiita.com/gpsnmeajp/items/a37d29f5fc0632e32577)を使用し、Unityでカメラを動かせば同様なことが簡単にできる（はず、自分で試していないけども）のでそちらを利用すると良いです。

##VRゲームで自身をアバターとして表示して録画する

手持ち無沙汰でなんとなくTwitterのTLを見ていると、VRのゲームを録画した映像にキャラクターが合成された動画が流れてくることが（多分）何回かはあると思います。
あれはどう実現されているかというとゲームのMODだったりいろいろと手はありますがここではLIVとバーチャルモーションキャプチャーを使った方法について書きます。

[LIV](https://liv.tv/)は、グリーンバック（以下GB）の前に人がいるカメラ映像と三人称視点で表示したVRのゲーム画面を合成するソフトです。

[バーチャルモーションキャプチャー](https://sh-akira.github.io/VirtualMotionCapture/)は、SteamVRから手や頭の位置情報を取得してアバターを動かしGBで表示するめっちゃ便利なソフトです。あきら氏（[@sh_akira](https://twitter.com/sh_akira)）が開発しています。

LIVへの『GB撮影されたカメラ映像』の代わりに『GBの前で動くアバターの映像』を仮想webカメラとして流し込むことで、VRゲームの中でアバターが動いているような映像になります。


##撮影するカメラを動かしたい

LIVとバーチャルモーションキャプチャーを使用した動画で今一番出回っているのはBeat Saberだと思われます。
その動画を見ると大体初めから終わりまで背後から固定したカメラでの映像です。
自分で撮影してみると、もっとアバターを別アングルで見てみたいとか絵的に派手にしたいとか欲が出てきます。
Beat Saberに限れば、MODのみでアバターを合成したり三人称視点にしてカメラを動かしたりは可能なようです。

https://www.youtube.com/watch?v=ivBVDMMO_YA

しかし、他のゲームでも同じようなことをするためにはMODに頼らずにどうにかする必要があります。

LIVにはVRデバイスに三人称視点のカメラを割り当てる機能があります。
誰かにVIVEのトラッカーやコントローラーを持ってもらってプレイヤーの周りを動いて撮影することはできますが、手ブレの問題があったり、何よりも一人では無理です。

コントローラーをプログラム的に動かす手が無いかとぼんやり考えながら過ごしていたところ、VR内から画像ファイルやデスクトップを見れる超絶便利なツール[VaNiiMenu](https://sabowl.sakura.ne.jp/gpsnmeajp/unity/vaniimenu/)開発者のgpsnmeajp氏（[@Seg_Faul](https://twitter.com/Seg_Faul)）がコントローラードライバのサンプルコードを[公開されていました](https://github.com/gpsnmeajp/SegsVRControllerDriverSample)。


そのサンプルコードではクライアントがドライバの共有メモリに座標を書き込んで、仮想のコントローラーを移動させることができるようになっていました。

クライアントのC++のコードを参考にPythonで書いてみたのがこれです。

```python:simpleShareMemWrite.py
import mmap

shareMem = mmap.mmap(0,16384,'pip1')

cameraPos = [0,1.5,-1] #単位はメートル　これで高さ1.5m　前方1m

posX = str(cameraPos[0])
posY = str(cameraPos[1])
posZ = str(cameraPos[2])

shareMem.seek(0)
shareMem.write(b'{"id":0,"v":['+posX.encode()+b','+posY.encode()+b','+posZ.encode()\
               +b'],"vd":[0,0,0],"vdd":[0,0,0],"r":[0,0,0,0],"rd":[0,0,0],"rdd":[0,0,0],"Valid":true}')

```
サンプルのドライバを入れた状態でスクリプトを動かすと目の前にベースステーションのモデルが移動します。
![コメント 2019-06-27 151313.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447701/21046389-1e0b-ec15-4934-d44c0cab7ba1.png)

カメラを動かしたいのですが3Dの座標の計算がよくわからないので、Blenderのカメラ位置をそのまま共有メモリに書き込むアドオンとして作りました。
![コメント 2019-06-27 020400.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447701/0d519675-2100-429c-00c1-1c5fe4d33448.png)

Blender上で何もないとどこをカメラで狙えばいいかわからないので適当にアバターやステージを置きました。
実際に撮れる動画と比べるとどうしてもズレるのであくまでも目安です。

フレームが変わるタイミングかsyncボタンを押すと、シーンでアクティブなカメラの位置と回転を仮想コントローラーへ送ることができます。
SteamVRとBlenderで座標の向きが違うようで、揃えるために入れ替えたりしてみたらカメラの方向がおかしくなってしまいました・・・。
多分ちゃんと計算すればいいのですが手動で切り替えることで無理やり動かしてます。
（カメラの回転の向きをTrack Toコンストレイントで決めていて、そこのプロパティを変更することで向きを変えている）


```python:blender_to_SteamVR.py
import bpy
import mmap

bl_info = {
    "name" : "camera move test",
    "author" : "imakami",
    "version" : (0,1),
    "blender" : (2, 7, 0),
    "location" : "",
    "description" : "",
    "warning" : "",
    "wiki_url" : "",
    "tracker_url" : "",
    "category" : ""
}

capture = False
shareMem = False

class AnimationCaptureToggle(bpy.types.Operator):
    bl_idname = "imakami.animation"
    bl_label = "Capture Toggle"
    bl_description = "Start Capture"
    bl_options = {'REGISTER', 'UNDO'}

    def invoke(self, context, event):
        global capture
        if capture == False:
            bpy.app.handlers.frame_change_pre.append(sendCameraPos)
            capture = True
        else:
            bpy.app.handlers.frame_change_pre.clear()
            capture = False
        return {'FINISHED'}

class CameraSync(bpy.types.Operator):
    bl_idname = "imakami.camera"
    bl_label = "Sync"
    bl_description = "Sync"
    bl_options = {'REGISTER', 'UNDO'}

    def invoke(self, context, event):
        sendCameraPos(self)
        return {'FINISHED'}

def sendCameraPos(self):
    global shareMem
    if shareMem == False:
        shareMem = mmap.mmap(0,16384,'pip1')
    nowCameraPos, nowCameraRotate, nowCameraScale = bpy.context.scene.camera.matrix_world.decompose() #scaleは使ってない
    posX = str(nowCameraPos[0]) #XAxis
    posY = str(nowCameraPos[2]) #ZAxis
    posZ = str(nowCameraPos[1]*-1) #YAxis
    rotQuat = []
    for i in range(4):
        if i == 2:
            rotQuat.append(str(nowCameraRotate[i]*-1))
        else:
            rotQuat.append(str(nowCameraRotate[i]))
    shareMem.seek(0)
    wait = shareMem.read(1)
    if wait != b'x':
        print('not wait')
        return
    shareMem.seek(0)
    shareMem.write(b'{"id":0,"v":['+posX.encode()+b','+posY.encode()+b','+posZ.encode()+b'],"vd":[0,0,0],"vdd":[0,0,0],"r":['\
                   +rotQuat[0].encode()+b','+rotQuat[1].encode()+b','+rotQuat[3].encode()+b','+rotQuat[2].encode()\
                   +b'],"rd":[0,0,0],"rdd":[0,0,0],"Valid":true}')
    #print(posX,posY,posZ)

class TransToVRCameraAxis(bpy.types.Operator):
    bl_idname = "imakami.to_vr"
    bl_label = "SteamVR"
    bl_description = "Switch Constraints To SteamVR"
    bl_options = {'REGISTER', 'UNDO'}

    def invoke(self, context, event):
        changeCameraTrackAxis('TRACK_Y','UP_Z')
        return {'FINISHED'}

class TransToBlenderCameraAxis(bpy.types.Operator):
    bl_idname = "imakami.to_bl"
    bl_label = "Blender"
    bl_description = "Switch Constraints To Blender"
    bl_options = {'REGISTER', 'UNDO'}

    def invoke(self, context, event):
        changeCameraTrackAxis('TRACK_NEGATIVE_Z','UP_Y')
        return {'FINISHED'}

def changeCameraTrackAxis(trackAxis, upAxis):
    for obj in bpy.data.objects:
        if obj.type == 'CAMERA':
            track = obj.constraints.get('Track To')
            if track != None:
                track.track_axis = trackAxis
                track.up_axis = upAxis

class addonPanel(bpy.types.Panel):
    bl_label = "Camera to SteamVR"
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_category = "Tools"

    def draw(self, context):
        global capture
        sc = context.scene
        layout = self.layout
        boxAxis = layout.box()
        boxAxis.label(text = "Switch Constraints To")
        boxAxis.operator(TransToVRCameraAxis.bl_idname, icon="MANIPUL")
        boxAxis.operator(TransToBlenderCameraAxis.bl_idname, icon="MANIPUL")
        boxCap = layout.box()
        boxCap.label(text = "Send To SteamVR")
        if capture == False:
            boxCap.operator(AnimationCaptureToggle.bl_idname, text="Start Capture", icon="PLAY")
        else:
            boxCap.operator(AnimationCaptureToggle.bl_idname, text="Stop",icon="REC")
        boxCap.operator(CameraSync.bl_idname, icon="SCENE")

classes = {TransToVRCameraAxis,TransToBlenderCameraAxis,AnimationCaptureToggle,CameraSync,addonPanel}
def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)
if __name__ == "__main__":
    register()

```

これでBlender上でアニメーションを再生すると、アクティブなカメラの位置と連動して仮想コントローラーが動くようになりました。
後はLIVでこの仮想コントローラーをカメラに指定して、バーチャルモーションキャプチャーとLIVで読み込むexternalcamera.cfgのx/y/z/rx/ry/rzを全て0にしたファイルを読み込ませるようにします。
そうして書いたBlenderアドオンとgpsnmeajp氏の書いたコントローラードライバのサンプルコードをそのまま使って以下の動画を撮りました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">最新版のバーチャルモーションキャプチャーが軽いのでかなり無茶なことができるようになった<br><br>Song:Hello by Capsule<br>Custom Map : Firm Retention<a href="https://twitter.com/hashtag/%E3%83%90%E3%83%BC%E3%83%81%E3%83%A3%E3%83%AB%E3%83%A2%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%AD%E3%83%A3%E3%83%97%E3%83%81%E3%83%A3%E3%83%BC?src=hash&amp;ref_src=twsrc%5Etfw">#バーチャルモーションキャプチャー</a> <a href="https://twitter.com/hashtag/BeatSaber?src=hash&amp;ref_src=twsrc%5Etfw">#BeatSaber</a> <a href="https://t.co/mqUXk0pJiu">pic.twitter.com/mqUXk0pJiu</a></p>&mdash; いまかみ (@imakami) <a href="https://twitter.com/imakami/status/1144244750578417664?ref_src=twsrc%5Etfw">2019年6月27日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


##まとめ

この手法だと対象となるゲームとLIVとバーチャルモーションキャプチャーとOBSとBlenderを同時に動作させないといけません。
LIVとバーチャルモーションキャプチャーの処理が結構重くてカクカクになったりしていたのですが、最新版のバーチャルモーションキャプチャー(V0.32)がかなり軽くなったのでこんな方法でも現実的になりました。
とはいえBeat Saberで最低のグラフィック設定にしたケースでの話です。自分のPC（CPU i7 4770 グラボ GTX1060）だと720pでギリギリでフルHDだと無理な感じです。

仮想コントローラーが手に持つコントローラと認識されるということがありました。コントローラの電源を入れなおしたりすると直ったりします。
バーチャルモーションキャプチャーの設定も手動で各コントローラーを指定するとうまく動くようになりました。

動画を撮る際、Blenderで普通に動画を作成するようにカメラ切り替えとアニメーションを前もってすべて作成するというめちゃくちゃ面倒なことをしています。
コントローラーを移動させたい座標とクォータニオンでの回転を共有メモリに書き込むだけなのでBlender抜きでコードを書いて、適当なボタンを押すとカメラが動くとかにした方が使いやすいと思われます。