What is the method for determining the dimensions of the rectangle that the camera can capture at a specific location?

In my latest project, I developed a small three.js application that showcases the movement of multiple circles from the bottom to the top of the canvas:

let renderer, scene, light, circles, camera;

initialize();
animate();

function initialize() {
  renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  scene = new THREE.Scene();

  light = new THREE.AmbientLight();
  scene.add(light);

  circles = new THREE.Group();
  scene.add(circles);

  camera = new THREE.PerspectiveCamera(45, renderer.domElement.clientWidth / renderer.domElement.clientHeight, 1);
  camera.position.z = circles.position.z + 500;
}


function animate() {
  // Update each circle.
  Array.from(circles.children).forEach(circle => {
    if (circle.position.y < visibleBox(circle.position.z).max.y) {
      circle.position.y += 4;
    } else {
      circles.remove(circle);
    }
  });

  // Create a new circle.
  let circle = new THREE.Mesh();
  circle.geometry = new THREE.CircleGeometry(30, 30);
  circle.material = new THREE.MeshToonMaterial({ color: randomColor(), transparent: true, opacity: 0.5 });
  circle.position.z = _.random(camera.position.z - camera.far, camera.position.z - (camera.far / 10));
  circle.position.x = _.random(visibleBox(circle.position.z).min.x, visibleBox(circle.position.z).max.x);
  circle.position.y = visibleBox(circle.position.z).min.y;
  circles.add(circle);

  // Render the scene.
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

function visibleBox(z) {
 return new THREE.Box2(
   new THREE.Vector2(-1000, -1000),
   new THREE.Vector2(1000, 1000)
 );
}

function randomColor() {
  return `#${ _.sampleSize("abcdef0123456789", 6).join("")}`;
}
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.js">
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js">
</script>

The usage of visibleBox(z) function is essential in determining the creation and removal of each circle. As of now, the return value for this function is hardcoded, but I intend to automate it so that it calculates the size of the visible rectangle within the camera's frustum at a particular depth, z.

To elaborate, my goal is for every circle to appear precisely at the base of the camera frustum (the lower edge of the red rectangle depicted in the image above) and vanish as soon as it reaches the frustum's top (the upper edge of the red rectangle).

Hence, how would I go about computing this rectangle?

Answer №1

Revise the function as shown below:

function visibleBox(z) {
    var t = Math.tan( THREE.Math.degToRad( camera.fov ) / 2 )
    var h = t * 2 * z;
    var w = h * camera.aspect;
    return new THREE.Box2(new THREE.Vector2(-w, h), new THREE.Vector2(w, -h));
}

Adjust the circle position with this code:

circle.position.z = _.random(-camera.near, -camera.far);
var visBox = visibleBox(circle.position.z)
circle.position.x = _.random(visBox.min.x, visBox.max.x);
circle.position.y = visBox.min.y;

Code demonstration:

let renderer, scene, light, circles, camera;

initialize();
animate();

function initialize() {
  renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  scene = new THREE.Scene();

  light = new THREE.AmbientLight();
  scene.add(light);

  circles = new THREE.Group();
  scene.add(circles);

  camera = new THREE.PerspectiveCamera(45, renderer.domElement.clientWidth / renderer.domElement.clientHeight, 1);
  camera.position.z = circles.position.z + 500;
}


function animate() {
  // Update each circle.
  Array.from(circles.children).forEach(circle => {
    if (circle.position.y < visibleBox(circle.position.z).max.y) {
      circle.position.y += 4;
    } else {
      circles.remove(circle);
    }
  });

  // Create a new circle.
  let circle = new THREE.Mesh();
  circle.geometry = new THREE.CircleGeometry(30, 30);
  circle.material = new THREE.MeshToonMaterial({ color: randomColor(), transparent: true, opacity: 0.5 });
  circle.position.z = _.random(-(camera.near+(camera.far-camera.near)/5), -camera.far);
  var visBox = visibleBox(circle.position.z)
  circle.position.x = _.random(visBox.min.x, visBox.max.x);
  circle.position.y = visBox.min.y;
  circles.add(circle);

  // Render the scene.
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

function visibleBox(z) {
    var t = Math.tan( THREE.Math.degToRad( camera.fov ) / 2 )
    var h = t * 2 * z;
    var w = h * camera.aspect;
    return new THREE.Box2(new THREE.Vector2(-w, h), new THREE.Vector2(w, -h));
}

function randomColor() {
  return `#${ _.sampleSize("abcdef0123456789", 6).join("")}`;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.js">
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js">
</script>


Explanation

The projection matrix outlines the transformation from 3D scene points to 2D viewport points. It converts eye space to clip space, where coordinates are transformed to normalized device coordinates (NDC) by dividing by the w component in clip coordinates. NDC values range from (-1,-1,-1) to (1,1,1).

https://i.sstatic.net/PGfgb.png

In perspective projection, the relationship between depth value and Z distance to the camera is nonlinear.
A typical perspective projection matrix looks like this:

r = right, l = left, b = bottom, t = top, n = near, f = far

2*n/(r-l)      0              0               0
0              2*n/(t-b)      0               0
(r+l)/(r-l)    (t+b)/(t-b)    -(f+n)/(f-n)    -1    
0              0              -2*f*n/(f-n)    0

This leads to the relationship between the Z coordinate in view space, normalized device coordinates Z component, and depth:

z_ndc = ( -z_eye * (f+n)/(f-n) - 2*f*n/(f-n) ) / -z_eye
depth = (z_ndc + 1.0) / 2.0

The inverse operation can be expressed as:

n = near, f = far

z_ndc = 2.0 * depth - 1.0;
z_eye = 2.0 * n * f / (f + n - z_ndc * (f - n));

If the perspective projection matrix is known, the calculation can be done as follows:

A = prj_mat[2][2]
B = prj_mat[3][2]
z_eye = B / (A + z_ndc)

Refer to How to render depth linearly in modern OpenGL with gl_FragCoord.z in fragment shader?


The relation between projected area in view space and the Z coordinate of the view space is linear and depends on the field of view angle and aspect ratio.

https://i.sstatic.net/QbXaB.png

The normalized device size can be converted to a size in view space using these formulas:

aspect = w / h
tanFov = tan( fov_y * 0.5 );

size_x = ndx_size_x * (tanFov * aspect) * z_eye;
size_y = ndx_size_y * tanFov * z_eye;

If the perspective projection matrix is known and the projection is symmetrical, the calculations can be made as follows:

size_x = ndx_size_x * / (prj_mat[0][0] * z_eye);
size_y = ndx_size_y * / (prj_mat[1][1] * z_eye);

See Field of view + Aspect Ratio + View Matrix from Projection Matrix (HMD OST Calibration)


Any position in normalized device coordinates can be transformed to view space coordinates using the inverse projection matrix:

mat4 inversePrjMat = inverse( prjMat );
vec4 viewPosH      = inversePrjMat * vec3( ndc_x, ndc_y, 2.0 * depth - 1.0, 1.0 );
vec3 viewPos       = viewPos.xyz / viewPos.w;

Refer to How to recover view space position given view space depth value and ndc xy


This means an unprojected rectangle with a specific depth can be calculated as follows:

vec4 viewLowerLeftH  = inversePrjMat * vec3( -1.0, -1.0, 2.0 * depth - 1.0, 1.0 );
vec4 viewUpperRightH = inversePrjMat * vec3(  1.0,  1.0, 2.0 * depth - 1.0, 1.0 );
vec3 viewLowerLeft   = viewLowerLeftH.xyz / viewLowerLeftH.w;
vec3 viewUpperRight  = viewUpperRightH.xyz / viewUpperRightH.w;

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

JavaScript code encounters an error stating "TypeError: n is undefined"

I recently crafted a JavaScript script to accomplish a specific task, and overall it was working smoothly. However, as I attempted to simplify the code for a demonstration here on this platform, I encountered two issues. Unfortunately, during this 're ...

"Encountering an issue when trying to choose a value from a select list using jQuery and the

What am I missing here or what is the correct approach to solve this? Take a look at the following code snippet: code snippet $(document).ready(function() { $(".metric_div").hide(); $("#solid_radio").click(function() { $("#solid").show(); ...

Detecting Whether a Vue/Vue Router Navigation was Triggered by the Back/Forward Button or a Manual Router Push

When using vue/vue-router, I have set up a watcher on $route that is triggered in two different ways: By clicking back or forward on the browser. When the user interacts with a form. There are watchers on the variables that the form uses, and these watch ...

Stop the repetition of event handlers by utilizing the .off() method to remove duplicates

Here's a scenario where I've assigned two event handlers: $('#myElement').on('click', '.classA', doSomething); $('#myElement').on('click', '.classB', doSomethingElse); Now, the task at ...

Implementing Custom Font Awesome Icons in Your Angular Project

I recently upgraded to a fontawesome subscription with a paid plan and have successfully created some custom icons. Now, I'm looking to integrate these icons into my angular app. Here are the dependencies listed in my package.json file: "@fortawe ...

Is it possible to retrieve the pixel color of a mesh by clicking on it in JavaScript?

On the current page, we have a mesh loaded with TreeJS and displayed in a canvas: https://i.sstatic.net/j8Ztj.png Is there a way to retrieve the color of the point where a click occurs? I attempted a method suggested in this thread: Getting the color val ...

Attempting to pass a limit argument to a query in urql, Strapi, and Next JS for the Query.posts field has resulted in a combination of errors including graphQLErrors

I need help figuring out how to pass a variable for the limit parameter in my GraphQL query. I am currently working with urql, Strapi, and its GraphQL plugin in a Next.js application. My goal is to introduce a variable for the limit to restrict the number ...

ngClass binding fails to update when using directives to communicate

I am looking to combine two directives in a nested structure. The 'inner directive' includes an ng-class attribute that is bound to a function taking parameters from both inner and outer scopes, and returning a Boolean value. This is the HTML co ...

What are some ways to customize the appearance of the Material UI table header?

How can I customize the appearance of Material's UI table header? Perhaps by adding classes using useStyle. <TableHead > <TableRow > <TableCell hover>Dessert (100g serving)</TableCell> ...

Building a multi-form application with HTML5 validation and Vue.js

I am working on a vue.js form that consists of multiple steps and requires HTML5 validation to be simplified. Here is a snippet of the code: <template> <div> <div v-if="activeForm === 1"> <h2> ...

Verifying if checkboxes are selected in PHP using JavaScript

echo '<div class="col-lg-10 col-lg-offset-1 panel">' . "<table id='data' class='table'> <tr> <th></th> <th>Document No</th> <th>AWB NO</th> ...

Importance of route prioritization in Express.js

I am looking to establish a specific order of priority for routes in express as follows: custom URLs, static files, error pages. This is how I currently have it set up: let router = express.Router(); // Custom URLs router.get("/foo", ...); app.use(rout ...

Getting an UnhandledPromiseRejectionWarning while attempting to navigate through Google Maps using Node.js Puppeteer

(node:15348) UnhandledPromiseRejectionWarning: Error: Execution context was destroyed due to a potential navigation issue. const browser = await puppeteer.launch({headless: false}); const page = await browser.newPage(); page.goto("https://www.google. ...

Expanding Perspective in React Native

Having trouble with implementing a camera feature that isn't scaling correctly. The issue seems to be related to the styling of the container View, as when the camera is rendered independently it works fine. The goal is for the camera to activate when ...

The deprecated body parser is throwing an error due to the lack of the extended option

I am encountering an issue with my code and I'm not sure how to resolve it. Since I am new to the codebase, I feel completely lost and clueless about what steps to take. body-parser deprecated undefined extended: provide extended option index.js:20:2 ...

What could be causing the browser.get method to fail to redirect to the specified URL?

I have organized my files in a folder structure that looks like this: project structure Currently, I am following the steps outlined in this particular tutorial The contents of my package.json file are as follows: { "name": "node_cucumber_e2e", "ver ...

Designate the preferred location for requesting the desktop site

Is there a way to specify the location of the 'Request desktop site' option? Here is the code I am currently using: var detector = new MobileDetect(window.navigator.userAgent); if(detector.mobile() != null || detector.phone() != null || det ...

Retrieving a parameter in NextJS from the request.body when it is not found

In the API code for my nextJS, I have implemented the following logic: export default async function handler(request, response) { if (request.method === "POST") { const type = request.body.type ?? 'body type' const ...

Tips for customizing the background color and image of a toaster

Looking to modify the background color and image based on the else condition (toaster.error) success: function (data) { if (data.message != ""){ toastr.success(data.message); ...

Struggling to understand the process of creating a new page in Express

I've created a file called ships.js in my routes folder: var express = require('express'); var router = express.Router(); /* GET Ships page. */ router.get('/ships', function(req, res, next) { res.render('ships', { tit ...