Common Lisp (6) - praktický príklad

22.05.2008 23:30 | Články | Adam Sloboda
V tejto časti si znova s pomocou SDL vykreslíme sľúbenú Mandelbrotovu množinu (najznámejší fraktál). Poďme rovno na to.

Vykreslenie Mandelbrotovej množiny.

Ako prvé si vytvoríme mandelbrot.asd:

(asdf:defsystem #:mandelbrot
  :depends-on (#:sdl)
  :components ((:file "mandelbrot")))

A začiatok zdrojového súboru mandelbrot.lisp (znovu exportujeme funkciu start):

(defpackage #:mandelbrot
  (:use #:cl #:sdl)
  (:export #:start))
 
(in-package #:mandelbrot)

Teraz môžeme začať písať kód. Základný výpočet Mandelbrotovej množiny je celkom jednoduchý, dokonca aj na Wikipedii tvorí malú časť celého článku. Prepíšeme si nasledujúci pseudokód do Lispu:

For each pixel on the screen do:
{
  x = x0 = x co-ordinate of pixel
  y = y0 = y co-ordinate of pixel

  iteration = 0
  max_iteration = 1000
 
  while ( x*x + y*y <= (2*2)  AND  iteration < max_iteration ) 
  {

    xtemp = x*x - y*y + x0
    y = 2*x*y + y0

    x = xtemp

    iteration = iteration + 1
  }
 
  if ( iteration == max_iteration ) 
  then 
    colour = black
  else 
    colour = iteration

  plot(x0,y0,colour)
}

Tento výpočet môžeme realizovať s jednoduchým použitím makra do (pre jednoduchšie konštrukcie existujú zjednodušené verzie dolist a dotimes):

(do ((var1 init-form [step-form])
     ...)
    (end-form
     [result-form])
    body*)

Prvý je zoznam premenných, ktorým sa priradia počiatočné hodnoty a po každej iterácii je možné im priradiť novú hodnotu. Ďalší je zoznam, ktorý obsahuje test na ukončenie (ukončí sa ak je výsledok pravdivý) a voliteľnú návratovú hodnotu. Ďalej nasledujú výrazy, ktoré sa majú vykonávať pri každej iterácii. Toto makro nám poskytuje také možnosti, že telo ani nevyužijeme:

(defun mandelbrot (x0 y0)
  (do ((iteration 0 (1+ iteration))
       (x 0 (+ x0 (- (* x x) (* y y))))
       (y 0 (+ y0 (* 2 x y) )))
      ((or (> (+ (* x x) (* y y)) 4)
           (>= iteration *iterations*))
       (if (= iteration *iterations*) nil iteration))))

V každej iterácii inkrementujeme premennú iteration a premenným x a y priradíme hodnotu podľa výpočtu. Výpočet a priradenie nových hodnôt môžeme spraviť narozdiel od pseudokódu v jednom kroku, pretože hodnoty sú najskôr vypočítané a potom priradené (paralelne), variant do* priradzuje postupne (sekvenčne). Takéto postupné priradenie spôsobuje zaujímavú deformáciu. Náš test na ukončenie je negovaný oproti pôvodnému, pretože sa používa v opačnom význame. Po splnení podmienky vrátime počet iterácii, resp. nil ak by sme ešte pokračovali (je to prvok množiny).

Teraz už len nejak tento výpočet zužitkovať. Globálne funkcie ovplyvňujúce zobrazenie a inicializácia SDL:

;; súbor pre screenshot
(defvar *file* "mandelbrot.bmp")
 
;; inicializácia SDL
(defvar *width* 640)
(defvar *height* 480)
(defvar *bpp* 24)
(defvar *flags* (logior sdl:+resizable+
                        sdl:+swsurface+))
 
;; obdĺžnik v komplexnej rovine, ktorý vykreslujeme
(defvar *rect* nil)
;; počet iterácií pre pixel
(defvar *iterations* 256)
;; priblíženie
(defvar *zoom* 1.0)
 
(defun init-sdl ()
  (sdl:init (logior sdl:+init-video+))
  (let ((surface (sdl:set-video-mode *width* *height* *bpp* *flags*)))
    (when (sgum:null-pointer-p surface)
      (error "Unable to set video mode"))
    (sdl:wm-set-caption "CL-SDL Mandelbrot set" nil)
    surface))

S našimi znalosťami nie je problém rozlúštiť ani nasledujúcu dlhú a nudnú funkciu (číselné kódy označujú šípky, ďalšie použité klávesy sú R na prekreslenie, W/E pre zoom, S pre uloženie a Q pre ukončenie).

(defun run-sdl-event-loop (surface pixel-fn)
  (sdl:event-loop
   (:key-down (scan-code key mod unicode)
              (cond ((= key (char-code #\q))
                     (return))
                    ((= key (char-code #\w))
                     (incf *zoom* 0.45))
                    ((= key (char-code #\e))
                     (decf *zoom* 0.45))
                    ((= key 273)
                     (incf (second *rect*) 0.1)
                     (incf (fourth *rect*) 0.1))
                    ((= key 274)
                     (decf (second *rect*) 0.1)
                     (decf (fourth *rect*) 0.1))
                    ((= key 275)
                     (incf (first *rect*) 0.1)
                     (incf (third *rect*) 0.1))
                    ((= key 276)
                     (decf (first *rect*) 0.1)
                     (decf (third *rect*) 0.1))
                    ((= key (char-code #\s))
                     (sdl:save-bmp surface *file*))
                    ((= key (char-code #\r))
                     (plot surface *rect* pixel-fn)))
              (format t "zoom: ~f~%rect: ~{~f ~}~%" *zoom* *rect*))
   (:key-up (scan-code key mod unicode)
            (cond ((= key (char-code #\q))
                   (return))))
   (:quit ()
          (return))
   (:resize (width height)
            (setf *width* width
                  *height* height
                  surface (sdl:set-video-mode *width* *height* *bpp* *flags*)))
   (:idle ()
          (sleep 0.05))))

Taktiež funkcia pre štart nášho programu:

(defun start (&key (rect nil rect-p) (transform-name :greyscale))
  (if rect-p
      (setf *rect* rect)
      (setf *rect* (list -2.2 1.2 0.8 -1.2)))
  (unwind-protect
       (progn
         (let ((surface (init-sdl)))
           (run-sdl-event-loop surface (make-pixel-fn transform-name))))
    (sdl:quit)))

Transformačná funkcia v našom programe spôsobí sfarbenie podľa návratovej hodnoty (nil alebo počet iterácií). Tu je funkcia, ktorá nám vráti kresliacu funkciu:

(defun make-pixel-fn (name)
  (cond
    ((equal name :pure)
     #'(lambda (surface x y colour)
         (if (null colour)
             (cl-sdl:draw-pixel surface x y 0 0 0
              :check-lock-p nil :update-p nil :clipping-p nil)
             (cl-sdl:draw-pixel surface x y 255 255 255
              :check-lock-p nil :update-p nil :clipping-p nil))))
    ((equal name :colours)
     (let ((palette '((  0  0  0) (  0  0 170) (  0 170  0) (  0 170 170) 
                      (170  0  0) (170  0 170) (170  85  0) (170 170 170) 
                      ( 85 85 85) ( 85 85 255) ( 85 255 85) ( 85 255 255) 
                      (255 85 85) (255 85 255) (255 255 85) (255 255 255))))
       #'(lambda (surface x y colour)
           (let ((c (nth (mod (or colour 0) 16) palette)))
             (cl-sdl:draw-pixel surface x y (first c) (second c) (third c)
              :check-lock-p nil :update-p nil :clipping-p nil)))))
    ((equal name :redscale)
     #'(lambda (surface x y colour)
         (if (null colour)
             (cl-sdl:draw-pixel surface x y 0 0 0
              :check-lock-p nil :update-p nil :clipping-p nil)
             (let ((c (ash colour 16)))
               (cl-sdl:draw-pixel surface x y 
                                  (mod (* 8 (ldb (byte 8 16) c)) 256)
                                  (mod (* 8 (ldb (byte 8 8) c)) 256)
                                  (mod (* 8 (ldb (byte 8 0) c)) 256)
                                  :check-lock-p nil
                                  :update-p nil
                                  :clipping-p nil)))))
    ((equal name :greyscale)
     #'(lambda (surface x y colour)
         (if (null colour)
             (cl-sdl:draw-pixel surface x y 0 0 0
              :check-lock-p nil :update-p nil :clipping-p nil)
             (let ((c (mod (ash colour 3) 256)))
               (cl-sdl:draw-pixel surface x y c c c
                :check-lock-p nil :update-p nil :clipping-p nil)))))))

Teraz nám už chýba len funkcia, ktorá pre všetky pixely spraví výpočet a vykreslí bod (táto funkcia prepočítava polohu bodu pixelu na polohu v obdĺžniku, ktorý naozaj vykreslujeme). Zase je to jednoduchá funkcia, kde aplikujeme základné poznatky. with-possible-lock-and-update používame na uzamknutie, pretože budeme zakreslovať pixely, na konci sa celá obrazovka prekreslí.

(defun plot (surface rect pixel-fn)
  (let ((x0 (first rect))
        (y0 (second rect))
        (x1 (third rect))
        (y1 (fourth rect)))
    (with-possible-lock-and-update (surface t t 0 0 *width* *height*)
      (let ((x-step (float (/ (/ (abs (- x0 x1)) *width*) *zoom*)))
            (y-step (float (/ (/ (abs (- y0 y1)) *height*) *zoom*))))
        (loop :for y :from 0 :below *height* :do
           (loop :for x :from 0 :below *width* :do
              (funcall pixel-fn surface x y (mandelbrot
                                             (float (+ x0 (* x-step x)))
                                             (float (- y0 (* y-step y)))))))))))

Makro with-possible-lock-and-update je požičané z knižnice cl-sdl a preto ho tu ani nebudem uvádzať.

A to je všetko (kompletný zdrojový kód). Pre záujemcov dávam k dispozícii optimalizovaný zdrojový kód (natívne kresliace funkcie knižnice a deklarácie typov premenných), na testovacom počítači bolo zobrazenie 640×480 vykreslené viac než 15x rýchlejšie. Nabudúce si trochu priblížime makrá.