之前有做过关于threejs开发的一些小功能项,最近正好在做一个仓库相关的3D场景,这里贴出核心代码和大家分享下。
首先要做3D场景,除了基础的组件要引入生成外,要先加入地板,在地板上添加一些3D模型达到各种各样的效果,所以首先插入地板,这里用的是vue框架,所以和原始的html使用方法上有一些区别,地板我这里也是创建了一个立体,只不过长和宽很大,高度只有1,所以看起来像一个地板,其次就是给地板添加一些纹理贴图,使地板看起来更真实一些,然后设置地板位置,最终加入到场景scene中,为了方便扩展性更好,这里把地面的宽高厚度设置为变量,方便后续修改
initFloor(){
let floorGeometry = new THREE.BoxGeometry(
this.floor.floorLength,this.floor.floorWidth,this.floor.floorDepth);
let texture = new THREE.TextureLoader().load( '/static/images/floor.jpg' )
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
texture.repeat.set(5,5)
let cubeMaterial = new THREE.MeshLambertMaterial( {
map:texture
} );
let floor = new THREE.Mesh( floorGeometry, cubeMaterial );
floor.name = '地板';
floor.position.set(this.floor.floorLength/2,this.floor.floorWidth/2,0)
scene.add(floor)
},
效果如下:
添加完地面后,开始添加墙面,没有墙面的话会显得物体比较突兀,而且场地没有边界感,如果是模拟的墙可以直接添加四面墙,不用考虑窗户和门,这里从简开发,就只添加四面墙,先设置好每面墙的高度和宽度,同样这里将墙体的相关参数设置为变量,方便后期修改。
//创建墙面
createCubeWall() {
let materialTie = [];
materialTie.push(new THREE.MeshPhongMaterial({color: 0xafc0ca})); //前 0xafc0ca :灰色
materialTie.push(new THREE.MeshPhongMaterial({color: 0x9cb2d1})); //后 0x9cb2d1:淡紫
materialTie.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec})); //上 0xd6e4ec: 偏白色
materialTie.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec})); //下
materialTie.push(new THREE.MeshPhongMaterial({color: 0xafc0ca})); //左 0xafc0ca :灰色
materialTie.push(new THREE.MeshPhongMaterial({color: 0xafc0ca})); //右
let wallList = []
let wall1 = {width:this.floor.floorLength, height:3, depth:20, angle:0, matArrayB:materialTie, x:this.floor.floorLength/2, y:0, z:10, name:"墙面"};
let wall2 = {width:this.floor.floorLength, height:3, depth:20, angle:1, matArrayB:materialTie, x:this.floor.floorLength/2, y:this.floor.floorWidth, z:10, name:"墙面"};
let wall3 = {width:this.floor.floorWidth, height:3, depth:20, angle:1.5, matArrayB:materialTie, x:0, y:(this.floor.floorWidth/2), z:10, name:"墙面"};
let wall4 = {width:this.floor.floorWidth, height:3, depth:20, angle:1.5, matArrayB:materialTie, x:this.floor.floorLength, y:(this.floor.floorWidth/2), z:10, name:"墙面"};
wallList.push(wall1);wallList.push(wall2);wallList.push(wall3);wallList.push(wall4);
for(let i=0;i<wallList.length;i++){
let cubeGeometry = new THREE.BoxGeometry(wallList[i].width, wallList[i].height, wallList[i].depth);
let cube = new THREE.Mesh(cubeGeometry, wallList[i].matArrayB);
cube.position.x = wallList[i].x;
cube.position.y = wallList[i].y;
cube.position.z = wallList[i].z;
cube.rotation.z += wallList[i].angle * Math.PI; //-逆时针旋转,+顺时针
cube.name = wallList[i].name;
scene.add(cube);
}
},
效果如下:
再接着就是添加一些货架,货架不推荐使用建模的方式开发,因为后续一但货架的尺寸发生了改变,那么就需要对模型重新改动再引入进来,太麻烦了,尽量使用threejs提供的建模方式进行绘制,因为需求的关系这里需要两两货架并拢,所以代码会显得有些乱,货架包括了四条腿,层板,五个模型,这里对五个模型的相关参数都设置为变量的方式,方便后面快速调整货架的模型,仓库也不会只有一个货架,这里用了循环,实现12排货架,每排两组货架,每组三个货架,
//初始化货架
initShelf(){
for(let n=0;n<2;n++) {
let offsetX = 60;
for (let i = 0; i < 12; i++) {
if(i%2 === 1){
offsetX = offsetX + 45;
}else{
offsetX = offsetX + 25;
this.addPaTrack(offsetX, this.indistanceY+n*180, 0,true,"car_"+n+"_"+i)
}
for (let j = 0; j < 3; j++) {
let shelfName = '货架' + j+"上"
this.shelfList.push({
shelfName: shelfName,
planeWidth: this.plane.planeWidth,
planeHeight: this.plane.planeHeight,
planeLength: this.plane.planeLength,
holderLength: this.holder.holderLength,
holderHeight: this.holder.holderHeight,
holderWidth: this.holder.holderWidth,
positionX: offsetX,
positionY: this.indistanceY + (j * 60)+n*180,
positionZ: this.holder.holderHeight + 2,
layerNum: this.layerNum,
columnNum: this.columnNum
});
}
}
}
for(let i = 0;i < this.shelfList.length; i++){
for(let j = 0; j < this.shelfList[i].layerNum; j++){
this.addShelf(
this.shelfList[i].positionX,
this.shelfList[i].positionY,
this.shelfList[i].positionZ*(j+1),
this.shelfList[i].planeWidth,
this.shelfList[i].planeLength,
this.shelfList[i].planeHeight,
this.shelfList[i].holderLength,
this.shelfList[i].holderWidth,
this.shelfList[i].holderHeight,
scene,
this.shelfList[i].shelfName+"$"+j,
this.shelfList[i].columnNum);
}
}
},
addShelf(x,y,z,plane_x,plane_y,plane_z,holder_x,holder_y,holder_z,scene,name,num){
let RackMat2 = new THREE.MeshPhongMaterial({color:0xFFFFFF});
let plane = new THREE.BoxGeometry( plane_x, plane_y/num,plane_z, );
let gz = [];
for(let i = 0; i < num; i++){
gz.push( y + plane_y/num/2 + (plane_y/num)*i );
let obj = new THREE.Mesh( plane, RackMat2 );
obj.position.set(x, gz[i], z) ;
scene.add(obj);
}
let holder = new THREE.BoxGeometry( holder_x, holder_y, holder_z );
let obj2 = new THREE.Mesh( holder, RackMat2, 0 );
let obj3 = new THREE.Mesh( holder, RackMat2, 0 );
let obj4 = new THREE.Mesh( holder, RackMat2, 0 );
let obj5 = new THREE.Mesh( holder, RackMat2, 0 );
obj2.position.set(x-plane_x/2+holder_x/2,y+holder_y/2,z-holder_z/2-plane_z/2,);
obj3.position.set(x+plane_x/2-holder_x/2,y+holder_y/2, z-holder_z/2-plane_z/2, );
obj4.position.set(x-plane_x/2+holder_x/2,y+plane_y-holder_y/2,z-holder_z/2-plane_z/2 );
obj5.position.set(x+plane_x/2-holder_x/2,y+plane_y-holder_y/2, z-holder_z/2-plane_z/2 );
scene.add(obj2);scene.add(obj3);scene.add(obj4);scene.add(obj5);
},
效果图如下:
有了货架还需要在货架上防止货物,货物可以根据货架的位置动态计算货物放置的位置,先做一个专门添加货物的功能,在做一个初始化货架的方法,在初始化货架中根据需求添加货物,再循环调用生成货物的方法添加多个货物。
//初始化货架
initCube(){
for(let q=0; q<this.shelfList.length;q++){
for(let i=0;i<this.layerNum;i++){
for(let j=0;j<this.columnNum;j++){
let shorageName = this.shelfList[q].shelfName+"_"+i+"层_"+j+"列"
let x = this.shelfList[q].positionX;
let y = this.shelfList[q].positionY + (this.box.boxDepth) + j*(this.plane.planeLength/3)
let z = this.shelfList[q].positionZ + (this.box.boxDepth/2) + i*(this.holder.holderHeight+this.plane.planeHeight)
this.addCube(x-6,y,z,"货物在"+"_"+shorageName+"_1")
this.addCube(x+5,y,z,"货物在"+"_"+shorageName+"_2")
}
}
}
},
//新增货架
addCube(x,y,z,name){
const loader = new GLTFLoader()
loader.load("/static/model/box.glb", (gltf) => {
gltf.scene.position.set(x, y, z+1) // 模型位置
gltf.scene.rotation.x = Math.PI / 2 // x轴旋转
gltf.scene.scale.set(0.6, 0.4, 0.5) // 模型位置
gltf.scene.name = name
scene.add(gltf.scene)
})
},
效果如下:
这边是添加的glb格式的货物模型,是一个蓝色的料箱,颜色比较深且没加贴图显得有点失真,大家可以根据需要添加贴图或者换成适合的货物模型,也可以自己做立方体添加贴图来代替。到这里一个厂区的模型大概成型了,但是一般还会添加AGV车,或者叉车模型,这里再做个AGV的模型,在添加AGV小车前先添加地上的二维码用于给下小车导航,最终是要对接到真实的二维码,这里就大概绘制一个二维码的区域,依然是和地板类似的方式添加一个个小方块,上面使用二维码的贴图,并循环咋区域内绘制出来。
initQRCode() {
for (let i = 0; i < 18; i++) {
for (let j = 0; j < 3; j++) {
this.addQRCode(50 + (i * 30), 40 + (j * 30), 0, 'qrcode')
}
}
},
addQRCode(x,y,z,name){
let qrCodeMat = new THREE.MeshLambertMaterial();
new THREE.TextureLoader().load( "/static/images/qrcode.png", function( map ) {
qrCodeMat.map = map;
qrCodeMat.needsUpdate = true;
} );
let geometry = new THREE.BoxGeometry( 5, 5, 1 );
let qrCode = new THREE.Mesh(geometry, qrCodeMat);
qrCode.position.set(x,y,1)
qrCode.name = name
scene.add(qrCode);
},
效果如下:
因为距离远会有些失真,不过二维码能起到表达这个意思就好了,添加完二维码后就是添加AGV车了,这里可以把AGV车放在第一个二维码的位置,最终AGV还是要对接到真实的AGV为止的,可以通过MQTT或者webscoket来实时获取AGV的位置来动态改变AGV模型在场景中的位置,下面添加AGV小车的模型:
initAGV() {
this.addAGV(50,40,2,true)
},
addAGV(x,y,z,box){
const loader = new GLTFLoader()
loader.load("/static/model/agv.gltf", (gltf) => {
gltf.scene.position.set(x,y,z) // 模型位置
gltf.scene.rotation.x = Math.PI / 2 // x轴旋转
gltf.scene.rotation.y = Math.PI / 2 // z轴旋转
gltf.scene.scale.set(0.1, 0.1 , 0.1) // 模型位置
gltf.scene.name ='1'
if(box){
loader.load("/static/model/box.glb", (gltf) => {
gltf.scene.position.set(x, y, z+10) // 模型位置
gltf.scene.rotation.x = Math.PI / 2 // x轴旋转
gltf.scene.scale.set(0.6, 0.4, 0.5) // 模型位置
gltf.scene.name = name
scene.add(gltf.scene)
})
}
this.agvList.push(gltf.scene);
scene.add(gltf.scene) // 加入场景
})
},
效果如下:
为了真实性我还给小车上放了一个货物的托盘,至此,一个简易的工厂模型就搭建好了,不过如果货物太多的话可能会导致浏览器卡顿,因此在开始绘制的初期就要考虑这个问题,如果是建模的方式来制作模型可以考虑做成低模的方式,在使用的时候再对模型文件进行压缩,如果是ThreeJs代码的方式,可以考虑做一些代码上的优化。
最终为了提高效果,我们可以再添加一个动画效果,飞入的方式展现,原理上就是控制相机做一个飞入的动作来实现好看的动画效果
效果如下:
仓库的3D效果
好了这里一个简单的工厂模型就好了,可以根据具体需求定制化开发一些其他功能,实现更好的效果。我只是刚开始做ThreeJs,这里做个记录,勿喷