Hypercell ein ] Hypercell aus ] Zeige Navigation ] Verstecke Navigation ]
c++.net  
   

Die mobilen Seiten von c++.net:
https://m.c-plusplus.net

  
C++ Forum :: Spiele-/Grafikprogrammierung ::  Raytracing / Pathtracing / Photon Mapping - Tutorial  
Gehen Sie zu Seite 1, 2, 3  Weiter
  Zeige alle Beiträge auf einer Seite
Auf Beitrag antworten
Autor Nachricht
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:26:53 10.10.2017   Titel:   Raytracing / Pathtracing / Photon Mapping - Tutorial            Zitieren

Einleitung

Es hat lange gedauert aber nun habe ich endlich die nötige Zeit und Motivation gefunden, um mein schon lange geplantes Raytracing-Tutorial zu schreiben. Die drei wichtigsten Bausteine im Raytracing sind meiner Meinung nach Photonmapping, Bidirektionales Pathtracing und Metropolis-Lighttransport. Alle heutigen 'guten' Verfahren bauen auf ein oder mehreren von diesen 3 Bausteinen auf.

In diesen Tutorial geht es um den ersten Baustein: Photonmapping und alles Vorwissen, was man braucht, um Photonmapping zu verstehen. (Die anderen beiden Bausteine kommen später/sind in mein Kopf zum teil schon erschaffen)

Worum geht’s beim Raytracing eigentlich? Es geht darum tolle Bilder mit dem Computer zu erzeugen. Hier ist ein Beispiel: https://www.chaosgroup.com/cn/gallery/albert-mizuno-maxim-dining

An wem wendet sich dieses Tutorial? Leute die Raytracingbilder interessant finden und die ein Raytracer selber schreiben wollen anstatt fertige von anderen Leuten zu nehmen.

Wie ist das Tutorial zu lesen? Chronologisch. Ich halte es für sinnvoll, dass der Leser all die Beispiele bei sich selber in einer Programmiersprache seiner Wahl 'mitprogrammiert'. Außerdem kommen am Ende noch Ideen, wo der Leser dann selbstständig Erweiterungen einbauen kann, die vom Schwierigkeitsgrad her schaffbar sein müssten.

Welches Vorwissen ist nötig/wird empfohlen?

Mithilfe von Vektorrechnung den Schnittpunkt von ein Strahl und einer Ebene berechnen können
Erste Erfahrungen mit OpenGL/DirectX, um ein Gefühl für 3D-Programmierung zu haben

Ich würde sagen, man kann das ganze benötigte Vorwissen auf eine Aufgabe reduzieren: Der Leser kann mit OpenGL/DirectX ein drehenden Würfel malen, wo Normalmapping verwendet wird:
http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/

Wenn jemand wirklich so gut ist und das hinbekommt, dann hat er schon mal was von einer TBN-Matrix und einer Rotationsmatrix gehört. Damit ist er dann sehr gut fürs Raytracing vorbereitet.

Warum denke ich ist dieses Tutorial nötig? Ich kenne bis zum heutigen Tage keine kostenlose Seite im Internet, wo all diese Grundlagen, die man für das Photonmapping-Verständnis braucht, erklärt werden. Es gibt zwar viele Paper aber diese sind meiner Meinung nach schwer zu verstehen und es fehlen meistens Beispielimplementierung, die so klein wie nötig aber so verständlich wie möglich sind. Die wenigen Programmierbeispiele, die ich gefunden habe, erwähnt sei hier smallpt und smallvcm, sind meiner Meinung nach vom Programmierstil her schwer zu lesen.

Dieses Tutorial soll nicht in erster Linie der Selbstbeweihräucherung dienen sondern der Leser soll so viel wie möglich dabei lernen. Von daher würde ich mich freuen, wenn Fragen/Unklarheiten hier in diesen Thread geposted werden so dass ich versuchen kann das Tutorial an der gegebenen Stelle weiter zu verbessern, so dass auch andere Leser es dann leichter haben.

_________________
Anfänger vom Dienst und Raytracingfreund


Zuletzt bearbeitet von XMAMan am 18:17:33 03.01.2018, insgesamt 2-mal bearbeitet
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:27:59 10.10.2017   Titel:              Zitieren

Ein einfacher Raytracer

Ein Raytracer funktioniert vom prinzipiellen Aufbau her so:

C#:
1
2
3
4
5
6
7
8
9
10
for (int x = 0; x < width; x++)
{
   for (int y = 0; y < height; y++)
   {
     Ray primaryRay = GetPrimaryRay(x, y);
     var point = GetFirstIntersectionPoint(primaryRay);
     var color = GetColorFromPoint(point);
     SetPixelColor(x, y, color);
   }
}


Man möchte ein Bild erstellen, was width-Pixel breit und height-Pixel hoch ist. Ich gehe dazu mit den For-Schleifen über jeden Pixel und erzeuge einen sogenannten 'Primärstrahl'. (Siehe Abschnitten 'Die Bildebene / Primärstrahlen'). Diesen Strahl schieße ich dann von der Kamera ausgehend in die Szene und ich schaue dann, welches Objekt zuerst getroffen wird. Dann bestimme ich die Farbe für den Pixel in Abhängigkeit vom getroffenen Objekt.

In den folgenden Abschnitten behandle ich die einzelnen Bausteine des Raytracer.

_________________
Anfänger vom Dienst und Raytracingfreund
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:28:52 10.10.2017   Titel:              Zitieren

Die Bildebene / Primärstrahlen
Wenn man ein Bild mit ein Raytracer erstellen möchte, was 320 Pixel breit ist und 200 Pixel hoch, dann muss man eine Bildebene (ImagePlane) definieren, welche 320 Einheiten breit und 200 Einheiten hoch ist. Die Kamera ist ein 3D-Punkt, welcher im Koordinatenursprung (0,0,0) liegt. Die Bildebene wird mit dem Abstand d auf der Z-Achse zum Koordinatenursprung platziert. Für jeden Pixel schieße ich nun ein Strahl, welcher beim Kamera-Punkt startet durch das Raster von der Bildeben. Im Bild rot dargestellt. Dieser Strahl wird Primärstrahl genannt. Die Szene ist üblicherweise aus einer Menge von 3D-Dreiecke aufgebaut. Es wird geschaut, welches Dreieck der Primärstrahl als erstes trifft. Auf die Weise weiß ich für jeden Pixel, welches Dreieck in der Szene ich sehe.

https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/01_SimpleRaytracer/Images/ImagePlane.png

Die Kamera hat folgende Parameter:
-Position (3D-Punkt)
-Blickrichtung (3D-Vektor)
-Vektor, welcher nach Oben zeigt
-Öffnungswinkel (Brennweite) = Field Of View (FoV)

Um nun den Abstand d der Bildebene vom Kamera-Punkt zu berechnen, muss ich ein blaues rechtwinkliges Dreieck definieren. Die eine Seite davon hat die Länge d, was gesucht ist. Fov/2 und ScreenHeigh/2 ist gegeben. FoV könnte z.B. 45 (Grad) sein und wenn das Bild 200 Pixel hoch sein soll, dann ist ScreenHeigh/2 = 100.

Ich kann nun folgende Gleichung aufstellen:

§\tan \frac{\mathit{foV}} 2=\frac{\mathit{ScreenHeigh}/2} d§

Jetzt muss ich diese Gleichung nach d umstellen (Ich multipliziere erst mit d und dividiere dann tan(foV/2) weg.

§d=\frac{\mathit{ScreenHeigh}/2}{\tan \frac{\mathit{foV}} 2}§

In C# sieht das ganze dann so aus. FoV heißt in den Beispiel hier openinAngleY. ImagePlaneDistance ist die Variable d.

C#:
1
2
3
4
5
6
7
8
public Camera(Vector position, Vector forward, Vector up, float openingAngleY, int screenWidth, int screenHeight)
{
   double gradRad = (openingAngleY * Math.PI / 180.0);
   float imagePlaneDistance = (float)(screenHeight / (2 * Math.Tan(gradRad / 2)));
 
   this.position = position;
   this.Forward = forward.Normalize();
   this.imagePlaneCorner = new Vector(-screenWidth / 2.0f, -screenHeight / 2.0f, imagePlaneDistance);


Hier wird erst der Abstand der Bildebene zum Kamera-Punkt berechnet und dann wird die linke untere Ecke von der Bildebene definiert.

Will ich nun für ein Pixel den Richtungsvektor für den Primärstrahl berechnen, dann berechne ich ein Punkt auf der BildEbene und bestimme dann ein Richtungsvektor, welcher vom Kamera-Punkt zum Bildebenenpunkt zeigt.

C#:
public Ray GetPrimaryRay(int x, int y)
{
   Vector pointOnImagePlane = this.imagePlaneCorner + new Vector(x, 0, 0) + new Vector(0, y, 0);
   Vector directionInCameraSpace = (pointOnImagePlane - new Vector(0, 0, 0)).Normalize();


Da die Bildebene ja genau so breit und hoch ist, wie das Bild, was ich erzeugen will, kann ich hier direkt mit
C#:
new Vector(x, 0, 0)
und
C#:
new Vector(0, y, 0)
arbeiten.

Vorhin habe ich gesagt, dass der Kamera-Punkt an der Stelle (0,0,0) liegt und nun sieht man hier in den C#-Beispiel, dass der Konstruktor von der Kamera-Klasse eine position-Variable bekommt. Es gibt hier zwei Räume. Den einem nenne ich jetzt mal CameraSpace und den anderen WorldSpace. Im CameraSpace liegt der Kamera-Punkt bei (0,0,0) und im WorldSpace liegt er an der Position 'position'. Alle Dreiecke befinden sich im WorldSpace.

Ich habe nun gezeigt, wie man den PrimaryRay im CameraSpace erzeugt. Um nun den Richtungsvektor vom CameraSpace in den WorldSpace zu transformieren, erzeuge ich folgende Matrix.

C#:
Vector N = forward.Normalize();                     // Forward
Vector B = Vector.Cross(N, up).Normalize();         // Left
Vector T = Vector.Cross(B, forward).Normalize();    // Up
 
this.cameraToWorldSpace = new float[] { B.X,  B.Y,  B.Z,
                                        T.X,  T.Y,  T.Z,
                                        N.X,  N.Y,  N.Z};


Nun kann ich die Transformation mit einer einfachen Vektor-Matrix-Multiplikation durchführen.

C#:
return new Ray(this.position, Matrix.MatrixVectorMultiplication(this.cameraToWorldSpace, directionInCameraSpace));


Hier diesen Beispiel arbeite ich mit Vektoren und Matrizen. Sollte es bei den ein oder anderen da ein paar Hänger geben, so wäre dieser Linke hier z.B. zum auffrischen da.
https://learnopengl.com/#!Getting-started/Transformations

Hier ist der Link für das Beispiel:

https://github.com/XMAMan/RaytracingTutorials/blob/master/01_SimpleRaytracer/RaytracingTutorials/Camera.cs

_________________
Anfänger vom Dienst und Raytracingfreund
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:29:47 10.10.2017   Titel:              Zitieren

Die Schnittpunktabfrage des Primärstrahls mit der Szene

Hat man es nach langen Mühen endlich geschafft den Primärstrahl zu erzeugen, so muss als nächstes der Schnittpunkt zwischen den Strahl und der Szene bestimmt werden. Die Szene besteht üblicherweise aus einer Menge von Dreiecken. Ein einfacher Algorithmus geht nun so vor, dass er mit einer For-Schleife durch alle Dreiecke geht und dann den Schnittpunkt zwischen den Strahl und dem jeweiligen Dreieck bestimmt. Es wird geschaut, ob es überhaupt ein Schnittpunkt gibt und wenn ja, ob ich bereits ein Schnittpunkt habe, der schon näher liegt. Wenn nein, dann merke ich mir den Punkt.

Die Klasse, welche diese Aufgabe übernimmt habe ich IntersectionFinder genannt. Diese sieht wie folgt aus:

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class IntersectionFinder
{
    private Triangle[] triangles;
 
    public IntersectionFinder(Triangle[] triangles)
    {
        this.triangles = triangles;
    }
 
    public IntersectionPoint GetIntersectionPoint(Ray ray)
    {
        float currentMinDistance = float.MaxValue;
 
        IntersectionPoint resultPoint = null;
 
        foreach (var triangle in this.triangles)
        {
            var point = triangle.Intersect(ray);
            if (point != null && point.DistanceToRayOrigin < currentMinDistance)
            {
                resultPoint = point;
                currentMinDistance = point.DistanceToRayOrigin;
            }
        }
 
        return resultPoint;
    }
}


Ich gehe durch die Dreiecksliste und schaue ob es überhaupt ein Schnittpunkt gibt, indem point != null sein muss. Ist der Schnittpunkt näher als der bisher nächste, so merke ich mir den Punkt.

Für die Schnittpunktabfrage nutze ich den Möller-Trumbore-Algorithmus.

https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm

Der Einsatz einer For-Schleife für die Schnittpunktbestimmung ist ziemlich ineffektiv. Normalerweise nimmt man dafür ein KD-Baum oder andere Beschleunigungsstrukturen. In diesen Tutorial möchte ich aber den Schwerpunkt auf andere Themengebiete setzen. Die Schnittpunktabfrage sollte für den Leser so leicht wie möglich zu verstehen sein.

_________________
Anfänger vom Dienst und Raytracingfreund
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:30:33 10.10.2017   Titel:              Zitieren

Die Farbbestimmung beim einfachen Raytracer

Nachdem man erst ein Primärstrahl erzeugt hat und dann die Position des Schnittpunktes berechnet hat, kann man nun über die Farbe nachdenken. So ein Schnittpunkt hat neben der Position auch ein Normalvektor. Bei mein Dreiecken habe ich einfach die Normale von den Dreieck genommen.

Der Schnittpunkt besteht aus einer Position, einer Normale und einer Farbe. Normale und Farbe sind direkt Daten aus dem Dreieck.

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class IntersectionPoint : IVertexPoint
{
    private Triangle triangle;
 
    public Vector Position { get; private set; }
    public Vector Normal { get { return this.triangle.Normal; } }
    public Vector Color { get { return this.triangle.Color; } }
    public float DistanceToRayOrigin { get; private set; }
 
    public IntersectionPoint(Vector position, Triangle triangle, float distanceToRayOrigin)
    {
        this.Position = position;
        this.triangle = triangle;
        this.DistanceToRayOrigin = distanceToRayOrigin;
    }


In diesen einfachen Beispiel berechne ich die Pixelfarbe einfach dadurch, indem ich die Normale mit der Primärstrahl-Richtung per Vektor-Punkt-Multiplikation verrechne und diese mit der Farbe vom Dreieck wichte.

C#:
1
2
3
4
5
6
7
8
9
10
11
for (int x = 0; x < width; x++)
 for (int y = 0; y < height; y++)
 {
    Ray primaryRay = data.Camera.GetPrimaryRay(x, y);
    var eyePoint = this.intersectionFinder.GetIntersectionPoint(primaryRay);
    if (eyePoint != null)
    {
        image.SetPixel(x, y, VectorToColor(eyePoint.Color * Vector.Dot(eyePoint.Normal, -primaryRay.Direction)));
 
    }
}

Das Ergebnis sieht dann so aus:

https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/01_SimpleRaytracer/Images/Result.png

Die Formel für die Farbbestimmung ist völlig willkürlich und hat nichts mit physikalischer Korrektheit zu tun. Ich wollte hier erst mal nur das Grundprinzip vom Raytracing erklären. In den nächsten Abschnitten wird der Raytracer nun schrittweise verbessert.

Beispielcode:
https://github.com/XMAMan/RaytracingTutorials/tree/master/01_SimpleRaytracer/RaytracingTutorials

_________________
Anfänger vom Dienst und Raytracingfreund
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:31:24 10.10.2017   Titel:              Zitieren

Ein einfacher Raytracer mit Schatten

Um den Raytracer um Schatten zu erweitern, muss für jeden Punkt, bei dem man die Farbe berechnen will, prüfen, ob zwischen den Punkt und der Lichtquelle ein anderes Objekt liegt oder nicht. Für diese Sichtbarkeitsprüfung wird ein sogenannter Schattenstrahl zu ein Punkt auf der Lichtquelle gesendet. Kommt dieser Schattenstrahl ohne Unterbrechung bei der Lichtquelle an, so ist dort kein Schatten.

Bei folgenden Bild soll für die Punkte P1 und P2 jeweils geprüft werden, ob sie im Schatten liegen oder nicht. Bei P1 geht der Schattenstrahl ohne Unterbrechung zur Lichtquelle. Also liegt P1 nicht im Schatten. Bei P2 kommt der Schattenstrahl nicht an. Er stößt gegen ein anderes Objekt aus der Szene. Deswegen liegt P2 im Schatten.
https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/02_DirectLightingOnly/Images/Schattenstrahl.png

Die Hauptschleife vom Raytracer sieht nun so aus:

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
for (int x = 0; x < width; x++)
  for (int y = 0; y < height; y++)
  {
   Ray primaryRay = data.Camera.GetPrimaryRay(x, y);
   var eyePoint = this.intersectionFinder.GetIntersectionPoint(primaryRay);
 
   Vector pixelColor = new Vector(0, 0, 0);
 
   if (eyePoint != null)
   {
       if (eyePoint.IsLocatedOnLightSource)
       {
           pixelColor += eyePoint.Emission;
       }
       else
       {
           //Direct Lighting
           var lightPoint = data.LightSource.GetRandomPointOnLightSource(rand);
           if (IsVisible(eyePoint.Position, lightPoint.Position))
           {
               pixelColor += eyePoint.Color;
           }
          }
      }
 
      image.SetPixel(x, y, VectorToColor(pixelColor));
  }


Erst erzeuge ich ein Primärstrahl und schaue, ob er ein Objekt in der Szene trifft. Wenn ja (eyePoint != null), dann schaue ich, ob der eyePoint bereits auf einer Lichtquelle liegt. D.h. dort bei diesen Bildbereich wird die Lichtquelle direkt von der Kamera angesehen. In diesem Falle mache ich keine Schattenstrahl-Berechnung.

Wenn der eyePoint aber nicht auf einer Lichtquelle liegt, so erzeuge ich ein zufälligen Punkt auf der Lichtquelle.

Die Lichtquelle besteht in meinen Beispiel aus zwei Dreiecken, welche gemeinsam ein Viereck bilden. Ich wähle zufällig eins von den beiden Dreiecken aus und bestimme dann ein Zufallspunkt auf den Dreieck.

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public LightSourcePoint GetRandomPointOnLightSource(Random rand)
{
    var triangle = this.triangles[rand.Next(this.triangles.Length)];
    Vector position = triangle.GetRandomPointOnSurface(rand);
 
    return new LightSourcePoint()
    {
        Position = position,
        Color = triangle.Color * this.Emission,
        Normal = triangle.Normal
    };
}
 
class LightSourcePoint : IVertexPoint
{
    public Vector Position { get; set; }
    public Vector Color;
    public Vector Normal { get; set; }
}


Die Triangle-Klasse erzeugt den Zufallspunkt indem zufällige Baryzentrische Koordinaten erzeuge und daraus dann die Position bestimme.

Nachdem ich ein Zufallspunkt auf der Lichtquelle bestimmt habe, mach ich nun den Schattenstrahl-Test mit der IsVisible-Methode.

Ich benutze dazu wieder den IntersectionFinder, und schieße den Schattenstrahl von point1 zur point2 (Punkt auf der Lichtquelle). Nur wenn der Punkt, den der IntersectionFinder findet, ganz nah an mein vorgegeben Punkt liegt, dann gehe ich davon aus, dass freie Sicht zwischen point1 und point2 vorhanden ist.

C#:
bool IsVisible(Vector point1, Vector point2)
{
  var point = this.intersectionFinder.GetIntersectionPoint(new Ray(point1, (point2 - point1).Normalize()));
  if (point == null) return false;
  if ((point.Position - point2).Length() < 0.0001f) return true;
  return false;
}


Liegt der Punkt nicht im Schatten, dann berechne ich die Pixelfarbe nun hiermit:

C#:
pixelColor += eyePoint.Color;


eyePoint ist ein Objekt von der IntersectionPoint-Klasse. Die Color-Property gibt einfach nur die Farbe vom Dreieck zurück.

C#:
public Vector Color { get { return this.triangle.Color; } }


So sieht das Bild nun mit Schatten aus. (Ziemlich verpixelte Schatten)
https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/02_DirectLightingOnly/Images/WithShadow.png

Beispielcode (In SimpleRenderer.cs ist das Version 1)

https://github.com/XMAMan/RaytracingTutorials/tree/master/02_DirectLightingOnly/RaytracingTutorials

_________________
Anfänger vom Dienst und Raytracingfreund
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:32:09 10.10.2017   Titel:              Zitieren

Der GeometryTerm

Der GeometryTerm ist ein Faktor (Float-Zahl), um dem die Lichtintensität abgeschwächt wird, wenn zwei Flächen Licht miteinander austauschen. Habe ich zwei Flächen, dessen Mittelpunkte den Abstand d haben und welche beide jeweils ihre eigene Flächenormale N1 und N2 haben, so ist der GeometryTerm wie folgt definiert.

§\mathit{GeomtryTerm}=\frac{\cos (\mathit{w1})\ast \cos (\mathit{w2})}{d\ast d}§

w1 und w2 ist der Winkel zwischen der Flächennormale und der Linie, welche zur jeweils anderen Fläche zeigt.

https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/02_DirectLightingOnly/Images/GeometryTerm.png

Wenn die Flächen genau aufeinander zuzeigen, so ist w1 und w2 0 Grad. Der Cosinus von 0 ist 1. D.h. Es wird mehr Licht zwischen den Flächen übertragen, wenn die Flächen aufeinander zu zeigen, da beide Cosinus-Terme dann 1 sind.

Den Grund für diese Cosinus-Abschwächung versuche ich mit folgenden Bild zu erklären:
https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/02_DirectLightingOnly/Images/CosinusLambda.png

Um so schräger die Fläche das Licht reflektiert oder versendet, um so kürzer wird l1/l2/l3. Wenn ich an den roten Punkten mich befinde und auf die Fläche schaue, so erscheint mir die Fläche immer kleiner, je schräger die Fläche zur mit ist.

Außerdem wird die Übertragene Lichtmenge mit den Quadrat-Abstand abgeschwächt. Um so größer der Abstand, um so größer ist die Abschwächung also.

Den Grund für die Abstandabschwächung versuche ich hiermit zu erklären:
https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/02_DirectLightingOnly/Images/DistanzeFaktor.png

Die gelben Linien stehen für ein jeweils versendetes Photon. F1, F2 und F3 sind Flächen, die alle gleichgroß sind und sich nur im Abstand unterscheiden. F1 erhält 5 Photonen. F2 3 und F3 ein Photon.

Bauen wir den GeometryTerm nun mal in den Raytracer.
C#:
var lightPoint = data.LightSource.GetRandomPointOnLightSource(rand);
if (IsVisible(eyePoint.Position, lightPoint.Position))
{
   pixelColor += eyePoint.Color * GeometryTerm(eyePoint, lightPoint);
}


→ Wenn man zwei normalisierte Vektoren multipliziert, dann entspricht die float-Zahl, die dabei rauskommt dem Cosinus von dem Winkel zwischen den beiden Vektoren. Somit ist die Vektor-Punkt-Multiplikation ein guter Weg, um bei Richtungsvektoren den Cosinus-Winkel zu berechnen.

C#:
1
2
3
4
5
6
7
8
9
float GeometryTerm(IVertexPoint point1, IVertexPoint point2)
{
    Vector v1To2 = point2.Position - point1.Position;
    float distance = v1To2.Length();
    Vector direction1To2 = v1To2 / distance;
    float lamda1 = Math.Max(Vector.Dot(point1.Normal, direction1To2), 0);
    float lamda2 = Math.Max(Vector.Dot(point2.Normal, -direction1To2), 0);
    return lamda1 * lamda2 / (distance * distance);
}


Nun sieht das Bild schon so aus:
https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/02_DirectLightingOnly/Images/WithGeometryTerm.png

Es sieht nun schon ein wenig besser aus.

Beispielcode (In SimpleRenderer.cs ist das Version 2)

https://github.com/XMAMan/RaytracingTutorials/tree/master/02_DirectLightingOnly/RaytracingTutorials

_________________
Anfänger vom Dienst und Raytracingfreund
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:32:59 10.10.2017   Titel:              Zitieren

Diffuse Brdf

Der nächste Schritt wird erst mal nicht sichtbar die Bildqualität verbessern aber er ist trotzdem für später wichtig. Es geht um die Brdf (Bidirektionele Reflektranzverteilungsfunktion).
https://de.wikipedia.org/wiki/Bidirektionale_Reflektanzverteilungsfunktion

Damit wird die Farbe und das Material (Glass, Spiegel, Diffuse Wand, Raues Metall) festgelegt. Hier in unseren Beispiel werde ich erst mal nur diffuse Materialien behandeln, da diese immer das erste Material sind, womit man sich beschäftigt^^

Also was ist eine Brdf? Das ist eine Funktion, welche angibt, wie stark ein Material eine gegebenen Lichtstrahl reflektiert.

Gegeben ist der Richtungsvektor für das einfallende und das ausgehende Licht.
https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/02_DirectLightingOnly/Images/Brdf.png

Die Brdf-Funktion sagt mir nun, wie viel Licht in Richtung des ausgehenden Vektors fließt. Die Lichtmenge wird üblicherweise als Vektor angegeben, wo die einzelnen Komponenten für RGB (Rot Grün Blau) stehen.

Wenn ich jetzt ein weißen Lichtstrahl von der Lichtquelle habe, so könnte der Lichtvektor z.B.
so aussehen (1,1,1). Ich gebe nun der Brdf-Funktion folgende Werte rein:
Welches Licht geht rein? -> (1,1,1)
Richtungsvektor, welcher von der Lichtquelle zum Brdf-Punkt zeigt
Richtungsvektor, welcher vom Brdf-Punkt zu ein beliebigen anderen Punkt (z.B. die Kamera) zeigt
Wenn ich nun eine diffuse Rote Wand habe, dann sieht die Brdf-Funktion wie folgt aus:

C#:
Vector RedDiffuseBrdf(Vector inputDirection, Vector outputDirection, Vector inputColor)
{
    return Vector.Mult(inputColor, new Vector(1, 0, 0) / Math.PI);
}


Die Farbe, die reingeht wird mit (1,0,0) / PI gewichtet.
D.h. Das weiß Licht (1,1,1) wird zu (0.32, 0,0)

Man sieht hier, dass die inputDirection und outputDirection egal sind und in der Funktion selber nicht verwendet werden. Das liegt daran, dass eine diffuse Fläche in alle Richtungen gleichstark reflektiert. Wenn ich eine andere Brdf habe, dann könnten diese beiden Angaben auf einmal wichtig werden. Hier bei der diffuse Funktion kann man sie aber auch weglassen.

Der Vektor (1,0,0) steht für die rote Farbe. Aber warum teilt man eigentlich durch PI? Würde es nicht reichen, wenn ich die weiße Farbe nur mit (1,0,0) multipliziere?

Die Antwort auf diese Frage ergibt sich aus der Bedingung, dass eine Brdf nicht mehr Licht reflektieren darf, als wie sie als Input bekommt (Energieerhaltungsregel).

Die Energieerhaltungsregel sieht wie folgt aus (siehe hier)https://de.wikipedia.org/wiki/Bidirektionale_Reflektanzverteilungsfunktion

§\forall w_i:\int _{\Omega }f_r(w_i,w_o)\ast \cos \phi_o\mathit{dw}_o\le 1§

Anmerkung: Das ist ein Lebesgue-Integral (https://de.wikipedia.org/wiki/Lebesgue-Integral). Ω
ist die Menge aller Richtungsvektoren, welche man innerhalb einer Halbkugel hat. 'wo' läuft über jedes Element aus Ω und es wird so die Summe gebildet.

Meiner Meinung nach muss man die Formel aber noch um den Li-Term erweitern, um sie richtig verstehen zu können^^

§\forall w_i:\int _{\Omega }f_r(w_i,w_o)\ast L_i\ast \cos \phi_o\mathit{dw}_o\le L_i§

Für jede Inputrichtung wi muss gelten: Die Summe aller ausgesendeten Energie darf nicht größer sein, als die aus wi kommende Lichtmenge Li. Fr ist hier die Brdf-Funktion. Die einkommende Menge Li wird mit dem Brdf-Faktor fr und dem cos-Faktor (stammt aus dem Geometry-Term) gewichtet, um somit Lo (ausgesendete Lichtmenge in Richtung wo) zu berechnen.
Das Integral läuft über alle möglichen wo-Richtungen und bildet darüber die Summe. Wenn also eine Fläche lediglich von einer einzigen Richtung wi Li Lichtenergie bekommt. So sagt mir dieses Integral dann, wie viel Lichtenergie insgesamt die Fläche ausstrahlt. Und diese Menge darf eben nicht mehr sein, als wie sie empfängt.

Wenn wir jetzt nun für fr nun unsere diffuse Brdf sein soll und wir aber nicht wissen, wie sie dann nun aussehen muss so setzen wir erstmal für fr die Zahl 1 ein. D.h. Die Fläche reflektiert so viel Licht wie möglich. Es muss jetzt nur noch geprüft werden, ob die <=Li-Gleichung weiterhin gilt.

Um diese Gleichung überprüfen zu können, muss man den dw-Term verstehen. Der dw-Term ist die türkisfarbene Fläche, welche im nachfolgenden Bild gezeigt wird und mit dΩ hier bezeichnet wird.
https://www.scratchapixel.com/images/upload/shading-intro/shad-light-beam7.png?

Ich ersetze dw durch sin(ɵ)dɵdɸ und fr(wi,wo) durch pd (Diffuse-Konstante, welche angibt, wie viel Prozent reflektiert wird).

§\forall w_i:\int _{\theta=0}^{2\pi}\int
_{\phi=0}^{\pi/2}p_d\ast L_i\ast \cos \phi_o\sin
(\phi_o)\mathit{d\phi}_o\mathit{d\theta}_o\le L_i§


Ich schreibe den Li-Term und pd vor die Integrale (beides Konstanten) und ordne nach ɸ und ɵ
§\forall w_i:L_i\ast p_d\ast \int _{\theta=0}^{2\pi}\mathit{d\theta}_o\ast \int
_{\phi=0}^{\pi/2}\cos \phi_o\sin
(\phi_o)\mathit{d\phi}_o\le L_i§


Das ɸ-Integral löst sich so:

§\int
_{\theta=0}^{2\pi}\mathit{d\theta}=[\theta]_0^{2\pi}=2\pi§


Für das zweite Integral finden wir die Stammfunktion mithilfe von Google^^
https://www.zum.de/Faecher/M/NRW/pm/mathe/stammfkt.htm

§\int _{\phi=0}^{\pi/2}\cos \phi\sin
(\phi)\mathit{d\phi}=[\frac 1 2\ast \sin ²\phi]_0^{\pi/2}=\frac 1
2\ast \sin ²(\pi/2)-\frac 1 2\ast \sin ²0=\frac 1 2-0=\frac 1 2§


Ich setze die beiden aufgelösten Integrale in die Ausgangsformel ein:

§\forall w_i:L_i\ast p_d\ast 2\pi\ast \frac 1 2\le L_i§

Ich kürze Li weg und multipliziere die Zahlen aus:
§\forall w_i:p_d\ast \pi\le 1§

Damit diese Gleichung immer erfüllt ist, darf pd nie größer als 1/П werden.

Will ich nun eine rote diffuse Fläche haben, dann darf die Rot-Komponente maximal 1/П reflektieren. Würde sie mehr reflektieren, dann würde die Lichtmenge in der Szene immer heller werden und wir erhalten ein unrealistisches Bild.

So wird nun der Raytracer um die Brdf erweitert:

C#:
1
2
3
4
5
6
7
8
9
static class DiffuseBrdf
{
    public static Vector Evaluate(IntersectionPoint point)
    {
        return point.Color / (float)Math.PI;
    }
}
 
pixelColor += Vector.Mult(DiffuseBrdf.Evaluate(eyePoint), eyePoint.Color * GeometryTerm(eyePoint, lightPoint));


https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/02_DirectLightingOnly/Images/WithBrdf.png

Das wirkt jetzt erst mal wie ein billige Verbesserung. Einfach nur die Farbe mit den Faktor PI wichten. Das ist nun wirklich kein Hexenwerk aber in ein späteren Abschnitt wird das hier noch wichtig und dann werden wir froh sein, dass wir diese PI-Division haben.

Beispielcode (In SimpleRenderer.cs ist das Version 3)

https://github.com/XMAMan/RaytracingTutorials/tree/master/02_DirectLightingOnly/RaytracingTutorials

_________________
Anfänger vom Dienst und Raytracingfreund


Zuletzt bearbeitet von XMAMan am 15:04:35 10.10.2017, insgesamt 3-mal bearbeitet
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:33:50 10.10.2017   Titel:              Zitieren

Schattenkanten verbessern

Die nächste Verbesserung wird nun das pixelige aussehen von den Schattenkanten verbessern. Dazu muss man lediglich wie folgt vorgehen:

C#:
1
2
3
4
5
6
7
8
9
10
11
12
for (int x = 0; x < width; x++)
 for (int y = 0; y < height; y++)
 {
     int sampleCount = 10;
     Vector sum = new Vector(0, 0, 0);
     for (int i = 0; i < sampleCount; i++)
     {
         sum += EstimatePixelColor(x, y, rand);
     }
     sum /= sampleCount;
     image.SetPixel(x, y, VectorToColor(sum));
 }


Anstatt pro Pixel nur einen Farbwert zu berechnen mache ich die vorher beschriebene Berechnung 10 mal. Bei jeden Rechenschritt bestimme ich ein neuen zufälligen Punkt auf der Lichtquelle.

So sieht das Bild dann aus, wenn ich mit verschiedenen Sample-Schritten arbeite:

https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/02_DirectLightingOnly/Images/SoftShadows.png

Beispielcode (In SimpleRenderer.cs ist das Version 4)

https://github.com/XMAMan/RaytracingTutorials/tree/master/02_DirectLightingOnly/RaytracingTutorials

Das ist nun der einfache Raytracer, welcher nur direktes Licht in Betracht zieht. Wenn man sich die Schatten ansieht, dann fällt auf, das diese schon ziemlich dunkel sind. Der Grund dafür ist, dass wenn ein Punkt im Schatten liegt, dann wird die Pixelfarbe fix auf (0,0,0) gesetzt. Wenn ich aber auch indirektes Licht möchte, dann benötige ich ein Algorithmus, welcher globale Beleuchtung genannt wird. Im nächsten Abschnitt werde ich darauf näher eingehen.

_________________
Anfänger vom Dienst und Raytracingfreund
XMAMan
Mitglied

Benutzerprofil
Anmeldungsdatum: 07.02.2011
Beiträge: 275
Beitrag XMAMan Mitglied 14:34:40 10.10.2017   Titel:              Zitieren

Globale Beleuchtung

Bei der globalen Beleuchtung wird berechnet, wie viel Licht zu ein gegebenen Punkt sowohl direkt von der Lichtquelle, als auch indirekt über die Reflektion über die Wände, gesendet wird.

Hier sind einige Flächen und in gelb die Pfade, die ein Photon zurück legt, dargestellt.
https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/03_MonteCarloIntegration/Images/LightPaths.png

Bei direkten Licht besteht ein Lichtpfad entweder aus 2 Punkten (Kamera-Punkt + Punkt auf Lichtquelle) oder aus 3 Punkten (Kamera-Punkt, Lichtquellenpunkt, Punkt in Szene).

Ein indirekter Lichtpfad besteht aus mehr als 3 Punkten.

Wenn ich nun berechnen will, wie viel Licht über so ein Pfad von der Lichtquelle bis zur Kamera fliegt, dann muss ich das Pfadgewicht (PathContribution) für ein gegebenen Lichtpfad berechnen.

Dieses Pfadgewicht ist ein Vektor. Er gibt für die RGB-Farben an, wie viel Licht er jeweils durchlässt. Das Pfadgewicht errechnet sich aus dem Produkt der GeometryTerme (Float-Zahl) und der Brdfs (RGB-Vektor).
https://raw.githubusercontent.com/XMAMan/RaytracingTutorials/master/03_MonteCarloIntegration/Images/PathContribution.png

Wenn ich nun ein Bild mit globaler Beleuchtung berechnen will, so muss ich alle möglich Lichtpfade, die man zwischen der Kamera und der Lichtquelle aufstellen kann, berechnen und für diese jeweils ihren Anteil zur Pixelfarbe hinzuaddieren.

Wenn ich die Summe über alle möglichen Lichtpfade bilde, die man bilden kann, dann weiß ich die Farbe für jedes Pixel und kann somit das Bild ausgeben.

Es gibt dabei folgende Schwierigkeiten. Ein einzelnen Punkt aus dem Lichtpfad ist ja ein 3D-Punkt, dessen Koordinaten über 3 Float-Zahlen angegeben werden. Dieser 3D-Punkt liegt auf ein Dreieck in der Szene. Wenn ich nun alle möglichen Punktpositionen erzeugen will, die es in der jeweiligen Szene gibt, dann sind das ja schier unendlich viele. Die Schleife, welche die Summe bildet, hätten also extrem viele Durchläufe und das würde sehr lange dauern. Außerdem gibt es ja unendlich viel verschiedenen Lichtpfadlängen. Erst erzeuge ich alle möglichen Lichtpfade mit 2 Punkten. Wenn ich die Summiert habe, dann erzeuge ich alle möglichen Pfade mit 3 Punkten. So geht das immer weiter bis ich bei einer unendlichen langen Pfadlänge bin.

Wie kann man diese Aufgabe in endlicher Zeit lösen?

Es gibt zwei Ansätze (die ich kenne):
Erster Ansatz (Radiosity): Ich unterteile die Szene in lauter Dreiecke und/oder Vierecke (Patches genannt) und ein Lichtpfad-Punkt darf immer nur in der Mitte von ein Patch liegen. Somit ist die Menge der möglichen Lichtpfad-Positionen schon stark eingeschränkt. Weiterhin beschränke ich die maximale Pfadlänge auf z.B. 10. Nun gibt es viel weniger Lichtpfade, die ich summiere muss und somit kann ich es schaffen.

Zweiter Ansatz(Monte Carlo Integration): Ich presse den gesamten Beleuchtungsalgorithmus in folgende Formel:


§Pixelfarbe = \frac 1 N\sum _{i=1}^N\frac{f(\mathit{xi})}{\mathit{pdf}(\mathit{xi})}§

f ist hier die PathContribution-Funktion. Xi ist zufällig generierter Lichtpfad (Array von 3D-Punkten). N ist die Anzahl der Samples. Pdf ist die Probabilty density function.

In den folgenden Abschnitten gehe ich näher auf die Monte Carlo Integration ein. Hier in diesen Abschnitt wollte ich erst mal nur klären, was ein Lichtpfad ist und wie man sich das visuell vorstellen kann.

_________________
Anfänger vom Dienst und Raytracingfreund
C++ Forum :: Spiele-/Grafikprogrammierung ::  Raytracing / Pathtracing / Photon Mapping - Tutorial  
Gehen Sie zu Seite 1, 2, 3  Weiter
Auf Beitrag antworten

Zeige alle Beiträge auf einer Seite




Nächstes Thema anzeigen
Vorheriges Thema anzeigen
Sie können Beiträge in dieses Forum schreiben.
Sie können auf Beiträge in diesem Forum antworten.
Sie können Ihre Beiträge in diesem Forum nicht bearbeiten.
Sie können Ihre Beiträge in diesem Forum nicht löschen.
Sie können an Umfragen in diesem Forum nicht mitmachen.

Powered by phpBB © 2001, 2002 phpBB Group :: FI Theme

c++.net ist Teilnehmer des Partnerprogramms von Amazon Europe S.à.r.l. und Partner des Werbeprogramms, das zur Bereitstellung eines Mediums für Websites konzipiert wurde, mittels dessen durch die Platzierung von Werbeanzeigen und Links zu amazon.de Werbekostenerstattung verdient werden kann.

Die Vervielfältigung der auf den Seiten www.c-plusplus.de, www.c-plusplus.info und www.c-plusplus.net enthaltenen Informationen ohne eine schriftliche Genehmigung des Seitenbetreibers ist untersagt (vgl. §4 Urheberrechtsgesetz). Die Nutzung und Änderung der vorgestellten Strukturen und Verfahren in privaten und kommerziellen Softwareanwendungen ist ausdrücklich erlaubt, soweit keine Rechte Dritter verletzt werden. Der Seitenbetreiber übernimmt keine Gewähr für die Funktion einzelner Beiträge oder Programmfragmente, insbesondere übernimmt er keine Haftung für eventuelle aus dem Gebrauch entstehenden Folgeschäden.