Textures part 3

OK, let's finish up textures with mipmapping and some examples of what the various options do.

Mipmapping is a technique for dealing with situations where multiple texels of a texture fit in the space of 1 pixel in the final image. In order to render that correctly, you would need to average the contributions from each texel weighted by the fraction of the pixel it covers in screen space, which would obviously get very expensive when we have lots of textures on screen.

We can get a reasonable approximation of this by keeping a set of lower resolution copies of the texture, and use whichever of those has texels closest to the size of the pixels of the final image. If we halve the dimensions for each copy, we at worst double the memory usage of the texture (for a 1xN or Nx1 texture), and usually only add ~1/3 or so (for a square texture).

The easiest way to get mipmaps is to let GL make them for us. There are 2 ways to do this, the older way is to set a flag on the texture with gl:tex-parameter, and the newer way is to explicitly tell GL to generate the mipmaps with gl:generate-mipmap. We'll use the first method for now, since it only really matters if you are changing the textures after loading them, and that way works on a wider range of drivers.

(defun load-a-texture (filename)
  (let ((texture (car (gl:gen-textures 1)))
        (image (sdl-image:load-image filename)))
    (gl:bind-texture :texture-2d texture)
    (gl:tex-parameter :texture-2d :generate-mipmap t) ;; <- new
    (gl:tex-parameter :texture-2d :texture-min-filter :linear-mipmap-linear) ;; <- new
    (sdl-base::with-pixel (pix (sdl:fp image))
      (let ((texture-format (ecase (sdl-base::pixel-bpp pix)
                              (1 :luminance)
                              (2 :luminance-alpha)
                              (3 :rgb)
                              (4 :rgba))))
        (assert (and (= (sdl-base::pixel-pitch pix)
                        (* (sdl:width image) (sdl-base::pixel-bpp pix)))
                     (zerop (rem (sdl-base::pixel-pitch pix) 4))))
        (gl:tex-image-2d :texture-2d 0 :rgba
                         (sdl:width image) (sdl:height image)
                         :unsigned-byte (sdl-base::pixel-data pix))))

Now that we have a full set of mipmaps, we can use a better :texture-mag-filter setting, :linear-mipmap-linear also known as "trilinear filtering" since it does bilinear filtering between the 4 nearest texels on each of the 2 nearest mipmap layers, then interpolates between the results.

The other new options are * :nearest-mipmap-nearest which uses closest texel on closest mipmap layer * :linear-mipmap-nearest which does bilinear filtering on the closest mipmap layer * :nearest-mipmap-linear which picks the closest points on the 2 nearest mipmap layers and interpolates between those

lisp alien logo Generally though, trilinear filtering is pretty much free on modern hardware, so these aren't used much anymore.

The other option for creating mipmaps is to make them by hand and load them into the texture one at a time, using the level argument to gl:tex-image-2d to specify which is being loaded. The auto-generation is good enough for many uses, but it can be useful to specify them explicitly for debugging, special effects, or for more control.

Now let's build something to show the effects of the various texture options.

We'll use another image from http://www.lisperati.com/logo.html so we have something to put in the background to see the effects of the alpha.

Since we have another texture now, we'll make a variable to store it in, and give the other one a better name:

(defparameter *background-texture* nil)
(defparameter *foreground-texture* nil)

Now we can change the draw function to draw both textures, and we'll make the foreground texture move around a bit.

Rather than repeat the code twice, we'll also pull out the rectangle code to a separate function, and move some settings that don't change out of the draw loop.

(defun rectangle (&key (texcoords '(0 0 1 1)))
  (gl:with-primitive :quads
    (gl:tex-coord (elt texcoords 0) (elt texcoords 3))
    (gl:vertex -1 -1 0)
    (gl:tex-coord (elt texcoords 2) (elt texcoords 3))
    (gl:vertex  1 -1 0)
    (gl:tex-coord (elt texcoords 2) (elt texcoords 1))
    (gl:vertex  1  1 0)
    (gl:tex-coord (elt texcoords 0) (elt texcoords 1))
    (gl:vertex -1  1 0)))

(defun draw ()
  (gl:clear :color-buffer-bit)
  (gl:color 1 1 1)
  ;; draw the background
  (gl:bind-texture :texture-2d *background-texture*)
  ;; draw the alien
    (gl:translate (* 0.5 (sin (get-universal-time)))
                  (* 0.5 (cos (get-universal-time)))
    (gl:bind-texture :texture-2d *foreground-texture*)

(defun init ()
  (gl:enable :blend)
  (gl:blend-func :src-alpha :one-minus-src-alpha)
  (gl:enable :texture-2d))

The main loop needs to call the init function and load the textures, and we will also add some support for handling key presses

(defun key-down (key state mod-key scancode unicode)
  (declare (ignore key state mod-key scancode unicode)))
(defun key-up (key state mod-key scancode unicode)
  (declare (ignore key state mod-key scancode unicode)))
(defun main-loop ()
  (sdl:with-init ()
    (sdl:window 320 240 :flags sdl:sdl-opengl)
    (setf cl-opengl-bindings:*gl-get-proc-address* #'sdl-cffi::sdl-gl-get-proc-address)
    (let ((*foreground-texture* (load-a-texture "lisplogo_alien_256.png"))
          (*background-texture* (load-a-texture "lisplogo_warning2_256.png")))

      (sdl:with-events ()
        (:quit-event () t)
        (:key-down-event (:state state :scancode scancode :key key
                        :mod-key mod-key :unicode unicode)
          (restartable (key-down key state mod-key scancode unicode)))
        (:key-up-event (:state state :scancode scancode :key key
                        :mod-key mod-key :unicode unicode)
          (restartable (key-up key state mod-key scancode unicode)))
        (:idle ()
               #+(and sbcl (not sb-thread)) (restartable
                                              (sb-sys:serve-all-events 0))
               (restartable (draw))))
      (gl:delete-textures (list *foreground-texture* *background-texture*)))))


alien alpha-blended over warning label So far, it looks like this:

If you have run the code as it is now, you probably noticed that the alien doesn't move very smoothly, since (get-universal-time) only changes once per second. Conveniently sdl has higher resolution timers available, so we just need to change it to use sdl:sdl-get-ticks (scaled down a bit, since it returns milliseconds) instead. (We could also use get-internal-real-time, but it seems to be fairly low resolution on at least a few lisps.)

While we're doing that, we can also adjust the texture coordinates of the warning label, so it isn't as stretched out (and to give us something to show the effects of the texture repeat settings)

(defun draw ()
  (gl:clear :color-buffer-bit)
  (gl:color 1 1 1)
  ;; draw the background
  (gl:bind-texture :texture-2d *background-texture*)
  (rectangle :texcoords '(-1.5 -0.5 2.5 1.5))
  ;; draw the alien
    (gl:translate (* 0.5 (sin (/ (sdl:sdl-get-ticks) 1000.0)))
                  (* 0.5 (cos (/ (sdl:sdl-get-ticks) 1000.0)))
    (gl:bind-texture :texture-2d *foreground-texture*)

alien alpha-blended over tiled warning label OK, reasonably smooth movement, and fairly close to the correct aspect ratio for the warning label.

Now we can add some keys to the key-up function to change various settings, and make the Esc key exit the program as well.

(defparameter *animate* t)
(defun key-down (key state mod scancode unicode)
  (declare (ignore state mod scancode unicode))
  (flet ((repeat (mode)
           (gl:bind-texture :texture-2d *background-texture*)
           (gl:tex-parameter :texture-2d :texture-wrap-s mode)
           (gl:tex-parameter :texture-2d :texture-wrap-t mode))
         (min-filter (mode)
           (gl:bind-texture :texture-2d *background-texture*)
           (gl:tex-parameter :texture-2d :texture-min-filter mode))
         (mag-filter (mode)
           (gl:bind-texture :texture-2d *background-texture*)
           (gl:tex-parameter :texture-2d :texture-mag-filter mode)))
    (case key
      (:sdl-key-a (gl:disable :blend))
      (:sdl-key-s (gl:enable :blend))
      (:sdl-key-d (gl:blend-func :src-alpha :one-minus-src-alpha))
      (:sdl-key-f (gl:blend-func :one :one))
      (:sdl-key-1 (min-filter :linear-mipmap-linear))
      (:sdl-key-2 (min-filter :linear-mipmap-nearest))
      (:sdl-key-3 (min-filter :nearest-mipmap-linear))
      (:sdl-key-4 (min-filter :nearest-mipmap-nearest))
      (:sdl-key-5 (min-filter :linear))
      (:sdl-key-6 (min-filter :nearest))

      (:sdl-key-8 (mag-filter :linear))
      (:sdl-key-9 (mag-filter :nearest))

      (:sdl-key-z (repeat :repeat))
      (:sdl-key-x (repeat :mirrored-repeat))
      (:sdl-key-c (repeat :clamp-to-edge))
      (:sdl-key-v (repeat :clamp-to-border))

       (setf *animate* (not *animate*)))

      (:sdl-key-escape  (sdl:push-quit-event)))))

The various texture filtering settings still aren't very obvious, so let's make one more change to the drawing function, and animate the background too.

(defun draw ()
  (gl:clear :color-buffer-bit)
  (gl:color 1 1 1)
  ;; draw the background
  (gl:matrix-mode :texture)
    (if *animate*
        (gl:translate 0.5 0.35 0)
        (gl:scale (+ 8 (* 8 (sin (/ (sdl:sdl-get-ticks) 1234.0))))
                  (+ 8 (* 8 (sin (/ (sdl:sdl-get-ticks) 1234.0))))
        (gl:rotate (/ (sdl:sdl-get-ticks) 33.0) 0 0 1))
      (gl:translate 0.5 0.5 0))
    (gl:bind-texture :texture-2d *background-texture*)
    (rectangle :texcoords '(-2 -1 2 1)))
  (gl:matrix-mode :modelview)
  ;; draw the alien
    (when *animate*
      (gl:translate (* 0.5 (sin (/ (sdl:sdl-get-ticks) 1000.0)))
                    (* 0.5 (cos (/ (sdl:sdl-get-ticks) 1000.0)))
    (gl:bind-texture :texture-2d *foreground-texture*)

screen shot gl:matrix-mode selects which of the built-in matrices is currently affected by calls to gl:translate, gl:scale, gl:rotate, etc. We use it here to select the :texture matrix, which is used to transform texture coordinates before they are used, so we can zoom in and out and rotate the texture without moving the background rectangle.

gl:with-pushed-matrix saves the current state of the active matrix, so we can make changes and they will be restored at the end of the form.

gl:load-identity resets the currently active matrix to the identity matrix.

screen shot screen shot

back to index