在 WebVR 中運用舞池

當 Google Data Arts 團隊接觸 Moniker 時 我便感到興奮,希望能一同探索 WebVR 帶來的無限可能我多年觀察他們團隊的成果,他們的專案總是能引起我的共鳴。透過這項合作計畫,我們成功打造出 Dance Tonite,這是與 LCD Soundsystem 和他們的粉絲相比,在這個瞬息萬變的 VR 舞蹈體驗。我們的做法如下

概念

我們一開始會使用 WebVR 開發一系列原型,而 WebVR 是一種開放標準,讓開發人員能透過瀏覽器造訪網站,進入 VR 模式。我們的目標是讓所有人都能輕鬆體驗 VR,無論使用何種裝置皆可。

我們走了這顆心。無論我們採用哪種方法,都應該適用於所有類型的 VR,從可搭配手機使用的 VR 頭戴式裝置 (例如 Google Daydream View、Cardboard 和 Samsung Gear VR),到可在房間內使用的系統 (例如 HTC VIVE 和 Oculus Rift),這些系統都能在虛擬環境中反映你的實際動作。或許最重要的是,我們認為打造出可供不具備 VR 裝置的使用者使用的內容,符合網路的精神。

1. 自行製作動作捕捉

我們希望讓使用者以創意方式參與,因此開始研究使用 VR 進行參與和自我表達的可能性。我們對您在 VR 中移動和環顧四周的細膩程度,以及畫面精確度感到驚豔。這讓我們有了想法。與其讓使用者觀看或創作,不如能記錄他們的動作呢?

有人使用 Dance Tonite 錄製自己的影片。後方的螢幕顯示耳機正在播放的內容

我們製作了原型,記錄舞蹈時的 VR 眼鏡和控制器位置。我們將記錄的位置替換成抽象形狀,結果令人驚艷。結果非常人性化,而且充滿個人風格!我們很快就發現可以透過 WebVR 在家中使用 WebVR。

透過 WebVR,開發人員可以透過 VRPose 物件存取使用者的頭部位置和方向。這個值會在每個影格中由 VR 硬體更新,以便程式碼從正確的觀點轉譯新影格。透過 GamePad API 與 WebVR,我們也可以透過 GamepadPose 物件存取使用者控制器的位置/方向。我們只需在每個影格中儲存所有這些位置和方向值,即可建立使用者動作的「錄影」。

2. 極簡主義與服裝

透過現今的房間規模 VR 設備,我們可以追蹤使用者身體的三個點:頭部和兩隻手。在 Dance Tonite 中,我們希望能持續聚焦於這 3 個點在空間中移動時的自然感。為達到這一目標,我們盡可能減少美學元素,專注於動作。我們很喜歡讓使用者動腦思考的想法。

這部影片展示了瑞典心理學家 Gunnar Johansson 的作品,也是我們在考慮盡可能簡化內容時參考的其中一個例子。這張圖片顯示,當浮動白點在移動時,我們可以立即辨識出人體。

在視覺方面,我們受到這部錄影帶的啟發,該錄影帶記錄了 Margarete Hastings 於 1970 年重現 Oskar Schlemmer 的「Triadic Ballet」時,所採用的彩色房間和幾何圖形服裝。

雖然 Schlemmer 選擇抽象幾何圖形服裝的原因,是為了讓舞者的動作與木偶和提線木偶的動作一致,但我們在製作《Dance Tonite》時,目標卻正好相反。

我們最後選擇的形狀,是根據這些形狀透過旋轉可傳達的資訊量。球體不論旋轉角度為何,外觀都會維持不變,但圓錐會朝著觀看方向指向,且正面和背面外觀不同。

3. 環狀移動踏板

我們想讓觀眾知道,有大量觀眾在跳舞和相互運動的錄影片段。由於 VR 裝置數量不多,因此無法直播。但我們仍希望一群人 透過運動來回應彼此我們想到諾曼·麥克倫 (Norman McClaren) 在 1964 年影片作品「Canon」中,重複出現的動作。

McClaren 的演出內容包括一系列經過精心編排的動作,這些動作會在每次迴圈後開始互動。就像音樂中的迴圈踏板,音樂家可以透過重疊不同的現場音樂,與自己即興演出,我們也想知道能否打造一個環境,讓使用者可以自由即興表演。

4. 連通房

連通房

和許多音樂一樣,LCD Soundsystem 的曲目是使用精確計時的節拍建構而成。他們的賽道 Tonite 是我們的專案所呈現的,特色是長度為 8 秒的特徵。我們希望使用者針對曲目中的每個 8 秒迴圈做出表演。雖然這些小節的節奏沒有改變,但音樂內容會有所不同。隨著歌曲的進行,有時會出現不同的樂器和人聲,表演者可以以不同的方式做出回應。每項指標都會以房間的形式呈現,讓使用者根據房間的特性進行表演。

效能最佳化:不要捨棄影格

要在單一程式碼集上執行多平台 VR 體驗,讓每個裝置或平台都能提供最佳效能,並非易事。

使用 VR 時,遇到最令人困擾的事物之一,就是影格速率無法跟上你的運動。如果你轉動頭部,但眼前的影像與內耳感受的動作不同,就會造成胃部不適。因此,我們必須避免任何影格速率延遲。以下是我們導入的一些最佳化方式。

1. 例項緩衝區幾何圖形

由於整個專案只使用少量 3D 物件,因此我們利用執行個體緩衝區幾何圖形,大幅提升了效能。基本上,您可以將物件上傳至 GPU 一次,並在單一繪圖呼叫中繪製任意數量的該物件「例項」。在 Dance Tonite 中,我們只有 3 個不同的物件 (圓錐、圓柱和有洞的房間),但這些物件可能有數百個副本。執行個體緩衝區幾何圖形是 ThreeJS 的一部分,但我們使用 Dusan Bosnjak 的實驗和開發中的分支實作 THREE.InstanceMesh,讓使用 Instanced Buffer Geometry 變得更簡單。

2. 避免垃圾收集器

與許多其他指令碼語言一樣,JavaScript 會找出哪些已分配的物件不再使用,然後自動釋出記憶體。這項程序稱為垃圾收集

開發人員無法控制這類情況何時發生。垃圾收集器隨時可能在門前出現,並開始清空垃圾,進而在利用最佳時間時捨棄影格。

解決方法是透過回收物件,盡可能減少垃圾產生。我們沒有為每項計算作業建立新的向量物件,而是標記了可重複使用的暫存物件。我們將參照移至範圍之外,因此保留這些參照,並未標示要移除。

舉例來說,以下程式碼可將使用者頭部和手的位置矩陣,轉換為儲存每個影格的位置/旋轉值陣列。藉由重複使用 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 個數字乘以每秒 90 格影格中的 3 個位置,因此產生極大的檔案,因此上傳和下載資料時會等待很久。我們只從轉換矩陣擷取位置和旋轉資料,因此得以將這些值從 16 降低至 7。

網站訪客經常點選連結,但不清楚會看到什麼內容,因此我們必須快速顯示視覺內容,否則他們會在幾秒內離開。

因此,我們希望確保專案能盡快開始玩遊戲。一開始,我們使用 JSON 格式載入移動資料。問題是,我們必須先載入完整的 JSON 檔案,才能剖析該檔案。不太進步。

為了讓 Dance Tonite 等專案以盡可能高的影格速率顯示,瀏覽器在每個影格中只會花費少許時間進行 JavaScript 計算。如果花費的時間太長,動畫就會開始卡頓。一開始,瀏覽器解碼這些大型 JSON 檔案時,我們會遇到卡頓情形。

我們發現一種方便的串流資料格式,稱為 NDJSON 或以換行符號分隔的 JSON。這裡的秘訣是建立一個檔案,其中包含一系列有效的 JSON 字串,每個字串都要一行。這樣您就可以在檔案載入時剖析,以便我們在檔案完全載入前顯示效能。

以下是其中一個錄音檔的部分內容:

{"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 個表演,因此需要將資料傳輸率降到比現有更低的程度。資料藝術團隊在虛擬藝術工作坊專案中也遇到了同樣的問題,他們會播放藝術家使用 Tilt Brush 在 VR 中繪畫的錄製內容。他們解決這個問題的方法,是製作使用者資料的中繼版本,並以較低的幀率和在回放時的幀間插補來處理。令人驚訝的是,我們很難發現以 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)。方向會以 四元數的形式儲存。

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

5. 與音樂同步動作

為了瞭解要播放哪個錄製動畫的畫面,我們需要知道音樂的目前時間,精確到毫秒。事實證明,雖然 HTML Audio 元素非常適合逐步載入及播放音訊,但它提供的時間屬性並不會與瀏覽器的框架迴圈同步變更。它總是會有些偏差,有時會提早幾毫秒,有時會晚幾毫秒。

這會導致精彩的舞蹈錄影出現斷斷續續的情形,而我們不希望發生這種情況。為解決這個問題,我們在 JavaScript 中實作了自己的計時器。如此一來,我們就能確定影格之間的變化時間就是自上一個影格以來經過的時間。每當計時器與音樂不同步超過 10 毫秒,我們就會再次同步。

6. 剔除和模糊處理

每個故事都需要一個好的結局,我們希望為完成體驗的使用者提供驚喜。離開最後一個房間後,你會進入一個有圓錐和圓柱的寧靜景觀。您可能會想:「這就是結局嗎?」當您進入球場時,音樂突然間會出現不同的圓錐體和圓柱體,跳舞到舞者上。你發現自己身處於一場盛大派對中!然後音樂突然停止,所有物品都掉落到地板上。

雖然觀眾會覺得很棒,但這會帶來一些效能障礙,需要解決。Room 可調整 Room 擴充的 VR 裝置及其高階遊戲裝置,以我們的新遊戲結局需要 40 種奇怪的額外效能展現極致效能。但某些行動裝置的影格速率甚至變成一半

為解決這個問題,我們引入了「霧」概念。經過特定距離後 所有減速都會變成黑色由於我們不需要計算或繪製不可見的內容,因此會剔除不可見房間中的效能,這可讓我們為 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 的使用者,尤其是他們可能是我們的最大觀眾群。我們不想製作假的 VR 體驗,而是想為螢幕裝置提供專屬的體驗。我們想以等角視角呈現從上方觀看的效能。在電腦遊戲中,這類視角的歷史相當悠久。它首次使用於 1982 年推出的太空射擊遊戲 Zaxxon。雖然 VR 使用者身處其中,但等角視角可提供如同上帝視角的動作畫面。我們選擇稍微向上擴充模型 以親身美觀

3. 陰影:偽造,直到你作出

我們發現,部分使用者在等角視角中難以看出深度。我很確定,Zaxxon 也是史上第一款在飛行物體下方投射動態陰影的電腦遊戲。

陰影

結果證明,在 3D 環境中製作陰影並不容易。尤其是手機等嚴格的裝置一開始,我們必須做出艱難的決定,將其排除在考量範圍之外,但在向 Three.js 的作者,也是經驗豐富的示範駭客 Mr doob 尋求建議後,他提出了一個新穎的想法:假造這些元素。

我們不必計算每個浮動物件會遮蔽光線的方式,進而擲回不同形狀的陰影,我們在每個物件下方繪製相同的圓形模糊紋理圖片。我們的視覺元素一開始無法模仿現實,所以只要稍做調整,就能輕易脫離現實世界。當物件靠近地面時,紋理就會變得較暗並縮小。當它們向上移動時,我們會讓紋理變得更透明且更大。

為了建立這些紋理,我們使用本紋理,並具有柔白至黑色的漸層 (在沒有 Alpha 透明度的情況下)。我們將材質設為透明,並使用減色混合。這樣一來,當兩者重疊時,就能完美地融合:

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 功能,也能從舞者的視角觀看事物。從這個角度來看,許多細節都會變得清晰可見。為了讓舞者能流暢地移動表演 舞者們會快速一覽彼此當球體進入房間時,你會看到他們緊張地看著球體的方向。雖然觀眾無法影響這些動作,但這確實能帶來令人驚豔的沉浸式體驗。我們再次強調,我們寧願採用這種做法,而非向使用者提供由滑鼠控制的平淡假 VR 版本。

5. 分享錄製內容

我們知道,在精心安排的 20 層表演者回應彼此反應的情況下,精心編排的精細影片片段會有多麼令人自豪。我們知道使用者可能會想向朋友展示。不過,這項壯舉的靜態圖片無法充分傳達資訊。我們希望使用者能夠分享自己的表演影片。其實,為什麼不使用 GIF 呢?我們的動畫採用平塗陰影,非常適合格式的有限調色盤。

分享錄音檔

我們轉向使用 GIF.js,這是一個 JavaScript 程式庫,可讓您在瀏覽器中對 GIF 動畫進行編碼。它會將頁框的編碼卸載至網路工作站,讓您能以個別程序在背景執行,從而充分運用多個並排運作的處理器。

遺憾的是,由於動畫需要的影格數量,編碼程序還是太慢。GIF 可使用有限的調色盤製作小檔案。我們發現大部分時間都花在找出每個像素的最接近顏色。我們透過駭客攻擊的方式,在這個過程中加入一個小捷徑,讓這個程序的效能提升十倍:如果像素的顏色與上一個像素相同,就使用調色盤中與上一個像素相同的顏色。

雖然現在的編碼速度很快,但產生的 GIF 檔案大小過大。GIF 格式可讓您定義處理方法,藉此指示每個影格在最後一個影格的顯示方式。為了縮減檔案大小,我們只會更新變更的像素,而非每個影格都更新每個像素。雖然再次減緩編碼程序,但確實能有效縮減檔案大小。

6. 穩固的基礎:Google Cloud 和 Firebase

「使用者自製內容」網站的後端通常複雜且不穩定,但我們利用 Google Cloud 和 Firebase 打造了簡單又強大的系統。當表演者將新舞蹈上傳至系統時,會由 Firebase 驗證以匿名方式進行驗證。使用者有權使用 Cloud Storage for Firebase 將錄製內容上傳至臨時空間。上傳完成後,用戶端機器會使用 Firebase 權杖呼叫 Cloud Functions for Firebase HTTP 觸發事件。這會觸發伺服器程序,用於驗證提交內容、建立資料庫記錄,以及將記錄移至 Google Cloud Storage 上的公開目錄。

堅實基礎

我們所有的公開內容都儲存在 Cloud Storage 值區中一系列的平面檔案中。這表示我們的資料可在全球快速存取,而且不必擔心高流量會影響資料可用性。

我們使用 Firebase 即時資料庫和 Cloud 函式端點建構了簡單的審核/收錄工具,方便我們在 VR 中觀察每個新提交的內容,並透過任何裝置發布新的播放清單。

7. Service Worker

Service worker 是近期推出的創新技術,可協助管理網站資產的快取作業。在本例中,服務工作人員迅速載入我們的內容,為回訪者快速載入,甚至讓網站離線運作。由於許多訪客會使用品質不一的行動網路連線,因此這些都是重要的功能。

由於有方便的 webpack 外掛程式可處理大部分繁重的工作,因此很容易在專案中加入服務工作者。在下方設定中,我們會產生服務工作者,自動快取所有靜態檔案。由於播放清單會持續更新,因此會從網路中提取最新的播放清單檔案 (如有)。如果可以,所有記錄 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',
        },
        },
    }],
    })
);

目前,外掛程式無法處理音樂檔案等漸進式載入的媒體資產,因此我們將這些檔案的 Cloud Storage Cache-Control 標頭設為 public, max-age=31536000,讓瀏覽器將檔案快取最多一年。

結論

我們很期待看到表演者如何運用這項功能,並將其當作工具,透過動作展現創意。我們已發布所有程式碼開放原始碼,詳情請參閱 https://github.com/puckey/dance-tonite。 在 VR 和 WebVR 的初期階段,我們期待看到這項新媒介將帶來哪些創意和意想不到的方向。Dance on