SukiEffect.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Text;
  7. using Avalonia;
  8. using Avalonia.Controls.ApplicationLifetimes;
  9. using Avalonia.Media;
  10. using Avalonia.Styling;
  11. using SkiaSharp;
  12. using SukiUI.Extensions;
  13. namespace SukiUI.Utilities.Effects
  14. {
  15. /// <summary>
  16. /// Represents an SKSL shader that SukiUI can handle and pass relevant uniforms into.
  17. /// Use the static methods <see cref="SukiEffect.FromEmbeddedResource"/> and <see cref="SukiEffect.FromString"/> for creation.
  18. /// </summary>
  19. public class SukiEffect
  20. {
  21. // Basic uniforms passed into the shader from the CPU.
  22. private static readonly string[] Uniforms =
  23. {
  24. "uniform float iTime;",
  25. "uniform float iDark;",
  26. "uniform float iAlpha;",
  27. "uniform vec3 iResolution;",
  28. "uniform vec3 iPrimary;",
  29. "uniform vec3 iAccent;",
  30. "uniform vec3 iBase;"
  31. };
  32. private static readonly List<SukiEffect> LoadedEffects = new();
  33. private readonly string _rawShaderString;
  34. private readonly string _shaderString;
  35. /// <summary>
  36. /// The compiled <see cref="SKRuntimeEffect"/> that will actually be used in draw calls.
  37. /// </summary>
  38. public SKRuntimeEffect Effect { get; }
  39. private SukiEffect(string shaderString, string rawShaderString)
  40. {
  41. _shaderString = shaderString;
  42. _rawShaderString = rawShaderString;
  43. var compiledEffect = SKRuntimeEffect.Create(_shaderString, out var errors);
  44. Effect = compiledEffect ?? throw new ShaderCompilationException(errors);
  45. }
  46. static SukiEffect()
  47. {
  48. if (Application.Current.ApplicationLifetime is IControlledApplicationLifetime controlled)
  49. controlled.Exit += (_, _) => EnsureDisposed();
  50. }
  51. /// <summary>
  52. /// Attempts to load and compile a ".sksl" shader file from the assembly.
  53. /// You don't need to provide the extension.
  54. /// The shader will be pre-compiled
  55. /// REMEMBER: For files to be discoverable in the assembly they should be marked as an embedded resource.
  56. /// </summary>
  57. /// <param name="shaderName">Name of the shader to load, with or without extension. - MUST BE .sksl</param>
  58. /// <returns>An instance of a SukiBackgroundShader with the loaded shader.</returns>
  59. public static SukiEffect FromEmbeddedResource(string shaderName)
  60. {
  61. shaderName = shaderName.ToLowerInvariant();
  62. if (!shaderName.EndsWith(".sksl"))
  63. shaderName += ".sksl";
  64. var assembly = Assembly.GetEntryAssembly();
  65. var resName = assembly!.GetManifestResourceNames()
  66. .FirstOrDefault(x => x.ToLowerInvariant().Contains(shaderName));
  67. if (resName is null)
  68. {
  69. assembly = Assembly.GetExecutingAssembly();
  70. resName = assembly.GetManifestResourceNames()
  71. .FirstOrDefault(x => x.ToLowerInvariant().Contains(shaderName));
  72. }
  73. if (resName is null)
  74. throw new FileNotFoundException(
  75. $"Unable to find a file with the name \"{shaderName}\" anywhere in the assembly.");
  76. using var tr = new StreamReader(assembly.GetManifestResourceStream(resName)!);
  77. return FromString(tr.ReadToEnd());
  78. }
  79. /// <summary>
  80. /// Attempts to compile an sksl shader from a string.
  81. /// The shader will be pre-compiled and any errors will be thrown as an exception.
  82. /// REMEMBER: For files to be discoverable in the assembly they should be marked as an embedded resource.
  83. /// </summary>
  84. /// <param name="shaderString">The shader code to be compiled.</param>
  85. /// <returns>An instance of a SukiBackgroundShader with the loaded shader</returns>
  86. public static SukiEffect FromString(string shaderString)
  87. {
  88. var sb = new StringBuilder();
  89. foreach (var uniform in Uniforms)
  90. sb.AppendLine(uniform);
  91. sb.Append(shaderString);
  92. var withUniforms = sb.ToString();
  93. return new SukiEffect(withUniforms, shaderString);
  94. }
  95. private static bool _disposed;
  96. /// <summary>
  97. /// Necessary to make sure all the unmanaged effects are disposed.
  98. /// </summary>
  99. internal static void EnsureDisposed()
  100. {
  101. if (_disposed)
  102. throw new InvalidOperationException(
  103. "SukiEffects should only be disposed once at the app lifecycle end.");
  104. _disposed = true;
  105. foreach (var loaded in LoadedEffects)
  106. loaded.Effect.Dispose();
  107. LoadedEffects.Clear();
  108. }
  109. public override bool Equals(object obj)
  110. {
  111. if (obj is not SukiEffect effect) return false;
  112. return effect._shaderString == _shaderString;
  113. }
  114. private static readonly float[] White = { 0.95f, 0.95f, 0.95f };
  115. private readonly float[] _backgroundAlloc = new float[3];
  116. private readonly float[] _backgroundAccentAlloc = new float[3];
  117. private readonly float[] _backgroundPrimaryAlloc = new float[3];
  118. private readonly float[] _boundsAlloc = new float[3];
  119. internal SKShader ToShaderWithUniforms(float timeSeconds, ThemeVariant activeVariant, Rect bounds,
  120. float animationScale, float alpha = 1f)
  121. {
  122. var suki = SukiTheme.GetInstance();
  123. if(suki is null) throw new InvalidOperationException("No Suki Theme Instance is available.");
  124. if (suki.ActiveColorTheme is null) throw new InvalidOperationException("No ActiveColorTheme is available.");
  125. // Update allocated color arrays.
  126. suki.ActiveColorTheme.Background.ToFloatArrayNonAlloc(_backgroundAlloc);
  127. suki.ActiveColorTheme.BackgroundAccent.ToFloatArrayNonAlloc(_backgroundAccentAlloc);
  128. suki.ActiveColorTheme.BackgroundPrimary.ToFloatArrayNonAlloc(_backgroundPrimaryAlloc);
  129. _boundsAlloc[0] = (float)bounds.Width;
  130. _boundsAlloc[1] = (float)bounds.Height;
  131. var inputs = new SKRuntimeEffectUniforms(Effect)
  132. {
  133. { "iResolution", _boundsAlloc },
  134. { "iTime", timeSeconds * animationScale },
  135. {
  136. "iBase",
  137. activeVariant == ThemeVariant.Dark
  138. ? _backgroundAlloc
  139. : White
  140. },
  141. { "iAccent", _backgroundAccentAlloc },
  142. { "iPrimary", _backgroundPrimaryAlloc },
  143. { "iDark", activeVariant == ThemeVariant.Dark ? 1f : 0f },
  144. { "iAlpha", alpha }
  145. };
  146. return Effect.ToShader(false, inputs);
  147. }
  148. internal SKShader ToShaderWithCustomUniforms(Func<SKRuntimeEffect,SKRuntimeEffectUniforms> uniformFactory, float timeSeconds, Rect bounds,
  149. float animationScale, float alpha = 1f)
  150. {
  151. var uniforms = uniformFactory(Effect);
  152. uniforms.Add("iResolution", new[] { (float)bounds.Width, (float)bounds.Height, 0f });
  153. uniforms.Add("iTime", timeSeconds * animationScale);
  154. uniforms.Add("iAlpha", alpha);
  155. return Effect.ToShader(false, uniforms);
  156. }
  157. /// <summary>
  158. /// Returns the pure shader string without uniforms.
  159. /// </summary>
  160. public override string ToString()
  161. {
  162. return _rawShaderString;
  163. }
  164. private class ShaderCompilationException : Exception
  165. {
  166. public ShaderCompilationException(string message) : base(message)
  167. {
  168. }
  169. }
  170. }
  171. }