Lesson 2: Exploring the Mandelbrot Set


Contents

Lesson 1

Lesson 3

Code Reference
mandelglow.cpp
mandeldata.h
mandeldata.cpp
mandelwind.h
mandelwind.cpp
Makefile
class Glow
class GlowComponent
class GlowSubwindow
class GlowWindow

Introduction

This lesson introduces GLOW events, and describes how you can respond to user input via mouse clicks. We modify the Mandelbrot Set viewing program from lesson 1 to include additional capabilities, including the ability to "zoom" into the set. At the end of this lesson, you should have a grasp of GLOW's event handling capabilities and a working knowledge of how to write interactive programs using GLOW.

Resizing the window

First, let's address the question we left off with in lesson 1: what to do when the user resizes the window. Ideally, we'd like to detect the event, recompute the image at the new window size, and redraw the window. Fortunately, GLOW provides a mechanism through which your window classes may receive notification of events such as a window resize. When such events occur, a corresponding virtual method in class GlowWindow is called. You may override this method to perform your own task in response to the event.

Let's take a look at the MandelWind class, in file mandelwind.h. Notice the new method "OnReshape()".

virtual void OnReshape(int width, int height);

This method is called whenever the window receives a "reshape event". This event is sent when the window is resized. GLOW passes the new width and height for the window to this method when it is called. Now, look at the implementation in mandelwind.cpp. We do two things here. First, we call glViewPort(), which updates the OpenGL viewport to encompass the entire new window area. Second, we call the SetSize() method on the MandelData object. This has the effect of resizing the data image, and marking it invalid so it will be recomputed. Don't worry about the setting of the variable _halfDiagonal yet; this is used by our zooming code.

After executing our OnReshape() method, GLOW automatically marks the window as needing update, which has the effect of causing our OnEndPaint() method to be called. Notice that we've made some changes to this method too. Notably, we've moved the code that recalculates the data and generates the pixel image out of the constructor and into OnEndPaint(). This is because this code will have to be re-executed every time the window is resized. So just before we redraw the image, we check to see if it is still valid, and if not, we recompute it.

Exactly when is OnReshape() called?

The method is called in response to three types of occurrences. First, it is called once when a window is first created. You can think of this as reshaping the window from "no size" to its initial size. Second, it is called when the user resizes the window. Third, it is also called whenever you explicitly resize a window by calling the Resize() method of GlowWindow.

Why is the recompute-code moved into OnEndPaint()? Wouldn't it make more sense to put it at the end of OnReshape()?

That's a good question, and the answer gets into a subtlety of how GLOW reports events. If you're familiar with GLUT's event-reporting mechanism, you probably remember that it attempts to condense events down to a minimal number. For example, if a window is resized, and then resized again before the first resize can be reported, the two resizes are condensed into one event, and only the last one is reported. GLOW works in the same way. Now, this in mind, note that there are many things that can cause the data to be recalculated. So far, we've seen window resizing, but later in this lesson we'll implement zooming, which can also cause a recomputation. Suppose the window is resized, and the data is zoomed, at the same time. If we recompute in response to each event, we'll recompute twice. However, both of these occurrences will cause the window to be marked dirty, which causes OnEndPaint() to be called; but GLOW will condense these two redraw events into one, and only actually call your OnEndPaint() once. By putting off recomputation of the image until the very last minute, in OnEndPaint(), we can potentially save some redundant recalculations.

This general technique is called lazy evaluation. You note that you need to perform some computation, but don't actually perform it until the last minute, in the hopes that maybe the need to do so will go away.

I looked at the entry for OnReshape() in the reference for class GlowWindow but didn't find much information. What's up with that?

That method, along with many of the methods in GlowWindow, is actually inherited from a base class. GlowWindow is a subclass of GlowSubwindow, which is itself a subclass of GlowComponent. You'll find the detailed description of OnReshape() in the GlowSubwindow reference, and the detailed description of OnEndPaint() in the GlowComponent reference. We'll learn more about subwindows and components in a later lesson. For now, just note that they're classes in a class hierarchy. In subsequent sections and subsequent lessons, we'll encounter many more such methods. The reference pages will tell you in which class the method actually originates.

Link toSource: mandelwind.h
Link toSource: mandelwind.cpp

Link toReference: class GlowComponent
Link toReference: class GlowSubwindow
Link toReference: class GlowWindow

Responding to mouse events

The real beauty of the Mandelbrot Set appears when you magnify parts of it. Let's construct a user interface for zooming in and out of the set. One simple way to do this is to use the mouse, and let the user drag out a rectangle to magnify. For simplicity, our implementation will limit these rectangles to have the same aspect ratio as the window itself, and we'll have the user drag the rectangle from center to corner. We'll also implement a similar interface to "zoom out".

About the event handling methods

Take another look at our class declaration in mandelwind.h. You'll see three virtual methods for handling mouse-related events: OnMouseDown(), OnMouseUp(), and OnMouseDrag().

virtual void OnMouseDown(Glow::MouseButton button, int x, int y,
    Glow::Modifiers modifiers);
virtual void OnMouseUp(Glow::MouseButton button, int x, int y,
    Glow::Modifiers modifiers);
virtual void OnMouseDrag(int x, int y);

OnMouseDown() is called when the user presses one of the mouse buttons. GLOW passes it a constant denoting which button was pressed, the window coordinates of the button press, and a set of flags denoting any modifier keys such as shift or alt that were down when the event occurred. OnMouseUp() is called in response to a button release. OnMouseDrag() is called repeatedly when the mouse is moved while buttons are down.

The button parameter is set to one of the values Glow::leftButton, Glow::middleButton or Glow::rightButton. Modifier flags that can be set are given by the masks Glow::shiftModifier, Glow::ctrlModifier and Glow::altModifier. The x and y coordinates are given in window coordinates. These are different from the OpenGL drawing coordinates. They start at (0,0) at the upper left of the window, and increase down and right. The bottom right pixel is denoted by (width-1,height-1), where width and height are the dimensions of the window.

Writing OnMouseDown() and OnMouseDrag()

Now, let's take a look at the implementation in a little more detail. First, OnMouseDown(). The first thing we do is check the value of _dragType, which is a flag that denotes the current status (zooming in, zooming out, or not dragging). We respond to a mouse-down only if we're not already dragging. Next, we make sure the button pressed is the left mouse button; we will ignore the other two mouse buttons for now. Next we check if the shift key is down. If it is, we go into zooming-out mode; if it isn't, we go into zooming-in mode. Note that the code for starting a zoom-out calls two methods, Width() and Height(). These are methods of GlowWindow that tell the current dimensions of the window.

...
if (button == Glow::leftButton) {
    if (modifiers & Glow::shiftModifier) {
        // Zoom out
        _xdown = Width()/2;   // Center of the window
        _ydown = Height()/2;
        _dragType = ZOOM_OUT_DRAG;
    } else {
        // Zoom in
        _xdown = x;
        _ydown = y;
        _dragType = ZOOM_IN_DRAG;
    }
    _ComputeZoomFactor(x, y);
    Refresh();  // Explicitly refresh
}
...

After setting the mode, OnMouseDown() calls a method to determine the initial zoom factor, and then calls a special method of GlowWindow called Refresh(). You can call this method to notify GLOW that the window needs to be redrawn. GLOW does not redraw the window immediately, but lazily marks the window as needing update. The actual redrawing will occur a short time later; at that point, GLOW will call your OnEndPaint(). Note that because GLOW is lazy about handling refresh requests, you can call Refresh() multiple times with little ill effect. GLOW, like GLUT, will condense the multiple refresh requests into just one, and call OnEndPaint() only once.

OnMouseDrag() is called whenever the position of the mouse changes while one of the buttons is down. We implement this method to simply update our current value for the zoom factor based on the new mouse coordinates, and then call Refresh() to update the window. OnMouseUp() does one final computing of the zoom factor., Next, it informs the data object to zoom in or out, resets _dragType to NO_DRAG, and calls Refresh().

A few more details

Now that we've implemented methods to respond to mouse events, we have two more things to do to make our zoomable Mandelbrot viewer work well. First, we should probably give some graphical feedback while the user is dragging a rectangle. We'll accomplish this by simply drawing the rectangle in gray atop the currently displayed image. Look at the end of OnEndPaint(), and you'll notice a bit of code that draws a rectangle if the user is currently dragging. Probably the only unfarmiliar part of this code is the call to the method NormalizeCoordinates(). This is a useful utility method of GlowWindow that converts window coordinates (given by the mouse events) to normalized OpenGL drawing coordinates in two dimensions, with (0,0) at the center of the window, x increasing to the right and y increasing to the top.

Second, we need to inform GLOW that our window now wants to receive mouse events. We accomplish this by modifying the constructor, changing one of the parameters from Glow::noEvents to Glow::mouseEvents | Glow::dragEvents. This sets the event mask for the window, telling GLOW to report mousedown, mouseup and mousedrag events for the window.

MandelWind::MandelWind(MandelData* data) :
GlowWindow("Mandelglow", GlowWindow::autoPosition, GlowWindow::autoPosition,
    data->Width(), data->Height(), Glow::rgbBuffer | Glow::doubleBuffer,
    Glow::mouseEvents | Glow::dragEvents)   // Receive mouse and drag events
{ ...

Why in the world did we have to set an event mask?

All GLOW events are reported via virtual methods, and each method does have a default implementation that simply ignores the event. From a functional point of view, we don't need the event mask. GLOW can call the virtual methods for every event that gets reported, and those that we're not interested in will fall through to the default, do-nothing event handler. However, there is some performance overhead involved with detecting and dispatching an event. The event mask, then, is a hint to GLOW to tell it which events a window is interested in receiving. GLOW uses this information internally to decide which events to listen for and dispatch to the virtual methods of the window object. More technically, the event mask describes which GLUT callbacks should be registered for a particular window.

When you're writing a window class for a GLOW program, be sure to pay attention to which events you want to receive. You need to implement the corresponding virtual method, AND set the appropriate bit in the event mask. If you are debugging a window class and discover that you don't seem to be receiving certain events, the first thing you should check is that your event mask is correct. (Another important thing to check is that you've spelled the names of the virtual methods correctly; that bug gets me quite often.) The constants corresponding to bits in the event mask can be found in class Glow.

I'm confused. Exactly what does Refresh() do and when does redrawing take place?

GLOW operates by raising and responding to events. "Redraw this window" is just another event that can be raised in the GLOW system. When you call Refresh(), GLOW does not immediately redraw the window. Instead, it raises a refresh event for the window in question; this event gets stuck into the event queue for later processing. Other things that can cause a refresh event to be raised include resizing the window, moving it so parts are un-obscured, de-iconifying the window, and so forth. Later, when GLOW gets around to responding to the event, it will call the OnEndPaint() method for the window, in which you actually perform the drawing.

As an example, let's consider our mouse-down handler. When the user presses the mouse button in our Mandelbrot viewer window, a mouse-down event is raised. In a short time, GLOW handles the event by calling our OnMouseDown() method. Now, while we're in this method, we call Refresh(). GLOW raises a refresh event, and inserts it into the event queue for later, but since we're still in OnMouseDown(), we're still handling the mouse-down event. (And in fact, if we now go into an infinite loop, the system will never redraw the window because it never "finishes" with the current mouse-down event.) Once we exit our mouse-down handler, GLOW resumes control and looks for the next event to handle, which may be the refresh event we raised, or it may be another event such as a window resize or a keypress.

Link toSource: mandelwind.h
Link toSource: mandelwind.cpp

Link toReference: class Glow
Link toReference: class GlowWindow

Putting it together

Here's an exercise for you. What do we need to change in the main program mandelglow.cpp to finish these modifications? The answer is... nothing! We've merely changed our window class to add some more capabilities. Our main program does not need to be changed; it still simply creates a window and then enters the GLOW main loop. We do not need to register new callbacks. GLOW handles this automatically for us, based on the information we provided in the window class.

Compile and run the program. Now notice that you can resize the window, and the program will adjust the image size accordingly. Try zooming in and out, by dragging rectangles using the left mouse button (with and without the shift key down). The Mandelbrot set includes some pretty intricate structures near the edges at high magnifications.

Link toSource: mandelglow.cpp

Where to go from here

At this point, you may want to try experimenting with different colors. Mandelbrot images can appear very beautiful when you color pixels differently according to the value computed. As an example, try a rainbow pattern: map values from 1 to 256 to colors interpolated between red and green, then values from 257 to 512 to colors interpolated between green and blue, and so forth up to 1024. You'll need to change the image computation code in MandelWind::OnEndPaint() to accomplish this. Now try zooming in towards the edge of the set. Neato! Another value that has an effect on the appearance of Mandelbrot images is the iteration threshhold, which you can set using the method MandelData::SetThreshhold(). Its default value is 1000. Smaller values result in more black; larger values result in slightly less black. The amount of time necessary for computation is generally proportional to this value. In actuality, a value of infinity will generate a perfect Mandelbrot set (but is impractical for computation!), and finite values will generate approximations.

Now that you know how to respond to mouse events, you can experiment with a few of the other event types. Try this as an exercise. Modify the window class so pressing the "A" key causes the zoom to revert to its original state. (Hint: you'll need to override the method GlowWindow::OnKeyboard(), and don't forget to add keyboard events to the event mask.)

In the next lesson we'll see how to add more user interface features through menus, and we'll look at GLOW's powerful generalized event reporting mechanism, through which you can define your own events.



Contents

Lesson 1

Lesson 3


The GLOW Toolkit