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/indexed-textures.html

603 lines
29 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>使用纹理索引来拾取和着色</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 Indexed Textures for Picking and Color">
<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>使用纹理索引来拾取和着色</h1>
</div>
<div class="lesson">
<div class="lesson-main">
<p>这篇文章是 <a href="align-html-elements-to-3d.html">对齐HTML元素到3D对象</a> 的延续。
如果你还没有读过上篇文章,你应该先从那里开始,然后再回来继续阅读。</p>
<p>有时候使用Three.js需要提出一些创造性的解决思路。我不确定这是一个很好的解决方案但我想我会分享它你可以看看是否可以为你的需求提供了一些解决思路或方案。</p>
<p><a href="align-html-elements-to-3d.html">上一篇文章中</a>我们在3D地球周围显示了国家名称那么我们如何做到让用户选中一个国家并高亮他的选择</p>
<p>第一个想法是为每个国家生成几何图形,我们可以 <a href="picking.html">使用射线拾取</a> ,就像之前介绍的那样。
我们将为每个国家构建3D几何对象。如果用户点击代表那个国家的网格对象我们就会知道对应的国家被点击了。</p>
<p>所以为了验证这个解决方案我尝试生成所有国家的3D网格对象使用了<a href="align-html-elements-to-3d.html">在上一篇文章中</a>和我生成轮廓一样的数据。
结果生成了15.5m的二进制GLTF(.glb)文件让用户下载15.5m的数据对于我来说实在太多了。
</p>
<p>有很多方法可以压缩数据。第一种可能是应用一些算法来降低轮廓的分辨率,但是我没有花时间来研究它。可能出现美国边界变大而加拿大边界变小的情况。</p>
<p>另一种解决方案是仅使用数据压缩比如gzip将其降至11m这减少了30%,但是还不够。</p>
<p>我们可以将所有数据存储为16位而不是32位浮点值。或者我们也可以使用像<a href="https://google.github.io/draco/">draco 压缩</a>
这种东西也许就够了。不过我没有去试,我推荐你去试下回来告诉我是怎么回事,因为我很想知道😅</p>
<p>就我而言,我考虑使用 <a href="picking.html">GPU拾取方案</a>
这在上一篇 <a href="picking.html">关于拾取的文章</a> 的最后有提到。这种方案中我们使用一种独特的颜色代表不同网格对象的ID然后我们绘制了所有网格看看哪个颜色被点击了。</p>
<p>基于这种灵感我们可以预先生成一张国家的地图每个国家的颜色是它在国家数组中的索引号。我们可以使用类似GPU拾取技术我们使用索引纹理绘制一个离屏全局画布查看颜色会告诉我们用户点击了那个国家ID。</p>
<p>因此,我 <a href="https://github.com/mrdoob/three.js/blob/master/manual/resources/tools/geo-picking/">写了一些代码</a>
生成这样的一个纹理,在这里: </p>
<div class="threejs_center"><img src="../examples/resources/data/world/country-index-texture.png"
style="width: 700px;"></div>
<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>它只有217k比国家网格对象的15m要好得多事实上我们可以使用更低的分辨率但现在217k似乎已经足够了。</p>
<p>所以让我们试着用它来选择国家。</p>
<p><a href="picking.html">GPU拾取案例中</a> 获取代码,我们需要一个场景来做拾取。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickingScene = new THREE.Scene();
pickingScene.background = new THREE.Color(0);
</pre>
<p>我们需要将带有索引纹理的地球添加到拾取场景中。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
const loader = new THREE.TextureLoader();
const geometry = new THREE.SphereGeometry(1, 64, 32);
+ const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
+ indexTexture.minFilter = THREE.NearestFilter;
+ indexTexture.magFilter = THREE.NearestFilter;
+
+ const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
+ pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
const material = new THREE.MeshBasicMaterial({map: texture});
scene.add(new THREE.Mesh(geometry, material));
}
</pre>
<p>然后我们把 <code class="notranslate" translate="no">GPUPickingHelper</code>这个类拷贝下,在使用前我们需要做一些小改动</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class GPUPickHelper {
constructor() {
// 创造一个 1x1 的渲染对象
this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
this.pixelBuffer = new Uint8Array(4);
- this.pickedObject = null;
- this.pickedObjectSavedColor = 0;
}
pick(cssPosition, scene, camera) {
const {pickingTexture, pixelBuffer} = this;
// 将视图偏移设置为仅表示鼠标下单个元素
const pixelRatio = renderer.getPixelRatio();
camera.setViewOffset(
renderer.getContext().drawingBufferWidth, // full width
renderer.getContext().drawingBufferHeight, // full top
cssPosition.x * pixelRatio | 0, // rect x
cssPosition.y * pixelRatio | 0, // rect y
1, // rect width
1, // rect height
);
// 渲染场景
renderer.setRenderTarget(pickingTexture);
renderer.render(scene, camera);
renderer.setRenderTarget(null);
// 清除视图偏移,使渲染恢复正常
camera.clearViewOffset();
// 读取像素
renderer.readRenderTargetPixels(
pickingTexture,
0, // x
0, // y
1, // width
1, // height
pixelBuffer);
+ const id =
+ (pixelBuffer[0] &lt;&lt; 16) |
+ (pixelBuffer[1] &lt;&lt; 8) |
+ (pixelBuffer[2] &lt;&lt; 0);
+
+ return id;
- const id =
- (pixelBuffer[0] &lt;&lt; 16) |
- (pixelBuffer[1] &lt;&lt; 8) |
- (pixelBuffer[2] );
- const intersectedObject = idToObject[id];
- if (intersectedObject) {
- // 获取第一个对象,它是离我们最近的
- this.pickedObject = intersectedObject;
- // 保存它的颜色
- this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
- // 将其自发光颜色设置为闪烁的红色/黄色
- this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
- }
}
}
</pre>
<p>现在我们可以用它来选择国家了。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const pickHelper = new GPUPickHelper();
function getCanvasRelativePosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * canvas.width / rect.width,
y: (event.clientY - rect.top ) * canvas.height / rect.height,
};
}
function pickCountry(event) {
// 如果我们还没有加载好数据,退出
if (!countryInfos) {
return;
}
const position = getCanvasRelativePosition(event);
const id = pickHelper.pick(position, pickingScene, camera);
if (id &gt; 0) {
// 我们点击了一个国家修改它的selected属性
const countryInfo = countryInfos[id - 1];
const selected = !countryInfo.selected;
// 如果我们选中这个国家,并且没有按住控制键,取消所有选中的国家
if (selected &amp;&amp; !event.shiftKey &amp;&amp; !event.ctrlKey &amp;&amp; !event.metaKey) {
unselectAllCountries();
}
numCountriesSelected += selected ? 1 : -1;
countryInfo.selected = selected;
} else if (numCountriesSelected) {
// 海洋或者天空被选中了
unselectAllCountries();
}
requestRenderIfNotRequested();
}
function unselectAllCountries() {
numCountriesSelected = 0;
countryInfos.forEach((countryInfo) =&gt; {
countryInfo.selected = false;
});
}
canvas.addEventListener('pointerup', pickCountry);
</pre>
<p>上面的代码,设置/重置了国家数组元素的 <code class="notranslate" translate="no">selected</code> 属性。如果 <code class="notranslate"
translate="no">shift</code><code class="notranslate" translate="no">ctrl</code><code
class="notranslate" translate="no">cmd</code>
被按下了,你就可以选择多个国家。</p>
<p>剩下的就是显示选择的国家,现在让我们更新标签</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function updateLabels() {
// 如果我们还没有加载好数据,退出
if (!countryInfos) {
return;
}
const large = settings.minArea * settings.minArea;
// 获取表示相机正对方向的矩阵
normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
// 获取相机位置
camera.getWorldPosition(cameraPosition);
for (const countryInfo of countryInfos) {
- const {position, elem, area} = countryInfo;
- // 足够大了?
- if (area &lt; large) {
+ const {position, elem, area, selected} = countryInfo;
+ const largeEnough = area &gt;= large;
+ const show = selected || (numCountriesSelected === 0 &amp;&amp; largeEnough);
+ if (!show) {
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/indexed-textures-picking.html"></iframe>
</div>
<a class="threejs_center" href="/manual/examples/indexed-textures-picking.html" target="_blank">点击在新窗口打开</a>
</div>
<p></p>
<p>代码仍然会根据地区显示对应的国家。不过如果你点击一个,只会显示对应的标签。</p>
<p>所以这似乎是选择国家的有效解决方案,但是如何突出显示选定的国家?</p>
<p>我们可以从 <em>调色板图形算法</em> 中获取灵感。</p>
<p><a href="https://en.wikipedia.org/wiki/Palette_%28computing%29">调色板算法</a>
或者 <a href="https://en.wikipedia.org/wiki/Indexed_color">索引颜色</a>
是被旧的系统比如Atari 800、Amiga、NES、Super Nintendolder及IBM
PCs所使用的。并非以RGBA的形式给每个颜色存储8位、每个像素至少32字节的位图他们存储位图是8位或者更少。每个像素都是一个调色板的索引值
所以举例一个像素值为3表示“显示3号颜色值”而定义3号颜色值的地方就叫“调色板”。</p>
<p>在JavaScript中就像这样</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const face7x7PixelImageData = [
0, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 0, 1,
1, 0, 2, 0, 2, 0, 1,
1, 0, 0, 0, 0, 0, 1,
1, 0, 3, 3, 3, 0, 1,
1, 0, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 1, 1,
];
const palette = [
[255, 255, 255], // 白
[ 0, 0, 0], // 黑
[ 0, 255, 255], // 青
[255, 0, 0], // 红
];
</pre>
<p>图像数据中每个像素都是调色板的索引,如果你分析上面调色板的数据你会得到这个图像:</p>
<div class="threejs_center"><img src="../resources/images/7x7-indexed-face.png"></div>
<p>
在我们的例子中上面已经有一个用不同id代表不同国家的纹理了所以我们可以通过调色板使用相同的纹理赋予每个国家各自的颜色。通过更改调色板颜色我们可以为单独的国家着色。比如通过设置整个调色板纹理为黑色给某个国家使用不同的颜色就可以凸显那个国家了。
</p>
<p>要做调色板索引的话需要一些自定义着色器代码让我们修改Three.js中默认的着色器这样我们也可以根据需要使用照明或其他特性。</p>
<p>就像我们在 <a href="optimize-lots-of-objects-animated.html">大量移动物体的优化</a> 这篇文章中提到的,通过
<code class="notranslate" translate="no">onBeforeCompile</code> 属性,我们可以通过向材质添加函数来修改默认的着色器。
</p>
<p>默认的片元着色器在编译之前看起来就像这样:</p>
<pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">#include &lt;common&gt;
#include &lt;color_pars_fragment&gt;
#include &lt;uv_pars_fragment&gt;
#include &lt;map_pars_fragment&gt;
#include &lt;alphamap_pars_fragment&gt;
#include &lt;aomap_pars_fragment&gt;
#include &lt;lightmap_pars_fragment&gt;
#include &lt;envmap_pars_fragment&gt;
#include &lt;fog_pars_fragment&gt;
#include &lt;specularmap_pars_fragment&gt;
#include &lt;logdepthbuf_pars_fragment&gt;
#include &lt;clipping_planes_pars_fragment&gt;
void main() {
#include &lt;clipping_planes_fragment&gt;
vec4 diffuseColor = vec4( diffuse, opacity );
#include &lt;logdepthbuf_fragment&gt;
#include &lt;map_fragment&gt;
#include &lt;color_fragment&gt;
#include &lt;alphamap_fragment&gt;
#include &lt;alphatest_fragment&gt;
#include &lt;specularmap_fragment&gt;
ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
#ifdef USE_LIGHTMAP
reflectedLight.indirectDiffuse += texture2D( lightMap, vLightMapUv ).xyz * lightMapIntensity;
#else
reflectedLight.indirectDiffuse += vec3( 1.0 );
#endif
#include &lt;aomap_fragment&gt;
reflectedLight.indirectDiffuse *= diffuseColor.rgb;
vec3 outgoingLight = reflectedLight.indirectDiffuse;
#include &lt;envmap_fragment&gt;
gl_FragColor = vec4( outgoingLight, diffuseColor.a );
#include &lt;premultiplied_alpha_fragment&gt;
#include &lt;tonemapping_fragment&gt;
#include &lt;colorspace_fragment&gt;
#include &lt;fog_fragment&gt;
}
</pre>
<p><a href="https://github.com/mrdoob/three.js/tree/dev/src/renderers/shaders/ShaderChunk">查看所有的片段</a>
我们发现THREE.js使用了一个名为 <code class="notranslate" translate="no">diffuseColor</code> 的变量去管理基本材质颜色。它在这里设置: <code
class="notranslate" translate="no">&lt;color_fragment&gt;</code> <a
href="https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/color_fragment.glsl.js">片段</a>
所以我们应该能够在这部分之后进行修改。</p>
<p><code class="notranslate" translate="no">diffuseColor</code>
在这个时刻应该已经是从我们轮廓纹理中获取的颜色了,所以我们应该可以从调色盘中获取颜色,然后把他们和最终颜色混合。</p>
<p>就像我们 <a href="optimize-lots-of-objects-animated.html">之前做的那样</a>
<a href="/docs/#api/en/materials/Material.onBeforeCompile"><code class="notranslate"
translate="no">Material.onBeforeCompile</code></a>我们使用一个用来搜索和替换着色器代码的数组。
</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
const loader = new THREE.TextureLoader();
const geometry = new THREE.SphereGeometry(1, 64, 32);
const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
indexTexture.minFilter = THREE.NearestFilter;
indexTexture.magFilter = THREE.NearestFilter;
const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
+ const fragmentShaderReplacements = [
+ {
+ from: '#include &lt;common&gt;',
+ to: `
+ #include &lt;common&gt;
+ uniform sampler2D indexTexture;
+ uniform sampler2D paletteTexture;
+ uniform float paletteTextureWidth;
+ `,
+ },
+ {
+ from: '#include &lt;color_fragment&gt;',
+ to: `
+ #include &lt;color_fragment&gt;
+ {
+ vec4 indexColor = texture2D(indexTexture, vUv);
+ float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
+ vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
+ vec4 paletteColor = texture2D(paletteTexture, paletteUV);
+ // diffuseColor.rgb += paletteColor.rgb; // 白轮廓
+ diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb; // 黑轮廓
+ }
+ `,
+ },
+ ];
const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
const material = new THREE.MeshBasicMaterial({map: texture});
+ material.onBeforeCompile = function(shader) {
+ fragmentShaderReplacements.forEach((rep) =&gt; {
+ shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
+ });
+ };
scene.add(new THREE.Mesh(geometry, material));
}
</pre>
<p>在上面可以看到我们添加了3个uniforms变量<code class="notranslate" translate="no">indexTexture</code>, <code
class="notranslate" translate="no">paletteTexture</code>,
and <code class="notranslate" translate="no">paletteTextureWidth</code>。我们从 <code class="notranslate"
translate="no">indexTexture</code>
获取颜色,并且把它转化成索引下标。 <code class="notranslate" translate="no">vUv</code>
是由Three.js提供的纹理坐标。然后我们使用索引下标从调色板中获取颜色。然后我们使用当前的 <code class="notranslate"
translate="no">diffuseColor</code>和最终的结果作混合。
<code class="notranslate"
translate="no">diffuseColor</code>在此时是我们黑色纹理,而调色盘是白色纹理。所以如果我们相加两个颜色,得出的是白色轮廓。如果我们二者相减,得出的是黑色轮廓。
</p>
<p>在我们渲染前我们还需要设置调色板纹理以及这3个uniforms变量。</p>
<p>对于调色板纹理,它只需要足够宽即可。每个国家保留一种颜色 + 一种海洋颜色。这里有240个国家或地区我们可以等到国家列表加载完成后以获取确切的数字来查找。不过选择一些更大的数字没有危害所以让我们选择512。</p>
<p>这里是创建调色板的代码</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const maxNumCountries = 512;
const paletteTextureWidth = maxNumCountries;
const paletteTextureHeight = 1;
const palette = new Uint8Array(paletteTextureWidth * 4);
const paletteTexture = new THREE.DataTexture(
palette, paletteTextureWidth, paletteTextureHeight);
paletteTexture.minFilter = THREE.NearestFilter;
paletteTexture.magFilter = THREE.NearestFilter;
</pre>
<p>一个<a href="/docs/#api/en/textures/DataTexture"><code class="notranslate"
translate="no">DataTexture</code></a> 提供原始的纹理数据。在这种情况下我们给他512个RGBA颜色每个颜色4字节每个包含0-255的红、绿、蓝分量。</p>
<p>让我们用随机颜色填充它,只是为了看看它是否有效</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let i = 1; i &lt; palette.length; ++i) {
palette[i] = Math.random() * 256;
}
// 设置海洋颜色 (索引 #0)
palette.set([100, 200, 255, 255], 0);
paletteTexture.needsUpdate = true;
</pre>
<p>任何时候我们想要Three.js通过 <code class="notranslate" translate="no">palette</code> 数组的内容来更新调色板上的纹理,我们需要去设置<code
class="notranslate" translate="no">paletteTexture.needsUpdate</code>
<code class="notranslate" translate="no">true</code></p>
<p>然后我们还需要设置材质上的uniforms变量</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const geometry = new THREE.SphereGeometry(1, 64, 32);
const material = new THREE.MeshBasicMaterial({map: texture});
material.onBeforeCompile = function(shader) {
fragmentShaderReplacements.forEach((rep) =&gt; {
shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
});
+ shader.uniforms.paletteTexture = {value: paletteTexture};
+ shader.uniforms.indexTexture = {value: indexTexture};
+ shader.uniforms.paletteTextureWidth = {value: paletteTextureWidth};
};
scene.add(new THREE.Mesh(geometry, material));
</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/indexed-textures-random-colors.html"></iframe>
</div>
<a class="threejs_center" href="/manual/examples/indexed-textures-random-colors.html" target="_blank">click
here to open in a separate window</a>
</div>
<p></p>
<p>现在我们可以看到索引和调色板纹理生效了,让我们控制调色板进行高亮显示</p>
<p>首先让我们创建一个函数我们传入一个THREE.js颜色并格式化为可以放入调色板纹理的值。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tempColor = new THREE.Color();
function get255BasedColor(color) {
tempColor.set(color);
const base = tempColor.toArray().map(v =&gt; v * 255);
base.push(255); // alpha
return base;
}
</pre>
<p>像这样来调用 <code class="notranslate" translate="no">color = get255BasedColor('red')</code> 会返回一个像 <code
class="notranslate" translate="no">[255, 0, 0, 255]</code>这样的数组。</p>
<p>接下来让我们用它来生成一些颜色并填充调色板。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const selectedColor = get255BasedColor('red');
const unselectedColor = get255BasedColor('#444');
const oceanColor = get255BasedColor('rgb(100,200,255)');
resetPalette();
function setPaletteColor(index, color) {
palette.set(color, index * 4);
}
function resetPalette() {
// 让所有的颜色都是未选择状态的颜色
for (let i = 1; i &lt; maxNumCountries; ++i) {
setPaletteColor(i, unselectedColor);
}
// 设置海洋颜色 (索引 #0)
setPaletteColor(0, oceanColor);
paletteTexture.needsUpdate = true;
}
</pre>
<p>现在让我们使用这些函数来更新调色板,当一个国家被选中时:</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * canvas.width / rect.width,
y: (event.clientY - rect.top ) * canvas.height / rect.height,
};
}
function pickCountry(event) {
// 如果我们还没有加载好数据,退出
if (!countryInfos) {
return;
}
const position = getCanvasRelativePosition(event);
const id = pickHelper.pick(position, pickingScene, camera);
if (id &gt; 0) {
const countryInfo = countryInfos[id - 1];
const selected = !countryInfo.selected;
if (selected &amp;&amp; !event.shiftKey &amp;&amp; !event.ctrlKey &amp;&amp; !event.metaKey) {
unselectAllCountries();
}
numCountriesSelected += selected ? 1 : -1;
countryInfo.selected = selected;
+ setPaletteColor(id, selected ? selectedColor : unselectedColor);
+ paletteTexture.needsUpdate = true;
} else if (numCountriesSelected) {
unselectAllCountries();
}
requestRenderIfNotRequested();
}
function unselectAllCountries() {
numCountriesSelected = 0;
countryInfos.forEach((countryInfo) =&gt; {
countryInfo.selected = false;
});
+ resetPalette();
}
</pre>
<p>我们应该能够突出显示1个或多个国家。</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/indexed-textures-picking-and-highlighting.html"></iframe>
</div>
<a class="threejs_center" href="/manual/examples/indexed-textures-picking-and-highlighting.html"
target="_blank">点击在新窗口打开</a>
</div>
<p></p>
<p>这看起来有效!</p>
<p>一件小事是我们不能不改变选中状态就渲染地球。如果我们选择一个国家然后想要旋转地球,选中状态将改变。</p>
<p>让我们尝试解决这个问题。我认为,我们可以检查两件事情。点击和松开经过了多少时间;用户是否移动了鼠标。如果时间很短,或者他们没有移动鼠标,那么这个行为可能是点击。否则他们可能正在尝试拖动地球。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const maxClickTimeMs = 200;
+const maxMoveDeltaSq = 5 * 5;
+const startPosition = {};
+let startTimeMs;
+
+function recordStartTimeAndPosition(event) {
+ startTimeMs = performance.now();
+ const pos = getCanvasRelativePosition(event);
+ startPosition.x = pos.x;
+ startPosition.y = pos.y;
+}
function getCanvasRelativePosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * canvas.width / rect.width,
y: (event.clientY - rect.top ) * canvas.height / rect.height,
};
}
function pickCountry(event) {
// exit if we have not loaded the data yet
if (!countryInfos) {
return;
}
+ // 如果用户触发后已经过了一段时间了
+ // 就认为这是一个拖动行为
+ const clickTimeMs = performance.now() - startTimeMs;
+ if (clickTimeMs &gt; maxClickTimeMs) {
+ return;
+ }
+
+ // 如果鼠标移动了,就认为这是一个拖动行为
+ const position = getCanvasRelativePosition(event);
+ const moveDeltaSq = (startPosition.x - position.x) ** 2 +
+ (startPosition.y - position.y) ** 2;
+ if (moveDeltaSq &gt; maxMoveDeltaSq) {
+ return;
+ }
- const position = {x: event.clientX, y: event.clientY};
const id = pickHelper.pick(position, pickingScene, camera);
if (id &gt; 0) {
const countryInfo = countryInfos[id - 1];
const selected = !countryInfo.selected;
if (selected &amp;&amp; !event.shiftKey &amp;&amp; !event.ctrlKey &amp;&amp; !event.metaKey) {
unselectAllCountries();
}
numCountriesSelected += selected ? 1 : -1;
countryInfo.selected = selected;
setPaletteColor(id, selected ? selectedColor : unselectedColor);
paletteTexture.needsUpdate = true;
} else if (numCountriesSelected) {
unselectAllCountries();
}
requestRenderIfNotRequested();
}
function unselectAllCountries() {
numCountriesSelected = 0;
countryInfos.forEach((countryInfo) =&gt; {
countryInfo.selected = false;
});
resetPalette();
}
+canvas.addEventListener('pointerdown', recordStartTimeAndPosition);
canvas.addEventListener('pointerup', pickCountry);
</pre>
<p>添加了这些操作,这<em>看起来</em> 对我有效。</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/indexed-textures-picking-debounced.html"></iframe>
</div>
<a class="threejs_center" href="/manual/examples/indexed-textures-picking-debounced.html"
target="_blank">点击在新窗口打开</a>
</div>
<p></p>
<p>我不是用户交互的专家,所以我很想知道是否有更好的解决方案。</p>
<p>我希望这能让你了解图形索引的用处以及如何修改Three.js的着色器以添加简单的功能。对于如何使用GLSL编写着色器语言对于本文来说太多了这个链接有一些少量的信息参考
<a href="post-processing.html">关于后处理的这篇文章</a>
</p>
</div>
</div>
</div>
<script src="../resources/prettify.js"></script>
<script src="../resources/lesson.js"></script>
</body>
</html>