Overview

For the first blog post, let’s just draw a triangle. I’m not going to explain much here, since there are better resources available. This is my first time working with Elixir, Erlang, (modern) OpenGL, GLSL, and wxWidgets, so I’m sure there are better ways to do just about everything here. Please let me know what I can improve!

Getting started

To get started, let’s make a new project:

$ mix new elixir_opengl
$ cd elixir_opengl

In our mix.exs, we need to add :wx to our extra_applications:

# ...
def application do
  [
    extra_applications: [:logger, :wx]
  ]
end
# ...

That’s it for now, as far as dependencies and project setup goes.

wxErlang

Now we can start with wxErlang. The first issue we run into, as noted in Will Fleming’s blog post, is that wxErlang makes extensive use of macros. We’ll be using the same pattern as outlined there, which is to export Erlang functions that just return the value of Erlang macros. It’s not pretty, and it’s a bit annoying to work with, but for now it’ll do.

$ mkdir src

And we’ll use two separate Erlang modules, wx_const and gl_const. We’ll start with src/wx_const.erl:

-module(wx_const).
-compile(nowarn_export_all).
-compile(export_all).

-include_lib("wx/include/wx.hrl").

wx_id_any() -> ?wxID_ANY.

wx_gl_core_profile() -> ?WX_GL_CORE_PROFILE.
wx_gl_major_version() -> ?WX_GL_MAJOR_VERSION.
wx_gl_minor_version() -> ?WX_GL_MINOR_VERSION.
wx_gl_doublebuffer() -> ?WX_GL_DOUBLEBUFFER.

And src/gl_const.erl:

-module(gl_const).
-compile(nowarn_export_all).
-compile(export_all).

-include_lib("wx/include/gl.hrl").

gl_vertex_shader() -> ?GL_VERTEX_SHADER.
gl_fragment_shader() -> ?GL_FRAGMENT_SHADER.

gl_array_buffer() -> ?GL_ARRAY_BUFFER.

gl_static_draw() -> ?GL_STATIC_DRAW.

gl_float() -> ?GL_FLOAT.

gl_false() -> ?GL_FALSE.

gl_color_buffer_bit() -> ?GL_COLOR_BUFFER_BIT.

gl_triangles() -> ?GL_TRIANGLES.

There’s one other thing that’s useful, which is defining records for all the wxErlang events. It’s not necessary, and doesn’t improve much for this simple example, but it will make expanding things much nicer. In lib/wx_records.ex:

defmodule WxRecords do
  require Record

  for {type, record} <- Record.extract_all(from_lib: "wx/include/wx.hrl") do
    Record.defrecord(type, record)
  end
end

With that, we can create a window and get the OpenGL canvas set up.

defmodule ElixirOpengl do
  import WxRecords

  @behaviour :wx_object

  @impl :wx_object
  def init(_config) do
    opts = [size: {800, 600}]

    wx = :wx.new()

    frame = :wxFrame.new(wx, :wx_const.wx_id_any, 'Elixir OpenGL', 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)

    {frame, %{
        frame: frame,
        canvas: canvas,
    }}
  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} = state) do
    :wxGLCanvas.destroy(canvas)

    {:stop, :normal, state}
  end
end

This gives us a basic window, with a double buffered OpenGL 3.3 Core Profile canvas. To run the program, we can start iex with iex -S mix in the root of the project, and then call :wx_object.start_link(ElixirOpengl, [], []).

blank window

Render loop

It’s just a blank window for now, so let’s add our render loop. We’ll be utilizing messages to do this, so let’s add a call in init/1 to start it off by adding this just before we return:

# ...

send(self(), :update)

# ...

We’ll need a handler for that, which looks like this:

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

  {:noreply, state}
end

Our render/1 is pretty straight forward:

defp render(%{canvas: canvas} = state) do
  draw(state)
  :wxGLCanvas.swapBuffers(canvas)
  send(self(), :update)

  :ok
end

This is a very basic render loop. We just draw, swap the buffers, and loop. Now we can start drawing some stuff.

OpenGL

Let’s draw our background:

defp draw(state) do
  :gl.clearColor(0.2, 0.1, 0.3, 1.0)
  :gl.clear(:gl_const.gl_color_buffer_bit)

  :ok
end
blank window with purple background

Now we should have a nice (Elixir-ish) shade of purple.

For this simple example, we’ll be embedding our GLSL directly in the program, but we could easily read the data from a file.

defp init_opengl() do
  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'

  fragment_source = '#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(0.44f, 0.35f, 0.5f, 1.0f);
}\0'
  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 = [
    -0.5, -0.5, 0.0,
     0.5, -0.5, 0.0,
     0.0,  0.5, 0.0,
  ] |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(32)>> end)

  [vao] = :gl.genVertexArrays(1)
  [vbo] = :gl.genBuffers(1)

  :gl.bindVertexArray(vao)

  :gl.bindBuffer(:gl_const.gl_array_buffer, vbo)
  :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)

  {shader_program, vao}
end

We’ll get shader_program and vao and keep them in the program’s state. Update the end of init/1 like so:

{shader_program, vao} = init_opengl()

send(self(), :update) # this should already be here

{frame, %{
  frame: frame,
  canvas: canvas,
  shader_program: shader_program,
  vao: vao,
}}

And finally, to use the shader program and draw a triangle, update draw/1:

:gl.useProgram(state.shader_program)

:gl.bindVertexArray(state.vao)
:gl.drawArrays(:gl_const.gl_triangles, 0, 3)
off-purple triangle with purple background

We should finally have a somewhat Elixir-y looking window now!

Conclusion

There are some subtleties that are easy to get wrong, and debugging things is challenging, especially on macOS. I’m going to go into more detail about how everything works in another post, but for anyone that is comfortable with Elixir, Erlang, wxWidgets, OpenGL, and GLSL, it should be mostly self-explanatory.

You can find the completed code on GitHub.