Explorar el Código

增加第一人称室内视角

WQQ hace 12 horas
padre
commit
6337a26c68

+ 0 - 105
RuoYi-Vue3/src/supermap-cesium-module/business-scenes/JuKouShuiZhaScene.js

@@ -1,11 +1,6 @@
 // 导入纹理和材质模块
 import foamTextureUrl from '../../texture/T_Waterfall_Foam.PNG';
-import waterNormal1Url from '../../texture/Water_1_Normal.PNG';
-import waterNormal2Url from '../../texture/Water_2_Normal.PNG';
-import waterNormal3Url from '../../texture/Water_3_Normal.PNG';
-import skyTextureUrl from '../../texture/sky.jpg';
 import { createWaterFoamPrimitive } from '../../materials/WaterFoamMaterial.js';
-import { createStylizedWaterMaterial, destroyStylizedWaterAnimation } from '../../materials/StylizedWaterMaterial.js';
 // 导入启闭机图标
 import qibiijiIcon from '../../assets/images/启闭机.png';
 
@@ -38,12 +33,6 @@ export const JuKouSceneConfig = {
     coordinates: '119.144345, 25.866551,24.5',
     size: 20  // 20米x20米
   },
-  // 简单面片(无材质,仅用于标记位置)
-  simplePlane: {
-    name: '标记面片',
-    coordinates: '119.148567, 25.871270,40.95',
-    size: 100  // 20米x20米
-  },
   camera: {
     position: [-2795169.7727823164,5012356.518227473,2783038.1732437233],
     direction: [0.5111708396854006, 0.3975398306001059, -0.7620147345962293],
@@ -67,8 +56,6 @@ export class JuKouShuiZhaController {
     this.planeEntities = [];        // 所有闸门面片实体数组
     this._planeAnimationHandlers = [];  // 所有面片动画处理器
     this._gatePlaneMap = new Map();     // gateName → { primitive, timeoutId }
-    this._simplePlanePrimitive = null;  // 简单标记面片
-    this._simplePlaneAnimHandler = null;  // 简单标记面片动画处理器
     this.gateStates = {};
     this.isInitialized = false;
     this.isModelLoaded = false;
@@ -96,12 +83,6 @@ export class JuKouShuiZhaController {
     try {
       await this.loadModel();
       this.loadPlane();       // 加载面片模型(闸门水泡沫面片)
-      try {
-        this.loadSimplePlane(); // 加载简单标记面片
-      } catch (err2) {
-        console.error('❌ loadSimplePlane 失败', err2);
-        this.onError && this.onError('loadSimplePlane: ' + err2.message);
-      }
       this.isInitialized = true;
       this.onSceneReady && this.onSceneReady(this.entity);
     } catch (err) {
@@ -293,82 +274,6 @@ export class JuKouShuiZhaController {
     console.log(`✅ 共加载 ${this.planeEntities.length} 个闸门面片, 尺寸: ${size}米×${size}米`);
   }
 
-  // ============================================================
-  // 加载水面面片(使用风格化水面材质 + 法线纹理)
-  // ============================================================
-  loadSimplePlane() {
-    const Cesium = window.Cesium;
-    const cfg = JuKouSceneConfig.simplePlane;
-    const size = cfg.size;  // 20米
-    const coords = cfg.coordinates.split(',').map(Number);
-    const centerLon = coords[0];
-    const centerLat = coords[1];
-    const altitude = coords[2];
-
-    // 清除之前已创建的简单面片
-    if (this._simplePlanePrimitive) {
-      this.viewer.scene.primitives.remove(this._simplePlanePrimitive);
-      this._simplePlanePrimitive = null;
-    }
-
-    // 计算矩形边界
-    const latOffset = size / 111000;
-    const lonOffset = size / (111000 * Math.cos(centerLat * Math.PI / 180));
-    const west = centerLon - lonOffset / 2;
-    const east = centerLon + lonOffset / 2;
-    const south = centerLat - latOffset / 2;
-    const north = centerLat + latOffset / 2;
-
-    const rectangle = Cesium.Rectangle.fromDegrees(west, south, east, north);
-
-    // 创建矩形几何
-    const rectangleGeometry = new Cesium.RectangleGeometry({
-      rectangle: rectangle,
-      height: altitude,
-      vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat
-    });
-
-    const geometryInstance = new Cesium.GeometryInstance({
-      geometry: rectangleGeometry
-    });
-
-    // 创建风格化水面材质(传入纹理 URL + 天空纹理)
-    const waterMaterial = createStylizedWaterMaterial({
-      alpha: 0.85,
-      waveHeight: 0.6,
-      flowSpeed: 0.5,
-      foamIntensity: 0.25,
-      normalMap1: waterNormal1Url,
-      normalMap2: waterNormal2Url,
-      normalMap3: waterNormal3Url,
-      skyTexture: skyTextureUrl
-    });
-
-    // 创建 Primitive
-    this._simplePlanePrimitive = this.viewer.scene.primitives.add(
-      new Cesium.Primitive({
-        geometryInstances: geometryInstance,
-        appearance: new Cesium.MaterialAppearance({
-          material: waterMaterial,
-          flat: true,
-          aboveGround: true
-        }),
-        asynchronous: false
-      })
-    );
-
-    // 启动动画
-    const startTime = Date.now();
-    this._simplePlaneAnimHandler = this.viewer.scene.preRender.addEventListener(() => {
-      if (this._simplePlanePrimitive && waterMaterial.uniforms) {
-        const time = (Date.now() - startTime) / 1000;
-        waterMaterial.uniforms.uSwTime = time;
-      }
-    });
-
-    console.log(`✅ 水面面片已创建: (${cfg.coordinates}), 尺寸: ${size}米×${size}米`);
-  }
-
   // 清除所有已创建的面片
   _clearPlanes() {
     // 停止所有面片动画
@@ -539,16 +444,6 @@ export class JuKouShuiZhaController {
       });
       this.planeEntities = [];
     }
-    // 停止简单标记面片动画
-    if (this._simplePlaneAnimHandler) {
-      destroyStylizedWaterAnimation(this._simplePlaneAnimHandler);
-      this._simplePlaneAnimHandler = null;
-    }
-    // 移除简单标记面片
-    if (this._simplePlanePrimitive) {
-      this.viewer.scene.primitives.remove(this._simplePlanePrimitive);
-      this._simplePlanePrimitive = null;
-    }
 
     this.entity = null;
     this.isInitialized = false;

+ 88 - 19
RuoYi-Vue3/src/supermap-cesium-module/components/first-person/SuperMapFirstPerson.vue

@@ -426,13 +426,13 @@ export default {
      */
     const calculateMoveDirection = (Cesium, viewer) => {
       const keys = keyState.value
-      
+
       if (!keys.forward && !keys.backward && !keys.left && !keys.right) {
         return null
       }
-      
+
       const heading = viewer.camera.heading
-      
+
       // 前方向(相机朝向)
       // heading=0(北): forward=(0,1,0)
       // heading=π/2(东): forward=(1,0,0)
@@ -442,7 +442,7 @@ export default {
         0
       )
       Cesium.Cartesian3.normalize(forward, forward)
-      
+
       // 右方向(垂直于前方向,指向右侧)
       // heading=0(北): right=(1,0,0) 东
       // heading=π/2(东): right=(0,-1,0) 南
@@ -452,10 +452,10 @@ export default {
         0
       )
       Cesium.Cartesian3.normalize(right, right)
-      
+
       // 计算移动方向
       const moveDirection = new Cesium.Cartesian3(0, 0, 0)
-      
+
       if (keys.forward) {
         Cesium.Cartesian3.add(moveDirection, forward, moveDirection)
       }
@@ -468,14 +468,75 @@ export default {
       if (keys.left) {
         Cesium.Cartesian3.subtract(moveDirection, right, moveDirection)
       }
-      
+
       if (Cesium.Cartesian3.magnitude(moveDirection) > 0) {
         Cesium.Cartesian3.normalize(moveDirection, moveDirection)
         return moveDirection
       }
-      
+
       return null
     }
+
+    /**
+     * 碰撞检测 - 使用射线检测
+     * 检测从当前位置到目标位置是否有障碍物
+     */
+    const checkCollision = (Cesium, viewer, currentPos, targetPos) => {
+      if (!props.enableCollision) return false
+
+      try {
+        // 计算方向和距离
+        const direction = Cesium.Cartesian3.subtract(targetPos, currentPos, new Cesium.Cartesian3())
+        const distance = Cesium.Cartesian3.magnitude(direction)
+        Cesium.Cartesian3.normalize(direction, direction)
+
+        // 创建射线
+        const ray = new Cesium.Ray(currentPos, direction)
+
+        // 使用 scene.pickFromRay 检测与场景中物体的碰撞
+        const pickResult = viewer.scene.pickFromRay(ray, [])
+
+        if (pickResult && pickResult.position) {
+          // 计算到碰撞点的距离
+          const collisionDistance = Cesium.Cartesian3.distance(currentPos, pickResult.position)
+
+          // 如果碰撞距离小于移动距离,则发生碰撞
+          if (collisionDistance < distance + props.collisionDistance) {
+            return true
+          }
+        }
+
+        return false
+      } catch (e) {
+        // 某些情况下 pickFromRay 可能失败,忽略错误继续移动
+        console.warn('碰撞检测失败:', e)
+        return false
+      }
+    }
+
+    /**
+     * 检测目标点是否在地形/模型内部
+     */
+    const checkPointCollision = (Cesium, viewer, position) => {
+      if (!props.enableCollision) return false
+
+      try {
+        // 使用 scene.pick 检测该点是否有模型
+        const windowPosition = new Cesium.Cartesian2()
+        viewer.scene.cartesianToCanvasCoordinates(position, windowPosition)
+
+        const pickResult = viewer.scene.pick(windowPosition)
+
+        // 如果检测到了物体,检查距离
+        if (pickResult && pickResult.primitive) {
+          return true
+        }
+
+        return false
+      } catch (e) {
+        return false
+      }
+    }
     
     /**
      * 更新相机位置
@@ -484,43 +545,51 @@ export default {
     const updateCameraPosition = (Cesium, viewer, deltaTime) => {
       const moveDirection = calculateMoveDirection(Cesium, viewer)
       if (!moveDirection) return
-      
+
       const moveDistance = actualMoveSpeed.value * deltaTime
-      
+
       // 获取当前位置
-      const currentPos = viewer.camera.position
+      const currentPos = viewer.camera.position.clone()
       const currentCartographic = Cesium.Cartographic.fromCartesian(currentPos)
       const lon = Cesium.Math.toDegrees(currentCartographic.longitude)
       const lat = Cesium.Math.toDegrees(currentCartographic.latitude)
       const height = currentCartographic.height
-      
+
       // 将移动方向转换为经纬度增量
       // moveDirection 是单位向量,需要转换为米/度
       // 1度经度 ≈ 111km * cos(lat),1度纬度 ≈ 111km
       const metersPerDegreeLat = 111000  // 纬度每度约111km
       const metersPerDegreeLon = 111000 * Math.cos(Cesium.Math.toRadians(lat))  // 经度每度随纬度变化
-      
+
       // moveDirection.x 对应经度变化(东/西)
       // moveDirection.y 对应纬度变化(北/南)
       const deltaLon = (moveDirection.x * moveDistance) / metersPerDegreeLon
       const deltaLat = (moveDirection.y * moveDistance) / metersPerDegreeLat
-      
+
       // 计算新位置
       const newLon = lon + deltaLon
       const newLat = lat + deltaLat
-      
+
+      // 计算目标位置(世界坐标)
+      const targetPosition = Cesium.Cartesian3.fromDegrees(newLon, newLat, height)
+
+      // 碰撞检测
+      if (checkCollision(Cesium, viewer, currentPos, targetPosition)) {
+        // 发生碰撞,不移动
+        emit('collision', { position: currentPos })
+        return
+      }
+
       // 更新相机位置
-      const finalPosition = Cesium.Cartesian3.fromDegrees(newLon, newLat, height)
-      
       viewer.camera.setView({
-        destination: finalPosition,
+        destination: targetPosition,
         orientation: {
           heading: viewer.camera.heading,
           pitch: viewer.camera.pitch,
           roll: 0
         }
       })
-      
+
       // 发射位置变化事件
       emit('position-change', {
         lon: newLon,

+ 131 - 15
RuoYi-Vue3/src/supermap-cesium-module/components/ju-kou-scene/JuKouShuiZhaPage.vue

@@ -3,7 +3,7 @@
     <!-- 左侧视角切换按钮 -->
     <div class="view-switch-panel">
       <div class="view-switch-buttons">
-        <el-button 
+        <el-button
           :type="viewMode === 'thirdPerson' ? 'primary' : 'default'"
           @click="switchToThirdPerson"
           class="view-btn"
@@ -11,7 +11,7 @@
         >
           <el-icon><View /></el-icon>
         </el-button>
-        <el-button 
+        <el-button
           :type="viewMode === 'firstPerson' ? 'primary' : 'default'"
           @click="switchToFirstPerson"
           class="view-btn"
@@ -20,10 +20,41 @@
           <el-icon><User /></el-icon>
         </el-button>
       </div>
-      <!-- 第一人称模式提示 -->
+      <!-- 第一人称模式场景选择 -->
+      <div v-if="viewMode === 'firstPerson'" class="scene-selector">
+        <div class="scene-selector-label">场景选择</div>
+        <div class="scene-buttons">
+          <el-button
+            :type="currentScene === 'outdoor' ? 'primary' : 'default'"
+            size="small"
+            @click="switchScene('outdoor')"
+            class="scene-btn"
+          >
+            室外
+          </el-button>
+          <el-button
+            :type="currentScene === 'indoor' ? 'primary' : 'default'"
+            size="small"
+            @click="switchScene('indoor')"
+            class="scene-btn"
+          >
+            室内
+          </el-button>
+        </div>
+      </div>
+      <!-- 第一人称模式操作提示 -->
       <div v-if="viewMode === 'firstPerson'" class="mode-hint">
         <span>WASD移动 | ESC退出</span>
       </div>
+      <!-- 闸门启闭开关按钮 -->
+      <el-button
+        :type="showGateControl ? 'primary' : 'default'"
+        @click="showGateControl = !showGateControl"
+        class="gate-toggle-btn"
+        title="闸门启闭控制"
+      >
+        闸门启闭
+      </el-button>
     </div>
 
     <!-- 右侧闸门控制面板 -->
@@ -107,16 +138,29 @@ export default {
   setup(props, { emit }) {
     const isLoading = ref(false)
     const viewMode = ref('thirdPerson')
-    const showGateControl = ref(true)
+    const showGateControl = ref(false)
     const gatePosition = ref(0)
-    
-    const firstPersonPosition = ref({
-      lon: 119.144412,
-      lat: 25.866431,
-      height: 35,
-      heading: 0,
-      pitch: -10
-    })
+    const currentScene = ref('outdoor')
+
+    // 第一人称场景配置
+    const firstPersonScenes = {
+      outdoor: {
+        lon: 119.144412,
+        lat: 25.866431,
+        height: 35,
+        heading: 0,
+        pitch: -10
+      },
+      indoor: {
+        lon: 119.143788,
+        lat: 25.868744,
+        height: 46.5,
+        heading: 180,
+        pitch: 0
+      }
+    }
+
+    const firstPersonPosition = ref({ ...firstPersonScenes.outdoor })
     
     const gateControlRef = ref(null)
     const firstPersonRef = ref(null)
@@ -180,21 +224,50 @@ export default {
     
     const switchToFirstPerson = () => {
       if (viewMode.value === 'firstPerson') return
-      
+
       // 使用 props.viewer 或 window.viewer
       const viewer = props.viewer || window.viewer
       if (!viewer) {
         ElMessage.warning('Viewer 未初始化')
         return
       }
-      
+
       console.log('🔄 切换到第一人称视角...')
-      
+
       saveThirdPersonCamera(viewer)
       viewMode.value = 'firstPerson'
       emit('view-change', { mode: 'firstPerson', position: firstPersonPosition.value })
       ElMessage.success('已进入第一人称漫游,点击画面开始')
     }
+
+    // 切换第一人称场景
+    const switchScene = (scene) => {
+      if (currentScene.value === scene) return
+
+      currentScene.value = scene
+      firstPersonPosition.value = { ...firstPersonScenes[scene] }
+
+      // 如果已经在第一人称模式,立即更新相机位置
+      if (viewMode.value === 'firstPerson' && firstPersonRef.value) {
+        const viewer = props.viewer || window.viewer
+        if (viewer) {
+          const Cesium = window.Cesium
+          if (Cesium) {
+            const pos = firstPersonPosition.value
+            const cartesian = Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.height)
+            viewer.camera.setView({
+              destination: cartesian,
+              orientation: {
+                heading: Cesium.Math.toRadians(pos.heading),
+                pitch: Cesium.Math.toRadians(pos.pitch),
+                roll: 0
+              }
+            })
+            ElMessage.success(`已切换到${scene === 'outdoor' ? '室外' : '室内'}场景`)
+          }
+        }
+      }
+    }
     
     const saveThirdPersonCamera = (viewer) => {
       if (!viewer) return
@@ -265,6 +338,7 @@ export default {
       viewMode,
       showGateControl,
       gatePosition,
+      currentScene,
       firstPersonPosition,
       gateControlRef,
       firstPersonRef,
@@ -272,6 +346,7 @@ export default {
       unloadScene,
       switchToThirdPerson,
       switchToFirstPerson,
+      switchScene,
       handleGatePositionChange,
       onFirstPersonEnter,
       onFirstPersonExit,
@@ -318,6 +393,20 @@ export default {
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
 }
 
+/* 闸门启闭开关按钮 */
+.gate-toggle-btn {
+  width: 100%;
+  height: 36px;
+  padding: 0 12px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  font-size: 13px;
+}
+
+.gate-toggle-btn.el-button--primary {
+  color: #fff;
+}
+
 /* 右侧闸门控制面板需要可点击 */
 .right-panel {
   position: absolute;
@@ -335,6 +424,33 @@ export default {
   font-size: 20px;
 }
 
+/* 场景选择器样式 */
+.scene-selector {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  padding: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.scene-selector-label {
+  font-size: 12px;
+  color: #666;
+  text-align: center;
+}
+
+.scene-buttons {
+  display: flex;
+  gap: 6px;
+}
+
+.scene-btn {
+  flex: 1;
+  min-width: 50px;
+}
+
 .mode-hint {
   display: flex;
   flex-direction: column;