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!
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.
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
= [size: {800, 600}]
opts
= :wx.new()
wx
= :wxFrame.new(wx, :wx_const.wx_id_any, 'Elixir OpenGL', opts)
frame
: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
]
]
= :wxGLCanvas.new(frame, opts ++ gl_attrib)
canvas = :wxGLContext.new(canvas)
ctx :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, [], [])
.
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:
# ...
(self(), :update)
send
# ...
We’ll need a handler for that, which looks like this:
@impl :wx_object
def handle_info(:update, state) do
(state)
render
{:noreply, state}
end
Our render/1
is pretty straight forward:
defp render(%{canvas: canvas} = state) do
(state)
draw:wxGLCanvas.swapBuffers(canvas)
(self(), :update)
send
:ok
end
This is a very basic render loop. We just draw, swap the buffers, and loop. Now we can start drawing some stuff.
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
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
= '#version 330 core
vertex_source layout (location = 0) in vec3 aPos;
void main() {
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}\0'
= '#version 330 core
fragment_source out vec4 FragColor;
void main() {
FragColor = vec4(0.44f, 0.35f, 0.5f, 1.0f);
}\0'
= :gl.createShader(:gl_const.gl_vertex_shader)
vertex_shader :gl.shaderSource(vertex_shader, [vertex_source])
:gl.compileShader(vertex_shader)
= :gl.createShader(:gl_const.gl_fragment_shader)
fragment_shader :gl.shaderSource(fragment_shader, [fragment_source])
:gl.compileShader(fragment_shader)
= :gl.createProgram()
shader_program :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()
(self(), :update) # this should already be here
send
{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)
We should finally have a somewhat Elixir-y looking window now!
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.