Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

3D L-System with C# and WPF

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
21 Dec 2014CPOL7 min read 32.5K   1.1K   17   5
How to use C# to create WPF 3D graphics.

Introduction

I wanted to learn a bit about 3D graphics using WPF and C# and decided a fun way would be to implement a simple 3D application to draw L-System fractals.

Background

L-System Basics

An L-system (Lindenmayer system) is a character re-writing system first developed to algorithmically describe plant development. Using a few simple rules you can describe a number of fractal patterns, and patterns that closely resemble plants.

A typical L-System has an axiom (starting string) and rules for expanding the string. This program implements the following rules:

[              push
]              pop
F              Draw forward
f              move forward
B              draw backwards
b              move backwards
+              rotate +z              pitch
-              rotate -z              pitch
}              rotate +y              yaw
{              rotate -y              yaw
>              rotate +x              roll
<              rotate -X              roll

For a 2D system only the + and – rotations are implemented. For 3D system you also have to implement rotating in the other two dimensions. You also have to define how far you rotate, and the line lengths.

It is often useful to be able use placeholder characters or redefine the command characters to make setting up the rules simpler. This application also supports command redefinition.

Simple 2D Examples

Axiom: A

Rules: A=BA, B=AB

Iterations

  1. A
  2. BA
  3. ABBA
  4. BAABABBA

Now, this example doesn’t do anything, but if the rules and axiom consist of the letters in the rules above then you end up with a long string that gives the commands for drawing a shape. For example:

Axiom: F

Rule: F=F+F--F+F

Angle: 60

Iterations: 2

Creates this picture:

Image 1

Branching patterns can be done using the push and pop commands:

Axiom: F

Angle: 35

Rule: F=FF-[-F+F+F]+[+F-F-F]

Image 2

 

 

 

3D Example

This next example is a 3D Hilbert cube (3D version of the Hilbert Square).

Axiom:X

Angle: 90

Rule: X=+<XF+<XFX{F+>>XFX-F}>>XFX{F>X{>

Note that X is not a functional letter. Axioms and rules can use non-functional letters as placeholders that make the more complex patterns easier to implement as l-systems.

 

Image 3

Using the code

Most examples for 3D WPF code have a lot of XAML source. XAML is an easy way to implement simple 3D scenes, but I wanted to implement my application using C# code. This demo application uses XAML to setup the form that allows the rules and other parameters to be entered, but all the 3D coding is done using C# calls.

Minimum for 3D WPF Scene

To draw a 3D scene under WPF you need the following at a minimum:

  • A control to draw the scene on (I use a grid)
  • A ViewPort
  • A 3D model
  • A camera
  • A light

The grid is named GridDraw. A camera and light is created and added to the viewport. A simple 3D model is created and added to the viewport, along with a transform so the model can be moved and rotated. The following code is the minimum to display a 3D scene in WPF.

C#
viewport3D = new Viewport3D();
modelVisual3DLight = new ModelVisual3D();
baseModel = new ModelVisual3D();
model3DGroup = new Model3DGroup();

transformGroup = new Transform3DGroup();  // for rotating the model via the mouse

camera = new PerspectiveCamera();
camera.Position = new Point3D(0, 0, -32);
camera.LookDirection = new Vector3D(0, 0, 1);
camera.FieldOfView = 60;
camera.NearPlaneDistance = 0.125;

light = new DirectionalLight();
light.Color = Colors.White;
light.Direction = new Vector3D(1, 0, 1);
modelVisual3DLight.Content = light;

// draw a 3D box from currLocation to nextLocation, color c
geometryBaseBox = Create3DLine(currLocation, nextLocation, c, lineThickness, boxUp);
model3DGroup.Children.Add(geometryBaseBox);
// the above two lines of code is repeated for each box that makes up the current
// pattern. More on that later

baseModel.Transform = transformGroup;
baseModel.Content = model3DGroup
viewport3D.Camera = camera;
viewport3D.Children.Add(modelVisual3DLight);
viewport3D.Children.Add(baseModel);
GridDraw.Children.Add(viewport3D);

 

At this point WPF will draw our box on the grid control.

Creating the Command String

Of course, one of the l-system images will have a large number of boxes making up the model which are added in a loop to a Model3DGroup as we read the command string created by processing the axiom and rules. While drawing the command string requires a recursive function to support the push/pop commands, the creation of the command string is purely iterative.

The code to process the axiom and rules is pretty simple:

  • Add the axiom to a string
    • For each iteration
      • For each rule
        • Scan the string, if the character matches a rule then replace the character with the rule string.
        • If the current character has no rules, just copy it over
    • Replace any redefined commands

Drawing the Command String

Once the axiom and rules are defined we have a long character string that is the instructions to draw an image. Now all we have to do is process this string, character by character, following the instructions. In a 2D system this is very straight forward and you can just use simple trigonometry to draw the pattern.

For example if the final string was F+F+F we would:

  1. Draw a line from the current location forward in the current direction.
  2. Rotate the current direction
  3. Draw a line in the new direction
  4. Rotate
  5. Draw the last line pointing in the new direction

Rotating in 3D requires tracking the roll, pitch, and yaw of our current direction. Trying to do this using three separate angles and trigonometry causes many problems including gimbal lock. So we use Quaternions to track the current angle. Each time we have to rotate we just multiple our current direction by a Quaternion that has its axis pointing along the axis we want to rotate on and its angle set to the current amount to rotate. The WPF quaternion structure supports these methods, but it lacks a method to give you a vector that points in the direction the quaternion is currently pointing that can be used to know where the next location is, that is, to move along the direction the quaternion is pointing. Under XNA this would just be the Q.Forward. A simple function that uses the WPF Matrix structure solves this issue:

C#
// make a vector point in the direction of the quaternion with magnitude dd
private Vector3D QuatToVect(Quaternion q, double dd, Vector3D f)
{
   Matrix3D m = Matrix3D.Identity;
   m.Rotate(q);
   f=m.Transform(f * dd);

   return f;
   }

Now we have all pieces required to process the full command string. Here is an excerpt from the code that creates the 3D model from the command string. Only the F + and – commands are included here. The download has the full code.

Variables:

  • str : command string
  • max : length of string
  • currLocation a Vector3D
  • nextLocation a Vector3D
  • boxUp a Vector3D that points “up” on the current box
  • quatRot a Quaternion that is the current direction we are drawing towards
  • vPitch a Quaternion with its axis along the Z-axis
  • vForward how we move forward
C#
Vector3D vPitch = new Vector3D(0, 0, 1);
Color[] someColors = { Colors.Red, Colors.Blue, Colors.Green };
Vector3D vMove = new Vector3D();
Vector3D vForward = new Vector3D(1, 0, 0);

for (i = index; i < max; i++)
       {
       c=someColors[i%someColors.Length]; //pick one of the colours
       switch (str[i])
              {
              case 'F':  // draw forward
                     vMove = QuatToVect(quatRot, lineLength, vForward);
                     nextLocation = currLocation + vMove;
                               geometryBaseBox = Create3DLine(currLocation, nextLocation, c,
                               lineThickness, boxUp);
                     model3DGroup.Children.Add(geometryBaseBox);
                     currLocation = nextLocation;
                    break;

              case '+': // Pitch
                     quatRot *= new Quaternion(vPitch, rotAngle);
                     break;

              case '-':  // Pitch
                     quatRot *= new Quaternion(vPitch, -rotAngle);
                     break;
              }
       }

Once all the boxes are added we then add the whole group to our model:

 

// add the model created by DrawStringLow
baseModel.Transform = transformGroup;
baseModel.Content = model3DGroup;

The above code is just an excerpt from the full function that ‘draws’ the command string. This function is called recursively to handle the push/pop commands required for branching structures. At a high level the full code does the following:

  • Get user parameters
  • Create the command string
  • Setup the 3D WPF environment
  • Recursively add 3D boxes to the model, each box is a 3D ‘line’ in the model.

Transforming the Model

Next we want to be able to view the model from various angles and positions. One way to do this is to move the camera around the model. This is a very good method as it doesn’t require adding transforms to the model, but we only use this method to zoom in and out. There are lots of WPF examples for moving the camera on a logical sphere centered on your model. Here is the zooming code:

C#
// zoom camera in and out, shift key zooms faster
        private void MouseWheel_GridDraw(object sender, MouseWheelEventArgs e)
            {
            double speed = 100.0;

            if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
                speed /= 8.0;

            camera.Position = new Point3D(
            camera.Position.X,
            camera.Position.Y,
            camera.Position.Z - e.Delta / speed);
            }

All this does is change the Z position of the camera.

To rotate and translate the model we get the users mouse input and add rotation or translation transforms. This simple code just keeps on adding more transforms as the user moves the mouse. This is just a demo of how to transform a model. I wouldn’t use this method in a ‘real’ application. For a ‘real’ application you would want to minimize the number of transforms applied to model by calculating the new overall transformation matrix and only apply that one transform.

Note that transformGroup is the transformGroup for the whole bundle of small boxes added to modelGroup.

C#
// translate or rotate our model
        // some people just move the camera, but this is a demo
        private void GridDraw_MouseMove(object sender, MouseEventArgs e)
            {
            if (currState == state.rotate)
                {
                double dx, dy;
                dx = mX - e.GetPosition(GridDraw).X;
                dy = mY - e.GetPosition(GridDraw).Y;
                RotateTransform3D rotateT;

                if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
                    {
                    dx *= 4.0;
                    dy *= 4.0;
                    }

                if (Math.Abs(dx) > smallD)
                    {
                    // rotate on the Z axis
                    rotateT = new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 0, 1), dx));
                    transformGroup.Children.Add(rotateT); // this is the transform for the model so this                                                          //  rotates it.
                    }

                if (Math.Abs(dy) > smallD)
                    {
                    // rotate on the Z axis
                    rotateT = new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(1, 0, 0), dy));
                    transformGroup.Children.Add(rotateT); // this is the transform for the model so this                                                          //  rotates it.
                    }

                mX = e.GetPosition(GridDraw).X;
                mY = e.GetPosition(GridDraw).Y;
                }
            else
                {
                if (currState == state.translate)
                    {
                    double ts;
                    double dx, dy;
                    ts = translateSensitivity;
                    if ( Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
                        ts /= 4.0;

                    dx = (mX - e.GetPosition(GridDraw).X)/ts;
                    dy = (mY - e.GetPosition(GridDraw).Y)/ts;
                    TranslateTransform3D translateT;

                    if (Math.Abs(dx) > smallD)
                        {
                        // move in x direction
                        translateT = new TranslateTransform3D(dx, 0, 0);
                        transformGroup.Children.Add(translateT);
                        }

                    if (Math.Abs(dy) > smallD)
                        {
                        // move in y direction
                        translateT = new TranslateTransform3D(0, dy, 0);
                        transformGroup.Children.Add(translateT);
                        }

                    mX = e.GetPosition(GridDraw).X;
                    mY = e.GetPosition(GridDraw).Y;
                    }
                }
            }

Saving and Loading

And finally, we want to be able to save and load the parameters for these patterns. XML is the way to save this sort of thing these days (I think I just dated myself) so here are excerpts from the save and load code. The load code loads the whole xml file into memory then plunks the data back into the correct text boxes. The save code saves each parameter in its own xml node, and all the rules in a set.

C#
// lets save the parms in a trendy xml file
        private void ButtonSave_Click(object sender, sw.RoutedEventArgs e)
            {
            // save this pattern in a simple xml file
            XmlDocument xml = new XmlDocument();
            XmlNode root;
            XmlNode node;

            root = xml.CreateElement("pattern");
            xml.AppendChild(root);

            node = xml.CreateElement("axiom");
            node.InnerText = TextboxAxiom.Text;
            root.AppendChild(node);

            // do the same for the others parms here…
            // all the rules
            string[] sep = { "\r\n" };
            string[] lines = TextboxRules.Text.Split(sep, StringSplitOptions.RemoveEmptyEntries);
            foreach (string str in lines)
                {
                node = xml.CreateElement("rule");
                node.InnerText = str;
                root.AppendChild(node);
                }

            Microsoft.Win32.SaveFileDialog diag = new Microsoft.Win32.SaveFileDialog();
            diag.Filter = "xml files (*.xml)|*.xml|text files (*.txt)|*.txt|all files(*.*)|*.*";
            diag.FilterIndex = 0;
            Nullable<bool> result=diag.ShowDialog();
       
            if ( result == true )
                {
                xml.Save(diag.FileName);
                }
            }

        // lets read in all the parms from a trendy xml file
        private void ButtonLoad_Click(object sender, sw.RoutedEventArgs e)
            {
            Microsoft.Win32.OpenFileDialog diag = new Microsoft.Win32.OpenFileDialog();
            diag.Filter = "xml files (*.xml)|*.xml|text files (*.txt)|*.txt|all files(*.*)|*.*";
            diag.FilterIndex = 0;
            Nullable<bool> result=diag.ShowDialog();
            if ( result == true)
                {
                try
                    {
                    XmlNode node;
                    XmlNodeList nodeList;
                    XmlDocument xml = new XmlDocument();
                    xml.Load(diag.FileName);

                    node = xml.SelectSingleNode("/pattern");
                    if (node == null)
                        {
                        sw.MessageBox.Show("Root node: pattern not found. Likely not a pattern file.");
                        }
                    else
                        {
                        node = xml.SelectSingleNode("pattern/axiom");
                        if (node != null)
                            {
                            TextboxAxiom.Text = node.InnerText;
                            }
                        node = xml.SelectSingleNode("pattern/initialAngle");
                        if (node != null)
                            {
                            TextboxInitialAngle.Text = node.InnerText;
                            }
                          //  do the same for the rest of the parms here…

                        TextboxRules.Clear();
                        foreach (XmlNode n in nodeList)
                            {
                            TextboxRules.AppendText(n.InnerText + "\r\n");
                            }
                        // above loop adds a blank rule to the end, makestring has to ignore this extra                         //   blank line
                        }
                    }

                catch (Exception ex)
                    {
                    sw.MessageBox.Show("Error: " + ex.Message);
                    }
                }
            }

Final Comments

When you download the code and have a look at it you might be wondering what the “up” vector is when drawing the 3D boxes. Given two 3D points it isn’t obvious which way is ‘up’ when drawing a box. You can get a plane that is perpendicular to the two points easy enough, but that doesn’t help much orienting the box. The up vector keeps track of which way is UP. It starts off hard-coded to be up and then it is rotated each time the direction is rotated to keep it in sync with the drawing.

This is partially based on my 2D article.

History

First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Canada Canada
Professional Programmer living in Beautiful Vancouver, BC, Canada.

Comments and Discussions

 
QuestionAhem - it's XAML, not AXML. Pin
Pete O'Hanlon22-Dec-14 2:10
subeditorPete O'Hanlon22-Dec-14 2:10 
AnswerRe: Ahem - it's XAML, not AXML. Pin
arussell22-Dec-14 3:23
professionalarussell22-Dec-14 3:23 
Generalfunny :) Pin
roks nicolas22-Dec-14 1:53
professionalroks nicolas22-Dec-14 1:53 
GeneralRe: funny :) Pin
arussell22-Dec-14 6:22
professionalarussell22-Dec-14 6:22 
GeneralRe: funny :) Pin
roks nicolas22-Dec-14 11:09
professionalroks nicolas22-Dec-14 11:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.