In the last couple of episodes, we were introduced to OpenGL via wx in Elixir. Today we're going to expand on the existing work and introduce a basic tool for building '3D Bitmaps', where a bitmap is just a list of lists of bits, and each bit that's on is represented as a cube in the scene. Let's get started.

Project

We're starting out in the same project we were in before. I've tagged it with before_episode_238 if you want to follow along.

Let's have a look at Lesson12 to see what we'll be using as our starting point:

iex -S mix
gc = GameCore.start_link
GameCore.load(gc, Lesson12)

So here we can see a bunch of cubes that are spaced apart a bit and textured with an image. We don't care about the texturing, but the reused shape is useful. We'll look at that in a bit.

Let's go ahead and make a new module that we'll use for our bitmap renderer:

vim lib/cubes.ex

First, we'll read in Lesson12 just to get a good starting point:

:r lib/lesson_12.ex
  # Remove the bits of state that we don't want to keep around
  defmodule State do
    defstruct [
      :parent,
      :config,
      :canvas,
      :timer,
      :time,
      :box,
      :xrot,
      :yrot,
    ]
  end

# Remove Texture struct

  def do_init(config) do
    # ...

    # Update our initial state to remove items we don't care for any longer
    state = %State{
      parent: parent,
      config: config,
      canvas: canvas,
      xrot: 0.0,
      yrot: 0.0
    }

    new_state = setup_gl(state)
    timer = :timer.send_interval(20, self, :update)

    {parent, %State{ new_state | timer: timer } }
  end

  def setup_gl(state) do
    {w, h} = :wxWindow.getClientSize(state.parent)
    resize_gl_scene(w, h)
    new_state = setup_display_lists(state)

    :gl.enable(:wx_const.gl_texture_2d)
    :gl.shadeModel(:wx_const.gl_smooth)
    :gl.clearColor(0.0, 0.0, 0.0, 0.0)
    :gl.clearDepth(1.0)
    :gl.enable(:wx_const.gl_depth_test)
    :gl.depthFunc(:wx_const.gl_lequal)
    :gl.enable(:wx_const.gl_light0)
    :gl.enable(:wx_const.gl_lighting)
    :gl.enable(:wx_const.gl_color_material)
    :gl.hint(:wx_const.gl_perspective_correction_hint, :wx_const.gl_nicest)

    new_state
  end

  def setup_display_lists(state) do
    # Display Lists are groups of GL commands that have been stored for
    # subsequent execution.
    # :gl.genLists generates a contiguous set of empty display lists.  Here
    # we're only generating one.  The return value is a positive integer that
    # can be used to reference that display list in the future.
    box = :gl.genLists(1)
    # Next, we'll create a new list.  The first argument is the list number, and
    # the second is the mode we're using.  We'll just compile the commands here.
    # That means they won't be executed until the list is used later.
    :gl.newList(box, :wx_const.gl_compile)

    # Now we just use the existing GL interfaces that we've already become
    # familiar with, and they'll be compiled into the display list rather than
    # executed in place.
    :gl.begin(:wx_const.gl_quads)

    # Here we just create a cube that's 2 units on a side.
    # Front Face
    :gl.vertex3f(-1.0, -1.0,  1.0)
    :gl.vertex3f( 1.0, -1.0,  1.0)
    :gl.vertex3f( 1.0,  1.0,  1.0)
    :gl.vertex3f(-1.0,  1.0,  1.0)

    # Back Face
    :gl.vertex3f(-1.0, -1.0, -1.0)
    :gl.vertex3f(-1.0,  1.0, -1.0)
    :gl.vertex3f( 1.0,  1.0, -1.0)
    :gl.vertex3f( 1.0, -1.0, -1.0)

    # Top Face
    :gl.vertex3f(-1.0,  1.0, -1.0)
    :gl.vertex3f(-1.0,  1.0,  1.0)
    :gl.vertex3f( 1.0,  1.0,  1.0)
    :gl.vertex3f( 1.0,  1.0, -1.0)

    # Bottom Face
    :gl.vertex3f(-1.0, -1.0, -1.0)
    :gl.vertex3f( 1.0, -1.0, -1.0)
    :gl.vertex3f( 1.0, -1.0,  1.0)
    :gl.vertex3f(-1.0, -1.0,  1.0)

    # Right Face
    :gl.vertex3f( 1.0, -1.0, -1.0)
    :gl.vertex3f( 1.0,  1.0, -1.0)
    :gl.vertex3f( 1.0,  1.0,  1.0)
    :gl.vertex3f( 1.0, -1.0,  1.0)

    # Left Face
    :gl.vertex3f(-1.0, -1.0, -1.0)
    :gl.vertex3f(-1.0, -1.0,  1.0)
    :gl.vertex3f(-1.0,  1.0,  1.0)
    :gl.vertex3f(-1.0,  1.0, -1.0)

    :gl.end
    # And now we're done with that list.
    :gl.endList

    # Finally, we'll update our state so we can reference this displayList via
    # its integer later.
    %State{ state | box: box }
  end

  def draw(state) do
    use Bitwise
    :gl.clear(bor(:wx_const.gl_color_buffer_bit, :wx_const.gl_depth_buffer_bit))
    # We aren't using the texture so remove this line
    #:gl.bindTexture(:wx_const.gl_texture_2d, state.texture.id)

    # Here we'll make up a bitmap that represents the same general data format
    # that we used when creating our tetris board representation in the past.
    bitmap =
      [
        [ 0, 0, 1, 0, 0 ],
        [ 0, 0, 1, 0, 0 ],
        [ 0, 1, 1, 0, 0 ]
      ]
    # And we'll call a function, `draw_bitmap`, that will draw this bitmap as cubes.
    draw_bitmap(bitmap, state)

    state
  end
  # We get rid of everything after draw because we won't use it.

  # We'll add a function for drawing a bitmap.  This will just iterate over all
  # of the rows and draw each of them.
  def draw_bitmap(bitmap, state) do
    for {row, row_num} <- Enum.with_index(bitmap) do
      draw_row({row, row_num}, state)
    end
  end

  # Drawing a row will iterate over all of the cells in that row and draw a box
  # in each cell that has a 1 in it.
  def draw_row({row, row_num}, state) do
    for {cell, cell_num} <- Enum.with_index(row) do
      draw_cell(row_num, {cell, cell_num}, state)
    end
  end

  # When we draw a cell with a 0 in it, we don't do anything
  def draw_cell(_, {0, _}, _), do: :ok
  # Otherwise, we'll use the row and cell number to determine the appropriate
  # offset
  def draw_cell(row_num, {cell, cell_num}, state) do
    # We'll set the cursor back to the origin
    :gl.loadIdentity()
    # We'll move the cursor appropriately for each cube, leaving a little
    # padding betwen them.  We'll also push the cursor well into the screen.
    # These 'magic numbers' are just what I ended up with after some fiddling
    # with an existing example.
    :gl.translatef(1.4 + (cell_num * 2.8), ((6.0 - row_num) * 2.4) - 7.0, -40.0)
    # We'll set the color to green
    :gl.color3f(0.0, 1.0, 0.0)
    # And we'll use `:gl.callList` to evaluate the pre-compiled display list
    # commands, drawing a cube where the cursor is.
    :gl.callList(state.box)
  end
end

That's really it. This obviously should have felt an awful lot like the other tetris renderers. Let's see what it looks like:

iex -S mix
gc = GameCore.start_link
GameCore.load(gc, Cubes)

Summary

And there we have it. A basic bitmap renderer where each pixel is a cube with a little padding between pixels. I hope you enjoyed it!

Resources

Josh Adams

I've been building web-based software for businesses for over 18 years. In the last four years I realized that functional programming was in fact amazing, and have been pretty eager since then to help people build software better.

  1. Comments for OpenGL: 3D Bitmaps

You must login to comment

You May Also Like