WebVR Bagian 4: Visualisasi Data Kanvas
Diterbitkan: 2022-03-11Hore! Kami mulai membuat Bukti Konsep untuk WebVR. Posting blog kami sebelumnya menyelesaikan simulasi, jadi sekarang saatnya bermain kreatif.
Ini adalah waktu yang sangat menyenangkan untuk menjadi seorang desainer dan pengembang karena VR adalah perubahan paradigma.
Pada tahun 2007, Apple menjual iPhone pertama, memulai revolusi konsumsi smartphone. Pada tahun 2012, kami memasuki desain web yang “utamakan seluler” dan “responsif”. Pada 2019, Facebook dan Oculus merilis headset VR seluler pertama. Mari kita lakukan!
Internet "mobile-first" bukanlah tren, dan saya memperkirakan internet "VR-first" juga tidak. Dalam tiga artikel dan demo sebelumnya, saya menunjukkan kemungkinan teknologi di browser Anda saat ini .
Jika Anda mengambil ini di tengah seri, kami sedang membangun simulasi gravitasi langit dari planet berduri.
- Bagian 1: Intro dan Arsitektur
- Bagian 2: Pekerja Web memberi kami utas browser tambahan
- Bagian 3: WebAssembly dan AssemblyScript untuk kode kemacetan kinerja O(n²) kami
Berdiri di atas pekerjaan yang telah kita lakukan, saatnya untuk bermain kreatif. Dalam dua posting terakhir, kita akan menjelajahi kanvas dan WebVR dan pengalaman pengguna.
- Bagian 4: Visualisasi Data Kanvas (posting ini)
- Bagian 5: Visualisasi Data WebVR
Hari ini, kita akan menghidupkan simulasi kita. Menengok ke belakang, saya menyadari betapa lebih bersemangat dan tertariknya saya dalam menyelesaikan proyek setelah saya mulai mengerjakan visualisator. Visualisasi membuatnya menarik bagi orang lain.
Tujuan dari simulasi ini adalah untuk mengeksplorasi teknologi yang akan memungkinkan WebVR - Virtual Reality di browser - dan web VR-first yang akan datang. Teknologi yang sama ini dapat mendukung komputasi tepi browser.
Melengkapi Bukti Konsep kami, hari ini pertama-tama kami akan membuat visualisasi kanvas.
Di posting terakhir, kita akan melihat desain VR dan membuat versi WebVR untuk menyelesaikan proyek ini.
Hal Paling Sederhana yang Mungkin Bisa Bekerja: console.log()
Kembali ke RR (Realitas Nyata). Mari kita buat beberapa visualisasi untuk simulasi "n-body" berbasis browser kita. Saya telah menggunakan kanvas dalam aplikasi video web di proyek sebelumnya tetapi tidak pernah sebagai kanvas seniman. Mari kita lihat apa yang bisa kita lakukan.
Jika Anda ingat arsitektur proyek kami, kami mendelegasikan visualisasi ke nBodyVisualizer.js
.
nBodySimulator.js
memiliki simulasi loop start()
yang memanggil fungsi step()
, dan bagian bawah step()
memanggil 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() }
Saat kami menekan tombol hijau, utas utama menambahkan 10 badan acak ke sistem. Kami menyentuh kode tombol di posting pertama, dan Anda dapat melihatnya di repo di sini. Badan-badan itu bagus untuk menguji bukti konsep, tapi ingat kita berada di wilayah kinerja yang berbahaya - O(n²).
Manusia dirancang untuk peduli dengan orang-orang dan hal-hal yang dapat mereka lihat, jadi trimDebris()
menghilangkan objek yang terbang dari pandangan sehingga mereka tidak memperlambat sisanya. Ini adalah perbedaan antara kinerja yang dirasakan dan kinerja yang sebenarnya.
Sekarang kita telah membahas semuanya kecuali yang terakhir this.visualize()
, mari kita lihat!
// 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) }
Kedua fungsi ini memungkinkan kita menambahkan beberapa visualizer. Ada dua visualizer dalam versi kanvas:
// 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")) ) …
Dalam versi kanvas, visualizer pertama adalah tabel angka putih yang ditampilkan sebagai HTML. Visualisator kedua adalah elemen kanvas hitam di bawahnya.
Untuk membuat ini, saya mulai dengan kelas dasar sederhana di 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)) } }
Kelas ini mencetak ke konsol (setiap 33 md!) dan juga melacak htmlElement - yang akan kita gunakan di subkelas agar mudah dideklarasikan di main.js
.
Ini adalah hal paling sederhana yang mungkin bisa berhasil.
Namun, meskipun visualisasi console
ini sangat sederhana, sebenarnya tidak "berfungsi". Konsol browser (dan manusia penjelajahan) tidak dirancang untuk memproses pesan log dengan kecepatan 33ms. Mari kita temukan hal paling sederhana berikutnya yang mungkin berhasil.
Memvisualisasikan Simulasi dengan Data
Iterasi "cetak cantik" berikutnya adalah mencetak teks ke elemen HTML. Ini juga merupakan pola yang kami gunakan untuk implementasi kanvas.
Perhatikan bahwa kita menyimpan referensi ke htmlElement
yang akan dilukis oleh visualizer. Seperti semua hal lain di web, ia memiliki desain yang mengutamakan seluler. Di desktop, ini mencetak tabel data objek dan koordinatnya di sebelah kiri halaman. Di ponsel itu akan menghasilkan kekacauan visual jadi kami melewatkannya.
/** * 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 } }
Visualisator "aliran data" ini memiliki dua fungsi:
- Ini adalah cara untuk "memeriksa kewarasan" input simulasi ke dalam visualizer. Ini adalah jendela "debug".
- Ini keren untuk dilihat, jadi mari kita simpan untuk demo desktop!
Sekarang setelah kita cukup yakin dengan input kita, mari kita bicara tentang grafik dan kanvas.

Memvisualisasikan Simulasi dengan Kanvas 2D
"Mesin Game" adalah "Mesin Simulasi" dengan ledakan. Keduanya adalah alat yang sangat rumit karena berfokus pada saluran aset, pemuatan level streaming, dan semua jenis hal yang sangat membosankan yang seharusnya tidak pernah diperhatikan.
Web juga telah menciptakan "hal-hal yang tidak boleh diperhatikan" sendiri dengan desain "yang mengutamakan seluler". Jika browser mengubah ukuran, CSS kanvas kami akan mengubah ukuran elemen kanvas di DOM, jadi visualizer kami harus menyesuaikan atau menerima penghinaan pengguna.
#visCanvas { margin: 0; padding: 0; background-color: #1F1F1F; overflow: hidden; width: 100vw; height: 100vh; }
Persyaratan ini mendorong resize()
di kelas dasar nBodyVisualizer
dan implementasi kanvas.
/** * 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') }
Ini menghasilkan visualizer kami memiliki tiga sifat penting:
-
this.vis
- dapat digunakan untuk menggambar primitif -
this.sizeX
-
this.sizeY
- dimensi area gambar
Catatan Desain Visualisasi 2D Kanvas
Pengubahan ukuran kami tidak sesuai dengan implementasi kanvas default. Jika kami memvisualisasikan produk atau grafik data, kami ingin:
- Gambar ke kanvas (pada ukuran dan rasio aspek yang diinginkan)
- Kemudian minta browser mengubah ukuran gambar itu ke dalam elemen DOM selama tata letak halaman
Dalam kasus penggunaan yang lebih umum ini, produk atau grafik adalah fokus dari pengalaman.
Visualisasi kami justru merupakan visualisasi teatrikal dari luasnya ruang , yang didramatisasi dengan melemparkan lusinan dunia kecil ke dalam kehampaan untuk bersenang-senang.
Benda langit kita menunjukkan ruang itu melalui kesederhanaan - menjaga jarak antara 0 dan 20 piksel. Pengubahan ukuran ini menskalakan ruang di antara titik-titik untuk menciptakan rasa kelapangan "ilmiah" dan meningkatkan kecepatan yang dirasakan.
Untuk menciptakan rasa skala antara objek dengan massa yang sangat berbeda, kami menginisialisasi benda dengan drawSize
yang sebanding dengan massa:
// 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) } }
Kerajinan Tangan Tata Surya dipesan lebih dahulu
Sekarang, ketika kita membuat tata surya kita di main.js
, kita akan memiliki semua alat yang kita butuhkan untuk visualisasi kita:
// 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()
Anda mungkin melihat dua "asteroid" di bagian bawah. Objek bermassa nol ini adalah peretasan yang digunakan untuk "menyematkan" area pandang terkecil dari simulasi ke area 30x30 yang berpusat pada 0,0.
Kami sekarang siap untuk fungsi cat kami. Awan benda bisa “goyang” menjauhi asal (0,0,0), jadi kita juga harus menggeser selain skala.
Kami "selesai" ketika simulasi memiliki nuansa alami. Tidak ada cara yang "benar" untuk melakukannya. Untuk mengatur posisi planet awal, saya hanya mengutak-atik angka-angkanya sampai cukup lama untuk menjadi menarik.
// 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 } }
Kode gambar kanvas sebenarnya hanya lima baris - masing-masing dimulai dengan this.vis
. Sisa kode adalah pegangan adegan.
Seni Tidak Pernah Selesai, Harus Ditinggalkan
Ketika klien tampaknya menghabiskan uang yang tidak akan menghasilkan uang bagi mereka, sekarang adalah saat yang tepat untuk mengungkapkannya. Berinvestasi dalam seni adalah keputusan bisnis.
Klien untuk proyek ini (saya) memutuskan untuk beralih dari implementasi kanvas ke WebVR. Saya menginginkan demo WebVR yang penuh sensasi. Jadi mari kita selesaikan ini dan dapatkan beberapa dari itu!
Dengan apa yang telah kami pelajari, kami dapat membawa proyek kanvas ini ke berbagai arah. Jika Anda ingat dari posting kedua, kami membuat beberapa salinan data tubuh di memori:
Jika kinerja lebih penting daripada kompleksitas desain, buffer memori kanvas dapat diteruskan ke WebAssembly secara langsung. Ini menghemat beberapa salinan memori, yang menambah kinerja:
- Prototipe CanvasRenderingContext2D ke AssemblyScript
- Mengoptimalkan Panggilan Fungsi CanvasRenderingContext2D Menggunakan AssemblyScript
- OffscreenCanvas — Percepat Operasi Kanvas Anda dengan Web Worker
Sama seperti WebAssembly dan AssemblyScript, proyek ini menangani jeda kompatibilitas upstream karena spesifikasinya membayangkan fitur browser baru yang menakjubkan ini.
Semua proyek ini - dan semua sumber terbuka yang saya gunakan di sini - sedang membangun fondasi untuk masa depan internet bersama yang mengutamakan VR. Kami melihat Anda dan terima kasih!
Di postingan terakhir, kita akan melihat beberapa perbedaan desain penting antara membuat adegan VR vs. halaman web datar. Dan karena VR tidak sepele, kami akan membangun dunia berputar kami dengan kerangka kerja WebVR. Saya memilih A-Frame Google, yang juga dibuat di atas kanvas.
Merupakan perjalanan panjang untuk sampai ke awal WebVR. Tapi seri ini bukan tentang demo hello world A-Frame. Saya menulis seri ini dalam kegembiraan saya untuk menunjukkan kepada Anda dasar-dasar teknologi browser yang akan menggerakkan dunia VR pertama di internet yang akan datang.