Textures part 2

Continuing from part 1, this time we will load a texture from a file.

In this version, we will take the easy way out, and let lispbuilder-sdl-image do all the real work dealing with file formats.

lisp alien First we need an image file to load, so how about a nice lisp alien from http://www.lisperati.com/logo.html (but remember that older versions of OpenGL don't like non-power-of-2 dimensions, so you might need to try another image on older hardware or drivers)

Then we ask sdl-image to load it for us with (sdl-image:load-image "lisplogo_alien_256.png") and send it to gl as before.

To extract the image data from the sdl:surface returned by sdl-image:load-image, we use sdl:fp and sdl-base::with-pixel, which gives us an sdl-base::pixels object and some accessors to use on it.

We need to get some details about the image so we can set up the texture, which we can get from sdl:width, sdl:height, and the sdl-base::pixels accessors.

(let ((image (sdl-image:load-image "lisplogo_alien_256.png")))
  (sdl-base::with-pixel (pix (sdl:fp image))
    ;; we should probably be a bit more intelligent about this, but this
    ;; handles some common cases
    (let ((texture-format (ecase (sdl-base::pixel-bpp pix)
                            (3 :rgb)
                            (4 :rgba))))
      ;; we should also handle this properly, by adjusting the
      ;; settings of gl:pixel-store
      (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)
                       0
                       texture-format
                       :unsigned-byte (sdl-base::pixel-data pix)))))

This only works for 24 and 32 bit images, 8 and 16 bit could be made to work as well, but would probably require translating the format before sending it to GL.

Now we can combine that with the texture setup code form last time and build a function to load a texture:

(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 :texture-min-filter :linear)

    (sdl-base::with-pixel (pix (sdl:fp image))
      ;; we should probably be a bit more intelligent about this, but this
      ;; handles some common cases
      (let ((texture-format (ecase (sdl-base::pixel-bpp pix)
                              (3 :rgb)
                              (4 :rgba))))
        ;; we should also handle this properly, by adjusting the
        ;; settings of gl:pixel-store
        (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)
                         0
                         texture-format
                         :unsigned-byte (sdl-base::pixel-data pix))))
    texture))

and try it out (keeping restartable, *the-texture* and draw from last time)

(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 ((*the-texture* (load-a-texture "lisplogo_alien_256.png")))
      (sdl:with-events ()
        (:quit-event () t)
        (:idle ()
               #+(and sbcl (not sb-thread)) (restartable
                                              (sb-sys:serve-all-events 0))
               (restartable (draw))))
      (gl:delete-textures (list *the-texture*)))))


(main-loop)

lisp alien in a texture and we get this:

You might notice that in addition to being stretched out a bit due to the window being a different shape from the texture, the edges of the texture don't look quite as nice as the original image. That is because we aren't doing anything with the alpha channel in the texture.

To fix that, we just need to enable blending, and set up the blend function. We'll put it into the draw function so it takes effect without restarting:

(defun draw ()
  ;; set up blending
  (gl:enable :blend)
  (gl:blend-func :src-alpha :one-minus-src-alpha)
  (gl:clear :color-buffer-bit)
  (when *the-texture*
    (gl:enable :texture-2d)
    (gl:bind-texture :texture-2d *the-texture*))
  (gl:color 1 1 1)
  (gl:with-primitive :quads
    (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))
  (gl:flush)
  (sdl:update-display))

alpha blended alien screen shot That looks much better.

gl:blend-func takes 2 parameters specifying the amount of the source and destination colors to blend to get the final color, in this case we use the alpha value to linearly interpolate (commonly abbreviated 'lerp') between source and destination colors:
final-color = source-color * source-alpha + destination-color * (1 - source-alpha)
Other possible multipliers include :zero, :one, source or destination alpha or color, constants, and some variants of those. (gl:blend-func :one :one) for example gives additive blending, which can be useful for things like glows and fire particles.

Next time, more texture options...

back to index