This commit is contained in:
2025-11-21 13:51:19 +08:00
parent e047368930
commit 92f624cd45
4 changed files with 817 additions and 361 deletions

View File

@@ -6,215 +6,312 @@
* @LastEditTime: 2025-11-21 10:04:12
-->
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
/**
* 核心库导入
*/
import * as THREE from 'three' // Three.js 核心库
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' // 轨道控制器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' // GLTF模型加载器
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' // 后期处理合成器
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js' // 轮廓描边通道
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js' // 基础渲染通道
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
// 物体信息类型
/**
* 物体信息接口
* 存储被选中物体的详细信息用于显示在UI标签中
*/
interface ObjectInfo {
name: string
material: string
position: THREE.Vector3
mesh: THREE.Mesh
name: string // 物体名称
material: string // 材质类型
position: THREE.Vector3 // 点击位置(世界坐标)
mesh: THREE.Mesh // 物体网格引用
}
const threeRef = shallowRef<HTMLDivElement>()
/**
* Three.js 基础对象引用
*/
const threeRef = shallowRef<HTMLDivElement>() // Three.js 容器DOM引用
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let animationId: number
let raycaster: THREE.Raycaster
let mouse: THREE.Vector2
let model: THREE.Group
// 场景核心对象使用let声明在init中初始化
let scene: THREE.Scene // 3D场景
let camera: THREE.PerspectiveCamera // 透视相机
let renderer: THREE.WebGLRenderer // WebGL渲染器
let controls: OrbitControls // 轨道控制器
let animationId: number // 动画帧ID用于取消动画
// 后期处理相关
let composer: EffectComposer
let outlinePass: OutlinePass
/**
* 射线检测相关
* 用于实现鼠标与3D物体的交互悬停、点击
*/
let raycaster: THREE.Raycaster // 射线投射器,用于检测鼠标与物体的交互
let mouse: THREE.Vector2 // 归一化的鼠标坐标 (-1 到 1)
let model: THREE.Group // 加载的GLTF模型组
// 交互状态
const hoveredObject = shallowRef<THREE.Mesh | null>(null)
const selectedObject = ref<ObjectInfo | null>(null)
const labelPosition = ref({ x: 0, y: 0 })
/**
* 后期处理相关
* 实现轮廓描边等视觉效果
*/
let composer: EffectComposer // 后期处理合成器
let outlinePass: OutlinePass // 轮廓描边通道
// 辅助线
let axesHelper: THREE.AxesHelper | null = null
/**
* 交互状态管理
*/
const hoveredObject = shallowRef<THREE.Mesh | null>(null) // 当前鼠标悬停的物体
const selectedObject = ref<ObjectInfo | null>(null) // 当前选中的物体信息
const labelPosition = ref({ x: 0, y: 0 }) // 信息标签的屏幕坐标位置
/**
* 辅助工具
*/
let axesHelper: THREE.AxesHelper | null = null // 坐标轴辅助线,显示在悬停物体中心
/**
* 初始化Three.js场景和交互系统
* 创建场景、相机、渲染器、光照、后期处理和事件监听
* 核心功能模型与Web的交互鼠标悬停、点击、信息展示
*/
function init() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
// ========== 场景设置 ==========
// 创建深蓝紫色背景的3D场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0x1A1A2E)
scene.background = new THREE.Color(0x1A1A2E) // 深蓝紫色背景
// ========== 相机设置 ==========
// 创建透视相机视野75度初始位置在斜上方
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 5, 15)
camera.position.set(0, 5, 15) // 从前上方观察原点
// ========== 渲染器设置 ==========
// 创建WebGL渲染器启用抗锯齿适配高分屏
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setPixelRatio(window.devicePixelRatio) // 适配高分辨率屏幕
threeRef.value!.appendChild(renderer.domElement)
// ========== 轨道控制器设置 ==========
// 允许用户通过鼠标控制视角,启用阻尼效果
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.enableDamping = true // 启用阻尼(惯性)
controls.dampingFactor = 0.05 // 阻尼系数,值越小越平滑
// 射线检测器
raycaster = new THREE.Raycaster()
mouse = new THREE.Vector2()
// ========== 射线检测器初始化 ==========
// 用于检测鼠标与3D物体的交互
raycaster = new THREE.Raycaster() // 从相机发射射线
mouse = new THREE.Vector2() // 归一化鼠标坐标(-1到1
// 添加灯光 - 增强照明
// 环境光 - 提供基础亮度
// ========== 光照系统 ==========
// 多光源设置,确保模型各个角度都有良好的照明
// 环境光:提供基础亮度,避免场景过暗
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 5)
scene.add(ambientLight)
// 主方向光 - 从右上方照射
// 主方向光:模拟太阳光,从右上方照射,产生阴影
const mainLight = new THREE.DirectionalLight(0xFFFFFF, 1.5)
mainLight.position.set(10, 15, 10)
mainLight.castShadow = true
mainLight.position.set(10, 15, 10) // 右上方
mainLight.castShadow = true // 投射阴影
scene.add(mainLight)
// 辅助方向光 - 从左侧照射,补充阴影区域
// 辅助方向光:从左侧补光,减少阴影区域的暗度
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.8)
fillLight.position.set(-10, 10, -10)
fillLight.position.set(-10, 10, -10) // 左上方
scene.add(fillLight)
// 背光 - 从后方照射,增轮廓感
// 背光从后方照射,增强物体轮廓感
const backLight = new THREE.DirectionalLight(0xFFFFFF, 0.6)
backLight.position.set(0, 5, -15)
backLight.position.set(0, 5, -15) // 后方
scene.add(backLight)
// 点光源 - 增加局部亮度
// 点光源:从顶部照射,增加局部亮度
const pointLight = new THREE.PointLight(0xFFFFFF, 1, 50)
pointLight.position.set(0, 10, 0)
pointLight.position.set(0, 10, 0) // 顶部中心
scene.add(pointLight)
// 添加地面
// ========== 地面创建 ==========
// 创建100x100的灰色地面用于承载模型
const groundGeometry = new THREE.PlaneGeometry(100, 100)
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x555555,
roughness: 0.8,
metalness: 0.2,
color: 0x555555, // 中灰色
roughness: 0.8, // 较高粗糙度
metalness: 0.2, // 轻微金属感
})
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
ground.rotation.x = -Math.PI / 2
ground.rotation.x = -Math.PI / 2 // 旋转90度使其水平
ground.position.y = 0
scene.add(ground)
// 设置后期处理
// ========== 设置后期处理 ==========
// 配置轮廓描边效果
setupPostProcessing()
// 加载模型
// ========== 加载GLTF模型 ==========
const loader = new GLTFLoader()
loader.load('/mzjc_bansw.glb', (gltf) => {
model = gltf.scene
// 遍历模型所有子节点,添加自定义属性
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
// 为每个网格添加元数据,用于信息展示
child.userData.objectType = '建筑构件'
child.userData.objectName = child.name || '未命名物体'
}
})
scene.add(model)
})
// 事件监听
renderer.domElement.addEventListener('mousemove', onMouseMove)
renderer.domElement.addEventListener('click', onCanvasClick)
window.addEventListener('resize', onResize)
// ========== 事件监听 ==========
// 监听鼠标移动、点击和窗口大小变化
renderer.domElement.addEventListener('mousemove', onMouseMove) // 鼠标移动检测
renderer.domElement.addEventListener('click', onCanvasClick) // 鼠标点击检测
window.addEventListener('resize', onResize) // 窗口大小变化
// 启动渲染循环
animate()
}
// 设置后期处理(轮廓描边)
/**
* 设置后期处理效果(轮廓描边)
* 使用EffectComposer实现物体选中时的绿色轮廓高亮效果
*
* 工作原理:
* 1. RenderPass渲染基础场景
* 2. OutlinePass为指定物体添加轮廓描边
*/
function setupPostProcessing() {
// 创建后期处理合成器
composer = new EffectComposer(renderer)
// 添加基础渲染通道(渲染场景本身)
const renderPass = new RenderPass(scene, camera)
composer.addPass(renderPass)
// 创建轮廓描边通道
outlinePass = new OutlinePass(
new THREE.Vector2(threeRef.value!.clientWidth, threeRef.value!.clientHeight),
scene,
camera,
)
outlinePass.edgeStrength = 3
outlinePass.edgeGlow = 0.5
outlinePass.edgeThickness = 2
outlinePass.pulsePeriod = 0
outlinePass.visibleEdgeColor.set('#00ff00')
outlinePass.hiddenEdgeColor.set('#00ff00')
// 配置轮廓效果参数
outlinePass.edgeStrength = 3 // 边缘强度
outlinePass.edgeGlow = 0.5 // 边缘发光强度
outlinePass.edgeThickness = 2 // 边缘厚度
outlinePass.pulsePeriod = 0 // 脉冲周期0=不脉冲)
outlinePass.visibleEdgeColor.set('#00ff00') // 可见边缘颜色:绿色
outlinePass.hiddenEdgeColor.set('#00ff00') // 隐藏边缘颜色:绿色
// 添加轮廓通道到合成器
composer.addPass(outlinePass)
}
/**
* 动画循环函数
* 每帧执行的逻辑:
* 1. 更新轨道控制器(处理阻尼效果)
* 2. 更新信息标签位置(跟随选中物体)
* 3. 渲染场景(包含后期处理效果)
*/
function animate() {
animationId = requestAnimationFrame(animate)
controls.update()
controls.update() // 更新轨道控制器
// 更新标签位置
// 如果有选中物体,实时更新标签位置
if (selectedObject.value) {
updateLabelPosition()
}
// 使用后期处理合成器渲染(包含轮廓描边)
composer.render()
}
// 更新标签位置
/**
* 更新信息标签的屏幕位置
* 将3D世界坐标转换为2D屏幕坐标
*
* 步骤:
* 1. 计算物体包围盒,获取高度
* 2. 将标签位置设置在物体顶部上方30%处
* 3. 将3D坐标投影到屏幕空间
* 4. 转换为像素坐标
*/
function updateLabelPosition() {
if (!selectedObject.value)
return
const position = selectedObject.value.position.clone()
// 获取物体的包围盒来计算高度
// 计算物体包围盒,确定标签显示高度
const box = new THREE.Box3().setFromObject(selectedObject.value.mesh)
const height = box.max.y - box.min.y
// 标签显示在物体顶部上方30%高度处
position.y = box.max.y + height * 0.3
// 将3D世界坐标投影到归一化设备坐标-1到1
position.project(camera)
// 转换为屏幕像素坐标
labelPosition.value = {
x: (position.x * 0.5 + 0.5) * threeRef.value!.clientWidth,
y: (-(position.y * 0.5) + 0.5) * threeRef.value!.clientHeight,
x: (position.x * 0.5 + 0.5) * threeRef.value!.clientWidth, // X坐标
y: (-(position.y * 0.5) + 0.5) * threeRef.value!.clientHeight, // Y坐标翻转
}
}
// 鼠标移动事件
/**
* 鼠标移动事件处理
* 实现鼠标悬停高亮效果
*
* 功能:
* 1. 将鼠标坐标转换为归一化设备坐标(-1到1
* 2. 使用射线检测鼠标下的物体
* 3. 为悬停物体添加绿色轮廓描边
* 4. 在悬停物体中心显示坐标轴
* 5. 切换物体时清除旧效果
*/
function onMouseMove(event: MouseEvent) {
// ========== 计算归一化鼠标坐标 ==========
// 获取Canvas的屏幕位置和尺寸
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
// 将鼠标坐标转换为归一化设备坐标(-1到1
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 // X左-1右1
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 // Y上1下-1翻转
// ========== 射线检测 ==========
// 从相机通过鼠标位置发射射线
raycaster.setFromCamera(mouse, camera)
if (model) {
// 检测射线与模型的交互true=递归检测所有子对象)
const intersects = raycaster.intersectObject(model, true)
// ========== 处理悬停物体 ==========
if (intersects.length > 0 && intersects[0]) {
const object = intersects[0].object as THREE.Mesh
// 如果是新的悬停物体
// 如果是新的悬停物体(避免重复处理)
if (hoveredObject.value !== object) {
hoveredObject.value = object
// 添加轮廓描边
// 添加绿色轮廓描边效果
outlinePass.selectedObjects = [object]
// 添加辅助坐标线
removeAxesHelper()
const box = new THREE.Box3().setFromObject(object)
const center = box.getCenter(new THREE.Vector3())
axesHelper = new THREE.AxesHelper(2)
axesHelper.position.copy(center)
// 添加坐标轴辅助线到物体中心
removeAxesHelper() // 先移除旧的
const box = new THREE.Box3().setFromObject(object) // 计算包围盒
const center = box.getCenter(new THREE.Vector3()) // 获取中心点
axesHelper = new THREE.AxesHelper(2) // 创建2单位长度的坐标轴
axesHelper.position.copy(center) // 定位到物体中心
scene.add(axesHelper)
}
}
else {
// 鼠标没有悬停在物体上
// ========== 鼠标离开物体 ==========
// 如果之前有悬停物体,且该物体不是当前选中的物体,则清除悬停效果
if (hoveredObject.value && hoveredObject.value !== selectedObject.value?.mesh) {
clearHoverEffects()
}
@@ -222,106 +319,154 @@ function onMouseMove(event: MouseEvent) {
}
}
// 画布点击事件
/**
* 画布点击事件处理
* 实现物体选中和信息展示
*
* 功能:
* 1. 检测点击的物体
* 2. 提取物体信息(名称、材质、坐标)
* 3. 显示信息标签
* 4. 保持轮廓描边和坐标轴
* 5. 点击空白处取消选择
*/
function onCanvasClick(event: MouseEvent) {
// ========== 计算归一化鼠标坐标 ==========
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
// ========== 射线检测 ==========
raycaster.setFromCamera(mouse, camera)
if (model) {
// 检测射线与模型的交互
const intersects = raycaster.intersectObject(model, true)
if (intersects.length > 0 && intersects[0]) {
// ========== 点击到物体 ==========
const object = intersects[0].object as THREE.Mesh
const point = intersects[0].point
const point = intersects[0].point // 点击的3D空间坐标
// 取材质信息
// 取材质信息
let materialName = '未知材质'
if (object.material) {
// 处理多材质情况
if (Array.isArray(object.material)) {
materialName = object.material.map((m: THREE.Material) => m.type).join(', ')
}
else {
// 单一材质
materialName = object.material.type
}
}
// 设置选中物体
// 设置选中物体信息触发UI标签显示
selectedObject.value = {
name: object.userData.objectName || object.name || '未命名物体',
material: materialName,
position: point.clone(),
mesh: object,
name: object.userData.objectName || object.name || '未命名物体', // 物体名称
material: materialName, // 材质类型
position: point.clone(), // 点击位置(用于标签定位)
mesh: object, // 物体引用
}
// 保持轮廓描边和坐标线
// 保持视觉效果(轮廓描边和坐标轴)
hoveredObject.value = object
outlinePass.selectedObjects = [object]
// 计算并更新标签位置
updateLabelPosition()
}
else {
// 点击空白处,清除所有效果
// ========== 点击空白处 ==========
// 清除所有选中和悬停效果
clearAllEffects()
}
}
}
// 清除悬停效果
/**
* 清除鼠标悬停效果
* 当鼠标离开物体时调用
* 注意:如果物体是当前选中的,不清除效果(保持选中状态)
*/
function clearHoverEffects() {
hoveredObject.value = null
// 只有在没有选中物体的情况下才清除视觉效果
if (!selectedObject.value) {
outlinePass.selectedObjects = []
removeAxesHelper()
outlinePass.selectedObjects = [] // 清除轮廓描边
removeAxesHelper() // 移除坐标轴
}
}
// 清除所有效果
/**
* 清除所有交互效果
* 当点击空白处时调用
* 清除悬停、选中、轮廓描边、坐标轴和信息标签
*/
function clearAllEffects() {
hoveredObject.value = null
selectedObject.value = null
outlinePass.selectedObjects = []
removeAxesHelper()
hoveredObject.value = null // 清除悬停物体
selectedObject.value = null // 清除选中物体(隐藏信息标签)
outlinePass.selectedObjects = [] // 清除轮廓描边
removeAxesHelper() // 移除坐标轴
}
// 移除辅助坐标线
/**
* 移除辅助坐标轴
* 从场景中移除坐标轴辅助线,并清空引用
*/
function removeAxesHelper() {
if (axesHelper) {
scene.remove(axesHelper)
axesHelper = null
scene.remove(axesHelper) // 从场景移除
axesHelper = null // 清空引用
}
}
/**
* 窗口大小变化处理
* 更新相机宽高比、渲染器尺寸和后期处理合成器尺寸
*/
function onResize() {
if (!threeRef.value)
return
const width = threeRef.value.clientWidth
const height = threeRef.value.clientHeight
// 更新相机宽高比
camera.aspect = width / height
camera.updateProjectionMatrix()
camera.updateProjectionMatrix() // 更新投影矩阵
// 更新渲染器和后期处理合成器尺寸
renderer.setSize(width, height)
composer.setSize(width, height)
}
// ========== Vue生命周期 ==========
// 组件挂载时初始化场景
onMounted(init)
// 组件卸载时清理资源,防止内存泄漏
onUnmounted(() => {
cancelAnimationFrame(animationId)
window.removeEventListener('resize', onResize)
cancelAnimationFrame(animationId) // 取消动画帧
window.removeEventListener('resize', onResize) // 移除窗口事件监听
// 移除Canvas事件监听
if (renderer.domElement) {
renderer.domElement.removeEventListener('mousemove', onMouseMove)
renderer.domElement.removeEventListener('click', onCanvasClick)
}
renderer.dispose()
composer.dispose()
// 释放WebGL资源
renderer.dispose() // 释放渲染器资源
composer.dispose() // 释放后期处理资源
})
</script>
<template>
<!-- Three.js渲染容器占满整个视口 -->
<div ref="threeRef" class="w-full h-full" />
<!-- 物体信息标签 -->
<!-- 物体信息标签显示选中物体的详细信息 -->
<!-- 浮动信息标签跟随选中物体显示动态定位 -->
<div
v-if="selectedObject"
class="object-label"
@@ -331,13 +476,16 @@ onUnmounted(() => {
}"
>
<div class="label-content">
<!-- 物体名称 -->
<div class="label-title">
{{ selectedObject.name }}
</div>
<!-- 材质类型 -->
<div class="label-item">
<span class="label-key">材质</span>
<span class="label-value">{{ selectedObject.material }}</span>
</div>
<!-- 点击位置的3D坐标 -->
<div class="label-item">
<span class="label-key">坐标</span>
<span class="label-value">
@@ -349,10 +497,12 @@ onUnmounted(() => {
</div>
</div>
<!-- 导航链接跳转到下一个示例 -->
<router-link to="/three/11" class="position-fixed left-20px top-20px link-btn">
后期处理效果
</router-link>
<!-- 交互说明面板显示操作提示 -->
<div class="position-fixed right-20px top-20px info-panel">
<div class="info-text">
鼠标移入物体显示轮廓和坐标轴

View File

@@ -1,89 +1,133 @@
<!--
* @Description: 后期处理效果
* @Description: 后期处理效果示例 - 演示 Three.js 中的各种后期处理技术
* @Autor: 相卿
* @Date: 2025-11-17 16:07:33
* @LastEditors: 相卿
* @LastEditTime: 2025-11-21 09:43:16
*
* 本示例展示了如何在 Three.js 中使用后期处理Post-processing技术
* 包含多种内置和自定义的效果通道Pass可以组合使用
-->
<script lang="ts" setup>
// Three.js 核心库
import * as THREE from 'three'
// 轨道控制器 - 用于相机的交互式控制
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// GLTF 模型加载器 - 用于加载 3D 模型文件
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
// 点阵效果 - 产生类似印刷品的点阵图案
import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'
// 效果合成器 - 管理和组合多个后期处理通道
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
// 故障效果 - 产生数字故障、屏幕撕裂的视觉效果
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'
// 渲染通道 - 基础渲染层,将场景渲染到纹理
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
// 着色器通道 - 用于应用自定义着色器效果
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
// 抗锯齿通道 - 使用 SMAA 算法平滑边缘
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'
// 辉光效果 - 为明亮物体添加发光效果
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
// Vue 3 组合式 API
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
// DOM 引用 - Three.js 渲染容器
const threeRef = shallowRef<HTMLDivElement>()
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let animationId: number
let model: THREE.Group
// Three.js 核心对象声明
let scene: THREE.Scene // 场景 - 所有 3D 对象的容器
let camera: THREE.PerspectiveCamera // 透视相机 - 模拟人眼视角
let renderer: THREE.WebGLRenderer // WebGL 渲染器 - 负责渲染场景
let controls: OrbitControls // 轨道控制器 - 处理相机交互
let animationId: number // 动画帧 ID - 用于取消动画循环
let model: THREE.Group // 3D 模型组 - 存储加载的 GLTF 模型
// 后期处理相关
let composer: EffectComposer
let glitchPass: GlitchPass
let smaaPass: SMAAPass
let dotScreenPass: DotScreenPass
let bloomPass: UnrealBloomPass
let flickerPass: ShaderPass
let waterPass: ShaderPass
// 后期处理相关对象
let composer: EffectComposer // 效果合成器 - 管理所有后期处理通道
let glitchPass: GlitchPass // 故障效果通道
let smaaPass: SMAAPass // 抗锯齿通道
let dotScreenPass: DotScreenPass // 点阵效果通道
let bloomPass: UnrealBloomPass // 辉光效果通道
let flickerPass: ShaderPass // 闪烁效果通道(自定义)
let waterPass: ShaderPass // 水波效果通道(自定义)
// 控制面板状态
// ============================================================
// 控制面板状态管理
// ============================================================
// 使用响应式对象管理所有后期处理效果的开关和参数
const effects = ref({
glitch: false,
antialiasing: false,
dotScreen: false,
bloom: false,
bloomStrength: 1.5,
flicker: false,
water: false,
glitch: false, // 故障效果开关
antialiasing: false, // 抗锯齿开关
dotScreen: false, // 点阵效果开关
bloom: false, // 辉光效果开关
bloomStrength: 1.5, // 辉光强度0-3
flicker: false, // 闪烁效果开关
water: false, // 水波效果开关
})
// 自定义着色器 - 屏幕闪动效果
// ============================================================
// 自定义着色器 1屏幕闪烁效果
// ============================================================
// 模拟老式显示器或荧光灯管的闪烁效果
const FlickerShader = {
// 着色器统一变量Uniforms- 从 JavaScript 传递给着色器的值
uniforms: {
tDiffuse: { value: null },
amount: { value: 0.5 },
time: { value: 0 },
tDiffuse: { value: null }, // 输入纹理(上一个通道的渲染结果)
amount: { value: 0.5 }, // 闪烁强度0-1
time: { value: 0 }, // 时间变量,用于动画
},
// 顶点着色器 - 处理每个顶点的位置
vertexShader: `
varying vec2 vUv;
varying vec2 vUv; // 传递给片段着色器的 UV 坐标
void main() {
vUv = uv;
vUv = uv; // 将 UV 坐标传递到片段着色器
// 计算顶点的最终位置
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
// 片段着色器 - 处理每个像素的颜色
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float amount;
uniform float time;
varying vec2 vUv;
uniform sampler2D tDiffuse; // 输入纹理采样器
uniform float amount; // 闪烁强度
uniform float time; // 时间
varying vec2 vUv; // 从顶点着色器接收的 UV 坐标
void main() {
// 从输入纹理中采样颜色
vec4 color = texture2D(tDiffuse, vUv);
// 使用正弦函数生成周期性闪烁
// sin(time * 10.0): 快速振荡(频率 10
// * 0.5 + 0.5: 将范围从 [-1,1] 转换为 [0,1]
float flicker = sin(time * 10.0) * 0.5 + 0.5;
// 对闪烁值进行三次方运算,使效果更加明显
flicker = pow(flicker, 3.0);
// 根据闪烁值降低颜色亮度
color.rgb *= 1.0 - (flicker * amount * 0.3);
gl_FragColor = color;
}
`,
}
// 自定义着色器 - 水底波浪效果
// ============================================================
// 自定义着色器 2水底波浪效果
// ============================================================
// 模拟水下观察的动态扭曲效果
const WaterShader = {
// 着色器统一变量
uniforms: {
tDiffuse: { value: null },
time: { value: 0 },
distortion: { value: 0.5 },
tDiffuse: { value: null }, // 输入纹理
time: { value: 0 }, // 时间变量
distortion: { value: 0.5 }, // 扭曲强度0-1
},
// 顶点着色器
vertexShader: `
varying vec2 vUv;
void main() {
@@ -91,6 +135,8 @@ const WaterShader = {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
// 片段着色器
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float time;
@@ -98,183 +144,252 @@ const WaterShader = {
varying vec2 vUv;
void main() {
vec2 uv = vUv;
vec2 uv = vUv; // 复制 UV 坐标以便修改
// 创建多层波浪效果
// 创建多层波浪效果,模拟真实水波的复杂性
// 波浪 1水平方向的快速波动
float wave1 = sin(uv.x * 10.0 + time) * 0.01;
// 波浪 2垂直方向的波动速度和方向不同
float wave2 = sin(uv.y * 15.0 - time * 1.5) * 0.01;
// 波浪 3对角线方向的慢速波动增加复杂度
float wave3 = sin((uv.x + uv.y) * 8.0 + time * 0.5) * 0.005;
// 将波浪效果应用到 UV 坐标上,产生扭曲
uv.x += (wave1 + wave3) * distortion;
uv.y += (wave2 + wave3) * distortion;
// 添加水下的颜色调整
// 使用扭曲后的 UV 坐标采样纹理
vec4 color = texture2D(tDiffuse, uv);
color.rgb *= vec3(0.8, 0.95, 1.1); // 蓝绿色调
// 添加水下的颜色调整 - 蓝绿色调
// R: 0.8减少红色G: 0.95略减绿色B: 1.1(增加蓝色)
color.rgb *= vec3(0.8, 0.95, 1.1);
gl_FragColor = color;
}
`,
}
// ============================================================
// 初始化函数 - 设置整个 Three.js 场景
// ============================================================
function init() {
// 获取容器尺寸
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
// 创建场景 - 所有 3D 对象的容器
scene = new THREE.Scene()
scene.background = new THREE.Color(0x1A1A2E)
scene.background = new THREE.Color(0x1A1A2E) // 设置深蓝灰色背景
// 创建透视相机
// 参数:视野角度(75°), 宽高比, 近裁剪面(0.1), 远裁剪面(1000)
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 5, 15)
camera.position.set(0, 5, 15) // 设置相机位置
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
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 // 使用柔和阴影
threeRef.value!.appendChild(renderer.domElement) // 将渲染器的 canvas 添加到 DOM
// 创建轨道控制器 - 允许用户通过鼠标控制相机
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.enableDamping = true // 启用阻尼(惯性)
controls.dampingFactor = 0.05 // 阻尼系数
setupLighting()
setupGround()
setupPostProcessing()
loadModel()
// 调用各个初始化子函数
setupLighting() // 设置灯光
setupGround() // 设置地面
setupPostProcessing() // 设置后期处理
loadModel() // 加载 3D 模型
// 监听窗口大小变化
window.addEventListener('resize', onResize)
// 启动动画循环
animate()
}
// ============================================================
// 灯光设置函数
// ============================================================
function setupLighting() {
// 环境光
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.0)
// 1. 环境光 - 为整个场景提供均匀的基础照明
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.0) // 白色,强度 1.0
scene.add(ambientLight)
// 主方向光
const mainLight = new THREE.DirectionalLight(0xFFFFFF, 1.5)
mainLight.position.set(10, 15, 10)
mainLight.castShadow = true
// 2. 主方向光 - 模拟太阳光,产生阴影
const mainLight = new THREE.DirectionalLight(0xFFFFFF, 1.5) // 白色,强度 1.5
mainLight.position.set(10, 15, 10) // 从右上方照射
mainLight.castShadow = true // 启用投射阴影
// 设置阴影贴图分辨率(越高越清晰,但性能消耗越大)
mainLight.shadow.mapSize.width = 2048
mainLight.shadow.mapSize.height = 2048
scene.add(mainLight)
// 辅助光
const fillLight = new THREE.DirectionalLight(0x4488FF, 0.8)
fillLight.position.set(-10, 10, -10)
// 3. 辅助光(补光)- 从另一侧照亮,避免阴影过暗
const fillLight = new THREE.DirectionalLight(0x4488FF, 0.8) // 蓝色调,强度 0.8
fillLight.position.set(-10, 10, -10) // 从左上后方照射
scene.add(fillLight)
// 添加发光球体(用于演示光效果)
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32)
// 4. 添加发光球体(用于演示光效果)
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32) // 半径 0.532段精度
const emissiveMaterial = new THREE.MeshStandardMaterial({
color: 0x00FFFF,
emissive: 0x00FFFF,
emissiveIntensity: 2,
color: 0x00FFFF, // 青色
emissive: 0x00FFFF, // 自发光颜色(青色)
emissiveIntensity: 2, // 自发光强度
})
// 在四个角落放置发光球体
const positions = [
{ x: -8, y: 3, z: -8 },
{ x: 8, y: 3, z: -8 },
{ x: -8, y: 3, z: 8 },
{ x: 8, y: 3, z: 8 },
{ x: -8, y: 3, z: -8 }, // 左前角
{ x: 8, y: 3, z: -8 }, // 右前角
{ x: -8, y: 3, z: 8 }, // 左后角
{ x: 8, y: 3, z: 8 }, // 右后角
]
positions.forEach((pos) => {
// 创建发光球体
const sphere = new THREE.Mesh(sphereGeometry, emissiveMaterial.clone())
sphere.position.set(pos.x, pos.y, pos.z)
scene.add(sphere)
// 添加点光源
const pointLight = new THREE.PointLight(0x00FFFF, 2, 20)
// 在球体位置添加点光源,增强照明效果
const pointLight = new THREE.PointLight(0x00FFFF, 2, 20) // 青色强度2范围20
pointLight.position.copy(sphere.position)
scene.add(pointLight)
})
}
// ============================================================
// 地面设置函数
// ============================================================
function setupGround() {
// 创建地面几何体 - 100x100 的平面
const groundGeometry = new THREE.PlaneGeometry(100, 100)
// 创建地面材质 - 使用标准材质以支持光照和阴影
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x333333,
roughness: 0.8,
metalness: 0.2,
color: 0x333333, // 深灰色
roughness: 0.8, // 粗糙度0=光滑镜面1=完全粗糙)
metalness: 0.2, // 金属度0=非金属1=金属)
})
// 创建地面网格
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.add(ground)
// 网格辅助线
// 添加网格辅助线 - 帮助观察空间和距离
// 参数:大小(100), 分割数(50), 中心线颜色, 网格线颜色
const gridHelper = new THREE.GridHelper(100, 50, 0x555555, 0x333333)
scene.add(gridHelper)
}
// ============================================================
// 后期处理设置函数 - 这是本示例的核心
// ============================================================
function setupPostProcessing() {
// 创建效果合成器 - 管理所有后期处理通道
// 它会将多个处理通道Pass串联起来形成处理管线
composer = new EffectComposer(renderer)
// 基础渲染通道
// 1. 添加基础渲染通道 - 必须是第一个通道
// 它将场景渲染到纹理上,供后续通道使用
const renderPass = new RenderPass(scene, camera)
composer.addPass(renderPass)
// 1. 故障效果
// 2. 故障效果通道 (Glitch Pass)
// 产生数字故障、屏幕撕裂、RGB 分离等电子干扰效果
glitchPass = new GlitchPass()
glitchPass.enabled = effects.value.glitch
glitchPass.enabled = effects.value.glitch // 根据控制面板状态启用/禁用
composer.addPass(glitchPass)
// 2. 抗锯齿
// 3. 抗锯齿通道 (SMAA Pass)
// 使用 SMAASubpixel Morphological Anti-Aliasing算法
// 平滑边缘,减少锯齿,提升画面质量
smaaPass = new SMAAPass()
smaaPass.enabled = effects.value.antialiasing
composer.addPass(smaaPass)
// 3. 点效果
// 4. 点效果通道 (Dot Screen Pass)
// 将画面转换为点阵图案,模拟印刷品或老式显示器效果
dotScreenPass = new DotScreenPass()
dotScreenPass.enabled = effects.value.dotScreen
composer.addPass(dotScreenPass)
// 4. 光效果
// 5. 光效果通道 (Unreal Bloom Pass)
// 为明亮的物体添加发光效果,常用于表现霓虹灯、激光等
bloomPass = new UnrealBloomPass(
// 参数1渲染分辨率
new THREE.Vector2(threeRef.value!.clientWidth, threeRef.value!.clientHeight),
effects.value.bloomStrength,
0.4,
0.85,
effects.value.bloomStrength, // 参数2强度0-3
0.4, // 参数3半径发光扩散距离
0.85, // 参数4阈值多亮的物体开始发光0-1
)
bloomPass.enabled = effects.value.bloom
composer.addPass(bloomPass)
// 5. 屏幕闪效果
// 6. 屏幕闪效果通道(自定义)
// 使用自定义的 FlickerShader 创建老旧屏幕闪烁效果
flickerPass = new ShaderPass(FlickerShader)
flickerPass.enabled = effects.value.flicker
composer.addPass(flickerPass)
// 6. 水底波浪效果
// 7. 水底波浪效果通道(自定义)
// 使用自定义的 WaterShader 创建水下观察的扭曲效果
waterPass = new ShaderPass(WaterShader)
waterPass.enabled = effects.value.water
composer.addPass(waterPass)
}
// ============================================================
// 模型加载函数
// ============================================================
function loadModel() {
// 创建 GLTF 加载器实例
const loader = new GLTFLoader()
// 加载模型文件
// 参数1模型文件路径位于 public 目录)
// 参数2加载成功的回调函数
loader.load('/mzjc_bansw.glb', (gltf) => {
model = gltf.scene
model = gltf.scene // 获取模型场景
// 遍历模型中的所有子对象
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = true
child.receiveShadow = true
child.castShadow = true // 投射阴影
child.receiveShadow = true // 接收阴影
}
})
scene.add(model)
scene.add(model) // 将模型添加到场景中
})
}
// ============================================================
// 动画循环函数 - 每帧执行一次
// ============================================================
function animate() {
// 请求下一帧动画(递归调用)
animationId = requestAnimationFrame(animate)
// 更新轨道控制器(处理阻尼效果)
controls.update()
// 获取当前时间(秒)
const time = performance.now() * 0.001
// 更新自定义着色器的时间
// 更新自定义着色器的时间统一变量
// 这样着色器就能产生随时间变化的动画效果
if (flickerPass.uniforms.time) {
flickerPass.uniforms.time.value = time
}
@@ -282,62 +397,95 @@ function animate() {
waterPass.uniforms.time.value = time
}
// 让装饰球体浮动
// 让装饰球体浮动 - 遍历场景中的所有对象
scene.traverse((object) => {
// 检查是否是具有自发光材质的网格(即我们的发光球体)
if (object instanceof THREE.Mesh && object.material.emissive) {
// 使用正弦函数创建上下浮动效果
// 每个球体根据其 x 位置有不同的相位,产生波浪效果
object.position.y = 3 + Math.sin(time + object.position.x) * 0.5
}
})
// 使用合成器渲染(而不是直接使用 renderer.render
// 这样会依次执行所有后期处理通道
composer.render()
}
// ============================================================
// 窗口大小调整处理函数
// ============================================================
function onResize() {
if (!threeRef.value)
return
// 获取新的容器尺寸
const width = threeRef.value.clientWidth
const height = threeRef.value.clientHeight
// 更新相机宽高比
camera.aspect = width / height
camera.updateProjectionMatrix()
camera.updateProjectionMatrix() // 必须调用此方法使更改生效
// 更新渲染器尺寸
renderer.setSize(width, height)
// 更新合成器尺寸(重要!否则后期处理效果会变形)
composer.setSize(width, height)
}
// 监听效果参数变化
// ============================================================
// 响应式监听 - 监听效果参数变化
// ============================================================
// 当用户在控制面板切换效果时,动态启用/禁用对应的通道
// 监听故障效果开关
watch(() => effects.value.glitch, (value) => {
glitchPass.enabled = value
})
// 监听抗锯齿开关
watch(() => effects.value.antialiasing, (value) => {
smaaPass.enabled = value
})
// 监听点阵效果开关
watch(() => effects.value.dotScreen, (value) => {
dotScreenPass.enabled = value
})
// 监听辉光效果开关
watch(() => effects.value.bloom, (value) => {
bloomPass.enabled = value
})
// 监听辉光强度变化
watch(() => effects.value.bloomStrength, (value) => {
bloomPass.strength = value
bloomPass.strength = value // 实时更新强度参数
})
// 监听闪烁效果开关
watch(() => effects.value.flicker, (value) => {
flickerPass.enabled = value
})
// 监听水波效果开关
watch(() => effects.value.water, (value) => {
waterPass.enabled = value
})
// ============================================================
// 生命周期钩子
// ============================================================
// 组件挂载时初始化 Three.js 场景
onMounted(init)
// 组件卸载时清理资源,防止内存泄漏
onUnmounted(() => {
cancelAnimationFrame(animationId)
window.removeEventListener('resize', onResize)
renderer.dispose()
composer.dispose()
cancelAnimationFrame(animationId) // 停止动画循环
window.removeEventListener('resize', onResize) // 移除事件监听
renderer.dispose() // 释放渲染器资源
composer.dispose() // 释放合成器资源
})
</script>

View File

@@ -6,117 +6,141 @@
* @LastEditTime: 2025-11-20 17:28:46
-->
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
/**
* 核心库导入
*/
import * as THREE from 'three' // Three.js 核心库
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' // 轨道控制器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' // GLTF模型加载器
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>()
/**
* Three.js 基础对象引用
*/
const threeRef = shallowRef<HTMLDivElement>() // Three.js 容器DOM引用
const camera = shallowRef<THREE.PerspectiveCamera>() // 透视相机
const scene = shallowRef<THREE.Scene>() // 3D场景
const controls = shallowRef<OrbitControls>() // 轨道控制器
const renderer = shallowRef<THREE.WebGLRenderer>() // WebGL渲染器
// 告警相关
const alertMesh = shallowRef<THREE.Mesh>()
const originalMaterial = shallowRef<THREE.Material>()
const alertOverlay = shallowRef<THREE.Mesh>() // 告警覆盖层
const isAlerting = ref(false)
let alertTime = 0
/**
* 设备告警相关变量
* 用于实现设备报警时的视觉反馈和相机聚焦效果
*/
const alertMesh = shallowRef<THREE.Mesh>() // 告警设备网格对象 (fmz_2_jzf_bs015)
const originalMaterial = shallowRef<THREE.Material>() // 原始材质备份(预留,当前未使用)
const alertOverlay = shallowRef<THREE.Mesh>() // 红色半透明告警覆盖层
const isAlerting = ref(false) // 告警状态标志
let alertTime = 0 // 告警动画计时器,用于控制闪烁效果
// 相机动画相关
let cameraAnimating = false
let cameraAnimationProgress = 0
const cameraStartPosition = new THREE.Vector3()
const cameraStartTarget = new THREE.Vector3()
const cameraEndPosition = new THREE.Vector3()
const cameraEndTarget = new THREE.Vector3()
/**
* 相机动画相关变量
* 用于实现相机平滑移动到告警设备的动画效果
*/
let cameraAnimating = false // 相机动画进行标志
let cameraAnimationProgress = 0 // 动画进度 (0-1)
const cameraStartPosition = new THREE.Vector3() // 相机起始位置
const cameraStartTarget = new THREE.Vector3() // 相机起始观察目标
const cameraEndPosition = new THREE.Vector3() // 相机结束位置
const cameraEndTarget = new THREE.Vector3() // 相机结束观察目标
/**
* 初始化Three.js场景、相机、渲染器、灯光和模型
* 这是整个3D场景的核心初始化函数
*/
function initModel() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
// 创建场景
// ========== 场景设置 ==========
// 创建3D场景深蓝紫色背景添加雾效增强深度感
scene.value = new THREE.Scene()
scene.value.background = new THREE.Color(0x1A1A2E)
scene.value.fog = new THREE.Fog(0x1A1A2E, 10, 50)
scene.value.background = new THREE.Color(0x1A1A2E) // 深蓝紫色背景
scene.value.fog = new THREE.Fog(0x1A1A2E, 10, 50) // 线性雾效10米开始50米完全遮蔽
// 坐标轴辅助器 (红X 绿Y 蓝Z)
const axesHelper = new THREE.AxesHelper(5)
scene.value.add(axesHelper)
// 创建透视相机
// ========== 相机设置 ==========
// 创建透视相机: 视野60度初始位置在斜上方
camera.value = new THREE.PerspectiveCamera(
60,
width / height,
0.1,
1000,
60, // 视野角度
width / height, // 宽高比
0.1, // 近裁剪面
1000, // 远裁剪面
)
camera.value.position.set(0, 5, 15)
camera.value.position.set(0, 5, 15) // 初始相机位置
// 创建渲染器
// ========== 渲染器设置 ==========
// 创建WebGL渲染器启用抗锯齿和阴影效果
renderer.value = new THREE.WebGLRenderer({ antialias: true })
renderer.value.setSize(width, height)
renderer.value.setPixelRatio(window.devicePixelRatio)
renderer.value.shadowMap.enabled = true
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap
renderer.value.setPixelRatio(window.devicePixelRatio) // 适配高分辨率屏幕
renderer.value.shadowMap.enabled = true // 启用阴影
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap // 柔和阴影
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.enableDamping = true // 启用阻尼(惯性)效果
controls.value.dampingFactor = 0.05 // 阻尼系数,值越小越平滑
controls.value.minDistance = 2 // 最小缩放距离
controls.value.maxDistance = 50 // 最大缩放距离
// 添加环境光
// ========== 灯光系统 ==========
// 环境光:提供基础照明,避免场景过暗
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.6)
scene.value.add(ambientLight)
// 添加主平行光
// 主平行光:模拟太阳光,产生阴影
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.2)
directionalLight.position.set(10, 15, 10)
directionalLight.castShadow = true
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)
scene.value.add(fillLight)
// 添加地面
// ========== 地面创建 ==========
// 创建100x100的灰色地面用于承载模型和接收阴影
const groundGeometry = new THREE.PlaneGeometry(100, 100)
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x555555,
roughness: 0.8,
metalness: 0.2,
color: 0x555555, // 中灰色
roughness: 0.8, // 较高粗糙度
metalness: 0.2, // 轻微金属感
})
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.receiveShadow = true
ground.receiveShadow = true // 接收阴影
scene.value.add(ground)
// 加载模型
// ========== GLTF模型加载 ==========
const loader = new GLTFLoader()
loader.load('/mzjc_bansw.glb', (gltf) => {
const model = gltf.scene
// 遍历模型,找到 fmz_2_jzf_bs015 并启用阴影
// 遍历模型所有子节点,配置阴影并查找告警设备
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = true
child.receiveShadow = true
child.castShadow = true // 投射阴影
child.receiveShadow = true // 接收阴影
// 找到告警设备
// 查找名为 'fmz_2_jzf_bs015' 的告警设备
if (child.name === 'fmz_2_jzf_bs015') {
alertMesh.value = child
// 保存原始材质
// 备份原始材质(预留功能,当前版本使用覆盖层方式)
originalMaterial.value = child.material.clone()
}
}
@@ -127,47 +151,57 @@ function initModel() {
console.error('模型加载失败:', error)
})
/**
* 动画循环函数
* 每帧执行的逻辑:告警闪烁、相机动画、渲染更新
*/
function animate() {
requestAnimationFrame(animate)
// 告警闪烁效果
// ========== 告警闪烁效果 ==========
// 使用正弦波函数实现红色覆盖层的透明度闪烁
if (isAlerting.value && alertOverlay.value) {
alertTime += 0.05
// 使用正弦波创建闪烁效果,频率较快
const intensity = (Math.sin(alertTime * 5) + 1) / 2 // 0-1之间震荡
alertTime += 0.05 // 累加时间
// sin(t*5) 产生快速震荡,映射到 0-1 范围
const intensity = (Math.sin(alertTime * 5) + 1) / 2
if (alertOverlay.value.material instanceof THREE.Material) {
const material = alertOverlay.value.material as THREE.MeshBasicMaterial
material.opacity = 0.3 + intensity * 0.4 // 透明度在0.3-0.7之间震荡
// 透明度在 0.3-0.7 之间震荡,产生闪烁效果
material.opacity = 0.3 + intensity * 0.4
}
}
// 相机动画
// ========== 相机平滑动画 ==========
// 当触发告警时,相机平滑移动到设备位置
if (cameraAnimating && camera.value && controls.value) {
cameraAnimationProgress += 0.008 // 控制移动速度
cameraAnimationProgress += 0.008 // 每帧增加进度(0.8%)
// 动画完成检测
if (cameraAnimationProgress >= 1) {
cameraAnimationProgress = 1
cameraAnimating = false
controls.value.enabled = true // 动画结束后恢复控制
controls.value.enabled = true // 恢复用户控制
}
// 使用缓动函数使运动更平滑
// 使用缓动函数计算插值系数,使运动先慢后快再慢
const t = easeInOutCubic(cameraAnimationProgress)
// 插值相机位置
// 线性插值相机位置(起始位置 → 目标位置)
camera.value.position.lerpVectors(cameraStartPosition, cameraEndPosition, t)
// 插值观察目标
// 线性插值相机观察目标(起始目标 → 设备位置)
const currentTarget = new THREE.Vector3()
currentTarget.lerpVectors(cameraStartTarget, cameraEndTarget, t)
controls.value.target.copy(currentTarget)
}
// 更新轨道控制器(处理阻尼效果)
if (controls.value) {
controls.value.update()
}
// 执行渲染
if (renderer.value && scene.value && camera.value) {
renderer.value.render(scene.value, camera.value)
}
@@ -195,13 +229,25 @@ function initModel() {
})
}
// 缓动函数
/**
* 三次缓动函数 (EaseInOutCubic)
* 先加速后减速的平滑过渡曲线
* @param t 进度值 (0-1)
* @returns 缓动后的值 (0-1)
*/
function easeInOutCubic(t: number): number {
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2
}
// 设备报警处理
/**
* 设备报警处理函数
* 核心功能:
* 1. 切换告警状态
* 2. 创建/移除红色闪烁覆盖层
* 3. 触发相机平滑移动到告警设备
*/
function handleAlert() {
// 前置检查:确保必要对象已初始化
if (!alertMesh.value || !camera.value || !controls.value) {
console.warn('告警设备或相机未就绪')
return
@@ -211,29 +257,28 @@ function handleAlert() {
isAlerting.value = !isAlerting.value
if (isAlerting.value) {
// 开始告警
alertTime = 0
// ========== 开始告警 ==========
alertTime = 0 // 重置闪烁计时器
// 创建半透明红色覆盖层
// ========== 创建红色告警覆盖层 ==========
// 克隆设备几何体,创建完全贴合的覆盖网格
const geometry = alertMesh.value.geometry.clone()
const overlayMaterial = new THREE.MeshBasicMaterial({
color: 0xFF0000,
transparent: true,
opacity: 0.2,
depthWrite: false,
side: THREE.DoubleSide,
color: 0xFF0000, // 红色
transparent: true, // 启用透明度
opacity: 0.2, // 初始透明度
depthWrite: false, // 关闭深度写入,避免遮挡问题
side: THREE.DoubleSide, // 双面渲染
})
alertOverlay.value = new THREE.Mesh(geometry, overlayMaterial)
// 复制告警物体的变换矩阵
// 完全复制告警物体的变换属性(位置、旋转、缩放)
alertOverlay.value.position.copy(alertMesh.value.position)
// alertOverlay.value.position.y -= 0.6
// alertOverlay.value.position.x += 0.6
// alertOverlay.value.position.z += 2
alertOverlay.value.rotation.copy(alertMesh.value.rotation)
alertOverlay.value.scale.copy(alertMesh.value.scale).multiplyScalar(1.01) // 稍微放大避免z-fighting
// 稍微放大1%避免 z-fighting(深度冲突闪烁)
alertOverlay.value.scale.copy(alertMesh.value.scale).multiplyScalar(1.01)
// 如果告警物体有父节点,添加到同一父节点
// 添加到与告警物体相同的父节点,保持层级关系
if (alertMesh.value.parent) {
alertMesh.value.parent.add(alertOverlay.value)
}
@@ -241,45 +286,50 @@ function handleAlert() {
scene.value?.add(alertOverlay.value)
}
// 计算告警设备的世界坐标
// ========== 计算相机动画参数 ==========
// 获取设备在世界坐标系中的绝对位置
const worldPosition = new THREE.Vector3()
alertMesh.value.getWorldPosition(worldPosition)
// 计算包围盒获取设备的大小
// 计算设备包围盒获取其尺寸
const box = new THREE.Box3().setFromObject(alertMesh.value)
const size = box.getSize(new THREE.Vector3())
const maxDim = Math.max(size.x, size.y, size.z)
const maxDim = Math.max(size.x, size.y, size.z) // 最大尺寸维度
// 根据设备大小计算合适的观察距离,使用较小的倍数以靠近物体
const distance = Math.max(maxDim * 1.5, 2) // 距离是设备最大尺寸的1.5倍,最小2单位
// 根据设备大小动态计算观察距离
// 距离 = 设备尺寸 × 1.5最小2单位
const distance = Math.max(maxDim * 1.5, 2)
// 计算观察方向 - 从设备的斜上前方观察,更接近物体
// 计算相机目标位置:从设备的斜上前方观察
// 方向向量 (-1.2, 1.2, -1.8) 表示左上前方
const direction = new THREE.Vector3(-1.2, 1.2, -1.8).normalize()
const targetPosition = worldPosition.clone().add(direction.multiplyScalar(distance))
// 保存当前相机状态
// 保存相机当前状态(动画起点)
cameraStartPosition.copy(camera.value.position)
cameraStartTarget.copy(controls.value.target)
// 设置目标状态 - 相机位置和观察目标
cameraEndPosition.copy(targetPosition)
cameraEndTarget.copy(worldPosition) // 直接看向设备的世界坐标
// 设置相机目标状态(动画终点)
cameraEndPosition.copy(targetPosition) // 相机移动到计算位置
cameraEndTarget.copy(worldPosition) // 相机看向设备中心
// 开始动画
// 启动相机动画
cameraAnimating = true
cameraAnimationProgress = 0
controls.value.enabled = false // 动画期间禁用控制
cameraAnimationProgress = 0 // 重置进度
controls.value.enabled = false // 动画期间禁用用户控制
}
else {
// 停止告警,移除覆盖层
// ========== 停止告警 ==========
// 清理告警覆盖层,释放内存
if (alertOverlay.value) {
alertOverlay.value.parent?.remove(alertOverlay.value)
alertOverlay.value.geometry.dispose()
alertOverlay.value.parent?.remove(alertOverlay.value) // 从场景中移除
alertOverlay.value.geometry.dispose() // 释放几何体内存
if (alertOverlay.value.material instanceof THREE.Material) {
alertOverlay.value.material.dispose()
alertOverlay.value.material.dispose() // 释放材质内存
}
alertOverlay.value = undefined
alertOverlay.value = undefined // 清空引用
}
// 注意:相机不会自动返回原位,用户可手动控制
}
}

View File

@@ -6,149 +6,257 @@
* @LastEditTime: 2025-11-21 09:04:18
-->
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
/**
* 核心库导入
*/
import * as THREE from 'three' // Three.js 核心库
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' // 轨道控制器
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
const threeRef = shallowRef<HTMLDivElement>()
const isEmitting = ref(false)
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let animationId: number
const particles: THREE.Sprite[] = []
let particleTexture: THREE.Texture
/**
* Three.js 基础对象引用
*/
const threeRef = shallowRef<HTMLDivElement>() // Three.js 容器DOM引用
const isEmitting = ref(false) // 粒子发射状态标志
// 场景核心对象使用let声明因为在init中初始化
let scene: THREE.Scene // 3D场景
let camera: THREE.PerspectiveCamera // 透视相机
let renderer: THREE.WebGLRenderer // WebGL渲染器
let controls: OrbitControls // 轨道控制器
let animationId: number // 动画帧ID用于取消动画
/**
* 粒子系统相关
*/
const particles: THREE.Sprite[] = [] // 粒子精灵数组,存储所有活跃的粒子
let particleTexture: THREE.Texture // 烟雾纹理,所有粒子共享
/**
* 初始化Three.js场景和粒子系统
* 创建场景、相机、渲染器、控制器、光照和粒子纹理
*/
function init() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
// ========== 场景设置 ==========
// 创建黑色背景的3D场景便于观察白色烟雾粒子
scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
scene.background = new THREE.Color(0x000000) // 黑色背景
// ========== 相机设置 ==========
// 创建透视相机视野角75度初始位置在斜上方
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 5, 10)
camera.position.set(0, 5, 10) // 从前上方观察原点
// ========== 渲染器设置 ==========
// 创建WebGL渲染器启用抗锯齿
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
threeRef.value!.appendChild(renderer.domElement)
// ========== 轨道控制器 ==========
// 允许用户通过鼠标控制视角
controls = new OrbitControls(camera, renderer.domElement)
// ========== 光照系统 ==========
// 添加环境光,提供基础照明
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5)
scene.add(ambientLight)
// ========== 创建粒子纹理 ==========
// 生成用于所有粒子的烟雾纹理(圆形径向渐变)
particleTexture = createSmokeTexture()
// 启动渲染循环
animate()
// 监听窗口大小变化
window.addEventListener('resize', onResize)
}
/**
* 创建烟雾纹理
* 使用Canvas 2D API生成径向渐变纹理模拟烟雾效果
* 纹理特点:
* - 中心不透明度高(白色)
* - 边缘完全透明
* - 形成柔和的圆形烟雾效果
*
* @returns THREE.Texture 烟雾纹理对象
*/
function createSmokeTexture() {
// 创建32x32的Canvas画布
const canvas = document.createElement('canvas')
canvas.width = 32
canvas.height = 32
const ctx = canvas.getContext('2d')!
const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16)
grad.addColorStop(0, 'rgba(255, 255, 255, 1)')
grad.addColorStop(1, 'rgba(255, 255, 255, 0)')
// 创建径向渐变:从中心(16,16)向外扩散
const grad = ctx.createRadialGradient(
16, 16, 0, // 内圆中心点半径0
16, 16, 16, // 外圆中心点半径16
)
grad.addColorStop(0, 'rgba(255, 255, 255, 1)') // 中心:不透明白色
grad.addColorStop(1, 'rgba(255, 255, 255, 0)') // 边缘:完全透明
// 填充渐变
ctx.fillStyle = grad
ctx.fillRect(0, 0, 32, 32)
// 将Canvas转换为Three.js纹理
const texture = new THREE.Texture(canvas)
texture.needsUpdate = true
texture.needsUpdate = true // 标记纹理需要上传到GPU
return texture
}
/**
* 切换粒子发射状态
* 功能:
* - 开启时:每帧创建新粒子
* - 关闭时:清空所有现有粒子
*/
function toggleParticle() {
isEmitting.value = !isEmitting.value
// 关闭粒子发射时,清空所有粒子
if (!isEmitting.value) {
particles.forEach(p => scene.remove(p))
particles.length = 0
particles.forEach(p => scene.remove(p)) // 从场景中移除
particles.length = 0 // 清空数组
}
}
/**
* 动画循环函数
* 每帧执行的逻辑:
* 1. 创建新粒子(如果发射开启)
* 2. 更新所有粒子的位置、大小、透明度
* 3. 移除生命周期结束的粒子
* 4. 渲染场景
*/
function animate() {
animationId = requestAnimationFrame(animate)
controls.update()
controls.update() // 更新轨道控制器
// ========== 粒子生成 ==========
// 如果发射开启,每帧创建一个新粒子
if (isEmitting.value) {
// Create new particle
// 创建粒子材质
const material = new THREE.SpriteMaterial({
map: particleTexture,
transparent: true,
opacity: 0.5,
color: 0xAAAAAA,
depthWrite: false, // Important for transparency
map: particleTexture, // 使用烟雾纹理
transparent: true, // 启用透明度
opacity: 0.5, // 初始透明度
color: 0xAAAAAA, // 浅灰色
depthWrite: false, // 关闭深度写入,避免粒子之间的遮挡问题
})
const particle = new THREE.Sprite(material)
particle.position.set(0, 0, 0)
particle.position.x = (Math.random() - 0.5) * 2
particle.position.z = (Math.random() - 0.5) * 2
// 创建精灵粒子始终面向相机的2D平面
const particle = new THREE.Sprite(material)
particle.position.set(0, 0, 0) // 初始位置在原点
// 在X和Z轴上随机偏移模拟从一个小区域发射
particle.position.x = (Math.random() - 0.5) * 2 // -1 到 1
particle.position.z = (Math.random() - 0.5) * 2 // -1 到 1
// 为粒子添加自定义属性:速度向量
;(particle as any).velocity = new THREE.Vector3(
(Math.random() - 0.5) * 0.05,
0.1 + Math.random() * 0.1,
(Math.random() - 0.5) * 0.05,
(Math.random() - 0.5) * 0.05, // X轴随机飘动-0.025 到 0.025
0.1 + Math.random() * 0.1, // Y轴向上运动0.1 到 0.2
(Math.random() - 0.5) * 0.05, // Z轴随机飘动-0.025 到 0.025
)
;(particle as any).age = 0
;(particle as any).age = 0 // 粒子年龄(帧数)
scene.add(particle)
particles.push(particle)
}
// ========== 粒子更新 ==========
// 从后向前遍历(便于删除元素)
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]
if (!p)
continue
const velocity = (p as any).velocity
// 更新位置:根据速度向量移动
p.position.add(velocity)
// 增加年龄
;(p as any).age++
// 更新大小:随年龄增长而扩大(模拟烟雾扩散)
// 大小 = 1 + 年龄 × 0.05
p.scale.setScalar(1 + (p as any).age * 0.05)
// 更新透明度:随年龄增长而降低(模拟烟雾消散)
// 透明度从0.5逐渐降到0
p.material.opacity = 0.5 - ((p as any).age / 100) * 0.5
// ========== 粒子生命周期结束 ==========
// 当粒子年龄超过100帧时移除并释放资源
if ((p as any).age > 100) {
scene.remove(p)
particles.splice(i, 1)
p.material.dispose() // Clean up
scene.remove(p) // 从场景移除
particles.splice(i, 1) // 从数组移除
p.material.dispose() // 释放材质内存
}
}
// 渲染场景
renderer.render(scene, camera)
}
/**
* 窗口大小变化处理
* 更新相机宽高比和渲染器尺寸
*/
function onResize() {
if (!threeRef.value)
return
const width = threeRef.value.clientWidth
const height = threeRef.value.clientHeight
// 更新相机宽高比
camera.aspect = width / height
camera.updateProjectionMatrix()
camera.updateProjectionMatrix() // 更新投影矩阵
// 更新渲染器尺寸
renderer.setSize(width, height)
}
// ========== Vue生命周期 ==========
// 组件挂载时初始化场景
onMounted(init)
// 组件卸载时清理资源,防止内存泄漏
onUnmounted(() => {
cancelAnimationFrame(animationId)
window.removeEventListener('resize', onResize)
renderer.dispose()
cancelAnimationFrame(animationId) // 取消动画帧
window.removeEventListener('resize', onResize) // 移除事件监听
renderer.dispose() // 释放渲染器资源
// 清理所有粒子
particles.forEach((p) => {
scene.remove(p)
p.material.dispose()
scene.remove(p) // 从场景移除
p.material.dispose() // 释放材质内存
})
})
</script>
<template>
<!-- Three.js 渲染容器占满整个视口 -->
<div ref="threeRef" class="w-full h-full" />
<!-- 导航链接跳转到下一个示例 -->
<router-link to="/three/10" class="position-fixed left-20px top-20px">
模型与Web的交互
</router-link>
<!-- 控制面板粒子发射开关 -->
<div class="position-fixed right-20px top-20px flex flex-col gap-12px">
<div class="flex gap-12px">
<!-- 切换按钮控制蒸汽粒子的生成和停止 -->
<button @click="toggleParticle">
{{ isEmitting ? '关闭' : '打开' }}粒子效果蒸汽
</button>