在 WebVR 中運用舞池

當 Google 資料藝術團隊與 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,開發人員可以透過 VRPose 物件存取使用者的頭部位置和方向。VR 硬體會更新每個影格,讓程式碼從正確的視角轉譯新影格。我們也可以透過 GamePad API 和 WebVR 使用 GamepadPose 物件,存取使用者控制器的位置/方向。我們只要儲存每個頁框的位置和方向值,就能為使用者的動作建立「記錄」。

2. 極簡主義與服裝

利用現今的房間縮放 VR 設備,我們可以追蹤使用者身體的三個點:頭部和兩隻手。在 Dance Tonite 中,我們的目標是專注於人類掌握這 3 個太空中的運動。為實現這個目標,我們盡可能地將美感推向最低,以便專注於運動。我們很喜歡在工作上運用人腦的想法。

這段影片示範瑞典心理學家 Gunnar Johansson 的故事,是盡可能減少內容減少的參考範例。它可以展示浮動白點在移動時如何立即辨識為身體。

在 Margarete Hastings 於 1970 年重現奧斯卡希勒姆三角帶 (Oskar Schlemmer) 三角帶 (Triadic Baallet) 的 1970 年重演過程記錄,我們從不同彩繪房間和幾何服裝的錄音中汲取靈感。

Schlemmer 選擇抽象幾何服裝的原因是限制了舞者的舞者的布偶和木偶移動,而「舞 Tonite」的相反目標

我們最終是根據選擇的形態決定旋轉所傳達的資訊量。無論旋轉方式為何,圓環的外觀都相同,但圓錐曲線實際上指向其所呈現的方向,且看起來與背面不同。

3. 環形腳踏板運動

我們希望展示大量舞蹈和運動的人。如今 VR 裝置沒有足夠數量的規模,因此推出線上直播也不可行。但我們還是想讓一群人透過運動回應彼此我們一開始考慮從 Norman McClaren 在 1964 年的影片《Canon》中採取遞迴性演出。

McClaren 的表演特色在於精心編排的一系列動作,會在每次循環結束後開始與其他人互動。就像音樂界的環狀自行車一樣,音樂家透過層層疊放各種現場音樂來演奏,我們想瞭解是否可以打造一個環境,讓使用者能自由即興演出的冷門版本。

4. 連通房

連通房

就像許多音樂一樣,LCD 音響系統的音樂是使用精準計時的測量方法建構而成。我們的專案主打《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 中拍攝使用者的動作,我們需要先將使用者的頭戴式裝置與控制器的順序和旋轉作業序列化,然後將這項資料上傳到我們的伺服器。我們開始擷取每個影格的完整轉換矩陣。這種做法的效能不錯,但當每個影格每秒 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 錄製的內插錄影差異來說,很難發現兩者間的差異。

如要親自瞭解,您可以強制 Dance Tonite 使用 ?dataRate= 查詢字串,以不同速率播放資料。您可以用這個方式比較所記錄的動作為每秒 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
    );

針對方向,我們會在主要畫面格之間進行球面線性內插 (穩定器) 值。方向會儲存為四元數

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

5. 正在將樂譜與音樂同步

為了知道錄製的動畫哪個影格要播放,我們需要知道音樂的目前時間到毫秒。儘管 HTML 音訊元素可以逐步載入及播放音效,但所提供的時間屬性並不會與瀏覽器的影格迴圈同步。有時難免會有點失常。有時部分 ms 會太早,有時可能會遲到。

這會導緻美麗的舞蹈錄音出現延遲,我們是希望所有成本都避免的。為解決這項問題,我們在 JavaScript 中導入了自己的計時器。這樣一來,我們就能確定影格之間的變化時間,就是自上一個影格以來經過的時間長度。只要計時器超過 10 毫秒與音樂同步,我們就會再次同步。

6. 起霧與霧

每個故事都需要良好的結局,而我們也希望能為使用者感到驚喜,讓他們在畢業前獲得無窮的體驗。當你離開最後一間房間時,就能進入 沉浸在寧靜圓錐和圓柱環境中的感覺。你想知道「這是結束嗎?」隨著您深入領域中,音樂的調性會突然造成不同的圓錐和圓柱組成,舞者的舞者才懂。你發現自己正處於一場大派對的中段了!等到音樂突然停止,一切就會落入地面。

這感覺就像觀眾一樣,但也造成效能上的困難。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 中觀看專案時,您可以從黃色或 B 的視角進行這項操作。當你走到不同房間時,舞者就會對你的表現產生反應。 這些人可以手勢輸入內容、在身後跳舞、在背後做出有趣的動作,並且迅速拉近與你的距離,這樣他們就不會碰到你。黃色的球體始終是 注意力的中心

這是因為在錄製表演時,黃色的球體在房間中央移動時會與音樂同步,並且循環播放。球體的位置可讓表演者瞭解自己目前在迴圈中停留的時間以及剩餘的時間。讓他們自然而然地從中打造效能。

2. 其他觀點

我們沒有希望在沒有 VR 功能的情況下就離開使用者,尤其是因為他們可能是我們的最大觀眾群。與其提供擬真的 VR 體驗,我們的目標是為螢幕裝置提供專屬的體驗。我們希望能從同異角度呈現上述效能這種觀點在電腦遊戲中擁有豐富的歷史。它首度在 1982 年的太空射擊遊戲 Zaxxon 中首次使用。VR 使用者所處的環境較厚,而同軸的視角則能為該動作提供類似上神的檢視畫面。因此選擇稍微擴充模型 藉此展現房子的美感

3. 陰影:假裝自己殺手

我們發現部分使用者在同個觀點中難以查看深度。我很確定因為這樣,Zaxxon 也是史上第一款電腦遊戲之一,在移動的物體下投射動態陰影。

陰影

但他們的 3D 陰影並不容易。特別是對於手機等 嚴格限制的裝置一開始,我們必須做出艱難的決定,而必須做出艱難的決定,但在詢問 Three.js 的作者以及經驗豐富的駭客示範駭客先生獲得建議後,他才提出了一種新想法。

我們不用計算每個浮動物件對光源的模糊程度,並擲回不同形狀的陰影,只要在每個浮動物件下方繪製相同的圓形模糊紋理圖片即可。我們一開始看起來不會想模仿現實,所以只要稍做調整,就能輕易解決。當物體接近地面時,紋理就會變暗並變小。而隨著位置的移動,紋理看起來更加透明且較大

我們使用這個紋理建立這類紋理,其具有柔白色到黑色的漸層 (沒有 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 外掛程式可以為您處理大部分的繁重作業,輕鬆將 Service Worker 新增至專案。在下列設定中,我們產生了一個 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',
        },
        },
    }],
    })
);

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

結語

我們很期待看到成效者可如何融入這項體驗,並運用它做為運用動態效果揮灑創意的工具。我們已發布所有開放原始碼程式碼,您可以在 https://github.com/puckey/dance-tonite 找到。在 VR 發展初期,尤其是 WebVR 的這幾天,我們很期待看到這種嶄新媒介,會出現什麼新的創意,以及出乎意料的方向。 深入探索