3D智慧城市:blender建模、骨骼、动画、VUE、threeJs引入渲染,飞行视角,涟漪、人物行走

一、项目成果

作为一名前端开发者,我一直对 3D 可视化技术充满兴趣。在一次个人项目中,我希望通过实践来拓展自己的技术栈,于是选择了实现一个智慧城市的 3D 可视化系统。这个项目不仅需要前端开发技能,还需要掌握 3D 建模和动画制作技术,对我来说是一次很好的自我挑战。

3D城市、人物

后续还会继续完整的写完第一视角漫游,包括人物上楼梯等

---------------------如果需要模型自己尝试一下就私聊-----------------

二、技术栈选择

前端技术

  • 框架:Vue 3 + Composition API
  • 3D 引擎:Three.js
  • 构建工具:Vite
  • 包管理器:npm

3D 建模工具

  • 建模软件:Blender 3.0+
  • 模型格式:GLB (glTF 2.0)

三、开发流程

1. 需求分析与技术规划

作为自我驱动的学习者,我首先明确了项目目标:

  • 实现 3D 城市模型的加载和渲染
  • 添加人物模型并实现基本动画
  • 创建道路流光效果增强视觉表现
  • 实现相机视角切换功能
  • 添加基本的用户交互控制

技术规划:

  1. 学习 Blender 基础,创建简单的城市建筑和人物模型
  2. 掌握 Three.js 核心概念,实现模型加载和渲染
  3. 探索 Shader 编程,实现道路流光效果
  4. 研究动画系统,实现人物动画和相机控制
  5. 持续优化性能,确保应用流畅运行

2. 自主学习与技能拓展

2.1 Blender 建模学习

作为前端开发者,我之前没有 3D 建模经验,通过自学掌握了:

  • Blender 基本操作和界面
  • 基础几何体建模
  • 骨骼绑定和蒙皮技术
  • 关键帧动画制作
  • 模型导出设置
2.2 Three.js 技术探索

通过文档学习和实践,我掌握了:

  • 场景、相机、渲染器的基本设置
  • GLTFLoader 加载模型
  • AnimationMixer 处理骨骼动画
  • ShaderMaterial 实现自定义效果
  • OrbitControls 实现相机控制

3. 项目实现

3.1 场景搭建
javascript 复制代码
// 创建场景
function createScene() {
  const scene = new THREE.Scene();
  return scene;
}

// 创建相机
function createCamera() {
  const camera = new THREE.PerspectiveCamera(
    75,
    innerWidth / innerHeight,
    0.1,
    10000,
  );
  camera.position.set(179, 12, 164);
  camera.lookAt(0, 0, 0);
  return camera;
}
3.2 模型加载与定位
javascript 复制代码
// 加载建筑模型
function loadModel() {
  const loader = new GLTFLoader();
  loader.load(
    "/src/assets/models/city.glb",
    (gltf) => {
      // 清理之前的模型
      if (cityModel) {
        scene.remove(cityModel);
        cityModel.traverse((child) => {
          if (child.isMesh) {
            child.geometry.dispose();
            if (child.material) {
              if (Array.isArray(child.material)) {
                child.material.forEach(material => material.dispose());
              } else {
                child.material.dispose();
              }
            }
          }
        });
        cityModel = null;
      }
      
      cityModel = gltf.scene;

      // 计算模型中心并定位
      const buildingBox = new THREE.Box3();
      cityModel.traverse((child) => {
        if (child.isMesh) {
          let isBuilding = false;
          for (const buildingName of buildingNames) {
            if (child.name.includes(buildingName)) {
              isBuilding = true;
              break;
            }
          }
          if (isBuilding) {
            const childBox = new THREE.Box3().setFromObject(child);
            buildingBox.union(childBox);
          }
        }
      });

      // 获取大楼模型的中心位置
      buildingCenter = new THREE.Vector3();
      buildingBox.getCenter(buildingCenter);
      buildingCenter.y = 1.15; // 手动调整使得底部与地面平齐

      // 将模型移动到世界原点
      const offset = new THREE.Vector3().subVectors(
        new THREE.Vector3(0, 0, 0),
        buildingCenter,
      );
      cityModel.position.copy(offset);

      // 添加模型到场景
      scene.add(cityModel);
      // 加载人物模型
      loadCharacterModel();
      // 设置颜色和动画
      setupColorsAndAnimations();
      // 设置灯光
      setupLights();
    },
    (xhr) => {
      console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
    },
    (error) => {
      console.error("模型加载失败:", error);
    },
  );
}
3.3 人物动画与交互
javascript 复制代码
// 加载人物模型
function loadCharacterModel() {
  const loader = new GLTFLoader();
  loader.load(
    "/src/assets/models/people.glb",
    (gltf) => {
      characterModel = gltf.scene;
      characterModel.position.set(0, 0, 45);
      scene.add(characterModel);
      characterModel.scale.set(1.5, 1.5, 1.5);

      // 处理动画
      if (gltf.animations && gltf.animations.length > 0) {
        mixer = new THREE.AnimationMixer(characterModel);
        // 播放站立动画
        playStandAnimation(mixer, gltf);
        // 添加键盘控制
        addKeyboardControls(characterModel, mixer, gltf);
      }
    },
    (xhr) => {
      console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
    },
    (error) => {
      console.error("人物模型加载失败:", error);
    },
  );
}
3.4 道路流光效果
javascript 复制代码
// 创建道路流光效果的着色器材质
function createRoadShaderMaterial() {
  const vertexShader = `
    varying vec2 vUv;
    varying vec3 vPosition;
    void main() {
      vUv = uv;
      vPosition = position;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `;

  const fragmentShader = `
    uniform float time;
    varying vec2 vUv;
    varying vec3 vPosition;

    float random(vec2 st) {
      return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
    }

    void main() {
      // 多车道流动效果
      float lane = floor(vUv.y * 3.0);
      float speed = 1.0 + lane * 0.5;
      float flow = mod(vPosition.x * 0.005 + time * speed + random(vUv) * 0.5, 1.0);
      float intensity = smoothstep(0.1, 0.3, flow) - smoothstep(0.7, 0.9, flow);

      // 不同车道的颜色变化
      vec3 color1 = vec3(1.0, 0.4, 0.0); // 橘色
      vec3 color2 = vec3(1.0, 1.0, 0.0); // 黄色
      vec3 color3 = vec3(1.0, 0, 0.3); // 橙黄色

      vec3 color;
      if (lane == 0.0) {
        color = mix(color1, color2, intensity);
      } else if (lane == 1.0) {
        color = mix(color2, color3, intensity);
      } else {
        color = mix(color3, color1, intensity);
      }

      // 添加随机闪烁效果
      float flicker = 0.8 + random(vec2(time * 10.0, vPosition.x)) * 0.2;
      color *= flicker;

      gl_FragColor = vec4(color, 1.0);
    }
  `;

  return new THREE.ShaderMaterial({
    vertexShader,
    fragmentShader,
    uniforms: {
      time: { value: 0.0 },
    },
  });
}

4. 性能优化

作为自我驱动的开发者,我注重代码质量和性能优化:

4.1 渲染循环优化
javascript 复制代码
// 渲染循环
function animate(timestamp = 0) {
  animationId = requestAnimationFrame(animate);
  
  // 计算时间差,限制最大时间差
  const deltaTime = (timestamp - lastTime) / 1000;
  lastTime = timestamp;
  const clampedDeltaTime = Math.min(deltaTime, 0.033); // 限制在30fps

  // 更新动画
  if (mixer) {
    mixer.update(clampedDeltaTime);
  }

  // 限制更新频率
  if (cityModel && Date.now() % 2 === 0) {
    cityModel.traverse((child) => {
      if (child.isMesh && child.userData.isRoad === true) {
        child.userData.animationTime += 0.04;
        if (child.material.uniforms && child.material.uniforms.time) {
          child.material.uniforms.time.value = child.userData.animationTime;
        }
      }
    });
  }

  // 渲染场景
  renderer.render(scene, camera);
}
4.2 内存管理
javascript 复制代码
// 清理函数
function cleanup() {
  // 停止动画循环
  if (animationId) {
    cancelAnimationFrame(animationId);
  }

  // 移除事件监听器
  eventListeners.forEach(({ target, type, listener }) => {
    target.removeEventListener(type, listener);
  });
  eventListeners = [];

  // 释放模型资源
  if (cityModel) {
    scene.remove(cityModel);
    cityModel.traverse((child) => {
      if (child.isMesh) {
        child.geometry.dispose();
        if (child.material) {
          if (Array.isArray(child.material)) {
            child.material.forEach(material => material.dispose());
          } else {
            child.material.dispose();
          }
        }
      }
    });
    cityModel = null;
  }

  // 释放其他资源...
}

四、自我驱动的学习与成长

1. 遇到的挑战

在项目开发过程中,我遇到了许多挑战:

1.1 3D 建模技术

作为前端开发者,我需要从零开始学习 Blender:

  • 掌握 3D 空间思维
  • 学习骨骼绑定和蒙皮技术
  • 理解动画制作原理
  • 解决模型导出问题
1.2 Three.js 性能优化

性能优化是一个持续的挑战:

  • 内存泄漏问题
  • 渲染卡顿现象
  • 动画同步问题
  • 资源加载优化
1.3 跨领域知识整合

将前端开发与 3D 技术结合需要跨领域知识:

  • 理解 WebGL 底层原理
  • 掌握 Shader 编程基础
  • 学习 3D 数学知识
  • 了解计算机图形学基本概念

2. 解决方法与学习过程

2.1 自主学习资源

通过多种渠道获取学习资源:

  • Blender 官方文档和教程
  • Three.js 官方文档和示例
  • 在线课程和视频教程
  • 社区论坛和问题解答
2.2 实践驱动学习

采用实践驱动的学习方法:

  • 从小规模测试开始
  • 逐步增加功能复杂度
  • 遇到问题及时查阅资料
  • 不断迭代和优化代码
2.3 问题解决思路

遇到问题时的解决思路:

  • 分解问题,逐步排查
  • 利用浏览器开发者工具分析性能
  • 参考官方示例和社区解决方案
  • 通过实验验证解决方案

3. 技能提升与收获

通过这个项目,我获得了多方面的技能提升:

3.1 技术技能
  • 3D 建模:掌握了 Blender 基础操作和建模技术
  • Three.js:深入理解了 Three.js 核心概念和 API
  • Shader 编程:学习了 GLSL 基础和自定义材质
  • 性能优化:掌握了前端 3D 应用的性能优化策略
  • 动画系统:理解了骨骼动画和状态管理
3.2 软技能
  • 自学能力:提高了自主学习和解决问题的能力
  • 项目管理:学会了如何规划和执行复杂项目
  • 代码组织:提升了代码结构设计和模块化能力
  • 调试能力:增强了问题定位和调试技能

五、项目成果与反思

1. 项目成果

通过自我驱动的学习和实践,我成功实现了:

  • 3D 城市模型的加载和渲染
  • 人物模型的骨骼动画
  • 道路流光效果
  • 相机视角平滑切换
  • 键盘交互控制
  • 性能优化和内存管理

2. 反思与改进空间

项目完成后,我对整个开发过程进行了反思:

2.1 技术改进
  • 模型质量:可以进一步提高模型的细节和质感
  • 动画效果:可以添加更多动画状态和过渡效果
  • 交互体验:可以增加更多交互方式和反馈
  • 性能优化:可以进一步优化渲染性能和加载速度
2.2 学习反思
  • 学习方法:实践驱动的学习方式效果显著
  • 知识整合:跨领域知识的整合需要系统学习
  • 持续学习:前端和 3D 技术发展迅速,需要保持学习
  • 社区参与:可以更多参与社区交流,分享经验

六、未来规划

作为自我驱动的开发者,我对未来有以下规划:

1. 技术深入

  • WebGL:深入学习 WebGL 底层原理
  • 3D 引擎:探索更多 3D 引擎和工具
  • VR/AR:学习 VR/AR 技术和应用
  • 图形学:学习计算机图形学基础理论

2. 项目扩展

  • 功能扩展:添加更多交互功能和视觉效果
  • 数据可视化:整合实时数据,实现数据可视化
  • 跨平台:优化移动端体验,实现跨平台适配
  • 协作开发:与其他开发者合作,共同开发更复杂的项目

3. 知识分享

  • 技术博客:分享学习心得和技术经验
  • 开源贡献:参与开源项目,贡献自己的代码
  • 社区活动:参与技术社区活动,交流学习
  • 教学实践:帮助其他开发者学习相关技术

七、总结

通过这个智慧城市 3D 可视化项目,我不仅实现了一个功能完整的应用,更重要的是通过自我驱动的学习,拓展了自己的技术栈,提高了综合能力。

未来,我将继续保持自我驱动的学习态度,不断探索前端与 3D 技术的结合点,为前端开发领域的发展贡献自己的力量。我相信,通过不断学习和实践,我们可以创造出更加丰富和创新的前端应用。

完整代码

js 复制代码
<!-- 渲染城市模型 -->
<template>
 <div id="cityModel"></div>
 <div class="button_container">
   <button @click="owerSwitch" :disabled="isOwer">第一视角漫游</button>
   <button @click="buildingSwitch" :disabled="isBuilding">建筑视角漫游</button>
 </div>
</template>

<script setup>
import { onMounted, nextTick, ref, onUnmounted } from "vue";
import * as THREE from "three";
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

// 初始化函数
onMounted(() => {
 nextTick(() => {
   init();
 });
});

// 组件销毁时清理资源
onUnmounted(() => {
 cleanup();
});

// 全局变量
let cityModel = null;
let characterModel = null;
let scene, camera, renderer, controls;
let DOMCityModel, innerHeight, innerWidth;
let rippleMesh = null;
let mixer = null;
let animationId = null;
let eventListeners = [];

// 模型配置
const buildingNames = [
 "lou1F",
 "lou2F",
 "lou3F",
 "lou4F",
 "lou5F",
 "lou6F",
 "lou7F",
 "lou8F",
 "lou9F",
 "lou10F",
 "lou11F",
 "louding",
];
const waysName = "Ways";
let buildingCenter = null;

// 定义方向键
let keyList = {
 "ArrowUp": false,
 "ArrowDown": false,
 "ArrowLeft": false,
 "ArrowRight": false,
 " ": false, // 空格字符
 "Space": false, // 空格键
 "w": false,
 "a": false,
 "s": false,
 "d": false,
 "W": false,
 "A": false,
 "S": false,
 "D": false
};

// 初始化函数
function init() {
 // 获取DOM元素
 DOMCityModel = document.getElementById("cityModel");
 innerHeight = DOMCityModel.clientHeight;
 innerWidth = DOMCityModel.clientWidth;

 // 创建场景
 scene = createScene();

 // 创建相机
 camera = createCamera();

 // 创建渲染器
 renderer = createRenderer();

 // 绘制辅助坐标系
 drawHelper();

 // 创建轨迹球控制器
 controls = createControls();

 // 加载模型
 loadModel();

 // 启动渲染循环
 startAnimation();

 // 设置事件监听器
 setupEventListeners();

 // 启动帧率监控
 monitorFPS();
}

// 创建场景
function createScene() {
 const scene = new THREE.Scene();
 // scene.background = new THREE.Color(0xffffff);
 return scene;
}

// 绘制辅助坐标系
function drawHelper() {
 const axesHelper = new THREE.AxesHelper(1000);
 //x轴:红,y轴:绿,z轴:蓝
 axesHelper.position.set(0, 0, 0); // 将坐标轴放置在世界原点
 scene.add(axesHelper);
}

// 创建相机
function createCamera() {
 const camera = new THREE.PerspectiveCamera(
   75,
   innerWidth / innerHeight,
   0.1,
   10000,
 );
 // 调整相机位置
 camera.position.set(179, 12, 164);
 // camera.position.set(2, 1, 52);
 camera.lookAt(0, 0, 0);
 return camera;
}

// 创建渲染器
function createRenderer() {
 const renderer = new THREE.WebGLRenderer({ antialias: true });
 renderer.setSize(innerWidth, innerHeight);
 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); // 限制像素比,提高性能
 renderer.physicallyCorrectLights = true;
 DOMCityModel.appendChild(renderer.domElement);
 return renderer;
}

// 创建轨迹球控制器
function createControls() {
 const controls = new OrbitControls(camera, renderer.domElement);

 // 控制设置
 controls.zoomSpeed = 0.4;
 controls.enableDamping = false;
 controls.enableZoom = true;
 controls.enableRotate = true;
 controls.enablePan = false;
 controls.staticMoving = true;
 controls.dynamicDampingFactor = 0.3;
 controls.target.set(0, 0, 0);

 return controls;
}

// 加载建筑模型
function loadModel() {
 const loader = new GLTFLoader();
 loader.load(
   "/src/assets/models/city.glb",
   (gltf) => {
     // 清理之前的模型
     if (cityModel) {
       scene.remove(cityModel);
       cityModel.traverse((child) => {
         if (child.isMesh) {
           child.geometry.dispose();
           if (child.material) {
             if (Array.isArray(child.material)) {
               child.material.forEach(material => material.dispose());
             } else {
               child.material.dispose();
             }
           }
         }
       });
       cityModel = null;
     }
     
     cityModel = gltf.scene;

     if (gltf.scene.children.length === 0) {
       console.warn("模型场景为空,可能模型文件有问题");
     }

     // 优化模型
     optimizeModel(cityModel);

     // 计算模型的中心位置
     const buildingBox = new THREE.Box3();
     cityModel.traverse((child) => {
       if (child.isMesh) {
         let isBuilding = false;
         for (const buildingName of buildingNames) {
           if (child.name.includes(buildingName)) {
             isBuilding = true;
             break;
           }
         }
         if (isBuilding) {
           const childBox = new THREE.Box3().setFromObject(child);
           buildingBox.union(childBox);
         }
       }
     });
     //使用THREE.Box3计算大楼模型的边界框,获取X轴方向的最小值---用于调整人物模型的位置
     const buildingMinx = buildingBox.min.x;
     const buildingMaxX = buildingBox.max.x;
     const buildingMinZ = buildingBox.min.z;
     const buildingMaxZ = buildingBox.max.z;
     const peopleX = Math.floor(buildingMaxX) + 10;
     const peopleZ = Math.floor(buildingMaxZ) + 10;
     // console.log(
     //   peopleX,
     //   peopleZ,
     //   "大楼模型的边界框,获取X轴方向的最大值---用于调整人物模型的位置",
     // );

     // //偏量计算,使得整个模型的底部与地面平齐
     // 获取大楼模型的中心位置
     buildingCenter = new THREE.Vector3();
     buildingBox.getCenter(buildingCenter);
     // buildingCenter.y = 0; // 将模型向上移动,使得底部与地面平齐
     buildingCenter.y = 1.15; //因为模型问题,手动调整使得底部与地面平齐

     // 将模型移动到世界原点
     const offset = new THREE.Vector3().subVectors(
       new THREE.Vector3(0, 0, 0),
       buildingCenter,
     );
     cityModel.position.copy(offset);
     // 调整模型的Y轴位置,使得底部与地面平齐
     console.log("大楼的中心位置为:", buildingCenter);
     console.log("目前模型的中心位置:", cityModel.position);

     // 创建涟漪效果
     // addRippleEffect();

     // 添加模型到场景
     scene.add(cityModel);
     // 加载人物模型
     loadCharacterModel();
     // 设置颜色和动画
     setupColorsAndAnimations();

     // 设置灯光
     setupLights();
   },
   (xhr) => {
     // 加载进度
     // console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
   },
   (error) => {
     console.error("模型加载失败:", error);
   },
 );
}

// 优化模型
function optimizeModel(model) {
 model.traverse((child) => {
   if (child.isMesh) {
     // 计算法向量
     child.geometry.computeVertexNormals();
     
     // 优化材质
     if (child.material) {
       // 禁用不必要的特性
       if (child.material.isMeshStandardMaterial) {
         child.material.envMapIntensity = 0; // 禁用环境映射
         child.material.needsUpdate = true;
       }
     }
   }
 });
}

// 加载人物模型
function loadCharacterModel() {
 const loader = new GLTFLoader();
 loader.load(
   "/src/assets/models/people.glb",
   (gltf) => {
     // 清理之前的模型
     if (characterModel) {
       scene.remove(characterModel);
       characterModel.traverse((child) => {
         if (child.isMesh) {
           child.geometry.dispose();
           if (child.material) {
             if (Array.isArray(child.material)) {
               child.material.forEach(material => material.dispose());
             } else {
               child.material.dispose();
             }
           }
         }
       });
       characterModel = null;
     }
     
     characterModel = gltf.scene;
     characterModel.position.set(0, 0, 45); // 根据需要调整位置
     scene.add(characterModel);
     //缩放模型,使得人物模型与大楼模型的比例协调
     characterModel.scale.set(1.5, 1.5, 1.5);

     // 处理动画
     if (gltf.animations && gltf.animations.length > 0) {
       // 创建动画混合器
       mixer = new THREE.AnimationMixer(characterModel);
       console.log("mixer 初始化成功:", mixer);
       // 播放站立动画
       playStandAnimation(mixer, gltf);
       // 播放行走动画
       // playWalkAnimation(mixer, gltf);

       // 添加人物模型的键盘控制
       addKeyboardControls(characterModel, mixer, gltf);
     }
   },
   (xhr) => {
     // 加载进度
     // console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
   },
   (error) => {
     console.error("人物模型加载失败:", error);
   },
 );
}

// 人物站立动画
function playStandAnimation(mixer, gltf) {
 if (!mixer || !gltf.animations || gltf.animations.length === 0) {
   console.warn("mixer 未初始化或没有动画");
   return;
 }
 
 // 查找站立动画
 const standAnimation = gltf.animations.find(
   (animation) => {
     return animation.name === "stand";
   },
 );
 
 if (standAnimation) {
   // 停止所有其他动画
   if (mixer.stopAllActions) {
     mixer.stopAllActions();
   } else {
     gltf.animations.forEach((animation) => {
       const action = mixer.clipAction(animation);
       action.stop();
     });
   }
   
   // 播放站立动画
   const characterAction = mixer.clipAction(standAnimation);
   characterAction.loop = THREE.LoopRepeat;
   characterAction.play();
 } else {
   console.warn("未找到stand动画");
 }
}

// 人物行走动画
function playWalkAnimation(mixer, gltf) {
 if (!mixer || !gltf.animations || gltf.animations.length === 0) {
   console.warn("mixer 未初始化或没有动画");
   return;
 }
 
 // 查找行走动画
 const walkAnimation = gltf.animations.find(
   (animation) => {
     return animation.name === "walk";
   },
 );
 
 if (walkAnimation) {
   // 停止所有其他动画
   if (mixer.stopAllActions) {
     mixer.stopAllActions();
   } else {
     gltf.animations.forEach((animation) => {
       const action = mixer.clipAction(animation);
       action.stop();
     });
   }
   
   // 播放行走动画
   const characterAction = mixer.clipAction(walkAnimation);
   characterAction.loop = THREE.LoopRepeat;
   characterAction.play();
 } else {
   console.warn("未找到walk动画");
 }
}

//添加人物模型的键盘控制
function addKeyboardControls(characterModel, mixer, gltf) {
 // 键盘事件处理函数
 const handleKeyDown = (event) => {
   console.log("按下键盘:", event.key, "keyCode:", event.keyCode);
   // 同时支持 "Space" 和 " "(空格字符)
   const key = event.key === " " ? "Space" : event.key;
   if (keyList.hasOwnProperty(key)) {
     keyList[key] = true;
     if (characterModel && mixer) {
       updateCharacter(mixer, characterModel, gltf);
     }
   }
 };

 const handleKeyUp = (event) => {
   console.log("释放键盘:", event.key, "keyCode:", event.keyCode);
   // 同时支持 "Space" 和 " "(空格字符)
   const key = event.key === " " ? "Space" : event.key;
   if (keyList.hasOwnProperty(key)) {
     keyList[key] = false;
     if (characterModel && mixer) {
       updateCharacter(mixer, characterModel, gltf);
     }
   }
 };

 // 监听键盘事件
 document.addEventListener("keydown", handleKeyDown);
 eventListeners.push({
   target: document,
   type: "keydown",
   listener: handleKeyDown
 });
 
 // 监听键盘释放事件
 document.addEventListener("keyup", handleKeyUp);
 eventListeners.push({
   target: document,
   type: "keyup",
   listener: handleKeyUp
 });
 
 console.log("键盘事件监听器已注册");
}

//更新人物状态
function updateCharacter(mixer, characterModel, gltf) {
 // 检查 mixer 是否有效
 if (!mixer) {
   console.warn("mixer 未初始化");
   return;
 }
 
 // 检查是否按下了空格键
 if (keyList[" "] || keyList["Space"]) {
   // 空格键逻辑:播放站立动画
   console.log("按下空格键,播放站立动画");
   playStandAnimation(mixer, gltf);
   return; // 直接返回,不执行其他逻辑
 }
 
 // 检查是否有方向键被按下
 let hasKey = false;
 for (let key in keyList) {
   if (keyList[key]) {
     hasKey = true;
     break;
   }
 }
 
 // 如果有方向键被按下,播放行走动画,否则播放站立动画
 if (hasKey) {
   console.log("按下方向键,播放行走动画");
   playWalkAnimation(mixer, gltf);
   // 计算人物移动方向
   let moveDirection = new THREE.Vector3(0, 0, 0);
   if (keyList["ArrowUp"] || keyList["w"] || keyList["W"]) {
     moveDirection.z = 1;
   }
   if (keyList["ArrowDown"] || keyList["s"] || keyList["S"]) {
     moveDirection.z = -1;
   }
   if (keyList["ArrowLeft"] || keyList["a"] || keyList["A"]) {
     moveDirection.x = -1;
   }
   if (keyList["ArrowRight"] || keyList["d"] || keyList["D"]) {
     moveDirection.x = 1;
   }
   
   // 归一化方向量
   if (moveDirection.length() > 0) {
     moveDirection.normalize();
     // 更新人物位置
     characterModel.position.x += moveDirection.x * 0.5;
     characterModel.position.z += moveDirection.z * 0.5;

     // 更新人物朝向
     const targetRotation = Math.atan2(moveDirection.x, moveDirection.z);
     // 人物模型面向移动方向
     characterModel.rotation.y = targetRotation;
   }
 } else {
   console.log("没有按键按下,播放站立动画");
   playStandAnimation(mixer, gltf);
 }
}

// 设置灯光
function setupLights() {
 // 建筑正面光源
 const frontLight = new THREE.DirectionalLight(0xffffff, 0.8);
 frontLight.position.set(0, 0, 5000);
 frontLight.target.position.copy(buildingCenter);
 scene.add(frontLight);
 scene.add(frontLight.target);

 // 建筑背面光源
 const backLight = new THREE.DirectionalLight(0xffffff, 1.8);
 backLight.position.set(2500, 0, 1000).normalize().multiplyScalar(5000);
 backLight.target.position.copy(new THREE.Vector3(2500, 0, 1000));
 scene.add(backLight);
 scene.add(backLight.target);

 // 建筑右侧光源
 const rightLight = new THREE.DirectionalLight(0xffffff, 1.5);
 rightLight.position.set(650, 70, -1950).normalize().multiplyScalar(5000);
 rightLight.target.position.copy(new THREE.Vector3(650, 70, -1950));
 scene.add(rightLight);
 scene.add(rightLight.target);

 // 建筑左侧光源
 const leftLight = new THREE.DirectionalLight(0xffffff, 1.5);
 leftLight.position.set(-740, 80, 1640).normalize().multiplyScalar(5000);
 leftLight.target.position.copy(new THREE.Vector3(-740, 80, 1640));
 scene.add(leftLight);
 scene.add(leftLight.target);

 // 建筑顶部光源
 const topLight = new THREE.DirectionalLight(0xffffff, 1.5);
 topLight.position.set(0, 5000, 0).normalize().multiplyScalar(5000);
 topLight.target.position.copy(buildingCenter);
 scene.add(topLight);
 scene.add(topLight.target);
}

// 设置颜色和动画
function setupColorsAndAnimations() {
 cityModel.traverse((child) => {
   if (child.isMesh) {
     // 重置动画标记
     child.userData.isRoad = false;
     child.userData.isBuilding = false;

     // 检查是否是大楼
     for (const buildingName of buildingNames) {
       if (child.name.includes(buildingName)) {
         child.userData.isBuilding = true;
         break;
       }
     }

     if (child.userData.isBuilding) {
       // 大楼保持原始材质
     } else if (child.name === waysName) {
       // 为道路添加流光效果
       child.userData.isRoad = true;
       child.userData.animationTime = 0;

       // 创建ShaderMaterial
       const shaderMaterial = createRoadShaderMaterial();
       child.userData.originalMaterial = child.material;
       child.material = shaderMaterial;
     } else {
       // 为其他网格添加颜色和边框
       child.material.color.set(0x004857);
       child.material.transparent = true;
       child.material.opacity = 0.8;

       // 添加边框效果
       const edgeGeometry = new THREE.EdgesGeometry(child.geometry);
       const edgeMaterial = new THREE.LineBasicMaterial({
         color: 0x004857,
         linewidth: 1,
       });
       const edgeLine = new THREE.Line(edgeGeometry, edgeMaterial);
       child.add(edgeLine);
     }
   }
 });
}

// 创建道路流光效果的着色器材质
function createRoadShaderMaterial() {
 const vertexShader = `
   varying vec2 vUv;
   varying vec3 vPosition;
   void main() {
     vUv = uv;
     vPosition = position;
     gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
   }
 `;

 const fragmentShader = `
   uniform float time;
   varying vec2 vUv;
   varying vec3 vPosition;

   float random(vec2 st) {
     return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
   }

   void main() {
     // 多车道流动效果
     float lane = floor(vUv.y * 3.0);
     float speed = 1.0 + lane * 0.5;
     float flow = mod(vPosition.x * 0.005 + time * speed + random(vUv) * 0.5, 1.0);
     float intensity = smoothstep(0.1, 0.3, flow) - smoothstep(0.7, 0.9, flow);

     // 不同车道的颜色变化
     vec3 color1 = vec3(1.0, 0.4, 0.0); // 橘色
     vec3 color2 = vec3(1.0, 1.0, 0.0); // 黄色
     vec3 color3 = vec3(1.0, 0, 0.3); // 橙黄色

     vec3 color;
     if (lane == 0.0) {
       color = mix(color1, color2, intensity);
     } else if (lane == 1.0) {
       color = mix(color2, color3, intensity);
     } else {
       color = mix(color3, color1, intensity);
     }

     // 添加随机闪烁效果
     float flicker = 0.8 + random(vec2(time * 10.0, vPosition.x)) * 0.2;
     color *= flicker;

     gl_FragColor = vec4(color, 1.0);
   }
 `;

 return new THREE.ShaderMaterial({
   vertexShader,
   fragmentShader,
   uniforms: {
     time: { value: 0.0 },
   },
 });
}

// 创建2D涟漪效果
function addRippleEffect() {
 const geometry = new THREE.PlaneGeometry(1000, 1000, 100, 100);

 const vertexShader = `
   varying vec2 vUv;
   varying vec3 vPosition;
   uniform float time;
   uniform vec3 center;

   void main() {
     vUv = uv;
     vPosition = position;

     // 计算到中心的距离
     float distance = length(position - center);

     // 创建多圈涟漪效果
     float ripple = sin(distance * 0.05 - time * 5.0) * 0.5;

   // 添加更多频率的涟漪
     ripple += sin(distance * 0.1 - time * 7.0) * 0.3;
     ripple += sin(distance * 0.15 - time * 9.0) * 0.2;

     // 只在一定范围内产生涟漪
     float rippleArea = smoothstep(0.0, 50.0, distance);
     ripple *= (1.0 - rippleArea);

     // 修改顶点位置
     vec3 newPosition = position;
     newPosition.y += ripple * 0.8; // 调整涟漪高度

     gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
   }
 `;

 const fragmentShader = `
   varying vec2 vUv;
   varying vec3 vPosition;
   uniform float time;
   uniform vec3 center;

   void main() {
     // 计算到中心的距离
     float distance = length(vPosition - center);

     // 创建多圈涟漪效果
     float ripple = sin(distance * 0.05 - time * 5.0) * 0.5 + 0.5;
     // 添加更多频率的涟漪
     ripple += sin(distance * 0.1 - time * 7.0) * 0.3;
     ripple += sin(distance * 0.15 - time * 9.0) * 0.2;
     ripple = clamp(ripple, 0.0, 1.0);

     // 只在一定范围内产生涟漪
       float rippleArea = smoothstep(0.0, 200.0, distance);
     float alpha = 1.0 - rippleArea;

       // 涟漪颜色 - 青色
     vec3 color = vec3(0.0, 1.0, 1.0) * ripple * 1.5; // 增强颜色强度

     gl_FragColor = vec4(color, alpha * 0.9); // 调整整体透明度
   }
 `;

 const material = new THREE.ShaderMaterial({
   vertexShader,
   fragmentShader,
   uniforms: {
     time: { value: 0.0 },
     center: { value: new THREE.Vector3(0, 0, 0) },
   },
   transparent: true,
   side: THREE.DoubleSide,
 });

 rippleMesh = new THREE.Mesh(geometry, material);
 rippleMesh.rotation.x = -Math.PI / 2; // 平放在地面上
 rippleMesh.position.set(0, 0, 0); // 放置在世界原点
 rippleMesh.position.y = -49; // 调整高度以避免与地面重叠

 scene.add(rippleMesh);
}


// 设置事件监听器
function setupEventListeners() {
 // 窗口大小变化监听器
 const handleResize = () => {
   innerHeight = DOMCityModel.clientHeight;
   innerWidth = DOMCityModel.clientWidth;
   camera.aspect = innerWidth / innerHeight;
   camera.updateProjectionMatrix();
   renderer.setSize(innerWidth, innerHeight);
 };
 
 window.addEventListener("resize", handleResize);
 eventListeners.push({
   target: window,
   type: "resize",
   listener: handleResize
 });

 // 鼠标点击事件监听器
 const handleMouseDown = (event) => {
   const mouse = new THREE.Vector2();
   mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
   mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

   const raycaster = new THREE.Raycaster();
   raycaster.setFromCamera(mouse, camera);

   if (cityModel) {
     const intersects = raycaster.intersectObject(cityModel, true);
     if (intersects.length > 0) {
       console.log("点击了模型:", intersects[0].object.name);
       console.log("相机位置:", camera.position);
     }
   }
 };
 
 window.addEventListener("mousedown", handleMouseDown);
 eventListeners.push({
   target: window,
   type: "mousedown",
   listener: handleMouseDown
 });
}

// 全局变量
let lastTime = 0;

// 启动渲染循环
function startAnimation() {
 animationId = requestAnimationFrame(animate);
}

// 渲染循环
function animate(timestamp = 0) {
 animationId = requestAnimationFrame(animate);
 // ------------------------------------------------------------
 /**
  * 这一段代码负责:计算时间差,限制最大时间差,避免动画跳变
  * 修复人物在不同角度下的动画速度不一样的bug,确保动画在30fps以下,避免跳变
  * */
 // 计算时间差
 const deltaTime = (timestamp - lastTime) / 1000;
 lastTime = timestamp;
 // 限制最大时间差,避免动画跳变
 const clampedDeltaTime = Math.min(deltaTime, 0.033); // 限制在30fps

 if (mixer) {
   mixer.update(clampedDeltaTime);
 }

 // ------------------------------------------------------------
 /**
  * 这一段代码负责:相机飞行动画,
  * */
 // 更新相机平滑过渡
 if (cameraAnimation.active) {
   const elapsed = performance.now() - cameraAnimation.startTime;
   const progress = Math.min(elapsed / cameraAnimation.duration, 1);

   // 使用缓动函数
   const easeProgress = easeInOutCubic(progress);

   // 计算当前位置
   const currentPosition = new THREE.Vector3().lerpVectors(
     cameraAnimation.startPosition,
     cameraAnimation.targetPosition,
     easeProgress,
   );

   // 更新相机位置
   camera.position.copy(currentPosition);

   // 相机看向原点
   camera.lookAt(0, 0, 0);

   // 过渡完成
   if (progress >= 1) {
     cameraAnimation.active = false;
     // 调用回调函数
     if (typeof cameraAnimation.onComplete === "function") {
       cameraAnimation.onComplete();
     }
   }
 }
 // ------------------------------------------------------------
 /**
  * 这一段代码负责:相机更新,包括相机位置、看向目标、旋转角度等
  * */
 controls.update();
 // ------------------------------------------------------------
 /**
  * 这一段代码负责:更新道路流光效果
  * */
 // 更新道路流光效果
 if (cityModel) {
   // 限制更新频率
   if (Date.now() % 2 === 0) { // 每2帧更新一次
     cityModel.traverse((child) => {
       if (child.isMesh && child.userData.isRoad === true) {
         child.userData.animationTime += 0.04;

         if (child.material.uniforms && child.material.uniforms.time) {
           child.material.uniforms.time.value = child.userData.animationTime;
         }
       }
     });
   }
 }
 // ------------------------------------------------------------
 /**
  * 这一段代码负责:更新涟漪效果
  * */
 // 更新涟漪效果
 if (rippleMesh && rippleMesh.material.uniforms) {
   // 限制更新频率
   if (Date.now() % 2 === 0) { // 每2帧更新一次
     rippleMesh.material.uniforms.time.value += 0.02;
   }
 }

 // 渲染场景
 renderer.render(scene, camera);
}

//"第一视角漫游"和"建筑视角漫游"按钮状态
let isOwer = ref(false); //第一视角漫游按钮状态--默认可点击 disabled=false
let isBuilding = ref(true); //建筑视角漫游按钮状态--默认不可点击 disabled=true
/**
* owerSwitch 切换第一视角漫游
* */
function owerSwitch() {
 isOwer.value = true;
 isBuilding.value = false;
 // 切换到第一视角漫游的相机位置--平滑
 smoothCameraTo(new THREE.Vector3(-2, 2, 52), 2000, () => {
   console.log("已到达第一视角");
 });
}
/**
* buildingSwitch 切换建筑视角漫游
* */
function buildingSwitch() {
 isOwer.value = false;
 isBuilding.value = true;
 // 切换到建筑视角漫游的相机位置--平滑
 smoothCameraTo(new THREE.Vector3(179, 12, 164), 2000, () => {
   console.log("已到达建筑视角");
 });
}
// 全局变量
let cameraAnimation = {
 active: false,
 startPosition: new THREE.Vector3(),
 targetPosition: new THREE.Vector3(),
 startTime: 0,
 duration: 2000, // 默认2秒过渡
 onComplete: null, // 动画完成回调
};
/**
* 相机平滑飞行到目标位置
* @param {THREE.Vector3|Array} targetPosition 目标位置,可以是Vector3对象或[x,y,z]数组
* @param {number} duration 过渡时间(毫秒),默认2000ms
* @param {Function} onComplete 过渡完成后的回调函数
*/
function smoothCameraTo(targetPosition, duration = 2000, onComplete = null) {
 console.log("调用相机飞行函数");
 console.log("目标坐标:", targetPosition);

 // 转换目标位置为Vector3
 if (Array.isArray(targetPosition) && targetPosition.length === 3) {
   targetPosition = new THREE.Vector3(
     targetPosition[0],
     targetPosition[1],
     targetPosition[2],
   );
 } else if (!(targetPosition instanceof THREE.Vector3)) {
   console.error("目标位置必须是THREE.Vector3对象或[x,y,z]数组");
   return;
 }

 // 1.记录起始状态
 cameraAnimation.active = true;
 cameraAnimation.startPosition.copy(camera.position);
 cameraAnimation.targetPosition.copy(targetPosition);
 cameraAnimation.startTime = performance.now();
 cameraAnimation.duration = duration;
 cameraAnimation.onComplete = onComplete;
 // 2. 渲染循环中的更新逻辑--在animate函数中调用
}
// 缓动函数
function easeInOutCubic(t) {
 return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

// 帧率监控
let frameCount = 0;
let lastFrameTime = 0;

function monitorFPS() {
 frameCount++;
 const currentTime = performance.now();
 
 if (currentTime - lastFrameTime >= 1000) { // 每秒计算一次
   const fps = frameCount;
   console.log(`FPS: ${fps}`);
   
   // 如果帧率过低,执行性能优化
   if (fps < 30) {
     console.warn("帧率过低,执行性能优化");
     optimizePerformance();
   }
   
   frameCount = 0;
   lastFrameTime = currentTime;
 }
 
 requestAnimationFrame(monitorFPS);
}

// 性能优化函数
function optimizePerformance() {
 // 降低渲染质量
 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.0));
 
 // 简化场景
 if (cityModel) {
   cityModel.traverse((child) => {
     if (child.isMesh) {
       // 降低几何体精度
       if (child.geometry) {
         // 可以在这里添加几何体简化逻辑
       }
     }
   });
 }
}

// 清理函数
function cleanup() {
 // 停止动画循环
 if (animationId) {
   cancelAnimationFrame(animationId);
 }

 // 移除事件监听器
 eventListeners.forEach(({ target, type, listener }) => {
   target.removeEventListener(type, listener);
 });
 eventListeners = [];

 // 释放模型资源
 if (cityModel) {
   scene.remove(cityModel);
   cityModel.traverse((child) => {
     if (child.isMesh) {
       child.geometry.dispose();
       if (child.material) {
         if (Array.isArray(child.material)) {
           child.material.forEach(material => material.dispose());
         } else {
           child.material.dispose();
         }
       }
     }
   });
   cityModel = null;
 }

 if (characterModel) {
   scene.remove(characterModel);
   characterModel.traverse((child) => {
     if (child.isMesh) {
       child.geometry.dispose();
       if (child.material) {
         if (Array.isArray(child.material)) {
           child.material.forEach(material => material.dispose());
         } else {
           child.material.dispose();
         }
       }
     }
   });
   characterModel = null;
 }

 if (rippleMesh) {
   scene.remove(rippleMesh);
   rippleMesh.geometry.dispose();
   rippleMesh.material.dispose();
   rippleMesh = null;
 }

 // 释放渲染器
 if (renderer) {
   renderer.dispose();
   renderer = null;
 }
 
 // 释放其他资源
 mixer = null;
 controls = null;
 camera = null;
 scene = null;
}
</script>

<style scoped>
#cityModel {
 width: 100vw;
 height: 100vh;
 background-color: #ffffff;
}
.button_container {
 position: fixed;
 top: 10px;
 left: 10px;
 display: block;
}
.button_container button {
 padding: 10px 20px;
 background-color: #fff;
 margin: 10px;
 border-radius: 10px;
}
</style>
相关推荐
患得患失9492 小时前
【前端websocket】企业级功能清单
前端·websocket·网络协议
落魄江湖行2 小时前
基础篇四 Nuxt4 全局样式与 CSS 模块
前端·css·typescript·nuxt4
禅思院2 小时前
前端性能优化:从"术"到"道"的完整修炼指南
前端·架构·前端框架
架构师老Y3 小时前
003、Python Web框架深度对比:Django vs Flask vs FastAPI
前端·python·django
Yao.Li4 小时前
PVN3D ORT CUDA Custom Ops 实现与联调记录
人工智能·3d·具身智能
小陈工6 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
NPCZ10 小时前
vite与tailwindcss创建大屏可视化项目
vue
xiaotao13110 小时前
第九章:Vite API 参考手册
前端·vite·前端打包
午安~婉10 小时前
Electron桌面应用聊天(续)
前端·javascript·electron