iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
Modern Web

p5.js 的環形藝術系列 第 24

[Day 24] glsl 基礎教學(三) –– 著色器原理(二)

  • 分享至 

  • xImage
  •  

今天繼續講解關於 glsl 的基礎語法。

範例程式

  • 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.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
    }
}

變數型態

glsl 需要編譯並運行在 gpu 上面,因此必須是低階語言,在宣告變數的時候只要指定變數的型態。

glsl 支持的基本變數型態有:

变量类型 描述 示例
float 浮點數 float a = 0.5;
int 整數 int i = 10;
uint 無符號整數 uint u = 10u;
bool 布林值(true 或 false) bool b = true;
vec2 2D 向量,包含兩個 float vec2 v = vec2(0.5);
vec3 3D 向量,包含三個 float vec3 v = vec3(1.0);
vec4 4D 向量,包含四個 float vec4 v = vec4(1.0);

glsl 還支援其他的變數型態,比如說矩陣(ex. mat4)或是紋理採樣器(ex. sampler2D),但因為這系列教學不會使用到,所以就先略過。

glsl 和其他靜態語言比較不一樣的是它支援向量型態 vec* 的變數,像是 vec3 就是擁有三個 float 分量的向量型態,那我要如何取得 vec* 的分量呢?

通常 vec3 向量可以用來表示三維空間中的座標點,因此可以用 x, y, z 來分別表示三個分量:

vec3 vect = vec3(0.1f, 0.2f, 0.3f);
float x = vect.x; // 取第一個分量
float y = vect.y; // 取第二個分量
float z = vect.z; // 取第三個分量
vec2 xy = vect.xy; // 取第一個分量和第二個分量組成 2D 向量
vec2 xz = vect.xz; // 取第一個分量和第三個分量組成 2D 向量
vec3 xxx = vect.xxx; // 第一個分量取三次組成 3D 向量
vec3 xyx = vect.xyx; // 取 xyx 組成 3D 向量

頂點著色器

當 p5.js 的圖案頂點數據傳入到 shader.vert 經過處理,就會映射到所謂的標準化設備座標(Normalized Device Coordinates, NDC),這個座標系統,只有 x 和 y 座標在 -1 ~ 1 之間的二維空間才會被顯示在螢幕上,所有在這個範圍之外的部分,都會被 glsl 丟棄。

Imgur

現在我們來好好討論 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;
}
  • #version 300 es

這一行指定了 GLSL 的版本,版本決定了整個程式要如何進行編譯,因此必須放在第一行。

300 es 代表這是 WebGL 2.0 的 GLSL ES 3.0 版本。

其中 es 代表 Embedded Systems,表示這是針對 WebGL 的 GLSL 版本,與桌面版 OpenGL 的 GLSL 版本不同。

  • precision highp float;

這一行指定 float 型態的精度,highp 是最高精度,可以用於位置等需要高精度的計算。

  • in vec3 aPosition;

in 用於宣告一個輸入變數,這個輸入代表從 p5.js 那邊傳來的圖案頂點數據,在這個例子中,會接收到的頂點數據只有 rect(0,0,width, height); 的四個頂點。

  • void main() {

就跟 p5.js 的程式需要 setupdraw 函數,main 函數是著色器必須的入口函數,所有著色器的功能都是從 main 函數的執行為起始。

  • vec4 positionVec4 = vec4(aPosition, 1.0);

aPosition 為頂點數據的三維座標,這行程式多加了一個 w 分量 1.0,儲存在四維向量 positionVec4 裡面。

這個 w 分量的用途為透視投影,這是比較困難的部分,所以這系列的教學沒有詳細說明。

但可以將 w 分量看作該物體和螢幕視角的距離,我們可以將螢幕視角理解為一個相機,物體和相機在這個 世界空間 的遠近,決定了我們在螢幕上看到這個物體的大小。

但這系列教學不會處理到螢幕視角的部分,所以統一都將 w 分量設為 1.0 就行了。

  • positionVec4.xy = positionVec4.xy * 2.0 - 1.0;

我們需要對坐標系的轉換進行詳細的說明,才能知道這一行轉換的用意。

現在只需要知道傳入 aPosition 的四個頂點座標分別為:

左上角:(0.0, 0.0, 0)
右上角:(1.0, 0.0, 0)
左下角:(0.0, 1.0, 0)
右下角:(1.0, 1.0, 0)

奇怪的是根據 p5.js 的程式 createCanvas(600, 600, WEBGL);rect(0,0,width, height);,傳入頂點著色器的座標數據應該要是:

左上角:(0.0, 0.0, 0)
右上角:(600.0, 0.0, 0)
左下角:(0.0, 600.0, 0)
右下角:(600.0, 600.0, 0)

這和 glsl 如何看待整個系統的座標系有關,這會留在下面再詳細說明。

  • gl_Position = positionVec4;

gl_Position 是 glsl 的內建變數,是一個四維向量,用來代表頂點的最終位置,最後此數據會傳遞到光柵化的流程中。

GLSL 的座標系統

WEBGL 模式的座標系統

首先要從 p5.js 的 WEBGL 模式講起,在最開始的 p5.js 基礎教學(一) –– 座標與旋轉圖案 就有提到,預設模式下的 p5.js 座標原點在畫布的左上角:

Imgur

但 WEBGL 模式卻不太一樣,他的座標原點座落在畫布的正中央(其實這樣還比較直覺一點),但同樣的都是 x 軸正向為由左到右,而 y 軸正向為由上到下。

因此當我下了指令 createCanvas(100, 100, WEBGL),這個畫布的的原點和邊界就圖同下方所示:

Imgur

GLSL 的座標系統

glsl 的座標系統就比較複雜了,因為要處理三維空間的物件,並且需要根據觀察視角來投影到二維的螢幕上,這過程中會經歷很多層的座標轉換:

  1. 物體模型座標 Object or model coordinates
  2. 世界空間座標 World coordinates
  3. 相機座標 Eye or camera coordinates
  4. 裁剪座標 Clip coordinates
  5. 標準設備座標 Normalized device coordinates
  6. 螢幕座標 Windows coordinates

看起來讓人眼花暸亂,但這系列教學講解的範例不會經過這麼多層的座標轉換,而是直接跳過某些座標系,因為我們不需要在那些座標空間中對物體進行操作。

#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.vert 中, aPosition 接收到的資料就是物體模型座標系之下的頂點座標,所以從 rect(0,0,width, height); 接收過來的數據才會是:

左上角:(0.0, 0.0, 0)
右上角:(1.0, 0.0, 0)
左下角:(0.0, 1.0, 0)
右下角:(1.0, 1.0, 0)

因為物體模型座標只描述頂點之間的方向跟相對距離,這些頂點是相對於物體自身的原點而定義的,如果要觀察頂點的實際距離,就必須轉換到世界空間才行。

#version 300 es

precision highp float;

in vec3 aPosition;
uniform mat4 uModelViewMatrix;

void main() {
    vec4 positionVec4 = vec4(aPosition, 1.0);

    // 用 uModelViewMatrix 轉成世界空間座標
    vec4 worldPosition = uModelViewMatrix * positionVec4;

    // worldPosition 分別接收四個座標
    // 左上角:(0.0, 0.0, 0)
    // 右上角:(600.0, 0.0, 0)
    // 左下角:(0.0, 600.0, 0)
    // 右下角:(600.0, 600.0, 0)
    positionVec4.xy = worldPosition.xy * 1.0 / 300.0 - 1.0;
    gl_Position = positionVec4;
}

shader.vert 中使用內建的 uModelViewMatrix 可以將頂點座標轉換到世界空間。

接著另一個要提到的變數是 gl_Position,他是 shader.vert 的輸出,也就是頂點最終所在的位置。

被頂點著色器處理過的頂點會被映射到標準設備座標上,也就是說 gl_Position 就是在標準設備座標上的頂點。

標準設備座標是一個被裁切的三維空間,只有在 x, y, z 座標都在 -1 ~ 1 之間的空間才被視為是有效的,其他在範圍之外的座標都會被丟棄,不會傳入到後續的片段著色器中。

所以頂點著色器會經歷以下座標系的轉換:

  1. 物體模型座標 Object or model coordinates
  2. 世界空間座標 World coordinates
  3. 相機座標 Eye or camera coordinates
  4. 裁剪座標 Clip coordinates
  5. 標準設備座標 Normalized device coordinates

前一個座標系轉換到下一個座標系,都會有一個對應的內建矩陣可以做轉換,但目前我們只會實作二維空間的視覺效果,所以只需要將 rect(0,0,width, height); 這個長方形,完整布滿整個畫布,然後用片段著色器作為我們主要的創作工具就行了。

因為 rect 不論邊長還是位置,他投射到的物體模型空間座標都是:

左上角:(0.0, 0.0, 0)
右上角:(1.0, 0.0, 0)
左下角:(0.0, 1.0, 0)
右下角:(1.0, 1.0, 0)

且在標準設備座標上,整個畫布的四個頂點皆為:

左上角:(-1.0, -1.0)
右上角:(1.0, -1.0)
左下角:(-1.0, 1.0)
右下角:(1.0, 1.0)

因此常會看到 glsl 平面作品的頂點著色器都會有以下指令:

positionVec4.xy = positionVec4.xy * 2.0 - 1.0;

為的就是讓 rect 的範圍藉由頂點著色器鋪滿整個畫布。

片段著色器

前一篇 glsl 基礎教學(二) –– 著色器原理(一) 和程式註解已經提供了功能面的解說,本篇會再針對語法層面做補充:

  • #version 300 esprecision highp float;

頂點著色器也有一樣的程式,用來指定編譯版本為 WebGL 2.0 的 GLSL ES 3.0 版本,且浮點數精度設為高精度。

  • #define PI 3.14159265358979323846

因為 glsl 並沒有內建變數儲存圓周率的值,所以我們要顯式指定圓周率 PI

  • uniform vec2 u_resolution;

uniform 關鍵字用來接受外部指定的參數,也就是說片段著色器不只接收光柵化流程傳來的片段數據(片段數據存在變數 gl_FragCoord),也能從 p5.js 接收額外的參數。

在範例中的 uniform 變數有:

  • u_resolution 用來標準化 gl_FragCoord 的 xy 數據
  • u_diameter 用來指定圓圈的直徑
  • u_edge_width 用來指定圓圈輪廓的寬度

在 p5.js 我們可以用 rectShader.setUniform({變數名稱}, {傳入變數}) 來指定變數。

這裡要特別提到 u_resolution 的用途,u_resolution 是大部分 glsl 作品都會傳入的 uniform 參數,用來標準化 gl_FragCoord 的長寬單位。

前面有提到頂點著色器的 gl_Position 輸出位在標準設備座標上(xy 座標皆在 -1 ~ 1 之間),但經過光柵化的片段著色器輸入 gl_FragCoord 已經被轉換到螢幕座標上 (x 座標介在 0 ~ width 之間,y 座標介在 0 ~ height 之間),相當於當初在 p5.js 創建的畫布大小。

但我們沒辦法在片段著色器知道 p5.js 創建的畫布大小,所以從外部傳入畫布尺寸 [width, height] 進入變數 u_resolution,並執行 vec2 uv = gl_FragCoord.xy / u_resolution;,不管 heightwidth 的數值為何,uv 的 xy 座標皆在 0 ~ 1 之間。

也就是說我們把整個畫布的範圍映射到 (0, 0)(0, 1)(1, 0)(1, 1) 所框起來座標空間中,確保片段著色器在一個統一的座標空間中計算每個像素該有的顏色。

近期三章已經初步講解完 glsl 渲染的整個流程,以及程式之間的關係,本系列教學後期會著重在片段著色器上各種視覺效果的數學技巧,等待明天繼續講解。

參考資料

https://flyhead.medium.com/%E7%82%BA%E4%BB%80%E9%BA%BC%E9%9C%80%E8%A6%81%E9%BD%8A%E6%AC%A1%E5%BA%A7%E6%A8%99-homogeneous-coordinate-bd86356f67b1
https://blog.csdn.net/aoxuestudy/article/details/109923749
https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/08%20Coordinate%20Systems/


上一篇
[Day 23] glsl 基礎教學(二) –– 著色器原理(一)
下一篇
[Day 25] glsl 基礎教學(四) –– 繪製線條
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言