Bounding box in ThreeJS for a 3D entity represented in 2D dimensions

I'm currently trying to calculate the area on my screen that is occupied by a 3D object.

Despite searching online, I have been unsuccessful in finding a solution to this problem.

The function geometry.computeBoundingBox() only provides me with the 3D bounding box.

Is there a way to convert this information into a 2D bounding box?

Answer №1

To easily create a 2D bounding box, all you need to do is convert the vertices to screen space:

function generateScreenBoundingBox(mesh, camera) {
  var vertices = mesh.geometry.vertices;
  var vertex = new THREE.Vector3();
  var min = new THREE.Vector3(1, 1, 1);
  var max = new THREE.Vector3(-1, -1, -1);

  for (var i = 0; i < vertices.length; i++) {
    var worldCoord = vertex.copy(vertices[i]).applyMatrix4(mesh.matrixWorld);
    var screenSpace = worldCoord.project(camera);
    min.min(screenSpace);
    max.max(screenSpace);
  }

  return new THREE.Box2(min, max);
}

The resulting Box2 uses normalized screen coordinates [-1, 1]. To get pixel values, simply multiply by half of your renderer's width and height:

function normalizeToPixels(coord, renderWidth, renderHeight) {
  var halfScreen = new THREE.Vector2(renderWidth / 2, renderHeight / 2)
  return coord.clone().multiply(halfScreen);
}

Check out a demo here: http://jsfiddle.net/holgerl/6fy9d54t/

UPDATE: Memory usage in inner loop reduced per suggestion from @WestLangley

UPDATE2: Bug fix identified by @manthrax has been corrected

Answer №2

Sorry for joining the discussion a little late, but I have an updated version that can efficiently handle groups, children, and buffered geometry:

function calculateBoundingRegion(obj, camera) {
    var minBound;
    var maxBound;

    if(Array.isArray(obj)) {
        for(var i = 0; i < obj.length; ++i) {
            let boundingBox = calculateBoundingRegion(obj[i], camera);
            if(minBound === undefined) {
                minBound = boundingBox.min.clone();
                maxBound = boundingBox.max.clone();
            } else {
                minBound.min(boundingBox.min);
                maxBound.max(boundingBox.max);
            }
        }
    }

    if(obj.geometry !== undefined) {
        var vertices = obj.geometry.vertices;
        if(vertices === undefined
            && obj.geometry.attributes !== undefined
            && 'position' in obj.geometry.attributes) {
            
            var vertexPoint = new THREE.Vector3();       
            var pos = obj.geometry.attributes.position;
            for(var i = 0; i < pos.count * pos.itemSize; i += pos.itemSize)
            {
                vertexPoint.set(pos.array[i], pos.array[i + 1], pos.array[1 + 2]);
                var worldVertexCoord = vertexPoint.applyMatrix4(obj.matrixWorld);
                var screenSpaceVertex = worldVertexCoord.project(camera);
                if(minBound === undefined) {
                    minBound = screenSpaceVertex.clone();
                    maxBound = screenSpaceVertex.clone();
                }
                minBound.min(screenSpaceVertex);
                maxBound.max(screenSpaceVertex);
            }
        } else {
         
            var vertexPoint = new THREE.Vector3();       
            for(var i = 0; i < vertices.length; ++i) {
                var worldVertexCoord = vertexPoint.copy(vertices[i]).applyMatrix4(obj.matrixWorld);
                var screenSpaceVertex = worldVertexCoord.project(camera);
                if(minBound === undefined) {
                    minBound = screenSpaceVertex.clone();
                    maxBound = screenSpaceVertex.clone();
                }
                minBound.min(screenSpaceVertex);
                maxBound.max(screenSpaceVertex);
            }
        }
    }
    
    if(obj.children !== undefined) {
        for(var i = 0; i < obj.children.length; ++i) {
            let boundingBox = calculateBoundingRegion(obj.children[i], camera);
            if(minBound === undefined) {
                minBound = boundingBox.min.clone();
                maxBound = boundingBox.max.clone();
            } else {
                minBound.min(boundingBox.min);
                maxBound.max(boundingBox.max);
            }
        }
    }
    
    return new THREE.Box2(minBound, maxBound);
}

Answer №3

I'm also running a bit behind schedule, but I managed to convert the scscsc code into TypeScript. Additionally, I have incorporated an additional feature where I compute the bounding box only from visible objects/meshes.

The coordinates range from -1 to 1, and to determine sizes, simply multiply these values by the canvas size.

Update: I included normalization after projection because in certain scenarios, I was getting negative or larger values than [-1, 1].

computeScreenSpaceBoundingBox(obj: Object3D, camera: Camera): Box2 {
    let min: Vector2 | undefined = undefined;
    let max: Vector2 | undefined = undefined;

    // Check if it's an array of objects
    if (Array.isArray(obj)) {
      for (let i = 0; i < obj.length; ++i) {
        if (obj[i].visible) {
          const box2 = Utils3D.computeScreenSpaceBoundingBox(obj[i], camera);
          if (min === undefined) min = box2.min.clone();
          else min.min(box2.min);

          if (max === undefined) max = box2.max.clone();
          else max.max(box2.max);
        }
      }
    }

    // Does the object have geometry?
    if (obj.visible) {
      if (obj instanceof Mesh && obj.geometry !== undefined) {
        const vertices = obj.geometry.vertices;
        if (vertices === undefined && obj.geometry.attributes !== undefined && "position" in obj.geometry.attributes) {
          // Buffered geometry
          const vertex = new Vector3();
          const pos = obj.geometry.attributes.position;
          for (let i = 0; i < pos.count * pos.itemSize; i += pos.itemSize) {
            vertex.set(pos.array[i], pos.array[i + 1], pos.array[i + 2]);
            const vertexWorldCoord = vertex.applyMatrix4(obj.matrixWorld);
            const vertexScreenSpace = vertexWorldCoord.project(camera).normalize();
            if (min === undefined) {
              min = new Vector2(vertexScreenSpace.x, vertexScreenSpace.y);
            } else {
              Utils3D.min(min, vertexScreenSpace);
            }

            if (max === undefined) max = new Vector2(vertexScreenSpace.x, vertexScreenSpace.y);
            else Utils3D.max(max, vertexScreenSpace);
          }
        } else {
          // Regular geometry
          const vertex = new Vector3();
          for (let i = 0; i < vertices.length; ++i) {
            const vertexWorldCoord = vertex.copy(vertices[i]).applyMatrix4(obj.matrixWorld);
            const vertexScreenSpace = vertexWorldCoord.project(camera).normalize();
            if (min === undefined) {
              min = new Vector2(vertexScreenSpace.x, vertexScreenSpace.y);
            } else {
              Utils3D.min(min, vertexScreenSpace);
            }

            if (max === undefined) max = new Vector2(vertexScreenSpace.x, vertexScreenSpace.y);
            else Utils3D.max(max, vertexScreenSpace);
          }
        }
      }
    }

    // Are there any children objects?
    if (obj.children !== undefined) {
      for (let i = 0; i < obj.children.length; ++i) {
        if (obj.children[i].visible) {
          const box2 = Utils3D.computeScreenSpaceBoundingBox(obj.children[i], camera);
          if (min === undefined) min = box2.min.clone();
          else min.min(box2.min);

          if (max === undefined) max = box2.max.clone();
          else max.max(box2.max);
        }
      }
    }

    return new Box2(min, max);
  }

Utilizing Utils3D functions which were necessary due to errors in types definition.

 static min(v2: Vector2, v3: Vector3): void {
    if (v2.x > v3.x) v2.x = v3.x;
    if (v2.y > v3.y) v2.y = v3.y;
  }

  static max(v2: Vector2, v3: Vector3): void {
    if (v2.x < v3.x) v2.x = v3.x;
    if (v2.y < v3.y) v2.y = v3.y;
  }

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

Getting started with TypeScript in combination with Node.js, Express, and MongoDB

I'm a beginner in working with TypeScript, Node.js, Express, and MongoDB. I need guidance on the end-to-end flow for these technologies. Can someone please suggest steps or provide links for a step-by-step process? What is the procedure to compile/r ...

Refreshing the page allows Socket.io to establish multiple connections

I've been working on setting up a chatroom, but I've noticed that each time the page refreshes, more connections are being established. It's interesting because initially only one connection is created when I visit the chat room page. Howeve ...

Update the CSS styles using jQuery

I am looking to update the content within a CSS tag using jQuery. In this instance, I need to change "My content" to "New Content". Here is the CSS code I have: a.appari:focus:after{ background: #333; background: rgba(0,0,0,.8); border-radius: 5px ...

Serving files from a Node.js server and allowing users to download them in their browser

I am facing an issue with my file repository. When I access it through the browser, the file automatically downloads, which is fine. However, I want to make a request to my server and then serve the file result in the browser. Below is an example of the GE ...

Troubleshooting Problem with JQuery Datepicker Input Field Value

There are two text boxes on my page named fromDate and toDate for date search. By default, the current day is displayed in both of them. Here is the working code: Jquery (document).ready(function () { $('.dateClass').datetimepicker({timepi ...

A guide on effectively mocking a Vuex store within the parentComponent of VueJS test-utils

I am currently using Jest in conjunction with vue-test-utils to test the reaction of a child component to an $emit event triggered by the parent component. The VueJS test-utils library offers a parentComponent option that can be utilized when mounting or ...

Tips on incorporating a high-quality animated gif for optimal user engagement

I'm looking for advice on how to optimize the loading of a large animated gif (1900px wide) on my website. Currently, the initial load time is quite lengthy and the animation appears choppy. Is there a more efficient method to load the gif without slo ...

Updating reactive form fields with patched observable data in Angular

Struggling with initializing a reactive form in my component's onInit() method, as well as handling data from a service to update or create entries in a MySQL table. The issue lies in using patchValue to pass the retrieved data into the form: compone ...

A guide on integrating array map with a v-for loop in Vue

Struggling to understand how to implement a loop within another loop in Vue? This task might seem simple with React, but Vue presents its own challenges when it comes to using loops with arrays in templates/JSX. The input data follows a specific format fr ...

Encountering the error "Cannot read property 'header' of undefined" while conducting tests on Nodejs using supertest

In my server.js file, I have set up my express app. I tried to run a demo test in my test file using express, but unfortunately, the test did not run successfully. const request = require('supertest'); const express = require('express' ...

Change the anchor text dynamically with JQuery

The page contains href links with incomplete text. For example, the link text displayed on the page is "link1", but it should actually be "link1 - Module33". Both the page text and actual text start with the same initial text ("link1" in this case). I retr ...

Is there a way to dynamically update the button color within JavaScript and have it reflect in a separate function?

I have been working on developing a button effect in JavaScript that can be called within another function. The idea is that when the button is clicked, it will change color, and then revert back to its original color. While this could be achieved using th ...

Most effective method for utilizing Ajax to upload files and ensuring they are uploaded before submission

I am currently working on a form that contains various fields, including file upload functionality for multiple files. I have also explored the option of using AJAX to upload files. My goal is to seamlessly upload files via AJAX while simultaneously fillin ...

Angular 2: Executing a function after ngFor has completed

Within Angular 1, I crafted a personalized directive called "repeater-ready" to pair with ng-repeat for triggering a callback method upon completion of an iteration: if ($scope.$last === true) { $timeout(() => { $scope.$parent.$parent.$ ...

Need to update React textarea with value that is currently set as readonly

In my React application, I have a textarea that is populated with a specific value. My goal is to allow this textarea to be updated and then submit the form in order to update the corresponding row in the database. <textarea id="description" className= ...

Colorful displaced pixels in WEBGL

I am a beginner in the world of WebGL and shaders, and I am curious about the most effective way to achieve the following effect. Still a work in progress https://i.sstatic.net/SZMGh.jpg End goal effect https://i.sstatic.net/7NQxW.jpg For the text, I ...

What is preventing me from adjusting the width of an image that extends outside of its parent container, even though I am able to modify the height?

I am facing an issue with a div on my website that has specific properties: #viewport{ overflow: hidden; display: block; position: relative; width: 323px; height: 323px; } Embedded within this div is an element with the following prop ...

Reload a tab on an ajax-enabled webpage

I am currently facing an issue with refreshing a tab instead of the entire page using Ajax. The specific tab in question involves deleting credit cards. When I select the credit card I want to delete and confirm, I use "window.location.reload();" to refres ...

Trapping an anchor tag event in asp.net

I am currently working on a menu bar using HTML code (I am unable to use asp link buttons). <ul> <li><a href="#"><span>Reconciliation</span></a> <ul> ...

Using Vercel for a Next.js Custom Directory Configuration

Seeking guidance on deploying Next.js with Vercel, I have made changes to the structure of my Next.js project: frontend (Current Directory for Next.js) node_modules next.config.js jsconfig.json package-lock.json package.json Contents of package.json scri ...