Skip to content

59. 实战:双人斗舞(二)

Published:

这节我们继续来完善双人斗舞的场景。

首先,选中某个人的时候,给她添加描边,然后调一下相机位置,把她放到中心。

先添加射线点击的交互:

首先给两个 dancer 一个名字,并且遍历设置所有子对象的 target 都是这个 dancer:

image.png

dancer.name = 'dancer1';
dancer.traverse((obj) => {
    obj.target = dancer;
})
dancer.name = 'dancer2';
dancer.traverse((obj) => {
    obj.target = dancer;
})

这样可以点击的时候整体描边。

加一个描边的后期通道:

image.png

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);

然后点击的时候,给选中的人添加描边:

image.png

这里我用了 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) : [];
});

试下效果:

2025-04-08 12.54.00.gif

没啥问题。

然后点击某个人的时候,我们改一下相机位置,把她放到中央:

先用 ObritControls 确定下相机位置:

image.png

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

2025-04-08 13.02.52.gif

dancer2 是 42、1008、479

image.png

dancer1 是 24、955、-580

image.png

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

image.png

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 的不同来修改相机位置。

2025-04-08 13.12.40.gif

当然,现在太突兀了一点,我们用 tween.js 加一个缓动的相机动画。

安装 tween.js

npm install --save @tweenjs/tween.js

这里用 tween.js 的 Group 来保存所有的 tween,然后渲染循环里统一 update:

image.png

image.png

const tweenGroup = new Group();

function render(time) {
    composer.render();
    requestAnimationFrame(render);
    
    tweenGroup.update(time);
}

render();

点击 dancer 的时候,修改相机位置,启动 tween 动画:

image.png

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);

试一下:

2025-04-08 14.58.58.gif

2025-04-08 15.00.28.gif

这样切换视角的感觉就有了。

没音乐总像缺了点什么,把这个音频下载下来:

image.png

放到 public 目录下:

image.png

在代码里引入:

image.png

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();
  }
});

这样就有音乐了:

jaudio

然后,我们给她俩加上对话,用 CSS2DRenderer 的标注功能:

改下 index.html

image.png

<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 上加两个标签:

image.png

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 来渲染:

image.png

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 一下。

2025-04-08 18.40.03.gif

这样,我们就用标注来实现了对话功能。

案例代码上传了小册仓库

总结

这节我们完善了双人斗舞的场景。

加了描边的后期 Pass,点击的时候给舞者添加描边效果。

并且相机用 tween.js 来做相机缓动动画,把舞者放到视野中央。

用 Audio 的 api 实现了音频的播放。

然后用 CSS2DRenderer 的标注实现了对话功能。

这个实战我们综合用到了聚光灯、阴影、gltf 模型加载、后期处理、射线和点击、css2d 标注、tweenjs 相机缓动动画、音频播放等基础知识,是一个比较综合的实战。

评论