using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; using UnityEngine; using UnityEditor; namespace MCPForUnity.Editor.Helpers { /// /// Utility class for common Renderer property operations. /// Used by ManageVFX for ParticleSystem, LineRenderer, and TrailRenderer components. /// public static class RendererHelpers { public readonly struct EnsureMaterialResult { public EnsureMaterialResult(bool materialReplaced, string replacementReason) { MaterialReplaced = materialReplaced; ReplacementReason = replacementReason ?? string.Empty; } public bool MaterialReplaced { get; } public string ReplacementReason { get; } } /// /// 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 EnsureMaterialResult EnsureMaterial(Renderer renderer) { if (renderer == null) { return new EnsureMaterialResult(false, "renderer_missing"); } var existingMaterial = renderer.sharedMaterial; string replacementReason = string.Empty; bool pipelineInvalid = RenderPipelineUtility.IsMaterialInvalidForActivePipeline(existingMaterial, out string pipelineReason); if (existingMaterial != null && !pipelineInvalid && IsUsableMaterial(existingMaterial)) { return new EnsureMaterialResult(false, string.Empty); } if (existingMaterial != null) { var shaderName = existingMaterial.shader != null ? existingMaterial.shader.name : "(null)"; McpLog.Warn($"[RendererHelpers] Replacing invalid VFX material '{existingMaterial.name}' (shader: {shaderName})."); replacementReason = !string.IsNullOrWhiteSpace(pipelineReason) ? pipelineReason : "invalid_material"; } else { replacementReason = "missing_material"; } Material replacement = ResolveReplacementMaterial(renderer); if (replacement != null) { Undo.RecordObject(renderer, "Assign default renderer material"); renderer.sharedMaterial = replacement; EditorUtility.SetDirty(renderer); return new EnsureMaterialResult(true, replacementReason); } return new EnsureMaterialResult(false, replacementReason); } private static bool IsUsableMaterial(Material material) { if (material == null) { return false; } var shader = material.shader; if (shader == null) { return false; } var shaderName = shader.name ?? string.Empty; if (shaderName.IndexOf("InternalErrorShader", StringComparison.OrdinalIgnoreCase) >= 0) { return false; } return shader.isSupported; } private static Material ResolveReplacementMaterial(Renderer renderer) { if (renderer is ParticleSystemRenderer) { return RenderPipelineUtility.GetOrCreateDefaultVFXMaterial(RenderPipelineUtility.VFXComponentType.ParticleSystem); } if (renderer is LineRenderer) { return RenderPipelineUtility.GetOrCreateDefaultVFXMaterial(RenderPipelineUtility.VFXComponentType.LineRenderer); } if (renderer is TrailRenderer) { return RenderPipelineUtility.GetOrCreateDefaultVFXMaterial(RenderPipelineUtility.VFXComponentType.TrailRenderer); } return RenderPipelineUtility.GetOrCreateDefaultSceneMaterial(); } /// /// Applies common Renderer properties (shadows, lighting, probes, sorting, rendering layer). /// Used by ParticleSetRenderer, LineSetProperties, TrailSetProperties. /// public static void ApplyCommonRendererProperties(Renderer renderer, JObject @params, List changes) { // Shadows if (@params["shadowCastingMode"] != null && Enum.TryParse(@params["shadowCastingMode"].ToString(), true, out var shadowMode)) { renderer.shadowCastingMode = shadowMode; changes.Add("shadowCastingMode"); } if (@params["receiveShadows"] != null) { renderer.receiveShadows = @params["receiveShadows"].ToObject(); changes.Add("receiveShadows"); } // Note: shadowBias is only available on specific renderer types (e.g., ParticleSystemRenderer), not base Renderer // Lighting and probes if (@params["lightProbeUsage"] != null && Enum.TryParse(@params["lightProbeUsage"].ToString(), true, out var probeUsage)) { renderer.lightProbeUsage = probeUsage; changes.Add("lightProbeUsage"); } if (@params["reflectionProbeUsage"] != null && Enum.TryParse(@params["reflectionProbeUsage"].ToString(), true, out var reflectionUsage)) { renderer.reflectionProbeUsage = reflectionUsage; changes.Add("reflectionProbeUsage"); } // Motion vectors if (@params["motionVectorGenerationMode"] != null && Enum.TryParse(@params["motionVectorGenerationMode"].ToString(), true, out var motionMode)) { renderer.motionVectorGenerationMode = motionMode; changes.Add("motionVectorGenerationMode"); } // Sorting if (@params["sortingOrder"] != null) { renderer.sortingOrder = @params["sortingOrder"].ToObject(); changes.Add("sortingOrder"); } if (@params["sortingLayerName"] != null) { renderer.sortingLayerName = @params["sortingLayerName"].ToString(); changes.Add("sortingLayerName"); } if (@params["sortingLayerID"] != null) { renderer.sortingLayerID = @params["sortingLayerID"].ToObject(); changes.Add("sortingLayerID"); } // Rendering layer mask (for SRP) if (@params["renderingLayerMask"] != null) { renderer.renderingLayerMask = @params["renderingLayerMask"].ToObject(); changes.Add("renderingLayerMask"); } } /// /// Gets common Renderer properties for GetInfo methods. /// public static object GetCommonRendererInfo(Renderer renderer) { return new { shadowCastingMode = renderer.shadowCastingMode.ToString(), receiveShadows = renderer.receiveShadows, lightProbeUsage = renderer.lightProbeUsage.ToString(), reflectionProbeUsage = renderer.reflectionProbeUsage.ToString(), sortingOrder = renderer.sortingOrder, sortingLayerName = renderer.sortingLayerName, renderingLayerMask = renderer.renderingLayerMask }; } /// /// Sets width properties for LineRenderer or TrailRenderer. /// /// JSON parameters containing width, startWidth, endWidth, widthCurve, widthMultiplier /// List to track changed properties /// Action to set start width /// Action to set end width /// Action to set width curve /// Action to set width multiplier /// Function to parse animation curve from JToken public static void ApplyWidthProperties(JObject @params, List changes, Action setStartWidth, Action setEndWidth, Action setWidthCurve, Action setWidthMultiplier, Func parseAnimationCurve) { if (@params["width"] != null) { float w = @params["width"].ToObject(); setStartWidth(w); setEndWidth(w); changes.Add("width"); } if (@params["startWidth"] != null) { setStartWidth(@params["startWidth"].ToObject()); changes.Add("startWidth"); } if (@params["endWidth"] != null) { setEndWidth(@params["endWidth"].ToObject()); changes.Add("endWidth"); } if (@params["widthCurve"] != null) { setWidthCurve(parseAnimationCurve(@params["widthCurve"], 1f)); changes.Add("widthCurve"); } if (@params["widthMultiplier"] != null) { setWidthMultiplier(@params["widthMultiplier"].ToObject()); changes.Add("widthMultiplier"); } } /// /// Sets color properties for LineRenderer or TrailRenderer. /// /// JSON parameters containing color, startColor, endColor, gradient /// List to track changed properties /// Action to set start color /// Action to set end color /// Action to set gradient /// Function to parse color from JToken /// Function to parse gradient from JToken /// If true, sets end color alpha to 0 when using single color public static void ApplyColorProperties(JObject @params, List changes, Action setStartColor, Action setEndColor, Action setGradient, Func parseColor, Func parseGradient, bool fadeEndAlpha = false) { if (@params["color"] != null) { Color c = parseColor(@params["color"]); setStartColor(c); setEndColor(fadeEndAlpha ? new Color(c.r, c.g, c.b, 0f) : c); changes.Add("color"); } if (@params["startColor"] != null) { setStartColor(parseColor(@params["startColor"])); changes.Add("startColor"); } if (@params["endColor"] != null) { setEndColor(parseColor(@params["endColor"])); changes.Add("endColor"); } if (@params["gradient"] != null) { setGradient(parseGradient(@params["gradient"])); changes.Add("gradient"); } } /// /// Sets material for a Renderer. /// /// The renderer to set material on /// JSON parameters containing materialPath /// Name for the undo operation /// Function to find material by path /// 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)) { 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}" }; Undo.RecordObject(renderer, undoName); renderer.sharedMaterial = mat; EditorUtility.SetDirty(renderer); return new { success = true, message = $"Set material to {mat.name}" }; } /// /// Applies Line/Trail specific properties (loop, alignment, textureMode, etc.). /// public static void ApplyLineTrailProperties(JObject @params, List changes, Action setLoop, Action setUseWorldSpace, Action setNumCornerVertices, Action setNumCapVertices, Action setAlignment, Action setTextureMode, Action setGenerateLightingData) { if (@params["loop"] != null && setLoop != null) { setLoop(@params["loop"].ToObject()); changes.Add("loop"); } if (@params["useWorldSpace"] != null && setUseWorldSpace != null) { setUseWorldSpace(@params["useWorldSpace"].ToObject()); changes.Add("useWorldSpace"); } if (@params["numCornerVertices"] != null && setNumCornerVertices != null) { setNumCornerVertices(@params["numCornerVertices"].ToObject()); changes.Add("numCornerVertices"); } if (@params["numCapVertices"] != null && setNumCapVertices != null) { setNumCapVertices(@params["numCapVertices"].ToObject()); changes.Add("numCapVertices"); } if (@params["alignment"] != null && setAlignment != null && Enum.TryParse(@params["alignment"].ToString(), true, out var align)) { setAlignment(align); changes.Add("alignment"); } if (@params["textureMode"] != null && setTextureMode != null && Enum.TryParse(@params["textureMode"].ToString(), true, out var texMode)) { setTextureMode(texMode); changes.Add("textureMode"); } if (@params["generateLightingData"] != null && setGenerateLightingData != null) { setGenerateLightingData(@params["generateLightingData"].ToObject()); changes.Add("generateLightingData"); } } /// /// Applies sensible default values to a newly-created ParticleSystem. /// Unity's raw defaults (startSize=1, startSpeed=5, startLifetime=5, maxParticles=1000) /// produce oversized particle clouds in most scenes. These gentler defaults are a better /// starting point that callers can override with particle_set_main or set_property. /// public static void SetSensibleParticleDefaults(ParticleSystem ps) { if (ps == null) return; var main = ps.main; main.startSize = new ParticleSystem.MinMaxCurve(0.1f); main.startSpeed = new ParticleSystem.MinMaxCurve(1f); main.startLifetime = new ParticleSystem.MinMaxCurve(2f); main.maxParticles = 100; main.playOnAwake = false; main.simulationSpace = ParticleSystemSimulationSpace.World; var emission = ps.emission; emission.rateOverTime = new ParticleSystem.MinMaxCurve(20f); var shape = ps.shape; shape.radius = 0.25f; } } }