WebVR 4부: 캔버스 데이터 시각화

게시 됨: 2022-03-11

만세! 우리는 WebVR에 대한 개념 증명을 만들기 시작했습니다. 이전 블로그 게시물에서 시뮬레이션이 완료되었으므로 이제 약간의 창의적인 플레이를 할 시간입니다.

VR은 패러다임의 전환이기 때문에 디자이너와 개발자에게 매우 흥미로운 시기입니다.

2007년 애플은 최초의 아이폰을 판매하며 스마트폰 소비 혁명을 일으켰다. 2012년까지 우리는 "모바일 우선" 및 "반응형" 웹 디자인에 뛰어들었습니다. 2019년, 페이스북과 오큘러스는 최초의 모바일 VR 헤드셋을 출시했습니다. 해보자!

"모바일 우선" 인터넷은 유행이 아니었고 "VR 우선" 인터넷도 유행하지 않을 것이라고 예상합니다. 이전 세 개의 기사와 데모에서 현재 브라우저의 기술적 가능성을 보여주었습니다.

시리즈 중간에 이것을 선택한다면 우리는 회전하는 행성의 천체 중력 시뮬레이션을 구축하고 있습니다.

  • 1부: 소개 및 아키텍처
  • 2부: 웹 작업자는 추가 브라우저 스레드를 얻습니다.
  • 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 에는 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개의 임의의 바디를 추가합니다. 첫 번째 게시물에서 버튼 코드를 터치했고 여기 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)) } }

이 클래스는 콘솔에 인쇄하고(33ms마다!) 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 } }

이 "데이터 스트림" 시각화 도우미에는 두 가지 기능이 있습니다.

  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 - 도면 영역의 치수

캔버스 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,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에 직접 전달할 수 있습니다. 이렇게 하면 몇 가지 메모리 복사본이 저장되어 성능이 향상됩니다.

  • CanvasRenderingContext2D 프로토타입을 AssemblyScript로
  • AssemblyScript를 사용하여 CanvasRenderingContext2D 함수 호출 최적화
  • OffscreenCanvas — 웹 작업자로 캔버스 작업 속도 향상

WebAssembly 및 AssemblyScript와 마찬가지로 이러한 프로젝트는 사양이 이러한 놀라운 새 브라우저 기능을 구상함에 따라 업스트림 호환성 중단을 처리하고 있습니다.

이 모든 프로젝트와 내가 여기서 사용한 모든 오픈 소스는 VR 우선 인터넷 공유지의 미래를 위한 기반을 구축하고 있습니다. 우리는 당신을보고 감사합니다!

마지막 게시물에서 우리는 VR 장면을 만드는 것과 플랫 웹 페이지를 만드는 것 사이의 몇 가지 중요한 디자인 차이점을 살펴볼 것입니다. 그리고 VR은 사소하지 않기 때문에 WebVR 프레임워크를 사용하여 회전하는 세상을 구축할 것입니다. 캔버스를 기반으로 하는 Google의 A-Frame을 선택했습니다.

WebVR의 시작에 도달하는 것은 긴 여정이었습니다. 그러나 이 시리즈는 A-Frame hello world 데모에 관한 것이 아닙니다. 인터넷의 VR 우선 세계에 힘을 실어줄 브라우저 기술 기반을 보여드리고자 이 시리즈를 작성했습니다.