288 lines
9.0 KiB
HTML
288 lines
9.0 KiB
HTML
<!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> |