This commit is contained in:
nicomacbookpro
2025-12-12 16:21:46 +08:00
commit c0e405b34c
40 changed files with 25844 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

288
index.html Normal file
View File

@@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模型拆解动画 - ExplosionManager</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
overflow: hidden;
}
#box {
width: 100vw;
height: 100vh;
position: relative;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
z-index: 100;
text-align: center;
}
.loading-spinner {
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid white;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.info {
position: absolute;
top: 20px;
left: 20px;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 15px;
border-radius: 8px;
font-size: 14px;
z-index: 10;
max-width: 300px;
}
.info h2 {
margin-bottom: 10px;
font-size: 18px;
}
.info p {
margin: 5px 0;
line-height: 1.6;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div id="box"></div>
<div id="loading" class="loading">
<div class="loading-spinner"></div>
<div>正在加载模型...</div>
</div>
<div class="info">
<h2>3D模型拆解动画</h2>
<p>使用鼠标拖拽旋转视角</p>
<p>使用滚轮缩放</p>
<p>右侧控制面板可控制动画</p>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
"gsap": "https://cdn.jsdelivr.net/npm/gsap@3.12.5/index.js",
"dat.gui": "https://cdn.jsdelivr.net/npm/dat.gui@0.7.9/build/dat.gui.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import * as dat from 'dat.gui'
import { ExplosionManager } from './src/ExplosionManager.js'
const box = document.getElementById('box')
const loading = document.getElementById('loading')
// 创建场景
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x1a1a2e)
// 创建相机
const camera = new THREE.PerspectiveCamera(75, box.clientWidth / box.clientHeight, 0.1, 1000)
camera.position.set(5, 5, 5)
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true })
renderer.setSize(box.clientWidth, box.clientHeight)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
box.appendChild(renderer.domElement)
// 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.minDistance = 2
controls.maxDistance = 50
// 窗口大小调整
window.addEventListener('resize', () => {
renderer.setSize(box.clientWidth, box.clientHeight)
camera.aspect = box.clientWidth / box.clientHeight
camera.updateProjectionMatrix()
})
// 添加光照
scene.add(new THREE.AmbientLight(0xffffff, 0.6))
const pointLight = new THREE.PointLight(0xffffff, 1.5, 0, 2)
pointLight.position.set(5, 5, 5)
pointLight.castShadow = true
scene.add(pointLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 2)
directionalLight.position.set(-5, 5, -5)
directionalLight.castShadow = true
scene.add(directionalLight)
// 添加坐标轴辅助线
scene.add(new THREE.AxesHelper(1000))
// 创建爆炸管理器
const explosionManager = new ExplosionManager(scene)
// 加载模型
async function loadModel() {
try {
// 加载OBJ模型public目录下的模型
await explosionManager.loadOBJ(
'./public/model/刘家大堰室内示例.obj',
'./public/model/刘家大堰室内示例.mtl',
(progress) => {
if (progress.lengthComputable) {
const percent = (progress.loaded / progress.total) * 100
console.log(`加载进度: ${percent.toFixed(2)}%`)
}
}
)
console.log('模型加载成功')
loading.classList.add('hidden')
// 自动调整相机位置以适应模型
const model = explosionManager.getModel()
if (model) {
const box = new THREE.Box3().setFromObject(model)
const center = box.getCenter(new THREE.Vector3())
const size = box.getSize(new THREE.Vector3())
const maxDim = Math.max(size.x, size.y, size.z)
const distance = maxDim * 2
camera.position.set(center.x + distance, center.y + distance, center.z + distance)
controls.target.copy(center)
controls.update()
}
} catch (error) {
console.error('模型加载失败:', error)
loading.innerHTML = '<div style="color: #ff6b6b;">模型加载失败,请检查文件路径</div>'
}
}
// 创建GUI控制面板
const gui = new dat.GUI({ name: '控制面板' })
gui.domElement.style.position = 'absolute'
gui.domElement.style.top = '20px'
gui.domElement.style.right = '20px'
gui.domElement.style.zIndex = '100'
// 动画控制
const controlsObj = {
'拆解动画': () => {
explosionManager.explode({
duration: params.duration,
distance: params.distance,
ease: params.ease,
onComplete: () => {
console.log('拆解动画完成')
}
})
},
'还原动画': () => {
explosionManager.restore({
duration: params.duration,
ease: params.ease,
onComplete: () => {
console.log('还原动画完成')
}
})
},
'切换状态': () => {
explosionManager.toggle({
duration: params.duration,
distance: params.distance,
ease: params.ease
})
}
}
gui.add(controlsObj, '拆解动画').name('💥 拆解')
gui.add(controlsObj, '还原动画').name('🔄 还原')
gui.add(controlsObj, '切换状态').name('🔄 切换')
// 动画参数
const params = {
duration: 1,
distance: 1.5,
ease: 'power2.inOut'
}
const paramsFolder = gui.addFolder('动画参数')
paramsFolder.add(params, 'duration', 0.1, 3).name('时长(秒)').onChange((value) => {
explosionManager.setAnimationParams({ duration: value })
})
paramsFolder.add(params, 'distance', 0.5, 15).name('距离').onChange((value) => {
explosionManager.setAnimationParams({ distance: value })
})
paramsFolder.add(params, 'ease', [
'power1.inOut',
'power2.inOut',
'power3.inOut',
'power4.inOut',
'back.inOut',
'elastic.inOut',
'bounce.inOut'
]).name('缓动函数').onChange((value) => {
explosionManager.setAnimationParams({ ease: value })
})
paramsFolder.open()
// 渲染循环
function animate() {
requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
// 开始加载模型和动画
loadModel()
animate()
</script>
</body>
</html>

BIN
public/.DS_Store vendored Normal file

Binary file not shown.

BIN
public/model/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,13 @@
{
"property": {
"DataTime": "Data Time,,8",
"Department": "Department,,3",
"Floor": "Floor,,4",
"Height": "Floor Height,,5",
"Level": "Model Level,,6",
"ModelNum": "Model Number,,1",
"ModelType": "Model Type,,7",
"Name": "Model Name,,2",
"Note": "Note,,9"
}
}

View File

@@ -0,0 +1,161 @@
# DPModeler v2.0 Wavefront OBJ Exporter
newmtl mtl_default
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
newmtl 07393339-e8ff-4696-bd35-299f19f7ad70.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\07393339-e8ff-4696-bd35-299f19f7ad70.png
newmtl 0bc7b63b-54d6-493c-8ebf-c10aece151b7.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\0bc7b63b-54d6-493c-8ebf-c10aece151b7.png
newmtl 11558897-d7b9-45b6-a59f-813554b27d6f.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\11558897-d7b9-45b6-a59f-813554b27d6f.png
newmtl 1869d08b-1712-4968-b7da-4be28f9340b1.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\1869d08b-1712-4968-b7da-4be28f9340b1.png
newmtl 19a330ad-ee21-4946-9cb6-73a617bf3df7.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\19a330ad-ee21-4946-9cb6-73a617bf3df7.png
newmtl 1a281a2c-f905-4acb-a454-f6da2f8c2b54.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\1a281a2c-f905-4acb-a454-f6da2f8c2b54.png
newmtl 1dba4ecd-5d4b-429e-b7f1-6e242f195b00.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\1dba4ecd-5d4b-429e-b7f1-6e242f195b00.png
newmtl 1e0c4705-ee00-4b51-a8cc-2e791be60ea6.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\1e0c4705-ee00-4b51-a8cc-2e791be60ea6.png
newmtl 3ae451bb-0285-40e7-b1e2-909e3cefd851.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\3ae451bb-0285-40e7-b1e2-909e3cefd851.png
newmtl 4189d971-987b-4ac6-af72-197758af6462.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\4189d971-987b-4ac6-af72-197758af6462.png
newmtl 44b5a010-2000-48ac-afa2-80112105dc88.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\44b5a010-2000-48ac-afa2-80112105dc88.png
newmtl 4b1fcb93-ef46-4632-bc24-6c1e3a0f27d3.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\4b1fcb93-ef46-4632-bc24-6c1e3a0f27d3.png
newmtl 4d4f9325-8a05-478a-a438-e546852769c7.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\4d4f9325-8a05-478a-a438-e546852769c7.png
newmtl 6b7189ea-dcf5-49c9-89be-906188e5dca6.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\6b7189ea-dcf5-49c9-89be-906188e5dca6.png
newmtl 6e6c8786-b57d-410e-b987-50589474e873.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\6e6c8786-b57d-410e-b987-50589474e873.png
newmtl 71312cde-dbac-4581-8dc3-2e2bc21c6727.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\71312cde-dbac-4581-8dc3-2e2bc21c6727.png
newmtl 713c0a3a-060b-44af-9933-c668140eb188.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\713c0a3a-060b-44af-9933-c668140eb188.png
newmtl 72188fe6-b3b8-4655-a931-1818245fc5a0.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\72188fe6-b3b8-4655-a931-1818245fc5a0.png
newmtl 7663e55c-ac6f-4bfb-af4d-24fecea73cdd.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\7663e55c-ac6f-4bfb-af4d-24fecea73cdd.png
newmtl 7a11ec70-35f5-4ff8-aaaf-93798dc995ef.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\7a11ec70-35f5-4ff8-aaaf-93798dc995ef.png
newmtl 80d03b10-169e-45a3-b090-6993cd0226be.jpg
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\80d03b10-169e-45a3-b090-6993cd0226be.jpg
newmtl 9abe28b2-e14e-4c41-865e-ab15fb27eba0.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\9abe28b2-e14e-4c41-865e-ab15fb27eba0.png
newmtl a9f237dc-215d-40f4-8347-d0a24bbe2908.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\a9f237dc-215d-40f4-8347-d0a24bbe2908.png
newmtl b41a9325-c2a5-41b2-b6d8-7ab0acee546e.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\b41a9325-c2a5-41b2-b6d8-7ab0acee546e.png
newmtl be0fe97f-6615-4f4d-a08f-7ad320da6a05.jpg
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\be0fe97f-6615-4f4d-a08f-7ad320da6a05.jpg
newmtl cfd9d930-51c1-49e7-bb1a-1a376c5e7334.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\cfd9d930-51c1-49e7-bb1a-1a376c5e7334.png
newmtl dbcc2093-b9b5-44e8-94e6-cd23db461f15.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\dbcc2093-b9b5-44e8-94e6-cd23db461f15.png
newmtl e352b5d2-9024-4308-8c7f-22c5d4e79c86.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\e352b5d2-9024-4308-8c7f-22c5d4e79c86.png
newmtl eb226b64-db49-47f8-ac67-fc9659dcdd71.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\eb226b64-db49-47f8-ac67-fc9659dcdd71.png
newmtl ebc49bba-9241-47b8-a491-20a294323009.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\ebc49bba-9241-47b8-a491-20a294323009.png
newmtl f6df5ef4-50b0-45a5-8a52-b768f952b0ca.png
Ka 1.000 1.000 1.000
Kd 1.000 1.000 1.000
map_Kd .\Image\f6df5ef4-50b0-45a5-8a52-b768f952b0ca.png

File diff suppressed because it is too large Load Diff

319
src/ExplosionManager.js Normal file
View File

@@ -0,0 +1,319 @@
import * as THREE from 'three'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js'
import gsap from 'gsap'
/**
* 爆炸拆解管理器
* 用于管理3D模型的拆解和还原动画
*/
export class ExplosionManager {
constructor(scene) {
this.scene = scene
this.model = null
this.isExploded = false
this.animationDuration = 1
this.explosionDistance = 1.5
this.easeType = 'power2.inOut'
// 初始化加载器
this.gltfLoader = new GLTFLoader()
this.objLoader = new OBJLoader()
this.mtlLoader = new MTLLoader()
// 设置DRACO解码器
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('https://z2586300277.github.io/3d-file-server/js/three/draco/')
this.gltfLoader.setDRACOLoader(dracoLoader)
}
/**
* 加载GLTF/GLB模型
* @param {string} url - 模型文件路径
* @param {Function} onProgress - 加载进度回调
* @param {Function} onError - 错误回调
* @returns {Promise<THREE.Group>}
*/
loadGLTF(url, onProgress, onError) {
return new Promise((resolve, reject) => {
this.gltfLoader.load(
url,
(gltf) => {
this.setupModel(gltf.scene)
resolve(gltf.scene)
},
onProgress,
onError || reject
)
})
}
/**
* 加载OBJ模型
* @param {string} objUrl - OBJ文件路径
* @param {string} mtlUrl - MTL文件路径可选
* @param {Function} onProgress - 加载进度回调
* @param {Function} onError - 错误回调
* @returns {Promise<THREE.Group>}
*/
loadOBJ(objUrl, mtlUrl = null, onProgress, onError) {
return new Promise((resolve, reject) => {
const loadObj = (obj) => {
this.setupModel(obj)
resolve(obj)
}
if (mtlUrl) {
// 先加载材质
this.mtlLoader.load(
mtlUrl,
(materials) => {
materials.preload()
this.objLoader.setMaterials(materials)
this.objLoader.load(
objUrl,
loadObj,
onProgress,
onError || reject
)
},
onProgress,
onError || reject
)
} else {
// 直接加载OBJ
this.objLoader.load(
objUrl,
loadObj,
onProgress,
onError || reject
)
}
})
}
/**
* 设置模型,准备拆解
* @param {THREE.Object3D} model - 3D模型对象
*/
setupModel(model) {
// 移除旧模型
if (this.model) {
this.scene.remove(this.model)
}
this.model = model
this.isExploded = false
// 计算模型中心点
const box = new THREE.Box3().setFromObject(model)
const center = new THREE.Vector3()
box.getCenter(center)
model.center = center
// 遍历所有mesh保存初始位置
model.traverse((child) => {
if (child.isMesh) {
// 转换为世界坐标
child.localToWorld(child.position)
// 保存初始位置
child.startPosition = child.position.clone()
}
})
// 添加到场景
this.scene.add(model)
}
/**
* 生成随机方向向量
* @returns {Object} 包含x, y, z的随机方向
*/
getRandomDirection() {
const distance = () => Math.random() > 0.5 ? this.explosionDistance : -this.explosionDistance
return {
x: distance(),
y: distance(),
z: distance()
}
}
/**
* 拆解动画
* @param {Object} options - 配置选项
* @param {number} options.duration - 动画时长(秒)
* @param {number} options.distance - 爆炸距离
* @param {string} options.ease - 缓动函数类型
* @param {Function} options.onComplete - 完成回调
*/
explode(options = {}) {
if (!this.model) {
console.warn('ExplosionManager: 没有加载模型')
return
}
if (this.isExploded) {
console.warn('ExplosionManager: 模型已经拆解')
return
}
const {
duration = this.animationDuration,
distance = this.explosionDistance,
ease = this.easeType,
onComplete
} = options
const originalDistance = this.explosionDistance
this.explosionDistance = distance
this.model.traverse((child) => {
if (child.isMesh && child.startPosition) {
const dir = this.getRandomDirection()
gsap.to(child.position, {
x: child.startPosition.x + dir.x,
y: child.startPosition.y + dir.y,
z: child.startPosition.z + dir.z,
duration,
ease,
onComplete: () => {
if (onComplete && child === this.getLastMesh()) {
onComplete()
}
}
})
}
})
this.explosionDistance = originalDistance
this.isExploded = true
}
/**
* 还原动画
* @param {Object} options - 配置选项
* @param {number} options.duration - 动画时长(秒)
* @param {string} options.ease - 缓动函数类型
* @param {Function} options.onComplete - 完成回调
*/
restore(options = {}) {
if (!this.model) {
console.warn('ExplosionManager: 没有加载模型')
return
}
if (!this.isExploded) {
console.warn('ExplosionManager: 模型未拆解')
return
}
const {
duration = this.animationDuration,
ease = this.easeType,
onComplete
} = options
this.model.traverse((child) => {
if (child.isMesh && child.startPosition) {
gsap.to(child.position, {
x: child.startPosition.x,
y: child.startPosition.y,
z: child.startPosition.z,
duration,
ease,
onComplete: () => {
if (onComplete && child === this.getLastMesh()) {
onComplete()
}
}
})
}
})
this.isExploded = false
}
/**
* 获取最后一个mesh用于回调判断
* @returns {THREE.Mesh|null}
*/
getLastMesh() {
let lastMesh = null
if (this.model) {
this.model.traverse((child) => {
if (child.isMesh) {
lastMesh = child
}
})
}
return lastMesh
}
/**
* 切换拆解/还原状态
* @param {Object} options - 配置选项
*/
toggle(options = {}) {
if (this.isExploded) {
this.restore(options)
} else {
this.explode(options)
}
}
/**
* 设置动画参数
* @param {Object} params - 参数对象
* @param {number} params.duration - 动画时长
* @param {number} params.distance - 爆炸距离
* @param {string} params.ease - 缓动函数类型
*/
setAnimationParams(params) {
if (params.duration !== undefined) {
this.animationDuration = params.duration
}
if (params.distance !== undefined) {
this.explosionDistance = params.distance
}
if (params.ease !== undefined) {
this.easeType = params.ease
}
}
/**
* 获取当前模型
* @returns {THREE.Object3D|null}
*/
getModel() {
return this.model
}
/**
* 获取拆解状态
* @returns {boolean}
*/
getExplodedState() {
return this.isExploded
}
/**
* 移除模型
*/
dispose() {
if (this.model) {
// 停止所有动画
this.model.traverse((child) => {
if (child.isMesh && child.position) {
gsap.killTweensOf(child.position)
}
})
this.scene.remove(this.model)
this.model = null
this.isExploded = false
}
}
}

127
src/example.js Normal file
View File

@@ -0,0 +1,127 @@
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as dat from 'dat.gui'
import { ExplosionManager } from './ExplosionManager.js'
const box = document.getElementById('box')
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, box.clientWidth / box.clientHeight, 0.1, 1000)
camera.position.set(5, 5, 5)
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true })
renderer.setSize(box.clientWidth, box.clientHeight)
box.appendChild(renderer.domElement)
new OrbitControls(camera, renderer.domElement)
window.onresize = () => {
renderer.setSize(box.clientWidth, box.clientHeight)
camera.aspect = box.clientWidth / box.clientHeight
camera.updateProjectionMatrix()
}
// 添加光照
scene.add(new THREE.AmbientLight(0xffffff, 1))
const pointLight = new THREE.PointLight(0xffffff, 1.5, 0, 2)
pointLight.position.set(5, 5, 5)
scene.add(pointLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 2)
directionalLight.position.set(-5, 5, -5)
scene.add(directionalLight)
scene.add(new THREE.AxesHelper(1000))
// 创建爆炸管理器
const explosionManager = new ExplosionManager(scene)
// 加载模型示例
// 方式1: 加载GLTF/GLB模型
// explosionManager.loadGLTF('/model/car.glb')
// .then((model) => {
// console.log('模型加载成功', model)
// })
// .catch((error) => {
// console.error('模型加载失败', error)
// })
// 方式2: 加载OBJ模型支持MTL材质
explosionManager.loadOBJ(
'/model/刘家大堰室内示例.obj',
'/model/刘家大堰室内示例.mtl'
)
.then((model) => {
console.log('模型加载成功', model)
})
.catch((error) => {
console.error('模型加载失败', error)
})
// 创建GUI控制面板
const gui = new dat.GUI()
gui.add({
'拆解动画': () => {
explosionManager.explode({
duration: 1,
distance: 1.5,
ease: 'power2.inOut',
onComplete: () => {
console.log('拆解动画完成')
}
})
}
}, '拆解动画')
gui.add({
'还原动画': () => {
explosionManager.restore({
duration: 1,
ease: 'power2.inOut',
onComplete: () => {
console.log('还原动画完成')
}
})
}
}, '还原动画')
gui.add({
'切换状态': () => {
explosionManager.toggle({
duration: 1,
distance: 1.5,
ease: 'power2.inOut'
})
}
}, '切换状态')
// 动画参数控制
const params = {
duration: 1,
distance: 1.5,
ease: 'power2.inOut'
}
gui.add(params, 'duration', 0.1, 3).onChange((value) => {
explosionManager.setAnimationParams({ duration: value })
})
gui.add(params, 'distance', 0.5, 5).onChange((value) => {
explosionManager.setAnimationParams({ distance: value })
})
gui.add(params, 'ease', ['power1.inOut', 'power2.inOut', 'power3.inOut', 'power4.inOut', 'back.inOut', 'elastic.inOut']).onChange((value) => {
explosionManager.setAnimationParams({ ease: value })
})
// 渲染循环
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()