I came up with this after a bit of work. The beauty of this solution is that it is hardware agnostic, so I can very easily support Oculus Touch, HTC Vive, Leap Motion, and whatever new motion controllers Valve eventually releases. This implementation I came up with is designed to be future proof. Here is a short video of the final result working with Oculus Touch:
I decided to decouple my VR hardware from characters. I think I may one of the few people who did this, but it is one of the best software engineering decisions I’ve made so far, and the results speak for themselves. I’m not going to go into details on how I did that (since its not the topic here). I think at this stage in the VR industry, there isn’t going to be much, if any documentation or tutorials on how people set this up because most people are still figuring it out for themselves. I’ll share a lot of my hand implementation so you can use it as inspiration for your own solution. Maybe someone will improve upon it or point out some limitations
The basic gist of my approach is to have a skeletal mesh animation with bones for each finger, and then I manually set the bone rotations for each finger based on various states of the VR input hardware. I do this within the animation blueprint for the controlled character. Here is the most important bit from my animation blueprint which modifies per-finger bone rotations. This is for the left pointer finger.
I collect the input from hardware devices such as motion controllers. When the player pulls the trigger button, the character grips their hand into a fist. Here’s how I do it:
It may not be super obvious from that image, but I have a custom “Input” interface I created which invokes interface calls on whatever character is being controlled. Here, the hardware inputs generated a “grab” event. I send an interface call to whatever creature I’m controlling and it may have its own handling logic for how to process any grab events. Then, I set all fingers in the hand to their maximum curl value. I use a “desired curl”, which means that the hand is going to try to move the finger curl values to the desired value over time. This is done within the creature tick function and the end result is a gradual curling and uncurling of fingers over time. Fingers could curl around the geometery of a held object such as a soda bottle without too much extra coding.
Some devices such as the Oculus Touch have capacitive touch. That means if your finger is even resting on a button, you get some input data which lets you respond to those events. In effect, I created finger pointing, thumbs up, and a combination of both based on various finger touching configurations. This implementation really isn’t too hardware specific, so if there was ever a second hardware motion controller out on the market (
ahem valve’s new one), then it’s relatively easy and straight forward to add in support with minimal refactoring.
A lot of the magic happens in my hand data structures within code. Here are the relevant structs and classes which represent a player hand. Look through these and read the comments. If you have any questions, let me know.
USTRUCT(BlueprintType)
struct FBoneXForm
{
GENERATED_USTRUCT_BODY()
//world space position of the bone
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Bone")
FVector Position;
//This is the minimum rotational constraint value of this socket bone. This is where we are at when "curl == 0".
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Bone")
FRotator MinimumConstraint;
//This is the maximum rotational constraint value for this socket bone. This is where we are at when "curl == 1".
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Bone")
FRotator MaximumConstraint;
//This is the current curl of a finger. Range: 0.0 -> 1.0
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Bone", meta = (UIMin = "0", UIMax = "1", ClampMin = "0", ClampMax = "1"))
float Curl = 0.0f;
FORCEINLINE FBoneXForm();
FORCEINLINE FBoneXForm(FVector Pos, FRotator MinRot, FRotator MaxRot);
};
FBoneXForm::FBoneXForm()
{
Position = FVector::ZeroVector;
MinimumConstraint = FRotator::ZeroRotator;
MaximumConstraint = FRotator::ZeroRotator;
}
FBoneXForm::FBoneXForm(FVector Pos, FRotator MinRot, FRotator MaxRot)
{
Position = Pos;
MinimumConstraint = MinRot;
MaximumConstraint = MaxRot;
//default to a slightly cupped hand so that any capacitive touch code can be visually obvious
//CurMinCurl = 0.2f;
Curl = 0.2f;
//DesiredCurl = 0.2f;
}
USTRUCT(BlueprintType)
struct FPlayerFinger
{
GENERATED_USTRUCT_BODY()
//3 bones for the finger
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger")
TArray<FBoneXForm> Bone;
//whether the finger is touching the controller. By default, it's touching the controller.
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger")
float CapTouch = 1.0;
//How much the finger is curled.
//0 = fully opened;
//1 = fully closed;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger", meta = (UIMin = "0", UIMax = "1", ClampMin = "0", ClampMax = "1"))
float Curl = 0.0f;
//This is the current minimum curl for a finger when we have capacitive touch.
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger", meta = (UIMin = "0", UIMax = "1", ClampMin = "0", ClampMax = "1"))
float CurMinCurl = 0.0f;
//This is the value which "curl" is trying to reach over time
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger", meta = (UIMin = "0", UIMax = "1", ClampMin = "0", ClampMax = "1"))
float DesiredCurl = 0.0f;
FORCEINLINE FPlayerFinger();
};
FPlayerFinger::FPlayerFinger()
{
Bone.SetNumZeroed(3, false);
Bone[0] = FBoneXForm(FVector(0, 0, 0), FRotator(0, -20, 0), FRotator(0, 90, 0));
Bone[1] = FBoneXForm(FVector(0, 0, 0), FRotator(0, -10, 0), FRotator(0, 110, 0));
Bone[2] = FBoneXForm(FVector(0, 0, 0), FRotator(0, -10, 0), FRotator(0, 30, 0));
Bone[0].Curl = Curl;
Bone[1].Curl = Curl;
Bone[2].Curl = Curl;
}
USTRUCT(BlueprintType)
struct FPlayerHand
{
GENERATED_USTRUCT_BODY()
//Whether or not this hand was tracked
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand")
bool Tracked = false;
//This is how much vertex weight we give to the prebaked animation vertices vs. hardware vertices.
//0 = full anim weight
//1 = full hardware weight
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand", meta = (UIMin = "0", UIMax = "1", ClampMin = "0", ClampMax = "1"))
float AnimWeight;
//This is where we want to move our anim weight to over time
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand", meta = (UIMin = "0", UIMax = "1", ClampMin = "0", ClampMax = "1"))
float DesiredAnimWeight;
//A normalized value which indicates the blend weight of a hand between fully open hand and fully closed hand.
//This is used for non-leap motion, per-finger bone rotation values.
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand")
float Curl = 0.2f;
//This is the elbow position
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Socket")
FTransform Elbow;
//This is the pivot point for the whole hand
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Socket")
FTransform WristBone;
//the center position of the palm
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Socket")
FTransform Palm;
//3 bones for the thumb
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger")
FPlayerFinger Thumb;
//3 bones for the pointer finger
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger")
FPlayerFinger Pointer;
//3 bones for the middle finger
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger")
FPlayerFinger Middle;
//3 bones for the ring finger
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger")
FPlayerFinger Ring;
//3 bones for the pinky finger
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Finger")
FPlayerFinger Pinky;
FORCEINLINE FPlayerHand();
};
FPlayerHand::FPlayerHand()
{
Palm = FTransform(FRotator(5, 0, 0), FVector(4.5, 0, 0));
Thumb.Bone[0] = FBoneXForm(FVector(3.1f, 0, 0), FRotator(0, -20, 30), FRotator(0, 20, 0));
Thumb.Bone[1] = FBoneXForm(FVector(3.2f, 0, 0), FRotator(0, 0, 0), FRotator(0, 15, 0));
Thumb.Bone[2] = FBoneXForm(FVector(3.2f, 0, 0), FRotator(0, 0, 0), FRotator(0, 60, 0));
Pointer.Bone[0] = FBoneXForm(FVector(9.61f, 0, 0), FRotator(0, -20, 0), FRotator(0, 90, 0));
Pointer.Bone[1] = FBoneXForm(FVector(3.25f, 0, 0), FRotator(0, -10, 0), FRotator(0, 110, 0));
Pointer.Bone[2] = FBoneXForm(FVector(2.28f, 0, 0), FRotator(0, -10, 0), FRotator(0, 30, 0));
Middle.Bone[0] = FBoneXForm(FVector(9.28f, 0, 0), FRotator(0, -20, 0), FRotator(0, 90, 0));
Middle.Bone[1] = FBoneXForm(FVector(3.8f, 0, 0), FRotator(0, -10, 0), FRotator(0, 110, 0));
Middle.Bone[2] = FBoneXForm(FVector(2.8f, 0, 0), FRotator(0, -10, 0), FRotator(0, 30, 0));
Ring.Bone[0] = FBoneXForm(FVector(8.5f, 0, 0), FRotator(0, -20, 0), FRotator(0, 90, 0));
Ring.Bone[1] = FBoneXForm(FVector(3.18f, 0, 0), FRotator(0, -10, 0), FRotator(0, 110, 0));
Ring.Bone[2] = FBoneXForm(FVector(2.57f, 0, 0), FRotator(0, -10, 0), FRotator(0, 30, 0));
Pinky.Bone[0] = FBoneXForm(FVector(7.5f, 0, 0), FRotator(0, -20, 0), FRotator(0, 90, 0));
Pinky.Bone[1] = FBoneXForm(FVector(3.06f, 0, 0), FRotator(0, -10, 0), FRotator(0, 110, 0));
Pinky.Bone[2] = FBoneXForm(FVector(1.68f, 0, 0), FRotator(0, -10, 0), FRotator(0, 30, 0));
}
I would tentatively say that this isn’t going to be something anyone can just easily copy/paste and have running in a few hours. Expect a few days/weeks of planning, design, implementation and testing. I hope what I’ve shared helps accelerate your dev cycles or gives people a solid head start on a direction.