Change Landscape Material/Textures at Runtime?

I know UE’s landscapes are perfectly static and there is generally no way to create or modify them at runtime without the editor.

I got used to the fact that I can’t alter any height- or material-weight data at runtime (with a heavy heart…), but I still haven’t given up on trying to exchange the entire landscape-material instead. The point is to be able to apply a snowy material when it’s snowing, a “wet” material when it’s raining etc.

I tried a couple of things, but without success. Unfortunately, dynamic material instances don’t seem to work with landscapes.

Then I thought I had found a workaround by iterating through a landscape’s components, setting their override-material to whatever I want them to use, and then calling their member-function UpdateMaterialInstances(). But as it turns out, that function is editor-only as well :frowning:

Does anyone have a workaround for this?

1 Like

AFAIK, you can’t change those at runtime. Why don’t you make snow and wetness as a part of your landscape material though?

Actually I found a kind of workaround to use dynamic material instances with landscape components. It’s dirty, but it works…

Hi MaxPower42,
Im trying to change materials for my landscape aswell. Would you mind to share some of your code? Or do you have some more detailed explanation. I would really appreciate it!! Thanks alot!!

So…

  1. I haven’t actually used the code in a long time, so there is a chance that it doesn’t even work anymore.
  2. It’s using a kind of hacky cast, because I set an MID where an MIC is expected. I didn’t have any problems at all with it, but if your computer explodes, don’t blame me, you have been warned…

That said, if you have a pointer “Landscape” to your landscape-actor, you could try something like this:

	for (int i = 0; i < Landscape->LandscapeComponents.Num(); i++)
	{
		ULandscapeComponent* C = Landscape->LandscapeComponents[i];

		if (C->IsRenderStateCreated())
		{
			C->MarkRenderStateDirty();
			FlushRenderingCommands();
		}
		for (int j = 0; j < C->MaterialInstances.Num(); j++)
		{
			if (!C->MaterialInstances[j]->IsA(UMaterialInstanceDynamic::StaticClass()))
			{
				C->MaterialInstances[j] = (UMaterialInstanceConstant*)UMaterialInstanceDynamic::Create(C->MaterialInstances[j], GetTransientPackage());// HACKY CAST!
			}
			UMaterialInstanceDynamic* MID = (UMaterialInstanceDynamic*)C->MaterialInstances[j];

			for (int k = 0; k < Env->Textures.Num(); k++)
			{
				MID->SetTextureParameterValue(pn[k], Env->Textures[k]);
			}
		}
		C->RecreateRenderState_Concurrent();
	}

“pn” is an array of specific texture-parameter names to set and Env is an “Environment Descriptor”(c) class instance that has a specific array of ground texures and other stuff.

This does not change anything about the material weighting or even height data on the landscape, only replaces the weighted materials with different and dynamic ones. If you want to achieve the former, I don’t know if it is possible. I think you can access the height and weight data textures somehow, but I wouldn’t be too optimistic that you can change them as easily.

Thanks alot!! I will try it out in a couple of days!!

OK, so I managed to do it and it goes like this. The landscape itself has to have “Use dynamic material instance” option checked.

You do have to create a material instance with a proper parameter ofc.

In 5.1, there is a much simpler way to do this now (and it should work when shipping):

On the Landscape actor, in the Landscape section, is a flag called “Use Dynamic Material Instance”. If this is checked, it will generate the dynamic materials for you, and then you can use the above component loop to gather the various material instances for updating. You can call GetMaterialInstanceDynamic() in either Blueprint or C++ for this purpose.

Note: these will allow you to update material parameters on a per-component basis. However, sometimes you may want to change something across all the components (which is the most likely use-case,) you can just call:

ALandscapeProxy::SetLandscapeMaterialTextureParameterValue()
ALandscapeProxy::SetLandscapeMaterialVectorParameterValue()
ALandscapeProxy::SetLandscapeMaterialScalarParameterValue()

The above calls are Blueprint-able and should be invoked from the ALandscape actor (which derives from ALandscapeProxy) Note: these still require the “Use Dynamic Material Instance” flag to be true.

1 Like

Hi @NinjaCatFail , could you, please go trough the process of changing the material for landscape, cause I don’t quite get it. I’ve been trying to get landscape from my player controller and the change the material in it, but it does not work. If you could give an example, I would be grateful.

Are you trying to change the Material at runtime? This is not supported without significant Engine changes. The engine WILL generate dynamic materials for you at runtime if you set the flag, as I mentioned above.

Ok,

Here is a quick rundown of this working, with the caveat that I assume you have used Unreal a bit:

  1. New Level (Basic)
    A. Delete Floor Actor.
    B. Set Editor to Landscape Mode (Shift+2)
    C. Create Landscape with default settings.
    D. Back to Selection Mode (Shift+1)
  2. In the Content Browser, Create New Material (M_Landscape_Example)
  3. Open M_Landscape_Example Material Editor
    A. Press ‘3’ and Left Click in the editor to create a Constant 3 Vector (or create it with the menu.)
    B. Drag the Vector RGB Pin to the Material Output’s Base Color Pin.
    C. Right-click on the Color Vector and convert it to a Parameter (keep it named ‘Param’)
    D. Set the Color to anything other than black as default.
    E. Save and Close the Material Editor
  4. In the Content Browser, right Click the new material (M_Landscape_Example) and create a Material Instance out of it (M_Landscape_Example_Inst)
  5. Select the Landscape actor in the Map Editor, find the Landscape Material property, and set it to the Material Instance (M_Landscape_Example_Inst) you just created. (At this point, the landscape should show your selected color.)
  6. Find the “Use Dynamic Material Instance” property flag a few properties below the Landscape Material property and make sure it is set to TRUE.
  7. Now, Create a new Blueprint in the Content Browser. Derive from Actor and call it BP_DynamicLandscapeMaterial.
  8. Drag the new Blueprint Actor into the Map and place it anywhere.
  9. Open the BP_DynamicLandscapeMaterial Blueprint Editor.
    A. Create a new Variable.
    B. Call it Landscape, and set the type to a Landscape Object Reference.
    C. Make the new variable “Instance Editable” in the variable properties.
    D. In the Event Graph, drag the variable into the Blueprint and place a getter.
    E. Convert the getter to a Validated Get and attach the input Pin to BeginPlay
    F. Drag out from the Landscape Pin of the Getter and Find the SetLandscapeMaterialVectorParameterValue Blueprint Node.
    G. Set the Parameter name to “Param”
    H. Change the color to something different than the color in your material.
    I. Compile and Close the Blueprint Editor.
  10. In the Map, select the BP_DynamicLandscapeMaterial Actor, find the Landscape property, and set it to the Landscape in your level.
  11. Play the Map.
  12. Profit (optional.)

Note: there is no way to change the material itself on the Landscape at runtime without massive amounts of C++ engine code, but take heart because you shouldn’t need to do this since you can change textures, scalars, and vectors to your heart’s content on the material that you DO have.

3 Likes
TArray<AActor*> Actors;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ALandscapeProxy::StaticClass(),Actors);
	
	for (AActor* Actor : Actors)
	{
		ALandscapeProxy* LandscapeProxy = Cast<ALandscapeProxy>(Actor);
		ProxyObject = LandscapeProxy;
		LandscapeProxy->SetLandscapeMaterialScalarParameterValue(FName("ScalarParam1"), value);
		LandscapeProxy->SetLandscapeMaterialScalarParameterValue(FName("ScalarParam2"), value);
	}	

I did it like this and worked perfectly. Just don’t forget to check “Use Dynamic Material Instance” in Landscape settings.

Thanks for sharing it in detail. It works. :smiling_face_with_tear:

Can you apply this to a small area? For example, if I create a sphere trace to the ground, i want to apply different texture within the sphear.

Still works! Just tested on 5.3.2. Though, with “Use dynamic material instance” option there is no need for hacky cast anymore) Here is a bit changed code without MID cast to MIC and vice versa:

// in .h file
#include "LandscapeComponent.h"
#include "Landscape.h"

UFUNCTION(BlueprintCallable, Category = "Landscape")
static void UpdateLandscapeMIDs(ALandscape* Landscape, UMaterialInstanceDynamic* NewMID);

// in .cpp file
void YourCppClass::UpdateLandscapeMIDs(ALandscape* Landscape, UMaterialInstanceDynamic* NewMID)
{
	for (ULandscapeComponent* LandscapeComp : Landscape->LandscapeComponents)
	{
		if (LandscapeComp->GetLandscapeProxy())
		{
			if (LandscapeComp->GetLandscapeProxy()->bUseDynamicMaterialInstance)
			{
				int32 NumMaterials = LandscapeComp->MaterialInstancesDynamic.Num();
				LandscapeComp->MaterialInstancesDynamic.Empty(NumMaterials);

				for (int32 i = 0; i < NumMaterials; ++i)
				{
					LandscapeComp->MaterialInstancesDynamic.Add(NewMID);
				}

				if (LandscapeComp->IsRenderStateCreated())
				{
					LandscapeComp->MarkRenderStateDirty();
				}
				else
				{
					LandscapeComp->RecreateRenderState_Concurrent();
				}
			}
		}
	}
}

Here I set all landscape components to one MID for performance reasons as I just want to be able to change materials for whole landscape, but most likely you can set different MIDs for each landscape component… although I havent tested it.