|
@@ -1,34 +1,37 @@
|
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
|
|
+import BuildingInfoPanel from '../../../../components/BuildingInfoPanel.vue';
|
|
|
import * as THREE from 'three';
|
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
|
|
|
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
|
|
|
-
|
|
|
-
|
|
|
+const infoPanel = ref<InstanceType<typeof BuildingInfoPanel> | null>(null);
|
|
|
+const raycaster = new THREE.Raycaster()
|
|
|
+const mouse = new THREE.Vector2();
|
|
|
// 添加字体加载器
|
|
|
const fontLoader = new FontLoader();
|
|
|
const container = ref<HTMLDivElement | null>(null);
|
|
|
const width = 600;
|
|
|
const height = 400;
|
|
|
+const clickableBuildings: any = []; // 存储所有可点击建筑物
|
|
|
|
|
|
// 场景变量
|
|
|
const scene = new THREE.Scene();
|
|
|
-const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
|
+const camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
|
let controls: any = null;
|
|
|
let animateId: any = null;
|
|
|
|
|
|
// 初始化场景
|
|
|
-const initScene = () => {
|
|
|
+const initScene = (width: number, height: number) => {
|
|
|
// 设置渲染器
|
|
|
- renderer.setSize(width, height);
|
|
|
+ renderer.setSize(width, width * 2 / 3);
|
|
|
renderer.setClearColor(0x87ceeb); // 天空蓝色背景
|
|
|
renderer.shadowMap.enabled = true;
|
|
|
container.value && container.value.appendChild(renderer.domElement);
|
|
|
|
|
|
// 设置相机位置
|
|
|
- camera.position.set(0, 10, 100);
|
|
|
+ camera.position.set(0, 30, 60);
|
|
|
|
|
|
// 添加轨道控制器
|
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
@@ -66,40 +69,152 @@ const initScene = () => {
|
|
|
|
|
|
// 创建椭圆操场
|
|
|
createEllipticalPlayground(0, 0.02, 0);
|
|
|
+
|
|
|
+ // 添加篮球场
|
|
|
+ createBasketballCourt(-33, 0.02, -16);
|
|
|
+ createBasketballCourt(-33, 0.02, 0);
|
|
|
+ createBasketballCourt(-33, 0.02, 16);
|
|
|
+};
|
|
|
+
|
|
|
+// 处理鼠标点击事件
|
|
|
+const handleClick = (event: any) => {
|
|
|
+ // 将鼠标位置归一化为设备坐标 (-1到+1)
|
|
|
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
|
+ mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
|
|
|
+
|
|
|
+ // 更新射线投射器
|
|
|
+ raycaster.setFromCamera(mouse, camera);
|
|
|
+ // console.log(clickableBuildings);
|
|
|
+ // 计算与射线相交的物体
|
|
|
+ const intersects = raycaster.intersectObjects(clickableBuildings);
|
|
|
+
|
|
|
+ if (intersects.length > 0) {
|
|
|
+ // 找到第一个被点击的建筑物
|
|
|
+ const clickedBuilding = intersects[0].object;
|
|
|
+
|
|
|
+ // 执行点击后的操作
|
|
|
+ onBuildingClick(clickedBuilding);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 建筑物点击回调
|
|
|
+const onBuildingClick = (building: any) => {
|
|
|
+ console.log("点击了建筑物:", building.userData.buildingInfo);
|
|
|
+ // 这里可以显示信息面板、高亮建筑物等
|
|
|
+ highlightBuilding(building);
|
|
|
+ showBuildingInfo(building.userData.buildingInfo);
|
|
|
+};
|
|
|
+
|
|
|
+// 高亮建筑物
|
|
|
+const highlightBuilding = (building: any) => {
|
|
|
+ // 先重置所有高亮
|
|
|
+ clickableBuildings.forEach((b: any) => {
|
|
|
+ if (b.userData.originalMaterial) {
|
|
|
+ b.material = b.userData.originalMaterial;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存原始材质
|
|
|
+ if (!building.userData.originalMaterial) {
|
|
|
+ building.userData.originalMaterial = building.material;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建高亮材质
|
|
|
+ const highlightMaterial = new THREE.MeshStandardMaterial({
|
|
|
+ color: 0xffff00,
|
|
|
+ emissive: 0x888800,
|
|
|
+ metalness: 0.5,
|
|
|
+ roughness: 0.1
|
|
|
+ });
|
|
|
+
|
|
|
+ // 应用高亮材质
|
|
|
+ building.material = highlightMaterial;
|
|
|
};
|
|
|
|
|
|
+// 修改showBuildingInfo函数
|
|
|
+const showBuildingInfo = (info: any) => {
|
|
|
+ if (infoPanel.value) {
|
|
|
+ infoPanel.value.show(info);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加临时高亮动画
|
|
|
+ if (info.highlightObject) {
|
|
|
+ const originalColor = info.highlightObject.material.color.getHex();
|
|
|
+ info.highlightObject.material.color.setHex(0xffff00);
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ info.highlightObject.material.color.setHex(originalColor);
|
|
|
+ }, 2000);
|
|
|
+ }
|
|
|
+};
|
|
|
// 创建校园建筑物
|
|
|
-const createBuildings = async() => {
|
|
|
+const createBuildings = async () => {
|
|
|
// 主教学楼
|
|
|
- const mainBuilding = await createBuilding(15, 8, 10, 0xcccccc, 0, 4, 38,"教学楼");
|
|
|
+ const mainBuilding = await createBuilding(15, 8, 10, 0xcccccc, 0, 4, 38, "教学楼", {
|
|
|
+ name: "主教学楼",
|
|
|
+ function: "教学",
|
|
|
+ openTime: "07:00-22:00",
|
|
|
+ description: "校园主要教学场所,包含多个教室和实验室"
|
|
|
+ });
|
|
|
scene.add(mainBuilding);
|
|
|
// 添加左右两侧建筑
|
|
|
-
|
|
|
- const leftBuilding = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, 36);
|
|
|
+
|
|
|
+ const leftBuilding = await createBuilding(10, 5, 8, 0xcccccc, -30, 3, 36, "一号教学楼");
|
|
|
scene.add(leftBuilding);
|
|
|
- const rightBuilding = await createBuilding(10, 5, 8, 0xcccccc, 30, 4, 36);
|
|
|
+ const rightBuilding = await createBuilding(10, 5, 8, 0xcccccc, 30, 3, 36, "二号教学楼");
|
|
|
scene.add(rightBuilding);
|
|
|
|
|
|
// 右侧三栋建筑
|
|
|
- const rightBuilding1 = await createBuilding(10, 5, 8, 0xcccccc, 30, 4, 16);
|
|
|
+ const rightBuilding1 = await createBuilding(10, 5, 8, 0xcccccc, 30, 3, 16, "一号宿舍楼", {
|
|
|
+ name: "一号宿舍楼",
|
|
|
+ function: "休息",
|
|
|
+ openTime: "07:00-22:00",
|
|
|
+ description: "供学生休息"
|
|
|
+ });
|
|
|
scene.add(rightBuilding1);
|
|
|
- const rightBuilding2 = await createBuilding(10, 5, 8, 0xcccccc, 30, 4, -10);
|
|
|
+ const rightBuilding2 = await createBuilding(10, 5, 8, 0xcccccc, 30, 3, -4, "二号宿舍楼", {
|
|
|
+ name: "二号宿舍楼",
|
|
|
+ function: "休息",
|
|
|
+ openTime: "07:00-22:00",
|
|
|
+ description: "供学生休息"
|
|
|
+ });
|
|
|
scene.add(rightBuilding2);
|
|
|
- const rightBuilding3 = await createBuilding(10, 5, 8, 0xcccccc, 30, 4, -36);
|
|
|
+ const rightBuilding3 = await createBuilding(10, 5, 8, 0xcccccc, 30, 3, -24, "三号宿舍楼", {
|
|
|
+ name: "三号宿舍楼",
|
|
|
+ function: "休息",
|
|
|
+ openTime: "07:00-22:00",
|
|
|
+ description: "供学生休息"
|
|
|
+ });
|
|
|
scene.add(rightBuilding3);
|
|
|
// 左侧三栋建筑
|
|
|
- const leftBuilding1 = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, 16);
|
|
|
- scene.add(leftBuilding1);
|
|
|
- const leftBuilding2 = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, -10);
|
|
|
+ // const leftBuilding1 = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, 16);
|
|
|
+ // scene.add(leftBuilding1);
|
|
|
+ const leftBuilding2 = await createBuilding(10, 5, 8, 0xcccccc, -0, 3, -35);
|
|
|
scene.add(leftBuilding2);
|
|
|
- const leftBuilding3 = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, -36);
|
|
|
+ const leftBuilding3 = await createBuilding(10, 5, 8, 0xcccccc, -30, 3, -35);
|
|
|
scene.add(leftBuilding3);
|
|
|
// 添加窗户细节
|
|
|
addBuildingDetails();
|
|
|
};
|
|
|
|
|
|
// 创建建筑物通用函数
|
|
|
-const createBuilding = async(width: any, height: any, depth: any, color: any, x: any, y: any, z: any,name: any="") => {
|
|
|
+type BuildingInfo = {
|
|
|
+ name?: string;
|
|
|
+ function?: string;
|
|
|
+ openTime?: string;
|
|
|
+ [key: string]: any;
|
|
|
+};
|
|
|
+const createBuilding = async (
|
|
|
+ width: any,
|
|
|
+ height: any,
|
|
|
+ depth: any,
|
|
|
+ color: any,
|
|
|
+ x: any,
|
|
|
+ y: any,
|
|
|
+ z: any,
|
|
|
+ name: any = "",
|
|
|
+ buildingInfo: BuildingInfo = {}
|
|
|
+) => {
|
|
|
const geometry = new THREE.BoxGeometry(width, height, depth);
|
|
|
const material = new THREE.MeshStandardMaterial({
|
|
|
color: color,
|
|
@@ -111,15 +226,29 @@ const createBuilding = async(width: any, height: any, depth: any, color: any, x:
|
|
|
building.receiveShadow = true;
|
|
|
// 添加建筑物名称
|
|
|
if (name) {
|
|
|
+ building.name = name;
|
|
|
const text = await create3DText(name, {
|
|
|
size: 2,
|
|
|
height: 0.1,
|
|
|
color: 0xffffff,
|
|
|
- position: { x: x, y: y + height/2 + 0.5, z: z },
|
|
|
- rotation: { x: 0, y: 0, z: 0 }
|
|
|
+ position: { x: x, y: y + height / 2 + 0.5, z: z },
|
|
|
+ rotation: { x: 0, y: 0, z: 0 },
|
|
|
});
|
|
|
scene.add(text);
|
|
|
}
|
|
|
+ // 添加可点击属性
|
|
|
+ building.userData = {
|
|
|
+ isBuilding: true,
|
|
|
+ buildingInfo: {
|
|
|
+ name: buildingInfo.name || "未命名建筑",
|
|
|
+ function: buildingInfo.function || "教学/办公",
|
|
|
+ openTime: buildingInfo.openTime || "08:00-22:00",
|
|
|
+ ...buildingInfo
|
|
|
+ }
|
|
|
+ };
|
|
|
+ clickableBuildings.push(building);
|
|
|
+ // console.log("添加可点击属性", building)
|
|
|
+
|
|
|
return building;
|
|
|
};
|
|
|
|
|
@@ -138,14 +267,14 @@ const create3DText = async (message: any, options: TextOptions = {}) => {
|
|
|
height = 0.2,
|
|
|
color = 0xffffff,
|
|
|
position = { x: 0, y: 0, z: 0 },
|
|
|
- rotation = { x: 0, y: 0, z: 0 }
|
|
|
+ rotation = { x: 0, y: 0, z: 0 },
|
|
|
} = options;
|
|
|
-
|
|
|
+
|
|
|
// 加载字体
|
|
|
const font = await new Promise<any>((resolve) => {
|
|
|
fontLoader.load('/FangSong_Regular.json', (font) => resolve(font));
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
// 创建文字几何体
|
|
|
const textGeometry = new TextGeometry(message, {
|
|
|
font: font,
|
|
@@ -154,20 +283,20 @@ const create3DText = async (message: any, options: TextOptions = {}) => {
|
|
|
curveSegments: 12,
|
|
|
bevelEnabled: false
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
// 居中文字
|
|
|
textGeometry.computeBoundingBox();
|
|
|
- const textWidth = textGeometry.boundingBox &&textGeometry.boundingBox.max.x - textGeometry.boundingBox.min.x;
|
|
|
- if(textWidth){
|
|
|
+ const textWidth = textGeometry.boundingBox && textGeometry.boundingBox.max.x - textGeometry.boundingBox.min.x;
|
|
|
+ if (textWidth) {
|
|
|
textGeometry.translate(-textWidth / 2, 0, 0);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const textMaterial = new THREE.MeshStandardMaterial({ color });
|
|
|
const textMesh = new THREE.Mesh(textGeometry, textMaterial);
|
|
|
-
|
|
|
+
|
|
|
textMesh.position.set(position.x, position.y, position.z);
|
|
|
textMesh.rotation.set(rotation.x, rotation.y, rotation.z);
|
|
|
-
|
|
|
+
|
|
|
return textMesh;
|
|
|
};
|
|
|
|
|
@@ -325,6 +454,69 @@ const createEnvironment = () => {
|
|
|
entranceRoadLine3.position.z = 25;
|
|
|
scene.add(entranceRoadLine3);
|
|
|
|
|
|
+ // 右侧建筑物道路
|
|
|
+ const roadMaterial4 = new THREE.MeshStandardMaterial({ color: 0xdddddd });
|
|
|
+ const entranceRoad4 = new THREE.Mesh(
|
|
|
+ new THREE.PlaneGeometry(1.5, 90),
|
|
|
+ roadMaterial4
|
|
|
+ );
|
|
|
+ entranceRoad4.rotation.x = -Math.PI / 2;
|
|
|
+ entranceRoad4.position.y = 0.01;
|
|
|
+ entranceRoad4.position.z = 0;
|
|
|
+ entranceRoad4.position.x = 30.5;
|
|
|
+ scene.add(entranceRoad4);
|
|
|
+
|
|
|
+ const entranceRoad5 = new THREE.Mesh(
|
|
|
+ new THREE.PlaneGeometry(70, 1.5),
|
|
|
+ roadMaterial4
|
|
|
+ );
|
|
|
+ entranceRoad5.rotation.x = -Math.PI / 2;
|
|
|
+ entranceRoad5.position.y = 0.01;
|
|
|
+ entranceRoad5.position.z = 0;
|
|
|
+ entranceRoad5.position.x = -10;
|
|
|
+ entranceRoad5.position.z = -25;
|
|
|
+ scene.add(entranceRoad5);
|
|
|
+
|
|
|
+ const entranceRoad6 = new THREE.Mesh(
|
|
|
+ new THREE.PlaneGeometry(1.5, 30),
|
|
|
+ roadMaterial4
|
|
|
+ );
|
|
|
+ entranceRoad6.rotation.x = -Math.PI / 2;
|
|
|
+ entranceRoad6.position.y = 0.01;
|
|
|
+ entranceRoad6.position.z = 0;
|
|
|
+ entranceRoad6.position.z = -30;
|
|
|
+ scene.add(entranceRoad6);
|
|
|
+
|
|
|
+ const entranceRoad7 = new THREE.Mesh(
|
|
|
+ new THREE.PlaneGeometry(1.5, 10),
|
|
|
+ roadMaterial4
|
|
|
+ );
|
|
|
+ entranceRoad7.rotation.x = -Math.PI / 2;
|
|
|
+ entranceRoad7.position.y = 0.01;
|
|
|
+ entranceRoad7.position.z = -30;
|
|
|
+ entranceRoad7.position.x = -30;
|
|
|
+ scene.add(entranceRoad7);
|
|
|
+
|
|
|
+ const entranceRoad8 = new THREE.Mesh(
|
|
|
+ new THREE.PlaneGeometry(10, 1.5),
|
|
|
+ roadMaterial4
|
|
|
+ );
|
|
|
+ entranceRoad8.rotation.x = -Math.PI / 2;
|
|
|
+ entranceRoad8.position.y = 0.01;
|
|
|
+ entranceRoad8.position.z = 35;
|
|
|
+ entranceRoad8.position.x = 40;
|
|
|
+ scene.add(entranceRoad8);
|
|
|
+
|
|
|
+ const entranceRoad9 = new THREE.Mesh(
|
|
|
+ new THREE.PlaneGeometry(10, 1.5),
|
|
|
+ roadMaterial4
|
|
|
+ );
|
|
|
+ entranceRoad9.rotation.x = -Math.PI / 2;
|
|
|
+ entranceRoad9.position.y = 0.01;
|
|
|
+ entranceRoad9.position.z = 35;
|
|
|
+ entranceRoad9.position.x = -40;
|
|
|
+ scene.add(entranceRoad9);
|
|
|
+
|
|
|
// 添加树木
|
|
|
// for (let i = -40; i <= 40; i += 10) {
|
|
|
// for (let j = -40; j <= 40; j += 10) {
|
|
@@ -362,14 +554,28 @@ const createTree = (x: any, y: any, z: any) => {
|
|
|
scene.add(leaves);
|
|
|
};
|
|
|
// 创建椭圆操场
|
|
|
-const createEllipticalPlayground = (x:any, y:any, z:any) => {
|
|
|
+const createEllipticalPlayground = (x: any, y: any, z: any) => {
|
|
|
+
|
|
|
+ // const PlaneGeometry = new THREE.PlaneGeometry(25, 35);
|
|
|
+ // const PlaneMaterial = new THREE.MeshStandardMaterial({
|
|
|
+ // color: 0xdddddd,
|
|
|
+ // transparent:true,//开启透明
|
|
|
+ // opacity:0.5,//设置透明度
|
|
|
+ // // side: THREE.DoubleSide
|
|
|
+ // });
|
|
|
+ // const Plane = new THREE.Mesh(PlaneGeometry, PlaneMaterial);
|
|
|
+ // Plane.rotation.x = -Math.PI / 2;
|
|
|
+ // Plane.position.set(x, y + 0.01, z);
|
|
|
+ // Plane.receiveShadow = true;
|
|
|
+ // scene.add(Plane);
|
|
|
+
|
|
|
// 椭圆跑道参数
|
|
|
const a = 13; // 长轴半径
|
|
|
const b = 18; // 短轴半径
|
|
|
const trackWidth = 3; // 跑道宽度
|
|
|
const innerA = a - trackWidth; // 内椭圆长轴
|
|
|
const innerB = b - trackWidth; // 内椭圆短轴
|
|
|
-
|
|
|
+
|
|
|
// 创建椭圆跑道
|
|
|
const trackGeometry = new THREE.EllipseCurve(
|
|
|
0, 0, // 中心点
|
|
@@ -378,21 +584,21 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
|
|
|
false, // 是否顺时针
|
|
|
0 // 旋转角度
|
|
|
);
|
|
|
-
|
|
|
+
|
|
|
const innerTrackGeometry = new THREE.EllipseCurve(
|
|
|
0, 0,
|
|
|
innerA, innerB,
|
|
|
0, 2 * Math.PI,
|
|
|
false,
|
|
|
- 0
|
|
|
+ 0,
|
|
|
);
|
|
|
-
|
|
|
+
|
|
|
const trackPoints = trackGeometry.getPoints(100);
|
|
|
const innerTrackPoints = innerTrackGeometry.getPoints(100);
|
|
|
-
|
|
|
+
|
|
|
// 合并点集形成跑道形状
|
|
|
const allPoints = trackPoints.concat(innerTrackPoints.reverse());
|
|
|
-
|
|
|
+
|
|
|
// 创建跑道形状
|
|
|
const trackShape = new THREE.Shape();
|
|
|
trackShape.moveTo(trackPoints[0].x, trackPoints[0].y);
|
|
@@ -404,16 +610,16 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
|
|
|
trackShape.lineTo(innerTrackPoints[i].x, innerTrackPoints[i].y);
|
|
|
}
|
|
|
trackShape.lineTo(trackPoints[0].x, trackPoints[0].y);
|
|
|
-
|
|
|
+
|
|
|
// 创建跑道网格
|
|
|
const trackExtrudeSettings = {
|
|
|
steps: 1,
|
|
|
depth: 0.1,
|
|
|
bevelEnabled: false
|
|
|
};
|
|
|
-
|
|
|
+
|
|
|
const trackGeometry3D = new THREE.ExtrudeGeometry(trackShape, trackExtrudeSettings);
|
|
|
- const trackMaterial = new THREE.MeshStandardMaterial({
|
|
|
+ const trackMaterial = new THREE.MeshStandardMaterial({
|
|
|
color: 0x654321,
|
|
|
roughness: 0.9
|
|
|
});
|
|
@@ -421,23 +627,23 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
|
|
|
track.rotation.x = -Math.PI / 2;
|
|
|
track.position.set(x, y, z);
|
|
|
scene.add(track);
|
|
|
-
|
|
|
+
|
|
|
// 创建内部足球场
|
|
|
// 使用CircleGeometry并通过缩放实现椭圆
|
|
|
const fieldGeometry = new THREE.CircleGeometry(1, 100);
|
|
|
- const fieldMaterial = new THREE.MeshStandardMaterial({
|
|
|
+ const fieldMaterial = new THREE.MeshStandardMaterial({
|
|
|
color: 0x228B22,
|
|
|
- side: THREE.DoubleSide
|
|
|
+ // side: THREE.DoubleSide
|
|
|
});
|
|
|
const field = new THREE.Mesh(fieldGeometry, fieldMaterial);
|
|
|
field.scale.set(innerA, innerB, 1); // 椭圆缩放
|
|
|
field.rotation.x = -Math.PI / 2;
|
|
|
field.position.set(x, y + 0.01, z);
|
|
|
scene.add(field);
|
|
|
-
|
|
|
+
|
|
|
// 添加足球场标记线
|
|
|
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
|
|
|
-
|
|
|
+
|
|
|
// 中圈
|
|
|
const centerCircleGeometry = new THREE.CircleGeometry(1, 32);
|
|
|
const centerCircle = new THREE.Line(
|
|
@@ -448,7 +654,7 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
|
|
|
centerCircle.rotation.x = -Math.PI / 2;
|
|
|
centerCircle.position.set(x, y + 0.02, z);
|
|
|
scene.add(centerCircle);
|
|
|
-
|
|
|
+
|
|
|
// 中线
|
|
|
const points = [];
|
|
|
points.push(new THREE.Vector3(-innerA, 0, 0));
|
|
@@ -458,9 +664,166 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
|
|
|
midline.rotation.x = -Math.PI / 2;
|
|
|
midline.position.set(x, y + 0.02, z);
|
|
|
scene.add(midline);
|
|
|
-
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
return { track, field };
|
|
|
};
|
|
|
+// 创建篮球场(尺寸调整为20x10)
|
|
|
+const createBasketballCourt = (x: any, y: any, z: any, rotationY = 0) => {
|
|
|
+ // 篮球场尺寸 (调整为20x10)
|
|
|
+ const courtLength = 20;
|
|
|
+ const courtWidth = 10;
|
|
|
+ const lineWidth = 0.2;
|
|
|
+
|
|
|
+ // 创建篮球场地板
|
|
|
+ const courtGeometry = new THREE.PlaneGeometry(courtLength, courtWidth);
|
|
|
+ const courtMaterial = new THREE.MeshStandardMaterial({
|
|
|
+ color: 0x1a3b6e, // 深蓝色
|
|
|
+ roughness: 0.7,
|
|
|
+ metalness: 0.1
|
|
|
+ });
|
|
|
+ const court = new THREE.Mesh(courtGeometry, courtMaterial);
|
|
|
+ court.rotation.x = -Math.PI / 2;
|
|
|
+ court.position.set(x, y, z);
|
|
|
+ court.rotation.y = rotationY;
|
|
|
+ court.receiveShadow = true;
|
|
|
+ scene.add(court);
|
|
|
+
|
|
|
+ // 篮球场边界线
|
|
|
+ const borderLineGeometry = new THREE.EdgesGeometry(new THREE.BoxGeometry(courtLength, 0.1, courtWidth));
|
|
|
+ const borderLineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 });
|
|
|
+ const borderLine = new THREE.LineSegments(borderLineGeometry, borderLineMaterial);
|
|
|
+ borderLine.position.set(x, y + 0.01, z);
|
|
|
+ borderLine.rotation.y = rotationY;
|
|
|
+ scene.add(borderLine);
|
|
|
+
|
|
|
+ // 中线
|
|
|
+ const midLineGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
|
+ new THREE.Vector3(0, 0.02, -courtWidth / 2),
|
|
|
+ new THREE.Vector3(0, 0.02, courtWidth / 2)
|
|
|
+ ]);
|
|
|
+ const midLine = new THREE.Line(midLineGeometry, borderLineMaterial);
|
|
|
+ midLine.position.set(x, y + 0.02, z);
|
|
|
+ midLine.rotation.y = rotationY;
|
|
|
+ scene.add(midLine);
|
|
|
+
|
|
|
+ // 中圈 (按比例缩小)
|
|
|
+ const centerCircleGeometry = new THREE.RingGeometry(1.2, 1.4, 32);
|
|
|
+ const centerCircleMaterial = new THREE.MeshBasicMaterial({
|
|
|
+ color: 0xffffff,
|
|
|
+ side: THREE.DoubleSide
|
|
|
+ });
|
|
|
+ const centerCircle = new THREE.Mesh(centerCircleGeometry, centerCircleMaterial);
|
|
|
+ centerCircle.rotation.x = -Math.PI / 2;
|
|
|
+ centerCircle.position.set(x, y + 0.02, z);
|
|
|
+ centerCircle.rotation.y = rotationY;
|
|
|
+ scene.add(centerCircle);
|
|
|
+
|
|
|
+ // 三分线 (按比例调整)
|
|
|
+ const threePointLineGeometry = new THREE.EllipseCurve(
|
|
|
+ 0, 0,
|
|
|
+ 5.7, 5.7, // 缩小三分线半径
|
|
|
+ -Math.PI * 0.4, Math.PI * 0.4,
|
|
|
+ false,
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ const threePointPoints = threePointLineGeometry.getPoints(50);
|
|
|
+ const threePointLine = new THREE.Line(
|
|
|
+ new THREE.BufferGeometry().setFromPoints(threePointPoints),
|
|
|
+ borderLineMaterial
|
|
|
+ );
|
|
|
+ threePointLine.position.set(x - courtLength / 2 + 1.2, y + 0.02, z); // 调整位置
|
|
|
+ threePointLine.rotation.x = Math.PI / 2;
|
|
|
+ threePointLine.rotation.y = rotationY;
|
|
|
+ scene.add(threePointLine);
|
|
|
+ // 三分线 (按比例调整)
|
|
|
+ const threePointLineGeometry1 = new THREE.EllipseCurve(
|
|
|
+ 0, 0,
|
|
|
+ -5.7, 5.7, // 缩小三分线半径
|
|
|
+ -Math.PI * 0.4, Math.PI * 0.4,
|
|
|
+ false,
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ const threePointPoints1 = threePointLineGeometry1.getPoints(50);
|
|
|
+ const threePointLine1 = new THREE.Line(
|
|
|
+ new THREE.BufferGeometry().setFromPoints(threePointPoints1),
|
|
|
+ borderLineMaterial
|
|
|
+ );
|
|
|
+ threePointLine1.position.set(x + courtLength / 2 - 1.2, y + 0.02, z); // 调整位置
|
|
|
+ threePointLine1.rotation.x = Math.PI / 2;
|
|
|
+ threePointLine1.rotation.y = rotationY;
|
|
|
+ scene.add(threePointLine1);
|
|
|
+
|
|
|
+ // 篮板和篮筐
|
|
|
+ const createBackboard = (offsetX: any) => {
|
|
|
+ // 篮板 (尺寸也相应缩小)
|
|
|
+ const backboardGeometry = new THREE.BoxGeometry(1.5, 1, 0.05);
|
|
|
+ const backboardMaterial = new THREE.MeshStandardMaterial({
|
|
|
+ color: 0xffffff,
|
|
|
+ transparent: true,
|
|
|
+ opacity: 0.3,
|
|
|
+ metalness: 0.9,
|
|
|
+ roughness: 0.1
|
|
|
+ });
|
|
|
+ const backboard = new THREE.Mesh(backboardGeometry, backboardMaterial);
|
|
|
+ backboard.position.set(
|
|
|
+ x + offsetX * (courtLength / 2 - 0.4), // 调整位置
|
|
|
+ y + 2.5, // 降低高度
|
|
|
+ z
|
|
|
+ );
|
|
|
+ backboard.rotation.y = rotationY + (offsetX > 0 ? Math.PI : 0);
|
|
|
+ scene.add(backboard);
|
|
|
+
|
|
|
+ // 篮筐支架 (缩短)
|
|
|
+ const poleGeometry = new THREE.CylinderGeometry(0.05, 0.05, 2.5, 16);
|
|
|
+ const poleMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc });
|
|
|
+ const pole = new THREE.Mesh(poleGeometry, poleMaterial);
|
|
|
+ pole.position.set(
|
|
|
+ x + offsetX * (courtLength / 2 - 0.4),
|
|
|
+ y + 1.25,
|
|
|
+ z
|
|
|
+ );
|
|
|
+ scene.add(pole);
|
|
|
+
|
|
|
+ // 篮筐 (环)
|
|
|
+ const hoopGeometry = new THREE.TorusGeometry(0.2, 0.02, 16, 32);
|
|
|
+ const hoopMaterial = new THREE.MeshStandardMaterial({ color: 0xff7700 });
|
|
|
+ const hoop = new THREE.Mesh(hoopGeometry, hoopMaterial);
|
|
|
+ hoop.position.set(
|
|
|
+ x + offsetX * (courtLength / 2 - 0.4),
|
|
|
+ y + 2.55,
|
|
|
+ z
|
|
|
+ );
|
|
|
+ hoop.rotation.x = Math.PI / 2;
|
|
|
+ hoop.rotation.y = rotationY + (offsetX > 0 ? Math.PI : 0);
|
|
|
+ scene.add(hoop);
|
|
|
+
|
|
|
+ // 篮网 (缩小)
|
|
|
+ const netGeometry = new THREE.ConeGeometry(0.2, 0.35, 8);
|
|
|
+ const netMaterial = new THREE.MeshStandardMaterial({
|
|
|
+ color: 0xffffff,
|
|
|
+ wireframe: true,
|
|
|
+ transparent: true,
|
|
|
+ opacity: 0.7
|
|
|
+ });
|
|
|
+ const net = new THREE.Mesh(netGeometry, netMaterial);
|
|
|
+ net.position.set(
|
|
|
+ x + offsetX * (courtLength / 2 - 0.4),
|
|
|
+ y + 2.4,
|
|
|
+ z
|
|
|
+ );
|
|
|
+ net.rotation.x = Math.PI;
|
|
|
+ net.rotation.y = rotationY + (offsetX > 0 ? Math.PI : 0);
|
|
|
+ scene.add(net);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 创建两个篮板
|
|
|
+ createBackboard(1); // 正方向篮板
|
|
|
+ createBackboard(-1); // 反方向篮板
|
|
|
+
|
|
|
+ return court;
|
|
|
+};
|
|
|
|
|
|
// 动画循环
|
|
|
const animate = () => {
|
|
@@ -471,19 +834,75 @@ const animate = () => {
|
|
|
|
|
|
// 处理窗口大小变化
|
|
|
const onWindowResize = () => {
|
|
|
- camera.aspect = width / height;
|
|
|
+ camera.aspect = 3 / 2;
|
|
|
camera.updateProjectionMatrix();
|
|
|
- renderer.setSize(width, height);
|
|
|
+
|
|
|
+ let containerWidth = width;
|
|
|
+ let containerHeight = height;
|
|
|
+ if (container.value) {
|
|
|
+ containerWidth = container.value.clientWidth || width;
|
|
|
+ containerHeight = container.value.clientHeight || height;
|
|
|
+ }
|
|
|
+ renderer.setSize(containerWidth, containerWidth * (2 / 3));
|
|
|
+};
|
|
|
+// 添加鼠标移动事件处理
|
|
|
+const onDocumentMouseMove = (event: any) => {
|
|
|
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
|
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
|
+
|
|
|
+ raycaster.setFromCamera(mouse, camera);
|
|
|
+ const intersects = raycaster.intersectObjects(scene.children, true);
|
|
|
+
|
|
|
+ // 重置所有建筑物的悬停状态
|
|
|
+ scene.traverse((obj: any) => {
|
|
|
+ if (obj.userData.isBuilding && obj.userData.originalColor) {
|
|
|
+ obj.material.color.setHex(obj.userData.originalColor);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (intersects.length > 0) {
|
|
|
+ const hoveredObject = intersects[0].object;
|
|
|
+ if (hoveredObject.userData.isBuilding) {
|
|
|
+ // 存储原始颜色
|
|
|
+ if (!hoveredObject.userData.originalColor) {
|
|
|
+ hoveredObject.userData.originalColor = hoveredObject.material.color.getHex();
|
|
|
+ }
|
|
|
+ // 设置悬停颜色
|
|
|
+ hoveredObject.material.color.setHex(0xaaaaaa);
|
|
|
+
|
|
|
+ // 更改鼠标指针
|
|
|
+ if (container.value) {
|
|
|
+ container.value.style.cursor = 'pointer';
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (container.value) {
|
|
|
+ container.value.style.cursor = 'default';
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
onMounted(() => {
|
|
|
- initScene();
|
|
|
- animate();
|
|
|
+
|
|
|
+ // 获取容器宽高
|
|
|
+ let containerWidth = width;
|
|
|
+ let containerHeight = height;
|
|
|
+ if (container.value) {
|
|
|
+ containerWidth = container.value.clientWidth || width;
|
|
|
+ containerHeight = container.value.clientHeight || height;
|
|
|
+ initScene(containerWidth, containerHeight);
|
|
|
+ animate();
|
|
|
+ }
|
|
|
+
|
|
|
window.addEventListener('resize', onWindowResize);
|
|
|
+ window.addEventListener('click', handleClick, false);
|
|
|
+ window.addEventListener('mousemove', onDocumentMouseMove, false);
|
|
|
});
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
window.removeEventListener('resize', onWindowResize);
|
|
|
+ window.removeEventListener('mousemove', onDocumentMouseMove, false);
|
|
|
+ window.removeEventListener('click', handleClick, false);
|
|
|
cancelAnimationFrame(animateId);
|
|
|
if (container.value && container.value.contains(renderer.domElement)) {
|
|
|
container.value.removeChild(renderer.domElement);
|
|
@@ -492,7 +911,9 @@ onBeforeUnmount(() => {
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <div ref="container" id="webglContainer" style=""></div>
|
|
|
+ <div ref="container" id="webglContainer" style="width: 100%; height: 100%;">
|
|
|
+ <BuildingInfoPanel ref="infoPanel" />
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<style scoped lang="less">
|