暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

基于Three.js给3D模型添加文字标注

8496

1. 功能需求

需求:基于 Three.js 3D引擎,在原有的3D模型中添加文字标注,并用连接线指向3D模型对应部位的中心位置。


2. 核心步骤

开发过程的核心步骤如下:

(1) 把普通的文字通过 canvas 绘制成可以添加到场景中的 Texture
(2) 通过new THREE.SpriteMaterial() 将Texture生成精灵图材质,并用其定义精灵图实现标注永远正面朝向摄像机
(3) 使用new THREE.Line() 定义连接线,并按要求传入指定参数(起始位置端点、材质、连接线类型)
(4) 使用new THREE.Object3D() 创建三维物体,将连接线、精灵图添加到三维物体中,最后将三维物体添加到场景中
(5) 在初始化模型的时候使用new THREE.Box3().setFromObject(child).getCenter() 获取模型各个位置的中心点坐标,用于指定、计算起始位置的坐标


3. 功能实现

说明: 本业务使用了 vue2 进行工程化开发,需要提前搭建好开发环境;核心功能均是基于 Three.js 3D引擎的,需提前安装 Three.js、ThreeJS FBXLoader  依赖包;由于本文重点内容为给3D模型添加文字标注及连接线,所以场景,透视投影摄像机、渲染器、控制器、灯光、雾化效果的创建及配置代码已省略。

(1)  加载3D模型,获取各部位中心点坐标并进行存储

const that = this
let fbxLoader = new FBXLoader();
fbxLoader.load(
  'static/fbx/' + that.modelPath + '.FBX', 3D模型文件路径
  function(object) {
      object.children.forEach(function(child) {
          获取各个部位的中心点坐标
          let center = new THREE.Box3().setFromObject(child).getCenter();
          child.centerPosition = center;
      // 设置各个部位的材质
          child.material = new THREE.MeshLambertMaterial({
              color: 0x666666
          });
      });
      that.modelChildList = object.children;
      let temCount = 100;
      object.scale.set(temCount, temCount, temCount);
      object.position.set(0, 100, 0);
      that.scene.add(object);
      setTimeout(() => { 3D模型加载完成,取消loading效果
          that.fullLoading = false;
      }, 800);
  }
);

获取的中心点坐标数据格式如下图所示:

加载完3D模型后的效果如下图所示:

(2)  创建文字标注、生成Sprite平面、创建连接线等,将其加入场景并设置坐标位置等;最后将以上功能封装成函数方便重复调用。

// 添加文字标注 => 参数分别是 三维物体对象,文字标注的位置坐标对象,连接到3D模型中心位置的坐标,坐标轴放大倍率(因为模型文件提供的坐标是缩放后的),标注文字,控制连接线延伸方向的变量
addMarkers(group, startPosition, endPosition, multiple, text, num = 1) {
  const that = this;
// 定义几何体
  const geometry = new THREE.Geometry();
// 定义连接线的材质
  let material = new THREE.LineBasicMaterial({ vertexColors: true });
  声明连接线端点的颜色变量
  let color1 = new THREE.Color(0x88ddff);
  let color2 = new THREE.Color(0x9afc00);
  用 THREE.Vector3 来创建用来连接模型和标注的线的两个端点
  let p1 = new THREE.Vector3(startPosition.x * multiple - 50 * num, startPosition.y * multiple + 100 + 20, startPosition.z * multiple);
  let p2 = new THREE.Vector3(endPosition.x * multiple, endPosition.y * multiple + 100, endPosition.z * multiple);
// 把端点加入到几何体中
  geometry.vertices.push(p1);
  geometry.vertices.push(p2);
// 设置端点颜色
  geometry.colors.push(color1, color2);

  定义连接线实例对象
  let line = new THREE.Line(geometry, material, THREE.LineSegments);
// 将连接线加入到场景中
  that.scene.add(line);

  把传入的标注文字通过 canvas 修改为可以添加到场景中的 texture
  const offScreenCanvas = document.createElement('canvas');
  const offScreenCtx = offScreenCanvas.getContext('2d');
// 配置字体、大小、颜色等
  offScreenCtx.font = '16px 黑体';
  const txt = text;
  const textWidth = offScreenCtx.measureText(txt).width;
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  if (ctx !== null) {
      pixelRatio: 像素密度
      canvas.width = (5 + textWidth + 5) * 2;
      canvas.height = 18 * 2;
      ctx.setTransform(2, 0, 0, 2, 0, 0);
      ctx.font = '16px 黑体';
      ctx.fillStyle = '#fff'; 标注背景颜色
      ctx.fillRect(0, 0, 5 + textWidth + 5, 18);
      ctx.fillStyle = '#000'; 标注字体颜色
      ctx.fillText(txt, 5, 16);
  }
// 将标注变成可以添加到场景中的 texture 对象
  const texture = new THREE.CanvasTexture(canvas);
// 定义精灵图平面的材质;sizeAttenuation: false => 使Sprite在视野中保持大小不变
  let spriteMaterial = new THREE.SpriteMaterial({ map: texture, sizeAttenuation: false });
  定义精灵图来控制展示内容
  let sprite = new THREE.Sprite(spriteMaterial);
// 将精灵图平面的文字标注放置于连接线的起始位置处
  sprite.position.set(startPosition.x * multiple - 50 * num, startPosition.y * multiple + 100 + 20, startPosition.z * multiple);
  const scaleY = 0.03; 控制缩放大小
  const scaleX = (scaleY * canvas.width) canvas.height;
  sprite默认会令canvas变形 所以需要通过scale调整比例
  sprite.scale.set(scaleX, scaleY, 1); 设置精灵大小
  // 将三维物体、精灵图添加到场景中
  that.scene.add(sprite);
  group.add(line);
  group.add(sprite);
  that.scene.add(group);
},

(3)  在需要添加文字标注的时机处理好添加函数所需的参数,调用函数实现功能

// 定义放大倍数,用于调整坐标点
let multiple = 100;
// 定义三维物体对象
let group = new THREE.Object3D();
// 遍历模型对象,对3D模型的各个部位进行处理
that.modelChildList.forEach(function(child) {
  for (let item in temAry) {
      if (
          child['ID'] === temAry[item]['modelId'] && temAry[item]['value'] && that.isPlay
      ) { // 找到符合条件的模型部位,其他业务代码,不多做解释
          if (temAry[item]['color']) { // 设置模型各部位的颜色,其他业务代码,不多做解释
              child.material.color.setStyle(
                  temAry[item]['color'] || '#666666'
              );
              if (child.geometry) { // 如果遍历项是 geometry 对象,则调用方法在此位置添加文字标注,需要显示的标注文字请自行处理

                  let temObj = child.centerPosition;
                  if (temObj.x > 0) { // 如果该模型位置的中心点坐标x轴大于0,则传递第六个参数为 -1,使连接线延伸方向为x轴负方向
                      that.addMarkers(group, temObj, temObj, multiple, `${Number(temAry[item]['value']).toFixed(0)}`, -1);
                  } else {
                      that.addMarkers(group, temObj, temObj, multiple, `${Number(temAry[item]['value']).toFixed(0)}`);
                  }
              }
          }
      }
  }
});

最终实现效果如下图所示:

4. 参考资料

  1. Three.js 官方文档:https://threejs.org/docs/index.html#manual/zh/introduction/Creating-a-scene

  2. segmentfault(思否) “稀烂小青蛙”博主的demo分享:https://segmentfault.com/q/1010000015649560?bd_source_light=4746641

  3. CSDN “Crimaster·W” 博主的技术分享:https://blog.csdn.net/Kreme/article/details/121831161?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~aggregatepage~first_rank_ecpm_v1~rank_v31_ecpm-1-121831161.pc_agg_new_rank&utm_term=THREEJS%E8%8E%B7%E5%8F%96mesh%E7%9A%84%E4%B8%AD%E5%BF%83%E7%82%B9&spm=1000.2123.3001.4430

  4. CSDN “眼眸中的温柔” 博主的技术分享:https://blog.csdn.net/popstarqq/article/details/121358143?spm=1001.2101.3001.6650.11&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ELandingCtr%7ERate-11.queryctrv4&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ELandingCtr%7ERate-11.queryctrv4&utm_relevant_index=16

文章转载自布尔科技技术团队,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论