# Turret rotation

> A complete turret script for a vehicle: the ring turns around the vehicle's up axis, the barrel tilts, and both follow the seated avatar's camera at their own speeds.

A turret in a vehicle: the ring (`Turret`) turns only around the vehicle's up axis and the barrel
(`Barrel`) only tilts, each at its own top speed, following the camera of whoever sits on the
root prim. It applies `ConstrainYaw`, `ConstrainPitch` and `StepRotation` from
[Constraining rotations](/guides/constraining-rotations/).

:::caution[Untested in-world]
This script has been reviewed against the API pages but not tested in Second Life.
:::

## What to link

- The base (the vehicle, or a stand) is the **root prim**. The script goes in it, and the avatar
  sits on it.
- A child prim named `Turret`: the ring that turns.
- A child prim named `Barrel`: the part that tilts.

The script finds the ring and barrel by name with `LinkByName`; see
[Working with link numbers](/guides/link-numbers/#find-links-by-name) for how that works and how
to look the links up again when the linkset changes.

## The script

The script is complete and can be pasted as is. It includes the functions it uses (folded at the
top); [Constraining rotations](/guides/constraining-rotations/) is the reference for those
functions and their limits.

```lsl collapse={1-37}
// Link number of a prim by name (see Find links by name).
integer LinkByName(string needle)
{
    integer prims = llGetNumberOfPrims() + 1;
    while (--prims)
        if (llGetLinkName(prims) == needle) return prims;
    return FALSE;
}

rotation ConstrainYaw(rotation R, vector N)
{
    vector U = N;
    vector L = llRot2Fwd(R) % U;
    vector F = llVecNorm(U % L);
           L = llVecNorm(U % F);
    return llAxes2Rot(F, L, U);
}

rotation ConstrainPitch(rotation R, vector N)
{
    vector F = llRot2Fwd(R);
    vector L = llVecNorm(F % N);
    vector U = -llVecNorm(F % L);
           L = U % F;
    return llAxes2Rot(F, L, U);
}

// Turns from towards to by at most maxAngle radians, taking the shorter way round.
rotation StepRotation(rotation from, rotation to, float maxAngle)
{
    rotation delta = (ZERO_ROTATION / from) * to;
    // q and -q are the same rotation; pick the one with the smaller angle.
    if (delta.s < 0.0) delta = <-delta.x, -delta.y, -delta.z, -delta.s>;
    if (llRot2Angle(delta) <= maxAngle) return to;
    return from * llAxisAngle2Rot(llRot2Axis(delta), maxAngle);
}

integer LinkTurret;
integer LinkBarrel;
vector hardpointNormal = <0.0, 0.0, 1.0>;  // the turret's up axis, in the root's frame; unit length
vector turretBase = <0.0, 0.0, 1.0>;       // the barrel's pivot, in the root's frame
float turretTraverseSpeed = 1.0;           // radians per second
float turretPitchSpeed = 0.5;              // radians per second
float interval = 0.1;                      // seconds between updates
rotation lastYaw = ZERO_ROTATION;
rotation lastPitch = ZERO_ROTATION;

default
{
    state_entry()
    {
        LinkTurret = LinkByName("Turret");
        LinkBarrel = LinkByName("Barrel");
        llSitTarget(<0.0, 0.0, 0.5>, ZERO_ROTATION);
    }

    changed(integer change)
    {
        if (change & CHANGED_LINK)
        {
            key avatar = llAvatarOnSitTarget();
            if (avatar) llRequestPermissions(avatar, PERMISSION_TRACK_CAMERA);
            else llSetTimerEvent(0.0);
        }
    }

    run_time_permissions(integer permissions)
    {
        if (permissions & PERMISSION_TRACK_CAMERA) llSetTimerEvent(interval);
    }

    timer()
    {
        // The camera's rotation relative to the vehicle.
        rotation localRot = llGetCameraRot() / llGetRootRotation();
        rotation targetYaw = ConstrainYaw(localRot, hardpointNormal);
        // Remove the yaw; what remains is the barrel's tilt.
        rotation targetPitch = ConstrainPitch(localRot / targetYaw, hardpointNormal);

        rotation yaw = StepRotation(lastYaw, targetYaw, turretTraverseSpeed * interval);
        rotation pitch = StepRotation(lastPitch, targetPitch, turretPitchSpeed * interval);
        lastYaw = yaw;
        lastPitch = pitch;

        llSetLinkPrimitiveParamsFast(LINK_THIS, [
            PRIM_LINK_TARGET, LinkTurret,
            PRIM_ROT_LOCAL, yaw,
            PRIM_LINK_TARGET, LinkBarrel,
            PRIM_ROT_LOCAL, pitch * yaw,
            PRIM_POS_LOCAL, turretBase + ((<0.6, 0.0, 0.0> * pitch) * yaw)
        ]);
    }
}
```

The barrel's rotation is `pitch * yaw`: tilt first, then turn with the ring. Its position is
**turretBase** plus a 0.6 m offset rotated the same way.

## Values to tune

- **turretTraverseSpeed** and **turretPitchSpeed**: the ring's and barrel's top speeds in radians
  per second. `StepRotation` moves each by at most its speed times **interval** per update.
- **interval**: the seconds between updates.
- **turretBase**: the barrel's pivot, in the root's frame. The 0.6 m offset `<0.6, 0.0, 0.0>` in
  the `PRIM_POS_LOCAL` line is where the barrel sits relative to that pivot before it is rotated.
- **hardpointNormal**: the turret's up axis, in the root's frame; it must be unit length.
- The sit target, `llSitTarget(<0.0, 0.0, 0.5>, ZERO_ROTATION)` in `state_entry`: where the avatar
  sits on the root prim.

## How to use it

Sit on the root prim. The script asks the seated avatar for
[`PERMISSION_TRACK_CAMERA`](/constants/PERMISSION_TRACK_CAMERA/) and, once it is granted, starts
the timer. From then on the ring and barrel follow that avatar's camera
([`llGetCameraRot`](/functions/llGetCameraRot/)), relative to the vehicle: move the camera to aim.
Standing up stops the timer.

The script only aims. For firing, see the [Combat](/features/combat/) feature.

## Notes

- The turret and barrel must be child prims, and the script must be in the root prim with the sit
  target.
- `ConstrainYaw` and `ConstrainPitch` have no answer when the camera looks exactly along
  **hardpointNormal** (straight up or down); see the notes in
  [Constraining rotations](/guides/constraining-rotations/#notes-and-limits).

## Source and licence

From [`rotations.lsl`](https://github.com/Martin-Pitt/NexiiLSL/blob/996fbd7/rotations.lsl) in
[NexiiLSL](https://github.com/Martin-Pitt/NexiiLSL) by Martin Pitt, © 2026, under the
[MIT licence](https://github.com/Martin-Pitt/NexiiLSL/blob/996fbd7/LICENSE), at commit `996fbd7`.

Changes from the original:

- **The turret example called `stepRotation`, which was not defined.** It now uses
  `StepRotation`, written for [Constraining rotations](/guides/constraining-rotations/), and is a
  complete script with the sitting, permission and timer code it needs. `frameRate` is renamed
  `interval`, because it is the time between updates.
- The older, shorter turret example from the same file is left out; the updated one supersedes it.

---

From lsl.dev: https://lsl.dev/guides/constraining-rotations/turret/
