本篇開始講解一些使用在片段著色器的繪圖技巧,第一步就是學會如何繪製基本的函數曲線,經過本單元的講解,我們可以學會如何繪製以下的圖案:
因為是著重在片段著色器的繪圖技巧,所以本單元只會改動片段著色器 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) < 0.004) {
fragColor = vec4(vec3(0.0,1.0,0.0), 1.0);
} else {
fragColor = vec4(vec3(0.0), 1.0);
}
}
我們判斷距離的方法很簡單,衡量目標像素點的 y 座標,和 f(x) = x^5 這個曲線的「垂直距離」,若超過 0.004
,則渲染成背景的黑色,反之則是綠色,就能做出類似的效果了。
但現在的問題是,這個線條看起來有些粗糙,上面會有一些畫素過低的顆粒感,解決方法是,不直接套用非黑即綠的粗暴繪製方法,而是根據距離摻入過渡的混合色。
這個方法必須引入之前在 p5.js 實戰演練(六) –– 煙霧動畫實作(一) 介紹到的 smoothstep
函數。
smoothstep
函數的數學表示法為
在計算機圖學中,用來做平滑的過渡插值,我們要使用這個函數,來進行線條顏色(綠色)和背景色(黑色)的過渡插值。
若將該函數圖形化,會長這樣:
在 glsl 中,我們不用自己列出算式來計算 smoothstep
的數值,glsl 已經有一個同名的函數給我們使用:
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);
}
https://thebookofshaders.com/05/