WebVR 第 4 部分:畫布數據可視化

已發表: 2022-03-11

歡呼! 我們著手為 WebVR 創建概念證明。 我們之前的博客文章完成了模擬,所以現在是時候進行一些創造性的遊戲了。

對於設計師和開發人員來說,這是一個令人難以置信的激動人心的時刻,因為 VR 是一種範式轉變。

2007 年,蘋果推出第一款 iPhone,開啟了智能手機消費革命。 到 2012 年,我們已經進入了“移動優先”和“響應式”的網頁設計。 2019 年,Facebook 和 Oculus 發布了第一款移動 VR 耳機。 我們開工吧!

“移動優先”的互聯網並不是一種時尚,我預測“VR 優先”的互聯網也不會成為時尚。 在之前的三篇文章和演示中,我展示了您當前瀏覽器中的技術可能性。

如果您在本系列的中間看到這個,我們正在構建一個旋轉行星的天體引力模擬。

  • 第 1 部分:介紹和架構
  • 第 2 部分: Web Workers 為我們提供額外的瀏覽器線程
  • 第 3 部分:我們的 O(n²) 性能瓶頸代碼的 WebAssembly 和 AssemblyScript

站在我們已經完成的工作上,是時候進行一些創造性的遊戲了。 在最後兩篇文章中,我們將探討畫布和 WebVR 以及用戶體驗。

  • 第 4 部分:畫布數據可視化(這篇文章)
  • 第 5 部分: WebVR 數據可視化

今天,我們將把我們的模擬變為現實。 回首往事,我注意到一旦我開始使用可視化工具,我對完成這個項目更加興奮和感興趣。 可視化讓其他人感興趣。

本次模擬的目的是探索支持 WebVR(瀏覽器中的虛擬現實)和即將到來的VR 優先網絡的技術。 這些相同的技術可以為瀏覽器邊緣計算提供動力。

完善我們的概念證明,今天我們將首先創建一個畫布可視化。

畫布可視化
Canvas Visualizer 演示,示例代碼

在最後一篇文章中,我們將著眼於 VR 設計並製作 WebVR 版本以“完成”這個項目。

WebVR 數據可視化

可能工作的最簡單的事情: console.log()

回到 RR(現實現實)。 讓我們為基於瀏覽器的“n-body”模擬創建一些可視化。 我在過去的項目中在網絡視頻應用程序中使用過畫布,但從未將其用作藝術家的畫布。 讓我們看看我們能做些什麼。

如果您還記得我們的項目架構,我們將可視化委託給nBodyVisualizer.js

將可視化委託給 nBodyVisualizer.js

nBodySimulator.js有一個模擬循環start()調用它的step()函數, step()底部調用this.visualize()

 // src/nBodySimulator.js /** * This is the simulation loop. */ async step() { // Skip calculation if worker not ready. Runs every 33ms (30fps). Will skip. if (this.ready()) { await this.calculateForces() } else { console.log(`Skipping calculation: ${this.workerReady} ${this.workerCalculating}`) } // Remove any "debris" that has traveled out of bounds // This keeps the button from creating uninteresting work. this.trimDebris() // Now Update forces. Reuse old forces if worker is already busy calculating. this.applyForces() // Now Visualize this.visualize() }

當我們按下綠色按鈕時,主線程將 10 個隨機體添加到系統中。 我們在第一篇文章中觸摸了按鈕代碼,您可以在此處的 repo 中看到它。 這些機構非常適合測試概念驗證,但請記住,我們處於危險的性能領域 - O(n²)。

人類被設計為關心他們可以看到的人和事物,因此trimDebris()刪除了飛出視線的物體,這樣它們就不會減慢其他物體的速度。 這是感知性能和實際性能之間的差異。

現在我們已經涵蓋了除了最後的this.visualize()的所有內容,讓我們來看看吧!

 // src/nBodySimulator.js /** * Loop through our visualizers and paint() */ visualize() { this.visualizations.forEach(vis => { vis.paint(this.objBodies) }) } /** * Add a visualizer to our list */ addVisualization(vis) { this.visualizations.push(vis) }

這兩個功能讓我們可以添加多個可視化工具。 畫布版本中有兩個可視化工具:

 // src/main.js window.onload = function() { // Create a Simulation const sim = new nBodySimulator() // Add some visualizers sim.addVisualization( new nBodyVisPrettyPrint(document.getElementById("visPrettyPrint")) ) sim.addVisualization( new nBodyVisCanvas(document.getElementById("visCanvas")) ) …

在畫布版本中,第一個可視化工具是顯示為 HTML 的白色數字表。 第二個可視化器是下面的黑色畫布元素。

畫布可視化器
在左側,HTML 可視化工具是白色數字表。 黑色畫布可視化器位於下方

為了創建它,我從nBodyVisualizer.js中的一個簡單基類開始:

 // src/nBodyVisualizer.js /** * This is a toolkit of visualizers for our simulation. */ /** * Base class that console.log()s the simulation state. */ export class nBodyVisualizer { constructor(htmlElement) { this.htmlElement = htmlElement this.resize() } resize() {} paint(bodies) { console.log(JSON.stringify(bodies, null, 2)) } }

這個類打印到控制台(每 33 毫秒!)並且還跟踪一個 htmlElement - 我們將在子類中使用它以使它們易於在main.js中聲明。

這是可能工作的最簡單的事情。

然而,雖然這個console可視化絕對是簡單的,但它實際上並沒有“工作”。 瀏覽器控制台(和瀏覽人)並非旨在以 33 毫秒的速度處理日誌消息。 讓我們找到下一個可能起作用的最簡單的東西

使用數據可視化模擬

下一個“漂亮打印”迭代是將文本打印到 HTML 元素。 這也是我們用於畫布實現的模式。

請注意,我們正在保存對可視化工具將在其上繪製的htmlElement的引用。 與網絡上的所有其他內容一樣,它具有移動優先的設計。 在桌面上,這會在頁面左側打印對象的數據表及其坐標。 在移動設備上,它會導致視覺混亂,所以我們跳過它。

 /** * Pretty print simulation to an htmlElement's innerHTML */ export class nBodyVisPrettyPrint extends nBodyVisualizer { constructor(htmlElement) { super(htmlElement) this.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); } resize() {} paint(bodies) { if (this.isMobile) return let text = '' function pretty(number) { return number.toPrecision(2).padStart(10) } bodies.forEach( body => { text += `<br>${body.name.padStart(12)} { x:${pretty(body.x)} y:${pretty(body.y)} z:${pretty(body.z)} mass:${pretty(body.mass)}) }` }) if (this.htmlElement) this.htmlElement.innerHTML = text } }

這個“數據流”可視化工具有兩個功能:

  1. 這是一種“健全檢查”模擬輸入可視化器的方法。 這是一個“調試”窗口。
  2. 看起來很酷,所以讓我們保留它用於桌面演示!

現在我們對我們的輸入相當有信心,讓我們來談談圖形和畫布。

使用 2D 畫布可視化模擬

“遊戲引擎”是帶有爆炸的“模擬引擎”。 兩者都是極其複雜的工具,因為它們專注於資產管道、流式關卡加載以及各種不應該被注意到的令人難以置信的無聊東西。

網絡還通過“移動優先”設計創造了自己的“永遠不應該被注意到的東西”。 如果瀏覽器調整大小,我們的畫布的 CSS 將調整 DOM 中的畫布元素的大小,因此我們的可視化器必須適應或遭受用戶的蔑視。

 #visCanvas { margin: 0; padding: 0; background-color: #1F1F1F; overflow: hidden; width: 100vw; height: 100vh; }

此要求驅動nBodyVisualizer基類和畫布實現中的resize()

 /** * Draw simulation state to canvas */ export class nBodyVisCanvas extends nBodyVisualizer { constructor(htmlElement) { super(htmlElement) // Listen for resize to scale our simulation window.onresize = this.resize.bind(this) } // If the window is resized, we need to resize our visualization resize() { if (!this.htmlElement) return this.sizeX = this.htmlElement.offsetWidth this.sizeY = this.htmlElement.offsetHeight this.htmlElement.width = this.sizeX this.htmlElement.height = this.sizeY this.vis = this.htmlElement.getContext('2d') }

這導致我們的可視化器具有三個基本屬性:

  • this.vis - 可用於繪製圖元
  • this.sizeX
  • this.sizeY - 繪圖區域的尺寸

Canvas 2D 可視化設計筆記

我們的調整大小適用默認畫布實現。 如果我們正在可視化產品或數據圖,我們希望:

  1. 繪製到畫布(以首選大小和縱橫比)
  2. 然後讓瀏覽器在頁面佈局期間將該繪圖調整為 DOM 元素

在這個更常見的用例中,產品或圖表是體驗的焦點。

相反,我們的可視化是對廣闊空間的戲劇化可視化,通過將數十個微小的世界拋入虛空來獲得樂趣,從而將其戲劇化。

我們的天體通過謙虛展示了這個空間——將它們自己保持在 0 到 20 像素之間。 這種調整大小會縮放點之間的空間,以創造一種“科學”的寬敞感,並提高感知速度。

為了在具有巨大不同質量的對象之間創建一種比例感,我們使用與質量成比例的drawSize來初始化物體:

 // nBodySimulation.js export class Body { constructor(name, color, x, y, z, mass, vX, vY, vZ) { ... this.drawSize = Math.min( Math.max( Math.log10(mass), 1), 10) } }

手工定制太陽能係統

現在,當我們在main.js中創建太陽係時,我們將擁有可視化所需的所有工具:

 // Set Z coords to 1 for best visualization in overhead 2D canvas // Making up stable universes is hard // name color x y z m vz vy vz sim.addBody(new Body("star", "yellow", 0, 0, 0, 1e9)) sim.addBody(new Body("hot jupiter", "red", -1, -1, 0, 1e4, .24, -0.05, 0)) sim.addBody(new Body("cold jupiter", "purple", 4, 4, -.1, 1e4, -.07, 0.04, 0)) // A couple far-out asteroids to pin the canvas visualization in place. sim.addBody(new Body("asteroid", "black", -15, -15, 0, 0)) sim.addBody(new Body("asteroid", "black", 15, 15, 0, 0)) // Start simulation sim.start()

您可能會注意到底部的兩個“小行星”。 這些零質量對像是一種黑客,用於將模擬的最小視口“固定”到以 0,0 為中心的 30x30 區域。

我們現在已經準備好我們的繪畫功能了。 物體雲可以“擺動”遠離原點 (0,0,0),因此除了比例之外,我們還必須移動。

當模擬對它有一種自然的感覺時,我們就“完成”了。 沒有“正確”的方法可以做到這一點。 為了安排最初的行星位置,我只是擺弄了數字,直到它保持足夠長的時間變得有趣。

 // Paint on the canvas paint(bodies) { if (!this.htmlElement) return // We need to convert our 3d float universe to a 2d pixel visualization // calculate shift and scale const bounds = this.bounds(bodies) const shiftX = bounds.xMin const shiftY = bounds.yMin const twoPie = 2 * Math.PI let scaleX = this.sizeX / (bounds.xMax - bounds.xMin) let scaleY = this.sizeY / (bounds.yMax - bounds.yMin) if (isNaN(scaleX) || !isFinite(scaleX) || scaleX < 15) scaleX = 15 if (isNaN(scaleY) || !isFinite(scaleY) || scaleY < 15) scaleY = 15 // Begin Draw this.vis.clearRect(0, 0, this.vis.canvas.width, this.vis.canvas.height) bodies.forEach((body, index) => { // Center const drawX = (body.x - shiftX) * scaleX const drawY = (body.y - shiftY) * scaleY // Draw on canvas this.vis.beginPath(); this.vis.arc(drawX, drawY, body.drawSize, 0, twoPie, false); this.vis.fillStyle = body.color || "#aaa" this.vis.fill(); }); } // Because we draw the 3D space in 2D from the top, we ignore z bounds(bodies) { const ret = { xMin: 0, xMax: 0, yMin: 0, yMax: 0, zMin: 0, zMax: 0 } bodies.forEach(body => { if (ret.xMin > body.x) ret.xMin = body.x if (ret.xMax < body.x) ret.xMax = body.x if (ret.yMin > body.y) ret.yMin = body.y if (ret.yMax < body.y) ret.yMax = body.y if (ret.zMin > body.z) ret.zMin = body.z if (ret.zMax < body.z) ret.zMax = body.z }) return ret } }

實際的畫布繪製代碼只有五行——每行都以this.vis 。 其餘的代碼是場景的抓地力。

藝術永遠不會完成,它必須被拋棄

當客戶似乎花的錢不會讓他們賺錢時,現在是提出這個問題的好時機。 投資藝術是一項商業決策。

這個項目的客戶(我)決定從畫布實現轉移到 WebVR。 我想要一個華麗的、充滿炒作的 WebVR 演示。 所以讓我們把它包起來,得到一些!

有了我們所學到的知識,我們可以將這個畫布項目帶入多個方向。 如果你還記得第二篇文章,我們在內存中製作了幾個身體數據的副本:

內存中身體數據的副本

如果性能比設計複雜性更重要,可以將畫布的內存緩衝區直接傳遞給 WebAssembly。 這可以節省一些內存副本,從而提高性能:

  • 到 AssemblyScript 的 CanvasRenderingContext2D 原型
  • 使用 AssemblyScript 優化 CanvasRenderingContext2D 函數調用
  • OffscreenCanvas — 使用 Web Worker 加快畫布操作

就像 WebAssembly 和 AssemblyScript 一樣,這些項目正在處理上游兼容性中斷,因為規範設想了這些驚人的新瀏覽器功能。

所有這些項目——以及我在這裡使用的所有開源項目——都在為 VR 優先的互聯網公共空間的未來奠定基礎。 我們見到你,謝謝你!

在最後一篇文章中,我們將了解創建 VR 場景與創建平面網頁之間的一些重要設計差異。 由於 VR 不平凡,我們將使用 WebVR 框架構建我們的旋轉世界。 我選擇了 Google 的 A-Frame,它也是基於畫布構建的。

進入 WebVR 的起點是一段漫長的旅程。 但是這個系列不是關於 A-Frame hello world 演示的。 我興奮地寫了這個系列,向您展示將為互聯網的 VR 優先世界提供動力的瀏覽器技術基礎。