First a bit of theory (First I am going to bore you to death and then I am going to give you the code
)
You can generate any geometric primitive or surface that can be mathematically
described (cube, cone, pyramid, torus, cylinder, sphere ... ). If the shape you want
can not be parameterized (i.e. Doom 3 character) you’ll need something like 3DMax
or Milkshape (or even better, create your own 3D modeler
).
With TV3D / DirectX you can create several different primitive types
First two are obviously good for solid geometry creation.
TV_TRIANGLELIST is good for creating faceted surfaces
since each triangle has 3 unique vertices and 3 unique normal vectors.
TV_TRIANGLESTRIP is good for creating smooth surfaces since DirectX treats
triangle list as one large polygon with multiple triangles.
It also uses fewer vertices.
There are benefits to using one over another, but I am not going to get into that.
Look at the cylinder below. If you cut it along the red line and open it up,
you get a very simple rectangle. That rectangle/polygon is very similar to
above definition of triangle strip. A third picture below applies the triangle
strip to the rectangle.
Just make sure vertices are added in a clockwise order (i.e. V1 → V2 → V3).
Now, the tricky part is to do all this while going in a circle. This is where cylinder
parametric equations come in handy.
Cylinder Parametric equations:
X=R*COS(Theta)
Y=Y
Z=R*SIN(Theta)
Or in our case:
X=R*COS(Theta)
Y=H
Z=R*SIN(Theta)
H - Height
R - Radius
Theta - Angular increment (from 0 - 360 deg or 0 - 2PI rad)
Public Sub Cylinder(Radius As Single, Height As Single, Sides As Single) Dim Theta As Single 'Current Angle Dim Inc As Single 'Angular increment Dim x As Single 'x coord Dim y As Single 'y coord Dim z As Single 'z coord Dim i As Integer Mesh.SetPrimitiveType TV_TRIANGLESTRIP 'Cylinder Precision Inc = 2 * PI_ / Sides 'where each side has two triangles Theta = 0 For i = 0 To Sides 'Calculate Vertices x = Radius * Cos(Theta) y = Height z = Radius * Sin(Theta) 'Vertex at the top of the cylinder Mesh.AddVertex x, 0, z, 0, 0, 0 'Vertex at the bottom of the cylinder Mesh.AddVertex x, y, z, 0, 0, 0 Theta = Theta + Inc Next End Sub
Run the above code using the following parameters
Cylinder (40, 40, 30)
30 sides give us 30*2 triangles and 30*2 + 2 vertices
This is what you get:
You can not really see anything since we did not generate any normals.
Switch to wireframe to get a better view ( Scene.SetRenderMode TV_LINE )
Normals are unit vectors that define the angle between the surface and the light.
Look at the picture below for the cylinder normals.
First drawing shows the side view.
To get a nice light on your cylinders surface normals should be perpendicular
to that surface. Second drawing shows the top view.
If you want the edges to disappear you have to tell your light to bounce
directly off the edge(s).
Keep in mind one thing, vertex normals are not really on the vertex like the
image above shows.In the image below there are 3 identical vectors where
the blue one is the “real" normal vector.
Obviously, normal vectors (normals) for vertices on top of the cylinder
are identical to the ones at the bottom (you cut normals calculations by 50%).
The same deal for the top view. Red normal vectors are just representations
of real normal vectors (blue).
Back to coding.
As you can see in the above images normals have exactly the same
cords as the vertices (minus the y coord).
N.x = X = R*COS(Theta)
N.y = Y = 0
N.z = Z = R*SIN(Theta)
Public Sub Cylinder(Radius As Single, Height As Single, Sides As Single) Dim Theta As Single 'Current Angle Dim Inc As Single 'Angular increment Dim x As Single 'x coord Dim y As Single 'y coord Dim z As Single 'z coord Dim i As Integer Dim n As D3DVECTOR 'vertex normal Mesh.SetPrimitiveType TV_TRIANGLESTRIP 'Cylinder Precision Inc = 2 * PI_ / Sides 'where each side has two triangles Theta = 0 For i = 0 To Sides 'Calculate Vertices x = Radius * Cos(Theta) y = Height z = Radius * Sin(Theta) 'Make sure you normalize vertex cords to get proper normals TVVec3Normalize n, Vector(x, 0, z) 'Vertex at the top of the cylinder Mesh.AddVertex x, 0, z, 0, 0, 0, n.x, n.y, n.z 'Vertex at the bottom of the cylinder Mesh.AddVertex x, y, z, 0, 0, 0, n.x, n.y, n.z Theta = Theta + Inc Next End Sub
Run the above code using the same parameters again
Cylinder (40, 40, 30)
This is what you get:
There is lot of different ways to calculate vertex normals. This one
seems to be good for the cylinder.
My explanations here are not particularly good nor in depth. You have
2 links below, where people did a much better job.
Now that you have a “good looking” cylinder it is time to apply
a texture to it (to make it even better looking
).
To apply texture you’ll have to figure out the UV coordinates.
U and V coordinates are just a way to talk about which pixel should be used in a texture map, independent of how big the texture map finally turns out to be. If U and V are 0.25 and 0.5 for a given vertex in your model, and the texture map is 512 by 512, the pixel at 128,256 will be applied to the vertex
In general, UV coordinates range between 0 and 1.
See the sample image below.
What UV (1, 1) really means is UV (100% of texture width, 100% of texture height)
or UV (0.5, 1) really means is UV (50% of texture width, 100% of texture height).
So, in the case of cylinder we open it up again to get a rectangle.
UV1=UV(0,0)
UV2=UV(0,1)
UV3=UV(1,1)
UV4=UV(1,0)
This is fine except we have applied UV cords to corner vertices only.
Next image shows triangle strip applied to textured rectangle.
There are 12 vertices (6 on top and 6 on bottom),
10 triangles and 5 sides (2 triangles per side).
6 vertices (top and bottom) are evenly spaced,
so the step between U cords is 1/Sides = 1/5.
...and once again the code:
Public Sub Cylinder(Radius As Single, Height As Single, Sides As Single) Dim Theta As Single 'Current Angle Dim Inc As Single 'Angular increment Dim x As Single 'x coord Dim y As Single 'y coord Dim z As Single 'z coord Dim i As Integer Dim n As D3DVECTOR 'vertex normal Dim tu As Single 'U coord Dim tv As Single 'V coord Dim UStep As Single 'step between U coords Mesh.SetPrimitiveType TV_TRIANGLESTRIP 'Cylinder Precision Inc = 2 * PI_ / Sides 'where each side has two triangles Theta = 0 'Initial value UStep = 1 / Sides tu = 0 'Initial value For i = 0 To Sides 'Calculate Vertices x = Radius * Cos(Theta) y = Height z = Radius * Sin(Theta) 'Make sure you normalize vertex cords to get proper normals TVVec3Normalize n, Vector(x, 0, z) 'Vertex at the top of the cylinder 'tv value is always 1for top vertices Mesh.AddVertex x, 0, z, 0, tu, 1, n.x, n.y, n.z 'Vertex at the bottom of the cylinder 'tv value is always 0 for bottom vertices Mesh.AddVertex x, y, z, 0, tu, 0, n.x, n.y, n.z Theta = Theta + Inc tu = tu + UStep Next End Sub
Run the above code (once again) using the same parameters Cylinder (40, 40, 30)
This is what you get:
It is good idea to use checkered texture to verify texture coordinates, since it would be very easy to spot deformed squares.
There is one problem with texture we applied in previous chapter.
Texture looks blurry and stretched.
The reason for is that we applied perfectly square texture to a rectangle.
Modify the previous code by replacing
tu = tu + UStep
with
tu = tu + UStep*2
and run it again.
The end result:
It looks a lot better then before.
By multiplying UStep by two you have doubled UV coordinates of
the rectangle (cylinder) and effectively forced tiling of your texture.
The same way, if you use
tu = tu + UStep*4
you will force 4 texture tiling in U direction.
New result:
Checkered texture is nice to verify texture coordinates,
but it can not show effect of tiling.
Use this texture to see UV manipulation effects.
With Original code:
One texture wrapped around
With
tu = tu + UStep*4
Four textures wrapped around
You are not limited to just one dimensional tiling
Do these modifications
tu = tu + UStep*10
and (replace tv value of 1 with 2)
Mesh.AddVertex x, 0, z, 0, tu, 2, n.x, n.y, n.z
This results in 2×10 texture tiling:
To make this cylinder complete it would be good idea to close it up or add
caps to top and bottom.
Ok, it is time to cut the cylinder along the red lines and open it up.
This time cut results in two circles and the rectangle.
Last picture below applies the triangle strip to the rectangle and circles.
Vertex numbers are added to the triangle strip just to show how vertices are
shared between two circles and the rectangle.
The mesh is divided into three regions Bottom Cap, Cylinder and Top Cap,
and these regions are going to be rendered in that order.
As usual we have to make sure vertices are added in clockwise order.
Parametric equations stay unchanged as well, since both, top and bottom
caps share cylindrical mesh vertices.
X=R*COS(Theta)
Y=H
Z=R*SIN(Theta)
At this point its a good idea to look at the Bottom Cap triangle strip
and figure out the algorithm.
The image shows Bottom cap in terms of angular increment theta.
There are two approaches
First approach algorithm would look something like this:
V1 @ 0 deg V2 @ 45 deg V8 @ 315 deg V3 @ 90 deg V7 @ 270 deg ...
Second approach algorithm:
V1 @ 0 deg V2 @ 45 deg V8 @ -45 deg V3 @ 90 deg V7 @ -90 deg ...
Both approaches are mathematically identical
i.e.
For V8 @ 315 deg COS(315) = 0.707 For V8 @ -45 deg COS(-45) = 0.707
Second one looks more manageable (just a personal preference), feel free to play with different approaches.
Time to code
Public Sub Cylinder(Radius As Single, Height As Single, Sides As Single) Dim Theta As Single 'Current Angle Dim Inc As Single 'Angular increment Dim x As Single 'x coord Dim y As Single 'y coord Dim z As Single 'z coord Dim i As Integer Dim n As D3DVECTOR 'vertex normal Dim tu As Single 'U coord Dim tv As Single 'V coord Dim UStep As Single 'step between U coords Dim Sign As Integer 'a flag to indicate theta sign change Mesh.SetPrimitiveType TV_TRIANGLESTRIP 'Cylinder Precision Inc = 2 * PI_ / Sides 'where each side has two triangles Theta = 0 'Initial value UStep = 1 / Sides tu = 0 'Initial value 'Bottom Cap Sign = -1 For i = 1 To Sides 'Draw The Bottom Cap Loop 'Calculate vertices x = Radius * Cos(Sign * Theta) y = 0 z = Radius * Sin(Sign * Theta) Mesh.AddVertex x, 0, z, 0, 0, 0, 0, 0, 0 If Sign = 1 Then 'Change the sign Sign = -1 ElseIf Sign = -1 Then 'Change the sign Sign = 1 'Increment theta for the next loop Theta = Theta + Inc End If Next 'The Cylinder 'At this point it is important not to reset theta to zero 'even though at the end of the following loop accumulated theta 'will equal 3PI 'See the "NOTE 1" below 'Original code from "Building A Cylinder" 'Including vertex normals and texture coordinates tu = 0 'Initial value For i = 0 To Sides 'Draw The Cylinder Loop 'Calculate Vertices x = Radius * Cos(Theta) y = Height z = Radius * Sin(Theta) 'Make sure you normalize vertex cords to get proper normals TVVec3Normalize n, Vector(x, 0, z) 'Vertex at the top of the cylinder Mesh.AddVertex x, 0, z, 0, tu, 2, n.x, n.y, n.z 'Vertex at the bottom of the cylinder Mesh.AddVertex x, y, z, 0, tu, 0, n.x, n.y, n.z Theta = Theta + Inc tu = tu + UStep * 10 Next End Sub
NOTE 1: Look at FIGURE No.3. When drawing the Bottom Cap triangle strip, last vertex drawn is vertex V5 (V1->V2->V8->V3->V7->V4->V6->V5). This last vertex V5 is drawn at the angle Theta = 180 deg. To keep the triangle strip flow continuous it is necessary to start the "Cylindrical" triangle strip where the "Bottom Cap" triangle strip left off, at Theta = 180 deg. Nothing changes mathematically. It is absolutely the same to go from 0 to 2PI (as in previous tutorial) or from PI to 3PI (as long as you complete the full circle of 360 deg or 2PI rad). The same way, we will have to start drawing the "Top Cap" triangle strip where "Cylindrical" triangle strip stopped ... but more on that later.
Run the above code as Cylinder (40, 40, 30)
This is what we get:
Of course, bottom cap is black because we have no vertex normals defined.
Look at the cylinder side view image below. To get proper light for
Top and Bottom caps it is important that normals are perpendicular
to their surface.
It is obvious from the image below that all normals for the Top cap
are identical. The same goes for the Bottom cap.
Top Cap normals point in positive Y Axis direction and Bottom Cap
normals point in negative Y Axis direction.
FIGURE No. 5
There is no need for normal calculations and normalization here.
Top cap normals are all unit vectors:
Vector(0,1,0)
and Bottom cap normals are all unit vectors:
Vector(0,-1,0)
By replacing (in Draw The Bottom Cap Loop )
Mesh.AddVertex x, 0, z, 0, 0, 0, 0, 0, 0
with
Mesh.AddVertex x, 0, z, 0, 0, 0, 0, -1, 0
we get:
Ok, at this point we have Bottom Cap and the Cylindrical body.
Its time for the Top Cap
So far the code did something like this (in simplified pseudocode):
Program Start
Theta = 0
Calculate Bottom Cap Vertices (Start at Theta=0)
Theta = PI (180 deg)
Calculate Cylindrical Body Vertices (Start at Theta=PI)
Theta = 3*PI (540 deg)
Based on previous logic (Note 1) we would expect to continue like this
Calculate Top Cap Vertices (Start at Theta=3*PI)
Program End
Well ... starting Top Cap from 540 deg (3PI) just does not sound
very nice or clean (see FIGURE No. 7).
This looks quite ugly
Since we are dealing with circles and angles here its time to use
trigonometry to our advantage
As far as a full circle is concerned trigonometry does not care for
how many times we went around that circle.
Simply put, we start removing full circles (360 deg) from our starting 540 deg
we ended up with:
540-360=180
this is where we stop since 180 deg is half a circle
(no more full circles to remove)
Same calculation in terms of radians where 2PI is a full circle:
3PI-2PI=PI
we stop again since PI is half circle.
Now, to see that in world of Trigonometry 540⇔180 are “equivalent”
we can do this:
COS(540)=-1
COS(180)=-1
Well, looks the same to me
So, we have removed one full circle from our starting 540 deg Theta
and ended up with a new staring angle Theta=180 deg.
We do the same thing for all incremental Thetas in FIGURE No. 7
This is what we end up with:
I am going to make a leap of faith here and replace V8 theta of
495 with -225. Quick check:
COS(495)=-0.707
COS(-255)=0.707
Looks fine
Following the same logic we get this:
Another way to look at it:
FIGURE No. 9 is the one I like (just a matter of personal preference).
FIgures 7, 8, 10 are also valid, as long as you figure out the proper algorithm.
There is really only one important thing here:
The fact that we need to start with Theta=180 (PI).
How we go around the circle from that point is up to us.
Enough BS, its time to code again
Public Sub Cylinder(Radius As Single, Height As Single, Sides As Single) Dim Theta As Single 'Current Angle Dim Inc As Single 'Angular increment Dim x As Single 'x coord Dim y As Single 'y coord Dim z As Single 'z coord Dim i As Integer Dim n As D3DVECTOR 'vertex normal Dim tu As Single 'U coord Dim tv As Single 'V coord Dim UStep As Single 'step between U coords Dim Sign As Integer 'a flag to indicate theta sign change Mesh.SetPrimitiveType TV_TRIANGLESTRIP 'Cylinder Precision Inc = 2 * PI_ / Sides 'where each side has two triangles Theta = 0 'Initial value UStep = 1 / Sides tu = 0 'Initial value 'Bottom Cap Sign = -1 For i = 1 To Sides 'Draw The Bottom Cap Loop 'Calculate vertices x = Radius * Cos(Sign * Theta) y = 0 z = Radius * Sin(Sign * Theta) Mesh.AddVertex x, 0, z, 0, 0, 0, 0, -1, 0 If Sign = 1 Then 'Change the sign Sign = -1 ElseIf Sign = -1 Then 'Change the sign Sign = 1 'Increment theta for the next loop Theta = Theta + Inc End If Next 'The Cylinder 'At this point it is important not to reset theta to zero 'even though at the end of the following loop accumulated theta 'will equal 3PI 'See the "NOTE 1" below 'Original code from "Building A Cylinder" 'Including vertex normals and texture coordinates tu = 0 'Initial value For i = 0 To Sides 'Draw The Cylinder Loop 'Calculate Vertices x = Radius * Cos(Theta) y = Height z = Radius * Sin(Theta) 'Make sure you normalize vertex cords to get proper normals TVVec3Normalize n, Vector(x, 0, z) 'Vertex at the top of the cylinder Mesh.AddVertex x, 0, z, 0, tu, 2, n.x, n.y, n.z 'Vertex at the bottom of the cylinder Mesh.AddVertex x, y, z, 0, tu, 0, n.x, n.y, n.z Theta = Theta + Inc tu = tu + UStep * 10 Next 'The Top Cap Theta = PI_ Sign = 1 For i = 1 To Sides 'Draw The Top Cap Loop 'Calculate vertices x = Radius * Cos(Sign * Theta) y = Height z = Radius * Sin(Sign * Theta) 'All Vertex Normals equal to +1 in Y axis direction Mesh.AddVertex x, y, z, 0, 0, 0, 0, 1, 0 If Sign = 1 Then 'Change the sign Sign = -1 'Increment theta for the next loop Theta = Theta + Inc ElseIf Sign = -1 Then 'Change the sign Sign = 1 End If Next End Sub
Run the above code as Cylinder (40, 40, 30)
This is what we get:
With checkered Texture
FIGURE No. 11
With TV3D Texture
FIGURE No. 12
One thing we are missing here are cap textures.
Bottom and Top Caps are basically a 2D circle.
Parametric equation of circle in a Cartesian coordinate system is (Circle):
X=R*COS (Theta)
Y=R*SIN (Theta)
R - Circle radius
for a circle with center in origin x=0 and y=0.
Similarly, parametric equation of circle in a “texture world”
or UV coordinate system is :
U=R*COS (Theta)
V=R*SIN (Theta)
R - Circle radius
for a circle with center in origin U=0 and V=0.
Knowing that UV coordinates range between 0 and 1
we need a “unit circle” (Circle with diameter D=1).
Next image shows the inscribed Unit Circle (D=1 and R=D/2=0.5).
Now, if we want our caps to show the texture exactly as it appears
in FIGURE No. 14 we need to adjust the above equations.
The problem is that our circle center is not in the origin (0,0).
Circle center is offset by R in U and V direction (see NOTE 2).
The problem is solved by rewriting the equations as:
U=R*COS(Theta)+R
V=R*SIN(Theta)+R
or
U=0.5*COS(Theta)+0.5
V=0.5*SIN(Theta)+0.5
or in code
tu = 0.5 * Cos(Sign * Theta) + 0.5 tv = 0.5 * Sin(Sign * Theta) + 0.5
NOTE 2: Even without adding these offsets, the texture would be applied nicely. Its just that we would not get the desired effect (FIGURE No.14).
Public Sub Cylinder(Radius As Single, Height As Single, Sides As Single) Dim Theta As Single 'Current Angle Dim Inc As Single 'Angular increment Dim x As Single 'x coord Dim y As Single 'y coord Dim z As Single 'z coord Dim i As Integer Dim n As D3DVECTOR 'vertex normal Dim tu As Single 'U coord Dim tv As Single 'V coord Dim UStep As Single 'step between U coords Dim Sign As Integer 'a flag to indicate theta sign change Mesh.SetPrimitiveType TV_TRIANGLESTRIP 'Cylinder Precision Inc = 2 * PI_ / Sides 'where each side has two triangles Theta = 0 'Initial value UStep = 1 / Sides tu = 0 'Initial value 'Bottom Cap Sign = -1 For i = 1 To Sides 'Draw The Bottom Cap Loop 'Calculate vertices x = Radius * Cos(Sign * Theta) y = 0 z = Radius * Sin(Sign * Theta) tu = 0.5 * Cos(Sign * Theta) + 0.5 tv = 0.5 * Sin(Sign * Theta) + 0.5 Mesh.AddVertex x, 0, z, 0, tu, tv, 0, -1, 0 If Sign = 1 Then 'Change the sign Sign = -1 ElseIf Sign = -1 Then 'Change the sign Sign = 1 'Increment theta for the next loop Theta = Theta + Inc End If Next 'The Cylinder 'At this point it is important not to reset theta to zero 'even though at the end of the following loop accumulated theta 'will equal 3PI 'See the "NOTE 1" bellow 'Original code from "Building A Cylinder" 'Including vertex normals and texture coordinates tu = 0 'Initial value For i = 0 To Sides 'Draw The Cylinder Loop 'Calculate Vertices x = Radius * Cos(Theta) y = Height z = Radius * Sin(Theta) 'Make sure you normalize vertex cords to get proper normals TVVec3Normalize n, Vector(x, 0, z) 'Vertex at the top of the cylinder Mesh.AddVertex x, 0, z, 0, tu, 2, n.x, n.y, n.z 'Vertex at the bottom of the cylinder Mesh.AddVertex x, y, z, 0, tu, 0, n.x, n.y, n.z Theta = Theta + Inc tu = tu + UStep * 10 Next 'The Top Cap Theta = PI_ Sign = 1 For i = 1 To Sides 'Draw The Top Cap Loop 'Calculate vertices x = Radius * Cos(Sign * Theta) y = Height z = Radius * Sin(Sign * Theta) tu = 0.5 * Cos(Sign * Theta) + 0.5 tv = 0.5 * Sin(Sign * Theta) + 0.5 'All Vertex Normals equal to +1 in Y axis direction Mesh.AddVertex x, y, z, 0, tu, tv, 0, 1, 0 If Sign = 1 Then 'Change the sign Sign = -1 'Increment theta for the next loop Theta = Theta + Inc ElseIf Sign = -1 Then 'Change the sign Sign = 1 End If Next End Sub
As usual, run the code as Cylinder(40,40,30).
Bottom Cap result:
Bottom cap looks fine.
Top Cap result:
Well, Top cap is “fine” but mirrored.
There is a reason for a mirrored texture (believe it or not
)
The main reason is the identical code we use for both, the bottom cap and the top cap.
tu = 0.5 * Cos(Sign * Theta) + 0.5 tv = 0.5 * Sin(Sign * Theta) + 0.5
Look at the three steps in FIGURE No. 17 (Bottom Cap View) .
Second step shows texture applied to the bottom cap, and that looks fine.
Now, if you use the same code for the top cap, you basically take the bottom
cap texture and “translate” it to the top cap along the cylinder height(step 3).
To make this even more clear, look at the same three steps from a different
perspective (Top Cap View FIGURE No. 18).
There is your mirrored texture view, right there at step 3.
Ok, hopefully this last explanation made sense
.
Fixing this mirrored/flipped texture is quite easy.
All you have to do is make your U OR V coordinate negative.
Make sure you do not change both coordinates to negative values.
Changing U=-U and V=-V will still give you mirrored texture.
New code for Top Cap:
tu = -(0.5 * Cos(Sign * Theta) + 0.5) tv = 0.5 * Sin(Sign * Theta) + 0.5
Make this change and run the code.
Everything should work fine now, except I do not like that
single large texture applied to top and bottom cap
.
Its time to tile the texture
.
Tiling is very easy:
All you have to do is increase the size of the Unit Circle.
You increase the circle size by increasing its radius.
Lets say we want 3×3 tiling:
For Bottom Cap:
tu = 0.5 * 3 * Cos(Sign * Theta) + 0.5 tv = 0.5 * 3 * Sin(Sign * Theta) + 0.5
For Top Cap:
tu = -(0.5 * 3 * Cos(Sign * Theta) + 0.5) tv = 0.5 * 3 * Sin(Sign * Theta) + 0.5
Insert this code to get:
Or for better view:
Keep in mind that this is far from “good” and “optimized” code.
This code will work if you have your application setup correctly
Textures, materials, lights ... (see Getting Started Tutorials)
If you have questions, suggestions, comments ... etc, you can PM me (Vuli)
on TrueVision3D forum TrueVision3D Forum
Have fun