364 lines
10 KiB
TypeScript
364 lines
10 KiB
TypeScript
|
|
/**
|
|||
|
|
* 完整模式 - 模型加载
|
|||
|
|
* 支持完整的模型层级结构和合并优化
|
|||
|
|
*/
|
|||
|
|
import { ElMessage } from 'element-plus'
|
|||
|
|
import * as THREE from 'three'
|
|||
|
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
|
|||
|
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|||
|
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|||
|
|
import { markRaw } from 'vue'
|
|||
|
|
import type { ModelModeContext } from '../types'
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 配置常量
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
/** DRACO解码器路径 */
|
|||
|
|
const DRACO_DECODER_PATH = 'https://www.gstatic.com/draco/v1/decoders/'
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 辅助函数
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从URL提取文件名(不含路径和扩展名)
|
|||
|
|
* @param url - 文件URL
|
|||
|
|
* @returns 文件名
|
|||
|
|
*/
|
|||
|
|
const extractFileName = (url: string): string => {
|
|||
|
|
const urlParts = url.split('/')
|
|||
|
|
const fileNameWithExt = urlParts[urlParts.length - 1]
|
|||
|
|
const fileName = fileNameWithExt.split('.').slice(0, -1).join('.')
|
|||
|
|
return fileName || '模型'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 递归打印模型中所有物体的路径(用于调试)
|
|||
|
|
* @param object - Three.js 对象
|
|||
|
|
* @param path - 当前路径
|
|||
|
|
*/
|
|||
|
|
const printModelObjectPaths = (object: THREE.Object3D, path: string = '') => {
|
|||
|
|
const fullPath = path ? `${path}/${object.name}` : object.name
|
|||
|
|
object.children.forEach(child => {
|
|||
|
|
printModelObjectPaths(child, fullPath)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 几何体合并
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 合并子树中的所有网格
|
|||
|
|
* 用于优化渲染性能
|
|||
|
|
* @param root - 要合并的根对象
|
|||
|
|
*/
|
|||
|
|
const mergeSubTree = (root: THREE.Object3D) => {
|
|||
|
|
// 收集所有网格
|
|||
|
|
const meshes: THREE.Mesh[] = []
|
|||
|
|
root.traverse(child => {
|
|||
|
|
if ((child as THREE.Mesh).isMesh) {
|
|||
|
|
meshes.push(child as THREE.Mesh)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 检查是否需要合并
|
|||
|
|
if (meshes.length === 0) return
|
|||
|
|
if (meshes.length === 1 && root === meshes[0] && root.children.length === 0) return
|
|||
|
|
|
|||
|
|
const geometries: THREE.BufferGeometry[] = []
|
|||
|
|
const materials: THREE.Material[] = []
|
|||
|
|
const materialIndexMap = new Map<string, number>()
|
|||
|
|
|
|||
|
|
// 更新世界矩阵
|
|||
|
|
root.updateMatrixWorld(true)
|
|||
|
|
const rootInverse = root.matrixWorld.clone().invert()
|
|||
|
|
|
|||
|
|
// 检查UV属性兼容性
|
|||
|
|
const hasUVAttribute = meshes.some(mesh => mesh.geometry.attributes.uv)
|
|||
|
|
const allHaveUV = meshes.every(mesh => mesh.geometry.attributes.uv)
|
|||
|
|
|
|||
|
|
if (hasUVAttribute && !allHaveUV) {
|
|||
|
|
console.warn(`节点 ${root.name} 的几何体UV属性不一致,将移除UV属性以确保合并成功`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理每个网格
|
|||
|
|
meshes.forEach(mesh => {
|
|||
|
|
const geom = mesh.geometry.clone()
|
|||
|
|
mesh.updateMatrixWorld(true)
|
|||
|
|
const matrix = mesh.matrixWorld.clone().premultiply(rootInverse)
|
|||
|
|
geom.applyMatrix4(matrix)
|
|||
|
|
|
|||
|
|
// 重新计算法线
|
|||
|
|
if (geom.attributes.normal) {
|
|||
|
|
delete geom.attributes.normal
|
|||
|
|
}
|
|||
|
|
geom.computeVertexNormals()
|
|||
|
|
|
|||
|
|
// 处理UV属性兼容性
|
|||
|
|
if (hasUVAttribute && !allHaveUV && geom.attributes.uv) {
|
|||
|
|
delete geom.attributes.uv
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const meshMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
|||
|
|
|
|||
|
|
// 处理几何体组
|
|||
|
|
if (!geom.groups || geom.groups.length === 0) {
|
|||
|
|
geom.clearGroups()
|
|||
|
|
if (geom.attributes.position) {
|
|||
|
|
geom.addGroup(0, geom.attributes.position.count, 0)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重新映射材质索引
|
|||
|
|
const newGroups = []
|
|||
|
|
for (const group of geom.groups) {
|
|||
|
|
const originalMat = meshMaterials[group.materialIndex || 0]
|
|||
|
|
if (!originalMat) continue
|
|||
|
|
|
|||
|
|
const matUuid = originalMat.uuid
|
|||
|
|
let newMatIndex = materialIndexMap.get(matUuid)
|
|||
|
|
|
|||
|
|
if (newMatIndex === undefined) {
|
|||
|
|
newMatIndex = materials.length
|
|||
|
|
materials.push(originalMat)
|
|||
|
|
materialIndexMap.set(matUuid, newMatIndex)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
newGroups.push({
|
|||
|
|
start: group.start,
|
|||
|
|
count: group.count,
|
|||
|
|
materialIndex: newMatIndex,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
geom.groups = newGroups
|
|||
|
|
geometries.push(geom)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 执行合并
|
|||
|
|
if (geometries.length > 0) {
|
|||
|
|
try {
|
|||
|
|
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries, true)
|
|||
|
|
|
|||
|
|
// 重新计算法线
|
|||
|
|
if (mergedGeometry.attributes.normal) {
|
|||
|
|
mergedGeometry.computeVertexNormals()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mergedMesh = new THREE.Mesh(mergedGeometry, materials)
|
|||
|
|
mergedMesh.name = root.name
|
|||
|
|
mergedMesh.userData = { ...root.userData }
|
|||
|
|
|
|||
|
|
// 替换原对象
|
|||
|
|
const parent = root.parent
|
|||
|
|
if (parent) {
|
|||
|
|
mergedMesh.position.copy(root.position)
|
|||
|
|
mergedMesh.rotation.copy(root.rotation)
|
|||
|
|
mergedMesh.scale.copy(root.scale)
|
|||
|
|
parent.remove(root)
|
|||
|
|
parent.add(mergedMesh)
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.warn('合并节点失败:', root.name, e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 主钩子函数
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 完整模式组合式函数
|
|||
|
|
* @param context - 模型模式上下文
|
|||
|
|
*/
|
|||
|
|
export function useFullMode(context: ModelModeContext) {
|
|||
|
|
const {
|
|||
|
|
store,
|
|||
|
|
isLoading,
|
|||
|
|
archetypeModel,
|
|||
|
|
modelGroup,
|
|||
|
|
objectMap,
|
|||
|
|
modelCache,
|
|||
|
|
replaceWithPBRMaterial,
|
|||
|
|
generateHierarchy,
|
|||
|
|
initEnvironment,
|
|||
|
|
applyLightingMode,
|
|||
|
|
fitView,
|
|||
|
|
findNodeById,
|
|||
|
|
rebuildObjectMap,
|
|||
|
|
generateBreadcrumbs,
|
|||
|
|
} = context
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 模型加载
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 加载模型
|
|||
|
|
* @param url - 模型URL
|
|||
|
|
* @param onLoaded - 加载完成回调
|
|||
|
|
*/
|
|||
|
|
const loadModel = (url: string, onLoaded?: () => void) => {
|
|||
|
|
isLoading.value = true
|
|||
|
|
store.isModelLoading = true
|
|||
|
|
|
|||
|
|
const fileName = extractFileName(url)
|
|||
|
|
|
|||
|
|
// 检查缓存
|
|||
|
|
if (store.modelCacheEnabled && modelCache.has(fileName)) {
|
|||
|
|
console.log('从缓存加载模型:', fileName)
|
|||
|
|
const cachedModel = modelCache.get(fileName)
|
|||
|
|
|
|||
|
|
// 使用缓存的模型
|
|||
|
|
const clonedModel = cachedModel!.clone()
|
|||
|
|
replaceWithPBRMaterial(clonedModel)
|
|||
|
|
archetypeModel.value = markRaw(clonedModel)
|
|||
|
|
|
|||
|
|
// 生成层级结构
|
|||
|
|
const rootPath = store.currentNodeId
|
|||
|
|
const hierarchy = generateHierarchy(archetypeModel.value, rootPath)
|
|||
|
|
store.modelHierarchy = [hierarchy]
|
|||
|
|
|
|||
|
|
initEnvironment()
|
|||
|
|
applyLightingMode()
|
|||
|
|
|
|||
|
|
isLoading.value = false
|
|||
|
|
store.isModelLoading = false
|
|||
|
|
|
|||
|
|
if (onLoaded) onLoaded()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建加载器
|
|||
|
|
const loader = new GLTFLoader()
|
|||
|
|
const dracoLoader = new DRACOLoader()
|
|||
|
|
dracoLoader.setDecoderPath(DRACO_DECODER_PATH)
|
|||
|
|
loader.setDRACOLoader(dracoLoader)
|
|||
|
|
|
|||
|
|
loader.load(
|
|||
|
|
url,
|
|||
|
|
gltf => {
|
|||
|
|
replaceWithPBRMaterial(gltf.scene)
|
|||
|
|
|
|||
|
|
// 缓存模型
|
|||
|
|
if (store.modelCacheEnabled) {
|
|||
|
|
modelCache.set(fileName, gltf.scene.clone())
|
|||
|
|
console.log('模型已缓存:', fileName)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置原型模型
|
|||
|
|
archetypeModel.value = markRaw(gltf.scene)
|
|||
|
|
|
|||
|
|
// 生成层级结构
|
|||
|
|
const rootPath = store.currentNodeId
|
|||
|
|
const hierarchy = generateHierarchy(archetypeModel.value, rootPath)
|
|||
|
|
store.modelHierarchy = [hierarchy]
|
|||
|
|
|
|||
|
|
initEnvironment()
|
|||
|
|
applyLightingMode()
|
|||
|
|
|
|||
|
|
isLoading.value = false
|
|||
|
|
store.isModelLoading = false
|
|||
|
|
dracoLoader.dispose()
|
|||
|
|
|
|||
|
|
if (onLoaded) onLoaded()
|
|||
|
|
},
|
|||
|
|
undefined,
|
|||
|
|
error => {
|
|||
|
|
console.error('模型加载错误:', error)
|
|||
|
|
ElMessage.error('模型加载失败,请检查网络连接或模型文件是否存在')
|
|||
|
|
isLoading.value = false
|
|||
|
|
store.isModelLoading = false
|
|||
|
|
dracoLoader.dispose()
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 模型处理
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理模型(核心逻辑)
|
|||
|
|
* 合并子节点并更新场景
|
|||
|
|
* @param nodeId - 节点ID
|
|||
|
|
* @param shouldFitView - 是否适配视图
|
|||
|
|
*/
|
|||
|
|
const processModel = (nodeId: string, shouldFitView: boolean = false) => {
|
|||
|
|
if (!archetypeModel.value) return
|
|||
|
|
|
|||
|
|
// 克隆模型
|
|||
|
|
const newModel = archetypeModel.value.clone()
|
|||
|
|
newModel.name = store.currentNodeId || '模型'
|
|||
|
|
|
|||
|
|
// 查找目标节点
|
|||
|
|
let targetNode = findNodeById(newModel, nodeId)
|
|||
|
|
if (!targetNode) {
|
|||
|
|
console.warn(`节点 ${nodeId} 未找到,回退到根节点`)
|
|||
|
|
targetNode = newModel
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 合并子节点
|
|||
|
|
const childrenToMerge = [...targetNode.children]
|
|||
|
|
childrenToMerge.forEach(child => {
|
|||
|
|
mergeSubTree(child)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 清空模型组
|
|||
|
|
modelGroup.clear()
|
|||
|
|
|
|||
|
|
// 计算世界变换
|
|||
|
|
newModel.updateMatrixWorld(true)
|
|||
|
|
const worldPosition = new THREE.Vector3()
|
|||
|
|
const worldQuaternion = new THREE.Quaternion()
|
|||
|
|
const worldScale = new THREE.Vector3()
|
|||
|
|
|
|||
|
|
targetNode.getWorldPosition(worldPosition)
|
|||
|
|
targetNode.getWorldQuaternion(worldQuaternion)
|
|||
|
|
targetNode.getWorldScale(worldScale)
|
|||
|
|
|
|||
|
|
// 添加到模型组
|
|||
|
|
modelGroup.add(targetNode)
|
|||
|
|
|
|||
|
|
// 应用世界变换
|
|||
|
|
targetNode.position.copy(worldPosition)
|
|||
|
|
targetNode.quaternion.copy(worldQuaternion)
|
|||
|
|
targetNode.scale.copy(worldScale)
|
|||
|
|
|
|||
|
|
// 重建物体映射
|
|||
|
|
objectMap.clear()
|
|||
|
|
rebuildObjectMap(targetNode)
|
|||
|
|
|
|||
|
|
// 更新层级结构
|
|||
|
|
const newHierarchy = generateHierarchy(targetNode, targetNode.userData.id)
|
|||
|
|
store.modelHierarchy = [newHierarchy]
|
|||
|
|
|
|||
|
|
// 生成面包屑(返回供调用者处理)
|
|||
|
|
const breadcrumbs = generateBreadcrumbs(nodeId)
|
|||
|
|
|
|||
|
|
// 适配视图
|
|||
|
|
if (shouldFitView) {
|
|||
|
|
fitView(targetNode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调试:打印物体路径
|
|||
|
|
printModelObjectPaths(newModel)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 导出接口
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
/** 加载状态(只读) */
|
|||
|
|
isLoading: readonly(isLoading),
|
|||
|
|
/** 加载模型 */
|
|||
|
|
loadModel,
|
|||
|
|
/** 处理模型 */
|
|||
|
|
processModel,
|
|||
|
|
}
|
|||
|
|
}
|