#animation #physics #spring #physics-engine #game-engine #game #engine

modulator

一个用于抽象、解耦调制源的特质

3个版本 (破坏性更新)

0.4.0 2023年3月7日
0.3.0 2022年10月21日
0.1.0 2018年11月22日

#943 in 游戏开发

MIT许可协议

68KB
615

Modulator

点击此处观看Modulator工具箱和应用程序的介绍视频 观看此视频了解应用程序和工具箱的介绍!

点击此处访问Modulator Play应用程序仓库

一个用于抽象、解耦调制源的特质。此工具箱包含

  1. Modulator<T>特质定义
  2. 调制器的环境(宿主)类型ModulatorEnv<T>
  3. 许多可用的类型,它们实现了调制器特质

0.4.0版本中的更改

  • ScalarSpring::undamp参数的行为进行了修订 -- 范围现在是0.0(完全阻尼)和1.0(移除所有阻尼)之间 -- 无阻尼弹簧模拟对任何大小的时间戳都是无条件稳定的 -- 当undamp==1.0时,弹簧可以无限期地振荡 -- 弹簧振荡会损失与时间步长持续时间成比例的能量

0.3.0版本中的更改

  • 更新到Rust 2021版
  • 更新到最新版本的rand,传播API更改
  • metro替换了调制器环境使用的哈希 -- 我们对环境的哈希安全性并不关心,而metro比默认的std哈希器要快得多
  • (仅仓库) 修复了由合并的PR引入的Newtonian中的错误;这个更改从未发布
  • (仅仓库) 删除了由合并的PR添加的依赖项和基于竞技场的环境,这些依赖项和环境存在问题;这个更改从未发布

介绍

调制器是随时间变化的来源,它们独立于它们影响的参数,即它们的目标

这里提出的架构部分灵感来源于音频合成领域,因此让我们通过类比来介绍主要概念。

合成器是一种音乐仪器,其中电子波形发生器产生基本声音,然后经过滤波、放大并输出。虽然波形生成方法和对其应用的加工是声音结果的关键,但仅凭这些本身不足以产生有趣、生动的结果。

为了增加复杂性和深度,可以添加调制来随时间改变合成参数,并产生演变、有机的声音。合成师可以通过连接调制来源到目标来塑造输出。

调制来源包括周期函数、低频振荡器、噪声发生器、性能控制等。

目标通常是影响声音振幅、频率或谐波结构的参数。这导致在时间上对波形进行震颤、颤音、频谱和音色变化等效果。

调制来源和目标应该理想地完全解耦。目标应该能够通过在两者之间建立连接来考虑来自兼容源的数据。

这种起源于模块化合成器的通用方法为乐器可以编程的声音范围增添了巨大的广度。

使这些电子声音丰富的相同调制模型可以用于其他领域。调制可以为计算机程序使用的任何参数集添加生命和多样性。非交互式视觉元素可以动画化,用户反馈可以增强,AI实体的行为可以随时间演变,等等。

包含的有用调制器

当涉及到动画化一个属性时,无论是视觉、听觉还是行为属性,我们通常希望结果是

  • 随机的、非脚本化的,并且没有脚本化的感觉
  • 可控的、精确的
  • 可靠地平滑,没有奇点
  • 物理正确,本能上令人愉悦

这个包提供了如ScalarSpringNewtonianScalarGoalFollowerShiftRegister之类的调制器,它们本身及其组合允许创建具有上述某些或所有特性的调制。

调制器如何工作

调制器需要能够做到以下至少一项

  • 在当前时间点返回其值
  • 随着时间的推移而发展其状态

m为实现了Modulator特质的类型的值,那么

let value = m.value();

返回调制器的当前值。要使用dt微秒来演变调制器,请使用

m.advance(dt);

在实践中,很少直接这样做,因为使用一个环境(调制器的主机)如包含的ModulatorEnv类型,要方便得多。

调制器环境

ModulatorEnv<T>类型是调制器的拥有主机。通常,你会在应用程序中创建一个或多个环境,如下所示

// Somewhere in a struct...
m1: ModulatorEnv<f32>, // hosts modulators that give scalar f32 values

// Somewhere in constructor of that struct...
m1: ModulatorEnv::new(),

上述操作在一个结构体中创建了一个调制器环境m1,可能是一个收集应用程序中与调制相关的所有状态/数据的调制结构。

然后,在应用程序的某个地方,必须将环境通过当前帧的已流逝的dt微秒向前推进,如下所示

// Here st is the modulation data struct that contains m1, dt is elapsed micros
st.m1.advance(dt);

环境推进它所承载的所有启用的调制器。重要的是要注意关于ModulatorEnv的两个方面

  1. 环境拥有它所承载的调制器
  2. 环境与宿主调制器的值T相同,是通用的。

第2点意味着,由于特性Modulator<T>在T中是泛型的,因此一个环境中的所有调制器的值T都必须相同。本包提供的所有调制器类型都是Modulator<f32>,即:它们的值是类型f32的标量。

第1点意味着调制器的生命周期由环境管理,因此您可以“创建并传递”您的调制器,并让环境在它被丢弃时删除它们(ModulatorEnv提供手动管理其调制器生命周期的功能,如果需要的话)。

以下是一个使用Wave调制器的示例。Wave调制器是包含类型中最简单的——它接收一个闭包/Fn来更新其值,并具有振幅和频率值。由于它使用闭包,它可以制作任何信号:波形、常数、随机数等。例如

// Create a sine wave modulator, initial amplitude of 1 and frequency of 0.5Hz
let wave = Wave::new(1.0, 0.5).wave(Box::new(|w, t| {
        (t * w.frequency * f32::consts::PI * 2.0).sin() * w.amplitude
    }));

// Give the modulator to the environment
st.m1.take("wave_sin", Box::new(wave));

这创建了一个产生振幅为1、频率为0.5Hz的正弦波调制器。闭包接收调制器w和经过的时间(t:f32)。

一旦创建,wave传递给宿主m1,宿主将其接收并带有键"wave_sin"

另一个示例

// Create a wave modulator, amplitude (2.0) here is used to define walk bounds,
// while frequency (0.1) is the random range the value moves each time it advances
let wave = Wave::new(2.0, 0.1).wave(Box::new(|w, _| {
    let n = w.value + thread_rng().gen_range(-w.frequency, w.frequency);
    f32::min(f32::max(n, -w.amplitude), w.amplitude)
}));

// Now give the modulator to the environment
st.m1.take("wave_rnd", Box::new(wave));

这个闭包将调制器的当前值在每次advance(dt)时偏移一个随机偏移量(由频率设置),并将其限制在-/+振幅之间。这创建了一个简单的随机游走。

一旦创建了上述调制器并将它们传递给宿主,它们的值可以随时按以下方式读取

let v0 = st.m1.value("wave_sin"); // current value of sine modulator
let v1 = st.m1.value("wave_rnd"); // current value of random walk modulator

调制器详细信息

请注意,调制器在前进时应该缓存它们的value,这意味着即使前进可能很昂贵,读取它们的值也必须始终很快。此外,环境一次将所有调制器向前推进,以确保读取相互依赖的值始终一致。

重要的是要注意,调制器不一定可逆。实际上,大多数都不会。它们只能随时间向前发展。

这种限制的原因是,虽然调制器通常预计是帧率无关的(它们应该将它们的演变表示为时间的函数),但它们也经常会有离散状态变化

例如,包含的调制器ScalarGoalFollower选择一个随机值,将其设置为包含的子调制器的目标,然后观察它,直到它确定子调制器已到达目标。一旦它做到了,追随者就设定一个新的目标并重复该过程。

这种离散状态、随机的行为要使其可逆将很昂贵,并且需要缓存随机生成的值,以及其他问题。由于可逆性在大多数应用中都不是关键的,因此Modulator特性不将其作为其合同的一部分——多功能性更受青睐。

调制器通常期望是帧率无关的,但不是必须的。所有与该包一起提供的调制器都是,即使在包含如上所述的ScalarGoalFollower等离散事件的调制器中也是如此,并且它们在帧长度变化时也保持一致地发展。

波调制器是一个特殊情况,因为它使用闭包来计算其值,它可能是基于时间的,也可能不是,这取决于给定的函数。

回想一下我们之前给 Wave 提供的“正弦波”闭包,其实现显然是时间的函数(在这个简单的情况下,它也是可逆的)。另一方面,“随机游走”闭包则不是,因为值的更新速率是 advance(dt) 被调用的次数的函数,而不是经过的时间。一个根据帧率变化频率的随机游走将非常有限制,在生产代码中,我们会实现一个更复杂的随机游走,其更新速率用每秒的变化来表示。

调制器生命周期和交互

一个 ModulatorEnv 宿主只知道关于它拥有的调制器两件事

  1. 它们实现了 Modulator<T>
  2. 它们有相同的 T (值类型)

这意味着环境可以对其调制器执行的唯一操作是由 Modulator 特性定义的操作。

虽然 sources.rs 中提供的调制器类型都是为了它们作为调制器的角色而设计的,但其他类型可以实现调制器特性并获得调制能力(尽管在这种情况下,它们可能不会被存储在一个 拥有 环境中)。

很明显,ModulatorEnv 的内容是异质的 - 它们唯一知道的是,它们为相同的 T 实现了 impl Modulator<T>。这是 Rust 的 特制对象 的一个正确用例,实际上这正是 ModulatorEnv 存储它拥有的调制器的方式。

通常调制器会被创建,添加到环境中,然后在目标点进行计算,目标点通过在添加时给宿主分配的符号名称来指定。例如

// Here we are updating some value by scaling it with a modulator, source
// is the name of the modulator in environment m1
self.height = self.base + self.range * st.m1.value(source);

然而,有时您可能希望从环境中访问一个调制器并修改其某些内容,也许是为了通过另一个调制器来调制其设置。

由于 ModulatorEnv 以特制对象的形式存储其内容,因此需要知道其类型并将其向下转换才能将调制器借回。假设我们想要通过同一个环境中的另一个调制器 "amp_mod" 来调制我们之前的 "wave_sin" 调制器的幅度

let ampmod = st.m1.value("amp_mod"); // amplitude modulation value
if let Some(sw) = st.m1.get_mut("wave_sin") { // borrow trait object
    if let Some(ss) = sw.as_any().downcast_mut::<Wave>() { // safely cast it
        ss.amplitude = 1.0 + ampmod; // modify its amplitude attribute
    }
}

在这里,我们读取了 "amp_mod" 的当前值,然后我们以可变方式借用了 "wave_sin" 特制对象的引用。 as_any() 方法是 Modulator 特性的一部分,因此所有调制器都必须实现此转换,通常就像这样

fn as_any(&mut self) -> &mut Any {
    self
}

一旦特征对象被转换为一个 Any,我们就使用 downcast_mut 方法来安全地将其转换回其原始类型,当然,这必须已知。在上面的例子中,我们将它转换回 Wave,然后根据当前值调整 "wave_sin" 的振幅。

注意,虽然 ModulatorEnv 类型在许多情况下既方便又有用,但它不是必需的。有无数种替代方法可以托管调制器,包括根本不需要专门的托管器。调制器只需要可访问并且能够适当地更新,而 ModulatorEnv 只是实现这一目标的一种方法。

Modulator 特质的其它方法

除了 value()advance()as_any() 之外,Modulator 包还定义了几个其他方法。这些大多数都是可选的,并且调制器不需要以有意义的方式实现它们。请参阅特质方法以获取详细信息,然后是每个包含的调制器的实现。

最后,注意调制器启用状态方法

/// Check if the modulator is disabled
fn enabled(&self) -> bool;

/// Toggle enabling/disabling the modulator
fn set_enabled(&mut self, enabled: bool);

注意,ModulatorEnv 会检查其调制器的启用状态,如果它们被禁用,则不会更新它们。这允许暂停/恢复调制器。

包含的调制器

sources.rs 中提供了几个调制器。每个都有本地文档,但我们将在此提供摘要。

  1. Wave

使用值闭包/Fn 的简单调制器,具有频率和振幅。闭包接收 self、经过的时间(以秒为单位)并返回一个新值。

  1. ScalarSpring

临界阻尼弹簧调制器。以平滑的秒数延迟向其设置的 goal 移动,临界阻尼其到达,使其在目标处减速并停止,而不会超出或振荡。

如果希望超出,可以设置 undamp 的正值以在目标周围添加人工的超出/振荡。

  1. Newtonian

一个使用经典力学移动到其 goal 的调制器 - 它保证无论设置如何,都会平滑地加速、减速和速度限制。

目标计算计算运动方程的解析解。当设置新的目标时,从各自的范围内选择 speed_limitaccelerationdeceleration 值,然后从当前值开始,以 0 速度加速到所选速率,然后以所选的减速率减速,以确保它在目标处停止。

运动方程的解析解确保无论输入如何,值始终以所选速率加速和减速,并且永远不会超过速度最大值。如果没有足够的时间达到峰值速度,则值将尽可能地加速,同时确保它将减速并在 goal 处精确停止(0 速度)。

  1. ScalarGoalFollower

可编程的目标跟随器。为其拥有的follower调制器在其regions中的一个选择一个goal,然后监控其进度,直到跟随器到达目标距离threshold,并且速度为vel_threshold或更低,此时它认为已经到达。

一旦达到目标,它从pause_range中选取一个暂停持续时间(微秒),等待暂停时间过去,然后选择一个新的目标并重复此过程。

此调制器可以为其拥有的follower指定任何其他调制器类型,但无法追求并到达给定goal的类型,当然,永远无法满足到达的条件。

  1. ShiftRegister

受到经典模拟移位寄存器(如Buchla合成器中使用的)的启发,此调制器有一个包含从value_range中选择的值的值向量buckets

寄存器的period是指值遍历寄存器中所有桶所需的时间(秒)。一旦周期结束,值将回到第一个桶并继续移动。

如果interpShiftRegisterInterp::None,则返回的值对应于正在访问的当前桶。如果是ShiftRegisterInterp::Linear,则它是当前桶和下一个桶的线性插值。如果是ShiftRegisterInterp::Quadratic,则值是先前、当前和下一个桶的值的多项式插值结果。

每次值离开一个桶(它完成了周期内的访问)时,它有odds的机会替换它刚刚离开的桶中的值,其中odds的范围从0.0(值永远不会更改)到1.0(值始终更改)。

参数age_range可用于指定一个年龄(周期),在此期间值更改的概率线性增加。例如:如果将odds设置为0.1(10%),将age_range设置为[200, 1000),则在前200个周期内,值的更改概率为10%,在200到1000个周期之间从10%增加到100%。默认情况下,age_range设置为[u32::MAX, u32::MAX],因此概率永远不会更改。

结果是移位寄存器是周期的,表现出模式(给定足够低的概率),但仍然以有机的方式随时间发展。

版权© 2018-22 Ready At Dawn Studios

依赖关系

~330KB