obj文件和mtl文件分析

教程

OpenGLES入门教程一-Tutorial01-GL基特
OpenGLES入门教程二-Tutorial02-shader入门
OpenGLES入门教程三-Tutorial03-三维变换
OpenGLES入门教程肆-Tutorial04-GLKit进阶
OpenGLES进阶教程一-Tutorial0五-地球月亮
OpenGLES进阶教程2-Tutorial0陆-光线
OpenGLES进阶教程3-Tutorial07-粒子效果
OpenGLES进阶教程肆-Tutorial0八-帧缓存
OpenGLES进阶教程5-Tutorial0玖-碰碰车
OpenGLES进阶教程6-Tutorial10-平截体优化
OpenGLES进阶教程7-Tutorial11-天空盒效果

离开上1篇教程已经有7个月了,那6个月详细阅读GPUImage的源码,并写了详实剖析,发现对OpenGLES的历历在目摸底很有救助。
前一周三个简书的情人问笔者,假使有叁个.obj文件,怎么样用OpenGL
ES把它显获得iOS显示屏上。

  • obj文件如下

前面学习blender的时候,在国外的贰个多元教程有提到解析obj文件,那篇教程便来介绍如何解析obj和mtl文件,并用OpenGL
ES展现出来。

  最初的文章地址:WebGL学习(3) –
3D模型

  相信广大人是以创办逼真酷炫的三个维度效果为对象而上学webGL的呢,首先小编正是。小编领会了丰盛的webGL本领后,正准备大展身手时,蒙受了一种左右为难够的状态:照旧做不出想要的东西。为什么呢,因为未有3D模型可供操作啊,纯粹用代码创设复杂的3D模型完全不行想像。那必须选择3d马克斯,maya,以及开源的blender等建立模型软件拓展塑造。既然已经入了webGL的坑了,那也不得不硬着头皮继续求学3D建立模型,断断续续学了三个多月的blender教程,总算入门了。
  那节首要学习怎么样导入模型文件,然后用代码应用效益,操作模型。首先体现下笔者的绝唱,喷火战斗机的3D模型:webGL
喷火战斗机

概念介绍

图片 1

1、obj文件

obj文件是1种3D模型文件。

  • 文件格式

其中

v 几何体顶点 (吉优metric vertices)
vt 贴图坐标点 (Texture vertices)
vn 顶点法线 (Vertex normals)
f 面 (Face)

spit fire

2、mtl文件

mtl文件则是obj文件的依附文件,描述几何体的外表属性。

  • 文件格式

其中

条件反射 Ka r g b
漫反射 Kd r g b
镜反射 Ks r g b
反射指数 Ns exponent 钦定材质的反射指数,定义了反光高光度
折射值 Ni ptical density 钦点材料表面包车型大巴光密度,即折射值
渐隐指数 d factor 参数factor表示物体融合背景的数量

内容大纲

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

主导思路

新建一个工程,读入obj和mtl文件,解析文件内容,写入到.h/.c文件中,把.h/.c文件参加新的工程引用。

模型文件

  blender导出的模型文件plane.obj,
同时还蕴涵材料文件plane.mtl。模型包蕴2800多个顶峰,2200五个面,共200多k的体量,内容相比大,所以只可以将文件加载入html文件相比方便。
  那怎么加载呢?1般会采用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 %}

职能体现

model.gif

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

1、文件分析

自定义Model结构体来储存读取的消息,通过1行行读入文件,并用字符串相配来分析数据。

typedef struct Model
{
    int vertices;
    int positions;
    int texels;
    int normals;
    int faces;
    int materials;
}Model;


Model getOBJinfo(string fp)
{
    Model model = {0};

    ifstream inOBJ;
    inOBJ.open(fp);
    if (!inOBJ.good()) {
        cout << "error on open " << fp << endl;
        exit(1);
    }

    while (!inOBJ.eof()) {
        string line;
        getline(inOBJ, line);
        string type = line.substr(0, 2);

        if (type.compare("v ") == 0) {
            model.positions++;
        }
        else if (type.compare("vt") == 0) {
            model.texels++;
        }
        else if (type.compare("vn") == 0) {
            model.normals++;
        }
        else if (type.compare("f ") == 0) {
            model.faces++;
        }
    }

    model.vertices = model.faces * 3;

    inOBJ.close();

    return model;
}

着色器

二、文件写入

把Model中存款和储蓄的剖析消息,分别写入到.h/.c文件中。

void writeCtexels(string fp, string name, Model model, int faces[][10], float texels[][2])
{
    // Append C file
    ofstream outC;
    outC.open(fp, ios::app);

    // Texels
    outC << "const float " << name << "Texels[" << model.vertices*2 << "] = " << endl;
    outC << "{" << endl;
    // Texels
    for(int j=0; j<model.materials; j++)
    {
        for(int i=0; i<model.faces; i++)
        {
            if(faces[i][9] == j)
            {
                int vtA = faces[i][1] - 1;
                int vtB = faces[i][4] - 1;
                int vtC = faces[i][7] - 1;

                outC << texels[vtA][0] << ", " << texels[vtA][1] << ", " << endl;
                outC << texels[vtB][0] << ", " << texels[vtB][1] << ", " << endl;
                outC << texels[vtC][0] << ", " << texels[vtC][1] << ", " << endl;
            }
        }
    }
    outC << "};" << endl;
    outC << endl;

    // Close C file
    outC.close();
}

极限着色器

  顶点着色器相比轻易,和事先的区分相比大的是,把总结颜色光照部分移到了片元着色器,那样能够兑现逐片元光照,效果会更为绘身绘色和自然。

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

三、最后文件

Materials 材质
Diffuse 漫反射
Specular 镜面反射
Normal 法线
Texel 纹理
Position 位置

extern const int starshipVertices;
extern const float starshipPositions[198];
extern const float starshipTexels[132];
extern const float starshipNormals[198];

extern const int starshipMaterials;
extern const int starshipFirsts[3];
extern const int starshipCounts[3];

extern const float starshipDiffuses[3][3];
extern const float starshipSpeculars[3][3];

光照

  光照相关的总计首要在片元着色器中,首先科学普及一下光照的有关消息。

物体呈现出颜色亮度就是表面的反射光导致,计算反射光公式如下:
<表面的反射光颜色> = <漫反射光颜色> + <环境反射光颜色> + <镜面反射光颜色>

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

光线入射角度可以由光线方向和表面的法线进行点积求得:
<光线入射角度> = <光线方向> * <法线方向>

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

2. 环境反射光颜色根据如下公式得到:
<环境反射光颜色> = <入射光颜色> * <表面基底色>

3. 镜面(高光)反射光颜色公式,这里使用的是冯氏反射原理
<镜面反射光颜色> = <高光颜色> * <镜面反射亮度权重> 

其中镜面反射亮度权重又如下
<镜面反射亮度权重> = (<观察方向的单位向量> * <入射光反射方向>) ^光泽度

四、场景渲染

渲染的长河中,设置好BaseEffect后,举行渲染;有多组参数时,要分别设置再渲染。

/**
 *  渲染场景代码
 */
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    [self setMatrices];


    for(int i=0; i<starshipMaterials; i++)
    {
        // 设置材质
        self.baseEffect.material.diffuseColor = GLKVector4Make(starshipDiffuses[i][0], starshipDiffuses[i][1], starshipDiffuses[i][2], 1.0f);
        self.baseEffect.material.specularColor = GLKVector4Make(starshipSpeculars[i][0], starshipSpeculars[i][1], starshipSpeculars[i][2], 1.0f);

        [self.baseEffect prepareToDraw];

        glDrawArrays(GL_TRIANGLES, starshipFirsts[i], starshipCounts[i]);
    }

}

片元着色器

  着色器代码正是对上边公式内容的演绎

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

总结

那篇教程有四个工程,贰个承受解析,贰个用来渲染。解析的逻辑简单但较麻烦,能够一向看这里;渲染的逻辑和事先教程类似,唯一复杂的一对是光照部分的装置,能够看这里
除此以外,blender这几个工具十三分好用。以前为了求学blender,寻觅了诸多科目。demo的剖析部分代码,也是参照教程里面包车型客车内容,出处已经找不到。

模型转换

  那里先安装光照相关的启幕标准,然后是mvp矩阵转换和法向量矩阵相关的推测,具体知识点可参看此前的篇章*[WebGL学习(2)

3D场景](https://link.jianshu.com?t=http%3A%2F%2Fjeffzhong.space%2F2017%2F11%2F14%2Fwebgl2%2F)*
  要专注的是转败为胜置矩阵,重要用来计算模型转变之后的法向量,有了更改后的法向量本事科学总计光照。

 求逆转置矩阵步骤
    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模型依然挺费时间,要求花心机稳步调控,技术做出相比全面包车型地铁模型。