Rotating Around a Center

A new friend on one of the scripting lists was working to rotate an object assembly around an axis. They had found an article about doing that, and were working from it. Something had gone wrong and it wasn’t working. While I had an idea about how to debug it, I thought it would be better to build something that worked the way I think such things should work. I built a little example, and I’ll include it here at the end of the article. What I’m going to do here, though, is do it all over again, telling you how I work, when I’m at my best, and showing you how it goes.

There are a number of ways to do this kind of thing. You can hard-code in various settings, like the center of rotation in world coordinates, the position of the object and its rotation at the beginning, and so on. Or you can be more dynamic, which is the approach we’ll start with here. So as shown in the picture, we’ve got a center axis object, and an object that’s going to rotate around it. We want our rotating object to always face the center object as our angle of rotation changes.

When you think about it, then, it’s pretty easy to see that there are two issues for us in positioning the object given a particular angle of rotation. We have to put the object in the right place, as it circles around the center. And we have to rotate it, so that it always faces “inward”. We’ll consider those two issues separately.

rotator_001

Here I am with my starting setup, a red axis object, cleverly named “axis”, and a turquoise pointy object named “rotator”. My plan is to make rotator rotate around axis. Thinking about it, and with a little experience from last time, I think I’ll start by having axis say “step” on some channel when I click it, and then make rotator move around when axis says to do it. So let’s start with the axis script:

// axis script
// say step on 231 when touched
// JR 2018-09-01

default {
  touch_start(integer num_detected) {
    if ( llDetectedKey(0) != llGetOwner() ) return;
    llSay(321, "step");
  }
}

I have an attachment that lets me listen to channels, so I know this little script is saying “step” every time I click it. Now let’s get to the meat of the issue, making rotator work. So I need to think a little about what I want it to do. Let’s say this:

When a rotator object starts up, it will sense for an object named “axis”, and it will remember its position, and its own rotation as of that moment. It will think of that situation as “rotation_zero”, no matter where it is relative to axis. When it hears step, it will add some increment, I think ten degrees, to its rotation, then position and rotate itself accordingly.

There are other approaches we could take: this will be the one I’ll start with. I reserve the right to change my mind as I learn.

Anyway, I’ll start with a bit of boilerplate to start the rotator up and collect the information it needs. That will be, let’s say, the position of axis, and our own position and rotation. We’ll see shortly whether that’s just what we need.

// rotate around "axis" object
// JR 2018-09-01

integer  can_move = FALSE;
vector   axis_position;
vector   start_position;
rotation start_rotation;

default {
  on_rez(integer start_param) {
    llResetScript();
  }

  state_entry() {
    llListen(321, "axis", "", "");
    llSensor("axis", "", ACTIVE|PASSIVE, 10, PI);
  }

  no_sensor() {
    can_move = FALSE;
    llOwnerSay("can't find axis");
  }

  sensor(integer num_detected) {
    can_move = TRUE;
    llOwnerSay("found axis");
  }

  listen(integer channel, string name, key id, string message) {
    if ( message == "step" ) {
      if ( can_move )
        llOwnerSay("I'd be moving now if I knew how.");
      else
        llOwnerSay("I can't move but I know I'm supposed to.");
    }
  }
}

That’s quite a bit but I hope it’s pretty clear. On rezzing, we reset, which will send us to state_entry. On state_entry, we sense for axis. If we don’t find it, we say we ca’t, and set that we can’t move. If we do see it, we set that we can move and say we’ve found it.

And we hear the axis saying “step” we either say that we would move if we could, or that we can’t.

Now this is probably more code than I’d usually write without testing but it works just right. I like to go in tiny steps, being sure all the time that everything is OK.

Now that that works, I’ll save the information when I find the axis:

  sensor(integer num_detected) {
    can_move = TRUE;
    llOwnerSay("found axis");
    axis_position = llDetectedPos(0);
    start_position = llGetPos();
    start_rotation = llGetRot();
  }

I’m pretty sure that’s the right info but other than printing it, there’s no useful way yet to be sure. Just to show you how I might do that, I’ll put a print in there:

  sensor(integer num_detected) {
    can_move = TRUE;
    llOwnerSay("found axis");
    axis_position = llDetectedPos(0);
    start_position = llGetPos();
    start_rotation = llGetRot();
    llOwnerSay(llDumpList2String(
      [axis_position,
      start_position,
      start_rotation], ", "));
  }

And that print the values I expected.

I checked that by looking in the object editor. Now there’s no putting it off, I need to start rotating. That’s easy at first:

  listen(integer channel, string name, key id, string message) {
    if ( message == "step" ) {
      if ( can_move )
        move_one_step();
      else
        llOwnerSay("I can't move but I know I'm supposed to.");
    }
  }

I was taught to call this “programming by intention”. At the place where something is supposed to happen, I just say what’s supposed to happen. Inside the listen, if we can move, we want to move one step. Now we “just” have to write that function. What do we want to do?

I think what I’d like to do is to have a rotation called current_rotation, update it by “adding” my step, then position the object and rotate it, according to the current rotation. So I need to start by adding the current_rotation object and initializing it to zero.

integer  can_move = FALSE;
vector   axis_position;
vector   start_position;
rotation start_rotation;
rotation current_rotation = ZERO_ROTATION;

OK, now that that’s done, let’s think about what move_one_step has to do. It’s pretty simple: it should adjust current_rotation by ten degrees or whatever our desired number is, and then move the object to that rotation. Like this:


move_one_step() {
  vector angle_to_rotate = *DEG_TO_RAD;
  rotation to_rotate = llEuler2Rot(angle_to_rotate);
  current_rotation = current_rotation*to_rotate;
  move_to(current_rotation);
}

move_to(rotation r) {
  // do the move
}

So I’ve sliced off the easy bit, leaving the tricky bit yet to so. I’ll generally work this way, slowing slicing off the things I know how to do, and isolating the mysterious bits more and more until they are magically solved. Or, sometimes, solved by hard work. But even then I’ve isolated my confusion to tiny little places.

Let’s go one more step here. The move_to operation has two parts, positioning the object around the circle, and rotating it so it faces in. Let’s break those out, and still not solve them:

move_one_step() {
  vector angle_to_rotate = *DEG_TO_RAD;
  rotation to_rotate = llEuler2Rot(angle_to_rotate);
  current_rotation = current_rotation*to_rotate;
  move_to(current_rotation);
}

move_to(rotation r) {
  position_to(r);
  rotate_to(r);
}

position_to(rotation r) {

}

rotate_to(rotation r) {

}

Mind you, coding these little phases has taken far less time than it has taken to write it up, and less, probably, than it took you to read it. And now I’ve got the problem almost completely solved except for the two tiny details of moving the object and spinning it. Spinning it, as we’ll see, is easier, but I’m going to solve the moving bit first, because it’ll be easier to observe whether it works.

Now here’s a thing you just need to learn and memorize. To move one object in a circle around another, moving from one angle to another, we consider these things: the center position, the object’s starting offset from the center, and the new rotation we want it to move to. The equation is

new_position = center_position + offset*new_rotation

Why is this the case? Well, to rotate a vector, you multiply it times the rotation you want it to have, and that adjusts the vector. But that assumes the vector starts at zero, so to do it in general, we get the offset of the object, rotate the offset, then add it back into the center to get the new position. In our case, the code might look like this:

position_to(rotation r) {
  vector old_offset = start_position - axis_position;
  vector new_offset = old_offset*r;
  vector new_position = axis_position + new_offset;
  llSetLinkPrimitiveParamsFast(1, [PRIM_POSITION, new_position]);
}

And woot! When we click our axis object now, the turquoise object moves around it as planned, not spinning yet, just moving. After three clicks, it looks like this:

rotator_002

Now, if you’re an experienced scripter, you may be getting worried about things being calculated too often. Me too, but I try never to confuse making the code work with making the code efficient. We’ll deal with the efficiency in a bit, and I think you’ll see that it turns out all right.

Now what about rotating our object? Well, it just needs to adjust its own rotation by whatever the current rotation is, so it looks like this:

rotate_to(rotation r) {
  rotation new_rotation = start_rotation*r;
  llSetLinkPrimitiveParamsFast(1, [PRIM_ROTATION, new_rotation]);
}

Sure enough, this works. Here are a couple of pictures of the object now, at various rotations around the axis:

OK, it’s working. Let’s review the code and see how to make it nice. We’ll just look at the working bits now, and review the whole thing at the end.

integer  can_move = FALSE;
vector   axis_position;
vector   start_position;
rotation start_rotation;
rotation current_rotation = ZERO_ROTATION;

move_one_step() {
  vector angle_to_rotate = *DEG_TO_RAD;
  rotation to_rotate = llEuler2Rot(angle_to_rotate);
  current_rotation = current_rotation*to_rotate;
  move_to(current_rotation);
}

move_to(rotation r) {
  position_to(r);
  rotate_to(r);
}

position_to(rotation r) {
  vector old_offset = start_position - axis_position;
  vector new_offset = old_offset*r;
  vector new_position = axis_position + new_offset;
  llSetLinkPrimitiveParamsFast(1, [PRIM_POSITION, new_position]);
}

rotate_to(rotation r) {
  rotation new_rotation = start_rotation*r;
  llSetLinkPrimitiveParamsFast(1, [PRIM_ROTATION, new_rotation]);
}

OK first of all, we’re making two calls to llSetLinkPrimiviteParamsFast, and that’s inefficient. We’ll get that down to one by returning the operation lists back to move_to, and doing the actual setting there:

move_to(rotation r) {
  list operations = position_to(r);
  operations += rotate_to(r);
  llSetLinkPrimitiveParamsFast(1, operations);
}

list position_to(rotation r) {
  vector old_offset = start_position - axis_position;
  vector new_offset = old_offset*r;
  vector new_position = axis_position + new_offset;
  return [PRIM_POSITION, new_position];
}

list rotate_to(rotation r) {
  rotation new_rotation = start_rotation*r;
  return [PRIM_ROTATION, new_rotation];
}

This was a very simple change and just took moments. I changed the position_to and rotate-to to return lists, and edited their llSLPPF lines just to return the lists instead of using them. Then I declared an operations list in move_to, appended to it, and applied it. This is nearly a no-brain-required operation.

And it’s one that I’m glad came up, though I didn’t really plan it. Quite often you’ll have a lot of moving and rotating to do. It’s always best to do that in as few calls as possible, so building up a list and then applying it at the last minute is a very useful technique. I use it a lot in programming our locomotives, which move all kinds of running gear as they go.

What else? Let’s look back at our move_one_step.

move_one_step() {
  vector angle_to_rotate = *DEG_TO_RAD;
  rotation to_rotate = llEuler2Rot(angle_to_rotate);
  current_rotation = current_rotation*to_rotate;
  move_to(current_rotation);
}

OK, those first two lines are always the same so there’s no real reason to do them over and over again on each move. Let’s declare those as globals, initialize them with the rest of our inits, and just use them:

integer  can_move = FALSE;
vector   angle_to_rotate = ;
vector   axis_position;
vector   start_position;
rotation start_rotation;
rotation to_rotate;
rotation current_rotation = ZERO_ROTATION;

...

  sensor(integer num_detected) {
    can_move = TRUE;
    llOwnerSay("found axis");
    axis_position = llDetectedPos(0);
    start_position = llGetPos();
    start_rotation = llGetRot();
    to_rotate = llEuler2Rot(angle_to_rotate*DEG_TO_RAD);
  }

I think I’m going to call this good enough, though I can see some other things one might do. Maybe you’d like to move the initialization out of sensor to a separate function. Maybe you’d do more things in line. For example, the current version of move_to is this:

move_to(rotation r) {
  list operations = position_to(r);
  operations += rotate_to(r);
  llSetLinkPrimitiveParamsFast(1, operations);
}

We could instead do this:

move_to(rotation r) {
  llSetLinkPrimitiveParamsFast(1, position_to(r) + rotate_to(r));
}

Perhaps the first version is ore explanatory but with the function names being as they are, I kind of prefer this more compact one. I didn’t think I would but having tried it, I prefer it.

And there’s an important point. When we are making changes like the one just above, it’s called “refactoring”. We’re making changes to the code’s design, to how it says what it says, but those changes can’t break the code. (Well, by can’t, I mean they’re very unlikely to, because the operations we’re doing are very simple and rote. We could still mess up, and I sometimes do. But by making very small design improvements like this, we aren’t likely to make a mistake, and if we don’t like what we see, we can switch back. in this case I like it.

I’m still reviewing the code for improvement, and I notice this:

list position_to(rotation r) {
  vector old_offset = start_position - axis_position;
  vector new_offset = old_offset*r;
  vector new_position = axis_position + new_offset;
  return [PRIM_POSITION, new_position];
}

The old_offset is a constant, because it’s based on two globals that can’t change. So I should be able to extract it to a global, as I did with the angle and rotation. However, the first time I tried it, my object flew away, which means I’m going to try it again more carefully. I’ll come back to discuss the mistake in a moment but first let’s try this again. First, in this code:

list position_to(rotation r) {
  vector old_offset = start_position - axis_position;
  vector new_offset = old_offset*r;
  vector new_position = axis_position + new_offset;
  return [PRIM_POSITION, new_position];
}

I’ll promote old_offset to be a global, which should be harmless.

vector   old_offset;

list position_to(rotation r) {
  old_offset = start_position - axis_position;
  vector new_offset = old_offset*r;
  vector new_position = axis_position + new_offset;
  return [PRIM_POSITION, new_position];
}

That works, unsurprisingly. Now I want to remove the calculation of old_offset from position_to, and move it to the sensor, where the other things are initialized. That presently looks like this:

  sensor(integer num_detected) {
    can_move = TRUE;
    llOwnerSay("found axis");
    axis_position = llDetectedPos(0);
    start_position = llGetPos();
    start_rotation = llGetRot();
    to_rotate = llEuler2Rot(angle_to_rotate*DEG_TO_RAD);
  }

So I’ll change those to look like this, and everything should still be ok:


list position_to(rotation r) {
  vector new_offset = old_offset*r;
  vector new_position = axis_position + new_offset;
  return [PRIM_POSITION, new_position];
}

  sensor(integer num_detected) {
    can_move = TRUE;
    llOwnerSay("found axis");
    axis_position = llDetectedPos(0);
    start_position = llGetPos();
    old_offset = start_position - axis_position;
    start_rotation = llGetRot();
    to_rotate = llEuler2Rot(angle_to_rotate*DEG_TO_RAD);
  }

And that works just fine. No idea what I did wrong last time, but it’s a good opportunity to talk about safety with objects like this that move around.

Probably the simplest thing to do is to sit on an object that’s going to move. Then, when with its inevitable betrayal, it flies away, you’ll be sitting on it and you can get it back. Depending on the way you move it, of course, it can probably only move ten meters away but if it’s moving on its own, that can add up quickly. In early days, and when making extensive changes, it can be best to print what’s going to happen with llOwnerSay, and comment out the actual move. You could put a checker in to make sure the object doesn’t try to move to , or outside a certain range, or the like. I’ve even put in a timer that checks to see if it’s too far from home, and if it is tells me its location. I’ve lost a lot of objects over the years. Which reminds me of an important technique: don’t call it “object”. Give it a unique name so that your area search or similar tool has a chance of finding it!

I do see a few more changes that we could make. The variable start_position is really only used in the initialization of old_offset now, so we could make that local or remove it entirely. We could probably give that a better name, now that it’s global, perhaps object_offset or original_offset or something like that. That may not seem important now, and I’m not going to do it. But I’m probably wrong. If I come back to this code in a month (or in my case, 24 hours) I’m probably going to wonder why it has that name. But this article is long enough.

No wait, here’s something that is important. In my call to llSetLinkPrimitiveParamsFast, I use 1 as the link number parameter. The way that function works, if it’s a single prim, you move it with zero, otherwise with one. So if you put this code in an object of your own, and it is only one prim, and it doesn’t move, it’s because of that parameter. Often I write a little function, and use it like this:

move_to(rotation r) {
  llSetLinkPrimitiveParamsFast(link_number(), position_to(r) + rotate_to(r));
}

integer link_number() {
  if ( llGetNumberOfPrims() == 1 ) return 0;
  return 1;
}

Someone with a preference for clever code might write

integer link_number() {
  return llGetNumberOfPrims() != 1;
}

I generally don’t do that. It works fine but it’s too tricky for my tiny brain.

Anyway, for the record, here’s the whole program:

// rotate around "axis" object
// JR 2018-09-01

integer  can_move = FALSE;
vector   angle_to_rotate = ;
vector   axis_position;
vector   start_position;
vector   old_offset;
rotation start_rotation;
rotation to_rotate;
rotation current_rotation = ZERO_ROTATION;


move_one_step() {
  current_rotation = current_rotation*to_rotate;
  move_to(current_rotation);
}

move_to(rotation r) {
  llSetLinkPrimitiveParamsFast(link_number(), position_to(r) + rotate_to(r));
}

integer link_number() {
  return llGetNumberOfPrims() != 1;
}

list position_to(rotation r) {
  vector new_offset = old_offset*r;
  vector new_position = axis_position + new_offset;
  return [PRIM_POSITION, new_position];
}

list rotate_to(rotation r) {
  rotation new_rotation = start_rotation*r;
  return [PRIM_ROTATION, new_rotation];
}

default {
  on_rez(integer start_param) {
    llResetScript();
  }

  state_entry() {
    llListen(321, "axis", "", "");
    llSensor("axis", "", ACTIVE|PASSIVE, 10, PI);
  }

  no_sensor() {
    can_move = FALSE;
    llOwnerSay("can't find axis");
  }

  sensor(integer num_detected) {
    can_move = TRUE;
    llOwnerSay("found axis");
    axis_position = llDetectedPos(0);
    start_position = llGetPos();
    old_offset = start_position - axis_position;
    start_rotation = llGetRot();
    to_rotate = llEuler2Rot(angle_to_rotate*DEG_TO_RAD);
  }

  listen(integer channel, string name, key id, string message) {
    if ( message == "step" ) {
      if ( can_move ) 
        move_one_step();
      else 
        llOwnerSay("I can't move but I know I'm supposed to.");
    }
  }
}

I’d call that fairly clean, and hope you think so too. I’ll leave comments open, as I usually do, in case anyone reads this and has questions. If I’m still alive when you ask, I’ll try to answer!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: