6

I have a box (collada file) that loads within a three.js canvas. I can interact with it as expected. However, the box size varies as users can change the size.

When I load it into a 500px by 500px canvas, if the box is large, users have to zoom in before seeing it, and if it is small, it is tiny and users need to zoom in. The size changes depending on the variables that are passed.

How would I have the object (collada file) fit in the canvas on load, and then let users zoom? Here is the code that loads on click to show the 3D object in a three.js canvas:

$scope.generate3D = function () {

        // 3D OBJECT - Variables
        var texture0 = baseBlobURL + 'Texture_0.png';
        var boxDAE = baseBlobURL + 'Box.dae';
        var scene;
        var camera;
        var renderer;
        var box;
        var controls;
        var newtexture;

        // Update texture
        newtexture = THREE.ImageUtils.loadTexture(texture0);

        // Initial call to render scene, from this point, Orbit Controls render the scene per the event listener
        THREE.DefaultLoadingManager.onProgress = function (item, loaded, total) {
            // console.log( item, loaded, total ); // debug
            if (loaded === total) render();
        };

        //Instantiate a Collada loader
        var loader = new THREE.ColladaLoader();
        loader.options.convertUpAxis = true;
        loader.load(boxDAE, function (collada) {
            box = collada.scene;
            box.traverse(function (child) {
                if (child instanceof THREE.SkinnedMesh) {
                    var animation = new THREE.Animation(child, child.geometry.animation);
                    animation.play();
                }
            });
            box.scale.x = box.scale.y = box.scale.z = .2;
            box.updateMatrix();
            init();
        });

        function init() {
            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000);
            renderer = new THREE.WebGLRenderer();
            renderer.setClearColor(0xdddddd);

            //renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setSize(500, 500);

            // Load the box file
            scene.add(box);

            // Lighting
            var light = new THREE.AmbientLight();
            scene.add(light);

            // Camera
            camera.position.x = 40;
            camera.position.y = 40;
            camera.position.z = 40;

            camera.lookAt(scene.position);

            // Rotation Controls
            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.addEventListener('change', render);
            controls.rotateSpeed = 5.0;
            controls.zoomSpeed = 5;
            controls.noZoom = false;
            controls.noPan = false;

            // Add 3D rendering to HTML5 DOM element
            var myEl = angular.element(document.querySelector('#webGL-container'));
            myEl.append(renderer.domElement);

        }

        // Render scene
        function render() {
            renderer.render(scene, camera);
            console.log('loaded');
        }
    }

    // Initial 3D Preview Load
    $scope.generate3D();

Update: I have evaluated the solution presented here: How to Fit Camera to Object but am unsure how to define the distance for my collada file as it can be different depending on what dimensions the user enters. The collada file is generated by users sending variables to a third party vendor that returns a collada file that is subsequently loaded into three.js.

Update 2: Thanks to @Blindman67 I am closer to understanding how this interplays. When I manually up the camera.position x,y,z values, the object is in the screen. The challenge I have is how to determine what the correct x,y,z values will be as each box is dynamically changed and I literally have over 280 Million variations. I know that @Blindman67 already gave me the answer logically, but I just need a final push to discover how to get the right position for objects that vary each time so I can set the correct x,y,z.

Community
  • 1
  • 1
Kode
  • 3,073
  • 18
  • 74
  • 140
  • 1
    http://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object google works wonders http://stackoverflow.com/questions/20059612/calculate-camera-zoom-required-for-object-to-fit-in-screen-height – 2pha Dec 04 '15 at 22:37
  • Thanks. I looked, but I guess I was searching by the wrong nomenclature. – Kode Dec 04 '15 at 22:50
  • Additional help still required, as I am loading collada files and not creating geometry shapes, so I am unsure how to target this so it is zoomed in. – Kode Dec 05 '15 at 07:40
  • 1
    It should not matter, all loaders will create an Object3D, which will have a geometry property – 2pha Dec 05 '15 at 10:11
  • Since I only need to allow the user to interact with the object, is there a different camera I should use/what is the advantage of perspective vs. orthographic? – Kode Dec 05 '15 at 14:16
  • ? what does that have to do with your original question? Why would you need a different camera? search google for the difference between perspective and orthographic. – 2pha Dec 05 '15 at 23:02
  • Didn't know if another camera choice in threejs was more aligned to how I am rendering/my use case. – Kode Dec 06 '15 at 03:09
  • In looking at http://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object I am not sure how to get the distance for a collada file loaded in. Any guidance on how to do this? – Kode Dec 07 '15 at 14:04

3 Answers3

11

Fit a 3D object to a view

There are several ways to fit a 3d object to the camera view.

  • Move the camera backward or forward.
  • Increase or decrease the focal length (this also effects FOV (field of View))
  • Change the world space scale or the object's local space scale to make the object bigger or smaller.

You can discount the last option as it is impractical in most cases. (though I do notice you are doing that in the code given there is enough information in this answer to work out how to scale the object to fit. But I do not recommend you do that)

So you are left with either moving the camera in or out or keeping it stationary and zooming it. It is the same as with a real camera, you zoom or get closer.

Both methods have pros and cons.

Translation

Moving the camera (dolly) is the best for most situations as it keeps the perspective the same (how rapidly lines converge to the vanishing point) and thus does not distort the object in view. In 3D there are 3 problems with this method.

  • The view frustum (the volume in which the scene is displayed) has a back plane (max distance an object will be display) and the font plane (the closest an object can be display). Moving the camera to fit very large or very small object can cause the object to be outside the front or back planes and be clipped in part or completely.
  • Moving the planes to fit the object can also have undesired results. Moving both the front and back planes to hold the object can cause objects nearer or further in the scene to be clipped out.
  • Expanding the total distance between the back and front plane can also cause Z-buffer aliasing artifacts. But these problems only apply to very large or very small objects and scenes.

Zoom

Zooming involves changing the focal length of the camera. In the library you use this is done by adjusting the FOV (Field of view) this is the angle between the left and right side of the view given in degrees. Reducing the FOV effectively increases the focal length and zooms in (3D graphics don't have a focal length like a camera). Increasing FOV zooms out. There are problems with this method.

  • As the FOV decreases the perspective (parallax) decreases making the scene appear less and less 3D. As the FOV increase the perspective increases distorting objects and making objects in the distance very small.
  • As the camera does not move the front and back planes stay in place, but zooming in or out on objects near the back or front planes may still cause z-buffer aliasing artifact.

What method to use is ip to you, you can use one or the other or combine both.

How it is done

To the Question at hand. We need to know how big the object will appear in the scene and use this information to change the camera setting to the desired effect (namely fit the object to the display).

Diagram displaying the camera and the various properties requiered to contruct a view Figure 1. The camera, object, and view.

So there are some values that are needed. See fig1 for visual explanation.

  • FOV. I will convert this to Radians
  • Object's distance from the camera
  • Object's bounding sphere radius.
  • Front plane
  • Back plane
  • Screen Pixel size

You will need to calculate the bounding sphere of the object. Or use another value that approximates the bounding sphere. I will leave that up to you.

The code

var oL,cL; // for the math to make it readable
var FOV = 45 * (Math.PI / 180); // convert to radians
var objectLocation = oL = {x : 0, y : 0, z : 400};
var objectRadius = 50;
var cameraLocation = cL = {x : 0, y : 0, z : 0};
var farPlane = 1000;
var nearPlane = 200;
var displayWidth = 1600;
var displayHeight = 1000;

To work out how large the bounding sphere will appear on the view is simple trig.

// Get the distance from camera to object
var distToObject = Math.sqrt(Math.pow(oL.x - cL.x, 2) + Math.pow(oL.y - cL.y, 2) + Math.pow(oL.z - cL.z, 2));Figure 1

As we are using the right triangle (see fig1) we multiply the result by 2 to give the total angular size

// trig inverse tan of opposite over adjacent.
var objectAngularSize = Math.atan( (objectRadius) / distToObject ) * 2;

Get the fraction of the FOV that the object occupies.

var objectView = objectAngularSize / FOV;

And finally you get the pixel size of the object.

var objectPixelSize = objectView * displayWidth;

That is all you need to know to do what you ask. It would help you understand if you use the above code and math to try and rearrange the calculation so that you get the object to occupy ta desired pixel size by either moving the camera or FOV. Copying code does not teach you much, using the above information by applying it will set it in your mind and will make many other processes you need for 3D easier in future.

That said copying code is the quick solution and is what frameworks and libraries are all about. No need to know how you have better things to learn.

Zoom to fit.

This is the easiest and gets the object's angular size and adjust the FOV to fit. (NOTE I am using radians. the Three.js uses degrees for FOV you will need to convert)

var requieredObjectPixelSize = 900;
var distToObject = Math.sqrt(Math.pow(oL.x - cL.x, 2) + Math.pow(oL.y - cL.y, 2) + Math.pow(oL.z - cL.z, 2));
var objectAngularSize = Math.atan( (objectRadius) / distToObject ) * 2;
// get the amount the FOV must be expanded by
var scaling = displayWidth / requieredObjectPixelSize;
// change the FOV to set the objects size 
FOV =  objectAngularSize * scaling;

Convert FOV to degrees and use it to create the camera.

Translate to fit

Move the camera to fit the object. This is a little more involved but is the better method.

// Approx size in pixels you want the object to occupy
var requieredObjectPixelSize = 900;

// camera distance to object
var distToObject = Math.sqrt(Math.pow(oL.x - cL.x, 2) + Math.pow(oL.y - cL.y, 2) + Math.pow(oL.z - cL.z, 2));

// get the object's angular size.
var objectAngularSize = Math.atan( (objectRadius) / distToObject ) * 2;

// get the fraction of the FOV the object must occupy to be 900 pixels
var scaling = requieredObjectPixelSize / displayWidth;

// get the angular size the object has to be
var objectAngularSize = FOV * scaling;

// use half the angular size to get the distance the camera must be from the object
distToObject = objectRadius / Math.tan(objectAngularSize / 2);

Now to move the camera. It must be moved along the vector between the object and the camera.

// Get the vector from the object to the camera
var toCam = {
    x : cL.x - oL.x,
    y : cL.y - oL.y,
    z : cL.z - oL.z,
}

Normalise the vector. This means make the length of the vector equal to 1 and is done by dividing each component (x,y,z) by the vector's length.

// First length
var len = Math.sqrt(Math.pow(toCam.x, 2) + Math.pow(toCam.y, 2) + Math.pow(toCam.z, 2));
// Then divide to normalise (you may want to test for divide by zero)
toCam.x /= len;
toCam.y /= len;
toCam.z /= len;

Now you can scale the vector to make it equal to the distance the camera must be from the object.

toCam.x *= distToObject;
toCam.y *= distToObject;
toCam.z *= distToObject;

Then its just a matter of adding the vector to the object's location and putting it in the camera location

cL.x = oL.x + toCam.x;
cL.y = oL.y + toCam.y;
cL.z = oL.z + toCam.z;

cl now holds the camera location.

One last thing. You need to check if the object is inside the view.

if (distToObject - objectRadius < nearPlane) {
    nearPlane = (distToObject - objectRadius) * 0.8; // move the near plane towards the camera 
                                                 // by 20% of the distance between the front of the object and the camera
}

if (distToObject + objectRadius > farPlane) {
    farPlane = distToObject + objectRadius * 1.2; // move the far plane away from the camera 
                                              // by 1.2 time the object radius
}

There is still one issue. The object if very small may be so close that the front of the object is behind the camera. If this happens you will need to use the Zoom method and move the camera back. This will be only for very select cases and can be ignored on the most part.

I have not given information on how to integrate this with Three.js but this is intended to be a generic answer that applies to all 3D packages. You will have to consult the three.js documentation on how to change the various camera settings. Its is straightforward and applies to the perspective camera.

Ok a big answer and I need to forget it for a bit as I don't see the typos and mistakes without a break. I will return and fix it up later in the day.

Hope it helps

Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • This is by far the most instructive post I have ever seen on StackOverflow. Irrespective of needing to figure out how to make this work in threejs code, I just learned a ton, which is more valuable than just some code. You could say it gave me greater "perspective"... – Kode Dec 08 '15 at 02:53
  • This really helped up my knowledge, but I need to know how to achieve this in threejs. Any takers? – Kode Dec 09 '15 at 15:10
  • 1
    You need to get the size of the box and its position in 3d space. Use `box.computeBoundingSphere();` and `var boxRadius = box.boundingSphere;`to get the size, and then `box.position` has the box (x,y,z) position. Use those values in the answer example then use `camera.fov` to set the FOV or `camera.position` to set the position. Apart from that I don't know where you are stuck – Blindman67 Dec 09 '15 at 19:31
  • I guess the challenge I am having is seeing this in the context of my code. Logically, it is making more sense, but which values and variables go where is the issue. I tried adding box.computeBoundingSphere but it states that it is not a function. I noticed that if I manually change the camera.position.x, camera.position.y, and camera.position.z it zoomed in more, but I am still confused on how to get the right x,y,z values. – Kode Dec 11 '15 at 08:16
  • You need to know what size the box is then you can work out its angular size in the view, because you know what the camera FOV (the angular view size is Field Of View) you can move it to the correct distance to. All the code is under Translate to fit (in the answer) but you need the size of the box. You move the camera along the line between the object and its self. – Blindman67 Dec 11 '15 at 09:58
  • Thanks. This is where I am struggling a bit. How do I get the size of the box? – Kode Dec 11 '15 at 13:55
  • I do have the length, width, and height in inches. Perhaps converting those to pixels will give us the box size? – Kode Dec 11 '15 at 16:38
  • @Kode Inches? The box will be in whatever units the scene is in and has no real relationship to pixels. Try using the size you have. I do notice that you scale the box. You will have to scale the size as well, or not do the scale. – Blindman67 Dec 11 '15 at 23:00
  • When you say size. Do you mean the width or height in pixels of my box. Should I not scale the box/what's the most basic example of a big box (where sizes change), in a scene? – Kode Dec 12 '15 at 01:12
  • 1
    Size is the size in 3D space, the actual pixels size will be determined by the device screen and the desired pixel size `var requieredObjectPixelSize = ?;` after applying the translate to fit process. The coordinate system for 3D objects are independent of screen resolution. You can scale the box to fit but then you would also have to scale everything in the scene. – Blindman67 Dec 12 '15 at 01:25
  • So, it is really about the camera position? This is where I am stuck. I can manually change that, but since the boxes dynamically change, how do I get the values for camera position? – Kode Dec 12 '15 at 01:28
  • The camera and box each have a position. The first bit of code under the heading **the code** is where I set the camera and box location `cameraLocation` and `objectLocation` I use shortCuts `cl` & `ol` and in the translate to fit method you will see how I move the camera along the line created by the two points. I get the vector from the object to camera, normalise it, multiply that vector by the distance the camera must be away from object, then add the object pos then set the camera position. Its set out step by step in the answer. – Blindman67 Dec 12 '15 at 01:38
2

This code will 'auto-center' the camera around the given object.

For user 'zooming', also take a look at three.js examples/js/controls/TrackballControls.js which dollies the camera position (and changes lookat and up). If you choose to use that, I remember that you need to initialize the controls before moving the camera position with centerCam. In that case centerCam gets the whole object in view to prep for the user interaction, and TrackballControls listeners take over after that.

<body onload="initPage()">
  <canvas id="cadCanvas" width="400" height="300"></canvas>
  <button onclick="loadFile()">Load Collada File</button>
</body>

function initPage(){
  var domCanvas = document.getElementById('cadCanvas');
  scene = new THREE.Scene();
  var fovy = 75;
  camera = new THREE.PerspectiveCamera( fovy, domCanvas.width/domCanvas.height, 0.1, 1000 );
  renderer = new THREE.WebGLRenderer({canvas:domCanvas});
  renderer.setSize( domCanvas.width, domCanvas.height );
}

function loadFile(){
  new THREE.ColladaLoader().load( url, function(obj3D) {
    scene.add(obj3D);
    centerCam(obj3D);
    renderer.render(scene, camera);
  });
}

function centerCam(aroundObject3D){

  //calc cam pos from Bounding Box
  var BB = new THREE.Box3().setFromObject(aroundObject3D);
  var centerpoint = BB.center();
  var size = BB.size();
  var backup = (size.y / 2) / Math.sin( (camera.fov/2)*(Math.PI/180) );
  var camZpos = BB.max.z + backup + camera.near ;

  //move cam
  camera.position.set(centerpoint.x, centerpoint.y, camZpos);
  camera.far = camera.near + 10*size.z;
  camera.updateProjectionMatrix();

}
Michelle Norris
  • 568
  • 7
  • 14
-1

Thanks to Blindmann67, I was able to better understand all these complexities and come up with a solution in threejs. There is one particular line where the box scale was being manually set:

box.scale.x = box.scale.y = box.scale.z = .2;

Since I know the width of my object (dynamic), and the size of the canvas (500px), I was able to get the correct scale ratio via the following:

var initialZoomScale = 500 / canvasWidthProdPixels; 
// canvasWidthProdPixels is my large object (ex. 25,000 px wide) dividing these gives me a small fraction such as .02 which scales the object perfectly)

box.scale.x = box.scale.y = box.scale.z = initialZoomScale;
Kode
  • 3,073
  • 18
  • 74
  • 140