Common LispでGPUベクトルベースフォントレンダリング

  • 7
    いいね
  • 0
    コメント

以前ブログに、Webブラウザを作ろうとしていることを書きました。
その時少しだけ書いた、文字列描画ライブラリの紹介です。

背景

ブラウザのレンダリングエンジンを作るにあたって、扱いやすい描画バックエンドが必要だった。
GeckoはCairoを使っているようだった(過去の話?)ので、cl-cffi-gtkを試した。
しかし文字列の描画を細かく制御するにはPangoも利用しなければならなかった。
OpenGLのプリミティブぐらい気軽に扱えるものが欲しい。

ちょうどこんなものを発見した。
WebGLとシェーダでベジエ曲線を描く応用で、TTFフォントをレンダリングするというものだ。
今回はこれをベースに、文字列描画ライブラリとしてCommon Lispに移植した。

GLisph

Glyph rendering engine using OpenGL shading language for Common Lisp.

screenshot.gif

https://github.com/tamamu/glisph

GLisphはOpenGLシェーダを用いたCommon Lispのためのグリフレンダリングエンジンです。
固定機能パイプラインを使っていないので、描画処理がコンテキストに依存しません。

フォントのベクトルデータをGPUに転送しているため、拡大縮小などによるオーバーヘッドが生じないようになっています。
曲線の描画には2次ベジエ曲線を用いているため、TTFフォント以外の描画には対応していません。

このライブラリはZPB-TTFに依存しています。

以下にGIF動画で表示されている例の実装を示します。

(require :cl-glut)
(require :glisph)

;; ウィンドウサイズ
(defvar *width* 800)
(defvar *height* 600)

;; フォントとグリフテーブル(キャッシュ)
(defvar *font*)
(defvar *glyph-table*)

(defvar *origin-x* 0.0)
(defvar *origin-y* 0.0)
(defvar *display-x* 0.0)
(defvar *display-y* 0.0)
(defvar *zoom* 1.0)
(defvar *frame-count* 0)

;; ウィンドウクラスの定義
;; ステンシルバッファとマルチサンプリングを利用する(必須)
(defclass test-window (glut:window)
  ()
  (:default-initargs :title "GLisphTest"
                     :width *width* :height *height*
                     :mode '(:stencil :multisample)
                     :tick-interval (round (/ 1000 60))))

(defmethod glut:mouse ((w test-window) button state x y)
  (declare (ignore w state))
  (case button
    (:left-button
      (setf *origin-x* (- x *display-x*)
            *origin-y* (- y *display-y*)))
    (:wheel-down
      (setf *zoom* (/ *zoom* 1.2))
      (glut:post-redisplay))
    (:wheel-up
      (setf *zoom* (* *zoom* 1.2))
      (glut:post-redisplay))))

(defmethod glut:motion ((w test-window) x y)
  (setf *display-x* (- x *origin-x*)
        *display-y* (- y *origin-y*))
  (glut:post-redisplay))

(defmethod glut:reshape ((w test-window) width height)
  (gl:viewport *display-x* (- *display-y*) width height)
  (gl:matrix-mode :projection)
  (gl:load-identity)
  (gl:ortho 0 width height 0 -1 1)
  (gl:matrix-mode :modelview)
  (gl:load-identity)
  (setf *width* width
        *height* height))

;; ウィンドウ表示前に初期化処理を行う
;; フォントを読み込んで、グリフテーブルに文字を登録している
(defmethod glut:display-window :before ((w test-window))
  (setf *font*
    (gli:open-font-loader "/usr/share/fonts/OTF/TakaoGothic.ttf"))
  (setf *glyph-table* (gli:make-glyph-table *font*))
  (loop for ch across "Hello World!
  The quick brown fox jumps over the lazy dog.色は匂へと 散りぬるを"
        do (gli:regist-glyph *glyph-table* ch))
  (gli:init))

(defmethod glut:tick ((w test-window))
  (incf *frame-count*)
  (when (>= *frame-count* 360)
    (setf *frame-count* 0))
  (glut:post-redisplay))

;; 描画処理
(defmethod glut:display ((w test-window))
  (gl:viewport *display-x* (- *display-y*) *width* *height*)
  (gl:clear-color 0 0 0 1)
  (gl:clear-stencil 0)
  (gl:clear :color-buffer-bit :stencil-buffer-bit)
  (gl:color 0.5 0.0 0.0 1.0)
  (gl:with-primitive :quads
    (gl:vertex 0 0)
    (gl:vertex 300 0)
    (gl:vertex 300 300)
    (gl:vertex 0 300))
  ;; グリフの拡大縮小行列を設定
  (gli:gscale (float (* *zoom* (/ 2 *width*)))
              (float (* *zoom* (/ -2 *height*)))
              1.0)
  (let* ((rad (* (coerce pi 'single-float) (/ *frame-count* 180)))
        (sinr (sin rad))
        (cosr (cos rad)))
    ;; グリフの回転をゼロに設定
    (gli:grotate 0.0 0.0 0.0)
    ;; 文字列の描画
    (gli:draw-string *glyph-table* "The quick brown fox jumps over the lazy dog."
      -350.0 0.0 0.0 30.0
      :color '(1 1 1 1))
    (gli:grotate 0.0 0.0 rad)
    (gli:draw-string *glyph-table* "Hello World!"
      -300.0 -150.0 0.0 (+ 45.0 (* 30.0 cosr))
      :color '(1 1 0 1))
    (gli:draw-string *glyph-table* "色は匂へと 散りぬるを"
      -300.0 200.0 0.0 40.0
      :color `(0 1 1 1)
      :spacing sinr))
  (gl:flush))

;; 終了時にfinalizeを実行する
(defmethod glut:close ((w test-window))
  (gli:delete-glyph-table *glyph-table*)
  (gli:finalize)
  (format t "close~%"))

(glut:display-window (make-instance 'test-window))

この例ではdraw-stringという糖衣構文を利用していますが、これはrender-glyphというプリミティブ描画に相当する関数を内部で呼び出しています。この関数もエクスポートしているので、必要に応じて使い分けることが可能です。

課題

  • シェーダを対応しているGLSLのバージョンに合わせて自動生成する
  • cl-annotを利用しているものの上手く使えてない気がする
  • 一部のフォント(源真ゴシックなど)の一部の文字が上手く描画出来ない
  • cl-glut以外での動作確認
  • ピクセルベースレンダリング(freetype)との比較
この投稿は Lisp Advent Calendar 201612日目の記事です。