138 lines
4.7 KiB
C#
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;
|
|
}
|
|
}
|