一、实现方案
单独贴代码可能容易混乱,所以这里只讲实现思路,代码放在最后汇总了下。
想要实现一个简单的工业园区、主要包含的内容是一个大楼、左右两片停车位、四条道路以及多个可在道路上随机移动的车辆、遇到停车位时随机选择是否要停车,简单设计图如下
二、实现步奏
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 = moveOb