RuZman

5568

Illustration

Abstract

Aufbauend auf den Blogpost Leap Motion - JavaFX 3D Hand (1) wird die 3D Visualisierung der Hand erweitert. Dazu verbinden wir die einzelnen Spheres mit Cylinder, sodass wir ein noch realistischeres Skelett darstellen können.

JavaFX 3D - Rotation

Im ersten Schritt wird aufgezeigt wie in JavaFX 3D Cylinder verschoben und rotiert werden. Hierfür erstellen wir die Klasse CylinderApp. Dort platzieren wir eine Sphere und einen Cylinder bei [30, 20, 100]. Die Klasse CylinderApp.java gibt folgenden Output:

JavaFX Cylinder mit Sphere

Ersichtlich ist, dass der Ursprung des Cylinders sein Mittelpunkt ist. Aus diesem Grund verschieben wir den Cylinder um seine halbe Höhe nach oben, sodass dieser in der Sphere beginnt.

cylinder.setTranslateY(sphere.getTranslateY() - cylinder.getHeight() / 2);

Lassen wir nun den Cylinder langsam um die Z-Achse rotieren, dreht sich dieser weiterhin um den Mittelpunkt. Deshalb müssen wir den Bezugspunkt (pivot) ans untere Ende des Cylinders setzen. Dadurch dreht sich der Cylinder um die Sphere.

JavaFX Cylinder Rotation JavaFX Cylinder Rotation mit Pivot

Einfachheitshalber steuern wir die Rotation für jede Achse separat. Mit folgendem Code vervollständigen wir die Klasse CylinderApp.java:

Rotate rx = new Rotate(0, 0, cylinder.getHeight() / 2, 0, Rotate.X_AXIS);
Rotate ry = new Rotate(0, 0, cylinder.getHeight() / 2, 0, Rotate.Y_AXIS);
Rotate rz = new Rotate(0, 0, cylinder.getHeight() / 2, 0, Rotate.Z_AXIS);

cylinder.getTransforms().addAll(rx, ry, rz);

JavaFX 3D - Refactoring

Die Klasse HandFX3D.java aus dem Blogpost Leap Motion - JavaFX 3D Hand (1) kann bereits bestimmte Punkte der Hand in JavaFX als ein Verbund von Spheres visualisieren. Um daraus ein realistischeres Skelett zu bauen, werden wir die logisch zusammengehörenden Punkte mit einem Cylinder verbinden. Dazu passen wir den zuvor erstellen Code so an, dass ein Cylinder seine Position, Höhe und Winkel automatisch nach zwei bestimmten Spheres ausrichtet.

Zur Vorbereitung benennen wir die Variable sphere in fromSphere um und erstellen eine identische Sphere bei [-20, -10, -10] mit der Bezeichnung toSphere. Die Initialisierung der Sphere übernimmt vorerst die Methode createSphere().

//...
Sphere fromSphere = createSphere(30, 20, 100);
Sphere toSphere = createSphere(-20, -10, -10);
//...
private Sphere createSphere(double x, double y, double z) {
	Sphere sphere = new Sphere(5);
	addMaterial(sphere);

	sphere.setTranslateX(x);
	sphere.setTranslateY(y);
	sphere.setTranslateZ(z);

	return sphere;
}

JavaFX 3D - Joint Position

Anstatt weiterhin den Setter des Cylinders aufzurufen, binden wir nun die translateProperty an die Position des fromSphere:

private void connect(Cylinder cylinder, Sphere fromSphere, Sphere toSphere) {
	cylinder.translateXProperty().bind(fromSphere.translateXProperty());
	cylinder.translateYProperty().bind(
			fromSphere.translateYProperty().subtract(
				cylinder.heightProperty().divide(2)));
	cylinder.translateZProperty().bind(fromSphere.translateZProperty());
}

Jedes mal, wenn sich fromSphere bewegt, passt cylinder die Position an. Der aktuelle Stand der Klasse sieht folgendermaßen aus: CylinderApp.java.

JavaFX 3D - Joint Höhe

Im nächsten Schritt bestimmen wir die Höhe von cylinder. Die Formel zur Berechnung der Distanz zwischen zwei Punkten im Raum lautet:

Distanz zwischen zwei Punkten im Raum

Das Binding setzen wir mit einer selbst definierten DoubleProperty um:

DoubleBinding height = new DoubleBinding() {
	@Override
	protected double computeValue() {
		return Math.sqrt(Math.pow(toSphere.getTranslateX() - fromSphere.getTranslateX(), 2)
				+ Math.pow(toSphere.getTranslateY() - fromSphere.getTranslateY(), 2)
				+ Math.pow(toSphere.getTranslateZ() - fromSphere.getTranslateZ(), 2));
	}
};
cylinder.heightProperty().bind(height);

JavaFX 3D - Joint Rotation

Der Cylinder liegt auf dem Ausgangspunkt und besitzt die ideale Höhe um den Endpunkt zu erreichen. Jetzt fehlt uns nur noch die Rotationsache und -winkel. Um die Rotationsachse zu finden müssen wir das Kreuzprodukt von zwei Vektoren bilden. In der Leap Motion API findet man unter Vector#cross(Vector other) ein tolles Bild dazu. Den einen Vektor bilden wir aus den Koordinaten der fromSphere und toSphere. Als zweiten Vektor können wir [0, -1, 0] nehmen, da dies dem Vektor des Cylinder entspricht, welcher aktuell noch senkrecht nach oben steht. Dadurch verfälschen wir die Länge des Kreuzprodukts, aber diese spielt für uns ohnehin keine Rolle. Zumindest können wir so ein paar Berechnungen sparen. Vereinfachen wir das Kreuzprodukt per Hand, bleiben nur noch zwei Variablen übrig:

Kreuzprodukt von a und [0, -1, 0]
Rotate rotate = new Rotate();

double dx = fromSphere.getTranslateX() - toSphere.getTranslateX();
double dy = fromSphere.getTranslateY() - toSphere.getTranslateY();
double dz = fromSphere.getTranslateZ() - toSphere.getTranslateZ();

rotate.setAxis(new Point3D(dz, 0, -dx));

Den Winkel setzen wir bequem mit:

rotate.setAngle(180 - new Point3D(dx, dy, dz).angle(new Point3D(0, -1, 0)));

Aktueller Stand der Klasse CylinderApp.java.

Leap Motion - Joints

Obwohl die Properties von JavaFX sich zum Großteil leicht integrieren lassen, verursachen diese einen enormen Overhead. Aus diesem Grund werden die Berechnungen in eine update() Methode gepackt, sodass diese jedes Mal beim Aktualisieren der Leap Motion Daten aufgerufen wird.

public void update() {
	double dx = (float) (fromSphere.getTranslateX() - toSphere.getTranslateX());
	double dy = (float) (fromSphere.getTranslateY() - toSphere.getTranslateY());
	double dz = (float) (fromSphere.getTranslateZ() - toSphere.getTranslateZ());

	bone.setHeight(Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)
			+ Math.pow(dz, 2)));
	bone.setTranslateX(fromSphere.getTranslateX());
	bone.setTranslateY(fromSphere.getTranslateY() - bone.getHeight() / 2);
	bone.setTranslateZ(fromSphere.getTranslateZ());

	joint.setPivotY(bone.getHeight() / 2);
	joint.setAxis(new Point3D(dz, 0, -dx));
	joint.setAngle(180 - new Point3D(dx, -dy, dz).angle(Rotate.Y_AXIS));
}

Vollständiger Code der Klasse HandFX3D.

Demonstration des Skeleton Tracking

Wer sich das Projekt genauer anschauen möchte, kann sich den Quellcode auf GitHub auschecken. Der Tag JavaFX 3D – Hand (2) entspricht dem Stand zum Zeitpunkt dieses Blogposts. Interessierte können sich auf gerne die Demonstration des Projekts auf Youtube anschauen: