Skip to content

Coordinate frames

A vector in LSL is three numbers, and nothing in the type says what they are measured from or along which axes. <1.0, 0.0, 0.0> can mean “one metre east of the region’s corner”, “one metre along the root prim’s forward axis”, “a little into the screen” or “forward, whichever way this object faces”. Most frame bugs come from mixing two of these. So for every position, rotation or direction you handle, ask one question: which frame is it in? Then convert before you combine it with anything from another frame.

For a one-page summary of which function uses which frame, see Coordinate frames reference.

Region and global coordinatesA grid of regions seen from above at an angle. One region is outlined, with its +X (east), +Y (north) and +Z (up) axes at its south-west corner and a ruler along X from 0 to 256 metres. A dashed line from the grid origin to that corner is labelled llGetRegionCorner(). An object inside the region is at llGetPos() <180, 150, 24>; its global position is the region corner plus llGetPos().+Y north+Z up0m64m128m192m256mgrid <0,0,0>llGetRegionCorner()llGetPos() <180, 150, 24>global = corner + llGetPos()this regionneighbour
Region coordinates run 0–256 m from the region's south-west corner. Global coordinates add llGetRegionCorner(), the corner's position on the grid.

The region frame is the one most functions use. Its origin is the region’s south-west corner at height 0, +X points east, +Y north and +Z up, in metres; a standard region runs 0 to 256 on X and Y. These are all region coordinates:

Velocities have a frame too. llGetVel is in region axes; to get the velocity along the object’s own axes (forward speed, sideways slip), rotate it back:

vector localVel = llGetVel() / llGetRot();
float forwardSpeed = localVel.x;

Global coordinates place a point on the whole grid. llGetRegionCorner returns the global position of the region’s south-west corner in metres (with z 0), so a global position is the corner plus the region position. Divide the corner by 256 to get the region’s grid offset.

vector corner = llGetRegionCorner();
vector globalPos = corner + llGetPos();
// The region's grid offset, in regions
integer gridX = (integer)(corner.x / 256.0);
integer gridY = (integer)(corner.y / 256.0);
Rotating a vector with * rot and / rotTwo top-down panels of an object facing 35 degrees. Left: the local vector <2,0,0> times the object's rotation becomes a vector pointing along the object's forward axis in region axes. Right: a region-space offset target minus llGetPos(), divided by the rotation, becomes <2,0,0>, dead ahead for the object.local → region: v * rot<2,0,0> local<2,0,0> * rotahead, in region axesobject faces 35°region → local: v / rottarget − llGetPos()… / rot= <2,0,0> aheadobject faces 35°
* rot turns a vector from the object's frame into region axes; / rot turns it back.

Two operators move a direction between frames:

  • v * rot takes a vector measured along rot’s axes and gives it in the outer frame’s axes (local to region)
  • v / rot does the inverse (region to local)

So <1.0, 0.0, 0.0> * llGetRot() is the object’s forward direction in region axes (the same as llRot2Fwd), and (target - llGetPos()) / llGetRot() is where a target lies from the object’s point of view: positive x in front, positive y to its left, positive z above.

Rotations combine left to right: a * b applies a first, then b. A rotation inside a frame times the frame’s rotation gives the rotation in the outer frame, which is why every conversion below has the inner thing on the left.

Positions need a translation as well as a rotation: rotate the offset, then add the frame’s origin. To rez something forward and a little above a prim, turn the offset by the prim’s rotation and add its position:

vector relativeOffset = <2.0, 0.0, 1.0>; // forward and a little above, in the prim's axes
vector myPos = llGetPos();
rotation myRot = llGetRot();
vector rezPos = myPos + (relativeOffset * myRot);
llRezObjectWithParams("Object", [REZ_POS, rezPos, FALSE, FALSE]); // region coordinates
Root and child primsA root prim above the region grid, rotated 35 degrees around Z, with its own grid and axes. A child prim sits at llGetLocalPos() <2.6, 1.2, 1.0>, drawn as dashed steps along the root's red X, green Y and blue Z axes. A ruler in metres runs along the root's X axis. A dashed line from the region origin to the root is labelled llGetRootPosition().region <0,0,0>0m1m2m3mllGetRootPosition()rootchildllGetLocalPos() <2.6, 1.2, 1.0>
llGetLocalPos() is measured along the root's own axes (the dashed steps), not the region's. The faint lines are the axes running through each frame.

In a linkset the root prim lives in the region frame, and every child prim’s position and rotation are measured in the root’s frame: an offset from the root’s position, along the root’s axes. llGetLocalPos, llGetLocalRot, llSetLocalRot, PRIM_POS_LOCAL and PRIM_ROT_LOCAL all use this frame. On the root itself, llGetLocalPos returns the region position (or, when attached, the offset from the attach point).

To convert between a child’s local values and region values:

// Region position and rotation of a child, from its local values
vector regionPos = llGetRootPosition() + localPos * llGetRootRotation();
rotation regionRot = localRot * llGetRootRotation();
// And back
vector localPos = (regionPos - llGetRootPosition()) / llGetRootRotation();
rotation localRot = regionRot / llGetRootRotation();

Not every function uses the same frame for getting and setting, or for root and child:

Seated avatars are links too, numbered after the last prim (see Working with link numbers), and setting their PRIM_POSITION is relative to the root like any other child.

These assume an unattached object and work from any prim in it.

// Root-relative (local) position to region coordinates
vector LocalToRegion(vector local)
{
return llGetRootPosition() + local * llGetRootRotation();
}
// Region coordinates to a root-relative (local) position
vector RegionToLocal(vector region)
{
return (region - llGetRootPosition()) / llGetRootRotation();
}
// Root-relative (local) rotation to a region rotation
rotation LocalRotToRegion(rotation local)
{
return local * llGetRootRotation();
}
// Region rotation to a root-relative (local) rotation
rotation RegionRotToLocal(rotation region)
{
return region / llGetRootRotation();
}
// Region direction (no translation) to the root's axes
vector RegionDirToLocal(vector dir)
{
return dir / llGetRootRotation();
}

For example, a child can move itself to a region position with llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_POS_LOCAL, RegionToLocal(target)]).

Attachments: what the simulator seesA side view of the simulator's physics shape of a standing avatar, a faceted hull narrow at the feet and widest at the shoulders, with the avatar's position at its centre and its axes: +X forward, +Z up. To the simulator, the avatar's position is every attachment's attach point: the attachment root is that position plus its llGetLocalPos() offset, and llGetPos() and llGetRot() report the avatar. A faint waving figure is the animated pose the viewer renders; a dashed copy of the attachment sits on its raised hand, the same offset from the real attach point, which only the viewer knows.physics shape for an agent height (z of llGetAgentSize) of 1.189 mwhere the viewer draws it<0.4, 0, 0> along the hand's +X(the thumb), viewer onlyATTACH_LHAND, animated+Y fingers, +Z into the screenattachment root,as the simulator sees itavatar position + <0.4, 0, 0>along the avatar's +X+X forward+Z upavatar position= the simulator's attach pointllGetPos(), llGetRot()
The simulator has no skeleton: to it, every attach point is the avatar's position, and llGetPos() and llGetRot() report the avatar. llGetLocalPos() is the attachment's offset from its attach point; the viewer applies the same offset from the attach point on the animated skeleton.

The simulator has no skeleton. It knows an avatar’s position, rotation and physics shape (the hull in the diagram), and to the simulator every attach point is the avatar’s position, whichever one an attachment uses. The attachment’s offset and rotation from its attach point are real and stored with it (llGetLocalPos and llGetLocalRot report them), but where that attach point is, on the animated skeleton, only viewers know; that is how a hand attachment ends up in the hand. So a script in an attachment never sees where the attachment is drawn: llGetPos reports the avatar’s position, and llSensor in the root looks the way the avatar faces, wherever the object is attached.

In an attachment:

HUD attachment frameThe viewer window as a rectangle with the eight HUD attach points: TOP_LEFT, TOP_CENTER, TOP_RIGHT, CENTER_1 and CENTER_2 in the middle, BOTTOM_LEFT, BOTTOM and BOTTOM_RIGHT. At the centre, +Y points left, +Z points up and +X points into the screen. The window is about 1 unit tall, from Z −0.5 to +0.5.viewer windowTOP_LEFTTOP_CENTERTOP_RIGHTCENTER_1 / CENTER_2BOTTOM_LEFTBOTTOMBOTTOM_RIGHT+Y left+Z up+X into the screenZ +0.5Z 0Z −0.5
Each HUD attach point is its own origin in the same frame. The camera looks along +X, so +X points into the screen; the window is about 1 unit tall and its width depends on the aspect ratio.

A HUD attachment lives on the viewer window, not in the region. Each HUD attach point is an origin on the screen, and they all share the same axes: +Y to the left, +Z up, and +X into the screen (the HUD camera looks along +X). The window is about one unit tall (Z from −0.5 to +0.5), and its width depends on the window’s aspect ratio, so anything placed near the sides can end up off screen on a narrower window.

llSetPos on a HUD’s root sets its offset from the attach point in these units. To move a HUD button 0.1 up and 0.2 to the right:

vector pos = llGetLocalPos();
llSetPos(pos + <0.0, -0.2, 0.1>);

llDetectedTouchPos on a HUD isn’t in region coordinates; the llDetectedTouchPos page has a GetRealTouchPos helper that adds the attach point’s offset for one aspect ratio. For positions within one face, the touch’s ST coordinates (below) are usually simpler.

The local flag of physics functionsTwo top-down panels of an object turned 35 degrees. With llApplyImpulse(<1,0,0>, FALSE) the push points due east along the region's X axis. With TRUE it points along the object's own +X.FALSE: region axesdue eastllApplyImpulse(<1,0,0>, FALSE)TRUE: object axesalong object +XllApplyImpulse(<1,0,0>, TRUE)
With FALSE the vector is in region axes, whichever way the object faces. With TRUE it is in the object's own axes.

Physics functions that take a direction also take a local flag: llApplyImpulse, llSetForce, llSetTorque, llSetAngularVelocity and llApplyRotationalImpulse. With FALSE the vector is in region axes; with TRUE it is along the object’s own axes, so <1.0, 0.0, 0.0> means “forward” whichever way the object faces.

// Push east, whichever way the object faces
llApplyImpulse(<5.0, 0.0, 0.0>, FALSE);
// Push forward along the object's own X axis
llApplyImpulse(<5.0, 0.0, 0.0>, TRUE);

Other functions have a fixed frame:

  • llTargetOmega on the root spins around a region axis (an attachment’s around the attachment’s axis); on a child it spins around the child’s own axis. On a non-physical object or a child it’s only a viewer-side effect
  • llRotLookAt takes a region rotation. To point the object’s +Y at a target, its page uses llRotBetween(<0.0, 1.0, 0.0>, llVecNorm(target - llGetPos()))
  • llSitTarget’s offset and rotation are relative to the prim the script is in
  • llSetLinkCamera’s eye and focus offsets are local to the prim

llGetCameraPos and llGetCameraRot are in the region frame and need PERMISSION_TRACK_CAMERA. The camera looks along its rotation’s +X. To find where the camera is pointing relative to a vehicle, rotate it into the vehicle’s frame:

rotation camRot = llGetCameraRot();
rotation camInVehicle = camRot / llGetRootRotation();

llSetCameraParams mixes two frames. With CAMERA_POSITION_LOCKED and CAMERA_FOCUS_LOCKED, CAMERA_POSITION and CAMERA_FOCUS are region positions in the region the avatar is in, not offsets from the avatar or the object. The viewer turns them into a fixed point in the world when they arrive, so a locked camera stays put while the avatar or vehicle moves. Near a region border, remember that they are measured from the avatar’s region, which may not be the scripted object’s. The follow-camera rules, such as CAMERA_DISTANCE, CAMERA_PITCH and CAMERA_FOCUS_OFFSET, are relative to the avatar instead: a focus offset of ZERO_VECTOR looks at the avatar’s own llGetPos(). To lock the camera behind and above an object, convert the offset to region coordinates first:

vector pos = llGetPos();
rotation rot = llGetRot();
llSetCameraParams([
CAMERA_ACTIVE, TRUE,
CAMERA_POSITION_LOCKED, TRUE,
CAMERA_FOCUS_LOCKED, TRUE,
CAMERA_POSITION, pos + (<-4.0, 0.0, 2.0> * rot),
CAMERA_FOCUS, pos
]);
Touch ST and UV coordinatesTwo copies of one face. Left, llDetectedTouchST: S runs right and T up from 0,0 at the bottom-left corner to 1,1 at the top-right, unaffected by texture settings; the touch is at <0.68, 0.65>. Right, llDetectedTouchUV: the texture's tiles, repeated twice and rotated 20 degrees, are drawn over the face and labelled by whole UV, so the same touch reads in texture space.llDetectedTouchST+S+T0,01,1ST = <0.68, 0.65>unaffected by texture settingsllDetectedTouchUV-2,-2-2,-1-2,0-2,1-2,2-2,3-1,-2-1,-1-1,0-1,1-1,2-1,30,-20,-10,00,10,20,31,-21,-11,01,11,21,32,-22,-12,02,12,22,33,-23,-13,03,13,23,3UV in texture tilestiles labelled by whole UV · repeats 2, rotation 20°
ST is fixed to the face, 0–1 from the bottom-left corner. UV is in the texture's space, so repeats and rotation change it.

A touch tells you where on a face it landed in several frames:

Use ST for buttons and sliders laid out on a face, and UV to find the pixel of the texture that was touched. On a glTF mesh the vertical coordinate is flipped for both.

touch_start(integer total)
{
vector st = llDetectedTouchST(0);
// Left half or right half of the face
if (st.x < 0.5) llOwnerSay("Left");
else llOwnerSay("Right");
}
Blinn-Phong and PBR texture transformsTwo texture squares seen from above. Blinn-Phong: the origin 0,0 is at the bottom-left, +U right and +V up; the pivot is the centre, positive rotation is counter-clockwise, offsets are from the centre, and the order is rotate, then scale, then offset. PBR (glTF): the origin is at the top-left, +U right and +V down; the pivot is the top-left corner, positive rotation is clockwise, the V offset has the opposite sign, and the order is scale, then rotate, then offset.Blinn-Phong: PRIM_TEXTURE+U+V up0,0+rot counter-clockwisepivot: centreoffset from the centreorderrotatescale→offset→PBR (glTF): PRIM_GLTF_*+U+V down0,0+rot clockwisepivot: corneroffset V: −Blinn-Phong valueorderscalerotate→offset→
Rotation and V offset have opposite signs, the pivot moves from the centre to the top-left corner, and scale and rotation swap order, so a rotated, non-uniform repeat only converts exactly at multiples of 90°.

Faces have two sets of texture transforms, in different conventions:

  • Blinn-Phong (PRIM_TEXTURE, PRIM_NORMAL, PRIM_SPECULAR) works around the face’s centre: the viewer subtracts 0.5, rotates, scales by the repeats, adds the offset and adds 0.5 back. V points up and offsets are measured from the centre. Repeats are per face with default mapping, or per half metre with planar mapping. See Texture coordinates for atlas maths
  • PBR (PRIM_GLTF_BASE_COLOR and the other PRIM_GLTF_* parameters) uses glTF’s KHR_texture_transform: the origin and pivot are the top-left corner, V points down, and the order is scale, then rotate, then offset. Texture animation (llSetTextureAnim) is applied before this, in the SL convention

So the same numbers give different results in the two. The rotation has the opposite sign, the V offset has the opposite sign, and the pivot moves from the centre to a corner. Because Blinn-Phong scales after rotating and glTF scales before, a non-uniform repeat on a rotated texture shears differently: the conversion is exact only at multiples of 90°, and Linden Lab’s own converter calls it close enough at 45°.

These functions follow the viewer’s conversion (convertTextureTransformToPBR and convertPBRTransformToTexture in llgltfmaterial.cpp). Rotations are in radians. Offsets are periodic, so the result is wrapped into −1 to 1, the range PRIM_GLTF_* documents.

// Blinn-Phong repeats, offsets and rotation to PRIM_GLTF_* values: [repeats, offsets, rotation]
list TextureToPBR(vector repeats, vector offsets, float rot)
{
float c = llCos(rot);
float s = llSin(rot);
vector scale = <(repeats.x * c * c) + (repeats.y * s * s),
(repeats.x * s * s) + (repeats.y * c * c), 0.0>;
float ps = -0.5 * scale.x;
float pt = -0.5 * scale.y;
float rotS = (ps * c) - (pt * s);
float rotT = (ps * s) + (pt * c);
float ox = rotS + 0.5 + offsets.x;
float oy = rotT + 0.5 - offsets.y;
// Wrap the offsets into (-1, 1)
ox -= (integer)ox;
oy -= (integer)oy;
return [scale, <ox, oy, 0.0>, -rot];
}
// PRIM_GLTF_* repeats, offsets and rotation to Blinn-Phong values: [repeats, offsets, rotation]
list PBRToTexture(vector repeats, vector offsets, float rot)
{
float texRot = -rot;
float c = llCos(texRot);
float s = llSin(texRot);
float denom = (c * c) - (s * s);
vector scale;
if (llFabs(denom) < 0.0001)
{
// At 45°, the two repeats can't be separated
float avg = (repeats.x + repeats.y) * 0.5;
scale = <avg, avg, 0.0>;
}
else
{
scale = <((repeats.x * c * c) - (repeats.y * s * s)) / denom,
((repeats.y * c * c) - (repeats.x * s * s)) / denom, 0.0>;
}
float ps = -0.5 * repeats.x;
float pt = -0.5 * repeats.y;
float rotS = (ps * c) - (pt * s);
float rotT = (ps * s) + (pt * c);
float ox = offsets.x - rotS - 0.5;
float oy = -(offsets.y - rotT - 0.5);
ox -= (integer)ox;
oy -= (integer)oy;
return [scale, <ox, oy, 0.0>, texRot];
}

To copy a face’s Blinn-Phong transform onto its PBR base colour:

list tex = llGetLinkPrimitiveParams(LINK_THIS, [PRIM_TEXTURE, 0]);
list pbr = TextureToPBR(llList2Vector(tex, 1), llList2Vector(tex, 2), llList2Float(tex, 3));
// Keep the other base colour values; replace repeats, offsets and rotation
list base = llGetLinkPrimitiveParams(LINK_THIS, [PRIM_GLTF_BASE_COLOR, 0]);
llSetLinkPrimitiveParamsFast(LINK_THIS,
[PRIM_GLTF_BASE_COLOR, 0] + llListReplaceList(base, pbr, 1, 3));

Negative repeats (a flipped texture) keep their sign at 0° rotation. At other angles the formula mixes the two repeats through cos² and sin², so a flip on one axis doesn’t convert cleanly; flip in whichever system you are working in rather than converting a flipped transform.

Tank: hull, turret and barrel framesA tank on a slope. The hull is the root prim, its rotation llGetRootRotation(). The turret sits on the hull at turretBase and is turned (yaw) around the hull's up axis; dashed lines show hull forward and turret forward. The barrel pivots at turretBase and is tilted (pitch) and then turned with the turret. The muzzle at the barrel's end is where a projectile is rezzed.hull forwardturret forwardhull = rootllGetRootRotation()turretyaw around the hull's upturretBasebarrelpitch, then yawmuzzle: rez pointregion
The hull is the root, on a slope. The turret yaws around the hull's up axis; the barrel pitches around the turret's left axis, then turns with it.

A tank uses almost every frame on this page. The hull is the root, sitting on a slope in the region. The turret turns around the hull’s up axis, and the barrel tilts up and down, then turns with the turret.

Linksets are flat: every child is relative to the root, never to another child. The barrel is logically mounted on the turret, but LSL doesn’t know that, so the script multiplies the turret’s rotation in by hand:

  • Turret: PRIM_ROT_LOCAL is yaw
  • Barrel: PRIM_ROT_LOCAL is pitch * yaw (tilt, then turn with the ring), and PRIM_POS_LOCAL is turretBase + ((offset * pitch) * yaw), where turretBase is the turret’s pivot in the hull’s frame and offset is where the barrel sits from that pivot
Building the barrel's transform step by stepFour small views of the same tank. 1: the barrel offset <0.95,0,0> times pitch tilts it around the pivot. 2: times yaw turns it with the turret. 3: plus turretBase places it on the hull; this is PRIM_POS_LOCAL. 4: times llGetRootRotation() plus llGetRootPosition() puts it in the region, with the hull on its slope.1. tiltoffset * pitch2. turn… * yaw→3. on the hullturretBase + …→4. in the region… * rootRot + rootPos→
Read LSL rotations left to right, innermost first: offset * pitch, then * yaw, then + turretBase gives PRIM_POS_LOCAL; * llGetRootRotation() + llGetRootPosition() gives the region position.

Outward, to the region. To fire, you need the muzzle’s region position and direction, for llRezObjectWithParams or llCastRay. Keep going left to right: add the hull’s rotation and position.

rotation hullRot = llGetRootRotation();
vector muzzleLocal = turretBase + ((<barrelLength, 0.0, 0.0> * pitch) * yaw);
vector muzzle = llGetRootPosition() + (muzzleLocal * hullRot);
rotation barrelRot = (pitch * yaw) * hullRot;
vector fireDir = llRot2Fwd(barrelRot);
list hit = llCastRay(muzzle, muzzle + (fireDir * 100.0), []);
llRezObjectWithParams("Shell", [
REZ_POS, muzzle, FALSE, FALSE, // region coordinates, centre of the shell
REZ_ROT, barrelRot, FALSE, // region rotation
REZ_VEL, fireDir * 40.0, FALSE, TRUE, // region axes, plus the tank's own velocity
REZ_PARAM, 1
]);

Every rez rule says which frame its value is in: REZ_POS and REZ_ROT take a relative flag, and REZ_VEL a local flag. The muzzle, rotation and direction above are already in region space, so all three are FALSE. REZ_VEL’s last flag, inherit, adds the rezzer’s own velocity, so the shell leaves a moving tank at the right speed.

Inward, for aiming. To aim at a target in the region, go the other way: bring the target into the hull’s frame, then split the direction into yaw and pitch with ConstrainYaw and ConstrainPitch from Constraining rotations. In the hull’s frame its up axis is just <0.0, 0.0, 1.0>.

vector dir = (target - llGetRootPosition()) / llGetRootRotation();
rotation aim = llRotBetween(<1.0, 0.0, 0.0>, llVecNorm(dir));
rotation yaw = ConstrainYaw(aim, <0.0, 0.0, 1.0>);
rotation pitch = ConstrainPitch(aim / yaw, <0.0, 0.0, 1.0>);

Turret rotation has a complete script that follows a seated avatar’s camera this way.

  • These conversions assume an unattached object. In an attachment, llGetPos, llGetRot and llGetRootRotation describe the avatar (see Attachments), and animations aren’t visible to scripts at all
  • The helpers read the root’s position and rotation each time; if the object is moving, values computed a moment apart describe slightly different frames
  • The texture transform conversion is approximate for rotated, non-uniform repeats, and untested in-world (see the caveat above)