Helpers for OpenGL in Elixir

Ian Harris

Helpers

There are a few things that would make our lives easier when working with OpenGL in Elixir. I’m not going to bother wrapping the wx and gl modules just to avoid using the :atom syntax, but there are a few things that will make our lives easier.

Shaders

Writing shaders in charlists embedded in Elixir is awkward and error prone, so let’s read them from files.

defmodule Shader do
  defstruct id: nil
  @type t :: %__MODULE__{
    id: non_neg_integer(),
  }

  @spec new(Path.t(), Path.t()) :: Shader.t()
  def new(vertex_path, fragment_path) do
    vertex_source = File.read!(vertex_path) <> <<0>>
    fragment_source = File.read!(fragment_path) <> <<0>>

    vertex_shader = :gl.createShader(:gl_const.gl_vertex_shader)
    fragment_shader = :gl.createShader(:gl_const.gl_fragment_shader)

    :gl.shaderSource(vertex_shader, [vertex_source])
    :gl.compileShader(vertex_shader)

    success = :gl.getShaderiv(vertex_shader, :gl_const.gl_compile_status)

    unless success do
      res = :gl.getShaderInfoLog(vertex_shader, 512)
      IO.puts("vs_res: #{res}")
    end

    :gl.shaderSource(fragment_shader, [fragment_source])
    :gl.compileShader(fragment_shader)

    success = :gl.getShaderiv(fragment_shader, :gl_const.gl_compile_status)

    unless success do
      res = :gl.getShaderInfoLog(fragment_shader, 512)
      IO.puts("fs_res: #{res}")
    end

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

    success = :gl.getProgramiv(id, :gl_const.gl_link_status)

    unless success do
      res = :gl.getProgramInfoLog(id, 512)
      IO.puts("link res: #{res}")
    end

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

    %Shader{
      id: id,
    }
  end
end

This is all that you really need, but I’m going to make it a little more Elixiry by adding use/1 and uniform/3:

def use(%Shader{id: id}) do
  :gl.useProgram(id)
end

def uniform(%Shader{id: id}, name, true) do
  location = :gl.getUniformLocation(id, name)
  :gl.uniform1i(location, :gl_const.gl_true)
end

def uniform(%Shader{id: id}, name, false) do
  location = :gl.getUniformLocation(id, name)
  :gl.uniform1i(location, :gl_const.gl_false)
end

def uniform(%Shader{id: id}, name, val) when is_integer(val) do
  location = :gl.getUniformLocation(id, name)
  :gl.uniform1i(location, val)
end

def uniform(%Shader{id: id}, name, val) when is_float(val) do
  location = :gl.getUniformLocation(id, name)
  :gl.uniform1f(location, val)
end

We can add the rest of the functions, but we need to make some decisions about what the default number types should be (easy enough), and figure out how we’re going to deal with matrices and vectors. 1

Bits

One of the awkward things in the triangle example is this:

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)

I haven’t decided the best way to deal with this. I think a macro is nice, since the type (and size) will change, but then we can break things a little too easily. We’ll use a function, make_bits/3 for this:

defp make_bits(list, :float, bits \\ 32) do
  list
  |> Enum.reduce(<<>>, fn el, acc -> acc <> <<el::float-native-size(bits)>> end)
end

For now, this is the only way we’re using it, so a shorter helper would be nice:

defp make_bits(list) do
  make_bits(list, :float)
end

Matrices

Vectors


  1. foo↩︎