Vue3 + Three.js 联手,如何实现酷炫的 3D 模型交互预览

Vue3 + Three.js 联手,如何实现酷炫的 3D 模型交互预览

简介:因为最近工作需求涉及到threejs的开发,并且自己也一直想要学习和攻克这方面的知识,就通过自学之后记录下了这篇文章。这篇文章主要包含threejs的几个方面模型预览、环境贴图、模型的缩放、平移、打光、清晰度调整等方面。非常详细,有兴趣的同学可以收藏慢慢看!!!

一、threejs安装

通过npm或者yarn安装threejs库

npm install three or yarn add three

导入整个 three.js核心库

import * as THREE from 'three';

二、场景创建

html

<template>
    <div ref="container"></div>
</template>

script

const container = ref(null)

// 创建场景
const scene = new THREE.Scene();

// 创建相机
camera = new THREE.OrthographicCamera( 
    window.innerWidth / -10.5,
    window.innerWidth / 10.5,
    window.innerHeight / 10.5,
    window.innerHeight / - 10.5,
    0.1,
    1000
);// 这边设置的是OrthographicCamera解决模型近大远小的问题

// 相机位置
camera.position.z = 200; // 根据模型大小调整
camera.position.y = 0;
camera.position.x = 0;
camera.lookAt(0, 0, 0)

// 创建渲染器
renderer = new THREE.WebGLRenderer({ alpha: true }); // 设置背景为透明
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0);
container.value.appendChild(renderer.domElement);

// 如果需要添加环境贴图(添加背景环境)可以加上以下代码
// import { RGBELoader} from "three/examples/jsm/loaders/RGBELoader"
// let rgbeLoader = new RGBELoader();
// const rgbUrl = "xxxxx"; // 环境贴图hdr文件路径
// rgbeLoader.load(rgbUrl, envMap => {
//     // 设置球形贴图
//     envMap.mapping = THREE.EquirectangularReflectionMapping;
//     // 设置环境贴图
//     scene.background = envMap;
//     scene.environment = envMap;
// })

// 渲染场景
const animate = () => {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
};

animate();

// 监听窗口缩放
window.addEventListener("resize", () => {
    renderer.setSize( window.innerWidth, window.innerHeight );
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
});

三、模型引入

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'

const modelContainer = new THREE.Object3D();
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./draco/')

// 加载 glb 格式的 3D 模型
loader.setDRACOLoader(dracoLoader)
const modelPath = url // url是你模型的路径
loader.load(
    modelPath, 
    (gltf) => {
      // 加载成功后的回调函数
      model = gltf.scene;
      const box = new THREE.Box3().setFromObject(model);
      // 获取包围盒中心点
      const center = box.getCenter(new THREE.Vector3());
      model.position.sub(center); // 将模型位置移到原点处
      // 将模型添加到父对象中
      modelContainer.add(model);
      scene.add(modelContainer);
      
    },
    (xhr) => {
      // 加载过程中的回调函数
      // emit("handleProgress",Math.floor((xhr.loaded / xhr.total) * 100))
      // 如果有写模型加载进度条的要求,可以在这里判断
    },
    (error) => {
      // 加载失败的回调函数
      console.error("Failed to load model", error);
    }
  );
  

四、交互事件

变量

const STATE = {
    NONE: -1,
    MOVE: 0,
    ZOOM_OR_PAN: 1,
    POSITION_MOVE: 1,
}
const mouseInfo = ref({startX: 0, startY: 0, isDown: false, startPointerDistance: 0, state: STATE.NONE}) // 触控参数存储

const rotateSpeed = 0.010; // 拖动速度 可自行调整
const twoFirst = ref(true)
const lastPoint = ref({
    touchStartX1: 0,
    touchStartY1: 0,
    touchStartX2: 0,
    touchStartY2: 0,
})

旋转、缩放、平移方法

// 开始触碰
renderer.domElement.addEventListener('touchstart', (event) => {
  handleTouchStart(event)
});

const handleTouchStart = (event) => {
  mouseInfo.value.isDown = true
  // 双指
  const touch0 = event.touches[0]
  const touch1 = event.touches[1]
  // 单点触控
  if (event.touches.length === 1) {
      mouseInfo.value.startX = touch0.pageX
      mouseInfo.value.startY = touch0.pageY
      mouseInfo.value.state = STATE.MOVE 
  } else if (event.touches.length === 2) {
      // 双指,计算两指距离,一般是平移或者缩放
      const dx = (touch0.pageX - touch1.pageX)
      const dy = (touch0.pageY - touch1.pageY)
    
      mouseInfo.value.startPointerDistance = Math.sqrt(dx * dx + dy * dy)
      mouseInfo.value.startX = (touch0.pageX + touch1.pageX) / 2
      mouseInfo.value.startY = (touch0.pageY + touch1.pageY) / 2
      mouseInfo.value.state = STATE.ZOOM_OR_PAN
  }
}

// 监听缩放\旋转操作
renderer.domElement.addEventListener('touchmove', (event) => {
  handleTouchMove(event)
});

const handleTouchMove = (event) => {
  if (!mouseInfo.value.isDown) {
      return
  }
  switch (mouseInfo.value.state) {
      case STATE.MOVE:
          if (event.touches.length === 1) {
              handleRotate(event)
          } else if (event.touches.length === 2) {
              // 兼容处理,如果是移动过程中出现双指时,需要取消单指,然后再重新计算
              renderer.domElement.removeEventListener("touchmove",handleTouchMove, false)
              renderer.domElement.removeEventListener("touchend",handleTouchEnd, false)
              handleTouchStart(event)
          }
          break
      case STATE.ZOOM_OR_PAN:
          if (event.touches.length === 1) {
          } else if (event.touches.length === 2) {
              handleZoomOrPan(event)
          }
          break
      default:
          break
  }
}

// 模型旋转
const handleRotate = (event) => {
  const x = event.touches[0].pageX
  const y = event.touches[0].pageY
  
  const {startX, startY} = mouseInfo.value
  const deltaX = x - startX;
  const deltaY = y - startY;

  modelContainer.rotation.y += deltaX * rotateSpeed;
  modelContainer.rotation.x += deltaY * rotateSpeed;

  mouseInfo.value.startX = x
  mouseInfo.value.startY = y
}

// 模型缩放
const handleZoomOrPan = (event) => {
  let {touchStartX1,touchStartY1, touchStartX2, touchStartY2} = lastPoint.value;
  let initialScale = modelContainer.scale.x;
  const touch0 = event.touches[0]
  const touch1 = event.touches[1]
  const dx = (touch0.pageX - touch1.pageX)
  const dy = (touch0.pageY - touch1.pageY)
  const distance = Math.sqrt(dx * dx + dy * dy)
  let obj = {
    touchStartX1: touch0.pageX,
    touchStartY1: touch0.pageY,
    touchStartX2: touch1.pageX,
    touchStartY2: touch1.pageY,
  }
  if (twoFirst.value) {
      twoFirst.value = false;
      lastPoint.value = {...obj}
  } else {
      let deltaScale = initialScale * (distance / mouseInfo.value.startPointerDistance);
      // 限制缩放距离
      if (deltaScale < -2) {
          deltaScale = -2
      } else if (deltaScale > 2) {
          deltaScale = 2
      }
      mouseInfo.value.startPointerDistance = distance;
      modelContainer.scale.set(deltaScale, deltaScale, deltaScale);

      const avgX = (touch0.pageX + touch1.pageX) / 2;
      const avgY = (touch0.pageY + touch1.pageY) / 2;
      const deltaX = avgX - (touchStartX1 + touchStartX2) / 2;
      const deltaY = avgY - (touchStartY1 + touchStartY2) / 2;

      modelContainer.position.x += deltaX * 0.3;
      modelContainer.position.y -= deltaY * 0.3; 
      
      lastPoint.value = {
        touchStartX1: touch0.pageX,
        touchStartY1: touch0.pageY,
        touchStartX2: touch1.pageX,
        touchStartY2: touch1.pageY,
      }
  }
}

// 监听触控结束
renderer.domElement.addEventListener('touchend', (event) => {
  handleTouchEnd()
}); 

const handleTouchEnd = () => {
  mouseInfo.value.isDown = false
  mouseInfo.value.state = STATE.NONE
  twoFirst.value = true;
  lastPoint.value.touchStartX1 = 0;
  lastPoint.value.touchStartY1 = 0;
  lastPoint.value.touchStartX2 = 0;
  lastPoint.value.touchStartY2 = 0;
  // 取消移动事件监听
  renderer.domElement.removeEventListener("touchmove",handleTouchMove, false)
  renderer.domElement.removeEventListener("touchstart",handleTouchStart, false)
}

自动旋转、重置位置方法

// 开启自动旋转
const rotateTimer = ref(null)
const setTimer = () => {
    rotateTimer.value = setInterval(() => {
        modelContainer.rotation.y -= 0.1
    }, 100)
}

// 重置模型位置
const ResetPosition = () => {
  modelContainer.position.z = 0;
  modelContainer.position.y = 0;
  modelContainer.position.x = 0;
  modelContainer.scale.z = 1;
  modelContainer.scale.y = 1;
  modelContainer.scale.x = 1;
  modelContainer.rotation.z = 0;
  modelContainer.rotation.y = 0;
  modelContainer.rotation.x = 0;
}

五、优化调整

打光

// 环境光 (这是一定要的)
const ambientLight = new THREE.AmbientLight(0xffffff, 2);
scene.add(ambientLight);

// 白色平行光(模型更明亮)
const directionalLight = new THREE.DirectionalLight( 0xffffff, 2 ); // 参数自行调整
directionalLight.position.x = 1;
directionalLight.position.y = 1;
directionalLight.position.z = 80;
directionalLight.target = modelContainer; // target指向模型
scene.add( directionalLight );

// *创建点光源(这个看情况给)
var pointLight = new THREE.PointLight(0xffffff, 500); // 设置点光源的颜色和强度
pointLight.position.set(0, 0, 100); // 设置点光源的位置
scene.add(pointLight);

清晰度

// 在创建渲染器时,添加antiallias:true抗锯齿,让模型看起来更加平滑
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
// 设置画布分辨率 提高画质
renderer.setPixelRatio(window.devicePixelRatio);   

ios上safari网页缩放问题

在safari上打开缩放模型会和网页缩放冲突,添加以下方法可以解决。

// 在main.ts中添加
window.onload = function() {
  var lastTouchEnd = 0;
  document.addEventListener('touchstart', function(event) {
      if (event.touches.length > 1) {
          event.preventDefault();
      }
  });
  document.addEventListener('touchend', function(event) {
      var now = (new Date()).getTime();
      if (now - lastTouchEnd <= 300) {
          event.preventDefault();
      }
      lastTouchEnd = now;
  }, false);
  document.addEventListener('gesturestart', function(event) {
      event.preventDefault();
  });
  document.addEventListener('dblclick', function (event) {
      event.preventDefault();
})
}

六、参考

模型下载:sketchfab.com/ or market.pmnd.rs/

环境贴图hdr下载:hdrmaps.com/

threejs文档参考:www.yanhuangxueyuan.com/threejs/doc…

七、总结

如果有不懂的地方或者不够明确的地方可以评论区留言,期待和各位大佬的交流😊

第一次写文章不易,挥挥你们的小手点个赞,谢谢大家!

原文链接:https://juejin.cn/post/7352728445962633266 作者:LLLliam

(0)
上一篇 2024年4月2日 下午4:54
下一篇 2024年4月2日 下午5:04

相关推荐

发表评论

登录后才能评论