Heim >Web-Frontend >js-Tutorial >Dreiecke im Web zeichnen etwas
Diese Serie stellt WebGPU und Computergrafik im Allgemeinen vor.
Schauen wir uns zunächst an, was wir bauen werden,
Lebensspiel
3D-Rendering
3D-Rendering, aber mit Beleuchtung
3D-Modell rendern
Außer den Grundkenntnissen von JS sind keine Vorkenntnisse erforderlich.
Das Tutorial ist bereits auf meinem Github fertig, zusammen mit dem Quellcode.
WebGPU ist eine relativ neue API für die GPU. Obwohl es als WebGPU bezeichnet wird, kann es tatsächlich als eine Schicht über Vulkan, DirectX 12 und Metal, OpenGL und WebGL betrachtet werden. Es ist als Low-Level-API konzipiert und soll für Hochleistungsanwendungen wie Spiele und Simulationen verwendet werden.
In diesem Kapitel werden wir etwas auf den Bildschirm zeichnen. Der erste Teil bezieht sich auf das Google Codelabs-Tutorial. Wir werden ein Lebensspiel auf dem Bildschirm erstellen.
Wir erstellen einfach ein leeres Vanilla-JS-Projekt in Vite mit aktiviertem Typescript. Löschen Sie dann alle zusätzlichen Codes und lassen Sie nur main.ts übrig.
const main = async () => { console.log('Hello, world!') } main()
Überprüfen Sie vor dem eigentlichen Codieren, ob in Ihrem Browser WebGPU aktiviert ist. Sie können es unter WebGPU-Beispiele überprüfen.
Chrome ist jetzt standardmäßig aktiviert. In Safari sollten Sie zu den Entwicklereinstellungen gehen, Einstellungen markieren und WebGPU aktivieren.
Wir müssen außerdem drei Typen für WebGPU aktivieren, @webgpu/types installieren und in den TSC-Compiler-Optionen „types“ hinzufügen: ["@webgpu/types"].
Außerdem ersetzen wir das
Es gibt viele Standardcodes für WebGPU, so sieht es aus.
Zuerst benötigen wir Zugriff auf die GPU. In WebGPU geschieht dies durch das Konzept eines Adapters, der eine Brücke zwischen der GPU und dem Browser darstellt.
const adapter = await navigator.gpu.requestAdapter();
Dann müssen wir ein Gerät vom Adapter anfordern.
const device = await adapter.requestDevice(); console.log(device);
Wir zeichnen unser Dreieck auf die Leinwand. Wir müssen das Canvas-Element abrufen und konfigurieren.
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });
Hier verwenden wir getContext, um relative Informationen über die Leinwand abzurufen. Durch die Angabe von webgpu erhalten wir einen Kontext, der für das Rendern mit WebGPU verantwortlich ist.
CanvasFormat ist eigentlich der Farbmodus, zum Beispiel srgb. Normalerweise verwenden wir einfach das bevorzugte Format.
Zuletzt konfigurieren wir den Kontext mit dem Gerät und dem Format.
Bevor wir tiefer in die technischen Details eintauchen, müssen wir zunächst verstehen, wie die GPU mit dem Rendern umgeht.
Die GPU-Rendering-Pipeline besteht aus einer Reihe von Schritten, die die GPU ausführt, um ein Bild zu rendern.
Die auf der GPU ausgeführte Anwendung wird als Shader bezeichnet. Der Shader ist ein Programm, das auf der GPU läuft. Der Shader verfügt über eine spezielle Programmiersprache, die wir später besprechen werden.
Die Render-Pipeline umfasst die folgenden Schritte:
Abhängig von den Grundelementen, der kleinsten Einheit, die die GPU rendern kann, kann die Pipeline unterschiedliche Schritte haben. Normalerweise verwenden wir Dreiecke, die der GPU signalisieren, alle drei Scheitelpunktgruppen als Dreieck zu behandeln.
Render Pass ist ein Schritt des vollständigen GPU-Renderings. Wenn ein Renderdurchgang erstellt wird, beginnt die GPU mit dem Rendern der Szene und umgekehrt, wenn sie abgeschlossen ist.
Um einen Render-Pass zu erstellen, müssen wir einen Encoder erstellen, der für die Kompilierung des Render-Passes in GPU-Codes verantwortlich ist.
const main = async () => { console.log('Hello, world!') } main()Dann erstellen wir einen Renderdurchgang.
const adapter = await navigator.gpu.requestAdapter();Hier erstellen wir einen Renderdurchgang mit einem Farbanhang. Anhang ist ein Konzept in der GPU, das das Bild darstellt, das gerendert werden soll. Ein Bild kann viele Aspekte haben, die die GPU verarbeiten muss, und jeder davon ist ein Anhang.
Hier haben wir nur einen Anhang, nämlich den Farbanhang. Die Ansicht ist das Panel, auf dem die GPU rendert. Hier stellen wir es auf die Textur der Leinwand ein.
loadOp ist der Vorgang, den die GPU vor dem Renderdurchgang ausführt. Clear bedeutet, dass die GPU zunächst alle vorherigen Daten aus dem letzten Frame löscht, und storeOp ist der Vorgang, den die GPU nach dem Renderdurchlauf ausführt. Store bedeutet GPU speichert die Daten in der Textur.
loadOp kann „load“ sein, wodurch die Daten aus dem letzten Frame erhalten bleiben, oder „clear“, wodurch die Daten aus dem letzten Frame gelöscht werden. StoreOp kann Store sein, wodurch die Daten in der Textur gespeichert werden, oder Discard, wodurch die Daten verworfen werden.
Jetzt rufen Sie einfach pass.end() auf, um den Renderdurchlauf zu beenden. Jetzt wird der Befehl im Befehlspuffer der GPU gespeichert.
Um den kompilierten Befehl zu erhalten, verwenden Sie den folgenden Code:
const device = await adapter.requestDevice(); console.log(device);Und schließlich senden Sie den Befehl an die Renderwarteschlange der GPU.
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });Jetzt sollten Sie eine hässliche schwarze Leinwand sehen.
Basierend auf unseren stereotypen Vorstellungen von 3D würden wir erwarten, dass leerer Raum eine blaue Farbe hat. Das können wir erreichen, indem wir die klare Farbe festlegen.
const encoder = device.createCommandEncoder();Zeichnen eines Dreiecks mit Shader
Jetzt zeichnen wir ein Dreieck auf die Leinwand. Wir werden dazu einen Shader verwenden. Die Shader-Sprache ist wgsl, WebGPU Shading Language.
Angenommen, wir möchten ein Dreieck mit den folgenden Koordinaten zeichnen:
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(), loadOp: "clear", storeOp: "store", }] });Wie bereits erwähnt, benötigen wir zum Vervollständigen einer Render-Pipeline einen Vertex-Shader und einen Fragment-Shader.
Vertex-Shader
Verwenden Sie den folgenden Code, um Shader-Module zu erstellen.
const commandBuffer = encoder.finish();label ist hier einfach ein Name, der zum Debuggen gedacht ist. Code ist der eigentliche Shader-Code.
Vertex-Shader ist eine Funktion, die jeden Parameter annimmt und die Position des Scheitelpunkts zurückgibt. Entgegen unserer Erwartung gibt der Vertex-Shader jedoch einen vierdimensionalen Vektor und keinen dreidimensionalen Vektor zurück. Die vierte Dimension ist die W-Dimension, die zur perspektivischen Unterteilung verwendet wird. Wir werden es später besprechen.
Jetzt können Sie einen vierdimensionalen Vektor (x, y, z, w) einfach als einen dreidimensionalen Vektor (x / w, y / w, z / w) betrachten.
Es gibt jedoch noch ein weiteres Problem: Wie werden die Daten an den Shader übergeben und wie erhält man die Daten aus dem Shader?
Um die Daten an den Shader zu übergeben, verwenden wir den vertexBuffer, einen Puffer, der die Daten der Vertices enthält. Mit dem folgenden Code können wir einen Puffer erstellen:
const main = async () => { console.log('Hello, world!') } main()Hier erstellen wir einen Puffer mit einer Größe von 24 Bytes, 6 Floats, was der Größe der Scheitelpunkte entspricht.
Nutzung ist die Nutzung des Puffers, der VERTEX für Scheitelpunktdaten ist. GPUBufferUsage.COPY_DST bedeutet, dass dieser Puffer als Kopierziel gültig ist. Für alle Puffer, deren Daten von der CPU geschrieben werden, müssen wir dieses Flag setzen.
Die Zuordnung bedeutet hier, den Puffer der CPU zuzuordnen, was bedeutet, dass die CPU den Puffer lesen und schreiben kann. Das Aufheben der Zuordnung bedeutet, dass die Zuordnung des Puffers aufgehoben wird, was bedeutet, dass die CPU den Puffer nicht mehr lesen und schreiben kann und der Inhalt somit für die GPU verfügbar ist.
Jetzt können wir die Daten in den Puffer schreiben.
const adapter = await navigator.gpu.requestAdapter();Hier ordnen wir den Puffer der CPU zu und schreiben die Daten in den Puffer. Dann heben wir die Zuordnung des Puffers auf.
vertexBuffer.getMappedRange() gibt den Bereich des Puffers zurück, der der CPU zugeordnet ist. Wir können es verwenden, um die Daten in den Puffer zu schreiben.
Dies sind jedoch nur Rohdaten, und die GPU weiß nicht, wie sie sie interpretieren soll. Wir müssen das Layout des Puffers definieren.
const device = await adapter.requestDevice(); console.log(device);ArrayStride ist hier die Anzahl der Bytes, die die GPU benötigt, um im Puffer vorwärts zu springen, wenn sie nach der nächsten Eingabe sucht. Wenn der ArrayStride beispielsweise 8 ist, überspringt die GPU 8 Bytes, um die nächste Eingabe zu erhalten.
Da wir hier float32x2 verwenden, beträgt der Schritt 8 Bytes, 4 Bytes für jedes Float und 2 Floats für jeden Scheitelpunkt.
Jetzt können wir den Vertex-Shader schreiben.
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });Hier bedeutet @vertex, dass es sich um einen Vertex-Shader handelt. @location(0) bedeutet den Standort des Attributs, der wie zuvor definiert 0 ist. Bitte beachten Sie, dass es sich in der Shader-Sprache um das Layout des Puffers handelt. Wenn Sie also einen Wert übergeben, müssen Sie entweder eine Struktur übergeben, deren Felder @location definiert haben, oder nur einen Wert mit @location.
vec2f ist ein zweidimensionaler Gleitkomma-Vektor und vec4f ist ein vierdimensionaler Gleitkomma-Vektor. Da der Vertex-Shader eine vec4f-Position zurückgeben muss, müssen wir diese mit @builtin(position) annotieren.
Fragment-Shader
Fragment-Shader ist ebenfalls etwas, das die interpolierte Vertex-Ausgabe übernimmt und die Anhänge, in diesem Fall die Farbe, ausgibt. Interpoliert bedeutet, dass, obwohl nur bestimmte Pixel an den Eckpunkten einen bestimmten Wert haben, die Werte für jedes andere Pixel entweder linear, gemittelt oder auf andere Weise interpoliert werden. Die Farbe des Fragments ist ein vierdimensionaler Vektor, der der Farbe des Fragments entspricht, nämlich Rot, Grün, Blau und Alpha.
Bitte beachten Sie, dass die Farbe im Bereich von 0 bis 1 und nicht im Bereich von 0 bis 255 liegt. Darüber hinaus definiert der Fragment-Shader die Farbe jedes Scheitelpunkts und nicht die Farbe des Dreiecks. Die Farbe des Dreiecks wird durch Interpolation durch die Farbe der Eckpunkte bestimmt.
Da wir uns derzeit nicht die Mühe machen, die Farbe des Fragments zu steuern, können wir einfach eine konstante Farbe zurückgeben.
const main = async () => { console.log('Hello, world!') } main()Pipeline rendern
Dann definieren wir die angepasste Render-Pipeline, indem wir den Vertex- und Fragment-Shader ersetzen.
const adapter = await navigator.gpu.requestAdapter();Beachten Sie, dass wir im Fragment-Shader das Format des Ziels angeben müssen, das das Format der Leinwand ist.
Anruf ziehen
Bevor der Renderdurchlauf endet, fügen wir den Draw-Aufruf hinzu.
const device = await adapter.requestDevice(); console.log(device);Hier in setVertexBuffer ist der erste Parameter der Index des Puffers, in den Pipeline-Definitionsfeldpuffern, und der zweite Parameter ist der Puffer selbst.
Beim Aufruf von draw ist der Parameter die Anzahl der zu zeichnenden Scheitelpunkte. Da wir 3 Eckpunkte haben, zeichnen wir 3.
Jetzt sollten Sie ein gelbes Dreieck auf der Leinwand sehen.
Zeichne Lebensspielzellen
Jetzt optimieren wir unsere Codes ein wenig – da wir ein Lebensspiel erstellen wollen, müssen wir Quadrate statt Dreiecke zeichnen.
Ein Quadrat besteht eigentlich aus zwei Dreiecken, also müssen wir 6 Eckpunkte zeichnen. Die Änderungen hier sind einfach und Sie benötigen keine detaillierte Erklärung.
const canvas = document.getElementById('app') as HTMLCanvasElement; const context = canvas.getContext("webgpu")!; const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: device, format: canvasFormat, });Jetzt sollten Sie ein gelbes Quadrat auf der Leinwand sehen.
Koordinatensystem
Wir haben das Koordinatensystem der GPU nicht besprochen. Es ist, nun ja, ziemlich einfach. Das eigentliche Koordinatensystem der GPU ist ein rechtshändiges Koordinatensystem, das heißt, die x-Achse zeigt nach rechts, die y-Achse zeigt nach oben und die z-Achse zeigt aus dem Bildschirm heraus.
Der Bereich des Koordinatensystems reicht von -1 bis 1. Der Ursprung liegt in der Mitte des Bildschirms. Die z-Achse reicht von 0 bis 1, 0 ist die nahe Ebene und 1 ist die ferne Ebene. Die Z-Achse dient jedoch der Tiefe. Wenn Sie 3D-Rendering durchführen, können Sie nicht nur die Z-Achse verwenden, um die Position des Objekts zu bestimmen, sondern Sie müssen auch die perspektivische Unterteilung verwenden. Dies wird als NDC (normalisierte Gerätekoordinate) bezeichnet.
Wenn Sie beispielsweise ein Quadrat in der oberen linken Ecke des Bildschirms zeichnen möchten, sind die Eckpunkte (-1, 1), (-1, 0), (0, 1), (0, 0). , allerdings müssen Sie zum Zeichnen zwei Dreiecke verwenden.
Das obige ist der detaillierte Inhalt vonDreiecke im Web zeichnen etwas. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!