You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
earthquake_3d_viewer_front/three/manual/zh/align-html-elements-to-3d.html

681 lines
32 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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">
<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">
&lt;body&gt;
- &lt;canvas id="c"&gt;&lt;/canvas&gt;
+ &lt;div id="container"&gt;
+ &lt;canvas id="c"&gt;&lt;/canvas&gt;
+ &lt;div id="labels"&gt;&lt;/div&gt;
+ &lt;/div&gt;
&lt;/body&gt;</pre>
<p>通过将Canvas元素和 <code class="notranslate" translate="no">&lt;div id="labels"&gt;</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&gt;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&gt;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">&lt;div&gt;</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) =&gt; {
+cubes.forEach((cubeInfo, ndx) =&gt; {
+ 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) =&gt; {
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 &amp;&amp; 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) &gt; 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&gt;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 = 相对于相机而言,位于球体的边缘
+ // &lt; 0 = 远离相机
+ const dot = tempV.dot(cameraToPoint);
+
+ // 如果方向不面向我们,隐藏它
+ if (dot &lt; 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>对于 问题#2Label太多了我们需要一些方法来决定显示哪些。一种方式是只显示大国的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 &lt; 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 &gt; maxVisibleDot) {
+ if (dot &gt; 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>