2
0

cleanup.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <!DOCTYPE html><html lang="ko"><head>
  2. <meta charset="utf-8">
  3. <title>메모리 해제하기</title>
  4. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  5. <meta name="twitter:card" content="summary_large_image">
  6. <meta name="twitter:site" content="@threejs">
  7. <meta name="twitter:title" content="Three.js – 메모리 해제하기">
  8. <meta property="og:image" content="https://threejs.org/files/share.png">
  9. <link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  10. <link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)">
  11. <link rel="stylesheet" href="/manual/resources/lesson.css">
  12. <link rel="stylesheet" href="/manual/resources/lang.css">
  13. <!-- Import maps polyfill -->
  14. <!-- Remove this when import maps will be widely supported -->
  15. <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
  16. <script type="importmap">
  17. {
  18. "imports": {
  19. "three": "../../build/three.module.js"
  20. }
  21. }
  22. </script>
  23. <link rel="stylesheet" href="/manual/ko/lang.css">
  24. </head>
  25. <body>
  26. <div class="container">
  27. <div class="lesson-title">
  28. <h1>메모리 해제하기</h1>
  29. </div>
  30. <div class="lesson">
  31. <div class="lesson-main">
  32. <p>Three.js 앱은 생각보다 많은 메모리를 사용합니다. 3D 모델의 정점 데이터는 보통 1MB에서 20MB 정도의 메모리를 차지하죠. 텍스처로 JPG 파일을 사용하는 모델은 텍스처를 사용하기 위해 JPG 파일의 압축을 완전히 풀어야 하는데, 이 텍스처는 1024x1024당 약 4에서 6MB 정도의 메모리를 사용합니다.</p>
  33. <p>대다수의 Three.js 앱은 자원을 한 번 불러오면 페이지가 닫히기 전까지 해당 자원을 버릴 일이 없습니다. 하지만 시간이 지남에 따라 데이터를 바꿔야 한다면 어떨까요?</p>
  34. <p>Three.js는 자바스크립트와 달리 할당한 메모리를 알아서 비우지 못합니다. 페이지를 전환하는 경우야 브라우저가 알아서 해당 자원을 메모리에서 지우겠지만, 그 밖의 경우 메모리 해제는 전적으로 개발자에게 달렸습니다.</p>
  35. <p>Three.js에서는 <a href="textures.html">textures</a>, <a href="primitives.html">geometries</a>, <a href="materials.html">materials</a>의 <code class="notranslate" translate="no">dispose</code> 메서드를 호출해 메모리를 해제할 수 있습니다.</p>
  36. <p>가장 간단한 방법은 일일이 호출하는 겁니다. 초기화 시에 아래와 같이 지원을 메모리에 할당하고</p>
  37. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxGeometry = new THREE.BoxGeometry(...);
  38. const boxTexture = textureLoader.load(...);
  39. const boxMaterial = new THREE.MeshPhongMaterial({ map: texture });
  40. </pre>
  41. <p>아래와 같이 직접 메서드를 호출해 메모리를 해제할 수 있죠.</p>
  42. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">boxGeometry.dispose();
  43. boxTexture.dispose();
  44. boxMaterial.dispose();
  45. </pre>
  46. <p>하지만 자원이 많아질수록 코드는 지저분해질 겁니다.</p>
  47. <p>자원을 추적하는 클래스를 하나 만드는 게 좋겠네요. 클래스에 자원을 지정하고 한 번에 버리도록 해보겠습니다.</p>
  48. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  49. constructor() {
  50. this.resources = new Set();
  51. }
  52. track(resource) {
  53. if (resource.dispose) {
  54. this.resources.add(resource);
  55. }
  56. return resource;
  57. }
  58. untrack(resource) {
  59. this.resources.delete(resource);
  60. }
  61. dispose() {
  62. for (const resource of this.resources) {
  63. resource.dispose();
  64. }
  65. this.resources.clear();
  66. }
  67. }
  68. </pre>
  69. <p><a href="textures.html">텍스처에 관한 글</a>의 첫 번째 예제에 이 클래스를 써 봅시다. 먼저 클래스의 인스턴스를 만듭니다.</p>
  70. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const resTracker = new ResourceTracker();
  71. </pre>
  72. <p>좀 더 간단한 형태로 쓰기 위해 <code class="notranslate" translate="no">track</code> 메서드를 함수로 만듭니다.</p>
  73. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const resTracker = new ResourceTracker();
  74. +const track = resTracker.track.bind(resTracker);
  75. </pre>
  76. <p>그리고 각 geometry, 텍스처, 재질(material)에 <code class="notranslate" translate="no">track</code> 함수를 호출합니다.</p>
  77. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxWidth = 1;
  78. const boxHeight = 1;
  79. const boxDepth = 1;
  80. -const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  81. +const geometry = track(new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth));
  82. const cubes = []; // 정육면체를 회전시키기 위한 배열
  83. const loader = new THREE.TextureLoader();
  84. -const material = new THREE.MeshBasicMaterial({
  85. - map: loader.load('resources/images/wall.jpg'),
  86. -});
  87. +const material = track(new THREE.MeshBasicMaterial({
  88. + map: track(loader.load('resources/images/wall.jpg')),
  89. +}));
  90. const cube = new THREE.Mesh(geometry, material);
  91. scene.add(cube);
  92. cubes.push(cube); // 회전 애니메이션을 위해 배열에 추가
  93. </pre>
  94. <p>자원을 해제할 때 정육면체를 장면에서 제거하고 <code class="notranslate" translate="no">resTracker.dispose</code> 메서드를 호출하도록 합니다.</p>
  95. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (const cube of cubes) {
  96. scene.remove(cube);
  97. }
  98. cubes.length = 0; // 정육면체 배열을 비웁니다
  99. resTracker.dispose();
  100. </pre>
  101. <p>하지만 실제로 테스트해보니 귀찮은 작업을 추가해야 합니다. <code class="notranslate" translate="no">ResourceTracker</code>에 코드를 추가하겠습니다.</p>
  102. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  103. constructor() {
  104. this.resources = new Set();
  105. }
  106. track(resource) {
  107. - if (resource.dispose) {
  108. + if (resource.dispose || resource instanceof THREE.Object3D) {
  109. this.resources.add(resource);
  110. }
  111. return resource;
  112. }
  113. untrack(resource) {
  114. this.resources.delete(resource);
  115. }
  116. dispose() {
  117. for (const resource of this.resources) {
  118. - resource.dispose();
  119. + if (resource instanceof THREE.Object3D) {
  120. + if (resource.parent) {
  121. + resource.parent.remove(resource);
  122. + }
  123. + }
  124. + if (resource.dispose) {
  125. + resource.dispose();
  126. + }
  127. + }
  128. this.resources.clear();
  129. }
  130. }
  131. </pre>
  132. <p>이제 정육면체를 추적할 수 있습니다.</p>
  133. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const material = track(new THREE.MeshBasicMaterial({
  134. map: track(loader.load('resources/images/wall.jpg')),
  135. }));
  136. const cube = track(new THREE.Mesh(geometry, material));
  137. scene.add(cube);
  138. cubes.push(cube); // 회전 애니메이션을 위해 배열에 추가
  139. </pre>
  140. <p>별도로 정육면체를 제거해야할 필요가 없으니 코드를 삭제합니다.</p>
  141. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-for (const cube of cubes) {
  142. - scene.remove(cube);
  143. -}
  144. cubes.length = 0; // 정육면체 배열을 비웁니다
  145. resTracker.dispose();
  146. </pre>
  147. <p>코드를 정리해 정육면체, 텍스처, 재질을 다시 추가할 수 있도록 만들고</p>
  148. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  149. *const cubes = []; // 정육면체를 회전시키기 위한 배열
  150. +function addStuffToScene() {
  151. const resTracker = new ResourceTracker();
  152. const track = resTracker.track.bind(resTracker);
  153. const boxWidth = 1;
  154. const boxHeight = 1;
  155. const boxDepth = 1;
  156. const geometry = track(new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth));
  157. const loader = new THREE.TextureLoader();
  158. const material = track(new THREE.MeshBasicMaterial({
  159. map: track(loader.load('resources/images/wall.jpg')),
  160. }));
  161. const cube = track(new THREE.Mesh(geometry, material));
  162. scene.add(cube);
  163. cubes.push(cube); // 회전 애니메이션을 위해 배열에 추가
  164. + return resTracker;
  165. +}
  166. </pre>
  167. <p>시간에 지남에 따라 물체들을 사라지고 나타나게 합니다.</p>
  168. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function waitSeconds(seconds = 0) {
  169. return new Promise(resolve =&gt; setTimeout(resolve, seconds * 1000));
  170. }
  171. async function process() {
  172. for (;;) {
  173. const resTracker = addStuffToScene();
  174. await wait(2);
  175. cubes.length = 0; // 정육면체 배열을 비웁니다
  176. resTracker.dispose();
  177. await wait(1);
  178. }
  179. }
  180. process();
  181. </pre>
  182. <p>아래 예제는 정육면체, 텍스처, 재질을 렌더링한 뒤 2초 후에 해당 자원을 버리고, 다시 1초 후에 생성하기를 반복합니다.</p>
  183. <p></p><div translate="no" class="threejs_example_container notranslate">
  184. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cleanup-simple.html"></iframe></div>
  185. <a class="threejs_center" href="/manual/examples/cleanup-simple.html" target="_blank">새 탭에서 보기</a>
  186. </div>
  187. <p></p>
  188. <p>딱히 오류는 없네요.</p>
  189. <p>불러온 파일을 해제하려면 코드를 좀 더 추가해야 합니다. Three.js의 로더는 대부분 최상위 <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>만을 반환하기에 어떤 자원을 사용했는지 체크하려면 일일이 하위 요소를 뒤져봐야 합니다.</p>
  190. <p><code class="notranslate" translate="no">ResourceTracker</code>를 업데이트해 저 역할을 맡겨보죠.</p>
  191. <p>먼저 자원이 <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>인지 확인해 해당 요소의 geometry, 재질, 하위 요소를 추적하도록 합니다.</p>
  192. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  193. constructor() {
  194. this.resources = new Set();
  195. }
  196. track(resource) {
  197. if (resource.dispose || resource instanceof THREE.Object3D) {
  198. this.resources.add(resource);
  199. }
  200. + if (resource instanceof THREE.Object3D) {
  201. + this.track(resource.geometry);
  202. + this.track(resource.material);
  203. + this.track(resource.children);
  204. + }
  205. return resource;
  206. }
  207. ...
  208. }
  209. </pre>
  210. <p>그리고 <code class="notranslate" translate="no">resource.geometry</code>, <code class="notranslate" translate="no">resource.material</code>, <code class="notranslate" translate="no">resource.children</code>이 null이나 undefined일 수 있으므로 <code class="notranslate" translate="no">track</code> 메서드 상단에서 체크해줍니다.</p>
  211. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  212. constructor() {
  213. this.resources = new Set();
  214. }
  215. track(resource) {
  216. + if (!resource) {
  217. + return resource;
  218. + }
  219. if (resource.dispose || resource instanceof THREE.Object3D) {
  220. this.resources.add(resource);
  221. }
  222. if (resource instanceof THREE.Object3D) {
  223. this.track(resource.geometry);
  224. this.track(resource.material);
  225. this.track(resource.children);
  226. }
  227. return resource;
  228. }
  229. ...
  230. }
  231. </pre>
  232. <p><code class="notranslate" translate="no">resource.children</code>이나 <code class="notranslate" translate="no">resource.material</code>은 배열 형식일 수 있습니다. 배열일 경우 배열의 요소를 추적하도록 합니다.</p>
  233. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  234. constructor() {
  235. this.resources = new Set();
  236. }
  237. track(resource) {
  238. if (!resource) {
  239. return resource;
  240. }
  241. + // 하위 요소 또는 재질이 배열일 경우
  242. + if (Array.isArray(resource)) {
  243. + resource.forEach(resource =&gt; this.track(resource));
  244. + return resource;
  245. + }
  246. if (resource.dispose || resource instanceof THREE.Object3D) {
  247. this.resources.add(resource);
  248. }
  249. if (resource instanceof THREE.Object3D) {
  250. this.track(resource.geometry);
  251. this.track(resource.material);
  252. this.track(resource.children);
  253. }
  254. return resource;
  255. }
  256. ...
  257. }
  258. </pre>
  259. <p>그리고 재질의 속성 중 텍스처와 균등 변수(uniform)를 처리해줍니다.</p>
  260. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  261. constructor() {
  262. this.resources = new Set();
  263. }
  264. track(resource) {
  265. if (!resource) {
  266. return resource;
  267. }
  268. * // 하위 요소 또는 재질이 배열일 경우,
  269. * // 균등 변수가 텍스처 배열일 경우
  270. if (Array.isArray(resource)) {
  271. resource.forEach(resource =&gt; this.track(resource));
  272. return resource;
  273. }
  274. if (resource.dispose || resource instanceof THREE.Object3D) {
  275. this.resources.add(resource);
  276. }
  277. if (resource instanceof THREE.Object3D) {
  278. this.track(resource.geometry);
  279. this.track(resource.material);
  280. this.track(resource.children);
  281. - }
  282. + } else if (resource instanceof THREE.Material) {
  283. + // 재질에 텍스처가 있는지 검사해 추적합니다.
  284. + for (const value of Object.values(resource)) {
  285. + if (value instanceof THREE.Texture) {
  286. + this.track(value);
  287. + }
  288. + }
  289. + // 균등 변수가 텍스처 또는 텍스처의 배열인지 체크합니다.
  290. + if (resource.uniforms) {
  291. + for (const value of Object.values(resource.uniforms)) {
  292. + if (value) {
  293. + const uniformValue = value.value;
  294. + if (uniformValue instanceof THREE.Texture ||
  295. + Array.isArray(uniformValue)) {
  296. + this.track(uniformValue);
  297. + }
  298. + }
  299. + }
  300. + }
  301. + }
  302. return resource;
  303. }
  304. ...
  305. }
  306. </pre>
  307. <p>수정한 클래스를 <a href="load-gltf.html">glTF 파일 불러오기</a>에서 썼던 예제에 적용해 무료 glTF 파일을 불러와보도록 합시다.</p>
  308. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gltfLoader = new GLTFLoader();
  309. function loadGLTF(url) {
  310. return new Promise((resolve, reject) =&gt; {
  311. gltfLoader.load(url, resolve, undefined, reject);
  312. });
  313. }
  314. function waitSeconds(seconds = 0) {
  315. return new Promise(resolve =&gt; setTimeout(resolve, seconds * 1000));
  316. }
  317. const fileURLs = [
  318. 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf',
  319. 'resources/models/3dbustchallange_submission/scene.gltf',
  320. 'resources/models/mountain_landscape/scene.gltf',
  321. 'resources/models/simple_house_scene/scene.gltf',
  322. ];
  323. async function loadFiles() {
  324. for (;;) {
  325. for (const url of fileURLs) {
  326. const resMgr = new ResourceTracker();
  327. const track = resMgr.track.bind(resMgr);
  328. const gltf = await loadGLTF(url);
  329. const root = track(gltf.scene);
  330. scene.add(root);
  331. // 해당 요소의 모든 하위 물체를 포함하는 육면체를 계산합니다.
  332. const box = new THREE.Box3().setFromObject(root);
  333. const boxSize = box.getSize(new THREE.Vector3()).length();
  334. const boxCenter = box.getCenter(new THREE.Vector3());
  335. // 카메라가 화면을 전부 담도록 설정합니다.
  336. frameArea(boxSize * 1.1, boxSize, boxCenter, camera);
  337. await waitSeconds(2);
  338. renderer.render(scene, camera);
  339. resMgr.dispose();
  340. await waitSeconds(1);
  341. }
  342. }
  343. }
  344. loadFiles();
  345. </pre>
  346. <p></p><div translate="no" class="threejs_example_container notranslate">
  347. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cleanup-loaded-files.html"></iframe></div>
  348. <a class="threejs_center" href="/manual/examples/cleanup-loaded-files.html" target="_blank">새 탭에서 보기</a>
  349. </div>
  350. <p></p>
  351. <p>코드에 대해 몇 가지 설명하고 끝내겠습니다.</p>
  352. <p>만약 2개 이상의 파일을 한 번에 불러오고 나중에 따로 해제하려면 <code class="notranslate" translate="no">ResourceTracker</code>를 파일별로 생성하면 됩니다.</p>
  353. <p>위 예제에서는 <code class="notranslate" translate="no">gltf.scene</code>에만 <code class="notranslate" translate="no">track</code> 메서드를 사용했습니다. 이것만으로 지금 <code class="notranslate" translate="no">ResourceTracker</code>는 포함된 모든 요소를 추적하겠죠. 화면에 뭔가를 더 추가하려면 해당 자원을 추적할지 말지를 먼저 결정해야 합니다.</p>
  354. <p>특정 도구를 캐릭터의 자식 요소로 추가해 손에 쥐어 주는 경우를 예로 들 수 있습니다. 그냥 도구만 추가해서는 해당 요소를 추적할 수 없을 테니까요. 모르긴 해도 꽤나 흔한 경우일 거라 생각합니다.</p>
  355. <p>처음에 <code class="notranslate" translate="no">ResourceTracker</code>를 작성했을 때는 모든 것을 <code class="notranslate" translate="no">track</code>이 아니라 <code class="notranslate" translate="no">dispose</code> 메서드 안에서 해결하려고 했습니다. 하지만 캐릭터의 손에 도구를 쥐어 주는 경우를 생각해보니 <code class="notranslate" translate="no">track</code>을 통해 등록한 자원을 해제하는 게 확장성 면에서도 그렇고 더 나은 방법 같더군요. 씬 그래프 전체를 해제시키는 것보다는 불러온 파일만 해제시키는 게 나을 테니까요.</p>
  356. <p><code class="notranslate" translate="no">ResourceTracker</code>를 만들긴 했지만 100% 만족스럽진 않습니다. 3D 엔진에서 자원을 이런 식으로 관리하는 건 흔한 일이 아니거든요. 어떤 자원이 올라올지 추측하는 게 아니라 미리 알고 있어야 하는 쪽이 맞습니다. Three.js의 파일 로더가 불러온 자원의 주소값을 전부 반환하도록 바뀐다면 좋겠지만, 지금은 장면(scene)을 불러올 때 다른 선택지가 없기에 이 해결책이 최선이겠죠.</p>
  357. <p>이 예시가 Three.js에서 자원을 해제하는 데 조금이나마 도움이 되었으면 합니다.</p>
  358. </div>
  359. </div>
  360. </div>
  361. <script src="/manual/resources/prettify.js"></script>
  362. <script src="/manual/resources/lesson.js"></script>
  363. </body></html>