【转载】UE4[C++] ——IK 的 实现方法

595 阅读5分钟

原文链接:UE4[C++]IK(逆向运动学)的实现方法 | 大侠刘茗

首先给出 FK 和 IK 的概念

  • FK(Forward kinematics)正向运动学,通俗来说是给定父骨骼位置以及它的变换来得出子骨骼的位置以及变换。
  • IK(Inverse kinematics)逆向运动学,IK 和 FK 恰好相反,即给出子骨骼位置,从而逆向推出骨骼节点链上父级节点的位置。

举个例子

手臂的旋转属于 FK 的变换,而使用手去触碰物体导致的手臂及躯干的移动属于 IK 的变换。

本文中,IK 的实现通过 C++ 代码动画蓝图 实现。

代码部分

(1)继承 UAnimInstance 类,新建 UIKAnimInstance 类。

这里声明的变量用于数据更新

UIKAnimInstance.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "IKAnimInstance.generated.h"

/**
 * 
 */
UCLASS(transient, Blueprintable, hideCategories = AnimInstance, BlueprintType)
class IKSYSTEM_API UIKAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere,BlueprintReadWrite)
		FVector  LeftJointLoc = FVector(-15.97f, 1000.0f, -15.97f);
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FVector  RightJointLoc = FVector(-16.8f, 26.9, 15.97f);
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		float LeftEffectorLoc = 0.0f;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		float RightEffectorLoc = 0.0f;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		float HipsOffset = 0.0f;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FRotator LeftFootRotation = FRotator(0.f,0.f,0.f);
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FRotator RightFootRotation = FRotator(0.f, 0.f, 0.f);
	UIKAnimInstance(const FObjectInitializer& ObjectInitializer);
};

(2)继承 Character 类,新建类 AIKCharacter

AIKCharacter.h

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Engine/SkeletalMeshSocket.h"
#include "Components/SkeletalMeshComponent.h"
#include "Kismet/KismetMathLibrary.h"
#include "Kismet/KismetSystemLibrary.h"
#include "IKAnimInstance.h"
#include "IKCharacter.generated.h"

UCLASS(config=Game)
class AIKCharacter : public ACharacter
{
	GENERATED_BODY()

	/** Camera boom positioning the camera behind the character */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class USpringArmComponent* CameraBoom;

	/** Follow camera */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class UCameraComponent* FollowCamera;
public:
	AIKCharacter();

	virtual void Tick(float DeltaSeconds) override;

	/** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
	float BaseTurnRate;

	/** Base look up/down rate, in deg/sec. Other scaling may affect final rate. */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
	float BaseLookUpRate;

	float IKCapsuleHalfHeight = 0.f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = IK)
		float IKTraceDistance = 55.0f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = IK)
		float IKAdjustOffset = 2.0f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = IK)
		FName LeftFootSocket = "LeftFootSocket";

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = IK)
		FName RightFootSocket = "RightFootSocket";

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = IK)
		float IKHIspsInterpSpeed = 7.0f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = IK)
		float IKFeetInterpSpeed = 13.0f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = IK)
		float IKTimeout = 0.2f;

	UIKAnimInstance* AnimInstance ;

	FVector Impact = FVector(0.f, 0.f, 0.f);

	float Scale = 0.0f;
protected:
	/** Called for forwards/backward input */
	void MoveForward(float Value);

	/** Called for side to side input */
	void MoveRight(float Value);

	/** 
	 * Called via input to turn at a given rate. 
	 * @param Rate	This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
	 */
	void TurnAtRate(float Rate);

	/**
	 * Called via input to turn look up/down at a given rate. 
	 * @param Rate	This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
	 */
	void LookUpAtRate(float Rate);

	/** Handler for when a touch input begins. */
	void TouchStarted(ETouchIndex::Type FingerIndex, FVector Location);

	/** Handler for when a touch input stops. */
	void TouchStopped(ETouchIndex::Type FingerIndex, FVector Location);

	float IKFootTrace(float TraceDistance, FName SocketName);

	void UpdateCapsuleHalfHeight(float HipShifts, bool ResetDefault);

	UFUNCTION(BlueprintCallable,BlueprintPure,category="IK")
	FRotator NormalToRotator(FVector Normal);

	void IKUpdateFootOffset(float TargetValue, float &EffectorVal, float InterpSpeed);

	void IKUpdateFootRotation(FRotator TargetValue, FRotator &RotationVar, float InterpSpeed);

	void IKResetVars();

	bool IsMoving();

	UFUNCTION(BlueprintCallable, category = "IK")
	void IKUpdate(bool bEnable);

	USkeletalMeshComponent* Mesh = nullptr;
protected:
	// APawn interface
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	// End of APawn interface

public:
	/** Returns CameraBoom subobject **/
	FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
	/** Returns FollowCamera subobject **/
	FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};

AIKCharacter.cpp

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.

#include "IKCharacter.h"
#include "HeadMountedDisplayFunctionLibrary.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "DrawDebugHelpers.h"
#include "Components/SceneComponent.h"
#include "Components/ArrowComponent.h"
#include "GameFramework/SpringArmComponent.h"

//////////////////////////////////////////////////////////////////////////
// AIKCharacter

AIKCharacter::AIKCharacter()
{
	// Set size for collision capsule
	GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

	// set our turn rates for input
	BaseTurnRate = 45.f;
	BaseLookUpRate = 45.f;

	// Don't rotate when the controller rotates. Let that just affect the camera.
	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	// Configure character movement
	GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input...	
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f); // ...at this rotation rate
	GetCharacterMovement()->JumpZVelocity = 600.f;
	GetCharacterMovement()->AirControl = 0.2f;

	// Create a camera boom (pulls in towards the player if there is a collision)
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->SetupAttachment(RootComponent);
	CameraBoom->TargetArmLength = 300.0f; // The camera follows at this distance behind the character	
	CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller

	// Create a follow camera
	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
	FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation
	FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm

	// Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character) 
	// are set in the derived blueprint asset named MyCharacter (to avoid direct content references in C++)

	//Get Capsule Half height
	IKCapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();

	Mesh = GetMesh();
	if (!Mesh)
		return;
	PrimaryActorTick.bCanEverTick = true;
	PrimaryActorTick.bStartWithTickEnabled = true;
	
	Scale = GetActorScale().Z;
	
}

void AIKCharacter::Tick(float DeltaSeconds)
{
	/*if (Mesh->GetAnimInstance() == nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("Get AnimInstance Failed!"));
		return;
	}*/
	AnimInstance = Cast<UIKAnimInstance>(Mesh->GetAnimInstance());
	if (AnimInstance == nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("Tick Misssing AnimInstance!"));
		return;
	}

	IKUpdate(true);
}

//////////////////////////////////////////////////////////////////////////
// Input

void AIKCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
	// Set up gameplay key bindings
	check(PlayerInputComponent);
	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
	PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);

	PlayerInputComponent->BindAxis("MoveForward", this, &AIKCharacter::MoveForward);
	PlayerInputComponent->BindAxis("MoveRight", this, &AIKCharacter::MoveRight);

	// We have 2 versions of the rotation bindings to handle different kinds of devices differently
	// "turn" handles devices that provide an absolute delta, such as a mouse.
	// "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick
	PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
	PlayerInputComponent->BindAxis("TurnRate", this, &AIKCharacter::TurnAtRate);
	PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
	PlayerInputComponent->BindAxis("LookUpRate", this, &AIKCharacter::LookUpAtRate);

	// handle touch devices
	PlayerInputComponent->BindTouch(IE_Pressed, this, &AIKCharacter::TouchStarted);
	PlayerInputComponent->BindTouch(IE_Released, this, &AIKCharacter::TouchStopped);

}

void AIKCharacter::TouchStarted(ETouchIndex::Type FingerIndex, FVector Location)
{
		Jump();
}

void AIKCharacter::TouchStopped(ETouchIndex::Type FingerIndex, FVector Location)
{
		StopJumping();
}

float AIKCharacter::IKFootTrace(float TraceDistance, FName SocketName)
{
	bool ValidHit = false;
	FHitResult OutHit;
	float Offset = 0.f;
	FVector SocketVec = Mesh->GetSocketLocation(SocketName);
	FVector ActorVec = GetActorLocation();
	FVector StartVec = FVector(SocketVec.X, SocketVec.Y, ActorVec.Z);
	FVector EndVec = FVector(SocketVec.X, SocketVec.Y, ActorVec.Z - IKCapsuleHalfHeight - TraceDistance);

	
	FCollisionQueryParams RV_TraceParams = FCollisionQueryParams(FName(TEXT("RV_Trace")), true, this);
	RV_TraceParams.bTraceComplex = true;
	RV_TraceParams.bTraceAsyncScene = true;
	RV_TraceParams.bReturnPhysicalMaterial = false;

	ValidHit = UKismetSystemLibrary::LineTraceSingle(
		GetWorld(),
		StartVec,
		EndVec,
		ETraceTypeQuery::TraceTypeQuery1,
		true,
		TArray<AActor*>(),
		EDrawDebugTrace::None,
		OutHit,
		true,
		FLinearColor::Yellow
	);
	if (ValidHit)
	{
		FVector HitVector = OutHit.Location - OutHit.TraceEnd;

		//UKismetSystemLibrary::PrintString(GetWorld(), HitVector.ToString());

		Offset = IKAdjustOffset + HitVector.Size() - TraceDistance;
		Offset = UKismetMathLibrary::SelectFloat(Offset, 0.f, ValidHit);		
	}

	Impact = OutHit.Normal;
	return Offset;
}

void AIKCharacter::UpdateCapsuleHalfHeight(float HipShifts, bool ResetDefault)
{
	float fShift = IKCapsuleHalfHeight - (FMath::Abs(HipShifts) / 2);
	float fTarget = UKismetMathLibrary::SelectFloat(IKCapsuleHalfHeight, fShift, ResetDefault);
	float finterp = FMath::FInterpTo(
		IKCapsuleHalfHeight,
		fTarget,
		GetWorld()->GetDeltaSeconds(),
		IKHIspsInterpSpeed
	);
	GetCapsuleComponent()->SetCapsuleHalfHeight(finterp, true);

}

FRotator AIKCharacter::NormalToRotator(FVector Normal)
{

	float XRoll =FMath::RadiansToDegrees(FMath::Atan2(Normal.Y, Normal.Z));
	float YPitch = FMath::RadiansToDegrees(FMath::Atan2(Normal.X, Normal.Z) * (-1));
	FRotator Rot = FRotator(YPitch, 0.f, XRoll);
	UE_LOG(LogTemp, Warning, TEXT("%f , %f , %f"), &Rot.Roll, &Rot.Pitch, &Rot.Yaw);
	return Rot;
}

void AIKCharacter::IKUpdateFootOffset(float TargetValue, float &EffectorVal, float InterpSpeed)
{
	float fInterp = FMath::FInterpTo(EffectorVal, TargetValue, GetWorld()->GetDeltaSeconds(), InterpSpeed);
	EffectorVal = fInterp;
}

void AIKCharacter::IKUpdateFootRotation(FRotator TargetValue, FRotator &RotationVar, float InterpSpeed)
{
	FRotator rInterp = FMath::RInterpTo(RotationVar, TargetValue, GetWorld()->GetDeltaSeconds(), InterpSpeed);
	RotationVar = rInterp;
}

void AIKCharacter::IKResetVars()
{
	/*Reset Foot Loction*/
	IKUpdateFootOffset(0.0f, AnimInstance->RightEffectorLoc, IKFeetInterpSpeed);
	IKUpdateFootOffset(0.0f, AnimInstance->LeftEffectorLoc, IKFeetInterpSpeed);
	/*Reset Foot Rotation*/
	IKUpdateFootRotation(FRotator(0.f, 0.f, 0.f), AnimInstance->RightFootRotation, IKFeetInterpSpeed);
	IKUpdateFootRotation(FRotator(0.f, 0.f, 0.f), AnimInstance->LeftFootRotation, IKFeetInterpSpeed);
	/*Reste Hips Loction*/
	IKUpdateFootOffset(0.0f, AnimInstance->HipsOffset, IKHIspsInterpSpeed);
	UpdateCapsuleHalfHeight(0.0f, true);
}

bool AIKCharacter::IsMoving()
{
	if (GetVelocity().Size() > 0)
		return true;
	return false;
}

void AIKCharacter::IKUpdate(bool bEnable)
{
	if (bEnable == false)
	{
		return;
	}
	if (AnimInstance == nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("IKUpdate Misssing AnimInstance!"));
		return;
	}
	/*Trace Foot offset*/
	float LeftFootOffset = IKFootTrace(IKTraceDistance, LeftFootSocket);
	FRotator Normal = NormalToRotator(Impact);
	IKUpdateFootRotation(Normal, AnimInstance->LeftFootRotation, IKFeetInterpSpeed);
	//UKismetSystemLibrary::PrintString(GetWorld(),FString::Printf(TEXT("Rotation is %f ,%f ,%f"),GetActorRotation().Pitch , GetActorRotation().Roll, GetActorRotation().Yaw), 4.0f);
	float RightFootOffset = IKFootTrace(IKTraceDistance, RightFootSocket);
	Normal = NormalToRotator(Impact);
	IKUpdateFootRotation(Normal, AnimInstance->RightFootRotation, IKFeetInterpSpeed);

	/*Update Hip translation*/
	bool pickA = FMath::Min(LeftFootOffset, RightFootOffset) < 0;
	float HipOffset = UKismetMathLibrary::SelectFloat(FMath::Min(LeftFootOffset, RightFootOffset), 0.f, pickA);
	IKUpdateFootOffset(HipOffset, AnimInstance->HipsOffset, IKHIspsInterpSpeed);
	UpdateCapsuleHalfHeight(HipOffset, false);

	/*Update Foot Locations*/
	IKUpdateFootOffset(LeftFootOffset - HipOffset , AnimInstance->LeftEffectorLoc, IKFeetInterpSpeed);
	IKUpdateFootOffset(RightFootOffset - HipOffset, AnimInstance->RightEffectorLoc, IKFeetInterpSpeed);

}

void AIKCharacter::TurnAtRate(float Rate)
{
	// calculate delta for this frame from the rate information
	AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds());
}

void AIKCharacter::LookUpAtRate(float Rate)
{
	// calculate delta for this frame from the rate information
	AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds());
}

void AIKCharacter::MoveForward(float Value)
{
	if ((Controller != NULL) && (Value != 0.0f))
	{
		// find out which way is forward
		const FRotator Rotation = Controller->GetControlRotation();
		const FRotator YawRotation(0, Rotation.Yaw, 0);

		// get forward vector
		const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
		AddMovementInput(Direction, Value);
	}
}

void AIKCharacter::MoveRight(float Value)
{
	if ( (Controller != NULL) && (Value != 0.0f) )
	{
		// find out which way is right
		const FRotator Rotation = Controller->GetControlRotation();
		const FRotator YawRotation(0, Rotation.Yaw, 0);
	
		// get right vector 
		const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
		// add movement in that direction
		AddMovementInput(Direction, Value);
	}
}

蓝图部分

本示例中使用的是官方的第三人称模板。

编译代码,将模板中的蓝图 ThirdPersonCharacter 的基类选择刚才编译的 AIKCharacter

打开 ThirdPersonCharacter 使用的动画蓝图 ThirdPerson_AnimBP ,将基类改为 UIKAnimInstance

Event Graph 中,使用模板中的蓝图

Anim Graph 中,使用连接如下节点

Transform Modify 节点如下配置

Two Bone IK 节点:

最后把 Cached Pose 连接至最终 Pose

大功告成! 现在看看效果吧:

觉得本文有帮助的朋友们请去原文点赞