WebGL学习(3) - 3D模型

原文地址:WebGL学习(3) - 3D模型
相信很多人是以创建逼真酷炫的三维效果为目标而学习webGL的吧,首先我就是😂。我掌握了足够的webGL技巧后,正准备大展身手时,遇到了一种尴尬的情况:还是做不出想要的东西😭。为啥呢,因为没有3D模型可供操作啊,纯粹用代码构建复杂的3D模型完全不可想象。

必须使用3dMax,maya,以及开源的blender等建模软件进行构建。既然已经入了webGL的坑了,那也只能硬着头皮继续学习3D建模,断断续续学了一个多月的blender教程,总算入门了。

这节主要学习如何导入模型文件,然后用代码应用效果,操作模型。首先展示下我的大作,喷火战斗机的3D模型:webGL 喷火战斗机

splitfire.gif

内容大纲

  1. 模型文件
  2. 着色器
  3. 光照
  4. 模型变换
  5. 事件处理

模型文件

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模型还是挺费时间,需要花心机慢慢调整,才能做出比较完美的模型。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • WebGL从2012年开始接触,后面因为开始专注前端其他方面的事情,慢慢地就把它给遗忘。最近前端开始又流行起绘画制...
    我不是传哥阅读 4,076评论 1 22
  • <转>我也忘了转自哪里,抱歉,感谢原作者 什么是Shader Shader(着色器)是一段能够针对3D对象进行操作...
    星易乾川阅读 5,574评论 1 16
  • 1 前言 一直想沿着图像处理这条线建立一套完整的理论知识体系,同时积累实际应用经验。因此有了从使用AVFounda...
    RichardJieChen阅读 5,642评论 5 12
  • 这篇文章讲的是,我们对时间的感知,是受到多巴胺影响的。 我们可能都有过这样的经历,快乐兴奋时感觉光阴似箭,痛苦无...
    73feb922c323阅读 575评论 0 0
  • 我开始奔跑,向着远方! 即使只见烈日和荒芜的沙漠。 即使没有玫瑰和精良的行囊。 甚至没有同伴抚慰我的忧伤。 我依然...
    美崎静香阅读 179评论 0 0