OpenGL Part 2
Drawing a triangle
Related Listening
Posted on 2024-09-10
Following part one, now I’d like to actually draw some geometry, so let’s do the ‘ol classic and draw a triangle. Everyone loves a triangle. The journey of a thousand triangles begins with a single triangle.
This post is basically porting the C++ from this learnopengl.com chapter to Elixir.
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, vao} = init_opengl()
send(self(), :update)
{frame,
%{
frame: frame,
canvas: canvas,
shader_program: shader_program,
vao: 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 =
[
-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
@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
@impl :wx_object
def handle_info(:update, state) do
render(state)
{:noreply, state}
end
defp render(%{canvas: canvas} = state) do
draw(state)
:wxGLCanvas.swapBuffers(canvas)
send(self(), :update)
:ok
end
defp draw(%{canvas: _canvas} = 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.vao)
:gl.drawArrays(:gl_const.gl_triangles(), 0, 3)
:ok
end
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
restart: :permanent
}
end
end
Boy, that is a LOT just to render a dang ol triangle. OpenGL doesn’t let you get anything for cheap, huh. Regardless, it’s pretty cool that I can get this to render on my machine with no outside dependencies! Who knew that Elixir shipped with a fully functional GUI library?
As an aside, it is an absolute tragedy what the Elixir formatter does to a nicely formatted list of floats. I don’t want to disable the formatter entirely, but I am pretty unhappy with what it does by default. Unfortunately, the formatter doesn’t allow disabling for a specific block or something, maybe I could create a formatter plugin?