Malen mit Zahlen, Teil RS2 – Drahtgittermodelle

Im letzten Teil haben wir nur Punkte gemalt. In dieser Folge zeichnen wir endlich Linien im Raum. Das ergibt Bilder wie in Abb. 1, in dem die Kanten von drei Würfeln und Breiten-/Längenkreise auf einer Kugel zu sehen sind. Wenn wir Würfel aus kleinen Drahtstücken zusammenlöten, sehen sie so ähnlich aus wie in Abb. 1. Man spricht daher von Drahtgitter- bzw. Wireframe-Modellen.

Abb. 1: Ein paar Drahtgitter-Modelle im Raum.

Punkte haben wir schon projiziert, aber wie projizieren wir Linien im Raum auf den Schirm?

Zentralprojektion eines Geradenstücks

In Abb. 2 haben wir eine Linie AB im Raum vor dem Schirm unserer Standardkamera (s. Teil RS1). Die Endpunkte A und B bilden mit dem Augpunkt O das blaue Dreieck. Dieses Dreieck und unsere orangene Schirmebene schneiden einander in der Linie A_CB_C. Wenn A, B und O nicht auf einer Geraden liegen (nicht kollinear sind), wird die Linie AB immer auf eine Linie projiziert (ansonsten auf einen Punkt).

Abb. 2: Die Projektion der Linie AB ist die Linie am Schirm, die die projizierten Punkte A_C und B_C verbindet.

Die Punkte A_C und B_C sind nach unserer Konstruktion die Projektionen von A und B. Nehmen wir einen beliebigen Punkt P auf der Linie AB, ist seine Projektion der Punkt P_C. Weil der Projektionsstrahl von P in der Ebene des blauen Dreiecks liegt, muss P_C ebenfalls in dieser Ebene liegen. Zusätzlich muss er aber auch auf unserer Schirmebene liegen. Daher muss er auf der Schnittlinie A_CB_C liegen.

Alle Punkte der Linie AB werden daher auf die Linie A_CB_C projiziert. Deshalb reicht es, die Endpunkte einer Linie AB auf den Schirm zu projizieren, und dann mit irgendeiner (schnellen) Methode eine Linie durch die projizierten Punkte A_C und B_C am Schirm zu zeichnen.

Die Zentralprojektion von Punkten der Linie AB zu Punkten der Linie A_CB_C ist allerdings nichtlinear! Der Punkt P in Abb. 2 wurde genau in der Mitte von A und B gewählt. Seine Projektion P_C liegt aber wegen der perspektivischen Verzerrung nicht mehr in der Mitte von A_C und B_C.

Wie schon in Teil RS1 erwähnt, ist die Abbildung von P auf P_C nicht umkehrbar. Jeder Punkt auf der Geraden durch A und O wird auf A_C projiziert, ebenso wie jeder Punkt auf der Geraden durch B und O auf B_C abgebildet wird. Es gibt also unendlich viele Linien, deren Projektion die Linie A_CB_C ist.

Drahtgittermodelle

Im Programm gibt es jetzt die neue Klasse WireFrame. Ihre Aufgabe besteht darin, alle Informationen zu einem Wireframe-Modell zu speichern. Speziell sind das die Endpunkte der Linien, die sog. Vertices (Vertex in der Einzahl) und natürlich die Linien selber.

class WireFrame {
    // Listen der zu zeichnenden Linien und ihrer
    // Endpunkte (Vertices)
    final ArrayList<Line&gt; lines;
    final ArrayList<Point3D&gt; vertices;

    // der Konstruktor legt leere Listen an
    WireFrame() {
        lines = new ArrayList<Line&gt;();
        vertices = new ArrayList<Point3D&gt;();
    }

    // fügt dem Modell einen Endpunkt (Vertex) hinzu
    void addVertex(final Point3D p) {
        vertices.add(p);
    }

    // fügt dem Modell eine Linie hinzu
    // es werden nur die Nummern der Punkte (Vertices)
    // gespeichert
    void addLine(final int v0, final int v1) {
        lines.add(new Line(v0, v1));
    }
}

Weil ein Vertex der Endpunkt von mehr als einer Linie sein kann, werden nicht die Punkte selber in der Linie gespeichert, sondern nur die entsprechenden Nummern in der Vertices-Liste. (Java fängt bei 0 zu zählen an!) Entsprechend einfach ist die Line-Klasse (die Point3D-Klasse wurde nicht verändert).

class Line {
    // die Nummern der Linienendpunkte (Vertices)
    // in der Punktliste eines Wireframe-Modells
    final int v0;
    final int v1;

    // der Konstruktor speichert die Nummern einfach ab
    Line(final int v0, final int v1) {
        this.v0 = v0;
        this.v1 = v1;
    }
}

Wie die Sache funktioniert sieht man an zwei Beispielen, die ich in der Datei WireFrameExamples.pde gespeichert habe. Um einen Würfel zu zeichnen gibt es die von WireFrame abgeleitete Klasse Cube. Diese legt einen Würfel mit vorgegebenem Mittelpunkt und Seitenlänge an.

class Cube extends WireFrame {
    // der Konstruktor legt einen Würfel mit Mittelpunkt center
    // und Seitenlänge len an; die Würfelkanten sind parallel zu den 
    // Koordinatenachsen
    Cube(final Point3D center, final float len) {
        super();

        // Punkte hinzufügen
        addVertex(new Point3D(center.x - len/2, center.y - len/2, center.z + len/2)); // 0
        addVertex(new Point3D(center.x + len/2, center.y - len/2, center.z + len/2)); // 1
        addVertex(new Point3D(center.x + len/2, center.y + len/2, center.z + len/2)); // 2
        addVertex(new Point3D(center.x - len/2, center.y + len/2, center.z + len/2)); // 3
        addVertex(new Point3D(center.x - len/2, center.y - len/2, center.z - len/2)); // 4
        addVertex(new Point3D(center.x + len/2, center.y - len/2, center.z - len/2)); // 5
        addVertex(new Point3D(center.x + len/2, center.y + len/2, center.z - len/2)); // 6
        addVertex(new Point3D(center.x - len/2, center.y + len/2, center.z - len/2)); // 7

        // Linien hinzufügen
        // Vorderseite
        addLine(0, 1);
        addLine(1, 2);
        addLine(2, 3);
        addLine(3, 0);
        // Rückseite
        addLine(4, 5);
        addLine(5, 6);
        addLine(6, 7);
        addLine(7, 4);
        // Linien der Seitenflächen
        addLine(0, 4);
        addLine(1, 5);
        addLine(2, 6);
        addLine(3, 7);
    }
}

Die Sphere-Klasse erzeugt eine Kugel mit vorgegebenem Mittelpunkt und Radius. Die Nummerierung der Punkte ist hier ein bisschen komplizierter, aber es wird von Süden kommend immer ein ganzer Breitenkreis aus N Punkten konstruiert. Der Südpol hat die Nummer 0, der südlichste Breitenkreis geht von Nummer 1 bis N, der darüber von N + 1 bis 2N, etc.

class Sphere extends WireFrame {
    private final int N = 16;

    // der Konstruktor legt eine Kugel mit Mittelpunkt center
    // und Radius radius an
    Sphere(final Point3D center, final float radius) {
        super();

        // Punkte hinzufügen
        // Südpol
        addVertex(new Point3D(center.x, center.y - radius, center.z));
        // alle Punkte zwischen Süd- und Nordpol
        for (int i = 1; i < N/2; ++i) {
        // "geographische" Breite; wir beginnen beim Südpol
            float theta = PI - i*(PI/(N/2)); 
            for (int j = 0; j < N; ++j) {
                // "geographische" Länge
                float phi = j*(TAU/N);
                addVertex(new Point3D(center.x + radius*sin(theta)*sin(phi),
                                      center.y + radius*cos(theta),
                                      center.z + radius*sin(theta)*cos(phi)));
            }
        }
        // Nordpol
        addVertex(new Point3D(center.x, center.y + radius, center.z));

        // Linien hinzufügen
        int jp1; // j + 1
        // Dreiecke um den Südpol
        for (int j = 1; j <= N; ++j) {
            if (j < N) {
                jp1 = j + 1;
            }
            else {
                jp1 = 1;
            }
            addLine(0, j);
            addLine(j, jp1);
        }
        // Quadrate um Breiten-/Längenkreise zu zeichnen
        for (int i = 0; i < N/2 - 2; ++i) {
            for (int j = 1; j <= N; ++j) {
                if (j < N) {
                    jp1 = j + 1;
                }
                else {
                    jp1 = 1;
                }
                addLine(j + i*N, j + (i + 1)*N);
                addLine(j + (i + 1)*N, jp1 + (i + 1)*N);
            }
        }
        // Dreiecke um den Nordpol
        for (int j = 1; j <= N; ++j) {
            addLine(vertices.size() - N - 2 + j, vertices.size() - 1);
        }
    }
}

Wenn wir z.B. mit

Cube c = new Cube(new Point3D(0.0, -0.75, -1.5), 0.5);
Sphere s = new Sphere(new Point3D(0.0, 0.25, -1.5), 0.5);

einen Würfel und eine Kugel anlegen, werden die Punkte und Linien innerhalb der beiden WireFrames jeweils von 0 weg gezählt. Das müssen wir dann im Rasterizer berücksichtigen.

Die Scene-Klasse speichert jetzt statt einzelner Punkte einzelne WireFrames.

class Scene {
    // die Liste aller Wireframe-Modelle, die zu malen sind
    final ArrayList<WireFrame&gt; wireFrames;

    // der Konstruktor legt eine leere Liste an
    Scene() {
        wireFrames = new ArrayList<WireFrame&gt;();
    }

    // wir können der Szene jederzeit ein Wireframe-Modell
    // hinzufügen
    void addWireFrame(final WireFrame wf) {
        wireFrames.add(wf);
    }
}

Der Rasterizer hat eine private Liste der zu zeichnenden Linien und ihrer Vertices. In einem ersten Schritt müssen daher die Punkte und Linien aller Drahtgittermodelle der Szene in den Rasterizer kopiert werden. Das passiert in der Funktion stageLines(), die außerdem die Nummern der Punkte in den Linien anpassen muss, weil die Nummerierung jedes WireFrames wieder bei 0 beginnt.

class Rasterizer {
    // private Referenz auf die Szene
    private final Scene scene;

    // private Listen der zu zeichnenden Linien und ihrer
    // Endpunkte, der Vertices
    private final ArrayList<Line&gt; lines;
    private final ArrayList<Point3D&gt; vertices;

    // der Konstruktor speichert eine private Referenz auf die Szene
    // und legt die Linien- und Endpunktlisten an
    public Rasterizer(Scene scene) {
        this.scene = scene;

        lines = new ArrayList<Line&gt;();
        vertices = new ArrayList<Point3D&gt;();
    }

    // füge die Linien aller Wireframe-Modelle in der Szene
    // dem Rasterizer hinzu
    public void stageLines() {

        // wir bearbeiten alle Wireframe-Modelle in der Szene
        for (WireFrame wf : scene.wireFrames) {

            // in den Wireframe-Modellen starten die Punktnummern
            // immer bei 0; hier werden sie jedoch hintereinander
            // eingefügt; wir müssen die bisherige Anzahl an
            // Punkten also speichern
            final int numOfVertices = vertices.size();

            // jetzt werden die neuen Punkte eingefügt
            for (Point3D v : wf.vertices) {
                vertices.add(v);
            }

            // hier werden die neuen Linien eingefügt und die
            // Punktnummern entsprechend angepasst
            for (Line l : wf.lines) {
                lines.add(new Line(l.v0 + numOfVertices, l.v1 + numOfVertices));
            }
        }
    }

    // male die Szene
    public void paintScene() {

        // male alle zu rasternden Linien
        for (Line l : lines) {

            // lade die Endpunkte der aktuellen Linie
            final Point3D v0 = vertices.get(l.v0);
            final Point3D v1 = vertices.get(l.v1);

            // und berechne ihre screen coordinates
            final int x0 = (int)((1.0 - v0.x/v0.z)*width/2.0);
            final int y0 = (int)((1.0 + v0.y/v0.z)*height/2.0);
            final int x1 = (int)((1.0 - v1.x/v1.z)*width/2.0);
            final int y1 = (int)((1.0 + v1.y/v1.z)*height/2.0);

            // der line-Befehl von Processing kümmert sich
            // um das Clipping links/rechts und oben/unten
            line(x0, y0, x1, y1);
        }
    }
}

Im zweiten Schritt wird die Szene dann mit der paintScene()-Funktion gemalt. In einer Schleife über alle im Rasterizer gespeicherten Linien holt sie zunächst die tatsächlichen Endpunkte aus der Vertices-Liste, projiziert sie auf den Schirm und überlässt das tatsächliche Malen der line()-Funktion von Processing.

Das Hauptprogramm legt in der setup()-Funktion alle Wireframe-Modelle an und fügt sie der Szene hinzu. Ein Modell, das der Szene nicht hinzugefügt wurde, wird auch nicht gezeichnet. Die draw()-Funktion malt die Szene dann und speichert sie als Bild ab.

Scene theScene;
Rasterizer theRasterizer;

Cube c1;
Cube c2;
Cube c3;
Sphere s1;

void setup() {
    // öffne ein Zeichenfenster mit 640x640 Pixeln
    size(640, 640);

    theScene = new Scene();

    // alle Modelle werden angelegt und der Szene hinzugefügt

    c1 = new Cube(new Point3D(0.0, -0.75, -1.5), 0.5);
    theScene.addWireFrame(c1);

    c2 = new Cube(new Point3D(-0.75, 0.75, -1.5), 0.25);
    theScene.addWireFrame(c2);

    c3 = new Cube(new Point3D(0.75, 0.75, -1.5), 0.25);
    theScene.addWireFrame(c3);

    s1 = new Sphere(new Point3D(0.0, 0.25, -1.5), 0.5);
    theScene.addWireFrame(s1);

    theRasterizer = new Rasterizer(theScene);
}

void draw() {
    // wir füllen das Fenster schwarz
    background(0);

    // und setzen die Malfarbe auf grün
    stroke(0, 255, 0);

    // der Rasterizer erhält zuerst die zu malenden
    // Linien der Szene und malt sie dann
    theRasterizer.stageLines();
    theRasterizer.paintScene();

    // speichert das Bild ab
    save("RS02_WF.png");

    // es reicht, das Bild einmal zu malen
    noLoop();
}

Der gesamte Code findet sich hier, und das Ergebnis zeigt Abb. 1 oben.

Clipping

Im letzten Teil haben wir Punkte, die hinter dem Schirm lagen gar nicht erst gezeichnet. Das müssten wir bei Linien, die hinter dem Schirm liegen ebenfalls machen. Aber was soll mit Linien passieren, von denen ein Endpunkt vor und der andere hinter dem Schirm liegt?

So eine Situation zeigt Abb. 3, wobei der Endpunkt A vor und der Endpunkt B hinter dem Schirm liegt. In diesem Fall sollte die Linie nur bis zum Schnittpunkt C_C mit der Schirmebene gezeichnet werden. Die Projektion B_C von B liegt zwar in der Schirmebene, aber »irgendwo«. Die Linie muss also entsprechend abgeschnitten (clipping) werden.

Abb. 3: Wenn ein Endpunkt der Linie hinter dem Schirm liegt (hier B), muss die Linie am Schnittpunkt C_C mit der Schirmebene abgeschnitten werden.

Wie finden wir diesen Schnittpunkt C_C? Eine Gerade im Raum kann durch die Parameterdarstellung

C=A+t\cdot(B-A)

angegeben werden. Dabei ist C ein beliebiger Punkt auf der Geraden durch die Punkte A und B, und t ist ein reeller Parameter, der im Prinzip von -\infty bis +\infty laufen kann. Für t = 0 ist C = A und für t = 1 ist C = B.

Die obige Gleichung sind eigentlich drei Gleichungen in einer, nämlich in den x-, y– und z-Koordinaten

\displaystyle\begin{pmatrix}C_x\\C_y\\C_z\end{pmatrix}=\begin{pmatrix}A_x+t\cdot(B_x-A_x)\\A_y+t\cdot(B_y-A_y)\\A_z+t\cdot(B_z-A_z)\end{pmatrix}\,.

Für die Schirmebene unserer Standardkamera gilt z = -1, also muss das auch für die z-Koordinate unseres Schnittpunkts C_C gelten

C_{C,z}=-1=A_z+t\cdot(B_z-A_z) .

Daraus können wir

\displaystyle t=-\frac{1 + A_z}{B_z - A_z}

ausrechnen. Wenn wir das in die x– bzw. y-Koordinate einsetzen, erhalten wir den kompletten Schnittpunkt C_C.

Änderungen fürs Clipping

Um das Clipping ein-/ausschalten zu können, enthält der Rasterizer jetzt das Attribut

// Clipping ein- oder ausgeschaltet?
boolean doClipping;

das im Konstruktor standardmäßig auf true, also eingeschaltet, gesetzt wird.

Die stageLines()-Methode muss natürlich angepasst werden. Zunächst wird die maximale z-Koordinate des Wireframes ermittelt. Wenn das Clipping ausgeschaltet ist oder die maximale z-Koordinate vor dem Schirm liegt, können wir alle Linien wie sie sind kopieren (und die Vertex-Nummern anpassen).

// füge die Linien aller Wireframe-Modelle in der Szene
// dem Rasterizer hinzu
public void stageLines() {

    // wir bearbeiten alle Wireframe-Modelle in der Szene
    for (WireFrame wf : scene.wireFrames) {

        // in den Wireframe-Modellen starten die Punktnummern
        // immer bei 0; hier werden sie jedoch hintereinander
        // eingefügt; wir müssen die bisherige Anzahl an
        // Punkten also speichern
        final int numOfVertices = vertices.size();

        // die maximale z-Koordinate eines WireFrames
        float maxZ = Float.NEGATIVE_INFINITY;

        // jetzt werden die neuen Punkte eingefügt
        for (Point3D v : wf.vertices) {
            vertices.add(v);

            // falls die z-Koordinate größer als die bisher maximale
            // ist, speichern wir sie
            maxZ = max(maxZ, v.z);
        }

        // wenn das Clipping ausgeschaltet ist oder die maximale
        // z-Koordinate vor der Kamera liegt, können wie
        // alle Linien einfach so einfügen
        if (!doClipping || (maxZ <= -1.0)) {

            // hier werden die neuen Linien eingefügt und die
            // Punktnummern entsprechend angepasst
            for (Line l : wf.lines) {
                lines.add(new Line(l.v0 + numOfVertices, l.v1 + numOfVertices));
            }
        }
        // wenn das Clipping eingeschaltet ist und eine z-Koordinate
        // hinter dem Schirm liegt, müssen wir die Linie ev. abschneiden
        else {
            for (Line l : wf.lines) {
                clipLine(new Line(l.v0 + numOfVertices, l.v1 + numOfVertices));
            }
        }
    }
}

Die clipLine()-Funktion schneidet die Linie entsprechend ab, wenn es sein muss. Allfällige Schnittpunkte werden am Ende der Vertices-Liste eingefügt. Weil Java bei 0 zu zählen beginnt, ist die Nummer des letzten Elements vertices.size() - 1.

// überprüft, ob eine Linie abgeschnitten werden muss, weil sie nicht
// komplett vor dem Schirm liegt; falls ja, wird die Linie entsprechend
// angepasst
private void clipLine(Line l) {

    // lade die Endpunkte der Linie l
    final Point3D v0 = vertices.get(l.v0);
    final Point3D v1 = vertices.get(l.v1);

    // v0 ist vor dem Schirm
    if (v0.z <= -1.0) {

        // v0 und v1 sind beide vor dem Schirm, wir können die Linie
        // wie sie ist hinzufügen
        if (v1.z <= -1.0) {
            lines.add(l);
        }
        // v0 ist vor, v1 ist hinter dem Schirm
        else {

            // der Schnittpunkt mit der Schirmebene wird als
            // letzter Vertex hinzugefügt
            vertices.add(getIntersectionPointWithScreen(v0, v1));

            // statt v1 muss der neue Punkt als Endpunkt verwendet werden
            lines.add(new Line(l.v0, vertices.size() - 1));
        }
    }
    // v0 ist hinter dem Schirm
    else {

        // v0 ist hinter, v1 ist vor dem Schirm
        if (v1.z <= -1.0) {

            // der Schnittpunkt mit der Schirmebene wird als
            // letzter Vertex hinzugefügt
            vertices.add(getIntersectionPointWithScreen(v0, v1));

            // statt v0 muss der neue Punkt als Endpunkt verwendet werden
            lines.add(new Line(vertices.size() - 1, l.v1));
        }
        // v0 und v1 sind beide hinter dem Schirm, wir
        // ignorieren die Linie daher
        else {
        }
    }
}

Die Berechnung des Schnittpunkt erledigt die getIntersectionPointWithScreen()-Funktion, die obige Formeln einfach eins zu eins übernimmt.

// A und B sind zwei Punkte, von denen einer vor, einer hinter dem Schirm
// ist; die z-Koordinate des Schnittpunkts mit der Schirmebene muss -1
// sein:
// A_z + t*(B_z - A_z) == -1
// wenn wir daraus t ausrechnen, können wir den Schnittpunkt der Linie AB
// mit der Schirmebene zurückgeben
private Point3D getIntersectionPointWithScreen(Point3D A, Point3D B) {
    final float t = -(1.0 + A.z) / (B.z - A.z);

    return new Point3D(A.x + t*(B.x - A.x), A.y + t*(B.y - A.y), -1.0);
}

Weil clipLine() und getIntersectionPointWithScreen() interne Funktionen des Rasterizers sind, wurden sie als private markiert. Die paintScene()-Funktion können wir lassen wie sie ist.

Der gesamte Code findet sich hier. Im Hauptprogramm habe ich den Würfel c2 (links oben) und die Kugel s auf der z-Achse etwas nach vorne geschoben. Das Ergebnis ohne Clipping zeigt Abb. 4. Weil die Kugel näher an den Augpunkt gerückt ist, erscheint sie viel größer und verzerrt, obwohl ihr Radius nicht geändert wurde.

Abb. 4: Wie in Abb. 1, aber die Kugel und der Würfel links oben sind etwas nach vorne geschoben. Dadurch erscheinen beide etwas größer und verzerrter. Das Clipping ist ausgeschaltet.

Mit eingeschaltetem Clipping ergibt sich Abb. 5. Die Vorderseite des Würfels c2 fehlt, weil sie hinter dem Schirm ist. Die Seitenlinien sind entsprechend abgeschnitten. Von der Kugel s wurde überhaupt der Großteil entfernt, und nur eine keine Kalotte ist noch sichtbar.

Abb. 5: Wie Abb. 4, aber das Clipping wurde jetzt eingeschaltet. Die Vorderseite des Würfels links oben und ein Großteil der Kugel sind hinter dem Schirm und werden daher abgeschnitten.

Diskussion

Eigentlich wollte ich hier noch ein bisschen was zum Clipping sagen. Ist dann aber etwas ausgeufert und wird ein für Rasterung/Schattierung und Raytracing gemeinsamer Teil.

Parallelisierung

In obigen Programmen werden zuerst alle Punkte/Linien dem Rasterizer übergeben (und dabei ev. geclippt) und dann werden sie in einem zweiten Schritt gezeichnet. Das ist natürlich sehr ineffizient. Eine vorbereitete Linie (bzw. später ein Dreieck) könnte sofort gezeichnet werden während die nächste Linie vorbereitet wird. Moderne Graphikkarten haben außerdem einige tausend Shader-Einheiten, die parallel zueinander arbeiten können. Diese massive Parallelisierung hat in den letzten 25 Jahren (beginnend in den 1990ern) die Graphikqualität von Spielen stark verbessert.

Gerade Linien bleiben gerade?

Wir haben zu Beginn gezeigt, dass die Projektion einer geraden Linie wieder eine gerade Linie ergibt. Das gilt allerdings nur dann, wenn sich die Lichtstrahlen vom Objekt zum Schirm geradlinig ausbreiten. Möchte man relativistische Vorgänge simulieren, ist das nicht mehr der Fall. Es gilt auch nicht mehr für größere Linsensysteme, die am Rand meistens eine Verzeichnung aufweisen. Um das mit unserer Methode darstellen zu können, müssten wir die Linien/Dreiecke in kleinere Stücke zerlegen, wo wir für jeden kleinen Teil unsere Näherung verwenden können. Neuere Graphikkarten haben spezielle Geometry- und Tesselation-Shader, die für solche Nacharbeiten verwendet werden können.

Kollisionserkennung

Das Clipping wird notwendig, wenn wir Objekte hinter der Kamera haben oder mit der Kamera in ein Objekt »hineingefahren« sind. Letzteres sollte eigentlich durch eine funktionierende Kollisionserkennung im Programm verhindert werden. Allerdings muss der Rasterizer auch bei nicht funktionierender Kollisionserkennung ein sinnvolles Resultat liefern.

Benötigte Mathematik

Zusätzlich zu den schon verwendeten Strahlensatz, Grundrechnungsarten, Abrunden von Dezimalzahlen zu Ganzzahlen, Kugelkoordinaten (inkl. Sinus und Cosinus): Parameterdarstellung von Geraden im Raum, Schnitt mit Ebenen und Lösen linearer Gleichungen.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google Foto

Du kommentierst mit Deinem Google-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.