外星圖像渲染語言?如何應用 Shader 跟 WebGL 達成絢麗視覺效果

本篇文章老闆要來聊聊什麼是 Shader 、把 Shader 實作在專案中的經驗、以及如何開始玩玩看 Shader。 3D 技術逐漸應用於網頁上,瞭解 Shader 就可以透過 GPU ,以更快的速度渲染出各式各樣有趣的視覺效果和互動,快跟著老闆看下去吧!

吳哲宇的創作 Spike Mountains

閱讀完此篇文章,你會知道:

  • 什麼是 Shader
  • Shader 應用案例
  • 如何開始學習 Shader

相關資源

Shader 作品和必追蹤的藝術家


Shader

老闆第一次接觸 Shader,是在 Codrops 看到的 WebGL Distortion Hover Effects 這篇文章,第一印象覺得它根本就像是外星語言,因為它是非常底層的、類似 C++ 的程式語言。

滑鼠滑過物件的互動,對網頁工程師來說,大概就是簡單地在 css 寫上 &hover 然後可能換個背景圖片和顏色,再加上 transition 。但這篇文章中的 Demo 卻做出了各種讓人為之驚艷的效果,像是類似液體那樣 ㄍㄡˊ ㄍㄡˊ 的感覺、三角形漸層、稜鏡感、刷色感、鋸齒狀…等等。老闆原本想說,這大概需要研究個兩三天用很多行的程式碼才寫得出來,殊不知點開它的 code ,發現做出這麼酷炫視覺效果的程式碼,嚴格上來說只有短短的六行!

大概就是這個部分:

// hover-effect.js
void main() {
  vec4 disp = texture2D(disp, vUv);
  vec2 dispVec = vec2(disp.r, disp.g);

  vec2 uv = 0.5 * gl_FragCoord.xy / (res.xy) ;
  vec2 myUV = (uv - vec2(0.5))*res.zw + vec2(0.5);

  vec2 distortedPosition1 = myUV + getRotM(angle1) * dispVec * intensity1 * dispFactor;
  vec2 distortedPosition2 = myUV + getRotM(angle2) * dispVec * intensity2 * (1.0 - dispFactor);
  vec4 _texture1 = texture2D(texture1, distortedPosition1);
  vec4 _texture2 = texture2D(texture2, distortedPosition2);
  gl_FragColor = mix(_texture1, _texture2, dispFactor);
}
`;

為了瞭解他如何運作,老闆接觸到 Shadertoy 這個網站,裡面有類似剛剛那種 hover 效果的超級進階版應用,像是用 200 行左右的程式碼做出 超逼真海水 ,或者像這樣利用造點圖片加上扭曲旋轉效果即完成的 銀河 Galaxy

Shadertoy 上的範例作品銀河 Galaxy
Shadertoy 上的範例作品銀河 Galaxy

什麼是 Shader ?

What is a fragment shader?

Shaders are a set of instructions, but the instructions are executed all at once for every single pixel on the screen. That means the code you write has to behave differently depending on the position of the pixel on the screen. Like a type press, your program will work as a function that receives a position and returns a color, and when it’s compiled it will run extraordinarily fast. - The Book of Shaders

Shader 是繪製螢幕上內容的著色器,最重要的特性在於它是以每顆像素個別做渲染的方式運作,可以想像成活字印刷機,透過定義每顆像素在每個位置的表現組成一個完整畫面。

為什麼 Shader 渲染可以這麼快?

因為它不是用 CPU ,而是用 GPU 來運算。 CPU (中央處理器)通常被大家理解為是電腦的大腦,負責執行作業系統所需的指令與程序,它是線性的加工廠,一次可以處理一個工作。而 GPU (繪圖處理器)則是由許多更小也更專業的核心組成,所以我們就可以一次性地送出我們想處理的像素,一次性地處理完之後,再渲染出來。

用水管來比喻的話, CPU 就像一根大水管,但它一次只能處理一顆像素,而我們有堆積如山的像素等著被處理; GPU 則像是很多個水管並列,所以我們可以讓很多顆像素同時通過不同的小水管。

圖片來源:The Book of Shaders
圖片來源:The Book of Shaders

但是問題來了,我們要怎麼告訴這些不同的小水管,每顆像素該如何做處理呢?

在撰寫 Shaders 時使用的語言是 GLSL (OpenGL Shading Language) ,也稱做 GLslang ,是一個以 C 語言為基礎的高階著色語言。它讓我們可以跟 GPU 溝通,安排每個小水管該負責什麼工作。

這也是讓 Shaders 看起來不是很平易近人的原因。

首先,為了讓每根小水管能夠同時獨立運作,它們彼此之間的資料會是單向且無法溝通的,就像上方圖片示意那樣,資料處理的方向必須是相同的,而且每根水管都沒有辦法知道彼此處理完的像素變成什麼樣子。此外,因為 GPU 會讓工作接踵而至,換句話說會一直塞東西進水管,所以這些水管也就忙到沒辦法記得它們自己上一個工作的內容。

Each thread is not just blind but also memoryless. Besides the abstraction required to code a general function that changes the result pixel by pixel depending on its position, the blind and memoryless constraints make shaders not very popular among beginning programmers. - The Book of Shaders

Hello World

讓瀏覽器顯示 Hello World 通常是學新語言時會做的第一個練習,但是要在 GPU 領域渲染文字是件相對困難的事情,所以這個範例就用色塊來說明:

#ifdef GL_ES
precision mediump float;
#endif

uniform float u_time;

void main() {
  gl_FragColor = vec4(0,0,0,0); // red, green, blue, alpha
}

接著也可以嘗試代入變數uniform

我們可以透過 uniform 從 CPU 發送資料到 GPU ,且透過 uniform 宣告的變數會是全域且獨特的,可以在所有的 Shaders 中讀取。

#ifdef GL_ES
precision mediump float;
#endif

uniform float u_time;

void main() {
  gl_FragColor = vec4(abs(sin(u_time)),0.0,0.0,1.0);

  // gl_FragColor = vec4(abs(sin(mod(u_time, 0.5))),0.0,0.0,1.0); // 例如這樣寫可以讓閃爍變快
}

在 GLSL 中有提供許多計算數值的 functions ,像是 sin(), cos(), tan(), asin(), acos(), atan(), pow(), exp(), log(), sqrt(), abs(), sign(), floor(), ceil(), fract(), mod(), min(), max() and clamp() 等等,我們可以用這些函式搭配時間做出會不斷變動的圖形。

gl_FragCoord

除了像上面的範例那樣,用 uniform 宣告同樣的 input ,再用 default output vec4 gl_FragColor 呈現結果的做法外, GLSL 也給了我們 default input vec4 gl_FragCoord ,其中儲存了每個水管正在處理的 pixel 或 screen fragment 的座標,有了這個資訊,我們就可以針對每個座標做個別的處理,而這個情況下所使用的變數就不會叫做 uniform ,而會稱作 varying 。

#ifdef GL_ES
precision mediump float;
#endif

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

void main() {
  vec2 st = gl_FragCoord.xy/u_resolution;
  // vec2 st = gl_FragCoord.xy/u_mouse; // 例如這樣可以用滑鼠改變漸層的位置

  gl_FragColor = vec4(st.x,st.y,0.0,1.0);
}

在 Shader 裡面畫圖形是很困難的事情

因為用「點」連成「面」是一個相對高階的概念,在 GLSL 中如果要做出形狀,就得把每個邊的位置都計算出來之後再組合起來,才可以變成圖形。所以就會需要很多角度、三角函數、指數等等的數學計算。


Shader 可以做什麼?

把 Shader 做為材質來使用

可以用圖層的概念來想: 基礎圖形 + 一些 filter + 扭轉座標 = 看起來就會像有躁點的漩渦。

把 Shader 用在 3D 的模型上

用 Shaders 渲染一顆 3D 的球,也可以根據滑鼠位置做變化。

綜合應用

用 p5.js 畫一張動態圖片 + 把 Shaders 做成扭曲 filter。

如果註解掉 frag 標籤下, void main(){} 裡面的這三行程式碼,就可以得到沒有經過 shader 濾鏡的原始 p5.js 動態畫面:

distorted_st.x+=cnoise(vec3(st.x*5000.,st.y*3000.,u_time))/(1.+(sin(sqrt(st.y+u_time/20.)*50.)+1.)*500.)/2.; // 電視雜訊 1
distorted_st.y+=cnoise(vec3(st.x*5000.,st.y*3000.,u_time))/(1.+(sin(sqrt(st.y+u_time/10.)*50.)+1.)*1000.)/2.; // 電視雜訊 2
distorted_st.x += sin(distorted_st.y*(50.+sin(st.x)*20.)+u_time)*distorted_st.y*distorted_st.y/10.; // X方向的扭曲

Shader 就像是非常高功能的透鏡,但困難的點在於,我們要定義每個像素經過這個透鏡的行為。

與 3D 整合:Three.js – Displacement map

3D 軟體的渲染,像是 Unreal、Vray、Octane 等等軟體內的材質編輯器,跟 Shader 運用的概念都一樣,所以如果沒有要做到更底層的應用,使用 Shader 就像是在自討苦吃。因為把 Shader 做為 3D 環境的材質來使用,勢必得考慮光影的渲染,例如光打到某個點上,材質會如何變化、入射方向、反光效果等等,會更加複雜。

例如要做出像磁磚獲木紋那樣凹凸的效果,除了貼上紋理之外,還需要告訴軟體根據這個紋理渲染對應的陰影。 Shader 的 displacement map 就是透過指定一個畫面,算出對應的陰影和光線反射,讓整個畫面看起來更真實。

應用案例

日本藝術家 Sayama 的創作

日本藝術家 Sayama – 200426

實務應用

所有技術最重要的還是要能夠應用,譬如工作室願意把它用到上線的專案中,它才有意義。

實務上老闆把 Shader 拿來畫 OUTERNETS 的 Home 和 About us ,做出光暈和躁點效果,還有非常繽紛科幻可以跟滑鼠互動的旋轉球球。

Outernets

Outernets
Outernets

Outernets – About us

Outernets - About us
Outernets – About us

墨雨工作室也嘗試把 Shader 應用在 跟18天台灣生啤酒1起吃飯8! 這個活動網頁上,做出啤酒噴灑出來的動態扭曲效果。

跟18天台灣生啤酒1起吃飯8!
跟18天台灣生啤酒1起吃飯8!

如何開始學習 Shader ?

The Book of Shaders

這個網站用深入淺出的說明加上可以操作的範例,也提供了一個好用的 編輯器 (從網站首頁點選 Examples Gallery ,再點擊 “Hello World” 下面的色塊),可以先從這邊熟悉 Shader ,之後再嘗試放到自己的專案中。

Touch Designer

探索 Shader 也可以從 Touch Designer 開始,它幫我們把各種模組都寫好,讓我們不用寫任何程式碼,只要載入不同的圖片和效果就可以玩出各式各樣的變化。

Banana Test

例如老闆之前在嘗試的時候,把香蕉經過 transform 再合成到背景上面。

Banana test
Banana test

題外話:各位如果對 3D 有興趣的話,可以去玩玩看 Blender ,它是開源的免費軟體。

在 p5 裡面玩 Shader

ITP – p5js shaders 介紹了如何把 p5 和 Shader 結合使用,把 Shader 當成材質或者影像的即時處理。

老闆也在 OpenProcessing 製作了 Shader template ,基礎的架設都已經完成了,歡迎 fork 之後自己玩玩看囉!

簡單的 Demo

雜訊效果:可以用 rand() 給原本位置的像素一個隨機的位置資訊,就會讓畫面有看起來霧霧的效果。

// frag 標籤中的第 20 行開始
void main(){
  vec2 st = var_vertTexCoord.xy /u_resolution.xy;

  st.x += rand(st); // 霧霧的效果
  st.x += rand(st) / (1.+st.y*10.); // 上面會霧霧的,但越下面會越平滑

  vec3 color = vec3(st.x,st.y,1.0);
  float d = distance(u_mouse,st);
  color*=1.-d;
  gl_FragColor= vec4(color,1.0);
}

扭曲效果:用 sin() 給 y 位置的資訊(但因為底圖沒有東西所以看不太出來效果)

// frag 標籤中的第 20 行開始
void main(){
  vec2 st = var_vertTexCoord.xy /u_resolution.xy;

  st.y += sin(st.x/10.)/10.; // 像這樣

  vec3 color = vec3(st.x,st.y,1.0);
  float d = distance(u_mouse,st);
  color*=1.-d;
  gl_FragColor= vec4(color,1.0);
}

範例 test bubble 根據左側的底圖,做完扭曲效果後就變成右側:

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

  st.x += rand(st)/5.; // 模糊效果
  st.y += sin(st.x * 50.) /10. ; // 扭曲效果

  vec3 color = vec3(0.);
  float ang = atan(st.y-0.55,st.x-0.5) ;
  float r = sin(ang*5.+u_time*2.+u_mouse.y*3. +u_mouse.x*3.);
  vec2 displacemenetMap = vec2(cos(ang),sin(ang))*r /150. ;
  vec3 spray = texture2D(tex0,st + displacemenetMap -vec2(0.,0.2) ).rgb;

  if (!u_mouse_pressed){
    color += spray;
  }else{
    color = vec3(displacemenetMap,0.5)*500.;
  }

  gl_FragColor= vec4(color,1.0);
}

也可以拿上一格留下的畫面做扭曲,看起來就會像液體效果


結語

老闆覺得現在 3D 的趨勢應該會越來越往網頁發展,像 Autodesk 也出了 Fusion360 可以在網頁上製作 3D 物件,如果可以把這些效果應用在案子的網頁上,想必可以創作出更加吸睛的視覺。

五分鐘問答時間

Q. 會開 Shader 課程嗎?

感覺有點難募資達標,有點太小眾了😂

Q. 最近有看到什麼用 Shader 做的酷案例嗎?

可以在 awwwards 上找到很多厲害的範例!搭配 p5.js 或是 three.js 就可以用在像是 carousel (輪播)或是其他扭曲的效果。

Q. Node Editor 怎麼做?

可以使用現成函式庫 Rete.js ,把自己的 function 做成可以給前端使用的介面。但技術尚未非常成熟,用在實務要自己注意一下囉。

小範例:胎死腹中的 文章產生器 原本想說可以做一份教材,用模組編輯的方式組裝成 A, B, C… 多種不同的教材,就可以因材施教。但發現沒有特別方便,而且概念其實也蠻類似現在的筆記工具 Notion 。

老闆貼心提示: CodePen 不是工程師實務上會使用的

使用 CodePen 或者 OpenProcessing 等平台,主要是為了讓學生快速上手,不需要自己架設開發環境,如果想實作到 production ,還需要再去更進階了解 JavaScript 的模組化概念,以及使用 Webpack 或者 Rollup 之類的打包工具。


工商時間

互動藝術程式創作入門
互動藝術程式創作入門

《互動藝術程式創作入門》課程學生作品分享

Hahow 課程作業成果

課程加碼單元

此篇直播筆記由幫手 Yuan 協助整理

墨雨設計banner

PHP Code Snippets Powered By : XYZScripts.com