three模型编辑功能实现

18 阅读5分钟

简介

最近项目中需要搭建三维环境,并且可以拖拽模型进行编辑,这里便把从环境搭建到功能实现的过程整理出来,供君参考

ScreenCapture20240529203133305.png

three环境搭建

引入three相关依赖

import { ref, onMounted, watch } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

初始化环境

初始化环境,包括场景,摄像机,渲染器,光源,控制器等

let myArray = ref([
  {
    url: carPng,
    title: "汽车",
  },
]);
let currentCamera;
let scene, renderer, control;
let groupAdd = "";
let meshGroup = "";
let canvas = "";
let qicheFbx = "";
let copyObj = "";
let tipText =
  "拖拽右侧图标进入坐标系,增加模型,双击选中模型,快捷键 'W' 移动 |'E' 旋转 | 'R' 缩放 |'Esc' 取消选中 或者点击左侧按钮操作模型";
let choseMeshValue = ref([]);
let operateType = ref("");
let controls = "";
let img = "";
// 加载场景
function initScene() {
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0xECF1FB)
}
// 添加摄像机
function initCamera() {
  const aspect = window.innerWidth / window.innerHeight;
  currentCamera = new THREE.PerspectiveCamera(50, aspect, 0.01, 30000);
  currentCamera.position.set(1000, 500, 1000);
  currentCamera.lookAt(0, 200, 0);
}
// 渲染器
function initRender() {
  renderer = new THREE.WebGLRenderer();
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight - 100);
  document.body.appendChild(renderer.domElement);
  renderer.sortObjects = true; // 渲染顺序
  canvas = renderer.domElement;
}
//光源
function initLight() {
  let aLight = new THREE.AmbientLight(0xffffff, 0.8) // 环境光
  let dLight = new THREE.DirectionalLight(0xffffff, 0.2) // 平行光源
  dLight.position.set(0, -21, 1000000)
  scene.add(dLight)
  scene.add(aLight)
}
// 控制器
function initControls() {
  controls = new OrbitControls(currentCamera, renderer.domElement);
  controls.update();
  controls.addEventListener("change", render);
}
// 辅助坐标
function axesHelper() {
  const axesHelper = new THREE.AxesHelper(300);
  scene.add(axesHelper);
}
async function init() {
  initScene(); //加载场景
  initCamera(); //添加摄像机
  initRender(); //渲染器
  initControls(); //控制器
  axesHelper(); // 坐标系
  initLight(); //光源
}

到此一个带有光源的three环境就搭建出来了

搭建模型编辑功能

引入模型及相关依赖

import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader"; //引入obj模型加载库OBJLoader.js
import draggable from "vuedraggable";
import { TransformControls } from "three/examples/jsm/controls/TransformControls"; //可视化平移控件
import carPng from "../assets/car.png";

构建模型拖拽到场景中

<div class="icon-class">
      <draggable @end="checkMove" v-model="myArray" item-key="title">
        <template #item="{ element }">
          <img :src="element.url" :title="element.title" />
        </template>
      </draggable>
    </div>

这里使用vue的vuedraggable组件实现拖拽功能,主要是拖拽示意图到场景中,再根据对应拖拽内容,渲染对应模型

//将鼠标点击位置的屏幕坐标转换成threejs中的标准坐标
function convertCoodsToThree(mouseX, mouseY, mouseZ = 0) {
  const x =
    ((mouseX - canvas.getBoundingClientRect().left) / canvas.offsetWidth) * 2 -
    1;
  const y =
    1 -
    ((mouseY - canvas.getBoundingClientRect().top) / canvas.offsetHeight) * 2;
  var vec = new THREE.Vector3();
  var pos = new THREE.Vector3();
  vec.set(x, y, 0.5);
  vec.unproject(currentCamera);
  vec.sub(currentCamera.position).normalize();
  var distance = (mouseZ - currentCamera.position.z) / vec.z;
  pos.copy(currentCamera.position).add(vec.multiplyScalar(distance));
  return [pos.x, pos.y, pos.z];
}
// 拖动图标事件
function checkMove(e) {
  let array = convertCoodsToThree(
    e.originalEvent.clientX,
    e.originalEvent.clientY
  );
  addCylinder(array, e.originalEvent.target.title);
}
/**
 * @author liujie22
 * @desc 添加模型
 */
function addCylinder(array, title) {
  var obj = "";
  if (title === "汽车") {
    obj = qicheFbx.clone();
    obj.scale.set(1, 1, 1);
    obj.position.set(array[0], array[1], 10);
  }
  if (obj.type === "Mesh") {
    meshGroup = new THREE.Group();
    meshGroup.add(obj);
    meshGroup.name = title;
    meshGroup.position.set(array[0], array[1], 0);
    obj.position.set(0, 0, 0);
    groupAdd.add(meshGroup);
  } else {
    obj.name = title;
    groupAdd.add(obj);
  }
  scene.add(groupAdd);
  render();
}

qicheFbx就是对应的模型

let qicheFbx = await createFbl("static/modle/car.fbx");
 // 创建STL加载器
function createFbl(fbxUrl) {
  return new Promise((resolve, reject) => {   
    let fbxLoader = new FBXLoader();
    fbxLoader.load(fbxUrl, (obj) => {
      obj.scale.set(1, 1, 1);
      obj.position.set(0, 0, 0);
      resolve(obj);
    });
  });
}

实现模型选中及操纵功能

// 模型操作初始化
function modleContral() {
  control = new TransformControls(currentCamera, renderer.domElement);
  control.addEventListener("change", render);

  window.addEventListener(
    "dblclick",
    (e) => {
      onMouseDblclick(e);
    },
    false
  );
  window.addEventListener(
    "click",
    (e) => {
      if (operateType.value === "C") {
        copyConfirm(e);
      }
    },
    false
  );
  window.addEventListener("resize", onWindowResize());
  window.addEventListener("keydown", (event) => {
    switch (event.keyCode) {
      case 87: // W
        operateType.value = "W";
        control.setMode("translate");
        break;
      case 69: // E
        operateType.value = "E";
        control.setMode("rotate");
        break;
      case 82: // R
        operateType.value = "R";
        control.setMode("scale");
        break;
      case 27: // Esc
        // control.reset();
        operateType.value = "";
        choseMeshValue.value = [];
        control.detach();
        controls.enabled = true;
        onWindowResize();
        break;
    }
  });
}

TransformControls 为three可视化平移组件,这里是鼠标双击选中,下面是鼠标双击,通过鼠标点的位置和当前相机的矩阵计算出raycaster,获取raycaster直线和所有模型相交的数组集合,然后将选中的模型放到控制器中,然后进行编辑控制

//鼠标双击触发的方法
function onMouseDblclick(event) {
  let rayCaster = new THREE.Raycaster();
  let mouse = new THREE.Vector2();
  //将鼠标点击位置的屏幕坐标转换成threejs中的标准坐标
  mouse.x =
    ((event.clientX - canvas.getBoundingClientRect().left) /
      canvas.offsetWidth) *
      2 -
    1;
  mouse.y =
    1 -
    ((event.clientY - canvas.getBoundingClientRect().top) /
      canvas.offsetHeight) *
      2;
  // 通过鼠标点的位置和当前相机的矩阵计算出raycaster
  rayCaster.setFromCamera(mouse, currentCamera);
  // 获取raycaster直线和所有模型相交的数组集合
  var intersects = rayCaster.intersectObjects(groupAdd.children, true);
  if (intersects.length > 0) {
    choseMeshValue.value = intersects;
    operateType.value = "W";
  } else {
    choseMeshValue.value = [];
    operateType.value = "";
  }
  if (choseMeshValue.value.length > 1) {
    controls.enabled = false;
    control.attach(choseMeshValue.value[0].object.parent);
    scene.add(control);
  } else if (choseMeshValue.value.length === 1) {
    if (choseMeshValue.value[0].object.parent.children.length > 0) {
      controls.enabled = false;
      control.attach(choseMeshValue.value[0].object.parent);
      scene.add(control);
    } else {
      controls.enabled = false;
      control.attach(choseMeshValue.value[0].object);
      scene.add(control);
    }
  } else {
    choseMeshValue.value = [];
    control.detach();
    controls.enabled = true;
  }
  onWindowResize();
}

这里添加键盘快捷建,拖拽右侧图标进入坐标系,增加模型,双击选中模型,快捷键 'W' 移动 |'E' 旋转 | 'R' 缩放 |'Esc' 取消选中 或者点击左侧按钮操作模型

  switch (event.keyCode) {
      case 87: // W
        operateType.value = "W";
        control.setMode("translate");
        break;
      case 69: // E
        operateType.value = "E";
        control.setMode("rotate");
        break;
      case 82: // R
        operateType.value = "R";
        control.setMode("scale");
        break;
      case 27: // Esc
        // control.reset();
        operateType.value = "";
        choseMeshValue.value = [];
        control.detach();
        controls.enabled = true;
        onWindowResize();
        break;
    }

为了方便操作,我们又引入按钮控制事件

<div class="button-class" :key="operateType">
  <el-radio-group v-model="operateType">
    <el-radio-button label="W" :disabled="choseMeshValue.length === 0"
      >移动</el-radio-button
    >
    <el-radio-button label="E" :disabled="choseMeshValue.length === 0"
      >旋转</el-radio-button
    >
    <el-radio-button label="R" :disabled="choseMeshValue.length === 0"
      >缩放</el-radio-button
    >
    <el-radio-button label="C" :disabled="choseMeshValue.length === 0"
      >复制</el-radio-button
    >
  </el-radio-group>
</div>
watch(operateType, (newValue) => {
  switch (newValue) {
    case "W": // W
      control.setMode("translate");
      break;

    case "E": // E
      control.setMode("rotate");
      break;

    case "R": // R
      control.setMode("scale");
      break;
    case "C":
      copyAction();
      break;
  }
});

选中模型后,就需要取选中,取消和删除操作的功能,方法如下

/**
 * @author liujie22
 * @desc 删除选中
 */
function deleteAction() {
  if (choseMeshValue.value.length > 1) {
    choseMeshValue.value[0].object.parent.parent.remove(
      choseMeshValue.value[0].object.parent
    );
  } else if (choseMeshValue.value.length === 1) {
    if (choseMeshValue.value[0].object.parent.children.length > 0) {
      choseMeshValue.value[0].object.parent.parent.remove(
        choseMeshValue.value[0].object.parent
      );
    } else {
      scene.remove(choseMeshValue.value[0].object);
    }
  }
  let objArray = groupAdd.children;
  for (let i = 0; i < objArray.length; i++) {
    if (objArray[i].type === "Group") {
      if (objArray[i].children.length === 0) {
        objArray.splice(i, 1);
      }
    }
  }
  operateType.value = "";
  choseMeshValue.value = [];
  control.detach();
  controls.enabled = true;
  onWindowResize();
}
/**
 * @author liujie22
 * @desc 取消选中
 */
function cancleAction() {
  operateType.value = "";
  choseMeshValue.value = [];
  control.detach();
  controls.enabled = true;
  onWindowResize();
}

这里原理主要控制controls的相关属性实现,control.detach()就是取消选中,controls.enabled = true;取消选中后,场景又可以操作了

为了方便操作,我们还贴心的加了复制功能,将你已经编辑好的模型,直接复制,这样就能保留之前编辑好的模型的一些属性,比如大小,旋转角度,缩放比等

/**
 * @author liujie22
 * @desc 复制功能
 */
function copyAction() {
  let target = "";
  if (choseMeshValue.value.length > 1) {
    target = choseMeshValue.value[0].object.parent;
  } else if (choseMeshValue.value.length === 1) {
    if (choseMeshValue.value[0].object.parent.children.length > 0) {
      target = choseMeshValue.value[0].object.parent;
    } else {
      target = choseMeshValue.value[0].object;
    }
  }
  let obj = target.clone();
  copyObj = obj;
}

到此,three环境下模型操作功能就都实现了,后期考虑引入自己画墙体,块体,再通过引入模型,自己搭建三维地图等功能。

引用地址: threejs.org/examples/#w…