By VParamix
ReacTails
Drives nine tail sections from one shared phase and exposes the detected smile.
- Version
- 1.0.0
- Published
- Aug 21, 2026
Face Tracking
ReacTails turns face-tracking expressions into a smooth nine-section tail wave. It accelerates when you smile, slows for a sad expression, and exposes the detected smile as a separate output.
Reference
- Idea and reference: ReacTails for Vbridger by Katomon.
-- ReacTails
-- Drives nine tail sections from one shared phase and exposes the detected smile.
local params = vparamix.params
local face = vparamix.face
-- @inspect min=0 max=2 step=0.01
local normal_speed = 0.20
-- @inspect min=0 max=4 step=0.01
local happy_speed = 0.70
-- @inspect min=0 max=2 step=0.01
local sad_speed = 0.07
-- @inspect min=0 max=90 step=1
local section_delay = 25.0
-- @inspect min=0 max=1.5 step=0.01
local amplitude = 1.0
-- @inspect min=0 max=1 step=0.01
local smile_trigger = 0.55
local function clamp(value, min_value, max_value)
if value < min_value then return min_value end
if value > max_value then return max_value end
return value
end
local output = params.declare({
{ name = "ReacTailSmile", min = 0, max = 1, default = 0 },
{ name = "ReacTailWave1", min = -1, max = 1, default = 0 },
{ name = "ReacTailWave2", min = -1, max = 1, default = 0 },
{ name = "ReacTailWave3", min = -1, max = 1, default = 0 },
{ name = "ReacTailWave4", min = -1, max = 1, default = 0 },
{ name = "ReacTailWave5", min = -1, max = 1, default = 0 },
{ name = "ReacTailWave6", min = -1, max = 1, default = 0 },
{ name = "ReacTailWave7", min = -1, max = 1, default = 0 },
{ name = "ReacTailWave8", min = -1, max = 1, default = 0 },
{ name = "ReacTailWave9", min = -1, max = 1, default = 0 },
})
function update()
local smile = (face.get("MouthSmileLeft") + face.get("MouthSmileRight")) * 0.5
smile = clamp(smile, 0.0, 1.0)
local eye_wide = (face.get("EyeWideLeft") + face.get("EyeWideRight")) * 0.5
local frown = (face.get("MouthFrownLeft") + face.get("MouthFrownRight")) * 0.5
local brow_down = (face.get("BrowDownLeft") + face.get("BrowDownRight")) * 0.5
local eye_blink = (face.get("EyeBlinkLeft") + face.get("EyeBlinkRight")) * 0.5
local eye_relaxed = (1.0 - eye_wide) * 0.5 + eye_blink * 0.5
local sad = (
face.get("BrowInnerUp") * 0.35
+ brow_down * 0.25
+ frown * 0.4
+ eye_relaxed * 0.1
- smile * 0.35
- 0.12
) / 0.85
sad = clamp(sad, 0.0, 1.0)
local happy_target = clamp(eye_wide * 0.15, 0.0, 1.0)
if smile >= smile_trigger then
happy_target = 1.0
end
local happy_blend = load("happy_blend", 0.0)
local blend_speed = happy_target > happy_blend and 2.0 or 3.0
local blend_step = clamp(blend_speed * math.min(dt(), 0.1), 0.0, 1.0)
happy_blend = clamp(
happy_blend + (happy_target - happy_blend) * blend_step,
0.0,
1.0
)
save("happy_blend", happy_blend)
local speed = normal_speed + (happy_speed - normal_speed) * happy_blend
speed = math.max(0.0, speed + (sad_speed - speed) * sad)
local angle = load("angle", 0.0) + speed * 360.0 * math.min(dt(), 0.1)
angle = angle % 360.0
save("angle", angle)
local wave_amplitude = clamp(amplitude, 0.0, 1.5)
local delay = clamp(section_delay, 0.0, 90.0)
local radians = math.pi / 180.0
local function wave(section_index)
return clamp(
math.sin((angle - delay * section_index) * radians) * wave_amplitude,
-1.0,
1.0
)
end
output.set("ReacTailSmile", smile)
output.set("ReacTailWave1", wave(0))
output.set("ReacTailWave2", wave(1))
output.set("ReacTailWave3", wave(2))
output.set("ReacTailWave4", wave(3))
output.set("ReacTailWave5", wave(4))
output.set("ReacTailWave6", wave(5))
output.set("ReacTailWave7", wave(6))
output.set("ReacTailWave8", wave(7))
output.set("ReacTailWave9", wave(8))
end