lights.html 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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="../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>光照</h1>
  26. </div>
  27. <div class="lesson">
  28. <div class="lesson-main">
  29. <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>
  30. <p>接下来我们学习如何在 three.js 中使用各种不同类型的光照。</p>
  31. <p>在一个基本场景的基础上,我们调整一下相机的设置。将 fov 设置为 45, far 设置为 100,然后移动相机位置到 (0, 10, 20)。</p>
  32. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">*const fov = 45;
  33. const aspect = 2; // canvas 的默认宽高 300:150
  34. const near = 0.1;
  35. *const far = 100;
  36. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  37. +camera.position.set(0, 10, 20);
  38. </pre>
  39. <p>然后我们添加一个 <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> 让我们可以围绕某一个点旋转控制相机。<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> 是 three.js 的可选模块,所以我们首先需要引入这个模块。</p>
  40. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  41. +import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  42. </pre>
  43. <p>然后我们就可以使用了。创建 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> 时传入两个参数,一个是要控制的相机对象,第二个是检测事件的 DOM 元素。</p>
  44. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const controls = new OrbitControls(camera, canvas);
  45. controls.target.set(0, 5, 0);
  46. controls.update();
  47. </pre>
  48. <p>我们还将 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> 的观察点设置为 (0, 5, 0) 的位置,设置完需要调用一下 <code class="notranslate" translate="no">controls.update</code>,这样才真正更新观察点位置。</p>
  49. <p>下面我们创建一些东西来打光。首先,创建一个地平面,并用下方展示的 2x2 像素的黑白格图片来作为纹理。</p>
  50. <div class="threejs_center">
  51. <img src="../examples/resources/images/checker.png" class="border" style="
  52. image-rendering: pixelated;
  53. width: 128px;
  54. ">
  55. </div>
  56. <p>首先加载这个纹理,设置重复模式(<a href="/docs/#api/zh/textures/Texture#wrapS"><code class="notranslate" translate="no">wrapS</code></a>, <a href="/docs/#api/zh/textures/Texture#wrapT"><code class="notranslate" translate="no">wrapT</code></a>),采样模式(<a href="/docs/#api/zh/textures/Texture#magFilter"><code class="notranslate" translate="no">magFilter</code></a>)以及重复的次数。因为贴图是 2x2 大小,通过设置成平铺模式,并且重复次数是边长的一半,就可以让每个格子正好是1个单位的大小。</p>
  57. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const planeSize = 40;
  58. const loader = new THREE.TextureLoader();
  59. const texture = loader.load('resources/images/checker.png');
  60. texture.wrapS = THREE.RepeatWrapping;
  61. texture.wrapT = THREE.RepeatWrapping;
  62. texture.magFilter = THREE.NearestFilter;
  63. texture.colorSpace = THREE.SRGBColorSpace;
  64. const repeats = planeSize / 2;
  65. texture.repeat.set(repeats, repeats);
  66. </pre>
  67. <p>接着我们创建一个平面几何体,一个材质,再用这两个作为参数,创建一个 <a href="/docs/#api/zh/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> 对象并且添加到场景中。因为创建的平面默认是在 XY 平面上(竖直平面),我们希望得到一个 XZ 平面(水平平面),所以我们将他旋转 90°。</p>
  68. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
  69. const planeMat = new THREE.MeshPhongMaterial({
  70. map: texture,
  71. side: THREE.DoubleSide,
  72. });
  73. const mesh = new THREE.Mesh(planeGeo, planeMat);
  74. mesh.rotation.x = Math.PI * -.5;
  75. scene.add(mesh);
  76. </pre>
  77. <p>接着再添加一个立方体和一个球体,这样我们就有三个物体可以打光。</p>
  78. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  79. const cubeSize = 4;
  80. const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
  81. const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
  82. const mesh = new THREE.Mesh(cubeGeo, cubeMat);
  83. mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
  84. scene.add(mesh);
  85. }
  86. {
  87. const sphereRadius = 3;
  88. const sphereWidthDivisions = 32;
  89. const sphereHeightDivisions = 16;
  90. const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
  91. const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
  92. const mesh = new THREE.Mesh(sphereGeo, sphereMat);
  93. mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
  94. scene.add(mesh);
  95. }
  96. </pre>
  97. <p>一切准备就绪,我们开始添加光源。</p>
  98. <h2 id="-ambientlight-">环境光(<a href="/docs/#api/zh/lights/AmbientLight"><code class="notranslate" translate="no">AmbientLight</code></a>)</h2>
  99. <p>首先创建一个 <a href="/docs/#api/zh/lights/AmbientLight"><code class="notranslate" translate="no">AmbientLight</code></a></p>
  100. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
  101. const intensity = 1;
  102. const light = new THREE.AmbientLight(color, intensity);
  103. scene.add(light);
  104. </pre>
  105. <p>我们添加一些控制代码,使我们可以动态地改变光照的参数,还是使用 <a href="https://github.com/georgealways/lil-gui">lil-gui</a> 来实现。为了可以通过 <code class="notranslate" translate="no">lil-gui</code> 调节颜色,我们创建一个辅助对象。对象内有一个 <code class="notranslate" translate="no">getter</code> 和 <code class="notranslate" translate="no">setter</code>,当 <code class="notranslate" translate="no">lil-gui</code> 从对象内获取 <code class="notranslate" translate="no">value</code> 值的时候,触发了 <code class="notranslate" translate="no">getter</code>,会根据创建对象实例时传入的 <code class="notranslate" translate="no">object</code> 和 <code class="notranslate" translate="no">prop</code>,返回一个十六进制色值的字符串,当通过 <code class="notranslate" translate="no">lil-gui</code> 控制改变这个 <code class="notranslate" translate="no">value</code> 的时候,就触发了 <code class="notranslate" translate="no">setter</code>,会用十六进制的色值字符串作为参数调用 <code class="notranslate" translate="no">object.prop.set</code>。</p>
  106. <p>以下是 helper 类的代码:</p>
  107. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ColorGUIHelper {
  108. constructor(object, prop) {
  109. this.object = object;
  110. this.prop = prop;
  111. }
  112. get value() {
  113. return `#${this.object[this.prop].getHexString()}`;
  114. }
  115. set value(hexString) {
  116. this.object[this.prop].set(hexString);
  117. }
  118. }
  119. </pre>
  120. <p>以及创建 lil-gui 的代码:</p>
  121. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  122. gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
  123. gui.add(light, 'intensity', 0, 2, 0.01);
  124. </pre>
  125. <p>结果如下所示:</p>
  126. <p></p><div translate="no" class="threejs_example_container notranslate">
  127. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-ambient.html"></iframe></div>
  128. <a class="threejs_center" href="/manual/examples/lights-ambient.html" target="_blank">点击此处在新标签页中打开</a>
  129. </div>
  130. <p></p>
  131. <p>可以在场景内点击和拖拽鼠标来改变相机的位置,观察场景。</p>
  132. <p>可以看到场景内的物体看起来没有立体感。环境光 (<a href="/docs/#api/zh/lights/AmbientLight"><code class="notranslate" translate="no">AmbientLight</code></a>)只是简单地将材质的颜色与光照颜色进行叠加(PhotoShop 里的正片叠底模式),再乘以光照强度。</p>
  133. <pre class="prettyprint showlinemods notranslate notranslate" translate="no">// 这里的颜色计算是 RBG 通道上的值分别对应相乘
  134. // 例: rgb(0.64,0.64,0.64) = rgb(0.8,0.8,0.8) * rgb(0.8,0.8,0.8) * 1
  135. color = materialColor * light.color * light.intensity;
  136. </pre><p>这就是环境光,它没有方向,无法产生阴影,场景内任何一点受到的光照强度都是相同的,除了改变场景内所有物体的颜色以外,不会使物体产生明暗的变化,看起来并不像真正意义上的光照。通常的作用是提亮场景,让暗部不要太暗。</p>
  137. <h2 id="-hemispherelight-">半球光(<a href="/docs/#api/zh/lights/HemisphereLight"><code class="notranslate" translate="no">HemisphereLight</code></a>)</h2>
  138. <p>接下来介绍半球光(<a href="/docs/#api/zh/lights/HemisphereLight"><code class="notranslate" translate="no">HemisphereLight</code></a>)。半球光(<a href="/docs/#api/zh/lights/HemisphereLight"><code class="notranslate" translate="no">HemisphereLight</code></a>)的颜色是从天空到地面两个颜色之间的渐变,与物体材质的颜色作叠加后得到最终的颜色效果。一个点受到的光照颜色是由所在平面的朝向(法向量)决定的 —— 面向正上方就受到天空的光照颜色,面向正下方就受到地面的光照颜色,其他角度则是两个颜色渐变区间的颜色。</p>
  139. <p>下面是修改后的代码:</p>
  140. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const color = 0xFFFFFF;
  141. +const skyColor = 0xB1E1FF; // light blue
  142. +const groundColor = 0xB97A20; // brownish orange
  143. const intensity = 1;
  144. -const light = new THREE.AmbientLight(color, intensity);
  145. +const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
  146. scene.add(light);
  147. </pre>
  148. <p>同时修改一下 <code class="notranslate" translate="no">lil-gui</code> 部分,使得可以控制两种颜色:</p>
  149. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  150. -gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
  151. +gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('skyColor');
  152. +gui.addColor(new ColorGUIHelper(light, 'groundColor'), 'value').name('groundColor');
  153. gui.add(light, 'intensity', 0, 2, 0.01);
  154. </pre>
  155. <p>结果如下:</p>
  156. <p></p><div translate="no" class="threejs_example_container notranslate">
  157. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-hemisphere.html"></iframe></div>
  158. <a class="threejs_center" href="/manual/examples/lights-hemisphere.html" target="_blank">点击此处在新标签页中打开</a>
  159. </div>
  160. <p></p>
  161. <p>场景基本上也没有太大的立体感。半球光 (<a href="/docs/#api/zh/lights/HemisphereLight"><code class="notranslate" translate="no">HemisphereLight</code></a>) 与其他类型光照结合使用,可以很好地表现天空和地面颜色照射到物体上时的效果。所以最好的使用场景就是与其他光照结合使用,或者作为环境光(<a href="/docs/#api/zh/lights/AmbientLight"><code class="notranslate" translate="no">AmbientLight</code></a>)的一种替代方案。</p>
  162. <h2 id="-directionallight-">方向光(<a href="/docs/#api/zh/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>)</h2>
  163. <p>下面介绍方向光(<a href="/docs/#api/zh/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>)。
  164. 方向光(<a href="/docs/#api/zh/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>)常常用来表现太阳光照的效果。</p>
  165. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
  166. const intensity = 1;
  167. const light = new THREE.DirectionalLight(color, intensity);
  168. light.position.set(0, 10, 0);
  169. light.target.position.set(-5, 0, 0);
  170. scene.add(light);
  171. scene.add(light.target);
  172. </pre>
  173. <p>注意,不仅 <code class="notranslate" translate="no">light</code> ,我们还把 <a href="/docs/#api/zh/lights/DirectionalLight#target"><code class="notranslate" translate="no">light.target</code></a> 也添加到了场景中。方向光(<a href="/docs/#api/zh/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>)的方向是从它的位置照向目标点的位置。</p>
  174. <p>下面代码是将目标点坐标属性添加到 <code class="notranslate" translate="no">lil-gui</code>,使得我们可以控制目标位置</p>
  175. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  176. gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
  177. gui.add(light, 'intensity', 0, 2, 0.01);
  178. gui.add(light.target.position, 'x', -10, 10);
  179. gui.add(light.target.position, 'z', -10, 10);
  180. gui.add(light.target.position, 'y', 0, 10);
  181. </pre>
  182. <p></p><div translate="no" class="threejs_example_container notranslate">
  183. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-directional.html"></iframe></div>
  184. <a class="threejs_center" href="/manual/examples/lights-directional.html" target="_blank">点击此处在新标签页中打开</a>
  185. </div>
  186. <p></p>
  187. <p>目前有点难以观察。Three.js 提供了一些辅助对象,添加到场景中之后就可以显示出场景中的不可见对象(例如光照、相机等)。在这里我们使用 <a href="/docs/#api/zh/helpers/DirectionalLightHelper"><code class="notranslate" translate="no">DirectionalLightHelper</code></a>,它会绘制一个方形的小平面代表方向光的位置,一条连接光源与目标点的直线,代表了光的方向。创建对象时,传入光源对象作为参数,然后添加到场景中,就可以呈现。</p>
  188. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const helper = new THREE.DirectionalLightHelper(light);
  189. scene.add(helper);
  190. </pre>
  191. <p>我们顺便实现一下对光源位置和目标点位置的控制逻辑。我们创建一个辅助函数,使得可以通过 <code class="notranslate" translate="no">lil-gui</code> 改变传入的 <a href="/docs/#api/zh/math/Vector3"><code class="notranslate" translate="no">Vector3</code></a> 类型对象的 <code class="notranslate" translate="no">x</code>,<code class="notranslate" translate="no">y</code>,和 <code class="notranslate" translate="no">z</code> 的值。</p>
  192. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeXYZGUI(gui, vector3, name, onChangeFn) {
  193. const folder = gui.addFolder(name);
  194. folder.add(vector3, 'x', -10, 10).onChange(onChangeFn);
  195. folder.add(vector3, 'y', 0, 10).onChange(onChangeFn);
  196. folder.add(vector3, 'z', -10, 10).onChange(onChangeFn);
  197. folder.open();
  198. }
  199. </pre>
  200. <p>注意,当辅助对象所表示的不可见对象有所改变的时候,我们必须调用辅助对象的 <code class="notranslate" translate="no">update</code> 方法来更新辅助对象本身的状态。因此我们传入一个 <code class="notranslate" translate="no">onChangeFn</code> 函数,每当 <code class="notranslate" translate="no">lil-gui</code> 改变了某个值的时候,就会被调用。</p>
  201. <p>应用到光照位置与目标点位置的控制,就如下所示:</p>
  202. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function updateLight() {
  203. + light.target.updateMatrixWorld();
  204. + helper.update();
  205. +}
  206. +updateLight();
  207. const gui = new GUI();
  208. gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
  209. gui.add(light, 'intensity', 0, 2, 0.01);
  210. +makeXYZGUI(gui, light.position, 'position', updateLight);
  211. +makeXYZGUI(gui, light.target.position, 'target', updateLight);
  212. </pre>
  213. <p>现在我们可以控制光源以及目标点位置了。</p>
  214. <p></p><div translate="no" class="threejs_example_container notranslate">
  215. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-directional-w-helper.html"></iframe></div>
  216. <a class="threejs_center" href="/manual/examples/lights-directional-w-helper.html" target="_blank">点击此处在新标签页中打开</a>
  217. </div>
  218. <p></p>
  219. <p>旋转相机可以看得更清楚。方形的小平面代表了一个方向光(<a href="/docs/#api/zh/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>),方向光表示的是来自一个方向上的光,并不是从某个点发射出来的,而是从一个无限大的平面内,发射出全部相互平行的光线。</p>
  220. <h2 id="-pointlight-">点光源(<a href="/docs/#api/zh/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>)</h2>
  221. <p>点光源(<a href="/docs/#api/zh/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>)表示的是从一个点朝各个方向发射出光线的一种光照效果。我们修改一下代码:</p>
  222. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
  223. -const intensity = 1;
  224. +const intensity = 150;
  225. -const light = new THREE.DirectionalLight(color, intensity);
  226. +const light = new THREE.PointLight(color, intensity);
  227. light.position.set(0, 10, 0);
  228. -light.target.position.set(-5, 0, 0);
  229. scene.add(light);
  230. -scene.add(light.target);
  231. </pre>
  232. <p>同时添加一个 <a href="/docs/#api/zh/helpers/PointLightHelper"><code class="notranslate" translate="no">PointLightHelper</code></a></p>
  233. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const helper = new THREE.DirectionalLightHelper(light);
  234. +const helper = new THREE.PointLightHelper(light);
  235. scene.add(helper);
  236. </pre>
  237. <p>因为点光源没有 <code class="notranslate" translate="no">target</code> 属性,所以 <code class="notranslate" translate="no">onChange</code> 函数可以简化。</p>
  238. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function updateLight() {
  239. - light.target.updateMatrixWorld();
  240. helper.update();
  241. }
  242. -updateLight();
  243. </pre>
  244. <p><a href="/docs/#api/zh/helpers/PointLightHelper"><code class="notranslate" translate="no">PointLightHelper</code></a> 不是一个点,而是在光源的位置绘制了一个小小的线框宝石体来代表点光源。也可以使用其他形状来表示点光源,只要给点光源添加一个自定义的 <a href="/docs/#api/zh/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> 子节点即可。</p>
  245. <p>点光源(<a href="/docs/#api/zh/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>)有额外的一个范围(<a href="/docs/#api/zh/lights/PointLight#distance"><code class="notranslate" translate="no">distance</code></a>)属性。
  246. 如果 <code class="notranslate" translate="no">distance</code> 设为 0,则光线可以照射到无限远处。如果大于 0,则只可以照射到指定的范围,光照强度在这个过程中逐渐衰减,在光源位置时,<code class="notranslate" translate="no">intensity</code> 是设定的大小,在距离光源 <code class="notranslate" translate="no">distance</code> 位置的时候,<code class="notranslate" translate="no">intensity</code> 为 0。</p>
  247. <p>下面是添加对 distance 参数控制的代码:</p>
  248. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  249. gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
  250. gui.add(light, 'intensity', 0, 2, 0.01);
  251. +gui.add(light, 'distance', 0, 40).onChange(updateLight);
  252. makeXYZGUI(gui, light.position, 'position', updateLight);
  253. -makeXYZGUI(gui, light.target.position, 'target', updateLight);
  254. </pre>
  255. <p>效果如下:</p>
  256. <p></p><div translate="no" class="threejs_example_container notranslate">
  257. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-point.html"></iframe></div>
  258. <a class="threejs_center" href="/manual/examples/lights-point.html" target="_blank">点击此处在新标签页中打开</a>
  259. </div>
  260. <p></p>
  261. <p>注意 <code class="notranslate" translate="no">distance</code> &gt; 0 时光照强度的衰减现象。</p>
  262. <h2 id="-spotlight-">聚光灯(<a href="/docs/#api/zh/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>)</h2>
  263. <p>聚光灯可以看成是一个点光源被一个圆锥体限制住了光照的范围。实际上有两个圆锥,内圆锥和外圆锥。光照强度在两个锥体之间从设定的强度递减到 0(具体可以看下方 <a href="/docs/#api/zh/lights/SpotLight#penumbra"><code class="notranslate" translate="no">penumbra</code></a> 参数)。</p>
  264. <p>聚光灯(<a href="/docs/#api/zh/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>)类似方向光(<a href="/docs/#api/zh/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>)一样需要一个目标点,光源的位置是圆锥的顶点,目标点处于圆锥的中轴线上。</p>
  265. <p>修改上面 <a href="/docs/#api/zh/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a> 的代码如下:</p>
  266. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
  267. -const intensity = 1;
  268. +const intensity = 150;
  269. -const light = new THREE.DirectionalLight(color, intensity);
  270. +const light = new THREE.SpotLight(color, intensity);
  271. scene.add(light);
  272. scene.add(light.target);
  273. -const helper = new THREE.DirectionalLightHelper(light);
  274. +const helper = new THREE.SpotLightHelper(light);
  275. scene.add(helper);
  276. </pre>
  277. <p>聚光灯的圆锥顶部角度大小通过 <a href="/docs/#api/zh/lights/SpotLight#angle"><code class="notranslate" translate="no">angle</code></a> 属性设置,以弧度作单位。所以我们用介绍 <a href="textures.html">纹理</a> 时用到的 <code class="notranslate" translate="no">DegRadHelper</code> 来控制。</p>
  278. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">gui.add(new DegRadHelper(light, 'angle'), 'value', 0, 90).name('angle').onChange(updateLight);
  279. </pre>
  280. <p>内圆锥是通过设置 <a href="/docs/#api/zh/lights/SpotLight#penumbra"><code class="notranslate" translate="no">penumbra</code></a> 属性来定义的,属性值代表了内圆锥相对外圆锥大小变化的百分比。当 <code class="notranslate" translate="no">penumbra</code> 为 0 时,内圆锥大小与外圆锥大小一致;当 <code class="notranslate" translate="no">penumbra</code> 为 1 时,内圆锥大小为 0,光照强度从中轴线就开始往外递减;当 <code class="notranslate" translate="no">penumbra</code> 为 0.5 时,光照强度从外圆锥半径的中点处开始往外递减。</p>
  281. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">gui.add(light, 'penumbra', 0, 1, 0.01);
  282. </pre>
  283. <p></p><div translate="no" class="threejs_example_container notranslate">
  284. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-spot-w-helper.html"></iframe></div>
  285. <a class="threejs_center" href="/manual/examples/lights-spot-w-helper.html" target="_blank">点击此处在新标签页中打开</a>
  286. </div>
  287. <p></p>
  288. <p>注意观察,当 <code class="notranslate" translate="no">penumbra</code> 为默认值 0 的时候,聚光灯会有非常清晰的边缘,而当把 <code class="notranslate" translate="no">penumbra</code> 向 1 调节的时候,边缘会开始模糊。</p>
  289. <p>示例中有点难以看到聚光灯的整个圆锥体,因为圆锥底部在平面下方。将 <code class="notranslate" translate="no">distance</code> 减小到 5 左右,就可以看到圆锥的底部。</p>
  290. <h2 id="-rectarealight-">矩形区域光(<a href="/docs/#api/zh/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a>)</h2>
  291. <p>Three.js 中还有一种类型的光照,矩形区域光(<a href="/docs/#api/zh/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a>), 顾名思义,表示一个矩形区域的发射出来的光照,例如长条的日光灯或者天花板上磨砂玻璃透进来的自然光。</p>
  292. <p><a href="/docs/#api/zh/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a> 只能影响 <a href="/docs/#api/zh/materials/MeshStandardMaterial"><code class="notranslate" translate="no">MeshStandardMaterial</code></a> 和 <a href="/docs/#api/zh/materials/MeshPhysicalMaterial"><code class="notranslate" translate="no">MeshPhysicalMaterial</code></a>,所以我们把所有的材质都改为 <a href="/docs/#api/zh/materials/MeshStandardMaterial"><code class="notranslate" translate="no">MeshStandardMaterial</code></a>。</p>
  293. <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> ...
  294. const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
  295. - const planeMat = new THREE.MeshPhongMaterial({
  296. + const planeMat = new THREE.MeshStandardMaterial({
  297. map: texture,
  298. side: THREE.DoubleSide,
  299. });
  300. const mesh = new THREE.Mesh(planeGeo, planeMat);
  301. mesh.rotation.x = Math.PI * -.5;
  302. scene.add(mesh);
  303. }
  304. {
  305. const cubeSize = 4;
  306. const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
  307. - const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
  308. + const cubeMat = new THREE.MeshStandardMaterial({color: '#8AC'});
  309. const mesh = new THREE.Mesh(cubeGeo, cubeMat);
  310. mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
  311. scene.add(mesh);
  312. }
  313. {
  314. const sphereRadius = 3;
  315. const sphereWidthDivisions = 32;
  316. const sphereHeightDivisions = 16;
  317. const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
  318. - const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
  319. + const sphereMat = new THREE.MeshStandardMaterial({color: '#CA8'});
  320. const mesh = new THREE.Mesh(sphereGeo, sphereMat);
  321. mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
  322. scene.add(mesh);
  323. }
  324. </pre>
  325. <p>为了使用 <a href="/docs/#api/zh/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a>,我们需要引入 three.js 的<code class="notranslate" translate="no">RectAreaLightUniformsLib</code> 模块,同时使用 <a href="/docs/#api/zh/helpers/RectAreaLightHelper"><code class="notranslate" translate="no">RectAreaLightHelper</code></a> 来辅助查看灯光对象。</p>
  326. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  327. +import {RectAreaLightUniformsLib} from 'three/addons/lights/RectAreaLightUniformsLib.js';
  328. +import {RectAreaLightHelper} from 'three/addons/helpers/RectAreaLightHelper.js';
  329. </pre>
  330. <p>我们需要先调用 <code class="notranslate" translate="no">RectAreaLightUniformsLib.init</code></p>
  331. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  332. const canvas = document.querySelector('#c');
  333. const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
  334. + RectAreaLightUniformsLib.init();
  335. </pre>
  336. <p>如果忘了引入和使用 <code class="notranslate" translate="no">RectAreaLightUniformsLib</code>,光照还是可以显示,但是会看起来很奇怪(译者注:在示例的简单场景中没有发现区别),所以要确保有使用。</p>
  337. <p>然后我们可以创建光照了</p>
  338. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
  339. *const intensity = 5;
  340. +const width = 12;
  341. +const height = 4;
  342. *const light = new THREE.RectAreaLight(color, intensity, width, height);
  343. light.position.set(0, 10, 0);
  344. +light.rotation.x = THREE.MathUtils.degToRad(-90);
  345. scene.add(light);
  346. *const helper = new RectAreaLightHelper(light);
  347. *light.add(helper);
  348. </pre>
  349. <p>需要注意的是,与方向光(<a href="/docs/#api/zh/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>)和聚光灯(<a href="/docs/#api/zh/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>)不同,矩形光不是使用目标点(<code class="notranslate" translate="no">target</code>),而是使用自身的旋转角度来确定光照方向。另外,矩形光的辅助对象(<a href="/docs/#api/zh/helpers/RectAreaLightHelper"><code class="notranslate" translate="no">RectAreaLightHelper</code></a>)应该添加为光照的子节点,而不是添加为场景的子节点。</p>
  350. <p>同时我们修改一下 GUI 代码,使我们可以旋转光源,调整 <code class="notranslate" translate="no">width</code> 和 <code class="notranslate" translate="no">height</code> 属性。</p>
  351. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  352. gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
  353. gui.add(light, 'intensity', 0, 10, 0.01);
  354. gui.add(light, 'width', 0, 20);
  355. gui.add(light, 'height', 0, 20);
  356. gui.add(new DegRadHelper(light.rotation, 'x'), 'value', -180, 180).name('x rotation');
  357. gui.add(new DegRadHelper(light.rotation, 'y'), 'value', -180, 180).name('y rotation');
  358. gui.add(new DegRadHelper(light.rotation, 'z'), 'value', -180, 180).name('z rotation');
  359. makeXYZGUI(gui, light.position, 'position');
  360. </pre>
  361. <p>场景如下所示:</p>
  362. <p></p><div translate="no" class="threejs_example_container notranslate">
  363. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-rectarea.html"></iframe></div>
  364. <a class="threejs_center" href="/manual/examples/lights-rectarea.html" target="_blank">点击此处在新标签页中打开</a>
  365. </div>
  366. <p></p>
  367. <p>需要注意,每添加一个光源到场景中,都会降低 three.js 渲染场景的速度,所以应该尽量使用最少的资源来实现想要的效果。</p>
  368. <p>接下来我们学习 three.js 中的 <a href="cameras.html">相机</a>。</p>
  369. <p><canvas id="c"></canvas></p>
  370. <script type="module" src="../resources/threejs-lights.js"></script>
  371. </div>
  372. </div>
  373. </div>
  374. <script src="../resources/prettify.js"></script>
  375. <script src="../resources/lesson.js"></script>
  376. </body></html>