Understanding C4 Transforms
From C4 Engine Wiki
Note: The code below has been updated to work with v2.7 of the engine.
This code below allows you to move and rotate a geometry and to see the effect of various actions on the World and Local Transform (4x4) matrices. You can choose to rotate using "SetRotationAbout" or by using "SetEulerAngles".
Note that the "local" transform here refers to the 4x4 matrix to transform from your entity (child node) to its parent node. So if your entity is directly attached the root node (and it probably will be), then the local and world transforms will be identical.
Steps:
- Make a new project as described in the HelloWorld Tutorial.
- Use this code instead for your HelloWorld.h and HelloWorld.cpp files. I called the files TransformTest.h and TransformTest.cpp instead.
- Compile
- Add the following to your engine.cfg file (under Data/engine). Note that I'm using a German keyboard - use other keys (say "h","k","b","n") if you don't know the values for the arrow keys for your keyboard:
bind "NACH-RECHTS" %trgt; bind "NACH-LINKS" %tlft; bind "NACH-OBEN" %tfwd; bind "NACH-UNTEN" %tbck; bind "u" %tnup; bind "j" %tdwn; bind "r" %rmde; bind "t" %tmde; bind "e" %eulr;
- Open (for example) tutorials/world/water.wld and place a Geometry in the world. Use the world editor's "transform page" - see the pages menu - to position it say at (X=10.0, Y=5.0, Z=3.0) with 0's for all rotations (or place it wherever you want!!).
- With the Geometry still selected, press Ctrl-I to open the Node Info dialog. Then go to the Controller tab and select the "Transform Test" Controller. Press OK to save that change.
- Save your world under a name like TransformTest.wld. Its best not to overwrite the original Water.wld which you can use again later for other test projects.
- Now choose World > "Save and Play" from the menu (or press Ctrl-P) and use the keys described below to move and rotate your Geometry.
Keys:
- "WASD" and mouse are as usual for camera control
- "R" is to swith on and off rotation control
- "T" is to switch on and off translation control
- "E" is to toggle use of Euler angles in the transforms
- Arrow Keys as well as "U" and "J" are for moving and rotating the entity.
Snap Shot: Here is a picture showing the matrix info in the top left of the screen:
-- Jimbobjames
TransformTest.h
#ifndef TransformTest_h
#define TransformTest_h
#include "C4Application.h"
#include "C4Engine.h"
#include "C4World.h"
#include "C4Cameras.h"
#include "C4Interface.h"
#include "C4Input.h"
extern "C"
{
C4MODULEEXPORT C4::Application *ConstructApplication(void);
}
namespace C4
{
enum
{
kActionForward = 'frwd',
kActionBackward = 'bkwd',
kActionLeft = 'left',
kActionRight = 'rght',
kActionUp = 'jump',
kActionDown = 'down',
kActionTranslateRight = 'trgt',
kActionTranslateLeft = 'tlft',
kActionTranslateForward = 'tfwd',
kActionTranslateBack = 'tbck',
kActionTranslateUp = 'tnup',
kActionTranslateDown = 'tdwn',
kActionTranslateMode = 'tmde',
kActionRotateMode = 'rmde',
kActionEulerMode = 'eulr'
};
enum
{
kEntityTest = 'test'
};
enum
{
kControllerTransform = 'tfrm'
};
enum
{
kLocatorSpectator = 'spec'
};
class MovementAction : public Action
{
private:
unsigned long movementFlag;
public:
MovementAction(unsigned long type, unsigned long flag);
~MovementAction();
void Begin(void);
void End(void);
};
class TransformController : public Controller
{
private:
float transformRate;
float displacementX, displacementY, displacementZ;
float rotationX, rotationY, rotationZ;
float previousDisplacementX, previousDisplacementY, previousDisplacementZ;
float previousRotationX, previousRotationY, previousRotationZ;
Transform4D originalTransform; // The target's original transform
Transform4D translation;
Point3D myTranslation;
TextWidget *pText[4][4];
TextWidget *pTextLocal[4][4];
TextWidget *matrixTitleText[2]; //Names of the various matrices
TextWidget *rotateText;
TextWidget *translateText;
TextWidget *eulerText;
public:
TransformController();
~TransformController();
static bool ValidNode(const Node *node);
void Preprocess(void);
void Move(void);
TextWidget *DisplayText(char *pText, const float x, const float y);
};
class GameWorld : public World
{
private:
SpectatorCamera spectatorCamera;
public:
GameWorld(const char *name);
~GameWorld();
SpectatorCamera *GetSpectatorCamera(void)
{
return (&spectatorCamera);
}
WorldResult Preprocess(void);
void Render(void);
};
class Game : public Singleton<Game>, public Application
{
private:
DisplayEventHandler displayEventHandler;
ControllerReg<TransformController> transformControllerReg;
ModelRegistration testModelEntityReg;
LocatorRegistration locatorReg;
InputMgr::KeyProc *prevEscapeProc;
void *prevEscapeData;
MovementAction *forwardAction;
MovementAction *backwardAction;
MovementAction *leftAction;
MovementAction *rightAction;
MovementAction *upAction;
MovementAction *downAction;
MovementAction *translateRightAction;
MovementAction *translateLeftAction;
MovementAction *translateForwardAction;
MovementAction *translateBackAction;
MovementAction *translateUpAction;
MovementAction *translateDownAction;
MovementAction *translateModeAction;
MovementAction *rotateModeAction;
MovementAction *eulerModeAction;
static World *ConstructWorld(const char *name, void *data);
static void HandleDisplayEvent(const DisplayEventData *eventData, void *data);
static void EscapeProc(void *data);
public:
Game();
~Game();
};
extern Game *TheGame;
}
#endif
TransformTest.cpp
#include "TransformTest.h"
using namespace C4;
Game *C4::TheGame = nullptr;
bool translateRight = false;
bool translateLeft = false;
bool translateForward = false;
bool translateBack = false;
bool translateMode = false;
bool translateUp = false;
bool translateDown = false;
bool rotateMode = true;
bool eulerMode = false;
inline String<5> MyFloatToString(float num) {
return (String<5>(num));
}
C4::Application *ConstructApplication(void)
{
return (new Game);
}
MovementAction::MovementAction(unsigned long type, unsigned long flag) : Action(type)
{
movementFlag = flag;
}
MovementAction::~MovementAction()
{
}
void MovementAction::Begin(void)
{
GameWorld *world = static_cast<GameWorld *>(TheWorldMgr->GetWorld());
if (world)
{
SpectatorCamera *camera = world->GetSpectatorCamera();
camera->SetSpectatorFlags(camera->GetSpectatorFlags() | movementFlag);
}
switch (GetActionType())
{
case kActionTranslateRight:
translateRight = true;
break;
case kActionTranslateLeft:
translateLeft = true;
break;
case kActionTranslateForward:
translateForward = true;
break;
case kActionTranslateBack:
translateBack = true;
break;
case kActionTranslateUp:
translateUp = true;
break;
case kActionTranslateDown:
translateDown = true;
break;
}
}
void MovementAction::End(void)
{
GameWorld *world = static_cast<GameWorld *>(TheWorldMgr->GetWorld());
if (world)
{
SpectatorCamera *camera = world->GetSpectatorCamera();
camera->SetSpectatorFlags(camera->GetSpectatorFlags() & ~movementFlag);
}
switch (GetActionType())
{
case kActionTranslateRight:
translateRight = false;
break;
case kActionTranslateLeft:
translateLeft = false;
break;
case kActionTranslateForward:
translateForward = false;
break;
case kActionTranslateBack:
translateBack = false;
break;
case kActionTranslateUp:
translateUp = false;
break;
case kActionTranslateDown:
translateDown = false;
break;
case kActionRotateMode:
rotateMode = !rotateMode;
break;
case kActionTranslateMode:
translateMode = !translateMode;
break;
case kActionEulerMode:
eulerMode = !eulerMode;
break;
}
}
TransformController::TransformController() : Controller(kControllerTransform)
{
transformRate = 0.005F;
}
TransformController::~TransformController()
{
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++) {
delete pText[i][j];
delete pTextLocal[i][j];
}
delete rotateText;
delete translateText;
delete eulerText;
}
bool TransformController::ValidNode(const Node *node)
{
return (node->GetNodeType() == kNodeGeometry);
}
void TransformController::Preprocess(void)
{
Controller::Preprocess();
rotateText = new TextWidget("Rotate Mode OFF");
translateText = new TextWidget("Translate Mode OFF");
eulerText = new TextWidget("Euler Mode OFF");
matrixTitleText[0] = new TextWidget("World Transform Matrix:");
matrixTitleText[1] = new TextWidget("Local (Child to Parent) Transform Matrix:");
matrixTitleText[0]->SetWidgetPosition(Point3D(1.0f, 20.0f, 0.0f));
TheInterfaceMgr->AddWidget(matrixTitleText[0]);
matrixTitleText[1]->SetWidgetPosition(Point3D(1.0f, 130.0f, 0.0f));
TheInterfaceMgr->AddWidget(matrixTitleText[1]);
for (int i = 0;i < 4; i++)
for (int j = 0; j < 4; j++) {
pText [i][j] = new TextWidget("0.0");
pTextLocal [i][j] = new TextWidget("0.0");
}
// Grab the original transform of the target node
const Node *target = GetTargetNode();
originalTransform = target->GetNodeTransform();
previousDisplacementX = previousDisplacementY = previousDisplacementZ = 0.0F;
previousRotationX = previousRotationY = previousRotationZ = 0.0F;
translation.SetIdentity();
myTranslation.x = myTranslation.y = myTranslation.z = 0.0F;
displacementX = displacementY = displacementZ = 0.0F;
rotationX = rotationY = rotationZ = 0.0F;
}
void TransformController::Move(void)
{
Matrix3D rotator;
// Calculate the new transform angle based on how much time has passed
if (translateLeft) {
if (translateMode)
displacementX = previousDisplacementX + transformRate * TheTimeMgr->GetFloatDeltaTime();
if (rotateMode)
rotationX = previousRotationX + transformRate * TheTimeMgr->GetFloatDeltaTime();
}
if (translateRight) {
if (translateMode)
displacementX = previousDisplacementX - transformRate * TheTimeMgr->GetFloatDeltaTime();
if (rotateMode)
rotationX = previousRotationX - transformRate * TheTimeMgr->GetFloatDeltaTime();
}
if (translateBack) {
if (translateMode)
displacementY = previousDisplacementY + transformRate * TheTimeMgr->GetFloatDeltaTime();
if (rotateMode)
rotationY = previousRotationY + transformRate * TheTimeMgr->GetFloatDeltaTime();
}
if (translateForward) {
if (translateMode)
displacementY = previousDisplacementY - transformRate * TheTimeMgr->GetFloatDeltaTime();
if (rotateMode)
rotationY = previousRotationY - transformRate * TheTimeMgr->GetFloatDeltaTime();
}
if (translateUp) {
if (translateMode)
displacementZ = previousDisplacementZ + transformRate * TheTimeMgr->GetFloatDeltaTime();
if (rotateMode)
rotationZ = previousRotationZ + transformRate * TheTimeMgr->GetFloatDeltaTime();
}
if (translateDown) {
if (translateMode)
displacementZ = previousDisplacementZ - transformRate * TheTimeMgr->GetFloatDeltaTime();
if (rotateMode)
rotationZ = previousRotationZ - transformRate * TheTimeMgr->GetFloatDeltaTime();
}
previousDisplacementX = displacementX;
previousDisplacementY = displacementY;
previousDisplacementZ = displacementZ;
previousRotationX = rotationX;
previousRotationY = rotationY;
previousRotationZ = rotationZ;
Node *target = GetTargetNode();
if (translateMode) {
myTranslation.x = displacementX;
myTranslation.y = displacementY;
myTranslation.z = displacementZ;
translation.SetTranslation(myTranslation);
target->SetNodeTransform(originalTransform * translation);
}
// Now make a 3x3 rotation matrix
if (rotateMode) {
rotator.SetRotationAboutZ(0.0F);
if (!eulerMode) {
if (translateRight || translateLeft)
rotator.SetRotationAboutZ(rotationX);
if (translateForward || translateBack)
rotator.SetRotationAboutX(rotationY);
if (translateUp || translateDown)
rotator.SetRotationAboutY(rotationZ);
} else {
target->SetNodeMatrix3D(Matrix3D().SetEulerAngles(rotationX, rotationY, rotationZ));
}
} else {
rotationX = rotationY = rotationZ = 0.0F;
rotator.SetRotationAboutX(0.0F);
rotator.SetRotationAboutY(0.0F);
rotator.SetRotationAboutZ(0.0F);
}
const Point3D& worldCenter = target->GetBoundingSphere()->GetCenter();
Point3D objectCenter = target->GetInverseWorldTransform() * worldCenter;
// Make a 3x4 transform that rotates about the center point
Transform4D transform(rotator, objectCenter - rotator * objectCenter);
//translation.SetIdentity();
//target->SetNodePosition(myTranslation);//
if (!eulerMode)
target->SetNodeTransform(originalTransform * translation * transform);
Transform4D worldTransform = target->GetWorldTransform();
Transform4D localTransform = target->GetNodeTransform();
int verticalTextOffset = 10.0F;
String<20> rotateModeText = (rotateMode ? "Rotate ON" : "Rotate OFF");
String<20> translateModeText = (translateMode ? "Translate ON" : "Translate OFF");
String<20> eulerModeText = (eulerMode ? "Euler ON" : "Euler OFF");
Point3D worldPosition = worldTransform.GetTranslation();
Point3D localPosition = localTransform.GetTranslation();
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++)
delete pText[i][j];
delete rotateText;
delete translateText;
delete eulerText;
rotateText = this->DisplayText(rotateModeText, 1.0F, 2.0F);
translateText = this->DisplayText(translateModeText, 100.0F, 2.0F);
eulerText = this->DisplayText(eulerModeText, 200.0F, 2.0F);
for (int i = 0; i < 3; i++) {
pText[i][0] = this->DisplayText(MyFloatToString(worldTransform[i].x) + " ", 20.0F, verticalTextOffset + 30.0F + 20.0 * i);
pText[i][1] = this->DisplayText(MyFloatToString(worldTransform[i].y) + " ", 90.0F, verticalTextOffset + 30.0F + 20.0 * i);
pText[i][2] = this->DisplayText(MyFloatToString(worldTransform[i].z) + " ", 160.0F, verticalTextOffset + 30.0F + 20.0 * i);
}
//fourth row of a Transform4D is always (0,0,0,1)
pText[3][0] = this->DisplayText("0.0", 20.0F, verticalTextOffset + 90.0F);
pText[3][1] = this->DisplayText("0.0", 90.0F, verticalTextOffset + 90.0F);
pText[3][2] = this->DisplayText("0.0", 160.0F, verticalTextOffset + 90.0F);
//fourth column of Transform contains the translation (poistion)
pText[0][3] = this->DisplayText(MyFloatToString(worldPosition.x), 230.0F, verticalTextOffset + 30.0F);
pText[1][3] = this->DisplayText(MyFloatToString(worldPosition.y), 230.0F, verticalTextOffset + 50.0F);
pText[2][3] = this->DisplayText(MyFloatToString(worldPosition.z), 230.0F, verticalTextOffset + 70.0F);
pText[3][3] = this->DisplayText("1.0", 230.0F, verticalTextOffset + 90.0F);
//now for the local transform
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++)
delete pTextLocal[i][j];
for (int i = 0; i < 3; i++) {
pTextLocal[i][0] = this->DisplayText(MyFloatToString(localTransform[i].x) + " ", 20.0F, verticalTextOffset + 140.0F + 20.0 * i);
pTextLocal[i][1] = this->DisplayText(MyFloatToString(localTransform[i].y) + " ", 90.0F, verticalTextOffset + 140.0F + 20.0 * i);
pTextLocal[i][2] = this->DisplayText(MyFloatToString(localTransform[i].z) + " ", 160.0F, verticalTextOffset + 140.0F + 20.0 * i);
//pTextLocal[i][3] = this->DisplayText("0.0, ", 220.0F, 30.0F + 20.0 * i);
}
//fourth row of a Transform4D is always (0,0,0,1)
pTextLocal[3][0] = this->DisplayText("0.0", 20.0F, verticalTextOffset + 200.0F);
pTextLocal[3][1] = this->DisplayText("0.0", 90.0F, verticalTextOffset + 200.0F);
pTextLocal[3][2] = this->DisplayText("0.0", 160.0F, verticalTextOffset + 200.0F);
//pTextLocal[3][3] = this->DisplayText("1.0", 220.0F, 90.0F);
//fourth column of Transform contains the translation (poistion)
pTextLocal[0][3] = this->DisplayText(MyFloatToString(localPosition.x), 230.0F, verticalTextOffset + 140.0F);
pTextLocal[1][3] = this->DisplayText(MyFloatToString(localPosition.y), 230.0F, verticalTextOffset + 160.0F);
pTextLocal[2][3] = this->DisplayText(MyFloatToString(localPosition.z), 230.0F, verticalTextOffset + 180.0F);
pTextLocal[3][3] = this->DisplayText("1.0", 230.0F, verticalTextOffset + 200.0F);
// Invalidate the target node so that it gets updated properly
target->Invalidate();
}
TextWidget *TransformController::DisplayText(char *pText, const float x, const float y)
{
TextWidget *pTextWidget = new TextWidget(Vector2D(80.0f,16.0f),pText);
pTextWidget->SetFont("font/Bold");
pTextWidget->SetWidgetColor(ColorRGBA(0.0f, 0.0f, 1.0f));
pTextWidget->SetWidgetPosition(Point3D(x, y, 0.0f));
TheInterfaceMgr->AddWidget(pTextWidget);
return pTextWidget;
}
GameWorld::GameWorld(const char *name) :
World(name),
spectatorCamera(2.0F, 1.0F, 0.3F)
{
}
GameWorld::~GameWorld()
{
}
WorldResult GameWorld::Preprocess(void)
{
WorldResult result = World::Preprocess();
if (result != kWorldOkay) return (result);
Zone *zone = GetRootNode();
if (zone)
{
const Marker *marker = zone->GetFirstMarker();
while (marker)
{
MarkerType type = marker->GetMarkerType();
if (type == kMarkerLocator)
{
if (static_cast<const LocatorMarker *>(marker)->GetLocatorType() == kLocatorSpectator)
{
const Vector3D direction = marker->GetWorldTransform()[0];
float azimuth = Atan(direction.y, direction.x);
float altitude = Atan(direction.z, Sqrt(direction.x * direction.x + direction.y * direction.y));
spectatorCamera.SetCameraAzimuth(azimuth);
spectatorCamera.SetCameraAltitude(altitude);
spectatorCamera.SetNodePosition(marker->GetWorldPosition());
}
}
marker = marker->ListElement<Marker>::Next();
}
}
else
{
spectatorCamera.SetNodePosition(Point3D(0.0F, 0.0F, 1.0F));
}
SetCamera(&spectatorCamera);
return (kWorldOkay);
}
void GameWorld::Render(void)
{
World::Render();
}
Game::Game() :
Singleton<Game>(TheGame),
displayEventHandler(&HandleDisplayEvent),
testModelEntityReg(kEntityTest, nullptr, "model/testModel", kModelPrecache, kControllerTransform),
transformControllerReg(kControllerTransform, "Test Model"),
locatorReg(kLocatorSpectator, "Spectator Camera")
{
TheDisplayMgr->InstallDisplayEventHandler(&displayEventHandler);
prevEscapeProc = TheInputMgr->GetEscapeProc();
prevEscapeData = TheInputMgr->GetEscapeData();
TheInputMgr->SetEscapeProc(&EscapeProc, this);
TheWorldMgr->SetWorldConstructor(&ConstructWorld);
forwardAction = new MovementAction(kActionForward, kSpectatorMoveForward);
backwardAction = new MovementAction(kActionBackward, kSpectatorMoveBackward);
leftAction = new MovementAction(kActionLeft, kSpectatorMoveLeft);
rightAction = new MovementAction(kActionRight, kSpectatorMoveRight);
upAction = new MovementAction(kActionUp, kSpectatorMoveUp);
downAction = new MovementAction(kActionDown, kSpectatorMoveDown);
translateRightAction = new MovementAction(kActionTranslateRight, 0);
translateLeftAction = new MovementAction(kActionTranslateLeft, 0);
translateForwardAction = new MovementAction(kActionTranslateForward, 0);
translateBackAction = new MovementAction(kActionTranslateBack, 0);
translateUpAction = new MovementAction(kActionTranslateUp, 0);
translateDownAction = new MovementAction(kActionTranslateDown, 0);
rotateModeAction = new MovementAction(kActionRotateMode, 0);
translateModeAction = new MovementAction(kActionTranslateMode, 0);
eulerModeAction = new MovementAction(kActionEulerMode, 0);
TheInputMgr->AddAction(forwardAction);
TheInputMgr->AddAction(backwardAction);
TheInputMgr->AddAction(leftAction);
TheInputMgr->AddAction(rightAction);
TheInputMgr->AddAction(upAction);
TheInputMgr->AddAction(downAction);
TheInputMgr->AddAction(translateRightAction);
TheInputMgr->AddAction(translateLeftAction);
TheInputMgr->AddAction(translateForwardAction);
TheInputMgr->AddAction(translateBackAction);
TheInputMgr->AddAction(translateUpAction);
TheInputMgr->AddAction(translateDownAction);
TheInputMgr->AddAction(translateModeAction);
TheInputMgr->AddAction(rotateModeAction);
TheInputMgr->AddAction(eulerModeAction);
TheInterfaceMgr->SetInputManagementMode(kInputManagementAutomatic);
TheMessageMgr->BeginSinglePlayerGame();
}
Game::~Game()
{
TheWorldMgr->UnloadWorld();
TheWorldMgr->SetWorldConstructor(nullptr);
TheMessageMgr->EndGame();
delete downAction;
delete upAction;
delete rightAction;
delete leftAction;
delete backwardAction;
delete forwardAction;
delete translateRightAction;
delete translateLeftAction;
delete translateForwardAction;
delete translateBackAction;
delete translateUpAction;
delete translateDownAction;
delete translateModeAction;
delete rotateModeAction;
delete eulerModeAction;
TheInputMgr->SetEscapeProc(prevEscapeProc, prevEscapeData);
}
World *Game::ConstructWorld(const char *name, void *data)
{
return (new GameWorld(name));
}
void Game::HandleDisplayEvent(const DisplayEventData *eventData, void *data)
{
// This function is called when a display event occurs (because we
// registered it in the Game constructor).
if (eventData->eventType == kEventDisplayChange)
{
// The screen resolution has changed. Handle accordingly.
}
}
void Game::EscapeProc(void *data)
{
}

