Skip to Content
ScriptingVectors & Quaternion Rotation

Lua vectors and quaternion rotation

Use a vector for a position or direction, a number for a distance, and a quaternion for an orientation. Mixing those roles can produce valid Lua that moves an object incorrectly. These are CfxLua types, not ordinary Lua tables or JavaScript objects.

For threads and timers, use Lua scheduling. This guide concentrates on the math helpers missing from that workflow.

Pick the type before the operation

Type or helperInput and resultPractical distinction
vector2(x, y)Two componentsUseful for a horizontal displacement.
vector3(x, y, z)Three componentsPositions and directions have the same type but different meanings.
vector4(x, y, z, w)Four componentsA convention such as storing heading in w belongs to your application, not the type.
#vVector to numberLength, not component count.
norm(v)Vector or quaternion to normalized valueCheck for a near-zero length before normalizing a direction.
dot(a, b)Matching vector types, or quaternions, to numberVector multiplication is not a replacement for a dot product.
cross(a, b)Two vector3 values to vector3Reversing the arguments reverses the perpendicular direction. The vector2 overload returns a number.
quat(w, x, y, z)Four numbers to quaternionThe scalar component comes first.
inv(q)Quaternion to inverse quaternionThis is not the vector-negation helper.
slerp(a, b, t)Matching supported vectors or quaternionsFor a blend between endpoints, keep t between zero and one.

The official vector3 , vector4 , dot  and cross  references define the supported operations. Do not assume that an operation valid for one dimension is valid for every dimension.

Run a deterministic distance check

Create resources/[local]/doc_math/fxmanifest.lua:

fx_version 'cerulean' game 'gta5' client_script 'client.lua'

Create client.lua. The example uses fixed coordinates, so it does not depend on where a player happens to stand:

local function directionBetween(origin, target) local delta = target - origin local distance = #delta if distance < 0.000001 then return nil, distance end return delta / distance, distance end RegisterCommand('docmath', function() local origin = vector3(10.0, 20.0, 30.0) local target = vector3(13.0, 24.0, 30.0) local direction, distance = directionBetween(origin, target) local sameDirection, sameDistance = directionBetween(origin, origin) assert(math.abs(distance - 5.0) < 0.000001) assert(direction and math.abs(#direction - 1.0) < 0.000001) assert(sameDirection == nil and sameDistance == 0.0) local horizontal = vector2(target.x - origin.x, target.y - origin.y) print(('3D distance %.1f; horizontal distance %.1f'):format(distance, #horizontal)) print(('Unit direction %.1f, %.1f, %.1f'):format(direction.x, direction.y, direction.z)) end, false)

Run refresh and ensure doc_math in the server console, then docmath in the client’s F8 console. Expected output is distance 5.0 in both measurements and direction 0.6, 0.8, 0.0. For a second test, change target.z to 42.0 and the distance assertion from 5.0 to 13.0. The 3D distance becomes 13.0, while the horizontal distance stays 5.0.

This distinguishes two different interaction rules. A flat map marker might use horizontal distance; a usable object on another floor usually needs a height-aware check. Neither client-side calculation authorizes a server reward. Perform privileged checks against trusted server state.

Normalize deliberately

Subtract positions to obtain a displacement. Normalize that displacement only when its magnitude is nonzero. The guard above returns nil when there is no meaningful direction; callers must decide whether to stop, keep the previous direction, or use an explicit fallback.

For an angle between directions, normalize both nonzero vectors, calculate dot(a, b), clamp the result to [-1, 1], then use math.acos. The clamp prevents floating-point rounding from making the inverse cosine undefined. Convert radians with math.deg only when the receiving API expects degrees. See norm  for the runtime helper.

Keep quaternion ordering explicit

The raw constructor is quat(w, x, y, z). The angle-axis overload is quat(angle, axis), with the angle in degrees. Do not reorder a quaternion by treating it as an ordinary vector4. For an external API, inspect that API’s component order separately.

This optional block can run in the same client script:

local axis = vector3(0.0, 0.0, 1.0) local initial = quat(1.0, 0.0, 0.0, 0.0) local target = quat(60.0, axis) local halfway = slerp(initial, target, 0.5) local undo = inv(target) print('Quaternion blend:', halfway) print('Inverse rotation:', undo)

Use normalized orientations and a nonzero, normalized axis in your own interpolation code. This block calculates values; it does not animate an entity or establish which rotation convention a native uses. The contracts are documented in quat , slerp  and inv .

Cross API boundaries safely

When a native needs separate coordinates, pass position.x, position.y and position.z explicitly. In particular, the experimental OAL mode does not support automatic vector unpacking. Check the manifest runtime flags before blaming the arithmetic.

For JSON or another language, define an explicit object such as { x = position.x, y = position.y, z = position.z }. Validate and reconstruct the vector at the receiving boundary instead of assuming a serialized value retains CfxLua’s type.

Verification boundary

Run the fixed-coordinate command, the changed-height case and the coincident-point case in the intended client track. Stop doc_math and remove its ensure line afterward. These examples have not been executed inside FiveM as part of this documentation change; a website build cannot certify native vector handling or entity rotation.