A core tool for both Animators and Riggers is Constraints. Now Unreal does provide “Attachments” which essentially act as animatable re-parenting, as great as that is it is hard to set up and not very flexible.

My first approach to this was to create an interface around the existing Attachment system, but I ran into a few snags.
1. Attachments can only have one target
2. Attachments do not work on ControlRig Controls.
So I either needed to update the Engine to suit my requirements, or create a new component type. I spoke with some developers and animators and came to the conclusion that developers would prefer I updated the engine, and animators would prefer I created a re-usable component type.
Since a component is less effort and preffered by the intended users, I will go with that for now.

As always, I split my requirements into “Minimal” and “Production-Ready” to ensure I don’t get off track.

Minimal Requirements:

  • Common Constraint types (Point, Orient, Parent, Aim)
  • Be able to filter which channels are affected
  • Be configurable after creation
  • Work on both Actors and Controls
  • Be able to create based on selection
  • Default to a user friendly name
  • Be animatable
  • Provide an additional offset control to tweak constraint axis

Production-Ready Requirements

  • Support blending between multiple targets per constraint
  • Support multiple constraints driving a single actor/control
  • Dynamic property editor display based on current constraint type

Implementation:

There are many ways we could create and display constraint information, for now I’ve gone with my gut as we can change this after a round of feedback from animators.

Evaluation Structure:
I want to structure this by blending all target constraints together, then filtering which channels it affects at the end, this reduces the complexity at cost of flexibility. We could add your filters to each target but it will make for a very cluttered properties panel.
I also know that I’ll need an enum to change the constraint type, and another enum for aim axis for aim constraints.
Since we also have an offset control, we can simplify these axis’s to X, Y, Z and let the animator adjust the offset for other directions.

Enums:
These may not be the best names, but they work for now.

UENUM()
enum class EConstraintComponentType : uint8
{
	Disabled,
	SnapToTarget,
	AimAtTarget
};

UENUM()
enum class EConstraintComponentAxis : uint8
{
	X,
	Y,
	Z,
};

UENUM()
enum class EConstraintComponentAimAxis : uint8
{
	LocalX,
	LocalY,
	LocalZ,
	TargetX,
	TargetY,
	TargetZ,
};

Target Struct:
Each target has it’s own weight for blending, and if it’s a control we need to include the control name.
For ControlRigs it’s important we store the actor and not the rig otherwise it will crash when the scene re-opens.

USTRUCT(BlueprintType, Blueprintable)
struct FConstraintComponentTarget
{
    GENERATED_BODY()
		
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
    AActor* TargetActor;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FName ControlName;
	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, meta=(ClampMin=0.0f, ClampMax=1.0f))
	float Weight = 1.0f;
};

Class Header:
The constraint is fairly simple, evaluation is done in TickComponent, I have also exposed all properties as public so we can interface with them from a UI.
As with the target struct if we are applying the constraint to a controlrig control we need to store the name.
By adding “Interp” to my properties it provides a convenient button to set a keyframe, unless there is a good reason not to make something animatable I recommend adding this to everything.

UCLASS(hidecategories = (Object, LOD, Physics, Collision), EditInlineNew, meta = (BlueprintSpawnableComponent), ClassGroup = Utility)
class ANIMTOOLKIT_API UConstraintComponent : public UActorComponent
{
	GENERATED_BODY()
public:
    UConstraintComponent( const FObjectInitializer& PCIP );
    
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

public:
	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Common)
	FTransform Offset;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category=Common)
    EConstraintComponentType ConstraintType;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Common, DisplayName="Apply To Control")
    FName ControlName;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Common)
    TArray<FConstraintComponentTarget> Targets;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category=Common)
    EConstraintComponentAxis AimAxis = EConstraintComponentAxis::X;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category=Common)
    EConstraintComponentAimAxis AimUpAxis = EConstraintComponentAimAxis::LocalZ;

	
	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Translate, DisplayName="Constrain X")
	bool TranslateConstrainX=true;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Translate, DisplayName="Constrain Y")
	bool TranslateConstrainY=true;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Translate, DisplayName="Constrain Z")
	bool TranslateConstrainZ=true;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Rotate, DisplayName="Constrain X")
	bool RotateConstrainX=true;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Rotate, DisplayName="Constrain Y")
	bool RotateConstrainY=true;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Rotate, DisplayName="Constrain Z")
	bool RotateConstrainZ=true;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Scale, DisplayName="Constrain X")
	bool ScaleConstrainX=true;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Scale, DisplayName="Constrain Y")
	bool ScaleConstrainY=true;

	UPROPERTY(Interp, EditAnywhere, BlueprintReadWrite, Category = Scale, DisplayName="Constrain Z")
	bool ScaleConstrainZ=true;

private:
	void ApplyTransform( FTransform& Output, const FTransform& Transform, float Blend );
	FTransform FilterTransform(const FTransform& Base, const FTransform& Transform);
};

Class Implementation:
First things first, instruct the class to tick in edit mode, this adds scene overhead but is fast enough for now.

UConstraintComponent::UConstraintComponent( const FObjectInitializer& PCIP)
	: Super(PCIP)
{
	PrimaryComponentTick.bCanEverTick = true;
	bTickInEditor = true;
	bAutoActivate = true;

}

Before we cover evaluation, let’s look at the methods ApplyTransform and FilterTransform.

ApplyTransform is used to blend a new transform over the current one, this is an easy way to have infinite targets without needing to micromanage blend values. I can tell you from experience animators do not like this way of blending, but it’s the easiest way to set it up and saves us micromanaging the data.

void UConstraintComponent::ApplyTransform( FTransform& Output, const FTransform& Transform, float Blend )
{
    FTransform Current(Output);
    Output = FilterTransform(Output, Transform);
    if (Blend < 1.0)
        Output.BlendWith(Current, 1.0 - Blend);
}

FilterTransform is simply used to only apply transforms based on what filters the animator has selected. This gives them control for PointConstraint to one target but AimConstraint at another

FTransform UConstraintComponent::FilterTransform( const FTransform& Base, const FTransform& Transform )
{

    FVector Translation = Base.GetTranslation();
    FVector TargetTranslation = Transform.GetTranslation();
    if ( TranslateConstrainX )
        Translation.X = TargetTranslation.X;
    if ( TranslateConstrainY )
        Translation.Y = TargetTranslation.Y;
    if ( TranslateConstrainZ )
        Translation.Z = TargetTranslation.Z;
    
    FVector Rotation = Base.GetRotation().Euler();
    FVector TargetRotation = Transform.GetRotation().Euler();
    if (RotateConstrainX)
        Rotation.X = TargetRotation.X;
    if (RotateConstrainY)
        Rotation.Y = TargetRotation.Y;
    if (RotateConstrainZ)
        Rotation.Z = TargetRotation.Z;

    FVector Scale = Base.GetScale3D();
    FVector TargetScale = Transform.GetScale3D();
    if (ScaleConstrainX)
        Scale.X = TargetScale.X;
    if (ScaleConstrainY)
        Scale.Y = TargetScale.Y;
    if (ScaleConstrainZ)
        Scale.Z = TargetScale.Z;
    
    FTransform Output;
    Output.SetTranslation(Translation);
    Output.SetRotation(FQuat::MakeFromEuler(Rotation));
    Output.SetScale3D(Scale);
    return Output;

}

For the tick component there is a lot going on, so I’ve commented inline but essentially we multiply the targets together to get an evaluation result, filter it based on input and apply it back on the source root component or control.

void UConstraintComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent( DeltaTime, TickType, ThisTickFunction);
    // If we are disabled or have no targets bail early
    if (ConstraintType == EConstraintComponentType::Disabled || Targets.Num() == 0)
        return;
	AActor* Actor = GetOwner();
    // If no actor then we are ticking to early
	if (!Actor)
		return;
    
    // Determine the initial transform
    FTransform Result;
    if (ControlName.IsNone() || ControlName.ToString().IsEmpty()) {
        Result = Actor->GetRootComponent()->GetComponentTransform();
    }
    else {
        // If this is a controlrig, find the control transform and put it in world space
        UControlRig* ControlRig = GetControlRig(Actor);
        if (ControlRig)
            Result = ControlRig->GetControlHierarchy().GetGlobalTransform(ControlName) * Actor->GetRootComponent()->GetComponentTransform();
    }

    // Iterate the targets and blend as needed
    int i = 0;
    for (auto Target : Targets) {
        if (!Target.TargetActor)
            continue;

        // Get the transform of the target actor or control
        FTransform TargetTransform;
        if (Target.ControlName.IsNone() || Target.ControlName.ToString().IsEmpty()) {
            TargetTransform = Target.TargetActor->GetRootComponent()->GetComponentTransform();
        }
        else {
            // If this is a controlrig, find the control transform and put it in world space
            UControlRig* ControlRig = GetControlRig(Target.TargetActor);
            if (ControlRig)
                TargetTransform = ControlRig->GetControlHierarchy().GetGlobalTransform(Target.ControlName) * Target.TargetActor->GetRootComponent()->GetComponentTransform();
        }
        // First item must be of full weight
        float Weight = (i == 0) ? 1.0 : Target.Weight;
        if (ConstraintType == EConstraintComponentType::SnapToTarget) {
            // Snap to target is simply copy the transform
            ApplyTransform(Result, TargetTransform, Target.Weight);
        } else if ( ConstraintType == EConstraintComponentType::AimAtTarget ) {
            // Aim target is simply normalized difference in translations
            FVector LookAt = Result.GetLocation() - TargetTransform.GetLocation();
            // Check input for requested up axis
            FVector LookUp;
            switch (AimUpAxis) {
            case EConstraintComponentAimAxis::LocalX:
                LookUp = FVector(1, 0, 0);
                break;
            case EConstraintComponentAimAxis::LocalY:
                LookUp = FVector(0, 1, 0);
                break;
            case EConstraintComponentAimAxis::LocalZ:
                LookUp = FVector(0, 0, 1);
                break;
            case EConstraintComponentAimAxis::TargetX:
                LookUp = TargetTransform.GetUnitAxis(EAxis::X);
                break;
            case EConstraintComponentAimAxis::TargetY:
                LookUp = TargetTransform.GetUnitAxis(EAxis::Y);
                break;
            case EConstraintComponentAimAxis::TargetZ:
                LookUp = TargetTransform.GetUnitAxis(EAxis::Z);
                break;
            };
            // Create a rotator
            FQuat Rotation = FRotationMatrix::MakeFromXZ(LookAt, LookUp).ToQuat();
            FTransform AimedTransform(Result);
            // Rotate according to requested facing axis
            switch (AimAxis) {
            case EConstraintComponentAxis::X:
                break;
            case EConstraintComponentAxis::Y:
                Rotation*= FQuat::MakeFromEuler(FVector(0, 0, 90));
                break;
            case EConstraintComponentAxis::Z:
                Rotation*= FQuat::MakeFromEuler(FVector(0, 90, 0));
                break;
            };
            AimedTransform.SetRotation(Rotation);
            ApplyTransform(Result, AimedTransform, Target.Weight);

        }   
        i += 1;
    }
    // Apply the offset
    Result = FilterTransform(Result, Offset * Result);
    // Apply transform to source
    if (ControlName.IsNone() || ControlName.ToString().IsEmpty()) {
        Actor->GetRootComponent()->SetWorldTransform(Result);
    }
    else {
        // SetGlobalTransform is actually relative to actor root
        UControlRig* ControlRig = GetControlRig(Actor);
        if (ControlRig)
            ControlRig->GetControlHierarchy().SetGlobalTransform(ControlName, Result * Actor->GetRootComponent()->GetComponentTransform().Inverse());
    }
}

Interface:
We could just give it to animators like that, but I think it’s important to make it easy for them to use it, so I’ve gone ahead and made a quick UI. It’s not pretty, but it saves them digging through settings and copying control name strings.

Simple Constraint Tool

I won’t go over how to set up the UI as it’s just buttons, but here is what it does:
Each button calls a method to create a constraint:
We first find the constraint source and target from selection, create the constraint then tweak the properties depending on the requested setup. Here is point constraint:

FReply SConstraintTool::MakePointConstraint()
{
    FConstraintTargets Targets = GetConstraintTargets();
    if (!Targets.IsValid)
        return FReply::Unhandled();
    UConstraintComponent* Constraint = SetupConstraint(Targets, EConstraintComponentType::SnapToTarget, true, false, false);
    if (!Constraint)
        return FReply::Unhandled();
    Constraint->TranslateConstrainX = true;
    Constraint->TranslateConstrainY = true;
    Constraint->TranslateConstrainZ = true;
    Constraint->RotateConstrainX = false;
    Constraint->RotateConstrainY = false;
    Constraint->RotateConstrainZ = false;
    Constraint->ScaleConstrainX = false;
    Constraint->ScaleConstrainY = false;
    Constraint->ScaleConstrainZ = false;
    return FReply::Handled();
}

By making some assumptions we can get the source and target selection into a temporary struct:

struct FConstraintTargets {
    bool IsValid = false;
    AActor* SourceActor=nullptr;
    FName SourceControlName;
    AActor* TargetActor=nullptr;
    FName TargetControlName;
};

FConstraintTargets GetConstraintTargets()
{
    FConstraintTargets Targets;
    UControlRig* ControlRig = GetActiveControlRig();
    TArray<FName> Controls;
    if (ControlRig)
        Controls = ControlRig->CurrentControlSelection();
    TArray<AActor*> SelectedActors;
    GEditor->GetSelectedActors()->GetSelectedObjects(SelectedActors);
    // if controls selected, remove actor from selected actors
    if (Controls.Num() && SelectedActors.Num()) {
        AActor* ControlRigActor = ControlRig->GetObjectBinding()->GetHostingActor();
        if (ControlRigActor && SelectedActors.Contains(ControlRigActor))
            SelectedActors.Remove(ControlRigActor);
    }
    if (Controls.Num() + SelectedActors.Num() != 2) {
		UE_LOG(LogTemp, Error, TEXT("ConstraintTool only allows constraining one object at a time!"));
        return Targets;
    }
    if (!SelectedActors.Num()) {
        // only controls selected
        Targets.IsValid = true;
        AActor* ControlRigActor = ControlRig->GetObjectBinding()->GetHostingActor();
        Targets.SourceActor = ControlRigActor;
        Targets.SourceControlName = Controls[0];
        Targets.TargetActor = ControlRigActor;
        Targets.TargetControlName = Controls[1];

    }
    else if (!Controls.Num()) {
        // Only actors selected
        Targets.IsValid = true;
        Targets.SourceActor = SelectedActors[0];
        Targets.TargetActor = SelectedActors[1];
    }
    else {
        // mix of controls and actors
        // Currently unreal only allows control selected after actor so control must be target
        Targets.IsValid = true;
        AActor* ControlRigActor = ControlRig->GetObjectBinding()->GetHostingActor();
        Targets.SourceActor = SelectedActors[0];
        Targets.TargetActor = ControlRigActor;
        Targets.TargetControlName = Controls[0];
    }
    if (!Targets.SourceActor || !Targets.TargetActor)
        Targets.IsValid = false;
    return Targets;
}

And then we make the constraint. You’ll notice here that we first check if there is an existing constraint on the source and if it’s compatible with the requested constraint, if not we make a new one before appending our new target.

UConstraintComponent* SetupConstraint(const FConstraintTargets & Targets, EConstraintComponentType Type, bool Translate=true, bool Rotate=true, bool Scale=true)
{
    UConstraintComponent* Constraint = Cast<UConstraintComponent>(Targets.SourceActor->GetComponentByClass(UConstraintComponent::StaticClass()));
    float Weight = 1.0f;
    if (Constraint) {
        // Check if parameters can be added to an existing constraint
        if (Type != Constraint->ConstraintType)
            Constraint = nullptr;
        if (Targets.SourceControlName != Constraint->ControlName)
            Constraint = nullptr; /// Control is a pseudo source
        if (Translate && !(Constraint->TranslateConstrainX || Constraint->TranslateConstrainY || Constraint->TranslateConstrainZ))
            Constraint = nullptr;
        if (Rotate && !(Constraint->RotateConstrainX || Constraint->RotateConstrainY || Constraint->RotateConstrainZ))
            Constraint = nullptr;
        if (Scale && !(Constraint->ScaleConstrainX || Constraint->ScaleConstrainY || Constraint->ScaleConstrainZ))
            Constraint = nullptr;
    }
    if (!Constraint) {
        FString Name;
        if (!Targets.SourceControlName.IsNone()) {
            if (!Targets.TargetControlName.IsNone()) {
                Name = FString::Printf(TEXT("Constrain_%s_to_%s:%s"), *Targets.SourceControlName.ToString(), *Targets.TargetActor->GetName(), *Targets.TargetControlName.ToString());
            }
            else {
                Name = FString::Printf(TEXT("Constrain_%s_to_%s"), *Targets.SourceControlName.ToString(), *Targets.TargetActor->GetName());
            }
        }
        else {
            if (!Targets.TargetControlName.IsNone()) {
                Name = FString::Printf(TEXT("ConstrainTo_%s:%s"), *Targets.TargetActor->GetName(), *Targets.TargetControlName.ToString());
            }
            else {
                Name = FString::Printf(TEXT("ConstrainTo_%s"), *Targets.TargetActor->GetName());
            }
        }

        Constraint = NewObject<UConstraintComponent>(Targets.SourceActor, UConstraintComponent::StaticClass(), FName(Name));
		Targets.SourceActor->Modify();
        USceneComponent* AttachTo = Targets.SourceActor->GetRootComponent();
		Targets.SourceActor->AddInstanceComponent(Constraint);
		Constraint->OnComponentCreated();
		Constraint->RegisterComponent();
		Targets.SourceActor->RerunConstructionScripts();
        Constraint->ControlName = Targets.SourceControlName;
        Constraint->ConstraintType = Type;
    }
    else {
        Weight = 0.5f;
    }
    FConstraintComponentTarget NewTarget;
    NewTarget.TargetActor = Targets.TargetActor;
    NewTarget.ControlName = Targets.TargetControlName;
    NewTarget.Weight = Weight;
    Constraint->Targets.Add(NewTarget);
    return Constraint;
}

Final Result:

Constraint Tool

Closing Thoughts

I’m not certain this is the best way of doing constraints as it steps outside the “unreal” worflows and a little close to “maya” workflows, but I honestly don’t know a better way of providing this level of interaction without making things too complex for animation.
At the end of the day, it works and will render just fine in film.

Regards,
Mr MinimalEffort