Saturday, March 2, 2013

Fun with Three.js Camera Orientation

‹prev | My Chain | next›

I am ready to finally extract my Three.js labeling code out into a library. But first one or two minor tweaks.

The first is just an API adjustment. Instead of indicating the duration that a label should apply with an object literal:
  new Label(earth, "Earth", {remove: 5});
  new Label(mars, "Mars");
I think I prefer to simply make the third argument the optional value for the duration that the label should persist:
  new Label(earth, "Earth", 5);
  new Label(mars, "Mars");
This is an easy enough change:
  function Label(object, content, duration) {
    this.object = object;
    this.content = content;
    if (duration) this.remove(duration);

    this.el = this.buildElement();
The next thing that I would like to change is a bit more involved. It has to do with how the label is placed above the Three.js object. If the camera orientation is not changed, then the calculation is a (relatively) straight-forward transformation from 3D coordinates to 2D coordinates.

But if the camera is rotated in any direction, then things go awry. For instance, in my solar system simulation, I include an "Earth-cam" view of Mars. To see Mars, I have to rotate the camera around the X-axis. This throws off my label placement. Specifically, the label goes in the the middle of the planet instead of above it:

To get around this, I have to multiply by a series of sines and cosines:
  Label.prototype.render = function(scene, cam) {
    var p3d = this.object.position.clone();
    p3d.z = p3d.z + this.object.boundRadius * Math.sin(cam.rotation.x) * Math.cos(cam.rotation.y);
    p3d.y = p3d.y + this.object.boundRadius * Math.cos(cam.rotation.x) * Math.cos(cam.rotation.z);
    p3d.x = p3d.x - this.object.boundRadius * Math.sin(cam.rotation.z) * Math.sin(cam.rotation.y);

    var projector = new THREE.Projector(),
        pos = projector.projectVector(p3d, cam),
        width = window.innerWidth,
        height = window.innerHeight,
        w = this.el.offsetWidth,
        h = this.el.offsetHeight; = '' + (height/2 - height/2 * pos.y - h - 10) + 'px'; = '' + (width/2 * pos.x + width/2 - w/2) + 'px';
The end result being that the label is again placed above the planet:

I more or less figure these sines and cosines out by guessing. I know that cosine of zero is 1 and decreases and that sine of zero is 0 and increase. I use that to make educated guess of where to start with each value. For instance, the original above-the-solar system view had no rotation at all and only needed the target position to shift up in the Y direction. Given that, I knew that I needed cosines for the shift in the Y direction. But after that, I mostly play around with the cameras to figure this out by experimentation.

In the end, this seems to work for most of the cases that I tried, which ought to be good enough for the book. And if people find a case in which I got it wrong, I can always revisit.

(live code demo with the new library)

Day #678

1 comment:

  1. Does your technique to add labels handle the situation of maintaining a static size i.e. so that zooming out does not shrink the text down to an illegible size? If so, I would love you to point me to a working demo (I could not get the 'live code demo' link above to work for me). Thanks!