??原文地址: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> ???<script type="text/template" id="tplObj"> ???{% include '../model/plane.obj' %} ???</script> ???<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 vec4 v_Scolor; ???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; ???????v_Scolor = a_Scolor; ???}
光照
??光照相关的计算主要在片元着色器中,首先科普一下光照的相关信息。
???物体呈现出颜色亮度就是表面的反射光导致,计算反射光公式如下: ???<表面的反射光颜色> = <漫反射光颜色> + <环境反射光颜色> + <镜面反射光颜色> ???1. 其中漫反射公式如下: ???<漫反射光颜色> = <入射光颜色> * <表面基底色> * <光线入射角度> ???光线入射角度可以由光线方向和表面的法线进行点积求得: ???<光线入射角度> = <光线方向> * <法线方向> ???最后的漫反射公式如下: ???<漫反射光颜色> = <入射光颜色> * <表面基底色> * (<光线方向> * <法线方向>) ???2. 环境反射光颜色根据如下公式得到: ???<环境反射光颜色> = <入射光颜色> * <表面基底色> ???3. 镜面(高光)反射光颜色公式,这里使用的是冯氏反射原理 ???<镜面反射光颜色> = <高光颜色> * <镜面反射亮度权重> ????其中镜面反射亮度权重又如下 ???<镜面反射亮度权重> = (<观察方向的单位向量> * <入射光反射方向>) * 光泽度的幂
片元着色器
??着色器代码就是对上面公式内容的演绎
???#ifdef GL_ES ???precision mediump float; ???#endif ???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;//顶点颜色 ???varying vec4 v_Scolor;//顶点高光颜色 ???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 = ?v_Scolor.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模型还是挺费时间,需要花心机慢慢调整,才能做出比较完美的模型。