diff --git a/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs b/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs index 8cdba8f53..2065d1738 100644 --- a/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs +++ b/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; +using UnityEditor; namespace MCPForUnity.Editor.Helpers { @@ -14,6 +16,15 @@ internal enum PipelineKind Custom } + internal enum VFXComponentType + { + ParticleSystem, + LineRenderer, + TrailRenderer + } + + private static Dictionary s_DefaultVFXMaterials = new Dictionary(); + private static readonly string[] BuiltInLitShaders = { "Standard", "Legacy Shaders/Diffuse" }; private static readonly string[] BuiltInUnlitShaders = { "Unlit/Color", "Unlit/Texture" }; private static readonly string[] UrpLitShaders = { "Universal Render Pipeline/Lit", "Universal Render Pipeline/Simple Lit" }; @@ -192,5 +203,82 @@ private static void WarnIfPipelineMismatch(string shaderName, PipelineKind activ 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(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; + } } } diff --git a/MCPForUnity/Editor/Helpers/RendererHelpers.cs b/MCPForUnity/Editor/Helpers/RendererHelpers.cs index 83eab85af..07e39a548 100644 --- a/MCPForUnity/Editor/Helpers/RendererHelpers.cs +++ b/MCPForUnity/Editor/Helpers/RendererHelpers.cs @@ -12,6 +12,43 @@ namespace MCPForUnity.Editor.Helpers /// public static class RendererHelpers { + /// + /// Ensures a renderer has a material assigned. If not, auto-assigns a default material + /// based on the render pipeline and component type. + /// + /// The renderer to check + 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; + } + } + } /// /// Applies common Renderer properties (shadows, lighting, probes, sorting, rendering layer). @@ -127,12 +164,48 @@ public static void ApplyColorProperties(JObject @params, List changes, /// JSON parameters containing materialPath /// Name for the undo operation /// Function to find material by path - public static object SetRendererMaterial(Renderer renderer, JObject @params, string undoName, Func findMaterial) + /// If true, auto-assigns default material when materialPath is not provided + public static object SetRendererMaterial(Renderer renderer, JObject @params, string undoName, Func findMaterial, bool autoAssignDefault = true) { if (renderer == null) return new { success = false, message = "Renderer not found" }; 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); if (mat == null) return new { success = false, message = $"Material not found: {path}" }; diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs index 1b7028168..8f631979e 100644 --- a/MCPForUnity/Editor/Tools/ManageScript.cs +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -130,7 +130,7 @@ public static object HandleCommand(JObject @params) } // Extract parameters - string action = @params["action"]?.ToString()?.ToLower(); + string action = @params["action"]?.ToString()?.ToLowerInvariant(); string name = @params["name"]?.ToString(); string path = @params["path"]?.ToString(); // Relative to Assets/ string contents = null; diff --git a/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs b/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs index da04509a9..fcaf55b52 100644 --- a/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs +++ b/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; +using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Tools.Vfx { @@ -18,6 +19,29 @@ public static object CreateLine(JObject @params) lr.positionCount = 2; lr.SetPosition(0, start); lr.SetPosition(1, end); + + RendererHelpers.EnsureMaterial(lr); + + // Apply optional width + if (@params["width"] != null) + { + float w = @params["width"].ToObject(); + lr.startWidth = w; + lr.endWidth = w; + } + if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject(); + if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject(); + + // 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); return new { success = true, message = "Created line" }; @@ -49,6 +73,28 @@ public static object CreateCircle(JObject @params) lr.SetPosition(i, point); } + RendererHelpers.EnsureMaterial(lr); + + // Apply optional width + if (@params["width"] != null) + { + float w = @params["width"].ToObject(); + lr.startWidth = w; + lr.endWidth = w; + } + if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject(); + if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject(); + + // 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); return new { success = true, message = $"Created circle with {segments} segments" }; } @@ -82,6 +128,28 @@ public static object CreateArc(JObject @params) lr.SetPosition(i, point); } + RendererHelpers.EnsureMaterial(lr); + + // Apply optional width + if (@params["width"] != null) + { + float w = @params["width"].ToObject(); + lr.startWidth = w; + lr.endWidth = w; + } + if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject(); + if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject(); + + // 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); return new { success = true, message = $"Created arc with {segments} segments" }; } @@ -123,6 +191,28 @@ public static object CreateBezier(JObject @params) lr.SetPosition(i, point); } + RendererHelpers.EnsureMaterial(lr); + + // Apply optional width + if (@params["width"] != null) + { + float w = @params["width"].ToObject(); + lr.startWidth = w; + lr.endWidth = w; + } + if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject(); + if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject(); + + // 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); return new { success = true, message = $"Created {(isQuadratic ? "quadratic" : "cubic")} Bezier" }; } diff --git a/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs b/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs index c4f9aa493..339e9c8f0 100644 --- a/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs +++ b/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs @@ -13,6 +13,8 @@ public static object SetPositions(JObject @params) LineRenderer lr = LineRead.FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; + RendererHelpers.EnsureMaterial(lr); + JArray posArr = @params["positions"] as JArray; if (posArr == null) return new { success = false, message = "Positions array required" }; @@ -35,6 +37,8 @@ public static object AddPosition(JObject @params) LineRenderer lr = LineRead.FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; + RendererHelpers.EnsureMaterial(lr); + Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]); Undo.RecordObject(lr, "Add Line Position"); @@ -51,6 +55,8 @@ public static object SetPosition(JObject @params) LineRenderer lr = LineRead.FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; + RendererHelpers.EnsureMaterial(lr); + int index = @params["index"]?.ToObject() ?? -1; if (index < 0 || index >= lr.positionCount) return new { success = false, message = $"Invalid index {index}" }; @@ -68,6 +74,8 @@ public static object SetWidth(JObject @params) LineRenderer lr = LineRead.FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; + RendererHelpers.EnsureMaterial(lr); + Undo.RecordObject(lr, "Set Line Width"); var changes = new List(); @@ -85,6 +93,8 @@ public static object SetColor(JObject @params) LineRenderer lr = LineRead.FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; + RendererHelpers.EnsureMaterial(lr); + Undo.RecordObject(lr, "Set Line Color"); var changes = new List(); @@ -108,9 +118,49 @@ public static object SetProperties(JObject @params) LineRenderer lr = LineRead.FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; + RendererHelpers.EnsureMaterial(lr); + Undo.RecordObject(lr, "Set Line Properties"); var changes = new List(); + // 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(); + lr.positionCount = count; + changes.Add("positionCount"); + } + RendererHelpers.ApplyLineTrailProperties(@params, changes, v => lr.loop = v, v => lr.useWorldSpace = v, v => lr.numCornerVertices = v, v => lr.numCapVertices = v, diff --git a/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs b/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs index 2aa428a03..fc5217c88 100644 --- a/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs +++ b/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs @@ -20,14 +20,257 @@ namespace MCPForUnity.Editor.Tools.Vfx /// - Visual Effect Graph (modern GPU particles, currently only support HDRP, other SRPs may not work) /// - LineRenderer (lines, bezier curves, shapes) /// - 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. /// [McpForUnityTool("manage_vfx", AutoRegister = false)] public static class ManageVFX { + private static readonly Dictionary ParamAliases = new Dictionary(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) { - string action = @params["action"]?.ToString(); + JObject normalizedParams = NormalizeParams(@params); + string action = normalizedParams["action"]?.ToString(); if (string.IsNullOrEmpty(action)) { return new { success = false, message = "Action is required" }; @@ -46,25 +289,25 @@ public static object HandleCommand(JObject @params) // ParticleSystem actions (particle_*) if (actionLower.StartsWith("particle_")) { - return HandleParticleSystemAction(@params, actionLower.Substring(9)); + return HandleParticleSystemAction(normalizedParams, actionLower.Substring(9)); } // VFX Graph actions (vfx_*) if (actionLower.StartsWith("vfx_")) { - return HandleVFXGraphAction(@params, actionLower.Substring(4)); + return HandleVFXGraphAction(normalizedParams, actionLower.Substring(4)); } // LineRenderer actions (line_*) if (actionLower.StartsWith("line_")) { - return HandleLineRendererAction(@params, actionLower.Substring(5)); + return HandleLineRendererAction(normalizedParams, actionLower.Substring(5)); } // TrailRenderer actions (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_" }; diff --git a/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs b/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs index 583fefc15..4fb8584f2 100644 --- a/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs +++ b/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; +using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Tools.Vfx { @@ -42,6 +43,16 @@ public static object Control(JObject @params, string action) ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); 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(); + if (renderer != null) + { + RendererHelpers.EnsureMaterial(renderer); + } + } + bool withChildren = @params["withChildren"]?.ToObject() ?? true; switch (action) @@ -62,6 +73,13 @@ public static object AddBurst(JObject @params) ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; + // Ensure material is assigned + var renderer = ps.GetComponent(); + if (renderer != null) + { + RendererHelpers.EnsureMaterial(renderer); + } + Undo.RecordObject(ps, "Add Burst"); var emission = ps.emission; diff --git a/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs b/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs index 19629c870..21c0384fa 100644 --- a/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs +++ b/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs @@ -14,6 +14,21 @@ public static object SetMain(JObject @params) ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; + // Ensure material is assigned before any configuration + var renderer = ps.GetComponent(); + 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"); var main = ps.main; var changes = new List(); @@ -34,6 +49,14 @@ public static object SetMain(JObject @params) if (@params["maxParticles"] != null) { main.maxParticles = @params["maxParticles"].ToObject(); changes.Add("maxParticles"); } 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)}" }; } @@ -42,6 +65,13 @@ public static object SetEmission(JObject @params) ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; + // Ensure material is assigned + var renderer = ps.GetComponent(); + if (renderer != null) + { + RendererHelpers.EnsureMaterial(renderer); + } + Undo.RecordObject(ps, "Set ParticleSystem Emission"); var emission = ps.emission; var changes = new List(); @@ -59,6 +89,13 @@ public static object SetShape(JObject @params) ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; + // Ensure material is assigned + var renderer = ps.GetComponent(); + if (renderer != null) + { + RendererHelpers.EnsureMaterial(renderer); + } + Undo.RecordObject(ps, "Set ParticleSystem Shape"); var shape = ps.shape; var changes = new List(); @@ -82,6 +119,13 @@ public static object SetColorOverLifetime(JObject @params) ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; + // Ensure material is assigned + var renderer = ps.GetComponent(); + if (renderer != null) + { + RendererHelpers.EnsureMaterial(renderer); + } + Undo.RecordObject(ps, "Set ParticleSystem Color Over Lifetime"); var col = ps.colorOverLifetime; var changes = new List(); @@ -98,6 +142,13 @@ public static object SetSizeOverLifetime(JObject @params) ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; + // Ensure material is assigned + var renderer = ps.GetComponent(); + if (renderer != null) + { + RendererHelpers.EnsureMaterial(renderer); + } + Undo.RecordObject(ps, "Set ParticleSystem Size Over Lifetime"); var sol = ps.sizeOverLifetime; var changes = new List(); @@ -130,6 +181,13 @@ public static object SetVelocityOverLifetime(JObject @params) ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; + // Ensure material is assigned + var renderer = ps.GetComponent(); + if (renderer != null) + { + RendererHelpers.EnsureMaterial(renderer); + } + Undo.RecordObject(ps, "Set ParticleSystem Velocity Over Lifetime"); var vol = ps.velocityOverLifetime; var changes = new List(); @@ -150,6 +208,13 @@ public static object SetNoise(JObject @params) ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; + // Ensure material is assigned + var renderer = ps.GetComponent(); + if (renderer != null) + { + RendererHelpers.EnsureMaterial(renderer); + } + Undo.RecordObject(ps, "Set ParticleSystem Noise"); var noise = ps.noise; var changes = new List(); @@ -174,6 +239,9 @@ public static object SetRenderer(JObject @params) var renderer = ps.GetComponent(); 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"); var changes = new List(); @@ -199,10 +267,20 @@ public static object SetRenderer(JObject @params) 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; - 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) { var findInst = new JObject { ["find"] = @params["trailMaterialPath"].ToString() }; diff --git a/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs b/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs index 18db2ffd8..ad6acc6e3 100644 --- a/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs +++ b/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; +using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Tools.Vfx { @@ -21,6 +22,8 @@ public static object Emit(JObject @params) TrailRenderer tr = TrailRead.FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + RendererHelpers.EnsureMaterial(tr); + #if UNITY_2021_1_OR_NEWER Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]); tr.AddPosition(pos); diff --git a/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs b/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs index 06e1a1113..fa11bbff2 100644 --- a/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs +++ b/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs @@ -14,6 +14,8 @@ public static object SetTime(JObject @params) TrailRenderer tr = TrailRead.FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + RendererHelpers.EnsureMaterial(tr); + float time = @params["time"]?.ToObject() ?? 5f; Undo.RecordObject(tr, "Set Trail Time"); @@ -28,6 +30,8 @@ public static object SetWidth(JObject @params) TrailRenderer tr = TrailRead.FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + RendererHelpers.EnsureMaterial(tr); + Undo.RecordObject(tr, "Set Trail Width"); var changes = new List(); @@ -45,6 +49,8 @@ public static object SetColor(JObject @params) TrailRenderer tr = TrailRead.FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + RendererHelpers.EnsureMaterial(tr); + Undo.RecordObject(tr, "Set Trail Color"); var changes = new List(); @@ -68,9 +74,43 @@ public static object SetProperties(JObject @params) TrailRenderer tr = TrailRead.FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + RendererHelpers.EnsureMaterial(tr); + Undo.RecordObject(tr, "Set Trail Properties"); var changes = new List(); + // 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(); 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(); + tr.startWidth = w; + tr.endWidth = w; + changes.Add("width"); + } + if (@params["startWidth"] != null) { tr.startWidth = @params["startWidth"].ToObject(); changes.Add("startWidth"); } + if (@params["endWidth"] != null) { tr.endWidth = @params["endWidth"].ToObject(); changes.Add("endWidth"); } + } + if (@params["minVertexDistance"] != null) { tr.minVertexDistance = @params["minVertexDistance"].ToObject(); changes.Add("minVertexDistance"); } if (@params["autodestruct"] != null) { tr.autodestruct = @params["autodestruct"].ToObject(); changes.Add("autodestruct"); } if (@params["emitting"] != null) { tr.emitting = @params["emitting"].ToObject(); changes.Add("emitting"); } diff --git a/Server/src/cli/commands/vfx.py b/Server/src/cli/commands/vfx.py index a03233080..3001da13e 100644 --- a/Server/src/cli/commands/vfx.py +++ b/Server/src/cli/commands/vfx.py @@ -10,6 +10,27 @@ 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() def vfx(): """VFX operations - particle systems, line renderers, trails.""" @@ -43,7 +64,7 @@ def particle_info(target: str, search_method: Optional[str]): params["searchMethod"] = search_method 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)) except UnityConnectionError as 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 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)) if result.get("success"): 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 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)) if result.get("success"): print_success(f"Stopped particle system: {target}") @@ -113,7 +134,7 @@ def particle_pause(target: str, search_method: Optional[str]): params["searchMethod"] = search_method 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)) except UnityConnectionError as 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 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)) except UnityConnectionError as 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 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)) except UnityConnectionError as e: print_error(str(e)) @@ -188,7 +209,7 @@ def line_info(target: str, search_method: Optional[str]): params["searchMethod"] = search_method 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)) except UnityConnectionError as 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 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)) except UnityConnectionError as 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 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)) except UnityConnectionError as 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 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)) except UnityConnectionError as e: print_error(str(e)) @@ -304,7 +325,7 @@ def line_clear(target: str, search_method: Optional[str]): params["searchMethod"] = search_method 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)) except UnityConnectionError as e: print_error(str(e)) @@ -332,7 +353,7 @@ def trail_info(target: str, search_method: Optional[str]): params["searchMethod"] = search_method 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)) except UnityConnectionError as 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 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)) except UnityConnectionError as e: print_error(str(e)) @@ -378,7 +399,7 @@ def trail_clear(target: str, search_method: Optional[str]): params["searchMethod"] = search_method 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)) except UnityConnectionError as 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 request_params.update(extra_params) 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)) except UnityConnectionError as e: print_error(str(e)) diff --git a/Server/src/services/tools/manage_texture.py b/Server/src/services/tools/manage_texture.py index 44306bcbc..fd423c5bf 100644 --- a/Server/src/services/tools/manage_texture.py +++ b/Server/src/services/tools/manage_texture.py @@ -454,7 +454,6 @@ def _normalize_import_settings(value: Any) -> tuple[dict | None, str | None]: description=( "Procedural texture generation for Unity. Creates textures with solid fills, " "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" ), annotations=ToolAnnotations( diff --git a/Server/src/services/tools/manage_vfx.py b/Server/src/services/tools/manage_vfx.py index a584946d3..709122bd3 100644 --- a/Server/src/services/tools/manage_vfx.py +++ b/Server/src/services/tools/manage_vfx.py @@ -39,86 +39,15 @@ "trail_set_material", "trail_set_properties", "trail_clear", "trail_emit" ] -ALL_ACTIONS = ["ping"] + PARTICLE_ACTIONS + \ - VFX_ACTIONS + LINE_ACTIONS + TRAIL_ACTIONS +ALL_ACTIONS = ["ping"] + PARTICLE_ACTIONS + VFX_ACTIONS + LINE_ACTIONS + TRAIL_ACTIONS @mcp_for_unity_tool( - description="""Unified VFX management for Unity visual effects components. - -Each action prefix requires a specific component on the target GameObject: -- `particle_*` actions require **ParticleSystem** component -- `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+)""", + description=( + "Manage Unity VFX components (ParticleSystem, VisualEffect, LineRenderer, TrailRenderer). " + "Action prefixes: particle_*, vfx_*, line_*, trail_*. " + "Action-specific parameters go in `properties` (keys match ManageVFX.cs)." + ), annotations=ToolAnnotations( title="Manage VFX", destructiveHint=True, @@ -126,283 +55,16 @@ ) async def manage_vfx( ctx: Context, - action: Annotated[str, "Action to perform. Use prefix: particle_, vfx_, line_, or trail_"], - - # 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, + action: Annotated[str, "Action to perform (prefix: particle_, vfx_, line_, trail_)."], + target: Annotated[str | None, "Target GameObject (name/path/id)."] = None, search_method: Annotated[ 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, - - # === 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]: """Unified VFX management tool.""" @@ -437,294 +99,14 @@ async def manage_vfx( 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} - - # Target + if properties is not None: + params_dict["properties"] = properties if target is not None: params_dict["target"] = target if search_method is not None: params_dict["searchMethod"] = search_method - # === PARTICLE SYSTEM === - # Pass through all values - C# side handles parsing (ParseColor, ParseVector3, ParseMinMaxCurve, ToObject) - 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} # Send to Unity