0
\$\begingroup\$

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

C:\Users\Benjamin Marty\Desktop\Test.png

When I run this program and click on the form, I get what I expect:

Screenshot with texture function

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 Points instead of Vector2s 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 Ints instead of Floats:

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: Screenshot with texelFetch

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:

Screenshot after switching to VertexAttribIPointer

\$\endgroup\$
1
\$\begingroup\$

I'm not sure about C#/OpenTK, but in OpenGL when specifying an integer vertex attribute you must use glVertexAttribIPointer, not glVertexAttribPointer; see https://www.opengl.org/sdk/docs/man4/html/glVertexAttribPointer.xhtml

For glVertexAttribPointer ... values will be converted to floats directly without normalization.

For glVertexAttribIPointer ... Values are always left as integer values.

I expect that OpenTK has an equivalent.

\$\endgroup\$
  • \$\begingroup\$ I'm confused because the documentation at opengl.org/sdk/docs/man4/html/glVertexAttribPointer.xhtml says "Specifies the data type of each component in the array. The symbolic constants GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, and GL_UNSIGNED_INT are accepted by glVertexAttribPointer and glVertexAttribIPointer." And the normalized parameter controls whether values are normalized: "For glVertexAttribPointer, specifies whether fixed-point data values should be normalized (GL_TRUE) or converted directly as fixed-point values (GL_FALSE) when they are accessed." \$\endgroup\$ – BlueMonkMN Oct 30 '16 at 1:58
  • \$\begingroup\$ @BlueMonkMN - the part you're missing is that these data types are also converted to floats when using glVertexAttribPointer; to have them left as integers you must use glVertexAttribIPointer. \$\endgroup\$ – Maximus Minimus Oct 30 '16 at 12:48
  • \$\begingroup\$ All the overloads of GL.VertexAttribIPointer take IntPtr or references to a generic type. I don't understand how to specify the offset in those terms. \$\endgroup\$ – BlueMonkMN Oct 30 '16 at 12:54
  • \$\begingroup\$ It turns out that VertexAttribIPointer does work in my edit to the question, but, because ivec2 must be flat (according to compiler errors I was getting), will only allow a single texture pixel color to be used for a whole primitive. Therefore I decided to abandon VertexAttribIPointer and just postpone the type-casting to ivec2 until immediately before texelFetch. \$\endgroup\$ – BlueMonkMN Oct 30 '16 at 14:50
0
\$\begingroup\$

To allow the fragment shader to interpolate texture coordinates between the vertices, it was necessary to figure out how to remove the flatness of the vectors being communicated between the vector shader and the fragment shader. One way to do this is to make vec2 copies of the ivec2 structures to allow interpolation between the shaders, then type-cast back to ivec2 in the fragment shader. But why bother casting to float and then back to int when all I'm really trying to accomplish is to use non-normalized coordinates. I can eliminate the need to use VertexAttribIPointer altogether and just postpone the casting to ivec2 until the fragment shader. I end up with the original vertex shader that was dealing only with vec2 structures, and the following fragment shader:

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 = texelFetch(tex, ivec2(vTex), 0);
    }");
\$\endgroup\$
  • \$\begingroup\$ I ended up not using texelFetch, BTW. It turns out I did want some interpolation. If you use texelFetch and rotate an image, the pixels are not anti-aliased. They look better when anti-aliased and sampled from surrounding pixels. My problem was that logic was picking up pixels from neighboring textures. My way around that was to use sampler2DArray and 2d texture arrays instead of putting all my images in one texture. \$\endgroup\$ – BlueMonkMN Nov 5 '16 at 13:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.