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
后缀)作为 SceneMeta 的name
字段自动实现这个 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
RanimTimeline 和 Rabject 这两个类型非常重要,将贯穿整个 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) | 获取对应 id 的 Timeline 的不可变引用 |
r.timeline_mut(id) | 获取对应 id 的 Timeline 的可变引用 |
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>>
的封装。