Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting Started

注意:

当前本章内容非常不完善,结构不清晰、内容不完整,目前建议结合 Example 和源码来了解。

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

use ranim::prelude::*;

#[scene]
struct HelloWorldScene;

impl TimelineConstructor for HelloWorldScene {
    fn construct(
        self,
        r: &mut RanimScene,
        r_cam: TimelineId<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 来构造一个 RanimScene 并对其进行渲染,渲染结果将被输出到 <output_dir>/<scene_name>/ 目录下。

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

  • timeline: &mut RanimScene:Ranim API 的主要入口,几乎全部对动画的编码操作都发生在这个结构上
  • camera: TimelineId<CameraFrame>:默认的相机时间线的 Id,也是 RanimScene 中被创建的第一个 Timeline

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

1. Timeline 基础

RanimScene 中有若干个「物件时间线」,每个物件时间线中包含一个动画列表。

通过 r.insert(state) 可以创建一个 Timeline

let square: Square = Square::new(2.0).with(|square| {
    square.set_color(manim::BLUE_C);
});
let r_square: TimelineId<Square> = r.insert(square);

RanimScene 中有关 Timeline 的方法如下:

方法描述
r.init_timeline(state)创建一个 Timeline
r.timeline(id)获取对应 idTimeline 的不可变引用
r.timeline_mut(id)获取对应 idTimeline 的可变引用
r.timelines()获取类型擦除后的全部时间线的不可变引用
r.timelines_mut()获取类型擦除后的全部时间线的可变引用

时间线是用于编码动画的结构,首先介绍几个最基本的操作:

  • 使用 timeline.forward(duration_secs) 来使时间线推进一段时间
  • 使用 timeline.play(anim) 来向时间线中插入一段动画
  • 使用 timeline.show()timeline.hide() 可以控制物体接下来 forward 时显示与否。

下面的例子使用一个 Square 初始化了一个时间线,然后编码了淡入1秒、显示0.5秒、消失0.5秒、显示0.5秒、淡出1秒的动画:

// A Square with size 2.0 and color blue
let square = Square::new(2.0).with(|square| {
    square.set_color(manim::BLUE_C);
});

let timeline = r.init_timeline(square.clone());
timeline.play(square.clone().fade_in());
timeline.forward(1.0);
timeline.hide();
timeline.forward(1.0);
timeline.show();
timeline.forward(1.0);
timeline.play(square.fade_out());

时间线内部维护了一个物件的状态值,在 forward 时会使用它来编码静态的动画,通过 timeline.state() 可以获取时间线内部的物件状态值。于是上面的代码也可以写作:

let timeline: &mut ItemTimeline<Square> = r.init_timeline(square);
timeline.play(timeline.state().clone().fade_in());
// ...
timeline.play(timeline.state().clone().fade_out());

同时为了便捷,还有一个 timeline.play_with(builder) 方法来编码动画:

impl<T: Clone + 'static> ItemTimeline<T> {
    // ...
    pub fn play_with(&mut self, anim_func: impl FnOnce(T) -> AnimationSpan<T>) -> T {
        self.play(anim_func(self.state.clone()))
    }
}

于是之前的代码也可以写作:

let timeline: &mut ItemTimeline<Square> = r.init_timeline(square);
timeline.play_with(|square| square.fade_in());
// ...
timeline.play_with(|square| square.fade_out());

核心概念

动画

本节将对 Ranim 中 动画 的实现思路进行讲解。

EvalDynamic<T> Trait

一个标准化的动画其实本质上就是一个函数 ,它的输入是一个进度值 ,输出是该动画在对应进度处的结果

这个函数 不仅定义了动画的 求值,同时其内部也包含了求值所需要的 信息。对应到计算机世界,其实也就是 算法数据,而对应到编程语言上也就是 方法数据类型

在由 Rust 实现的 Ranim 中也就是 EvalDynamic<T> Trait 和实现了它的类型 T

pub trait EvalDynamic<T> {
    fn eval_alpha(&self, alpha: f64) -> T;
}

它接受自身的不可变引用和一个进度值作为输入,经过计算,输出一个自身类型的结果。

Transform 动画为例,其内部包含了物件初始状态和目标状态,以及用于插值的对齐后的初始和目标状态,在 EvalDynamic<T> 的实现中使用内部的数据进行计算求值得到结果:

/// Transform Anim
pub struct Transform<T: TransformRequirement> {
    src: T,
    dst: T,
    aligned_src: T,
    aligned_dst: T,
}

impl<T: TransformRequirement> EvalDynamic<T> for Transform<T> {
    fn eval_alpha(&self, alpha: f64) -> T {
        if alpha == 0.0 {
            self.src.clone()
        } else if 0.0 < alpha && alpha < 1.0 {
            self.aligned_src.lerp(&self.aligned_dst, alpha)
        } else if alpha == 1.0 {
            self.dst.clone()
        } else {
            unreachable!()
        }
    }
}

AnimationSpan

有了以进度 为输入标准化的动画函数后,加上持续秒数 、速率函数 ,就可以构造一个以秒 为输入的动画函数

在 Ranim 中,这对应着 AnimationSpan 结构:

pub struct AnimationSpan<T> {
    pub(crate) evaluator: Evaluator<T>,
    pub rate_func: fn(f64) -> f64,
    pub duration_secs: f64,
}

impl<T> AnimationSpan<T> {
    pub fn eval_alpha(&self, alpha: f64) -> EvalResult<T> {
        self.eval_sec(alpha * self.duration_secs)
    }
    pub fn eval_sec(&self, local_sec: f64) -> EvalResult<T> {
        self.evaluator.eval_alpha((self.rate_func)(
            (local_sec / self.duration_secs).clamp(0.0, 1.0),
        ))
    }
}

其中的 evaluator: Evaluator<T> 其实就是对 Box<dyn EvalDynamic<T>> 的封装。