Files

187 lines
8.0 KiB
C#

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
{
/// <summary>
/// Shader discovery / introspection commands (CLI-213): <c>list_shaders</c> so an agent can pick a
/// valid shader name, and <c>get_shader_properties</c> so it knows a shader's settable property
/// names / types / ranges / texture dimensions / flags before authoring a material.
///
/// Property introspection uses <see cref="Shader"/> instance APIs exclusively:
/// <c>GetPropertyCount</c>, <c>GetPropertyName</c>, <c>GetPropertyType</c>,
/// <c>GetPropertyDescription</c>, <c>GetPropertyRangeLimits</c>,
/// <c>GetPropertyTextureDimension</c>, and <c>GetPropertyFlags</c>. The reported type set is
/// <see cref="UnityEngine.Rendering.ShaderPropertyType"/>: Color, Vector, Float, Range, Texture, Int.
/// </summary>
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<ShaderInfo> 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<ShaderInfo>();
var seen = new HashSet<string>(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<Shader>(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;
}
/// <summary>
/// Decompose a <see cref="ShaderPropertyFlags"/> bitmask into its individual flag names (the
/// enum is a [Flags] type). <c>None</c> is reported as an empty list rather than ["None"].
/// </summary>
private static List<string> DescribeFlags(ShaderPropertyFlags flags)
{
var list = new List<string>();
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;
}
}
}