前一篇 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); }

Imgur

因為像素點和中心點的距離放在分母,所以 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); }

Imgur

我們用了上一個單元的 get_smooth_ratio 在半徑 0.1 的地方畫了一個模糊的藍色的環,和原本的顏色向量疊加,就成功的在發光體外加上一層藍色光圈了!

讀著可以試著自己調整參數,看還能做出什麼新花樣。

繪製發光線條

接下來要教大家如何讓畫布上的線條發光,基本的原則就是計算片段像素點對直線的最短距離,然後根據最短距離來決定該像素點的亮度。

但在前一個單元就提到過,計算像素點對一個函數曲線的最短距離,在數學上要靠一個公式取得精確解是很困難的(要看該曲線用什麼函數來描述他)。

但我們仍然可以取一個近似解,在該線條上取樣多個點,逐一去比較每個點對片段像素點的距離,取最短的那一個作為我們的最短距離。

在這裡我們用貝茲曲線進行示範,因為貝茲曲線是有限長的曲線,可以平均取樣 1000 個點進行最小距離的計算。

根據 p5.js 基礎教學(九) –– 貝茲曲線 的內容,三階貝茲曲線是最常用的貝茲曲線,由四個控制點定義。公式如下:

Imgur

因為 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); }

以下是執行結果:

Imgur

但距離倒數的光暈衰減太快,我們可以嘗試另一個指數衰減曲線 exp(-d <em> 50.0),也就是 f(d) = e^(-50 </em> 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); }

以下是執行結果:

Imgur

可能有讀者會有疑問,指數函數不是會比倒數函數衰減還要快嗎,為什麼指數函數的光暈可以擴散的範圍比較大呢?

那是因為在距離 d 還很小的時候,倒數函數衰減的會比較快,比如說當 d1/2500 的時候,若往上加 1/50d 變成 1/2500 + 1/50),倒數函數 1.0/d <em> 0.0015 會衰減 50 倍,指數函數 exp(-d </em> 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); }

以下為執行結果:

Imgur

作品網址:
https://openprocessing.org/sketch/2346033