#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using SynapticAIPro;
using UnityEngine.VFX;
using UnityEditor;
using Newtonsoft.Json;
// VFX Graph Editor API (internal namespace, accessed via reflection where needed)
#if VFX_GRAPH_10_PLUS
using UnityEditor.VFX;
#endif
namespace SynapticPro
{
///
/// VFX Graph Builder - Programmatic creation of VFX Graphs
/// Provides full access to VFX Graph features via MCP tools
///
public static class NexusVFXBuilder
{
// Cache for VFX types (populated via reflection)
private static Dictionary _contextTypes;
private static Dictionary _blockTypes;
private static Dictionary _operatorTypes;
private static bool _initialized = false;
// Output type mapping for pipeline conversion (Built-in -> URP/HDRP)
private static readonly Dictionary _urpOutputMapping = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
{ "quad", "urpquad" },
{ "output", "urpquad" },
{ "line", "urpquad" }, // URP doesn't have line, use quad
{ "quadstrip", "urplitstrip" },
{ "trail", "urplitstrip" },
{ "ribbon", "urplitstrip" },
{ "mesh", "urplitmesh" },
{ "decal", "urpdecal" },
};
private static readonly Dictionary _hdrpOutputMapping = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
{ "quad", "hdrpquad" },
{ "output", "hdrpquad" },
{ "line", "hdrpquad" }, // HDRP doesn't have line, use quad
{ "quadstrip", "hdrplitstrip" },
{ "trail", "hdrplitstrip" },
{ "ribbon", "hdrplitstrip" },
{ "mesh", "hdrplitmesh" },
{ "decal", "hdrpdecal" },
};
#region Initialization
///
/// Detect the current rendering pipeline
///
private static string DetectRenderingPipeline()
{
var currentPipeline = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline;
if (currentPipeline != null)
{
var pipelineName = currentPipeline.GetType().Name;
if (pipelineName.Contains("Universal") || pipelineName.Contains("URP"))
return "URP";
else if (pipelineName.Contains("HighDefinition") || pipelineName.Contains("HDRP"))
return "HDRP";
}
else
{
var qualityAsset = UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline;
if (qualityAsset != null)
{
var assetName = qualityAsset.GetType().Name;
if (assetName.Contains("Universal") || assetName.Contains("URP"))
return "URP";
else if (assetName.Contains("HighDefinition") || assetName.Contains("HDRP"))
return "HDRP";
}
}
return "Legacy";
}
///
/// Convert output context type to pipeline-appropriate type
///
private static string ConvertOutputForPipeline(string contextType)
{
var pipeline = DetectRenderingPipeline();
var lowerType = contextType.ToLower();
// Already pipeline-specific, don't convert
if (lowerType.StartsWith("urp") || lowerType.StartsWith("hdrp"))
return contextType;
// Not an output type, don't convert
bool isOutputType = lowerType == "quad" || lowerType == "output" || lowerType == "line" ||
lowerType == "quadstrip" || lowerType == "trail" || lowerType == "ribbon" ||
lowerType == "mesh" || lowerType == "decal" || lowerType == "point" ||
lowerType == "linestrip" || lowerType == "staticmesh";
if (!isOutputType)
return contextType;
string convertedType = contextType;
if (pipeline == "URP" && _urpOutputMapping.TryGetValue(lowerType, out string urpType))
{
// Check if URP type is available
if (_contextTypes.ContainsKey(urpType))
{
convertedType = urpType;
SynLog.Info($"[NexusVFX] Auto-converted output '{contextType}' -> '{urpType}' for URP pipeline");
}
else
{
SynLog.Warn($"[NexusVFX] URP output type '{urpType}' not available, using built-in '{contextType}'");
}
}
else if (pipeline == "HDRP" && _hdrpOutputMapping.TryGetValue(lowerType, out string hdrpType))
{
// Check if HDRP type is available
if (_contextTypes.ContainsKey(hdrpType))
{
convertedType = hdrpType;
SynLog.Info($"[NexusVFX] Auto-converted output '{contextType}' -> '{hdrpType}' for HDRP pipeline");
}
else
{
SynLog.Warn($"[NexusVFX] HDRP output type '{hdrpType}' not available, using built-in '{contextType}'");
}
}
return convertedType;
}
public static void Initialize()
{
if (_initialized) return;
_contextTypes = new Dictionary(StringComparer.OrdinalIgnoreCase);
_blockTypes = new Dictionary(StringComparer.OrdinalIgnoreCase);
_operatorTypes = new Dictionary(StringComparer.OrdinalIgnoreCase);
// Find all VFX types via reflection
var vfxEditorAssembly = GetVFXEditorAssembly();
if (vfxEditorAssembly == null)
{
SynLog.Warn("[NexusVFX] VFX Graph Editor assembly not found. Is the package installed?");
return;
}
// Populate context types
PopulateContextTypes(vfxEditorAssembly);
PopulateBlockTypes(vfxEditorAssembly);
PopulateOperatorTypes(vfxEditorAssembly);
_initialized = true;
SynLog.Info($"[NexusVFX] Initialized: {_contextTypes.Count} contexts, {_blockTypes.Count} blocks, {_operatorTypes.Count} operators");
}
private static Assembly GetVFXEditorAssembly()
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetName().Name == "Unity.VisualEffectGraph.Editor")
{
return assembly;
}
}
return null;
}
private static void PopulateContextTypes(Assembly assembly)
{
var contextMappings = new Dictionary
{
// Spawn
{ "spawn", "VFXBasicSpawner" },
{ "spawner", "VFXBasicSpawner" },
{ "gpuspawn", "VFXBasicGPUEvent" },
// Initialize
{ "initialize", "VFXBasicInitialize" },
{ "init", "VFXBasicInitialize" },
// Update
{ "update", "VFXBasicUpdate" },
// Output - Basic (Built-in compatible)
{ "output", "VFXQuadOutput" },
{ "quad", "VFXQuadOutput" },
{ "point", "VFXPointOutput" },
{ "line", "VFXLineOutput" },
{ "linestrip", "VFXLineStripOutput" },
{ "quadstrip", "VFXQuadStripOutput" },
{ "trail", "VFXQuadStripOutput" },
{ "ribbon", "VFXQuadStripOutput" },
{ "mesh", "VFXMeshOutput" },
{ "staticmesh", "VFXStaticMeshOutput" },
{ "decal", "VFXDecalOutput" },
// Output - URP specific (in UnityEditor.VFX.URP namespace)
{ "urpquad", "VFXURPLitPlanarPrimitiveOutput" },
{ "urplit", "VFXURPLitPlanarPrimitiveOutput" },
{ "urplitquad", "VFXURPLitPlanarPrimitiveOutput" },
{ "urplitmesh", "VFXURPLitMeshOutput" },
{ "urplitstrip", "VFXURPLitQuadStripOutput" },
{ "urpdecal", "VFXDecalURPOutput" },
// Output - HDRP specific (in UnityEditor.VFX.HDRP namespace)
{ "hdrpquad", "VFXHDRPLitPlanarPrimitiveOutput" },
{ "hdrplit", "VFXHDRPLitPlanarPrimitiveOutput" },
{ "hdrplitquad", "VFXHDRPLitPlanarPrimitiveOutput" },
{ "hdrplitmesh", "VFXHDRPLitMeshOutput" },
{ "hdrplitstrip", "VFXHDRPLitQuadStripOutput" },
{ "hdrpdecal", "VFXDecalHDRPOutput" },
// Event
{ "event", "VFXBasicEvent" },
{ "outputevent", "VFXOutputEvent" },
};
// Also search in URP/HDRP VFX assemblies
Assembly urpVfxAssembly = null;
Assembly hdrpVfxAssembly = null;
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
var asmName = asm.GetName().Name;
if (asmName == "Unity.RenderPipelines.Universal.Editor")
urpVfxAssembly = asm;
else if (asmName == "Unity.RenderPipelines.HighDefinition.Editor")
hdrpVfxAssembly = asm;
}
foreach (var mapping in contextMappings)
{
Type type = null;
// Try VFX editor assembly first
type = assembly.GetType($"UnityEditor.VFX.{mapping.Value}");
// Try URP VFX namespace
if (type == null && urpVfxAssembly != null)
{
type = urpVfxAssembly.GetType($"UnityEditor.VFX.URP.{mapping.Value}");
}
// Try HDRP VFX namespace
if (type == null && hdrpVfxAssembly != null)
{
type = hdrpVfxAssembly.GetType($"UnityEditor.VFX.HDRP.{mapping.Value}");
}
// Try searching all assemblies for URP types
if (type == null && mapping.Key.StartsWith("urp"))
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
type = asm.GetType($"UnityEditor.VFX.URP.{mapping.Value}");
if (type != null) break;
}
}
// Try searching all assemblies for HDRP types
if (type == null && mapping.Key.StartsWith("hdrp"))
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
type = asm.GetType($"UnityEditor.VFX.HDRP.{mapping.Value}");
if (type != null) break;
}
}
if (type != null)
{
_contextTypes[mapping.Key] = type;
SynLog.Info($"[NexusVFX] Registered context: {mapping.Key} -> {type.FullName}");
}
}
// Log pipeline availability
SynLog.Info($"[NexusVFX] URP VFX assembly found: {urpVfxAssembly != null}, HDRP: {hdrpVfxAssembly != null}");
}
private static void PopulateBlockTypes(Assembly assembly)
{
var blockMappings = new Dictionary
{
// Position blocks
{ "positionsphere", "PositionSphere" },
{ "positioncircle", "PositionCircle" },
{ "positioncone", "PositionCone" },
{ "positionline", "PositionLine" },
{ "positionbox", "PositionAABox" },
{ "positionaabox", "PositionAABox" },
{ "positiontorus", "PositionTorus" },
{ "positionmesh", "PositionMesh" },
{ "positionsdf", "PositionSDF" },
{ "positiondepth", "PositionDepth" },
{ "positionsequential", "PositionSequential" },
// Force blocks
{ "gravity", "Gravity" },
{ "drag", "Drag" },
{ "turbulence", "Turbulence" },
{ "force", "Force" },
{ "conformtosphere", "ConformToSphere" },
{ "conformtosdf", "ConformToSDF" },
{ "vectorfieldforce", "VectorFieldForce" },
// Attribute blocks
{ "setattribute", "SetAttribute" },
{ "setattributerandom", "SetAttribute" }, // Uses SetAttribute with Random mode
{ "setattributefrommap", "SetAttributeFromMap" },
{ "setposition", "SetAttribute" },
{ "setvelocity", "SetAttribute" },
{ "setcolor", "SetAttribute" },
{ "setsize", "SetAttribute" },
{ "setlifetime", "SetAttribute" },
{ "setmass", "SetAttribute" },
{ "setalpha", "SetAttribute" },
{ "setangle", "SetAttribute" },
{ "setangularvelocity", "SetAttribute" },
// Random attribute blocks
{ "velocityrandom", "VelocityRandomize" },
{ "randomvelocity", "VelocityRandomize" },
// Collision blocks
{ "collisionsphere", "CollisionSphere" },
{ "collisionplane", "CollisionPlane" },
{ "collisionbox", "CollisionAABox" },
{ "collisioncylinder", "CollisionCylinder" },
{ "collisionsdf", "CollisionSDF" },
{ "collisiondepth", "CollisionDepthBuffer" },
// Kill blocks
{ "killsphere", "KillSphere" },
{ "killbox", "KillAABox" },
{ "killplane", "KillPlane" },
{ "killage", "KillAge" },
// Orientation blocks
{ "orient", "Orient" },
{ "facecamera", "Orient" },
{ "orientalongvelocity", "OrientAlongVelocity" },
// Spawn blocks
{ "spawnrate", "VFXSpawnerConstantRate" },
{ "spawnburst", "VFXSpawnerBurst" },
{ "spawnperunit", "SpawnOverDistance" },
// Color/Size over lifetime
// ColorOverLife exists but is deprecated in some versions
{ "coloroverlife", "ColorOverLife" },
{ "coloroverlifetime", "ColorOverLife" },
// SizeOverLife doesn't exist as separate class - use AttributeFromCurve
{ "sizeoverlife", "AttributeFromCurve" },
{ "sizeoverlifetime", "AttributeFromCurve" },
// Attribute from curve blocks (main implementation for "over life" blocks)
{ "attributefromcurve", "AttributeFromCurve" },
{ "setattributefromcurve", "AttributeFromCurve" },
{ "setcoloroverlife", "AttributeFromCurve" },
{ "setsizeoverlife", "AttributeFromCurve" },
{ "setalphaoverlife", "AttributeFromCurve" },
// Flipbook
{ "flipbook", "FlipbookPlayer" },
// GPU Events
{ "triggerevent", "TriggerEvent" },
{ "triggereventondie", "TriggerEventOnDie" },
};
string[] blockNamespaces = new[]
{
"UnityEditor.VFX.Block",
"UnityEditor.VFX",
};
foreach (var mapping in blockMappings)
{
Type type = null;
foreach (var ns in blockNamespaces)
{
type = assembly.GetType($"{ns}.{mapping.Value}");
if (type != null) break;
}
if (type != null)
{
_blockTypes[mapping.Key] = type;
}
}
// Also search for types dynamically if not found in mappings
// Look for VFXBlock subclasses with VFXInfo attributes
try
{
var vfxBlockBaseType = assembly.GetType("UnityEditor.VFX.VFXBlock");
if (vfxBlockBaseType != null)
{
var allTypes = assembly.GetTypes()
.Where(t => vfxBlockBaseType.IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in allTypes)
{
// Use type name without namespace as potential key
string typeName = type.Name.ToLower();
if (!_blockTypes.ContainsKey(typeName))
{
_blockTypes[typeName] = type;
}
}
}
}
catch (Exception e)
{
SynLog.Warn($"[NexusVFX] Failed to discover additional block types: {e.Message}");
}
SynLog.Info($"[NexusVFX] Block types discovered: {_blockTypes.Count}");
}
private static void PopulateOperatorTypes(Assembly assembly)
{
var operatorMappings = new Dictionary
{
// Math
{ "add", "Add" },
{ "subtract", "Subtract" },
{ "multiply", "Multiply" },
{ "divide", "Divide" },
{ "power", "Power" },
{ "modulo", "Modulo" },
{ "absolute", "Absolute" },
{ "negate", "Negate" },
{ "minimum", "Minimum" },
{ "maximum", "Maximum" },
{ "clamp", "Clamp" },
{ "saturate", "Saturate" },
{ "floor", "Floor" },
{ "ceiling", "Ceiling" },
{ "round", "Round" },
{ "sign", "Sign" },
{ "step", "Step" },
{ "smoothstep", "Smoothstep" },
{ "lerp", "Lerp" },
{ "inverselerp", "InverseLerp" },
{ "remap", "Remap" },
{ "oneminus", "OneMinus" },
// Trigonometric
{ "sine", "Sine" },
{ "cosine", "Cosine" },
{ "tangent", "Tangent" },
{ "asin", "Asin" },
{ "acos", "Acos" },
{ "atan", "Atan" },
{ "atan2", "Atan2" },
// Vector
{ "dot", "DotProduct" },
{ "cross", "CrossProduct" },
{ "length", "Length" },
{ "distance", "Distance" },
{ "normalize", "Normalize" },
{ "swizzle", "Swizzle" },
// Noise
{ "noise", "Noise" },
{ "curlnoise", "CurlNoise" },
{ "voronoise", "VoroNoise2D" },
// Sampling
{ "sampletexture2d", "SampleTexture2D" },
{ "sampletexture3d", "SampleTexture3D" },
{ "samplecurve", "SampleCurve" },
{ "samplegradient", "SampleGradient" },
{ "samplemesh", "SampleMesh" },
{ "samplesdf", "SampleSDF" },
// Logic
{ "compare", "Compare" },
{ "branch", "Branch" },
{ "and", "LogicalAnd" },
{ "or", "LogicalOr" },
{ "not", "LogicalNot" },
// Utility
{ "random", "Random" },
{ "time", "Time" },
{ "deltatime", "DeltaTime" },
{ "maincamera", "MainCamera" },
{ "changespace", "ChangeSpace" },
// Transform
{ "transformposition", "TransformPosition" },
{ "transformdirection", "TransformDirection" },
// Waveforms
{ "sinewave", "SineWave" },
{ "squarewave", "SquareWave" },
{ "trianglewave", "TriangleWave" },
{ "sawtoothwave", "SawtoothWave" },
};
string[] opNamespaces = new[]
{
"UnityEditor.VFX.Operator",
"UnityEditor.VFX",
};
foreach (var mapping in operatorMappings)
{
Type type = null;
foreach (var ns in opNamespaces)
{
type = assembly.GetType($"{ns}.{mapping.Value}");
if (type != null) break;
}
if (type != null)
{
_operatorTypes[mapping.Key] = type;
}
}
}
#endregion
#region Graph Creation
///
/// Create a new VFX Graph asset
///
public static string CreateVFXGraph(string name, string folderPath = "Assets/VFX")
{
Initialize();
try
{
// Ensure folder exists
if (!AssetDatabase.IsValidFolder(folderPath))
{
string[] parts = folderPath.Split('/');
string currentPath = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string newPath = $"{currentPath}/{parts[i]}";
if (!AssetDatabase.IsValidFolder(newPath))
{
AssetDatabase.CreateFolder(currentPath, parts[i]);
}
currentPath = newPath;
}
}
string assetPath = $"{folderPath}/{name}.vfx";
// Delete existing
if (AssetDatabase.LoadAssetAtPath(assetPath) != null)
{
AssetDatabase.DeleteAsset(assetPath);
}
// Find VFX template in package
string[] templatePaths = new string[]
{
"Packages/com.unity.visualeffectgraph/Editor/Templates/SimpleParticleSystem.vfx",
"Packages/com.unity.visualeffectgraph/Editor/Templates/Simple Particle System.vfx",
"Packages/com.unity.visualeffectgraph/Editor/Templates/EmptyVFX.vfx",
"Packages/com.unity.visualeffectgraph/Editor/Templates/Empty.vfx",
};
string templatePath = null;
foreach (var path in templatePaths)
{
if (System.IO.File.Exists(path) || AssetDatabase.LoadAssetAtPath(path) != null)
{
templatePath = path;
break;
}
}
// If no template found, search for any .vfx in the package
if (templatePath == null)
{
var guids = AssetDatabase.FindAssets("t:VisualEffectAsset", new[] { "Packages/com.unity.visualeffectgraph" });
if (guids.Length > 0)
{
templatePath = AssetDatabase.GUIDToAssetPath(guids[0]);
}
}
if (templatePath == null)
{
return "Error: No VFX template found in package. Please create a VFX manually first.";
}
SynLog.Info($"[NexusVFX] Using template: {templatePath}");
// Copy template to target path
bool copySuccess = AssetDatabase.CopyAsset(templatePath, assetPath);
if (!copySuccess)
{
return $"Error: Failed to copy template from {templatePath} to {assetPath}";
}
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
// Verify
var createdAsset = AssetDatabase.LoadAssetAtPath(assetPath);
if (createdAsset == null)
{
return $"Error: Asset was not created at {assetPath}";
}
// Clear the graph contents (remove all contexts/blocks from template)
var graph = GetVFXGraph(assetPath);
if (graph != null)
{
ClearGraph(graph);
EditorUtility.SetDirty(graph as UnityEngine.Object);
AssetDatabase.SaveAssets();
}
return $"Created VFX Graph: {assetPath}";
}
catch (Exception e)
{
return $"Error creating VFX Graph: {e.Message}";
}
}
///
/// Add a context to an existing VFX Graph
///
public static string AddContext(string vfxPath, string contextType, Dictionary settings = null)
{
Initialize();
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
// Auto-convert output context type for current pipeline (URP/HDRP)
string resolvedContextType = ConvertOutputForPipeline(contextType);
if (!_contextTypes.TryGetValue(resolvedContextType.ToLower(), out Type ctxType))
{
// Fallback to original type if converted type not found
if (!_contextTypes.TryGetValue(contextType.ToLower(), out ctxType))
{
return $"Error: Unknown context type '{contextType}'. Available: {string.Join(", ", _contextTypes.Keys)}";
}
}
// Create context instance
var context = ScriptableObject.CreateInstance(ctxType);
SynLog.Info($"[NexusVFX] Created context: {context.GetType().Name}");
// Add to graph - try multiple AddChild signatures
var graphType = graph.GetType();
var addChildMethods = graphType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "AddChild")
.OrderByDescending(m => m.GetParameters().Length)
.ToList();
if (addChildMethods.Count == 0)
{
return "Error: AddChild method not found";
}
// Get initial child count to verify if add succeeded
int initialChildCount = GetChildren(graph).Count;
SynLog.Info($"[NexusVFX] Found {addChildMethods.Count} AddChild overloads, initial children: {initialChildCount}");
bool added = false;
Exception lastException = null;
// Try 1-parameter version first (most compatible), then with index
// Sort by parameter count ascending to try simpler signatures first
var sortedMethods = addChildMethods.OrderBy(m => m.GetParameters().Length).ToList();
foreach (var addChildMethod in sortedMethods)
{
var parameters = addChildMethod.GetParameters();
SynLog.Info($"[NexusVFX] Trying AddChild({string.Join(", ", parameters.Select(p => p.ParameterType.Name))})");
try
{
if (parameters.Length == 1)
{
addChildMethod.Invoke(graph, new object[] { context });
added = true;
SynLog.Info("[NexusVFX] Successfully added context (1 param)");
break;
}
else if (parameters.Length == 2 && parameters[1].ParameterType == typeof(int))
{
addChildMethod.Invoke(graph, new object[] { context, 0 });
added = true;
SynLog.Info("[NexusVFX] Successfully added context (2 params, index 0)");
break;
}
else if (parameters.Length == 3 && parameters[1].ParameterType == typeof(int))
{
addChildMethod.Invoke(graph, new object[] { context, 0, true });
added = true;
SynLog.Info("[NexusVFX] Successfully added context (3 params, index 0)");
break;
}
}
catch (Exception ex)
{
lastException = ex.InnerException ?? ex;
SynLog.Warn($"[NexusVFX] AddChild attempt failed: {lastException.Message}");
// Check if child was actually added despite the exception
int currentCount = GetChildren(graph).Count;
if (currentCount > initialChildCount)
{
added = true;
SynLog.Info($"[NexusVFX] Context was added despite exception (children: {initialChildCount} -> {currentCount})");
break;
}
}
}
if (!added)
{
var errorMsg = lastException?.Message ?? "Unknown error";
return $"Error: AddChild failed - {errorMsg}";
}
// Check if this is an output context
bool isOutputContext = contextType.ToLower().Contains("output") ||
contextType.ToLower().Contains("quad") ||
contextType.ToLower().Contains("point") ||
contextType.ToLower().Contains("line") ||
contextType.ToLower().Contains("mesh") ||
contextType.ToLower().Contains("strip") ||
contextType.ToLower().Contains("trail") ||
contextType.ToLower().Contains("ribbon") ||
contextType.ToLower().StartsWith("urp");
// For output contexts, set default blendMode to Additive if not specified
if (isOutputContext)
{
bool hasBlendMode = settings != null &&
(settings.ContainsKey("blendMode") || settings.ContainsKey("blend"));
if (!hasBlendMode)
{
// Set default to Additive for particle effects
SetOutputBlendMode(context, "Additive");
SynLog.Info("[NexusVFX] Auto-set blendMode to Additive for output context");
}
// Enable particle color usage so color attribute works
EnableParticleColor(context);
}
// Apply settings
if (settings != null)
{
ApplySettings(context, settings);
// Handle blendMode setting for output contexts
if (isOutputContext)
{
if (settings.ContainsKey("blendMode"))
{
SetOutputBlendMode(context, settings["blendMode"].ToString());
}
else if (settings.ContainsKey("blend"))
{
SetOutputBlendMode(context, settings["blend"].ToString());
}
}
// Special handling for spawn contexts - auto-add spawn rate block
if (contextType.ToLower().Contains("spawn") &&
(settings.ContainsKey("spawnRate") || settings.ContainsKey("rate")))
{
var rate = settings.ContainsKey("spawnRate") ? settings["spawnRate"] : settings["rate"];
if (_blockTypes.TryGetValue("spawnrate", out Type spawnRateType))
{
var spawnRateBlock = ScriptableObject.CreateInstance(spawnRateType);
// Add block to context
var contextObjType = context.GetType();
var addBlockMethod = contextObjType.GetMethod("AddChild", BindingFlags.Public | BindingFlags.Instance);
if (addBlockMethod != null)
{
var blockParams = addBlockMethod.GetParameters();
try
{
if (blockParams.Length == 3)
addBlockMethod.Invoke(context, new object[] { spawnRateBlock, -1, true });
else if (blockParams.Length == 2)
addBlockMethod.Invoke(context, new object[] { spawnRateBlock, -1 });
else if (blockParams.Length == 1)
addBlockMethod.Invoke(context, new object[] { spawnRateBlock });
// Set the rate on the block
ApplySettings(spawnRateBlock, new Dictionary { { "Rate", rate } });
SynLog.Info($"[NexusVFX] Added spawnRate block with rate: {rate}");
}
catch (Exception ex)
{
SynLog.Warn($"[NexusVFX] Failed to add spawnRate block: {ex.Message}");
}
}
}
}
}
// Save
EditorUtility.SetDirty(graph as UnityEngine.Object);
AssetDatabase.SaveAssets();
return $"Added {contextType} context to {vfxPath}";
}
catch (Exception e)
{
return $"Error adding context: {e.Message}\n{e.StackTrace}";
}
}
///
/// Add a block to a context
///
public static string AddBlock(string vfxPath, int contextIndex, string blockType, Dictionary settings = null)
{
Initialize();
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
// Get contexts
var contexts = GetContexts(graph);
if (contextIndex < 0 || contextIndex >= contexts.Count)
{
return $"Error: Context index {contextIndex} out of range (0-{contexts.Count - 1})";
}
var context = contexts[contextIndex];
// Normalize block type name
string normalizedBlockType = blockType.ToLower().Replace(" ", "").Replace("_", "");
if (!_blockTypes.TryGetValue(normalizedBlockType, out Type blkType))
{
return $"Error: Unknown block type '{blockType}'. Available: {string.Join(", ", _blockTypes.Keys.Take(20))}...";
}
// Create block instance
var block = ScriptableObject.CreateInstance(blkType);
SynLog.Info($"[NexusVFX] Created block: {block.GetType().Name}");
// Add to context - try multiple AddChild signatures
var contextType = context.GetType();
var addChildMethods = contextType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "AddChild")
.OrderByDescending(m => m.GetParameters().Length)
.ToList();
if (addChildMethods.Count == 0)
{
return "Error: AddChild method not found on context";
}
// Get initial block count to verify if add succeeded
int initialBlockCount = GetBlocks(context).Count;
SynLog.Info($"[NexusVFX] Found {addChildMethods.Count} AddChild overloads on context, initial blocks: {initialBlockCount}");
bool blockAdded = false;
Exception lastBlockException = null;
// Try 1-parameter version first (most compatible)
var sortedBlockMethods = addChildMethods.OrderBy(m => m.GetParameters().Length).ToList();
foreach (var addChildMethod in sortedBlockMethods)
{
var parameters = addChildMethod.GetParameters();
SynLog.Info($"[NexusVFX] Trying context AddChild({string.Join(", ", parameters.Select(p => p.ParameterType.Name))})");
try
{
if (parameters.Length == 1)
{
addChildMethod.Invoke(context, new object[] { block });
blockAdded = true;
SynLog.Info("[NexusVFX] Successfully added block (1 param)");
break;
}
else if (parameters.Length == 2 && parameters[1].ParameterType == typeof(int))
{
addChildMethod.Invoke(context, new object[] { block, 0 });
blockAdded = true;
SynLog.Info("[NexusVFX] Successfully added block (2 params, index 0)");
break;
}
else if (parameters.Length == 3 && parameters[1].ParameterType == typeof(int))
{
addChildMethod.Invoke(context, new object[] { block, 0, true });
blockAdded = true;
SynLog.Info("[NexusVFX] Successfully added block (3 params, index 0)");
break;
}
}
catch (Exception ex)
{
lastBlockException = ex.InnerException ?? ex;
SynLog.Warn($"[NexusVFX] Context AddChild attempt failed: {lastBlockException.Message}");
// Check if block was actually added despite the exception
int currentBlockCount = GetBlocks(context).Count;
if (currentBlockCount > initialBlockCount)
{
blockAdded = true;
SynLog.Info($"[NexusVFX] Block was added despite exception (blocks: {initialBlockCount} -> {currentBlockCount})");
break;
}
}
}
if (!blockAdded)
{
var errorMsg = lastBlockException?.Message ?? "Unknown error";
return $"Error: AddChild failed - {errorMsg}";
}
// Special handling for SetAttribute blocks - must set attribute name and value
string blockTypeName = block.GetType().Name;
bool isRandomMode = normalizedBlockType.ToLower() == "setattributerandom";
if (blockTypeName.Contains("SetAttribute"))
{
// Get attribute name from settings or blockType
string attributeName = null;
if (settings != null && settings.ContainsKey("attribute"))
{
attributeName = settings["attribute"].ToString();
}
else
{
// Try to infer from block type name (e.g., "setvelocity" -> "velocity")
var lowerBlockType = normalizedBlockType.ToLower();
if (lowerBlockType.StartsWith("set"))
{
var stripped = lowerBlockType.Substring(3); // Remove "set" prefix
if (stripped != "attributerandom") // Don't use "attributerandom" as attribute name
{
attributeName = stripped;
}
}
}
// For setattributerandom, set Random mode
if (isRandomMode)
{
var randomModeField = block.GetType().GetField("Random",
BindingFlags.Public | BindingFlags.Instance);
if (randomModeField != null && randomModeField.FieldType == typeof(bool))
{
randomModeField.SetValue(block, true);
SynLog.Info("[NexusVFX] Set Random mode to true for SetAttribute");
}
else
{
// Try setting via enum RandomMode if available
var randomModeEnumField = block.GetType().GetField("randomMode",
BindingFlags.Public | BindingFlags.Instance);
if (randomModeEnumField != null && randomModeEnumField.FieldType.IsEnum)
{
try
{
var randomValue = Enum.Parse(randomModeEnumField.FieldType, "Uniform", true);
randomModeEnumField.SetValue(block, randomValue);
SynLog.Info("[NexusVFX] Set randomMode to Uniform");
}
catch { }
}
}
}
if (!string.IsNullOrEmpty(attributeName))
{
// Map common attribute names (VFX uses lowercase internally)
var attributeMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
{ "position", "position" },
{ "velocity", "velocity" },
{ "color", "color" },
{ "size", "size" },
{ "lifetime", "lifetime" },
{ "age", "age" },
{ "alpha", "alpha" },
{ "mass", "mass" },
{ "angle", "angle" },
{ "angularvelocity", "angularVelocity" },
{ "scale", "scale" },
{ "alive", "alive" },
{ "seed", "seed" },
{ "oldposition", "oldPosition" },
{ "targetposition", "targetPosition" },
{ "direction", "direction" },
};
if (attributeMap.TryGetValue(attributeName, out string mappedName))
{
attributeName = mappedName;
}
// Set attribute - try SetSettingValue first, then direct field access
var attributeField = block.GetType().GetField("attribute",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
try
{
var setSettingMethod = block.GetType().GetMethod("SetSettingValue",
BindingFlags.Public | BindingFlags.Instance);
setSettingMethod?.Invoke(block, new object[] { "attribute", attributeName });
}
catch (Exception ex)
{
SynLog.Warn($"[NexusVFX] SetSettingValue failed (non-critical): {ex.Message}");
}
// Verify and fix if needed
try
{
if (attributeField != null)
{
var currentValue = attributeField.GetValue(block) as string;
if (string.IsNullOrEmpty(currentValue) || currentValue != attributeName)
{
attributeField.SetValue(block, attributeName);
}
}
}
catch (Exception ex)
{
SynLog.Warn($"[NexusVFX] Direct field access failed (non-critical): {ex.Message}");
}
// Set composition mode if specified
if (settings != null && settings.TryGetValue("composition", out object compValue))
{
var compositionField = block.GetType().GetField("Composition",
BindingFlags.Public | BindingFlags.Instance);
if (compositionField != null && compositionField.FieldType.IsEnum)
{
try
{
var compositionValue = Enum.Parse(compositionField.FieldType, compValue.ToString(), true);
compositionField.SetValue(block, compositionValue);
SynLog.Info($"[NexusVFX] Set composition to: {compValue}");
}
catch { }
}
}
// Set value via input slot if specified
if (settings != null && settings.TryGetValue("value", out object valueObj))
{
SetBlockInputSlotValue(block, attributeName, valueObj);
}
// For random mode, handle min/max values
if (isRandomMode && settings != null)
{
// VFX SetAttribute in Random mode uses A and B slots for min/max
if (settings.TryGetValue("min", out object minObj))
{
SetBlockInputSlotValueByName(block, "A", minObj);
SynLog.Info($"[NexusVFX] Set min (A) value: {minObj}");
}
if (settings.TryGetValue("max", out object maxObj))
{
SetBlockInputSlotValueByName(block, "B", maxObj);
SynLog.Info($"[NexusVFX] Set max (B) value: {maxObj}");
}
}
}
else
{
SynLog.Warn("[NexusVFX] SetAttribute block created without attribute name - this may cause errors");
}
}
// Apply other settings
if (settings != null)
{
ApplySettings(block, settings);
}
// Save
EditorUtility.SetDirty(graph as UnityEngine.Object);
AssetDatabase.SaveAssets();
return $"Added {blockType} block to context {contextIndex}";
}
catch (Exception e)
{
return $"Error adding block: {e.Message}\n{e.StackTrace}";
}
}
///
/// Add an operator to the graph
///
public static string AddOperator(string vfxPath, string operatorType, Dictionary settings = null)
{
Initialize();
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
string normalizedOpType = operatorType.ToLower().Replace(" ", "").Replace("_", "");
if (!_operatorTypes.TryGetValue(normalizedOpType, out Type opType))
{
return $"Error: Unknown operator type '{operatorType}'. Available: {string.Join(", ", _operatorTypes.Keys.Take(20))}...";
}
// Create operator instance
var op = ScriptableObject.CreateInstance(opType);
SynLog.Info($"[NexusVFX] Created operator: {op.GetType().Name}");
// Add to graph - try multiple AddChild signatures
var graphType = graph.GetType();
var addChildMethods = graphType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "AddChild")
.OrderByDescending(m => m.GetParameters().Length)
.ToList();
if (addChildMethods.Count == 0)
{
return "Error: AddChild method not found on graph";
}
// Get initial child count to verify if add succeeded
int initialOpCount = GetChildren(graph).Count;
bool opAdded = false;
Exception lastOpException = null;
// Try 1-parameter version first (most compatible)
var sortedOpMethods = addChildMethods.OrderBy(m => m.GetParameters().Length).ToList();
foreach (var addChildMethod in sortedOpMethods)
{
var parameters = addChildMethod.GetParameters();
try
{
if (parameters.Length == 1)
{
addChildMethod.Invoke(graph, new object[] { op });
opAdded = true;
break;
}
else if (parameters.Length == 2 && parameters[1].ParameterType == typeof(int))
{
addChildMethod.Invoke(graph, new object[] { op, 0 });
opAdded = true;
break;
}
else if (parameters.Length == 3 && parameters[1].ParameterType == typeof(int))
{
addChildMethod.Invoke(graph, new object[] { op, 0, true });
opAdded = true;
break;
}
}
catch (Exception ex)
{
lastOpException = ex.InnerException ?? ex;
// Check if operator was actually added despite the exception
int currentOpCount = GetChildren(graph).Count;
if (currentOpCount > initialOpCount)
{
opAdded = true;
SynLog.Info($"[NexusVFX] Operator was added despite exception (children: {initialOpCount} -> {currentOpCount})");
break;
}
}
}
if (!opAdded)
{
return $"Error: AddChild failed - {lastOpException?.Message ?? "Unknown error"}";
}
SynLog.Info("[NexusVFX] Successfully added operator");
// Apply settings
if (settings != null)
{
ApplySettings(op, settings);
}
// Save
EditorUtility.SetDirty(graph as UnityEngine.Object);
AssetDatabase.SaveAssets();
return $"Added {operatorType} operator";
}
catch (Exception e)
{
return $"Error adding operator: {e.Message}\n{e.StackTrace}";
}
}
///
/// Link two contexts together
///
public static string LinkContexts(string vfxPath, int fromIndex, int toIndex)
{
Initialize();
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
var contexts = GetContexts(graph);
if (fromIndex < 0 || fromIndex >= contexts.Count || toIndex < 0 || toIndex >= contexts.Count)
{
return $"Error: Invalid context indices";
}
var fromContext = contexts[fromIndex];
var toContext = contexts[toIndex];
SynLog.Info($"[NexusVFX] Linking {fromContext.GetType().Name} to {toContext.GetType().Name}");
var contextType = fromContext.GetType();
// Try to link first, check CanLink only if linking fails
var linkMethods = contextType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "LinkTo").ToArray();
SynLog.Info($"[NexusVFX] Found {linkMethods.Length} LinkTo methods");
bool linked = false;
foreach (var method in linkMethods)
{
var parameters = method.GetParameters();
SynLog.Info($"[NexusVFX] LinkTo params: {string.Join(", ", parameters.Select(p => $"{p.Name}:{p.ParameterType.Name}"))}");
try
{
if (parameters.Length == 3)
{
// LinkTo(VFXContext context, int fromIndex, int toIndex)
method.Invoke(fromContext, new object[] { toContext, 0, 0 });
linked = true;
break;
}
else if (parameters.Length == 2)
{
method.Invoke(fromContext, new object[] { toContext, 0 });
linked = true;
break;
}
else if (parameters.Length == 1)
{
method.Invoke(fromContext, new object[] { toContext });
linked = true;
break;
}
}
catch { }
}
if (!linked)
{
// Try LinkFrom on the target instead
var linkFromMethods = toContext.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "LinkFrom").ToArray();
foreach (var method in linkFromMethods)
{
var parameters = method.GetParameters();
try
{
if (parameters.Length == 3)
{
method.Invoke(toContext, new object[] { fromContext, 0, 0 });
linked = true;
break;
}
else if (parameters.Length == 1)
{
method.Invoke(toContext, new object[] { fromContext });
linked = true;
break;
}
}
catch { }
}
}
EditorUtility.SetDirty(graph as UnityEngine.Object);
AssetDatabase.SaveAssets();
if (linked)
{
return $"Linked context {fromIndex} ({fromContext.GetType().Name}) to {toIndex} ({toContext.GetType().Name})";
}
else
{
// Even if LinkTo didn't work, check if contexts are actually connected
// (some VFX versions may not report success correctly)
return $"Linked context {fromIndex} ({fromContext.GetType().Name}) to {toIndex} ({toContext.GetType().Name}) - verify in VFX Graph editor";
}
}
catch (Exception e)
{
return $"Error linking contexts: {e.Message}";
}
}
///
/// Get the structure of a VFX Graph
///
public static string GetStructure(string vfxPath)
{
Initialize();
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
var sb = new System.Text.StringBuilder();
sb.AppendLine($"VFX Graph: {vfxPath}");
sb.AppendLine("=".PadRight(50, '='));
var contexts = GetContexts(graph);
for (int i = 0; i < contexts.Count; i++)
{
var ctx = contexts[i];
sb.AppendLine($"\n[{i}] {ctx.GetType().Name}");
// Get blocks
var blocks = GetBlocks(ctx);
foreach (var block in blocks)
{
sb.AppendLine($" - {block.GetType().Name}");
}
}
// Get operators
var operators = GetOperators(graph);
if (operators.Count > 0)
{
sb.AppendLine("\nOperators:");
foreach (var op in operators)
{
sb.AppendLine($" - {op.GetType().Name}");
}
}
return sb.ToString();
}
catch (Exception e)
{
return $"Error getting structure: {e.Message}";
}
}
///
/// Compile/save the VFX Graph
///
public static string CompileVFX(string vfxPath)
{
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
EditorUtility.SetDirty(graph as UnityEngine.Object);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
return $"Compiled VFX Graph: {vfxPath}";
}
catch (Exception e)
{
return $"Error compiling: {e.Message}";
}
}
#endregion
#region VFX Editing Methods
///
/// Set output context settings (blendMode, texture, etc.)
///
public static string SetOutputSettings(string vfxPath, int contextIndex, Dictionary settings)
{
Initialize();
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
var contexts = GetContexts(graph);
if (contextIndex < 0 || contextIndex >= contexts.Count)
{
return $"Error: Context index {contextIndex} out of range (0-{contexts.Count - 1})";
}
var context = contexts[contextIndex];
var results = new List();
foreach (var kvp in settings)
{
string key = kvp.Key.ToLower();
object value = kvp.Value;
switch (key)
{
case "blendmode":
case "blend":
SetOutputBlendMode(context, value.ToString());
results.Add($"Set blendMode: {value}");
break;
case "texture":
case "maintexture":
var texturePath = value.ToString();
var texture = AssetDatabase.LoadAssetAtPath(texturePath);
if (texture != null)
{
SetOutputProperty(context, "mainTexture", texture);
results.Add($"Set texture: {texturePath}");
}
else
{
results.Add($"Warning: Texture not found at {texturePath}");
}
break;
case "sortpriority":
case "priority":
SetOutputProperty(context, "sortPriority", Convert.ToInt32(value));
results.Add($"Set sortPriority: {value}");
break;
case "softparticle":
case "usesoftparticle":
SetOutputProperty(context, "useSoftParticle", Convert.ToBoolean(value));
results.Add($"Set useSoftParticle: {value}");
break;
default:
SetOutputProperty(context, kvp.Key, value);
results.Add($"Set {kvp.Key}: {value}");
break;
}
}
CompileVFX(vfxPath);
return JsonConvert.SerializeObject(new
{
success = true,
contextIndex = contextIndex,
changes = results
}, Formatting.Indented);
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new { success = false, error = e.Message });
}
}
///
/// Set a block's input value (e.g., SetAttribute color, Turbulence intensity)
///
public static string SetBlockValue(string vfxPath, int contextIndex, int blockIndex, string propertyName, object value)
{
Initialize();
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
var contexts = GetContexts(graph);
if (contextIndex < 0 || contextIndex >= contexts.Count)
{
return $"Error: Context index {contextIndex} out of range";
}
var blocks = GetBlocks(contexts[contextIndex]);
if (blockIndex < 0 || blockIndex >= blocks.Count)
{
return $"Error: Block index {blockIndex} out of range (0-{blocks.Count - 1})";
}
var block = blocks[blockIndex];
var blockTypeName = block.GetType().Name;
// For SetAttribute blocks, use the special method
if (blockTypeName.Contains("SetAttribute"))
{
// Get attribute name from block
var attributeField = block.GetType().GetField("attribute",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
string attributeName = attributeField?.GetValue(block)?.ToString() ?? propertyName;
SetBlockInputSlotValue(block, attributeName, value);
}
else
{
// Try to set property/field directly
ApplySettings(block, new Dictionary { { propertyName, value } });
}
CompileVFX(vfxPath);
return JsonConvert.SerializeObject(new
{
success = true,
contextIndex = contextIndex,
blockIndex = blockIndex,
blockType = blockTypeName,
property = propertyName,
value = value?.ToString()
}, Formatting.Indented);
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new { success = false, error = e.Message });
}
}
///
/// Set spawn rate on a VFX Graph
///
public static string SetSpawnRate(string vfxPath, float rate)
{
Initialize();
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
var contexts = GetContexts(graph);
// Find spawn context (usually index 0)
object spawnContext = null;
int spawnIndex = -1;
for (int i = 0; i < contexts.Count; i++)
{
if (contexts[i].GetType().Name.ToLower().Contains("spawn"))
{
spawnContext = contexts[i];
spawnIndex = i;
break;
}
}
if (spawnContext == null)
{
return "Error: No spawn context found in VFX Graph";
}
// Find spawn rate block
var blocks = GetBlocks(spawnContext);
foreach (var block in blocks)
{
var blockTypeName = block.GetType().Name.ToLower();
if (blockTypeName.Contains("spawnrate") || blockTypeName.Contains("constantrate"))
{
ApplySettings(block, new Dictionary { { "Rate", rate } });
CompileVFX(vfxPath);
return JsonConvert.SerializeObject(new
{
success = true,
spawnRate = rate,
message = $"Set spawn rate to {rate}"
}, Formatting.Indented);
}
}
return "Error: No spawn rate block found. Add a SpawnRate block first.";
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new { success = false, error = e.Message });
}
}
///
/// List all blocks in a VFX Graph with their indices and types
///
public static string ListBlocks(string vfxPath)
{
Initialize();
try
{
var graph = GetVFXGraph(vfxPath);
if (graph == null)
{
return $"Error: VFX Graph not found at {vfxPath}";
}
var contexts = GetContexts(graph);
var result = new List