Skip to content
lsl.devlsl.devLSL Dev

Turret rotation

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.

  • 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 for how that works and how to look the links up again when the linkset changes.

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

37 collapsed lines
// 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.

  • 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.

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

The script only aims. For firing, see the Combat feature.

  • 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.

From rotations.lsl in NexiiLSL by Martin Pitt, © 2026, under the MIT licence, at commit 996fbd7.

Changes from the original:

  • The turret example called stepRotation, which was not defined. It now uses StepRotation, written for 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.