cleanup.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <!DOCTYPE html><html lang="zh"><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 – 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="../resources/lesson.css">
  12. <link rel="stylesheet" href="../resources/lang.css">
  13. <script type="importmap">
  14. {
  15. "imports": {
  16. "three": "../../build/three.module.js"
  17. }
  18. }
  19. </script>
  20. </head>
  21. <body>
  22. <div class="container">
  23. <div class="lesson-title">
  24. <h1>清除</h1>
  25. </div>
  26. <div class="lesson">
  27. <div class="lesson-main">
  28. <p>Three.js应用经常使用大量的内存。一个3D模型的所有节点,可能占用1-20M内存。
  29. 一个模型可能会使用很多纹理,即使它们被压缩成了图片文件,也必须被展开成为未压缩的形态来使用。每个 1024 x 1024 大小的纹理会占用4-5M内存。</p>
  30. <p>大多数的three.js应用在初始化的时候加载资源,并且一直使用这些资源直到页面关闭。但是,如果你想随时间的变动加载和改变资源怎么办呢?</p>
  31. <p>不像大多数的JavaScript库,three.js不能自动的清除这些资源。
  32. 如果你切换页面,浏览器会清除这些资源,其它时候如何管理它们取决于你。这是WebGL设计的问题,three.js没有追索权只能将释放资源的责任托付给你。</p>
  33. <p>通过在<a href="textures.html">纹理</a>、
  34. <a href="primitives.html">图元</a>和
  35. <a href="materials.html">材质</a>对象上调用<code class="notranslate" translate="no">dispose</code>方法来释放资源
  36. </p>
  37. <p>你可以手动来处理。起初,你可能创建了一些资源。</p>
  38. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxGeometry = new THREE.BoxGeometry(...);
  39. const boxTexture = textureLoader.load(...);
  40. const boxMaterial = new THREE.MeshPhongMaterial({map: texture});
  41. </pre>
  42. <p>然后,当你处理完了它们,可以释放它们</p>
  43. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">boxGeometry.dispose();
  44. boxTexture.dispose();
  45. boxMaterial.dispose();
  46. </pre>
  47. <p>随着你使用越来越多的资源,这将会变得越来越乏味。</p>
  48. <p>为了减少一些乏味的工作,让我们创建一个类来跟踪这些资源。我们会请求这个类来帮我们做清除的工作。
  49. </p>
  50. <p>这个类一开始是这个样子。</p>
  51. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  52. constructor() {
  53. this.resources = new Set();
  54. }
  55. track(resource) {
  56. if (resource.dispose) {
  57. this.resources.add(resource);
  58. }
  59. return resource;
  60. }
  61. untrack(resource) {
  62. this.resources.delete(resource);
  63. }
  64. dispose() {
  65. for (const resource of this.resources) {
  66. resource.dispose();
  67. }
  68. this.resources.clear();
  69. }
  70. }
  71. </pre>
  72. <p>让我们在<a href="textures.html">纹理文章</a>中的例子中使用这个类。我们可以创建一个这个类的实例。</p>
  73. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const resTracker = new ResourceTracker();
  74. </pre>
  75. <p>然后为了让这个类的使用更加地简单,让我们来为<code class="notranslate" translate="no">track</code>方法创建一个绑定函数。</p>
  76. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const resTracker = new ResourceTracker();
  77. +const track = resTracker.track.bind(resTracker);
  78. </pre>
  79. <p>现在,我们只需要在我们创建的每个图元、纹理、材质对象上调用<code class="notranslate" translate="no">track</code>方法就可以使用它。</p>
  80. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxWidth = 1;
  81. const boxHeight = 1;
  82. const boxDepth = 1;
  83. -const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  84. +const geometry = track(new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth));
  85. const cubes = []; // an array we can use to rotate the cubes
  86. const loader = new THREE.TextureLoader();
  87. -const material = new THREE.MeshBasicMaterial({
  88. - map: loader.load('resources/images/wall.jpg'),
  89. -});
  90. +const material = track(new THREE.MeshBasicMaterial({
  91. + map: track(loader.load('resources/images/wall.jpg')),
  92. +}));
  93. const cube = new THREE.Mesh(geometry, material);
  94. scene.add(cube);
  95. cubes.push(cube); // add to our list of cubes to rotate
  96. </pre>
  97. <p>然后,我们从场景中移除这些立方体,再然后调用<code class="notranslate" translate="no">resTracker.dispose</code>来释放它们。</p>
  98. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (const cube of cubes) {
  99. scene.remove(cube);
  100. }
  101. cubes.length = 0; // clears the cubes array
  102. resTracker.dispose();
  103. </pre>
  104. <p>这起作用了,但是我发现必须从场景中移除立方体有些乏味。让我们给<code class="notranslate" translate="no">ResourceTracker</code>增加这个功能。</p>
  105. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  106. constructor() {
  107. this.resources = new Set();
  108. }
  109. track(resource) {
  110. - if (resource.dispose) {
  111. + if (resource.dispose || resource instanceof THREE.Object3D) {
  112. this.resources.add(resource);
  113. }
  114. return resource;
  115. }
  116. untrack(resource) {
  117. this.resources.delete(resource);
  118. }
  119. dispose() {
  120. for (const resource of this.resources) {
  121. - resource.dispose();
  122. + if (resource instanceof THREE.Object3D) {
  123. + if (resource.parent) {
  124. + resource.parent.remove(resource);
  125. + }
  126. + }
  127. + if (resource.dispose) {
  128. + resource.dispose();
  129. + }
  130. + }
  131. this.resources.clear();
  132. }
  133. }
  134. </pre>
  135. <p>现在我们可以跟踪这些立方体了</p>
  136. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const material = track(new THREE.MeshBasicMaterial({
  137. map: track(loader.load('resources/images/wall.jpg')),
  138. }));
  139. const cube = track(new THREE.Mesh(geometry, material));
  140. scene.add(cube);
  141. cubes.push(cube); // add to our list of cubes to rotate
  142. </pre>
  143. <p>我们不再需要编码从场景中移除这些立方体了。</p>
  144. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-for (const cube of cubes) {
  145. - scene.remove(cube);
  146. -}
  147. cubes.length = 0; // clears the cube array
  148. resTracker.dispose();
  149. </pre>
  150. <p>让我们来调整一下代码,这样我们可以重新添加立方体、纹理和材质。</p>
  151. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  152. *const cubes = []; // just an array we can use to rotate the cubes
  153. +function addStuffToScene() {
  154. const resTracker = new ResourceTracker();
  155. const track = resTracker.track.bind(resTracker);
  156. const boxWidth = 1;
  157. const boxHeight = 1;
  158. const boxDepth = 1;
  159. const geometry = track(new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth));
  160. const loader = new THREE.TextureLoader();
  161. const material = track(new THREE.MeshBasicMaterial({
  162. map: track(loader.load('resources/images/wall.jpg')),
  163. }));
  164. const cube = track(new THREE.Mesh(geometry, material));
  165. scene.add(cube);
  166. cubes.push(cube); // add to our list of cubes to rotate
  167. + return resTracker;
  168. +}
  169. </pre>
  170. <p>让我们来编写一些随着时间变动添加和移除物体的代码。</p>
  171. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function waitSeconds(seconds = 0) {
  172. return new Promise(resolve =&gt; setTimeout(resolve, seconds * 1000));
  173. }
  174. async function process() {
  175. for (;;) {
  176. const resTracker = addStuffToScene();
  177. await wait(2);
  178. cubes.length = 0; // remove the cubes
  179. resTracker.dispose();
  180. await wait(1);
  181. }
  182. }
  183. process();
  184. </pre>
  185. <p>代码将会创建立方体、纹理和材质,等待2秒,然后释放它们,然后等待1秒,重复这个过程。</p>
  186. <p></p><div translate="no" class="threejs_example_container notranslate">
  187. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cleanup-simple.html"></iframe></div>
  188. <a class="threejs_center" href="/manual/examples/cleanup-simple.html" target="_blank">点击此处打开独立窗口</a>
  189. </div>
  190. <p></p>
  191. <p>这好像能工作了。</p>
  192. <p>对于加载文件来说,还需要一点额外的工作。大多数的加载器仅仅返回一个<a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>
  193. 对象,作为它加载的层次对象的根节点,因此我们需要去发现所有的这些资源是哪些。</p>
  194. <p>让我们更新<code class="notranslate" translate="no">ResourceTracker</code>来试着去实现它。</p>
  195. <p>首先,我们来检测这个物体是否是一个<a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>,然后跟踪它的图元、材质和子对象。</p>
  196. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  197. constructor() {
  198. this.resources = new Set();
  199. }
  200. track(resource) {
  201. if (resource.dispose || resource instanceof THREE.Object3D) {
  202. this.resources.add(resource);
  203. }
  204. + if (resource instanceof THREE.Object3D) {
  205. + this.track(resource.geometry);
  206. + this.track(resource.material);
  207. + this.track(resource.children);
  208. + }
  209. return resource;
  210. }
  211. ...
  212. }
  213. </pre>
  214. <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,
  215. 我们将在<code class="notranslate" translate="no">track</code>的入口执行检查。</p>
  216. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  217. constructor() {
  218. this.resources = new Set();
  219. }
  220. track(resource) {
  221. + if (!resource) {
  222. + return resource;
  223. + }
  224. if (resource.dispose || resource instanceof THREE.Object3D) {
  225. this.resources.add(resource);
  226. }
  227. if (resource instanceof THREE.Object3D) {
  228. this.track(resource.geometry);
  229. this.track(resource.material);
  230. this.track(resource.children);
  231. }
  232. return resource;
  233. }
  234. ...
  235. }
  236. </pre>
  237. <p>同时,因为<code class="notranslate" translate="no">resource.children</code>是一个数组,
  238. 同时<code class="notranslate" translate="no">resource.material</code>也可能是数组,让我们对数组做检测。
  239. </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. + // handle children and when material is an array of materials.
  249. + if (Array.isArray(resource)) {
  250. + resource.forEach(resource =&gt; this.track(resource));
  251. + return resource;
  252. + }
  253. if (resource.dispose || resource instanceof THREE.Object3D) {
  254. this.resources.add(resource);
  255. }
  256. if (resource instanceof THREE.Object3D) {
  257. this.track(resource.geometry);
  258. this.track(resource.material);
  259. this.track(resource.children);
  260. }
  261. return resource;
  262. }
  263. ...
  264. }
  265. </pre>
  266. <p>最后我们需要遍历这些材质的属性和uniforms来寻找纹理。</p>
  267. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ResourceTracker {
  268. constructor() {
  269. this.resources = new Set();
  270. }
  271. track(resource) {
  272. if (!resource) {
  273. return resource;
  274. }
  275. * // handle children and when material is an array of materials or
  276. * // uniform is array of textures
  277. if (Array.isArray(resource)) {
  278. resource.forEach(resource =&gt; this.track(resource));
  279. return resource;
  280. }
  281. if (resource.dispose || resource instanceof THREE.Object3D) {
  282. this.resources.add(resource);
  283. }
  284. if (resource instanceof THREE.Object3D) {
  285. this.track(resource.geometry);
  286. this.track(resource.material);
  287. this.track(resource.children);
  288. - }
  289. + } else if (resource instanceof THREE.Material) {
  290. + // We have to check if there are any textures on the material
  291. + for (const value of Object.values(resource)) {
  292. + if (value instanceof THREE.Texture) {
  293. + this.track(value);
  294. + }
  295. + }
  296. + // We also have to check if any uniforms reference textures or arrays of textures
  297. + if (resource.uniforms) {
  298. + for (const value of Object.values(resource.uniforms)) {
  299. + if (value) {
  300. + const uniformValue = value.value;
  301. + if (uniformValue instanceof THREE.Texture ||
  302. + Array.isArray(uniformValue)) {
  303. + this.track(uniformValue);
  304. + }
  305. + }
  306. + }
  307. + }
  308. + }
  309. return resource;
  310. }
  311. ...
  312. }
  313. </pre>
  314. <p>让我们来使用<a href="load-gltf.html">“加载gltf文件文章“</a>中的例子,让它能够加载和释放文件。</p>
  315. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gltfLoader = new GLTFLoader();
  316. function loadGLTF(url) {
  317. return new Promise((resolve, reject) =&gt; {
  318. gltfLoader.load(url, resolve, undefined, reject);
  319. });
  320. }
  321. function waitSeconds(seconds = 0) {
  322. return new Promise(resolve =&gt; setTimeout(resolve, seconds * 1000));
  323. }
  324. const fileURLs = [
  325. 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf',
  326. 'resources/models/3dbustchallange_submission/scene.gltf',
  327. 'resources/models/mountain_landscape/scene.gltf',
  328. 'resources/models/simple_house_scene/scene.gltf',
  329. ];
  330. async function loadFiles() {
  331. for (;;) {
  332. for (const url of fileURLs) {
  333. const resMgr = new ResourceTracker();
  334. const track = resMgr.track.bind(resMgr);
  335. const gltf = await loadGLTF(url);
  336. const root = track(gltf.scene);
  337. scene.add(root);
  338. // compute the box that contains all the stuff
  339. // from root and below
  340. const box = new THREE.Box3().setFromObject(root);
  341. const boxSize = box.getSize(new THREE.Vector3()).length();
  342. const boxCenter = box.getCenter(new THREE.Vector3());
  343. // set the camera to frame the box
  344. frameArea(boxSize * 1.1, boxSize, boxCenter, camera);
  345. await waitSeconds(2);
  346. renderer.render(scene, camera);
  347. resMgr.dispose();
  348. await waitSeconds(1);
  349. }
  350. }
  351. }
  352. loadFiles();
  353. </pre>
  354. <p>然后我们得到了</p>
  355. <p></p><div translate="no" class="threejs_example_container notranslate">
  356. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cleanup-loaded-files.html"></iframe></div>
  357. <a class="threejs_center" href="/manual/examples/cleanup-loaded-files.html" target="_blank">点击此处在独立窗口中打开</a>
  358. </div>
  359. <p></p>
  360. <p>关于代码的一些注释。</p>
  361. <p>如果我们想要加载2个或者更多的文件,并且想要随时地释放它们,我们将要每个文件使用一个<code class="notranslate" translate="no">ResourceTracker</code>。</p>
  362. <p>在上面中,我们只是在场景加载之后跟踪了<code class="notranslate" translate="no">gltf.scene</code>。
  363. 根据<code class="notranslate" translate="no">ResourceTracker</code>的当前实现版本,它会跟踪刚刚加载的所有资源。如果我们向场景中添加了更多的资源,我们需要决定是否要跟踪它们。
  364. </p>
  365. <p>举例来说,在我们加载了一个角色之后,我们把一个工具放入它的手中,这是通过把工具成为手的子对象来实现的。因此工具将不会被释放。我猜更多的时候,这不是我们想要的。</p>
  366. <p>这带来了一个问题。当我起初写上面的<code class="notranslate" translate="no">ResourceTracker</code>的时候,
  367. 我是在<code class="notranslate" translate="no">dispose</code>方法中遍历所有对象而不是在
  368. <code class="notranslate" translate="no">track</code>方法中。稍后,我就想到了上面的成为手的子对象的工具的这个例子,
  369. 在<code class="notranslate" translate="no">track</code>方法中确切地跟踪哪些对象需要被释放更加地灵活,按理来说也更加地准确,因为我们可以跟踪从文件中加载了什么,而不是稍后从资源图中释放状态。
  370. </p>
  371. <p>诚实地说,我对ResourceTracker并不是100%满意。在3D引擎中,做这样的事情并不是很常见。我们不应该去猜测什么资源被加载了,我们应该知道。
  372. 如果three.js能做出改变,所有的文件加载器返回能够引用所有加载资源的标准对象,就太好了。至少在这个时候,three.js在场景加载的时候并没有提供给我们其它的信息。因此,这个方法是有效的。</p>
  373. <p>我希望这个例子能给你带来帮助或者至少成为在three.js中如何释放资源的一份好的参考</p>
  374. </div>
  375. </div>
  376. </div>
  377. <script src="../resources/prettify.js"></script>
  378. <script src="../resources/lesson.js"></script>
  379. </body></html>