package tinyray.raytracer;
import java.util.Iterator;
import java.util.LinkedList;
/**
* Die Klasse Tinyray repräsentiert das Tinyray-Programm als Raytracer.
*/
public class Tinyray extends Element {
// Die globale Kamera.
private Camera camera;
// Der Hintergrund (die Leinwand).
private Background background;
// Das globale Umgebungslicht.
private Ambience ambience;
// Der globale Nebel.
private Fog fog;
// Die geometrischen Objekte der Szene.
private LinkedList<Geo> geos;
// Die Sonnen als Lichtquellen der Szene.
private LinkedList<Sun> suns;
/**
* Initialisiert den Raytracer.
*/
public Tinyray() {
super();
this.camera = new Camera();
this.background = new Background(new Vector3D(0.0));
this.ambience = new Ambience(new Vector3D(1.0));
this.fog = new Fog(new Vector3D(1.0), 0.0);
this.geos = new LinkedList<Geo>();
this.suns = new LinkedList<Sun>();
}
/**
* Gibt die Kamera des Raytracers zurück.
* @return Die Kamera.
*/
public Camera getCamera() {
return this.camera;
}
/**
* Setzt die Kamera des Raytracers.
* @param camera Die Kamera.
*/
public void setCamera(Camera camera) {
this.camera = camera;
}
/**
* Gibt den Hintergrund des Raytraces zurück.
* @return Der Hintergrund.
*/
public Background getBackground() {
return this.background;
}
/**
* Setzt den Hintergrund des Raytracers.
* @param background Der Hintergrund.
*/
public void setBackground(Background background) {
this.background = background;
}
/**
* Gibt die globale Ambienz des Raytracers zurück.
* @return Die globale Ambienz.
*/
public Ambience getAmbience() {
return this.ambience;
}
/**
* Setzt die globale Ambienz des Raytracers.
* @param ambience Die globale Ambienz.
*/
public void setAmbience(Ambience ambience) {
this.ambience = ambience;
}
/**
* Gibt den Nebel des Raytracers zurück.
* @return Der Nebel.
*/
public Fog getFog() {
return this.fog;
}
/**
* Setzt den Nebel des Raytracers.
* @param fog Der Nebel.
*/
public void setFog(Fog fog) {
this.fog = fog;
}
/**
* Gibt die Geoobjekte der Szene zurück.
* @return Die Geoobjekte.
*/
public Iterator<Geo> getGeos() {
return this.geos.iterator();
}
/**
* Fügt der Szene ein Geoobjekt hinzu.
* @param geo Das Geoobjekt.
*/
public void addGeo(Geo geo) {
this.geos.add(geo);
}
/**
* Gibt die Sonnen der Szene zurück.
* @return Die Sonnen.
*/
public Iterator<Sun> getSuns() {
return this.suns.iterator();
}
/**
* Fügt der Szene eine Sonne hinzu.
* @param sun Die Sonne.
*/
public void addSun(Sun sun) {
this.suns.add(sun);
}
/**
* Gibt den Raytracer als Tinyray-Code zurück.
*/
public String toString() {
String result = "Tinyray {";
result += "\n " + this.getCamera();
result += "\n " + this.getBackground();
result += "\n " + this.getAmbience();
result += "\n " + this.getFog();
Iterator<Geo> iterGeos = this.getGeos();
while (iterGeos.hasNext()) {
result += "\n " + iterGeos.next();
}
Iterator<Sun> iterSuns = this.getSuns();
while (iterSuns.hasNext()) {
result += "\n " + iterSuns.next();
}
result += "\n}";
return result;
}
/**
* Rendert die Szene des Raytracers und gibt das resultierende Bild auf
* den Viewport aus. Trifft ein Strahl (Ray) auf die Oberfläche eines
* Geoobjektes der Szene, so prallt dieser wieder ab. Die Tiefe legt fest,
* wie oft ein Strahl maximal abprallen kann.
* Damit die Szene nicht so pixelig wirkt, wird jedes Pixel des Viewports
* in ein quadratisches Raster unterteilt, damit gleich mehrere Strahlen
* durch ein Pixel geschossen, geschickt oder gelegt werden können.
* @param viewport Das resultierende Bild.
* @param maxdepth Die Tiefe.
* @param antialias Die Rasterbreite bzw. -höhe in einem Pixel.
*/
public void render(Viewport viewport, int maxdepth, int antialias) {
// Zusichern, dass antialias mindestens den Wert 1 hat.
antialias = Math.max(1, antialias);
// viewDir = normalize(cam.LookAt - cam.Location).
Vector3D viewDir = Vector3D.sub(this.camera.getLookAt(),
this.camera.getLocation()).getNormalized();
// viewRight = viewDir x cam.Up.
Vector3D viewRight = Vector3D.cross(viewDir, this.camera.getUp());
// viewUp = viewRight x viewDir.
Vector3D viewUp = Vector3D.cross(viewRight, viewDir);
// Breite des Viewport als Double.
double viewportWidth = (double)viewport.getWidth();
// Höhe des Viewport als Double.
double viewportHeight = (double)viewport.getHeight();
// Field Of View mit 30 Grad.
double wh = 2.0 * Math.tan(Math.PI / 180.0 * (30.0 / 2.0));
// Höhe und Breite des virtuellen Bildes ins richtige Verhaeltnis
// setzen (Schlagwort: aspectRatio).
double width = wh;
double height = wh;
if (viewportWidth > viewportHeight) {
width = viewportWidth / viewportHeight * height;
} else {
height = viewportHeight / viewportWidth * width;
}
// Rasterabstände des virtuellen Bildes ermitteln.
double dy = height / viewportHeight;
double dx = width / viewportWidth;
/* Der eigentliche Rendervorgang. */
double y = height / 2.0;
for (int sy = 0; sy < viewport.getHeight(); sy++) {
// Gibt die Renderinformation aus.
if (this.renderLineInfo(sy, viewport.getHeight())) {
return;
}
double x = -width / 2.0;
for (int sx = 0; sx < viewport.getWidth(); sx++) {
LinkedList<Vector3D> colors = new LinkedList<Vector3D>();
// Erzeugt in Abhängigkeit von antialias:
// (antialias * antialias)-viele Strahlen pro Pixel,
// wobei jeder Strahl in eine leicht andere Richtung zeigt.
for (int ai = 0; ai < antialias; ai++) {
for (int aj = 0; aj < antialias; aj++) {
double di = dx * (double)ai / (double)antialias;
double dj = dy * (double)aj / (double)antialias;
// Richtung des jeweiligen Strahles.
Vector3D dir = Vector3D.add(
viewDir,
Vector3D.add(
Vector3D.mult(x + di, viewRight),
Vector3D.mult(y + dj, viewUp)
)
);
// Erzeugt einen Strahl.
Ray theRay = new Ray(this, this.camera.getLocation(),
dir.getNormalized(), 0, maxdepth);
// Berechnet den Farbwert des Strahles.
colors.add(theRay.getColor());
}
}
// Berechnet den Farbwert des Pixels.
Vector3D color = Vector3D.ZERO;
Iterator<Vector3D> iterator = colors.listIterator();
while (iterator.hasNext()) {
color = Vector3D.add(color, iterator.next());
}
int raysPerPixel = antialias * antialias;
color = Vector3D.mult(1.0 / (double)raysPerPixel, color);
// Setzt den Farbwert des Pixels im Viewport.
viewport.setPixel(sx, sy, color);
x += dx;
}
y -= dy;
}
// Zu guter Letzt noch die Renderinformation ausgeben.
this.renderLineInfo(viewport.getHeight(), viewport.getHeight());
}
/**
* Gibt eine kurze Information über den Rendervorgang auf der Konsole aus.
* Bevor eine Zeile gerendert wird, sollte dem Anwender eine Fortschritts-
* information des Rendervorgangs angezeigt werden.
* @param line Die aktuelle Zeile.
* @param height Die Anzahl der zu rendernden Zeilen.
* @return true, falls der Rendervorgang abgebrochen werden soll.
* Hier wird der Rendervorgang nie abgebrochen, es wird stets
* falls zurückgegeben. Die Rückgabe von true, wäre beispielsweise
* interessant, wenn man den Rendervorgang vorzeitig abbrechen
* möchte.
*/
protected boolean renderLineInfo(int line, int height) {
System.out.print("Reihe " + line + " von " + height);
System.out.print(" (" + (line * 100 / height) + "%)");
System.out.print(" \r");
if (line == height) {
System.out.print("Rendering beendet!");
System.out.println(" ");
}
return false;
}
/**
* Die innere Klasse Ray stellt einen Strahl, der durch die Szene
* geschickt werden kann, dar.
*/
public final class Ray {
// Das Tinyray-Programm, welches die Szene kapselt.
private Tinyray tinyray;
// Der Ursprung des Strahles.
private Vector3D origin;
// Die Richtung des Strahles.
private Vector3D direction;
// Die aktuelle Rekursionstiefe.
private int depth;
// Die maximale Rekursionstiefe.
private int maxdepth;
/**
* Initialisiert den Strahl.
* @param tinyray Das dazugehörige Tinyray-Programm.
* @param origin Der Ursprung des Strahles.
* @param direction Die Richtung des Strahles.
* @param depth Die aktuelle Rekursionstiefe.
* @param maxdepth Die maximale Rekursionstiefe.
*/
public Ray(Tinyray tinyray, Vector3D origin, Vector3D direction,
int depth, int maxdepth) {
this.tinyray = tinyray;
this.origin = origin.copy();
this.direction = direction.copy();
this.depth = depth;
this.maxdepth = maxdepth;
}
/**
* Gibt den Ursprung des Strahles zurück.
* @return Der Ursprung.
*/
public Vector3D getOrigin() {
return this.origin;
}
/**
* Gibt die Richtung des Strahles zurück.
* @return Die Richtung.
*/
public Vector3D getDirection() {
return this.direction;
}
/**
* Berechnet den Farbwert, den der Strahl besitzt, wenn dieser durch
* die Szene geschickt wird.
* @return Der teuer berechnete Farbwert.
*/
public Vector3D getColor() {
Vector3D result = Vector3D.ZERO; // Schwarze Farbe
// Gewinner feststellen
Geo closest = null;
double minDist = Double.MAX_VALUE;
Iterator<Geo> geos = this.tinyray.getGeos();
while (geos.hasNext()) {
Geo geo = geos.next();
Geo.Distance geoDistance = geo.getIntersect(this);
if (geoDistance != null) {
double distance = geoDistance.getDistance();
if ((0.0 < distance) && (distance < minDist)) {
minDist = distance;
closest = geoDistance.getGeo();
}
}
}
if (closest == null) {
if (this.depth <= 0) {
result = this.tinyray.getBackground().getColor();
}
} else if (this.maxdepth < 0) {
// Ray-Casting, falls die maximale Tiefe negativ ist.
result = closest.getParameters().getColor();
} else {
/* Ray-Tracing */
Vector3D intersectionPosition = Vector3D.add(
this.origin,
Vector3D.mult(minDist, this.direction)
);
Vector3D normal = closest.getNormal(intersectionPosition);
// Berechnet den vom getroffenen Objekt reflektierten Strahl.
Ray reflectedRay = new Ray(this.tinyray, intersectionPosition,
this.direction.getReflectedAt(normal).getNormalized(),
this.depth + 1, this.maxdepth);
// Ermittelt in Abhängigkeit jeder Sonne einen Farbwert.
Iterator<Sun> suns = this.tinyray.getSuns();
while (suns.hasNext()) {
Sun sun = suns.next();
// Nur wenn der Strahl selbst nicht zur Sonne zeigt, wird
// die Farbe berechnet.
// Liegt zw. der Sonne und dem Geoobjekt ein Hindernis?
Ray rayOfSun = new Ray(this.tinyray, Vector3D.add(
intersectionPosition, Vector3D.mult(
Vector3D.EPSILON, sun.getDirection())),
sun.getDirection(), 0, this.maxdepth);
boolean somethingIntersected = false;
Iterator<Geo> geos_sun = this.tinyray.getGeos();
while (geos_sun.hasNext()) {
Geo geo = geos_sun.next();
Geo.Distance distance = geo.getIntersect(rayOfSun);
if (distance != null) {
if (distance.getDistance() > 0.0) {
somethingIntersected = true;
break;
}
}
}
// Falls kein Hindernis zw. Sonne und dem getroffenen
// Geoobjekt (closest) vorhanden ist, dann wird auf den
// aktuellen Farbwert ein weiterer Farbwert in
// Abhängigkeit der aktuellen Sonne hinzuaddiert.
if (!somethingIntersected) {
result = Vector3D.add(
result,
this.getPhongLightingColor(reflectedRay,
normal, closest, sun)
);
}
}
// Wenn nun alle Sonnen durchiteriert sind, wird noch das
// globale Umgebungslichtfarbe mit der lokalen, ambienten
// Farbe multipliziert, und schließlich zum aktuellen
// Farbwert hinzuaddiert.
result = Vector3D.add(
Vector3D.mult(
this.tinyray.getAmbience().getColor(),
closest.getParameters().getAmbient()
),
result
);
// Spiegelung mit Rekursion
if (this.depth < this.maxdepth) {
result = Vector3D.add(
result,
Vector3D.mult(
closest.getParameters().getMirror(),
reflectedRay.getColor()
)
);
}
}
// Fog-Berechnung
if (this.tinyray.getFog().getDensity() != 0.0) {
Vector3D fogColor = this.tinyray.getFog().getColor();
if (minDist >= 0.0) {
// Nebeldichte.
double d = this.tinyray.getFog().getDensity();
// Exponentieller (EXP2) Nebelwert.
double f = Math.exp(-(minDist * d) * (minDist * d));
f = Math.min(Math.max(f, 0.0), 1.0);
result = Vector3D.add(
Vector3D.mult(f, result),
Vector3D.mult(1.0 - f, fogColor)
);
} else {
result = fogColor;
}
}
// Endlich die Rückgabe des Farbwertes zum Strahl, der durch die
// Szene geschickt wurde.
return result;
}
// Farbberechnung mittels Phong-Beleuchtungsmodell.
private Vector3D getPhongLightingColor(Ray reflectedRay,
Vector3D normal, Geo geo, Sun sun) {
Vector3D result = Vector3D.ZERO;
/* Diffusen Farbwert berechnen (diffuse). */
double diffuseFactor = Math.max(
0.0,
Vector3D.dot(sun.getDirection(), normal.getNormalized())
);
Vector3D diffuse = Vector3D.mult(
diffuseFactor,
geo.getParameters().getColor()
);
/* Die Lichtreflection berechnen (specularity).
*
* Das Herzstück des Phong-Beleuchtungsmodells,
* die Spiegelung der Lichtquelle im geometrischen Objekt.
* Je besser das Licht ins Auge des Betrachters trifft,
* desto heller (perfekter) wird die Lichtspiegelung.
*/
Vector3D sunRefl = sun.getDirection().getReflectedAt(normal);
Vector3D eyeRefl = Vector3D.sub(
reflectedRay.origin,
this.origin
);
double sun_eye = Math.max(
0.0,
Vector3D.dot(
sunRefl.getNormalized(),
eyeRefl.getNormalized())
);
double exponent = Math.pow(
sun_eye,
geo.getParameters().getShininess()
);
double specular = geo.getParameters().getSpecular() * exponent;
Vector3D specularity = Vector3D.mult(specular, sun.getColor());
/* Zusammenführen der Farbanteile (diffuse und specularity). */
result = Vector3D.add(diffuse, specularity);
// Natürlich hängt der resultierende Farbwert von der Farbe
// der Sonne ab.
result = Vector3D.mult(sun.getColor(), result);
return result;
}
}
} |