97ac0f71f5
https://assetstore.unity.com/packages/tools/generative-ai/synaptic-ai-pro-natural-language-control-for-unity-336030
4516 lines
182 KiB
C#
4516 lines
182 KiB
C#
#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
|
|
{
|
|
/// <summary>
|
|
/// VFX Graph Builder - Programmatic creation of VFX Graphs
|
|
/// Provides full access to VFX Graph features via MCP tools
|
|
/// </summary>
|
|
public static class NexusVFXBuilder
|
|
{
|
|
// Cache for VFX types (populated via reflection)
|
|
private static Dictionary<string, Type> _contextTypes;
|
|
private static Dictionary<string, Type> _blockTypes;
|
|
private static Dictionary<string, Type> _operatorTypes;
|
|
private static bool _initialized = false;
|
|
|
|
// Output type mapping for pipeline conversion (Built-in -> URP/HDRP)
|
|
private static readonly Dictionary<string, string> _urpOutputMapping = new Dictionary<string, string>(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<string, string> _hdrpOutputMapping = new Dictionary<string, string>(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
|
|
|
|
/// <summary>
|
|
/// Detect the current rendering pipeline
|
|
/// </summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert output context type to pipeline-appropriate type
|
|
/// </summary>
|
|
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<string, Type>(StringComparer.OrdinalIgnoreCase);
|
|
_blockTypes = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
|
_operatorTypes = new Dictionary<string, Type>(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<string, string>
|
|
{
|
|
// 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<string, string>
|
|
{
|
|
// 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<string, string>
|
|
{
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Create a new VFX Graph asset
|
|
/// </summary>
|
|
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<VisualEffectAsset>(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<VisualEffectAsset>(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<VisualEffectAsset>(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}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a context to an existing VFX Graph
|
|
/// </summary>
|
|
public static string AddContext(string vfxPath, string contextType, Dictionary<string, object> 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<string, object> { { "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}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a block to a context
|
|
/// </summary>
|
|
public static string AddBlock(string vfxPath, int contextIndex, string blockType, Dictionary<string, object> 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<string, string>(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}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add an operator to the graph
|
|
/// </summary>
|
|
public static string AddOperator(string vfxPath, string operatorType, Dictionary<string, object> 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}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Link two contexts together
|
|
/// </summary>
|
|
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}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the structure of a VFX Graph
|
|
/// </summary>
|
|
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}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compile/save the VFX Graph
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Set output context settings (blendMode, texture, etc.)
|
|
/// </summary>
|
|
public static string SetOutputSettings(string vfxPath, int contextIndex, Dictionary<string, object> 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<string>();
|
|
|
|
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<Texture2D>(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 });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set a block's input value (e.g., SetAttribute color, Turbulence intensity)
|
|
/// </summary>
|
|
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<string, object> { { 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 });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set spawn rate on a VFX Graph
|
|
/// </summary>
|
|
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<string, object> { { "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 });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// List all blocks in a VFX Graph with their indices and types
|
|
/// </summary>
|
|
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<object>();
|
|
|
|
for (int ctxIdx = 0; ctxIdx < contexts.Count; ctxIdx++)
|
|
{
|
|
var context = contexts[ctxIdx];
|
|
var contextType = context.GetType().Name;
|
|
var blocks = GetBlocks(context);
|
|
var blockList = new List<object>();
|
|
|
|
for (int blkIdx = 0; blkIdx < blocks.Count; blkIdx++)
|
|
{
|
|
var block = blocks[blkIdx];
|
|
var blockType = block.GetType().Name;
|
|
|
|
// Try to get attribute name for SetAttribute blocks
|
|
string attributeName = null;
|
|
if (blockType.Contains("SetAttribute"))
|
|
{
|
|
var attrField = block.GetType().GetField("attribute",
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
|
attributeName = attrField?.GetValue(block)?.ToString();
|
|
}
|
|
|
|
blockList.Add(new
|
|
{
|
|
index = blkIdx,
|
|
type = blockType,
|
|
attribute = attributeName
|
|
});
|
|
}
|
|
|
|
result.Add(new
|
|
{
|
|
contextIndex = ctxIdx,
|
|
contextType = contextType,
|
|
blocks = blockList
|
|
});
|
|
}
|
|
|
|
return JsonConvert.SerializeObject(new
|
|
{
|
|
success = true,
|
|
path = vfxPath,
|
|
contexts = result
|
|
}, Formatting.Indented);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonConvert.SerializeObject(new { success = false, error = e.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a block from a context
|
|
/// </summary>
|
|
public static string RemoveBlock(string vfxPath, int contextIndex, int blockIndex)
|
|
{
|
|
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 context = contexts[contextIndex];
|
|
var blocks = GetBlocks(context);
|
|
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;
|
|
|
|
// Remove block using RemoveChild method
|
|
var removeChildMethods = context.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(m => m.Name == "RemoveChild").ToArray();
|
|
|
|
foreach (var removeMethod in removeChildMethods)
|
|
{
|
|
var parameters = removeMethod.GetParameters();
|
|
try
|
|
{
|
|
if (parameters.Length == 1)
|
|
{
|
|
removeMethod.Invoke(context, new object[] { block });
|
|
}
|
|
else if (parameters.Length == 2)
|
|
{
|
|
// RemoveChild(model, notify)
|
|
removeMethod.Invoke(context, new object[] { block, true });
|
|
}
|
|
else if (parameters.Length == 3)
|
|
{
|
|
// RemoveChild(model, index, notify)
|
|
removeMethod.Invoke(context, new object[] { block, blockIndex, true });
|
|
}
|
|
|
|
CompileVFX(vfxPath);
|
|
|
|
return JsonConvert.SerializeObject(new
|
|
{
|
|
success = true,
|
|
removed = blockTypeName,
|
|
contextIndex = contextIndex,
|
|
blockIndex = blockIndex
|
|
}, Formatting.Indented);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] RemoveChild with {parameters.Length} params failed: {ex.Message}");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return "Error: Could not find working RemoveChild method";
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonConvert.SerializeObject(new { success = false, error = e.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get detailed info about a specific block's current values
|
|
/// </summary>
|
|
public static string GetBlockInfo(string vfxPath, int contextIndex, int blockIndex)
|
|
{
|
|
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";
|
|
}
|
|
|
|
var block = blocks[blockIndex];
|
|
var blockType = block.GetType();
|
|
var info = new Dictionary<string, object>
|
|
{
|
|
["type"] = blockType.Name,
|
|
["fullType"] = blockType.FullName
|
|
};
|
|
|
|
// Get input slots
|
|
var inputSlotsProperty = blockType.GetProperty("inputSlots",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
if (inputSlotsProperty != null)
|
|
{
|
|
var inputSlots = inputSlotsProperty.GetValue(block) as System.Collections.IEnumerable;
|
|
if (inputSlots != null)
|
|
{
|
|
var slots = new List<object>();
|
|
foreach (var slot in inputSlots)
|
|
{
|
|
var slotType = slot.GetType();
|
|
var nameProperty = slotType.GetProperty("name", BindingFlags.Public | BindingFlags.Instance);
|
|
var valueProperty = slotType.GetProperty("value", BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
slots.Add(new
|
|
{
|
|
name = nameProperty?.GetValue(slot)?.ToString(),
|
|
slotType = slotType.Name,
|
|
value = valueProperty?.GetValue(slot)?.ToString()
|
|
});
|
|
}
|
|
info["inputSlots"] = slots;
|
|
}
|
|
}
|
|
|
|
// Get attribute for SetAttribute blocks
|
|
if (blockType.Name.Contains("SetAttribute"))
|
|
{
|
|
var attrField = blockType.GetField("attribute",
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
|
info["attribute"] = attrField?.GetValue(block)?.ToString();
|
|
}
|
|
|
|
return JsonConvert.SerializeObject(new
|
|
{
|
|
success = true,
|
|
contextIndex = contextIndex,
|
|
blockIndex = blockIndex,
|
|
blockInfo = info
|
|
}, Formatting.Indented);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return JsonConvert.SerializeObject(new { success = false, error = e.Message });
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
private static object GetVFXGraph(string assetPath)
|
|
{
|
|
// Force refresh and import
|
|
AssetDatabase.Refresh();
|
|
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
|
|
|
|
// Load VisualEffectAsset
|
|
var vfxAsset = AssetDatabase.LoadAssetAtPath<VisualEffectAsset>(assetPath);
|
|
if (vfxAsset == null)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] VisualEffectAsset not found at {assetPath}");
|
|
return null;
|
|
}
|
|
|
|
SynLog.Info($"[NexusVFX] Loaded VFXAsset: {vfxAsset.name}");
|
|
|
|
// Get VFX Editor assembly
|
|
var vfxEditorAssembly = GetVFXEditorAssembly();
|
|
if (vfxEditorAssembly == null)
|
|
{
|
|
SynLog.Warn("[NexusVFX] VFX Editor assembly not found");
|
|
return null;
|
|
}
|
|
|
|
// Try VisualEffectObjectExtensions.GetResource() static method
|
|
var extensionsType = vfxEditorAssembly.GetType("UnityEditor.VFX.VisualEffectObjectExtensions");
|
|
if (extensionsType != null)
|
|
{
|
|
SynLog.Info("[NexusVFX] Found VisualEffectObjectExtensions");
|
|
|
|
var getResourceMethod = extensionsType.GetMethod("GetResource",
|
|
BindingFlags.Public | BindingFlags.Static);
|
|
|
|
if (getResourceMethod != null)
|
|
{
|
|
SynLog.Info("[NexusVFX] Found GetResource method");
|
|
try
|
|
{
|
|
var resource = getResourceMethod.Invoke(null, new object[] { vfxAsset });
|
|
if (resource != null)
|
|
{
|
|
SynLog.Info($"[NexusVFX] Got resource: {resource.GetType().Name}");
|
|
|
|
// Get graph property from resource
|
|
var resourceType = resource.GetType();
|
|
var graphProp = resourceType.GetProperty("graph",
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
if (graphProp != null)
|
|
{
|
|
var graph = graphProp.GetValue(resource);
|
|
if (graph != null)
|
|
{
|
|
SynLog.Info($"[NexusVFX] Got VFXGraph: {graph.GetType().Name}");
|
|
return graph;
|
|
}
|
|
}
|
|
|
|
// Try GetOrCreateGraph
|
|
var getGraphMethod = resourceType.GetMethod("GetOrCreateGraph",
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
|
if (getGraphMethod != null)
|
|
{
|
|
var graph = getGraphMethod.Invoke(resource, null);
|
|
if (graph != null)
|
|
{
|
|
SynLog.Info($"[NexusVFX] Got VFXGraph via GetOrCreateGraph");
|
|
return graph;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SynLog.Warn("[NexusVFX] GetResource returned null");
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] GetResource failed: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try VisualEffectAssetExtensions.GetOrCreateGraph()
|
|
var assetExtType = vfxEditorAssembly.GetType("UnityEditor.VFX.VisualEffectAssetExtensions");
|
|
if (assetExtType != null)
|
|
{
|
|
SynLog.Info("[NexusVFX] Found VisualEffectAssetExtensions");
|
|
var getGraphMethod = assetExtType.GetMethod("GetOrCreateGraph",
|
|
BindingFlags.Public | BindingFlags.Static);
|
|
if (getGraphMethod != null)
|
|
{
|
|
try
|
|
{
|
|
var graph = getGraphMethod.Invoke(null, new object[] { vfxAsset });
|
|
if (graph != null)
|
|
{
|
|
SynLog.Info($"[NexusVFX] Got graph via VisualEffectAssetExtensions");
|
|
return graph;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] GetOrCreateGraph failed: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: try loading sub-assets
|
|
var subAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
|
|
SynLog.Info($"[NexusVFX] Checking {subAssets.Length} sub-assets");
|
|
foreach (var sub in subAssets)
|
|
{
|
|
if (sub != null)
|
|
{
|
|
SynLog.Info($"[NexusVFX] Sub-asset: {sub.name} ({sub.GetType().Name})");
|
|
if (sub.GetType().Name == "VFXGraph")
|
|
{
|
|
SynLog.Info("[NexusVFX] Found VFXGraph in sub-assets");
|
|
return sub;
|
|
}
|
|
}
|
|
}
|
|
|
|
SynLog.Warn("[NexusVFX] Could not get VFXGraph from asset");
|
|
return null;
|
|
}
|
|
|
|
private static void ClearGraph(object graph)
|
|
{
|
|
if (graph == null) return;
|
|
|
|
try
|
|
{
|
|
// Get all children and remove them
|
|
var graphType = graph.GetType();
|
|
var childrenProp = graphType.GetProperty("children",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (childrenProp != null)
|
|
{
|
|
var children = childrenProp.GetValue(graph) as System.Collections.IEnumerable;
|
|
if (children != null)
|
|
{
|
|
var childList = new List<object>();
|
|
foreach (var child in children)
|
|
{
|
|
childList.Add(child);
|
|
}
|
|
|
|
// Find RemoveChild method with correct signature
|
|
var removeMethods = graphType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(m => m.Name == "RemoveChild").ToArray();
|
|
|
|
foreach (var child in childList)
|
|
{
|
|
foreach (var method in removeMethods)
|
|
{
|
|
var parameters = method.GetParameters();
|
|
if (parameters.Length == 1)
|
|
{
|
|
var paramType = parameters[0].ParameterType;
|
|
if (paramType.IsAssignableFrom(child.GetType()))
|
|
{
|
|
try
|
|
{
|
|
method.Invoke(graph, new object[] { child });
|
|
break;
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Failed to clear graph: {e.Message}");
|
|
}
|
|
}
|
|
|
|
private static List<object> GetContexts(object graph)
|
|
{
|
|
var result = new List<object>();
|
|
var children = GetChildren(graph);
|
|
|
|
foreach (var child in children)
|
|
{
|
|
var typeName = child.GetType().Name;
|
|
var baseTypeName = child.GetType().BaseType?.Name ?? "";
|
|
if (typeName.Contains("Context") || baseTypeName.Contains("Context") ||
|
|
typeName.Contains("Spawner") || typeName.Contains("Initialize") ||
|
|
typeName.Contains("Update") || typeName.Contains("Output"))
|
|
{
|
|
result.Add(child);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static List<object> GetBlocks(object context)
|
|
{
|
|
return GetChildren(context);
|
|
}
|
|
|
|
private static List<object> GetOperators(object graph)
|
|
{
|
|
var result = new List<object>();
|
|
var children = GetChildren(graph);
|
|
|
|
foreach (var child in children)
|
|
{
|
|
var typeName = child.GetType().Name;
|
|
var baseTypeName = child.GetType().BaseType?.Name ?? "";
|
|
if (typeName.Contains("Operator") || baseTypeName.Contains("Operator"))
|
|
{
|
|
result.Add(child);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static List<object> GetChildren(object parent)
|
|
{
|
|
var result = new List<object>();
|
|
if (parent == null) return result;
|
|
|
|
try
|
|
{
|
|
var type = parent.GetType();
|
|
|
|
// Find children property - handle ambiguous case
|
|
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(p => p.Name == "children").ToArray();
|
|
|
|
PropertyInfo childrenProp = null;
|
|
if (props.Length == 1)
|
|
{
|
|
childrenProp = props[0];
|
|
}
|
|
else if (props.Length > 1)
|
|
{
|
|
// Pick the one that returns IEnumerable
|
|
childrenProp = props.FirstOrDefault(p =>
|
|
typeof(System.Collections.IEnumerable).IsAssignableFrom(p.PropertyType));
|
|
}
|
|
|
|
if (childrenProp != null)
|
|
{
|
|
var children = childrenProp.GetValue(parent) as System.Collections.IEnumerable;
|
|
if (children != null)
|
|
{
|
|
foreach (var child in children)
|
|
{
|
|
if (child != null)
|
|
{
|
|
result.Add(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] GetChildren failed: {e.Message}");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalidate a VFX model to trigger recompilation
|
|
/// </summary>
|
|
private static void InvalidateModel(object model)
|
|
{
|
|
var vfxEditorAssembly = GetVFXEditorAssembly();
|
|
var invalidationCauseType = vfxEditorAssembly?.GetType("UnityEditor.VFX.VFXModel+InvalidationCause");
|
|
if (invalidationCauseType == null) return;
|
|
|
|
var invalidateMethod = model.GetType().GetMethod("Invalidate",
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
|
if (invalidateMethod == null) return;
|
|
|
|
try
|
|
{
|
|
var settingChanged = Enum.Parse(invalidationCauseType, "kSettingChanged");
|
|
invalidateMethod.Invoke(model, new object[] { settingChanged });
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set input slot value on a VFX block (e.g., SetAttribute value)
|
|
/// </summary>
|
|
private static void SetBlockInputSlotValue(object block, string attributeName, object value)
|
|
{
|
|
try
|
|
{
|
|
// Handle Dictionary wrapper - MCP sometimes wraps values
|
|
if (value is Dictionary<string, object> dict)
|
|
{
|
|
if (dict.TryGetValue("value", out object innerValue))
|
|
{
|
|
value = innerValue;
|
|
}
|
|
else if (dict.Count == 1)
|
|
{
|
|
value = dict.Values.First();
|
|
}
|
|
SynLog.Info($"[NexusVFX] Unwrapped Dictionary to: {value} ({value?.GetType().Name})");
|
|
}
|
|
|
|
// Get input slots from the block
|
|
var inputSlotsProperty = block.GetType().GetProperty("inputSlots",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (inputSlotsProperty == null)
|
|
{
|
|
SynLog.Warn("[NexusVFX] inputSlots property not found on block");
|
|
return;
|
|
}
|
|
|
|
var inputSlots = inputSlotsProperty.GetValue(block) as System.Collections.IEnumerable;
|
|
if (inputSlots == null)
|
|
{
|
|
SynLog.Warn("[NexusVFX] inputSlots is null");
|
|
return;
|
|
}
|
|
|
|
// Find the first input slot (SetAttribute blocks typically have one main input)
|
|
object targetSlot = null;
|
|
foreach (var slot in inputSlots)
|
|
{
|
|
targetSlot = slot;
|
|
break;
|
|
}
|
|
|
|
if (targetSlot == null)
|
|
{
|
|
SynLog.Warn("[NexusVFX] No input slots found on block");
|
|
return;
|
|
}
|
|
|
|
// Get slot's expected type
|
|
var slotType = targetSlot.GetType();
|
|
var slotTypeName = slotType.Name;
|
|
|
|
// Convert value based on attribute type
|
|
object vfxValue = ConvertToVFXValue(value, attributeName);
|
|
|
|
// VFXSlotVector expects UnityEditor.VFX.Vector, not Vector3
|
|
if (slotTypeName.Contains("Vector") && vfxValue is Vector3 v3)
|
|
{
|
|
var vfxEditorAssembly = GetVFXEditorAssembly();
|
|
var vfxVectorType = vfxEditorAssembly?.GetType("UnityEditor.VFX.Vector");
|
|
if (vfxVectorType != null)
|
|
{
|
|
// Try constructor with Vector3
|
|
var ctor = vfxVectorType.GetConstructor(new[] { typeof(Vector3) });
|
|
if (ctor != null)
|
|
{
|
|
vfxValue = ctor.Invoke(new object[] { v3 });
|
|
}
|
|
else
|
|
{
|
|
// Try creating and setting vector field
|
|
var vfxVec = Activator.CreateInstance(vfxVectorType);
|
|
var vecField = vfxVectorType.GetField("vector", BindingFlags.Public | BindingFlags.Instance);
|
|
vecField?.SetValue(vfxVec, v3);
|
|
vfxValue = vfxVec;
|
|
}
|
|
}
|
|
}
|
|
// Adjust Float3 vs Float4 and handle type mismatches
|
|
else if (slotTypeName.Contains("Float3"))
|
|
{
|
|
if (vfxValue is Vector4 v4)
|
|
{
|
|
vfxValue = new Vector3(v4.x, v4.y, v4.z);
|
|
}
|
|
else if (vfxValue is float floatForVec3)
|
|
{
|
|
// Float to Vector3 conversion (e.g., Angle attribute)
|
|
vfxValue = new Vector3(floatForVec3, floatForVec3, floatForVec3);
|
|
SynLog.Info($"[NexusVFX] Converted float {floatForVec3} to Vector3 for Float3 slot");
|
|
}
|
|
else if (vfxValue is double doubleForVec3)
|
|
{
|
|
float f = (float)doubleForVec3;
|
|
vfxValue = new Vector3(f, f, f);
|
|
SynLog.Info($"[NexusVFX] Converted double {doubleForVec3} to Vector3 for Float3 slot");
|
|
}
|
|
else if (vfxValue is int intForVec3)
|
|
{
|
|
float f = intForVec3;
|
|
vfxValue = new Vector3(f, f, f);
|
|
SynLog.Info($"[NexusVFX] Converted int {intForVec3} to Vector3 for Float3 slot");
|
|
}
|
|
}
|
|
else if (slotTypeName.Contains("Float4"))
|
|
{
|
|
if (vfxValue is Vector3 vec3)
|
|
{
|
|
vfxValue = new Vector4(vec3.x, vec3.y, vec3.z, 1f);
|
|
SynLog.Info($"[NexusVFX] Converted Vector3 to Vector4 for Float4 slot: {vfxValue}");
|
|
}
|
|
else if (vfxValue is Vector4 vec4)
|
|
{
|
|
// Vector4 is correct for Float4, keep as is
|
|
SynLog.Info($"[NexusVFX] Using Vector4 for Float4 slot: {vfxValue}");
|
|
}
|
|
else if (vfxValue is Color col)
|
|
{
|
|
vfxValue = new Vector4(col.r, col.g, col.b, col.a);
|
|
SynLog.Info($"[NexusVFX] Converted Color to Vector4 for Float4 slot: {vfxValue}");
|
|
}
|
|
}
|
|
// Handle Float slot expecting float but receiving Vector3
|
|
else if (slotTypeName.Contains("Float") && !slotTypeName.Contains("Float3") && !slotTypeName.Contains("Float4"))
|
|
{
|
|
if (vfxValue is Vector3 vecToFloat)
|
|
{
|
|
vfxValue = vecToFloat.x;
|
|
SynLog.Info($"[NexusVFX] Converted Vector3 to float {vfxValue} for Float slot");
|
|
}
|
|
else if (vfxValue is Vector4 vec4ToFloat)
|
|
{
|
|
vfxValue = vec4ToFloat.x;
|
|
SynLog.Info($"[NexusVFX] Converted Vector4 to float {vfxValue} for Float slot");
|
|
}
|
|
}
|
|
|
|
// Method 1: Try SetValue method
|
|
var setValueMethod = slotType.GetMethod("SetValue",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (setValueMethod != null)
|
|
{
|
|
try
|
|
{
|
|
setValueMethod.Invoke(targetSlot, new object[] { vfxValue });
|
|
SynLog.Info($"[NexusVFX] Set slot value via SetValue: {vfxValue}");
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] SetValue failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Method 2: Try value property
|
|
var valueProperty = slotType.GetProperty("value",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (valueProperty != null && valueProperty.CanWrite)
|
|
{
|
|
try
|
|
{
|
|
valueProperty.SetValue(targetSlot, vfxValue);
|
|
SynLog.Info($"[NexusVFX] Set slot value via property: {vfxValue}");
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] value property set failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Method 3: Try m_Value field
|
|
var valueField = slotType.GetField("m_Value",
|
|
BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
if (valueField != null)
|
|
{
|
|
try
|
|
{
|
|
valueField.SetValue(targetSlot, vfxValue);
|
|
SynLog.Info($"[NexusVFX] Set slot value via m_Value field: {vfxValue}");
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] m_Value field set failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
SynLog.Warn($"[NexusVFX] Could not set value on slot for attribute: {attributeName}");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] SetBlockInputSlotValue failed: {e.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set input slot value by slot name (e.g., "A", "B" for min/max in Random mode)
|
|
/// </summary>
|
|
private static void SetBlockInputSlotValueByName(object block, string slotName, object value)
|
|
{
|
|
try
|
|
{
|
|
var inputSlotsProperty = block.GetType().GetProperty("inputSlots",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (inputSlotsProperty == null)
|
|
{
|
|
SynLog.Warn("[NexusVFX] inputSlots property not found");
|
|
return;
|
|
}
|
|
|
|
var inputSlots = inputSlotsProperty.GetValue(block) as System.Collections.IEnumerable;
|
|
if (inputSlots == null) return;
|
|
|
|
object targetSlot = null;
|
|
foreach (var slot in inputSlots)
|
|
{
|
|
var name = GetSlotName(slot);
|
|
if (name.Equals(slotName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
targetSlot = slot;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (targetSlot == null)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Slot '{slotName}' not found");
|
|
return;
|
|
}
|
|
|
|
// Convert value
|
|
object vfxValue = ConvertToVFXValue(value, slotName);
|
|
|
|
// Set value
|
|
var valueProperty = targetSlot.GetType().GetProperty("value",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (valueProperty != null && valueProperty.CanWrite)
|
|
{
|
|
valueProperty.SetValue(targetSlot, vfxValue);
|
|
SynLog.Info($"[NexusVFX] Set slot '{slotName}' value: {vfxValue}");
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] SetBlockInputSlotValueByName failed: {e.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert a value to VFX-compatible type based on attribute
|
|
/// </summary>
|
|
private static object ConvertToVFXValue(object value, string attributeName)
|
|
{
|
|
if (value == null) return null;
|
|
|
|
// Handle Dictionary - extract actual value
|
|
if (value is Dictionary<string, object> dict)
|
|
{
|
|
if (dict.TryGetValue("value", out object innerValue))
|
|
{
|
|
value = innerValue;
|
|
}
|
|
else if (dict.Count == 1)
|
|
{
|
|
value = dict.Values.First();
|
|
}
|
|
else
|
|
{
|
|
SynLog.Warn($"[NexusVFX] ConvertToVFXValue received Dictionary for {attributeName}, cannot extract value");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Handle already correct types
|
|
if (value is float floatVal) return floatVal;
|
|
if (value is double doubleVal) return (float)doubleVal;
|
|
if (value is int intVal) return (float)intVal;
|
|
if (value is Vector3 vec3Val) return vec3Val;
|
|
if (value is Vector4 vec4Val) return vec4Val;
|
|
if (value is Color colorVal) return new Vector4(colorVal.r, colorVal.g, colorVal.b, colorVal.a);
|
|
|
|
string strValue = value.ToString();
|
|
|
|
// Color attributes use Vector3 (RGB) in VFX Graph - Alpha is separate attribute
|
|
if (attributeName.ToLower() == "color")
|
|
{
|
|
// Parse color string "r,g,b" or "r,g,b,a" or hex "#RRGGBB"
|
|
if (strValue.StartsWith("#"))
|
|
{
|
|
if (ColorUtility.TryParseHtmlString(strValue, out Color c))
|
|
{
|
|
return new Vector3(c.r, c.g, c.b); // RGB only, alpha is separate
|
|
}
|
|
}
|
|
|
|
var parts = strValue.Split(',');
|
|
if (parts.Length >= 3)
|
|
{
|
|
float r = float.Parse(parts[0].Trim());
|
|
float g = float.Parse(parts[1].Trim());
|
|
float b = float.Parse(parts[2].Trim());
|
|
return new Vector3(r, g, b); // RGB only for VFX Graph color
|
|
}
|
|
|
|
return new Vector3(1, 1, 1); // Default white
|
|
}
|
|
|
|
// Vector attributes (position, velocity, direction, scale)
|
|
// VFX Graph uses UnityEditor.VFX.Vector, not UnityEngine.Vector3
|
|
if (attributeName.ToLower() == "position" ||
|
|
attributeName.ToLower() == "velocity" ||
|
|
attributeName.ToLower() == "direction" ||
|
|
attributeName.ToLower() == "scale")
|
|
{
|
|
var parts = strValue.Split(',');
|
|
if (parts.Length >= 3)
|
|
{
|
|
var vec3 = new Vector3(
|
|
float.Parse(parts[0].Trim()),
|
|
float.Parse(parts[1].Trim()),
|
|
float.Parse(parts[2].Trim())
|
|
);
|
|
|
|
// Try to create VFX Vector type
|
|
var vfxEditorAssembly = GetVFXEditorAssembly();
|
|
var vfxVectorType = vfxEditorAssembly?.GetType("UnityEditor.VFX.Vector");
|
|
if (vfxVectorType != null)
|
|
{
|
|
var ctor = vfxVectorType.GetConstructor(new[] { typeof(Vector3) });
|
|
if (ctor != null)
|
|
return ctor.Invoke(new object[] { vec3 });
|
|
}
|
|
|
|
return vec3;
|
|
}
|
|
}
|
|
|
|
// Scalar attributes (size, lifetime, alpha, mass, angle)
|
|
if (float.TryParse(strValue, out float parsedFloat))
|
|
{
|
|
return parsedFloat;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
private static void ApplySettings(object target, Dictionary<string, object> settings)
|
|
{
|
|
if (settings == null || target == null) return;
|
|
|
|
var type = target.GetType();
|
|
|
|
// Keys to skip (MCP internal params, not VFX settings)
|
|
// spawnRate is a block setting (VFXSpawnerConstantRate), not a context setting
|
|
// attribute, value, composition are handled specially for SetAttribute blocks
|
|
var skipKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"operationId", "vfxPath", "contextType", "blockType", "contextIndex",
|
|
"spawnRate", "rate", "capacity", "bounds", // These need special handling
|
|
"attribute", "value", "composition" // Handled in SetAttribute block processing
|
|
};
|
|
|
|
foreach (var kvp in settings)
|
|
{
|
|
if (skipKeys.Contains(kvp.Key)) continue;
|
|
|
|
try
|
|
{
|
|
// Try SetSettingValue method first (VFX specific)
|
|
var setSettingMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(m => m.Name == "SetSettingValue").ToArray();
|
|
|
|
bool set = false;
|
|
foreach (var method in setSettingMethods)
|
|
{
|
|
var parameters = method.GetParameters();
|
|
if (parameters.Length == 2)
|
|
{
|
|
try
|
|
{
|
|
method.Invoke(target, new object[] { kvp.Key, kvp.Value });
|
|
set = true;
|
|
SynLog.Info($"[NexusVFX] Set {kvp.Key} = {kvp.Value}");
|
|
break;
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
if (set) continue;
|
|
|
|
// Try property
|
|
var prop = type.GetProperty(kvp.Key,
|
|
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
|
|
if (prop != null && prop.CanWrite)
|
|
{
|
|
prop.SetValue(target, Convert.ChangeType(kvp.Value, prop.PropertyType));
|
|
SynLog.Info($"[NexusVFX] Set property {kvp.Key} = {kvp.Value}");
|
|
continue;
|
|
}
|
|
|
|
// Try field
|
|
var field = type.GetField(kvp.Key,
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
|
|
if (field != null)
|
|
{
|
|
field.SetValue(target, Convert.ChangeType(kvp.Value, field.FieldType));
|
|
SynLog.Info($"[NexusVFX] Set field {kvp.Key} = {kvp.Value}");
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Only warn for actual VFX settings, not internal params
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Exposed Parameters
|
|
|
|
/// <summary>
|
|
/// Add an exposed parameter to the VFX Graph
|
|
/// </summary>
|
|
public static string AddParameter(string vfxPath, string paramName, string paramType, object defaultValue = null, bool exposed = true)
|
|
{
|
|
Initialize();
|
|
|
|
try
|
|
{
|
|
var graph = GetVFXGraph(vfxPath);
|
|
if (graph == null)
|
|
{
|
|
return $"Error: VFX Graph not found at {vfxPath}";
|
|
}
|
|
|
|
var vfxEditorAssembly = GetVFXEditorAssembly();
|
|
|
|
// Get VFXParameter type
|
|
var parameterType = vfxEditorAssembly?.GetType("UnityEditor.VFX.VFXParameter");
|
|
if (parameterType == null)
|
|
{
|
|
return "Error: VFXParameter type not found";
|
|
}
|
|
|
|
// Create parameter
|
|
var parameter = ScriptableObject.CreateInstance(parameterType);
|
|
|
|
// Set m_Exposed field directly (exposed property is read-only)
|
|
var exposedField = parameterType.GetField("m_Exposed",
|
|
BindingFlags.NonPublic | BindingFlags.Instance);
|
|
if (exposedField != null)
|
|
{
|
|
exposedField.SetValue(parameter, exposed);
|
|
SynLog.Info($"[NexusVFX] Set m_Exposed = {exposed}");
|
|
}
|
|
else
|
|
{
|
|
SynLog.Warn("[NexusVFX] Could not find m_Exposed field on VFXParameter");
|
|
}
|
|
|
|
// Set m_ExposedName field directly (exposedName property is read-only)
|
|
var exposedNameField = parameterType.GetField("m_ExposedName",
|
|
BindingFlags.NonPublic | BindingFlags.Instance);
|
|
if (exposedNameField != null)
|
|
{
|
|
exposedNameField.SetValue(parameter, paramName);
|
|
SynLog.Info($"[NexusVFX] Set m_ExposedName = {paramName}");
|
|
}
|
|
else
|
|
{
|
|
SynLog.Warn("[NexusVFX] Could not find m_ExposedName field on VFXParameter");
|
|
}
|
|
|
|
// Determine value type based on paramType
|
|
Type valueType = GetVFXValueType(paramType);
|
|
if (valueType != null)
|
|
{
|
|
// Set output slot type using SetSettingValue
|
|
var setSettingMethod = parameterType.GetMethod("SetSettingValue",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
if (setSettingMethod != null)
|
|
{
|
|
try
|
|
{
|
|
setSettingMethod.Invoke(parameter, new object[] { "m_Type", paramType });
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
// Add to graph - try multiple AddChild signatures
|
|
var addChildMethods = graph.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(m => m.Name == "AddChild")
|
|
.ToList();
|
|
|
|
// Get initial child count to verify if add succeeded
|
|
int initialParamCount = GetChildren(graph).Count;
|
|
|
|
bool paramAdded = false;
|
|
Exception lastParamException = null;
|
|
|
|
// Try 1-parameter version first (most compatible)
|
|
var sortedParamMethods = addChildMethods.OrderBy(m => m.GetParameters().Length).ToList();
|
|
|
|
foreach (var addChildMethod in sortedParamMethods)
|
|
{
|
|
var addParams = addChildMethod.GetParameters();
|
|
try
|
|
{
|
|
if (addParams.Length == 1)
|
|
{
|
|
addChildMethod.Invoke(graph, new object[] { parameter });
|
|
paramAdded = true;
|
|
SynLog.Info("[NexusVFX] Added parameter (1 param)");
|
|
break;
|
|
}
|
|
else if (addParams.Length == 2 && addParams[1].ParameterType == typeof(int))
|
|
{
|
|
addChildMethod.Invoke(graph, new object[] { parameter, 0 });
|
|
paramAdded = true;
|
|
SynLog.Info("[NexusVFX] Added parameter (2 params, index 0)");
|
|
break;
|
|
}
|
|
else if (addParams.Length == 3 && addParams[1].ParameterType == typeof(int))
|
|
{
|
|
addChildMethod.Invoke(graph, new object[] { parameter, 0, true });
|
|
paramAdded = true;
|
|
SynLog.Info("[NexusVFX] Added parameter (3 params, index 0)");
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
lastParamException = ex.InnerException ?? ex;
|
|
SynLog.Warn($"[NexusVFX] AddChild attempt for parameter failed: {lastParamException.Message}");
|
|
// Check if parameter was actually added despite the exception
|
|
int currentParamCount = GetChildren(graph).Count;
|
|
if (currentParamCount > initialParamCount)
|
|
{
|
|
paramAdded = true;
|
|
SynLog.Info($"[NexusVFX] Parameter was added despite exception (children: {initialParamCount} -> {currentParamCount})");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!paramAdded)
|
|
{
|
|
return $"Error: Failed to add parameter - {lastParamException?.Message ?? "AddChild not found"}";
|
|
}
|
|
|
|
// Set default value if provided
|
|
if (defaultValue != null)
|
|
{
|
|
var outputSlots = GetOutputSlots(parameter);
|
|
if (outputSlots.Count > 0)
|
|
{
|
|
SetSlotValue(outputSlots[0], defaultValue);
|
|
}
|
|
}
|
|
|
|
EditorUtility.SetDirty(graph as UnityEngine.Object);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return $"Added parameter '{paramName}' ({paramType}) to {vfxPath}";
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return $"Error adding parameter: {e.Message}\n{e.StackTrace}";
|
|
}
|
|
}
|
|
|
|
private static Type GetVFXValueType(string typeName)
|
|
{
|
|
var typeMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
{ "float", "VFXValueFloat" },
|
|
{ "int", "VFXValueInt" },
|
|
{ "uint", "VFXValueUint" },
|
|
{ "bool", "VFXValueBool" },
|
|
{ "vector2", "VFXValueVector2" },
|
|
{ "vector3", "VFXValueVector3" },
|
|
{ "vector4", "VFXValueVector4" },
|
|
{ "color", "VFXValueVector4" },
|
|
{ "texture2d", "VFXValueTexture2D" },
|
|
{ "texture3d", "VFXValueTexture3D" },
|
|
{ "cubemap", "VFXValueCubemap" },
|
|
{ "mesh", "VFXValueMesh" },
|
|
{ "gradient", "VFXValueGradient" },
|
|
{ "curve", "VFXValueCurve" },
|
|
{ "animationcurve", "VFXValueCurve" },
|
|
};
|
|
|
|
if (typeMap.TryGetValue(typeName, out string vfxTypeName))
|
|
{
|
|
var assembly = GetVFXEditorAssembly();
|
|
return assembly?.GetType($"UnityEditor.VFX.{vfxTypeName}");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Slot Connection
|
|
|
|
/// <summary>
|
|
/// Connect an output slot to an input slot
|
|
/// </summary>
|
|
public static string ConnectSlots(string vfxPath, int sourceNodeIndex, int sourceSlotIndex,
|
|
int targetNodeIndex, int targetSlotIndex, string sourceType = "operator", string targetType = "block")
|
|
{
|
|
Initialize();
|
|
|
|
try
|
|
{
|
|
var graph = GetVFXGraph(vfxPath);
|
|
if (graph == null)
|
|
{
|
|
return $"Error: VFX Graph not found at {vfxPath}";
|
|
}
|
|
|
|
// Get source node
|
|
object sourceNode = null;
|
|
if (sourceType.ToLower() == "operator")
|
|
{
|
|
var operators = GetOperators(graph);
|
|
if (sourceNodeIndex < 0 || sourceNodeIndex >= operators.Count)
|
|
return $"Error: Source operator index {sourceNodeIndex} out of range";
|
|
sourceNode = operators[sourceNodeIndex];
|
|
}
|
|
else if (sourceType.ToLower() == "parameter")
|
|
{
|
|
var parameters = GetParameters(graph);
|
|
if (sourceNodeIndex < 0 || sourceNodeIndex >= parameters.Count)
|
|
return $"Error: Source parameter index {sourceNodeIndex} out of range";
|
|
sourceNode = parameters[sourceNodeIndex];
|
|
}
|
|
else if (sourceType.ToLower() == "context")
|
|
{
|
|
var contexts = GetContexts(graph);
|
|
if (sourceNodeIndex < 0 || sourceNodeIndex >= contexts.Count)
|
|
return $"Error: Source context index {sourceNodeIndex} out of range";
|
|
sourceNode = contexts[sourceNodeIndex];
|
|
}
|
|
|
|
// Get target node
|
|
object targetNode = null;
|
|
if (targetType.ToLower() == "block")
|
|
{
|
|
// Need context index and block index
|
|
var contexts = GetContexts(graph);
|
|
// targetNodeIndex encodes both: high 16 bits = context, low 16 bits = block
|
|
int contextIdx = targetNodeIndex >> 16;
|
|
int blockIdx = targetNodeIndex & 0xFFFF;
|
|
|
|
if (contextIdx < 0 || contextIdx >= contexts.Count)
|
|
return $"Error: Context index {contextIdx} out of range";
|
|
|
|
var blocks = GetBlocks(contexts[contextIdx]);
|
|
if (blockIdx < 0 || blockIdx >= blocks.Count)
|
|
return $"Error: Block index {blockIdx} out of range";
|
|
|
|
targetNode = blocks[blockIdx];
|
|
}
|
|
else if (targetType.ToLower() == "operator")
|
|
{
|
|
var operators = GetOperators(graph);
|
|
if (targetNodeIndex < 0 || targetNodeIndex >= operators.Count)
|
|
return $"Error: Target operator index {targetNodeIndex} out of range";
|
|
targetNode = operators[targetNodeIndex];
|
|
}
|
|
|
|
if (sourceNode == null || targetNode == null)
|
|
{
|
|
return "Error: Could not find source or target node";
|
|
}
|
|
|
|
// Get output slots from source
|
|
var outputSlots = GetOutputSlots(sourceNode);
|
|
if (sourceSlotIndex < 0 || sourceSlotIndex >= outputSlots.Count)
|
|
return $"Error: Source slot index {sourceSlotIndex} out of range (found {outputSlots.Count} slots)";
|
|
|
|
// Get input slots from target
|
|
var inputSlots = GetInputSlots(targetNode);
|
|
if (targetSlotIndex < 0 || targetSlotIndex >= inputSlots.Count)
|
|
return $"Error: Target slot index {targetSlotIndex} out of range (found {inputSlots.Count} slots)";
|
|
|
|
var outputSlot = outputSlots[sourceSlotIndex];
|
|
var inputSlot = inputSlots[targetSlotIndex];
|
|
|
|
// Connect using Link method - Link(VFXSlot other, bool notify = true)
|
|
var linkMethod = inputSlot.GetType().GetMethod("Link",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
bool connected = false;
|
|
if (linkMethod != null)
|
|
{
|
|
var linkParams = linkMethod.GetParameters();
|
|
SynLog.Info($"[NexusVFX] Link method params: {string.Join(", ", linkParams.Select(p => $"{p.Name}:{p.ParameterType.Name}"))}");
|
|
|
|
try
|
|
{
|
|
if (linkParams.Length == 2)
|
|
{
|
|
// Link(VFXSlot other, bool notify)
|
|
var result = linkMethod.Invoke(inputSlot, new object[] { outputSlot, true });
|
|
connected = result is bool b && b;
|
|
}
|
|
else if (linkParams.Length == 1)
|
|
{
|
|
var result = linkMethod.Invoke(inputSlot, new object[] { outputSlot });
|
|
connected = result is bool b && b;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Link on inputSlot failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Try linking from output slot if input slot link failed
|
|
if (!connected)
|
|
{
|
|
var outputLinkMethod = outputSlot.GetType().GetMethod("Link",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
if (outputLinkMethod != null)
|
|
{
|
|
var linkParams = outputLinkMethod.GetParameters();
|
|
try
|
|
{
|
|
if (linkParams.Length == 2)
|
|
{
|
|
var result = outputLinkMethod.Invoke(outputSlot, new object[] { inputSlot, true });
|
|
connected = result is bool b && b;
|
|
}
|
|
else if (linkParams.Length == 1)
|
|
{
|
|
var result = outputLinkMethod.Invoke(outputSlot, new object[] { inputSlot });
|
|
connected = result is bool b && b;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Link on outputSlot failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!connected)
|
|
{
|
|
return $"Warning: Could not connect slots (types may be incompatible)";
|
|
}
|
|
|
|
EditorUtility.SetDirty(graph as UnityEngine.Object);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return $"Connected slot {sourceSlotIndex} of {sourceType} {sourceNodeIndex} to slot {targetSlotIndex} of {targetType}";
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return $"Error connecting slots: {e.Message}\n{e.StackTrace}";
|
|
}
|
|
}
|
|
|
|
private static List<object> GetOutputSlots(object node)
|
|
{
|
|
var result = new List<object>();
|
|
|
|
var outputSlotsProp = node.GetType().GetProperty("outputSlots",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (outputSlotsProp != null)
|
|
{
|
|
var slots = outputSlotsProp.GetValue(node) as System.Collections.IEnumerable;
|
|
if (slots != null)
|
|
{
|
|
foreach (var slot in slots)
|
|
{
|
|
result.Add(slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static List<object> GetInputSlots(object node)
|
|
{
|
|
var result = new List<object>();
|
|
|
|
var inputSlotsProp = node.GetType().GetProperty("inputSlots",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (inputSlotsProp != null)
|
|
{
|
|
var slots = inputSlotsProp.GetValue(node) as System.Collections.IEnumerable;
|
|
if (slots != null)
|
|
{
|
|
foreach (var slot in slots)
|
|
{
|
|
result.Add(slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static void SetSlotValue(object slot, object value)
|
|
{
|
|
var valueProp = slot.GetType().GetProperty("value",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (valueProp != null && valueProp.CanWrite)
|
|
{
|
|
valueProp.SetValue(slot, value);
|
|
}
|
|
}
|
|
|
|
private static string GetSlotName(object slot)
|
|
{
|
|
var nameProp = slot.GetType().GetProperty("name",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (nameProp != null)
|
|
{
|
|
return nameProp.GetValue(slot)?.ToString() ?? "";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
private static List<object> GetParameters(object graph)
|
|
{
|
|
var result = new List<object>();
|
|
|
|
var childrenProp = graph.GetType().GetProperty("children",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (childrenProp != null)
|
|
{
|
|
var children = childrenProp.GetValue(graph) as System.Collections.IEnumerable;
|
|
if (children != null)
|
|
{
|
|
foreach (var child in children)
|
|
{
|
|
if (child.GetType().Name.Contains("Parameter"))
|
|
{
|
|
result.Add(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SetAttribute Helpers
|
|
|
|
/// <summary>
|
|
/// Set attribute value on a SetAttribute block
|
|
/// </summary>
|
|
public static string SetBlockAttribute(string vfxPath, int contextIndex, int blockIndex,
|
|
string attributeName, object value, string compositionMode = "overwrite")
|
|
{
|
|
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";
|
|
|
|
var block = blocks[blockIndex];
|
|
|
|
// Check if it's a SetAttribute block
|
|
if (!block.GetType().Name.Contains("SetAttribute") &&
|
|
!block.GetType().Name.Contains("Set"))
|
|
{
|
|
return $"Error: Block at index {blockIndex} is not a SetAttribute block";
|
|
}
|
|
|
|
// Set the attribute name - it's a public field, not a setting
|
|
var attributeField = block.GetType().GetField("attribute",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
if (attributeField != null)
|
|
{
|
|
attributeField.SetValue(block, attributeName);
|
|
SynLog.Info($"[NexusVFX] Set attribute field to: {attributeName}");
|
|
}
|
|
else
|
|
{
|
|
SynLog.Warn("[NexusVFX] Could not find attribute field on SetAttribute block");
|
|
}
|
|
|
|
// Set composition mode - it's also a public field (Composition)
|
|
var compositionField = block.GetType().GetField("Composition",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
if (compositionField != null && compositionField.FieldType.IsEnum)
|
|
{
|
|
try
|
|
{
|
|
var compositionValue = Enum.Parse(compositionField.FieldType, compositionMode, true);
|
|
compositionField.SetValue(block, compositionValue);
|
|
SynLog.Info($"[NexusVFX] Set Composition to: {compositionMode}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Could not set Composition: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Invalidate to update the block (optional - may have multiple overloads)
|
|
try
|
|
{
|
|
var vfxEditorAssembly = GetVFXEditorAssembly();
|
|
var invalidationCauseType = vfxEditorAssembly?.GetType("UnityEditor.VFX.VFXModel+InvalidationCause");
|
|
if (invalidationCauseType != null)
|
|
{
|
|
// Find Invalidate method that takes InvalidationCause parameter
|
|
var invalidateMethods = block.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
|
|
.Where(m => m.Name == "Invalidate" && m.GetParameters().Length == 1)
|
|
.ToList();
|
|
|
|
var invalidateMethod = invalidateMethods.FirstOrDefault(m =>
|
|
m.GetParameters()[0].ParameterType == invalidationCauseType ||
|
|
m.GetParameters()[0].ParameterType.Name.Contains("InvalidationCause"));
|
|
|
|
if (invalidateMethod != null)
|
|
{
|
|
var settingChanged = Enum.Parse(invalidationCauseType, "kSettingChanged");
|
|
invalidateMethod.Invoke(block, new object[] { settingChanged });
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Could not invalidate block (non-critical): {ex.Message}");
|
|
}
|
|
|
|
// Set the value on input slot
|
|
var inputSlots = GetInputSlots(block);
|
|
foreach (var slot in inputSlots)
|
|
{
|
|
var slotName = slot.GetType().GetProperty("name")?.GetValue(slot)?.ToString();
|
|
if (slotName?.ToLower() == attributeName.ToLower() ||
|
|
slotName?.ToLower() == "value" ||
|
|
slotName?.ToLower() == attributeName.ToLower().Replace("set", ""))
|
|
{
|
|
SetSlotValue(slot, ConvertToVFXValue(value, slot));
|
|
break;
|
|
}
|
|
}
|
|
|
|
EditorUtility.SetDirty(graph as UnityEngine.Object);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return $"Set attribute '{attributeName}' = {value} on block {blockIndex}";
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return $"Error setting attribute: {e.Message}\n{e.StackTrace}";
|
|
}
|
|
}
|
|
|
|
private static object ConvertToVFXValue(object value, object slot)
|
|
{
|
|
if (value == null) return null;
|
|
|
|
var slotValueType = slot.GetType().GetProperty("value")?.PropertyType;
|
|
if (slotValueType == null) return value;
|
|
|
|
// Handle string values (parse to appropriate type)
|
|
if (value is string strValue)
|
|
{
|
|
// Vector3
|
|
if (slotValueType == typeof(Vector3))
|
|
{
|
|
var parts = strValue.Split(',');
|
|
if (parts.Length >= 3)
|
|
{
|
|
return new Vector3(
|
|
float.Parse(parts[0].Trim()),
|
|
float.Parse(parts[1].Trim()),
|
|
float.Parse(parts[2].Trim())
|
|
);
|
|
}
|
|
}
|
|
// Vector4 / Color
|
|
else if (slotValueType == typeof(Vector4))
|
|
{
|
|
if (strValue.StartsWith("#"))
|
|
{
|
|
// Color hex
|
|
if (ColorUtility.TryParseHtmlString(strValue, out Color color))
|
|
{
|
|
return new Vector4(color.r, color.g, color.b, color.a);
|
|
}
|
|
}
|
|
var parts = strValue.Split(',');
|
|
if (parts.Length >= 4)
|
|
{
|
|
return new Vector4(
|
|
float.Parse(parts[0].Trim()),
|
|
float.Parse(parts[1].Trim()),
|
|
float.Parse(parts[2].Trim()),
|
|
float.Parse(parts[3].Trim())
|
|
);
|
|
}
|
|
else if (parts.Length == 3)
|
|
{
|
|
// RGB without alpha - default alpha to 1
|
|
return new Vector4(
|
|
float.Parse(parts[0].Trim()),
|
|
float.Parse(parts[1].Trim()),
|
|
float.Parse(parts[2].Trim()),
|
|
1f // Full alpha
|
|
);
|
|
}
|
|
}
|
|
// Color type
|
|
else if (slotValueType == typeof(Color))
|
|
{
|
|
if (strValue.StartsWith("#"))
|
|
{
|
|
if (ColorUtility.TryParseHtmlString(strValue, out Color hexColor))
|
|
{
|
|
return hexColor;
|
|
}
|
|
}
|
|
var parts = strValue.Split(',');
|
|
if (parts.Length >= 4)
|
|
{
|
|
return new Color(
|
|
float.Parse(parts[0].Trim()),
|
|
float.Parse(parts[1].Trim()),
|
|
float.Parse(parts[2].Trim()),
|
|
float.Parse(parts[3].Trim())
|
|
);
|
|
}
|
|
else if (parts.Length == 3)
|
|
{
|
|
return new Color(
|
|
float.Parse(parts[0].Trim()),
|
|
float.Parse(parts[1].Trim()),
|
|
float.Parse(parts[2].Trim()),
|
|
1f
|
|
);
|
|
}
|
|
}
|
|
// Vector2
|
|
else if (slotValueType == typeof(Vector2))
|
|
{
|
|
var parts = strValue.Split(',');
|
|
if (parts.Length >= 2)
|
|
{
|
|
return new Vector2(
|
|
float.Parse(parts[0].Trim()),
|
|
float.Parse(parts[1].Trim())
|
|
);
|
|
}
|
|
}
|
|
// Float
|
|
else if (slotValueType == typeof(float))
|
|
{
|
|
return float.Parse(strValue);
|
|
}
|
|
// Int
|
|
else if (slotValueType == typeof(int))
|
|
{
|
|
return int.Parse(strValue);
|
|
}
|
|
// Bool
|
|
else if (slotValueType == typeof(bool))
|
|
{
|
|
return bool.Parse(strValue);
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region High-Level Presets
|
|
|
|
/// <summary>
|
|
/// Create a VFX preset (fire, smoke, sparks, etc.)
|
|
/// </summary>
|
|
public static string CreatePreset(string name, string presetType, string folder = "Assets/VFX")
|
|
{
|
|
Initialize();
|
|
|
|
try
|
|
{
|
|
// First create the base graph
|
|
var createResult = CreateVFXGraph(name, folder);
|
|
if (createResult.StartsWith("Error"))
|
|
return createResult;
|
|
|
|
string vfxPath = $"{folder}/{name}.vfx";
|
|
|
|
switch (presetType.ToLower())
|
|
{
|
|
case "fire":
|
|
return CreateFirePreset(vfxPath);
|
|
case "smoke":
|
|
return CreateSmokePreset(vfxPath);
|
|
case "sparks":
|
|
return CreateSparksPreset(vfxPath);
|
|
case "trail":
|
|
return CreateTrailPreset(vfxPath);
|
|
case "explosion":
|
|
return CreateExplosionPreset(vfxPath);
|
|
default:
|
|
return $"Unknown preset type: {presetType}. Available: fire, smoke, sparks, trail, explosion";
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return $"Error creating preset: {e.Message}";
|
|
}
|
|
}
|
|
|
|
private static string CreateFirePreset(string vfxPath)
|
|
{
|
|
// === FIRE EFFECT - Professional VFX Graph Setup ===
|
|
// Fire rises upward with flickering motion, fades from yellow to orange to red
|
|
|
|
// Spawn: 200 particles/sec for dense fire
|
|
AddContext(vfxPath, "spawn", new Dictionary<string, object> { { "spawnRate", 200f } });
|
|
|
|
// Initialize: capacity = 200 * 2 = 400, use 500 for safety
|
|
AddContext(vfxPath, "initialize", new Dictionary<string, object> { { "capacity", 500 } });
|
|
|
|
// Update context for forces
|
|
AddContext(vfxPath, "update", null);
|
|
|
|
// Output: Additive blend for fire glow effect with default particle texture
|
|
AddContext(vfxPath, "quad", new Dictionary<string, object> {
|
|
{ "blendMode", "additive" }
|
|
});
|
|
|
|
// Set fire texture from Kenney pack
|
|
SetParticleTexture(vfxPath, 3, "flame_01");
|
|
|
|
// Link the particle lifecycle
|
|
LinkContexts(vfxPath, 0, 1); // spawn -> init
|
|
LinkContexts(vfxPath, 1, 2); // init -> update
|
|
LinkContexts(vfxPath, 2, 3); // update -> output
|
|
|
|
// === INITIALIZE CONTEXT ===
|
|
// Position: Small circle at base for flame origin
|
|
AddBlock(vfxPath, 1, "positioncircle", new Dictionary<string, object> { { "radius", 0.3f } });
|
|
|
|
// Random lifetime: 0.8-2.0 seconds for variety
|
|
AddBlock(vfxPath, 1, "setattributerandom", new Dictionary<string, object> {
|
|
{ "attribute", "lifetime" },
|
|
{ "min", 0.8f },
|
|
{ "max", 2.0f }
|
|
});
|
|
|
|
// Initial velocity: Upward with slight random spread
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "velocity" },
|
|
{ "value", "0,2.5,0" }
|
|
});
|
|
|
|
// Random velocity spread for natural movement
|
|
AddBlock(vfxPath, 1, "velocityrandom", new Dictionary<string, object> {
|
|
{ "speed", 0.8f }
|
|
});
|
|
|
|
// Random size: 0.3-0.7 for variety
|
|
AddBlock(vfxPath, 1, "setattributerandom", new Dictionary<string, object> {
|
|
{ "attribute", "size" },
|
|
{ "min", 0.3f },
|
|
{ "max", 0.7f }
|
|
});
|
|
|
|
// Initial color: Bright yellow-white core
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "color" },
|
|
{ "value", "1,0.9,0.3" }
|
|
});
|
|
|
|
// Random Z angle: 0-360 degrees for rotation variety
|
|
AddBlock(vfxPath, 1, "setattributerandom", new Dictionary<string, object> {
|
|
{ "attribute", "angle" },
|
|
{ "min", 0f },
|
|
{ "max", 360f }
|
|
});
|
|
|
|
// === UPDATE CONTEXT ===
|
|
// Force: Continuous upward force to keep fire rising
|
|
AddBlock(vfxPath, 2, "force", new Dictionary<string, object> {
|
|
{ "force", "0,2,0" } // Upward force vector
|
|
});
|
|
|
|
// Turbulence: Flickering motion - stronger for fire
|
|
AddBlock(vfxPath, 2, "turbulence", new Dictionary<string, object> {
|
|
{ "intensity", 3.0f },
|
|
{ "frequency", 5f },
|
|
{ "octaves", 3 }
|
|
});
|
|
|
|
// Drag: Slow down over time for natural look
|
|
AddBlock(vfxPath, 2, "drag", new Dictionary<string, object> {
|
|
{ "coefficient", 1.2f }
|
|
});
|
|
|
|
// Angular velocity: Slow rotation over time for flickering
|
|
AddBlock(vfxPath, 2, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "angularVelocity" },
|
|
{ "value", 30f },
|
|
{ "composition", "add" }
|
|
});
|
|
|
|
// === OUTPUT CONTEXT ===
|
|
// Color over life: Yellow -> Orange -> Red fade
|
|
AddBlock(vfxPath, 3, "coloroverlife", null);
|
|
|
|
// Size over life: Expand then shrink
|
|
AddBlock(vfxPath, 3, "sizeoverlife", null);
|
|
|
|
// Orient: Face camera
|
|
AddBlock(vfxPath, 3, "orient", null);
|
|
|
|
CompileVFX(vfxPath);
|
|
|
|
return $"Created fire preset: {vfxPath}\n" +
|
|
"Settings: SpawnRate=200, Capacity=500, Velocity Y=3, Force Y=2, Turbulence=2.5";
|
|
}
|
|
|
|
private static string CreateSmokePreset(string vfxPath)
|
|
{
|
|
// === SMOKE EFFECT - Professional VFX Graph Setup ===
|
|
// Smoke characteristics: slow rise, expansion over time, alpha blend, soft edges
|
|
// Reference: Unity 6-way lighting smoke, Diablo 3 smoke recreation tutorials
|
|
|
|
// Spawn: Lower rate (25-40) for wispy smoke, not dense cloud
|
|
AddContext(vfxPath, "spawn", new Dictionary<string, object> { { "spawnRate", 35f } });
|
|
|
|
// Initialize: capacity = 35 * 5 (max lifetime) ≈ 175, use 250 for safety
|
|
AddContext(vfxPath, "initialize", new Dictionary<string, object> { { "capacity", 250 } });
|
|
|
|
// Update context
|
|
AddContext(vfxPath, "update", null);
|
|
|
|
// Output: Alpha blend for translucent smoke (NOT additive)
|
|
AddContext(vfxPath, "quad", new Dictionary<string, object> { { "blendMode", "alpha" } });
|
|
|
|
// Set smoke texture from Kenney pack
|
|
SetParticleTexture(vfxPath, 3, "whitePuff00");
|
|
|
|
// Link contexts
|
|
LinkContexts(vfxPath, 0, 1);
|
|
LinkContexts(vfxPath, 1, 2);
|
|
LinkContexts(vfxPath, 2, 3);
|
|
|
|
// === INITIALIZE CONTEXT ===
|
|
// Position: Small sphere for concentrated smoke source
|
|
AddBlock(vfxPath, 1, "positionsphere", new Dictionary<string, object> { { "radius", 0.15f } });
|
|
|
|
// Lifetime: 3-5 seconds for slow dissipation
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "lifetime" },
|
|
{ "value", 4f }
|
|
});
|
|
|
|
// Initial velocity: Slow upward drift
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "velocity" },
|
|
{ "value", "0,0.3,0" }
|
|
});
|
|
|
|
// Initial size: Small start, will expand over lifetime
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "size" },
|
|
{ "value", 0.2f }
|
|
});
|
|
|
|
// Initial color: Gray with some opacity
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "color" },
|
|
{ "value", "0.5,0.5,0.5" }
|
|
});
|
|
|
|
// Initial alpha: Semi-transparent
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "alpha" },
|
|
{ "value", 0.6f }
|
|
});
|
|
|
|
// Random angle for variation
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "angle" },
|
|
{ "value", 0f } // Will use random in actual implementation
|
|
});
|
|
|
|
// === UPDATE CONTEXT ===
|
|
// Turbulence: Gentle swirling motion for realistic smoke
|
|
AddBlock(vfxPath, 2, "turbulence", new Dictionary<string, object> {
|
|
{ "intensity", 0.8f },
|
|
{ "frequency", 1.5f },
|
|
{ "octaves", 3 },
|
|
{ "roughness", 0.5f }
|
|
});
|
|
|
|
// Drag: Higher drag for smoke (air resistance)
|
|
// Smoke slows down significantly over lifetime
|
|
AddBlock(vfxPath, 2, "drag", new Dictionary<string, object> {
|
|
{ "coefficient", 1.2f }
|
|
});
|
|
|
|
// Slight upward force (hot smoke rises)
|
|
AddBlock(vfxPath, 2, "gravity", new Dictionary<string, object> {
|
|
{ "force", -0.5f }
|
|
});
|
|
|
|
// === OUTPUT CONTEXT ===
|
|
// Size over life: Expand from 0.2 to 1.5+ as smoke dissipates
|
|
AddBlock(vfxPath, 3, "sizeoverlife", null);
|
|
|
|
// Color/Alpha over life: Fade out alpha towards end
|
|
AddBlock(vfxPath, 3, "coloroverlife", null);
|
|
|
|
// Orient: Face camera
|
|
AddBlock(vfxPath, 3, "orient", null);
|
|
|
|
CompileVFX(vfxPath);
|
|
|
|
return $"Created professional smoke preset: {vfxPath}\n" +
|
|
"Settings: SpawnRate=35, Capacity=250, Lifetime=4s, Drag=1.2, Alpha blend";
|
|
}
|
|
|
|
private static string CreateSparksPreset(string vfxPath)
|
|
{
|
|
// === SPARKS/EMBERS EFFECT - Professional VFX Graph Setup ===
|
|
// Sparks: fast, small, bright, affected by gravity, stretched billboard
|
|
// Reference: Unity VFX Graph tutorials, Brian David VR VFX series
|
|
|
|
// Spawn: High rate for continuous spark shower (150-300)
|
|
AddContext(vfxPath, "spawn", new Dictionary<string, object> { { "spawnRate", 200f } });
|
|
|
|
// Initialize: capacity = 200 * 1.5 = 300, use 400 for bursts
|
|
AddContext(vfxPath, "initialize", new Dictionary<string, object> { { "capacity", 400 } });
|
|
|
|
// Update context
|
|
AddContext(vfxPath, "update", null);
|
|
|
|
// Output: Line output for stretched sparks, additive for glow
|
|
AddContext(vfxPath, "line", new Dictionary<string, object> { { "blendMode", "additive" } });
|
|
|
|
// Set spark texture from Kenney pack
|
|
SetParticleTexture(vfxPath, 3, "spark_01");
|
|
|
|
// Link contexts
|
|
LinkContexts(vfxPath, 0, 1);
|
|
LinkContexts(vfxPath, 1, 2);
|
|
LinkContexts(vfxPath, 2, 3);
|
|
|
|
// === INITIALIZE CONTEXT ===
|
|
// Position: Small point source
|
|
AddBlock(vfxPath, 1, "positionsphere", new Dictionary<string, object> { { "radius", 0.05f } });
|
|
|
|
// Lifetime: Very short (0.3-1.0 seconds) - sparks burn out quickly
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "lifetime" },
|
|
{ "value", 0.6f }
|
|
});
|
|
|
|
// High initial velocity: Sparks burst outward
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "velocity" },
|
|
{ "value", "0,3,0" } // Strong upward initial burst
|
|
});
|
|
|
|
// Very small size: Point-like sparks
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "size" },
|
|
{ "value", 0.02f }
|
|
});
|
|
|
|
// Bright orange/yellow color
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "color" },
|
|
{ "value", "1,0.6,0.1" } // Hot orange
|
|
});
|
|
|
|
// Random velocity direction for spread
|
|
AddBlock(vfxPath, 1, "velocityrandom", new Dictionary<string, object> {
|
|
{ "speed", 2f }
|
|
});
|
|
|
|
// === UPDATE CONTEXT ===
|
|
// Gravity: Sparks fall with real gravity (-9.81)
|
|
AddBlock(vfxPath, 2, "gravity", new Dictionary<string, object> {
|
|
{ "force", 9.81f } // Earth gravity (positive = downward in Unity)
|
|
});
|
|
|
|
// Light turbulence for random flickering paths
|
|
AddBlock(vfxPath, 2, "turbulence", new Dictionary<string, object> {
|
|
{ "intensity", 0.5f },
|
|
{ "frequency", 5f }
|
|
});
|
|
|
|
// Drag: Air resistance slows sparks
|
|
AddBlock(vfxPath, 2, "drag", new Dictionary<string, object> {
|
|
{ "coefficient", 0.8f }
|
|
});
|
|
|
|
// === OUTPUT CONTEXT ===
|
|
// Color over life: Bright -> dim (cooling ember)
|
|
AddBlock(vfxPath, 3, "coloroverlife", null);
|
|
|
|
// Size over life: Slight shrink as spark burns out
|
|
AddBlock(vfxPath, 3, "sizeoverlife", null);
|
|
|
|
CompileVFX(vfxPath);
|
|
|
|
return $"Created professional sparks preset: {vfxPath}\n" +
|
|
"Settings: SpawnRate=200, Capacity=400, Lifetime=0.6s, Gravity=9.81, Line output";
|
|
}
|
|
|
|
private static string CreateTrailPreset(string vfxPath)
|
|
{
|
|
// === TRAIL/RIBBON EFFECT - Professional VFX Graph Setup ===
|
|
// Trail characteristics: connected particles, smooth tapering, follows motion
|
|
// Used for: weapon trails, vehicle exhaust, magic effects
|
|
|
|
// Spawn: Moderate rate for smooth trail (40-80)
|
|
AddContext(vfxPath, "spawn", new Dictionary<string, object> { { "spawnRate", 60f } });
|
|
|
|
// Initialize: capacity = 60 * 1.5 = 90, use 150 for smooth trails
|
|
AddContext(vfxPath, "initialize", new Dictionary<string, object> { { "capacity", 150 } });
|
|
|
|
// Update context
|
|
AddContext(vfxPath, "update", null);
|
|
|
|
// Output: QuadStrip for connected ribbon, additive for energy effect
|
|
AddContext(vfxPath, "quadstrip", new Dictionary<string, object> { { "blendMode", "additive" } });
|
|
|
|
// Set trail texture from Kenney pack
|
|
SetParticleTexture(vfxPath, 3, "trace_01");
|
|
|
|
// Link contexts
|
|
LinkContexts(vfxPath, 0, 1);
|
|
LinkContexts(vfxPath, 1, 2);
|
|
LinkContexts(vfxPath, 2, 3);
|
|
|
|
// === INITIALIZE CONTEXT ===
|
|
// Position: Line for ribbon effect (or inherit from parent)
|
|
AddBlock(vfxPath, 1, "positionline", null);
|
|
|
|
// Lifetime: 1-2 seconds for visible trail length
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "lifetime" },
|
|
{ "value", 1.2f }
|
|
});
|
|
|
|
// Size: Width of trail ribbon
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "size" },
|
|
{ "value", 0.15f }
|
|
});
|
|
|
|
// Color: Bright cyan/blue for energy trail
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "color" },
|
|
{ "value", "0.3,0.8,1" } // Cyan blue
|
|
});
|
|
|
|
// === UPDATE CONTEXT ===
|
|
// Subtle turbulence for organic movement
|
|
AddBlock(vfxPath, 2, "turbulence", new Dictionary<string, object> {
|
|
{ "intensity", 0.3f },
|
|
{ "frequency", 2f }
|
|
});
|
|
|
|
// Light drag for smooth deceleration
|
|
AddBlock(vfxPath, 2, "drag", new Dictionary<string, object> {
|
|
{ "coefficient", 0.2f }
|
|
});
|
|
|
|
// === OUTPUT CONTEXT ===
|
|
// Color over life: Bright start, fade to transparent
|
|
AddBlock(vfxPath, 3, "coloroverlife", null);
|
|
|
|
// Size over life: Taper from full width to thin tip
|
|
AddBlock(vfxPath, 3, "sizeoverlife", null);
|
|
|
|
CompileVFX(vfxPath);
|
|
|
|
return $"Created professional trail preset: {vfxPath}\n" +
|
|
"Settings: SpawnRate=60, Capacity=150, Lifetime=1.2s, QuadStrip output";
|
|
}
|
|
|
|
private static string CreateExplosionPreset(string vfxPath)
|
|
{
|
|
// === EXPLOSION EFFECT - Professional VFX Graph Setup ===
|
|
// Explosion: Single burst spawn, radial velocity, fast decay
|
|
// Components: Core flash, debris/sparks, smoke cloud, shockwave (simplified here)
|
|
// Reference: Unity explosive visuals blog, VionixStudio explosion tutorial
|
|
|
|
// Spawn: Single burst (not continuous)
|
|
AddContext(vfxPath, "spawn", null);
|
|
|
|
// Initialize: High capacity for burst
|
|
AddContext(vfxPath, "initialize", new Dictionary<string, object> { { "capacity", 1000 } });
|
|
|
|
// Update context
|
|
AddContext(vfxPath, "update", null);
|
|
|
|
// Output: Additive quad for explosion glow
|
|
AddContext(vfxPath, "quad", new Dictionary<string, object> { { "blendMode", "additive" } });
|
|
|
|
// Set explosion texture from Kenney pack
|
|
SetParticleTexture(vfxPath, 3, "explosion00");
|
|
|
|
// Link contexts
|
|
LinkContexts(vfxPath, 0, 1);
|
|
LinkContexts(vfxPath, 1, 2);
|
|
LinkContexts(vfxPath, 2, 3);
|
|
|
|
// === SPAWN CONTEXT ===
|
|
// Single burst of 300 particles
|
|
AddBlock(vfxPath, 0, "spawnburst", new Dictionary<string, object> {
|
|
{ "count", 300 }
|
|
});
|
|
|
|
// === INITIALIZE CONTEXT ===
|
|
// Position: Small sphere at explosion center
|
|
AddBlock(vfxPath, 1, "positionsphere", new Dictionary<string, object> { { "radius", 0.1f } });
|
|
|
|
// Lifetime: Short (0.5-1.5s) for quick explosion
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "lifetime" },
|
|
{ "value", 0.8f }
|
|
});
|
|
|
|
// High radial velocity: Particles burst outward
|
|
AddBlock(vfxPath, 1, "velocityrandom", new Dictionary<string, object> {
|
|
{ "speed", 8f } // High speed burst
|
|
});
|
|
|
|
// Size: Medium particles
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "size" },
|
|
{ "value", 0.5f }
|
|
});
|
|
|
|
// Color: Bright orange/yellow explosion core
|
|
AddBlock(vfxPath, 1, "setattribute", new Dictionary<string, object> {
|
|
{ "attribute", "color" },
|
|
{ "value", "1,0.7,0.2" } // Hot orange
|
|
});
|
|
|
|
// === UPDATE CONTEXT ===
|
|
// High drag: Explosion particles slow down rapidly
|
|
AddBlock(vfxPath, 2, "drag", new Dictionary<string, object> {
|
|
{ "coefficient", 3f } // High drag for quick deceleration
|
|
});
|
|
|
|
// Slight gravity: Debris falls
|
|
AddBlock(vfxPath, 2, "gravity", new Dictionary<string, object> {
|
|
{ "force", 2f }
|
|
});
|
|
|
|
// Turbulence: Add chaos to explosion
|
|
AddBlock(vfxPath, 2, "turbulence", new Dictionary<string, object> {
|
|
{ "intensity", 2f },
|
|
{ "frequency", 4f }
|
|
});
|
|
|
|
// === OUTPUT CONTEXT ===
|
|
// Color over life: Bright -> orange -> red -> black (fade)
|
|
AddBlock(vfxPath, 3, "coloroverlife", null);
|
|
|
|
// Size over life: Expand then shrink
|
|
AddBlock(vfxPath, 3, "sizeoverlife", null);
|
|
|
|
// Orient: Face camera
|
|
AddBlock(vfxPath, 3, "orient", null);
|
|
|
|
CompileVFX(vfxPath);
|
|
|
|
return $"Created professional explosion preset: {vfxPath}\n" +
|
|
"Settings: Burst=300, Capacity=1000, Lifetime=0.8s, RadialSpeed=8, Drag=3";
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Available Types Query
|
|
|
|
public static string GetAvailableContexts()
|
|
{
|
|
Initialize();
|
|
return string.Join(", ", _contextTypes.Keys.OrderBy(k => k));
|
|
}
|
|
|
|
public static string GetAvailableBlocks()
|
|
{
|
|
Initialize();
|
|
return string.Join(", ", _blockTypes.Keys.OrderBy(k => k));
|
|
}
|
|
|
|
public static string GetAvailableOperators()
|
|
{
|
|
Initialize();
|
|
return string.Join(", ", _operatorTypes.Keys.OrderBy(k => k));
|
|
}
|
|
|
|
public static string GetAvailablePresets()
|
|
{
|
|
return "fire, smoke, sparks, trail, explosion";
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Output Context Settings
|
|
|
|
/// <summary>
|
|
/// Configure output context settings (texture, blend mode, etc.)
|
|
/// </summary>
|
|
public static string ConfigureOutput(string vfxPath, int contextIndex, Dictionary<string, object> 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";
|
|
}
|
|
|
|
var context = contexts[contextIndex];
|
|
var contextTypeName = context.GetType().Name;
|
|
|
|
// Verify it's an output context
|
|
if (!contextTypeName.Contains("Output") && !contextTypeName.Contains("Quad") &&
|
|
!contextTypeName.Contains("Point") && !contextTypeName.Contains("Line") &&
|
|
!contextTypeName.Contains("Mesh"))
|
|
{
|
|
return $"Warning: Context at index {contextIndex} ({contextTypeName}) may not be an output context";
|
|
}
|
|
|
|
var results = new List<string>();
|
|
|
|
foreach (var kvp in settings)
|
|
{
|
|
string key = kvp.Key.ToLower();
|
|
object value = kvp.Value;
|
|
|
|
try
|
|
{
|
|
switch (key)
|
|
{
|
|
case "texture":
|
|
case "maintexture":
|
|
case "basetexture":
|
|
var texturePath = value.ToString();
|
|
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
|
|
if (texture != null)
|
|
{
|
|
SetOutputProperty(context, "mainTexture", texture);
|
|
results.Add($"Set texture: {texturePath}");
|
|
}
|
|
else
|
|
{
|
|
results.Add($"Warning: Texture not found at {texturePath}");
|
|
}
|
|
break;
|
|
|
|
case "blendmode":
|
|
case "blend":
|
|
SetOutputBlendMode(context, value.ToString());
|
|
results.Add($"Set blend mode: {value}");
|
|
break;
|
|
|
|
case "sortpriority":
|
|
case "priority":
|
|
SetOutputProperty(context, "sortPriority", Convert.ToInt32(value));
|
|
results.Add($"Set sort priority: {value}");
|
|
break;
|
|
|
|
case "usesofparticle":
|
|
case "softparticle":
|
|
case "soft":
|
|
SetOutputProperty(context, "useSoftParticle", Convert.ToBoolean(value));
|
|
results.Add($"Set soft particle: {value}");
|
|
break;
|
|
|
|
case "castsshadows":
|
|
case "castshadow":
|
|
case "shadow":
|
|
SetOutputProperty(context, "castShadows", Convert.ToBoolean(value));
|
|
results.Add($"Set cast shadows: {value}");
|
|
break;
|
|
|
|
default:
|
|
// Try generic property setting
|
|
SetOutputProperty(context, kvp.Key, value);
|
|
results.Add($"Set {kvp.Key}: {value}");
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
results.Add($"Failed to set {key}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
EditorUtility.SetDirty(graph as UnityEngine.Object);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return $"Configured output context {contextIndex}:\n" + string.Join("\n", results);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return $"Error configuring output: {e.Message}";
|
|
}
|
|
}
|
|
|
|
private static void SetOutputProperty(object context, string propertyName, object value)
|
|
{
|
|
var contextType = context.GetType();
|
|
|
|
// For texture properties, try input slots first (VFX Graph uses slots for textures)
|
|
if (propertyName.ToLower().Contains("texture") && value is Texture2D texture)
|
|
{
|
|
var inputSlots = GetInputSlots(context);
|
|
foreach (var slot in inputSlots)
|
|
{
|
|
var slotName = GetSlotName(slot);
|
|
var slotTypeName = slot.GetType().Name;
|
|
|
|
// Look for texture slots
|
|
if (slotTypeName.Contains("Texture") || slotName.ToLower().Contains("texture"))
|
|
{
|
|
var valueProp = slot.GetType().GetProperty("value",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
if (valueProp != null && valueProp.CanWrite)
|
|
{
|
|
try
|
|
{
|
|
valueProp.SetValue(slot, texture);
|
|
SynLog.Info($"[NexusVFX] Set texture on slot '{slotName}': {texture.name}");
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Failed to set texture on slot: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try SetSettingValue first
|
|
var setSettingMethod = contextType.GetMethod("SetSettingValue",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (setSettingMethod != null)
|
|
{
|
|
try
|
|
{
|
|
setSettingMethod.Invoke(context, new object[] { propertyName, value });
|
|
SynLog.Info($"[NexusVFX] Set output setting {propertyName} = {value}");
|
|
return;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
// Try property
|
|
var prop = contextType.GetProperty(propertyName,
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
|
|
if (prop != null && prop.CanWrite)
|
|
{
|
|
prop.SetValue(context, value);
|
|
SynLog.Info($"[NexusVFX] Set output property {propertyName} = {value}");
|
|
return;
|
|
}
|
|
|
|
// Try field
|
|
var field = contextType.GetField(propertyName,
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
|
|
if (field != null)
|
|
{
|
|
field.SetValue(context, value);
|
|
SynLog.Info($"[NexusVFX] Set output field {propertyName} = {value}");
|
|
return;
|
|
}
|
|
|
|
SynLog.Warn($"[NexusVFX] Could not find property/field {propertyName} on {contextType.Name}");
|
|
}
|
|
|
|
private static void SetOutputBlendMode(object context, string blendMode)
|
|
{
|
|
var contextType = context.GetType();
|
|
var vfxEditorAssembly = GetVFXEditorAssembly();
|
|
|
|
// Try multiple field names for different pipeline versions
|
|
string[] blendModeFieldNames = { "blendMode", "m_BlendMode", "colorMapping", "m_ColorMapping" };
|
|
FieldInfo blendModeField = null;
|
|
|
|
foreach (var fieldName in blendModeFieldNames)
|
|
{
|
|
blendModeField = contextType.GetField(fieldName,
|
|
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
|
|
|
|
if (blendModeField == null)
|
|
{
|
|
// Try walking up the inheritance chain
|
|
var baseType = contextType.BaseType;
|
|
while (baseType != null && blendModeField == null)
|
|
{
|
|
blendModeField = baseType.GetField(fieldName,
|
|
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
|
baseType = baseType.BaseType;
|
|
}
|
|
}
|
|
|
|
if (blendModeField != null)
|
|
{
|
|
SynLog.Info($"[NexusVFX] Found blend field: {fieldName} on {contextType.Name}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If still not found, try SetSettingValue method
|
|
if (blendModeField == null)
|
|
{
|
|
var setSettingMethod = contextType.GetMethod("SetSettingValue",
|
|
BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (setSettingMethod != null)
|
|
{
|
|
try
|
|
{
|
|
// Try setting blendMode via method
|
|
setSettingMethod.Invoke(context, new object[] { "blendMode", blendMode });
|
|
SynLog.Info($"[NexusVFX] Set blendMode via SetSettingValue: {blendMode}");
|
|
return;
|
|
}
|
|
catch
|
|
{
|
|
// URP may not have blendMode setting, just skip
|
|
SynLog.Warn($"[NexusVFX] blendMode not supported on {contextType.Name} - skipping");
|
|
return;
|
|
}
|
|
}
|
|
|
|
SynLog.Warn($"[NexusVFX] Could not find blendMode field on {contextType.Name} - skipping");
|
|
return;
|
|
}
|
|
|
|
// Get the enum type from the field
|
|
var blendModeType = blendModeField.FieldType;
|
|
|
|
var modeMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
{ "additive", "Additive" },
|
|
{ "add", "Additive" },
|
|
{ "alpha", "Alpha" },
|
|
{ "alphablend", "Alpha" },
|
|
{ "premultiply", "AlphaPremultiply" },
|
|
{ "premultiplied", "AlphaPremultiply" },
|
|
{ "alphapremultiply", "AlphaPremultiply" },
|
|
{ "opaque", "Opaque" },
|
|
{ "masked", "Masked" },
|
|
};
|
|
|
|
string enumName = modeMap.GetValueOrDefault(blendMode.ToLower(), blendMode);
|
|
|
|
try
|
|
{
|
|
// Get enum values and find matching one
|
|
var enumValues = Enum.GetValues(blendModeType);
|
|
foreach (var val in enumValues)
|
|
{
|
|
if (val.ToString().Equals(enumName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
blendModeField.SetValue(context, val);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enable particle color on output context so color attribute affects rendering
|
|
/// </summary>
|
|
private static void EnableParticleColor(object context)
|
|
{
|
|
var contextType = context.GetType();
|
|
|
|
// Try multiple approaches to enable particle color
|
|
// Method 1: useParticleColor field (common in URP Lit outputs)
|
|
string[] colorFieldNames = { "useParticleColor", "m_UseParticleColor", "useColor", "m_UseColor" };
|
|
foreach (var fieldName in colorFieldNames)
|
|
{
|
|
var field = FindFieldInHierarchy(contextType, fieldName);
|
|
if (field != null && field.FieldType == typeof(bool))
|
|
{
|
|
field.SetValue(context, true);
|
|
SynLog.Info($"[NexusVFX] Enabled particle color via {fieldName}");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Method 2: colorMode setting (set to "Multiply" or particle color mode)
|
|
string[] colorModeFieldNames = { "colorMode", "m_ColorMode", "colorMapping", "m_ColorMapping" };
|
|
foreach (var fieldName in colorModeFieldNames)
|
|
{
|
|
var field = FindFieldInHierarchy(contextType, fieldName);
|
|
if (field != null && field.FieldType.IsEnum)
|
|
{
|
|
try
|
|
{
|
|
var enumValues = Enum.GetValues(field.FieldType);
|
|
foreach (var val in enumValues)
|
|
{
|
|
string valName = val.ToString().ToLower();
|
|
// Look for a mode that uses particle color (Multiply, VertexColor, ParticleColor)
|
|
if (valName.Contains("multiply") || valName.Contains("vertex") ||
|
|
valName.Contains("particle") || valName.Contains("color"))
|
|
{
|
|
field.SetValue(context, val);
|
|
SynLog.Info($"[NexusVFX] Set color mode to {val} via {fieldName}");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
// Method 3: Try SetSettingValue method
|
|
var setSettingMethod = contextType.GetMethod("SetSettingValue", BindingFlags.Public | BindingFlags.Instance);
|
|
if (setSettingMethod != null)
|
|
{
|
|
try
|
|
{
|
|
setSettingMethod.Invoke(context, new object[] { "useParticleColor", true });
|
|
SynLog.Info("[NexusVFX] Enabled particle color via SetSettingValue");
|
|
return;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
SynLog.Info("[NexusVFX] Could not find particle color setting - may need manual configuration");
|
|
}
|
|
|
|
private static FieldInfo FindFieldInHierarchy(Type type, string fieldName)
|
|
{
|
|
var currentType = type;
|
|
while (currentType != null)
|
|
{
|
|
var field = currentType.GetField(fieldName,
|
|
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
|
if (field != null) return field;
|
|
currentType = currentType.BaseType;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set particle texture by name from Kenney pack (Assets/Synaptic AI Pro/Resources/VFX/Textures/)
|
|
/// </summary>
|
|
private static void SetParticleTexture(string vfxPath, int outputContextIndex, string textureName)
|
|
{
|
|
try
|
|
{
|
|
var graph = GetVFXGraph(vfxPath);
|
|
if (graph == null) return;
|
|
|
|
var contexts = GetContexts(graph);
|
|
if (outputContextIndex < 0 || outputContextIndex >= contexts.Count) return;
|
|
|
|
var outputContext = contexts[outputContextIndex];
|
|
|
|
// Build texture path
|
|
string texturePath = $"Assets/Synaptic AI Pro/Resources/VFX/Textures/{textureName}.png";
|
|
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
|
|
|
|
if (texture != null)
|
|
{
|
|
SetOutputProperty(outputContext, "mainTexture", texture);
|
|
SynLog.Info($"[NexusVFX] Set particle texture: {textureName}");
|
|
}
|
|
else
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Texture not found: {texturePath}, using default");
|
|
SetDefaultParticleTexture(vfxPath, outputContextIndex);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Failed to set texture {textureName}: {e.Message}");
|
|
}
|
|
}
|
|
|
|
private static void SetDefaultParticleTexture(string vfxPath, int outputContextIndex)
|
|
{
|
|
try
|
|
{
|
|
var graph = GetVFXGraph(vfxPath);
|
|
if (graph == null) return;
|
|
|
|
var contexts = GetContexts(graph);
|
|
if (outputContextIndex < 0 || outputContextIndex >= contexts.Count) return;
|
|
|
|
var outputContext = contexts[outputContextIndex];
|
|
|
|
// Get or create soft particle texture asset
|
|
Texture2D particleTexture = GetOrCreateSoftParticleTexture();
|
|
|
|
if (particleTexture != null)
|
|
{
|
|
SetOutputProperty(outputContext, "mainTexture", particleTexture);
|
|
SynLog.Info($"[NexusVFX] Set particle texture: {particleTexture.name}");
|
|
}
|
|
else
|
|
{
|
|
SynLog.Warn("[NexusVFX] Could not find or create default particle texture");
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Failed to set default texture: {e.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or create soft particle texture as a persistent asset
|
|
/// </summary>
|
|
private static Texture2D GetOrCreateSoftParticleTexture()
|
|
{
|
|
// Path to store the texture asset
|
|
string texturePath = "Assets/Synaptic AI Pro/Resources/VFX/Textures/SoftParticle.png";
|
|
|
|
// Try to load existing texture
|
|
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
|
|
if (texture != null)
|
|
{
|
|
SynLog.Info($"[NexusVFX] Loaded existing particle texture: {texturePath}");
|
|
return texture;
|
|
}
|
|
|
|
// Create new texture procedurally
|
|
int size = 128; // Higher resolution for better quality
|
|
texture = new Texture2D(size, size, TextureFormat.RGBA32, false);
|
|
texture.name = "SoftParticle";
|
|
texture.wrapMode = TextureWrapMode.Clamp;
|
|
texture.filterMode = FilterMode.Bilinear;
|
|
|
|
float center = size / 2f;
|
|
float maxDist = center;
|
|
|
|
for (int y = 0; y < size; y++)
|
|
{
|
|
for (int x = 0; x < size; x++)
|
|
{
|
|
float dist = Vector2.Distance(new Vector2(x, y), new Vector2(center, center));
|
|
float normalizedDist = dist / maxDist;
|
|
|
|
// Smooth circular falloff (soft particle look)
|
|
float alpha = Mathf.Clamp01(1f - normalizedDist);
|
|
// Apply smooth curve for better soft edge
|
|
alpha = Mathf.SmoothStep(0f, 1f, alpha);
|
|
alpha = alpha * alpha; // Extra softness
|
|
|
|
// White color with alpha falloff
|
|
texture.SetPixel(x, y, new Color(1f, 1f, 1f, alpha));
|
|
}
|
|
}
|
|
|
|
texture.Apply();
|
|
|
|
// Ensure directory exists
|
|
string directory = System.IO.Path.GetDirectoryName(texturePath);
|
|
if (!System.IO.Directory.Exists(directory))
|
|
{
|
|
System.IO.Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
// Save as PNG asset
|
|
byte[] pngData = texture.EncodeToPNG();
|
|
System.IO.File.WriteAllBytes(texturePath, pngData);
|
|
|
|
// Import the asset
|
|
AssetDatabase.ImportAsset(texturePath, ImportAssetOptions.ForceUpdate);
|
|
|
|
// Set texture import settings
|
|
TextureImporter importer = AssetImporter.GetAtPath(texturePath) as TextureImporter;
|
|
if (importer != null)
|
|
{
|
|
importer.textureType = TextureImporterType.Default;
|
|
importer.alphaIsTransparency = true;
|
|
importer.wrapMode = TextureWrapMode.Clamp;
|
|
importer.filterMode = FilterMode.Bilinear;
|
|
importer.mipmapEnabled = true;
|
|
importer.SaveAndReimport();
|
|
}
|
|
|
|
// Reload the saved texture
|
|
texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
|
|
SynLog.Info($"[NexusVFX] Created and saved particle texture: {texturePath}");
|
|
|
|
return texture;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set a gradient for color over life
|
|
/// </summary>
|
|
public static string SetColorGradient(string vfxPath, int contextIndex, int blockIndex,
|
|
string[] colors, float[] times = null)
|
|
{
|
|
Initialize();
|
|
|
|
try
|
|
{
|
|
var graph = GetVFXGraph(vfxPath);
|
|
if (graph == null)
|
|
{
|
|
return $"Error: VFX Graph not found at {vfxPath}";
|
|
}
|
|
|
|
var contexts = GetContexts(graph);
|
|
|
|
// Create gradient first
|
|
var gradient = new Gradient();
|
|
var colorKeys = new GradientColorKey[colors.Length];
|
|
var alphaKeys = new GradientAlphaKey[colors.Length];
|
|
|
|
for (int i = 0; i < colors.Length; i++)
|
|
{
|
|
float time = times != null && i < times.Length ? times[i] : (float)i / (colors.Length - 1);
|
|
|
|
if (ColorUtility.TryParseHtmlString(colors[i], out Color color))
|
|
{
|
|
colorKeys[i] = new GradientColorKey(color, time);
|
|
alphaKeys[i] = new GradientAlphaKey(color.a == 0 ? 1f : color.a, time);
|
|
}
|
|
else
|
|
{
|
|
colorKeys[i] = new GradientColorKey(Color.white, time);
|
|
alphaKeys[i] = new GradientAlphaKey(1f, time);
|
|
}
|
|
}
|
|
|
|
gradient.SetKeys(colorKeys, alphaKeys);
|
|
|
|
// Auto-detect: if contextIndex is -1, find ALL Output contexts with Color gradient blocks
|
|
if (contextIndex < 0)
|
|
{
|
|
SynLog.Info("[NexusVFX] Auto-detecting ALL Color gradient blocks in Output contexts...");
|
|
int blocksModified = 0;
|
|
|
|
// Search all contexts
|
|
for (int ci = 0; ci < contexts.Count; ci++)
|
|
{
|
|
var ctx = contexts[ci];
|
|
var ctxTypeName = ctx.GetType().Name;
|
|
|
|
// Look for Output contexts (contain "Output" in type name)
|
|
if (ctxTypeName.Contains("Output"))
|
|
{
|
|
var ctxBlocks = GetBlocks(ctx);
|
|
for (int bi = 0; bi < ctxBlocks.Count; bi++)
|
|
{
|
|
var blk = ctxBlocks[bi];
|
|
var blkTypeName = blk.GetType().Name;
|
|
|
|
// Check if it's a color-related block
|
|
if (blkTypeName.Contains("ColorOverLife") || blkTypeName.Contains("AttributeFromCurve"))
|
|
{
|
|
bool isColorBlock = false;
|
|
|
|
// For AttributeFromCurve, check if it's for Color attribute
|
|
if (blkTypeName.Contains("AttributeFromCurve"))
|
|
{
|
|
var blkSlots = GetInputSlots(blk);
|
|
foreach (var slot in blkSlots)
|
|
{
|
|
var slotName = GetSlotName(slot);
|
|
if (slotName.ToLower().Contains("color"))
|
|
{
|
|
isColorBlock = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ColorOverLife is always a color block
|
|
isColorBlock = true;
|
|
}
|
|
|
|
if (isColorBlock)
|
|
{
|
|
// Set gradient on this block
|
|
if (SetGradientOnBlock(blk, gradient))
|
|
{
|
|
blocksModified++;
|
|
SynLog.Info($"[NexusVFX] Set gradient on Context {ci}, Block {bi} ({blkTypeName})");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (blocksModified == 0)
|
|
{
|
|
return "Error: Could not find any Color gradient blocks in Output contexts.";
|
|
}
|
|
|
|
EditorUtility.SetDirty(graph as UnityEngine.Object);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return $"Set color gradient with {colors.Length} colors on {blocksModified} block(s)";
|
|
}
|
|
|
|
// Manual mode: set specific block
|
|
if (contextIndex < 0 || contextIndex >= contexts.Count)
|
|
{
|
|
return $"Error: Context index out of range";
|
|
}
|
|
|
|
var blocks = GetBlocks(contexts[contextIndex]);
|
|
if (blockIndex < 0 || blockIndex >= blocks.Count)
|
|
{
|
|
return $"Error: Block index out of range";
|
|
}
|
|
|
|
var block = blocks[blockIndex];
|
|
|
|
bool gradientSet = SetGradientOnBlock(block, gradient);
|
|
|
|
if (!gradientSet)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Could not find gradient slot to set");
|
|
}
|
|
|
|
EditorUtility.SetDirty(graph as UnityEngine.Object);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return $"Set color gradient with {colors.Length} colors";
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return $"Error setting gradient: {e.Message}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method to set gradient on a single block
|
|
/// </summary>
|
|
private static bool SetGradientOnBlock(object block, Gradient gradient)
|
|
{
|
|
var inputSlots = GetInputSlots(block);
|
|
foreach (var slot in inputSlots)
|
|
{
|
|
var slotType = slot.GetType();
|
|
var slotTypeName = slotType.Name;
|
|
|
|
// For VFXSlotGradient, use the value property
|
|
var valueProp = slotType.GetProperty("value", BindingFlags.Public | BindingFlags.Instance);
|
|
if (valueProp != null)
|
|
{
|
|
// Check if it's a Gradient type or assignable from Gradient
|
|
if (valueProp.PropertyType == typeof(Gradient) ||
|
|
valueProp.PropertyType.IsAssignableFrom(typeof(Gradient)) ||
|
|
slotTypeName.Contains("Gradient"))
|
|
{
|
|
try
|
|
{
|
|
valueProp.SetValue(slot, gradient);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SynLog.Warn($"[NexusVFX] Failed to set gradient directly: {ex.Message}");
|
|
|
|
// Try setting via SerializedObject
|
|
var slotAsObject = slot as UnityEngine.Object;
|
|
if (slotAsObject != null)
|
|
{
|
|
var so = new SerializedObject(slotAsObject);
|
|
var gradientProp = so.FindProperty("m_Value");
|
|
if (gradientProp != null)
|
|
{
|
|
gradientProp.gradientValue = gradient;
|
|
so.ApplyModifiedProperties();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
#endif
|