前一篇 glsl 基礎教學(四) –– 繪製線條 示範了如何畫出函數曲線、座標格線和圓形,但光是這樣,並沒有完全發揮到 glsl 在顏色呈現上的優勢。
本單元會示範另一種繪製圖形和線條的方法,這個方法還多了顏色漸層的效果,讓線條和物體看起來像發光一樣。
要讓物體有發光的效果,有一個精髓就是取「距離的倒數」,比如說我想讓畫布的中心點發光,我可以這樣做:
#version 300 es
precision highp float;
uniform vec2 u_resolution;
out vec4 fragColor;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
vec3 c = vec3(0.0);
float dist = distance(st, vec2(0.5, 0.5));
float light_ratio = 300.0/dist * 0.00015;
c += light_ratio * vec3(1.0, 1.0, 1.0);
fragColor = vec4(c, 1.0);
}
因為像素點和中心點的距離放在分母,所以 light_ratio
在距離小的時候會急劇放大,導致中心點的亮度會非常大,但因為在 glsl 中最亮的白色就是 (1.0, 1.0, 1.0)
,在靠近中心的某個範圍內,亮度超過 (1.0, 1.0, 1.0)
的區域就會被視作為 (1.0, 1.0, 1.0)
,所以中心就會是個顏色全白的球體。
但重點是當距離加大,亮度成倒數衰減所構成的光暈效果看起來非常真實,就會呈現上圖中的效果。
有趣的是當我們做一些簡單的調整,就能在球體外附加一層光圈的效果:
#version 300 es
precision highp 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;
vec3 c = vec3(0.0);
float dist = distance(st, vec2(0.5, 0.5));
float light_ratio = 300.0/dist * 0.00015;
c += light_ratio * vec3(1.0, 1.0, 1.0);
// 在半徑 0.1 的地方畫一個圓,並加大 smoothstep 的平滑範圍 0.2
c += vec3(.0, .0, get_smooth_ratio(0.2, dist-0.1));
fragColor = vec4(c, 1.0);
}
我們用了上一個單元的 get_smooth_ratio
在半徑 0.1 的地方畫了一個模糊的藍色的環,和原本的顏色向量疊加,就成功的在發光體外加上一層藍色光圈了!
讀著可以試著自己調整參數,看還能做出什麼新花樣。
接下來要教大家如何讓畫布上的線條發光,基本的原則就是計算片段像素點對直線的最短距離,然後根據最短距離來決定該像素點的亮度。
但在前一個單元就提到過,計算像素點對一個函數曲線的最短距離,在數學上要靠一個公式取得精確解是很困難的(要看該曲線用什麼函數來描述他)。
但我們仍然可以取一個近似解,在該線條上取樣多個點,逐一去比較每個點對片段像素點的距離,取最短的那一個作為我們的最短距離。
在這裡我們用貝茲曲線進行示範,因為貝茲曲線是有限長的曲線,可以平均取樣 1000 個點進行最小距離的計算。
根據 p5.js 基礎教學(九) –– 貝茲曲線 的內容,三階貝茲曲線是最常用的貝茲曲線,由四個控制點定義。公式如下:
因為 glsl 沒辦法使用 p5.js 的 bezier
所以我們只能根據公式自己寫一個:
vec2 bezier(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) {
float u = 1.0 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
vec2 p = uuu * p0; // (1-t)^3 * P0
p += 3.0 * uu * t * p1; // 3 * (1-t)^2 * t * P1
p += 3.0 * u * tt * p2; // 3 * (1-t) * t^2 * P2
p += ttt * p3; // t^3 * P3
return p;
}
將 t 代入 0~1
之間的任意實數,得到的所有座標點描繪出來的軌跡就是貝茲曲線的樣子,如果要計算某一個二維座標 vec2 uv
對這條貝茲曲線的最短距離,我們可以建構以下函數計算:
float distanceToBezier(vec2 uv, vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
float minDist = 1.0;
for (float t = 0.0; t <= 1.0; t += 0.001) {
vec2 pointOnCurve = bezier(p0, p1, p2, p3, t);
float dist = distance(uv, pointOnCurve);
minDist = min(minDist, dist);
}
return minDist;
}
以 0.001
為區間疊加 t
進行取樣,就能夠在 0~1
之間取樣 1000 個點進行最小距離的計算,然後就是根據距離計算像素點的發光比例:
#version 300 es
#ifdef GL_ES
precision highp float;
#endif
uniform vec2 u_resolution;
out vec4 fragColor;
vec2 bezier(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) {
float u = 1.0 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
vec2 p = uuu * p0; // (1-t)^3 * P0
p += 3.0 * uu * t * p1; // 3 * (1-t)^2 * t * P1
p += 3.0 * u * tt * p2; // 3 * (1-t) * t^2 * P2
p += ttt * p3; // t^3 * P3
return p;
}
float distanceToBezier(vec2 uv, vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
float minDist = 1.0;
for (float t = 0.0; t <= 1.0; t += 0.001) {
vec2 pointOnCurve = bezier(p0, p1, p2, p3, t);
float dist = distance(uv, pointOnCurve);
minDist = min(minDist, dist);
}
return minDist;
}
void main() {
// 將片段座標轉換為 uv 坐標
vec2 uv = gl_FragCoord.xy / u_resolution;
// 定義 Bézier 曲線的控制點
vec2 p0 = vec2(0.2, 0.7);
vec2 p1 = vec2(0.4, 0.3);
vec2 p2 = vec2(0.6, 0.7);
vec2 p3 = vec2(0.8, 0.3);
// 計算片段距離到曲線的最小距離
float d = distanceToBezier(uv, p0, p1, p2, p3);
// 根據距離計算發光比例
float light_ratio = 1.0/d * 0.0015;
// 設定顏色(白色發光曲線)
vec3 color = vec3(light_ratio);
fragColor = vec4(color, 1.0);
}
以下是執行結果:
但距離倒數的光暈衰減太快,我們可以嘗試另一個指數衰減曲線 exp(-d * 50.0)
,也就是 f(d) = e^(-50 * d)
:
#version 300 es
#ifdef GL_ES
precision highp float;
#endif
uniform vec2 u_resolution;
out vec4 fragColor;
vec2 bezier(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) {
float u = 1.0 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
vec2 p = uuu * p0; // (1-t)^3 * P0
p += 3.0 * uu * t * p1; // 3 * (1-t)^2 * t * P1
p += 3.0 * u * tt * p2; // 3 * (1-t) * t^2 * P2
p += ttt * p3; // t^3 * P3
return p;
}
float distanceToBezier(vec2 uv, vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
float minDist = 1.0;
for (float t = 0.0; t <= 1.0; t += 0.001) {
vec2 pointOnCurve = bezier(p0, p1, p2, p3, t);
float dist = distance(uv, pointOnCurve);
minDist = min(minDist, dist);
}
return minDist;
}
void main() {
// 將片段座標轉換為 uv 坐標
vec2 uv = gl_FragCoord.xy / u_resolution;
// 定義 Bézier 曲線的控制點
vec2 p0 = vec2(0.2, 0.7);
vec2 p1 = vec2(0.4, 0.3);
vec2 p2 = vec2(0.6, 0.7);
vec2 p3 = vec2(0.8, 0.3);
// 計算片段距離到曲線的最小距離
float d = distanceToBezier(uv, p0, p1, p2, p3);
// 根據距離計算發光比例
//float light_ratio = 1.0/d * 0.0015;
float light_ratio = exp(-d * 50.0);
// 設定顏色(白色發光曲線)
vec3 color = vec3(light_ratio);
fragColor = vec4(color, 1.0);
}
以下是執行結果:
可能有讀者會有疑問,指數函數不是會比倒數函數衰減還要快嗎,為什麼指數函數的光暈可以擴散的範圍比較大呢?
那是因為在距離 d
還很小的時候,倒數函數衰減的會比較快,比如說當 d
為 1/2500
的時候,若往上加 1/50
(d
變成 1/2500 + 1/50
),倒數函數 1.0/d * 0.0015
會衰減 50 倍,指數函數 exp(-d * 50.0)
卻只會衰減 e
倍(也就是 2.71828...
),然後光暈的產生區間,就處在倒數函數衰減比較快的區間。
最後來試試看把 p5.js 創作應用(九) –– 貝茲曲線隨機動畫 做到 glsl 上面會是什麼樣的效果:
mySketch.js
let rectShader;
function preload(){
rectShader = loadShader('shader.vert', 'shader.frag');
}
function setup() {
pixelDensity(1);
createCanvas(600, 600, WEBGL);
noStroke();
}
function vector_rotation(x,y,angle) {
return [cos(angle)*x-sin(angle)*y,sin(angle)*x+cos(angle)*y];
}
function draw() {
shader(rectShader);
var center_list = [
[30, 20],
[30, 20],
[30, 20],
[30, 20]
];
var radius_list = [
100,
100,
100,
100
];
var rotate_speed_list = [
1/60/1.8 * 2 * PI,
1/60/3 * 2 * PI,
-1/60/3 * 2 * PI,
-1/60/1.5 * 2 * PI
];
var curve_num = 8;
var all_rotate_speed = 1/60/8 * 2 * PI;
var point_list = [];
for (var i = 0; i < 4; i++) {
point_list.push(
[
center_list[i][0] + radius_list[i] * cos(frameCount * rotate_speed_list[i]),
center_list[i][1] + radius_list[i] * sin(frameCount * rotate_speed_list[i])
]
);
}
for (var i = 0; i < 4; i++) {
point_list[i] = vector_rotation(
point_list[i][0],
point_list[i][1],
frameCount * all_rotate_speed,
);
}
rectShader.setUniform('u_resolution', [width, height]);
rectShader.setUniform('u_time', frameCount/100);
rectShader.setUniform('u_curve_num', curve_num);
rectShader.setUniform('u_point_list', point_list.flat());
rect(0,0,width, height);
}
shader.frag
#version 300 es
#ifdef GL_ES
precision highp float;
#endif
uniform float u_curve_num;
uniform vec2 u_resolution;
uniform vec2 u_point_list[4];
out vec4 fragColor;
#define PI 3.14159265358979323846
vec2 bezier(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) {
float u = 1.0 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
vec2 p = uuu * p0; // (1-t)^3 * P0
p += 3.0 * uu * t * p1; // 3 * (1-t)^2 * t * P1
p += 3.0 * u * tt * p2; // 3 * (1-t) * t^2 * P2
p += ttt * p3; // t^3 * P3
return p;
}
float distanceToBezier(vec2 uv, vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
float minDist = 1.0;
for (float t = 0.0; t <= 1.0; t += 0.01) {
vec2 pointOnCurve = bezier(p0, p1, p2, p3, t);
float dist = distance(uv, pointOnCurve);
minDist = min(minDist, dist);
}
return minDist;
}
vec2 vector_rotation(float x, float y, float angle) {
return vec2(cos(angle)*x-sin(angle)*y,sin(angle)*x+cos(angle)*y);
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 center = vec2(0.5);
vec3 color = vec3(0.0);
for (float i = 0.0; i < u_curve_num; i += 1.0) {
vec2 p0 = vector_rotation(
u_point_list[0][0],
u_point_list[0][1],
2.0 * PI * i / u_curve_num
);
p0 = vec2(
center.x + p0[0] / u_resolution.x,
center.y + p0[1] / u_resolution.y
);
vec2 p1 = vector_rotation(
u_point_list[1][0],
u_point_list[1][1],
2.0 * PI * i / u_curve_num
);
p1 = vec2(
center.x + p1[0] / u_resolution.x,
center.y + p1[1] / u_resolution.y
);
vec2 p2 = vector_rotation(
u_point_list[2][0],
u_point_list[2][1],
2.0 * PI * i / u_curve_num
);
p2 = vec2(
center.x + p2[0] / u_resolution.x,
center.y + p2[1] / u_resolution.y
);
vec2 p3 = vector_rotation(
u_point_list[3][0],
u_point_list[3][1],
2.0 * PI * i / u_curve_num
);
p3 = vec2(
center.x + p3[0] / u_resolution.x,
center.y + p3[1] / u_resolution.y
);
float d = distanceToBezier(uv, p0, p1, p2, p3);
float light_ratio = exp(-d * 100.0);
color += vec3(light_ratio);
}
fragColor = vec4(color, 1.0);
}
以下為執行結果:
作品網址:
https://openprocessing.org/sketch/2346033