WebVR でダンストナイト

Google Data Arts チームから、Moniker と私に、WebVR によってもたらされる可能性を探求するための共同作業について連絡があったとき、私は興奮しました。長年にわたってこのチームの仕事を観察してきましたが、そのプロジェクトはいつも心に響きました。このコラボレーションの結果、Dance Tonite は、LCD サウンドシステムとそのファンによる、絶えず変化する VR ダンス エクスペリエンスを創出しました。具体的な手順は次のとおりです。

概要

Google はまず、WebVR を使用して一連のプロトタイプの開発を始めました。WebVR は、ブラウザでウェブサイトにアクセスして VR を開始できるオープン スタンダードです。目標は、どんなデバイスでも、誰もが VR 体験を簡単に楽しめるようにすることです。

Google は、このフィードバックを真摯に受け止め、どのような方法でも、Google の Daydream View、Cardboard、Samsung の Gear VR などのスマートフォンに対応した VR ヘッドセットから、HTC VIVE や Oculus Rift などのルームスケール システムまで、あらゆる種類の VR で動作するはずです。これらのシステムは、仮想環境に物理的な動きを反映します。おそらく最も重要な点は、VR デバイスを所有していないすべての人にとっても役に立つものを作ることがウェブの精神に則っていると考えたことです。

1. DIY モーション キャプチャ

ユーザーを創造的に巻き込みたいと考えたため、VR を使用した参加と自己表現の可能性を検討し始めました。驚くべきことに、VR では動き回ったり見回したりできるだけでなく、再現性の高さにも驚かされました。そこで、あるアイデアが浮かびました。ユーザーに何かを表示したり作成したりしてもらうのではなく、ユーザーの動きが記録されるようにするのはどうでしょうか。

ダンス トゥナイトで自分自身を撮影している人物。後ろの画面には、ヘッドセットに表示されている内容が表示されます。

私たちは、ダンスをしながら VR ゴーグルとコントローラの位置を記録したプロトタイプを作成しました。私たちは記録された位置を抽象的な形状に置き換えて 驚くべきことに驚きました結果は非常に人間的で、個性にあふれていました。すぐに、WebVR を使用して自宅で安価なモーション キャプチャを行うことができることに気づきました。

WebVR では、デベロッパーは VRPose オブジェクトを介してユーザーの頭の位置と向きにアクセスできます。この値は VR ハードウェアによってフレームごとに更新されるため、コードは正しい視点から新しいフレームをレンダリングできます。WebVR の GamePad API を使用すると、GamepadPose オブジェクトを介してユーザー コントローラの位置や向きにアクセスすることもできます。これらの位置と向きの値をフレームごとにすべて保存し、ユーザーの動きが「記録」されるようにします。

2. ミニマリズムと衣装

現在のルームスケール VR 機器では、ユーザーの頭と両手という 3 つのポイントをトラッキングできます。Dance Tonite では、空間内の 3 つのポイントの動きにおける人間らしさに焦点を当てています。これを実現するため、動きに集中できるように、デザインは最小限に抑えました。人々の頭脳を働かせるというアイデアが好きでした。

スウェーデンの心理学者 Gunnar Johansson の作品を示すこの動画は、できるだけシンプルにすることを検討する際に参考にした例の一つです。浮かんでいる白い点が動いているときに物体として瞬時に認識できることを示しています。

視覚的には、1970 年に Margarete Hastings が再構成した Oskar Schlemmer の「三位一体のバレエ」の録画で、色付きの部屋と幾何学的な衣装にインスピレーションを得ました。

シュレマーが抽象的な幾何学的な衣装を選んだ理由は、ダンサーの動きを人形やマリオネットの動きに制限することでしたが、Dance Tonite ではその逆の目標を掲げました。

最終的に、回転によって伝えられる情報量に基づいて形状を選択しました。球体はどの方向に回転しても同じに見えますが、円錐は向いている方向を向いていて、前面と背面が異なります。

3. 移動用のループペダル

撮影された大勢の人が一緒に踊ったり動いたりする様子を表現したいと考えました。VR デバイスはまだ十分に普及していないため、ライブ配信は現実的ではありません。それでも、動きを通じて人々が互いに反応し合うようにしたいと考えました。Norman McClaren が 1964 年に発表した動画「Canon」で 再帰的にパフォーマンスを披露しました

McClaren のパフォーマンスは、高度に振り付けられた一連の動きで構成されており、ループごとに相互作用が始まります。ミュージシャンがさまざまなライブ音楽を重ねて一緒にジャムを楽しむ音楽のループペダルのように、ルーズな演奏をユーザーが自由に即興で演奏できる環境を作ることができるかと考えました。

4. 相互接続された部屋

相互接続された部屋

多くの音楽と同様に、LCD Soundsystem のトラックの構成は、正確にタイミングをとった小節で構成されています。本プロジェクトで取り上げているトラック Tonite は、ちょうど 8 秒の長さのメジャーを特徴としています。トラックの 8 秒ループごとにユーザーがパフォーマンスを披露できるようにしました。これらの小節のリズムは変わりませんが、音楽の内容は変わります。曲が進むにつれて、楽器やボーカルが変化し、演奏者がさまざまな方法で反応する場面があります。これらの各測定値は部屋として表現され、ユーザーはそれに適したパフォーマンスを作成できます。

パフォーマンスの最適化: フレームをドロップしない

単一のコードベースで実行され、デバイスやプラットフォームごとに最適なパフォーマンスを実現するマルチプラットフォーム VR エクスペリエンスを作成することは、簡単なことではありません。

VR で最も吐き気を催す原因の一つは、動きにフレームレートが追いつかないことです。頭を回しても目で確認できる視覚と内耳の動きが合わないと、すぐに腹が足りなくなります。このため、大きなフレームレートの遅延を回避する必要がありました。実装した最適化の例を以下に示します。

1. インスタンス化されたバッファのジオメトリ

プロジェクト全体で使用している 3D オブジェクトはごく少数であるため、インスタンス化バッファ ジオメトリを使用することでパフォーマンスを大幅に向上させることができました。基本的に、オブジェクトを GPU に 1 回アップロードし、1 回の描画呼び出しでそのオブジェクトの「インスタンス」を任意の数だけ描画できます。Dance Tonite には、3 つの異なるオブジェクト(円錐、円柱、穴のある部屋)しかありませんが、これらのオブジェクトのコピーは数百個になる可能性があります。インスタンス バッファ ジオメトリは ThreeJS の一部ですが、THREE.InstanceMesh を実装する Dusan Bosnjak の試験運用中かつ進行中のフォークを使用して、インスタンス化されたバッファ ジオメトリの操作がはるかに簡単になりました。

2. ガベージ コレクタの回避

他の多くのスクリプト言語と同様に、JavaScript は、割り振られたオブジェクトのうち、使用されていないオブジェクトを検出して、メモリを自動的に解放します。このプロセスはガベージ コレクションと呼ばれます。

デベロッパーは、このタイミングを制御できません。ガベージ コレクタはいつでもドアを開けてゴミを空にします。その間、フレームがドロップされる可能性があります。

この問題を解決するには、オブジェクトを再利用して、ゴミをできるだけ減らします。計算ごとに新しいベクター オブジェクトを作成する代わりに、再利用できるようにスクラッチ オブジェクトをマークしました。Google は、これらのリファレンスを Google のスコープの外部に移動することで保持しているため、削除対象としてマークされていません。

たとえば、次のコードは、ユーザーの頭と手の位置マトリックスを、各フレームで保存する位置 / 回転値の配列に変換します。SERIALIZE_POSITIONSERIALIZE_ROTATIONSERIALIZE_SCALE を再利用することで、関数が呼び出されるたびに新しいオブジェクトを作成した場合に発生するメモリ割り当てとガベージ コレクションを回避できます。

const SERIALIZE_POSITION = new THREE.Vector3();
const SERIALIZE_ROTATION = new THREE.Quaternion();
const SERIALIZE_SCALE = new THREE.Vector3();
export const serializeMatrix = (matrix) => {
    matrix.decompose(SERIALIZE_POSITION, SERIALIZE_ROTATION, SERIALIZE_SCALE);
    return SERIALIZE_POSITION.toArray()
    .concat(SERIALIZE_ROTATION.toArray())
    .map(compressNumber);
};

3. モーションのシリアル化とプログレッシブ再生

VR でのユーザーの動きをキャプチャするには、ヘッドセットとコントローラの位置と回転をシリアル化し、このデータをサーバーにアップロードする必要がありました。最初は、すべてのフレームの完全な変換行列をキャプチャしていました。これはパフォーマンスは良好でしたが、16 個の数字を 3 つの位置でそれぞれ 90 フレーム / 秒で乗算すると、非常に大きなファイルになり、データのアップロードとダウンロードに時間がかかりました。変換行列から位置データと回転データのみを抽出することで、これらの値を 16 から 7 に減らすことができました。

ウェブサイトの訪問者は、何が期待できるかを正確に把握せずにリンクをクリックすることがよくあります。そのため、視覚的なコンテンツをすばやく表示する必要があります。そうしないと、数秒以内に離脱してしまいます。

そのため、プロジェクトをできるだけ早く開始できるようにしたいと考えました。当初、Google は移動データを読み込む形式として JSON を使用していました。問題は、JSON ファイルを解析する前に、JSON ファイルをすべて読み込む必要があることです。あまり進歩的ではない。

Dance Tonite のようなプロジェクトを可能な限り高いフレームレートで表示するため、ブラウザでは各フレームで JavaScript の計算を行う時間が短くなっています。時間がかかりすぎると、アニメーションが途切れ始めます。最初は、これらの巨大な JSON ファイルがブラウザでデコードされるため、スタッタリングがありました。

改行区切り JSON または NDJSON と呼ばれる便利なストリーミング データ形式を見つけました。ここでのコツは、一連の有効な JSON 文字列が 1 行に 1 つずつ含まれるファイルを作成することです。これにより、読み込み中にファイルを解析し、読み込みが完了する前にパフォーマンスを表示できます。

ある録音の一部は次のようになります。

{"fps":15,"count":1,"loopIndex":"1","hideHead":false}
[-464,17111,-6568,-235,-315,-44,9992,-3509,7823,-7074, ... ]
[-583,17146,-6574,-215,-361,-38,9991,-3743,7821,-7092, ... ]
[-693,17158,-6580,-117,-341,64,9993,-3977,7874,-7171, ... ]
[-772,17134,-6591,-93,-273,205,9994,-4125,7889,-7319, ... ]
[-814,17135,-6620,-123,-248,408,9988,-4196,7882,-7376, ... ]
[-840,17125,-6644,-173,-227,530,9982,-4174,7815,-7356, ... ]
[-868,17120,-6670,-148,-183,564,9981,-4069,7732,-7366, ... ]
...

NDJSON を使用すると、パフォーマンスの個々のフレームのデータ表現を文字列として保持できます。必要な時間に達するまで待ってから、位置データにデコードすることで、必要な処理を時間の経過とともに分散させることができます。

4. モーションの補間

同時に実行されるパフォーマンスを 30 ~ 60 個表示することを目標としていたため、データレートをさらに抑える必要がありました。Data Arts チームは、Tilt Brush を使用して VR で絵を描くアーティストの録音を再生する Virtual Art Sessions プロジェクトで同じ問題に取り組みました。この問題は、フレームレートの低いユーザーデータの中間バージョンを作成し、再生中にフレーム間を補間することで解決しました。15 FPS で実行される補間された録画と元の 90 FPS 録画の違いをほとんど見分けることができなかったのは驚きでした。

実際に確認するには、?dataRate= クエリ文字列を使用して、Dance Tonite でさまざまなレートでデータを再生するように強制します。これを使用して、90 フレーム/秒45 フレーム/秒15 フレーム/秒で記録されたモーションを比較できます。

位置については、キーフレーム間の時間の近さ(比率)に基づいて、前のキーフレームと次のキーフレームとの間で線形補間を行います。

const { x: x1, y: y1, z: z1 } = getPosition(previous, performanceIndex, limbIndex);
const { x: x2, y: y2, z: z2 } = getPosition(next, performanceIndex, limbIndex);
interpolatedPosition = new THREE.Vector3();
interpolatedPosition.set(
    x1 + (x2 - x1) * ratio,
    y1 + (y2 - y1) * ratio,
    z1 + (z2 - z1) * ratio
    );

向きについては、キーフレーム間で球面線形補間(slerp)を行います。向きは Quaternions として保存されます。

const quaternion = getQuaternion(previous, performanceIndex, limbIndex);
quaternion.slerp(
    getQuaternion(next, performanceIndex, limbIndex),
    ratio
    );

5. 動きを音楽に同期する

録画したアニメーションのどのフレームを再生するかを把握するには、音楽の現在の時刻をミリ秒単位で把握する必要があります。HTML Audio 要素は音声の段階的な読み込みと再生に最適ですが、提供する時間プロパティはブラウザのフレームループと同期して変化しません。常に少しずれています。数ミリ秒早い場合もあれば、数ミリ秒遅い場合もあります。

これにより、美しいダンスの録画が途切れてしまい、これは絶対に避けるべきことです。これを修正するため、JavaScript で独自のタイマーを実装しました。これにより、フレーム間の変化時間と、前回のフレームから経過した時間が正確に一致するようになります。タイマーが音楽と 10 ms 以上ずれた場合は、タイマーを再度同期します。

6. 除去とフォグ

どのストーリーも良い結末には欠かせない要素です。Google は、その体験を終えたユーザーに驚くような何かを望みました。最後の部屋を出ると、円錐と円柱が並ぶ静かな風景が広がります。「これで終わりか?」と疑問に思うかもしれません。フィールドの奥に進むと、音楽の音色によって、コーンや円柱のさまざまなグループがダンサーに変化します。大きなパーティーの真っ最中です。音楽が突然止まると、すべてが地面に落ちます。

視聴者としては快適でしたが、パフォーマンスの問題がいくつか発生しました。新しいエンディングに必要な 40 回以上の追加のパフォーマンスを、ルームスケール VR デバイスとそのハイエンド ゲーム用機材で完璧に実行できました。ただし、一部のモバイル デバイスではフレームレートが半減しました。

この問題に対処するため、フォグを導入しました。一定の距離を過ぎると、すべてがゆっくりと黒くなります。可視でないものを計算したり描画したりする必要がないため、可視でない部屋のパフォーマンスを除外し、CPU と GPU の両方の負荷を軽減できます。では、適切な距離をどのように決めればよいでしょうか。

デバイスによっては、どんなものでも処理できるものもあれば、制限が厳しいものもあります。スライド制を導入することになりました。毎秒のフレーム数を継続的に測定することで、それに応じて霧の距離を調整できます。フレームレートがスムーズに実行されている限り、フォグを押し出してレンダリング作業を増やします。フレームレートが十分にスムーズに実行されていない場合は、フォグを近づけて、暗闇でのレンダリング パフォーマンスをスキップできるようにします。

// this is called every frame
// the FPS calculation is based on stats.js by @mrdoob
tick: (interval = 3000) => {
    frames++;
    const time = (performance || Date).now();
    if (prevTime == null) prevTime = time;
    if (time > prevTime + interval) {
    fps = Math.round((frames * 1000) / (time - prevTime));
    frames = 0;
    prevTime = time;
    const lastCullDistance = settings.cullDistance;

    // if the fps is lower than 52 reduce the cull distance
    if (fps <= 52) {
        settings.cullDistance = Math.max(
        settings.minCullDistance,
        settings.cullDistance - settings.roomDepth
        );
    }
    // if the FPS is higher than 56, increase the cull distance
    else if (fps > 56) {
        settings.cullDistance = Math.min(
        settings.maxCullDistance,
        settings.cullDistance + settings.roomDepth
        );
    }
    }

    // gradually increase the cull distance to the new setting
    cullDistance = cullDistance * 0.95 + settings.cullDistance * 0.05;

    // mask the edge of the cull distance with fog
    viewer.fog.near = cullDistance - settings.roomDepth;
    viewer.fog.far = cullDistance;
}

誰もが楽しめるもの: ウェブ向け VR の構築

マルチプラットフォームの非対称エクスペリエンスを設計、開発するには、デバイスに応じて各ユーザーのニーズを考慮する必要があります。設計上のあらゆる決定において、それが他のユーザーにどのような影響を与えるかを検討する必要がありました。VR でも、VR なしでも同じようにエキサイティングな体験をするにはどうすればよいのでしょうか?

1. 黄色い球体

つまり、ルームスケールの VR ユーザーはパフォーマンスを撮影しますが、モバイル VR デバイス(Cardboard、Daydream View、Samsung Gear など)のユーザーはこのプロジェクトをどのように体験するのでしょうか。そのため、環境に新しい要素(黄色いオーブ)を導入しました。

黄色い球体
黄色のオーブ

VR でプロジェクトを視聴すると、黄色い球体の視点から視聴できます。部屋から部屋へと浮かび上がる中、ダンサーたちはあなたの存在に反応します。 ジェスチャーをしたり、周囲を踊ったり、背後で変な動きをしたりするほか、ぶつからないようすばやく道を譲ります。黄色い球体は常に注目の的です。

これは、演奏の録画中に、黄色いオーブが音楽に合わせて部屋の中央を移動し、ループして戻ってくるためです。オーブの位置は、パフォーマーが時間のどこにいて、ループに残りどれくらいの時間があるかを知る手がかりになります。開発者にとっては パフォーマンスの構築に集中できます

2. 別の視点から

VR を使用しないユーザーを除外したくはありませんでした。特に、最大のオーディエンスとなる可能性が高いため、Google は、人為的な VR 体験を作り出すのではなく、画面ベースのデバイスに独自の体験を提供しようと考えました。パフォーマンスをアイソメトリックな視点から上から表示するというアイデアがありました。この考え方は、コンピュータ ゲームに豊富な歴史があります。最初に使用されたのは、1982 年のスペース シューティング ゲーム「Zaxxon」です。VR ユーザーはゲームの中心にいるのに対し、アイソメトリック ビューでは、神のような視点でアクションを把握できます。モデルを少し拡大して、ドールハウスのような外観にしました。

3. シャドウ: うまくいくまで、うまくいっているふりをする

一部のユーザーが、等角的な視点で深度を確認するのが難しいことが判明しました。Zaxxon が、飛行物体の下にダイナミックなシャドウを投影した史上初のコンピュータ ゲームの 1 つになったのも、この理由からでしょう。

影

3D で影を作るのは難しいことがわかりました。特に、携帯電話などの制限のあるデバイスでは、当初、それらを除外するという難しい決断を下す必要がありましたが、Three.js の作者であり、経験豊富なデモハッカーである Mr doob にアドバイスを求めたところ、彼は… フェイクにするという斬新なアイデアを思いつきました。

それぞれの浮遊物体が光を遮り、さまざまな形状の影を投げかける様子を計算しなくても、それぞれの下に同じ円形のぼかしテクスチャ画像を描画します。そもそもビジュアルは現実を模倣しようとするものではないため、ほんの少し調整するだけで、問題なく対応できました。対象物が地面に近づくほど テクスチャは暗くなります上に向かって移動すると、テクスチャの透明度が上がり、サイズが大きくなります。

これらを作成するため、白から黒の柔らかなグラデーション(アルファ透明なし)のこのテクスチャを使用しました。マテリアルを透明に設定し、減算ブレンドを使用します。これにより、重なり合うときにうまくブレンドされます。

function createShadow() {
    const texture = new THREE.TextureLoader().load(shadowTextureUrl);
    const material = new THREE.MeshLambertMaterial({
        map: texture,
        transparent: true,
        side: THREE.BackSide,
        depthWrite: false,
        blending: THREE.SubtractiveBlending,
    });
    const geometry = new THREE.PlaneBufferGeometry(0.5, 0.5, 1, 1);
    const plane = new THREE.Mesh(geometry, material);
    return plane;
    }

4. そこにいること

VR を使用しないビジターは、パフォーマーの頭をクリックすることで、ダンサーの視点で動画を視聴できます。この角度から見ると、細かい部分がよくわかります。ダンサーは、パフォーマンスのテンポを合わせるために、互いをすばやくちらっと見ます。球体が部屋に入ってくると、猫は緊張してその方向を見つめます。視聴者はこれらの動きに影響することはできませんが、没入感は驚くほどよく伝わります。繰り返しになりますが、Google は、マウス操作の単調な疑似 VR バージョンをユーザーに提示するよりも、この方法を優先しました。

5. 録画の共有

20 人もの出演者が互いに反応し合う、複雑な振り付けの録画が成功したとき、クリエイターがどれほど誇らしく思うか、YouTube は理解しています。ユーザーが友達に見せたいと考えるであろうことはわかっていました。しかし、この偉業の静止画像では十分に伝わりません。パフォーマンスの動画を共有できるようにしたいと考えました実は、GIF でもいいのではないでしょうか?アニメーションはフラットシェーディングで、このフォーマットの制限されたカラーパレットに最適です。

録音の共有

そのため、ブラウザ内からアニメーション GIF をエンコードできる JavaScript ライブラリである GIF.js を採用しました。フレームのエンコードを、個別のプロセスとしてバックグラウンドで実行できる Web Worker にオフロードします。これにより、並列して動作する複数のプロセッサを利用できます。

残念ながら、アニメーションに必要なフレーム数では、エンコード プロセスがまだ遅すぎました。GIF は、制限されたカラーパレットを使用して小さなファイルを作成できます。時間のほとんどが、各ピクセルに最も近い色を見つけることに費やされていることがわかりました。小さなショートカットをハッキングすることで、このプロセスを 10 倍に最適化することができました。ピクセルの色が前回と同じ場合は、パレットから前回と同じ色を使用します。

エンコードは高速になりましたが、生成される GIF ファイルのサイズが大きすぎました。GIF 形式では、各フレームの破棄方法を定義することで、各フレームを前フレームの上にどのように表示するかを指定できます。ファイルサイズを小さくするため、フレームごとに各ピクセルを更新するのではなく、変更されたピクセルのみを更新します。エンコード プロセスは再び遅くなりましたが、ファイルサイズは大幅に削減されました。

6. 堅実な基盤: Google Cloud と Firebase

「ユーザー作成コンテンツ」サイトのバックエンドは複雑で脆弱になりがちですが、Google Cloud と Firebase のおかげで、シンプルで堅牢なシステムを構築できました。パフォーマーが新しいダンスをシステムにアップロードすると、Firebase Authentication によって匿名で認証されます。Cloud Storage for Firebase を使用して、録音を一時的なスペースにアップロードする権限が付与されます。アップロードが完了すると、クライアント マシンは Firebase トークンを使用して Cloud Functions for Firebase HTTP トリガーを呼び出します。これにより、送信の検証、データベース レコードの作成、Google Cloud Storage の公開ディレクトリへの記録移動を行うサーバー プロセスがトリガーされます。

地面がしっかりしている

公開コンテンツはすべて、Cloud Storage バケット内の一連のフラット ファイルに保存されます。つまり、世界中のどこからでもデータに迅速にアクセスでき、トラフィックの負荷が高くなってもデータの可用性に影響することはありません。

Firebase Realtime Database と Cloud Functions エンドポイントを使用して、新しい投稿を VR で視聴し、任意のデバイスから新しい再生リストを公開できるシンプルなモデレーション/キュレーション ツールを構築しました。

7. Service Worker

Service Worker は、ウェブサイト アセットのキャッシュ管理に役立つ比較的新しい技術です。たとえば、Service Worker を使用すると、リピーターのコンテンツを瞬時に読み込み、サイトをオフラインで動作させることもできます。訪問者の多くは、品質の異なるモバイル接続を使用しているため、これらの機能は重要です。

プロジェクトに Service Worker を追加するのは簡単でした。煩雑な作業のほとんどを処理してくれる便利な webpack プラグインのおかげです。次の構成では、すべての静的ファイルを自動的にキャッシュに保存する Service Worker を生成します。プレイリストは常に更新されるため、最新のプレイリスト ファイルがネットワークから取得されます(利用可能な場合)。録音 JSON ファイルは変更されないため、利用可能な場合はキャッシュから取得する必要があります。

const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
config.plugins.push(
    new SWPrecacheWebpackPlugin({
    dontCacheBustUrlsMatching: /\.\w{8}\./,
    filename: 'service-worker.js',
    minify: true,
    navigateFallback: 'index.html',
    staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
    runtimeCaching: [{
        urlPattern: /playlist\.json$/,
        handler: 'networkFirst',
    }, {
        urlPattern: /\/recordings\//,
        handler: 'cacheFirst',
        options: {
        cache: {
            maxEntries: 120,
            name: 'recordings',
        },
        },
    }],
    })
);

現時点では、プラグインは音楽ファイルのように段階的に読み込まれるメディア アセットを処理しないため、ブラウザがファイルを最大 1 年間キャッシュに保存できるように、これらのファイルの Cloud Storage Cache-Control ヘッダーを public, max-age=31536000 に設定することでこの問題に対処しました。

まとめ

クリエイターがこの機能にどのような工夫を加え、モーションを使った創造的な表現のツールとして活用するか、楽しみにしています。すべてのコードはオープンソースとしてリリースされています。コードは https://github.com/puckey/dance-tonite で入手できます。VR の初期段階、特に WebVR の初期段階において、この新しいメディアがどのような新しい創造的で予想外の方向に進むのか、楽しみにしています。踊り明かす