本文介绍threejs引入glb格式模型展示
1.鼠标事件交互
2.局部放大效果
3.端口状态渲染
4.点击鼠标改变端口状态
<template>
<!-- 三维画布 -->
<div style="width:100%;height:100%;position:relative;">
<div id="three_div" ref="draw" class="draw" />
<div v-if="loadSuccess" class="loadingBox">
<div class="progress">
<div class="progress-bar progress-bar-danger progress-bar-striped active" :style="{width:progress}">
<div class="progress-value">{{ progress }}</div>
</div>
</div>
</div>
</div>
</template>
js代码
<script>
import * as THREE from 'three' // 三维
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' // 控制器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' // 控制器
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' // 控制器
export default {
props: {
faceList: {
type: Array,
default: () => []
},
capacity: {
type: [Number, String],
default: 0
}
},
data() {
return {
// 声明渲染器
renderer: '',
// 声明相机
camera: '',
// 声明场景
scene: '',
// 声明几何体
geometry: '',
// 声明材质
material: '',
// 声明网格
mesh: '',
// 声明相机控制器
controls: '',
// 画布大小
clientWidth: '',
clientHeight: '',
// 模型组
faceGroup: [],
// 记住选中的端口
selectedPort: null,
progress: 0,
loadSuccess: true
}
},
computed: {
portStatus() {
let img = new THREE.TextureLoader().load('/res/glb/glbstatus/white.jpg')
if (this.capacity === 96 || this.capacity === 24) {
img = new THREE.TextureLoader().load('/res/glb/glbstatus/blue48.jpg')
}
return {
IDLE: {
img: img,
name: this.$t('PORT_STATUS_IDLE'),
typeName: 'IDLE'
},
PROCESS: {
img: new THREE.TextureLoader().load('/res/glb/glbstatus/process.jpg'),
name: this.$t('SERVICE_AVAILABLE'),
typeName: 'PROCESS'
},
MAIN: {
img: new THREE.TextureLoader().load('/res/glb/glbstatus/occupy.jpg'),
name: this.$t('SERVICE_UNAVAILABLE'),
typeName: 'MAIN'
},
BACKUP: {
img: new THREE.TextureLoader().load('/res/glb/glbstatus/backup.jpg'),
name: this.$t('PORT_STATUS_BACKUP'),
typeName: 'BACKUP'
},
DAMAGE: {
img: new THREE.TextureLoader().load('/res/glb/glbstatus/damage.jpg'),
name: this.$t('PORT_STATUS_DAMAGE'),
typeName: 'DAMAGE'
},
OCCUPY: {
img: new THREE.TextureLoader().load('/res/glb/glbstatus/damage1.jpg'),
name: this.$t('LINK_FAILURE'),
typeName: 'OCCUPY'
},
CONN: {
img: new THREE.TextureLoader().load('/res/glb/glbstatus/occupy.jpg'),
name: this.$t('LINK_FAILURE'),
typeName: 'CONN'
}
}
}
},
watch: {
faceList(val) {
console.log(val)
}
},
mounted() {
this.init()
},
beforeDestroy() {
window.removeEventListener('resize', this.changeSize)
window.removeEventListener('click', this.portClick)
window.removeEventListener('wheel', this.handleMouseWheel)
this.removeObj(this.scene)
this.renderer && this.renderer.dispose()
this.renderer.forceContextLoss()
this.renderer.domElement = null
this.renderer.content = null
this.renderer = null
cancelAnimationFrame(this._animate)
THREE.Cache.clear()
},
methods: {
init() {
// 初始化渲染器
this.initRenderer()
// 初始化场景
this.initScene()
// 初始化相机
this.initCamera()
// 引入模型
this.initgltfLoader()
// 初始化光源
this.initLight()
// 初始化动画
this.animate()
// 添加事件
this.addmeth()
},
// 初始化渲染器
initRenderer() {
// 实例化渲染器
this.renderer = new THREE.WebGLRenderer({
antialias: true, // 是否开启抗锯齿
alpha: true // 是否可以将背景色设置为透明
})
// 设置渲染区域尺寸
this.renderer.setSize(
this.$refs.draw.offsetWidth,
this.$refs.draw.offsetHeight
)
// 告诉渲染器需要阴影效果
this.renderer.shadowMap.enabled = true
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
// 设置背景色
this.renderer.setClearColor(0x000000, 0) // 设置背景颜色
this.$refs.draw.appendChild(this.renderer.domElement)
},
// 初始化场景
initScene() {
// 实例化场景
this.scene = new THREE.Scene()
// 红线是X轴,绿线是Y轴,蓝线是Z轴
var axesHelper = new THREE.AxesHelper(0)
this.scene.add(axesHelper)
},
// 初始化相机
initCamera() {
this.clientWidth = this.$refs.draw.clientWidth
this.clientHeight = this.$refs.draw.clientHeight
const k = this.clientWidth / this.clientHeight // 窗口宽高比
// 参数:PerspectiveCamera
// fov — 垂直视野角度(从底部到顶部,以度为单位。 默认值为50。)
// aspect — 长宽比(一般为渲染器、画布长宽比,默认为1)
// near — 近距离(默认值为0.1)
// far — 远距离(默认为2000,必须大于近距离的值。)
this.camera = new THREE.PerspectiveCamera(45, k, 0.1, 1000)
this.camera.position.set(30, 30, 100)
// 创建相机控制器
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableZoom = false
},
// 引入外部模型 gltf
initgltfLoader() {
const gltfLoader = new GLTFLoader()
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/res/glb/draco/gltf/')// 这个是加载draco算法,这样才能解析压缩后的gltf模型格式.
gltfLoader.setDRACOLoader(dracoLoader)
// 引入默认纹理
gltfLoader.load('/res/glb/device3d/AFS' + this.capacity + '.glb', (gltf) => {
const model = gltf.scene
console.log(model)
if (this.capacity === 24) {
model.scale.set(0.3, 0.3, 0.3) // 缩放
model.position.set(0, -5, 0)
} else if (this.capacity === 96) {
model.scale.set(0.7, 0.7, 0.7) // 缩放
model.position.set(0, -30, 0)
} else {
model.scale.set(0.7, 0.7, 0.7) // 缩放
model.position.set(0, -20, 0)
}
const portGroup = model.children && model.children[0].children.find(item => item.name === 'PORT_GROUP')
if (!portGroup) return
const portList = portGroup.children
portList.forEach(item => {
item.nameIndex = item.name.replace(/[^\d]/g, '')
})
portList.sort((a, b) => { return a.nameIndex - b.nameIndex })
this.faceGroup[0] = portList.filter(item => item.name.includes('Port_A'))
this.faceGroup[1] = portList.filter(item => item.name.includes('Port_B'))
this.faceList.forEach((item, index) => {
item.portList.forEach((port, k) => {
this.faceGroup[index][k].portData = port
this.faceGroup[index][k].portId = port.portInfo.portId
})
})
setTimeout(() => {
// 给端口重新赋纹理
this.faceGroup.forEach(item => {
item.forEach(port => {
const texture = this.portStatus[port.portData.portInfo.status]
port.children.forEach(mesh => {
mesh.material = new THREE.MeshPhongMaterial({ color: 0xFFFFFF })
mesh.material.map = texture.img
mesh.textureName = texture.typeName
})
})
})
}, 0)
this.scene.add(model)
}, (xhr) => {
const percentage = Math.floor(xhr.loaded / xhr.total * 100)
this.progress = percentage + '%'
if (percentage >= 99) {
setTimeout(() => {
this.loadSuccess = false
}, 2000)
}
})
},
// 添加光源
initLight() {
// 全局环境光
const ambientLight = new THREE.AmbientLight('#ffffff', 0.1)
this.scene.add(ambientLight)
// 点光源
const pointLight = new THREE.PointLight('#ffffff', 1)
pointLight.position.set(100, 500, 500)
this.camera.add(pointLight)
this.scene.add(this.camera)
},
addmeth() {
// 监听窗口尺寸变化
window.addEventListener('resize', this.changeSize, false)
window.addEventListener('click', this.portClick, false)
window.addEventListener('wheel', this.handleMouseWheel, false)
},
handleMouseWheel(event) {
// 设置相机缩放比数值越大缩放越明显
const factor = 2
// 从鼠标位置转化为webgl屏幕坐标位置
const glScreenX = (event.clientX / this.controls.domElement.width) * 2 - 1
const glScreenY = -(event.clientY / this.controls.domElement.height) * 2 + 1
const vector = new THREE.Vector3(glScreenX, glScreenY, 0)
// 从屏幕向量转为3d空间向量
vector.unproject(this.controls.object)
// 相机偏移量
vector.sub(this.controls.object.position).setLength(factor)
if (event.deltaY < 0) {
this.controls.object.position.add(vector)
this.controls.target.add(vector)
} else {
this.controls.object.position.sub(vector)
this.controls.target.sub(vector)
}
this.controls.update()
},
// 运行动画
animate() {
this._animate = requestAnimationFrame(this.animate.bind(this)) // 循环调用函数
// 刷新相机控制器
this.controls.update()
this.renderer.render(this.scene, this.camera)
},
// 点击端口
portClick(event) {
// 保持原事件
event.preventDefault()
this.getIntersects(event.layerX, event.layerY)
},
getIntersects(layerX, layerY) {
// 建立射线
const raycaster = new THREE.Raycaster()
// // 建立一个空物体
const mouseVector = new THREE.Vector3()
const x = (layerX / this.clientWidth) * 2 - 1
const y = -(layerY / this.clientHeight) * 2 + 1
mouseVector.set(x, y, 1)
raycaster.setFromCamera(mouseVector, this.camera)
raycaster.params.Line.threshold = 0.01
const intersections = raycaster.intersectObjects(this.scene.children, true)
let selectedObject = null // 被选中的模型
if (intersections.length > 0) {
for (var i = 0; i < intersections.length; i++) {
// 遍历线相交模型
if (intersections[i].object instanceof THREE.Mesh) {
// 取第一个(距离最近)的相交Mesh类型模型
// 如果要排除地面等参照模型,也可在此处添加判断条件
selectedObject = intersections[i].object
break
}
}
}
if (selectedObject) {
const selected = new THREE.TextureLoader().load('/res/glb/glbstatus/selected.jpg')
if (selectedObject.parent.name.includes('Port_')) {
if (this.selectedPort && this.selectedPort.parent.portData.connPortId !== selectedObject.parent.portData.connPortId) {
this.faceGroup.forEach(item => {
item.forEach(port => {
const texture = this.portStatus[port.portData.portInfo.status]
if (this.selectedPort.parent.portData.connPortId === port.portData.connPortId ||
this.selectedPort.parent.portData.connPortId === port.portData.portInfo.portId) {
port.children.forEach(mesh => {
mesh.material.map = texture.img
mesh.textureName = texture.typeName
})
}
})
})
}
selectedObject.parent.children.forEach(item => {
if (item.textureName !== 'SELECTED') {
this.curTexture = item.textureName
item.material.map = selected
item.textureName = 'SELECTED'
this.selectedPort = selectedObject
this.$emit('portClick', selectedObject.parent.portData)
} else {
item.material.map = this.portStatus[this.curTexture].img
item.textureName = this.curTexture
this.$emit('portClick', null)
}
})
this.faceGroup.forEach(item => {
const obj = item.find(port => port.portId === selectedObject.parent.portData.connPortId)
if (obj) {
obj.children.forEach(mesh => {
if (mesh.textureName !== 'SELECTED') {
mesh.material.map = selected
mesh.textureName = 'SELECTED'
} else {
mesh.material.map = this.portStatus[this.curTexture].img
mesh.textureName = this.curTexture
}
})
}
})
}
// console.log(selectedObject)
}
},
// 监听尺寸变化
changeSize() {
// 重置渲染器输出画布canvas尺寸
this.renderer.setSize(
this.$refs.draw.offsetWidth,
this.$refs.draw.offsetHeight
)
this.clientWidth = this.$refs.draw.clientWidth
this.clientHeight = this.$refs.draw.clientHeight
const k = this.clientWidth / this.clientHeight // 窗口宽高比
// 重置相机投影的相关参数
this.camera.aspect = k
// 如果相机的一些属性发生了变化,
// 需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
this.camera.updateProjectionMatrix()
},
removeObj(obj) {
let arr = obj.children.filter(x => x)
arr.forEach(item => {
if (item.children.length) {
this.removeObj(item)
} else {
item.clear()
}
})
obj.clear()
arr = null
}
}
}
</script>
css代码
<style lang="scss">
#three_div{
width:100%;
height: 100%;
}
//进度条
.loadingBox{
position: absolute;
top:0;
left:0;
right: 0;
bottom: 0;
background: #020721;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.progress {
height: 17px;
background: #262626;
padding: 3px;
overflow: visible;
border-radius: 20px;
border-top: 1px solid #000;
border-bottom: 1px solid #7992a8;
margin-top: 50px;
width: 300px;
.progress-bar {
border-radius: 20px;
position: relative;
animation: animate-positive 2s;
float: left;
width: 0;
height: 100%;
font-size: 12px;
line-height: 20px;
color: #fff;
text-align: center;
background-color: #2962f9;
-webkit-transition: width .6s ease;
-o-transition: width .6s ease;
transition: width .6s ease;
}
.active {
animation: reverse stripes 0.40s linear infinite, animate-positive 2s;
}
.progress-value {
display: none;
padding: 3px 7px;
font-size: 13px;
color: #fff;
border-radius: 4px;
background: #191919;
border: 1px solid #000;
position: absolute;
top: -40px;
right: -10px;
}
.progress-value:after {
content: "";
border-top: 10px solid #191919;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
position: absolute;
bottom: -6px;
left: 26%;
}
}
.progress-bar-striped {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
-webkit-background-size: 40px 40px;
background-size: 40px 40px;
}
@-webkit-keyframes stripes {
from {
background-position: 40px 0
}
to {
background-position: 0 0
}
}
@-o-keyframes stripes {
from {
background-position: 40px 0
}
to {
background-position: 0 0
}
}
@keyframes stripes {
from {
background-position: 40px 0
}
to {
background-position: 0 0
}
}
@-webkit-keyframes animate-positive {
0% {
width: 0;
}
}
@keyframes animate-positive {
0% {
width: 0;
}
}
</style>