Technische Universität München Institut für Informatik Lehrstuhl für Computer Graphik & Visualisierung Praktikum: Grundlagen der Programmierung Prof. R. Westermann, A. Lehmann, R. Fraedrich, F. Reichl WS 2010 Lösungsblatt 9 Anmerkung: Da viele Fragen zur Diskussion anregen sollen, sind die hier vorgestellten Antworten lediglich als Lösungsskizze zu verstehen. Vererbung II, generische Datentypen 9.1 (Ü) Allgemeine Fragen zu Vererbung (a) Welche Sichtbarkeiten gibt es in Java? Tragen Sie die zugehörigen Schlüsselworte sowie die daraus resultierenden Zugriffsberechtigungen in die folgende Tabelle ein. Schlüsselwort Klasse Package Erbende Klassen Global (b) Wieviele Ober- und wieviele Unterklassen kann eine Klasse haben? (c) Was sind abstrakte Methoden? (d) Was ist Overriding? Was ist dynamische Bindung? (e) Betrachten Sie den Code in folgender Klasse (Grundlage sind die bekannten Klassen des Vehicle-Beispiels von Blatt 8): de.tum.ws2010.propra.blatt09.vehicletesting.VehicleTest Was passiert in diesem Code-Abschnitt? Wie sieht die Ausgabe auf der Konsole aus? for ( int i =0; i < vehicles . length ; i ++) { System . out . println ( vehicles [ i ]. toString ()); } Wieso wird offensichtlich für jedes Objekt eine andere Methode aufgerufen, obwohl alle Referenzen des Arrays vom Typ Vehicle sind? (f) Lässt sich durch Casts eine überschriebene Methode der Superklasse aufrufen? 1 Lösungsvorschlag (a) Weitere Informationen finden Sie z.B. hier: http://download.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html Schlüsselwort public protected (default) private Klasse Package Erbende Klassen Global + + + + + + + + + + - (b) Maximal eine Oberklasse (singuläre Vererbung ) und beliebig viele Unterklassen. (c) In abstrakten Klassen können abstrakte Methoden deklariert werden. Diese haben – analog zu den Methoden-Signaturen in Interfaces – keinen Rumpf, sondern geben lediglich an, dass jede von der abstrakten Klasse abgeleitete (nicht-abstrakte) Klasse eine Implementierung für diese Methode bereitstellen muss. Der Vorteil liegt darin, dass diese Methode auch von Referenzen des Typs der abstrakten Klasse aufgerufen werden kann. Grund dafür ist, dass jedes Objekt, das dieser Referenz zugewiesen werden kann, die Methode implementieren muss. (d) Als Overriding bezeichnet man das Überschreiben einer aus einer Superklasse ererbten Methode, d.h. das Deklarieren einer Methode mit identischer Signatur. Eine solche Methode wird in der Regel (analog zur Implementierung von Methoden eines Interfaces) mit @Override gekennzeichnet. Wird diese Methode für ein Objekt der erbenden Klasse aufgerufen, so wird diese neue Methode ausgeführt. Dabei spielt es keine Rolle, von welchem Typ die Referenz ist, da erst zur Laufzeit die richtige Methode für das entsprechende Objekt ausgewählt wird. Dies bezeichnet man als sogenannte Dynamische Bindung. (e) In der Schleife werden die vier zuvor in das Array eingefügten Elemente durchlaufen und dabei mittels ihrer toString()-Methoden auf der Konsole ausgegeben: Hello, Hello, Hello, Hello, I’m I’m I’m I’m a a a a car! train! ship! plane! Hier handelt es sich um ein Beispiel für dynamische Bindung in Java - es wird nicht die toString()-Methode für Vehicle, sondern für die jeweilige tatsächliche Instanz aufgerufen. Die toString()-Methode ist hierbei bereits in der Klasse Object deklariert, von der in Java alle Klassen implizit erben. (f) Nein. 2 9.2 (Ü) Adressbuch Auf Übungsblatt 6 wurde ein Verzeichnis für Freunde mit einer einfach verketteten Liste realisiert. Im Folgenden soll nun ein komplexeres Adressbuch für verschiedene Arten von Einträgen erstellt werden. Es sollen Firmen, Kollegen und private Kontakte gespeichert werden können. Eine Firma hat einen Firmennamen und eine Homepage. Kollegen haben einen Vor- und Nachnamen, eine Email-Adresse und eine Raumnummer. Private Kontakte haben einen Vor- und Nachnamen, eine Email-Adresse sowie einen Geburtstag (verwenden Sie hierfür die vorgefertigte Klasse Birthday). Alle zu erstellenden Klassen sollen sich im Package de.tum.ws2010.propra.blatt09.addressbook befinden. (a) Alle Kontakteintragtypen werden von der abstrakten Klasse AddressBookItem abgeleitet. Sehen Sie sich nun die vorgefertigen Klassen AddressBookItem und Firm an. i) Was bedeutet extends AddressBookItem? ii) Was bedeutet implements Comparable? iii) Wieso ist AddressBookItem kein Interface? iv) Welche Methoden sind in der Klasse Firm sichtbar? v) Welche in AddressBookItem? (b) Die Einträge des Adressbuches können in einem (vorgegebenen) sortierten AddressBook gespeichert werden. Fügen wir nun einige Firmenkontakte in das Adressbuch ein: public class TestAddressBook { public static void main ( String [] args ) { AddressBook book = new AddressBook (); book . a dd Ad dr es sB oo kI te m ( new Firm ( " SUN " ," www . sun . com " )); book . a dd Ad dr es sB oo kI te m ( new Firm ( " Canonical Ltd . " ," www . ubuntu . com " )); book . a dd Ad dr es sB oo kI te m ( new Firm ( " IBM " ," www . ibm . com " )); System . out . println ( book ); } } Sehen Sie sich die Methode AddressBook.toString() an. Erklären Sie, wie sie funktioniert! Wieso wird anscheinend Firm.toString() aufgerufen und nicht AddressBookItem.toString()? (c) Erarbeiten Sie zusammen mit Ihrem Tutor, wie mit den Kollegen und privaten Kontakten verfahren werden muss. Fügen Sie eventuell eine neue (abstrakte) Klasse in die Hierarchie ein. Implementieren Sie anschließend alle benötigten Klassen! (d) Fügen Sie nun auch ein paar Kollegen und private Kontakte in Ihr Adressbuch ein: public class TestAddressBook { public static void main ( String [] args ) { 3 AddressBook book = new AddressBook (); book . a dd Ad dr es sB oo kI te m ( new Firm ( " SUN " ," www . sun . com " )); book . a dd Ad dr es sB oo kI te m ( new Firm ( " Canonical Ltd . " ," www . ubuntu . com " )); book . a dd Ad dr es sB oo kI te m ( new Firm ( " IBM " ," www . ibm . com " )); book . a dd Ad dr es sB oo kI te m ( new Colleague ( " Karl " ," Kollege " , " karl@kollege . de " ," 01.13.52 " )); book . a dd Ad dr es sB oo kI te m ( new PrivateContact ( " Paul " ," Privat " , " paul@privat . de " , new Birthday (24 ,12 ,1960))); System . out . println ( book ); } } Sehen Sie sich die Methode AddressBookList.toString() nochmals an. Erklären Sie, wieso welche toString-Methode in der Schleife aufgerufen wird. Lösungsvorschlag Siehe beiligende .java-Files. Zu (a): i) Firm erbt von AddressBookItem. ii) AddressBookItem (und damit alle abgeleiteten Klassen) implementiert das Interface Comparable. iii) Da ansonsten keine Attribute und Methoden mit Rumpf implementiert werden könnten, die in der erbenden Klasse zur Verfügung stehen. iv) toString, getName, getHomepage, setHomepage, compareTo v) toString (da von Object geerbt), getName, compareTo Zu (b) und (d): Dynamische Bindung! (vgl. Aufgabe 9.1e) 9.3 (Ü) Allgemeine Fragen zu generischen Datentypen (a) Was ist eine generische Klasse? (b) In dem Package de.tum.ws2010.propra.blatt09.generics finden Sie die Klassen StringItem und GenericItem, die einen Eintrag einer verketteten Liste von Strings bzw. Objekten eines generischen Typs darstellen. Wo liegen Unterschiede und Gemeinsamkeiten? StringItem.java: public class StringItem { private String content ; private StringItem next ; public StringItem ( String content , StringItem next ) { this . content = content ; this . next = next ; } 4 public StringItem getNext () { return next ; } public void setNext ( StringItem next ) { this . next = next ; } public String getContent () { return content ; } } GenericItem.java public class GenericItem <T > { private T content ; private GenericItem <T > next ; public GenericItem ( T content , GenericItem <T > next ) { this . content = content ; this . next = next ; } public GenericItem <T > getNext () { return next ; } public void setNext ( GenericItem <T > next ) { this . next = next ; } public T getContent () { return content ; } } (c) Desweiteren gibt es eine Klasse StringSinglyLinkedList, die mit StringItem-Objekten arbeitet. Entwickeln Sie zusammen mit Ihrem Tutor eine analoge Klasse GenericSinglyLinkedList. public class S t r i n g S i n g l y L i n k e d L i s t { private StringItem first ; public void add ( String element ) { StringItem item = new StringItem ( element , null ); if ( first == null ) { first = item ; } else { 5 StringItem curr = first ; while ( curr . getNext () != null ) { curr = curr . getNext (); } curr . setNext ( item ); } } } (d) Erarbeiten Sie außerdem eine Methode boolean contains(T entry) in GenericSinglyLinkedList, die prüft, ob ein Eintrag in der Liste vorhanden ist. Wie können Sie die Objekt-Vergleiche durchführen, ohne zu wissen, von welchem Typ sie sind? (e) Wie wird nun eine GenericSinglyLinkedList von Strings angelegt? Auf was müssen Sie achten, wenn Sie eine Liste mit ganzzahligen Werten anlegen möchten? (f) Java bietet eine große Menge an fertig implementierten generischen Datentypen (Collection) an, so zum Beispiel: LinkedList<E> Queue<E> Stack<E> PriorityQueue<E> ArrayList<E> Sie sind alle in der Java API (http://download.oracle.com/javase/6/docs/api/) beschrieben. Wie legt man eine LinkedList von Strings an? Lösungsvorschlag (a) Häufig werden Klassen (z.B. Datenstrukturen) geschrieben, deren grundsätzliche Anwendungslogik für alle Typen identisch ist. Hier muss abgewogen werden, ob die Typsicherheit (es können nur Strings zu einer String-Datenstruktur hinzugefügt werden) oder die universelle Einsetzbarkeit (die Datenstruktur funktioniert für beliebige Typen - dies lässt sich durch die Wahl des Typs Object erreichen) höhere Priorität hat. Generische Klassen lösen diesen Konflikt: Sie sind universelle Klassen, die die gleiche Funktionalität für verschiedene Typen anbieten. Der gewünschte Typ wird bei der Instanziierung eines Objekts festgelegt. (b) Der grundsätzliche Aufbau ist identisch, allerdings sind alle Vorkommen des Typbezeichners String in GenericItem durch einen Typparameter ersetzt, der bereits in der Deklaration der Klasse vermerkt ist. (c) Alle Vorkommen des Typs String müssen durch einen Typ-Parameter ersetzt werden. Für die Implementierung siehe Lösung (d). (d) Die Vergleichsmethode o1.equals(o2) ist steht für Object und damit für beliebige JavaTypen zur Verfügung. Solange diese Methode nicht überschrieben wird werden analog zum 6 Operator == lediglich die Referenzen der beiden Instanzen verglichen. Sollen wie z.B. bei der Klasse Integer die Werte zweier Instanzen verglichen werden, so muss equals() von der entsprechenden Klasse überschrieben werden. Die fertige generische Liste sieht anschließend folgendermaßen aus: public class GenericSinglyLinkedList <T > { private GenericItem <T > first ; public void add ( T element ) { GenericItem <T > item = new GenericItem <T >( element , null ); if ( first == null ) { first = item ; } else { GenericItem <T > curr = first ; while ( curr . getNext () != null ) { curr = curr . getNext (); } curr . setNext ( item ); } } public boolean contains ( T entry ) { GenericItem <T > curr = first ; while ( curr != null ) { if ( curr . getContent (). equals ( entry )) { return true ; } curr = curr . getNext (); } return false ; } } (e) GenericSinglyLinkedList<String> list = new GenericSinglyLinkedList<String>(); GenericSinglyLinkedList<Integer> list = new GenericSinglyLinkedList<Integer>(); (Achtung: Integer statt int) (f) LinkedList<String> list = new LinkedList<String>(); 9.4 (Ü) Generische Anwendung von Comparable Erinnern Sie sich: Auf Blatt 8 haben Sie das Interface Comparable so implementiert, dass ein Objekt grundsätzlich mit Objekten beliebigen Typs vergleichbar gemacht werden mussten. Für den (eigentlich relevanten) Vergleich mit Objekten desselben Typs – in unserem Fall mit anderen Friend-Objekten – musste zunächst mit dem Operator instanceof geprüft werden, ob es sich um ein solches handelt, und anschließend ein Cast auf die richtige Klasse durchgeführt werden. Betrachten Sie nun die Spezifikation der generischen Version von Comparable: 7 http://download.oracle.com/javase/1.5.0/docs/api/java/lang/Comparable.html Wie müssen Sie Friend umzuschreiben, um seine Instanzen lediglich mit anderen Friend-Objekten vergleichbar zu machen? Lösungsvorschlag Siehe beiligende .java-Files. 9.5 (Ü) Allgemeine Fragen zu Bäumen (a) Was ist ein Binärbaum? Was haben in diesem Zusammenhang die Begriffe Wurzel, innerer Knoten und Blätter zu bedeuten? (b) Wie könnte eine möglichst einfache Java-Klasse aussehen, die einen einzelnen Knoten in einem Binärbaum repräsentiert, von dem alle Kinder und deren Nachkommen erreichbar sein sollen? (c) Ist es notwendig, eine weitere Klasse zu definieren, die einen Baum als Ganzes repräsentiert? (d) Was ist ein binärer Suchbaum? Wie kann man in einem solchen Baum feststellen, ob ein Element enthalten ist? (e) Wie sieht ein binärer Suchbaum für Zahlen aus, in den die Einträge 7, 2, 3, 8, 1 und 4 nacheinander eingefügt werden? Lösungsvorschlag (a) Ein Binärbaum ist eine hierarchische Anordnung von so genannten Knoten. Ausgehend von einem einzelnen Wurzelknoten darf jeder Knoten maximal zwei Knoten als Kinder haben. Knoten, die keine Kinder haben, werden Blätter genannt. Knoten, die weder Wurzel noch Blatt sind, heißen innere Knoten. (b) class BinaryTreeNode { BinaryTreeNode leftChild ; BinaryTreeNode rightChild ; } (c) Wenn man die Wurzel kennt, ist dadurch implizit der komplette Baum eindeutig bestimmt. (d) Ein binärer Suchbaum ist ein sortierter Binärbaum, in dem alle Knoten einen Wert besitzen und zusätzlich die folgenden Eigenschaften haben: aller Knoten im linken Unterbaum haben Werte kleiner als der Wert des aktuellen Knotens 8 alle Knoten im rechten Unterbaum haben Werte größer gleich dem Wert des aktuellen Knotens. (e) Der Binärbaum sieht nach dem Einfügen von 7, 2, 3, 8, 1 und 4 wie folgt aus: 7 2 1 8 3 4 9.6 (Ü) Binärer Suchbaum Erstellen sie eine Klasse IntBinaryTreeNode, die einen Knoten in einem binären Suchbaum repräsentiert. Jeder Knoten soll zusätzlich eine Ganzzahl speichern. Stellen Sie die nötigen Konstruktoren, Getter und Setter bereit, wobei der Wert im Nachhinein nicht verändert werden darf. Der Suchbaum soll wie folgt geordnet sein: Der Wert aller Knoten im linken Unterbaum ist kleiner als der Wert des aktuellen Knotens und alle Knoten im rechten Unterbaum haben Werte größer gleich dem Wert des aktuellen Knotens. Ihre Klasse soll die folgenden Methoden enthalten: (a) public boolean isLeaf() Gibt an, ob der Knoten ein Blatt ist oder nicht. (b) public String toString() Gibt alle Elemente des Baumes in aufsteigend sortierter Reihenfolge als String zurück, wobei die einzelnen Einträge durch ein Komma getrennt werden sollen. Wie muss die Baumstruktur (rekursiv) durchlaufen werden, um die enthaltenen Einträge in der richtigen Reihenfolge auszugeben? (c) public void insert(int data) Mit dieser Methode wird bei einem bestehenden Baum ein neues Datenelement eingefügt. Das Datenelement soll an die richtige Stelle im Baum eingefügt werden, so dass die oben beschriebene Sortierung der Knoten erhalten bleibt. Sie müssen sich nicht um eine gleichmäßige Anordnung der Elemente (d.h. um eine Ausbalancierung des Baums) kümmern – durchlaufen Sie in ihrer Implementierung den Baum einfach bis zu einer Stelle, an der das neue Element als Blatt hinzugefügt werden kann. (d) Schreiben Sie Ihren Baum nun so um, dass er mit Hilfe eines generischen Typ-Parameters für beliebige Typen verwendet werden kann. Welche Bedingung müssen diese Typen erfüllen? Welche Änderungen müssen Sie an der Implementierung vornehmen? Lösungsvorschlag 9 Siehe beiligende .java-Files. Zu (d): Neben dem Ersetzen von int durch einen Typ-Parameter muss die Syntax der Vergleiche angepasst werden. Hier kommt die compareTo-Methode anstatt der Vergleichsoperatoren < und > zum Einsatz. Deshalb muss darauf geachtet werden, dass nur Typen, die das Interface Comparable implementieren, verwendet werden können. 9.7 (H) Binärbaum (++) Verwenden Sie die generische Version des in der Übung implementierten Binärbaums, um AddressBookItems sortiert zu verwalten. Testen Sie die Funktionalität ausführlich. Implementieren Sie anschließend folgende zusätzlichen Methoden für Ihren generischen Binärbaum: (a) public int getNumberOfNodes() Liefert die Zahl der Knoten eines Baumes (einschließlich des Wurzelknotens). (b) public int getHeight() Gibt die Höhe des Baumes zurück, dessen Wurzel dieser Knoten ist. Die Höhe eines Baumes ist definiert als Länge des längsten Pfades von der Würzel zu einem Blatt. Ein Beispiel: Ein Baum, der nur aus der Wurzel besteht, hat also insbesondere die Höhe 0. Ein Baum bestehend aus Wurzel und einem weiteren Knoten hat die Höhe 1. (c) public boolean contains(T request) Prüft, ob ein bestimmter Eintrag in dem Baum vorhanden ist oder nicht. Lösungsvorschlag Siehe beiligende .java-Files. 10 9.8 (H) Raytracer (++) In dieser Aufgabe sollen Sie lernen, mit einem bereits bestehenden Projekt zu arbeiten und dieses um zusätzliche Klassen und Methoden erweitern. Das Ziel ist dabei, einen Raytracer zu implementieren, mit dem man beispielsweise die nachfolgenden Bilder erzeugen kann. Abb. 1: Mit Raytracing generierte Bilder. Der Großteil der Klassen und Methoden ist bereits implementiert und steht in Ihrem Projekt als eingebundene Bibliothek zur Verfügung. Die Dokumentation der verfügbaren Komponenten ist ebenfalls im ZIP-Archiv dieses Projekts enthalten. Ihre Aufgabe wird es sein, verschiedene Arten von Lichtquellen zu implementieren, mit denen die Szenen beleuchtet werden. Alle fehlenden Bestandteile können mit Ihrem aktuellen Kenntnisstand implementiert werden. Die einzelnen Teilaufgaben sind so beschrieben, dass Hintergrundwissen zum Gesamtprojekt nicht zwingend erforderlich ist. Um Ihnen dennoch einen Einblick in die Funktionsweise eines Raytracers zu geben, wird das Grundprinzip im folgenden Abschnitt beschrieben. Achten Sie dabei insbesondere auf die Beschreibung der Beleuchtungsberechung und wie die Verschattung von Lichtquellen bestimmt werden kann. Überblick Raytracing ist ein Verfahren der Computergrafik. Der Name leitet sich davon ab, dass das Bild erzeugt wird, indem Strahlen von der Kamera in die Szene geworfen und verfolgt werden. Mit Hilfe der Strahlen wird festgestellt, welche Objekte zu sehen sind und welchen Farbwert die einzelnen Bildpunkte (Pixel) bekommen sollen. Wie in der Realität hat die Kamera dabei eine Position und Blickrichtung in der Szene, von der ein Bild erzeugt werden soll. Die Kamera betrachtet dabei die Szene durch ein Raster, welches der Auflösung des zu erzeugenden Bildes entspricht. Um zu bestimmen, was durch die einzelnen Pixel hindurch zu sehen ist, wird für jeden Pixel ein Strahl erzeugt, dessen Ursprung die Kameraposition ist und der durch das Zentrum des Pixels verläuft (siehe rote Pfeile in Abbildung 2). 11 Abb. 2: Grundprinzip des Raytracings Anschließend wird für alle Objekte in der Szene (z.B. Kugeln, Würfel, Dreiecke) mathematisch bestimmt, ob es einen oder mehrere Schnittpunkte mit diesem Strahl gibt. Falls der Strahl mehrere Objekte schneidet, ist das Objekt zu sehen, dessen Schnittpunkt die kürzeste Entfernung zur Kamera hat. Im Anschluss an die Schnittpunktbestimmung muss die Farbe bestimmt werden, die dort vom Betrachter aus wahrgenommen wird. Diese hängt zum einen von den Materialeigenschaften (wie z.B. die Farbe) ab und zum anderen, von wo aus dieser Punkt beleuchtet wird und wie wie das Material das einfallende Licht in Richtung des Betrachters reflektiert (wie man z.B. an den weißen Highlights auf den Kugeln in Abbildung 1 erkennen kann). Die Beleuchtung der Szene kann durch verschiedene Arten von Lichtquellen modelliert werden, wie zum Beispiel Punktlichtquellen ähnlich einer Glühlampe oder Richtungslichtquellen, die dem Lichteinfall der Sonne entsprechen. Die Verschattung einer Lichtquelle an einem Punkt kann dadurch geprüft werden, indem von diesem Punkt ein Strahl in Richtung Lichtquelle erzeugt wird. Schneidet dieser Strahl zwischen dem Schnittpunkt und der Lichtquelle eines der geometrischen Objekte in der Szene, ist die Lichtquelle verdeckt und kann deswegen nicht zur Beleuchtung an dieser Stelle beitragen (siehe blaue Pfeile in Abbildung 2). Bei spiegelnden Oberflächen ist der zu sehende Farbwert an einem Schnittpunkt zusätzlich (bei einem perfekten Spiegel sogar ausschließlich) davon abhängig, was durch die Spiegelung in diesem Punkt zu sehen ist (vgl. die Spiegelungen Abbildung 1). Dies kann ganz einfach dadurch bestimmt werden, dass ein Reflektionsstrahl vom Schnittpunkt entlang der Spiegelung erzeugt wird und man wiederum (rekursiv) bestimmt, welches Objekt zu sehen ist und welchen Farbwert der Schnittpunkt hat. Je nach Grad der Verspiegelung trägt der Farbwert aus der Spiegelung oder der lokale Farbwert mehr zur endgültigen Farbe des Pixels bei. Das endgültige Bild entsteht dadurch, dass die Bestimmung vom nähesten Schnittpunkt und Berechnung des Farbwertes für den Schnittpunkt für alle Pixel durchgeführt wird. 12 Aufgabe: Hierarchie von Lichtquellen Im Raytracing-Projekt sind bis auf die Lichtquellen bereits alle notwendigen Klassen enthalten. In der Klasse Raytracer können Sie Eigenschaften wie die zu renderende Szene und die Bildauflösung festlegen. Um das Bild tatsächlich zu Rendern und anzuzeigen stehen Ihnen zwei Varianten zur Verfügung: RaytracerApplet: Startet den Raytracer als Applet. RaytracerSwing: Startet den Raytracer als Anwendung. Um nun Bilder von beleuchteten Szenen erzeugen zu können, sollen vier verschiedene Arten von Lichtquellen zur Verfügung stehen: Abb. 4: V.l.n.r. AmbientLight, DirectionalLight, PointLight und SpotLight. Ihre Aufgabe ist es, sich basierend auf der folgenden Beschreibung eine Klassenhierarchie für die verschiedenen Lichtquellen zu entwerfen und diese anschließend zu implementieren. Light Die abstrakte Klasse Light repräsentiert die Gemeinsamkeit aller Lichtquellen, nämlich dass sie eine Intensität (∈ [0, 1]) besitzen sowie eine Methode public abstract Color illuminate ( Intersection i , Scenery s ) welche die Farbe bestimmt, die am Schnittpunkt aufgrund der Beleuchtung durch die Lichtquelle zu sehen ist. Da diese Berechnung von der Art der Lichtquelle abhängt ist die Methode als abstract deklariert und muss durch die abgeleiteten Klassen implementiert werden. Die Klasse Light ist bereits im Projektrumpf enthalten. AmbientLight Die einfachste aller Lichtquellen ist AmbientLight, welche eine Annäherung des “Umgebungslichts” in einer Szene darstellt und dessen Intensität überall in der Szene identisch ist. Dementsprechend kann eine solche Lichtquelle niemals verschattet sein. Die Klasse soll einen Konstruktor public AmbientLight ( double intensity ) enthalten, der eine Lichtquelle mit der übergebenen Intensität instanziert. 13 DirectionalLight Ein DirectionalLight ist eine unendlich weit entfernte Lichtquelle, welche alle Punkte im Raum aus derselben Richtung beleuchtet (wie z.B. die Sonne auf der Erde). Mit dem Konstruktor public DirectionalLight ( double intensity , Vector3d direction ) soll die Intensität und Richtung der Lichtquelle festlegt werden. Ein DirectionalLight ist verdeckt, wenn der Schattenstrahl vom zu beleuchtenden Punkt in die entgegengesetzte Lichtrichtung irgendein Objekt in der Szene schneidet. PointLight Ein PointLight ist eine Lichtquelle, welche sich an einem bestimmten Punkt im Raum befindet und um sie herum in alle Richtung strahlt (vergleichbar mit einer Glühlampe). Mit Hilfe des Konstruktors public PointLight ( double intensity , Vector3d position ) soll die Intensität und die Position der Lichtquelle gesetzt werden. Ein PointLight ist verdeckt, wenn der Schattenstrahl vom zu beleuchtenden Punkt zur Position der Lichtquelle ein Objekt in der Szene schneidet. SpotLight Ein SpotLight ist eine Lichtquelle ähnlich einer Schreibtischlampe mit Schirm, die also neben einer Intensität und einer Position im Raum auch noch eine Richtung r und einen Abschirmwinkel ρ besitzt. Diese Eigenschaften sollen mit dem Konstruktor public SpotLight ( double intensity , Vector3d pos , Vector3d lookAt , double halfAngle ) gesetzt werden können, wobei der Abschirmwinkel im Bogenmaß (∈ [0, π]) gegeben ist. Ein PointLight ist verdeckt, wenn der Schattenstrahl vom zu beleuchtenden Punkt p zur Position der Lichtquelle l ein Objekt in der Szene schneidet oder der Strahl von l nach p mehr als der Abschirmwinkel ρ von der Lichtrichtung abweicht. Beleuchtungsberechnung Die eigentliche Berechnung der zu sehenden Farbe ist von vielen Faktoren abhängig. Dazu gehören die Intensität der Lichtquelle, die Lichtrichtung, die Blickrichtung und die Ausrichtung der Oberfläche (gegeben durch dessen Normale). Zum anderen hängt die Farbe eines beleuchteten Punktes aber selbstverständlich auch von den Material-Eigenschaften ab, wie der Farbe und die Art und Weise wie das Licht von der Lichtquelle aus über die Oberfläche in die Kamera reflektiert wird (bedenken Sie z.B. die unterschiedliche Erscheinung von Gummi und Metall). Aus diesem Grund wird die eigentliche Beleuchtungsberechnung in der Klasse Material durchgeführt, wofür Ihnen die Methoden shade(...) und shadeAmbient(...) zur Verfügung stehen. Bedenken Sie, dass es im Sinne der Abstraktion für Sie völlig unerheblich ist, wie die Berechnung erfolgt, und Sie sich nur darüber Gedanken machen müssen, welche Parameter die Methoden benötigen. Die entsprechenden Informationen finden Sie in der Dokumentation. 14 Schattenstrahl Wie bereits im Überblick, kann man die Verdeckung von direkten Lichtquellen (z.B. einer Punktlichtquelle) dadurch prüfen, dass man einen Schattenstrahl erzeugt, der vom Schnittpunkt aus in die entgegengesetzte Lichtrichtung geworfen wird. Falls irgendein Objekt in der Szene den Strahl zwischen seinem Ursprung und der Lichtquelle schneidet, ist der Punkt verschattet und die Lichtquelle trägt nicht zu seiner Beleuchtung bei. In diesem Fall soll die Methode illuminate(...) die Farbe Schwarz (Color.black) zurück liefern. Beim Erstellen des Schattenstrahls sollte der Ursprung um einen Bruchteil in Richtung der Strahlrichtung verschoben werden, um Selbstverschattung aufgrund numerischer Ungenauigkeiten zu vermeiden. Denken Sie selbtständig darüber nach, wie Sie die Entfernung zwischen zwei Punkten bestimmen können und nutzen Sie dazu die Methoden, die Ihnen die Klasse Vector3d zur Verfügung stellt. Hinweise: Lesen Sie die Dokumentation der Klasse Intersection um sich darüber zu informieren, auf welche Eigenschaften Sie Zugriff haben. Suchen Sie in der Dokumentation zur Klasse Scenery nach der Methode, die es Ihnen erlaubt den Schattenstrahl innerhalb der Szene zu verfolgen und festzustellen, ob es einen Schnittpunkt zwischen dem Schnittpunkt und der Lichtquelle gibt. Für die Implementierung der Lichtquellen-Hierarchie dürfen alle anderen Klassen nicht verändert werden. Erinnerungen an die Schulmathematik: Der Winkel zwischen zwei normalisierten Vektoren ~a und ~b kann wie folgt berechnet werden: α = acos(~a · ~b), wobei ~a · ~b das Skalarprodukt zwischen den beiden Vektoren ~a und ~b repräsentiert und der Winkel α im Bogenmaß gegeben ist. Um den Arkuskosinus zu berechnen können Sie die Funktion Math.acos(...) verwenden. Hilfestellung: Wenn Sie Probleme haben, eine geeignete Klassen-Hierarchie zu entwerfen, finden Sie die Beschreibung der Musterlösung unter: http://wwwcg.in.tum.de/Teaching/WS2010/ProPra/ Material/propra_angabe_blatt09_hilfe.pdf. 15 Entwurf einer Klassen-Hierarchie für Lichtquellen Aus der Aufgabenstellung können folgende Gemeinsamkeiten und Unterschiede herausgelesen werden: AmbientLight hat, abgesehen von der Vaterklasse Light, keine weiteren Gemeinsamkeiten zu den anderen Lichtquellen, und muss dementsprechend direkt von dieser Klasse abgeleitet werden. DirectionalLight, PointLight und SpotLight unterscheiden sich lediglich darin, wie die Lichtrichtung auf einen gegebenen Schittpunkt bestimmt wird und wie man auf Verschattung prüfen muss. Gemeinsam ist hingegehen die generelle Beleuchtung: Falls der Schnittpunkt verschattet ist, wird schwarz zurückgegeben und wenn nicht, wird die Methode shade abhängig von der Lichtrichtung aufgerufen. Zusätzlich bietet es sich an auch eine Methode zur Erzeugung des Schattenstrahls zu implementieren, welche ebenfalls identisch für die drei Lichtquellen ist. Diese Gemeinsamkeiten (illuminate und getShadowRay) können in eine eigene abstrakte Oberklasse DirectLight ausgelagert werden. Da diese Methoden von der Berechnung der Lichtrichtung und dem Schattentest abhängen, müssen diese Funktionen als abstrakte Methoden getLightDirection und isShadowed hinzugefügt werden, welche von den Unterklassen implementiert werden müssen. Darüber hinaus ist SpotLight offensichtlich eine Spezialisierung von PointLight, die sich nur dadurch äußert, dass beim Schattentest zusätzlich der Winkel zur Lichtrichtung geprüft werden muss. Dementsprechend bietet sich folgende Klassenhierarchie an: Klassenhierarchie der Lichtquellen 16