Animation Blend
Runnable three.js example: Animation Blend. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
说明:两个动画片段同时作用于同一套骨架,用权重滑块混合出不同的摆臂姿态
核心逻辑:AnimationMixer 播放两条 AnimationClip,用 action 的有效权重决定混合比例
依赖:three.js 0.160.0 · jsDelivr CDN(ESM + importmap)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>动画混合</title>
<style>
html,body{height:100%;margin:0;background:#0d1117}
body{display:flex;flex-direction:column;overflow:hidden;
font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif;color:#e6e8ec}
.bar{flex:0 0 auto;padding:8px 12px;background:#161b22;border-bottom:1px solid #252c36;
display:flex;gap:14px;align-items:center;font-size:12px;color:#8b93a1;flex-wrap:wrap}
.bar b{color:#58a6ff;font-weight:600}
.bar input[type=range]{width:120px;accent-color:#4f9ef8}
.bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:5px;cursor:pointer;
border:1px solid #303845;background:#1d2531;color:#c8d2e0}
.bar button:hover{background:#253040}
.bar .sp{flex:1 1 auto}
#stage{flex:1 1 auto;min-height:0;position:relative}
#stage canvas{display:block}
</style>
</head>
<body>
<div class="bar">
<label>上臂 A <input type="range" id="wa" min="0" max="1" step="0.01" value="0.6"></label>
<span><b id="wav">0.60</b></span>
<label>小臂 B <input type="range" id="wb" min="0" max="1" step="0.01" value="0.35"></label>
<span><b id="wbv">0.35</b></span>
<button id="pause">暂停</button>
<span class="sp"></span>
<span>时间 <b id="time">0.00</b> s</span>
</div>
<div id="stage"></div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const stage = document.getElementById('stage');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b1016);
scene.fog = new THREE.Fog(0x0b1016, 40, 90);
const camera = new THREE.PerspectiveCamera(
42, stage.clientWidth / stage.clientHeight, 0.5, 200
);
camera.position.set(11, 10, 14);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 7, 0);
controls.enableDamping = true;
controls.minDistance = 8;
controls.maxDistance = 40;
controls.maxPolarAngle = Math.PI * 0.49;
controls.update();
scene.add(new THREE.HemisphereLight(0xbcd8ff, 0x16202a, 1.15));
const sun = new THREE.DirectionalLight(0xfff0d8, 2.3);
sun.position.set(-14, 24, 16);
scene.add(sun);
const ground = new THREE.Mesh(
new THREE.CircleGeometry(22, 60),
new THREE.MeshStandardMaterial({ color: 0x151d26, roughness: 1 })
);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
/* ---------- 用基本体拼一条两节机械臂 ---------- */
const rig = new THREE.Group();
rig.name = 'rig';
scene.add(rig);
const baseMat = new THREE.MeshStandardMaterial({ color: 0x46566a, roughness: 0.5, metalness: 0.35 });
const armMat = new THREE.MeshStandardMaterial({ color: 0x7f97b3, roughness: 0.45, metalness: 0.25 });
const jointMat = new THREE.MeshStandardMaterial({ color: 0xf0b429, roughness: 0.4, metalness: 0.4 });
const base = new THREE.Mesh(new THREE.CylinderGeometry(2.2, 2.6, 1.6, 24), baseMat);
base.position.y = 0.8;
rig.add(base);
const column = new THREE.Mesh(new THREE.CylinderGeometry(0.9, 1.1, 5.4, 16), armMat);
column.position.y = 4.3;
rig.add(column);
/* 上臂:几何体平移半个长度,使原点成为转动轴心 */
const upperGeo = new THREE.BoxGeometry(6, 0.9, 0.9);
upperGeo.translate(3, 0, 0);
const upper = new THREE.Mesh(upperGeo, armMat);
upper.name = 'upper';
upper.position.set(0, 7, 0);
rig.add(upper);
const elbow = new THREE.Mesh(new THREE.SphereGeometry(0.62, 18, 12), jointMat);
upper.add(elbow);
const lowerGeo = new THREE.BoxGeometry(5, 0.7, 0.7);
lowerGeo.translate(2.5, 0, 0);
const lower = new THREE.Mesh(lowerGeo, armMat);
lower.name = 'lower';
lower.position.set(6, 0, 0);
upper.add(lower);
const gripper = new THREE.Mesh(new THREE.ConeGeometry(0.55, 1.3, 12), jointMat);
gripper.position.set(5.4, 0, 0);
gripper.rotation.z = -Math.PI / 2;
lower.add(gripper);
/* ---------- 两条片段:各自只驱动一个关节 ---------- */
const TIMES = [0, 0.6, 1.2, 1.8, 2.4];
const Z = new THREE.Vector3(0, 0, 1);
function quatValues(angles) {
const out = [];
const q = new THREE.Quaternion();
angles.forEach(function (a) {
q.setFromAxisAngle(Z, a);
out.push(q.x, q.y, q.z, q.w);
});
return out;
}
const trackUpper = new THREE.QuaternionKeyframeTrack(
'upper.quaternion',
TIMES,
quatValues([0, 0.62, 0, -0.62, 0])
);
const trackLower = new THREE.QuaternionKeyframeTrack(
'lower.quaternion',
TIMES,
quatValues([0, -0.85, 0.2, 0.75, 0])
);
const clipUpper = new THREE.AnimationClip('upper-swing', 2.4, [trackUpper]);
const clipLower = new THREE.AnimationClip('lower-swing', 2.4, [trackLower]);
const mixer = new THREE.AnimationMixer(rig);
const actionA = mixer.clipAction(clipUpper);
const actionB = mixer.clipAction(clipLower);
actionA.play();
actionB.play();
actionA.setEffectiveWeight(0.6);
actionB.setEffectiveWeight(0.35);
const wa = document.getElementById('wa');
const wb = document.getElementById('wb');
const wav = document.getElementById('wav');
const wbv = document.getElementById('wbv');
const timeEl = document.getElementById('time');
const pauseBtn = document.getElementById('pause');
let paused = false;
wa.addEventListener('input', function () {
const w = Number(wa.value);
actionA.setEffectiveWeight(w);
wav.textContent = w.toFixed(2);
});
wb.addEventListener('input', function () {
const w = Number(wb.value);
actionB.setEffectiveWeight(w);
wbv.textContent = w.toFixed(2);
});
pauseBtn.addEventListener('click', function () {
paused = !paused;
pauseBtn.textContent = paused ? '继续' : '暂停';
});
const clock = new THREE.Clock();
renderer.setAnimationLoop(function () {
const dt = Math.min(clock.getDelta(), 0.05);
if (!paused) mixer.update(dt);
timeEl.textContent = mixer.time.toFixed(2);
controls.update();
renderer.render(scene, camera);
});
window.addEventListener('resize', function () {
const w = stage.clientWidth;
const h = stage.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.