/** * 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() // 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, } }