上一個單元 glsl 基礎教學(一) –– glsl 和 p5.js 的差異 講到了新的程式語言 glsl,可以用來製作更精細的視覺效果,也稍微比較了它和 p5.js 之間的寫法差異。

今天要更進一步介紹 glsl 的繪製原理和語法細節,接下來的內容會開始變比較複雜,但我會盡可能寫的簡單好懂,非常感謝大家持續的觀看!

p5.js 的 WEBGL 模式

雖然 glsl 是另一個編譯在 gpu 上面的程式語言,但 p5.js 有提供 loadShadercreateShader 等等接口可以直接傳入 glsl code 進行編譯,所以我們一樣能藉由 p5.js 這個框架來寫 glsl。

如果要讓 glsl 在 p5.js 上面運行,首先我們要啟動 p5.js 的 WEBGL 模式。

p5.js 的 WEBGL 模式可以用來進行三維圖像的創建以及操作,比如說 box()sphere() 等等三維圖元的函數可以在這個模式下使用,並且能用硬體加速來提高渲染的效能,而 glsl 同樣的也是以三維座標系為基礎的程式語言,所以 p5.js 必須開啟 WEBGL 模式才能結合 glsl 的編譯結果。

要開啟 WEBGL 模式很簡單,只需要在 createCanvas 的第三個參數指定就行了:

function setup() {
  createCanvas(400, 400, WEBGL);
}

function draw() { background(200); rotateX(frameCount * 0.01); rotateY(frameCount * 0.01); box(100); }

Imgur

看起來是不是非常酷炫,開啟了 WEBGL 我們就能從二維圖像跳脫出來,進入到三維視角世界,p5.js 的創作方向又變的寬廣許多。

雖然在這系列不會涉及到以下功能,但 WEBGL 模式還有以下額外功能,讀者可以自己去探索:

光照和材質:p5.js 的 WebGL 模式支持基本的光照效果(如 ambientLight()directionalLight())和材質應用(如 texture()normalMaterial()),這讓你可以模擬真實的光影效果。

相機控制:WebGL 模式提供了簡單的相機控制,你可以使用 camera()perspective() 等函數來設置視角和投影方式,並使用 orbitControl() 等函數來實現基本的相機交互。

前置設定

在我們開始寫 glsl 之前,要在 open processing 介面做一些前置設定,但這不是必要的,只是作者本人的寫法需要做這樣的設定。

打開一個全新的 sketch 編輯頁面,然後在界面打開側邊欄。

Imgur

側邊欄會看到 mode 的選項,有 P5.jsHTML/CSS/JSPjs,請選擇 HTML/CSS/JS

Imgur

接著會看到多出了兩個檔案 style.cssindex.html,但這兩個檔案我們不會去動它。

前置設定就只有這樣,非常的簡單。

寫入範例程式

接下來要寫入待會要講解的範例程式,首先要把 mySketch.js 改成:

let rectShader; 
  
function preload(){   
  rectShader = loadShader('shader.vert', 'shader.frag'); 
} 
  
function setup() { 
  pixelDensity(1);
  createCanvas(600, 600, WEBGL); 
  noStroke();    
}
  
function draw() {   
  shader(rectShader); 
  rectShader.setUniform('u_resolution', [width, height]); 
  rectShader.setUniform('u_diameter', 25.0);
  rectShader.setUniform('u_edge_width', 1.0); 
  rect(0,0,width, height); 
}

然後再新增兩個檔案 shader.vertshader.frag

shader.vert

#version 300 es

precision highp float;

in vec3 aPosition;

void main() { vec4 positionVec4 = vec4(aPosition, 1.0); positionVec4.xy = positionVec4.xy * 2.0 - 1.0; gl_Position = positionVec4; }

shader.frag

#version 300 es

// 我們可以把這整個檔案當成一個 glsl_circle 函數的內容

precision highp float;

// 下面這三個是 glsl_circle 函數的參數 // u_resolution 和 glsl_circle 函數的功能本身並不相關,先不說明 // u_diameter 就是直徑長度 // u_edge_width 就是 glsl_circle 函數的圓邊框寬度,可以認定為 p5.js 的 strokeWeight uniform vec2 u_resolution; uniform float u_diameter; uniform float u_edge_width;

// FragColor 為片段著色器的輸出,用來決定目標像素最終的顏色 out vec4 FragColor;

#define PI 3.14159265358979323846

void main() { // glsl_circle 函數還有一個參數是目標像素點的位置座標 gl_FragCoord.xy // 原本畫布的大小為 600x600,但從 glsl 通常會將畫布大小視為 1x1 // 所以每個傳入的參數都要用 u_resolution 調整比例 vec2 uv = gl_FragCoord.xy / u_resolution; float radius = u_diameter / 2.0 / u_resolution.x; float edge = u_edge_width / u_resolution.x;

// 因為畫布大小為 1x1,所以 (0.5, 0.5) 為畫布中心點 vec2 circleCenter = vec2(0.5, 0.5);

// 計算目標像素點與中心點距離 float dist = distance(uv, circleCenter);

// 根據目標像素點與中心點距離大小,填上不同顏色 // gl_FragColor 代表最後目標像素點的顏色 if (dist < radius - edge) { FragColor = vec4(1.0, 1.0, 1.0, 1.0); // White } else if (dist < radius) { FragColor = vec4(0.0, 0.0, 0.0, 1.0); // Black } else { FragColor = vec4(200.0 / 255.0, 200.0 / 255.0, 200.0 / 255.0, 1.0); // Gray } }

以下是 p5.js 和 glsl 執行的結果:

Imgur

著色器(shader)解說

如果要在 open processing 平台上寫 glsl 除了 mySketch.js,還要多寫兩個檔案 shader.vertshader.frag,這兩個分別代表 頂點著色器片段著色器 上面運行的程式。

頂點著色器片段著色器 就是在整個渲染過程的其中兩個步驟,在 glsl 處理的整段渲染過程,就是把程式邏輯中的三維空間和三維圖像,轉變為螢幕上顯示的二維投影。

這裡參考 LearnOpenGL-CN 的 glsl 教學,我們在程式中描述要渲染的圖像,這些圖像的頂點數據會進入到 OpenGL 的圖形渲染管線,最後轉變為二維的有色像素矩陣顯示在螢幕上,這段渲染過程可區分為六個階段:

頂點著色器(vertex shader)

  • 圖元裝配(shape assembly)
  • 幾何著色器(geometry shader)
  • 光柵化(rasterization)

片段著色器(fragment shader)

  • 測試與混合(tests and blending)

整個圖案渲染過程其實非常複雜,但通常在寫 glsl 的時候,我們會配置的部分就只有頂點著色器片段著色器

頂點著色器的用意就是將程式邏輯中的圖像頂點座標數據,映射到另外一個座標系,為什麼要這樣做呢?比如說在 p5.js 呼叫了三維圖像函數 box(100)(邊長為 100 px 的立方體),頂點著色器的程式 shader.vert 可以將這立方體的八個頂點映射到一個被稱作 世界空間 的座標系統中,然後再投影到顯示在螢幕上的 2D 空間。

也就是說,頂點著色器的程式 shader.vert 可以重新分配圖案的頂點在 世界空間 中的位置,並且決定我們要以怎樣的視角來觀看這個圖案。

片段著色器會接收光柵化之後產生的一系列片段(fragment)數據,每個片段代表某個渲染圖案上的一個像素位置,然後片段著色器會根據這個像素的各種訊息來生成該像素最後要顯示的顏色。

代表片段著色器的程式 shader.frag 也是我們進行 glsl 視覺藝術創作的主軸,因為我們要用顏色來呈現各種高級的視覺效果。

範例程式解說

回到我們的範例程式,先來看 Mysketch.js,以下列出了和 glsl 程式串接的重要部分:

rectShader = loadShader('shader.vert', 'shader.frag');

在最一開始程式先定義了 preload 函數:

let rectShader; 
  
function preload(){   
  rectShader = loadShader('shader.vert', 'shader.frag'); 
}

preload 函數會在 setup 函數之前執行,用來確認程式所需的外部資源都在開始渲染之前都 load 進來。

loadShader 函數用來載入 glsl 所必須的頂點著色器和片段著色器程式,將它存在變數 rectShader 裡面,用在操作圖案的 shader 渲染。

createCanvas(600, 600, WEBGL);

就如同最前面所說,要啟動 glsl 的 shader 渲染,必需開啟 p5.js 的 WEBGL 模式。

shader(rectShader);

啟動 shader 程式,確保之後建構出來的圖案,都會經過 shader 渲染,比如說最後的 rect(0,0,width, height);

rectShader.setUniform('u_resolution', [width, height]);

rectShader.setUniform 用來為 shader 程式傳入額外的參數,可以看一下所有呼叫到 rectShader.setUniform 的指令:

rectShader.setUniform('u_resolution', [width, height]); 
  rectShader.setUniform('u_diameter', 25.0);
  rectShader.setUniform('u_edge_width', 1.0);

glsl 基礎教學(一) –– glsl 和 p5.js 的差異 有提到,可以將 shader.frag 程式視為一個 glsl_circle 函數,且其傳入參數有三個:

u_diameter: 代表 circle 的直徑

u_edge_width: 代表 circle 邊線寬度

gl_FragCoord.xy: 代表目標像素點的位置

其中前面兩個參數 u_diameteru_edge_width 以及 u_resolution 就是用 p5.js 的 rectShader.setUniform 函數把參數傳進去的。

著色器的程式因為是類 C 語言,需要花更多篇幅說明,所以我們留到下一個單元繼續講解。

參考資料

https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/04%20Hello%20Triangle/