今天繼續講解關於 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 丟棄。
現在我們來好好討論 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 的程式需要 setup
和 draw
函數,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 的內建變數,是一個四維向量,用來代表頂點的最終位置,最後此數據會傳遞到光柵化的流程中。
首先要從 p5.js 的 WEBGL 模式講起,在最開始的 p5.js 基礎教學(一) –– 座標與旋轉圖案 就有提到,預設模式下的 p5.js 座標原點在畫布的左上角:
但 WEBGL 模式卻不太一樣,他的座標原點座落在畫布的正中央(其實這樣還比較直覺一點),但同樣的都是 x 軸正向為由左到右,而 y 軸正向為由上到下。
因此當我下了指令 createCanvas(100, 100, WEBGL)
,這個畫布的的原點和邊界就圖同下方所示:
glsl 的座標系統就比較複雜了,因為要處理三維空間的物件,並且需要根據觀察視角來投影到二維的螢幕上,這過程中會經歷很多層的座標轉換:
Object or model coordinates
World coordinates
Eye or camera coordinates
Clip coordinates
Normalized device coordinates
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
之間的空間才被視為是有效的,其他在範圍之外的座標都會被丟棄,不會傳入到後續的片段著色器中。
所以頂點著色器會經歷以下座標系的轉換:
Object or model coordinates
World coordinates
Eye or camera coordinates
Clip coordinates
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 es
、precision 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;
,不管 height
和 width
的數值為何,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/