Pipe Tutorial
From GMLwiki
This is a step-by-step description of code and shape development. You can compare your results with Models/ MyTrial.genmod
Start the program. Userlib is selected in the library browser.
When starting from scratch, you typically first create a new dictionary via Edit.New Dictionary, for instance Userlib.MyTrial
Create a new function in this dictionary using Edit.New_Item, for instance Userlib.MyTrial.test1.
Make this item visible in the library browser by double-clicking on Userlib.MyTrial, and select Userlib.MyTrials.test1. The Code window now shows the contents of this function: It is empty.
Type in the following code in the code window: deleteallmacros newmacro clear
(0,0,0) (0,-1,0) 1.0 4 circle
When you run it (Alt-X or Edit.Run or double-click on Userlib.MyTrials.test1), you see a quadrangle in the 3D window!
The circle operator expects midpoint, plane normal, radius or start vector, and number of segments on the stack. Such brief, but hopefully sufficient, information can be found in the appendix for all operators. So for the rest of this tutorial, only operator essentials will be discussed.
Take the deleteallmacros newmacro clear for now as just something every function needs to start with. The macro issue will be explained in a different tutorial.
Now copy the code using Ctrl-Pos1, Shift-Ctrl-End, Ctrl-C, and create a new item called test2. Paste the code using Ctrl-V and try Alt-X. It works. – From now on we will create a new item for each step. This helps to keep track of the changes. 10. Add the following lines to test2 to create your first mesh:
/brzskin setcurrentmaterial 1 poly2doubleface
The current material belongs to the state of the interpreter. The operator pops the name and changes the state, but it doesn’t push anything, so the stack top is again the polygon.
At program startup a number of default materials are loaded (from file Models/tubs.mtl). When you have a look at the material lib, you see that you can find out about available materials using getmaterialnames.
The stack now contains just one halfedge. Try out some double-clicks on Systemlib.BRep .faceCCW or .faceCW, or enter navigation commands in the prompt window, for instance dup vertexCW dup faceCCW edgemate
The cool thing now is that you can play around with parameters! Have a look at the documentation of poly2doubleface to see what the 1 means and try for instance 0.
Now copy test2 to a new test3, add a code line (0,1,3) extrude at the end, and run it. Now you’ve created a box! For extrude, an argument (x,y,m) means a displacement of the border of x units to the left, within the face plane, and y units in normal direction. The mode m determines the distribution of smooth and sharp edges. Now have a look in the documentation to see what m means!
Play around and change some parameters. For the argument of the extrude, instead of (0,1,3) try for instance (0.5,0.5,3), (0,1,2), or (0,1,4). Try more circle segments: Change the 4 in the third code line to 6,8, or 20.
Note that extrude pops and also pushes one halfedge! – This makes it possible to create a sequence of extrudes. Your test7 might look like this:
deleteallmacros newmacro clear (0,0,0) (0,-1,0) 1 8 circle /brzskin setcurrentmaterial 1 poly2doubleface (0.2,0.5,0) extrude (0.2,0.5,0) extrude
The extrude operator is polymorphic and understands also an array of vectors. But in this case, the (x,y,m) displacementsc are absolute: You can obtain the same result as with the two extrudes using only (test8):
[ (0.2,0.5,0) (0.4,1,0) ] extrude
But what you cannot do within a single extrude is change the face normal direction! This is what we try next. Add the following code below a copy of test8:
usereg !e :e facenormal (1,0,0) 10.0 rot_vec !n :n 0.5 mul :e facemidpoint add !p :p :n mul !d :p [ :p :p :n add ]
This uses a named register to store (!e) and retrieve (:e) the halfedge pushed by extrude.
The :e face’s normal vector is then rotated by 10 degrees around the positive x-axis, so it stays normalized. Next we add half of this normal to the face midpoint to retrieve a displaced midpoint :p.
The GML format for a plane in 3-space isn d, where n nx ny nz is the normalized face normal vector, and the scalar d is the signed distance of the plane from the origin. Given a point p on the plane, d can be computed as the dot product d pd. In the GML, mul: P3 P3 gives the dot product: :p :n mul !d
The last line is just for visualization: An array (aka polygon) of two points (:p and :p :n add) is a line segment, and :p just places a dot in 3-space to show the location of the point.
Now we want to project a copy of the :e face polygon to the plane :n :d. The face polygon is obtained by ring2poly which pushes a polygon, a point array, on the stack. Now insert the following mysterious lines before endreg to obtain test10 from test9:
:e ring2poly
{ dup (0,1,0) add :n :d
intersect_lineplane pop }
map
Now we introduce the powerful map operator. It expects an array and a function on the stack. What it does is to loop through the array; it pushes each element on the stack, executes the function, and expects the function to leave something on the stack. So each time after the function, it pops the stack, and creates a new array from the elements it found. This array is what it leaves on the stack when it’s done. This way, the map operator transforms one array into another array by mapping a given function to each element.
GML functions are just arrays! To see that, delete the map statement for a moment, run the code, and have a look at the stack. The top elements are a function and an array, and this is what map will get. Now type aload at the prompt and press Enter. This pushes all array elements individually on the stack, which works also for executable arrays. So what you see on the stack are the statements of your function!
To do the reverse, enter 7 array at the prompt, and your operators are put together again to form an array; but it’s not executable. So do a cvx (“convert to executable”), and you’ve got your function back. To try it out, just enter map at the prompt.
This demonstrates a remarkable advantage of the PostScript approach: It is extremely easy to generate program code. Just imagine you had to write a computer program that generates source code for C++ or Java!
But what does map do here? It calls an operator that intersects a line with a plane (our :n :d). The line is given by two points. We find one of them on the stack. We compute the second point from the first by adding (0,1,0), which is :e’s face normal. (We could have used another local variable for it). The intersection operator returns also the t value of the intersection point along the line p1 t p2 p1. But we don’t need it, so it gets popped.
To see that this is indeed the projected face polygon, insert the following line at the end:
- e (0,1,1) extrude.
Switch Render.Control mesh on and Render.Solid faces off to see that it touches the mesh.
Now let’s clean up and re-order the code, so that we obtain test11 as:
deleteallmacros newmacro clear (0,0,0) (0,-1,0) 1.0 8 circle /brzskin setcurrentmaterial 1 poly2doubleface [ (0.2,0.5,0) (0.4,1,0) ] extrude usereg !e :e facenormal (1,0,0) 10.0 rot_vec !n :e (0,1,0) :n :e facemidpoint :n 0.5 mul add :n mul project_ringplane
We actually want to project the face to the plane, not just the face polygon. So in principle we could use moveV, the “move vertex” operator, with map. But for faces, there’s a builtin operator to do that. It expects an edge, a projection direction (in our example (0,1,0) as :e’s face normal), and a plane :n :d on the stack.
As :d is used only once, we can compute it directly on the stack with :e facemidpoint :n 0.5 mul add :n mul. This is another PostScript technique! If you compute e from a b c op1 and e is then used ind e f op2, you can “inline” e: d a b c op1 f op2
Re-use of modeling operations. The sequence of operations after userreg is a compact little modeling tool! Now create a new dictionary Userlib.MyTrial.Tools with a new item turnface, and copy and paste the function code to it.
The new version of our program then behaves identically but now simply looks like this (test12):
deleteallmacros newmacro clear (0,0,0) (0,-1,0) 1.0 8 circle /brzskin setcurrentmaterial 1 poly2doubleface [ (0.2,0.5,0) (0.4,1,0) ] extrude MyTrial.Tools.turnface
But now that we have a single function, we can easily use it not only once but many times! The following is identical to writing down extrude ... turnface nine times:
9 { (0,0.1,0) extrude
MyTrial.Tools.turnface
} repeat
If we have more than one tool, it is both inconvenient and inefficient to always write down the whole pathname. In this caseit is more convenient to change the scope by pushing the Tools dictionary on top of the dictionary stack. So the same effect as above can be achieved by
MyTrial.Tools begin
9 { (0,0.1,0) extrude turnface }
repeat
end
Another advantage of this variant is that it is more easy to switch between different versions of turnface. By choosing the appropriate dictionary, the model “vocabulary” can be easily changed. This reflects the idea of style libraries.
Now we have constructed a pipe bent by 90 degrees. But because we used projections, the profile is no longer a circle but merely ellipsoidal. We can see this if we draw a circle (an 8-gon) around the face midpoint. Note that an edge of the last face remains on the stack, so we use it to make the circle:
dup facemidpoint exch facenormal 0.6 8 circle 4 THE PIPE TUTORIAL 8
So let’s start with a new idea to create a bent pipe: We rotate the polygon and create double-sided faces from each copy. Then we connect the faces along the pipe. We can do this so that the last copy of the polygon is always on the stack as input for the next step. So let test14 look like this:
deleteallmacros newmacro clear
(0,0,0) (0,-1,0) 0.6 8 circle
dup 5 poly2doubleface pop
{ (0,0,-2) (1,0,0) 18.0 rot_pt } map
dup 5 poly2doubleface pop
{ (0,0,-2) (1,0,0) 18.0 rot_pt } map
dup 5 poly2doubleface pop
You can see immediately that the last two lines could be used in a loop, for example with 5 ... repeat. But when a loop operates on the result from the previous loop pass, it is easier to design the loop body in an “unrolled” fashion like this.
The bridgerings operator can connect two different faces if they have the same number of vertices. It simply makes edges between corresponding vertices, traversing one face clockwise and the other counterclockwise. It starts by connecting the vertices of the two halfedges it pops from the stack. Finally it leaves one such bridge edge as result on the stack.
The idea is to remember the last face built as !e, rotate the polygon and make a doubleface, connect the backside to the previously built face, and store the frontside face as new !e. This results in the following code (test15):
deleteallmacros newmacro clear
(0,0,0) (0,-1,0) 0.6 8 circle
usereg
dup 5 poly2doubleface !e
{ (0,0,-2) (1,0,0) 18.0 rot_pt } map
dup 5 poly2doubleface
dup edgemate faceCCW :e
0 bridgerings pop !e
{ (0,0,-2) (1,0,0) 18.0 rot_pt } map
dup 5 poly2doubleface
dup edgemate faceCCW :e
0 bridgerings pop !e
Question: Why are the ’horizontal’ edges sharp and ’vertical’ edges smooth in this example? Answer: Background info. The poly2doubleface mode parameter has the following meaning: With even modes 0,2,4,6 , the face has smooth edges, and with odd modes 1,3,5,7 it has smooth faces. But the four diffent numbers determine the vertex types. This is important for the type of the the ’vertical’ edges created later when such a face is extruded. Modes 0/1 make all vertices smooth, 2/3 makes all vertices corners. But Modes 4/5 are interesting because they make vertices smooth by default, and they make a corner only if a point occurs twice in the polygon. And when you use bridgerings with mode 2, it will create smooth or sharp bridge edges according to the vertex types from poly2doubleface.
Now these four lines will basically make up the body of a loop (test16). Making it more general by introducing a few parameters, we can add MyTrial.Tools.polycirclepipe as another tool in our toolset. Note that it returns the first and last faces of the pipe because these will most likely be further processed.
usereg
!angle !k !axis !center !poly
:poly 4 poly2doubleface !e
:e edgemate faceCCW !eStart
:poly
:k {
{ :center :axis :angle rot_pt } map
dup 4 poly2doubleface
dup edgemate faceCCW :e
2 bridgerings pop !e
} repeat pop
:eStart :e
We can use this tool now to make a true pipe with a thin wall. Topologically this is a torus, and it can in principle be made by subtracting a smaller pipe from a thicker pipe. But we can also construct the interior walls directly. Note what happens when we use our 4 THE PIPE TUTORIAL 9 new tool but simply reverse the orientation of the circle. This can be accomplished by using (0,1,0) instead of 0 1 0 as plane normal for the circle:
deleteallmacros newmacro clear /brzskin setcurrentmaterial MyTrial.Tools begin (0,0,-2) (1,0,0) 3 18.0 usereg !angle !k !axis !point (0,0,0) (0,1,0) 0.6 6 circle :point :axis :k :angle polycirclepipe end
The resulting object is perfectly allright but it looks strange, because it is inverted: The faces are clockwise oriented. So faces closer to the viewer are actually backfaces, and faces on the backside of the object actually face the viewer. Now this is exactly what we want for the pipe interior.
But now we create two tubes, one for the outside and one, smaller and reversed, for the inside. The begin and end faces are returned by polycirclepipe! So all we need to do is to make the face of the smaller tube a ring of the larger tube, and that creates a hole! – Thanks to Euler operators.
deleteallmacros newmacro clear /brzskin setcurrentmaterial MyTrial.Tools begin (0,0,-2) (1,0,0) 3 18.0 usereg !angle !k !axis !point (0,0,0) (0,-1,0) 0.6 6 circle :point :axis :k :angle polycirclepipe !e0 !e1 (0,0,0) (0,1,0) 0.55 6 circle :point :axis :k :angle polycirclepipe !f0 !f1 :f0 :e0 killFmakeRH :f1 :e1 killFmakeRH end
Again following our tools creation philosophy the logical step is to consider the two circles as input parameter for a new tool. We call it MyTrial.Tools.emptycirclepipe. It returns all four end faces:
usereg !angle !k !axis !point !poly2 !poly1 :poly1 :point :axis :k :angle polycirclepipe !e0 !e1 :poly2 :point :axis :k :angle polycirclepipe !f0 !f1 :f0 :e0 killFmakeRH :f1 :e1 killFmakeRH :e0 :e1 :f0 :f1
This reduces again our test program, to basically four lines (test19):
deleteallmacros newmacro clear /brzskin setcurrentmaterial (0,0,0) (0,-1,0) 0.6 6 circle (0,0,0) (0,1,0) 0.55 6 circle (0,0,-1) (1,0,0) 5 12.0 MyTrial.Tools.emptycirclepipe
Now it’s time for some variation of the input polygons. The following code creates a heart-shaped polygon:
(0,0,-1) !c (1,0,0.3) !pr (0,0,1) !m (-1,0,0.3) !pl :pr :m :pr :c sub neg circle_dir !ml :pl :m :pl :c sub neg circle_dir !mr [ :c ] [ :pr :midl :m ] (0,-1,0) 0.1 2 circleseg arrayappend [ :m :midr :pl ] (0,-1,0) 0.1 2 circleseg arrayappend [ :c ] arrayappend
The circle_dir operator finds the center of a circle, given two points on the circle and a direction vector. A circle segment is basically specified as [a m b], where a and b are points on the circle (start and 5 THE LEGO TUTORIAL 10 end of circle segment), and m is the center of the circle. The circleseg operator turns this into an array, similar to the circle operator. The center :c is appended again in the end to create a sharp corner (see background info on sharpness modes).
The inner polygon is created from another circle segment, and a line segment with double end points to make these sharp corners.
[ (0.9,0,0.4) (0,0,0) (-0.9,0,0.4) ] (0,-1,0) 0.05 2 circleseg [ (-0.4,0,-0.3) dup (0.4,0,-0.3) dup ] arrayappend reverse
With those two polygons on the stack two familiar lines suffice to make a pipe with a more interesting profile (test22):
(0,0,-2) (1,0,0) 5 10.0 emptycirclepipe
