バーチャル アート セッション

アート セッションの詳細

概要

6 人のアーティストが招待され、VR で絵画、デザイン、彫刻を制作しました。以下は、セッションを記録し、データを変換して、ウェブブラウザでリアルタイムに表示するプロセスです。

https://g.co/VirtualArtSessions

素晴らしい時代ですね。消費者向け製品としてバーチャル リアリティが導入され、未知の可能性が発見されています。HTC Vive で利用できる Google プロダクトの Tilt Brush では、3 次元空間に描画できます。Tilt Brush を初めて試したとき、モーション トラッキング コントローラで描画する感覚と、「スーパーパワーのある部屋にいる」という存在感は、ずっと残ります。周囲の空きスペースに描画できるような体験は他にありません。

仮想アート作品

Google のデータ アート チームは、VR ヘッドセットをお持ちでない方にも、Tilt Brush がまだ動作していないウェブで、このエクスペリエンスをご利用いただけるようにするという課題に直面しました。そのため、チームは彫刻家、イラストレーター、コンセプト デザイナー、ファッション アーティスト、インスタレーション アーティスト、ストリート アーティストを招き、この新しいメディアで独自のスタイルのアートワークを制作しました。

バーチャル リアリティで描画する

Unity で構築された Tilt Brush ソフトウェア自体は、ルームスケール VR を使用してヘッドの位置(ヘッドマウント ディスプレイ、HMD)と両手のコントローラを追跡するデスクトップ アプリケーションです。Tilt Brush で作成されたアートワークは、デフォルトで .tilt ファイルとしてエクスポートされます。このエクスペリエンスをウェブに提供するには、アートワーク データ以上のものが必要であることがわかりました。Tilt Brush チームと緊密に連携して Tilt Brush を変更し、元に戻す/削除するアクションと、アーティストの頭と手の位置を 1 秒あたり 90 回エクスポートするようにしました。

描画時に、Tilt Brush はコントローラの位置と角度を取得し、時間の経過とともに複数のポイントを「ストローク」に変換します。例については、こちらをご覧ください。これらのストロークを抽出して未加工の JSON として出力するプラグインを作成しました。

    {
      "metadata": {
        "BrushIndex": [
          "d229d335-c334-495a-a801-660ac8a87360"
        ]
      },
      "actions": [
        {
          "type": "STROKE",
          "time": 12854,
          "data": {
            "id": 0,
            "brush": 0,
            "b_size": 0.081906750798225,
            "color": [
              0.69848710298538,
              0.39136275649071,
              0.211316883564
            ],
            "points": [
              [
                {
                  "t": 12854,
                  "p": 0.25791856646538,
                  "pos": [
                    [
                      1.9832634925842,
                      17.915264129639,
                      8.6014995574951
                    ],
                    [
                      -0.32014992833138,
                      0.82291424274445,
                      -0.41208130121231,
                      -0.22473378479481
                    ]
                  ]
                }, ...many more points
              ]
            ]
          }
        }, ... many more actions
      ]
    }

上記のスニペットは、スケッチの JSON 形式の概要を示しています。

ここでは、各ストロークがアクションとして保存され、タイプは「STROKE」です。ストローク アクションに加えて、アーティストが間違いを犯したり、スケッチ中に考えを変えたりすることを示す必要があったため、ストローク全体の消去または元に戻すアクションとして機能する「削除」アクションを保存することが重要でした。

各ストロークの基本情報が保存されるため、ブラシの種類、ブラシサイズ、色の RGB がすべて収集されます。

最後に、ストロークの各頂点が保存されます。これには、位置、角度、時間、コントローラのトリガーの圧力強度(各ポイント内の p として示されます)が含まれます。

回転は 4 成分の四元数です。これは、後でストロークをレンダリングするときにジンバルロックを回避するために重要です。

WebGL によるスケッチの再生

スケッチをウェブブラウザに表示するために、THREE.js を使用し、Tilt Brush の内部処理を模倣したジオメトリ生成コードを作成しました。

Tilt Brush は、ユーザーの手の動きに応じてリアルタイムで三角形のストリップを生成しますが、ウェブに表示される時点でスケッチ全体がすでに「完成」しています。これにより、リアルタイム計算の多くをバイパスし、読み込み時にジオメトリをベイクできます。

WebGL スケッチ

ストローク内の各頂点ペアは、方向ベクトルを生成します(上記の各点を接続する青い線、以下のコード スニペットの moveVector)。各ポイントには、コントローラの現在の角度を表すクォータニオンである向きも含まれています。三角形ストリップを生成するには、これらのポイントを反復処理して、方向とコントローラの向きに垂直な法線を生成します。

各ストロークの三角形ストリップを計算するプロセスは、Tilt Brush で使用されるコードとほぼ同じです。

const V_UP = new THREE.Vector3( 0, 1, 0 );
const V_FORWARD = new THREE.Vector3( 0, 0, 1 );

function computeSurfaceFrame( previousRight, moveVector, orientation ){
    const pointerF = V_FORWARD.clone().applyQuaternion( orientation );

    const pointerU = V_UP.clone().applyQuaternion( orientation );

    const crossF = pointerF.clone().cross( moveVector );
    const crossU = pointerU.clone().cross( moveVector );

    const right1 = inDirectionOf( previousRight, crossF );
    const right2 = inDirectionOf( previousRight, crossU );

    right2.multiplyScalar( Math.abs( pointerF.dot( moveVector ) ) );

    const newRight = ( right1.clone().add( right2 ) ).normalize();
    const normal = moveVector.clone().cross( newRight );
    return { newRight, normal };
}

function inDirectionOf( desired, v ){
    return v.dot( desired ) >= 0 ? v.clone() : v.clone().multiplyScalar(-1);
}

ストロークの方向と向きを組み合わせるだけでは、数学的に曖昧な結果が返されます。複数の法線が導出され、ジオメトリに「ねじれ」が生じる可能性があります。

ストロークの点を反復処理するときに、「優先右」ベクトルを維持し、これを関数 computeSurfaceFrame() に渡します。この関数は、ストロークの方向(最後のポイントから現在のポイント)とコントローラの向き(クォータニオン)に基づいて、四角形ストリップ内の四角形を導出できる法線を返します。さらに重要なのは、次の計算セット用の新しい「優先右」ベクトルも返すことです。

ストローク

各ストロークのコントロール ポイントに基づいて四角形を生成した後、四角形の角を補間して、四角形を融合します。

function fuseQuads( lastVerts, nextVerts) {
    const vTopPos = lastVerts[1].clone().add( nextVerts[0] ).multiplyScalar( 0.5
);
    const vBottomPos = lastVerts[5].clone().add( nextVerts[2] ).multiplyScalar(
0.5 );

    lastVerts[1].copy( vTopPos );
    lastVerts[4].copy( vTopPos );
    lastVerts[5].copy( vBottomPos );
    nextVerts[0].copy( vTopPos );
    nextVerts[2].copy( vBottomPos );
    nextVerts[3].copy( vBottomPos );
}
融合された四角形
統合された四角形。

各クワッドには、次のステップとして生成される UV も含まれています。一部のブラシには、さまざまなストローク パターンが含まれています。これにより、ストロークごとに異なるペイントブラシで描いたような印象を与えることができます。これは、_テクスチャ アトラス化_を使用して実現します。各ブラシ テクスチャには、考えられるすべてのバリエーションがあります。ストロークの UV 値を変更することで、正しいテクスチャが選択されます。

function updateUVsForSegment( quadVerts, quadUVs, quadLengths, useAtlas,
atlasIndex ) {
    let fYStart = 0.0;
    let fYEnd = 1.0;

    if( useAtlas ){
    const fYWidth = 1.0 / TEXTURES_IN_ATLAS;
    fYStart = fYWidth * atlasIndex;
    fYEnd = fYWidth * (atlasIndex + 1.0);
    }

    //get length of current segment
    const totalLength = quadLengths.reduce( function( total, length ){
    return total + length;
    }, 0 );

    //then, run back through the last segment and update our UVs
    let currentLength = 0.0;
    quadUVs.forEach( function( uvs, index ){
    const segmentLength = quadLengths[ index ];
    const fXStart = currentLength / totalLength;
    const fXEnd = ( currentLength + segmentLength ) / totalLength;
    currentLength += segmentLength;

    uvs[ 0 ].set( fXStart, fYStart );
    uvs[ 1 ].set( fXEnd, fYStart );
    uvs[ 2 ].set( fXStart, fYEnd );
    uvs[ 3 ].set( fXStart, fYEnd );
    uvs[ 4 ].set( fXEnd, fYStart );
    uvs[ 5 ].set( fXEnd, fYEnd );

    });

}
オイルブラシのテクスチャ アトラスの 4 つのテクスチャ
油彩ブラシのテクスチャ アトラス内の 4 つのテクスチャ
Tilt Brush で
Tilt Brush 内
WebGL で
WebGL 内

各スケッチのストロークの数に上限はなく、ストロークを実行時に変更する必要がないため、ストローク ジオメトリを事前に計算して 1 つのメッシュに統合します。新しいブラシタイプはそれぞれ独自のマテリアルである必要がありますが、それでも描画呼び出しはブラシごとに 1 回に減ります。

上記のスケッチ全体は、WebGL の 1 回の描画呼び出しで実行されます。
上記のスケッチ全体は、WebGL の 1 回の描画呼び出しで実行されます

システムをストレス テストするために、できるだけ多くの頂点で空間を埋めるのに 20 分かかるスケッチを作成しました。生成されたスケッチは、WebGL で引き続き 60 fps で再生されました。

ストロークの元の頂点には時間も含まれているため、データを簡単に再生できます。フレームごとにストロークを再計算するのは非常に時間がかかるため、代わりに、読み込み時にスケッチ全体を事前計算し、表示するタイミングで各四角形を表示するようにしました。

四角形を非表示にするには、頂点を 0,0,0 ポイントに収縮するだけです。四角形が開示される時間になると、頂点を元の位置に戻します。

改善の余地があるのは、シェーダーを使用して GPU で頂点を完全に操作することです。現在の実装では、現在のタイムスタンプから頂点配列をループし、表示する必要がある頂点を確認して、ジオメトリを更新することで、頂点を配置します。これにより CPU に負荷がかかり、ファンが回転してバッテリーの消耗が早まります。

仮想アート作品

アーティストの録音

スケッチだけでは不十分だと判断しました。アーティストがスケッチの中にいて、ブラシストロークを描いている様子を表現したいと考えました。

アーティストをキャプチャするために、Microsoft Kinect カメラを使用して、空間内のアーティストの体の深度データを記録しました。これにより、描画と同じ空間に 3 次元の図形を表示できます。

アーティストの身体が遮蔽物となり、背後にあるものを見ることができなくなるため、2 台の Kinect システムを使用しました。どちらも部屋の反対側に置き、中央を向いています。

深度情報に加えて、標準のデジタル一眼レフカメラでシーンの色情報をキャプチャしました。優れた DepthKit ソフトウェアを使用して、深度カメラとカラーカメラの映像をキャリブレーションし、統合しました。Kinect はカラーの撮影が可能です。しかし、露出設定を制御し、美しいハイエンド レンズを使用し、高解像度で撮影できるため、DSLR を使用することを選択しました。

映像を撮影するため、HTC Vive、アーティスト、カメラを収容する特別な部屋を建設しました。すべての表面は、赤外光を吸収する素材で覆われており、より鮮明なポイントクラウドが得られます(壁はデュベタイン、床はリブ付きゴムマット)。素材がポイントクラウド フッテージに写り込んだ場合、白色の素材ほど目立たないように、黒色の素材を選択しました。

レコーディング アーティスト

得られた動画の記録から、粒子システムを投影するのに十分な情報を得ることができました。openFrameworks で追加のツールを作成して、映像をさらにクリーンアップし、特に床、壁、天井を削除しました。

録画された動画セッションの 4 つのチャンネル(上部に 2 つのカラー チャンネル、下部に 2 つの深度チャンネル)
録画された動画セッションの 4 つのチャンネルすべて(上部に 2 つのカラー チャンネル、下部に 2 つの深度チャンネル)

アーティストの表示に加えて、HMD とコントローラも 3D でレンダリングしたいと考えました。これは、最終的な出力で HMD をはっきりと表示するために重要だっただけでなく(HTC Vive の反射レンズが Kinect の IR 測定値を狂わせていたため)、パーティクル出力のデバッグや動画とスケッチの調整に役立ちました。

ヘッドマウント ディスプレイ、コントローラ、粒子が並んでいる
ヘッドマウント ディスプレイ、コントローラ、粒子が並んでいる

これは、HMD とコントローラの位置をフレームごとに抽出するカスタム プラグインを Tilt Brush に書き込むことで実現しました。Tilt Brush は 90 fps で実行されるため、大量のデータがストリーミングされ、スケッチの入力データは圧縮されていない状態で 20 MB を超えていました。また、この手法を使用して、アーティストがツールパネルでオプションを選択したときや、ミラー ウィジェットの位置など、通常の Tilt Brush の保存ファイルには記録されないイベントをキャプチャしました。

収集した 4 TB のデータを処理する際の最大の課題の一つは、さまざまなビジュアル/データソースをすべて調整することでした。デジタル一眼レフカメラの各動画は、対応する Kinect と位置合わせして、空間と時間の両方でピクセルが揃うようにする必要があります。次に、これらの 2 つのカメラ リグからの映像を調整して、1 人のアーティストを形成する必要がありました。次に、3D アーティストを、描画からキャプチャされたデータに合わせる必要がありました。ぜひこうしたタスクのほとんどを支援するブラウザベースのツールを作成しました。こちらからお試しいただけます。

レコーディング アーティスト

データが調整されたら、NodeJS で記述されたスクリプトを使用して、すべてを処理し、動画ファイルと一連の JSON ファイルを出力します。これらはすべてトリミングされ、同期されています。ファイルサイズを削減するために、次の 3 つの方法を採用しました。まず、各浮動小数点数の精度を下げて、最大 3 桁の精度に抑えました。次に、ポイント数を 3 分の 1 に減らして 30 fps とし、クライアントサイドで位置を補間しました。最後に、データをシリアル化しました。これにより、Key-Value ペアを含む単純な JSON を使用する代わりに、HMD とコントローラの位置と回転の値の順序が作成されます。これにより、ファイルサイズは 3 MB 未満に削減され、ワイヤー経由で配信できるようになりました。

レコーディング アーティスト

動画自体は HTML5 動画要素として提供され、WebGL テクスチャによって読み込まれてパーティクルになるため、動画自体はバックグラウンドで非表示で再生する必要がありました。シェーダーは、深度画像の色を 3D 空間内の位置に変換します。James George は、DepthKit から直接取得した映像をどのように活用できるかについて、優れた例を共有しています。

iOS では、インライン動画の再生に制限が設けられています。これは、自動再生されるウェブ動画広告によってユーザーが煩わされることを防ぐためと思われます。ウェブ上の他の回避策と同様の手法を使用しました。つまり、動画フレームをキャンバスにコピーし、動画のシークの時間を 1/30 秒ごとに手動で更新します。

videoElement.addEventListener( 'timeupdate', function(){
    videoCanvas.paintFrame( videoElement );
});

function loopCanvas(){

    if( videoElement.readyState === videoElement.HAVE\_ENOUGH\_DATA ){

    const time = Date.now();
    const elapsed = ( time - lastTime ) / 1000;

    if( videoState.playing && elapsed >= ( 1 / 30 ) ){
        videoElement.currentTime = videoElement.currentTime + elapsed;
        lastTime = time;
    }

    }

}

frameLoop.add( loopCanvas );

このアプローチには、動画からキャンバスへのピクセル バッファのコピーが非常に CPU 使用率が高いため、iOS のフレームレートが大幅に低下するという残念な副作用がありました。この問題を回避するため、iPhone 6 で 30 fps 以上を実現できる、同じ動画のサイズを小さくしたバージョンを配信しました。

まとめ

2016 年の VR ソフトウェア開発に関する一般的なコンセンサスは、HMD で 90 fps 以上で実行できるように、ジオメトリとシェーダーをシンプルに保つことです。Tilt Brush で使用されている手法は WebGL に非常に適しているため、これは WebGL デモの優れたターゲットになりました。

複雑な 3D メッシュを表示するウェブブラウザ自体は魅力的ではありませんが、VR とウェブの相互作用が完全に可能であることを示すコンセプト プロトタイプでした。