Rendering Text in CL-OpenGL with ZPB-TTF December 9th, 2009
Patrick Stein

I promised in an earlier post that I would share my code for rendering anti-aliased text in CL-OpenGL using ZPB-TTF to load the fonts.

Overview

I use a three step process to render a string. First, I render the outline of the string using anti-alised lines. Then, I render the string as solid polygons into the stencil buffer. Then, I fill a rectangle having OpenGL only fill where the stencil buffer is set.

Why the three-step process? OpenGL can draw polygons. In fact, it can draw anti-aliased polygons. However, it can only draw convex polygons. Most of the polygons involved in fonts are non-convex. The stencil buffer, however, has a drawing mode where you can flip the bits of the value in the stencil buffer each time OpenGL goes to write to it.

Imagine for a moment the letter O. It is made of two separate contours… one for the exterior of the circle and one for the interior of the circle. If I rendered each of these to the color buffer in OpenGL, I would end up with one big filled in circle. However, if I render each of these to the stencil buffer with the invert operation, then I end up setting all of the bits in the filled circle area when I draw the exterior contour and turning the middle bits off again when I render the internal contour. I am left with only the bits in the letter itself filled in.

The same sort of thing works even for more complicated letters like the letter C. As I am progressing along the outer edge, I am inadvertently filling the interior region. But, as I progress along the inner edge, I am inadvertently erasing that region again.

Once I am done, I use the stencil buffer as a stencil, draw a rectangle over it, and Bob’s your uncle.

The problem with the stencil buffer is that it has no subtlety. Bits are either set in it or not. So, I cannot anti-alias in the stencil buffer. I am left to draw the outline with anti-aliased lines.

Here are the relevant files:

  • draw.lisp: The code which actually draws strings in a given font.
  • gl.lisp: The code that makes a basic OpenGL window and invokes the font rendering on a given font and string.
  • test.lisp: Some simple code that loads the required libraries and opens a window.

The test program assumes that okolaksRegular.ttf is in your current directory. Tweak it to some TTF you do have. Or, get Okolaks itself.

Drawing a string

Here is the basic outline of the string drawing function. I am centering the string at the origin. I first calculate the bounding box. Then, I determine the scaling factor needed to get from the font-units into my own units and I scale the universe accordingly. Then, I translate the universe to center the bounding box. Then, I draw the anti-aliased outline of the text. If desired, I then draw the filled text to the stencil buffer and paint in the stencil.

(defun draw-string (font-loader string &key (size 48) (filled t))
  (gl:with-pushed-matrix
    (let* ((box (zpb-ttf:string-bounding-box string font-loader :kerning t))
           (bx1 (aref box 0))
           (by1 (aref box 1))
           (bx2 (aref box 2))
           (by2 (aref box 3)))

      (let ((ss (/ size (zpb-ttf:units/em font-loader))))
        (gl:scale ss ss 1))

      (gl:translate (/ (- bx1 bx2) 2) (/ (- by1 by2) 2) 0)
 
      (gl:with-pushed-attrib (:current-bit :color-buffer-bit :line-bit
                                           :hint-bit :stencil-buffer-bit)
      <<draw antialiased lines>>
      (when filled
        <<fill stencil buffer with filled-in-glyph>>
        <<fill in area subject to stencil>>)))))

To draw antialiased lines, I turn on blending. I tell it to blend the color in according to the alpha value of the portion of line it is trying to draw. I tell it to draw smooth lines in the nicest way it knows how. Then, I save the transformation-state so that I can move around inside the actual string rendering and still get back to this exact same place later. When I am done, I no longer need blending.

(gl:enable :blend)
(gl:blend-func :src-alpha :one-minus-src-alpha)
(gl:enable :line-smooth)
(gl:hint :line-smooth-hint :nicest)
(gl:with-pushed-matrix
    (render-string string font-loader nil))

To fill stencil buffer with filled-in-glyph, I turn off writing to the color buffer. I turn on the stencil testing. I set myself up to use a 1-bit stencil buffer. I set the buffer up so that when I clear it, it will all be zero-ed. I clear the stencil buffer. I set the stencil test to always write one bit. And, I set the stencil buffer to always write in invert mode. Then, I draw the string filled.

(gl:color-mask nil nil nil nil)
(gl:enable :stencil-test)
(gl:stencil-mask 1)
(gl:clear-stencil 0)
(gl:clear :stencil-buffer-bit)
(gl:stencil-func :always 1 1)
(gl:stencil-op :invert :invert :invert)
(gl:with-pushed-matrix
    (render-string string font-loader t))

To fill in area subject to stencil, I set myself up again to write to the color buffer. I set up the stencil test to let me write to my buffers only where the stencil is set to one. Then, I draw a filled rectangle over the whole bounding box.

(gl:color-mask t t t t)
(gl:stencil-func :equal 1 1)
(gl:with-primitives :quads
  (gl:vertex bx1 by1)
  (gl:vertex bx2 by1)
  (gl:vertex bx2 by2)
  (gl:vertex bx1 by2))

Rendering the string

To render the string, I loop through each character in it. For characters other than the first one, I adjust their position based on the kerning offset. Then, I render the character glyph and adjust the position so that I am ready to start the next character.

(defun render-string (string font-loader fill)
  (loop :for pos :from 0 :below (length string)
     :for cur = (zpb-ttf:find-glyph (aref string pos) font-loader)
     :for prev = nil :then cur
     :do (when prev
           (gl:translate (- (zpb-ttf:kerning-offset prev cur font-loader)
                            (zpb-ttf:left-side-bearing cur))
                         0 0))
         (render-glyph cur (if fill :polygon :line-strip))
         (gl:translate (zpb-ttf:advance-width cur) 0 0)))

Rendering a glyph

Here’s where the rubber really meets the road. My render-glyph function takes two arguments: a glyph and the mode in which to render it. The mode is :polygon for filled polygons and :line-strip for just the outline.

I loop through each contour in the glyph. With each contour, I tell OpenGL that I am going to render either a single polygon or a single line strip (according to the mode). To render the contour then, I loop through each contour segment. Each contour segment is either a straight line or a quadratic spline. If it is a straight line, then I have it easy. I just have to render the end points. If it is a spline, I have to render the starting point, create some interpolators to interpolate points along the spline, use those interpolators, and then render the end point.

(defun render-glyph (glyph mode)
  (zpb-ttf:do-contours (contour glyph)
    (gl:with-primitives mode
      (zpb-ttf:do-contour-segments (start ctrl end) contour
        (let ((sx (zpb-ttf:x start))
              (sy (zpb-ttf:y start))
              (cx (when ctrl (zpb-ttf:x ctrl)))
              (cy (when ctrl (zpb-ttf:y ctrl)))
              (ex (zpb-ttf:x end))
              (ey (zpb-ttf:y end)))
          (gl:vertex sx sy)
          (when ctrl
            (let ((int-x (make-interpolator sx cx ex))
                  (int-y (make-interpolator sy cy ey)))
              (interpolate sx sy ex ey int-x int-y)))
          (gl:vertex ex ey))))))

The starting point of one contour segment is always the ending point of the previous contour segment. So, if I wanted to bother, I could save myself a few OpenGL calls by remembering the first starting point of the first segment of the contour, not rendering the end points of any contour segment, and then rendering the starting point of the first segment again at the end (when I’m in outline mode). Instead, for clarity, I just always render the end point.

Making spline interpolators

If I want to make an quadratic spline that goes from x_0 to x_1 with control point c as time t goes from zero to one, I would use the polynomial (x_0 - 2c + x_1) t^2 + 2( c - x_0 ) t + x_0. So, given the parameters of a contour segment, I’m going to create interpolators for the x and y coordinates independently like so:

(defun make-interpolator (ss cc ee)
  (let ((xx (+ ss (* -2 cc) ee))
        (yy (* 2 (- cc ss)))
        (zz ss))
    #'(lambda (tt)
        (+ (* xx tt tt) (* yy tt) zz))))

Interpolating the spline

To interpolate the spline, I use a recursive function. It is given the x and y coordinates of the starting and ending points for the current portion of the spline, the starting and ending time coordinates of the current portion of the spline, and the interpolators created above. From there, it calculates the midpoint of a line segment from the start to the end and the x and y coordinates that the midpoint should be at based on the spline interpolator functions. If the calculated spline midpoint is really close to the line-segment midpoint, we just break off the recursion. If it is not really close, then we interpolate the first half of the current portion, render the calculated spline portion midpoint, and interpolate the second half of the current portion.

(defun interpolate (sx sy ex ey int-x int-y &optional (st 0) (et 1))
  (let ((mx (/ (+ sx ex) 2.0))
        (my (/ (+ sy ey) 2.0))
        (mt (/ (+ st et) 2.0)))
    (let ((nx (funcall int-x mt))
          (ny (funcall int-y mt)))
      (let ((dx (- mx nx))
            (dy (- my ny)))
        (when (< 1 (+ (* dx dx) (* dy dy)))
          (interpolate sx sy nx ny int-x int-y st mt)
          (gl:vertex nx ny)
          (interpolate nx ny ex ey int-x int-y mt et))))))

Technically, that check with the squared distance from the calculated midpoint to the segment midpoint shouldn’t just compare against the number one. It should be dynamic based on the current OpenGL transformation. I should project the origin and the points (1,0) and (0,1) through the current OpenGL projections, then use the reciprocal of the maximum distance those projected points are from the projected origin in place of the one. Right now, I am probably rendering many vertexes inside each screen pixel.

Summary

So, there you have it. Should you ever need to render text in CL-OpenGL, I hope this saves you many hours of trial-and-error and keeps you from resorting to rendering the glyphs to a texture map and dealing with how poorly they scale.

GUI Discussion — TC Lispers November Meeting December 9th, 2009
Patrick Stein

Cross-posted from TCLispers.org:

Lisp GUI Discussion on Vimeo.

Robert Goldman, Paul Krueger, and Patrick Stein gave short presentations on different approaches to Graphical User Interfaces with Common Lisp.

Cross-Platform Development November 25th, 2009
Patrick Stein

In preparation for the upcoming TC Lispers meeting, I was testing my Sheeple-based CL-OpenGL GUI code on all of the different systems that I use. This was important because part of my goal in starting from bare-bones OpenGL was to make something that was pretty easy to port. The OpenGL libraries are the only non-Lisp dependencies, and they are pretty standard.

I run Mac OS X 10.6.2 as my primary development machine. I knew everything I had so far worked there under SBCL 1.0.30 and CMU-CL 20a. It took a little tweaking to get Sheeple built under Allegro CL 8.1 (Trial Edition), but after that I was five-by-five under Allegro, too. Unfortunately, I cannot get CL-OpenGL to run under Clozure 1.3-r11936 on Mac OS X. Also, ECL 9.10.2 doesn’t support weak key hashtables which Sheeple needs. And, my install of clisp broke somewhere along the lines, so I haven’t tried it.

I have an Ubuntu Linux box. There, I am using SBCL 1.0.11.debian (eeps). Everything ran perfectly over there (even displaying through X Windows back to my Mac).

I also run Windows Vista under VMWare Fusion on my Mac. I hadn’t done any development on it for months and months. Fortunately, in Google-ing to find out how to fix it, I stumbled upon what I had written about how I got things set up originally. Over the last two hours, I got SBCL upgraded to 1.0.29 under Vista. I got ASDF-Install set up with a bunch of help from this article. And, from there, I got CFFI and cl-opengl and ZPB-TTF and Sheeple installed.

ZPB-TTF and Sheeple both used some tar format options that archive_0.7.0 didn’t like. For those, I had to jump through some hoops to untar and retar them to get them to install.

Here was my final /Users/Patrick/.sbclrc file:

(require :asdf)

;; from Zach Beane
(defmethod asdf:perform :around ((o asdf:load-op)
                                (c asdf:cl-source-file))
  (handler-case (call-next-method o c)
    (#+sbcl sb-ext:invalid-fasl
     #+allegro excl::file-incompatible-fasl-error
     #+lispworks conditions:fasl-error
     #+cmu ext:invalid-fasl
     #-(or sbcl allegor lispworks cmu) error ()
     (asdf:perform (make-instance 'asdf:compile-op) c)
     (call-next-method))))

(dolist (pkg  '("alexandria/"
                "archive_0.7.0/"
                "asdf-install/asdf-install/"
                "babel_0.3.0/"
                "cffi_0.10.5/"
                "cl-opengl/"
                "flexi-streams-1.0.7/"
                "gzip-stream_0.2.8/"
                "salza2-2.0.7/"
                "sykopomp-sheeple-ceab213/"
                "trivial-features_0.6/"
                "trivial-gray-streams-2008-11-02/"
                "woolly/"
                "zpb-ttf-1.0/") )
  (pushnew (merge-pathnames pkg
                            (merge-pathnames "ASDF-Systems/"
                                             (user-homedir-pathname)))
           asdf:*central-registry*))

(asdf:oos 'asdf:load-op 'asdf-install)

;; for my sanity
(setf asdf-install:*locations*
      (list (list (merge-pathnames "ASDF-Systems/" (user-homedir-pathname))
                  (merge-pathnames "ASDF-Systems/" (user-homedir-pathname))
                  "My install spot")))

;; via http://sean-ross.blogspot.com/2007/05/asdf-install-windows.html
#+win32
(asdf:oos 'asdf:load-op 'gzip-stream)

#+win32
(asdf:oos 'asdf:load-op 'archive)

#+win32
(defun asdf-install-extractor (to-dir tarball)
  (let ((name nil))
    (gzip-stream:with-open-gzip-file (ins tarball)
      (archive:with-open-archive (archive ins)
        (let ((*default-pathname-defaults* (pathname to-dir)))
          (archive:do-archive-entries (entry archive name)
            (archive:extract-entry archive entry)
            (unless name (setf name (archive:name entry)))))))
    (string name)))

#+win32
(push 'asdf-install-extractor asdf-install:*tar-extractors*)

The list of packages in the middle were about half installed manually to get the archive extraction code working and half installed through ASDF-Install. If I recall correctly, I had to manually install: archive_0.7.0, flexi-streams-1.0.7, gzip-stream_0.2.8, salza2-2.0.7, trivial-gray-streams-2008-11-02, and a fresh copy of asdf-install. I also had to download a compiled version of freeglut.dll and tuck it into my Windows\system32 directory.

Getting SBCL to use the fresh copy of asdf-install was annoying. I ended up starting up a Command Prompt as Administrator (right click on the “command.exe” icon or menu-item and select “Run as Administrator”). Then, I went to the SBCL directory (“C:\Program Files\Steel Bank Common Lisp\1.0.29\”) and did the following:

% rename asdf-install asdf-install.old
% mklink /d asdf-install "C:\Users\Patrick\ASDF-Systems\asdf-install\asdf-install"

I had extracted the tar-ball from the ASDF-Install distribution into my ASDF-Systems directory.

Then, I went back and made my GUI code use double-buffered OpenGL windows because running Lisp to OpenGL to Windows Vista to VMWare to Quartz had some wicked flickering going on.

Two hours is a long time for something that’s supposed to be easily portable. But, I would have spent at least 95% of that time even if I were using an all-Lisp solution. And, this is far less time than I ever spent porting anything else to Windows.

Playing with Sheeple-based GUI atop CL-OpenGL November 19th, 2009
Patrick Stein

Inspired by the previous TC Lispers meeting and spurred on by the probable topic of the next TC Lispers meeting, I have spent the little bit of coding time I’ve had over the past two weeks on making a GUI layer using Sheeple atop CL-OpenGL and employing ZPB-TTF for font-loading.

I have dubbed this project Woolly (because it’s made from Sheeple and because Woolly sounds sorta like GUI).

woolly-peekI spent about half of my coding time so far getting the basic framework in place (proven through clickable buttons with labels). I am trying to keep it cleanly separated between generic GUI stuff and the CL-OpenGL specifics in the event that someone would like to port it to some other I/O spec.

The other half of my coding time was spent getting the font-rendering to be anti-aliased. I promise to write more about how I accomplished the font-rendering in a future post so that if you’re ever stuck rendering fonts in OpenGL, you won’t be stuck with pixelated blockiness or resorting to rendering to a texture-map and letting the mipmapper figure it out.

For this post, however, I’ll just show you the code that sets up the interface depicted here.

(require :asdf)
(asdf:operate 'asdf:load-op 'woolly-gl :verbose nil)

;; make it easier to change renderer/controller later
(rename-package 'woolly-gl 'toolkit)

(defun test ()
  (let ((font (sheeple:object :parents toolkit:=font=
                              :em-size 48
                              :pathname "okolaks/okolaksRegular.ttf")))
    (let ((app (sheeple:object :parents toolkit:=app=))
          (win (sheeple:object :parents toolkit:=window=
                               :title "Woolly Window 1"
                               :width 640
                               :height 480))
          (but (sheeple:object :parents toolkit:=button=
                               :offset-x 40
                               :offset-y 40
                               :width 300
                               :height 100
                               :font font
                               :label "Button")))
      (woolly:display-window win)
      (woolly:add win but)
      (woolly:main-loop app)
      (woolly:destroy-window win))))

Next up on my agenda is to make the background and button prettier. It should be easy enough to do with GL_LIGHTING and some vertex-coloring for gradations. After that, it’s on to more controls like labels, panels, checkboxes, drop-downs, borders, and (my dread) text input boxes. Then, it’s on to a layout manager.

woolly-peekEdit: Here’s the same GUI a day later. I’m using a simple lighting scheme and rendering the button in 3D. I haven’t yet hooked in the bit to render it depressed when the button is pressed. I’ve tested the code that draws it the other way, but I haven’t hooked it into the mouse handlers yet.

Edit #2: Actually, it only took a few minutes for me to hook in the rendering it pressed vs. unpressed. When I did it though, it looked like the label was sliding around because the effect of the contrast between the light and dark edges of the button was so great that you perceive the whole button sliding when it’s pressed. So, I added a little bit in there to actually slide the text by an amount close to what is perceived. So, now… it looks pretty spiffy.

DSL for Drawing Floor Plans October 28th, 2009
Patrick Stein

Last week, I needed create a scale drawing of my basement floor plan. My license for OmniGraffle Professional are long since out-of-date. I didn’t want to pay $200 for a new license or even another $75 if I can dig up one of my old license keys. So, what’s a hacker to do? Roll his own (on top of Zach’s Vecto library).

My first cut worked, but was pretty ugly:

(with-floor-plan #P"basement.png" 200.0
   (interior-wall :start (cons (- 91 31 1/2) 0) ; hose cover
                  :north 18
                  :east  (+ 31 1/2))
   (interior-wall :start (cons 91 (- 103 27 30)) ; storage wall
                  :south (- 103 27 30))
   (interior-wall :start (cons (+ 91 83) 103)  ; fish-tank wall
                  :to '(91 . 103)
                  :south 27)
   ...)

basement
I ended up with large calculations like that (- 103 27 30) you see there. It worked. I got an okay looking floor-plan out of it. But, it was obvious that it needed some rethinking.

My next thought was turtle graphics! Tell things where to move. Tell it to start drawing an interior wall. Tell it where to move. Tell it to start drawing a window. Tell it where to move. Tell it to stop drawing a window. Etc. This has possibilities, especially for an internal representation (which I need because I want to autoscale the drawing to fit on a sheet of paper well, so I can’t start drawing until I’ve determined the extents). However, it seems awkward to go with all of the start/stop commands. I am thinking of going more in this sort of direction:

;; The floor-plan stuff works in units.  To facility readability, I am
;; going to make some simple functions that use feet and inches instead
;; of units.  If you want centimeters and meters, go for it.

(flet ((feet (n) (* n 12))
       (inches (n) n))
  (with-floor-plan (#P"floor-plan.png"
                      :max-width       8.0  ; maximum width of output (units)
                      :max-height     10.0  ; maximum height of output (units)
                      :dots-per-unit 300.0  ; resolution of output
                      :grid     (inches 6)) ; size of background-grid

    (compass :north 180.0)   ; draw north indication 180 degrees CCW from right

    (exterior-wall         ; start drawing an exterior wall
       :closed t           ; that closes at the end
       (left (feet 10))    ; extend wall left 10 feet
       (up (feet 6))       ; extend wall up 6 feet
       (window
          (up (inches 30))) ; draw 30" window
       (up (inches 8))      ; draw 8" wall
       (door :starts :hinge-side      ; or :latch-side
             :opens :left             ; or :right seen from direction of motion
             (up (inches 30)))        ; this door goes up, so :left is to
                                      ; the left of the page
       (up (inches 8))
       (right (feet 10)))

    (left (feet 4))                    ; move four feet to the left
    (interior-wall
       (up (feet 5))
       (right (inches 8))
       (door :starts :latch-side
             :opens :right
             (right (inches 30)))
       (right-to (feet 0)))            ; move to an absolute left-right coordinate
    (move (feet 2) (feet (+ 2 1/2)))
    (label "Bathroom")))

Has anyone tackled this problem well already? Or, have any suggestions of how to improve this?

Updates In Email

Email:

l