Contents

Shader入門 -- GLSL與GPU運行原則 Shader的基本運作方式

   2024年03月29日     2 min read

使用 GLSL 的基本操作

本文重點

  • Shader 與 GPU 的關係
  • Shader 語言 GLSL 入門
  • Shader 範例

前言

在開始進行 Shader 的編寫之前,要先來了解 GLSL 的基本操作。相比於在 CPU 程式中使用的 print 或 console.log 等方法,GPU 上的 Debug 要複雜得多。在 GPU 上,我們無法像在 CPU 上那樣直接輸出日誌或訊息。然而,即使在這樣的環境下,我們仍然可以通過一些小技巧來檢查我們的 Shader 程式碼的執行情況。

Hello World

precision mediump float;

uniform vec2 u_resolution;

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

  vec3 color = vec3(uv, 0.);

  gl_FragColor = vec4(color, 1.0);
}

上面是一個可以透過 VSCode 執行的 GLSL 初始範本,跟大多數語言一樣有個 main function 入口,最主要就是對 gl_FragColor 做操作。

CPU vs GPU

而一般在 CPU 執行的程式是由一個 Thread 持續處裡一個程式,GPU 則是交由多個 Thread 去一批一批處理各個像素(Pixel)。

CPU

CPU 用單一強大運算能力的核心 快速的處裡多個複雜的工作

GPU

GPU 用大量不強的核心 同時處裡多個複雜的工作

圖片來源:The Book of Shaders

上面的程式碼轉為 CPU 的大概就會變成


public static Color[] Pixels;

const int c_resolutionX = 1920;
const int c_resolutionY = 1080;


void Draw (int x, int y)
{
  var res = new Color((float)x / c_resolutionX, (float)y / c_resolutionY, 0f);

  Pixels[x * c_resolutionY + y] = res;
}

void Main ()
{
  Pixels = new float[c_resolutionX * c_resolutionY];
  while(true)
  {
    for (var x = 0; x < c_resolutionX; ++x)
    {
      for (var y = 0; y < c_resolutionY; ++y)
      {
        Draw(x, y);
      }
    }
  }
}

上面的程式碼的意思就是跑過一個 1920x1080 的畫面,每個 Pixel 的結果根據帶入的座標來算顏色

兩段程式碼結果都會得到下面的結果

結果

此外 GPU 執行時有兩個分常重要的限制

  • 核心是完全獨立的,無法知道其他核心現在的運行狀況。
  • 核心只有現在的狀況(Input),無法知道上一次執行的狀況(Input)

分析 Shader 內容

回到我們一開始的 Shader,我們該怎麼確定 gl_FragCoord 的值?與一般程式語言不同 Shader 並沒有任何的 Print 功能能幫我們知道 gl_FragCoord 的值,所以唯一能測試的方式就只有把結果印出來而已。

precision mediump float;

uniform vec2 u_resolution;

void main()
{
  vec3 color = vec3(gl_FragCoord, 0.);

  gl_FragColor = vec4(color, 1.0);
}

上面這個例子印出來的結果是什麼呢?

結果

嗯?怎麼會是全黃的呢?

gl_FragCoord 帶入的值是一個二維(vec2)的值,代表該 Pixel 的座標 (x, y),如下圖。

Pixel Coordinate

而在電腦科學裡顏色通常是由 3 個數字組成,分別代表紅藍綠(RGB);P.S.關於色彩空間的細節可以看之前的文章;而且在 Shader 裡顏色值的範圍是介於 0~1 之間,因此直接將 gl_FragCoord 拿來用的話 就會變成最左下的值是(0, 0, 0)黑色,最下面那一行是(1, 0, 0)紅色,最左邊那一列是(0, 1, 0)紅色,而剩下的區域都是(1, 1, 0)黃色,上面的結果仔細看其實有一條綠色跟紅色的線,把結果放大來看其實就會變成如下圖。

Pixel scale up

回到最一開始的程式裡有著這麼一個行為

  vec2 uv = gl_FragCoord.xy / u_resolution;

所做的事就是將寬 0 ~ 1919(1920 - 1)、高 0 ~ 1079(1080 - 1) 各自除以畫面的寬高(1920, 1080)將值縮放到 0 ~ 1 之間,得到紅藍各自 0 ~ 1 之間的混合,結果就如一開始的範例一樣,左到右紅色越來越多,下到上綠色越來越多。

結果

Shader 的範例

上段我們可以知道 Shader 最基本的 Input 值就只有一個 pixel 的座標,但就一個如此簡單的資訊,只要結合許多數學知識也可以創造出非常驚人的畫面效果。

Sample

Sample

Sample

學習資源

The Book of Shaders

The Book of Shaders有大量基礎的範例及程式碼,還提供了一個線上的編輯器讓使用者可以測試自己的。

Shadertoy

Shadertoy上有著各路大神所創造的成果,並且可以即時觀看、編寫程式碼,你會發現很多看起來很厲害的效果,背後的程式碼可能不超過 200 行;這裡很多作品最主要是運用了 Ray Marching 這個演算法,未來也會寫文章來介紹。

結語

在遊戲開發中「渲染」絕對是無法避免的一個基本要素,雖然大多數的遊戲引擎都將渲染封裝起來,令使用者可以不用懂得太多渲染的細節,就可以完成一款遊戲的開發,但若想要讓遊戲更有風格的話,使用 Shader 就是一個必要的工具。

GuiltyGear系列

GuiltyGear 系列的 Toon Shader 風格是業界翹楚。

Persona 3 Reload

Persona 3 Reload 的 Shader 加強了遊戲中那獨特世界觀的印象。

Minecraft

Minecraft 用 Shader 令看似簡單的方塊世界有不同的面貌。