Dieser Artikel erschien erstmals im PC Magazin 11/2001. Die Wieder­veröffentlichung erfolgt mit freundlicher Genehmigung der WEKA Media Publishing GmbH.

Animierte Modelle mit Milkshape 3D

Spiel-Szene

Um Spielfiguren zu entwickeln, animieren Sie Ihre Milkshape-3D-Polygonmodelle mit realistischen Knochensystemen.

Carsten Dachsbacher

Den 3D-Modellen, die Sie in der letzten Ausgabe von PC Underground (10/01, S. 238) geschaffen haben, fehlt noch ein Knochengerüst. In diesem Beitrag zeigen wir Ihnen, wie Sie mit Milkshape Ihre 3D-Modelle mit Hilfe von Knochen­systemen (Bone Systems) realistisch animieren und in eigene Spiele einbauen.

DAS KNOCHENSKELETT des Ego-Shooters Unreal Tournament
DAS KNOCHENSKELETT des Ego-Shooters Unreal Tournament

Beim Modellieren eines humanoiden Lebewesens bilden Sie ein Knochenskelett nach. Dieses enthält nicht so viele Knochen wie das menschliche Skelett. Im Egoshooter Unreal Tournament finden Sie ein Skelett, das etwa 40 Knochen enthält.

Bauen Sie das 3D-Modell (Dreiecke) auf. Die Form des Modells sollte sich den Gegebenheiten des möglichst genau gebildeten Skeletts anpassen. Gehen Sie vor, wie in der letzten Ausgabe beschrieben: Texturieren Sie das 3D-Modell.

Jedem Vertex des Modells weisen Sie einen Knochen zu, mit dem er assoziiert ist. Die Bewegung dieses Knochens wirkt sich auf den Vertex und damit assoziierte Vertizes aus. Sie müssen bei der Animation des 3D-Modells nur das Skelett animieren, das Modell passt sich den Bewegungen an. Sie können so Animationen, die Sie einmal auf Basis des Skeletts angelegt haben, für mehrere 3D-Modelle verwenden.

Das Skelett ist hierarchisch aufgebaut: Knochen können anderen Knochen untergeordnet sein. So bewegen sich der Unterarm und alle daran befestigten Knochen mit, wenn das Modell den Oberarm bewegt. Diese Animations­technik heißt Skeletal Animation. Auf Grund der Rechenleistung heutiger Hardware hat sie in nahezu allen modernen Spielen Einzug gehalten. Ihre Vorteile: Sie können Animationen wieder­verwenden, verschiedene Animation überblenden und überlagern.

So animieren Sie zwei Abläufe unterschied­lich voneinander: In der ersten Animation lassen Sie eine Spielfigur laufen. Die zweite beschreibt eine Drehung des Kopfes zur Seite. Die Spielfigur kann gehend den Kopf zur Seite drehen.

Für diese Animations­technik gibt es Hardware, die über 30000 Mark kostet. Diese Motion­capturing-Systeme bauen grob ein Skelett und versehen eine Person, von der Sie Bewegungen aufzeichnen wollen, mit Sensoren oder Marken. Anschließend zeichnet der Computer anhand der Sensoren oder Kamerabilder die Bewegungs­abläufe auf und speichert diese für Ihr Skelett. So erreichen Studios realistische Bewegungen in Perfektion.

Ein Roboterarm entsteht

UNSER ROBOTERARM besteht aus fünf Bones und sechs Joints.
UNSER ROBOTERARM besteht aus fünf Bones und sechs Joints.

Anhand eines Roboterarms erkennen Sie die hierarchische Animation, und Sie überschauen die Anzahl der Knochen (Bones) – in diesem Fall der Armteile. Der Roboterarm soll komplett drehbar sein, zwei Armsegmente und eine Greifhand besitzen.

Sie benötigen fünf Knochen. Diese sind in Milkshape 3D durch Joints (Verbindungs­stücke/Gelenke) definiert. Ein Bone beginnt und endet in einem Joint.

Sie legen Joints an, indem Sie im Model-Tab von Milkshape 3D das Joint-Tool wählen, und die Joints in den Modelling-Fenstern per Mausklick positionieren. Milkshape 3D erzeugt automatisch einen Bone zwischen einem neu eingefügten und dem letzten Joint. Wenn Sie an einem Joint einen zweiten Bone befestigen wollen, dann wählen Sie den entsprechenden Joint im Joints-Tab. Er wird dann rot dargestellt. Die schon daran befindlichen Bones sind grün zu erkennen.

AN EINEM JOINT können Sie mehrere Bones befestigen.
AN EINEM JOINT können Sie mehrere Bones befestigen.

Im nächsten Schritt modellieren Sie den Roboterarm. Das Beispiel begnügt sich mit einfachen Zylindern und einer Kugel, die grob die Form gestalten. Sie haben nun die Möglichkeit das Aussehen des Roboters nach Ihren Vorstellungen zu verfeinern.

WEISEN SIE allen Vertizes einen Bone zu.
WEISEN SIE allen Vertizes einen Bone zu.

Weisen Sie den Vertizes der Zylinder jeweils einen Bone zu. Dazu markieren Sie die Vertizes mit dem Select-Tool aus dem Model-Tab. Wählen Sie im Joints-Tab den Joint und damit alle daran befestigten (unter­geordneten) Bones aus, und klicken Sie auf den Assign-Button. Beachten Sie folgende Punkte:
•Beginnen Sie in der Hierarchie ganz oben, und arbeiten Sie sich nach unten durch. Das vereinfacht die Zuweisungen. Beim Roboterarm fällt die Entscheidung, welche Vertizes welchen Bones zugeordnet werden sollen, leicht, weil Sie die 3D-Teile des Arms der Reihe nach anlegen. Bei einem Low-Polygon-Modell eines Menschen sind eventuell mehrere Versuche nötig, um das beste Ergebnis zu erzielen. Das 3D-Modell soll möglichst genau entlang dem Skelett verlaufen. Weisen Sie alle Vertizes zu. Um herauszufinden, welche Sie noch nicht bearbeitet haben, deselektieren Sie zunächst alle eventuell markierten Vertizes. Wenn Sie auf den Select unassigned (SelUnAssigned) Button klicken, markiert Milkshape 3D alle diese Vertices für Sie. Analog bewirkt der Select Assigned (SelAssigned) Button, dass alle Vertices, die mit dem gerade gewählten Bone assoziiert sind, sichtbar werden.

Skelett animieren

Animieren Sie das Skelett. Aktivieren Sie im Menü die Funktion ShowKeyframer. Im unteren Teil des Programm­fensters erscheinen eine Zeitachse und diverse Buttons. Mit dem Anim-Button aktivieren Sie die Animations­modellierung.

Beim Keyframing legen Sie die Stellung der Gelenke, also die Position und Drehung der Joints, nur für bestimmte Zeitpunkte fest. Die Positionen und Winkel zwischen den angegebenen Zeitpunkten bestimmt das Animations­programm durch Interpolation.

Mit dem Schieberegler wählen Sie einen Zeitpunkt (ein Frame), für den Sie einen Animations­schritt festlegen wollen. Mit den Optionen Select, Move und Rotate aus dem Model-Tab können Sie den Roboterarm in jede mögliche Position bringen. Wenn Ihnen das Resultat zusagt, speichern Sie das Frame mit dem Menüpunkt Animate/Set Keyframe oder mit der Tasten­kombination [Strg-K].

Wenn Sie alle Keyframes festgelegt haben, können Sie die fertige Animation mit dem Play-Button oder durch Bewegung des Schiebereglers auf der Zeitachse ansehen und überprüfen. Wenn Sie Ihre Arbeit als MS3D-Datei speichern, werden alle Bone- und Keyframing-Informationen automatisch mit abgelegt.

Animationen in eigenen Programmen

Das Beispiel­programm der letzten Ausgabe las die Vertex-, Polygon- und Material-Informationen von MS3D-Dateien aus. Diese Informationen erschienen in den Dateien immer in der gleichen Reihenfolge. Ebenso verhält es sich mit Animations­daten, die den Materialdaten folgen. Zuerst finden Sie die Frames per Second, das in Milkshape 3D ausgewählte Frame und die Gesamtzahl der Frames in der MS3D-Datei:


float fAnimationFPS;
float fCurrentTime;
int iTotalFrames;
		

Als nächstes finden Sie den 16-Bit-Wert unsigned word, der die Anzahl der Joints im Skelett angibt:

word nJoints;

Um die Animations­daten einzulesen, definieren Sie folgende Strukturen:


typedef struct
{
	float time;
	float rotation[3];
} MS3D_KEYFRAME_ROT;

typedef struct
{
	float time;
	float position[3];
} MS3D_KEYFRAME_POS;

typedef struct
{
	byte flags;
	char name[32];
	char parentName[32];
	float rotation[3];
	float position[3];
	word nKeyFramesRot;
	word nKeyFramesPos;
	MS3D_KEYFRAME_ROT *keyFramesRot;
	MS3D_KEYFRAME_POS *keyFramesPos;
} MS3D_JOINT;
		
UNSER EIGENER Milkshape-3D-Animationsplayer
UNSER EIGENER Milkshape-3D-Animationsplayer

Die Strukturen MS3D_KEYFRAME_ ROT und MS3D_KEYFRAME_POS enthalten die Rotation oder Position eines Joints für ein Keyframe. Diese Werte werden in einem Array der MS3D_ JOINT-Struktur gespeichert. Diese Struktur enthält außer Flags deren Namen und den Namen des in der Hierarchie höheren Joints (parentName). Außerdem sind die initiale Position und Rotation gespeichert. Die Daten lesen Sie in der Reihenfolge, die Sie in der MS3D_JOINT-Struktur finden, aus der MS3D-Datei aus.

Erste Positionierung der Bones

Nachdem Sie alle Daten aus der MS3D-Datei gelesen haben, müssen Sie die Vertizes entsprechend der initialen Position und Rotation der zugehörigen Bones transformieren. Auf Grund der zusammen­gesetzten Rotation und Translation (Verschiebung) und des hierarchischen Aufbaus der Animation setzen Sie Matrizen ein. Speichern Sie für jeden Bone mehrere Matrizen. Diese umfassen die Zwischen­ergebnisse, die relative Transformation eines Bones sowie die absolute:


typedef float MATRIX[4][4];

typedef struct
{
	MATRIX mRelative;
	MATRIX mAbsolute;
	MATRIX mRelativeFinal;
	MATRIX mFinal;
} BONE;

pBone = new BONE[nJoints];
		

Die initiale Positionierung berechnen Sie für jeden Bone. Die folgende Routine geht davon aus, dass in der Bone-Liste die Hierarchie absteigend ist. Das heißt, entweder ist ein Joint Beginn eines Knochens, oder das in der Hierarchie nächsthöhere Joint, mit dem es einen Bone bildet, ist vorher in der Liste enthalten.

Für jeden Joint berechnen Sie die Rotations­matrix (createRotationMatrix(...)) und fügen die Translation hinzu, die sich bei einer 4x4-Matrix in der rechten Spalte befindet. Die Matrixroutinen finden Sie in der Datei matrix.h und in fast jeder mathematischen Formelsammlung:


for(i = 0; i < nJoints; i++)
{
	MS3D_JOINT *bone = &pJoint[i];
	createRotationMatrix(pBone[i].mRelative,
		bone->rotation[0]);

	// Translation
	pBone[i].mRelative[0][3] = bone->pos[0];
	pBone[i].mRelative[1][3] = bone->pos[1];
	pBone[i].mRelative[2][3] = bone->pos[2];
		

Jetzt suchen Sie den in der Hierarchie nächsthöheren Joint. Beginnt mit dem aktuellen Joint ein Knochenstrang, werden Sie keinen Parent Bone finden, womit Sie die oben berechnete Transformations­matrix speichern. Wenn Sie einen Parent Bone finden, müssen Sie dessen Transformations­matrix mit der Matrix des aktuellen Bones multiplizieren und speichern:


// nächsthöheren Joint suchen
nParentBone = -1;
for(int j = 0; j < nJoints; j++)
	if(strcmp(pJoint[j].name, bone->parentName) == 0)
	{
		nParentBone = j;
		break;
	}

if(nParentBone != -1)
{
	// Parent Bone gefunden
	pBone[i].mAbsolute = pBone[i].mRelative *
		pBone[nParentBone].mAbsolute);
	pBone[i].mFinal = pBone[i].mAbsolute;
} else
{
	// kein Parent Bone
	pBone[i].mAbsolute = pBone[i].mRelative;
	pBone[i].mFinal = pBone[i].mRelative;
}
		

Transformieren Sie alle Vertizes mit der inversen Transformations­matrix des assoziierten Bones. Es ist nicht notwendig, aufwändige mathematische Verfahren zur Matrix­inversion anzuwenden, da es sich beim Rotationsteil um eine orthogonale 3x3-Matrix handelt. Bei dieser ist die transponierte Matrix gleich der inversen. Die Verschiebung können Sie invertieren, indem Sie vor der Rotation die Verschiebung negiert anwenden:


for(int j = 0; j < nVertices; j++)
{
	MS3D_VERTEX *pV = &pVertex[j];
	if(pV->boneId != -1)
	{
		MATRIX *a = &pBone[pV->boneId].mAbsolute;
		pV->vertex[0] -= a[0][3];
		pV->vertex[1] -= a[1][3];
		pV->vertex[2] -= a[2][3];
		invrotate(temp,
			pBone[pV->boneId].mAbsolute,
			pV->vertex);

		pV->vertex = temp;
	}
}
		

Animationframe berechnen

Berechnen Sie die Transformations­matrizen für ein bestimmtes Animations-Frame. Dabei gehen Sie in etwa so vor wie bei der Initialisierung. Zunächst haben Sie ein Frame, also eine Zeit, angegeben. Behandeln Sie wieder einen Joint nach dem anderen. Für jeden Joint haben Sie die Arrays


MS3D_KEYFRAME_ROT *keyFramesRot;
MS3D_KEYFRAME_POS *keyFramesPos;
		

gespeichert, in denen die Zeitpunkte, die Rotations- oder Positions­informationen abgelegt sind. Suchen Sie für jeden Joint jeweils die nächsten Keyframe-Informationen, die vor und nach dem gewünschten Frame liegt. Dadurch haben Sie ein Zeitintervall gegeben und können die Werte interpolieren, wie Sie sie an Hand der Positions­berechnung sehen:


MS3D_KEYFRAME_POS
	*pLastPositionKey = NULL,
	*pThisPositionKey = NULL;

for(j = 0; j < nPositionKeyCount; j++)
{
	pPositionKey = &bone->keyFramesPos[j];
	if (pPositionKey->time >= frame)
	{
		pThisPositionKey = pPositionKey;
		break;
	}
	pLastPositionKey = pPositionKey;
}

// Position interpolieren
d = pThisPositionKey->time -
	pLastPositionKey->time;
s = (frame - pLastPositionKey->time) / d;
vPos = pLastPositionKey->position +
	(pThisPositionKey->position -
	pLastPositionKey->position) * s;
		

Mit der berechneten Rotation und Translation erzeugen Sie wieder die relative Transformations­matrix. Wenn der gerade betrachtete Joint an einem hierarchisch höher angesiedelten Joint befestigt ist, berück­sichtigen Sie wieder dessen Transformation, genau wie bei der Initialisierung. Zuletzt wenden Sie die berechneten Transformationen auf die Vertizes an: Sie erhalten genau die Animation, die Sie vorher in Milkshape 3D angelegt haben! Um bei der Animation die korrekte Beleuchtung der 3D-Objekte zu gewährleisten, müssen Sie die Normalen transformieren. Wenden Sie nur die Rotation auf die Normalen­vektoren an, nicht die Translation: Verwenden Sie in matrix.h die Funktion rotate(...) und nicht transform(...).