前言
本篇文章主要介绍如何用three.js实现模型渲染、以及通过手动点击动态添加新模型及标签文本,点击已经添加的模型时作提示
效果如图:
开发环境
vue3
typescript
three.js(我使用的是.obj的3D模型文件渲染)
具体实现
模板代码
<template>
<div class="pageBox">
<div
class="threeC"
ref="threeJsContainer"
style="width: 100%; height: 73vh"
@click="bindEventListeners"
></div>
<div class="btnsBox">
<button class="saveBtn" @click="saveModels">保存</button>
<button class="clearBtn" @click="clearModels">清空</button>
</div>
</div>
</template>
threeJsContainer:准备主模型容器
script代码-导入
import { onMounted, onBeforeUnmount, watchEffect,nextTick } from "vue";
import * as THREE from "three";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import {
CSS2DRenderer,
CSS2DObject,
} from "three/examples/jsm/renderers/CSS2DRenderer";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { toRaw } from "@vue/reactivity";
script代码-渲染
const threeJsContainer = ref(null);
let scene = new THREE.Scene();
const camera = ref(null);
const renderer = ref(null);
const labelRenderer = ref(null);
const controls = ref(null);
const loader = new OBJLoader();
const addedModels = ref([]);
const modelIdCounter = ref(0);
const modelData = ref([]);
const mainModel = ref([]);
// 初始化模型
const initThreeJs = () => {
const container = threeJsContainer.value;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
camera.value = new THREE.PerspectiveCamera(
75,
container.offsetWidth / container.offsetHeight,
0.1,
1000
);
camera.value.position.set(0, 1, 5);
renderer.value = new THREE.WebGLRenderer();
renderer.value.setSize(container.offsetWidth, container.offsetHeight);
container.appendChild(renderer.value.domElement);
const ambientLight = new THREE.AmbientLight(0x404040, 1.8);
scene.add(toRaw(ambientLight));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(0, 1, 0).normalize();
scene.add(toRaw(directionalLight));
labelRenderer.value = new CSS2DRenderer();
labelRenderer.value.setSize(container.offsetWidth, container.offsetHeight);
labelRenderer.value.domElement.style.position = "absolute";
labelRenderer.value.domElement.style.top = "0px";
container.appendChild(labelRenderer.value.domElement);
controls.value = new OrbitControls(camera.value, container);
controls.value.enableDamping = true;
controls.value.dampingFactor = 0.25;
controls.value.enableZoom = true;
animate();
};
const animate = () => {
requestAnimationFrame(animate);
renderer.value.render(scene, camera.value);
labelRenderer.value.render(scene, camera.value);
};
const onDocumentMouseDown = (event: any) => {
event.preventDefault();
const mouse = new THREE.Vector2(
((event.clientX - threeJsContainer.value.getBoundingClientRect().left) /
threeJsContainer.value.offsetWidth) *
2 -
1,
-(
(event.clientY - threeJsContainer.value.getBoundingClientRect().top) /
threeJsContainer.value.offsetHeight
) *
2 +
1
);
let standardVector = new THREE.Vector3(mouse.x, mouse.y, 1); // 标准设备坐标
let worldVector = standardVector.unproject(camera.value); // 标准设备坐标转世界坐标
let ray = worldVector.sub(camera.value.position).normalize(); // 射线投射方向单位向量(worldVector坐标减相机位置坐标)
let raycaster = new THREE.Raycaster(camera.value.position, ray);
// const raycaster = new THREE.Raycaster();
// raycaster.setFromCamera(mouse, camera.value);
const intersectsAdded = raycaster.intersectObjects(addedModels.value, true);
console.log(addedModels.value, "addedModels.value");
if (intersectsAdded.length > 0) {
const intersection = intersectsAdded[0];
const modelIndex = intersection.object.parent.userData.modelId;
alert(`点击了第${modelIndex + 1}个模型`);
console.log(`点击了第${modelIndex + 1}个模型`);
return; // 如果点击了已添加的模型,则不继续执行
}
const intersectsMain = raycaster.intersectObjects([mainModel.value], true);
if (intersectsMain.length > 0) {
const intersection = intersectsMain[0];
const point = intersection.point;
addNewModel(point);
}
};
// 渲染主模型文件
const loadMainModel = () => {
const objPath = "http://你的主模型文件地址.obj";
loader.load(
objPath,
(object: any) => {
mainModel.value = object;
mainModel.value.position.set(-1, -4, -3);
mainModel.value.scale.set(0.005, 0.005, 0.005); // 调整模型尺寸
scene.add(toRaw(mainModel.value));
},
undefined,
(error: any) => {
console.error("An error happened:", error);
}
);
};
// 添加模型的方法
const addNewModel = (position: any) => {
console.log("Adding new model at position", position);
const newModelPath = "http://你需要添加的模型文件地址.obj";
loader.load(
newModelPath,
(object: any) => {
object.position.copy(position);
object.scale.set(0.1, 0.1, 0.1);
// 修改模型颜色和光照响应
object.traverse((child: any) => {
if (child.isMesh) {
child.material.color.setHex(0xffc0cb); // 粉色
child.material.specular.setHex(0xffffff); // 镜面反射为白色,增加亮度
child.material.shininess = 10; // 增加光泽度
}
});
const modelId = modelIdCounter.value++;
object.userData.modelId = modelId;
addedModels.value.push(object);
scene.add(toRaw(object));
console.log("Model added to scene");
addLabel(position, `测点 ${modelId + 1}`);
},
undefined,
(error: any) => {
console.error("An error happened:", "An error happened:", error);
}
);
};
// 添加标签文本
const addLabel = (position: any, text: any) => {
const divElement = document.createElement("div");
divElement.className = "label-card";
divElement.textContent = text;
divElement.style.color = "#000";
divElement.style.marginTop = "-28px";
const label = new CSS2DObject(divElement);
label.position.copy(position);
label.position.y += 0.1;
labelRenderer.value.domElement.appendChild(divElement);
scene.add(toRaw(label));
modelData.value.push({
position: position.toArray(),
text: text,
});
};
// 保存模型(如果需要保存已添加的模型)
const saveModels = () => {
localStorage.setItem("modelsData", JSON.stringify(modelData.value)); // 可以替换成接口保存
router.go(0);
};
// 重新加载已保存的模型
const loadSavedModels = () => {
const savedData = JSON.parse(localStorage.getItem("modelsData")!);
if (savedData) {
savedData.forEach((data: any) => {
loader.load(
"http://你需要添加的模型文件地址.obj",
(object: any) => {
object.position.fromArray(data.position);
object.scale.set(0.1, 0.1, 0.1);
// 修改模型颜色和光照响应
object.traverse((child: any) => {
if (child.isMesh) {
child.material.color.setHex(0xffc0cb); // 粉色
child.material.specular.setHex(0xffffff); // 镜面反射为白色,增加亮度
child.material.shininess = 10; // 增加光泽度
}
});
const modelId = modelIdCounter.value++;
object.userData.modelId = modelId;
addedModels.value.push(object);
scene.add(toRaw(object));
addLabel(new THREE.Vector3().fromArray(data.position), data.text);
},
undefined,
(error: any) => {
console.error("An error happened:", error);
}
);
});
}
};
// 清空已添加的模型
const clearModels = () => {
addedModels.value.forEach((model: any) => {
scene.remove(model);
});
addedModels.value = [];
labelRenderer.value.domElement.innerHTML = "";
modelData.value = [];
localStorage.removeItem("modelsData");
router.go(0); // 刷新页面
};
onMounted(() => {
initThreeJs();
loadMainModel();
window.addEventListener("click", onDocumentMouseDown); // 添加事件
loadSavedModels();
});
onBeforeUnmount(() => {
window.removeEventListener("click", onDocumentMouseDown); // 移除事件
});
注意
- 如果使用的是其他格式的3D文件,代码会有不同