Drive

Drivers produce a value over time. Build them with MotionFX.Drive, then combine them to shape anything from a single kick to a live binding to game state.

You're at the deepest level. Every primitive and every driver is open here, and nothing is hidden underneath: the Effect builder and the ready-made tracks are made from these same pieces. You can still drop ready-made tracks into any effect where they fit.

local Drive = MotionFX.Drive

A driver is a table { Start: (PlayContext) -> Sampler<T>, Duration: number? }. The sampler is called as sampler(t, dt). t is the number of seconds since the track began and is never negative. dt is the frame step multiplied by the effect's Speed. A Duration of nil means the driver is open-ended.

DriverValueDuration
Constantany Topen-ended
TweenLerpable Tduration
EnvelopenumberAttack + Hold + Release
Noisenumber | Vector3open-ended
Springscalable Tuntil settled
Bindany Topen-ended
CycleLerpable Topen-ended
Modulatecarrier's Tshorter finite one
SumTlongest, if all are finite
SequenceTtotal, if the last one is finite
MapUsame as the source
Which driver for which primitive?

A driver fits a primitive when it produces that primitive's value type. See the table in Which drivers fit, and the recipes under each primitive.

Basic drivers

Drive.Constant

function
Drive.Constant<T>(value: T): Driver<T>

Always returns value. It is open-ended, so the effect runs until stopped unless it has a Duration. Pair it with Modulate to shape a fixed value with an envelope.

{ Primitive = "Vignette", Driver = Drive.Constant(0.65) }

Drive.Tween

function
Drive.Tween<T>(from: T, to: T, duration: number, easing: Easing?): Driver<T>

Animates from from to to over duration seconds, which must be greater than 0, then ends. Works with number, Vector3, Color3, ColorValue and VignetteLayer. The default easing is Quad / Out.

To hold the end value, follow the tween with a Constant in a Sequence, or give the effect a longer Duration.

0.6 s
Drive.Tween(0, 1, 0.6)
Drive.Tween(0, 1, 0.5, { Style = Enum.EasingStyle.Back, Direction = Enum.EasingDirection.Out })

Drive.Envelope

function
Drive.Envelope(config: EnvelopeConfig, peak: number?): Driver<number>

Rises from 0 to peak (default 1), optionally holds, then falls back to 0. Use it on its own for scalar primitives (Blur, FOV, Vignette), or as the second argument to Modulate to give any carrier an attack and a decay.

FieldTypeDescription
Attacknumber?Seconds to reach the peak. Default 0.
Holdnumber?Seconds at the peak. Default 0.
Releasenumber?Seconds to return to 0. Default 0.
EasingEasing?Applied to both the rise and the fall.
AttackHoldRelease
Drive.Envelope({ Attack = 0.15, Hold = 0.3, Release = 0.8 })
-- Blur flashes to 14 and decays over 0.9 s
{ Primitive = "Blur", Driver = Drive.Envelope({ Attack = 0, Release = 0.9 }, 14) }

Drive.Noise

function
Drive.Noise<T>(config: NoiseConfig<T>): Driver<T>

Smooth Perlin noise in [-Amplitude, Amplitude], with each axis independent for Vector3. It is open-ended. Shape it with Modulate(noise, envelope) to get a shake that has an attack and a decay.

FieldTypeDescription
FrequencynumberOscillations per second, roughly.
Amplitudenumber | Vector3Peak value, per axis for a Vector3.
Octavesnumber?Layers of finer detail. Each one has half the amplitude and double the frequency. Default 1.
Seednumber?Defaults to the play's seed (PlayOptions.Seed, or a random one).
1 s2 s
Drive.Noise({ Frequency = 3, Amplitude = 1, Octaves = 2 })
Drive.Noise({ Frequency = 18, Amplitude = Vector3.new(1.2, 1.2, 2.5) })

Drive.Spring

function
Drive.Spring<T>(config: SpringConfig<T>): Driver<T>

A damped spring released from Kick, an initial displacement, that settles back to 0. The spring is solved in closed form, so it gives the same result at any frame rate. It ends once it has settled, which takes about six decay time constants.

FieldTypeDescription
KickTStarting displacement, for example 7 FOV degrees or Vector3.new(3.5, 0, 0) degrees of pitch.
FrequencynumberNatural frequency in Hz. Must be greater than 0.
DampingnumberBelow 1 overshoots and wobbles. 1 is critically damped.
0.5 s1 s
Drive.Spring({ Kick = 1, Frequency = 3, Damping = … })Damping 0.30.61
-- Recoil: pitch up and settle with a little overshoot
Drive.Spring({ Kick = Vector3.new(3.5, 0, 0), Frequency = 9, Damping = 0.4 })

Drive.Bind

function
Drive.Bind<T>(read: (dt: number) -> T): Driver<T>

Calls read(dt) every frame, which permanently ties the primitive to live data such as health, speed, or an event's last value. It is open-ended. The value is still scaled by the effect's intensity and fades.

Drive.Bind(function()
	local speed = root.AssemblyLinearVelocity.Magnitude
	return math.clamp((speed - 30) / 4, 0, 12) -- FOV widens past 30 studs/s
end)

Drive.Cycle

function
Drive.Cycle<T>(values: { T }, period: number, easing: Easing?): Driver<T>

Loops through values forever, easing from each one to the next over period seconds and wrapping from the last back to the first. It needs at least two values. Two values make an oscillation, and more make a palette walk. The default easing is Sine / InOut.

0.5 s1 s1.5 s
Drive.Cycle({ 0, 1 }, 0.5)
Drive.Cycle({ Color3.fromRGB(205, 140, 255), Color3.fromRGB(255, 180, 110), Color3.fromRGB(130, 170, 255) }, 1.6)

Composition

Drive.Modulate

function
Drive.Modulate<T>(carrier: Driver<T>, envelope: Driver<number>): Driver<T>

Multiplies the carrier by the envelope every frame. It ends when the shorter finite driver ends. This is the standard way to build a shake: Noise × Envelope.

0.5 s1 s
Drive.Modulate(Drive.Noise({ Frequency = 14 }), Drive.Envelope({ Attack = 0.03, Release = 0.9 }))envelopenoise × envelope
local envelope = Drive.Envelope({ Attack = 0.02, Release = 0.5 })
Drive.Modulate(Drive.Noise({ Frequency = 18, Amplitude = Vector3.new(1.2, 1.2, 2.5) }), envelope)

Drive.Sum

function
Drive.Sum<T>(...: Driver<T>): Driver<T>

Adds the drivers' values together. It is finite only if every driver is finite, in which case it lasts as long as the longest one.

2 s4 s
Drive.Sum(Drive.Constant(8), Drive.Noise({ Frequency = 0.6, Amplitude = 4 }))Constant(8)sum
-- A base blur of 8 that wobbles by ±4
Drive.Sum(Drive.Constant(8), Drive.Noise({ Frequency = 0.6, Amplitude = 4 }))

Drive.Sequence

function
Drive.Sequence<T>(...: Driver<T>): Driver<T>

Plays the drivers one after another. Only the last one may be open-ended.

second spring
Drive.Sequence(Drive.Spring({ Kick = -2.5, … }), Drive.Spring({ Kick = -1.5, … }))
Drive.Sequence(
	Drive.Spring({ Kick = -2.5, Frequency = 5, Damping = 0.5 }),
	Drive.Spring({ Kick = -1.5, Frequency = 5, Damping = 0.5 })
)

Drive.Map

function
Drive.Map<T, U>(source: Driver<T>, transform: (T) -> U): Driver<U>

Reshapes another driver's values. It has the same duration as the source. Typical uses are turning a Cycle of Color3 into ColorValue tints, or an envelope into a coloured VignetteLayer.

Drive.Map(Drive.Envelope({ Attack = 0.05, Hold = 0.9, Release = 1.4 }), function(amount: number): Types.VignetteLayer
	return { Intensity = amount * 0.7, Color = Color3.fromRGB(120, 0, 0), Inset = 0.3, Size = 0.9 }
end)

Value math

Every driver and channel works through three operations, Scale, Add and Lerp, which support these value types:

TypeScale toward neutralAddLerp
number, Vector3multiplyaddlinear
Color3——Color3:Lerp
ColorValuetint toward white, the rest toward 0tints multiply, the rest addper field
VignetteLayerIntensity onlyIntensities add, the rest comes from the first layerper field, and the Image switches at 0.5
Color3 is lerp-only.

You can Tween or Cycle a Color3, but primitives don't accept one directly. Map it into a ColorValue or a VignetteLayer first.

Custom drivers

Any table with the right shape works as a driver. Start runs once per play, so keep per-play state in locals inside it:

local Types = require(ReplicatedStorage.MotionFX.Types)

local function Pulse(rate: number, amount: number): Types.Driver<number>
	return {
		Duration = nil, -- open-ended
		Start = function(context)
			local phase = (context.Seed % 100) / 100 * math.pi * 2
			return function(t, _dt)
				return amount * (0.5 + 0.5 * math.sin(t * rate * math.pi * 2 + phase))
			end
		end,
	}
end