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, gl, model, scale, reverse) {
      var request = new XMLHttpRequest()

      request.onreadystatechange = function () {
        if (request.readyState === 4 && request.status !== 404) {
          onReadOBJFile(request.responseText, fileName, gl, model, scale, reverse)
        }
      }
      request.open('GET', fileName, true) //创建请求
      request.send() //发送请求
    }

    var g_objDoc = null //OBJ文件信息
    var g_drawingInfo = null //绘制三维模型信息

    //文件读取完成处理
    function onReadOBJFile(fileString, fileName, gl, o, scale, reverse) {
      var objDoc = new OBJDoc(fileName) //创建OBJDoc对象
      var result = objDoc.parse(fileString, scale, reverse) //解析文件
      if (!result) {
        g_objDoc = null
        g_drawingInfo = null
        console.log('OBJ文件解析失败')
        return
      }
      g_objDoc = objDoc //解析成功赋值给全局变量g_objDoc
    }

    //模型矩阵、模型视图投影矩阵、法向量矩阵
    var g_modelMatrix = new Matrix4()
    var g_mvpMatrix = new Matrix4()
    var g_normalMatrix = new Matrix4()

    //绘制函数
    function draw(gl, program, angle, viewProjMatrix, model) {
      // OBJ和MTL文件解析完成
      if (g_objDoc != null && g_objDoc.isMTLComplete()) {
        g_drawingInfo = onReadComplete(gl, model, g_objDoc)
        g_objDoc = null
      }
      if (!g_drawingInfo) return //文件解析未完成返回

      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) //清空颜色和深度缓冲区

      //计算模型矩阵
      g_modelMatrix.setRotate(angle, 1.0, 0.0, 0.0)
      g_modelMatrix.rotate(angle, 0.0, 1.0, 0.0)
      g_modelMatrix.rotate(angle, 0.0, 0.0, 1.0)

      //根据模型矩阵计算变换法向量的矩阵并传值
      g_normalMatrix.setInverseOf(g_modelMatrix)
      g_normalMatrix.transpose()
      gl.uniformMatrix4fv(program.u_NormalMatrix, false, g_normalMatrix.elements)

      //计算模型视图投影矩阵并传值
      g_mvpMatrix.set(viewProjMatrix)
      g_mvpMatrix.multiply(g_modelMatrix)
      gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.elements)

      //绘制
      gl.drawElements(
        gl.TRIANGLES,
        g_drawingInfo.indices.length,
        gl.UNSIGNED_SHORT,
        0
      )
    }

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

    // OBJDoc 对象构造函数
    var OBJDoc = function (fileName) {
      this.fileName = fileName
      this.mtls = new Array(0) //MTL材质列表
      this.objects = new Array(0) //Obj对象列表
      this.vertices = new Array(0) //顶点列表
      this.normals = new Array(0) //法线列表
    }

    // 解析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
    }

    OBJDoc.prototype.parseMtllib = function (sp, fileName) {
      var i = fileName.lastIndexOf('/')
      var dirPath = ''
      if (i > 0) dirPath = fileName.substr(0, i + 1)

      return dirPath + sp.getWord()
    }

    OBJDoc.prototype.parseObjectName = function (sp) {
      var name = sp.getWord()
      return new OBJObject(name)
    }

    OBJDoc.prototype.parseVertex = function (sp, scale) {
      var x = sp.getFloat() * scale
      var y = sp.getFloat() * scale
      var z = sp.getFloat() * scale
      return new Vertex(x, y, z)
    }

    OBJDoc.prototype.parseNormal = function (sp) {
      var x = sp.getFloat()
      var y = sp.getFloat()
      var z = sp.getFloat()
      return new Normal(x, y, z)
    }

    OBJDoc.prototype.parseUsemtl = function (sp) {
      return sp.getWord()
    }

    OBJDoc.prototype.parseFace = function (sp, materialName, vertices, reverse) {
      var face = new Face(materialName)
      //获取索引
      for (;;) {
        var word = sp.getWord()
        if (word == null) break
        var subWords = word.split('/')
        if (subWords.length >= 1) {
          var vi = parseInt(subWords[0]) - 1
          face.vIndices.push(vi)
        }
        if (subWords.length >= 3) {
          var ni = parseInt(subWords[2]) - 1
          face.nIndices.push(ni)
        } else {
          face.nIndices.push(-1)
        }
      }

      //计算法向量
      var v0 = [
        vertices[face.vIndices[0]].x,
        vertices[face.vIndices[0]].y,
        vertices[face.vIndices[0]].z
      ]
      var v1 = [
        vertices[face.vIndices[1]].x,
        vertices[face.vIndices[1]].y,
        vertices[face.vIndices[1]].z
      ]
      var v2 = [
        vertices[face.vIndices[2]].x,
        vertices[face.vIndices[2]].y,
        vertices[face.vIndices[2]].z
      ]

      //根据面上的三个点计算面的法线
      var normal = calcNormal(v0, v1, v2)
      //前三个点计算法线为空,重新计算
      if (normal == null) {
        if (face.vIndices.length >= 4) {
          //面上顶点数大于4,取第二三四个顶点计算法线
          var v3 = [
            vertices[face.vIndices[3]].x,
            vertices[face.vIndices[3]].y,
            vertices[face.vIndices[3]].z
          ]
          normal = calcNormal(v1, v2, v3)
        }
        if (normal == null) {
          //法线为空没有四个以上顶点取Y轴反向单位向量作为法线
          normal = [0.0, 1.0, 0.0]
        }
      }
      if (reverse) {
        normal[0] = -normal[0]
        normal[1] = -normal[1]
        normal[2] = -normal[2]
      }
      face.normal = new Normal(normal[0], normal[1], normal[2])

      // 面上顶点数大于3,拆分为三角形
      if (face.vIndices.length > 3) {
        var n = face.vIndices.length - 2
        var newVIndices = new Array(n * 3)
        var newNIndices = new Array(n * 3)
        for (var i = 0; i < n; i++) {
          newVIndices[i * 3 + 0] = face.vIndices[0]
          newVIndices[i * 3 + 1] = face.vIndices[i + 1]
          newVIndices[i * 3 + 2] = face.vIndices[i + 2]
          newNIndices[i * 3 + 0] = face.nIndices[0]
          newNIndices[i * 3 + 1] = face.nIndices[i + 1]
          newNIndices[i * 3 + 2] = face.nIndices[i + 2]
        }
        face.vIndices = newVIndices
        face.nIndices = newNIndices
      }
      face.numIndices = face.vIndices.length

      return face
    }

    // 解析材质文件
    function onReadMTLFile(fileString, mtl) {
      var lines = fileString.split('\\n') //根据换行符拆分成数组
      lines.push(null) //添加结束标识
      var index = 0 // 初始化当前行索引

      // 按行解析
      var line //接收当前文本行内容
      var name = '' //接收当前材质名称
      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 'newmtl': //读取材质文件
            name = mtl.parseNewmtl(sp) //获取材质名称
            continue //继续解析
          case 'Kd': //读取法线
            if (name == '') continue //name为空继续读取
            var material = mtl.parseRGB(sp, name)
            mtl.materials.push(material)
            name = ''
            continue //继续读取
        }
      }
      mtl.complete = true
    }

    //材质检查
    OBJDoc.prototype.isMTLComplete = function () {
      if (this.mtls.length == 0) return true
      for (var i = 0; i < this.mtls.length; i++) {
        if (!this.mtls[i].complete) return false
      }
      return true
    }

    //根据材质名查找颜色
    OBJDoc.prototype.findColor = function (name) {
      for (var i = 0; i < this.mtls.length; i++) {
        for (var j = 0; j < this.mtls[i].materials.length; j++) {
          if (this.mtls[i].materials[j].name == name) {
            return this.mtls[i].materials[j].color
          }
        }
      }
      return new Color(0.8, 0.8, 0.8, 1)
    }

    //获取将要绘制三维模型的信息
    OBJDoc.prototype.getDrawingInfo = function () {
      //创建存放顶点坐标、法线、颜色和索引的数组
      var numIndices = 0
      for (var i = 0; i < this.objects.length; i++) {
        numIndices += this.objects[i].numIndices
      }
      var numVertices = numIndices
      var vertices = new Float32Array(numVertices * 3)
      var normals = new Float32Array(numVertices * 3)
      var colors = new Float32Array(numVertices * 4)
      var indices = new Uint16Array(numIndices)

      //设置顶点、法线、颜色
      var index_indices = 0
      for (var i = 0; i < this.objects.length; i++) {
        var object = this.objects[i]
        for (var j = 0; j < object.faces.length; j++) {
          var face = object.faces[j]
          var color = this.findColor(face.materialName)
          var faceNormal = face.normal
          for (var k = 0; k < face.vIndices.length; k++) {
            //设置索引
            indices[index_indices] = index_indices
            //复制顶点
            var vIdx = face.vIndices[k]
            var vertex = this.vertices[vIdx]
            vertices[index_indices * 3 + 0] = vertex.x
            vertices[index_indices * 3 + 1] = vertex.y
            vertices[index_indices * 3 + 2] = vertex.z
            //复制颜色
            colors[index_indices * 4 + 0] = color.r
            colors[index_indices * 4 + 1] = color.g
            colors[index_indices * 4 + 2] = color.b
            colors[index_indices * 4 + 3] = color.a
            //复制法线
            var nIdx = face.nIndices[k]
            if (nIdx >= 0) {
              var normal = this.normals[nIdx]
              normals[index_indices * 3 + 0] = normal.x
              normals[index_indices * 3 + 1] = normal.y
              normals[index_indices * 3 + 2] = normal.z
            } else {
              normals[index_indices * 3 + 0] = faceNormal.x
              normals[index_indices * 3 + 1] = faceNormal.y
              normals[index_indices * 3 + 2] = faceNormal.z
            }
            index_indices++
          }
        }
      }

      return new DrawingInfo(vertices, normals, colors, indices)
    }

    // MTLDoc对象声明
    var MTLDoc = function () {
      this.complete = false // MTL默认未完成解析
      this.materials = new Array(0)
    }

    MTLDoc.prototype.parseNewmtl = function (sp) {
      return sp.getWord() //获取MTL名称
    }

    MTLDoc.prototype.parseRGB = function (sp, name) {
      var r = sp.getFloat()
      var g = sp.getFloat()
      var b = sp.getFloat()
      return new Material(name, r, g, b, 1)
    }

    // Material对象声明
    var Material = function (name, r, g, b, a) {
      this.name = name
      this.color = new Color(r, g, b, a)
    }

    // Vertex声明
    var Vertex = function (x, y, z) {
      this.x = x
      this.y = y
      this.z = z
    }

    // Normal声明
    var Normal = function (x, y, z) {
      this.x = x
      this.y = y
      this.z = z
    }

    // Color声明
    var Color = function (r, g, b, a) {
      this.r = r
      this.g = g
      this.b = b
      this.a = a
    }

    // OBJObject声明
    var OBJObject = function (name) {
      this.name = name
      this.faces = new Array(0)
      this.numIndices = 0
    }

    OBJObject.prototype.addFace = function (face) {
      this.faces.push(face)
      this.numIndices += face.numIndices
    }

    // Face对象声明
    var Face = function (materialName) {
      this.materialName = materialName
      if (materialName == null) this.materialName = ''
      this.vIndices = new Array(0)
      this.nIndices = new Array(0)
    }

    // DrawInfo对象声明
    var DrawingInfo = function (vertices, normals, colors, indices) {
      this.vertices = vertices
      this.normals = normals
      this.colors = colors
      this.indices = indices
    }

    // StringParser对象构造函数
    var StringParser = function (str) {
      this.str //存放字符串化的参数
      this.index //当前读取的字符的下标
      this.init(str)
    }
    //StringParser对象初始化
    StringParser.prototype.init = function (str) {
      this.str = str
      this.index = 0
    }

    // 跳过分隔符
    StringParser.prototype.skipDelimiters = function () {
      for (var i = this.index, len = this.str.length; i < len; i++) {
        var c = this.str.charAt(i)
        if (c == '\\t' || c == ' ' || c == '(' || c == ')' || c == '"') continue
        break
      }
      this.index = i
    }

    //跳到下一个单词
    StringParser.prototype.skipToNextWord = function () {
      this.skipDelimiters()
      var n = getWordLength(this.str, this.index)
      this.index += n + 1
    }

    //获取单词
    StringParser.prototype.getWord = function () {
      this.skipDelimiters()
      var n = getWordLength(this.str, this.index)
      if (n == 0) return null
      var word = this.str.substr(this.index, n)
      this.index += n + 1

      return word
    }

    //获取整数
    StringParser.prototype.getInt = function () {
      return parseInt(this.getWord())
    }

    //获取浮点数
    StringParser.prototype.getFloat = function () {
      return parseFloat(this.getWord())
    }

    //获取单词的长度
    function getWordLength(str, start) {
      var n = 0
      for (var i = start, len = str.length; i < len; i++) {
        var c = str.charAt(i)
        if (c == '\\t' || c == ' ' || c == '(' || c == ')' || c == '"') break
      }
      return i - start
    }

    function calcNormal(p0, p1, p2) {
      // 向量v0由p1指向p0, v1; 向量v1由p1指向p2,
      var v0 = new Float32Array(3)
      var v1 = new Float32Array(3)
      for (var i = 0; i < 3; i++) {
        v0[i] = p0[i] - p1[i]
        v1[i] = p2[i] - p1[i]
      }

      //计算向量v0、v1的叉乘
      var c = new Float32Array(3)
      c[0] = v0[1] * v1[2] - v0[2] * v1[1]
      c[1] = v0[2] * v1[0] - v0[0] * v1[2]
      c[2] = v0[0] * v1[1] - v0[1] * v1[0]

      //叉乘结果归一化
      var v = new Vector3(c)
      v.normalize()
      return v.elements
    }

    init()
  </script>
</body>

</html>

© 版权声明
THE END
喜欢就支持一下吧
点赞693 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容