---- DATABASE ERROR ----
Digging Deep: Understanding
Gilles Tran's MakeSnow Macro
by Mike
Kost
Introduction
Povray features a powerful scripting
language that can be used to automate complex tasks. One of the best
ways to understand a scripting language is to disect a script someone
else has written. Due to the time of year and all the snow that's
falling this winter,
this Digging Deep will focus on understanding Gilles Tran's MakeSnow
macro.
Quick Reading
Gilles
Tran's MakeSnow
webpage has a description, examples,
and the MakeSnow.inc
macro itself.
Also, several Povray functions (both built in and include file based)
are going to be used and discussed
If you're not up on vector math, it'll be worth while brushing up with
a few topics from the Wikipedia
The MakeSnow Macro
The MakeSnow macro creates a snow blob that overlays on an object and
look like a layer of snow. The MakeSnow macro takes 6 parameters:
- 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.
Now, lets get into the good stuff.
The Setup
If you havn't already, read through makesnow.inc
to get a feel for what's coming.
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);
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
The
Blob
This is where all the snow particles are blobbed together.
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
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.
The Inner Loop
Below is the inner loop. This is a good hunk of script that places
individual snow globs onto the object. All the interesting stuff inside
the setup section will get used here.
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
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)
Now lets get into some detail.
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>;
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>
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}
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.
Closing Up
This final bit of code finishes off the macro
48:
texture{T_Snow}
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.
The Result
All of this combines to produced a script that's able to
snow cover anything with a layer of snow
Notes And Disclaimers
[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