---
tags: 圖學
---
# WebGL 學習手冊
> 此手冊為學習 WebGL编程指南 與 WebGL 基礎概念 的學習心得文件.
> [WebGL编程指南](https://www.tenlong.com.tw/products/9787121229428?list_name=srh)
> [WebGL Beginner's Guide](https://www.tenlong.com.tw/products/9781849691727)
> [WebGL 基礎概念](https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-fundamentals.html)
> [The Book Of Shaders](https://thebookofshaders.com/?lan=ch)
> [阿凱Shader入門](https://docs.google.com/presentation/d/1PG7e9a2y9YFi6wy5ljuNge_q1MYnXCe1_LZCRxwnua0/edit#slide=id.g17fd6042b3_0_43)
> [史丹佛VR課](https://stanford.edu/class/ee267/lectures/lecture3.pdf)
> [圖解渲染流程](https://zhuanlan.zhihu.com/p/137780634)
> [3D图形学基础 - 柯灵杰](https://zhuanlan.zhihu.com/p/27846162)
> [20分钟让你了解OpenGL](https://zhuanlan.zhihu.com/p/56693625)
> [color=#3b75c6]
> [Real-Time-Rendering-4th 中譯版](https://www.wolai.com/dpaCHvw9WZRaB3aM2BzF9j)
> [color=#F7A004]
## 著色器 shader
### 初始化著色器 initialize shader
1. getCanvas (Html canvas element)
2. getWebGLContext
3. 初始化著色器
4. setCanvasBGColor
5. gl.clearColor + gl.clear(gl.COLOR_BUFFER_BIT)
6. Render ur shader
### 頂點著色器 Vertex Shader
* vec4 gl_Position 表頂點位置 (必須值)
* float gl_PointSize 表頂點大小 (pixel 數量)
> gl_Position = vec4(0.0, 0.0, 0.0, 1.0)
> 這當中的`1.0` ,也就是第四的參數值被稱為齊次座標,當其為1.0可以視為三維座標中的點

[color=#42B983]
### 片元著色器 Fragment Shader
* vec4 gl_FragColor 指定片元顏色 (RGBA格式)
## WebGL 座標系統

## shader 變數
### attribute 變數
attribute 被稱為`存除限定符` (storage qualifer),其必須聲明為全域變數。
```
attribute vec4 a_Position;
存儲限定符 類型 變數名
```
> attribute 變數以 a_ 前綴開頭,uniform 變數以 u_ 前綴開頭,本書以此為編碼風格

* `gl.getAttribLocation(program, name)` 取得 name 參數指定的 attribute 記憶體位置
* `gl.vertexAttrib3f(location, v1, v2, v3)` 將(v1,v2,v3)傳給 location 指向的 attribute 變數

> **只有頂點著色器能使用 attribute 變數,使用片元著色器就需要用 uniform 變數或是使用 varying 變數**
> [color=#42B983]
### uniform 變數

> uniform 變數用來傳輸給 Vertex Shader、Fragment Shader,`一致的`數據
> [color=#42B983]
```
uniform vec4 u_FragColor;
存儲限定符 類型 變數名
```
### varying 變數
#### 畫彩色三角形
頂點著色器中的 v_Color 變量在傳入片元著色器之前經過了內插過程。所以片元著色器中的 v_Color 變量和頂點著色器中的 v_Color 變量實際上並不是一回事,這也正是我們將這種變量稱為“varying”(變化的)變量的原因。

WebGL2 後請用 `in`、`out`
> 在GL3.x中,廢棄了attribute關鍵字(以及varying關鍵字)
> 屬性變量統一用in/out作為前置關鍵字 [color=#42B983]
## 緩衝區物件 Buffer Object
緩衝區物件(buffer object),可以一次向著色器傳入多個頂點,它是WebGL中的一塊記憶體區域,可以一次性地向 BO 填充大量的頂點資料,然後將資料傳給著色器
### Buffer Object 使用流程

1. gl.createBuffer()
2. gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
3. gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
4. gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
5. gl.enableVertexAttribArray(a_Position)

### 1. 創建多個緩衝區對象
vertex buffer & size buffer

### 2. 透過 gl.vertexAttribPointer 傳入多項資訊
創建 vertexSize buffer ,將 2x3 與 1x3 的 array 合成為 3x3的 Float32Array,在透過 `gl.vertexAttribPointer` 的 `stride` 與 `offset` 去取資料
### 類型化陣列
需要用`new`來調用構造函數來建構並傳入參數


## 基本圖形API
`gl.drawarrys`當中的參數 mode,能繪出7種主要基本圖形
| 基本圖形 | 參數 mode | 描述 |
| ------ | ------ | ----------- |
|點 |gl.POINTS| 一系列點,繪製在v0,v1,v2......處
|線段 | gl.LINES |一系列`單獨的線段`,繪製在(v0,v1)、(v2,v3)、(v4,v5)... 處,如果點的個數是奇數,最後一個點將被忽略
|線條 | gl.LINE_STRIP |一系列`連接的線段`,被繪製在(v0,vl)、(v1 v2)、(v2,v3)... 處,第1個點是第1條線段的起點,第2個點是第1條線段 的終點和第2條線段的起點......第i(i>1)個點是第i-1 條線 段的終點和第i條線段的起點,以此類推。最後一個點是最後 一條線段的終點
|迴路 | gl.LINE_LOOP |一系列`連接的線段`。與 g1.LINE_STRIP 繪製的線條相比,增 加了一條從最後一個點到第1個點的線段。因此,線段被繪製 在(v0,vl)、(v1,v2) (vn,v0)處,其中vn是最後一個 點
|三角形 | gl.TRIANGLES |一系列`單獨的三角形`,繪製在(v0,v1,v2)、(v3,v4,v5)... 處。如果點的個數不是3的整數倍,最後剩下的一或兩個點將 被忽略
|三角帶 | gl.TRIANGLE_STRIP |一系列`條帶狀的三角形`,前三個點構成了第1個三角形,從 第2個點開始的三個點構成了第2個三角形(該三角形與前一個三角形共享一條邊),以此類推。這些三角形被繪製在 (v0,v1,v2)、(v2,v1,v3)、(v2,v3,v4)............處(注意點的順序)'
|三角扇 | gl.TRIANGLE_FAN |一系列三角形組成的類似於扇形的圖形。前三個點構成了第1 個三角形,接下來的一個點和前一個三角形的最後一條邊組 成接下來的一個三角形。這些三角形被繪製在(v0,v1,v2)、 (v0,v2,v3), (v0,v3,v4). 處

## 平移、旋轉、縮放、向量與變換矩陣
### 平移 (向量)
```javascript
...
const vs = `
attribute vec4 a_Position;
uniform vec4 u_Transition;
void main() {
gl_Position = a_Position + u_Transition;
}
`;
var u_Transition = gl.getUniformLocation(gl.program, 'u_Transition');
gl.uniform4f(u_Transition, .5, .5, 0.0, 0.0);
...
```
### 旋轉 (向量)
``` javascript
/** 旋轉公式
* x2 = x1 * cosθ - y1 * sinθ
* y2 = x1 * sinθ + y1 * cosθ
* z2 = z1
*
* 轉換矩陣 transformation matrix (以旋轉矩陣為例)
* [x2,y2,z2] = [ a b c [ x
* d e f * y
* g h i ] z ]
* 展開後可得
* x2 = ax + by + cz
* y2 = dx + ey + fz
* z2 = gx + hy + iz
*
* 將其與旋轉公式對應可得
* [x2,y2,z2] = [ cosθ -sinθ 0 [ x
* sinθ cosθ 0 * y
* 0 0 1 ] z ]
**/
const vs = `
attribute vec4 a_Position;
uniform float u_CosB, u_SinB;
void main() {
gl_Position.x = a_Position.x * u_CosB - a_Position.y * u_SinB;
gl_Position.y = a_Position.x * u_SinB + a_Position.y * u_CosB;
gl_Position.z = a_Position.z;
gl_Position.w = 1.0;
}
`;
```
### 變換矩陣
``` javascript
// 因 按列主序(column major order)行列轉置過來,才能讓 WebGL 由上而下的將原矩陣內數據
// 儲存到陣列中!
// 旋轉矩陣
var xformMatrix = new Float32Array([
cosB, sinB, 0.0, 0.0,
-sinB, cosB, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
]);
// 平移矩陣
var xformMatrix = new Float32Array([
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
x, y, z, 1.0,
]);
// 縮放矩陣
var xformMatrix = new Float32Array([
sx, 0.0, 0.0, 0.0,
0.0, sy, 0.0, 0.0,
0.0, 0.0, sz, 0.0,
0.0, 0.0, 0.0, 1.0,
]);
// 上面的 xformMatrix 選其一套入 其它請註釋掉
var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
const vs = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
gl_Position = u_xformMatrix * a_Position;
}
`;
```
### 平移後旋轉
>等式4.1
> <“平移”後的座標 >=<平移矩陣>×<原始座標>
然後對<平移後的座標進行旋轉。
>等式 4.2
> <“平移後旋轉”後的座標 >=<旋轉矩陣>×<平移後的座標>
當然你也可以分步計算這兩個等式,但更好的方法是,將4.1代入到4.2中, 把兩個等式組合起來:
>等式 4.3
> <“平移後旋轉”後的座標>=<旋轉矩陣>×(<平移矩陣>×<原始座標>)
這裡
> <旋轉矩陣>×(<平移矩陣>×<原始坐標 >)
等於
(注意括號的位置)
> (< 旋轉矩陣>×<平移矩陣>)×<原始坐標>
最後,我們可以在JavaScript 中計算<旋轉矩陣><平移矩陣>,然後將得到的 矩陣傳入頂點著色器。像這樣,我們就可以把多個變換複合起來了。一個模型經過多個轉換,將多個轉換合成一個等效的轉換,就得到了`模型轉換(model transformation)` 或稱之為 `建模轉換(modeling transformation)`,相對應地模型轉換的矩陣稱之為`模型矩陣(model matrix)`
## 圖形裝配與光柵化
* **圖形裝配** : 將孤立的頂點座標裝配成幾何圖案 , 幾何圖形裝配 (geometric shape assembly)階段將 gl_Position 中的點,畫成 mode 參數中指定的圖案, 又稱為 primitives assembly process。 因為被畫出來的基本圖形又稱之為[圖元(primitives)](https://en.wikipedia.org/wiki/Geometric_primitive)
* **光柵化** : 將裝配的幾何圖案轉化為片元/點陣。EX.[字形光柵化](https://zh.wikipedia.org/zh-tw/%E5%AD%97%E4%BD%93%E5%85%89%E6%A0%85%E5%8C%96)

## 紋理 Texture
### 紋理座標

> 紋理座標很通用,因為座標值與圖片本身的尺寸無關。
> EX. 128x128 或 128x256 的右上角紋理座標都是 (1.0,1.0)
> [color=#42B983]
### 紋理映射

> 另一種常用的命名習慣是用 `uv` 當作紋理座標名稱。這邊用 `st` 是因為GLSL ES 也用 st 分量名取得紋理
> [color=#42B983]
### 設定紋理座標
``` javascript
function initVertexBuffers(gl) {
const verticesTexCoords = new Float32Array([
-.5, .5, /**/ 0.0, 1.0,
-.5, -.5, /**/ 0.0, 0.0,
.5, .5, /**/ 1.0, 1.0,
.5, -.5, /**/ 1.0, 0.0
]);
const len = verticesTexCoords.length / 4;
// 1. 創建 buffer
const vertexTexCoordBuffer = gl.createBuffer();
if (!vertexTexCoordBuffer) return -1;
// 2. Binfing buffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
// 3. Setting buffer obj data
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
const FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
// 4. Assign attribute form buffer & enable vertex attribute
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
gl.enableVertexAttribArray(a_Position)
const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord)
return len;
}
```
### 配置和加載紋理
``` javascript
function initTexture(gl, n) {
// 1. 創建紋理 Buffer
const texture = gl.createTexture();
// 取得Shader 的紋理變數
const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
// 創建圖片
const image = new Image();
image.onload = () => {
// Y軸反轉
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 2. 開啟 Texture Unit (WebGL 至少可用 8個 Texture Unit)
gl.activeTexture(gl.TEXTURE0);
// 3. 綁定紋理 (申請2D 紋理空間)
gl.bindTexture(gl.TEXTURE_2D, texture);
// 4. 紋理縮放參數
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 5. 寫入紋理資料 到 紋理 Buffer
// (image 如果是png,圖像格式&紋理格式 需設定 RGBA)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
// 6. 將紋理 Buffer 傳至 shader 變數
// 將0號紋理傳遞給著色器
gl.uniform1i(u_Sampler, 0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);
}
image.src = 'imgs/sky.jpg';
}
```
#### 反轉 y 軸

#### 開啟紋理單元 (gl.activeTexture())


#### 綁定紋理 (gl.bindTexture())

#### 配置紋理對象參數 (gl.texParameteri())
``` javascript
/** gl.texParameteri
* 將 param 的值賦予給目標紋理物件(target)的 pname 參數上
* target: gl.TEXTURW_2D / gl.TEXTURE_CUBE_MAP
* pname: gl.TEXTURE_MAG_FILTER|gl.TEXTURE_MIN_FILTER|gl.TEXTURE_WRAP_S|gl.TEXTURE_WRAP_T
* param: GLint (OpenGL ES 定義的 number)
**/
texParameteri(target: GLenum, pname: GLenum, param: GLint): void;
```
4種效果與預設對應的默認值

在 gl.TEXTURE_MAG_FILTER / gl.TEXTURE_MIN_FILTER 使用的param常數

在 gl.TEXTURE_WRAP_S / gl.TEXTURE_WRAP_T 使用的param常數

> 
> [color=#42B983]
#### 將紋理貼圖分配給紋理物件 (gl.texImage2D())
[MDN - WebGLRenderingContext.texImage2D()](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texImage2D)
``` javascript
gl.texParameteri(target, level, internalformat, format, type, source);
```
* **target** 目標的紋理物件,這邊主要是 `gl.TEXTURE_2D`, `gl.TEXTURE_CUBE_MAP` 三維紋理暫不討論
* **level** 直接用 0 就好了,因為我們沒用到金字塔紋理。
* **internalformat** 表示 the color components in the texture. 在 WebGL 中, internalformat 和 format 必須一致。In WebGL 2, the combinations are listed in [this table](https://registry.khronos.org/webgl/specs/latest/2.0/#TEXTURE_TYPES_FORMATS_FROM_DOM_ELEMENTS_TABLE)
* **format** 表示 the format of the texel data , 具體取值如表5.6所示,你必需根據紋理圖像的格式來選擇這個參數。範例中使用的紋理圖片是JPG格式的,該格式將每個像素用RGB三個分量來表示,所以我們將參數指定為g1.RGB。對其他格式的圖像,如 PNG ,通常使用gl.RGBA, BMP 通常使用g1.RGB,而 g1.LUMINANCE 和g1.LUMINANCE_ALPHA 通常用在灰階圖上等等。

> 這裡的流明(luminance)表示我們感知到的物體表面的亮度。通常使用物體表面紅、 綠、藍顏色分量值的加權平均來計算流明。
> [color=#42B983]
* **type** 指定紋理數據類型,見表5.7。通常我們使用 `g1.UNSIGNED BYTE` 數據類型。當然也可以使用其他數據類型,如 `gl.UNSIGNED_SHORT_5_6_5` (將RGB三分量壓縮入16bits 中)。後面的幾種數據格式通常被用來壓縮數據,以**減少瀏覽器加載圖像的時間**。

#### 將紋理單元傳遞給 fragment shader (gl.uniform1i())
一但將紋理圖像傳入了 WebGL,就必須將其傳入片元著色器,映射到圖形的表面上去。
另外我們使用 uniform 變數來表示紋理,因為紋理圖像不會隨著片元變化。

#### 從 vs 向 fs 傳輸紋理座標
``` javascript
const vs = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {
gl_Position = a_Position;
v_TexCoord = a_TexCoord;
}
`;
```
#### 在 fs 取 紋理像素顏色
``` javascript
const fs = `
precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`;
```

### 使用多張紋理貼圖

### 進階
[Real-Time Rendering 3rd - Texturing](https://github.com/QianMo/Game-Programmer-Study-Notes/tree/master/Content/%E3%80%8AReal-Time%20Rendering%203rd%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/Content/BlogPost05)
## 3D 世界
### 視點 Eye point、 視線 viewing direction、觀察點 look-at point、上方向 up direction
* **視點**: 觀察者所處的位置點
* **視線**: 從視點沿觀察方向的射線
* **觀察方向**: 觀察者自己在的位置相對於場景的方向
* **可視距離**: 視點 到 視線上最遠的點 之間的距離
* **觀察目標點**: 即 注視點
* **上方向**: 視點與觀察點之間連線的向上法線方向
### 視圖矩陣

### 可視空間
由水平、垂直、深度視角去定義 **可視空間(view volume)**
#### 2D: 正交投影 3D: 透視投影

#### 盒狀可視空間

#### 透視投影可視空間

可視空間這塊區域又稱之為 [frustum 錐體](https://zh.wikipedia.org/zh-tw/%E9%94%A5%E5%8F%B0)
### 投影矩陣

### 深度測試 / 隱藏面消除
1. 開啟隱藏面消除功能 `gl.enable(gl.DEPTH_TEST)`
2. 在繪製之前,清除深度緩衝區 `gl.clear(gl.DEPTH_BUFFER_BIT)`


### 裁剪(Clipping)
在經過投影過程把頂點坐標轉換到裁剪空間後,GPU就可以進行 Clipping。
Clipping 的目的就是把 Camera 看不到的頂點剔除出去,使他們不被渲染到。
判斷頂點是否可以免受裁剪也十分簡單,只需要滿足

在 Clipping 後,GPU 需要把頂點映射到屏幕空間,這是一個從三維空間轉換到二維空間的操作,更符合大家對“投影”的理解。
對透視裁剪空間來說,GPU需要對裁剪空間中的頂點執行齊次除法(其實就是將齊次坐標系中的w分量除x、y、z分量),得到頂點的歸一化的設備坐標(Normalized Device Coordinates, NDC),經過齊次除法後,透視裁剪空間會變成一個x、y、z三個坐標都在[-1,1]區間內的立方體。

對於正交裁剪空間就要簡單得多,只需要把w分量去掉即可。
此時頂點的x、y坐標就已經很接近於它們在屏幕上所處的位置了,不過還有一個多出來的z分量,不過它也不會被白白丟棄,而是被寫入了深度緩衝(z-buffer)中,可以做一些有關於頂點到Camera距離的計算。
### 深度衝突 (z-fighting)
``` javascript
function main() {
...
// Enable the polygon offset function
gl.enable(gl.POLYGON_OFFSET_FILL);
// Draw the triangles
gl.drawArrays(gl.TRIANGLES, 0, n/2); // The green triangle
// Set the polygon offset
gl.polygonOffset(1.0, 1.0);
gl.drawArrays(gl.TRIANGLES, n/2, n/2); // The yellow triangle
}
```
[詳解](https://juejin.cn/post/7049621665751728142#heading-13)
在 camera 的 near 和 far 的相差非常大的時候,更加容易引起 z-fighting問題。

## 光照
### 光照原理
所有物體都是由物體反射光線進到瞳孔,進而產生的成像,才能夠看見物體並辨別顏色。
當光照到物體時,有兩個重要現象:
* 根據光源和光線方向,物體表面的明暗程度不一樣
* 根據光源和光線方向﹑物體產生陰影


### 光源類型
**平行光光**: 平行光的光線是相互平行的,平行光具有方向。平行光可以看 作是無限遠處的光源(比如太陽)發出的光。因為太陽距離地球很遠,所以陽光到達地 球時可以認為是平行的。平行光很簡單,可以用一個方向和一個顏色來定義。
**點光源**: 點光源光是從一個點向周圍的所有方向發出的光。點光源光可以用來表 示現實中的燈泡、火焰等。我們需要指定點光源的位置和顏色。光線的方向將根據點光 源的位置和被照射之處的位置計算出來,因為點光源的光線的方向在場景內的不同位置 是不同的。
**環境光**: 環境光(間接光)是指那些經光源(點光源或平行光源)發出後,被牆壁 等物體多次反射,然後照到物體表面上的光。環境光從各個角度照射物體,其強度都是一致的。比如說,在夜間打開冰箱的門,整個廚房都會有些微微亮,這就是環境光的作用。環境光不用指定位置和方向,只需要指定顏色即可。

### 反射類型
#### 漫反射
#### 環境反射
### 逆轉置矩陣
[轉置矩陣](https://zh.wikipedia.org/zh-tw/%E8%BD%AC%E7%BD%AE%E7%9F%A9%E9%98%B5)
[逆矩陣](https://zh.wikipedia.org/zh-tw/%E9%80%86%E7%9F%A9%E9%98%B5)
## Advanced Techniques
### 用滑鼠使物體旋轉
### 選中一個表面
### 霧化
### Alpha 混和 (Alpha Blending)
### 偵緩衝物件 & 渲染緩衝物件 (Framebuffer Object and Renderbuffer Object)
By default, the WebGL system draws using a color buffer and, when using the hidden
surface removal function, a depth buffer. ***The final image is kept in the color buffer***.
The `framebuffer object` is an alternative mechanism you can use instead of a `color buffer` or a `depth buffer` ( Figure 10.18 ). Unlike a color buffer, the content drawn in a framebuffer object is not directly displayed on the ``<canvas>``. Therefore, you can use it if you want to perform different types of processing before displaying the drawn content. Or you can use it as a texture image. Such a technique is often referred to as `offscreen drawing` .


The ***framebuffer object*** has the structure shown in Figure 10.19 and ***supports substitutes for the color buffer and the depth buffer***. As you can see, drawing is not carried out in the framebuffer itself, but in the drawing areas of ***the objects that the framebuffer points to***.
These objects are attached to the framebuffer using its **attachment** function. A **color attachment** specifies the destination for drawing to be a replacement for the color buffer. A **depth attachment** and a **stencil attachment** specify the replacements for the depth buffer and stencil buffer.


``` javascript
function initFramebufferObject(gl) {
var framebuffer, texture, depthBuffer;
// Define the error handling function
var error = function() {
if (framebuffer) gl.deleteFramebuffer(framebuffer);
if (texture) gl.deleteTexture(texture);
if (depthBuffer) gl.deleteRenderbuffer(depthBuffer);
return null;
}
// 1. Create a frame buffer object (FBO)
framebuffer = gl.createFramebuffer();
if (!framebuffer) {
console.log('Failed to create frame buffer object');
return error();
}
// 2. Create a texture object and set its size and parameters
texture = gl.createTexture();
if (!texture) {
console.log('Failed to create texture object');
return error();
}
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// Store the texture object into FBO
framebuffer.texture = texture;
// 3. Create a renderbuffer object
depthBuffer = gl.createRenderbuffer();
// 4. Bind the renderbuffer object to the target and set its size (must equal to texture size)
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT);
// 5. Attach the texture object to the color attachment of the framebuffer object
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// WebGL COLOR_ATTACHMENT only cab be COLOR_ATTACHMENT0, WebGL2 can be more
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0 /* level (MIPMAP tex level)*/);
// 6. Attach the renderbuffer object to the depth attachment of the framebuffer object
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
// 7. Check if FBO is configured correctly
var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (gl.FRAMEBUFFER_COMPLETE !== e) {
console.log('Frame buffer object is incomplete: ' + e.toString());
return error();
}
// Unbind the buffer object
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
return framebuffer;
}
```
1. createFramebuffer
2. createTexture
3. [gl.createRenderbuffer](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/createRenderbuffer)
4. [gl.bindRenderbuffer](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bindRenderbuffer) / [gl.renderbufferStorage()](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/renderbufferStorage)
5. [gl.bindFramebuffer](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bindFramebuffer) / [gl.framebufferTexture2D()](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/framebufferTexture2D)
6. [gl.framebufferRenderbuffer](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/framebufferRenderbuffer)
7. [gl.checkFramebufferStatus](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/checkFramebufferStatus)
8. draw(webgl, canvas, fbo, plane, cube, angle, texture, viewProjMatrix, viewProjMatrixFBO)
``` javascript
function draw(gl, canvas, fbo, plane, cube, angle, texture, viewProjMatrix, viewProjMatrixFBO) {
// Change the drawing destination to FBO
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
// Set a viewport for FBO
gl.viewport(0, 0, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT);
// Clear FBO
gl.clearColor(0.2, 0.2, 0.4, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
drawTexturedCube(gl, gl.program, cube, angle, texture, viewProjMatrixFBO);
// Change the drawing destination to color buffer
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// Set the size of viewport back to that of <canvas>
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
drawTexturedPlane(gl, gl.program, plane, angle, fbo.texture, viewProjMatrix);
}
```
### 陰影
我們需要兩個著色器來實現陰影
1. 用來計算光源到物體的距離,並用一張 texture 將深度結果記錄起來
2. 根據(1)中計算的距離繪製場景,並將(1)的 texture 結果傳到下一個 shader 中,來繪製畫面中的陰影
這張紋理貼圖就被稱作為 `陰影貼圖 (shadow/depth map) `
而通過陰影貼圖實現陰影的方法就被稱為 `陰影映射 (shadow mapping)`
[陰影映射 (shadow mapping)](https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping) 包括以下兩步:
1. 將視點(eye-point)移到光源處,並運行(1)的 shader 。此時那些"將被繪出"的片元就是被光照射到的,即此格像素上位於最前面的片元。我們不會畫出其片元的顏色,而是傳遞片元中的 z值(深度值)到 "陰影貼圖" (FBO.texture <- the depth buffer.)
2. 將視點(eye-point)移回原位置,運行(2)的 shader 。此時每個片元在光源坐標系下的座標,與陰影貼圖中的Z值比較,前者大於後者,即代表片元處在陰影之中


## 番外 COCOS 渲染流程
director.mainLoop -> director.tick -> root.frameMove(dt)
-> pipeline.render(cameraList) -> (預設) render-pipeline.render(camera)
-> render-flow.render(camera) -> render-stage.render(camera)
<style>
.markdown-body b {
font-weight: 700;
}
.markdown-body code,
code {
color: #99999 !important;
background-color: #FFD080;
}
</style>