SSShooter

SSShooter

Write like you're running out of time.

對 fragment shader 的一點點見解

注意事項:

  • 本文僅涉及 WebGL1.0
  • 本文僅涉及 fragment shader
  • 本文重點參考對象:The Book of Shaders
  • 一些圖片來源於 open.gl
  • 下面所有例子都可以貼到這個 Playground 直接運行

圖形流水線

本質#

從上面流水線的圖也能看出來,fragment shader 的本質就是對每個(像素)點作處理。大家都知道 GPU 擅長並行的簡單計算,目的就是要做好這件事。相反,CPU 擅長複雜計算,但處理器卻很少。

The Book of Shaders 給出一個形象的比喻:

CPU

GPU

uniform#

uniform 之所以叫 uniform 是因為它是統一的。由 CPU 傳到 GPU,在處理每一個像素時,這個值都是統一的。相對的是 varying,這些值由 vertex shader 計算後傳到 fragment shader,針對每個三角形,收到的值是不一致的。

輸入輸出#

uniform vec2 u_resolution;

void main() {
	// `gl_FragCoord.xy/u_resolution` 將 xy 值映射為 0 到 1
	vec2 st = gl_FragCoord.xy/u_resolution;
	gl_FragColor = vec4(st.x,st.y,0.0,1.0);
}

gl_FragCoord 是內置輸入,表示一個點的位置;gl_FragColor 是固定的輸出值,表示這個點應該顯示為何種顏色。

映射#

關鍵輸入是位置,關鍵輸出是顏色,那麼很明顯,要使用 fragment shader 繪圖的重點就是找到位置和顏色的關係,也可以說 GPU 要做的就是要計算每個點的顏色。再換一個說法就是你要發揮你的聰明才智,利用位置 x 和 y 的關係生成一個好看的圖像。

所以為什麼 0 到 1 的映射這麼重要呢,因為 webgl 的 rgb 輸出就是 0 到 1 的範圍,這是構建位置和顏色關係最直接的方式。此外,像 smoothstepfract 這樣的重要內置函數都返回範圍為 0 到 1 的值。

在上面的例子中,首先將 gl_FragCoord 映射為 01,然後分別把 x 和 y 值放到 r 和 g,(左下角是 0,0)所以出來的圖像左下角黑色,左上角紅色,右下角綠色。

P.S. WebGL2 的輸出方式不一樣

常用函數#

為了畫出各種花里胡哨的圖形,我們必須了解這些常用的函數:

角度和三角函數相關#

指數相關#

常用數學方法#

GEOMETRIC FUNCTIONS#

可以開始畫了嗎!#

可以!在了解上面一堆常用函數之後,你已經可以畫出出神入化的圖像了!例如下面這樣的簡短的代碼就能營造一種熔岩燈的感覺(看效果戳這裡):

// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }

float snoise(vec2 v) {
    const vec4 C = vec4(0.211324865405187,  // (3.0-sqrt(3.0))/6.0
                        0.366025403784439,  // 0.5*(sqrt(3.0)-1.0)
                        -0.577350269189626,  // -1.0 + 2.0 * C.x
                        0.024390243902439); // 1.0 / 41.0
    vec2 i  = floor(v + dot(v, C.yy) );
    vec2 x0 = v -   i + dot(i, C.xx);
    vec2 i1;
    i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec4 x12 = x0.xyxy + C.xxzz;
    x12.xy -= i1;
    i = mod289(i); // Avoid truncation effects in permutation
    vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
        + i.x + vec3(0.0, i1.x, 1.0 ));

    vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
    m = m*m ;
    m = m*m ;
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;
    m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
    vec3 g;
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * x12.xz + h.yz * x12.yw;
    return 130.0 * dot(m, g);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    st.x *= u_resolution.x/u_resolution.y;
    vec3 color = vec3(0.0);
    vec2 pos = vec2(st*3.);

    float DF = 0.0;

    // Add a random position
    float a = 0.0;
    vec2 vel = vec2(u_time*.1);
    DF += snoise(pos+vel)*.25+.25;

    // Add a random position
    a = snoise(pos*vec2(cos(u_time*0.15),sin(u_time*0.1))*0.1)*3.1415;
    vel = vec2(cos(a),sin(a));
    DF += snoise(pos+vel)*.25+.25;

    color = vec3( smoothstep(.7,.75,fract(DF)) );

    gl_FragColor = vec4(1.0-color,1.0);
}

開玩笑的,離獨自構思出這種圖像還有很大一段距離 😂

0 到 1 有幾種走法#

在實際使用中,必定不可能只存在線性關係,所以利用各種函數調整 xy 和 0 到 1 的映射,是學習編寫 fragment 不能繞過的一步。

The Book of Shaders 中,作者使用 plot 函數將映射關係可視化,同時將 x 值的映射賦給 gl_FragColor,這樣可以比較直觀地看到不同曲線的實際效果:

// Author: Inigo Quiles
// Title: Pcurve

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

//  Function from Iñigo Quiles
//  www.iquilezles.org/www/articles/functions/functions.htm
float pcurve( float x, float a, float b ){
    float k = pow(a+b,a+b) / (pow(a,a)*pow(b,b));
    return k * pow( x, a ) * pow( 1.0-x, b );
}

float plot(vec2 st, float pct){
  return  smoothstep( pct-0.02, pct, st.y) -
          smoothstep( pct, pct+0.02, st.y);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution;

    float y = pcurve(st.x,3.0,1.0);

    vec3 color = vec3(y);

    float pct = plot(st,y);
    color = (1.0-pct)*color+pct*vec3(0.0,1.0,0.0);

    gl_FragColor = vec4(color,1.0);
}

渲染結果可以直接點這裡

方與圓#

float rect (vec2 st,float width){
     // bottom-left
    vec2 bl = step(vec2(0.5-width/2.),st);
    float pct = bl.x * bl.y;

    // top-right
    vec2 tr = step(vec2(0.5-width/2.),1.0-st);
    pct *= tr.x * tr.y;
    return pct;
}

float circle(in vec2 _st, in float _radius){
    vec2 dist = _st-vec2(0.5);
	return 1.-smoothstep(_radius-(_radius*0.01),
                         _radius+(_radius*0.01),
                         dot(dist,dist)*4.0);
}

void main(){
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 rectcolor = vec3(rect(st,.5));
    vec3 circlecolor = vec3(1) - vec3(circle(st,.5));

    gl_FragColor = vec4(circlecolor + rectcolor,1.0);
}

方圓是最容易理解的圖形,上面的代碼有幾個需要注意的點:

  • 方形的代碼有一點長,不過其實注意一下以乘法製造交集就好了
  • 例子中使用的點積畫圓形,因為點積就是兩個向量的投影關係,所以自己投到自己身上就是一種計算長度的方法
  • 當然你也可以使用 lengthdistance 甚至自己用兩點間距離算法來畫圓
  • 畫圓形(或者說曲線)也不是不可以用 step,只是使用 smoothstep 會有抗鋸齒效果
  • 程序員的好朋友抽象永遠能幫你實現更高效簡潔的代碼,你可以輕易調個函數畫出複雜的圖形(即使暫時沒看懂繪圖的原理)

小星球#

一些手機圖片處理應用會有一個叫 “小世界” 的濾鏡,其實就是把圖片從笛卡爾坐標映射成極坐標。如果我們希望用 fragment shader 畫出雪花、齒輪等圖形,可以根據同樣的道理把對應的 “波形” 映射到極坐標,代碼如下:

void main(){
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 color = vec3(0.0);

    vec2 pos = vec2(0.5)-st;

    float r = length(pos)*2.0;
    float a = atan(pos.y,pos.x);

    float f = cos(a*3.); // 3 瓣
    // f = abs(cos(a*3.)); // 6 瓣
    // f = abs(cos(a*2.5))*.5+.3; // 5 瓣
    // f = abs(cos(a*12.)*sin(a*3.))*.8+.1; // 雪花
    // f = smoothstep(-.5,1., cos(a*10.))*0.2+0.5; // 齒輪

    color = vec3( 1.-smoothstep(f,f+0.02,r) );

    gl_FragColor = vec4(color, 1.0);
}
  • a 通過反正切求出點與中心連線的角度,相當於原本的 X 軸,返回值為 -PI 到 PI
  • r 可以理解為原本的 Y 軸
  • f 則是 Y 軸的值,color 就是用 smoothstep 分割 f,小於 f 顯黑,大於 f 顯白
  • 所以順眼的寫法其實是 float y = cos(x*3.);
  • 通過將不同函數映射到極坐標可以產生各種有意思的形狀

Pattern#

這裡的 Pattern 不是設計模式,而是指重複的圖案。之前講的都是單個圖案,那麼怎麼批量生成一大堆排列整齊的圖案呢?

答案是fract()

void main() {
	vec2 st = gl_FragCoord.xy/u_resolution;
    vec3 color = vec3(0.0);

    st *= 3.0;      // Scale up the space by 3
    st = fract(st); // Wrap around 1.0

    // Now we have 9 spaces that go from 0-1

    color = vec3(st,0.0);
    // color = vec3(circle(st,0.5));

	gl_FragColor = vec4(color,1.0);
}

人類的本質是復讀機,而 fract() 可以算是 GLSL 的復讀機了吧 😅,通過 fract() 可以讓數值在 0 到 1 間的小數中不斷循環。

  • 首先將 xy 值映射為 0 到 1
  • 再將其映射為 0 到 3
  • 經過 fract() 的處理,就會變成復讀 3 次 0 到 1,效果就出來了

伪随机#

對於生成圖像,也不涉及金錢交易什麼的,伪隨機就夠用了,至於真正的隨機,用到硬件噪聲等無法預測的輸入,這裡暫不討論。

image

y = fract(sin(x)*1.0); 就是伪隨機的基礎(之一)。雖然從連續的圖像上看,非常規律,但是要是以 x 為整數,取 y,作為一個正常人類未必能反應出來這就是一個普通的 sin,我們使用 JavaScript 輸出一下整數作為參數輸出的 “隨機值”:

function fract(num) {
  return Math.abs(num - Math.trunc(num))
}
function random(x) {
  return fract(Math.sin(x) * 1)
}
const nums = []
for (let i = 1; i < 20; i++) {
  nums.push(random(i))
}
// output=> [0.8414709848078965, 0.9092974268256817, 0.1411200080598672, 0.7568024953079282, 0.9589242746631385, 0.27941549819892586, 0.6569865987187891, 0.9893582466233818, 0.4121184852417566, 0.5440211108893698, 0.9999902065507035, 0.5365729180004349, 0.4201670368266409, 0.9906073556948704, 0.6502878401571168, 0.2879033166650653, 0.9613974918795568, 0.7509872467716762, 0.14987720966295234]

在這個基礎上,sin(x) 的乘數越大,結果看起來就越隨機:

image

現在我們知道可以用 y = fract(sin(x)*10000.0); 把 x 映射為伪隨機的 0 到 1,那麼接下來就很簡單了吧?把像素位置映射為隨機數,然後把隨機數設置為輸出顏色,便能看到隨機圖像。

float random (vec2 st) {
    return fract(sin(dot(st.xy,
                         vec2(12.9898,78.233)))*
        10000.0);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
	// st = st * 15.;
	// st = floor(st);
    float rnd = random( st );

    gl_FragColor = vec4(vec3(rnd),1.0);
}

上面的代碼可以繪製出類似雪花屏的效果,需要注意的是 random 函數中的 “x” 是用 xy 值與一個向量求點積(投影長度)所得。如果想要馬賽克也很簡單,用之前的復讀方法映射一下就完事啦(去掉兩個註釋可以看到效果)!

其他函數#

矩陣相關#

向量相關#

材質相關#

工具#

原文鏈接:https://ssshooter.com/2022-10-12-fragment-shader/

參考#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。