現實世界中的物體被光線照射時,會反射一部分光。只有當反射光線進人你的眼睛時,你才能夠看到物體并辯認出它的顏色。
光源類型
- 平行光(Directional Light):光線是相互平行的,平行光具有方向。平行光可以看作是無限遠處的光源(比如太陽)發出的光。因為太陽距離地球很遠,所以陽光到達地球時可以認為是平行的。平行光很簡單,可以用一個方向和一個顏色來定義
- 點光源(Point Light):是從一個點向周圍的所有方向發出的光。點光源光可以用來表示現實中的燈泡、火焰等。我們需要指定點光源的位置和顏色。光線的方向將根據點光源的位置和被照射之處的位置計算出來,因為點光源的光線的方向在場景內的不同位置是不同的。
- 環境光(Ambient Light):環境光(間接光)是指那些經光源(點光源或平行光源)發出后,被墻壁等物體多次反射,然后照到物體表面上的光。環境光從各個角度照射物體,其強度都是致的。比如說,在夜間打開冰箱的門,整個廚房都會有些微微亮,這就是環境光的作用。環境光不用指定位置和方向,只需要指定顏色即可。
反射類型
- 漫反射(Diffuse Reflection):是針對平行光或點光源而言的。漫反射的反射光在各個方向上是均勻的,如果物體表面像鏡子一樣光滑,那么光線就會以特定的角度反射出去;但是現實中的大部分材質,比如紙張、巖石、塑料等,其表面都是粗糙的,在這種情況下反射光就會以不固定的角度反射出去。
- 環境反射(Ambient Reflection):環境反射是針對環境光而言的。在環境反射中,反射光的方向可以認為就是人射光的反方向。由于環境光照射物體的方式就是各方向均勻、強度相等的,所以反射光也是各向均勻的。
漫反射顏色公式
漫反射顏色 = 入射光顏色 * 表面基底色 * cos A
式子中,入射光顏色指的是點光源或平行光的顏色,乘法操作是在顏色矢量上逐分量(R、G、B)進行的。因為漫反射光在各個方向上都是“均勻”的,所以從任何角度看上去其強度都相等。
環境反射顏色公式
環境反射顏色 = 環境光顏色 * 表面基底色
當漫反射和環境反射同時存在時,將兩者加起來,就會得到物體最終被觀察到的顏色
計算入射角
根據入射光的方向和物體表面的朝向(即法線方向)來計算出入射角。在創建三維模型的時候,無法預先確定光線將以怎樣的角度照射到每個表面上
但是可以確定每個表面的朝向。在指定光源的時候,再確定光的方向,就可以用這兩項信息來計算出入射角了。
在線性代數當中,對矢量n和1作點積運算,公式為:n·1 = |n||1|cosA,其中||符號表示向量的模(長度)。如果兩個矢量長度都是1,則點積運算結果為
cosA。
那么就可以對前面漫反射顏色公式進行調整:
漫反射顏色 = 入射光顏色 * 表面基底色 * (光線方向 * 法線方向)
- 光線方向矢量和表面法線矢量的長度必須為1(單位向量)
- 光線方向,實際上是入射方向的反方向,即從入射點指向光源方向
法線:表面朝向
物體表面的朝向,即垂直于表面的方向,又稱法線或法向量。法向量有三個分量,向量(Nx,Ny,Nz)表示從(0,0,0)到(Nx,Ny,Nz)的方向。
- 矢量n為(Nx,Ny,Nz)則其長度為|n| = sqrt(Nx^2 + Ny^2 + Nz^2)
- 對矢量進行歸一化后的結果是(Nx/m,Ny/m,Nz/m),其中m是n的的模。如矢量(2.0,2.0,1.0)的長度|n|=sqrt(2.02+2.02+1.0^2)=sqrt(9)
=3.0,那么歸一化后的結果是(2.0/3.0,2.0/3.0,1.0/3.0)
平行光
角度的余弦值
首先來補充一下數學知識,看一下各個角度的余弦值:(這里一起把正弦和正切都加上了)
角度 (°) | 余弦值 (Cos) | 正弦值 (Sin) | 正切值 (Tan) |
---|---|---|---|
0 | 1 | 0 | 0 |
30 | √3/2 | 1/2 | √3/3 |
45 | √2/2 | √2/2 | 1 |
60 | 1/2 | √3/2 | √3 |
90 | 0 | 1 | 無定義 (∞) |
120 | -1/2 | √3/2 | -√3 |
135 | -√2/2 | √2/2 | -1 |
150 | -√3/2 | 1/2 | -√3/3 |
180 | -1 | 0 | 0 |
210 | -√3/2 | -1/2 | √3/3 |
225 | -√2/2 | -√2/2 | 1 |
240 | -1/2 | -√3/2 | √3 |
270 | 0 | -1 | 無定義 (-∞) |
300 | 1/2 | -√3/2 | -√3 |
315 | √2/2 | -√2/2 | -1 |
330 | √3/2 | -1/2 | -√3/3 |
360 | 1 | 0 | 0 |
那么再根據前面的入射角的公式,那么我們簡單計算一下幾個案例,在反射之后的顏色值
漫反射顏色 = 入射光顏色 * 表面基底色 * (光線方向 * 法線方向) = 入射光顏色 * 表面基底色 * cos A
入射光顏色 | 表面基底色 | 角度 | 角度余弦值 | 計算RGB | 漫反射顏色 |
---|---|---|---|---|---|
(1.0,1.0,1.0) 白色 | (1.0,0,0)紅色 | 0 | 1.0 | R=(1 * 1 * 1) G=(1 * 0 * 1) B=(1 * 0 * 1) | (1,0,0) |
(1.0,1.0,1.0) 白色 | (1.0,0,0)紅色 | 90 | 0 | R=(1 * 1 * 0) G=(1 * 0 * 0) B=(1 * 0 * 0) | (0,0,0) |
平行光案例
補充:前面都是采用drawArray方法繪制的正方體,這樣的話數組對象太多內容了,看的頭都暈了,還可以采用drawElements對前面的代碼進行重構優化一下。
數據對象可以進行一個拆分。boxArray數組表示的是每一個面的四個頂點的坐標位置,以第一行為例,就是從v0-v1-v2-v3的位置。那么對應的index就表示頂點位置的索引(因為一個正方形要拆分成兩個三角形,這也是這里的index一行為什么是6個數據的原因)。
// v6----- v5// /| /|// v1------v0|// | | | |// | |v7---|-|v4// |/ |/// v2------v3let boxArray = [1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, // v0-v1-v2-v31.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, 1.0, // v0-v3-v4-v51.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, // v0-v5-v6-v1-1.0, 1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, // v1-v6-v7-v2-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, // v7-v4-v3-v21.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, 1.0 // v4-v7-v6-v5
];let index = [0, 1, 2, 0, 2, 3, // front4, 5, 6, 4, 6, 7, // right8, 9, 10, 8, 10, 11, // up12, 13, 14, 12, 14, 15, // left16, 17, 18, 16, 18, 19, // down20, 21, 22, 20, 22, 23 // back
];
后面進行數據組合的方法和之前是一樣的。注意一下綁定的著色器的變量即可,以及最后drawElements方法,
let pointPosition = new Float32Array(boxArray);
let aPsotion = webGL.getAttribLocation(program, 'a_position');
let triangleBuffer = webGL.createBuffer();
webGL.bindBuffer(webGL.ARRAY_BUFFER, triangleBuffer);
webGL.bufferData(webGL.ARRAY_BUFFER, pointPosition, webGL.STATIC_DRAW);
webGL.enableVertexAttribArray(aPsotion);
webGL.vertexAttribPointer(aPsotion, 4, webGL.FLOAT, false, 4 * 4, 0);let indexBuffer = webGL.createBuffer();
let indices = new Uint8Array(index);
webGL.bindBuffer(webGL.ELEMENT_ARRAY_BUFFER, indexBuffer);
webGL.bufferData(webGL.ELEMENT_ARRAY_BUFFER, indices, webGL.STATIC_DRAW);webGL.drawElements(webGL.TRIANGLES, 36, webGL.UNSIGNED_BYTE, 0);
平行光案例實現:調整著色器代碼,看一下整個著色器代碼調整的完整流程。
graph TBsubgraph 頂點著色器 by modifyA(頂點坐標 a_position)B(透視投影 u_formMatrix)C(法向量 a_Normal)D(光照方向 u_LightDirection)E(漫射光 u_DiffuseLight)F(環境光 u_AmbientLight)G(顏色 v_Color)endsubgraph 片元著色器Z(v_Color)endC --> C1(歸一化法向量 normalize)D --> D1(歸一化光線方向 normalize)C1 -- dot計算點積、max取最大值 --> H(法向量與光線方向的點積)D1 --> HE --> I(計算漫反射顏色)H --> IF --> F1(計算環境光顏色)F1 -- 相加 --> J(顏色合并)I -- 相加 --> JG -- 利用varying變量傳值 --> 片元著色器J --> 片元著色器
通過這個流程圖也就結合了前面計算漫反射公式得到了漫反射的顏色,所以最后在片元著色器中利用varying變量傳值,進行顏色合并。那么也就渲染到了物體上。
let vertexString = `attribute vec4 a_position;uniform mat4 u_formMatrix;attribute vec4 a_Normal;uniform vec3 u_LightDirection;uniform vec3 u_DiffuseLight;uniform vec3 u_AmbientLight;varying vec4 v_Color;void main(void){gl_Position = u_formMatrix * a_position;vec3 normal = normalize(a_Normal.xyz);vec3 LightDirection = normalize(u_LightDirection.xyz);float nDotL = max(dot(LightDirection, normal), 0.0);vec3 diffuse = u_DiffuseLight * vec3(1.0,0,1.0)* nDotL;vec3 ambient = u_AmbientLight * vec3(1.0,0,1.0);v_Color = vec4(diffuse + ambient, 1);}`;
let fragmentString = `precision mediump float;varying vec4 v_Color;void main(void){gl_FragColor =v_Color;}`;
第二步就是設置法向量和光線方向,以及漫反射和環境光。而后結合前面的通過drawElements進行繪制。那也就完成了平行光案例。
let normals = new Float32Array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v0-v1-v2-v3 front1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // v0-v3-v4-v5 right0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v5-v6-v1 up-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, // v1-v6-v7-v2 left0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, // v7-v4-v3-v2 down0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0 // v4-v7-v6-v5 back
]);
let aNormal = webGL.getAttribLocation(program, 'a_Normal');
let normalsBuffer = webGL.createBuffer();
let normalsArr = new Float32Array(normals);
webGL.bindBuffer(webGL.ARRAY_BUFFER, normalsBuffer);
webGL.bufferData(webGL.ARRAY_BUFFER, normalsArr, webGL.STATIC_DRAW);
webGL.enableVertexAttribArray(aNormal);
webGL.vertexAttribPointer(aNormal, 3, webGL.FLOAT, false, 3 * 4, 0);let u_DiffuseLight = webGL.getUniformLocation(program, 'u_DiffuseLight');
webGL.uniform3f(u_DiffuseLight, 1.0, 1.0, 1.0);
let u_LightDirection = webGL.getUniformLocation(program, 'u_LightDirection');
webGL.uniform3fv(u_LightDirection, [0, 0, 10.0]);
let u_AmbientLight = webGL.getUniformLocation(program, 'u_AmbientLight');
webGL.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
點光源
漫反射光顏色 = 入射光顏色 * 表面基底色 * cos A
cos A = 光線方向 * 法線方向
在點光源是沒有光照方向的,光照方向需要通過光源位置-頂點位置來計算。兩者相減就會得到入射光方向向量。這樣就需要調整一下著色器代碼。
- 新增變量:u_PointLightPosition,u_NormalMatrix(法線變換矩陣)
- 計算normal,將法線向量從模型空間轉換到視圖空間或世界空間
- 計算入射光方向向量
let vertexString = `attribute vec4 a_position;uniform mat4 u_formMatrix;attribute vec4 a_Normal;uniform vec3 u_PointLightPosition;uniform vec3 u_DiffuseLight;uniform vec3 u_AmbientLight;varying vec4 v_Color;uniform mat4 u_NormalMatrix;void main(void){gl_Position = u_formMatrix * a_position;vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));vec3 LightDirection = normalize(vec3(gl_Position.xyz) - u_PointLightPosition);float nDotL = max(dot(LightDirection, normal), 0.0);vec3 diffuse = u_DiffuseLight * vec3(1.0,0,1.0)* nDotL;vec3 ambient = u_AmbientLight * vec3(1.0,0,1.0);v_Color = vec4(diffuse + ambient, 1);}`;
接著就是在js當中設置u_PointLightPosition,u_NormalMatrix。
let u_PointLightPosition = webGL.getUniformLocation(program, 'u_PointLightPosition');
webGL.uniform3fv(u_PointLightPosition, [10, 0, 0]);let uniformNormalMatrix = webGL.getUniformLocation(program, 'u_NormalMatrix');
let normalMatrix = mat4.create();
mat4.identity(normalMatrix);
mat4.invert(normalMatrix, ModelMatrix);
mat4.transpose(normalMatrix, ModelMatrix);
webGL.uniformMatrix4fv(uniformNormalMatrix, false, normalMatrix);
環境光
環境光相對于平行光和點光源來說,相對簡單些,不用再去計算漫反射光了,只需要計算環境光。那么其著色器代碼調整如下:只需要傳遞一個環境光進來,然后直接和基底色相乘就是渲染后的顏色了。
let vertexString = `attribute vec4 a_position;uniform mat4 u_formMatrix;uniform vec3 u_AmbientLight;varying vec4 v_Color;void main(void){gl_Position = u_formMatrix * a_position;vec3 ambient = u_AmbientLight * vec3(1.0,1.0,1.0);v_Color = vec4(ambient, 1);}`;
傳值也將其他的都進行刪去,設置u_AmbientLight即可。那么這里的值就是(0.8,0.1,0)顏色值就是 (2550.8, 2550, 255*0.1) = (
204,0,51) 橙紅色。
let u_AmbientLight = webGL.getUniformLocation(program, 'u_AmbientLight');
webGL.uniform3f(u_AmbientLight, 0.8, 0.1, 0);
逐片元光照
再來先回顧一下webGL整個渲染的流程
逐頂點著色
在逐頂點渲染中,前面講的光照或顏色的計算是在頂點著色器中進行的,頂點著色器運行結束后,每一個頂點都有一個顏色值,在片元著色器執行前,webGL會對這些頂點的顏色數據進行線性插值,從而得到每個片元處的顏色。這就是webGL繪制三角形的原理,為什么只給了3個頂點的顏色值就能得到一個彩色的三角形的緣故,即三角形中其他點(片元)的顏色值都是通過這給定的3個頂點的顏色值通過線性插值得到的。
逐片元著色
每個像素都被填充了光柵化處理后的顏色,并寫入顏色緩沖區,直到最后一個片元被處理完成,瀏覽器就會顯示出最終的彩色三角形
逐片元的計算光照條件:
- 片元在世界坐標系下的坐標。
- 片元處表面的法向量。可以在頂點著色器中,將頂點的世界坐標和法向量以varying變量的形式傳人片元著色器,片元著色器中的同名變量就已經是內插后的逐片元值了。
繪制球
在前面繪制立體圖形都是長方體這種可以確定具體的頂點坐標,那么繪制球體的時候我們怎么拿到對應的坐標再進行繪制呢?
球體任意一點點坐標
如下圖所示,這是一個球,現在已知半徑為r,求球上一點P的坐標,其中該點與中心點連線與z軸的夾角為θ,該點往平面做投影,投影到中心點連線和x軸的夾角為φ。
那么就可以得到p點的xyz坐標:并且現在只需要將φ轉360度,θ轉180度,即可得到球上任意一點的xyz坐標。
- x=rsinθcosφ
- y=rsinθsinφ
- z=rcosθ
webGL渲染球體(逐頂點著色)
在webGL當中所有的圖形都是通過很多個三角形進行組成的,下面開始計算球體的頂點坐標:也就是將上面的數學公式轉成js代碼。(在前面所有學習和實現的效果都是采用的逐頂點著色,也就是js將顏色值傳遞到頂點著色器當中,頂點著色器將所有的顏色都處理好了之后再通過varying傳遞給片元著色器)
let positions = [];
const SPHERE_DIV = 10;let i, ai, si, ci;
let j, aj, sj, cj;for (j = 0; j <= SPHERE_DIV; j++) {aj = j * Math.PI / SPHERE_DIV;sj = Math.sin(aj);cj = Math.cos(aj);for (i = 0; i <= SPHERE_DIV; i++) {ai = i * 2 * Math.PI / SPHERE_DIV;si = Math.sin(ai);ci = Math.cos(ai);positions.push(ci * sj); // Xpositions.push(cj); // Ypositions.push(si * sj); // Z}
}webgl.drawArrays(webgl.TRIANGLES, 0, positions.length / 3);
使用drawArrays進行渲染,直接根據頂點緩沖區的數據順序繪制。這里的頂點數量不夠,因為只計算了一些點,并且這些點沒有復用,組成的三角形不能完全覆蓋球體,所以就是這種效果
改用drawElements進行渲染,需要再加上計算點索引的數組的代碼。
let p1, p2;for (j = 0; j < SPHERE_DIV; j++) {for (i = 0; i < SPHERE_DIV; i++) {p1 = j * (SPHERE_DIV + 1) + i;p2 = p1 + (SPHERE_DIV + 1);indices.push(p1);indices.push(p2);indices.push(p1 + 1);indices.push(p1 + 1);indices.push(p2);indices.push(p2 + 1);}
}webGL.drawElements(webGL.TRIANGLES, indices.length, webGL.UNSIGNED_BYTE, 0);
webGL渲染球體(逐片元著色)
逐片元著色和逐頂點著色的區別就是,逐片元著色是在片元著色器中計算光照,逐頂點著色是在頂點著色器中計算光照。那么就調整一下著色器代碼
let vertexString = `attribute vec4 a_position;uniform mat4 u_formMatrix;attribute vec4 a_Normal;varying vec4 v_Normal;varying vec4 v_position;void main(void){gl_Position = u_formMatrix * a_position;v_position = gl_Position;v_Normal= a_Normal;}`;
let fragmentString = `precision mediump float;varying vec4 v_Normal;varying vec4 v_position;uniform vec3 u_PointLightPosition;uniform vec3 u_DiffuseLight;uniform vec3 u_AmbientLight;void main(void){vec3 normal = normalize(v_Normal.xyz);vec3 lightDirection = normalize(u_PointLightPosition - vec3(v_position.xyz));float nDotL = max(dot(lightDirection, normal), 0.0);vec3 diffuse = u_DiffuseLight * vec3(1.0,0,1.0) * nDotL;vec3 ambient = u_AmbientLight * vec3(1.0,0,1.0);gl_FragColor = vec4(diffuse + ambient, 1);}`;