通过动画让我的 3D 甜甜圈栩栩如生

上周我开始改进我的实验性甜甜圈网站。这帮助我再次熟悉了 React 的基础知识,并更好地了解了 three.js 的基础知识。
我一直在阅读一些 react-three/drei 文档,看看我能做什么,并得到了一些我立即尝试实施的想法。
设置动画
因为我按照 Blender 教程创建了甜甜圈,所以我还创建了一个简单的动画,我想在用户点击甜甜圈时播放它。
幸运的是,这比我想象的要容易得多。我基于模型生成的代码已经实现了一些钩子,这些钩子处理了大部分。
const { nodes,materials, animations } = useGLTF(
"/new-donut-for-threejs.gltf"
);
const { actions } = useAnimations(animations, group);
我正在使用包useGLTF()中的钩子@react-three/drei,它useLoader在GLTFLoader内部使用,以加载甜甜圈模型。
为了实际播放动画,我需要使用一个AnimationMixer,为了轻松做到这一点,我可以使用useAnimations钩子,它抽象出了AnimationMixer.
然后,我可以使用挂钩返回的对象中的解构动作变量来处理动画。因为我想在用户点击模型时播放动画序列,所以我必须创建一个onClick添加到整个模型组的函数。事后看来,我可能应该将它添加到网格上,但无论哪种方式都有效。
如何播放动画
从理论上讲,播放动画非常容易。actions 变量同时具有 aplay()和 astop()函数,并且它在技术上完美地工作。
我创建了一个animationState检查我是否应该开始或停止动画并分别更新变量。
const [animationState,setAnimation] = useState ( false );
consthandleClick = () => {
if(!animationState) {
setAnimation(true);
actions.DonutAction.play();
} else {
setAnimation(false);
actions.DonutAction.stop();
}
}
唯一的问题是,该stop()函数完全重置了动画。这意味着甜甜圈突然跳回了它的起始位置。现在这很好,但我真的很想找到一个解决方案,当用户再次单击时,动画会在原处停止并从那里继续。
跟随我的鼠标
我想让甜甜圈跟随用户的鼠标移动,让整个网站感觉更有活力。这实际上很容易通过一些简单的数学和useFrame钩子来实现。

钩子useFrame被称为每一帧,非常适合实现这样的效果。
它接受一个回调函数,该函数接收一个包含大量有用数据的状态,例如鼠标位置,这非常适合我想做的事情。
首先,我根据鼠标位置和视口尺寸计算了 x 和 y 坐标,并将其除以一个基本上代表效果敏感程度的数字。
然后我只使用lookAt()我之前初始化的引用中的方便函数并将代码包装在 if 语句中,因为它应该只在动画不活动时跟随鼠标。
const group = useRef();
useFrame ( ( { mouse, viewport } ) => {
if (!animationState) {
const x = (mouse.x * viewport.width ) / 2.5 ; const y = (mouse.y * viewport.height ) /2.5 ; group.当前.lookAt (x, y, 1 ) ; } });
修复动画
在让小鼠标功能正常工作后,我回去尝试解决我的动画问题。经过将近一个小时的反复试验和阅读文档,我终于找到了这个setEffectiveTimeScale()功能。
为了理解为什么该stop()功能首先不起作用,我想更详细地解释一下。在处理AnimationAction具有一些属性的实例的地方,以跟踪动画的当前状态。
例如:如果您调用isRunning()一个动作,则必须满足一些条件才能使函数返回 true。paused必须等于false,enabled必须等于true,timeScale不为0,没有延迟启动的调度。后者在本案中并不重要。
那么当你调用函数时会发生什么stop()?动画将立即停止并重置,这意味着paused设置为 false,enabled设置为 true 和time0。这就是动画突然跳转到其初始状态的原因,所以我不得不找到另一个解决方案。
这是我找到该setEffectiveTimeScale()功能的地方。顾名思义,它让我设置timeScale动画的和停止,或者更好的是,暂停动画我只是将值设置为 0。
它就像我想要的那样工作。动画恰好停在它暂停的地方。现在唯一的问题是我突然无法继续动画。调试我的代码后,我更加困惑,因为一切都按我预期的那样被调用。
所以回到阅读文档。事实证明,调用play()并不一定意味着动画会立即开始。它还受某些条件的约束。我很快意识到我的具体情况的问题所在。
从文档中引用:
其他一些设置(paused =true、enabled =false、weight =0、timeScale =0)也可以阻止动画播放。
猜猜我刚刚做了什么?确切地!我设置timeScale为0!
所以现在我的解决方案是在调用函数timeScale之前将 设置为 1 。play()
consthandleClick = () => {
if(!animationState) {
setAnimation(true);
actions.DonutAction.setEffectiveTimeScale(1)
actions.DonutAction.play();
} else {
setAnimation(false);
actions.DonutAction.setEffectiveTimeScale(0)
}
}这绝对不像预期的方式,但它现在有效。
添加一些弹簧
现在我已经有了一些非常基本的东西,我想与甜甜圈有更多可能的互动和效果。而且我已经看到一些示例,其中react-spring与 three.js 结合使用,所以我想尝试一下!
该软件包带有自己的一套挂钩和动画元素,让您的生活尽可能轻松。我可能最常使用的钩子是useSpring.
我的想法很简单。当用户将鼠标悬停在甜甜圈上时,它应该变大,当光标离开甜甜圈时,它应该恢复到正常大小。而 react-spring 实际上让这对你来说非常简单。
首先,我需要一种方法来检测用户是否将鼠标悬停在甜甜圈上。嗯,最大的问题是弄清楚该属性实际上被称为什么,因为有 3 种不同的变体,但onPointerOver最终onPointerOut对我有用。
我快速设置了另一个状态变量,我称之为“活动”。
命名非常好,以后阅读代码的人乍一看都不知道它为什么存在。
在onPointerOver我刚刚调用的函数中setActive(true),我知道用户当前正在将鼠标悬停在甜甜圈上,而在函数中onPointerOut我做相反的事情并调用setActive(false)。
这样我就可以初始化挂钩useSpring并根据活动状态更改比例属性。
const { scale } = useSpring({
scale : active ? 1.5 : 1 ,
});
现在我只需要用附带的元素更改mesh元素,一切都按预期进行。animated.mesh@react-spring/three
与此同时,我还将启动和暂停动画的逻辑从函数onClick移到了悬停逻辑,因为对我来说,这样感觉更流畅。
<animated.mesh
scale={scale}
onPointerOver={() => {
setAnimation(true);
actions.DonutAction.setEffectiveTimeScale(1)
actions.DonutAction.play();
setActive(true)
}
}
onPointerOut={() =>{
setAnimation(false);
actions.DonutAction.setEffectiveTimeScale(0)
setActive(false)
}
}
>
</animated.mesh>
这是我本周的最后一项成就。我想添加更多颜色,而不仅仅是这个无聊的黑色背景。我在 react spring 文档中找到了一个很好的示例,用于使用异步 CSS 变量更改背景颜色。这可能比它必须的要复杂得多,但让它工作是一个很好的小挑战。

https://codesandbox.io/s/8x50e?file=/src/App.tsx
那我是怎么做到的呢?
在我目前实现我的甜甜圈组件的 App 组件中,它也是反应应用程序的根,我粘贴了 spring 的代码。
const [{ background }] = useSpring(
() => ({
from: { background: "var(--step0)" } ,
to: [
{ background: "var(--step1)" },
{ background: "var(--step2)" },
{ background: "var(--step3)" },
{ background: "var(--step4)" },
] ,
config: config.molasses,
loop: {
reverse: true ,
} ,
}),
[]
);
我在 CSS 文件中添加了变量。
:root {
--step0 : #050505 ;
--step1 : #bad5ea ;
--step2:#fd8769;
--step3:#ffdcb3;
--step4:#ff615d;
}
动画部分应该可以正常工作。现在我只需要找出一种方法来了解用户何时将鼠标悬停在甜甜圈上以将根 div 的样式更改为此动画背景。
那么我的解决方案很简单。我再次在 App 组件中创建了一个新的状态变量。
const [isHovering,setIsHovering] = useState ( false );
出于某种原因,这次我决定正确命名.. 仍然不完美但更好。
然后我在我刚刚调用的 Donut 组件中实现了一个回调函数,onHover如果它在函数中被调用,我将传递它 true onPointerOver,或者在onPointerOut函数中传递 false。
<animated.mesh
onPointerOver={() => {
setAnimation(true);
props.onHover(true);
actions.DonutAction.setEffectiveTimeScale(1);
actions.DonutAction.play();
setActive(true);
}}
onPointerOut={() => {
setAnimation(false);
props.onHover(false);
actions.DonutAction.setEffectiveTimeScale(0);
setActive(false);
}}
>
</animated.mesh>
现在看这个,代码肯定需要一些重构,但我稍后再做......
回到 App 组件中,我刚刚调用了setIsHovering()回调函数
< DonutGLTF onHover={ ( isAnimating ) => { setIsHovering (isAnimating)}} />
现在我可以对根 div 进行最后的更改。因为它应该是动画的,所以我还需要将它更改为一个animated.div元素,最后,我只需将这一行添加为一个属性,它只检查是否为isHovering真,然后将背景添加为样式,如果为假,则为 null。
style ={isHovering ? {background } : null}
下周我想要实现的是,在甜甜圈周围添加粒子并实现某种 3D 文本,因为当我要创建我的实际站点时,这将是必不可少的。
感谢您的阅读!
资料来源:
react-three/drei 文档: https: //github.com/pmndrs/drei
AnimationAction 文档:https://threejs.org/docs/#api/en/animation/AnimationAction
AnimationMixer 的文档:https://threejs.org/docs/#api/en/animation/AnimationMixer
使用弹簧的文档:https ://react-spring.dev/docs/components/use-spring
动画背景示例:https://codesandbox.io/s/8x50e?file=/src /App.tsx