;;;; wheelwork.lisp (in-package #:wheelwork) (defvar *application* nil "current application") (defclass/std unit () ((cached-model :a) (container :with :a) (width height :with :std 1.0) (rotation x y :with :std 0.0) (opacity :std 1.0 :doc "0.0 indicates it will not be rendred."))) (defmethod (setf closer-mop:slot-value-using-class) :after (newval class (unit unit) slot) (when (member (closer-mop:slot-definition-name slot) '(x y width height rotation )) (setf (cached-model unit) nil))) (defclass/std container () ((units :with :a)) (:documentation "Just a list of units. Made into a class so that transformation affine transformations methods can be specialzied on whole groups of units")) (defun add-unit (container unit) "Adds a unit to the end of a container (thus affecting render order). Makes sure to remove the unit from its current container if necessary." (when (unit-container unit) (remove-unit unit)) (setf (container-units container) (nconc (container-units container) (list unit))) unit) (defun remove-unit (unit) "Removes a unit from its container. Returns T if the unit actually was removed." (when (unit-container unit) (setf (container-units (unit-container unit)) (delete unit (container-units (unit-container units))) (unit-container unit) nil) t)) (defclass/std application (container) ((title :with :std "Wheelwork App") (asset-root :ri :std #P"./" :doc "Directory under which assets are stored.") (asset-classifiers :std '(("png" texture)) :doc "ALIST of (EXT CLASS). EXT is a string, file estension. CLASS is a symbol, class name.") (assets :with :a :std (make-hash-table :test 'equal) :doc "maps asset names to asset instances.") (scale :with :std 1.0) (width height :with :std 800) (projection :with :a :doc "The projection matrix for the scene. Orthographic by default.") (window :with :a) (refocus-on-mousedown-p :std t) (focus last-motion-target :with :a) (frame-wait :std (/ 1000 30) :doc "Frames Per Second" :a))) (defun can-set-projection-p (app) (and (slot-boundp app 'width) (slot-boundp app 'height) (slot-boundp app 'scale))) (defun set-projection (app) (when (can-set-projection-p app) (with-slots (projection scale width height) app ;; set projection matrix (setf projection (mat:mortho 0.0 (/ width scale) 0 (/ height scale) -1.0 1.0))))) (defmethod initialize-instance :after ((app application) &key) (set-projection app)) (defmethod (setf closer-mop:slot-value-using-class) :after (new-value class (app application) slot) (when (member (closer-mop:slot-definition-name slot) '(scale width height)) (set-projection app))) (defgeneric boot (app) (:documentation "Specialized for each subclass of APPLICATION. Responsble for setting the app up once the system resoruces are avaialble.") (:method ((app application)) nil)) (defgeneric cleanup (thing) (:documentation "Clean up applications, textures, and so on.") (:method ((any t)) nil)) (defmethod cleanup ((app application)) (loop for asset being the hash-value of (application-assets app) do (cleanup asset)) (call-next-method)) (defmethod cleanup ((container container)) (dolist (u (container-units container)) (cleanup u))) (defun start (app &key (x :centered) (y :centered)) (sdl2:with-init (:everything) (sdl2:gl-set-attr :context-major-version 3) (sdl2:gl-set-attr :context-minor-version 3) (sdl2:gl-set-attr :context-profile-mask sdl2-ffi:+sdl-gl-context-profile-core+) (sdl2:gl-set-attr :doublebuffer 1) (sdl2:with-window (window :flags '(:shown :opengl) :title (application-title app) :w (application-width app) :h (application-height app) :x x :y y) (setf (application-window app) window) (sdl2:with-gl-context (ctx window) (sdl2:gl-make-current window ctx) (gl:viewport 0 0 (application-width app) (application-height app)) (gl:enable :depth-test) (let ((*application* app)) (unwind-protect (progn (boot app) (eventloop app)) (cleanup app))))))) (defvar *frame-time* nil "Bound and available once per frame. The result of GET-UNIVERSAL-TIME.") (defgeneric render (thing)) (defmethod render ((app application)) (gl:clear-color 0.0 0.0 0.0 1.0) (gl:clear :depth-buffer-bit :color-buffer-bit) (dolist (thing (container-units app)) (render thing)) (sdl2:gl-swap-window (application-window app))) (defgeneric translate-by (thing dx dy)) (defgeneric rotate-by (thing radians)) (defgeneric scale-by (thing sx sy)) (defgeneric pixel-width (thing)) (defgeneric (setf pixel-width) (newval thing)) (defgeneric pixel-height (thing)) (defgeneric (setf pixel-height) (newval thing)) (defgeneric visible-pixel-at-p (object x y) (:documentation "returns T if the visible pixel at screen coordintaes x and y belogns to object. Used for event handling.")) (defgeneric model-matrix (thing) (:documentation "Returns the model matrix")) (defmethod model-matrix ((u unit)) (or (cached-model u) (setf (cached-model u) (let ((m (mat:meye 4))) (mat:nmtranslate m (vec:vec (unit-x u) (unit-y u) 0.0)) (mat:nmtranslate m (vec:v* 0.5 (vec:vec (unit-width u) (unit-height u) 0.0))) (mat:nmrotate m vec:+vz+ (unit-rotation u)) (mat:nmtranslate m (vec:v* -0.5 (vec:vec (unit-width u) (unit-height u) 0.0))) (mat:nmscale m (vec:vec (unit-width u) (unit-height u) 1.0)) m)))) (defgeneric ensure-loaded (asset) (:documentation "Ensures that the asset is loaded into memory and ready for use. Returns the asset.")) (defclass/std asset () ((path :with :ri :std (error "An asset requires a path")) (loadedp :with :a))) (defmethod cleanup :around ((asset asset)) (when (asset-loadedp asset) (call-next-method)) (setf (asset-loadedp asset) nil)) (defmethod ensure-loaded :around ((thing asset)) (unless (asset-loadedp thing) (call-next-method) (setf (asset-loadedp thing) t)) thing) (defclass/std texture (asset) ((width height id mipmap :with :r) (internal-format image-format :ri :with :std :rgba) (wrap-s wrap-t :ri :with :std :repeat) (min-filter mag-filter :ri :with :std :nearest))) (defmethod cleanup ((texture texture)) (gl:delete-texture (texture-id texture))) (defmethod ensure-loaded ((texture texture)) (with-slots (width height id wrap-s wrap-t min-filter mag-filter internal-format image-format) texture (pngload:with-png-in-static-vector (png (asset-path texture) :flip-y t) (setf width (pngload:width png) height (pngload:height png) id (gl:gen-texture)) (gl:bind-texture :texture-2d id) (gl:tex-parameter :texture-2d :texture-wrap-s wrap-s) (gl:tex-parameter :texture-2d :texture-wrap-t wrap-t) (gl:tex-parameter :texture-2d :texture-min-filter min-filter) (gl:tex-parameter :texture-2d :texture-mag-filter mag-filter) (gl:tex-image-2d :texture-2d 0 internal-format width height 0 image-format :unsigned-byte (pngload:data png)) (gl:bind-texture :texture-2d 0)))) (defclass/std event-handler () ((event-type handler-function :ri))) (defclass/std listener () ((keydown keyup mousedown mouseup mousemove mousewheel focus blur perframe :r :with :type event-handler) (keydown-table keyup-table mousedown-table mouseup-table mousemove-table mousewheel-table focus-table blur-table perframe-table :static :std (make-hash-table) :doc "Keyed by DISPLAY-UNIT instance, holds an EVENT-HANDLER if handler is defined for unit.")) (:documentation "Event handlers per object. The static hash tables are keyed by UNIT and hold Event-Handler instances.")) (defclass/std interactive-unit (unit) ((listener :type (or null listener) :std nil :a))) (defun set-handler (unit handler) (when (null (listener unit)) (setf (listener unit) (make-instance 'listener))) (setf (slot-value (listener unit) (event-type handler)) handler (gethash unit (listener-table-for (listener unit) (event-type handler))) t)) (defun unset-handler (unit handler-or-event-type) "Handler can be an instance of EVENT-HANDLER or can be a symbol whose name is an event type." (when (listener unit) (let ((event-type (etypecase handler-or-event-type (keyword (intern (symbol-name handler-or-event-type))) (symbol (intern (symbol-name handler-or-event-type))) (event-handler (event-type handler-or-event-type))))) (setf (slot-value (listener unit) event-type) nil) (remhash unit (listener-table-for (listener unit) event-type))))) (defun listener-table-for (listener event-type) (ecase event-type (keydown (keydown-table listener)) (keyup (keyup-table listener)) (mousedown (mousewheel-table listener)) (mouseup (mouseup-table listener)) (mousemove (mousemove-table listener)) (mousewheel (mousewheel-table listener)) (focus (focus-table listener)) (blur (blur-table listener)) (perframe (perframe-table listener)))) (defun should-listen-for-p (listener event-type) (plusp (hash-table-count (listener-table-for listener event-type)))) (defun refocus-on (target &optional (app *application*)) "Handles changing application focus, calling appropriate blur and focus handlers." (when-let (blur-handler (and (application-focus app) (get-handler-for (application-focus app) 'blur))) (funcall (handler-function blur-handler) (application-focus app))) (setf (application-focus app) target) (when-let (focus-handler (get-handler-for target 'focus)) (funcall (handler-function focus-handler) target))) (defun get-focus (&optional (app *application*)) (or (application-focus app) app)) (defun get-handler-for (unit event-type) "EVENT-TYPE must be one of the slot value names for WHEELWORK::LISTENER." (when (listener unit) (slot-value (listener unit) event-type))) (defun eventloop-keydown (app sdl-keysym) (let ((target (get-focus app))) (format t "Calling eventloop-keydown~%") (when-let (handler (get-handler-for target 'keydown)) (format t "Handling the event.~%") (apply (handler-function handler) target (sdl2:scancode sdl-keysym) (sdl2:mod-keywords (sdl2:mod-value sdl-keysym)))))) (defun eventloop (app) (sdl2:with-event-loop (:method :poll) (:keydown (:keysym keysym) (eventloop-keydown app keysym)) (:idle () (render app)) (:quit () t))) (defclass/std bitmap (interactive-unit) ((texture :ri :std (error "A bitmap requires a texture.")) (vao shader :with :r :static))) (defmethod cleanup ((bitmap bitmap)) (with-slots (vao shader) bitmap (when vao (gl:delete-vertex-arrays (list vao))) (when shader (gl:delete-program shader)) (setf vao nil shader nil))) (defun shader-by-type (type) (case type (:vertex :vertex-shader) (:geometry :geometry-shader) (:fragment :fragment-shader))) (defun gl-shader (type stage) (let ((shader (gl:create-shader type))) (gl:shader-source shader (varjo:glsl-code stage)) (gl:compile-shader shader) (unless (gl:get-shader shader :compile-status) (error "failed to compile ~a shader:~%~a~%" type (gl:get-shader-info-log shader))) shader)) (defun create-shader (&rest sources) (let* ((stages (varjo:rolling-translate (mapcar (lambda (source) (destructuring-bind (type inputs uniforms code) source (varjo:make-stage type inputs uniforms '(:330) code))) sources))) (shaders (loop :for stage :in stages :for source :in sources :collect (gl-shader (shader-by-type (car source)) stage))) (program (gl:create-program))) (dolist (shader shaders) (gl:attach-shader program shader)) (gl:link-program program) (unless (gl:get-program program :link-status) (error "failed to link program: ~%~a~%" (gl:get-program-info-log program))) (dolist (shader shaders) (gl:detach-shader program shader) (gl:delete-shader shader)) program)) (defun gl-array (type &rest contents) (let ((array (gl:alloc-gl-array type (length contents)))) (dotimes (i (length contents) array) (setf (gl:glaref array i) (elt contents i))))) (defmacro with-gl-array ((var type &rest contents) &body body) `(let ((,var (gl-array ,type ,@contents))) (unwind-protect (progn ,@body) (gl:free-gl-array ,var)))) (define-symbol-macro +float-size+ (cffi:foreign-type-size :float)) (defmethod initialize-instance :after ((bitmap bitmap) &key) (with-slots (vao shader width height texture) bitmap (setf texture (ensure-loaded texture) height (texture-height texture) width (texture-width texture)) (unless shader (setf shader (create-shader '(:vertex ((vert :vec2)) ((transform :mat4)) ((values (* transform (vari:vec4 vert 0.0 1.0)) vert))) ;color '(:fragment ((tc :vec2)) ((tex :sampler-2d)) ((let ((frag (vari:texture tex tc))) (if (< (aref frag 3) 0.01) (vari:discard) frag)))))) (gl:program-uniformi shader (gl:get-uniform-location shader "TEX") 0)) (unless vao (setf vao (gl:gen-vertex-array)) (gl:bind-vertex-array vao) (let ((vbo (gl:gen-buffer))) (with-gl-array (verts :float 0.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0) (gl:bind-buffer :array-buffer vbo) (gl:buffer-data :array-buffer :static-draw verts))) (gl:enable-vertex-attrib-array 0) (gl:vertex-attrib-pointer 0 2 :float 0 (* +float-size+ 2) 0) (gl:bind-buffer :array-buffer 0) (gl:bind-vertex-array 0)))) (defmethod render ((bitmap bitmap)) (with-slots (texture vao shader) bitmap (gl:active-texture 0) (gl:bind-texture :texture-2d (texture-id texture)) (gl:use-program shader) (gl:program-uniform-matrix-4fv shader (gl:get-uniform-location shader "TRANSFORM") (mat:marr (mat:m* (application-projection *application*) (model-matrix bitmap)))) (gl:bind-vertex-array vao) (gl:draw-arrays :triangles 0 6) (gl:bind-vertex-array 0))) (defun asset-class-for (asset-id &optional (app *application*)) "Given an asset-id (see GET-ASSET), retrieve the symbol name of a the class that will be used to instantiate the asset object. That class should be a subclass of ASSET. Additional clases can be added to the application's ASSET-CLASSIFIERS association list." (second (assoc (pathname-type asset-id) (asset-classifiers app) :test #'string-equal))) (defun get-asset (asset-id &optional (app *application*)) "ASSET-ID is a pathname namestring relative to the application's ASSET-ROOT. GET-ASSET retrieves an already-available asset from the application's ASSETS table, or, if not available, loads the asset from disk." (or (gethash asset-id (application-assets app)) (setf (gethash asset-id (application-assets app)) (ensure-loaded (make-instance (asset-class-for asset-id) :path (uiop:merge-pathnames* asset-id (asset-root app))))))) (defmacro on-perframe ((target time) &body body) `(make-instance 'event-handler :event-type 'wheelwork::perframe :handler-function (lambda (,target ,time) (declare (ignorable ,target ,time)) ,@body))) (defmacro on-keydown ((target scancode modifiers) &body body) "Creates a lambda suitable for the value of a keydown event handler. The function accepts two positional arguments TARGET and SCANCODE and one &REST argument MODIFIERS. SCANCODE will be a keyword of the form :SCANCODE-A, :SCANCODE-B ... The members of MODIFIERS look like :LSHIFT, :RCTRL, RALT, etc" `(make-instance 'event-handler :event-type 'wheelwork::keydown :handler-function (lambda (,target ,scancode &rest ,modifiers) (declare (ignorable ,target ,scancode ,modifiers)) ,@body))) (defmacro on-keyup ((target scancode modifiers) &body body) "Creates a lambda suitable for the value of a keyup event handler. The function accepts two positional arguments TARGET and SCANCODE and one &REST argument MODIFIERS. SCANCODE will be a keyword of the form :SCANCODE-A, :SCANCODE-B ... The members of MODIFIERS look like :LSHIFT, :RCTRL, RALT, etc" `(make-instance 'event-handler :event-type 'wheelwork::keyup :handler-function (lambda (,target ,scancode &rest ,modifiers) (declare (ignorable ,target ,scancode ,modifiers)) ,@body))) (defmacro on-mousemove ((target x y xrel yrel state) &body body) `(make-instance 'event-handler :event-type 'wheelwork::mousemove :handler-function (lambda (,target ,x ,y ,xrel ,yrel ,state) (declare (ignorable ,target ,x ,y ,xrel ,yrel ,state)) ,@body))) (defmacro on-mousedown ((target x y clicks button) &body body) `(make-instance 'event-handler :event-type 'wheelwork::mousedown :handler-function (lambda (,target ,x ,y ,clicks ,button) (declare (ignorable ,target ,x ,y ,clicks ,button)) ,@body))) (defmacro on-mouseup ((target x y clicks button) &body body) `(make-instance 'event-handler :event-type 'wheelwork::mouseup :handler-function (lambda (,target ,x ,y ,clicks ,button) (declare (ignorable ,target ,x ,y ,clicks ,button)) ,@body))) (defmacro on-mousewheel ((target x y dir) &body body) `(make-instance 'event-handler :event-type 'wheelwork::mousewheel :handler-function (lambda (,target ,x ,y ,dir) (declare (ignorable ,target ,x ,y ,dir)) ,@body))) (defmacro on-blur ((target) &body body) `(make-instance 'event-handler :event-type 'wheelwork::blur :handler-function (lambda (,target) (declare (ignorable ,target)) ,@body))) (defmacro on-focus ((target) &body body) `(make-instance 'event-handler :event-type 'wheelwork::focus :handler-function (lambda (,target) (declare (ignorable ,target)) ,@body)))