开启Three.js之旅(十)带你实现一个完整的人物漫游

559 阅读9分钟

前提

c10d5f2da9584bf09ada65dc3a31264b.gif

此章节会有一些空间向量的一些计算,我会在其他文章整理出来(还没整理完)

实践(附完整代码)

本章节结合官方示例,在其基础之上实现漫游相关功能

webgl_animation_skinning_additive_blending

运动

  1. 设定键操作
// 记录四个按键的状态
const keyStates = {
        W: false,
        S: false,
        A: false,
        D: false
}
// 监听键盘事件-按下
document.addEventListener('keydown', function (event) {
        const key = event.key.toUpperCase();
        if (keyStates.hasOwnProperty(key)) {
                keyStates[key] = true;
        }
});
// 监听键盘事件-松开
document.addEventListener('keyup', function (event) {
        const key = event.key.toUpperCase();
        if (keyStates.hasOwnProperty(key)) {
                keyStates[key] = false;
        }
});
...
...
...
// 速度
const v = new THREE.Vector3(0, 0, 3);
// animate
function animate() {
        for ( let i = 0; i !== numAnimations; ++ i ) {
                const action = allActions[ i ];
                const clip = action.getClip();
                const settings = baseActions[ clip.name ] || additiveActions[ clip.name ];
                settings.weight = action.getEffectiveWeight();
        }
        const mixerUpdateDelta = clock.getDelta();
        if(keyStates.W){
                const deltaPos = v.clone().multiplyScalar(mixerUpdateDelta); // 速度乘以时间间隔
                model.position.add(deltaPos); // 原位置加上增量位置
        }
        mixer.update( mixerUpdateDelta );
        renderer.render( scene, camera );
        stats.update();
}

我们可以发现我们可以沿着z轴方向位移,再配合动作,是不是就有那个味道了

1.gif

  1. 逐渐加速->匀速

我们上面速度是给定了一个固定值,正常应该是有个加速度的过程

// 速度
const v = new THREE.Vector3(0, 0, 0);
// 加速度
const a = 12
// animate
function animate() {
        for ( let i = 0; i !== numAnimations; ++ i ) {
                const action = allActions[ i ];
                const clip = action.getClip();
                const settings = baseActions[ clip.name ] || additiveActions[ clip.name ];
                settings.weight = action.getEffectiveWeight();
        }
        const mixerUpdateDelta = clock.getDelta();
        if(keyStates.W){
                const front = new THREE.Vector3(0, 0, 1);
                // 达到峰值不再加速
                if(v.length() < 5){
                        v.add(front.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
                }
        }
        const deltaPos = v.clone().multiplyScalar(mixerUpdateDelta); // 速度乘以时间间隔
        model.position.add(deltaPos); // 原位置加上增量位置
        mixer.update( mixerUpdateDelta );
        renderer.render( scene, camera );
        stats.update();
}

我们可以看到此流程上会有一个初步加速,然后匀速的过程 3.gif

  1. 逐渐减速->停止

对于速度来说就是要逐渐减小

// 速度
const v = new THREE.Vector3(0, 0, 0);
// 加速度
const a = 12
// animate
function animate() {
        for ( let i = 0; i !== numAnimations; ++ i ) {
                const action = allActions[ i ];
                const clip = action.getClip();
                const settings = baseActions[ clip.name ] || additiveActions[ clip.name ];
                settings.weight = action.getEffectiveWeight();
        }
        const mixerUpdateDelta = clock.getDelta();
        if(keyStates.W){
                const front = new THREE.Vector3(0, 0, 1);
                // 达到峰值不再加速
                if(v.length() < 5){
                        v.add(front.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
                }
        }
        // v = v*(1 - 0.02) = v*(1 + damping) = v + v*damping = v.addScaledVector(v, damping)
        v.addScaledVector(v, -0.02) // 不用写在if里面,阻力一直都有,当然也可以写里面
        const deltaPos = v.clone().multiplyScalar(mixerUpdateDelta); // 速度乘以时间间隔
        model.position.add(deltaPos); // 原位置加上增量位置
        mixer.update( mixerUpdateDelta );
        renderer.render( scene, camera );
        stats.update();
}

可以看出我们的效果已经有了,看着很舒服

Snipaste_2025-02-05_14-10-29.jpg 5.gif

我们再顺手补充上后退键 S

// animate
function animate() {
        for (let i = 0; i !== numAnimations; ++i) {
                const action = allActions[i];
                const clip = action.getClip();
                const settings = baseActions[clip.name] || additiveActions[clip.name];
                settings.weight = action.getEffectiveWeight();
        }
        const mixerUpdateDelta = clock.getDelta();
        // 达到峰值不再加速
        if (v.length() < 5) {
                if (keyStates.W) {
                        const front = new THREE.Vector3(0, 0, 1);
                        v.add(front.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
                } else if (keyStates.S) {
                        const back = new THREE.Vector3(0, 0, -1);
                        v.add(back.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
                }
        }
        v.addScaledVector(v, -0.02) // 不用写在if里面,阻力一直都有,当然也可以写里面
        const deltaPos = v.clone().multiplyScalar(mixerUpdateDelta); // 速度乘以时间间隔
        model.position.add(deltaPos); // 原位置加上增量位置
        mixer.update(mixerUpdateDelta);
        renderer.render(scene, camera);
        stats.update();
}

6.gif

视角跟随人物运动

原理:相机加入目标模型中,人物模型整体运动会自动带着相机运动

具体位置,如图所示(画的比较抽象):

Snipaste_2025-02-05_15-14-01.jpg

额外更新代码部分(去除controls、更改一个明显的地面)

loader.load('models/gltf/Xbot.glb', function (gltf) {
        model = gltf.scene;
        // camera
        camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100);
        camera.position.set( 0, 1.6, -5.5);
        camera.lookAt(0, 1.6, 0);
        model.add(camera);
        scene.add(model);
        model.traverse(function (object) {
                if (object.isMesh) object.castShadow = true;
        });
}
...
// ground
const grid = new THREE.GridHelper(100, 100, 0x000000, 0x000000);
grid.material.opacity = 0.2;
grid.material.transparent = true;
scene.add(grid);

7.gif

鼠标左右拖动改变玩家视角

camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100);
model.add(camera);
camera.position.set( 0, 1.6, -5.5);
camera.lookAt(0, 1.6, 0);
scene.add(model);
let leftFlag = false;
document.addEventListener('mousedown', function (event) {
        leftFlag = true
});
document.addEventListener('mousemove', function (event) {
        if (leftFlag) {
                model.rotation.y -= event.movementX / 600
        }
});
document.addEventListener('mouseup', function (event) {
        leftFlag = false
});

我们看一下效果

9.gif

获取人物模型(相机)正前方向

我们之前的方向都是Z轴,我们需要对此进行优化

模型自身坐标系没有任何旋转的情况下,模型坐标系与世界坐标系一致。我们可以利用这一点,人物模型正前方最好与Z轴一致,直接用getWorldDirection()获取在世界坐标中的方向

function animate() {
        for (let i = 0; i !== numAnimations; ++i) {
                const action = allActions[i];
                const clip = action.getClip();
                const settings = baseActions[clip.name] || additiveActions[clip.name];
                settings.weight = action.getEffectiveWeight();
        }
        const mixerUpdateDelta = clock.getDelta();
        // 达到峰值不再加速
        if (v.length() < 5) {
                if (keyStates.W) {
                        const front = new THREE.Vector3(0, 0, 0);
                        model.getWorldDirection(front); // 结果保存到front中
                        v.add(front.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
                } else if (keyStates.S) {
                        const back = new THREE.Vector3(0, 0, 0);
                        model.getWorldDirection(back); 
                        v.add(back.multiplyScalar(- a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
                }
        }
        v.addScaledVector(v, -0.02) // 不用写在if里面,阻力一直都有,当然也可以写里面
        const deltaPos = v.clone().multiplyScalar(mixerUpdateDelta); // 速度乘以时间间隔
        model.position.add(deltaPos); // 原位置加上增量位置
        mixer.update(mixerUpdateDelta);
        renderer.render(scene, camera);
        stats.update();
}

10.gif

鼠标上下移动改变视角

注意视角要操作相机,不要操作人物模型,给camera外层加一个组,方便操作相机

model = gltf.scene;
// camera
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100);
const cameraGroup = new THREE.Group();
cameraGroup.add(camera);
model.add(cameraGroup);
camera.position.set( 0, 1.6, -5.5);
camera.lookAt(0, 1.6, 0);
scene.add(model);
let leftFlag = false;
document.addEventListener('mousedown', function (event) {
        leftFlag = true
});
const angleMin = THREE.MathUtils.degToRad(-15)
const angleMax = THREE.MathUtils.degToRad(15)
document.addEventListener('mousemove', function (event) {
        if (leftFlag) {
                model.rotation.y -= event.movementX / 600
                // 限制上下旋转角度
                let angle = cameraGroup.rotation.x + event.movementY / 600  // 个人习惯,我习惯上下反转
                if(angle > angleMin && angle < angleMax){
                        cameraGroup.rotation.x = angle
                }
        }
});
document.addEventListener('mouseup', function (event) {
        leftFlag = false
});

13.gif

人物模型左右运动

我们需要根据人物模型的高度方向和正前方两个向量进行叉乘,得出一个垂直于二者的向量,就是左右的方向

叉乘得出的向量具体方向,参考右手螺旋定则

// 达到峰值不再加速
if (v.length() < 5) {
        if (keyStates.W) {
                const front = new THREE.Vector3(0, 0, 0);
                model.getWorldDirection(front); // 结果保存到front中
                v.add(front.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
        } else if (keyStates.S) {
                const back = new THREE.Vector3(0, 0, 0);
                model.getWorldDirection(back); 
                v.add(back.multiplyScalar(- a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
        }else if (keyStates.A) {
                const front = new THREE.Vector3(0, 0, 0);
                model.getWorldDirection(front); // 结果保存到front中
                const up = new THREE.Vector3(0, 1, 0); // 这个就不用获取对应的世界坐标了,因为就没有变过,都一样
                const left = up.cross(front); // 叉乘,得到的就是left
                v.add(left.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
        }else if (keyStates.D) {
                const front = new THREE.Vector3(0, 0, 0);
                model.getWorldDirection(front); // 结果保存到front中
                const up = new THREE.Vector3(0, 1, 0); // 这个就不用获取对应的世界坐标了,因为就没有变过,都一样
                const right = up.cross(front).multiplyScalar(-1); // 叉乘,得到的就是right
                v.add(right.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快	
        }
}

14.gif

鼠标指针锁定模式

可以了解一下requestPointerLock这个API

// camera
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100);
const cameraGroup = new THREE.Group();
cameraGroup.add(camera);
model.add(cameraGroup);
camera.position.set( 0, 1.6, -5.5);
camera.lookAt(0, 1.6, 0);
scene.add(model);
document.addEventListener('mousedown', function (event) {
        document.body.requestPointerLock() // 进入鼠标锁定模式
});
const angleMin = THREE.MathUtils.degToRad(-15)
const angleMax = THREE.MathUtils.degToRad(15)
document.addEventListener('mousemove', function (event) {
        if (document.pointerLockElement === document.body) { // 判断是否在鼠标锁定模式下,锁定模式下doucment.pointerLockElement指向document.body
                model.rotation.y -= event.movementX / 600
                // 限制上下旋转角度
                let angle = cameraGroup.rotation.x + event.movementY / 600
                if(angle > angleMin && angle < angleMax){
                        cameraGroup.rotation.x = angle
                }
        }
});

切换第一人称和第三人称

示意图

Snipaste_2025-02-05_17-20-20.jpg

let viewFlag = true
document.addEventListener('keydown', function (event) {
        if (event.key === 'v') {
                if(viewFlag){
                        camera.position.z = 0.8 // 第一人称视角
                }else{
                        camera.position.z = -2.5 // 第三人称视角
                }
                viewFlag = !viewFlag
        }	
})

16.gif

完整代码

<!DOCTYPE html>
<html lang="en">

<head>
	<title>three.js webgl - additive animation - skinning</title>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
	<link type="text/css" rel="stylesheet" href="main.css">
	<style>
		a {
			color: blue;
		}

		.control-inactive button {
			color: #888;
		}
	</style>
</head>

<body>
	<div id="container"></div>
	<script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

	<script type="module">

		import * as THREE from 'three';

		import Stats from 'three/addons/libs/stats.module.js';
		import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
		import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
		import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

		let scene, renderer, camera, stats;
		let model, skeleton, mixer, clock;

		const crossFadeControls = [];

		let currentBaseAction = 'idle';
		const allActions = [];
		const baseActions = {
			idle: { weight: 1 },
			walk: { weight: 0 },
			run: { weight: 0 }
		};
		const additiveActions = {
			sneak_pose: { weight: 0 },
			sad_pose: { weight: 0 },
			agree: { weight: 0 },
			headShake: { weight: 0 }
		};
		let panelSettings, numAnimations;

		// 记录四个按键的状态
		const keyStates = {
			W: false,
			S: false,
			A: false,
			D: false
		}
		// 监听键盘事件-按下
		document.addEventListener('keydown', function (event) {
			const key = event.key.toUpperCase();
			if (keyStates.hasOwnProperty(key)) {
				keyStates[key] = true;
			}
		});
		// 监听键盘事件-松开
		document.addEventListener('keyup', function (event) {
			const key = event.key.toUpperCase();
			if (keyStates.hasOwnProperty(key)) {
				keyStates[key] = false;
			}
		});

		init();

		function init() {

			const container = document.getElementById('container');
			clock = new THREE.Clock();

			scene = new THREE.Scene();
			scene.background = new THREE.Color(0xa0a0a0);
			scene.fog = new THREE.Fog(0xa0a0a0, 10, 50);

			const hemiLight = new THREE.HemisphereLight(0xffffff, 0x8d8d8d, 3);
			hemiLight.position.set(0, 20, 0);
			scene.add(hemiLight);

			const dirLight = new THREE.DirectionalLight(0xffffff, 3);
			dirLight.position.set(3, 10, 10);
			dirLight.castShadow = true;
			dirLight.shadow.camera.top = 2;
			dirLight.shadow.camera.bottom = - 2;
			dirLight.shadow.camera.left = - 2;
			dirLight.shadow.camera.right = 2;
			dirLight.shadow.camera.near = 0.1;
			dirLight.shadow.camera.far = 40;
			scene.add(dirLight);

			// ground
			const grid = new THREE.GridHelper(100, 100, 0x000000, 0x000000);
			grid.material.opacity = 0.2;
			grid.material.transparent = true;
			scene.add(grid);

			const loader = new GLTFLoader();
			loader.load('models/gltf/Xbot.glb', function (gltf) {
				model = gltf.scene;
				// camera
				camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100);
				const cameraGroup = new THREE.Group();
				cameraGroup.add(camera);
				model.add(cameraGroup);
				camera.position.set( 0, 1.6, -2.5);
				camera.lookAt(0, 1.6, 0);
				scene.add(model);
				document.addEventListener('mousedown', function (event) {
					document.body.requestPointerLock() // 进入鼠标锁定模式
				});
				const angleMin = THREE.MathUtils.degToRad(-15)
				const angleMax = THREE.MathUtils.degToRad(15)
				document.addEventListener('mousemove', function (event) {
					if (document.pointerLockElement === document.body) { // 判断是否在鼠标锁定模式下,锁定模式下doucment.pointerLockElement指向document.body
						model.rotation.y -= event.movementX / 600
						// 限制上下旋转角度
						let angle = cameraGroup.rotation.x + event.movementY / 600
						if(angle > angleMin && angle < angleMax){
							cameraGroup.rotation.x = angle
						}
					}
				});

				let viewFlag = true
				document.addEventListener('keydown', function (event) {
					if (event.key === 'v') {
						if(viewFlag){
							camera.position.z = 0.8 // 第一人称视角
						}else{
							camera.position.z = -2.5 // 第三人称视角
						}
						viewFlag = !viewFlag
					}	
				})

				model.traverse(function (object) {

					if (object.isMesh) object.castShadow = true;

				});

				skeleton = new THREE.SkeletonHelper(model);
				skeleton.visible = false;
				scene.add(skeleton);

				const animations = gltf.animations;
				mixer = new THREE.AnimationMixer(model);

				numAnimations = animations.length;

				for (let i = 0; i !== numAnimations; ++i) {

					let clip = animations[i];
					const name = clip.name;

					if (baseActions[name]) {

						const action = mixer.clipAction(clip);
						activateAction(action);
						baseActions[name].action = action;
						allActions.push(action);

					} else if (additiveActions[name]) {

						// Make the clip additive and remove the reference frame

						THREE.AnimationUtils.makeClipAdditive(clip);

						if (clip.name.endsWith('_pose')) {

							clip = THREE.AnimationUtils.subclip(clip, clip.name, 2, 3, 30);

						}

						const action = mixer.clipAction(clip);
						activateAction(action);
						additiveActions[name].action = action;
						allActions.push(action);

					}

				}

				createPanel();

				renderer.setAnimationLoop(animate);

			});

			renderer = new THREE.WebGLRenderer({ antialias: true });
			renderer.setPixelRatio(window.devicePixelRatio);
			renderer.setSize(window.innerWidth, window.innerHeight);
			renderer.shadowMap.enabled = true;
			container.appendChild(renderer.domElement);

			stats = new Stats();
			container.appendChild(stats.dom);

			window.addEventListener('resize', onWindowResize);

		}

		function createPanel() {

			const panel = new GUI({ width: 310 });

			const folder1 = panel.addFolder('Base Actions');
			const folder2 = panel.addFolder('Additive Action Weights');
			const folder3 = panel.addFolder('General Speed');

			panelSettings = {
				'modify time scale': 1.0
			};

			const baseNames = ['None', ...Object.keys(baseActions)];

			for (let i = 0, l = baseNames.length; i !== l; ++i) {

				const name = baseNames[i];
				const settings = baseActions[name];
				panelSettings[name] = function () {

					const currentSettings = baseActions[currentBaseAction];
					const currentAction = currentSettings ? currentSettings.action : null;
					const action = settings ? settings.action : null;

					if (currentAction !== action) {

						prepareCrossFade(currentAction, action, 0.35);

					}

				};

				crossFadeControls.push(folder1.add(panelSettings, name));

			}

			for (const name of Object.keys(additiveActions)) {

				const settings = additiveActions[name];

				panelSettings[name] = settings.weight;
				folder2.add(panelSettings, name, 0.0, 1.0, 0.01).listen().onChange(function (weight) {

					setWeight(settings.action, weight);
					settings.weight = weight;

				});

			}

			folder3.add(panelSettings, 'modify time scale', 0.0, 1.5, 0.01).onChange(modifyTimeScale);

			folder1.open();
			folder2.open();
			folder3.open();

			crossFadeControls.forEach(function (control) {

				control.setInactive = function () {

					control.domElement.classList.add('control-inactive');

				};

				control.setActive = function () {

					control.domElement.classList.remove('control-inactive');

				};

				const settings = baseActions[control.property];

				if (!settings || !settings.weight) {

					control.setInactive();

				}

			});

		}

		function activateAction(action) {

			const clip = action.getClip();
			const settings = baseActions[clip.name] || additiveActions[clip.name];
			setWeight(action, settings.weight);
			action.play();

		}

		function modifyTimeScale(speed) {

			mixer.timeScale = speed;

		}

		function prepareCrossFade(startAction, endAction, duration) {

			// If the current action is 'idle', execute the crossfade immediately;
			// else wait until the current action has finished its current loop

			if (currentBaseAction === 'idle' || !startAction || !endAction) {

				executeCrossFade(startAction, endAction, duration);

			} else {

				synchronizeCrossFade(startAction, endAction, duration);

			}

			// Update control colors

			if (endAction) {

				const clip = endAction.getClip();
				currentBaseAction = clip.name;

			} else {

				currentBaseAction = 'None';

			}

			crossFadeControls.forEach(function (control) {

				const name = control.property;

				if (name === currentBaseAction) {

					control.setActive();

				} else {

					control.setInactive();

				}

			});

		}

		function synchronizeCrossFade(startAction, endAction, duration) {

			mixer.addEventListener('loop', onLoopFinished);

			function onLoopFinished(event) {

				if (event.action === startAction) {

					mixer.removeEventListener('loop', onLoopFinished);

					executeCrossFade(startAction, endAction, duration);

				}

			}

		}

		function executeCrossFade(startAction, endAction, duration) {

			// Not only the start action, but also the end action must get a weight of 1 before fading
			// (concerning the start action this is already guaranteed in this place)

			if (endAction) {

				setWeight(endAction, 1);
				endAction.time = 0;

				if (startAction) {

					// Crossfade with warping

					startAction.crossFadeTo(endAction, duration, true);

				} else {

					// Fade in

					endAction.fadeIn(duration);

				}

			} else {

				// Fade out

				startAction.fadeOut(duration);

			}

		}

		function setWeight(action, weight) {

			action.enabled = true;
			action.setEffectiveTimeScale(1);
			action.setEffectiveWeight(weight);

		}

		function onWindowResize() {
			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix();
			renderer.setSize(window.innerWidth, window.innerHeight);
		}

		// 速度
		const v = new THREE.Vector3(0, 0, 0);
		// 加速度
		const a = 12
		// animate
		function animate() {
			for (let i = 0; i !== numAnimations; ++i) {
				const action = allActions[i];
				const clip = action.getClip();
				const settings = baseActions[clip.name] || additiveActions[clip.name];
				settings.weight = action.getEffectiveWeight();
			}
			const mixerUpdateDelta = clock.getDelta();
			// 达到峰值不再加速
			if (v.length() < 5) {
				if (keyStates.W) {
					const front = new THREE.Vector3(0, 0, 0);
					model.getWorldDirection(front); // 结果保存到front中
					v.add(front.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
				} else if (keyStates.S) {
					const back = new THREE.Vector3(0, 0, 0);
					model.getWorldDirection(back); 
					v.add(back.multiplyScalar(- a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
				}else if (keyStates.A) {
					const front = new THREE.Vector3(0, 0, 0);
					model.getWorldDirection(front); // 结果保存到front中
					const up = new THREE.Vector3(0, 1, 0); // 这个就不用获取对应的世界坐标了,因为就没有变过,都一样
					const left = up.cross(front); // 叉乘,得到的就是left
					v.add(left.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快
				}else if (keyStates.D) {
					const front = new THREE.Vector3(0, 0, 0);
					model.getWorldDirection(front); // 结果保存到front中
					const up = new THREE.Vector3(0, 1, 0); // 这个就不用获取对应的世界坐标了,因为就没有变过,都一样
					const right = up.cross(front).multiplyScalar(-1); // 叉乘,得到的就是right
					v.add(right.multiplyScalar(a * mixerUpdateDelta)); // 速度增量。随着时间增加,速度会越来越快	
				}
			}
			v.addScaledVector(v, -0.02) // 不用写在if里面,阻力一直都有,当然也可以写里面
			const deltaPos = v.clone().multiplyScalar(mixerUpdateDelta); // 速度乘以时间间隔
			model.position.add(deltaPos); // 原位置加上增量位置
			mixer.update(mixerUpdateDelta);
			renderer.render(scene, camera);
			stats.update();
		}
	</script>

</body>

</html>