// 全局变量 let sceneManager; let morphAdapter; let animator; let enabledExpressions = new Set(); let expressionDurations = {}; window.addEventListener('DOMContentLoaded', function () { init(); initCollapseStates(); initExpressionList(); }); function init() { // 初始化场景管理器 sceneManager = new BabylonSceneManager('renderCanvas'); sceneManager.init(); // 初始化形态键适配器 morphAdapter = new BabylonMorphTargetAdapter(); // 初始化动画SDK animator = new BlendShapeAnimator({ blendShapeScale: 1.0, dataFps: 30, onStatusChange: showStatus }); animator.setMorphTargetAdapter(morphAdapter); // 导出全局变量供表情库使用 window.setIdleAnimation = (name, target, duration, easing) => { animator.setIdleAnimation(name, target, duration, easing); }; window.expressionParams = animator.expressionParams; window.enabledExpressions = enabledExpressions; window.expressionDurations = expressionDurations; // 加载3D模型 sceneManager.loadModel('head_a01.glb', (meshes) => { showStatus("模型加载成功", "success"); const totalTargets = morphAdapter.buildCache(meshes); if (totalTargets === 0) { showStatus("警告: 未找到形态键", "error"); } else { console.log(`✓ 构建缓存完成,共 ${totalTargets} 个形态键`); } }, (message) => { showStatus("模型加载失败: " + message, "error"); } ); } async function generateAnimation() { const text = document.getElementById('textInput').value.trim(); const apiUrl = document.getElementById('apiUrl').value; const btn = document.getElementById('generateBtn'); if (!text) { showStatus("请输入文字", "error"); return; } btn.disabled = true; showStatus("生成中...", "info"); try { const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text, language: 'zh-CN' }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || '请求失败'); } animator.loadAnimationFrames(data.frames); console.log("动画数据:", data.frames); showStatus(`动画生成成功!共 ${data.frames.length} 帧`, "success"); } catch (err) { showStatus("错误: " + err.message, "error"); } finally { btn.disabled = false; } } function playAnimation() { animator.playAnimation(); } function stopAnimation() { animator.stopAnimation(); } function updateScale(value) { animator.updateConfig('blendShapeScale', parseFloat(value)); document.getElementById('scaleValue').textContent = value; } function updateFps(value) { animator.updateConfig('dataFps', parseInt(value, 10)); document.getElementById('fpsValue').textContent = value; } function testBlendShape() { if (morphAdapter.getCacheSize() === 0) { showStatus("模型未加载", "error"); return; } const testShapes = { 'jawopen': 1.0, 'mouthsmileleft': 1.0, 'mouthsmileright': 1.0 }; let successCount = 0; for (const [name, value] of Object.entries(testShapes)) { morphAdapter.setInfluence(name, value); if (morphAdapter.getInfluence(name) > 0) { console.log(`✓ ${name} = ${value}`); successCount++; } else { console.warn(`✗ 未找到: ${name}`); } } showStatus(`测试: ${successCount}/3 成功`, successCount > 0 ? "success" : "error"); } function resetBlendShapes() { morphAdapter.resetAll(); showStatus("已重置", "info"); } function toggleBlink() { const enabled = document.getElementById('blinkEnabled').checked; animator.toggleBlink(enabled); } function toggleEyeLook() { const enabled = document.getElementById('eyeLookEnabled').checked; animator.toggleEyeLook(enabled); } function toggleRandomExpression() { const enabled = document.getElementById('expressionEnabled').checked; animator.toggleRandomExpression(enabled); } // 参数更新函数 function updateBlinkInterval() { const min = parseFloat(document.getElementById('blinkIntervalMin').value) * 1000; const max = parseFloat(document.getElementById('blinkIntervalMax').value) * 1000; animator.blinkParams.intervalMin = min; animator.blinkParams.intervalMax = max; document.getElementById('blinkIntervalValue').textContent = `${min/1000}-${max/1000}`; } function updateBlinkDuration(value) { animator.blinkParams.duration = parseInt(value); document.getElementById('blinkDurationValue').textContent = value; } function updateBlinkSpeed(value) { animator.blinkParams.speed = parseInt(value); document.getElementById('blinkSpeedValue').textContent = value; } function updateEyeLookInterval() { const min = parseFloat(document.getElementById('eyeLookIntervalMin').value) * 1000; const max = parseFloat(document.getElementById('eyeLookIntervalMax').value) * 1000; animator.eyeLookParams.intervalMin = min; animator.eyeLookParams.intervalMax = max; document.getElementById('eyeLookIntervalValue').textContent = `${min/1000}-${max/1000}`; } function updateEyeLookDuration() { const min = parseInt(document.getElementById('eyeLookDurationMin').value); const max = parseInt(document.getElementById('eyeLookDurationMax').value); animator.eyeLookParams.durationMin = min; animator.eyeLookParams.durationMax = max; document.getElementById('eyeLookDurationValue').textContent = `${min}-${max}`; } function updateEyeLookSpeed(value) { animator.eyeLookParams.speed = parseInt(value); document.getElementById('eyeLookSpeedValue').textContent = value; } function updateExpressionInterval() { const min = parseFloat(document.getElementById('expressionIntervalMin').value) * 1000; const max = parseFloat(document.getElementById('expressionIntervalMax').value) * 1000; animator.expressionParams.intervalMin = min; animator.expressionParams.intervalMax = max; document.getElementById('expressionIntervalValue').textContent = `${min/1000}-${max/1000}`; if (window.ExpressionLibrary) { window.ExpressionLibrary.randomPlayer.intervalMin = min; window.ExpressionLibrary.randomPlayer.intervalMax = max; } } function updateExpressionSpeed(value) { animator.expressionParams.speed = parseInt(value); document.getElementById('expressionSpeedValue').textContent = value; } // 折叠/展开功能 function toggleCollapse(event, paramGroupId) { event.preventDefault(); event.stopPropagation(); const paramGroup = document.getElementById(paramGroupId); const btn = event.target; if (paramGroup.classList.contains('collapsed')) { paramGroup.classList.remove('collapsed'); paramGroup.style.maxHeight = paramGroup.scrollHeight + 'px'; btn.classList.remove('collapsed'); } else { paramGroup.style.maxHeight = paramGroup.scrollHeight + 'px'; setTimeout(() => { paramGroup.classList.add('collapsed'); btn.classList.add('collapsed'); }, 10); } } function initCollapseStates() { const paramGroups = ['blinkParams', 'eyeLookParams', 'expressionParams']; paramGroups.forEach(id => { const paramGroup = document.getElementById(id); const btn = paramGroup.previousElementSibling.querySelector('.collapse-btn'); paramGroup.classList.add('collapsed'); btn.classList.add('collapsed'); }); } function initExpressionList() { if (!window.ExpressionLibrary) { setTimeout(initExpressionList, 100); return; } const expressionList = document.getElementById('expressionList'); const expressions = window.ExpressionLibrary.getExpressionNames(); expressions.forEach(expr => { enabledExpressions.add(expr.key); const exprData = window.ExpressionLibrary.expressions[expr.key]; expressionDurations[expr.key] = exprData.duration; }); expressions.forEach(expr => { const item = document.createElement('div'); item.className = 'expression-item'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `expr_${expr.key}`; checkbox.checked = true; checkbox.onchange = () => toggleExpression(expr.key, checkbox.checked); const label = document.createElement('label'); label.textContent = expr.name; label.htmlFor = `expr_${expr.key}`; const durationInput = document.createElement('input'); durationInput.type = 'number'; durationInput.min = '500'; durationInput.max = '10000'; durationInput.step = '100'; durationInput.value = expressionDurations[expr.key]; durationInput.onchange = () => updateExpressionDuration(expr.key, durationInput.value); const durationLabel = document.createElement('span'); durationLabel.className = 'duration-label'; durationLabel.textContent = 'ms'; item.appendChild(checkbox); item.appendChild(label); item.appendChild(durationInput); item.appendChild(durationLabel); expressionList.appendChild(item); }); } function updateExpressionDuration(key, value) { expressionDurations[key] = parseInt(value); } function toggleExpression(key, enabled) { if (enabled) { enabledExpressions.add(key); } else { enabledExpressions.delete(key); } } function showStatus(message, type) { const status = document.getElementById('status'); status.textContent = message; status.className = 'status show ' + type; }