ThreeJS渲染一个.obj三维模型文件(Vue)

先来看一个效果:


image.png

最近有个项目需要实现三维模型的web端渲染,以前虽然也做过类似的项目,单是两个项目一个是java Application,一个是安卓结合,两个我都只参与到的建模环节,所以知道三维模型文件的大概结构,要想在web端实现渲染,首先要做的就是读取这些模型文件,对里面的点、面、法线、材质进行逐行解析。

各种对比后,发现了ThreeJS。它不仅可以解析obj模型文件,还可以解析大部分市场上有的模型格式文件。
npm 安装后,在node_modules/three/examples/jsm/loaders/目录下可以看到它支持的模型格式。


image.png

image.png

image.png

PS:demo中使用了最流行vue语法。

1.首先要有个dom容器

<div id="canvas_div"></div>

2.引用

import * as THREE from 'three'
import Stats from 'three/examples/jsm/libs/stats.module.js'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

3.数据存储

data () {
    return {
      msg: 'This is webGL',
      renderer: null,
      camera: null,
      scene: null,
      light: null,
      stats: null,
      controls: null,
      canvasDiv: {
        element: null,
        wigth: '',
        height: ''
      },
      mesh: null,
      objChildren: [],
      mousePosition: {
        mouseX: 0,
        mouseY: 0
      },
      raycaster: null,
      mouse: null,
      selectObj: null,
      timer: null,
      animationRenId: null,
      animationAniId: null
    }

4.方法

容器
/* *
     * 初始化容器
     * */
    initThree () {
      let _el = document.getElementById('canvas_div')
      this.canvasDiv.element = _el
      this.canvasDiv.wigth = _el.clientWidth
      this.canvasDiv.height = _el.clientHeight
    }
渲染器
initRenderer () {
      this.renderer = new THREE.WebGLRenderer()
      // this.renderer.setPixelRatio(window.devicePixelRatio) // 设置像素比
      this.renderer.setSize(this.canvasDiv.wigth, this.canvasDiv.height)
      this.renderer.setClearColor(0xCCCCCC, 1.0)
      this.canvasDiv.element.appendChild(this.renderer.domElement)

    }
相机
/* *
     * 初始化相机
     * */
    initCamera () {

      this.camera = new THREE.PerspectiveCamera(75, this.canvasDiv.wigth / this.canvasDiv.height, 0.5, 1000)

      // 设置相机距离原点坐标的位置
      this.camera.position.x = 200
      this.camera.position.y = 200
      this.camera.position.z = 200

      // this.camera.up.x = 0
      // this.camera.up.y = 0
      // this.camera.up.z = 1
      // this.camera.lookAt({
      //   x: 0,
      //   y: 0,
      //   z: 0
      // })
    }
场景
 /* *
     * 初始化场景
     * */
    initScene () {
      this.scene = new THREE.Scene()
    }
加载模型
/* *
     * 加载模型
     * */
    initModel () {
      // 辅助工具:一个轴对象,以一种简单的方式可视化三个轴。
      // X轴为红色。 Y轴为绿色。 Z轴为蓝色。
      var helper = new THREE.AxesHelper(1000)
      this.scene.add(helper)

      let that = this

      var objLoader = new OBJLoader()
      var mtlLoader = new MTLLoader()
      mtlLoader.setPath('./static/webgl/')
      // 加载mtl文件
      mtlLoader.load('rate.mtl', function (material) {
        // 预加载
        material.preload()

        // 设置当前加载的纹理
        objLoader.setMaterials(material)
        objLoader.setPath('./static/webgl/')
        objLoader.load('rate.obj', function (object) {
          if (object.children) {
            var meshes = object.children
            meshes.forEach(element => {
              element.material.color.setHex('0xfafafa')
            })
            // 获取模型的某个部位
            var obj2 = object.children[2]

            // 设置模型某部位的样式
            obj2.material.color.setHex('0xff0000')
            obj2.material.opacity = 0.6
            obj2.material.transparent = true
            // obj2.material.depthTest = false
            that.objChildren.push(obj2)

            // 将模型缩放并添加到场景当中
            object.scale.set(1, 1, 1)
            that.scene.add(object)
          }
        }, that.onProgress, that.onError)
      })
    }
模型加载进程
/*
     * 模型加载进程
     */
    onProgress (xhr) {
      if (xhr.lengthComputable) {
        var percentComplete = xhr.loaded / xhr.total * 100
        console.log(Math.round(percentComplete, 2) + '% downloaded')
      }
    }
灯光
initLight () {
      const ambientLight = new THREE.AmbientLight(0xCCCCCC, 0.4) // 环境光
      this.scene.add(ambientLight)

      this.light = new THREE.PointLight(0xffffff, 1) // 点光源
      this.light.position.set(50, 200, 100)
      // this.light.position.multiplyScalar(1.3) // 标量
      // this.light.castShadow = true // 告诉平行光需要开启阴影投射
      // this.light.shadow.mapSize.width = 500
      // this.light.shadow.mapSize.height = 500

      // var d = 300

      // this.light.shadow.camera.left = -d
      // this.light.shadow.camera.right = d
      // this.light.shadow.camera.top = d
      // this.light.shadow.camera.bottom = -d

      // this.light.shadow.camera.far = 1000

      this.scene.add(this.light)
    }
性能检测插件
/* *
     * 初始化性能检测插件
     * */
    initStats () {
      this.stats = new Stats()
      this.stats.dom.style.position = 'absolute'
      this.stats.dom.style.left = '0px'
      this.stats.dom.style.top = '0px'
      this.canvasDiv.element.appendChild(this.stats.dom)
    }
用户交互插件
/* *
     * 用户交互插件 鼠标左键按住旋转,右键按住平移,滚轮缩放
     * */
    initControls () {
      this.controls = new OrbitControls(this.camera, this.renderer.domElement) // , this.renderer.domElement

      // 如果使用animate方法时,将此函数删除
      // this.controls.addEventListener('change', this.render)
      // 使动画循环使用时阻尼或自转 意思是否有惯性
      this.controls.enableDamping = true
      // 动态阻尼系数 就是鼠标拖拽旋转灵敏度
      // controls.dampingFactor = 0.25;
      // 是否可以缩放
      this.controls.enableZoom = true
      // 是否自动旋转
      this.controls.autoRotate = false
      // 设置相机距离原点的最远距离
      this.controls.minDistance = 1
      // 设置相机距离原点的最远距离
      this.controls.maxDistance = 1000
      // 是否开启右键拖拽
      this.controls.enablePan = true
    }
渲染
/* *
      * 渲染
     * */
    render () {
      this.renderer.clear()
      this.renderer.render(this.scene, this.camera)
      requestAnimationFrame(this.render)
    }
鼠标事件
/*
     * 鼠标跟随事件
     */
    onDocumentMouseMove (event) {
      event.preventDefault()

      if (this.timer) {
        clearTimeout(this.timer)
      }
      this.timer = setTimeout(() => {
        // this.mousePosition.mouseX = (event.clientX - this.canvasDiv.wigth) / 2
        // this.mousePosition.mouseY = (event.clientY - this.canvasDiv.height) / 2
        // 光标的位置

        this.mouse = new THREE.Vector2()
        this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
        this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1

        // 获取焦点
        var raycaster = new THREE.Raycaster()
        if (this.camera) {
          raycaster.setFromCamera(this.mouse, this.camera)
        }
        // console.log(this.scene.children[3])
        if (this.scene && this.scene.children && this.scene.children.length > 0) {
          var vPicker = this.scene.children[3] !== undefined ? this.scene.children[3]['children'] : []
          var intersects = raycaster.intersectObjects(vPicker)
          // console.log(this.scene.children[3])
          // console.log(this.scene.children[3]['children'])
          // console.log(intersects)
          if (intersects.length > 0) {
            var res = intersects.filter(function (res) {
              return res && res.object
            })[0]
            if (res && res.object) {
              this.selectObj = res.object
              // 暂存原有材质颜色
              // var _color = res.object.material.color.getHex()
              // this.selectObj.currentHex = parseInt('0x' + _color)
              // console.log(this.selectObj.currentHex)

              document.getElementsByTagName('body')[0].style.cursor = 'pointer' // 移到物体上时鼠标显示为手
              this.selectObj.material.color.setHex('0xffc466')
            }
          } else {
            // 鼠标移除时恢复材质颜色
            if (this.selectObj) this.selectObj.material.color.setHex('0xfafafa')
            document.getElementsByTagName('body')[0].style.cursor = 'default' // 移出物体时鼠标显示为默认
            // this.selectObj = null
          }
        }
      }, 200)
    }
页面优化第一步

组件注销前,解绑全局事件、停止刷新。beforeDestroy()

beforeDestroy () {
    // 解绑事件、停止刷新
    window.removeEventListener('mousemove', this.onDocumentMouseMove, false)
    window.cancelAnimationFrame(this.animationRenId)
    window.cancelAnimationFrame(this.animationAniId)
    this.renderer.dispose()

    // 清理数据
    this.raycaster = null
    this.objChildren = []
    this.camera = null
    this.light = null
    this.scene = null
    this.renderer = null
  }

调用方法:
window.addEventListener('mousemove', this.onDocumentMouseMove, false)
mousemove事件是一个频繁发生的事件,这里使用setTimeout控制延迟200ms执行,达到优化的目的。
思路:鼠标移入获取焦点,设置材质的颜色为黄色

效果如下:


image.png

PS:鼠标移入时,先暂存材质的颜色,移除后再恢复。实现过程有bug,getHex()未得到有效的颜色,所以恢复不到原有的材质颜色,正在查找原因。如有有哪位大神发现问题,请赐教。

页面加载性能优化:
问题描述:页面模型加载后,页面整体性能下降,出现卡顿。

优化内容:

  1. 本项目不需要对象和纹理显示单独的加载进度,所以在模型加载步骤中,去掉了LoadingManager
var manager = new THREE.LoadingManager()

2.页面组件销毁时,解绑鼠标事件
3.组件销毁时,清除缓存数据

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