using System; using System.Collections.Generic; using Unity.Pipeline.Commands; using Unity.Pipeline.Editor.Authoring; using Unity.Pipeline.Models; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; namespace Unity.Pipeline.Editor.Commands.Materials { /// /// Shader discovery / introspection commands (CLI-213): list_shaders so an agent can pick a /// valid shader name, and get_shader_properties so it knows a shader's settable property /// names / types / ranges / texture dimensions / flags before authoring a material. /// /// Property introspection uses instance APIs exclusively: /// GetPropertyCount, GetPropertyName, GetPropertyType, /// GetPropertyDescription, GetPropertyRangeLimits, /// GetPropertyTextureDimension, and GetPropertyFlags. The reported type set is /// : Color, Vector, Float, Range, Texture, Int. /// public static class ShaderCommands { [CliCommand("list_shaders", "Discover available shaders so an agent can pick a valid name for set_material_properties / create_asset. Returns [{ name, assetPath|null, isBuiltin, isSupported }].")] public static List ListShaders( [CliArg("filter", "Case-insensitive substring matched against the shader name (e.g. \"URP\", \"Lit\").")] string filter = null, [CliArg("includeBuiltin", "Include built-in/engine shaders (those with no project asset path). Default true.")] bool includeBuiltin = true, [CliArg("limit", "Maximum number of shaders to return (default 200).")] int limit = 200) { if (limit <= 0) limit = 200; var results = new List(); var seen = new HashSet(StringComparer.Ordinal); // AssetDatabase.FindAssets("t:Shader") enumerates project + package shaders (each with an // asset path); ShaderUtil.GetAllShaderInfo() additionally surfaces built-in engine shaders // that have no asset path (e.g. "Standard", "Sprites/Default"). Merge both, dedup by name. foreach (var guid in AssetDatabase.FindAssets("t:Shader")) { var path = AssetDatabase.GUIDToAssetPath(guid); if (string.IsNullOrEmpty(path)) continue; var shader = AssetDatabase.LoadAssetAtPath(path); if (shader == null) continue; if (!seen.Add(shader.name)) continue; if (!PassesFilter(shader.name, filter)) continue; results.Add(new ShaderInfo { Name = shader.name, AssetPath = path, IsBuiltin = false, IsSupported = shader.isSupported, }); if (results.Count >= limit) return results; } if (includeBuiltin) { // GetAllShaderInfo() enumerates every shader the editor knows about, including built-in // engine shaders that have no project/package asset path. Any name NOT already added by // the FindAssets("t:Shader") pass above is a built-in (assetPath null). `info.supported` // already reflects Shader::IsSupported(), so we don't re-run Shader.Find per entry. foreach (var info in ShaderUtil.GetAllShaderInfo()) { if (!seen.Add(info.name)) continue; if (!PassesFilter(info.name, filter)) continue; results.Add(new ShaderInfo { Name = info.name, AssetPath = null, IsBuiltin = true, IsSupported = info.supported, }); if (results.Count >= limit) break; } } return results; } [CliCommand("get_shader_properties", "Introspect a shader's declared property list (name, description, type Color|Vector|Float|Range|TexEnv|Int, range, textureDimension, flags). Provide 'shader' (by name) OR 'material' (read the shader off that material).")] public static ShaderPropertiesResult GetShaderProperties( [CliArg("shader", "Shader name (e.g. \"Universal Render Pipeline/Lit\"). Provide this OR 'material'.")] string shader = null, [CliArg("material", "Reference to a material to read the shader from instead of naming it. Provide this OR 'shader'.")] ObjectRef material = null) { Shader resolved; if (!string.IsNullOrWhiteSpace(shader)) { resolved = Shader.Find(shader); if (resolved == null) throw new ShaderNotFoundException( $"Shader '{shader}' not found. Shader.Find only resolves shaders that are compiled/included in the build (or referenced by a material/scene). Use list_shaders to discover valid names."); } else if (material != null && !material.IsEmpty) { var mat = MaterialCommands.ResolveMaterial(material, out _); resolved = mat.shader; if (resolved == null) throw new ArgumentException($"Material '{material}' has no shader assigned."); } else { throw new ArgumentException("Provide either 'shader' (a shader name) or 'material' (a material reference)."); } var result = new ShaderPropertiesResult { Shader = resolved.name }; int count = resolved.GetPropertyCount(); for (int i = 0; i < count; i++) { var propType = resolved.GetPropertyType(i); var entry = new ShaderPropertyInfo { Name = resolved.GetPropertyName(i), Description = resolved.GetPropertyDescription(i), Type = MaterialValueConverter.TypeLabel(propType), Flags = DescribeFlags(resolved.GetPropertyFlags(i)), }; if (propType == ShaderPropertyType.Range) { var limits = resolved.GetPropertyRangeLimits(i); entry.Range = new MaterialRange { Min = limits.x, Max = limits.y, }; } if (propType == ShaderPropertyType.Texture) entry.TextureDimension = resolved.GetPropertyTextureDimension(i).ToString(); result.Properties.Add(entry); } return result; } private static bool PassesFilter(string name, string filter) { if (string.IsNullOrWhiteSpace(filter)) return true; return name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0; } /// /// Decompose a bitmask into its individual flag names (the /// enum is a [Flags] type). None is reported as an empty list rather than ["None"]. /// private static List DescribeFlags(ShaderPropertyFlags flags) { var list = new List(); if (flags == ShaderPropertyFlags.None) return list; foreach (ShaderPropertyFlags flag in Enum.GetValues(typeof(ShaderPropertyFlags))) { if (flag == ShaderPropertyFlags.None) continue; if ((flags & flag) == flag) list.Add(flag.ToString()); } return list; } } }