Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fit 3D Object (Collada File) within Three.JS Canvas on initial load

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.

like image 976
Kode Avatar asked Dec 04 '15 22:12

Kode


2 Answers

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

like image 58
Blindman67 Avatar answered Dec 01 '22 00:12

Blindman67


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

}
like image 27
BeatriceThalo Avatar answered Dec 01 '22 00:12

BeatriceThalo