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

552 lines
17 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.

/**
* 模型管理系统
* 处理3D模型的加载、层级结构生成、材质处理和相机适配
*/
import * as TWEEN from '@tweenjs/tween.js'
import * as THREE from 'three'
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
import { usePowerStationStore } from '~/stores/powerStation'
import { useFullMode } from './modes/useFullMode'
import { useSimplifiedMode } from './modes/useSimplifiedMode'
import type { BreadcrumbItem, LightingMode, ModelHierarchyNode } from './types'
// ============================================================
// 配置常量
// ============================================================
/** 光源配置 */
const LIGHT_CONFIG = {
/** 环境光颜色 */
ambientColor: 0xffffff,
/** 环境光强度 */
ambientIntensity: 0.6,
/** 方向光颜色 */
directionalColor: 0xffffff,
/** 方向光强度 */
directionalIntensity: 1,
/** 方向光位置 */
directionalPosition: new THREE.Vector3(5, 3, 5),
} as const
/** PBR材质配置 */
const PBR_MATERIAL_CONFIG = {
/** 金属度 */
metalness: 0.6,
/** 粗糙度 */
roughness: 0.6,
/** 环境贴图强度 */
envMapIntensity: 1.5,
} as const
/** 相机动画配置 */
const CAMERA_ANIMATION_CONFIG = {
/** 动画时长(毫秒) */
duration: 900,
/** 距离系数 */
distanceMultiplier: 1.2,
} as const
// ============================================================
// 主钩子函数
// ============================================================
/**
* 模型管理组合式函数
* @param getScene - 获取场景
* @param getCamera - 获取相机
* @param getControls - 获取控制器
* @param getRenderer - 获取渲染器
*/
export function useModelManager(
getScene: () => THREE.Scene,
getCamera: () => THREE.PerspectiveCamera,
getControls: () => any, // OrbitControls
getRenderer: () => THREE.WebGLRenderer
) {
const store = usePowerStationStore()
const isLoading = ref(true)
// ============================================================
// 模型数据
// ============================================================
// 使用 markRaw 标记 Three.js 对象为非响应式,防止 Vue DevTools 追踪导致性能问题
const modelGroup = markRaw(new THREE.Group())
const archetypeModel = shallowRef<THREE.Object3D | null>(null)
// 使用普通 MapThree.js 对象已是 markRawMap 本身不需要响应式)
const objectMap = new Map<string, THREE.Object3D>()
const modelCache = new Map<string, THREE.Object3D>()
// 环境贴图
let envTexture: THREE.Texture | null = null
// 光照模式
const lightingMode = ref<LightingMode>('basic')
// ============================================================
// 初始化
// ============================================================
/**
* 初始化模型系统
* 添加模型组和基础光源到场景
*/
const initModelSystem = () => {
const scene = getScene()
if (!scene) return
// 添加模型组
scene.add(modelGroup)
// 添加基础光源
const ambientLight = new THREE.AmbientLight(
LIGHT_CONFIG.ambientColor,
LIGHT_CONFIG.ambientIntensity
)
scene.add(ambientLight)
const dirLight = new THREE.DirectionalLight(
LIGHT_CONFIG.directionalColor,
LIGHT_CONFIG.directionalIntensity
)
dirLight.position.copy(LIGHT_CONFIG.directionalPosition)
scene.add(dirLight)
}
/**
* 初始化环境贴图
*/
const initEnvironment = () => {
const renderer = getRenderer()
const scene = getScene()
if (!renderer || !scene) return
// 避免重复创建
if (envTexture) return
const pmremGenerator = new THREE.PMREMGenerator(renderer)
pmremGenerator.compileEquirectangularShader()
const roomEnvironment = new RoomEnvironment()
envTexture = pmremGenerator.fromScene(roomEnvironment).texture
if (lightingMode.value === 'advanced') {
scene.environment = envTexture
}
// 清理临时对象
roomEnvironment.dispose()
pmremGenerator.dispose()
}
// ============================================================
// 层级结构生成
// ============================================================
/**
* 生成模型层级结构
* @param object - Three.js 对象
* @param currentPath - 当前路径
* @param depth - 当前深度
* @param maxDepth - 最大深度
* @returns 层级节点
*/
const generateHierarchy = (
object: THREE.Object3D,
currentPath: string,
depth: number = 1,
maxDepth: number = Infinity
): ModelHierarchyNode => {
const isRoot = object === archetypeModel.value
const nodeId = isRoot ? store.currentNodeId || currentPath : currentPath
// 设置 userData.id
object.userData.id = nodeId
const node: ModelHierarchyNode = {
id: nodeId,
label: isRoot
? store.currentNodeId || object.name || `节点 ${nodeId}`
: object.name || `节点 ${nodeId}`,
children: [],
}
// 检查是否为完整物体有名称且包含多个子Mesh
const isCompleteObject =
object.name && object.children.filter(child => child.type === 'Mesh').length > 1
// 达到最大深度则停止递归
if (depth >= maxDepth) {
return node
}
// 递归处理子节点
object.children.forEach(child => {
if (child.type === 'Mesh' || child.type === 'Group' || child.type === 'Object3D') {
const childName = child.name || '子对象'
const childPath = `${currentPath}~${childName}`
// 跳过完整物体中的无名Mesh子节点
if (isCompleteObject && child.type === 'Mesh' && !child.name) {
return
}
const childNode = generateHierarchy(child, childPath, depth + 1, maxDepth)
node.children.push(childNode)
}
})
return node
}
// ============================================================
// 节点查找
// ============================================================
/**
* 通过ID查找节点
* @param root - 根对象
* @param id - 节点ID
* @returns 找到的节点或null
*/
const findNodeById = (root: THREE.Object3D, id: string): THREE.Object3D | null => {
if (root.userData.id === id) return root
for (const child of root.children) {
const found = findNodeById(child, id)
if (found) return found
}
return null
}
/**
* 重建物体映射表
* @param root - 根对象
*/
const rebuildObjectMap = (root: THREE.Object3D) => {
if (root.userData.id) {
objectMap.set(root.userData.id, root)
}
root.children.forEach(child => rebuildObjectMap(child))
}
// ============================================================
// 面包屑生成
// ============================================================
/**
* 生成面包屑导航数据
* @param nodeId - 节点ID
* @returns 面包屑数组
*/
const generateBreadcrumbs = (nodeId: string): BreadcrumbItem[] => {
if (!archetypeModel.value) return []
const breadcrumbsList: BreadcrumbItem[] = []
const nodeIdParts = nodeId.split('~')
if (nodeIdParts.length > 0) {
let currentPath = ''
for (let i = 0; i < nodeIdParts.length; i++) {
const part = nodeIdParts[i]
currentPath = i === 0 ? part : `${currentPath}~${part}`
const currentObject = findNodeById(archetypeModel.value, currentPath)
const displayText = currentObject
? currentObject.name
: part || findNodeById(archetypeModel.value, currentPath)?.name || part
const item: BreadcrumbItem = { text: displayText }
if (currentPath !== nodeId) {
item.to = `?currentNodeId=${currentPath}`
}
breadcrumbsList.push(item)
}
}
// 使用当前节点ID的第一部分作为面包屑首项
if (breadcrumbsList.length > 0 && store.currentNodeId) {
const firstPart = store.currentNodeId.split('~')[0]
breadcrumbsList[0].text = firstPart
}
return breadcrumbsList
}
// ============================================================
// 相机适配
// ============================================================
/**
* 适配视图到目标对象
* @param targetObj - 目标对象默认为modelGroup
* @param keepOrientation - 是否保持当前相机朝向
*/
const fitView = (targetObj?: THREE.Object3D, keepOrientation: boolean = true) => {
const obj = targetObj || modelGroup
const camera = getCamera()
const controls = getControls()
console.log('[fitView] 开始执行:', {
hasTargetObj: !!targetObj,
objName: obj?.name,
objChildrenCount: obj?.children?.length,
hasCamera: !!camera,
hasControls: !!controls,
})
if (!obj || !camera || !controls) {
console.warn('[fitView] 提前返回: obj/camera/controls 缺失')
return
}
// 计算包围盒
const box = new THREE.Box3().setFromObject(obj)
console.log('[fitView] 包围盒:', {
isEmpty: box.isEmpty(),
min: box.min.toArray(),
max: box.max.toArray(),
})
if (box.isEmpty()) {
console.warn('[fitView] 提前返回: 包围盒为空')
return
}
const size = box.getSize(new THREE.Vector3())
const center = box.getCenter(new THREE.Vector3())
// 计算适配距离
const maxSize = Math.max(size.x, size.y, size.z)
const fitHeightDistance = maxSize / (2 * Math.atan((Math.PI * camera.fov) / 360))
const fitWidthDistance = fitHeightDistance / camera.aspect
const distance = CAMERA_ANIMATION_CONFIG.distanceMultiplier * Math.max(fitHeightDistance, fitWidthDistance)
// 计算相机方向
let direction: THREE.Vector3
if (keepOrientation) {
direction = new THREE.Vector3().subVectors(camera.position, controls.target).normalize()
if (direction.lengthSq() < 0.0001) {
direction = new THREE.Vector3(1, 1, 1).normalize()
}
} else {
direction = new THREE.Vector3(1, 1, 1).normalize()
}
const targetPosition = center.clone().add(direction.multiplyScalar(distance))
console.log('[fitView] 动画参数:', {
size: size.toArray(),
center: center.toArray(),
distance,
currentCameraPos: camera.position.toArray(),
targetCameraPos: targetPosition.toArray(),
})
// 相机位置动画
new TWEEN.Tween(camera.position)
.to({ x: targetPosition.x, y: targetPosition.y, z: targetPosition.z }, CAMERA_ANIMATION_CONFIG.duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onStart(() => console.log('[fitView] 相机位置动画开始'))
.onComplete(() => console.log('[fitView] 相机位置动画完成'))
.start()
// 控制目标动画
new TWEEN.Tween(controls.target)
.to({ x: center.x, y: center.y, z: center.z }, CAMERA_ANIMATION_CONFIG.duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onStart(() => console.log('[fitView] 控制目标动画开始'))
.onUpdate(() => {
controls.update()
})
.onComplete(() => console.log('[fitView] 控制目标动画完成'))
.start()
}
// ============================================================
// 材质处理
// ============================================================
/**
* 替换为PBR材质
* @param object - 要处理的对象
*/
const replaceWithPBRMaterial = (object: THREE.Object3D) => {
object.traverse(child => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
const newMaterials: THREE.MeshStandardMaterial[] = []
materials.forEach(mat => {
const color =
(mat as THREE.MeshBasicMaterial).color instanceof THREE.Color
? (mat as THREE.MeshBasicMaterial).color.clone()
: new THREE.Color(0xaaaaaa)
// 获取原始贴图
const originalMap = (mat as THREE.MeshBasicMaterial).map || null
const alphaMap = originalMap || null
const useAlpha = originalMap !== null
const pbrMaterial = new THREE.MeshStandardMaterial({
color: color,
metalness: PBR_MATERIAL_CONFIG.metalness,
roughness: PBR_MATERIAL_CONFIG.roughness,
transparent: useAlpha || ((mat as THREE.MeshBasicMaterial).transparent ?? false),
opacity: useAlpha ? 1 : ((mat as THREE.MeshBasicMaterial).opacity ?? 1),
side: mat.side || THREE.FrontSide,
map: originalMap,
alphaMap: alphaMap,
normalMap: (mat as THREE.MeshStandardMaterial).normalMap || null,
envMapIntensity: PBR_MATERIAL_CONFIG.envMapIntensity,
})
newMaterials.push(pbrMaterial)
})
if (newMaterials.length > 0) {
mesh.material = (Array.isArray(mesh.material) ? newMaterials : newMaterials[0]) as
| THREE.Material
| THREE.Material[]
}
}
})
}
/**
* 更新材质模式
* @param root - 根对象
* @param mode - 光照模式
*/
const updateMaterials = (root: THREE.Object3D, mode: LightingMode) => {
root.traverse(child => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
const newMaterials = materials.map(mat => {
if (mode === 'basic') {
// 切换到基础模式
if (mat.type === 'MeshLambertMaterial') return mat
if (mat.userData.lambertPartner) return mat.userData.lambertPartner
const source = mat as THREE.MeshStandardMaterial
const lambert = new THREE.MeshLambertMaterial({
color: source.color,
map: source.map,
transparent: source.transparent,
opacity: source.opacity,
side: source.side,
alphaMap: source.alphaMap,
aoMap: source.aoMap,
})
mat.userData.lambertPartner = lambert
lambert.userData.pbrPartner = mat
return lambert
} else {
// 切换到高级模式
if (mat.type === 'MeshStandardMaterial') return mat
if (mat.userData.pbrPartner) return mat.userData.pbrPartner
return mat
}
})
mesh.material = Array.isArray(mesh.material) ? newMaterials : newMaterials[0]
}
})
}
// ============================================================
// 光照模式控制
// ============================================================
/**
* 切换光照模式
*/
const toggleLighting = () => {
lightingMode.value = lightingMode.value === 'basic' ? 'advanced' : 'basic'
applyLightingMode()
}
/**
* 应用当前光照模式
*/
const applyLightingMode = () => {
const scene = getScene()
if (!scene) return
if (lightingMode.value === 'advanced') {
if (envTexture) scene.environment = envTexture
} else {
scene.environment = null
}
if (modelGroup) updateMaterials(modelGroup, lightingMode.value)
if (archetypeModel.value) updateMaterials(archetypeModel.value, lightingMode.value)
}
// ============================================================
// 模式上下文
// ============================================================
/** 供加载模式使用的共享上下文 */
const context = {
store,
isLoading,
archetypeModel,
modelGroup,
objectMap,
modelCache,
replaceWithPBRMaterial,
generateHierarchy,
initEnvironment,
applyLightingMode,
fitView,
findNodeById,
rebuildObjectMap,
generateBreadcrumbs,
}
// 实例化加载模式
const { loadModel, processModel } = useFullMode(context)
const { loadModelByFileName } = useSimplifiedMode(context)
// ============================================================
// 导出接口
// ============================================================
return {
// 数据
modelGroup,
isLoading,
lightingMode,
objectMap,
archetypeModel,
// 初始化
initModelSystem,
// 模型加载
loadModel,
processModel,
loadModelByFileName,
// 视图控制
fitView,
// 光照控制
toggleLighting,
// 工具函数
findNodeById,
generateBreadcrumbs,
}
}