一、实现方案

单独贴代码可能容易混乱,所以这里只讲实现思路,代码放在最后汇总了下。

想要实现一个简单的工业园区、主要包含的内容是一个大楼、左右两片停车位、四条道路以及多个可在道路上随机移动的车辆、遇到停车位时随机选择是否要停车,简单设计图如下

二、实现步奏

2.1 引入环境,天空和地面

 引入天空有三种方式:

  1) 第一种通过添加天空盒导入六个不同角度的天空图片可以形成,简单方便,缺点是在两个面之间会有视觉差

  2) 第二种是设置scene的背景和环境是一张天空图片来实现的,缺点图片单一,而且在天、地斜街处很生硬

  3) 不需要导入外部图片,通过在一个球体上添加渐变色实现,缺点球体只有一部分是天空颜色,内部为白色,需要设定旋转范围

  4) 使用Three.js中的example中的Sky.js实现,效果比较完美

 引入地面:给一个大平面添加一张草地纹理即可。

2.2 创建一块地基

  创建一个固定长度的平面,然后绕X轴旋转即可

2.3 布置围墙

  导入一个围墙模型作为一个围墙的基本单位A,算出围墙所占的长和宽,为了完整性,可以将园区的长和宽设定为A的整数倍。

2.4 办公楼、停车场、充电桩加载

  1)导入一个办公大楼模型

  2)创建一个停车场类Parking.js,主要用来创建单个停车位,其中需要计算出停车位的进入点,方便以后车辆进入。

  3)导入一个充电桩,每两个停车位使用一个充电桩

2.5 添加办公楼前景观、树、公交站点

  1)在指定位置导入景观模型和公交站点模型

  2)导入树模型,在园区前侧围墙均匀分布

2.6 铺设路面

                  

  首先道路可以细化为上下行多个车道,而车辆则是行驶在各车道的中心线位置处,所以为了方便后续车辆的控制,需要先将道路拆分,然后获取各个道路中心线和车道中心线信息

  1)创建一个道路类Road.js,道路点信息传入的是图中红色点信息(图中菱形点),需要标记出从哪个点开始道路非直线,

    比如点信息格式为:[{ coord: [10, 0], type: 1}, { coord: [10, 10], type: 0}, { coord: [0, ], type: 1}] ;0代表曲线点,1代表直线点

  2)由于使用传入的原始道路点无法绘制出平滑的曲线而且在细化道路点的时候直线点数据细化不明显,所以需要先按照一定的间隔插入部分点信息(图中绿色五角星点)

  3)根据细化后的道路点按照道路宽度向两边开始扩展点信息,扩展方式通过获取当前点A和前一个点B组成的直线,取AB上垂线且距AB直线距离均为路宽的点即可,最终得到道路左侧点A和右侧点B

  4)通过ThreeJS中创建一条平滑曲线获取曲线上的多个点即可得到三条平滑的曲线A、B、C。

  5)经过第四步虽然可以得到道路数据,但是无法区分上下行,仍然不满足使用,通过图二上下行车辆最后生成组合成的一条闭合轨迹应该是逆时针的,

     所以需要将最后生成的A、B线顶点反转拼接成一个完整的多边形,如果是逆时针则可以得到正确的上下行路线。

  6)根据道路顶点即可画出道路面以及道路边界和中心线。

2.7 添加车辆以及车辆在道路随机移动的逻辑

               

  创建一个移动类,可以承接车辆或者行人,当前以车辆为主,主要包含移动轨迹、当前移动所在道路和车道、车位停车、驶离车位等内容。

  1)创建一个Move.js类,创建对象时传入停车场对象信息、道路对象信息,方便后续移动时可以计算出轨迹信息

  2)根据提供的初始位置计算出最近的道路和车道信息,与当前位置拼接在一起即可生成行动轨迹。

  3)当车辆移动到道路尽头时可以获取到本道路的另外一条车道即可实现掉头

  4)路口的判断:图三中,车辆由M车道途径N车道时,由M车道左侧当前位置和上一个位置组成的线段与N车道右侧车道起始或者终止点组成的线段有交集时则代表有路口,同样方法可以得到右侧道路的路口信息

  5)路口处拐入其他车道的轨迹生成:根据4)可以找到转向N的车道信息,但是无法保证平稳转向,所以可以通过找到M和N的车道中心线所在直线获取到交点C,然后由A、C、B生成一条贝塞尔曲线即可平滑转弯

2.8 添加停车逻辑以及车辆驶离逻辑

  1)寻找停车场:如图四,车辆在向前移动时,移动到的每个点都和所有停车场的入口B的位置判断距离,如果小于一个固定值的则代表临近车位,可以停车。

  2)停车方式:根据6)获取到的停车位,同时在当前路径上继续向前取15个点的位置C、B、A组成的曲线则是倒车入口的路径线。

三、遗留问题、待优化点

1. 拐弯添加的点不多,所以在拐弯处速度较快

  ---  可以通过在拐弯处组成的多个点通过生成的线获取多个点来解决这个问题

2. 需要添加一个路口来管理各条之间的关系

  --- 优点:(1). 有了路口后,可以解决车辆在路口移动时实时计算和其他路口的位置关系,可能会导致路口转弯混乱,通过在路口中心点生成一个外接圆,如果进入路口,则锁死移动方向,如果移出路口则解除锁定

    (2). 解决在路口处,各道路绘制的边线有重叠问题,使各个道路之间能看着更平滑

    缺点:最好不需要导入路口,而是由各个道路之间的相交关系计算得出,计算逻辑较为复杂。

3. 最好能添加一个停车场方便管理车位以及车辆驶入、驶离停车位

  --- 添加停车场,车辆只需要和停车场的位置计算即可,不需要和每个停车位计算位置,减少冗余计算,而且车辆如果和单个停车位计算位置,可能存在从停车位A使出,途径相邻的停车位B,又会进入。

    添加停车场通过给停车场添加标识即可解决这个问题

4. 车位和车道的边缘线无法加宽

  --- Three.js目前的缺陷,尝试几种办法暂时没有解决

5. 没有添加车辆防碰撞功能

四、完整的代码

  为了简单点,没有用Node安装依赖包,下述JS中引入的其他文件均在threeJS安装包中可以找到,拷贝过来即可。

<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>园区案例</title>
</head> <body style="margin: 0;">
<div id="webgl" style="border: 1px solid;"></div>
<script type="importmap">
{
"imports": {
"three": "./three.module.js"
}
}
</script>
<script type="module" src="./Objects/Main.js"></script>
</script>
</body> </html>

主页面index.html

/**
* 办公园区
*/
import * as THREE from 'three';
import { OrbitControls } from '../OrbitControls.js';
import { GLTFLoader } from '../GLTFLoader.js';
import { addEnviorment, segmentsIntr } from '../Objects/Common.js';
import Move from './Move.js';
import Road from './Road.js';
import Parking from './Parking.js'; /**
* 1. 先引入环境 天空和地面
* 2. 创建一块全区的地皮
* 3. 布置围墙
* 4. 办公楼、停车场、充电桩的位置
* 5. 添加办公楼前装饰物、树、公交站点
* 6. 铺设路面
* 7. 写动态逻辑,设置页面动态化
*/ const wWidth = window.innerWidth; // 屏幕宽度
const wHeight = window.innerHeight; // 屏幕高度 const scene = new THREE.Scene();
let renderer = null;
let camera = null;
let controls = null;
const roadObj = []; // 存储道路数据
const moveObj = []; // 存储车辆数据 // 园区宽度本身
const long = 600; // 园区的长
const width = 300; // 园区的宽
// 停车场的长和宽
const [parkingW, parkingH] = [20, 30];
const parks = []; // 存储停车场数据 let everyL = 0; // 单个围墙的长度
let everyW = 0; // 单个围墙的厚度
let buildH = 0; // 办公楼的厚度
let wallNumL = 0; // 展示园区占多少个墙的长度,当前设置为最大的整数-1
let wallNumW = 0; /**
* 初始化
*/
function init() {
addEnvir(true, false);
createBase();
loadWall();
setTimeout(() => {
loadBuildings();
setTimeout(() => {
loadOrnament();
}, 200)
loadRoad();
loadBusAndPeople();
addClick();
}, 500)
} /**
* 添加相机等基础功能
*/
function addEnvir(lightFlag = true, axFlag = true, gridFlag = false) {
// 初始化相机
camera = new THREE.PerspectiveCamera(100, wWidth / wHeight, 1, 3000);
camera.position.set(300, 100, 300);
camera.lookAt(0, 0, 0); // 创建灯光
// 创建环境光
const ambientLight = new THREE.AmbientLight(0xf0f0f0, 1.0);
ambientLight.position.set(0,0,0);
scene.add(ambientLight);
if (lightFlag) {
// 创建点光源
const pointLight = new THREE.PointLight(0xffffff, 1);
pointLight.decay = 0.0;
pointLight.position.set(200, 200, 50);
scene.add(pointLight);
} // 添加辅助坐标系
if (axFlag) {
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);
} // 添加网格坐标
if (gridFlag) {
const gridHelper = new THREE.GridHelper(300, 25, 0x004444, 0x004444);
scene.add(gridHelper);
} // 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias:true, logarithmicDepthBuffer: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0xf0f0f0, 0.8);
renderer.setSize(wWidth, wHeight); //设置three.js渲染区域的尺寸(像素px)
renderer.render(scene, camera); //执行渲染操作 controls = new OrbitControls(camera, renderer.domElement);
// 设置拖动范围
controls.minPolarAngle = - Math.PI / 2;
controls.maxPolarAngle = Math.PI / 2 - Math.PI / 360; controls.addEventListener('change', () => {
renderer.render(scene, camera);
}) // 添加天空和草地
scene.add(...addEnviorment()); function render() {
// 随机选择一个移动物体作为第一视角
// const cur = moveObj[3];
// if (cur) {
// const relativeCameraOffset = new THREE.Vector3(0, 20, -15); // const cameraOffset = relativeCameraOffset.applyMatrix4( cur.target.matrixWorld ); // camera.position.x = cameraOffset.x;
// camera.position.y = cameraOffset.y;
// camera.position.z = cameraOffset.z;
// // 始终让相机看向物体
// controls.target = cur.target.position; // camera.lookAt(...cur.target.position.toArray());
// } renderer.render(scene, camera);
requestAnimationFrame(render);
}
render(); document.getElementById('webgl').appendChild(renderer.domElement);
} /**
* 创建园区的地基
*/
function createBase() {
const baseGeo = new THREE.PlaneGeometry(long, width);
baseGeo.rotateX(-Math.PI / 2);
const baseMesh = new THREE.Mesh(
baseGeo,
new THREE.MeshBasicMaterial({
color: '#808080',
side: THREE.FrontSide
})
);
baseMesh.name = 'BASE';
scene.add(baseMesh);
} /**
* 加载围墙
*/
function loadWall() {
const loader = new GLTFLoader();
loader.load('./Objects/model/wall.gltf', (gltf) => {
gltf.scene.scale.set(3, 3, 3);
const source = gltf.scene.clone(); // 获取单个围墙的大小
const box3 = new THREE.Box3().setFromObject(gltf.scene);
everyL = box3.max.x - box3.min.x;
everyW = box3.max.z - box3.min.z;
wallNumL = Math.floor(long / everyL) - 1;
wallNumW = Math.floor(width / everyL) - 1; // 加载后墙
// 墙的起点和终点
const backS = [-long / 2, 0, -width / 2];
for (let i = 0; i < wallNumL; i++) {
const cloneWall = source.clone();
cloneWall.position.x = backS[0] + everyL * i + everyL / 2;
cloneWall.position.z = backS[2];
scene.add(cloneWall);
} // 加载左侧墙
const leftS = [-long / 2, 0, -width / 2];
for (let i = 0; i < wallNumW; i++) {
const cloneWall = source.clone();
cloneWall.rotateY(Math.PI / 2);
cloneWall.position.x = leftS[0];
cloneWall.position.z = leftS[2] + everyL * i + everyL / 2;
scene.add(cloneWall);
} // 加载右侧墙
const rightS = [-long / 2 + wallNumL * everyL, 0, -width / 2];
for (let i = 0; i < wallNumW; i++) {
const cloneWall = source.clone();
cloneWall.rotateY(Math.PI / 2);
cloneWall.position.x = rightS[0];
cloneWall.position.z = rightS[2] + everyL * i + everyL / 2;
scene.add(cloneWall);
} // 加载前侧墙
const frontS = [-long / 2, 0, -width / 2 + wallNumW * everyL];
for (let i = 0; i < wallNumL; i++) {
if (i !== Math.floor(wallNumL / 2)) {
const cloneWall = source.clone();
cloneWall.position.x = frontS[0] + everyL * i + everyL / 2;
cloneWall.position.z = frontS[2];
scene.add(cloneWall);
}
}
})
} /**
* 加载办公大楼以及停车场和充电桩
*/
function loadBuildings() {
const loader = new GLTFLoader();
loader.load('./Objects/model/buildings.gltf', (gltf) => {
gltf.scene.scale.set(4, 4, 4); // 获取大楼的大小
const box3 = new THREE.Box3().setFromObject(gltf.scene);
buildH = box3.max.z - box3.min.z;
gltf.scene.position.z = -width / 2 + buildH / 2;
scene.add(gltf.scene);
}) // 添加左侧停车场
// 左侧停车场起始点坐标
const leftSPos = [-long / 2 + everyW + parkingH / 2, 0, -width / 2 + everyW + parkingW / 2 + 3];
for (let i = 0; i < 4; i++) {
const z = leftSPos[2] + i * parkingW;
const parking = new Parking({
name: `A00${i + 1}`,
width: parkingW,
height: parkingH,
position: [leftSPos[0], leftSPos[1] + 1, z]
})
scene.add(parking.group);
parks.push(parking);
} // 右侧充电桩起始点坐标 并预留位置给充电枪
const rightSPos = [-long / 2 + wallNumL * everyL - everyW - parkingH / 2 - 10, 0, -width / 2 + everyW + parkingW / 2 + 3];
for (let i = 0; i < 4; i++) {
const parking = new Parking({
name: `B00${i + 1}`,
width: parkingW,
height: parkingH,
position: [rightSPos[0], rightSPos[1] + 1, rightSPos[2] + i * parkingW],
rotate: Math.PI
})
scene.add(parking.group);
parks.push(parking);
} // 添加充电桩
const chargePos = [-long / 2 + wallNumL * everyL - everyW - 4, 0, -width / 2 + everyW + 3 + parkingW];
loader.load('./Objects/model/charging.gltf', (gltf) => {
for (let i = 0; i < 2; i++) {
const source = gltf.scene.clone();
source.scale.set(6, 6, 6);
source.rotateY(Math.PI / 2);
source.position.x = chargePos[0];
source.position.y = chargePos[1];
source.position.z = chargePos[2] + i * 2 * parkingW;
scene.add(source);
}
})
} /**
* 添加办公楼前装饰物、树、公交站点
*/
function loadOrnament() {
// 加载办公室前方雕塑
const loader = new GLTFLoader();
loader.load('./Objects/model/bed.gltf', (bedGltf) => {
bedGltf.scene.scale.set(2, 2, 2);
bedGltf.scene.rotateY(-Math.PI * 7 / 12);
loader.load('./Objects/model/sculpture.gltf', (sculGltf) => {
sculGltf.scene.scale.set(20, 20, 20);
sculGltf.scene.y = sculGltf.scene.y + 4;
const group = new THREE.Group();
group.add(bedGltf.scene);
group.add(sculGltf.scene);
group.position.set(0, 0, -width / 2 + everyW + buildH + 10);
scene.add(group);
});
}); // 加载树木,沿街用的是柏树
loader.load('./Objects/model/songshu.gltf', (gltf) => {
const source = gltf.scene;
source.scale.set(8, 8, 8);
// 前面墙的树木, 单个墙的中间区域放置一棵树
const frontS = [-long / 2 + everyL / 2, 0, -width / 2 + wallNumW * everyL - 5];
for (let i = 0; i < wallNumL; i++) {
// 同样门口不放置树
if (i !== Math.floor(wallNumL / 2)) {
const temp = source.clone();
temp.position.set(frontS[0] + i * everyL, frontS[1], frontS[2]);
scene.add(temp);
}
}
}); // 加载公交站点,位置在距离大门右侧第二单面墙处
loader.load('./Objects/model/busStops.gltf', (gltf) => {
const source = gltf.scene;
source.scale.set(4, 4, 4);
gltf.scene.position.set(-long / 2 + (Math.floor(wallNumL / 2) + 3) * everyL, 0, -width / 2 + wallNumW * everyL + everyW + 3);
scene.add(gltf.scene);
});
} /**
* 铺设园区和园区外面的公路
* 包含公路以及部分人行道路
*/
function loadRoad() {
const space = 40;
const outWidth = 40;
// 加载园区外面的公路
const outerP1 = [
{ coord: [-long / 2, 0, -width / 2 + wallNumW * everyL + space], type: 1 },
{ coord: [long / 2, 0, -width / 2 + wallNumW * everyL + space], type: 1 },
];
const road1 = new Road({
name: 'road_1',
sourceCoord: outerP1,
width: outWidth,
showCenterLine: true
});
scene.add(road1.group); const outerP2 = [
{ coord: [-long / 2 + wallNumL * everyL + outWidth / 2 + 10, 0, -width / 2 + wallNumW * everyL + space - outWidth / 2 + 0.5], type: 1 },
{ coord: [-long / 2 + wallNumL * everyL + outWidth / 2 + 10, 0, -width / 2], type: 1 },
];
const road2 = new Road({
name: 'road_2',
sourceCoord: outerP2,
width: outWidth,
showCenterLine: true,
zIndex: 0.8
});
scene.add(road2.group); // 加载园区内的道路
const innerWidth = 25;
const color = 0x787878;
const lineColor = 0xc2c2c2;
// 加载到停车场的道路
const innerP1 = [
{ coord: [-long / 2 + Math.floor(wallNumL / 2) * everyL + everyL / 2, 0, -width / 2 + wallNumW * everyL + space - outWidth / 2 + 0.5], type: 1 },
{ coord: [-long / 2 + Math.floor(wallNumL / 2) * everyL + everyL / 2, 0, -width / 2 + wallNumW * everyL + space - 60], type: 0 },
{ coord: [-long / 2 + Math.floor(wallNumL / 2) * everyL + everyL / 2 - innerWidth / 2, 0, -width / 2 + wallNumW * everyL + space - 60 - innerWidth / 2], type: 1 },
{ coord: [-long / 2 + parkingH + 20 + innerWidth / 2, 0, -width / 2 + wallNumW * everyL + space - 60 - innerWidth / 2], type: 0 },
{ coord: [-long / 2 + parkingH + 20, 0, -width / 2 + wallNumW * everyL + space - 60 - innerWidth], type: 1 },
{ coord: [-long / 2 + parkingH + 20, 0, -width / 2 + everyW + 10], type: 1 },
];
const street1 = new Road({
name: 'street_1',
sourceCoord: innerP1,
width: innerWidth,
showCenterLine: true,
zIndex: 0.8,
planeColor: color,
sideColor: lineColor
});
scene.add(street1.group); // 加载到充电桩的道路
const innerP2 = [
{ coord: [-long / 2 + Math.floor(wallNumL / 2) * everyL + everyL / 2, 0, -width / 2 + wallNumW * everyL + space - outWidth / 2 + 0.5], type: 1 },
{ coord: [-long / 2 + Math.floor(wallNumL / 2) * everyL + everyL / 2, 0, -width / 2 + wallNumW * everyL + space - 60], type: 0 },
{ coord: [-long / 2 + Math.floor(wallNumL / 2) * everyL + everyL / 2 + innerWidth / 2, 0, -width / 2 + wallNumW * everyL + space - 60 - innerWidth / 2], type: 1 },
{ coord: [-long / 2 + wallNumL * everyL - parkingH - everyW - 39, 0, -width / 2 + wallNumW * everyL + space - 60 - innerWidth / 2], type: 0 },
{ coord: [-long / 2 + wallNumL * everyL - parkingH - everyW - 39 + innerWidth / 2, 0, -width / 2 + wallNumW * everyL + space - 60 - innerWidth], type: 1 },
{ coord: [-long / 2 + wallNumL * everyL - parkingH - everyW - 39 + innerWidth / 2, 0, -width / 2 + everyW + 10], type: 1 },
];
const street2 = new Road({
name: 'street_2',
sourceCoord: innerP2,
width: innerWidth,
showCenterLine: true,
zIndex: 0.8,
planeColor: color,
sideColor: lineColor
});
scene.add(street2.group); roadObj.push(
road1,
road2,
street1,
street2
); calFork();
} /**
* 计算pointA和pointB 组成的直线与点集points是否有相交
* @param {*} points
* @param {*} pontA
* @param {*} pointB
*/
function judgeIntersect(points, pointA, pointB) {
let res = { flag: false, interP: [] };
for (let i = 0; i < points.length - 1; i++) {
const cur = points[i];
const nextP = points[i + 1];
const interP = segmentsIntr(cur, nextP, pointA, pointB, true)
if ( interP !== false) {
res.flag = true;
res.interP = interP;
res.index = i;
break;
}
}
return res;
} /**
* 计算各条道路的岔口信息并统计到道路对象中
*/
function calFork() {
function setInter(cur, next, interP, corner, width) {
const circle = new THREE.ArcCurve(corner[0], corner[2], width * 2).getPoints(20);
const cirPoints = circle.map(e => new THREE.Vector3(e.x, 0, e.y)); cur.intersect.push({ name: next.name,
interPoint: interP,
corner: cirPoints,
cornerCenter: corner
}); next.intersect.push({
name: cur.name,
interPoint: interP,
corner: cirPoints,
cornerCenter: corner
});
} roadObj.forEach((e, i) => {
if (i < roadObj.length - 1) {
for (let j = i + 1; j < roadObj.length; j++) {
if (e.intersect.map(e => e.name).indexOf(roadObj[j].name) < 0) {
const middle = roadObj[j].middle;
// 计算路牙和其他道路是否有相交
// 左边路牙和下一条路的起始位置做对比
let inter = judgeIntersect(e.left, middle[0], middle[1]);
if (inter.flag) {
const cornerCenter = segmentsIntr(e.middle[inter.index], e.middle[inter.index + 1], middle[0], middle[1]);
setInter(e, roadObj[j], inter.interP, cornerCenter, roadObj[j].width);
continue;
} // 左边路牙和下一条路的终止位置做对比
inter = judgeIntersect(e.left, middle[middle.length - 1], middle[middle.length - 2])
if (inter.flag) {
const cornerCenter = segmentsIntr(e.middle[inter.index], e.middle[inter.index + 1], middle[middle.length - 1], middle[middle.length - 2]);
setInter(e, roadObj[j], inter.interP, cornerCenter, roadObj[j].width);
continue;
} // 右边路牙和下一条路的起始位置做对比
inter = judgeIntersect(e.right, middle[0], middle[1]);
if (inter.flag) {
const cornerCenter = segmentsIntr(e.middle[inter.index], e.middle[inter.index + 1], middle[0], middle[1]);
setInter(e, roadObj[j], inter.interP, cornerCenter, roadObj[j].width);
continue;
} // 右边路牙和下一条路的终止位置做对比
inter = judgeIntersect(e.right, middle[middle.length - 1], middle[middle.length - 2]);
if (inter.flag) {
const cornerCenter = segmentsIntr(e.middle[inter.index], e.middle[inter.index + 1], middle[middle.length - 1], middle[middle.length - 2]);
setInter(e, roadObj[j], inter.interP, cornerCenter, roadObj[j].width);
continue;
}
}
}
}
})
} function actionTemp(target, name, flag, moveName) {
const filter = roadObj.filter(e => e.name === name)[0]; const carObject = new Move({
name: moveName,
target: target,
roads: roadObj,
startPos: flag ? filter.left[0] : filter.right[0],
parks: parks
});
moveObj.push(carObject);
} /**
* 加载行人和汽车
*/
function loadBusAndPeople() {
// 加载汽车和公交车
const loader = new GLTFLoader(); const carId = [
'car0',
'car2',
'car4',
'car5',
'bus', 'car3',
];
const roadIds = [
'road_1',
'road_2',
'street_1',
'street_2',
'street_2', 'road_2',
]; carId.forEach((e, i) => {
loader.load(`./Objects/model/${e}.gltf`, (gltf) => {
gltf.scene.scale.set(4, 4, 4);
scene.add(gltf.scene);
gltf.scene.name = e;
actionTemp(gltf.scene, roadIds[i], false, e);
});
})
} /**
* 点击汽车驶离停车位
*/
function addClick() {
renderer.domElement.addEventListener('click', (event) => {
const px = event.offsetX;
const py = event.offsetY;
const x = (px / wWidth) * 2 - 1;
const y = -(py / wHeight) * 2 + 1;
//创建一个射线发射器
const raycaster = new THREE.Raycaster();
// .setFormCamera()计算射线投射器的射线属性ray
// 即在点击位置创造一条射线,被射线穿过的模型代表选中
raycaster.setFromCamera(new THREE.Vector2(x, y), camera); const intersects = raycaster.intersectObjects(moveObj.map(e => e.target));
if (intersects.length > 0) {
const move = moveObj.filter(e => e.name === intersects[0].object.parent.name || e.name === intersects[0].object.parent.parent.name)[0];
if (move && move.pause) {
move.unParkCar();
}
}
})
} init();

控制器Main.js

import * as THREE from 'three';
import { getCurvePoint, getSidePoints, segmentsIntr, clone, isClockWise } from './Common.js'; /**
* 移动类,实现物体如何按照路径运动以及在岔路口如何选择等功能
* 后期可以增加碰撞检测避让等功能
*/
class Road {
constructor(props) { // 道路的原始点信息,通过这些点信息扩展道路
this.sourceCoord = props.sourceCoord; // 道路名称
this.name = props.name; // 道路宽度
this.width = props.width; // 是否显示道路中心线
this.showCenterLine = props.showCenterLine === false ? false : true; // 左侧路牙点集合
this.left = []; // 道路中心线点集合
this.middle = []; // 右侧路牙点集合
this.right = []; // 道路面的颜色
this.planeColor = props.planeColor || 0x606060; // 道路边线的颜色
this.sideColor = props.sideColor || 0xffffff; // 道路中心线的颜色
this.middleColor = props.middleColor || 0xe0e0e0; // 道路的层级
this.zIndex = props.zIndex || 0.5; // 车道信息
this.lanes = []; // 道路组合对象
this.group = null; // 相交的道路名称 数据格式{name: ***, interPoint: [xx,xx,xx]}
this.intersect = []; this.lineInsert();
this.create();
} /**
* 由于直线获取贝塞尔点的时候插入的点较少导致物体运动较快,所以在
* 平行与X、Y轴的线插入部分点,保证物体运动平滑,插入点时保证X或者Z轴间距为路宽的一半
*/
lineInsert() {
const temp = [];
const half = this.width / 2;
this.sourceCoord.forEach((cur, i) => {
temp.push(cur);
if (i < this.sourceCoord.length - 1) {
const e = cur.coord;
const nextP = this.sourceCoord[i + 1].coord;
// 处理直线
if (cur.type === 1) {
if (e[0] - nextP[0] === 0) {
// 平行Z轴
if (e[2] < nextP[2]) {
for (let i = e[2] + half; i < nextP[2]; i += half) {
temp.push({
coord: [e[0], e[1], i],
type: 1
});
}
} else {
for (let i = e[2] - half; i > nextP[2]; i -= half) {
temp.push({
coord: [e[0], e[1], i],
type: 1
});
}
}
} else if (e[2] - nextP[2] === 0) {
// 平行X轴
if (e[0] < nextP[0]) {
for (let i = e[0] + half; i < nextP[0]; i += half) {
temp.push({
coord: [i, e[1], e[2]],
type: 1
});
}
} else {
for (let i = e[0] - half; i > nextP[0]; i -= half) {
temp.push({
coord: [i, e[1], e[2]],
type: 1
});
}
}
}
}
}
})
this.sourceCoord = temp;
} /**
* 创建道路
*/
create() {
const group = new THREE.Group();
const roadPoints = this.getPoints(this.sourceCoord, this.width); this.left = roadPoints[0];
this.middle = roadPoints[1];
this.right = roadPoints[2]; const isWise = isClockWise(this.left.concat(clone(this.right).reverse())); // 添加左车道
this.lanes.push(new Lane({
name: `${this.name}_lane_0`,
type: 'left',
isReverse: isWise,
side: this.left,
middle: this.middle
})); // 添加右车道
this.lanes.push(new Lane({
name: `${this.name}_lane_1`,
type: 'right',
isReverse: !isWise,
side: this.right,
middle: this.middle
})); const outlinePoint = roadPoints[0].concat(clone(roadPoints[2]).reverse());
outlinePoint.push(roadPoints[0][0]);
const shape = new THREE.Shape();
outlinePoint.forEach((e, i) => {
if (i === 0) {
shape.moveTo(e[0], e[2], e[1]);
} else {
shape.lineTo(e[0], e[2], e[1]);
}
}) // 创建道路面
const plane = new THREE.Mesh(
new THREE.ShapeGeometry(shape),
new THREE.MeshBasicMaterial({
color: this.planeColor,
side: THREE.DoubleSide
})
)
plane.rotateX(Math.PI / 2);
group.add(plane); // 创建道路边沿线
const sideL = new THREE.Line(
new THREE.BufferGeometry().setFromPoints(roadPoints[0].map(e => new THREE.Vector3(...e))),
new THREE.LineBasicMaterial({ color: this.sideColor, linewidth: 10 })
); const sideR = new THREE.Line(
new THREE.BufferGeometry().setFromPoints(roadPoints[2].map(e => new THREE.Vector3(...e))),
new THREE.LineBasicMaterial({ color: this.sideColor, linewidth: 10 })
);
group.add(sideL, sideR);
// 创建道路中心虚线
if (this.showCenterLine) {
const sideM = new THREE.Line(
new THREE.BufferGeometry().setFromPoints(roadPoints[1].map(e => new THREE.Vector3(...e))),
new THREE.LineDashedMaterial({ color: this.middleColor, linewidth: 10, gapSize: 5, dashSize: 5 })
);
sideM.computeLineDistances();
group.add(sideM);
} group.position.y = group.position.y + this.zIndex;
group.name = this.name;
this.group = group;
} /**
* 获取道路的顶点信息
*/
getPoints(points) {
const half = this.width / 2; // 存储左中右三条线路的顶点信息
const [left, middle, right] = [[], [], []];
for (let i = 0; i < points.length;) {
const e = points[i].coord;
if (points[i].type === 1) {
// 直线处理方式
if (i === 0) {
const nextP = points[i + 1].coord;
const side = getSidePoints(e, nextP, e, half);
left.push(side[0]);
middle.push(e);
right.push(side[1]);
} else {
const preP = points[i - 1].coord;
const side = getSidePoints(preP, e, e, half);
left.push(side[0]);
middle.push(e);
right.push(side[1]);
}
i++;
} else {
// 曲线处理方式
const preMidP = points[i - 1].coord;
const nextMidP1 = points[i + 1].coord;
const nextMidP2 = points[i + 2].coord; // 获取两侧点信息
const sideP1 = getSidePoints(preMidP, e, e, half);
const sideP2 = getSidePoints(nextMidP1, nextMidP2, nextMidP1, half);
const sideP3 = getSidePoints(nextMidP1, nextMidP2, nextMidP2, half); // 左侧
const interLeft = segmentsIntr(left[left.length - 1], sideP1[0], sideP2[0], sideP3[0]);
const curveLeft = getCurvePoint(sideP1[0], interLeft, sideP2[0]);
left.push(...curveLeft); // 中间
const interMid = segmentsIntr(middle[middle.length - 1], e, nextMidP1, nextMidP2);
const curveMid = getCurvePoint(e, interMid, nextMidP1);
middle.push(...curveMid); // 右侧
const interRight = segmentsIntr(right[right.length - 1], sideP1[1], sideP2[1], sideP3[1]);
const curveRight = getCurvePoint(sideP1[1], interRight, sideP2[1]);
right.push(...curveRight);
i += 2;
}
} return [left, middle, right];
}
} /**
* 车道对象
*/
class Lane {
constructor(options) {
//车道名称
this.name = options.name; // 标识左车道还是右车道
this.type = options.type; // 行驶方向和点的方向是否一致
this.direction = options.direction; // 车道中心线
this.coord = this.getCenter(options.middle, options.side, options.isReverse);
}
getCenter(middle, side, reverseFlag) {
const center = middle.map((e, i) => {
return [(e[0] + side[i][0]) / 2, e[1], (e[2] + side[i][2]) / 2];
});
return reverseFlag ? center.reverse() : center;
}
} export default Road;

道路类Road.js

import * as THREE from 'three';
import { TextGeometry } from '../TextGeometry.js';
import { FontLoader } from '../FontLoader.js'; /**
* 停车场类
*
*/
class Parking {
constructor(props) {
// 停车场的名称
this.name = props.name; // 停车场的宽
this.width = props.width; // 停车场的长
this.height = props.height; // 停车场的中心位置
this.position = props.position; // 停车场的顶点信息
this.coord = []; // 停车场有时候需要根据不同的显示位置进行Z轴旋转
this.rotate = props.rotate; // 停车位的入口点,主要确保车辆进出的方向
this.entryPoint = []; // 车位已经停车标识
this.placed = false; this.create();
} /**
* 创建道路
*/
create() {
const points = [
new THREE.Vector3(-this.height / 2, this.width / 2, 0),
new THREE.Vector3(-this.height / 2, -this.width / 2, 0),
new THREE.Vector3(this.height / 2, -this.width / 2, 0),
new THREE.Vector3(this.height / 2, this.width / 2, 0)
]; // 停车场四周白色边线
const line = new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(points),
new THREE.LineBasicMaterial({ color: 0xe0e0e0, linewidth: 1 })
); // 停车场面
const plane = new THREE.Mesh(
new THREE.PlaneGeometry(-this.height, this.width),
new THREE.MeshLambertMaterial({ color: 0xc0e9bf, side: THREE.DoubleSide })
); // 添加车位编号
const loader = new FontLoader();
loader.load('./Objects/helvetiker_regular.typeface.json', (font) => {
const textGeo = new TextGeometry(this.name, {
font: font,
size: 2,
height: 0
});
textGeo.rotateZ(-Math.PI / 2);
const text = new THREE.Mesh(
textGeo,
new THREE.MeshBasicMaterial({ color: 0xffffff })
);
text.position.x = this.height * 3 / 8;
text.position.y = this.name.length / 2;
text.position.z = text.position.z + 1;
group.add(text);
});
const group = new THREE.Group();
group.add(line);
group.add(plane);
group.rotateX(-Math.PI / 2);
if (this.rotate) {
group.rotateZ(this.rotate);
}
this.group = group;
group.position.set(...this.position);
let beta = 1;
if (this.rotate === Math.PI) {
beta = -1;
}
this.entryPoint = [this.height * beta / 2 + this.position[0], this.position[1], this.position[2]];
}
} export default Parking;

停车位类Parking.js

import * as THREE from 'three';
import { calDistance, drawLine, clone, pointInPolygon, segmentsIntr } from './Common.js'; /**
* 移动类,实现物体如何按照路径运动以及在岔路口如何选择等功能
* 后期可以增加碰撞检测避让等功能
*/
class Move {
constructor(props) {
// 所有道路信息
this.roads = {};
props.roads.forEach(e => this.roads[e.name] = e); this.name = props.name; // 物体对象
this.target = props.target; // 前进还是倒车 1:前进;-1:倒车
this.direction = 1; // 物体初始位置
this.startPos = props.startPos; // 移动轨迹
this.trace = []; // 当前形式的道路
this.curRoad = null; // 锁定下一步行动趋势,主要是为了解决在路口物体获取到下一步行动后再次获取行动导致功能乱掉
this.trendLock = false; this.preRoadName = null; // 当前移动所在的车道
this.curLane = null; // 是否暂停移动
this.pause = false; // 园区停车场信息
this.parks = props.parks; this.parkObj = null; this.init();
} /**
* 获取当前实体所在道路以及移动方向和移动轨迹
*/
init() {
let minDis = {
roadName: '', // 起始位置所在道路的道路名
distance: 100000, // 起始物体与车道左右边线的最小距离
curLane: null, // 移动的车道
};
for (let o in this.roads) {
const road = this.roads[o];
const startLeftDis = calDistance(road.lanes[0].coord[0], this.startPos);
const startRightDis = calDistance(road.lanes[1].coord[0], this.startPos);
const endLeftDis = calDistance(road.lanes[0].coord[road.lanes[0].coord.length - 1], this.startPos);
const endRightDis = calDistance(road.lanes[1].coord[road.lanes[1].coord.length - 1], this.startPos);
const min = Math.min(startLeftDis, startRightDis, endLeftDis, endRightDis); if (minDis.distance > min) {
minDis = {
roadName: o,
distance: min,
curLane: (min === startRightDis || min === endRightDis) ? road.lanes[0] : road.lanes[1],
index: (startLeftDis === min || startRightDis === min) ? 0 : road.left.length - 1
};
}
} this.curLane = minDis.curLane; this.curRoad = this.roads[minDis.roadName];
this.trace = this.getSpreadPoint(this.curLane.coord);
this.moveIndex = minDis.index;
this.move();
} /**
* 获取车道中心线
* 将车道的左右线取平均值即可
* @param {*} params
*/
getLaneCenter(points1, points2) {
return points1.map((e, i) => {
return [(e[0] + points2[i][0]) / 2, e[1], (e[2] + points2[i][2]) / 2];
})
} move() {
if (!this.pause) {
this.checkMoveOutCorner();
this.parkCar();
// 如果移动到当前轨迹的最后一个点时,需要进行掉头重新寻找轨迹
const forks = this.checkFork();
if (forks.length !== 0) {
this.getRandomTrend(forks);
} else if (this.moveIndex === this.trace.length - 1) {
this.getTurnPoint();
}
const curPosition = this.trace[this.moveIndex].toArray();
this.target.position.set(...curPosition);
if (this.direction === 1) {
// 前进
if (this.moveIndex !== this.trace.length - 1) {
this.target.lookAt(...this.trace[this.moveIndex + 1].toArray());
}
} else {
// 倒车
if (this.moveIndex !== 0) {
this.target.lookAt(...this.trace[this.moveIndex - 1].toArray());
}
} this.moveIndex ++;
}
requestAnimationFrame(this.move.bind(this));
} /**
* 在锁定后检查物体是否已经移出路口,如果移出则解除锁定
*/
checkMoveOutCorner() {
if (!this.trendLock) {
return false;
}
const preObj = this.curRoad.intersect.filter(e => e.name === this.preRoadName)[0];
if (preObj && !pointInPolygon(this.trace[this.moveIndex], preObj.corner)) {
this.trendLock = false;
this.preRoadName = null;
}
return this.trendLock;
} /**
* 根据提供的点信息寻找最近的点值
*/
findNearIndex(points, pointA) {
let min = 100000;
let index = -1;
for (let i = 0; i < points.length; i++) {
const dis = calDistance(points[i], pointA);
if (dis < min) {
min = dis;
} else {
index = i - 1;
break;
}
}
return index;
} /**
* 在岔路口时随机获取下一步行动方向
*/
getRandomTrend(forks) {
const isEnd = calDistance(this.trace[this.moveIndex].toArray(), this.trace[this.trace.length - 1].toArray()) < this.curRoad.width / 2;
// 从多条路中随机选择一条,当前园区路况简单 路口数据目前只有一条
const randomRoad = forks[Math.floor(Math.random() * forks.length)];
// 分别代表掉头、转弯、直行四种情况
let types = [0, 1, 2];
if (isEnd) {
// 如果是道路的尽头 可以选择掉头或者转弯
types = [0, 1, 2];
} else {
// 如果不是道路的尽头,可以选择转弯或者直行
types = [1, 2];
} const random = types[Math.floor(Math.random() * types.length)];
if (random === 0) {
// 掉头
this.trendLock = true;
this.getTurnPoint();
} else if (random === 1) {
// 转弯
this.trendLock = true;
this.getForkPoint(randomRoad, isEnd);
} else if (random === 2) {
this.preRoadName = randomRoad;
// 直线
this.trendLock = true;
}
} /**
* 在岔路口时根据获取道路轨迹信息
*/
getForkPoint(name, isEnd) {
this.preRoadName = this.curRoad.name;
const roadObj = this.roads[name];
let splitPoint = [];
if (isEnd) {
// 如果在道路尽头转弯,随机产生是左侧还是右侧道路
const leftOrRight = Math.floor(Math.random() * roadObj.lanes.length);
const coord = roadObj.lanes[leftOrRight].coord;
this.curLane = roadObj.lanes;
const index = this.findNearIndex(coord, this.trace[this.moveIndex].toArray());
splitPoint = coord.slice(index + 1);
// 为了平滑过渡获取道路末端和当前行驶位置的交点
const corInter = segmentsIntr(splitPoint[0], splitPoint[1], this.trace[this.trace.length - 2].toArray(), this.trace[this.trace.length - 3].toArray());
if (corInter) {
splitPoint.unshift(corInter);
}
splitPoint.unshift(this.trace[this.moveIndex]);
} else {
// 转弯前需要判断当前路可以向那个方向转弯,比如临近路口只能转向右车道,非临近路口需要转到对象车道
// 可以根据当前点和车道点的距离来判断
const lane1Dis = calDistance(roadObj.lanes[0].coord[0], this.trace[this.moveIndex].toArray());
const lane2Dis = calDistance(roadObj.lanes[1].coord[0], this.trace[this.moveIndex].toArray());
let temp = null;
if (lane1Dis < lane2Dis) {
temp = clone(roadObj.lanes[0].coord);
this.curLane = roadObj.lanes[0];
} else {
temp = clone(roadObj.lanes[1].coord);
this.curLane = roadObj.lanes[1];
}
this.curRoad = roadObj;
const index = this.findNearIndex(temp, this.trace[this.moveIndex].toArray());
splitPoint = temp.slice(index + 1);
// 为了平滑过渡获取道路末端和当前行驶位置的交点
const corInter = segmentsIntr(splitPoint[0], splitPoint[1], this.trace[this.moveIndex].toArray(), this.trace[this.moveIndex + 1].toArray());
if (corInter) {
splitPoint.unshift(corInter);
}
splitPoint.unshift(this.trace[this.moveIndex]);
} this.trace = this.getSpreadPoint(splitPoint).map(e => new THREE.Vector3(...e));
// drawLine(this.target.parent, this.trace); this.moveIndex = 0;
} /**
* 掉头后获取道路轨迹信息
*/
getTurnPoint() {
const roadObj = this.curRoad;
const nextLane = roadObj.lanes.filter(e => e.name !== this.curLane.name)[0]
const clonePoint = clone(nextLane.coord);
clonePoint.unshift(this.trace[this.moveIndex].toArray());
this.trace = this.getSpreadPoint(clonePoint);
this.curLane = nextLane;
// drawLine(this.target.parent, this.trace);
this.moveIndex = 0;
} /**
* 获取离散点即将点与点之间更加细化,使物体运行起来更加平滑
* @param {*} points
* @returns
*/
getSpreadPoint(points, beta = 1) {
const trail = new THREE.CatmullRomCurve3([...points.map(e => new THREE.Vector3(...e))]);
return trail.getPoints(trail.getLength() * beta);
} /**
* 将车辆停入停车场内
*/
parkCar() {
// 是否允许停车
if (this.parkEnable) {
return;
}
if (this.parkObj) {
if (this.direction === -1 && this.moveIndex === this.trace.length - 2) {
this.pause = true;
this.parkObj.object.placed = true;
}
if (this.moveIndex === this.trace.length - 1) {
this.direction = -1;
this.trace = this.parkObj.backTrace;
// drawLine(this.target.parent, this.trace);
this.moveIndex = 0;
} else {
return;
}
} else {
let flag = false;
let parkObj = null;
const frontIndex = 13;
for (let i = 0; i < this.parks.length; i++) {
if (calDistance(this.parks[i].entryPoint, this.trace[this.moveIndex].toArray()) < 13 && !this.parks[i].placed) {
flag = true;
parkObj = this.parks[i];
break;
}
}
// return flag;
if (flag) {
const random = Math.floor(Math.random() * 2);
// 1代表停车
if (random === 1) {
const front = this.trace.slice(this.moveIndex, this.moveIndex + frontIndex).map(e => new THREE.Vector3(...e));
const back = this.getSpreadPoint([front[front.length - 1], parkObj.entryPoint, parkObj.position]);
this.moveIndex = 0;
this.trace = front;
this.parkObj = { object: parkObj, backTrace: back };
}
}
}
} unParkCar() {
this.direction = 1;
const parkObj = this.parkObj.object;
parkObj.placed = false;
const index = this.findNearIndex(this.curLane.coord, parkObj.entryPoint);
const temp = this.curLane.coord.slice(index);
temp.unshift(parkObj.entryPoint);
temp.unshift(parkObj.position);
this.trace = this.getSpreadPoint(temp);
this.moveIndex = 0;
this.pause = false;
this.parkObj = null;
this.parkEnable = true;
// 不想再扩展功能了 就在这临时用标识符处理了下出库时不断循环查询车位的情况
setTimeout(() => {
this.parkEnable = false;
}, 3000)
} /**
* 检查岔路口
*/
checkFork() {
if (this.trendLock) {
return [];
}
const forks = [];
for (let i = 0; i < this.curRoad.intersect.length; i++) {
if (this.moveIndex < this.trace.length - 1) {
const dis1 = calDistance(this.trace[this.moveIndex].toArray(), this.curRoad.intersect[i].interPoint);
const dis2 = calDistance(this.trace[this.moveIndex + 1].toArray(), this.curRoad.intersect[i].interPoint);
if (dis1 < this.curRoad.width && dis1 > dis2) {
forks.push(this.curRoad.intersect[i].name);
}
}
}
return forks;
}
} export default Move;

移动类Move.js

Threejs实现一个园区的更多相关文章

  1. 【原创】threejs实现一个全景地球

    介绍 本demo实现一个旋转的全景地球,效果如下 技术分析 1.球体 2.球体表面贴图 实现 创建容器 <div id="container"></div> ...

  2. 【webGl】threejs实现一个简单的动画-弹跳的小球

    在这里,我们将动态画面简称为动画(animation).正如动画片的原理一样,动画的本质是利用了人眼的视觉暂留特性,快速地变换画面,从而产生物体在运动的假象.而对于Three.js程序而言,动画的实现 ...

  3. 使用webgl(three.js)搭建一个3D智慧园区、3D建筑,3D消防模拟,web版3D,bim管理系统——第四课

    序:这段时间忙于奔波,好久没有更新了,今天更新一下,继续上节课的完善讲解,算是对前段时间的一个总结吧.披星戴月的时光也算有点应用效果了. 对于webgl(three.js)性能这一块我在上节课< ...

  4. 初识webgl--我的webgl学习第一课(基于threeJs)

    一,我为什么要学习webgl 一个偶然的机会,在和朋友的聊天过程中,听说了webgl,也许过去也看到过,但是没有特别在意过.原来,JavaScript也可以很好的渲染并在网页上显示三维动画,不用借助插 ...

  5. 教你如何利用threejs对3D模型皮肤进行DIY

    一步一步教你如何利用threejs加载gltf模型来实现DIY换肤功能. 模型准备 模型制作 模型可以通过网上下载,也可以自己通过c4d.maya.blender等模型制作软件得到.这里就不叙述有关模 ...

  6. Web GIS 航拍实现的智慧园区数字孪生应用

    前言 随着智慧城市建设的不断发展,智慧园区作为智慧城市的先行区,其覆盖区域越来越大,产值越来越集中,对于园区数字化建设和智能化管理的诉求也愈加强烈.园区数字化管理是以实现园区多维度业务数据汇聚.融合. ...

  7. 如何用webgl(three.js)搭建处理3D园区、3D楼层、3D机房管线问题(机房升级版)-第九课(一)

    写在前面的话: 说点啥好呢?就讲讲前两天的小故事吧,让我确实好好反省了一下. 前两天跟朋友一次技术对话,对方问了一下Geometry与BufferGeometry的具体不同,我一下子脑袋短路,没点到重 ...

  8. 【BIM】BIMFACE中创建雾化效果

    背景 在BIM运维场景初始化的时候,一般都是首先将整个运维对象呈现在用户面前,例如一座大厦.一座桥梁.一个园区等等,以便于用户进行总览,总体把握运维对象,如果这个宏大的场景边界过于清晰,与背景融合也不 ...

  9. 我就骂你了,我tm还想打你呢

    从地铁出来,一男的抽烟走在我前面,走了一路闻了一路二手烟. 进门,一个园区的,我直接骂了一句:caoni妈的 这哥们瞪着我,我也瞪着他 你骂我干什么 我闻了一路子二手烟 你可以走前面啊 我不走啊 我不 ...

  10. 内网网络摄像机(RTSP/IPC/NVR)如何能在公网进行RTMP/HLS/HTTP-FLV直播

    一.背景需求 传统监控行业里不管是设备端.服务器端亦或是客户端都在一个内网里面.而且现在的大部分监控方案都是这样的格局,小到一个公司范围内的监控,再到一个园区.一个仓库监控.一个农业园林监控.一个养殖 ...

随机推荐

  1. 文心一言 VS 讯飞星火 VS chatgpt (155)-- 算法导论12.3 2题

    二.用go语言,假设通过反复向一棵树中插人互不相同的关键字来构造一棵二叉搜索树.证明:在这棵树中查找关键字所检查过的结点数目等于先前插入这个关键字所检查的结点数目加 1. 文心一言: 为了证明这个结论 ...

  2. 通过 VS Code 优雅地编辑 Pod 内的代码(非 NodePort)

    目录 1. 概述 2. NodePort 方式 3. Ingress 方式 4. 救命稻草 5. 其他 1. 概述 今天聊点啥呢,话说,你有没有想过怎样用 VS Code 连上 K8s 集群内的某个 ...

  3. C++ Qt开发:QItemDelegate 自定义代理组件

    Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍QStyled ...

  4. Ef Core花里胡哨系列(10) 动态起来的 DbContext

    Ef Core花里胡哨系列(10) 动态起来的 DbContext 我们知道,DbContext有两种托管方式,一种是AddDbContext和AddDbContextFactory,但是呢他们各有优 ...

  5. 一些JavaSE学习过程中的思路整理(三)(主观性强,持续更新中...)

    目录 一些JavaSE学习过程中的思路整理(三)(主观性强,持续更新中...) Java线程同步的几种常见情况分析 由简单到复杂的几种单例模式写法 死锁的实现与破解 使用lambda表达式化简代码 J ...

  6. Flutter PageView(轮动图)

    Flutter中的轮动图以及抖音上下滑页切换视频功能等等,这些都可以通过 PageView 轻松实现 PageView常见属性: PageView 的使用 class MyPage extends S ...

  7. 带你认识多模数据库GeminiDB架构与应用实践

    本文分享自华为云社区<多模归一,一生万物--华为云多模数据库GeminiDB架构与应用实践>,作者: GaussDB 数据库 . 在这个信息爆炸的时代,数据的管理和应用变得越来越重要.互联 ...

  8. 云图说|OLAP开源引擎的一匹黑马,MRS集群组件之ClickHouse

    摘要:ClickHouse是俄罗斯公司Yandex在2016年开源的高性能.开源联机分析列式数据库管理系统.开源后,凭借卓越的分析性能.极好的线性扩展能力和丰富的功能,被业界公认为实时分析领域OLAP ...

  9. Hyper-V CentOS Linux 硬盘扩容

    同一物理盘进行扩容(非挂载),首先在虚机内将硬盘空间扩大, Hyper-V 需要将检查点删除 查看物理卷和卷组,并将物理卷加入到卷组 #将剩余空间添加到逻辑卷 /dev/centos/root lve ...

  10. 数组递增的判断【python实现】

    有时候需要对某一组数组的数据进行判断是否 递增 的场景,比如我在开发一些体育动作场景下,某些肢体动作是需要持续朝着垂直方向向上变化,那么z轴的值是会累增的.同理,逆向考虑,递减就是它的对立面. 下面是 ...