Theatre.js动画制作

在这个教程中,我们将介绍 Theatre.js 的基础知识并探索如何制作令人惊叹的动画序列。 我们将演示如何为 Three.js 立方体制作动画、集成引人注目的视觉效果、修改颜色、试验 HTML 元素以及以特定时间间隔将动画与声音播放同步。

1、Theatre.js安装和设置

首先,我们需要一个带有 Three.js 的入门模板和一个基本场景。 Theater.js 有两个基本包:

  • @theatre/studio 是我们用来创建动画的编辑器 GUI
  • @theatre/core 播放我们创建的动画。

我们可以像这样添加 Theater.js 包:

# with npm
npm install --save @theatre/core @theatre/studio
# with yarn
yarn add @theatre/core @theatre/studio

或者下载这个入门模板,其中包含所有必需的依赖项,然后运行以下命令:

# Install the dependencies 
yarn install

# Start the server
yarn run dev.

2、创建立方体和地板

入门模板为我们提供了一个简单的立方体、地板、一些照明和轨道控件。

// Cube
  const geometry = new THREE.BoxGeometry(10, 10, 10);
  const material = new THREE.MeshPhongMaterial({ color: 0x049ef4 });
  const box = new THREE.Mesh(geometry, material);
  box.castShadow = true;
  box.receiveShadow = true;
  scene.add(box);

// Floor
  const floorGeometry = new THREE.CylinderGeometry(30, 30, 300, 30);
  const floorMaterial = new THREE.MeshPhongMaterial({ color: 0xf0f0f0 });
  const floor = new THREE.Mesh(floorGeometry, floorMaterial);
  floor.position.set(0, -150, 0);
  floor.receiveShadow = true;
  scene.add(floor);

// Lights
  const ambLight = new THREE.AmbientLight(0xfefefe, 0.2);
  const dirLight = new THREE.DirectionalLight(0xfefefe, 1);
  dirLight.castShadow = true;
  dirLight.shadow.mapSize.width = 1024;
  dirLight.shadow.mapSize.height = 1024;
  dirLight.shadow.camera.far = 100;
  dirLight.shadow.camera.near = 1;
  dirLight.shadow.camera.top = 40;
  dirLight.shadow.camera.right = 40;
  dirLight.shadow.camera.bottom = -40;
  dirLight.shadow.camera.left = -40;
  dirLight.position.set(20, 30, 20);
  scene.add(ambLight, dirLight);

// OrbitControls
  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableZoom = true;
  controls.enableDamping = true;
  controls.autoRotate = false;
  controls.dampingFactor = 0.1;
  controls.minDistance = 2.4;
  controls.target.set(0, 20, 0);

3、导入 Theater 并创建项目

我们需要从 @theatre/core 导入 { getProject, types }。 完成后,我们还需要从 @theatre/studio 导入并初始化 studio

Theatre.js 中的项目就像一个保存的文件。 项目存储在 localStorage 中,因此如果关闭并重新打开浏览器,你不会丢失进度。 创建一个新的剧院项目并为其命名。
然后我们创建一个新工作表。 工作表包含所有可以设置动画的对象。

import { getProject, types } from '@theatre/core';
import studio from '@theatre/studio';
studio.initialize();

// Create a project for the animation
const project = getProject('TheatreTutorial_1');

// Create a sheet
const sheet = project.sheet('AnimationScene');

4、物体和属性

每个需要动画的对象都有一个相应的 Theater Sheet 对象。 这些工作表对象包含属性(Props),可以对它们进行动画处理以在场景中创建运动和其他动态效果。

让我们创建一个新的 boxObject 并将其命名为 Box

const boxObj = sheet.object('Box', {});

属性对应于可以动画化的对象的特定特征,可以有不同的类型,可以使用 import {types} from '@theatre/core' 导入。

我们将添加一些道具。 让我们从旋转开始,创建一个复合类型的属性,并添加数值类型的xR、yR和zR,值:0,范围:[-Math.PI,Math.PI]。

同样,让我们添加位置和比例属性。 向其中添加一个 nudgeMultiplier 可以让我们进行更精细的控制。

const boxObj = sheet.object('Box', {
    rotation: types.compound({
      xR: types.number(0, { range: [-Math.PI, Math.PI] }),
      yR: types.number(0, { range: [-Math.PI, Math.PI] }),
      zR: types.number(0, { range: [-Math.PI, Math.PI] }),
    }),
    position: types.compound({
      x: types.number(0, { nudgeMultiplier: 0.1 }),
      y: types.number(0, { nudgeMultiplier: 0.1 }),
      z: types.number(0, { nudgeMultiplier: 0.1 }),
    }),
    scale: types.compound({
      xS: types.number(1, { nudgeMultiplier: 0.1 }),
      yS: types.number(1, { nudgeMultiplier: 0.1 }),
      zS: types.number(1, { nudgeMultiplier: 0.1 }),
    }),
});

现在我们可以看到工作表下有一个新的 Box 对象。

5、为立方体设置动画

是时候为我们的立方体制作动画了。 我们需要一种根据 boxObj 属性的值来旋转立方体网格的方法。 这可以通过使用 onValuesChange() 钩子监听 boxObj 的变化来完成。

boxObj.onValuesChange((values) => {
    const { xR, yR, zR } = values.rotation;
    box.rotation.set(xR, yR, zR);
    const { x, y, z } = values.position;
    box.position.set(x, y, z);
    const { xS, yS, zS } = values.scale;
    box.scale.set(xS, yS, zS);
});

移动滑块现在会实时影响我们的立方体。

6、添加关键帧

让我们添加一些关键帧。 可以右键单击任何属性,然后单击“序列”或“序列全部”。

这将打开带有属性的序列编辑器(Sequence Editor)。 我们可以调整序列时间线的大小、放大或缩小,并使用蓝色指针擦洗序列。

拖动以移动指针,然后单击黄色按钮添加关键帧。

让我们将序列时间线的大小调整为 2 秒多一点。 然后添加关键帧来为立方体的 y 位置设置动画。 同样,让我们对比例进行排序并向其添加关键帧。 你可以使用这些值或自己尝试一下,直到你觉得合适为止。

然后按空格键播放序列:

7、图编辑器

单击序列编辑器中每个属性旁边的按钮将打开图编辑器(Graph Editor)或多轨曲线编辑器(multi-track curve editor)。 当我们想要通过手动编辑一个或多个轨道的速度曲线来精细化动画时,这会派上用场。

单击关键帧之间的链接可显示可使用的默认缓动曲线列表。

8、修改颜色

让我们继续看看如何使用 Theater.js 修改颜色。 让我们创建一个新对象并将其命名为 Colors。 背景颜色是 types.rgba()。 同样,我们还为地板颜色和盒子颜色创建属性:

const colorObj = sheet.object('Colors',{
    backgroundColor: types.rgba(),
    floorColor: types.rgba(),
    boxColor: types.rgba(),
})

onValuesChange() 钩子中,我们可以设置 scene.background 或底层 HTML 元素的背景颜色。 使用 setRGB(),我们设置地板和盒子材质的颜色。 单击并拖动颜色选择器以更改颜色。

colorObj.onValuesChange((values)=>{
    // scene.background = new THREE.Color(values.backgroundColor.toString());
    // @ts-ignore
    document.body.style.backgroundColor = values.backgroundColor;
    floorMaterial.color.setRGB(values.floorColor.r,values.floorColor.g,values.floorColor.b)
    boxMaterial.color.setRGB(values.boxColor.r,values.boxColor.g,values.boxColor.b)
})

如果立方体在拉伸时能发光那就太好了。 让我们创建一个新的Theatre对象: boxEffects

const boxEffectsObj = sheet.object('Effects',{
    boxGlow:types.rgba(),
})
boxEffectsObj.onValuesChange((values)=>{
    boxMaterial.emissive.setRGB(values.boxGlow.r,values.boxGlow.g,values.boxGlow.b);
})

让我们编排其序列,并在前几帧上添加两个 emissive#000000 的关键帧,并为压缩状态选择一种颜色。 然后在最后一帧恢复正常。

9、速度线效果

要添加卡通速度线 vFx,让我们创建三个立方体并将它们缩放为看起来像线,然后将它们以组的形式添加到场景中。

// Swoosh Effect Objects
const swooshMaterial = new THREE.MeshBasicMaterial({color:0x222222,transparent:true,opacity:1});
const swooshEffect = new THREE.Group();

const swooshBig = new THREE.Mesh(geometry, swooshMaterial );
swooshBig.scale.set(0.02,2,0.02)
swooshBig.position.set(1,0,-2)

const swooshSmall1 = new THREE.Mesh(geometry, swooshMaterial );
swooshSmall1.scale.set(0.02,1,0.02)
swooshSmall1.position.set(1,0,3)

const swooshSmall2 = new THREE.Mesh(geometry, swooshMaterial );
swooshSmall2.scale.set(0.02,1.4,0.02)
swooshSmall2.position.set(-3,0,0)

swooshEffect.add( swooshBig, swooshSmall1, swooshSmall2 );
swooshEffect.position.set(0,20,0)
scene.add(swooshEffect)

让我们向 boxEffect 对象添加更多道具来调整线条的比例、位置和不透明度。 尝试使用该关键帧以获得所需的效果。

const boxEffectsObj = sheet.object('Effects',{
    boxGlow:types.rgba(),
    swooshScale:types.number(1,{nudgeMultiplier:0.01}),
    swooshPosition:types.number(0,{nudgeMultiplier:0.01}),
    swooshOpacity:types.number(1,{nudgeMultiplier:0.01})
})
boxEffectsObj.onValuesChange((values)=>{
    boxMaterial.emissive.setRGB(values.boxGlow.r,values.boxGlow.g,values.boxGlow.b);
    swooshEffect.scale.setY(values.swooshScale);
    swooshEffect.position.setY(values.swooshPosition);
    swooshMaterial.opacity=values.swooshScale;
})

10、漫画文字效果

是时候来一些动漫文字效果了:“Boink!”

THREE 导入 {CSS2DRenderer,CSS2DObject} 并创建一个 textRenderer。 让我们将 style.position 设置为 absolute 并更新 OrbitControlsdomElement

创建一个新的 CSS2D 对象,将其添加到场景中,然后添加一个表示该对象的 HTML 元素。 将文本添加到框中,使其跟随屏幕上的框位置。

  <div id="boink">Boink!!</div>
import {CSS2DRenderer,CSS2DObject} from 'three/examples/jsm/renderers/CSS2DRenderer'
let textRenderer = new CSS2DRenderer();
textRenderer.setSize(window.innerWidth,window.innerHeight);
textRenderer.domElement.style.position = 'absolute';
textRenderer.domElement.style.top = "0";
textRenderer.domElement.style.left = "0";
textRenderer.domElement.style.width = "100%";
textRenderer.domElement.style.height = "100%";
textRenderer.domElement.style.zIndex = "2";
document.body.appendChild(textRenderer.domElement)

// OrbitControls
controls = new OrbitControls(camera, textRenderer.domElement);

// Text Effects
const boinkDom = document.getElementById('boink');
const boinkText = new CSS2DObject(boinkDom);
boinkText.position.set(-25,0,0)
box.add(boinkText);

// add this to your render()/tick() function
// textRenderer.render(scene, camera);

创建一个新的 theatre.js 对象: textEffectObj,其中包含不透明度、文本和比例的属性。

使用 onValuesChange(),更新HTML 元素的 innerText。 Theater.js 的一个有趣之处是:它也可以用于修改文本和动画文本。 对所有属性进行排序并添加关键帧,使文本在盒子弹起时弹出。

const textEffectObj = sheet.object('text',{
    opacity:1,
    text:"",
    scale: 1
});

textEffectObj.onValuesChange((values)=>{
    if(!boinkDom)return;
    boinkDom.innerText = values.text;
    boinkDom.style.opacity = ""+values.opacity
    boinkDom.style.fontSize = ""+values.scale+"px";
})

11、鼠标音效

最后,为了让一切变得栩栩如生,让我们添加声音效果。 我在 Pixabay 上搜索了一些免费的声音并将它们导入到项目中。 然后我使用 Three.js AudioLoader 加载它们。 以下是我向 Three.js 项目添加声音的方法:

// importing my sounds as urls
import swooshSound from '../assets/sounds/whoosh.mp3';
import boinkSound from '../assets/sounds/boink.mp3';
import thudSound from '../assets/sounds/loud-thud-45719.mp3';

const listener = new THREE.AudioListener();
const loader = new THREE.AudioLoader(loadingMgr);
let soundReady = false;
const swoosh = new THREE.Audio(listener)
const boink = new THREE.Audio(listener)
const thud = new THREE.Audio(listener)

setupSounds();

function setupSounds() {
  camera.add(listener);

  audioSetup(swoosh,swooshSound,0.3,loader)
  audioSetup(boink,boinkSound,0.2,loader)
  audioSetup(thud,thudSound,0.5,loader)
}

function audioSetup(sound:THREE.Audio, url:string, volume:number, loader:THREE.AudioLoader){
  loader.load(
    url,
    // onLoad callback
    function ( audioBuffer ) {
      sound.setBuffer( audioBuffer );
      sound.setVolume(volume)
      sound.loop=false;
    },
  );
}

设置完成后,我们可以根据序列中的指针位置继续播放声音。 我们可以通过利用  onChange() 钩子并监视指针位置的变化以在特定时间间隔触发声音播放来实现此目的。

// play the audio based on pointer position
onChange(sheet.sequence.pointer.position, (position) => {
    if(!soundReady)return;
    if(position > 0.79 && position < 0.83){
        if(!thud.isPlaying){
            thud.play();
        }
    }
    else if(position > 1.18 && position < 1.23){
        if(!boink.isPlaying){
            boink.play();
        }
    }
    else if(position > 0.00 && position<0.04){
        if(!swoosh.isPlaying){
            swoosh.playbackRate= 1.7;
            swoosh.play();
        }
    }
})

要为 click添加新的事件侦听器,将 soundReady 设置为 true,并利用 sheet.sequence.play() 播放动画,迭代计数为 Infinity,范围为 0-2。

<style>
.enterSceneContainer{
  z-index: 4;
  position: absolute;
  display: block;
  width: 100%;
  height: 100%;
  text-align: center;
  transition: all 0.5s ease;
}
</style>
<div class="enterSceneContainer" id="tapStart">
    <p>Tap to start</p>
</div>
// Play sequence on click once all assets are loaded
const tapStart = document.getElementById('tapStart');

tapStart.addEventListener(
    'click',
    function () {
        soundReady = true;
        tapStart.style.opacity = "0";
        setTimeout(()=>{
          tapStart.style.display = "none";
        },400)
        sheet.sequence.play({ iterationCount: Infinity, range: [0, 2] });
    }
);

12、色调映射和编码器

为了增强场景的颜色,你可以为渲染器指定不同的 toneMappingsoutputEncodings

在尝试了各种选项后,我选择将它们设置为这个特定项目的 LinearToneMappingsRGBEncoding

renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.LinearToneMapping;

要添加雾并将其与场景背景同步,可以在 colorObj.onValuesChange() 钩子中包含相关代码。

scene.fog = new THREE.FogExp2(0xffffff, 0.009);
colorObj.onValuesChange((values)=>{
    // @ts-ignore
    scene.fog.color = new THREE.Color(values.backgroundColor.toString());
    // ... rest of the code here ...
})

13、部署到生产环境

为了完成该项目,我们需要导出动画并将其部署到生产中。 只需单击Studio UI 中的项目名称并选择“导出到 JSON”,然后保存状态即可。

将保存的状态导入到 main.js 中,并在 getProject 中传递保存的状态。

import projectState from '../assets/Saved_TheatreState.json';

project = getProject('TheatreTutorial_1', { state: projectState });

导出动画后,你可以删除 studio importstudio.initialize(),因为制作不需要它们。 或者,你可以根据需要有条件地删除或包含它们。

let project;
// Using Vite
if (import.meta.env.DEV) {
    studio.initialize();
    // Create a project from local state
    project = getProject('TheatreTutorial_1');
}
else {
    // Create a project from saved state
    project = getProject('TheatreTutorial_1', { state: projectState });
}

不要忘记查看最终代码。 或者,也可以点击此链接来观看视频教程。


原文链接:Mastering Theatre.js Animations: Learn to Create Dynamic Visuals

BimAnt翻译整理,转载请标明出处