WebGL學(xué)習(xí)(3) - 3D模型

原文地址:WebGL學(xué)習(xí)(3) - 3D模型
相信很多人是以創(chuàng)建逼真酷炫的三維效果為目標(biāo)而學(xué)習(xí)webGL的吧,首先我就是??。我掌握了足夠的webGL技巧后,正準(zhǔn)備大展身手時(shí),遇到了一種尷尬的情況:還是做不出想要的東西??。為啥呢,因?yàn)闆]有3D模型可供操作啊,純粹用代碼構(gòu)建復(fù)雜的3D模型完全不可想象。

必須使用3dMax,maya,以及開源的blender等建模軟件進(jìn)行構(gòu)建。既然已經(jīng)入了webGL的坑了,那也只能硬著頭皮繼續(xù)學(xué)習(xí)3D建模,斷斷續(xù)續(xù)學(xué)了一個(gè)多月的blender教程,總算入門了。

這節(jié)主要學(xué)習(xí)如何導(dǎo)入模型文件,然后用代碼應(yīng)用效果,操作模型。首先展示下我的大作,噴火戰(zhàn)斗機(jī)的3D模型:webGL 噴火戰(zhàn)斗機(jī)

splitfire.gif

內(nèi)容大綱

  1. 模型文件
  2. 著色器
  3. 光照
  4. 模型變換
  5. 事件處理

模型文件

blender導(dǎo)出的模型文件plane.obj, 同時(shí)還包括材質(zhì)文件plane.mtl。模型包括2800多個(gè)頂點(diǎn),2200多個(gè)面,共200多k的體積,內(nèi)容比較大,所以只能將文件加載入html文件比較方便。

怎么加載呢?一般會(huì)使用ajax獲取,但我這里有更方便的辦法。那就是將模型文件內(nèi)容預(yù)編譯直出到html中,這樣不但提高了加載性能,開發(fā)也更方便。具體可參考我之前的文章:前端快速開發(fā)模版

這里使用我之前的開發(fā)模版, 將模型(obj、mtl)文件以字符串的形式寫入text/template模版中,同時(shí)將GLSL語言寫的著色器也預(yù)編譯到html中。到時(shí)用gulp的命令構(gòu)建頁面,所有內(nèi)容就會(huì)自動(dòng)生成到頁面中,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>上下左右方向鍵 調(diào)整視角,W/S/A/D鍵 旋轉(zhuǎn)模型, +/-鍵 放大縮小</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>
<!-- 頂點(diǎn)著色器 -->
<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文件包含的是模型的頂點(diǎn)法線索引等信息。這里以最簡單的立方體為例。

  • v 幾何體頂點(diǎn)
  • vt 貼圖坐標(biāo)點(diǎn)
  • vn 頂點(diǎn)法線
  • f 面:頂點(diǎn)索引 / 紋理坐標(biāo)索引 / 法線索引
  • usemtl 使用的材質(zhì)名稱
# 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文件包含的是模型的材質(zhì)信息

  • Ka 環(huán)境色 rgb
  • Kd 漫反射色,材質(zhì)顏色 rgb
  • Ks 高光色,材質(zhì)高光顏色 rgb
  • Ns 反射高光度 指定材質(zhì)的反射指數(shù)
  • Ni 折射值 指定材質(zhì)表面的光密度
  • 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讀取解析,想知道內(nèi)部原理,可以查看源代碼,這里不詳述。

提取出需要的信息后,就可將模型信息寫入緩沖區(qū),然后渲染出來。

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');
    //...

    // 創(chuàng)建空數(shù)據(jù)緩沖
    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();

    // 將數(shù)據(jù)寫入緩沖區(qū)
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, drawingInfo.vertices, gl.STATIC_DRAW);
    //...
}

著色器

頂點(diǎn)著色器

頂點(diǎn)著色器比較簡單,和之前的區(qū)別比較大的是,把計(jì)算顏色光照部分移到了片元著色器,這樣可以實(shí)現(xiàn)逐片元光照,效果會(huì)更加逼真和自然。

attribute vec4 a_Position;//頂點(diǎn)位置
attribute vec4 a_Color;//頂點(diǎn)顏色
attribute vec4 a_Scolor;//頂點(diǎn)高光顏色
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;
    // 計(jì)算頂點(diǎn)在世界坐標(biāo)系的位置
    v_Position = vec3(u_ModelMatrix * a_Position);
    // 計(jì)算變換后的法向量并歸一化
    v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));
    v_Color = a_Color;
}

光照

光照相關(guān)的計(jì)算主要在片元著色器中,首先科普一下光照的相關(guān)信息。

物體呈現(xiàn)出顏色亮度就是表面的反射光導(dǎo)致,計(jì)算反射光公式如下:
<表面的反射光顏色> = <漫反射光顏色> + <環(huán)境反射光顏色> + <鏡面反射光顏色>

1. 其中漫反射公式如下:
<漫反射光顏色> = <入射光顏色> * <表面基底色> * <光線入射角度>

光線入射角度可以由光線方向和表面的法線進(jìn)行點(diǎn)積求得:
<光線入射角度> = <光線方向> * <法線方向>

最后的漫反射公式如下:
<漫反射光顏色> = <入射光顏色> * <表面基底色> * (<光線方向> * <法線方向>)

2. 環(huán)境反射光顏色根據(jù)如下公式得到:
<環(huán)境反射光顏色> = <入射光顏色> * <表面基底色>

3. 鏡面(高光)反射光顏色公式,這里使用的是馮氏反射原理
<鏡面反射光顏色> = <高光顏色> * <鏡面反射亮度權(quán)重> 

其中鏡面反射亮度權(quán)重又如下
<鏡面反射亮度權(quán)重> = (<觀察方向的單位向量> * <入射光反射方向>) ^ 光澤度

片元著色器

著色器代碼就是對上面公式內(nèi)容的演繹

precision mediump float;
uniform vec3 u_LightPosition;//光源位置
uniform vec3 u_diffuseColor;//漫反射光顏色
uniform vec3 u_AmbientColor;//環(huán)境光顏色
uniform vec3 u_specularColor;//鏡面反射光顏色
uniform float u_MaterialShininess;// 鏡面反射光澤度
varying vec3 v_Normal;//法向量
varying vec3 v_Position;//頂點(diǎn)位置
varying vec4 v_Color;//頂點(diǎn)顏色

void main() {
    // 對法線歸一化
    vec3 normal = normalize(v_Normal);
    // 計(jì)算光線方向(光源位置-頂點(diǎn)位置)并歸一化
    vec3 lightDirection = normalize(u_LightPosition - v_Position);
    // 計(jì)算光線方向和法向量點(diǎn)積
    float nDotL = max(dot(lightDirection, normal), 0.0);
    // 漫反射光亮度
    vec3 diffuse = u_diffuseColor  * nDotL * v_Color.rgb;
    // 環(huán)境光亮度
    vec3 ambient = u_AmbientColor * v_Color.rgb;
    // 觀察方向的單位向量V
    vec3 eyeDirection = normalize(-v_Position);
    // 反射方向
    vec3 reflectionDirection = reflect(-lightDirection, normal);
    // 鏡面反射亮度權(quán)重
    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);
}

模型變換

這里先設(shè)置光照相關(guān)的初始條件,然后是mvp矩陣變換和法向量矩陣相關(guān)的計(jì)算,具體知識點(diǎn)可參考之前的文章WebGL學(xué)習(xí)(2) - 3D場景

要注意的是逆轉(zhuǎn)置矩陣,主要用于計(jì)算模型變換之后的法向量,有了變換后的法向量才能正確計(jì)算光照。

求逆轉(zhuǎn)置矩陣步驟
1.求原模型矩陣的逆矩陣
2.將逆矩陣轉(zhuǎn)置

<變換后法向量> = <逆轉(zhuǎn)置矩陣> * <變換前法向量>

給著色器變量賦值然后繪制出模型,最后調(diào)用requestAnimationFrame不斷執(zhí)行動(dòng)畫。矩陣的旋轉(zhuǎn)部分可結(jié)合下面的keydown事件進(jìn)行查看。

function main() {
    //...

    // 光線方向
    gl.uniform3f(u_LightPosition, 0.0, 2.0, 12.0);
    // 漫反射光照顏色
    gl.uniform3f(u_diffuseColor, 1.0, 1.0, 1.0);
    // 設(shè)置環(huán)境光顏色
    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軸旋轉(zhuǎn)
        modelMatrix.rotate(angleX % 360, 1, 0, 0); // 繞x軸旋轉(zhuǎn)

        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);

        // 視點(diǎn)投影
        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);
        // 根據(jù)模型矩陣計(jì)算用來變換法向量的矩陣
        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);

        // 根據(jù)頂點(diǎn)索引繪制圖形(圖形類型,繪制頂點(diǎn)個(gè)數(shù),頂點(diǎn)索引數(shù)據(jù)類型,頂點(diǎn)索引中開始繪制的位置)
        gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_SHORT, 0);
        requestAnimationFrame(animate);
    })();
}

事件處理

+/- 鍵實(shí)現(xiàn)放大/縮小場景的功能;WSAD鍵實(shí)現(xiàn)模型的旋轉(zhuǎn),也就是實(shí)現(xiàn)繞x軸和y軸旋轉(zhuǎn);上下左右方向鍵實(shí)現(xiàn)的是視點(diǎn)的旋轉(zhuǎn)。矩陣變換的相關(guān)實(shí)現(xiàn)參考上面代碼的動(dòng)畫部分。

模型旋轉(zhuǎn)和視點(diǎn)旋轉(zhuǎn)看著很相似,其實(shí)又有不同的。視點(diǎn)的旋轉(zhuǎn)是整個(gè)場景比如光照模型等都是跟著變化的,如果以場景做參照物,它就相當(dāng)于人改變觀察位置觀看物體。而模型旋轉(zhuǎn)呢,它只旋轉(zhuǎn)模型自身,外部的光照和場景都是不變的,以場景做參照物,相當(dāng)于人在同一位置觀看模型在運(yùn)動(dòng)。從demo的光照可以看出兩種方式的區(qū)別。

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 );

總結(jié)

最后,個(gè)人感覺建立3D模型還是挺費(fèi)時(shí)間,需要花心機(jī)慢慢調(diào)整,才能做出比較完美的模型。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • WebGL從2012年開始接觸,后面因?yàn)殚_始專注前端其他方面的事情,慢慢地就把它給遺忘。最近前端開始又流行起繪畫制...
    我不是傳哥閱讀 4,356評論 1 22
  • <轉(zhuǎn)>我也忘了轉(zhuǎn)自哪里,抱歉,感謝原作者 什么是Shader Shader(著色器)是一段能夠針對3D對象進(jìn)行操作...
    星易乾川閱讀 5,846評論 1 16
  • 1 前言 一直想沿著圖像處理這條線建立一套完整的理論知識體系,同時(shí)積累實(shí)際應(yīng)用經(jīng)驗(yàn)。因此有了從使用AVFounda...
    RichardJieChen閱讀 5,928評論 5 12
  • 這篇文章講的是,我們對時(shí)間的感知,是受到多巴胺影響的。 我們可能都有過這樣的經(jīng)歷,快樂興奮時(shí)感覺光陰似箭,痛苦無...
    73feb922c323閱讀 710評論 0 0
  • 我開始奔跑,向著遠(yuǎn)方! 即使只見烈日和荒蕪的沙漠。 即使沒有玫瑰和精良的行囊。 甚至沒有同伴撫慰我的憂傷。 我依然...
    美崎靜香閱讀 207評論 0 0

友情鏈接更多精彩內(nèi)容