原文地址:WebGL學習(3) - 3D模型
相信很多人是以創建逼真酷炫的三維效果為目標而學習webGL的吧,首先我就是😂。我掌握了足夠的webGL技巧后,正准備大展身手時,遇到了一種尷尬的情況:還是做不出想要的東西😭。為啥呢,因為沒有3D模型可供操作啊,純粹用代碼構建復雜的3D模型完全不可想象。
必須使用3dMax,maya,以及開源的blender等建模軟件進行構建。既然已經入了webGL的坑了,那也只能硬着頭皮繼續學習3D建模,斷斷續續學了一個多月的blender教程,總算入門了。
這節主要學習如何導入模型文件,然后用代碼應用效果,操作模型。首先展示下我的大作,噴火戰斗機的3D模型:webGL 噴火戰斗機
內容大綱
- 模型文件
- 着色器
- 光照
- 模型變換
- 事件處理
模型文件
blender導出的模型文件plane.obj, 同時還包括材質文件plane.mtl。模型包括2800多個頂點,2200多個面,共200多k的體積,內容比較大,所以只能將文件加載入html文件比較方便。
怎么加載呢?一般會使用ajax獲取,但我這里有更方便的辦法。那就是將模型文件內容預編譯直出到html中,這樣不但提高了加載性能,開發也更方便。具體可參考我之前的文章:前端快速開發模版
這里使用我之前的開發模版, 將模型(obj、mtl)文件以字符串的形式寫入text/template模版中,同時將GLSL語言寫的着色器也預編譯到html中。到時用gulp的命令構建頁面,所有內容就會自動生成到頁面中,html部分的代碼如下所示:
{% extends '../layout/layout.html' %}
{% block title %}spitfire fighter{% endblock %}
{% block js %}
<script src="./lib/webgl.js"></script>
<script src="./lib/objParse.js"></script>
<script src="./lib/matrix.js"></script>
<script src="./js/index.js"></script>
{% endblock %}
{% block content %}
<div class="content">
<p>上下左右方向鍵 調整視角,W/S/A/D鍵 旋轉模型, +/-鍵 放大縮小</p>
<canvas id="canvas" width="800" height="600"></canvas>
</div>
<!-- obj文件 -->
<script type="text/template" id="tplObj">
{% include '../model/plane.obj' %}
</script>
<!-- mtl文件 -->
<script type="text/template" id="tplMtl">
{% include '../model/plane.mtl' %}
</script>
<!-- 頂點着色器 -->
<script type="x-shader/x-vertex" id="vs">
{% include '../glsl/vs.glsl' %}
</script>
<!-- 片元着色器 -->
<script type="x-shader/x-fragment" id="fs">
{% include '../glsl/fs.glsl' %}
</script>
{% endblock %}
obj文件
obj文件包含的是模型的頂點法線索引等信息。這里以最簡單的立方體為例。
- v 幾何體頂點
- vt 貼圖坐標點
- vn 頂點法線
- f 面:頂點索引 / 紋理坐標索引 / 法線索引
- usemtl 使用的材質名稱
# Blender v2.79 (sub 0) OBJ File: ''
# www.blender.org
mtllib cube.mtl
o Cube
v -0.442946 -1.000000 -1.000000
v -0.442946 -1.000000 1.000000
v -2.442946 -1.000000 1.000000
v -2.442945 -1.000000 -1.000000
v -0.442945 1.000000 -0.999999
v -0.442946 1.000000 1.000001
v -2.442946 1.000000 1.000000
v -2.442945 1.000000 -1.000000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn -0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
usemtl Material
s off
f 1//1 2//1 3//1 4//1
f 5//2 8//2 7//2 6//2
f 1//3 5//3 6//3 2//3
f 2//4 6//4 7//4 3//4
f 3//5 7//5 8//5 4//5
f 5//6 1//6 4//6 8//6
mtl文件
mtl文件包含的是模型的材質信息
- Ka 環境色 rgb
- Kd 漫反射色,材質顏色 rgb
- Ks 高光色,材質高光顏色 rgb
- Ns 反射高光度 指定材質的反射指數
- Ni 折射值 指定材質表面的光密度
- d 透明度
# Blender MTL File: 'None'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
知道了obj和mtl文件的格式,我們需要做的就是讀取它們,逐行分析,這里使用的objParse讀取解析,想知道內部原理,可以查看源代碼,這里不詳述。
提取出需要的信息后,就可將模型信息寫入緩沖區,然后渲染出來。
var canvas = document.getElementById('canvas'),
gl = get3DContext(canvas, true),
objElem = document.getElementById('tplObj'),
mtlElem = document.getElementById('tplMtl');
function main() {
//...
//獲取變量地址
var program = gl.program;
program.a_Position = gl.getAttribLocation(gl.program, 'a_Position');
//...
// 創建空數據緩沖
var vertexBuffer = createEmptyArrayBuffer(gl, program.a_Position, 3, gl.FLOAT);
//...
// 分析模型字符串
var objDoc = new OBJDoc('plane',objElem.text,mtlElem.text);
if(!objDoc.parse(1, false)){return;}
var drawingInfo = objDoc.getDrawingInfo();
// 將數據寫入緩沖區
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, drawingInfo.vertices, gl.STATIC_DRAW);
//...
}
着色器
頂點着色器
頂點着色器比較簡單,和之前的區別比較大的是,把計算顏色光照部分移到了片元着色器,這樣可以實現逐片元光照,效果會更加逼真和自然。
attribute vec4 a_Position;//頂點位置
attribute vec4 a_Color;//頂點顏色
attribute vec4 a_Scolor;//頂點高光顏色
attribute vec4 a_Normal;//法向量
uniform mat4 u_MvpMatrix;//mvp矩陣
uniform mat4 u_ModelMatrix;//模型矩陣
uniform mat4 u_NormalMatrix;
varying vec4 v_Color;
varying vec3 v_Normal;
varying vec3 v_Position;
void main() {
gl_Position = u_MvpMatrix * a_Position;
// 計算頂點在世界坐標系的位置
v_Position = vec3(u_ModelMatrix * a_Position);
// 計算變換后的法向量並歸一化
v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));
v_Color = a_Color;
}
光照
光照相關的計算主要在片元着色器中,首先科普一下光照的相關信息。
物體呈現出顏色亮度就是表面的反射光導致,計算反射光公式如下:
<表面的反射光顏色> = <漫反射光顏色> + <環境反射光顏色> + <鏡面反射光顏色>
1. 其中漫反射公式如下:
<漫反射光顏色> = <入射光顏色> * <表面基底色> * <光線入射角度>
光線入射角度可以由光線方向和表面的法線進行點積求得:
<光線入射角度> = <光線方向> * <法線方向>
最后的漫反射公式如下:
<漫反射光顏色> = <入射光顏色> * <表面基底色> * (<光線方向> * <法線方向>)
2. 環境反射光顏色根據如下公式得到:
<環境反射光顏色> = <入射光顏色> * <表面基底色>
3. 鏡面(高光)反射光顏色公式,這里使用的是馮氏反射原理
<鏡面反射光顏色> = <高光顏色> * <鏡面反射亮度權重>
其中鏡面反射亮度權重又如下
<鏡面反射亮度權重> = (<觀察方向的單位向量> * <入射光反射方向>) ^ 光澤度
片元着色器
着色器代碼就是對上面公式內容的演繹
precision mediump float;
uniform vec3 u_LightPosition;//光源位置
uniform vec3 u_diffuseColor;//漫反射光顏色
uniform vec3 u_AmbientColor;//環境光顏色
uniform vec3 u_specularColor;//鏡面反射光顏色
uniform float u_MaterialShininess;// 鏡面反射光澤度
varying vec3 v_Normal;//法向量
varying vec3 v_Position;//頂點位置
varying vec4 v_Color;//頂點顏色
void main() {
// 對法線歸一化
vec3 normal = normalize(v_Normal);
// 計算光線方向(光源位置-頂點位置)並歸一化
vec3 lightDirection = normalize(u_LightPosition - v_Position);
// 計算光線方向和法向量點積
float nDotL = max(dot(lightDirection, normal), 0.0);
// 漫反射光亮度
vec3 diffuse = u_diffuseColor * nDotL * v_Color.rgb;
// 環境光亮度
vec3 ambient = u_AmbientColor * v_Color.rgb;
// 觀察方向的單位向量V
vec3 eyeDirection = normalize(-v_Position);
// 反射方向
vec3 reflectionDirection = reflect(-lightDirection, normal);
// 鏡面反射亮度權重
float specularLightWeighting = pow(max(dot(reflectionDirection, eyeDirection), 0.0), u_MaterialShininess);
// 鏡面高光亮度
vec3 specular = lightColor.rgb * specularLightWeighting ;
gl_FragColor = vec4(ambient + diffuse + specular, v_Color.a);
}
模型變換
這里先設置光照相關的初始條件,然后是mvp矩陣變換和法向量矩陣相關的計算,具體知識點可參考之前的文章WebGL學習(2) - 3D場景
要注意的是逆轉置矩陣,主要用於計算模型變換之后的法向量,有了變換后的法向量才能正確計算光照。
求逆轉置矩陣步驟
1.求原模型矩陣的逆矩陣
2.將逆矩陣轉置
<變換后法向量> = <逆轉置矩陣> * <變換前法向量>
給着色器變量賦值然后繪制出模型,最后調用requestAnimationFrame不斷執行動畫。矩陣的旋轉部分可結合下面的keydown事件進行查看。
function main() {
//...
// 光線方向
gl.uniform3f(u_LightPosition, 0.0, 2.0, 12.0);
// 漫反射光照顏色
gl.uniform3f(u_diffuseColor, 1.0, 1.0, 1.0);
// 設置環境光顏色
gl.uniform3f(u_AmbientColor, 0.5, 0.5, 0.5);
// 鏡面反射光澤度
gl.uniform1f(u_MaterialShininess, 30.0);
var modelMatrix = new Matrix4();
var mvpMatrix = new Matrix4();
var normalMatrix = new Matrix4();
var n = drawingInfo.indices.length;
(function animate() {
// 模型矩陣
if (notMan) {
angleY += 0.5;
}
modelMatrix.setRotate(angleY % 360, 0, 1, 0); // 繞y軸旋轉
modelMatrix.rotate(angleX % 360, 1, 0, 0); // 繞x軸旋轉
var eyeY = viewLEN * Math.sin((viewAngleY * Math.PI) / 180),
len = viewLEN * Math.cos((viewAngleY * Math.PI) / 180),
eyeX = len * Math.sin((viewAngleX * Math.PI) / 180),
eyeZ = len * Math.cos((viewAngleX * Math.PI) / 180);
// 視點投影
mvpMatrix.setPerspective(30, canvas.width / canvas.height, 1, 300);
mvpMatrix.lookAt( eyeX, eyeY, eyeZ, 0, 0, 0, 0, viewAngleY > 90 || viewAngleY < -90 ? -1 : 1, 0 );
mvpMatrix.multiply(modelMatrix);
// 根據模型矩陣計算用來變換法向量的矩陣
normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();
// 模型矩陣
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);
// mvp矩陣
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
// 法向量矩陣
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);
// 清屏|清深度緩沖
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 根據頂點索引繪制圖形(圖形類型,繪制頂點個數,頂點索引數據類型,頂點索引中開始繪制的位置)
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(animate);
})();
}
事件處理
+/- 鍵實現放大/縮小場景的功能;WSAD鍵實現模型的旋轉,也就是實現繞x軸和y軸旋轉;上下左右方向鍵實現的是視點的旋轉。矩陣變換的相關實現參考上面代碼的動畫部分。
模型旋轉和視點旋轉看着很相似,其實又有不同的。視點的旋轉是整個場景比如光照模型等都是跟着變化的,如果以場景做參照物,它就相當於人改變觀察位置觀看物體。而模型旋轉呢,它只旋轉模型自身,外部的光照和場景都是不變的,以場景做參照物,相當於人在同一位置觀看模型在運動。從demo的光照可以看出兩種方式的區別。
document.addEventListener( "keydown", function(e) {
if ([37, 38, 39, 65, 58, 83, 87, 40].indexOf(e.keyCode) > -1) notMan = false;
switch (e.keyCode) {
case 38: //up
viewAngleY -= 2;
if (viewAngleY < -270) viewAngleY += 360;
break;
case 40: //down
viewAngleY += 2;
if (viewAngleY > 270) viewAngleY -= 360;
break;
case 37: //left
viewAngleX += 2;
break;
case 39: //right
viewAngleX -= 2;
break;
case 87: //w
angleX -= 2;
break;
case 83: //s
angleX += 2;
break;
case 65: //a
angleY += 2;
break;
case 68: //d
angleY -= 2;
break;
case 187: //zoom in
if (viewLEN > 6) viewLEN--;
break;
case 189: //zoom out
if (viewLEN < 30) viewLEN++;
break;
default:
break;
}
}, false );
總結
最后,個人感覺建立3D模型還是挺費時間,需要花心機慢慢調整,才能做出比較完美的模型。