前提
此章节会有一些空间向量的一些计算,我会在其他文章整理出来(还没整理完)
实践(附完整代码)
本章节结合官方示例,在其基础之上实现漫游相关功能
webgl_animation_skinning_additive_blending
运动
- 设定键操作
// 记录四个按键的状态
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轴方向位移,再配合动作,是不是就有那个味道了
- 逐渐加速->匀速
我们上面速度是给定了一个固定值,正常应该是有个加速度的过程
// 速度
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();
}
我们可以看到此流程上会有一个初步加速,然后匀速的过程
- 逐渐减速->停止
对于速度来说就是要逐渐减小
// 速度
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();
}
可以看出我们的效果已经有了,看着很舒服
我们再顺手补充上后退键 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();
}
视角跟随人物运动
原理:相机加入目标模型中,人物模型整体运动会自动带着相机运动
具体位置,如图所示(画的比较抽象):
额外更新代码部分(去除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);
鼠标左右拖动改变玩家视角
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
});
我们看一下效果
获取人物模型(相机)正前方向
我们之前的方向都是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();
}
鼠标上下移动改变视角
注意视角要操作相机,不要操作人物模型,给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
});
人物模型左右运动
我们需要根据人物模型的高度方向和正前方两个向量进行叉乘,得出一个垂直于二者的向量,就是左右的方向
叉乘得出的向量具体方向,参考右手螺旋定则
// 达到峰值不再加速
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)); // 速度增量。随着时间增加,速度会越来越快
}
}
鼠标指针锁定模式
可以了解一下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
}
}
});
切换第一人称和第三人称
示意图
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
}
})
完整代码
<!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>