470 lines
13 KiB
TypeScript
470 lines
13 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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,
|
|||
|
|
}
|
|||
|
|
}
|