blender+Three.js 三维数据可视化:数字孪生
原来叫做系统仿真现在都叫做数字孪生了,我也凑个热闹,这个是一个仓库提货的场景仿真。延续上一个项目《blender+Three.js 三维数据可视化》2和3,有兴趣的可以去看。
环境:blender-2.93.6-windows-x64+Three.js 0.91.0+Echart2.0
这里是上一个场景的仓库部分,出货分为发货(直接从厂里发到客户,走内部流程)和自提(客户自己来取货),这里是自提部分,主要是这个自动装货机是我们项目的,所以借此机会客户加了一个这个需求。
废话说完,我们开始,首先分析需求和流程:
1、客户每天需要装车的订单是一个全集,这个全集会变化,我们5分钟更新一次;
2、拿到装货单排队等待的是一个子集,他们会入场等待;
3、有4个装卸口,4个自动装货机,每次装卸操作会记录装货的条码,可以通过条码得到品类、重量什么的信息;
需要模拟场景,拿到装货单的订单入场等待(模拟成一辆汽车,实际可能不止),在装卸口排队的或者装车的,模拟成装卸(车停在装卸口并且机械臂运动),装卸完成的,走人。
用blender创建仓库模型,用THREE.js加载进来。
function init() {
var container = document.getElementById("three");
camera = new THREE.PerspectiveCamera( 45, sceneWidth / sceneHeight, 1, 4000 );
//camera.position.set(900, 400, 900);
camera.position.set(1000, 500, 0);
scene = new THREE.Scene();
scene.background = new THREE.Color('#27313A');
scene.fog = new THREE.Fog('#27313A', 1500, 2500 );
var ambientLight=new THREE.AmbientLight('#FFFFFF');
scene.add(ambientLight);
var hemiLight = new THREE.HemisphereLight(0xFFFFFF,0x444444);
hemiLight.position.set(100, 200, 100);
scene.add(hemiLight);
var dirLight = new THREE.DirectionalLight('#666666');
dirLight.position.set( 200, 500, 200);
dirLight.intensity=1;
dirLight.castShadow = true;
dirLight.shadow.camera.top = 1000;
dirLight.shadow.camera.bottom = - 1000;
dirLight.shadow.camera.left = - 1000;
dirLight.shadow.camera.right = 1000;
dirLight.shadow.camera.far=2000;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
scene.add( dirLight );
//scene.add(new THREE.CameraHelper( dirLight.shadow.camera ) );
//地板
var mesh = new THREE.Mesh( new THREE.PlaneGeometry( 8000, 8000 ), new THREE.MeshBasicMaterial( {color: '#394855', depthWrite:false} ) );
mesh.rotation.set(- Math.PI / 2,0,0);
mesh.receiveShadow = true;
scene.add( mesh );
var grid = new THREE.GridHelper(8000, 200, '#2F3C46','#425462');
grid.material.opacity = 0.5;
grid.material.transparent = true;
scene.add(grid);
for (var val in whData) {
whData[val].percent=Math.round(Math.random()*10000)/100;
whData[val].goods=goodsArr[Math.round(Math.random()*100)%3];
}
var material = new THREE.MeshPhongMaterial( {color:'#2F3C46'});
var materialGround=new THREE.MeshStandardMaterial({color:'#2F3C46'});
var materialMask = new THREE.MeshBasicMaterial({color: '#0099FF',opacity:0.1,transparent:true});
var materialTruck = new THREE.MeshPhongMaterial({color: '#55711C'});
var materialWorkArea = new THREE.MeshPhongMaterial({color: '#663300',opacity:0.5,transparent:true});
var materialPark = new THREE.MeshPhongMaterial({color: '#2F3C46'});
var materialWall = new THREE.MeshPhongMaterial({color: '#2F3C46',opacity:0.2,transparent:true,wireframe:false});
var materialWallLine = new THREE.LineBasicMaterial({color: '#0099FF',opacity:0.5,transparent:true}) //仓库线框材质
var materialEmpty=new THREE.MeshPhongMaterial({color:'#2F3C46'}); //仓库空位
var materialCo = new THREE.MeshPhongMaterial({color:'#0099FF'}); //尿素
var materialPo4 = new THREE.MeshPhongMaterial({color:'#99FF00'}); //磷肥
var materialNh4 = new THREE.MeshPhongMaterial({color:'#FF0099'}); //二胺
var infoMaterial=new THREE.LineBasicMaterial({color:'#0099FF',opacity:0.3,transparent:true});
var alphaMap = new THREE.TextureLoader().load('images/alpha.png');
var loader = new THREE.FBXLoader();
loader.load('/object/warehouse.fbx', function ( object ) {
object.traverse( function ( child ) {
if ( child.isMesh ) {
if(child.name=="ground") //地板
{
child.material=materialGround;
child.castShadow = true;
child.receiveShadow = true;
}
else if(child.name.indexOf("newOrder")==0)
{
child.material =materialTruck;
child.castShadow = true;
child.receiveShadow = true;
newOrderTruck=child;
}
else if(child.name.indexOf("park")==0)
{
var parkObject=new THREE.Group();
parkObject.position.set(child.position.x,child.position.y,child.position.z);
var edges = new THREE.EdgesGeometry(child.geometry,1);
var lines = new THREE.LineSegments(edges,materialWallLine);
edges.scale(child.scale.x,child.scale.y,child.scale.z*15);
lines.rotation.set(- Math.PI / 2,0,0);
lines.position.set(0,65,0);
parkObject.add(lines);
child.material =materialPark;
child.castShadow = true;
child.receiveShadow = true;
child.position.set(0,0,0);
child.parent=null;
parkObject.add(child);
parks.push({id:Number(child.name.replace("park","")),object:parkObject,used:false});
scene.add(parkObject);
}
else if(child.name.indexOf("point")==0)
{
child.material =new THREE.MeshPhongMaterial( {color:'#666666'});
points[child.name]=child;
}
else if(child.name.indexOf("LotGround")==0) //停车场,用于显示订单信息
{
child.material = materialPark;
child.material.opacity=0.2;
child.material.transparent=true;
child.castShadow = true;
child.receiveShadow = true;
}
else if(child.name.indexOf("Store")==0) //主仓库
{
//加一个占位的线框
var edges = new THREE.EdgesGeometry(child.geometry,1);
var lines = new THREE.LineSegments(edges,materialWallLine);
edges.scale(child.scale.x,child.scale.y,child.scale.z);
lines.rotation.set(- Math.PI / 2,0,0);
lines.position.set(child.position.x,child.position.y,child.position.z);
scene.add(lines);
child.material =materialWall;
child.castShadow = true;
child.receiveShadow = true;
}
else if(child.name.indexOf("W")==0)
{
//加一个占位的线框
var edges = new THREE.EdgesGeometry(child.geometry,1);
var lines = new THREE.LineSegments(edges,materialWallLine);
edges.scale(child.scale.x,child.scale.y,child.scale.z);
lines.rotation.set(- Math.PI / 2,0,0);
lines.position.set(child.position.x,child.position.y,child.position.z);
scene.add(lines);
child.material =materialWall;
child.castShadow = true;
child.receiveShadow = true;
}
else if(child.name.indexOf("workArea")==0)
{
child.material = materialWorkArea;
workAreaArr.push(child);
child.castShadow = true;
child.receiveShadow = true;
}
else if(child.name.indexOf("HAND")==0)
{
child.material =material;
child.position.x=-Math.random()*40-180;
handArr.push({object:child,isBusy:false,animate:false,step:0.3});
}
else if(child.name.indexOf("Shelf")==0) //货架
{
//加一个占位的线框
var edges = new THREE.EdgesGeometry(child.geometry,1);
var edgesMaterial = new THREE.LineBasicMaterial({color: 0xffffff})
var lines = new THREE.LineSegments(edges,edgesMaterial);
edges.scale(child.scale.x+2,child.scale.y+2,child.scale.z+2);
lines.position.set(child.position.x-1,child.position.y+10,child.position.z-12);
scene.add(lines);
//处理容积
var childRealName=child.name.replace("Model",""); //IE11下莫名其妙的多了一个这个
if(whData[childRealName]!=undefined && whData[childRealName]!=null)
{
var goods=whData[childRealName].goods;
if(goods!=null && goods!="")
{
if(goods.indexOf("CO")==0) //尿素
{
child.material=materialCo;
lines.material.color.set(materialCo.color);
}
else if(goods.indexOf("PO4")==0) //磷肥
{
child.material=materialPo4;
lines.material.color.set(materialPo4.color);
}
else if(goods.indexOf("NH4")==0) //二胺
{
child.material=materialNh4;
lines.material.color.set(materialNh4.color);
}
}
else
{
child.material = materialEmpty;
child.scale.set(child.scale.x,child.scale.y,2);
}
//按比例高度
if(whData[child.name].percent!=null && whData[child.name].percent!="")
{
child.scale.set(child.scale.x,child.scale.y,whData[child.name].percent);
}
else
{
child.material = materialEmpty;
child.scale.set(child.scale.x,child.scale.y,2);
}
}
else
{
child.material = materialEmpty;
child.scale.set(child.scale.x,child.scale.y,2);
}
}
else
{
child.material = material;
child.castShadow = true;
child.receiveShadow = true;
}
}
} );
scene.add( object );
//匹配一下机械手和装卸区
for(var i=0;i<handArr.length;i++)
{
var handID=Number(handArr[i].object.name.replace("HAND",""));
for(var j=0;j<workAreaArr.length;j++)
{
var workAreaID=Number(workAreaArr[j].name.replace("workArea",""));
if(workAreaID==handID)
{
handArr[i].workArea=workAreaArr[j];
break;
}
}
}
//初始化系统数据
initSystemData();
//开始定时任务,刷新场景
//interAnimate=setInterval("animate()",1000/30);
//开始定时任务,刷新数据
interRefreshData=setInterval("refreshData()",5000);
} );
renderer = new THREE.WebGLRenderer( { antialias: true,alpha:true } );
renderer.setSize(sceneWidth, sceneHeight);
renderer.autoClear=true;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target.set( 0, 0, 0 );
controls.update();
window.addEventListener( 'resize', onWindowResize );
//animate_back();
}
主要是把停车位、机械臂和装卸口用数组保存起来,数组定义如下:
var orderObjects=[]; //订单信息
var parks=[]; //车位信息
var currentParkOrder=null; //当前进行停车场动画的订单
var handArr=[]; //机械臂
var workAreaArr=[]; //装卸口信息
var currentLoadOrder=null; //当前进行装卸口停车的订单
var currentOverOrders=[]; //出库的订单列表
然后是处理停车、进入装卸口和离场的动画部分:
/取得一个新的订单对象
function getOrderObject(id)
{
var orderObject={};
var object=new THREE.Group();
var truck=newOrderTruck.clone();
truck.position.set(0,0,0);
orderObject.truck=truck;
object.add(truck);
var tip=getTip(id);
tip.position.set(0,30,0);
object.add(tip);
orderObject.object=object;
orderObject.id=id;
orderObject.state="wait";
return orderObject;
}
//************************************
//************************************
// 物体运动动画处理过程
//************************************
//************************************
function animateOrder()
{
//----------------------处理停车场动画--------------------------
if(currentParkOrder!=null)
{
if(currentParkOrder.progress<1)
{
currentParkOrder.progress += 0.008;
var point = currentParkOrder.path.getPoint(currentParkOrder.progress);
if(currentParkOrder.object.position.z>point.z)
{
if(currentParkOrder.object.rotation.y>- Math.PI / 2)
{
currentParkOrder.object.rotation.set(0,currentParkOrder.object.rotation.y-0.08,0);
}
}
else
{
if(currentParkOrder.object.rotation.y<0)
{
currentParkOrder.object.rotation.set(0,currentParkOrder.object.rotation.y+0.08,0);
}
}
currentParkOrder.object.position.set(point.x,point.y,point.z);
}
else
{
currentParkOrder.pointIndex++;
if(currentParkOrder.pointIndex>=currentParkOrder.points.length)
{
currentParkOrder.state="parked";
currentParkOrder=null;
}
else
{
var curvePath=[];
var point=currentParkOrder.points[currentParkOrder.pointIndex-1];
curvePath.push(new THREE.Vector3(point.x,point.y,point.z));
point=currentParkOrder.points[currentParkOrder.pointIndex];
curvePath.push(new THREE.Vector3(point.x,point.y,point.z));
var curve = new THREE.CatmullRomCurve3(curvePath);
currentParkOrder.path=curve;
currentParkOrder.progress=0;
}
}
}
//如果停车场不忙的话
else
{
//处理新的动画
for(var i=0;i<parks.length;i++)
{
//如果停车场有空位
if(!parks[i].used)
{
//看看是否有等候的车辆,有的话就初始化一下它
for(var j=0;j<orderObjects.length;j++)
{
if(orderObjects[j].state=="wait")
{
parks[i].used=true;
orderObjects[j].state="parking";
scene.add(orderObjects[j].object);
initOrderParkAnimate(orderObjects[j],parks[i]);
i=parks.length+1; //强制退出循环
break;
}
}
}
}
}
//----------------------停车场动画处理完毕--------------------------
//----------------------处理装卸场动画--------------------------
if(currentLoadOrder!=null)
{
if(currentLoadOrder.progress<1)
{
currentLoadOrder.progress += 0.008;
var point = currentLoadOrder.path.getPoint(currentLoadOrder.progress);
if(currentLoadOrder.pointIndex==2 && currentLoadOrder.object.rotation.y>- Math.PI / 2)
{
currentLoadOrder.object.rotation.set(0,currentLoadOrder.object.rotation.y-0.02,0);
}
else if(currentLoadOrder.pointIndex==4 && currentLoadOrder.object.rotation.y<0)
{
currentLoadOrder.object.rotation.set(0,currentLoadOrder.object.rotation.y+0.08,0);
}
else if(currentLoadOrder.pointIndex==5 && currentLoadOrder.object.rotation.y<Math.PI / 2)
{
currentLoadOrder.object.rotation.set(0,currentLoadOrder.object.rotation.y+0.08,0);
}
else if(currentLoadOrder.pointIndex==6 && currentLoadOrder.object.rotation.y<Math.PI)
{
currentLoadOrder.object.rotation.set(0,currentLoadOrder.object.rotation.y+0.02,0);
}
currentLoadOrder.object.position.set(point.x,point.y,point.z);
}
else
{
currentLoadOrder.pointIndex++;
if(currentLoadOrder.pointIndex>=currentLoadOrder.points.length)
{
//计算装卸量
currentLoadOrder.hand.quantity=currentLoadOrder.co+currentLoadOrder.po4+currentLoadOrder.nh4;
currentLoadOrder.hand.over=0;
currentLoadOrder.hand.animate=true;
currentLoadOrder.state="loading";
currentLoadOrder=null;
}
else
{
var curvePath=[];
var point=currentLoadOrder.points[currentLoadOrder.pointIndex-1];
curvePath.push(new THREE.Vector3(point.x,point.y,point.z));
point=currentLoadOrder.points[currentLoadOrder.pointIndex];
curvePath.push(new THREE.Vector3(point.x,point.y,point.z));
var curve = new THREE.CatmullRomCurve3(curvePath);
currentLoadOrder.path=curve;
currentLoadOrder.progress=0;
}
}
}
else
{
for(var i=0;i<handArr.length;i++)
{
//如果装卸臂不忙的话
if(!handArr[i].isBusy)
{
//看看是否有等候的车辆,有的话就初始化一下它
for(var j=0;j<orderObjects.length;j++)
{
if(orderObjects[j].state=="parked")
{
handArr[i].isBusy=true;
orderObjects[j].park.used=false;
initOrderLoadAnimate(orderObjects[j],handArr[i]);
i=handArr.length+1; //强制退出循环
break;
}
}
}
}
}
//----------------------装卸场动画处理完毕--------------------------
//----------------------处理离场动画,是个排队数组,每次发车一个,防止撞车--------------------------
if(currentOverOrders.length>0)
{
var currentOverOrder=currentOverOrders[0];
if(currentOverOrder.progress<1)
{
currentOverOrder.progress += 0.008;
var point = currentOverOrder.path.getPoint(currentOverOrder.progress);
if(currentOverOrder.pointIndex==2 && currentOverOrder.object.rotation.y>Math.PI/2)
{
currentOverOrder.object.rotation.set(0,currentOverOrder.object.rotation.y-0.08,0);
}
if(currentOverOrder.pointIndex==3 && currentOverOrder.object.rotation.y<Math.PI)
{
currentOverOrder.object.rotation.set(0,currentOverOrder.object.rotation.y+0.08,0);
}
currentOverOrder.object.position.set(point.x,point.y,point.z);
}
else
{
currentOverOrder.pointIndex++;
if(currentOverOrder.pointIndex>=currentOverOrder.points.length)
{
//删除数组元素
currentOverOrders.splice(0,1);
for(var i=0;i<orderObjects.length;i++)
{
if(orderObjects[i].id==currentOverOrder.id)
{
orderObjects.splice(i,1);
break;
}
}
//释放动画对象
currentOverOrder.object.traverse(function(obj) {
if (obj.type == 'Mesh') {
obj.geometry.dispose();
obj.material.dispose();
}
if (obj.type == 'Sprite') {
obj.material.dispose();
}
})
// 删除场景对象scene的子对象group
scene.remove(currentOverOrder.object);
currentOverOrder=null;
}
else
{
var curvePath=[];
var point=currentOverOrder.points[currentOverOrder.pointIndex-1];
curvePath.push(new THREE.Vector3(point.x,point.y,point.z));
point=currentOverOrder.points[currentOverOrder.pointIndex];
curvePath.push(new THREE.Vector3(point.x,point.y,point.z));
var curve = new THREE.CatmullRomCurve3(curvePath);
currentOverOrder.path=curve;
currentOverOrder.progress=0;
}
}
}
//----------------------离场动画处理完毕--------------------------
//----------------------处理机械臂动画--------------------------
for(var i=0;i<handArr.length;i++)
{
if(handArr[i].animate)
{
if(handArr[i].object.position.x<-215)
{
handArr[i].step=0.3;
}
else if(handArr[i].object.position.x>-180)
{
handArr[i].step=-0.3;
//减去装货量2吨
handArr[i].over+=2;
if(handArr[i].over>handArr[i].quantity) handArr[i].over=handArr[i].quantity;
//更新显示
var id=handArr[i].object.name.replace("HAND","");
$('#dockOrder'+id).html("订单【"+handArr[i].orderObject.id+"】");
$('#dockInfo'+id).html(handArr[i].over+"/"+handArr[i].quantity);
var dockChart=echarts.getInstanceByDom($("#dockChart"+id)[0]);
//装完了,就走
if(handArr[i].quantity<=handArr[i].over)
{
//开始发车
handArr[i].orderObject.state="loaded";
initOrderOverAnimate(handArr[i].orderObject,handArr[i]);
dockChart_option.series[0].data[0].value=0;
finished+=handArr[i].quantity;
$('#finished').html(finished);
var finishedPercent=(finished>0)?Math.round(finished/Planned*1000)/10:0;
$('#finishedPercent').html(finishedPercent+"%");
}
else
{
dockChart_option.series[0].data[0].value=Math.round(handArr[i].over/handArr[i].quantity*100);
}
dockChart.setOption(dockChart_option,true);
}
handArr[i].object.position.x+=handArr[i].step;
}
}
//----------------------机械臂动画处理完毕--------------------------
}
//初始化离场动画
function initOrderOverAnimate(orderObject,handObject)
{
var workArea=handObject.workArea;
var pathPoints=[];
//起点
pathPoints.push({x:orderObject.object.position.x,y:orderObject.object.position.y,z:orderObject.object.position.z});
//出库
pathPoints.push({x:points.pointPark5.position.x,y:points.pointPark5.position.y,z:workArea.position.z+10});
//转弯
pathPoints.push({x:points.pointPark6.position.x,y:points.pointPark6.position.y,z:points.pointPark6.position.z});
//走人
pathPoints.push({x:points.pointEnd.position.x,y:points.pointEnd.position.y,z:points.pointEnd.position.z});
orderObject.points=pathPoints;
orderObject.pointIndex=0;
orderObject.progress=1;
handObject.animate=false;
handObject.isBusy=false;
currentOverOrders.push(orderObject);
}
//初始化装卸场动画
function initOrderLoadAnimate(orderObject,handObject)
{
var workArea=handObject.workArea;
var pathPoints=[];
//起点
pathPoints.push({x:orderObject.object.position.x,y:orderObject.object.position.y,z:orderObject.object.position.z});
//出库和第一个拐弯
if(orderObject.rowIndex==1)
{
//出库
pathPoints.push({x:points.pointPark4.position.x,y:orderObject.object.position.y,z:orderObject.object.position.z});
pathPoints.push({x:points.pointPark4.position.x,y:orderObject.object.position.y,z:orderObject.object.position.z+10});
pathPoints.push({x:points.pointPark4.position.x,y:points.pointPark4.position.y,z:points.pointPark4.position.z});
}
else
{
pathPoints.push({x:points.pointPark3.position.x,y:orderObject.object.position.y,z:orderObject.object.position.z});
pathPoints.push({x:points.pointPark3.position.x,y:orderObject.object.position.y,z:orderObject.object.position.z-20});
pathPoints.push({x:points.pointPark3.position.x,y:points.pointPark3.position.y,z:points.pointPark3.position.z});
}
//第二个拐弯
pathPoints.push({x:points.pointPark5.position.x,y:points.pointPark5.position.y,z:points.pointPark5.position.z});
//倒车点
pathPoints.push({x:points.pointPark5.position.x,y:points.pointPark5.position.y,z:workArea.position.z+10});
//倒车和入库
pathPoints.push({x:workArea.position.x,y:workArea.position.y,z:workArea.position.z});
orderObject.points=pathPoints;
orderObject.pointIndex=0;
orderObject.progress=1;
orderObject.hand=handObject;
handObject.orderObject=orderObject;
currentLoadOrder=orderObject;
}
//初始化停车动画
function initOrderParkAnimate(orderObject,park)
{
var pathPoints=[];
//起点
pathPoints.push({x:points.pointStart.position.x,y:points.pointStart.position.y,z:points.pointStart.position.z});
//转折
if(park.id<=6)
{
pathPoints.push({x:points.pointPark2.position.x,y:points.pointPark2.position.y,z:points.pointPark2.position.z});
//终点前一点
pathPoints.push({x:points.pointPark2.position.x,y:points.pointPark2.position.y,z:park.object.position.z});
orderObject.rowIndex=1;
}
else
{
pathPoints.push({x:points.pointPark1.position.x,y:points.pointPark1.position.y,z:points.pointPark1.position.z});
//终点前一点
pathPoints.push({x:points.pointPark1.position.x,y:points.pointPark1.position.y,z:park.object.position.z});
orderObject.rowIndex=2;
}
//终点
pathPoints.push({x:park.object.position.x,y:park.object.position.y,z:park.object.position.z});
orderObject.points=pathPoints;
orderObject.object.position.set(points.pointStart.position.x, points.pointStart.position.y, points.pointStart.position.z);
orderObject.pointIndex=0;
orderObject.progress=1;
orderObject.park=park;
//开始动画
currentParkOrder=orderObject;
}
在桢刷新时候调用(或者写一个定时器,这个比较稳定)
function animate_Frame()
{
animateOrder();
renderer.render(scene, camera);
requestAnimationFrame(animate_Frame);
}
主要的思路就是,在场景中放置标志点,然后将标志点连接成路径,均分为多少份,每次移动一个标志点的位置。
var curvePath=[];
var point=currentLoadOrder.points[currentLoadOrder.pointIndex-1];
curvePath.push(new THREE.Vector3(point.x,point.y,point.z));
point=currentLoadOrder.points[currentLoadOrder.pointIndex];
curvePath.push(new THREE.Vector3(point.x,point.y,point.z));
var curve = new THREE.CatmullRomCurve3(curvePath);
currentLoadOrder.path=curve;
currentLoadOrder.progress=0;
取两个点生成一个路径。
currentLoadOrder.progress += 0.008;
var point = currentLoadOrder.path.getPoint(currentLoadOrder.progress);
...
currentLoadOrder.object.position.set(point.x,point.y,point.z);
根据点密度,调整这个progress,实现快或者慢的效果。
掉头主要是根据点的位置来计算,例如:
if(currentLoadOrder.pointIndex==2 && currentLoadOrder.object.rotation.y>- Math.PI / 2)
{
currentLoadOrder.object.rotation.set(0,currentLoadOrder.object.rotation.y-0.02,0);
}
实现左转。
更多推荐
所有评论(0)