-
Notifications
You must be signed in to change notification settings - Fork 2
/
Shader.cs
199 lines (168 loc) · 8.13 KB
/
Shader.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using OpenTK;
using OpenTK.Graphics.OpenGL4;
namespace OpenTkNoesisGui
{
// A simple class meant to help create shaders.
public class Shader
{
public readonly int Handle;
private readonly Dictionary<string, int> _uniformLocations;
// This is how you create a simple shader.
// Shaders are written in GLSL, which is a language very similar to C in its semantics.
// The GLSL source is compiled *at runtime*, so it can optimize itself for the graphics card it's currently being used on.
// A commented example of GLSL can be found in shader.vert
public Shader(string vertPath, string fragPath)
{
// There are several different types of shaders, but the only two you need for basic rendering are the vertex and fragment shaders.
// The vertex shader is responsible for moving around vertices, and uploading that data to the fragment shader.
// The vertex shader won't be too important here, but they'll be more important later.
// The fragment shader is responsible for then converting the vertices to "fragments", which represent all the data OpenGL needs to draw a pixel.
// The fragment shader is what we'll be using the most here.
// Load vertex shader and compile
// LoadSource is a simple function that just loads all text from the file whose path is given.
var shaderSource = LoadSource(vertPath);
// GL.CreateShader will create an empty shader (obviously). The ShaderType enum denotes which type of shader will be created.
var vertexShader = GL.CreateShader(ShaderType.VertexShader);
// Now, bind the GLSL source code
GL.ShaderSource(vertexShader, shaderSource);
// And then compile
CompileShader(vertexShader);
// We do the same for the fragment shader
shaderSource = LoadSource(fragPath);
var fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(fragmentShader, shaderSource);
CompileShader(fragmentShader);
// These two shaders must then be merged into a shader program, which can then be used by OpenGL.
// To do this, create a program...
Handle = GL.CreateProgram();
// Attach both shaders...
GL.AttachShader(Handle, vertexShader);
GL.AttachShader(Handle, fragmentShader);
// And then link them together.
LinkProgram(Handle);
// When the shader program is linked, it no longer needs the individual shaders attacked to it; the compiled code is copied into the shader program.
// Detach them, and then delete them.
GL.DetachShader(Handle, vertexShader);
GL.DetachShader(Handle, fragmentShader);
GL.DeleteShader(fragmentShader);
GL.DeleteShader(vertexShader);
// The shader is now ready to go, but first, we're going to cache all the shader uniform locations.
// Querying this from the shader is very slow, so we do it once on initialization and reuse those values
// later.
// First, we have to get the number of active uniforms in the shader.
GL.GetProgram(Handle, GetProgramParameterName.ActiveUniforms, out var numberOfUniforms);
// Next, allocate the dictionary to hold the locations.
_uniformLocations = new Dictionary<string, int>();
// Loop over all the uniforms,
for (var i = 0; i < numberOfUniforms; i++)
{
// get the name of this uniform,
var key = GL.GetActiveUniform(Handle, i, out _, out _);
// get the location,
var location = GL.GetUniformLocation(Handle, key);
// and then add it to the dictionary.
_uniformLocations.Add(key, location);
}
}
private static void CompileShader(int shader)
{
// Try to compile the shader
GL.CompileShader(shader);
// Check for compilation errors
GL.GetShader(shader, ShaderParameter.CompileStatus, out var code);
if (code != (int)All.True)
{
// We can use `GL.GetShaderInfoLog(shader)` to get information about the error.
var infoLog = GL.GetShaderInfoLog(shader);
throw new Exception($"Error occurred whilst compiling Shader({shader}).\n\n{infoLog}");
}
}
private static void LinkProgram(int program)
{
// We link the program
GL.LinkProgram(program);
// Check for linking errors
GL.GetProgram(program, GetProgramParameterName.LinkStatus, out var code);
if (code != (int)All.True)
{
// We can use `GL.GetProgramInfoLog(program)` to get information about the error.
throw new Exception($"Error occurred whilst linking Program({program})");
}
}
// A wrapper function that enables the shader program.
public void Use()
{
GL.UseProgram(Handle);
}
// The shader sources provided with this project use hardcoded layout(location)-s. If you want to do it dynamically,
// you can omit the layout(location=X) lines in the vertex shader, and use this in VertexAttribPointer instead of the hardcoded values.
public int GetAttribLocation(string attribName)
{
return GL.GetAttribLocation(Handle, attribName);
}
// Just loads the entire file into a string.
private static string LoadSource(string path)
{
using (var sr = new StreamReader(path, Encoding.UTF8))
{
return sr.ReadToEnd();
}
}
// Uniform setters
// Uniforms are variables that can be set by user code, instead of reading them from the VBO.
// You use VBOs for vertex-related data, and uniforms for almost everything else.
// Setting a uniform is almost always the exact same, so I'll explain it here once, instead of in every method:
// 1. Bind the program you want to set the uniform on
// 2. Get a handle to the location of the uniform with GL.GetUniformLocation.
// 3. Use the appropriate GL.Uniform* function to set the uniform.
/// <summary>
/// Set a uniform int on this shader.
/// </summary>
/// <param name="name">The name of the uniform</param>
/// <param name="data">The data to set</param>
public void SetInt(string name, int data)
{
GL.UseProgram(Handle);
GL.Uniform1(_uniformLocations[name], data);
}
/// <summary>
/// Set a uniform float on this shader.
/// </summary>
/// <param name="name">The name of the uniform</param>
/// <param name="data">The data to set</param>
public void SetFloat(string name, float data)
{
GL.UseProgram(Handle);
GL.Uniform1(_uniformLocations[name], data);
}
/// <summary>
/// Set a uniform Matrix4 on this shader
/// </summary>
/// <param name="name">The name of the uniform</param>
/// <param name="data">The data to set</param>
/// <remarks>
/// <para>
/// The matrix is transposed before being sent to the shader.
/// </para>
/// </remarks>
public void SetMatrix4(string name, Matrix4 data)
{
GL.UseProgram(Handle);
GL.UniformMatrix4(_uniformLocations[name], true, ref data);
}
/// <summary>
/// Set a uniform Vector3 on this shader.
/// </summary>
/// <param name="name">The name of the uniform</param>
/// <param name="data">The data to set</param>
public void SetVector3(string name, Vector3 data)
{
GL.UseProgram(Handle);
GL.Uniform3(_uniformLocations[name], data);
}
}
}