iT邦幫忙

2021 iThome 鐵人賽

DAY 2
0

大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 2 篇文章。本系列文章從 WebGL 之基礎開始介紹,最後建構出繪製 3D、光影效果之網頁。本章節講述的是 WebGL 基本的運作機制以及如何使用其提供的功能

在讓電腦繪製一個三維場景時,我們實際在做的事情把這三維場景中物體的『表面』畫在畫面上,而構成一個面最少需要三個點,三個點構成一個三角形,而所有更複雜的形狀或是表面都可以用複數個三角形做出來,因此使用 3D 繪製相關的工具時基本的單位往往是三角形,我們就來使用 WebGL 畫一個三角形吧!

WebGL 的繪製流程

在使用 WebGL 時,你寫的主程式 (.js) 在 CPU 上跑,透過 WebGL API 對 GPU 『一個口令,一個動作』;不像是 HTML/CSS 那樣,給系統一個結構,然後系統會根據這個結構直接生成畫面。而且我們還要先告訴好 GPU 『怎麼畫』、『畫什麼』,講好之後再叫 GPU 進行『畫』這個動作

『怎麼畫』

我們會把一種特定格式的程式(program)傳送到 GPU 上,在『畫』的動作時執行,這段程式稱為 shader,而且分成 vertex(頂點)及 fragment(片段)兩種 shader,vertex shader 負責計算每個形狀(通常是三角形)的每個頂點在畫布上的位置、fragment shader 負責計算填滿形狀時每個 pixel 使用的顏色,兩者組成這個所謂特定格式的程式

『畫什麼』

除了 shader 之外,還要傳送給程式(主要是 vertex shader)使用的資料,在 shader 中這些資料叫做 attribute,並且透過 buffer 來傳送到 GPU 上

『畫』這個動作

首先執行 vertex shader,每執行一次產生一個頂點,且每次執行只會從 buffer 中拿出對應的片段作為 attribute,接著 GPU 會把每三個頂點組成三角形(模式是三角形的話),接著點陣化(rasterization)以對應螢幕的 pixel,最後為每個 pixel 分別執行 fragment shader

以接下來要畫的三角形為例,筆者畫了簡易的示意圖表示這個流程:

draw-flow

為什麼是這樣的流程其實筆者也不得而知,或許就是維基百科 openGL 頁面這邊所說的:『它是為大部分或者全部使用硬體加速而設計的』,稍微想像一下,每個頂點位置以及每個 pixel 著色的計算工作可以高度平行化,而在顯示卡硬體上可以針對這個特性使這些工作平行地在大量的 ALU / FPU 上同時計算以達到加速效果

建立 shader

當筆者第一次看到這個的時候,第一個反應是『原來可以在瀏覽器裡面寫 C 呀』,這個語言稱為 OpenGL Shading Language,簡稱 GLSL,雖然看起來很像 C 語言,但是不能直接當成 C 來寫,他有自己的資料格式,我們直接來看畫三角形用的 vertex shader:

attribute vec2 a_position;
 
void main() {
  gl_Position = vec4(a_position, 0, 1);
}
  • 每次 shader 執行時跑 void main()
  • attribute vec2 a_position 是從 buffer 拿出對應的部份作為 attribute 變數 a_position,型別 vec2 表示有兩個浮點數的 vector
    • 接下來要繪製的三角形在 2D 上,只需要 x, y 即可,因此使用 vec2
  • gl_Position 是 GLSL 規定用來輸出在畫布上位置的變數,其型別是 vec4
    • 這個變數的第一到第三個元素分別是 x, y, z,必須介於 -1+1 才會落在畫布中,這個範圍稱為 clip space
    • vec4() 建構一個 vec4,理論上應該寫成 vec4(x, y, z, w),因為 a_positionvec2,這邊有語法糖自動展開,所以也可以寫成 vec4(a_position[0], a_position[1], 0, 1)
    • 第四個元素我們先傳 1,到後面的章節再討論

假設有個 vec4 的變數叫做 var,不僅可以使用 var[i] 這樣的寫法取得第 i 個元素(當然,從 0 開始),還可以用 var.x / var.y / var.z / var.w 取得第一、第二、第三、第四個元素,甚至有種叫做 swizzling 的寫法:var.xzz 等同於 vec3(var[0], var[2], var[2])

這個 shader 其實沒做什麼事,只是直接把輸入到 buffer 的位置資料放到 gl_Position,接著是 fragment shader,這次更簡單了:

void main() {
  gl_FragColor = vec4(0.4745, 0.3333, 0.2823, 1);
}
  • 每個 pixel 都要跑一次 void main()
  • gl_FragColor 是 GLSL 規定用來輸出在畫布上顏色的變數,其型別是 vec4
    • 各個元素分別是介於 01 之間的 red, green, blue, alpha

為了不要讓資訊量太爆炸,我們先不要介紹更多功能,這個 fragment shader 只會輸出一種顏色,所以我們會得到的三角形是純色的

編譯、連結 shader 成為 program

由於 shader 建立的 WebGL API 實在太繁瑣,這邊直接建立兩個 function:

function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  const ok = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (ok) return shader;

  console.log(gl.getShaderInfoLog(shader));
  gl.deleteShader(shader);
}

function createProgram(gl, vertexShader, fragmentShader) {
  const program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);

  const ok = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (ok) return program;

  console.log(gl.getProgramInfoLog(program));
  gl.deleteProgram(program);
}

我們可以分別把 vertex shader, fragment shader 的 GLSL 原始碼以 template literals (backtick 字串) 寫在 .js 中,並傳給 createShader(gl, type, source)source 進行『編譯』:

const vertexShaderSource = `
attribute vec2 a_position;
 
void main() {
  gl_Position = vec4(a_position, 0, 1);
}
`;
 
const fragmentShaderSource = `
void main() {
  gl_FragColor = vec4(0.4745, 0.3333, 0.2823, 1);
}
`;

const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

編譯完成後,使用 createProgram(gl, vertexShader, fragmentShader) 把 GPU 內『怎麼畫』的流程串起來:

const program = createProgram(gl, vertexShader, fragmentShader);

這樣一來 program 就建立完成,下一篇我們再繼續『畫什麼』的資料部份

本篇的完整程式碼可以在這邊找到:


上一篇
Hello WebGL
下一篇
畫一個三角形(下)
系列文
如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
jerrythepotato
iT邦新手 4 級 ‧ 2021-10-19 22:01:50

身為初學者有點混淆,
請問a_position是自己定義的名稱還是GLSL定義的?

還有文中提到:

attribute vec2 a_position 是
「從 buffer 拿出對應的部份」
作為 attribute 變數 a_position

請問是從buffer中拿出對應「誰」的部分?會這樣問主要是小弟懂得結構不多,大概只知道JS物件會有key對應value,所以object.key就很好理解,就是從 object 拿出對應key的變數

PastLeo iT邦新手 4 級 ‧ 2021-10-20 15:34:51 檢舉

首先感謝閱讀與提問,a_position 這個變數名稱是開發者(我們)定義的,關於

buffer中拿出對應「誰」的部分

可以參考下篇文:畫一個三角形(下),這篇有針對 buffer 與 attribute 之關聯做說明

我要留言

立即登入留言