Files
nanocode/particle-lab/37-mixamo-defeated/index.html
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

353 lines
13 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>37 - Mixamo Defeated Animation</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
overflow: hidden;
background: #0a0a0f;
font-family: 'Segoe UI', sans-serif;
}
#canvas-container {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
}
#ui {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 10;
}
#title {
position: absolute;
top: 25px; left: 30px;
color: #fff;
font-size: 20px;
letter-spacing: 2px;
}
#title span { color: #ff4757; font-size: 14px; }
#stats {
position: absolute;
top: 25px; right: 30px;
color: #888;
text-align: right;
font-size: 12px;
}
#stats .big {
font-size: 22px;
color: #fff;
font-weight: bold;
}
#controls {
position: absolute;
bottom: 30px; left: 30px;
pointer-events: auto;
background: rgba(0,0,0,0.85);
padding: 15px;
border-radius: 10px;
border: 1px solid rgba(255,71,87,0.3);
}
#controls button {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
color: #fff;
padding: 10px 18px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s;
}
#controls button:hover {
background: rgba(255,71,87,0.3);
border-color: #ff4757;
}
#loading {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: #ff4757;
font-size: 20px;
text-align: center;
z-index: 100;
background: rgba(0,0,0,0.9);
padding: 40px 60px;
border-radius: 10px;
border: 1px solid rgba(255,71,87,0.3);
}
#loading .progress {
font-size: 13px;
color: #888;
margin-top: 15px;
}
#loading-bar {
width: 220px;
height: 4px;
background: #333;
margin: 15px auto;
border-radius: 2px;
overflow: hidden;
}
#progress-fill {
width: 0%;
height: 100%;
background: #ff4757;
transition: width 0.3s;
}
</style>
</head>
<body>
<div id="loading">
<div>🎭 加载 Mixamo 动画...</div>
<div id="loading-bar"><div id="progress-fill"></div></div>
<div class="progress" id="loading-text">准备下载 2.3MB...</div>
<div style="font-size:11px;color:#666;margin-top:10px;">首次加载约需 10-20 秒</div>
</div>
<div id="canvas-container"></div>
<div id="ui">
<div id="title">37 - Mixamo <span>DEFEATED</span> 🎭</div>
<div id="stats">
<div class="big">FBX Animation</div>
<div>Mixamo 骨骼动画</div>
<div>2.3MB 压缩传输</div>
<div>Skinned Mesh</div>
</div>
<div id="controls">
<div style="color:#ff4757;margin-bottom:10px;font-size:12px;">▶ 播放控制</div>
<button onclick="togglePlay()" id="play-btn">⏸ 暂停</button>
<button onclick="resetAnimation()">↺ 重置</button>
<div style="margin-top:10px;color:#888;font-size:11px;">
拖动旋转 | 滚轮缩放
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/FBXLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js"></script>
<script>
// ========== 场景 ==========
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
scene.fog = new THREE.Fog(0x0a0a0f, 1, 50);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 80, 200);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
document.getElementById('canvas-container').appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 40, 0);
// ========== 灯光 ==========
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(50, 100, 50);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
scene.add(dirLight);
const backLight = new THREE.DirectionalLight(0x4444ff, 0.3);
backLight.position.set(-50, 50, -50);
scene.add(backLight);
// ========== 地面 ==========
const gridHelper = new THREE.GridHelper(200, 20, 0x333333, 0x222222);
scene.add(gridHelper);
const planeGeometry = new THREE.PlaneGeometry(200, 200);
const planeMaterial = new THREE.MeshStandardMaterial({
color: 0x111111,
roughness: 0.8,
metalness: 0.2
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true;
scene.add(plane);
// ========== 加载 FBX ==========
let mixer = null;
let model = null;
let isPlaying = true;
async function loadModel() {
// 先用压缩文件,失败则用原始文件
const tryLoad = async (url, isCompressed) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
if (isCompressed) {
xhr.onprogress = function(e) {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 60);
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('loading-text').textContent =
`下载压缩文件... ${(e.loaded/1024/1024).toFixed(1)}MB`;
}
};
} else {
document.getElementById('loading-text').textContent = '下载原始文件 (8.7MB)...';
}
xhr.onload = function() {
if (xhr.status === 200) {
resolve({ data: xhr.response, compressed: isCompressed });
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send();
});
};
try {
let result;
try {
result = await tryLoad('Defeated.bin', true);
} catch (e) {
console.log('压缩文件加载失败,尝试原始文件:', e);
result = await tryLoad('Defeated.fbx', false);
}
document.getElementById('loading-text').textContent = result.compressed ? '解压数据...' : '解析模型...';
document.getElementById('progress-fill').style.width = result.compressed ? '75%' : '50%';
xhr.onprogress = function(e) {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 60);
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('loading-text').textContent =
`下载中... ${(e.loaded/1024/1024).toFixed(1)}MB / ${(e.total/1024/1024).toFixed(1)}MB`;
}
};
xhr.onload = function() {
if (xhr.status === 200) {
document.getElementById('loading-text').textContent = '解压数据...';
document.getElementById('progress-fill').style.width = '75%';
// 解压(如果需要)
let fbxData;
if (result.compressed) {
const compressed = new Uint8Array(result.data);
const decompressed = pako.inflate(compressed);
fbxData = decompressed.buffer;
} else {
fbxData = result.data;
}
document.getElementById('loading-text').textContent = '解析模型...';
document.getElementById('progress-fill').style.width = '90%';
// 解析 FBX
const loader = new THREE.FBXLoader();
const object = loader.parse(fbxData);
// 显示模型
model = object;
object.scale.set(0.5, 0.5, 0.5);
object.position.set(0, 0, 0);
object.traverse(function(child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
child.material.emissive = new THREE.Color(0x222222);
child.material.emissiveIntensity = 0.2;
}
});
scene.add(object);
// 动画
if (object.animations && object.animations.length > 0) {
mixer = new THREE.AnimationMixer(object);
const action = mixer.clipAction(object.animations[0]);
action.play();
const sizeText = result.compressed ? '8.7MB → 2.3MB' : '8.7MB';
document.getElementById('stats').innerHTML = `
<div class="big">${object.animations.length} 动画</div>
<div>${Math.round(object.animations[0].duration * 60)} 帧</div>
<div>Mixamo 骨骼</div>
<div>${sizeText}</div>
`;
}
document.getElementById('progress-fill').style.width = '100%';
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
}, 300);
} catch (error) {
console.error('加载失败:', error);
document.getElementById('loading').innerHTML =
`<div style="color:#ff4757">加载失败: ${error.message}<br><small>请刷新重试</small></div>`;
}
}
// 开始加载
loadModel();
// ========== 控制函数 ==========
function togglePlay() {
if (!mixer) return;
isPlaying = !isPlaying;
document.getElementById('play-btn').textContent = isPlaying ? '⏸ 暂停' : '▶ 播放';
}
function resetAnimation() {
if (!mixer) return;
mixer.setTime(0);
}
// ========== 动画循环 ==========
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
if (mixer && isPlaying) {
mixer.update(delta);
}
controls.update();
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>