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

アートセッションの詳細

概要

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

https://g.co/VirtualArtSessions

生きていていいんだ!仮想現実が消費者製品として導入されたことで、 新しい可能性や未開拓の可能性が発見されています。Tilt Brush は HTC Vive で入手できる Google プロダクトで、3 次元空間に描画できます。Tilt Brush を初めて試したとき、モーション トラッキング コントローラと「超能力のある部屋」という存在感を組み合わせても絵を描く感覚は残ります。しかし、周りの何もない空間に絵を描けるような体験はありえません。

バーチャルなアート作品

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

バーチャル リアリティで絵を描く

Unity に組み込まれた Tilt Brush ソフトウェア自体は、ルームスケール VR を使用して頭の位置(ヘッドマウント ディスプレイ(HMD))と両手のコントローラを追跡するデスクトップ アプリケーションです。Tilt Brush で作成されたアートワークは、デフォルトでは .tilt ファイルとしてエクスポートされます。こうしたエクスペリエンスをウェブでも実現するには、アートワークのデータだけでは不十分だという結論に至りました。Google は 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 形式の形式を示しています。

ここでは、各ストロークは「ストローク」というタイプのアクションとして保存されます。ストローク操作に加えて、アーティストがスケッチの途中で間違いを犯して気が変わっている様子を見せたいと考えているため、ストローク全体で消去または元に戻す操作として機能する「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() に渡します。この関数により法線が得られ、ストロークの方向(最終点から現在の点まで)とコントローラの向き(四元数)に基づいて、クワッド ストリップのクワッドを導出できます。さらに重要なこととして、次の計算セット用の新しい「優先する右」ベクトルも返されます。

ストローク

各ストロークのコントロール ポイントに基づいてクワッドを生成した後、角を補間してクワッドを融合します。その角を 1 つのクワッドから次のクワッドへと補間します。

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 を使用
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 );

私たちのアプローチには、動画からキャンバスへのピクセル バッファのコピーは CPU の負荷が非常に大きいため、iOS フレームレートを大幅に下げるという残念な副作用がありました。これを回避するため、iPhone 6 で 30 fps 以上の動画サイズを小さくしました。

まとめ

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

複雑な 3D メッシュを表示するウェブブラウザ自体は魅力的とは言えませんが、これは VR 作品とウェブの相互作用を交互に繰り返すことを目指す概念実証です。