【问题标题】:GPU Picking inconsistent across devicesGPU Picking 跨设备不一致
【发布时间】:2020-05-27 08:10:06
【问题描述】:

我正在尝试使用我在本文后半部分修改的代码https://threejsfundamentals.org/threejs/lessons/threejs-picking.html

使用 Points 实现 GPU 拾取

它在桌面上对我来说运行良好,但我开始测试不同的浏览器和设备,但它并不能始终如一地运行。我做了一个 Codepen 来说明https://codepen.io/deklanw/pen/OJVVmEd?editors=1111

body {
  margin: 0;
}
#c {
  width: 100vw;
  height: 100vh;
  display: block;
}
<canvas id="c"></canvas>
<script type="module">
// Three.js - Picking - RayCaster w/Transparency
// from https://threejsfundamentals.org/threejs/threejs-picking-gpu.html

import * as THREE from "https://threejsfundamentals.org/threejs/resources/threejs/r113/build/three.module.js";

function main() {
  const canvas = document.querySelector("#c");
  const renderer = new THREE.WebGLRenderer({ canvas });

  const fov = 60;
  const aspect = 2; // the canvas default
  const near = 0.1;
  const far = 200;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.z = 30;

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

  // put the camera on a pole (parent it to an object)
  // so we can spin the pole to move the camera around the scene
  const cameraPole = new THREE.Object3D();
  scene.add(cameraPole);
  cameraPole.add(camera);

  function randomNormalizedColor() {
    return Math.random();
  }

  function getRandomInt(n) {
    return Math.floor(Math.random() * n);
  }

  function getCanvasRelativePosition(e) {
    const rect = canvas.getBoundingClientRect();
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    };
  }

  const textureLoader = new THREE.TextureLoader();
  const particleTexture =
    "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/sprites/ball.png";

  const vertexShader = `
    attribute float size;
    attribute vec3 customColor;

    varying vec3 vColor;

    void main() {
        vColor = customColor;
        vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
        gl_PointSize = size * ( 100.0 / length( mvPosition.xyz ) );
        gl_Position = projectionMatrix * mvPosition;
    }
`;

  const fragmentShader = `
    uniform sampler2D texture;
    varying vec3 vColor;

    void main() {
        vec4 tColor = texture2D( texture, gl_PointCoord );
        if (tColor.a < 0.5) discard;
        gl_FragColor = mix( vec4( vColor.rgb, 1.0 ), tColor, 0.1 );
    }
`;

  const pickFragmentShader = `
    uniform sampler2D texture;
    varying vec3 vColor;

    void main() {
      vec4 tColor = texture2D( texture, gl_PointCoord );
      if (tColor.a < 0.25) discard;
      gl_FragColor = vec4( vColor.rgb, 1.0);
    }
`;

  const materialSettings = {
    uniforms: {
      texture: {
        type: "t",
        value: textureLoader.load(particleTexture)
      }
    },
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    blending: THREE.NormalBlending,
    depthTest: true,
    transparent: false
  };

  const createParticleMaterial = () => {
    const material = new THREE.ShaderMaterial(materialSettings);
    return material;
  };

  const createPickingMaterial = () => {
    const material = new THREE.ShaderMaterial({
      ...materialSettings,
      fragmentShader: pickFragmentShader,
      blending: THREE.NormalBlending
    });
    return material;
  };

  const geometry = new THREE.BufferGeometry();
  const pickingGeometry = new THREE.BufferGeometry();
  const colors = [];
  const sizes = [];
  const pickingColors = [];
  const pickingColor = new THREE.Color();
  const positions = [];

  for (let i = 0; i < 30; i++) {
    colors[3 * i] = randomNormalizedColor();
    colors[3 * i + 1] = randomNormalizedColor();
    colors[3 * i + 2] = randomNormalizedColor();

    const rgbPickingColor = pickingColor.setHex(i + 1);
    pickingColors[3 * i] = rgbPickingColor.r;
    pickingColors[3 * i + 1] = rgbPickingColor.g;
    pickingColors[3 * i + 2] = rgbPickingColor.b;

    sizes[i] = getRandomInt(20);

    positions[3 * i] = getRandomInt(20);
    positions[3 * i + 1] = getRandomInt(20);
    positions[3 * i + 2] = getRandomInt(20);
  }

  geometry.setAttribute(
    "position",
    new THREE.Float32BufferAttribute(positions, 3)
  );
  geometry.setAttribute(
    "customColor",
    new THREE.Float32BufferAttribute(colors, 3)
  );
  geometry.setAttribute("size", new THREE.Float32BufferAttribute(sizes, 1));

  geometry.computeBoundingBox();

  const material = createParticleMaterial();
  const points = new THREE.Points(geometry, material);

  // setup geometry and material for GPU picking
  pickingGeometry.setAttribute(
    "position",
    new THREE.Float32BufferAttribute(positions, 3)
  );
  pickingGeometry.setAttribute(
    "customColor",
    new THREE.Float32BufferAttribute(pickingColors, 3)
  );
  pickingGeometry.setAttribute(
    "size",
    new THREE.Float32BufferAttribute(sizes, 1)
  );

  pickingGeometry.computeBoundingBox();

  const pickingMaterial = createPickingMaterial();
  const pickingPoints = new THREE.Points(pickingGeometry, pickingMaterial);

  scene.add(points);
  pickingScene.add(pickingPoints);

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }
    return needResize;
  }

  class GPUPickHelper {
    constructor() {
      // create a 1x1 pixel render target
      this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
      this.pixelBuffer = new Uint8Array(4);
    }
    pick(cssPosition, pickingScene, camera) {
      const { pickingTexture, pixelBuffer } = this;

      // set the view offset to represent just a single pixel under the mouse
      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
      );
      // render the scene
      renderer.setRenderTarget(pickingTexture);
      renderer.render(pickingScene, camera);
      renderer.setRenderTarget(null);
      // clear the view offset so rendering returns to normal
      camera.clearViewOffset();
      //read the pixel
      renderer.readRenderTargetPixels(
        pickingTexture,
        0, // x
        0, // y
        1, // width
        1, // height
        pixelBuffer
      );

      const id =
        (pixelBuffer[0] << 16) | (pixelBuffer[1] << 8) | pixelBuffer[2];
      
      console.log(`You clicked sphere number ${id}`);
      
      return id;
    }
  }

  const pickHelper = new GPUPickHelper();

  function render(time) {
    time *= 0.001; // convert to seconds;

    if (resizeRendererToDisplaySize(renderer)) {
      const canvas = renderer.domElement;
      camera.aspect = canvas.clientWidth / canvas.clientHeight;
      camera.updateProjectionMatrix();
    }

    cameraPole.rotation.y = time * 0.1;

    renderer.render(scene, camera);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);

  function onClick(e) {
    const pickPosition = getCanvasRelativePosition(e);
    const pickedID = pickHelper.pick(pickPosition, pickingScene, camera);
  }

  function onTouch(e) {
    const touch = e.touches[0];
    const pickPosition = getCanvasRelativePosition(touch);
    const pickedID = pickHelper.pick(pickPosition, pickingScene, camera);
  }

  window.addEventListener("mousedown", onClick);
  window.addEventListener("touchstart", onTouch);
}

main();
</script>

如果您单击(或点击)节点,它们的 ID 应该会在控制台中弹出。在某些设备上,我只得到 0,就像选择背景一样。

有人知道为什么吗?

另外,如果有一种方法可以在这种情况下进行拾取(通过 ShaderMaterial 具有可变大小点的点网格),使用更简单但仍然有效的方法,我很好奇如何

编辑:

我删除了 1x1 渲染目标优化,它似乎已修复它。现在我想知道那个优化会导致什么问题..

【问题讨论】:

    标签: javascript three.js picking mouse-picking ray-picking


    【解决方案1】:

    问题是您不能以这种方式跨设备使用积分。

    当一个点的中心在屏幕外时是否绘制一个点是否与设备无关(OpenGL ES / WebGL 规范说它仍然应该被绘制,OpenGL 规范说它不是。没有针对它的测试,所以每个驱动程序都是不同),对于 WebGL 实现来说,工作量太大,所以它们不会。 AFAIK 英特尔和 NVidia 确实绘制了它们。 AMD 和基于 PowerVR 的 (iPhone) 不会绘制它们。

    如果您将圆圈变大并确保它们离开屏幕(并且您可能需要将画布变小),您会看到此问题。在某些设备上,它们会平稳地离开屏幕,而在其他设备上,一旦中心离开屏幕,它们就会消失(通常取决于点的大小和视口的大小)

    这意味着无论有没有 1x1 像素渲染目标,您的示例在这两种情况下都不起作用在某些设备上不会被绘制。当您使渲染目标与画布的大小匹配时,大多数圆圈的中心都在内部,但您仍然会在边缘处出现拾取错误。

    要解决此问题,您需要使用四边形而不是点来绘制点。有很多方法可以做到这一点。将每个四边形绘制为单独的网格或精灵,或将所有四边形合并到另一个网格中,或使用InstancedMesh,您需要每个点的矩阵,或编写自定义着色器来处理点(参见this article 上的最后一个示例)

    请注意,积分还有其他问题。默认情况下,它们不会相对于画布大小进行缩放(当然,您可以在着色器中修复此问题,并且 three.js 也有此选项)。它们还具有独立于设备的最大尺寸,根据规范可以低至 1 个像素。它们对设备像素比设置反应不佳(尽管您也可以在代码中修复它)。由于所有这些原因,积分的用途有限。代码绘制的大圆圈可能超出了这个限制。

    【讨论】:

    • 非常感谢!老实说,我永远不会想到这一点。看起来我必须阅读 WebGL。准备开始阅读您的 WebGL 基础知识网站...不过,我想问一下:在您提到的那些替代可能性中,哪一个性能最高?目前我正在渲染大约 30k~ 这些点,但我有更多的野心。
    • 在一次绘制调用中全部绘制比单独绘制要快。将它们绘制为四边形网格可能比 InstancedMesh 更快或更慢,具体取决于 GPU/驱动程序和数据。我认为对于四边形来说,单个网格更快,但可能不会快很多。
    • 要模拟 POINTS,您需要 6 个顶点作为四边形,然后您需要在顶点着色器中的某个中心点周围移动视图空间(对于 3d)或屏幕空间(对于 2d)中的位置.我没有一个简单的例子来展示如何做到这一点,但this old example 做到了。或者您可以提出一个新问题,尽管可能已经有了答案。
    • 我做了一些阅读和思考。制作了一个新线程,因为它完全是一个不同的主题stackoverflow.com/questions/60313368/… 但是,对于这个主题,你说“你仍然会在边缘出现挑选错误”。如果在某些设备上,当中心离开视口时,这些点就从视图中消失了,为什么会出现拾取错误?球体消失可能会很麻烦,但我不明白它会如何导致采摘问题
    • 我想如果您使用的是非 1x1 像素解决方案,那么您是正确的,其中心在画布外略微倾斜的球体将从可见视图(画布)和采摘中提前消失同时查看(渲染目标),因此不会出现拾取错误。我只是假设您不希望球体在它们的中心不在视野之外时突然消失。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-12-19
    • 1970-01-01
    • 2021-11-17
    • 2014-05-30
    • 1970-01-01
    • 1970-01-01
    • 2012-12-22
    相关资源
    最近更新 更多