Token Optimization for VFX (#626)

* Token Optimization for VFX

* Update VFXs

* Small fix based on AI feedback

* Update ManageVFX.cs
main
Shutong Wu 2026-01-25 19:34:28 -05:00 committed by GitHub
parent 2eb26b85cf
commit bb56f78ad3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 745 additions and 660 deletions

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering; using UnityEngine.Rendering;
using UnityEditor;
namespace MCPForUnity.Editor.Helpers namespace MCPForUnity.Editor.Helpers
{ {
@ -14,6 +16,15 @@ namespace MCPForUnity.Editor.Helpers
Custom Custom
} }
internal enum VFXComponentType
{
ParticleSystem,
LineRenderer,
TrailRenderer
}
private static Dictionary<string, Material> s_DefaultVFXMaterials = new Dictionary<string, Material>();
private static readonly string[] BuiltInLitShaders = { "Standard", "Legacy Shaders/Diffuse" }; private static readonly string[] BuiltInLitShaders = { "Standard", "Legacy Shaders/Diffuse" };
private static readonly string[] BuiltInUnlitShaders = { "Unlit/Color", "Unlit/Texture" }; private static readonly string[] BuiltInUnlitShaders = { "Unlit/Color", "Unlit/Texture" };
private static readonly string[] UrpLitShaders = { "Universal Render Pipeline/Lit", "Universal Render Pipeline/Simple Lit" }; private static readonly string[] UrpLitShaders = { "Universal Render Pipeline/Lit", "Universal Render Pipeline/Simple Lit" };
@ -192,5 +203,82 @@ namespace MCPForUnity.Editor.Helpers
break; break;
} }
} }
internal static Material GetOrCreateDefaultVFXMaterial(VFXComponentType componentType)
{
var pipeline = GetActivePipeline();
string cacheKey = $"{pipeline}_{componentType}";
if (s_DefaultVFXMaterials.TryGetValue(cacheKey, out Material cachedMaterial) && cachedMaterial != null)
{
return cachedMaterial;
}
Material material = null;
if (pipeline == PipelineKind.BuiltIn)
{
string builtinPath = componentType == VFXComponentType.ParticleSystem
? "Default-Particle.mat"
: "Default-Line.mat";
material = AssetDatabase.GetBuiltinExtraResource<Material>(builtinPath);
}
if (material == null)
{
Shader shader = ResolveDefaultUnlitShader(pipeline);
if (shader == null)
{
shader = Shader.Find("Unlit/Color");
}
if (shader != null)
{
material = new Material(shader);
material.name = $"Auto_Default_{componentType}_{pipeline}";
// Set default color (white is standard for VFX)
if (material.HasProperty("_Color"))
{
material.SetColor("_Color", Color.white);
}
if (material.HasProperty("_BaseColor"))
{
material.SetColor("_BaseColor", Color.white);
}
if (componentType == VFXComponentType.ParticleSystem)
{
material.renderQueue = 3000;
if (material.HasProperty("_Mode"))
{
material.SetFloat("_Mode", 2);
}
if (material.HasProperty("_SrcBlend"))
{
material.SetFloat("_SrcBlend", (float)UnityEngine.Rendering.BlendMode.SrcAlpha);
}
if (material.HasProperty("_DstBlend"))
{
material.SetFloat("_DstBlend", (float)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
}
if (material.HasProperty("_ZWrite"))
{
material.SetFloat("_ZWrite", 0);
}
}
McpLog.Info($"[RenderPipelineUtility] Created default VFX material for {componentType} using {shader.name}");
}
}
if (material != null)
{
s_DefaultVFXMaterials[cacheKey] = material;
}
return material;
}
} }
} }

View File

@ -12,6 +12,43 @@ namespace MCPForUnity.Editor.Helpers
/// </summary> /// </summary>
public static class RendererHelpers public static class RendererHelpers
{ {
/// <summary>
/// Ensures a renderer has a material assigned. If not, auto-assigns a default material
/// based on the render pipeline and component type.
/// </summary>
/// <param name="renderer">The renderer to check</param>
public static void EnsureMaterial(Renderer renderer)
{
if (renderer == null || renderer.sharedMaterial != null)
{
return;
}
RenderPipelineUtility.VFXComponentType? componentType = null;
if (renderer is ParticleSystemRenderer)
{
componentType = RenderPipelineUtility.VFXComponentType.ParticleSystem;
}
else if (renderer is LineRenderer)
{
componentType = RenderPipelineUtility.VFXComponentType.LineRenderer;
}
else if (renderer is TrailRenderer)
{
componentType = RenderPipelineUtility.VFXComponentType.TrailRenderer;
}
if (componentType.HasValue)
{
Material defaultMat = RenderPipelineUtility.GetOrCreateDefaultVFXMaterial(componentType.Value);
if (defaultMat != null)
{
Undo.RecordObject(renderer, "Assign default VFX material");
EditorUtility.SetDirty(renderer);
renderer.sharedMaterial = defaultMat;
}
}
}
/// <summary> /// <summary>
/// Applies common Renderer properties (shadows, lighting, probes, sorting, rendering layer). /// Applies common Renderer properties (shadows, lighting, probes, sorting, rendering layer).
@ -127,12 +164,48 @@ namespace MCPForUnity.Editor.Helpers
/// <param name="params">JSON parameters containing materialPath</param> /// <param name="params">JSON parameters containing materialPath</param>
/// <param name="undoName">Name for the undo operation</param> /// <param name="undoName">Name for the undo operation</param>
/// <param name="findMaterial">Function to find material by path</param> /// <param name="findMaterial">Function to find material by path</param>
public static object SetRendererMaterial(Renderer renderer, JObject @params, string undoName, Func<string, Material> findMaterial) /// <param name="autoAssignDefault">If true, auto-assigns default material when materialPath is not provided</param>
public static object SetRendererMaterial(Renderer renderer, JObject @params, string undoName, Func<string, Material> findMaterial, bool autoAssignDefault = true)
{ {
if (renderer == null) return new { success = false, message = "Renderer not found" }; if (renderer == null) return new { success = false, message = "Renderer not found" };
string path = @params["materialPath"]?.ToString(); string path = @params["materialPath"]?.ToString();
if (string.IsNullOrEmpty(path)) return new { success = false, message = "materialPath required" };
if (string.IsNullOrEmpty(path))
{
if (!autoAssignDefault)
{
return new { success = false, message = "materialPath required" };
}
RenderPipelineUtility.VFXComponentType? componentType = null;
if (renderer is ParticleSystemRenderer)
{
componentType = RenderPipelineUtility.VFXComponentType.ParticleSystem;
}
else if (renderer is LineRenderer)
{
componentType = RenderPipelineUtility.VFXComponentType.LineRenderer;
}
else if (renderer is TrailRenderer)
{
componentType = RenderPipelineUtility.VFXComponentType.TrailRenderer;
}
if (componentType.HasValue)
{
Material defaultMat = RenderPipelineUtility.GetOrCreateDefaultVFXMaterial(componentType.Value);
if (defaultMat != null)
{
Undo.RecordObject(renderer, undoName);
renderer.sharedMaterial = defaultMat;
EditorUtility.SetDirty(renderer);
return new { success = true, message = $"Auto-assigned default material: {defaultMat.name}" };
}
}
return new { success = false, message = "materialPath required" };
}
Material mat = findMaterial(path); Material mat = findMaterial(path);
if (mat == null) return new { success = false, message = $"Material not found: {path}" }; if (mat == null) return new { success = false, message = $"Material not found: {path}" };

View File

@ -130,7 +130,7 @@ namespace MCPForUnity.Editor.Tools
} }
// Extract parameters // Extract parameters
string action = @params["action"]?.ToString()?.ToLower(); string action = @params["action"]?.ToString()?.ToLowerInvariant();
string name = @params["name"]?.ToString(); string name = @params["name"]?.ToString();
string path = @params["path"]?.ToString(); // Relative to Assets/ string path = @params["path"]?.ToString(); // Relative to Assets/
string contents = null; string contents = null;

View File

@ -1,6 +1,7 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Tools.Vfx namespace MCPForUnity.Editor.Tools.Vfx
{ {
@ -18,6 +19,29 @@ namespace MCPForUnity.Editor.Tools.Vfx
lr.positionCount = 2; lr.positionCount = 2;
lr.SetPosition(0, start); lr.SetPosition(0, start);
lr.SetPosition(1, end); lr.SetPosition(1, end);
RendererHelpers.EnsureMaterial(lr);
// Apply optional width
if (@params["width"] != null)
{
float w = @params["width"].ToObject<float>();
lr.startWidth = w;
lr.endWidth = w;
}
if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject<float>();
if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject<float>();
// Apply optional color
if (@params["color"] != null)
{
Color c = ManageVfxCommon.ParseColor(@params["color"]);
lr.startColor = c;
lr.endColor = c;
}
if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]);
if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]);
EditorUtility.SetDirty(lr); EditorUtility.SetDirty(lr);
return new { success = true, message = "Created line" }; return new { success = true, message = "Created line" };
@ -49,6 +73,28 @@ namespace MCPForUnity.Editor.Tools.Vfx
lr.SetPosition(i, point); lr.SetPosition(i, point);
} }
RendererHelpers.EnsureMaterial(lr);
// Apply optional width
if (@params["width"] != null)
{
float w = @params["width"].ToObject<float>();
lr.startWidth = w;
lr.endWidth = w;
}
if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject<float>();
if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject<float>();
// Apply optional color
if (@params["color"] != null)
{
Color c = ManageVfxCommon.ParseColor(@params["color"]);
lr.startColor = c;
lr.endColor = c;
}
if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]);
if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]);
EditorUtility.SetDirty(lr); EditorUtility.SetDirty(lr);
return new { success = true, message = $"Created circle with {segments} segments" }; return new { success = true, message = $"Created circle with {segments} segments" };
} }
@ -82,6 +128,28 @@ namespace MCPForUnity.Editor.Tools.Vfx
lr.SetPosition(i, point); lr.SetPosition(i, point);
} }
RendererHelpers.EnsureMaterial(lr);
// Apply optional width
if (@params["width"] != null)
{
float w = @params["width"].ToObject<float>();
lr.startWidth = w;
lr.endWidth = w;
}
if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject<float>();
if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject<float>();
// Apply optional color
if (@params["color"] != null)
{
Color c = ManageVfxCommon.ParseColor(@params["color"]);
lr.startColor = c;
lr.endColor = c;
}
if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]);
if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]);
EditorUtility.SetDirty(lr); EditorUtility.SetDirty(lr);
return new { success = true, message = $"Created arc with {segments} segments" }; return new { success = true, message = $"Created arc with {segments} segments" };
} }
@ -123,6 +191,28 @@ namespace MCPForUnity.Editor.Tools.Vfx
lr.SetPosition(i, point); lr.SetPosition(i, point);
} }
RendererHelpers.EnsureMaterial(lr);
// Apply optional width
if (@params["width"] != null)
{
float w = @params["width"].ToObject<float>();
lr.startWidth = w;
lr.endWidth = w;
}
if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject<float>();
if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject<float>();
// Apply optional color
if (@params["color"] != null)
{
Color c = ManageVfxCommon.ParseColor(@params["color"]);
lr.startColor = c;
lr.endColor = c;
}
if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]);
if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]);
EditorUtility.SetDirty(lr); EditorUtility.SetDirty(lr);
return new { success = true, message = $"Created {(isQuadratic ? "quadratic" : "cubic")} Bezier" }; return new { success = true, message = $"Created {(isQuadratic ? "quadratic" : "cubic")} Bezier" };
} }

View File

@ -13,6 +13,8 @@ namespace MCPForUnity.Editor.Tools.Vfx
LineRenderer lr = LineRead.FindLineRenderer(@params); LineRenderer lr = LineRead.FindLineRenderer(@params);
if (lr == null) return new { success = false, message = "LineRenderer not found" }; if (lr == null) return new { success = false, message = "LineRenderer not found" };
RendererHelpers.EnsureMaterial(lr);
JArray posArr = @params["positions"] as JArray; JArray posArr = @params["positions"] as JArray;
if (posArr == null) return new { success = false, message = "Positions array required" }; if (posArr == null) return new { success = false, message = "Positions array required" };
@ -35,6 +37,8 @@ namespace MCPForUnity.Editor.Tools.Vfx
LineRenderer lr = LineRead.FindLineRenderer(@params); LineRenderer lr = LineRead.FindLineRenderer(@params);
if (lr == null) return new { success = false, message = "LineRenderer not found" }; if (lr == null) return new { success = false, message = "LineRenderer not found" };
RendererHelpers.EnsureMaterial(lr);
Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]); Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]);
Undo.RecordObject(lr, "Add Line Position"); Undo.RecordObject(lr, "Add Line Position");
@ -51,6 +55,8 @@ namespace MCPForUnity.Editor.Tools.Vfx
LineRenderer lr = LineRead.FindLineRenderer(@params); LineRenderer lr = LineRead.FindLineRenderer(@params);
if (lr == null) return new { success = false, message = "LineRenderer not found" }; if (lr == null) return new { success = false, message = "LineRenderer not found" };
RendererHelpers.EnsureMaterial(lr);
int index = @params["index"]?.ToObject<int>() ?? -1; int index = @params["index"]?.ToObject<int>() ?? -1;
if (index < 0 || index >= lr.positionCount) return new { success = false, message = $"Invalid index {index}" }; if (index < 0 || index >= lr.positionCount) return new { success = false, message = $"Invalid index {index}" };
@ -68,6 +74,8 @@ namespace MCPForUnity.Editor.Tools.Vfx
LineRenderer lr = LineRead.FindLineRenderer(@params); LineRenderer lr = LineRead.FindLineRenderer(@params);
if (lr == null) return new { success = false, message = "LineRenderer not found" }; if (lr == null) return new { success = false, message = "LineRenderer not found" };
RendererHelpers.EnsureMaterial(lr);
Undo.RecordObject(lr, "Set Line Width"); Undo.RecordObject(lr, "Set Line Width");
var changes = new List<string>(); var changes = new List<string>();
@ -85,6 +93,8 @@ namespace MCPForUnity.Editor.Tools.Vfx
LineRenderer lr = LineRead.FindLineRenderer(@params); LineRenderer lr = LineRead.FindLineRenderer(@params);
if (lr == null) return new { success = false, message = "LineRenderer not found" }; if (lr == null) return new { success = false, message = "LineRenderer not found" };
RendererHelpers.EnsureMaterial(lr);
Undo.RecordObject(lr, "Set Line Color"); Undo.RecordObject(lr, "Set Line Color");
var changes = new List<string>(); var changes = new List<string>();
@ -108,9 +118,49 @@ namespace MCPForUnity.Editor.Tools.Vfx
LineRenderer lr = LineRead.FindLineRenderer(@params); LineRenderer lr = LineRead.FindLineRenderer(@params);
if (lr == null) return new { success = false, message = "LineRenderer not found" }; if (lr == null) return new { success = false, message = "LineRenderer not found" };
RendererHelpers.EnsureMaterial(lr);
Undo.RecordObject(lr, "Set Line Properties"); Undo.RecordObject(lr, "Set Line Properties");
var changes = new List<string>(); var changes = new List<string>();
// Handle material if provided
if (@params["materialPath"] != null)
{
Material mat = ManageVfxCommon.FindMaterialByPath(@params["materialPath"].ToString());
if (mat != null)
{
lr.sharedMaterial = mat;
changes.Add($"material={mat.name}");
}
else
{
McpLog.Warn($"Material not found: {@params["materialPath"]}");
}
}
// Handle positions if provided
if (@params["positions"] != null)
{
JArray posArr = @params["positions"] as JArray;
if (posArr != null && posArr.Count > 0)
{
var positions = new Vector3[posArr.Count];
for (int i = 0; i < posArr.Count; i++)
{
positions[i] = ManageVfxCommon.ParseVector3(posArr[i]);
}
lr.positionCount = positions.Length;
lr.SetPositions(positions);
changes.Add($"positions({positions.Length})");
}
}
else if (@params["positionCount"] != null)
{
int count = @params["positionCount"].ToObject<int>();
lr.positionCount = count;
changes.Add("positionCount");
}
RendererHelpers.ApplyLineTrailProperties(@params, changes, RendererHelpers.ApplyLineTrailProperties(@params, changes,
v => lr.loop = v, v => lr.useWorldSpace = v, v => lr.loop = v, v => lr.useWorldSpace = v,
v => lr.numCornerVertices = v, v => lr.numCapVertices = v, v => lr.numCornerVertices = v, v => lr.numCapVertices = v,

View File

@ -20,14 +20,257 @@ namespace MCPForUnity.Editor.Tools.Vfx
/// - Visual Effect Graph (modern GPU particles, currently only support HDRP, other SRPs may not work) /// - Visual Effect Graph (modern GPU particles, currently only support HDRP, other SRPs may not work)
/// - LineRenderer (lines, bezier curves, shapes) /// - LineRenderer (lines, bezier curves, shapes)
/// - TrailRenderer (motion trails) /// - TrailRenderer (motion trails)
/// - More to come based on demand and feedback! ///
/// COMPONENT REQUIREMENTS:
/// - particle_* actions require ParticleSystem component on target GameObject
/// - vfx_* actions require VisualEffect component (+ com.unity.visualeffectgraph package)
/// - line_* actions require LineRenderer component
/// - trail_* actions require TrailRenderer component
///
/// TARGETING:
/// Use 'target' parameter with optional 'searchMethod':
/// - by_name (default): "Fire" finds first GameObject named "Fire"
/// - by_path: "Effects/Fire" finds GameObject at hierarchy path
/// - by_id: "12345" finds GameObject by instance ID (most reliable)
/// - by_tag: "Enemy" finds first GameObject with tag
///
/// AUTOMATIC MATERIAL ASSIGNMENT:
/// VFX components (ParticleSystem, LineRenderer, TrailRenderer) automatically receive
/// appropriate default materials based on the active rendering pipeline when no material
/// is explicitly specified:
/// - Built-in Pipeline: Uses Unity's built-in Default-Particle.mat and Default-Line.mat
/// - URP/HDRP: Creates materials with pipeline-appropriate unlit shaders
/// - Materials are cached to avoid recreation
/// - Explicit materialPath parameter always overrides auto-assignment
/// - Auto-assigned materials are logged for transparency
///
/// AVAILABLE ACTIONS:
///
/// ParticleSystem (particle_*):
/// - particle_get_info: Get system info and current state
/// - particle_set_main: Set main module (duration, looping, startLifetime, startSpeed, startSize, startColor, gravityModifier, maxParticles, simulationSpace, playOnAwake, etc.)
/// - particle_set_emission: Set emission module (rateOverTime, rateOverDistance)
/// - particle_set_shape: Set shape module (shapeType, radius, angle, arc, position, rotation, scale)
/// - particle_set_color_over_lifetime: Set color gradient over particle lifetime
/// - particle_set_size_over_lifetime: Set size curve over particle lifetime
/// - particle_set_velocity_over_lifetime: Set velocity (x, y, z, speedModifier, space)
/// - particle_set_noise: Set noise turbulence (strength, frequency, scrollSpeed, damping, octaveCount, quality)
/// - particle_set_renderer: Set renderer (renderMode, material, sortMode, minParticleSize, maxParticleSize, etc.)
/// - particle_enable_module: Enable/disable modules by name
/// - particle_play/stop/pause/restart/clear: Playback control (withChildren optional)
/// - particle_add_burst: Add emission burst (time, count, cycles, interval, probability)
/// - particle_clear_bursts: Clear all bursts
///
/// Visual Effect Graph (vfx_*):
/// Asset Management:
/// - vfx_create_asset: Create new VFX asset file (assetName, folderPath, template, overwrite)
/// - vfx_assign_asset: Assign VFX asset to VisualEffect component (target, assetPath)
/// - vfx_list_templates: List available VFX templates in project and packages
/// - vfx_list_assets: List all VFX assets (folder, search filters)
/// Runtime Control:
/// - vfx_get_info: Get VFX info including exposed parameters
/// - vfx_set_float/int/bool: Set exposed scalar parameters (parameter, value)
/// - vfx_set_vector2/vector3/vector4: Set exposed vector parameters (parameter, value as array)
/// - vfx_set_color: Set exposed color (parameter, color as [r,g,b,a])
/// - vfx_set_gradient: Set exposed gradient (parameter, gradient)
/// - vfx_set_texture: Set exposed texture (parameter, texturePath)
/// - vfx_set_mesh: Set exposed mesh (parameter, meshPath)
/// - vfx_set_curve: Set exposed animation curve (parameter, curve)
/// - vfx_send_event: Send event with attributes (eventName, position, velocity, color, size, lifetime)
/// - vfx_play/stop/pause/reinit: Playback control
/// - vfx_set_playback_speed: Set playback speed multiplier (playRate)
/// - vfx_set_seed: Set random seed (seed, resetSeedOnPlay)
///
/// LineRenderer (line_*):
/// - line_get_info: Get line info (position count, width, color, etc.)
/// - line_set_positions: Set all positions (positions as [[x,y,z], ...])
/// - line_add_position: Add position at end (position as [x,y,z])
/// - line_set_position: Set specific position (index, position)
/// - line_set_width: Set width (width, startWidth, endWidth, widthCurve, widthMultiplier)
/// - line_set_color: Set color (color, gradient, startColor, endColor)
/// - line_set_material: Set material (materialPath)
/// - line_set_properties: Set renderer properties (loop, useWorldSpace, alignment, textureMode, numCornerVertices, numCapVertices, etc.)
/// - line_clear: Clear all positions
/// Shape Creation:
/// - line_create_line: Create simple line (start, end, segments)
/// - line_create_circle: Create circle (center, radius, segments, normal)
/// - line_create_arc: Create arc (center, radius, startAngle, endAngle, segments, normal)
/// - line_create_bezier: Create Bezier curve (start, end, controlPoint1, controlPoint2, segments)
///
/// TrailRenderer (trail_*):
/// - trail_get_info: Get trail info
/// - trail_set_time: Set trail duration (time)
/// - trail_set_width: Set width (width, startWidth, endWidth, widthCurve, widthMultiplier)
/// - trail_set_color: Set color (color, gradient, startColor, endColor)
/// - trail_set_material: Set material (materialPath)
/// - trail_set_properties: Set properties (minVertexDistance, autodestruct, emitting, alignment, textureMode, etc.)
/// - trail_clear: Clear trail
/// - trail_emit: Emit point at current position (Unity 2021.1+)
///
/// COMMON PARAMETERS:
/// - target (string): GameObject identifier
/// - searchMethod (string): "by_id" | "by_name" | "by_path" | "by_tag" | "by_layer"
/// - materialPath (string): Asset path to material (e.g., "Assets/Materials/Fire.mat")
/// - color (array): Color as [r, g, b, a] with values 0-1
/// - position (array): 3D position as [x, y, z]
/// - gradient (object): {colorKeys: [{color: [r,g,b,a], time: 0-1}], alphaKeys: [{alpha: 0-1, time: 0-1}]}
/// - curve (object): {keys: [{time: 0-1, value: number, inTangent: number, outTangent: number}]}
///
/// For full parameter details, refer to Unity documentation for each component type.
/// </summary> /// </summary>
[McpForUnityTool("manage_vfx", AutoRegister = false)] [McpForUnityTool("manage_vfx", AutoRegister = false)]
public static class ManageVFX public static class ManageVFX
{ {
private static readonly Dictionary<string, string> ParamAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "size_over_lifetime", "size" },
{ "start_color_line", "startColor" },
{ "sorting_layer_id", "sortingLayerID" },
{ "material", "materialPath" },
};
private static JObject NormalizeParams(JObject source)
{
if (source == null)
{
return new JObject();
}
var normalized = new JObject();
var properties = ExtractProperties(source);
if (properties != null)
{
foreach (var prop in properties.Properties())
{
normalized[NormalizeKey(prop.Name, true)] = NormalizeToken(prop.Value);
}
}
foreach (var prop in source.Properties())
{
if (string.Equals(prop.Name, "properties", StringComparison.OrdinalIgnoreCase))
{
continue;
}
normalized[NormalizeKey(prop.Name, true)] = NormalizeToken(prop.Value);
}
return normalized;
}
private static JObject ExtractProperties(JObject source)
{
if (source == null)
{
return null;
}
if (!source.TryGetValue("properties", StringComparison.OrdinalIgnoreCase, out var token))
{
return null;
}
if (token == null || token.Type == JTokenType.Null)
{
return null;
}
if (token is JObject obj)
{
return obj;
}
if (token.Type == JTokenType.String)
{
try
{
return JToken.Parse(token.ToString()) as JObject;
}
catch (JsonException ex)
{
throw new JsonException(
$"Failed to parse 'properties' JSON string. Raw value: {token}",
ex);
}
}
return null;
}
private static string NormalizeKey(string key, bool allowAliases)
{
if (string.IsNullOrEmpty(key))
{
return key;
}
if (string.Equals(key, "action", StringComparison.OrdinalIgnoreCase))
{
return "action";
}
if (allowAliases && ParamAliases.TryGetValue(key, out var alias))
{
return alias;
}
if (key.IndexOf('_') >= 0)
{
return ToCamelCase(key);
}
return key;
}
private static JToken NormalizeToken(JToken token)
{
if (token == null)
{
return null;
}
if (token is JObject obj)
{
var normalized = new JObject();
foreach (var prop in obj.Properties())
{
normalized[NormalizeKey(prop.Name, false)] = NormalizeToken(prop.Value);
}
return normalized;
}
if (token is JArray array)
{
var normalized = new JArray();
foreach (var item in array)
{
normalized.Add(NormalizeToken(item));
}
return normalized;
}
return token;
}
private static string ToCamelCase(string key)
{
if (string.IsNullOrEmpty(key) || key.IndexOf('_') < 0)
{
return key;
}
var parts = key.Split('_');
if (parts.Length == 0)
{
return key;
}
var first = parts[0];
var rest = string.Concat(parts.Skip(1).Select(part =>
string.IsNullOrEmpty(part) ? "" : char.ToUpperInvariant(part[0]) + part.Substring(1)));
return first + rest;
}
public static object HandleCommand(JObject @params) public static object HandleCommand(JObject @params)
{ {
string action = @params["action"]?.ToString(); JObject normalizedParams = NormalizeParams(@params);
string action = normalizedParams["action"]?.ToString();
if (string.IsNullOrEmpty(action)) if (string.IsNullOrEmpty(action))
{ {
return new { success = false, message = "Action is required" }; return new { success = false, message = "Action is required" };
@ -46,25 +289,25 @@ namespace MCPForUnity.Editor.Tools.Vfx
// ParticleSystem actions (particle_*) // ParticleSystem actions (particle_*)
if (actionLower.StartsWith("particle_")) if (actionLower.StartsWith("particle_"))
{ {
return HandleParticleSystemAction(@params, actionLower.Substring(9)); return HandleParticleSystemAction(normalizedParams, actionLower.Substring(9));
} }
// VFX Graph actions (vfx_*) // VFX Graph actions (vfx_*)
if (actionLower.StartsWith("vfx_")) if (actionLower.StartsWith("vfx_"))
{ {
return HandleVFXGraphAction(@params, actionLower.Substring(4)); return HandleVFXGraphAction(normalizedParams, actionLower.Substring(4));
} }
// LineRenderer actions (line_*) // LineRenderer actions (line_*)
if (actionLower.StartsWith("line_")) if (actionLower.StartsWith("line_"))
{ {
return HandleLineRendererAction(@params, actionLower.Substring(5)); return HandleLineRendererAction(normalizedParams, actionLower.Substring(5));
} }
// TrailRenderer actions (trail_*) // TrailRenderer actions (trail_*)
if (actionLower.StartsWith("trail_")) if (actionLower.StartsWith("trail_"))
{ {
return HandleTrailRendererAction(@params, actionLower.Substring(6)); return HandleTrailRendererAction(normalizedParams, actionLower.Substring(6));
} }
return new { success = false, message = $"Unknown action: {action}. Actions must be prefixed with: particle_, vfx_, line_, or trail_" }; return new { success = false, message = $"Unknown action: {action}. Actions must be prefixed with: particle_, vfx_, line_, or trail_" };

View File

@ -2,6 +2,7 @@ using System;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Tools.Vfx namespace MCPForUnity.Editor.Tools.Vfx
{ {
@ -42,6 +43,16 @@ namespace MCPForUnity.Editor.Tools.Vfx
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" }; if (ps == null) return new { success = false, message = "ParticleSystem not found" };
// Ensure material is assigned before playing
if (action == "play" || action == "restart")
{
var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer != null)
{
RendererHelpers.EnsureMaterial(renderer);
}
}
bool withChildren = @params["withChildren"]?.ToObject<bool>() ?? true; bool withChildren = @params["withChildren"]?.ToObject<bool>() ?? true;
switch (action) switch (action)
@ -62,6 +73,13 @@ namespace MCPForUnity.Editor.Tools.Vfx
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" }; if (ps == null) return new { success = false, message = "ParticleSystem not found" };
// Ensure material is assigned
var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer != null)
{
RendererHelpers.EnsureMaterial(renderer);
}
Undo.RecordObject(ps, "Add Burst"); Undo.RecordObject(ps, "Add Burst");
var emission = ps.emission; var emission = ps.emission;

View File

@ -14,6 +14,21 @@ namespace MCPForUnity.Editor.Tools.Vfx
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" }; if (ps == null) return new { success = false, message = "ParticleSystem not found" };
// Ensure material is assigned before any configuration
var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer != null)
{
RendererHelpers.EnsureMaterial(renderer);
}
// Stop particle system if it's playing and duration needs to be changed
bool wasPlaying = ps.isPlaying;
bool needsStop = @params["duration"] != null && wasPlaying;
if (needsStop)
{
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
}
Undo.RecordObject(ps, "Set ParticleSystem Main"); Undo.RecordObject(ps, "Set ParticleSystem Main");
var main = ps.main; var main = ps.main;
var changes = new List<string>(); var changes = new List<string>();
@ -34,6 +49,14 @@ namespace MCPForUnity.Editor.Tools.Vfx
if (@params["maxParticles"] != null) { main.maxParticles = @params["maxParticles"].ToObject<int>(); changes.Add("maxParticles"); } if (@params["maxParticles"] != null) { main.maxParticles = @params["maxParticles"].ToObject<int>(); changes.Add("maxParticles"); }
EditorUtility.SetDirty(ps); EditorUtility.SetDirty(ps);
// Restart particle system if it was playing
if (needsStop && wasPlaying)
{
ps.Play(true);
changes.Add("(restarted after duration change)");
}
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
} }
@ -42,6 +65,13 @@ namespace MCPForUnity.Editor.Tools.Vfx
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" }; if (ps == null) return new { success = false, message = "ParticleSystem not found" };
// Ensure material is assigned
var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer != null)
{
RendererHelpers.EnsureMaterial(renderer);
}
Undo.RecordObject(ps, "Set ParticleSystem Emission"); Undo.RecordObject(ps, "Set ParticleSystem Emission");
var emission = ps.emission; var emission = ps.emission;
var changes = new List<string>(); var changes = new List<string>();
@ -59,6 +89,13 @@ namespace MCPForUnity.Editor.Tools.Vfx
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" }; if (ps == null) return new { success = false, message = "ParticleSystem not found" };
// Ensure material is assigned
var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer != null)
{
RendererHelpers.EnsureMaterial(renderer);
}
Undo.RecordObject(ps, "Set ParticleSystem Shape"); Undo.RecordObject(ps, "Set ParticleSystem Shape");
var shape = ps.shape; var shape = ps.shape;
var changes = new List<string>(); var changes = new List<string>();
@ -82,6 +119,13 @@ namespace MCPForUnity.Editor.Tools.Vfx
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" }; if (ps == null) return new { success = false, message = "ParticleSystem not found" };
// Ensure material is assigned
var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer != null)
{
RendererHelpers.EnsureMaterial(renderer);
}
Undo.RecordObject(ps, "Set ParticleSystem Color Over Lifetime"); Undo.RecordObject(ps, "Set ParticleSystem Color Over Lifetime");
var col = ps.colorOverLifetime; var col = ps.colorOverLifetime;
var changes = new List<string>(); var changes = new List<string>();
@ -98,6 +142,13 @@ namespace MCPForUnity.Editor.Tools.Vfx
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" }; if (ps == null) return new { success = false, message = "ParticleSystem not found" };
// Ensure material is assigned
var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer != null)
{
RendererHelpers.EnsureMaterial(renderer);
}
Undo.RecordObject(ps, "Set ParticleSystem Size Over Lifetime"); Undo.RecordObject(ps, "Set ParticleSystem Size Over Lifetime");
var sol = ps.sizeOverLifetime; var sol = ps.sizeOverLifetime;
var changes = new List<string>(); var changes = new List<string>();
@ -130,6 +181,13 @@ namespace MCPForUnity.Editor.Tools.Vfx
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" }; if (ps == null) return new { success = false, message = "ParticleSystem not found" };
// Ensure material is assigned
var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer != null)
{
RendererHelpers.EnsureMaterial(renderer);
}
Undo.RecordObject(ps, "Set ParticleSystem Velocity Over Lifetime"); Undo.RecordObject(ps, "Set ParticleSystem Velocity Over Lifetime");
var vol = ps.velocityOverLifetime; var vol = ps.velocityOverLifetime;
var changes = new List<string>(); var changes = new List<string>();
@ -150,6 +208,13 @@ namespace MCPForUnity.Editor.Tools.Vfx
ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); ParticleSystem ps = ParticleCommon.FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" }; if (ps == null) return new { success = false, message = "ParticleSystem not found" };
// Ensure material is assigned
var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer != null)
{
RendererHelpers.EnsureMaterial(renderer);
}
Undo.RecordObject(ps, "Set ParticleSystem Noise"); Undo.RecordObject(ps, "Set ParticleSystem Noise");
var noise = ps.noise; var noise = ps.noise;
var changes = new List<string>(); var changes = new List<string>();
@ -174,6 +239,9 @@ namespace MCPForUnity.Editor.Tools.Vfx
var renderer = ps.GetComponent<ParticleSystemRenderer>(); var renderer = ps.GetComponent<ParticleSystemRenderer>();
if (renderer == null) return new { success = false, message = "ParticleSystemRenderer not found" }; if (renderer == null) return new { success = false, message = "ParticleSystemRenderer not found" };
// Ensure material is set before any other operations
RendererHelpers.EnsureMaterial(renderer);
Undo.RecordObject(renderer, "Set ParticleSystem Renderer"); Undo.RecordObject(renderer, "Set ParticleSystem Renderer");
var changes = new List<string>(); var changes = new List<string>();
@ -199,10 +267,20 @@ namespace MCPForUnity.Editor.Tools.Vfx
if (@params["materialPath"] != null) if (@params["materialPath"] != null)
{ {
var findInst = new JObject { ["find"] = @params["materialPath"].ToString() }; string matPath = @params["materialPath"].ToString();
var findInst = new JObject { ["find"] = matPath };
Material mat = ObjectResolver.Resolve(findInst, typeof(Material)) as Material; Material mat = ObjectResolver.Resolve(findInst, typeof(Material)) as Material;
if (mat != null) { renderer.sharedMaterial = mat; changes.Add("material"); } if (mat != null)
{
renderer.sharedMaterial = mat;
changes.Add($"material={mat.name}");
} }
else
{
McpLog.Warn($"Material not found at path: {matPath}. Keeping existing material.");
}
}
if (@params["trailMaterialPath"] != null) if (@params["trailMaterialPath"] != null)
{ {
var findInst = new JObject { ["find"] = @params["trailMaterialPath"].ToString() }; var findInst = new JObject { ["find"] = @params["trailMaterialPath"].ToString() };

View File

@ -1,6 +1,7 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Tools.Vfx namespace MCPForUnity.Editor.Tools.Vfx
{ {
@ -21,6 +22,8 @@ namespace MCPForUnity.Editor.Tools.Vfx
TrailRenderer tr = TrailRead.FindTrailRenderer(@params); TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
if (tr == null) return new { success = false, message = "TrailRenderer not found" }; if (tr == null) return new { success = false, message = "TrailRenderer not found" };
RendererHelpers.EnsureMaterial(tr);
#if UNITY_2021_1_OR_NEWER #if UNITY_2021_1_OR_NEWER
Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]); Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]);
tr.AddPosition(pos); tr.AddPosition(pos);

View File

@ -14,6 +14,8 @@ namespace MCPForUnity.Editor.Tools.Vfx
TrailRenderer tr = TrailRead.FindTrailRenderer(@params); TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
if (tr == null) return new { success = false, message = "TrailRenderer not found" }; if (tr == null) return new { success = false, message = "TrailRenderer not found" };
RendererHelpers.EnsureMaterial(tr);
float time = @params["time"]?.ToObject<float>() ?? 5f; float time = @params["time"]?.ToObject<float>() ?? 5f;
Undo.RecordObject(tr, "Set Trail Time"); Undo.RecordObject(tr, "Set Trail Time");
@ -28,6 +30,8 @@ namespace MCPForUnity.Editor.Tools.Vfx
TrailRenderer tr = TrailRead.FindTrailRenderer(@params); TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
if (tr == null) return new { success = false, message = "TrailRenderer not found" }; if (tr == null) return new { success = false, message = "TrailRenderer not found" };
RendererHelpers.EnsureMaterial(tr);
Undo.RecordObject(tr, "Set Trail Width"); Undo.RecordObject(tr, "Set Trail Width");
var changes = new List<string>(); var changes = new List<string>();
@ -45,6 +49,8 @@ namespace MCPForUnity.Editor.Tools.Vfx
TrailRenderer tr = TrailRead.FindTrailRenderer(@params); TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
if (tr == null) return new { success = false, message = "TrailRenderer not found" }; if (tr == null) return new { success = false, message = "TrailRenderer not found" };
RendererHelpers.EnsureMaterial(tr);
Undo.RecordObject(tr, "Set Trail Color"); Undo.RecordObject(tr, "Set Trail Color");
var changes = new List<string>(); var changes = new List<string>();
@ -68,9 +74,43 @@ namespace MCPForUnity.Editor.Tools.Vfx
TrailRenderer tr = TrailRead.FindTrailRenderer(@params); TrailRenderer tr = TrailRead.FindTrailRenderer(@params);
if (tr == null) return new { success = false, message = "TrailRenderer not found" }; if (tr == null) return new { success = false, message = "TrailRenderer not found" };
RendererHelpers.EnsureMaterial(tr);
Undo.RecordObject(tr, "Set Trail Properties"); Undo.RecordObject(tr, "Set Trail Properties");
var changes = new List<string>(); var changes = new List<string>();
// Handle material if provided
if (@params["materialPath"] != null)
{
Material mat = ManageVfxCommon.FindMaterialByPath(@params["materialPath"].ToString());
if (mat != null)
{
tr.sharedMaterial = mat;
changes.Add($"material={mat.name}");
}
else
{
McpLog.Warn($"Material not found: {@params["materialPath"]}");
}
}
// Handle time if provided
if (@params["time"] != null) { tr.time = @params["time"].ToObject<float>(); changes.Add("time"); }
// Handle width properties if provided
if (@params["width"] != null || @params["startWidth"] != null || @params["endWidth"] != null)
{
if (@params["width"] != null)
{
float w = @params["width"].ToObject<float>();
tr.startWidth = w;
tr.endWidth = w;
changes.Add("width");
}
if (@params["startWidth"] != null) { tr.startWidth = @params["startWidth"].ToObject<float>(); changes.Add("startWidth"); }
if (@params["endWidth"] != null) { tr.endWidth = @params["endWidth"].ToObject<float>(); changes.Add("endWidth"); }
}
if (@params["minVertexDistance"] != null) { tr.minVertexDistance = @params["minVertexDistance"].ToObject<float>(); changes.Add("minVertexDistance"); } if (@params["minVertexDistance"] != null) { tr.minVertexDistance = @params["minVertexDistance"].ToObject<float>(); changes.Add("minVertexDistance"); }
if (@params["autodestruct"] != null) { tr.autodestruct = @params["autodestruct"].ToObject<bool>(); changes.Add("autodestruct"); } if (@params["autodestruct"] != null) { tr.autodestruct = @params["autodestruct"].ToObject<bool>(); changes.Add("autodestruct"); }
if (@params["emitting"] != null) { tr.emitting = @params["emitting"].ToObject<bool>(); changes.Add("emitting"); } if (@params["emitting"] != null) { tr.emitting = @params["emitting"].ToObject<bool>(); changes.Add("emitting"); }

View File

@ -10,6 +10,27 @@ from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError from cli.utils.connection import run_command, UnityConnectionError
_VFX_TOP_LEVEL_KEYS = {"action", "target", "searchMethod", "properties"}
def _normalize_vfx_params(params: dict[str, Any]) -> dict[str, Any]:
params = dict(params)
properties: dict[str, Any] = {}
for key in list(params.keys()):
if key in _VFX_TOP_LEVEL_KEYS:
continue
properties[key] = params.pop(key)
if properties:
existing = params.get("properties")
if isinstance(existing, dict):
params["properties"] = {**properties, **existing}
else:
params["properties"] = properties
return {k: v for k, v in params.items() if v is not None}
@click.group() @click.group()
def vfx(): def vfx():
"""VFX operations - particle systems, line renderers, trails.""" """VFX operations - particle systems, line renderers, trails."""
@ -43,7 +64,7 @@ def particle_info(target: str, search_method: Optional[str]):
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -70,7 +91,7 @@ def particle_play(target: str, with_children: bool, search_method: Optional[str]
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
if result.get("success"): if result.get("success"):
print_success(f"Playing particle system: {target}") print_success(f"Playing particle system: {target}")
@ -93,7 +114,7 @@ def particle_stop(target: str, with_children: bool, search_method: Optional[str]
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
if result.get("success"): if result.get("success"):
print_success(f"Stopped particle system: {target}") print_success(f"Stopped particle system: {target}")
@ -113,7 +134,7 @@ def particle_pause(target: str, search_method: Optional[str]):
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -134,7 +155,7 @@ def particle_restart(target: str, with_children: bool, search_method: Optional[s
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -155,7 +176,7 @@ def particle_clear(target: str, with_children: bool, search_method: Optional[str
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -188,7 +209,7 @@ def line_info(target: str, search_method: Optional[str]):
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -223,7 +244,7 @@ def line_set_positions(target: str, positions: str, search_method: Optional[str]
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -253,7 +274,7 @@ def line_create_line(target: str, start: Tuple[float, float, float], end: Tuple[
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -286,7 +307,7 @@ def line_create_circle(target: str, center: Tuple[float, float, float], radius:
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -304,7 +325,7 @@ def line_clear(target: str, search_method: Optional[str]):
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -332,7 +353,7 @@ def trail_info(target: str, search_method: Optional[str]):
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -360,7 +381,7 @@ def trail_set_time(target: str, duration: float, search_method: Optional[str]):
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -378,7 +399,7 @@ def trail_clear(target: str, search_method: Optional[str]):
params["searchMethod"] = search_method params["searchMethod"] = search_method
try: try:
result = run_command("manage_vfx", params, config) result = run_command("manage_vfx", _normalize_vfx_params(params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))
@ -432,7 +453,7 @@ def vfx_raw(action: str, target: Optional[str], params: str, search_method: Opti
# Merge extra params # Merge extra params
request_params.update(extra_params) request_params.update(extra_params)
try: try:
result = run_command("manage_vfx", request_params, config) result = run_command("manage_vfx", _normalize_vfx_params(request_params), config)
click.echo(format_output(result, config.format)) click.echo(format_output(result, config.format))
except UnityConnectionError as e: except UnityConnectionError as e:
print_error(str(e)) print_error(str(e))

View File

@ -454,7 +454,6 @@ def _normalize_import_settings(value: Any) -> tuple[dict | None, str | None]:
description=( description=(
"Procedural texture generation for Unity. Creates textures with solid fills, " "Procedural texture generation for Unity. Creates textures with solid fills, "
"patterns (checkerboard, stripes, dots, grid, brick), gradients, and noise. " "patterns (checkerboard, stripes, dots, grid, brick), gradients, and noise. "
"Supports full CRUD operations and one-call sprite creation.\n\n"
"Actions: create, modify, delete, create_sprite, apply_pattern, apply_gradient, apply_noise" "Actions: create, modify, delete, create_sprite, apply_pattern, apply_gradient, apply_noise"
), ),
annotations=ToolAnnotations( annotations=ToolAnnotations(

View File

@ -39,86 +39,15 @@ TRAIL_ACTIONS = [
"trail_set_material", "trail_set_properties", "trail_clear", "trail_emit" "trail_set_material", "trail_set_properties", "trail_clear", "trail_emit"
] ]
ALL_ACTIONS = ["ping"] + PARTICLE_ACTIONS + \ ALL_ACTIONS = ["ping"] + PARTICLE_ACTIONS + VFX_ACTIONS + LINE_ACTIONS + TRAIL_ACTIONS
VFX_ACTIONS + LINE_ACTIONS + TRAIL_ACTIONS
@mcp_for_unity_tool( @mcp_for_unity_tool(
description="""Unified VFX management for Unity visual effects components. description=(
"Manage Unity VFX components (ParticleSystem, VisualEffect, LineRenderer, TrailRenderer). "
Each action prefix requires a specific component on the target GameObject: "Action prefixes: particle_*, vfx_*, line_*, trail_*. "
- `particle_*` actions require **ParticleSystem** component "Action-specific parameters go in `properties` (keys match ManageVFX.cs)."
- `vfx_*` actions require **VisualEffect** component (+ com.unity.visualeffectgraph package) ),
- `line_*` actions require **LineRenderer** component
- `trail_*` actions require **TrailRenderer** component
**If the component doesn't exist, the action will FAIL
Before using this tool, either:
1. Use `manage_gameobject` with `action="get_components"` to check if component exists
2. Use `manage_gameobject` with `action="add_component", component_name="ParticleSystem"` (or LineRenderer/TrailRenderer/VisualEffect) to add the component first
3. Assign material to the component beforehand to avoid empty effects
**TARGETING:**
Use `target` parameter to specify the GameObject:
- By name: `target="Fire"` (finds first GameObject named "Fire")
- By path: `target="Effects/Fire"` with `search_method="by_path"`
- By instance ID: `target="12345"` with `search_method="by_id"` (most reliable)
- By tag: `target="Player"` with `search_method="by_tag"`
**Component Types & Action Prefixes:**
- `particle_*` - ParticleSystem (legacy particle effects)
- `vfx_*` - Visual Effect Graph (modern GPU particles, requires com.unity.visualeffectgraph)
- `line_*` - LineRenderer (lines, curves, shapes)
- `trail_*` - TrailRenderer (motion trails)
**ParticleSystem Actions (particle_*):**
- particle_get_info: Get particle system info
- particle_set_main: Set main module (duration, looping, startLifetime, startSpeed, startSize, startColor, gravityModifier, maxParticles)
- particle_set_emission: Set emission (rateOverTime, rateOverDistance)
- particle_set_shape: Set shape (shapeType, radius, angle, arc, position, rotation, scale)
- particle_set_color_over_lifetime, particle_set_size_over_lifetime, particle_set_velocity_over_lifetime
- particle_set_noise: Set noise (strength, frequency, scrollSpeed)
- particle_set_renderer: Set renderer (renderMode, material)
- particle_enable_module: Enable/disable modules
- particle_play/stop/pause/restart/clear: Playback control
- particle_add_burst, particle_clear_bursts: Burst management
**VFX Graph Actions (vfx_*):**
- **Asset Management:**
- vfx_create_asset: Create a new VFX Graph asset file (requires: assetName, optional: folderPath, template, overwrite)
- vfx_assign_asset: Assign a VFX asset to a VisualEffect component (requires: target, assetPath)
- vfx_list_templates: List available VFX templates in project and packages
- vfx_list_assets: List all VFX assets in project (optional: folder, search)
- **Runtime Control:**
- vfx_get_info: Get VFX info
- vfx_set_float/int/bool: Set exposed parameters
- vfx_set_vector2/vector3/vector4: Set vector parameters
- vfx_set_color, vfx_set_gradient: Set color/gradient parameters
- vfx_set_texture, vfx_set_mesh: Set asset parameters
- vfx_set_curve: Set animation curve
- vfx_send_event: Send events with attributes (position, velocity, color, size, lifetime)
- vfx_play/stop/pause/reinit: Playback control
- vfx_set_playback_speed, vfx_set_seed
**LineRenderer Actions (line_*):**
- line_get_info: Get line info
- line_set_positions: Set all positions
- line_add_position, line_set_position: Modify positions
- line_set_width: Set width (uniform, start/end, curve)
- line_set_color: Set color (uniform, gradient)
- line_set_material, line_set_properties
- line_clear: Clear positions
- line_create_line: Create simple line
- line_create_circle: Create circle
- line_create_arc: Create arc
- line_create_bezier: Create Bezier curve
**TrailRenderer Actions (trail_*):**
- trail_get_info: Get trail info
- trail_set_time: Set trail duration
- trail_set_width, trail_set_color, trail_set_material, trail_set_properties
- trail_clear: Clear trail
- trail_emit: Emit point (Unity 2021.1+)""",
annotations=ToolAnnotations( annotations=ToolAnnotations(
title="Manage VFX", title="Manage VFX",
destructiveHint=True, destructiveHint=True,
@ -126,283 +55,16 @@ Use `target` parameter to specify the GameObject:
) )
async def manage_vfx( async def manage_vfx(
ctx: Context, ctx: Context,
action: Annotated[str, "Action to perform. Use prefix: particle_, vfx_, line_, or trail_"], action: Annotated[str, "Action to perform (prefix: particle_, vfx_, line_, trail_)."],
target: Annotated[str | None, "Target GameObject (name/path/id)."] = None,
# Target specification (common) - REQUIRED for most actions
# Using str | None to accept any string format
target: Annotated[str | None,
"Target GameObject with the VFX component. Use name (e.g. 'Fire'), path ('Effects/Fire'), instance ID, or tag. The GameObject MUST have the required component (ParticleSystem/VisualEffect/LineRenderer/TrailRenderer) for the action prefix."] = None,
search_method: Annotated[ search_method: Annotated[
Literal["by_id", "by_name", "by_path", "by_tag", "by_layer"] | None, Literal["by_id", "by_name", "by_path", "by_tag", "by_layer"] | None,
"How to find target: by_name (default), by_path (hierarchy path), by_id (instance ID - most reliable), by_tag, by_layer" "How to find the target GameObject.",
] = None,
properties: Annotated[
dict[str, Any] | str | None,
"Action-specific parameters (dict or JSON string).",
] = None, ] = None,
# === PARTICLE SYSTEM PARAMETERS ===
# Main module - All use Any to accept string coercion from MCP clients
duration: Annotated[Any,
"[Particle] Duration in seconds (number or string)"] = None,
looping: Annotated[Any,
"[Particle] Whether to loop (bool or string 'true'/'false')"] = None,
prewarm: Annotated[Any,
"[Particle] Prewarm the system (bool or string)"] = None,
start_delay: Annotated[Any,
"[Particle] Start delay (number or MinMaxCurve dict)"] = None,
start_lifetime: Annotated[Any,
"[Particle] Particle lifetime (number or MinMaxCurve dict)"] = None,
start_speed: Annotated[Any,
"[Particle] Initial speed (number or MinMaxCurve dict)"] = None,
start_size: Annotated[Any,
"[Particle] Initial size (number or MinMaxCurve dict)"] = None,
start_rotation: Annotated[Any,
"[Particle] Initial rotation (number or MinMaxCurve dict)"] = None,
start_color: Annotated[Any,
"[Particle/VFX] Start color [r,g,b,a] (array, dict, or JSON string)"] = None,
gravity_modifier: Annotated[Any,
"[Particle] Gravity multiplier (number or MinMaxCurve dict)"] = None,
simulation_space: Annotated[Literal["Local", "World",
"Custom"] | None, "[Particle] Simulation space"] = None,
scaling_mode: Annotated[Literal["Hierarchy", "Local",
"Shape"] | None, "[Particle] Scaling mode"] = None,
play_on_awake: Annotated[Any,
"[Particle] Play on awake (bool or string)"] = None,
max_particles: Annotated[Any,
"[Particle] Maximum particles (integer or string)"] = None,
# Emission
rate_over_time: Annotated[Any,
"[Particle] Emission rate over time (number or MinMaxCurve dict)"] = None,
rate_over_distance: Annotated[Any,
"[Particle] Emission rate over distance (number or MinMaxCurve dict)"] = None,
# Shape
shape_type: Annotated[Literal["Sphere", "Hemisphere", "Cone", "Box",
"Circle", "Edge", "Donut"] | None, "[Particle] Shape type"] = None,
radius: Annotated[Any,
"[Particle/Line] Shape radius (number or string)"] = None,
radius_thickness: Annotated[Any,
"[Particle] Radius thickness 0-1 (number or string)"] = None,
angle: Annotated[Any, "[Particle] Cone angle (number or string)"] = None,
arc: Annotated[Any, "[Particle] Arc angle (number or string)"] = None,
# Noise
strength: Annotated[Any,
"[Particle] Noise strength (number or MinMaxCurve dict)"] = None,
frequency: Annotated[Any,
"[Particle] Noise frequency (number or string)"] = None,
scroll_speed: Annotated[Any,
"[Particle] Noise scroll speed (number or MinMaxCurve dict)"] = None,
damping: Annotated[Any,
"[Particle] Noise damping (bool or string)"] = None,
octave_count: Annotated[Any,
"[Particle] Noise octaves 1-4 (integer or string)"] = None,
quality: Annotated[Literal["Low", "Medium", "High"]
| None, "[Particle] Noise quality"] = None,
# Module control
module: Annotated[str | None,
"[Particle] Module name to enable/disable"] = None,
enabled: Annotated[Any,
"[Particle] Enable/disable module (bool or string)"] = None,
# Burst
time: Annotated[Any,
"[Particle/Trail] Burst time or trail duration (number or string)"] = None,
count: Annotated[Any, "[Particle] Burst count (integer or string)"] = None,
min_count: Annotated[Any,
"[Particle] Min burst count (integer or string)"] = None,
max_count: Annotated[Any,
"[Particle] Max burst count (integer or string)"] = None,
cycles: Annotated[Any,
"[Particle] Burst cycles (integer or string)"] = None,
interval: Annotated[Any,
"[Particle] Burst interval (number or string)"] = None,
probability: Annotated[Any,
"[Particle] Burst probability 0-1 (number or string)"] = None,
# Playback
with_children: Annotated[Any,
"[Particle] Apply to children (bool or string)"] = None,
# === VFX GRAPH PARAMETERS ===
# Asset management
asset_name: Annotated[str | None,
"[VFX] Name for new VFX asset (without .vfx extension)"] = None,
folder_path: Annotated[str | None,
"[VFX] Folder path for new asset (default: Assets/VFX)"] = None,
template: Annotated[str | None,
"[VFX] Template name for new asset (use vfx_list_templates to see available)"] = None,
asset_path: Annotated[str | None,
"[VFX] Path to VFX asset to assign (e.g. Assets/VFX/MyEffect.vfx)"] = None,
overwrite: Annotated[Any,
"[VFX] Overwrite existing asset (bool or string)"] = None,
folder: Annotated[str | None,
"[VFX] Folder to search for assets (for vfx_list_assets)"] = None,
search: Annotated[str | None,
"[VFX] Search pattern for assets (for vfx_list_assets)"] = None,
# Runtime parameters
parameter: Annotated[str | None, "[VFX] Exposed parameter name"] = None,
value: Annotated[Any,
"[VFX] Parameter value (number, bool, array, or string)"] = None,
texture_path: Annotated[str | None, "[VFX] Texture asset path"] = None,
mesh_path: Annotated[str | None, "[VFX] Mesh asset path"] = None,
gradient: Annotated[Any,
"[VFX/Line/Trail] Gradient {colorKeys, alphaKeys} or {startColor, endColor} (dict or JSON string)"] = None,
curve: Annotated[Any,
"[VFX] Animation curve keys or {startValue, endValue} (array, dict, or JSON string)"] = None,
event_name: Annotated[str | None, "[VFX] Event name to send"] = None,
velocity: Annotated[Any,
"[VFX] Event velocity [x,y,z] (array or JSON string)"] = None,
size: Annotated[Any, "[VFX] Event size (number or string)"] = None,
lifetime: Annotated[Any, "[VFX] Event lifetime (number or string)"] = None,
play_rate: Annotated[Any,
"[VFX] Playback speed multiplier (number or string)"] = None,
seed: Annotated[Any, "[VFX] Random seed (integer or string)"] = None,
reset_seed_on_play: Annotated[Any,
"[VFX] Reset seed on play (bool or string)"] = None,
# === LINE/TRAIL RENDERER PARAMETERS ===
positions: Annotated[Any,
"[Line] Positions [[x,y,z], ...] (array or JSON string)"] = None,
position: Annotated[Any,
"[Line/Trail] Single position [x,y,z] (array or JSON string)"] = None,
index: Annotated[Any, "[Line] Position index (integer or string)"] = None,
# Width
width: Annotated[Any,
"[Line/Trail] Uniform width (number or string)"] = None,
start_width: Annotated[Any,
"[Line/Trail] Start width (number or string)"] = None,
end_width: Annotated[Any,
"[Line/Trail] End width (number or string)"] = None,
width_curve: Annotated[Any,
"[Line/Trail] Width curve (number or dict)"] = None,
width_multiplier: Annotated[Any,
"[Line/Trail] Width multiplier (number or string)"] = None,
# Color
color: Annotated[Any,
"[Line/Trail/VFX] Color [r,g,b,a] (array or JSON string)"] = None,
start_color_line: Annotated[Any,
"[Line/Trail] Start color (array or JSON string)"] = None,
end_color: Annotated[Any,
"[Line/Trail] End color (array or JSON string)"] = None,
# Material & properties
material_path: Annotated[str | None,
"[Particle/Line/Trail] Material asset path"] = None,
trail_material_path: Annotated[str | None,
"[Particle] Trail material asset path"] = None,
loop: Annotated[Any,
"[Line] Connect end to start (bool or string)"] = None,
use_world_space: Annotated[Any,
"[Line] Use world space (bool or string)"] = None,
num_corner_vertices: Annotated[Any,
"[Line/Trail] Corner vertices (integer or string)"] = None,
num_cap_vertices: Annotated[Any,
"[Line/Trail] Cap vertices (integer or string)"] = None,
alignment: Annotated[Literal["View", "Local", "TransformZ"]
| None, "[Line/Trail] Alignment"] = None,
texture_mode: Annotated[Literal["Stretch", "Tile", "DistributePerSegment",
"RepeatPerSegment"] | None, "[Line/Trail] Texture mode"] = None,
generate_lighting_data: Annotated[Any,
"[Line/Trail] Generate lighting data for GI (bool or string)"] = None,
sorting_order: Annotated[Any,
"[Line/Trail/Particle] Sorting order (integer or string)"] = None,
sorting_layer_name: Annotated[str | None,
"[Renderer] Sorting layer name"] = None,
sorting_layer_id: Annotated[Any,
"[Renderer] Sorting layer ID (integer or string)"] = None,
render_mode: Annotated[str | None,
"[Particle] Render mode (Billboard, Stretch, HorizontalBillboard, VerticalBillboard, Mesh, None)"] = None,
sort_mode: Annotated[str | None,
"[Particle] Sort mode (None, Distance, OldestInFront, YoungestInFront, Depth)"] = None,
# === RENDERER COMMON PROPERTIES (Shadows, Lighting, Probes) ===
shadow_casting_mode: Annotated[Literal["Off", "On", "TwoSided",
"ShadowsOnly"] | None, "[Renderer] Shadow casting mode"] = None,
receive_shadows: Annotated[Any,
"[Renderer] Receive shadows (bool or string)"] = None,
shadow_bias: Annotated[Any,
"[Renderer] Shadow bias (number or string)"] = None,
light_probe_usage: Annotated[Literal["Off", "BlendProbes", "UseProxyVolume",
"CustomProvided"] | None, "[Renderer] Light probe usage mode"] = None,
reflection_probe_usage: Annotated[Literal["Off", "BlendProbes", "BlendProbesAndSkybox",
"Simple"] | None, "[Renderer] Reflection probe usage mode"] = None,
motion_vector_generation_mode: Annotated[Literal["Camera", "Object",
"ForceNoMotion"] | None, "[Renderer] Motion vector generation mode"] = None,
rendering_layer_mask: Annotated[Any,
"[Renderer] Rendering layer mask for SRP (integer or string)"] = None,
# === PARTICLE RENDERER SPECIFIC ===
min_particle_size: Annotated[Any,
"[Particle] Min particle size relative to viewport (number or string)"] = None,
max_particle_size: Annotated[Any,
"[Particle] Max particle size relative to viewport (number or string)"] = None,
length_scale: Annotated[Any,
"[Particle] Length scale for stretched billboard (number or string)"] = None,
velocity_scale: Annotated[Any,
"[Particle] Velocity scale for stretched billboard (number or string)"] = None,
camera_velocity_scale: Annotated[Any,
"[Particle] Camera velocity scale for stretched billboard (number or string)"] = None,
normal_direction: Annotated[Any,
"[Particle] Normal direction 0-1 (number or string)"] = None,
pivot: Annotated[Any,
"[Particle] Pivot offset [x,y,z] (array or JSON string)"] = None,
flip: Annotated[Any,
"[Particle] Flip [x,y,z] (array or JSON string)"] = None,
allow_roll: Annotated[Any,
"[Particle] Allow roll for mesh particles (bool or string)"] = None,
# Shape creation (line_create_*)
start: Annotated[Any,
"[Line] Start point [x,y,z] (array or JSON string)"] = None,
end: Annotated[Any,
"[Line] End point [x,y,z] (array or JSON string)"] = None,
center: Annotated[Any,
"[Line] Circle/arc center [x,y,z] (array or JSON string)"] = None,
segments: Annotated[Any,
"[Line] Number of segments (integer or string)"] = None,
normal: Annotated[Any,
"[Line] Normal direction [x,y,z] (array or JSON string)"] = None,
start_angle: Annotated[Any,
"[Line] Arc start angle degrees (number or string)"] = None,
end_angle: Annotated[Any,
"[Line] Arc end angle degrees (number or string)"] = None,
control_point1: Annotated[Any,
"[Line] Bezier control point 1 (array or JSON string)"] = None,
control_point2: Annotated[Any,
"[Line] Bezier control point 2 (cubic) (array or JSON string)"] = None,
# Trail specific
min_vertex_distance: Annotated[Any,
"[Trail] Min vertex distance (number or string)"] = None,
autodestruct: Annotated[Any,
"[Trail] Destroy when finished (bool or string)"] = None,
emitting: Annotated[Any, "[Trail] Is emitting (bool or string)"] = None,
# Common vector params for shape/velocity
x: Annotated[Any,
"[Particle] Velocity X (number or MinMaxCurve dict)"] = None,
y: Annotated[Any,
"[Particle] Velocity Y (number or MinMaxCurve dict)"] = None,
z: Annotated[Any,
"[Particle] Velocity Z (number or MinMaxCurve dict)"] = None,
speed_modifier: Annotated[Any,
"[Particle] Speed modifier (number or MinMaxCurve dict)"] = None,
space: Annotated[Literal["Local", "World"] |
None, "[Particle] Velocity space"] = None,
separate_axes: Annotated[Any,
"[Particle] Separate XYZ axes (bool or string)"] = None,
size_over_lifetime: Annotated[Any,
"[Particle] Size over lifetime (number or MinMaxCurve dict)"] = None,
size_x: Annotated[Any,
"[Particle] Size X (number or MinMaxCurve dict)"] = None,
size_y: Annotated[Any,
"[Particle] Size Y (number or MinMaxCurve dict)"] = None,
size_z: Annotated[Any,
"[Particle] Size Z (number or MinMaxCurve dict)"] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Unified VFX management tool.""" """Unified VFX management tool."""
@ -437,294 +99,14 @@ async def manage_vfx(
unity_instance = get_unity_instance_from_context(ctx) unity_instance = get_unity_instance_from_context(ctx)
# Build parameters dict with normalized action to stay consistent with Unity
params_dict: dict[str, Any] = {"action": action_normalized} params_dict: dict[str, Any] = {"action": action_normalized}
if properties is not None:
# Target params_dict["properties"] = properties
if target is not None: if target is not None:
params_dict["target"] = target params_dict["target"] = target
if search_method is not None: if search_method is not None:
params_dict["searchMethod"] = search_method params_dict["searchMethod"] = search_method
# === PARTICLE SYSTEM ===
# Pass through all values - C# side handles parsing (ParseColor, ParseVector3, ParseMinMaxCurve, ToObject<T>)
if duration is not None:
params_dict["duration"] = duration
if looping is not None:
params_dict["looping"] = looping
if prewarm is not None:
params_dict["prewarm"] = prewarm
if start_delay is not None:
params_dict["startDelay"] = start_delay
if start_lifetime is not None:
params_dict["startLifetime"] = start_lifetime
if start_speed is not None:
params_dict["startSpeed"] = start_speed
if start_size is not None:
params_dict["startSize"] = start_size
if start_rotation is not None:
params_dict["startRotation"] = start_rotation
if start_color is not None:
params_dict["startColor"] = start_color
if gravity_modifier is not None:
params_dict["gravityModifier"] = gravity_modifier
if simulation_space is not None:
params_dict["simulationSpace"] = simulation_space
if scaling_mode is not None:
params_dict["scalingMode"] = scaling_mode
if play_on_awake is not None:
params_dict["playOnAwake"] = play_on_awake
if max_particles is not None:
params_dict["maxParticles"] = max_particles
# Emission
if rate_over_time is not None:
params_dict["rateOverTime"] = rate_over_time
if rate_over_distance is not None:
params_dict["rateOverDistance"] = rate_over_distance
# Shape
if shape_type is not None:
params_dict["shapeType"] = shape_type
if radius is not None:
params_dict["radius"] = radius
if radius_thickness is not None:
params_dict["radiusThickness"] = radius_thickness
if angle is not None:
params_dict["angle"] = angle
if arc is not None:
params_dict["arc"] = arc
# Noise
if strength is not None:
params_dict["strength"] = strength
if frequency is not None:
params_dict["frequency"] = frequency
if scroll_speed is not None:
params_dict["scrollSpeed"] = scroll_speed
if damping is not None:
params_dict["damping"] = damping
if octave_count is not None:
params_dict["octaveCount"] = octave_count
if quality is not None:
params_dict["quality"] = quality
# Module
if module is not None:
params_dict["module"] = module
if enabled is not None:
params_dict["enabled"] = enabled
# Burst
if time is not None:
params_dict["time"] = time
if count is not None:
params_dict["count"] = count
if min_count is not None:
params_dict["minCount"] = min_count
if max_count is not None:
params_dict["maxCount"] = max_count
if cycles is not None:
params_dict["cycles"] = cycles
if interval is not None:
params_dict["interval"] = interval
if probability is not None:
params_dict["probability"] = probability
# Playback
if with_children is not None:
params_dict["withChildren"] = with_children
# === VFX GRAPH ===
# Asset management parameters
if asset_name is not None:
params_dict["assetName"] = asset_name
if folder_path is not None:
params_dict["folderPath"] = folder_path
if template is not None:
params_dict["template"] = template
if asset_path is not None:
params_dict["assetPath"] = asset_path
if overwrite is not None:
params_dict["overwrite"] = overwrite
if folder is not None:
params_dict["folder"] = folder
if search is not None:
params_dict["search"] = search
# Runtime parameters
if parameter is not None:
params_dict["parameter"] = parameter
if value is not None:
params_dict["value"] = value
if texture_path is not None:
params_dict["texturePath"] = texture_path
if mesh_path is not None:
params_dict["meshPath"] = mesh_path
if gradient is not None:
params_dict["gradient"] = gradient
if curve is not None:
params_dict["curve"] = curve
if event_name is not None:
params_dict["eventName"] = event_name
if velocity is not None:
params_dict["velocity"] = velocity
if size is not None:
params_dict["size"] = size
if lifetime is not None:
params_dict["lifetime"] = lifetime
if play_rate is not None:
params_dict["playRate"] = play_rate
if seed is not None:
params_dict["seed"] = seed
if reset_seed_on_play is not None:
params_dict["resetSeedOnPlay"] = reset_seed_on_play
# === LINE/TRAIL RENDERER ===
if positions is not None:
params_dict["positions"] = positions
if position is not None:
params_dict["position"] = position
if index is not None:
params_dict["index"] = index
# Width
if width is not None:
params_dict["width"] = width
if start_width is not None:
params_dict["startWidth"] = start_width
if end_width is not None:
params_dict["endWidth"] = end_width
if width_curve is not None:
params_dict["widthCurve"] = width_curve
if width_multiplier is not None:
params_dict["widthMultiplier"] = width_multiplier
# Color
if color is not None:
params_dict["color"] = color
if start_color_line is not None:
params_dict["startColor"] = start_color_line
if end_color is not None:
params_dict["endColor"] = end_color
# Material & properties
if material_path is not None:
params_dict["materialPath"] = material_path
if trail_material_path is not None:
params_dict["trailMaterialPath"] = trail_material_path
if loop is not None:
params_dict["loop"] = loop
if use_world_space is not None:
params_dict["useWorldSpace"] = use_world_space
if num_corner_vertices is not None:
params_dict["numCornerVertices"] = num_corner_vertices
if num_cap_vertices is not None:
params_dict["numCapVertices"] = num_cap_vertices
if alignment is not None:
params_dict["alignment"] = alignment
if texture_mode is not None:
params_dict["textureMode"] = texture_mode
if generate_lighting_data is not None:
params_dict["generateLightingData"] = generate_lighting_data
if sorting_order is not None:
params_dict["sortingOrder"] = sorting_order
if sorting_layer_name is not None:
params_dict["sortingLayerName"] = sorting_layer_name
if sorting_layer_id is not None:
params_dict["sortingLayerID"] = sorting_layer_id
if render_mode is not None:
params_dict["renderMode"] = render_mode
if sort_mode is not None:
params_dict["sortMode"] = sort_mode
# Renderer common properties (shadows, lighting, probes)
if shadow_casting_mode is not None:
params_dict["shadowCastingMode"] = shadow_casting_mode
if receive_shadows is not None:
params_dict["receiveShadows"] = receive_shadows
if shadow_bias is not None:
params_dict["shadowBias"] = shadow_bias
if light_probe_usage is not None:
params_dict["lightProbeUsage"] = light_probe_usage
if reflection_probe_usage is not None:
params_dict["reflectionProbeUsage"] = reflection_probe_usage
if motion_vector_generation_mode is not None:
params_dict["motionVectorGenerationMode"] = motion_vector_generation_mode
if rendering_layer_mask is not None:
params_dict["renderingLayerMask"] = rendering_layer_mask
# Particle renderer specific
if min_particle_size is not None:
params_dict["minParticleSize"] = min_particle_size
if max_particle_size is not None:
params_dict["maxParticleSize"] = max_particle_size
if length_scale is not None:
params_dict["lengthScale"] = length_scale
if velocity_scale is not None:
params_dict["velocityScale"] = velocity_scale
if camera_velocity_scale is not None:
params_dict["cameraVelocityScale"] = camera_velocity_scale
if normal_direction is not None:
params_dict["normalDirection"] = normal_direction
if pivot is not None:
params_dict["pivot"] = pivot
if flip is not None:
params_dict["flip"] = flip
if allow_roll is not None:
params_dict["allowRoll"] = allow_roll
# Shape creation
if start is not None:
params_dict["start"] = start
if end is not None:
params_dict["end"] = end
if center is not None:
params_dict["center"] = center
if segments is not None:
params_dict["segments"] = segments
if normal is not None:
params_dict["normal"] = normal
if start_angle is not None:
params_dict["startAngle"] = start_angle
if end_angle is not None:
params_dict["endAngle"] = end_angle
if control_point1 is not None:
params_dict["controlPoint1"] = control_point1
if control_point2 is not None:
params_dict["controlPoint2"] = control_point2
# Trail specific
if min_vertex_distance is not None:
params_dict["minVertexDistance"] = min_vertex_distance
if autodestruct is not None:
params_dict["autodestruct"] = autodestruct
if emitting is not None:
params_dict["emitting"] = emitting
# Velocity/size axes
if x is not None:
params_dict["x"] = x
if y is not None:
params_dict["y"] = y
if z is not None:
params_dict["z"] = z
if speed_modifier is not None:
params_dict["speedModifier"] = speed_modifier
if space is not None:
params_dict["space"] = space
if separate_axes is not None:
params_dict["separateAxes"] = separate_axes
if size_over_lifetime is not None:
params_dict["size"] = size_over_lifetime
if size_x is not None:
params_dict["sizeX"] = size_x
if size_y is not None:
params_dict["sizeY"] = size_y
if size_z is not None:
params_dict["sizeZ"] = size_z
# Remove None values
params_dict = {k: v for k, v in params_dict.items() if v is not None} params_dict = {k: v for k, v in params_dict.items() if v is not None}
# Send to Unity # Send to Unity