EngineIntermediate

Coordinates and transforms

Understand Airogel’s axes, quaternions, hierarchy, and local versus world transforms.

World axes

  • +X: right
  • +Y: up
  • +Z: forward

The axes are right-handed. +X is to the right only when you look toward -Z; an object that faces +Z has +X on its left. The starter character controller relies on this: pressing left sets moveX = 1 and pressing right sets moveX = -1.

Characters display a yellow arrow above the head pointing along their local +Z. It is drawn while the game runs as well, including in a published game; turn off Show Direction Arrow in the character's inspector to hide it. To place a camera behind a character, use a negative local Z offset.

Position and rotation

getPosition() and setPosition(x, y, z) use world coordinates. Rotations are quaternions:

local angle = math.pi / 2
gameObject.setRotation(0, math.sin(angle / 2), 0, math.cos(angle / 2))

getRotation() returns the same four components as a table, {x, y, z, w}.

rotate(x, y, z) adds a rotation instead of replacing one. The arguments are Euler angles in radians, applied in the object's own frame on top of its current rotation, so gameObject.rotate(0, math.pi * deltaTime, 0) in update spins an object half a turn per second. It works the same way on every object: a body, a character, a light, an empty, and a parent, whose children turn with it.

Where an object's origin sits

An object's position names a point inside that object — but which point depends on the object's type. The convention is not uniform, and guessing wrong is a common source of objects that sit half-buried in the floor or cameras that render from hip height.

OriginObject typesWhat position names
centercube, sphere, cylinder, capsule (rigid or soft), softCloth, water, wheeled and tracked vehiclesThe centre of the shape. Bounds straddle the origin, so a 2m capsule runs from y = −1 to y = +1.
feetcharacterThe bottom of the capsule. Bounds run from y = 0 up to the full height, so a 2m character runs from y = 0 to y = 2.
baseheightFieldThe base of the terrain. Bounds run from y = 0 up to heightScale.
asset_originmeshWhatever origin the source asset was exported with. This is not a convention Airogel imposes — inspect the object rather than assuming.

The difference between the first two rows is easy to miss because the same collider dimensions produce different bounds. A character with collisionRadius 0.4 and collisionHalfHeight 0.6 and a capsule with radius 0.4 and halfHeight 0.6 are both 2m tall, but the character's origin is at its feet and the capsule's is at its centre.

Placing an object on the ground. A character stands on a floor at y = 0 when its position is y = 0. A capsule of the same size needs y = 1, or it sinks halfway in.

Computing eye height. For a 2m character, the eye sits at roughly position.y + 1.65, not position.y + 0.65. A built-in first-person camera does this arithmetic for you: its eyeHeight field (default 1.6) is measured from the follow target's origin, which for a character is its feet — so set eyeHeight to the eye height you want. See Characters.

Checking rather than guessing. spatial_inspect_scene reports an explicit origin field and the resolved local_bounds for every object, which is the reliable way to answer this for a mesh whose asset origin you did not author.

Hierarchy

Use getParent(), getChildren(), isRoot(), and setParent(parentId) to inspect or change hierarchy. setParent keeps the object where it is in the world and works out its new local transform for you; setParent(nil) makes it a root object again, also without moving it.

The editor and the server refuse two kinds of parent: a water object, and any link between a GUI element and a 3D object, in either direction. setParent does not check these while the scene runs, so keep scripts to the same rule.

A parent whose children collide has no collider of its own. Its colliding children each keep their own body instead. Children that collide with nothing (a mesh with collision set to None, a light, a camera) are decoration and leave the parent's body alone. To make a group of primitives collide as one rigid body, turn on Compound Children on the parent — see Physics, collisions, and triggers.

The API exposes both world and local transforms:

  • getWorldPosition(), getWorldRotation(), getWorldScale()
  • getLocalPosition(), getLocalRotation(), getLocalScale()
  • getForward() and getRight() for world-space facing vectors

The local transforms are read-only. There is no setLocalPosition, setLocalRotation or setLocalScale. setPosition() and setRotation() are world-space, and so are getPosition() and getRotation(), for a child as well as a root: to move a child relative to its parent, compose the value you want in world space and set it there. Moving or turning a parent carries its children, colliders included.

Facing vectors

getForward() and getRight() return world-space unit vectors. For an ordinary object getForward() is its local +Z and getRight() is its local +X — which, for an object facing +Z, points to the object's left. Negate getRight() when you want a character's or vehicle's own right-hand side. A vehicle given a positive steering input turns toward -getRight(), not toward getRight().

A camera is the exception. Its getForward() reports the direction it actually renders, and getRight() its screen-right. Three.js cameras look down their own local -Z while everything else here faces +Z, so a camera's rendered direction is not what you would get by applying the +Z rule to its rotation — read it from getForward() and it will agree with what is on screen. See gameObject API reference.

Camera-relative movement

Get the active camera with scene.getActiveCamera(), flatten its forward and right vectors onto the XZ plane, normalize them, and combine them from input. This keeps movement aligned with the rendered camera rather than fixed world axes.