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:
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:

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: