You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

232 lines
13 KiB

2 years ago
<!DOCTYPE html><html lang="ko"><head>
<meta charset="utf-8">
<title>불필요한 렌더링 없애기</title>
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@threejs">
<meta name="twitter:title" content="Three.js – 불필요한 렌더링 없애기">
<meta property="og:image" content="https://threejs.org/files/share.png">
<link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)">
<link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="/manual/resources/lesson.css">
<link rel="stylesheet" href="/manual/resources/lang.css">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "../../build/three.module.js"
}
}
</script>
<link rel="stylesheet" href="/manual/ko/lang.css">
</head>
<body>
<div class="container">
<div class="lesson-title">
<h1>불필요한 렌더링 없애기</h1>
</div>
<div class="lesson">
<div class="lesson-main">
<p>대부분의 개발자에게 이 주제는 너무 뻔할 수 있지만, 필요한 누군가를 위해
글을 써보려 합니다. 대부분의 Three.js 예제는 렌더링 과정을 계속 반복합니다.
그러니까 아래와 같이 재귀적으로 <code class="notranslate" translate="no">requestAnimationFrame</code> 함수를 사용한다는
거죠.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() {
...
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</pre>
<p>계속 애니메이션이 있는 경우에야 별 상관이 없지만, 애니메이션이 없는 경우라면
어떨까요? 이 경우 불필요한 렌더링을 반복하는 것은 연산 낭비일 뿐더러 사용
환경이 모바일이라면 사용자의 배터리까지 낭비하는 셈입니다.</p>
<p>처음 한 번만 렌더링하고, 그 후에 변화가 있을 때만 렌더링하는 것이 가장 정확한
해결책일 겁니다. 여기서 변화란 텍스처나 모델의 로딩이 끝났을 때, 외부에서
데이터를 받았을 때, 사용자가 카메라를 조정하거나, 설정을 바꾸거나, 인풋 값이
변경된 경우 등 다양하겠죠.</p>
<p><a href="responsive.html">반응형 디자인에 관한 글</a>에서 썼던 예제를 수정해
필요에 따른 렌더링을 구현해봅시다.</p>
<p>먼저 뭔가 변화를 일으킬 수 있는 요소가 필요하니 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>를 추가합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
...
const fov = 75;
const aspect = 2; // canvas 기본값
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
+const controls = new OrbitControls(camera, canvas);
+controls.target.set(0, 0, 0);
+controls.update();
</pre>
<p>정육면체에 애니메이션을 넣지 않을 것이니 이들을 참조할 필요가 없습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const cubes = [
- makeInstance(geometry, 0x44aa88, 0),
- makeInstance(geometry, 0x8844aa, -2),
- makeInstance(geometry, 0xaa8844, 2),
-];
+makeInstance(geometry, 0x44aa88, 0);
+makeInstance(geometry, 0x8844aa, -2);
+makeInstance(geometry, 0xaa8844, 2);
</pre>
<p>애니메이션과 <code class="notranslate" translate="no">requestAnimationFrame</code> 관련 코드도 제거합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function render(time) {
- time *= 0.001;
+function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
- cubes.forEach((cube, ndx) =&gt; {
- const speed = 1 + ndx * .1;
- const rot = time * speed;
- cube.rotation.x = rot;
- cube.rotation.y = rot;
- });
renderer.render(scene, camera);
- requestAnimationFrame(render);
}
-requestAnimationFrame(render);
</pre>
<p>그리고 <code class="notranslate" translate="no">render</code> 함수를 직접 호출합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">render();
</pre>
<p>이제 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>가 카메라 설정을 바꿀 때마다 직접 <code class="notranslate" translate="no">render</code> 함수를 호출해야
합니다. 뭔가 복잡할 것 같지만 다행히 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>에는 <code class="notranslate" translate="no">change</code> 이벤트가 있습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">controls.addEventListener('change', render);
</pre>
<p>또한 창 크기가 바뀔 때의 동작도 직접 처리해야 합니다. <code class="notranslate" translate="no">render</code> 함수를 계속 호출할
때는 해당 동작을 자동으로 처리했지만, 지금은 <code class="notranslate" translate="no">render</code> 함수를 수동으로 호출하므로
창의 크기가 바뀔 때 <code class="notranslate" translate="no">render</code> 함수를 호출하도록 하겠습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">window.addEventListener('resize', render);
</pre>
<p>이제 불필요한 렌더링을 반복하지 않습니다.</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/render-on-demand.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/render-on-demand.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p><a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>에는 관성(inertia) 옵션이 있습니다. <code class="notranslate" translate="no">enableDamping</code> 속성을 ture로
설정하면 동작이 좀 더 부드러워지죠.</p>
<p>※ damping: 감쇠.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">controls.enableDamping = true;
</pre>
<p>또한 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>가 부드러운 동작을 구현할 때 변경된 카메라 값을 계속 넘겨주도록
<code class="notranslate" translate="no">render</code> 함수 안에서 <code class="notranslate" translate="no">controls.update</code> 메서드를 호출해야 합니다. 하지만 이렇게 하면
<code class="notranslate" translate="no">change</code> 이벤트가 발생했을 때 <code class="notranslate" translate="no">render</code> 함수가 무한정 호출될 겁니다. controls가 <code class="notranslate" translate="no">change</code>
이벤트를 보내면 <code class="notranslate" translate="no">render</code> 함수가 호출되고, <code class="notranslate" translate="no">render</code> 함수는 <code class="notranslate" translate="no">controls.update</code> 메서드를
호출해 다시 <code class="notranslate" translate="no">change</code> 이벤트를 보내게 만들 테니까요.</p>
<p><code class="notranslate" translate="no">requestAnimationFrame</code>이 직접 <code class="notranslate" translate="no">render</code> 함수를 호출하게 하면 이 문제를 해결 할 수
있습니다. 너무 많은 프레임을 막기 위해 변수 하나를 두어 요청한 프레임이 없을 경우에만
프레임을 요청하도록 하면 되겠네요.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+let renderRequested = false;
function render() {
+ renderRequested = false;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
+ controls.update();
renderer.render(scene, camera);
}
render();
+function requestRenderIfNotRequested() {
+ if (!renderRequested) {
+ renderRequested = true;
+ requestAnimationFrame(render);
+ }
+}
-controls.addEventListener('change', render);
+controls.addEventListener('change', requestRenderIfNotRequested);
</pre>
<p>창 크기 변화가 일어났을 때도 <code class="notranslate" translate="no">requestRenderIfNotRequested</code>를 호출하도록 합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-window.addEventListener('resize', render);
+window.addEventListener('resize', requestRenderIfNotRequested);
</pre>
<p>차이점을 느끼기 어려울지도 모르겠습니다. 화살표 키를 쓰거나 예제를 드래그해 보고
다시 위 예제를 이리저리 돌려보세요. 차이점이 느껴질 거예요. 위 예제는 화살표 키를
눌렀을 때 일정 거리만큼 순간이동하지만 아래의 예제는 약간 미끄러집니다.</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/render-on-demand-w-damping.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/render-on-demand-w-damping.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>간단한 lil-gui를 추가해 반복 렌더링 여부를 제어할 수 있도록 하겠습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
</pre>
<p>먼저 각 정육면체의 색과 x축 스케일을 조정하는 GUI를 추가합니다. <a href="lights.html">조명에 관한 글</a>에서
썼던 <code class="notranslate" translate="no">ColorGUIHelper</code>를 가져와 쓰도록 하죠.</p>
<p>먼저 GUI를 생성합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
</pre>
<p>그리고 각 정육면체에 <code class="notranslate" translate="no">material.color</code>, <code class="notranslate" translate="no">cube.scale.x</code> 설정을 폴더로 묶어
추가합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeInstance(geometry, color, x) {
const material = new THREE.MeshPhongMaterial({color});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
cube.position.x = x;
+ const folder = gui.addFolder(`Cube${ x }`);
+ folder.addColor(new ColorGUIHelper(material, 'color'), 'value')
+ .name('color')
+ .onChange(requestRenderIfNotRequested);
+ folder.add(cube.scale, 'x', .1, 1.5)
+ .name('scale x')
+ .onChange(requestRenderIfNotRequested);
+ folder.open();
return cube;
}
</pre>
<p>lil-gui 컨트롤(control)의 <code class="notranslate" translate="no">onChange</code> 메서드에 콜백 함수를 넘겨주면 GUI 값이 바뀔
때마다 콜백 함수를 호출합니다. 예제의 경우에는 단순히 <code class="notranslate" translate="no">requestRenderIfNotRequested</code>
함수를 넘겨주면 되죠. 그리고 <code class="notranslate" translate="no">folder.open</code> 메서드를 호출해 폴더를 열어 둡니다.</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/render-on-demand-w-gui.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/render-on-demand-w-gui.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>이 글이 불필요한 렌더링 제거에 대한 개념을 조금이라도 잡아주었길 바랍니다. 보통
Three.js를 사용할 때는 이렇게 렌더링 루프를 제어할 일이 없습니다. 대게 게임 또는
애니메이션이 들어간 3D 컨텐츠이기 때문이죠. 하지만 지도나, 3D 에디터, 3D 그래프,
상품 목록 등에서는 이런 기법이 필요할 수도 있습니다.</p>
</div>
</div>
</div>
<script src="/manual/resources/prettify.js"></script>
<script src="/manual/resources/lesson.js"></script>
</body></html>