ハプティクスフィードバック
Deprecated
バッファー化ハプティクスフィードバックは廃止されており、レガシーのサポートのみを目的として残されています。このAPIは外部トラッキングセンサー付きの初期設定のRiftでのみサポートされていました。非バッファー化ハプティクスフィードバックはRiftおよびRift Sの両方で引き続きサポートされています。Rift Touchコントローラーは、インプット状態の報告の他に、振動によってハプティクスフィードバックを伝えることができます。
SDKでは、次の2種類のハプティクスがサポートされています。
- 非バッファー化ハプティクス – 振動数(Riftでは160Hzまた320Hz、Rift Sでは160Hzまたは500Hz)と振幅(0~255)を指定し、振動のオン/オフを切り替えることでハプティクスをコントロールします。コントローラーは、ハプティクス設定を変更するAPI呼び出しに対する応答に33msを必要とするため、非バッファー化ハプティクスは、厳しい遅延要件がない単純なエフェクトを対象としています。
- バッファー化ハプティクス – 外部トラッキングセンサーを備えた初期Riftでのみサポートされています。このアプローチでは、非バッファーハプティクスよりも利用できるエフェクトの幅が広がります(正弦波や正接関数に関連する振動振幅のパターン化、コントローラー間での振動のパン、さまざまな低周波搬送波の生成など)。バッファー化ハプティクスでは、アプリによって、バイトからなるバッファーが一方または両方のコントローラーに送信され、コントローラーではこれらのバイトが0(振動振幅なし)から255(最大振動振幅)までの範囲の振幅値として解釈されます。次にコントローラーは、バッファー振幅値を320Hz(または3.125msあたり1バイト)で「再生」します。このように、非バッファー化ハプティクスと比較すると振動振幅をより細かく制御できます(ただし、API呼び出時点からバッファーがコントローラー内で再生可能になるまでに約10msを必要とします)。
注: バッファー化機能と非バッファー化機能を一緒に使わないでください。予期しないハプティクスフィードバックの発生につながります。
ovr_SetControllerVibrationを呼び出して振動を有効にできます。
ovr_SetControllerVibration(Hmd, ovrControllerType_LTouch, freq, trigger);
振動を有効にするには振動数を指定します。0.0fを指定すると、160Hzで振動します。1.0fを指定すると、Riftでは320Hz、Rift Sでは500Hzで振動します。
Deprecated
バッファー化ハプティクスフィードバックは廃止されており、レガシーのサポートのみを目的として残されています。このAPIは外部トラッキングセンサー付きの初期設定のRiftでのみサポートされていました。バッファー化ハプティクスは、非バッファー化ハプティクスでは実現できない面白いエフェクトを作成できます。例えば、PC-SDKに付属のバッファー化ハプティクスのサンプルアプリでは、次のエフェクトが作成されます(このアプリについてはこのセクションで詳しく後述します)。
- 各波サイクルの終わりの「バズダウン」エフェクトを含むスムーズな正弦波振動
- パニングサイクルの終わりの「バズダウン」エフェクトを含む、左右のコントローラー間での振動パン
- 超低周波バズ(基本的に64Hzでの一連のティック)
- 三角法の正接波関数を利用するカオス式に基づく「メスアップ」低周波振動
バッファーは、値が0~255の一連のバイトで構成されています。値0は振幅なし(振動なし)、値255はSDKで許容されている振動の最大振幅(強度)を意味します。コードによりバッファー内の値が入力されたら、ovr_SubmitControllerVibrationを使ってバッファーをいずれかまたは両方のTouchコントローラーに送信します。その後、バッファーの各バイトが320Hzの速度で順次「再生」されます。最大バッファーサイズ(コントローラーに一度に送信できる最大バイト数であり、コントローラーの内部バッファーの最大サイズ)は256バイトです。1つの256バイトバッファーの「再生」にかかる時間は0.8秒です(320Hzの速度で256バイトを再生)。振動エフェクトの振幅は、解像度3.125ms(320Hzと同等)まで完全に制御できます。ただし振動数は、320Hzまたは320Hzの整商(320/2=160Hz、320/3=106.7Hz、320/4=80Hz、320/5=64Hzなど)でなければなりません。このような低い振動数を実現するには、ゼロ埋めバイトと、ゼロよりも大きい振幅値のバイトを組み合わせて送信します。以下に例を示します。
- 320Hz、全振幅 - [255, 255, 255, 255, ...]
- 160Hz、全振幅 - [255, 0, 255, 0, 255, 0, 255, 0, ...]
- 320Hz、半振幅 - [127, 127, 127, ..., 127, ...]
- 160Hz、半振幅 - [127, 0, 127, 0, 127, 0, ..., 127, 0, ...]
- シングルシャープティック(320Hz) - [0, 0, 255, 255, 255, 0, 0] [delay x ms] [0, 0, 255, 255, 255, 0, 0]
- シングルブラントティック(160Hz) - [0, 255, 0, 255, 0, 255, 0] [delay x ms] [0, 255, 0, 255, 0, 255, 0]
一般に、軽くシャープなアクションには320Hz共鳴モードを使い、重く鈍いアクションには160Hzモードを使います。
前述の「シャープティック」と「ブラントティック」の例では、1~50の範囲内でパルス計数を変更して、さまざまな長さのエフェクトを試すことができます。また、振幅を変更してさまざまな振動強度を試すこともできます。一般に、振幅と振動数はいずれも無作為(カオス的)に変更できます。また、インプットストリーム(コントローラーの動きや位置、またはVR体験でのその他の状態やイベントなど)に基づいて振動エフェクトを変化させることもできます。
前述のエフェクトタイプ以外にも、次のようなさまざまなエフェクトを実現できる可能性があります。
ハイブリッドティックと変調 - 繰り返しティックと振幅変調セグメントを組み合わせることができます。
複数のインプットストリームのミックス - 複数のインプットストリームを事前にミックスしてから、ハプティクスバッファーAPIに渡すことができます。
注: コントローラー内のハプティクスハードウェアの比較的低速な共鳴減衰が原因で、バッファー内の前のサンプルに基づいて非線形の結果が生じます。例えば、サンプルを50 %の振幅で共振させたいが、前のサンプルの振幅は100 %であった場合、前のサンプルが0 %であった場合とは異なる方法で振幅を駆動する必要があります。オーバードライブアルゴリズムにより応答が改善する可能性があります。ポジティブ(立ち上がり)とネガティブ(減衰)では異なる定数が必要となる可能性があります。基本的なアルゴリズムでは、単にサンプル間デルタが計算に入れられる可能性があります。より高度なアルゴリズムでは、最後のいくつかのサンプルの加重平均を使うことが必要な場合があります。
注: バッファーが(ovr_SubmitControllerVibrationにより)コントローラーに送信されると、バッファー全体が「再生」されます。バッファーがコントローラーに送信された後でバッファーの再生を停止することはできません。したがって、十分に注意して、VRアプリで再生したいコンテンツだけをバッファーに入れてください。
サンプルパイプラインは、ほぼ正しいサイズに保持しておくことが重要です。ハプティクスの振動数が320Hz、アプリのフレームレートが90Hzだとします。この場合、フレーム毎に10サンプル程度のバッファーサイズをターゲットにすることをおすすめします。これにより、非同期割り込みの原因であるバッファーゾーンを保護しながら、フレーム毎に3~4個のハプティクスバイトを再生することができます。再生リストにあるバイト数が多いほど割り込みの可能性は低くなりますが、新しく追加された振動が再生されるまでの遅延が長くなります。
コントローラー内の256バイトの内部バッファーでオーバーフローまたはアンダーフローが発生しないように注意してください。現在(循環)内部バッファーで使用可能なバイト数よりも大きなバッファーを指定してovr_SubmitControllerVibrationを呼び出すと、送信されたバッファー全体が破棄されます。ただし、バイトを送信するときの送信速度が、コントローラー内でのバッファーのバイトの消費に対応できない場合、振動再生でギャップが生じます。
バッファーのステータスを確認するには、ovr_GetControllerVibrationStateを呼び出します。
ovr_GetControllerVibrationState(ovrSession session, ovrControllerType controllerType, ovrHapticsPlaybackState* outState);
バッファーに送信するには、ovr_SubmitControllerVibrationを呼び出します。
ovr_SubmitControllerVibration(ovrSession session, ovrControllerType controllerType, const ovrHapticsBuffer* buffer);
次のコードは、ユーザーの興味を引く面白いエフェクトを実現する方法を示しています。このコードは、Oculus PC-SDKディストリビューションに含まれている基本的なサンプルアプリをVisual Studioプロジェクト{install_folder}\Samples\OculusRoomTiny_Advanced\ORT (Buffered Haptics)に拡張します。このコードをコピーしてプロジェクトに貼り付けることにより、必要に応じてmain.cppを上書きできます。
/// A sample to show vibration generation, using buffered input.
/// Press A to generate a sine wave vibration in the right controller.
/// Press B to generate a 320 Hz vibration that pans from the right controller to the left controller.
/// Press X to generate a low-frequency buzz at 64 hz (320 Hz / 5) in the left controller.
/// Press Y to generate a "messed up" low frequency vibration in the left controller, based on a tangent function.
/// Hold any of the buttons down in order to repeat the pattern continuously.
/// Note: In order to keep the sample minimal, the Touch controller is not graphically displayed within the VR scene.
#include "../Common/Win32_DirectXAppUtil.h" // DirectX
#include "../Common/Win32_BasicVR.h" // Basic VR
struct BufferedHaptics : BasicVR
{
BufferedHaptics(HINSTANCE hinst) : BasicVR(hinst, L"BufferedHaptics") {}
void MainLoop()
{
Layer[0] = new VRLayer(Session);
// We create haptic buffers that will be associated with the A, B, X, and Y buttons.
int bufferSize = 256;
unsigned char * dataBufferA = (unsigned char *)malloc(bufferSize);
unsigned char * dataBufferBRight = (unsigned char *)malloc(bufferSize);
unsigned char * dataBufferBLeft = (unsigned char *)malloc(bufferSize);
unsigned char * dataBufferX = (unsigned char *)malloc(bufferSize);
unsigned char * dataBufferY = (unsigned char *)malloc(bufferSize);
// Verify that the buffer format is what we expect.
ovrTouchHapticsDesc desc = ovr_GetTouchHapticsDesc(Session, ovrControllerType_LTouch);
if (desc.SampleSizeInBytes != 1) FATALERROR("Our assumption of 1 byte per element, is no longer valid");
if (desc.SubmitMaxSamples < bufferSize) FATALERROR("Can't handle this many samples");
desc = ovr_GetTouchHapticsDesc(Session, ovrControllerType_RTouch);
if (desc.SampleSizeInBytes != 1) FATALERROR("Our assumption of 1 byte per element, is no longer valid");
if (desc.SubmitMaxSamples < bufferSize) FATALERROR("Can't handle this many samples");
// Fill dataBufferA with a sine wave amplitude pattern that will smoothly
// rise and fall over a cycle period of 0.8 seconds. The 0.8 value is
// equal to 256/320, where 256 is the number of bytes that we will use
// to define a single sine wave cycle, and the bytes are "played"
// on the controller at 320 Hz. An interesting effect is added by lowering
// the effective frequency in latter half of the cycle by setting
// alternate intensities (amplitudes) to zero. The overall effect is a
// smoothly repeating vibrational pattern that "buzzes down" at the end of
// the wave cycle.
for (int i = 0; i < bufferSize; i++)
{
dataBufferA[i] = (unsigned char)(255.0f*(sin(((3.14159265359f*i) / ((float)bufferSize)))));
if ((i > bufferSize / 2) && (i % 2)) dataBufferA[i] = 0;
}
// Fill dataBufferBRight with values that decrement from 255 down to 0.
// Similarly, fill dataBufferBLeft with values that increment from 0 up to 255.
// An interesting effect is added by lowering the effective frequency in
// latter half of dataBufferBLeft by setting intensities (amplitudes) to zero
// for odd numbered bytes that are not divisible by 3. The overall effect
// is a right-to-left vibrational pan, with a distinct "buzzing down" feeling
// at the end of the panning cycle.
for (int i = 0; i < bufferSize; i++)
{
dataBufferBRight[i] = (unsigned char)(255.0f*(255 - i / ((float)bufferSize)));
dataBufferBLeft[i] = (unsigned char)(255.0f*(i / ((float)bufferSize)));
if ((i > bufferSize / 2) && ((i % 2) || (i % 3))) dataBufferBLeft[i] = 0;
}
// Fill dataBufferX with zeros, and set every fifth byte to 255. This creates
// maximum amplitude ticks at 64 Hz. (This is calculated by dividing
// the number of ticks in the buffer, 256/5 = 51.2, and dividing that
// by the time period over which the buffer is played, 256/320 of a second,
// which is 0.8 seconds.)
for (int i = 0; i < bufferSize; i++)
{
dataBufferX[i] = (unsigned char)0;
if (i % 5 == 0) dataBufferX[i] = 255;
}
// Fill dataBufferY with a tangent wave function that varies the intensity
// amplitude) between 0 and 255, but set every other byte to 0. This produces a
// "messed up" low frequency vibration.
for (int i = 0; i < bufferSize; i++)
{
dataBufferY[i] = (unsigned char)0;
if (i % 2 == 0) dataBufferY[i] = (unsigned char)(255.0f*(tan(((3.14159265359f*i) / ((float)bufferSize)))));
}
// Create the SDK structures that contain the buffers, and prepare
// them to be submitted to the controllers.
ovrHapticsBuffer bufferX;
bufferX.SubmitMode = ovrHapticsBufferSubmit_Enqueue;
bufferX.SamplesCount = bufferSize;
bufferX.Samples = (void *)dataBufferX;
ovrHapticsBuffer bufferY;
bufferY.SubmitMode = ovrHapticsBufferSubmit_Enqueue;
bufferY.SamplesCount = bufferSize;
bufferY.Samples = (void *)dataBufferY;
ovrHapticsBuffer bufferA;
bufferA.SubmitMode = ovrHapticsBufferSubmit_Enqueue;
bufferA.SamplesCount = bufferSize;
bufferA.Samples = (void *)dataBufferA;
ovrHapticsBuffer bufferBRight;
ovrHapticsBuffer bufferBLeft;
bufferBRight.SubmitMode = ovrHapticsBufferSubmit_Enqueue;
bufferBRight.SamplesCount = bufferSize;
bufferBRight.Samples = (void *)dataBufferBRight;
bufferBLeft.SubmitMode = ovrHapticsBufferSubmit_Enqueue;
bufferBLeft.SamplesCount = bufferSize;
bufferBLeft.Samples = (void *)dataBufferBLeft;
// Main Loop
while (HandleMessages())
{
Layer[0]->GetEyePoses();
// Submit the haptic buffers to "play" upon pressing A, B, X or Y buttons.
ovrInputState inputState;
ovr_GetInputState(Session, ovrControllerType_Touch, &inputState);
if (inputState.Buttons & ovrTouch_A)
{
// Only submit the buffer if there is enough space available.
ovrHapticsPlaybackState playbackState;
ovrResult result = ovr_GetControllerVibrationState(Session, ovrControllerType_RTouch, &playbackState);
if (playbackState.RemainingQueueSpace >= bufferSize)
{
ovr_SubmitControllerVibration(Session, ovrControllerType_RTouch, &bufferA);
}
}
if (inputState.Buttons & ovrTouch_B)
{
// Only submit the buffers if there is enough space available.
ovrHapticsPlaybackState playbackState;
ovrResult result = ovr_GetControllerVibrationState(Session, ovrControllerType_LTouch, &playbackState);
if (playbackState.RemainingQueueSpace >= bufferSize)
{
ovr_SubmitControllerVibration(Session, ovrControllerType_LTouch, &bufferBLeft);
}
result = ovr_GetControllerVibrationState(Session, ovrControllerType_RTouch, &playbackState);
if (playbackState.RemainingQueueSpace >= bufferSize)
{
ovr_SubmitControllerVibration(Session, ovrControllerType_RTouch, &bufferBRight);
}
}
if (inputState.Buttons & ovrTouch_X)
{
// Only submit the buffer if there is enough space available.
ovrHapticsPlaybackState playbackState;
ovrResult result = ovr_GetControllerVibrationState(Session, ovrControllerType_LTouch, &playbackState);
if (playbackState.RemainingQueueSpace >= bufferSize)
{
ovr_SubmitControllerVibration(Session, ovrControllerType_LTouch, &bufferX);
}
}
if (inputState.Buttons & ovrTouch_Y)
{
// Only submit the buffer if there is enough space available.
ovrHapticsPlaybackState playbackState;
ovrResult result = ovr_GetControllerVibrationState(Session, ovrControllerType_LTouch, &playbackState);
if (playbackState.RemainingQueueSpace >= bufferSize)
{
ovr_SubmitControllerVibration(Session, ovrControllerType_LTouch, &bufferY);
}
}
// Just render a standard scene in the HMD, to keep the code as simple as possible.
// The controllers are not rendered in the scene.
for (int eye = 0; eye < 2; ++eye)
{
XMMATRIX viewProj = Layer[0]->RenderSceneToEyeBuffer(MainCam, RoomScene, eye);
}
Layer[0]->PrepareLayerHeader();
DistortAndPresent(1);
}
free(dataBufferX);
free(dataBufferY);
free(dataBufferA);
free(dataBufferBLeft);
free(dataBufferBRight);
}
};
};