docs
This commit is contained in:
@@ -6,215 +6,312 @@
|
|||||||
* @LastEditTime: 2025-11-21 10:04:12
|
* @LastEditTime: 2025-11-21 10:04:12
|
||||||
-->
|
-->
|
||||||
<script lang="ts" setup>
|
<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 * as THREE from 'three' // Three.js 核心库
|
||||||
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' // 轨道控制器
|
||||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.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'
|
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
// 物体信息类型
|
/**
|
||||||
|
* 物体信息接口
|
||||||
|
* 存储被选中物体的详细信息,用于显示在UI标签中
|
||||||
|
*/
|
||||||
interface ObjectInfo {
|
interface ObjectInfo {
|
||||||
name: string
|
name: string // 物体名称
|
||||||
material: string
|
material: string // 材质类型
|
||||||
position: THREE.Vector3
|
position: THREE.Vector3 // 点击位置(世界坐标)
|
||||||
mesh: THREE.Mesh
|
mesh: THREE.Mesh // 物体网格引用
|
||||||
}
|
}
|
||||||
|
|
||||||
const threeRef = shallowRef<HTMLDivElement>()
|
/**
|
||||||
|
* Three.js 基础对象引用
|
||||||
|
*/
|
||||||
|
const threeRef = shallowRef<HTMLDivElement>() // Three.js 容器DOM引用
|
||||||
|
|
||||||
let scene: THREE.Scene
|
// 场景核心对象(使用let声明,在init中初始化)
|
||||||
let camera: THREE.PerspectiveCamera
|
let scene: THREE.Scene // 3D场景
|
||||||
let renderer: THREE.WebGLRenderer
|
let camera: THREE.PerspectiveCamera // 透视相机
|
||||||
let controls: OrbitControls
|
let renderer: THREE.WebGLRenderer // WebGL渲染器
|
||||||
let animationId: number
|
let controls: OrbitControls // 轨道控制器
|
||||||
let raycaster: THREE.Raycaster
|
let animationId: number // 动画帧ID,用于取消动画
|
||||||
let mouse: THREE.Vector2
|
|
||||||
let model: THREE.Group
|
|
||||||
|
|
||||||
// 后期处理相关
|
/**
|
||||||
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() {
|
function init() {
|
||||||
const width = threeRef.value!.clientWidth
|
const width = threeRef.value!.clientWidth
|
||||||
const height = threeRef.value!.clientHeight
|
const height = threeRef.value!.clientHeight
|
||||||
|
|
||||||
|
// ========== 场景设置 ==========
|
||||||
|
// 创建深蓝紫色背景的3D场景
|
||||||
scene = new THREE.Scene()
|
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 = 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 = new THREE.WebGLRenderer({ antialias: true })
|
||||||
renderer.setSize(width, height)
|
renderer.setSize(width, height)
|
||||||
renderer.setPixelRatio(window.devicePixelRatio)
|
renderer.setPixelRatio(window.devicePixelRatio) // 适配高分辨率屏幕
|
||||||
threeRef.value!.appendChild(renderer.domElement)
|
threeRef.value!.appendChild(renderer.domElement)
|
||||||
|
|
||||||
|
// ========== 轨道控制器设置 ==========
|
||||||
|
// 允许用户通过鼠标控制视角,启用阻尼效果
|
||||||
controls = new OrbitControls(camera, renderer.domElement)
|
controls = new OrbitControls(camera, renderer.domElement)
|
||||||
controls.enableDamping = true
|
controls.enableDamping = true // 启用阻尼(惯性)
|
||||||
controls.dampingFactor = 0.05
|
controls.dampingFactor = 0.05 // 阻尼系数,值越小越平滑
|
||||||
|
|
||||||
// 射线检测器
|
// ========== 射线检测器初始化 ==========
|
||||||
raycaster = new THREE.Raycaster()
|
// 用于检测鼠标与3D物体的交互
|
||||||
mouse = new THREE.Vector2()
|
raycaster = new THREE.Raycaster() // 从相机发射射线
|
||||||
|
mouse = new THREE.Vector2() // 归一化鼠标坐标(-1到1)
|
||||||
|
|
||||||
// 添加灯光 - 增强照明
|
// ========== 光照系统 ==========
|
||||||
// 环境光 - 提供基础亮度
|
// 多光源设置,确保模型各个角度都有良好的照明
|
||||||
|
|
||||||
|
// 环境光:提供基础亮度,避免场景过暗
|
||||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 5)
|
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 5)
|
||||||
scene.add(ambientLight)
|
scene.add(ambientLight)
|
||||||
|
|
||||||
// 主方向光 - 从右上方照射
|
// 主方向光:模拟太阳光,从右上方照射,产生阴影
|
||||||
const mainLight = new THREE.DirectionalLight(0xFFFFFF, 1.5)
|
const mainLight = new THREE.DirectionalLight(0xFFFFFF, 1.5)
|
||||||
mainLight.position.set(10, 15, 10)
|
mainLight.position.set(10, 15, 10) // 右上方
|
||||||
mainLight.castShadow = true
|
mainLight.castShadow = true // 投射阴影
|
||||||
scene.add(mainLight)
|
scene.add(mainLight)
|
||||||
|
|
||||||
// 辅助方向光 - 从左侧照射,补充阴影区域
|
// 辅助方向光:从左侧补光,减少阴影区域的暗度
|
||||||
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.8)
|
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.8)
|
||||||
fillLight.position.set(-10, 10, -10)
|
fillLight.position.set(-10, 10, -10) // 左上方
|
||||||
scene.add(fillLight)
|
scene.add(fillLight)
|
||||||
|
|
||||||
// 背光 - 从后方照射,增加轮廓感
|
// 背光:从后方照射,增强物体轮廓感
|
||||||
const backLight = new THREE.DirectionalLight(0xFFFFFF, 0.6)
|
const backLight = new THREE.DirectionalLight(0xFFFFFF, 0.6)
|
||||||
backLight.position.set(0, 5, -15)
|
backLight.position.set(0, 5, -15) // 后方
|
||||||
scene.add(backLight)
|
scene.add(backLight)
|
||||||
|
|
||||||
// 点光源 - 增加局部亮度
|
// 点光源:从顶部照射,增加局部亮度
|
||||||
const pointLight = new THREE.PointLight(0xFFFFFF, 1, 50)
|
const pointLight = new THREE.PointLight(0xFFFFFF, 1, 50)
|
||||||
pointLight.position.set(0, 10, 0)
|
pointLight.position.set(0, 10, 0) // 顶部中心
|
||||||
scene.add(pointLight)
|
scene.add(pointLight)
|
||||||
|
|
||||||
// 添加地面
|
// ========== 地面创建 ==========
|
||||||
|
// 创建100x100的灰色地面,用于承载模型
|
||||||
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
||||||
const groundMaterial = new THREE.MeshStandardMaterial({
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
||||||
color: 0x555555,
|
color: 0x555555, // 中灰色
|
||||||
roughness: 0.8,
|
roughness: 0.8, // 较高粗糙度
|
||||||
metalness: 0.2,
|
metalness: 0.2, // 轻微金属感
|
||||||
})
|
})
|
||||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||||
ground.rotation.x = -Math.PI / 2
|
ground.rotation.x = -Math.PI / 2 // 旋转90度使其水平
|
||||||
ground.position.y = 0
|
ground.position.y = 0
|
||||||
scene.add(ground)
|
scene.add(ground)
|
||||||
|
|
||||||
// 设置后期处理
|
// ========== 设置后期处理 ==========
|
||||||
|
// 配置轮廓描边效果
|
||||||
setupPostProcessing()
|
setupPostProcessing()
|
||||||
|
|
||||||
// 加载模型
|
// ========== 加载GLTF模型 ==========
|
||||||
const loader = new GLTFLoader()
|
const loader = new GLTFLoader()
|
||||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||||
model = gltf.scene
|
model = gltf.scene
|
||||||
|
|
||||||
|
// 遍历模型所有子节点,添加自定义属性
|
||||||
model.traverse((child) => {
|
model.traverse((child) => {
|
||||||
if (child instanceof THREE.Mesh) {
|
if (child instanceof THREE.Mesh) {
|
||||||
|
// 为每个网格添加元数据,用于信息展示
|
||||||
child.userData.objectType = '建筑构件'
|
child.userData.objectType = '建筑构件'
|
||||||
child.userData.objectName = child.name || '未命名物体'
|
child.userData.objectName = child.name || '未命名物体'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
scene.add(model)
|
scene.add(model)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 事件监听
|
// ========== 事件监听 ==========
|
||||||
renderer.domElement.addEventListener('mousemove', onMouseMove)
|
// 监听鼠标移动、点击和窗口大小变化
|
||||||
renderer.domElement.addEventListener('click', onCanvasClick)
|
renderer.domElement.addEventListener('mousemove', onMouseMove) // 鼠标移动检测
|
||||||
window.addEventListener('resize', onResize)
|
renderer.domElement.addEventListener('click', onCanvasClick) // 鼠标点击检测
|
||||||
|
window.addEventListener('resize', onResize) // 窗口大小变化
|
||||||
|
|
||||||
|
// 启动渲染循环
|
||||||
animate()
|
animate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置后期处理(轮廓描边)
|
/**
|
||||||
|
* 设置后期处理效果(轮廓描边)
|
||||||
|
* 使用EffectComposer实现物体选中时的绿色轮廓高亮效果
|
||||||
|
*
|
||||||
|
* 工作原理:
|
||||||
|
* 1. RenderPass:渲染基础场景
|
||||||
|
* 2. OutlinePass:为指定物体添加轮廓描边
|
||||||
|
*/
|
||||||
function setupPostProcessing() {
|
function setupPostProcessing() {
|
||||||
|
// 创建后期处理合成器
|
||||||
composer = new EffectComposer(renderer)
|
composer = new EffectComposer(renderer)
|
||||||
|
|
||||||
|
// 添加基础渲染通道(渲染场景本身)
|
||||||
const renderPass = new RenderPass(scene, camera)
|
const renderPass = new RenderPass(scene, camera)
|
||||||
composer.addPass(renderPass)
|
composer.addPass(renderPass)
|
||||||
|
|
||||||
|
// 创建轮廓描边通道
|
||||||
outlinePass = new OutlinePass(
|
outlinePass = new OutlinePass(
|
||||||
new THREE.Vector2(threeRef.value!.clientWidth, threeRef.value!.clientHeight),
|
new THREE.Vector2(threeRef.value!.clientWidth, threeRef.value!.clientHeight),
|
||||||
scene,
|
scene,
|
||||||
camera,
|
camera,
|
||||||
)
|
)
|
||||||
outlinePass.edgeStrength = 3
|
// 配置轮廓效果参数
|
||||||
outlinePass.edgeGlow = 0.5
|
outlinePass.edgeStrength = 3 // 边缘强度
|
||||||
outlinePass.edgeThickness = 2
|
outlinePass.edgeGlow = 0.5 // 边缘发光强度
|
||||||
outlinePass.pulsePeriod = 0
|
outlinePass.edgeThickness = 2 // 边缘厚度
|
||||||
outlinePass.visibleEdgeColor.set('#00ff00')
|
outlinePass.pulsePeriod = 0 // 脉冲周期(0=不脉冲)
|
||||||
outlinePass.hiddenEdgeColor.set('#00ff00')
|
outlinePass.visibleEdgeColor.set('#00ff00') // 可见边缘颜色:绿色
|
||||||
|
outlinePass.hiddenEdgeColor.set('#00ff00') // 隐藏边缘颜色:绿色
|
||||||
|
|
||||||
|
// 添加轮廓通道到合成器
|
||||||
composer.addPass(outlinePass)
|
composer.addPass(outlinePass)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画循环函数
|
||||||
|
* 每帧执行的逻辑:
|
||||||
|
* 1. 更新轨道控制器(处理阻尼效果)
|
||||||
|
* 2. 更新信息标签位置(跟随选中物体)
|
||||||
|
* 3. 渲染场景(包含后期处理效果)
|
||||||
|
*/
|
||||||
function animate() {
|
function animate() {
|
||||||
animationId = requestAnimationFrame(animate)
|
animationId = requestAnimationFrame(animate)
|
||||||
controls.update()
|
controls.update() // 更新轨道控制器
|
||||||
|
|
||||||
// 更新标签位置
|
// 如果有选中物体,实时更新标签位置
|
||||||
if (selectedObject.value) {
|
if (selectedObject.value) {
|
||||||
updateLabelPosition()
|
updateLabelPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用后期处理合成器渲染(包含轮廓描边)
|
||||||
composer.render()
|
composer.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新标签位置
|
/**
|
||||||
|
* 更新信息标签的屏幕位置
|
||||||
|
* 将3D世界坐标转换为2D屏幕坐标
|
||||||
|
*
|
||||||
|
* 步骤:
|
||||||
|
* 1. 计算物体包围盒,获取高度
|
||||||
|
* 2. 将标签位置设置在物体顶部上方30%处
|
||||||
|
* 3. 将3D坐标投影到屏幕空间
|
||||||
|
* 4. 转换为像素坐标
|
||||||
|
*/
|
||||||
function updateLabelPosition() {
|
function updateLabelPosition() {
|
||||||
if (!selectedObject.value)
|
if (!selectedObject.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
const position = selectedObject.value.position.clone()
|
const position = selectedObject.value.position.clone()
|
||||||
// 获取物体的包围盒来计算高度
|
|
||||||
|
// 计算物体包围盒,确定标签显示高度
|
||||||
const box = new THREE.Box3().setFromObject(selectedObject.value.mesh)
|
const box = new THREE.Box3().setFromObject(selectedObject.value.mesh)
|
||||||
const height = box.max.y - box.min.y
|
const height = box.max.y - box.min.y
|
||||||
|
// 标签显示在物体顶部上方30%高度处
|
||||||
position.y = box.max.y + height * 0.3
|
position.y = box.max.y + height * 0.3
|
||||||
|
|
||||||
|
// 将3D世界坐标投影到归一化设备坐标(-1到1)
|
||||||
position.project(camera)
|
position.project(camera)
|
||||||
|
|
||||||
|
// 转换为屏幕像素坐标
|
||||||
labelPosition.value = {
|
labelPosition.value = {
|
||||||
x: (position.x * 0.5 + 0.5) * threeRef.value!.clientWidth,
|
x: (position.x * 0.5 + 0.5) * threeRef.value!.clientWidth, // X坐标
|
||||||
y: (-(position.y * 0.5) + 0.5) * threeRef.value!.clientHeight,
|
y: (-(position.y * 0.5) + 0.5) * threeRef.value!.clientHeight, // Y坐标(翻转)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鼠标移动事件
|
/**
|
||||||
|
* 鼠标移动事件处理
|
||||||
|
* 实现鼠标悬停高亮效果
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 将鼠标坐标转换为归一化设备坐标(-1到1)
|
||||||
|
* 2. 使用射线检测鼠标下的物体
|
||||||
|
* 3. 为悬停物体添加绿色轮廓描边
|
||||||
|
* 4. 在悬停物体中心显示坐标轴
|
||||||
|
* 5. 切换物体时清除旧效果
|
||||||
|
*/
|
||||||
function onMouseMove(event: MouseEvent) {
|
function onMouseMove(event: MouseEvent) {
|
||||||
|
// ========== 计算归一化鼠标坐标 ==========
|
||||||
|
// 获取Canvas的屏幕位置和尺寸
|
||||||
const rect = renderer.domElement.getBoundingClientRect()
|
const rect = renderer.domElement.getBoundingClientRect()
|
||||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
// 将鼠标坐标转换为归一化设备坐标(-1到1)
|
||||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 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)
|
raycaster.setFromCamera(mouse, camera)
|
||||||
|
|
||||||
if (model) {
|
if (model) {
|
||||||
|
// 检测射线与模型的交互(true=递归检测所有子对象)
|
||||||
const intersects = raycaster.intersectObject(model, true)
|
const intersects = raycaster.intersectObject(model, true)
|
||||||
|
|
||||||
|
// ========== 处理悬停物体 ==========
|
||||||
if (intersects.length > 0 && intersects[0]) {
|
if (intersects.length > 0 && intersects[0]) {
|
||||||
const object = intersects[0].object as THREE.Mesh
|
const object = intersects[0].object as THREE.Mesh
|
||||||
|
|
||||||
// 如果是新的悬停物体
|
// 如果是新的悬停物体(避免重复处理)
|
||||||
if (hoveredObject.value !== object) {
|
if (hoveredObject.value !== object) {
|
||||||
hoveredObject.value = object
|
hoveredObject.value = object
|
||||||
|
|
||||||
// 添加轮廓描边
|
// 添加绿色轮廓描边效果
|
||||||
outlinePass.selectedObjects = [object]
|
outlinePass.selectedObjects = [object]
|
||||||
|
|
||||||
// 添加辅助坐标线
|
// 添加坐标轴辅助线到物体中心
|
||||||
removeAxesHelper()
|
removeAxesHelper() // 先移除旧的
|
||||||
const box = new THREE.Box3().setFromObject(object)
|
const box = new THREE.Box3().setFromObject(object) // 计算包围盒
|
||||||
const center = box.getCenter(new THREE.Vector3())
|
const center = box.getCenter(new THREE.Vector3()) // 获取中心点
|
||||||
axesHelper = new THREE.AxesHelper(2)
|
axesHelper = new THREE.AxesHelper(2) // 创建2单位长度的坐标轴
|
||||||
axesHelper.position.copy(center)
|
axesHelper.position.copy(center) // 定位到物体中心
|
||||||
scene.add(axesHelper)
|
scene.add(axesHelper)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 鼠标没有悬停在物体上
|
// ========== 鼠标离开物体 ==========
|
||||||
|
// 如果之前有悬停物体,且该物体不是当前选中的物体,则清除悬停效果
|
||||||
if (hoveredObject.value && hoveredObject.value !== selectedObject.value?.mesh) {
|
if (hoveredObject.value && hoveredObject.value !== selectedObject.value?.mesh) {
|
||||||
clearHoverEffects()
|
clearHoverEffects()
|
||||||
}
|
}
|
||||||
@@ -222,106 +319,154 @@ function onMouseMove(event: MouseEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 画布点击事件
|
/**
|
||||||
|
* 画布点击事件处理
|
||||||
|
* 实现物体选中和信息展示
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 检测点击的物体
|
||||||
|
* 2. 提取物体信息(名称、材质、坐标)
|
||||||
|
* 3. 显示信息标签
|
||||||
|
* 4. 保持轮廓描边和坐标轴
|
||||||
|
* 5. 点击空白处取消选择
|
||||||
|
*/
|
||||||
function onCanvasClick(event: MouseEvent) {
|
function onCanvasClick(event: MouseEvent) {
|
||||||
|
// ========== 计算归一化鼠标坐标 ==========
|
||||||
const rect = renderer.domElement.getBoundingClientRect()
|
const rect = renderer.domElement.getBoundingClientRect()
|
||||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
||||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
||||||
|
|
||||||
|
// ========== 射线检测 ==========
|
||||||
raycaster.setFromCamera(mouse, camera)
|
raycaster.setFromCamera(mouse, camera)
|
||||||
|
|
||||||
if (model) {
|
if (model) {
|
||||||
|
// 检测射线与模型的交互
|
||||||
const intersects = raycaster.intersectObject(model, true)
|
const intersects = raycaster.intersectObject(model, true)
|
||||||
|
|
||||||
if (intersects.length > 0 && intersects[0]) {
|
if (intersects.length > 0 && intersects[0]) {
|
||||||
|
// ========== 点击到物体 ==========
|
||||||
const object = intersects[0].object as THREE.Mesh
|
const object = intersects[0].object as THREE.Mesh
|
||||||
const point = intersects[0].point
|
const point = intersects[0].point // 点击的3D空间坐标
|
||||||
|
|
||||||
// 获取材质信息
|
// 提取材质信息
|
||||||
let materialName = '未知材质'
|
let materialName = '未知材质'
|
||||||
if (object.material) {
|
if (object.material) {
|
||||||
|
// 处理多材质情况
|
||||||
if (Array.isArray(object.material)) {
|
if (Array.isArray(object.material)) {
|
||||||
materialName = object.material.map((m: THREE.Material) => m.type).join(', ')
|
materialName = object.material.map((m: THREE.Material) => m.type).join(', ')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
// 单一材质
|
||||||
materialName = object.material.type
|
materialName = object.material.type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置选中物体
|
// 设置选中物体信息,触发UI标签显示
|
||||||
selectedObject.value = {
|
selectedObject.value = {
|
||||||
name: object.userData.objectName || object.name || '未命名物体',
|
name: object.userData.objectName || object.name || '未命名物体', // 物体名称
|
||||||
material: materialName,
|
material: materialName, // 材质类型
|
||||||
position: point.clone(),
|
position: point.clone(), // 点击位置(用于标签定位)
|
||||||
mesh: object,
|
mesh: object, // 物体引用
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保持轮廓描边和坐标线
|
// 保持视觉效果(轮廓描边和坐标轴)
|
||||||
hoveredObject.value = object
|
hoveredObject.value = object
|
||||||
outlinePass.selectedObjects = [object]
|
outlinePass.selectedObjects = [object]
|
||||||
|
|
||||||
|
// 计算并更新标签位置
|
||||||
updateLabelPosition()
|
updateLabelPosition()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 点击空白处,清除所有效果
|
// ========== 点击空白处 ==========
|
||||||
|
// 清除所有选中和悬停效果
|
||||||
clearAllEffects()
|
clearAllEffects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除悬停效果
|
/**
|
||||||
|
* 清除鼠标悬停效果
|
||||||
|
* 当鼠标离开物体时调用
|
||||||
|
* 注意:如果物体是当前选中的,不清除效果(保持选中状态)
|
||||||
|
*/
|
||||||
function clearHoverEffects() {
|
function clearHoverEffects() {
|
||||||
hoveredObject.value = null
|
hoveredObject.value = null
|
||||||
|
// 只有在没有选中物体的情况下才清除视觉效果
|
||||||
if (!selectedObject.value) {
|
if (!selectedObject.value) {
|
||||||
outlinePass.selectedObjects = []
|
outlinePass.selectedObjects = [] // 清除轮廓描边
|
||||||
removeAxesHelper()
|
removeAxesHelper() // 移除坐标轴
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除所有效果
|
/**
|
||||||
|
* 清除所有交互效果
|
||||||
|
* 当点击空白处时调用
|
||||||
|
* 清除悬停、选中、轮廓描边、坐标轴和信息标签
|
||||||
|
*/
|
||||||
function clearAllEffects() {
|
function clearAllEffects() {
|
||||||
hoveredObject.value = null
|
hoveredObject.value = null // 清除悬停物体
|
||||||
selectedObject.value = null
|
selectedObject.value = null // 清除选中物体(隐藏信息标签)
|
||||||
outlinePass.selectedObjects = []
|
outlinePass.selectedObjects = [] // 清除轮廓描边
|
||||||
removeAxesHelper()
|
removeAxesHelper() // 移除坐标轴
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除辅助坐标线
|
/**
|
||||||
|
* 移除辅助坐标轴
|
||||||
|
* 从场景中移除坐标轴辅助线,并清空引用
|
||||||
|
*/
|
||||||
function removeAxesHelper() {
|
function removeAxesHelper() {
|
||||||
if (axesHelper) {
|
if (axesHelper) {
|
||||||
scene.remove(axesHelper)
|
scene.remove(axesHelper) // 从场景移除
|
||||||
axesHelper = null
|
axesHelper = null // 清空引用
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 窗口大小变化处理
|
||||||
|
* 更新相机宽高比、渲染器尺寸和后期处理合成器尺寸
|
||||||
|
*/
|
||||||
function onResize() {
|
function onResize() {
|
||||||
if (!threeRef.value)
|
if (!threeRef.value)
|
||||||
return
|
return
|
||||||
const width = threeRef.value.clientWidth
|
const width = threeRef.value.clientWidth
|
||||||
const height = threeRef.value.clientHeight
|
const height = threeRef.value.clientHeight
|
||||||
|
|
||||||
|
// 更新相机宽高比
|
||||||
camera.aspect = width / height
|
camera.aspect = width / height
|
||||||
camera.updateProjectionMatrix()
|
camera.updateProjectionMatrix() // 更新投影矩阵
|
||||||
|
|
||||||
|
// 更新渲染器和后期处理合成器尺寸
|
||||||
renderer.setSize(width, height)
|
renderer.setSize(width, height)
|
||||||
composer.setSize(width, height)
|
composer.setSize(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Vue生命周期 ==========
|
||||||
|
// 组件挂载时初始化场景
|
||||||
onMounted(init)
|
onMounted(init)
|
||||||
|
|
||||||
|
// 组件卸载时清理资源,防止内存泄漏
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
cancelAnimationFrame(animationId)
|
cancelAnimationFrame(animationId) // 取消动画帧
|
||||||
window.removeEventListener('resize', onResize)
|
window.removeEventListener('resize', onResize) // 移除窗口事件监听
|
||||||
|
|
||||||
|
// 移除Canvas事件监听
|
||||||
if (renderer.domElement) {
|
if (renderer.domElement) {
|
||||||
renderer.domElement.removeEventListener('mousemove', onMouseMove)
|
renderer.domElement.removeEventListener('mousemove', onMouseMove)
|
||||||
renderer.domElement.removeEventListener('click', onCanvasClick)
|
renderer.domElement.removeEventListener('click', onCanvasClick)
|
||||||
}
|
}
|
||||||
renderer.dispose()
|
|
||||||
composer.dispose()
|
// 释放WebGL资源
|
||||||
|
renderer.dispose() // 释放渲染器资源
|
||||||
|
composer.dispose() // 释放后期处理资源
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Three.js渲染容器,占满整个视口 -->
|
||||||
<div ref="threeRef" class="w-full h-full" />
|
<div ref="threeRef" class="w-full h-full" />
|
||||||
|
|
||||||
<!-- 物体信息标签 -->
|
<!-- 物体信息标签:显示选中物体的详细信息 -->
|
||||||
|
<!-- 浮动信息标签:跟随选中物体显示,动态定位 -->
|
||||||
<div
|
<div
|
||||||
v-if="selectedObject"
|
v-if="selectedObject"
|
||||||
class="object-label"
|
class="object-label"
|
||||||
@@ -331,13 +476,16 @@ onUnmounted(() => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="label-content">
|
<div class="label-content">
|
||||||
|
<!-- 物体名称 -->
|
||||||
<div class="label-title">
|
<div class="label-title">
|
||||||
{{ selectedObject.name }}
|
{{ selectedObject.name }}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 材质类型 -->
|
||||||
<div class="label-item">
|
<div class="label-item">
|
||||||
<span class="label-key">材质:</span>
|
<span class="label-key">材质:</span>
|
||||||
<span class="label-value">{{ selectedObject.material }}</span>
|
<span class="label-value">{{ selectedObject.material }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 点击位置的3D坐标 -->
|
||||||
<div class="label-item">
|
<div class="label-item">
|
||||||
<span class="label-key">坐标:</span>
|
<span class="label-key">坐标:</span>
|
||||||
<span class="label-value">
|
<span class="label-value">
|
||||||
@@ -349,10 +497,12 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 导航链接:跳转到下一个示例 -->
|
||||||
<router-link to="/three/11" class="position-fixed left-20px top-20px link-btn">
|
<router-link to="/three/11" class="position-fixed left-20px top-20px link-btn">
|
||||||
后期处理效果
|
后期处理效果
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<!-- 交互说明面板:显示操作提示 -->
|
||||||
<div class="position-fixed right-20px top-20px info-panel">
|
<div class="position-fixed right-20px top-20px info-panel">
|
||||||
<div class="info-text">
|
<div class="info-text">
|
||||||
鼠标移入物体:显示轮廓和坐标轴
|
鼠标移入物体:显示轮廓和坐标轴
|
||||||
|
|||||||
@@ -1,89 +1,133 @@
|
|||||||
<!--
|
<!--
|
||||||
* @Description: 后期处理效果
|
* @Description: 后期处理效果示例 - 演示 Three.js 中的各种后期处理技术
|
||||||
* @Autor: 刘 相卿
|
* @Autor: 刘 相卿
|
||||||
* @Date: 2025-11-17 16:07:33
|
* @Date: 2025-11-17 16:07:33
|
||||||
* @LastEditors: 刘 相卿
|
* @LastEditors: 刘 相卿
|
||||||
* @LastEditTime: 2025-11-21 09:43:16
|
* @LastEditTime: 2025-11-21 09:43:16
|
||||||
|
*
|
||||||
|
* 本示例展示了如何在 Three.js 中使用后期处理(Post-processing)技术
|
||||||
|
* 包含多种内置和自定义的效果通道(Pass),可以组合使用
|
||||||
-->
|
-->
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
// Three.js 核心库
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
|
// 轨道控制器 - 用于相机的交互式控制
|
||||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||||
|
// GLTF 模型加载器 - 用于加载 3D 模型文件
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||||
|
// 点阵效果 - 产生类似印刷品的点阵图案
|
||||||
import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'
|
import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'
|
||||||
|
// 效果合成器 - 管理和组合多个后期处理通道
|
||||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
||||||
|
// 故障效果 - 产生数字故障、屏幕撕裂的视觉效果
|
||||||
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'
|
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'
|
||||||
|
// 渲染通道 - 基础渲染层,将场景渲染到纹理
|
||||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
|
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
|
||||||
|
// 着色器通道 - 用于应用自定义着色器效果
|
||||||
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
|
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
|
||||||
|
// 抗锯齿通道 - 使用 SMAA 算法平滑边缘
|
||||||
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'
|
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'
|
||||||
|
// 辉光效果 - 为明亮物体添加发光效果
|
||||||
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
|
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
|
||||||
|
// Vue 3 组合式 API
|
||||||
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
|
// DOM 引用 - Three.js 渲染容器
|
||||||
const threeRef = shallowRef<HTMLDivElement>()
|
const threeRef = shallowRef<HTMLDivElement>()
|
||||||
|
|
||||||
let scene: THREE.Scene
|
// Three.js 核心对象声明
|
||||||
let camera: THREE.PerspectiveCamera
|
let scene: THREE.Scene // 场景 - 所有 3D 对象的容器
|
||||||
let renderer: THREE.WebGLRenderer
|
let camera: THREE.PerspectiveCamera // 透视相机 - 模拟人眼视角
|
||||||
let controls: OrbitControls
|
let renderer: THREE.WebGLRenderer // WebGL 渲染器 - 负责渲染场景
|
||||||
let animationId: number
|
let controls: OrbitControls // 轨道控制器 - 处理相机交互
|
||||||
let model: THREE.Group
|
let animationId: number // 动画帧 ID - 用于取消动画循环
|
||||||
|
let model: THREE.Group // 3D 模型组 - 存储加载的 GLTF 模型
|
||||||
|
|
||||||
// 后期处理相关
|
// 后期处理相关对象
|
||||||
let composer: EffectComposer
|
let composer: EffectComposer // 效果合成器 - 管理所有后期处理通道
|
||||||
let glitchPass: GlitchPass
|
let glitchPass: GlitchPass // 故障效果通道
|
||||||
let smaaPass: SMAAPass
|
let smaaPass: SMAAPass // 抗锯齿通道
|
||||||
let dotScreenPass: DotScreenPass
|
let dotScreenPass: DotScreenPass // 点阵效果通道
|
||||||
let bloomPass: UnrealBloomPass
|
let bloomPass: UnrealBloomPass // 辉光效果通道
|
||||||
let flickerPass: ShaderPass
|
let flickerPass: ShaderPass // 闪烁效果通道(自定义)
|
||||||
let waterPass: ShaderPass
|
let waterPass: ShaderPass // 水波效果通道(自定义)
|
||||||
|
|
||||||
// 控制面板状态
|
// ============================================================
|
||||||
|
// 控制面板状态管理
|
||||||
|
// ============================================================
|
||||||
|
// 使用响应式对象管理所有后期处理效果的开关和参数
|
||||||
const effects = ref({
|
const effects = ref({
|
||||||
glitch: false,
|
glitch: false, // 故障效果开关
|
||||||
antialiasing: false,
|
antialiasing: false, // 抗锯齿开关
|
||||||
dotScreen: false,
|
dotScreen: false, // 点阵效果开关
|
||||||
bloom: false,
|
bloom: false, // 辉光效果开关
|
||||||
bloomStrength: 1.5,
|
bloomStrength: 1.5, // 辉光强度(0-3)
|
||||||
flicker: false,
|
flicker: false, // 闪烁效果开关
|
||||||
water: false,
|
water: false, // 水波效果开关
|
||||||
})
|
})
|
||||||
|
|
||||||
// 自定义着色器 - 屏幕闪动效果
|
// ============================================================
|
||||||
|
// 自定义着色器 1:屏幕闪烁效果
|
||||||
|
// ============================================================
|
||||||
|
// 模拟老式显示器或荧光灯管的闪烁效果
|
||||||
const FlickerShader = {
|
const FlickerShader = {
|
||||||
|
// 着色器统一变量(Uniforms)- 从 JavaScript 传递给着色器的值
|
||||||
uniforms: {
|
uniforms: {
|
||||||
tDiffuse: { value: null },
|
tDiffuse: { value: null }, // 输入纹理(上一个通道的渲染结果)
|
||||||
amount: { value: 0.5 },
|
amount: { value: 0.5 }, // 闪烁强度(0-1)
|
||||||
time: { value: 0 },
|
time: { value: 0 }, // 时间变量,用于动画
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 顶点着色器 - 处理每个顶点的位置
|
||||||
vertexShader: `
|
vertexShader: `
|
||||||
varying vec2 vUv;
|
varying vec2 vUv; // 传递给片段着色器的 UV 坐标
|
||||||
void main() {
|
void main() {
|
||||||
vUv = uv;
|
vUv = uv; // 将 UV 坐标传递到片段着色器
|
||||||
|
// 计算顶点的最终位置
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
// 片段着色器 - 处理每个像素的颜色
|
||||||
fragmentShader: `
|
fragmentShader: `
|
||||||
uniform sampler2D tDiffuse;
|
uniform sampler2D tDiffuse; // 输入纹理采样器
|
||||||
uniform float amount;
|
uniform float amount; // 闪烁强度
|
||||||
uniform float time;
|
uniform float time; // 时间
|
||||||
varying vec2 vUv;
|
varying vec2 vUv; // 从顶点着色器接收的 UV 坐标
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// 从输入纹理中采样颜色
|
||||||
vec4 color = texture2D(tDiffuse, vUv);
|
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;
|
float flicker = sin(time * 10.0) * 0.5 + 0.5;
|
||||||
|
|
||||||
|
// 对闪烁值进行三次方运算,使效果更加明显
|
||||||
flicker = pow(flicker, 3.0);
|
flicker = pow(flicker, 3.0);
|
||||||
|
|
||||||
|
// 根据闪烁值降低颜色亮度
|
||||||
color.rgb *= 1.0 - (flicker * amount * 0.3);
|
color.rgb *= 1.0 - (flicker * amount * 0.3);
|
||||||
|
|
||||||
gl_FragColor = color;
|
gl_FragColor = color;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义着色器 - 水底波浪效果
|
// ============================================================
|
||||||
|
// 自定义着色器 2:水底波浪效果
|
||||||
|
// ============================================================
|
||||||
|
// 模拟水下观察的动态扭曲效果
|
||||||
const WaterShader = {
|
const WaterShader = {
|
||||||
|
// 着色器统一变量
|
||||||
uniforms: {
|
uniforms: {
|
||||||
tDiffuse: { value: null },
|
tDiffuse: { value: null }, // 输入纹理
|
||||||
time: { value: 0 },
|
time: { value: 0 }, // 时间变量
|
||||||
distortion: { value: 0.5 },
|
distortion: { value: 0.5 }, // 扭曲强度(0-1)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 顶点着色器
|
||||||
vertexShader: `
|
vertexShader: `
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
void main() {
|
void main() {
|
||||||
@@ -91,6 +135,8 @@ const WaterShader = {
|
|||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
// 片段着色器
|
||||||
fragmentShader: `
|
fragmentShader: `
|
||||||
uniform sampler2D tDiffuse;
|
uniform sampler2D tDiffuse;
|
||||||
uniform float time;
|
uniform float time;
|
||||||
@@ -98,183 +144,252 @@ const WaterShader = {
|
|||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 uv = vUv;
|
vec2 uv = vUv; // 复制 UV 坐标以便修改
|
||||||
|
|
||||||
// 创建多层波浪效果
|
// 创建多层波浪效果,模拟真实水波的复杂性
|
||||||
|
// 波浪 1:水平方向的快速波动
|
||||||
float wave1 = sin(uv.x * 10.0 + time) * 0.01;
|
float wave1 = sin(uv.x * 10.0 + time) * 0.01;
|
||||||
|
|
||||||
|
// 波浪 2:垂直方向的波动,速度和方向不同
|
||||||
float wave2 = sin(uv.y * 15.0 - time * 1.5) * 0.01;
|
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;
|
float wave3 = sin((uv.x + uv.y) * 8.0 + time * 0.5) * 0.005;
|
||||||
|
|
||||||
|
// 将波浪效果应用到 UV 坐标上,产生扭曲
|
||||||
uv.x += (wave1 + wave3) * distortion;
|
uv.x += (wave1 + wave3) * distortion;
|
||||||
uv.y += (wave2 + wave3) * distortion;
|
uv.y += (wave2 + wave3) * distortion;
|
||||||
|
|
||||||
// 添加水下的颜色调整
|
// 使用扭曲后的 UV 坐标采样纹理
|
||||||
vec4 color = texture2D(tDiffuse, 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;
|
gl_FragColor = color;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 初始化函数 - 设置整个 Three.js 场景
|
||||||
|
// ============================================================
|
||||||
function init() {
|
function init() {
|
||||||
|
// 获取容器尺寸
|
||||||
const width = threeRef.value!.clientWidth
|
const width = threeRef.value!.clientWidth
|
||||||
const height = threeRef.value!.clientHeight
|
const height = threeRef.value!.clientHeight
|
||||||
|
|
||||||
|
// 创建场景 - 所有 3D 对象的容器
|
||||||
scene = new THREE.Scene()
|
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 = 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 })
|
// 创建 WebGL 渲染器
|
||||||
renderer.setSize(width, height)
|
renderer = new THREE.WebGLRenderer({ antialias: true }) // 启用抗锯齿
|
||||||
renderer.setPixelRatio(window.devicePixelRatio)
|
renderer.setSize(width, height) // 设置渲染尺寸
|
||||||
renderer.shadowMap.enabled = true
|
renderer.setPixelRatio(window.devicePixelRatio) // 设置像素比(支持高清屏幕)
|
||||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
renderer.shadowMap.enabled = true // 启用阴影
|
||||||
threeRef.value!.appendChild(renderer.domElement)
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap // 使用柔和阴影
|
||||||
|
threeRef.value!.appendChild(renderer.domElement) // 将渲染器的 canvas 添加到 DOM
|
||||||
|
|
||||||
|
// 创建轨道控制器 - 允许用户通过鼠标控制相机
|
||||||
controls = new OrbitControls(camera, renderer.domElement)
|
controls = new OrbitControls(camera, renderer.domElement)
|
||||||
controls.enableDamping = true
|
controls.enableDamping = true // 启用阻尼(惯性)
|
||||||
controls.dampingFactor = 0.05
|
controls.dampingFactor = 0.05 // 阻尼系数
|
||||||
|
|
||||||
setupLighting()
|
// 调用各个初始化子函数
|
||||||
setupGround()
|
setupLighting() // 设置灯光
|
||||||
setupPostProcessing()
|
setupGround() // 设置地面
|
||||||
loadModel()
|
setupPostProcessing() // 设置后期处理
|
||||||
|
loadModel() // 加载 3D 模型
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
window.addEventListener('resize', onResize)
|
window.addEventListener('resize', onResize)
|
||||||
|
|
||||||
|
// 启动动画循环
|
||||||
animate()
|
animate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 灯光设置函数
|
||||||
|
// ============================================================
|
||||||
function setupLighting() {
|
function setupLighting() {
|
||||||
// 环境光
|
// 1. 环境光 - 为整个场景提供均匀的基础照明
|
||||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.0)
|
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.0) // 白色,强度 1.0
|
||||||
scene.add(ambientLight)
|
scene.add(ambientLight)
|
||||||
|
|
||||||
// 主方向光
|
// 2. 主方向光 - 模拟太阳光,产生阴影
|
||||||
const mainLight = new THREE.DirectionalLight(0xFFFFFF, 1.5)
|
const mainLight = new THREE.DirectionalLight(0xFFFFFF, 1.5) // 白色,强度 1.5
|
||||||
mainLight.position.set(10, 15, 10)
|
mainLight.position.set(10, 15, 10) // 从右上方照射
|
||||||
mainLight.castShadow = true
|
mainLight.castShadow = true // 启用投射阴影
|
||||||
|
// 设置阴影贴图分辨率(越高越清晰,但性能消耗越大)
|
||||||
mainLight.shadow.mapSize.width = 2048
|
mainLight.shadow.mapSize.width = 2048
|
||||||
mainLight.shadow.mapSize.height = 2048
|
mainLight.shadow.mapSize.height = 2048
|
||||||
scene.add(mainLight)
|
scene.add(mainLight)
|
||||||
|
|
||||||
// 辅助光
|
// 3. 辅助光(补光)- 从另一侧照亮,避免阴影过暗
|
||||||
const fillLight = new THREE.DirectionalLight(0x4488FF, 0.8)
|
const fillLight = new THREE.DirectionalLight(0x4488FF, 0.8) // 蓝色调,强度 0.8
|
||||||
fillLight.position.set(-10, 10, -10)
|
fillLight.position.set(-10, 10, -10) // 从左上后方照射
|
||||||
scene.add(fillLight)
|
scene.add(fillLight)
|
||||||
|
|
||||||
// 添加发光球体(用于演示发光效果)
|
// 4. 添加发光球体(用于演示辉光效果)
|
||||||
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32)
|
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32) // 半径 0.5,32段精度
|
||||||
const emissiveMaterial = new THREE.MeshStandardMaterial({
|
const emissiveMaterial = new THREE.MeshStandardMaterial({
|
||||||
color: 0x00FFFF,
|
color: 0x00FFFF, // 青色
|
||||||
emissive: 0x00FFFF,
|
emissive: 0x00FFFF, // 自发光颜色(青色)
|
||||||
emissiveIntensity: 2,
|
emissiveIntensity: 2, // 自发光强度
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 在四个角落放置发光球体
|
||||||
const positions = [
|
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) => {
|
positions.forEach((pos) => {
|
||||||
|
// 创建发光球体
|
||||||
const sphere = new THREE.Mesh(sphereGeometry, emissiveMaterial.clone())
|
const sphere = new THREE.Mesh(sphereGeometry, emissiveMaterial.clone())
|
||||||
sphere.position.set(pos.x, pos.y, pos.z)
|
sphere.position.set(pos.x, pos.y, pos.z)
|
||||||
scene.add(sphere)
|
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)
|
pointLight.position.copy(sphere.position)
|
||||||
scene.add(pointLight)
|
scene.add(pointLight)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 地面设置函数
|
||||||
|
// ============================================================
|
||||||
function setupGround() {
|
function setupGround() {
|
||||||
|
// 创建地面几何体 - 100x100 的平面
|
||||||
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
||||||
|
|
||||||
|
// 创建地面材质 - 使用标准材质以支持光照和阴影
|
||||||
const groundMaterial = new THREE.MeshStandardMaterial({
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
||||||
color: 0x333333,
|
color: 0x333333, // 深灰色
|
||||||
roughness: 0.8,
|
roughness: 0.8, // 粗糙度(0=光滑镜面,1=完全粗糙)
|
||||||
metalness: 0.2,
|
metalness: 0.2, // 金属度(0=非金属,1=金属)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 创建地面网格
|
||||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||||
ground.rotation.x = -Math.PI / 2
|
ground.rotation.x = -Math.PI / 2 // 旋转90度使其水平(默认是垂直的)
|
||||||
ground.position.y = 0
|
ground.position.y = 0 // 放置在 y=0 平面
|
||||||
ground.receiveShadow = true
|
ground.receiveShadow = true // 接收阴影
|
||||||
scene.add(ground)
|
scene.add(ground)
|
||||||
|
|
||||||
// 网格辅助线
|
// 添加网格辅助线 - 帮助观察空间和距离
|
||||||
|
// 参数:大小(100), 分割数(50), 中心线颜色, 网格线颜色
|
||||||
const gridHelper = new THREE.GridHelper(100, 50, 0x555555, 0x333333)
|
const gridHelper = new THREE.GridHelper(100, 50, 0x555555, 0x333333)
|
||||||
scene.add(gridHelper)
|
scene.add(gridHelper)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 后期处理设置函数 - 这是本示例的核心
|
||||||
|
// ============================================================
|
||||||
function setupPostProcessing() {
|
function setupPostProcessing() {
|
||||||
|
// 创建效果合成器 - 管理所有后期处理通道
|
||||||
|
// 它会将多个处理通道(Pass)串联起来,形成处理管线
|
||||||
composer = new EffectComposer(renderer)
|
composer = new EffectComposer(renderer)
|
||||||
|
|
||||||
// 基础渲染通道
|
// 1. 添加基础渲染通道 - 必须是第一个通道
|
||||||
|
// 它将场景渲染到纹理上,供后续通道使用
|
||||||
const renderPass = new RenderPass(scene, camera)
|
const renderPass = new RenderPass(scene, camera)
|
||||||
composer.addPass(renderPass)
|
composer.addPass(renderPass)
|
||||||
|
|
||||||
// 1. 故障效果
|
// 2. 故障效果通道 (Glitch Pass)
|
||||||
|
// 产生数字故障、屏幕撕裂、RGB 分离等电子干扰效果
|
||||||
glitchPass = new GlitchPass()
|
glitchPass = new GlitchPass()
|
||||||
glitchPass.enabled = effects.value.glitch
|
glitchPass.enabled = effects.value.glitch // 根据控制面板状态启用/禁用
|
||||||
composer.addPass(glitchPass)
|
composer.addPass(glitchPass)
|
||||||
|
|
||||||
// 2. 抗锯齿
|
// 3. 抗锯齿通道 (SMAA Pass)
|
||||||
|
// 使用 SMAA(Subpixel Morphological Anti-Aliasing)算法
|
||||||
|
// 平滑边缘,减少锯齿,提升画面质量
|
||||||
smaaPass = new SMAAPass()
|
smaaPass = new SMAAPass()
|
||||||
smaaPass.enabled = effects.value.antialiasing
|
smaaPass.enabled = effects.value.antialiasing
|
||||||
composer.addPass(smaaPass)
|
composer.addPass(smaaPass)
|
||||||
|
|
||||||
// 3. 点效果
|
// 4. 点阵效果通道 (Dot Screen Pass)
|
||||||
|
// 将画面转换为点阵图案,模拟印刷品或老式显示器效果
|
||||||
dotScreenPass = new DotScreenPass()
|
dotScreenPass = new DotScreenPass()
|
||||||
dotScreenPass.enabled = effects.value.dotScreen
|
dotScreenPass.enabled = effects.value.dotScreen
|
||||||
composer.addPass(dotScreenPass)
|
composer.addPass(dotScreenPass)
|
||||||
|
|
||||||
// 4. 发光效果
|
// 5. 辉光效果通道 (Unreal Bloom Pass)
|
||||||
|
// 为明亮的物体添加发光效果,常用于表现霓虹灯、激光等
|
||||||
bloomPass = new UnrealBloomPass(
|
bloomPass = new UnrealBloomPass(
|
||||||
|
// 参数1:渲染分辨率
|
||||||
new THREE.Vector2(threeRef.value!.clientWidth, threeRef.value!.clientHeight),
|
new THREE.Vector2(threeRef.value!.clientWidth, threeRef.value!.clientHeight),
|
||||||
effects.value.bloomStrength,
|
effects.value.bloomStrength, // 参数2:强度(0-3)
|
||||||
0.4,
|
0.4, // 参数3:半径(发光扩散距离)
|
||||||
0.85,
|
0.85, // 参数4:阈值(多亮的物体开始发光,0-1)
|
||||||
)
|
)
|
||||||
bloomPass.enabled = effects.value.bloom
|
bloomPass.enabled = effects.value.bloom
|
||||||
composer.addPass(bloomPass)
|
composer.addPass(bloomPass)
|
||||||
|
|
||||||
// 5. 屏幕闪动效果
|
// 6. 屏幕闪烁效果通道(自定义)
|
||||||
|
// 使用自定义的 FlickerShader 创建老旧屏幕闪烁效果
|
||||||
flickerPass = new ShaderPass(FlickerShader)
|
flickerPass = new ShaderPass(FlickerShader)
|
||||||
flickerPass.enabled = effects.value.flicker
|
flickerPass.enabled = effects.value.flicker
|
||||||
composer.addPass(flickerPass)
|
composer.addPass(flickerPass)
|
||||||
|
|
||||||
// 6. 水底波浪效果
|
// 7. 水底波浪效果通道(自定义)
|
||||||
|
// 使用自定义的 WaterShader 创建水下观察的扭曲效果
|
||||||
waterPass = new ShaderPass(WaterShader)
|
waterPass = new ShaderPass(WaterShader)
|
||||||
waterPass.enabled = effects.value.water
|
waterPass.enabled = effects.value.water
|
||||||
composer.addPass(waterPass)
|
composer.addPass(waterPass)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 模型加载函数
|
||||||
|
// ============================================================
|
||||||
function loadModel() {
|
function loadModel() {
|
||||||
|
// 创建 GLTF 加载器实例
|
||||||
const loader = new GLTFLoader()
|
const loader = new GLTFLoader()
|
||||||
|
|
||||||
|
// 加载模型文件
|
||||||
|
// 参数1:模型文件路径(位于 public 目录)
|
||||||
|
// 参数2:加载成功的回调函数
|
||||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||||
model = gltf.scene
|
model = gltf.scene // 获取模型场景
|
||||||
|
|
||||||
|
// 遍历模型中的所有子对象
|
||||||
model.traverse((child) => {
|
model.traverse((child) => {
|
||||||
if (child instanceof THREE.Mesh) {
|
if (child instanceof THREE.Mesh) {
|
||||||
child.castShadow = true
|
child.castShadow = true // 投射阴影
|
||||||
child.receiveShadow = true
|
child.receiveShadow = true // 接收阴影
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
scene.add(model)
|
|
||||||
|
scene.add(model) // 将模型添加到场景中
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 动画循环函数 - 每帧执行一次
|
||||||
|
// ============================================================
|
||||||
function animate() {
|
function animate() {
|
||||||
|
// 请求下一帧动画(递归调用)
|
||||||
animationId = requestAnimationFrame(animate)
|
animationId = requestAnimationFrame(animate)
|
||||||
|
|
||||||
|
// 更新轨道控制器(处理阻尼效果)
|
||||||
controls.update()
|
controls.update()
|
||||||
|
|
||||||
|
// 获取当前时间(秒)
|
||||||
const time = performance.now() * 0.001
|
const time = performance.now() * 0.001
|
||||||
|
|
||||||
// 更新自定义着色器的时间
|
// 更新自定义着色器的时间统一变量
|
||||||
|
// 这样着色器就能产生随时间变化的动画效果
|
||||||
if (flickerPass.uniforms.time) {
|
if (flickerPass.uniforms.time) {
|
||||||
flickerPass.uniforms.time.value = time
|
flickerPass.uniforms.time.value = time
|
||||||
}
|
}
|
||||||
@@ -282,62 +397,95 @@ function animate() {
|
|||||||
waterPass.uniforms.time.value = time
|
waterPass.uniforms.time.value = time
|
||||||
}
|
}
|
||||||
|
|
||||||
// 让装饰球体浮动
|
// 让装饰球体浮动 - 遍历场景中的所有对象
|
||||||
scene.traverse((object) => {
|
scene.traverse((object) => {
|
||||||
|
// 检查是否是具有自发光材质的网格(即我们的发光球体)
|
||||||
if (object instanceof THREE.Mesh && object.material.emissive) {
|
if (object instanceof THREE.Mesh && object.material.emissive) {
|
||||||
|
// 使用正弦函数创建上下浮动效果
|
||||||
|
// 每个球体根据其 x 位置有不同的相位,产生波浪效果
|
||||||
object.position.y = 3 + Math.sin(time + object.position.x) * 0.5
|
object.position.y = 3 + Math.sin(time + object.position.x) * 0.5
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 使用合成器渲染(而不是直接使用 renderer.render)
|
||||||
|
// 这样会依次执行所有后期处理通道
|
||||||
composer.render()
|
composer.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 窗口大小调整处理函数
|
||||||
|
// ============================================================
|
||||||
function onResize() {
|
function onResize() {
|
||||||
if (!threeRef.value)
|
if (!threeRef.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
// 获取新的容器尺寸
|
||||||
const width = threeRef.value.clientWidth
|
const width = threeRef.value.clientWidth
|
||||||
const height = threeRef.value.clientHeight
|
const height = threeRef.value.clientHeight
|
||||||
|
|
||||||
|
// 更新相机宽高比
|
||||||
camera.aspect = width / height
|
camera.aspect = width / height
|
||||||
camera.updateProjectionMatrix()
|
camera.updateProjectionMatrix() // 必须调用此方法使更改生效
|
||||||
|
|
||||||
|
// 更新渲染器尺寸
|
||||||
renderer.setSize(width, height)
|
renderer.setSize(width, height)
|
||||||
|
|
||||||
|
// 更新合成器尺寸(重要!否则后期处理效果会变形)
|
||||||
composer.setSize(width, height)
|
composer.setSize(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听效果参数变化
|
// ============================================================
|
||||||
|
// 响应式监听 - 监听效果参数变化
|
||||||
|
// ============================================================
|
||||||
|
// 当用户在控制面板切换效果时,动态启用/禁用对应的通道
|
||||||
|
|
||||||
|
// 监听故障效果开关
|
||||||
watch(() => effects.value.glitch, (value) => {
|
watch(() => effects.value.glitch, (value) => {
|
||||||
glitchPass.enabled = value
|
glitchPass.enabled = value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听抗锯齿开关
|
||||||
watch(() => effects.value.antialiasing, (value) => {
|
watch(() => effects.value.antialiasing, (value) => {
|
||||||
smaaPass.enabled = value
|
smaaPass.enabled = value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听点阵效果开关
|
||||||
watch(() => effects.value.dotScreen, (value) => {
|
watch(() => effects.value.dotScreen, (value) => {
|
||||||
dotScreenPass.enabled = value
|
dotScreenPass.enabled = value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听辉光效果开关
|
||||||
watch(() => effects.value.bloom, (value) => {
|
watch(() => effects.value.bloom, (value) => {
|
||||||
bloomPass.enabled = value
|
bloomPass.enabled = value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听辉光强度变化
|
||||||
watch(() => effects.value.bloomStrength, (value) => {
|
watch(() => effects.value.bloomStrength, (value) => {
|
||||||
bloomPass.strength = value
|
bloomPass.strength = value // 实时更新强度参数
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听闪烁效果开关
|
||||||
watch(() => effects.value.flicker, (value) => {
|
watch(() => effects.value.flicker, (value) => {
|
||||||
flickerPass.enabled = value
|
flickerPass.enabled = value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听水波效果开关
|
||||||
watch(() => effects.value.water, (value) => {
|
watch(() => effects.value.water, (value) => {
|
||||||
waterPass.enabled = value
|
waterPass.enabled = value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 生命周期钩子
|
||||||
|
// ============================================================
|
||||||
|
// 组件挂载时初始化 Three.js 场景
|
||||||
onMounted(init)
|
onMounted(init)
|
||||||
|
|
||||||
|
// 组件卸载时清理资源,防止内存泄漏
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
cancelAnimationFrame(animationId)
|
cancelAnimationFrame(animationId) // 停止动画循环
|
||||||
window.removeEventListener('resize', onResize)
|
window.removeEventListener('resize', onResize) // 移除事件监听
|
||||||
renderer.dispose()
|
renderer.dispose() // 释放渲染器资源
|
||||||
composer.dispose()
|
composer.dispose() // 释放合成器资源
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,117 +6,141 @@
|
|||||||
* @LastEditTime: 2025-11-20 17:28:46
|
* @LastEditTime: 2025-11-20 17:28:46
|
||||||
-->
|
-->
|
||||||
<script lang="ts" setup>
|
<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'
|
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
const threeRef = shallowRef<HTMLDivElement>()
|
/**
|
||||||
const camera = shallowRef<THREE.PerspectiveCamera>()
|
* Three.js 基础对象引用
|
||||||
const scene = shallowRef<THREE.Scene>()
|
*/
|
||||||
const controls = shallowRef<OrbitControls>()
|
const threeRef = shallowRef<HTMLDivElement>() // Three.js 容器DOM引用
|
||||||
const renderer = shallowRef<THREE.WebGLRenderer>()
|
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)
|
const alertMesh = shallowRef<THREE.Mesh>() // 告警设备网格对象 (fmz_2_jzf_bs015)
|
||||||
let alertTime = 0
|
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()
|
let cameraAnimating = false // 相机动画进行标志
|
||||||
const cameraEndPosition = new THREE.Vector3()
|
let cameraAnimationProgress = 0 // 动画进度 (0-1)
|
||||||
const cameraEndTarget = new THREE.Vector3()
|
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() {
|
function initModel() {
|
||||||
const width = threeRef.value!.clientWidth
|
const width = threeRef.value!.clientWidth
|
||||||
const height = threeRef.value!.clientHeight
|
const height = threeRef.value!.clientHeight
|
||||||
|
|
||||||
// 创建场景
|
// ========== 场景设置 ==========
|
||||||
|
// 创建3D场景,深蓝紫色背景,添加雾效增强深度感
|
||||||
scene.value = new THREE.Scene()
|
scene.value = new THREE.Scene()
|
||||||
scene.value.background = new THREE.Color(0x1A1A2E)
|
scene.value.background = new THREE.Color(0x1A1A2E) // 深蓝紫色背景
|
||||||
scene.value.fog = new THREE.Fog(0x1A1A2E, 10, 50)
|
scene.value.fog = new THREE.Fog(0x1A1A2E, 10, 50) // 线性雾效:10米开始,50米完全遮蔽
|
||||||
|
|
||||||
|
// 坐标轴辅助器 (红X 绿Y 蓝Z)
|
||||||
const axesHelper = new THREE.AxesHelper(5)
|
const axesHelper = new THREE.AxesHelper(5)
|
||||||
scene.value.add(axesHelper)
|
scene.value.add(axesHelper)
|
||||||
|
|
||||||
// 创建透视相机
|
// ========== 相机设置 ==========
|
||||||
|
// 创建透视相机: 视野60度,初始位置在斜上方
|
||||||
camera.value = new THREE.PerspectiveCamera(
|
camera.value = new THREE.PerspectiveCamera(
|
||||||
60,
|
60, // 视野角度
|
||||||
width / height,
|
width / height, // 宽高比
|
||||||
0.1,
|
0.1, // 近裁剪面
|
||||||
1000,
|
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 = new THREE.WebGLRenderer({ antialias: true })
|
||||||
renderer.value.setSize(width, height)
|
renderer.value.setSize(width, height)
|
||||||
renderer.value.setPixelRatio(window.devicePixelRatio)
|
renderer.value.setPixelRatio(window.devicePixelRatio) // 适配高分辨率屏幕
|
||||||
renderer.value.shadowMap.enabled = true
|
renderer.value.shadowMap.enabled = true // 启用阴影
|
||||||
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap
|
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap // 柔和阴影
|
||||||
threeRef.value!.appendChild(renderer.value.domElement)
|
threeRef.value!.appendChild(renderer.value.domElement)
|
||||||
|
|
||||||
// 创建轨道控制器
|
// ========== 轨道控制器设置 ==========
|
||||||
|
// 允许用户通过鼠标拖拽、缩放、旋转视角
|
||||||
controls.value = new OrbitControls(camera.value, renderer.value.domElement)
|
controls.value = new OrbitControls(camera.value, renderer.value.domElement)
|
||||||
controls.value.enableDamping = true
|
controls.value.enableDamping = true // 启用阻尼(惯性)效果
|
||||||
controls.value.dampingFactor = 0.05
|
controls.value.dampingFactor = 0.05 // 阻尼系数,值越小越平滑
|
||||||
controls.value.minDistance = 2
|
controls.value.minDistance = 2 // 最小缩放距离
|
||||||
controls.value.maxDistance = 50
|
controls.value.maxDistance = 50 // 最大缩放距离
|
||||||
|
|
||||||
// 添加环境光
|
// ========== 灯光系统 ==========
|
||||||
|
// 环境光:提供基础照明,避免场景过暗
|
||||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.6)
|
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.6)
|
||||||
scene.value.add(ambientLight)
|
scene.value.add(ambientLight)
|
||||||
|
|
||||||
// 添加主平行光
|
// 主平行光:模拟太阳光,产生阴影
|
||||||
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.2)
|
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.2)
|
||||||
directionalLight.position.set(10, 15, 10)
|
directionalLight.position.set(10, 15, 10) // 从右上方照射
|
||||||
directionalLight.castShadow = true
|
directionalLight.castShadow = true // 投射阴影
|
||||||
|
// 配置阴影相机范围
|
||||||
directionalLight.shadow.camera.left = -20
|
directionalLight.shadow.camera.left = -20
|
||||||
directionalLight.shadow.camera.right = 20
|
directionalLight.shadow.camera.right = 20
|
||||||
directionalLight.shadow.camera.top = 20
|
directionalLight.shadow.camera.top = 20
|
||||||
directionalLight.shadow.camera.bottom = -20
|
directionalLight.shadow.camera.bottom = -20
|
||||||
directionalLight.shadow.mapSize.width = 2048
|
directionalLight.shadow.mapSize.width = 2048 // 阴影贴图分辨率
|
||||||
directionalLight.shadow.mapSize.height = 2048
|
directionalLight.shadow.mapSize.height = 2048
|
||||||
scene.value.add(directionalLight)
|
scene.value.add(directionalLight)
|
||||||
|
|
||||||
// 添加辅助光源
|
// 辅助光源:从左侧补光,减少暗部区域
|
||||||
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.5)
|
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.5)
|
||||||
fillLight.position.set(-10, 10, -10)
|
fillLight.position.set(-10, 10, -10)
|
||||||
scene.value.add(fillLight)
|
scene.value.add(fillLight)
|
||||||
|
|
||||||
// 添加地面
|
// ========== 地面创建 ==========
|
||||||
|
// 创建100x100的灰色地面,用于承载模型和接收阴影
|
||||||
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
||||||
const groundMaterial = new THREE.MeshStandardMaterial({
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
||||||
color: 0x555555,
|
color: 0x555555, // 中灰色
|
||||||
roughness: 0.8,
|
roughness: 0.8, // 较高粗糙度
|
||||||
metalness: 0.2,
|
metalness: 0.2, // 轻微金属感
|
||||||
})
|
})
|
||||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||||
ground.rotation.x = -Math.PI / 2
|
ground.rotation.x = -Math.PI / 2 // 旋转90度使其水平
|
||||||
ground.position.y = 0
|
ground.position.y = 0
|
||||||
ground.receiveShadow = true
|
ground.receiveShadow = true // 接收阴影
|
||||||
scene.value.add(ground)
|
scene.value.add(ground)
|
||||||
|
|
||||||
// 加载模型
|
// ========== GLTF模型加载 ==========
|
||||||
const loader = new GLTFLoader()
|
const loader = new GLTFLoader()
|
||||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||||
const model = gltf.scene
|
const model = gltf.scene
|
||||||
|
|
||||||
// 遍历模型,找到 fmz_2_jzf_bs015 并启用阴影
|
// 遍历模型所有子节点,配置阴影并查找告警设备
|
||||||
model.traverse((child) => {
|
model.traverse((child) => {
|
||||||
if (child instanceof THREE.Mesh) {
|
if (child instanceof THREE.Mesh) {
|
||||||
child.castShadow = true
|
child.castShadow = true // 投射阴影
|
||||||
child.receiveShadow = true
|
child.receiveShadow = true // 接收阴影
|
||||||
|
|
||||||
// 找到告警设备
|
// 查找名为 'fmz_2_jzf_bs015' 的告警设备
|
||||||
if (child.name === 'fmz_2_jzf_bs015') {
|
if (child.name === 'fmz_2_jzf_bs015') {
|
||||||
alertMesh.value = child
|
alertMesh.value = child
|
||||||
// 保存原始材质
|
// 备份原始材质(预留功能,当前版本使用覆盖层方式)
|
||||||
originalMaterial.value = child.material.clone()
|
originalMaterial.value = child.material.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,47 +151,57 @@ function initModel() {
|
|||||||
console.error('模型加载失败:', error)
|
console.error('模型加载失败:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画循环函数
|
||||||
|
* 每帧执行的逻辑:告警闪烁、相机动画、渲染更新
|
||||||
|
*/
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame(animate)
|
requestAnimationFrame(animate)
|
||||||
|
|
||||||
// 告警闪烁效果
|
// ========== 告警闪烁效果 ==========
|
||||||
|
// 使用正弦波函数实现红色覆盖层的透明度闪烁
|
||||||
if (isAlerting.value && alertOverlay.value) {
|
if (isAlerting.value && alertOverlay.value) {
|
||||||
alertTime += 0.05
|
alertTime += 0.05 // 累加时间
|
||||||
// 使用正弦波创建闪烁效果,频率较快
|
// sin(t*5) 产生快速震荡,映射到 0-1 范围
|
||||||
const intensity = (Math.sin(alertTime * 5) + 1) / 2 // 0-1之间震荡
|
const intensity = (Math.sin(alertTime * 5) + 1) / 2
|
||||||
|
|
||||||
if (alertOverlay.value.material instanceof THREE.Material) {
|
if (alertOverlay.value.material instanceof THREE.Material) {
|
||||||
const material = alertOverlay.value.material as THREE.MeshBasicMaterial
|
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) {
|
if (cameraAnimating && camera.value && controls.value) {
|
||||||
cameraAnimationProgress += 0.008 // 控制移动速度
|
cameraAnimationProgress += 0.008 // 每帧增加进度(0.8%)
|
||||||
|
|
||||||
|
// 动画完成检测
|
||||||
if (cameraAnimationProgress >= 1) {
|
if (cameraAnimationProgress >= 1) {
|
||||||
cameraAnimationProgress = 1
|
cameraAnimationProgress = 1
|
||||||
cameraAnimating = false
|
cameraAnimating = false
|
||||||
controls.value.enabled = true // 动画结束后恢复控制器
|
controls.value.enabled = true // 恢复用户控制
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用缓动函数使运动更平滑
|
// 使用缓动函数计算插值系数,使运动先慢后快再慢
|
||||||
const t = easeInOutCubic(cameraAnimationProgress)
|
const t = easeInOutCubic(cameraAnimationProgress)
|
||||||
|
|
||||||
// 插值相机位置
|
// 线性插值相机位置(起始位置 → 目标位置)
|
||||||
camera.value.position.lerpVectors(cameraStartPosition, cameraEndPosition, t)
|
camera.value.position.lerpVectors(cameraStartPosition, cameraEndPosition, t)
|
||||||
|
|
||||||
// 插值观察目标
|
// 线性插值相机观察目标(起始目标 → 设备位置)
|
||||||
const currentTarget = new THREE.Vector3()
|
const currentTarget = new THREE.Vector3()
|
||||||
currentTarget.lerpVectors(cameraStartTarget, cameraEndTarget, t)
|
currentTarget.lerpVectors(cameraStartTarget, cameraEndTarget, t)
|
||||||
controls.value.target.copy(currentTarget)
|
controls.value.target.copy(currentTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新轨道控制器(处理阻尼效果)
|
||||||
if (controls.value) {
|
if (controls.value) {
|
||||||
controls.value.update()
|
controls.value.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行渲染
|
||||||
if (renderer.value && scene.value && camera.value) {
|
if (renderer.value && scene.value && camera.value) {
|
||||||
renderer.value.render(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 {
|
function easeInOutCubic(t: number): number {
|
||||||
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2
|
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备报警处理
|
/**
|
||||||
|
* 设备报警处理函数
|
||||||
|
* 核心功能:
|
||||||
|
* 1. 切换告警状态
|
||||||
|
* 2. 创建/移除红色闪烁覆盖层
|
||||||
|
* 3. 触发相机平滑移动到告警设备
|
||||||
|
*/
|
||||||
function handleAlert() {
|
function handleAlert() {
|
||||||
|
// 前置检查:确保必要对象已初始化
|
||||||
if (!alertMesh.value || !camera.value || !controls.value) {
|
if (!alertMesh.value || !camera.value || !controls.value) {
|
||||||
console.warn('告警设备或相机未就绪')
|
console.warn('告警设备或相机未就绪')
|
||||||
return
|
return
|
||||||
@@ -211,29 +257,28 @@ function handleAlert() {
|
|||||||
isAlerting.value = !isAlerting.value
|
isAlerting.value = !isAlerting.value
|
||||||
|
|
||||||
if (isAlerting.value) {
|
if (isAlerting.value) {
|
||||||
// 开始告警
|
// ========== 开始告警 ==========
|
||||||
alertTime = 0
|
alertTime = 0 // 重置闪烁计时器
|
||||||
|
|
||||||
// 创建半透明红色覆盖层
|
// ========== 创建红色告警覆盖层 ==========
|
||||||
|
// 克隆设备几何体,创建完全贴合的覆盖网格
|
||||||
const geometry = alertMesh.value.geometry.clone()
|
const geometry = alertMesh.value.geometry.clone()
|
||||||
const overlayMaterial = new THREE.MeshBasicMaterial({
|
const overlayMaterial = new THREE.MeshBasicMaterial({
|
||||||
color: 0xFF0000,
|
color: 0xFF0000, // 红色
|
||||||
transparent: true,
|
transparent: true, // 启用透明度
|
||||||
opacity: 0.2,
|
opacity: 0.2, // 初始透明度
|
||||||
depthWrite: false,
|
depthWrite: false, // 关闭深度写入,避免遮挡问题
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide, // 双面渲染
|
||||||
})
|
})
|
||||||
|
|
||||||
alertOverlay.value = new THREE.Mesh(geometry, overlayMaterial)
|
alertOverlay.value = new THREE.Mesh(geometry, overlayMaterial)
|
||||||
// 复制告警物体的变换矩阵
|
// 完全复制告警物体的变换属性(位置、旋转、缩放)
|
||||||
alertOverlay.value.position.copy(alertMesh.value.position)
|
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.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) {
|
if (alertMesh.value.parent) {
|
||||||
alertMesh.value.parent.add(alertOverlay.value)
|
alertMesh.value.parent.add(alertOverlay.value)
|
||||||
}
|
}
|
||||||
@@ -241,45 +286,50 @@ function handleAlert() {
|
|||||||
scene.value?.add(alertOverlay.value)
|
scene.value?.add(alertOverlay.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算告警设备的世界坐标
|
// ========== 计算相机动画参数 ==========
|
||||||
|
// 获取设备在世界坐标系中的绝对位置
|
||||||
const worldPosition = new THREE.Vector3()
|
const worldPosition = new THREE.Vector3()
|
||||||
alertMesh.value.getWorldPosition(worldPosition)
|
alertMesh.value.getWorldPosition(worldPosition)
|
||||||
|
|
||||||
// 计算包围盒来获取设备的大小
|
// 计算设备包围盒,获取其尺寸
|
||||||
const box = new THREE.Box3().setFromObject(alertMesh.value)
|
const box = new THREE.Box3().setFromObject(alertMesh.value)
|
||||||
const size = box.getSize(new THREE.Vector3())
|
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 direction = new THREE.Vector3(-1.2, 1.2, -1.8).normalize()
|
||||||
const targetPosition = worldPosition.clone().add(direction.multiplyScalar(distance))
|
const targetPosition = worldPosition.clone().add(direction.multiplyScalar(distance))
|
||||||
|
|
||||||
// 保存当前相机状态
|
// 保存相机当前状态(动画起点)
|
||||||
cameraStartPosition.copy(camera.value.position)
|
cameraStartPosition.copy(camera.value.position)
|
||||||
cameraStartTarget.copy(controls.value.target)
|
cameraStartTarget.copy(controls.value.target)
|
||||||
|
|
||||||
// 设置目标状态 - 相机位置和观察目标
|
// 设置相机目标状态(动画终点)
|
||||||
cameraEndPosition.copy(targetPosition)
|
cameraEndPosition.copy(targetPosition) // 相机移动到计算位置
|
||||||
cameraEndTarget.copy(worldPosition) // 直接看向设备的世界坐标
|
cameraEndTarget.copy(worldPosition) // 相机看向设备中心
|
||||||
|
|
||||||
// 开始动画
|
// 启动相机动画
|
||||||
cameraAnimating = true
|
cameraAnimating = true
|
||||||
cameraAnimationProgress = 0
|
cameraAnimationProgress = 0 // 重置进度
|
||||||
controls.value.enabled = false // 动画期间禁用控制器
|
controls.value.enabled = false // 动画期间禁用用户控制
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 停止告警,移除覆盖层
|
// ========== 停止告警 ==========
|
||||||
|
// 清理告警覆盖层,释放内存
|
||||||
if (alertOverlay.value) {
|
if (alertOverlay.value) {
|
||||||
alertOverlay.value.parent?.remove(alertOverlay.value)
|
alertOverlay.value.parent?.remove(alertOverlay.value) // 从场景中移除
|
||||||
alertOverlay.value.geometry.dispose()
|
alertOverlay.value.geometry.dispose() // 释放几何体内存
|
||||||
if (alertOverlay.value.material instanceof THREE.Material) {
|
if (alertOverlay.value.material instanceof THREE.Material) {
|
||||||
alertOverlay.value.material.dispose()
|
alertOverlay.value.material.dispose() // 释放材质内存
|
||||||
}
|
}
|
||||||
alertOverlay.value = undefined
|
alertOverlay.value = undefined // 清空引用
|
||||||
}
|
}
|
||||||
|
// 注意:相机不会自动返回原位,用户可手动控制
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,149 +6,257 @@
|
|||||||
* @LastEditTime: 2025-11-21 09:04:18
|
* @LastEditTime: 2025-11-21 09:04:18
|
||||||
-->
|
-->
|
||||||
<script lang="ts" setup>
|
<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'
|
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
const threeRef = shallowRef<HTMLDivElement>()
|
/**
|
||||||
const isEmitting = ref(false)
|
* Three.js 基础对象引用
|
||||||
let scene: THREE.Scene
|
*/
|
||||||
let camera: THREE.PerspectiveCamera
|
const threeRef = shallowRef<HTMLDivElement>() // Three.js 容器DOM引用
|
||||||
let renderer: THREE.WebGLRenderer
|
const isEmitting = ref(false) // 粒子发射状态标志
|
||||||
let controls: OrbitControls
|
|
||||||
let animationId: number
|
|
||||||
const particles: THREE.Sprite[] = []
|
|
||||||
let particleTexture: THREE.Texture
|
|
||||||
|
|
||||||
|
// 场景核心对象(使用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() {
|
function init() {
|
||||||
const width = threeRef.value!.clientWidth
|
const width = threeRef.value!.clientWidth
|
||||||
const height = threeRef.value!.clientHeight
|
const height = threeRef.value!.clientHeight
|
||||||
|
|
||||||
|
// ========== 场景设置 ==========
|
||||||
|
// 创建黑色背景的3D场景,便于观察白色烟雾粒子
|
||||||
scene = new THREE.Scene()
|
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 = 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 = new THREE.WebGLRenderer({ antialias: true })
|
||||||
renderer.setSize(width, height)
|
renderer.setSize(width, height)
|
||||||
threeRef.value!.appendChild(renderer.domElement)
|
threeRef.value!.appendChild(renderer.domElement)
|
||||||
|
|
||||||
|
// ========== 轨道控制器 ==========
|
||||||
|
// 允许用户通过鼠标控制视角
|
||||||
controls = new OrbitControls(camera, renderer.domElement)
|
controls = new OrbitControls(camera, renderer.domElement)
|
||||||
|
|
||||||
|
// ========== 光照系统 ==========
|
||||||
|
// 添加环境光,提供基础照明
|
||||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5)
|
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5)
|
||||||
scene.add(ambientLight)
|
scene.add(ambientLight)
|
||||||
|
|
||||||
|
// ========== 创建粒子纹理 ==========
|
||||||
|
// 生成用于所有粒子的烟雾纹理(圆形径向渐变)
|
||||||
particleTexture = createSmokeTexture()
|
particleTexture = createSmokeTexture()
|
||||||
|
|
||||||
|
// 启动渲染循环
|
||||||
animate()
|
animate()
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
window.addEventListener('resize', onResize)
|
window.addEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建烟雾纹理
|
||||||
|
* 使用Canvas 2D API生成径向渐变纹理,模拟烟雾效果
|
||||||
|
* 纹理特点:
|
||||||
|
* - 中心不透明度高(白色)
|
||||||
|
* - 边缘完全透明
|
||||||
|
* - 形成柔和的圆形烟雾效果
|
||||||
|
*
|
||||||
|
* @returns THREE.Texture 烟雾纹理对象
|
||||||
|
*/
|
||||||
function createSmokeTexture() {
|
function createSmokeTexture() {
|
||||||
|
// 创建32x32的Canvas画布
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
canvas.width = 32
|
canvas.width = 32
|
||||||
canvas.height = 32
|
canvas.height = 32
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16)
|
|
||||||
grad.addColorStop(0, 'rgba(255, 255, 255, 1)')
|
// 创建径向渐变:从中心(16,16)向外扩散
|
||||||
grad.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
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.fillStyle = grad
|
||||||
ctx.fillRect(0, 0, 32, 32)
|
ctx.fillRect(0, 0, 32, 32)
|
||||||
|
|
||||||
|
// 将Canvas转换为Three.js纹理
|
||||||
const texture = new THREE.Texture(canvas)
|
const texture = new THREE.Texture(canvas)
|
||||||
texture.needsUpdate = true
|
texture.needsUpdate = true // 标记纹理需要上传到GPU
|
||||||
|
|
||||||
return texture
|
return texture
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换粒子发射状态
|
||||||
|
* 功能:
|
||||||
|
* - 开启时:每帧创建新粒子
|
||||||
|
* - 关闭时:清空所有现有粒子
|
||||||
|
*/
|
||||||
function toggleParticle() {
|
function toggleParticle() {
|
||||||
isEmitting.value = !isEmitting.value
|
isEmitting.value = !isEmitting.value
|
||||||
|
|
||||||
|
// 关闭粒子发射时,清空所有粒子
|
||||||
if (!isEmitting.value) {
|
if (!isEmitting.value) {
|
||||||
particles.forEach(p => scene.remove(p))
|
particles.forEach(p => scene.remove(p)) // 从场景中移除
|
||||||
particles.length = 0
|
particles.length = 0 // 清空数组
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画循环函数
|
||||||
|
* 每帧执行的逻辑:
|
||||||
|
* 1. 创建新粒子(如果发射开启)
|
||||||
|
* 2. 更新所有粒子的位置、大小、透明度
|
||||||
|
* 3. 移除生命周期结束的粒子
|
||||||
|
* 4. 渲染场景
|
||||||
|
*/
|
||||||
function animate() {
|
function animate() {
|
||||||
animationId = requestAnimationFrame(animate)
|
animationId = requestAnimationFrame(animate)
|
||||||
controls.update()
|
controls.update() // 更新轨道控制器
|
||||||
|
|
||||||
|
// ========== 粒子生成 ==========
|
||||||
|
// 如果发射开启,每帧创建一个新粒子
|
||||||
if (isEmitting.value) {
|
if (isEmitting.value) {
|
||||||
// Create new particle
|
// 创建粒子材质
|
||||||
const material = new THREE.SpriteMaterial({
|
const material = new THREE.SpriteMaterial({
|
||||||
map: particleTexture,
|
map: particleTexture, // 使用烟雾纹理
|
||||||
transparent: true,
|
transparent: true, // 启用透明度
|
||||||
opacity: 0.5,
|
opacity: 0.5, // 初始透明度
|
||||||
color: 0xAAAAAA,
|
color: 0xAAAAAA, // 浅灰色
|
||||||
depthWrite: false, // Important for transparency
|
depthWrite: false, // 关闭深度写入,避免粒子之间的遮挡问题
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 创建精灵粒子(始终面向相机的2D平面)
|
||||||
const particle = new THREE.Sprite(material)
|
const particle = new THREE.Sprite(material)
|
||||||
particle.position.set(0, 0, 0)
|
particle.position.set(0, 0, 0) // 初始位置在原点
|
||||||
particle.position.x = (Math.random() - 0.5) * 2
|
|
||||||
particle.position.z = (Math.random() - 0.5) * 2
|
// 在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(
|
;(particle as any).velocity = new THREE.Vector3(
|
||||||
(Math.random() - 0.5) * 0.05,
|
(Math.random() - 0.5) * 0.05, // X轴随机飘动:-0.025 到 0.025
|
||||||
0.1 + Math.random() * 0.1,
|
0.1 + Math.random() * 0.1, // Y轴向上运动:0.1 到 0.2
|
||||||
(Math.random() - 0.5) * 0.05,
|
(Math.random() - 0.5) * 0.05, // Z轴随机飘动:-0.025 到 0.025
|
||||||
)
|
)
|
||||||
;(particle as any).age = 0
|
;(particle as any).age = 0 // 粒子年龄(帧数)
|
||||||
|
|
||||||
scene.add(particle)
|
scene.add(particle)
|
||||||
particles.push(particle)
|
particles.push(particle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 粒子更新 ==========
|
||||||
|
// 从后向前遍历(便于删除元素)
|
||||||
for (let i = particles.length - 1; i >= 0; i--) {
|
for (let i = particles.length - 1; i >= 0; i--) {
|
||||||
const p = particles[i]
|
const p = particles[i]
|
||||||
if (!p)
|
if (!p)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
const velocity = (p as any).velocity
|
const velocity = (p as any).velocity
|
||||||
|
|
||||||
|
// 更新位置:根据速度向量移动
|
||||||
p.position.add(velocity)
|
p.position.add(velocity)
|
||||||
|
|
||||||
|
// 增加年龄
|
||||||
;(p as any).age++
|
;(p as any).age++
|
||||||
|
|
||||||
|
// 更新大小:随年龄增长而扩大(模拟烟雾扩散)
|
||||||
|
// 大小 = 1 + 年龄 × 0.05
|
||||||
p.scale.setScalar(1 + (p as any).age * 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
|
p.material.opacity = 0.5 - ((p as any).age / 100) * 0.5
|
||||||
|
|
||||||
|
// ========== 粒子生命周期结束 ==========
|
||||||
|
// 当粒子年龄超过100帧时,移除并释放资源
|
||||||
if ((p as any).age > 100) {
|
if ((p as any).age > 100) {
|
||||||
scene.remove(p)
|
scene.remove(p) // 从场景移除
|
||||||
particles.splice(i, 1)
|
particles.splice(i, 1) // 从数组移除
|
||||||
p.material.dispose() // Clean up
|
p.material.dispose() // 释放材质内存
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染场景
|
||||||
renderer.render(scene, camera)
|
renderer.render(scene, camera)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 窗口大小变化处理
|
||||||
|
* 更新相机宽高比和渲染器尺寸
|
||||||
|
*/
|
||||||
function onResize() {
|
function onResize() {
|
||||||
if (!threeRef.value)
|
if (!threeRef.value)
|
||||||
return
|
return
|
||||||
const width = threeRef.value.clientWidth
|
const width = threeRef.value.clientWidth
|
||||||
const height = threeRef.value.clientHeight
|
const height = threeRef.value.clientHeight
|
||||||
|
|
||||||
|
// 更新相机宽高比
|
||||||
camera.aspect = width / height
|
camera.aspect = width / height
|
||||||
camera.updateProjectionMatrix()
|
camera.updateProjectionMatrix() // 更新投影矩阵
|
||||||
|
|
||||||
|
// 更新渲染器尺寸
|
||||||
renderer.setSize(width, height)
|
renderer.setSize(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Vue生命周期 ==========
|
||||||
|
// 组件挂载时初始化场景
|
||||||
onMounted(init)
|
onMounted(init)
|
||||||
|
|
||||||
|
// 组件卸载时清理资源,防止内存泄漏
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
cancelAnimationFrame(animationId)
|
cancelAnimationFrame(animationId) // 取消动画帧
|
||||||
window.removeEventListener('resize', onResize)
|
window.removeEventListener('resize', onResize) // 移除事件监听
|
||||||
renderer.dispose()
|
renderer.dispose() // 释放渲染器资源
|
||||||
|
|
||||||
|
// 清理所有粒子
|
||||||
particles.forEach((p) => {
|
particles.forEach((p) => {
|
||||||
scene.remove(p)
|
scene.remove(p) // 从场景移除
|
||||||
p.material.dispose()
|
p.material.dispose() // 释放材质内存
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Three.js 渲染容器,占满整个视口 -->
|
||||||
<div ref="threeRef" class="w-full h-full" />
|
<div ref="threeRef" class="w-full h-full" />
|
||||||
|
|
||||||
|
<!-- 导航链接:跳转到下一个示例 -->
|
||||||
<router-link to="/three/10" class="position-fixed left-20px top-20px">
|
<router-link to="/three/10" class="position-fixed left-20px top-20px">
|
||||||
模型与Web的交互
|
模型与Web的交互
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<!-- 控制面板:粒子发射开关 -->
|
||||||
<div class="position-fixed right-20px top-20px flex flex-col gap-12px">
|
<div class="position-fixed right-20px top-20px flex flex-col gap-12px">
|
||||||
<div class="flex gap-12px">
|
<div class="flex gap-12px">
|
||||||
|
<!-- 切换按钮:控制蒸汽粒子的生成和停止 -->
|
||||||
<button @click="toggleParticle">
|
<button @click="toggleParticle">
|
||||||
{{ isEmitting ? '关闭' : '打开' }}粒子效果(蒸汽)
|
{{ isEmitting ? '关闭' : '打开' }}粒子效果(蒸汽)
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user