How do you properly use the blend space AddSample() function?

Hello,

First time posting, so if I am doing this wrong please let me know.

So I currently have a need to dynamically change the samples of a 1d blendspace at run time. Let me explain why. I have a stadium of a few thousand supporters who change between different idle animations. All of a sudden a score is scored and I want to blend from their idle animation to a cheering animation. So instead of having a different blend space for each transition (number of idle animations) x (number of cheering animation), it would be much nicer to just change the samples of a blend space to handle the transition.

So after seeing what I had available to me in C++, a 1d blend space has Add Sample or Edit Sample. So I have the following code samples for trying to change the samples:

if (BlendSpace1dPointer)
{
            BlendSpace1dPointer->ClearAllSamples();
    
            FBlendSample FirstBlendSample;
            FirstBlendSample.SampleValue.X = 0;
            FirstBlendSample.Animation = StartingAnimSequence;
            FirstBlendSample.bIsValid = true;
    
            BlendSpace1dPointer->AddSample(FirstBlendSample);
    
            FBlendSample SecondBlendSample;
            SecondBlendSample.SampleValue.X = 100;
            SecondBlendSample.Animation = EndingAnimSequence;
            SecondBlendSample.bIsValid = true;
    
            BlendSpace1dPointer->AddSample(SecondBlendSample);
    
            this->PlayAnimation(BlendSpace1dPointer, true);
}

Another example but this time using Edit Sample:

if (BlendSpace1dPointer)
    {

        TArray<FBlendSample> Samples = BlendSpace1dPointer->GetBlendSamples();

        BlendSpace1dPointer->EditSampleAnimation(Samples[0], StartingAnimSequence);
        BlendSpace1dPointer->EditSampleAnimation(Samples[1], EndingAnimSequence);

        this->PlayAnimation(BlendSpace1dPointer, true);
    }

With the provided examples I would expect it to play just the first animation sequence on loop but instead my model just stays in a T pose.

Even more confusing, if I go to the editor version of that 1d blend space, it now shows that it is using the new samples I set in the C++. And those samples stay changed even after I stop execution. Now if I click Apply Parameter Changes on the blend space in the editor and then start executing and just call play without adding or editing the samples, the model animates just as expected. So I tried following the logic that happens after you click Apply Parameter Changes in the editor and after calling some of the same functions after my previous code snippet, nothing works still.

After googling a bit, I saw one other guy had the same question as me but that was posted over 6 months ago and no one has replied. Here is the link to that post: https://answers.unrealengine.com/questions/378777/animation-blendspace-edit-specific-nodes-at-runtim.html

So my question for anyone, does anyone know how to use AddSample() or EditSample() of a 1d blend space at run time and not have your model stuck in a T pose after making said change?

Any help would be very much appreciated.

1 Like

So after digging around in the unreal engine source, I discovered that the initialization for the samples is only done when you hit Apply Parameter Changes. Basically that populates Grid samples data field and things will animate and blend correctly.

So I extended the blendspace1d class to include a new function to call for initializing the samples after you edit or add new samples. Keep in mind, a lot of this code is copy and pasted from the unreal editor file, SAnimationBlendSpace1D.cpp/h. Since the logic in that file can’t be used at run time, you need to duplicate the code into the class, as shown below.

So an example of it’s use would look like this:

            BlendSpace1dPointer->ClearAllSamples();

            FBlendSample FirstBlendSample;
            FirstBlendSample.SampleValue.X = 0;
            FirstBlendSample.Animation = StartingAnimSequence;
            FirstBlendSample.bIsValid = true;

            BlendSpace1dPointer->AddSample(FirstBlendSample);

            FBlendSample SecondBlendSample;
            SecondBlendSample.SampleValue.X = 100;
            SecondBlendSample.Animation = EndingAnimSequence;
            SecondBlendSample.bIsValid = true;

            BlendSpace1dPointer->AddSample(SecondBlendSample);

            BlendSpace1dPointer->InitializeSamples();

Here is the added sections for Blendspace1d.h:

Define this in public
ENGINE_API void InitializeSamples();

Define this in private:
struct FIndexLinePoint
    {
        FVector Point;
        int32	Index;
        FIndexLinePoint(FVector& InPoint, int32 InIndex) : Point(InPoint), Index(InIndex) {}
    };


    struct FLineElement
    {
        const FIndexLinePoint	Start;
        const FIndexLinePoint	End;
        const bool				bIsFirst;
        const bool				bIsLast;

        float Range;

        FLineElement(const FIndexLinePoint& InStart, const FIndexLinePoint& InEnd, bool bInIsFirst, bool bInIsLast) : Start(InStart), End(InEnd), bIsFirst(bInIsFirst), bIsLast(bInIsLast)
        {
            Range = End.Point.X - Start.Point.X;
        }

        bool PopulateElement(float ElementPosition, FEditorElement& Element) const;
    };

    /** Populates EditorElements based on the Sample points previously supplied to AddSamplePoint */
    void CalculateEditorElements();

    /**
    * Data Structure for line generation
    * SamplePointList is the input data
    */
    TArray<FVector> SamplePointList;

    /** Editor elements generated by CalculateEditorElements */
    TArray<FEditorElement> EditorElements;

    TArray<FLineElement> LineElements;

Then in blendspace1d.cpp add the following:

void UBlendSpace1D::InitializeSamples(void)
{
    ValidateSampleData();
    SamplePointList.Empty();

    for (int i = 0; i < SampleData.Num(); ++i)
    {
        SamplePointList.Add(SampleData[i].SampleValue);
    }

    CalculateEditorElements();

    FillupGridElements(SamplePointList, EditorElements);
}

bool UBlendSpace1D::FLineElement::PopulateElement(float ElementPosition, FEditorElement& Element) const
{
    if (ElementPosition < Start.Point.X)
    {
        check(bIsFirst); // We should never be processing a point before us unless we
                         // are the first line in the chain
        Element.Indices[0] = Start.Index;
        Element.Weights[0] = 1.0f;
        return true;
    }
    else if (ElementPosition > End.Point.X)
    {
        if (!bIsLast)
        {
            return false;
        }
        Element.Indices[0] = End.Index;
        Element.Weights[0] = 1.0f;
        return true;
    }
    else
    {
        //Calc End Point Weight
        Element.Indices[0] = End.Index;
        Element.Weights[0] = (ElementPosition - Start.Point.X) / Range;

        //Start Point Weight is inverse of End Point
        Element.Indices[1] = Start.Index;
        Element.Weights[1] = (1.0f - Element.Weights[0]);
        return true;
    }
}

void UBlendSpace1D::CalculateEditorElements()
{
    StartOfEditorRange = BlendParameters[0].Min;
    EndOfEditorRange = BlendParameters[0].Max;
    int32 NumEditorPoints = BlendParameters[0].GridNum + 1;

    struct FLinePointSorter
    {
        bool operator()(const FVector& PointA, const FVector& PointB) const
        {
            return PointA.X < PointB.X;
        }
    };

    //Create EditorElements
    EditorElements.Empty(NumEditorPoints);
    EditorElements.AddUninitialized(NumEditorPoints);

    if (SamplePointList.Num() == 0)
    {
        for (int ElementIndex = 0; ElementIndex < EditorElements.Num(); ++ElementIndex)
        {
            EditorElements.Add(FEditorElement());
        }
        return;
    }

    // Order points
    SamplePointList.Sort(FLinePointSorter());

    // Generate lines
    LineElements.Empty();
    for (int32 PointIndex = 0; PointIndex < SamplePointList.Num() - 1; ++PointIndex)
    {
        int32 EndPointIndex = PointIndex + 1;

        FIndexLinePoint StartPoint(SamplePointList[PointIndex], PointIndex);
        FIndexLinePoint EndPoint(SamplePointList[EndPointIndex], EndPointIndex);

        LineElements.Add(FLineElement(StartPoint, EndPoint, PointIndex == 0, EndPointIndex == (SamplePointList.Num() - 1)));
    }

    int32 NumEditorDivisions = (NumEditorPoints - 1); //number of spaces between points in the editor

    float EditorRange = EndOfEditorRange - StartOfEditorRange;
    float EditorStep = EditorRange / NumEditorDivisions;

    if (LineElements.Num() == 0)
    {
        // no lines made, just make everything sample 0
        FEditorElement Element;
        Element.Indices[0] = 0;
        Element.Weights[0] = 1.0f;
        for (int ElementIndex = 0; ElementIndex < EditorElements.Num(); ++ElementIndex)
        {
            EditorElements[ElementIndex] = Element;
        }
    }
    else
    {
        for (int ElementIndex = 0; ElementIndex < EditorElements.Num(); ++ElementIndex)
        {
            bool bPopulatedElement = false;

            float ElementPosition = (EditorStep * ElementIndex) + StartOfEditorRange;
            FEditorElement Element;

            for (int LineIndex = 0; LineIndex < LineElements.Num(); ++LineIndex)
            {
                if (LineElements[LineIndex].PopulateElement(ElementPosition, Element))
                {
                    bPopulatedElement = true;
                    break;
                }
            }

            check(bPopulatedElement);
            EditorElements[ElementIndex] = Element;
        }
    }
}