TrainDriver

Aus CINEMA 4D Wiki

Wechseln zu: Navigation, Suche

In diesem Artikel wird eine XPresso-Schaltung mit hohem Coffee-Anteil vorgestellt und dokumentiert. Sie ist ideal dafür, einen Eisenbahnzug mit einer beliebigen Anzahl Waggons auf einem Gleis fahren zu lassen. Die Länge von Lok und Waggons, sowie die Abstände der Radpaare lassen sich frei und für jeden Waggon separat definieren.
Beispielmovie (424 KBytes, QuickTime, H.264)

Die Lösung wurde zuerst von Michael "Sneaker" Rottke als reine XPresso-Schaltung entwickelt, und später im Rahmen eines Kundenprojekts von Frank "c4d-Jack" Willeke zum Großteil auf Coffee umgeschrieben. An dieser Stelle nochmal einen Herzlichen Dank an Sneaker, der zuerst auf die Idee gekommen ist. Zu Weihnachten 2008 erschien das Plugin außerdem als OpenSource C++ Version, ebenfalls von Frank Willeke (siehe TrainDriver (Plugin)).


Inhaltsverzeichnis


Aufgabe

Ein Eisenbahnzug mit einer beliebigen Anzahl Waggons unterschiedlicher Länge soll realistisch und leicht kontrollierbar auf einem beliebig gekrümmten Gleis entlangfahren. Die Waggons sollen automatisch hintereinander auf dem Spline platziert werden, und bei der Ausrichtung muss der Abstand der Raadpaare berücksichtigt werden.


Konzept

Positionierung 
Es wird der Reihe nach für jeden Waggon seine Länge und sein Rad-Abstand in einen prozentualen Wert umgerechnet, der die relative Position des jeweiligen Objekts auf dem Spline angibt. Die Startposition und Länge jedes Waggons wird zu einem glboalen Wert hinzuaddiert, so dass die Waggons hintereinander auf dem Spline landen. Hinweis: In der aktuellen Fassung wird die Waggon-Geometrie immer an der Position des vorderen Radpaares platziert.
Ausrichtung 
Beide Radpaare werden jeweils tangential am Spline ausgerichtet. Die Waggon-Geometrie hat ihren Pivot an derselben Position wie das vordere Radpaar, und wird über eine Ziel-Funktion auf das hintere Raadpaar ausgerichtet. Dadurch ergibt sich der Eindruck, dass der Waggon auf seinen beiden drehbaren Radpaaren gelagert ist.

Die Original-Schaltung von Sneaker funktionierte bereits sehr gut, hatte aber auch Nachteile:

  1. Da jeder Waggon eine eigene XPresso-Schaltung hatte, wurde die Ausführung bei sehr langen Zügen träge.
  2. Aufgrund eines Problems mit den Prioritäten hing die Schaltung immer ein Frame hinterher. So brauchte es zwei Klicks auf STOP, um den Zug in seine Anfangsposition zu bringen.
  3. Es wurde kein Rail-Spline unterstützt.

TrainDriver räumt nun mit diesen Nachteilen auf. Es ist performanter, benötigt nur ein Xpresso-Tag pro Zug, und kann per Rail-Spline die Waggons neigen.


Aufbau der Expression

Eigentlich ist TrainDriver eine reine Coffee-Lösung. Die "Verpackung" des Coffee-Codes in ein XPresso-Node dient lediglich dazu, leichter an die benötigten Benutzerdaten zu gelangen.


Objekte

Es werden folgende Objekte benötigt:
TrainDriver Objektstruktur
  1. Ein Pfad-Spline, welches den Verlauf des Gleises beschreibt
  2. Optional ein Rail-Spline, um Kurvenneigungen zu erzeugen. Die Y-Achse der Waggons wird auf das Rail-Spline ausgerichtet.
  3. Ein Null-Objekt, an dem das XPresso-Tag "TrainDriver" hängt, und dem die Waggons untergeordnet werden.
  4. Mindestens ein Waggon. Ein Waggon ist ein Null-Objekt mit drei weiteren untergeordneten Null-Objekten:
    Wheel1 
    Enthält die Geometrie für das vordere Radpaar
    Carriage 
    Enthält die Geometrie für den eigentlichen Waggon
    Wheel2 
    Enthält die Geometrie für das hintere Radpaar


Benutzerdaten

TrainDriver Tag

Das XPresso-Tag "TrainDriver" benötigt folgende Benutzerdaten:
TrainDriver Tag Benutzerdaten
Path Spline 
LINK
Hier wird das Pfad-Spline verlinkt.
Rail Spline 
LINK
Hier wird das optionale Rail-Spline verlinkt.
Spline Position 
FLOAT
Ein Slider ist praktisch hier. Maximalwert darf ruhig über 100% liegen.
Circular 
BOOLEAN
Aktiviert man diese Checkbox, kann der Zug im Kreis auf dem Spline fahren. Das bedeutet, wenn der Wert Spline Position über 100% steigt, verschwinden die Waggons am Ende des Splines und tauchen am Spline-Anfang wieder auf.


Waggon Objekt

Jedes Waggon-Nullobjekt benötigt die folgenden Benutzerdaten, um seine Eigenschaften zu beschreiben:
Waggon Benutzerdaten
Distance_RearWheels 
FLOAT
Gibt die Entfernung zwischen vorderem und hinterem Radpaar an.
Waggon_Length 
FLOAT
Gibt die Entfernung vom vorderen Raadpaar an, in welcher der nächste Waggon folgt.


Schaltung

Die XPresso-Schaltung für TrainDriver
Die Schaltung ist sehr klein, da lediglich Daten in das Coffee-Node "Train Controller" übertragen werden.


Coffee-Code

Der folgende Code kommt in das Coffee-Node "Train Controller" hinein.

Zuerst eine Hilfsfunktion:

CircularVal(Value, Circ)
{
if (Circ)
{ // Remove values >= 1, to get always a value between 0..1
return Modulo(Value, 1.0);
}
else
{ // Clamp value between 0..1
return Clamp(0.0, 1.0, Value);
}
}

Diese Funktion sorgt dafür, dass Werte > 1.0 auf ihre Kommastellen reduziert werden. Aus 1.25 wird z.B. 0.25, aus 3.76 wird 0.76, usw. Die Funktion wird bei aktivierter Circular-Option verwendet, um den Zug im Kreis auf dem Spline entlangfahren zu lassen.


Noch eine Hilfsfunktion:

HPBTarget(CurrPos, TargetPos, UpVector)
{
var p1 = CurrPos;
var p2 = TargetPos + p1;
var p3 = UpVector;

var m = new(Matrix);

m->SetV0(p1);
m->SetV3(vnorm(p2 - p1));
m->SetV1(vnorm(vcross(m->GetV3(), p1 - p3)));
m->SetV2(vnorm(vcross(m->GetV3(), m->GetV1())));

return m->GetHPB();
}

Diese Funktion ist ein ganz simples Target mit UpVektor. Zuerst wird die Z-Achse (V3) auf das Ziel ausgerichtet, dann wird über das Kreuzprodukt aus der Z-Achse und dem UpVektor die X-Achse konstruiert. Als letztes wird die Y-Achse aus dem Kreuzprodukt von Y-Achse und X-Achse konstruiert. Alle Achsen werden normalisiert. Der resultierende HPB-Ausrichtungswinkel wird zurückgegeben.


Darunter die Hauptroutine:

main()
{
// Obj				: Super object of Train group
// PathObj			: Railroad Path Spline
// RailObj			: Railroad Rail Spline
// sPos				: Position of train on Path Spline
// Circ				: Move cars in a circle?


// Offset calculation
// ------------------
var Offset = CircularVal(sPos, Circ);


// Getting the Spline Length
// -------------------------
// Get Path Spline Length
var PathLength = 0.0;
if (PathObj)
{
PathObj->InitLength(0);
PathLength = PathObj->GetLength();
}
// Get Rail Spline Length
var RailLength = 0.0;
if (RailObj)
{
RailObj->InitLength(0);
RailLength = RailObj->GetLength();
}


// Debug printing
/*
println("Train Expression executing...");
println("-----------------------------");
println("Offset: ", Offset);
println("Path Length: ", PathLength);
*/

// Variables
// ---------
var CurrOffset = Offset;		// Used to add up the offset as the cars get chained
var CurrPos = PathObj->GetMg()->GetMulP(PathObj->GetSplinePoint(PathObj->UniformToNatural(CircularVal(CurrOffset, Circ)), 0));

// Main loop
// =========
//
// The loop iterates all child objects of the Train object group.
// It retrieves the cars' properties from the car tags and
// places & aligns the cars and their parts.
//
// Get first car
var car = Obj->GetDown();

while (car && PathObj)
{
// For each car, do...
// -------------------

// Get car data
var CarWheelDist = car#ID_USERDATA:1;	// Get wheel distance
var CarLen = car#ID_USERDATA:2;				// Get car length

// Convert wheel distance to %
var WheelRelDist = CarWheelDist / PathLength;
var CarRelLen = CarLen / PathLength;

// Calculate relative offsets
var BodyRelOffset = CircularVal(CurrOffset, Circ);
var Wheel1RelOffset =  BodyRelOffset;
var Wheel2RelOffset = Wheel1RelOffset - CircularVal(WheelRelDist, Circ);

// Get positions and tangents
var BodyPos = PathObj->GetMg()->GetMulP(PathObj->GetSplinePoint(PathObj->UniformToNatural(BodyRelOffset), 0));
var Wheel1Pos = BodyPos;
var Wheel2Pos = PathObj->GetMg()->GetMulP(PathObj->GetSplinePoint(PathObj->UniformToNatural(Wheel2RelOffset), 0));

var Wheel1Tan = PathObj->GetMg()->GetMulV(PathObj->GetSplineTangent(PathObj->UniformToNatural(Wheel1RelOffset), 0));
var Wheel2Tan = PathObj->GetMg()->GetMulV(PathObj->GetSplineTangent(PathObj->UniformToNatural(Wheel2RelOffset), 0));

// Calculate the UpVectors
var BodyUpVector;
var Wheel1UpVector;
var Wheel2UpVector;
if (RailObj)
{
BodyUpVector = RailObj->GetMg()->GetMulP(RailObj->GetSplinePoint(RailObj->UniformToNatural(BodyRelOffset), 0));
Wheel1UpVector = RailObj->GetMg()->GetMulP(RailObj->GetSplinePoint(RailObj->UniformToNatural(Wheel1RelOffset), 0));
Wheel2UpVector = RailObj->GetMg()->GetMulP(RailObj->GetSplinePoint(RailObj->UniformToNatural(Wheel2RelOffset), 0));
}
else
{
BodyUpVector = BodyPos + vector(0.0, 1.0, 0.0);
Wheel1UpVector = Wheel1Pos + vector(0.0, 1.0, 0.0);
Wheel2UpVector = Wheel2Pos + vector(0.0, 1.0, 0.0);
}


// Position the car's parts

// Get car's parts
var Wheel1 = car->GetDown();		// Front wheels
var Body = Wheel1->GetNext();		// Body
var Wheel2 = Body->GetNext();		// Rear Wheels

// Init matrices
var Wheel1Mtx = new(Matrix);	// Front wheels matrix
var BodyMtx = new(Matrix);			// Body matrix
var Wheel2Mtx = new(Matrix);	// Rear wheels matrix

// Get scales
var CarScale = car->GetScale();
var BodyScale = Body->GetScale();
var Wheel1Scale = Wheel1->GetScale();
var Wheel2Scale = Wheel2->GetScale();


// Alignment
Wheel1Mtx->SetRotHPB(HPBTarget(Wheel1Pos, Wheel1Tan, Wheel1UpVector));
BodyMtx->SetRotHPB(HPBTarget(Wheel1Pos, -vnorm(Wheel2Pos - Wheel1Pos), BodyUpVector));
Wheel2Mtx->SetRotHPB(HPBTarget(Wheel2Pos, Wheel2Tan, Wheel2UpVector));

// Position
Wheel1Mtx->SetV0(Wheel1Pos);
BodyMtx->SetV0(BodyPos);
Wheel2Mtx->SetV0(Wheel2Pos);

// Pass results back to car
car->SetMg(Wheel1Mtx);
Wheel1->SetMg(Wheel1Mtx);
Body->SetMg(BodyMtx);
Wheel2->SetMg(Wheel2Mtx);

// Set scales
car->SetScale(CarScale);
Body->SetScale(BodyScale);
Wheel1->SetScale(Wheel1Scale);
Wheel2->SetScale(Wheel2Scale);


// Debug printing
/*
println(" ");
println(car->GetName());
println("     Car Spline Offset: ", Wheel1RelOffset);
println("     Car Length: ", CarLen);
println("     Car Length (%): ", CarRelLen);
println("     Wheel Distance: ", CarWheelDist);
println("     Wheel 2 Distance (%): ", WheelRelDist);
println("     Wheel 2 Offset (%): ", Wheel2RelOffset);
println("     Wheel 2 Distance (!): ", Wheel1RelOffset - Wheel2RelOffset);
*/

// Pass on CurrOffset
CurrOffset = CurrOffset - CarRelLen;
CurrPos = BodyPos - BodyMtx->GetV3() * CarLen;

// Continue with next car
car = car->GetNext();
}

// Free Spline length
PathObj->FreeLength();
if (RailObj) { RailObj->FreeLength(); }
}


Probleme / Bugs / Verbesserungsmöglichkeiten

  1. Wenn die Option "Circular" aktiviert ist, und die Lok exakt bei 100% des Splines steht, schlägt die Ausrichtung per Rail-Spline fehl.
  2. Wenn das Pfad-Spline sehr wenig Stützpunkte enthält, wird (scheinbar aufgrund einer Fließkomma-Ungenauigkeit) die Position der nachfolgenden Wagen nicht korrekt berechnet. Lösung: Die Spline-Funktion "Runden" auf das Spline anwenden, um für eine größere Anzahl Stützpunkte zu sorgen.
  3. Die Berechnung des Abstandes des hinteren Radpaares ist nicht korrekt, wenn die Skalierung des Splines nicht (1,1,1) ist. Entsprechend ist dann auch die Ausrichtung der Waggon-Geometrie falsch. Lösung: Splineskalierung immer auf (1,1,1) zurücksetzen.
  4. Die Expression funktioniert nicht in NETrender, weil dort der Ausdruck
    CarTag#ID_USERDATA:1;
    lediglich eine Fehlermeldung produziert. Scheinbar werden Benutzerdaten in NETrender anders behandelt, jedenfalls stehen Sie für Coffee nicht zur Verfügung. Die C++ Version hat diese Einschränkung nicht.
  5. Es wäre praktischer, die Benutzerdaten für die Waggon-Eigenschaften an ein Tag zu hängen, anstatt direkt an das Nullobjekt. So müsste man lediglich das Tag auf einen neuen Waggon kopieren. Die könnte recht simpel über den folgenden Code gelöst werden:
var CarTag = car->GetFirstTag();
var CarLength = CarTag#ID_USERDATA:1;
Allerdings gibt die Methode GetFirstTag() nur im Editor ein Tag zurück, beim Rendern funktioniert es nicht.
In der C++ Version wurde alles über Tags gelöst.


C++ Version

Siehe TrainDriver (Plugin).


Download

Das fertige XPresso-Setup kann hier heruntergeladen werden:


Lizenz

  • TrainDriver ist OpenSource.
  • Es wird keine Garantie auf fehlerfreie Funktion, oder Gewährleistung bei Schäden bzw. Datenverlust gegeben.





essay writer

Persönliche Werkzeuge