Thursday, May 7, 2020

Rotating the Cones

Our second step is to allow the user to drag the screen around causing the cones to rotate.  Since we have most of this code already, this should be simple right?

Sadly, not.  I think I knew at the time that the complexities of positioning, rotation and the fact that the center of the cone was not the point about which I wished to rotate them would come back to bite me.

Check out the tag ROTATE_CONES_WRONG_CENTER to see the details, but this is not going to do what we want but is going to rotate the two cones independently about their individual centers.  But it does get the generic event handler code out of the way.

First things first

The first thing we need to do is to promote the variables top and bottom from being scoped inside onLoad to being scoped across the entirety of the setup function.

Then we need a variable to track where the drag started so that we can update based on the relative mouse position.

So, at the top this looks like:
function loadConics() {
  var dragStart;
  var top, bottom;
And in onLoad we have:
top = cone(25, Math.PI);
bottom = cone(-25, 0);

Adding the Event Handlers

Again using the moon example as our model, we can add two event handlers for the dragging case.  The first one handles the start of the drag and remembers the starting position:
onDragStart: function (e) {
  dragStart = { x: e.x, y: e.y }
},
The second one handles the case when the mouse has been dragged.  Here we need to calculate the apparent rotation for the movement, update the cones and then remember the new "current" position.
onDragMove: function(e) {
  top.rotation.y += -(e.x - dragStart.x) / 100;
  top.rotation.x += -(e.y - dragStart.y) / 100;
  top.update();
  bottom.rotation.y += (e.x - dragStart.x) / 100;
  bottom.rotation.x += (e.y - dragStart.y) / 100;
  bottom.update();
  dragStart.x = e.x;
  dragStart.y = e.y;
}
This may look like I've really thought about how this is going to work, but nothing could be further from the truth.  I basically just copied the code from the moon example and then tinkered with it until it worked the way I expected.

Fairly obviously, we want the change to be proportional to the amount that the mouse has moved.  Almost equally obviously, we want the rotation about the y-axis to be correlated to the horizontal (x) movement of the mouse and the rotation about the x-axis to be correlated to the vertical (y) movement of the mouse.

Beyond that, the random constant 100 is proportional to the full travel of the mouse, which is 500 pixels in the defined canvas; that gives us the ability to rotate 5 radians, which is just short of 2π for a full rotation.  The signs I determined experimentally based on my intuition of what should happen: when it didn't, I reversed the most likely sign.

The update() calls are required to update the matrix, and then the final two lines simply remember the current position.

Animation

In order for this to actually work, we need to add a "frame loop".  Every time we draw the scene, we need to request that it be drawn again.  This is what the requestAnimationFrame() method does.
function draw() {
  ...
  PhiloGL.Fx.requestAnimationFrame(draw);
}

The Problem

The problem is that I want the whole scene to rotate about the origin.  But that's not what's happening: each cone is rotating about its own center, which, as we discovered before, it not at the base or the tip but in the center of the central axis.  This means that the two cones stop touching the moment you rotate them.  To fully appreciate the experience, you'll need to check out the broken code, but here's a sample:
The root of the problem, of course, is that matrix operations are not commutative: that is, that the order in which they are performed makes a difference to the outcome.  I'm not going to go into the details here (consult a matrix textbook) but you can perform a simple experiment with a piece of paper:
  • First, hold it up vertically in front of you.
  • Then rotate about the x-axis by 90º so that it falls "away" from you and lies flat.
  • Now rotate it about the y-axis by 90º so that the part that is further away from you moves to the left.
The paper should be horizontal with its top to your left hand side.
  • Now, hold it vertically again.
  • First rotate it about the y-axis by 90º so that the right hand side moves away from you.
  • Now rotate it about the x-axis by 90º so that the top moves away from you.
The paper should now be vertical but on its side with its top furthest from you.

The only difference here is the order of operations.

The update() Method

When we call update() on our cones, internally it is taking the rotation, scale and translation we have applied and performing them in a particular order.  Specifically, it is rotating and then translating.  We don't want that: we want to translate first.

Let's look at the code:
update: function() {
  var matrix = this.matrix,
  pos = this.position,
  rot = this.rotation,
  scale = this.scale;

  matrix.id();
  matrix.$translate(pos.x, pos.y, pos.z);
  matrix.$rotateXYZ(rot.x, rot.y, rot.z);
  matrix.$scale(scale.x, scale.y, scale.z);
}
Now, while this appears to do the translation first, that is again because of the properties of matrix math: the first operation specified is the last one "to take effect".  So this is scaling first, then rotating, then translating.

We simply want to do our translation and rotation in the other order, so we'll copy this code into ours, simplify and refactor what's left and update everything.  So we end up with this:
onDragMove: function(e) {
  rot(top, e, -1, -1);
  rot(bottom, e, 1, 1);
  dragStart.x = e.x;
  dragStart.y = e.y;

  function rot(comp, e, sgnx, sgny) {
    comp.rotation.y += sgnx * (e.x - dragStart.x) / 100;
    comp.rotation.x += sgny * (e.y - dragStart.y) / 100;

    var m = comp.matrix;
    m.id();
    m.$rotateXYZ(comp.rotation.x, comp.rotation.y, comp.rotation.z);
    m.$translate(comp.position.x, sgny*comp.position.y, comp.position.z);
    m.$scale(comp.scale.x, comp.scale.y, comp.scale.z);
  }
}
I'm not going to comment further on that code, since other than the changing of operation order it is just a series of refactorings from what we had before.  To check out all the code, use the tag ROTATE_CONES_SCENE_CENTER.

Summary

With a small hiccough, we managed to rotate the cones around the scene origin.

No comments:

Post a Comment