This is a common issue with WebGL, but in order to provide clarity, I will be using three.js to illustrate the problem at hand.
Imagine you have a plane and a perspective camera. Your goal is to determine the bounding rectangle of the plane relative to the viewport/window. Here is my current approach:
- Firstly, obtain the modelViewProjectionMatrix by multiplying the camera's projectionMatrix with the plane's matrix.
- Apply this modelViewProjectionMatrix to the 4 corner vertices of the plane.
- Determine the min/max values of the result and convert them back to viewport coordinates.
Everything works smoothly until the plane is clipped by the camera near plane (often occurs when using a high field of view), which distorts the results.
Is there a method to accurately retrieve the values even if parts of the plane are being clipped by the camera near plane? Possibly by finding the intersection between the plane and the camera near plane?
Edit: One solution could involve obtaining the two normalized vectors v1 and v2 as depicted in this diagram: intersections between a plane and the camera near plane schema. Then, it would be necessary to calculate the length of these vectors from the corners of the plane to the intersection point (taking into account the near plane Z position), though I am facing challenges with this final step.
Below is the code for three.js along with the corresponding jsfiddle (uncomment line 109 to display inaccurate coordinates): https://jsfiddle.net/fbao9jp7/1/
let scene = new THREE.Scene();
let ww = window.innerWidth;
let wh = window.innerHeight;
// camera configuration
const nearPlane = 0.1;
const farPlane = 200;
let camera = new THREE.PerspectiveCamera(45, ww / wh, nearPlane, farPlane);
scene.add(camera);
// renderer setup
let renderer = new THREE.WebGLRenderer();
renderer.setSize(ww, wh);
document.getElementById("canvas").appendChild(renderer.domElement);
// basic plane creation
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();
// keeping the plane at a fixed position along the Z-axis based on camera FOV
plane.position.z = -1 / (Math.tan((Math.PI / 180) * 0.5 * camera.fov) * 2.0);
plane.updateMatrix();
// acquire the plane's 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));
}
// determining the 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;
}
}
// defining the four coordinates
let worldBoundingRect = {
top: maxY,
right: maxX,
bottom: minY,
left: minX,
};
// converting 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,
};
// adding width and height
screenBoundingRect.width = screenBoundingRect.right - screenBoundingRect.left;
screenBoundingRect.height = screenBoundingRect.bottom - screenBoundingRect.top;
var boundingRectEl = document.getElementById("plane-bounding-rectangle");
// applying to the 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";
}
// rotating 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;
// rendering the scene
render();
// displaying the 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>
Thank you,