1111 計算機圖學

這篇文章是學習時整理的一些筆記,讓自己複習時方便,文章內容為上課之內容與閱讀清單之整理


OpenGL / WebGL

OpenGL

OpenGL是一個開放圖形庫,用於提供基於二維或者三維畫面渲染的一個程式介面,是一個用來和顯示卡溝通的API規範,該API會被用於和GPU交互,以便達成硬件加速的渲染。

WebGL

WebGL則是JavaScript的API,瀏覽器實作了OpenGL ES,將OpenGL和Javascript結合,讓能藉由OpenGL的幫助支援於瀏覽器環境中。

GLSL

GLSL 全稱為 OpenGL Shading Language ,是用來在OpenGL中編寫著色器程序的語言。

建立與設定畫布

canvas 是 WebGL 操作的畫布,畫出來的東西會在這個標籤中呈現

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello-webgl</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="webgl.js"></script>
</body>
</html>
1
2
3
4
5
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');

gl.clearColor(108/255, 225/255, 153/255, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

canvas因為沒有設定長寬,因此預設為300x150
gl.clearColor(red, green, blue, alpha)用來設定畫布清除用的顏色,值均介於0~1之間
設定好後gl.clear(gl.COLOR_BUFFER_BIT)進行清除

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 - reference

建立Shader

在 Javascript 中可以直接寫 GLSL,來建立 shader
Shader 範例:

vertex shader

1
2
3
4
5
attribute vec2 a_position;

void main() {
gl_Position = vec4(a_position, 0, 1);
}

gl_Position是 GLSL 規定輸出在畫布上位置的變數,型別是 vec4,第一到第三個元素分別是 x, y, z,必須介於 -1 至 +1 才會落在畫布中

  • vec4() 建構一個 vec4,因為 a_position 是 vec2,語法可以自動展開,也可寫 vec4(a_position[0], a_position[1], 0, 1)
1
2
3
4
5
in vec4 a_position;

void main() {
gl_Position = a_position;
}

上面這個範例則是有一vec4作為輸入

fragment shader

1
2
3
void main() {
gl_FragColor = vec4(0.4745, 0.3333, 0.2823, 1);
}

gl_FragColor 是 GLSL 規定輸出在畫布上顏色的變數,型別是 vec4

1
2
3
4
5
6
7
8
precision highp float;

uniform vec4 u_color;
out vec4 outColor;

void main() {
outColor = u_color;
}

上面這個範例則是有一個輸出

我們可以分別把 vertex shader, fragment shader 的 GLSL 原始碼以 template literals (backtick 字串) 寫在js中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
`;

編譯、連結Shader

建立shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function createShader(gl, type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}

console.log(gl.getShaderInfoLog(shader)); // eslint-disable-line
gl.deleteShader(shader);
return undefined;
}

function createProgram(gl, vertexShader, fragmentShader) {
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}

console.log(gl.getProgramInfoLog(program)); // eslint-disable-line
gl.deleteProgram(program);
return undefined;
}

編譯

建立了 shader 並連結成 program

1
2
3
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vertexShader, fragmentShader);

取得Position

以下概念會有點類似於pointer,會有一個buffer,並有一個pointer控制每次存取的位置與數量

取得 Attribute 位置

gl.getAttribLocation 可以取得 attribute 在 program 中的位置

1
const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');

建立並使用 Buffer

gl.createBuffer() 建立 buffer,並使用 gl.bindBuffer() 設定目前使用中的 array buffer,設定完成後,接下來呼叫的其他 WebGL API 就會對著設定好的目標做事

1
2
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

Vertex Attribute Array

1
gl.enableVertexAttribArray(positionAttributeLocation);

設定 attribute 取值方法

1
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);

positionAttributeLocation 設定的 attribute 位置
size設定了每次 vertex shader 執行時 attribute 要從 buffer 中拿出多少個數值,依序填入 vec 的各個元素,在先前範例第一個中因為是設定 vec2,因此這邊使用 2 剛好填滿 shader 中的 attribute vec2 a_position
type原始資料的型別,此範例為 gl.FLOAT
normalize可以把資料除以該型別的最大值使 attribute 變成介於 -1 ~ +1 之間的浮點數,不使用此功能傳 false 進去即可
strideoffset控制讀取 buffer 時的位置,stride 表示這次與下次 vertex shader 執行時 attribute 讀取的起始位置的距離,設定為 0 表示每份資料是緊密排列的,offset 則是第一份資料距離開始位置的距離,這兩個參數的單位皆為 byte

上述以經大致介紹完流程了,接下來重要的當然就是把要畫的位置資料傳入buffer~

傳入資料到 buffer,設定位置

1
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
  • 第二個參數即為 buffer 的資料,要傳入與 gl.vertexAttribPointer() type 符合的 TypedArray

畫出資料

1
2
gl.useProgram(program);
gl.drawArrays(gl.TRIANGLES, 0, 3);

因此整體流程為下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var positionAttributeLocation = gl.getAttribLocation(program, "a_position"); //取得 Attribute 位置

var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); //建立並使用 Buffer

var positions = generate_triangle_point();
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); //傳入資料到 buffer,設定位置

var vao = gl.createVertexArray();
gl.bindVertexArray(vao);

gl.enableVertexAttribArray(positionAttributeLocation); //Vertex Attribute Array
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset); //設定 attribute 拿資料的方法

gl.useProgram(program);
gl.drawArrays(gl.TRIANGLES, 0, 3); //畫出資料

整體流程圖

在原始資料buffer內的每個值會經過shader後產生對應的clipspace,因此若是有九個點,則shader會被調用9次
若是drawarray地方繪畫的是gl.Triangles,則GPU會每三個點製作一個三角形

在不同shader間傳遞變量可以用varying,WebGL 會將頂點著色器中的變量連接到片段著色器中具有相同名稱和類型的變量
因此以下有一個小小範例如何使用不同位置產稱不同顏色(同樣位置需對應相同顏色):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var vs = `#version 300 es
in vec2 a_position;

uniform mat3 u_matrix;

out vec4 v_color;

void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);

// Convert from clipspace to colorspace.
// Clipspace goes -1.0 to +1.0
// Colorspace goes from 0.0 to 1.0
v_color = gl_Position * 0.5 + 0.5;
}
`;

var fs = `#version 300 es

precision highp float;

in vec4 v_color;

out vec4 outColor;

void main() {
outColor = v_color;
}
`;

範例中我們使用位置資訊產生顏色,而相同變量v_color會被連結,
v_color = gl_Position * 0.5 + 0.5:由於位置資訊為-1+1,而顏色為01,因此先*0.5再+0.5

若是我們想傳遞更多資訊給fragment shader,我們可以將更多數據傳遞給頂點著色器,然後我們可以將其傳遞給片段著色器,下方範例為增加a_color

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
const vertexShaderSource = `
in vec2 a_position;
in vec4 a_color;
...
out vec4 v_color;

void main() {
...
// Copy the color from the attribute to the varying.
v_color = a_color;
}
`;

var fragmentShaderSource = `#version 300 es

precision highp float;

in vec4 v_color;

out vec4 outColor;

void main() {
outColor = v_color;
}
`;

const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
const colorAttributeLocation = gl.getAttribLocation(program, 'a_color');

// a_position
// ...



// a_color
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);

// Pick random colors.
var r1 = Math.random();
var b1 = Math.random();
var g1 = Math.random();

gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(
[ r1, b1, g1, 1,
r1, b1, g1, 1,
r1, b1, g1, 1]),
gl.STATIC_DRAW);

gl.enableVertexAttribArray(colorAttributeLocation);
gl.vertexAttribPointer(
colorAttributeLocation,
3, // size
gl.UNSIGNED_BYTE, // type
true, // normalize
0, // stride
0, // offset
);
// ...

function setColors(gl) {

}

gl.vertexAttribPointer() 因為顏色有 3 個 channel,因此對於每個頂點 gl.bufferData() 要給 3 個值
每個頂點有一組 (x, y) 座標值 a_position 以及顏色資料 a_color

GLSL 三種變量

  1. uniform:uniform變量是外部程式傳遞給shader的變量,在shader内部,uniform像是C語言的常量(const ),它不能被shader修改。(shader只能用,不能改),一般用來表示矩陣變換、光照參數、和顏色等訊息

  2. attribute:attribute是只能在vertex shader中使用。(它不能在fragment shader中宣告,也不能被fragment shader中使用),一般用来表示一些頂點數據

  3. varying:varying是shader之間傳遞數據用的。一般vertex shader修改varying的值,然后fragment shader使用值。因此varying在vertex和fragment shader兩者之間的宣告必須一致

要讓不同三角形有不同的顏色,要思考的是輸入資料/參數給 fragment shader 的方式,在 fragment shader 中可以使用 uniform,但是那樣的話所有三角形的顏色依然會是一樣,得用類似 attribute / buffer 『每次 shader 呼叫不同』的東西,不過 fragment shader 中是不能使用 attribute 的功能的,回想 Day 2 fragment shader 的運作方式:fragment shader 是每個 pixel 執行一次,不像是 vertex shader 以頂點為單位,取用 array buffer 的方式顯然對不起來,因此需要另外一種傳輸工具 – varying - reference

varying 這功能可以讓 vertex shader 輸出資料給 fragment shader 使用,但是兩者執行的回合數顯然是對不起來,vertex shader 執行三次得到三個頂點,執行一次 fragment shader 計算顏色,vertex #1 輸出一組資料、vertex #2 輸出一組資料、vertex #3 輸出一組資料,那麼 fragment #2, fragment #3, fragment #4 這些介於中間 pixel 執行的 fragment shader 會拿到什麼資料?答案是:WebGL 會把頂點與頂點之間輸出的 varying 做平滑化! - reference

Canvas 尺寸設定

繪製之前,我們應該調整畫布的大小以匹配它的顯示大小。由於在網頁上座標與WebGl座標系統不一樣,因此需要轉換。

1
2
3
webglUtils.resizeCanvasToDisplaySize(gl.canvas);

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
1

閱讀清單

1.Hello WebGL
2.GLSL 三种变量类型(uniform,attribute和varying)理解