// Copyright Epic Games, Inc. All Rights Reserved. #include "MetaHumanCharacterBodyTextureUtils.h" #include "Subsystem/MetaHumanCharacterSkinMaterials.h" #include "MetaHumanCharacterTextureSynthesis.h" #include "MetaHumanFaceTextureSynthesizer.h" #include "MetaHumanCharacterEditorModule.h" #include "Materials/MaterialInstanceDynamic.h" #include "Engine/Texture2D.h" #include "GenericPlatform/GenericPlatformMath.h" #include "UObject/Package.h" namespace UE::MetaHuman { static int32 GetMappedBodyTextureId(int32 InBodyTextureParam) { constexpr TStaticArray BodyTextureMapping = { 5, 18, 23, 12, 13, 15, 35, 11, 21 }; const int32 MappedBodyTextureId = BodyTextureMapping[InBodyTextureParam]; return MappedBodyTextureId; } static TObjectPtr GetBodyTexture(EBodyTextureType InTextureType, int32 InSkinToneIndex, int32 InSurfaceMapId) { FString TexturePath; switch (InTextureType) { case EBodyTextureType::Body_Basecolor: TexturePath = FString::Printf(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Optional/BodyTextures/T_Skin_V%d_Body_BC.T_Skin_V%d_Body_BC'"), InSkinToneIndex, InSkinToneIndex); break; case EBodyTextureType::Body_Normal: TexturePath = FString::Printf(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Optional/BodyTextures/SurfaceDetail/T_Chr%04d_Body_N.T_Chr%04d_Body_N'"), InSurfaceMapId, InSurfaceMapId); break; case EBodyTextureType::Body_Cavity: TexturePath =FString::Printf(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Optional/BodyTextures/SurfaceDetail/T_Chr%04d_Body_Ca.T_Chr%04d_Body_Ca'"), InSurfaceMapId, InSurfaceMapId); break; case EBodyTextureType::Body_Underwear_Basecolor: TexturePath = FString(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Textures/Shared/1K/T_Chr0000_Body_Underwear_BC.T_Chr0000_Body_Underwear_BC'")); break; case EBodyTextureType::Body_Underwear_Normal: TexturePath = FString(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Textures/Shared/1K/T_Chr0000_Body_Underwear_N.T_Chr0000_Body_Underwear_N'")); break; case EBodyTextureType::Body_Underwear_Mask: TexturePath = FString(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Textures/Shared/1K/T_Underwear_M.T_Underwear_M'")); break; case EBodyTextureType::Chest_Basecolor: TexturePath = FString::Printf(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Optional/BodyTextures/T_Skin_V%d_Chest_BC.T_Skin_V%d_Chest_BC'"), InSkinToneIndex, InSkinToneIndex); break; case EBodyTextureType::Chest_Normal: TexturePath = FString::Printf(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Optional/BodyTextures/SurfaceDetail/T_Chr%04d_Chest_N.T_Chr%04d_Chest_N'"), InSurfaceMapId, InSurfaceMapId); break; case EBodyTextureType::Chest_Cavity: TexturePath = FString::Printf(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Optional/BodyTextures/SurfaceDetail/T_Chr%04d_Chest_Ca.T_Chr%04d_Chest_Ca'"), InSurfaceMapId, InSurfaceMapId); break; case EBodyTextureType::Chest_Underwear_Basecolor: TexturePath = FString(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Textures/Shared/1K/T_Chr0000_Chest_Underwear_BC.T_Chr0000_Chest_Underwear_BC'")); break; case EBodyTextureType::Chest_Underwear_Normal: TexturePath = FString(TEXT("/Script/Engine.Texture2D'/" UE_PLUGIN_NAME "/Textures/Shared/1K/T_Chr0000_Chest_Underwear_N.T_Chr0000_Chest_Underwear_N'")); break; default: check(false) } TObjectPtr BodyTexture = LoadObject(nullptr, *TexturePath); check(BodyTexture); return BodyTexture; } static void GetBiasGain(const FMetaHumanFaceTextureSynthesizer& InFaceTextureSynthesizer, const FVector2f& InSkinUVFromUI, FVector3f& OutBias, FVector3f& OutGain) { FLinearColor LinearColorSkinTone = InFaceTextureSynthesizer.GetSkinTone(InSkinUVFromUI); OutBias[0] = FMath::Pow(LinearColorSkinTone.R, 2.2) * 256; OutBias[1] = FMath::Pow(LinearColorSkinTone.G, 2.2) * 256; OutBias[2] = FMath::Pow(LinearColorSkinTone.B, 2.2) * 256; OutGain = InFaceTextureSynthesizer.GetBodyAlbedoGain(InSkinUVFromUI); } } // namespace UE::MetaHuman int32 FMetaHumanCharacterBodyTextureUtils::GetSkinToneIndex(const FMetaHumanCharacterSkinProperties& InSkinProperties) { return (InSkinProperties.U < 0.5) ? 1 : 2; } int32 FMetaHumanCharacterBodyTextureUtils::GetBodySurfaceMapId(const FMetaHumanCharacterSkinProperties& InSkinProperties) { return UE::MetaHuman::GetMappedBodyTextureId(InSkinProperties.BodyTextureIndex); } void FMetaHumanCharacterBodyTextureUtils::InitBodyTextureData(const FMetaHumanCharacterSkinProperties& InSkinProperties, const TMap& InTextureInfo, TMap>& OutBodyTextures) { if (OutBodyTextures.IsEmpty()) { const bool bMetaHumanContentAvailable = FMetaHumanCharacterEditorModule::IsOptionalMetaHumanContentInstalled(); for (EBodyTextureType TextureType : TEnumRange()) { if (const FMetaHumanCharacterTextureInfo* TextureInfo = InTextureInfo.Find(TextureType)) { // Has high res texture - just create texture int32 TextureSizeX = TextureInfo->SizeX; int32 TextureSizeY = TextureInfo->SizeY; OutBodyTextures.FindOrAdd(TextureType) = CreateBodyTexture(TextureType, TextureSizeX, TextureSizeY); } else if (bMetaHumanContentAvailable) { OutBodyTextures.FindOrAdd(TextureType) = UE::MetaHuman::GetBodyTexture(TextureType, GetSkinToneIndex(InSkinProperties), GetBodySurfaceMapId(InSkinProperties)); } } } } void FMetaHumanCharacterBodyTextureUtils::UpdateBodyTextureSet(const TOptional& InCharacterSkinSettings, const FMetaHumanCharacterSkinProperties& InSkinProperties, TMap& InOutTextureInfo, TMap>& InOutBodyTextures) { if (InCharacterSkinSettings.IsSet()) { const FMetaHumanCharacterSkinProperties& OldSkinProperties = InCharacterSkinSettings.GetValue().Skin; if (OldSkinProperties.BodyTextureIndex != InSkinProperties.BodyTextureIndex) { static constexpr TStaticArray TextureIndexDependentBodyTextures = { EBodyTextureType::Body_Normal, EBodyTextureType::Body_Cavity, EBodyTextureType::Chest_Normal, EBodyTextureType::Chest_Cavity }; for (EBodyTextureType TextureType : TextureIndexDependentBodyTextures) { if (InOutTextureInfo.Contains(TextureType)) { InOutTextureInfo.Remove(TextureType); } InOutBodyTextures[TextureType] = UE::MetaHuman::GetBodyTexture(TextureType, GetSkinToneIndex(InSkinProperties), GetBodySurfaceMapId(InSkinProperties)); } } int32 OldSkinToneIndex = (OldSkinProperties.U < 0.5) ? 1 : 2; int32 NewSkinToneIndex = (InSkinProperties.U < 0.5) ? 1 : 2; if (OldSkinToneIndex != NewSkinToneIndex) { static constexpr TStaticArray SkinToneDependentBodyTextures = { EBodyTextureType::Body_Basecolor, EBodyTextureType::Chest_Basecolor }; for (EBodyTextureType TextureType : SkinToneDependentBodyTextures) { if (InOutTextureInfo.Contains(TextureType)) { InOutTextureInfo.Remove(TextureType); } InOutBodyTextures[TextureType] = UE::MetaHuman::GetBodyTexture(TextureType, GetSkinToneIndex(InSkinProperties), GetBodySurfaceMapId(InSkinProperties)); } } } } void FMetaHumanCharacterBodyTextureUtils::UpdateBodySkinBiasGain(const FMetaHumanFaceTextureSynthesizer& InFaceTextureSynthesizer, FMetaHumanCharacterSkinProperties& InOutSkinProperties) { if (InFaceTextureSynthesizer.IsValid()) { const FVector2f SkinUVFromUI{ InOutSkinProperties.U, InOutSkinProperties.V }; UE::MetaHuman::GetBiasGain(InFaceTextureSynthesizer, SkinUVFromUI, InOutSkinProperties.BodyBias, InOutSkinProperties.BodyGain); } } void FMetaHumanCharacterBodyTextureUtils::GetSkinToneAndUpdateMaterials(const FMetaHumanCharacterSkinProperties& InSkinProperties, const FMetaHumanFaceTextureSynthesizer& InFaceTextureSynthesizer, const TMap>& InBodyTextures, const FMetaHumanCharacterFaceMaterialSet& InFaceMaterialSet, TNotNull InBodyMID) { FVector3f RGBBias; FVector3f RGBGain; if (InFaceTextureSynthesizer.IsValid()) { const FVector2f SkinUVFromUI{ InSkinProperties.U, InSkinProperties.V }; UE::MetaHuman::GetBiasGain(InFaceTextureSynthesizer, SkinUVFromUI, RGBBias, RGBGain); } else { // If texture synthesis not available use last committed bias and gain from character RGBBias = InSkinProperties.BodyBias; RGBGain = InSkinProperties.BodyGain; } InFaceMaterialSet.ForEachSkinMaterial( [RGBBias, RGBGain](EMetaHumanCharacterSkinMaterialSlot, UMaterialInstanceDynamic* Material) { Material->SetScalarParameterValue(TEXT("rbias"), RGBBias[0]); Material->SetScalarParameterValue(TEXT("gbias"), RGBBias[1]); Material->SetScalarParameterValue(TEXT("bbias"), RGBBias[2]); Material->SetScalarParameterValue(TEXT("rgain"), RGBGain[0]); Material->SetScalarParameterValue(TEXT("ggain"), RGBGain[1]); Material->SetScalarParameterValue(TEXT("bgain"), RGBGain[2]); } ); InBodyMID->SetScalarParameterValue(TEXT("rbias"), RGBBias[0]); InBodyMID->SetScalarParameterValue(TEXT("gbias"), RGBBias[1]); InBodyMID->SetScalarParameterValue(TEXT("bbias"), RGBBias[2]); InBodyMID->SetScalarParameterValue(TEXT("rgain"), RGBGain[0]); InBodyMID->SetScalarParameterValue(TEXT("ggain"), RGBGain[1]); InBodyMID->SetScalarParameterValue(TEXT("bgain"), RGBGain[2]); // Set underwear and micro mask params float ShowTopUnderwearParamValue = InSkinProperties.bShowTopUnderwear ? 1.0f : 0.0f; float MicroMaskStrength = ShowTopUnderwearParamValue > 0.0f ? 1.0f : 0.0f; InFaceMaterialSet.ForEachSkinMaterial( [ShowTopUnderwearParamValue, MicroMaskStrength](EMetaHumanCharacterSkinMaterialSlot, UMaterialInstanceDynamic* Material) { Material->SetScalarParameterValue(TEXT("Show Top Underwear"), ShowTopUnderwearParamValue); Material->SetScalarParameterValue(TEXT("Micro MaskC Strength"), MicroMaskStrength); } ); InBodyMID->SetScalarParameterValue(TEXT("Show Top Underwear"), ShowTopUnderwearParamValue); InBodyMID->SetScalarParameterValue(TEXT("Micro MaskB Strength"), MicroMaskStrength); for (const TPair>& BodyTexturePair : InBodyTextures) { const EBodyTextureType TextureType = BodyTexturePair.Key; const TObjectPtr Texture = BodyTexturePair.Value; const FName TextureParameterName = FMetaHumanCharacterSkinMaterials::GetBodyTextureParameterName(TextureType); // Update body and face materials - underwear mask is set on both if (TextureType <= EBodyTextureType::Body_Underwear_Mask) { InBodyMID->SetTextureParameterValue(TextureParameterName, Texture); } if (TextureType >= EBodyTextureType::Body_Underwear_Mask) { InFaceMaterialSet.ForEachSkinMaterial([&TextureParameterName, Texture](EMetaHumanCharacterSkinMaterialSlot, UMaterialInstanceDynamic* Material) { Material->SetTextureParameterValue(TextureParameterName, Texture); }); } } } UTexture2D* FMetaHumanCharacterBodyTextureUtils::CreateBodyTexture(EBodyTextureType InTextureType, int32 InSizeX, int32 InSizeY) { // Sanity check check(InSizeX >= 0 && (InSizeX == InSizeY)); // Order should match the one in EBodyTextureType static constexpr TextureCompressionSettings TextureTypeToCompressionSettings[] = { TC_Default, // Body_Basecolor TC_Normalmap, // Body_Normal TC_Masks, // Body_Cavity TC_Default, // Body_Underwear_Basecolor TC_Normalmap, // Body_Underwear_Normal TC_Masks, // Body_Underwear_Mask TC_Default, // Chest_Basecolor TC_Normalmap, // Chest_Normal TC_Masks, // Chest_Cavity TC_Default, // Chest_Underwear_Basecolor TC_Normalmap // Chest_Underwear_Normal }; static constexpr TextureGroup TextureTypeToTextureGroup[] = { TEXTUREGROUP_Character, // Body_Basecolor TEXTUREGROUP_CharacterNormalMap, // Body_Normal TEXTUREGROUP_CharacterSpecular, // Body_Cavity TEXTUREGROUP_Character, // Body_Underwear_Basecolor TEXTUREGROUP_CharacterNormalMap, // Body_Underwear_Normal TEXTUREGROUP_Character, // Body_Underwear_Mask TEXTUREGROUP_Character, // Chest_Basecolor TEXTUREGROUP_CharacterNormalMap, // Chest_Normal TEXTUREGROUP_CharacterSpecular, // Chest_Cavity TEXTUREGROUP_Character, // Chest_Underwear_Basecolor TEXTUREGROUP_CharacterNormalMap // Chest_Underwear_Normal }; static_assert(UE_ARRAY_COUNT(TextureTypeToCompressionSettings) == static_cast(EBodyTextureType::Count)); static_assert(UE_ARRAY_COUNT(TextureTypeToTextureGroup) == static_cast(EBodyTextureType::Count)); // Create a sensible unique name for the texture to allow easy identification when debugging const FString TextureName = StaticEnum()->GetAuthoredNameStringByValue((int64) InTextureType); const FString CandidateName = FString::Format(TEXT("T_Body_{0}"), { TextureName }); const FName AssetName = MakeUniqueObjectName(GetTransientPackage(), UTexture2D::StaticClass(), FName{ CandidateName }, EUniqueObjectNameOptions::GloballyUnique); // Create texture EPixelFormat PixelFormat = EPixelFormat::PF_B8G8R8A8; UTexture2D* Texture = UTexture2D::CreateTransient(InSizeX, InSizeY, PixelFormat, AssetName); if (Texture) { // Set its properties Texture->CompressionSettings = TextureTypeToCompressionSettings[static_cast(InTextureType)]; Texture->AlphaCoverageThresholds.W = 1.0f; // Set texture to the "Character" texture group (rather than the default "World") Texture->LODGroup = TextureTypeToTextureGroup[static_cast(InTextureType)]; if (InTextureType == EBodyTextureType::Body_Underwear_Basecolor || InTextureType == EBodyTextureType::Chest_Underwear_Basecolor) { Texture->SRGB = true; } else { Texture->SRGB = false; } } return Texture; }