Threejs实现立体3D园区解决方案及代码

一、实现方案

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

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

二、实现步奏

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. 拐弯添加的点不多,所以在拐弯处速度较快

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

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

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

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

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

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

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

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

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

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

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

四、完整的代码

  为了简单点,没有用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 { 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
相关推荐
某林2121 天前
从硬件解耦、3D公差设计到ROS 2柔性导航
人工智能·3d·机器人·ros2·技术复盘
逸模1 天前
逸模智造・BIM 效能专栏一|极速土建翻模,2D 一键跨越 3D
笔记·3d·bim·土建·翻模
陈天伟教授2 天前
eDrawings 2026 x64 Edition-免费 3D 模型和 2D 工程图查看器
3d
Tian_Hang3 天前
eclipse ditto 学习笔记
运维·服务器·开发语言·javascript·3d
AI视觉网奇3 天前
BambuStudio 编译实战 2026
3d
AI前沿资讯3 天前
AI3D角色生产如何减少返工?用 V2Fun 前移建模与动画流程
人工智能·3d
蓝速科技3 天前
蓝速科技视觉 3D 全息舱 AI 数字人一体机带灯与无灯款深度评测
人工智能·科技·3d
尘中远3 天前
【Qwt 7.0 系列】3D 数据可视化 —— OpenGL 高性能三维绘图
qt·3d·qcustomplot·qwt·科学绘图·高性能绘图
林恒smileZAZ4 天前
Three.js 3D 地图特效与材质实现指南
javascript·3d·材质
蓝速科技4 天前
蓝速科技 3D 全息舱 AI 数字人博物馆导览效果实录
人工智能·科技·3d