// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. #if COMPILE_WITH_MATERIAL_GRAPH #include "MaterialGenerator.h" #include "Engine/Visject/ShaderGraphUtilities.h" #include "Engine/Platform/File.h" #include "Engine/Graphics/Materials/MaterialShader.h" #include "Engine/Graphics/Materials/MaterialShaderFeatures.h" /// /// Material shader source code template has special marks for generated code. /// Each starts with '@' char and index of the mapped string. /// enum MaterialTemplateInputsMapping { In_VersionNumber = 0, In_Constants = 1, In_ShaderResources = 2, In_Defines = 3, In_GetMaterialPS = 4, In_GetMaterialVS = 5, In_GetMaterialDS = 6, In_Includes = 7, In_Utilities = 8, In_Shaders = 9, In_MAX }; /// /// Material shader feature source code template has special marks for generated code. Each starts with '@' char and index of the mapped string. /// enum class FeatureTemplateInputsMapping { Defines = 0, Includes = 1, Constants = 2, Resources = 3, Utilities = 4, Shaders = 5, MAX }; struct FeatureData { MaterialShaderFeature::GeneratorData Data; String Inputs[(int32)FeatureTemplateInputsMapping::MAX]; bool Init(); }; namespace { // Loaded and parsed features data cache Dictionary Features; } bool FeatureData::Init() { // Load template file const String path = Globals::EngineContentFolder / TEXT("Editor/MaterialTemplates/Features/") + Data.Template; String contents; if (File::ReadAllText(path, contents)) { LOG(Error, "Cannot open file {0}", path); return true; } int32 i = 0; const int32 length = contents.Length(); // Skip until input start for (; i < length; i++) { if (contents[i] == '@') break; } // Load all inputs do { // Parse input type i++; const int32 inIndex = contents[i++] - '0'; ASSERT_LOW_LAYER(Math::IsInRange(inIndex, 0, (int32)FeatureTemplateInputsMapping::MAX - 1)); // Read until next input start const Char* start = &contents[i]; for (; i < length; i++) { const auto c = contents[i]; if (c == '@') break; } const Char* end = &contents[i]; // Set input Inputs[inIndex].Set(start, (int32)(end - start)); } while (i < length); return false; } MaterialValue MaterialGenerator::getUVs(VariantType::Vector2, TEXT("input.TexCoord")); MaterialValue MaterialGenerator::getTime(VariantType::Float, TEXT("TimeParam")); MaterialValue MaterialGenerator::getNormal(VariantType::Vector3, TEXT("input.TBN[2]")); MaterialValue MaterialGenerator::getVertexColor(VariantType::Vector4, TEXT("GetVertexColor(input)")); MaterialGenerator::MaterialGenerator() { // Register per group type processing events // Note: index must match group id _perGroupProcessCall[1].Bind(this); _perGroupProcessCall[3].Bind(this); _perGroupProcessCall[5].Bind(this); _perGroupProcessCall[6].Bind(this); _perGroupProcessCall[7].Bind(this); _perGroupProcessCall[8].Bind(this); _perGroupProcessCall[14].Bind(this); _perGroupProcessCall[16].Bind(this); } MaterialGenerator::~MaterialGenerator() { _layers.ClearDelete(); } bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo, BytesContainer& parametersData) { ASSERT_LOW_LAYER(_layers.Count() > 0); String inputs[In_MAX]; Array> features; // Setup and prepare layers _writer.Clear(); _includes.Clear(); _callStack.Clear(); _parameters.Clear(); _localIndex = 0; _vsToPsInterpolants.Clear(); _treeLayer = nullptr; _graphStack.Clear(); for (int32 i = 0; i < _layers.Count(); i++) { auto layer = _layers[i]; // Prepare layer->Prepare(); prepareLayer(_layers[i], true); // Assign layer variable name for initial layers layer->Usage[0].VarName = TEXT("material"); if (i != 0) layer->Usage[0].VarName += StringUtils::ToString(i); } inputs[In_VersionNumber] = StringUtils::ToString(MATERIAL_GRAPH_VERSION); // Cache data MaterialLayer* baseLayer = GetRootLayer(); MaterialGraphNode* baseNode = baseLayer->Root; _treeLayerVarName = baseLayer->GetVariableName(nullptr); _treeLayer = baseLayer; _graphStack.Add(&_treeLayer->Graph); const MaterialGraphBox* layerInputBox = baseLayer->Root->GetBox(0); const bool isLayered = layerInputBox->HasConnection(); // Initialize features #define ADD_FEATURE(type) \ { \ StringAnsiView typeName(#type, ARRAY_COUNT(#type) - 1); \ features.Add(typeName); \ if (!Features.ContainsKey(typeName)) \ { \ auto& feature = Features[typeName]; \ type::Generate(feature.Data); \ if (feature.Init()) \ return true; \ } \ } switch (baseLayer->Domain) { case MaterialDomain::Surface: if (materialInfo.TessellationMode != TessellationMethod::None) ADD_FEATURE(TessellationFeature); if (materialInfo.BlendMode == MaterialBlendMode::Opaque) ADD_FEATURE(LightmapFeature); break; case MaterialDomain::Terrain: if (materialInfo.TessellationMode != TessellationMethod::None) ADD_FEATURE(TessellationFeature); ADD_FEATURE(LightmapFeature); break; default: break; } #undef ADD_FEATURE // Check if material is using special features and update the metadata flags if (!isLayered) { baseLayer->UpdateFeaturesFlags(); } // Pixel Shader _treeType = MaterialTreeType::PixelShader; Value materialVarPS; if (isLayered) { materialVarPS = eatBox(baseNode, layerInputBox->FirstConnection()); } else { materialVarPS = Value(VariantType::Void, baseLayer->GetVariableName(nullptr)); _writer.Write(TEXT("\tMaterial {0} = (Material)0;\n"), materialVarPS.Value); if (baseLayer->Domain == MaterialDomain::Surface || baseLayer->Domain == MaterialDomain::Terrain || baseLayer->Domain == MaterialDomain::Particle) { eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Emissive); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Normal); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Color); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Metalness); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Specular); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::AmbientOcclusion); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Roughness); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Opacity); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Refraction); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::SubsurfaceColor); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Mask); } else if (baseLayer->Domain == MaterialDomain::Decal) { eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Emissive); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Normal); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Color); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Metalness); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Specular); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Roughness); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Opacity); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Mask); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::AmbientOcclusion); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Refraction); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::SubsurfaceColor); } else if (baseLayer->Domain == MaterialDomain::PostProcess) { eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Emissive); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Opacity); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Normal); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Color); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Metalness); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Specular); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::AmbientOcclusion); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Roughness); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Refraction); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Mask); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::SubsurfaceColor); } else if (baseLayer->Domain == MaterialDomain::GUI) { eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Emissive); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Opacity); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Mask); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Normal); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Color); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Metalness); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Specular); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::AmbientOcclusion); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Roughness); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Refraction); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::SubsurfaceColor); } else { CRASH; } } { // Flip normal for inverted triangles (used by two sided materials) _writer.Write(TEXT("\t{0}.TangentNormal *= input.TwoSidedSign;\n"), materialVarPS.Value); // Normalize and transform to world space if need to _writer.Write(TEXT("\t{0}.TangentNormal = normalize({0}.TangentNormal);\n"), materialVarPS.Value); if ((baseLayer->FeaturesFlags & MaterialFeaturesFlags::InputWorldSpaceNormal) == 0) { _writer.Write(TEXT("\t{0}.WorldNormal = normalize(TransformTangentVectorToWorld(input, {0}.TangentNormal));\n"), materialVarPS.Value); } else { _writer.Write(TEXT("\t{0}.WorldNormal = {0}.TangentNormal;\n"), materialVarPS.Value); _writer.Write(TEXT("\t{0}.TangentNormal = normalize(TransformWorldVectorToTangent(input, {0}.WorldNormal));\n"), materialVarPS.Value); } // Clamp values _writer.Write(TEXT("\t{0}.Metalness = saturate({0}.Metalness);\n"), materialVarPS.Value); _writer.Write(TEXT("\t{0}.Roughness = max(0.04, {0}.Roughness);\n"), materialVarPS.Value); _writer.Write(TEXT("\t{0}.AO = saturate({0}.AO);\n"), materialVarPS.Value); _writer.Write(TEXT("\t{0}.Opacity = saturate({0}.Opacity);\n"), materialVarPS.Value); // Return result _writer.Write(TEXT("\treturn {0};"), materialVarPS.Value); } inputs[In_GetMaterialPS] = _writer.ToString(); _writer.Clear(); clearCache(); // Domain Shader _treeType = MaterialTreeType::DomainShader; if (isLayered) { const Value layer = eatBox(baseNode, layerInputBox->FirstConnection()); _writer.Write(TEXT("\treturn {0};"), layer.Value); } else { _writer.Write(TEXT("\tMaterial material = (Material)0;\n")); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::WorldDisplacement); _writer.Write(TEXT("\treturn material;")); } inputs[In_GetMaterialDS] = _writer.ToString(); _writer.Clear(); clearCache(); // Vertex Shader _treeType = MaterialTreeType::VertexShader; if (isLayered) { const Value layer = eatBox(baseNode, layerInputBox->FirstConnection()); _writer.Write(TEXT("\treturn {0};"), layer.Value); } else { _writer.Write(TEXT("\tMaterial material = (Material)0;\n")); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::PositionOffset); eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::TessellationMultiplier); for (int32 i = 0; i < _vsToPsInterpolants.Count(); i++) { const auto value = tryGetValue(_vsToPsInterpolants[i], Value::Zero).AsVector4().Value; _writer.Write(TEXT("\tmaterial.CustomVSToPS[{0}] = {1};\n"), i, value); } _writer.Write(TEXT("\treturn material;")); } inputs[In_GetMaterialVS] = _writer.ToString(); _writer.Clear(); clearCache(); // Update material usage based on material generator outputs materialInfo.UsageFlags = baseLayer->UsageFlags; #define WRITE_FEATURES(input) for (auto f : features) _writer.Write(Features[f].Inputs[(int32)FeatureTemplateInputsMapping::input]); // Defines { _writer.Write(TEXT("#define MATERIAL_MASK_THRESHOLD ({0})\n"), baseLayer->MaskThreshold); _writer.Write(TEXT("#define CUSTOM_VERTEX_INTERPOLATORS_COUNT ({0})\n"), _vsToPsInterpolants.Count()); _writer.Write(TEXT("#define MATERIAL_OPACITY_THRESHOLD ({0})\n"), baseLayer->OpacityThreshold); WRITE_FEATURES(Defines); inputs[In_Defines] = _writer.ToString(); _writer.Clear(); } // Includes { for (auto& include : _includes) _writer.Write(TEXT("#include \"{0}\"\n"), include.Item); WRITE_FEATURES(Includes); inputs[In_Includes] = _writer.ToString(); _writer.Clear(); } // Constants { WRITE_FEATURES(Constants); if (_parameters.HasItems()) ShaderGraphUtilities::GenerateShaderConstantBuffer(_writer, _parameters); inputs[In_Constants] = _writer.ToString(); _writer.Clear(); } // Resources { int32 srv = 0; switch (baseLayer->Domain) { case MaterialDomain::Surface: if (materialInfo.BlendMode != MaterialBlendMode::Opaque) srv = 3; // Forward shading resources break; case MaterialDomain::Decal: srv = 1; break; case MaterialDomain::Terrain: srv = 3; // Heightmap + 2 splatmaps break; case MaterialDomain::Particle: srv = 5; break; } for (auto f : features) { const auto& text = Features[f].Inputs[(int32)FeatureTemplateInputsMapping::Resources]; const Char* str = text.Get(); int32 prevIdx = 0, idx = 0; while (true) { idx = text.Find(TEXT("__SRV__"), StringSearchCase::CaseSensitive, prevIdx); if (idx == -1) break; int32 len = idx - prevIdx; _writer.Write(StringView(str, len)); str += len; _writer.Write(StringUtils::ToString(srv)); srv++; str += ARRAY_COUNT("__SRV__") - 1; prevIdx = idx + ARRAY_COUNT("__SRV__") - 1; } _writer.Write(StringView(str)); } if (_parameters.HasItems()) { const auto error = ShaderGraphUtilities::GenerateShaderResources(_writer, _parameters, srv); if (error) { OnError(nullptr, nullptr, error); return true; } } inputs[In_ShaderResources] = _writer.ToString(); _writer.Clear(); } // Utilities { WRITE_FEATURES(Utilities); inputs[In_Utilities] = _writer.ToString(); _writer.Clear(); } // Shaders { WRITE_FEATURES(Shaders); inputs[In_Shaders] = _writer.ToString(); _writer.Clear(); } // Save material parameters data if (_parameters.HasItems()) MaterialParams::Save(parametersData, &_parameters); else parametersData.Release(); _parameters.Clear(); // Create source code { // Open template file String path = Globals::EngineContentFolder / TEXT("Editor/MaterialTemplates/"); switch (materialInfo.Domain) { case MaterialDomain::Surface: if (materialInfo.BlendMode == MaterialBlendMode::Opaque) path /= TEXT("SurfaceDeferred.shader"); else path /= TEXT("SurfaceForward.shader"); break; case MaterialDomain::PostProcess: path /= TEXT("PostProcess.shader"); break; case MaterialDomain::GUI: path /= TEXT("GUI.shader"); break; case MaterialDomain::Decal: path /= TEXT("Decal.shader"); break; case MaterialDomain::Terrain: path /= TEXT("Terrain.shader"); break; case MaterialDomain::Particle: path /= TEXT("Particle.shader"); break; default: LOG(Warning, "Unknown material domain."); return true; } auto file = FileReadStream::Open(path); if (file == nullptr) { LOG(Error, "Cannot open file {0}", path); return true; } // Format template const uint32 length = file->GetLength(); Array tmp; for (uint32 i = 0; i < length; i++) { char c = file->ReadByte(); if (c != '@') { source.WriteByte(c); } else { i++; int32 inIndex = file->ReadByte() - '0'; ASSERT_LOW_LAYER(Math::IsInRange(inIndex, 0, In_MAX - 1)); const String& in = inputs[inIndex]; if (in.Length() > 0) { tmp.EnsureCapacity(in.Length() + 1, false); StringUtils::ConvertUTF162ANSI(*in, tmp.Get(), in.Length()); source.WriteBytes(tmp.Get(), in.Length()); } } } // Close file Delete(file); // Ensure to have null-terminated source code source.WriteByte(0); } return false; } void MaterialGenerator::clearCache() { for (int32 i = 0; i < _layers.Count(); i++) _layers[i]->ClearCache(); for (auto& e : _functions) { for (auto& node : e.Value->Nodes) { for (int32 j = 0; j < node.Boxes.Count(); j++) node.Boxes[j].Cache.Clear(); } } _ddx = Value(); _ddy = Value(); _cameraVector = Value(); } void MaterialGenerator::writeBlending(MaterialGraphBoxes box, Value& result, const Value& bottom, const Value& top, const Value& alpha) { const auto& boxInfo = GetMaterialRootNodeBox(box); _writer.Write(TEXT("\t{0}.{1} = lerp({2}.{1}, {3}.{1}, {4});\n"), result.Value, boxInfo.SubName, bottom.Value, top.Value, alpha.Value); if (box == MaterialGraphBoxes::Normal) { _writer.Write(TEXT("\t{0}.{1} = normalize({0}.{1});\n"), result.Value, boxInfo.SubName); } } SerializedMaterialParam* MaterialGenerator::findParam(const Guid& id, MaterialLayer* layer) { // Use per material layer params mapping return findParam(layer->GetMappedParamId(id)); } MaterialGraphParameter* MaterialGenerator::findGraphParam(const Guid& id) { MaterialGraphParameter* result = nullptr; for (int32 i = 0; i < _layers.Count(); i++) { result = _layers[i]->Graph.GetParameter(id); if (result) break; } return result; } void MaterialGenerator::createGradients(Node* caller) { if (_ddx.IsInvalid()) _ddx = writeLocal(VariantType::Vector2, TEXT("ddx(input.TexCoord.xy)"), caller); if (_ddy.IsInvalid()) _ddy = writeLocal(VariantType::Vector2, TEXT("ddy(input.TexCoord.xy)"), caller); } MaterialGenerator::Value MaterialGenerator::getCameraVector(Node* caller) { if (_cameraVector.IsInvalid()) _cameraVector = writeLocal(VariantType::Vector3, TEXT("normalize(ViewPos.xyz - input.WorldPosition.xyz)"), caller); return _cameraVector; } void MaterialGenerator::eatMaterialGraphBox(String& layerVarName, MaterialGraphBox* nodeBox, MaterialGraphBoxes box) { // Cache data auto& boxInfo = GetMaterialRootNodeBox(box); // Get value const auto value = Value::Cast(tryGetValue(nodeBox, boxInfo.DefaultValue), boxInfo.DefaultValue.Type); // Write formatted value _writer.WriteLine(TEXT("\t{0}.{1} = {2};"), layerVarName, boxInfo.SubName, value.Value); } void MaterialGenerator::eatMaterialGraphBox(MaterialLayer* layer, MaterialGraphBoxes box) { auto& boxInfo = GetMaterialRootNodeBox(box); const auto nodeBox = layer->Root->GetBox(boxInfo.ID); eatMaterialGraphBox(_treeLayerVarName, nodeBox, box); } void MaterialGenerator::eatMaterialGraphBoxWithDefault(MaterialLayer* layer, MaterialGraphBoxes box) { auto& boxInfo = GetMaterialRootNodeBox(box); _writer.WriteLine(TEXT("\t{0}.{1} = {2};"), _treeLayerVarName, boxInfo.SubName, boxInfo.DefaultValue.Value); } void MaterialGenerator::ProcessGroupMath(Box* box, Node* node, Value& value) { switch (node->TypeID) { // Vector Transform case 30: { // Get input vector auto v = tryGetValue(node->GetBox(0), Value::InitForZero(VariantType::Vector3)); // Select transformation spaces ASSERT(node->Values[0].Type == VariantType::Int && node->Values[1].Type == VariantType::Int); ASSERT(Math::IsInRange(node->Values[0].AsInt, 0, (int32)TransformCoordinateSystem::MAX - 1)); ASSERT(Math::IsInRange(node->Values[1].AsInt, 0, (int32)TransformCoordinateSystem::MAX - 1)); auto inputType = static_cast(node->Values[0].AsInt); auto outputType = static_cast(node->Values[1].AsInt); if (inputType == outputType) { // No space change at all value = v; } else { // Switch by source space type const Char* format = nullptr; switch (inputType) { case TransformCoordinateSystem::Tangent: switch (outputType) { case TransformCoordinateSystem::Tangent: format = TEXT("{0}"); break; case TransformCoordinateSystem::World: format = TEXT("TransformTangentVectorToWorld(input, {0})"); break; case TransformCoordinateSystem::View: format = TEXT("TransformWorldVectorToView(input, TransformTangentVectorToWorld(input, {0}))"); break; case TransformCoordinateSystem::Local: format = TEXT("TransformWorldVectorToLocal(input, TransformTangentVectorToWorld(input, {0}))"); break; } break; case TransformCoordinateSystem::World: switch (outputType) { case TransformCoordinateSystem::Tangent: format = TEXT("TransformWorldVectorToTangent(input, {0})"); break; case TransformCoordinateSystem::World: format = TEXT("{0}"); break; case TransformCoordinateSystem::View: format = TEXT("TransformWorldVectorToView(input, {0})"); break; case TransformCoordinateSystem::Local: format = TEXT("TransformWorldVectorToLocal(input, {0})"); break; } break; case TransformCoordinateSystem::View: switch (outputType) { case TransformCoordinateSystem::Tangent: format = TEXT("TransformWorldVectorToTangent(input, TransformViewVectorToWorld(input, {0}))"); break; case TransformCoordinateSystem::World: format = TEXT("TransformViewVectorToWorld(input, {0})"); break; case TransformCoordinateSystem::View: format = TEXT("{0}"); break; case TransformCoordinateSystem::Local: format = TEXT("TransformWorldVectorToLocal(input, TransformViewVectorToWorld(input, {0}))"); break; } break; case TransformCoordinateSystem::Local: switch (outputType) { case TransformCoordinateSystem::Tangent: format = TEXT("TransformWorldVectorToTangent(input, TransformLocalVectorToWorld(input, {0}))"); break; case TransformCoordinateSystem::World: format = TEXT("TransformLocalVectorToWorld(input, {0})"); break; case TransformCoordinateSystem::View: format = TEXT("TransformWorldVectorToView(input, TransformLocalVectorToWorld(input, {0}))"); break; case TransformCoordinateSystem::Local: format = TEXT("{0}"); break; } break; } ASSERT(format != nullptr); // Write operation value = writeLocal(VariantType::Vector3, String::Format(format, v.Value), node); } break; } default: ShaderGenerator::ProcessGroupMath(box, node, value); break; } } #endif