Finally I got working water. In previous article I was saying about that Epic’s water plugin isn’t working properly especially the Water Body Custom one. I made my own water class which derives from that one. Some time ealier I wanted to code it in the way that user can just place water and he will not have to place manually the Physics Volume to make water functional.

Now it is done automatically. You are placing the water, and boom. Done.

Little presentation

I done additional things like:

  • Lack of need making separate materials for every water placed in the level
  • Changing water properties in real time and it is applying into the game without any additional materials

Here you can see the possibilities

User is able to change water surface color, udnerwater fog color, scattering, fog density and much more. I think I should expand this for many other options.

How it works?

This is very simple because it still uses the Epic’s water and underwater post process materials. My code only make a separate Dynamic ones. On actor placing or initialization editor is making dynamic instance of material and when you are changing properties it applies to that dynamic one. Properties are saved so when game is builed or you want to go into play mode, code is making the same as in the editor. I had to separate the editor things and game things in code with #if WITH_EDITOR tags. I am proud of myself because it is the thing no one had a solution for but I found one and made it even better.

DXWaterBodyCustom.cpp


// DX STUDIO 2024
#include "Entities/Actors/DXWaterBodyCustom.h"
#include "WaterBodyCustomComponent.h"

#if WITH_EDITOR #include "Builders/CubeBuilder.h" #include "Editor.h" #include "ActorFactories/ActorFactory.h" #endif

#include "Components/SplineComponent.h" #include "WaterSplineComponent.h" #include "Components/BrushComponent.h"

ADXWaterBodyCustom::ADXWaterBodyCustom() { #if WITH_EDITOR

if(WaterBodyComponent)
{
	CreateEditorDynamicWaterMaterial();		
}

#endif }

void ADXWaterBodyCustom::BeginPlay() { Super::BeginPlay();

CreateMaterialsFromProperties();

}

// This is called when actor is already placed onto the level and level is loaded up. void ADXWaterBodyCustom::PostLoad() { Super::PostLoad(); }

#if WITH_EDITOR void ADXWaterBodyCustom::OnConstruction(const FTransform& Transform) { Super::OnConstruction(Transform);

if (SplineComp)
{
	USplineComponent* Spline = Cast<USplineComponent>(SplineComp);
	if(Spline)
	{
		Spline->ClearSplinePoints();
	}

}
if(WaterPhysicsVolume && WaterPhysicsVolume->BrushBuilder) 
{
	WaterPhysicsVolume->GetOnVolumeShapeChangedDelegate().Broadcast(*WaterPhysicsVolume);
}

if(WaterBodyComponent && WaterBodyComponent->GetWaterMaterial() == nullptr)
{
	//UE_LOG(LogActor, Error, TEXT("WaterBodyComponent material is nullptr in: %s, trying to bring back the defaults and recreate MIDs"), *GetActorLabel());

	BringDefaultMats();
	CreateEditorDynamicWaterMaterial();
}

}

void ADXWaterBodyCustom::PostActorCreated() { Super::PostActorCreated(); if (WaterBodyComponent) { UMaterialInterface* Mat = Cast<UMaterialInterface>(WaterMaterialPath.TryLoad()); UMaterialInterface* MatPost = Cast<UMaterialInterface>(UnderwaterPostProcessMaterialPath.TryLoad()); UStaticMesh* Mesh = Cast<UStaticMesh>(WaterMeshPath.TryLoad());

	WaterBodyComponent-&gt;SetWaterAndUnderWaterPostProcessMaterial(Mat, MatPost);
	WaterBodyComponent-&gt;SetWaterMeshOverride(Mesh);

	CreateEditorDynamicWaterMaterial();
	CreateWaterVolume();
}

}

void ADXWaterBodyCustom::Destroyed() { Super::Destroyed();

if (WaterPhysicsVolume)
{
	WaterPhysicsVolume-&gt;Destroy();
	WaterPhysicsVolume = nullptr;
}

}

#endif

void ADXWaterBodyCustom::CreateMaterialsFromProperties() { if (!WaterBodyComponent) { UE_LOG(LogTemp, Error, TEXT("WaterBodyComponent is nullptr")) return; } #if WITH_EDITOR if (WaterBodyComponent->GetWaterMaterial() == nullptr || WaterBodyComponent->GetUnderwaterPostProcessMaterialInstance() == nullptr) { UE_LOG(LogActor, Error, TEXT("WaterMaterial or UnderwaterPostProcessMaterialInstance is nullptr in: %s, trying to bring back the defaults and recreate MIDs"), *GetActorLabel()); } #endif

BringDefaultMats();

WaterSurfaceMID = UMaterialInstanceDynamic::Create(WaterBodyComponent-&gt;GetWaterMaterial(), this, FName(&quot;WaterSurfaceMID_CPP&quot;));
WaterSurfaceMID-&gt;SetFlags(RF_Transient);
//WaterSurfaceMID-&gt;Parent = Cast&lt;UMaterialInterface&gt;(WaterParent.TryLoad());

if(!WaterSurfaceMID-&gt;Parent)
	UE_LOG(LogActor, Error, TEXT(&quot;Failed to load WaterParent material&quot;))

WaterSurfaceMID-&gt;SetVectorParameterValue(&quot;Absorption&quot;, WaterAbsorptionColor);
WaterSurfaceMID-&gt;SetVectorParameterValue(&quot;Scattering&quot;, WaterScatteringColor);
WaterSurfaceMID-&gt;SetScalarParameterValue(&quot;Refraction&quot;, Refraction);
WaterSurfaceMID-&gt;SetScalarParameterValue(&quot;Roughness&quot;, WaterRoughness);
WaterSurfaceMID-&gt;SetScalarParameterValue(&quot;Specular&quot;, WaterSpecular);
WaterSurfaceMID-&gt;SetScalarParameterValue(&quot;Default Near Normal Strength&quot;, WaterNormalStrenght_Near);

WaterBodyComponent-&gt;SetWaterMaterial(WaterSurfaceMID);

UMaterialInterface* MatPost = Cast&lt;UMaterialInterface&gt;(UnderwaterPostProcessMaterialPath.TryLoad());
UnderwaterPostProcessMID = UMaterialInstanceDynamic::Create(MatPost, this, FName(&quot;UnderwaterPostProcessMID_CPP&quot;));
UnderwaterPostProcessMID-&gt;SetFlags(RF_Transient);
UnderwaterPostProcessMID-&gt;SetScalarParameterValue(&quot;WaterZLine&quot;, GetActorLocation().Z);
UnderwaterPostProcessMID-&gt;SetScalarParameterValue(&quot;Fog&quot;, WaterFogDensity);
UnderwaterPostProcessMID-&gt;SetVectorParameterValue(&quot;Fog Ambient Color&quot;, UnderwaterFogAmbient);
UnderwaterPostProcessMID-&gt;SetVectorParameterValue(&quot;Fog Scatter Color&quot;, UnderwaterFogScatter);
UnderwaterPostProcessMID-&gt;SetVectorParameterValue(&quot;Absorption&quot;, Absorption);

WaterBodyComponent-&gt;SetUnderwaterPostProcessMaterial(UnderwaterPostProcessMID);

}

void ADXWaterBodyCustom::BringDefaultMats() {

UMaterialInterface* Mat = Cast&lt;UMaterialInterface&gt;(WaterMaterialPath.TryLoad());
UMaterialInterface* MatPost = Cast&lt;UMaterialInterface&gt;(UnderwaterPostProcessMaterialPath.TryLoad());

if (WaterBodyComponent-&gt;GetWaterMaterial() == nullptr)
{
	WaterBodyComponent-&gt;SetWaterMaterial(Mat);

#if WITH_EDITOR UE_LOG(LogActor, Log, TEXT("%s’s Water material set to default: %s"), *GetActorLabel(), *WaterBodyComponent->GetWaterMaterial()->GetFullName()); #endif }

if (WaterBodyComponent-&gt;GetWaterMaterial() != nullptr &amp;&amp; WaterBodyComponent-&gt;GetWaterMaterial()-&gt;GetFullName() != WaterMaterialPath.ToString())
{
	WaterBodyComponent-&gt;SetWaterMaterial(Mat);

#if WITH_EDITOR UE_LOG(LogActor, Log, TEXT("%s’s Water material set to default: %s"), *GetActorLabel(), *WaterBodyComponent->GetWaterMaterial()->GetFullName()); #endif }

}

#if WITH_EDITOR void ADXWaterBodyCustom::CreateEditorDynamicWaterMaterial() { WaterSurfaceMID_Editor = UMaterialInstanceDynamic::Create(WaterBodyComponent->GetWaterMaterial(), this, FName("WaterEditorMIDTest")); UE_LOG(LogActor, Log, TEXT("WaterSurfaceMID_Editor created: %s"), *WaterSurfaceMID_Editor->GetFullName());

UMaterialInterface* UnderMat = Cast&lt;UMaterialInterface&gt;(UnderwaterPostProcessMaterialPath.TryLoad());
UnderWaterPostProcessMID_Editor = UMaterialInstanceDynamic::Create(UnderMat, this, FName(&quot;UnderwaterEditorMIDTest&quot;));
UE_LOG(LogActor, Log, TEXT(&quot;UnderWaterPostProcessMID_Editor created: %s&quot;), *UnderWaterPostProcessMID_Editor-&gt;GetFullName());

if (WaterSurfaceMID_Editor)
{
	WaterSurfaceMID_Editor-&gt;SetFlags(RF_Transient);
	WaterSurfaceMID_Editor-&gt;SetVectorParameterValue(&quot;Absorption&quot;, WaterAbsorptionColor);
	WaterSurfaceMID_Editor-&gt;SetVectorParameterValue(&quot;Scattering&quot;, WaterScatteringColor);
	WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Refraction&quot;, Refraction);
	WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Roughness&quot;, WaterRoughness);
	WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Specular&quot;, WaterSpecular);
	WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Default Near Normal Strength&quot;, WaterNormalStrenght_Near);



	WaterBodyComponent-&gt;SetWaterMaterial(WaterSurfaceMID_Editor);
}
else
	UE_LOG(LogActor, Error, TEXT(&quot;%s: Failed to create WaterSurfaceMID_Editor&quot;), *GetActorLabel());

if (UnderWaterPostProcessMID_Editor)
{
	UnderWaterPostProcessMID_Editor-&gt;SetFlags(RF_Transient);
	UnderWaterPostProcessMID_Editor-&gt;SetScalarParameterValue(&quot;WaterZLine&quot;, GetActorLocation().Z);
	UnderWaterPostProcessMID_Editor-&gt;SetScalarParameterValue(&quot;Fog&quot;, WaterFogDensity);
	UnderWaterPostProcessMID_Editor-&gt;SetVectorParameterValue(&quot;Fog Ambient Color&quot;, UnderwaterFogAmbient);
	UnderWaterPostProcessMID_Editor-&gt;SetVectorParameterValue(&quot;Fog Scatter Color&quot;, UnderwaterFogScatter);
	UnderWaterPostProcessMID_Editor-&gt;SetVectorParameterValue(&quot;Absorption&quot;, Absorption);

	WaterBodyComponent-&gt;SetUnderwaterPostProcessMaterial(UnderWaterPostProcessMID_Editor);
}
else
	UE_LOG(LogActor, Error, TEXT(&quot;%s: Failed to create UnderWaterPostProcessMID_Editor&quot;), *GetActorLabel());

}

void ADXWaterBodyCustom::CreateWaterVolume() { FActorSpawnParameters PhysicsVolumeSpawnParams;

if (!WaterPhysicsVolume)
{/*
	WaterPhysicsVolume = GetWorld()-&gt;SpawnActor&lt;APhysicsVolume&gt;(GetActorLocation(), FRotator::ZeroRotator, PhysicsVolumeSpawnParams);
	WaterPhysicsVolume-&gt;SetActorLabel(FString::Printf(TEXT(&quot;%s_PhysicsVolume&quot;), *GetName()));

	ABrush* NewBrush = GetWorld()-&gt;SpawnBrush();
	NewBrush-&gt;BrushType = Brush_Default;
	NewBrush-&gt;BrushBuilder = NewObject&lt;UCubeBuilder&gt;(NewBrush, UCubeBuilder::StaticClass());
	UCubeBuilder* Builder = Cast&lt;UCubeBuilder&gt;(NewBrush-&gt;BrushBuilder);

	Builder-&gt;X = 128.0f;
	Builder-&gt;Y = 128.0f;
	Builder-&gt;Z = 128.0f;

	WaterPhysicsVolume-&gt;BrushBuilder = Builder;
	Builder-&gt;Build(GetWorld(), WaterPhysicsVolume);

	WaterPhysicsVolume-&gt;Brush = NewObject&lt;UModel&gt;(WaterPhysicsVolume, UModel::StaticClass());
	WaterPhysicsVolume-&gt;GetBrushComponent()-&gt;BuildSimpleBrushCollision();
	WaterPhysicsVolume-&gt;ReregisterAllComponents();

	UE_LOG(LogTemp, Warning, TEXT(&quot;WaterPhysicsVolume created, verts: %i&quot;), WaterPhysicsVolume-&gt;Brush-&gt;NumUniqueVertices);*/

	UObject* PhysVolumeObj = FindObject&lt;UObject&gt;(nullptr, TEXT(&quot;/Script/Engine.PhysicsVolume&quot;));
	UActorFactory* PhysVolumeFactory = GEditor-&gt;FindActorFactoryForActorClass(APhysicsVolume::StaticClass());
	FTransform Transform = GetActorTransform();

	WaterPhysicsVolume = Cast&lt;APhysicsVolume&gt;(PhysVolumeFactory-&gt;CreateActor(PhysVolumeObj, GetWorld()-&gt;PersistentLevel, Transform));
	WaterPhysicsVolume-&gt;bWaterVolume = true;
	WaterPhysicsVolume-&gt;SetActorLabel(FString::Printf(TEXT(&quot;%s_PhysicsVolume&quot;), *GetName()));
	WaterPhysicsVolume-&gt;AttachToActor(this, FAttachmentTransformRules::KeepWorldTransform);

	UCubeBuilder* Builder = Cast&lt;UCubeBuilder&gt;(WaterPhysicsVolume-&gt;BrushBuilder);
	Builder-&gt;X = 256;
	Builder-&gt;Y = 256;
	Builder-&gt;Z = 256; // Change to water depth
	

	WaterPhysicsVolume-&gt;BrushBuilder = Builder;
	Builder-&gt;Build(WaterPhysicsVolume-&gt;GetWorld(), WaterPhysicsVolume);

	WaterPhysicsVolume-&gt;GetBrushComponent()-&gt;RequestUpdateBrushCollision();

	
	WaterPhysicsVolume-&gt;SetActorLocation(GetActorLocation() - FVector(0,0,128));
	WaterPhysicsVolume-&gt;GetOnVolumeShapeChangedDelegate().Broadcast(*WaterPhysicsVolume);
	
}

}

void ADXWaterBodyCustom::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent);

if(WaterPhysicsVolume)
	WaterPhysicsVolume-&gt;GetOnVolumeShapeChangedDelegate().Broadcast(*WaterPhysicsVolume);

if(PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, WaterAbsorptionColor) || PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, WaterScatteringColor))
{
		if (WaterSurfaceMID_Editor)
		{
			WaterSurfaceMID_Editor-&gt;SetVectorParameterValue(&quot;Absorption&quot;, WaterAbsorptionColor);
			WaterSurfaceMID_Editor-&gt;SetVectorParameterValue(&quot;Scattering&quot;, WaterScatteringColor);
			WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Refraction&quot;, Refraction);
			WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Roughness&quot;, WaterRoughness);
			WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Specular&quot;, WaterSpecular);
			WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Default Near Normal Strength&quot;, WaterNormalStrenght_Near);

			if (WaterBodyComponent)
			{
				WaterBodyComponent-&gt;SetWaterMaterial(WaterSurfaceMID_Editor);
			}
		}
}
if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, Refraction))
{
	if (WaterSurfaceMID_Editor)
	{
		WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Refraction&quot;, Refraction);

			if (WaterBodyComponent)
			{
				WaterBodyComponent-&gt;SetWaterMaterial(WaterSurfaceMID_Editor);
			}
	}
}
if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, WaterRoughness))
{
			if (WaterSurfaceMID_Editor)
			{
							WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Roughness&quot;, WaterRoughness);

		if (WaterBodyComponent)
		{
							WaterBodyComponent-&gt;SetWaterMaterial(WaterSurfaceMID_Editor);
		}
	}
}
if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, WaterSpecular))
{
			if (WaterSurfaceMID_Editor)
			{
							WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Specular&quot;, WaterSpecular);

		if (WaterBodyComponent)
		{
							WaterBodyComponent-&gt;SetWaterMaterial(WaterSurfaceMID_Editor);
		}
	}
}
if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, WaterNormalStrenght_Near))
{
			if (WaterSurfaceMID_Editor)
			{
							WaterSurfaceMID_Editor-&gt;SetScalarParameterValue(&quot;Default Near Normal Strength&quot;, WaterNormalStrenght_Near);

		if (WaterBodyComponent)
		{
							WaterBodyComponent-&gt;SetWaterMaterial(WaterSurfaceMID_Editor);
		}
	}
}


// Underwater
if (UnderWaterPostProcessMID_Editor)
{
	if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, WaterFogDensity) || PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, UnderwaterFogAmbient) || PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, UnderwaterFogScatter) || PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(ADXWaterBodyCustom, Absorption))
	{
		UnderWaterPostProcessMID_Editor-&gt;SetScalarParameterValue(&quot;WaterZLine&quot;, GetActorLocation().Z);
		UnderWaterPostProcessMID_Editor-&gt;SetScalarParameterValue(&quot;Fog&quot;, WaterFogDensity);
		UnderWaterPostProcessMID_Editor-&gt;SetVectorParameterValue(&quot;Fog Ambient Color&quot;, UnderwaterFogAmbient);
		UnderWaterPostProcessMID_Editor-&gt;SetVectorParameterValue(&quot;Fog Scatter Color&quot;, UnderwaterFogScatter);
		UnderWaterPostProcessMID_Editor-&gt;SetVectorParameterValue(&quot;Absorption&quot;, Absorption);

		if (WaterBodyComponent)
		{
			WaterBodyComponent-&gt;SetUnderwaterPostProcessMaterial(UnderWaterPostProcessMID_Editor);
		}
	}


}

} #endif // WITH_EDITOR

I learned a lot of things, espacially about functions which are executed in the editor. I learned about material dynamic instances and many more. That’s it.

Preview 1

Preview 2

Preview 3 Underwater

Preview 4 Underwater