众所周知,UE4中的材质可以保存成资产,可以在材质编辑器中打开,通过设置材质表达式以及调用材质函数进行编辑。编辑完成后,可以设置在组件上,或者在蓝图中调用。
现在老板告诉你:我不想管理那么多材质文件,不想调用蓝图,能否用C++代码直接生成我想要的材质?
看完这篇文章你可以自信地说:OK。
一、准备知识
- 在材质编辑器中,通过材质表达式构建的材质
最右侧是待编辑的材质,可以看到它有各种属性节点(不是所有节点都同时有效,选择不同的材质域、混合模式、着色模型等,会开启或关闭相应的节点),每种属性的具体性质由左侧连接的材质表达式和材质函数来决定。 - 材质编辑器中的材质函数
带有 Input 前缀的节点表示函数输入,带有 Output 前缀的节点表示函数输出。
其实双击点开材质函数会发现,函数内依然是一堆材质表达式,所以,构建一个材质的基本单位就是材质表达式,无非就是要确定材质表达式的类型,以及它们之间如何是连接的。 材质函数是以资产的形式加载和保存的,在蓝图中可以直接访问,在C++中需要用LoadObject的方式主动加载访问。本篇文章中没有使用UMaterialFunction(主要是没搞懂怎么用,请大神指教),而是把所有节点都展开成UMaterialExpression来实现的。 - 材质表达式在C++中对应的类
你会用的这个目录下的类:
Engine/Source/Runtime/Engine/Classes/Materials
这个目录下包含了各种材质表达式,老多了,他们都是UMaterialExpression的子孙。蓝图中的每一种材质表达式都能在这里找到相应的C++类。 - 在C++中材质表达式的连接
输入节点:UMaterialExpression的子类可能拥有不同数量和名字的输入节点,用FExpressionInput保存,如下:
输出节点:没有输出节点,材质表达式(UMaterialExpression)本身就是下一个节点的输入,可以类比一个链表,链表的每个节点保存有指向上一个节点的指针。你可以看到FExpressionInput类中保存了UMaterialExpression指针,指向输入的材质表达式。
连接示例:所以如果要连接两个材质表达式A和B,你应该把B的某个FExpressionInput中的UmaterialExpression指针置为A。即 B->LightMass.Expression = A;
二、大致思路
- 先用材质编辑器构建出想要的材质,或者已经有现成的材质。
- 将其转换为C++实现。
注意:蓝图是基于C++封装的,所有蓝图一定能在C++中找到相应实现。在C++中实现自定义材质,其实就是用C++生成(New出对象)各种材质表达式,然后连接各节点。虽然材质球从直观的蓝图变成了不那么直观的C++代码,但它们表示的拓扑结构是一致的。 - 在C++中大致分为两步:(1)New出我们需要的表达式对象 (2)按编辑器中的结构去连接它们
三、代码示例
C++动态生成抠像材质
- 语境介绍
UE在播放视频时,会将视频帧Buffer转换成媒体纹理,然后根据媒体纹理构建材质并应用在Mesh上,这样就可以在物体表面播放视频。而我现在想集成视频抠像功能,一个办法就是在纹理转换成材质之前,加入抠像的算法。由于整个视频组件(包括生成Mesh、创建播放器、创建媒体源、播放视频画面…)都是C++实现,所以现在需要在代码中集成自定义抠像材质。(参考文章:在 UE4 中设置色键材质) - 将材质在编辑器中打开,查看我需要在C++中New哪些类?
这是一个现成的解决方案,用蓝图实现。可以看到,最左侧输入是纹理采样(对应UMaterialExpressionTextureSample),它来自于视频媒体纹理,左下角Param指示了抠像键值(对应UMaterialExpressionVectorParameter),绿幕抠像RGB值则是(0.0, 1.0, 0.0)。点开材质函数可以看到:
VectorLength也是材质函数,点开可以看到里面有一个常量和两个Distance:
这里有相当多的节点需要我们去实现。依次查看节点类型,可以发现有材质表达式常量(对应UMaterialExpressionConstant),有材质表达式相减(对应UMaterialExpressionSubtract),有材质表达式自定义ExtractColor(对应UMaterialExpressionCustom),等等。 - 转换成C++代码
在我的组件中,创建媒体纹理和通过纹理创建动态材质实例主要通过 CreateMediaMaterial 和 CreateMaterialFromTexture(创建抠像材质的实现在此函数中,重点关注此函数) 这两个函数实现。
- CreateMediaMaterial
void UPXVideoComponent::CreateMediaMaterial()
{
FString MediaTextureAssetName = GetOwner()->GetFName().ToString() + TEXT("_VideoTexture");
EObjectFlags theFlags = (RF_Public | RF_Transactional);
MediaTexture = NewObject<UMediaTexture>(this, UMediaTexture::StaticClass(), *MediaTextureAssetName, theFlags);
MediaTexture->UpdateResource();
MediaTexture->SetMediaPlayer(MediaPlayer);
UTexture* theTexture = Cast<UTexture>(MediaTexture);
// 通过以下函数由媒体纹理生成媒体材质,在此函数中实现材质的自定义
UObject* theMatObj = CreateMaterialFromTexture(theTexture);
UMaterialInterface* MediaTextureMat = Cast<UMaterialInterface>(theMatObj);
MediaDynamicMaterial = VideoMeshComponent->CreateAndSetMaterialInstanceDynamicFromMaterial(0, MediaTextureMat);
MediaDynamicMaterial->SetScalarParameterValue("Enable Video Alpha", 0.0f);
MediaDynamicMaterial->SetScalarParameterValue("Enable Video Texture", 1.0f);
MediaDynamicMaterial->SetTextureParameterValue(FName(TEXT("VideoTexture")), theTexture);
}
- CreateMaterialFromTexture(重点)
UObject* UPXVideoComponent::CreateMaterialFromTexture(UTexture* UnrealTexture)
{
FString MediaMaterialAssetName = UnrealTexture->GetFName().ToString() + TEXT("_Mat");
EObjectFlags theFlags = (RF_Public | RF_Transactional);
UMaterial* UnrealMaterial = nullptr;
UPackage* Package = nullptr;
UnrealMaterial = NewObject<UMaterial>(this, UMaterial::StaticClass(), *MediaMaterialAssetName, theFlags);
const int X = -340;
const int Y = 0;
// Create a new texture sample expression,
// this is our texture input node into the material output.
// [ 对应材质表达式纹理采样 ]
UMaterialExpressionTextureSample* UnrealTextureExpression = NewObject<UMaterialExpressionTextureSample>(UnrealMaterial);
UnrealTextureExpression->Texture = UnrealTexture;
UnrealTextureExpression->AutoSetSampleType();
UnrealTextureExpression->MaterialExpressionEditorX += X;
UnrealTextureExpression->MaterialExpressionEditorY += Y;
UnrealMaterial->Expressions.Add(UnrealTextureExpression);
UnrealMaterial->EmissiveColor.Expression = UnrealTextureExpression;
UnrealMaterial->SetShadingModel(EMaterialShadingModel::MSM_Unlit);
if (bEnableKeying)
{
// Chroma_Key_Alpha (Simplified)
// - Prepare for connection.
// [ 对应材质表达式向量参数 ]
UMaterialExpressionVectorParameter* ChromaParam = NewObject<UMaterialExpressionVectorParameter>(UnrealMaterial);
ChromaParam->DefaultValue.R = float(ChromaColor.R * 1.0f / 255.0f);
ChromaParam->DefaultValue.G = float(ChromaColor.G * 1.0f / 255.0f);
ChromaParam->DefaultValue.B = float(ChromaColor.B * 1.0f / 255.0f);
// [ 对应材质表达式常量 ]
UMaterialExpressionConstant* ConstantLumaMask = NewObject<UMaterialExpressionConstant>(UnrealMaterial);
ConstantLumaMask->R = 2.0f;
FString ShaderCode = TEXT("float Luma = dot(Color, 1); \
float ColorMask = exp(-Luma * 2 * PI / LumaMask); \
Color = lerp(Color, Luma, ColorMask); \
return Color / (dot(Color, 2));");
FCustomInput Color;
Color.InputName = FName(TEXT("Color"));
FCustomInput LumaMask;
LumaMask.InputName = FName(TEXT("LumaMask"));
// [ 对应材质表达式自定义 ]
UMaterialExpressionCustom* ExtractColor_Chroma = NewObject<UMaterialExpressionCustom>(UnrealMaterial);
ExtractColor_Chroma->Code = ShaderCode;
ExtractColor_Chroma->Inputs.Empty();
ExtractColor_Chroma->Inputs.Add(Color);
ExtractColor_Chroma->Inputs.Add(LumaMask);
ExtractColor_Chroma->OutputType = ECustomMaterialOutputType::CMOT_Float3;
ExtractColor_Chroma->Desc = TEXT("ExtractColor");
// [ 对应材质表达式自定义 ]
UMaterialExpressionCustom* ExtractColor_Image = NewObject<UMaterialExpressionCustom>(UnrealMaterial);
ExtractColor_Image->Code = ShaderCode;
ExtractColor_Image->Inputs.Empty();
ExtractColor_Image->Inputs.Add(Color);
ExtractColor_Image->Inputs.Add(LumaMask);
ExtractColor_Image->OutputType = ECustomMaterialOutputType::CMOT_Float3;
ExtractColor_Image->Desc = TEXT("ExtractColor");
// [ 对应材质表达式相减 ]
UMaterialExpressionSubtract* Subtract = NewObject<UMaterialExpressionSubtract>(UnrealMaterial);
// [ 对应材质表达式常量 ]
UMaterialExpressionConstant* ConstantZero = NewObject<UMaterialExpressionConstant>(UnrealMaterial);
ConstantZero->R = 0.0f;
// [ 对应材质表达式距离 ]
UMaterialExpressionDistance* Distance = NewObject<UMaterialExpressionDistance>(UnrealMaterial);
// - Graph node connection.
// 开始连接各个材质表达式,拓扑结构和材质编辑器中展现的结构保持一致
Distance->A.Expression = ConstantZero;
Distance->B.Expression = Subtract;
Subtract->A.Expression = ExtractColor_Chroma;
Subtract->B.Expression = ExtractColor_Image;
ExtractColor_Chroma->Inputs[0].Input.Expression = ChromaParam;
ExtractColor_Chroma->Inputs[1].Input.Expression = ConstantLumaMask;
ExtractColor_Image->Inputs[0].Input.Expression = UnrealTextureExpression;
ExtractColor_Image->Inputs[1].Input.Expression = ConstantLumaMask;
UnrealMaterial->Expressions.Add(Distance);
UnrealMaterial->OpacityMask.Expression = Distance;
UnrealMaterial->BlendMode = EBlendMode::BLEND_Masked;
}
UnrealMaterial->PostLoad();
return UnrealMaterial;
}
- CreateMaterialFromTextureV2(升级版实现 自动抠像 抠像效果更佳)
UObject* UPXVideoComponent::CreateMaterialFromTextureV2(UTexture* UnrealTexture)
{
FString MediaMaterialAssetName = UnrealTexture->GetFName().ToString() + TEXT("_Mat");
EObjectFlags theFlags = (RF_Public | RF_Transactional);
UMaterial* UnrealMaterial = nullptr;
UnrealMaterial = NewObject<UMaterial>(this, UMaterial::StaticClass(), *MediaMaterialAssetName, theFlags);
const int X = -340;
const int Y = 0;
// Create a new texture sample expression,
UMaterialExpressionTextureCoordinate* UVs = NewObject<UMaterialExpressionTextureCoordinate>(UnrealMaterial);
UVs->CoordinateIndex = 0;
UVs->UTiling = 1.0f;
UVs->VTiling = 1.0f;
UMaterialExpressionTextureSampleParameter2D* Video = NewObject<UMaterialExpressionTextureSampleParameter2D>(UnrealMaterial);
Video->SamplerType = EMaterialSamplerType::SAMPLERTYPE_External;
Video->Texture = UnrealTexture;
Video->Coordinates.Expression = UVs;
Video->MaterialExpressionEditorX += X;
Video->MaterialExpressionEditorY += Y;
UnrealMaterial->Expressions.Add(Video);
UnrealMaterial->EmissiveColor.Expression = Video;
UnrealMaterial->SetShadingModel(EMaterialShadingModel::MSM_Unlit);
if (bEnableKeying)
{
// M_CPCutout
// - Prepare for connection
// -- Emissive Color
UMaterialExpressionComponentMask* Mask_R = NewObject<UMaterialExpressionComponentMask>(UnrealMaterial);
Mask_R->R = 1;
Mask_R->G = 0;
Mask_R->B = 0;
Mask_R->A = 0;
UMaterialExpressionComponentMask* Mask_G = NewObject<UMaterialExpressionComponentMask>(UnrealMaterial);
Mask_G->R = 0;
Mask_G->G = 1;
Mask_G->B = 0;
Mask_G->A = 0;
UMaterialExpressionComponentMask* Mask_B = NewObject<UMaterialExpressionComponentMask>(UnrealMaterial);
Mask_B->R = 0;
Mask_B->G = 0;
Mask_B->B = 1;
Mask_B->A = 0;
UMaterialExpressionAdd* Add = NewObject<UMaterialExpressionAdd>(UnrealMaterial);
UMaterialExpressionDivide* Divide = NewObject<UMaterialExpressionDivide>(UnrealMaterial);
Divide->ConstB = 2.0f;
UMaterialExpressionIf* If = NewObject<UMaterialExpressionIf>(UnrealMaterial);
UMaterialExpressionAppendVector* Append_0 = NewObject<UMaterialExpressionAppendVector>(UnrealMaterial);
UMaterialExpressionAppendVector* Append_1 = NewObject<UMaterialExpressionAppendVector>(UnrealMaterial);
// -- Opacity Mask
UMaterialExpressionTextureObject* TextureObject = NewObject<UMaterialExpressionTextureObject>(UnrealMaterial);
TextureObject->SamplerType = EMaterialSamplerType::SAMPLERTYPE_External;
TextureObject->Texture = UnrealTexture;
UMaterialExpressionTextureCoordinate* TextureCoordinate = NewObject<UMaterialExpressionTextureCoordinate>(UnrealMaterial);
TextureCoordinate->CoordinateIndex = 0;
TextureCoordinate->UTiling = 1.0f;
TextureCoordinate->VTiling = 1.0f;
UMaterialExpressionConstant2Vector* Constant2Vector = NewObject<UMaterialExpressionConstant2Vector>(UnrealMaterial);
Constant2Vector->R = 0.0f;
Constant2Vector->G = 0.0f;
FString CustomCode = TEXT("Tex.Load(int3(Coordinate,0))");
FCustomInput Tex;
Tex.InputName = FName(TEXT("Tex"));
FCustomInput UV;
UV.InputName = FName(TEXT("UV"));
FCustomInput Coordinate;
Coordinate.InputName = FName(TEXT("Coordinate"));
UMaterialExpressionCustom* Custom = NewObject<UMaterialExpressionCustom>(UnrealMaterial);
Custom->Code = CustomCode;
Custom->Inputs.Empty();
Custom->Inputs.Add(Tex);
Custom->Inputs.Add(UV);
Custom->Inputs.Add(Coordinate);
Custom->OutputType = ECustomMaterialOutputType::CMOT_Float3;
Custom->Desc = TEXT("Custom");
UMaterialExpressionVectorParameter* AlphaThresOffset = NewObject<UMaterialExpressionVectorParameter>(UnrealMaterial);
AlphaThresOffset->ParameterName = "AlphaThresOffset";
AlphaThresOffset->DefaultValue.R = 1.0f;
AlphaThresOffset->DefaultValue.G = 0.0f;
AlphaThresOffset->DefaultValue.B = 0.0f;
AlphaThresOffset->DefaultValue.A = 1.0f;
UMaterialExpressionComponentMask* AlphaThresOffset_Mask = NewObject<UMaterialExpressionComponentMask>(UnrealMaterial);
AlphaThresOffset_Mask->R = 1;
AlphaThresOffset_Mask->G = 1;
AlphaThresOffset_Mask->B = 0;
AlphaThresOffset_Mask->A = 0;
UMaterialExpressionVectorParameter* WeightsRB = NewObject<UMaterialExpressionVectorParameter>(UnrealMaterial);
WeightsRB->ParameterName = "WeightsRB";
WeightsRB->DefaultValue.R = 0.5f;
WeightsRB->DefaultValue.G = 0.5f;
WeightsRB->DefaultValue.B = 0.0f;
WeightsRB->DefaultValue.A = 1.0f;
UMaterialExpressionComponentMask* WeightsRB_Mask = NewObject<UMaterialExpressionComponentMask>(UnrealMaterial);
WeightsRB_Mask->R = 1;
WeightsRB_Mask->G = 1;
WeightsRB_Mask->B = 0;
WeightsRB_Mask->A = 0;
UMaterialExpressionVectorParameter* ClipBW = NewObject<UMaterialExpressionVectorParameter>(UnrealMaterial);
ClipBW->ParameterName = "ClipBW";
ClipBW->DefaultValue.R = 0.0f;
ClipBW->DefaultValue.G = 1.0f;
ClipBW->DefaultValue.B = 0.0f;
ClipBW->DefaultValue.A = 1.0f;
UMaterialExpressionComponentMask* ClipBW_Mask = NewObject<UMaterialExpressionComponentMask>(UnrealMaterial);
ClipBW_Mask->R = 1;
ClipBW_Mask->G = 1;
ClipBW_Mask->B = 0;
ClipBW_Mask->A = 0;
UMaterialExpressionScalarParameter* Unpremult = NewObject<UMaterialExpressionScalarParameter>(UnrealMaterial);
Unpremult->DefaultValue = 0.0f;
FString ColorDiffKeyerCode = TEXT("const float epsilon = 0.001; \
float diffGR = max(KeyColor.g - KeyColor.r, epsilon); \
float diffGB = max(KeyColor.g - KeyColor.b, epsilon); \
float diff = min(diffGR, diffGB); \
float weightedRB = GreenScreen.g - (GreenScreen.r * WeightsRB.x + GreenScreen.b * WeightsRB.y); \
float alpha = weightedRB / diff; \
alpha /= AlphaThresOffset.x; \
alpha = (alpha - ClipBW.x) / (ClipBW.y - ClipBW.x); \
alpha = saturate((alpha - AlphaThresOffset.y) / (1.0 - AlphaThresOffset.y)); \
alpha = 1.0 - alpha; \
float3 rgb = lerp(GreenScreen, GreenScreen * alpha, Unpremult); \
return float4(rgb, alpha);");
FCustomInput GreenScreenInput;
GreenScreenInput.InputName = FName(TEXT("GreenScreen"));
FCustomInput KeyColorInput;
KeyColorInput.InputName = FName(TEXT("KeyColor"));
FCustomInput AlphaThresOffsetInput;
AlphaThresOffsetInput.InputName = FName(TEXT("AlphaThresOffset"));
FCustomInput WeightsRBInput;
WeightsRBInput.InputName = FName(TEXT("WeightsRB"));
FCustomInput ClipBWInput;
ClipBWInput.InputName = FName(TEXT("ClipBW"));
FCustomInput UnpremultInput;
UnpremultInput.InputName = FName(TEXT("Unpremult"));
UMaterialExpressionCustom* ColorDiffKeyer = NewObject<UMaterialExpressionCustom>(UnrealMaterial);
ColorDiffKeyer->Code = ColorDiffKeyerCode;
ColorDiffKeyer->Inputs.Empty();
ColorDiffKeyer->Inputs.Add(GreenScreenInput);
ColorDiffKeyer->Inputs.Add(KeyColorInput);
ColorDiffKeyer->Inputs.Add(AlphaThresOffsetInput);
ColorDiffKeyer->Inputs.Add(WeightsRBInput);
ColorDiffKeyer->Inputs.Add(ClipBWInput);
ColorDiffKeyer->Inputs.Add(UnpremultInput);
ColorDiffKeyer->OutputType = ECustomMaterialOutputType::CMOT_Float4;
ColorDiffKeyer->Desc = TEXT("ColorDiffKeyer");
UMaterialExpressionComponentMask* ColorDiffKeyer_Mask = NewObject<UMaterialExpressionComponentMask>(UnrealMaterial);
ColorDiffKeyer_Mask->R = 0;
ColorDiffKeyer_Mask->G = 0;
ColorDiffKeyer_Mask->B = 0;
ColorDiffKeyer_Mask->A = 1;
// - Graph node connection.
// -- Emissive Color
Mask_R->Input.Expression = Video;
Mask_G->Input.Expression = Video;
Mask_B->Input.Expression = Video;
Add->A.Expression = Mask_R;
Add->B.Expression = Mask_B;
Divide->A.Expression = Add;
If->A.Expression = Mask_G;
If->B.Expression = Divide;
If->AGreaterThanB.Expression = Divide;
If->AEqualsB.Expression = Mask_G;
If->ALessThanB.Expression = Mask_G;
Append_0->A.Expression = Mask_R;
Append_0->B.Expression = If;
Append_1->A.Expression = Append_0;
Append_1->B.Expression = Mask_B;
UnrealMaterial->Expressions.Add(Append_1);
UnrealMaterial->EmissiveColor.Expression = Append_1;
// -- Opacity Mask
Custom->Inputs[0].Input.Expression = TextureObject;
Custom->Inputs[1].Input.Expression = TextureCoordinate;
Custom->Inputs[2].Input.Expression = Constant2Vector;
AlphaThresOffset_Mask->Input.Expression = AlphaThresOffset;
WeightsRB_Mask->Input.Expression = WeightsRB;
ClipBW_Mask->Input.Expression = ClipBW;
ColorDiffKeyer->Inputs[0].Input.Expression = Video; // GreenScreen
ColorDiffKeyer->Inputs[1].Input.Expression = Custom; // KeyColor
ColorDiffKeyer->Inputs[2].Input.Expression = AlphaThresOffset_Mask; // AlphaThresOffset
ColorDiffKeyer->Inputs[3].Input.Expression = WeightsRB_Mask; // WeightsRB
ColorDiffKeyer->Inputs[4].Input.Expression = ClipBW_Mask; // ClipBW
ColorDiffKeyer->Inputs[5].Input.Expression = Unpremult; // Unpremult
ColorDiffKeyer_Mask->Input.Expression = ColorDiffKeyer;
UnrealMaterial->Expressions.Add(ColorDiffKeyer_Mask);
UnrealMaterial->OpacityMask.Expression = ColorDiffKeyer_Mask;
UnrealMaterial->BlendMode = EBlendMode::BLEND_Masked;
}
UnrealMaterial->PostLoad();
UE_LOG(LogVideo, Log, TEXT("[PXVideoComponent] Media material(%s) created. Keying(%d). Translucency(%d)"), *UnrealMaterial->GetFName().ToString(), bEnableKeying, bEnableTranslucency);
return UnrealMaterial;
}
四、自定义材质效果
代码中 bEnableKeying = false 对比 bEnableKeying = true
bEnableKeying = false:
bEnableKeying = true: