vue + threejs 给3D模型添加label标签(dom的方式)

该博客介绍了一段使用WebGL.js封装的Three.js代码,用于创建3D场景、摄像机、渲染器、光源和物体。通过示例展示了如何创建平面、立方体、点光源和平行光,并在Vue组件中应用,实现3D渲染和标签的动态定位。同时,还涉及到了Raycaster交互和Dat.GUI控制台的创建。

 webGL.js封装的代码。

const THREE = window.THREE;
// webGL对象配置
export const webglOBJ = {
  renderDom: null,
  Scene: null, // 场景 
  camera: null, // 摄像头
  renderer: null, // 渲染器
  senceAdd (objList = []) {
    objList.forEach(v => {
      webglOBJ.Scene.add(v);
    });
  },
  // 创建场景
  createSence (renderDom) {
    this.renderDom = renderDom;
    webglOBJ.Scene = new THREE.Scene();
    return webglOBJ.Scene;
  },
  // 创建摄像机
  createCamera ({innerWidth, innerHeight, position} = {}) {
    const { width, height } = this.renderDom.getBoundingClientRect();
    let camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
    camera.position.x = -50;
    camera.position.y = 30;
    camera.position.z = 50;
    camera.lookAt(webglOBJ.Scene.position); // 视角
    webglOBJ.camera = camera; // 视角
    return webglOBJ.camera;
  },
  createRenderer () {
    let renderer = new THREE.WebGLRenderer();
    const {width, height} = this.renderDom.getBoundingClientRect();
    renderer.setSize(width, height);
    renderer.setClearColor(new THREE.Color(0xcccccc));
    renderer.shadowMap.enabled = true;
    this.renderDom.appendChild(renderer.domElement);
    webglOBJ.renderer = renderer;
    return webglOBJ.renderer;
  },
  createPlane (textureLoaderUrl, textureNormalUrl) {
    let planeGeometry = new THREE.PlaneGeometry(60, 60, 1, 1); // 平面网格
    let textureLoader = new THREE.TextureLoader();
    let texture = textureLoader.load(textureLoaderUrl);
    let textureNormal = textureLoader.load(textureNormalUrl);
    // 加载高光贴图
    let planeMaterial = new THREE.MeshPhongMaterial({
      // specular: 0xff0000,//高光部分的颜色
      shininess: 30, //高光部分的亮度,默认30
      map: texture, // 普通纹理贴图
      roughness: 0.3,
      lightMap: textureNormal,
      // normalMap: textureNormal, //法线贴图
      bumpScale: 3
    }); // 材质对象Material
    // let planeMaterial = new THREE.MeshLambertMaterial({color: 0xcccccc});
    let plane = new THREE.Mesh(planeGeometry, planeMaterial);
    plane.rotation.x = -0.5 * Math.PI;
    plane.position.x = 0;
    plane.name = '平面物体ID=' + 1;
    plane.position.y = 0;
    plane.position.z = 0;
    plane.receiveShadow = true;
    return plane;
  },
  createBoxGeometry (textureLoaderUrl, {x, y, z}) {
    // 创建立方体
    let textureLoader = new THREE.TextureLoader();
    let textureNormal = textureLoader.load(textureLoaderUrl);
    let boxGeometry = new THREE.BoxGeometry(10, 10, 10, 200);
    let texture1 = textureLoader.load(textureLoaderUrl);
    let boxGeometryMaterial = new THREE.MeshLambertMaterial({
      // specular: 0xff0000,//高光部分的颜色
      shininess: 10, //高光部分的亮度,默认30
      normalScale: new THREE.Vector2(2.2, 2.2),
      map: texture1, // 普通纹理贴图
      normalMap: textureNormal, //法线贴图
      bumpMap: textureNormal,
      bumpScale: 0.3
    });
    let box = new THREE.Mesh(boxGeometry, boxGeometryMaterial);
    box.name = '正方物体ID=' + 2;
    box.position.x = x;
    box.position.y = y;
    box.position.z = z;
    box.castShadow = true;
    return box;
  },
  // 点光源
  createSpotLight () {
    // 点光源
    let spotLight = new THREE.SpotLight(0xffffff);
    spotLight.position.set(-60, 40, -20);
    spotLight.castShadow = true;
    return spotLight;
  },
  // 平行光
  createDirectionalLight (target) {
    // 平行光
    let directionalLight = new THREE.DirectionalLight(0xffffff, 1);
    // 设置光源的方向:通过光源position属性和目标指向对象的position属性计算
    directionalLight.position.set(-90, 80, -20);
    // 方向光指向对象网格模型mesh2,可以不设置,默认的位置是0,0,0
    // directionalLight.target = target;
    return directionalLight;
  },
  // 环境光
  createAmbient (color = 0x444444) {
    let ambient = new THREE.AmbientLight(color);
    // ambient.castShadow = true;
    return ambient;
  },
  createDatGui () {
    let gui = {
      bump: 0.03,
      animation: false,
    };
    let datGui = new dat.GUI();
    //将设置属性添加到gui当中,gui.add(对象,属性,最小值,最大值)
    datGui.add(gui, "bump", -1, 1).onChange(function (e) {
      box.material.bumpScale = e;
    });
    datGui.add(gui, "animation");
    return datGui;
  },
  // 创建控制轴
  createControls () {
    let controls = new THREE.OrbitControls(webglOBJ.camera, webglOBJ.renderDom);
    return controls;
  },
  // 创建帮助
  createAxisHelper () {
    let axisHelper = new THREE.AxisHelper(250);
    return axisHelper;
  },
  // 初始化webGL对象
  webglRender (Scene, camera) {
    webglOBJ.renderer.render(Scene, camera);
    window.requestAnimationFrame(webglOBJ.webglRender);
  }
};

/**
 * 添加标签:dom方式
 * @param {*} targePosition :需要传递当前标签的位置
 * @param {*} targetId :标签对应的dom的唯一ID,暂且用时间戳代替,避免重复
 * @param {*} innerHTML :标签对应html
 */
export function labelTag (camera, targePosition, targetId, innerHTML, webGLdom) {
  const { width, height } = webGLdom.getBoundingClientRect();
  let worldVector = new THREE.Vector3(targePosition.x, targePosition.y, targePosition.z);
  let vector = worldVector.project(camera);
  let halfWidth = width / 2,
    halfHeight = height / 2;
  let x = Math.round(vector.x * halfWidth + halfWidth);
  let y = Math.round(-vector.y * halfHeight + halfHeight);
  /**
   * 更新立方体元素位置
   */
  let div = document.getElementById(targetId);
  div.style.left = x + 'px';
  div.style.top = y + 'px';
  // div.innerHTML = `uuid:${innerHTML.uuid}`;
}

vue中引用: 

<template>
<div>
  <div class="three-box_wrapper"></div>
  <div :id="`sign${idx + 1}`" style="position: absolute;" v-for="(v, idx) in labels" :key="idx">
    <div class="sign" :uuid="v.uuid">
      <div class="name">我是标签1</div>
      <div class="data">数据: {{v.uuid}}</div>
    </div>
  </div>
</div>
</template>

<script>
import {webglOBJ, labelTag} from '@/utils/webGL/webGL.js';
export default {
  name: 'threeBox',
  data () {
    return {
      labels: []
    };
  },
  mounted () {
    this.int();
  },
  methods: {
    createLabel () {

    },
    int () {
      const imgBG = require('./img.jpg');
      const webGLdom = document.querySelector('.three-box_wrapper');
      const sence = webglOBJ.createSence(webGLdom);
      const camera = webglOBJ.createCamera();
      const renderer = webglOBJ.createRenderer();
      const plane = webglOBJ.createPlane(imgBG, imgBG);
      const boxGeometry = webglOBJ.createBoxGeometry(imgBG, {x: 10, y: 5, z: 10});
      const boxGeometry1 = webglOBJ.createBoxGeometry(imgBG, {x: -10, y: 5, z: 10});
      const spotLight = webglOBJ.createSpotLight();
      const directionalLight = webglOBJ.createDirectionalLight(boxGeometry);
      const ambient = webglOBJ.createAmbient();
      const datGui = webglOBJ.createDatGui();
      const controls = webglOBJ.createControls();
      const axisHelper = webglOBJ.createAxisHelper();
      // 将对象添加到场景中去
      webglOBJ.senceAdd([plane, boxGeometry, boxGeometry1, spotLight, directionalLight, ambient, datGui, controls, axisHelper]);
      // webglOBJ.webglRender(sence, camera, renderer);
      console.log(sence, 'sence');
      const vm = this;
      function render(html) {
        vm.labels = sence.children.filter(v => v.type == 'Mesh');
        vm.$nextTick(() => {
          sence.children.forEach((val, idx) => {
            if (val.type == 'Mesh') {
              const {x, y, z} = val.position;
              labelTag(camera, {x,  y, z}, `sign${idx + 1}`, val, webGLdom);
            }
          });
        });

        renderer.render(sence, camera);
        requestAnimationFrame(render);
      };
      render();

      // 监听点击事件查看点击的元素
      let raycaster = new THREE.Raycaster();
      let mouse = new THREE.Vector2();
      // 点击了哪个模型
      function clickEvent() {
        if (event.target.tagName == 'CANVAS') {
          mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
          mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
          sence.updateMatrixWorld(true);
          // 通过鼠标点的位置和当前相机的矩阵计算出raycaster
          raycaster.setFromCamera(mouse, camera);
          // 获取raycaster直线和所有模型相交的数组集合
          let intersects = raycaster.intersectObjects(sence.children, true);
          if (intersects[0]) {
            console.log(intersects[0]);
          }
        }
      }
      window.addEventListener('click', clickEvent, false);
    }
  }
};
</script>

<style lang="scss" scoped>
  .three-box_wrapper {
    position: relative;
    width: 100%;
    height: 800px;
    border: 1px solid #ccc;
  }
  div[id *= "sign"] {
    width: 250px;
    height: 100px;
    background: rgba(0, 0, 0, .65);
    .sign{
      div {
        color: #fff;
        text-align: left;
        padding: 0 5px;
      }
    }
  }
</style>

<think> 嗯,用户想了解在Vue2项目中集成Three.js实现数据可视化的具体方法和案例。从对话历史看,用户之前关注过Vue3+ThreeJS的方案,现在特别询问Vue2的兼容性问题,可能是受限于现有项目版本。 用户提供的两段引用很有价值:引用[1]提到3D可视化能提升数据理解效率,引用[2]则详细描述了可视化编辑器的架构设计。虽然引用中案例是Vue3,但核心集成逻辑相通,需要重点解决版本适配问题。 考虑从三个层面回答:首先是基础集成方案,因为任何ThreeJS可视化都需要完成库的引入和场景初始化;其次组件封装是关键,引用[2]提到的"图元"概念暗示了组件化的重要性;最后性能优化部分,引用[1]强调"直观生动"的效果,必然涉及渲染效率。 注意到用户特别要求"案例",但引用材料未提供Vue2具体示例。准备用伪代码形式呈现核心逻辑,并建议查看github.com/mrdoob/three.js的官方示例库——虽然非Vue专属,但多数可直接移植。 用户可能是前端开发者,从提问方式看已经具备基础Vue知识,但对ThreeJS可能处于入门阶段。回复中需要强调两个技术栈的配合模式,比如避免在Vue的响应式系统中直接操作ThreeJS对象这种常见陷阱。 值得补充的是,引用[2]提到的DataV和阿里的案例虽然强大,但属于成品方案;用户询问"方法和案例",显然更倾向自主开发方案,因此重点放在技术实现而非产品推荐上。 </think> 在Vue2项目中集成Three.js实现数据可视化的核心步骤如下,结合典型场景案例说明: --- ### **一、基础集成方法** 1. **安装依赖** ```bash npm install three vue-awesome # vue-awesome用于图标管理(可选) ``` 2. **ThreeJS场景初始化** 在Vue组件中创建渲染循环: ```vue <template> <div ref="canvasContainer" class="scene"></div> </template> <script> import * as THREE from 'three'; export default { mounted() { this.initThree(); }, methods: { initThree() { // 1. 创建场景 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ antialias: true }); // 2. 挂载到DOM renderer.setSize(this.$refs.canvasContainer.clientWidth, 500); this.$refs.canvasContainer.appendChild(renderer.domElement); // 3. 添加3D对象(示例:立方体) const geometry = new THREE.BoxGeometry(); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); camera.position.z = 5; // 4. 动画循环 const animate = () => { requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); }; animate(); } } } </script> ``` --- ### **二、关键实现技巧** 1. **响应式适配** ```javascript window.addEventListener('resize', () => { camera.aspect = this.$refs.canvasContainer.clientWidth / 500; camera.updateProjectionMatrix(); renderer.setSize(this.$refs.canvasContainer.clientWidth, 500); }); ``` 2. **数据驱动更新** 通过Vue的响应式数据控制3D对象属性: ```javascript props: ['rotationSpeed'], watch: { rotationSpeed(newVal) { cube.rotationSpeed = newVal; // 在动画循环中应用此值 } } ``` 3. **性能优化** - 使用`Stats.js`监控帧率 - 离开页面时销毁渲染循环: ```javascript beforeDestroy() { cancelAnimationFrame(this.animationFrame); } ``` --- ### **三、典型应用案例** #### **案例1:3D柱状图(数据分布可视化)** ```javascript // 根据数据生成柱状图 function createBars(data) { data.forEach((item, i) => { const height = item.value * 0.1; const geometry = new THREE.CylinderGeometry(0.5, 0.5, height, 32); const material = new THREE.MeshPhongMaterial({ color: item.color }); const bar = new THREE.Mesh(geometry, material); bar.position.set(i * 2 - data.length, height/2, 0); scene.add(bar); }); } ``` > **效果**:动态高度的彩色柱体,可用于展示销售数据对比[^1]。 #### **案例2:3D地理热力图(区域数据覆盖)** ```javascript // 创建球体并映射纹理 const globeGeometry = new THREE.SphereGeometry(5, 64, 64); const textureLoader = new THREE.TextureLoader(); textureLoader.load('world-topo-map.jpg', map => { const material = new THREE.MeshPhongMaterial({ map }); const globe = new THREE.Mesh(globeGeometry, material); scene.add(globe); // 添加热点标记 addDataPoints(geoJSONData); }); ``` > **应用场景**:全球疫情数据分布、物流中心覆盖分析[^2]。 --- ### **四、进阶实践建议** 1. **性能瓶颈突破** - 使用`BufferGeometry`代替`Geometry`提升渲染性能 - 对静态模型应用`mergeVertices()`合并网格 2. **交互增强** ```javascript import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; // 添加相机控制器 const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; // 启用阻尼惯性 ``` 3. **与2D UI融合** 通过CSS2DRenderer在3D场景叠加DOM标签: ```javascript import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'; const labelRenderer = new CSS2DRenderer(); // 创建数据标签 const dataLabel = new CSS2DObject(domElement); bar.add(dataLabel); // 将标签绑定到柱体 ``` --- ### **五、学习资源推荐** 1. [ThreeJS官方示例库](https://github.com/mrdoob/three.js/tree/dev/examples)(可直接复用的组件) 2. [Vue+ThreeJS数据大屏开源项目](https://github.com/DataV-Team/DataV)(阿里DataV架构参考) 3. [ThreeJS性能优化指南](https://discoverthreejs.com/tips-and-tricks/)(权威性能调优方案) > **注意事项**:Vue2与Vue3集成逻辑类似,但需注意Vue2需手动管理ThreeJS对象生命周期,避免内存泄漏[^2]。 ---
评论 21
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值