using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEditor.Graphing; using UnityEditor.ShaderGraph.Internal; namespace UnityEditor.ShaderGraph { [HasDependencies(typeof(MinimalSubGraphNode))] [Title("Utility", "Sub-graph")] class SubGraphNode : AbstractMaterialNode , IGeneratesBodyCode , IOnAssetEnabled , IGeneratesFunction , IMayRequireNormal , IMayRequireTangent , IMayRequireBitangent , IMayRequireMeshUV , IMayRequireScreenPosition , IMayRequireViewDirection , IMayRequirePosition , IMayRequireVertexColor , IMayRequireTime , IMayRequireFaceSign , IMayRequireCameraOpaqueTexture , IMayRequireDepthTexture , IMayRequireVertexSkinning , IMayRequireVertexID { [Serializable] public class MinimalSubGraphNode : IHasDependencies { [SerializeField] string m_SerializedSubGraph = string.Empty; public void GetSourceAssetDependencies(AssetCollection assetCollection) { var assetReference = JsonUtility.FromJson(m_SerializedSubGraph); string guidString = assetReference?.subGraph?.guid; if (!string.IsNullOrEmpty(guidString) && GUID.TryParse(guidString, out GUID guid)) { // subgraphs are read as artifacts // they also should be pulled into .unitypackages assetCollection.AddAssetDependency( guid, AssetCollection.Flags.ArtifactDependency | AssetCollection.Flags.IsSubGraph | AssetCollection.Flags.IncludeInExportPackage); } } } [Serializable] class SubGraphHelper { public SubGraphAsset subGraph; } [Serializable] class SubGraphAssetReference { public AssetReference subGraph = default; public override string ToString() { return $"subGraph={subGraph}"; } } [Serializable] class AssetReference { public long fileID = default; public string guid = default; public int type = default; public override string ToString() { return $"fileID={fileID}, guid={guid}, type={type}"; } } [SerializeField] string m_SerializedSubGraph = string.Empty; [NonSerialized] SubGraphAsset m_SubGraph; [SerializeField] List m_PropertyGuids = new List(); [SerializeField] List m_PropertyIds = new List(); public string subGraphGuid { get { var assetReference = JsonUtility.FromJson(m_SerializedSubGraph); return assetReference?.subGraph?.guid; } } void LoadSubGraph() { if (m_SubGraph == null) { if (string.IsNullOrEmpty(m_SerializedSubGraph)) { return; } var graphGuid = subGraphGuid; var assetPath = AssetDatabase.GUIDToAssetPath(graphGuid); if (string.IsNullOrEmpty(assetPath)) { // this happens if the editor has never seen the GUID // error will be printed by validation code in this case return; } m_SubGraph = AssetDatabase.LoadAssetAtPath(assetPath); if (m_SubGraph == null) { // this happens if the editor has seen the GUID, but the file has been deleted since then // error will be printed by validation code in this case return; } m_SubGraph.LoadGraphData(); name = m_SubGraph.name; concretePrecision = m_SubGraph.outputPrecision; } } public SubGraphAsset asset { get { LoadSubGraph(); return m_SubGraph; } set { if (asset == value) return; var helper = new SubGraphHelper(); helper.subGraph = value; m_SerializedSubGraph = EditorJsonUtility.ToJson(helper, true); m_SubGraph = null; UpdateSlots(); Dirty(ModificationScope.Topological); } } public override bool hasPreview { get { return true; } } public override PreviewMode previewMode { get { PreviewMode mode = m_PreviewMode; if ((mode == PreviewMode.Inherit) && (asset != null)) mode = asset.previewMode; return mode; } } public SubGraphNode() { name = "Sub Graph"; } public override bool allowedInSubGraph { get { return true; } } public override bool canSetPrecision { get { return false; } } public void GenerateNodeCode(ShaderStringBuilder sb, GenerationMode generationMode) { if (asset == null || hasError) { var outputSlots = new List(); GetOutputSlots(outputSlots); var outputPrecision = asset != null ? asset.outputPrecision : ConcretePrecision.Single; foreach (var slot in outputSlots) { sb.AppendLine($"{slot.concreteValueType.ToShaderString(outputPrecision)} {GetVariableNameForSlot(slot.id)} = {slot.GetDefaultValue(GenerationMode.ForReals)};"); } return; } var inputVariableName = $"_{GetVariableNameForNode()}"; GenerationUtils.GenerateSurfaceInputTransferCode(sb, asset.requirements, asset.inputStructName, inputVariableName); foreach (var outSlot in asset.outputs) sb.AppendLine("{0} {1};", outSlot.concreteValueType.ToShaderString(asset.outputPrecision), GetVariableNameForSlot(outSlot.id)); var arguments = new List(); foreach (var prop in asset.inputs) { prop.ValidateConcretePrecision(asset.graphPrecision); var inSlotId = m_PropertyIds[m_PropertyGuids.IndexOf(prop.guid.ToString())]; arguments.Add(GetSlotValue(inSlotId, generationMode, prop.concretePrecision)); } // pass surface inputs through arguments.Add(inputVariableName); foreach (var outSlot in asset.outputs) arguments.Add(GetVariableNameForSlot(outSlot.id)); foreach (var feedbackSlot in asset.vtFeedbackVariables) { string feedbackVar = GetVariableNameForNode() + "_" + feedbackSlot; sb.AppendLine("{0} {1};", ConcreteSlotValueType.Vector4.ToShaderString(ConcretePrecision.Single), feedbackVar); arguments.Add(feedbackVar); } sb.AppendIndentation(); sb.Append(asset.functionName); sb.Append("("); bool firstArg = true; foreach (var arg in arguments) { if (!firstArg) sb.Append(", "); firstArg = false; sb.Append(arg); } sb.Append(");"); sb.AppendNewLine(); } public void OnEnable() { UpdateSlots(); } public bool Reload(HashSet changedFileDependencies) { if (!changedFileDependencies.Contains(subGraphGuid)) { return false; } if (asset == null) { // asset missing or deleted return true; } if (changedFileDependencies.Contains(asset.assetGuid) || asset.descendents.Any(changedFileDependencies.Contains)) { m_SubGraph = null; UpdateSlots(); if (hasError) { return true; } owner.ClearErrorsForNode(this); ValidateNode(); Dirty(ModificationScope.Graph); } return true; } public virtual void UpdateSlots() { var validNames = new List(); if (asset == null) { return; } var props = asset.inputs; foreach (var prop in props) { SlotValueType valueType = prop.concreteShaderValueType.ToSlotValueType(); var propertyString = prop.guid.ToString(); var propertyIndex = m_PropertyGuids.IndexOf(propertyString); if (propertyIndex < 0) { propertyIndex = m_PropertyGuids.Count; m_PropertyGuids.Add(propertyString); m_PropertyIds.Add(prop.guid.GetHashCode()); } var id = m_PropertyIds[propertyIndex]; MaterialSlot slot = MaterialSlot.CreateMaterialSlot(valueType, id, prop.displayName, prop.referenceName, SlotType.Input, Vector4.zero, ShaderStageCapability.All); // Copy defaults switch (prop.concreteShaderValueType) { case ConcreteSlotValueType.Matrix4: { var tSlot = slot as Matrix4MaterialSlot; var tProp = prop as Matrix4ShaderProperty; if (tSlot != null && tProp != null) tSlot.value = tProp.value; } break; case ConcreteSlotValueType.Matrix3: { var tSlot = slot as Matrix3MaterialSlot; var tProp = prop as Matrix3ShaderProperty; if (tSlot != null && tProp != null) tSlot.value = tProp.value; } break; case ConcreteSlotValueType.Matrix2: { var tSlot = slot as Matrix2MaterialSlot; var tProp = prop as Matrix2ShaderProperty; if (tSlot != null && tProp != null) tSlot.value = tProp.value; } break; case ConcreteSlotValueType.Texture2D: { var tSlot = slot as Texture2DInputMaterialSlot; var tProp = prop as Texture2DShaderProperty; if (tSlot != null && tProp != null) tSlot.texture = tProp.value.texture; } break; case ConcreteSlotValueType.Texture2DArray: { var tSlot = slot as Texture2DArrayInputMaterialSlot; var tProp = prop as Texture2DArrayShaderProperty; if (tSlot != null && tProp != null) tSlot.textureArray = tProp.value.textureArray; } break; case ConcreteSlotValueType.Texture3D: { var tSlot = slot as Texture3DInputMaterialSlot; var tProp = prop as Texture3DShaderProperty; if (tSlot != null && tProp != null) tSlot.texture = tProp.value.texture; } break; case ConcreteSlotValueType.Cubemap: { var tSlot = slot as CubemapInputMaterialSlot; var tProp = prop as CubemapShaderProperty; if (tSlot != null && tProp != null) tSlot.cubemap = tProp.value.cubemap; } break; case ConcreteSlotValueType.Gradient: { var tSlot = slot as GradientInputMaterialSlot; var tProp = prop as GradientShaderProperty; if (tSlot != null && tProp != null) tSlot.value = tProp.value; } break; case ConcreteSlotValueType.Vector4: { var tSlot = slot as Vector4MaterialSlot; var vector4Prop = prop as Vector4ShaderProperty; var colorProp = prop as ColorShaderProperty; if (tSlot != null && vector4Prop != null) tSlot.value = vector4Prop.value; else if (tSlot != null && colorProp != null) tSlot.value = colorProp.value; } break; case ConcreteSlotValueType.Vector3: { var tSlot = slot as Vector3MaterialSlot; var tProp = prop as Vector3ShaderProperty; if (tSlot != null && tProp != null) tSlot.value = tProp.value; } break; case ConcreteSlotValueType.Vector2: { var tSlot = slot as Vector2MaterialSlot; var tProp = prop as Vector2ShaderProperty; if (tSlot != null && tProp != null) tSlot.value = tProp.value; } break; case ConcreteSlotValueType.Vector1: { var tSlot = slot as Vector1MaterialSlot; var tProp = prop as Vector1ShaderProperty; if (tSlot != null && tProp != null) tSlot.value = tProp.value; } break; case ConcreteSlotValueType.Boolean: { var tSlot = slot as BooleanMaterialSlot; var tProp = prop as BooleanShaderProperty; if (tSlot != null && tProp != null) tSlot.value = tProp.value; } break; } AddSlot(slot); validNames.Add(id); } var outputStage = asset.effectiveShaderStage; foreach (var slot in asset.outputs) { var newSlot = MaterialSlot.CreateMaterialSlot(slot.valueType, slot.id, slot.RawDisplayName(), slot.shaderOutputName, SlotType.Output, Vector4.zero, outputStage, slot.hidden); AddSlot(newSlot); validNames.Add(slot.id); } RemoveSlotsNameNotMatching(validNames, true); // sort slot order to match subgraph property order SetSlotOrder(validNames); } void ValidateShaderStage() { if (asset != null) { List slots = new List(); GetInputSlots(slots); GetOutputSlots(slots); var outputStage = asset.effectiveShaderStage; foreach (MaterialSlot slot in slots) slot.stageCapability = outputStage; } } public override void ValidateNode() { base.ValidateNode(); if (asset == null) { hasError = true; var assetGuid = subGraphGuid; var assetPath = string.IsNullOrEmpty(subGraphGuid) ? null : AssetDatabase.GUIDToAssetPath(assetGuid); if (string.IsNullOrEmpty(assetPath)) { owner.AddValidationError(objectId, $"Could not find Sub Graph asset with GUID {assetGuid}."); } else { owner.AddValidationError(objectId, $"Could not load Sub Graph asset at \"{assetPath}\" with GUID {assetGuid}."); } return; } if (owner.isSubGraph && (asset.descendents.Contains(owner.assetGuid) || asset.assetGuid == owner.assetGuid)) { hasError = true; owner.AddValidationError(objectId, $"Detected a recursion in Sub Graph asset at \"{AssetDatabase.GUIDToAssetPath(subGraphGuid)}\" with GUID {subGraphGuid}."); } else if (!asset.isValid) { hasError = true; owner.AddValidationError(objectId, $"Invalid Sub Graph asset at \"{AssetDatabase.GUIDToAssetPath(subGraphGuid)}\" with GUID {subGraphGuid}."); } else if (!owner.isSubGraph && owner.activeTargets.Any(x => asset.unsupportedTargets.Contains(x))) { SetOverrideActiveState(ActiveState.ExplicitInactive); owner.AddValidationError(objectId, $"Subgraph asset at \"{AssetDatabase.GUIDToAssetPath(subGraphGuid)}\" with GUID {subGraphGuid} contains nodes that are unsuported by the current active targets"); } // detect disconnected VT properties, and VT layer count mismatches foreach (var paramProp in asset.inputs) { if (paramProp is VirtualTextureShaderProperty vtProp) { int paramLayerCount = vtProp.value.layers.Count; var argSlotId = m_PropertyIds[m_PropertyGuids.IndexOf(paramProp.guid.ToString())]; // yikes if (!IsSlotConnected(argSlotId)) { owner.AddValidationError(objectId, $"A VirtualTexture property must be connected to the input slot \"{paramProp.displayName}\""); } else { var argProp = GetSlotProperty(argSlotId) as VirtualTextureShaderProperty; if (argProp != null) { int argLayerCount = argProp.value.layers.Count; if (argLayerCount != paramLayerCount) owner.AddValidationError(objectId, $"Input \"{paramProp.displayName}\" has different number of layers from the connected property \"{argProp.displayName}\""); } else { owner.AddValidationError(objectId, $"Input \"{paramProp.displayName}\" is not connected to a valid VirtualTexture property"); } } break; } } ValidateShaderStage(); } public override void CollectShaderProperties(PropertyCollector visitor, GenerationMode generationMode) { base.CollectShaderProperties(visitor, generationMode); if (asset == null) return; foreach (var property in asset.nodeProperties) { visitor.AddShaderProperty(property); } } public void CollectShaderKeywords(KeywordCollector keywords, GenerationMode generationMode) { if (asset == null) return; foreach (var keyword in asset.keywords) { keywords.AddShaderKeyword(keyword as ShaderKeyword); } } public override void CollectPreviewMaterialProperties(List properties) { base.CollectPreviewMaterialProperties(properties); if (asset == null) return; foreach (var property in asset.nodeProperties) { properties.Add(property.GetPreviewMaterialProperty()); } } public virtual void GenerateNodeFunction(FunctionRegistry registry, GenerationMode generationMode) { if (asset == null || hasError) return; foreach (var function in asset.functions) { registry.ProvideFunction(function.key, s => { s.AppendLines(function.value); }); } } public NeededCoordinateSpace RequiresNormal(ShaderStageCapability stageCapability) { if (asset == null) return NeededCoordinateSpace.None; return asset.requirements.requiresNormal; } public bool RequiresMeshUV(UVChannel channel, ShaderStageCapability stageCapability) { if (asset == null) return false; return asset.requirements.requiresMeshUVs.Contains(channel); } public bool RequiresScreenPosition(ShaderStageCapability stageCapability) { if (asset == null) return false; return asset.requirements.requiresScreenPosition; } public NeededCoordinateSpace RequiresViewDirection(ShaderStageCapability stageCapability) { if (asset == null) return NeededCoordinateSpace.None; return asset.requirements.requiresViewDir; } public NeededCoordinateSpace RequiresPosition(ShaderStageCapability stageCapability) { if (asset == null) return NeededCoordinateSpace.None; return asset.requirements.requiresPosition; } public NeededCoordinateSpace RequiresTangent(ShaderStageCapability stageCapability) { if (asset == null) return NeededCoordinateSpace.None; return asset.requirements.requiresTangent; } public bool RequiresTime() { if (asset == null) return false; return asset.requirements.requiresTime; } public bool RequiresFaceSign(ShaderStageCapability stageCapability) { if (asset == null) return false; return asset.requirements.requiresFaceSign; } public NeededCoordinateSpace RequiresBitangent(ShaderStageCapability stageCapability) { if (asset == null) return NeededCoordinateSpace.None; return asset.requirements.requiresBitangent; } public bool RequiresVertexColor(ShaderStageCapability stageCapability) { if (asset == null) return false; return asset.requirements.requiresVertexColor; } public bool RequiresCameraOpaqueTexture(ShaderStageCapability stageCapability) { if (asset == null) return false; return asset.requirements.requiresCameraOpaqueTexture; } public bool RequiresDepthTexture(ShaderStageCapability stageCapability) { if (asset == null) return false; return asset.requirements.requiresDepthTexture; } public bool RequiresVertexSkinning(ShaderStageCapability stageCapability) { if (asset == null) return false; return asset.requirements.requiresVertexSkinning; } public bool RequiresVertexID(ShaderStageCapability stageCapability) { if (asset == null) return false; return asset.requirements.requiresVertexID; } } }