【问题标题】:How to quickly find if a point is obscured in a complex scene?如何在复杂场景中快速发现某个点是否被遮挡?
【发布时间】:2016-08-13 05:52:14
【问题描述】:

我有一个复杂的 3D 场景,我需要根据 3D 坐标在其上显示 HTML 元素。 (我只是在顶部覆盖一个div 标记并用CSS 定位它。)但是,当3D 坐标被模型遮挡(或用另一个措辞表达)时,我还需要部分隐藏它(例如,使其透明)方式,当它在相机中不可见时)。这些模型可能有数十万张面孔,我需要一种方法来确定它是否被遮挡,并且速度足以每秒运行多次。

目前,我正在使用 Three.js 的内置光线追踪器,代码如下:

// pos   = vector with (normalized) x, y coordinates on canvas
// dir   = vector from camera to target point

const raycaster = new THREE.Raycaster();
const d = dir.length(); // distance to point
let intersects = false;
raycaster.setFromCamera(pos, camera);
const intersections = raycaster.intersectObject(modelObject, true);
if (intersections.length > 0 && intersections[0].distance < d)
    intersects = true;

// if ray intersects at a point closer than d, then the target point is obscured
// otherwise it is visible

但是,在这些复杂模型上,这非常很慢(帧速率从 50 fps 下降到 8 fps)。我一直在寻找更好的方法来做到这一点,但到目前为止,我还没有找到在这种情况下效果很好的方法。

有没有更好、更有效的方法来确定一个点是否被场景中的模型可见或遮挡?

【问题讨论】:

  • 你能用THREE.Sprite代替HTML元素吗?
  • @WestLangley 我需要动态生成和交互(可点击)的元素;这可以用精灵做得相当好吗?
  • 我不明白为什么不这样做。重用你的精灵。创建一个池。如果您超过了池中可用的数量,请在池中添加一个。
  • 你的场景是静态的还是动态的?如果场景是静态的,如果您事先准备好空间网格,您可以尝试使用基于 cpu 的光线测试。可能有一个库。可能是一些物理库。
  • @WacławJasper 我的场景大部分时间都是静态的。如果我对您的理解正确,那几乎就是我现在正在做的事情(请参阅问题中的代码),而且在大型模型上速度太慢了。

标签: javascript three.js webgl raytracing


【解决方案1】:

我不知道任何真正快速的方法,但您确实有一些选择。我对three.js 的了解还不够,无法告诉您如何使用该库进行操作,但一般来说WebGL...

如果您可以使用 WebGL 2.0,则可以使用遮挡查询。这归结为

var query = gl.createQuery();
gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);
// ... draw a small quad at the specified 3d position ...
gl.endQuery(gl.ANY_SAMPLES_PASSED);
// some time later, typically a few frames later (after a fraction of a second)
if (gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE))
{
     gl.getQueryParameter(query, gl.QUERY_RESULT);
}

但请注意,查询结果仅在几帧之后可用。

如果 WebGl 2.0 不是一个选项,那么您可能应该将场景绘制到帧缓冲区,在其中附加您自己的纹理以代替正常的 z 缓冲区使用。有一个使用适当深度纹理的扩展 (more details here),但在不可能的情况下,您总是可以使用输出每个像素深度的片段着色器来绘制场景。

然后您可以在深度纹理上使用 gl.ReadPixels()。同样,请注意 GPU->CPU 传输的延迟,这总是很重要的。

说了这么多,根据您的 DOM 对象的外观,将您的 DOM 对象渲染为纹理并使用四边形作为 3d 场景的一部分绘制该纹理可能会更容易和更快。

【讨论】:

  • 感谢您的回答,WebGL 目前绝对不是一个选择(它基本上需要支持任何可以定义为“现代”的浏览器),但我仍然会研究其余的您的建议。
  • 把渲染DOM元素当作纹理,这样还能让元素点击吗?
  • 你可以通过three.js让它可点击,但我怀疑任何简单的解决方案都会再次涉及到raycaster(我可能错了)。如果这是您的场景中唯一可点击的东西,那么让整个画布可点击会更快,使用您的模型-视图-投影矩阵手动将四边形/精灵投影到屏幕上,然后检查点击坐标是否在精灵的边界。
  • 仅供参考,但您知道什么查询代码不好,对吧?除非您向gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE) 询问并检查它是否返回true,否则您无法保证查询已经发生。希望在几帧内发生只是自找麻烦。有人会运行一个低功率的 GPU,它会花费更多的帧,或者另一个应用程序/页面将使用 GPU,而你等待的时间是不够的。离开检查结果是否真的准备好了,不要认为它只是因为你等了一会儿
【解决方案2】:

我会假设所需的html标签的内容是第三方数据,例如图像或iframe,不能与webgl一起使用,所以它必须是html标签,不能是sprite。

有用于 GPU 计算的食谱。每次场景变化时重复。对不起,我不能为 Three.js 做这件事(不知道引擎)。

第 1 阶段,构建带有标签可见性的图像

创建包含 html 标签大小、标签 ID(从 0 开始的整数)和标签位置的数组(和索引)缓冲区。

创建渲染缓冲区和新的 WebGL 程序,用于渲染到其中。该程序的着色器将渲染简化的场景,包括“标签的阴影”。现在片段着色器的简化算法如下:为任何对象渲染白色。除标签外,根据标签ID渲染颜色。

如果你当前的程序有雾、透明物体、高度图或一些程序逻辑,它可能也包含在着色器中(取决于它是否可以覆盖标签)。

结果可能如下所示(但这并不重要):

如果颜色与白色不同,则有标签。 (假设我只有3个标签,那么我的颜色是#000000、#010000、#020000,看起来都像黑色,其实不是。)

第 2 阶段,收集图像中标签的透明度数据

我们需要另一个 WebGL 程序和渲染缓冲区。我们将点渲染到渲染缓冲区中,每个点都是一个像素大并且彼此相邻。点代表标签。所以我们需要带有标签位置的数组缓冲区(和标签ID,但这可以在着色器中推断出来)。我们还绑定了前一阶段的纹理。

现在顶点着色器的代码将根据标签ID属性进行以下操作,它将设置点的位置。然后用纹理查找计算透明度,伪代码:

attribute vec3 tagPosition;
attribute float tagId;

float calculateTransparency(vec2 tagSize, vec2 tagPosition) {
    float transparency = 0;
    for(0-tagSize.x+tagPosition.x) {
        for(0-tagSize.y+tagPosition.y) {
            if(textureLookup == tagId) transparency++; // notice that texture lookup is used only for area where tag could be
        }
    }
    return transparency/totalSize;
}

vec2 tagSize2d = calculateSize(tagPosition);
float transparency = calculateTransparency(tagSize2d, tagPosition.xy);

点的位置和透明度将随着 FS 的变化而变化。 FS 将根据透明度渲染一些颜色(例如,白色表示完全可见,黑色表示不可见,灰色阴影表示部分可见)。

这个阶段的结果是图像,其中每个像素呈现一个标签,像素的颜色是标签透明度。根据您拥有的标签数量,某些像素可能毫无意义并且具有 clearColor 值。像素位置对应于标签 ID。

第 3 阶段,使用 javascript 读取值

要读回数据,请使用readPixels(或者可能使用texImage2D?)。 Simple way to do it.

然后,您使用基于标记 ID 的 forloop 并将数据从类型化数组写入您的 JavaScript 状态机。现在您在 javascript 中有透明度值,您可以更改 CSS 值。

想法

在第 1 阶段,减小渲染缓冲区的大小将显着提升性能(它也降低了第 2 阶段的纹理查找),成本几乎为零。

如果你在 stage 1 之后直接使用 readPixels 并尝试使用 javascript 从屏幕读取数据,即使你使用的 renderbuffer 只有 320*200px 大,js 也必须进行与分辨率一样多的迭代。所以万一场景每时每刻都在变化,那么就空forloop:

var time = Date.now();
for(var i=0;i<320*200*60;i++) { // 64000*60 times per second
}
console.log(Date.now() - time);

在我的机器上花费了大约 4100 毫秒。但是对于第 2 阶段,您必须只执行与可见区域中的标签一样多的迭代。 (对于 50 个标签,它可能是 3000*60)。

我看到的最大问题是实现的复杂性。

这种技术的瓶颈是读取像素和纹理查找。您可能会考虑不以 FPS 速度调用阶段 3,而是以较慢的预定义速度。

【讨论】:

    【解决方案3】:

    假设您的 div 定位与底层 3D 场景同步,您应该能够使用 div“下方”的 readPixels 查询 just one 像素。

    再次假设您可以控制几何图形,在 div 将覆盖并对其进行测试的纹理中添加“脱离上下文”颜色(或 alpha 值)不是一个可行的技巧吗?

    在没有纹理的情况下,修改几何图形以“包围”覆盖 div 边界内的单个顶点,并为其提供等效的“脱离上下文”颜色或 alpha 值,以提供给片段着色器。

    【讨论】:

      【解决方案4】:

      Here in this answer 你找到了一个很好的例子,使用THREE.Frustum 来检测对象是否可见:

      var frustum = new THREE.Frustum();
      var cameraViewProjectionMatrix = new THREE.Matrix4();
      camera.updateMatrixWorld();
      camera.matrixWorldInverse.getInverse( camera.matrixWorld );
      cameraViewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
      frustum.setFromMatrix( cameraViewProjectionMatrix );
      
      visible = frustum.intersectsObject( object );
      

      不确定这是否会给您带来您所追求的性能。 也许您可以测试一下它的效果如何,并为最终在这里寻找类似解决方案的其他人留下您的发现的评论。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-03-07
        • 1970-01-01
        • 1970-01-01
        • 2011-05-08
        • 1970-01-01
        • 1970-01-01
        • 2012-07-09
        相关资源
        最近更新 更多