news 2026/10/26 9:43:55

blender+Three.js 三维数据可视化:数字孪生

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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);
}

实现左转。

演示地址:仓库监控管理系统,为保证模型精度,文件比较大,加载要等一会

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 9:43:44

Trading-as-Git:量化交易防暴仓的OpenAlice风控闭环

如果你也被“回测曲线漂亮得像教科书&#xff0c;实盘账户却在一个月内把半年利润全还回去”这件事折磨过&#xff0c;那这篇文章应该能帮到你。我把自己踩了无数坑之后沉淀下来的量化框架 OpenAlice 完整拆开&#xff0c;讲清楚 Trading-as-Git 这个核心设计怎么落地到本地量化…

作者头像 李华
网站建设 2026/10/26 9:43:16

PLC实战经验90条:从编程到通讯、仿真、接线的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 9:42:51

基于YOLOv8的PCB缺陷检测系统实现与优化

1. PCB表面缺陷检测项目概述在电子制造业中&#xff0c;PCB&#xff08;印刷电路板&#xff09;作为电子产品的核心载体&#xff0c;其质量直接影响最终产品的性能和可靠性。传统的人工目检方式效率低下且容易漏检&#xff0c;而基于机器视觉的自动化检测技术正在成为行业新标准…

作者头像 李华
网站建设 2026/10/26 9:42:28

Cybertruck 48V低压架构解析:从三相FOC逆变到分流电流采样

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 9:42:15

Versal ACAP全局时钟网络核心指南:BUFG与Clock Region详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 9:40:29

词云图三种实现路线:ECharts自定义形状与3D球形滚动

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华