andy@andyleclair.dev$>_

OpenGL Part 3

Layering shapes, displaying FPS

Related Listening

Posted on 2024-09-11

Continuing on, I wanted to try the exercises at the bottom of this chapter, and this is what I came up with.

defmodule GlTest.Window do
  import WxRecords

  @behaviour :wx_object

  def start_link(_) do
    :wx_object.start_link(__MODULE__, [], [])
    {:ok, self()}
  end

  @impl :wx_object
  def init(_) do
    opts = [size: {800, 600}]
    wx = :wx.new()
    frame = :wxFrame.new(wx, :wx_const.wx_id_any(), ~c"Hello", opts)

    :wxWindow.connect(frame, :close_window)
    :wxFrame.show(frame)

    gl_attrib = [
      attribList: [
        :wx_const.wx_gl_core_profile(),
        :wx_const.wx_gl_major_version(),
        3,
        :wx_const.wx_gl_minor_version(),
        3,
        :wx_const.wx_gl_doublebuffer(),
        0
      ]
    ]

    canvas = :wxGLCanvas.new(frame, opts ++ gl_attrib)
    ctx = :wxGLContext.new(canvas)
    :wxGLCanvas.setCurrent(canvas, ctx)

    {shader_program, vao1, vao2, rect_vao} = init_opengl()
    frame_counter = :counters.new(1, [:atomics])

    send(self(), :update)
    now = System.monotonic_time(:millisecond)

    {frame,
     %{
       last_time: now,
       frame: frame,
       frame_counter: frame_counter,
       canvas: canvas,
       shader_program: shader_program,
       fps: 0,
       vao1: vao1,
       vao2: vao2,
       rect_vao: rect_vao
     }}
  end

  @vertex_source """
                 #version 330 core
                 layout (location = 0) in vec3 aPos;
                 void main() {
                 gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
                 }\0
                 """
                 |> String.to_charlist()

  @fragment_source """
                   #version 330 core
                   out vec4 FragColor;
                   void main() {
                   FragColor = vec4(0.44f, 0.35f, 0.5f, 1.0f);
                   }\0
                   """
                   |> String.to_charlist()

  def init_opengl() do
    vertex_shader = :gl.createShader(:gl_const.gl_vertex_shader())
    :gl.shaderSource(vertex_shader, [@vertex_source])
    :gl.compileShader(vertex_shader)

    fragment_shader = :gl.createShader(:gl_const.gl_fragment_shader())
    :gl.shaderSource(fragment_shader, [@fragment_source])
    :gl.compileShader(fragment_shader)

    shader_program = :gl.createProgram()
    :gl.attachShader(shader_program, vertex_shader)
    :gl.attachShader(shader_program, fragment_shader)
    :gl.linkProgram(shader_program)

    :gl.deleteShader(vertex_shader)
    :gl.deleteShader(fragment_shader)

    vertices = triangle_vertices()
    vertices_2 = triangle_vertices_2()

    [vao1, vao2, rect_vao] = :gl.genVertexArrays(3)
    [vbo1, vbo2, rect_vbo, ebo] = :gl.genBuffers(4)

    for {vertex_array, vertex_buffer, vertices} <- [
          {vao1, vbo1, vertices},
          {vao2, vbo2, vertices_2}
        ] do
      :gl.bindVertexArray(vertex_array)

      :gl.bindBuffer(:gl_const.gl_array_buffer(), vertex_buffer)

      :gl.bufferData(
        :gl_const.gl_array_buffer(),
        byte_size(vertices),
        vertices,
        :gl_const.gl_static_draw()
      )

      :gl.vertexAttribPointer(
        0,
        3,
        :gl_const.gl_float(),
        :gl_const.gl_false(),
        3 * byte_size(<<0.0::float-size(32)>>),
        0
      )

      :gl.enableVertexAttribArray(0)

      :gl.bindBuffer(:gl_const.gl_array_buffer(), 0)

      :gl.bindVertexArray(0)
    end

    rect_vertices = rectangle_vertices()
    rect_indices = rectangle_indices()

    :gl.bindVertexArray(rect_vao)
    :gl.bindBuffer(:gl_const.gl_array_buffer(), rect_vbo)

    :gl.bufferData(
      :gl_const.gl_array_buffer(),
      byte_size(rect_vertices),
      rect_vertices,
      :gl_const.gl_static_draw()
    )

    :gl.bindBuffer(:gl_const.gl_element_array_buffer(), ebo)

    :gl.bufferData(
      :gl_const.gl_element_array_buffer(),
      byte_size(rect_indices),
      rect_indices,
      :gl_const.gl_static_draw()
    )

    :gl.vertexAttribPointer(
      0,
      3,
      :gl_const.gl_float(),
      :gl_const.gl_false(),
      3 * byte_size(<<0.0::float-size(32)>>),
      0
    )

    :gl.enableVertexAttribArray(0)
    {shader_program, vao1, vao2, rect_vao}
  end

  @triangle_vertices [
                       [0.0, 1.0, 0.0],
                       [1.0, 0.0, 0.0],
                       [1.0, 1.0, 0.0]
                     ]
                     |> List.flatten()
                     |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end)
  def triangle_vertices do
    @triangle_vertices
  end

  @triangle_vertices_2 [
                         [-0.5, -0.5, 0.0],
                         [0.5, -0.5, 0.0],
                         [0.0, 0.5, 0.0]
                       ]
                       |> List.flatten()
                       |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end)
  def triangle_vertices_2 do
    @triangle_vertices_2
  end

  @rectangle_vertices [
                        [0.5, 0.5, 0.0],
                        [0.5, -0.5, 0.0],
                        [-0.5, -0.5, 0.0],
                        [-0.5, 0.5, 0.0]
                      ]
                      |> List.flatten()
                      |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end)

  def rectangle_vertices do
    @rectangle_vertices
  end

  @rectangle_indices [[0, 1, 3], [1, 2, 3]]
                     |> List.flatten()
                     |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::native-size(32)>> end)
  def rectangle_indices do
    @rectangle_indices
  end

  @impl :wx_object
  def handle_event(wx(event: wxClose()), state) do
    {:stop, :normal, state}
  end

  @impl :wx_object
  def handle_info(:stop, %{canvas: canvas, fps_counter_label: fps_counter_label} = state) do
    :wxGLCanvas.destroy(canvas)
    :wxStaticText.destroy(fps_counter_label)

    {:stop, :normal, state}
  end

  @impl :wx_object
  def handle_info(:update, state) do
    state = render(state)

    {:noreply, state}
  end

  defp render(%{canvas: canvas} = state) do
    state =
      state
      |> update_frame_counter()
      |> draw()

    :wxGLCanvas.swapBuffers(canvas)
    send(self(), :update)

    state
  end

  defp draw(%{frame: frame} = state) do
    :gl.clearColor(0.2, 0.1, 0.3, 1.0)
    :gl.clear(:gl_const.gl_color_buffer_bit())

    :gl.useProgram(state.shader_program)

    :gl.bindVertexArray(state.vao1)
    :gl.drawArrays(:gl_const.gl_triangles(), 0, 3)

    :gl.bindVertexArray(state.vao2)
    :gl.drawArrays(:gl_const.gl_triangles(), 0, 3)

    :gl.polygonMode(:gl_const.gl_front_and_back(), :gl_const.gl_line())
    :gl.bindVertexArray(state.rect_vao)
    :gl.drawElements(:gl_const.gl_triangles(), 6, :gl_const.gl_unsigned_int(), 0)
    :gl.polygonMode(:gl_const.gl_front_and_back(), :gl_const.gl_fill())

    :wxWindow.setLabel(frame, ~c"FPS: #{state.fps}")

    state
  end

  def update_frame_counter(%{last_time: last_time, frame_counter: frame_counter} = state) do
    now = System.monotonic_time(:millisecond)
    elapsed = now - last_time

    if elapsed > 100 do
      frames = :counters.get(frame_counter, 1)
      fps = (frames / elapsed * 1000) |> round()
      :counters.put(frame_counter, 1, 0)
      Map.merge(state, %{fps: fps, last_time: now})
    else
      :counters.add(frame_counter, 1, 1)
      state
    end
  end

  def child_spec(opts) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [opts]},
      restart: :permanent
    }
  end
end

I was reading some of the related links and I saw this article with a bit about adding a FPS counter, and I thought that would be a fun exercise to try.

I’m using Erlang’s :counters module to keep track of the number of frames rendered, and then every 100-ish ms I calculate the FPS and update the window title with the FPS. Then I zero out the frame counter.

I tried to layer a WxWidgets StaticText on top of the GLCanvas to display the FPS, but I couldn’t get it to show up. That took me to this thread, which says that if you’re using OpenGL in Wx, it’s _special_and that I should just avoid trying to mix the two. Eventually I will learn how to do it with OpenGL, which I think involves rendering the text as a bitmap?

Last post, I mentioned that I didn’t like how the formatter handled the vertexes, and I think I found a better way to do it this time. I’m using a list of lists of floats, flattening it, and then reducing it into a binary. It’s a bit more verbose, but I think it’s easier to read and understand, and since I’ve pushed it into a module attribute, it only gets evaluated at compile time.