Leap Motion - JavaFX 3D Hand (2)
Zoltan Ruzman 5568
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:
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.
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:
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:
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: