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

アート セッションの詳細

概要

6 人のアーティストが VR で絵を描き、デザインし、彫刻することができました。これは、Google がセッションを記録し、データを変換して、ウェブブラウザでリアルタイムに表示するプロセスです。

https://g.co/VirtualArtSessions

なんとすごい!バーチャル リアリティが消費者製品として導入されるなか、新しい可能性や未開拓の可能性が生まれています。HTC Vive で利用可能な Google 製品である Tilt Brush を使用すると、3 次元空間で描画できます。Tilt Brush を初めて試したとき、モーション トラッキング コントローラを使って絵を描くような感覚と、「超能力のある部屋にいる」という感覚が残ります。周りの何もない空間に絵を描くような体験はありえません。

バーチャルなアート作品

Google の Data Arts チームには、Tilt Brush がまだ動作していないウェブで VR ヘッドセットのない人にもこのエクスペリエンスを披露するという課題が提示されました。そこでチームは彫刻家、イラストレーター、コンセプト デザイナー、ファッション アーティスト、インスタレーション アーティスト、ストリート アーティストを招き、この新しいメディアで独自のスタイルでアートワークを制作しました。

バーチャル リアリティで図形描画を記録する

Unity に組み込まれた Tilt Brush ソフトウェア自体は、ルームスケール VR を使用して頭の位置(ヘッドマウント ディスプレイ(HMD))と両手のコントローラを追跡するデスクトップ アプリケーションです。Tilt Brush で作成したアートワークは、デフォルトでは .tilt ファイルとしてエクスポートされます。このエクスペリエンスをウェブでも実現するためには、アートワークのデータだけでは不十分であると認識しました。Tilt Brush チームと緊密に連携して Tilt Brush を修正し、元に戻す操作と削除操作、アーティストの頭と手の位置を 1 秒間に 90 回エクスポートできるようにしました。

描画時に、Tilt Brush がコントローラの位置と角度を取得し、時間の経過とともに複数のポイントを「ストローク」に変換します。例についてはこちらをご覧ください。Google は、これらのストロークを抽出し、未加工の 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」のアクションとして保存されています。ストロークのアクションに加えて、アーティストがスケッチの途中で間違えて考えを変える様子を見せたいと考えていたため、ストローク全体に対して消去または元に戻すアクションを行う「DELETE」アクションを保存することが重要でした。

各ストロークの基本情報が保存されるため、ブラシタイプ、ブラシサイズ、カラー(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 も含まれています。一部のブラシにはさまざまなストローク パターンが用意されており、それぞれのストロークがペイントブラシの異なるストロークのように感じられます。これは、各ブラシ テクスチャに考えられるバリエーションがすべて含まれている _texture アトラシングを使用して行います。ストロークの 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 の場合
WebGL の場合
WebGL の場合

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

上のスケッチ全体が WebGL での 1 回の描画呼び出しで実行されています。
上のスケッチ全体を WebGL での 1 回の描画呼び出しで実行

システムのストレステストを行うため、20 分かけてスペースを可能な限り多くの頂点で埋めるスケッチを作成しました。結果として得られるスケッチは、WebGL により 60 fps で引き続き再生されます。

ストロークの元の各頂点にも時間が含まれていたため、データを簡単に再生できます。フレームごとのストロークの再計算は非常に時間がかかるため、代わりに負荷時にスケッチ全体を事前計算し、適切なタイミングで各クワッドを明らかにしました。

クワッドを非表示にするということは、単にその頂点を 0,0,0 のポイントに折りたたむということです。クワッドが表示されるはずの時点に達したら、頂点の位置を元の位置に戻します。

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

バーチャルなアート作品

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

スケッチだけでは不十分だと感じました。私たちは、スケッチの中にあるアーティストたちに、それぞれの筆遣いを描きたいと考えました。

アーティストを撮影するために、Microsoft Kinect カメラを使用して、宇宙空間におけるアーティストの身体の奥行きデータを記録しました。これにより、図形描画と同じスペースに 3 次元の図形を表示できます。

アーティストの体が密閉されて背後にあるものが見えないよう、ダブル Kinect システムを使用しました。どちらも部屋の両側から中央に向けて設置しました。

奥行き情報に加えて、標準のデジタル一眼レフカメラでシーンの色情報も取得しました。優れた DepthKit ソフトウェアを使用して、深度カメラとカラーカメラの映像を調整し、統合しました。Kinect も色を記録できますが、露出設定を調整し、美しいハイエンド レンズを使用し、高解像度で記録できるため、デジタル一眼レフを使用することにしました。

動画を撮影するために、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 );

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

おわりに

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

複雑な 3D メッシュを表示するウェブブラウザ自体は面白くありませんが、これは VR 作品とウェブの相互作用を交差させるという概念実証です。