init
288
index.html
Normal 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
BIN
public/model/.DS_Store
vendored
Normal file
BIN
public/model/Image/07393339-e8ff-4696-bd35-299f19f7ad70.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/model/Image/0bc7b63b-54d6-493c-8ebf-c10aece151b7.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/model/Image/11558897-d7b9-45b6-a59f-813554b27d6f.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
public/model/Image/1869d08b-1712-4968-b7da-4be28f9340b1.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
public/model/Image/19a330ad-ee21-4946-9cb6-73a617bf3df7.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/model/Image/1a281a2c-f905-4acb-a454-f6da2f8c2b54.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/model/Image/1dba4ecd-5d4b-429e-b7f1-6e242f195b00.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
public/model/Image/1e0c4705-ee00-4b51-a8cc-2e791be60ea6.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
public/model/Image/3ae451bb-0285-40e7-b1e2-909e3cefd851.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
public/model/Image/4189d971-987b-4ac6-af72-197758af6462.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/model/Image/44b5a010-2000-48ac-afa2-80112105dc88.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
public/model/Image/4b1fcb93-ef46-4632-bc24-6c1e3a0f27d3.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/model/Image/4d4f9325-8a05-478a-a438-e546852769c7.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/model/Image/6b7189ea-dcf5-49c9-89be-906188e5dca6.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/model/Image/6e6c8786-b57d-410e-b987-50589474e873.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/model/Image/71312cde-dbac-4581-8dc3-2e2bc21c6727.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/model/Image/713c0a3a-060b-44af-9933-c668140eb188.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
public/model/Image/72188fe6-b3b8-4655-a931-1818245fc5a0.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/model/Image/7663e55c-ac6f-4bfb-af4d-24fecea73cdd.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/model/Image/7a11ec70-35f5-4ff8-aaaf-93798dc995ef.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/model/Image/80d03b10-169e-45a3-b090-6993cd0226be.jpg
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/model/Image/9abe28b2-e14e-4c41-865e-ab15fb27eba0.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/model/Image/a9f237dc-215d-40f4-8347-d0a24bbe2908.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
public/model/Image/b41a9325-c2a5-41b2-b6d8-7ab0acee546e.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/model/Image/be0fe97f-6615-4f4d-a08f-7ad320da6a05.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
public/model/Image/cfd9d930-51c1-49e7-bb1a-1a376c5e7334.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/model/Image/dbcc2093-b9b5-44e8-94e6-cd23db461f15.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/model/Image/e352b5d2-9024-4308-8c7f-22c5d4e79c86.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
public/model/Image/eb226b64-db49-47f8-ac67-fc9659dcdd71.png
Normal file
|
After Width: | Height: | Size: 100 B |
BIN
public/model/Image/ebc49bba-9241-47b8-a491-20a294323009.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/model/Image/f6df5ef4-50b0-45a5-8a52-b768f952b0ca.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
13
public/model/刘家大堰室内示例.json
Normal 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"
|
||||
}
|
||||
}
|
||||
161
public/model/刘家大堰室内示例.mtl
Normal 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
|
||||
|
||||
24936
public/model/刘家大堰室内示例.obj
Normal file
319
src/ExplosionManager.js
Normal 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
@@ -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()
|
||||
|
||||