Transform
Where something is, how it is turned, and how big it is -- with its matrix built on demand and cached. This is what an object that MOVES wants, as opposed to Box, which is a static prop and caches a translate-and-scale. A program animating something otherwise has to keep its own scratch buffers and remember the order the three parts compose in. ## No allocation after construction The matrix is one buffer owned by the Transform, rebuilt only when something actually changed. Setting the same position twice costs nothing, and a static object costs nothing per frame at all. Translation, rotation and scale compose as T * R * S -- scale first, then rotate, then move -- which is the order that behaves the way people expect: scaling does not drag the object away from its position, and rotation happens about its own centre. Composing them in any other order is a common and confusing bug, so the order is not a choice here.
Example
ball := Transform->New();
ball->SetScale(0.5);
# per frame
ball->SetPosition(x, height, z);
ball->Rotate(0.0, spin * delta, 0.0);
ball->MvpInto(mvp, view_projection);
shader->SetMatrix4("mvp", mvp);
mesh->Draw();Operations
- New
- GetMatrix
- GetMaxScale
- GetParent
- GetPosition
- GetVersion
- GetWorldMatrix
- GetWorldMaxScale
- GetWorldX
- GetWorldY
- GetWorldZ
- HasParent
- Move
- MvpInto
- Rebuild
- Rotate
- SetParent
- SetPosition
- SetRotation
- SetScale
GetMatrix #
The model matrix, T * R * S. Rebuilt only when something changed, into a buffer this object owns -- so the returned array is NOT a copy. Do not hold on to it across a change, and do not write to it.
method : public : GetMatrix() ~ Float[]Return
| Type | Description |
|---|---|
| Float | the model matrix, 16 floats column-major |
GetMaxScale #
The largest of the three scale factors. What a bounding sphere needs: a sphere has one radius, so a non-uniform scale has to be bounded by its largest component or the bound is wrong in the direction that was scaled most.
method : public : GetMaxScale() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the largest scale component |
GetParent #
method : public : GetParent() ~ TransformReturn
| Type | Description |
|---|---|
| Transform | the parent, or Nil when this transform is in world space |
GetPosition #
method : public : GetPosition() ~ Vector3Return
| Type | Description |
|---|---|
| Vector3 | the position as a new Vector3 |
GetVersion #
How many times this transform's matrix has been rebuilt. A child reads its parent's number to decide whether its own composed matrix is still good. Exposed because the mechanism is worth being able to see; a program has no reason to call it.
method : public : GetVersion() ~ IntReturn
| Type | Description |
|---|---|
| Int | a counter |
GetWorldMatrix #
The matrix that places this transform in the WORLD. The same as GetMatrix for an unparented transform, and parent * local for a parented one, composed up the whole chain. Recomputed only when something actually changed -- this transform, or any ancestor. That check is a version comparison rather than a notification, so nothing has to register with anything and a parent does not need to know its children exist.
method : public : GetWorldMatrix() ~ Float[]Return
| Type | Description |
|---|---|
| Float | the world matrix; do not modify it |
GetWorldMaxScale #
The largest scale factor this transform is under, parents included. Taken from the world matrix's own columns rather than by multiplying the chain's scale values: a rotation between two scales makes those two things different, and the columns are what the geometry is actually built from. Absolute, so a mirrored transform reports a positive size. A negative radius culls an object that is dead centre in view.
method : public : GetWorldMaxScale() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the world scale, at least 0 |
GetWorldX #
Where this transform actually is, after its parents are taken into account. GetX and its pair report the LOCAL position -- what SetPosition was given, relative to the parent. These report the world one, which is what a cull test, a distance sort or anything else asking "where is it" wants. The two are the same for an unparented transform, and that is exactly why the difference is easy to miss: everything worked until the first parented object, and then it worked everywhere except at the edge of the screen.
method : public : GetWorldX() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the x coordinate in world space |
GetWorldY #
method : public : GetWorldY() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the y coordinate in world space |
GetWorldZ #
method : public : GetWorldZ() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the z coordinate in world space |
HasParent #
method : public : HasParent() ~ BoolReturn
| Type | Description |
|---|---|
| Bool | true when this transform hangs off another |
Move #
Shift by an offset.
method : public : Move(dx:Float, dy:Float, dz:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| dx | Float | x offset |
| dy | Float | y offset |
| dz | Float | z offset |
MvpInto #
Model-view-projection, into a buffer the caller owns. The per-frame call. Uses the WORLD matrix, so a parented transform draws where the hierarchy puts it rather than where its local values say.
method : public : MvpInto(out:Float[], view_projection:Float[]) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| out | Float | destination, at least 16 elements |
| view_projection | Float | the camera's projection times view |
Rebuild #
T * R * S without a single matrix product. Two identities make this cheap. Scaling by a diagonal matrix on the RIGHT scales each COLUMN of the rotation, and translating on the LEFT only writes the last column -- so the whole composition is the rotation with its columns scaled and a translation dropped in. Spelling it as Multiply(Multiply(T, R), S) would allocate four matrices and do 128 multiply-adds to reach the same sixteen numbers. Checked against that composition in the regression suite, because this is the kind of shortcut that is wrong in a way that still looks like a transform.
method : private : Rebuild() ~ NilRotate #
Turn by an offset -- the per-frame form, multiplied by a delta.
method : public : Rotate(dyaw:Float, dpitch:Float, droll:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| dyaw | Float | change in yaw |
| dpitch | Float | change in pitch |
| droll | Float | change in roll |
SetParent #
Hang this transform off another, so it moves when the parent does. This is what makes articulated motion possible at all: a turret on a hull, a hand on an arm, a moon around a planet. Without it every part has to have its world position recomputed by the program each frame, which is the same arithmetic written once per part rather than once. Position, rotation and scale become RELATIVE to the parent. A child at (0, 1, 0) under a parent at (5, 0, 0) is at (5, 1, 0) in the world, and turning the parent swings the child around it. A cycle is refused rather than accepted and then hung on: attaching a transform to its own descendant would make GetWorldMatrix recurse until the stack ran out, and the crash would name neither of the two transforms involved.
method : public : SetParent(parent:Transform) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| parent | Transform | the transform to hang off, or Nil to return to world space |
Return
| Type | Description |
|---|---|
| Bool | true when attached; false when it would have made a cycle |
SetPosition #
method : public : SetPosition(x:Float, y:Float, z:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| x | Float | x position |
| y | Float | y position |
| z | Float | z position |
SetPosition #
method : public : SetPosition(position:Vector3) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| position | Vector3 | the position |