0
\$\begingroup\$

I have an enemy spaceship, and I want to make it turn to face the player with (for now) a constant angular speed. The ship's orientation is a quaternion. How do I do this?

Since this is totally free rotating space, I feel there should be no preferred up direction in the calculation except one based on the current enemy orientation quaternion... (perhaps the up vector of my coordinate system rotated by the enemy's orientation...?)

(I will eventually also want to make it angularly accelerate and decelerate, but I might be able to figure it out if I understand the solution for constant speed well enough.)

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

I got it. I was nearly there for ages, and flipping the multiplication on the final statement (rotationToApply * orientation, not orientation * rotationToApply) fixed it. Apparently whether you are working in an absolute frame of reference (like here) you put the second rotation to apply on the left side of the multiplication, but in an object's own frame of reference (like when applying angular velocity) you put it on the right. I don't understand any more than that yet.

I have some Lua code that does everything here (~= is "not equal to", and condition and a or b is Lua's equivalent to the ternary operator):

if state.player.position ~= entity.position then
    local entityToTargetDirection = vec3.normalise(state.player.position - entity.position)
    local curDirection = vec3.rotate(consts.forwardVector, entity.orientation)

    local crossResult = vec3.cross(curDirection, entityToTargetDirection)
    local rotationRequiredAxis = vec3.normalise(
        crossResult == vec3() -- This means the two vectors are parallel, so...
        and vec3.rotate(vec3(0, 1, 0), entity.orientation) -- ...pick an arbitrary direction (yaw right in this case)
        or crossResult
    )

    local dotClamped = math.max(-1, math.min(1, -- Clamping because it would sometimes be outside of [-1, 1] and therefore cause acos to return nan
        vec3.dot(curDirection, entityToTargetDirection)
    ))
    local rotationRequiredAngle = math.acos(dotClamped)

    local maxAngle = entity.maxAngularSpeed * dt
    local cappedAngle = math.min(maxAngle, math.max(-maxAngle, rotationRequiredAngle))

    local rotationAxisAngle = rotationRequiredAxis * cappedAngle

    -- if math.abs(getShortestAngleDifference(0, rotationRequiredAngle)) > consts.targettingAngleDistanceThreshold then
        entity.orientation = quat.normalise(quat.fromAxisAngle(rotationAxisAngle) * entity.orientation) -- Normalised to prevent numeric drift
    -- end
end

Convert to your use case as required. I'm going to refactor it to face arbitrary target points.

The rotate function's source code from my maths library (new creates a vec3 in this context):

local function rotate(v, q)
    local qxyz = new(q.x, q.y, q.z)
    local uv = cross(qxyz, v)
    local uuv = cross(qxyz, uv)
    return v + ((uv * q.w) + uuv) * 2
end

And getShortestAngleDifference, if you choose to make the entity stop rotating when within a threshold (I'm sure the maths can be simplified considering how it is used in the code above):

local function getShortestAngleDifference(a, b)
    -- a to b is b - a
    return (b - a + consts.tau / 2) % consts.tau - consts.tau / 2
end

Also fromAxisAngle from my maths library (# is length, rawnew creates a quaternion (in order x, y, z, w)):

local function fromAxisAngle(v)
    local angle = #v
    if angle == 0 then return rawnew(0, 0, 0, 1) end
    local axis = v / angle
    local s, c = sin(angle / 2), cos(angle / 2)
    return normalise(new(axis.x * s, axis.y * s, axis.z * s, c))
end
\$\endgroup\$
1
  • 3
    \$\begingroup\$ A recommendation that has saved GOBS of stress: always give rotation quaternions/DCMs a name. Don't use them inline. name them either somethingToOther or otherFromSomething, depending on which direction your API's quaternions rotate. Once you figure it out for your API, just use the same convention everywhere and you can check your rotations syntactiaclly by making sure the frames chain natrually. For example, if you have a conversion from A to B to C, and your API uses 'from' ordering, that would be CFromB * BFromA. That ability to visually cancel is worth its weight in gold. \$\endgroup\$
    – Cort Ammon
    Feb 25 at 15:39
0
\$\begingroup\$

There's many approaches, but since you're not worried about final orientation, the easiest approach is just to use a cross product. If you take the cross product between the enemy's current LOS and the desired LOS (towards you), you get a vector which is perpendicular to both. If you rotate your enemy's orientation around this vector, it will move towards your character.

Just use the formula for constructing a rotation quaternion, rotating theta radians about a vector v: cos(theta/2) + v*sin(theta/2)

\$\endgroup\$
1
  • \$\begingroup\$ Thanks, this helped! But since there turned out to be more to the story, I will post my own answer. \$\endgroup\$ Feb 25 at 11:54

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .