Difference between revisions of "Knowledgebase:While Loop Tutorial"

From POV-Wiki
Jump to navigation Jump to search
m (Protected "Knowledgebase:While Loop Tutorial" ([edit=autoconfirmed] (indefinite) [move=autoconfirmed] (indefinite)))
(No difference)

Revision as of 18:13, 5 September 2009

While-loops in POV-Ray are so often misunderstood and misused that I decided to write a whole page about them. Usually people who have never programmed have great difficulties understanding how simple while-loops work and how they should be used. When you get into nested loops, the problem is even worse.

Sometimes even people who have programmed a bit with some language get confused with POV-Ray's while-loops. This usually happens when they have only used a for-loop which in itself has an index variable (which is often even incremented automatically).

What a while-loop is and what it is not

A while-loop in POV-Ray is just a control structure which tells POV-Ray to loop a command block while the specified condition is true (ie. until it gets false).

That is, a while-loop is like this:

#while(condition)
  ...
#end

The commands between #while and #end are run over and over as long as the condition evaluates to true.

A while-loop is not a for-loop nor any kind of loop which has an index variable by itself (which may be incremented automatically in each loop).

The while-loop does not care what the conditions are between the parentheses (as long as they evaluate to some value) or what does the block between #while and #end contain. It will just execute that block until the condition becomes false.

The while-loop does not do anything else. You can think about it as a kind of dumb loop, which doesn't do anything automatically (and this is not necessarily a bad thing).

How does a single while-loop work?

The while-loop works like this:

  1. If the condition between the parentheses evaluates to false, jump to the command after the #end statement. If the condition evaluates to true, just continue normally.
  2. At the #end statement jump to the #while statement and start again.

That is:

  • When POV-Ray gets to the #while statement it evaluates the condition between parentheses.
  • If the statement evaluated to true then it will just continue normally with the next command.
  • However, if the statement evaluated to false, POV-Ray will skip the entire body of the loop and continue from the command after the #end statement.
  • At an #end statement POV-Ray will just jump back to the corresponding #while-statement and then continue normally (ie. testing the condition and so on).

Note that nowhere there's any mention about any index variable or anything else that could be used to automatically end the loop or whatever. As said, it's just a dumb loop that continues forever if necessary, only testing the statement between the parentheses (but it's not interested in what it is, only in its evaluated value).

Although one could easily think that this kind of dumb loop is bad and it should be more intelligent and better, the fact is that this kind of dumb loop is actually a lot more flexible and versatile. It allows you to make things not possible or very difficult to do with an intelligent for-loop with automatic index variables.

How do I make a while-loop?

It depends on what you are trying to make.

The most common usage is to use it as a simple for-loop, that is, a loop which loops a certain number of times (for example 10 times) with an index value getting successive values (for example 1, 2, 3, ..., 10).

For this you need to first declare your index identifier with the first value. For example:

#declare Index = 1;

Now you want to loop 10 times. Remember how the condition worked: The while-loop loops as long as the condition is true. So it should loop as long as our Index identifier is less or equal to 10:

#while(Index <= 10)

When the Index gets the value 11 the loop ends, as it should.

Now we only have to add 1 to Index at each loop, so we should do it at the end of the loop, thus getting:

#declare Index = 1;
#while(Index <= 10)

  (some commands here)

  #declare Index = Index + 1;
#end

The incrementation before the #end is important. If we don't do it, Index would always have the value 1 and the loop would go forever since 1 is always less or equal to 10.

What happens here?

  1. First POV-Ray sets the value 1 to Index.
  2. Then it sees the #while statement and evaluates what is between the parentheses: Index <= 10
  3. As Index has the value of 1 and 1 <= 10, the condition evaluates to true.
  4. So, it just continues normally. It executes the commands following the #while statement (denoted in the above example as (some commands here)).
  5. Then it arrives normally to the last #declare command in the block. This causes the value 2 to be assigned to Index.
  6. Now it arrives the the #end command and so it just jumps to the #while.
  7. After that it executes the steps 2-6 again because also 2 is less or equal to 10.
  8. After this has been done 10 times, the value 11 is assigned to Index in the last command of the block.
  9. Now, when POV-Ray evaluates the condition it sees that it's false (because 11 is not less or equal to 10). This causes POV-Ray to jump to the command after the #end statement.
  10. The net effect of all this is that POV-Ray looped 10 times and the Index variable got successive values from 1 to 10 along the way.

If you read carefully the above description you'll notice that the looping is done in a quite dumb way, that is, there's no higher logic hidden inside the loop structure. In fact, POV-Ray doesn't have the slightest idea how many times the loop is executed and what variable is used to count the loops. It just follows orders.

The higher logic in this type of loop is in the combination of commands we wrote. The effect of this combination is that the loop works like a simple for-loop in most programming languages (like BASIC, etc).

What is a condition and how do I make one?

A condition is an expression that evaluates to a boolean value (ie. true or false) and is used in POV-Ray in #while-loops and #if-statements.

A condition is mainly a comparison between two values (although there are also some other ways of making a condition, but that's not important now). For example:

1 < 2  is true
1 > 2  is false
1 = 1  is true
1 = 2  is false

and so on.

Usually it makes no sense to make comparisons like those. However, when comparing identifiers with some value or two identifiers together it starts to be very useful. Consider this:

#if(version < 3.1)
  #error "Wrong version!"
#end

If the identifier called version has a value which is less than 3.1 the #error line will be executed. If version has a value which is 3.1 or greater, the #error line is just skipped.

You can combine conditions together with the boolean operators & (and) and | (or). You can also invert the value of a condition with ! (not).

For example, if you want something to be done when 'a' is less than 10 and b is greater or equal to 20, the condition would be:

a<10 & b>=20

For more information about these comparison operators, see the Float operators section of the POV-Ray documentation.

What about loop types other than simple for-loops?

As POV-Ray doesn't care what the condition is and what we are using to make that condition, we can use the while-loop in many other ways.

For example, this is a typical use of the while-loop which is not a simple for-loop:

#declare S = seed(0);
#declare Point = <2*rand(S)-1, 2*rand(S)-1, 2*rand(S)-1>;
#while(vlength(Point) > 1)
  #declare Point = <2*rand(S)-1, 2*rand(S)-1, 2*rand(S)-1>;
#end

What we are doing here is this: Take a random point between <-1, -1, -1> and <1, 1, 1> and if it's not inside the unit sphere take another random point in that range. Do this until we get a point inside the unit sphere.

This is not an unrealistic example since it's very handy and I have used this type of loops many times.

As we see, this has nothing to do with an ordinary for-loop:

  • It doesn't have any index value which gets consecutive values during the loop.
  • We don't know how many times it will loop. In this case it loops a random number of times.
  • For-loops are usually used to get a series of things (eg. objects). At each loop another instance of that thing is created. Here, however, we are only interested in the value that results after the loop, not the values inside it.

As we can see, a while-loop can also be used for a task of type calculate a value or some values until the result is inside a specified range (among many other tasks).

By the way, there's a variant of this kind of loop where the task is: Calculate a value until the result is inside a specified range, but make only a certain number of tries. If the value doesn't get inside that range after that number of tries, stop trying. This is used when there's a possibility for the loop for going on forever.

In the above example about the point inside the unit sphere we don't need this because the random point will surely hit the inside of the sphere at some time. In some other situations, however, we can't be so sure.

In this case we need a regular index variable to count the number of loops. If we have made that amount of loops then we stop.

Suppose that we wanted to modify our point searching program to be completely safe and to try only up to 10 times. If the point doesn't hit the inside of the sphere after 10 tries, we just give up and take the point <0,0,0>.

#declare S = seed(0);
#declare Point = <2*rand(S)-1, 2*rand(S)-1, 2*rand(S)-1>;
#declare Index = 1;
#while(Index <= 10 & vlength(Point) > 1)
  #declare Point = <2*rand(S)-1, 2*rand(S)-1, 2*rand(S)-1>;
  #declare Index = Index + 1;
#end

#if(vlength(Point) > 1)
  #declare Point = <0,0,0>
#end

What did we do?

  • We added an Index value which counts the amount of loops gone so far. It's quite similar to the index loop of a simple for-loop.
  • We added an extra condition to the while-loop: Besides testing that the point is outside the unit sphere it also tests that our index variable has not bailed out. So now there are two conditions for the loop to continue: The Index value must be less or equal to 10 and the Point has to be outside the unit sphere. If either one of them fails, the loop is ended.
  • Then we check if the point is still outside the unit sphere. If it is, we just take <0,0,0>.

Btw, sometimes it's not convenient to make the test again in the #if statement. There's another way of determining whether the loop bailed out without successful termination or not: Since the loop ends when the Index gets the value 11, we can use this to test the successfulness of the loop:

#if(Index = 11)
  (loop was not successful)
#end

What about nested loops?

Even when one masters simple loops, nested loops can be a frightening thing (or at least hard to understand).

Nested loops are used for example for creating a 2D array of objects (with rows and columns of objects), etc. For example if you want to create a 10x20 array of spheres in your scene, a nested loop is the tool for it.

There's nothing special about nested loops. You only have to pay attention to where you initialize and update your index variables.

Let's recall the form of a simple for-loop:

#declare Index = initial_value;
#while(Index <= final_value)

  [Something here]

  #declare Index = Index + index_step;
#end

The [Something here] part can be anything. If it's another while-loop, then we have nested loops. The inner loop should have the same structure as the outer one.

Note: that proper indentation helps us distinguishing between the loops. It's always a good idea to use a good indentation scheme:

#declare Index1 = initial_value1;
#while(Index1 <= final_value1)

   #declare Index2 = initial_value2;
   #while(Index2 <= final_value2)

      [Something here]

      #declare Index2 = Index2 + index2_step;
   #end

   #declare Index1 = Index1 + index1_step;
#end

It's a common mistake for beginners to break this structure. For example it's common to declare the Index2 before the first #while. This breaks the for-loop structure and thus doesn't work. If you follow step by step what POV-Ray does, as explained earlier, you will see why it doesn't work. Don't mix the structures of the inner and the outer loops together or your code will simply not work as expected.

So, if we want to make our 10x20 array of spheres, it will look like this:

#declare Index1 = 0;
#while(Index1 <= 9)

   #declare Index2 = 0;
   #while(Index2 <= 19)

      sphere { <Index1, Index2, 0>, .5 }

      #declare Index2 = Index2 + 1;
   #end

   #declare Index1 = Index1 + 1;
#end

Note: how we now start from 0 and continue to 9 in the outer loop and from 0 to 19 in the inner loop. This has been done to get the sphere array start from the origin (instead of starting from <1, 1, 0>). Of course we could have made the Index1 and Index2 go from 1 to 10 and from 1 to 20 respectively and then created the sphere in this way:

  sphere { <Index1-1, Index2-1, 0>, .5 }

Although you should not mix the loop structures together, you can perfectly use the values of the outer loop in the inner loop (eg. in its condition). For example, if we wanted to create a triangular array of spheres instead of a rectangular one (that is, we create only half of the spheres), we could have made the inner #while like this:

  #while(Index2 < Index1*2)

(Index2 will go from 0 to the value of Index1 multiplied by 2.)

There's no reason why we should limit ourselves to just two nested loops. There's virtually no limit how many loops you can nest. For example, if we wanted to create a box-shape filled by spheres rows, colums and depth, we just make three nested loops, one for the x-axis, another for the y-axis and the third for the z-axis.

Mixed-type nested loops

It is perfectly possible to put a for-loop inside a non-for-loop or vice-versa. Again, you just have to be careful (with experience it gets easier).

For example, suppose that we want to create 50 spheres which are randomly placed inside the unit sphere.

So the distinction is clear: First we need a loop to create 50 spheres (a for-loop type suffices) and then we need another loop inside it to calculate the location of the sphere. It will look like this:

#declare S = seed(0);
#declare Index = 1;
#while(Index <= 50)

   #declare Point = <2*rand(S)-1, 2*rand(S)-1, 2*rand(S)-1>;
   #while(vlength(Point) > 1)
      #declare Point = <2*rand(S)-1, 2*rand(S)-1, 2*rand(S)-1>;
   #end

   sphere { Point, .1 }

   #declare Index = Index + 1;
#end

There are some important things to note in this specific example:

  • Although this is a nested loop, the sphere is not created in the inner loop but in the outer one. The reason is clear: We want to create 50 spheres, so the sphere creation has to be inside the loop that counts to 50. The inner loop just calculates an appropriate location.
  • The seed value S is declared outside all the loops although it's used only in the inner loop. Can you guess why? (Putting it inside the outer loop would have caused an undesired result: Which one?)

Other things to note

There's no reason why the index value in your simple for-loop should step one unit at a time. Since the while-loop doesn't care how the index changes, you can change it in whichever way you want. Eg:

// Decrements the index (be careful with the while
// loop condition):
#declare Index = Index - 1;

// Increases by steps of 0.2:
#declare Index = Index + 0.2;

// Doubles the value of the index at each step:
#declare Index = Index * 2;

etc.

Be careful where you put your while-loop.

I have seen this kind of mistake more than once:

#declare Index = 1;
#while(Index <= 10)
   blob
   {  threshold 0.6
      sphere { <Index, 0, 0>, 2, 1 }
   }
   #declare Index = Index + 1;
#end

You'll probably see immediately the problem.

This code creates 10 blobs with one component each. It doesn't seem to make much sense. Most probably the user wanted to make one blob with 10 components.

Why did this mistake happen? It may be that the user (more or less subconsciously) thought that the while-loop must be the outermost control structure and do not realize that while-loops can be anywhere, also inside objects (creating subcomponents or whatever).

The correct code is, of course:

blob
{  threshold 0.6

   #declare Index = 1;
   #while(Index <= 10)

      sphere { <Index, 0, 0>, 2, 1 }

      #declare Index = Index + 1;
   #end
}

The essential difference here is that it's only the sphere code which is run 10 times instead of the whole blob code. The net result is the same as if we had written the sphere code 10 times with proper values of Index.

Be also careful with the placement of the braces. If you put them in the wrong place you can end up accidentally repeating an opening or a closing brace 10 times. Again, a proper indentation usually helps a lot with this (as seen in the above example).

Tip: You can use while-loops in conjunction with arrays to automatize the creation of long lists of elements with differing data.

Imagine that you are making something like this:

  color_map
  {  [0.00 rgb <.1,1,.6>]
     [0.05 rgb <.8,.3,.6>]
     [0.10 rgb <.3,.7,.9>]
     [0.15 rgb <1,.7,.3>]
     ...
     and so on

It's tedious to have to write the same things over and over just changing the index value and the values in the vector (even if you use copy-paste to copy the lines).

There's also one very big problem here: If you ever want to add a new color to the color map or remove a color, you would have to renumber all the indices again, which can be extremely tedious and frustrating.

Wouldn't it be nice to automatize the creation of the color map so that you only have to write the vectors and that's it?

Well, you can. Using a while-loop which reads an array of vectors:

#declare MyColors = array[20]
   {  <.1,1,.6>, <.8,.3,.6>, <.3,.7,.9>,
      <1,.7,.3>, ...
   }

...

   color_map
   {  #declare LastIndex = dimension_size(MyColors, 1)-1;
      #declare Index = 0;
      #while(Index <= LastIndex)

         [Index/LastIndex rgb MyColors[Index]]

         #declare Index = Index + 1;
      #end
   }

Now it's easy to add, remove or modify colors in your color map. Just edit the vector array (remembering to change its size number accordingly) and the while-loop will automatically calculate the right values and create the color map for yourself.

End of Topic: Back to the Table of Contents