docs
This commit is contained in:
93
README_2.md
Normal file
93
README_2.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Three.js 3D 可视化开发
|
||||||
|
|
||||||
|
本文档整理了化工/工业仿真平台开发中 8 大核心功能的实现原理,以及 PBR 材质系统的基础概念。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第一部分:8 大核心功能模块实现原理
|
||||||
|
|
||||||
|
### 1. 设置地面 (Ground Setup)
|
||||||
|
* **原理**:使用 `PlaneGeometry` 创建平面网格,并旋转 90 度使其水平平铺。
|
||||||
|
* **材质风格**:
|
||||||
|
* **实景风**:使用 PBR 材质加载沥青、水泥或草地贴图。
|
||||||
|
* **科技风**:使用 `GridHelper` 或自定义 Shader 制作深色背景下的发光网格(工业软件常用)。
|
||||||
|
* **关键点**:必须开启 `receiveShadow = true`,否则模型会有“悬浮感”。
|
||||||
|
|
||||||
|
### 2. 地面光效 (Ground Lighting Effects)
|
||||||
|
* **原理**:利用 **Shader(着色器)** 或 **贴图映射** 模拟光效,而非消耗性能的实时光源。
|
||||||
|
* **常见效果**:
|
||||||
|
* **镜面反射**:使用 `Reflector` 或 `SSR` 模拟潮湿/打蜡地面。
|
||||||
|
* **雷达/扩散波**:在 Shader 中利用距离和正弦波 (`sin(time)`) 计算透明度,形成向外扩散的光环。
|
||||||
|
* **聚焦光**:配合 `SpotLight` 和体积光技术模拟丁达尔效应。
|
||||||
|
|
||||||
|
### 3. 模型漫游 (Roaming / Inspection)
|
||||||
|
* **本质**:摄像机(Camera)位置和朝向的连续变化。
|
||||||
|
* **实现方式**:
|
||||||
|
* **自动巡检**:使用 `CatmullRomCurve3` 定义路径,通过 `progress (0-1)` 获取坐标赋值给摄像机。使用 `getTangentAt` 保持视线朝前。
|
||||||
|
* **点击移动**:利用 **Raycaster** 获取地面点击坐标,使用动画库(GSAP/TWEEN)平滑过渡摄像机位置。
|
||||||
|
|
||||||
|
### 4. 模型动画 (Model Animation)
|
||||||
|
* **原理**:播放模型文件(GLTF/FBX)自带的骨骼或关键帧动画。
|
||||||
|
* **核心系统**:`AnimationMixer`。
|
||||||
|
* **流程**:获取 `clips` -> 创建 `mixer` -> 生成 `action` -> 调用 `.play()`。
|
||||||
|
* **驱动**:必须在渲染循环中每一帧调用 `mixer.update(deltaTime)`。
|
||||||
|
|
||||||
|
### 5. 修改材质与告警 (Material Changes / Alerts)
|
||||||
|
* **原理**:遍历模型树,查找目标 Mesh 并修改 Material 属性。
|
||||||
|
* **告警实现**:
|
||||||
|
* **查找**:`scene.getObjectByName('设备ID')`。
|
||||||
|
* **高亮**:修改 `material.emissive`(自发光颜色)及强度,确保在暗处可见。
|
||||||
|
* **呼吸闪烁**:在渲染循环中利用 `Math.sin(Date.now())` 动态改变发光强度或透明度。
|
||||||
|
|
||||||
|
### 6. 粒子效果 (Particles - 蒸汽/火焰)
|
||||||
|
* **原理**:大量始终面向摄像机的微小平面(Billboards/Sprites)。
|
||||||
|
* **数据结构**:`THREE.Points` + `BufferGeometry`。
|
||||||
|
* **运动逻辑**:每一帧更新粒子 Y 轴(上升),增加 X/Z 轴随机扰动(扩散),生命周期结束时重置位置。
|
||||||
|
|
||||||
|
### 7. 模型与 Web 交互 (Interaction)
|
||||||
|
* **鼠标 -> 3D (点击)**:
|
||||||
|
* 利用 **Raycaster** 将屏幕坐标转为射线,检测与 Mesh 的相交,获取点击对象 ID 以触发 UI 弹窗。
|
||||||
|
* **3D -> 2D (标签跟随)**:
|
||||||
|
* 利用 **`CSS2DRenderer`** 或坐标投影 (`vector.project(camera)`)。
|
||||||
|
* 将 3D 坐标转为屏幕 XY 坐标,通过 CSS `transform` 移动 HTML 标签,使其“粘”在模型上。
|
||||||
|
|
||||||
|
### 8. 后期处理 (Post-processing)
|
||||||
|
* **原理**:`EffectComposer` 接管渲染流程,对离屏缓冲区图像进行像素级处理。
|
||||||
|
* **常用流水线**:
|
||||||
|
1. `RenderPass` (基础画面)
|
||||||
|
2. `OutlinePass` (鼠标悬停高亮/描边)
|
||||||
|
3. `UnrealBloomPass` (辉光/泛光,增加科技感)
|
||||||
|
4. `FXAAPass` / `SMAAPass` (抗锯齿)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第二部分:PBR 材质贴图详解
|
||||||
|
|
||||||
|
在物理渲染(PBR)流程中,这三张贴图决定了模型的质感与真实度。
|
||||||
|
|
||||||
|
### 1. 纹理贴图 (Albedo / Base Color Map)
|
||||||
|
* **通俗解释**:**“包装纸”** 或 **“颜值”**。
|
||||||
|
* **作用**:决定物体表面的基础颜色和图案。
|
||||||
|
* **代码属性**:`material.map`
|
||||||
|
|
||||||
|
### 2. 法线贴图 (Normal Map)
|
||||||
|
* **通俗解释**:**“伪造的凹凸”** 或 **“光影魔术”**。
|
||||||
|
* **特征**:通常为紫蓝色图片。
|
||||||
|
* **作用**:欺骗光线,让平坦的模型表面呈现出凹凸细节(如砖缝、刻痕),无需增加几何体面数。
|
||||||
|
* **代码属性**:`material.normalMap`
|
||||||
|
|
||||||
|
### 3. 粗糙度贴图 (Roughness Map)
|
||||||
|
* **通俗解释**:**“磨砂纸的目数”** 或 **“质感”**。
|
||||||
|
* **特征**:黑白灰图片。
|
||||||
|
* **作用**:决定表面反光的清晰度。
|
||||||
|
* **黑色 (0.0)**:光滑、镜面反射(如湿瓷砖)。
|
||||||
|
* **白色 (1.0)**:粗糙、漫反射(如水泥、灰尘)。
|
||||||
|
* **灰色**:半哑光(如拉丝金属)。
|
||||||
|
* **代码属性**:`material.roughnessMap`
|
||||||
|
|
||||||
|
### 综合应用示例:旧化工反应罐
|
||||||
|
| 贴图类型 | 视觉贡献 |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Color Map** | 决定罐子是绿色的,局部有棕色锈迹。 |
|
||||||
|
| **Normal Map** | 让锈迹看起来凹凸不平,漆面有剥落感。 |
|
||||||
|
| **Roughness Map** | 让绿色漆面有微弱反光,而锈迹部分完全粗糙哑光。 |
|
||||||
@@ -40,6 +40,8 @@ const flameEmitters: any[] = [] // 所有火焰发射器数组
|
|||||||
const burningMeshes = new Set<THREE.Mesh>() // 已着火的网格集合
|
const burningMeshes = new Set<THREE.Mesh>() // 已着火的网格集合
|
||||||
let spreadTimer = 0 // 蔓延计时器
|
let spreadTimer = 0 // 蔓延计时器
|
||||||
const spreadInterval = 2 // 每2秒蔓延一次
|
const spreadInterval = 2 // 每2秒蔓延一次
|
||||||
|
let centerFireEmitter: any = null // 中心大火发射器
|
||||||
|
let totalMeshCount = 0 // 模型中网格总数
|
||||||
|
|
||||||
// 地板流动效果
|
// 地板流动效果
|
||||||
let flowingGround: any
|
let flowingGround: any
|
||||||
@@ -204,8 +206,11 @@ function loadModel(particleUtils: any) {
|
|||||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||||
model = gltf.scene
|
model = gltf.scene
|
||||||
|
|
||||||
|
// 统计网格总数
|
||||||
|
totalMeshCount = 0
|
||||||
model.traverse((child) => {
|
model.traverse((child) => {
|
||||||
if (child instanceof THREE.Mesh) {
|
if (child instanceof THREE.Mesh) {
|
||||||
|
totalMeshCount++
|
||||||
child.castShadow = true
|
child.castShadow = true
|
||||||
child.receiveShadow = true
|
child.receiveShadow = true
|
||||||
child.userData.objectType = '建筑构件'
|
child.userData.objectType = '建筑构件'
|
||||||
@@ -292,6 +297,54 @@ function spreadFire() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 检查是否已经蔓延到整个模型(80%以上的网格着火)
|
||||||
|
if (burningMeshes.size >= totalMeshCount * 0.8 && !centerFireEmitter) {
|
||||||
|
createCenterFire()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建中心大火效果
|
||||||
|
function createCenterFire() {
|
||||||
|
if (!model)
|
||||||
|
return
|
||||||
|
|
||||||
|
const particleUtils = useParticleSystem()
|
||||||
|
|
||||||
|
// 移除所有小火焰发射器
|
||||||
|
flameEmitters.forEach(({ emitter }) => {
|
||||||
|
emitter.stop()
|
||||||
|
// 保留发光效果
|
||||||
|
})
|
||||||
|
flameEmitters.length = 0
|
||||||
|
|
||||||
|
// 计算模型中心和范围
|
||||||
|
const modelBox = new THREE.Box3().setFromObject(model)
|
||||||
|
const modelCenter = modelBox.getCenter(new THREE.Vector3())
|
||||||
|
const modelSize = modelBox.getSize(new THREE.Vector3())
|
||||||
|
|
||||||
|
// 在中心创建大型火焰发射器
|
||||||
|
centerFireEmitter = particleUtils.createFlameEmitter(
|
||||||
|
scene,
|
||||||
|
new THREE.Vector3(modelCenter.x, modelBox.min.y + modelSize.y * 0.3, modelCenter.z),
|
||||||
|
30, // 大量粒子
|
||||||
|
)
|
||||||
|
|
||||||
|
// 添加多个位置的火焰,模拟整体燃烧
|
||||||
|
const positions = [
|
||||||
|
new THREE.Vector3(modelBox.min.x, modelBox.min.y + 1, modelCenter.z),
|
||||||
|
new THREE.Vector3(modelBox.max.x, modelBox.min.y + 1, modelCenter.z),
|
||||||
|
new THREE.Vector3(modelCenter.x, modelBox.min.y + 1, modelBox.min.z),
|
||||||
|
new THREE.Vector3(modelCenter.x, modelBox.min.y + 1, modelBox.max.z),
|
||||||
|
]
|
||||||
|
|
||||||
|
positions.forEach((pos) => {
|
||||||
|
const emitter = particleUtils.createFlameEmitter(scene, pos, 20)
|
||||||
|
flameEmitters.push({ emitter, mesh: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加主中心发射器
|
||||||
|
flameEmitters.push({ emitter: centerFireEmitter, mesh: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
@@ -495,6 +548,20 @@ function toggleFlame() {
|
|||||||
emitter.stop()
|
emitter.stop()
|
||||||
|
|
||||||
// 移除发光效果
|
// 移除发光效果
|
||||||
|
if (mesh && mesh.material) {
|
||||||
|
const material = mesh.material as THREE.MeshStandardMaterial
|
||||||
|
material.emissive = new THREE.Color(0x000000)
|
||||||
|
material.emissiveIntensity = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 停止中心大火
|
||||||
|
if (centerFireEmitter) {
|
||||||
|
centerFireEmitter = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除所有物体的发光效果
|
||||||
|
burningMeshes.forEach((mesh) => {
|
||||||
if (mesh.material) {
|
if (mesh.material) {
|
||||||
const material = mesh.material as THREE.MeshStandardMaterial
|
const material = mesh.material as THREE.MeshStandardMaterial
|
||||||
material.emissive = new THREE.Color(0x000000)
|
material.emissive = new THREE.Color(0x000000)
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
<!--
|
||||||
|
* @Description: Demo 5 - 地面光效动画
|
||||||
|
* @功能说明: 实现地面流动的彩色光效线条,模拟科幻风格的地面扫描效果
|
||||||
|
* @核心技术:
|
||||||
|
* 1. Three.js 裁剪平面(Clipping Planes)- 限制线条在地面范围内显示
|
||||||
|
* 2. 动态位置更新 - 实现线条在地面上来回移动的动画效果
|
||||||
|
* 3. 延迟启动机制 - 不同颜色线条依次出现,形成波浪式动画
|
||||||
|
* 4. 双轴运动 - 线条同时在 X 轴和 Z 轴方向移动,形成交叉效果
|
||||||
|
* @Autor: 刘 相卿
|
||||||
|
* @Date: 2025-11-17 16:07:33
|
||||||
|
* @LastEditors: 刘 相卿
|
||||||
|
* @LastEditTime: 2025-11-21 10:04:12
|
||||||
|
-->
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||||
@@ -5,32 +18,41 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|||||||
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
|
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
|
||||||
|
|
||||||
// ========== 常量定义 ==========
|
// ========== 常量定义 ==========
|
||||||
const GROUND_SIZE = 30
|
const GROUND_SIZE = 30 // 地面尺寸(30x30)
|
||||||
const GROUND_THICKNESS = 0.2
|
const GROUND_THICKNESS = 0.2 // 地面厚度
|
||||||
const LINE_COLORS = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF]
|
const LINE_COLORS = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF] // 6种光效颜色:红绿蓝黄紫青
|
||||||
const LINE_SPEED = 0.1
|
const LINE_SPEED = 0.1 // 线条移动速度
|
||||||
const LINE_DELAY = 300
|
const LINE_DELAY = 300 // 每条线启动延迟(毫秒)
|
||||||
|
|
||||||
// ========== 类型定义 ==========
|
// ========== 类型定义 ==========
|
||||||
|
/**
|
||||||
|
* 线条状态接口
|
||||||
|
* 用于追踪每条光效线的运动状态
|
||||||
|
*/
|
||||||
interface LineState {
|
interface LineState {
|
||||||
mesh: THREE.Mesh
|
mesh: THREE.Mesh // 线条网格对象
|
||||||
direction: number
|
direction: number // 移动方向:1 或 -1
|
||||||
startDelay: number
|
startDelay: number // 启动延迟时间(毫秒)
|
||||||
elapsed: number
|
elapsed: number // 已经过时间(毫秒)
|
||||||
axis: 'x' | 'z'
|
axis: 'x' | 'z' // 移动轴向:x 轴或 z 轴
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 状态变量 ==========
|
// ========== 状态变量 ==========
|
||||||
const threeRef = shallowRef<HTMLCanvasElement>()
|
const threeRef = shallowRef<HTMLCanvasElement>() // Canvas 容器引用
|
||||||
|
|
||||||
let scene: THREE.Scene
|
// Three.js 核心对象
|
||||||
let camera: THREE.PerspectiveCamera
|
let scene: THREE.Scene // 场景对象
|
||||||
let renderer: THREE.WebGLRenderer
|
let camera: THREE.PerspectiveCamera // 透视相机
|
||||||
let controls: OrbitControls
|
let renderer: THREE.WebGLRenderer // WebGL 渲染器
|
||||||
let animationId: number
|
let controls: OrbitControls // 轨道控制器
|
||||||
|
let animationId: number // 动画帧 ID
|
||||||
|
|
||||||
const lineStates: LineState[] = []
|
const lineStates: LineState[] = [] // 所有光效线条的状态数组
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取容器尺寸
|
||||||
|
* @returns {width, height} 容器的宽度和高度
|
||||||
|
*/
|
||||||
function getElementSize() {
|
function getElementSize() {
|
||||||
const width = threeRef.value!.clientWidth
|
const width = threeRef.value!.clientWidth
|
||||||
const height = threeRef.value!.clientHeight
|
const height = threeRef.value!.clientHeight
|
||||||
@@ -39,43 +61,48 @@ function getElementSize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== 初始化场景 ==========
|
// ========== 初始化场景 ==========
|
||||||
|
/**
|
||||||
|
* 初始化 Three.js 场景
|
||||||
|
* 创建场景、相机、渲染器、控制器等核心对象
|
||||||
|
*/
|
||||||
function initModel() {
|
function initModel() {
|
||||||
const { width, height } = getElementSize()
|
const { width, height } = getElementSize()
|
||||||
|
|
||||||
// 创建场景
|
// 创建场景 - 所有 3D 对象的容器
|
||||||
scene = new THREE.Scene()
|
scene = new THREE.Scene()
|
||||||
scene.background = new THREE.Color(0x000000)
|
scene.background = new THREE.Color(0x000000) // 黑色背景
|
||||||
|
|
||||||
// 创建相机
|
// 创建透视相机 - 模拟人眼视角
|
||||||
|
// 参数:视场角 75°, 宽高比, 近裁剪面 0.1, 远裁剪面 1000
|
||||||
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
|
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
|
||||||
camera.position.set(3, 8, 12)
|
camera.position.set(3, 8, 12) // 设置相机位置(斜上方观察)
|
||||||
|
|
||||||
// 创建渲染器
|
// 创建 WebGL 渲染器
|
||||||
renderer = new THREE.WebGLRenderer({ antialias: true })
|
renderer = new THREE.WebGLRenderer({ antialias: true }) // 开启抗锯齿
|
||||||
renderer.setSize(width, height)
|
renderer.setSize(width, height) // 设置渲染尺寸
|
||||||
renderer.setPixelRatio(window.devicePixelRatio)
|
renderer.setPixelRatio(window.devicePixelRatio) // 设置设备像素比(高清屏适配)
|
||||||
renderer.shadowMap.enabled = true
|
renderer.shadowMap.enabled = true // 启用阴影
|
||||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap // 使用 PCF 软阴影
|
||||||
renderer.localClippingEnabled = true
|
renderer.localClippingEnabled = true // 启用局部裁剪(用于光效线条边界裁剪)
|
||||||
threeRef.value!.appendChild(renderer.domElement)
|
threeRef.value!.appendChild(renderer.domElement) // 将渲染器的 canvas 添加到 DOM
|
||||||
|
|
||||||
// 加载模型
|
// 加载 GLTF 模型
|
||||||
const loader = new GLTFLoader()
|
const loader = new GLTFLoader()
|
||||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||||
scene.add(gltf.scene)
|
scene.add(gltf.scene) // 将模型添加到场景
|
||||||
})
|
})
|
||||||
|
|
||||||
// 创建控制器
|
// 创建轨道控制器 - 允许用户通过鼠标控制相机
|
||||||
controls = new OrbitControls(camera, renderer.domElement)
|
controls = new OrbitControls(camera, renderer.domElement)
|
||||||
controls.enableDamping = true
|
controls.enableDamping = true // 启用阻尼(惯性)
|
||||||
controls.dampingFactor = 0.05
|
controls.dampingFactor = 0.05 // 阻尼系数
|
||||||
|
|
||||||
// 添加辅助工具
|
// 添加坐标轴辅助工具(红 X, 绿 Y, 蓝 Z)
|
||||||
const axesHelper = new THREE.AxesHelper(5)
|
const axesHelper = new THREE.AxesHelper(5)
|
||||||
scene.add(axesHelper)
|
scene.add(axesHelper)
|
||||||
|
|
||||||
// 添加环境光
|
// 添加环境光 - 提供基础照明
|
||||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.2)
|
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.2) // 白色环境光,强度 1.2
|
||||||
scene.add(ambientLight)
|
scene.add(ambientLight)
|
||||||
|
|
||||||
// 添加地面
|
// 添加地面
|
||||||
@@ -86,135 +113,217 @@ function initModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== 添加地面 ==========
|
// ========== 添加地面 ==========
|
||||||
|
/**
|
||||||
|
* 创建地面平台
|
||||||
|
* 使用带厚度的立方体作为地面,用于承载光效动画
|
||||||
|
*/
|
||||||
function addGround() {
|
function addGround() {
|
||||||
|
// 创建立方体几何体(宽 30, 高 0.2, 深 30)
|
||||||
const geometry = new THREE.BoxGeometry(GROUND_SIZE, GROUND_THICKNESS, GROUND_SIZE)
|
const geometry = new THREE.BoxGeometry(GROUND_SIZE, GROUND_THICKNESS, GROUND_SIZE)
|
||||||
|
// 创建标准材质(深灰色)
|
||||||
const material = new THREE.MeshStandardMaterial({ color: 0x222222 })
|
const material = new THREE.MeshStandardMaterial({ color: 0x222222 })
|
||||||
|
// 创建网格对象
|
||||||
const ground = new THREE.Mesh(geometry, material)
|
const ground = new THREE.Mesh(geometry, material)
|
||||||
|
// 设置地面位置(Y 轴向下偏移一半厚度,使顶面在 Y=0)
|
||||||
ground.position.y = -GROUND_THICKNESS / 2
|
ground.position.y = -GROUND_THICKNESS / 2
|
||||||
ground.receiveShadow = true
|
ground.receiveShadow = true // 接收阴影
|
||||||
scene.add(ground)
|
scene.add(ground)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 添加地面光效 ==========
|
// ========== 添加地面光效 ==========
|
||||||
|
/**
|
||||||
|
* 添加地面流动光效
|
||||||
|
* 创建多条彩色光带在地面上来回移动,形成科幻扫描效果
|
||||||
|
*
|
||||||
|
* 技术要点:
|
||||||
|
* 1. 使用裁剪平面确保光带只在地面范围内显示
|
||||||
|
* 2. 为每条光带设置不同的启动延迟,形成波浪效果
|
||||||
|
* 3. 光带同时在 X 轴和 Z 轴方向运动,形成交叉图案
|
||||||
|
* 4. 使用半透明材质和不同透明度创造层次感
|
||||||
|
*/
|
||||||
function addGroundLightEffect() {
|
function addGroundLightEffect() {
|
||||||
// 创建裁剪平面
|
// 创建四个裁剪平面,限制光效在地面范围内
|
||||||
|
// 裁剪平面的法向量指向内部,距离原点 GROUND_SIZE/2
|
||||||
const clipPlanes = [
|
const clipPlanes = [
|
||||||
new THREE.Plane(new THREE.Vector3(1, 0, 0), GROUND_SIZE / 2),
|
new THREE.Plane(new THREE.Vector3(1, 0, 0), GROUND_SIZE / 2), // 左边界(法向量向右)
|
||||||
new THREE.Plane(new THREE.Vector3(-1, 0, 0), GROUND_SIZE / 2),
|
new THREE.Plane(new THREE.Vector3(-1, 0, 0), GROUND_SIZE / 2), // 右边界(法向量向左)
|
||||||
new THREE.Plane(new THREE.Vector3(0, 0, 1), GROUND_SIZE / 2),
|
new THREE.Plane(new THREE.Vector3(0, 0, 1), GROUND_SIZE / 2), // 前边界(法向量向后)
|
||||||
new THREE.Plane(new THREE.Vector3(0, 0, -1), GROUND_SIZE / 2),
|
new THREE.Plane(new THREE.Vector3(0, 0, -1), GROUND_SIZE / 2), // 后边界(法向量向前)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 创建光带的几何体(长 30, 高 0.1, 宽 0.5 的扁平立方体)
|
||||||
const geometry = new THREE.BoxGeometry(GROUND_SIZE, 0.1, 0.5)
|
const geometry = new THREE.BoxGeometry(GROUND_SIZE, 0.1, 0.5)
|
||||||
|
|
||||||
|
// 为每种颜色创建一对光带(X 轴和 Z 轴各一条)
|
||||||
LINE_COLORS.forEach((color, index) => {
|
LINE_COLORS.forEach((color, index) => {
|
||||||
|
// 创建基础材质
|
||||||
const material = new THREE.MeshBasicMaterial({
|
const material = new THREE.MeshBasicMaterial({
|
||||||
color: new THREE.Color(color),
|
color: new THREE.Color(color), // 光带颜色
|
||||||
transparent: true,
|
transparent: true, // 启用透明
|
||||||
opacity: 0.15 * (index + 1) * Math.random(),
|
opacity: 0.15 * (index + 1) * Math.random(), // 随机透明度(越后面越不透明)
|
||||||
clippingPlanes: clipPlanes,
|
clippingPlanes: clipPlanes, // 应用裁剪平面
|
||||||
clipShadows: true,
|
clipShadows: true, // 裁剪阴影
|
||||||
depthWrite: false,
|
depthWrite: false, // 禁用深度写入(避免透明物体遮挡问题)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 计算光带在垂直方向上的偏移位置
|
||||||
|
// 将地面平均分成 6 段,每条光带占一段的中心位置
|
||||||
const offset = -GROUND_SIZE / 2 + (GROUND_SIZE / LINE_COLORS.length) * (index + 0.5)
|
const offset = -GROUND_SIZE / 2 + (GROUND_SIZE / LINE_COLORS.length) * (index + 0.5)
|
||||||
const isEven = index % 2 === 0
|
const isEven = index % 2 === 0 // 判断是否为偶数索引
|
||||||
|
|
||||||
// 创建X轴线条
|
// ===== 创建 X 轴方向的光带 =====
|
||||||
const xMesh = new THREE.Mesh(geometry, material.clone())
|
const xMesh = new THREE.Mesh(geometry, material.clone())
|
||||||
|
// 设置初始位置:偶数索引从右边开始,奇数从左边开始
|
||||||
xMesh.position.set(isEven ? GROUND_SIZE : -GROUND_SIZE, 0, offset)
|
xMesh.position.set(isEven ? GROUND_SIZE : -GROUND_SIZE, 0, offset)
|
||||||
xMesh.renderOrder = 1
|
xMesh.renderOrder = 1 // 设置渲染顺序(确保正确的透明渲染)
|
||||||
scene.add(xMesh)
|
scene.add(xMesh)
|
||||||
|
|
||||||
|
// 记录 X 轴光带的状态
|
||||||
lineStates.push({
|
lineStates.push({
|
||||||
mesh: xMesh,
|
mesh: xMesh,
|
||||||
direction: isEven ? -1 : 1,
|
direction: isEven ? -1 : 1, // 移动方向:偶数向左,奇数向右
|
||||||
startDelay: index * LINE_DELAY,
|
startDelay: index * LINE_DELAY, // 启动延迟:依次增加
|
||||||
elapsed: 0,
|
elapsed: 0, // 已过时间初始化为 0
|
||||||
axis: 'x',
|
axis: 'x', // 运动轴向为 X
|
||||||
})
|
})
|
||||||
|
|
||||||
// 创建Z轴线条
|
// ===== 创建 Z 轴方向的光带 =====
|
||||||
const zMesh = new THREE.Mesh(geometry, material.clone())
|
const zMesh = new THREE.Mesh(geometry, material.clone())
|
||||||
|
// 设置初始位置:偶数索引从后边开始,奇数从前边开始
|
||||||
zMesh.position.set(offset, 0.01, isEven ? GROUND_SIZE : -GROUND_SIZE)
|
zMesh.position.set(offset, 0.01, isEven ? GROUND_SIZE : -GROUND_SIZE)
|
||||||
zMesh.rotation.y = Math.PI / 2
|
zMesh.rotation.y = Math.PI / 2 // 旋转 90° 使光带沿 Z 轴方向
|
||||||
zMesh.renderOrder = 2
|
zMesh.renderOrder = 2 // 渲染顺序略高于 X 轴光带
|
||||||
scene.add(zMesh)
|
scene.add(zMesh)
|
||||||
|
|
||||||
|
// 记录 Z 轴光带的状态
|
||||||
lineStates.push({
|
lineStates.push({
|
||||||
mesh: zMesh,
|
mesh: zMesh,
|
||||||
direction: isEven ? -1 : 1,
|
direction: isEven ? -1 : 1, // 移动方向:偶数向前,奇数向后
|
||||||
startDelay: index * LINE_DELAY,
|
startDelay: index * LINE_DELAY, // 启动延迟:依次增加
|
||||||
elapsed: 0,
|
elapsed: 0, // 已过时间初始化为 0
|
||||||
axis: 'z',
|
axis: 'z', // 运动轴向为 Z
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 更新线条动画 ==========
|
// ========== 更新线条动画 ==========
|
||||||
|
/**
|
||||||
|
* 更新所有光效线条的位置
|
||||||
|
* 实现来回移动的动画效果
|
||||||
|
*
|
||||||
|
* @param deltaTime - 距离上一帧的时间差(毫秒)
|
||||||
|
*
|
||||||
|
* 动画逻辑:
|
||||||
|
* 1. 检查延迟时间是否已到(未到则跳过)
|
||||||
|
* 2. 检测是否到达边界,到达则反转方向
|
||||||
|
* 3. 根据方向和速度更新位置
|
||||||
|
*/
|
||||||
function updateLines(deltaTime: number) {
|
function updateLines(deltaTime: number) {
|
||||||
lineStates.forEach((state) => {
|
lineStates.forEach((state) => {
|
||||||
|
// 累计已过时间
|
||||||
state.elapsed += deltaTime
|
state.elapsed += deltaTime
|
||||||
|
// 如果还未达到启动延迟时间,跳过此光带
|
||||||
if (state.elapsed < state.startDelay)
|
if (state.elapsed < state.startDelay)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
// 获取当前位置(X 轴或 Z 轴)
|
||||||
const pos = state.axis === 'x' ? state.mesh.position.x : state.mesh.position.z
|
const pos = state.axis === 'x' ? state.mesh.position.x : state.mesh.position.z
|
||||||
|
|
||||||
// 边界检测并反转方向
|
// 边界检测:到达边界时反转方向
|
||||||
|
// 向右/向后移动超过边界,改为向左/向前
|
||||||
if (pos >= GROUND_SIZE)
|
if (pos >= GROUND_SIZE)
|
||||||
state.direction = -1
|
state.direction = -1
|
||||||
|
// 向左/向前移动超过边界,改为向右/向后
|
||||||
else if (pos <= -GROUND_SIZE)
|
else if (pos <= -GROUND_SIZE)
|
||||||
state.direction = 1
|
state.direction = 1
|
||||||
|
|
||||||
// 更新位置
|
// 计算位置变化量(速度 × 方向)
|
||||||
const delta = LINE_SPEED * state.direction
|
const delta = LINE_SPEED * state.direction
|
||||||
|
|
||||||
|
// 根据轴向更新对应的位置
|
||||||
if (state.axis === 'x') {
|
if (state.axis === 'x') {
|
||||||
state.mesh.position.x += delta
|
state.mesh.position.x += delta // 更新 X 坐标
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
state.mesh.position.z += delta
|
state.mesh.position.z += delta // 更新 Z 坐标
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 主动画循环 ==========
|
// ========== 主动画循环 ==========
|
||||||
let lastTime = 0
|
let lastTime = 0 // 上一帧的时间戳
|
||||||
|
/**
|
||||||
|
* 主动画循环函数
|
||||||
|
* 每帧调用一次,更新场景并渲染
|
||||||
|
*
|
||||||
|
* @param time - 当前时间戳(毫秒)
|
||||||
|
*
|
||||||
|
* 执行流程:
|
||||||
|
* 1. 请求下一帧动画
|
||||||
|
* 2. 计算时间差(deltaTime)
|
||||||
|
* 3. 更新控制器状态
|
||||||
|
* 4. 更新光效动画
|
||||||
|
* 5. 响应式调整渲染器尺寸
|
||||||
|
* 6. 渲染场景
|
||||||
|
*/
|
||||||
function animate(time = 0) {
|
function animate(time = 0) {
|
||||||
|
// 请求下一帧动画(递归调用)
|
||||||
animationId = requestAnimationFrame(animate)
|
animationId = requestAnimationFrame(animate)
|
||||||
|
|
||||||
|
// 计算距离上一帧的时间差
|
||||||
const deltaTime = time - lastTime
|
const deltaTime = time - lastTime
|
||||||
lastTime = time
|
lastTime = time
|
||||||
|
|
||||||
// 更新控制器
|
// 更新轨道控制器(处理阻尼效果)
|
||||||
controls.update()
|
controls.update()
|
||||||
|
|
||||||
// 更新线条动画
|
// 更新所有光效线条的位置
|
||||||
updateLines(deltaTime)
|
updateLines(deltaTime)
|
||||||
|
|
||||||
// 响应式调整渲染器大小
|
// 响应式调整渲染器大小(窗口尺寸变化时)
|
||||||
const { width, height } = getElementSize()
|
const { width, height } = getElementSize()
|
||||||
if (camera.aspect !== width / height) {
|
if (camera.aspect !== width / height) {
|
||||||
camera.aspect = width / height
|
camera.aspect = width / height // 更新相机宽高比
|
||||||
camera.updateProjectionMatrix()
|
camera.updateProjectionMatrix() // 更新投影矩阵
|
||||||
renderer.setSize(width, height)
|
renderer.setSize(width, height) // 更新渲染器尺寸
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染场景
|
||||||
renderer.render(scene, camera)
|
renderer.render(scene, camera)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 清理资源 ==========
|
// ========== 清理资源 ==========
|
||||||
|
/**
|
||||||
|
* 清理所有 Three.js 资源
|
||||||
|
* 防止内存泄漏,组件卸载时调用
|
||||||
|
*
|
||||||
|
* 清理内容:
|
||||||
|
* 1. 取消动画帧
|
||||||
|
* 2. 销毁控制器
|
||||||
|
* 3. 销毁渲染器
|
||||||
|
* 4. 释放几何体和材质
|
||||||
|
* 5. 清空状态数组
|
||||||
|
*/
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
|
// 取消动画帧
|
||||||
if (animationId)
|
if (animationId)
|
||||||
cancelAnimationFrame(animationId)
|
cancelAnimationFrame(animationId)
|
||||||
|
|
||||||
|
// 销毁轨道控制器
|
||||||
if (controls)
|
if (controls)
|
||||||
controls.dispose()
|
controls.dispose()
|
||||||
|
|
||||||
|
// 销毁渲染器并移除 DOM 元素
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
renderer.dispose()
|
renderer.dispose()
|
||||||
threeRef.value?.removeChild(renderer.domElement)
|
threeRef.value?.removeChild(renderer.domElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 遍历场景,释放所有网格的资源
|
||||||
scene?.traverse((object) => {
|
scene?.traverse((object) => {
|
||||||
if (object instanceof THREE.Mesh) {
|
if (object instanceof THREE.Mesh) {
|
||||||
object.geometry?.dispose()
|
object.geometry?.dispose() // 释放几何体
|
||||||
|
// 处理材质(可能是单个或数组)
|
||||||
if (Array.isArray(object.material)) {
|
if (Array.isArray(object.material)) {
|
||||||
object.material.forEach(m => m.dispose())
|
object.material.forEach(m => m.dispose())
|
||||||
}
|
}
|
||||||
@@ -223,32 +332,49 @@ function cleanup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 清空光效状态数组
|
||||||
lineStates.length = 0
|
lineStates.length = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停地面光效动画
|
||||||
|
* 停止动画循环,但保留所有光带对象
|
||||||
|
*/
|
||||||
function pauseGroundLightEffect() {
|
function pauseGroundLightEffect() {
|
||||||
cancelAnimationFrame(animationId)
|
cancelAnimationFrame(animationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复地面光效动画
|
||||||
|
* 重新启动动画循环
|
||||||
|
*/
|
||||||
function resumeGroundLightEffect() {
|
function resumeGroundLightEffect() {
|
||||||
animate()
|
animate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁地面光效
|
||||||
|
* 从场景中移除所有光带对象
|
||||||
|
*/
|
||||||
function destoryGroundLightEffect() {
|
function destoryGroundLightEffect() {
|
||||||
lineStates.forEach((state) => {
|
lineStates.forEach((state) => {
|
||||||
state.mesh.parent?.remove(state.mesh)
|
state.mesh.parent?.remove(state.mesh) // 从父对象(场景)中移除
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(initModel)
|
// 生命周期钩子
|
||||||
onBeforeUnmount(cleanup)
|
onMounted(initModel) // 组件挂载时初始化场景
|
||||||
|
onBeforeUnmount(cleanup) // 组件卸载前清理资源
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="threeRef" class="model" />
|
<div ref="threeRef" class="model" />
|
||||||
|
<!-- 导航链接:跳转到下一个 Demo -->
|
||||||
<router-link to="/three/6" class="position-fixed left-20px top-20px">
|
<router-link to="/three/6" class="position-fixed left-20px top-20px">
|
||||||
模型漫游(巡检业务)
|
模型漫游(巡检业务)
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<!-- 控制按钮组 -->
|
||||||
<div class="position-fixed right-20px top-20px flex gap-12px">
|
<div class="position-fixed right-20px top-20px flex gap-12px">
|
||||||
<button @click="addGroundLightEffect">
|
<button @click="addGroundLightEffect">
|
||||||
添加地面光效
|
添加地面光效
|
||||||
|
|||||||
@@ -1,113 +1,191 @@
|
|||||||
|
<!--
|
||||||
|
* @Description: Demo 6 - 模型动画播放(巡检业务)
|
||||||
|
* @功能说明: 加载带有动画的 GLTF 模型并播放内置动画
|
||||||
|
* @核心技术:
|
||||||
|
* 1. GLTFLoader - 加载 GLTF/GLB 格式的 3D 模型
|
||||||
|
* 2. AnimationMixer - Three.js 动画混合器,用于管理和播放动画
|
||||||
|
* 3. AnimationAction - 动画动作控制器,控制动画的播放、暂停、停止等
|
||||||
|
* 4. Clock - 时钟对象,用于计算动画帧之间的时间差
|
||||||
|
* @业务场景: 巡检机器人/设备的运动轨迹动画播放
|
||||||
|
* @Autor: 刘 相卿
|
||||||
|
* @Date: 2025-11-17 16:07:33
|
||||||
|
-->
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||||
import { onMounted, shallowRef } from 'vue'
|
import { onMounted, shallowRef } from 'vue'
|
||||||
|
|
||||||
const threeRef = shallowRef<HTMLCanvasElement>()
|
// ========== 响应式状态 ==========
|
||||||
|
const threeRef = shallowRef<HTMLCanvasElement>() // Canvas 容器引用
|
||||||
|
|
||||||
|
// 模型中包含的所有动画片段
|
||||||
const animations = shallowRef<THREE.AnimationClip[]>([])
|
const animations = shallowRef<THREE.AnimationClip[]>([])
|
||||||
|
// 加载的 3D 模型对象
|
||||||
const model = shallowRef<THREE.Group>()
|
const model = shallowRef<THREE.Group>()
|
||||||
|
|
||||||
|
// ========== Three.js 核心对象 ==========
|
||||||
let scene: THREE.Scene | undefined
|
let scene: THREE.Scene | undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 Three.js 场景
|
||||||
|
* 创建场景、相机、渲染器、灯光等基础设施
|
||||||
|
*/
|
||||||
function initModel() {
|
function initModel() {
|
||||||
const width = threeRef.value!.clientWidth
|
const width = threeRef.value!.clientWidth
|
||||||
const height = threeRef.value!.clientHeight
|
const height = threeRef.value!.clientHeight
|
||||||
|
|
||||||
|
// 创建场景
|
||||||
scene = new THREE.Scene()
|
scene = new THREE.Scene()
|
||||||
scene.background = new THREE.Color(0x000000)
|
scene.background = new THREE.Color(0x000000) // 黑色背景
|
||||||
|
|
||||||
|
// 创建透视相机
|
||||||
|
// 参数:视场角 75°, 宽高比, 近裁剪面 0.1, 远裁剪面 1000
|
||||||
const camera = new THREE.PerspectiveCamera(
|
const camera = new THREE.PerspectiveCamera(
|
||||||
75,
|
75,
|
||||||
width / height,
|
width / height,
|
||||||
0.1,
|
0.1,
|
||||||
1000,
|
1000,
|
||||||
)
|
)
|
||||||
camera.position.set(0.1, 0.1, 1)
|
camera.position.set(0.1, 0.1, 1) // 相机位置(靠近原点观察)
|
||||||
|
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
// 创建 WebGL 渲染器
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true }) // 开启抗锯齿
|
||||||
renderer.setSize(width, height)
|
renderer.setSize(width, height)
|
||||||
renderer.shadowMap.enabled = true
|
renderer.shadowMap.enabled = true // 启用阴影渲染
|
||||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap // PCF 软阴影
|
||||||
threeRef.value!.appendChild(renderer.domElement)
|
threeRef.value!.appendChild(renderer.domElement)
|
||||||
|
|
||||||
|
// 创建轨道控制器 - 允许用户旋转、缩放视角
|
||||||
const controls = new OrbitControls(camera, renderer.domElement)
|
const controls = new OrbitControls(camera, renderer.domElement)
|
||||||
controls.enableDamping = true
|
controls.enableDamping = true // 启用阻尼(平滑过渡)
|
||||||
controls.dampingFactor = 0.05
|
controls.dampingFactor = 0.05 // 阻尼系数
|
||||||
|
|
||||||
|
// 添加坐标轴辅助工具(红 X, 绿 Y, 蓝 Z,长度 5)
|
||||||
const axesHelper = new THREE.AxesHelper(5)
|
const axesHelper = new THREE.AxesHelper(5)
|
||||||
scene.add(axesHelper)
|
scene.add(axesHelper)
|
||||||
|
|
||||||
// 环境光
|
// ===== 灯光系统 =====
|
||||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5)
|
|
||||||
|
// 环境光 - 提供无方向的均匀照明
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5) // 白色,强度 0.5
|
||||||
scene.add(ambientLight)
|
scene.add(ambientLight)
|
||||||
|
|
||||||
// 方向光(主光)
|
// 方向光(主光源)- 模拟太阳光
|
||||||
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 0.8)
|
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 0.8) // 白色,强度 0.8
|
||||||
directionalLight.position.set(20, 30, 20)
|
directionalLight.position.set(20, 30, 20) // 光源位置(右上方)
|
||||||
directionalLight.castShadow = true
|
directionalLight.castShadow = true // 投射阴影
|
||||||
directionalLight.shadow.mapSize.set(2048, 2048)
|
|
||||||
directionalLight.shadow.camera.near = 0.5
|
// 配置阴影参数
|
||||||
directionalLight.shadow.camera.far = 50
|
directionalLight.shadow.mapSize.set(2048, 2048) // 阴影贴图分辨率(越高越清晰)
|
||||||
|
directionalLight.shadow.camera.near = 0.5 // 阴影相机近裁剪面
|
||||||
|
directionalLight.shadow.camera.far = 50 // 阴影相机远裁剪面
|
||||||
|
// 阴影相机的可视范围(正交投影)
|
||||||
directionalLight.shadow.camera.left = -20
|
directionalLight.shadow.camera.left = -20
|
||||||
directionalLight.shadow.camera.right = 20
|
directionalLight.shadow.camera.right = 20
|
||||||
directionalLight.shadow.camera.top = 20
|
directionalLight.shadow.camera.top = 20
|
||||||
directionalLight.shadow.camera.bottom = -20
|
directionalLight.shadow.camera.bottom = -20
|
||||||
scene.add(directionalLight)
|
scene.add(directionalLight)
|
||||||
|
|
||||||
// 点光源
|
// 点光源 - 从一点向四周发光(蓝色调)
|
||||||
const pointLight = new THREE.PointLight(0x409EFF, 0.5, 50)
|
const pointLight = new THREE.PointLight(0x409EFF, 0.5, 50) // 颜色, 强度, 距离
|
||||||
pointLight.position.set(0, 10, 0)
|
pointLight.position.set(0, 10, 0) // 位置(头顶上方)
|
||||||
scene.add(pointLight)
|
scene.add(pointLight)
|
||||||
|
|
||||||
|
// ===== 加载 GLTF 模型 =====
|
||||||
const loader = new GLTFLoader()
|
const loader = new GLTFLoader()
|
||||||
loader.load('/lxb_grp.glb', (gltf) => {
|
loader.load('/lxb_grp.glb', (gltf) => {
|
||||||
scene!.add(gltf.scene)
|
// 模型加载成功回调
|
||||||
model.value = gltf.scene
|
scene!.add(gltf.scene) // 将模型添加到场景
|
||||||
animations.value = gltf.animations ?? []
|
model.value = gltf.scene // 保存模型引用(供动画使用)
|
||||||
|
animations.value = gltf.animations ?? [] // 提取模型内置的动画片段
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画循环函数
|
||||||
|
* 持续渲染场景,并处理窗口尺寸变化
|
||||||
|
*/
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame(animate)
|
requestAnimationFrame(animate) // 递归调用,保持动画循环
|
||||||
renderer.render(scene!, camera)
|
renderer.render(scene!, camera) // 渲染场景
|
||||||
|
|
||||||
|
// 响应式调整渲染器尺寸(窗口大小变化时)
|
||||||
renderer.setSize(width, height)
|
renderer.setSize(width, height)
|
||||||
camera.aspect = width / height
|
camera.aspect = width / height
|
||||||
camera.updateProjectionMatrix()
|
camera.updateProjectionMatrix()
|
||||||
}
|
}
|
||||||
|
|
||||||
animate()
|
animate() // 启动动画循环
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放指定索引的动画
|
||||||
|
*
|
||||||
|
* @param index - 动画片段的索引(对应 animations 数组)
|
||||||
|
*
|
||||||
|
* 工作流程:
|
||||||
|
* 1. 获取指定的动画片段(AnimationClip)
|
||||||
|
* 2. 创建动画混合器(AnimationMixer)- 绑定到模型
|
||||||
|
* 3. 创建动画动作(AnimationAction)- 从混合器和片段创建
|
||||||
|
* 4. 播放动画
|
||||||
|
* 5. 创建更新循环,持续更新混合器状态
|
||||||
|
*
|
||||||
|
* 技术要点:
|
||||||
|
* - AnimationMixer: 管理一个或多个动画的播放
|
||||||
|
* - AnimationAction: 控制单个动画片段的播放状态
|
||||||
|
* - Clock: 用于计算每帧的时间差(delta time)
|
||||||
|
* - mixer.update(delta): 根据时间差推进动画状态
|
||||||
|
*/
|
||||||
function handlePlay(index: number) {
|
function handlePlay(index: number) {
|
||||||
|
// 获取指定索引的动画片段
|
||||||
const clip = animations.value[index]
|
const clip = animations.value[index]
|
||||||
if (!clip)
|
if (!clip)
|
||||||
return
|
return // 如果动画不存在,直接返回
|
||||||
|
|
||||||
|
// 创建动画混合器,绑定到模型对象
|
||||||
|
// AnimationMixer 负责管理和更新动画状态
|
||||||
const mixer = new THREE.AnimationMixer(model.value!)
|
const mixer = new THREE.AnimationMixer(model.value!)
|
||||||
|
|
||||||
|
// 从混合器创建动画动作
|
||||||
|
// clipAction() 返回一个可控制的动画动作对象
|
||||||
const action = mixer.clipAction(clip)
|
const action = mixer.clipAction(clip)
|
||||||
|
|
||||||
|
// 播放动画
|
||||||
action.play()
|
action.play()
|
||||||
|
|
||||||
|
// 可选:定时停止动画(已注释)
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
// action.stop()
|
// action.stop()
|
||||||
// }, 3000)
|
// }, 3000)
|
||||||
|
|
||||||
|
// 创建时钟对象,用于计算帧之间的时间差
|
||||||
const clock = new THREE.Clock()
|
const clock = new THREE.Clock()
|
||||||
// 在动画循环中更新混合器
|
|
||||||
|
/**
|
||||||
|
* 动画更新循环
|
||||||
|
* 持续更新混合器状态,使动画平滑播放
|
||||||
|
*/
|
||||||
function updateMixer() {
|
function updateMixer() {
|
||||||
requestAnimationFrame(updateMixer)
|
requestAnimationFrame(updateMixer) // 递归调用,持续更新
|
||||||
const delta = clock.getDelta()
|
const delta = clock.getDelta() // 获取距离上一帧的时间差(秒)
|
||||||
mixer.update(delta)
|
mixer.update(delta) // 更新混合器,推进动画状态
|
||||||
}
|
}
|
||||||
updateMixer()
|
|
||||||
|
updateMixer() // 启动动画更新循环
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 组件挂载时初始化场景
|
||||||
onMounted(initModel)
|
onMounted(initModel)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="threeRef" class="model" />
|
<div ref="threeRef" class="model" />
|
||||||
|
<!-- 导航链接:跳转到下一个 Demo -->
|
||||||
<router-link to="/three/7" class="position-fixed left-20px top-20px">
|
<router-link to="/three/7" class="position-fixed left-20px top-20px">
|
||||||
模型漫游(巡检业务)
|
模型漫游(巡检业务)
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<!-- 动画控制按钮组 -->
|
||||||
|
<!-- 根据模型中的动画数量动态生成按钮 -->
|
||||||
<div class="position-fixed right-20px top-20px flex gap-12px">
|
<div class="position-fixed right-20px top-20px flex gap-12px">
|
||||||
<button v-for="(ani, index) in animations" :key="index" @click="handlePlay(index)">
|
<button v-for="(ani, index) in animations" :key="index" @click="handlePlay(index)">
|
||||||
播放动画{{ index + 1 }} - {{ ani.name }}
|
播放动画{{ index + 1 }} - {{ ani.name }}
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
<!--
|
<!--
|
||||||
* @Description: 模型漫游(巡检业务)
|
* @Description: Demo 7 - 模型漫游(巡检业务)
|
||||||
|
* @功能说明: 实现相机沿预设路径自动巡检,模拟无人机/机器人巡检业务
|
||||||
|
* @核心技术:
|
||||||
|
* 1. 路径插值动画 - 使用 lerp(线性插值)在路径点之间平滑移动
|
||||||
|
* 2. SmoothStep 缓动 - 使用平滑步进函数实现加速减速效果
|
||||||
|
* 3. 相机目标联动 - 相机位置和观察目标同步插值
|
||||||
|
* 4. OrbitControls 状态切换 - 自动模式禁用控制器,手动模式启用
|
||||||
|
* @业务场景:
|
||||||
|
* - 建筑/工厂自动巡检
|
||||||
|
* - 虚拟导览
|
||||||
|
* - 安防监控路线演示
|
||||||
|
* @技术要点:
|
||||||
|
* - 13个路径点构成完整巡检循环
|
||||||
|
* - 支持慢速(0.003)和快速(0.008)两种巡检速度
|
||||||
|
* - 使用 Fog 雾效增强空间深度感
|
||||||
* @Autor: 刘 相卿
|
* @Autor: 刘 相卿
|
||||||
* @Date: 2025-11-17 16:07:33
|
* @Date: 2025-11-17 16:07:33
|
||||||
* @LastEditors: 刘 相卿
|
* @LastEditors: 刘 相卿
|
||||||
@@ -11,148 +25,174 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
const threeRef = shallowRef<HTMLDivElement>()
|
// ========== 响应式状态 ==========
|
||||||
const camera = shallowRef<THREE.PerspectiveCamera>()
|
const threeRef = shallowRef<HTMLDivElement>() // Canvas 容器
|
||||||
const scene = shallowRef<THREE.Scene>()
|
const camera = shallowRef<THREE.PerspectiveCamera>() // 透视相机
|
||||||
const controls = shallowRef<OrbitControls>()
|
const scene = shallowRef<THREE.Scene>() // 场景对象
|
||||||
const renderer = shallowRef<THREE.WebGLRenderer>()
|
const controls = shallowRef<OrbitControls>() // 轨道控制器
|
||||||
|
const renderer = shallowRef<THREE.WebGLRenderer>() // WebGL 渲染器
|
||||||
|
|
||||||
// 漫游状态
|
// ========== 漫游状态管理 ==========
|
||||||
const tourMode = ref<'none' | 'slow' | 'fast'>('none')
|
const tourMode = ref<'none' | 'slow' | 'fast'>('none') // 巡检模式:无、慢速、快速
|
||||||
let tourTime = 0
|
let tourTime = 0 // 巡检动画时间累加器
|
||||||
const initialCameraPosition = new THREE.Vector3(0, 5, 15)
|
const initialCameraPosition = new THREE.Vector3(0, 5, 15) // 相机初始位置
|
||||||
const initialCameraTarget = new THREE.Vector3(0, 0, 0)
|
const initialCameraTarget = new THREE.Vector3(0, 0, 0) // 相机初始观察目标
|
||||||
|
|
||||||
// 巡检路径点位 - 模拟进入建筑内部进行详细巡检
|
// ========== 巡检路径定义 ==========
|
||||||
// 包含位置和对应的观察目标点
|
/**
|
||||||
|
* 路径点接口
|
||||||
|
* 定义相机在巡检过程中的位置和观察目标
|
||||||
|
*/
|
||||||
interface PathPoint {
|
interface PathPoint {
|
||||||
position: THREE.Vector3
|
position: THREE.Vector3 // 相机位置
|
||||||
lookAt: THREE.Vector3
|
lookAt: THREE.Vector3 // 相机观察目标(朝向)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检路径点数组
|
||||||
|
*
|
||||||
|
* 巡检路线设计:
|
||||||
|
* 1. 外部俯视 → 2. 正面接近 → 3. 进入前厅 →
|
||||||
|
* 4-5. 右侧巡检 → 6. 右后方 → 7. 后方中央 →
|
||||||
|
* 8-9. 左侧巡检 → 10. 左前方 → 11-12. 中央回顾 →
|
||||||
|
* 13. 退出俯视
|
||||||
|
*
|
||||||
|
* 形成完整的顺时针巡检循环
|
||||||
|
*/
|
||||||
const inspectionPath: PathPoint[] = [
|
const inspectionPath: PathPoint[] = [
|
||||||
// 1. 外部俯视全景
|
// 1. 外部俯视全景 - 建立整体认知
|
||||||
{ position: new THREE.Vector3(0, 8, 15), lookAt: new THREE.Vector3(0, 0, 0) },
|
{ position: new THREE.Vector3(0, 8, 15), lookAt: new THREE.Vector3(0, 0, 0) },
|
||||||
// 2. 从正面接近入口
|
// 2. 从正面接近入口 - 降低高度,准备进入
|
||||||
{ position: new THREE.Vector3(0, 3, 10), lookAt: new THREE.Vector3(0, 2, 0) },
|
{ position: new THREE.Vector3(0, 3, 10), lookAt: new THREE.Vector3(0, 2, 0) },
|
||||||
// 3. 进入前厅区域
|
// 3. 进入前厅区域 - 穿过入口
|
||||||
{ position: new THREE.Vector3(0, 2, 5), lookAt: new THREE.Vector3(0, 2, -2) },
|
{ position: new THREE.Vector3(0, 2, 5), lookAt: new THREE.Vector3(0, 2, -2) },
|
||||||
// 4. 右侧区域巡检
|
// 4. 右侧区域巡检 - 检查右侧设施
|
||||||
{ position: new THREE.Vector3(5, 2, 3), lookAt: new THREE.Vector3(8, 2, 0) },
|
{ position: new THREE.Vector3(5, 2, 3), lookAt: new THREE.Vector3(8, 2, 0) },
|
||||||
// 5. 深入右侧内部
|
// 5. 深入右侧内部 - 详细检查右侧深处
|
||||||
{ position: new THREE.Vector3(8, 2.5, 0), lookAt: new THREE.Vector3(10, 2, -3) },
|
{ position: new THREE.Vector3(8, 2.5, 0), lookAt: new THREE.Vector3(10, 2, -3) },
|
||||||
// 6. 右后方区域
|
// 6. 右后方区域 - 转向后方
|
||||||
{ position: new THREE.Vector3(6, 2, -5), lookAt: new THREE.Vector3(3, 2, -8) },
|
{ position: new THREE.Vector3(6, 2, -5), lookAt: new THREE.Vector3(3, 2, -8) },
|
||||||
// 7. 后方中央区域
|
// 7. 后方中央区域 - 检查后方设施
|
||||||
{ position: new THREE.Vector3(0, 2.5, -8), lookAt: new THREE.Vector3(0, 2, -5) },
|
{ position: new THREE.Vector3(0, 2.5, -8), lookAt: new THREE.Vector3(0, 2, -5) },
|
||||||
// 8. 左后方区域
|
// 8. 左后方区域 - 转向左侧
|
||||||
{ position: new THREE.Vector3(-6, 2, -5), lookAt: new THREE.Vector3(-3, 2, -8) },
|
{ position: new THREE.Vector3(-6, 2, -5), lookAt: new THREE.Vector3(-3, 2, -8) },
|
||||||
// 9. 深入左侧内部
|
// 9. 深入左侧内部 - 详细检查左侧深处
|
||||||
{ position: new THREE.Vector3(-8, 2.5, 0), lookAt: new THREE.Vector3(-10, 2, -3) },
|
{ position: new THREE.Vector3(-8, 2.5, 0), lookAt: new THREE.Vector3(-10, 2, -3) },
|
||||||
// 10. 左侧区域巡检
|
// 10. 左侧区域巡检 - 检查左侧设施
|
||||||
{ position: new THREE.Vector3(-5, 2, 3), lookAt: new THREE.Vector3(-8, 2, 0) },
|
{ position: new THREE.Vector3(-5, 2, 3), lookAt: new THREE.Vector3(-8, 2, 0) },
|
||||||
// 11. 返回中央区域
|
// 11. 返回中央区域 - 回到中心
|
||||||
{ position: new THREE.Vector3(0, 2, 0), lookAt: new THREE.Vector3(0, 3, -5) },
|
{ position: new THREE.Vector3(0, 2, 0), lookAt: new THREE.Vector3(0, 3, -5) },
|
||||||
// 12. 中央上升视角
|
// 12. 中央上升视角 - 俯瞰全局
|
||||||
{ position: new THREE.Vector3(0, 5, 2), lookAt: new THREE.Vector3(0, 0, 0) },
|
{ position: new THREE.Vector3(0, 5, 2), lookAt: new THREE.Vector3(0, 0, 0) },
|
||||||
// 13. 退出并俯视
|
// 13. 退出并俯视 - 完成巡检,恢复初始视角
|
||||||
{ position: new THREE.Vector3(0, 8, 12), lookAt: new THREE.Vector3(0, 0, 0) },
|
{ position: new THREE.Vector3(0, 8, 12), lookAt: new THREE.Vector3(0, 0, 0) },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 Three.js 场景
|
||||||
|
* 创建场景、相机、渲染器、灯光、地面等基础元素
|
||||||
|
*/
|
||||||
function initModel() {
|
function initModel() {
|
||||||
const width = threeRef.value!.clientWidth
|
const width = threeRef.value!.clientWidth
|
||||||
const height = threeRef.value!.clientHeight
|
const height = threeRef.value!.clientHeight
|
||||||
|
|
||||||
// 创建场景
|
// ===== 创建场景 =====
|
||||||
scene.value = new THREE.Scene()
|
scene.value = new THREE.Scene()
|
||||||
|
|
||||||
// 创建天空盒
|
// ===== 创建天空盒(立方体背景)=====
|
||||||
|
// 使用6个面的立方体模拟天空
|
||||||
const skyboxGeometry = new THREE.BoxGeometry(500, 500, 500)
|
const skyboxGeometry = new THREE.BoxGeometry(500, 500, 500)
|
||||||
const skyboxMaterials = [
|
const skyboxMaterials = [
|
||||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // right
|
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 右侧(深蓝灰)
|
||||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // left
|
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 左侧
|
||||||
new THREE.MeshBasicMaterial({ color: 0x0F0F1E, side: THREE.BackSide }), // top
|
new THREE.MeshBasicMaterial({ color: 0x0F0F1E, side: THREE.BackSide }), // 顶部(更深)
|
||||||
new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.BackSide }), // bottom
|
new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.BackSide }), // 底部(黑色)
|
||||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // front
|
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 前侧
|
||||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // back
|
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 后侧
|
||||||
]
|
]
|
||||||
const skybox = new THREE.Mesh(skyboxGeometry, skyboxMaterials)
|
const skybox = new THREE.Mesh(skyboxGeometry, skyboxMaterials)
|
||||||
scene.value.add(skybox)
|
scene.value.add(skybox)
|
||||||
|
|
||||||
scene.value.background = new THREE.Color(0x000000)
|
scene.value.background = new THREE.Color(0x000000) // 场景背景色(黑色)
|
||||||
// 添加雾效增强深度感
|
// 添加雾效增强深度感和空间层次
|
||||||
|
// Fog(颜色, 起始距离, 结束距离)
|
||||||
scene.value.fog = new THREE.Fog(0x000000, 10, 50)
|
scene.value.fog = new THREE.Fog(0x000000, 10, 50)
|
||||||
|
|
||||||
// 创建透视相机 PerspectiveCamera(视野角度, 宽高比, 近截面, 远截面)
|
// ===== 创建透视相机 =====
|
||||||
|
// PerspectiveCamera(视野角度, 宽高比, 近截面, 远截面)
|
||||||
camera.value = new THREE.PerspectiveCamera(
|
camera.value = new THREE.PerspectiveCamera(
|
||||||
60,
|
60, // 视野角度 60°(较窄,适合建筑巡检)
|
||||||
width / height,
|
width / height,
|
||||||
0.1,
|
0.1, // 近裁剪面
|
||||||
1000,
|
1000, // 远裁剪面
|
||||||
)
|
)
|
||||||
camera.value.position.copy(initialCameraPosition)
|
camera.value.position.copy(initialCameraPosition) // 设置初始位置
|
||||||
|
|
||||||
// 创建渲染器
|
// ===== 创建 WebGL 渲染器 =====
|
||||||
renderer.value = new THREE.WebGLRenderer({ antialias: true })
|
renderer.value = new THREE.WebGLRenderer({ antialias: true }) // 抗锯齿
|
||||||
renderer.value.setSize(width, height)
|
renderer.value.setSize(width, height)
|
||||||
renderer.value.setPixelRatio(window.devicePixelRatio)
|
renderer.value.setPixelRatio(window.devicePixelRatio) // 高清屏适配
|
||||||
// 开启阴影贴图
|
// 开启阴影贴图
|
||||||
renderer.value.shadowMap.enabled = true
|
renderer.value.shadowMap.enabled = true
|
||||||
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap
|
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap // PCF 软阴影
|
||||||
threeRef.value!.appendChild(renderer.value.domElement)
|
threeRef.value!.appendChild(renderer.value.domElement)
|
||||||
|
|
||||||
// 创建轨道控制器
|
// ===== 创建轨道控制器 =====
|
||||||
controls.value = new OrbitControls(camera.value, renderer.value.domElement)
|
controls.value = new OrbitControls(camera.value, renderer.value.domElement)
|
||||||
controls.value.enableDamping = true // 启用阻尼(惯性)
|
controls.value.enableDamping = true // 启用阻尼(惯性效果)
|
||||||
controls.value.dampingFactor = 0.05
|
controls.value.dampingFactor = 0.05 // 阻尼系数
|
||||||
controls.value.minDistance = 2
|
controls.value.minDistance = 2 // 最小缩放距离
|
||||||
controls.value.maxDistance = 50
|
controls.value.maxDistance = 50 // 最大缩放距离
|
||||||
controls.value.maxPolarAngle = Math.PI / 2 * 1.8 // 限制垂直旋转角度
|
controls.value.maxPolarAngle = Math.PI / 2 * 1.8 // 限制垂直旋转角度(防止翻转)
|
||||||
|
|
||||||
// 添加环境光
|
// ===== 灯光系统 =====
|
||||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.6)
|
|
||||||
|
// 环境光 - 提供基础照明
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.6) // 白色,强度 0.6
|
||||||
scene.value.add(ambientLight)
|
scene.value.add(ambientLight)
|
||||||
|
|
||||||
// 添加主平行光(模拟太阳光)
|
// 主平行光(模拟太阳光)
|
||||||
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.2)
|
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.2) // 强度 1.2
|
||||||
directionalLight.position.set(10, 15, 10)
|
directionalLight.position.set(10, 15, 10) // 光源位置(右上方)
|
||||||
directionalLight.castShadow = true
|
directionalLight.castShadow = true // 投射阴影
|
||||||
|
|
||||||
|
// 配置阴影相机范围(正交投影)
|
||||||
directionalLight.shadow.camera.left = -20
|
directionalLight.shadow.camera.left = -20
|
||||||
directionalLight.shadow.camera.right = 20
|
directionalLight.shadow.camera.right = 20
|
||||||
directionalLight.shadow.camera.top = 20
|
directionalLight.shadow.camera.top = 20
|
||||||
directionalLight.shadow.camera.bottom = -20
|
directionalLight.shadow.camera.bottom = -20
|
||||||
directionalLight.shadow.mapSize.width = 2048
|
directionalLight.shadow.mapSize.width = 2048 // 阴影贴图分辨率
|
||||||
directionalLight.shadow.mapSize.height = 2048
|
directionalLight.shadow.mapSize.height = 2048
|
||||||
scene.value.add(directionalLight)
|
scene.value.add(directionalLight)
|
||||||
|
|
||||||
// 添加辅助光源(从另一个方向)
|
// 辅助光源(填充光)- 从另一个方向照亮暗部
|
||||||
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.5)
|
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.5)
|
||||||
fillLight.position.set(-10, 10, -10)
|
fillLight.position.set(-10, 10, -10) // 左上后方
|
||||||
scene.value.add(fillLight)
|
scene.value.add(fillLight)
|
||||||
|
|
||||||
// 添加地面
|
// ===== 创建地面 =====
|
||||||
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
const groundGeometry = new THREE.PlaneGeometry(100, 100) // 100x100 的平面
|
||||||
const groundMaterial = new THREE.MeshStandardMaterial({
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
||||||
color: 0x999999,
|
color: 0x999999, // 灰色
|
||||||
roughness: 0.8,
|
roughness: 0.8, // 粗糙度(不光滑)
|
||||||
metalness: 0.2,
|
metalness: 0.2, // 金属度(略有金属感)
|
||||||
})
|
})
|
||||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||||
ground.rotation.x = -Math.PI / 2
|
ground.rotation.x = -Math.PI / 2 // 旋转 90° 使其水平
|
||||||
ground.position.y = 0
|
ground.position.y = 0 // 位于 Y=0 平面
|
||||||
ground.receiveShadow = true
|
ground.receiveShadow = true // 接收阴影
|
||||||
scene.value.add(ground)
|
scene.value.add(ground)
|
||||||
|
|
||||||
// 加载模型
|
// ===== 加载 GLTF 模型 =====
|
||||||
const loader = new GLTFLoader()
|
const loader = new GLTFLoader()
|
||||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||||
const model = gltf.scene
|
const model = gltf.scene
|
||||||
|
|
||||||
// 遍历模型,启用阴影
|
// 遍历模型所有子对象,启用阴影
|
||||||
model.traverse((child) => {
|
model.traverse((child) => {
|
||||||
if (child instanceof THREE.Mesh) {
|
if (child instanceof THREE.Mesh) {
|
||||||
child.castShadow = true
|
child.castShadow = true // 投射阴影
|
||||||
child.receiveShadow = true
|
child.receiveShadow = true // 接收阴影
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -161,46 +201,56 @@ function initModel() {
|
|||||||
console.error('模型加载失败:', error)
|
console.error('模型加载失败:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画循环函数
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 处理巡检动画逻辑(相机路径插值)
|
||||||
|
* 2. 更新轨道控制器
|
||||||
|
* 3. 渲染场景
|
||||||
|
*/
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame(animate)
|
requestAnimationFrame(animate) // 递归调用
|
||||||
|
|
||||||
// 巡检漫游效果
|
// ===== 巡检漫游动画逻辑 =====
|
||||||
if (tourMode.value !== 'none' && camera.value && controls.value) {
|
if (tourMode.value !== 'none' && camera.value && controls.value) {
|
||||||
|
// 根据模式确定移动速度
|
||||||
const speed = tourMode.value === 'slow' ? 0.003 : 0.008 // 慢速或快速
|
const speed = tourMode.value === 'slow' ? 0.003 : 0.008 // 慢速或快速
|
||||||
tourTime += speed
|
tourTime += speed // 累加时间
|
||||||
|
|
||||||
const totalPoints = inspectionPath.length
|
const totalPoints = inspectionPath.length // 路径点总数
|
||||||
const totalProgress = tourTime % totalPoints
|
const totalProgress = tourTime % totalPoints // 当前进度(0 到 totalPoints 循环)
|
||||||
const currentIndex = Math.floor(totalProgress)
|
const currentIndex = Math.floor(totalProgress) // 当前路径点索引
|
||||||
const nextIndex = (currentIndex + 1) % totalPoints
|
const nextIndex = (currentIndex + 1) % totalPoints // 下一个路径点索引(循环)
|
||||||
const t = totalProgress - currentIndex
|
const t = totalProgress - currentIndex // 两点之间的插值参数(0-1)
|
||||||
|
|
||||||
// 使用平滑的缓动函数
|
// 使用平滑的缓动函数(SmoothStep)
|
||||||
const smoothT = t * t * (3 - 2 * t) // smoothstep
|
// smoothstep: 在 0 和 1 处导数为 0,产生平滑的加速减速效果
|
||||||
|
const smoothT = t * t * (3 - 2 * t)
|
||||||
|
|
||||||
// 获取当前和下一个路径点
|
// 获取当前和下一个路径点
|
||||||
const currentPoint = inspectionPath[currentIndex]!
|
const currentPoint = inspectionPath[currentIndex]!
|
||||||
const nextPoint = inspectionPath[nextIndex]!
|
const nextPoint = inspectionPath[nextIndex]!
|
||||||
|
|
||||||
// 插值位置
|
// 插值计算相机位置
|
||||||
const newPosition = new THREE.Vector3()
|
const newPosition = new THREE.Vector3()
|
||||||
newPosition.lerpVectors(currentPoint.position, nextPoint.position, smoothT)
|
newPosition.lerpVectors(currentPoint.position, nextPoint.position, smoothT)
|
||||||
camera.value.position.copy(newPosition)
|
camera.value.position.copy(newPosition)
|
||||||
|
|
||||||
// 插值观察目标
|
// 插值计算观察目标
|
||||||
const newTarget = new THREE.Vector3()
|
const newTarget = new THREE.Vector3()
|
||||||
newTarget.lerpVectors(currentPoint.lookAt, nextPoint.lookAt, smoothT)
|
newTarget.lerpVectors(currentPoint.lookAt, nextPoint.lookAt, smoothT)
|
||||||
controls.value.target.copy(newTarget)
|
controls.value.target.copy(newTarget)
|
||||||
|
|
||||||
// 禁用控制器的用户交互
|
// 巡检模式下禁用用户控制
|
||||||
controls.value.enabled = false
|
controls.value.enabled = false
|
||||||
}
|
}
|
||||||
else if (controls.value) {
|
else if (controls.value) {
|
||||||
// 非漫游模式时启用控制器
|
// 非巡检模式时启用用户控制
|
||||||
controls.value.enabled = true
|
controls.value.enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新控制器
|
// 更新轨道控制器(处理阻尼等)
|
||||||
controls.value?.update()
|
controls.value?.update()
|
||||||
|
|
||||||
// 执行渲染
|
// 执行渲染
|
||||||
@@ -208,13 +258,16 @@ function initModel() {
|
|||||||
renderer.value.render(scene.value, camera.value)
|
renderer.value.render(scene.value, camera.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
animate()
|
animate() // 启动动画循环
|
||||||
|
|
||||||
// 窗口自适应
|
// 监听窗口大小变化
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理窗口大小变化
|
/**
|
||||||
|
* 处理窗口大小变化
|
||||||
|
* 更新相机宽高比和渲染器尺寸
|
||||||
|
*/
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
if (!threeRef.value || !camera.value || !renderer.value)
|
if (!threeRef.value || !camera.value || !renderer.value)
|
||||||
return
|
return
|
||||||
@@ -224,55 +277,66 @@ function handleResize() {
|
|||||||
|
|
||||||
// 更新相机宽高比
|
// 更新相机宽高比
|
||||||
camera.value.aspect = width / height
|
camera.value.aspect = width / height
|
||||||
camera.value.updateProjectionMatrix()
|
camera.value.updateProjectionMatrix() // 必须调用以应用更改
|
||||||
|
|
||||||
// 更新渲染器尺寸
|
// 更新渲染器尺寸
|
||||||
renderer.value.setSize(width, height)
|
renderer.value.setSize(width, height)
|
||||||
renderer.value.setPixelRatio(window.devicePixelRatio)
|
renderer.value.setPixelRatio(window.devicePixelRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 慢速巡检漫游
|
/**
|
||||||
|
* 启动慢速巡检
|
||||||
|
* 速度:0.003,适合详细观察
|
||||||
|
*/
|
||||||
function startSlowTour() {
|
function startSlowTour() {
|
||||||
tourMode.value = 'slow'
|
tourMode.value = 'slow'
|
||||||
tourTime = 0
|
tourTime = 0 // 重置时间,从第一个路径点开始
|
||||||
}
|
}
|
||||||
|
|
||||||
// 快速巡检漫游
|
/**
|
||||||
|
* 启动快速巡检
|
||||||
|
* 速度:0.008,适合快速浏览
|
||||||
|
*/
|
||||||
function startFastTour() {
|
function startFastTour() {
|
||||||
tourMode.value = 'fast'
|
tourMode.value = 'fast'
|
||||||
tourTime = 0
|
tourTime = 0 // 重置时间,从第一个路径点开始
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止漫游
|
/**
|
||||||
|
* 停止巡检
|
||||||
|
* 恢复相机到初始位置,重新启用用户控制
|
||||||
|
*/
|
||||||
function stopTour() {
|
function stopTour() {
|
||||||
tourMode.value = 'none'
|
tourMode.value = 'none'
|
||||||
tourTime = 0
|
tourTime = 0
|
||||||
if (camera.value && controls.value) {
|
if (camera.value && controls.value) {
|
||||||
camera.value.position.copy(initialCameraPosition)
|
camera.value.position.copy(initialCameraPosition) // 恢复初始位置
|
||||||
controls.value.target.copy(initialCameraTarget)
|
controls.value.target.copy(initialCameraTarget) // 恢复初始目标
|
||||||
controls.value.enabled = true
|
controls.value.enabled = true // 重新启用用户控制
|
||||||
controls.value.update()
|
controls.value.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(initModel)
|
// ===== 生命周期钩子 =====
|
||||||
|
onMounted(initModel) // 组件挂载时初始化
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 清理事件监听
|
// 组件卸载时清理资源
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize) // 移除事件监听
|
||||||
// 销毁控制器
|
controls.value?.dispose() // 销毁控制器
|
||||||
controls.value?.dispose()
|
renderer.value?.dispose() // 销毁渲染器
|
||||||
// 销毁渲染器
|
|
||||||
renderer.value?.dispose()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="threeRef" class="w-full h-full" />
|
<div ref="threeRef" class="w-full h-full" />
|
||||||
|
<!-- 导航链接 -->
|
||||||
<router-link to="/three/8" class="position-fixed left-20px top-20px">
|
<router-link to="/three/8" class="position-fixed left-20px top-20px">
|
||||||
漫游 修改材质(设备告警业务)
|
漫游 修改材质(设备告警业务)
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<!-- 控制面板 -->
|
||||||
<div class="position-fixed right-20px top-20px flex flex-col gap-12px">
|
<div class="position-fixed right-20px top-20px flex flex-col gap-12px">
|
||||||
|
<!-- 巡检控制按钮组 -->
|
||||||
<div class="flex gap-12px">
|
<div class="flex gap-12px">
|
||||||
<button
|
<button
|
||||||
:class="{ 'bg-blue-500 text-white': tourMode === 'slow' }"
|
:class="{ 'bg-blue-500 text-white': tourMode === 'slow' }"
|
||||||
@@ -296,6 +360,7 @@ onUnmounted(() => {
|
|||||||
停止漫游
|
停止漫游
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 状态提示 -->
|
||||||
<div v-if="tourMode !== 'none'" class="bg-white px-12px py-8px rounded shadow text-sm">
|
<div v-if="tourMode !== 'none'" class="bg-white px-12px py-8px rounded shadow text-sm">
|
||||||
正在进行{{ tourMode === 'slow' ? '慢速' : '快速' }}巡检...
|
正在进行{{ tourMode === 'slow' ? '慢速' : '快速' }}巡检...
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user