Declarative Animation
Declarative animation is a big topic. We'll scratch the surface here.
The declarative animation loop
One way you can achieve an animation with react-babylonjs
is by using the
Engine render loop, which runs many times per second. You can receive callbacks
for this loop by using the useBeforeRender
hook. As its name implies, it is
called before each frame render and allows you to make any changes needed to
simulate animations.
The beauty of Component based design is that each React component can register their own animation loop handler and take responsibility just for its small task.
Here's some basic usage:
const scene = useScene()useBeforeRender(() => {if (scene) {// This gets called ~60 FPS or whatever your frame rate is.// Don't do a lot in here, just essentials}})
Degrees of Freedom
Every object in 3D has a position (x, y, z) in space and a rotation (roll, pitch, yaw). As google states it:
Rotational movement is relative to the x, y, and z axes, commonly termed pitch, yaw, and roll. The position corresponds to translational movement along those axes, which can be thought of as moving forward or backward, moving left or right, and moving up or down.
You can read a lot about this elsewhere, so we won't cover it in much depth here. The main point to take away is that animation involves affecting one or more of these degrees of freedom on each object (animations can also be for colors, brightness, etc.).
Let's try useBeforeRender
to adjust these properties.
Creating the famous rotating box
Using a box as an example - we will rotate it about the Y axis, known as yaw. And, we'll do it many times per second.
Here's how to make a box (<box ... />
(recall from previous guide how it uses
the Babylon.js
CreateBox). We
need to make sure we are passing required constructor (or factory method)
arguments.
- Only
name
is required - theScene
object will be passed for you automatically. - Properties of the returned object of type
Mesh
can be set as well (ie:position
,rotation
). Changes to those properties will flow to the underling mesh - just like changing a DOM element attribute works.
<box name="box" size={2} position={new Vector3(0, 1, 0)} />
- Output
- Code
Now to make it spin. We will make the box spin about the Y axis. From Mesh rotation in Babylon we see that the Y axis is the vertical axis.
To achieve animation, we have to alter the box's Y rotation. Ok, let's do it. First let's try using state, so we need a state variable to hold the Y value:
const [y, setY] = useState(0)
Then, we need to alter it in the render loop:
useBeforeRender(() => {setY((oldY) => oldY + 0.1)})
To put everything together, we can create a Functional Component (FC) that wraps
the box
tag with our new behaviors.
import { Vector3 } from '@babylonjs/core/Maths/math.vector'import React, { FC, useState } from 'react'import { useBeforeRender } from 'react-babylonjs'const RotatingBox: FC = () => {const [y, setY] = useState(0)useBeforeRender(() => {setY((oldY) => oldY + 0.1)})return (<boxname="box"size={2}position={new Vector3(0, 1, 0)}rotation={new Vector3(0, y, 0)}/>)}
- Output
- Code
Using real-world timing in animations
The rotating box is fun, but it would be nice to control rotation speed in terms of real-world time (ie: smooth animation). Also, different devices will rotate different speeds depending on frame rate this way!
Let's adjust the rotation to Revolutions Per Minute (RPM). To do that, we need
to know that one complete revolution of yaw is Math.PI / 2
.
We also need to know how many milliseconds have passed between animation frames
(since it is not consistent). Fortunately Babylon.js provides a getDeltaTime()
that provides that duration. Using all that information, we can do this instead:
const RPM = 5setY((oldY) => oldY + (RPM / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000))
Making rpm
a component prop
Let's use the power of Typescript to create some spinning box props that accepts RPM (not needed in JavaScript projects):
type RotatingBoxProps = {rpm: number}
This will be the props that we pass to the box. Let's update the box component to use it:
const RotatingBox: FC<RotatingBoxProps> = ({ rpm }) => ...
And then update the hook:
useBeforeRender(() => {// ...setY((oldY) => oldY + (rpm / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000))})
Now that's a nice reusable component! We use it like this:
<RotatingBox rpm={10} />
Let's try it. We should see one rotation every 6 seconds when rpm
is 10
(60/10=6).
- Output
- Code
Making rpm
even more reactive
In the interests of completeness, you might notice the hook is referring to
rpm
. If rpm
were to change, we would want to make sure that new value took
effect. Therefore, just like useEffect
, rpm
is a dependency:
useBeforeRender(() => {// ...},undefined,undefined,undefined,[rpm])
You can try it out by choosing the RPM here and seeing that it alters the rotation speed accordingly.
- Output
- Code
Direct object manipulation (not via state)
The state props will flow on the reconciler schedule and changing state in the render loop can have very noticeable performance implications.
Directly altering Babylon.js object properties via refs bypasses that overhead and will be a more accurate animation smoothing. Whereas the reconciler will schedule the updates using it's own algorithm, this method will immediately apply the update. It should be noted as well that this will not cause re-renders of the component itself, which was innefficiently creating new Vector3 objects every render in above examples.
- Output
- Code
There is also in Example->Basic->Animations that uses an Animation object that can be looped and has easing functions, etc.