Triangles

Fans, Strips and Lists

Triangle Fans

Triangle Strips

Triangle Lists

Key Handler Update

Lesson Downloads

Fans, Strips and Lists

Triangles are the most complex object that today's video cards can render and these triangles come in 3 flavours: Fans, Strips and Lists. In this lesson we will explain how each of these is defined and show how to draw using them. To keep things easier to follow, each of the triangle drawing routines is in a separate function.

I will also cover WireFrame mode and make a change to our key handler to add support for it.

Triangle Fans

Fans are very efficient space-wise. For each added vertex after the first 3 you get another triangle. The primary limitation is that the very first vertex anchors the whole fan, all of the remaining triangles must use that first vertex as their first vertex. You can make squares, fans, and other radial shapes. Each and every isolated shape you want to draw requires a new function call to the renderer. This is inefficient in terms of CPU use and video card use. Modern video cards like to be fed a lot of data at a time, at least 100 triangles. Because of these limitations, Triangle Fans are rarely used anymore. Here is the declaration of the data for one of the Triangle Fans.

   protected D3D.CustomVertex.TransformedColored[] m_fan1;

   m_fan1 = new D3D.CustomVertex.TransformedColored[6];
   m_fan1[0] = new D3D.CustomVertex.TransformedColored(100.0f,100.0f,1.0f,1.0f,Color.Black.ToArgb());
   m_fan1[1] = new D3D.CustomVertex.TransformedColored(  0.0f,  0.0f,1.0f,1.0f,Color.Red.ToArgb());
   m_fan1[2] = new D3D.CustomVertex.TransformedColored(200.0f,  0.0f,1.0f,1.0f,Color.Green.ToArgb());
   m_fan1[3] = new D3D.CustomVertex.TransformedColored(200.0f,200.0f,1.0f,1.0f,Color.Blue.ToArgb());
   m_fan1[4] = new D3D.CustomVertex.TransformedColored(  0.0f,200.0f,1.0f,1.0f,Color.White.ToArgb());
   m_fan1[5] = new D3D.CustomVertex.TransformedColored(  0.0f,  0.0f,1.0f,1.0f,Color.Red.ToArgb());
   Protected _fan1() As D3D.CustomVertex.TransformedColored

   _fan1 = CType(Array.CreateInstance(GetType( _
       D3D.CustomVertex.TransformedColored), 6), _
       D3D.CustomVertex.TransformedColored())

   _fan1(0) = New D3D.CustomVertex.TransformedColored( _
       100.0F, 100.0F, 1.0F, 1.0F, Color.Black.ToArgb())
   _fan1(1) = New D3D.CustomVertex.TransformedColored( _
         0.0F, 0.0F, 1.0F, 1.0F, Color.Red.ToArgb())
   _fan1(2) = New D3D.CustomVertex.TransformedColored( _
       200.0F, 0.0F, 1.0F, 1.0F, Color.Green.ToArgb())
   _fan1(3) = New D3D.CustomVertex.TransformedColored( _
       200.0F, 200.0F, 1.0F, 1.0F, Color.Blue.ToArgb())
   _fan1(4) = New D3D.CustomVertex.TransformedColored( _
         0.0F, 200.0F, 1.0F, 1.0F, Color.White.ToArgb())
   _fan1(5) = New D3D.CustomVertex.TransformedColored( _
         0.0F, 0.0F, 1.0F, 1.0F, Color.Red.ToArgb())

The first declaration should be familiar. It's the same as the declaration used in the Points and Lines tutorials. The main difference is that rather than using an algorithmic approach and setting each member of the vertex individually, the vertex's constructor will be used. The first 4 floats passed to the constructor are the position for the vertex (x,y,z,rhw) followed by the vertex colour.

The first vertex plotted is the central point for the object. The first object is a square with coloured corners and center. The second fan is something more recognizable as "fan-like". The standard culling rules apply, so make sure all triangles are defined with clockwise winding order. Next is the code to draw this triangle fan.

   m_device.DrawUserPrimitives(D3D.PrimitiveType.TriangleFan, //The type of primitive we're rendering

                               m_fan1.Length - 2,         //The number of primitives we're rendering

                               m_fan1);                   //The data to render

   '   D3D.PrimitiveType.TriangleFan:  The type of primitive we're rendering

   '                  _fan1.Length-2:  The number of primitives we're rendering

   '                           _fan1:  The data to render

   _device.DrawUserPrimitives(D3D.PrimitiveType.TriangleFan, _fan1.Length - 2, _fan1)

One nice feature of .Net is that arrays automatically know how many elements they contain. Since it's known that the first 3 vertices define the first triangle and every additional vertex adds another triangle the number of primitives (triangles) can easily be calculated as the length of the array minus 2. Otherwise this is virtually the same as the calls used in previous tutorials. Since Triangle Fans are so rarely used (and for good reason) we won't spend more time on them.

Triangle Strips

Strips share the space-saving storage of Fans but have fewer limitations. The first 3 vertices define the first triangle and every additional vertex defines another triangle made from itself and the previous 2 vertices. Since a Strip isn't anchored to a single position like a Fan is you have fewer limitations in what you can represent. One odd thing about Strips is how the culling rules apply. For every second triangle, the culling rules are reversed, otherwise you wouldn't be able to make useful shapes with them. This can make generating Strips by hand a very tedious affair, but it's also something you would rarely do.

Another limitation Strips share with Fans is that you cannot create unconnected shapes. For each unconnected shape you would need a separate draw call. But with Strips there is a work around. You can use invisible triangles to stitch together otherwise unconnected shapes. There are 2 ways to do this. The first is to use back facing triangles, but if your model ever rotates then those "invisible" triangles could suddenly appear, so it's not a good solution. The second way is to use degenerate triangles. A degenerate triangle has no mathematical area, so it isn't rendered. This is generally done by repeating a vertex. If you have 2 strips, you can usually just repeat the last vertex of the first shape. Here is the definition of our Triangle Strip data.

   m_strip = new D3D.CustomVertex.TransformedColored[11];
   //The first shape

   m_strip[0] = new D3D.CustomVertex.TransformedColored(250.0f,150.0f,1.0f,1.0f,Color.Red.ToArgb());
   m_strip[1] = new D3D.CustomVertex.TransformedColored(300.0f, 50.0f,1.0f,1.0f,Color.Green.ToArgb());
   m_strip[2] = new D3D.CustomVertex.TransformedColored(350.0f,150.0f,1.0f,1.0f,Color.Blue.ToArgb());
   m_strip[3] = new D3D.CustomVertex.TransformedColored(400.0f, 50.0f,1.0f,1.0f,Color.Red.ToArgb());
   m_strip[4] = new D3D.CustomVertex.TransformedColored(450.0f,150.0f,1.0f,1.0f,Color.FromArgb(0xF7,0xF7,0x00).ToArgb());
   //A degenerate triangle to stitch them together

   m_strip[5] = new D3D.CustomVertex.TransformedColored(450.0f,150.0f,1.0f,1.0f,Color.FromArgb(0xF7,0xF7,0x00).ToArgb());
   //The second shape

   m_strip[6] = new D3D.CustomVertex.TransformedColored(250.0f,350.0f,1.0f,1.0f,Color.Red.ToArgb());
   m_strip[7] = new D3D.CustomVertex.TransformedColored(300.0f,250.0f,1.0f,1.0f,Color.Green.ToArgb());
   m_strip[8] = new D3D.CustomVertex.TransformedColored(350.0f,350.0f,1.0f,1.0f,Color.Blue.ToArgb());
   m_strip[9] = new D3D.CustomVertex.TransformedColored(400.0f,250.0f,1.0f,1.0f,Color.Red.ToArgb());
   m_strip[10] = new D3D.CustomVertex.TransformedColored(450.0f,350.0f,1.0f,1.0f,Color.FromArgb(0xF7,0xF7,0x00).ToArgb());
   _strip = CType(Array.CreateInstance(GetType( _
       D3D.CustomVertex.TransformedColored), 11), _
       D3D.CustomVertex.TransformedColored())
   'The first shape

   _strip(0) = New D3D.CustomVertex.TransformedColored( _
       250.0F, 150.0F, 1.0F, 1.0F, Color.Red.ToArgb())
   _strip(1) = New D3D.CustomVertex.TransformedColored( _
       300.0F, 50.0F, 1.0F, 1.0F, Color.Green.ToArgb())
   _strip(2) = New D3D.CustomVertex.TransformedColored( _
       350.0F, 150.0F, 1.0F, 1.0F, Color.Blue.ToArgb())
   _strip(3) = New D3D.CustomVertex.TransformedColored( _
       400.0F, 50.0F, 1.0F, 1.0F, Color.Red.ToArgb())
   _strip(4) = New D3D.CustomVertex.TransformedColored( _
       450.0F, 150.0F, 1.0F, 1.0F, Color.FromArgb(&HF7, &HF7, &H0).ToArgb())
   'A degenerate triangle to stitch them together

   _strip(5) = New D3D.CustomVertex.TransformedColored( _
       450.0F, 150.0F, 1.0F, 1.0F, Color.FromArgb(&HF7, &HF7, &H0).ToArgb())
   'The second shape

   _strip(6) = New D3D.CustomVertex.TransformedColored( _
       250.0F, 350.0F, 1.0F, 1.0F, Color.Red.ToArgb())
   _strip(7) = New D3D.CustomVertex.TransformedColored( _
       300.0F, 250.0F, 1.0F, 1.0F, Color.Green.ToArgb())
   _strip(8) = New D3D.CustomVertex.TransformedColored( _
       350.0F, 350.0F, 1.0F, 1.0F, Color.Blue.ToArgb())
   _strip(9) = New D3D.CustomVertex.TransformedColored( _
       400.0F, 250.0F, 1.0F, 1.0F, Color.Red.ToArgb())
   _strip(10) = New D3D.CustomVertex.TransformedColored( _
       450.0F, 350.0F, 1.0F, 1.0F, Color.FromArgb(&HF7, &HF7, &H0).ToArgb())

Initialization is similar to the Fan data above. Three of the vertices are initialized to have a colour which is not represented by a named colour. Often you'll want to set a custom colour and have a need to specify its exact colour value. One method is shown here. The FromArgb method has a few overloads, this one takes 3 colour values (red, green, blue) and initializes the colour with those values. The ToArgb method then converts that to an int, which is what the TransformedColored constructor expects. It's rather indirect, but it works.

The geometry may not be very exciting, but it should illustrate my point. Geometry that naturally falls into a strip-like shape is a good candidate for Strips, but with a bit of effort you can make anything work. While degenerate triangles aren't rendered some processing is still done on them. If you have a situation where you see that a lot of degenerate triangles are needed, you may want to consider using Triangle Lists instead.

Here is the code to draw the strip. Note that the formula to calculate the number of primitives is the same as it was for Triangle Fans.

   m_device.DrawUserPrimitives(D3D.PrimitiveType.TriangleStrip, //The type of primitive we're rendering

                               m_strip.Length - 2,         //The number of primitives we're rendering

                               m_strip);                   //The data to render

   ' D3D.PrimitiveType.TriangleStrip:  The type of primitive we're rendering

   '                 _strip.Length-2:  The number of primitives we're rendering

   '                          _strip:  The data to render

   ')

   _device.DrawUserPrimitives(D3D.PrimitiveType.TriangleStrip, _
       _strip.Length - 2, _strip)

Triangle Lists

Triangle Lists are by far the easiest type of triangles to deal with. They have the standard culling rules (clockwise only, by default anyway), but other than that you can do anything. You do pay a price for this flexibility though, each triangle has to be defined by 3 vertices on its own. In many cases, that's not a problem and their flexibility makes them worth using. In a later lesson we'll look at using Index Buffers, which make Triangle Lists almost as compact as similar Triangle Strips while maintaining their flexibility.

To illustrate their flexibility, all of the shapes drawn by the fans and strip will be drawn by using one single Triangle List. The original fan data had 12 vertices, but required 2 calls to DrawPrimitiveUP. The fan data as Triangle Lists has 24 vertices but only requires 1 call. The original strip data had 11 vertices (1 degenerate) while the list version has 18, but required no special handling. Only the first portion of that data will be shown here.

   m_list = new D3D.CustomVertex.TransformedColored[42];
   //Fan1

   m_list[0] = new D3D.CustomVertex.TransformedColored(100.0f,100.0f,1.0f,1.0f,Color.Black.ToArgb());
   m_list[1] = new D3D.CustomVertex.TransformedColored(  0.0f,  0.0f,1.0f,1.0f,Color.Red.ToArgb());
   m_list[2] = new D3D.CustomVertex.TransformedColored(200.0f,  0.0f,1.0f,1.0f,Color.Green.ToArgb());
   m_list[3] = new D3D.CustomVertex.TransformedColored(100.0f,100.0f,1.0f,1.0f,Color.Black.ToArgb());
   m_list[4] = new D3D.CustomVertex.TransformedColored(200.0f,  0.0f,1.0f,1.0f,Color.Green.ToArgb());
   m_list[5] = new D3D.CustomVertex.TransformedColored(200.0f,200.0f,1.0f,1.0f,Color.Blue.ToArgb());
   m_list[6] = new D3D.CustomVertex.TransformedColored(100.0f,100.0f,1.0f,1.0f,Color.Black.ToArgb());
   m_list[7] = new D3D.CustomVertex.TransformedColored(200.0f,200.0f,1.0f,1.0f,Color.Blue.ToArgb());
   m_list[8] = new D3D.CustomVertex.TransformedColored(  0.0f,200.0f,1.0f,1.0f,Color.White.ToArgb());
   m_list[9] = new D3D.CustomVertex.TransformedColored(100.0f,100.0f,1.0f,1.0f,Color.Black.ToArgb());
   m_list[10] = new D3D.CustomVertex.TransformedColored(  0.0f,200.0f,1.0f,1.0f,Color.White.ToArgb());
   m_list[11] = new D3D.CustomVertex.TransformedColored(  0.0f,  0.0f,1.0f,1.0f,Color.Red.ToArgb());
   _list = CType(Array.CreateInstance(GetType( _
       D3D.CustomVertex.TransformedColored), 42), _
       D3D.CustomVertex.TransformedColored())
   'Fan1

   _list(0) = New D3D.CustomVertex.TransformedColored( _
       100.0F, 100.0F, 1.0F, 1.0F, Color.Black.ToArgb())
   _list(1) = New D3D.CustomVertex.TransformedColored( _
         0.0F, 0.0F, 1.0F, 1.0F, Color.Red.ToArgb())
   _list(2) = New D3D.CustomVertex.TransformedColored( _
       200.0F, 0.0F, 1.0F, 1.0F, Color.Green.ToArgb())
   _list(3) = New D3D.CustomVertex.TransformedColored( _
       100.0F, 100.0F, 1.0F, 1.0F, Color.Black.ToArgb())
   _list(4) = New D3D.CustomVertex.TransformedColored( _
       200.0F, 0.0F, 1.0F, 1.0F, Color.Green.ToArgb())
   _list(5) = New D3D.CustomVertex.TransformedColored( _
       200.0F, 200.0F, 1.0F, 1.0F, Color.Blue.ToArgb())
   _list(6) = New D3D.CustomVertex.TransformedColored( _
       100.0F, 100.0F, 1.0F, 1.0F, Color.Black.ToArgb())
   _list(7) = New D3D.CustomVertex.TransformedColored( _
       200.0F, 200.0F, 1.0F, 1.0F, Color.Blue.ToArgb())
   _list(8) = New D3D.CustomVertex.TransformedColored( _
         0.0F, 200.0F, 1.0F, 1.0F, Color.White.ToArgb())
   _list(9) = New D3D.CustomVertex.TransformedColored( _
       100.0F, 100.0F, 1.0F, 1.0F, Color.Black.ToArgb())
   _list(10) = New D3D.CustomVertex.TransformedColored( _
         0.0F, 200.0F, 1.0F, 1.0F, Color.White.ToArgb())
   _list(11) = New D3D.CustomVertex.TransformedColored( _
         0.0F, 0.0F, 1.0F, 1.0F, Color.Red.ToArgb())

Doing this same data set with Strips would work as well, but it would require a number of degenerate triangles. Not only to stitch the separate shapes together, but also to reset your drawing position within the fans. This would add overhead and would likely not be much smaller than the equivalent Triangle List representation of the data.

To transform the Fan data to List format took a little work. The initial triangle is fine as it is. For the remaining triangles in each fan I set the first vertex to be equal to the initial/anchor vertex. The unique vertex was used as the 3rd vertex and the second vertex came from the previous triangles last vertex.

Transforming the Strip data took a bit more juggling because of the alternating culling rule. To be honest I just juggled things around until they looked right. Sometimes that's all it takes. Often the best thing you can do is doodle on some paper to see the vertices and the shape in context. Restaurant or cocktail napkins work well for this, though the ultimate doodling surface, serviettes are only available to Canadians. Just like chesterfields (not that you should doodle on your chesterfield, and no sawing the table either).

So even though Triangle Lists do take up much more space, they can fit any situation. Of course there is no reason that an application can't make use of all of these types. Just choose the representation that best fits your data. It should also be noted that modern hardware is optimized to handle Triangle Lists, so the costs associated with their use are often lower than you might think.

Key Handler Update

To support toggling back and forth between Wirefame mode, I added a new member variable (m_wireframe) and set it to be toggled by the 'w' key. The previous lessons also used a boolean to control which data set was shown. To handle the 3 different sets this lesson requires the variable was changed to an int and renamed to a more appropriate m_draw_mode. Here's the entire Key handler for reference.

   protected override void OnKeyDown(System.Windows.Forms.KeyEventArgs e){

      switch(e.KeyCode) {
         case Keys.T:
            m_draw_mode++;
            if (m_draw_mode > 2){
               m_draw_mode = 0;
            }
            break;
         case Keys.W:
            m_wireframe = !m_wireframe;
            break;
         default:
            base.OnKeyDown(e);
            break;
      }

   }
   Protected Overrides Sub OnKeyDown(ByVal e As System.Windows.Forms.KeyEventArgs)
      Select Case e.KeyCode
         Case Keys.T
            _drawMode = (_drawMode + 1) Mod 3
         Case Keys.W
            _wireFrame = Not _wireFrame
         Case Else
            MyBase.OnKeyDown(e)
      End Select
   End Sub
Wireframe

In Direct3D there are many renderstates. These set global parameters for how things should be rendered. The renderstate that controls Culling was introduced in an earlier lesson. Another very useful renderstate is the Fill Mode. The Fill Mode can be set to 3 different values: Point, Wireframe, Solid. Solid is the default so you're used to that. Point draws a pixel where each vertex is and Wireframe draws the edges of each triangle.

You generally won't use wireframe for rendering, but it's very handy for debugging. When you run the sample application and turn on wireframe (the 'w' key) you'll be able to see each triangle. Sometimes when a vertex is in the wrong position it can be hard to see exactly what is wrong. Switching to Wireframe mode often makes those problems easier to see.

Another common problem is having triangles not get rendered. If you change the Cull mode to counter-clockwise, then the triangles that would normally show up will disappear. If the triangle that was missing reappears, then you know it was a problem with it's winding order.

Here's the code added to our render function to toggle wireframe mode.

   if (m_wireframe){
      m_device.RenderState.FillMode = D3D.FillMode.WireFrame;
   }else{
      m_device.RenderState.FillMode = D3D.FillMode.Solid;
   }
   If _wireFrame Then
      _device.RenderState.FillMode = D3D.FillMode.WireFrame
   Else
      _device.RenderState.FillMode = D3D.FillMode.Solid
   End If

Lesson Downloads

Back