Ranim 是一个使用 Rust 编写的程序化动画引擎, 受 3b1b/manimjkjkil4/JAnim 启发

  • 矢量图形基于二阶贝塞尔曲线表示,使用 SDF 渲染
  • 使用 wgpu,兼容多种后端图形 API

注:目前 ranim 的接口仍并不稳定,而本页面的说明文字可能更新的不是特别勤(但是带视频的部分即 example 是会同步更新的)

Getting Started

在 Ranim 中,定义并渲染一段动画的方式基本长成下面这个样子:

use ranim::prelude::*;

#[scene]
struct HelloWorldScene;

impl TimelineConstructor for HelloWorldScene {
    fn construct(
        self,
        timeline: &RanimTimeline,
        camera: &mut Rabject<CameraFrame>,
    ) {
        // ...
    }
}

fn main() {
    render_scene(HelloWorldScene, &AppOptions::default());
}

HelloWorldScene 是一个 Scene,即下面两个 Trait 的组合:

  • SceneMetaTrait 实现了 fn meta(&self) -> SceneMeta 方法。

    使用 #[scene] 会以结构体的 snake_case 命名(去掉 Scene 后缀)作为 SceneMetaname 字段自动实现这个 Trait。

    也可以通过 #[scene(name = "<NAME>")] 来手动命名。

  • SceneConstructor 则是定义了动画的构造过程。

使用 render_scene 可以用一个 Scene 来构造一个 RanimTimeline 并对其进行渲染,渲染结果将被输出到 <output_dir>/<scene_name>/ 目录下。

construct 方法有两个关键的参数:

  • timeline: &'t RanimTimeline:Ranim API 的主要入口,几乎全部对动画的编码操作都发生在这个结构上
  • camera: &'r Rabject<'t, CameraFrame>:默认的相机 Rabject,也是 RanimTimeline 中被插入的第一个 Rabject

RanimTimelineRabject 这两个类型非常重要,将贯穿整个 Ranim 动画的编码。

1. RanimTimeline 和 Rabject

Ranim 使用一个 RanimTimeline 结构来编码动画,首先介绍两个最基本的操作:

  • 使用 timeline.forward(duration_secs) 来使时间线推进一段时间
  • 使用 timeline.insert(item) 来将一个 item: T 插入时间线,返回一个 Rabject<T>

Rabject<T> 的结构很简单,如下:

pub struct Rabject<'a, T> {
    pub id: usize,
    pub data: T,
}

当某个物件 T 被插入 RanimTimeline 中时,会被赋予一个 Id,以 Rabject<T> 的形式返回,同时在 RanimTimeline 内部会以 T 的值为初始状态创建一条 RabjectTimeline

使用 timeline.show(&rabject)timeline.hide(&rabject) 可以控制接下来 forward 时的表现。

此外 hide 有一个获取 Rabject 所有权的变体:timeline.remove(rabject),与 hide 效果相同。

下面的例子使用一个 VItem 物件和 timeline.insert 在时间线中创建了一个 Rabject<VItem> 并展示了 showhide 以及 remove 对其影响:

1use ranim::{color::palettes::manim, items::vitem::Square, prelude::*};
2
3#[scene]
4struct GettingStarted0Scene;
5
6impl TimelineConstructor for GettingStarted0Scene {
7 fn construct(self, timeline: &RanimTimeline, _camera: &mut Rabject<CameraFrame>) {
8 let mut square = Square(2.0).build(); // An VItem of a square
9 square.set_color(manim::BLUE_C);
10
11 timeline.forward(0.5);
12 let square = timeline.insert(square); // Create a "Rabject" in the timeline
13 timeline.forward(0.5); // By default the rabject timeline is at "show" state
14 timeline.hide(&square);
15 timeline.forward(0.5); // After called "hide", the forward will encode blank into timeline
16
17 timeline.show(&square);
18 timeline.forward(0.5);
19
20 timeline.remove(square); // Currently is equal to `timeline.hide(&rabject)`, but takes the owner ship
21 timeline.forward(0.5);
22 }
23}
24
25fn main() {
26 render_scene(GettingStarted0Scene, &AppOptions::default());
27}
28

2. 播放动画

Ranim 中的每一个动画都会为实现了对应 Trait 的物件添加对应的创建方法。

比如对于 FadingAnim,凡是实现了 Opacity + Interpolatable Trait 的物件都会拥有 fade_infade_out 方法。

对一个 Rabject<T> 调用创建动画的方法会返回一个 AnimSchedule<T>,将它传入 timeline.play(anim_schedule) 即可将这段动画编码在 RanimTimeline 中。

let mut square = timeline.insert(square);
timeline.play(square.fade_in());
timeline.play(square.fade_out());

上面的动画也可以这样写:

let mut square = timeline.insert(square);
timeline.play(square.fade_in().chain(|data| data.fade_out()));

AnimSchedule<T>chain 方法,接受一个 impl FnOnce(T) -> Animation<T>,会将两个动画拼接在一起。

T&'r mut Rabject<'t, T> 相同,也有创建动画的方法,不过返回的是 Animation<T>

1use ranim::{
2 AppOptions, animation::fading::FadingAnimSchedule, color::palettes::manim,
3 items::vitem::Square, prelude::*, render_scene,
4};
5
6#[scene]
7struct GettingStarted1Scene;
8
9impl TimelineConstructor for GettingStarted1Scene {
10 fn construct(self, timeline: &RanimTimeline, _camera: &mut Rabject<CameraFrame>) {
11 let mut square = Square(2.0).build();
12 square.set_color(manim::BLUE_C);
13
14 let mut square = timeline.insert(square);
15 #[allow(deprecated)]
16 timeline.play(square.fade_in());
17 timeline.play(square.fade_out());
18 }
19}
20
21fn main() {
22 render_scene(GettingStarted1Scene, &AppOptions::default());
23}
24

3. 动画参数

AnimSchedule<T>Animation<T> 都具有一些控制动画属性的参数,可以通过链式调用的方式来设置:

  • with_duration(duration_secs):设置动画持续时间
  • with_rate_func(rate_func):设置动画速率函数

此外在这个例子中你会发现,在播放了 transform_to(circle) 之后,再播放 fade_out 时,播放的并不是圆形的淡出,而是方形。

这并不是一个 Bug,而是一种刻意的设计,请继续向下阅读 4. 向 Rabject 应用动画变更,了解更多。

1use ranim::{
2 animation::{fading::FadingAnimSchedule, transform::TransformAnimSchedule},
3 color::palettes::manim,
4 items::vitem::{Circle, Square},
5 prelude::*,
6 utils::rate_functions::linear,
7};
8
9#[scene]
10struct GettingStarted2Scene;
11
12impl TimelineConstructor for GettingStarted2Scene {
13 fn construct(self, timeline: &RanimTimeline, _camera: &mut Rabject<CameraFrame>) {
14 let mut square = Square(2.0).build();
15 square.set_color(manim::BLUE_C);
16
17 let mut square = timeline.insert(square);
18 let mut circle = Circle(2.0).build();
19 circle.set_color(manim::RED_C);
20
21 timeline.play(
22 square
23 .transform_to(circle)
24 .with_duration(2.0)
25 .with_rate_func(linear),
26 ); // Anim Schedule won't change the data in Rabject
27 timeline.forward(1.0);
28 timeline.play(square.fade_out()); // Anim is created based on the data in Rabject
29 }
30}
31
32fn main() {
33 render_scene(GettingStarted2Scene, &AppOptions::default());
34}
35

4. 向 Rabject 应用动画变更(AnimSchedule 与 apply)

使用 Rabject 创建动画时是基于 Rabject 当前的内部数据来创建的,创建与播放动画并不会修改其内部数据。 如果想要一个动画的效果实际应用到 Rabject 中,那么需要对 AnimSchedule 使用 apply 方法。

这样的好处是对于一些对数据有 损坏性变更 的动画(比如 unwrite 等),我们不需要提前对数据进行备份。

1use std::f64::consts::PI;
2
3use glam::DVec3;
4use ranim::{
5 animation::{
6 creation::WritingAnimSchedule, fading::FadingAnimSchedule, transform::TransformAnimSchedule,
7 },
8 color::palettes::manim,
9 items::vitem::{Circle, Square},
10 prelude::*,
11};
12
13#[scene]
14struct HelloRanimScene;
15
16impl TimelineConstructor for HelloRanimScene {
17 fn construct(self, timeline: &RanimTimeline, _camera: &mut Rabject<CameraFrame>) {
18 let mut square = Square(2.0).build();
19 square.set_color(manim::BLUE_C);
20
21 let mut square = timeline.insert(square);
22 let mut circle = Circle(2.0).build();
23 circle.rotate(PI / 4.0 + PI, DVec3::Z);
24 circle.set_color(manim::RED_C);
25
26 timeline.play(square.transform_to(circle).apply()); // Use `apply` on an anim schedule to update rabject data
27 timeline.play(square.unwrite()); // Do not use `apply` to keep the data in Rabject not changed
28 timeline.play(square.write());
29 timeline.play(square.fade_out());
30 }
31}
32
33fn main() {
34 #[cfg(feature = "app")]
35 run_scene_app(HelloRanimScene);
36 #[cfg(not(feature = "app"))]
37 {
38 render_scene(HelloRanimScene, &AppOptions::default());
39 render_scene_at_sec(HelloRanimScene, 0.0, "preview.png", &AppOptions::default());
40 }
41}
42

不过 chain 是会以第一个动画的结束状态为基础创建下一个动画的,但是要注意此时的 AnimSchedule 是整个被拼接后的动画,如果不调用 apply 是不会更新 Rabject 内部的数据的,而调用 apply 会应用整个被拼接后的动画的变更:

// <-- Rabject's data is a square
timeline.play(
    square
        .transform_to(circle)
        .chain(|data| data.unwrite())
);
// <-- Rabject's data is still a square
timeline.play(square.write()); // This plays a square's unwrite, but not circle's
// <-- Rabject's data is a square
timeline.play(
    square
        .transform_to(circle)
        .chain(|data| data.unwrite())
        .apply(), // <-- Rabject's data is an unwrote circle now
);
timeline.play(square.write()); // This plays nothing, because after the apply, the data is empty(unwrote circle)

简单来说 AnimSchedule 的作用就是将具有紧密关系的动画组合在一起,通过 apply 会应用整个动画(类似 Transaction 的感觉)。