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

Three.js平面相对视口边界矩形的近平面裁剪问题求助

解决WebGL/Three.js中被近平面裁剪的平面视口边界矩形问题

你提到的思路完全正确——当平面被相机近平面裁剪时,仅用原平面的四个顶点计算边界肯定会出错,因为部分顶点已经落在视锥体外部了。我们需要把平面与近平面的交点也纳入边界计算,才能得到准确的视口包围矩形。

核心逻辑拆解

  1. 判断顶点可见性:先把平面顶点转换到相机空间(视图空间),这里近平面的Z坐标是-near(Three.js中相机空间Z轴指向屏幕外,近平面位于Z=-near处),如果顶点的Z坐标大于-near,说明它在近平面前方(可见)。
  2. 计算边与近平面的交点:遍历平面的四条边,如果一条边的两个顶点一个可见、一个不可见,就用线性插值算出这条边和近平面的交点。
  3. 重新计算边界:把所有可见顶点和新计算的交点一起转换到屏幕坐标,再取X/Y的最小、最大值,得到正确的包围矩形。

修改后的完整实现代码

let scene = new THREE.Scene();
let ww = window.innerWidth;
let wh = window.innerHeight;
// camera
const nearPlane = 0.1;
const farPlane = 200;
let camera = new THREE.PerspectiveCamera(45, ww / wh, nearPlane, farPlane);
scene.add(camera);
// renderer
let renderer = new THREE.WebGLRenderer();
renderer.setSize(ww, wh);
document.getElementById("canvas").appendChild(renderer.domElement);
// basic plane
let plane = new THREE.Mesh(
  new THREE.PlaneGeometry(0.75, 0.5),
  new THREE.MeshBasicMaterial({
    map: new THREE.TextureLoader().load('https://source.unsplash.com/EqFjlsOZULo/1280x720'),
    side: THREE.DoubleSide,
  })
);
scene.add(plane);

function displayBoundingRectangle() {
  camera.updateProjectionMatrix();
  // keep the plane at a constant position along Z axis based on camera FOV
  plane.position.z = -1 / (Math.tan((Math.PI / 180) * 0.5 * camera.fov) * 2.0);
  plane.updateMatrix();
  
  // 1. 将平面顶点转换到相机空间
  const viewMatrix = camera.matrixWorldInverse;
  const vertices = plane.geometry.vertices.map(v => 
    v.clone().applyMatrix4(plane.matrix).applyMatrix4(viewMatrix)
  );
  
  // 2. 收集可见顶点和近平面交点
  const pointsToProject = [];
  const nearZ = -nearPlane; // 相机空间近平面的Z坐标
  
  // PlaneGeometry的顶点顺序是0-1-2-3-0,对应四条边
  const edges = [[0,1], [1,2], [2,3], [3,0]];
  edges.forEach(([i1, i2]) => {
    const v1 = vertices[i1];
    const v2 = vertices[i2];
    
    const v1Visible = v1.z > nearZ;
    const v2Visible = v2.z > nearZ;
    
    // 把可见顶点加入集合
    if (v1Visible) pointsToProject.push(v1);
    
    // 如果边跨越南平面,计算交点并加入集合
    if (v1Visible !== v2Visible) {
      const t = (nearZ - v1.z) / (v2.z - v1.z);
      const intersection = v1.clone().lerp(v2, t);
      pointsToProject.push(intersection);
    }
  });
  
  // 3. 将所有点转换到NDC空间
  const projectedPoints = pointsToProject.map(p => 
    p.clone().applyMatrix4(camera.projectionMatrix)
  );
  
  // 4. 计算NDC空间的边界
  let minX = Infinity, maxX = -Infinity;
  let minY = Infinity, maxY = -Infinity;
  projectedPoints.forEach(corner => {
    minX = Math.min(minX, corner.x);
    maxX = Math.max(maxX, corner.x);
    minY = Math.min(minY, corner.y);
    maxY = Math.max(maxY, corner.y);
  });
  
  // 转换为屏幕坐标([0,1]范围)
  const screenBoundingRect = {
    top: 1 - (maxY + 1) / 2,
    right: (maxX + 1) / 2,
    bottom: 1 - (minY + 1) / 2,
    left: (minX + 1) / 2,
  };
  screenBoundingRect.width = screenBoundingRect.right - screenBoundingRect.left;
  screenBoundingRect.height = screenBoundingRect.bottom - screenBoundingRect.top;
  
  // 应用到DOM元素
  const boundingRectEl = document.getElementById("plane-bounding-rectangle");
  boundingRectEl.style.top = `${screenBoundingRect.top * wh}px`;
  boundingRectEl.style.left = `${screenBoundingRect.left * ww}px`;
  boundingRectEl.style.height = `${screenBoundingRect.height * wh}px`;
  boundingRectEl.style.width = `${screenBoundingRect.width * ww}px`;
}
// rotate the plane
plane.rotation.x = -2;
plane.rotation.y = -0.8;
/* 取消注释测试大视场角裁剪场景 */
camera.fov = 150;
// render scene
render();
// show our bounding rectangle
displayBoundingRectangle();

function render() {
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}
body {
  margin: 0;
}
#canvas {
  width: 100vw;
  height: 100vh;
}
#plane-bounding-rectangle {
  position: fixed;
  pointer-events: none;
  background: red;
  opacity: 0.2;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.min.js"></script>
<div id="canvas"></div>
<div id="plane-bounding-rectangle"></div>

关键修改说明

  • 相机空间转换:先把顶点转到相机空间,判断是否被裁剪的逻辑变得非常直观,直接对比Z坐标即可。
  • 边交点计算:通过线性插值找到跨越南平面的边与近平面的交点,确保我们不会漏掉裁剪后形成的新边界点。
  • 边界计算:用所有可见顶点+交点来计算NDC空间的极值,这样即使平面被部分裁剪,红色矩形也能准确包围视口中的可见区域。

现在你可以取消注释camera.fov = 150;这行,测试大视场角下的裁剪场景,应该能看到红色边界矩形准确贴合平面的可见部分了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 11:22:40