欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

使用Three.js实现深度选择功能

最编程 2024-08-14 11:20:48
...
threejs自带的接口实现坐标拾取一般使用射线法,该方法属于几何计算,通过使用Ray创建射线与geometry通过某个加速结构(bvh或者kdTree),最终与确定的一个三角形进行相交测试
这种方法有如下优点:
  • 计算结果精确,由于属于几何计算,射线拾取的精度非常高
  • 返回的信息详细,不仅可以计算当前拾取点的坐标,也可以返回当前三角形所属于的mesh,当前点的uv等......
  • 。。。。。 同样精度高带来的另一个问题便是性能消耗大,如果遇到稍微复杂的场景,整个计算过程将十分耗时,鼠标移动过程中的坐标拾取更是无法做到,同时,由于射线拾取依赖于几何数据,因此geometry的buffer数据需要常驻内存,这更会带来严重的性能问题。 针对坐标拾取这种常规功能,很多时候并不需要像射线拾取那样返回过多的信息,只需要计算坐标即可,因此,我们可以通过计算深度反酸世界坐标,主要方法如下,将NDC坐标转换到世界坐标

image.png 主要步骤如下:

  1. 由于是深度坐标拾取第一步先计算深度,关键代码如下
        let currentRenderTarget = pickRenderer.getRenderTarget();
        pickRenderer.getClearColor(oldClearColor);
        let oldClearAlpha = pickRenderer.getClearAlpha();
        let oldAutoAClear = pickRenderer.autoClear;
        let oldSceneEnv = scene.environment;
        let oldBackground = scene.background;
        pickRenderer.autoClear = false;
        pickRenderer.setClearColor(0x000000, 1);
        scene.environment = undefined;
        scene.background = undefined;
        //在深度材质下渲染一遍
        scene.overrideMaterial = this.depthMaterial;

        scene.frameState.passes.pick = true;
        scene.executeUpdate();

        this.pickScene.add(scene._renderCollection);

        pickRenderer.setSize(bufferSize.width, bufferSize.height);

        //深度信息将会保存到该target上
        pickRenderer.setRenderTarget(pickTarget);

        pickRenderer.clear();
        pickRenderer.render(this.pickScene, scene.camera);
        pickRenderer.clear();

        pickRenderer.setRenderTarget(currentRenderTarget);
        scene.frameState.passes.pick = false;
        pickRenderer.setRenderTarget(currentRenderTarget);
        pickRenderer.setClearColor(oldClearColor);
        pickRenderer.setClearAlpha(oldClearAlpha);
        pickRenderer.autoClear = oldAutoAClear;
        scene.background = oldBackground;
        scene.environment = oldSceneEnv;
        scene.frameState.passes.pick = false;
        scene.overrideMaterial = null;

2.计算深度之后,就需要获取点击区域的深度值了,根据屏幕坐标计算深度 关键代码如下

        //获取尺寸
        let bufferSize = scene.drawingBufferSize;
        let pickRenderer = scene.renderer;
        let pickTarget = this.renderTarget;

        x = Math.max(defaultValue(x, 0), 0);
        //这里由于坐标轴的原因,注意y值的坐标,同样可以说是renderTarget的flipY属性
        y = Math.max(defaultValue(bufferSize.height - y, 0), 0);
        
        //读取颜色值,由于是在深度材质下渲染的,因此颜色就表示深度
        let pixels = new Float32Array(4);
        pickRenderer.readRenderTargetPixels(pickTarget, x, y, 1, 1, pixels);
        、
        let depth;
        //如果depthMaterial的pack方式为RGBADepthPacking
        if (this.pickDepth.depthMaterial.depthPacking === RGBADepthPacking) {
            let packedDepth = Vector4.unpack(pixels, 0, scratchPackedDepth);
            depth = packedDepth.dot(UnpackFactors) * 2 - 1;
        } else{
            depth = -pixels[0] * 2.0 + 1.0;
        } 

Vector4.unpack方法

((Vector4 as any).unpack = function(array: Number[], startingIndex: number, result: any): Vector4 {
    Check.defined('array', array);

    if (!defined(result)) {
        result = new Vector4();
    }
    result.x = array[startingIndex++];
    result.y = array[startingIndex++];
    result.z = array[startingIndex++];
    result.w = array[startingIndex];
    return result;
});

3.根据深度反算世界坐标

   scene.renderer.getCurrentViewport(viewport);

   let ndc = new Vector3();
   ndc.x = (drawingBufferPosition.x / viewport.width) * 2.0 - 1.0;
   ndc.y = -(drawingBufferPosition.y / viewport.height) * 2.0 + 1.0;
   ndc.z = depth;

   ndc.unproject(camera);

   if (!defined(result)) {
       result = new Vector3();
   }
   result.set(ndc.x, ndc.y, ndc.z);

上述方法存在一个性能问题,即获取深度这一步我们简单粗暴的将整个场景在深度材质下渲染了一遍,但我们最终只需要获取指定屏幕坐标的深度即可,因此只需要渲染点击的那一小部分像素区域即可,这部分可以自行去优化,我们在做功能的时候,拿到深度是一个很常见的功能,因此可以具体结合自身的业务场景进行性能优化,或者确保只更新一个最小范围的像素