Three.js 平面的边界矩形相对于视口和近平面裁剪问题
Three.js bounding rectangle of a plane relative to viewport and near plane clipping issue
这是一个一般的 WebGL 问题,但为了清楚起见,我将在此处使用 three.js 来演示我的问题。
假设我有一架飞机和一台透视相机。我正在尝试获取平面相对于 viewport/window 的边界矩形。
到目前为止,我是这样做的:
- 首先,乘以camera得到modelViewProjectionMatrix
projectionMatrix 与平面矩阵。
- 将该 modelViewProjectionMatrix 应用于平面的 4 个角顶点。
- 获取结果的 min/max 值并将它们转换回视口坐标。
在飞机被靠近平面的相机(通常在使用高视野时)截断之前,它一直运行良好,弄乱了我的结果。
即使飞机附近的相机剪裁了我的飞机的一部分,有什么方法可以获得正确的值吗?也许通过在平面附近获取平面和相机之间的交点?
编辑:
我能想到的一个想法是获得两个标准化向量 v1 和 v2,如此模式所示:intersections between a plane and the camera near plane schema。
然后我必须得到这些矢量的长度,以便它们从平面的角到交点(知道近平面 Z 位置),但我仍在努力解决最后一部分。
无论如何,这是 three.js 代码和相应的 jsfiddle(取消注释第 109 行以显示错误的坐标):https://jsfiddle.net/fbao9jp7/1/
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();
// get the plane model view projection matrix
let modelViewProjectionMatrix = new THREE.Matrix4();
modelViewProjectionMatrix = modelViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, plane.matrix);
let vertices = plane.geometry.vertices;
// apply modelViewProjectionMatrix to our 4 vertices
let projectedPoints = [];
for (let i = 0; i < vertices.length; i++) {
projectedPoints.push(vertices[i].applyMatrix4(modelViewProjectionMatrix));
}
// get our min/max values
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
for (let i = 0; i < projectedPoints.length; i++) {
let corner = projectedPoints[i];
if (corner.x < minX) {
minX = corner.x;
}
if (corner.x > maxX) {
maxX = corner.x;
}
if (corner.y < minY) {
minY = corner.y;
}
if (corner.y > maxY) {
maxY = corner.y;
}
}
// we have our four coordinates
let worldBoundingRect = {
top: maxY,
right: maxX,
bottom: minY,
left: minX,
};
// convert coordinates from [-1, 1] to [0, 1]
let screenBoundingRect = {
top: 1 - (worldBoundingRect.top + 1) / 2,
right: (worldBoundingRect.right + 1) / 2,
bottom: 1 - (worldBoundingRect.bottom + 1) / 2,
left: (worldBoundingRect.left + 1) / 2,
};
// add width and height
screenBoundingRect.width = screenBoundingRect.right - screenBoundingRect.left;
screenBoundingRect.height = screenBoundingRect.bottom - screenBoundingRect.top;
var boundingRectEl = document.getElementById("plane-bounding-rectangle");
// apply to our bounding rectangle div using window width and height
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;
/* UNCOMMENT THIS LINE TO SHOW HOW NEAR PLANE CLIPPING AFFECTS OUR BOUNDING RECTANGLE VALUES */
//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>
非常感谢,
这是否表明问题所在?
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();
// get the plane model view projection matrix
let modelViewProjectionMatrix = new THREE.Matrix4();
modelViewProjectionMatrix = modelViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, plane.matrix);
let vertices = plane.geometry.vertices;
// get our min/max values
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
// apply modelViewProjectionMatrix to our 4 vertices
let corner = new THREE.Vector3();
for (let i = 0; i < vertices.length; i++) {
corner.copy(vertices[i]);
corner.applyMatrix4(modelViewProjectionMatrix);
minX = Math.min(corner.x, minX);
maxX = Math.max(corner.x, maxX);
minY = Math.min(corner.y, minY);
maxY = Math.max(corner.y, maxY);
}
// we have our four coordinates
let worldBoundingRect = {
top: maxY,
right: maxX,
bottom: minY,
left: minX,
};
document.querySelector('#info').textContent = `${minX.toFixed(2)}, ${maxX.toFixed(2)}, ${minY.toFixed(2)}, ${minY.toFixed(2)}`;
// convert coordinates from [-1, 1] to [0, 1]
let screenBoundingRect = {
top: 1 - (worldBoundingRect.top + 1) / 2,
right: (worldBoundingRect.right + 1) / 2,
bottom: 1 - (worldBoundingRect.bottom + 1) / 2,
left: (worldBoundingRect.left + 1) / 2,
};
// add width and height
screenBoundingRect.width = screenBoundingRect.right - screenBoundingRect.left;
screenBoundingRect.height = screenBoundingRect.bottom - screenBoundingRect.top;
var boundingRectEl = document.getElementById("plane-bounding-rectangle");
// apply to our bounding rectangle div using window width and height
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;
/* UNCOMMENT THIS LINE TO SHOW HOW NEAR PLANE CLIPPING AFFECTS OUR BOUNDING RECTANGLE VALUES */
//camera.fov = 150;
// render scene
render();
function render(time) {
camera.fov = THREE.MathUtils.lerp(45, 150, Math.sin(time / 1000) * 0.5 + 0.5);
// show our bounding rectangle
displayBoundingRectangle();
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;
}
#info {
position: absolute;
left: 0;
top: 0;
pointer-events: none;
color: white;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.min.js"></script>
<div id="canvas"></div>
<div id="plane-bounding-rectangle"></div>
<pre id="info"></pre>
如果不清楚,问题是您的观点之一是在相机截锥体后面。平截头体定义了一种从近到远的截断金字塔,但经过近(朝向相机)的数学运算最终会到达一个点,该点后面的任何东西都开始向外扩展
根据我在最初问题中的架构,我已经设法解决了我的问题。
我不会在这里 post 整个片段,因为它有点长而且冗长(而且感觉有点像肮脏的黑客),但这是主要思想:
获取裁剪角和非裁剪角
使用非裁剪角坐标和一个从该角到裁剪角的非常小的向量,并递归地将其添加到我们的非裁剪角坐标,直到我们到达近平面 Z 位置:我们发现与近平面的交点。
假设飞机的左上角没有被裁剪,但左下角被裁剪。要找到近平面相机与平面左侧的交点,我们将执行以下操作:
// find the intersection by adding a vector starting from a corner till we reach the near plane
function getIntersection(refPoint, secondPoint) {
// direction vector to add
let vector = secondPoint.sub(refPoint);
// copy our corner refpoint
var intersection = refPoint.clone();
// iterate till we reach near plane
while(intersection.z > -1) {
intersection.add(vector);
}
return intersection;
}
// get our top left corner projected coordinates
let topLeftCorner = vertices[0].applyMatrix4(modelViewProjectionMatrix);
// get a vector parallel to our left plane side toward the bottom left corner and project its coordinates as well
let directionVector = vertices[0].clone().sub(new THREE.Vector3(0, -0.05, 0)).applyMatrix4(modelViewProjectionMatrix);
// get the intersection with the near plane
let bottomLeftIntersection = getIntersection(topLeftCorner, directionVector);
我相信会有更多的分析方法来解决这个问题,但这个方法有效,所以我现在要坚持使用它。
这是一个一般的 WebGL 问题,但为了清楚起见,我将在此处使用 three.js 来演示我的问题。
假设我有一架飞机和一台透视相机。我正在尝试获取平面相对于 viewport/window 的边界矩形。 到目前为止,我是这样做的:
- 首先,乘以camera得到modelViewProjectionMatrix projectionMatrix 与平面矩阵。
- 将该 modelViewProjectionMatrix 应用于平面的 4 个角顶点。
- 获取结果的 min/max 值并将它们转换回视口坐标。
在飞机被靠近平面的相机(通常在使用高视野时)截断之前,它一直运行良好,弄乱了我的结果。
即使飞机附近的相机剪裁了我的飞机的一部分,有什么方法可以获得正确的值吗?也许通过在平面附近获取平面和相机之间的交点?
编辑: 我能想到的一个想法是获得两个标准化向量 v1 和 v2,如此模式所示:intersections between a plane and the camera near plane schema。 然后我必须得到这些矢量的长度,以便它们从平面的角到交点(知道近平面 Z 位置),但我仍在努力解决最后一部分。
无论如何,这是 three.js 代码和相应的 jsfiddle(取消注释第 109 行以显示错误的坐标):https://jsfiddle.net/fbao9jp7/1/
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();
// get the plane model view projection matrix
let modelViewProjectionMatrix = new THREE.Matrix4();
modelViewProjectionMatrix = modelViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, plane.matrix);
let vertices = plane.geometry.vertices;
// apply modelViewProjectionMatrix to our 4 vertices
let projectedPoints = [];
for (let i = 0; i < vertices.length; i++) {
projectedPoints.push(vertices[i].applyMatrix4(modelViewProjectionMatrix));
}
// get our min/max values
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
for (let i = 0; i < projectedPoints.length; i++) {
let corner = projectedPoints[i];
if (corner.x < minX) {
minX = corner.x;
}
if (corner.x > maxX) {
maxX = corner.x;
}
if (corner.y < minY) {
minY = corner.y;
}
if (corner.y > maxY) {
maxY = corner.y;
}
}
// we have our four coordinates
let worldBoundingRect = {
top: maxY,
right: maxX,
bottom: minY,
left: minX,
};
// convert coordinates from [-1, 1] to [0, 1]
let screenBoundingRect = {
top: 1 - (worldBoundingRect.top + 1) / 2,
right: (worldBoundingRect.right + 1) / 2,
bottom: 1 - (worldBoundingRect.bottom + 1) / 2,
left: (worldBoundingRect.left + 1) / 2,
};
// add width and height
screenBoundingRect.width = screenBoundingRect.right - screenBoundingRect.left;
screenBoundingRect.height = screenBoundingRect.bottom - screenBoundingRect.top;
var boundingRectEl = document.getElementById("plane-bounding-rectangle");
// apply to our bounding rectangle div using window width and height
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;
/* UNCOMMENT THIS LINE TO SHOW HOW NEAR PLANE CLIPPING AFFECTS OUR BOUNDING RECTANGLE VALUES */
//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>
非常感谢,
这是否表明问题所在?
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();
// get the plane model view projection matrix
let modelViewProjectionMatrix = new THREE.Matrix4();
modelViewProjectionMatrix = modelViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, plane.matrix);
let vertices = plane.geometry.vertices;
// get our min/max values
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
// apply modelViewProjectionMatrix to our 4 vertices
let corner = new THREE.Vector3();
for (let i = 0; i < vertices.length; i++) {
corner.copy(vertices[i]);
corner.applyMatrix4(modelViewProjectionMatrix);
minX = Math.min(corner.x, minX);
maxX = Math.max(corner.x, maxX);
minY = Math.min(corner.y, minY);
maxY = Math.max(corner.y, maxY);
}
// we have our four coordinates
let worldBoundingRect = {
top: maxY,
right: maxX,
bottom: minY,
left: minX,
};
document.querySelector('#info').textContent = `${minX.toFixed(2)}, ${maxX.toFixed(2)}, ${minY.toFixed(2)}, ${minY.toFixed(2)}`;
// convert coordinates from [-1, 1] to [0, 1]
let screenBoundingRect = {
top: 1 - (worldBoundingRect.top + 1) / 2,
right: (worldBoundingRect.right + 1) / 2,
bottom: 1 - (worldBoundingRect.bottom + 1) / 2,
left: (worldBoundingRect.left + 1) / 2,
};
// add width and height
screenBoundingRect.width = screenBoundingRect.right - screenBoundingRect.left;
screenBoundingRect.height = screenBoundingRect.bottom - screenBoundingRect.top;
var boundingRectEl = document.getElementById("plane-bounding-rectangle");
// apply to our bounding rectangle div using window width and height
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;
/* UNCOMMENT THIS LINE TO SHOW HOW NEAR PLANE CLIPPING AFFECTS OUR BOUNDING RECTANGLE VALUES */
//camera.fov = 150;
// render scene
render();
function render(time) {
camera.fov = THREE.MathUtils.lerp(45, 150, Math.sin(time / 1000) * 0.5 + 0.5);
// show our bounding rectangle
displayBoundingRectangle();
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;
}
#info {
position: absolute;
left: 0;
top: 0;
pointer-events: none;
color: white;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.min.js"></script>
<div id="canvas"></div>
<div id="plane-bounding-rectangle"></div>
<pre id="info"></pre>
如果不清楚,问题是您的观点之一是在相机截锥体后面。平截头体定义了一种从近到远的截断金字塔,但经过近(朝向相机)的数学运算最终会到达一个点,该点后面的任何东西都开始向外扩展
根据我在最初问题中的架构,我已经设法解决了我的问题。
我不会在这里 post 整个片段,因为它有点长而且冗长(而且感觉有点像肮脏的黑客),但这是主要思想:
获取裁剪角和非裁剪角
使用非裁剪角坐标和一个从该角到裁剪角的非常小的向量,并递归地将其添加到我们的非裁剪角坐标,直到我们到达近平面 Z 位置:我们发现与近平面的交点。
假设飞机的左上角没有被裁剪,但左下角被裁剪。要找到近平面相机与平面左侧的交点,我们将执行以下操作:
// find the intersection by adding a vector starting from a corner till we reach the near plane
function getIntersection(refPoint, secondPoint) {
// direction vector to add
let vector = secondPoint.sub(refPoint);
// copy our corner refpoint
var intersection = refPoint.clone();
// iterate till we reach near plane
while(intersection.z > -1) {
intersection.add(vector);
}
return intersection;
}
// get our top left corner projected coordinates
let topLeftCorner = vertices[0].applyMatrix4(modelViewProjectionMatrix);
// get a vector parallel to our left plane side toward the bottom left corner and project its coordinates as well
let directionVector = vertices[0].clone().sub(new THREE.Vector3(0, -0.05, 0)).applyMatrix4(modelViewProjectionMatrix);
// get the intersection with the near plane
let bottomLeftIntersection = getIntersection(topLeftCorner, directionVector);
我相信会有更多的分析方法来解决这个问题,但这个方法有效,所以我现在要坚持使用它。