使用纹理索引来拾取和着色

您所在的位置:网站首页 地图上每个国家的颜色不同呢 使用纹理索引来拾取和着色

使用纹理索引来拾取和着色

#使用纹理索引来拾取和着色| 来源: 网络整理| 查看: 265

这篇文章是 对齐HTML元素到3D对象 的延续。 如果你还没有读过上篇文章,你应该先从那里开始,然后再回来继续阅读。

有时候使用Three.js需要提出一些创造性的解决思路。我不确定这是一个很好的解决方案,但我想我会分享它,你可以看看是否可以为你的需求提供了一些解决思路或方案。

在 上一篇文章中,我们在3D地球周围显示了国家名称,那么我们如何做到,让用户选中一个国家并高亮他的选择?

第一个想法是为每个国家生成几何图形,我们可以 使用射线拾取 ,就像之前介绍的那样。 我们将为每个国家构建3D几何对象。如果用户点击代表那个国家的网格对象,我们就会知道对应的国家被点击了。

所以,为了验证这个解决方案,我尝试生成所有国家的3D网格对象,使用了在上一篇文章中和我生成轮廓一样的数据。 结果生成了15.5m的二进制GLTF(.glb)文件,让用户下载15.5m的数据对于我来说实在太多了。

有很多方法可以压缩数据。第一种可能是应用一些算法来降低轮廓的分辨率,但是我没有花时间来研究它。可能出现美国边界变大而加拿大边界变小的情况。

另一种解决方案是仅使用数据压缩,比如gzip将其降至11m,这减少了30%,但是还不够。

我们可以将所有数据存储为16位而不是32位浮点值。或者我们也可以使用像draco 压缩 这种东西也许就够了。不过我没有去试,我推荐你去试下回来告诉我是怎么回事,因为我很想知道😅

就我而言,我考虑使用 GPU拾取方案, 这在上一篇 关于拾取的文章 的最后有提到。这种方案中,我们使用一种独特的颜色代表不同网格对象的ID,然后我们绘制了所有网格,看看哪个颜色被点击了。

基于这种灵感,我们可以预先生成一张国家的地图,每个国家的颜色是它在国家数组中的索引号。我们可以使用类似GPU拾取技术,我们使用索引纹理绘制一个离屏全局画布,查看颜色会告诉我们用户点击了那个国家ID。

因此,我 写了一些代码 生成这样的一个纹理,在这里:

注意:生成这份纹理的数据来源于 这个网站 ,使用的协议是 CC-BY-SA。

它只有217k,比国家网格对象的15m要好得多,事实上我们可以使用更低的分辨率,但现在217k似乎已经足够了。

所以让我们试着用它来选择国家。

从 GPU拾取案例中 获取代码,我们需要一个场景来做拾取。

const pickingScene = new THREE.Scene(); pickingScene.background = new THREE.Color(0);

我们需要将带有索引纹理的地球添加到拾取场景中。

{ 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)); }

然后我们把 GPUPickingHelper这个类拷贝下,在使用前我们需要做一些小改动

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] { + shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to); + }); + }; scene.add(new THREE.Mesh(geometry, material)); }

在上面可以看到我们添加了3个uniforms变量,indexTexture, paletteTexture, and paletteTextureWidth。我们从 indexTexture 获取颜色,并且把它转化成索引下标。 vUv 是由Three.js提供的纹理坐标。然后我们使用索引下标从调色板中获取颜色。然后我们使用当前的 diffuseColor和最终的结果作混合。 diffuseColor在此时是我们黑色纹理,而调色盘是白色纹理。所以如果我们相加两个颜色,得出的是白色轮廓。如果我们二者相减,得出的是黑色轮廓。

在我们渲染前,我们还需要设置调色板纹理,以及这3个uniforms变量。

对于调色板纹理,它只需要足够宽即可。每个国家保留一种颜色 + 一种海洋颜色。这里有240个国家或地区,我们可以等到国家列表加载完成后以获取确切的数字来查找。不过选择一些更大的数字没有危害,所以让我们选择512。

这里是创建调色板的代码

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;

一个DataTexture 提供原始的纹理数据。在这种情况下,我们给他512个RGBA颜色,每个颜色4字节,每个包含0-255的红、绿、蓝分量。

让我们用随机颜色填充它,只是为了看看它是否有效

for (let i = 1; i < palette.length; ++i) { palette[i] = Math.random() * 256; } // 设置海洋颜色 (索引 #0) palette.set([100, 200, 255, 255], 0); paletteTexture.needsUpdate = true;

任何时候我们想要Three.js通过 palette 数组的内容来更新调色板上的纹理,我们需要去设置paletteTexture.needsUpdate 为 true。

然后我们还需要设置材质上的uniforms变量

const geometry = new THREE.SphereGeometry(1, 64, 32); const material = new THREE.MeshBasicMaterial({map: texture}); material.onBeforeCompile = function(shader) { fragmentShaderReplacements.forEach((rep) => { 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));

这样我们就得到了随机着色的国家

click here to open in a separate window

现在我们可以看到索引和调色板纹理生效了,让我们控制调色板进行高亮显示

首先让我们创建一个函数,我们传入一个THREE.js颜色,并格式化为可以放入调色板纹理的值。

const tempColor = new THREE.Color(); function get255BasedColor(color) { tempColor.set(color); const base = tempColor.toArray().map(v => v * 255); base.push(255); // alpha return base; }

像这样来调用 color = get255BasedColor('red') 会返回一个像 [255, 0, 0, 255]这样的数组。

接下来让我们用它来生成一些颜色并填充调色板。

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 < maxNumCountries; ++i) { setPaletteColor(i, unselectedColor); } // 设置海洋颜色 (索引 #0) setPaletteColor(0, oceanColor); paletteTexture.needsUpdate = true; }

现在让我们使用这些函数来更新调色板,当一个国家被选中时:

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 > 0) { const countryInfo = countryInfos[id - 1]; const selected = !countryInfo.selected; if (selected && !event.shiftKey && !event.ctrlKey && !eventaKey) { 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) => { countryInfo.selected = false; }); + resetPalette(); }

我们应该能够突出显示1个或多个国家。

点击在新窗口打开

这看起来有效!

一件小事是我们不能不改变选中状态就渲染地球。如果我们选择一个国家然后想要旋转地球,选中状态将改变。

让我们尝试解决这个问题。我认为,我们可以检查两件事情。点击和松开经过了多少时间;用户是否移动了鼠标。如果时间很短,或者他们没有移动鼠标,那么这个行为可能是点击。否则他们可能正在尝试拖动地球。

+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 > maxClickTimeMs) { + return; + } + + // 如果鼠标移动了,就认为这是一个拖动行为 + const position = getCanvasRelativePosition(event); + const moveDeltaSq = (startPosition.x - position.x) ** 2 + + (startPosition.y - position.y) ** 2; + if (moveDeltaSq > maxMoveDeltaSq) { + return; + } - const position = {x: event.clientX, y: event.clientY}; const id = pickHelper.pick(position, pickingScene, camera); if (id > 0) { const countryInfo = countryInfos[id - 1]; const selected = !countryInfo.selected; if (selected && !event.shiftKey && !event.ctrlKey && !eventaKey) { 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) => { countryInfo.selected = false; }); resetPalette(); } +canvas.addEventListener('pointerdown', recordStartTimeAndPosition); canvas.addEventListener('pointerup', pickCountry);

添加了这些操作,这看起来 对我有效。

点击在新窗口打开

我不是用户交互的专家,所以我很想知道是否有更好的解决方案。

我希望这能让你了解图形索引的用处,以及如何修改Three.js的着色器以添加简单的功能。对于如何使用GLSL,编写着色器语言对于本文来说太多了,这个链接有一些少量的信息,参考 关于后处理的这篇文章。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3