导语
在Three.js
中,使用CSS3DRenderer
和CSS3DSprite
可以轻松地实现模型标签文字的效果,为场景中的模型提供更直观的信息展示。本文将介绍如何使用这两个工具来实现模型标签文字,并提供相应的代码示例。
引言
Three.js
是一款强大的JavaScript 3D
库,用于在Web上
创建交互式的3D
图形应用程序。在Three.js
中,CSS3DRenderer
和CSS3DSprite
是两个重要的工具,它们可以用于在3D
场景中渲染HTML
元素,为用户提供更丰富的交互体验。
需要达到的效果
在导入模型,遍历模型的时候,使用CSS3DRenderer
和CSS3DSprite
对指定模型加上标签文字,并在标签文字下创建指示线(由几何图形绘制而成
),标签文字可以随着x,y,z
的旋转则一直对着详细展示,指示线则只沿着x,z两个方向根据相机旋转给用户展示。其中指示线+标签文字根据循环的每个模型进行自动定位,但是指示线要结合标签文字位置慢慢调试,即可达到效果。
实现的效果
实现模型标签文字的步骤
步骤一、导入所需库
// Three.js库
import * as THREE from 'three'; // CSS3DRenderer用于渲染CSS3D对象
import { CSS3DRenderer, CSS3DSprite } from "three/examples/jsm/renderers/CSS3DRenderer.js";
步骤二、初始化CSS3DRenderer
const labelRenderer = new CSS3DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器尺寸
labelRenderer.domElement.style.position = 'absolute'; // 设置渲染器样式
labelRenderer.domElement.style.top = '0'; // 设置渲染器样式
document.body.appendChild(labelRenderer.domElement); // 将渲染器挂载到页面上
步骤三、创建css3D标签
const div = document.createElement('div');
div.className = 'workshop-text'; // 添加样式类
div.innerHTML = '<p>浮法车间</p>'; // 添加标签文字
步骤四、创建CSS3DSprite对象
const sprite = new CSS3DSprite(div);
sprite.position.set(8, 3, 42); // 设置标签位置,这里根据模型具体位置调整
步骤五、将CSS3DSprite添加到模型中,并通过GUI控制器控制文字位置
object.add(sprite); // object是模型对象,这里需要替换为实际的模型对象
// 在GUI中添加文件夹用于调整标签位置
const tagFolder = gui.addFolder('浮法车间标签');
tagFolder.add(sprite.position, 'x', -200, 200).name('X Position'); // 调整标签x位置
tagFolder.add(sprite.position, 'y', -200, 200).name('Y Position'); // 调整标签y位置
tagFolder.add(sprite.position, 'z', -200, 200).name('Z Position'); // 调整标签z位置
tagFolder.open(); // 打开文件夹,默认显示控制器
步骤六、添加好tag标签后,创建指示线createPointerLine
- 创建指向线函数
/*** 创建指向线的函数,用于在 Three.js 场景中创建指向线* @param {THREE.Vector3} start 指向线的起点坐标* @param {THREE.Vector3} end 指向线的终点坐标* @param {string} color 指向线的颜色,格式为 CSS 颜色值,例如 "#ff0000" 表示红色* @param {number} width 指向线的宽度* @param {string} background 背景贴图的路径*/
// 创建指向线的函数
function createPointerLine(start, end, color, width, background) {// 创建指向线的几何体const geometry = new THREE.BufferGeometry();const vertices = new Float32Array([start.x,start.y,start.z,end.x,end.y,end.z,]);geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));// 创建指向线的材质const material = new THREE.LineBasicMaterial({color: "#ff0000", // 指定线的颜色linewidth: width, // 设置线的宽度});// 创建指向线对象const line = new THREE.Line(geometry, material);// 创建一个 Object3D 用于存放线const pointerGroup = new THREE.Object3D();pointerGroup.add(line);// 计算线的方向向量const direction = new THREE.Vector3().copy(end).sub(start).normalize();// 计算线头和线尾的位置, 设置圆饼偏移量const headPosition = new THREE.Vector3().copy(start).addScaledVector(direction,31.5);const tailPosition = new THREE.Vector3().copy(end).addScaledVector(direction, -28);//贴图// 使用 TextureLoader 加载贴图const yxTextureLoader = new THREE.TextureLoader();const yxTexture = yxTextureLoader.load("../../public/label/yuandianTop.png"); // 加载贴图// 创建线头圆饼const headGeometry = new THREE.CircleGeometry(4, 32);const headMaterial = new THREE.MeshBasicMaterial({color: "#02f1ff", // 指定线尾的颜色side: THREE.DoubleSide, // 设置双面可见map: yxTexture, // 设置贴图 transparent: true, // 设置材质为透明});const headMesh = new THREE.Mesh(headGeometry, headMaterial);headMesh.position.copy(headPosition);// 创建线尾圆饼const tailGeometry = new THREE.CircleGeometry(1, 32);const tailMaterial = new THREE.MeshBasicMaterial({color: "#02f1ff",side: THREE.DoubleSide,});const tailMesh = new THREE.Mesh(tailGeometry, tailMaterial);tailMesh.position.copy(tailPosition);// 使用 TextureLoader 加载背景纹理图片const backgroundMaterial = new THREE.MeshBasicMaterial({// map: backgroundTexture, // 设置背景的纹理贴图side: THREE.DoubleSide, // 设置双面可见color: "#02f1ff",});const backgroundGeometry = new THREE.PlaneGeometry(width,end.distanceTo(start), // 背景的长度,即线段的长度1,1);const backgroundMesh = new THREE.Mesh(backgroundGeometry, backgroundMaterial);// 设置背景的位置为线段的中点const midpoint = new THREE.Vector3().copy(start).add(end).multiplyScalar(0.5);backgroundMesh.position.copy(midpoint); // 设置背景的朝向// 设置背景的朝向(垂直方向上朝向相机位置),计算垂直方向上背景朝向的点,即与相机位置相同高度的点const verticalLookAtPoint = new THREE.Vector3(camera.position.x,backgroundMesh.position.y,camera.position.z);backgroundMesh.lookAt(verticalLookAtPoint);// backgroundMesh.up.set(0, 1, 0);// 将线和背景添加到场景中function updateOrientation() {headMesh.lookAt(camera.position); // 使线头始终朝向相机tailMesh.lookAt(camera.position); // 使线尾始终朝向相机// 获取相机的水平方向向量const cameraDirection = camera.getWorldDirection(new THREE.Vector3()).normalize();const cameraHorizontalDirection = new THREE.Vector3(cameraDirection.x,0,cameraDirection.z).normalize();// 让背景朝向相机的水平方向backgroundMesh.lookAt(backgroundMesh.position.clone().add(cameraHorizontalDirection));}// 在渲染循环中调用更新函数function render() {updateOrientation();requestAnimationFrame(render);}render();scene.add(headMesh);scene.add(backgroundMesh);scene.add(tailMesh);
}
- 所有代码:
<template><div class="app-container"><el-row v-if="isPointList" :gutter="20"><el-col :span="24" :xs="24"><el-row><el-buttonicon="el-icon-circle-plus-outline"size="mini"type="primary"@click.native="addWorkUnit">新增网关</el-button><span style="float: right; margin-bottom: 11px">入场日期:<el-date-pickerstyle="width: 195px;margin-right: 10px;position: relative;top: 1px;"size="mini"format="yyyy-MM-dd"value-format="yyyy-MM-dd"v-model="entryDate"type="daterange"range-separator="至"start-placeholder="开始日期"end-placeholder="结束日期"></el-date-picker><el-button size="mini" type="primary" @click.native="getList">查询</el-button><el-button type="info" size="mini" @click.native="resetDatePick">重置</el-button></span></el-row><el-row><hsk-table :data="geteWayTableData"><template v-slot:gatewaySn_siff_slot><el-inputv-model="queryParams.gatewaySn"size="mini"width="110px"placeholder="请输入"clearable@clear="getList()"@keyup.enter.native="getList()"/></template><template v-slot:gatewayName_siff_slot><el-inputv-model="queryParams.gatewayName"size="mini"width="100px"placeholder="请输入"clearable@clear="getList()"@keyup.enter.native="getList()"/></template><template v-slot:gatewayModelName_siff_slot><el-selectsize="mini"v-model="queryParams.gatewayModelId"placeholder="请选择"@change="getList"clearable@clear="getList"><el-optionv-for="item in modelList":key="item.id":label="item.label":value="item.value"></el-option></el-select></template><template v-slot:gatewayManufacturerName_siff_slot><el-selectsize="mini"v-model="queryParams.gatewayManufacturerId"placeholder="请选择"@change="getList"clearable@clear="getList"><el-optionv-for="item in manufacturerList":key="item.id":label="item.label":value="item.value"></el-option></el-select></template><template v-slot:createBy_siff_slot><el-inputv-model="queryParams.createBy"size="mini"placeholder="请输入"clearable@clear="getList()"@keyup.enter.native="getList()"/></template><template v-slot:description_siff_slot><el-inputv-model="queryParams.description"size="mini"placeholder="请输入"clearable@clear="getList()"@keyup.enter.native="getList()"/></template><template v-slot:controls_slot="scope"><el-button size="mini" type="text" @click="pointList(scope.row)">点位列表</el-button><el-buttonsize="mini"type="text"@click="editGetewayDevice(scope.row)">编辑</el-button><el-buttonsize="mini"type="text"style="color: red"@click="delGetewayDevice(scope.row)">删除</el-button></template></hsk-table><hsk-paginationv-show="queryParams.total > 0":total="queryParams.total":page.sync="queryParams.current":limit.sync="queryParams.pageSize"@sizeChange="handleSizeChange"@currentChange="currentChange"></hsk-pagination></el-row></el-col></el-row><el-row v-if="!isPointList" :gutter="20"><el-col :span="24" :xs="24"><el-row style="margin-bottom: 15px"><svg-iconicon-class="back"style="margin-right: 20px;font-weight: bold;cursor: pointer;width: 24px;height: 20px;opacity: 1;"@click.native="breakClick"/><span class="point-title">{{currentDeviceInfo.currentDeviceName}}</span><span class="point-title">{{currentDeviceInfo.currentDeviceSn}}</span></el-row><el-row style="margin-bottom: 11px"><el-buttonicon="el-icon-circle-plus-outline"size="mini"type="primary"@click.native="addDevicePointClick">新增点位</el-button><el-buttonicon="el-icon-circle-plus-outline"size="mini"disabledtype="primary"@click.native="addWorkUnit">点位导入</el-button><el-buttonicon="el-icon-circle-plus-outline"size="mini"type="primary"@click.native="pointClear">点位清空</el-button></el-row><el-row><hsk-table :data="pointTableData"><template v-slot:pointName_siff_slot><el-inputv-model="deviceQuery.pointName"size="mini"placeholder="请输入"clearable@clear="getDeviceList()"@keyup.enter.native="getDeviceList()"/></template><template v-slot:pointSn_siff_slot><el-inputv-model="deviceQuery.pointSn"size="mini"placeholder="请输入"clearable@clear="getDeviceList()"@keyup.enter.native="getDeviceList()"/></template><template v-slot:pointDataTypeName_siff_slot><el-selectsize="mini"v-model="deviceQuery.pointDataTypeId"placeholder="请选择"@change="getDeviceList"clearable@clear="getDeviceList"><el-optionv-for="item in dataTypeArr":key="item.id":label="item.label":value="item.value"></el-option></el-select></template><template v-slot:pointValueMsg_slot="scope"><span v-if="scope.row.pointDataTypeName === 'INT32(整数型)'">取值范围: {{ JSON.parse(scope.row.pointValueMsg).min }} ~{{ JSON.parse(scope.row.pointValueMsg).max }}</span><spanv-if="scope.row.pointDataTypeName === 'FLOAT(单精度浮点型)'">取值范围: {{ JSON.parse(scope.row.pointValueMsg).min }} ~{{ JSON.parse(scope.row.pointValueMsg).max }}</span><spanv-if="scope.row.pointDataTypeName === 'DOUBLE(双精度浮点型)'">取值范围: {{ JSON.parse(scope.row.pointValueMsg).min }} ~{{ JSON.parse(scope.row.pointValueMsg).max }}</span><span v-if="scope.row.pointDataTypeName === 'BOOL(布尔型)'">布尔值: <br />0 —{{ JSON.parse(scope.row.pointValueMsg).zero }}<br />1 — {{ JSON.parse(scope.row.pointValueMsg).one }}</span><span v-if="scope.row.pointDataTypeName === 'TEXT(字符串)'">数据长度: {{ JSON.parse(scope.row.pointValueMsg).dataLength }}</span></template></hsk-table><hsk-paginationv-show="deviceQuery.total > 0":total="deviceQuery.total":page.sync="deviceQuery.current":limit.sync="deviceQuery.pageSize"@sizeChange="handlePointSizeChange"@currentChange="currentPointChange"></hsk-pagination></el-row></el-col></el-row><EditgetewayV2 ref="editDevice" @getList="getList"></EditgetewayV2><AddDevicePointref="AddDevicePoint"@getList="getDeviceList"></AddDevicePoint></div>
</template>
<script>
import {getDictData,gatewayList,deteleGateway,gatewayPointList,gatewayClearPoint,
} from "@/api/tenant/gatewayV2.js";
import AddDevicePoint from "./addDevicePoint.vue";
import EditgetewayV2 from "./editgetewayV2.vue";
import { addDevicePoint } from "@/api/tenant/devicePointConfig";
export default {name: "gatewayAdministration",components: {EditgetewayV2,AddDevicePoint,},data() {return {modelList: [],manufacturerList: [],entryDate: null,workUnitData: [],geteWayTableData: {showHeader: true, //是否显示表头size: "small", //列表的型号fit: true, //列的宽度是否自己撑开height: "600", //表格高度isRowBgc: false, //如果需要设定行背景,需要指定rowClassNamerowClassName: {bgc: "pink",attrName: "xs",},isStripe: false, // 是否边框isBorder: true,isMutiSelect: false, //是否需要多选isRadio: false, //是否单选tableData: [],columns: [{label: "网关编码",prop: "gatewaySn",isCondition: true,align: "center",slot_siff_name: "gatewaySn_siff_slot",width: "",},{label: "名称",align: "center",prop: "gatewayName",isCondition: true,slot_siff_name: "gatewayName_siff_slot",width: "",},{label: "型号",align: "center",prop: "gatewayModelName",isCondition: true,slot_siff_name: "gatewayModelName_siff_slot",width: "",},{label: "厂家",align: "center",prop: "gatewayManufacturerName",isCondition: true,slot_siff_name: "gatewayManufacturerName_siff_slot",width: "",},{label: "入场日期",align: "center",prop: "entryFactoryTime",width: "",},{label: "创建人",align: "center",prop: "createBy",isCondition: true,slot_siff_name: "createBy_siff_slot",width: "",},{label: "创建日期",align: "center",prop: "createTime",width: "",},{label: "描述",align: "center",prop: "description",isCondition: true,slot_siff_name: "description_siff_slot",width: "",},{type: "slot",align: "center",label: "操作",slot_name: "controls_slot",width: "",},],},pointTableData: {showHeader: true, //是否显示表头size: "small", //列表的型号fit: true, //列的宽度是否自己撑开height: "600", //表格高度isRowBgc: false, //如果需要设定行背景,需要指定rowClassNamerowClassName: {bgc: "pink",attrName: "xs",},isStripe: false, // 是否边框isBorder: true,isMutiSelect: false, //是否需要多选isRadio: false, //是否单选tableData: [],columns: [{label: "点位名称",align: "center",prop: "pointName",isCondition: true,slot_siff_name: "pointName_siff_slot",width: "",},{label: "标识符",align: "center",prop: "pointSn",isCondition: true,slot_siff_name: "pointSn_siff_slot",width: "",},{label: "数据类型",align: "center",prop: "pointDataTypeName",isCondition: true,slot_siff_name: "pointDataTypeName_siff_slot",width: "",},{type: "slot",align: "center",slot_name: "pointValueMsg_slot",label: "数据定义",prop: "pointValueMsg",width: "",},{label: "单位",align: "center",prop: "unit",width: "",},],},fileDownLoadIp: process.env.VUE_APP_FILE_DWONLOAD,creationDate: "",dataTypeArr: [],pointListData: [],currentDeviceInfo: {currentDeviceId: null,currentDeviceName: null,currentDeviceSn: null,deviceId: null,},deviceQuery: {total: 0,current: 1,pageSize: 10,},isPointList: true,ids: [], // 选中数组queryParams: {total: 0,current: 1,pageSize: 10,parentId: null, // 左侧树ID},tableLoading: false,tableData: [], //列表数据showDialogVisible: false, //控制弹窗是否显示};},watch: {},computed: {},created() {},mounted() {this.getList();this.getDictData();},methods: {handlePointSizeChange(val) {this.deviceQuery.pageSize = val;this.getDeviceList()},currentPointChange(val) {this.deviceQuery.current = val;this.getDeviceList()},handleSizeChange(val) {this.queryParams.pageSize = val;this.getList();},currentChange(val) {this.queryParams.current = val;this.getList();},//新增点位addDevicePointClick() {this.$refs.AddDevicePoint.add(this.currentDeviceInfo);},//重置resetDatePick() {this.entryDate = null;this.getList();},pointClear() {console.log("this.deviceQuery.gatewayId",this.deviceQuery.gatewayId)gatewayClearPoint({ gatewayId: this.deviceQuery.gatewayId }).then((res) => {this.getDeviceList();this.$modal.msgSuccess("点位清空成功");});},//获取 点位,厂家,型号列表getDictData() {getDictData({ dictCodeList: ["point_data_type"] }).then((res) => {this.dataTypeArr = res.data.point_data_type;});getDictData({ dictCodeList: ["gateway_manufacturer"] }).then((res) => {this.manufacturerList = res.data.gateway_manufacturer;});getDictData({ dictCodeList: ["gateway_model"] }).then((res) => {this.modelList = res.data.gateway_model;});},breakClick() {this.isPointList = true;},// 点位列表pointList(row) {this.currentDeviceInfo.currentDeviceId = row.gatewayId;this.currentDeviceInfo.currentDeviceName = row.gatewayName;this.currentDeviceInfo.currentDeviceSn = row.gatewaySn;this.currentDeviceInfo.deviceId = row.deviceId;this.isPointList = !this.isPointList;this.deviceQuery.gatewayId = row.gatewayIdthis.deviceQuery.deviceId = row.deviceId;this.pointListData = [];this.getDeviceList();},getDeviceList() {gatewayPointList(this.deviceQuery).then((res) => {this.pointListData = res.data.data;this.pointTableData.tableData = res.data.data;this.deviceQuery.total = parseInt(res.data.total);});},userInfoImport() {this.$refs.openWindow.updataDiaglog();},//数据导入updataInfo(fileInfo) {let formData = new FormData();formData.append("file", fileInfo.file.raw);// formData.append("importType", 'user');excelImport(formData).then((res) => {this.init();this.getList();this.$modal.msgSuccess("导入成功");});},editDevice(row) {this.$refs.editDevice.edit(JSON.parse(JSON.stringify(row)));},//新增工作单位addWorkUnit() {this.$refs["editDevice"].add(this.currentNode);},//编辑工作单位editWorkUnit(row) {this.$refs["editDevice"].edit(this.currentNode, row);},//重置密码resetPassword(row) {const h = this.$createElement;let confirmText = [`确定重置账号【${row.userAccountName}】,密码为初始密码【JB23456】`,`操作后用户将不能使用之前密码登录,请谨慎操作。`,];this.hskMsgbox(h, "重置密码", confirmText).then((res) => {if (res) {resetPassword({ userId: row.userId }).then((res) => {this.$message({ message: "重置密码成功", type: "success" });});} else {this.$message("取消重置");}});},editGetewayDevice(row) {this.$refs.editDevice.edit(JSON.parse(JSON.stringify(row)));},delGetewayDevice(row) {const h = this.$createElement;let confirmText = [`是否确定删除该网关【${row.gatewaySn} ${row.gatewayName}】?`,`删除后,与点位解绑,且无法恢复。请谨慎操作。`,];this.hskMsgbox(h, "删除网关设备", confirmText).then((res) => {if (res) {deteleGateway({ id: row.gatewayId }).then((res) => {this.$modal.msgSuccess("删除成功");this.getList();});} else {this.$message("取消删除");}});console.log("row", row);},//编辑部门editDepart(departName, departId) {this.$refs["editDepart"].edit(JSON.parse(JSON.stringify({ departName, departId })));},//删除部门deleteDepart(departName, departId) {if (this.searchTree(this.treeData, departId).children &&this.searchTree(this.treeData, departId).children.length > 0) {const h = this.$createElement;let confirmText = [`部门内还有用户或者组织,不可删除。`];this.hskMsgbox(h, "删除部门", confirmText).then((res) => {if (res) {return;} else {this.$message("取消删除");}});} else {const h = this.$createElement;let confirmText = [`是否删除部门【${departName}】,如果删除,数据将无法找回,请谨慎操作`,];this.hskMsgbox(h, "删除部门", confirmText).then((res) => {if (res) {deleteDepart({ id: departId }).then((res) => {this.$message({ message: "删除部门成功", type: "success" });this.init();this.getList();});} else {this.$message("取消删除");}});}},//列表状态编辑handleStatusChange(row) {},//获取网关列表getList() {if (this.entryDate) {this.queryParams.entryFactoryStartTime = this.entryDate[0];this.queryParams.entryFactoryEndTime = this.entryDate[1];} else {this.queryParams.entryFactoryStartTime = null;this.queryParams.entryFactoryEndTime = null;}this.tableLoading = true;gatewayList(this.queryParams).then((res) => {this.tableData = res.data.data;this.geteWayTableData.tableData = res.data.data;this.queryParams.total = parseInt(res.data.total);this.tableLoading = false;}).catch((err) => {this.tableLoading = false;});},//用户编辑handleUpdate(row) {},//用户删除handleDelete(row) {},// 多选框选中数据handleSelectionChange(selection) {this.idsInfo = selection;this.ids = selection.map((item) => item.userId);this.single = selection.length != 1;this.multiple = !selection.length;},//弹窗关闭cancel() {this.showDialogVisible = false;},//提交弹窗表单submitForm(val) {},},
};
</script>
<style lang="scss" scoped>
.point-title {font-size: 20px;font-family: Microsoft YaHei, Microsoft YaHei;font-weight: bold;color: #000000;line-height: 0px;margin-right: 16px;
}
.app-container {padding: 15px !important;
}
</style>
<style lang="less" scoped>
/deep/ .ant-tree-node-content-wrapper {width: 100%;position: relative;
}
.more-list {position: absolute;right: 40px;
}
.tree-title-more {transform: rotate(90deg) scale(0.8);
}
.tree-title-more:hover {color: #1890ff;
}
.head-container {width: 100%;min-width: 200px;max-height: 600px;
}
</style><style>
.el-message-box {width: 500px !important;
}
.el-message-box__title {text-align: center;font-size: 16px;font-weight: bold;color: #333333;line-height: 21px;
}
</style>
<style>
.el-message-box__status::before {display: none;
}
/* .el-message-box__status + .el-message-box__message {padding-left: 0px;
} */
</style>
效果:
更多效果敬请期待~