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
Section titled “Region and global”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:
llGetPosin an unattached object, andllSetRegionPosllGetRootPositionllDetectedPosandllDetectedTouchPosllGetCameraPos, andCAMERA_POSITIONandCAMERA_FOCUSinllSetCameraParamsllCastRaystart and end points and hit positionsOBJECT_POSfromllGetObjectDetailsPRIM_POSITIONwhen you get it, for any prim
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 regionsinteger gridX = (integer)(corner.x / 256.0);integer gridY = (integer)(corner.y / 256.0);Rotating vectors
Section titled “Rotating vectors”Two operators move a direction between frames:
v * rottakes a vector measured alongrot’s axes and gives it in the outer frame’s axes (local to region)v / rotdoes 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 axesvector myPos = llGetPos();rotation myRot = llGetRot();vector rezPos = myPos + (relativeOffset * myRot);llRezObjectWithParams("Object", [REZ_POS, rezPos, FALSE, FALSE]); // region coordinatesLinksets: root and child
Section titled “Linksets: root and child”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 valuesvector regionPos = llGetRootPosition() + localPos * llGetRootRotation();rotation regionRot = localRot * llGetRootRotation();
// And backvector localPos = (regionPos - llGetRootPosition()) / llGetRootRotation();rotation localRot = regionRot / llGetRootRotation();Functions that mix frames
Section titled “Functions that mix frames”Not every function uses the same frame for getting and setting, or for root and child:
llSetPosis in region coordinates on an unattached root, relative to the attach point on an attachment’s root, and local to the root on a childllGetRoton a child returns its global (region) rotation, butllSetRoton a child is offset by the root’s rotation. Its page’s workaround is to divide by the root rotation (to set a rotation relative to the root), or divide by it twice (to set a global rotation). UsellSetLocalRotorPRIM_ROT_LOCALinstead:llSetLocalRot(globalRot / llGetRootRotation())sets a global rotation on a childPRIM_POSITIONget always returns region coordinates, andPRIM_ROTATIONget always the global rotation, so read local values withPRIM_POS_LOCALandPRIM_ROT_LOCALllGetGeometricCenteris relative to the root;llGetCenterOfMasscalled from a child returns that child’s centre of mass, still in region coordinates
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.
Helpers
Section titled “Helpers”These assume an unattached object and work from any prim in it.
// Root-relative (local) position to region coordinatesvector LocalToRegion(vector local){ return llGetRootPosition() + local * llGetRootRotation();}
// Region coordinates to a root-relative (local) positionvector RegionToLocal(vector region){ return (region - llGetRootPosition()) / llGetRootRotation();}
// Root-relative (local) rotation to a region rotationrotation LocalRotToRegion(rotation local){ return local * llGetRootRotation();}
// Region rotation to a root-relative (local) rotationrotation RegionRotToLocal(rotation region){ return region / llGetRootRotation();}
// Region direction (no translation) to the root's axesvector 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
Section titled “Attachments”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:
llGetPosin the root returns the wearer’s region position, not the attachment’sllGetPosin a child prim is relative to the avatar’s position and rotation plus the root’s offset, which matches what you see only atATTACH_AVATAR_CENTERwith zero offset andZERO_ROTATIONllGetLocalPosin the root is the offset from the attach pointllGetRotin the root reports the avatar’s rotation; usellGetLocalRotfor the attachment’s own rotation on its attach pointllGetRootRotationreturns the avatar’s region rotation. Its page givesllList2Rot(llGetLinkPrimitiveParams(!!llGetLinkNumber(), [PRIM_ROT_LOCAL]), 0)as working in all cases for the attachment’s local rotation- The avatar’s rotation is an accurate facing only when the avatar is seated or in mouselook; otherwise it is a rough direction
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.
Object axes
Section titled “Object 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 facesllApplyImpulse(<5.0, 0.0, 0.0>, FALSE);// Push forward along the object's own X axisllApplyImpulse(<5.0, 0.0, 0.0>, TRUE);Other functions have a fixed frame:
llTargetOmegaon 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 effectllRotLookAttakes a region rotation. To point the object’s +Y at a target, its page usesllRotBetween(<0.0, 1.0, 0.0>, llVecNorm(target - llGetPos()))llSitTarget’s offset and rotation are relative to the prim the script is inllSetLinkCamera’s eye and focus offsets are local to the prim
Camera
Section titled “Camera”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]);Surfaces: touch coordinates
Section titled “Surfaces: touch coordinates”A touch tells you where on a face it landed in several frames:
llDetectedTouchPos: the point, in region coordinatesllDetectedTouchNormalandllDetectedTouchBinormal: the surface normal and the binormal (the tangent along +T), in region axesllDetectedTouchST: the position on the face, usually 0 to 1 from the bottom-left corner, whatever texture is on itllDetectedTouchUV: the position in the texture’s space, which changes with repeats, offsets and rotation
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");}Texture transforms: Blinn-Phong and PBR
Section titled “Texture transforms: Blinn-Phong and PBR”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_COLORand the otherPRIM_GLTF_*parameters) uses glTF’sKHR_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 rotationlist 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.
Putting it together: a tank
Section titled “Putting it together: a tank”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_LOCALisyaw - Barrel:
PRIM_ROT_LOCALispitch * yaw(tilt, then turn with the ring), andPRIM_POS_LOCAListurretBase + ((offset * pitch) * yaw), whereturretBaseis the turret’s pivot in the hull’s frame andoffsetis where the barrel sits from that pivot
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.
Notes and limits
Section titled “Notes and limits”- These conversions assume an unattached object. In an attachment,
llGetPos,llGetRotandllGetRootRotationdescribe 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)