I'm trying to switch from using the GLSL texture()
function in my shader to using texelFetch()
because I want very little processing of the texture pixels (no smoothing), and I want to use pixel coordinates instead of normalized coordinates. I am quite new to GLSL, so I walked through a tutorial that helped me arrive at the following code, which I got working in an OpenTK-based application in C# in Visual Studio 2015 (the form has an OpenTK.GLControl
control on it called display
, which is not shown in this code). I wanted to get all the code in one place so I put it all in one file where I could get my head around everything that was involved.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private const TextureTarget texTarget = TextureTarget.Texture2D;
private void display_Click(object sender, EventArgs e)
{
VertexBuffer<ColoredVertex> vb = new VertexBuffer<ColoredVertex>(ColoredVertex.Size);
vb.AddVertex(new ColoredVertex(new Vector2() { X = 0, Y = 0 }, new Vector2() { X = 1f/4f, Y = 1f/4f }));
vb.AddVertex(new ColoredVertex(new Vector2() { X = 32, Y = 0 }, new Vector2() { X = 2f/4f, Y = 1f/4f }));
vb.AddVertex(new ColoredVertex(new Vector2() { X = 32, Y = 32 }, new Vector2() { X = 2f/4f, Y = 2f/4f }));
vb.AddVertex(new ColoredVertex(new Vector2() { X = 0, Y = 32 }, new Vector2() { X = 1f/4f, Y = 2f/4f }));
VertexBuffer<ColoredVertex> vb2 = new VertexBuffer<ColoredVertex>(ColoredVertex.Size);
vb2.AddVertex(new ColoredVertex(new Vector2() { X = 16, Y = 16 }, new Vector2() { X = 2f/4f, Y = 2f/4f }));
vb2.AddVertex(new ColoredVertex(new Vector2() { X = 48, Y = 16 }, new Vector2() { X = 3f/4f, Y = 2f/4f }));
vb2.AddVertex(new ColoredVertex(new Vector2() { X = 48, Y = 48 }, new Vector2() { X = 3f/4f, Y = 3f/4f }));
vb2.AddVertex(new ColoredVertex(new Vector2() { X = 16, Y = 48 }, new Vector2() { X = 2f/4f, Y = 3f/4f }));
int texture;
GL.GenTextures(1, out texture);
GL.BindTexture(texTarget, texture);
GL.TexParameter(texTarget, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
GL.TexParameter(texTarget, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
GL.TexParameter(texTarget, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
GL.TexParameter(texTarget, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
Bitmap bmpTexture = (Bitmap)Image.FromFile(@"C:\Users\Benjamin Marty\Desktop\Test.png");
System.Drawing.Imaging.BitmapData bmpData = bmpTexture.LockBits(new Rectangle(Point.Empty, bmpTexture.Size), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
GL.TexImage2D(texTarget, 0, PixelInternalFormat.Rgba8, bmpTexture.Width, bmpTexture.Height, 0, PixelFormat.Bgra, PixelType.UnsignedByte, bmpData.Scan0);
bmpTexture.UnlockBits(bmpData);
bmpTexture.Dispose();
Shader vshader = new Shader(ShaderType.VertexShader,
@"#version 130
// a projection transformation to apply to the vertex' position
uniform mat4 projectionMatrix;
// attributes of our vertex
in vec2 vPosition;
in vec2 vTexCoord;
out vec2 vTex; // must match name in fragment shader
void main()
{
// gl_Position is a special variable of OpenGL that must be set
gl_Position = projectionMatrix * vec4(vPosition, -2.0, 1.0);
vTex = vTexCoord;
}");
Shader fshader = new Shader(ShaderType.FragmentShader,
@"#version 130
in vec2 vTex; // must match name in vertex shader
out vec4 fragColor; // first out variable is automatically written to the screen
uniform sampler2D tex;
void main()
{
fragColor = texture(tex, vTex);
}");
ShaderProgram sp = new ShaderProgram(vshader, fshader);
VertexAttribute vap = new VertexAttribute("vPosition", 2, VertexAttribPointerType.Float, ColoredVertex.Size, 0);
VertexAttribute vac = new VertexAttribute("vTexCoord", 2, VertexAttribPointerType.Float, ColoredVertex.Size, 2 * 4);
VertexArray<ColoredVertex> av = new VertexArray<ColoredVertex>(vb, sp, vap, vac);
GL.ClearColor(Color.Purple);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
sp.Use();
Matrix4 projectionMatrix = Matrix4.CreateOrthographicOffCenter(
ClientRectangle.Left, ClientRectangle.Right, ClientRectangle.Bottom, ClientRectangle.Top, 1.0f, 10f);
GL.UniformMatrix4(sp.GetUniformLocation("projectionMatrix"), false, ref projectionMatrix);
vb.Bind();
av.Bind();
vb.BufferData();
vb.Draw();
vb2.BufferData();
vb2.Draw();
GL.BindVertexArray(0);
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
GL.UseProgram(0);
display.SwapBuffers();
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
if (display.Handle != IntPtr.Zero)
GL.Viewport(ClientRectangle);
}
}
struct ColoredVertex
{
public const int Size = (2 + 2) * 4; // size of struct in bytes
private readonly Vector2 position;
private readonly Vector2 texCoord;
public ColoredVertex(Vector2 position, Vector2 texCoord)
{
this.position = position;
this.texCoord = texCoord;
}
}
sealed class VertexBuffer<TVertex>
where TVertex : struct // vertices must be structs so we can copy them to GPU memory easily
{
private readonly int vertexSize;
private TVertex[] vertices = new TVertex[4];
private int count;
private readonly int handle;
public VertexBuffer(int vertexSize)
{
this.vertexSize = vertexSize;
// generate the actual Vertex Buffer Object
this.handle = GL.GenBuffer();
}
public void AddVertex(TVertex v)
{
// resize array if too small
if (this.count == this.vertices.Length)
Array.Resize(ref this.vertices, this.count * 2);
// add vertex
this.vertices[count] = v;
this.count++;
}
public void Bind()
{
// make this the active array buffer
GL.BindBuffer(BufferTarget.ArrayBuffer, this.handle);
}
public void BufferData()
{
// copy contained vertices to GPU memory
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(this.vertexSize * this.count),
this.vertices, BufferUsageHint.StreamDraw);
}
public void Draw()
{
// draw buffered vertices as triangles
GL.DrawArrays(PrimitiveType.Quads, 0, this.count);
}
}
public class ShaderException : Exception
{
public ShaderException(string message) : base(message) { }
}
sealed class Shader
{
private readonly int handle;
public int Handle { get { return this.handle; } }
public Shader(ShaderType type, string code)
{
// create shader object
this.handle = GL.CreateShader(type);
// set source and compile shader
GL.ShaderSource(this.handle, code);
GL.CompileShader(this.handle);
string info;
GL.GetShaderInfoLog(this.handle, out info);
if (!string.IsNullOrEmpty(info))
throw new ShaderException(info);
}
}
sealed class ShaderProgram
{
private readonly int handle;
public ShaderProgram(params Shader[] shaders)
{
// create program object
this.handle = GL.CreateProgram();
// assign all shaders
foreach (var shader in shaders)
GL.AttachShader(this.handle, shader.Handle);
// link program (effectively compiles it)
GL.LinkProgram(this.handle);
string info;
GL.GetProgramInfoLog(handle, out info);
if (!string.IsNullOrEmpty(info))
throw new ShaderException(info);
// detach shaders
foreach (var shader in shaders)
{
GL.DetachShader(this.handle, shader.Handle);
}
}
public void Use()
{
// activate this program to be used
GL.UseProgram(this.handle);
}
public int GetAttributeLocation(string name)
{
// get the location of a vertex attribute
return GL.GetAttribLocation(this.handle, name);
}
public int GetUniformLocation(string name)
{
// get the location of a uniform variable
return GL.GetUniformLocation(this.handle, name);
}
}
sealed class VertexArray<TVertex>
where TVertex : struct
{
private readonly int handle;
public VertexArray(VertexBuffer<TVertex> vertexBuffer, ShaderProgram program,
params VertexAttribute[] attributes)
{
// create new vertex array object
GL.GenVertexArrays(1, out this.handle);
// bind the object so we can modify it
this.Bind();
// bind the vertex buffer object
vertexBuffer.Bind();
// set all attributes
foreach (var attribute in attributes)
attribute.Set(program);
// unbind objects to reset state
GL.BindVertexArray(0);
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
}
public void Bind()
{
// bind for usage (modification or rendering)
GL.BindVertexArray(this.handle);
}
}
sealed class VertexAttribute
{
private readonly string name;
private readonly int size;
private readonly VertexAttribPointerType type;
private readonly bool normalize;
private readonly int stride;
private readonly int offset;
public VertexAttribute(string name, int size, VertexAttribPointerType type,
int stride, int offset, bool normalize = false)
{
this.name = name;
this.size = size;
this.type = type;
this.stride = stride;
this.offset = offset;
this.normalize = normalize;
}
public void Set(ShaderProgram program)
{
// get location of attribute from shader program
int index = program.GetAttributeLocation(this.name);
// enable and set attribute
GL.EnableVertexAttribArray(index);
GL.VertexAttribPointer(index, this.size, this.type,
this.normalize, this.stride, this.offset);
}
}
The PNG file (texture) I'm using for testing looks like this:
C:\Users\Benjamin Marty\Desktop\Test.png
When I run this program and click on the form, I get what I expect:
Now, I change the program to use ivec2
texture coordinates whose components are of type Int
instead of Float
. First I update my vertex buffer code to use pixel coordinates in Point
structures instead of using float
-based Vector2
structures:
VertexBuffer<ColoredVertex> vb = new VertexBuffer<ColoredVertex>(ColoredVertex.Size);
vb.AddVertex(new ColoredVertex(new Vector2() { X = 0, Y = 0 }, new Point() { X = 32, Y = 32 }));
vb.AddVertex(new ColoredVertex(new Vector2() { X = 32, Y = 0 }, new Point() { X = 64, Y = 32 }));
vb.AddVertex(new ColoredVertex(new Vector2() { X = 32, Y = 32 }, new Point() { X = 64, Y = 64 }));
vb.AddVertex(new ColoredVertex(new Vector2() { X = 0, Y = 32 }, new Point() { X = 32, Y = 64 }));
VertexBuffer<ColoredVertex> vb2 = new VertexBuffer<ColoredVertex>(ColoredVertex.Size);
vb2.AddVertex(new ColoredVertex(new Vector2() { X = 16, Y = 16 }, new Point() { X = 64, Y = 64 }));
vb2.AddVertex(new ColoredVertex(new Vector2() { X = 48, Y = 16 }, new Point() { X = 96, Y = 64 }));
vb2.AddVertex(new ColoredVertex(new Vector2() { X = 48, Y = 48 }, new Point() { X = 96, Y = 96 }));
vb2.AddVertex(new ColoredVertex(new Vector2() { X = 16, Y = 48 }, new Point() { X = 64, Y = 96 }));
Then I update the ColoredVertex
structure to accept Point
s instead of Vector2
s for texture coordinates:
struct ColoredVertex
{
public const int Size = (2 + 2) * 4; // size of struct in bytes
private readonly Vector2 position;
private readonly Point texCoord;
public ColoredVertex(Vector2 position, Point texCoord)
{
this.position = position;
this.texCoord = texCoord;
}
}
I change the vTexCoord
vertex attribute information to transfer Int
s instead of Float
s:
VertexAttribute vac = new VertexAttribute("vTexCoord", 2, VertexAttribPointerType.Int, ColoredVertex.Size, 2 * 4);
And finally I change my shaders to use ivec2
, and flat ivec2
texture coordinate variables instead of vec2
. I also change my texture
call to texelFetch
and add the appropriate 0
parameter to reference LOD 0.
Shader vshader = new Shader(ShaderType.VertexShader,
@"#version 130
// a projection transformation to apply to the vertex' position
uniform mat4 projectionMatrix;
// attributes of our vertex
in vec2 vPosition;
in ivec2 vTexCoord;
flat out ivec2 vTex; // must match name in fragment shader
void main()
{
// gl_Position is a special variable of OpenGL that must be set
gl_Position = projectionMatrix * vec4(vPosition, -2.0, 1.0);
vTex = vTexCoord;
}");
Shader fshader = new Shader(ShaderType.FragmentShader,
@"#version 130
flat in ivec2 vTex; // must match name in vertex shader
out vec4 fragColor; // first out variable is automatically written to the screen
uniform sampler2D tex;
void main()
{
fragColor = texelFetch(tex, vTex, 0);
}");
After all this, the result I get has something wrong because the texture is all black:
What am I missing?
Edit
I tried changing the Set
method of the VertexAttribute
class to the following code in an attempt to use VertexAttribIPointer
instead of VertexAttribPointer
when appropriate in this test cast:
public void Set(ShaderProgram program)
{
// get location of attribute from shader program
int index = program.GetAttributeLocation(this.name);
// enable and set attribute
GL.EnableVertexAttribArray(index);
if (type == VertexAttribPointerType.Int)
GL.VertexAttribIPointer(index, size, VertexAttribIntegerType.Int, stride, IntPtr.Add(IntPtr.Zero, offset));
else
GL.VertexAttribPointer(index, this.size, this.type,
this.normalize, this.stride, this.offset);
}
The result had an effect, but not the desired effect: