Search code examples
openglf#opentk

Color data not used in OpenGL "Hello world"


As usual when I try to get my fingers wet with modern OpenGL, using one of the clever demos I can find on some blogs, something goes wrong.

Expected behavior: Draw a triangle and the colors should be interpolated between the 3 vertices.

Found behavior: Triangle is red. And it does not matter which colors I write into the color array.

Code first (sorry 160 lines - modern OpenGL is spammy...).

open System
open System.Drawing
open System.Collections.Generic

open OpenTK
open OpenTK.Graphics
open OpenTK.Graphics.OpenGL
open OpenTK.Input

module Shaders =
    let vertexShader = 
        """#version 330

in vec3 vPosition;
in  vec3 vColor;
out vec4 color;
uniform mat4 modelview;

void
main()
{
    gl_Position = modelview * vec4(vPosition, 1.0);

    color = vec4( vColor, 1.0);
}
"""
    let fragmentShader = 
        """#version 330

in vec4 color;
out vec4 outputColor;

void
main()
{
  outputColor = color;
}
"""
    let initShaders () : int =
        let makeShader shaderType src =
            let sh = GL.CreateShader(shaderType)
            GL.ShaderSource(sh,src)
            GL.CompileShader(sh)
            sh
        let pgmId = GL.CreateProgram()
        let vsh = makeShader ShaderType.VertexShader vertexShader
        let fsh = makeShader ShaderType.FragmentShader fragmentShader
        GL.AttachShader(pgmId,vsh)
        GL.AttachShader(pgmId,fsh)
        GL.LinkProgram(pgmId)
        pgmId

let failMinusOne = function
    | -1 -> failwith "Something is -1 which should not be -1!"
    | x -> x

type Game(width,height) =
    inherit GameWindow(width, height, GraphicsMode.Default, "F# OpenTK Sample")
    do base.VSync <- VSyncMode.On

    let mutable shaderProgramId = -1
    let mutable attribute_vcol = -1
    let mutable attribute_vpos = -1
    let mutable uniform_mview = -1

    let mutable vbo_col = 0
    let mutable vbo_pos = 0

    let vertex_data = 
        [|
            Vector3(-0.8f, -0.8f, 0.f)
            Vector3( 0.8f, -0.8f, 0.f)
            Vector3( 0.f,  0.8f, 0.f)
        |]

    let col_data =
        [|
            Vector3( 1.f, 1.f, 1.f)
            Vector3( 0.f, 0.f, 1.f)
            Vector3( 0.f, 1.f, 0.f)
        |]

    let mview_data = [| Matrix4.Identity |]

    /// <summary>Load resources here.</summary>
    /// <param name="e">Not used.</param>
    override o.OnLoad e =
        base.OnLoad(e)
        o.Title <- "Hello OpenTK!"
        shaderProgramId <- Shaders.initShaders ()

        GL.ClearColor(Color.CornflowerBlue)
        GL.Enable(EnableCap.DepthTest)

        attribute_vpos <- GL.GetAttribLocation(shaderProgramId, "vPosition") |> failMinusOne 
        attribute_vcol <- GL.GetAttribLocation(shaderProgramId, "vColor") |> failMinusOne
        uniform_mview <- GL.GetUniformLocation(shaderProgramId, "modelview") |> failMinusOne

        vbo_col <- GL.GenBuffer()
        vbo_pos <- GL.GenBuffer()

    /// <summary>
    /// Called when your window is resized. Set your viewport here. It is also
    /// a good place to set up your projection matrix (which probably changes
    /// along when the aspect ratio of your window).
    /// </summary>
    /// <param name="e">Not used.</param>
    override o.OnResize e =
        base.OnResize e
        GL.Viewport(base.ClientRectangle.X, base.ClientRectangle.Y, base.ClientRectangle.Width, base.ClientRectangle.Height)
        let projection = Matrix4.CreatePerspectiveFieldOfView(float32 (Math.PI / 4.), float32 base.Width / float32 base.Height, 1.f, 64.f)
        GL.MatrixMode(MatrixMode.Projection)
        GL.LoadMatrix(ref projection)


    /// <summary>
    /// Called when it is time to setup the next frame. Add you game logic here.
    /// </summary>
    /// <param name="e">Contains timing information for framerate independent logic.</param>
    override o.OnUpdateFrame e =
        base.OnUpdateFrame e
        if base.Keyboard.[Key.Escape] then base.Close()
        else
            GL.BindBuffer(BufferTarget.ArrayBuffer,vbo_col)
            GL.BufferData<Vector3>(BufferTarget.ArrayBuffer,Array.length col_data * Vector3.SizeInBytes,col_data,BufferUsageHint.StaticDraw)
            GL.VertexAttribPointer(attribute_vcol, 3, VertexAttribPointerType.Float, false, 0, 0)

            GL.BindBuffer(BufferTarget.ArrayBuffer,vbo_pos)
            GL.BufferData<Vector3>(BufferTarget.ArrayBuffer,Array.length vertex_data * Vector3.SizeInBytes,vertex_data,BufferUsageHint.StaticDraw )
            GL.VertexAttribPointer(attribute_vpos, 3, VertexAttribPointerType.Float, false, 0, 0)

            GL.UniformMatrix4(uniform_mview,false,ref mview_data.[0])
            GL.BindBuffer(BufferTarget.ArrayBuffer,0)

            GL.UseProgram(shaderProgramId)

    /// <summary>
    /// Called when it is time to render the next frame. Add your rendering code here.
    /// </summary>
    /// <param name="e">Contains timing information.</param>
    override o.OnRenderFrame(e) =
        base.OnRenderFrame e
        GL.Clear(ClearBufferMask.ColorBufferBit ||| ClearBufferMask.DepthBufferBit);
        GL.EnableVertexArrayAttrib(attribute_vpos,0)
        GL.EnableVertexArrayAttrib(attribute_vcol,0)

        GL.DrawArrays(PrimitiveType.Triangles,0,Array.length vertex_data)

        GL.DisableVertexArrayAttrib(attribute_vpos,0)
        GL.DisableVertexArrayAttrib(attribute_vcol,0)

        GL.Flush()
        base.SwapBuffers()

[<EntryPoint>]
let main argv = 
    use game = new Game(800,600)
    do game.Run(30.,30.)
    0 // return an integer exit code

After a few hours trying to find what is going wrong I ran out of ideas. Adding more triangles also seems to flop. But then, the fact that the triangle shows as the vertices suggest, makes me think, the download of the data to the gpu is okay-ish.

But for you guys who do that day in day out, it will probably be easy to spot where things go wrong.


Solution

  • You are using an OpenGL 4.5 function, EnableVertexArrayAttrib, to enable your vertex attrib array on vertex array object (VAO) with ID 0. This is peculiar, since your shaders are versioned as 330, which is much, much older, but more importantly invalid, since you aren't using any VAO.

    You can enable/disable the vertex attrib arrays the classical way, like this:

    GL.EnableVertexAttribArray(attribute_vpos)
    GL.EnableVertexAttribArray(attribute_vcol)
    
    GL.DisableVertexAttribArray(attribute_vpos)
    GL.DisableVertexAttribArray(attribute_vcol)
    

    This results in the properly colored triangle on my NVidia GTX 760 desktop, because it acts on the currently active vertex array information.

    I would advise to have a second look at the way you are using the state machine. Enabling vertex arrays and defining their structure via VertexAttribPointer is program-specific and belongs together. Usually, you'd use a VAO to group this information and unbind it after drawing finishes. If the vertex attrib arrays are supposed to be global state, there's no point in disabling them, and this fact should be well-documented. As is, the code is in danger of developing spurious interactions between seemingly unrelated functions, because it's sharing complex state of the OpenGL state machine.

    A possible approach would be:

    • Setup

      • Create shaders, keep shader program handle and attrib locations

      • Create buffers, initialize data, then save their handles and leave them unbound

      • Create vertex array structure as VAO, then save its handle and leave it unbound

        • For each vertex array, enable and specify (e.g. VertexAttribPointer)
    • Drawing

      • Bind VAO
      • Bind changing/additional buffers (if necessary)
      • Bind program
      • Draw call
      • Unbind everything again (to fail fast if something wrongly relies on OpenGL state)

    Approaches like this group the interaction with the state machine in a more structured way, and reduce reliance of your program on OpenGL state machine values remaining unchanged.

    There's no need for the various mutable values and their initialization to -1. They can be bound in order, directly assigning the correct handle (assuming the OpenGL context is already present; see comments.)