Files
DianZhanDemo/app/composables/powerStation/useThreeScene.ts
ch197511161 ddce8fce18 init5
2025-12-11 01:29:41 +08:00

470 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Three.js 场景管理系统
* 提供场景、相机、渲染器、控制器的初始化和管理
* 支持投影模式切换、标准视图和视角旋转
*/
import * as TWEEN from '@tweenjs/tween.js'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import type { RotateDirection, StandardViewType } from './types'
// ============================================================
// 配置常量
// ============================================================
/** 相机配置 */
const CAMERA_CONFIG = {
/** 透视相机视角 */
fov: 45,
/** 近裁剪面 */
near: 0.01,
/** 远裁剪面 */
far: 10000,
/** 初始位置 */
position: new THREE.Vector3(10, 10, 10),
} as const
/** 正交相机视锥体大小 */
const ORTHOGRAPHIC_FRUSTUM_SIZE = 20
/** 渲染器配置 */
const RENDERER_CONFIG = {
/** 抗锯齿 */
antialias: true,
/** 透明背景 */
alpha: true,
/** 阴影映射 */
shadowMap: false,
/** 色调映射 */
toneMapping: THREE.ACESFilmicToneMapping,
/** 曝光度 */
toneMappingExposure: 1.0,
} as const
/** 控制器配置 */
const CONTROLS_CONFIG = {
/** 阻尼效果 */
enableDamping: true,
/** 阻尼系数 */
dampingFactor: 0.05,
/** 允许缩放 */
enableZoom: true,
/** 允许平移 */
enablePan: true,
} as const
/** 动画配置 */
const ANIMATION_CONFIG = {
/** 视图切换动画时长(毫秒) */
viewTransitionDuration: 800,
/** 旋转动画时长(毫秒) */
rotationDuration: 500,
/** 旋转角度(弧度) */
rotateAngle: Math.PI / 8, // 22.5度
} as const
/** 场景背景色 */
const SCENE_BACKGROUND_COLOR = 0x1a1a1a
// ============================================================
// 主钩子函数
// ============================================================
/**
* Three.js 场景组合式函数
* 管理Three.js的完整生命周期
*/
export function useThreeScene() {
// 容器引用
const containerRef = ref<HTMLElement>()
// Three.js 核心实例
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let composer: EffectComposer
let animationId: number
let resizeObserver: ResizeObserver
let renderPass: RenderPass
// 响应式状态
const isOrthographic = ref(false)
// 回调钩子
const onAnimateCallbacks: (() => void)[] = []
const onResizeCallbacks: (() => void)[] = []
// ============================================================
// 初始化
// ============================================================
/**
* 初始化Three.js场景
*/
const initThree = () => {
if (!containerRef.value) return
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
// 创建场景
scene = new THREE.Scene()
scene.background = new THREE.Color(SCENE_BACKGROUND_COLOR)
// 创建透视相机
camera = new THREE.PerspectiveCamera(
CAMERA_CONFIG.fov,
width / height,
CAMERA_CONFIG.near,
CAMERA_CONFIG.far
)
camera.position.copy(CAMERA_CONFIG.position)
// 创建渲染器
renderer = new THREE.WebGLRenderer({
antialias: RENDERER_CONFIG.antialias,
alpha: RENDERER_CONFIG.alpha,
})
renderer.setSize(width, height)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = RENDERER_CONFIG.shadowMap
renderer.toneMapping = RENDERER_CONFIG.toneMapping
renderer.toneMappingExposure = RENDERER_CONFIG.toneMappingExposure
renderer.outputColorSpace = THREE.SRGBColorSpace
containerRef.value.appendChild(renderer.domElement)
// 创建控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = CONTROLS_CONFIG.enableDamping
controls.dampingFactor = CONTROLS_CONFIG.dampingFactor
controls.enableZoom = CONTROLS_CONFIG.enableZoom
controls.enablePan = CONTROLS_CONFIG.enablePan
// 创建后期处理合成器
composer = new EffectComposer(renderer)
composer.setPixelRatio(window.devicePixelRatio)
renderPass = new RenderPass(scene, camera)
composer.addPass(renderPass)
// 创建尺寸观察器
resizeObserver = new ResizeObserver(() => onWindowResize())
resizeObserver.observe(containerRef.value)
// 启动渲染循环
animate()
}
// ============================================================
// 渲染循环
// ============================================================
/**
* 渲染循环
*/
const animate = () => {
animationId = requestAnimationFrame(animate)
// 更新TWEEN动画
TWEEN.update()
// 更新控制器
if (controls) controls.update()
// 执行外部回调
onAnimateCallbacks.forEach(cb => cb())
// 渲染
if (composer) {
composer.render()
} else if (renderer && scene && camera) {
renderer.render(scene, camera)
}
}
// ============================================================
// 尺寸调整
// ============================================================
/**
* 窗口尺寸变化处理
*/
const onWindowResize = () => {
if (!containerRef.value || !camera || !renderer || !composer) return
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
// 根据相机类型更新投影矩阵
if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = width / height
camera.updateProjectionMatrix()
} else if (camera instanceof THREE.OrthographicCamera) {
const aspect = width / height
camera.left = (-ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2
camera.right = (ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2
camera.top = ORTHOGRAPHIC_FRUSTUM_SIZE / 2
camera.bottom = -ORTHOGRAPHIC_FRUSTUM_SIZE / 2
camera.updateProjectionMatrix()
}
// 更新渲染器和合成器尺寸
renderer.setSize(width, height)
composer.setSize(width, height)
composer.setPixelRatio(window.devicePixelRatio)
// 执行外部回调
onResizeCallbacks.forEach(cb => cb())
}
// ============================================================
// 投影模式切换
// ============================================================
/**
* 切换投影模式(透视/正交)
*/
const toggleProjection = () => {
if (!containerRef.value || !camera || !controls) return
const width = containerRef.value.clientWidth
const height = containerRef.value.clientHeight
const aspect = width / height
// 保存当前状态
const target = controls.target.clone()
const position = camera.position.clone()
if (isOrthographic.value) {
// 切换到透视相机
camera = new THREE.PerspectiveCamera(
CAMERA_CONFIG.fov,
aspect,
CAMERA_CONFIG.near,
CAMERA_CONFIG.far
)
camera.position.copy(position)
} else {
// 切换到正交相机
camera = new THREE.OrthographicCamera(
(-ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2,
(ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2,
ORTHOGRAPHIC_FRUSTUM_SIZE / 2,
-ORTHOGRAPHIC_FRUSTUM_SIZE / 2,
CAMERA_CONFIG.near,
CAMERA_CONFIG.far
)
camera.position.copy(position)
camera.zoom = 1
}
camera.lookAt(target)
// 更新控制器和渲染通道
controls.object = camera
controls.update()
if (renderPass) {
renderPass.camera = camera
}
isOrthographic.value = !isOrthographic.value
onWindowResize() // 确保投影矩阵正确
}
// ============================================================
// 视图控制
// ============================================================
/**
* 设置标准视图
* @param view - 视图类型
*/
const setStandardView = (view: StandardViewType) => {
if (!camera || !controls) return
const target = controls.target.clone()
const distance = camera.position.distanceTo(target)
const newPosition = target.clone()
// 根据视图类型计算新位置
switch (view) {
case 'top':
newPosition.y += distance
break
case 'bottom':
newPosition.y -= distance
break
case 'front':
newPosition.z += distance
break
case 'back':
newPosition.z -= distance
break
case 'left':
newPosition.x -= distance
break
case 'right':
newPosition.x += distance
break
case 'iso': {
const isoDist = distance / Math.sqrt(3)
newPosition.set(target.x + isoDist, target.y + isoDist, target.z + isoDist)
break
}
}
// 动画过渡
new TWEEN.Tween(camera.position)
.to(newPosition, ANIMATION_CONFIG.viewTransitionDuration)
.easing(TWEEN.Easing.Cubic.Out)
.onUpdate(() => {
camera.lookAt(target)
controls.update()
})
.start()
}
/**
* 旋转视图
* @param direction - 旋转方向
*/
const rotateView = (direction: RotateDirection) => {
if (!controls) return
const offset = new THREE.Vector3().subVectors(camera.position, controls.target)
const spherical = new THREE.Spherical().setFromVector3(offset)
const rotateAngle = ANIMATION_CONFIG.rotateAngle
// 根据方向调整球坐标
switch (direction) {
case 'left':
spherical.theta -= rotateAngle
break
case 'right':
spherical.theta += rotateAngle
break
case 'up':
spherical.phi -= rotateAngle
break
case 'down':
spherical.phi += rotateAngle
break
case 'upleft':
spherical.theta -= rotateAngle
spherical.phi -= rotateAngle
break
case 'upright':
spherical.theta += rotateAngle
spherical.phi -= rotateAngle
break
case 'downleft':
spherical.theta -= rotateAngle
spherical.phi += rotateAngle
break
case 'downright':
spherical.theta += rotateAngle
spherical.phi += rotateAngle
break
}
// 限制phi范围避免翻转
spherical.phi = Math.max(0.01, Math.min(Math.PI - 0.01, spherical.phi))
const newPosition = new THREE.Vector3().setFromSpherical(spherical).add(controls.target)
// 动画过渡
new TWEEN.Tween(camera.position)
.to(newPosition, ANIMATION_CONFIG.rotationDuration)
.easing(TWEEN.Easing.Cubic.Out)
.onUpdate(() => {
camera.lookAt(controls.target)
controls.update()
})
.start()
}
// ============================================================
// 回调注册
// ============================================================
/**
* 添加渲染循环回调
* @param cb - 回调函数
*/
const addAnimateCallback = (cb: () => void) => {
onAnimateCallbacks.push(cb)
}
/**
* 添加尺寸变化回调
* @param cb - 回调函数
*/
const addResizeCallback = (cb: () => void) => {
onResizeCallbacks.push(cb)
}
// ============================================================
// 清理
// ============================================================
/**
* 清理资源
*/
const dispose = () => {
cancelAnimationFrame(animationId)
if (resizeObserver) resizeObserver.disconnect()
if (renderer) {
renderer.dispose()
if (containerRef.value && renderer.domElement) {
containerRef.value.removeChild(renderer.domElement)
}
}
if (composer) composer.dispose()
}
// 组件卸载时清理
onUnmounted(() => {
dispose()
})
// ============================================================
// 导出接口
// ============================================================
return {
// 引用
containerRef,
// 初始化
initThree,
// 获取器
getScene: () => scene,
getCamera: () => camera,
getRenderer: () => renderer,
getControls: () => controls,
getComposer: () => composer,
// 回调注册
addAnimateCallback,
addResizeCallback,
// 尺寸调整
onWindowResize,
// 投影控制
isOrthographic,
toggleProjection,
// 视图控制
setStandardView,
rotateView,
}
}