Schnelle Pixelmanipulationen in C# Bitmaps

Mit dem Befehl SetPixel der Klasse System.Drawing.Bitmap lassen sich einzelne Pixel in einem Bitmap verändern. Leider arbeitet diese Methode sehr langsam, und es ist nicht möglich sie aus mehreren Threads gleichzeitig aufzurufen. Möchte man also ein Bitmap Pixel für Pixel durch mehrere Threads berechnen lassen, so muss man die SetPixel-Aufrufe z.B. durch lock(…) Anweisungen vor zeitgleichen Aufrufen schützen. Dies macht den Geschwindigkeitsvorteil, den mehrere Threads bringen, teilweise wieder zunichte.

Abhilfe schafft die hier vorgestellte Klasse RawRgbBitmap. Sie bietet ebenfalls eine Methode namens SetPixel, welche jedoch deutlich schneller arbeitet als die der Bitmap-Klasse und zudem von mehreren Threads gleichzeitig aufgerufen werden kann. Durch den „implicit operator Bitmap“ wird ein RawRgbBitmap automatisch in ein System.Drawing.Bitmap umgewandelt, wann immer es einer System.Drawing.Bitmap-Variablen zugewiesen wird.

Und so funktioniert’s:

Der Konstruktor der Klasse RawRgbBitmap erzeugt zunächst ein Byte-Array. Anschließend wird ein neues System.Drawing.Bitmap erzeugt, dessen Bildspeicher auf genau dem gleichen Speicherbereich abgelegt wird, wie das Byte-Array. Wir können nun also alle Pixelmanipulationen direkt an dem Byte-Array vornehmen, ohne dafür das (langsame) Bitmap-Objekt zu verwenden. Da das Bitmap jedoch auf dem gleichen Speicherbereich liegt, wie das Array, wirken sich alle Änderungen im Array auch auf das Bitmap aus.

    public class RawRgbBitmap :IDisposable
    {
        int width;
        int height;
        int stride;

        byte[] raw;
        GCHandle handle;
        Bitmap bmp;

        public RawRgbBitmap(int width, int height)
        {
            this.width = width;
            this.height = height;
            int validBitsPerLine = width * 24;
            // Jede Zeile im Array muß ein ganzes Vielfaches von einem int32 (4 Byte) sein. Dies ist also die Schrittweite pro Bildzeile im Array:
            stride = ((validBitsPerLine + 31) / 32) * 4;

            raw = new byte[stride * height];
            handle = GCHandle.Alloc(raw, GCHandleType.Pinned);
            bmp = new Bitmap(width, height, stride, PixelFormat.Format24bppRgb, handle.AddrOfPinnedObject());
        }

        public void SetPixel(int x, int y, Color c)
        {
            int idx = y * stride + x*3;
            raw[idx] = c.B;
            raw[idx+1] = c.G;
            raw[idx+2] = c.R;
        }

        public static implicit operator Bitmap(RawRgbBitmap rbmp)
        {
            return rbmp.bmp;
        }

        public void Dispose()
        {
            bmp.Dispose();
            handle.Free();
        }
    }

Raytracing Teil 6: Das erste Bild rendern

Mit den Grundlagen, die wir in den vorherigen Teilen dieser Artikelserie erarbeitet haben, werden wir nun unsere erste 3D Grafik rendern. Zunächst erweitern wir die Klasse Scene um zwei Methoden „AddSphere“ und „AddParallelepiped“, mit denen wir einen Spat (bzw. Quader oder Würfel) und eine Kugel in die Szene einfügen können. Diese Methoden berechnen alle einzelnen Primitive, aus denen der Spat bzw. die Kugel besteht und fügen diese in die Szene ein.

Wir fügen die folgenden Methoden in die Klasse Scene ein:

        public void AddParallelepiped(Vector3D Origin, Vector3D U, Vector3D V, Vector3D W, Color color)
        { // Spat hinzufügen
            Primitives.Add(new Lozenge(Origin, U, V, color));
            Primitives.Add(new Lozenge(Origin, U, W, color));
            Primitives.Add(new Lozenge(Origin, V, W, color));
            Primitives.Add(new Lozenge(Origin + W, U, V, color));
            Primitives.Add(new Lozenge(Origin + V, U, W, color));
            Primitives.Add(new Lozenge(Origin + U, V, W, color));
        }

        public void AddSphere(Vector3D center, double radius, int noOfSteps, Color color)
        {
            double step = Math.PI / noOfSteps;

            for (int bN = 1; bN < noOfSteps; bN++)
            {
                double b = bN * step;
                for (int aN = 0; aN < noOfSteps * 2; aN++)
                {
                    double a = aN * step;

                    Vector3D p1 = new Vector3D(radius * Math.Cos(a) * Math.Sin(b), radius * Math.Cos(b), radius * Math.Sin(a) * Math.Sin(b)) + center;
                    Vector3D p2 = new Vector3D(radius * Math.Cos(a + step) * Math.Sin(b), radius * Math.Cos(b), radius * Math.Sin(a + step) * Math.Sin(b)) + center;
                    Vector3D p3 = new Vector3D(radius * Math.Cos(a) * Math.Sin(b + step), radius * Math.Cos(b+step), radius * Math.Sin(a) * Math.Sin(b + step)) + center;
                    Primitives.Add(new Trapezoid(p1, p2 - p1, p3 - p1, color));
                } 
            }

            for (int aN = 0; aN < noOfSteps * 2; aN++)
            {
                double a = aN * step;
                double b = step;
                Vector3D p1 = new Vector3D(radius * Math.Cos(a) * Math.Sin(b), radius * Math.Cos(b), radius * Math.Sin(a) * Math.Sin(b)) + center;
                Vector3D p2 = new Vector3D(radius * Math.Cos(a + step) * Math.Sin(b), radius * Math.Cos(b), radius * Math.Sin(a + step) * Math.Sin(b)) + center;
                Vector3D p3 = new Vector3D(0,radius,0) + center;
                Primitives.Add(new Trapezoid(p1, p2 - p1, p3 - p1, color));
            }
        }

Wir erstellen nun ein neues Fenster, bzw. eine Form und fügen eine PictureBox mit dem Namen „picture“ ein. Den Quelltext der Form ändern wir wie folgt:

    public partial class Form1 : Form
    {
        Scene scene = new Scene();
        RayTraceCamera cam;
        double camAngle = 0;

        public Form1()
        {
            InitializeComponent();

            // Elemente zur Szene hinzufügen:
            scene.Primitives.Add(new Plane(Vector3D.Zero, Vector3D.eX, Vector3D.eZ, Color.Green)); // Endlos ausgedehnter grüner Boden
            scene.AddParallelepiped(Vector3D.Zero, Vector3D.eX*4, Vector3D.eY*2, Vector3D.eZ*4, Color.Blue); // Blauer Quader
            scene.AddSphere(new Vector3D(-3,2.5,0), 2.5, 6, Color.Red); // Rote Kugel

            camAngle = 2.0; // Der Winkel, aus dem die Kamera auf die Szene schaut (in Radiant).
            Vector3D p = new Vector3D(Math.Cos(camAngle) * 23, 7, Math.Sin(camAngle) * 23); // Position der Kamera aus Winkel berechnen
            cam = new RayTraceCamera(scene, p, new Vector3D(0, 3, 0) - p);
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            cam.ResizeScreen(picture.Width, picture.Height); // Kamera-Screen an PictureBox anpassen
            picture.Image = cam.RenderBitmap(); // Das fertige Bild Rendern und in der PictureBox anzeigen
        }
    }

Beim Start der Applikation wird eine Szene erstellt und ein grüner Plane als Boden eingefügt. Es folgt ein blauer Quader und eine rote Kugel, welche durch viele kleine Primitive angenähert wird. Anschließend wird eine RayTraceCamera erstellt, welche ein Bild der Szene rendert. Nach dem Start der Applikation wird folgendes Bild angezeigt:

Alle Objekte werfen Schatten auf den Boden. Der Schatten der Kugel fällt sogar teilweise auf den Quader. Der Boden ist unendlich ausgedehnt und trifft hinten im Bild auf den Horizont. Die Farbe des Himmels ist die Farbe „Background“ des Scene-Objekts.

RayTracing Teil 5: Dreiecke, Trapeze und Rauten als weitere ebene Primitive

Basierend auf dem Plane-Primitiv, welches eine unendlich ausgedehnte Ebene darstellt können wir mit wenig Aufwand weitere ebene Primitive implementieren. Zum Beispiel Rauten, Trapeze und Dreiecke. Alle diese Formen sind eben und stellen damit also nur einen Ausschnitt aus einem Plane-Primitiv dar. Wir können für jede dieser geometrischen Grundformen also eine neue Klasse erstellen, welche von Plane erbt, und überschreiben nur die Methode „RayTrace“. Die neuen „RayTrace“ Methoden werden zu der Methode „RayTrace“ aus der Plane Klasse sehr ähnlich implementiert: Wir berechnen wieder den Schnittpunkt des Rays mit der Ebene, welche von V und W aufgespannt wird. Anschließend prüfen wir jedoch noch zusätzlich, ob der Schnittpunkt auch innerhalb der Form liegt. Ist dies der Fall, geben wir einen PointOnPlane zurück, andernfalls den Wert null.

Das Raute-Primitiv

    public class Lozenge : Plane // Eine Raute, die von V und W aufgespannt wird
    {
        public Lozenge(Vector3D Origin, Vector3D V, Vector3D W, Color color) : base(Origin, V, W, color)
        {
        }

        public override PointOnPrimitive RayTrace(Ray ray)
        {
            double v;
            double w;
            double d;
            if (Vector3D.cramer(V, W, ray.Direction, ray.Start - Origin, out v, out w, out d))
            {
                if (w < 0 || w > 1) return null; // Schnittpunkt außerhalb Raute
                if (v < 0 || v > 1) return null; // Schnittpunkt außerhalb Raute
                if (-d > 0) return new PointOnPlane(v, w, this, -d);
            }
            return null; // Ray und Lozenge schneiden sich nicht
        }
    }

Das Dreieck-Primitiv

    public class Triangle : Plane // Ein Dreieck, das von V und W aufgespannt wird
    {
        public Triangle(Vector3D Origin, Vector3D V, Vector3D W, Color color) : base(Origin, V, W, color)
        {
        }

        public override PointOnPrimitive RayTrace(Ray ray)
        {
            double v;
            double w;
            double d;
            if (Vector3D.cramer(V, W, ray.Direction, ray.Start - Origin, out v, out w, out d))
            {
                if (v < 0 || w < 0 || v + w > 1) return null; // Schittpunkt zur Ebene außerhalb Dreieck
                if (-d > 0) return new PointOnPlane(v, w, this, -d);
            }
            return null; // Ray und Dreieck schneiden sich nicht
        }
    }

Das Trapez-Primitiv

    public class Trapezoid : Plane // Ein Trapez, das von der Grundseite V und einem Schenkel W aufgespannt wird
    {
        public Trapezoid(Vector3D Origin, Vector3D V, Vector3D W, Color color) : base(Origin, V, W, color)
        {
        }

        public override PointOnPrimitive RayTrace(Ray ray)
        {
            double v;
            double w;
            double d;
            if (Vector3D.cramer(V, W, ray.Direction, ray.Start - Origin, out v, out w, out d))
            {
                if (v < 0 || w < 0 || w > 1) return null;
                double bottomMinusTop = 2 * (V.normalize() * W) / V.length();
                double vmax = 1 - w * bottomMinusTop;
                if (v > vmax) return null;
                if (-d > 0) return new PointOnPlane(v, w, this, -d);
            }
            return null; // Ray und Trapez schneiden sich nicht
        }
    }

Weiter

Raytracing Teil 4: Das Plane-Primitiv

Im ersten Teil dieser Artikelserie haben wir die beiden abstrakten Klassen Primitive und PointOnPrimitive definiert. Ein Primitiv ist allgemein eine elementare geometrische Form, welche vom Raytracer direkt verarbeitet werden kann. Alle komplexeren Formen und Objekte müssen aus mehreren Primitiven zusammengesetzt werden. Schreiben wir nun unsere erste Primitiv-Klasse namens „Plane“, welche von „Primitive“ erbt. Ein Plane ist eine unendlich ausgedehnte Ebene, welche ausgehend von dem Stützpunkt „Origin“ von den beiden Vektoren V und W aufgespannt wird:

public class Plane : Primitive // Eine unendliche Ebene, die von V und W aufgespannt wird
    { 
        protected Vector3D Origin { get; private set; }
        protected Vector3D W { get; private set; } // Vektor in W-Richtung (lokales Koordinatensystem auf der Ebene) [Daumen rechte Hand]
        protected Vector3D V { get; private set; } // Vektor in V-Richtung (lokales Koordinatensystem auf der Ebene) [Mittelfinger rechte Hand]. Entspricht der unteren Kante des TexturBitmap

        protected Vector3D Normal { get; private set; } // Einheitsvektor Senkrecht zu V und W [Zeigefinger Rechte-Hand-Regel]
        protected double CosAlpha { get; private set; } // Cosinus des von V und W eingeschlossenen Winkels.
        protected double Vlength { get; private set; }
        protected double Wlength { get; private set; }

        public Plane(Vector3D Origin, Vector3D V, Vector3D W, Color color) : base(color)
        {
            this.Origin = Origin; this.V = V; this.W = W;
            Normal = V.cross(W).normalize();
            CosAlpha = (V * W) / (V.length() * W.length());
            Vlength = V.length();
            Wlength = W.length();
        }

Dem Konstruktor wird neben den drei Vektoren Origin, V und W auch noch eine Farbe (Color) übergeben. Dies ist die Farbe des Primitivs. Im Konstruktor werdennoch einige Werte berechnet, welche des Öfteren benötigt werden: Der Normalenvektor „Normal“, welcher senkrecht auf der Ebne steht, der Kosinus CosAlpha des zwischen V und W eingeschlossenen Winkels und die Längen der Vektoren V und W.
Bevor wir die Methode „RayTrace“ aus der abstrakten Klasse „Primitive“ in die Klasse „Plane“ implementieren, erstellen wir noch die Klasse „PointOnPlane“, welche eine Implementierung der abstrakten Klasse „PointOnPrimitive“ darstellt. Neben dem Konstruktor gibt es in dieser Klasse noch die Methoden „GetColor“, welche die Farbe des PointOnPrimitive liefert, „GetNormal“, welche den Normalenvektor der Ebene zurück gibt und „GetPrimitive“, welche das Primitiv selbst, auf dem sich der PointOnPrimitive befindet, zurück liefert. Weiterhin können wir einen PoitOnPrimitive mit der Methode ToPointInSpace in XYZ-Koordinaten umwandeln. Die Klassenvariablen v und w sind dabei die Vielfachen der Vektoren V und W der Ebene. Die XYZ-Koordinaten lassen sich also so berechnen: PointInSpace = Origin + V*v + W*w .

        public class PointOnPlane : PointOnPrimitive
        {
            private double v; // in Vielfachen von V.length()
            private double w; // in Vielfachen von W.length()
            private Plane plane;

            public PointOnPlane(double v, double w, Plane plane, double distanceToRayStart) : base(distanceToRayStart)
            {
                this.v = v;
                this.w = w;
                this.plane = plane;
            }

            public override Color GetColor()
            {
                return plane.color;
            }

            public override Vector3D GetNormal()
            {
                return plane.Normal;
            }

            public override Vector3D ToPointInSpace()
            {
                return plane.Origin + (plane.V * v) + (plane.W * w);
            }

            public override Primitive GetPrimitive()
            {
                return plane;
            }
        } // class PointOnPlane

Um den Schnittpunkt einer Ebene mit einem Ray zu ermitteln müssen wir das folgende lineare Gleichungssystem nach den skalaren Variablen v, w und d lösen:
V*v + W*w + rayDirection*d = rayStart – Origin
Die Klasse Vector3D stellt die Methode „cramer“ bereit, welche ein lineares Gleichungssystem mit Hilfe der Cramerschen Regel löst. Wenn das Gleichungssystem lösbar ist, gibt „cramer“ true zurück. Wenn sich der Punkt dann noch vor der Kamera (und nicht dahinter) befindet, erstellt „Raytrace“ einen neuen PointOnPlane als Rückgabewert.

        public override PointOnPrimitive RayTrace(Ray ray)
        {
            double v;
            double w;
            double d;
            if (Vector3D.cramer(V, W, ray.Direction, ray.Start - Origin, out v, out w, out d))
            {
                if (-d > 0) return new PointOnPlane(v, w, this, -d);
            }
            return null; // Ray und Plane sind parallel, oder der Schnittpunkt liegt in negative Richtung
        }

Mit der Methode „RayTrace“ können wir ein Plane-Primitiv also fragen, ob und an welcher Stelle es von einem Ray getroffen wird. Damit ist auch diese Klasse fertig.

Weiter

Raytracing Teil 3: Die Szene

Im ersten Teil dieser Artikelreihe haben wir das Interface „IRayTraceable“, welches die Schnittstelle der Szene zur RayTraceCamera aus Teil 2 darstellt, definiert. Jedes Objekt, welches „IRayTraceable“ implemetiert, kann von der RayTraceCamera als Szene verwendet werden.

public interface IRayTraceable
{
    Vector3D DirectionToSun { get; set; }
    Color Background { get; set; }
    double MaxLightening { get; set; } // Gültige Werte von 0 (Originalfarbe) bis 1 (jede Farbe wird bis auf Weiss aufgehellt)
    double MaxDarkening { get; set; } // Gültige Werte von 0 (Originalfarbe) bis -1 (jede Farbe wird bis auf Schwarz abgedunkelt)
    PointOnPrimitive RayTrace(Ray ray);
    bool RayTraceHits(Ray ray); // Liefert true, wenn der Ray auf ein beliebiges Primitive trifft.
} 

Wir werden uns nun mit der Klasse „Scene“ beschäftigen, welche eine Implementierung dieses Interfaces darstellt:

    public class Scene : IRayTraceable
    {
        public Color Background { get; set; }
        public Vector3D DirectionToSun { get; set; }
        public double MaxLightening { get; set; } // Gültige Werte von 0 (Originalfarbe) bis 1 (jede Farbe wird bis auf Weiss aufgehellt)
        public double MaxDarkening { get; set; } // Gültige Werte von 0 (Originalfarbe) bis -1 (jede Farbe wird bis auf Schwarz abgedunkelt)

        public List<Primitive>; Primitives = new List<Primitive>();

        public Scene()
        {
            Background = Color.SkyBlue;
            DirectionToSun = new Vector3D(1, 1, 1).normalize();
            MaxLightening = 0.8;
            MaxDarkening = -0.8;
        }
}

Der Konstruktor setzt Standardwerte für alle Properties aus dem Interface. Weiterhin bekommt die Klasse die Liste „Primitives“, in der alle Primitive der Szene abgelegt werden sollen. Nun fehlen noch die Methoden „RayTrace“ und „RayTraceHits“ aus dem Interface. Die Methode „RayTrace“ soll ermitteln ob und auf welches Primitiv ein Ray trifft. Zurückgegeben wird ein PointOnPrimitive. Sollte der Ray auf mehrere Primitive treffen, so wird nur der PointOnPrimitive zurückgegeben, welcher der Kamera am nächsten ist, denn alle dahinterliegenden Primitive sind für die Kamera nicht sichtbar. Es gibt mehrere Möglichkeiten das entsprechende Primitiv zu ermitteln. Leider sind effiziente Algorithmen für diese Aufgabe nicht einfach zu implementieren. Daher wollen wir uns mit diesem sehr einfachen, aber auch sehr langsamen Algorithmus begnügen: Wir überprüfen einfach immer alle Primitive auf Schnittpunkte mit dem Ray. Dies hat den Nachteil, dass auch Primitive aus weit entfernten Bildbereichen immer mit geprüft werden. Zum Beschleunigen des Raytracings gibt es in dieser Funktion ein großes Potential.

        public PointOnPrimitive RayTrace(Ray ray)
        {
            PointOnPrimitive nearestPoint = null;
            foreach (var p in Primitives)
            {
                PointOnPrimitive q = p.RayTrace(ray);
                if (nearestPoint == null || (q != null && q.distanceToRayStart < nearestPoint.distanceToRayStart)) nearestPoint = q;
            }
            return nearestPoint;
        }

Die Methode „RayTraceHits“ soll nur ein true oder false zurück liefern, abhängig davon ob ein beliebiges Primitiv von dem Ray getroffen wird, oder nicht. Wir können sie also ähnlich der Methode „RayTrace“ implementieren, jedoch reicht es, wenn wir die Schleife über alle Primitive abbrechen, sobald wir den ersten Schnittpunkt mit einem Primitiv gefunden haben:

        public bool RayTraceHits(Ray ray)
        {
            foreach (var p in Primitives)
            {
                PointOnPrimitive pop = p.RayTrace(ray);
                if (pop != null) return true; // Schnittpunkt mit Primitiv gefunden
            }
            return false;
        }

Nachdem wir diese beiden Methoden zur Klasse „Scene“ hinzugefügt haben, ist sie einsatzbereit.

Weiter

 

Raytracing Teil 2: Die Kamera

Im ersten Teil dieser Artikelreihe haben wir uns mit den Grundlagen des Raytracings beschäftigt und Schnittstellen definiert, welche die einzelnen Softwarekomponenten bzw. ihre Funktionalität beschreiben. Wir werden im Folgenden eine der wichtigsten Komponente entwickeln: Die Kamera.

Aufgabe der Kamera ist es eine Bitmap-Grafik von einer Szene zu rendern. Sie sendet dazu für jeden Pixel einen Ray durch die Szene und ermittelt so, auf welches Primitiv der jeweilige Ray trifft. Der Pixel bekommt dann die Farbe des Primitivs. Weiterhin untersucht die Kamera die Ausrichtung der Fläche, auf der ein Ray endet, zum Licht und hellt den Pixel entsprechend auf, oder dunkelt ihn ab.

Eine Kamera hat eine Position im Raum, welche durch den Vektor CamPosition beschrieben wird. Weiterhin hat sie eine Blickrichtung, welche durch den Vektor CamDirection beschrieben wird. Die Länge des Vektors CamDirection entspricht dem Abstand der Abbildungsebene von der CamPosition. Um einer Verwechselungsgefahr mit dem dreidimensionalen Koordinatensystem x, y, z vorzubeugen, wollen wir die zweidimensionalen Koordinaten auf der Abbildungsebene nicht mit x und y sondern mit v und w bezeichnen. Die CamPosition befindet sich genau senkrecht über dem Nullpunkt (v=0, w=0) der Abbildungsebene. Eine Längeneinheit auf der Abbildungsebene entspricht genau einem Pixel. Der im Bild grün hervorgehobene Pixel hat also die Koordinaten (v=2, w=4). Da der Nullpunkt der Abbildungsebene hier genau in der Mitte des Bildes (und nicht wie sonst üblich in der oberen linken Ecke) liegt, können die Koordinaten eines Pixels auch negativ sein.

Die Klasse RayTraceCamera

Wir erstellen nun die Klasse RayTraceCamera. Im Konstruktor bekommt diese Klasse unter anderem ein IRayTraceable übergeben. Dies ist die Szene, in der sich alle Primitive befinden. Wie im vorhergehenden Teil beschrieben enthält das Interface IRayTraceable alle Methoden, die wir benötigen um die Szene mit Strahlen bzw. Rays zu untersuchen und daraus ein Bild zu rendern. Wie diese Methoden der Szene genau implementiert werden, soll uns an dieser Stelle zunächst egal sein. Weiterhin bekommt der Konstruktor der RayTraceCamera die Position und Blickrichtung der Kamera übergeben.

    public class RayTraceCamera
    {
        IRayTraceable scene;
        Vector3D camPosition;
        Vector3D camDirection; // Direction Vector with Length = eyeDistanceToScreen
        Vector3D camDirectionNorm; // Normalized Direction Vector
        Vector3D upDirection = Vector3D.eY; // Richtung nach oben 
        const double eyeDistanceToScreen = 1.5; // Abstand des Auges zur Abbildungsebene in Längeneinheiten des XYZ-Koordiantensystems
        const double screenWidth = 1; // Die Breite der Abbildungsebene in Längeneinheiten des XYZ-Koordiantensystems
        Vector3D eV; // Einheitsvektor in V-Richtung (horizontal) der Screen Ebene mit der Länge 1px
        Vector3D eW; // Einheitsvektor in W-Richtung (vertikal) der Screen Ebene mit der Länge 1px

        int screenWidthPx = 300;
        int screenHeightPx = 200;

        public RayTraceCamera(IRayTraceable scene, Vector3D camPosition, Vector3D camDirection)
        {
            this.scene = scene;
            MoveTo(camPosition, camDirection);
        }

        public void MoveTo(Vector3D position, Vector3D direction, Vector3D upDirection = null)
        {
            if (upDirection != null) this.upDirection = upDirection.normalize(); // wenn null, bleibt die upDirection unverändert
            camPosition = position;
            camDirectionNorm = direction.normalize();
            camDirection = camDirectionNorm * eyeDistanceToScreen; // direction-Länge auf eyeDistanceToScreen ändern
            Init();
        }

        public void ResizeScreen(int widthPx, int heightPx)
        { // Größe des gerenderten Bild in Pixeln setzen
            screenWidthPx = widthPx;
            screenHeightPx = heightPx;
            Init();
        }

        private void Init()
        {
            eV = camDirectionNorm.cross(this.upDirection);
            eW = eV.cross(camDirectionNorm);
            eV = eV * (screenWidth / screenWidthPx); // Länge auf 1px ändern
            eW = eW * (screenWidth / screenHeightPx); // Länge auf 1px ändern
        }
    }

Betrachten wir zunächst die Klassenvariablen: „scene“, „camPosition“ und „camDirection“ wurden bereits erwähnt und werden hier nicht weiter erläutert. Der Vektor „camDirectionNorm“ zeigt in die gleiche Richtung wie „camDirection“, er bekommt jedoch stets die Länge 1. Bisher haben wir nicht berücksichtigt, wie die Kamera seitlich geneigt ist (Rollwinkel). Um ein Bild zu Rendern brauchen wir jedoch diese Information (schließlich könnte die Kamera auch auf dem Kopf stehen). Daher gibt es den Vektor „upDirection“ der festlegt, wo in der Szene oben sein soll. Dieser Vektor wird mit dem Einheitsvektor eY vorbelegt. Im allgemeinen ist es nicht notwendig diesen Vektor zu ändern – auch dann nicht, wenn die Kamera nach unten oder oben geneigt wird. Die Konstante „eyeDistanceToScreen“ definiert den Abstand des Auges zur Abbildungsebene. Sie entspricht der Brennweite des Kameraobjektivs. Würde man den Wert vergrößern, so würde man in die Szene hineinzoomen. Die Länge des Vektors „CamDirection“ wird, wenn die Blickrichtung der Kamera geändert wird, stets an diesen Wert angepasst.
Die Konstante „screenWidth“ definiert, wie breit die Abbildungsebene in Längeneinheiten des XYZ-Koordinatensystems ist. Wenn wir diesen Wert durch die Anzahl der Bildbreite in Pixeln dividieren, erhalten wir die Größe eines Pixels im XYZ-Koordinatensystem.
Für unsere Berechnungen benötigen wir weiterhin zwei Vektoren, welche die Abbildungsebene selbst aufspannen, also der V und W-Achse entsprechen. Wir definieren daher noch die Einheitsvektoren eV und eW, welche parallel zur V bzw. W-Achse verlaufen und die Länge 1 Pixel haben.

In der Klasse RayTraceCamera befinden sich bereits die beiden public-Methoden „MoveTo“ und „ResizeScreen“, mit denen der Ort und die Blickrichtung der Kamera sowie die Größe des zu rendernden Bitmaps geändert werden kann. Beide Methoden rufen am Ende die Methode „Init“ auf, welche die Einheitsvektoren eV und eW neu berechnet. Da der Vektor eV immer horizontal liegen soll, wird er aus dem Kreuzprodukt der Kamera-Blickrichtung und der nach-oben-Richtung gebildet.

Weitere RayTraceCamera-Methoden

Wir werden nun die RayTraceCamera um noch ein paar Methoden erweitern. Aus dem Bild zu Beginn dieses Artikels können wir entnehmen, dass wir jedem Pixel (bzw. jeder VW-Koordinate) einen Ray zuordnen können. Der Startpunkt jedes Rays ist die CamPosition. Seine Richtung ist eine Linearkombination aus der CamDirection und den Vektoren eV und eW. Die Methode „VWtoRay“ gibt den zu einer VW-Koordinate gehörenden Ray zurück:

public Ray VWtoRay(double v,double w)
{
    return new Ray(camPosition, camDirection + eV * v + eW * w);
}

Als nächstes erstellen wir die Methode „AdjustColor“. Sie bekommt eine Farbe und einen Helligkeitswert (brightness) übergeben und liefert eine neue Farbe zurück. Wenn brightness == 0 ist, entspricht die zurückgegebene Farbe genau der übergebenen Farbe. Ist brightness > 0 wird die Farbe aufgehellt, ist brightness < 0 wird sie abgedunkelt. Der Wert für brightness muß immer zwischen -1 und 1 liegen.

static Color AdjustColor(Color c, double brightness)
{
    if (brightness &gt;= 0)
        return Color.FromArgb( // Die Farbe aufhellen
            c.R + (int)((255 - c.R) * brightness),
            c.G + (int)((255 - c.G) * brightness),
            c.B + (int)((255 - c.B) * brightness)
        );
    else // if brightness &lt; 0
        return Color.FromArgb( // Die Farbe abdunkeln
            (int)(c.R * (1 + brightness)),
            (int)(c.G * (1 + brightness)),
            (int)(c.B * (1 + brightness))
        );
}

Die Farbe eines Pixels berechnen

Kommen wir nun zu der kompliziertesten Methode der Kamera: Die Methode „ColorAt“ soll die Farbe eines Punktes mit den Koordinaten v und w berechnen.

        static readonly double minDistToRayStart = 1E-10;

        Color ColorAt(double v, double w)
        {
            Ray ray = VWtoRay(v, w);
            PointOnPrimitive p = scene.RayTrace(ray);
            if (p == null)
                return scene.Background; // Wenn der Ray auf kein Primitiv trifft, Hintergrundfarbe der Szene zurückgeben
            else
            { // Prüfen, ob der PointOnPrimitive im Schatten liegt
                Ray rayToSun = new Ray(p.ToPointInSpace() + scene.DirectionToSun * minDistToRayStart, scene.DirectionToSun);
                if (scene.RayTraceHits(rayToSun)) return AdjustColor(p.GetColor(), scene.MaxDarkening); // Wenn keine freie Sicht auf die Sonne =&gt; Schatten

                Vector3D n = p.GetNormal(); // Muß die Länge 1 haben!
                if (n == null) return p.GetColor(); // Wenn keine Normale berechnet werden kann, Originalfarbe zurückgeben
                // Die Normale muß eine Komponente in Richtung des Betrachters haben. Sie darf nicht von ihm weg zeigen.
                if (ray.Direction * n &gt; 0) n = n.opposite(); // Wenn das Skalaprodukt &gt; 0 ist, ist der Winkel kleiner als 90° und wir drehen den Vektor um.

                double brightness;
                double cosAngleToLight = scene.DirectionToSun * n;

                if (cosAngleToLight &gt; 0)
                    brightness = scene.MaxDarkening + (scene.MaxLightening - scene.MaxDarkening) * cosAngleToLight; // Es fällt Licht auf die Fläche
                else
                    brightness = scene.MaxDarkening; // Es fällt kein Licht auf die Fläche

                return AdjustColor(p.GetColor(), brightness);
            }
        }

Die Methode „ColorAt“ ermittelt zuerst mit „VWtoRay“ einen Ray, den sie mit „scene.RayTrace“ durch die Szene sendet. Wenn der Ray auf kein Primitiv trifft, kann man an dieser Stelle durch die Szene hindurch sehen, und die Hintergrundfarbe „Scene.Background“ wird zurück gegeben. Wenn der Ray auf ein Primitiv trifft, wird ein PointOnPrimitive ermittelt. Wir prüfen zunächst, ob der PointOnPrimitive im Schatten liegt, indem wir ausgehendend von seinen XYZ-Koordinaten einen neuen Ray „rayToSun“ Richtung Sonne senden. Trifft dieser Ray auf ein Primitiv, d.h. Scene.RayTraceHits(rayToSun) == true, so wird die Farbe des PointOnPrimitives maximal mit dem Wert Scene.MaxDarkening abgedunkelt und dieser Farbwert von „ColorAt“ zurückgeliefert. Es fällt auf, dass der Startpunkt von „rayToSun“ beim Erstellen des Rays ein beliebig kleines Stück der Länge minDistToRayStart in Richtung Sonne verschoben wird. Dies geschieht, damit beim Raytracen zur Sonne nicht wieder das Primitiv selbst gefunden wird und ein Primitiv sich nicht sozusagen selbst in den Schatten stellt.

Wenn der PointOnPrimitive nicht im Schatten liegt, müssen wir aus dem Winkel seiner Oberfläche und der Beleuchtungsrichtung bestimmen, wie hell dieser ist. Eine Fläche, die genau senkrecht zum Sonnenlicht steht, wird mit dem maximalen Wert „Scene.MaxLightening“ aufgehellt. Wir fragen den PointOnPrimitive zunächst mit „p.GetNormal()“ nach seinem Flächennormalenvektor „n“ (einem Vektor, der senkrecht auf der Fläche steht). Da eine Fläche zwei Seiten hat, gibt es zwei (gegenüberliegende) Richtungen, in die dieser Vektor zeigen kann. Eine dieser Richtungen hat eine Komponente in Richtung der Kamera. Die andere zeigt von der Kamera weg. Wir wollen nur die Vektorrichtung haben, welche in Richtung der Kamera zeigt. D.h. wenn der Winkel zwischen Kamerarichtung und dem Normalenvektor kleiner als 90° ist (ray.Direction * n > 0), drehen wir den Vektor mit „n.opposite()“ um.

Wir berechnen nun den Kosinus „cosAngleToLight“ aus dem Winkel zwischen dem Normalenvektor n und der Sonnenrichtung. Ist der Kosinus größer als 0 bzw. der Winkel kleiner als 90°, so fällt Licht auf die Fläche und wir berechnen aus dem Kosinus die brightness. Wenn die Fläche von der Sonne abgewendet ist (cosAngleToLight <0), fällt dagegen kein Licht darauf und wir wählen die maximale Abdunkelung „Scene.MaxDarkening“.

Das fertige Bitmap rendern

Abschließend fügen wir noch die Methode „RenderBitmap“ in die Klasse ein, welche uns eine fertige Bitmap-Grafik von der Kamera liefern soll:

        public Bitmap RenderBitmap()
        {
            int centerV = screenWidthPx / 2;
            int centerW = screenHeightPx / 2;
            var result = new Bitmap(screenWidthPx, screenHeightPx);

            Parallel.For(0, screenHeightPx, (w) =>
              {
                  for (int v = 0; v < screenWidthPx; v++)
                  {
                      Color c = ColorAt(v - centerV, centerW - w);
                      lock (result)
                      {
                          result.SetPixel(v, w, c); // ... den Pixel auf diese Farbe setzen.
                      }
                  }
              });
            return result;
        }

Wie zuvor erklärt, liegt der Nullpunkt der Abbildungsebene genau in ihrer Mitte. Daher berechnen wir zunächst seine Koordinaten „centerV“ und „centerW“, welche jeweils die Hälfte der Bildbreite bzw. -höhe betragen. Weiterhin erstellen wir ein neues Bitmap namens „result“, in das wir unsere Szene Rendern werden. Wir starten nun zwei For-Schleifen für die Koordinaten w und v, welche jeden Pixel berechnen werden. Die äußere For-Schleife wurde dabei mit „Parallel.For“ realisiert. Dies hat zur Folge, dass das .NET Framework alle Durchläufe der Schleife gleichzeitig in eigenen Threads ausführen kann. Es kann also theoretisch für jede Pixelzeile ein eigener Thread gestartet werden, was die Berechnung merklich beschleunigt.
Die Farbe des jeweiligen Pixels berechnet die Methode „ColorAt“, wobei wir die v- und w-Koordinate vor ihrem Aufruf um centerV und centerW verschieben müssen. Die Farbe weisen wir dann mit „SetPixel“ dem jeweiligen Pixel zu. Da niemals zwei Threads gleichzeitig „SetPixel“ aufrufen dürfen, wurde dieser Aufruf mit einem „lock“ abgesichert.

Damit ist die Kamera fertig!

Weiter

 

Raytracing Teil 1: Einleitung

Jeder kennt (z.B. aus Computerspielen) die künstlichen, teilweise sogar fotorealistischen, 3D-Computergrafiken welche durch Raytracing berechnet werden. In den folgenden Beiträgen werde ich die Mathematik, die der Berechnung von 3D-Bildern zugrunde liegt, erarbeiten und einen Raytracer selbst implementieren. Natürlich gibt es bereits unzählige fertige Raytracing-Engines, die z.B. durch Hardwarebeschleunigung viel effektiver arbeiten und über sehr ausgereifte Effekte verfügen. Praktisch alle Probleme rund um das Raytracing wurden also bereits gelöst und eigentlich scheint es wenig sinnvoll das „Rad erneut zu erfinden“. Das Ziel dieses Projekts ist es daher nicht in irgend einem Aspekt besser zu sein, als bereits vorhandene Raytracer, sondern schlichtweg ein sehr tiefes Verständnis für diese Technik zu erlangen. Bei der Entwicklung des Raytracers werde ich daher vollständig auf externe Bibliotheken verzichten und alle Funktionen selbst implementieren.

Das Grundprizip

Wenn wir ein 3D-Bild auf z.B. einem Computermonitor betrachten, so sieht es so aus, als ob sich die dreidimensionale Szene hinter dem Monitor befinden würde. Der Monitor selbst erscheint uns wie ein Fenster, durch das wir die Szene betrachten. In Wirklichkeit besteht das Bild auf dem Monitor natürlich aus einzelnen Pixeln, von denen jedes Pixel eine andere Farbe hat. Die einzelnen Pixelfarben möchten wir berechnen.

Wir möchten also das Bild einer dreidimensionalen Szene (hier: zwei Kugeln) auf einer Abbildungsebene (Monitor), welche sich zwischen dem Auge des Betrachters und der Szene befindet, berechnen. Dazu berechnen wir ausgehend von dem Auge einen Strahl durch jeden einzelnen Pixel im Pixelgitter der Abbildungsebene und verfolgen den Strahl so lange durch die Szene, bis er auf ein Objekt stößt. Die Farbe dieses Objekts ist dann die Farbe des jeweiligen Pixels.

Um einen solchen Strahl zu beschreiben definieren wir zunächst die Klasse Ray:

    public class Ray
    {
        public Vector3D Start { get; private set; }
        public Vector3D Direction { get; private set; }

        public Ray(Vector3D start, Vector3D direction)
        {
            this.Start = start; this.Direction = direction.normalize();
        }
    }
}

Ein Ray besteht also aus zwei Vektoren. Seiner Startposition (hier dem Auge des Betrachters) und seiner Richtung. Die Klasse Vector3D habe ich hier beschrieben. Der Richtungsvektor „Direction“ wird im Konstruktor eines Rays normalisiert. Er hat also immer die Länge 1.

Die Szene, Objekte und Primitive

Als Szene bezeichnen wir die Menge aller geometrischen Objekte, welche durch unseren Raytracer auf die Abbildungsebene abgebildet werden sollen. Ein Objekt (z.B. ein Haus, ein Baum, etc.) besteht im Allgemeinen aus vielen kleinen Primitiven. Ein Primitiv (z.B. ein Dreieck oder Viereck) ist ein elementarer Baustein einer Szene, der vom Raytracer direkt verarbeitet werden kann. Alle aufwändigeren Objekte (Häuser, etc.) müssen aus vielen kleinen Primitiven zusammengesetzt werden.

Definieren wir zunächst die abstrakte Klasse Primitive, von der später alle konkreten Primitive (Dreiecke, Vierecke, etc.) abgeleitet werden:

    abstract public class Primitive
    {
        public Color color { get; private set; }
        public abstract PointOnPrimitive RayTrace(Ray ray);

        public Primitive(Color color)
        {
            this.color = color;
        }

    }

Jedes Primitiv muss eine Farbe haben. Deshalb muss diese bereits beim Konstruktoraufruf angegeben werden. Weiterhin verfügt jedes Primitiv über eine Methode namens RayTrace, welche beim Aufruf einen Ray als Parameter erhält. In dieser Methode berechnet das Primitiv, ob der Ray seine Oberfläche schneidet. Ist dies nicht der Fall, wird null zurückgegeben. Gibt es einen Schnittpunkt, wird ein PointOnPrimitive zurückgegeben. Dieses Objekt beschreibt einen Schnittpunkt zwischen einem Ray und einem Primitiv.

Die abstrakte Klasse PointOnPrimitive sieht wie folgt aus:

    abstract public class PointOnPrimitive
    {
        public double distanceToRayStart { get; private set; }
        public abstract Vector3D ToPointInSpace();
        public abstract Color GetColor();
        public abstract Vector3D GetNormal();
        public abstract Primitive GetPrimitive();

        public PointOnPrimitive(double distanceToRayStart)
        {
            this.distanceToRayStart = distanceToRayStart;
        }
    }

Je nachdem, um was für ein Primitiv es sich handelt, kann es verschiedene Implementierungen dieser abstrakten Klasse geben, um die wir uns später kümmern. Schauen wir uns zunächst die Methoden dieser Klasse an:

Die Methode ToPointInSpace() berechnet die Position des PointOnPrimitive im Raum, d.h. seine X, Y und Z Koordinate, und gibt diesen als Vector3D zurück. Dieser Punkt hat einen Abstand von vom Startpunkt des Rays (bzw. des Beobachters), welchen wir mit distanceToRayStart abfragen können. Wenn wir alle Schnittpunkte eines Rays mit allen Primitiven in der Szene berechnen, so wird der Ray in der Regel mehrere Primitive schneiden. Für die Farbe des Pixels auf der Abbildungsebene ist nur der erste Schnittpunkt mit einem Primitiv von Bedeutung, da alle hinteren Primitive von dem vorderen Primitiv verdeckt sind. Finden wir also zu einem Ray mehrere PointOnPrimitives, so müssen wir nur den PointOnPrimitive mit dem geringsten distanceToRayStart verwenden. Mit GetColor() können wir die Farbe des Primitivs, auf dem der PointOnPrimitive liegt, abfragen. Mit GetPrimitive() erhalten wir das Primitiv selbst.

Die Helligkeit eines PointOnPrimitive hängt von dem Winkel zwischen dem Lichteinfall und der Oberfläche des Primitivs ab, denn eine Fläche, die quer zum einfallenden Licht steht, ist wesentlich heller, als eine Fläche, die vom Licht nicht angestrahlt wird. Um herauszufinden, wie die Fläche, auf dem der PointOnPrimitive liegt, geneigt ist, verwenden wir die Methode GetNormal(). Sie liefert uns einen Normalenvektor zu der Ebene, also einen Vektor, der genau senkrecht auf der Primitivoberfläche steht.

Zurück zur Szene:

Die Szene ist also unser Raum, in dem sich alle Primitive befinden. Zusammen bilden die Primitive z.B. eine Landschaft mit Häusern, Bäumen, etc. Aus Entwicklersicht ist eine Szene etwas, in dem wir Raytracen können. Wir definieren daher nun das Interface IRayTraceable. Jedes Objekt, welches dieses Interface implementiert, repräsentiert also einen Raum mit Primitiven, durch den wir Strahlen bzw. Rays senden können. Ein Ray trifft dabei entweder auf ein Primitiv oder er passiert den Raum ungehindert.

Das Interface IRayTraceable sieht wie folgt aus:

    public interface IRayTraceable
    {
        Vector3D DirectionToSun { get; set; }
        Color Background { get; set; }
        double MaxLightening { get; set; } // Gültige Werte von 0 (Originalfarbe) bis 1 (jede Farbe wird bis auf Weiss aufgehellt)
        double MaxDarkening { get; set; } // Gültige Werte von 0 (Originalfarbe) bis -1 (jede Farbe wird bis auf Schwarz abgedunkelt)
        PointOnPrimitive RayTrace(Ray ray);
        bool RayTraceHits(Ray ray); // Liefert true, wenn der Ray auf ein beliebiges Primitive trifft.
    }

Jede Szene braucht eine Lichtquelle, damit für unterschiedliche Ausrichtungen von Oberflächen unterschiedliche Farben bzw. Helligkeitswerte berechnet werden können. Würde eine Lichtquelle fehlen, dann würde eine Kugel aussehen wie ein flacher einfarbiger Kreis und der dreidimensionale Eindruck ginge verloren. Eine Kugel wirkt erst durch die unterschiedlichen Helligkeits-Schattierungen dreidimensional. Um die Szene nach ihrer Beleuchtungsrichtung zu fragen, gibt es in dem Interface den Vektor „DirectionToSun“. Je nachdem in welchem Winkel das Licht auf eine Oberfläche fällt, wird diese ausgehend von ihrer Originalfarbe aufgehellt oder abgedunkelt. Mit „MaxDarkening“ wird die maximale Abdunkelung einer Oberfläche festgelegt. Ist dieser Wert auf -1, so sind alle Flächen, auf die kein Sonnenlicht fällt absolut schwarz. Wird der Wert größer gewählt (z.B. -0,5) so gibt es ein diffuses Umgebungslicht in der Szene, welches die Abdunkelung begrenzt. Mit „MaxLightening“ wird festgelegt, wie intensiv die Sonneneinstrahlung ist. Wird der Wert auf 1 gesetzt, so ist jede Oberfläche die senkrecht zur Sonnenstrahlung steht, absolut weiß. Wird ein kleinerer Wert gewählt (z.B. 0.8), so wird die Farbe nicht ganz so weit aufgehellt. Einige Rays werden beim Raytracen die Szene passieren, ohne auf ein Primitiv zu treffen. Diese Pixel werden in der Farbe „Background“ des Interfaces dargestellt.

Die Methoden „RayTrace“ und „RayTraceHits“ dienen dazu Strahlen durch die Szene zu senden. Der Strahl bzw. Ray selbst wird als Parameter übergeben. Trifft dieser Ray auf ein Primitiv, so gibt die Methode „RayTrace“ einen PointOnPrimitive zurück, welcher den Schnittpunkt zwischen Ray und Primitiv beschreibt. Trifft der Ray auf kein Primitiv, so wird null zurückgegeben. Die Methode RayTraceHits arbeitet sehr ähnlich. Sie gibt jedoch nur den Wert true wenn der Ray auf ein beliebiges Primitiv trifft, und false wenn der Ray die Szene ungehindert passiert.

Mit diesen Interfaces bzw. abstrakten Klassen haben wir bereits alle wichtigen Schnittstellen in unserem Raytracer beschrieben. In den folgenden Beiträgen werden wir die einzelnen konkreten Klassen, welche diese Schnittstellen verwenden, implementieren.

Weiter

Vektorrechnung in C#

Wer Software entwickelt, die geometrische Berechnungen durchführt, kommt meist nicht drum herum sich erneut mit der aus der Schulzeit bereits bekannten Vektorrechnung zu beschäftigen. Die im Folgenden vorgestellte Klasse Vector3D macht uns das Leben etwas einfacher. Ein Objekt dieser Klasse repräsentiert einen dreidimensionalen Vektor und alle gängigen Vektoroperationen sind bereits implementiert.

Hier sind einige Anwendungsbeispiele:

            // Zwei neue Vektoren erstellen:
            Vector3D V = new Vector3D(1, 3, 4);
            Vector3D W = new Vector3D(2, 1, 5);

            // Die Vektoren addieren, subtrahieren und multiplizieren:
            Vector3D sum = V + W;
            Vector3D diff = V - W;
            double s = V * W; // Skalarprodukt berechnen
            Vector3D k = V.cross(W); // Kreuzprodukt berechnen
            Vector3D V_resize = V * 2.5; // Vektor mit Skalar multiplizieren

            // Die Länge eines Vektors berechnen:
            double V_length = V.length();

            // Einen Vektor normalisieren (d.h. seine Länge auf 1 ändern):
            Vector3D V_normalized = V.normalize();

            // Gegenvektor berechnen (d.h. mit -1 multiplizieren):
            Vector3D V_opposite = V.opposite();

            // Prüfen, ob zwei Vektoren parallel sind:
            if(V.isCollinearTo(W))
            {
                Console.WriteLine("V und W sind parallel!");
            }

In der Klasse Vector3D gibt es noch einige weitere Methoden, z.B. um einen Vektor zu drehen oder auf eine Ebene oder einen anderen Vektor zu projizieren. Diese Methoden werden durch Kommentare im Quelltext erklärt.

Die Klasse Vektor3D:

    public class Vector3D
    {
        public double X { get; private set; }
        public double Y { get; private set; }
        public double Z { get; private set; }

        // Einheitsvektoren:
        public static readonly Vector3D eX = new Vector3D(1, 0, 0);
        public static readonly Vector3D eY = new Vector3D(0, 1, 0);
        public static readonly Vector3D eZ = new Vector3D(0, 0, 1);
        public static readonly Vector3D Zero = new Vector3D(0, 0, 0);

        public Vector3D(double _x, double _y, double _z)
        {
            X = _x; Y = _y; Z = _z;
        }

        public static Vector3D operator +(Vector3D v, Vector3D w)
        {
            return new Vector3D(v.X + w.X, v.Y + w.Y, v.Z + w.Z);
        }

        public static Vector3D operator -(Vector3D v, Vector3D w)
        {
            return new Vector3D(v.X - w.X, v.Y - w.Y, v.Z - w.Z);
        }

        public static double operator *(Vector3D v, Vector3D w)
        {
            return v.X * w.X + v.Y * w.Y + v.Z * w.Z;
        }

        public static Vector3D operator *(Vector3D v, double w)
        {
            return new Vector3D(v.X * w, v.Y * w, v.Z * w);
        }

        public Vector3D cross(Vector3D w) // Kreuzprodukt (this x W)
        {
            return new Vector3D(Y * w.Z - Z * w.Y, Z * w.X - X * w.Z, X * w.Y - Y * w.X);
        }

        public double length()
        {
            return Math.Sqrt(X * X + Y * Y + Z * Z);
        }

        public Vector3D normalize()
        {
            double L = length();
            return new Vector3D(X / L, Y / L, Z / L);
        }

        public Vector3D opposite()
        {
            return new Vector3D(-X, -Y, -Z);
        }

        public void assertNormal()
        {
            double l = length();
            if (l < 0.9999 || l > 1.0001) throw new Exception("Vector3D is not normalized!");
        }

        public bool isCollinearTo(Vector3D w)
        {
            double sp = this * w;
            double l = this.length() * w.length();
            return Math.Abs(sp - l) < 0.0001 || Math.Abs(sp + l) < 0.0001;
        }

        public override string ToString()
        {
            return "(" + X.ToString("F2") + "; " + Y.ToString("F2") + "; " + Z.ToString("F2") + ")";
        }

        public static double matrixDet(Vector3D a, Vector3D b, Vector3D c)
        {
            return a.X * b.Y * c.Z + b.X * c.Y * a.Z + c.X * a.Y * b.Z
                 - a.Z * b.Y * c.X - b.Z * c.Y * a.X - c.Z * a.Y * b.X;
        }

        public static bool cramer(Vector3D A, Vector3D B, Vector3D C, Vector3D Sum, out double a, out double b, out double c)
        { // Löst das LGS a*A + b*B +c*C = Sum. Wenn es keine Lösung gibt wird false zurück gegeben.
            double det = Vector3D.matrixDet(A, B, C);
            if (det == 0) { a = 0; b = 0; c = 0; return false; }
            a = Vector3D.matrixDet(Sum, B, C) / det;
            if (Double.IsNaN(a)) { b = 0; c = 0; return false; }
            b = Vector3D.matrixDet(A, Sum, C) / det;
            if (Double.IsNaN(b)) { c = 0; return false; }
            c = Vector3D.matrixDet(A, B, Sum) / det;
            if (Double.IsNaN(c)) return false;
            return true;
        }

        public Vector3D projectOnPlane(Vector3D planeNormal)
        { // Projiziert den Vektor auf die Ebene senkrecht zu planeNormal, bzw. entfernt die Komponente in planeNormal-Richtung aus dem Vektor.
            planeNormal = planeNormal.normalize();
            return this - planeNormal * (planeNormal * this);
        }

        public Vector3D projectOnVector3D(Vector3D vec)
        {// Projeziert den Vektor auf vec, bzw. gibt nur seine Komponente in vec-Richtung zurück.
            vec = vec.normalize();
            return vec * (this * vec);
        }

        public Vector3D rotateAround(Vector3D startPoint, Vector3D axis, double angle)
        { // Rotiert den durch this beschriebenen Punkt um die Gerade mit Stützpunkt startPoint und Richtung axis. Bei Blickrichtung in axis-Richtung wird der Punkt bei positivem angle im Uhrzeigersinn rotiert.
            axis = axis.normalize();
            Vector3D V = (this - startPoint).projectOnPlane(axis); // Vektor von Gerade (Projektionspunkt) zu this, senkrecht zu axis
            Vector3D W = axis.cross(V).normalize() * V.length();  // W= V um 90° im Uhrzeigersinn weiter gedreht.
            double a = (this - V - startPoint).length(); // Abstand von startPoint zum Projektionspunkt von this auf die Gerade.
            return startPoint + (axis * a) + V * Math.Cos(angle) + W * Math.Sin(angle);
        }

        double angle(Vector3D P1, Vector3D P2)
        { // Berechnet den Winkel zwischen den Punkten P1 und P2 am Punkt this in Radiant
            Vector3D V1 = (P1 - this);
            Vector3D V2 = (P2 - this);
            return Math.Acos(V1 * V2 / (V1.length() * V2.length()));
        }
    }

SignalR-Hubs mit Nutzername und Passwort absichern

Sehr häufig sollen nicht alle Anwender eines Systems berechtigt sein alle, durch SignalR-Hubs bereitgestellten Funktionen zu nutzen. Im Folgenden wird gezeigt, wie man eine Benutzeranmeldung mit Username und Password für einen SignalR-Hub selbst implementieren kann.

Das AuthorizeAttribute

Die AuthorizeAttribute Klasse ist Bestandteil von SignalR und dient dazu Zugriffe auf einen Hub einzuschränken. Da sie von der Attribute-Klasse erbt, kann man ihre Objekte wie ein „Tag“ an andere Klassen (z.B. einen SignalR-Hub) „anheften“. SignalR verwendet dieses AuthorizeAttribute-Objekt dann um herauszufingen, ob ein Zugriff auf den Hub berechtigt ist, oder nicht.

Wir erstellen nun eine neue Klasse namens HubAuthorizationAttribute, welche von AuthorizeAttribute erbt, und implementieren darin unsere Funktionen zur Passwort-Überprüfung. Anschließend werden wir ein Objekt dieser Klasse als Attribut an einen SignalR-Hub anheften.

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

    public class HubAuthorizationAttribute : AuthorizeAttribute
    {
        public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
        {
            string user = request.Headers["X-User"];
            string passwd = request.Headers["X-PW"];
        
            // Hier prüfen, ob user und passwd gültig sind!
            // Wenn der Zugriff erlaubt ist, true zurückgeben. Andernfalls false.

            return true; // In diesem Fall: Zugriff erlaubt.
        }

        public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
        {
            return true;
        }
    }

Die Methode AuthorizeHubConnection wird immer aufgerufen, wenn ein Client sich mit einem Hub verbinden möchte. Gibt diese Methode ein true zurück, ist die Verbindung erlaubt. Gibt die Methode dagegen ein false zurück, wird die Verbindung abgewiesen. Im obigen Beispiel-Code gehen wir davon aus, dass der Client seinen Nutzernamen und Password in die Felder X-User und X-PW des Http-Headers der Anfrage geschrieben hat. Da wir beim Aufruf von AuthorizeConnection den Http-Request als Parameter übergeben bekommen, haben wir Zugriff auf diese Felder. Im Beispiel oben kopieren wir den Nutzernamen und das Password in die String-Variablen user und passwd. Wir können uns nun z.B. mit einer Benutzerdatenbank verbinden und prüfen, ob die Login-Daten gültig sind. Im Beispiel oben haben wir diesen Schritt einfach ausgelassen und geben immer ein true zurück, denn es soll hier lediglich das Prinzip erklärt werden.

Einen SignalR-Hub absichern

Wir können nun einen SignalR-Hub absichern, indem wir unser neues Attribut HubAuthorization an den Hub anheften, d.h. wir schreiben es in eckigen Klammern vor die Hub-Klasse (hier: ExampleHub):

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
 
namespace MyWebApplication
{
    [HubAuthorization]
    [HubName("ExampleHub")]
    public class ExampleHub : Hub
    { 
        public ExampleHub()
        {
        }
 
        public int Add(int a, int b)
        {
            return a+b;
        }
    }
}

Immer wenn nun eine Verbindung zu diesem Hub hergestellt werden soll, wird diese  von dem HubAuthorizationAttribute geprüft und ggf. abgelehnt.

User und Password durch einen C# Client übermitteln

Das folgende Beispiel zeigt, wie die Felder X-User und X-PW in einem C# Client in den Http-Header eingetragen werden können:

var hubConnection = new HubConnection("http://localhost:57256/");
var proxy = hubConnection.CreateHubProxy("ExampleHub");
hubConnection.Headers["X-User"] = "Beispielnutzer";
hubConnection.Headers["X-PW"] = "geheim";

hubConnection.Start().Wait();
Console.WriteLine(proxy.Invoke<int>("Add", new object[] { 2,3} ).Result); // Ergebnis: 5

Datenaustausch zwischen Client und Server in C# mit SignalR

Bei der Entwicklung von Anwendungen bestehend aus einem Server und einem oder mehreren Clients tritt folgender Ablauf sehr häufig auf:

  1. Ein Client fordert über die API des Servers einen Datensatz an.
  2. Der Server holt diesen Datensatz aus z.B. einer Datenbank.
  3. Der Server entfernt aus diesem Datensatz alle Werte, die für den Client nicht relevant sind oder (z.B. aus Datenschutzgründen) nicht an den Client übertragen werden sollen. Oder er kopiert nur die für den Client relevanten Daten aus dem Datensatz in eine neue Datenstruktur.
  4. Der Datensatz oder die neue Datenstruktur wird an den Client gesendet.
  5. Der Client bzw. sein Anwender ändert einige Werte des Datensatzes und sendet ihn zurück an den Server.
  6. Der Server muss nun den Datensatz aus der Datenbank und die vom Client gesendeten Daten zusammenführen und wieder in der Datenbank speichern. Dabei sind alle Werte, welche vom Client nicht zurück gesendet wurden (also den Wert null haben) nicht zwangsläufig vom Client geändert worden, sondern wurden möglicherweise bereits vorher vom Server entfernt. Diese dürfen natürlich nun nicht auch in der Datenbank gelöscht werden.

Dem Client soll also nur eine Untermenge der tatsächlich in einem Datensatz befindlichen Daten bekannt sein. Das hat zudem den Vorteil, dass eine Erweiterung des Datensatzes in neueren Versionen des Servers keine Auswirkungen auf den Client hat(sofern dieser nicht auch mit den neuen Daten arbeiten soll).

Im Folgenden wird ein generischer Ansatz vorgestellt werden, der eine nachträgliche  Änderung der Datenstrukturen mit minimalen Aufwand ermöglicht.

Unsere Beispielanwendung

Gehen wir davon aus, wir möchten die Mitarbeiter einer Firma in einer Datenbank verwalten. Die Webapplikation speichert von jedem Mitarbeiter eine Id, den Vor- und Nachnamen, seine Abteilung, sein Geburtsdatum und sein Gehalt. Es soll eine Client-Anwendung geben, welche sich via SignalR mit der Webapplikation auf dem Server verbindet und die Mitarbeiter-Datensätze herunterladen kann. Der Anwender des Clients kann einen Mitarbeiter nun bearbeiten, z.B. seine Abteilung ändern. Anschließend werden diese Änderungen wieder an die Webapplikation übertragen und in der Datenbank gespeichert. Aus Gründen des Datenschutzes, dürfen die Daten für Geburtstag und Gehalt jedoch nicht mit an den Client übertragen werden. Der Client kennt also nur einen Teil der über einen Mitarbeiter gespeicherten Daten. Vor dem Übertragen eines Mitarbeiter-Datensatzes an den Client müssen also die Vertraulichen Daten entfernt werden. Nach dem Zurücksenden eines Datensatzes vom Client zum Server muss dieser mit den vertraulichen Daten wieder zusammengeführt werden.

Webapplikation, Client und eine gemeinsame DLL

Um das Problem elegant zu lösen, erstellen wir zunächst drei Projekte: Die Webapplikation, eine Client-Anwendung (z.B. für den Desktop) und eine DLL, welche von den beiden anderen Projekten referenziert wird.

In der Webapplikation erstellen wir eine Klasse namens Employee, welche alle Daten über einen Mitarbeiter speichert und z.B. mit dem Entity Framework auf eine Datenbanktabelle abgebildet wird. In der DLL erstellen wir eine Klasse namens EmployeeSubset. Diese Klasse enthält alle Daten eines Mitarbeiters, welche auch dem Client bekannt sein sollen. Für die Kommunikation zwischen Server und Client verwenden wir nun ausschließlich die Klasse EmployeeSubset.

Der Server muss also alle Employee-Objekte aus der Datenbank in EmployeeSubset-Objekte umwandeln können und EmployeeSubset-Objekte vom Client mit den dazu gehörenden Employee-Objekten aus der Datenbank zusammenführen können.

Auf den ersten Blick liegt es Nahe, zwischen den Klassen Employee und EmployeeSubset eine Vererbung einzurichten. In diesem Fall könnten Employee-Objekte einfach als EmployeeSubset gecastet werden. Aus folgenden Gründen führt dies jedoch nicht zum Ziel:

  • Nur weil ein Objekt auf einen anderen Typ gecastet wird, ändert sich dadurch nicht der Typ des Objekts selbst. SignalR wird das Objekt weiterhin als Employee-Objekt erkennen und den kompletten Datensatz zum Client übertragen.
  • In einer Anwendung mit mehreren unterschiedlichen Clients, die alle eine andere Subset-Klasse benötigen, müsste die Employee-Klasse von allen diesen Klassen erben. In C# kann jedoch immer nur eine Klasse an eine andere Klasse vererben.
  • Zum Zusammenführen von Employee-Objeken und EmployeeSubset-Objekten würde uns eine Vererbung keine Vorteile bringen.

Objekte durch Reflection umwandeln und zusammenführen

Im Folgendem sehen wir einen Ansatz, wie man die Umwandlung zwischen den Klassen Employee und EmployeeSubset mittels Reflection durchführen können. Die Methode CreateSubset (welche eine Extension Method ist und daher in einer static Klasse stehen muß) sucht nach allen schreibbaren Properties in der Subset-Klasse T und kopiert den Inhalt einer gleichnamigen Property aus dem als source-Parameter übergebenen Objekt in das Subset-Objekt.
Die Methode ImportSubset sucht alle lesbaren Properties in dem Subset-Parameter. Wenn eine Property mit dem gleichen Namen in dem target

        public static T CreateSubset<T>(this object source) where T : new()
        {
            T subset = new T();
            Type srcType = source.GetType();
            foreach (PropertyInfo p in typeof(T).GetProperties().Where(p => p.CanWrite))
            {
                PropertyInfo sp = srcType.GetProperty(p.Name);
                if (sp == null) throw new Exception("The property '" + p.Name + "' of type '" + typeof(T).FullName + "' does not exist in type '" + srcType.FullName + "'!");
                p.SetValue(subset, sp.GetValue(source));
            }
            return subset;
        }

        public static void ImportSubset(this object target, object subset)
        {
            Type tgtType = target.GetType();
            foreach (PropertyInfo p in subset.GetType().GetProperties().Where(p => p.CanRead))
            {
                PropertyInfo tp = tgtType.GetProperty(p.Name);
                if (tp == null) throw new Exception("The property '" + p.Name + "' of type '" + subset.GetType().FullName + "' does not exist in type '" + tgtType.FullName + "'!");
                tp.SetValue(target, p.GetValue(subset));
            }
        }

Wir können diese Methode also wie folgt verwenden:

            Employee employee = new Employee() { // Beispiel-Employee erstellen
                Id = 1,
                Forename = "Max",
                Surname = "Muster",
                Department = "Entwicklung",
                Birthday = new DateTime(1990, 6, 5),
                Salary = 65000
            };

            // Erstellen eines Subsets durch den Server
            EmployeeSubset subset = employee.CreateSubset<EmployeeSubset>();  

            // Hier würde das Subset z.B. durch SignalR an den Client übertragen werden.

            // Der Client verändert das Subset:
            subset.Department = "Vertrieb";

            // Hier würde das geänderte Subset zurück an den Server gesendet.

            // Der Server führt den ursprünglichen Datensatz mit dem Subset zusammen
            employee.ImportSubset(subset);

            // employee.Department hat nun ebenfalls den Wert "Vertrieb"

Die Vorteile dieses Vorgehens:

  • Das Erstellen und Zusammenführen des Subsets erfordert nur noch je eine Zeile Programmcode.
  • Die Datensätze Employee und EmployeeSubset lassen sich mit sehr wenig Aufwand erweitern: Es reicht dazu aus, einfach neue Properties in die Klassen einzufügen. Wenn in beiden Klassen neue Properties mit gleichem Namen und Typ eingefügt werden, werden diese automatisch bei der Subset-Erstellung und dem Zusammenführen berücksichtigt.