03 января 2012

Common Lisp. 3d plot renderer.

Завлекушная картинка.

Захотелось тут написать простой интерактивный просмотрщик дву- и трехмерных графиков.

Я начал с библиотеки clx. Данная библиотека реализует протокол общения с графическим X сервером и не содержит внешних зависимостей. X сервер содержит расширение glx позволяющее использовать opengl. Я думал, что расширение glx заведется. Не завелось. Хотя под windows+cygwin/x+clx успех был, но это не моя конфигурация по-умолчанию.

На канале #lisp подсказали библиотеку glop, которую можно назвать урезанным clx'ом. Данная библиотека не реализует протокол самостоятельно, а просто является оберткой над сишными клиентами, а под windows и macos использует родные api.

Сегодня пойдет речь о том, как создать рендерер графиков на коммон лиспе с помощью библиотеки glop. По сути, это будет движок для приближения/отдаления точки и вращения вокруг нее.

Сразу же код:

(defpackage :3dplot
  (:use #:cl )
  (:export #:draw-plot #:while))

(in-package #:3dplot)

(defvar *min-zoom* 0.1)
(defvar *zoom-step* 0.1)
(defvar *rotate-multiplicator* 0.3)
(defvar *viewport-multiplicator* 10)

(defclass plotwindow (glop:window)
  ((1st-pressed :initform nil :accessor 1st-pressed)
   (zoom :initform 1 :accessor zoom)
   (xangle :initform 0 :accessor xangle)
   (yangle :initform 0 :accessor yangle)))

(defmethod glop:on-event ((window plotwindow) (event glop:key-event))
  (when (eq (glop:keysym event) :escape)
    (glop:push-close-event window))
  (when (and (glop:pressed event) (eq (glop:keysym event) :f))
    (glop:toggle-fullscreen window))
  (when (and (glop:pressed event) (eq (glop:keysym event) :g))
    (glop:set-fullscreen window)))

(defmethod glop:on-event ((window plotwindow) (event glop:button-event))
  (case (glop:button event)
    (1 ;; main button
     (setf (1st-pressed window) (glop:pressed event)))
    (4 ;; scroll up
     (when (> (zoom window) *min-zoom*)
       (decf (zoom window) *zoom-step*)
       (glop:push-event window (make-instance 'glop:resize-event :height (glop:window-height window)
                                                                 :width (glop:window-width window))))) 
    (5 ;; scroll down
     (incf (zoom window) *zoom-step*)
     (glop:push-event window (make-instance 'glop:resize-event :height (glop:window-height window)
                                                               :width (glop:window-width window))))))

(defmethod glop:on-event ((window plotwindow) (event glop:mouse-motion-event))
  (when (1st-pressed window)
    (incf (xangle window) (* *rotate-multiplicator* (glop:dx event)))
    (incf (yangle window) (* *rotate-multiplicator* (glop:dy event)))))

(defmethod glop:on-event ((window plotwindow) (event glop:resize-event))
  (let* ((width (glop:width event))
         (height (glop:height event))
         (aspect-ratio (/ height width))
         (zoom (zoom window)))
    (gl:viewport 0 0 width height)
    (gl:matrix-mode :projection)
    (gl:load-identity)
    (gl:ortho
     (* zoom (- *viewport-multiplicator*))
     (* zoom *viewport-multiplicator*)
     (* zoom (- (* *viewport-multiplicator* aspect-ratio)))
     (* zoom (* *viewport-multiplicator* aspect-ratio))
     (* zoom (- *viewport-multiplicator*))
     (* zoom *viewport-multiplicator*))))

(defmacro while (condition &body body)
  (let ((var (gensym)))
    `(do ((,var nil (progn ,@body)))
         ((not ,condition) ,var))))

(defun draw-3d-axes ()
  "Draw opengl 3d axes"
  (gl:color 0 1 0)
  (gl:with-primitives :lines
    (gl:vertex -10.0 0.0 0.0)
    (gl:vertex 10.0 0.0 0.0)
    ;; arrow
    (gl:vertex 10.0 0.0 0.0)
    (gl:vertex 9.5 0.5 0.0)
    (gl:vertex 10.0 0.0 0.0)
    (gl:vertex 9.5 -0.5 0.0)

    (gl:vertex 1.0 -0.2 0.0)
    (gl:vertex 1.0 0.2 0.0))
  (gl:color 1 1 0)
  (gl:with-primitives :lines
    (gl:vertex 0.0 -10.0 0.0)
    (gl:vertex 0.0 10.0 0.0)
    ;; arrow
    (gl:vertex 0.0 10.0 0.0)
    (gl:vertex -0.5 9.5 0.0)
    (gl:vertex 0.0 10.0 0.0)
    (gl:vertex 0.5 9.5 0.0)
    ;;unit
    (gl:vertex -0.2 1.0 0.0)
    (gl:vertex 0.2 1.0 0.0))
  (gl:color 0.4 0.5 1)
  (gl:with-primitives :lines
    (gl:vertex 0.0 0.0 -10.0)
    (gl:vertex 0.0 0.0 10.0)
    ;; arrow
    (gl:vertex 0.0 0.0 10.0)
    (gl:vertex -0.5 0.0 9.5)
    (gl:vertex 0.0 0.0 10.0)
    (gl:vertex 0.5 0.0 9.5)
    ;; unit
    (gl:vertex -0.2 0 1.0)
    (gl:vertex 0.2 0 1.0)))

(defun draw-plot-points (fn x-start x-end x-step y-start y-end y-step)
  "Draw plot of given function"
  (gl:color 1 1 1)
  (do ((x x-start (+ x x-step)))
      ((< x-end x) nil)
    (gl:with-primitives :line-strip
      (do ((y y-start (+ y y-step)))
          ((< y-end y) nil)
        (gl:vertex x y (funcall fn x y)))))
  (do ((y y-start (+ y y-step)))
      ((< y-end y) nil)
    (gl:with-primitives :line-strip
      (do ((x x-start (+ x x-step)))
          ((< x-end x) nil)
        (gl:vertex x y (funcall fn x y))))))


(defun draw-plot (fn x-start x-end x-step y-start y-end y-step)
  (glop:with-window (win "Interactive 3d plot" 800 600 :win-class 'plotwindow)
    ;; GL init
    (gl:clear-color 0 0 0 0)
    ;; idle loop, we draw here anyway
    (let ((frames 0)
          (last-time (get-universal-time)))
      (while (glop:dispatch-events win :blocking nil :on-foo nil)
        ;; rendering
        (gl:matrix-mode :modelview)
        (gl:load-identity)
        (gl:scale 1 1 -1)
        (gl:clear :color-buffer)
        ;; transform view
        (gl:with-pushed-matrix 
          (gl:rotate (xangle win) 0.0 1.0 0.0)
          (gl:rotate (yangle win) 1.0 0.0 0.0)
          (gl:color 1 1 1)
          (draw-3d-axes)
          (draw-plot-points fn x-start x-end x-step y-start y-end y-step))
        (gl:flush)
        (glop:swap-buffers win)
        (incf frames)
        (when (< 1 (- (get-universal-time) last-time))
          (format *standard-output* "fps ~a~%" (/ frames  (- (get-universal-time) last-time)))
          (setf last-time (get-universal-time))
          (setf frames 0))))))

Объявляем пакет 3dplot, экспортируем из него символ draw-plot.

draw-plot fn x-start x-end x-step y-start y-end y-step

Функция принимает:

  • fn функция от двух аргументов x и y, должна вернуть числовое значение
  • x-start x-end x-step начальное, конечное и приращение аргумента x
  • y-start y-end y-step начальное, конечное и приращение аргумента y

Пример, сетчатого графика для функции z = sin(x) + cos(x), где X э [-5,5] и Y э [-5,5], шаг 0.1 (э должна быть перевернута:), можете покрутить колесиком мыши или зажав левую клавишу подвигать ею.

(ql:quickload :glop)
(ql:quickload :cl-opengl)

(3dplot:draw-plot (lambda (x y) (+ (sin x) (cos y))) -5 5 0.1 -5 5 0.1)

Почему так много скобок.

Повторюсь: то, что получилось, представляет собой небольшой 3d движок. Можно даже сказать совсем небольшой. Что в нем реализовано:

  • Фокусирование на точке (0, 0, 0)
  • Приближение к данной точке
  • Отдаление от данной точки
  • Вращение вокруг данной точки

(defvar *min-zoom* 0.1)
(defvar *zoom-step* 0.1)
(defvar *rotate-multiplicator* 0.3)
(defvar *viewport-multiplicator* 10)

Это регуляторы для нашего движка.

  • *min-zoom* минимальное расстояние, меньше которого приближаться нельзя
  • *zoom-step* скорость приближения
  • *rotate-multiplicator* скорость поворота
  • *viewport-multiplicator* глобальное увеличение

Далее класс нашего окна для рисований. Наследуем его от glop:window.

(defclass plotwindow (glop:window)
  ...

Объект такого класса будет хранить следующие параметры.

  • 1st-pressed зажата ли левая клавиша мыши
  • zoom текущее увеличение
  • xangle текущий поворот относительно оси x
  • yangle текущий поворот относительно оси y

В этот же класс можно было бы добавить глобальные константы, но это в следующий раз.

Теперь реализуем обработчики событий от нашего окна. Первый специфируемый параметер: окно, второй - событие.

(defmethod glop:on-event ((window plotwindow) (event glop:key-event)
  ....

Обрабатываем нажатые клавиши мыши:

  • ESC - выход из программы. Осуществляется отсылкой сообщения glop:close-event нашему окну.
  • f - перключение полноэкранного режима
  • g - включение полноэкранного режима

(defmethod glop:on-event ((window plotwindow) (event glop:button-event)
  ....

Обрабатываем события от мыши. Если нажата левая клавиша: устанавливаем флаг в слоте окна 1st-pressed. Если нажата scroll up, приближем сцену, если scroll down - отдаляем. Для применения приближения или отдаления отсылаем сообщение об изменениях размеров. В обработчике того события происходит настройка сцены.

(defmethod glop:on-event ((window plotwindow) (event glop:mouse-motion-event))
  ....

Обработчик события передвижения мыши. Если зажата левая (главная клавиша) увеличиваем углы поворота сцены на "пробег" мыши.

(defmethod glop:on-event ((window plotwindow) (event glop:resize-event))
  ....

Здесь обрабатываем изменение размеров окна. Кроме того, данное событие "наступает", когда пользователь воспользовался функцией приближения/отдаления. Я не буду объяснять данный код, так как в книжках по opengl (например, redbook) это сделано гораздо лучше.

(defmacro while (condition &body body)
  ....

Вспомогательный макрос.

(defun draw-3d-axes ()
  ....

Отрисовываем оси координат, единичные отрезки и даже стрелочки. Делаем это разным цветом.

  • Ось X - зеленая
  • Ось Y - желтая
  • Ось Z - синяя

(defun draw-plot-points (fn x-start x-end x-step y-start y-end y-step)
  ....

Примитивная отрисовка графика.

(defun draw-plot (fn x-start x-end x-step y-start y-end y-step)
  ....

Главная функция. Создаем окно, настраиваем цвет фона. И в цикле обработки сообщений отрисовываем нашу сцену.

5 комментариев:

  1. Правильно ли я понимаю, что я могу это скомпилить и запустить под своим линуксом? И если да - то где репозиторий на гитхабе?

    ОтветитьУдалить
  2. Печально, но у меня sbcl умирает без суда и следствия: Lisp connection closed unexpectedly: connection broken by remote peer

    ОтветитьУдалить
  3. Сразу после вызова (draw-plot ...)

    ОтветитьУдалить
  4. @rigidus это пока черновичок.
    @LinkFly и у меня падает иногда. будет время отпиши разработчикам glop. https://github.com/patzy/glop

    ОтветитьУдалить
  5. Оказалось это моя виртуальная видеокарточка не хочет дружить с glop'ом (или наоборот). Жалко, но не обязана, да. На основном хосте, кстати работает, красиво однако:). Правда и тут не обошлось без проблем, glop не обрабатывает корректно слот rate в x11-video-mode структуре. После нехитрого патча стало работать как по-маслу:) Разработчикам черканул пару строк: https://github.com/patzy/glop/issues/11

    ОтветитьУдалить