这节我们继续来完善双人斗舞的场景。
首先,选中某个人的时候,给她添加描边,然后调一下相机位置,把她放到中心。
先添加射线点击的交互:
首先给两个 dancer 一个名字,并且遍历设置所有子对象的 target 都是这个 dancer:

dancer.name = 'dancer1';
dancer.traverse((obj) => {
obj.target = dancer;
})
dancer.name = 'dancer2';
dancer.traverse((obj) => {
obj.target = dancer;
})
这样可以点击的时候整体描边。
加一个描边的后期通道:

const v = new THREE.Vector2(window.innerWidth, window.innerWidth);
const outlinePass = new OutlinePass(v, scene, camera);
outlinePass.edgeStrength = 10;
outlinePass.edgeThickness = 10;
outlinePass.pulsePeriod = 1;
composer.addPass(outlinePass);
然后点击的时候,给选中的人添加描边:

这里我用了 Set 去重,因为可能点中一个人身体的多个部位,只保留一个 dancer。
而且可能从某个方向能同时点中两个人,这种只保留一个数组元素。
renderer.domElement.addEventListener('click', (e) => {
const y = -((e.offsetY / height) * 2 - 1);
const x = (e.offsetX / width) * 2 - 1;
const rayCaster = new THREE.Raycaster();
rayCaster.setFromCamera(new THREE.Vector2(x, y), camera);
const intersections = rayCaster.intersectObjects(stage.children);
const set = new Set();
intersections.forEach(item => {
if(item.object.target) {
set.add(item.object.target);
}
});
outlinePass.selectedObjects = set.size ? [...set].slice(0, 1) : [];
});
试下效果:

没啥问题。
然后点击某个人的时候,我们改一下相机位置,把她放到中央:
先用 ObritControls 确定下相机位置:

controls.addEventListener('change', () => {
console.log(camera.position);
});

dancer2 是 42、1008、479

dancer1 是 24、955、-580

然后点击的时候改一下相机位置:

const dancerArr = [...set].slice(0, 1);
outlinePass.selectedObjects = set.size ? dancerArr : [];
if(dancerArr.length) {
const isDancer1 = dancerArr[0].name === 'dancer1';
if(isDancer1) {
camera.position.set(24, 955, -580);
camera.lookAt(0, 0, 0);
} else {
camera.position.set(42, 1008, 479);
camera.lookAt(0, 0, 0);
}
}
根据点击的 dancer 的不同来修改相机位置。

当然,现在太突兀了一点,我们用 tween.js 加一个缓动的相机动画。
安装 tween.js
npm install --save @tweenjs/tween.js
这里用 tween.js 的 Group 来保存所有的 tween,然后渲染循环里统一 update:


const tweenGroup = new Group();
function render(time) {
composer.render();
requestAnimationFrame(render);
tweenGroup.update(time);
}
render();
点击 dancer 的时候,修改相机位置,启动 tween 动画:

const tween = new Tween(camera.position)
.to(isDancer1 ? {x:24, y:955, z:-580}: {x:42, y:1008, z:479}, 2000)
.repeat(0)
.easing(Easing.Quadratic.InOut)
.onUpdate((obj) => {
camera.position.copy(new THREE.Vector3(obj.x, obj.y, obj.z));
camera.lookAt(0, 0, 0);
}).start();
tweenGroup.add(tween);
试一下:


这样切换视角的感觉就有了。
没音乐总像缺了点什么,把这个音频下载下来:

放到 public 目录下:

在代码里引入:

const listener = new THREE.AudioListener();
const audio = new THREE.Audio( listener );
const loader = new THREE.AudioLoader();
loader.load('./superman.mp3', function ( buffer ) {
audio.setBuffer( buffer );
});
document.body.addEventListener('click', () => {
if(!audio.isPlaying) {
audio.setLoop(true);
audio.setVolume(1);
audio.play();
}
});
这样就有音乐了:
然后,我们给她俩加上对话,用 CSS2DRenderer 的标注功能:
改下 index.html

<style>
.dialog {
line-height: 40px;
text-align: center;
font-size: 20px;
padding: 10px;
border: 1px solid #000;
background: #fff;
border-radius: 4px;
}
</style>
<div id="dialog" style="display:none;" class="dialog">
I'm superman.
</div>
<div id="dialog2" style="display:none;" class="dialog">
You are loser.
</div>
刚开始把标签隐藏。
这里用 style 来设置 display:none,因为渲染标签的时候会设置 display:block 刚好把这个覆盖掉,就显示出来了。
然后在 dancer 上加两个标签:

const ele = document.getElementById('dialog');
const obj = new CSS2DObject(ele);
dancer.add(obj);
obj.position.set(1, 0, 0);
ele.style.display = 'block';
setTimeout(() => {
ele.textContent = '谁叫你还搞不清楚我跟你的差别';
}, 5000);
const ele = document.getElementById('dialog2');
const obj = new CSS2DObject(ele);
ele.style.display = 'block';
dancer.add(obj);
obj.position.set(1, 0, 0);
setTimeout(() => {
ele.textContent = '超人没空给你给你安慰';
}, 8000);
因为之前设置过 scale 30 倍,所以这里 position 设置 1 就会放大成 30,你也可以用 GUI 可视化调试。
在 main.js 里引入 CSS2DRenderer 来渲染:

const css2Renderer = new CSS2DRenderer();
css2Renderer.setSize(width, height);
function render(time) {
css2Renderer.render(scene, camera);
composer.render();
requestAnimationFrame(render);
tweenGroup.getAll().map(item => item.update(time))
}
render();
// document.body.append(renderer.domElement);
const div = document.createElement('div');
div.style.position = 'relative';
div.appendChild(css2Renderer.domElement);
css2Renderer.domElement.style.position = 'absolute';
css2Renderer.domElement.style.left = '0px';
css2Renderer.domElement.style.top = '0px';
css2Renderer.domElement.style.pointerEvents = 'none';
div.appendChild(renderer.domElement);
document.body.appendChild(div);
这里前面写过,就是创建一个 div,下面包含两个 renderer 的 domElement,绝对定位上面那个。
创建 CSS2DRenderer,每次渲染循环 render 一下。

这样,我们就用标注来实现了对话功能。
案例代码上传了小册仓库。
总结
这节我们完善了双人斗舞的场景。
加了描边的后期 Pass,点击的时候给舞者添加描边效果。
并且相机用 tween.js 来做相机缓动动画,把舞者放到视野中央。
用 Audio 的 api 实现了音频的播放。
然后用 CSS2DRenderer 的标注实现了对话功能。
这个实战我们综合用到了聚光灯、阴影、gltf 模型加载、后期处理、射线和点击、css2d 标注、tweenjs 相机缓动动画、音频播放等基础知识,是一个比较综合的实战。