Bounding rectangle of a plane in Three.js according to the viewport and the problem of clipping near planes

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,

Answer №1

Is the problem clear from this display?

const scene = new THREE.Scene();

const ww = window.innerWidth;
const wh = window.innerHeight;

// setting up camera
const nearPlane = 0.1;
const farPlane = 200;
let camera = new THREE.PerspectiveCamera(45, ww / wh, nearPlane, farPlane);

scene.add(camera);

// creating renderer
let renderer = new THREE.WebGLRenderer();
renderer.setSize(ww, wh);
document.getElementById("canvas").appendChild(renderer.domElement);

// adding a 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 showBoundingRectangle() {
  camera.updateProjectionMatrix();

  // keeping the plane at a constant Z position based on camera's FOV
  plane.position.z = -1 / (Math.tan((Math.PI / 180) * 0.5 * camera.fov) * 2.0);

  plane.updateMatrix();

  let modelViewProjectionMatrix = new THREE.Matrix4();
  modelViewProjectionMatrix = modelViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, plane.matrix);

  let vertices = plane.geometry.vertices;
  
  let minX = Infinity;
  let maxX = -Infinity;

  let minY = Infinity;
  let maxY = -Infinity;

  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);
  }

  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)}`;

  let screenBoundingRect = {
    top: 1 - (worldBoundingRect.top + 1) / 2,
    right: (worldBoundingRect.right + 1) / 2,
    bottom: 1 - (worldBoundingRect.bottom + 1) / 2,
    left: (worldBoundingRect.left + 1) / 2,
  };

  screenBoundingRect.width = screenBoundingRect.right - screenBoundingRect.left;
  screenBoundingRect.height = screenBoundingRect.bottom - screenBoundingRect.top;

  var 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";
}


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();


function render(time) {
  camera.fov = THREE.MathUtils.lerp(45, 150, Math.sin(time / 1000) * 0.5 + 0.5);

  showBoundingRectangle();
  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>

If it's not clear, one of your points is possibly going beyond the camera frustum cone. The frustum acts as a kind of pyramid from near to far, but any point located behind that single point eventually expands outward.

https://i.sstatic.net/1IFke.png

Answer №2

After analyzing the schema presented in my initial question, I was able to successfully resolve the issue at hand.

I will refrain from including the entire code snippet here due to its length and verbosity (as well as its semblance of a somewhat unconventional solution), but the key concept revolves around:

  • Determining the coordinates of both clipped and non-clipped corners.

  • Utilizing the coordinates of non-clipped corners along with a minute vector extending from that corner to the clipped one, recursively adding it to our non-clipped corner coordinates until reaching the near plane Z position - effectively identifying an intersection with the near plane.

For instance, if the top left corner of the plane remains unclipped while the bottom left corner is clipped, locating the intersection between the camera's near plane and the plane's left side involves a process similar to this:

// Calculating intersection by incrementally adjusting a vector from one corner towards the near plane
function getIntersection(refPoint, secondPoint) {
    // Create direction vector to include
    let vector = secondPoint.sub(refPoint);
    // Duplicate our reference point
    var intersection = refPoint.clone();
    // Iterate until we reach the near plane
    while(intersection.z > -1) {
        intersection.add(vector);
    }

    return intersection;
}

// Acquiring projected coordinates for the top left corner
let topLeftCorner = vertices[0].applyMatrix4(modelViewProjectionMatrix);

// Generating a vector parallel to the left side of the plane towards the bottom left corner, also projecting its coordinates
let directionVector = vertices[0].clone().sub(new THREE.Vector3(0, -0.05, 0)).applyMatrix4(modelViewProjectionMatrix);

// Finding the intersection with the near plane
let bottomLeftIntersection = getIntersection(topLeftCorner, directionVector);

While there may exist more analytical methods to tackle this challenge, the current solution proves effective, prompting me to maintain this approach for the time being.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Issue: The object is unable to be executed as a function, resulting in the failure to return an array

Currently, I am extracting table values from the UI row by row. This involves clicking on each row and retrieving the corresponding data. exports.getTableData = function(callback){     var uiArray =[];     var count;         aLib.loadCheck(c ...

"Employing regular expressions to extract the name of the web browser

Experiencing issues with regular expressions in JavaScript. Attempting to extract the version number and browser name, such as "Firefox 22.0" or "MSIE 8.0". console.log(navigatorSaysWhat()) function navigatorSaysWhat() { var rexp = new RegExp(/(firefox ...

Ensuring the node array is easily accessible within the view

I am currently working on building a simple messaging system using express/nodejs. Although I have successfully implemented the feature to send messages to all users, I now want to enable users to have private 1-to-1 conversations. My aim is straightforwa ...

Having trouble getting jQuery to function properly in Safari

I am facing an issue with jQuery in the Safari browser. While my jQuery functions are functioning perfectly on GC, FF, IE, and Opera, they seem to fail when tested on Safari. My question is: Is there a specific code snippet that I can integrate into my w ...

Guide on sending emails with AngularJS and the Ionic framework

I have been attempting to send an email by using various links, but without success. I am looking for guidance on how to successfully send an email. Below is the code I have been using for sending emails. Any suggestions for changes would be greatly apprec ...

Where can I find the specific code needed to create this button style?

There is a button here for adding an event, but unfortunately, the style code is not displayed. Is there a way to obtain the full cascading style sheet for this button? ( ) ...

Rendering in Three JS involves efficiently utilizing one buffer to display the output within itself

I have been struggling with a particular issue and I really need some assistance: In my three js context, I have created a custom material and rendered it into a texture. ` /* Rendering in texture */ fbo_renderer_scene = new THREE.Scene(); fbo_r ...

Three.js - implementing a browser alert for updating an element within an Object3D

When replacing an element in Object3D with another element in the same position in the scene, it is necessary to trigger a window alert for the effect to properly refresh. Otherwise, there may be a delay or the old element might still appear along with the ...

The extruded spline is experiencing a lack of response to the Three.Ray in THREE.SceneUtils.createMultiMaterialObject

I am currently working on a project that combines elements from the extruded spline example and the mouse tooltip example. I am in the process of debugging the initial components of this project before moving forward. The mouse tooltip feature is functioni ...

What is the best way to refresh the text within a span element?

Working on a web page where users can select an item by clicking a button. The chosen item's details will be shown in a dropbox. Currently, it updates the quantity if the user selects the same item again. However, there's an issue I'm facing ...

PLupload does not support Flash runtime in Internet Explorer 8

I am facing a dilemma with a basic JavaScript function placed within the $(function() { ... }); block. var uploader = new plupload.Uploader({ runtimes: 'html5,flash,silverlight', browse_button: 'pickfiles', c ...

Live tweet moderation made easy with the Twitter widget

Currently in search of a widget, jQuery plugin, hosted service, or equivalent that can be easily integrated into an HTML page. This tool should display and automatically update live tweets featuring a specific hashtag, only from designated accounts set by ...

Utilizing VueUse Intersection Observer to monitor numerous elements within a single component

I am faced with the challenge of applying an intersection observer from VueUse to multiple elements. Instead of creating separate refs for each element, which has become cumbersome due to having over 10 elements in every component, I have opted to use a ...

"An ng-repeat directive with a filter applied results in an empty

After successfully implementing the ng-repeat loop below: <div ng-repeat="einschItem in einschaetzungen.alldata | filter: { savedatum: lolatage[tagarrayindex].tagestring } | orderBy : '-savetimestamp'"> I wanted to check if the filtered r ...

Tips for refreshing CSS following an ajax request

Currently, I am facing an issue with the CSS formatting on my page. I am using Django endless pagination to load content on page scroll. However, when new content is loaded, the CSS applied to the page does not work properly and needs to be refreshed. Can ...

Is there a way to stop text from wrapping between words and punctuation marks using CSS or jQuery?

There is a paragraph containing some text. One issue I am facing is that punctuation at the end of a word may get wrapped to the next line, as shown here: This is the sentence, This is a new line Is there a way to fix this using CSS or jQuery? ...

Having trouble entering text into a textbox in Reactjs when the state is empty

I am currently working on integrating a newsletter submit form using Reactjs/nextjs. However, I am facing an issue where once I submit the form, I am unable to type anything in the textbox. I have tried making the "state" empty but it did not resolve the ...

I'm having trouble with my TextGeometry not displaying on screen. Can someone help me figure

Here is the code I am using for Three.js: <html> <head> <title>My first Three.js app</title> <style> body { margin: 0; } canvas { width: 100%; height: 100% } </style> ...

Tips for restricting User access and displaying specific sections of the menu

I have a component that utilizes map to display all menu parts. Is there a way to make certain parts of the menu hidden if the user's access rights are equal to 0? const Aside: React.FunctionComponent = () => { const[hasRight, setHasRight] = us ...

Is there a way to make local storage update instantly after calling setItem, without needing to refresh the page?

On the user authentication page, I am attempting to store the response token obtained from an API call in local storage. Then, when navigating to the home page, I want to display the profile details if there is a value in the local storage. However, even t ...