threejs实现动态添加模型及标签文本(vue3中)

前言

本篇文章主要介绍如何用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文件,代码会有不同
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值