本篇開始講解一些使用在片段著色器的繪圖技巧,第一步就是學會如何繪製基本的函數曲線,經過本單元的講解,我們可以學會如何繪製以下的圖案:

程式基礎模板
因為是著重在片段著色器的繪圖技巧,所以本單元只會改動片段著色器 shader.frag 的程式,p5.js 主程式和頂點著色器 shader.vert 並不會做任何更動,以下是本單元的基礎模板:
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]);
rect(0,0,width, height);
}
shader.vert
#version 300 es
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
precision mediump float;
uniform vec2 u_resolution;
out vec4 fragColor;
void main() {
fragColor = vec4(vec3(0.8), 1.0);
}
根據這個程式的模板,你應該會看到一個灰色的畫布。
根據線條距離決定顏色
因為 glsl 是在像素層級上的運算,程式沒有辦法知道另一個像素的狀態,因此無法以一個畫布做為整體進行繪製。
假設要在畫布上繪製一個 f(x) = x^5 函數,比較簡單的方法是,計算目前片段(也就是目標像素點)的座標點距離 f(x) = x^5 這個函數有多近,來決定要繪製畫布背景色,還是線條的顏色:
#version 300 es
precision mediump float;
uniform vec2 u_resolution;
out vec4 fragColor;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
float y = pow(st.x, 5.0);
if (abs(st.y - y) genType smoothstep(genType edge0, genType edge1, genType x);
所謂 genType 代表 smoothstep 支援多種型態,包括 float、vec2、vec3、vec4,但現在只會用到 float,所以:
> float smoothstep(float edge0, float edge1, float x);
在數學上的 smoothstep 函數,是以 0 和 1 作為邊界,但 glsl 的 smoothstep 函數可以自己設定邊界,比如說 smoothstep(2, 4, x) 的函數圖像化:
又或是反向的邊界 smoothstep(2, 1, x):
通常我們會用這樣的組合函數 smooth(0.5, 1, x) - smooth(1, 1.5, x) 來柔化線條的顆粒感:
越靠近中間點,線條主色就越強,越偏離中心點,背景色就會越強,所以用這樣的思維去更改程式:
#version 300 es
precision mediump float;
uniform vec2 u_resolution;
out vec4 fragColor;
float get_smooth_ratio(float width, float bias) {
return smoothstep(-width, 0.0, bias) – smoothstep(0.0, width, bias);
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
float y = pow(st.x, 5.0);
float smooth_ratio = get_smooth_ratio(0.01, y-st.y);
vec3 c = smooth_ratio * vec3(0.0, 1.0, 0.0) +
(1.0-smooth_ratio) * vec3(0.0);
fragColor = vec4(c, 1.0);
}
現在我們已經成功的柔化了線條邊界,看不到原先的顆粒感了,雖然還是有線條粗度的問題,主要是比對 y 座標的距離,而不是像素點針對該曲線的真正最短路徑,但因為這是一個很複雜的數學問題,無法在這系列的教學中完全解決,我們只能關注其他目前技術可以克服的部分。
繪製座標網格
掌握繪製線條的方法之後,我們接下來嘗試在畫布上繪製座標網格:
#version 300 es
precision mediump float;
uniform vec2 u_resolution;
uniform float u_grid_unit;
out vec4 fragColor;
float get_smooth_ratio(float width, float bias) {
return smoothstep(-width, 0.0, bias) – smoothstep(0.0, width, bias);
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
vec3 c = vec3(0.0);
for (float i = 0.0; i < gl_FragCoord.x/u_resolution.x; i += u_grid_unit/u_resolution.x) { // 畫直線 float x = i; // 畫出 x = i 的函數線 float smooth_ratio = get_smooth_ratio(0.005, x-st.x); c = smooth_ratio * vec3(0.2, 0.2, 0.2) + (1.0-smooth_ratio) * c; }
for (float i = 0.0; i < gl_FragCoord.y/u_resolution.y; i += u_grid_unit/u_resolution.y) { // 畫直線 float y = i; // 畫出 y = i 的函數線 float smooth_ratio = get_smooth_ratio(0.005, y-st.y); c = smooth_ratio * vec3(0.2, 0.2, 0.2) + (1.0-smooth_ratio) * c; }
float y = pow(st.x, 5.0); // 畫出 f(x) = x^5 的函數線
float smooth_ratio = get_smooth_ratio(0.01, y-st.y);
c = smooth_ratio * vec3(0.0, 1.0, 0.0) +
(1.0-smooth_ratio) * c;
fragColor = vec4(c, 1.0);
}
在程式中我另外加了一個 uniform 變數 u_grid_unit,所以在 MySketch.js 我加入 rectShader.setUniform('u_grid_unit', 30); 代表一個座標格的長度單位為 30 pixel。
可以注意到直線和橫線分別用不一樣的式子(x-st.x、y-st.y)來作為 get_smooth_ratio 的 bias 參數,主因是參考點的不同,因為畫直線的時候是要參考目標像素點的 x 座標和 x = i 的距離,畫橫線的時候則是要參考目標像素點的 y 座標和 y = i 的距離,用文字可能還是難以描述,讀者需要自行領略其中差別 XD。
繪製圓形
接下來我們要再畫一個圓形,這個圓形的 bias 參考點不打算用 x 座標和 y 座標來計算,而是用極座標的 r,也就是說我們要以目標像素點和圓心的距離作為 bias 參考點。
#version 300 es
precision mediump float;
uniform vec2 u_resolution;
uniform float u_grid_unit;
out vec4 fragColor;
float get_smooth_ratio(float width, float bias) {
return smoothstep(-width, 0.0, bias) – smoothstep(0.0, width, bias);
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
vec3 c = vec3(0.0);
for (float i = 0.0; i < gl_FragCoord.x/u_resolution.x; i += u_grid_unit/u_resolution.x) { // 畫直線 float x = i; // 畫出 x = i 的函數線 float smooth_ratio = get_smooth_ratio(0.005, x-st.x); c = smooth_ratio * vec3(0.2, 0.2, 0.2) + (1.0-smooth_ratio) * c; }
for (float i = 0.0; i < gl_FragCoord.y/u_resolution.y; i += u_grid_unit/u_resolution.y) { // 畫直線 float y = i; // 畫出 y = i 的函數線 float smooth_ratio = get_smooth_ratio(0.005, y-st.y); c = smooth_ratio * vec3(0.2, 0.2, 0.2) + (1.0-smooth_ratio) * c; }
float smooth_ratio;
vec2 center = vec2(0.5);
float r = distance(center, st); // 畫出圓心為 (0.5, 0.5),半徑為 0.2 的圓
smooth_ratio = get_smooth_ratio(0.005, 0.2-r);
c = smooth_ratio * vec3(0.0, 1.0, 1.0) +
(1.0-smooth_ratio) * c;
float y = pow(st.x, 5.0); // 畫出 y = x^5 的函數線
smooth_ratio = get_smooth_ratio(0.01, y-st.y);
c = smooth_ratio * vec3(0.0, 1.0, 0.0) +
(1.0-smooth_ratio) * c;
fragColor = vec4(c, 1.0);
}
“`
