353 lines
13 KiB
HTML
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>
|