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