iT邦幫忙

2024 iThome 鐵人賽

DAY 26
0
Modern Web

p5.js 的環形藝術系列 第 26

[Day 26] glsl 基礎教學(五) –– 繪製發光線條和物體

  • 分享至 

  • xImage
  •  

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

以下是執行結果:

Imgur

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

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

以下為執行結果:

Imgur

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


上一篇
[Day 25] glsl 基礎教學(四) –– 繪製線條
下一篇
[Day 27] p5.js 實戰演練(十) –– 行星環繞動畫(一)
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言