using System; using System.Text; using System.Linq; using System.Collections.Generic; using System.Globalization; using UnityEngine; using UnityEngine.VFX; namespace UnityEditor.VFX.Block { class AttributeVariantProvider : VariantProvider { private readonly string m_Attribute; public AttributeVariantProvider(string attribute) { m_Attribute = attribute; } public override IEnumerable GetVariants() { var randoms = new[] { RandomMode.Off, RandomMode.Uniform, RandomMode.PerComponent }; var sources = new[] { SetAttribute.ValueSource.Slot, SetAttribute.ValueSource.Source }; var compositions = new[] { AttributeCompositionMode.Overwrite, AttributeCompositionMode.Add, AttributeCompositionMode.Multiply, AttributeCompositionMode.Blend }; var attributeRefSize = VFXExpressionHelper.GetSizeOfType(VFXAttributesManager.FindBuiltInOnly(m_Attribute).type);; foreach (var random in randoms) { foreach (var source in sources) { foreach (var composition in compositions) { if (random != RandomMode.Off && source == SetAttribute.ValueSource.Source) continue; if (composition != AttributeCompositionMode.Overwrite && source == SetAttribute.ValueSource.Source) continue; if (composition != AttributeCompositionMode.Overwrite && m_Attribute == VFXAttribute.Alive.name) continue; if (random == RandomMode.PerComponent && attributeRefSize == 1) continue; // This is the main variant settings if (composition == AttributeCompositionMode.Overwrite && source == SetAttribute.ValueSource.Slot && random == RandomMode.Off) continue; var compositionString = $"{VFXBlockUtility.GetNameString(composition)}"; yield return new Variant( source != SetAttribute.ValueSource.Source ? $"{compositionString} {m_Attribute} | Random {random}" : $"Inherit source {m_Attribute}", m_Attribute != VFXAttribute.Alive.name ? compositionString : null, typeof(SetAttribute), new[] { new KeyValuePair("attribute", m_Attribute), new KeyValuePair("Random", random), new KeyValuePair("Source", source), new KeyValuePair("Composition", composition) }); } } } } } class SetAttributeVariantReadWritable : VariantProvider { public override IEnumerable GetVariants() { var attributes = VFXAttributesManager.GetBuiltInNamesOrCombination(true, false, false, true); foreach (var attribute in attributes) { yield return new Variant( $"Set {attribute}", "Attribute", typeof(SetAttribute), new[] { new KeyValuePair("attribute", attribute), new KeyValuePair("Random", RandomMode.Off), new KeyValuePair("Source", SetAttribute.ValueSource.Slot), new KeyValuePair("Composition", AttributeCompositionMode.Overwrite) }, () => new AttributeVariantProvider(attribute)); } } } [VFXHelpURL("Block-SetAttribute")] [VFXInfo(variantProvider = typeof(SetAttributeVariantReadWritable))] class SetAttribute : VFXBlock { public enum ValueSource { Slot, Source } [VFXSetting(VFXSettingAttribute.VisibleFlags.InInspector), StringProvider(typeof(ReadWritableAttributeProvider))] public string attribute; [VFXSetting(VFXSettingAttribute.VisibleFlags.InInspector), Tooltip("Specifies what operation to perform on the chosen attribute. The input value can overwrite, add to, multiply with, or blend with the existing attribute value.")] public AttributeCompositionMode Composition = AttributeCompositionMode.Overwrite; [VFXSetting(VFXSettingAttribute.VisibleFlags.InInspector), Tooltip("Specifies the source of the attribute data. 'Slot' enables a user input to modify the value, while a 'Source' attribute derives its value from a Spawn event attribute or inherits it from a parent system via a GPU event.")] public ValueSource Source = ValueSource.Slot; [VFXSetting(VFXSettingAttribute.VisibleFlags.InInspector), Tooltip("Specifies whether random values can be derived from this block. Random values can be turned off, derived per component, or be uniform.")] public RandomMode Random = RandomMode.Off; [VFXSetting, Tooltip("Specifies which channels to use in this block. This is useful for only storing relevant data if not all channels are used.")] public VariadicChannelOptions channels = VariadicChannelOptions.XYZ; private static readonly char[] channelNames = new char[] { 'x', 'y', 'z' }; public override string name => ComputeName(); private bool TryGetAttribute(out VFXAttribute vfxAttribute) { vfxAttribute = currentAttribute; return !string.IsNullOrEmpty(vfxAttribute.name); } private string ComputeName() { if (!TryGetAttribute(out var vfxAttribute)) { vfxAttribute = new VFXAttribute { name = attribute }; } if (Source != ValueSource.Slot && Source != ValueSource.Source) throw new NotImplementedException(Source.ToString()); var builder = new StringBuilder(24); if (Source == ValueSource.Slot) builder.AppendFormat("{0} ", VFXBlockUtility.GetNameString(Composition)); else builder.Append("Inherit Source "); builder.Append(ObjectNames.NicifyVariableName(attribute)); if (vfxAttribute.variadic == VFXVariadic.True) builder.AppendFormat(".{0}", channels.ToString()); if (Source == ValueSource.Slot) { if (Random != RandomMode.Off) builder.AppendFormat(" {0}", VFXBlockUtility.GetNameString(Random)); } else builder.AppendFormat(" ({0})", VFXBlockUtility.GetNameString(Composition)); return builder.ToString(); } public override VFXContextType compatibleContexts => VFXContextType.InitAndUpdateAndOutput; public override VFXDataType compatibleData => VFXDataType.Particle; public override void Sanitize(int version) { if (VFXBlockUtility.SanitizeAttribute(GetGraph(), ref attribute, ref channels, version)) Invalidate(InvalidationCause.kSettingChanged); base.Sanitize(version); if (version <= 1 && inputSlots.Any(o => o.spaceable)) { //Space has been added with on a few specific attributes, automatically copying space from context var contextSpace = GetParent().space; foreach (var slot in inputSlots.Where(o => o.spaceable)) { slot.space = contextSpace; } Debug.Log($"Sanitizing attribute {attribute} : settings space to {contextSpace} (retrieved from context)"); } } public override void CheckGraphBeforeImport() { base.CheckGraphBeforeImport(); SyncCustomAttributeIfNeeded(); } protected override IEnumerable filteredOutSettings { get { if (Source != ValueSource.Slot) yield return "Random"; if (!TryGetAttribute(out var vfxAttribute) || vfxAttribute.variadic == VFXVariadic.False) yield return "channels"; foreach (var setting in base.filteredOutSettings) yield return setting; } } public override IEnumerable attributes { get { if (TryGetAttribute(out var attrib)) { var attributeMode = Composition == AttributeCompositionMode.Overwrite ? VFXAttributeMode.Write : VFXAttributeMode.ReadWrite; if (attrib.variadic == VFXVariadic.True) { var channelsString = channels.ToString(); foreach (var channel in channelsString) yield return new VFXAttributeInfo(VFXAttributesManager.FindBuiltInOnly(attrib.name + channel), attributeMode); } else { yield return new VFXAttributeInfo(attrib, attributeMode); } if (Random != RandomMode.Off && Source == ValueSource.Slot) yield return new VFXAttributeInfo(VFXAttribute.Seed, VFXAttributeMode.ReadWrite); } } } private static string GenerateLocalAttributeName(string name) { return "_" + name[0].ToString().ToUpper(CultureInfo.InvariantCulture) + name.Substring(1); } public override string source { get { if (!TryGetAttribute(out var attrib)) return string.Empty; string source = ""; int attributeSize = VFXExpression.TypeToSize(attrib.type); int loopCount = 1; if (attrib.variadic == VFXVariadic.True) { attributeSize = 1; loopCount = channels.ToString().Length; } for (int i = 0; i < loopCount; i++) { string paramPostfix = (attrib.variadic == VFXVariadic.True) ? "." + channelNames[i] : ""; string attributePostfix = (attrib.variadic == VFXVariadic.True) ? char.ToUpper(channels.ToString()[i]).ToString() : ""; string channelSource = ""; if (Source == ValueSource.Slot) { if (Random == RandomMode.Off) channelSource = VFXBlockUtility.GetRandomMacroString(Random, attributeSize, paramPostfix, GenerateLocalAttributeName(attrib.name)); else channelSource = VFXBlockUtility.GetRandomMacroString(Random, attributeSize, paramPostfix, "A", "B"); } else { channelSource = VFXBlockUtility.GetRandomMacroString(RandomMode.Off, attributeSize, paramPostfix, "Value"); } if (Composition == AttributeCompositionMode.Blend) channelSource = VFXBlockUtility.GetComposeString(Composition, attrib.name + attributePostfix, channelSource, "Blend"); else channelSource = VFXBlockUtility.GetComposeString(Composition, attrib.name + attributePostfix, channelSource); if (i < loopCount - 1) channelSource += "\n"; source += channelSource; } return source; } } public override IEnumerable parameters { get { if (TryGetAttribute(out var attrib)) { foreach (var param in base.parameters) { if (param.name is "Value" or "A" or "B" && Source == ValueSource.Source) continue; yield return param; } if (Source == ValueSource.Source) { VFXExpression sourceExpression = null; if (attrib.variadic == VFXVariadic.True) { var currentChannels = channels.ToString().Select(c => char.ToUpper(c)); var currentChannelsExpression = currentChannels.Select(o => { var subAttrib = VFXAttributesManager.FindBuiltInOnly(attribute + o); return new VFXAttributeExpression(subAttrib, VFXAttributeLocation.Source); }).ToArray(); if (currentChannelsExpression.Length == 1) sourceExpression = currentChannelsExpression[0]; else sourceExpression = new VFXExpressionCombine(currentChannelsExpression); } else { sourceExpression = new VFXAttributeExpression(attrib, VFXAttributeLocation.Source); } yield return new VFXNamedExpression(sourceExpression, "Value"); } } } } private int ChannelToIndex(char channel) { switch (channel) { default: case 'X': return 0; case 'Y': return 1; case 'Z': return 2; case 'W': return 3; } } protected override IEnumerable inputProperties { get { if (TryGetAttribute(out var attrib)) { if (Source != ValueSource.Source) { TooltipAttribute tooltip = new TooltipAttribute(attrib.description); var attr = attrib.Equals(VFXAttribute.Color) ? new VFXPropertyAttributes(new ShowAsColorAttribute(), tooltip) : new VFXPropertyAttributes(tooltip); Type slotType = VFXExpression.TypeToType(attrib.type); object content = attrib.value.GetContent(); if (attrib.space != SpaceableType.None) { var contentAsVector3 = (Vector3)content; switch (attrib.space) { case SpaceableType.Position: content = (Position)contentAsVector3; break; case SpaceableType.Direction: content = (DirectionType)contentAsVector3; break; case SpaceableType.Vector: content = (Vector)contentAsVector3; break; default: throw new InvalidOperationException("Space is not handled for attribute : " + attrib.name + " space : " + attrib.space); } slotType = content.GetType(); } if (attrib.variadic == VFXVariadic.True) { string channelsString = channels.ToString(); int length = channelsString.Length; switch (length) { case 1: slotType = typeof(float); content = ((Vector3)content)[ChannelToIndex(channelsString[0])]; break; case 2: slotType = typeof(Vector2); Vector2 v = (Vector2)(Vector3)content; for (int i = 0; i < 2; i++) v[i] = ((Vector3)content)[ChannelToIndex(channelsString[i])]; content = v; break; case 3: slotType = typeof(Vector3); break; default: break; } } if (Random == RandomMode.Off) { yield return new VFXPropertyWithValue(new VFXProperty(slotType, GenerateLocalAttributeName(attrib.name), attr), content); } else { yield return new VFXPropertyWithValue(new VFXProperty(slotType, "A", attr), content); yield return new VFXPropertyWithValue(new VFXProperty(slotType, "B", attr), content); } } if (Composition == AttributeCompositionMode.Blend) yield return new VFXPropertyWithValue(new VFXProperty(typeof(float), "Blend", new RangeAttribute(0.0f, 1.0f))); } } } private VFXAttribute currentAttribute { get { if (GetGraph() is { } graph) { if (graph.attributesManager.TryFind(attribute, out var vfxAttribute)) { return vfxAttribute; } } else // Happens when the node is not yet added to the graph, but should be ok as soon as it's added (see OnAdded) { var attr = VFXAttributesManager.FindBuiltInOnly(attribute); if (string.Compare(attribute, attr.name, StringComparison.OrdinalIgnoreCase) == 0) { return attr; } } // Temporary attribute return default; } } public override void Rename(string oldName, string newName) { if (GetGraph() is {} graph && graph.attributesManager.IsCustom(newName)) { attribute = newName; SyncSlots(VFXSlot.Direction.kInput, true); } } internal sealed override void GenerateErrors(VFXErrorReporter report) { base.GenerateErrors(report); if (!CustomAttributeUtility.IsShaderCompilableName(attribute)) { report.RegisterError("InvalidCustomAttributeName", VFXErrorType.Error, $"Custom attribute name '{attribute}' is not valid.\n -The name must not contain spaces or any special character\n -The name must not start with a digit character", this); } if (Source == ValueSource.Source && GetParent() is { } parentContext && (parentContext.contextType & VFXContextType.UpdateAndOutput) != 0) { report.RegisterError("SourceNotAllowed", VFXErrorType.Warning, "Inherit attribute is not supported in Update and Output contexts", this); } if (GetNbInputSlots() == 0) { return; } if (attribute == VFXAttribute.Lifetime.name) { var context = new VFXExpression.Context(VFXExpressionContextOption.CPUEvaluation | VFXExpressionContextOption.ConstantFolding); var expression = GetInputSlot(0).GetExpression(); context.RegisterExpression(expression); context.Compile(); if (context.GetReduced(expression) is var lifeTimeExpression && lifeTimeExpression.Is(VFXExpression.Flags.Constant) && lifeTimeExpression.Get() is var lifeTime && lifeTime > 1e4) { report.RegisterError("TooLongLifeTime", VFXErrorType.Warning, $"The lifetime is pretty high: {TimeSpan.FromSeconds(lifeTime):hh\\hmm\\m}.\nYou might prefer to make immortal particles by removing the Set Lifetime block.", this); } } if (attribute == VFXAttribute.scale.name) { var context = new VFXExpression.Context(VFXExpressionContextOption.CPUEvaluation | VFXExpressionContextOption.ConstantFolding); var expression = GetInputSlot(0).GetExpression(); context.RegisterExpression(expression); context.Compile(); if (context.GetReduced(expression) is var scaleExpression && scaleExpression.Is(VFXExpression.Flags.Constant)) { if (scaleExpression.valueType == VFXValueType.Float && Mathf.Approximately(scaleExpression.Get(), 0f) || scaleExpression.valueType == VFXValueType.Float2 && scaleExpression.Get() is var vec2 && Mathf.Approximately(vec2.x * vec2.y, 0f) || scaleExpression.valueType == VFXValueType.Float3 && scaleExpression.Get() is var vec3 && Mathf.Approximately(vec3.x * vec3.y * vec3.z, 0f) ) { report.RegisterError("ZeroScaleValue", VFXErrorType.Warning, "Scale is set to zero", this); } } } } protected override void OnAdded() { base.OnAdded(); SyncCustomAttributeIfNeeded(); } private void SyncCustomAttributeIfNeeded() { var graph = GetGraph(); if (graph != null) { if (graph.attributesManager.IsCustom(attribute)) { Invalidate(InvalidationCause.kUIChangedTransient); SyncSlots(VFXSlot.Direction.kInput, true); } else if (!string.IsNullOrEmpty(attribute) && !graph.attributesManager.TryFind(attribute, out _)) { graph.TryAddCustomAttribute(attribute, VFXValueType.Float, string.Empty, false, out _); graph.SetCustomAttributeDirty(); Invalidate(InvalidationCause.kUIChangedTransient); } } } } }