一、项目成果
作为一名前端开发者,我一直对 3D 可视化技术充满兴趣。在一次个人项目中,我希望通过实践来拓展自己的技术栈,于是选择了实现一个智慧城市的 3D 可视化系统。这个项目不仅需要前端开发技能,还需要掌握 3D 建模和动画制作技术,对我来说是一次很好的自我挑战。
3D城市、人物
后续还会继续完整的写完第一视角漫游,包括人物上楼梯等
---------------------如果需要模型自己尝试一下就私聊-----------------
二、技术栈选择
前端技术
- 框架:Vue 3 + Composition API
- 3D 引擎:Three.js
- 构建工具:Vite
- 包管理器:npm
3D 建模工具
- 建模软件:Blender 3.0+
- 模型格式:GLB (glTF 2.0)
三、开发流程
1. 需求分析与技术规划
作为自我驱动的学习者,我首先明确了项目目标:
- 实现 3D 城市模型的加载和渲染
- 添加人物模型并实现基本动画
- 创建道路流光效果增强视觉表现
- 实现相机视角切换功能
- 添加基本的用户交互控制
技术规划:
- 学习 Blender 基础,创建简单的城市建筑和人物模型
- 掌握 Three.js 核心概念,实现模型加载和渲染
- 探索 Shader 编程,实现道路流光效果
- 研究动画系统,实现人物动画和相机控制
- 持续优化性能,确保应用流畅运行
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>