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. 参考资料
Three.js 官方文档:https://threejs.org/docs/index.html#manual/zh/introduction/Creating-a-scene
segmentfault(思否) “稀烂小青蛙”博主的demo分享:https://segmentfault.com/q/1010000015649560?bd_source_light=4746641
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
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




