cameras.html 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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 – 摄像机">
  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/zh/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 系列文章的一部分。第一篇文章是 <a href="fundamentals.html">three.js 基础</a>。如果你还没看过而且对three.js 还不熟悉,那应该从那里开始,并且了解如何<a href="setup.html">设置开发环境</a>。上一篇文章介绍了 three.js 中的 <a href="textures.html">纹理</a>。</p>
  33. <p>我们开始谈谈three.js中的摄像机. 我们已经在<a href="fundamentals.html">第一篇文章</a> 中涉及到了摄像机的一些知识, 这里我们要更深入一些. </p>
  34. <p>在three.js中最常用的摄像机并且之前我们一直用的摄像机是<code class="notranslate" translate="no">透视摄像机 PerspectiveCamera</code>, 它可以提供一个近大远小的3D视觉效果. </p>
  35. <p><a href="/docs/#api/zh/cameras/PerspectiveCamera"><code class="notranslate" translate="no">PerspectiveCamera</code></a> 定义了一个 <em>视锥frustum</em>. <a href="https://en.wikipedia.org/wiki/Frustum"><em>frustum</em> 是一个切掉顶的三角锥或者说实心金字塔型</a>.
  36. 说到<em>实心体solid</em>, 在这里通常是指一个立方体, 一个圆锥, 一个球, 一个圆柱或锥台.</p>
  37. <div class="spread">
  38. <div><div data-diagram="shapeCube"></div><div>立方体</div></div>
  39. <div><div data-diagram="shapeCone"></div><div>圆锥</div></div>
  40. <div><div data-diagram="shapeSphere"></div><div>球</div></div>
  41. <div><div data-diagram="shapeCylinder"></div><div>圆柱</div></div>
  42. <div><div data-diagram="shapeFrustum"></div><div>锥台</div></div>
  43. </div>
  44. <p>重新讲一遍这些东西是因为我好久没有在意过了. 很多书或者文章提到<em>锥台</em>这个东西的时候我扫一眼就过去了. 再了解一下不同几何体会让下面的一些表述变得更为感性...吧😅</p>
  45. <p><a href="/docs/#api/zh/cameras/PerspectiveCamera"><code class="notranslate" translate="no">PerspectiveCamera</code></a>通过四个属性来定义一个视锥. <code class="notranslate" translate="no">near</code>定义了视锥的前端, <code class="notranslate" translate="no">far</code>定义了后端, <code class="notranslate" translate="no">fov</code>是视野, 通过计算正确的高度来从摄像机的位置获得指定的以<code class="notranslate" translate="no">near</code>为单位的视野, 定义的是视锥的前端和后端的高度. <code class="notranslate" translate="no">aspect</code>间接地定义了视锥前端和后端的宽度, 实际上视锥的宽度是通过高度乘以aspect来得到的. </p>
  46. <p><img src="../resources/frustum-3d.svg" width="500" class="threejs_center"></p>
  47. <p>我们借用<a href="lights.html">上一篇文章</a>的场景. 其中包含一个地平面, 一个球和一个立方体, 我们可以在其中调整摄像机的设置.
  48. ·
  49. 我们通过<code class="notranslate" translate="no">MinMaxGUIHelper</code>来调整<code class="notranslate" translate="no">near</code>, <code class="notranslate" translate="no">far</code>的设置. 显然<code class="notranslate" translate="no">near</code>应该总是比<code class="notranslate" translate="no">far</code>要小. lil-gui有<code class="notranslate" translate="no">min</code>和<code class="notranslate" translate="no">max</code>两个属性可调, 然后这两个属性将决定摄像机的设置. </p>
  50. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class MinMaxGUIHelper {
  51. constructor(obj, minProp, maxProp, minDif) {
  52. this.obj = obj;
  53. this.minProp = minProp;
  54. this.maxProp = maxProp;
  55. this.minDif = minDif;
  56. }
  57. get min() {
  58. return this.obj[this.minProp];
  59. }
  60. set min(v) {
  61. this.obj[this.minProp] = v;
  62. this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
  63. }
  64. get max() {
  65. return this.obj[this.maxProp];
  66. }
  67. set max(v) {
  68. this.obj[this.maxProp] = v;
  69. this.min = this.min; // 这将调用min的setter
  70. }
  71. }
  72. </pre>
  73. <p>现在我们可以将GUI设置为</p>
  74. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function updateCamera() {
  75. camera.updateProjectionMatrix();
  76. }
  77. const gui = new GUI();
  78. gui.add(camera, 'fov', 1, 180).onChange(updateCamera);
  79. const minMaxGUIHelper = new MinMaxGUIHelper(camera, 'near', 'far', 0.1);
  80. gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
  81. gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far').onChange(updateCamera);
  82. </pre>
  83. <p>任何时候摄像机的设置变动, 我们需要调用摄像机的<a href="/docs/#api/zh/cameras/PerspectiveCamera#updateProjectionMatrix"><code class="notranslate" translate="no">updateProjectionMatrix</code></a>来更新设置. 我们写一个函数<code class="notranslate" translate="no">updataCamera</code>, 当lil-gui改变了属性的时候会调用它来更新参数. </p>
  84. <p></p><div translate="no" class="threejs_example_container notranslate">
  85. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-perspective.html"></iframe></div>
  86. <a class="threejs_center" href="/manual/examples/cameras-perspective.html" target="_blank">点击此处在新标签页中打开</a>
  87. </div>
  88. <p></p>
  89. <p>现在可以调整这些数值来观察这些参数是如何影响摄像机的. 注意我们并没有改变<code class="notranslate" translate="no">aspect</code>, 因为这个参数来自于窗口的大小. 如果想调整<code class="notranslate" translate="no">aspect</code>, 只需要开个新窗口然后调整窗口大小就可以了. </p>
  90. <p>即便是这样, 观察参数对视野的影响还是挺麻烦的. 所以我们来设置两台摄像机吧! 一台是跟上面一样展现出摄像机中看到的实际场景, 另一个则是用来观察这个实际工作的摄像机, 然后画出摄像机的视锥. </p>
  91. <p>我们需要用到three.js的剪函数(scissor function)来画两个场景和两个摄像机. </p>
  92. <p>首先让我们用HTML和CSS来定义两个肩并肩的元素. 这也将帮助我们将两个摄像机赋予不同的<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>. </p>
  93. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  94. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  95. + &lt;div class="split"&gt;
  96. + &lt;div id="view1" tabindex="1"&gt;&lt;/div&gt;
  97. + &lt;div id="view2" tabindex="2"&gt;&lt;/div&gt;
  98. + &lt;/div&gt;
  99. &lt;/body&gt;
  100. </pre>
  101. <p>CSS将控制两个视窗并排显示在canvas中</p>
  102. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">.split {
  103. position: absolute;
  104. left: 0;
  105. top: 0;
  106. width: 100%;
  107. height: 100%;
  108. display: flex;
  109. }
  110. .split&gt;div {
  111. width: 100%;
  112. height: 100%;
  113. }
  114. </pre>
  115. <p>接下来将添加一个<a href="/docs/#api/zh/helpers/CameraHelper"><code class="notranslate" translate="no">CameraHelper</code></a>, 它可以把摄像机的视锥画出来</p>
  116. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cameraHelper = new THREE.CameraHelper(camera);
  117. ...
  118. scene.add(cameraHelper);
  119. </pre>
  120. <p>我们现在需要查找到刚刚定义的两个元素</p>
  121. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const view1Elem = document.querySelector('#view1');
  122. const view2Elem = document.querySelector('#view2');
  123. </pre>
  124. <p>现在只给第一个视窗中的摄像机分配<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a></p>
  125. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const controls = new OrbitControls(camera, canvas);
  126. +const controls = new OrbitControls(camera, view1Elem);
  127. </pre>
  128. <p>我们定义第二个<a href="/docs/#api/zh/cameras/PerspectiveCamera"><code class="notranslate" translate="no">PerspectiveCamera</code></a>和<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>. </p>
  129. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const camera2 = new THREE.PerspectiveCamera(
  130. 60, // fov
  131. 2, // aspect
  132. 0.1, // near
  133. 500, // far
  134. );
  135. camera2.position.set(40, 10, 30);
  136. camera2.lookAt(0, 5, 0);
  137. const controls2 = new OrbitControls(camera2, view2Elem);
  138. controls2.target.set(0, 5, 0);
  139. controls2.update();
  140. </pre>
  141. <p>最后我们需要</p>
  142. <p>最后,我们需要使用剪刀功能从每个摄影机的视角渲染场景,以仅渲染画布的一部分。
  143. 这个函数接受一个元素, 计算这个元素在canvas上的重叠面积, 这将设置剪刀函数和视角长宽并返回aspect</p>
  144. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function setScissorForElement(elem) {
  145. const canvasRect = canvas.getBoundingClientRect();
  146. const elemRect = elem.getBoundingClientRect();
  147. // 计算canvas的尺寸
  148. const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left;
  149. const left = Math.max(0, elemRect.left - canvasRect.left);
  150. const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top;
  151. const top = Math.max(0, elemRect.top - canvasRect.top);
  152. const width = Math.min(canvasRect.width, right - left);
  153. const height = Math.min(canvasRect.height, bottom - top);
  154. // 设置剪函数以仅渲染一部分场景
  155. const positiveYUpBottom = canvasRect.height - bottom;
  156. renderer.setScissor(left, positiveYUpBottom, width, height);
  157. renderer.setViewport(left, positiveYUpBottom, width, height);
  158. // 返回aspect
  159. return width / height;
  160. }
  161. </pre>
  162. <p>我们用这个函数在<code class="notranslate" translate="no">render</code>中绘制两遍场景</p>
  163. <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> function render() {
  164. - if (resizeRendererToDisplaySize(renderer)) {
  165. - const canvas = renderer.domElement;
  166. - camera.aspect = canvas.clientWidth / canvas.clientHeight;
  167. - camera.updateProjectionMatrix();
  168. - }
  169. + resizeRendererToDisplaySize(renderer);
  170. +
  171. + // 启用剪刀函数
  172. + renderer.setScissorTest(true);
  173. +
  174. + // 渲染主视野
  175. + {
  176. + const aspect = setScissorForElement(view1Elem);
  177. +
  178. + // 用计算出的aspect修改摄像机参数
  179. + camera.aspect = aspect;
  180. + camera.updateProjectionMatrix();
  181. + cameraHelper.update();
  182. +
  183. + // 来原视野中不要绘制cameraHelper
  184. + cameraHelper.visible = false;
  185. +
  186. + scene.background.set(0x000000);
  187. +
  188. + // 渲染
  189. + renderer.render(scene, camera);
  190. + }
  191. +
  192. + // 渲染第二台摄像机
  193. + {
  194. + const aspect = setScissorForElement(view2Elem);
  195. +
  196. + // 调整aspect
  197. + camera2.aspect = aspect;
  198. + camera2.updateProjectionMatrix();
  199. +
  200. + // 在第二台摄像机中绘制cameraHelper
  201. + cameraHelper.visible = true;
  202. +
  203. + scene.background.set(0x000040);
  204. +
  205. + renderer.render(scene, camera2);
  206. + }
  207. - renderer.render(scene, camera);
  208. requestAnimationFrame(render);
  209. }
  210. requestAnimationFrame(render);
  211. }
  212. </pre>
  213. <p>上面的代码还将主辅摄像机的背景色区分开以利观察. </p>
  214. <p>我们可以移除<code class="notranslate" translate="no">updateCamera</code>了, 因为所有的东西在<code class="notranslate" translate="no">render</code>中更新过了.</p>
  215. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function updateCamera() {
  216. - camera.updateProjectionMatrix();
  217. -}
  218. const gui = new GUI();
  219. -gui.add(camera, 'fov', 1, 180).onChange(updateCamera);
  220. +gui.add(camera, 'fov', 1, 180);
  221. const minMaxGUIHelper = new MinMaxGUIHelper(camera, 'near', 'far', 0.1);
  222. -gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
  223. -gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far').onChange(updateCamera);
  224. +gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near');
  225. +gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far');
  226. </pre>
  227. <p>现在我们就可以在辅摄像机中观察到主摄像机的视锥轮廓了. </p>
  228. <p></p><div translate="no" class="threejs_example_container notranslate">
  229. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-perspective-2-scenes.html"></iframe></div>
  230. <a class="threejs_center" href="/manual/examples/cameras-perspective-2-scenes.html" target="_blank">点击此处在新标签页中打开</a>
  231. </div>
  232. <p></p>
  233. <p>左侧可以看到主摄像机的视角, 右侧则是辅摄像机观察主摄像机和主摄像机的视锥轮廓. 可以调整<code class="notranslate" translate="no">near</code>, <code class="notranslate" translate="no">far</code>, <code class="notranslate" translate="no">fov</code>和用鼠标移动摄像机来观察视锥轮廓和场景之间的关系. </p>
  234. <p>将<code class="notranslate" translate="no">near</code>调整到大概20左右, 前景就会在视锥中消失. <code class="notranslate" translate="no">far</code>低于35时, 远景也不复存在. </p>
  235. <p>这带来一个问题, 为什么不把<code class="notranslate" translate="no">near</code>设置到0.0000000001然后将<code class="notranslate" translate="no">far</code>设置成100000000, 使得一切都可以尽收眼底? 原因是你的GPU 8太行, 没有足够的精度来决定什么在前什么在后. 更糟的是, 在默认情况下, 离摄像机近的将会更清晰, 远的模糊, 从<code class="notranslate" translate="no">near</code>到<code class="notranslate" translate="no">far</code>逐渐过渡. </p>
  236. <p>从上面的例子出发, 我们向场景中添加20个球</p>
  237. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  238. const sphereRadius = 3;
  239. const sphereWidthDivisions = 32;
  240. const sphereHeightDivisions = 16;
  241. const sphereGeo = new THREE.SphereBufferGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
  242. const numSpheres = 20;
  243. for (let i = 0; i &lt; numSpheres; ++i) {
  244. const sphereMat = new THREE.MeshPhongMaterial();
  245. sphereMat.color.setHSL(i * .73, 1, 0.5);
  246. const mesh = new THREE.Mesh(sphereGeo, sphereMat);
  247. mesh.position.set(-sphereRadius - 1, sphereRadius + 2, i * sphereRadius * -2.2);
  248. scene.add(mesh);
  249. }
  250. }
  251. </pre>
  252. <p>把 <code class="notranslate" translate="no">near</code> 设置成0.00001</p>
  253. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const fov = 45;
  254. const aspect = 2; // canvas 默认
  255. -const near = 0.1;
  256. +const near = 0.00001;
  257. const far = 100;
  258. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  259. </pre>
  260. <p>调整一下GUI使得能设置到0.00001</p>
  261. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
  262. +gui.add(minMaxGUIHelper, 'min', 0.00001, 50, 0.00001).name('near').onChange(updateCamera);
  263. </pre>
  264. <p>你觉得会发生什么?</p>
  265. <p></p><div translate="no" class="threejs_example_container notranslate">
  266. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-z-fighting.html"></iframe></div>
  267. <a class="threejs_center" href="/manual/examples/cameras-z-fighting.html" target="_blank">点击此处在新标签页中打开</a>
  268. </div>
  269. <p></p>
  270. <p>这就是一个典型的<em>z冲突</em>的例子. GPU没有足够的精度来决定哪个像素在前哪个在后. </p>
  271. <p>以防你的机器太好出现不了我说的情况, 我把我看到的截图放在这</p>
  272. <div class="threejs_center"><img src="../resources/images/z-fighting.png" style="width: 570px;"></div>
  273. <p>解决的方法之一是告诉three.js使用不同的方法计算像素的前后关系. 我们可以在创建<a href="/docs/#api/zh/renderers/WebGLRenderer"><code class="notranslate" translate="no">WebGLRenderer</code></a>时开启<code class="notranslate" translate="no">logarithmicDepthBuffer</code></p>
  274. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const renderer = new THREE.WebGLRenderer({canvas});
  275. +const renderer = new THREE.WebGLRenderer({
  276. + canvas,
  277. + logarithmicDepthBuffer: true,
  278. +});
  279. </pre>
  280. <p>这看起来就行了</p>
  281. <p></p><div translate="no" class="threejs_example_container notranslate">
  282. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-logarithmic-depth-buffer.html"></iframe></div>
  283. <a class="threejs_center" href="/manual/examples/cameras-logarithmic-depth-buffer.html" target="_blank">点击此处在新标签页中打开</a>
  284. </div>
  285. <p></p>
  286. <p>如果这不行的话, 那你就遇到了<em>为什么不能无脑使用这种解决方案</em>的情况了. 到2018年9月, 绝大多数台式机可以但是几乎没有移动设备支持这个功能. </p>
  287. <p>另一个最好别用这种解决方案的原因是这会大大降低运行速度. </p>
  288. <p>即便是现在跑得好好地, 选择太小的<code class="notranslate" translate="no">near</code>和太大的<code class="notranslate" translate="no">far</code>最终也会遇到同样的问题. </p>
  289. <p>所以说你需要选择好好抉择<code class="notranslate" translate="no">near</code>和<code class="notranslate" translate="no">far</code>的设置, 来和你的场景配合. 既不丢失重要的近景, 也不让远处的东西消失不见. 如果你想渲染一个巨大的场景, 不但能看清面前的人的眼睫毛又想看到50公里以外的玩意, 你得自己想一个<em>厉害的</em>方案, 这里就不涉及了. 现在, 好好地选个需要的参数就行. </p>
  290. <p>第二种常见的摄像机是<code class="notranslate" translate="no">正交摄像机 OrthographicCamera</code>, 和指定一个视锥不同的是, 它需要设置<code class="notranslate" translate="no">left</code>, <code class="notranslate" translate="no">right</code>
  291. <code class="notranslate" translate="no">top</code>, <code class="notranslate" translate="no">bottom</code>, <code class="notranslate" translate="no">near</code>, 和<code class="notranslate" translate="no">far</code>指定一个长方体, 使得视野是平行的而不是透视的. </p>
  292. <p>我们来把上面的例子改成<a href="/docs/#api/zh/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>, 首先来设置摄像机</p>
  293. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const left = -1;
  294. const right = 1;
  295. const top = 1;
  296. const bottom = -1;
  297. const near = 5;
  298. const far = 50;
  299. const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
  300. camera.zoom = 0.2;
  301. </pre>
  302. <p>我们将<code class="notranslate" translate="no">left</code>和<code class="notranslate" translate="no">bottom</code>设置成 -1 <code class="notranslate" translate="no">right</code> 和 <code class="notranslate" translate="no">top</code>设成 1, 这样就使盒子宽为两个单位, 高两个单位. 我们接下来通过调整<code class="notranslate" translate="no">left</code>和<code class="notranslate" translate="no">top</code>来选择其aspect. 我们将用<code class="notranslate" translate="no">zoom</code>属性来调整相机到底展现多少的单位大小. </p>
  303. <p>给GUI添加<code class="notranslate" translate="no">zoom</code>设置</p>
  304. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  305. +gui.add(camera, 'zoom', 0.01, 1, 0.01).listen();
  306. </pre>
  307. <p><code class="notranslate" translate="no">listen</code>调用告诉lil-gui去监视属性的变化. 写在这里是因为<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>同样可以控制缩放. 在这个例子中, 鼠标滚轮将会通过<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>控件来控制缩放. </p>
  308. <p>最后更改aspect然后更新摄像机</p>
  309. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  310. const aspect = setScissorForElement(view1Elem);
  311. // 使用aspect更新摄像机
  312. - camera.aspect = aspect;
  313. + camera.left = -aspect;
  314. + camera.right = aspect;
  315. camera.updateProjectionMatrix();
  316. cameraHelper.update();
  317. // 在主摄像机中不绘制视野辅助线
  318. cameraHelper.visible = false;
  319. scene.background.set(0x000000);
  320. renderer.render(scene, camera);
  321. }
  322. </pre>
  323. <p>现在就可以看到<a href="/docs/#api/zh/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>工作了. </p>
  324. <p></p><div translate="no" class="threejs_example_container notranslate">
  325. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-orthographic-2-scenes.html"></iframe></div>
  326. <a class="threejs_center" href="/manual/examples/cameras-orthographic-2-scenes.html" target="_blank">点击此处在新标签页中打开</a>
  327. </div>
  328. <p></p>
  329. <p>大多数情况下, 绘制2D图像的时候会用到<a href="/docs/#api/zh/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>. 你可以自己决定摄像机的视野大小. 比如说你想让canvas的一个像素匹配摄像机的一个单位, 你可以这么做</p>
  330. <p>将原点置于中心, 令一个像素等于一个单位</p>
  331. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">camera.left = -canvas.width / 2;
  332. camera.right = canvas.width / 2;
  333. camera.top = canvas.height / 2;
  334. camera.bottom = -canvas.height / 2;
  335. camera.near = -1;
  336. camera.far = 1;
  337. camera.zoom = 1;
  338. </pre>
  339. <p>或者如果我们想让原点在左上, 就像是2D canvas</p>
  340. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">camera.left = 0;
  341. camera.right = canvas.width;
  342. camera.top = 0;
  343. camera.bottom = canvas.height;
  344. camera.near = -1;
  345. camera.far = 1;
  346. camera.zoom = 1;
  347. </pre>
  348. <p>这样左上角就成了0,0</p>
  349. <p>试试, 这样设置摄像机</p>
  350. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const left = 0;
  351. const right = 300; // 默认的canvas大小
  352. const top = 0;
  353. const bottom = 150; // 默认的canvas大小
  354. const near = -1;
  355. const far = 1;
  356. const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
  357. camera.zoom = 1;
  358. </pre>
  359. <p>然后我们载入六个材质, 生成六个平面, 一一对应. 把每一个平面绑定到父对象<a href="/docs/#api/zh/core/Object3D"><code class="notranslate" translate="no">THREE.Object3D</code></a>上, 以便调整每个平面和左上角原点的相对关系</p>
  360. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const loader = new THREE.TextureLoader();
  361. const textures = [
  362. loader.load('resources/images/flower-1.jpg'),
  363. loader.load('resources/images/flower-2.jpg'),
  364. loader.load('resources/images/flower-3.jpg'),
  365. loader.load('resources/images/flower-4.jpg'),
  366. loader.load('resources/images/flower-5.jpg'),
  367. loader.load('resources/images/flower-6.jpg'),
  368. ];
  369. const planeSize = 256;
  370. const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
  371. const planes = textures.map((texture) =&gt; {
  372. const planePivot = new THREE.Object3D();
  373. scene.add(planePivot);
  374. texture.magFilter = THREE.NearestFilter;
  375. const planeMat = new THREE.MeshBasicMaterial({
  376. map: texture,
  377. side: THREE.DoubleSide,
  378. });
  379. const mesh = new THREE.Mesh(planeGeo, planeMat);
  380. planePivot.add(mesh);
  381. // 调整平面使得左上角为原点
  382. mesh.position.set(planeSize / 2, planeSize / 2, 0);
  383. return planePivot;
  384. });
  385. </pre>
  386. <p>然后当canvas更新后我们更新摄像机设置</p>
  387. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() {
  388. if (resizeRendererToDisplaySize(renderer)) {
  389. camera.right = canvas.width;
  390. camera.bottom = canvas.height;
  391. camera.updateProjectionMatrix();
  392. }
  393. ...
  394. </pre>
  395. <p><code class="notranslate" translate="no">planes</code> 是<a href="/docs/#api/zh/objects/Mesh"><code class="notranslate" translate="no">THREE.Mesh</code></a>的数组, 每一个对应一个平面.
  396. 现在让它随着时间移动</p>
  397. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  398. time *= 0.001; // 转换为秒;
  399. ...
  400. const distAcross = Math.max(20, canvas.width - planeSize);
  401. const distDown = Math.max(20, canvas.height - planeSize);
  402. // 来回运动的总距离
  403. const xRange = distAcross * 2;
  404. const yRange = distDown * 2;
  405. const speed = 180;
  406. planes.forEach((plane, ndx) =&gt; {
  407. // 为每个平面单独计算时间
  408. const t = time * speed + ndx * 300;
  409. // 在0到最远距离之间获取一个值
  410. const xt = t % xRange;
  411. const yt = t % yRange;
  412. // 0到距离的一半, 向前运动
  413. // 另一半的时候往回运动
  414. const x = xt &lt; distAcross ? xt : xRange - xt;
  415. const y = yt &lt; distDown ? yt : yRange - yt;
  416. plane.position.set(x, y, 0);
  417. });
  418. renderer.render(scene, camera);
  419. </pre>
  420. <p>你可以看到图片在其中弹跳, 和边际完美契合, 就是2D canvas的效果一样</p>
  421. <p></p><div translate="no" class="threejs_example_container notranslate">
  422. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/cameras-orthographic-canvas-top-left-origin.html"></iframe></div>
  423. <a class="threejs_center" href="/manual/examples/cameras-orthographic-canvas-top-left-origin.html" target="_blank">点击此处在新标签页中打开</a>
  424. </div>
  425. <p></p>
  426. <p>另一个常见的用途是用<a href="/docs/#api/zh/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>来展示模型的三视图.</p>
  427. <div class="threejs_center"><img src="../resources/images/quad-viewport.png" style="width: 574px;"></div>
  428. <p>上面的截图展示了一个透视图和三个正交视角.</p>
  429. <p>这就是摄像机的基础. 我们在其他的文章中会介绍另外的一些摄像机用法. 现在, 我们移步到<a href="shadows.html">阴影</a>.</p>
  430. <p><canvas id="c"></canvas></p>
  431. <script type="module" src="../resources/threejs-cameras.js"></script>
  432. </div>
  433. </div>
  434. </div>
  435. <script src="/manual/resources/prettify.js"></script>
  436. <script src="/manual/resources/lesson.js"></script>
  437. </body></html>