第34节:反向运动学与角色动画自然化
第34节:反向运动学与角色动画自然化
概述
反向运动学是3D角色动画的核心技术,通过计算关节链末端效应器(如手、脚)的目标位置,自动推导出中间关节的合理角度,实现自然流畅的角色动作。本节将深入探索IK算法原理、实现技术,以及如何与骨骼动画系统集成,创造逼真的角色交互。
反向运动学系统架构:
核心原理深度解析
反向运动学基础理论
IK系统通过数学计算解决从末端到根节点的关节角度:
正向运动学(FK)公式:
末端位置 = f(θ₁, θ₂, ..., θₙ)
反向运动学(IK)公式:
[θ₁, θ₂, ..., θₙ] = f⁻¹(目标位置)
常用IK算法对比
| 算法类型 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| CCD | 循环坐标下降 | 实现简单、计算快 | 可能陷入局部最优 | 实时应用、简单链 |
| FABRIK | 前向反向传递 | 收敛快、自然 | 实现复杂 | 人形角色、复杂链 |
| 雅可比矩阵 | 矩阵求逆 | 精度高、数学严谨 | 计算量大 | 机器人、精密控制 |
| 解析法 | 几何求解 | 实时性最好 | 只适用简单链 | 两关节、三关节链 |
完整代码实现
高级反向运动学系统
<template><div class="ik-animation-container"><!-- 3D渲染画布 --><canvas ref="animationCanvas" class="animation-canvas"></canvas><!-- 角色控制面板 --><div class="character-controls"><div class="control-section"><h3>🎭 角色设置</h3><div class="character-presets"><button v-for="preset in characterPresets" :key="preset.id"@click="loadCharacterPreset(preset)"class="preset-button":class="{ active: currentCharacter?.id === preset.id }">{{ preset.name }}</button></div><div class="control-group"><label>角色比例: {{ characterScale }}</label><input type="range" v-model="characterScale" min="0.5" max="2" step="0.1"></div></div><div class="control-section"><h3>🦵 IK 系统配置</h3><div class="ik-chain-controls"><div class="chain-section"><h4>腿部 IK</h4><div class="control-group"><label>启用腿部 IK</label><input type="checkbox" v-model="legIKEnabled"></div><div class="control-group"><label>脚部旋转: {{ footRotation }}°</label><input type="range" v-model="footRotation" min="-45" max="45" step="1":disabled="!legIKEnabled"></div><div class="control-group"><label>膝盖弯曲: {{ kneeBend }}°</label><input type="range" v-model="kneeBend" min="0" max="90" step="1":disabled="!legIKEnabled"></div></div><div class="chain-section"><h4>手臂 IK</h4><div class="control-group"><label>启用手臂 IK</label><input type="checkbox" v-model="armIKEnabled"></div><div class="control-group"><label>肘部弯曲: {{ elbowBend }}°</label><input type="range" v-model="elbowBend" min="0" max="120" step="1":disabled="!armIKEnabled"></div><div class="control-group"><label>手腕旋转: {{ wristRotation }}°</label><input type="range" v-model="wristRotation" min="-90" max="90" step="1":disabled="!armIKEnabled"></div></div></div></div><div class="control-section"><h3>🚶 动作控制</h3><div class="animation-presets"><button v-for="anim in animationPresets" :key="anim.id"@click="playAnimation(anim)"class="anim-button":class="{ active: currentAnimation?.id === anim.id }">{{ anim.name }}</button></div><div class="animation-controls"><div class="control-group"><label>动画速度: {{ animationSpeed }}</label><input type="range" v-model="animationSpeed" min="0.1" max="2" step="0.1"></div><div class="control-group"><label>混合权重: {{ blendWeight }}</label><input type="range" v-model="blendWeight" min="0" max="1" step="0.05"></div></div><div class="motion-controls"><div class="joystick-container"><div class="joystick-label">移动控制</div><div class="virtual-joystick" @mousedown="startJoystick" @touchstart="startJoystick"><div class="joystick-base"><div class="joystick-handle" :style="joystickStyle"></div></div></div></div></div></div><div class="control-section"><h3>⚙️ 约束系统</h3><div class="constraint-controls"><div class="control-group"><label>关节限制</label><input type="checkbox" v-model="jointLimitsEnabled"></div><div class="control-group"><label>地面适配</label><input type="checkbox" v-model="groundAdaptation"></div><div class="control-group"><label>平衡控制</label><input type="checkbox" v-model="balanceControl"></div><div class="control-group"><label>物理模拟</label><input type="checkbox" v-model="physicsEnabled"></div></div><div class="debug-controls"><div class="control-group"><label>显示骨骼</label><input type="checkbox" v-model="showSkeleton"></div><div class="control-group"><label>显示IK目标</label><input type="checkbox" v-model="showIKTargets"></div><div class="control-group"><label>显示约束</label><input type="checkbox" v-model="showConstraints"></div></div></div><div class="control-section"><h3>📊 性能监控</h3><div class="performance-stats"><div class="stat-item"><span>IK求解时间:</span><span>{{ ikSolveTime.toFixed(2) }}ms</span></div><div class="stat-item"><span>骨骼数量:</span><span>{{ boneCount }}</span></div><div class="stat-item"><span>活动IK链:</span><span>{{ activeIKChains }}</span></div><div class="stat-item"><span>帧率:</span><span>{{ currentFPS }} FPS</span></div></div></div></div><!-- 角色状态显示 --><div class="character-status"><div class="status-panel"><h4>角色状态</h4><div class="status-content"><div>姿势: {{ currentPose }}</div><div>平衡: {{ balanceStatus }}</div><div>接触点: {{ contactPoints }} 个</div><div>动作权重: {{ (animationWeight * 100).toFixed(0) }}%</div></div></div></div><!-- 环境控制 --><div class="environment-controls"><button @click="toggleEnvironment" class="env-button">{{ showEnvironment ? '隐藏环境' : '显示环境' }}</button><button @click="resetScene" class="env-button">重置场景</button></div><!-- 加载界面 --><div v-if="isLoading" class="loading-overlay"><div class="loading-content"><div class="skeleton-loader"><div class="bone-segment"></div><div class="bone-segment"></div><div class="bone-segment"></div><div class="bone-segment"></div></div><h3>初始化IK系统...</h3><div class="loading-progress"><div class="progress-bar"><div class="progress-fill" :style="loadingProgressStyle"></div></div><span>{{ loadingMessage }}</span></div></div></div></div>
</template><script>
import { onMounted, onUnmounted, ref, reactive, computed } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';// CCD反向运动学求解器
class CCDIKSolver {constructor(boneChain, maxIterations = 20, tolerance = 0.01) {this.boneChain = boneChain;this.maxIterations = maxIterations;this.tolerance = tolerance;this.iterations = 0;this.solveTime = 0;}// 求解IK链solve(targetPosition) {const startTime = performance.now();const endEffector = this.boneChain[this.boneChain.length - 1];for (this.iterations = 0; this.iterations < this.maxIterations; this.iterations++) {// 从末端效应器向根节点迭代for (let i = this.boneChain.length - 2; i >= 0; i--) {const bone = this.boneChain[i];const endPos = endEffector.getWorldPosition(new THREE.Vector3());const bonePos = bone.getWorldPosition(new THREE.Vector3());// 计算到目标和当前末端的方向向量const toEnd = new THREE.Vector3().subVectors(endPos, bonePos).normalize();const toTarget = new THREE.Vector3().subVectors(targetPosition, bonePos).normalize();// 计算旋转轴和角度const rotationAxis = new THREE.Vector3().crossVectors(toEnd, toTarget).normalize();const dotProduct = THREE.MathUtils.clamp(toEnd.dot(toTarget), -1, 1);const rotationAngle = Math.acos(dotProduct);if (rotationAngle < this.tolerance) {this.solveTime = performance.now() - startTime;return true; // 已收敛}// 应用旋转const quaternion = new THREE.Quaternion().setFromAxisAngle(rotationAxis, rotationAngle);bone.quaternion.multiplyQuaternions(quaternion, bone.quaternion);// 更新骨骼变换bone.updateMatrixWorld(true);// 检查是否达到目标const newEndPos = endEffector.getWorldPosition(new THREE.Vector3());if (newEndPos.distanceTo(targetPosition) < this.tolerance) {this.solveTime = performance.now() - startTime;return true;}}}this.solveTime = performance.now() - startTime;return false; // 未收敛}// 获取求解统计getStats() {return {iterations: this.iterations,solveTime: this.solveTime,converged: this.iterations < this.maxIterations};}
}// FABRIK反向运动学求解器
class FABRIKIKSolver {constructor(boneChain, maxIterations = 10, tolerance = 0.01) {this.boneChain = boneChain;this.maxIterations = maxIterations;this.tolerance = tolerance;this.originalLengths = this.calculateBoneLengths();this.positions = new Array(boneChain.length);}// 计算骨骼长度calculateBoneLengths() {const lengths = [];for (let i = 0; i < this.boneChain.length - 1; i++) {const bone1 = this.boneChain[i];const bone2 = this.boneChain[i + 1];const pos1 = bone1.getWorldPosition(new THREE.Vector3());const pos2 = bone2.getWorldPosition(new THREE.Vector3());lengths.push(pos1.distanceTo(pos2));}return lengths;}// FABRIK求解solve(targetPosition) {const startTime = performance.now();// 初始化位置数组for (let i = 0; i < this.boneChain.length; i++) {this.positions[i] = this.boneChain[i].getWorldPosition(new THREE.Vector3());}const rootPos = this.positions[0].clone();const endEffectorPos = this.positions[this.positions.length - 1];// 检查目标是否可达let totalLength = this.originalLengths.reduce((sum, len) => sum + len, 0);const rootToTarget = targetPosition.distanceTo(rootPos);if (rootToTarget > totalLength) {// 目标不可达,完全伸展this.stretchToTarget(targetPosition);} else {// 目标可达,执行FABRIK迭代for (let iter = 0; iter < this.maxIterations; iter++) {// 前向传递:从末端到目标this.positions[this.positions.length - 1] = targetPosition.clone();for (let i = this.positions.length - 2; i >= 0; i--) {const direction = new THREE.Vector3().subVectors(this.positions[i], this.positions[i + 1]).normalize();this.positions[i] = new THREE.Vector3().copy(this.positions[i + 1]).add(direction.multiplyScalar(this.originalLengths[i]));}// 反向传递:从根节点回固定位置this.positions[0] = rootPos.clone();for (let i = 1; i < this.positions.length; i++) {const direction = new THREE.Vector3().subVectors(this.positions[i], this.positions[i - 1]).normalize();this.positions[i] = new THREE.Vector3().copy(this.positions[i - 1]).add(direction.multiplyScalar(this.originalLengths[i - 1]));}// 检查收敛if (endEffectorPos.distanceTo(targetPosition) < this.tolerance) {break;}}}// 应用计算的位置到骨骼this.applyPositionsToBones();return performance.now() - startTime;}// 伸展到不可达目标stretchToTarget(targetPosition) {const rootPos = this.positions[0];const direction = new THREE.Vector3().subVectors(targetPosition, rootPos).normalize();for (let i = 1; i < this.positions.length; i++) {this.positions[i] = new THREE.Vector3().copy(this.positions[i - 1]).add(direction.multiplyScalar(this.originalLengths[i - 1]));}this.applyPositionsToBones();}// 应用位置到骨骼applyPositionsToBones() {for (let i = 0; i < this.boneChain.length; i++) {const bone = this.boneChain[i];if (i === 0) {// 根节点只设置位置bone.position.copy(this.positions[i]);} else {// 计算从父节点到当前节点的方向const parentPos = this.positions[i - 1];const currentPos = this.positions[i];const direction = new THREE.Vector3().subVectors(currentPos, parentPos).normalize();// 计算旋转const defaultDirection = new THREE.Vector3(0, 1, 0); // 假设骨骼默认向上const rotation = new THREE.Quaternion().setFromUnitVectors(defaultDirection, direction);bone.quaternion.copy(rotation);bone.position.copy(parentPos);}bone.updateMatrixWorld(true);}}
}// 角色IK系统
class CharacterIKSystem {constructor(character) {this.character = character;this.ikSolvers = new Map();this.constraints = new Map();this.ikTargets = new Map();this.isEnabled = true;this.setupDefaultIKChains();this.setupConstraints();}// 设置默认IK链setupDefaultIKChains() {// 左腿IK链const leftLegChain = [this.character.getBone('LeftUpperLeg'),this.character.getBone('LeftLowerLeg'), this.character.getBone('LeftFoot')].filter(Boolean);if (leftLegChain.length === 3) {this.ikSolvers.set('leftLeg', new FABRIKIKSolver(leftLegChain));this.ikTargets.set('leftLeg', new THREE.Object3D());}// 右腿IK链const rightLegChain = [this.character.getBone('RightUpperLeg'),this.character.getBone('RightLowerLeg'),this.character.getBone('RightFoot')].filter(Boolean);if (rightLegChain.length === 3) {this.ikSolvers.set('rightLeg', new FABRIKIKSolver(rightLegChain));this.ikTargets.set('rightLeg', new THREE.Object3D());}// 左手IK链const leftArmChain = [this.character.getBone('LeftUpperArm'),this.character.getBone('LeftLowerArm'),this.character.getBone('LeftHand')].filter(Boolean);if (leftArmChain.length === 3) {this.ikSolvers.set('leftArm', new CCDIKSolver(leftArmChain));this.ikTargets.set('leftArm', new THREE.Object3D());}// 右手IK链const rightArmChain = [this.character.getBone('RightUpperArm'),this.character.getBone('RightLowerArm'), this.character.getBone('RightHand')].filter(Boolean);if (rightArmChain.length === 3) {this.ikSolvers.set('rightArm', new CCDIKSolver(rightArmChain));this.ikTargets.set('rightArm', new THREE.Object3D());}}// 设置约束setupConstraints() {// 关节角度限制this.constraints.set('jointLimits', {apply: (bone, rotation) => {const limits = this.getJointLimits(bone.name);if (limits) {rotation.x = THREE.MathUtils.clamp(rotation.x, limits.min.x, limits.max.x);rotation.y = THREE.MathUtils.clamp(rotation.y, limits.min.y, limits.max.y); rotation.z = THREE.MathUtils.clamp(rotation.z, limits.min.z, limits.max.z);}return rotation;}});// 极向量约束(防止肘部/膝盖翻转)this.constraints.set('poleVector', {apply: (bone, rotation, chain, index) => {if (chain.length >= 3 && index === 1) { // 中间关节(肘部/膝盖)const poleTarget = this.getPoleVectorTarget(chain);if (poleTarget) {// 计算极向量约束的旋转const adjustedRotation = this.calculatePoleVectorRotation(bone, chain, poleTarget);rotation.copy(adjustedRotation);}}return rotation;}});}// 获取关节限制getJointLimits(boneName) {const limits = {'LeftUpperLeg': { min: new THREE.Vector3(-45, -45, -45), max: new THREE.Vector3(45, 45, 45) },'RightUpperLeg': { min: new THREE.Vector3(-45, -45, -45), max: new THREE.Vector3(45, 45, 45) },'LeftLowerLeg': { min: new THREE.Vector3(0, -10, -10), max: new THREE.Vector3(120, 10, 10) },'RightLowerLeg': { min: new THREE.Vector3(0, -10, -10), max: new THREE.Vector3(120, 10, 10) },'LeftUpperArm': { min: new THREE.Vector3(-90, -90, -45), max: new THREE.Vector3(90, 90, 180) },'RightUpperArm': { min: new THREE.Vector3(-90, -90, -180), max: new THREE.Vector3(90, 90, 45) },'LeftLowerArm': { min: new THREE.Vector3(0, -45, -45), max: new THREE.Vector3(145, 45, 45) },'RightLowerArm': { min: new THREE.Vector3(0, -45, -45), max: new THREE.Vector3(145, 45, 45) }};return limits[boneName];}// 获取极向量目标getPoleVectorTarget(chain) {const chainName = this.getChainName(chain);if (chainName.includes('Leg')) {return new THREE.Vector3(0, 1, 0); // 腿部向前弯曲} else if (chainName.includes('Arm')) {return new THREE.Vector3(0, -1, 0); // 手臂自然弯曲}return null;}// 计算极向量旋转calculatePoleVectorRotation(bone, chain, poleTarget) {// 简化的极向量约束实现const parent = chain[0];const child = chain[2];const parentPos = parent.getWorldPosition(new THREE.Vector3());const bonePos = bone.getWorldPosition(new THREE.Vector3());const childPos = child.getWorldPosition(new THREE.Vector3());const planeNormal = new THREE.Vector3().subVectors(childPos, parentPos).normalize();const toPole = new THREE.Vector3().subVectors(poleTarget, bonePos).normalize();const projectedPole = new THREE.Vector3().copy(toPole).projectOnPlane(planeNormal).normalize();const currentDir = new THREE.Vector3().subVectors(childPos, bonePos).normalize();const targetDir = new THREE.Vector3().subVectors(childPos, parentPos).normalize();const rotationAxis = new THREE.Vector3().crossVectors(currentDir, targetDir).normalize();const dotProduct = THREE.MathUtils.clamp(currentDir.dot(targetDir), -1, 1);const angle = Math.acos(dotProduct);return new THREE.Quaternion().setFromAxisAngle(rotationAxis, angle);}// 获取链名称getChainName(chain) {for (const [name, solver] of this.ikSolvers) {if (solver.boneChain === chain) {return name;}}return '';}// 更新IK系统update(deltaTime) {if (!this.isEnabled) return;let totalSolveTime = 0;let activeChains = 0;for (const [chainName, solver] of this.ikSolvers) {const target = this.ikTargets.get(chainName);if (target && this.isChainActive(chainName)) {const solveTime = solver.solve(target.getWorldPosition(new THREE.Vector3()));totalSolveTime += solveTime;activeChains++;// 应用约束this.applyConstraints(solver.boneChain);}}return {solveTime: totalSolveTime,activeChains: activeChains};}// 检查链是否激活isChainActive(chainName) {// 基于角色状态决定IK链是否激活return true; // 简化实现}// 应用约束applyConstraints(chain) {for (let i = 0; i < chain.length; i++) {const bone = chain[i];let rotation = new THREE.Euler().setFromQuaternion(bone.quaternion);for (const constraint of this.constraints.values()) {rotation = constraint.apply(bone, rotation, chain, i);}bone.quaternion.setFromEuler(rotation);bone.updateMatrixWorld(true);}}// 设置IK目标位置setIKTarget(chainName, position) {const target = this.ikTargets.get(chainName);if (target) {target.position.copy(position);}}// 获取IK目标getIKTarget(chainName) {return this.ikTargets.get(chainName);}
}// 角色类
class Character {constructor() {this.skeleton = new THREE.Group();this.bones = new Map();this.animations = new Map();this.currentAnimation = null;this.mixer = null;this.setupDefaultSkeleton();}// 设置默认骨骼setupDefaultSkeleton() {// 创建根骨骼const root = new THREE.Bone();root.name = 'Root';this.skeleton.add(root);this.bones.set('Root', root);// 创建脊柱链const spineBones = this.createBoneChain(root, 'Spine', 3, 2);// 创建头部const head = new THREE.Bone();head.name = 'Head';head.position.y = 1.5;spineBones[spineBones.length - 1].add(head);this.bones.set('Head', head);// 创建左腿const leftHip = new THREE.Bone();leftHip.name = 'LeftUpperLeg';leftHip.position.set(-0.5, 0, 0);root.add(leftHip);this.bones.set('LeftUpperLeg', leftHip);const leftKnee = new THREE.Bone();leftKnee.name = 'LeftLowerLeg';leftKnee.position.y = -2.5;leftHip.add(leftKnee);this.bones.set('LeftLowerLeg', leftKnee);const leftFoot = new THREE.Bone();leftFoot.name = 'LeftFoot';leftFoot.position.y = -2.5;leftKnee.add(leftFoot);this.bones.set('LeftFoot', leftFoot);// 创建右腿const rightHip = new THREE.Bone();rightHip.name = 'RightUpperLeg';rightHip.position.set(0.5, 0, 0);root.add(rightHip);this.bones.set('RightUpperLeg', rightHip);const rightKnee = new THREE.Bone();rightKnee.name = 'RightLowerLeg';rightKnee.position.y = -2.5;rightHip.add(rightKnee);this.bones.set('RightLowerLeg', rightKnee);const rightFoot = new THREE.Bone();rightFoot.name = 'RightFoot';rightFoot.position.y = -2.5;rightKnee.add(rightFoot);this.bones.set('RightFoot', rightFoot);// 创建左臂const leftShoulder = new THREE.Bone();leftShoulder.name = 'LeftUpperArm';leftShoulder.position.set(-1.5, 1.5, 0);spineBones[spineBones.length - 1].add(leftShoulder);this.bones.set('LeftUpperArm', leftShoulder);const leftElbow = new THREE.Bone();leftElbow.name = 'LeftLowerArm';leftElbow.position.y = -2.0;leftShoulder.add(leftElbow);this.bones.set('LeftLowerArm', leftElbow);const leftHand = new THREE.Bone();leftHand.name = 'LeftHand';leftHand.position.y = -2.0;leftElbow.add(leftHand);this.bones.set('LeftHand', leftHand);// 创建右臂const rightShoulder = new THREE.Bone();rightShoulder.name = 'RightUpperArm';rightShoulder.position.set(1.5, 1.5, 0);spineBones[spineBones.length - 1].add(rightShoulder);this.bones.set('RightUpperArm', rightShoulder);const rightElbow = new THREE.Bone();rightElbow.name = 'RightLowerArm';rightElbow.position.y = -2.0;rightShoulder.add(rightElbow);this.bones.set('RightLowerArm', rightElbow);const rightHand = new THREE.Bone();rightHand.name = 'RightHand';rightHand.position.y = -2.0;rightElbow.add(rightHand);this.bones.set('RightHand', rightHand);// 更新骨骼世界矩阵this.skeleton.updateMatrixWorld(true);}// 创建骨骼链createBoneChain(parent, prefix, count, spacing) {const bones = [];let currentParent = parent;for (let i = 0; i < count; i++) {const bone = new THREE.Bone();bone.name = `${prefix}${i + 1}`;bone.position.y = spacing;currentParent.add(bone);this.bones.set(bone.name, bone);bones.push(bone);currentParent = bone;}return bones;}// 获取骨骼getBone(name) {return this.bones.get(name);}// 播放动画playAnimation(animationName) {const animation = this.animations.get(animationName);if (animation) {if (this.mixer) {this.mixer.stopAllAction();} else {this.mixer = new THREE.AnimationMixer(this.skeleton);}const action = this.mixer.clipAction(animation);action.play();this.currentAnimation = animationName;}}// 更新动画update(deltaTime) {if (this.mixer) {this.mixer.update(deltaTime);}}
}export default {name: 'IKAnimationSystem',setup() {const animationCanvas = ref(null);const characterScale = ref(1.0);const legIKEnabled = ref(true);const armIKEnabled = ref(true);const footRotation = ref(0);const kneeBend = ref(45);const elbowBend = ref(90);const wristRotation = ref(0);const animationSpeed = ref(1.0);const blendWeight = ref(1.0);const jointLimitsEnabled = ref(true);const groundAdaptation = ref(true);const balanceControl = ref(true);const physicsEnabled = ref(false);const showSkeleton = ref(true);const showIKTargets = ref(true);const showConstraints = ref(false);const ikSolveTime = ref(0);const boneCount = ref(0);const activeIKChains = ref(0);const currentFPS = ref(0);const currentPose = ref('Idle');const balanceStatus = ref('Balanced');const contactPoints = ref(2);const animationWeight = ref(1.0);const showEnvironment = ref(true);const isLoading = ref(true);const loadingMessage = ref('初始化角色骨骼...');const joystickPosition = reactive({ x: 0, y: 0 });const characterPresets = [{ id: 'human', name: '人形角色' },{ id: 'robot', name: '机器人' },{ id: 'creature', name: '生物' }];const animationPresets = [{ id: 'idle', name: '待机' },{ id: 'walk', name: '行走' },{ id: 'run', name: '奔跑' },{ id: 'jump', name: '跳跃' },{ id: 'wave', name: '挥手' },{ id: 'dance', name: '舞蹈' }];let currentCharacter = ref(characterPresets[0]);let currentAnimation = ref(animationPresets[0]);let scene, camera, renderer, controls;let character, ikSystem;let clock, stats;let frameCount = 0;let lastFpsUpdate = 0;// 初始化场景const initScene = async () => {// 创建场景scene = new THREE.Scene();scene.background = new THREE.Color(0x87ceeb);// 创建相机camera = new THREE.PerspectiveCamera(75,window.innerWidth / window.innerHeight,0.1,1000);camera.position.set(5, 5, 5);// 创建渲染器renderer = new THREE.WebGLRenderer({canvas: animationCanvas.value,antialias: true});renderer.setSize(window.innerWidth, window.innerHeight);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));renderer.shadowMap.enabled = true;// 添加控制器controls = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true;// 创建环境setupEnvironment();// 创建角色loadingMessage.value = '创建角色骨骼...';await createCharacter();// 初始化IK系统loadingMessage.value = '初始化IK系统...';await setupIKSystem();isLoading.value = false;// 启动渲染循环clock = new THREE.Clock();animate();};// 设置环境const setupEnvironment = () => {// 添加环境光const ambientLight = new THREE.AmbientLight(0x404040, 0.6);scene.add(ambientLight);// 添加方向光const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);directionalLight.position.set(10, 10, 5);directionalLight.castShadow = true;scene.add(directionalLight);// 创建地面const groundGeometry = new THREE.PlaneGeometry(20, 20);const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x7cfc00,roughness: 0.8,metalness: 0.2});const ground = new THREE.Mesh(groundGeometry, groundMaterial);ground.rotation.x = -Math.PI / 2;ground.receiveShadow = true;scene.add(ground);// 添加网格辅助const gridHelper = new THREE.GridHelper(20, 20);scene.add(gridHelper);};// 创建角色const createCharacter = async () => {character = new Character();scene.add(character.skeleton);boneCount.value = character.bones.size;// 创建简单的骨骼可视化if (showSkeleton.value) {createSkeletonVisualization();}};// 创建骨骼可视化const createSkeletonVisualization = () => {// 为每个骨骼创建球体表示for (const bone of character.bones.values()) {const geometry = new THREE.SphereGeometry(0.1);const material = new THREE.MeshBasicMaterial({ color: 0xff0000,transparent: true,opacity: 0.7});const sphere = new THREE.Mesh(geometry, material);bone.add(sphere);}// 创建骨骼连接线createBoneConnections();};// 创建骨骼连接const createBoneConnections = () => {// 简化的骨骼连接创建const connections = [['Root', 'LeftUpperLeg'], ['LeftUpperLeg', 'LeftLowerLeg'], ['LeftLowerLeg', 'LeftFoot'],['Root', 'RightUpperLeg'], ['RightUpperLeg', 'RightLowerLeg'], ['RightLowerLeg', 'RightFoot'],['Root', 'Spine1'], ['Spine1', 'Spine2'], ['Spine2', 'Spine3'],['Spine3', 'LeftUpperArm'], ['LeftUpperArm', 'LeftLowerArm'], ['LeftLowerArm', 'LeftHand'],['Spine3', 'RightUpperArm'], ['RightUpperArm', 'RightLowerArm'], ['RightLowerArm', 'RightHand'],['Spine3', 'Head']];for (const [from, to] of connections) {const fromBone = character.getBone(from);const toBone = character.getBone(to);if (fromBone && toBone) {const lineMaterial = new THREE.LineBasicMaterial({ color: 0x00ff00 });const points = [new THREE.Vector3(0, 0, 0),toBone.position.clone()];const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);const line = new THREE.Line(lineGeometry, lineMaterial);fromBone.add(line);}}};// 设置IK系统const setupIKSystem = async () => {ikSystem = new CharacterIKSystem(character);activeIKChains.value = ikSystem.ikSolvers.size;// 添加IK目标可视化if (showIKTargets.value) {createIKTargetVisualization();}// 设置初始IK目标位置setupInitialIKTargets();};// 创建IK目标可视化const createIKTargetVisualization = () => {for (const target of ikSystem.ikTargets.values()) {const geometry = new THREE.SphereGeometry(0.15);const material = new THREE.MeshBasicMaterial({ color: 0x0000ff,transparent: true,opacity: 0.6});const sphere = new THREE.Mesh(geometry, material);target.add(sphere);scene.add(target);}};// 设置初始IK目标const setupInitialIKTargets = () => {// 设置腿部IK目标ikSystem.setIKTarget('leftLeg', new THREE.Vector3(-0.5, 0, 2));ikSystem.setIKTarget('rightLeg', new THREE.Vector3(0.5, 0, -2));// 设置手臂IK目标ikSystem.setIKTarget('leftArm', new THREE.Vector3(-2, 2, 0));ikSystem.setIKTarget('rightArm', new THREE.Vector3(2, 2, 0));};// 加载角色预设const loadCharacterPreset = (preset) => {currentCharacter.value = preset;// 实际实现应该加载不同的角色配置console.log('加载角色预设:', preset.name);};// 播放动画const playAnimation = (animation) => {currentAnimation.value = animation;currentPose.value = animation.name;// 实际实现应该播放对应的动画console.log('播放动画:', animation.name);};// 虚拟摇杆控制const startJoystick = (event) => {event.preventDefault();const joystickBase = event.currentTarget;const handleJoystickMove = (moveEvent) => {const rect = joystickBase.getBoundingClientRect();const centerX = rect.left + rect.width / 2;const centerY = rect.top + rect.height / 2;const clientX = moveEvent.clientX || moveEvent.touches[0].clientX;const clientY = moveEvent.clientY || moveEvent.touches[0].clientY;const deltaX = clientX - centerX;const deltaY = clientY - centerY;const distance = Math.min(Math.sqrt(deltaX * deltaX + deltaY * deltaY), rect.width / 2);const angle = Math.atan2(deltaY, deltaX);joystickPosition.x = (distance * Math.cos(angle)) / (rect.width / 2);joystickPosition.y = (distance * Math.sin(angle)) / (rect.height / 2);// 更新角色移动updateCharacterMovement();};const stopJoystick = () => {document.removeEventListener('mousemove', handleJoystickMove);document.removeEventListener('touchmove', handleJoystickMove);document.removeEventListener('mouseup', stopJoystick);document.removeEventListener('touchend', stopJoystick);joystickPosition.x = 0;joystickPosition.y = 0;};document.addEventListener('mousemove', handleJoystickMove);document.addEventListener('touchmove', handleJoystickMove);document.addEventListener('mouseup', stopJoystick);document.addEventListener('touchend', stopJoystick);};// 更新角色移动const updateCharacterMovement = () => {// 基于摇杆输入更新角色位置和IK目标const moveSpeed = 0.1;const rootBone = character.getBone('Root');if (rootBone) {rootBone.position.x += joystickPosition.x * moveSpeed;rootBone.position.z += joystickPosition.y * moveSpeed;rootBone.updateMatrixWorld(true);}// 更新IK目标位置updateIKTargets();};// 更新IK目标const updateIKTargets = () => {// 基于角色状态更新IK目标位置const time = performance.now() * 0.001;// 腿部IK目标动画if (legIKEnabled.value) {const leftFootTarget = ikSystem.getIKTarget('leftLeg');const rightFootTarget = ikSystem.getIKTarget('rightLeg');if (leftFootTarget && rightFootTarget) {leftFootTarget.position.y = Math.sin(time * 2) * 0.5;rightFootTarget.position.y = Math.cos(time * 2) * 0.5;}}// 手臂IK目标动画if (armIKEnabled.value) {const leftHandTarget = ikSystem.getIKTarget('leftArm');const rightHandTarget = ikSystem.getIKTarget('rightArm');if (leftHandTarget && rightHandTarget) {leftHandTarget.position.x = -2 + Math.sin(time) * 0.5;leftHandTarget.position.y = 2 + Math.cos(time) * 0.3;rightHandTarget.position.x = 2 + Math.cos(time) * 0.5;rightHandTarget.position.y = 2 + Math.sin(time) * 0.3;}}};// 切换环境显示const toggleEnvironment = () => {showEnvironment.value = !showEnvironment.value;// 实际实现应该切换环境物体的可见性};// 重置场景const resetScene = () => {const rootBone = character.getBone('Root');if (rootBone) {rootBone.position.set(0, 0, 0);rootBone.rotation.set(0, 0, 0);rootBone.updateMatrixWorld(true);}joystickPosition.x = 0;joystickPosition.y = 0;};// 动画循环const animate = () => {requestAnimationFrame(animate);const deltaTime = clock.getDelta();// 更新控制器controls.update();// 更新角色动画if (character) {character.update(deltaTime * animationSpeed.value);}// 更新IK系统if (ikSystem) {const ikStats = ikSystem.update(deltaTime);ikSolveTime.value = ikStats.solveTime;activeIKChains.value = ikStats.activeChains;}// 更新IK目标updateIKTargets();// 渲染场景renderer.render(scene, camera);// 更新性能统计updatePerformanceStats(deltaTime);};// 更新性能统计const updatePerformanceStats = (deltaTime) => {frameCount++;lastFpsUpdate += deltaTime;if (lastFpsUpdate >= 1.0) {currentFPS.value = Math.round(frameCount / lastFpsUpdate);frameCount = 0;lastFpsUpdate = 0;}};// 计算属性const joystickStyle = computed(() => ({transform: `translate(${joystickPosition.x * 20}px, ${joystickPosition.y * 20}px)`}));const loadingProgressStyle = computed(() => ({width: '100%'}));onMounted(() => {initScene();window.addEventListener('resize', handleResize);});onUnmounted(() => {if (renderer) {renderer.dispose();}window.removeEventListener('resize', handleResize);});const handleResize = () => {if (!camera || !renderer) return;camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth, window.innerHeight);};return {animationCanvas,characterScale,legIKEnabled,armIKEnabled,footRotation,kneeBend,elbowBend,wristRotation,animationSpeed,blendWeight,jointLimitsEnabled,groundAdaptation,balanceControl,physicsEnabled,showSkeleton,showIKTargets,showConstraints,ikSolveTime,boneCount,activeIKChains,currentFPS,currentPose,balanceStatus,contactPoints,animationWeight,showEnvironment,isLoading,loadingMessage,characterPresets,animationPresets,currentCharacter,currentAnimation,joystickPosition,joystickStyle,loadingProgressStyle,loadCharacterPreset,playAnimation,startJoystick,toggleEnvironment,resetScene};}
};
</script><style scoped>
.ik-animation-container {width: 100%;height: 100vh;position: relative;background: #1a1a1a;overflow: hidden;
}.animation-canvas {width: 100%;height: 100%;display: block;
}.character-controls {position: absolute;top: 20px;right: 20px;width: 350px;background: rgba(0, 0, 0, 0.9);padding: 20px;border-radius: 12px;color: white;backdrop-filter: blur(10px);border: 1px solid rgba(255, 255, 255, 0.1);max-height: 80vh;overflow-y: auto;
}.control-section {margin-bottom: 25px;padding-bottom: 15px;border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}.control-section:last-child {margin-bottom: 0;border-bottom: none;
}.control-section h3 {color: #00ff88;margin-bottom: 15px;font-size: 16px;display: flex;align-items: center;gap: 8px;
}.character-presets {display: grid;grid-template-columns: 1fr 1fr;gap: 10px;margin-bottom: 15px;
}.preset-button {padding: 10px;border: 2px solid #444;border-radius: 6px;background: rgba(255, 255, 255, 0.1);color: white;cursor: pointer;font-size: 12px;transition: all 0.3s ease;
}.preset-button:hover {border-color: #00ff88;
}.preset-button.active {border-color: #00ff88;background: rgba(0, 255, 136, 0.2);
}.control-group {margin-bottom: 15px;
}.control-group label {display: flex;justify-content: space-between;align-items: center;margin-bottom: 8px;color: #ccc;font-size: 14px;
}.control-group input[type="range"] {width: 100%;height: 6px;background: #444;border-radius: 3px;outline: none;opacity: 0.7;transition: opacity 0.2s;
}.control-group input[type="range"]:hover {opacity: 1;
}.control-group input[type="range"]::-webkit-slider-thumb {appearance: none;width: 16px;height: 16px;border-radius: 50%;background: #00ff88;cursor: pointer;
}.control-group input[type="checkbox"] {width: 18px;height: 18px;accent-color: #00ff88;
}.ik-chain-controls {display: grid;grid-template-columns: 1fr 1fr;gap: 20px;
}.chain-section h4 {color: #00aaff;margin-bottom: 10px;font-size: 14px;
}.animation-presets {display: grid;grid-template-columns: 1fr 1fr;gap: 8px;margin-bottom: 15px;
}.anim-button {padding: 8px;border: 1px solid #444;border-radius: 4px;background: rgba(255, 255, 255, 0.05);color: white;cursor: pointer;font-size: 11px;transition: all 0.3s ease;
}.anim-button:hover {border-color: #00aaff;
}.anim-button.active {border-color: #00aaff;background: rgba(0, 170, 255, 0.2);
}.animation-controls {display: grid;grid-template-columns: 1fr 1fr;gap: 15px;margin-bottom: 15px;
}.motion-controls {display: flex;justify-content: center;margin-top: 15px;
}.joystick-container {text-align: center;
}.joystick-label {color: #ccc;margin-bottom: 10px;font-size: 12px;
}.virtual-joystick {width: 80px;height: 80px;background: rgba(255, 255, 255, 0.1);border-radius: 50%;position: relative;cursor: pointer;border: 2px solid rgba(255, 255, 255, 0.3);
}.joystick-base {width: 100%;height: 100%;position: relative;
}.joystick-handle {width: 30px;height: 30px;background: #00ff88;border-radius: 50%;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);transition: transform 0.1s ease;box-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
}.constraint-controls,
.debug-controls {display: grid;grid-template-columns: 1fr 1fr;gap: 10px;
}.performance-stats {display: flex;flex-direction: column;gap: 8px;
}.stat-item {display: flex;justify-content: space-between;align-items: center;padding: 6px 0;font-size: 12px;
}.stat-item span:first-child {color: #ccc;
}.stat-item span:last-child {color: #00ff88;font-weight: bold;
}.character-status {position: absolute;top: 20px;left: 20px;background: rgba(0, 0, 0, 0.8);padding: 15px;border-radius: 8px;color: white;backdrop-filter: blur(10px);border: 1px solid rgba(255, 255, 255, 0.1);
}.status-panel h4 {color: #00ff88;margin-bottom: 10px;font-size: 14px;
}.status-content {display: flex;flex-direction: column;gap: 5px;font-size: 12px;
}.status-content div {display: flex;justify-content: space-between;gap: 10px;
}.environment-controls {position: absolute;bottom: 20px;left: 20px;display: flex;gap: 10px;
}.env-button {padding: 10px 15px;border: none;border-radius: 6px;background: rgba(255, 255, 255, 0.1);color: white;cursor: pointer;font-size: 12px;transition: background 0.3s;
}.env-button:hover {background: rgba(255, 255, 255, 0.2);
}.loading-overlay {position: absolute;top: 0;left: 0;width: 100%;height: 100%;background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);display: flex;justify-content: center;align-items: center;z-index: 1000;
}.loading-content {text-align: center;color: white;
}.skeleton-loader {display: flex;justify-content: center;align-items: center;gap: 10px;margin-bottom: 30px;height: 60px;
}.bone-segment {width: 8px;height: 40px;background: white;border-radius: 4px;animation: bonePulse 1.5s infinite ease-in-out;
}.bone-segment:nth-child(1) { animation-delay: 0s; }
.bone-segment:nth-child(2) { animation-delay: 0.2s; }
.bone-segment:nth-child(3) { animation-delay: 0.4s; }
.bone-segment:nth-child(4) { animation-delay: 0.6s; }.loading-content h3 {margin-bottom: 20px;color: white;font-size: 20px;
}.loading-progress {width: 300px;margin: 0 auto;
}.progress-bar {width: 100%;height: 6px;background: rgba(255, 255, 255, 0.2);border-radius: 3px;overflow: hidden;margin-bottom: 10px;
}.progress-fill {height: 100%;background: linear-gradient(90deg, #00ff88, #00aaff);border-radius: 3px;transition: width 0.3s ease;
}@keyframes bonePulse {0%, 100% {transform: scaleY(1);opacity: 0.7;}50% {transform: scaleY(1.5);opacity: 1;}
}/* 响应式设计 */
@media (max-width: 768px) {.character-controls {width: 300px;right: 10px;top: 10px;padding: 15px;}.ik-chain-controls {grid-template-columns: 1fr;}.animation-presets {grid-template-columns: 1fr;}.character-status {left: 10px;top: 10px;}
}
</style>
高级IK特性实现
自然动作混合系统
// 动作混合器
class MotionBlender {constructor(character) {this.character = character;this.layers = new Map();this.transitionTime = 0.3;this.currentTransition = null;}// 添加动作层addLayer(name, animation, weight = 1.0, mask = null) {this.layers.set(name, {animation: animation,weight: weight,mask: mask,action: null,enabled: true});}// 混合动作blendAnimations(basePose, deltaTime) {let finalPose = this.clonePose(basePose);for (const [name, layer] of this.layers) {if (layer.enabled && layer.weight > 0) {const layerPose = this.getLayerPose(layer, deltaTime);finalPose = this.blendPoses(finalPose, layerPose, layer.weight, layer.mask);}}return finalPose;}// 获取层姿势getLayerPose(layer, deltaTime) {if (layer.action) {layer.action.time += deltaTime;}return this.sampleAnimation(layer.animation, layer.action?.time || 0);}// 采样动画sampleAnimation(animation, time) {const pose = {};// 实现动画采样逻辑return pose;}// 混合姿势blendPoses(pose1, pose2, weight, mask) {const blendedPose = { ...pose1 };for (const boneName in pose2) {if (!mask || mask[boneName]) {const bone1 = pose1[boneName];const bone2 = pose2[boneName];blendedPose[boneName] = {position: bone1.position.lerp(bone2.position, weight),rotation: bone1.rotation.slerp(bone2.rotation, weight),scale: bone1.scale.lerp(bone2.scale, weight)};}}return blendedPose;}// 克隆姿势clonePose(pose) {const cloned = {};for (const boneName in pose) {cloned[boneName] = {position: pose[boneName].position.clone(),rotation: pose[boneName].rotation.clone(),scale: pose[boneName].scale.clone()};}return cloned;}// 过渡到新动作transitionTo(newAnimation, transitionTime = 0.3) {this.currentTransition = {fromPose: this.getCurrentPose(),toAnimation: newAnimation,progress: 0,duration: transitionTime};}// 更新过渡updateTransition(deltaTime) {if (this.currentTransition) {this.currentTransition.progress += deltaTime / this.currentTransition.duration;if (this.currentTransition.progress >= 1) {this.currentTransition = null;}}}
}
物理集成系统
// 物理IK集成器
class PhysicsIKIntegrator {constructor(ikSystem, physicsWorld) {this.ikSystem = ikSystem;this.physicsWorld = physicsWorld;this.ragdollBodies = new Map();this.blendWeight = 0;this.setupRagdoll();}// 设置布娃娃系统setupRagdoll() {// 为每个重要骨骼创建物理体const bonesToSimulate = ['Hip', 'Spine', 'Head', 'LeftUpperArm', 'LeftLowerArm', 'LeftHand','RightUpperArm', 'RightLowerArm', 'RightHand','LeftUpperLeg', 'LeftLowerLeg', 'LeftFoot','RightUpperLeg', 'RightLowerLeg', 'RightFoot'];for (const boneName of bonesToSimulate) {const bone = this.ikSystem.character.getBone(boneName);if (bone) {const body = this.createPhysicsBody(bone);this.ragdollBodies.set(boneName, body);// 创建约束this.createPhysicsConstraints(bone, body);}}}// 创建物理体createPhysicsBody(bone) {// 根据骨骼类型创建不同形状的物理体const boneSize = this.getBoneSize(bone.name);const shape = new CANNON.Sphere(boneSize.radius);const body = new CANNON.Body({mass: boneSize.mass,position: new CANNON.Vec3(bone.getWorldPosition(new THREE.Vector3()).x,bone.getWorldPosition(new THREE.Vector3()).y,bone.getWorldPosition(new THREE.Vector3()).z)});body.addShape(shape);this.physicsWorld.addBody(body);return body;}// 获取骨骼尺寸getBoneSize(boneName) {const sizes = {'Hip': { radius: 0.2, mass: 10 },'Spine': { radius: 0.15, mass: 5 },'Head': { radius: 0.18, mass: 4 },'UpperArm': { radius: 0.08, mass: 2 },'LowerArm': { radius: 0.06, mass: 1.5 },'Hand': { radius: 0.05, mass: 1 },'UpperLeg': { radius: 0.1, mass: 4 },'LowerLeg': { radius: 0.08, mass: 3 },'Foot': { radius: 0.07, mass: 2 }};for (const [key, value] of Object.entries(sizes)) {if (boneName.includes(key)) {return value;}}return { radius: 0.1, mass: 2 };}// 创建物理约束createPhysicsConstraints(bone, body) {const parent = bone.parent;if (parent && this.ragdollBodies.has(parent.name)) {const parentBody = this.ragdollBodies.get(parent.name);const constraint = new CANNON.PointToPointConstraint(body,new CANNON.Vec3(0, 0, 0),parentBody,new CANNON.Vec3(bone.position.x,bone.position.y, bone.position.z));this.physicsWorld.addConstraint(constraint);}}// 更新物理集成update(deltaTime) {if (this.blendWeight > 0) {this.syncPhysicsToAnimation();}}// 同步物理到动画syncPhysicsToAnimation() {for (const [boneName, body] of this.ragdollBodies) {const bone = this.ikSystem.character.getBone(boneName);if (bone) {// 混合动画和物理位置const animPos = bone.getWorldPosition(new THREE.Vector3());const physicsPos = new THREE.Vector3(body.position.x,body.position.y,body.position.z);const blendedPos = animPos.lerp(physicsPos, this.blendWeight);bone.position.copy(blendedPos);// 更新骨骼变换bone.updateMatrixWorld(true);}}}// 启用物理enablePhysics(blendTime = 0.5) {this.startBlendToPhysics(blendTime);}// 开始混合到物理startBlendToPhysics(blendTime) {this.blendWeight = 0;this.targetBlendWeight = 1;this.blendSpeed = 1 / blendTime;}// 禁用物理disablePhysics(blendTime = 0.5) {this.targetBlendWeight = 0;this.blendSpeed = 1 / blendTime;}
}
注意事项与最佳实践
-
性能优化策略
- 选择合适的IK算法(CCD用于实时,FABRIK用于质量)
- 限制迭代次数和求解精度
- 使用骨骼LOD系统
- 批量处理相似的IK链
-
动画质量优化
- 实现自然的动作过渡
- 使用约束防止不自然的关节角度
- 添加次要动作(如呼吸、微移动)
- 实现地面适配和平衡控制
-
物理集成要点
- 合理设置物理体质量和约束
- 实现平滑的动画-物理混合
- 处理碰撞和穿透问题
- 优化物理更新频率
下一节预告
第35节:全局光照与路径追踪探索
将深入探索现代光照技术,包括:实时全局光照方案、路径追踪原理、屏幕空间反射、光照探针系统,实现电影级渲染效果。
