Tesearis.ShaderLabParser
1.0.0
dotnet add package Tesearis.ShaderLabParser --version 1.0.0
NuGet\Install-Package Tesearis.ShaderLabParser -Version 1.0.0
<PackageReference Include="Tesearis.ShaderLabParser" Version="1.0.0" />
<PackageVersion Include="Tesearis.ShaderLabParser" Version="1.0.0" />
<PackageReference Include="Tesearis.ShaderLabParser" />
paket add Tesearis.ShaderLabParser --version 1.0.0
#r "nuget: Tesearis.ShaderLabParser, 1.0.0"
#:package Tesearis.ShaderLabParser@1.0.0
#addin nuget:?package=Tesearis.ShaderLabParser&version=1.0.0
#tool nuget:?package=Tesearis.ShaderLabParser&version=1.0.0
Tesearis.ShaderLabParser
Standalone ShaderLab lexer, parser and syntax tree for .shader files.
Tesearis.ShaderLabParser turns ShaderLab source into an immutable, strongly-typed AST so you can inspect, tool, or lint .shader files.
Features
- Zero dependencies: no runtime dependencies at all.
- Full ShaderLab grammar: Properties, nesting, passes, tags, command blocks, program blocks...
- Forward-compatible: commands the parser doesn't specifically recognize still parse as generic so newer syntax won't break parsing.
- Program directive scanning: preprocessor directives are recognized without parsing the shader code itself.
- Conditional resolution:
#if/#ifdef/#ifndef/#elif/#elseresolve against a caller-supplied macro environment, kept up to date automatically as#define/#undefare encountered across program/include blocks. - Best-effort recovery: never throws on malformed input; genuine syntax errors become Error diagnostics while parsing continues.
- Targeted:
netstandard2.0(for use in the Unity Editor) andnet8.0.
What this library does not do
- It never reads files from disk.
- It never resolves
#include,Fallback,UsePass, orDependencytargets. - It doesn't tokenize or parse the CG/HLSL/GLSL code inside program blocks. Content is captured verbatim.
Usage
Parse a shader
using Tesearis.ShaderLabParser.Parsing;
var result = ShaderLab.Parse(shaderSource, fileName: "Toon.shader");
// Parsing never throws, even on malformed input — check diagnostics instead.
if (result.HasErrors)
{
foreach (var diagnostic in result.Diagnostics)
Console.WriteLine(diagnostic); // "Toon.shader(7,13): error SL0104: ..."
}
ShaderNode shader = result.Shader; // null only if the source has no `Shader { ... }` at all
Walk the tree
using Tesearis.ShaderLabParser.Syntax;
shader.Name; // "Custom/Toon"
foreach (var property in shader.Properties.Items)
Console.WriteLine($"{property.Name} : {property.TypeName} (\"{property.DisplayName}\")");
// AllSubShaders also flattens SubShaders nested inside legacy Category blocks.
foreach (var subShader in shader.AllSubShaders)
{
var renderType = subShader.Tags?.GetValue("RenderType"); // case-insensitive lookup
foreach (var pass in subShader.Passes.OfType<PassNode>())
Console.WriteLine($"Pass {pass.Name ?? "<unnamed>"}: {pass.ProgramBlocks.Count} program block(s)");
}
CommandNode covers render-state and other commands (Blend, ZWrite, Stencil, ...), recognized or not: command.NameIs("ZWrite"), command.GetArgumentText(0), command.IsRecognized. Every node also exposes Span, Children, DescendantsAndSelf(), and FindNodeAt(position) for offset-based lookups — handy for editor tooling.
Dump the tree
string tree = ShaderLabTreeDumper.Dump(shader, result.Source);
Console.WriteLine(tree);
Shader "Custom/Toon" @1:1
Properties (2) @3:5
Property _MainTex : 2D = "white" {} ("Texture") @5:9
Property _Color : Color = (1, 1, 1, 1) ("Tint") @6:9
SubShader (LOD -1, 1 passes) @8:5
Tags (1) @10:9
Tag "RenderType" = "Opaque" @10:16
Pass <unnamed> @11:9
ProgramBlock Hlsl/Program (116 chars) @13:13
#pragma vertex vert @14
#pragma fragment frag @15
#include "UnityCG.cginc" @16
Custom traversal
Subclass ShaderLabVisitor and override only what you need — call DefaultVisit to keep descending into a node's children:
sealed class PropertyNameCollector : ShaderLabVisitor
{
public List<string> Names { get; } = new();
public override void VisitProperty(PropertyNode node)
{
Names.Add(node.Name);
DefaultVisit(node);
}
}
var collector = new PropertyNameCollector();
collector.Visit(shader); // Names: ["_MainTex", "_Color"]
Program directives and conditional resolution
#pragma/#include directives inside every ProgramBlockNode are scanned automatically during Parse — no separate call needed:
foreach (var block in pass.ProgramBlocks)
{
foreach (var pragma in block.Directives.Pragmas)
Console.WriteLine($"#pragma {pragma.Name} {string.Join(" ", pragma.Arguments)}");
foreach (var include in block.Directives.Includes)
Console.WriteLine(include.Path); // raw and unresolved, like Fallback/Dependency/UsePass
}
Pass an isMacroDefined lookup to Parse to resolve #if/#ifdef/#ifndef/#elif/#else branches against your build's macros. It's tri-state (true/false/null = unknown), so partial knowledge is fine — a branch that can't be resolved is conservatively kept "live" rather than dropped:
var definedMacros = new HashSet<string> { "SHADER_API_MOBILE" };
var result = ShaderLab.Parse(shaderSource, "Toon.shader", name => definedMacros.Contains(name));
var block = result.Shader.AllSubShaders.First().Passes.OfType<PassNode>().First().ProgramBlocks[0];
foreach (var directive in block.Directives.All)
{
if (!directive.IsBranchLive) continue; // provably dead for this macro set
// ...
}
ProgramDirectiveScanner.Scan(...) is also public directly, for re-scanning a block with different options (e.g. reportUnknownPragmas: true) or scanning raw text with no ProgramBlockNode of its own — such as an #include file's contents once you've resolved it yourself.
#define/#undef tracking
#define NAME / #define NAME(args) ... and #undef NAME are recognized as DefineDirectiveNode/UndefDirectiveNode — structurally only: the macro name (and, for #define, whether it's function-like) is captured, but the replacement text is kept verbatim and never expanded anywhere, matching this library's "no macro-value substitution" scope.
What is handled automatically: a #define/#undef seen in one program or include block resolves #if/#ifdef/#ifndef in a later block within the same file scope — e.g. a CGINCLUDE's #define gates a subsequent CGPROGRAM's #if in the same Pass. This is scoped correctly per SubShader/Pass/Category (a scope's own #defines are visible to its children but never leak to sibling scopes) and per shader language (Cg/HLSL/GLSL are tracked independently, since Unity never concatenates one into another). No extra wiring is needed beyond calling Parse.
For callers who want to seed platform/build macros up front instead of (or alongside) the bare isMacroDefined delegate, MacroTable is available as a structured, tri-state, pre-seedable macro environment.
Building & testing
dotnet build Tesearis.ShaderLabParser.slnx
dotnet test Tesearis.ShaderLabParser.slnx
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 102 | 8/21/2026 |