Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Applications

filonenko-mikhail edited this page Mar 26, 2011 · 16 revisions

There are two pieces underlying any Penumbra application: the callbacks, and the initial state. Callbacks are functions which are called when a specific event happens. As parameters, they take contextual information followed by the current state of the application. The value they return represents the new state of the application (except for :display, whose return value is ignored). If nil is returned, the state is left unchanged. If a non-nil value is returned, the state is updated and the application is repainted.

Some examples

A simple triangle

(ns example.wiki.triangle1
  (:use [penumbra.opengl])
  (:require [penumbra.app :as app]))

(defn reshape [[x y width height] state]
  (frustum-view 60.0 (/ (double width) height) 1.0 100.0)
  (load-identity)
  state)

(defn display [[delta time] state]
  (translate 0 -0.93 -3)
  (draw-triangles
    (color 1 0 0) (vertex 1 0)
    (color 0 1 0) (vertex -1 0)
    (color 0 0 1) (vertex 0 1.86)))

(app/start 
  {:display display, :reshape reshape} 
  {})

This application draws a triangle. (app/start ...) starts the application. The first parameter is a hash containing the callbacks, the second parameter is the initial state. In this case, we only have two callbacks, one to setup the view when the window changes in size, and another to render the triangle. Nothing changes in this program, so our state is empty. As a side effect of the unchanging state, this program will only render once, unless some OS event forces it to repaint.

A rotating triangle

(ns example.wiki.triangle2
  (:use [penumbra.opengl])
  (:require [penumbra.app :as app]))

(defn init [state]
  (app/vsync! true)
  state)

(defn reshape [[x y width height] state]
  (frustum-view 60.0 (/ (double width) height) 1.0 100.0)
  (load-identity)
  state)

(defn display [[delta time] state]
  (translate 0 -0.93 -3)
  (rotate (rem (* 90 time) 360) 0 1 0)
  (draw-triangles
   (color 1 0 0) (vertex 1 0)
   (color 0 1 0) (vertex -1 0)
   (color 0 0 1) (vertex 0 1.86))
  (app/repaint!))

(app/start 
  {:display display, :reshape reshape, :init init} 
  {})

This draws a triangle which completes a full rotation around the Y-axis every 4 seconds. Since our state is still empty, we need to call (app/repaint!) in our :display callback to force a repaint of the triangle. Also, since we don’t really want this to be drawn more than 60 times a second, vertical sync is enabled in the :init callback.

Also, note that we are calling transforms repeatedly inside :display. This is fine, since it is implicitly wrapped by (push-matrix ...), so the transforms will not be cumulative. In other callbacks, however, they will persist (so be careful).

Controlling the triangle

Controlling the rotation of the triangle is easy; all we have to do is define a callback for when the user drags the mouse.

(ns example.wiki.triangle3
  (:use [penumbra.opengl])
  (:require [penumbra.app :as app]))

(defn init [state]
  (app/vsync! true)
  state)

(defn reshape [[x y width height] state]
  (frustum-view 60.0 (/ (double width) height) 1.0 100.0)
  (load-identity)
  state)

(defn mouse-drag [[dx dy] [x y] button state]
  (assoc state
    :rot-x (+ (:rot-x state) dy)
    :rot-y (+ (:rot-y state) dx)))

(defn display [[delta time] state]
  (translate 0 -0.93 -3)
  (rotate (:rot-x state) 1 0 0)
  (rotate (:rot-y state) 0 1 0)
  (draw-triangles
    (color 1 0 0) (vertex 1 0)
    (color 0 1 0) (vertex -1 0)
    (color 0 0 1) (vertex 0 1.86)))

(app/start 
  {:display display, :reshape reshape, :mouse-drag mouse-drag, :init init} 
  {:rot-x 0, :rot-y 0})

In this example, the :mouse-drag callback is used, which is called whenever the mouse is moved with a button pressed down. It provides the changes in the x and y locations since the last time the :mouse-drag or :mouse-move callback was called, the position of the cursor, and the button which is pressed. If multiple buttons are pressed, this callback will be called once for each button.

In this version, we’ve removed (app/repaint!). By altering the state in :mouse-drag, we force a repaint. Thus, the application will draw as often as it needs to accurately reflect the current state, and no more.

The :update callback

So far, you’ve only seen how to update the state in response to user input. Another option is to use the :update callback, which is called whenever a frame is rendered, just before :display. Using this callback, we can implement the continuously rotating triangle as a stateful application.

(ns example.wiki.triangle4
  (:use [penumbra.opengl])
  (:require [penumbra.app :as app]))

(defn init [state]
  (app/vsync! true)
  state)

(defn reshape [[x y width height] state]
  (frustum-view 60.0 (/ (double width) height) 1.0 100.0)
  (load-identity)
  state)
  
(defn update [[delta time] state]
  (update-in state [:rot-y] #(rem (+ % (* 90 delta)) 360)))

(defn display [[delta time] state]
  (translate 0 -0.93 -3)
  (rotate (:rot-y state) 0 1 0)
  (draw-triangles
   (color 1 0 0) (vertex 1 0)
   (color 0 1 0) (vertex -1 0)
   (color 0 0 1) (vertex 0 1.86))
  (app/repaint!))

(app/start 
  {:display display, :reshape reshape, :init init, :update update} 
  {:rot-y 0})

This will behave exactly like the other rotating triangle example, but makes use of the application state. In :update we use the delta parameter, which gives the elapased time (in seconds) since :update was last called.

Periodic updates

All of the above examples only update themselves in response to user input or a frame being rendered. In many cases, however, this is not what we want. Consider the following example:

Drawing a clock

Here is a function that draws the hands of a clock:

(defn draw-clock [hour minute second]
  (push-matrix
   (scale 0.5 0.5 1)
   (rotate 180 0 0 1)
   (push-matrix
    (rotate (* -30 (rem hour 24)) 0 0 1)
    (color 1 1 1)
    (line-width 5)
    (draw-lines (vertex 0 0) (vertex 0 -0.5)))
   (push-matrix
    (rotate (* -6 (rem minute 60)) 0 0 1)
    (color 1 1 1)
    (line-width 2)
    (draw-lines (vertex 0 0) (vertex 0 -1)))
   (push-matrix
    (rotate (* -6 (rem second 60)) 0 0 1)
    (color 1 0 0)
    (line-width 1)
    (draw-lines (vertex 0 0) (vertex 0 -1)))))

There are a few ways we can use this function to animate a clock.

We can hand the function the time we’re given in the :display callback:

(ns example.wiki.clock1
  (:use [penumbra.opengl])
  (:require [penumbra.app :as app]))

(defn init [state]
  (app/vsync! true)
  state)

(defn reshape [[x y w h] state]
  (let [aspect (/ (float w) h)
        height (if (> 1 aspect) (/ 1.0 aspect) 1)
        aspect (max 1 aspect)]
    (ortho-view (- aspect) aspect (- height) height -1 1)
    state))

(defn display [[delta time] state]
  (draw-clock (/ time 3600) (/ time 60) time)
  (app/repaint!))

(app/start 
  {:display display, :reshape reshape, :init init} 
  {})

We can use the :update callback to alter the application state:

(ns example.wiki.clock2
  (:use [penumbra.opengl])
  (:require [penumbra.app :as app]))

(defn init [state]
  (app/vsync! true)
  state)

(defn reshape [[x y w h] state]
  (let [aspect (/ (float w) h)
        height (if (> 1 aspect) (/ 1.0 aspect) 1)
        aspect (max 1 aspect)]
    (ortho-view (- aspect) aspect (- height) height -1 1)
    state))

(defn update [[delta time] state]
  (assoc state
    :hour (+ (:hour state) (/ delta 3600))
    :minute (+ (:minute state) (/ delta 60))
    :second (+ (:second state) delta)))

(defn display [[delta time] state]
  (draw-clock (:hour state) (:minute state) (:second state))
  (app/repaint!))

(app/start
  {:display display, :reshape reshape, :update update, :init init}
  {:hour 0 :minute 0 :second 0})

Both of these examples will update the hands many times a second. But what if we don’t care about moving the second hand more than once a second, or the minute hand more than once a minute? In this case, we can use update loops. These are created using (penumbra.app/periodic-update! frequency callback-fn), where frequency is the number of times per second that callback-fn should be called. Just like the other callbacks, whenever callback-fn alters the state, it forces the application to repaint.

(ns example.wiki.clock3
  (:use [penumbra.opengl])
  (:require [penumbra.app :as app]))

(defn incrementer [k]
  (fn [state] (update-in state [k] inc)))

(defn init [state]
  (app/periodic-update! 1 (incrementer :second))
  (app/periodic-update! (/ 1 60) (incrementer :minute))
  (app/periodic-update! (/ 1 3600) (incrementer :hour))    
  state)

(defn reshape [[x y w h] state]
  (let [aspect (/ (float w) h)
        height (if (> 1 aspect) (/ 1.0 aspect) 1)
        aspect (max 1 aspect)]
    (ortho-view (- aspect) aspect (- height) height -1 1)
    state))

(defn display [[delta time] state]
  (draw-clock (:hour state) (:minute state) (:second state)))

(app/start
  {:display display, :reshape reshape, :init init}
  {:hour 0 :minute 0 :second 0})

Asynchronous updates to the state can be useful in a number of situations. In the Tetris example app, update loops are used to control the descent of the blocks. In the Asteroids example app, they are used to test for bullet/asteroid intersection, and to emit flame particles from the player’s ship. In most cases, anything in an update loop could be placed in the :update callback, but for events that aren’t tightly coupled to rendering or user input, it can be a much simpler way to think about your application.

Clone this wiki locally