Threejs實現一個園區

来源:https://www.cnblogs.com/codeOnMar/p/17968150
-Advertisement-
Play Games

一、實現方案 單獨貼代碼可能容易混亂,所以這裡只講實現思路,代碼放在最後彙總了下。 想要實現一個簡單的工業園區、主要包含的內容是一個大樓、左右兩片停車位、四條道路以及多個可在道路上隨機移動的車輛、遇到停車位時隨機選擇是否要停車,簡單設計圖如下 二、實現步奏 2.1 引入環境,天空和地面 引入天空有三 ...


       

一、實現方案

單獨貼代碼可能容易混亂,所以這裡只講實現思路,代碼放在最後彙總了下。

想要實現一個簡單的工業園區、主要包含的內容是一個大樓、左右兩片停車位、四條道路以及多個可在道路上隨機移動的車輛、遇到停車位時隨機選擇是否要停車,簡單設計圖如下

二、實現步奏

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	   

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Ansible命令格式和常用模塊介紹 Ansible命令格式介紹 Ansible命令格式 ansible [群組名] -m [模塊名] -a [參數] Ansible命令返回值的顏色表示 綠色:代表對遠程節點不進行相應修改,沒有發生改變,命令執行成功 黃色:代表對遠程節點進行了相應的修改,命令執行成 ...
  • 什麼是大數據 大數據(Big Data)是指在傳統數據處理方法難以處理的情況下,需要新的處理模式來具有更強的決策力、洞察發現力和過程優化能力的海量、高增長率和多樣化的信息資產。大數據的特征通常被概括為“4V”,即: Volume(容量):大數據的規模非常龐大,通常以 TB(太位元組)、PB(拍位元組)或 ...
  • 當前隨著企業內外部數據源的不斷擴展和積累,數據呈現出大規模、多樣化、質量參差不齊等顯著特征。如何有效激活這些結構複雜且類型多樣的數據資產,挖掘其深層價值,已成為眾多企業亟待解決的實際挑戰。 袋鼠雲數棧作為新一代一站式大數據基礎軟體,其核心優勢在於不僅提供了快速便捷、易於上手的底層數據開發模塊,更推出 ...
  • 簡介 CloudCanal 推出 跨互聯網安全數據同步 方案之後,有一些商業客戶落地,效果良好,不過客戶也反饋了一些改進和新需求,其中最大的一個需求即雙向同步防迴圈。 近期 CloudCanal 版本支持了這個特性,整體方案進一步升級,最大特點包括: 兩端資料庫完全不開放公網埠 兩端資料庫可雙向同 ...
  • 本文介紹瞭如何在Python / pyspark環境中使用graphx進行圖計算。通過結合Python / pyspark和graphx,可以輕鬆進行圖分析和處理。首先需要安裝Spark和pyspark包,然後配置環境變數。接著介紹了GraphFrames的安裝和使用,包括創建圖數據結構、計算節點的... ...
  • 華為攜手伙伴和開發者駛向更廣闊的的未來,一起打造一個高級簡約、極致流暢、隱私安全、開放共贏的新生態體系。 ...
  • 該組件還有一個名為 `selectable` 的屬性,用於控制選項是否可選。如果要實現選項變為灰色且不可選的效果,需要同時將選項的 `disabled` 屬性設置為 `true`,將 `seletable` 屬性設置為 `false`。 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 翻轉圖像是在視覺上比較兩個不同圖像的常用方法。單擊其中一個將翻轉它,並顯示另一個圖像。 佈局 佈局結構如下: <div class="flipping-images"> <div class="flipping-images__inner ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...