WebGL入门(四十三)-WebGL加载OBJ-MTL三维模型

Posted 点燃火柴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WebGL入门(四十三)-WebGL加载OBJ-MTL三维模型相关的知识,希望对你有一定的参考价值。

1. demo效果

在这里插入图片描述

2. 相关知识点

2.1 OBJ文件内容说明

在这里插入图片描述
上图是obj文件格式模型的内容,接下来解释一下分别代表什么,先说一下文件中的空格表示不同内容之间的间隔

  1. 注释# 开头的行表示注释行,第1行和第2行为注释内容
  2. 引入外部材质文件mtllib 开头的行表示引入外部材质文件,后接外部材质名,第3行表示引入外部材质文件 cube.mtl
  3. 模型名称o 开头的行表示模型名称,后接模型名称,第4行表示该模型的文件名为 Cube
  4. 顶点坐标v 开头的行表示顶点坐标,后接坐标的分量 xyz[w],第5行到第12行分别表示立方体的8个顶点
  5. 材质usemtl 开头的行表示确定模型材质,后接材质名,表示使用第3行引入的材质文件中的由材质名指定的材质,第13行表示使用从Cube.mtl文件引入的名称为Material的材质,接下来一般会指定哪些面使用该材质
  6. 材质使用范围f 开头的行表示定义使用指定材质的面,后接顶点索引定义的面(此例只用顶点索引,实际还可以包括纹理坐标索引和法线索引),这里需要注意 索引是从1开始。第14行表示由顶点索引1,2,3,4构成的面使用指定材质,剩下第15行到第18行也通过对应的顶点索引指定了使用材质的面

第19行和第20行定义了使用另一个材质的表面

拓展
如果指定使用材质的面时,通过顶点和法向量一起指定,那需要遵照以下格式

f v1//vn1 v2//vn2 v3//vn3 ...
其中v1,v2,v3表示顶点索引, vn1,vn2,vn3表示法向量索引

2.2 解析OBJ文件过程

接下来看一下解析OBJ文件的代码

// 解析OBJ文件中的文本
OBJDoc.prototype.parse = function (fileString, scale, reverse) {
  var lines = fileString.split('\\n') //根据换行符拆分成数组
  lines.push(null) //添加结束标识
  var index = 0 //初始化当前行索引

  var currentObject = null
  var currentMaterialName = ''
  // 按行解析
  var line //接收当前文本行内容
  var sp = new StringParser() // 创建StringParser对象
  while ((line = lines[index++]) != null) {
    sp.init(line) //初始化sp
    var command = sp.getWord() //获取指令名
    if (command == null) continue //判空处理

    switch (command) {
      case '#':
        continue //注释跳过
      case 'mtllib': //读取材质文件
        var path = this.parseMtllib(sp, this.fileName)
        var mtl = new MTLDoc() //创建MTLDoc对象
        this.mtls.push(mtl)
        var request = new XMLHttpRequest()
        request.onreadystatechange = function () {
          if (request.readyState == 4) {
            if (request.status != 404) {
              onReadMTLFile(request.responseText, mtl)
            } else {
              mtl.complete = true
            }
          }
        }
        request.open('GET', path, true) //创建请求
        request.send() //发送请求
        continue //继续解析
      case 'o':
      case 'g': //读取对象名
        var object = this.parseObjectName(sp)
        this.objects.push(object)
        currentObject = object
        continue
      case 'v': //读取顶点
        var vertex = this.parseVertex(sp, scale)
        this.vertices.push(vertex)
        continue
      case 'vn': //读取法线
        var normal = this.parseNormal(sp)
        this.normals.push(normal)
        continue
      case 'usemtl': //读取材质名
        currentMaterialName = this.parseUsemtl(sp)
        continue
      case 'f': //读取表面
        var face = this.parseFace(
          sp,
          currentMaterialName,
          this.vertices,
          reverse
        )
        currentObject.addFace(face)
        continue //继续解析
    }
  }
  return true
}

2.3 MTL文件内容说明

在这里插入图片描述
上图是mtl文件格式模型的内容,接下来解释一下分别代表什么,与obj文件一样,空格表示不同内容之间的间隔

  1. 注释# 开头的行表示注释行,第1行和第2行为注释内容
  2. 定义材质newmtl 开头的行表示定义一个新材质,后接要定义的材质名,第3行表示定义一个名为Material的材质
  3. 定义环境色Ka 开头的行表示定义材质的环境色,后接使用RGB格式定义的颜色,第4行表示材质的环境色为(0.0,0.0,0.0)
  4. 定义漫射色Kd 开头的行表示定义材质的漫射色,后接使用RGB格式定义的颜色,第5行表示材质的漫射色为(1.0,0.0,0.0)
  5. 定义高光色Ks 开头的行表示定义材质的高光色,后接使用RGB格式定义的颜色,第6行表示材质的高光色为(0.0,0.0,0.0)
  6. 定义高光色权重Ns 开头的行表示定义材质的高光色权重,后接权重值,第7行表示定义材质的高光色权重
  7. 定义光学密度Ni 开头的行表示定义材质的光学密度,后接光学密度值,第8行表示定义材质的光学密度
  8. 定义透明度d 开头的行表示定义材质的透明度,后接透明度,第8行表示定义材质的透明度
  9. 指定光照模型illum 开头的行表示指定材质的光照模型,后接光照模型,第9行表示指定材质的光照模型

第3行到第10行定义了一个名为Material的材质
第11行到第18行定义了一个名为Material.001的材质
解析mtl格式文件的代码与解析obj文件代码类似

2.4 准备绘图需要的缓冲区对象

//创建一个对象存放绘制需要的绑定在缓冲区对象的变量
function initVertexBuffers(gl, program) {
  var o = new Object()
  o.vertexBuffer = createEmptyArrayBuffer(gl, program.a_Position, 3, gl.FLOAT)
  o.normalBuffer = createEmptyArrayBuffer(gl, program.a_Normal, 3, gl.FLOAT)
  o.colorBuffer = createEmptyArrayBuffer(gl, program.a_Color, 4, gl.FLOAT)
  o.indexBuffer = gl.createBuffer()
  if (!o.vertexBuffer || !o.normalBuffer || !o.colorBuffer || !o.indexBuffer) {
    return null
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, null)

  return o
}

//为变量创建缓冲区对象、分配缓存并开启
function createEmptyArrayBuffer(gl, a_attribute, num, type) {
  var buffer = gl.createBuffer() //创建缓冲区对象
  if (!buffer) {
    console.log('创建缓冲区对象失败')
    return null
  }
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer) //将buffer绑定到缓冲区对象
  gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0) //缓冲区对象分配给a_attribute指定的地址
  gl.enableVertexAttribArray(a_attribute) //开启a_attribute指定的变量

  return buffer
}

2.4 加载完成数据写入缓冲区对象

// OBJ文件读取并解析
function onReadComplete(gl, model, objDoc) {
  //从OBJ文件中读取顶点坐标、颜色、法线、索引等用于绘图的信息
  var drawingInfo = objDoc.getDrawingInfo()

  //顶点坐标、颜色、法线写入缓冲区对象
  gl.bindBuffer(gl.ARRAY_BUFFER, model.vertexBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, drawingInfo.vertices, gl.STATIC_DRAW)

  gl.bindBuffer(gl.ARRAY_BUFFER, model.normalBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, drawingInfo.normals, gl.STATIC_DRAW)

  gl.bindBuffer(gl.ARRAY_BUFFER, model.colorBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, drawingInfo.colors, gl.STATIC_DRAW)

  //顶点索引写入缓冲区对象
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.indexBuffer)
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, drawingInfo.indices, gl.STATIC_DRAW)

  return drawingInfo
}

3. demo代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title></title>
</head>

<body>
  <!--通过canvas标签创建一个800px*800px大小的画布-->
  <canvas id="webgl" width="800" height="800"></canvas>
  <script type="text/javascript" src="./lib/cuon-matrix.js"></script>
  <script>
    //顶点着色器
    var VSHADER_SOURCE = '' +
      'attribute vec4 a_Position;\\n' + //声明attribute变量a_Position,用来存放顶点位置信息
      'attribute vec4 a_Color;\\n' + //声明attribute变量a_Color,用来存放顶点颜色信息
      'attribute vec4 a_Normal;\\n' + //声明attribute变量a_Normal,用来存放法向量
      'uniform mat4 u_MvpMatrix;\\n' + //声明uniform变量u_MvpMatrix,用来存放模型视图投影组合矩阵
      'uniform mat4 u_NormalMatrix;\\n' + //声明uniform变量u_NormalMatrix,用来存放变换法向量矩阵
      'varying vec4 v_Color;\\n' + //声明varying变量v_Color,用来向片元着色器传值顶点颜色信息
      'void main(){\\n' +
      '  vec3 lightDirection = vec3(-0.35, 0.35, 0.87);\\n' + //声明存放光线方向的变量
      '  gl_Position = u_MvpMatrix * a_Position;\\n' + //将模型视图投影组合矩阵与顶点坐标相乘赋值给顶点着色器内置变量gl_Position
      '  vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\\n' + //计算变换后的法向量并归一化处理
      '  float nDotL = max(dot(normal, lightDirection), 0.0);\\n' + //计算光线方向和法向量点积
      '  v_Color = vec4(a_Color.rgb * nDotL, a_Color.a);\\n' + //计算漫反射光的颜色传给片元着色器
      '}\\n'

    //片元着色器
    var FSHADER_SOURCE = '' +
      '#ifdef GL_ES\\n' +
      ' precision mediump float;\\n' + // 设置精度
      '#endif\\n' +
      'varying vec4 v_Color;\\n' + //声明varying变量v_Color,用来接收顶点着色器传送的片元颜色信息
      'void main(){\\n' +
      '  gl_FragColor = v_Color;\\n' + //将顶点着色器传送的片元颜色赋值给内置变量gl_FragColor
      '}\\n'

    //创建程序对象
    function createProgram(gl, vshader, fshader) {
      //创建顶点着色器对象
      var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader)
      //创建片元着色器对象
      var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader)

      if (!vertexShader || !fragmentShader) {
        return null
      }

      //创建程序对象program
      var program = gl.createProgram()
      if (!gl.createProgram()) {
        return null
      }
      //分配顶点着色器和片元着色器到program
      gl.attachShader(program, vertexShader)
      gl.attachShader(program, fragmentShader)
      //链接program
      gl.linkProgram(program)

      //检查程序对象是否连接成功
      var linked = gl.getProgramParameter(program, gl.LINK_STATUS)
      if (!linked) {
        var error = gl.getProgramInfoLog(program)
        console.log('程序对象连接失败: ' + error)
        gl.deleteProgram(program)
        gl.deleteShader(fragmentShader)
        gl.deleteShader(vertexShader)
        return null
      }
      //使用program
      gl.useProgram(program)
      gl.program = program
      //返回程序program对象
      return program
    }

    function loadShader(gl, type, source) {
      // 创建顶点着色器对象
      var shader = gl.createShader(type)
      if (shader == null) {
        console.log('创建着色器失败')
        return null
      }

      // 引入着色器源代码
      gl.shaderSource(shader, source)

      // 编译着色器
      gl.compileShader(shader)

      // 检查顶是否编译成功
      var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
      if (!compiled) {
        var error = gl.getShaderInfoLog(shader)
        console.log('编译着色器失败: ' + error)
        gl.deleteShader(shader)
        return null
      }

      return shader
    }

    function init() {
      //通过getElementById()方法获取canvas画布
      var canvas = document.getElementById('webgl')

      //通过方法getContext()获取WebGL上下文
      var gl = canvas.getContext('webgl')

      //初始化着色器
      var program = createProgram(gl, VSHADER_SOURCE, FSHADER_SOURCE) //创建绘制单色立方体的程序对象
      if (!program) {
        console.log('初始化着色器失败')
        return
      }

      // 设置canvas的背景色
      gl.clearColor(0.2, 0.2, 0.2, 1)

      //开启隐藏面消除
      gl.enable(gl.DEPTH_TEST)

      //获取绘制图形相关变量的存储地址
      program.a_Position = gl.getAttribLocation(program, 'a_Position') //顶点坐标
      program.a_Color = gl.getAttribLocation(program, 'a_Color') //顶点颜色
      program.a_Normal = gl.getAttribLocation(program, 'a_Normal') //顶点法向量
      program.u_MvpMatrix = gl.getUniformLocation(program, 'u_MvpMatrix') //模型视图投影矩阵
      program.u_NormalMatrix = gl.getUniformLocation(program, 'u_NormalMatrix') //变换法向量矩阵

      if (
        program.a_Position < 0 ||
        program.a_Color < 0 ||
        program.a_Normal < 0 ||
        !program.u_MvpMatrix ||
        !program.u_NormalMatrix
      ) {
        console.log('获取attribute变量或uniform变量存储地址失败')
        return
      }

      //创建缓冲区对象,存放绘制所需要的变量
      var model = initVertexBuffers(gl, program)
      if (!model) {
        console.log('初始化单色立方体顶点信息失败')
        return
      }
      //创建、设置视图投影矩阵
      var viewProjMatrix = new Matrix4()
      viewProjMatrix.setPerspective(30, 1, 1, 5000)
      viewProjMatrix.lookAt(0.0, 500.0, 200.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)

      //读取OBJ文件
      readOBJFile('./models/cube.obj', gl, model, 60, true)

      var currentAngle = 0.0
      var tick = function () {
        currentAngle = getCurrentAngle(currentAngle) //获取当前要旋转的角度    
        draw(gl, program, currentAngle, viewProjMatrix, model) //绘图
        requestAnimationFrame(tick)
      }

      tick() // 调用tick
    }

    var g_LastTime = Date.now() // 上次绘制的时间
    var ANGLE_SET = 30.0 // 旋转速度(度/秒)
    function getCurrentAngle(angle) {
      var now = Date.now()
      var elapsed = now - g_LastTime //上次调用与当前时间差
      g_LastTime = now
      var newAngle = angle + (ANGLE_SET * elapsed) / 1000
      return (newAngle %= 360)
    }

    //创建一个对象存放绘制需要的绑定在缓冲区对象的变量
    function initVertexBuffers(gl, program) {
      var o = new Object()
      o.vertexBuffer = createEmptyArrayBuffer(gl, program.a_Position, 3, gl.FLOAT)
      o.normalBuffer = createEmptyArrayBuffer(gl, program.a_Normal, 3, gl.FLOAT)
      o.colorBuffer = createEmptyArrayBuffer(gl, program.a_Color, 4, gl.FLOAT)
      o.indexBuffer = gl.createBuffer()
      if (!o.vertexBuffer || !o.normalBuffer || !o.colorBuffer || !o.indexBuffer) {
        return null
      }

      gl.bindBuffer(gl.ARRAY_BUFFER, null)

      return o
    }

    //为变量创建缓冲区对象、分配缓存并开启
    function createEmptyArrayBuffer(gl, a_attribute, num, type) {
      var buffer = gl.createBuffer() //创建缓冲区对象
      if (!buffer) {
        console.log('创建缓冲区对象失败')
        return null
      }
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer) //将buffer绑定到缓冲区对象
      gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0) //缓冲区对象分配给a_attribute指定的地址
      gl.enableVertexAttribArray(a_attribute) //开启a_attribute指定的变量

      return buffer
    }

    function readOBJFile(fileName以上是关于WebGL入门(四十三)-WebGL加载OBJ-MTL三维模型的主要内容,如果未能解决你的问题,请参考以下文章

WebGL入门(四十二)-使用(FBO)实现阴影效果

WebGL入门(四十二)-使用(FBO)实现阴影效果

WebGL入门(四十)-通过切换着色器实现一个页面同时展示多个立方体

WebGL入门(四十一)-使用帧缓冲区对象(FBO)实现将渲染结果作为纹理绘制到另一个物体上

前端WebGL技术应用入门

WebGL入门教程-webgl颜色