- 创建好HTML标注的提示框
- 创建three的场景等系列
- three加载GLTF模型
- 点击按钮选中模型 + 交互
- 示例(大概意思就是我点击那个按钮显示那个提示框,模型变材质 + 线)
- 直接代码
<panel-group @handleSetLineChartData="handleSetLineChartData" />
<div v-if="tagFlage == 'newVisitis'">
<el-tag v-for="(item, index) of tagData" :key="index" :type="item.flag ? 'danger' : ''"
@click="tagClick(item, index)">{{ item.name }}</el-tag>
</div>
<el-row style="background:#fff;padding:16px 16px 0; margin-bottom:32px;height: calc(100vh - 340px);">
<canvas id="three"></canvas>
</el-row>
<div id="tag" style="display: none;"></div>
<div :id="item.message" v-for="(item, index) of tagData" :key="index" style="
visibility:hidden;
width:230px;
height:180px;
position: absolute;
color: #fff;
z-index: 2;
font-size: 16px;
background: rgba(86, 183, 195, 0.47);
box-shadow: rgba(86, 183, 195, 0.47) 0px 0px 15px 3px;"
:style="{ left: item.x1 + 'px', top: item.y1 + 'px' }">
<div style="position:relative;padding: 10px;">
<div style="display: flex;">
<span>标识:</span>
<div id="granaryName" style="font-size:16px">平房仓 P_01</div>
</div>
<div style="display: flex;align-items: flex-end;margin-top: 5px;">
<span>温度:</span>
<span id="temperature">19</span>℃
</div>
<div style="display: flex;align-items: flex-end;margin-top: 5px;">
<span>名字:</span>
<span id="weight">3600</span>
</div>
<div style="display: flex;align-items: flex-end;margin-top: 5px;">
<span>高度:</span>
<span id="granaryHeight">12</span>m
</div>
<div style="display: flex;align-items: flex-end;margin-top: 5px;">
<span>宽度:</span>
<span id="grainHeight">5</span> m
</div>
</div>
</div>
<style lang="scss" scoped>
.dashboard-editor-container {
padding: 32px;
background-color: rgb(240, 242, 245);
position: relative;
.chart-wrapper {
background: #fff;
padding: 16px 16px 0;
margin-bottom: 32px;
}
}
@media (max-width:1024px) {
.chart-wrapper {
padding: 8px;
}
}
#import-template {
width: 100%;
height: 100%;
}
#three {
width: 100%;
height: calc(591px - 32px);
}
.el-tag--medium {
margin-right: 10px;
}
</style>
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import {
CSS2DObject,
CSS2DRenderer
} from 'three/examples/jsm/renderers/CSS2DRenderer.js';
data() {
return {
tagFlage: null,
scene: null, // 场景
camera: null, // 照相机
renderer: null, // 渲染器
// mesh: null, // 网格
// textureLoader: null, // 纹理加载器
// mixer: null,
// groupBox: null,
// stats: null, // 性能监测
// control: null, // 相机控件
// scene2: null,
// render3D: null,
publicPath: process.env.BASE_URL,
// clearAnim: null,
// clock: null,
publicPath: process.env.BASE_URL,
granaryArr: [],
chooseMesh: null,
messageTags: [],
tagData: [
{
name: '场地',
message: 'HuaZhuang',
x: 10,
y: 10,
z: 15,
x1: -90,
y1: -60,
flag: false,
},
{
name: '建筑墙面',
message: 'JianZhu_ZhuTi',
x: 15,
y: 5,
z: 1,
x1: -50,
y1: 50,
flag: false,
},
{
name: '条幅',
message: 'TeXiao_010101',
x: 15,
y: 15,
z: -10,
x1: 80,
y1: -60,
flag: false,
},
],
idArr: ["granaryName", "temperature",
"weight", "granaryHeight", "grainHeight"
],
messageData: {
HuaZhuang: {
granaryName: "数据一(HuaZhu)",
temperature: 30,
weight: 'RC100',
granaryHeight: 6,
grainHeight: 25.1
},
JianZhu_ZhuTi: {
granaryName: "数据二(JianZhu)",
temperature: 30,
weight: 'SP500',
granaryHeight: 66,
grainHeight: 23.8
},
TeXiao_010101: {
granaryName: "数据三(TeXiao)",
temperature: 30,
weight: 'DataAPI',
granaryHeight: 666,
grainHeight: 9.2
}
},
}
},
mounted() {
this.initThree();
},
methods: {
initThree() {
let this_ = this
this_.scene = new THREE.Scene();
this_.scene.background = new THREE.Color(0xeeeeee);
this_.scene.fog = new THREE.Fog(0xa0a0a0, 200, 1000);
const canvas = document.querySelector("#three");
this_.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
this_.renderer.shadowMap.enabled = true;
this.camera = new THREE.PerspectiveCamera(
40,
window.innerWidth / window.innerHeight,
1,
1000
);
const controls = new OrbitControls(this.camera, this.renderer.domElement);
const gltfLoader = new GLTFLoader();
gltfLoader.load(`${this.publicPath}factory/XXX.gltf`, (gltf) => {
let model = gltf.scene;
model.scale.set(1, 1, 1);
controlsBox(model);
gltf.scene.traverse(function (object) {
if (object.type === 'Mesh') {
// 批量更改所有Mesh的材质
object.material = new THREE.MeshLambertMaterial({
map: object.material.map, //获取原来材质的颜色贴图属性值
color: object.material.color, //读取原来材质的颜色
})
}
})
gltf.scene.children.forEach((obj, index) => {
if (obj.type === 'Mesh') {
this_.granaryArr.push(obj);
}
})
this_.scene.add(model);
}, undefined, (error) => console.error(error));
// 将模型的中心点设置到canvas坐标系的中心点,保证模型显示是居中的
function controlsBox(model) {
let box = new THREE.Box3().setFromObject(model); // 获取模型的包围盒
let mdlen = box.max.x - box.min.x; // 模型长度
let mdwid = box.max.z - box.min.z; // 模型宽度
let mdhei = box.max.y - box.min.y; // 模型高度
let xPoiition = box.min.x + mdlen / 2; // 模型中心点坐标X
let yPoiition = box.min.y + mdhei / 2; // 模型中心点坐标Y
let zPoiition = box.min.z + mdwid / 2; // 模型中心点坐标Z
// 获取模型整体对角线长度,这里获取模型模型对角线的目的是为了保证模型可以完全的展示在视线范围内
let diagonal = Math.sqrt(Math.pow(Math.sqrt(Math.pow(mdlen, 2) + Math.pow(mdwid, 2)), 2) + Math.pow(mdhei, 2));
// 假设我们需要的进入视角为45度
// 设置相机位置,向上偏移,确定可以包裹整个模型
controls.object.position.set(box.max.x + diagonal / 2, (diagonal * 2) / Math.tan(Math.PI / 180 * 45) + Math.abs(box.max.y), box.max.z + diagonal / 2);
// 设置相机的视角方向,看向模型的中心点
controls.target.set(xPoiition, yPoiition, zPoiition);
controls.update(); // 更新相机
}
const hemLight = new THREE.HemisphereLight(0xffffff, 0xffffff);
hemLight.position.set(0, 20, 0);
this_.scene.add(hemLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
//光源等位置
dirLight.position.set(0, 20, 10);
//可以产生阴影
dirLight.castShadow = true;
dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
this_.scene.add(dirLight);
// 添加地板
let floorGeometry = new THREE.PlaneGeometry(8000, 3000);
let floorMaterial = new THREE.MeshPhongMaterial({
color: 0x5cb3cc,
shininess: 0,
});
let floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -0.5 * Math.PI;
floor.receiveShadow = true;
floor.position.y = -0.001;
// this_.scene.add(floor);
controls.enableDamping = true;
this.tagData.forEach((item) => {
this.labelRenderer = new CSS2DRenderer();
let three = document.querySelector('#three')
this.labelRenderer.setSize(three.getBoundingClientRect().width,three.getBoundingClientRect().height);
this.labelRenderer.domElement.style.position = 'absolute';
this.labelRenderer.domElement.style.top = '320px';
this.labelRenderer.domElement.style.left = '250px';
this.labelRenderer.domElement.style.pointerEvents = 'none';
document.body.appendChild(this.labelRenderer.domElement);
var messageTag = this.tag(item.message); //创建粮仓标注的标签
this.scene.add(messageTag);
this.messageTags.push(messageTag)
})
function animate() {
controls.update();
this_.renderer.render(this_.scene, this_.camera);
this_.labelRenderer.render(this_.scene, this_.camera); //渲染HTML标签对象
requestAnimationFrame(animate);
if (resizeRendererToDisplaySize(this_.renderer)) {
const canvas = this_.renderer.domElement;
this_.camera.aspect = canvas.clientWidth / canvas.clientHeight;
this_.camera.updateProjectionMatrix();
}
}
animate();
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
var width = window.innerWidth;
var height = window.innerHeight;
var canvasPixelWidth = canvas.width / window.devicePixelRatio;
var canvasPixelHeight = canvas.height / window.devicePixelRatio;
const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
},
}
tagClick(item, index) {
let this_ = this
var messageTag = this.tag(item.message);
let visibilitys = messageTag.element.style.visibility
let chooseMeshData = null
chooseMeshData = this_.choose(event, messageTag, 2, item.message); //执行射线拾取的代码
// 选中不同粮仓,HTML标签信息跟着改变
if (chooseMeshData) {
//批量更新粮仓chooseMesh的标签信息
this_.idArr.forEach(function (id) {
var dom = document.querySelector(`#${item.message} #${id}`)
dom.innerHTML = this_.messageData[chooseMeshData.name][id];
})
this.messageTags.forEach(function (messageTag) {
// 判断是否为当前需要更新的弹窗对象
if (document.querySelector(`#${item.message}`).id == messageTag.element.id) { // 替判断当前弹窗对象的条件
if (visibilitys == 'visible') {
messageTag.element.style.visibility = 'hidden';
this_.tagData[index].flag = false;
} else {
messageTag.element.style.visibility = 'visible';
this_.tagData[index].flag = true;
}
// 创建线条材质
var lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000, linewidth: 100 });
// 获取点击模型和弹窗的位置坐标
var modelPosition = chooseMeshData.position.clone();
var newPositions = new THREE.Vector3().copy(messageTag.position); // 创建新的Vector3对象并复制点击对象的坐标
messageTag.position.copy(newPositions);
// var popupPosition = messageTag.position.clone();
var popupPosition = messageTag.position.copy({
x: item.x,
y: item.y,
z: item.z
});
// 创建线条的顶点数组
var positions = [];
positions.push(modelPosition.x, modelPosition.y, modelPosition.z);
positions.push(popupPosition.x, popupPosition.y, popupPosition.z);
// 创建线条的属性数组
var geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
// 创建线条对象
var line = new THREE.LineSegments(geometry, lineMaterial);
// 添加线条到场景中
line.name = item.message
this_.scene.add(line);
// 获取场景中的所有线段
var allLineSegments = [];
this_.scene.traverse(function (child) {
if (child instanceof THREE.LineSegments) {
allLineSegments.push(child);
}
});
// 打印所有线段
if (visibilitys == 'visible') {
allLineSegments.forEach((node) => {
if (node.name == item.message) {
this_.scene.remove(node);
}
})
}
}
});
}
},
tag(domID) {
var dom = document.getElementById(domID);
var label = new CSS2DObject(dom);
label.name = domID
dom.style.pointerEvents = 'none';
return label;
},
choose(event, messageTag, type, name) {
let chooseMesh = null
if (type == 1) {
//这个逻辑里用不上,正常是点击模型里的 后来改成点击按钮了
if (this.chooseMesh) {
this.chooseMesh.material.color.set(0xffffff); // 把上次选中的mesh设置为原来的颜色
}
var Sx = event.clientX;
var Sy = event.clientY;
let mainCanvas = document.querySelector('canvas')
var x = ((event.clientX - mainCanvas.getBoundingClientRect().left) / mainCanvas.offsetWidth) * 2 - 1;
var y = -((event.clientY - mainCanvas.getBoundingClientRect().top) / mainCanvas.offsetHeight) * 2 + 1;
var raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);
var intersects = raycaster.intersectObjects(this.granaryArr);
console.log("射线器返回的对象", intersects);
if (intersects.length > 0) {
this.chooseMesh = intersects[0].object;
this.chooseMesh.material.color.set(0x00ffff); //选中改变颜色,这样材质颜色贴图.map和color颜色会相乘
this.chooseMesh.point = intersects[0].point;
} else {
this.chooseMesh = null;
}
} else {
this.granaryArr.forEach((item) => {
if (item.name == name) {
console.log(item, 'd点击后的item', item.material.color.getHex())
chooseMesh = item;
if (item.material.color.getHex() == 16777215) {
chooseMesh.material.color.set(0xff0000); //选中改变颜色,这样材质颜色贴图.map和color颜色会相乘
} else {
chooseMesh.material.color.set(0xffffff); // 把上次选中的mesh设置为原来的颜色
}
chooseMesh.point = item.position;
// chooseMesh.point = item.point;
// this.chooseMesh.point = {
// x: item.x,
// y: item.y,
// z: item.z
// }
}
})
}
return chooseMesh
},
handleSetLineChartData(type) {
console.log(type)
this.tagFlage = type
},
以上就是完整的,
弹窗的位置,和线的结束点是我写死的,因为我不知道创建的弹窗的位置该怎么设置
新手three 有大佬的话 可以给些建议
后续补充了一点新的 弹窗位置和线条的起始结束点
给我的当前弹窗点击拿到后设置
var popupPosition = messageTag.position.copy({
x: item.x,
y: item.y,
z: item.z
});
这组就是线条的结束点 push到一个存起点和结束点的数组里然后放到THREE.Float32BufferAttribute文章来源:https://www.toymoban.com/news/detail-762045.html
数据的坐标点基本都这样的
{
name: ‘XXX’,
message: ‘XXX’,
x: -2,
y: 0.8,
z: 1,
x1: -43,
y1: -45,
flag: false,
},文章来源地址https://www.toymoban.com/news/detail-762045.html
到了这里,关于threeJS 实现加载模型 + 页面按钮交互 + 显示css2Renderer标注的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!