Three.js平面相对视口边界矩形的近平面裁剪问题求助
解决WebGL/Three.js中被近平面裁剪的平面视口边界矩形问题
你提到的思路完全正确——当平面被相机近平面裁剪时,仅用原平面的四个顶点计算边界肯定会出错,因为部分顶点已经落在视锥体外部了。我们需要把平面与近平面的交点也纳入边界计算,才能得到准确的视口包围矩形。
核心逻辑拆解
- 判断顶点可见性:先把平面顶点转换到相机空间(视图空间),这里近平面的Z坐标是
-near(Three.js中相机空间Z轴指向屏幕外,近平面位于Z=-near处),如果顶点的Z坐标大于-near,说明它在近平面前方(可见)。 - 计算边与近平面的交点:遍历平面的四条边,如果一条边的两个顶点一个可见、一个不可见,就用线性插值算出这条边和近平面的交点。
- 重新计算边界:把所有可见顶点和新计算的交点一起转换到屏幕坐标,再取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
相关产品推荐
相关产品推荐

