前端小白正在学习three.js,有感兴趣的大家一起相互交流
代码主要包括模型发光、3D卡片、视角转换、管道流动等在数字孪生、智慧工厂中常用的一些方法
我这里用的三个box,在实际应用中多为obj+mtl格式、或者gltf的模型,这些方法也是一样通用的
<template>
<div class="about">
<div id="model">
<div id="canvas1" style="display: none">canvas1</div>
<div id="canvas2" style="display: none">canvas2</div>
<div id="canvas3" style="display: none">canvas3</div>
<div id="canvas4" style="display: none">
<span @click.stop="deviceDetail(1)">设备详情</span>
</div>
<div id="canvas5" style="display: none">
<span @click="deviceDetail(2)">设备详情</span>
</div>
<div id="canvas6" style="display: none">
<span @click.stop="deviceDetail(3)">设备详情</span>
</div>
<div id="btns">
<span @click.stop="toView(1)">主视角</span>
<span @click.stop="toView(2)">俯视角</span>
</div>
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
import TWEEN from '@tweenjs/tween.js'
import $ from 'jquery'
// import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
// import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import {
CSS3DRenderer,
CSS3DObject,
} from 'three/examples/jsm/renderers/CSS3DRenderer'
export default {
name: '',
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
cssScene: null,
cssRender: null,
cssControls: null,
light: null,
light2: null,
flowingLineTexture: null,
group: new THREE.Group(),
composer: null, // 控制发光
outlinePass: null,
renderPass: null,
// 选中的模型
selectedObjects: [],
mouse: new THREE.Vector2(),
raycaster: new THREE.Raycaster(),
tween: null,
// 数据显示面板 1-3 数据 4-6为设备详情按钮
dataPanel: {
plane1: null,
plane2: null,
plane3: null,
plane4: null,
plane5: null,
plane6: null,
},
// 管道纹理
flowingLineTexture1: null,
flowingLineTexture2: null,
}
},
components: {},
computed: {},
watch: {},
created() {},
mounted() {
if (this.scene !== null) {
this.scene = null
}
this.draw()
},
destroyed() {
this.scene.autoUpdate = false
cancelAnimationFrame(this.animate)
this.renderer.domElement.innerHTML = ''
this.renderer.forceContextLoss()
this.renderer.dispose()
this.scene.children = []
},
methods: {
initScene() {
this.scene = new THREE.Scene()
this.cssScene = new THREE.Scene()
},
initCamera() {
this.camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
50000
)
this.camera.position.set(0, 1000, 2900)
// this.camera.lookAt(new THREE.Vector3(0,0,0))
},
initLight() {
var ambientLight = new THREE.AmbientLight(0x404040)
this.scene.add(ambientLight)
this.light = new THREE.DirectionalLight(0x333333)
this.light.position.set(20, 20, 20)
this.light2 = new THREE.DirectionalLight(0xdddddd)
this.light2.position.set(-20, 20, -20)
this.scene.add(this.light)
this.scene.add(this.light2)
},
initRender() {
//dom元素渲染器
this.cssRender = new CSS3DRenderer({ antialias: true })
this.cssRender.setSize(window.innerWidth, window.innerHeight)
this.cssRender.domElement.style.position = 'absolute'
this.cssRender.domElement.style.top = '0'
this.cssRender.domElement.style.outline = 'none'
document.getElementById('model').appendChild(this.cssRender.domElement)
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setClearColor(new THREE.Color(0x01050f))
//window.devicePixelRatio 当前设备的物理分辨率与css分辨率之比
this.renderer.setPixelRatio(window.devicePixelRatio)
//按层级先后渲染
this.renderer.sortObjects = true
document.getElementById('model').appendChild(this.renderer.domElement)
},
initModel() {
//坐标系
var axes = new THREE.AxesHelper(4000)
this.scene.add(axes)
var planGeometry = new THREE.PlaneGeometry(3000, 2000)
var planeMaterial = new THREE.MeshLambertMaterial({
color: 0xcccccc,
side: THREE.DoubleSide,
// wireframe:true
})
var plane = new THREE.Mesh(planGeometry, planeMaterial)
plane.rotation.x = -Math.PI / 2
plane.position.set(0, 0, 0)
this.scene.add(plane)
var cubeGeometry = new THREE.BoxGeometry(400, 200, 200)
var cubeMaterial = new THREE.MeshLambertMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.8,
})
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
cube.position.set(0, 120, 0)
cube.name = 'cube1'
this.scene.add(cube)
var cubeGeometry2 = new THREE.BoxGeometry(400, 200, 200)
var cubeMaterial2 = new THREE.MeshLambertMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.8,
})
var cube2 = new THREE.Mesh(cubeGeometry2, cubeMaterial2)
cube2.position.set(1000, 120, -500)
cube2.name = 'cube2'
this.scene.add(cube2)
var cubeGeometry3 = new THREE.BoxGeometry(400, 200, 200)
var cubeMaterial3 = new THREE.MeshLambertMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.8,
})
var cube3 = new THREE.Mesh(cubeGeometry3, cubeMaterial3)
cube3.position.set(-1000, 100, 500)
cube3.name = 'cube3'
this.scene.add(cube3)
// 生成管道
this.generatePipe(1)
this.generatePipe(2)
// 生成数据显示面板
this.generateDataPanel(1, 0, 400, 0)
this.generateDataPanel(2, 1000, 400, -500)
this.generateDataPanel(3, -1000, 400, 500)
this.generateDataPanel(4, 100, 0, 120)
this.generateDataPanel(5, 1000, 0, -400)
this.generateDataPanel(6, -1000, 100, 700)
// 出现数据显示面板
for (var i = 1; i <= 3; i++) {
$('#canvas' + i).css('display', 'block')
}
$('#btns').css('display', 'block')
},
//高亮显示模型(呼吸灯)
outlineObj(selectedObjects) {
// 创建一个EffectComposer(效果组合器)对象,然后在该对象上添加后期处理通道。
this.composer = new EffectComposer(this.renderer)
// 新建一个场景通道 为了覆盖到原理来的场景上
this.renderPass = new RenderPass(this.scene, this.camera)
this.composer.addPass(this.renderPass)
// 物体边缘发光通道
this.outlinePass = new OutlinePass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
this.scene,
this.camera,
selectedObjects
)
this.outlinePass.edgeStrength = 5.0 // 高光边缘边框的亮度
this.outlinePass.edgeGlow = 1 // 光晕[0,1] 边缘微光强度
this.outlinePass.usePatternTexture = false // 是否使用父级的材质,纹理覆盖
this.outlinePass.edgeThickness = 1 // 边框宽度,高光厚度
this.outlinePass.downSampleRatio = 1 // 边框弯曲度
this.outlinePass.pulsePeriod = 3 // 呼吸闪烁的速度,数值越大,律动越慢
this.outlinePass.visibleEdgeColor.set(parseInt(0xff800)) // 呼吸显示的颜色
this.outlinePass.hiddenEdgeColor = new THREE.Color(0, 0, 0) // 呼吸消失的颜色
// this.outlinePass.clear = true
this.composer.addPass(this.outlinePass) // 加入高光特效
this.outlinePass.selectedObjects = selectedObjects // 需要高光的模型
},
// 鼠标点击模型
onMouseClick(event) {
//通过鼠标点击的位置计算出raycaster所需要的点的位置,以屏幕中心为原点,值的范围为-1到1
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
this.raycaster.setFromCamera(this.mouse, this.camera)
// 获取raycaster直线和所有模型相交的数组集合
var intersects = this.raycaster.intersectObjects(this.scene.children)
if (intersects[0].object.geometry instanceof THREE.PlaneGeometry) {
return
} else {
this.selectedObjects = []
this.selectedObjects.push(intersects[0].object)
this.outlineObj(this.selectedObjects)
if (intersects[0].object.name === 'cube1') {
this.initTween(0, 850, 1000)
$('#canvas4').css('display', 'block')
} else if (intersects[0].object.name === 'cube2') {
this.initTween(1500, 500, -1000)
$('#canvas5').css('display', 'block')
} else if (intersects[0].object.name === 'cube3') {
this.initTween(-1400, 400, 1000)
$('#canvas6').css('display', 'block')
}
}
},
// 相机移动动画
initTween(targetX, targetY, targetZ) {
// 需要保留this
var initPosition = {
x: this.camera.position.x,
y: this.camera.position.y,
z: this.camera.position.z,
}
var tween = new TWEEN.Tween(initPosition)
.to({ x: targetX, y: targetY, z: targetZ }, 2000)
.easing(TWEEN.Easing.Sinusoidal.InOut)
var onUpdate = (pos) => {
var x = pos.x
var y = pos.y
var z = pos.z
this.camera.position.set(x, y, z)
}
tween.onUpdate(onUpdate)
tween.start()
this.controls.target.set(0, 0, 0)
this.cssControls.target.set(0, 0, 0)
},
// 生成数据显示面板
generateDataPanel(id, x, y, z) {
// CSS3DObject实现3D卡片
var dataplane = document.getElementById('canvas' + id)
this.dataPanel['plane' + id] = new CSS3DObject(dataplane)
this.dataPanel['plane' + id].scale.set(1.2, 1.2, 1.2)
this.dataPanel['plane' + id].position.set(x, y, z)
this.cssScene.add(this.dataPanel['plane' + id])
},
// 生成管道
generatePipe(id) {
var curve
var tubeGeometry
var material
if (id === 1) {
curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 200, 0),
new THREE.Vector3(0, 200, -500),
new THREE.Vector3(10, 200, -500),
new THREE.Vector3(20, 200, -500),
new THREE.Vector3(1000, 200, -500),
])
tubeGeometry = new THREE.TubeGeometry(curve, 80, 10)
this.flowingLineTexture1 = new THREE.TextureLoader().load(
'static/arrow.png'
)
this.flowingLineTexture1.wrapS = this.flowingLineTexture1.wrapT =
THREE.RepeatWrapping
this.flowingLineTexture1.repeat.set(10, 1)
this.flowingLineTexture1.needsUpdate = true
material = new THREE.MeshBasicMaterial({
map: this.flowingLineTexture1,
side: THREE.DoubleSide,
transparent: true,
})
} else if (id === 2) {
curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 200, 0),
new THREE.Vector3(0, 200, 500),
new THREE.Vector3(-10, 200, 500),
new THREE.Vector3(-500, 200, 500),
new THREE.Vector3(-800, 200, 500),
])
tubeGeometry = new THREE.TubeGeometry(curve, 80, 10)
this.flowingLineTexture2 = new THREE.TextureLoader().load(
'static/arrow.png'
)
this.flowingLineTexture2.wrapS = this.flowingLineTexture2.wrapT =
THREE.RepeatWrapping
this.flowingLineTexture2.repeat.set(10, 1)
this.flowingLineTexture2.needsUpdate = true
material = new THREE.MeshBasicMaterial({
map: this.flowingLineTexture2,
side: THREE.DoubleSide,
transparent: true,
})
}
let tube = new THREE.Mesh(tubeGeometry, material)
this.scene.add(tube)
},
// 视角改变
toView(id) {
if (id === 1) {
this.initTween(0, 1000, 2900)
} else if (id === 2) {
this.initTween(0, 4000, 0)
}
},
// 设备详情页面跳转
deviceDetail(id) {
console.log(id)
},
onWindowResize() {
this.cssRender.setSize(window.innerWidth, window.innerHeight)
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
},
initControls() {
this.cssControls = new OrbitControls(
this.camera,
this.cssRender.domElement
)
// //动态阻尼系数 即鼠标拖拽旋转的灵敏度
// this.cssControls.dampingFactor = 0.25
// this.cssControls.target.set(0, 900, 0)
// //摄像机距离原点的距离
// this.cssControls.minDistance = 1
// this.cssControls.maxDistance = 20000
// //上下旋转范围
this.cssControls.minPolarAngle = 0
this.cssControls.maxPolarAngle = 1.5
// //左右旋转范围
// this.cssControls.minAzimuthAngle = -Math.PI * 2
// this.cssControls.maxAzimuthAngle = Math.PI * 2
//是否开启右键拖拽
this.cssControls.enabledPan = false
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.dampingFactor = 0.25
// // //缩放倍数
this.controls.zoomSpeed = 1.0
this.controls.minDistance = 1
this.controls.maxDistance = 20000
this.minPolarAngle = 0
this.maxPolarAngle = 1.5
this.minAzimuthAngle = -Math.PI * 2
this.maxAzimuthAngle = Math.PI * 2
this.controls.enabledPan = false
},
animate() {
this.render()
this.controls.update()
this.cssControls.update()
if (this.composer) {
this.composer.render()
}
requestAnimationFrame(this.animate)
},
render() {
// 数据显示面板的旋转角度与相机的旋转角度一致
this.dataPanel.plane1.rotation.copy(this.camera.rotation)
this.dataPanel.plane1.updateMatrix()
this.dataPanel.plane2.rotation.copy(this.camera.rotation)
this.dataPanel.plane2.updateMatrix()
this.dataPanel.plane3.rotation.copy(this.camera.rotation)
this.dataPanel.plane3.updateMatrix()
this.dataPanel.plane4.rotation.copy(this.camera.rotation)
this.dataPanel.plane4.updateMatrix()
this.dataPanel.plane5.rotation.copy(this.camera.rotation)
this.dataPanel.plane5.updateMatrix()
this.dataPanel.plane6.rotation.copy(this.camera.rotation)
this.dataPanel.plane6.updateMatrix()
// 管道流动,更新管道纹理的偏移量
// 管道流动速度
var speed=0.01
this.flowingLineTexture1.offset.x+=speed
this.flowingLineTexture2.offset.x+=speed
// 一定要激活
TWEEN.update()
this.renderer.render(this.scene, this.camera)
this.cssRender.render(this.cssScene, this.camera)
},
draw() {
this.initScene()
this.initCamera()
this.initLight()
this.initRender()
this.initModel()
this.initControls()
this.animate()
window.onresize = this.onWindowResize
window.onclick = this.onMouseClick
},
},
}
</script>
<style scoped lang="scss">
.about {
width: 100%;
height: 100%;
#model {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
#canvas1,
#canvas2,
#canvas3 {
width: 150px;
height: 200px;
// background-color: rgba(8, 29, 54, 0.5);
background-color: aquamarine;
border-radius: 3px;
// opacity: 0.5;
color: red;
font-size: 20px;
}
#canvas4,#canvas5,#canvas6{
span{
display: block;
width: 100px;
height: 40px;
background-color: cornflowerblue;
color: #fff;
position: absolute;
text-align: center;
line-height: 40px;
cursor: pointer;
}
}
#btns {
position: absolute;
bottom: 0px;
width: 100%;
padding: 10px;
text-align: center;
z-index: 9999;
span {
width: 100px;
display: inline-block;
height: 30px;
background-color: red;
margin-right: 20px;
cursor: pointer;
}
}
}
}
</style>