cleanup.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>Cleanup</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 – Cleanup">
  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. </head>
  24. <body>
  25. <div class="container">
  26. <div class="lesson-title">
  27. <h1>Cleanup</h1>
  28. </div>
  29. <div class="lesson">
  30. <div class="lesson-main">
  31. <p>Three.js apps often use lots of memory. A 3D model
  32. might be 1 to 20 meg memory for all of its vertices.
  33. A model might use many textures that even if they are
  34. compressed into jpg files they have to be expanded
  35. to their uncompressed form to use. Each 1024x1024
  36. texture takes 4 to 6meg of memory.</p>
  37. <p>Most three.js apps load resources at init time and
  38. then use those resources forever until the page is
  39. closed. But, what if you want to load and change resources
  40. over time?</p>
  41. <p>Unlike most JavaScript, three.js can not automatically
  42. clean these resources up. The browser will clean them
  43. up if you switch pages but otherwise it's up to you
  44. to manage them. This is an issue of how WebGL is designed
  45. and so three.js has no recourse but to pass on the
  46. responsibility to free resources back to you.</p>
  47. <p>You free three.js resource this by calling the <code class="notranslate" translate="no">dispose</code> function on
  48. <a href="textures.html">textures</a>,
  49. <a href="primitives.html">geometries</a>, and
  50. <a href="materials.html">materials</a>.</p>
  51. <p>You could do this manually. At the start you might create
  52. some of these resources</p>
  53. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxGeometry = new THREE.BoxGeometry(...);
  54. const boxTexture = textureLoader.load(...);
  55. const boxMaterial = new THREE.MeshPhongMaterial({map: texture});
  56. </pre>
  57. <p>and then when you're done with them you'd free them</p>
  58. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">boxGeometry.dispose();
  59. boxTexture.dispose();
  60. boxMaterial.dispose();
  61. </pre>
  62. <p>As you use more and more resources that would get more and
  63. more tedious.</p>
  64. <p>To help remove some of the tedium let's make a class to track
  65. the resources. We'll then ask that class to do the cleanup
  66. for us.</p>
  67. <p>Here's a first pass at such a class</p>
  68. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  69. constructor() {
  70. this.resources = new Set();
  71. }
  72. track(resource) {
  73. if (resource.dispose) {
  74. this.resources.add(resource);
  75. }
  76. return resource;
  77. }
  78. untrack(resource) {
  79. this.resources.delete(resource);
  80. }
  81. dispose() {
  82. for (const resource of this.resources) {
  83. resource.dispose();
  84. }
  85. this.resources.clear();
  86. }
  87. }
  88. </pre>
  89. <p>Let's use this class with the first example from <a href="textures.html">the article on textures</a>.
  90. We can create an instance of this class</p>
  91. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const resTracker = new ResourceTracker();
  92. </pre>
  93. <p>and then just to make it easier to use let's create a bound function for the <code class="notranslate" translate="no">track</code> method</p>
  94. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const resTracker = new ResourceTracker();
  95. +const track = resTracker.track.bind(resTracker);
  96. </pre>
  97. <p>Now to use it we just need to call <code class="notranslate" translate="no">track</code> with for each geometry, texture, and material
  98. we create</p>
  99. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxWidth = 1;
  100. const boxHeight = 1;
  101. const boxDepth = 1;
  102. -const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  103. +const geometry = track(new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth));
  104. const cubes = []; // an array we can use to rotate the cubes
  105. const loader = new THREE.TextureLoader();
  106. -const material = new THREE.MeshBasicMaterial({
  107. - map: loader.load('resources/images/wall.jpg'),
  108. -});
  109. +const material = track(new THREE.MeshBasicMaterial({
  110. + map: track(loader.load('resources/images/wall.jpg')),
  111. +}));
  112. const cube = new THREE.Mesh(geometry, material);
  113. scene.add(cube);
  114. cubes.push(cube); // add to our list of cubes to rotate
  115. </pre>
  116. <p>And then to free them we'd want to remove the cubes from the scene
  117. and then call <code class="notranslate" translate="no">resTracker.dispose</code></p>
  118. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (const cube of cubes) {
  119. scene.remove(cube);
  120. }
  121. cubes.length = 0; // clears the cubes array
  122. resTracker.dispose();
  123. </pre>
  124. <p>That would work but I find having to remove the cubes from the
  125. scene kind of tedious. Let's add that functionality to the <code class="notranslate" translate="no">ResourceTracker</code>.</p>
  126. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  127. constructor() {
  128. this.resources = new Set();
  129. }
  130. track(resource) {
  131. - if (resource.dispose) {
  132. + if (resource.dispose || resource instanceof THREE.Object3D) {
  133. this.resources.add(resource);
  134. }
  135. return resource;
  136. }
  137. untrack(resource) {
  138. this.resources.delete(resource);
  139. }
  140. dispose() {
  141. for (const resource of this.resources) {
  142. - resource.dispose();
  143. + if (resource instanceof THREE.Object3D) {
  144. + if (resource.parent) {
  145. + resource.parent.remove(resource);
  146. + }
  147. + }
  148. + if (resource.dispose) {
  149. + resource.dispose();
  150. + }
  151. + }
  152. this.resources.clear();
  153. }
  154. }
  155. </pre>
  156. <p>And now we can track the cubes</p>
  157. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const material = track(new THREE.MeshBasicMaterial({
  158. map: track(loader.load('resources/images/wall.jpg')),
  159. }));
  160. const cube = track(new THREE.Mesh(geometry, material));
  161. scene.add(cube);
  162. cubes.push(cube); // add to our list of cubes to rotate
  163. </pre>
  164. <p>We no longer need the code to remove the cubes from the scene.</p>
  165. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-for (const cube of cubes) {
  166. - scene.remove(cube);
  167. -}
  168. cubes.length = 0; // clears the cube array
  169. resTracker.dispose();
  170. </pre>
  171. <p>Let's arrange this code so that we can re-add the cube,
  172. texture, and material.</p>
  173. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  174. *const cubes = []; // just an array we can use to rotate the cubes
  175. +function addStuffToScene() {
  176. const resTracker = new ResourceTracker();
  177. const track = resTracker.track.bind(resTracker);
  178. const boxWidth = 1;
  179. const boxHeight = 1;
  180. const boxDepth = 1;
  181. const geometry = track(new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth));
  182. const loader = new THREE.TextureLoader();
  183. const material = track(new THREE.MeshBasicMaterial({
  184. map: track(loader.load('resources/images/wall.jpg')),
  185. }));
  186. const cube = track(new THREE.Mesh(geometry, material));
  187. scene.add(cube);
  188. cubes.push(cube); // add to our list of cubes to rotate
  189. + return resTracker;
  190. +}
  191. </pre>
  192. <p>And then let's write some code to add and remove things over time.</p>
  193. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function waitSeconds(seconds = 0) {
  194. return new Promise(resolve =&gt; setTimeout(resolve, seconds * 1000));
  195. }
  196. async function process() {
  197. for (;;) {
  198. const resTracker = addStuffToScene();
  199. await wait(2);
  200. cubes.length = 0; // remove the cubes
  201. resTracker.dispose();
  202. await wait(1);
  203. }
  204. }
  205. process();
  206. </pre>
  207. <p>This code will create the cube, texture and material, wait for 2 seconds, then dispose of them and wait for 1 second
  208. and repeat.</p>
  209. <p></p><div translate="no" class="threejs_example_container notranslate">
  210. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cleanup-simple.html"></iframe></div>
  211. <a class="threejs_center" href="/manual/examples/cleanup-simple.html" target="_blank">click here to open in a separate window</a>
  212. </div>
  213. <p></p>
  214. <p>So that seems to work.</p>
  215. <p>For a loaded file though it's a little more work. Most loaders only return an <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>
  216. as a root of the hierarchy of objects they load so we need to discover what all the resources
  217. are.</p>
  218. <p>Let's update our <code class="notranslate" translate="no">ResourceTracker</code> to try to do that.</p>
  219. <p>First we'll check if the object is an <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> then track its geometry, material, and children</p>
  220. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  221. constructor() {
  222. this.resources = new Set();
  223. }
  224. track(resource) {
  225. if (resource.dispose || resource instanceof THREE.Object3D) {
  226. this.resources.add(resource);
  227. }
  228. + if (resource instanceof THREE.Object3D) {
  229. + this.track(resource.geometry);
  230. + this.track(resource.material);
  231. + this.track(resource.children);
  232. + }
  233. return resource;
  234. }
  235. ...
  236. }
  237. </pre>
  238. <p>Now, because any of <code class="notranslate" translate="no">resource.geometry</code>, <code class="notranslate" translate="no">resource.material</code>, and <code class="notranslate" translate="no">resource.children</code>
  239. might be null or undefined we'll check at the top of <code class="notranslate" translate="no">track</code>.</p>
  240. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  241. constructor() {
  242. this.resources = new Set();
  243. }
  244. track(resource) {
  245. + if (!resource) {
  246. + return resource;
  247. + }
  248. if (resource.dispose || resource instanceof THREE.Object3D) {
  249. this.resources.add(resource);
  250. }
  251. if (resource instanceof THREE.Object3D) {
  252. this.track(resource.geometry);
  253. this.track(resource.material);
  254. this.track(resource.children);
  255. }
  256. return resource;
  257. }
  258. ...
  259. }
  260. </pre>
  261. <p>Also because <code class="notranslate" translate="no">resource.children</code> is an array and because <code class="notranslate" translate="no">resource.material</code> can be
  262. an array let's check for arrays</p>
  263. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  264. constructor() {
  265. this.resources = new Set();
  266. }
  267. track(resource) {
  268. if (!resource) {
  269. return resource;
  270. }
  271. + // handle children and when material is an array of materials.
  272. + if (Array.isArray(resource)) {
  273. + resource.forEach(resource =&gt; this.track(resource));
  274. + return resource;
  275. + }
  276. if (resource.dispose || resource instanceof THREE.Object3D) {
  277. this.resources.add(resource);
  278. }
  279. if (resource instanceof THREE.Object3D) {
  280. this.track(resource.geometry);
  281. this.track(resource.material);
  282. this.track(resource.children);
  283. }
  284. return resource;
  285. }
  286. ...
  287. }
  288. </pre>
  289. <p>And finally we need to walk the properties and uniforms
  290. of a material looking for textures.</p>
  291. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  292. constructor() {
  293. this.resources = new Set();
  294. }
  295. track(resource) {
  296. if (!resource) {
  297. return resource;
  298. }
  299. * // handle children and when material is an array of materials or
  300. * // uniform is array of textures
  301. if (Array.isArray(resource)) {
  302. resource.forEach(resource =&gt; this.track(resource));
  303. return resource;
  304. }
  305. if (resource.dispose || resource instanceof THREE.Object3D) {
  306. this.resources.add(resource);
  307. }
  308. if (resource instanceof THREE.Object3D) {
  309. this.track(resource.geometry);
  310. this.track(resource.material);
  311. this.track(resource.children);
  312. - }
  313. + } else if (resource instanceof THREE.Material) {
  314. + // We have to check if there are any textures on the material
  315. + for (const value of Object.values(resource)) {
  316. + if (value instanceof THREE.Texture) {
  317. + this.track(value);
  318. + }
  319. + }
  320. + // We also have to check if any uniforms reference textures or arrays of textures
  321. + if (resource.uniforms) {
  322. + for (const value of Object.values(resource.uniforms)) {
  323. + if (value) {
  324. + const uniformValue = value.value;
  325. + if (uniformValue instanceof THREE.Texture ||
  326. + Array.isArray(uniformValue)) {
  327. + this.track(uniformValue);
  328. + }
  329. + }
  330. + }
  331. + }
  332. + }
  333. return resource;
  334. }
  335. ...
  336. }
  337. </pre>
  338. <p>And with that let's take an example from <a href="load-gltf.html">the article on loading gltf files</a>
  339. and make it load and free files.</p>
  340. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gltfLoader = new GLTFLoader();
  341. function loadGLTF(url) {
  342. return new Promise((resolve, reject) =&gt; {
  343. gltfLoader.load(url, resolve, undefined, reject);
  344. });
  345. }
  346. function waitSeconds(seconds = 0) {
  347. return new Promise(resolve =&gt; setTimeout(resolve, seconds * 1000));
  348. }
  349. const fileURLs = [
  350. 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf',
  351. 'resources/models/3dbustchallange_submission/scene.gltf',
  352. 'resources/models/mountain_landscape/scene.gltf',
  353. 'resources/models/simple_house_scene/scene.gltf',
  354. ];
  355. async function loadFiles() {
  356. for (;;) {
  357. for (const url of fileURLs) {
  358. const resMgr = new ResourceTracker();
  359. const track = resMgr.track.bind(resMgr);
  360. const gltf = await loadGLTF(url);
  361. const root = track(gltf.scene);
  362. scene.add(root);
  363. // compute the box that contains all the stuff
  364. // from root and below
  365. const box = new THREE.Box3().setFromObject(root);
  366. const boxSize = box.getSize(new THREE.Vector3()).length();
  367. const boxCenter = box.getCenter(new THREE.Vector3());
  368. // set the camera to frame the box
  369. frameArea(boxSize * 1.1, boxSize, boxCenter, camera);
  370. await waitSeconds(2);
  371. renderer.render(scene, camera);
  372. resMgr.dispose();
  373. await waitSeconds(1);
  374. }
  375. }
  376. }
  377. loadFiles();
  378. </pre>
  379. <p>and we get</p>
  380. <p></p><div translate="no" class="threejs_example_container notranslate">
  381. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cleanup-loaded-files.html"></iframe></div>
  382. <a class="threejs_center" href="/manual/examples/cleanup-loaded-files.html" target="_blank">click here to open in a separate window</a>
  383. </div>
  384. <p></p>
  385. <p>Some notes about the code.</p>
  386. <p>If we wanted to load 2 or more files at once and free them at
  387. anytime we would use one <code class="notranslate" translate="no">ResourceTracker</code> per file.</p>
  388. <p>Above we are only tracking <code class="notranslate" translate="no">gltf.scene</code> right after loading.
  389. Based on our current implementation of <code class="notranslate" translate="no">ResourceTracker</code> that
  390. will track all the resources just loaded. If we added more
  391. things to the scene we need to decide whether or not to track them.</p>
  392. <p>For example let's say after we loaded a character we put a tool
  393. in their hand by making the tool a child of their hand. As it is
  394. that tool will not be freed. I'm guessing more often than not
  395. this is what we want. </p>
  396. <p>That brings up a point. Originally when I first wrote the <code class="notranslate" translate="no">ResourceTracker</code>
  397. above I walked through everything inside the <code class="notranslate" translate="no">dispose</code> method instead of <code class="notranslate" translate="no">track</code>.
  398. It was only later as I thought about the tool as a child of hand case above
  399. that it became clear that tracking exactly what to free in <code class="notranslate" translate="no">track</code> was more
  400. flexible and arguably more correct since we could then track what was loaded
  401. from the file rather than just freeing the state of the scene graph later.</p>
  402. <p>I honestly am not 100% happy with <code class="notranslate" translate="no">ResourceTracker</code>. Doing things this
  403. way is not common in 3D engines. We shouldn't have to guess what
  404. resources were loaded, we should know. It would be nice if three.js
  405. changed so that all file loaders returned some standard object with
  406. references to all the resources loaded. At least at the moment,
  407. three.js doesn't give us any more info when loading a scene so this
  408. solution seems to work.</p>
  409. <p>I hope you find this example useful or at least a good reference for what is
  410. required to free resources in three.js</p>
  411. </div>
  412. </div>
  413. </div>
  414. <script src="/manual/resources/prettify.js"></script>
  415. <script src="/manual/resources/lesson.js"></script>
  416. </body></html>