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 helper | Input and result | Practical distinction |
|---|---|---|
vector2(x, y) | Two components | Useful for a horizontal displacement. |
vector3(x, y, z) | Three components | Positions and directions have the same type but different meanings. |
vector4(x, y, z, w) | Four components | A convention such as storing heading in w belongs to your application, not the type. |
#v | Vector to number | Length, not component count. |
norm(v) | Vector or quaternion to normalized value | Check for a near-zero length before normalizing a direction. |
dot(a, b) | Matching vector types, or quaternions, to number | Vector multiplication is not a replacement for a dot product. |
cross(a, b) | Two vector3 values to vector3 | Reversing the arguments reverses the perpendicular direction. The vector2 overload returns a number. |
quat(w, x, y, z) | Four numbers to quaternion | The scalar component comes first. |
inv(q) | Quaternion to inverse quaternion | This is not the vector-negation helper. |
slerp(a, b, t) | Matching supported vectors or quaternions | For 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.