getting started with textures in cl-opengl

At some point, I need to add a section explaining the GL state machine concept, but for now a quick summary here:

Some functions modify the current global GL state, like gl:color, which applies to (most) drawing operations until the color is changed. In other cases like textures, groups of state variables are grouped into objects. To use those, you create an object, bind it, and modify the state of the currently bound object, then later you can switch between existing sets of state with 1 bind call instead of resetting each parameter for every change.

texture objects

The first thing we need to do is create a texture object with gl:gen-textures which takes a parameter specifying how many texture objects to create, and returns them in a list.

(GL just gives us back an integer which it calls a "texture name", not an actual object. In early versions of GL, you could just pick your own numbers, but that doesn't scale very well and has been removed from current versions)

when we are done with it, we need to clean it up with gl:delete-textures

(let ((texture-name (car (gl:gen-textures 1))))
  ;; ...
  (gl:delete-textures (list texture-name)))

then we call gl:bind-texture to make that the current texture, and adjust some settings with gl:tex-parameter

(let ((texture-name (car (gl:gen-textures 1))))
  (gl:bind-texture :texture-2d texture-name)
  (gl:tex-parameter :texture-2d :texture-min-filter :linear)
  ;; these are actually the defaults, just here for reference
  (gl:tex-parameter :texture-2d :texture-mag-filter :linear)
  (gl:tex-parameter :texture-2d :texture-wrap-s :repeat)
  (gl:tex-parameter :texture-2d :texture-wrap-t :repeat)
  (gl:tex-parameter :texture-2d :texture-border-color '(0 0 0 0))
  ;;...
  (gl:delete-textures (list texture-name)))

:texture-min-filter and :texture-mag-filter control how GL interpolates between texels when a sample would cover more than one texel ("minification") or less than one texel ("magnification") respectively.

  • :nearest just picks the closes texel to the sampled location On modern hardware, you generally only want that for special effects, since it looks bad (either blocky for mag-filter, or noisy for min-filter), and it no longer has any speed benefit (and in fact can even be slower than some of the better looking options)
  • :linear does a weighted average of the 4 nearest texels to the sampled location This is generally what you want for mag-filter, but is still not very good for min-filter, but the best you can do without mipmaps, which we will cover in a bit.

Texture coordinates normally range from 0 to 1, :texture-wrap-s and :texture-wrap-t tell GL what to do with coordinates outside that range

  • :repeat chops off the integer part of the texture coordinate, effectively causing the texture to repeat infinitely.
  • :mirrored-repeat is like :repeat except if the integer part of the texture coordinate is odd, it uses 1 - (fractional part of texture coordinate) instead of using the fractional part directly, causing every other repetition to be mirrored
  • :clamp, treats texture coordinates less than 0 as 0, and greater than 1 as 1
  • :clamp-to-edge and :clamp-to-border are the same as :clamp but clamp to the center of the edge texel or half a texel outside of the texture respectively, which are useful for controlling whether the border color is blended with edge texels or not.

:texture-border-color sets the color used when any of the 'nearest texel' linear filtering is outside the actual texture data and repeat is set to :clamp or :clamp-to-border. (:clamp-to-edge keeps texture coordinates just far enough inside the texture that all 4 of the "nearest texels" are always inside the actual texture)

uploading texture data

once we have our texture object set up, we need to tell GL what it looks like.

To start with, we will just type in a simple gray-scale texture by hand, and we upload it with gl:tex-image-2d

(let ((texture-name (car (gl:gen-textures 1)))
      (texture-data #(#x00 #x00 #xff #xff #xff #xff #x00 #x00
                      #x00 #xff #x00 #x00 #x00 #x00 #xff #x00
                      #xff #x00 #xff #x00 #xff #x00 #x00 #xff
                      #xff #x00 #x00 #x00 #x00 #x00 #x00 #xff
                      #xff #x00 #xff #x00 #x00 #xff #x00 #xff
                      #x00 #xff #x00 #xff #xff #x00 #xff #x00
                      #x00 #x00 #xff #xff #xff #xff #x00 #x00)))
  (gl:bind-texture :texture-2d texture-name)
  (gl:tex-parameter :texture-2d :texture-min-filter :linear)
  ;; these are actually the defaults, just here for reference
  (gl:tex-parameter :texture-2d :texture-mag-filter :linear)
  (gl:tex-parameter :texture-2d :texture-wrap-s :repeat)
  (gl:tex-parameter :texture-2d :texture-wrap-t :repeat)
  (gl:tex-parameter :texture-2d :texture-border-color '(0 0 0 0))
  (gl:tex-image-2d :texture-2d 0 :rgba 8 8 0 :luminance :unsigned-byte texture-data)
  ;; ...
  (gl:delete-textures (list texture-name)))

the gl:tex-image-2d parameters are:

  • target = :texture-2d : specifies that we want to create a 2d texture
  • level = 0 : specifies that we are modifying mipmap level 0
  • internal format = :rgba : tells GL to store the texture internally as RGBA (not that we need it for this texture)
  • width = height = 8 : dimensions of the texture. Before GL 2.0, texture dimensions were required to be powers of 2, but now we can use any values from 1 to the maximum supported by the driver. (If width isn't a multiple of 4 bytes though, we need to change the :pixel-unpack-alignment setting with gl:pixel-store since the default assumes 4-byte aligned rows.)
  • border = 0 : tells GL we don't have a border on the texture
  • format = :luminance : specifies how the data in the texture-data array should be interpreted, in this case we want the value replicated to R,G,B channels, with alpha set to 1
  • type = :unsigned-byte : specifies the type of the data elements in texture-data, here unsigned-byte uses the C definition, which would be (unsigned-byte 8) in lisp terms, so values range from 0 to 255, and get normalized to 0..1 internally.
  • data = texture-data : the array of texel data, generally left-to-right then top-to-bottom, exact layout depends on the gl:pixel-store settings

putting it together

now we can combine it with the previous code, and draw a textured square

(defmacro restartable (&body body)
  "helper macro since we use continue restarts a lot
 (remember to hit C in slime or pick the restart so errors don't kill the app)"
  `(restart-case
      (progn ,@body)
    (continue () :report "Continue"  )))

(defparameter *the-texture* nil)

(defun draw ()
  "draw a frame"
  (gl:clear :color-buffer-bit)
  ;; if our texture is loaded, activate it and turn on texturing
  (when *the-texture*
    (gl:enable :texture-2d)
    (gl:bind-texture :texture-2d *the-texture*))
  ;; draw the entire square white so it doens't interfere with the texture
  (gl:color 1 1 1)
  ;; draw a square
  (gl:with-primitive :quads
    ;; we need to specify a texture coordinate for every vertex now...
    (gl:tex-coord 0 1)
    (gl:vertex -1 -1 0)
    (gl:tex-coord 1 1)
    (gl:vertex  1 -1 0)
    (gl:tex-coord 1 0)
    (gl:vertex  1  1 0)
    (gl:tex-coord 0 0)
    (gl:vertex -1  1 0))
  ;; finish the frame
  (gl:flush)
  (sdl:update-display))

(defun main-loop ()
  (sdl:with-init ()
    (sdl:window 320 240 :flags sdl:sdl-opengl)
    ;; cl-opengl needs platform specific support to be able to load GL
    ;; extensions, so we need to tell it how to do so in lispbuilder-sdl
    (setf cl-opengl-bindings:*gl-get-proc-address* #'sdl-cffi::sdl-gl-get-proc-address)
    (let ((*the-texture* (car (gl:gen-textures 1)))
          (texture-data #(#xff #x00 #x00 #x00 #x00 #x00 #x00 #xff
                          #x00 #x00 #xff #xff #xff #xff #x00 #x00
                          #x00 #xff #x00 #xff #xff #x00 #xff #x00
                          #x00 #xff #xff #xff #xff #xff #xff #x00
                          #x00 #xff #x00 #xff #xff #x00 #xff #x00
                          #x00 #xff #x30 #x00 #x00 #x30 #xff #x00
                          #x00 #x00 #xff #xff #xff #xff #x00 #x00
                          #xff #x00 #x00 #x00 #x00 #x00 #x00 #xff)))
      (gl:bind-texture :texture-2d *the-texture*)
      (gl:tex-parameter :texture-2d :texture-min-filter :linear)
      ;; these are actually the defaults, just here for reference
      (gl:tex-parameter :texture-2d :texture-mag-filter :linear)
      (gl:tex-parameter :texture-2d :texture-wrap-s :clamp-to-edge)
      (gl:tex-parameter :texture-2d :texture-wrap-t :clamp-to-edge)
      (gl:tex-parameter :texture-2d :texture-border-color '(0 0 0 0))
      (gl:tex-image-2d :texture-2d 0 :rgba 8 8 0 :luminance :unsigned-byte texture-data)
      (sdl:with-events ()
        (:quit-event () t)
        (:idle ()
               ;; this lets slime keep working while the main loop is running
               ;; in sbcl using the :fd-handler swank:*communication-style*
               ;; (something similar might help in some other lisps, not sure which though)
               #+(and sbcl (not sb-thread)) (restartable
                                              (sb-sys:serve-all-events 0))
               (restartable (draw))))
      (gl:delete-textures (list *the-texture*)))))


(main-loop)

texture sample screenshot

Next time, loading textures from files...

back to index