vue中使用Three.js心得记录

2,792 阅读4分钟

记录

最近公司接到一个新项目,有关包装设计的,里面需要涉及的技术难点就是3d模型这块,由于我们公司的前端都没有接触过3d,也不知为何,老大将重任交付给了我,咱也只好接下了,选用的技术是Nuxt+three.js,这里主要讲一下three.js 的使用

初始化

在vue的项目中先要安装必要的three.js相关插件

npm install three three-orbitcontrols three-obj-mtl-loader stats-js

  • three:核心库;
  • stats-js: 性能检测
  • three-orbitcontrols:控制器鼠标旋转3D模型使用;
  • three-obj-mtl-loader:模型导入加载器; 一个模型能显示在屏幕上的必然条件
  • 需要一个cavnas作为3D渲染的空间;
  • 场景: 用来放置各种模型,相机或者光源的;
  • 相机: 用来观看场景; 相机分为透视相机和正视相机;
  • 光源: 用来照亮场景,以及物体;
  • 模型: 模型可以自己通过three.js构建,也可以采用3D建模软件建立好的模型进行导入,本文采用的是.Obj格式的模型文件导入。

1、创建场景

createScene() {
    this.container = document.getElementById("view-box");
    // 创建场景(所有的模型,相机,灯光,都需要通过add方法添加到场景中)
    this.scene = new THREE.Scene();
    // 添加场景背景色
    this.scene.background = new THREE.Color(0xf3f3f3);
    // 创建渲染函数
    this.renderer = new THREE.WebGLRenderer({
        // 抗锯齿 模型质量更高 消耗性能越大
        antialias: true, 
        // 开启透明度
        alpha: true 
    });
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize(this.windowHalfX, this.windowHalfY);
    // 将渲染的3d场景挂载到指定的DOM上
    this.container.appendChild(this.renderer.domElement);
}

2、创建相机

createCamera() {
    this.camera = new THREE.PerspectiveCamera(
        // 摄像机视锥体垂直视野角度 
        45, 
        // 摄像机视锥体长宽比 (要渲染区域的 长 / 宽 ,如果不对渲染会出现变形效果)
        this.windowHalfX / this.windowHalfY,
        // 摄像机视锥体远端面 (最近能看到的距离)
        1, 
        // 摄像机视锥体近端面 (最远能看到的距离)
        2000 
    );
    // 设置相机距离屏幕的距离
    this.camera.position.z = 50; 
    // 将相机添加到场景中
    this.scene.add(this.camera);
},

3、创建灯光

灯光不做详细介绍,在本次项目中没有使用到,只用到了基本的环境光

createLight() {
    let color = 0xf9f9f9
    // 创建环境光,环境光没有投影,会均匀的照亮环境中所有的物体
    let ambientLight = new THREE.AmbientLight(color);
    // 将环境光添加到场景中
    this.scene.add(ambientLight);
    // 创建聚光灯光源,有投影
    let directionalLight = new THREE.DirectionalLight(0xb4b6fd); 
    directionalLight.position.set(40, 60, 20);
    directionalLight.castShadow = true;
    directionalLight.shadowCameraNear = 2;
    directionalLight.shadowCameraFar = 100;
    directionalLight.shadowCameraLeft = -50;
    directionalLight.shadowCameraRight = 50;
    directionalLight.shadowCameraTop = 50;
    directionalLight.shadowCameraBottom = -50;
    // 将聚光灯添加到场景中
    this.scene.add(directionalLight);
},

4、创建模型 导入的模型格式为(.obj)

createModel() {
    let mtlLoader = new MTLLoader() 
    let loader = new OBJLoader(this.manager);
    // 加载材质.mtl文件
    mtlLoader.load(`/obj/white-box.mtl`, (materials) => {
    materials.preload();
    loader.load(`/obj/抽屉盒-简模贴图.obj`, (obj) => {
        this.object = obj;
        // 设置模型中心点居中
        this.object.children[0].geometry.center()
        this.object.children[0].geometry.computeBoundingBox();
      }
    );
  })
},

5、创建贴图

涉及到需要改变每个面上贴图的会有多层,所以选择使用canvas贴图,在canvas上操作更方便

/**
 * 创建一个贴图
 */
createTexture() {
  this.currentModelTexture.top = new THREE.CanvasTexture(this.createCanvas());
  this.lodingModelAndTexture()
},
/**
 * 加载模型贴图
 */
lodingModelAndTexture() {
    let loadModel = () => {
    this.object.traverse((child) => {
    // 设置模型的长宽高比例
    if (child.isMesh) child.scale.set(1, 1, 1);
        if (child instanceof THREE.Mesh) {
          if (child.material instanceof Array) {
          	child.material.forEach(item => {
                // 本身项目中有很多面
                // item.map = this.currentModelTexture[item.name]  
                // 这是为了方便理解,只开放了一个面
                item.map = this.currentModelTexture.top 
            });
            child.castShadow = true;
            child.receiveShadow = true;
          }
        }
    });
    // 将模型添加到场景
    this.scene.add(this.object);
  };
},
/**
 * 返回一个canvas
 */
createCanvas() {
    const canvas = utils.createHiDPICanvas(width, height, 4)
    const ctx = canvas.getContext('2d');
          ctx.fillStyle = #fff;
          ctx.fillRect(0, 0, 画布宽, 画布高);
          ctx.textBaseline = "hanging";
    let img = new Image();
        img.src = '1.jpg';
    // 透明度 | 旋转
    let alpha = 1, 
    	deg = 180;
    img.onload = (e) => {
      // 保存画布状态
      ctx.save(); 
      ctx.globalAlpha = alpha;
      // 设置旋转按照图片的中心旋转
      ctx.translate(画布宽 / 2, 画布高 / 2); 
      ctx.rotate(deg * Math.PI / 180);
      ctx.drawImage(zindex.img, x, y, 图片原始宽度, 图片原始高度, -(w / 2), -(h / 2), w, h);
      // 恢复状态
      ctx.restore(); 
    }
    return canvas
},
/**
 * 创建高清canvas画布
 * @param { 宽 } w 
 * @param { 高 } h 
 * @param { 像素 } ratio 
 */
createHiDPICanvas(w, h, ratio) {
  const PIXEL_RATIO = (function() {
      const c = document.createElement("canvas"),
            ctx = c.getContext("2d"),
            dpr = window.devicePixelRatio || 1,
            bsr = ctx['webkitBackingStorePixelRatio'] ||
            ctx['mozBackingStorePixelRatio'] ||
            ctx['msBackingStorePixelRatio'] ||
            ctx['oBackingStorePixelRatio'] ||
            ctx['backingStorePixelRatio'] || 1;
      return dpr / bsr;
  })();
  if (!ratio) { ratio = PIXEL_RATIO; }
  const can = document.createElement("canvas");
        can.width = w * ratio;
        can.height = h * ratio;
        can.style.width = w + "px";
        can.style.height = h + "px";
        can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
  return can;
}

6、动画

// 动画
animate() {
  if(this.object) {
      this.object.children[0].rotation.y += 0.001
  }
  // 原生js动画函数
  requestAnimationFrame(this.animate);
  this.renderer && this.render();
},
// 渲染
render() {
  this.camera.lookAt(this.scene.position);
  this.renderer.render(this.scene, this.camera);
},
init() {
  this.createScene()
  this.createCamera()
  this.createLight()
  this.createTexture()
  this.$nextTick(() => {
    this.createModel()
  });
  this.createOther()
},
// 开始加载
loding() {
  this.windowHalfX = this.$refs.three.offsetWidth;
  this.windowHalfY = window.innerHeight - 80;
  this.init();
  this.animate();
},

7、其他控制器以及辅助设置

createOther() {
  // 辅助对象
  var AxesHelper = new THREE.AxesHelper(150);
  this.scene.add(AxesHelper);
  // 控制器
  var controls = new OrbitControls(this.camera, this.renderer.domElement);
  this.renderer.domElement.removeAttribute('tabindex')
  controls.addEventListener("change", this.render);
  this.render();
},

8、性能优化,缓存清理

// 清空渲染器缓存
clearRenderer() {
  this.object && this.clearCache(this.object.children[0])
  this.renderer.forceContextLoss();
  this.currentModelTexture = null
  this.renderer.dispose();
  this.renderer.domElement = null;
  this.renderer = null;
}
// 在vue的销毁钩子中调用
destroyed() {
  this.clearRenderer()
}

9、其他用到的函数方法

插入排序

/**
 * @param { 排序数组 } arr
 * @param { 要对比的值 } val
 */
const Insertion = (arr, val) => {
    let handle = []
        handle.push(arr[0])
    for (let i = 1; i < arr.length; i++) {
        let a = arr[i][val]
        for (let j = handle.length - 1; j >= 0; j--) {
            let b = handle[j][val]
            if (a > b) {
                handle.splice(j + 1, 0, arr[i])
                break
            }
            if (j === 0) {
                handle.unshift(arr[i])
            }
        }
    }
    return handle
}

图片转base64

/**
 * @param { 图片加载地址 } url 
 */
const converbase64Img = (url) => {
    var canvas = document.createElement('CANVAS'),
        ctx = canvas.getContext('2d');
    return new Promise((resolve, reject) => {
        let img = new Image;
            img.crossOrigin = '';
            img.src = url;
            img.onload = function() {
                canvas.height = img.height;
                canvas.width = img.width;
                ctx.drawImage(img, 0, 0);
                var dataURL = canvas.toDataURL('image/png');
                resolve(dataURL)
                canvas = null;
            };
            img.onerror = (e) => {
                reject(e)
            }
    })
}