原文地址:WebGL多模型光照綜合例項
WebGL是一個非常的接近硬體底層的光柵化API, 從非常類似C/C++風格的API呼叫方式就可以看出來, 習慣了高階語言的我們會覺得很不友好,覺得特別繁瑣. 這個也是很多人覺得WebGL難的原因之一. 如果我們要使用WebGL做一些專案,毫無疑問要麼使用Three.js之類的3D庫, 要麼需要對原生的API進行封裝. 這段時間檢視了一些WebGL工具庫的原始碼, 參考封裝出了一個簡單的工具庫,這樣往後用WebGL做小專案就方便多了.
經過前面章節的學習, WebGL的知識點掌握的差不多了, 終於到了做特效和Demo的階段了,來看一下這節實現的特效:
WebGL多物體多光源場景
內容大綱
實現圖形繞座標原點旋轉, 同時給所有的物體增加環境光, 漫反射, 高光. 其中旋轉功能使用矩陣複合變換實現; 光照部分比較複雜,實現了多個光源照射.
- 著色器
- 模型變換
著色器
頂點著色器
程式碼很簡單,逐頂點傳入座標,法向量矩陣,模型矩陣,mvp矩陣.
attribute vec4 a_position;
attribute vec4 a_normal;
uniform mat4 u_modelMatrix;
uniform mat4 u_normalMatrix;
uniform mat4 u_mvpMatrix;
varying vec3 v_normal;
varying vec3 v_position;
void main() {
gl_Position = u_mvpMatrix * a_position;
v_normal=vec3(u_normalMatrix * a_normal);
v_position= vec3(u_modelMatrix * a_position);
}
片元著色器
分別在左前方和右後方新增了平行光源和點光源, 平行光源的高光使用的是賓氏模型, 它的高光過渡效果比較平滑; 點光源的高光使用的是馮氏模型, 它的高光部分比較明亮, 反射的效果比較好.
最後將兩個光源照射產生的漫反射,高光亮度相加,就得到它們的綜合光照效果了.
precision mediump float;
uniform vec3 u_lightColor;
uniform vec3 u_lightPosition;
uniform vec3 u_lightPosition2;
uniform vec3 u_ambientColor;
uniform vec3 u_viewPosition;
uniform vec4 u_color;
varying vec3 v_normal;
varying vec3 v_position;
void main() {
// 法向量歸一化
vec3 normal = normalize(v_normal);
// 計算環境光反射顏色
vec3 ambient = u_ambientColor * u_color.rgb;
// 第一個光源:平行光
vec3 lightDirection = normalize(u_lightPosition);
// 計演算法向量和光線的點積
float cosTheta = max(dot(lightDirection, normal), 0.0);
// 計算漫反射光的顏色
vec3 diffuse = u_lightColor * u_color.rgb * cosTheta;
// 賓氏模型高光
float shininess =100.0;
vec3 specularColor =vec3(1.0,1.0,1.0);
vec3 viewDirection = normalize(u_viewPosition-v_position);
vec3 halfwayDir = normalize(lightDirection + viewDirection);
float specularWeighting = pow(max(dot(normal, halfwayDir), 0.0), shininess);
vec3 specular = specularColor.rgb * specularWeighting * step(cosTheta,0.0);
// 第二個光源:點光源
vec3 lightDirection2 = normalize(u_lightPosition2 - v_position.xyz);
// 計演算法向量和光線的點積
float cosTheta2 = max(dot(lightDirection2, normal), 0.0);
// 計算漫反射光的顏色
vec3 diffuse2 = u_lightColor * u_color.rgb * cosTheta2;
// 馮氏模型高光
float shininess2 =30.0;
vec3 specularColor2 =vec3(1.0,1.0,1.0);
vec3 reflectionDirection = reflect(-lightDirection2, normal);
float specularWeighting2 = pow(max(dot(reflectionDirection, viewDirection), 0.0), shininess2);
vec3 specular2 = specularColor2.rgb * specularWeighting2 * step(cosTheta,0.0);
// 兩個光源亮度相加
gl_FragColor = vec4(diffuse+diffuse2+ambient+specular+specular2,u_color.a);
}
模型變換
js程式碼部分使用工具庫封裝了原生WebGL的很多細節, 現在寫起程式碼來要愉快得多了, 感覺和寫canvas差不了太多?. 這裡重點介紹模型變換部分, 其他部分程式碼的細節之前章節已經介紹過了,所以不再詳述.
首先初始化著色器,並建立program物件; 這裡使用了多種圖形(圓球,立方體,圓柱體,圓錐體), 所以分別為它們建立緩衝區, 緩衝區資料主要包括頂點,法向量,索引.
接著建立200個圖形物件, 給每個物件賦予 隨機x/y軸角速度, 隨機初始點, 隨機顏色.
最後終於到了動畫的環節了, 繞原點旋轉可以分解為x軸旋轉, y軸旋轉和位移. 要注意的是矩陣複合變換相乘的順序, 也就是左乘和右乘是有區別的, 學過線性代數的應該都有印象. 這裡要實現圖形先位移z,然後再旋轉, 那麼對應的複合變換矩陣就是這樣
<模型矩陣> = <繞x軸旋轉矩陣> * <繞y軸旋轉矩陣> * <位移矩陣>
模型矩陣與檢視投影矩陣相乘得出mvp矩陣, 對模型矩陣逆轉置後還可以求出逆轉置矩陣.
將矩陣和變數的值傳遞給著色器, 輸出對應的圖形緩衝區,然後根據圖形物件依次繪製圖形, 最後呼叫requestAnimationFrame遞迴執行動畫函式.
var canvas=document.getElementById('canvas'),
gl=get3DContext(canvas,true);
function main() {
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
var program = createProgramInfo(gl, ['vs', 'fs']),
sphereBuffer = createBufferInfoFromArrays(gl, Sphere(50)),
cubeBuffer = createBufferInfoFromArrays(gl, Cube()),
cylinderBuffer = createBufferInfoFromArrays(gl, Cylinder(0.5, 2, 40)),
coneBuffer = createBufferInfoFromArrays(gl, Cone(0.5, 2, 40)),
buffers = [sphereBuffer, cubeBuffer, cylinderBuffer, coneBuffer];
for(var i=0;i<NUM;i++){
Polygons.push({
xRotation: Random(-60,60),
yRotation: Random(-60,60),
xAngle:0,
yAngle:0,
x:Random(-15,15),
y:Random(-15,15),
z:Random(3,20),
color:Color.rgbToWebgl(Color.hslToRgb(RandomHsl()))
});
}
gl.clearColor(0.1,0.1,0.1,1);
gl.enable(gl.DEPTH_TEST);
gl.viewport(0, 0, canvas.width, canvas.height);//設定繪圖區域
gl.useProgram(program.program);
var modelMatrix = new Matrix4(),
normalMatrix = new Matrix4(),
mvpMatrix = new Matrix4(),
last = Date.now();
(function animate(){
var now = new Date(),
elapsed = now - last;
last = now;
// ...
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
setUniforms(program,{
u_lightColor: [1,1,1],
u_lightPosition: LIGHT_POS,
u_lightPosition2: LIGHT_POS2,
u_ambientColor:[0.4,0.4,0.4],
u_viewPosition:[eyeX,eyeY,eyeZ]
});
var buffer;
Polygons.forEach((polygon,i)=>{
polygon.xAngle+=polygon.xRotation * elapsed / 1000;
polygon.yAngle+=polygon.yRotation * elapsed / 1000;
polygon.xAngle%=360;
polygon.yAngle%=360;
// 模型矩陣
modelMatrix.setRotate(polygon.xAngle, 1, 0, 0);
modelMatrix.rotate(polygon.yAngle, 0, 1, 0);
modelMatrix.translate(0,0,polygon.z);
// modelMatrix.translate(polygon.x,polygon.y,polygon.z);
// 每次重置mvp矩陣
mvpMatrix.setPerspective(45, canvas.width / canvas.height, 1.0, 200.0);
mvpMatrix.lookAt(eyeX,eyeY,eyeZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
mvpMatrix.multiply(modelMatrix);
// 逆轉置矩陣
normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();
buffer = buffers[i % buffers.length];
setBuffersAndAttributes(gl,program,buffer);
setUniforms(program,{
u_color:polygon.color,
u_modelMatrix: modelMatrix.elements,
u_normalMatrix: normalMatrix.elements,
u_mvpMatrix: mvpMatrix.elements
});
gl.drawElements(gl.TRIANGLES, buffer.numElements, buffer.indexType, 0);
});
requestAnimationFrame(animate);
}());
}
總結
使用工具類省略了很多繁瑣無聊的部分,不用再去扣語法細節.比如獲取變數地址, 賦值, 建立緩衝區. 我們可以把精力都集中到業務邏輯方面.