align-html-elements-to-3d.html 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>对齐HTML元素到3D对象</title>
  6. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  7. <meta name="twitter:card" content="summary_large_image">
  8. <meta name="twitter:site" content="@threejs">
  9. <meta name="twitter:title" content="Three.js – Aligning HTML Elements to 3D">
  10. <meta property="og:image" content="https://threejs.org/files/share.png">
  11. <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  12. <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
  13. <link rel="stylesheet" href="../resources/lesson.css">
  14. <link rel="stylesheet" href="../resources/lang.css">
  15. <!-- Import maps polyfill -->
  16. <!-- Remove this when import maps will be widely supported -->
  17. <script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
  18. <script type="importmap">
  19. {
  20. "imports": {
  21. "three": "../../build/three.module.js"
  22. }
  23. }
  24. </script>
  25. <link rel="stylesheet" href="/manual/zh/lang.css">
  26. </head>
  27. <body>
  28. <div class="container">
  29. <div class="lesson-title">
  30. <h1>对齐HTML元素到3D对象</h1>
  31. </div>
  32. <div class="lesson">
  33. <div class="lesson-main">
  34. <p>本文是THREE.js系列文章中的一部分。第一篇是 <a href="fundamentals.html">THREE.js 基础</a>,如果你还没有读过或者你是THREE.js新手,你可能需要考虑从那开始。
  35. </p>
  36. <p>有时你想在 3D 场景中显示一些文本,这有很多种选择,每一种都有各自的优缺点。</p>
  37. <ul>
  38. <li>
  39. <p>使用 3D 文本</p>
  40. <p>如果你看过 <a href="primitives.html">图元章节</a> 你就会看到 <a href="/docs/#api/en/geometries/TextGeometry"><code
  41. class="notranslate" translate="no">TextGeometry</code></a> 可以
  42. 生成3D文本,这可能对飞行类的Logo很有效,但对统计、信息、标记类不是很合适。</p>
  43. </li>
  44. <li>
  45. <p>使用带2D文本的纹理图</p>
  46. <p>这篇文章 <a href="canvas-textures.html">使用Canvas作为纹理</a> 提到Canvas可以作为物体的纹理绘制。你可以向Canvas中绘制文字并且 <a
  47. href="billboards.html">以Billboard的方式展示它</a>。这种方法的优点是文本已被集成到3D场景中,像3D场景中的计算机终端,这可能是比较完美的。</p>
  48. </li>
  49. <li>
  50. <p>使用HTML元素并定位它们以匹配3D场景</p>
  51. <p>这种方法的好处是您可以使用所有的HTML能力。你的HTML中可以有多个元素,可以通过CSS设置样式,它也可以被用户选中因为它就是实际的文本内容。 </p>
  52. </li>
  53. </ul>
  54. <p>本文将介绍上述的最后一种方法。</p>
  55. <p>让我们从简单的开始,我们将使用一些图元制作一个3D场景,然后为每个图元添加一个标签。我们会从这篇<a href="responsive.html">响应式开发</a>中的一个例子开始。 </p>
  56. <p>我们会添加一个 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate"
  57. translate="no">OrbitControls</code></a> 就像我们在 <a href="lights.html">这篇光照的文章</a>里做的一样。</p>
  58. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">
  59. import * as THREE from 'three';
  60. +import {OrbitControls} from 'three/addons/controls/OrbitControls.js';</pre>
  61. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">
  62. const controls = new OrbitControls(camera, canvas);
  63. controls.target.set(0, 0, 0);
  64. controls.update();</pre>
  65. <p>我们需要提供一个HTML元素来包含我们的标签元素。</p>
  66. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">
  67. &lt;body&gt;
  68. - &lt;canvas id="c"&gt;&lt;/canvas&gt;
  69. + &lt;div id="container"&gt;
  70. + &lt;canvas id="c"&gt;&lt;/canvas&gt;
  71. + &lt;div id="labels"&gt;&lt;/div&gt;
  72. + &lt;/div&gt;
  73. &lt;/body&gt;</pre>
  74. <p>通过将Canvas元素和 <code class="notranslate" translate="no">&lt;div id="labels"&gt;</code>
  75. 放在一个父元素里面,我们可以用这个CSS让它们重叠。</p>
  76. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">
  77. #c {
  78. - width: 100%;
  79. - height: 100%;
  80. + width: 100%; /* 让我们的容器决定尺寸 */
  81. + height: 100%;
  82. display: block;
  83. }
  84. +#container {
  85. + position: relative; /* 作为子元素的相对定位元素 */
  86. + width: 100%;
  87. + height: 100%;
  88. + overflow: hidden;
  89. +}
  90. +#labels {
  91. + position: absolute; /* 把Label定位在容器内 */
  92. + left: 0; /* 默认定位在左上角 */
  93. + top: 0;
  94. + color: white;
  95. +}</pre>
  96. <p>让我们也为Label本身添加一些CSS。</p>
  97. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">
  98. #labels&gt;div {
  99. position: absolute; /* 让我们的容器决定尺寸 */
  100. left: 0; /* 默认定位在左上角 */
  101. top: 0;
  102. cursor: pointer; /* 当悬浮时,变为一个小手 */
  103. font-size: large;
  104. user-select: none; /* 不允许文字被选中 */
  105. text-shadow: /* 创造一个黑色阴影 */
  106. -1px -1px 0 #000,
  107. 0 -1px 0 #000,
  108. 1px -1px 0 #000,
  109. 1px 0 0 #000,
  110. 1px 1px 0 #000,
  111. 0 1px 0 #000,
  112. -1px 1px 0 #000,
  113. -1px 0 0 #000;
  114. }
  115. #labels&gt;div:hover {
  116. color: red;
  117. }</pre>
  118. <p>现在进入我们的代码,我们不必添加太多,我们有一个函数<code class="notranslate"
  119. translate="no">makeInstance</code>,可以用来生成立方体。我们现在让它同时添加一个Label元素。</p>
  120. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">
  121. +const labelContainerElem = document.querySelector('#labels');
  122. -function makeInstance(geometry, color, x) {
  123. +function makeInstance(geometry, color, x, name) {
  124. const material = new THREE.MeshPhongMaterial({color});
  125. const cube = new THREE.Mesh(geometry, material);
  126. scene.add(cube);
  127. cube.position.x = x;
  128. + const elem = document.createElement('div');
  129. + elem.textContent = name;
  130. + labelContainerElem.appendChild(elem);
  131. - return cube;
  132. + return {cube, elem};
  133. }</pre>
  134. <p>你可以发现,我们正添加一个 <code class="notranslate" translate="no">&lt;div&gt;</code> 到容器里, 每一个立方体各一个。 我们也返回一个对象,包含<code
  135. class="notranslate" translate="no">cube</code>和Label元素<code class="notranslate" translate="no">elem</code>。
  136. </p>
  137. <p>为了调用它,我们需要为每一个立方体起个名字</p>
  138. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">
  139. const cubes = [
  140. - makeInstance(geometry, 0x44aa88, 0),
  141. - makeInstance(geometry, 0x8844aa, -2),
  142. - makeInstance(geometry, 0xaa8844, 2),
  143. + makeInstance(geometry, 0x44aa88, 0, 'Aqua'),
  144. + makeInstance(geometry, 0x8844aa, -2, 'Purple'),
  145. + makeInstance(geometry, 0xaa8844, 2, 'Gold'),
  146. ];</pre>
  147. <p>剩下的就是在渲染时定位Label元素。</p>
  148. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
  149. ...
  150. -cubes.forEach((cube, ndx) =&gt; {
  151. +cubes.forEach((cubeInfo, ndx) =&gt; {
  152. + const {cube, elem} = cubeInfo;
  153. const speed = 1 + ndx * .1;
  154. const rot = time * speed;
  155. cube.rotation.x = rot;
  156. cube.rotation.y = rot;
  157. + // 获取立方体中心的位置
  158. + cube.updateWorldMatrix(true, false);
  159. + cube.getWorldPosition(tempV);
  160. +
  161. + // 获取标准化屏幕坐标,x和y都会在-1和1区间
  162. + // x = -1 表示在最左侧
  163. + // y = -1 表示在最底部
  164. + tempV.project(camera);
  165. +
  166. + // 将标准屏幕坐标转化为CSS坐标
  167. + const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  168. + const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  169. +
  170. + // 将元素移动到此位置
  171. + elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  172. });</pre>
  173. <p>这样我们就有了与物体对齐的Label。</p>
  174. <p></p>
  175. <div translate="no" class="threejs_example_container notranslate">
  176. <div><iframe class="threejs_example notranslate" translate="no" style=" "
  177. src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-to-3d.html"></iframe></div>
  178. <a class="threejs_center" href="/manual/examples/align-html-to-3d.html" target="_blank">点击在新窗口打开</a>
  179. </div>
  180. <p></p>
  181. <p>这里有一些问题我们需要处理。</p>
  182. <p>一个是我们旋转对象,一旦它们重叠了,那么它们对应的Label可能也会重叠。</p>
  183. <div class="threejs_center"><img src="../resources/images/overlapping-labels.png" style="width: 307px;"></div>
  184. <p>另外一个问题是,我们缩小了视野,物体移出了视锥体范围,Label还是在显示。</p>
  185. <p>重叠对象的一种解决办法是 <a href="picking.html">用这篇文章中的拾取方法</a>,我们将传递对象在屏幕上的位置,然后调用<code class="notranslate"
  186. translate="no">RayCaster</code>来告诉我们和哪些对象相交了。
  187. 如果我们的对象不是结果的第一个,说明我们并不在它最前面。
  188. </p>
  189. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
  190. +const raycaster = new THREE.Raycaster();
  191. ...
  192. cubes.forEach((cubeInfo, ndx) =&gt; {
  193. const {cube, elem} = cubeInfo;
  194. const speed = 1 + ndx * .1;
  195. const rot = time * speed;
  196. cube.rotation.x = rot;
  197. cube.rotation.y = rot;
  198. // 获取立方体中心的位置
  199. cube.updateWorldMatrix(true, false);
  200. cube.getWorldPosition(tempV);
  201. // 获取标准化屏幕坐标,x和y都会在-1和1区间
  202. // x = -1 表示在最左侧
  203. // y = -1 表示在最底部
  204. tempV.project(camera);
  205. + // 调用Raycast获取所有相交的物体
  206. + // 以相机为起点,物体为终点
  207. + raycaster.setFromCamera(tempV, camera);
  208. + const intersectedObjects = raycaster.intersectObjects(scene.children);
  209. + // 如果第一个相交的是此物体,那么就是可见的
  210. + const show = intersectedObjects.length &amp;&amp; cube === intersectedObjects[0].object;
  211. +
  212. + if (!show) {
  213. + // 隐藏Label
  214. + elem.style.display = 'none';
  215. + } else {
  216. + // 显示Label
  217. + elem.style.display = '';
  218. // 将标准屏幕坐标转化为CSS坐标
  219. const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  220. const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  221. // 将元素移动到此位置
  222. elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  223. + }
  224. });</pre>
  225. <p>这解决了重叠问题。</p>
  226. <p>为了处理超出视锥体不可见的问题,我们通过检查 <code class="notranslate" translate="no">tempV.z</code>检查此对象的原点是否在截锥体之外。</p>
  227. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">- if (!show) {
  228. + if (!show || Math.abs(tempV.z) &gt; 1) {
  229. // 隐藏Label
  230. elem.style.display = 'none';</pre>
  231. <p>这 <em>部分工作</em> 有效是因为我们计算的标准化坐标包含一个<code class="notranslate" translate="no">z</code>
  232. 值,它从-1开始,也就是相机视锥体的 <code class="notranslate" translate="no">near</code> 值,
  233. +1结束,也就是相机视锥体的 <code class="notranslate" translate="no">far</code>值。</p>
  234. <p></p>
  235. <div translate="no" class="threejs_example_container notranslate">
  236. <div><iframe class="threejs_example notranslate" translate="no" style=" "
  237. src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-to-3d-w-hiding.html"></iframe>
  238. </div>
  239. <a class="threejs_center" href="/manual/examples/align-html-to-3d-w-hiding.html" target="_blank">点击在新窗口打开</a>
  240. <p></p>
  241. <p>对于视锥体检查,上面的解决方案失败了。因为我们只检查对象的原点,对于一个大对象,它的原点可能会超出视锥体,但是对象仍然有一部分处于可视范围内。</p>
  242. <p>更正确的解决方案是检查对象本身是否在视锥体中。不幸的是,检查很慢。对于3个立方体来说,这不是问题。但是其他情况不一定。</p>
  243. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 初始化
  244. const frustum = new THREE.Frustum();
  245. const viewProjection = new THREE.Matrix4();
  246. ...
  247. // 在检查前
  248. camera.updateMatrix();
  249. camera.updateMatrixWorld();
  250. camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
  251. ...
  252. // 然后,对每一个Mesh
  253. someMesh.updateMatrix();
  254. someMesh.updateMatrixWorld();
  255. viewProjection.multiplyMatrices(
  256. camera.projectionMatrix, camera.matrixWorldInverse);
  257. frustum.setFromProjectionMatrix(viewProjection);
  258. const inFrustum = frustum.contains(someMesh));</pre>
  259. <p>我们当前的重叠解决方案有类似的问题,拾取很慢。我们可以使用基于GPU的拾取方案, 参考<a href="picking.html">拾取章节</a>,不过它也并非没有代价。使用哪个解决方案取决于你的需要。</p>
  260. <p>另外一个问题是Label显示顺序,如果我们修改了代码以生成更长的Label</p>
  261. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cubes = [
  262. - makeInstance(geometry, 0x44aa88, 0, 'Aqua'),
  263. - makeInstance(geometry, 0x8844aa, -2, 'Purple'),
  264. - makeInstance(geometry, 0xaa8844, 2, 'Gold'),
  265. + makeInstance(geometry, 0x44aa88, 0, 'Aqua Colored Box'),
  266. + makeInstance(geometry, 0x8844aa, -2, 'Purple Colored Box'),
  267. + makeInstance(geometry, 0xaa8844, 2, 'Gold Colored Box'),
  268. ];</pre>
  269. <p>然后设置CSS让它们不换行</p>
  270. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels&gt;div {
  271. + white-space: nowrap;</pre>
  272. <p>然后我们可能就会遇到这个问题</p>
  273. <div class="threejs_center"><img src="../resources/images/label-sorting-issue.png" style="width: 401px;">
  274. </div>
  275. <p>你可以看到紫色盒子在后面,但它的Label却在水蓝色盒子的前面。</p>
  276. <p>我们可以修复这个问题,通过给每一个元素设置 <code class="notranslate" translate="no">zIndex</code>。投影生成的位置有一个 <code
  277. class="notranslate" translate="no">z</code> 值,
  278. -1表示最前面,1表示最后面。 <code class="notranslate" translate="no">zIndex</code> 却是一个整型,并且含义相反,
  279. <code class="notranslate" translate="no">zIndex</code>越大表示越靠前,所以下面的代码可能有用。
  280. </p>
  281. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 将标准屏幕坐标转化为CSS坐标
  282. const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  283. const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  284. // 将元素移动到此位置
  285. elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  286. +// 设置排序用的zIndex
  287. +elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;</pre>
  288. <p>由于投影 z 值的取值限制,我们需要选择一个大数来分散这些值,否则许多Label将具有相同的值。为了保证Label不和页面其他的部分重叠,通过设置 <code class="notranslate"
  289. translate="no">z-index</code> 给Label的容器,我们可以让浏览器创建一个新的 <a
  290. href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context">层叠上下文</a>
  291. </p>
  292. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels {
  293. position: absolute; /* 把自己定位在容器内 */
  294. + z-index: 0; /* 创建一个新的层叠上下文,这样子节点就不会和页面其他内容冲突 */
  295. left: 0; /* 默认定位在左上角 */
  296. top: 0;
  297. color: white;
  298. z-index: 0;
  299. }</pre>
  300. <p>现在Label应该总是按正确的顺序排列。</p>
  301. <p></p>
  302. <div translate="no" class="threejs_example_container notranslate">
  303. <div><iframe class="threejs_example notranslate" translate="no" style=" "
  304. src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-to-3d-w-sorting.html"></iframe>
  305. </div>
  306. <a class="threejs_center" href="/manual/examples/align-html-to-3d-w-sorting.html"
  307. target="_blank">点击在新窗口打开</a>
  308. </div>
  309. <p></p>
  310. <p>我们在这里用一个例子说明更多的问题。让我们像谷歌地球一样画一个地球仪并标记国家。</p>
  311. <p>我找到 <a href="http://thematicmapping.org/downloads/world_borders.php">这些数据</a>,
  312. 包含了各个国家的边界信息,用的协议是
  313. <a href="http://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>。
  314. </p>
  315. 加载这份数据, 可以生成国家的轮廓,大部分都带有国家的名称和定位。</p>
  316. <div class="threejs_center"><img src="../examples/resources/data/world/country-outlines-4k.png"
  317. style="background: black; width: 700px"></div>
  318. <p>JSON数据是一个类似这样结构的数组</p>
  319. <pre class="prettyprint showlinemods notranslate lang-json" translate="no">[
  320. {
  321. "name": "Algeria",
  322. "min": [
  323. -8.667223,
  324. 18.976387
  325. ],
  326. "max": [
  327. 11.986475,
  328. 37.091385
  329. ],
  330. "area": 238174,
  331. "lat": 28.163,
  332. "lon": 2.632,
  333. "population": {
  334. "2005": 32854159
  335. }
  336. },
  337. ...</pre>
  338. <p>其中min,max,lat,lon都是经纬度信息。</p>
  339. <p>开始加载它,这份代码是基于这篇<a href="optimize-lots-of-objects.html">优化大量对象</a>,尽管我们没有绘制大量对象,但我们将使用
  340. 相同的解决办法,和 <a href="rendering-on-demand.html">按需渲染</a> 方案一样。</p>
  341. <p>第一件事是创建一个球体,并且使用轮廓纹理。</p>
  342. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  343. const loader = new THREE.TextureLoader();
  344. const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
  345. const geometry = new THREE.SphereGeometry(1, 64, 32);
  346. const material = new THREE.MeshBasicMaterial({map: texture});
  347. scene.add(new THREE.Mesh(geometry, material));
  348. }</pre>
  349. <p>然后我们先创建一个loader,来加载JSON文件</p>
  350. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">async function loadJSON(url) {
  351. const req = await fetch(url);
  352. return req.json();
  353. }</pre>
  354. <p>然后调用</p>
  355. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let countryInfos;
  356. async function loadCountryData() {
  357. countryInfos = await loadJSON('resources/data/world/country-info.json');
  358. ...
  359. }
  360. requestRenderIfNotRequested();
  361. }
  362. loadCountryData();</pre>
  363. <p>现在让我们用这些数据来生成和放置Labels</p>
  364. <p>在这一篇文章 <a href="optimize-lots-of-objects.html">优化大量对象</a>,
  365. 我们已经创建了一个小辅助对象,以便于计算地球上的经纬度位置,具体可以看看这篇文章是如何解释它们怎么工作的。</p>
  366. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lonFudge = Math.PI * 1.5;
  367. const latFudge = Math.PI;
  368. // 这些小工具会使得盒模型定位非常容易
  369. // 我们可以旋转lonHelper Y轴上的分量到经度上
  370. const lonHelper = new THREE.Object3D();
  371. // 我们可以旋转latHelper X轴上的分量到纬度上
  372. const latHelper = new THREE.Object3D();
  373. lonHelper.add(latHelper);
  374. // positionHelper将对象移动到球体的边缘
  375. const positionHelper = new THREE.Object3D();
  376. positionHelper.position.z = 1;
  377. latHelper.add(positionHelper);</pre>
  378. <p>我们将使用它去计算每一个Label的位置</p>
  379. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const labelParentElem = document.querySelector('#labels');
  380. for (const countryInfo of countryInfos) {
  381. const {lat, lon, name} = countryInfo;
  382. // 调整helper,旋转指向经纬度点的位置
  383. lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
  384. latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
  385. // 获取经纬度位置
  386. positionHelper.updateWorldMatrix(true, false);
  387. const position = new THREE.Vector3();
  388. positionHelper.getWorldPosition(position);
  389. countryInfo.position = position;
  390. // 给每一个国家添加一个Label
  391. const elem = document.createElement('div');
  392. elem.textContent = name;
  393. labelParentElem.appendChild(elem);
  394. countryInfo.elem = elem;</pre>
  395. <p>上面的代码看起来非常类似于我们为制作立方体Label而编写的代码,每个Label对应一个元素,完成后我们有一个数组 <code class="notranslate"
  396. translate="no">countryInfos</code>,
  397. 对于我们添加的每个国家/地区都有一个 <code class="notranslate" translate="no">elem</code>
  398. 属性代表Label元素 和一个 <code class="notranslate" translate="no">position</code> 代表它的位置。</p>
  399. <p>就像我们对立方体所做的那样,我们需要在渲染的时候先更新Label。</p>
  400. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
  401. function updateLabels() {
  402. // 如果JSON文件还没加载进来,就退出
  403. if (!countryInfos) {
  404. return;
  405. }
  406. for (const countryInfo of countryInfos) {
  407. const {position, elem} = countryInfo;
  408. // 获取标准化屏幕坐标,x和y都会在-1和1区间
  409. // x = -1 表示在最左侧
  410. // y = -1 表示在最底部
  411. tempV.copy(position);
  412. tempV.project(camera);
  413. // 将标准屏幕坐标转化为CSS坐标
  414. const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  415. const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  416. // 将元素移动到此位置
  417. elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  418. // 设置排序用的zIndex
  419. elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
  420. }
  421. }</pre>
  422. <p>您可以看到上面的代码与之前的立方体示例基本类似,唯一的区别我们在初始化时预先计算了Label位置,我们可以这样做因为地球上的国家永远不会移动,只有我们的相机在移动。</p>
  423. <p>然后我们需要在我们的渲染循环中调用 <code class="notranslate" translate="no">updateLabels</code> </p>
  424. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() {
  425. renderRequested = false;
  426. if (resizeRendererToDisplaySize(renderer)) {
  427. const canvas = renderer.domElement;
  428. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  429. camera.updateProjectionMatrix();
  430. }
  431. controls.update();
  432. + updateLabels();
  433. renderer.render(scene, camera);
  434. }</pre>
  435. <p>这就是我们得到的结果</p>
  436. <p></p>
  437. <div translate="no" class="threejs_example_container notranslate">
  438. <div><iframe class="threejs_example notranslate" translate="no" style=" "
  439. src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-elements-to-3d-globe-too-many-labels.html"></iframe>
  440. </div>
  441. <a class="threejs_center" href="/manual/examples/align-html-elements-to-3d-globe-too-many-labels.html"
  442. target="_blank">点击在新窗口打开</a>
  443. </div>
  444. <p></p>
  445. <p>整出了密集恐惧症!</p>
  446. <p>现在有两个问题:</p>
  447. <ol>
  448. <li>
  449. <p>出现了背对我们的Label</p>
  450. </li>
  451. <li>
  452. <p>Label真的太多了</p>
  453. </li>
  454. </ol>
  455. <p>对于 问题#1 我们不能像上面那种方式使用 <code class="notranslate" translate="no">RayCaster</code> ,因为除了地球以外没有什么可相交的。相反,我们可以
  456. 检查特定的国家是否远离我们,这是可行的,因为Label的位置围绕的是一个球体。事实上,我们使用的是一个半径1.0的单位球体,这意味着这些位置已经是单位向量,数学计算上比较简单。</p>
  457. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3();
  458. +const cameraToPoint = new THREE.Vector3();
  459. +const cameraPosition = new THREE.Vector3();
  460. +const normalMatrix = new THREE.Matrix3();
  461. function updateLabels() {
  462. // 如果JSON文件还没加载进来,就退出
  463. if (!countryInfos) {
  464. return;
  465. }
  466. + const minVisibleDot = 0.2;
  467. + // 获取表示相机相对方向的变换矩阵
  468. + normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
  469. + // 获取相机的世界坐标
  470. + camera.getWorldPosition(cameraPosition);
  471. for (const countryInfo of countryInfos) {
  472. const {position, elem} = countryInfo;
  473. + // 根据相机的方向定位位置
  474. + // 由于球体在原点并且球体是半径为1.0的单位球体
  475. + // 这就能获取相对于相机的单位向量
  476. + tempV.copy(position);
  477. + tempV.applyMatrix3(normalMatrix);
  478. +
  479. + // 计算从相机到这个位置的方向向量
  480. + cameraToPoint.copy(position);
  481. + cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize();
  482. +
  483. + // 求得相机方向 和相机连点方向 的点积.
  484. + // 1 = 正对相机
  485. + // 0 = 相对于相机而言,位于球体的边缘
  486. + // &lt; 0 = 远离相机
  487. + const dot = tempV.dot(cameraToPoint);
  488. +
  489. + // 如果方向不面向我们,隐藏它
  490. + if (dot &lt; minVisibleDot) {
  491. + elem.style.display = 'none';
  492. + continue;
  493. + }
  494. +
  495. + // 将元素恢复为其默认显示样式
  496. + elem.style.display = '';
  497. // 获取标准化屏幕坐标,x和y都会在-1和1区间
  498. // x = -1 表示在最左侧
  499. // y = -1 表示在最底部
  500. tempV.copy(position);
  501. tempV.project(camera);
  502. // 将标准屏幕坐标转化为CSS坐标
  503. const x = (tempV.x * .5 + .5) * canvas.clientWidth;
  504. const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
  505. // 将元素移动到此位置
  506. countryInfo.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  507. // 设置排序用的zIndex
  508. elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
  509. }
  510. }</pre>
  511. <p>
  512. 上面我们使用位置作为方向向量并获得相对于相机的位置,点乘得到向量之间的余弦值,这给了我们一个-1到1之间的值,其中-1表示正对相机,0表示相对于相机球体的边缘上,大于0表示处在后方。然后我们使用该值来显示或隐藏元素。
  513. </p>
  514. <div class="spread">
  515. <div>
  516. <div data-diagram="dotProduct" style="height: 400px"></div>
  517. </div>
  518. </div>
  519. <p>
  520. 在上图中,我们可以看到Label方向的点乘方向是从相机指向该位置的方向。如果你旋转角度,你会看到正对相机时点乘结果为-1.0,正好在球体相对相机的切线上时为0.0,或者换一种说法,两个向量互相垂直点乘结果为0,夹角大于90度时,Label在球体后面。
  521. </p>
  522. <p>对于 问题#2,Label太多了,我们需要一些方法来决定显示哪些。一种方式是只显示大国的Label,我们加载的数据包含一个国家包含经纬度的最大和最小值,从中我们可以计算出一个区域,然后用它来判断是否显示国家。
  523. </p>
  524. <p>开始的时候我们先计算区域面积</p>
  525. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const labelParentElem = document.querySelector('#labels');
  526. for (const countryInfo of countryInfos) {
  527. const {lat, lon, min, max, name} = countryInfo;
  528. // 调整helper,旋转指向经纬度点的位置
  529. lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
  530. latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
  531. // 获取经纬度位置
  532. positionHelper.updateWorldMatrix(true, false);
  533. const position = new THREE.Vector3();
  534. positionHelper.getWorldPosition(position);
  535. countryInfo.position = position;
  536. + // 计算每个国家的面积
  537. + const width = max[0] - min[0];
  538. + const height = max[1] - min[1];
  539. + const area = width * height;
  540. + countryInfo.area = area;
  541. // a给每一个国家添加一个Label
  542. const elem = document.createElement('div');
  543. elem.textContent = name;
  544. labelParentElem.appendChild(elem);
  545. countryInfo.elem = elem;
  546. }</pre>
  547. <p>然后在渲染时让我们根据区域来决定是否显示Label</p>
  548. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const large = 20 * 20;
  549. const maxVisibleDot = 0.2;
  550. // 获取表示相机相对方向的变换矩阵
  551. normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
  552. // 获取相机的世界坐标
  553. camera.getWorldPosition(cameraPosition);
  554. for (const countryInfo of countryInfos) {
  555. - const {position, elem} = countryInfo;
  556. + const {position, elem, area} = countryInfo;
  557. + // large enough?
  558. + if (area &lt; large) {
  559. + elem.style.display = 'none';
  560. + continue;
  561. + }
  562. ...</pre>
  563. <p>最后,由于我不确定这些值设多少好,于是添加一个GUI,就可以调试了</p>
  564. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  565. import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  566. +import {GUI} from 'three/addons/libs/lil-gui.module.min.js';</pre>
  567. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const settings = {
  568. + minArea: 20,
  569. + maxVisibleDot: -0.2,
  570. +};
  571. +const gui = new GUI({width: 300});
  572. +gui.add(settings, 'minArea', 0, 50).onChange(requestRenderIfNotRequested);
  573. +gui.add(settings, 'maxVisibleDot', -1, 1, 0.01).onChange(requestRenderIfNotRequested);
  574. function updateLabels() {
  575. if (!countryInfos) {
  576. return;
  577. }
  578. - const large = 20 * 20;
  579. - const maxVisibleDot = -0.2;
  580. + const large = settings.minArea * settings.minArea;
  581. // 获取表示相机相对方向的变换矩阵
  582. normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
  583. // 获取相机的世界坐标
  584. camera.getWorldPosition(cameraPosition);
  585. for (const countryInfo of countryInfos) {
  586. ...
  587. // 如果方向不面向我们,隐藏它
  588. - if (dot &gt; maxVisibleDot) {
  589. + if (dot &gt; settings.maxVisibleDot) {
  590. elem.style.display = 'none';
  591. continue;
  592. }</pre>
  593. <p>结果出来了</p>
  594. <p></p>
  595. <div translate="no" class="threejs_example_container notranslate">
  596. <div><iframe class="threejs_example notranslate" translate="no" style=" "
  597. src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-elements-to-3d-globe.html"></iframe>
  598. </div>
  599. <a class="threejs_center" href="/manual/examples/align-html-elements-to-3d-globe.html"
  600. target="_blank">点击在新窗口打开</a>
  601. </div>
  602. <p></p>
  603. <p>你可以看到,随着你的旋转,后面地球的Label消失了。
  604. 调整 <code class="notranslate" translate="no">minVisibleDot</code> 可以查看阈值的变化。
  605. 你也可以调整 <code class="notranslate" translate="no">minArea</code> 可以看到更大或更小的国家出现。</p>
  606. </div>
  607. <p>
  608. 我在这方面做得越多,就越意识到谷歌地图做了多少工作。他们还必须决定使用哪些Label来显示。我很确定他们使用各种信息,例如你现在的位置、你的默认语言设置、你的帐户设置(如果你有的话),他们可能使用人口数量或人气程度,他们可能会优先考虑到视图中心的国家,等等……要考虑很多。
  609. </p>
  610. <p>无论如何,我希望这些示例能让你了解如何用HTML对齐你的3D元素,我也或许会做出小小的贡献。</p>
  611. <p>下一步我们来实现 <a href="indexed-textures.html">拾取和高亮一个城市</a>。</p>
  612. </div>
  613. </div>
  614. </div>
  615. <p>
  616. <link rel="stylesheet" href="../resources/threejs-align-html-elements-to-3d.css">
  617. </p>
  618. <script type="module" src="../resources/threejs-align-html-elements-to-3d.js"></script>
  619. <script src="../resources/prettify.js"></script>
  620. <script src="../resources/lesson.js"></script>
  621. </body>
  622. </html>