diff --git a/Godot 4 Tests/Run.cs b/Godot 4 Tests/Run.cs index 13482f4..c9e204e 100644 --- a/Godot 4 Tests/Run.cs +++ b/Godot 4 Tests/Run.cs @@ -65,6 +65,7 @@ private static IEnumerable> Tests yield return ITest.GetTest; yield return ITest.GetTest; yield return ITest.GetTest; + yield return ITest.GetTest; yield return ITest.GetTest; yield return ITest.GetTest; yield return ITest.GetTest; diff --git a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs new file mode 100644 index 0000000..743e337 --- /dev/null +++ b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs @@ -0,0 +1,49 @@ +using FluentAssertions; +using Godot; +using GodotSharp.BuildingBlocks.TestRunner; + +namespace GodotTests.TestScenes; + +[ShaderGlobals] +public static partial class ShaderGlobals; + +[SceneTree] +public partial class ShaderGlobalsAttributeTests : Node, ITest +{ + void ITest.InitTests() + { + ShaderGlobals.A.Should().BeTrue(); + ShaderGlobals.B.Should().Be(2); + ShaderGlobals.C.Should().Be(0); + ShaderGlobals.D.Should().Be(9); + ShaderGlobals.E.Should().Be(875); + ShaderGlobals.F.Should().Be(new Vector2I(565, 0)); + ShaderGlobals.G.Should().Be(new Vector3I(0, 410, 0)); + ShaderGlobals.H.Should().Be(new Vector4I(0, 475, 0, 180)); + ShaderGlobals.I.Should().Be(new Rect2I(50, 0, 145, 0)); + ShaderGlobals.J.Should().Be(345); + ShaderGlobals.K.Should().Be(new Vector2I(295, 355)); + ShaderGlobals.L.Should().Be(new Vector3I(0, 195, 0)); + ShaderGlobals.M.Should().Be(new Vector4I(0, 0, 275, 0)); + ShaderGlobals.N.Should().Be(0.205f); + ShaderGlobals.O.Should().Be(new Vector2(0.23f, 0.385f)); + ShaderGlobals.P.Should().Be(new Vector3(0.0f, 0.435f, 0.0f)); + ShaderGlobals.Q.Should().Be(new Vector4(0.22f, 0.0f, 0.275f, 0.0f)); + ShaderGlobals.R.Should().Be(new Color(0.517647f, 0.921569f, 0.52549f, 0.788235f)); + ShaderGlobals.S.Should().Be(new Rect2(0.19f, 0.0f, 0.065f, 0.1f)); + ShaderGlobals.T.Should().Be(new Vector4(1.36f, 0.44f, 0.22f, 1.0f)); + ShaderGlobals.U.Should().Be(new Basis(1.0f, 0.205f, 1.37f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f)); + ShaderGlobals.V.Should().Be(new Projection(1.0f, 0.46f, 0.92f, 0.0f, 0.0f, 1.315f, 0.92f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f)); + ShaderGlobals.W.Should().Be(new Transform2D(1.0f, 0.0f, 1.885f, 1.0f, 0.755f, 0.0f)); + ShaderGlobals.X.Should().Be(new Transform3D(1.0f, 0.0f, 0.0f, 0.0f, 1.35f, 0.59f, 0.0f, 0.0f, 1.0f, 0.0f, 0.54f, 0.0f)); + ShaderGlobals.Y.Should().BeNull(); + ShaderGlobals.Y.GetDeclaredType().Should().Be(typeof(Texture2D)); + ShaderGlobals.Z.Should().BeNull(); + ShaderGlobals.Z.GetDeclaredType().Should().Be(typeof(Texture2DArray)); + ShaderGlobals.Ä.Should().Be(GD.Load("res://TestScenes/Feature.ShaderGlobals/Noise.tres")); + ShaderGlobals.Ö.Should().BeNull(); + ShaderGlobals.Ö.GetDeclaredType().Should().Be(typeof(Cubemap)); + ShaderGlobals.Ü.Should().BeNull(); + ShaderGlobals.Ü.GetDeclaredType().Should().Be(typeof(ExternalTexture)); + } +} diff --git a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid new file mode 100644 index 0000000..5bd4640 --- /dev/null +++ b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid @@ -0,0 +1 @@ +uid://dm8vgm0v4e6hi diff --git a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn new file mode 100644 index 0000000..9c3d263 --- /dev/null +++ b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://qlum4js31ian"] + +[ext_resource type="Script" uid="uid://dm8vgm0v4e6hi" path="res://TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs" id="1_y0unj"] + +[node name="ShaderGlobalsAttributeTests" type="Node"] +script = ExtResource("1_y0unj") diff --git a/Godot 4 Tests/Utils/Extensions/ReflectionExtensions.cs b/Godot 4 Tests/Utils/Extensions/ReflectionExtensions.cs index 6138bb6..5fd9107 100644 --- a/Godot 4 Tests/Utils/Extensions/ReflectionExtensions.cs +++ b/Godot 4 Tests/Utils/Extensions/ReflectionExtensions.cs @@ -45,4 +45,6 @@ public static void ShouldContain(this Type t, if (Properties is not null) t.Properties().Should().Contain(Properties); if (NestedTypes is not null) t.NestedTypes().Should().Contain(NestedTypes); } + + public static Type GetDeclaredType(this T t) => typeof(T); } diff --git a/Godot 4 Tests/project.godot b/Godot 4 Tests/project.godot index 2cae71b..446a33c 100644 --- a/Godot 4 Tests/project.godot +++ b/Godot 4 Tests/project.godot @@ -164,3 +164,122 @@ avoidance/layer_16="- With Leading - 16" avoidance/layer_17="7 With Leading Numeric 17" avoidance/layer_18=". With Leading . 18" avoidance/layer_19="中文 With Leading Unicode 19" + +[shader_globals] + +a={ +"type": "bool", +"value": true +} +b={ +"type": "bvec2", +"value": 2 +} +c={ +"type": "bvec3", +"value": 0 +} +d={ +"type": "bvec4", +"value": 9 +} +e={ +"type": "int", +"value": 875 +} +f={ +"type": "ivec2", +"value": Vector2i(565, 0) +} +g={ +"type": "ivec3", +"value": Vector3i(0, 410, 0) +} +h={ +"type": "ivec4", +"value": Vector4i(0, 475, 0, 180) +} +i={ +"type": "rect2i", +"value": Rect2i(50, 0, 145, 0) +} +j={ +"type": "uint", +"value": 345 +} +k={ +"type": "uvec2", +"value": Vector2i(295, 355) +} +l={ +"type": "uvec3", +"value": Vector3i(0, 195, 0) +} +m={ +"type": "uvec4", +"value": Vector4i(0, 0, 275, 0) +} +n={ +"type": "float", +"value": 0.205 +} +o={ +"type": "vec2", +"value": Vector2(0.23, 0.385) +} +p={ +"type": "vec3", +"value": Vector3(0, 0.435, 0) +} +q={ +"type": "vec4", +"value": Vector4(0.22, 0, 0.275, 0) +} +r={ +"type": "color", +"value": Color(0.517647, 0.921569, 0.52549, 0.788235) +} +s={ +"type": "rect2", +"value": Rect2(0.19, 0, 0.065, 0.1) +} +t={ +"type": "mat2", +"value": PackedFloat32Array(1.36, 0.44, 0.22, 1) +} +u={ +"type": "mat3", +"value": Basis(1, 0.205, 1.37, 0, 1, 0, 0, 0, 1) +} +v={ +"type": "mat4", +"value": Projection(1, 0.46, 0.92, 0, 0, 1.315, 0.92, 0, 0, 0, 1, 0, 0, 0, 0, 1) +} +w={ +"type": "transform_2d", +"value": Transform2D(1, 0, 1.885, 1, 0.755, 0) +} +x={ +"type": "transform", +"value": Transform3D(1, 0, 0, 0, 1.35, 0.59, 0, 0, 1, 0, 0.54, 0) +} +y={ +"type": "sampler2D", +"value": "" +} +z={ +"type": "sampler2DArray", +"value": "" +} +"ä"={ +"type": "sampler3D", +"value": "res://TestScenes/Feature.ShaderGlobals/Noise.tres" +} +"ö"={ +"type": "samplerCube", +"value": "" +} +"ü"={ +"type": "samplerExternalOES", +"value": "" +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/Resources.cs b/SourceGenerators/ShaderGlobalsExtensions/Resources.cs new file mode 100644 index 0000000..1b70b16 --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/Resources.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; + +internal static class Resources +{ + private const string shaderGlobalsTemplate = "GodotSharp.SourceGenerators.ShaderGlobalsExtensions.ShaderGlobalsTemplate.scriban"; + public static readonly string ShaderGlobalsTemplate = Assembly.GetExecutingAssembly().GetEmbeddedResource(shaderGlobalsTemplate); +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs new file mode 100644 index 0000000..686a96b --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs @@ -0,0 +1,9 @@ +using System.Runtime.CompilerServices; + +namespace Godot; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class ShaderGlobalsAttribute([CallerFilePath] string classPath = null) : Attribute +{ + public string ClassPath { get; } = classPath; +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs new file mode 100644 index 0000000..f74d8ce --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs @@ -0,0 +1,137 @@ +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; + +namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; + +internal class ShaderGlobalsDataModel : ClassDataModel +{ + public abstract record ShaderGlobalDefault + { + public abstract string GetDefault(string type); + } + + public record ShaderGlobalResourceDefault(string ResourcePath) : ShaderGlobalDefault + { + private static readonly Regex ResourcePathRegex = new(@"^""res://.+""$"); + public static bool TryParse(string @default, out ShaderGlobalDefault outDefault) + { + if (ResourcePathRegex.IsMatch(@default)) + { + outDefault = new ShaderGlobalResourceDefault(@default); + return true; + } + + outDefault = null; + return false; + } + + public override string GetDefault(string type) => $"GD.Load<{type}>({ResourcePath})"; + } + + public record ShaderGlobalConstructorDefault(string Parameters) : ShaderGlobalDefault + { + private static readonly Regex ConstructorRegex = new(@"^.+\((?.+)\)$"); + public static bool TryParse(string @default, out ShaderGlobalDefault outDefault) + { + var match = ConstructorRegex.Match(@default); + if (match.Success) + { + outDefault = new ShaderGlobalConstructorDefault(match.Groups["Parameters"].Value); + return true; + } + + outDefault = null; + return false; + } + + public override string GetDefault(string type) => $"new {type}({string.Join(",", Parameters + .Split(',') + .Select(ConvertFloatingLiteral))})"; + } + + public record ShaderGlobalLiteralDefault(string Literal) : ShaderGlobalDefault + { + public override string GetDefault(string type) => ConvertFloatingLiteral(Literal); + } + + // We need to append 'f' to float literals. + private static string ConvertFloatingLiteral(string literal) => literal.Contains('.') ? literal + "f" : literal; + + public record ShaderGlobal(string Type, string Name, string Default, string RawName); + + public IList ShaderGlobals { get; } + + public ShaderGlobalsDataModel(INamedTypeSymbol symbol, string csPath, string gdRoot) : base(symbol) + { + ShaderGlobals = ShaderGlobalsScraper + .GetShaderGlobals(csPath, gdRoot) + .Select(Convert) + .ToArray(); + + static ShaderGlobal Convert(ShaderGlobalsScraper.ShaderGlobal global) + { + var csType = ConvertType(global.Type); + return new(csType, global.Name.ToPascalCase(), ConvertDefault(global.Default)?.GetDefault(csType) ?? "default", global.Name); + } + + static ShaderGlobalDefault ConvertDefault(string @default) + => @default is null or "" or "\"\"" + ? null + : ShaderGlobalResourceDefault.TryParse(@default, out var outDefault) + ? outDefault + : ShaderGlobalConstructorDefault.TryParse(@default, out outDefault) + ? outDefault + : new ShaderGlobalLiteralDefault(@default); + + static string ConvertType(string type) + => type switch + { + "bvec2" => "int", + "bvec3" => "int", + "bvec4" => "int", + + "ivec2" => "Vector2I", + "ivec3" => "Vector3I", + "ivec4" => "Vector4I", + + "uvec2" => "Vector2I", + "uvec3" => "Vector3I", + "uvec4" => "Vector4I", + + "vec2" => "Vector2", + "vec3" => "Vector3", + "vec4" => "Vector4", + + "color" => "Color", + + "rect2" => "Rect2", + "rect2i" => "Rect2I", + + "mat2" => "Vector4", + "mat3" => "Basis", + "mat4" => "Projection", + + "transform_2d" => "Transform2D", + "transform" => "Transform3D", + + "sampler2D" => "Texture2D", + "sampler2DArray" => "Texture2DArray", + "sampler3D" => "Texture3D", + "samplerCube" => "Cubemap", + "samplerExternalOES" => "ExternalTexture", + + _ => type, + }; + } + + protected override string Str() + { + return string.Join("\n", ShaderGlobals()); + + IEnumerable ShaderGlobals() + { + foreach (var (type, name, @default, rawType) in this.ShaderGlobals) + yield return $"Type: {type}, Name: {name}, Default: {@default}, RawType: {rawType}"; + } + } +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs new file mode 100644 index 0000000..88064e7 --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs @@ -0,0 +1,82 @@ +using System.Text.RegularExpressions; + +namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; + +internal static class ShaderGlobalsScraper +{ + private static readonly Regex BeginGlobalRegex = new(@"^""?(?.+?)""?={$"); + private static readonly Regex EndGlobalRegex = new("^}$"); + private static readonly Regex TypeRegex = new(@"^""type"": ""(?.+)"",?$"); + private static readonly Regex DefaultRegex = new(@"^""value"": (?.+?),?$"); + + public record ShaderGlobal(string Type, string Name, string Default); + + public static IEnumerable GetShaderGlobals(string csFile, string gdRoot) + { + var gdFile = GD.GetProjectFile(csFile, gdRoot); + Log.Debug($"Scraping {gdFile} [Compiling {csFile}]"); + + return MatchShaderConstants(gdFile); + + static IEnumerable MatchShaderConstants(string gdFile) + { + var found = false; + string name = null; + string type = null; + string @default = null; + foreach (var line in File.ReadLines(gdFile).Where(line => line != string.Empty)) + { + Log.Debug($"Line: {line}"); + + + if (line is "[shader_globals]") + { + found = true; + continue; + } + + if (found) + { + if (name != null) + { + if (EndGlobalRegex.IsMatch(line)) + { + if (type is null) + { + Log.Warn($" - Ignoring shader global without type {name}"); + continue; + } + + yield return new(type, name, @default); + + name = null; + type = null; + @default = null; + } + else if (TypeRegex.Match(line) is { Success: true } typeMatch) + { + if (type != null) + Log.Warn($" - Duplicate type {type} for shader global {name}"); + + type = typeMatch.Groups["Type"].Value; + } + else if (DefaultRegex.Match(line) is { Success: true } defaultMatch) + { + if (@default != null) + Log.Warn($" - Duplicate default {@defaultMatch} for shader global {name}"); + + @default = defaultMatch.Groups["Default"].Value; + } + } + else + { + if (line.StartsWith("[")) + yield break; + else if (BeginGlobalRegex.Match(line) is { Success: true } beginMatch) + name = beginMatch.Groups["Name"].Value; + } + } + } + } + } +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs new file mode 100644 index 0000000..5e3ef5d --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Scriban; + +namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; + +[Generator] +internal class ShaderGlobalsSourceGenerator : SourceGeneratorForDeclaredTypeWithAttribute +{ + private static Template ShaderGlobalsTemplate => field ??= Template.Parse(Resources.ShaderGlobalsTemplate); + + protected override (string GeneratedCode, DiagnosticDetail Error) GenerateCode(Compilation compilation, SyntaxNode node, INamedTypeSymbol symbol, AttributeData attribute, AnalyzerConfigOptions options) + { + var data = ReconstructAttribute(); + var model = new ShaderGlobalsDataModel(symbol, data.ClassPath, options.TryGetGodotProjectDir()); + Log.Debug($"--- MODEL ---\n{model}\n"); + + var output = ShaderGlobalsTemplate.Render(model, member => member.Name); + Log.Debug($"--- OUTPUT ---\n{output}\n"); + + return (output, null); + + Godot.ShaderGlobalsAttribute ReconstructAttribute() => new( + (string)attribute.ConstructorArguments[0].Value); + } +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban new file mode 100644 index 0000000..abc02e7 --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel; + +using Godot; + +{{~ Namespace ~}} + +partial class {{ClassName}} +{ +{{~ for global in ShaderGlobals ~}} + private static readonly StringName _{{global.Name}}Name = "{{global.RawName}}"; + private static {{global.Type}} _{{global.Name}} = {{global.Default}}; + /// A statically typed wrapper for the shader global {{global.RawName}} defined in godot.project. + /// When the shader global is modified outside of this property, the change will not be reflected in the property. + public static {{global.Type}} {{global.Name}} + { + get => _{{global.Name}}; + set + { + RenderingServer.GlobalShaderParameterSet(_{{global.Name}}Name, value); + _{{global.Name}} = value; + } + } +{{~ end ~}} +}