Turtle fractals in AutoCAD using .NET - Part 6

What, yet another part? The series that just goes on and on... ๐Ÿ™‚

To catch up, here are parts 1, 2, 3, 4 and 5, and the original post.

This post looks at more organic forms, once again, but this time in 3D. I had lots of fun with this one: I took the principle shown in Part 2, where we looked at adding random factors to fractal patterns when creating 2D trees and extended it to work with the 3D version of the TurtleEngine. The principle is the same, we're just creating three branches at relatively evenly spaced angles from each trunk (or senior branch). To add a little colour we draw "leaves" at the end (really just green cylinders, but they look good enough from a distance :-).

Here's the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Colors;

using System;

namespace TurtleGraphics

{

  // This class encapsulates pen

  // information and will be

  // used by our TurtleEngine

  class Pen

  {

    // Private members

    private Color m_color;

    private double m_width;

    private bool m_down;

    // Public properties

    public Color Color

    {

      get { return m_color; }

      set { m_color = value; }

    }

    public double Width

    {

      get { return m_width; }

      set { m_width = value; }

    }

    public bool Down

    {

      get { return m_down; }

      set { m_down = value; }

    }

    // Constructor

    public Pen()

    {

      m_color =

        Color.FromColorIndex(ColorMethod.ByAci, 0);

      m_width = 0.0;

      m_down = false;

    }

  }

  // The main Turtle Graphics engine

  class TurtleEngine : IDisposable

  {

    // Private members

    private Transaction m_trans;

    private Polyline3d m_poly;

    private Circle m_profile;

    private Pen m_pen;

    private CoordinateSystem3d m_ecs;

    private bool m_updateGraphics;

    // Public properties

    public Point3d Position

    {

      get { return m_ecs.Origin; }

      set {

        m_ecs =

          new CoordinateSystem3d(

            value,

            m_ecs.Xaxis,

            m_ecs.Yaxis

          );

      }

    }

    public Vector3d Direction

    {

      get { return m_ecs.Xaxis; }

    }

    // Constructor

    public TurtleEngine(Transaction tr)

    {

      m_pen = new Pen();

      m_trans = tr;

      m_poly = null;

      m_profile = null;

      m_ecs =

        new CoordinateSystem3d(

          Point3d.Origin,

          Vector3d.XAxis,

          Vector3d.YAxis

        );

      m_updateGraphics = false;

    }

    public void Dispose()

    {

      TerminateCurrentSection();

    }

    // Public methods

    public void Turn(double angle)

    {

      // Rotate our direction by the

      // specified angle

      Matrix3d mat =

        Matrix3d.Rotation(

          angle,

          m_ecs.Zaxis,

          Position

        );

      m_ecs =

        new CoordinateSystem3d(

          m_ecs.Origin,

          m_ecs.Xaxis.TransformBy(mat),

          m_ecs.Yaxis.TransformBy(mat)

        );

    }

    public void Pitch(double angle)

    {

      // Pitch in our direction by the

      // specified angle

      Matrix3d mat =

        Matrix3d.Rotation(

          angle,

          m_ecs.Yaxis,

          m_ecs.Origin

        );

      m_ecs =

        new CoordinateSystem3d(

          m_ecs.Origin,

          m_ecs.Xaxis.TransformBy(mat),

          m_ecs.Yaxis

        );

    }

    public void Roll(double angle)

    {

      // Roll along our direction by the

      // specified angle

      Matrix3d mat =

        Matrix3d.Rotation(

          angle,

          m_ecs.Xaxis,

          m_ecs.Origin

        );

      m_ecs =

        new CoordinateSystem3d(

          m_ecs.Origin,

          m_ecs.Xaxis,

          m_ecs.Yaxis.TransformBy(mat)

        );

    }

    public void Move(double distance)

    {

      // Move the cursor by a specified

      // distance in the direction in

      // which we're pointing

      Point3d oldPos = m_ecs.Origin;

      Point3d newPos = oldPos + m_ecs.Xaxis * distance;

      m_ecs =

        new CoordinateSystem3d(

          newPos,

          m_ecs.Xaxis,

          m_ecs.Yaxis

        );

      // If the pen is down, we draw something

      if (m_pen.Down)

        GenerateSegment(oldPos, newPos);

    }

    public void PenDown()

    {

      m_pen.Down = true;

    }

    public void PenUp()

    {

      m_pen.Down = false;

      // We'll start a new entity with the next

      // use of the pen

      TerminateCurrentSection();

    }

    public void SetPenWidth(double width)

    {

      m_pen.Width = width;

      TerminateCurrentSection();

    }

    public void SetPenColor(int idx)

    {

      // Right now we just use an ACI,

      // to make the code simpler

      Color col =

        Color.FromColorIndex(

          ColorMethod.ByAci,

          (short)idx

        );

      // If we have to change the color,

      // we'll start a new entity

      // (if the entity type we're creating

      // supports per-segment colors, we

      // don't need to do this)

      if (col != m_pen.Color)

      {

        TerminateCurrentSection();

        m_pen.Color = col;

      }

    }

    // Internal helper to generate geometry

    private void GenerateSegment(

      Point3d oldPos, Point3d newPos)

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

      Autodesk.AutoCAD.ApplicationServices.

      TransactionManager tm =

        doc.TransactionManager;

      // Create the current object, if there is none

      if (m_poly == null)

      {

        BlockTable bt =

          (BlockTable)m_trans.GetObject(

            db.BlockTableId,

            OpenMode.ForRead

          );

        BlockTableRecord ms =

          (BlockTableRecord)m_trans.GetObject(

            bt[BlockTableRecord.ModelSpace],

            OpenMode.ForWrite

          );

        // Create the polyline

        m_poly = new Polyline3d();

        m_poly.Color = m_pen.Color;

        // Add the polyline to the database

        ms.AppendEntity(m_poly);

        m_trans.AddNewlyCreatedDBObject(m_poly, true);

        // Add the first vertex

        PolylineVertex3d vert =

          new PolylineVertex3d(oldPos);

        m_poly.AppendVertex(vert);

        m_trans.AddNewlyCreatedDBObject(vert, true);

        m_profile =

          new Circle(oldPos, Direction, m_pen.Width);

        ms.AppendEntity(m_profile);

        m_trans.AddNewlyCreatedDBObject(m_profile, true);

        m_profile.DowngradeOpen();

      }

      // Add the new vertex

      PolylineVertex3d vert2 =

        new PolylineVertex3d(newPos);

      m_poly.AppendVertex(vert2);

      m_trans.AddNewlyCreatedDBObject(vert2, true);

      // Display the graphics, to avoid long,

      // black-box operations

      if (m_updateGraphics)

      {

        tm.QueueForGraphicsFlush();

        tm.FlushGraphics();

        ed.UpdateScreen();

      }

    }

    // Internal helper to generate 3D geometry

    private void TerminateCurrentSection()

    {

      if (m_profile != null && m_poly != null)

      {

        Document doc =

          Application.DocumentManager.MdiActiveDocument;

        Database db = doc.Database;

        Editor ed = doc.Editor;

        try

        {

          // Generate a Region from our circular profile

          DBObjectCollection col =

            new DBObjectCollection();

          col.Add(m_profile);

          DBObjectCollection res =

            Region.CreateFromCurves(col);

          Region reg =

            res[0] as Region;

          if (reg != null)

          {

            BlockTable bt =

              (BlockTable)m_trans.GetObject(

                db.BlockTableId,

                OpenMode.ForRead

              );

            BlockTableRecord ms =

              (BlockTableRecord)m_trans.GetObject(

                bt[BlockTableRecord.ModelSpace],

                OpenMode.ForWrite

              );

            // Extrude our Region along the Polyline3d path

            Solid3d sol = new Solid3d();

            sol.ExtrudeAlongPath(reg, m_poly, 0.0);

            sol.Color = m_pen.Color;

            // Add the generated Solid3d to the database

            ms.AppendEntity(sol);

            m_trans.AddNewlyCreatedDBObject(sol, true);

            // Get rid of the Region, profile and path

            reg.Dispose();

            m_profile.UpgradeOpen();

            m_profile.Erase();

            m_poly.Erase();

          }

        }

        catch (System.Exception ex)

        {

          ed.WriteMessage(

            "\nException: {0}",

            ex.Message

          );

        }

      }

      m_profile = null;

      m_poly = null;

    }

  }

  public class Commands

  {

    static public bool GetTreeInfo(

      out Point3d position,

      out double treeLength,

      out int variability

    )

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      treeLength = 0;

      position = Point3d.Origin;

      variability = 0;

      PromptPointOptions ppo =

        new PromptPointOptions(

          "\nSelect base point of tree: "

        );

      PromptPointResult ppr =

        ed.GetPoint(ppo);

      if (ppr.Status != PromptStatus.OK)

        return false;

      position = ppr.Value;

      PromptDoubleOptions pdo =

        new PromptDoubleOptions(

          "\nEnter tree length <100>: "

        );

      pdo.AllowNone = true;

      PromptDoubleResult pdr =

        ed.GetDouble(pdo);

      if (pdr.Status != PromptStatus.None &&

          pdr.Status != PromptStatus.OK)

        return false;

      if (pdr.Status == PromptStatus.OK)

        treeLength = pdr.Value;

      else

        treeLength = 100;

      PromptIntegerOptions pio =

        new PromptIntegerOptions(

          "\nEnter variability percentage <20>: "

        );

      pio.AllowNone = true;

      PromptIntegerResult pir =

        ed.GetInteger(pio);

      if (pir.Status != PromptStatus.None &&

          pir.Status != PromptStatus.OK)

        return false;

      if (pir.Status == PromptStatus.OK)

        variability = pir.Value;

      else

        variability = 20;

      return true;

    }

    static void Random3DTree(

      TurtleEngine te,

      double distance,

      int variability

    )

    {

      // Some constants

      const double kLeafSize = 3.0;

      const double kBasePitch = Math.PI / 4;

      const double kBaseRoll = 2 * Math.PI / 3;

      const double kBranchFactor = 0.6;

      const double kBranchWidth = 0.05;

      const int kWoodColor = 24;

      const int kLeafColor = 3;

      // Under a certain size, we draw a leaf

      if (distance < kLeafSize)

      {

        // Draw a leaf-like cylinder

        te.SetPenColor(kLeafColor);

        te.PenDown();

        te.Move(kLeafSize);

        te.PenUp();

        te.Move(-kLeafSize);

        return;

      }

      // Generate 7 random factors, each on the same basis:

      //  a base amount = 100 - half the variability

      //  + a random amount from 0 to the variability

      // So a variability of 20 results in 90 to 110 (0.9-1.1)

      Random rnd = new Random();

      int basic = 100 - (variability / 2);

      int num = rnd.Next(variability);

      double factor1 = (basic + num) / 100.0;

      num = rnd.Next(variability);

      double factor2 = (basic + num) / 100.0;

      num = rnd.Next(variability);

      double factor3 = (basic + num) / 100.0;

      num = rnd.Next(variability);

      double factor4 = (basic + num) / 100.0;

      num = rnd.Next(variability);

      double factor5 = (basic + num) / 100.0;

      num = rnd.Next(variability);

      double factor6 = (basic + num) / 100.0;

      num = rnd.Next(variability);

      double factor7 = (basic + num) / 100.0;

      // Multiple out the various items by the factors

      double move1 = factor1 * distance;

      double pitch1 = factor2 * kBasePitch;

      double pitch2 = factor3 * kBasePitch;

      double pitch3 = factor4 * kBasePitch;

      double roll1 = factor5 * 2 * Math.PI;

      double roll2 = roll1 + (factor6 * kBaseRoll);

      double roll3 = roll1 + roll2 + (factor7 * kBaseRoll);

      // Width of the trunk/branch is a tenth of

      // of the length

      te.SetPenWidth(move1 * kBranchWidth);

      te.SetPenColor(kWoodColor);

      te.PenDown();

      // Draw the main trunk/branch

      te.Move(move1);

      // Draw the first sub-tree

      te.Roll(roll1);

      te.Pitch(pitch1);

      Random3DTree(te, distance * kBranchFactor, variability);

      te.Pitch(-pitch1);

      te.Roll(-roll1);

      // Draw the second sub-tree

      te.Roll(roll2);

      te.Pitch(pitch2);

      Random3DTree(te, distance * kBranchFactor, variability);

      te.Pitch(-pitch2);

      te.Roll(-roll2);

      // Draw the third sub-tree

      te.Roll(roll3);

      te.Pitch(pitch3);

      Random3DTree(te, distance * kBranchFactor, variability);

      te.Pitch(-pitch3);

      te.Roll(-roll3);

      // Draw back down to the start of this sub-

      // tree, with the same thickness, as this

      // may have changed in deeper sub-trees

      te.PenUp();

      te.Move(-move1);

    }

    [CommandMethod("3DT")]

    static public void ThreeDimensionalTree()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      double treeLength;

      Point3d position;

      int variability;

      if (!GetTreeInfo(

            out position,

            out treeLength,

            out variability

            )

          )

        return;

      Transaction tr =

        doc.TransactionManager.StartTransaction();

      using (tr)

      {

        TurtleEngine te = new TurtleEngine(tr);

        using (te)

        {

          // Draw a random fractal tree

          te.Position = position;

          te.Pitch(Math.PI / -2);

          Random3DTree(te, treeLength, variability);

        }

        tr.Commit();

      }

    }

  }

}

Bear in mind that the trees created are, once again, pretty heavy models given the level of detail. You wouldn't draw trees this way in a real-world model, for instance - it would add significant bloat to your designs.

Now for some trees created by the 3DT command, taking the default options.

First in plan view:

3D trees - plan

Then in full 3D:

3D trees - 3D view

We can also use AutoCAD's sun implementation to see our trees' shadows (in this case at around 2:30pm in California):

3D trees - 3D view with shadows

Getting the trees to draw properly was quite a challenge. Along the way there were some very interesting (even beautiful) failures... Here are a couple, for fun:

1st attempt - branches in the cloud

I especially liked this one:

2nd attempt - alien flower

7 responses to “Turtle fractals in AutoCAD using .NET - Part 6”

  1. Fernando Malard Avatar
    Fernando Malard

    Kean, really nice!
    I like those trees.

    One thing came to my mind is the number of created entities which certainly will cause some visualization slow down on big trees/drawings.

    I was wondering if you can use blocks for each leave and branch segment so these entities can be reused through BlockReferences avoiding a huge number of single entities.

    Do you think this is possible?

    Regards.

  2. Kean Walmsley Avatar

    Hi Fernando,

    Absolutely - the size & level of detail of these models will cause performance issues.

    It's certainly feasible to make more use of blocks for this: we could have a block containing a single cylinder of unit length & width (with colour set ByBlock) which could then be inserted at the appropriate colour/position/rotation & (most importantly) a non-uniform scale.

    It would definitely help in this tree example - as the width changed for each branch/twig/leaf, which means a lot of separate cylinders - but the advantages would be fewer where we have more complex extrusion paths (such as the Hilbert cube example in Part 5).

    One option would be to use blocks only for the case of 2-vertex extrusion paths, and leave longer paths to use the existing approach.

    Very good suggestion, though, thank you!

    Kean

  3. Fernando Malard Avatar
    Fernando Malard

    Kean,

    Another approach would be a custom entity where your tree parameters like branch, leaf size, etc; would be the entity's properties.

    Using the same logic you did for creating the tree entities we can draw the full tree using AcGi drawing primitives inside custom entity's worldDraw().

    I'm sure this will be faster to draw than the previous "block solution". Further we could create a "light drawing" version of this tree which will represent it only by simple color line segments avoiding performance issues during drawing editing.

    Sound better?
    Regards.

  4. Kean Walmsley Avatar

    Hi Fernando,

    I actually feels it goes without saying that this could be implemented as a custom entity: my aim was not to implement custom objects, but to capture and create turtle graphics as standard entities. Perhaps that isn't clear from the series itself, but it was certainly one of the design tenets I was following.

    Yes, it would be more performant, but that for me wasn't the ultimate goal: I'm more interested in seeing how enabling a turtle graphics implementation might be within AutoCAD (especially when modelling organic/fractal forms) and ultimately considering what optimizations might be made to the engine on the back-end (which is why I like the block approach - it doesnn't change the client code at all). I'm not really interested in writing a tree design application. ๐Ÿ™‚

    Regards,

    Kean

  5. Hi Kean,
    I was looking at something one of the guys here in the office was working on today and it struck me that the TurtleGfx code would be just the ticket - the problem being I'm not sure how to implement it:

    Basically, the Turtle would need to follow the path of a polyline that has a block at each end. Normally this polyline would have only a start and an end point (thus making it straight). Along the length of the polyline the turtle should carry out instructions based on information contained in a csv file. Primarily these instructions involve placing specific blocks at measured locations and associated text notes (along the length of the line)

    Do you think this might work? (or is it akin to using a sledgehammer to break an egg?)
    Where would I even begin to look for code that would allow the TurtleEngine to travel along a given polyline path? (Have I already missed it in one of your previous posts?)

    Thanks in advance,

    Alex.

  6. Hi Alex,

    No, I didn't show that... in your case, though, I do think TurtleGraphics is overkill: you'd have to extend the engine so much to do what you need that you might as well create something more specific.

    I'd suggest just using Curve.GetPointAtParameter() or Curve[2/3]d.GetSamplePoints() to get points along the segments of your Polylines, and then add what you need to at those locations. The info could, of course, come from a CSV file.

    Regards,

    Kean

  7. Thanks Kean, that's an excellent starting point. [goes off to read up on Curve.GetPointAtParameter()]

    ๐Ÿ™‚

Leave a Reply to Kean Walmsley Cancel reply

Your email address will not be published. Required fields are marked *