最簡WebGL教程,僅需 75 行代碼

做者:Avik Das

翻譯:瘋狂的技術宅javascript

原文:https://avikdas.com/2020/07/0...html

未經容許嚴禁轉載前端

現代 OpenGL(以及名爲WebGL的擴展)與我過去學習的傳統 OpenGL 有很大不一樣。我瞭解柵格化的工做原理,因此對這些概念很滿意。可是我所閱讀的每篇教程都介紹了抽象和輔助函數,這使我很難理解哪些部分是 OpenGL API 的真正核心。java

明確地說,在實際的應用程序中,把位置數據和渲染功能分離到單獨的類這樣的抽象很重要。可是,這些抽象把代碼分佈到了多個區域,而且因爲模板的重複以及邏輯單元之間的數據傳遞而致使大量的開銷。而個人最佳學習方式是線性代碼流,其中每一行都是手頭主題的核心。程序員

首先,本文要歸功於我所學過的教程。從這個基礎開始,我剝離了全部抽象,直到有了一個「最小可行的程序」爲止。但願這將幫助你使用現代OpenGL入門。這就咱們要作的:web

image.png

一個等邊三角形,頂部爲綠色,左下爲黑色,右下爲紅色,中間有過渡顏色面試

初始化

要使用 WebGL,須要用 canvas 進行繪製。你確定會想包括一些經常使用的 HTML 骨架、某些樣式等,可是 canvas 纔是最關鍵的。加載 DOM 後,咱們將可以用 Javascript 訪問畫布。canvas

<canvas id="container" width="500" height="500"></canvas>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    // 全部的 Javascript 代碼將會出如今這裏
  });
</script>

咱們能夠經過畫布的可訪問性得到 WebGL 的渲染上下文,並將其初始化爲透明色。 OpenGL 的世界中的顏色是RGBA,每一個份量都在 01 之間。透明色是用於在從新繪製場景的幀的開始時繪製畫布的顏色。segmentfault

const canvas = document.getElementById('container');
const gl = canvas.getContext('webgl');

gl.clearColor(1, 1, 1, 1);

在實際的程序中,還能夠進行更多的初始化。須要特別注意的是啓用了「深度緩衝區(depth buffer)」,這將容許基於 Z 座標對幾何圖形進行排序。對於只包含一個三角形的最簡程序,咱們將會忽略這種狀況。服務器

編譯着色器

OpenGL 的核心是柵格化框架,在這裏咱們能夠決定如何實現除柵格化以外的全部內容。這須要在 GPU 上至少運行兩段代碼:

  1. 爲輸入所執行的頂點着色器,每一個輸入都會對應輸出一個3D位置(其實是齊次座標中的4D)。
  2. 爲屏幕上的每一個像素所執行的片斷着色器,負責輸出這個像素應該是哪一種顏色。

在這兩個步驟之間,OpenGL 從頂點着色器獲取幾何圖形,並肯定這個幾何圖形實際上覆蓋了屏幕上的哪些像素。這是柵格化部分。

兩種着色器一般都是用 GLSL(OpenGL 着色語言)編寫的,而後將其編譯爲 GPU 的機器代碼。機器代碼隨後被髮送到 GPU,所以能夠在渲染過程當中運行。我不會把太多時間花在 GLSL 上,由於我只是在展現基礎知識,可是這種語言與 C 很接近,着足以讓大多數程序員感到熟悉。

首先,咱們編譯頂點着色器並將其發送到GPU。此處着色器的源代碼被存儲在字符串中,可是也能夠從其餘位置加載。最終,該字符串被髮送到 WebGL API。

const sourceV = `
  attribute vec3 position;
  varying vec4 color;

  void main() {
    gl_Position = vec4(position, 1);
    color = gl_Position * 0.5 + 0.5;
  }
`;

const shaderV = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shaderV, sourceV);
gl.compileShader(shaderV);

if (!gl.getShaderParameter(shaderV, gl.COMPILE_STATUS)) {
  console.error(gl.getShaderInfoLog(shaderV));
  throw new Error('Failed to compile vertex shader');
}

在這裏的 GLSL 代碼中有一些須要提到的變量:

  1. 一個名爲 position屬性。屬性本質上是一個輸入,而且爲每一個這樣的輸入調用着色器。
  2. 一種稱爲 colorvarying。這既是頂點着色器的輸出(每一個頂點着色器都有一個),也是片斷着色器的輸入。值被傳遞到片斷着色器時,將根據柵格化的屬性對值進行插值計算。
  3. gl_Position 值。本質上是頂點着色器的輸出,如任何存在變化的值。這很特別,由於它用於肯定須要去繪製哪些像素。

還有一個稱爲 uniform 的變量類型,該變量類型在屢次調用頂點着色器時將會保持不變。這些 uniform 用於變換矩陣之類的屬性,對於單個幾何圖形上的頂點來講,它們都是恆定的。

接下來,咱們用片斷着色器執行相同的操做,將其編譯併發送到 GPU。注意,片斷着色器如今能夠讀取頂點着色器中的 color 變量。

const sourceF = `
  precision mediump float;
  varying vec4 color;

  void main() {
    gl_FragColor = color;
  }
`;

const shaderF = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shaderF, sourceF);
gl.compileShader(shaderF);

if (!gl.getShaderParameter(shaderF, gl.COMPILE_STATUS)) {
  console.error(gl.getShaderInfoLog(shaderF));
  throw new Error('Failed to compile fragment shader');
}

最後,頂點着色器和片斷着色器都被連接到單個 OpenGL 程序中。

const program = gl.createProgram();
gl.attachShader(program, shaderV);
gl.attachShader(program, shaderF);
gl.linkProgram(program);

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  console.error(gl.getProgramInfoLog(program));
  throw new Error('Failed to link program');
}

gl.useProgram(program);

咱們告訴 GPU,上面所定義的着色器就是咱們要運行的着色器。因此剩下事情的就是建立輸入,並讓 GPU 在這些輸入上進行運算。

將輸入數據發送到 GPU

輸入的數據將會存儲在 GPU 的內存中,並從那裏進行處理。與其對每一個輸入進行單獨的繪製調用(一次僅傳輸一個相關數據),不如將整個輸入傳輸到 GPU 並從那裏讀取。 (傳統 OpenGL 一次只能傳輸一份數據,從而致使性能降低。)

OpenGL 提供了一種被稱爲「頂點緩衝對象」(VBO)的抽象。我仍在試圖徹底弄清楚它的工做原理,可是最終,咱們將會使用抽象來進行如下操做:

  1. 將一系列字節存儲在 CPU 的內存中。
  2. 用經過 gl.createBuffe() 建立的惟一緩衝區和 gl.ARRAY_BUFFER 的綁定點(binding point)將字節傳輸到 GPU 的內存。

儘管在頂點着色器中每一個輸入變量(屬性)都有一個 VBO,但也能夠把一個 VBO 用於多個輸入。

const positionsData = new Float32Array([
  -0.75, -0.65, -1,
   0.75, -0.65, -1,
   0   ,  0.65, -1,
]);

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positionsData, gl.STATIC_DRAW);

一般你將會用對程序有意義的任何座標來指定幾何圖形,而後在頂點着色器中使用一系列轉換將它們轉換爲 OpenGL 的「剪輯空間(clip space)」。我不會介紹剪輯空間的詳細信息(它們與同構座標有關),可是如今,X 和Y 在 -1+1 之間變化。因爲頂點着色器僅按原樣傳遞輸入數據,所以能夠直接在剪輯空間中指定座標。

接下來,咱們還會把緩衝區與頂點着色器中的變量之一相關聯:

  1. 從上面建立的程序中獲取 position 變量的句柄。
  2. 告訴 OpenGL 從 gl.ARRAY_BUFFER 綁定點讀取數據,每批 3 個,其特殊參數如 offsetstride 爲零。
const attribute = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(attribute);
gl.vertexAttribPointer(attribute, 3, gl.FLOAT, false, 0, 0);

請注意,咱們能夠建立 VBO 並將其與「頂點着色器」屬性相關聯,由於要一個接一個地作。若是咱們將這兩個功能分開(例如一次性建立全部 VBO,而後將它們與各個屬性相關聯),則須要在將每一個 VBO 與對應的屬性相關聯以前調用 gl.bindBuffer(...)

繪製!

最後,按照咱們想要的方式設置 GPU 內存中的全部數據,咱們能夠告訴 OpenGL 清除屏幕並在設置的陣列上運行程序。做爲柵格化的一部分(肯定哪些像素被頂點覆蓋),咱們告訴 OpenGL 將 3 個一組的頂點視爲三角形。

gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);

以線性方式進行設置確實意味着能夠一次就能使程序運行。在任何實際的應用中,咱們都會以結構化的方式存儲數據,在數據發生變化時將其發送到 GPU,並在每一幀進行繪製。


將全部內容放在一塊兒,下圖顯示了在屏幕上顯示第一個三角形的最小概念集。即便這樣,該圖仍是被大大簡化了,因此你最好配合本文所介紹的 75 行代碼放在一塊兒進行研究。

image.png

完整的處理流程:首先建立着色器,經過 VBO 將數據傳輸到 GPU,把二者關聯在一塊兒,而後 GPU 在再將全部內容組裝成最終的圖像。

最後的步驟,儘管通過了簡化,但完整描述了三角形所需的步驟順序

對我而言,學習 OpenGL 的難點在於得到屏幕上最基本圖像所需的大量模板。因爲柵格化框架要求咱們提供 3D 渲染功能,而且與 GPU 的通訊很是冗長,因此有不少概念須要預先學習。我但願本文所展現的基礎知識比其餘教程更簡單!

前端刷題神器

掃碼進入前端面試星球🌍,解鎖刷題神器,還能夠獲取800+道前端面試題一線常見面試高頻考點

173382ede7319973.gif


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索