WebVR 第 5 部分:設計與實現
已發表: 2022-03-11我喜歡讓項目“完成”。 我們已經到了旅程的終點——我們在 WebVR 中的天體引力模擬的誕生。
在最後一篇文章中,我們將把我們的高性能模擬代碼(第 1、2、3 條)插入到基於畫布可視化器(第 4 條)的 WebVR 可視化器中。
- “n體問題”介紹和架構
- Web Workers 為我們提供了額外的瀏覽器線程
- WebAssembly 和 AssemblyScript 用於我們的 O(n²) 性能瓶頸代碼
- 畫布數據可視化
- WebVR 數據可視化
這是一篇較長的文章,因此我們將跳過之前介紹的一些技術細節。 如果您想要一個方向,請查看以前的帖子,或者危險地繼續閱讀。
我們一直在探索瀏覽器從單線程 JavaScript 運行時到多線程(網絡工作者)高性能運行時(WebAssembly)的範式轉變。 這些高性能桌面計算功能在漸進式 Web 應用程序和 SaaS 分發模型中可用。
VR 將創建引人注目的、無干擾的銷售和營銷環境,以進行溝通、說服和衡量參與度(眼動追踪和互動)。 數據仍將是 0 和 1,但預期的執行摘要和消費者體驗將是 WebVR——就像我們今天為平面網絡構建移動儀表板體驗一樣。
這些技術還支持分佈式瀏覽器邊緣計算。 例如,我們可以創建一個基於 Web 的應用程序來運行我們的 WebAssembly 計算,以模擬數百萬顆恆星。 另一個例子是一個動畫應用程序,它在您編輯自己的作品時呈現其他用戶的作品。
娛樂內容正在引領虛擬現實的發展,就像在移動設備上引領娛樂一樣。 然而,一旦 VR 成為常態(就像今天的移動優先設計),它將是預期的體驗(VR 優先設計)。 對於設計師和開發人員來說,這是一個非常激動人心的時刻——而 VR 是一種完全不同的設計範式。
如果你不能抓握,你就不是 VR 設計師。 這是一個大膽的聲明,今天是對 VR 設計的深入研究。 當您閱讀本文時,該領域正在被發明。 我的目的是分享我在軟件和電影方面的經驗,以啟動“VR 優先設計”的對話。 我們都互相學習。
考慮到這些宏大的預測,我想將這個項目作為一個專業的技術演示來完成——WebVR 是一個很好的選擇!
WebVR 和 Google A-Frame
WebVR git repo 是畫布版本的一個分支,原因有兩個。 它使在 Github 頁面上託管項目變得更加容易,並且 WebVR 需要進行一些更改,這會使畫布版本和這些文章變得混亂。
如果您還記得我們關於架構的第一篇文章,我們將整個模擬委託給nBodySimulator
。
網絡工作者帖子顯示nBodySimulator
有一個step()
函數,每 33 毫秒模擬一次調用一次。 step()
調用calculateForces()
來運行我們的 O(n²) WebAssembly 模擬代碼(文章 3),然後更新位置並重新繪製。 在我們之前創建畫布可視化的文章中,我們使用畫布元素實現了這一點,從這個基類開始:
/** * Base class that console.log()s the simulation state. */ export class nBodyVisualizer { constructor(htmlElement) { this.htmlElement = htmlElement this.resize() this.scaleSize = 25 // divided into bodies drawSize. drawSize is log10(mass) // This could be refactored to the child class. // Art is never finished. It must be abandoned. } resize() {} paint(bodies) { console.log(JSON.stringify(bodies, null, 2)) } }
定義集成挑戰
我們有模擬。 現在,我們希望與 WebVR 集成 - 無需重新構建我們的項目。 無論我們對模擬所做的任何調整,每 33 毫秒都會在函數paint(bodies)
的主 UI 線程中發生。
這就是我們衡量“完成”的方式。 我很興奮——讓我們開始工作吧!
如何製作虛擬現實
首先,我們需要一個設計:
- VR是由什麼製成的?
- WebVR設計是如何表達的?
- 我們如何與之互動?
虛擬現實可以追溯到時間的黎明。 每一個篝火故事都是一個被瑣碎細節掩蓋的古怪誇張的微小虛擬世界。
通過添加 3D 立體視覺和音頻,我們可以將我們的篝火故事放大 10 倍。 我的電影製作預算指導老師曾經說過, “我們只為海報付費。 我們不是在構建現實。”
如果您熟悉瀏覽器 DOM,您就會知道它創建了一個樹狀的層次結構。
網絡設計中隱含的是觀眾從“正面”觀看。 從側面看會將 DOM 元素顯示為線條,而從背面看,我們只會看到<body>
標籤,因為它會遮擋其子元素。
VR 沉浸式體驗的一部分是讓用戶控制他們的觀點、風格、節奏和交互順序。 他們不必特別注意任何事情。 如果您以編程方式移動或旋轉相機,它們實際上會因 VR 病而嘔吐。
請注意,VR 疾病可不是開玩笑的。 我們的眼睛和內耳都能檢測到運動。 這對於直立行走的動物來說非常重要。 當這些運動傳感器不同意時,我們的大腦自然會認為我們的嘴又在胡說八道並嘔吐。 我們都曾經是孩子。 已經有很多關於 VR 中這種生存本能的文章。 “Epic Fun”標題在 Steam 上是免費的,而過山車是我發現的最好的 VR 疾病演示。
虛擬現實被表示為“場景圖”。 場景圖與 DOM 具有相同的樹狀模式,以隱藏令人信服的 3D 環境的細節和復雜性。 但是,我們不是滾動和路由,而是將查看器定位在他們想要將體驗拉向他們的位置。
這是來自 Google 的 A-Frame WebVR 框架的 Hello World 場景圖:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello, WebVR! • A-Frame</title> <meta name="description" content="Hello, WebVR! • A-Frame"> <script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script> </head> <body> <a-scene background="color: #FAFAFA"> <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow></a-box> <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere> <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder> <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow></a-plane> </a-scene> </body> </html>
這個 HTML 文檔在瀏覽器中創建了一個 DOM。 <a-*>
標籤是 A-Frame 框架的一部分,而<a-scene>
是場景圖的根。 在這裡,我們看到場景中顯示了四個 3D 圖元。
首先,請注意我們正在從平面網絡瀏覽器查看場景。 右下角的小面具邀請用戶切換到 3D 立體模式。
理論上,您應該能夠:
- 在你的手機上打開這個
- 將手機舉到臉前
- 享受新現實的輝煌!
如果沒有 VR 耳機的精美鏡頭,我從來沒有讓它工作。 你可以以便宜的價格買到適用於 Android 手機的 VR 耳機(基於 Google Cardboard 的基本設備),但是,對於開發內容,我建議使用獨立的 HMD(頭戴式顯示器),例如 Oculus Quest。
就像水肺潛水或跳傘一樣,虛擬現實是一項齒輪運動。
VR設計師學習“懸崖”
請注意,A-Frame Hello World 場景具有默認照明和攝像頭:
- 立方體的面是不同的顏色 - 立方體是自陰影的。
- 立方體在平面上投下陰影 - 有一個定向光。
- 立方體和平面之間沒有縫隙——這是一個有重力的世界。
這些是對觀眾說的關鍵線索, “放鬆,你臉上的這個東西是完全正常的。”
另請注意,此默認設置隱含在上面的 Hello World 場景代碼中。 A-Frame 明智地提供了一個合理的默認設置,但請注意——相機和照明是平面網頁設計師創建 VR 必須跨越的鴻溝。
我們認為默認的照明設置是理所當然的。 例如,按鈕:
請注意這種隱式照明在設計和攝影中的普遍性。 即使是“扁平化設計”按鈕也無法擺脫網絡的默認照明——它會向下和向右投下陰影。
設計、溝通和實施照明和相機設置是 WebVR 設計師的學習懸崖。 “電影語言”是文化規範的集合——以不同的攝影機和燈光設置表示——將故事情感傳達給觀眾。 設計/移動場景周圍的燈光和相機的電影專業人士是抓地力部門。
回到我們的虛擬現實
現在,讓我們重新開始工作。 我們的天體 WebVR 場景也有類似的模式:
<!DOCTYPE> <html> <head> <script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/aframe-event-set-component.min.js"></script> <script src="main.js"></script> </head> <body> <a-scene> <a-sky color="#222"></a-sky> <a-entity geometry="primitive: circle; radius: 12" position="0 0 -.5" material="color: #333; transparent: true; opacity: 0.5"> <a-sphere color="black" radius=."02"></a-sphere> </a-entity> <a-entity></a-entity> <a-entity geometry="primitive: plane; width: 2; height: auto" position="0 -10 .3" rotation="55 0 0" material="color: blue" text="value: Welcome Astronaut!..."> </a-entity> <a-entity position="0 -12 .7" rotation="55 0 0"> <a-camera> <a-cursor color="#4CC3D9" fuse="true" timeout="1"></a-cursor> </a-camera> </a-entity> </a-scene> </body> </html>
此 HTML 文檔加載 A-Frame 框架和交互插件。 我們的場景從<a-scene>
開始。
在內部,我們從<a-sky color="#222"></a-sky>
元素開始為場景中未定義的所有內容設置背景色。
接下來,我們創建一個“軌道平面”,讓觀眾在他們飛過我們陌生而未知的世界時“抓住”他們。 我們將其創建為一個圓盤和一個位於 (0,0,0) 處的黑色小球。 沒有這個,轉身對我來說“毫無根據”:
<a-entity geometry="primitive: circle; radius: 12" position="0 0 -.5" material="color: #333; transparent: true; opacity: 0.5"> <a-sphere color="black" radius=."02"></a-sphere> </a-entity>
接下來,我們定義一個集合,我們可以在其中添加/刪除/重新定位 A-Frame 實體。
<a-entity></a-entity>
這是nBodyVisualizer
的paint(bodies)
完成其工作的空地。
然後,我們創建了觀眾與這個世界之間的關係。 作為一個技術演示,這個世界的目的是讓觀眾探索 WebVR 和支持它的瀏覽器技術。 一個簡單的“宇航員”敘事創造了一種遊戲感,而這個星標是導航的另一個參考點。
<a-entity geometry="primitive: plane; width: 2; height: auto" position="0 -10 .3" rotation="55 0 0" material="color: blue" text="value: Welcome Astronaut!\n ..."> </a-entity>
這完成了我們的場景圖。 最後,我希望在電話演示中用戶和這個旋轉世界之間進行某種交互。 我們如何在 VR 中重新創建“Throw Debris”按鈕?
按鈕是所有現代設計的基本元素——VR 按鈕在哪裡?
WebVR 中的交互
虛擬現實有自己的“上方”和“下方”。 觀眾的第一次互動是通過他們的頭像或相機。 這是縮放的所有控件。
如果您在桌面上閱讀此內容,您可以使用 WASD 移動和鼠標旋轉相機。 這種探索揭示了信息,但不表達你的意願。
現實現實有幾個非常重要的特性,這些特性在網絡上並不常見:
- 透視- 當物體遠離我們時,它們會明顯變小。
- 遮擋- 根據位置隱藏和顯示對象。
VR 模擬這些功能以創建 3D 效果。 它們還可以在 VR 中用於顯示信息和界面 - 並在呈現交互之前設置情緒。 我發現大多數人在繼續前進之前只需要一分鐘來享受這種體驗。
在 WebVR 中,我們在 3D 空間中進行交互。 為此,我們有兩個基本工具:
- 碰撞- 當兩個對象共享相同空間時觸發的被動 3D 事件。
- 投影- 一個活動的 2D 函數調用,列出了與一條線相交的所有對象。
碰撞是最“VR-like”的交互
在 VR 中,“碰撞”正是它聽起來的樣子:當兩個對象共享同一個空間時,A-Frame 會創建一個事件。
為了讓用戶“按下”一個按鈕,我們必須給他們一個棋子和一些可以用來按下按鈕的東西。

不幸的是,WebVR 還不能假設控制器——許多人會在他們的桌面或手機上查看平面網絡版本,並且許多人會使用像 Google Cardboard 或三星的 Gear VR 這樣的耳機來顯示立體版本。
如果用戶沒有控制器,他們就無法伸手“觸摸”事物,因此任何碰撞都必須與他們的“個人空間”發生衝突。
我們可以給玩家一個宇航員形狀的棋子來四處移動,但是強迫用戶進入一個旋轉的行星瘴氣似乎有點令人反感,並且與我們設計的寬敞性背道而馳。
投影是 3D 空間中的 2D“類 Web”點擊
除了“碰撞”,我們還可以使用“投影”。 我們可以在我們的場景中投射一條線,看看它接觸到了什麼。 最常見的例子是“傳送射線”。
傳送光線追踪世界中的一條線,以顯示玩家可以移動的位置。 這個“投影”尋找著陸的地方。 它返回投影路徑中的一個或多個對象。 這是一個傳送射線示例:
請注意,射線實際上是作為指向下方的拋物線實現的。 這意味著它像拋擲物體一樣自然地與“地面”相交。 這也自然地設置了最大傳送距離。 限制是 VR 中最重要的設計選擇。 幸運的是,現實有許多自然限制。
投影將 3D 世界“扁平化”為 2D,因此您可以像鼠標一樣指向東西來點擊它。 第一人稱射擊遊戲是精心製作的“2D 點擊”遊戲,在精美的令人沮喪的按鈕上進行 - 通常有一個精心製作的故事來解釋為什麼那些該死的按鈕不能“點擊”你回來。
VR 中有這麼多槍,因為槍已經被完善為準確可靠的 3D 鼠標——而點擊是消費者無需學習就知道如何做的事情。
投影還提供了與場景關係中距離的安全性。 請記住,在 VR 中更接近某些事物自然會遮擋所有其他可能尚未揭示其重要性的事物。
使用“凝視”進行無控制器投影
為了在沒有控制器的 WebVR 中創建這種交互原語,我們可以將觀看者的“凝視”投影為視線“光標”。 該光標可以通過編程方式使用“保險絲”與對象進行交互。 這作為一個藍色的小圓圈傳達給觀眾。 現在我們點擊!
如果您記得篝火故事,謊言越大,出售它所需的細節就越少。 一個明顯而荒謬的“凝視”互動是凝視太陽。 我們使用這種“凝視”來觸發將新的“碎片”行星添加到我們的模擬中。 從來沒有觀眾質疑過這種選擇——虛擬現實在荒謬的時候非常迷人。
在 A-Frame 中,我們將相機(玩家的隱形棋子)和這條視線“光標”表示為我們的相機索具。 將<a-cursor>
放在<a-camera>
內會導致相機的變換也應用於光標。 當玩家移動/旋轉他們的棋子( a-camera
)時,它也會移動/旋轉他們的視線( a-cursor
)。
// src/index.html <a-entity position="0 -12 .7" rotation="55 0 0"> <a-camera> <a-cursor color="#4CC3D9" fuse="true" timeout="1"></a-cursor> </a-camera> </a-entity>
光標的“熔斷器”會等到一整秒的“凝視”時間過後才會發出事件。
我使用了默認照明,因此您可能會注意到太陽的“背面”沒有被照亮。 雖然我沒有離開過軌道平面,但我不認為太陽是這樣工作的。 但是,它適用於我們的現實技術演示海報。
另一種選擇是將照明放在相機元素內,因此它會隨著用戶移動。 這將創造一種更親密——也可能是令人毛骨悚然的——小行星礦工體驗。 這些是有趣的設計選擇。
我們有一個整合計劃
有了這個,我們現在有了 A-Frame <a-scene>
和我們的 JavaScript 模擬之間的集成點:
A-Frame <a-scene>
:
實體的命名集合:
<a-entity></a-entity>
將發出投影事件的光標:
<a-cursor color="#4CC3D9" fuse="true" timeout="1"></a-cursor>
我們的 JavaScript 模擬:
nBodyVisWebVR.paint(bodies)
- 從模擬主體中添加/刪除/重新定位 VR 實體addBodyArgs(name, color, x, y, z, mass, vX, vY, vZ)
將新的碎片體添加到模擬中
index.html
加載main.js
,它像畫布版本一樣初始化我們的模擬:
// src/main.js import { nBodyVisualizer, nBodyVisWebVR } from ."/nBodyVisualizer" import { Body, nBodySimulator } from ."/nBodySimulator" window.onload = function() { // Create a Simulation const sim = new nBodySimulator() // this Visualizer manages the UI sim.addVisualization(new nBodyVisWebVR(document.getElementById("a-bodies"), sim)) // making up stable universes is hard // name color xyzm vz vy vz sim.addBody(new Body("star", "yellow", 0, 0, 1, 1e9)) sim.addBody(new Body("hot-jupiter", "red", -1, -1, 1, 1e4, .24, -0.05, 0)) sim.addBody(new Body("cold-jupiter", "purple", 4, 4, .5, 1e4, -.07, 0.04, 0)) // Start simulation sim.start() // Add another sim.addBody(new Body("saturn", "blue", -8, -8, .1, 1e3, .07, -.035, 0)) }
您會注意到這裡我們將可視化工具的htmlElement
設置為a-bodies
集合來保存實體。
從 JavaScript 以編程方式管理 A-Frame 對象
在index.html
中聲明了我們的場景後,我們現在可以編寫可視化器了。
首先,我們設置nBodyVisualizer
從nBodySimulation
列表中讀取並在<a-entity></a-entity>
集合中創建/更新/刪除 A-Frame 對象。
// src/nBodyVisualizer.js /** * This is the WebVR visualizer. * It's responsible for painting and setting up the entire scene. */ export class nBodyVisWebVR extends nBodyVisualizer { constructor(htmlElement, sim) { // HTML Element is a-collection#a-bodies. super(htmlElement) // We add these to the global namespace because // this isn't the core problem we are trying to solve. window.sim = sim this.nextId = 0 } resize() {}
在構造函數中,我們保存我們的 A-Frame 集合,為我們的凝視事件設置一個全局變量以查找模擬,並初始化一個 id 計數器,我們將使用它來匹配我們的模擬和 A-Frame 場景之間的主體。
paint(bodies) { let i // Create lookup table: lookup[body.aframeId] = body const lookup = bodies.reduce( (total, body) => { // If new body, give it an aframeId if (!body.aframeId) body.aframeId = `a-sim-body-${body.name}-${this.nextId++}` total[body.aframeId] = body return total }, {}) // Loop through existing a-sim-bodies and remove any that are not in // the lookup - this is our dropped debris const aSimBodies = document.querySelectorAll(."a-sim-body") for (i = 0; i < aSimBodies.length; i++) { if (!lookup[aSimBodies[i].id]) { // if we don't find the scene's a-body in the lookup table of Body()s, // remove the a-body from the scene aSimBodies[i].parentNode.removeChild(aSimBodies[i]); } } // loop through sim bodies and upsert let aBody bodies.forEach( body => { // Find the html element for this aframeId aBody = document.getElementById(body.aframeId) // If html element not found, make one. if (!aBody) { this.htmlElement.innerHTML += ` <a-sphere class="a-sim-body" dynamic-body ${ (body.name === "star") ? "debris-listener event-set__enter='_event: mouseenter; color: green' event-set__leave='_event: mouseleave; color: yellow'" : ""} position="0 0 0" radius="${body.drawSize/this.scaleSize}" color="${body.color}"> </a-sphere>` aBody = document.getElementById(body.aframeId) } // reposition aBody.object3D.position.set(body.x, body.y, body.z) }) }
首先,我們循環遍歷模擬主體以標記和/或創建查找表,以將 A-Frame 實體與模擬主體匹配。
接下來,我們循環遍歷現有的 A-Frame 主體,並刪除任何被模擬修剪的超出邊界的物體。 這增加了體驗的感知性能。
最後,我們循環遍歷 sim 主體,為丟失的主體創建一個新<a-sphere>
,並使用aBody.object3D.position.set(body.x, body.y, body.z)
重新定位其他主體
我們可以使用標準 DOM 函數以編程方式更改 A-Frame 場景中的元素。 為了向場景中添加元素,我們將一個字符串附加到容器的 innerHTML 中。 這段代碼對我來說味道很奇怪,但它可以工作,而且我沒有找到更好的東西。
您會注意到,當我們創建要附加的字符串時,我們在“star”附近有一個三元運算符來設置屬性。
<a-sphere class="a-sim-body" dynamic-body ${ (body.name === "star") ? "debris-listener event-set__enter='_event: mouseenter; color: green' event-set__leave='_event: mouseleave; color: yellow'" : ""} position="0 0 0" radius="${body.drawSize/this.scaleSize}" color="${body.color}"> </a-sphere>`
如果主體是“星”,我們添加一些額外的屬性來描述它的事件。 下面是我們的星星掛載在 DOM 中時的樣子:
<a-sphere class="a-sim-body" dynamic-body="" debris-listener="" event-set__enter="_event: mouseenter; color: green" event-set__leave="_event: mouseleave; color: yellow" position="0 0 0" radius="0.36" color="yellow" material="" geometry=""></a-sphere>
三個屬性,fragments debris-listener
, event-set__enter
和event-set__leave
,建立了我們的交互,是我們整合的最後一圈。
定義 A-Frame 事件和交互
我們在實體的屬性中使用 NPM 包“aframe-event-set-component”來改變觀察者“看”太陽時的顏色。
這種“凝視”是觀察者位置和旋轉的投影,交互提供了他們的凝視正在做某事的必要反饋。
我們的星球現在有兩個由插件啟用的速記事件event-set__enter
和event-set__leave
:
<a-sphere ... event-set__enter="_event: mouseenter; color: green" event-set__leave="_event: mouseleave; color: yellow" … ></a-sphere>
接下來,我們用我們將作為自定義 A-Frame 組件實現的debris-listener
來裝飾我們的星球。
<a-sphere ... debris-listener="" … ></a-sphere>
A-Frame 組件在全局級別定義:
// src/nBodyVisualizer.js // Component to add new bodies when the user stares at the sun. See HTML AFRAME.registerComponent('debris-listener', { init: function () { // Helper function function rando(scale) { return (Math.random()-.5) * scale } // Add 10 new bodies this.el.addEventListener('click', function (evt) { for (let x=0; x<10; x++) { // name, color, x, y, z, mass, vx, vy, vz window.sim.addBodyArgs("debris", "white", rando(10), rando(10), rando(10), 1, rando(.1), rando(.1), rando(.1)) } }) } })
這個 A-Frame 組件就像一個“點擊”監聽器,可以由凝視光標觸發,向我們的場景添加 10 個新的隨機物體。
總結一下:
- 我們用標準 HTML 中的 A-Frame 聲明 WebVR 場景。
- 我們可以通過 JavaScript 以編程方式在場景中添加/刪除/更新 A-Frame 實體。
- 我們可以通過 A-Frame 插件和組件在 JavaScript 中創建與事件處理程序的交互。
WebVR:Veni、Vidi、Vici
我希望你能像我一樣從這個技術演示中得到盡可能多的東西。 我們將這些功能(網絡工作者和 WebAssembly)應用於 WebVR 的地方,它們也可以應用於瀏覽器邊緣計算。
一個巨大的技術浪潮已經到來——虛擬現實(VR)。 無論您第一次拿著智能手機時的感受如何,第一次體驗 VR 都會在計算的各個方面提供 10 倍的情感體驗。 距離第一部 iPhone 僅 12 年。
VR 存在的時間要長得多,但將 VR 帶給普通用戶所需的技術是通過移動革命和 Facebook 的 Oculus Quest 實現的,而不是 PC 革命。
互聯網和開源是人類世界上最偉大的奇蹟之一。 致所有創建平面互聯網的人——我為你們的勇氣和冒險精神乾杯。
宣言! 我們將建造世界,因為我們擁有創造的力量。