Lesson 21 - Orthographic Projections

 

Introduction

This lesson is about orthographic projections. Orthographic projection is a means of representing a three-dimensional object in two dimensions. It is a form of parallel projection, where all the projection lines are orthogonal to the projection plane, resulting in every plane of the scene appearing in an affine transformation on the viewing surface.

There are many types of projections, including orthographic, perspective, isometric, oblique as well as the complex geographic projections that map a curved surface onto a 2D surface. Three.js supports both the common perspective transformation (which we have been using throughout these lessons up to now) as well as orthographic.

Orthographic projections have the important quality that the objects in the projection all have the same dimensions, no matter the distance from the obsever. Consider this diagram:



Note that in both "scenes" the red and yellow balls are actually the same size, but in the perspective projection, the red ball is distinctly smaller. This seems natural to us, as our vision is essentially a perspective projection onto the back of our eyeball. Things in the distance are smaller, are they not? But note that in the right hand figure, the red ball is still the same size as the yellow ball.

Orthographic projection is widely used in the engineering industry, as depicted in this diagram:



Note that the left-hand 3D representation is also an orthographic projection. Most often, an orthographic projection is in 2D (e.g. architectural drawings, etc.) but orthographic projections can also be in 3D, which is the subject of this lesson. So let's look at the lesson.

Perspective and Orthographic Projections

Most of this lesson is actually about changes made in GFXScene to support both perspective and orthographic projections. Let's first review perspective projections.

A perspective camera (the default) is intialized like this:

this.camera = new THREE.PerspectiveCamera(this.fov, this.canvasWidth / this.canvasHeight, 
                               this.near, this.far);

Where fov is the field of view (the angle of the view, as shown in the diagram above). The default in GFXScene is 45 degrees. The next parameter is the aspect ratio of the view. and the last two parameters are the near and far clipping planes. Pretty straighforward (though there is a lot of gnarly matrix math in three.js hidden under the covers).

An orthographic camera's instantiation looks like this:

var aspect = this.canvasWidth / this.canvasHeight;
var w2 = this.orthoSize * aspect / 2;
var h2 = this.orthoSize / 2;
this.camera = new THREE.OrthographicCamera( -w2, w2, h2, -h2, 0.01, 1000);

Where orthoSize is the height of the viewport in user-units, i.e. in the units we want to draw in, as opposed to screen coordinates. So we're just setting up a rectangle in user-units that the projection will be mapped into. Again, pretty straightforward (ignoring all that gnarly matrix math :-). Let's see how it is used in practice.

Initialization

Our instantiation of the scene is this:

var gfxScene = new GFX.Scene( {
    perspective:false,
	 orthoSize: REPEAT_SIZE * 2,
	 cameraPos:[15,10,10],
	 axisHeight:10,
	 controls:true,
	 displayStats:true,
    floorX:REPEAT_SIZE*2,
    floorZ:REPEAT_SIZE*2,
	 floorRepeat:REPEAT_SIZE,
    floorImage: "images/grey_checkerboard.jpg"});

The keys here are that we pass in perspective:false, telling the scene that we an orthographic camera. Set the orthoSize to the size of the "floor" we are setting up. We set the camera position to a reasonable value to the right and back from the floor. We set some reasonable values for the (checkerboard) floor and that's it for instantiation.

In initializeDemo we set up a big rectangular array of "crates":

var boxGeometry = new THREE.BoxGeometry(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE);
	
crateTexture = new THREE.ImageUtils.loadTexture("images/Crate.jpg");
var boxMaterial = new THREE.MeshLambertMaterial({
    map:crateTexture,
    side:THREE.DoubleSide
});

var boxMesh;
for ( var z=-REPEAT_SIZE; z<=REPEAT_SIZE; z += 1 ) {
    for (var x = -REPEAT_SIZE; x <= REPEAT_SIZE; x += 1) {
        boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);
        boxMesh.position.set(x, yTranslation, z);
        gfxScene.add(boxMesh);
    }
}

We provide a key handler so the user can switch between perspective and orthographic projections. This triggers a call to a new method in GFScene (which will come to in a moment)

if (keyChar === 'p')
    gfxScene.setCamera({ perspective:true, cameraPos:[15,10,10], fov : 90  });
else if (keyChar === 'o')
    gfxScene.setCamera({ perspective:false, cameraPos:[15,10,10] });

Note that we set a fov for the perspective camera of 90 degrees. This is pretty extreme, but it serves to exaggerate (for the demo) the difference between orthographic and perspective transformations.

The setCamera Method

Now, in GFXScene, we have a new method, setCamera():

setCamera: function ( jsonObj ) {
    if (jsonObj !== null)
	    GFX.setParameters(this, jsonObj);
        
    var aspect = this.canvasWidth / this.canvasHeight;

    if (this.perspective === true)
	      this.camera = new THREE.PerspectiveCamera(this.fov, aspect, this.near, this.far);
    else {
        var w2 = this.orthoSize * aspect / 2;
        var h2 = this.orthoSize / 2;
        this.camera = new THREE.OrthographicCamera( -w2, w2, h2, -h2, 0.01, 1000);
    }

    this.camera.updateProjectionMatrix();

    if (this.cameraPos === undefined)
        this.camera.position.set(0, 10, 20);
    else
        this.camera.position.set(this.cameraPos[0], this.cameraPos[1], this.cameraPos[2]);

    this.camera.lookAt(this.scene.position);

    if (this.controls === true && this.renderer !== null)
        this.orbitControls = new THREE.OrbitControls( this.camera, 
                                                      this.renderer.domElement );
},

The first block checks if the user passed in parameters (as do above in the keyhandler) and parses the parameters out and stores them in the GFXScene object. If perspective is true (the default) we set up the camera as we have done before. But if the camera is orthographic we have to calculate the bounds of the sides of the camera, which is just orthoSize adjusted for the aspect ratio of the window. We then telll three.js to update the projection matrix, then set the camera's position and what it is looking at. Last but not least, note that we have to update the orbitControls since we have changed the camera.

Handling Window Resizing

Finally, it's worth noting that handling of the resizing of the window has to take into account what type of projection is being used:

var _self = this;

// add an event listener to handle changing the size of the window
window.addEventListener('resize', function() {
    _self.canvasWidth  = window.innerWidth;
    _self.canvasHeight = window.innerHeight;
    var aspect = _self.canvasWidth / _self.canvasHeight;

     if (_self.perspective === true ) {
          _self.camera.aspect = aspect;
     } else {
         var w2 = _self.orthoSize * aspect / 2;
         var h2 = _self.orthoSize / 2;

         _self.camera.left   = -w2;
         _self.camera.right  = w2;
         _self.camera.top    = h2;
         _self.camera.bottom = -h2;
      }

      _self.camera.updateProjectionMatrix();
      _self.renderer.setSize( _self.canvasWidth, _self.canvasHeight );
});

Note that the use of _self is because an anonymous function like this doesn't have access to GFXScene's this. Instead, this in the anonymous function would mean the window's this, which is definitely not what we want. Otherwise, this code mirrors the code that sets up the cameras in the first place.

And that's it! Click on this link to see the actual rendered demo in all it's orthographic glory!

As always, the original sources are on github here.



About Us | Contact Us | ©2017 Geo-F/X