一、實現方案 單獨貼代碼可能容易混亂,所以這裡只講實現思路,代碼放在最後彙總了下。 想要實現一個簡單的工業園區、主要包含的內容是一個大樓、左右兩片停車位、四條道路以及多個可在道路上隨機移動的車輛、遇到停車位時隨機選擇是否要停車,簡單設計圖如下 二、實現步奏 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