注意事項:
- 本文僅涉及 WebGL1.0
- 本文僅涉及 fragment shader
- 本文重點參考對象:The Book of Shaders
- 一些圖片來源於 open.gl
- 下面所有例子都可以貼到這個 Playground 直接運行
本質#
從上面流水線的圖也能看出來,fragment shader 的本質就是對每個(像素)點作處理。大家都知道 GPU 擅長並行的簡單計算,目的就是要做好這件事。相反,CPU 擅長複雜計算,但處理器卻很少。
The Book of Shaders 給出一個形象的比喻:
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 的範圍,這是構建位置和顏色關係最直接的方式。此外,像 smoothstep
和 fract
這樣的重要內置函數都返回範圍為 0 到 1 的值。
在上面的例子中,首先將 gl_FragCoord
映射為 0
到 1
,然後分別把 x 和 y 值放到 r 和 g,(左下角是 0,0
)所以出來的圖像左下角黑色,左上角紅色,右下角綠色。
P.S. WebGL2 的輸出方式不一樣
常用函數#
為了畫出各種花里胡哨的圖形,我們必須了解這些常用的函數:
角度和三角函數相關#
指數相關#
常用數學方法#
- abs() 絕對值
- sign(x) 把 x 映射為 0、1、-1
- floor() 向下取整
- ceil() 向下取整
- fract() 只取小數部分
- mod(x, y) 對 x 取模
- min(x, y) 返回兩值中較小值
- max(x, y) 返回兩值中較大值
- clamp(x, minVal, maxVal) 仅在最大最小值範圍內變化,超出範圍時輸出邊界值
- mix(x, y, a) 從 x 漸變到 y,a 是變換的 “進度”
- step(edge, x) 一種突變,將 x 映射為 0 到 1,小於 edge 返回 0,否則返回 1
- smoothstep(edge0, edge1, x) 在 edge0 和 edge1 的區間內,將 x(使用 Hermite interpolation)絲滑地映射為 0 到 1,需要注意的是改變 edge0 和 edge1 順序的話效果是不一樣的
GEOMETRIC FUNCTIONS#
- length() 一個向量的長度
- distance() 兩個點的距離,其實跟 length 差不多是一個東西了,只是解讀的角度有點不同
- dot() 向量點積
- cross() 向量叉積
- normalize() 返回單位向量
- facefoward() 返回與參考向量指向相同方向的向量
- reflect() 反射計算
- refract() 折射計算
可以開始畫了嗎!#
可以!在了解上面一堆常用函數之後,你已經可以畫出出神入化的圖像了!例如下面這樣的簡短的代碼就能營造一種熔岩燈的感覺(看效果戳這裡):
// 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);
}
方圓是最容易理解的圖形,上面的代碼有幾個需要注意的點:
- 方形的代碼有一點長,不過其實注意一下以乘法製造交集就好了
- 例子中使用的點積畫圓形,因為點積就是兩個向量的投影關係,所以自己投到自己身上就是一種計算長度的方法
- 當然你也可以使用
length
、distance
甚至自己用兩點間距離算法來畫圓 - 畫圓形(或者說曲線)也不是不可以用
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 到 PIr
可以理解為原本的 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,效果就出來了
伪随机#
對於生成圖像,也不涉及金錢交易什麼的,伪隨機就夠用了,至於真正的隨機,用到硬件噪聲等無法預測的輸入,這裡暫不討論。
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)
的乘數越大,結果看起來就越隨機:
現在我們知道可以用 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/