acdream/src/AcDream.App/Rendering/Shader.cs

138 lines
4.7 KiB
C#

using System.Numerics;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
public sealed class Shader : IDisposable
{
private readonly GL _gl;
private readonly Dictionary<string, int> _uniformLocations = new(StringComparer.Ordinal);
public uint Program { get; private set; }
public Shader(GL gl, string vertexPath, string fragmentPath)
: this(gl, vertexPath, fragmentPath, includeCommonPreamble: false)
{
}
/// <summary>
/// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when
/// <paramref name="includeCommonPreamble"/> is true, the text of
/// <c>Shaders/common.glsl</c> — sitting alongside <paramref name="vertexPath"/>
/// — is spliced into both sources right after their leading
/// <c>#version</c>/<c>#extension</c> block. GL has no <c>#include</c>, so this
/// is plain string concatenation at load time rather than a GLSL-level
/// mechanism. Every existing two-argument-path caller is unaffected: the
/// convenience constructor above always passes false.
/// </summary>
public Shader(GL gl, string vertexPath, string fragmentPath, bool includeCommonPreamble)
{
_gl = gl;
string vertexSource = File.ReadAllText(vertexPath);
string fragmentSource = File.ReadAllText(fragmentPath);
if (includeCommonPreamble)
{
string? directory = Path.GetDirectoryName(vertexPath);
string commonPath = directory is null
? "common.glsl"
: Path.Combine(directory, "common.glsl");
string commonSource = File.ReadAllText(commonPath);
vertexSource = InjectPreamble(vertexSource, commonSource);
fragmentSource = InjectPreamble(fragmentSource, commonSource);
}
Program = ShaderProgramConstruction.Build(
new GlShaderProgramBuildApi(gl),
vertexSource,
fragmentSource);
}
/// <summary>
/// Inserts <paramref name="preamble"/> right after the shader's leading
/// <c>#version</c>/<c>#extension</c>/blank-line block. GLSL requires
/// <c>#version</c> to be the very first statement in the source, so the
/// preamble cannot simply be prepended — it has to land after that block,
/// before the first real declaration.
/// </summary>
private static string InjectPreamble(string source, string preamble)
{
int insertAt = 0;
int lineStart = 0;
while (lineStart < source.Length)
{
int lineEnd = source.IndexOf('\n', lineStart);
if (lineEnd < 0)
lineEnd = source.Length;
string trimmed = source[lineStart..lineEnd].TrimStart();
bool isLeadingLine =
trimmed.Length == 0
|| trimmed.StartsWith("#version", StringComparison.Ordinal)
|| trimmed.StartsWith("#extension", StringComparison.Ordinal);
if (!isLeadingLine)
break;
insertAt = lineEnd < source.Length ? lineEnd + 1 : lineEnd;
lineStart = lineEnd + 1;
}
return string.Concat(source.AsSpan(0, insertAt), preamble, "\n", source.AsSpan(insertAt));
}
public void Use() => _gl.UseProgram(Program);
public unsafe void SetMatrix4(string name, Matrix4x4 m)
{
int loc = GetUniformLocation(name);
_gl.UniformMatrix4(loc, 1, false, (float*)&m);
}
public void SetInt(string name, int value)
{
int loc = GetUniformLocation(name);
_gl.Uniform1(loc, value);
}
public void SetFloat(string name, float value)
{
int loc = GetUniformLocation(name);
_gl.Uniform1(loc, value);
}
public void SetVec3(string name, Vector3 v)
{
int loc = GetUniformLocation(name);
_gl.Uniform3(loc, v.X, v.Y, v.Z);
}
public void SetVec2(string name, Vector2 v)
{
int loc = GetUniformLocation(name);
_gl.Uniform2(loc, v.X, v.Y);
}
public void SetVec4(string name, Vector4 v)
{
int loc = GetUniformLocation(name);
_gl.Uniform4(loc, v.X, v.Y, v.Z, v.W);
}
private int GetUniformLocation(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
if (_uniformLocations.TryGetValue(name, out int location))
return location;
location = _gl.GetUniformLocation(Program, name);
_uniformLocations.Add(name, location);
return location;
}
public void Dispose()
{
uint program = Program;
if (program == 0)
return;
_uniformLocations.Clear();
GlResourceCommand.DeleteProgram(_gl, program, $"delete Shader program {program}");
Program = 0;
}
}