WebVRパート4:キャンバスデータの視覚化
公開: 2022-03-11やあ! WebVRの概念実証の作成に着手しました。 以前のブログ投稿でシミュレーションが完了したので、今度は少しクリエイティブなプレイをします。
VRはパラダイムシフトであるため、これはデザイナーおよび開発者にとって非常にエキサイティングな時期です。
2007年、Appleは最初のiPhoneを販売し、スマートフォンの消費革命を開始しました。 2012年までに、私たちは「モバイルファースト」および「レスポンシブ」なWebデザインに精通しました。 2019年、FacebookとOculusは最初のモバイルVRヘッドセットをリリースしました。 これをやろう!
「モバイルファースト」のインターネットは流行ではなく、「VRファースト」のインターネットも流行ではないと私は予測しています。 前の3つの記事とデモでは、現在のブラウザーでの技術的な可能性を示しました。
シリーズの途中でこれを取り上げている場合は、とげのある惑星の天体重力シミュレーションを構築しています。
- パート1:イントロとアーキテクチャ
- パート2: Webワーカーは追加のブラウザースレッドを取得します
- パート3: O(n²)パフォーマンスボトルネックコード用のWebAssemblyとAssemblyScript
私たちが行った仕事に立って、それはいくつかの創造的な遊びの時間です。 最後の2つの投稿では、canvasとWebVR、およびユーザーエクスペリエンスについて説明します。
- パート4:キャンバスデータの視覚化(この投稿)
- パート5: WebVRデータの視覚化
今日は、シミュレーションを実現します。 振り返ってみると、ビジュアライザーの作業を開始すると、プロジェクトを完了することにどれほど興奮し、興味を持っているかに気づきました。 視覚化はそれを他の人々にとって面白くしました。
このシミュレーションの目的は、WebVR(ブラウザーのバーチャルリアリティ)と今後のVRファーストWebを可能にするテクノロジーを調査することでした。 これらの同じテクノロジーは、ブラウザーエッジコンピューティングを強化することができます。
概念実証の締めくくりとして、今日は最初にキャンバスの視覚化を作成します。
最後の投稿では、VRの設計を見て、このプロジェクトを「完了」させるためのWebVRバージョンを作成します。
おそらく機能する可能性のある最も単純なもの: console.log()
RR(Real Reality)に戻ります。 ブラウザベースの「n-body」シミュレーション用の視覚化をいくつか作成しましょう。 私は過去のプロジェクトでWebビデオアプリケーションでキャンバスを使用しましたが、アーティストのキャンバスとして使用したことはありません。 私たちに何ができるか見てみましょう。
プロジェクトアーキテクチャを覚えている場合は、視覚化をnBodyVisualizer.js
に委任しました。
nBodySimulator.js
には、 step()
関数を呼び出すシミュレーションループstart()
があり、step()の下部はthis.visualize()
step()
を呼び出します。
// 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個のランダムなボディをシステムに追加します。 最初の投稿でボタンコードに触れましたが、こちらのリポジトリで確認できます。 これらのボディは概念実証をテストするのに最適ですが、危険なパフォーマンス領域である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) }
これらの2つの関数を使用すると、複数のビジュアライザーを追加できます。 キャンバスバージョンには2つのビジュアライザーがあります。
// 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として表示される白い数字のテーブルです。 2番目のビジュアライザーは、下にある黒いキャンバス要素です。
これを作成するために、 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
の視覚化は確かに単純ですが、実際には「機能」しません。 ブラウザコンソール(およびブラウジングする人間)は、33msの速度でログメッセージを処理するようには設計されていません。 おそらく機能する可能性のある次の最も単純なものを見つけましょう。
データを使用したシミュレーションの視覚化
次の「きれいな印刷」の反復は、テキストを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 } }
この「データストリーム」ビジュアライザーには、次の2つの機能があります。
- これは、ビジュアライザーへのシミュレーションの入力を「健全性チェック」する方法です。 これは「デバッグ」ウィンドウです。
- 見るのはかっこいいので、デスクトップデモ用に残しておきましょう!
入力にかなり自信が持てたので、グラフィックとキャンバスについて話しましょう。

2Dキャンバスを使用したシミュレーションの視覚化
「ゲームエンジン」は爆発を伴う「シミュレーションエンジン」です。 どちらも、アセットパイプライン、ストリーミングレベルの読み込み、および気付かれることのないあらゆる種類の非常に退屈なものに焦点を当てているため、非常に複雑なツールです。
ウェブはまた、「モバイルファースト」のデザインで独自の「決して気付かれるべきではないもの」を作成しました。 ブラウザのサイズが変更されると、キャンバスのCSSがDOMのcanvas要素のサイズを変更するため、ビジュアライザーはユーザーの軽蔑に適応するか、苦しむ必要があります。
#visCanvas { margin: 0; padding: 0; background-color: #1F1F1F; overflow: hidden; width: 100vw; height: 100vh; }
この要件により、 nBodyVisualizer
基本クラスとcanvas実装の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') }
これにより、ビジュアライザーには次の3つの重要なプロパティがあります。
-
this.vis
プリミティブの描画に使用できます this.sizeX
-
this.sizeY
作図領域の寸法
Canvas2Dビジュアライゼーションデザインノート
サイズ変更は、デフォルトのキャンバス実装に対して機能します。 製品またはデータグラフを視覚化する場合は、次のことを行います。
- キャンバスに描画します(適切なサイズとアスペクト比で)
- 次に、ページレイアウト中に、ブラウザにその描画を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()
下部にある2つの「小惑星」に気付くかもしれません。 これらのゼロマスオブジェクトは、シミュレーションの最小のビューポートを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 } }
実際のキャンバス描画コードはわずか5行で、それぞれがthis.vis
で始まります。 コードの残りの部分は、シーンのグリップです。
アートは決して終わらない、それは放棄されなければならない
クライアントが彼らにお金を稼ぐつもりのないお金を使っているように見えるとき、今はそれを育てる良い時期です。 アートへの投資はビジネス上の決定です。
このプロジェクトのクライアント(私)は、キャンバスの実装からWebVRに移行することを決定しました。 派手な誇大広告でいっぱいのWebVRデモが欲しかった。 それでは、これをまとめて、その一部を入手しましょう!
私たちが学んだことで、このキャンバスプロジェクトをさまざまな方向に進めることができました。 2番目の投稿から覚えている場合は、メモリ内にボディデータのコピーをいくつか作成しています。
デザインの複雑さよりもパフォーマンスが重要な場合は、キャンバスのメモリバッファをWebAssemblyに直接渡すことができます。 これにより、メモリコピーがいくつか節約され、パフォーマンスが向上します。
- CanvasRenderingContext2DプロトタイプからAssemblyScriptへ
- AssemblyScriptを使用したCanvasRenderingContext2D関数呼び出しの最適化
- OffscreenCanvas —Webワーカーを使用してCanvas操作を高速化
WebAssemblyやAssemblyScriptと同様に、これらのプロジェクトは、仕様がこれらの驚くべき新しいブラウザー機能を想定しているため、アップストリームの互換性の中断を処理しています。
これらすべてのプロジェクト(およびここで使用したすべてのオープンソース)は、VRファーストのインターネットコモンズの将来の基盤を構築しています。 よろしくお願いします!
最後の投稿では、VRシーンの作成とフラットWebページの作成の重要なデザインの違いをいくつか見ていきます。 また、VRは重要であるため、WebVRフレームワークを使用してとげのある世界を構築します。 私はGoogleのA-Frameを選びました。これもキャンバス上に構築されています。
WebVRの始まりに到達するまでには長い道のりがありました。 しかし、このシリーズはA-Framehelloworldデモに関するものではありませんでした。 私は興奮してこのシリーズを書き、インターネットのVRファーストの世界を動かすブラウザテクノロジーの基盤を紹介しました。