버블 구현 방법에 대한 자세한 소개

이전 글에서는 IE10의 향상된 JavaScript 성능에 대해 설명했습니다. 오늘 우리는 JavaScript 성능이 얼마나 향상되었는지 직접 눈으로 확인해 볼 수 있도록 버블을 만들었습니다. 이 버블은 Alex Gavriolov의 시뮬레이션 BubbleMark에서 영감을 받아 제작한 것입니다. 이 최신 버전은 웹 플랫폼의 새로운 기능을 활용하도록 대폭 확장되었으며 HTML5 게임의 일반적인 특성을 포함하고 있습니다. 이 글에서는 이 데모를 어떻게 만들었는지를 설명하고, 이 데모의 성능에 영향을 미치는 주요 요소에 대해 알아봅니다.

Windows 8 Release Preview의 Metro 스타일 IE10에서 실행 중인 버블 데모 스크린샷
Windows 8 Release Preview의 Metro 스타일 IE10에서 실행 중인 버블 데모

데모의 구조

이 데모는 공간을 떠다니는 여러 버블 애니메이션으로 구성됩니다. 이러한 애니메이션을 구현하는 핵심 비결은 이전에 비해 간소화된 JavaScript 물리 엔진에 있습니다. 이 물리 엔진은 모든 애니메이션 프레임에서 초당 약 60회 정도 모든 버블의 위치를 다시 계산하고, 중력을 적용하여 각 버블의 속도를 조정하고, 충돌을 계산합니다. 이러한 모든 계산은 집약적인 부동 소수점 산술과 관련되어 있습니다. 각 버블은 CSS 변환이 적용된 DOM 이미지 요소를 통해 화면에 표현됩니다. 이미지는 원점을 기준으로 변환된 후 동적으로 확장되어 팽창하는 버블 효과를 구현합니다. JavaScript에서 모든 버블은 ECMAScript 5에 도입된 접근자 속성이 있는 개체로 표현됩니다.

떠다니는 버블 뒤에 있는 큰 이미지는 <canvas> 요소에 렌더링된 완전한 불투명 마스크로 가려진 상태로 시작됩니다. 버블 두 개가 충돌할 때마다 마스크의 불투명도 구성 요소에 가우스 필터가 적용되어 가리기 마스크의 일부가 제거되므로 확산 투명 효과가 나타납니다. 이러한 프로세스는 또한 형식 배열의 요소에 대해 수행되는 많은 부동 소수점 곱셈과 관련이 있습니다(브라우저에서 지원하는 경우).

떠다니는 버블 위에는 실행 중인 브라우저에서 터치 입력을 지원할 경우 터치 입력, 지원하지 않을 경우 마우스 이벤트에 응답하는 터치 서피스가 있습니다. 터치에 대한 응답으로 터치로 시뮬레이션된 자기 반발(magnetic repulsion)이 떠다니는 버블에 적용되어 버블이 분산됩니다.

효율적인 애니메이션

IE10는 requestAnimationFrame API를 지원합니다. 애니메이션을 지원하는 JavaScript 응용 프로그램을 제작할 경우에도 지원되는 브라우저에서 setTimeout 또는 setInterval 대신 이 API를 사용하는 것이 좋습니다. 하드웨어를 최대한 활용하는 방법에 관한 이전 글에서 설명한 것처럼 이 API를 사용하면 디스플레이에서 지원 가능한 최대 프레임 속도를 구현하는 동시에 사용자가 볼 수 없는 불필요한 작업을 피할 수 있습니다. 따라서 최소한의 작업으로 최상의 사용자 환경을 제공하여 배터리 전력을 절약할 수 있습니다. IE10 Release Preview는 공급업체 접두사가 없는 이러한 API를 지원하지만 이전 버전의 IE10 프리뷰 릴리스와의 호환성을 위해 접두사가 붙은 버전도 유지됩니다. 버블 데모는 이 API를 사용하며, 이 API를 사용할 수 없는 경우 setTimeout으로 돌아갑니다.

Demo.prototype.requestAnimationFrame = function () {

var that = this;

if (window.requestAnimationFrame)

this.animationFrameTimer =

window.requestAnimationFrame(function () { that.onAnimationFrame(); });

else

this.animationFrameTimer =

setTimeout(function () { that.onAnimationFrame(); }, this.animationFrameDuration);

}

 

Demo.prototype.cancelAnimationFrame = function () {

if (window.cancelRequestAnimationFrame)

window.cancelRequestAnimationFrame(this.animationFrameTimer);

else

clearTimeout(this.animationFrameTimer);

}

DOM 값을 문자열에서 숫자로 변환

JavaScript는 매우 유용하며 다양한 형식 값을 광범위하게 자동으로 변환하는 기능이 포함되어 있습니다. 예를 들어 산술 연산에서 문자열 값이 사용될 경우 자동으로 숫자 값으로 변환됩니다. 오늘날 브라우저에서 이와 같은 편리한 기능은 성능을 심각하게 저하시킬 수 있습니다. 최신 JavaScript 컴파일러에서 생성하는 형식에 특화된 코드는 알려진 형식 값에 대한 산술에서는 매우 유용하지만 예상치 못한 형식 값을 처리할 때는 엄청난 오버헤드가 발생합니다.

버블 데모가 로드되면 numberOfBubbles 속성이 값 100부터 시작됩니다. 각 애니메이션 프레임에서 각 버블의 위치가 조정됩니다.

function Demo() {

this.numberOfBubbles = 100;

//...

}

 

Demo.prototype.moveBubbles = function(elapsedTime) {

for (var i = 0; i < this.numberOfBubbles; i++) {

this.bubbles[i].move(elapsedTime, this.gravity);

}

}

사용자가 UI에서 다른 값을 선택한 경우 numberOfBubbles 속성 값을 적절히 조정해야 합니다. 간단한 이벤트 처리기는 다음과 같이 처리할 수 있습니다.

Demo.prototype.onNumberOfBubblesChange = function () {

this.numberOfBubbles = document.getElementById("numberOfBubblesSelector").value;

//...

}

정상적으로 사용자 입력을 읽는 것처럼 보이는 이러한 방법도 데모의 JavaScript 부분에서 약 10%의 오버헤드를 발생시킵니다. 드롭다운에서 가져와 numberOfBubbles에 할당된 값이 숫자가 아닌 문자열이므로 애니메이션의 각 프레임에서 moveBubbles 루프의 모든 반복에 대해 이 값을 숫자로 변환해야 합니다.

이는 DOM에서 추출된 값을 산술 연산에서 사용하기 전에 명시적으로 변환하기 위한 좋은 방법임을 나타냅니다. DOM 속성 값은 일반적으로 JavaScript의 문자열 형식이며 문자열-숫자 자동 변환을 반복해서 수행할 경우 비용이 상당히 많이 들 수 있습니다. 데모에 나와 있는 것처럼 사용자 선택에 따라 numberOfBubbles를 업데이트하는 더 좋은 방법은 다음과 같습니다.

Demo.prototype.onNumberOfBubblesChange = function () {

this.numberOfBubbles = parseInt(document.getElementById("numberOfBubblesSelector").value);

//...

}

ES5 접근자 속성 작업

ECMAScript 5 접근자 속성은 데이터 캡슐화, 계산된 속성, 데이터 유효성 검사 또는 변경 알림을 위한 간편한 메커니즘입니다. 버블 데모에서는 버블이 팽창할 때마다 반경이 조정되고, 버블의 이미지 크기를 조정해야 함을 알리는 계산된 radiusChanged 속성이 설정됩니다.

Object.defineProperties(Bubble.prototype, {

//...

radius: {

get: function () {

return this.mRadius;

},

set: function (value) {

if (this.mRadius != value) {

this.mRadius = value;

this.mRadiusChanged = true;

}

}

},

//...

});

모든 브라우저에서 접근자 속성은 데이터 속성에 비해 오버헤드가 많이 발생합니다. 오버헤드의 정확한 크기는 브라우저마다 다릅니다.

Canvas ImageData 액세스 최소화

중요한 성능 경로의 루프에서 DOM 호출 횟수를 최소화할 수 있는 안정적인 방법입니다. 예를 들어 버블 데모가 아래와 같이 문서에서 해당하는 요소를 검색하여 각 버블의 위치를 업데이트한 경우 성능에 부정적인 영향을 줄 수 있습니다.

Bubble.prototype.render = function () {

document.getElementById("bubble" + this.id).style.left = Math.round(this.x) + "px";

document.getElementById("bubble" + this.id).style.top = Math.round(this.y) + "px";

this.updateScale();

}

대신에 각 버블에 해당하는 요소를 JavaScript의 bubble 개체에 한 번 캐시한 다음 각 애니메이션 프레임에서 직접 액세스합니다.

Bubble.prototype.render = function () {

this.element.style.left = Math.round(this.x) + "px";

this.element.style.top = Math.round(this.y) + "px";

this.updateScale();

}

불확실한 점은 <canvas>로 작업할 때 유사한 오버헤드가 발생하지 않도록 주의를 기울여야 한다는 점입니다. canvas.getContext("2D").getImageData()를 호출하여 가져온 개체도 마찬가지로 DOM 개체입니다. Canvas에 버블 충돌 효과를 그리려면 데모에 아래 코드를 사용할 수 있습니다. 이 버전에서는 루프를 반복할 때마다 imgData.data를 읽으므로 DOM 호출이 필요하고 엄청난 오버헤드가 추가됩니다.

BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {

var imgData = this.canvasContext.getImageData(/*...*/)

//...

for (var my = myMin; my <= myMax; my++) {

for (var mx = mxMin; mx <= mxMax; mx++) {

var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;

imgData.data[4 * i + 3] = 255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];

}

}

this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);

}

<canvas> 이미지 데이터를 업데이트하기 위한 더 나은 방법은 다음 코드 조각에서처럼 data 속성을 캐시하는 것입니다. data 속성은 형식 배열(PixelArray)이고 JavaScript에서 매우 효율적으로 액세스할 수 있습니다.

BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {

var imgData = this.canvasContext.getImageData(/*...*/)

var imgColorComponents = imgData.data;

//...

for (var my = myMin; my <= myMax; my++) {

for (var mx = mxMin; mx <= mxMax; mx++) {

var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;

imgColorComponents[4 * i + 3] =

255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];

}

}

this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);

}

형식 배열을 사용하여 부동 소수점 숫자 저장

IE10에는 형식 배열에 대한 지원이 추가되었습니다. 부동 소수점 숫자로 작업할 때는 JavaScript 배열(Array) 대신 형식 배열(Float32Array 또는 Float64Array)을 사용하는 것이 좋습니다. JavaScript 배열은 모든 종류의 요소를 포함할 수 있지만 일반적으로 부동 소수점 값을 배열에 추가하기 전에 힙(박싱)에 할당해야 합니다. 이는 성능에 영향을 줍니다. 오늘날의 브라우저에서 높은 성능을 일관적으로 유지하려면 부동 소수점 값을 저장하는 의도를 나타내는 Float32Array 또는 Float64Array를 사용해야 합니다. 이렇게 하면 JavaScript 엔진이 힙 박싱을 피하고 다른 컴파일러 최적화(예: 형식에 특화된 연산)를 활성화할 수 있습니다.

BubbleTank.prototype.generateOcclusionMask = function() {

if (typeof Float64Array != "undefined") {

this.occlusionMask = new Float64Array(this.canvasWidth * this.canvasHeight);

} else {

this.occlusionMask = new Array(this.canvasWidth * this.canvasHeight);

}

this.resetOcclusionMask();

}

위의 예에서는 버블 데모에서 Float64Array를 사용하여 Canvas에 적용된 배경 이미지를 숨기는 폐색 마스크를 업데이트하는 방법을 보여 줍니다. 브라우저에서 형식 배열을 지원하지 않을 경우 코드가 일반 배열로 돌아갑니다. 버블 데모에서 형식 배열을 사용할 경우 얻을 수 있는 이점은 설정에 따라 다릅니다. IE10의 중간 크기 창에서 형식 배열은 전반적인 프레임 속도를 약 10% 개선합니다.

요약

이 글에서는 새로 제작한 버블 데모에 대해 살펴보고 IE10 Release Preview에서 JavaScript를 실행할 경우 얻을 수 있는 놀라운 이점을 활용하는 방법을 알아보았습니다. 또한 애니메이션 기반 응용 프로그램에서 우수한 성능을 구현할 수 있는 몇 가지 중요한 방법을 설명했습니다. IE10 Release Preview의 JavaScript Runtime(Chakra) 변경 사항에 대한 자세한 기술적인 내용은 이전 글을 참조하십시오. 앞으로 더 많은 개발자들이 IE10 Release Preview의 향상된 성능과 새 기능을 통해 웹 표준 및 기술을 사용하여 훨씬 더 멋진 응용 프로그램을 제작할 수 있기를 바랍니다.

- JavaScript 프로그램 관리자, Andrew Miadowicz