GLSL 著色器

著色器使用 GLSL(OpenGL 著色語言),這是一種語法類似於 C 的特殊 OpenGL 著色語言。GLSL 由圖形管線直接執行。有幾種型別的著色器,但兩種常用於在 Web 上建立圖形:頂點著色器和片段(畫素)著色器。頂點著色器將形狀位置轉換為 3D 繪製座標。片段著色器計算形狀顏色和其他屬性的渲染。

GLSL 不如 JavaScript 直觀。GLSL 是強型別的,並且涉及大量的向量和矩陣數學。它會很快變得非常複雜。在本文中,我們將透過一個簡單的程式碼示例來渲染一個立方體。為了加速背景程式碼,我們將使用 Three.js API。

正如您可能還記得從基礎理論文章中,頂點是 3D 座標系統中的一個點。頂點可以,而且通常也具有附加屬性。3D 座標系統定義了空間,而頂點有助於在該空間中定義形狀。

著色器型別

著色器本質上是一個在螢幕上繪製東西所需的功能。著色器執行在GPU(圖形處理單元)上,GPU 針對此類操作進行了最佳化。使用 GPU 處理著色器可以將一些計算任務從 CPU 解除安裝。這使得 CPU 可以將其處理能力集中用於其他任務,例如執行程式碼。

頂點著色器

頂點著色器操作 3D 空間中的座標,並且每個頂點呼叫一次。頂點著色器的目的是設定 gl_Position 變數 — 這是一個特殊的、全域性的、內建的 GLSL 變數。gl_Position 用於儲存當前頂點的座標。

void main() 函式是定義 gl_Position 變數的標準方式。void main() 中的一切都將由頂點著色器執行。頂點著色器會生成一個變數,其中包含如何在 3D 空間中將頂點的座標投影到 2D 螢幕上的資訊。

片段著色器

片段(或紋理)著色器為正在處理的每個畫素定義 RGBA(紅、綠、藍、Alpha)顏色 — 每個畫素呼叫一次片段著色器。片段著色器的目的是設定 gl_FragColor 變數。gl_FragColor 是一個內建的 GLSL 變數,類似於 gl_Position

計算結果將是一個包含 RGBA 顏色資訊的變數。

演示

讓我們來構建一個簡單的演示來解釋這些著色器的實際應用。請務必先閱讀Three.js 教程,以掌握場景、其物件和材質的概念。

注意:請記住,您不必使用 Three.js 或任何其他庫來編寫您的著色器 — 原生的 WebGL(Web 圖形庫)就足夠了。我們在這裡使用 Three.js 是為了使背景程式碼更簡單、更易於理解,這樣您就可以專注於著色器程式碼。Three.js 和其他 3D 庫為您抽象了很多東西 — 如果您想在原始 WebGL 中建立這樣的示例,您需要編寫大量額外的程式碼才能使其正常工作。

環境設定

要開始使用 WebGL 著色器,請按照使用 Three.js 構建基本演示中描述的環境設定步驟進行操作,以確保 Three.js 按預期工作。

HTML 結構

這是我們將要使用的 HTML 結構。

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>MDN Games: Shaders demo</title>
    <style>
      html,
      body,
      canvas {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100%;
        font-size: 0;
      }
    </style>
    <script src="three.min.js"></script>
  </head>
  <body>
    <script id="vertexShader" type="x-shader/x-vertex">
      // vertex shader's code goes here
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      // fragment shader's code goes here
    </script>
    <script>
      // scene setup goes here
    </script>
  </body>
</html>

它包含一些基本資訊,例如文件的<title>,以及一些 CSS 來設定 Three.js 將在頁面中插入的<canvas> 元素的 widthheight,使其全屏顯示。<head> 中的<script> 元素將 Three.js 庫包含到頁面中;我們將在 <body> 標籤中將程式碼寫入三個 script 標籤。

  1. 第一個將包含頂點著色器。
  2. 第二個將包含片段著色器。
  3. 第三個將包含生成場景的實際 JavaScript 程式碼。

在繼續閱讀之前,請將此程式碼複製到一個新的文字檔案中,並將其儲存在您的工作目錄中,檔名為 index.html。我們將在此檔案中建立一個展示簡單立方體的場景,以解釋著色器的工作原理。

立方體的原始碼

我們不必從頭開始建立所有內容,而是可以重用使用 Three.js 構建基本演示中的立方體原始碼。大多陣列件,如渲染器、相機和燈光將保持不變,但我們將使用著色器來設定立方體的顏色和位置,而不是基本材質。

轉到 GitHub 上的cube.html 檔案,複製第二個<script> 元素內的所有 JavaScript 程式碼,然後貼上到當前示例的第三個 <script> 元素中。儲存並使用瀏覽器載入 index.html — 您應該會看到一個藍色的立方體。

頂點著色器程式碼

讓我們繼續編寫一個簡單的頂點著色器 — 將下面的程式碼新增到 body 的第一個 <script> 標籤中

glsl
void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x+10.0, position.y, position.z+5.0, 1.0);
}

計算出的 gl_Position 是透過將模型檢視和投影矩陣分別乘以每個向量來獲得的,以獲得每個向量的最終頂點座標。

注意:您可以從頂點處理段落中瞭解更多關於模型檢視投影變換的資訊,您也可以檢視本文末尾的連結以瞭解更多相關資訊。

projectionMatrixmodelViewMatrix 均由 Three.js 提供,向量透過新的 3D 位置傳遞,這會導致原始立方體沿著 x 軸移動 10 個單位,沿著 z 軸移動 5 個單位,透過著色器進行轉換。我們可以忽略第四個引數,並將其保留為預設的 1.0 值;它用於操縱 3D 空間中頂點位置的裁剪,但在本例中我們不需要。

紋理著色器程式碼

現在我們將紋理著色器新增到程式碼中 — 將下面的程式碼新增到 body 的第二個 <script> 標籤中

glsl
void main() {
  gl_FragColor = vec4(0.0, 0.58, 0.86, 1.0);
}

這將設定一個 RGBA 顏色來重現當前淺藍色 — 前三個浮點值(範圍從 0.01.0)代表紅色、綠色和藍色通道,而第四個值是 Alpha 透明度(範圍從 0.0 — 完全透明 — 到 1.0 — 完全不透明)。

應用著色器

要將新建立的著色器實際應用到立方體上,請先註釋掉 basicMaterial 的定義

js
// const basicMaterial = new THREE.MeshBasicMaterial({color: 0x0095DD});

然後,建立shaderMaterial

js
const shaderMaterial = new THREE.ShaderMaterial({
  vertexShader: document.getElementById("vertexShader").textContent,
  fragmentShader: document.getElementById("fragmentShader").textContent,
});

此著色器材質從指令碼中獲取程式碼,並將其應用於分配給它的物件。

然後,在定義立方體的行中,我們需要將 basicMaterial 替換為新建立的 shaderMaterial

js
// const cube = new THREE.Mesh(boxGeometry, basicMaterial);
const cube = new THREE.Mesh(boxGeometry, shaderMaterial);

Three.js 會編譯並執行附加到分配了此材質的網格的著色器。在我們的例子中,立方體將同時應用頂點和紋理著色器。就是這樣 — 您剛剛建立了最簡單的著色器,恭喜您!立方體應該看起來像這樣

Three.js blue cube demo

它看起來與 Three.js 立方體演示完全相同,但稍微不同的位置和相同的藍色都是透過著色器實現的。

最終程式碼

HTML

html
<script src="https://end3r.github.io/MDN-Games-3D/Shaders/js/three.min.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
  void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x+10.0, position.y, position.z+5.0, 1.0);
  }
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
  void main() {
      gl_FragColor = vec4(0.0, 0.58, 0.86, 1.0);
  }
</script>

JavaScript

js
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(WIDTH, HEIGHT);
renderer.setClearColor(0xdddddd, 1);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(70, WIDTH / HEIGHT);
camera.position.z = 50;
scene.add(camera);

const boxGeometry = new THREE.BoxGeometry(10, 10, 10);

const shaderMaterial = new THREE.ShaderMaterial({
  vertexShader: document.getElementById("vertexShader").textContent,
  fragmentShader: document.getElementById("fragmentShader").textContent,
});

const cube = new THREE.Mesh(boxGeometry, shaderMaterial);
scene.add(cube);
cube.rotation.set(0.4, 0.2, 0);

function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}
render();

CSS

css
body {
  margin: 0;
  padding: 0;
  font-size: 0;
}
canvas {
  width: 100%;
  height: 100%;
}

結果

總結

本文教授了著色器的基礎知識。我們的示例功能不多,但您可以使用著色器做許多很酷的事情 — 檢視 ShaderToy 上一些非常酷的示例以獲取靈感並從其原始碼中學習。

另見