Files
DianZhanDemo/app/composables/powerStation/useThreeScene.ts

470 lines
13 KiB
TypeScript
Raw Permalink Normal View History

2025-12-11 01:29:41 +08:00
/**
* 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,
}
}