WebVR Bölüm 4: Kanvas Veri Görselleştirmeleri

Yayınlanan: 2022-03-11

Yaşasın! WebVR için bir Kavram Kanıtı oluşturmak üzere yola çıktık. Önceki blog yazılarımız simülasyonu tamamladı, şimdi biraz yaratıcı oyun zamanı.

Bu, tasarımcı ve geliştirici olmak için inanılmaz derecede heyecan verici bir zaman çünkü VR bir paradigma kayması.

2007'de Apple, akıllı telefon tüketim devrimini başlatan ilk iPhone'u sattı. 2012'ye gelindiğinde, "önce mobil" ve "duyarlı" web tasarımına girmiştik. 2019'da Facebook ve Oculus, ilk mobil VR başlığını piyasaya sürdü. Bunu yapalım!

“Önce mobil” internet bir moda değildi ve “önce VR” internetin de olmayacağını tahmin ediyorum. Önceki üç makale ve demoda, mevcut tarayıcınızdaki teknolojik olasılığı gösterdim.

Bunu serinin ortasında alıyorsanız, dönen gezegenlerin göksel yerçekimi simülasyonunu yapıyoruz.

  • Bölüm 1: Giriş ve Mimari
  • 2. Bölüm: Web Çalışanları bize ek tarayıcı ileti dizileri sağlar
  • Bölüm 3: O(n²) performans darboğaz kodumuz için WebAssembly ve AssemblyScript

Yaptığımız işin üzerinde dururken, biraz yaratıcı oyun zamanı. Son iki gönderide, tuvali ve WebVR'yi ve kullanıcı deneyimini keşfedeceğiz.

  • Bölüm 4: Kanvas Veri Görselleştirme (bu gönderi)
  • Bölüm 5: WebVR Veri Görselleştirme

Bugün simülasyonumuzu hayata geçireceğiz. Geriye dönüp baktığımda, görselleştiriciler üzerinde çalışmaya başladığımda projeyi tamamlama konusunda ne kadar heyecanlı ve ilgili olduğumu fark ettim. Görselleştirmeler onu diğer insanlar için ilginç hale getirdi.

Bu simülasyonun amacı, WebVR'yi - tarayıcıda Sanal Gerçekliği - ve VR öncelikli web'i etkinleştirecek teknolojiyi keşfetmekti. Aynı teknolojiler, tarayıcı sınırındaki bilgi işlemine güç sağlayabilir.

Kavram Kanıtımızı tamamlayarak, bugün önce bir tuval görselleştirmesi oluşturacağız.

Kanvas görselleştirme
Kanvas Görüntüleyici Demosu, Örnek Kod

Son gönderide, VR tasarımına bakacağız ve bu projenin "bitmesi" için bir WebVR versiyonu yapacağız.

WebVR Veri Görselleştirme

İşe Yarayabilecek En Basit Şey: console.log()

RR'ye (Gerçek Gerçeklik) dönüş. Tarayıcı tabanlı “n-body” simülasyonumuz için bazı görselleştirmeler oluşturalım. Tuvali önceki projelerde web video uygulamalarında kullandım ama asla bir sanatçının tuvali olarak kullanmadım. Ne yapabileceğimize bir bakalım.

Proje mimarimizi hatırlarsanız, görselleştirmeyi nBodyVisualizer.js .

Görselleştirmeyi nBodyVisualizer.js'ye devredin

nBodySimulator.js , step() işlevini çağıran bir simülasyon döngüsü start() 'a sahiptir ve step() öğesinin alt kısmı this.visualize() çağırır.

 // 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() }

Yeşil tuşa bastığımızda ana iş parçacığı sisteme rastgele 10 cisim ekliyor. İlk gönderide buton koduna dokunduk ve buradaki repoda görebilirsiniz. Bu gövdeler bir kavram kanıtını test etmek için harikadır, ancak tehlikeli performans bölgesinde olduğumuzu unutmayın - O(n²).

İnsanlar, insanları ve görebilecekleri şeyleri önemsemek üzere tasarlanmıştır, bu nedenle trimDebris() , gözden kaybolan nesneleri kaldırır, böylece geri kalanları yavaşlatmazlar. Bu, algılanan ve gerçek performans arasındaki farktır.

Şimdi this.visualize() sonuncusu dışındaki her şeyi ele aldığımıza göre, hadi bir göz atalım!

 // 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) }

Bu iki işlev, birden çok görselleştirici eklememize izin verir. Tuval versiyonunda iki görselleştirici vardır:

 // 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")) ) …

Tuval versiyonunda, ilk görselleştirici HTML olarak görüntülenen beyaz sayılar tablosudur. İkinci görselleştirici, altındaki siyah bir tuval öğesidir.

Tuval görselleştiricileri
Soldaki HTML görselleştiricisi beyaz sayıların tablosudur. Siyah tuval görselleştiricisi altında

Bunu oluşturmak için nBodyVisualizer.js basit bir temel sınıfla başladım:

 // 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)) } }

Bu sınıf konsola yazdırır (her 33 ms'de bir!) ve ayrıca bir htmlElement'i izler - bu, alt sınıflarda, main.js bildirilmelerini kolaylaştırmak için kullanacağız.

Bu, işe yarayabilecek en basit şeydir.

Ancak, bu console görselleştirmesi kesinlikle basit olsa da, aslında “çalışmıyor”. Tarayıcı konsolu (ve gezinen insanlar), günlük mesajlarını 33ms hızında işlemek için tasarlanmamıştır. İşe yarayabilecek bir sonraki en basit şeyi bulalım.

Simülasyonları Verilerle Görselleştirme

Bir sonraki "güzel baskı" yinelemesi, metni bir HTML öğesine yazdırmaktı. Bu aynı zamanda tuval uygulaması için kullandığımız kalıptır.

Görselleştiricinin boyayacağı bir htmlElement referansını kaydettiğimize dikkat edin. Web'deki diğer her şey gibi, mobil öncelikli bir tasarıma sahiptir. Masaüstünde bu, sayfanın solundaki nesnelerin veri tablosunu ve koordinatlarını yazdırır. Mobilde görsel dağınıklığa neden olacağından atlıyoruz.

 /** * 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 } }

Bu "veri akışı" görselleştiricisinin iki işlevi vardır:

  1. Bu, simülasyonun görselleştiriciye yaptığı girdileri "akıl sağlığını kontrol etmenin" bir yoludur. Bu bir "hata ayıklama" penceresidir.
  2. Bakması harika, bu yüzden masaüstü demosu için kalalım!

Artık girdilerimizden oldukça emin olduğumuza göre, grafikler ve tuval hakkında konuşalım.

2B Kanvas ile Simülasyonları Görselleştirme

Bir "Oyun Motoru", patlamaları olan bir "Simülasyon Motoru"dur. Her ikisi de inanılmaz derecede karmaşık araçlar çünkü varlık boru hatlarına, akış düzeyinde yüklemeye ve asla fark edilmemesi gereken her türlü inanılmaz sıkıcı şeye odaklanıyorlar.

Web ayrıca "önce mobil" tasarımıyla kendi "fark edilmemesi gereken şeyleri" yarattı. Tarayıcı yeniden boyutlandırılırsa, tuvalimizin CSS'si DOM'daki tuval öğesini yeniden boyutlandıracaktır, bu nedenle görselleştiricimiz uyum sağlamalı veya kullanıcıların küçümsemesine maruz kalmalıdır.

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

Bu gereksinim, nBodyVisualizer temel sınıfında ve tuval uygulamasında resize() işlevini yönlendirir.

 /** * 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') }

Bu, görselleştiricimizin üç temel özelliğe sahip olmasını sağlar:

  • this.vis - ilkelleri çizmek için kullanılabilir
  • this.sizeX
  • this.sizeY - çizim alanının boyutları

Tuval 2D Görselleştirme Tasarım Notları

Yeniden boyutlandırmamız, varsayılan tuval uygulamasına karşı çalışır. Bir ürünü veya veri grafiğini görselleştiriyor olsaydık, şunları yapmak isterdik:

  1. Tuvale çizin (tercih edilen bir boyut ve en boy oranında)
  2. Ardından, sayfa düzeni sırasında tarayıcının bu çizimi DOM öğesine yeniden boyutlandırmasını sağlayın

Bu daha yaygın kullanım durumunda, ürün veya grafik, deneyimin odak noktasıdır.

Görselleştirmemiz, bunun yerine, düzinelerce küçük dünyayı eğlence için boşluğa fırlatarak dramatize edilen, uzayın genişliğinin teatral bir görselleştirmesidir.

Gök cisimlerimiz bu alanı alçakgönüllülük yoluyla gösterir - kendilerini 0 ila 20 piksel genişliğinde tutar. Bu yeniden boyutlandırma, "bilimsel" bir genişlik hissi yaratmak için noktalar arasındaki boşluğu ölçeklendirir ve algılanan hızı artırır.

Çok farklı kütlelere sahip nesneler arasında bir ölçek duygusu yaratmak için, gövdeleri kütleyle orantılı bir drawSize ile başlatırız:

 // 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) } }

El İşi Ismarlama Güneş Sistemleri

Şimdi, main.js güneş sistemimizi oluşturduğumuzda, görselleştirmemiz için ihtiyacımız olan tüm araçlara sahip olacağız:

 // 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()

Alttaki iki “asteroidi” fark edebilirsiniz. Bu sıfır kütleli nesneler, simülasyonun en küçük görüntü portunu 0,0 merkezli 30x30'luk bir alana "sabitlemek" için kullanılan bir hack'tir.

Artık boyama fonksiyonumuz için hazırız. Cisim bulutu başlangıç ​​noktasından (0,0,0) "sallanabilir", bu nedenle ölçeğe ek olarak biz de değişmeliyiz.

Simülasyon doğal bir his verdiğinde “bitiririz”. Bunu yapmanın "doğru" bir yolu yoktur. İlk gezegen konumlarını düzenlemek için, ilginç olacak kadar uzun süre bir arada tutana kadar sayılarla oynadım.

 // 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 } }

Gerçek tuval çizim kodu yalnızca beş satırdır - her biri this.vis ile başlar. Kodun geri kalanı sahnenin tutuşudur.

Sanat Asla Bitmez Terk Edilmelidir

Müşteriler kendilerine para kazandırmayacak para harcıyor gibi görünüyorsa, şu an konuyu gündeme getirmenin tam zamanıdır. Sanata yatırım yapmak bir ticari karardır.

Bu projenin müşterisi (ben) tuval uygulamasından WebVR'a geçmeye karar verdi. Gösterişli, heyecan dolu bir WebVR demosu istedim. Öyleyse, bunu toparlayalım ve bir kısmını alalım!

Öğrendiklerimizle, bu tuval projesini çeşitli yönlere götürebiliriz. İkinci gönderiden hatırlarsanız, bellekteki vücut verilerinin birkaç kopyasını yapıyoruz:

Vücut verilerinin bellekteki kopyaları

Performans, tasarımın karmaşıklığından daha önemliyse, tuvalin bellek arabelleğini doğrudan WebAssembly'ye geçirmek mümkündür. Bu, performans için ekleyen birkaç bellek kopyası kaydeder:

  • AssemblyScript'e CanvasRenderingContext2D prototipi
  • AssemblyScript Kullanarak CanvasRenderingContext2D İşlev Çağrılarını Optimize Etme
  • OffscreenCanvas — Bir Web Çalışanıyla Kanvas İşlemlerinizi Hızlandırın

Tıpkı WebAssembly ve AssemblyScript gibi, bu projeler, spesifikasyonlar bu şaşırtıcı yeni tarayıcı özelliklerini öngördüğü için yukarı akış uyumluluk kesintilerini ele alıyor.

Tüm bu projeler - ve burada kullandığım tüm açık kaynaklar - VR öncelikli internet ortak alanlarının geleceği için temeller oluşturuyor. Seni görüyoruz ve teşekkür ediyoruz!

Son gönderide, VR sahnesi oluşturma ile düz web sayfası oluşturma arasındaki bazı önemli tasarım farklılıklarına bakacağız. VR önemsiz olmadığı için, spinny dünyamızı bir WebVR çerçevesiyle inşa edeceğiz. Ben de tuval üzerine inşa edilmiş Google'ın A-Frame'ini seçtim.

WebVR'ın başlangıcına ulaşmak uzun bir yolculuk oldu. Ancak bu seri, A-Frame merhaba dünya demosu ile ilgili değildi. Bu seriyi, internetin VR-ilk dünyalarına güç verecek tarayıcı teknolojisi temellerini size göstermek için heyecanla yazdım.