3D mach es selbst. Teil 1: Pixel und Linien



Ich möchte diese Artikelserie Lesern widmen, die die Welt der 3D-Programmierung von Grund auf neu erkunden möchten, sowie Menschen, die die Grundlagen der Erstellung der 3D-Komponente von Spielen und Anwendungen erlernen möchten. Wir werden jede Operation von Grund auf neu implementieren, um jeden Aspekt zu verstehen, auch wenn es bereits eine vorgefertigte Funktion gibt, die sie schneller macht. Nachdem wir gelernt haben, werden wir zu den integrierten Werkzeugen für die Arbeit mit 3D wechseln. Nachdem Sie die Artikelserie gelesen haben, werden Sie verstehen, wie Sie komplexe dreidimensionale Szenen mit Licht, Schatten, Texturen und Effekten erstellen, wie Sie dies alles ohne fundierte mathematische Kenntnisse tun und vieles mehr. Sie können dies alles unabhängig und mit Hilfe von vorgefertigten Werkzeugen tun.

Im ersten Teil werden wir betrachten:

  • Rendering-Konzepte (Software, Hardware)
  • Was ist ein Pixel / eine Oberfläche?
  • Detaillierte Analyse der Zeilenausgabe

Um Ihre kostbare Zeit nicht mit dem Lesen von Artikeln zu verschwenden, die für eine unvorbereitete Person möglicherweise unverständlich sind, werde ich mich sofort den Anforderungen zuwenden. Sie können sicher mit dem Lesen von Artikeln in 3D beginnen, wenn Sie die Grundlagen der Programmierung in einer beliebigen Sprache kennen, weil Ich werde mich nur auf das Studium der 3D-Programmierung konzentrieren und nicht auf das Studium der Merkmale der Sprache und der Grundlagen der Programmierung. Was die mathematische Vorbereitung betrifft, sollten Sie sich hier keine Sorgen machen, obwohl viele keine Lust haben, 3D zu studieren, weil Sie haben Angst vor komplexen Berechnungen und wütenden Formeln, von denen Albträume später träumen, aber tatsächlich gibt es keinen Grund zur Sorge. Ich werde versuchen, alles, was für 3D notwendig ist, so klar wie möglich zu erklären. Man muss nur multiplizieren, dividieren, addieren und subtrahieren können. Wenn Sie die Auswahlkriterien erfüllt haben, können Sie mit dem Lesen beginnen.

Bevor wir uns mit der interessanten Welt von 3D befassen, wählen wir als Beispiel eine Programmiersprache sowie eine Entwicklungsumgebung. Welche Sprache soll ich für die Programmierung von 3D-Grafiken wählen? Jeder, Sie können dort arbeiten, wo Sie sich am wohlsten fühlen, die Mathematik wird überall gleich sein. In diesem Artikel werden alle Beispiele im Kontext von JS gezeigt (hier fliegen Tomaten in mich hinein). Warum js? Es ist einfach - in letzter Zeit habe ich hauptsächlich mit ihm gearbeitet, und deshalb kann ich Ihnen die Essenz effektiver vermitteln. Ich werde alle Funktionen von JS in den Beispielen umgehen, weil Wir brauchen nur die grundlegendsten Funktionen, die eine Sprache hat, daher werden wir speziell auf 3D achten. Aber du wählst, was du liebst, weil In den Artikeln werden nicht alle Formeln an die Funktionen einer Programmiersprache gebunden. Welche Umgebung soll ich wählen? Es spielt keine Rolle,Im Fall von JS ist jeder Texteditor geeignet. Sie können den nächstgelegenen verwenden.

In allen Beispielen wird Leinwand zum Malen verwendet, z Damit können Sie sehr schnell und ohne detaillierte Analyse mit dem Zeichnen beginnen. Canvas ist ein leistungsstarkes Werkzeug mit vielen vorgefertigten Methoden zum Zeichnen, aber von all seinen Funktionen werden wir zum ersten Mal nur die Pixelausgabe verwenden! 

Alle dreidimensionalen Anzeigen auf dem Bildschirm mit Pixeln. Später in den Artikeln werden Sie sehen, wie dies geschieht. Wird es langsamer? Ohne Hardwarebeschleunigung (zB Beschleunigung durch eine Grafikkarte) - wird. Im ersten Artikel werden wir keine Beschleunigungen verwenden, sondern alles von Grund auf neu schreiben, um die grundlegenden Aspekte von 3D zu verstehen. Schauen wir uns einige Begriffe an, die in zukünftigen Artikeln erwähnt werden:

  • (Rendering) — 3D- . , 3D- , , .
  • (Software Rendering) — . , , , - . , . 3D- , — .
  • Hardware-Rendering - Ein hardwareunterstützter Rendering-Prozess. Ich benutze es Spiele und Anwendungen. Alles funktioniert sehr schnell, weil Viel Routine-Computing übernimmt die dafür vorgesehene Grafikkarte.

Ich strebe nicht den Titel "Definition des Jahres" an und versuche, alle Begriffsbeschreibungen so klar wie möglich zu formulieren. Die Hauptsache ist, die Idee zu verstehen, die dann unabhängig entwickelt werden kann. Ich möchte auch darauf hinweisen, dass alle Codebeispiele, die in den Artikeln gezeigt werden, häufig nicht auf Geschwindigkeit optimiert sind, um das Verständnis zu erleichtern. Wenn Sie die Hauptsache verstehen - wie 3D-Grafiken funktionieren, können Sie alles selbst optimieren.

Erstellen Sie zunächst ein Projekt. Für mich ist es nur eine Textdatei index.html mit folgendem Inhalt:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>3D it’s easy. Part 1</title>
</head>

<body>
    <!--         -->
    <canvas id="surface" width="800" height="600"></canvas>

    <script>
        //    
    </script>
</body>

</html>

Ich werde mich jetzt nicht zu sehr auf JS und Canvas konzentrieren - dies sind nicht die Hauptfiguren dieses Artikels. Zum allgemeinen Verständnis werde ich jedoch klarstellen, dass <canvas ...> ein Rechteck (in meinem Fall 800 x 600 Pixel groß) ist, auf dem alle Grafiken angezeigt werden. Ich habe Canvas einmal registriert und werde es nicht mehr ändern.

<script></script> 

Skript - ein Element, in das wir die gesamte Logik zum Rendern von 3D-Grafiken mit unseren eigenen Händen (in JavaScript) schreiben. 

Wenn wir gerade die Struktur der Datei index.html des neu erstellten Projekts überprüft haben , werden wir uns mit 3D-Grafiken befassen.

Wenn wir etwas in das Fenster zeichnen, wird dies in der endgültigen Zählung zu Pixeln, da diese auf dem Monitor angezeigt werden. Je mehr Pixel, desto schärfer das Bild, aber der Computer lädt auch mehr. Wie wird gespeichert, was wir im Fenster zeichnen? Die Grafiken in jedem Fenster können als Pixelarray dargestellt werden, und das Pixel selbst ist nur eine Farbe. Das heißt, eine Bildschirmauflösung von 800 x 600 bedeutet, dass unser Fenster 600 Zeilen mit jeweils 800 Pixeln enthält, nämlich 800 * 600 = 480000 Pixel, nicht wahr? Pixel werden in einem Array gespeichert. Überlegen wir uns, in welchem ​​Array wir die Pixel speichern würden. Wenn wir 800 mal 600 Pixel haben sollten, dann ist die naheliegendste Option ein zweidimensionales Array von 800 mal 600 Pixel. Und dies ist fast die richtige Option oder vielmehr die völlig richtige Option. Bei den Pixeln des Fensters ist es jedoch besser, in einem eindimensionalen Array von 480.000 Elementen zu speichern (wenn die Auflösung 800 x 600 beträgt).nur weil es schneller ist, mit einem eindimensionalen Array zu arbeiten, weil es wird in einer fortlaufenden Folge von Bytes im Speicher gespeichert (alles liegt in der Nähe und ist daher leicht zu bekommen). In einem zweidimensionalen Array (zum Beispiel im Fall von JS) kann jede Zeile an verschiedenen Stellen im Speicher verstreut sein, so dass der Zugriff auf die Elemente eines solchen Arrays länger dauert. Um über ein eindimensionales Array zu iterieren, ist nur 1 Zyklus erforderlich, und für zweidimensionale ganze Zahlen 2 ist hier die Geschwindigkeit wichtig, da Zehntausende von Iterationen des Zyklus durchgeführt werden müssen. Was ist ein Pixel in einem solchen Array? Wie oben erwähnt - dies ist nur eine Farbe oder vielmehr 3 ihrer Komponenten (rot, grün, blau). Jedes selbst das farbenfrohste Bild besteht nur aus einer Reihe von Pixeln in verschiedenen Farben. Ein Pixel im Speicher kann beliebig gespeichert werden, entweder als Array mit 3 Elementen oder in einer Struktur, in der Rot, Grün,Blau; oder etwas anderes. Ein Bild, das aus einer Reihe von Pixeln besteht, die wir gerade analysieren, werde ich weiterhin als Oberfläche bezeichnen. Es stellt sich heraus, dass, da alles, was auf dem Bildschirm angezeigt wird, in einem Array von Pixeln gespeichert ist und dann Elemente (Pixel) in diesem Array geändert werden, das Bild auf dem Bildschirm Pixel für Pixel geändert wird. Genau das werden wir in diesem Artikel tun.

Es gibt keine Pixelzeichnungsfunktion in der Leinwand, aber es ist möglich, auf ein eindimensionales Array von Pixeln zuzugreifen, das wir oben besprochen haben. Wie das geht, sehen Sie im folgenden Beispiel (dieses und alle zukünftigen Beispiele befinden sich nur noch im Skriptelement):

//     ()    
const ctx = document
.getElementById('surface')
.getContext('2d')

//     ,    
// +       
const imageData = ctx.createImageData(800, 600)

Im Beispiel ist imageData ein Objekt mit drei Eigenschaften:

  • Höhe und Breite - Ganzzahlen, die die Höhe und Breite des Fensters zum Zeichnen speichern
  • Daten - 8-Bit-Ganzzahl-Array ohne Vorzeichen (Sie können Zahlen im Bereich von 0 bis 255 darin speichern)

Das Datenarray hat eine einfache, aber erklärende Struktur. Dieses eindimensionale Array speichert Daten jedes Pixels, die auf dem Bildschirm im folgenden Format angezeigt werden:
Die ersten 4 Elemente des Arrays (Indizes 0,1,2,3) sind die Daten des ersten Pixels in der ersten Zeile. Die zweiten 4 Elemente (Indizes 4, 5, 6, 7) sind die Daten des zweiten Pixels der ersten Zeile. Wenn wir zum 800. Pixel der ersten Zeile gelangen, vorausgesetzt, das Fenster ist 800 Pixel breit, gehört das 801. Pixel bereits zur zweiten Zeile. Wenn wir es ändern, sehen wir auf dem Bildschirm, dass sich das 1. Pixel der 2. Zeile geändert hat (obwohl es nach der Anzahl im Array das 801. Pixel ist). Warum gibt es 4 Elemente für jedes Pixel im Array? Dies liegt daran, dass auf Leinwand zusätzlich zu jeder Farbe 1 Element zugewiesen wird - Rot, Grün, Blau (dies sind 3 Elemente), 1 weiteres Element für Transparenz (sie sagen auch den Alphakanal oder die Deckkraft). Der Alphakanal wird wie die Farbe im Bereich von 0 (transparent) bis 255 (undurchsichtig) eingestellt. Mit dieser Struktur erhalten wir ein 32-Bit-Bild,weil jedes Pixel aus 4 Elementen mit 8 Bits besteht. Zusammenfassend: Jedes Pixel enthält: rote, grüne, blaue Farben und Alphakanal (Transparenz). Dieses Farbschema heißt ARGB (Alpha Red Green Blue). Und die Tatsache, dass jedes Pixel 32 Bit belegt, besagt, dass wir ein 32-Bit-Bild haben (sie sagen auch ein Bild mit einer Farbtiefe von 32 Bit).

Standardmäßig wird das gesamte Array von Pixeln imageData.data (Daten sind eine Eigenschaft, in der das Array von Pixeln und imageData nur ein Objekt ist) mit den Werten 0 gefüllt. Wenn wir versuchen würden, ein solches Array auszugeben, wird auf dem Bildschirm nichts Interessantes angezeigt, da 0 , 0, 0 ist schwarz, aber da die Transparenz hier auch 0 ist und dies eine vollständig transparente Farbe ist, sehen wir nicht einmal Schwarz auf dem Bildschirm!

Es ist unpraktisch, direkt mit einem solchen eindimensionalen Array zu arbeiten, daher schreiben wir eine Klasse dafür, in der wir Methoden zum Zeichnen erstellen. Ich werde die Klasse benennen - Schublade. Diese Klasse speichert nur die erforderlichen Daten und führt die erforderlichen Berechnungen durch, wobei so viel wie möglich von dem zum Rendern verwendeten Tool abstrahiert wird. Deshalb werden wir alle Berechnungen platzieren und mit dem Array darin arbeiten. Und genau den Aufruf der Anzeigemethode auf Leinwand werden wir außerhalb der Klasse platzieren, weil Es könnte etwas anderes als Leinwand geben. In diesem Fall muss unsere Klasse nicht geändert werden. Um mit einem Array von Pixeln (Oberfläche) zu arbeiten, ist es für uns bequemer, es in der Drawer-Klasse sowie in der Breite und Höhe des Bildes zu speichern, damit wir korrekt auf das gewünschte Pixel zugreifen können. Die Drawer-Klasse sieht für mich so aus, während die für das Zeichnen erforderlichen Mindestdaten beibehalten werden:

class Drawer {
    surface = null
    width = 0
    height = 0

    constructor(surface, width, height) {
        this.surface = surface
        this.width = width
        this.height = height
    }
}

Wie Sie im Konstruktor sehen können, nimmt die Drawer-Klasse alle erforderlichen Daten und speichert sie. Jetzt können Sie eine Instanz dieser Klasse erstellen und ein Array aus Pixeln, Breite und Höhe übergeben (wir haben bereits alle diese Daten, da wir sie oben erstellt und in imageData gespeichert haben):

const drawer = new Drawer(
    imageData.data,
    imageData.width,
    imageData.height
)

In der Drawer-Klasse werden wir verschiedene Zeichenfunktionen schreiben, um die Arbeit in Zukunft zu vereinfachen. Wir werden eine Funktion zum Zeichnen eines Pixels, eine Funktion zum Zeichnen einer Linie und in weiteren Artikeln Funktionen zum Zeichnen eines Dreiecks und anderer Formen haben. Beginnen wir jedoch mit der Pixelzeichnungsmethode. Ich werde ihn drawPixel nennen. Wenn wir ein Pixel zeichnen, sollte es neben der Farbe auch Koordinaten haben:

drawPixel(x, y, r, g, b)  { }

Bitte beachten Sie, dass die drawPixel-Funktion den Alpha-Parameter (Transparenz) nicht akzeptiert. Oben haben wir herausgefunden, dass das Pixel-Array aus 3 Farbparametern und 1 Transparenzparameter besteht. Ich habe nicht ausdrücklich auf Transparenz hingewiesen, da wir sie für Beispiele absolut nicht benötigen. Standardmäßig setzen wir 255 (d. H. Alles ist undurchsichtig). Lassen Sie uns nun darüber nachdenken, wie Sie die gewünschte Farbe in ein Array von Pixeln in x, y-Koordinaten schreiben. Da wir alle Informationen über das Bild in einem eindimensionalen Array gespeichert haben, in dem 1 Pixel für jedes Pixel (8 Bit) zugeordnet ist. Um auf das gewünschte Pixel im Array zuzugreifen, müssen wir zuerst den roten Positionsindex bestimmen, da jedes Pixel damit beginnt (z. B. [r, g, b, a]). Eine kleine Erklärung der Struktur des Arrays:



Die grüne Tabelle zeigt an, wie Farbkomponenten in einem eindimensionalen Oberflächenarray gespeichert werden. Ihre Indizes im selben Array sind blau angegeben, und die Koordinaten des Pixels, das die drawPixel-Funktionen akzeptiert, die wir in Indizes im eindimensionalen Array konvertieren müssen, geben r, g, b, a für das Pixel in blau an. Aus der Tabelle ist also ersichtlich, dass für jedes Pixel die rote Komponente der Farbe an erster Stelle steht. Beginnen wir damit. Angenommen, wir möchten die rote Komponente der Pixelfarbe in den Koordinaten X1Y1 mit einer Bildgröße von 2 x 2 Pixel ändern. In der Tabelle sehen wir, dass dies Index 12 ist, aber wie berechnet man ihn? Zuerst finden wir den Index der Zeile, die wir benötigen, dafür multiplizieren wir die Bildbreite mit Y und mit 4 (die Anzahl der Werte pro Pixel) - dies ist:

width * y * 4 
//  :
2 * 1 * 4 = 8

Wir sehen, dass die 2. Zeile mit Index 8 beginnt. Wenn wir mit der Platte vergleichen, konvergiert das Ergebnis.

Jetzt müssen Sie dem gefundenen Zeilenindex einen Spaltenversatz hinzufügen, um den gewünschten roten Index zu erhalten. Fügen Sie dazu dem Zeilenindex X mal 4 hinzu. Die vollständige Formel lautet:

width * y * 4 + x * 4 
//     :
(width * y + x) * 4
//  :
(2 * 1 + 1) * 4 = 12

Jetzt vergleichen wir 12 mit der Tabelle und sehen, dass das Pixel X1Y1 wirklich mit Index 12 beginnt.

Um die Indizes anderer Farbkomponenten zu finden, müssen Sie dem roten Index einen Farbversatz hinzufügen: +1 (grün), +2 (blau), +3 (alpha) . Jetzt können wir die drawPixel-Methode in der Drawer-Klasse mithilfe der obigen Formel implementieren:

drawPixel(x, y, r, g, b) {
    const offset = (this.width * y + x) * 4

    this.surface[offset] = r
    this.surface[offset + 1] = g
    this.surface[offset + 2] = b
    this.surface[offset + 3] = 255
}

Bei dieser drawPixel-Methode habe ich den sich wiederholenden Teil der Formel auf die Offset-Konstante gerendert. Es ist auch zu sehen, dass ich in Alpha nur 255 schreibe, weil es ist in der Struktur, aber jetzt müssen wir keine Pixel mehr ausgeben.

Es ist Zeit, den Code zu testen und endlich das erste Pixel auf dem Bildschirm zu sehen. Hier ist ein Beispiel mit der Pixel-Render-Methode:

//     Drawer
drawer.drawPixel(10, 10, 255, 0, 0)
drawer.drawPixel(10, 20, 0, 0, 255)

//         canvas
ctx.putImageData(imageData, 0, 0)

Im obigen Beispiel zeichne ich 2 Pixel, eines rot 255, 0, 0, das andere blau 0, 0, 255. Die Änderungen im Array imageData.data (es ist auch die Oberfläche innerhalb der Drawer-Klasse) werden jedoch nicht auf dem Bildschirm angezeigt. Zum Zeichnen müssen Sie ctx.putImageData (imageData, 0, 0) aufrufen, wobei imageData das Objekt ist, in dem das Pixelarray und die Breite / Höhe des Zeichenbereichs angezeigt werden, und 0, 0 der Punkt ist, relativ zu dem das Pixelarray angezeigt wird (lassen Sie immer 0, 0 ) Wenn Sie alles richtig gemacht haben, wird oben links im Canvas-Element im Browserfenster das folgende Bild angezeigt: Haben Sie die



Pixel gesehen? Sie sind so klein und wie viel Arbeit wurde geleistet.

Versuchen wir nun, dem Beispiel ein wenig Dynamik hinzuzufügen, sodass sich unser Pixel alle 10 Millisekunden nach rechts verschiebt (wir ändern X Pixel alle 10 Millisekunden um +1). Wir korrigieren den Pixelzeichnungscode in Intervallen um eins:

let x = 10
setInterval(() => {

    drawer.drawPixel(x++, 20, 0, 0, 255)
    ctx.putImageData(imageData, 0, 0)

}, 10)

In diesem Beispiel habe ich nur die Ausgabe des blauen Pixels belassen und die Funktion setInterval mit dem Parameter 10 in JavaScript umbrochen. Dies bedeutet, dass der Code ungefähr alle 10 Millisekunden aufgerufen wird. Wenn Sie ein solches Beispiel ausführen, werden Sie feststellen, dass anstelle eines nach rechts verschobenen Pixels etwa Folgendes angezeigt wird:



Ein so langer Streifen (oder eine so lange Spur) bleibt erhalten, da die Farbe des vorherigen Pixels im Oberflächenarray nicht gelöscht wird. Bei jedem Aufruf des Intervalls, das wir hinzufügen ein Pixel. Schreiben wir eine Methode, mit der die Oberfläche in ihren ursprünglichen Zustand versetzt wird. Mit anderen Worten, füllen Sie das Array mit Nullen. Fügen Sie der Klasse Drawer die Methode clearSurface hinzu:

clearSurface() {
    const surfaceSize = this.width * this.height * 4
    for (let i = 0; i < surfaceSize; i++) {
        this.surface[i] = 0
    }
}

In diesem Array gibt es keine Logik, nur das Auffüllen mit Nullen. Es wird empfohlen, diese Methode jedes Mal aufzurufen, bevor Sie ein neues Bild zeichnen. Im Fall einer Pixelanimation vor dem Zeichnen dieses Pixels:

let x = 10
setInterval(() => {
    drawer.clearSurface()
    drawer.drawPixel(x++, 20, 0, 0, 255)
    ctx.putImageData(imageData, 0, 0)
}, 10)

Wenn Sie nun dieses Beispiel ausführen, wird das Pixel nacheinander nach rechts verschoben - ohne unnötige Spuren von vorherigen Koordinaten.

Das Letzte, was wir im ersten Artikel implementieren, ist die Strichzeichnungsmethode. Fügen Sie es natürlich der Drawer-Klasse hinzu. Die Methode, die ich drawLine nennen werde. Was wird er nehmen? Im Gegensatz zu einem Punkt hat die Linie immer noch die Koordinaten, an denen sie endet. Mit anderen Worten, die Linie hat einen Anfang, ein Ende und eine Farbe, die wir an die Methode übergeben werden:

drawLine(x1, y1, x2, y2, r, g, b) { }

Jede Zeile besteht aus Pixeln, es bleibt nur, um sie korrekt mit Pixeln von x1, y1 bis x2, y2 zu füllen. Da die Zeile aus Pixeln besteht, geben wir sie zunächst Pixel für Pixel in der Schleife aus. Wie berechnet man jedoch, wie viele Pixel ausgegeben werden sollen? Um beispielsweise eine Linie von [0, 0] nach [3, 0] zu zeichnen, ist intuitiv klar, dass Sie 4 Pixel benötigen ([0, 0], [1, 0], [2, 0], [3, 0],). . Von [12, 6] bis [43, 14] ist jedoch bereits nicht klar, wie lang die Linie sein wird (wie viele Pixel angezeigt werden sollen) und welche Koordinaten sie haben werden. Erinnern Sie sich dazu an eine kleine Geometrie. Wir haben also eine Linie, die bei x1, y1 beginnt und bei x2, y2 endet.


Zeichnen wir eine gepunktete Linie vom Anfang und vom Ende, so dass wir ein Dreieck erhalten (Bild oben). Wir werden sehen, dass sich an der Verbindungsstelle der gezeichneten Linien ein Winkel von 90 Grad gebildet hat. Wenn das Dreieck einen solchen Winkel hat, wird das Dreieck als rechteckig bezeichnet, und seine Seiten, zwischen denen der Winkel 90 Grad beträgt, werden als Beine bezeichnet. Die dritte durchgezogene Linie (die wir zu zeichnen versuchen) heißt Hypotenuse in einem Dreieck. Mit diesen beiden eingeführten Beinen (c1 und c2 in der Abbildung) können wir die Länge der Hypotenuse mit dem Satz von Pythagoras berechnen. Mal sehen, wie es geht. Die Formel für die Länge der Hypotenuse (oder Linienlänge) lautet wie folgt: 

=12+22


Wie man beide Beine bekommt, sieht man auch aus dem Dreieck. Unter Verwendung der obigen Formel finden wir nun die Hypotenuse, die die lange Zeile (die Anzahl der Pixel) sein wird:

 drawLine(x1, y1, x2, y2, r, g, b) {
         const c1 = y2 - y1
         const c2 = x2 - x1

         const length = Math.sqrt(c1 * c1 + c2 * c2)

Wir wissen bereits, wie viele Pixel gezeichnet werden müssen, um eine Linie zu zeichnen. Wir wissen jedoch noch nicht, wie Pixel verschoben werden. Das heißt, wir müssen eine Linie von x1, y1 bis x2, y2 zeichnen. Wir wissen, dass die Linienlänge beispielsweise 20 Pixel beträgt. Wir können das 1. Pixel in x1, y1 und das letzte in x2, y2 zeichnen, aber wie findet man die Koordinaten der Zwischenpixel? Dazu müssen wir herausfinden, wie jedes nächste Pixel in Bezug auf x1, y1 verschoben wird, um die gewünschte Linie zu erhalten. Ich werde noch ein Beispiel geben, um besser zu verstehen, um welche Art von Verschiebung es sich handelt. Wir haben Punkte [0, 0] und [0, 3], wir müssen eine Linie auf sie ziehen. Aus dem Beispiel ist klar ersichtlich, dass der nächste Punkt nach [0, 0] [0, 1] und dann [0, 2] und schließlich [0, 3] sein wird. Das heißt, X von jedem Punkt wurde nicht verschoben, oder wir können sagen, dass es um 0 Pixel verschoben wurde, und Y wurde um 1 Pixel verschoben, dies ist der Versatz.es kann als [0, 1] geschrieben werden. Ein weiteres Beispiel: Wir haben einen Punkt [0, 0] und einen Punkt [3, 6]. Versuchen wir in unserem Kopf zu berechnen, wie sie sich verschieben. Der erste ist [0, 0], dann [0,5, 1], dann [1, 2]. dann [1.5, 3] usw. zu [3, 6], in diesem Beispiel beträgt der Versatz [0.5, 1]. Wie berechnet man das? 

Sie können die folgende Formel verwenden:

   = 2 /  
  Y = 1 /   

Im Programmcode haben wir Folgendes:

const xStep = c2 / length
const yStep = c1 / length

Alle Daten sind bereits vorhanden: die Länge der Linie, der Versatz der Pixel entlang X und Y. Wir beginnen im Zyklus mit dem Zeichnen:

for (let i = 0; i < length; i++) {
    this.drawPixel(
        Math.trunc(x1 + xStep * i),
        Math.trunc(y1 + yStep * i),
        r, g, b,
    )
}

Als X-Koordinate der Pixelfunktion übertragen wir den Anfang der X-Linie + Versatz X * i. Um die Koordinate des i-ten Pixels zu erhalten, berechnen wir auch die Y-Koordinate. Math.trunc ist eine Methode in JS, mit der Sie den Bruchteil einer Zahl verwerfen können. Der gesamte Methodencode sieht folgendermaßen aus:

drawLine(x1, y1, x2, y2, r, g, b) {
    const c1 = y2 - y1
    const c2 = x2 - x1

    const length = Math.sqrt(c1 * c1 + c2 * c2)

    const xStep = c2 / length
    const yStep = c1 / length

    for (let i = 0; i < length; i++) {
        this.drawPixel(
            Math.trunc(x1 + xStep * i),
            Math.trunc(y1 + yStep * i),
            r, g, b,
        )
    }
}

Der erste Teil ist zu Ende gegangen, ein langer, aber aufregender Weg, um die 3D-Welt zu verstehen. Es gab noch nichts Dreidimensionales, aber wir haben vorbereitende Operationen zum Zeichnen durchgeführt: Wir haben die Funktionen zum Zeichnen eines Pixels, einer Linie, zum Löschen eines Fensters implementiert und einige Begriffe gelernt. Der gesamte Code der Drawer-Klasse kann unter dem Spoiler angezeigt werden:

Schubladenklassencode
class Drawer {
  surface = null
  width = 0
  height = 0

  constructor(surface, width, height) {
    this.surface = surface
    this.width = width
    this.height = height
  }

  drawPixel(x, y, r, g, b)  {
    const offset = (this.width * y + x) * 4

    this.surface[offset] = r
    this.surface[offset + 1] = g
    this.surface[offset + 2] = b
    this.surface[offset + 3] = 255
  }

  drawLine(x1, y1, x2, y2, r, g, b) {
    const c1 = y2 - y1
    const c2 = x2 - x1

    const length = Math.sqrt(c1 * c1 + c2 * c2)

    const xStep = c2 / length
    const yStep = c1 / length

    for (let i = 0 ; i < length ; i++) {
        this.drawPixel(
          Math.trunc(x1 + xStep * i),
          Math.trunc(y1 + yStep * i),
          r, g, b,
        )
    }
  }

  clearSurface() {
    const surfaceSize = this.width * this.height * 4
    for (let i = 0; i < surfaceSize; i++) {
      this.surface[i] = 0
    }
  }
}

Was weiter?


Im nächsten Artikel werden wir untersuchen, wie eine so einfache Operation wie die Ausgabe eines Pixels und einer Linie zu interessanten 3D-Objekten werden kann. Wir werden uns mit Matrizen und Operationen an ihnen vertraut machen, ein dreidimensionales Objekt in einem Fenster anzeigen und sogar Animationen hinzufügen.

All Articles