Files
UnrealEngine/Engine/Plugins/MetaHuman/MetaHumanCharacter/Source/MetaHumanDefaultPipeline/Private/Item/MetaHumanGroomPipeline.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

294 lines
10 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Item/MetaHumanGroomPipeline.h"
#include "MetaHumanDefaultPipelineLog.h"
#include "MetaHumanItemEditorPipeline.h"
#include "Algo/Find.h"
#include "GroomAsset.h"
#include "GroomBindingAsset.h"
#include "GroomComponent.h"
#include "Logging/StructuredLog.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Engine/Texture.h"
UMetaHumanGroomPipeline::UMetaHumanGroomPipeline()
{
// Initialize the specification
{
Specification = CreateDefaultSubobject<UMetaHumanCharacterPipelineSpecification>("Specification");
Specification->BuildOutputStruct = FMetaHumanGroomPipelineBuildOutput::StaticStruct();
Specification->AssemblyInputStruct = FMetaHumanGroomPipelineAssemblyInput::StaticStruct();
Specification->AssemblyOutputStruct = FMetaHumanGroomPipelineAssemblyOutput::StaticStruct();
}
}
#if WITH_EDITOR
void UMetaHumanGroomPipeline::SetDefaultEditorPipeline()
{
EditorPipeline = nullptr;
const TSubclassOf<UMetaHumanItemEditorPipeline> EditorPipelineClass = GetEditorPipelineClass();
if (EditorPipelineClass)
{
EditorPipeline = NewObject<UMetaHumanItemEditorPipeline>(this, EditorPipelineClass);
}
}
const UMetaHumanItemEditorPipeline* UMetaHumanGroomPipeline::GetEditorPipeline() const
{
// If there's no editor pipeline instance, we can use the Class Default Object, because
// pipelines are stateless and won't be modified when used.
//
// This is unfortunately a slow path, as it involves looking the class up by name. We could
// cache this if it becomes a performance issue.
if (!EditorPipeline)
{
const TSubclassOf<UMetaHumanItemEditorPipeline> EditorPipelineClass = GetEditorPipelineClass();
if (EditorPipelineClass)
{
return EditorPipelineClass.GetDefaultObject();
}
}
return EditorPipeline;
}
TSubclassOf<UMetaHumanItemEditorPipeline> UMetaHumanGroomPipeline::GetEditorPipelineClass() const
{
const TSoftClassPtr<UMetaHumanItemEditorPipeline> SoftEditorPipelineClass(FSoftObjectPath(TEXT("/Script/MetaHumanDefaultEditorPipeline.MetaHumanGroomEditorPipeline")));
return SoftEditorPipelineClass.Get();
}
#endif
void UMetaHumanGroomPipeline::AssembleItem(
const FMetaHumanPaletteItemPath& BaseItemPath,
const TArray<FMetaHumanPipelineSlotSelectionData>& SlotSelections,
const FMetaHumanPaletteBuiltData& ItemBuiltData,
const FInstancedStruct& AssemblyInput,
TNotNull<UObject*> OuterForGeneratedObjects,
const FOnAssemblyComplete& OnComplete) const
{
const FInstancedStruct& BuildOutput = ItemBuiltData.ItemBuiltData[BaseItemPath].BuildOutput;
if (!BuildOutput.GetPtr<FMetaHumanGroomPipelineBuildOutput>())
{
UE_LOGFMT(LogMetaHumanDefaultPipeline, Error, "Build output not provided to Groom pipeline during assembly");
OnComplete.ExecuteIfBound(FMetaHumanAssemblyOutput());
return;
}
if (!AssemblyInput.GetPtr<FMetaHumanGroomPipelineAssemblyInput>())
{
UE_LOGFMT(LogMetaHumanDefaultPipeline, Error, "Assembly input not provided to Groom pipeline during assembly");
OnComplete.ExecuteIfBound(FMetaHumanAssemblyOutput());
return;
}
const FMetaHumanGroomPipelineBuildOutput& GroomBuildOutput = BuildOutput.Get<FMetaHumanGroomPipelineBuildOutput>();
const FMetaHumanGroomPipelineAssemblyInput& GroomAssemblyInput = AssemblyInput.Get<FMetaHumanGroomPipelineAssemblyInput>();
FMetaHumanAssemblyOutput AssemblyOutput;
FMetaHumanInstanceParameterOutput InstanceParameterOutput;
FMetaHumanGroomPipelineAssemblyOutput& GroomAssemblyOutput = AssemblyOutput.PipelineAssemblyOutput.InitializeAs<FMetaHumanGroomPipelineAssemblyOutput>();
if (!GroomBuildOutput.bRequiresBinding)
{
// This groom is already into the skin material and doesn't need a binding
OnComplete.ExecuteIfBound(MoveTemp(AssemblyOutput));
return;
}
const TObjectPtr<UGroomBindingAsset>* GroomBindingPtr = GroomBuildOutput.Bindings.FindByPredicate(
[TargetMesh = GroomAssemblyInput.TargetMesh](const TObjectPtr<UGroomBindingAsset>& Binding)
{
return Binding
&& TargetMesh == Binding->GetTargetSkeletalMesh();
});
if (!GroomBindingPtr)
{
UE_LOGFMT(LogMetaHumanDefaultPipeline, Error, "The requested skeletal mesh ({Mesh}) was not found in the Groom pipeline's build output", GetPathNameSafe(GroomAssemblyInput.TargetMesh));
OnComplete.ExecuteIfBound(FMetaHumanAssemblyOutput());
return;
}
GroomAssemblyOutput.Binding = *GroomBindingPtr;
// TODO: Could initialise this only if there are parameters
FMetaHumanGroomPipelineParameterContext& ParameterContext = InstanceParameterOutput.ParameterContext.InitializeAs<FMetaHumanGroomPipelineParameterContext>();
TSet<FName> UsedMaterialNames;
if (GroomAssemblyOutput.Binding)
{
if (const UGroomAsset* Groom = GroomAssemblyOutput.Binding->GetGroom())
{
const TArray<FHairGroupsMaterial>& HairGroupMaterials = Groom->GetHairGroupsMaterials();
for (int32 SlotIndex = 0; SlotIndex < HairGroupMaterials.Num(); ++SlotIndex)
{
const FHairGroupsMaterial& GroupMaterial = HairGroupMaterials[SlotIndex];
ParameterContext.AvailableSlots.Add(GroupMaterial.SlotName);
if (GroomAssemblyOutput.OverrideMaterials.Contains(GroupMaterial.SlotName))
{
// A slot with the same name has already been processed.
//
// We can only support one slot for each slot name.
continue;
}
TObjectPtr<UMaterialInterface> AssemblyMaterial = GroupMaterial.Material;
const TObjectPtr<UMaterialInterface>* PipelineMaterialOverride = OverrideMaterials.Find(GroupMaterial.SlotName);
if (PipelineMaterialOverride != nullptr)
{
AssemblyMaterial = *PipelineMaterialOverride;
}
if (!AssemblyMaterial)
{
// No material is assigned to this slot
continue;
}
bool bNewMaterial = false;
if (!AssemblyMaterial->IsA<UMaterialInstanceDynamic>())
{
bNewMaterial = true;
AssemblyMaterial = UMaterialInstanceDynamic::Create(AssemblyMaterial, nullptr);
}
UMaterialInstanceDynamic* AssemblyMaterialDynamic = CastChecked<UMaterialInstanceDynamic>(AssemblyMaterial);
OverrideInitialMaterialValues(AssemblyMaterialDynamic, GroupMaterial.SlotName, SlotIndex);
const TArray<FMetaHumanMaterialParameter> MaterialParamsForThisSlot = RuntimeMaterialParameters.FilterByPredicate(
[SlotName = GroupMaterial.SlotName, SlotIndex](const FMetaHumanMaterialParameter& Parameter)
{
switch (Parameter.SlotTarget)
{
case EMetaHumanRuntimeMaterialParameterSlotTarget::SlotNames:
return Parameter.SlotNames.Contains(SlotName);
case EMetaHumanRuntimeMaterialParameterSlotTarget::SlotIndices:
return Parameter.SlotIndices.Contains(SlotIndex);
default:
return false;
}
});
const bool bSuccessful = UE::MetaHuman::MaterialUtils::ParametersToPropertyBag(
AssemblyMaterialDynamic,
MaterialParamsForThisSlot,
InstanceParameterOutput.Parameters);
if (!bSuccessful)
{
continue;
}
if (bNewMaterial)
{
FString StrippedMaterialName;
{
StrippedMaterialName = AssemblyMaterial->GetName();
constexpr FStringView PrefixMID = TEXTVIEW("MID_");
constexpr FStringView PrefixMI = TEXTVIEW("MI_");
// UMaterialInstanceDynamic::Create, called above, adds "MID_" to the front
// of the parent material name, so a well named Material Instance Constant
// will end up as "MID_MI_...".
//
// This code aims to strip both of these prefixes.
if (StrippedMaterialName.StartsWith(PrefixMID))
{
StrippedMaterialName = StrippedMaterialName.RightChop(PrefixMID.Len());
}
if (StrippedMaterialName.StartsWith(PrefixMI))
{
StrippedMaterialName = StrippedMaterialName.RightChop(PrefixMI.Len());
}
}
FName GeneratedName = FName(FString::Format(TEXT("MI_{0}_{1}"),
{ BaseItemPath.GetPathEntry(BaseItemPath.GetNumPathEntries() - 1).ToAssetNameString(), StrippedMaterialName }));
// Start with no number, then increment if there's a collision
GeneratedName.SetNumber(0);
while (UsedMaterialNames.Contains(GeneratedName))
{
GeneratedName.SetNumber(GeneratedName.GetNumber() + 1);
}
UsedMaterialNames.Add(GeneratedName);
AssemblyOutput.Metadata.Emplace(AssemblyMaterial, TEXT("Grooms"), GeneratedName.ToString());
AssemblyMaterial->Rename(nullptr, OuterForGeneratedObjects);
}
ParameterContext.MaterialSlotToMaterialInstance.Add(GroupMaterial.SlotName, AssemblyMaterialDynamic);
if (AssemblyMaterial != GroupMaterial.Material)
{
GroomAssemblyOutput.OverrideMaterials.Add(GroupMaterial.SlotName, AssemblyMaterial);
}
}
}
}
if (InstanceParameterOutput.Parameters.IsValid())
{
AssemblyOutput.InstanceParameters.Add(BaseItemPath, MoveTemp(InstanceParameterOutput));
}
OnComplete.ExecuteIfBound(MoveTemp(AssemblyOutput));
}
void UMetaHumanGroomPipeline::SetInstanceParameters(const FInstancedStruct& ParameterContext, const FInstancedPropertyBag& Parameters) const
{
if (!ParameterContext.GetPtr<FMetaHumanGroomPipelineParameterContext>())
{
// Can't do anything without context
return;
}
const FMetaHumanGroomPipelineParameterContext& GroomParameterContext = ParameterContext.Get<FMetaHumanGroomPipelineParameterContext>();
UE::MetaHuman::MaterialUtils::SetInstanceParameters(
RuntimeMaterialParameters,
GroomParameterContext.MaterialSlotToMaterialInstance,
GroomParameterContext.AvailableSlots,
Parameters);
}
TNotNull<const UMetaHumanCharacterPipelineSpecification*> UMetaHumanGroomPipeline::GetSpecification() const
{
return Specification;
}
void UMetaHumanGroomPipeline::ApplyGroomAssemblyOutputToGroomComponent(const FMetaHumanGroomPipelineAssemblyOutput& GroomAssemblyOutput, UGroomComponent* GroomComponent)
{
GroomComponent->SetGroomAsset(GroomAssemblyOutput.Binding ? GroomAssemblyOutput.Binding->GetGroom() : nullptr, GroomAssemblyOutput.Binding);
GroomComponent->EmptyOverrideMaterials();
for (const TPair<FName, TObjectPtr<UMaterialInterface>>& OverrideMaterial : GroomAssemblyOutput.OverrideMaterials)
{
const int32 MaterialIndex = GroomComponent->GetMaterialIndex(OverrideMaterial.Key);
if (MaterialIndex != INDEX_NONE)
{
GroomComponent->SetMaterial(MaterialIndex, OverrideMaterial.Value);
}
}
}