Imaginantia

思ったことを書きます

頭の振動周期を計算する

VRChatでは頭の位置を気軽に取得できます。そして頭はよく揺れますなんとなくその揺れの周期を計算してみようと思いました。

まず入力データは次の値 headY を使います。

var player = Networking.LocalPlayer;
var head = player.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
float headY = head.position.y;
headY += (head.rotation * Vector3.forward).y * 0.1f;

つまり位置の上下変化に加えて視線方向の上下変化もちょっと入っています。なんとなく。

身長による差異を吸収する為にhighpassもどきに通します。

float dt = Time.deltaTime;
lowpass = Mathf.Lerp(headY, lowpass, Mathf.Exp(-dt*4));
headY -= lowpass;

これで基本的に headY は静止状態では 0 になるはずです。

で、周期を測るわけですが、ここでは「headY が正から負になった瞬間」の時間差を使うことにしました。

if(Mathf.Abs(headY) > 0.001f) waitBorder = true;
bool flash = prevHeadY > 0 && headY < 0 && waitBorder;
passed += dt;
if(flash) {
  // ...
  passed = 0;
  waitBorder = false;
}
prevHeadY = headY;

微振動による反応を防ぐ為に少しくらい揺れてからじゃないと認識しないことになっています。

で、経過時間 passed を使うわけですが、1Fの間のどこで境界を超えたかをできるだけ正確に把握する為に headYprevHeadY を使って補正をします。要は移動速度が一定であると仮定すれば正負の境界を超えた瞬間が計算できるのです。

float ratio = headY / (headY - prevHeadY); // headYが負であることに注意
passed -= ratio * dt;

今回は8回までの記録を残すようにしました。また、後々使うので DateTime.Now.Ticks を使った正確な時間も覚えておきます。

long lastDate = prevDate[0]; // also for later use
int n = 8;
for(int i=0;i<n-1;i++) {
  prevDate[i] = prevDate[i+1];
  prevTime[i] = prevTime[i+1];
}
prevDate[n-1] = DateTime.Now.Tics - (long) (dt * ratio * 10000000);
prevTime[n-1] = passed;
recordCount++;
if(recordCount > 8) recordCount = 8;

今気づいたけど recordCount 使ってないわ。

おおよその周期は平均を取れば計算できると言えますが、まずそれ以前に「周期的かどうか」を知るべきだという話があります。

今回は8個のデータの標準偏差が0.045以下のときに周期的であると判断することにしました。これは実際にやってみて決めた数値です。

float sumTime = 0;
for(int i=0;i<n;i++) {
  sumTime += prevTime[i];
}
float aveTime = sumTime / n;
float sigma = 0.0f;
for(int i=0;i<n;i++) {
  sigma += Mathf.Pow(prevTime[i] - aveTime, 2);
}
sigma = Mathf.Sqrt(sigma / n);

周期的であるときに限って「実際に計算を行って振動数を決定する」という処理をするため、preciseMode という変数で現在の状態を記録しておくことにします。

そして、周期的であるときには「周期性が始まったタイミングから現在までの時間」を「拍数」で割る方が誤差が小さくなっていくと考えられます。というわけで、このときは prevDate を使って時間を計算します。

if(preciseMode) {
  if(sigma > 0.045f) {
    // Reset
    preciseMode = false;
    recordCount = 0;
  } else {
    // Continue to measure
    long passed = prevDate[n-1] - preciseBegin;
    float dur = passed / 10000000.0f;
    preciseCount++;
    dur /= preciseCount;
    float bpm = 60 / dur;
    UseTempo(bpm); // Yes
  }
} else {
  if(sigma < 0.045f) {
    // Start
    preciseMode = true;
    preciseBegin = lastDate;
    preciseCount = n;
  }
}

というわけで無事、頭の振動周期が計算できました。だんだん周期が変化する場合にはあんまりうまく対応出来ていなかったりもするんですが、その場合には無理やり頭の振動をやめることで対応していたりします。

加えて重いとちゃんと精度が出ないという問題があり、70人くらいが視界に何故か入っていると preciseMode に一切入ってくれません。横を向くことで対応しています。

 

はい。

これは私が行った方法なので、さらに精度良く周期を測れる方法が存在しそうな気はします。特にこの辺の分野は何もわからんので。

まぁとりあえず。一例ということで。