You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Three.js GPU拾取在Retina与非Retina显示器间的差异问题求助

解决高DPI屏幕下WebGL GPU拾取坐标偏移问题

我完全理解你遇到的困扰——Retina等高DPI屏幕上GPU拾取的准确性大打折扣,鼠标离目标点老远就触发hover,但降低像素比又会让画面变模糊。核心问题其实是鼠标的CSS像素坐标和WebGL渲染目标的设备像素坐标不匹配,下面是具体的解决方案:

问题根源

当你设置renderer.setPixelRatio(window.devicePixelRatio)时,WebGL实际渲染的画布尺寸是window.innerWidth * devicePixelRatio × window.innerHeight * devicePixelRatio,但你的拾取逻辑直接用了e.clientX和e.clientY(这是CSS像素坐标)去读取渲染目标的像素,导致坐标错位。高DPI下1个CSS像素对应多个设备像素,鼠标位置被“放大”,自然会提前触发拾取。

修复步骤

1. 正确初始化拾取渲染目标

创建pickingTexture时,要基于设备像素比设置实际尺寸,同时改用最近邻滤波避免颜色插值干扰ID识别:

var pixelRatio = window.devicePixelRatio;
var w = window.innerWidth * pixelRatio;
var h = window.innerHeight * pixelRatio;
var pickingTexture = new THREE.WebGLRenderTarget(w, h);
pickingTexture.texture.minFilter = THREE.NearestFilter;

2. 鼠标坐标转换为设备像素

在mousemove事件中,把鼠标的CSS坐标转换成设备像素坐标,同时修正WebGL的y轴方向(WebGL y轴从下到上,鼠标y轴从上到下):

canvas.addEventListener('mousemove', function(e) {
  var pixelRatio = window.devicePixelRatio;
  var w = window.innerWidth * pixelRatio;
  var h = window.innerHeight * pixelRatio;

  // 窗口resize时同步更新拾取纹理尺寸
  if (pickingTexture.width !== w || pickingTexture.height !== h) {
    pickingTexture.setSize(w, h);
  }

  renderer.render(pickingScene, camera, pickingTexture);
  var pixelBuffer = new Uint8Array(4);
  // 转换鼠标坐标到设备像素
  var x = e.clientX * pixelRatio;
  var y = pickingTexture.height - (e.clientY * pixelRatio);
  renderer.readRenderTargetPixels(pickingTexture, x, y, 1, 1, pixelBuffer);
  
  var id = (pixelBuffer[0] << 16) | (pixelBuffer[1] << 8) | pixelBuffer[2] || '';
  document.querySelector('#selected').textContent = id ? 'You are hovering on element number ' + (id - 1) : '';
})

3. 同步窗口resize逻辑

在resize事件中,别忘了更新拾取渲染目标的尺寸和相机投影矩阵:

window.addEventListener('resize', function() {
  var w = window.innerWidth;
  var h = window.innerHeight;
  var pixelRatio = window.devicePixelRatio;
  
  camera.aspect = w / h;
  camera.updateProjectionMatrix(); // 必须更新相机投影矩阵
  renderer.setSize(w, h);
  // 同步更新拾取纹理尺寸
  pickingTexture.setSize(w * pixelRatio, h * pixelRatio);
})

4. 优化拾取材质(可选)

为了避免点的边缘透明导致像素读取错误,给拾取场景的mesh单独设置一个实心材质:

// 替换原来的pickingScene.add(mesh.clone())
var pickingMaterial = new THREE.RawShaderMaterial({
  vertexShader: document.getElementById('vertex-shader').textContent,
  fragmentShader: `
    precision highp float;
    varying vec3 vColor;
    void main() {
      // 确保点是实心的,过滤边缘透明像素
      float distanceToCenter = length(gl_PointCoord - vec2(0.5));
      if (distanceToCenter > 0.5) discard;
      gl_FragColor = vec4(vColor, 1.0);
    }
  `,
});
var pickingMesh = mesh.clone();
pickingMesh.material = pickingMaterial;
pickingScene.add(pickingMesh);

为什么这样有效?

  • 让拾取渲染目标的尺寸和WebGL实际渲染画布完全匹配(都是设备像素级),保证坐标一一对应。
  • 把鼠标的CSS坐标转换成设备像素坐标,确保读取的是鼠标真正指向的像素。
  • 使用NearestFilter避免颜色插值,防止相邻实例的颜色混合导致ID识别错误。

这样修改后,既能保留高DPI屏幕的清晰渲染,又能保证GPU拾取的准确性。

内容的提问来源于stack exchange,提问作者duhaime

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 07:38:14