Threejs实现一个园区

时间:2024-01-22 19:55:55
/** * 办公园区 */ 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();