Also, several Povray functions (both built in and include file based) are going to be used and discussed

- Float
functions: seed, rand, vdot,
vlength, and acos

- Vector
functions: vnormalize,
min_extend, max_extend

- transforms.inc functions: Reorient_Trans

- Obj - the object to be covered in snow
- Particles - the number of snowflakes to coat the object with
- Size - the snowflake radius

- Thickness - how thick the snow layer is
- MinHeight - the lowest hight
that snow will fall from. Normally set equal to MaxHeight

- MaxHeight - the maximum
height that snow will fall from. 1 is the highest point on the object.

To start with, lets walk through the macro's initial setup.

0:
#include
"transforms.inc"

1: #macro MakeSnow(Obj,Particles,Size,Thickness,MinHeight,MaxHeight,Direction)

2: #ifndef (T_Snow)

3: #local T_Snow=texture{

4: pigment{rgb 1}

5: finish{ambient 0 diffuse 1}

6: }

7: #end

8: #ifndef (rd)

9: #local rd=seed(2003);

10: #end

11: #local Obj2=object{Obj Reorient_Trans(-y,<-Direction.x,Direction.y,-Direction.z>)}

12: #local Min=min_extent(Obj2);

13: #local Max=max_extent(Obj2);

1: #macro MakeSnow(Obj,Particles,Size,Thickness,MinHeight,MaxHeight,Direction)

2: #ifndef (T_Snow)

3: #local T_Snow=texture{

4: pigment{rgb 1}

5: finish{ambient 0 diffuse 1}

6: }

7: #end

8: #ifndef (rd)

9: #local rd=seed(2003);

10: #end

11: #local Obj2=object{Obj Reorient_Trans(-y,<-Direction.x,Direction.y,-Direction.z>)}

12: #local Min=min_extent(Obj2);

13: #local Max=max_extent(Obj2);

After an include (line 0) and the macro declaration (line 1), the macro proceeds to establish a couple user overridable variables it'll need later on. In lines 2 - 7, the T_Snow texture is created if it's not already defined before the macro was invoked. By using this method, it allows the user to override the snow texture by declaring it before invoking MakeSnow. In the same style, a random number seed, rd, is declared in lines 8 - 10. The user could override the declaration if they wanted a different random numbers to vary the snow distribution in their rendering.

Line 11 is the first complex function performed in the macro. The Direction vector that's passed into the MakeSnow macro defines the direction the snow is falling. What happens here is that another object, Obj2, is created that's a rotated version of the original object, Obj, that the macro's being used on. The result is that snow falling along the vector Direction at Obj will look the same as snow falling along the vector -y at Obj2. Believe it or not, this will make the math easier later.

To so what happens when you use the Reorient_Trans function, look at the two examples below. The first image is a cone pointing straight up toward the camera. The second image uses the Reorient_Trans function to change the direction the cone is pointing.

Camera = <0, 5,
0> No reorientation |
Camera = <0, 5,
0> Reorient_Trans(-y,x-y-z) |

Finally, we determine the space the object is in. This is done using the min_extent and max_extent functions. These functions essentially return the bounding box for Obj2. If we were to construct the box, box { Min, Max }, it would completely surround Obj2 with no bits sticking out. Also, Max.x - Min.x would tell me how wide Obj2 was in the X direction.

In the example below, the semi-transparent red square shows the bounding box around the cone created with the min_extent and max_extent function

14:
blob{

15: threshold 0.6

16: #local i=0; // first layer

17: #debug "first layer of snow\n"

18: #while (i<Particles/3)

-- Inner Loop Happens Here --

31: #end

32: #local i=0;

33: #debug "second layer of snow\n"

34: #while (i<Particles*2/3) // second layer of smaller particles

-- Inner Loop Happens Here --

47: #end

15: threshold 0.6

16: #local i=0; // first layer

17: #debug "first layer of snow\n"

18: #while (i<Particles/3)

-- Inner Loop Happens Here --

31: #end

32: #local i=0;

33: #debug "second layer of snow\n"

34: #while (i<Particles*2/3) // second layer of smaller particles

-- Inner Loop Happens Here --

47: #end

This code declares the blob object and starts iterating on two inner loops. The first inner loop places 1/3 of the particles. The second inner loop places 2/3 of the particles, but these are 1/4 the size of the first loop. Debug messages are sent out to update how far things have progressed.

19:
#local
Start=<Min.x+rand(rd)*(Max.x-Min.x),(MinHeight+rand(rd)*(MaxHeight-MinHeight))*Max.y,Min.z+rand(rd)*(Max.z-Min.z)>;

20: #local Norm=<0,0,0>;

21: #local Inter= trace (Obj2, Start, -y, Norm );

22: #if (vlength(Norm)!=0)

23: #local Angle=pow((180-abs(degrees(acos(min(1,vdot(vnormalize(Norm),y))))))/180,2);

24: #local FlakeSize=Angle*<1+rand(rd)*1.3,(Angle*0.5+rand(rd))*Thickness,1+rand(rd)*1.3>;

25: sphere{0,Size*2,1 scale FlakeSize Reorient_Trans(y,Norm) translate Inter}

26: #if (mod(i,1000)=0)

27: #debug concat(str(i,0,0),"\n")

28: #end

29: #local i=i+1;

30: #end

20: #local Norm=<0,0,0>;

21: #local Inter= trace (Obj2, Start, -y, Norm );

22: #if (vlength(Norm)!=0)

23: #local Angle=pow((180-abs(degrees(acos(min(1,vdot(vnormalize(Norm),y))))))/180,2);

24: #local FlakeSize=Angle*<1+rand(rd)*1.3,(Angle*0.5+rand(rd))*Thickness,1+rand(rd)*1.3>;

25: sphere{0,Size*2,1 scale FlakeSize Reorient_Trans(y,Norm) translate Inter}

26: #if (mod(i,1000)=0)

27: #debug concat(str(i,0,0),"\n")

28: #end

29: #local i=i+1;

30: #end

The inner loop is where most of the hard work is done. To get a handle on this, we'll start with a high level walk-through:

- Find the starting place for the snowflake (Line 19)
- Drop the snowflake toward the object (Line 21)
- If the snowflake hits the
object, do the following (Line 22)

- Determine how big the snowflake should be (Line 23 - 24)
- Place a blob sphere at the intersection point (Line 25)
- Every 1000 snowflakes that hit, print a message (Line 26 - 28)
- Note that we hit (Line 29)

The first task is to pick the starting point for the snowflake. To simplify the discussion, lets unravel Line 19 some.

#local
startX = Min.x + rand(rd) * (Max.x - Min.x)

#local startZ = Min.z + rand(rd) * (Max.z - Min.z)

#local startY = (MinHeight + rand(rd) * (MaxHeight-MinHeight)) * Max.y

#local Start = <startX, startY, startZ>;

#local startZ = Min.z + rand(rd) * (Max.z - Min.z)

#local startY = (MinHeight + rand(rd) * (MaxHeight-MinHeight)) * Max.y

#local Start = <startX, startY, startZ>;

Because we're dropping the snowflake straight down (remember, we used Reorient_Trans to do this), our X & Z starting points are easy to find out. We start from the most negative point and add a random amount up to the width of the object [1]. The Y starting point is a little more difficult. To let snow appear on lower branches of a tree, for example, you can't always start above the object because the higher branches will prevent the snowflakes from getting that far down. To get around this, the MinHeight and MaxHeight values were added. If both MinHeight and MaxHeight are greater than 1, you'll always start above the highest point in the object. However, as MinHeight approaches 0, the starting height will range from MinHeight * Max.y to MaxHeight * Max.y. This gives flexibility to the artistic effect. It should be noted that this algorithm assumes that Min.y = 0 [2].

After all this, we assign our starting point, Start, and drop the snowflake at our object (lines 20 - 21). This is done with the trace() function. The trace function takes an object, a starting point, and a direction. It shoots a ray from that starting point in the specified direction at the object and returns the first place it would hit and the surface normal at the hit point. The point of impact is stored in Inter and the surface normal is stored in Norm in this case. It is good form to preinitialize normal storage with <0,0,0> before calling trace (line 20).

The trace() function may not hit anything. The only reliable way to verify a hit is to check whether the normal, Norm, is <0,0,0>. If it is, trace() missed the object. The vlength() function returns the length of a vector, and vlength(<0,0,0>) = 0, making it an easy test. Once we have a snowflake that has hit the object, we need to put a blob there. But how big a snowflake do we want?

Gilles Tran goes through some complicated mathematics to determine the snowflake size. Again, lets unravel all the math in lines 23 and 24:

#local
strikeDot = vdot(normalize(Norm), y);

#local strikeAngleRad = acos(min(1, strikeDot));

#local strikeAngleDeg = abs(degrees(strikeAngleRad));

#local strike = (180 - strikeAngleDeg)/180;

#local Angle = pow(strike, 2);

#local FlakeSizeX = 1 + rand(rd) * 1.3;

#local FlakeSizeY = (Angle * 0.5 + rand(rd)) * Thickness;

#local FlakeSizeZ = 1 + rand(rd) * 1.3

#local FlakeSize = Angle * <FlakeSizeX, FlakeSizeY, FlakeSizeZ>

#local strikeAngleRad = acos(min(1, strikeDot));

#local strikeAngleDeg = abs(degrees(strikeAngleRad));

#local strike = (180 - strikeAngleDeg)/180;

#local Angle = pow(strike, 2);

#local FlakeSizeX = 1 + rand(rd) * 1.3;

#local FlakeSizeY = (Angle * 0.5 + rand(rd)) * Thickness;

#local FlakeSizeZ = 1 + rand(rd) * 1.3

#local FlakeSize = Angle * <FlakeSizeX, FlakeSizeY, FlakeSizeZ>

The math to get the value Angle is determining whether it was a direct hit or a glancing blow [3]. If the snowflake hits the surface direct on, Angle will be 1. If the snowflake hits with more of a glancing blow, then Angle will be close to 0. From this, we alter the size of the snowflakes - bigger for direct hits, smaller for glancing blows.

Next, lets place the blob sphere. Line 25 shows this happening. The blob sphere is reoriented using Reorient_Trans() to point the Y axis along the surface normal. This results in the snowflake thickness looking like it's growing away from the object. Next, the translate command places the snowflake at the intersection point, Inter.

It should be noted that the first inner loop and the second inner loop differ in how the snowflakes scale. The second inner loop makes the snowflakes 1/4 the size of the first inner loop as shown below

25:
sphere{0,Size*2,1 scale FlakeSize Reorient_Trans(y,Norm) translate
Inter}

41: sphere{0,Size*2,1 scale FlakeSize*0.25 Reorient_Trans(y,Norm) translate Inter}

41: sphere{0,Size*2,1 scale FlakeSize*0.25 Reorient_Trans(y,Norm) translate Inter}

And finally, some house keeping. Line 26 - 28 output a debug message to keep tabs on the MakeSnow progress. Line 29 increments the variable i that's tracking how many snow flakes have hit the object.

48:
texture{T_Snow}

49: Reorient_Trans(<-Direction.x,Direction.y,-Direction.z>,-y)

50: }

51: #end

49: Reorient_Trans(<-Direction.x,Direction.y,-Direction.z>,-y)

50: }

51: #end

The texture (line 48) colors the blob that's just been created and the Reorient_Trans function (line 49) aligns the blob with the original object. Lines 50 and 51 close out the blob and the macro.

[1] | Lets take the startX
range for an example.
rand(rd)
is guaranteed to be 0 - 1. If rand() returns 0, startX = Min.x + 0 *
(Max.x - Min.x) = Min.x. If rand() returns 1, startX = Min.x + Max.x -
Min.x = Max.x. If 0 < rand() < 1, startX will end up
between Min.x and Max.x. Thus, the starting point is inside the
bounding box above the object. |

[2] |
Since MinHeight and
MaxHeight are supposed to be >= 0 (per the description inside
the MakeSnow.inc file), there could be problems if Obj2 extends below
y=0. This is because the startY point is not a general solution. The
general solution would be startY = Min.y + (MinHeight + rand(rd) *
(MaxHeight-MinHeight) ) * (Max.y - Min.y). If you take Min.y = 0, the
general solution reduces down to the startY derivation inside the
MakeSnow macro |

[3] |
The full derivation of
this glancing blow is a bit complicated. strikeDot is the dot
product between the surface's
normal and the incoming snowflake's direction. If they're
perpendicular, the dot product is zero. This is a glancing blow. If
they're parallel, the dot product is 1. This is a direct hit. It turns
out that the dot product is defined as vdot(vect1, vect2) =
vlength(vect1) * vlength(vect2) * cos(theta). Since Norm is normalized
and y has a vlength of 1, vdot(y, Norm) = cos(theta). The next
2 lines convert strikeDot into a usable angle (in degrees)
strikeAngleDeg by using the inverse cosine function. Because the acos
function is being used, a direct hit
returns 0 and a glancing blow returns 180. strike = (180 -
strikeAngleDeg) / 180 reverses the relationship and scales it from 180
- 0 to 1.0 - 0. |

Last edited: 2/5/2005

Copyright (C) 2005 Mike Kost