实现流动管道的思路
在到这里的同学应该是对Three.js的基础有了一定的认知,如果对材质不太了解的同学可以去官方网站查阅一下资料。
言归正传,如果想要完成一个可以看见管道内部流水的效果,首先离不开的是构造函数“管道几何体(TubeGeometry)”,它可以根据三维点坐标,连接起来进而生成一个管道体,但是一个管道体想要实现上图中的效果还是不够的,具体思路就是一个管道两个管道体,外层管道体加上透明材质,内层管道体加上水的材质并设置 其材质偏移量即可实现流动效果。
实现流动管道的代码实施
在写代码之前首先认识一下管道几何体(TubeGeometry):
TubeGeometry(path, tubularSegments, radius, radialSegments, closed)
path — Curve - 一个由基类Curve继承而来的路径。
tubularSegments — Integer - 组成这一管道的分段数,默认值为64。
radius — Float - 管道的半径,默认值为1。
radialSegments — Integer - 管道横截面的分段数目,默认值为8。
closed — Boolean 管道的两端是否闭合,默认值为false。
结合上文,在实际开发中也是第一步,根据三维点坐标定义路径:
posList.forEach(p => {
v3List.push(new THREE.Vector3(p.x, p.y, p.z))
});
let curve = new THREE.CatmullRomCurve3(v3List, false);
然后创建一个内部流水管道体,给其添加流水材质:
let tubeGeometry = new THREE.TubeGeometry(curve, 100, 1, 16, false);
let material = new THREE.MeshBasicMaterial({
map: texture,
transparent: false,
// opacity: 0.8,
});
let tube1 = new THREE.Mesh(tubeGeometry, material);
this.scene.add(tube1);
然后创建一个外部流水管道体,给其添加透明材质:
let tubeGeometry2 = new THREE.TubeGeometry(curve, 100, 2, 16, false);
let tubeMaterial2 = new THREE.MeshPhongMaterial({
color: 0xaaaaaa,
transparent: true,
opacity: 0.5,
});
let tube2 = new THREE.Mesh(tubeGeometry2, tubeMaterial2);
this.scene.add(tube2);
需要注意的是,俩个管道体用是同一个的路径,只不过是管道半径不同而已,管道生成以后,控制内部管道材质偏移即可:
this.textureList.forEach(t=>{
t.offset.x -= 0.05
});
下面附完整代码:
<template>
<div class="factory-view">
<div class="back-img">
<img src="images/factory/back.png">
</div>
<div class="three-view" id="myThree"></div>
<div class="title-view">
<div class="title-left">
<div class="back-btn" @click="goBack">返回</div>
</div>
<div class="title-center">
<img src="images/factory/title.png"/>
<div class="text">车间生产实时数据</div>
</div>
<div class="title-right">
<span>{{nowDate | formatDate('yyyy-MM-dd hh:mm')}}</span>
</div>
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import {OBJLoader} from 'three/examples/jsm/loaders/OBJLoader'
import {MTLLoader} from 'three/examples/jsm/loaders/MTLLoader'
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'
export default {
data() {
return {
nowDate: new Date().getTime(),
container: null,
camera: null,
scene: null,
renderer: null,
controls: null,
//用于点击事件
raycaster: new THREE.Raycaster(),
mouse: new THREE.Vector2(),
//设备列表
equipmentList: [
{
name: 'equipment1',
mtl: './objs/factory/waterTank.mtl',
obj: './objs/factory/waterTank.obj',
position: {
x: -100,
y: 50,
z: 0
}
},
{
name: 'equipment2',
mtl: './objs/factory/waterTank.mtl',
obj: './objs/factory/waterTank.obj',
position: {
x: 70,
y: 50,
z: 0
}
},
{
name: 'equipment3',
mtl: './objs/factory/motor.mtl',
obj: './objs/factory/motor.obj',
position: {
x: 0,
y: -50,
z: -5
}
}
],
//管道列表
pipelinePosList: [
{
pipelineName: 'pipeline1',
pipelinePng: 'water1.jpg',
posList: [
{
x: -50,
y: 50,
z: 40
}, {
x: -5,
y: 50,
z: 40
}, {
x: -5,
y: -50,
z: 40
}, {
x: -5,
y: -50,
z: 30
}
]
},
{
pipelineName: 'pipeline2',
pipelinePng: 'water2.jpg',
posList: [
{
x: 50,
y: 50,
z: 60
}, {
x: 10,
y: 50,
z: 60
}, {
x: 10,
y: -50,
z: 60
}, {
x: 10,
y: -50,
z: 30
}
]
},
{
pipelineName: 'pipeline3',
pipelinePng: 'water3.jpg',
posList: [
{
x: 25,
y: -50,
z: 21
},
{
x: 170,
y: -50,
z: 21
}
]
}
],
//管道材质,用于流动使用
textureList: []
}
},
mounted() {
this.getNowDate();
this.initAll();
},
methods: {
/**
* 获取当前时间
*/
getNowDate() {
setInterval(() => {
this.nowDate = new Date().getTime()
}, 60000)
},
initAll() {
this.initScene();
this.initCamera();
this.initRender();
this.initLight();
this.initContent();
window.addEventListener('resize', this.onWindowResize, false);
this.container.addEventListener('click', this.onMouseClick, false);
this.initControls();
this.animate()
},
/**
* 注册场景
*/
initScene() {
this.container = document.getElementById('myThree');
this.scene = new THREE.Scene();
},
/**
* 注册摄像机
*/
initCamera() {
let camera = new THREE.PerspectiveCamera(60, this.container.clientWidth / this.container.clientHeight, 1, 10000);
camera.position.set(0, -200, 150);
camera.lookAt(new THREE.Vector3(0, 0, 0));
this.camera = camera;
},
/**
* 注册渲染器
*/
initRender() {
let renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setSize(this.container.clientWidth, this.container.clientHeight);
renderer.setClearAlpha(0.5);
this.container.appendChild(renderer.domElement);
this.renderer = renderer;
},
/**
* 注册灯光
*/
initLight() {
this.scene.add(new THREE.AmbientLight(0x0c0c0c));
let spotLight1 = new THREE.SpotLight(0xdadada);
spotLight1.position.set(-100, -300, 300);
this.scene.add(spotLight1);
},
/**
* 注册控制模块
*/
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
// 使动画循环使用时阻尼或自转 意思是否有惯性
this.controls.enableDamping = false;
//是否可以缩放
this.controls.enableZoom = true;
//是否自动旋转
this.controls.autoRotate = false;
//设置相机距离原点的最远距离
this.controls.minDistance = 200;
//设置相机距离原点的最远距离
this.controls.maxDistance = 600;
//是否开启右键拖拽
this.controls.enablePan = true;
},
/**
* 加载实际模型
*/
initContent() {
//创建一个平面(地面)
let geometry = new THREE.PlaneGeometry(400, 200);
let material = new THREE.MeshBasicMaterial({color: 0xaaaaaa});
let rect = new THREE.Mesh(geometry, material);
this.scene.add(rect);
//添加工厂模型
this.loadEquipment(this.equipmentList);
//添加流动管道
this.loadQipeLine(this.pipelinePosList);
//添加一个参考坐标轴
//this.scene.add(new THREE.AxesHelper(300));
},
/**
* 添加交互模型
*/
loadEquipment(list) {
list.forEach(i => {
let mtlLoader = new MTLLoader();
mtlLoader.load(i.mtl, (materials) => {
//obj的模型会和MaterialCreator包含的材质对应起来
let objLoader = new OBJLoader();
objLoader.setMaterials(materials);
objLoader.load(i.obj, (obj) => {
obj.name = i.name;
obj.traverse(function (child) {
if (child instanceof THREE.Mesh) {
child.name = i.name;
}
});
obj.scale.set(1.2, 1.2, 1.2); //放大obj组对象
obj.position.set(i.position.x, i.position.y, i.position.z);
//绕x轴旋转π/4
// obj.rotateZ(Math.PI/2);
this.scene.add(obj);
this.render();
})
});
});
},
loadQipeLine(list) {
list.forEach(i => {
let v3List = [];
i.posList.forEach(p => {
v3List.push(new THREE.Vector3(p.x, p.y, p.z))
});
//1、添加管道路径
let curve = new THREE.CatmullRomCurve3(v3List, false);
//2、创建一个流水模型
let tubeGeometry = new THREE.TubeGeometry(curve, 100, 1, 16, false);
let tLoader = new THREE.TextureLoader();
tLoader.load(`./objs/factory/${i.pipelinePng}`,
// onLoad回调
(texture) => {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.x = 20;
texture.name = i.pipelineName;
this.textureList.push(texture);
let material = new THREE.MeshBasicMaterial({
map: texture,
transparent: false,
// opacity: 0.8,
});
let tube1 = new THREE.Mesh(tubeGeometry, material);
this.scene.add(tube1);
//3、创建一个半透明管道
let tubeGeometry2 = new THREE.TubeGeometry(curve, 100, 2, 16, false);
let tubeMaterial2 = new THREE.MeshPhongMaterial({
color: 0xaaaaaa,
transparent: true,
opacity: 0.5,
});
let tube2 = new THREE.Mesh(tubeGeometry2, tubeMaterial2);
this.scene.add(tube2);
this.render();
});
});
},
/**
* 配合controls使用
*/
animate() {
if (this.controls) {
this.controls.update();
}
this.render();
requestAnimationFrame(this.animate);
this.textureList.forEach(t=>{
t.offset.x -= 0.05
});
},
render() {
this.renderer.render(this.scene, this.camera);
},
/**
* 监听窗口变化
*/
onWindowResize() {
this.camera.aspect = this.container.clientWidth / this.container.clientHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
},
/**
* 添加点击监听一系列方法
*/
onMouseClick(event) {
// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
this.mouse.x = (event.clientX / this.container.clientWidth) * 2 - 1;
this.mouse.y = -(event.clientY / this.container.clientHeight) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
// 计算物体和射线的焦点
let intersects = this.raycaster.intersectObjects(this.scene.children, true);
// 获取选中最近的 Mesh 对象
if (intersects.length !== 0 && intersects[0].object instanceof THREE.Mesh) {
// let selectName = intersects[0].object.name;
}
},
goBack() {
history.back()
},
}
}
</script>
<style scoped>
.factory-view {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #000000;
position: relative;
}
.back-img {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
}
.three-view {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 10;
}
.title-view {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 20;
display: flex;
flex-wrap: wrap;
color: #eeeeee;
}
.title-view .title-left {
width: 25%;
}
.title-view .title-left .back-btn {
width: 80px;
border-radius: 0 0 5px 0;
text-align: center;
background: #2E3755;
font-size: 18px;
font-weight: bold;
letter-spacing: 2px;
color: #efefef;
padding: 10px;
}
.title-view .title-left .back-btn:hover {
cursor: pointer;
color: #2E3755;
background: #70b4de;
}
.title-view .title-center {
width: 50%;
text-align: center;
position: relative;
}
.title-view .title-center img {
width: auto;
height: 60px;
}
.title-view .title-center .text {
position: absolute;
top: 0;
width: 100%;
padding-top: 10px;
font-size: 24px;
font-weight: bold;
letter-spacing: 2px;
}
.title-view .title-right {
width: 25%;
padding: 15px;
font-size: 20px;
font-weight: bold;
letter-spacing: 1px;
text-align: right;
}
</style>
{{ cmt.username }}
{{ cmt.content }}
{{ cmt.commentDate | formatDate('YYYY.MM.DD hh:mm') }}