-
-
Notifications
You must be signed in to change notification settings - Fork 457
Expand file tree
/
Copy pathProgram.cs
More file actions
329 lines (268 loc) · 16.4 KB
/
Program.cs
File metadata and controls
329 lines (268 loc) · 16.4 KB
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
using System;
using System.IO;
using Silk.NET.Maths;
using Silk.NET.Windowing;
using Silk.NET.OpenGL;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = System.Drawing.Color;
// Textures!
// In this tutorial, you'll learn how to load and render textures.
namespace Tutorial
{
public class Program
{
private static IWindow _window;
private static GL _gl;
private static uint _vao;
private static uint _vbo;
private static uint _ebo;
private static uint _program;
private static uint _texture;
public static void Main(string[] args)
{
WindowOptions options = WindowOptions.Default;
options.Size = new Vector2D<int>(800, 600);
options.Title = "1.3 - Textures";
_window = Window.Create(options);
_window.Load += OnLoad;
_window.Update += OnUpdate;
_window.Render += OnRender;
_window.FramebufferResize += OnFramebufferResize;
_window.Run();
_window.Dispose();
}
private static unsafe void OnLoad()
{
_gl = _window.CreateOpenGL();
_gl.ClearColor(Color.CornflowerBlue);
// Create the VAO.
_vao = _gl.GenVertexArray();
_gl.BindVertexArray(_vao);
// The quad vertices data.
// You may have noticed an addition - texture coordinates!
// Texture coordinates are a value between 0-1 (see more later about this) which tell the GPU which part
// of the texture to use for each vertex.
float[] vertices =
{
// aPosition-------- aTexCoords
0.5f, 0.5f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f
};
// Create the VBO.
_vbo = _gl.GenBuffer();
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
// Upload the vertices data to the VBO.
fixed (float* buf = vertices)
_gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint) (vertices.Length * sizeof(float)), buf, BufferUsageARB.StaticDraw);
// The quad indices data.
uint[] indices =
{
0u, 1u, 3u,
1u, 2u, 3u
};
// Create the EBO.
_ebo = _gl.GenBuffer();
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ebo);
// Upload the indices data to the EBO.
fixed (uint* buf = indices)
_gl.BufferData(BufferTargetARB.ElementArrayBuffer, (nuint) (indices.Length * sizeof(uint)), buf, BufferUsageARB.StaticDraw);
// The vertex shader code.
const string vertexCode = @"
#version 330 core
layout (location = 0) in vec3 aPosition;
// On top of our aPosition attribute, we now create an aTexCoords attribute for our texture coordinates.
layout (location = 1) in vec2 aTexCoords;
// Likewise, we also assign an out attribute to go into the fragment shader.
out vec2 frag_texCoords;
void main()
{
gl_Position = vec4(aPosition, 1.0);
// This basic vertex shader does no additional processing of texture coordinates, so we can pass them
// straight to the fragment shader.
frag_texCoords = aTexCoords;
}";
// The fragment shader code.
const string fragmentCode = @"
#version 330 core
// This in attribute corresponds to the out attribute we defined in the vertex shader.
in vec2 frag_texCoords;
out vec4 out_color;
// Now we define a uniform value!
// A uniform in OpenGL is a value that can be changed outside of the shader by modifying its value.
// A sampler2D contains both a texture and information on how to sample it.
// Sampling a texture is basically calculating the color of a pixel on a texture at any given point.
uniform sampler2D uTexture;
void main()
{
// We use GLSL's texture function to sample from the texture at the given input texture coordinates.
out_color = texture(uTexture, frag_texCoords);
}";
// Create our vertex shader, and give it our vertex shader source code.
uint vertexShader = _gl.CreateShader(ShaderType.VertexShader);
_gl.ShaderSource(vertexShader, vertexCode);
// Attempt to compile the shader.
_gl.CompileShader(vertexShader);
// Check to make sure that the shader has successfully compiled.
_gl.GetShader(vertexShader, ShaderParameterName.CompileStatus, out int vStatus);
if (vStatus != (int) GLEnum.True)
throw new Exception("Vertex shader failed to compile: " + _gl.GetShaderInfoLog(vertexShader));
// Repeat this process for the fragment shader.
uint fragmentShader = _gl.CreateShader(ShaderType.FragmentShader);
_gl.ShaderSource(fragmentShader, fragmentCode);
_gl.CompileShader(fragmentShader);
_gl.GetShader(fragmentShader, ShaderParameterName.CompileStatus, out int fStatus);
if (fStatus != (int) GLEnum.True)
throw new Exception("Fragment shader failed to compile: " + _gl.GetShaderInfoLog(fragmentShader));
// Create our shader program, and attach the vertex & fragment shaders.
_program = _gl.CreateProgram();
_gl.AttachShader(_program, vertexShader);
_gl.AttachShader(_program, fragmentShader);
// Attempt to "link" the program together.
_gl.LinkProgram(_program);
// Similar to shader compilation, check to make sure that the shader program has linked properly.
_gl.GetProgram(_program, ProgramPropertyARB.LinkStatus, out int lStatus);
if (lStatus != (int) GLEnum.True)
throw new Exception("Program failed to link: " + _gl.GetProgramInfoLog(_program));
// Detach and delete our shaders. Once a program is linked, we no longer need the individual shader objects.
_gl.DetachShader(_program, vertexShader);
_gl.DetachShader(_program, fragmentShader);
_gl.DeleteShader(vertexShader);
_gl.DeleteShader(fragmentShader);
// Set up our vertex attributes! These tell the vertex array (VAO) how to process the vertex data we defined
// earlier. Each vertex array contains attributes.
// Our stride constant. The stride must be in bytes, so we take the first attribute (a vec3), multiply it
// by the size in bytes of a float, and then take our second attribute (a vec2), and do the same.
const uint stride = (3 * sizeof(float)) + (2 * sizeof(float));
// Enable the "aPosition" attribute in our vertex array, providing its size and stride too.
const uint positionLoc = 0;
_gl.EnableVertexAttribArray(positionLoc);
_gl.VertexAttribPointer(positionLoc, 3, VertexAttribPointerType.Float, false, stride, (void*) 0);
// Now we need to enable our texture coordinates! We've defined that as location 1 so that's what we'll use
// here. The code is very similar to above, but you must make sure you set its offset to the **size in bytes**
// of the attribute before.
const uint textureLoc = 1;
_gl.EnableVertexAttribArray(textureLoc);
_gl.VertexAttribPointer(textureLoc, 2, VertexAttribPointerType.Float, false, stride, (void*) (3 * sizeof(float)));
// Unbind everything as we don't need it.
_gl.BindVertexArray(0);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
// Now we begin the process of creating our texture!
// First, we create the texture handle. Then, we must set an active texture unit. Each texture unit is a
// separate bindable texture that we can use in a shader. GPUs have a maximum number of texture units they
// can use, however the OpenGL spec states there MUST be at least 32 units available.
// Much like buffers, we then bind the texture to the Texture2D target.
_texture = _gl.GenTexture();
_gl.ActiveTexture(TextureUnit.Texture0);
_gl.BindTexture(TextureTarget.Texture2D, _texture);
// Use ImageSharp to load an image from our PNG file.
using (Image<Rgba32> image = Image.Load<Rgba32>("silk.png"))
{
// Now, let's create the texture itself.
// Much like buffers, the texture is not created until you call glTexImage2D, where you define some
// parameters to describe the texture. Let's go over each parameter used here:
// 1. Tell OpenGL that we want to use the texture bound in the Texture2D target.
// 2. We are creating the "base" texture level, therefore this value should be 0. You don't need to
// worry about texture levels for now.
// 3. We tell OpenGL that we want the GPU to store this data as RGBA formatted data on the GPU itself.
// 4. The image's width.
// 5. The image's height.
// 6. This is the image's border. This value MUST be 0. It is a leftover component from legacy OpenGL,
// and it serves no purpose.
// 7. Our image data is formatted as RGBA data, therefore we must tell OpenGL we are uploading RGBA data.
// 8. The Rgba32 struct contains four color channels stored as bytes. Therefore we must tell OpenGL we
// each color channel will be an unsigned byte.
// 9. This parameter is a pointer to an array of texture data. ImageSharp doesn't provide a single array
// of data, so we just pass in null here, and upload our texture data below instead.
_gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, (uint) image.Width, (uint) image.Height,
0, PixelFormat.Rgba, PixelType.UnsignedByte, null);
// Upload our texture data line-by-line.
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y++)
{
fixed (void* data = accessor.GetRowSpan(y))
{
// glTexSubImage2D allows us to upload data to arbitrary locations within the texture.
// This can be used to upload to sub-regions of the texture.
// In this case, we use it to upload each line of our texture data to the texture.
_gl.TexSubImage2D(TextureTarget.Texture2D, 0, 0, y, (uint) accessor.Width, 1,
PixelFormat.Rgba, PixelType.UnsignedByte, data);
}
}
});
}
// Now let's set some texture parameters!
// This tells the GPU how it should sample the texture.
// Set the texture wrap mode to repeat.
// The texture wrap mode defines what should happen when the texture coordinates go outside of the 0-1 range.
// In this case, we set it to repeat. The texture will just repeatedly tile over and over again.
// You'll notice we're using S and T wrapping here. This is OpenGL's version of the standard UV mapping you
// may be more used to, where S is on the X-axis, and T is on the Y-axis.
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) TextureWrapMode.Repeat);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) TextureWrapMode.Repeat);
// The min and mag filters define how the texture should be sampled as it resized.
// The min, or minification filter, is used when the texture is reduced in size.
// The mag, or magnification filter, is used when the texture is increased in size.
// We're using bilinear filtering here, as it produces a generally nice result.
// You can also use nearest (point) filtering, or anisotropic filtering, which is only available on the min
// filter.
// You may notice that the min filter defines a "mipmap" filter as well. We'll go over mipmaps below.
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Linear);
// Generate mipmaps for this texture.
// Note: We MUST do this or the texture will appear as black (this is an option you can change but this is
// out of scope for this tutorial).
// What is a mipmap?
// A mipmap is essentially a smaller version of the existing texture. When generating mipmaps, the texture
// size is continuously halved, generally stopping once it reaches a size of 1x1 pixels. (Note: there are
// exceptions to this, for example if the GPU reaches its maximum level of mipmaps, which is both a hardware
// limitation, and a user defined value. You don't need to worry about this for now, so just assume that
// the mips will be generated all the way down to 1x1 pixels).
// Mipmaps are used when the texture is reduced in size, to produce a much nicer result, and to reduce moire
// effect patterns.
_gl.GenerateMipmap(TextureTarget.Texture2D);
// Unbind the texture as we no longer need to update it any further.
_gl.BindTexture(TextureTarget.Texture2D, 0);
// Get our texture uniform, and set it to 0.
// We can easily do this by using glGetUniformLocation and giving it a name.
// Setting it to 0 tells it that you want it to use the 0th texture unit.
// Generally, OpenGL should automatically initialize all uniform values to their default value (which is
// almost always 0), however you should get into the practice of initializing all uniform values to a known
// value, before you use them in your shader.
int location = _gl.GetUniformLocation(_program, "uTexture");
_gl.Uniform1(location, 0);
// Finally a bit of blending!
// If you disable blending, you'll notice a black border around the texture.
// The texture is partially transparent, however OpenGL doesn't know how to handle this by default.
// By enabling blending, and giving it a blend function, you can tell OpenGL how to handle transparency.
// In this case, it removes the black background and just leaves the texture on its own.
// The blend function is out of scope for this tutorial, so don't worry if you don't understand it too much.
// The program will function just fine without blending!
_gl.Enable(EnableCap.Blend);
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
}
private static void OnUpdate(double dt) { }
private static unsafe void OnRender(double dt)
{
// Clear the window to the color we set earlier.
_gl.Clear(ClearBufferMask.ColorBufferBit);
// Bind our VAO, then the program.
_gl.BindVertexArray(_vao);
_gl.UseProgram(_program);
// Much like our texture creation earlier, we must first set our active texture unit, and then bind the
// texture to use it during draw!
_gl.ActiveTexture(TextureUnit.Texture0);
_gl.BindTexture(TextureTarget.Texture2D, _texture);
// Draw our quad! We use a count of 6 here because we have 6 total vertices that makes up a quad.
_gl.DrawElements(PrimitiveType.Triangles, 6, DrawElementsType.UnsignedInt, (void*) 0);
}
private static void OnFramebufferResize(Vector2D<int> newSize)
{
_gl.Viewport(newSize);
}
}
}