Download

Per Instance custom data inside of material

Before you do this, you need the source build to be able to compile the code

Here is a quick demo of what this tutorial accomplishes:

https://youtube.com/watch?v=g5vhF1vpvTQ

Basically, this allows you to have a custom value assigned to every instance you add to a HISM component, which is useful in the material editor to have the isntances look different based on the value they’re assigned (at least in my case). This is achieved by using the PerInstanceRandom node in the material editor, which usually produces a random value, but it’ll produce your custom value that you assign the instance.


If you have any questions / concerns, feel free to ask me

Here is my solution as of 4.20.2: (I will try to be as clear as possible, and I would recommend backing up engine/project/etc. that you are using just in case)

First thing that needs to be done is you need to navigate to the InstancedStaticMeshComponent.h file in the UE4 solution.
In there, first navigate to the USTRUCT() FInstancedStaticMeshInstanceData (around line 73) and add a new float UPROPERTY under the existing transform property, and call it Param.


USTRUCT()
struct FInstancedStaticMeshInstanceData
{
    GENERATED_USTRUCT_BODY()

        UPROPERTY(EditAnywhere, Category = Instances)
        FMatrix Transform;

        UPROPERTY(EditAnywhere, Category = Instances)
        float Param = 0;

    FInstancedStaticMeshInstanceData()
        : Transform(FMatrix::Identity)
    {
    }

    FInstancedStaticMeshInstanceData(const FMatrix& InTransform)
        : Transform(InTransform)
    {
    }

    friend FArchive& operator<<(FArchive& Ar, FInstancedStaticMeshInstanceData& InstanceData)
    {
        // @warning BulkSerialize: FInstancedStaticMeshInstanceData is serialized as memory dump
        // See TArray::BulkSerialize for detailed description of implied limitations.
        Ar << InstanceData.Transform;
        return Ar;
    }
};

Second, we need to add in 2 new declarations. Copy and paste the declaration for UFUNCTION AddInstance, and rename it AddInstance2 and add in a float parameter ‘Param’
Do the same thing, but for AddInstanceInternal(Copy and paste the declaration for UFUNCTION AddInstanceInternal, and rename it AddInstanceInternal2 and add in a float parameter ‘Param’)


    UFUNCTION(BlueprintCallable, Category = "Components|InstancedStaticMesh")
        virtual int32 AddInstance2(const FTransform& InstanceTransform, float Param);

    /** Internal version of AddInstance */
    int32 AddInstanceInternal2(int32 InstanceIndex, FInstancedStaticMeshInstanceData* InNewInstanceData, const FTransform& InstanceTransform, float Param);

Lastly, find the declaration of SetupNewInstanceData and modify it so it has our float parameter ‘Param’


    /** Sets up new instance data to sensible defaults, creates physics counterparts if possible. */
    void SetupNewInstanceData(FInstancedStaticMeshInstanceData& InOutNewInstanceData, int32 InInstanceIndex, const FTransform& InInstanceTransform, float Param);

** Moving on to InstancedStaticMesh.cpp **

First thing we need to do in here is add definitions for our two declarations we just put in. If you look at the AddInstance declaration, its calling AddInstanceInternal, so copy and paste that declaration but change ‘AddInstance’ to ‘AddInstance2’ and ‘AddInstanceInternal’ to ‘AddInstanceInternal2’ and add our float parameter.


int32 UInstancedStaticMeshComponent::AddInstance2(const FTransform& InstanceTransform, float Param)
{
    return AddInstanceInternal2(PerInstanceSMData.Num(), nullptr, InstanceTransform, Param);
}

Now we need to add our definition for AddInstanceInternal2, so first copy and paste the AddInstanceInternal definition and change it to ‘AddInstanceInternal2’ and add our float parameter. Then in the call of SetupNewInstanceData, plug in ‘Param’ into the last parameter.


int32 UInstancedStaticMeshComponent::AddInstanceInternal2(int32 InstanceIndex, FInstancedStaticMeshInstanceData* InNewInstanceData, const FTransform& InstanceTransform, float Param)
{
    FInstancedStaticMeshInstanceData* NewInstanceData = InNewInstanceData;

    if (NewInstanceData == nullptr)
    {
        NewInstanceData = new(PerInstanceSMData) FInstancedStaticMeshInstanceData();
    }

    SetupNewInstanceData(*NewInstanceData, InstanceIndex, InstanceTransform, Param);

#if WITH_EDITOR
    if (SelectedInstances.Num())
    {
        SelectedInstances.Add(false);
    }
#endif

    PartialNavigationUpdate(InstanceIndex);

    InstanceUpdateCmdBuffer.Edit();
    MarkRenderStateDirty();

    return InstanceIndex;
}

Now, in the original AddInstanceInternal definition, locate the SetupNewInstanceData call and add


    check(InstancingRandomSeed != 0);
    FRandomStream RandomStream = FRandomStream(InstancingRandomSeed);

right above it, and in for the last parameter in SetupNewInstanceData, plug in ‘RandomStream.GetFraction()’ (to preserve default engine behavior)


int32 UInstancedStaticMeshComponent::AddInstanceInternal(int32 InstanceIndex, FInstancedStaticMeshInstanceData* InNewInstanceData, const FTransform& InstanceTransform)
{
    FInstancedStaticMeshInstanceData* NewInstanceData = InNewInstanceData;

    if (NewInstanceData == nullptr)
    {
        NewInstanceData = new(PerInstanceSMData) FInstancedStaticMeshInstanceData();
    }

    check(InstancingRandomSeed != 0);
    FRandomStream RandomStream = FRandomStream(InstancingRandomSeed);

    SetupNewInstanceData(*NewInstanceData, InstanceIndex, InstanceTransform, RandomStream.GetFraction());

#if WITH_EDITOR
    if (SelectedInstances.Num())
    {
        SelectedInstances.Add(false);
    }
#endif

    PartialNavigationUpdate(InstanceIndex);

    InstanceUpdateCmdBuffer.Edit();
    MarkRenderStateDirty();

    return InstanceIndex;
}

Now, we need to locate the actual definition of SetupNewInstanceData, and make sure you add your float parameter ‘Param’ to the declaration.
Then, right under


    InOutNewInstanceData.Transform = InInstanceTransform.ToMatrixWithScale();

add this line:


    InOutNewInstanceData.Param = Param;

so it should look like:


void UInstancedStaticMeshComponent::SetupNewInstanceData(FInstancedStaticMeshInstanceData& InOutNewInstanceData, int32 InInstanceIndex, const FTransform& InInstanceTransform, float Param)
{
    InOutNewInstanceData.Transform = InInstanceTransform.ToMatrixWithScale();
    InOutNewInstanceData.Param = Param;

    if (bPhysicsStateCreated)
    {
        if (InInstanceTransform.GetScale3D().IsNearlyZero())
        {
            InstanceBodies.Insert(nullptr, InInstanceIndex);
        }
        else
        {
            FBodyInstance* NewBodyInstance = new FBodyInstance();
            int32 BodyIndex = InstanceBodies.Insert(NewBodyInstance, InInstanceIndex);
            check(InInstanceIndex == BodyIndex);
            InitInstanceBody(BodyIndex, NewBodyInstance);
        }
    }
}

** Moving on to HierarchicalInstancedStaticMeshComponent.h **

Add in this line of code underneath the ‘AddInstance’ one:


    virtual int32 AddInstance2(const FTransform& InstanceTransform, float Param) override;

** Moving on to HierarchicalInstancedStaticMesh.cpp **

Copy the declaration of AddInstance and paste it, then rename it to ‘AddInstance2’ and give it our float parameter ‘Param’.
Then, on the second line of AddInstance2, you should see this


    int32 InstanceIndex = UInstancedStaticMeshComponent::AddInstance(InstanceTransform);

Change the ‘AddInstance’ call to ‘AddInstance2’, and plug in ‘Param’ as the last parameter.
Now, navigate towards the top of the file, and locate the ‘FClusterBuilder’ class. Scroll down until you find the ‘public’ header, and add this under it:


    TArray<FInstancedStaticMeshInstanceData> inInstanceData;

this will be so that we can pass the builder our param data.
Now scroll up a bit until you find the BuildInstanceBuffer function. Delete the for loop at the bottom and add this code block in:


            for (int32 i = 0; i < NumInstances; ++i)
            {
                int32 RenderIndex = Result->InstanceReorderTable*;
                float RandomID = RandomStream.GetFraction();
                if (RenderIndex >= 0)
                {
                    if (i < inInstanceData.Num() && inInstanceData*.Param != 0))
                    {
                        RandomID = inInstanceData*.Param;
                    }
                    BuiltInstanceData->SetInstance(RenderIndex, Transforms*, RandomID, LightmapUVBias, ShadowmapUVBias);
                }
                // correct light/shadow map bias will be setup on game thread side if needed
            }

What this does is it checks if it is receiving valid instance data and isn’t 0, and if so, set the PerInstanceRandom value (which derives from the instance’s origin W value) to our Param which we set, otherwise set it to a random number (to preserve default engine behavior).

Now the last thing we need to do is pass in our instance data when this function is called. To do this, first locate the function

void UHierarchicalInstancedStaticMeshComponent::BuildTree()

and scroll down until you see something like this:


        FClusterBuilder Builder(InstanceTransforms, GetStaticMesh()->GetBounds().GetBox(), DesiredInstancesPerLeaf(), CurrentDensityScaling, InstancingRandomSeed);

        Builder.BuildTreeAndBuffer();

All you need to do is add this line below the initialization of FClusterBuilder Builder so it looks like this:


        FClusterBuilder Builder(InstanceTransforms, GetStaticMesh()->GetBounds().GetBox(), DesiredInstancesPerLeaf(), CurrentDensityScaling, InstancingRandomSeed);
        Builder.inInstanceData = PerInstanceSMData;

        Builder.BuildTreeAndBuffer();

(PerInstanceSMData is the struct that holds our param value)
This function is called only once more, and that should be everything we need to do.
Then locate the function


void UHierarchicalInstancedStaticMeshComponent::BuildTreeAsync()

and scroll down until you see a section that looks something like:


        int32 Num = PerInstanceSMData.Num();
        TArray<FMatrix> InstanceTransforms;
        InstanceTransforms.SetNumUninitialized(Num);
        for (int32 Index = 0; Index < Num; Index++)
        {
            InstanceTransforms[Index] = PerInstanceSMData[Index].Transform;
        }

        UE_LOG(LogStaticMesh, Verbose, TEXT("Copied %d transforms in %.3fs."), Num, float(FPlatformTime::Seconds() - StartTime));

        TSharedRef<FClusterBuilder, ESPMode::ThreadSafe> Builder(new FClusterBuilder(InstanceTransforms, GetStaticMesh()->GetBounds().GetBox(), DesiredInstancesPerLeaf(), CurrentDensityScaling, InstancingRandomSeed));

        bIsAsyncBuilding = true;

and add the same line as we did in the last function right before bIsAsyncBuilding, so it looks like this:


        int32 Num = PerInstanceSMData.Num();
        TArray<FMatrix> InstanceTransforms;
        InstanceTransforms.SetNumUninitialized(Num);
        for (int32 Index = 0; Index < Num; Index++)
        {
            InstanceTransforms[Index] = PerInstanceSMData[Index].Transform;
        }

        UE_LOG(LogStaticMesh, Verbose, TEXT("Copied %d transforms in %.3fs."), Num, float(FPlatformTime::Seconds() - StartTime));

        TSharedRef<FClusterBuilder, ESPMode::ThreadSafe> Builder(new FClusterBuilder(InstanceTransforms, GetStaticMesh()->GetBounds().GetBox(), DesiredInstancesPerLeaf(), CurrentDensityScaling, InstancingRandomSeed));

        Builder->inInstanceData = PerInstanceSMData;

        bIsAsyncBuilding = true;

Congrats! all the engine parts are complete now. Once it is built, to utilize it in c++, make sure you call at as UHierarchicalInstancedStaticMeshComponent::AddInstance2 to be safe, and in blueprint, just run the AddInstance2 node with a HISM reference. To access this data in the material, use the PerInstanceRandom node.

- TheApplePieGod

Cool. I’d buy this if you made it into a plugin on the marketplace.

Would absolutely buy as a plugin. Since we use other plug-ins that don’t allow us to use custom engine builds. (i.e. Simplygon and Ikinema). Would love to see this as a plugin! But phenomenal work nonetheless!

AddInstance2 does not show up in my blueprint. I can only find the default “Add Instance” function. Maybe Unreal does not accept changes in the core files. For Unreal Editor 4.22.2 this tutorial does not work for me.

Edit: Ok, now I have rebuilt my whole project and everything succeeded, but in my blueprints I can still not find my modified function. Other functions that I have defined outside of the engine folder are available for me in the blueprint. I just cannot get the engine file changes to work, even though the builds have not errors.

Worked great for me! Thanks a million!

This is crazy why isn’t this available in the engine by default?. What’s the performance like?

Performance is stellar.

This works as is for HISM’s. If you want it to also work for regular ISMs Make the following change in InstancedStaticMesh.cpp.

Function: void UInstancedStaticMeshComponent::BuildRenderData

Scroll down until you see

OutData.SetInstance(RenderIndex, InstanceData.Transform, RandomStream.GetFraction(), LightmapUVBias, ShadowmapUVBias);

Delete that line and replace with the following:

if (PerInstanceSMData[Index].Param > 0.0f)
{

        OutData.SetInstance(RenderIndex, InstanceData.Transform, PerInstanceSMData[Index].Param, LightmapUVBias, ShadowmapUVBias);
    }
    else
    {
        OutData.SetInstance(RenderIndex, InstanceData.Transform, RandomStream.GetFraction(), LightmapUVBias, ShadowmapUVBias);
    }

Now regular ISM’s will work using the AddInstance2 function you already added!

Thanks

CH

By doing this i seem to have lost PerInstanceRandom functionally on foliage. Anyone know a fix?

Getting A Crash now when i try to reopen a map with Foliage, any ideas?

I think it’s because you have your Foliage created in the old, original classes. So you have a conflict between your BP and code.

I couldn’t even delete my BP actor with HierarchicalInstancedStaticMesh component, created before. So I deleted BP actors with Windows Explorer, not UEContent Browser.