学完骨骼动画、css2d 标注后,我们来做一个双人斗舞的实战。
舞台上有两个人,面对面站着。
点击某个人的时候,会把相机切到那个人的角度,用 tweenjs 做相机动画。
在右边展示这个人的介绍信息,用 css2d 标签。
然后点击开始按钮,就开始跳舞,播放骨骼动画。
我们还可以用聚光灯、阴影增加舞台的感觉。
大概就是这样的场景,我们来实现一下:
npx create-vite two-dancer

进入项目,安装依赖:
npm install
npm install --save three
npm install --save-dev @types/three
改下 src/main.js
import './style.css';
import * as THREE from 'three';
import {
OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import stage from './stage.js';
const scene = new THREE.Scene();
scene.add(stage);
const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(500, 400, 300);
scene.add(directionLight);
const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);
const width = window.innerWidth;
const height = window.innerHeight;
const helper = new THREE.AxesHelper(500);
scene.add(helper);
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 10000);
camera.position.set(500, 600, 800);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
document.body.append(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
window.onresize = function () {
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setSize(width,height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
};
创建 Scene、Light、Camera、Renderer。
处理下 window.resize,resize 的时候重新设置宽高比。
改下 style.css
body {
margin: 0;
}
然后创建 stage.js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
const stage = new THREE.Group();
loader.load("./stage.glb", function (gltf) {
console.log(gltf);
stage.add(gltf.scene);
});
export default stage;
我们这次直接加载一个房间的模型做舞台。
你可以从这里下载:

放在 public 目录下:

跑一下:
npm run dev


有点小,放大一下:
gltf.scene.scale.set(50,50,50);


然后把舞者的模型加载进来:

下载下来放到 public 目录下:

在代码里加载:

loader.load("./Michelle.glb", function (gltf) {
stage.add(gltf.scene);
gltf.scene.scale.set(300, 300, 300);
gltf.scene.position.z = 500;
gltf.scene.rotateY(Math.PI);
});

播放下跳舞的骨骼动画:

const mixer = new THREE.AnimationMixer(gltf.scene);
const clipAction = mixer.clipAction(gltf.animations[0]);
clipAction.play();
const clock = new THREE.Clock();
function render() {
const delta = clock.getDelta();
mixer.update(delta);
requestAnimationFrame(render);
}
render();

然后我们再加载一个放对面,因为骨骼动画适合模型绑定的,不能直接 clone,我们重新加载一次:
封装个方法:

加载两次模型,传入 z、旋转角度:
loadDancer((dancer)=> {}, 200, Math.PI);
loadDancer((dancer) => {}, -200, 0);
function loadDancer(callback, z, angle) {
loader.load("./Michelle.glb", function (gltf) {
callback(gltf.scene);
stage.add(gltf.scene);
gltf.scene.scale.set(300, 300, 300);
gltf.scene.position.z = z;
gltf.scene.rotateY(angle);
const mixer = new THREE.AnimationMixer(gltf.scene);
const clipAction = mixer.clipAction(gltf.animations[0]);
clipAction.play();
const clock = new THREE.Clock();
function render() {
const delta = clock.getDelta();
mixer.update(delta);
requestAnimationFrame(render);
}
render();
});
}

给第二个 dancer 改个颜色:

dancer.traverse(obj => {
if(obj.isMesh) {
obj.material = obj.material.clone();
obj.material.color.set('orange');
}
})
这里遍历找到 mesh,先复制一份材质,不然共用材质会相互影响,然后改下颜色。

然后我们加个后期效果:


import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { GlitchPass } from 'three/addons/postprocessing/GlitchPass.js';
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
const glitchPass = new GlitchPass();
composer.addPass(glitchPass);
function render() {
composer.render();
requestAnimationFrame(render);
}
这里用 RenderPass 和 GlitchPass 两个 Pass。GlitchPass 是闪屏的效果。
看一下:

可以看到,每隔一段时间有个闪屏效果。
但它会导致颜色变暗,后期通道那节讲过可以加伽马校正来修复颜色:

这里我们就不修复了,暗点效果更好。
我们可以把灯光调亮一下:


好多了。
去掉 AxesHelper,把平行光稍微调暗点,我们再加一个聚光灯:

const spotLight = new THREE.SpotLight('white', 5000000);
spotLight.angle = Math.PI / 6;
spotLight.position.set(0, 800, 0);
spotLight.lookAt(0, 0, 0);
scene.add(spotLight);

然后加一下聚光灯的阴影:
首先开启舞者的投射阴影属性:

gltf.scene.traverse(obj => {
obj.castShadow = true;
});
然后开启舞台的接收阴影属性:

gltf.scene.traverse(obj => {
obj.receiveShadow = true;
});
因为它们都有很多子对象,所以要遍历设置。

spotLight.castShadow = true;
const cameraHelper = new THREE.CameraHelper(spotLight.shadow.camera);
scene.add(cameraHelper);
开启聚光灯的投射阴影属性,然后用 CameraHelper 可视化一下阴影相机。

最后开启下 renderer 的阴影设置。
renderer.shadowMap.enabled = true;
看下效果:

可以看到,没有投射阴影,因为阴影相机的 far 不够大。
改一下:

spotLight.shadow.camera.far = 10000;

这样就好了:

案例代码上传了小册仓库。
总结
这节我们实现了双人斗舞的一个场景。
主要用到了 gltf 模型的加载,骨骼动画的播放,聚光灯、阴影,后期处理这些基础知识。
下节我们继续给这个场景加一些交互。