VUE+THREE.JS 点击模型相机缓入查看模型相关信息
点击模型相机缓入查看模型相关信息
- 1.引入
- 2.初始化CSS3DRenderer
- 3.animate 加入一直执行渲染
- 4.点击事件
- 4.1 初始化renderer时加入监听事件
- 4.2 触发点击事件
- 5. 关键代码分析
- 5.1 移除模型
- 5.2 创建模型上方的弹框
- 5.3 相机缓入动画
- 5.4 动画执行
1.引入
引入模型所要呈现的3DSprite精灵模型,优势在于可以随着视野的变化,跟随方向变化,大小是近大远小的模式
import { CSS3DRenderer, CSS3DSprite } from "three/examples/jsm/renderers/CSS3DRenderer.js";
import TWEEN from "@tweenjs/tween.js";//相机缓入动画
2.初始化CSS3DRenderer
// 初始化 CSS3DRenderer 设备信息框
initObjectRender() {
labelRender = new CSS3DRenderer();
labelRender.setSize(this.$refs.draw.offsetWidth, this.$refs.draw.offsetHeight);
labelRender.domElement.style.position = "absolute";
labelRender.domElement.style.top = "0px";
labelRender.domElement.style.pointerEvents = "none";
document.getElementById("workshop").appendChild(labelRender.domElement);
},
3.animate 加入一直执行渲染
labelRender.render(scene, camera);
TWEEN.update();
4.点击事件
4.1 初始化renderer时加入监听事件
renderer.domElement.addEventListener("click", this.onClick, false);
4.2 触发点击事件
//监听点击事件
onClick(event) {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// 计算鼠标或触摸点的位置
mouse.x = (event.clientX / this.$refs.draw.offsetWidth) * 2 - 1;
mouse.y = -(event.clientY / this.$refs.draw.offsetHeight) * 2 + 1;
// 更新射线 注意——> camera 是相机 定义到data里的
raycaster.setFromCamera(mouse, camera);
// 计算与所有对象的交点
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
//获取点击模型的相关信息
//以下为我的处理逻辑
const interObj = intersects[0].object;
//获取模型名称,此名称是用blender创建模型时,创建的名称
const interName = this.getParentName(interObj);
//模型的位置
const interPoint = intersects[0].point;
if (interName) {
this.removeOthersEqp(interName); //移除此设备以外的设备
this.getEqpInfo(interName, interObj, interPoint); //获取设备信息
} else {
console.log("获取世界坐标", interPoint.x, ",", interPoint.y, ",", interPoint.z);
}
}
},
//获取点击的设备名称
getParentName(data) {
if (!data) {
return;
}
const regex = /[^\_\)]+(?=\()/g;
const eqpEnCode = data.name.match(regex);
return eqpEnCode?.length > 0 ? eqpEnCode[0] : this.getParentName(data.parent);
},
//移除此设备以外的设备
removeOthersEqp(interName) {
const meshes = scene.children.filter((o) => {
return o.name !== `${interName}EqpInfo` && o.name.indexOf("EqpInfo") > -1;
});
meshes.forEach((l) => {
l.remove(...l.children);
});
scene.remove(...meshes);
},
//获取设备信息
toolTipGroup: new THREE.Group(),//弹框参数
getEqpInfo(interName, interObj, interPoint) {
// 获取设备详细信息
let params = {
system: "",
enCode: interName,
};
getEqpInfoReq(params).then((res) => {
if (res.code === 200) {
const { encode, oeeStatus, taktTime, yield: resYield } = res.result;
const shpereMesh = this.createCpointMesh(`${interName}EqpInfo`, interObj.position.x, interObj.position.y + 1000, interObj.position.z);
this.toolTipGroup.add(shpereMesh);
//关闭弹框标签
const closeInfo = document.createElement("div");
closeInfo.setAttribute("style", "width:100%;padding: 0.5rem 0.5rem 0 0;text-align:right");
closeInfo.innerHTML = "<i class='iconfont icon-yuyinguanbi' style='font-size:0.5rem;color:#27eeea;cursor: pointer;'></i>";
//弹框点击关闭事件
closeInfo.onclick = function (event) {
const meshes = scene.children.filter((o) => {
return o.name === `${interName}EqpInfo`;
});
meshes.forEach((l) => {
l.remove(...l.children);
});
scene.remove(...meshes);
event.cancelBubble = true;
};
//基础信息展示
const cardBaseInfo = `
<div class='base-infos'>
<div class='base-info'>
<span class='name'>编码:</span>
<span>${encode}</span>
</div>
<div class='base-info'>
<span class='name'>名称:</span>
<span>${interObj.name.match(/[^\(\)]+(?=\))/g)[0]}</span>
</div>
<div class='base-info'>
<span class='name'>状态:</span>
<span>${oeeStatus}</span>
</div>
</div>`;
//设备其他信息
const cardOthersInfo = `
<div class='base-infos'>
<div class='base-info'>
<span class='name'>Yield:</span>
<span>${resYield}%</span>
</div>
<div class='base-info'>
<span class='name'>TaktTime:</span>
<span>${taktTime}</span>
</div>
</div>`;
const cardInfo = document.createElement("div");
cardInfo.style.padding = "0 0 1rem 0";
cardInfo.innerHTML = cardBaseInfo + cardOthersInfo;
const pContainer = document.createElement("div");
pContainer.id = `${interName}EqpInfo`;
pContainer.className = "workshop-tooltip";
pContainer.style.pointerEvents = "none"; //避免HTML标签遮挡三维场景的鼠标事件
pContainer.appendChild(closeInfo); //关闭按钮
pContainer.appendChild(cardInfo); //基础信息
const cPointLabel = new CSS3DSprite(pContainer);
// cPointLabel.scale.set(5, 5, 5); //根据相机渲染范围控制HTML 3D标签尺寸
cPointLabel.rotateY(Math.PI / 2); //控制HTML标签CSS3对象姿态角度
cPointLabel.position.set(interObj.position.x, interObj.position.y + 1000, interObj.position.z);
cPointLabel.name = `${interName}EqpInfo`;
scene.add(cPointLabel);
//相机位置移动
this.handlePosition(interPoint, true);
}
});
},
//创建基础模型
createCpointMesh(name, x, y, z) {
const geo = new THREE.BoxGeometry(0.1);
const mat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
mesh.name = name;
return mesh;
},
// 动态调整相机位置
handlePosition(targetPosition, falg) {
// 计算点击位置与当前相机位置之间的向量
const direction = new THREE.Vector3().subVectors(targetPosition, camera.position);
// 计算相机与目标位置之间的距离
let distance = camera.position.distanceTo(targetPosition);
// 以某种方式将距离转换为缩放因子
let scaleFactor = falg ? this.functionOfDistance(distance) : 0;
// 缩放向量,使其稍远一点
// const scaleFactor = 0.5; // 缩放因子,可以根据需要进行调整
const offset = direction.multiplyScalar(scaleFactor);
const finalPosition = camera.position.clone().add(offset);
// 创建 Tween 实例
const startPosition = camera.position.clone();
const duration = 1000; // 动画持续时间,单位毫秒
tween = new TWEEN.Tween(startPosition)
.to(finalPosition, duration)
.onUpdate(() => {
// 更新相机位置
camera.position.copy(startPosition);
camera.lookAt(targetPosition);
})
.start();
},
//计算距离
functionOfDistance(distance) {
// 设定最小和最大距离以及对应的缩放因子
const minDistance = 4100;
const maxDistance = 18000;
const minScaleFactor = 0;
const maxScaleFactor = 0.8;
if (distance < minDistance) {
return minScaleFactor;
} else if (distance > maxDistance) {
return maxScaleFactor;
}
// 根据距离范围内的比例,计算缩放因子
const ratio = (distance - minDistance) / (maxDistance - minDistance);
return minScaleFactor + ratio * (maxScaleFactor - minScaleFactor);
},
5. 关键代码分析
5.1 移除模型
- 1.获取想要移除的模型名称
const meshes = scene.children.filter((o) => {
return o.name !== `${interName}EqpInfo` && o.name.indexOf("EqpInfo") > -1;
});
- 2.移除模型的子模型
meshes.forEach((l) => {
l.remove(...l.children);
});
- 3.移除模型
scene.remove(...meshes)
5.2 创建模型上方的弹框
- 1.创建基础模型
const shpereMesh = this.createCpointMesh(`${interName}EqpInfo`, interObj.position.x, interObj.position.y + 1000, interObj.position.z);
//创建基础模型
createCpointMesh(name, x, y, z) {
const geo = new THREE.BoxGeometry(0.1);
const mat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
mesh.name = name;
return mesh;
},
- 2.创建动态div,渲染到基础模型中
由于我这里是一个弹框,我希望他能够点击关闭,所以多加了个关闭事件
- 2.1 关闭按钮的渲染及触发
//关闭弹框标签
const closeInfo = document.createElement("div");
closeInfo.setAttribute("style", "width:100%;padding: 0.5rem 0.5rem 0 0;text-align:right");
closeInfo.innerHTML = "<i class='iconfont icon-yuyinguanbi' style='font-size:0.5rem;color:#27eeea;cursor: pointer;'></i>";
//弹框点击关闭事件
closeInfo.onclick = function (event) {
const meshes = scene.children.filter((o) => {
return o.name === `${interName}EqpInfo`;
});
meshes.forEach((l) => {
l.remove(...l.children);
});
scene.remove(...meshes);
event.cancelBubble = true;
};
- 2.2 弹框信息显示
注意:pContainer.style.pointerEvents = "none"; //避免HTML标签遮挡三维场景的鼠标事件
必须要写这个 要不然会导致模型无法推拽移动
//基础信息展示
const cardBaseInfo = `
<div class='base-infos'>
<div class='base-info'>
<span class='name'>编码:</span>
<span>${encode}</span>
</div>
<div class='base-info'>
<span class='name'>名称:</span>
<span>${interObj.name.match(/[^\(\)]+(?=\))/g)[0]}</span>
</div>
<div class='base-info'>
<span class='name'>状态:</span>
<span>${oeeStatus}</span>
</div>
</div>`;
//设备其他信息
const cardOthersInfo = `
<div class='base-infos'>
<div class='base-info'>
<span class='name'>Yield:</span>
<span>${resYield}%</span>
</div>
<div class='base-info'>
<span class='name'>TaktTime:</span>
<span>${taktTime}</span>
</div>
</div>`;
const cardInfo = document.createElement("div");
cardInfo.style.padding = "0 0 1rem 0";
cardInfo.innerHTML = cardBaseInfo + cardOthersInfo;
const pContainer = document.createElement("div");
pContainer.id = `${interName}EqpInfo`;
pContainer.className = "workshop-tooltip";
pContainer.style.pointerEvents = "none"; //避免HTML标签遮挡三维场景的鼠标事件
pContainer.appendChild(closeInfo); //关闭按钮
pContainer.appendChild(cardInfo); //基础信息
const cPointLabel = new CSS3DSprite(pContainer);
// cPointLabel.scale.set(5, 5, 5); //根据相机渲染范围控制HTML 3D标签尺寸
cPointLabel.rotateY(Math.PI / 2); //控制HTML标签CSS3对象姿态角度
cPointLabel.position.set(interObj.position.x, interObj.position.y + 1000, interObj.position.z);
cPointLabel.name = `${interName}EqpInfo`;
scene.add(cPointLabel);
5.3 相机缓入动画
动态的缩放因子是
为了避免弹框占满整个屏幕
,使其稍远一点,默认是1
// 动态调整相机位置
handlePosition(targetPosition, falg) {
// 计算点击位置与当前相机位置之间的向量
const direction = new THREE.Vector3().subVectors(targetPosition, camera.position);
// 计算相机与目标位置之间的距离
let distance = camera.position.distanceTo(targetPosition);
// 以某种方式将距离转换为缩放因子
let scaleFactor = falg ? this.functionOfDistance(distance) : 0;
// 缩放向量,使其稍远一点
// const scaleFactor = 0.5; // 缩放因子,可以根据需要进行调整
const offset = direction.multiplyScalar(scaleFactor);
const finalPosition = camera.position.clone().add(offset);
},
- 动态缩放因子的获取
也不能将缩放因子固定,因为当相近模型点击时,弹框会越来越近,直至占满整个屏幕,
所以:设定最小的距离和最大的距离,当模型相对于相机距离远,就设定缩放因子为0.8,
模型相对相机距离近,就设定缩放因子为0,表示不缩放
//计算距离
functionOfDistance(distance) {
// 设定最小和最大距离以及对应的缩放因子(可视情况调整)
const minDistance = 4100;
const maxDistance = 18000;
const minScaleFactor = 0;
const maxScaleFactor = 0.8;
if (distance < minDistance) {
return minScaleFactor;
} else if (distance > maxDistance) {
return maxScaleFactor;
}
// 根据距离范围内的比例,计算缩放因子
const ratio = (distance - minDistance) / (maxDistance - minDistance);
return minScaleFactor + ratio * (maxScaleFactor - minScaleFactor);
},
5.4 动画执行
// 创建 Tween 实例
const startPosition = camera.position.clone();
const duration = 1000; // 动画持续时间,单位毫秒
tween = new TWEEN.Tween(startPosition)
.to(finalPosition, duration)
.onUpdate(() => {
// 更新相机位置
camera.position.copy(startPosition);
camera.lookAt(targetPosition);
})
.start();