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
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) => {
|
||
|
- 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>
|