Skip to content

Instantly share code, notes, and snippets.

@tobyapi
Created November 6, 2025 13:46
Show Gist options
  • Select an option

  • Save tobyapi/ab23ae0e6424ba778661032d1127d4b8 to your computer and use it in GitHub Desktop.

Select an option

Save tobyapi/ab23ae0e6424ba778661032d1127d4b8 to your computer and use it in GitHub Desktop.
1€ Filters for Unity (Vector3 & Quaternion)
using UnityEngine;
// https://gery.casiez.net/1euro/
public sealed class OneEuroFilterVector3
{
readonly float dCutoff;
readonly LowPassFilterVector3 xFilt = new();
readonly LowPassFilterVector3 dxFilt = new();
// micCutoff を減らすとゆっくり動いているときの jitter が減る
float minCutoff;
// beta を増やすと素早く動いているときの lag が減る
float beta;
float tPrev;
bool isFirstTime = true;
public OneEuroFilterVector3(float minCutoff = 1.0f, float beta = 0.0f, float dCutoff = 1.0f)
{
this.minCutoff = minCutoff;
this.beta = beta;
this.dCutoff = dCutoff;
}
public void SetMinCutoff(float minCutoff)
{
this.minCutoff = minCutoff;
}
public void SetBeta(float beta)
{
this.beta = beta;
}
public Vector3 Filter(Vector3 x)
{
float t = Time.time;
float rate = t - tPrev;
if (rate <= 1e-9f)
{
return xFilt.LastValue();
}
tPrev = t;
var dx = isFirstTime switch
{
true => Vector3.zero,
false => (x - xFilt.LastValue()) / rate
};
isFirstTime = false;
var edx = dxFilt.Filter(dx, Alpha(rate, dCutoff));
var cutoff = minCutoff + beta * edx.magnitude;
return xFilt.Filter(x, Alpha(rate, cutoff));
}
float Alpha(float rate, float cutoff)
{
var r = 2.0f * Mathf.PI * cutoff * rate;
return r / (r + 1.0f);
}
}
public sealed class OneEuroFilterQuaternion
{
readonly float dCutoff;
readonly LowPassFilterQuaternion xFilt = new();
readonly LowPassFilterVector3 dxFilt = new();
float minCutoff;
float beta;
float tPrev;
bool isFirstTime = true;
public OneEuroFilterQuaternion(float minCutoff = 1.0f, float beta = 0.0f, float dCutoff = 1.0f)
{
this.minCutoff = minCutoff;
this.beta = beta;
this.dCutoff = dCutoff;
}
public void SetMinCutoff(float minCutoff)
{
this.minCutoff = minCutoff;
}
public void SetBeta(float beta)
{
this.beta = beta;
}
public Quaternion Filter(Quaternion x)
{
float t = Time.time;
float rate = t - tPrev;
if (rate <= 1e-9f)
{
return xFilt.LastValue();
}
tPrev = t;
Vector3 dx;
if (isFirstTime)
{
dx = Vector3.zero;
}
else
{
var deltaQ = x * Quaternion.Inverse(xFilt.LastValue());
deltaQ.ToAngleAxis(out var angle, out var axis);
if (angle > 180f)
{
angle -= 360f;
}
dx = axis * (angle * Mathf.Deg2Rad / rate);
}
isFirstTime = false;
var edx = dxFilt.Filter(dx, Alpha(rate, dCutoff));
var cutoff = minCutoff + beta * edx.magnitude;
return xFilt.Filter(x, Alpha(rate, cutoff));
}
float Alpha(float rate, float cutoff)
{
var r = 2.0f * Mathf.PI * cutoff * rate;
return r / (r + 1.0f);
}
}
sealed class LowPassFilterVector3
{
Vector3 hatx;
bool isFirstTime = true;
public Vector3 LastValue()
{
return hatx;
}
public Vector3 Filter(Vector3 value, float alpha)
{
hatx = isFirstTime switch
{
true => value,
false => alpha * value + (1.0f - alpha) * hatx
};
isFirstTime = false;
return hatx;
}
}
sealed class LowPassFilterQuaternion
{
Quaternion hatx;
bool isFirstTime = true;
public Quaternion LastValue()
{
return hatx;
}
public Quaternion Filter(Quaternion value, float alpha)
{
hatx = isFirstTime switch
{
true => value,
false => Quaternion.Slerp(hatx, value, alpha)
};
isFirstTime = false;
return hatx;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment