本实例主要讲解内容
这个Three.js示例展示了**反向运动学(Inverse Kinematics, IK)**在3D角色动画中的应用。通过加载一个角色模型,演示了如何使用IK技术实现自然的肢体运动控制,如手部抓取物体的动作。
核心技术包括:
- CCD反向运动学求解器
- 实时IK计算与应用
- 角色头部跟踪
- 镜面反射效果
- 交互式控制器
完整代码注释
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - animation - skinning - ik</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="author" content="Antoine BERNIER (abernier)" />
<link type="text/css" rel="stylesheet" href="main.css">
<style>
body {color:white;}
#info a {
color:#4d6675;
}
</style>
</head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - webgl - inverse kinematics<br />
Character model by <a href="https://assetstore.unity.com/packages/3d/characters/humanoids/humans/kira-lowpoly-character-100303" target="_blank" rel="noopener">Aki</a>, furnitures from <a href="https://poly.pizza" target="_blank" rel="noopener">poly.pizza</a>, scene by <a href="https://abernier.name/three.js/examples/webgl_esher.html" target="_blank" rel="noopener">abernier</a>. CC0.
</div>
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { CCDIKSolver, CCDIKHelper } from 'three/addons/animation/CCDIKSolver.js';
import Stats from 'three/addons/libs/stats.module.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
let scene, camera, renderer, orbitControls, transformControls;
let mirrorSphereCamera;
const OOI = {}; // 感兴趣的对象集合
let IKSolver; // IK求解器
let stats, gui, conf; // 统计面板、控制面板和配置对象
const v0 = new THREE.Vector3(); // 临时向量,用于计算
init();
async function init() {
conf = {
followSphere: false, // 相机是否跟随球体
turnHead: true, // 头部是否转向球体
ik_solver: true, // 是否自动更新IK
update: updateIK // 手动更新IK的函数
};
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2( 0xffffff, .17 ); // 设置指数雾
scene.background = new THREE.Color( 0xffffff ); // 设置背景色为白色
camera = new THREE.PerspectiveCamera( 55, window.innerWidth / window.innerHeight, 0.001, 5000 );
camera.position.set( 0.9728517749133652, 1.1044765132727201, 0.7316689528482836 );
camera.lookAt( scene.position );
// 添加环境光,照亮整个场景
const ambientLight = new THREE.AmbientLight( 0xffffff, 8 ); // 柔和的白色光
scene.add( ambientLight );
// 初始化DRACO加载器,用于加载压缩的GLTF模型
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'jsm/libs/draco/' );
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader( dracoLoader );
// 加载GLTF模型
const gltf = await gltfLoader.loadAsync( 'models/gltf/kira.glb' );
gltf.scene.traverse( n => {
// 收集感兴趣的对象,用于后续控制
if ( n.name === 'head' ) OOI.head = n;
if ( n.name === 'lowerarm_l' ) OOI.lowerarm_l = n;
if ( n.name === 'Upperarm_l' ) OOI.Upperarm_l = n;
if ( n.name === 'hand_l' ) OOI.hand_l = n;
if ( n.name === 'target_hand_l' ) OOI.target_hand_l = n;
if ( n.name === 'boule' ) OOI.sphere = n; // 球体对象
if ( n.name === 'Kira_Shirt_left' ) OOI.kira = n; // 角色主体
} );
scene.add( gltf.scene );
// 记录球体的初始位置,用于轨道控制器
const targetPosition = OOI.sphere.position.clone();
// 将球体附加到角色的左手上,使其跟随手部移动
OOI.hand_l.attach( OOI.sphere );
// 创建镜面球体的立方相机
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 1024 );
mirrorSphereCamera = new THREE.CubeCamera( 0.05, 50, cubeRenderTarget );
scene.add( mirrorSphereCamera );
// 使用立方相机的渲染结果作为球体的环境贴图,实现镜面效果
const mirrorSphereMaterial = new THREE.MeshBasicMaterial( { envMap: cubeRenderTarget.texture } );
OOI.sphere.material = mirrorSphereMaterial;
// 将角色的骨骼根节点添加到角色对象中
OOI.kira.add( OOI.kira.skeleton.bones[ 0 ] );
// 设置IK求解器配置
const iks = [
{
target: 22, // "target_hand_l" 目标对象ID
effector: 6, // "hand_l" 效应器ID(手)
links: [
{
index: 5, // "lowerarm_l" 下臂
rotationMin: new THREE.Vector3( 1.2, - 1.8, - .4 ), // 最小旋转角度
rotationMax: new THREE.Vector3( 1.7, - 1.1, .3 ) // 最大旋转角度
},
{
index: 4, // "Upperarm_l" 上臂
rotationMin: new THREE.Vector3( 0.1, - 0.7, - 1.8 ),
rotationMax: new THREE.Vector3( 1.1, 0, - 1.4 )
},
],
}
];
// 创建CCDIK求解器,用于计算反向运动学
IKSolver = new CCDIKSolver( OOI.kira, iks );
// 创建IK辅助工具,可视化IK链
const ccdikhelper = new CCDIKHelper( OOI.kira, iks, 0.01 );
scene.add( ccdikhelper );
// 创建控制面板
gui = new GUI();
gui.add( conf, 'followSphere' ).name( 'follow sphere' ); // 相机是否跟随球体
gui.add( conf, 'turnHead' ).name( 'turn head' ); // 头部是否转向球体
gui.add( conf, 'ik_solver' ).name( 'IK auto update' ); // 是否自动更新IK
gui.add( conf, 'update' ).name( 'IK manual update()' ); // 手动更新IK按钮
gui.open();
// 初始化渲染器
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate ); // 设置动画循环
document.body.appendChild( renderer.domElement );
// 初始化轨道控制器,允许用户旋转和缩放相机
orbitControls = new OrbitControls( camera, renderer.domElement );
orbitControls.minDistance = 0.2;
orbitControls.maxDistance = 1.5;
orbitControls.enableDamping = true; // 启用阻尼效果,使相机移动更平滑
orbitControls.target.copy( targetPosition ); // 设置控制器目标位置
// 初始化变换控制器,允许用户交互式移动、旋转和缩放对象
transformControls = new TransformControls( camera, renderer.domElement );
transformControls.size = 0.75;
transformControls.showX = false; // 不显示X轴
transformControls.space = 'world'; // 在世界坐标系下操作
transformControls.attach( OOI.target_hand_l ); // 附加到左手目标对象
scene.add( transformControls.getHelper() ); // 添加控制器辅助工具
// 当使用变换控制器时,禁用轨道控制器
transformControls.addEventListener( 'mouseDown', () => orbitControls.enabled = false );
transformControls.addEventListener( 'mouseUp', () => orbitControls.enabled = true );
// 添加性能统计面板
stats = new Stats();
document.body.appendChild( stats.dom );
// 添加窗口大小变化事件监听
window.addEventListener( 'resize', onWindowResize, false );
}
function animate( ) {
if ( OOI.sphere && mirrorSphereCamera ) {
// 更新镜面球体的反射效果
OOI.sphere.visible = false; // 临时隐藏球体,避免反射自身
OOI.sphere.getWorldPosition( mirrorSphereCamera.position ); // 将相机放置在球体位置
mirrorSphereCamera.update( renderer, scene ); // 更新立方相机渲染
OOI.sphere.visible = true; // 重新显示球体
}
if ( OOI.sphere && conf.followSphere ) {
// 相机跟随球体
OOI.sphere.getWorldPosition( v0 );
orbitControls.target.lerp( v0, 0.1 ); // 平滑过渡到球体位置
}
if ( OOI.head && OOI.sphere && conf.turnHead ) {
// 头部转向球体
OOI.sphere.getWorldPosition( v0 );
OOI.head.lookAt( v0 );
// 调整头部旋转,使其看起来更自然
OOI.head.rotation.set( OOI.head.rotation.x, OOI.head.rotation.y + Math.PI, OOI.head.rotation.z );
}
if ( conf.ik_solver ) {
// 更新IK求解器
updateIK();
}
orbitControls.update(); // 更新轨道控制器
renderer.render( scene, camera ); // 渲染场景
stats.update(); // 更新性能统计
}
function updateIK() {
// 更新IK求解器
if ( IKSolver ) IKSolver.update();
// 重新计算所有蒙皮网格的边界球体
scene.traverse( function ( object ) {
if ( object.isSkinnedMesh ) object.computeBoundingSphere();
} );
}
function onWindowResize() {
// 窗口大小变化时调整相机和渲染器
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
</script>
</body>
</html>
反向运动学原理与应用
反向运动学(IK)是计算机动画中的重要技术,与正向运动学(FK)相对。
正向运动学与反向运动学的区别
正向运动学(FK):从父关节到子关节的运动传递方式。例如,当你移动肩膀时,上臂、下臂和手都会随之移动。这是传统骨骼动画的工作方式。
反向运动学(IK):根据末端效应器(如手或脚)的目标位置,计算出所有关节的旋转角度。例如,当你指定手要抓住某个物体时,IK系统会自动计算出肩膀、上臂和下臂的正确角度。
CCD IK求解器
本示例使用了Three.js提供的**CCDIK(Cyclic Coordinate Descent)**求解器:
工作原理:从末端效应器开始,逐个调整关节角度,使效应器逐渐接近目标位置,直到达到收敛条件或最大迭代次数。
参数配置:
target
:目标位置对象IDeffector
:末端效应器IDlinks
:关节链,每个关节有最小和最大旋转限制- 可以配置多个独立的IK链
IK在游戏和动画中的应用
IK技术在游戏和动画中有广泛应用:
- 角色交互:角色抓取物体、攀爬、游泳等动作
- 脚部放置:角色在不平整地面行走时自动调整脚部位置和姿态
- 面部表情:控制面部骨骼实现表情动画
- 物理模拟:与物理引擎结合实现更真实的动作
IK技术可以大大减少动画师的工作量,尤其是对于复杂的肢体运动。同时,它也能使角色行为更加自然,增强游戏和虚拟环境的沉浸感。