WebVR ตอนที่ 4: การแสดงข้อมูล Canvas

เผยแพร่แล้ว: 2022-03-11

เย่! เรามุ่งมั่นที่จะสร้าง Proof of Concept สำหรับ WebVR บล็อกโพสต์ก่อนหน้าของเราได้เสร็จสิ้นการจำลอง ดังนั้นถึงเวลาสำหรับการเล่นเชิงสร้างสรรค์เล็กน้อย

นี่เป็นช่วงเวลาที่น่าตื่นเต้นอย่างเหลือเชื่อในการเป็นนักออกแบบและนักพัฒนา เนื่องจาก VR เป็นการปรับเปลี่ยนกระบวนทัศน์

ในปี 2550 Apple ขาย iPhone เครื่องแรกโดยเริ่มต้นการปฏิวัติการบริโภคสมาร์ทโฟน ภายในปี 2012 เราเข้าสู่การออกแบบเว็บที่ "เน้นมือถือเป็นหลัก" และ "ตอบสนอง" ได้ดี ในปี 2019 Facebook และ Oculus ได้เปิดตัวชุดหูฟัง VR มือถือเครื่องแรก ลงมือทำกันเถอะ!

อินเทอร์เน็ตที่ "เน้นอุปกรณ์พกพา" ไม่ใช่แฟชั่น และฉันคาดการณ์ว่าอินเทอร์เน็ต "VR-first" จะไม่เป็นเช่นนั้น ในบทความและการสาธิตสามบทความก่อนหน้านี้ ฉันได้แสดงให้เห็นถึงความเป็นไปได้ทางเทคโนโลยีในเบราว์เซอร์ ปัจจุบัน ของคุณ

หากคุณกำลังเลือกสิ่งนี้ในช่วงกลางของซีรีส์ เรากำลังสร้างแบบจำลองแรงโน้มถ่วงบนท้องฟ้าของดาวเคราะห์หมุนวน

  • ส่วนที่ 1: บทนำและสถาปัตยกรรม
  • ส่วนที่ 2: Web Workers รับเธรดเบราว์เซอร์เพิ่มเติมให้เรา
  • ส่วนที่ 3: WebAssembly และ AssemblyScript สำหรับรหัสคอขวดด้านประสิทธิภาพ O(n²) ของเรา

ยืนหยัดกับงานที่เราทำ ถึงเวลาแล้วสำหรับการเล่นที่สร้างสรรค์ ในสองโพสต์สุดท้าย เราจะสำรวจพื้นที่ทำงานและ WebVR และประสบการณ์ของผู้ใช้

  • ส่วนที่ 4: การแสดงข้อมูล Canvas (โพสต์นี้)
  • ส่วนที่ 5: การแสดงข้อมูล WebVR

วันนี้ เราจะมาทำให้การจำลองของเรามีชีวิต เมื่อมองย้อนกลับไป ฉันสังเกตเห็นว่าฉันรู้สึกตื่นเต้นและสนใจมากขึ้นเพียงใดในการทำโครงงานนี้ให้เสร็จเมื่อฉันเริ่มทำงานกับโปรแกรมสร้างภาพ การสร้างภาพข้อมูลทำให้คนอื่นน่าสนใจ

จุดประสงค์ของการจำลองนี้คือการสำรวจเทคโนโลยีที่จะเปิดใช้งาน WebVR - Virtual Reality ในเบราว์เซอร์ - และเว็บ VR-first ที่กำลังจะมีขึ้น เทคโนโลยีเดียวกันนี้สามารถขับเคลื่อนการประมวลผลแบบเอดจ์เบราว์เซอร์ได้

ในการสรุป Proof of Concept ของเรา วันนี้ ขั้นแรกเราจะสร้างการแสดงภาพแคนวาส

การสร้างภาพผ้าใบ
การสาธิต Canvas Visualizer, โค้ดตัวอย่าง

ในโพสต์สุดท้าย เราจะดูการออกแบบ VR และสร้างเวอร์ชัน WebVR เพื่อให้โครงการนี้ "เสร็จสิ้น"

การแสดงข้อมูล WebVR

สิ่งที่ง่ายที่สุดที่อาจใช้งานได้: console.log()

กลับไปที่ RR (Real Reality) มาสร้างการแสดงภาพข้อมูลสำหรับการจำลอง "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")) ) …

ในเวอร์ชันแคนวาส Visualizer แรกคือตารางตัวเลขสีขาวที่แสดงเป็น 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)) } }

คลาสนี้จะพิมพ์ไปที่คอนโซล (ทุก 33ms!) และยังติดตาม htmlElement - ซึ่งเราจะใช้ในคลาสย่อยเพื่อให้ง่ายต่อการประกาศใน main.js

นี่คือสิ่งที่ง่ายที่สุดที่อาจใช้ได้

อย่างไรก็ตาม แม้ว่าการแสดงภาพ console นี้จะเรียบง่าย แต่จริงๆ แล้ว "ไม่ได้ผล" คอนโซลของเบราว์เซอร์ (และการเรียกดูโดยมนุษย์) ไม่ได้ออกแบบมาเพื่อประมวลผลข้อความบันทึกที่ความเร็ว 33 มิลลิวินาที มาหา สิ่งที่ง่ายที่สุดต่อไป ที่อาจใช้ได้

การสร้างภาพจำลองด้วย Data

การทำซ้ำ "การพิมพ์ที่สวยงาม" ครั้งต่อไปคือการพิมพ์ข้อความไปยังองค์ประกอบ HTML นี่เป็นรูปแบบที่เราใช้สำหรับการนำผืนผ้าใบไปใช้

สังเกตว่า เรากำลังบันทึกการอ้างอิงไปยัง htmlElement ที่ visualizer จะใช้ เช่นเดียวกับทุกอย่างบนเว็บ มีการออกแบบที่เน้นมือถือเป็นหลัก บนเดสก์ท็อป สิ่งนี้จะพิมพ์ตารางข้อมูลของออบเจ็กต์และพิกัดทางด้านซ้ายของหน้า บนมือถือจะส่งผลให้ภาพรกดังนั้นเราจึงข้ามมันไป

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

“Game Engine” คือ “Simulation Engine” ที่มีการระเบิด ทั้งสองเป็นเครื่องมือที่ซับซ้อนอย่างไม่น่าเชื่อเพราะมุ่งเน้นไปที่ท่อส่งสินทรัพย์ การโหลดระดับการสตรีม และสิ่งน่าเบื่ออย่างเหลือเชื่อทุกประเภทที่ไม่ควรมองข้าม

เว็บยังได้สร้าง "สิ่งที่ไม่ควรสังเกตเห็น" ด้วยการออกแบบ "เน้นอุปกรณ์พกพา" ของตัวเอง หากเบราว์เซอร์ปรับขนาด CSS ของแคนวาสของเราจะปรับขนาดองค์ประกอบแคนวาสใน DOM ดังนั้น Visualizer ของเราต้องปรับหรือรับการดูหมิ่นของผู้ใช้

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

ข้อกำหนดนี้ขับเคลื่อนการ resize() ในคลาสฐาน nBodyVisualizer และการนำผ้าใบไปใช้

 /** * 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 - สามารถใช้วาด primitives ได้
  • this.sizeX
  • this.sizeY - ขนาดของพื้นที่วาด

หมายเหตุการออกแบบการแสดงภาพ 2 มิติของ Canvas

การปรับขนาดของเราทำงาน กับ การใช้งานผ้าใบเริ่มต้น หากเรากำลังแสดงภาพผลิตภัณฑ์หรือกราฟข้อมูล เราต้องการ:

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

คุณอาจสังเกตเห็น "ดาวเคราะห์น้อย" สองดวงที่ด้านล่าง วัตถุที่มีมวลเป็นศูนย์เหล่านี้เป็นแฮ็กที่ใช้เพื่อ "ตรึง" วิวพอร์ตที่เล็กที่สุดของการจำลองไปยังพื้นที่ 30x30 ที่มีศูนย์กลางอยู่ที่ 0,0

ตอนนี้เราพร้อมแล้วสำหรับฟังก์ชั่นการระบายสีของเรา ก้อนเมฆของร่างกายสามารถ "โยกเยก" ออกจากจุดกำเนิด (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 โค้ดที่เหลือคือส่วนยึดของฉาก

ศิลปะไม่มีวันจบ ต้องละทิ้ง

เมื่อลูกค้าดูเหมือนจะใช้จ่ายเงินซึ่งไม่สามารถหาเงินได้ ตอนนี้เป็นเวลาที่ดีที่จะพูดถึงมัน การลงทุนด้านศิลปะคือการตัดสินใจทางธุรกิจ

ลูกค้าสำหรับโปรเจ็กต์นี้ (ฉัน) ตัดสินใจย้ายจากการใช้งาน canvas ไปเป็น WebVR ฉันต้องการตัวอย่าง WebVR ที่ฉูดฉาดฉูดฉาด มาปิดท้ายและรับของกัน!

ด้วยสิ่งที่เราได้เรียนรู้ เราสามารถดำเนินโครงการผืนผ้าใบนี้ได้ในหลายทิศทาง หากคุณจำได้จากโพสต์ที่สอง เรากำลังทำสำเนาข้อมูลร่างกายหลายชุดในหน่วยความจำ:

สำเนาข้อมูลร่างกายในหน่วยความจำ

หากประสิทธิภาพมีความสำคัญมากกว่าความซับซ้อนในการออกแบบ ก็สามารถส่งบัฟเฟอร์หน่วยความจำของ canvas ไปยัง WebAssembly ได้โดยตรง ซึ่งจะช่วยประหยัดสำเนาหน่วยความจำสองสามชุด ซึ่งเพิ่มขึ้นเพื่อประสิทธิภาพ:

  • ต้นแบบ CanvasRenderingContext2D เป็น AssemblyScript
  • การเพิ่มประสิทธิภาพการเรียกใช้ฟังก์ชัน CanvasRenderingContext2D โดยใช้ AssemblyScript
  • OffscreenCanvas — เร่งความเร็วการทำงานของ Canvas ด้วย Web Worker

เช่นเดียวกับ WebAssembly และ AssemblyScript โปรเจ็กต์เหล่านี้กำลังจัดการตัวแบ่งความเข้ากันได้ของต้นน้ำ เนื่องจากข้อกำหนดจะมองเห็นคุณลักษณะใหม่ของเบราว์เซอร์ที่น่าทึ่งเหล่านี้

โปรเจ็กต์เหล่านี้ทั้งหมด และโอเพ่นซอร์สทั้งหมดที่ฉันใช้ที่นี่ กำลังสร้างรากฐานสำหรับอนาคตของคอมมอนส์อินเทอร์เน็ตที่เน้น VR เป็นครั้งแรก เราพบคุณและขอขอบคุณ!

ในโพสต์สุดท้าย เราจะดูความแตกต่างในการออกแบบที่สำคัญระหว่างการสร้างฉาก VR กับหน้าเว็บแบบเรียบ และเนื่องจาก VR นั้นไม่สำคัญ เราจะสร้างโลกที่หมุนวนด้วยเฟรมเวิร์ก WebVR ฉันเลือก A-Frame ของ Google ซึ่งสร้างจากผ้าใบด้วย

การเดินทางสู่จุดเริ่มต้นของ WebVR เป็นการเดินทางที่ยาวนาน แต่ซีรีส์นี้ไม่เกี่ยวกับการสาธิต A-Frame สวัสดีชาวโลก ฉันเขียนซีรีส์นี้ด้วยความตื่นเต้นเพื่อแสดงให้คุณเห็นถึงพื้นฐานเทคโนโลยีเบราว์เซอร์ที่จะขับเคลื่อนโลก VR แรกของอินเทอร์เน็ตที่จะมาถึง