using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using MCPForUnity.Editor.Helpers; using UnityEngine; using UnityEditor; #if UNITY_VFX_GRAPH //Please enable the symbol in the project settings for VisualEffectGraph to work using UnityEngine.VFX; #endif namespace MCPForUnity.Editor.Tools { /// /// Tool for managing Unity VFX components: /// - ParticleSystem (legacy particle effects) /// - 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! /// [McpForUnityTool("manage_vfx", AutoRegister = false)] public static class ManageVFX { public static object HandleCommand(JObject @params) { string action = @params["action"]?.ToString(); if (string.IsNullOrEmpty(action)) { return new { success = false, message = "Action is required" }; } try { string actionLower = action.ToLowerInvariant(); // Route to appropriate handler based on action prefix if (actionLower == "ping") { return new { success = true, tool = "manage_vfx", components = new[] { "ParticleSystem", "VisualEffect", "LineRenderer", "TrailRenderer" } }; } // ParticleSystem actions (particle_*) if (actionLower.StartsWith("particle_")) { return HandleParticleSystemAction(@params, actionLower.Substring(9)); } // VFX Graph actions (vfx_*) if (actionLower.StartsWith("vfx_")) { return HandleVFXGraphAction(@params, actionLower.Substring(4)); } // LineRenderer actions (line_*) if (actionLower.StartsWith("line_")) { return HandleLineRendererAction(@params, actionLower.Substring(5)); } // TrailRenderer actions (trail_*) if (actionLower.StartsWith("trail_")) { return HandleTrailRendererAction(@params, actionLower.Substring(6)); } return new { success = false, message = $"Unknown action: {action}. Actions must be prefixed with: particle_, vfx_, line_, or trail_" }; } catch (Exception ex) { return new { success = false, message = ex.Message, stackTrace = ex.StackTrace }; } } #region Common Helpers // Parsing delegates for use with RendererHelpers private static Color ParseColor(JToken token) => VectorParsing.ParseColorOrDefault(token); private static Vector3 ParseVector3(JToken token) => VectorParsing.ParseVector3OrDefault(token); private static Vector4 ParseVector4(JToken token) => VectorParsing.ParseVector4OrDefault(token); private static Gradient ParseGradient(JToken token) => VectorParsing.ParseGradientOrDefault(token); private static AnimationCurve ParseAnimationCurve(JToken token, float defaultValue = 1f) => VectorParsing.ParseAnimationCurveOrDefault(token, defaultValue); // Object resolution - delegates to ObjectResolver private static GameObject FindTargetGameObject(JObject @params) => ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString()); private static Material FindMaterialByPath(string path) => ObjectResolver.ResolveMaterial(path); #endregion // ==================== PARTICLE SYSTEM ==================== #region ParticleSystem private static object HandleParticleSystemAction(JObject @params, string action) { switch (action) { case "get_info": return ParticleGetInfo(@params); case "set_main": return ParticleSetMain(@params); case "set_emission": return ParticleSetEmission(@params); case "set_shape": return ParticleSetShape(@params); case "set_color_over_lifetime": return ParticleSetColorOverLifetime(@params); case "set_size_over_lifetime": return ParticleSetSizeOverLifetime(@params); case "set_velocity_over_lifetime": return ParticleSetVelocityOverLifetime(@params); case "set_noise": return ParticleSetNoise(@params); case "set_renderer": return ParticleSetRenderer(@params); case "enable_module": return ParticleEnableModule(@params); case "play": return ParticleControl(@params, "play"); case "stop": return ParticleControl(@params, "stop"); case "pause": return ParticleControl(@params, "pause"); case "restart": return ParticleControl(@params, "restart"); case "clear": return ParticleControl(@params, "clear"); case "add_burst": return ParticleAddBurst(@params); case "clear_bursts": return ParticleClearBursts(@params); default: return new { success = false, message = $"Unknown particle action: {action}. Valid: get_info, set_main, set_emission, set_shape, set_color_over_lifetime, set_size_over_lifetime, set_velocity_over_lifetime, set_noise, set_renderer, enable_module, play, stop, pause, restart, clear, add_burst, clear_bursts" }; } } private static ParticleSystem FindParticleSystem(JObject @params) { GameObject go = FindTargetGameObject(@params); return go?.GetComponent(); } private static ParticleSystem.MinMaxCurve ParseMinMaxCurve(JToken token, float defaultValue = 1f) { if (token == null) return new ParticleSystem.MinMaxCurve(defaultValue); if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer) { return new ParticleSystem.MinMaxCurve(token.ToObject()); } if (token is JObject obj) { string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "constant"; switch (mode) { case "constant": float constant = obj["value"]?.ToObject() ?? defaultValue; return new ParticleSystem.MinMaxCurve(constant); case "random_between_constants": case "two_constants": float min = obj["min"]?.ToObject() ?? 0f; float max = obj["max"]?.ToObject() ?? 1f; return new ParticleSystem.MinMaxCurve(min, max); case "curve": AnimationCurve curve = ParseAnimationCurve(obj, defaultValue); return new ParticleSystem.MinMaxCurve(obj["multiplier"]?.ToObject() ?? 1f, curve); default: return new ParticleSystem.MinMaxCurve(defaultValue); } } return new ParticleSystem.MinMaxCurve(defaultValue); } private static ParticleSystem.MinMaxGradient ParseMinMaxGradient(JToken token) { if (token == null) return new ParticleSystem.MinMaxGradient(Color.white); if (token is JArray arr && arr.Count >= 3) { return new ParticleSystem.MinMaxGradient(ParseColor(arr)); } if (token is JObject obj) { string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "color"; switch (mode) { case "color": return new ParticleSystem.MinMaxGradient(ParseColor(obj["color"])); case "two_colors": Color colorMin = ParseColor(obj["colorMin"]); Color colorMax = ParseColor(obj["colorMax"]); return new ParticleSystem.MinMaxGradient(colorMin, colorMax); case "gradient": return new ParticleSystem.MinMaxGradient(ParseGradient(obj)); default: return new ParticleSystem.MinMaxGradient(Color.white); } } return new ParticleSystem.MinMaxGradient(Color.white); } private static object ParticleGetInfo(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) { return new { success = false, message = "ParticleSystem not found" }; } var main = ps.main; var emission = ps.emission; var shape = ps.shape; var renderer = ps.GetComponent(); return new { success = true, data = new { gameObject = ps.gameObject.name, isPlaying = ps.isPlaying, isPaused = ps.isPaused, particleCount = ps.particleCount, main = new { duration = main.duration, looping = main.loop, startLifetime = main.startLifetime.constant, startSpeed = main.startSpeed.constant, startSize = main.startSize.constant, gravityModifier = main.gravityModifier.constant, simulationSpace = main.simulationSpace.ToString(), maxParticles = main.maxParticles }, emission = new { enabled = emission.enabled, rateOverTime = emission.rateOverTime.constant, burstCount = emission.burstCount }, shape = new { enabled = shape.enabled, shapeType = shape.shapeType.ToString(), radius = shape.radius, angle = shape.angle }, renderer = renderer != null ? new { renderMode = renderer.renderMode.ToString(), sortMode = renderer.sortMode.ToString(), material = renderer.sharedMaterial?.name, trailMaterial = renderer.trailMaterial?.name, minParticleSize = renderer.minParticleSize, maxParticleSize = renderer.maxParticleSize, // Shadows & lighting shadowCastingMode = renderer.shadowCastingMode.ToString(), receiveShadows = renderer.receiveShadows, lightProbeUsage = renderer.lightProbeUsage.ToString(), reflectionProbeUsage = renderer.reflectionProbeUsage.ToString(), // Sorting sortingOrder = renderer.sortingOrder, sortingLayerName = renderer.sortingLayerName, renderingLayerMask = renderer.renderingLayerMask } : null } }; } private static object ParticleSetMain(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; Undo.RecordObject(ps, "Set ParticleSystem Main"); var main = ps.main; var changes = new List(); if (@params["duration"] != null) { main.duration = @params["duration"].ToObject(); changes.Add("duration"); } if (@params["looping"] != null) { main.loop = @params["looping"].ToObject(); changes.Add("looping"); } if (@params["prewarm"] != null) { main.prewarm = @params["prewarm"].ToObject(); changes.Add("prewarm"); } if (@params["startDelay"] != null) { main.startDelay = ParseMinMaxCurve(@params["startDelay"], 0f); changes.Add("startDelay"); } if (@params["startLifetime"] != null) { main.startLifetime = ParseMinMaxCurve(@params["startLifetime"], 5f); changes.Add("startLifetime"); } if (@params["startSpeed"] != null) { main.startSpeed = ParseMinMaxCurve(@params["startSpeed"], 5f); changes.Add("startSpeed"); } if (@params["startSize"] != null) { main.startSize = ParseMinMaxCurve(@params["startSize"], 1f); changes.Add("startSize"); } if (@params["startRotation"] != null) { main.startRotation = ParseMinMaxCurve(@params["startRotation"], 0f); changes.Add("startRotation"); } if (@params["startColor"] != null) { main.startColor = ParseMinMaxGradient(@params["startColor"]); changes.Add("startColor"); } if (@params["gravityModifier"] != null) { main.gravityModifier = ParseMinMaxCurve(@params["gravityModifier"], 0f); changes.Add("gravityModifier"); } if (@params["simulationSpace"] != null && Enum.TryParse(@params["simulationSpace"].ToString(), true, out var simSpace)) { main.simulationSpace = simSpace; changes.Add("simulationSpace"); } if (@params["scalingMode"] != null && Enum.TryParse(@params["scalingMode"].ToString(), true, out var scaleMode)) { main.scalingMode = scaleMode; changes.Add("scalingMode"); } if (@params["playOnAwake"] != null) { main.playOnAwake = @params["playOnAwake"].ToObject(); changes.Add("playOnAwake"); } if (@params["maxParticles"] != null) { main.maxParticles = @params["maxParticles"].ToObject(); changes.Add("maxParticles"); } EditorUtility.SetDirty(ps); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object ParticleSetEmission(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; Undo.RecordObject(ps, "Set ParticleSystem Emission"); var emission = ps.emission; var changes = new List(); if (@params["enabled"] != null) { emission.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } if (@params["rateOverTime"] != null) { emission.rateOverTime = ParseMinMaxCurve(@params["rateOverTime"], 10f); changes.Add("rateOverTime"); } if (@params["rateOverDistance"] != null) { emission.rateOverDistance = ParseMinMaxCurve(@params["rateOverDistance"], 0f); changes.Add("rateOverDistance"); } EditorUtility.SetDirty(ps); return new { success = true, message = $"Updated emission: {string.Join(", ", changes)}" }; } private static object ParticleSetShape(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; Undo.RecordObject(ps, "Set ParticleSystem Shape"); var shape = ps.shape; var changes = new List(); if (@params["enabled"] != null) { shape.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } if (@params["shapeType"] != null && Enum.TryParse(@params["shapeType"].ToString(), true, out var shapeType)) { shape.shapeType = shapeType; changes.Add("shapeType"); } if (@params["radius"] != null) { shape.radius = @params["radius"].ToObject(); changes.Add("radius"); } if (@params["radiusThickness"] != null) { shape.radiusThickness = @params["radiusThickness"].ToObject(); changes.Add("radiusThickness"); } if (@params["angle"] != null) { shape.angle = @params["angle"].ToObject(); changes.Add("angle"); } if (@params["arc"] != null) { shape.arc = @params["arc"].ToObject(); changes.Add("arc"); } if (@params["position"] != null) { shape.position = ParseVector3(@params["position"]); changes.Add("position"); } if (@params["rotation"] != null) { shape.rotation = ParseVector3(@params["rotation"]); changes.Add("rotation"); } if (@params["scale"] != null) { shape.scale = ParseVector3(@params["scale"]); changes.Add("scale"); } EditorUtility.SetDirty(ps); return new { success = true, message = $"Updated shape: {string.Join(", ", changes)}" }; } private static object ParticleSetColorOverLifetime(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; Undo.RecordObject(ps, "Set ParticleSystem Color Over Lifetime"); var col = ps.colorOverLifetime; var changes = new List(); if (@params["enabled"] != null) { col.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } if (@params["color"] != null) { col.color = ParseMinMaxGradient(@params["color"]); changes.Add("color"); } EditorUtility.SetDirty(ps); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object ParticleSetSizeOverLifetime(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; Undo.RecordObject(ps, "Set ParticleSystem Size Over Lifetime"); var sol = ps.sizeOverLifetime; var changes = new List(); // Auto-enable module if size properties are being set (unless explicitly disabled) bool hasSizeProperty = @params["size"] != null || @params["sizeX"] != null || @params["sizeY"] != null || @params["sizeZ"] != null; if (hasSizeProperty && @params["enabled"] == null && !sol.enabled) { sol.enabled = true; changes.Add("enabled"); } else if (@params["enabled"] != null) { sol.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } if (@params["separateAxes"] != null) { sol.separateAxes = @params["separateAxes"].ToObject(); changes.Add("separateAxes"); } if (@params["size"] != null) { sol.size = ParseMinMaxCurve(@params["size"], 1f); changes.Add("size"); } if (@params["sizeX"] != null) { sol.x = ParseMinMaxCurve(@params["sizeX"], 1f); changes.Add("sizeX"); } if (@params["sizeY"] != null) { sol.y = ParseMinMaxCurve(@params["sizeY"], 1f); changes.Add("sizeY"); } if (@params["sizeZ"] != null) { sol.z = ParseMinMaxCurve(@params["sizeZ"], 1f); changes.Add("sizeZ"); } EditorUtility.SetDirty(ps); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object ParticleSetVelocityOverLifetime(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; Undo.RecordObject(ps, "Set ParticleSystem Velocity Over Lifetime"); var vol = ps.velocityOverLifetime; var changes = new List(); if (@params["enabled"] != null) { vol.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } if (@params["space"] != null && Enum.TryParse(@params["space"].ToString(), true, out var space)) { vol.space = space; changes.Add("space"); } if (@params["x"] != null) { vol.x = ParseMinMaxCurve(@params["x"], 0f); changes.Add("x"); } if (@params["y"] != null) { vol.y = ParseMinMaxCurve(@params["y"], 0f); changes.Add("y"); } if (@params["z"] != null) { vol.z = ParseMinMaxCurve(@params["z"], 0f); changes.Add("z"); } if (@params["speedModifier"] != null) { vol.speedModifier = ParseMinMaxCurve(@params["speedModifier"], 1f); changes.Add("speedModifier"); } EditorUtility.SetDirty(ps); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object ParticleSetNoise(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; Undo.RecordObject(ps, "Set ParticleSystem Noise"); var noise = ps.noise; var changes = new List(); if (@params["enabled"] != null) { noise.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } if (@params["strength"] != null) { noise.strength = ParseMinMaxCurve(@params["strength"], 1f); changes.Add("strength"); } if (@params["frequency"] != null) { noise.frequency = @params["frequency"].ToObject(); changes.Add("frequency"); } if (@params["scrollSpeed"] != null) { noise.scrollSpeed = ParseMinMaxCurve(@params["scrollSpeed"], 0f); changes.Add("scrollSpeed"); } if (@params["damping"] != null) { noise.damping = @params["damping"].ToObject(); changes.Add("damping"); } if (@params["octaveCount"] != null) { noise.octaveCount = @params["octaveCount"].ToObject(); changes.Add("octaveCount"); } if (@params["quality"] != null && Enum.TryParse(@params["quality"].ToString(), true, out var quality)) { noise.quality = quality; changes.Add("quality"); } EditorUtility.SetDirty(ps); return new { success = true, message = $"Updated noise: {string.Join(", ", changes)}" }; } private static object ParticleSetRenderer(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; var renderer = ps.GetComponent(); if (renderer == null) return new { success = false, message = "ParticleSystemRenderer not found" }; Undo.RecordObject(renderer, "Set ParticleSystem Renderer"); var changes = new List(); // ParticleSystem-specific render modes if (@params["renderMode"] != null && Enum.TryParse(@params["renderMode"].ToString(), true, out var renderMode)) { renderer.renderMode = renderMode; changes.Add("renderMode"); } if (@params["sortMode"] != null && Enum.TryParse(@params["sortMode"].ToString(), true, out var sortMode)) { renderer.sortMode = sortMode; changes.Add("sortMode"); } // Particle size limits if (@params["minParticleSize"] != null) { renderer.minParticleSize = @params["minParticleSize"].ToObject(); changes.Add("minParticleSize"); } if (@params["maxParticleSize"] != null) { renderer.maxParticleSize = @params["maxParticleSize"].ToObject(); changes.Add("maxParticleSize"); } // Stretched billboard settings if (@params["lengthScale"] != null) { renderer.lengthScale = @params["lengthScale"].ToObject(); changes.Add("lengthScale"); } if (@params["velocityScale"] != null) { renderer.velocityScale = @params["velocityScale"].ToObject(); changes.Add("velocityScale"); } if (@params["cameraVelocityScale"] != null) { renderer.cameraVelocityScale = @params["cameraVelocityScale"].ToObject(); changes.Add("cameraVelocityScale"); } if (@params["normalDirection"] != null) { renderer.normalDirection = @params["normalDirection"].ToObject(); changes.Add("normalDirection"); } // Alignment and pivot if (@params["alignment"] != null && Enum.TryParse(@params["alignment"].ToString(), true, out var alignment)) { renderer.alignment = alignment; changes.Add("alignment"); } if (@params["pivot"] != null) { renderer.pivot = ParseVector3(@params["pivot"]); changes.Add("pivot"); } if (@params["flip"] != null) { renderer.flip = ParseVector3(@params["flip"]); changes.Add("flip"); } if (@params["allowRoll"] != null) { renderer.allowRoll = @params["allowRoll"].ToObject(); changes.Add("allowRoll"); } //special case for particle system renderer if (@params["shadowBias"] != null) { renderer.shadowBias = @params["shadowBias"].ToObject(); changes.Add("shadowBias"); } // Common Renderer properties (shadows, lighting, probes, sorting) RendererHelpers.ApplyCommonRendererProperties(renderer, @params, changes); // Material if (@params["materialPath"] != null) { var findInst = new JObject { ["find"] = @params["materialPath"].ToString() }; Material mat = ManageGameObject.FindObjectByInstruction(findInst, typeof(Material)) as Material; if (mat != null) { renderer.sharedMaterial = mat; changes.Add("material"); } } if (@params["trailMaterialPath"] != null) { var findInst = new JObject { ["find"] = @params["trailMaterialPath"].ToString() }; Material mat = ManageGameObject.FindObjectByInstruction(findInst, typeof(Material)) as Material; if (mat != null) { renderer.trailMaterial = mat; changes.Add("trailMaterial"); } } EditorUtility.SetDirty(renderer); return new { success = true, message = $"Updated renderer: {string.Join(", ", changes)}" }; } private static object ParticleEnableModule(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; string moduleName = @params["module"]?.ToString()?.ToLowerInvariant(); bool enabled = @params["enabled"]?.ToObject() ?? true; if (string.IsNullOrEmpty(moduleName)) return new { success = false, message = "Module name required" }; Undo.RecordObject(ps, $"Toggle {moduleName}"); switch (moduleName.Replace("_", "")) { case "emission": var em = ps.emission; em.enabled = enabled; break; case "shape": var sh = ps.shape; sh.enabled = enabled; break; case "coloroverlifetime": var col = ps.colorOverLifetime; col.enabled = enabled; break; case "sizeoverlifetime": var sol = ps.sizeOverLifetime; sol.enabled = enabled; break; case "velocityoverlifetime": var vol = ps.velocityOverLifetime; vol.enabled = enabled; break; case "noise": var n = ps.noise; n.enabled = enabled; break; case "collision": var coll = ps.collision; coll.enabled = enabled; break; case "trails": var tr = ps.trails; tr.enabled = enabled; break; case "lights": var li = ps.lights; li.enabled = enabled; break; default: return new { success = false, message = $"Unknown module: {moduleName}" }; } EditorUtility.SetDirty(ps); return new { success = true, message = $"Module '{moduleName}' {(enabled ? "enabled" : "disabled")}" }; } private static object ParticleControl(JObject @params, string action) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; bool withChildren = @params["withChildren"]?.ToObject() ?? true; switch (action) { case "play": ps.Play(withChildren); break; case "stop": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmitting); break; case "pause": ps.Pause(withChildren); break; case "restart": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmittingAndClear); ps.Play(withChildren); break; case "clear": ps.Clear(withChildren); break; } return new { success = true, message = $"ParticleSystem {action}" }; } private static object ParticleAddBurst(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; Undo.RecordObject(ps, "Add Burst"); var emission = ps.emission; float time = @params["time"]?.ToObject() ?? 0f; short minCount = (short)(@params["minCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30); short maxCount = (short)(@params["maxCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30); int cycles = @params["cycles"]?.ToObject() ?? 1; float interval = @params["interval"]?.ToObject() ?? 0.01f; var burst = new ParticleSystem.Burst(time, minCount, maxCount, cycles, interval); burst.probability = @params["probability"]?.ToObject() ?? 1f; int idx = emission.burstCount; var bursts = new ParticleSystem.Burst[idx + 1]; emission.GetBursts(bursts); bursts[idx] = burst; emission.SetBursts(bursts); EditorUtility.SetDirty(ps); return new { success = true, message = $"Added burst at t={time}", burstIndex = idx }; } private static object ParticleClearBursts(JObject @params) { ParticleSystem ps = FindParticleSystem(@params); if (ps == null) return new { success = false, message = "ParticleSystem not found" }; Undo.RecordObject(ps, "Clear Bursts"); var emission = ps.emission; int count = emission.burstCount; emission.SetBursts(new ParticleSystem.Burst[0]); EditorUtility.SetDirty(ps); return new { success = true, message = $"Cleared {count} bursts" }; } #endregion // ==================== VFX GRAPH ==================== #region VFX Graph private static object HandleVFXGraphAction(JObject @params, string action) { #if !UNITY_VFX_GRAPH return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; #else switch (action) { // Asset management case "create_asset": return VFXCreateAsset(@params); case "assign_asset": return VFXAssignAsset(@params); case "list_templates": return VFXListTemplates(@params); case "list_assets": return VFXListAssets(@params); // Runtime parameter control case "get_info": return VFXGetInfo(@params); case "set_float": return VFXSetParameter(@params, (vfx, n, v) => vfx.SetFloat(n, v)); case "set_int": return VFXSetParameter(@params, (vfx, n, v) => vfx.SetInt(n, v)); case "set_bool": return VFXSetParameter(@params, (vfx, n, v) => vfx.SetBool(n, v)); case "set_vector2": return VFXSetVector(@params, 2); case "set_vector3": return VFXSetVector(@params, 3); case "set_vector4": return VFXSetVector(@params, 4); case "set_color": return VFXSetColor(@params); case "set_gradient": return VFXSetGradient(@params); case "set_texture": return VFXSetTexture(@params); case "set_mesh": return VFXSetMesh(@params); case "set_curve": return VFXSetCurve(@params); case "send_event": return VFXSendEvent(@params); case "play": return VFXControl(@params, "play"); case "stop": return VFXControl(@params, "stop"); case "pause": return VFXControl(@params, "pause"); case "reinit": return VFXControl(@params, "reinit"); case "set_playback_speed": return VFXSetPlaybackSpeed(@params); case "set_seed": return VFXSetSeed(@params); default: return new { success = false, message = $"Unknown vfx action: {action}. Valid: create_asset, assign_asset, list_templates, list_assets, get_info, set_float, set_int, set_bool, set_vector2/3/4, set_color, set_gradient, set_texture, set_mesh, set_curve, send_event, play, stop, pause, reinit, set_playback_speed, set_seed" }; } #endif } #if UNITY_VFX_GRAPH private static VisualEffect FindVisualEffect(JObject @params) { GameObject go = FindTargetGameObject(@params); return go?.GetComponent(); } /// /// Creates a new VFX Graph asset file from a template /// private static object VFXCreateAsset(JObject @params) { string assetName = @params["assetName"]?.ToString(); string folderPath = @params["folderPath"]?.ToString() ?? "Assets/VFX"; string template = @params["template"]?.ToString() ?? "empty"; if (string.IsNullOrEmpty(assetName)) return new { success = false, message = "assetName is required" }; // Ensure folder exists if (!AssetDatabase.IsValidFolder(folderPath)) { string[] folders = folderPath.Split('/'); string currentPath = folders[0]; for (int i = 1; i < folders.Length; i++) { string newPath = currentPath + "/" + folders[i]; if (!AssetDatabase.IsValidFolder(newPath)) { AssetDatabase.CreateFolder(currentPath, folders[i]); } currentPath = newPath; } } string assetPath = $"{folderPath}/{assetName}.vfx"; // Check if asset already exists if (AssetDatabase.LoadAssetAtPath(assetPath) != null) { bool overwrite = @params["overwrite"]?.ToObject() ?? false; if (!overwrite) return new { success = false, message = $"Asset already exists at {assetPath}. Set overwrite=true to replace." }; AssetDatabase.DeleteAsset(assetPath); } // Find and copy template string templatePath = FindVFXTemplate(template); UnityEngine.VFX.VisualEffectAsset newAsset = null; if (!string.IsNullOrEmpty(templatePath) && System.IO.File.Exists(templatePath)) { // templatePath is a full filesystem path, need to copy file directly // Get the full destination path string projectRoot = System.IO.Path.GetDirectoryName(Application.dataPath); string fullDestPath = System.IO.Path.Combine(projectRoot, assetPath); // Ensure directory exists string destDir = System.IO.Path.GetDirectoryName(fullDestPath); if (!System.IO.Directory.Exists(destDir)) System.IO.Directory.CreateDirectory(destDir); // Copy the file System.IO.File.Copy(templatePath, fullDestPath, true); AssetDatabase.Refresh(); newAsset = AssetDatabase.LoadAssetAtPath(assetPath); } else { // Create empty VFX asset using reflection to access internal API // Note: Develop in Progress, TODO:// Find authenticated way to create VFX asset try { // Try to use VisualEffectAssetEditorUtility.CreateNewAsset if available var utilityType = System.Type.GetType("UnityEditor.VFX.VisualEffectAssetEditorUtility, Unity.VisualEffectGraph.Editor"); if (utilityType != null) { var createMethod = utilityType.GetMethod("CreateNewAsset", BindingFlags.Public | BindingFlags.Static); if (createMethod != null) { createMethod.Invoke(null, new object[] { assetPath }); AssetDatabase.Refresh(); newAsset = AssetDatabase.LoadAssetAtPath(assetPath); } } // Fallback: Create a ScriptableObject-based asset if (newAsset == null) { // Try direct creation via internal constructor var resourceType = System.Type.GetType("UnityEditor.VFX.VisualEffectResource, Unity.VisualEffectGraph.Editor"); if (resourceType != null) { var createMethod = resourceType.GetMethod("CreateNewAsset", BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic); if (createMethod != null) { var resource = createMethod.Invoke(null, new object[] { assetPath }); AssetDatabase.Refresh(); newAsset = AssetDatabase.LoadAssetAtPath(assetPath); } } } } catch (Exception ex) { return new { success = false, message = $"Failed to create VFX asset: {ex.Message}" }; } } if (newAsset == null) { return new { success = false, message = "Failed to create VFX asset. Try using a template from list_templates." }; } return new { success = true, message = $"Created VFX asset: {assetPath}", data = new { assetPath = assetPath, assetName = newAsset.name, template = template } }; } /// /// Finds VFX template path by name /// private static string FindVFXTemplate(string templateName) { // Get the actual filesystem path for the VFX Graph package using PackageManager API var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); var searchPaths = new List(); if (packageInfo != null) { // Use the resolved path from PackageManager (handles Library/PackageCache paths) searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates")); searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples")); } // Also search project-local paths searchPaths.Add("Assets/VFX/Templates"); string[] templatePatterns = new[] { $"{templateName}.vfx", $"VFX{templateName}.vfx", $"Simple{templateName}.vfx", $"{templateName}VFX.vfx" }; foreach (string basePath in searchPaths) { if (!System.IO.Directory.Exists(basePath)) continue; foreach (string pattern in templatePatterns) { string[] files = System.IO.Directory.GetFiles(basePath, pattern, System.IO.SearchOption.AllDirectories); if (files.Length > 0) return files[0]; } // Also search by partial match try { string[] allVfxFiles = System.IO.Directory.GetFiles(basePath, "*.vfx", System.IO.SearchOption.AllDirectories); foreach (string file in allVfxFiles) { if (System.IO.Path.GetFileNameWithoutExtension(file).ToLower().Contains(templateName.ToLower())) return file; } } catch { } } // Search in project assets string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset " + templateName); if (guids.Length > 0) { return AssetDatabase.GUIDToAssetPath(guids[0]); } return null; } /// /// Assigns a VFX asset to a VisualEffect component /// private static object VFXAssignAsset(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect component not found" }; string assetPath = @params["assetPath"]?.ToString(); if (string.IsNullOrEmpty(assetPath)) return new { success = false, message = "assetPath is required" }; // Normalize path if (!assetPath.StartsWith("Assets/") && !assetPath.StartsWith("Packages/")) assetPath = "Assets/" + assetPath; if (!assetPath.EndsWith(".vfx")) assetPath += ".vfx"; var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (asset == null) { // Try searching by name string searchName = System.IO.Path.GetFileNameWithoutExtension(assetPath); string[] guids = AssetDatabase.FindAssets($"t:VisualEffectAsset {searchName}"); if (guids.Length > 0) { assetPath = AssetDatabase.GUIDToAssetPath(guids[0]); asset = AssetDatabase.LoadAssetAtPath(assetPath); } } if (asset == null) return new { success = false, message = $"VFX asset not found: {assetPath}" }; Undo.RecordObject(vfx, "Assign VFX Asset"); vfx.visualEffectAsset = asset; EditorUtility.SetDirty(vfx); return new { success = true, message = $"Assigned VFX asset '{asset.name}' to {vfx.gameObject.name}", data = new { gameObject = vfx.gameObject.name, assetName = asset.name, assetPath = assetPath } }; } /// /// Lists available VFX templates /// private static object VFXListTemplates(JObject @params) { var templates = new List(); // Get the actual filesystem path for the VFX Graph package using PackageManager API var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); var searchPaths = new List(); if (packageInfo != null) { // Use the resolved path from PackageManager (handles Library/PackageCache paths) searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates")); searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples")); } // Also search project-local paths searchPaths.Add("Assets/VFX/Templates"); searchPaths.Add("Assets/VFX"); // Precompute normalized package path for comparison string normalizedPackagePath = null; if (packageInfo != null) { normalizedPackagePath = packageInfo.resolvedPath.Replace("\\", "/"); } // Precompute the Assets base path for converting absolute paths to project-relative string assetsBasePath = Application.dataPath.Replace("\\", "/"); foreach (string basePath in searchPaths) { if (!System.IO.Directory.Exists(basePath)) continue; try { string[] vfxFiles = System.IO.Directory.GetFiles(basePath, "*.vfx", System.IO.SearchOption.AllDirectories); foreach (string file in vfxFiles) { string absolutePath = file.Replace("\\", "/"); string name = System.IO.Path.GetFileNameWithoutExtension(file); bool isPackage = normalizedPackagePath != null && absolutePath.StartsWith(normalizedPackagePath); // Convert absolute path to project-relative path string projectRelativePath; if (isPackage) { // For package paths, convert to Packages/... format projectRelativePath = "Packages/" + packageInfo.name + absolutePath.Substring(normalizedPackagePath.Length); } else if (absolutePath.StartsWith(assetsBasePath)) { // For project assets, convert to Assets/... format projectRelativePath = "Assets" + absolutePath.Substring(assetsBasePath.Length); } else { // Fallback: use the absolute path if we can't determine the relative path projectRelativePath = absolutePath; } templates.Add(new { name = name, path = projectRelativePath, source = isPackage ? "package" : "project" }); } } catch { } } // Also search project assets string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset"); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); if (!templates.Any(t => ((dynamic)t).path == path)) { string name = System.IO.Path.GetFileNameWithoutExtension(path); templates.Add(new { name = name, path = path, source = "project" }); } } return new { success = true, data = new { count = templates.Count, templates = templates } }; } /// /// Lists all VFX assets in the project /// private static object VFXListAssets(JObject @params) { string searchFolder = @params["folder"]?.ToString(); string searchPattern = @params["search"]?.ToString(); string filter = "t:VisualEffectAsset"; if (!string.IsNullOrEmpty(searchPattern)) filter += " " + searchPattern; string[] guids; if (!string.IsNullOrEmpty(searchFolder)) guids = AssetDatabase.FindAssets(filter, new[] { searchFolder }); else guids = AssetDatabase.FindAssets(filter); var assets = new List(); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null) { assets.Add(new { name = asset.name, path = path, guid = guid }); } } return new { success = true, data = new { count = assets.Count, assets = assets } }; } private static object VFXGetInfo(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; return new { success = true, data = new { gameObject = vfx.gameObject.name, assetName = vfx.visualEffectAsset?.name ?? "None", aliveParticleCount = vfx.aliveParticleCount, culled = vfx.culled, pause = vfx.pause, playRate = vfx.playRate, startSeed = vfx.startSeed } }; } private static object VFXSetParameter(JObject @params, Action setter) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; string param = @params["parameter"]?.ToString(); if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; JToken valueToken = @params["value"]; if (valueToken == null) return new { success = false, message = "Value required" }; Undo.RecordObject(vfx, $"Set VFX {param}"); T value = valueToken.ToObject(); setter(vfx, param, value); EditorUtility.SetDirty(vfx); return new { success = true, message = $"Set {param} = {value}" }; } private static object VFXSetVector(JObject @params, int dims) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; string param = @params["parameter"]?.ToString(); if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; Vector4 vec = ParseVector4(@params["value"]); Undo.RecordObject(vfx, $"Set VFX {param}"); switch (dims) { case 2: vfx.SetVector2(param, new Vector2(vec.x, vec.y)); break; case 3: vfx.SetVector3(param, new Vector3(vec.x, vec.y, vec.z)); break; case 4: vfx.SetVector4(param, vec); break; } EditorUtility.SetDirty(vfx); return new { success = true, message = $"Set {param}" }; } private static object VFXSetColor(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; string param = @params["parameter"]?.ToString(); if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; Color color = ParseColor(@params["value"]); Undo.RecordObject(vfx, $"Set VFX Color {param}"); vfx.SetVector4(param, new Vector4(color.r, color.g, color.b, color.a)); EditorUtility.SetDirty(vfx); return new { success = true, message = $"Set color {param}" }; } private static object VFXSetGradient(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; string param = @params["parameter"]?.ToString(); if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; Gradient gradient = ParseGradient(@params["gradient"]); Undo.RecordObject(vfx, $"Set VFX Gradient {param}"); vfx.SetGradient(param, gradient); EditorUtility.SetDirty(vfx); return new { success = true, message = $"Set gradient {param}" }; } private static object VFXSetTexture(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; string param = @params["parameter"]?.ToString(); string path = @params["texturePath"]?.ToString(); if (string.IsNullOrEmpty(param) || string.IsNullOrEmpty(path)) return new { success = false, message = "Parameter and texturePath required" }; var findInst = new JObject { ["find"] = path }; Texture tex = ManageGameObject.FindObjectByInstruction(findInst, typeof(Texture)) as Texture; if (tex == null) return new { success = false, message = $"Texture not found: {path}" }; Undo.RecordObject(vfx, $"Set VFX Texture {param}"); vfx.SetTexture(param, tex); EditorUtility.SetDirty(vfx); return new { success = true, message = $"Set texture {param} = {tex.name}" }; } private static object VFXSetMesh(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; string param = @params["parameter"]?.ToString(); string path = @params["meshPath"]?.ToString(); if (string.IsNullOrEmpty(param) || string.IsNullOrEmpty(path)) return new { success = false, message = "Parameter and meshPath required" }; var findInst = new JObject { ["find"] = path }; Mesh mesh = ManageGameObject.FindObjectByInstruction(findInst, typeof(Mesh)) as Mesh; if (mesh == null) return new { success = false, message = $"Mesh not found: {path}" }; Undo.RecordObject(vfx, $"Set VFX Mesh {param}"); vfx.SetMesh(param, mesh); EditorUtility.SetDirty(vfx); return new { success = true, message = $"Set mesh {param} = {mesh.name}" }; } private static object VFXSetCurve(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; string param = @params["parameter"]?.ToString(); if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; AnimationCurve curve = ParseAnimationCurve(@params["curve"], 1f); Undo.RecordObject(vfx, $"Set VFX Curve {param}"); vfx.SetAnimationCurve(param, curve); EditorUtility.SetDirty(vfx); return new { success = true, message = $"Set curve {param}" }; } private static object VFXSendEvent(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; string eventName = @params["eventName"]?.ToString(); if (string.IsNullOrEmpty(eventName)) return new { success = false, message = "Event name required" }; VFXEventAttribute attr = vfx.CreateVFXEventAttribute(); if (@params["position"] != null) attr.SetVector3("position", ParseVector3(@params["position"])); if (@params["velocity"] != null) attr.SetVector3("velocity", ParseVector3(@params["velocity"])); if (@params["color"] != null) { var c = ParseColor(@params["color"]); attr.SetVector3("color", new Vector3(c.r, c.g, c.b)); } if (@params["size"] != null) attr.SetFloat("size", @params["size"].ToObject()); if (@params["lifetime"] != null) attr.SetFloat("lifetime", @params["lifetime"].ToObject()); vfx.SendEvent(eventName, attr); return new { success = true, message = $"Sent event '{eventName}'" }; } private static object VFXControl(JObject @params, string action) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; switch (action) { case "play": vfx.Play(); break; case "stop": vfx.Stop(); break; case "pause": vfx.pause = !vfx.pause; break; case "reinit": vfx.Reinit(); break; } return new { success = true, message = $"VFX {action}", isPaused = vfx.pause }; } private static object VFXSetPlaybackSpeed(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; float rate = @params["playRate"]?.ToObject() ?? 1f; Undo.RecordObject(vfx, "Set VFX Play Rate"); vfx.playRate = rate; EditorUtility.SetDirty(vfx); return new { success = true, message = $"Set play rate = {rate}" }; } private static object VFXSetSeed(JObject @params) { VisualEffect vfx = FindVisualEffect(@params); if (vfx == null) return new { success = false, message = "VisualEffect not found" }; uint seed = @params["seed"]?.ToObject() ?? 0; bool resetOnPlay = @params["resetSeedOnPlay"]?.ToObject() ?? true; Undo.RecordObject(vfx, "Set VFX Seed"); vfx.startSeed = seed; vfx.resetSeedOnPlay = resetOnPlay; EditorUtility.SetDirty(vfx); return new { success = true, message = $"Set seed = {seed}" }; } #endif #endregion // ==================== LINE RENDERER ==================== #region LineRenderer private static object HandleLineRendererAction(JObject @params, string action) { switch (action) { case "get_info": return LineGetInfo(@params); case "set_positions": return LineSetPositions(@params); case "add_position": return LineAddPosition(@params); case "set_position": return LineSetPosition(@params); case "set_width": return LineSetWidth(@params); case "set_color": return LineSetColor(@params); case "set_material": return LineSetMaterial(@params); case "set_properties": return LineSetProperties(@params); case "clear": return LineClear(@params); case "create_line": return LineCreateLine(@params); case "create_circle": return LineCreateCircle(@params); case "create_arc": return LineCreateArc(@params); case "create_bezier": return LineCreateBezier(@params); default: return new { success = false, message = $"Unknown line action: {action}. Valid: get_info, set_positions, add_position, set_position, set_width, set_color, set_material, set_properties, clear, create_line, create_circle, create_arc, create_bezier" }; } } private static LineRenderer FindLineRenderer(JObject @params) { GameObject go = FindTargetGameObject(@params); return go?.GetComponent(); } private static object LineGetInfo(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; var positions = new Vector3[lr.positionCount]; lr.GetPositions(positions); return new { success = true, data = new { gameObject = lr.gameObject.name, positionCount = lr.positionCount, positions = positions.Select(p => new { x = p.x, y = p.y, z = p.z }).ToArray(), startWidth = lr.startWidth, endWidth = lr.endWidth, loop = lr.loop, useWorldSpace = lr.useWorldSpace, alignment = lr.alignment.ToString(), textureMode = lr.textureMode.ToString(), numCornerVertices = lr.numCornerVertices, numCapVertices = lr.numCapVertices, generateLightingData = lr.generateLightingData, material = lr.sharedMaterial?.name, // Shadows & lighting shadowCastingMode = lr.shadowCastingMode.ToString(), receiveShadows = lr.receiveShadows, lightProbeUsage = lr.lightProbeUsage.ToString(), reflectionProbeUsage = lr.reflectionProbeUsage.ToString(), // Sorting sortingOrder = lr.sortingOrder, sortingLayerName = lr.sortingLayerName, renderingLayerMask = lr.renderingLayerMask } }; } private static object LineSetPositions(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; JArray posArr = @params["positions"] as JArray; if (posArr == null) return new { success = false, message = "Positions array required" }; var positions = posArr.Select(p => ParseVector3(p)).ToArray(); Undo.RecordObject(lr, "Set Line Positions"); lr.positionCount = positions.Length; lr.SetPositions(positions); EditorUtility.SetDirty(lr); return new { success = true, message = $"Set {positions.Length} positions" }; } private static object LineAddPosition(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; Vector3 pos = ParseVector3(@params["position"]); Undo.RecordObject(lr, "Add Line Position"); int idx = lr.positionCount; lr.positionCount = idx + 1; lr.SetPosition(idx, pos); EditorUtility.SetDirty(lr); return new { success = true, message = $"Added position at index {idx}", index = idx }; } private static object LineSetPosition(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; int index = @params["index"]?.ToObject() ?? -1; if (index < 0 || index >= lr.positionCount) return new { success = false, message = $"Invalid index {index}" }; Vector3 pos = ParseVector3(@params["position"]); Undo.RecordObject(lr, "Set Line Position"); lr.SetPosition(index, pos); EditorUtility.SetDirty(lr); return new { success = true, message = $"Set position at index {index}" }; } private static object LineSetWidth(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; Undo.RecordObject(lr, "Set Line Width"); var changes = new List(); RendererHelpers.ApplyWidthProperties(@params, changes, v => lr.startWidth = v, v => lr.endWidth = v, v => lr.widthCurve = v, v => lr.widthMultiplier = v, ParseAnimationCurve); EditorUtility.SetDirty(lr); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object LineSetColor(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; Undo.RecordObject(lr, "Set Line Color"); var changes = new List(); RendererHelpers.ApplyColorProperties(@params, changes, v => lr.startColor = v, v => lr.endColor = v, v => lr.colorGradient = v, ParseColor, ParseGradient, fadeEndAlpha: false); EditorUtility.SetDirty(lr); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object LineSetMaterial(JObject @params) { LineRenderer lr = FindLineRenderer(@params); return RendererHelpers.SetRendererMaterial(lr, @params, "Set Line Material", FindMaterialByPath); } private static object LineSetProperties(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; Undo.RecordObject(lr, "Set Line Properties"); var changes = new List(); // Line-specific properties RendererHelpers.ApplyLineTrailProperties(@params, changes, v => lr.loop = v, v => lr.useWorldSpace = v, v => lr.numCornerVertices = v, v => lr.numCapVertices = v, v => lr.alignment = v, v => lr.textureMode = v, v => lr.generateLightingData = v); // Common Renderer properties (shadows, lighting, probes, sorting) RendererHelpers.ApplyCommonRendererProperties(lr, @params, changes); EditorUtility.SetDirty(lr); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object LineClear(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; int count = lr.positionCount; Undo.RecordObject(lr, "Clear Line"); lr.positionCount = 0; EditorUtility.SetDirty(lr); return new { success = true, message = $"Cleared {count} positions" }; } private static object LineCreateLine(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; Vector3 start = ParseVector3(@params["start"]); Vector3 end = ParseVector3(@params["end"]); Undo.RecordObject(lr, "Create Line"); lr.positionCount = 2; lr.SetPosition(0, start); lr.SetPosition(1, end); EditorUtility.SetDirty(lr); return new { success = true, message = "Created line" }; } private static object LineCreateCircle(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; Vector3 center = ParseVector3(@params["center"]); float radius = @params["radius"]?.ToObject() ?? 1f; int segments = @params["segments"]?.ToObject() ?? 32; Vector3 normal = @params["normal"] != null ? ParseVector3(@params["normal"]).normalized : Vector3.up; Vector3 right = Vector3.Cross(normal, Vector3.forward); if (right.sqrMagnitude < 0.001f) right = Vector3.Cross(normal, Vector3.up); right = right.normalized; Vector3 forward = Vector3.Cross(right, normal).normalized; Undo.RecordObject(lr, "Create Circle"); lr.positionCount = segments; lr.loop = true; for (int i = 0; i < segments; i++) { float angle = (float)i / segments * Mathf.PI * 2f; Vector3 point = center + (right * Mathf.Cos(angle) + forward * Mathf.Sin(angle)) * radius; lr.SetPosition(i, point); } EditorUtility.SetDirty(lr); return new { success = true, message = $"Created circle with {segments} segments" }; } private static object LineCreateArc(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; Vector3 center = ParseVector3(@params["center"]); float radius = @params["radius"]?.ToObject() ?? 1f; float startAngle = (@params["startAngle"]?.ToObject() ?? 0f) * Mathf.Deg2Rad; float endAngle = (@params["endAngle"]?.ToObject() ?? 180f) * Mathf.Deg2Rad; int segments = @params["segments"]?.ToObject() ?? 16; Vector3 normal = @params["normal"] != null ? ParseVector3(@params["normal"]).normalized : Vector3.up; Vector3 right = Vector3.Cross(normal, Vector3.forward); if (right.sqrMagnitude < 0.001f) right = Vector3.Cross(normal, Vector3.up); right = right.normalized; Vector3 forward = Vector3.Cross(right, normal).normalized; Undo.RecordObject(lr, "Create Arc"); lr.positionCount = segments + 1; lr.loop = false; for (int i = 0; i <= segments; i++) { float t = (float)i / segments; float angle = Mathf.Lerp(startAngle, endAngle, t); Vector3 point = center + (right * Mathf.Cos(angle) + forward * Mathf.Sin(angle)) * radius; lr.SetPosition(i, point); } EditorUtility.SetDirty(lr); return new { success = true, message = $"Created arc with {segments} segments" }; } private static object LineCreateBezier(JObject @params) { LineRenderer lr = FindLineRenderer(@params); if (lr == null) return new { success = false, message = "LineRenderer not found" }; Vector3 start = ParseVector3(@params["start"]); Vector3 end = ParseVector3(@params["end"]); Vector3 cp1 = ParseVector3(@params["controlPoint1"] ?? @params["control1"]); Vector3 cp2 = @params["controlPoint2"] != null || @params["control2"] != null ? ParseVector3(@params["controlPoint2"] ?? @params["control2"]) : cp1; int segments = @params["segments"]?.ToObject() ?? 32; bool isQuadratic = @params["controlPoint2"] == null && @params["control2"] == null; Undo.RecordObject(lr, "Create Bezier"); lr.positionCount = segments + 1; lr.loop = false; for (int i = 0; i <= segments; i++) { float t = (float)i / segments; Vector3 point; if (isQuadratic) { float u = 1 - t; point = u * u * start + 2 * u * t * cp1 + t * t * end; } else { float u = 1 - t; point = u * u * u * start + 3 * u * u * t * cp1 + 3 * u * t * t * cp2 + t * t * t * end; } lr.SetPosition(i, point); } EditorUtility.SetDirty(lr); return new { success = true, message = $"Created {(isQuadratic ? "quadratic" : "cubic")} Bezier" }; } #endregion // ==================== TRAIL RENDERER ==================== #region TrailRenderer private static object HandleTrailRendererAction(JObject @params, string action) { switch (action) { case "get_info": return TrailGetInfo(@params); case "set_time": return TrailSetTime(@params); case "set_width": return TrailSetWidth(@params); case "set_color": return TrailSetColor(@params); case "set_material": return TrailSetMaterial(@params); case "set_properties": return TrailSetProperties(@params); case "clear": return TrailClear(@params); case "emit": return TrailEmit(@params); default: return new { success = false, message = $"Unknown trail action: {action}. Valid: get_info, set_time, set_width, set_color, set_material, set_properties, clear, emit" }; } } private static TrailRenderer FindTrailRenderer(JObject @params) { GameObject go = FindTargetGameObject(@params); return go?.GetComponent(); } private static object TrailGetInfo(JObject @params) { TrailRenderer tr = FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; return new { success = true, data = new { gameObject = tr.gameObject.name, time = tr.time, startWidth = tr.startWidth, endWidth = tr.endWidth, minVertexDistance = tr.minVertexDistance, emitting = tr.emitting, autodestruct = tr.autodestruct, positionCount = tr.positionCount, alignment = tr.alignment.ToString(), textureMode = tr.textureMode.ToString(), numCornerVertices = tr.numCornerVertices, numCapVertices = tr.numCapVertices, generateLightingData = tr.generateLightingData, material = tr.sharedMaterial?.name, // Shadows & lighting shadowCastingMode = tr.shadowCastingMode.ToString(), receiveShadows = tr.receiveShadows, lightProbeUsage = tr.lightProbeUsage.ToString(), reflectionProbeUsage = tr.reflectionProbeUsage.ToString(), // Sorting sortingOrder = tr.sortingOrder, sortingLayerName = tr.sortingLayerName, renderingLayerMask = tr.renderingLayerMask } }; } private static object TrailSetTime(JObject @params) { TrailRenderer tr = FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; float time = @params["time"]?.ToObject() ?? 5f; Undo.RecordObject(tr, "Set Trail Time"); tr.time = time; EditorUtility.SetDirty(tr); return new { success = true, message = $"Set trail time to {time}s" }; } private static object TrailSetWidth(JObject @params) { TrailRenderer tr = FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; Undo.RecordObject(tr, "Set Trail Width"); var changes = new List(); RendererHelpers.ApplyWidthProperties(@params, changes, v => tr.startWidth = v, v => tr.endWidth = v, v => tr.widthCurve = v, v => tr.widthMultiplier = v, ParseAnimationCurve); EditorUtility.SetDirty(tr); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object TrailSetColor(JObject @params) { TrailRenderer tr = FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; Undo.RecordObject(tr, "Set Trail Color"); var changes = new List(); RendererHelpers.ApplyColorProperties(@params, changes, v => tr.startColor = v, v => tr.endColor = v, v => tr.colorGradient = v, ParseColor, ParseGradient, fadeEndAlpha: true); EditorUtility.SetDirty(tr); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object TrailSetMaterial(JObject @params) { TrailRenderer tr = FindTrailRenderer(@params); return RendererHelpers.SetRendererMaterial(tr, @params, "Set Trail Material", FindMaterialByPath); } private static object TrailSetProperties(JObject @params) { TrailRenderer tr = FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; Undo.RecordObject(tr, "Set Trail Properties"); var changes = new List(); // Trail-specific properties (not shared with LineRenderer) 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"); } // Shared Line/Trail properties RendererHelpers.ApplyLineTrailProperties(@params, changes, null, null, // Trail doesn't have loop or useWorldSpace v => tr.numCornerVertices = v, v => tr.numCapVertices = v, v => tr.alignment = v, v => tr.textureMode = v, v => tr.generateLightingData = v); // Common Renderer properties (shadows, lighting, probes, sorting) RendererHelpers.ApplyCommonRendererProperties(tr, @params, changes); EditorUtility.SetDirty(tr); return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; } private static object TrailClear(JObject @params) { TrailRenderer tr = FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; Undo.RecordObject(tr, "Clear Trail"); tr.Clear(); return new { success = true, message = "Trail cleared" }; } private static object TrailEmit(JObject @params) { TrailRenderer tr = FindTrailRenderer(@params); if (tr == null) return new { success = false, message = "TrailRenderer not found" }; #if UNITY_2021_1_OR_NEWER Vector3 pos = ParseVector3(@params["position"]); tr.AddPosition(pos); return new { success = true, message = $"Emitted at ({pos.x}, {pos.y}, {pos.z})" }; #else return new { success = false, message = "AddPosition requires Unity 2021.1+" }; #endif } #endregion } }