1
0

load-obj.html 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. <!DOCTYPE html><html lang="zh"><head>
  2. <meta charset="utf-8">
  3. <title>加载 .OBJ 文件</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 – 加载 .OBJ 文件">
  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. <link rel="stylesheet" href="/manual/zh/lang.css">
  21. </head>
  22. <body>
  23. <div class="container">
  24. <div class="lesson-title">
  25. <h1>加载 .OBJ 文件</h1>
  26. </div>
  27. <div class="lesson">
  28. <div class="lesson-main">
  29. <p>人们最想用three.js做的事情之一就是加载和显示三维模型。
  30. 一个常见的格式是.OBJ 3D格式,我们现在尝试加载一个。</p>
  31. <p>在网上搜索,我找到了<a href="https://www.blendswap.com/blends/view/69174">CC-BY-NC 3.0风车3D模型</a>,作者是<a href="https://www.blendswap.com/user/ahedov">阿赫多夫</a>。</p>
  32. <div class="threejs_center"><img src="../resources/images/windmill-obj.jpg"></div>
  33. <p>我从那个网站下载了.blend文件,并将其加载到<a href="https://Blender.org">Blender</a>并将其导出为.OBJ文件。</p>
  34. <div class="threejs_center"><img style="width: 827px;" src="../resources/images/windmill-export-as-obj.jpg"></div>
  35. <blockquote>
  36. <p>注意:如果你从未使用过Blender,你可能会感到意外,Blender里做的事情不同于你用过的其他程序。你可能需要留出一些时间阅读Blender的一些基本操作。</p>
  37. <p>我还要补充一点,3D程序通常超过1000个功能。它们是最复杂的软件之一。当我1996年第一次学习3D Studio Max时,我阅读了600页的手册70%内容,每天花几个小时,持续了大约3周。几年后当我学习Maya时,以前学过的都适用于Maya。所以,要知道如果你真的希望能够使用三维软件来构建三维资源或者修改现有的3D模型,把学习3D的计划放在你的日程表上,好好学习。</p>
  38. </blockquote>
  39. <p>一般我会使用以下的导出设置:</p>
  40. <div class="threejs_center"><img style="width: 239px;" src="../resources/images/windmill-export-options.jpg"></div>
  41. <p>让我们一起来尝试将它展示出来。</p>
  42. <p>基于<a href="lights.html">光线文章</a>中的定向光线(<a href="/docs/#api/zh/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>)示例,结合半球光线(<a href="/docs/#api/zh/lights/HemisphereLight"><code class="notranslate" translate="no">HemisphereLight</code></a>)示例。相对于示例,我删除了所有与调整灯光相关的GUI内容,还删除了添加到场景中的立方体和球体。</p>
  43. <p>第一件要做的事就是将<a href="/docs/#examples/loaders/OBJLoader"><code class="notranslate" translate="no">OBJLoader</code></a>添加到代码中。</p>
  44. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
  45. </pre>
  46. <p>然后创建<a href="/docs/#examples/loaders/OBJLoader"><code class="notranslate" translate="no">OBJLoader</code></a>的实例并通过URL加载我们的.OBJ文件,并在回调函数中将已加载完的模型添加到场景(scene)里。</p>
  47. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  48. const objLoader = new OBJLoader();
  49. objLoader.load('resources/models/windmill/windmill.obj', (root) =&gt; {
  50. scene.add(root);
  51. });
  52. }
  53. </pre>
  54. <p>如果这样跑一下会怎么样?</p>
  55. <p></p><div translate="no" class="threejs_example_container notranslate">
  56. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-obj-no-materials.html"></iframe></div>
  57. <a class="threejs_center" href="/manual/examples/load-obj-no-materials.html" target="_blank">点击此处在新标签页中打开</a>
  58. </div>
  59. <p></p>
  60. <p>这已经相当接近,但是材质(material)显示异常。场景里材质显示异常是因为.OBJ文件格式是不包含材质数据的。</p>
  61. <p>.OBJ加载器可以传入 名称/材质对 的对象。当它加载.OBJ文件时,会加载对应名称的材质,若该材质不存在就会使用默认材质。</p>
  62. <p>有时.OBJ文件会带有一个.MTL文件来定义材质。在我们的示例里也会创建一个.MTL文件。.MTL文件是一个ASCII码文件,所以可以直接阅读。</p>
  63. <pre class="prettyprint showlinemods notranslate lang-mtl" translate="no"># Blender MTL File: 'windmill_001.blend'
  64. # Material Count: 2
  65. newmtl Material
  66. Ns 0.000000
  67. Ka 1.000000 1.000000 1.000000
  68. Kd 0.800000 0.800000 0.800000
  69. Ks 0.000000 0.000000 0.000000
  70. Ke 0.000000 0.000000 0.000000
  71. Ni 1.000000
  72. d 1.000000
  73. illum 1
  74. map_Kd windmill_001_lopatky_COL.jpg
  75. map_Bump windmill_001_lopatky_NOR.jpg
  76. newmtl windmill
  77. Ns 0.000000
  78. Ka 1.000000 1.000000 1.000000
  79. Kd 0.800000 0.800000 0.800000
  80. Ks 0.000000 0.000000 0.000000
  81. Ke 0.000000 0.000000 0.000000
  82. Ni 1.000000
  83. d 1.000000
  84. illum 1
  85. map_Kd windmill_001_base_COL.jpg
  86. map_Bump windmill_001_base_NOR.jpg
  87. map_Ns windmill_001_base_SPEC.jpg
  88. </pre>
  89. <p>我们能看到2个材质,引用了五张纹理图片,但是这些纹理文件在哪里呢?</p>
  90. <div class="threejs_center"><img style="width: 757px;" src="../resources/images/windmill-exported-files.png"></div>
  91. <p>我们只得到一个.OBJ文件以及一个.MTL文件。</p>
  92. <p>我们所下载的.blend文件是包含了模型的纹理,所以我们可以让blender点击<strong>File-&gt;External Data-&gt;Unpack All Into Files</strong>导出这些文件。</p>
  93. <div class="threejs_center"><img style="width: 828px;" src="../resources/images/windmill-export-textures.jpg"></div>
  94. <p>选择 <strong>Write Files to Current Directory</strong></p>
  95. <div class="threejs_center"><img style="width: 828px;" src="../resources/images/windmill-overwrite.jpg"></div>
  96. <p>这样就会在.blend文件的目录下创建出一个<strong>textures</strong>文件夹。</p>
  97. <div class="threejs_center"><img style="width: 758px;" src="../resources/images/windmill-exported-texture-files.png"></div>
  98. <p>我复制这些纹理放到我导出的.OBJ目录里。</p>
  99. <div class="threejs_center"><img style="width: 757px;" src="../resources/images/windmill-exported-files-with-textures.png"></div>
  100. <p>现在.MTL文件就能加载到这些纹理。</p>
  101. <p>首先要引用 <a href="/docs/#examples/loaders/MTLLoader"><code class="notranslate" translate="no">MTLLoader</code></a>;</p>
  102. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  103. import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  104. import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
  105. +import {MTLLoader} from 'three/addons/loaders/MTLLoader.js';
  106. </pre>
  107. <p></p>
  108. <p>然后我们先加载.MTL文件,在它加载完材质后利用<code class="notranslate" translate="no">MtlObjBridge</code>将材质传给<a href="/docs/#examples/loaders/OBJLoader"><code class="notranslate" translate="no">OBJLoader</code></a>,再加载.OBJ文件。</p>
  109. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  110. + const mtlLoader = new MTLLoader();
  111. + mtlLoader.load('resources/models/windmill/windmill.mtl', (mtl) =&gt; {
  112. + mtl.preload();
  113. + objLoader.setMaterials(mtl);
  114. objLoader.load('resources/models/windmill/windmill.obj', (root) =&gt; {
  115. scene.add(root);
  116. });
  117. + });
  118. }
  119. </pre>
  120. <p>尝试一下:</p>
  121. <p></p><div translate="no" class="threejs_example_container notranslate">
  122. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-obj-materials.html"></iframe></div>
  123. <a class="threejs_center" href="/manual/examples/load-obj-materials.html" target="_blank">点击此处在新标签页中打开</a>
  124. </div>
  125. <p></p>
  126. <p>注意当旋转模型时,会发现风车布是消失了的。</p>
  127. <div class="threejs_center"><img style="width: 528px;" src="../resources/images/windmill-missing-cloth.jpg"></div>
  128. <p>我们需要设置这个材质为双面,在<a href="materials.html">关于材质的文章</a>有详细说明。
  129. 在.MTL里不好修复这个问题,在我脑海里有三个解决办法:</p>
  130. <ol>
  131. <li><p>加载模型后,遍历所有材质,设置成双面。</p>
  132. <pre class="prettyprint showlinemods notranslate notranslate" translate="no"> const mtlLoader = new MTLLoader();
  133. mtlLoader.load('resources/models/windmill/windmill.mtl', (mtl) =&gt; {
  134. mtl.preload();
  135. for (const material of Object.values(mtl.materials)) {
  136. material.side = THREE.DoubleSide;
  137. }
  138. ...
  139. </pre><p>这样能解决问题,但是最理想的情况下是,仅设置特定的材质为双面,其它无需设置成双面,毕竟渲染双面的效率低于单面。</p>
  140. </li>
  141. <li><p>手动设置一个特定材质。</p>
  142. <p>看到.MTL里有两个材质,一个是<code class="notranslate" translate="no">"windmill"</code>另一个是<code class="notranslate" translate="no">"Material"</code>。经过反复尝试,发现可以单独设置一个材质。</p>
  143. <pre class="prettyprint showlinemods notranslate notranslate" translate="no"> const mtlLoader = new MTLLoader();
  144. mtlLoader.load('resources/models/windmill/windmill.mtl', (mtl) =&gt; {
  145. mtl.preload();
  146. mtl.materials.Material.side = THREE.DoubleSide;
  147. ...
  148. </pre></li>
  149. <li><p>在.MTL文件上无法解决,可以不使用它而使用自行创建的材质。</p>
  150. <pre class="prettyprint showlinemods notranslate notranslate" translate="no"> objLoader.load('resources/models/windmill/windmill.obj', (root) =&gt; {
  151. const materials = {
  152. Material: new THREE.MeshPhongMaterial({...}),
  153. windmill: new THREE.MeshPhongMaterial({...}),
  154. };
  155. root.traverse(node =&gt; {
  156. const material = materials[node.material?.name];
  157. if (material) {
  158. node.material = material;
  159. }
  160. })
  161. scene.add(root);
  162. });
  163. </pre></li>
  164. </ol>
  165. <p>采用哪个方案取决于你。1是最简单,3是最灵活,2是两者之间,现在我选择2 。</p>
  166. <p>修改之后,当你从后面看的时候,你仍然可以看到风车布,但是还有一个问题。如果我们放大近看,我们看到的东西正在变成块状。</p>
  167. <div class="threejs_center"><img style="width: 700px;" src="../resources/images/windmill-blocky.jpg"></div>
  168. <p>发生了什么?</p>
  169. <p>看看纹理,有2个纹理命名包含了NOR,这是法线贴图(NORmal map)。法线贴图通常是紫色的,而凹凸贴图则是黑白的。法线贴图表示表面的方向,而凹凸贴图则表示表面的高度。</p>
  170. <div class="threejs_center"><img style="width: 256px;" src="../examples/resources/models/windmill/windmill_001_base_NOR.jpg"></div>
  171. <p>看<a href="https://github.com/mrdoob/three.js/blob/1a560a3426e24bbfc9ca1f5fb0dfb4c727d59046/examples/js/loaders/MTLLoader.js#L432">MTLLoader源码</a>,<code class="notranslate" translate="no">norm</code>是标记为法线贴图,我们手动修改一下.MTL文件。</p>
  172. <pre class="prettyprint showlinemods notranslate lang-mtl" translate="no"># Blender MTL File: 'windmill_001.blend'
  173. # Material Count: 2
  174. newmtl Material
  175. Ns 0.000000
  176. Ka 1.000000 1.000000 1.000000
  177. Kd 0.800000 0.800000 0.800000
  178. Ks 0.000000 0.000000 0.000000
  179. Ke 0.000000 0.000000 0.000000
  180. Ni 1.000000
  181. d 1.000000
  182. illum 1
  183. map_Kd windmill_001_lopatky_COL.jpg
  184. -map_Bump windmill_001_lopatky_NOR.jpg
  185. +norm windmill_001_lopatky_NOR.jpg
  186. newmtl windmill
  187. Ns 0.000000
  188. Ka 1.000000 1.000000 1.000000
  189. Kd 0.800000 0.800000 0.800000
  190. Ks 0.000000 0.000000 0.000000
  191. Ke 0.000000 0.000000 0.000000
  192. Ni 1.000000
  193. d 1.000000
  194. illum 1
  195. map_Kd windmill_001_base_COL.jpg
  196. -map_Bump windmill_001_base_NOR.jpg
  197. +norm windmill_001_base_NOR.jpg
  198. map_Ns windmill_001_base_SPEC.jpg
  199. </pre>
  200. <p>现在当我们加载它时它将使用法线贴图作为法线贴图,我们可以看到叶片的背面。</p>
  201. <p></p><div translate="no" class="threejs_example_container notranslate">
  202. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-obj-materials-fixed.html"></iframe></div>
  203. <a class="threejs_center" href="/manual/examples/load-obj-materials-fixed.html" target="_blank">点击此处在新标签页中打开</a>
  204. </div>
  205. <p></p>
  206. <p>现在尝试加载不同的模型:</p>
  207. <p>网络搜索我找到<a href="https://creativecommons.org/licenses/by-nc/4.0/">CC-BY-NC</a> 风车3D模型,由<a href="http://www.gerzi.ch/">Roger Gerzner / GERIZ.3D Art</a>制作。</p>
  208. <div class="threejs_center"><img src="../resources/images/windmill-obj-2.jpg"></div>
  209. <p>它已经有了一个.obj版本。让我们加载它(注意,我现在已经删除了.MTL加载器)</p>
  210. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">- objLoader.load('resources/models/windmill/windmill.obj', ...
  211. + objLoader.load('resources/models/windmill-2/windmill.obj', ...
  212. </pre>
  213. <p></p><div translate="no" class="threejs_example_container notranslate">
  214. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-obj-wat.html"></iframe></div>
  215. <a class="threejs_center" href="/manual/examples/load-obj-wat.html" target="_blank">点击此处在新标签页中打开</a>
  216. </div>
  217. <p></p>
  218. <p>嗯,没有东西出现。是什么问题?我想知道这个模型的尺寸是多少?我们可以问THREE.js模型的大小,并尝试自动设置相机的参数。</p>
  219. <p>首先,我们可以让THREE.js计算一个包含我们刚刚加载的场景的盒子,并计算出它的大小和中心。</p>
  220. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">objLoader.load('resources/models/windmill_2/windmill.obj', (root) =&gt; {
  221. scene.add(root);
  222. + const box = new THREE.Box3().setFromObject(root);
  223. + const boxSize = box.getSize(new THREE.Vector3()).length();
  224. + const boxCenter = box.getCenter(new THREE.Vector3());
  225. + console.log(boxSize);
  226. + console.log(boxCenter);
  227. </pre>
  228. <p>在<a href="debugging-javascript.html">JavaScript控制台</a>能看到:</p>
  229. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">size 2123.6499788469982
  230. center p {x: -0.00006103515625, y: 770.0909731090069, z: -3.313507080078125}
  231. </pre>
  232. <p>我们的相机目前只显示大约100个单位,最近0.1和最远100。我们的地平面只有40个单位宽,所以这个风车模型是如此之大,达2000个单位,摄像机被包含在模型里面,所有能展示的部分都在截锥体外面。</p>
  233. <div class="threejs_center"><img style="width: 280px;" src="../resources/images/camera-inside-windmill.svg"></div>
  234. <p>我们可以手动修复该问题,也可以使相机自动对场景进行构图。 让我们尝试一下,我们可以使用刚计算出的框调整摄像机设置以查看整个场景。 请注意,关于将相机放置在哪里,并没有正确的答案。 我们可以从任何方向,在任何高度面对场景,因此我们只需要选择一些东西即可。</p>
  235. <p>在<a href="cameras.html">关于相机的文章</a>讨论提到,相机定义了一个截锥体。该截锥体由视场(<code class="notranslate" translate="no">fov</code>:the field of view)和远(<code class="notranslate" translate="no">far</code>)近(<code class="notranslate" translate="no">near</code>)设置定义。我们想知道的是,给定当前的视场,摄像机需要设置多远,才能使包含场景的盒子在截锥体内,假设截锥体永远延伸,即假设<code class="notranslate" translate="no">near</code>是0.00000001,<code class="notranslate" translate="no">far</code>是无穷大(infinity)。</p>
  236. <p>因为我们知道盒子的大小也知道视野(FOV:the field of view),所以我们有了这个三角形。</p>
  237. <div class="threejs_center"><img style="width: 600px;" src="../resources/images/camera-fit-scene.svg"></div>
  238. <p>你可以看到在左边的是相机,在它前方投射出蓝色的截锥体。我们只是计算了包含风车的盒子。我们需要计算相机应该离方框有多远,使得盒子在截锥体内。</p>
  239. <p>利用基本的直角三角形函数和<a href="https://www.google.com/search?q=SOHCAHTOA">SOHCAHTOA</a>,已知截锥体的视场和方框的大小,就可以计算出距离。</p>
  240. <div class="threejs_center"><img style="width: 600px;" src="../resources/images/field-of-view-camera.svg"></div>
  241. <p>根据该图,计算距离的公式为:</p>
  242. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">distance = halfSizeToFitOnScreen / tangent(halfFovY)
  243. </pre>
  244. <p>我们把它转换成代码。首先做一个函数,计算出距离<code class="notranslate" translate="no">distance</code>。然后移动相机,离盒子中心<code class="notranslate" translate="no">distance</code>远。然后把摄像机对准盒子中心。</p>
  245. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
  246. const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
  247. const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
  248. const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
  249. // compute a unit vector that points in the direction the camera is now
  250. // from the center of the box
  251. const direction = (new THREE.Vector3()).subVectors(camera.position, boxCenter).normalize();
  252. // move the camera to a position distance units way from the center
  253. // in whatever direction the camera was from the center already
  254. camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
  255. // pick some near and far values for the frustum that
  256. // will contain the box.
  257. camera.near = boxSize / 100;
  258. camera.far = boxSize * 100;
  259. camera.updateProjectionMatrix();
  260. // point the camera to look at the center of the box
  261. camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
  262. }
  263. </pre>
  264. <p>我们传入了两种尺寸:盒子大小<code class="notranslate" translate="no">boxSize</code>和尺寸<code class="notranslate" translate="no">sizeToFitOnScreen</code>。如果我们只是传递<code class="notranslate" translate="no">boxSize</code>并将其用作<code class="notranslate" translate="no">sizeToFitOnScreen</code>,那么数学运算将使这个盒子完美地嵌入到截锥。我们想要在上面和下面留出一点额外的空间,所以我们要传一个稍微大一点的尺寸。</p>
  265. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  266. const objLoader = new OBJLoader();
  267. objLoader.load('resources/models/windmill_2/windmill.obj', (root) =&gt; {
  268. scene.add(root);
  269. + // compute the box that contains all the stuff
  270. + // from root and below
  271. + const box = new THREE.Box3().setFromObject(root);
  272. +
  273. + const boxSize = box.getSize(new THREE.Vector3()).length();
  274. + const boxCenter = box.getCenter(new THREE.Vector3());
  275. +
  276. + // set the camera to frame the box
  277. + frameArea(boxSize * 1.2, boxSize, boxCenter, camera);
  278. +
  279. + // update the Trackball controls to handle the new size
  280. + controls.maxDistance = boxSize * 10;
  281. + controls.target.copy(boxCenter);
  282. + controls.update();
  283. });
  284. }
  285. </pre>
  286. <p>你可以看到在上面,我们传递了<code class="notranslate" translate="no">boxSize * 1.2</code>,当试图把它放入截锥体时,给了20%额外空间在盒子的上下面。我们还更新了<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>,使相机转动时是围绕它进行转动。</p>
  287. <p>现在的效果是:</p>
  288. <p></p><div translate="no" class="threejs_example_container notranslate">
  289. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-obj-auto-camera.html"></iframe></div>
  290. <a class="threejs_center" href="/manual/examples/load-obj-auto-camera.html" target="_blank">点击此处在新标签页中打开</a>
  291. </div>
  292. <p></p>
  293. <p>这效果已经差不多了。用鼠标旋转照相机,你应该能看到风车。问题是风车很大,盒子中心约为(0,770,0)。所以,当我们把相机从(0,10,20)移到到<code class="notranslate" translate="no">distance</code>单位距离,摄像头是风车的正下方。</p>
  294. <div class="threejs_center"><img style="width: 360px;" src="../resources/images/computed-camera-position.svg"></div>
  295. <p>修改代码使摄像头不管在哪个方向,都能对准盒子侧面中心。我们要做的就是把盒子到摄像机的<code class="notranslate" translate="no">y</code>分量归零。然后,当我们标准化这个向量它就会变成一个平行于XZ平面的向量。换句话说,平行于地面。</p>
  296. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-// compute a unit vector that points in the direction the camera is now
  297. -// from the center of the box
  298. -const direction = (new THREE.Vector3()).subVectors(camera.position, boxCenter).normalize();
  299. +// compute a unit vector that points in the direction the camera is now
  300. +// in the xz plane from the center of the box
  301. +const direction = (new THREE.Vector3())
  302. + .subVectors(camera.position, boxCenter)
  303. + .multiply(new THREE.Vector3(1, 0, 1))
  304. + .normalize();
  305. </pre>
  306. <p>如果你看风车的底部,你会看到一个小正方形,这是我们的地板。</p>
  307. <div class="threejs_center"><img style="width: 365px;" src="../resources/images/tiny-ground-plane.jpg"></div>
  308. <p>它只有40x40个单位,相对于风车来说太小了。由于风车超过2000个单位大,让我们改变地面的大小来适配。我们还需要调整重复次数,否则不能看到棋盘。</p>
  309. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const planeSize = 40;
  310. +const planeSize = 4000;
  311. const loader = new THREE.TextureLoader();
  312. const texture = loader.load('resources/images/checker.png');
  313. texture.wrapS = THREE.RepeatWrapping;
  314. texture.wrapT = THREE.RepeatWrapping;
  315. texture.magFilter = THREE.NearestFilter;
  316. -const repeats = planeSize / 2;
  317. +const repeats = planeSize / 200;
  318. texture.repeat.set(repeats, repeats);
  319. </pre>
  320. <p>现在再看看这个风车:</p>
  321. <p></p><div translate="no" class="threejs_example_container notranslate">
  322. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-obj-auto-camera-xz.html"></iframe></div>
  323. <a class="threejs_center" href="/manual/examples/load-obj-auto-camera-xz.html" target="_blank">点击此处在新标签页中打开</a>
  324. </div>
  325. <p></p>
  326. <p>现在把材质加回去,就像之前有一个.mtl文件,它引用了一些纹理。但是看了一下这些文件,我很快发现了一个问题。</p>
  327. <pre class="prettyprint showlinemods notranslate lang-shell" translate="no"> $ ls -l windmill
  328. -rw-r--r--@ 1 gregg staff 299 May 20 2009 windmill.mtl
  329. -rw-r--r--@ 1 gregg staff 142989 May 20 2009 windmill.obj
  330. -rw-r--r--@ 1 gregg staff 12582956 Apr 19 2009 windmill_diffuse.tga
  331. -rw-r--r--@ 1 gregg staff 12582956 Apr 20 2009 windmill_normal.tga
  332. -rw-r--r--@ 1 gregg staff 12582956 Apr 19 2009 windmill_spec.tga
  333. </pre>
  334. <p>这是TARGA (.tga)文件,并且体积很大!</p>
  335. <p>THREE.js实际上有一个TGA加载器,但是在大多数情况下使用它是错误的。如果你制作一个查看器,你想让用户查看他们在网上找到的随机3D文件,那么可能,只是可能,你可能想加载TGA文件。(<a href="#loading-scenes">*</a>)</p>
  336. <p>TGA文件的一个问题是根本不能很好地压缩。TGA只支持非常简单的压缩,在上面我们可以看到文件根本没有被压缩,因为它们的大小完全相同的几率非常低。每个文件12M!!如果我们使用这些文件,用户必须下载36M才能看到风车。</p>
  337. <p>TGA的另一个问题是,浏览器本身不支持它们,所以加载它们可能比加载.jpg和.png等受支持的格式要慢。</p>
  338. <p>我很确定将它们转换成.jpg是最好的选择。看里面,我看到他们是3个通道(RGB),没有阿尔法通道(alpha)。JPG只支持3个频道,所以很适合。JPG也支持有损压缩,所以我们可以使文件更小的下载。</p>
  339. <p>加载文件的大小分别为2048x2048。这对我来说似乎是一种浪费,但当然这取决于您的实际情况。我把它们分别做成1024x1024,并在Photoshop中以50%的质量保存。文件列表:</p>
  340. <pre class="prettyprint showlinemods notranslate lang-shell" translate="no"> $ ls -l ../threejs.org/manual/examples/resources/models/windmill
  341. -rw-r--r--@ 1 gregg staff 299 May 20 2009 windmill.mtl
  342. -rw-r--r--@ 1 gregg staff 142989 May 20 2009 windmill.obj
  343. -rw-r--r--@ 1 gregg staff 259927 Nov 7 18:37 windmill_diffuse.jpg
  344. -rw-r--r--@ 1 gregg staff 98013 Nov 7 18:38 windmill_normal.jpg
  345. -rw-r--r--@ 1 gregg staff 191864 Nov 7 18:39 windmill_spec.jpg
  346. </pre>
  347. <p>从36M转到0.55M!当然,艺术家可能不喜欢这种压缩,所以一定要咨询他们,进行权衡。</p>
  348. <p>现在,为了使用.mtl文件,我们需要编辑它来引用.jpg文件,而不是.tga文件。幸运的是,它是一个简单的文本文件,所以很容易编辑。</p>
  349. <pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">newmtl blinn1SG
  350. Ka 0.10 0.10 0.10
  351. Kd 0.00 0.00 0.00
  352. Ks 0.00 0.00 0.00
  353. Ke 0.00 0.00 0.00
  354. Ns 0.060000
  355. Ni 1.500000
  356. d 1.000000
  357. Tr 0.000000
  358. Tf 1.000000 1.000000 1.000000
  359. illum 2
  360. -map_Kd windmill_diffuse.tga
  361. +map_Kd windmill_diffuse.jpg
  362. -map_Ks windmill_spec.tga
  363. +map_Ks windmill_spec.jpg
  364. -map_bump windmill_normal.tga
  365. -bump windmill_normal.tga
  366. +map_bump windmill_normal.jpg
  367. +bump windmill_normal.jpg
  368. </pre>
  369. <p>现在。mtl文件指向一些合理大小的纹理,我们需要加载它,所以我们就像上面做的那样,首先加载材质,然后将它们设置在<a href="/docs/#examples/loaders/OBJLoader"><code class="notranslate" translate="no">OBJLoader</code></a>上。</p>
  370. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  371. + const mtlLoader = new MTLLoader();
  372. + mtlLoader.load('resources/models/windmill_2/windmill-fixed.mtl', (mtl) =&gt; {
  373. + mtl.preload();
  374. + const objLoader = new OBJLoader();
  375. + objLoader.setMaterials(mtl);
  376. objLoader.load('resources/models/windmill/windmill.obj', (root) =&gt; {
  377. root.updateMatrixWorld();
  378. scene.add(root);
  379. // compute the box that contains all the stuff
  380. // from root and below
  381. const box = new THREE.Box3().setFromObject(root);
  382. const boxSize = box.getSize(new THREE.Vector3()).length();
  383. const boxCenter = box.getCenter(new THREE.Vector3());
  384. // set the camera to frame the box
  385. frameArea(boxSize * 1.2, boxSize, boxCenter, camera);
  386. // update the Trackball controls to handle the new size
  387. controls.maxDistance = boxSize * 10;
  388. controls.target.copy(boxCenter);
  389. controls.update();
  390. });
  391. + });
  392. }
  393. </pre>
  394. <p>在我们真正尝试之前,我想到了一些问题,我不想显示出失败,我只是要检查一下。</p>
  395. <p>问题1:三个<a href="/docs/#examples/loaders/MTLLoader"><code class="notranslate" translate="no">MTLLoader</code></a>创建的材质会将材质的漫反射颜色,乘以漫反射纹理贴图。</p>
  396. <p>这是一个很有用的特性,但是要查看行上面的.MTL文件</p>
  397. <pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">Kd 0.00 0.00 0.00
  398. </pre>
  399. <p>将漫反射颜色设置为0。纹理贴图的值* 0 =黑色! 但可能用于制造风车的建模工具,并没有将漫反射纹理贴图乘以漫反射颜色,所以在艺术家制作模型时仍显示正常。</p>
  400. <p>为了修复它,这样修改:</p>
  401. <pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">Kd 1.00 1.00 1.00
  402. </pre>
  403. <p>因为 纹理贴图的值 * 1 = 纹理贴图的值.</p>
  404. <p>问题2:高光的颜色也是黑色的</p>
  405. <p>以<code class="notranslate" translate="no">Ks</code>开头的行指定了高光颜色。跟上面漫反射贴图一样,使用高光贴图的颜色来实现高光效果。这里仍需要设置一个高光颜色。</p>
  406. <p>跟上面一样,直接修改.MTL文件:</p>
  407. <pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">-Ks 0.00 0.00 0.00
  408. +Ks 1.00 1.00 1.00
  409. </pre>
  410. <p>问题3: <code class="notranslate" translate="no">windmill_normal.jpg</code> 是法线贴图而不是凹凸贴图。</p>
  411. <p>跟上面一样修改.MTL文件:</p>
  412. <pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">-map_bump windmill_normal.jpg
  413. -bump windmill_normal.jpg
  414. +norm windmill_normal.jpg
  415. </pre>
  416. <p>现在试一试附带材质的效果:</p>
  417. <p></p><div translate="no" class="threejs_example_container notranslate">
  418. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-obj-materials-windmill2.html"></iframe></div>
  419. <a class="threejs_center" href="/manual/examples/load-obj-materials-windmill2.html" target="_blank">点击此处在新标签页中打开</a>
  420. </div>
  421. <p></p>
  422. <p>加载模型经常遇到这类问题。常见的问题包括</p>
  423. <ul>
  424. <li><p>需要知道大小</p>
  425. <p>上图中,我们让摄像机试图将场景框起来,但这并不总是合适的做法。一般来说,最合适的做法是自己制作模型或下载模型,加载到一些3D软件中,查看它们的大小,并在需要时进行调整。</p>
  426. </li>
  427. <li><p>方向错误
  428. Three.js一般是以Y为上(up)。有些建模包默认是Z为上,有些是Y为上。有些是可设置的。如果你遇到这种情况,你加载一个模型,它在它的一边。你可以让代码在加载后旋转模型(不推荐),或者你可以模型加载到你最喜欢的建模工具里旋转物体至合适的位置,就像你会为您的网站编辑图像而不是使用代码调整它。Blender导出时可以选择改变方向。</p>
  429. </li>
  430. <li><p>没有.mtl文件、错误的材质或不兼容的参数</p>
  431. <p>上面我们使用了一个.MTL文件,该文件可以帮助我们加载材料,但是存在问题。 我们手动编辑了.MTL文件进行修复。
  432. 通常,也可以在.OBJ文件中查看是否有什么材质,或者将.OBJ文件加载到THREE.js中,然后打印出所有材质。
  433. 然后,修改代码以制作自定义材质并在适当的位置对其进行调整,比如可以创建一个名称/材质对对象传递给加载器,而不是加载.MTL文件,也可以在场景加载后遍历场景进行调整。</p>
  434. </li>
  435. <li><p>纹理过大</p>
  436. <p>大多数3D模型是为建筑、电影和广告或游戏制作的。对于建筑和电影,没有人真正关心纹理的大小。对于游戏来说,因为游戏内存有限,并且大多数游戏都在本地运行,所以文件也不能太多。网页一般要求加载快,所以你需要看纹理,并尽量使他们尽可能小,但仍然看起来很好。事实上,第一个风车模型也应该调整纹理,它们目前总共是10M。</p>
  437. <p>还记得我们在<a href="textures.html">关于纹理的文章</a>中提到的纹理占用内存,所以一个50k的JPG扩展到4096x4096会下载很快,但仍然需要大量的内存。</p>
  438. </li>
  439. </ul>
  440. <p>最后我想展示的是让风车旋转起来。不幸的是. obj文件没有层次结构(hierarchy)。这意味着每个风车模型基本上都是一个单独的网格(mesh)。你不能转动风车的叶片,因为它们没有与建筑物的其他部分分开。</p>
  441. <p>这就是为什么.obj不是一个好的3D格式的主要原因之一。如果我猜一下,它比其他格式更常见的原因是它很简单,而且不支持很多特性。特别是如果你在做一些静态的物体,比如建筑图像,没必要动起来。</p>
  442. <p>接下来我们将尝试加载一个gLTF场景。gLTF格式支持更多特性。</p>
  443. </div>
  444. </div>
  445. </div>
  446. <script src="../resources/prettify.js"></script>
  447. <script src="../resources/lesson.js"></script>
  448. </body></html>