Figure 1: Caution: Electrified 1911 Goudy Bookletter Ampersand. Please do not stand in any water puddles or other conductive liquids while following this tutorial. Thank you.
A -do … -while block repeatly executes until the condition given as an argument to -while becomes false. Execution then continues with the commands following -while.
The format of the command is:
-do <statements> …-while <condition>
The condition is an expression reducing to either a numeral or a string. So long as the numeral is non-zero or the string resolves to a existing file (boolean true), the commands, if any, enclosed in the -do … -while block execute repeatedly. Zero or a file that can no longer be found (boolean false) stops the iteration.
Commands enclosed within a -do … -while block always execute at least once. Following an initial pass through the block, the interpreter evaluates the condition expression for the first time and only if it is true, recommences execution at the first command following -do. Otherwise the interpreter proceeds to the next command following -while.
The commands enclosed in the -do … -while block never cease executing if the condition remains true. In anticipation of this possibility, a programmer can build an 'escape hatch' within the body of the -do … -while block, using the -break command, usually in the body of an -if … -endif block. Similarly, a -continue command terminates the current iteration and forces an immediate re-evaluation of -while's conditional argument.
Usually, the commands constituting the body of the -do … -while block have some direct or indirect effect on the condition given as an argument to -while. Searching for items that meet certain criteria is a case where one uses a -do … -while block; eventually finding items that match the search criteria flips the condition given to -while, finishing the search loop.
Two points which are distant "as the crow flies" may be effectively close at hand when connected by a smooth-surface, graded road. On the other hand, a point "nearby" in terms of flying crows is, as a practical matter, distant when there are large mountains or oceans, or implacibly fierce Corgis inhabiting the intervening regions. When factors other than spatial separation alter the effective distances between points, the -distance command can take a "cost map" as an argument, which account for such factors and which alters how the -distance command records the separation between points and isovalues.
This map assigns a cost to each pixel and when such a map is in place, the distance between any two points becomes the sum of costs of intervening points. This gives rise to a minimal path, that collection of intervening points which sum to the smallest cost, the effective "shortest distance" connecting two points. The minimal path need not be a straight line; it may even look like a river or a lightning bolt.
The -distance command produces a potental or return map as an additional output when given a cost map and mode arguments of three or four (low- or high-connectivity Dijkstra metric functions, respectively. See -label for further remarks on connectivity). The potential map produces minimal paths connecting arbitrary points to isovalues — that particular value establishing reference points from which one measures distances.
For each pixel in the selected image, the potential map contains a routing directive, an unsigned eight bit integer encoding the direction that some imagined cursor should go when taking one step on a minimal path. Every pixel in the potential map is a routing directive on at least one minimal path. When we place our imagined cursor anywhere in the potential map, it reads the routing directive from the pixel it is sitting on to determine the next step it should take to follow some minimal path to the nearest isovalue. We generally read pixels from a potential map and chart our course on a separate image.
The routing directives which populate potential maps look like this:
A routing directive consists of three, two bit nibbles preceded by an unused nibble containing two unset (zero) bits. From the most significant bit to the least, the nibbles dictate z (depth), y (vertical), or x (horizontal) movement. In each nibble, the most significant bit flags forward (positive) movement; the least significant bit flags reverse (negative) movement.
Commencing from an arbitrary pixel with a non-zero routing directive, the imagined cursor steps once to the neighboring pixel that may or may not be closer to the nearest isovalue "as the crow flies", but is on the cheapest path to that point, according to the -distance command with cost map that computed the routing directive. The cursor then assesses the routing directive of the neighboring pixel and if that too is non-zero, steps furher on, repeating the cycle until encountering a routing directives equal to zero. That is, all three nibbles have unset forward and reverse directives; the cursor has arrived at a point corresponding to an isovalue in the original image. This permanently stops the imagined cursor. Without a direction in which to move, it can no longer obtain new routing directives. The course it has traced from its given arbitrary starting pixel to the isovalue corresponds to the minimal cost path between the initial pixel and the isovalue. In a properly composed potential map, zero-valued routing directives correspond to isovalues in the selected image and all other pixels have non-zero routing directives so composed as to steer an imagined cursor toward the nearest isovalue.
The lightning bold crackling fiercely in the frontpiece stems from some 180 arbitrarily chosen points, from which minimal paths were traced to a single isovalue. For the cost map, we rendered a mode 0 -turbulence pattern, further sharpened through inverse diffusion. We then set an isovalue of one in an otherwise black image at a location coincident to the minimal value of the turbulence image.
Astute readers will, no doubt, recognize that we are playing the Watershed Game, tracing the paths of so many droplets dribbling their ways downslope to the basin containing the minimum point. This behavior follows readily from the turbulence-generated cost map. This particular turbulence forms discontinuous rilles which tend to snake in river-like ways. They are also local minima, cheap pixels over which one may traverse when the turbulence is used as a cost map. Our game for river or lightning bolt emulation is straightforward: harness -distance to generate a potential map corresponding to a turbulence image. Then, for a supply of a few hundred arbitrarily chosen pixels, trace the minimal paths of each to the isovalue. If we render these paths in dark gray and then composite them through an additive operator, the more heavily traveled paths will tend to whites and light greys; less traveled paths will be darker. Particulars follow.
gmic -srand 9717 \ -input 1024,1024,1,1 \ -turbulence[-1] 512,12,3,0,0 \ |
|
![]() |
The genesis of the frontpiece illustration is mode 0 -turbulence. We base the height, cost and potential map on this underlying rendering. As noted in the -plasma tutorial, it is our wont to set a particular seed in G'MIC's random number generator (via -srand) so that we can reproduce a particular pattern; what we have here may as well be called "Turbulence Pattern 9717". Mode 0 turbulence employs the -abs command as a mixer; at each stage of its development, any zero-crossings are folded back into the positive region, generating numerous discontinuities – creases – both large and small. These emulate rilles and waterways, somewhat. Being local minima, the rilles have low intensities; when we derive a cost map from this turbulence, rivers or lightning bolts will tend to follow these rilles. We revisit this idea downstream from here. We render the turbulence so that it exhibits large-scale features, develop the pattern through 12 octaves and set a modest decay factor of three between each octave. While large-scale features are present, low-magnitude, small-scale features persist. |
-input http://particularart.com/static/media/uploads/command_reference/repeat/ampersand_goudybl1911.svg \ -resize[-1] [-2],[-2],[-2],[-2],5,0 \ -blur[-1] 11,1,0 \ -normalize[-1] 0.4,1 \ -mul[-2,-1] \ -sharpen 50 \ --sqr[-1] \ |
|
![]() |
From this point forward, we modify the turbulence patterns in various ways to make our height, cost and potential maps. We anticipate that as lightning bolts trace along minimal paths, those will thread through the lower elevation ampersand on the way to the isovalue. Once we have blurred and scaled the ampersand, we multiply it with the original turbulence, giving us the final form of our height map. Our cost map follows directly from sharpening and squaring operations. Sharpening tends to magnify fine detail, alternately, increasing or decreasing the values of out-of-bound pixels, driving them from the mean value of the image and towards its extremes. Squaring makes expensive (light colored) pixels even more expensive. In the larger scheme of things, we are making a cost map where minimal paths will meander a great deal, as we are creating pits and rises and other obstacles that mitigate against the straightforward progression of minimal paths to the isovalue. |
-input 100%,100%,100%,100% \ -set[-1] 1,'{xm#-3}','{ym#-3}'\ -distance[-1] 1,[-2],4 \ -output[-1] potmap.cimg,uchar \ -output[0] height.png \ |
|
![]() |
We harness the -distance command to make a potential map. To that end, we require an image in which we can measure distances. We don't have one, so we conjure a black image from the aether and set a single pixel to isovalue one within it, this pixel situated in the same location as the minimal point in our turbulence-based cost map. It is to this point that all lightning bolts meander when we play the Watershed Game. Don't let the black uniformity of this conjured image fool you. In playing the Watershed Game, we will harness a special form of the -distance command to associate our -turbulence-generated cost map with this seemingly uniform space. When we do that, the cost map will set a 'traversal price' for every pixel in this uniform seeming image. If a pixel in the cost map is white, or very nearly so, the corresponding pixel in the conjured image becomes expensive to cross. In contrast, pixels corresponding to very nearly black counterparts in the cost map are very nearly free. The special form of the -distance command accounts for the non-uniform traversal costs of pixels set by the cost map and makes a new map, the It is worthwhile to comment on the math parser notation that allows us to extract the minimal point from our turbulence-based cost map and translate it to the isovalue-one pixel in our conjured image. G'MIC regards strings within curley braces ({}) as substitution sequences, and math expressions appearing within substitution sequences simply evaluate themselves, then replace themselves with their computed values. G'MIC math expressions have a host of predefined variables that convey metrics about images. Predefined variables xm and ym contain the coordinates of the minimal pixel value; for multi-slice images with depth, add zm to your kit. On the chance that two or more pixels vie for the minimal (or any other metric) distinction, G'MIC chooses the pixel with the smallest depth, column and row coordinates – that is, the first qualifying pixel in the image stream. These and all predefined math parser variables, unadorned, implicitly refer to the metrics of the last image on the image list. Variables may be postfixed with a hashtag (#) followed by a list index to explicity choose an image on the list. Thus, the math parser substitutes the expression xm#-3 with the column (x) coordinate of the first minimal pixel in the third image from the end of the image list. Had we omitted the negative sign, we would have referenced the All We invoke the -distance[-1] 1,[-2],4 command in a special way to associate a cost map – the -turbulence graphic – with our conjured image currently sitting in the last slot of the image list. The cost map, with its origins in the -turbulence generator, ensures that the cheapest path from any pixel in the conjured image to the isovalue is not necessarily the direct line connecting the two points.
We save out the first image on the list, the height map, depicted on the immediate left. Downstream, it is involved with -light_relief and other fake lighting methods, which many of you will think of as "bump mapping". Finally, we save out the potential map, taking care to specify to the -output command a particular data type, unsigned character, to prevent the default conversion to floating point, a calamity which would destroy the routing directives stored in this map. Observe, too, that we save the potential map in G'MIC's native CIMG format to further avoid the inadvertant casting of the routing directives into an unreadable format. |
![]() |
The conventional output from -distance is a displacement map which furnishes point-wise offsets of each pixel in the original image to the nearest isovalue. For the Great Watershed Game, we use the potential map instead and throw away the displacement map, unused. The potential map set is a point-wise collection of control words, described above, which we call routing directives, one corresponding to each pixel in the conjured image. Given a pixel, the associated routing directive conveys to an imagined cursor the least expensive direction it needs to step in order to move "closer" to the isovalue, in quotes because the routing directive may shift the imagined cursor away from the isovalue, at least in terms of Euclidean distance. However, when -distance is furnished with a point-wise cost map, as we have done here, the "shortest distance" is the minimal path with the lowest cost sums, which likely is not the shortest distance by Euclidean measure. On the left, we have schematically reproduced the 9×9 pixel neighborhood around the minimum point in the potential map. The numerals reprsent routing directives, each with three two-bit directional fields that collectively move an imagined cursor away from, or toward the isovalue, along each cardinal direction. Numerals 1, 5, 4, 6, 2, 10, 8 and 9 direct an imagined cursor, respectively, west, northwest, north, northeast, east, southeast, south or sourthwest. The special routing directive 0 tells the cursor not to move; once a cursor is given such a directive it necessarily stalls, as it has been told to go nowhere so can never acquire another directive. The -distance command computes these point-wise routing directives based on the cost map; in aggregate, these routing directives put an imagined cursor at a given point on a minimal path toward the nearest isovalue, one that accumulates the least cost along its length. This brings us to the Watershed Game. Both rivers and lightning bolts, among other phenomena, attempt to minimize a cost of travelling from an initial to a destination point. Armed with a potential map such as the one on the left, we can emulate droplets of water streaming from arbitrary locations towards some minimum point. Our working mechanism is a -do … -while block: while routing directives are non-zero, step in the direction it gives us, then read a new directive. Repeat the block. Continue doing so until we read a zero-valued routing directive. We have arrived at the minimum point and the path followed respresents the smallest cost possible from our starting point, whatever that may be. In doing this, of course, we trust that -distance has computed all of the routing directives correctly. Particulars follow. |
gmic pcnt=180 -input potmap.cimg \ -input 100%,100%,100%,100% \ -input '{$pcnt}',2,1,1,'round(1024*u)' \ |
|
![]() |
To play the Watershed Game, we -input three "images" (in the broad G'MIC sense, meaning they might not be pretty to look at).
From each of these 180 coordinates, we seek the minimal path to the isovalue, the one point in the potential map with a routing directive of zero. How to we actually follow a minimal path? One implementation of a minimal path follower is next on tap. Repeating for each pair in our bag of coordinates, we do a minimal path tracking, running from each of the randomly generated coordinates to the single isovalue we had initially set. |
-repeat '{$pcnt}' \ rdir=0 \ kx='{i(#-1,$>,0,0,0,0)}' \ ky='{i(#-1,$>,1,0,0,0)}' \ -do \ -set[-2] '{i(#-2,$kx,$ky,0,0,0)+1}','{$kx}','{$ky}',0,0 \ rdir='{i(#-3,$kx,$ky,0,0,0)}' \ -if '{$rdir&1}' \ kx='{$kx-1}' \ -elif '{$rdir&2}' \ kx='{$kx+1}' \ -endif \ -if '{$rdir&4}' \ ky='{$ky-1}' \ -elif '{$rdir&8}' \ ky='{$ky+1}' \ -endif \ -while '{$rdir>0}' \ -done \ |
|
![]() |
And this, in its entirety, is the Watershed Game: We iterate over our 180 points, and for each randomly chosen coordinate, trace the minimal path from it to the isovalue. The tracing is an example of a -do … -while block. While routing directives remain non-zero, we step to the pixel the directive indicates and read the next directive. We keep on doing that until we read the zero directive, which stops the iteration. This gives us one meandering river, or lightning bolt, depending on how you want to interpret it. We do this for as many randomly generated coordinates as we have. The body of the -do … -while loop concerns itself with parsing the routing directive, with the current directive retained in $rdir. We initialize $kx and $ky with the current randomly chosen coordinate, then successively mask $rdir against powers of two so as to read its depth, column and row directional nibbles, as described above. For example, the math parser expression {$rdir&1} evaluates to '1' if the decrement x nibble is set. These expresssions parse $rdir's bit fields and increments or decrements $kx and $ky respectively, advancing our cursor along the minimal path. We don't bother with the depth field, since our rivers/lightning bolts are flat, but we could. In fact, there is a G'MIC command which is a complete implementation of the partial effort here: see -minimal_path. We drop out of the -do … -while loop when our traversal of the minimal path hits upon the directive corresponding to the isovalue. All nibbles are unset in that directive; the cursor has nowhere else to go. The outer -repeat …&nobsp;-done block gives us another randomly generated starting coordinate to trace a subsequent minimal path. When our supply of randomly generated coordinates is exhaused, we are -done. There are some artistic things we do as side effects. When stepping onto a pixel, we mark our passage by bumping its intensity by one, to wit: -set[-2] '{i(#-2,$kx,$ky,0,0,0)+1}','{$kx}','{$ky}',0,0. The two or three inexpensive approaches to the isovalue tend to be shared by many minimal paths; these become very nearly white, while the less traveled segments remain almost black. The line which increments the pixel's intensity illustrate another substitution sequence. A comma-separated list of complete image coordinates, width, height, depth, slice, and channel, fetches the current pixel value at that locale, here, on the penultimate position (-2) of the image stack. |
-rm[-3,-1] \ -add[-1] 1e-10 \ -log[-1] \ -div[-1] '{log($pcnt)}' \ -cut -1,'{iM}' \ -normalize[-1] 0,255 \ -dilate[-1] 3 \ -repeat 2 \ -smooth[-1] 10,0.2,0.8,5,3 \ -done \ -normalize[-1] 0,255 \ -output[-1] lbolt.png \ |
|
![]() ![]() |
Once we have plotted our 180 paths, we rescale the the intensity levels according to a logarithmic scale so that the 'less traveled' paths are more readly visible. We dilate, then anisotropically smooth the pathways. We used pretty aggressive smoothing here, giving flame-like bolts, but our choices here are a matter of taste. Less aggressive smoothing lends itself to more jagged electrical-like bolts. |
Garry Osgood