Vue + Three.js导入obj模型并实现爆炸效果

Three.js官方文档:https://threejs.org/manual/#zh/load-obj

注意点:obj文件需要放到public文件夹下的static文件夹中(路径写法错误会导致模型不显示,可以在network里查看文件是否被加载)
// 完整代码
<template>
  <div id="container"></div>
  <el-slider
    v-model="value"
    show-input
    :min="1"
    :max="100"
    style="
      width: 50%;
      position: absolute;
      bottom: 20%;
      left: 50%;
      transform: translateX(-50%);
    "
    @change="onChange"
  />
</template>

<script>
import * as THREE from "three";
import Stats from "stats-js";
import * as Dat from "dat-gui";
import TrackballControls from "three-trackballcontrols";
// import { OBJLoader } from "three-obj-mtl-loader";
// import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";

export default {
  name: "LoadObjMtl",
  data() {
    return {
      step: 0,
      value: 0,
    };
  },
  mounted() {
    this.renderer = "";
    this.camera = "";
    this.scene = "";
    this.light = "";
    this.gui = "";
    this.axesHelper = "";
    this.stats = "";
    this.trackballControls = "";
    this.clock = "";
    this.jeepCar = "";
    this.controls = "";
    this.objLoader = "";
    this.mtlLoader = "";
    this.gltfLoader = "";
    this.plane = "";

    // 执行
    this.execute();
    // 窗口大小变化
    window.onresize = this.onWindowResize;
  },
  methods: {
    initScene() {
      this.scene = new THREE.Scene();
    },
    initCamera() {
      this.camera = new THREE.PerspectiveCamera(
        45,
        window.innerWidth / window.innerHeight,
        0.1,
        5000
      );
      // 设置相机位置
      this.camera.position.x = -400;
      this.camera.position.y = 400;
      this.camera.position.z = 400;
      // 设置相机指向的位置 默认0,0,0
      this.camera.lookAt(this.scene.position);
    },
    initHelper() {
      //   this.axesHelper = new THREE.AxesHelper(100);
      //   this.scene.add(this.axesHelper);
    },
    initRender() {
      this.renderer = new THREE.WebGLRenderer({ antialias: true });
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      // 告诉渲染器需要阴影效果
      // this.renderer.shadowMap.enabled = true;
      // this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
      // 设置背景色
      this.renderer.setClearColor(new THREE.Color("rgb(61,61,61)"));
      document
        .querySelector("#container")
        .appendChild(this.renderer.domElement);
    },
    initStats() {
      this.stats = new Stats();
      document.body.appendChild(this.stats.dom);
    },
    getWorldCenterPosition(box, scalar = 0.5) {
      return new THREE.Vector3()
        .addVectors(box.max, box.min)
        .multiplyScalar(scalar);
    },
    explodeModel(model, scalar) {
      model.traverse(function (value) {
        // @ts-ignore
        if (!value.isMesh || !value.userData.originPosition) return;
        const distance = value.userData.worldDir
          .clone()
          .multiplyScalar(value.userData.worldDistance.length() * scalar);
        const offset = new THREE.Vector3().subVectors(
          value.userData.meshCenter,
          value.userData.originPosition
        );
        const center = value.userData.explodeCenter;
        const newPos = new THREE.Vector3()
          .copy(center)
          .add(distance)
          .sub(offset);
        const localPosition = value.parent?.worldToLocal(newPos.clone());
        localPosition && value.position.copy(localPosition);
      });
    },
    initExplodeModel(modelObject) {
      if (!modelObject) return;

      // 计算模型中心
      const explodeBox = new THREE.Box3();
      explodeBox.setFromObject(modelObject);
      const explodeCenter = this.getWorldCenterPosition(explodeBox);

      const meshBox = new THREE.Box3();

      const that = this;

      // 遍历整个模型,保存数据到userData上,以便爆炸函数使用
      modelObject.traverse(function (value) {
        if (
          value.isMark ||
          value.isMarkChild ||
          value.isLine ||
          value.isSprite
        ) {
          return;
        }
        if (value.isMesh) {
          meshBox.setFromObject(value);

          const meshCenter = that.getWorldCenterPosition(meshBox);
          // 爆炸方向
          value.userData.worldDir = new THREE.Vector3()
            .subVectors(meshCenter, explodeCenter)
            .normalize();
          // 爆炸距离 mesh中心点到爆炸中心点的距离
          value.userData.worldDistance = new THREE.Vector3().subVectors(
            meshCenter,
            explodeCenter
          );
          // 原始坐标
          value.userData.originPosition = value.getWorldPosition(
            new THREE.Vector3()
          );
          // mesh中心点
          value.userData.meshCenter = meshCenter.clone();
          value.userData.explodeCenter = explodeCenter.clone();
        }
      });
    },
    initModel() {
      // 实例化mtl loader
      // this.mtlLoader = new MTLLoader();
      const that = this;

      // 加载mtl
      //   this.mtlLoader.load("objs/jeep/jeepCar.mtl", function(materials) {
      //     materials.preload()
      //     that.objLoader.setMaterials(materials)
      //     // 加载obj
      //     that.objLoader.load("objs/jeep/jeepCar.obj", function(obj) {
      //       // 模型文件太大,缩小一下比例,方便显示
      //       obj.scale.set(0.1, 0.1, 0.1)
      //       // 设置可以投影
      //       obj.children.forEach(item => {
      //         item.castShadow = true
      //         item.receiveShadow = true
      //       })
      //       that.jeepCar = obj
      //       // 添加到场景
      //       that.scene.add(that.jeepCar)
      //     })
      //   })

      // 实例化obj loader
      this.objLoader = new OBJLoader();

      this.objLoader.load("static/f019d04068.obj", function (obj) {
        console.log(obj);
        obj.traverse((node) => {
          node.material = new THREE.MeshLambertMaterial({
            color: "#72757A",
            emissive: "#000000",
            shininess: 150
          });
        });
        obj.scale.set(1, 1, 1);
        obj.children[0].material.color.set(0xccd072);
        // obj.children[4].material.color.set(0xe1a07f);
        obj.children.forEach((item) => {
          item.castShadow = true;
          item.receiveShadow = true;
        });
        obj.castShadow = true;
        obj.receiveShadow = true;
        that.jeepCar = obj;
        // 添加到场景
        that.scene.add(that.jeepCar);
        that.initExplodeModel(that.jeepCar);
      });

      // 创建一个几何平面,作为地板,400,400
      const planeGeometry = new THREE.PlaneGeometry(400, 400);
      // 创建带颜色材质,更换为MeshLambertMaterial材质,去掉网格结构
      const planeMaterial = new THREE.MeshLambertMaterial({
        color: "rgb(110,110,110)",
      });
      this.plane = new THREE.Mesh(planeGeometry, planeMaterial);

      // 平面开启接收阴影效果
      this.plane.receiveShadow = true;

      // 设置平面角度和位置
      this.plane.rotation.x = -0.5 * Math.PI;
      this.plane.position.x = 0;
      this.plane.position.y = 0;
      this.plane.position.z = 0;
      // 添加平面
      //   this.scene.add(this.plane);

      //   this.gltfLoader = new GLTFLoader();
      //   this.gltfLoader.load("static/gltf/scene.gltf", function (gltf) {
      //     console.log(gltf);
      //     gltf.scene.scale.set(80, 80, 80);
      //     that.jeepCar = gltf.scene;
      //     // 添加到场景
      //     that.scene.add(gltf.scene);
      //     that.initExplodeModel(gltf.scene);
      //   });
    },
    onChange(val) {
      console.log(val);
      this.explodeModel(this.jeepCar, val);
    },
    initLight() {
      // 为场景所有物体添加基础光源
      const ambientLight = new THREE.AmbientLight(0x72757a, 2);
      this.scene.add(ambientLight);

      // 添加聚光灯光源
      const spotLight = new THREE.SpotLight(0xffffff, 2, 1000);
      spotLight.shadow.mapSize.set(2048, 2048);
      spotLight.position.set(-300, 0, -200);
      // 开启投影
      // spotLight.castShadow = true;
      this.scene.add(spotLight);
      const anotherSpotLight = new THREE.SpotLight(0xffffff, 2, 1000);
      anotherSpotLight.shadow.mapSize.set(2048, 2048);
      anotherSpotLight.position.set(300, 0, 300);
      this.scene.add(anotherSpotLight);

      const spotLightHelper = new THREE.SpotLightHelper(spotLight);
      this.scene.add(spotLightHelper);

      //   const light = new THREE.DirectionalLight(0xe8b73b, 2, 1000); // 光源颜色
      //   light.shadow.mapSize.set(2048, 2048);
      //   light.position.set(100, 100, 0);
      //   this.scene.add(light);

      //   const directionalLightHelper = new THREE.SpotLightHelper(light);
      //   this.scene.add(directionalLightHelper);
    },
    initGui() {
      // 为带贴图MTL的OBJ模型添加UI控制(xy坐标,xyz角度)
      this.controls = {
        positionX: -18,
        positionZ: -18,
        rotationX: 0,
        rotationY: 45,
        rotationZ: -15,
      };

      const gui = new Dat.GUI();
      // 设置允许操作范围
      gui.add(this.controls, "positionX", -200, 200);
      gui.add(this.controls, "positionZ", -200, 200);
      gui.add(this.controls, "rotationX", -360, 360);
      gui.add(this.controls, "rotationY", -360, 360);
      gui.add(this.controls, "rotationZ", -360, 360);
    },
    initControls() {
      this.trackballControls = new TrackballControls(
        this.camera,
        this.renderer.domElement
      );
      this.trackballControls.rotateSpeed = 1.0;
      this.trackballControls.zoomSpeed = 1;
      this.trackballControls.panSpeed = 1;
      this.trackballControls.noZoom = false;
      this.trackballControls.noPan = false;
      this.trackballControls.staticMoving = true;
      this.trackballControls.dynamicDampingFactor = 0.3;
      this.trackballControls.keys = [65, 83, 68];
    },
    initClock() {
      this.clock = new THREE.Clock();
    },
    render() {
      this.trackballControls.update(this.clock.getDelta());
      this.stats.update();

      // UI事件
      if (this.jeepCar) {
        this.jeepCar.position.x = this.controls.positionX;
        this.jeepCar.position.z = this.controls.positionZ;

        // 这里设置的角度,需要转换为弧度
        this.jeepCar.rotation.x = this.angle2Radian(this.controls.rotationX);
        this.jeepCar.rotation.y = this.angle2Radian(this.controls.rotationY);
        this.jeepCar.rotation.z = this.angle2Radian(this.controls.rotationZ);
      }

      // render using requestAnimationFrame
      requestAnimationFrame(this.render);
      this.renderer.render(this.scene, this.camera);
    },
    onWindowResize() {
      this.camera.aspect = window.innerWidth / window.innerHeight;
      this.camera.updateProjectionMatrix();
      this.renderer.setSize(window.innerWidth, window.innerHeight);
    },
    // 角度转弧度(弧度 = π / 180 * 角度)
    angle2Radian(angle) {
      return (Math.PI / 180) * angle;
    },
    execute() {
      // 初始化场景
      this.initScene();
      // 初始化摄像头
      this.initCamera();
      // 初始化三维坐标系
      this.initHelper();
      // 初始化辅助UI
      this.initGui();
      // 初始化帧数显示工具
      this.initStats();
      // 初始化时钟工具
      this.initClock();
      // 初始化模型
      this.initModel();
      // 初始化渲染器
      this.initRender();
      // 初始化光源
      this.initLight();
      // 初始化控制器
      this.initControls();
      // 执行渲染
      this.render();
    },
  },
};
</script>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,183评论 6 516
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,850评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,766评论 0 361
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,854评论 1 299
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,871评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,457评论 1 311
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,999评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,914评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,465评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,543评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,675评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,354评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,029评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,514评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,616评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,091评论 3 378
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,685评论 2 360

推荐阅读更多精彩内容