The Rendering Loop

This page contains advanced, low-level information for people who are ready to create their own custom effect algorithms. Not to scare you off, but don’t be discouraged if you are new to Afterglow, and this content seems rather difficult. Start experimenting in other places, and by the time you need this information, it will make much more sense!

And once you are ready to really dive deep, you can learn how to extend the rendering loop to incorporate completely new kinds of elements, such as integrating with Pangolin Beyond laser shows.

Frame Rendering Stages

When an afterglow show is running, that is, from when (show/start!) has been called, until (show/stop!) or (show/stop-all!) is called, there is a background task scheduled to run many times per second, to calculate the next “frame” of control values to send to the universes controlled by the show, and then send those values. The rate at which this activity is scheduled is determined by the refresh-interval value established when the show was created. If not explicitly set as a parameter to (show/show), an interval of 25 milliseconds is used, causing the lights to be updated forty times each second. If your DMX interface is running at a different rate, you will want to configure your show to match it, so that you are getting the best results possible without wasting computation on frames that never get seen.

Once a show has started running, you can get a sense of how heavily it is taxing your hardware by looking at the show’s :statistics atom:
(clojure.pprint/pprint @(:statistics *show*))
; {:afterglow-version "0.1.0-SNAPSHOT",
;  :total-time 70429,
;  :frames-sent 105828,
;  :average-duration 0.6655044,
;  :recent #amalloy/ring-buffer [30 (0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1 0 0 1)],
;  :recent-total 6,
;  :recent-average 0.2}
; -> nil

This tells you how many milliseconds have been spent in the rendering loop while the show is running, how many frames of DMX data have been sent to the show universes, and the average number of milliseconds spent in the rendering loop. If the duration of a rendering cycle ever exceeds the refresh interval, Afterglow will log a warning that it is unable to keep up with the effects you are trying to run.

If you have a show’s web interface open, the :recent keys in this atom are used to display the Load bar at the top of the screen, which fills up and turns red as the time within each frame available for calculating and sending control values to the lights gets used up.

When it is time to determine the next set of values to send to the show’s patched fixtures, this is what happens:

  1. The first thing Afterglow does is take a snapshot of the show metronome to identify a common point in time for all the effects to use in deciding how they should look. This enables a consistent, coordinated appearance for everything in the frame.

  2. The next step is to zero out the buffers that will be used to send DMX data for each universe, so that if no effect tries to set a value for a particular channel on this frame, a zero value will be sent for it.

  3. Then Afterglow loops over all Effects that are currently active for the show, and asks if any are ready to end, as described in the Effect Lifecycle section. If any are, they are removed from the list of active effects.

  4. Then it makes another loop over any effects that did not end, to see how they want to affect the lights. It does this by calling, in order, each effect’s generate function, passing in the show and the metronome snapshot. In order to make the lights do things, each effect returns a list of Assigners specifying the things it wants to happen. These are gathered in the order they were returned by each effect.

  5. While being gathered, the assigners are separated into individual lists, divided first by the kind value of the assigner (:channel, :function, :color, :pan-tilt, :direction, or :aim, or some other value added by a rendering loop extension), and within each kind, further divided into lists based on the target ID that the assigner wants to affect. The structure of a target ID is up to the Assigner kind, and may be a simple number or tuple, depending on the needs of the Assigner implementation; the important thing from the Show’s perspective is that if two target IDs are equal to each other, the assigners are affecting the same element of the show.
    Since the effects are run in priority order (lower priority first, with effects of the same priority running in the order in which they were added to the show), higher-priority and more recent effects’ assigners will come later in the gathered lists, and will get the chance to modify or veto any assigners from earlier and lower-priority effects which are trying to control the same thing. If the effect doesn’t want to do anything this frame, it can simply return an empty assigner list.

  6. Once all the assigners have been collected into their kind/target lists, each list is evaluated. The assigner kinds are processed in the order established by show/resolution-order and starts with low-level, single channel :channel assigners, then moves up to more complex :function, :color, :pan-tilt, :direction, and :aim assigners. (This order was chosen because different kinds of assigners might end up affecting the same DMX channel; the higher levels of abstraction are allowed to win by running last.) Within each kind, however, the lists can be processed in parallel, since they will all affect separate targets.

    To process a list, each assigner’s assign function is called, again passing in the show and the metronome snapshot, the target which is being assigned (a DMX channel in a show universe, or a fixture or fixture head, depending on the assigner kind), as well as the assignment value the effect wanted to establish (a number, color, or head direction, again depending on the assigner type), and the previous assignment (if any) that an earlier assigner wanted to set for this target. The assigner can decide what to do with the previous assignment: Ignore it, blend the current assignment with it somehow, or honor it, depending on the nature and configuration of the assigner. The assign function returns a single resolved value of the appropriate type for the assignment, and Afterglow records it, potentially to pass it to the next assigner on the list. The assigner can also veto any previous assignment and say that nothing should happen by returning an assignment with nil for its value.

    The input to assign might be a Dynamic Parameter, and the resulting value may be as well, or the assigner may choose to resolve it into a non-dynamic value, in order to decide between or blend competing assignments.

    At the end of this process, Afterglow is left with a single assigned value for every target which any effect wanted to influence for the current frame.

  7. Afterglow uses these assignment results to establish actual DMX values for the frame, using the resolve-assignment multimethod, whose implementations (which are specific for each of the possible assignment kinds) finally resolve any remaining dynamic parameters, and then turn abstractions like color objects and aiming vectors into appropriate DMX channel values for the target that is being assigned.

  8. Finally, the resulting buffers of DMX values, with zeros in any channels which were not affected by assigners, are sent to their corresponding universes, causing the lights to produce the desired effects.

Assigners

As described above, the role of an assigner in the rendering loop is to actually decide what value (color, direction, or the like) is going to be sent to a target (a lighting fixture head for more abstract assigners, or a simple DMX channel for Channel assigners), at a given point in time. It really is the heart of implementing an effect.

The assigner fulfills this responsibility by implementing the assign function in the IAssigner protocol. It is passed the show, the metronome snapshot which identifies the point in musical time that has been reached, the target being assigned, and the value that any earlier assigners of the same type have decided should be assigned to the target.

It performs its magic, using the values established in setting up the effect, and the algorithm that the effect author designed, to come up with the resulting value that it wants assigned to the target, which may or may not be influenced by the previous assignment, and returns that value for Afterglow to either use, or pass on to the next assigner of that type in the effect chain.

The best way to understand this is probably to look at examples of effects that ship with Afterglow, starting with simple ones like color-effect, dimmer-effect, and direction-effect, then slightly more complex strobe and sparkle effects, and on up to more sophisticated compound effects like color-cycle-chase, and the spatially mapped elaborations of it like iris-out-color-cycle-chase.

Once you can understand how all of those pieces fit together, you will be ready to build your own complex and mesmerizing effects!

Channel Assigners

Channel assigners have a kind of :channel, and their target-id is a tuple of universe ID and channel address, so [1 234] would represent an assignment to universe 1, address 234. The assignment values they return are either a valid DMX data value (see next paragraph), a dynamic parameter which will resolve to a valid DMX data value, or nil, meaning no assignment should take place.

The DMX data value is a number in the range [0-256). In other words, it can take any value from zero up to but not reaching 256. Non-integer values are supported, because the channel might be a fine-channel which uses two bytes to offer more precision in control than a single byte can offer. In that case, the integer portion of the value is sent as the most-significant byte on the main channel, and the fractional portion is converted to a least-significant byte and sent on the fine channel. If the channel does not have a fine channel attached to it, any fractional part of the assigned value is simply discarded.

Channels can also be inverted, which means the DMX values are reversed from the value being assigned. This is needed to support some fixtures which have inverted dimmers, is established by the presence of an :inverted-from entry in the channel specification, and taken care of by apply-channel-value, which is invoked by the channel assignment resolver, so channel assigners do not need to worry about this detail, and can always work in terms of non-inverted channel values. (This is important, for example, when implementing highest-takes-precedence rules for a dimmer channel. Bigger numbers will always mean brighter, even if at the last step before sending them to the fixture they are inverted because of the nature of the channel.)

Function Assigners

Function assigners have a kind of :function, and their target-id is a tuple of the head or fixture ID and the function keyword, so [3 :strobe] would represent an assignment to the fixture or head with ID 3, setting the value of that head’s :strobe function. The assignment values they return are either a percentage value, a dynamic parameter which will resolve to a percentage value, or nil, meaning no assignment should take place.

When the assignment is resolved, the percentage is translated to an actual DMX value along the range defined in each fixture’s function specification. For example, if the function was defined as existing on the range 20-29 for a particular fixture, and the assigned percentage was 50.0, then the assignment for that fixture would send a value of 25 to the function’s channel.

Color Assigners

Color assigners have a kind of :color, and their target-id is the head or fixture ID; 42 would represent an assignment to the fixture or head with ID 42. The assignment values they return are either a color object, a dynamic parameter which will resolve to a color object, or nil, meaning no assignment should take place.

When the assignment is resolved, Afterglow uses all available color channels in the target head to mix the specified color. It is automatically able to use :color intensity channels of type :red, :green, :blue, and :white. It will also use any other :color channels whose hue has been specified in the fixture definition.

If the head or fixture uses a color wheel to make colors, rather than trying to mix colors using channel intensities, Afterglow will find the color wheel hue closest to the hue of the color being assigned, and send the function value needed to set the color wheel to that position. The color wheel hue has to be “close enough” to the assigned hue for Afterglow to use it. By default, as long as the hue values are within 60° of each other (which is very lenient), Afterglow will use it. You can adjust this tolerance by setting a different value in the show variable :color-wheel-hue-tolerance. The color being assigned must also have a saturation of at least 40% for the color wheel to be considered (this minimum saturation can be adjusted by setting a different value in the show variable :color-wheel-min-saturation).

Pan/Tilt Assigners

Pan/Tilt assigners have a kind of :pan-tilt, and their target-id is the head or fixture ID; 68 would represent an assignment to the fixture or head with ID 68. The assignment values they return are either a javax.vecmath.Vector2d, a dynamic parameter which will resolve to a Vector2d object, or nil, meaning no assignment should take place.

When the assignment is resolved, the vector indicates the pan and tilt angles away from the z axis of the frame of reference of the show to aim the fixture or head. Afterglow translates this vector to the appropriate values to send to the fixture’s pan and tilt channels to aim it in the specified direction, if possible. Otherwise it gets as close as the fixture allows.

If multiple fixtures or heads are assigned the same pan-tilt vector, they will all be aimed in exactly the same direction, regardless of the location and orientation with which they were hung.

If there is an active Direction or Aim Assigner which affects the same target, it will run later, so its effects will be the ones that matter.

Direction Assigners

Direction assigners have a kind of :direction, and their target-id is the head or fixture ID; 42 would represent an assignment to the the fixture or head with ID 42. The assignment values they return are either a javax.vecmath.Vector3d, a dynamic parameter which will resolve to a Vector3d object, or nil, meaning no assignment should take place.

When the assignment is resolved, the vector indicates the direction in the frame of reference of the show to aim the fixture or head. Afterglow translates this vector to the appropriate values to send to the fixture’s pan and tilt channels to aim it in the specified direction, if possible. Otherwise it gets as close as the fixture allows.

If multiple fixtures or heads are assigned the same direction vector, they will all be aimed in exactly the same direction, regardless of the location and orientation with which they were hung.

If there is an active Aim Assigner which affects the same target, it will run later, so its effects will be the ones that matter.

Aim Assigners

Aim assigners have a kind of :aim, and their target-id is the head or fixture ID; 17 would represent an assignment to the fixture or head with ID 17. The assignment values they return are either a javax.vecmath.Point3d, a dynamic parameter which will resolve to a Point3d object, or nil, meaning no assignment should take place.

When the assignment is resolved, the point identifies the precise location in the frame of reference of the show to aim the fixture or head. Afterglow translates this point to the appropriate values to send to the fixture’s pan and tilt channels to aim it at that exact spot, if possible. Otherwise it gets as close as the fixture allows.

If multiple fixtures or heads are assigned the same aiming point, they will all be aimed at exactly the same spot, regardless of the location and orientation with which they were hung.

Extensions

If you want Afterglow to control something that does not respond to DMX values, you might be able to do so by extending the rendering loop. There is an example of doing just this to control laser shows by communicating with Pangolin’s Beyond software in the afterglow.beyond namespace, and another example in afterglow.effects.show-variable, which creates effects that set show variables when they are run.

Introducing New Assigner Types

The first thing you need to do is identify the kinds of assigners that your new effect types will need. They will need their own unique kind keywords, and a structure for their target-id values which lets Afterglow keep track of which assigners are affecting the same value. The Beyond integration uses :beyond-color and :beyond-cue for kind values. :beyond-color is global, and thus uses a target-id that references the entire Beyond server instance. In contrast, more than one :beyond-cue can be active at once, so its target-id is composed of both the server ID and the cue coordinates.

Afterglow needs to be told how to handle your new kinds of assigners. First, you need to establish the order in which they should be run by calling show/set-extension-resolution-order! with your unique extension key and the list of all your assigner types in the order in which they should be resolved. You need to do this even if you don’t care about the order, or have only one new assigner type, in order to get them added to stage 6 of the frame rendering process, as described above. This is done towards the end of the Beyond extension source, if you would like to see a concrete example.

Then you need to tell afterglow how to actually resolve one of your assigners. You do this in the same way Afterglow registers its own built-in assigners, by using defmethod to add a new implementation of the resolve-assignment multimethod, for your new assigner keyword. Again, the end of the Beyond integration provides a concrete example.

Customizing Fades for your Assigner Types

If you want to support smooth fades between different values being returned by your assigners, you will also want to defmethod an implementation of the fade-between-assignments multimethod. This is the last thing that the Beyond integration does.

If you do not provide an implementation of fade-between-assignments tailored to your specific assigner kind, the default implementation is used: it simply selects whichever assigner is on the side of the fade which is currently above 50%.

Buffering and Sending Your Frame Data

Chances are good that your extension will need to do some sort of setup at the start of a frame before your assigners can be resolved, and then will want to actually do something when the frame is rendered and being sent to the lights. To accomplish these tasks, you register functions with a show: add-empty-buffer-fn! tells the show to call the supplied function when a frame is about to be rendered, allowing you to set up any buffers your assigners will need, and add-send-buffer-fn! tells the show to call the supplied function when it is time to actually send out the frame. The Beyond integration calls these in its bind-to-show function.

Having done all these things, it becomes possible to create cues which launch or end Beyond laser cues, and effects which change the color of the laser beam to match (or contrast with) colors being sent to the lights, as well as effects which simply set show variables so that other effects can respond to the fact that they are running. Perhaps looking at these example implementations can help inspire your own extension in a completely new direction! (Links to the namespaces' API documentation are at the top of this section, and as always, the API docs have view source buttons which take you right to the code that makes them work.)