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>() // 已着火的网格集合
|
||||
let spreadTimer = 0 // 蔓延计时器
|
||||
const spreadInterval = 2 // 每2秒蔓延一次
|
||||
let centerFireEmitter: any = null // 中心大火发射器
|
||||
let totalMeshCount = 0 // 模型中网格总数
|
||||
|
||||
// 地板流动效果
|
||||
let flowingGround: any
|
||||
@@ -204,8 +206,11 @@ function loadModel(particleUtils: any) {
|
||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||
model = gltf.scene
|
||||
|
||||
// 统计网格总数
|
||||
totalMeshCount = 0
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
totalMeshCount++
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
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() {
|
||||
@@ -495,6 +548,20 @@ function toggleFlame() {
|
||||
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) {
|
||||
const material = mesh.material as THREE.MeshStandardMaterial
|
||||
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>
|
||||
import * as THREE from 'three'
|
||||
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'
|
||||
|
||||
// ========== 常量定义 ==========
|
||||
const GROUND_SIZE = 30
|
||||
const GROUND_THICKNESS = 0.2
|
||||
const LINE_COLORS = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF]
|
||||
const LINE_SPEED = 0.1
|
||||
const LINE_DELAY = 300
|
||||
const GROUND_SIZE = 30 // 地面尺寸(30x30)
|
||||
const GROUND_THICKNESS = 0.2 // 地面厚度
|
||||
const LINE_COLORS = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF] // 6种光效颜色:红绿蓝黄紫青
|
||||
const LINE_SPEED = 0.1 // 线条移动速度
|
||||
const LINE_DELAY = 300 // 每条线启动延迟(毫秒)
|
||||
|
||||
// ========== 类型定义 ==========
|
||||
/**
|
||||
* 线条状态接口
|
||||
* 用于追踪每条光效线的运动状态
|
||||
*/
|
||||
interface LineState {
|
||||
mesh: THREE.Mesh
|
||||
direction: number
|
||||
startDelay: number
|
||||
elapsed: number
|
||||
axis: 'x' | 'z'
|
||||
mesh: THREE.Mesh // 线条网格对象
|
||||
direction: number // 移动方向:1 或 -1
|
||||
startDelay: number // 启动延迟时间(毫秒)
|
||||
elapsed: number // 已经过时间(毫秒)
|
||||
axis: 'x' | 'z' // 移动轴向:x 轴或 z 轴
|
||||
}
|
||||
|
||||
// ========== 状态变量 ==========
|
||||
const threeRef = shallowRef<HTMLCanvasElement>()
|
||||
const threeRef = shallowRef<HTMLCanvasElement>() // Canvas 容器引用
|
||||
|
||||
let scene: THREE.Scene
|
||||
let camera: THREE.PerspectiveCamera
|
||||
let renderer: THREE.WebGLRenderer
|
||||
let controls: OrbitControls
|
||||
let animationId: number
|
||||
// Three.js 核心对象
|
||||
let scene: THREE.Scene // 场景对象
|
||||
let camera: THREE.PerspectiveCamera // 透视相机
|
||||
let renderer: THREE.WebGLRenderer // WebGL 渲染器
|
||||
let controls: OrbitControls // 轨道控制器
|
||||
let animationId: number // 动画帧 ID
|
||||
|
||||
const lineStates: LineState[] = []
|
||||
const lineStates: LineState[] = [] // 所有光效线条的状态数组
|
||||
|
||||
/**
|
||||
* 获取容器尺寸
|
||||
* @returns {width, height} 容器的宽度和高度
|
||||
*/
|
||||
function getElementSize() {
|
||||
const width = threeRef.value!.clientWidth
|
||||
const height = threeRef.value!.clientHeight
|
||||
@@ -39,43 +61,48 @@ function getElementSize() {
|
||||
}
|
||||
|
||||
// ========== 初始化场景 ==========
|
||||
/**
|
||||
* 初始化 Three.js 场景
|
||||
* 创建场景、相机、渲染器、控制器等核心对象
|
||||
*/
|
||||
function initModel() {
|
||||
const { width, height } = getElementSize()
|
||||
|
||||
// 创建场景
|
||||
// 创建场景 - 所有 3D 对象的容器
|
||||
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.position.set(3, 8, 12)
|
||||
camera.position.set(3, 8, 12) // 设置相机位置(斜上方观察)
|
||||
|
||||
// 创建渲染器
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setSize(width, height)
|
||||
renderer.setPixelRatio(window.devicePixelRatio)
|
||||
renderer.shadowMap.enabled = true
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
||||
renderer.localClippingEnabled = true
|
||||
threeRef.value!.appendChild(renderer.domElement)
|
||||
// 创建 WebGL 渲染器
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true }) // 开启抗锯齿
|
||||
renderer.setSize(width, height) // 设置渲染尺寸
|
||||
renderer.setPixelRatio(window.devicePixelRatio) // 设置设备像素比(高清屏适配)
|
||||
renderer.shadowMap.enabled = true // 启用阴影
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap // 使用 PCF 软阴影
|
||||
renderer.localClippingEnabled = true // 启用局部裁剪(用于光效线条边界裁剪)
|
||||
threeRef.value!.appendChild(renderer.domElement) // 将渲染器的 canvas 添加到 DOM
|
||||
|
||||
// 加载模型
|
||||
// 加载 GLTF 模型
|
||||
const loader = new GLTFLoader()
|
||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||
scene.add(gltf.scene)
|
||||
scene.add(gltf.scene) // 将模型添加到场景
|
||||
})
|
||||
|
||||
// 创建控制器
|
||||
// 创建轨道控制器 - 允许用户通过鼠标控制相机
|
||||
controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
controls.enableDamping = true // 启用阻尼(惯性)
|
||||
controls.dampingFactor = 0.05 // 阻尼系数
|
||||
|
||||
// 添加辅助工具
|
||||
// 添加坐标轴辅助工具(红 X, 绿 Y, 蓝 Z)
|
||||
const axesHelper = new THREE.AxesHelper(5)
|
||||
scene.add(axesHelper)
|
||||
|
||||
// 添加环境光
|
||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.2)
|
||||
// 添加环境光 - 提供基础照明
|
||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.2) // 白色环境光,强度 1.2
|
||||
scene.add(ambientLight)
|
||||
|
||||
// 添加地面
|
||||
@@ -86,135 +113,217 @@ function initModel() {
|
||||
}
|
||||
|
||||
// ========== 添加地面 ==========
|
||||
/**
|
||||
* 创建地面平台
|
||||
* 使用带厚度的立方体作为地面,用于承载光效动画
|
||||
*/
|
||||
function addGround() {
|
||||
// 创建立方体几何体(宽 30, 高 0.2, 深 30)
|
||||
const geometry = new THREE.BoxGeometry(GROUND_SIZE, GROUND_THICKNESS, GROUND_SIZE)
|
||||
// 创建标准材质(深灰色)
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x222222 })
|
||||
// 创建网格对象
|
||||
const ground = new THREE.Mesh(geometry, material)
|
||||
// 设置地面位置(Y 轴向下偏移一半厚度,使顶面在 Y=0)
|
||||
ground.position.y = -GROUND_THICKNESS / 2
|
||||
ground.receiveShadow = true
|
||||
ground.receiveShadow = true // 接收阴影
|
||||
scene.add(ground)
|
||||
}
|
||||
|
||||
// ========== 添加地面光效 ==========
|
||||
/**
|
||||
* 添加地面流动光效
|
||||
* 创建多条彩色光带在地面上来回移动,形成科幻扫描效果
|
||||
*
|
||||
* 技术要点:
|
||||
* 1. 使用裁剪平面确保光带只在地面范围内显示
|
||||
* 2. 为每条光带设置不同的启动延迟,形成波浪效果
|
||||
* 3. 光带同时在 X 轴和 Z 轴方向运动,形成交叉图案
|
||||
* 4. 使用半透明材质和不同透明度创造层次感
|
||||
*/
|
||||
function addGroundLightEffect() {
|
||||
// 创建裁剪平面
|
||||
// 创建四个裁剪平面,限制光效在地面范围内
|
||||
// 裁剪平面的法向量指向内部,距离原点 GROUND_SIZE/2
|
||||
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(0, 0, 1), GROUND_SIZE / 2),
|
||||
new THREE.Plane(new THREE.Vector3(0, 0, -1), 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), // 后边界(法向量向前)
|
||||
]
|
||||
|
||||
// 创建光带的几何体(长 30, 高 0.1, 宽 0.5 的扁平立方体)
|
||||
const geometry = new THREE.BoxGeometry(GROUND_SIZE, 0.1, 0.5)
|
||||
|
||||
// 为每种颜色创建一对光带(X 轴和 Z 轴各一条)
|
||||
LINE_COLORS.forEach((color, index) => {
|
||||
// 创建基础材质
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: new THREE.Color(color),
|
||||
transparent: true,
|
||||
opacity: 0.15 * (index + 1) * Math.random(),
|
||||
clippingPlanes: clipPlanes,
|
||||
clipShadows: true,
|
||||
depthWrite: false,
|
||||
color: new THREE.Color(color), // 光带颜色
|
||||
transparent: true, // 启用透明
|
||||
opacity: 0.15 * (index + 1) * Math.random(), // 随机透明度(越后面越不透明)
|
||||
clippingPlanes: clipPlanes, // 应用裁剪平面
|
||||
clipShadows: true, // 裁剪阴影
|
||||
depthWrite: false, // 禁用深度写入(避免透明物体遮挡问题)
|
||||
})
|
||||
|
||||
// 计算光带在垂直方向上的偏移位置
|
||||
// 将地面平均分成 6 段,每条光带占一段的中心位置
|
||||
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())
|
||||
// 设置初始位置:偶数索引从右边开始,奇数从左边开始
|
||||
xMesh.position.set(isEven ? GROUND_SIZE : -GROUND_SIZE, 0, offset)
|
||||
xMesh.renderOrder = 1
|
||||
xMesh.renderOrder = 1 // 设置渲染顺序(确保正确的透明渲染)
|
||||
scene.add(xMesh)
|
||||
|
||||
// 记录 X 轴光带的状态
|
||||
lineStates.push({
|
||||
mesh: xMesh,
|
||||
direction: isEven ? -1 : 1,
|
||||
startDelay: index * LINE_DELAY,
|
||||
elapsed: 0,
|
||||
axis: 'x',
|
||||
direction: isEven ? -1 : 1, // 移动方向:偶数向左,奇数向右
|
||||
startDelay: index * LINE_DELAY, // 启动延迟:依次增加
|
||||
elapsed: 0, // 已过时间初始化为 0
|
||||
axis: 'x', // 运动轴向为 X
|
||||
})
|
||||
|
||||
// 创建Z轴线条
|
||||
// ===== 创建 Z 轴方向的光带 =====
|
||||
const zMesh = new THREE.Mesh(geometry, material.clone())
|
||||
// 设置初始位置:偶数索引从后边开始,奇数从前边开始
|
||||
zMesh.position.set(offset, 0.01, isEven ? GROUND_SIZE : -GROUND_SIZE)
|
||||
zMesh.rotation.y = Math.PI / 2
|
||||
zMesh.renderOrder = 2
|
||||
zMesh.rotation.y = Math.PI / 2 // 旋转 90° 使光带沿 Z 轴方向
|
||||
zMesh.renderOrder = 2 // 渲染顺序略高于 X 轴光带
|
||||
scene.add(zMesh)
|
||||
|
||||
// 记录 Z 轴光带的状态
|
||||
lineStates.push({
|
||||
mesh: zMesh,
|
||||
direction: isEven ? -1 : 1,
|
||||
startDelay: index * LINE_DELAY,
|
||||
elapsed: 0,
|
||||
axis: 'z',
|
||||
direction: isEven ? -1 : 1, // 移动方向:偶数向前,奇数向后
|
||||
startDelay: index * LINE_DELAY, // 启动延迟:依次增加
|
||||
elapsed: 0, // 已过时间初始化为 0
|
||||
axis: 'z', // 运动轴向为 Z
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 更新线条动画 ==========
|
||||
/**
|
||||
* 更新所有光效线条的位置
|
||||
* 实现来回移动的动画效果
|
||||
*
|
||||
* @param deltaTime - 距离上一帧的时间差(毫秒)
|
||||
*
|
||||
* 动画逻辑:
|
||||
* 1. 检查延迟时间是否已到(未到则跳过)
|
||||
* 2. 检测是否到达边界,到达则反转方向
|
||||
* 3. 根据方向和速度更新位置
|
||||
*/
|
||||
function updateLines(deltaTime: number) {
|
||||
lineStates.forEach((state) => {
|
||||
// 累计已过时间
|
||||
state.elapsed += deltaTime
|
||||
// 如果还未达到启动延迟时间,跳过此光带
|
||||
if (state.elapsed < state.startDelay)
|
||||
return
|
||||
|
||||
// 获取当前位置(X 轴或 Z 轴)
|
||||
const pos = state.axis === 'x' ? state.mesh.position.x : state.mesh.position.z
|
||||
|
||||
// 边界检测并反转方向
|
||||
// 边界检测:到达边界时反转方向
|
||||
// 向右/向后移动超过边界,改为向左/向前
|
||||
if (pos >= GROUND_SIZE)
|
||||
state.direction = -1
|
||||
// 向左/向前移动超过边界,改为向右/向后
|
||||
else if (pos <= -GROUND_SIZE)
|
||||
state.direction = 1
|
||||
|
||||
// 更新位置
|
||||
// 计算位置变化量(速度 × 方向)
|
||||
const delta = LINE_SPEED * state.direction
|
||||
|
||||
// 根据轴向更新对应的位置
|
||||
if (state.axis === 'x') {
|
||||
state.mesh.position.x += delta
|
||||
state.mesh.position.x += delta // 更新 X 坐标
|
||||
}
|
||||
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) {
|
||||
// 请求下一帧动画(递归调用)
|
||||
animationId = requestAnimationFrame(animate)
|
||||
|
||||
// 计算距离上一帧的时间差
|
||||
const deltaTime = time - lastTime
|
||||
lastTime = time
|
||||
|
||||
// 更新控制器
|
||||
// 更新轨道控制器(处理阻尼效果)
|
||||
controls.update()
|
||||
|
||||
// 更新线条动画
|
||||
// 更新所有光效线条的位置
|
||||
updateLines(deltaTime)
|
||||
|
||||
// 响应式调整渲染器大小
|
||||
// 响应式调整渲染器大小(窗口尺寸变化时)
|
||||
const { width, height } = getElementSize()
|
||||
if (camera.aspect !== width / height) {
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(width, height)
|
||||
camera.aspect = width / height // 更新相机宽高比
|
||||
camera.updateProjectionMatrix() // 更新投影矩阵
|
||||
renderer.setSize(width, height) // 更新渲染器尺寸
|
||||
}
|
||||
|
||||
// 渲染场景
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
// ========== 清理资源 ==========
|
||||
/**
|
||||
* 清理所有 Three.js 资源
|
||||
* 防止内存泄漏,组件卸载时调用
|
||||
*
|
||||
* 清理内容:
|
||||
* 1. 取消动画帧
|
||||
* 2. 销毁控制器
|
||||
* 3. 销毁渲染器
|
||||
* 4. 释放几何体和材质
|
||||
* 5. 清空状态数组
|
||||
*/
|
||||
function cleanup() {
|
||||
// 取消动画帧
|
||||
if (animationId)
|
||||
cancelAnimationFrame(animationId)
|
||||
|
||||
// 销毁轨道控制器
|
||||
if (controls)
|
||||
controls.dispose()
|
||||
|
||||
// 销毁渲染器并移除 DOM 元素
|
||||
if (renderer) {
|
||||
renderer.dispose()
|
||||
threeRef.value?.removeChild(renderer.domElement)
|
||||
}
|
||||
|
||||
// 遍历场景,释放所有网格的资源
|
||||
scene?.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
object.geometry?.dispose()
|
||||
object.geometry?.dispose() // 释放几何体
|
||||
// 处理材质(可能是单个或数组)
|
||||
if (Array.isArray(object.material)) {
|
||||
object.material.forEach(m => m.dispose())
|
||||
}
|
||||
@@ -223,32 +332,49 @@ function cleanup() {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 清空光效状态数组
|
||||
lineStates.length = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停地面光效动画
|
||||
* 停止动画循环,但保留所有光带对象
|
||||
*/
|
||||
function pauseGroundLightEffect() {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复地面光效动画
|
||||
* 重新启动动画循环
|
||||
*/
|
||||
function resumeGroundLightEffect() {
|
||||
animate()
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁地面光效
|
||||
* 从场景中移除所有光带对象
|
||||
*/
|
||||
function destoryGroundLightEffect() {
|
||||
lineStates.forEach((state) => {
|
||||
state.mesh.parent?.remove(state.mesh)
|
||||
state.mesh.parent?.remove(state.mesh) // 从父对象(场景)中移除
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(initModel)
|
||||
onBeforeUnmount(cleanup)
|
||||
// 生命周期钩子
|
||||
onMounted(initModel) // 组件挂载时初始化场景
|
||||
onBeforeUnmount(cleanup) // 组件卸载前清理资源
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="threeRef" class="model" />
|
||||
<!-- 导航链接:跳转到下一个 Demo -->
|
||||
<router-link to="/three/6" class="position-fixed left-20px top-20px">
|
||||
模型漫游(巡检业务)
|
||||
</router-link>
|
||||
<!-- 控制按钮组 -->
|
||||
<div class="position-fixed right-20px top-20px flex gap-12px">
|
||||
<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>
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { onMounted, shallowRef } from 'vue'
|
||||
|
||||
const threeRef = shallowRef<HTMLCanvasElement>()
|
||||
// ========== 响应式状态 ==========
|
||||
const threeRef = shallowRef<HTMLCanvasElement>() // Canvas 容器引用
|
||||
|
||||
// 模型中包含的所有动画片段
|
||||
const animations = shallowRef<THREE.AnimationClip[]>([])
|
||||
// 加载的 3D 模型对象
|
||||
const model = shallowRef<THREE.Group>()
|
||||
|
||||
// ========== Three.js 核心对象 ==========
|
||||
let scene: THREE.Scene | undefined
|
||||
|
||||
/**
|
||||
* 初始化 Three.js 场景
|
||||
* 创建场景、相机、渲染器、灯光等基础设施
|
||||
*/
|
||||
function initModel() {
|
||||
const width = threeRef.value!.clientWidth
|
||||
const height = threeRef.value!.clientHeight
|
||||
|
||||
// 创建场景
|
||||
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(
|
||||
75,
|
||||
width / height,
|
||||
0.1,
|
||||
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.shadowMap.enabled = true
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
||||
renderer.shadowMap.enabled = true // 启用阴影渲染
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap // PCF 软阴影
|
||||
threeRef.value!.appendChild(renderer.domElement)
|
||||
|
||||
// 创建轨道控制器 - 允许用户旋转、缩放视角
|
||||
const controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
controls.enableDamping = true // 启用阻尼(平滑过渡)
|
||||
controls.dampingFactor = 0.05 // 阻尼系数
|
||||
|
||||
// 添加坐标轴辅助工具(红 X, 绿 Y, 蓝 Z,长度 5)
|
||||
const axesHelper = new THREE.AxesHelper(5)
|
||||
scene.add(axesHelper)
|
||||
|
||||
// 环境光
|
||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5)
|
||||
// ===== 灯光系统 =====
|
||||
|
||||
// 环境光 - 提供无方向的均匀照明
|
||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5) // 白色,强度 0.5
|
||||
scene.add(ambientLight)
|
||||
|
||||
// 方向光(主光)
|
||||
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 0.8)
|
||||
directionalLight.position.set(20, 30, 20)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.shadow.mapSize.set(2048, 2048)
|
||||
directionalLight.shadow.camera.near = 0.5
|
||||
directionalLight.shadow.camera.far = 50
|
||||
// 方向光(主光源)- 模拟太阳光
|
||||
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 0.8) // 白色,强度 0.8
|
||||
directionalLight.position.set(20, 30, 20) // 光源位置(右上方)
|
||||
directionalLight.castShadow = true // 投射阴影
|
||||
|
||||
// 配置阴影参数
|
||||
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.right = 20
|
||||
directionalLight.shadow.camera.top = 20
|
||||
directionalLight.shadow.camera.bottom = -20
|
||||
scene.add(directionalLight)
|
||||
|
||||
// 点光源
|
||||
const pointLight = new THREE.PointLight(0x409EFF, 0.5, 50)
|
||||
pointLight.position.set(0, 10, 0)
|
||||
// 点光源 - 从一点向四周发光(蓝色调)
|
||||
const pointLight = new THREE.PointLight(0x409EFF, 0.5, 50) // 颜色, 强度, 距离
|
||||
pointLight.position.set(0, 10, 0) // 位置(头顶上方)
|
||||
scene.add(pointLight)
|
||||
|
||||
// ===== 加载 GLTF 模型 =====
|
||||
const loader = new GLTFLoader()
|
||||
loader.load('/lxb_grp.glb', (gltf) => {
|
||||
scene!.add(gltf.scene)
|
||||
model.value = gltf.scene
|
||||
animations.value = gltf.animations ?? []
|
||||
// 模型加载成功回调
|
||||
scene!.add(gltf.scene) // 将模型添加到场景
|
||||
model.value = gltf.scene // 保存模型引用(供动画使用)
|
||||
animations.value = gltf.animations ?? [] // 提取模型内置的动画片段
|
||||
})
|
||||
|
||||
/**
|
||||
* 动画循环函数
|
||||
* 持续渲染场景,并处理窗口尺寸变化
|
||||
*/
|
||||
function animate() {
|
||||
requestAnimationFrame(animate)
|
||||
renderer.render(scene!, camera)
|
||||
requestAnimationFrame(animate) // 递归调用,保持动画循环
|
||||
renderer.render(scene!, camera) // 渲染场景
|
||||
|
||||
// 响应式调整渲染器尺寸(窗口大小变化时)
|
||||
renderer.setSize(width, height)
|
||||
camera.aspect = width / height
|
||||
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) {
|
||||
// 获取指定索引的动画片段
|
||||
const clip = animations.value[index]
|
||||
if (!clip)
|
||||
return
|
||||
return // 如果动画不存在,直接返回
|
||||
|
||||
// 创建动画混合器,绑定到模型对象
|
||||
// AnimationMixer 负责管理和更新动画状态
|
||||
const mixer = new THREE.AnimationMixer(model.value!)
|
||||
|
||||
// 从混合器创建动画动作
|
||||
// clipAction() 返回一个可控制的动画动作对象
|
||||
const action = mixer.clipAction(clip)
|
||||
|
||||
// 播放动画
|
||||
action.play()
|
||||
|
||||
// 可选:定时停止动画(已注释)
|
||||
// setTimeout(() => {
|
||||
// action.stop()
|
||||
// }, 3000)
|
||||
|
||||
// 创建时钟对象,用于计算帧之间的时间差
|
||||
const clock = new THREE.Clock()
|
||||
// 在动画循环中更新混合器
|
||||
|
||||
/**
|
||||
* 动画更新循环
|
||||
* 持续更新混合器状态,使动画平滑播放
|
||||
*/
|
||||
function updateMixer() {
|
||||
requestAnimationFrame(updateMixer)
|
||||
const delta = clock.getDelta()
|
||||
mixer.update(delta)
|
||||
}
|
||||
updateMixer()
|
||||
requestAnimationFrame(updateMixer) // 递归调用,持续更新
|
||||
const delta = clock.getDelta() // 获取距离上一帧的时间差(秒)
|
||||
mixer.update(delta) // 更新混合器,推进动画状态
|
||||
}
|
||||
|
||||
updateMixer() // 启动动画更新循环
|
||||
}
|
||||
|
||||
// 组件挂载时初始化场景
|
||||
onMounted(initModel)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="threeRef" class="model" />
|
||||
<!-- 导航链接:跳转到下一个 Demo -->
|
||||
<router-link to="/three/7" class="position-fixed left-20px top-20px">
|
||||
模型漫游(巡检业务)
|
||||
</router-link>
|
||||
<!-- 动画控制按钮组 -->
|
||||
<!-- 根据模型中的动画数量动态生成按钮 -->
|
||||
<div class="position-fixed right-20px top-20px flex gap-12px">
|
||||
<button v-for="(ani, index) in animations" :key="index" @click="handlePlay(index)">
|
||||
播放动画{{ 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: 刘 相卿
|
||||
* @Date: 2025-11-17 16:07:33
|
||||
* @LastEditors: 刘 相卿
|
||||
@@ -11,148 +25,174 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||
|
||||
const threeRef = shallowRef<HTMLDivElement>()
|
||||
const camera = shallowRef<THREE.PerspectiveCamera>()
|
||||
const scene = shallowRef<THREE.Scene>()
|
||||
const controls = shallowRef<OrbitControls>()
|
||||
const renderer = shallowRef<THREE.WebGLRenderer>()
|
||||
// ========== 响应式状态 ==========
|
||||
const threeRef = shallowRef<HTMLDivElement>() // Canvas 容器
|
||||
const camera = shallowRef<THREE.PerspectiveCamera>() // 透视相机
|
||||
const scene = shallowRef<THREE.Scene>() // 场景对象
|
||||
const controls = shallowRef<OrbitControls>() // 轨道控制器
|
||||
const renderer = shallowRef<THREE.WebGLRenderer>() // WebGL 渲染器
|
||||
|
||||
// 漫游状态
|
||||
const tourMode = ref<'none' | 'slow' | 'fast'>('none')
|
||||
let tourTime = 0
|
||||
const initialCameraPosition = new THREE.Vector3(0, 5, 15)
|
||||
const initialCameraTarget = new THREE.Vector3(0, 0, 0)
|
||||
// ========== 漫游状态管理 ==========
|
||||
const tourMode = ref<'none' | 'slow' | 'fast'>('none') // 巡检模式:无、慢速、快速
|
||||
let tourTime = 0 // 巡检动画时间累加器
|
||||
const initialCameraPosition = new THREE.Vector3(0, 5, 15) // 相机初始位置
|
||||
const initialCameraTarget = new THREE.Vector3(0, 0, 0) // 相机初始观察目标
|
||||
|
||||
// 巡检路径点位 - 模拟进入建筑内部进行详细巡检
|
||||
// 包含位置和对应的观察目标点
|
||||
// ========== 巡检路径定义 ==========
|
||||
/**
|
||||
* 路径点接口
|
||||
* 定义相机在巡检过程中的位置和观察目标
|
||||
*/
|
||||
interface PathPoint {
|
||||
position: THREE.Vector3
|
||||
lookAt: THREE.Vector3
|
||||
position: THREE.Vector3 // 相机位置
|
||||
lookAt: THREE.Vector3 // 相机观察目标(朝向)
|
||||
}
|
||||
|
||||
/**
|
||||
* 巡检路径点数组
|
||||
*
|
||||
* 巡检路线设计:
|
||||
* 1. 外部俯视 → 2. 正面接近 → 3. 进入前厅 →
|
||||
* 4-5. 右侧巡检 → 6. 右后方 → 7. 后方中央 →
|
||||
* 8-9. 左侧巡检 → 10. 左前方 → 11-12. 中央回顾 →
|
||||
* 13. 退出俯视
|
||||
*
|
||||
* 形成完整的顺时针巡检循环
|
||||
*/
|
||||
const inspectionPath: PathPoint[] = [
|
||||
// 1. 外部俯视全景
|
||||
// 1. 外部俯视全景 - 建立整体认知
|
||||
{ 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) },
|
||||
// 3. 进入前厅区域
|
||||
// 3. 进入前厅区域 - 穿过入口
|
||||
{ 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) },
|
||||
// 5. 深入右侧内部
|
||||
// 5. 深入右侧内部 - 详细检查右侧深处
|
||||
{ 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) },
|
||||
// 7. 后方中央区域
|
||||
// 7. 后方中央区域 - 检查后方设施
|
||||
{ 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) },
|
||||
// 9. 深入左侧内部
|
||||
// 9. 深入左侧内部 - 详细检查左侧深处
|
||||
{ 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) },
|
||||
// 11. 返回中央区域
|
||||
// 11. 返回中央区域 - 回到中心
|
||||
{ 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) },
|
||||
// 13. 退出并俯视
|
||||
// 13. 退出并俯视 - 完成巡检,恢复初始视角
|
||||
{ position: new THREE.Vector3(0, 8, 12), lookAt: new THREE.Vector3(0, 0, 0) },
|
||||
]
|
||||
|
||||
/**
|
||||
* 初始化 Three.js 场景
|
||||
* 创建场景、相机、渲染器、灯光、地面等基础元素
|
||||
*/
|
||||
function initModel() {
|
||||
const width = threeRef.value!.clientWidth
|
||||
const height = threeRef.value!.clientHeight
|
||||
|
||||
// 创建场景
|
||||
// ===== 创建场景 =====
|
||||
scene.value = new THREE.Scene()
|
||||
|
||||
// 创建天空盒
|
||||
// ===== 创建天空盒(立方体背景)=====
|
||||
// 使用6个面的立方体模拟天空
|
||||
const skyboxGeometry = new THREE.BoxGeometry(500, 500, 500)
|
||||
const skyboxMaterials = [
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // right
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // left
|
||||
new THREE.MeshBasicMaterial({ color: 0x0F0F1E, side: THREE.BackSide }), // top
|
||||
new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.BackSide }), // bottom
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // front
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // back
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 右侧(深蓝灰)
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 左侧
|
||||
new THREE.MeshBasicMaterial({ color: 0x0F0F1E, side: THREE.BackSide }), // 顶部(更深)
|
||||
new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.BackSide }), // 底部(黑色)
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 前侧
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 后侧
|
||||
]
|
||||
const skybox = new THREE.Mesh(skyboxGeometry, skyboxMaterials)
|
||||
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)
|
||||
|
||||
// 创建透视相机 PerspectiveCamera(视野角度, 宽高比, 近截面, 远截面)
|
||||
// ===== 创建透视相机 =====
|
||||
// PerspectiveCamera(视野角度, 宽高比, 近截面, 远截面)
|
||||
camera.value = new THREE.PerspectiveCamera(
|
||||
60,
|
||||
60, // 视野角度 60°(较窄,适合建筑巡检)
|
||||
width / height,
|
||||
0.1,
|
||||
1000,
|
||||
0.1, // 近裁剪面
|
||||
1000, // 远裁剪面
|
||||
)
|
||||
camera.value.position.copy(initialCameraPosition)
|
||||
camera.value.position.copy(initialCameraPosition) // 设置初始位置
|
||||
|
||||
// 创建渲染器
|
||||
renderer.value = new THREE.WebGLRenderer({ antialias: true })
|
||||
// ===== 创建 WebGL 渲染器 =====
|
||||
renderer.value = new THREE.WebGLRenderer({ antialias: true }) // 抗锯齿
|
||||
renderer.value.setSize(width, height)
|
||||
renderer.value.setPixelRatio(window.devicePixelRatio)
|
||||
renderer.value.setPixelRatio(window.devicePixelRatio) // 高清屏适配
|
||||
// 开启阴影贴图
|
||||
renderer.value.shadowMap.enabled = true
|
||||
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap
|
||||
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap // PCF 软阴影
|
||||
threeRef.value!.appendChild(renderer.value.domElement)
|
||||
|
||||
// 创建轨道控制器
|
||||
// ===== 创建轨道控制器 =====
|
||||
controls.value = new OrbitControls(camera.value, renderer.value.domElement)
|
||||
controls.value.enableDamping = true // 启用阻尼(惯性)
|
||||
controls.value.dampingFactor = 0.05
|
||||
controls.value.minDistance = 2
|
||||
controls.value.maxDistance = 50
|
||||
controls.value.maxPolarAngle = Math.PI / 2 * 1.8 // 限制垂直旋转角度
|
||||
controls.value.enableDamping = true // 启用阻尼(惯性效果)
|
||||
controls.value.dampingFactor = 0.05 // 阻尼系数
|
||||
controls.value.minDistance = 2 // 最小缩放距离
|
||||
controls.value.maxDistance = 50 // 最大缩放距离
|
||||
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)
|
||||
|
||||
// 添加主平行光(模拟太阳光)
|
||||
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.2)
|
||||
directionalLight.position.set(10, 15, 10)
|
||||
directionalLight.castShadow = true
|
||||
// 主平行光(模拟太阳光)
|
||||
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.2) // 强度 1.2
|
||||
directionalLight.position.set(10, 15, 10) // 光源位置(右上方)
|
||||
directionalLight.castShadow = true // 投射阴影
|
||||
|
||||
// 配置阴影相机范围(正交投影)
|
||||
directionalLight.shadow.camera.left = -20
|
||||
directionalLight.shadow.camera.right = 20
|
||||
directionalLight.shadow.camera.top = 20
|
||||
directionalLight.shadow.camera.bottom = -20
|
||||
directionalLight.shadow.mapSize.width = 2048
|
||||
directionalLight.shadow.mapSize.width = 2048 // 阴影贴图分辨率
|
||||
directionalLight.shadow.mapSize.height = 2048
|
||||
scene.value.add(directionalLight)
|
||||
|
||||
// 添加辅助光源(从另一个方向)
|
||||
// 辅助光源(填充光)- 从另一个方向照亮暗部
|
||||
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.5)
|
||||
fillLight.position.set(-10, 10, -10)
|
||||
fillLight.position.set(-10, 10, -10) // 左上后方
|
||||
scene.value.add(fillLight)
|
||||
|
||||
// 添加地面
|
||||
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
||||
// ===== 创建地面 =====
|
||||
const groundGeometry = new THREE.PlaneGeometry(100, 100) // 100x100 的平面
|
||||
const groundMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0x999999,
|
||||
roughness: 0.8,
|
||||
metalness: 0.2,
|
||||
color: 0x999999, // 灰色
|
||||
roughness: 0.8, // 粗糙度(不光滑)
|
||||
metalness: 0.2, // 金属度(略有金属感)
|
||||
})
|
||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||
ground.rotation.x = -Math.PI / 2
|
||||
ground.position.y = 0
|
||||
ground.receiveShadow = true
|
||||
ground.rotation.x = -Math.PI / 2 // 旋转 90° 使其水平
|
||||
ground.position.y = 0 // 位于 Y=0 平面
|
||||
ground.receiveShadow = true // 接收阴影
|
||||
scene.value.add(ground)
|
||||
|
||||
// 加载模型
|
||||
// ===== 加载 GLTF 模型 =====
|
||||
const loader = new GLTFLoader()
|
||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||
const model = gltf.scene
|
||||
|
||||
// 遍历模型,启用阴影
|
||||
// 遍历模型所有子对象,启用阴影
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
child.castShadow = true // 投射阴影
|
||||
child.receiveShadow = true // 接收阴影
|
||||
}
|
||||
})
|
||||
|
||||
@@ -161,46 +201,56 @@ function initModel() {
|
||||
console.error('模型加载失败:', error)
|
||||
})
|
||||
|
||||
/**
|
||||
* 动画循环函数
|
||||
*
|
||||
* 职责:
|
||||
* 1. 处理巡检动画逻辑(相机路径插值)
|
||||
* 2. 更新轨道控制器
|
||||
* 3. 渲染场景
|
||||
*/
|
||||
function animate() {
|
||||
requestAnimationFrame(animate)
|
||||
requestAnimationFrame(animate) // 递归调用
|
||||
|
||||
// 巡检漫游效果
|
||||
// ===== 巡检漫游动画逻辑 =====
|
||||
if (tourMode.value !== 'none' && camera.value && controls.value) {
|
||||
// 根据模式确定移动速度
|
||||
const speed = tourMode.value === 'slow' ? 0.003 : 0.008 // 慢速或快速
|
||||
tourTime += speed
|
||||
tourTime += speed // 累加时间
|
||||
|
||||
const totalPoints = inspectionPath.length
|
||||
const totalProgress = tourTime % totalPoints
|
||||
const currentIndex = Math.floor(totalProgress)
|
||||
const nextIndex = (currentIndex + 1) % totalPoints
|
||||
const t = totalProgress - currentIndex
|
||||
const totalPoints = inspectionPath.length // 路径点总数
|
||||
const totalProgress = tourTime % totalPoints // 当前进度(0 到 totalPoints 循环)
|
||||
const currentIndex = Math.floor(totalProgress) // 当前路径点索引
|
||||
const nextIndex = (currentIndex + 1) % totalPoints // 下一个路径点索引(循环)
|
||||
const t = totalProgress - currentIndex // 两点之间的插值参数(0-1)
|
||||
|
||||
// 使用平滑的缓动函数
|
||||
const smoothT = t * t * (3 - 2 * t) // smoothstep
|
||||
// 使用平滑的缓动函数(SmoothStep)
|
||||
// smoothstep: 在 0 和 1 处导数为 0,产生平滑的加速减速效果
|
||||
const smoothT = t * t * (3 - 2 * t)
|
||||
|
||||
// 获取当前和下一个路径点
|
||||
const currentPoint = inspectionPath[currentIndex]!
|
||||
const nextPoint = inspectionPath[nextIndex]!
|
||||
|
||||
// 插值位置
|
||||
// 插值计算相机位置
|
||||
const newPosition = new THREE.Vector3()
|
||||
newPosition.lerpVectors(currentPoint.position, nextPoint.position, smoothT)
|
||||
camera.value.position.copy(newPosition)
|
||||
|
||||
// 插值观察目标
|
||||
// 插值计算观察目标
|
||||
const newTarget = new THREE.Vector3()
|
||||
newTarget.lerpVectors(currentPoint.lookAt, nextPoint.lookAt, smoothT)
|
||||
controls.value.target.copy(newTarget)
|
||||
|
||||
// 禁用控制器的用户交互
|
||||
// 巡检模式下禁用用户控制
|
||||
controls.value.enabled = false
|
||||
}
|
||||
else if (controls.value) {
|
||||
// 非漫游模式时启用控制器
|
||||
// 非巡检模式时启用用户控制
|
||||
controls.value.enabled = true
|
||||
}
|
||||
|
||||
// 更新控制器
|
||||
// 更新轨道控制器(处理阻尼等)
|
||||
controls.value?.update()
|
||||
|
||||
// 执行渲染
|
||||
@@ -208,13 +258,16 @@ function initModel() {
|
||||
renderer.value.render(scene.value, camera.value)
|
||||
}
|
||||
}
|
||||
animate()
|
||||
animate() // 启动动画循环
|
||||
|
||||
// 窗口自适应
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
/**
|
||||
* 处理窗口大小变化
|
||||
* 更新相机宽高比和渲染器尺寸
|
||||
*/
|
||||
function handleResize() {
|
||||
if (!threeRef.value || !camera.value || !renderer.value)
|
||||
return
|
||||
@@ -224,55 +277,66 @@ function handleResize() {
|
||||
|
||||
// 更新相机宽高比
|
||||
camera.value.aspect = width / height
|
||||
camera.value.updateProjectionMatrix()
|
||||
camera.value.updateProjectionMatrix() // 必须调用以应用更改
|
||||
|
||||
// 更新渲染器尺寸
|
||||
renderer.value.setSize(width, height)
|
||||
renderer.value.setPixelRatio(window.devicePixelRatio)
|
||||
}
|
||||
|
||||
// 慢速巡检漫游
|
||||
/**
|
||||
* 启动慢速巡检
|
||||
* 速度:0.003,适合详细观察
|
||||
*/
|
||||
function startSlowTour() {
|
||||
tourMode.value = 'slow'
|
||||
tourTime = 0
|
||||
tourTime = 0 // 重置时间,从第一个路径点开始
|
||||
}
|
||||
|
||||
// 快速巡检漫游
|
||||
/**
|
||||
* 启动快速巡检
|
||||
* 速度:0.008,适合快速浏览
|
||||
*/
|
||||
function startFastTour() {
|
||||
tourMode.value = 'fast'
|
||||
tourTime = 0
|
||||
tourTime = 0 // 重置时间,从第一个路径点开始
|
||||
}
|
||||
|
||||
// 停止漫游
|
||||
/**
|
||||
* 停止巡检
|
||||
* 恢复相机到初始位置,重新启用用户控制
|
||||
*/
|
||||
function stopTour() {
|
||||
tourMode.value = 'none'
|
||||
tourTime = 0
|
||||
if (camera.value && controls.value) {
|
||||
camera.value.position.copy(initialCameraPosition)
|
||||
controls.value.target.copy(initialCameraTarget)
|
||||
controls.value.enabled = true
|
||||
camera.value.position.copy(initialCameraPosition) // 恢复初始位置
|
||||
controls.value.target.copy(initialCameraTarget) // 恢复初始目标
|
||||
controls.value.enabled = true // 重新启用用户控制
|
||||
controls.value.update()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(initModel)
|
||||
// ===== 生命周期钩子 =====
|
||||
onMounted(initModel) // 组件挂载时初始化
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理事件监听
|
||||
window.removeEventListener('resize', handleResize)
|
||||
// 销毁控制器
|
||||
controls.value?.dispose()
|
||||
// 销毁渲染器
|
||||
renderer.value?.dispose()
|
||||
// 组件卸载时清理资源
|
||||
window.removeEventListener('resize', handleResize) // 移除事件监听
|
||||
controls.value?.dispose() // 销毁控制器
|
||||
renderer.value?.dispose() // 销毁渲染器
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="threeRef" class="w-full h-full" />
|
||||
<!-- 导航链接 -->
|
||||
<router-link to="/three/8" class="position-fixed left-20px top-20px">
|
||||
漫游 修改材质(设备告警业务)
|
||||
</router-link>
|
||||
<!-- 控制面板 -->
|
||||
<div class="position-fixed right-20px top-20px flex flex-col gap-12px">
|
||||
<!-- 巡检控制按钮组 -->
|
||||
<div class="flex gap-12px">
|
||||
<button
|
||||
:class="{ 'bg-blue-500 text-white': tourMode === 'slow' }"
|
||||
@@ -296,6 +360,7 @@ onUnmounted(() => {
|
||||
停止漫游
|
||||
</button>
|
||||
</div>
|
||||
<!-- 状态提示 -->
|
||||
<div v-if="tourMode !== 'none'" class="bg-white px-12px py-8px rounded shadow text-sm">
|
||||
正在进行{{ tourMode === 'slow' ? '慢速' : '快速' }}巡检...
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user