Beispiel: Zeichnen mittels Java3D


Inhalt:

"Installation" und Projekt einrichten
Einstieg in Zeichenoperation
Licht an!
Interaktion

Hier wird ein alternatives Zeichnen mittels Java3D in Grundlagen vorgestellt. Der Code der drei untenstehenden Beispiele findet sich hier: Java3DTest.zip

Homepage von Java3D: https://java3d.dev.java.net/

Tutorials:
Die volle Breitseite mit viel Theorie:
http://java.sun.com/developer/onlineTraining/java3d/
Schnelleinstieg:
http://www.diplom-informatikerin.de/tutorials/java-3d.html

Anmerkung:
Es ist natürlich völlig übertrieben, mit 3D-Kanonen auf 2D-Spatzen zu schießen, und wir handeln uns viele Probleme (z.B. Mapping von Koordinaten der virtuellen Welt auf Pixelkoordinaten) ein. Aber vielleicht findet sich ja jemand, der 3D-Diagramme zeichnen will ;-).


"Installation" und Projekt einrichten

Wir verzichten natürlich auf Installer (pfui!) und entpacken die Dateien selbst! Wir finden sie hier:
https://java3d.dev.java.net/binary-builds.html
Aktuell ist Version 1.5.2. Für Standard-Windows-Systeme ist "j3d-1_5_2-windows-i586.zip" interessant. Zu empfehlen ist außerdem die API-Doc "j3d-1_5_2-api-docs.zip".

Das Java3D-Paket "j3d-1_5_2-windows-i586.zip" enthält eine Datei "j3d-jre.zip". Diese wird irgendwohin entpackt.

Im Eclipse-Projekt werden die drei Dateien "j3dcore.jar", "j3dutils.jar" und "vecmath.jar" aus dem Verzeichnis "lib\ext" dem "Build Path" zugefügt:
Java3D-Libraries
Optional können wir bei den Libraries den JavaDoc-Pfad angeben. Dazu dreimal die Unterknoten "Javadoc location" auswählen und den Pfad zum Javadoc-Paket-Verzeichnis angeben:
Java3D-Javadoc

Beim Ausführen müssen die Java3D-DLLs dem Path zugefügt werden. Dies könnte man natürlich über die System-Umgebungsvariable "PATH" machen, eleganter ist es aber, dies in der "Run Configuration" des Eclipse-Projekts zu tun.
Dazu wird auf dem Karteireiter "Environment" eine neue Environment Variable "PATH" zugefügt, die auf das "bin"-Verzeichnis der Java3D-Implementation zeigt.
Java3D-DLLs
Tun wir das nicht, führt es beim Start zu folgender Exception:
06.05.2009 18:45:31 javax.media.j3d.NativePipeline getSupportedOglVendor
SCHWERWIEGEND: java.lang.UnsatisfiedLinkError: no j3dcore-ogl-chk in java.library.path
Exception in thread "main" java.lang.UnsatisfiedLinkError: no j3dcore-d3d in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1682)
	at java.lang.Runtime.loadLibrary0(Runtime.java:823)
	at java.lang.System.loadLibrary(System.java:1030)
	at javax.media.j3d.NativePipeline$1.run(NativePipeline.java:231)
	at java.security.AccessController.doPrivileged(Native Method)
	at javax.media.j3d.NativePipeline.loadLibrary(NativePipeline.java:200)
	at javax.media.j3d.NativePipeline.loadLibraries(NativePipeline.java:157)
	at javax.media.j3d.MasterControl.loadLibraries(MasterControl.java:987)
	at javax.media.j3d.VirtualUniverse.(VirtualUniverse.java:299)
	at de.fhw.swprojekt.knauf.java3d.Java3dTestFrame.(Java3dTestFrame.java:63)
	at de.fhw.swprojekt.knauf.java3d.Java3dTestFrame.main(Java3dTestFrame.java:185)


Einstieg in Zeichenoperation

Das Beispiel "de.fhw.swprojekt.knauf.java3d.Java3dTestFrame" zeigt einige Grundlagen für Java3D.

Initialisierung
Es wird ein javax.media.j3d.Canvas3D erzeugt und dem aktuellen Fenster zugefügt, das die 3D-Welt darstellt. Dem Canvas wird die Default-Konfiguration übergeben.
import java.awt.GraphicsConfiguration;
import javax.media.j3d.Canvas3D;
import com.sun.j3d.utils.universe.SimpleUniverse;

public class Java3dTestFrame extends JFrame
{
  ...
  public Java3dTestFrame(String title)
  {
    ...
    GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
    
    Canvas3D canvas3D = new Canvas3D (config);
    add(canvas3D);
Das 3D-Modell wird in einem Universum gehalten. Für uns reicht ein com.sun.j3d.utils.universe.SimpleUniverse.
    SimpleUniverse universe = new SimpleUniverse(canvas3D);
    universe.getViewingPlatform().setNominalViewingTransform();
Der Aufruf von setNominalViewingTransform sorgt dafür, dass der Abstand des Betrachters von der x/y-Ebene so ist, dass ein Bereich von -1/+1 der x-Achse eingesehen werden kann (die y-Achse ist natürlich eingeschränkt, da der Monitor nicht quadratisch ist).

Abschließend erfolgt ein wenig Konfiguration des Canvas3D bzw. der darin enthaltenen Default-View auf das Modell:
canvas3D.getView().setSceneAntialiasingEnable(true);
Dieser Aufruf aktiviert Antialiasing.


Die so erzeugte Ansicht hat die Besonderheit, dass die Szenerie beim Fenster-Verkleinern skaliert wird, so dass man immer das gleiche Bild sieht.


Alternative: Feste Distanz zur View
import javax.media.j3d.View;
    ...
    canvas3D.getView().setWindowEyepointPolicy(View.RELATIVE_TO_WINDOW);
Das Ändern der "WindowEyepointPolicy" auf View.RELATIVE_TO_WINDOW (oder View.RELATIVE_TO_SCREEN, hier habe ich keinen Unterschied gesehen) sorgt dafür, dass unabhängig von der Fenstergröße die Z-Position des Betrachters konstant bleibt, und man deshalb beim Verkleinern des Fensters weniger von der 3D-Landschaft sieht (der Rahmen des Fensters ist dann sozusagen ein größeres oder kleineres "Guckloch" direkt vor dem Auge). Würde man das nicht setzen, wäre das Defaultverhalten, dass man immer den Ausschnitt von "-1" bis "+1" der x-Achse des Universums sieht und die "Kamera" deshalb näher heranfährt oder weiter herauszoomt (das Bild also skaliert wird). Allerdings habe ich den Eindruck, dass dies sich immer auf einen Bildschirm der Größe 1280x1024 bezieht, denn an den FH-Rechnern haben ich nicht das volle Bild gesehen, und das Herunterschalten der Auflösung zeigte ebenfalls nicht das volle Bild.

Bei dieser "WindowEyepointPolicy" ändert sich der Sichtbereich beim Fenster-Verschieben nicht, d.h. egal wohin wir das (nicht maximierte) Fenster schieben, wir sehen immer den gleichen Ausschnitt. Dagegen helfen folgende zwei Aufrufe:
    canvas3D.getView().setWindowMovementPolicy(View.VIRTUAL_WORLD);
    canvas3D.getView().setWindowResizePolicy(View.VIRTUAL_WORLD);
Jetzt ändert sich der sichtbaren Ausschnitt beim Fenster-Verschieben/vergrößern so, als würde man eine Lupe über ein Blatt Papier schieben.
Anmerkung: das Bild zeichnet sich nur, wenn man das Fenster einmal auf die Taskleiste minimiert und dann wiederherstellt.

Viewbereich vergrößern
Wir können den sichtbaren Bereich vergrößern/verkleinern, indem wir vor dem Aufruf von setNominalViewingTransform() das "Field of View" ändern (dieser Wert geht beim Aufruf von "setNominalViewingTransform()" in die Berechnung des benötigten Abstands zur Z-Achse ein).
    canvas3D.getView().setFieldOfView(0.1);
    universe.getViewingPlatform().setNominalViewingTransform();
Der Default des "Field of View" ist "Pi / 4". Ein Wert von z.B. "0.1" sorgt für ein kleineres Bild (herauszoomen).
Dies funktioniert NUR, wenn die "WindowEyepointPolicy" auf View.RELATIVE_TO_WINDOW oder View.RELATIVE_TO_SCREEN steht!


Initialisierung des Modells
Jetzt sind wir bereit dafür, den "Scene Graph" zu definieren. Dazu werden Renderobjekte zugefügt. Jedes zugefügte Objekt hängt zuerst nur am Nullpunkt und wird mittels Transformationen an die richtige Position geschoben (und gedreht und was sonst noch an Transformationen möglich ist). Man kann eine Transformation für eine ganze Gruppe von Objekten gleichzeitig durchführen.
Das Java3D-Objektmodell ist eine Baumstruktur von javax.media.j3d.Node-Objekten, und jeder dieser Nodes kann beliebig viele Children haben. Ein Child kann ein graphisches Objekt (eine Subklasse von com.sun.j3d.utils.geometry.Primitive) sein, oder eine Transformation (javax.media.j3d.TransformGroup), oder eine Gruppen von Objekten, die als Container für einen eigenständigen Teil des Baums (= "Subgraph") dient (javax.media.j3d.BranchGroup). Aus diesem Modell ergibt sich, dass z.B. Transformationen beliebig tief ineinander geschachtelt sein können.

Ein Renderobjekt definiert sich wohl über Dreiecke. Je mehr davon es hat, desto mehr Details lassen sich herausarbeiten. Im Package com.sun.j3d.utils.geometry finden wir ein paar Standardobjekte, die wir im folgenden benutzen.

Als ersten Schritt erzeugen wir uns eine javax.media.j3d.BranchGroup, die die Wurzel der Objekthierarchie bildet, und hängen sie ins Universum:
import javax.media.j3d.BranchGroup;
...
    BranchGroup branchgroup = new BranchGroup();
	...
    universe.addBranchGraph(branchgroup);
Wichtig ist, dass wir die Branchgroup erst ins Universum hängen dürfen, wenn wir sie fertig aufgebaut haben (mehr dazu im nächsten Beispiel!)

Simple Objekte: Quader
Folgendes Codeschnipsel erzeugt eine grüne Box:
import java.awt.Color;
import javax.media.j3d.Appearance;
import javax.media.j3d.ColoringAttributes;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import com.sun.j3d.utils.geometry.Box;

    ...
    Appearance appearanceGreen = new Appearance();
    ColoringAttributes coloringAttributesGreen = new ColoringAttributes();
    coloringAttributesGreen.setColor(new Color3f(Color.green));
    appearanceGreen.setColoringAttributes(coloringAttributesGreen);
    
    Box box = new Box (0.2f, 0.1f, 0.2f, appearanceGreen);

    Transform3D transform3dBox = new Transform3D();
    //Um 20% nach rechts schieben:
    transform3dBox.setTranslation(new Vector3d (0.6f,0.0,0));
    
    TransformGroup transformGroupBox = new TransformGroup(transform3dBox);

    transformGroupBox.addChild(box);
Es wird zuerst eine Appearance erzeugt, die die Erscheinung aller vier Seiten angibt, die ColoringAttributes werden auf grün gesetzt.
Anschließend wird eine Box erzeugt, wobei die Konstruktorparameter Höhe, Breite und Tiefe angeben. Eine Höhe von 0 wurde ein flaches gefülltes Viereck bauen.
Diese Box wird über eine Transform3D um 0.6 nach rechts geschoben (setTranslation definiert eine Verschiebung der Transformation. Dazu muss man die Box der Node-Subklasse Transform3D zufügen. Die Transformation wird in eine Gruppe TransformGroup gepackt, und dieser Gruppe wird außerdem die Box angehängt.

Am Ende wird die TransformGroup in die Wurzel-Branchgroup des Universums gepackt:
   branchgroup.addChild(transformGroupBox);


Simple Objekte: Kegel
Folgendes Codeschnipsel erzeugt einen blauen Kegel, der links der x-Achse und unterhalb der y-Achse liegt und außerdem um 45 Grad auf den Betrachter zugedreht ist.
import com.sun.j3d.utils.geometry.Cone;
...
    ...
    Appearance appearanceBlue = new Appearance();
    ColoringAttributes coloringAttributesBlue = new ColoringAttributes();
    coloringAttributesBlue.setColor(new Color3f(Color.blue));
    appearanceBlue.setColoringAttributes(coloringAttributesBlue);
    
    Cone cone = new Cone (0.2f, 0.8f, appearanceBlue);

    Transform3D transform3dCone = new Transform3D();
    transform3dCone.setTranslation(new Vector3d (-0.2f,0, 0));
    //Um 45 Grad an der x-Achse rotieren => Spitze geht auf User zu.
    transform3dCone.rotX( Math.PI / 4);
    
    TransformGroup transformGroupCone = new TransformGroup(transform3dCone);

    transformGroupCone.addChild(cone);
Einzige Besonderheit ist die Rotation, die die die Transformation über rotX eingebaut wird.


Simple Objekte: Text
Folgendes Codeschnipsel erzeugt einen weißen Text mit Schriftgröße 16 und Fettschrift, der in der oberen Bildhälfte liegt.
import com.sun.j3d.utils.geometry.Text2D;
...
    ...
    Color3f colorWhite = new Color3f(Color.WHITE);
    Text2D text2D = new Text2D("Keks", colorWhite, "TimesNewRoman", 16, Font.BOLD);
    
    Transform3D transform3dText = new Transform3D();
    transform3dText.setTranslation(new Vector3d (0, 0.4f, 0));
    
    TransformGroup transformGroupText = new TransformGroup(transform3dText);

    transformGroupText.addChild(text2D);


Simple Objekte: Linie
Folgendes Codeschnipsel erzeugt eine grüne Linie, die von links unten nach rechts oben verläuft.
import javax.media.j3d.LineArray;
import javax.media.j3d.Shape3D;
import javax.vecmath.Point3f;
	
    // Linie von "-1/-1/0" zu "+1/+1/0":
    Point3f[] points = new Point3f[2];
    points[0] = new Point3f(-1.0f, -1.0f, 0.0f);
    points[1] = new Point3f(1.0f, 1.0f, 0.0f);
    LineArray lineArray = new LineArray(2, LineArray.COORDINATES);
    lineArray.setCoordinates(0, points);
    Shape3D shapeLine = new Shape3D(lineArray, appearanceGreen);
    
    // neue Transformgruppe
    TransformGroup transformGroupLine = new TransformGroup();

    //Cone an Transformgruppe hängen
    transformGroupLine.addChild(shapeLine);
Die Linie wird als Shape3D beschrieben, diesem wird ein Array von allen Punkten der Linie übergeben.
Quelle:
http://www.java-tips.org/other-api-tips/java3d/how-to-draw-lines-with-java3d.html

Anmerkung: für echte 3D-Objekte hätte man ein Shape3D aus einem javax.media.j3d.TriangleArray erzeugen müssen.

Man beachte, wie die Linie und der Kegel sich berühren. That's 3D!


Licht an!

Die Klasse Java3dLightFrame zeigt Grundlagen für Beleuchtung.
Das Konzept ist einfach: Es werden Lichtquellen definiert, die Licht einer bestimmten Farbe abgeben und einen bestimmten Bereich ausleuchten. Wichtig ist allerdings, dass für jedes 3D-Objekt eine Oberfläche definiert werden muss, die festlegt, wie es bei indirektem oder direktem Lichteinfall aussieht.

Licht
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.DirectionalLight;
import javax.vecmath.Color3f;
    ...
    BoundingSphere worldBounds = new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 1000.0);
    
    DirectionalLight light = new DirectionalLight();
    light.setInfluencingBounds(worldBounds);
    light.setColor (new Color3f (Color.white));
    light.setEnable(true);
	
    branchgroup.addChild(light);
Hier wird ein DirectionalLight definiert, das per Default über dem Universum steht und parallele Lichtstrahlen senkrecht auf die x/y-Ebene strahlt (es kommt also im Prinz von da, wo auch das Auge des Betrachters liegt). Das Licht hat die Farbe weiß und ist eingeschaltet. Außerdem muss ein Bereich definiert sein, auf den das Licht wirkt (die "Influencing Bounds"). Im Beispiel wird eine BoundingSphere definiert, die am Nullpunkt liegt und einen Radius von 1000 hat (also das gesamte Universum umfassen sollte).
Das Licht wird am Ende der Wurzel-Branchgroup zugefügt, und diese kommt ins Universum.


Cone
Im Folgenden wird der Kegel aus dem obigen Beispiel so definiert, dass er bei indirektem Lichteinfall in hellem Rot erscheinen soll (setDiffuseColor). Bei direkter Beleuchtung soll er weiß werden (setSpecularColor), "weiß" ist auch der Default. Die "Shininess" (Werte zwischen 1 und 128) liegt per Default bei 64. Je niedriger, desto stärker kommt die "SpecularColor" zum Tragen.
import javax.media.j3d.Appearance;
import javax.media.j3d.Material;
import com.sun.j3d.utils.geometry.Cone;
    ...
    Appearance appearanceCone = new Appearance();
    
    Material materialCone = new Material();

    materialCone.setDiffuseColor(new Color3f(0.4f, 0.0f, 0.0f));
    materialCone.setSpecularColor(new Color3f(1.0f, 1.0f, 1.0f));
    
    materialCone.setShininess(1.0f);
    
    appearanceCone.setMaterial(materialCone);
	
    Cone cone = new Cone (0.2f, 0.8f, appearanceCone);
Das Zusammenspiel der Parameter sei dem Forscher überlassen.

Box
Jetzt werden zwei Boxes definiert. Sie haben beide das gleiche Material, liegen aber unterschiedlich zum User. Anhand dieses Beispiels sieht man die Colors besser:
    Appearance appearanceBox = new Appearance();

    Material materialBox = new Material();
    materialBox.setDiffuseColor(new Color3f(0.0f, 0.5f, 0.0f));
    materialBox.setSpecularColor(new Color3f(0.0f, 1.0f, 0.0f));

    appearanceBox.setMaterial(materialBox);

    Box box1 = new Box (0.2f, 0.1f, 0.2f, appearanceBox);

    //Rechtsverschiebung:
    Transform3D transform3dBox1 = new Transform3D();
    transform3dBox1.setTranslation(new Vector3d (0.6f,0.0,0));
    
    TransformGroup transformGroupBox1 = new TransformGroup(transform3dBox1);
    transformGroupBox1.addChild(box1);
    
    Box box2 = new Box (0.2f, 0.1f, 0.2f, appearanceBox);

    //Erst Rotation um die y-Achse, dann Rechtsverschiebung:
    Transform3D transform3dBox2 = new Transform3D();
    transform3dBox2.rotY(Math.PI / 4);
    transform3dBox2.setTranslation(new Vector3d (0.6f,0.3f,0));
    
    TransformGroup transformGroupBox2 = new TransformGroup(transform3dBox2);
    transformGroupBox2.addChild(box2);
Das Ergebnis sieht so aus:
Beleuchtete Boxes
Die obere Box ist "Box 2", also die leicht in y-Richtung gedrehte. Dadurch, dass keine der Flächen einen 90°-Winkel bildet, ist sie nicht voll angestrahlt, deshalb geht die Farbe eher hin zur Diffuse Color.
Die untere Box ist "Box 1", die parallel zur x/y-Ebene liegt. Sie wird voll vom Licht getroffen und ist deshalb (gemäß "Specular Color") hellgrün.

Eine Frage stellt sich noch: im obigen Simpelbeispiel ohne Licht war die linke Seite der Box ebenfalls erkennbar. Wieso sieht man sie hier nicht? Die Erklärung ist einfach: es fällt kein Licht darauf. Das gleiche gilt hier für Box 2, deren untere Seitenfläche ist ebenfalls nicht beleuchtet und deshalb unsichtbar.

Anmerkung
Sobald wir einem Objekt ein Material geben, wird es unsichtbar, sofern kein Licht darauffällt (dann ist es nämlich schwarz).


AmbientLight
Mittels der Klasse javax.media.j3d.AmbientLight könnten wir die Szenerie mit einer ungerichteten Beleuchtung versehen. Dadurch wären wohl auch die nicht beleuchteten Seiten der obigens Boxes sichtbar. Um das Beispiel simpel zu halten, habe ich darauf verzichtet.

Ein detailliertes Beispiel findet sich hier:
http://www.java2s.com/Code/Java/3D/ExAmbientLightillustrateuseofambientlights.htm

Interaktion

In dem Beispiel Java3dInteractionFrame kann man mit der Maus Position und Durchmesser einer Sphere definieren. Ein Rechtsklick löscht alle unter der Maus befindlichen Spheres.

Die Hauptlogik findet hier in einem anonymen java.awt.event.MouseAdapter statt.

Hinzufügen einer Sphere
Zuerst einmal der Code für das Hinzufügen einer Sphere (ohne Kommentare und Debuggingausgaben):
    this.canvas3D.addMouseListener( new MouseAdapter ()
    {
      private double dblStartX = 0.0;
      private double dblStartY = 0.0;
      
      @Override
      public void mousePressed(MouseEvent e)
      {
        Point2d pointMaus = convertMousePositionToWorld(e.getX(), e.getY());
        this.dblStartX = pointMaus.x;
        this.dblStartY = pointMaus.y;
        
		...Hier kommt später der Code für "Klick mit rechter Maustaste"
      }

      @Override
      public void mouseReleased(MouseEvent e)
      {
        if (e.getButton() == MouseEvent.BUTTON1)
        {
          Point2d pointMaus = convertMousePositionToWorld(e.getX(), e.getY());
          double dblWidth = pointMaus.x - this.dblStartX;
          
          drawSphere(this.dblStartX, this.dblStartY, dblWidth);
        }
      }
    } );

Im MouseAdapter gibt es zwei Membervariablen, die die Startposition des Mausklicks speichern. Im mousePressed werden sie ermittelt. Die Schwierigkeit ist hier, von Bildschirmkoordinaten auf Echtweltkoordinaten zu kommen. Dies geschieht über eine Hilfsmethode convertMousePositionToWorld, die ich hier gefunden habe:
http://forums.java.net/jive/thread.jspa?messageID=237589.
Diese Methode gibt einen Punkt in Weltkoordinaten auf x/y-Ebene zurück. Die z-Ebene spielt hier keine Rolle.
  private Point2d convertMousePositionToWorld (int iX, int iY)
  {
    Point3d eye_pos = new Point3d();
    Point3d mousePosn = new Point3d();

    //get the eye point and mouse click point
    this.canvas3D.getCenterEyeInImagePlate(eye_pos);
    this.canvas3D.getPixelLocationInImagePlate(iX, iY, mousePosn);

    //Transform from ImagePlate coordinates to Vworld coordinates
    Transform3D motion = new Transform3D();
    this.canvas3D.getImagePlateToVworld(motion);

    motion.transform(eye_pos);
    motion.transform(mousePosn);

    //calculate the intersection point on Z=0
    double dblX = (-eye_pos.z / (mousePosn.z - eye_pos.z)) * (mousePosn.x - eye_pos.x) + eye_pos.x;
    double dblY = (-eye_pos.z / (mousePosn.z - eye_pos.z)) * (mousePosn.y - eye_pos.y) + eye_pos.y;
    
    return new Point2d (dblX, dblY);
  }
Nach dem Loslassen der Maus (mouseReleased) werden ebenfalls die Weltkoordinaten ermittelt, und danach wird die Hilfsmethode drawSphere aufgerufen. Zu beachten ist, dass hier nur etwas passiert, wenn die linke Maustaste betroffen ist. Ansonsten würde das Klicken mit der rechte Taste ebenfalls neue Spheres zeichnen.

Spheres zum Modell zufügen
Das Java3D-Modell läßt sich nach dem erstmaligen Erzeugen frei manipulieren. Allerdings gibt es hier einiges zu beachten, da die Manipulation per Default nicht zugelassen ist.
Beim Erzeugen der Wurzel-BranchGroup des Universums müssen allerdings einige "Capabilities" gesetzt werden:
    this.branchgroup.setCapability(BranchGroup.ALLOW_DETACH);
    this.branchgroup.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
    this.branchgroup.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);

Setzt man BranchGroup.ALLOW_CHILDREN_EXTENT nicht, so wird das Hinzufügen von Children zur BranchGroup mit folgender Fehlermeldung quittiert:
Exception in thread "AWT-EventQueue-0" javax.media.j3d.CapabilityNotSetException: Group: no capability to append children
	at javax.media.j3d.Group.addChild(Group.java:287)
	at de.fhw.swprojekt.knauf.java3d.Java3dInteractionFrame.drawSphere(Java3dInteractionFrame.java:252)
	at de.fhw.swprojekt.knauf.java3d.Java3dInteractionFrame.access$2(Java3dInteractionFrame.java:215)
	at de.fhw.swprojekt.knauf.java3d.Java3dInteractionFrame$1.mouseReleased(Java3dInteractionFrame.java:191)
	...
Setzt man BranchGroup.ALLOW_CHILDREN_WRITE nicht, so wird das Löschen von Children aus der BranchGroup mit folgender Fehlermeldung quittiert:
Exception in thread "AWT-EventQueue-0" javax.media.j3d.CapabilityNotSetException: Group: no capability to remove children
	at javax.media.j3d.Group.removeChild(Group.java:373)
	at de.fhw.swprojekt.knauf.java3d.Java3dInteractionFrame$1.mousePressed(Java3dInteractionFrame.java:161)
	...
Die letzte hier definierten Capabilities, BranchGroup.ALLOW_DETACH, ist in meinem Beispiel für die Wurzel-Branchhgroup nicht nötig. Sie wird dann verwendet, wenn man z.B. eine BranchGroup komplett aus ihrem Parent entfernen will, und genau dies passiert beim Löschen einer Sphere (siehe nächster Block), d.h. sie muss bei Sub-Branchgroups definiert sein.
Die Capabilities müssen pro BranchGroup neu definiert werden, sie vererben sich nicht.

Jetzt endlich können wir uns die Methode drawSphere anschauen:
  private void drawSphere (double dX, double dY, double dWidth)
  {
    //Erscheinungsbild des Kreises: gelb.
    Appearance appearanceSphere = new Appearance();
    ColoringAttributes coloringAttributes = new ColoringAttributes();
    coloringAttributes.setColor(new Color3f(Color.yellow));
    appearanceSphere.setColoringAttributes(coloringAttributes);
    
    
    //Die errechnete Breite durch 2 teilen, da wir den Radius angeben müssen.
    float radius = (float) (dWidth / 2.0);
    //Insgesamt soll der Rand aus 100 Segmenten bestehen (je mehr, desto runder sieht sie aus).
    //Der Parameter "primitiveFlags" scheint per Default auf "GenerateNormals" (=1) zu stehen,
    //also übergebe ich das hier.
    Sphere sphereNew = new Sphere (radius, Primitive.GENERATE_NORMALS, 100, appearanceSphere);
    
    Transform3D transformSphere = new Transform3D();
    //x/y verschieben:
    //Bei x-Verschiebung den Radius aufrechnen! Beim y nicht. Z-Koordinate ist natürlich 0.
    transformSphere.setTranslation(new Vector3d (dX + radius,dY,0));
    
    //neue Transformgruppe
    TransformGroup transformGroupSphere = new TransformGroup(transformSphere);
   
    //colorcube an Transformgruppe hängen
    transformGroupSphere.addChild(sphereNew);
    
    //JEDE Sphere muss in eine eigene BranchGroup, damit man löschen kann!
    BranchGroup branchGroupSphereAktuell = new BranchGroup();
    //Hier muss "DETACH" erlaubt sein:
    branchGroupSphereAktuell.setCapability(BranchGroup.ALLOW_DETACH);
    
    branchGroupSphereAktuell.addChild(transformGroupSphere);
    
    //Und diese komme in Gesamt-Branchgroup:
    this.branchgroup.addChild(branchGroupSphereAktuell);
  }
Wichtig ist hier, dass wir jede Sphere in eine eigene BranchGroup packen! Tun wir das nicht, ist ein Hinzufügen von Subknoten nicht möglich (weil das Universum bereits compiliert wurde und danach nicht mehr manipulierbar ist):
Exception in thread "AWT-EventQueue-0" javax.media.j3d.RestrictedAccessException: Group: only a BranchGroup node may be added
	at javax.media.j3d.Group.addChild(Group.java:284)
	at de.fhw.swprojekt.knauf.java3d.Java3dInteractionFrame.drawSphere(Java3dInteractionFrame.java:251)
	at de.fhw.swprojekt.knauf.java3d.Java3dInteractionFrame.access$2(Java3dInteractionFrame.java:212)
	at de.fhw.swprojekt.knauf.java3d.Java3dInteractionFrame$1.mouseReleased(Java3dInteractionFrame.java:188)
	...
Beim Erzeugen der Sphere wurde ein Konstruktor verwendet, der den Parameter "divisions" enthält. Grund hierfür: per Default wird die Sphere aus wenigen Dreiecken gezeichnet und sieht kantig aus. Der Wert "100" macht es war nicht gerade schneller, aber dafür schön rund.


Entfernen einer Sphere
Die Vorgehen ist so, dass man sich eine Subklasse von javax.media.j3d.PickShape definiert, das den angeklickten Punkt (auf der x/y-Ebene, also mit z=0) umfasst. In meinem Fall ist das ein javax.media.j3d.PickBounds-Objekt, das genau einen Pixel umfasst. Die Wurzel-BranchGroup kann über branchgroup.pickAll die Objekte zurückliefern, die in diesem Bereich liegen. Es kommt ein javax.media.j3d.SceneGraphPath-Array zurück. Über dieses laufen wir. Jeder SceneGraphPath enthält eine Liste von javax.media.j3d.Node, und jeder dieser Nodes ist in unserem Beispiel eine Sphere. Von diesen Spheres holen wir den Parent und wissen, dass das die TransformGroup zur Positionierung ist. Deren Parent ist wiederum eine BranchGroup, und diese können wir aus ihrem Parent, der Wurzel-BranchGroup, entfernen.
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Node;
import javax.media.j3d.PickBounds;
import javax.media.j3d.SceneGraphPath;
import javax.media.j3d.TransformGroup;

import javax.vecmath.Point3d;

      ...
      @Override
      public void mousePressed(MouseEvent e)
      {
        Point2d pointMaus = convertMousePositionToWorld(e.getX(), e.getY());
        this.dblStartX = pointMaus.x;
        this.dblStartY = pointMaus.y;
        
        if (e.getButton() == MouseEvent.BUTTON3)
        {
          Point3d pickPoint = new Point3d (this.dblStartX, this.dblStartY, 0.0);
          PickBounds pickBounds = new PickBounds ( new BoundingBox (pickPoint, pickPoint));
          SceneGraphPath[] pickedItems = branchgroup.pickAll(pickBounds);
          if (pickedItems != null)
          {
            for (int intIndexSceneGraph = 0; intIndexSceneGraph < pickedItems.length; intIndexSceneGraph++)
            {
              for (int intIndexNode = 0; intIndexNode < pickedItems[intIndexSceneGraph].nodeCount(); intIndexNode++)
              {
                //Jeder der Nodes sollte eine Sphere sein:
                Node nodeAktuell = pickedItems[intIndexSceneGraph].getNode(intIndexNode);
                
                //Parent des Knotens ist die TransformGroup:
                TransformGroup transformGroupRemove = (TransformGroup) nodeAktuell.getParent();
                //Deren Parent ist wiederum eine BranchGroup (das weiß ich, weil der Code im "drawSphere"
                //dies so erzeugt:
                BranchGroup branchGroupRemove = (BranchGroup) transformGroupRemove.getParent();
                
                //Und diese BranchGroup können wir aus der Toplevel-Branchgroup des Universums kicken:
                branchgroup.removeChild (branchGroupRemove);
              }
            }
          }
        } //Ende if (rechte Maustaste)
      }



Stand 10.05.2009
Historie:
10.05.2009: Erstellt