如何快速判断一个点是否在复杂场景中被遮挡?

11

我有一个复杂的3D场景,需要在基于3D坐标的情况下在其上显示HTML元素。(我只是在顶部覆盖了一个

标签,并用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
@WestLangley 我需要动态生成和交互式(可点击)元素;使用精灵图能否合理地完成这个任务? - Frxstrem
1
我不认为有什么不可以的。重复使用你的精灵图。创建一个池来存储它们。如果你超过了池中可用的数量,就向池中添加一个。 - WestLangley
你的场景是静态的还是动态的?如果场景是静态的,你可以尝试在事先准备好空间网格之后使用基于CPU的光线测试。可能有一个库可以做到这一点。也许是一些物理库。 - WacławJasper
@WacławJasper 我的场景大部分时间都是静态的。如果我理解你的意思正确的话,这基本上就是我现在正在做的事情(请参见问题中的代码),但在大型模型上速度太慢了。 - Frxstrem
显示剩余2条评论
4个回答

3
我不知道有什么快速的方法,但你有几个选择。我对three.js了解不够,无法告诉你如何使用该库,但是就WebGL而言,有以下几种方式:
如果你可以使用WebGL 2.0,你可以使用遮挡查询(occlusion queries)。这归结为:
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不是一个选项,那么你应该将场景绘制到framebuffer中,在那里附加自己的纹理以代替正常的z缓冲区。有一种扩展可以使用正确的深度纹理(更多详细信息请参见:此处),但如果不可能,则可以始终使用片段着色器绘制场景并输出每个像素的深度。
然后可以在深度纹理上使用gl.ReadPixels()。同样要注意GPU到CPU传输的延迟,这总是会很重要。
话虽如此,根据你的DOM对象的外观,将DOM对象渲染成纹理并使用四边形作为你的3D场景的一部分来绘制该纹理可能会更容易和更快。

感谢您的回答,WebGL目前绝对不是一个选项(它基本上需要支持任何可以定义为“现代”的浏览器),但我仍然会研究您提供的其他建议。 - Frxstrem
关于将DOM元素渲染为纹理,这样做是否仍会使元素可点击? - Frxstrem
你可以通过three.js使其可点击,但我怀疑任何简单的解决方案都涉及到再次使用射线投射器(我可能错了)。如果这是场景中唯一可点击的东西,那么最快的方法是将整个画布设为可点击,手动使用模型视图投影矩阵将四边形/精灵投影到屏幕上,然后检查点击坐标是否在精灵的边界内。 - Gio
1
只是提醒一下,你知道查询代码是不好的吧?除非你使用 gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE) 进行询问并检查其是否返回 true,否则无法保证查询已经发生。希望在几帧之后它会发生只是在自找麻烦。有人会运行低功率 GPU,这将需要更多的帧数或其他应用/页面将使用 GPU,而你等待的时间将不足够。请检查结果是否真正准备好,不要仅仅因为等了一会儿就假设它已经准备好了。 - gman

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

再次假设您控制几何图形,将一个“超出上下文”的颜色(或alpha值)添加到纹理中,以便div覆盖并针对其进行测试,这不是可行的黑客攻击吗?

如果没有纹理,在重叠的div边界内“包围”单个顶点的几何图形,并为其提供等效的“超出上下文”颜色或alpha值,以供片段着色器使用。


0

我假设所需的HTML标签内容是第三方数据,例如图像或iframe,并且不能与WebGL一起使用,因此必须是HTML标签,而不能是sprite。

有一个GPU计算的食谱。每次场景更改时重复。很抱歉我无法为Three.js(不知道引擎)执行此操作。

阶段1,使用标签可见性构建图像

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

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

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

结果可能看起来像这样(但这并不重要):

content of renderbuffer

如果颜色不是白色,则存在标签。(假设我只有3个标签,那么我的颜色是#000000、#010000、#020000,它们看起来都像黑色,但实际上不是。)

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

我们需要另一个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。

第三阶段,使用JavaScript读取值

要读取数据,请使用readPixels(或者可能使用texImage2D?)。简单的方法来做这个

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

想法

在第一阶段,减小渲染缓冲区的大小会显著提高性能(它还降低了第二阶段中的纹理查找),几乎没有成本。
如果您在第一阶段直接使用readPixels,并尝试使用javascript从屏幕读取数据,即使您仅使用320*200像素大小的渲染缓冲区,js也必须执行分辨率相同次数的迭代。因此,如果场景每时每刻都会改变,则只需空置for循环。
var time = Date.now();
for(var i=0;i<320*200*60;i++) { // 64000*60 times per second
}
console.log(Date.now() - time);

在我的机器上需要大约4100毫秒。但是使用第二阶段,您只需要执行与可见区域中的标签数量相同的迭代次数。(对于50个标签,可能是3000 * 60)。

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

这种技术的瓶颈是readpixels和texture lookups。您可以考虑不以FPS速度调用第三阶段,而是以较慢的预定义速度调用。


0

在这个答案中,你可以找到一个很好的例子,使用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 );

不确定这个方法是否能达到你想要的性能。也许你可以测试一下它的效果,并留下评论,供其他寻找类似解决方案的人参考。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接