Set Material of Landscape

I would like to dynamically change the material of my landscape object (or at least create a dynamic instance for it) during runtime in c++. However, I can’t find anything in the documentation about how to go about doing so. How should I go about this?

1 Like

I think that’s not an intended feature what you’re asking for but it may be possible. If the following doesn’t work it will probably need an engine modification though.

Looking at ALandscapeProxy::PostEditChangeProperty in Source\Runtime\Landscape\Private\LandscapeEdit.cpp all LandscapeComponents will have UpdateMaterialInstances called on them. Something similar happens when OverrideMaterial is set or SetMaterial is called for a LandscapeComponent. ULandscapeComponent::UpdateMaterialInstances will update the MaterialInstance (after creating a new one if necessary, otherwise uses the existing one). In any case it’s probably a good idea to start digging into the related source code (there’s a lot of Landscape related stuff).

The following is untested but it should quickly give you a answer in that it either works or it doesn’t (I am not sure what happens when you attempt to call SetParameterValue functionality on a MaterialInstanceConstant, it could crash, print an error/warning (UDK used to do that) or just work). You can’t just call SetParameterValueEditorOnly though. These functions first check(GIsEditor). But you can work around this with a little bit of custom code.

So at the point you want to use SetVectorParameterValue on the Landscape this might help. The same approach would be required for the other parameter types.


/**
 * Start of code taken from MaterialInstance.cpp
 */
/**
 * Updates a parameter on the material instance from the game thread.
 */
template <typename ParameterType>
void GameThread_UpdateMIParameter(const UMaterialInstance* Instance, const ParameterType& Parameter)
{
	ENQUEUE_UNIQUE_RENDER_COMMAND_THREEPARAMETER_CREATE_TEMPLATE(
		SetMIParameterValue,ParameterType,
		const UMaterialInstance*,Instance,
		FName,Parameter.ParameterName,
		typename ParameterType::ValueType,ParameterType::GetValue(Parameter)
		);
}

/**
 * Cache uniform expressions for the given material.
 * @param MaterialInstance - The material instance for which to cache uniform expressions.
 */
void CacheMaterialInstanceUniformExpressions(const UMaterialInstance* MaterialInstance)
{
	// Only cache the unselected + unhovered material instance. Selection color
	// can change at runtime and would invalidate the parameter cache.
	if (MaterialInstance->Resources[0])
	{
		MaterialInstance->Resources[0]->CacheUniformExpressions_GameThread();
	}
}
/**
 * End of code taken from MaterialInstance.cpp
 */

void SetVectorParameterValue(ALandscapeProxy* Landscape, FName ParameterName, FLinearColor Value)
{
	if (Landscape)
	{
		for (int32 Index = 0; Index < Landscape->LandscapeComponents.Num(); ++Index)
		{
			if (Landscape->LandscapeComponents[Index])
			{
				UMaterialInstanceConstant* MIC = Landscape->LandscapeComponents[Index]->MaterialInstance;
				if (MIC)
				{
					/**
					 * Start of code taken from UMaterialInstance::SetSetVectorParameterValueInternal and adjusted to use MIC instead of this
					 */
					 FVectorParameterValue* ParameterValue = GameThread_FindParameterByName( //from MaterialInstanceSupport.h
					 	MIC->VectorParameterValues,
					 	ParameterName
						);

					if(!ParameterValue)
					{
						// If there's no element for the named parameter in array yet, add one.
						ParameterValue = new(VectorParameterValues) FVectorParameterValue;
						ParameterValue->ParameterName = ParameterName;
						ParameterValue->ExpressionGUID.Invalidate();
						// Force an update on first use
						ParameterValue->ParameterValue.B = Value.B - 1.f;
					}

					// Don't enqueue an update if it isn't needed
					if (ParameterValue->ParameterValue != Value)
					{
						ParameterValue->ParameterValue = Value;
						// Update the material instance data in the rendering thread.
						GameThread_UpdateMIParameter(MIC, *ParameterValue);
						CacheMaterialInstanceUniformExpressions(MIC);
					}
					/**
					 * End of code taken from UMaterialInstance::SetSetVectorParameterValueInternal and adjusted to use MIC instead of this
					 */
				}
			}
		}
	}
}

The alternative solution would be to change all ULandscapeMaterialInstanceConstant related code to a new class ULandscapeMaterialInstanceDynamic which would require a bit more work but is probably the cleaner approach.

Would be nice if you could post back if the above code works or not.

Thank you for helping me with my problem! The code you provided above, after adding in some headers, returns this error:


/home//Documents/Unreal Projects/Colonize/Source/Colonize/GameDirector.cpp:21:2: error: use of undeclared identifier 'EURCMacro_SetMIParameterValue'
        ENQUEUE_UNIQUE_RENDER_COMMAND_THREEPARAMETER_CREATE_TEMPLATE(
        ^
/home//Downloads/UnrealEngine/Engine/Source/Runtime/RenderCore/Public/RenderingThread.h:346:2: note: expanded from macro 'ENQUEUE_UNIQUE_RENDER_COMMAND_THREEPARAMETER_CREATE_TEMPLATE'
        ENQUEUE_UNIQUE_RENDER_COMMAND_THREEPARAMETER_CREATE(TypeName<TemplateParamName>,ParamType1,ParamValue1,ParamType2,ParamValue2,ParamType3,ParamValue3)
        ^
/home//Downloads/UnrealEngine/Engine/Source/Runtime/RenderCore/Public/RenderingThread.h:327:15: note: expanded from macro 'ENQUEUE_UNIQUE_RENDER_COMMAND_THREEPARAMETER_CREATE'
                        TGraphTask<EURCMacro_##TypeName>::CreateTask().ConstructAndDispatchWhenReady(ParamValue1,ParamValue2,ParamValue3); \
                                   ^
<scratch space>:70:1: note: expanded from here
EURCMacro_SetMIParameterValue
^

To clarify what I am doing, I would like to toggle an overlay on my landscape object, showing a grid with data displayed on it. Here are some screenshots from inside the editor of the material instance I have created:

Sorry you need this first, then the code I posted above.


/**
 * Start of code taken from MaterialInstance.cpp
 */
ENQUEUE_UNIQUE_RENDER_COMMAND_THREEPARAMETER_DECLARE_TEMPLATE(
	SetMIParameterValue,ParameterType,
	const UMaterialInstance*,Instance,Instance,
	FName,ParameterName,Parameter.ParameterName,
	typename ParameterType::ValueType,Value,ParameterType::GetValue(Parameter),
{
	Instance->Resources[0]->RenderThread_UpdateParameter(ParameterName, Value);
	if (Instance->Resources[1])
	{
		Instance->Resources[1]->RenderThread_UpdateParameter(ParameterName, Value);
	}
	if (Instance->Resources[2])
	{
		Instance->Resources[2]->RenderThread_UpdateParameter(ParameterName, Value);
	}
});
/**
 * End of code taken from MaterialInstance.cpp
 */

Again I can’t guarantee it works. What it’s supposed to do is update the parameter of the MaterialInstanceConstant at runtime.

The first thing to solve is how you want to populate the data you’re going to show on the landscape. Can you use vertex painting as a way of selecting what texture to show? If so then getting the look you’re after is fairly easy in a shader, you’ll just have to pack that data into a colour channel and write it to the vertex colour data somehow. You’ll need to make sure every material you use on the landscape has this code, so use instances of the same material if possible.

Once you’ve done that, toggling it should be as easy as what UnrealEverything pasted above. To sum up what you need to do regardless of if you’re in code or blueprints:

  1. Get the material(s) for the landscape
  2. Assign a dynamic material instance of that material
  3. Edit parameter values by name in code/blueprint

Try using a scalar parameter to turn the effect on and off. it’s faster to generate both the regular material and the overlay at once and then lerp between them than it is to do if statements. You get the ability to fade in your overlay effect for free.

Thank you for your continued help. After adding the code above, it then returns this error:


/home//Documents/Unreal Projects/Colonize/Source/Colonize/GameDirector.cpp:85:28: error: use of undeclared identifier 'VectorParameterValues'; did you mean 'UMaterialInstanceConstant::VectorParameterValues'?
                                                ParameterValue = new(VectorParameterValues) FVectorParameterValue;
                                                                     ^~~~~~~~~~~~~~~~~~~~~
                                                                     UMaterialInstanceConstant::VectorParameterValues
/home//Downloads/UnrealEngine/Engine/Source/Runtime/Engine/Classes/Materials/MaterialInstance.h:202:39: note: 'UMaterialInstanceConstant::VectorParameterValues' declared here
        TArray<struct FVectorParameterValue> VectorParameterValues;
                                             ^
/home//Documents/Unreal Projects/Colonize/Source/Colonize/GameDirector.cpp:85:28: error: invalid use of non-static data member 'VectorParameterValues'
                                                ParameterValue = new(VectorParameterValues) FVectorParameterValue;
                                                                     ^~~~~~~~~~~~~~~~~~~~~

Changing the code to what the compiler suggests yields this error:


/home/username/Documents/Unreal Projects/Colonize/Source/Colonize/GameDirector.cpp:85:55: error: invalid use of non-static data member 'VectorParameterValues'
                                                ParameterValue = new(UMaterialInstanceConstant::VectorParameterValues) FVectorParameterValue;

@Antidamage To clarify what is happening, the material I am using has a value to control how opaque the shown grid is. It can be dynamically instanced, or two preset materials can be used to toggle between grid and no grid. However, the Landscape actor class has no functions whatsoever to be able to easily change materials or set its material to one that is dynamically instanced. is attempting to step around the system to be able to change values of the material during runtime.

Sorry I missed that again VectorParameterValues is a public member variable of UMaterialInstance. The original code would work since the function is a member of UMaterialInstance and could therefore access VectorParameterValues from “this”. Since we no longer are in a member function of UMaterialInstance and are accessing the MIC through the Landscapes LandscapeComponents instead, the correct line would be


ParameterValue = new(MIC->VectorParameterValues) FVectorParameterValue;

It really would be easier if I’d actually compile it on my end, or if you would solve all compiler errors yourself (that would also be faster for you instead of waiting for me to respond).

@Antidamage: Unless I have missed something (again :() you cannot set a Landscape's (or LandscapeComponent's) Material. While you can access and set ALandscapeProxy::LandscapeMaterial, doing so does not have an effect outside the editor from what I can tell (e.g. looking at ALandscapeProxy::PostEditChangeProperty for how a LandscapeMaterial change is processed in the editor). The same essentially holds for ULandscapeComponent which extends UPrimitiveComponent and therefore has a SetMaterial function. This function is overridden only #if WITH_EDITOR. Otherwise it is not and looking at UPrimitiveComponent::SetMaterial the function does not do anything (empty body). Also setting ULandscapeComponent::OverrideMaterial is processed in similar fashion to ALandscapeProxy::LandscapeMaterial in ULandscapeComponent::PostEditChangeProperty.

The approach of the code I posted is trying to change the parameters of a MaterialInstanceConstant in the game (it runs pretty much the same code as if calling MaterialInstanceConstant::SetVectorParameterValueEditorOnly) with the exception that it does NOT check(GIsEditor). I am aware that this is not the intended usage of MIC but it seemed like the easier approach compared to customizing the engine. Especially for (seemingly) new forum members (not to say you aren’t familiar or experienced enough with either C++ or UE4 @CodeCube) :slight_smile:

Thank you so much! It now works :smiley: For any other people who might have similar problems, here is the full code after all the patches/includes:


#include "UObject/NameTypes.h"
#include "GameDirector.h"
#include "Materials/MaterialInstance.h"
#include "Materials/MaterialInterface.h"
#include "Materials/Material.h"
#include "Public/RenderingThread.h"
#include "Runtime/Engine/Private/Materials/MaterialInstanceSupport.h"

/**
 * Start of code taken from MaterialInstance.cpp
 */
ENQUEUE_UNIQUE_RENDER_COMMAND_THREEPARAMETER_DECLARE_TEMPLATE(
	SetMIParameterValue,ParameterType,
	const UMaterialInstance*,Instance,Instance,
	FName,ParameterName,Parameter.ParameterName,
	typename ParameterType::ValueType,Value,ParameterType::GetValue(Parameter),
{
	Instance->Resources[0]->RenderThread_UpdateParameter(ParameterName, Value);
	if (Instance->Resources[1])
	{
		Instance->Resources[1]->RenderThread_UpdateParameter(ParameterName, Value);
	}
	if (Instance->Resources[2])
	{
		Instance->Resources[2]->RenderThread_UpdateParameter(ParameterName, Value);
	}
});

/**
 * Updates a parameter on the material instance from the game thread.
 */
template <typename ParameterType>
void GameThread_UpdateMIParameter(const UMaterialInstance* Instance, const ParameterType& Parameter)
{
	ENQUEUE_UNIQUE_RENDER_COMMAND_THREEPARAMETER_CREATE_TEMPLATE(
		SetMIParameterValue,ParameterType,
		const UMaterialInstance*,Instance,
		FName,Parameter.ParameterName,
		typename ParameterType::ValueType,ParameterType::GetValue(Parameter)
		);
}

/**
 * Cache uniform expressions for the given material.
 *  @param MaterialInstance - The material instance for which to cache uniform expressions.
 */
void CacheMaterialInstanceUniformExpressions(const UMaterialInstance* MaterialInstance)
{
	// Only cache the unselected + unhovered material instance. Selection color
	// can change at runtime and would invalidate the parameter cache.
	if (MaterialInstance->Resources[0])
	{
		MaterialInstance->Resources[0]->CacheUniformExpressions_GameThread();
	}
}
/**
 * End of code taken from MaterialInstance.cpp
 */

void SetVectorParameterValue(ALandscapeProxy* Landscape, FName ParameterName, FLinearColor Value)
{
	if (Landscape)
	{
		for (int32 Index = 0; Index < Landscape->LandscapeComponents.Num(); ++Index)
		{
			if (Landscape->LandscapeComponents[Index])
			{
				UMaterialInstanceConstant* MIC = Landscape->LandscapeComponents[Index]->MaterialInstance;
				if (MIC)
				{
					/**
					 * Start of code taken from UMaterialInstance::SetSetVectorParameterValueInternal and adjusted to use MIC instead of this
					 */
					 FVectorParameterValue* ParameterValue = GameThread_FindParameterByName( //from MaterialInstanceSupport.h
					 	MIC->VectorParameterValues,
					 	ParameterName
						);

					if(!ParameterValue)
					{
						// If there's no element for the named parameter in array yet, add one.
						ParameterValue = new(MIC->VectorParameterValues) FVectorParameterValue;
						ParameterValue->ParameterName = ParameterName;
						ParameterValue->ExpressionGUID.Invalidate();
						// Force an update on first use
						ParameterValue->ParameterValue.B = Value.B - 1.f;
					}

					// Don't enqueue an update if it isn't needed
					if (ParameterValue->ParameterValue != Value)
					{
						ParameterValue->ParameterValue = Value;
						// Update the material instance data in the rendering thread.
						GameThread_UpdateMIParameter(MIC, *ParameterValue);
						CacheMaterialInstanceUniformExpressions(MIC);
					}
					/**
					 * End of code taken from UMaterialInstance::SetSetVectorParameterValueInternal and adjusted to use MIC instead of this
					 */
				}
			}
		}
	}
}

// Called when the game starts or when spawned
void AGameDirector::BeginPlay()
{
	Super::BeginPlay();
	SetVectorParameterValue(terrain, TEXT("DynamicFadeControl"), FLinearColor(0.4, 0.4, 0.4, 1.0)); //<---- makes the grid on the terrain semitransparent
}

(GameDirector is my actor which manages the whole game)
I have a similar issue that also needs to be solved, but I will post it and annotate the link onto this post.
https://forums.unrealengine.com/showthread.php?102905-Set-Texture-of-Landscape-s-Material&p=486742#post486742

1 Like

Ah yeah, good point. Landscape shaders are recompiled so that there’s a shader per chunk or something. I haven’t really played with it in relation to landscapes but it seems like a real challenge. Still, I would assume that fetching the material instance and setting a parameter value would work. I can try tonight if you’re still stuck and at least confirm that it’s not working.

Edit: I see the post after that one that CodeCube was successful, so that’s good. :slight_smile: