Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Real Tree 2

0.00/5 (No votes)
2 Apr 2009 2  
This application shows a simple algorithm for drawing random flowers and trees. The logic is based on fractal sets.
Sample Image

Contents

Introduction

When you watch the shapes made by this software, you may imagine that there are huge and complex mathematical formulas behind it, but it’s not true. The program uses only some simple formulas (like SIN and COS) with a recursive method, and that’s all. All other parts of the program are to make the shapes look better and natural.

In the previous version (DotNet Real Tree), I explained some mathematical logic, and now I wish to add some new features that make it more funny (like real leaves and flowers), but I removed some parts of the program that I thought may make some issues (such as Swastika!). I also removed some other controls and scales to make the algorithm easier and more useful.

What is a Fractal?

A fractal is a shape made by repeating a mathematical formula. Every time, it does a similar action and when you start it from anywhere, it will make the same shape. There are some famous formulas for this job, such as Mandelbrot set or Julia set.

How It Works?

The technique that is used in this application is very similar to the file system in your computer. When you are working on your hard disk drive (e.g. Drive D:\ as in the following figure), you see some files and directories. When you go to a directory, you will see some other files and directories, and when you continue to go into new directories, you will find more; eventually there is a final point that there are no more directories, so you have to go back and search another place.

And, the directory structure:

In this application, the directories are similar to the branches, and files are equal to leaves, flowers or fruits on the tree. As your directories may contain different files, your tree’s branches may contain different objects.

What About Recursion?

Our main function in this application gets information of the current branch and calculates and draws new branches. There is a control for choosing your ideal divisions:

And the result is shown with 3 different selections:

Division per Step = 2 Division per Step = 4 Division per Step = 10

But the procedure makes only one level (step) of the tree, what about the others? This is what we want to speak about.

This is our main procedure in brief:

private void nextBranch(float startX, float startY, float startAngle)
{
        if (myTreeInfo.myStep >=  myTreeInfo.totalSteps) return;
        myTreeInfo.myStep++;

        endX = startX + (float)Math.Cos(startAngle * Math.PI / 180) * branchSize;
        endY = startY - (float)Math.Sin(startAngle * Math.PI / 180) * branchSize;

        DrawImage();

        for (int j = 1; j <= myTreeInfo.divisionPerStep; j++)
        {
            newAngle = startAngle - maxAngle/2+ maxAngle*(j-1);
            nextBranch(endX, endY, newAngle);
        }

        myTreeInfo.myStep--;
 }

And I simulate it practically with 4 steps of generation for branches, you can download the Flash version here:

Using the Code

The algorithm is simple, something like the following flowchart:

Some parts of the code are as follows:

Variables

Initially I declared some Lists and Classes. objectCollection class is for collecting all information about flowers, leaves and fruits (in calculation time). And BranchCollection class is for collecting information of branches (the items for drawing a branch are different from a flower or a fruit). There is also a treeInfo class for holding general information of the tree, such as maximum Tree size, angle between branches and more.

private List<string> imageTypes = new List<string>(new string[] 
			{ "*.gif", "*.png", "*.jpg", "*.bmp" });
private List<Image> picsBackground=new List<Image> ();
private List<Image> picsBase=new List<Image> ();
private List<Image> picsLeaves=new List<Image> ();
private List<Image> picsFlowers=new List<Image> ();
private List<Image> picsFruits=new List<Image>();

private class objectCollection    	// for collecting information of flowers, 
				// leaves and fruits.
{
    internal Image myImage = null;
    internal float myX = 0;
    internal float myY = 0;
    internal float myWidth = 0;
    internal float myHeight = 0;
    internal int myStep = 0;
}
private objectCollection myObjectInfo;
private List<objectCollection> myAllObjectsCollection;

private class BranchCollection
{
    internal float startX = 0;
    internal float startY = 0;
    internal float endX = 0;
    internal float endY = 0;
    internal int myStep = 0;
    internal float myWidth = 0;
    internal Color myColor;
}
private BranchCollection myBranchInfo;
private List<BranchCollection> myAllBranchCollection;

private class treeInfo
{
    internal int myStep, totalSteps;
    internal bool fixedSize, fixedAngle, brokenBranches;
    internal float divisionPerStep, startingBranch, maxSize, maxAngle, maxBrokenBranches;
    internal float leafLevel, trunkHeight, widthSize;
    internal float myProgress, flowerPercent, fruitPercent, leafPercent;
}
private treeInfo myTreeInfo=new treeInfo() ;

private Bitmap myBitmapTree;
private Pen myPen = new Pen(Color.Black);
private Random myRandom = new Random();
private static bool plzStopCalculation; // for manually stopping the calculation
private static bool plzStopDrawing; 	// for manually stopping the drawing

Loading Images

Then I load some images from hard disk and internal resources and select random images for different parts of the application (Background, Fruits...) .

private void firstStart()
{
    loadPictures();
    picGround.BackgroundImage = picsBase[myRandom.Next(picsBase.Count)];
    picBack.BackgroundImage = picsBackground[myRandom.Next(picsBackground.Count)];
    picFlower.BackgroundImage = picsFlowers[myRandom.Next(picsFlowers.Count)];
    picFruit.BackgroundImage = picsFruits[myRandom.Next(picsFruits.Count)];
    picLeaf.BackgroundImage = picsLeaves[myRandom.Next(picsLeaves.Count)];

    picTree.Image = Properties.Resources.about;

    // making "SAVE" directory for saving output images
    try
    {
        DirectoryInfo myDir = new DirectoryInfo(Application.StartupPath + "\\Save\\");
        if (!myDir.Exists) myDir.Create();
    }
    catch (Exception e)
    {
        MessageBox.Show(e.Message);
    }
}

// at the start of the program, load some pictures and shapes
private void loadPictures()
{
    // add some pictures from internal resources
    picsBase.Add(Properties.Resources.ground1);
    picsBackground.Add(Properties.Resources.Edinburgh_Castle__Edinburgh__Scotland );
    picsFlowers.Add(Properties.Resources.Flower__30_);
    picsFlowers.Add(Properties.Resources.Flower__32_);
    picsFruits.Add(Properties.Resources.icons6362);
    picsFruits.Add(Properties.Resources.icons6367);
    picsLeaves.Add(Properties.Resources.leaf_31);

    //load some pictures from Hard disk (different sub directories)
    loadFromHard("Ground", picsBase);
    loadFromHard("Back", picsBackground);
    loadFromHard("Fruits", picsFruits);
    loadFromHard("Flowers", picsFlowers);
    loadFromHard("Leaves", picsLeaves);
}

// load pictures from the hard disk
private void loadFromHard(string myDir, List<Image> myPicList)
{
    DirectoryInfo myFileDir = new DirectoryInfo(Application.StartupPath + "\\" + myDir);
    if (myFileDir.Exists)
    {
        // For each image extension (.jpg, .png, etc.)
        foreach (string imageType in imageTypes)
        {
            // all graphic files in the directory
            foreach (FileInfo myFile in myFileDir.GetFiles(imageType))
            {
                // add image
                try
                {
                    Image image = Image.FromFile(myFile.FullName);
                    myPicList.Add(image);
                }
                catch (OutOfMemoryException)
                {
                    continue;
                }
            }
        }
    }
}

RUN Button

This button has 3 different actions:

  1. Starting a new process.
  2. Stopping the calculation.
  3. Stopping the drawing.

The following procedures control this button:

private void btnOK_Click(object sender, System.EventArgs e)
{
    if (btnOK.Text == "Run")
    {
        goRun();
    }
    else if (btnOK.Text == "Stop Calculation") buttonsStatus(1);
    else if (btnOK.Text == "Stop Drawing") buttonsStatus(2);
}

private void goRun()
{
    setPictures();
    Application.DoEvents();
    getTreeInfo();
    // go for calculations
    buttonsStatus(0);
    calculateTree();
    // go for drawing
    buttonsStatus(1);
    if (!plzStopDrawing) startPaint();
    if (mnuAutoSave.Checked) ImageSave(); //Auto save image on hard disk

    // get ready for another user order
    buttonsStatus(2);
}

private void buttonsStatus(byte myStatus)
{
    if (myStatus == 0)
    {
        // start of calculation
        plzStopCalculation = false;
        plzStopDrawing = false;
        progressBar.Visible = true;
        btnOK.Text = "Stop Calculation";
    }
    else if (myStatus == 1)
    {
        // end of calculation and start of drawing
        plzStopCalculation = true;
        btnOK.Text = "Stop Drawing";
    }
    else
    {
        // at the end of drawing
        plzStopCalculation = true;
        plzStopDrawing = true;
        progressBar.Visible = false;
        btnOK.Text = "Run";
    }
} 

At the beginning of a new shape, the following procedures change random images and set control values to variables.

// choose random picture for different parts
private void setPictures()
{
    if (rdoBackgroundTexture.Checked)
    {
        if (chkRandomGround.Checked) picGround.BackgroundImage =
        					picsBase[myRandom.Next(picsBase.Count)];
        if (chkRandomBack.Checked) picBack.BackgroundImage =
        			picsBackground[myRandom.Next(picsBackground.Count)];
    }
    else
    {
        if (chkRandomColor.Checked)lblBackgroundColor.BackColor =
        	Color.FromArgb(myRandom.Next(255), myRandom.Next(255), myRandom.Next(255));
    }
    if (chkRandomFlower.Checked && chkFlowerObjects.Checked)
    	picFlower.BackgroundImage = picsFlowers[myRandom.Next(picsFlowers.Count)];
    if (chkRandomFruit.Checked && chkFruitObjects.Checked)
    	picFruit.BackgroundImage = picsFruits[myRandom.Next(picsFruits.Count)];
    if (chkRandomLeaf.Checked && chkLeafObjects.Checked)
    	picLeaf.BackgroundImage = picsLeaves[myRandom.Next(picsLeaves.Count)];
}

//getting data from Controls
private void getTreeInfo()
{
    myTreeInfo.totalSteps = (int)updTotalSteps.Value;

    myTreeInfo.divisionPerStep = (int)updDivisionPerStep.Value;

    myTreeInfo.startingBranch = (int)updStartingBranch.Value;
    myTreeInfo.maxSize = (float)updMaxSize.Value;
    myTreeInfo.maxAngle = (float)updMaxAngle.Value;
    myTreeInfo.maxBrokenBranches = (float)updBrokenBranches.Value;

    myTreeInfo.fixedSize = chkFixedSize.Checked;
    myTreeInfo.fixedAngle = chkFixedAngle.Checked;
    myTreeInfo.brokenBranches = chkBrokenBranches.Checked;

    myTreeInfo.leafLevel = (float)trbBranchLevel.Value;
    myTreeInfo.trunkHeight = (float)trbTrunkHeight.Value;
    myTreeInfo.widthSize = (float)trbWidthSize.Value;

    myTreeInfo.flowerPercent = (float)updFlowerObjects.Value;
    myTreeInfo.fruitPercent = (float)updFruitObjects.Value;
    myTreeInfo.leafPercent = (float)updLeafObjects.Value;

    //some corrections in data
    if (chkFixedAngle.Checked) myTreeInfo.maxAngle *= 2 / myTreeInfo.divisionPerStep;
    myTreeInfo.maxSize -= (myTreeInfo.trunkHeight / 5 - 8) * 1.6F;
    if (myTreeInfo.maxSize < 1) myTreeInfo.maxSize = 1;
}

Calculations

This is the main part; the following procedure starts the recursion method.

private void calculateTree()
{
    myTreeInfo.myStep = 0; // starting Step.
    myTreeInfo.myProgress = 0;
    myAllBranchCollection = new List<BranchCollection>();
    myAllObjectsCollection = new List<objectCollection>();
    nextBranch(picTree.Width / 2, picTree.Height *4 / 5, 90);
}

And this is the recursion part:

private void nextBranch(float startX, float startY, float startAngle)
{
    float endX, endY, newAngle, angleGrow, branchSize;
    if (!plzStopCalculation && myTreeInfo.myStep < myTreeInfo.totalSteps)
    {
        //following 6 lines are only for showing progress bar.
        if (myTreeInfo.myStep == 3)
        {
            myTreeInfo.myProgress +=	(float)(100 / 
		Math.Pow(myTreeInfo.divisionPerStep, myTreeInfo.myStep));
            if (myTreeInfo.myProgress > 100) myTreeInfo.myProgress = 100;
            progressBar.Value = (int)myTreeInfo.myProgress;
        }

        // for making broken branches, also when you reach the maximum step.
        if (myTreeInfo.brokenBranches && myTreeInfo.myStep > 2 &&
        	myRandom.NextDouble ()*100 < myTreeInfo.maxBrokenBranches +
        	(myTreeInfo.maxBrokenBranches * ((myTreeInfo.myStep * 2 -
        	myTreeInfo.totalSteps) / myTreeInfo.totalSteps)) * 0.7) return;

        myTreeInfo.myStep++;

        //different colors from root to leaves
        myPen.Color = Color.FromArgb(100, (int)(255 * 
			myTreeInfo.myStep / myTreeInfo.totalSteps), 35);

        //different width for branches from root to leaves.
        //you can replace following 2 lines with "myPen.Width=3;".
        myPen.Width = 10 * myTreeInfo.widthSize * 
		(float)Math.Pow((myTreeInfo.totalSteps - myTreeInfo.myStep), 3) / 
		(float)Math.Pow(myTreeInfo.totalSteps, 4);
        if (myPen.Width < 1) myPen.Width = 1;

        // size of current branch. you can replace following 9 lines 
        // with only "branchSize=15;".
        branchSize = (myTreeInfo.totalSteps - myTreeInfo.myStep * 
					myTreeInfo.leafLevel / 50);
        if (myTreeInfo.leafLevel >= 50) branchSize *= myTreeInfo.leafLevel / 50;
        else branchSize *= (myTreeInfo.leafLevel + 50) / 100;
        if (branchSize <= 0) branchSize = 1;
        branchSize *= (float)(picTree.Height / 
		Math.Pow(myTreeInfo.totalSteps, 1.9)) * myTreeInfo.maxSize / 80;
        if (!myTreeInfo.fixedSize) branchSize *= 
	(float)(myRandom.NextDouble() * 2 + 0.1); // only when Size is not fixed.

        // more control for height of trunk.
        if (myTreeInfo.myStep < 3) branchSize +=
        	picTree.Height / (30 * myTreeInfo.myStep) + branchSize *
        		(myTreeInfo.trunkHeight / 10 - 4) / (myTreeInfo.myStep + 1.5F) *
        		myTreeInfo.totalSteps / 15;

        // calculating end points. [* Math.PI / 180] is for changing degrees to radians.
        endX = startX + (float)Math.Cos(startAngle * Math.PI / 180) * branchSize;
        endY = startY - (float)Math.Sin(startAngle * Math.PI / 180) * branchSize;

        try
        {
            //adding branch info to collection.
            if (myTreeInfo.myStep >= myTreeInfo.startingBranch) // this "if" condition
            				//is for "Starting Branch" control.
            {
                myBranchInfo = new BranchCollection();
                myBranchInfo.startX = startX;
                myBranchInfo.startY = startY;
                myBranchInfo.endX = endX;
                myBranchInfo.endY = endY;
                myBranchInfo.myStep = myTreeInfo.myStep;
                myBranchInfo.myColor = myPen.Color;
                myBranchInfo.myWidth = myPen.Width;
                myAllBranchCollection.Add(myBranchInfo);
            }

            //adding leaves to collection
            if (chkLeafObjects.Checked)
            {
                if (myTreeInfo.myStep > myTreeInfo.totalSteps / 4) //leaves must be 
							//only on higher branches
                {
                    if (myRandom.Next(100000) * myTreeInfo.myStep < 
			Math.Pow(myTreeInfo.leafPercent, 4))// how many leaves ?
                    {
                        myObjectInfo = new objectCollection();
                        myObjectInfo.myImage = picLeaf.BackgroundImage;
                        myObjectInfo.myStep = myBranchInfo.myStep;
                        float myScale = (float)myRandom.Next(13) / 
					myObjectInfo.myImage.Width;
                        myObjectInfo.myWidth = myObjectInfo.myImage.Width * myScale;
                        myObjectInfo.myHeight = myObjectInfo.myImage.Height * myScale;
                        myObjectInfo.myX = startX - myObjectInfo.myWidth / 2;
                        myObjectInfo.myY = startY - myObjectInfo.myHeight / 2;

                        myAllObjectsCollection.Add(myObjectInfo);
                    }
                }
            }

            //adding flowers to collection
            if (chkFlowerObjects.Checked)
            {
                if (myTreeInfo.myStep > myTreeInfo.totalSteps / 2) //flowers must be 
							//only on higher branches
                {

                    if (myRandom.Next(100000) * myTreeInfo.myStep < 
			Math.Pow(myTreeInfo.flowerPercent, 3.5))// how many flowers ?
                    {
                        myObjectInfo = new objectCollection();
                        myObjectInfo.myImage = picFlower.BackgroundImage;
                        myObjectInfo.myStep = myBranchInfo.myStep;
                        float myScale = (float)myRandom.Next(12) / 
						myObjectInfo.myImage.Width;
                        myObjectInfo.myWidth = myObjectInfo.myImage.Width * myScale;
                        myObjectInfo.myHeight = myObjectInfo.myImage.Height * myScale;
                        myObjectInfo.myX = startX - myObjectInfo.myWidth / 2;
                        myObjectInfo.myY = startY - myObjectInfo.myHeight / 2;

                        myAllObjectsCollection.Add(myObjectInfo);
                    }
                }
            }

            //adding fruits to collection
            if (chkFruitObjects.Checked)
            {
                if (myTreeInfo.myStep > myTreeInfo.totalSteps * 4 / 5) //fruits must 
						// be only on higher branches
                {
                    if (myRandom.Next(100000) * myTreeInfo.myStep < 
			Math.Pow(myTreeInfo.fruitPercent, 3))// how many fruits ?
                    {
                        myObjectInfo = new objectCollection();
                        myObjectInfo.myImage = picFruit.BackgroundImage;
                        myObjectInfo.myStep = myBranchInfo.myStep;
                        float myScale = (float)myRandom.Next(15) / 
					myObjectInfo.myImage.Width;
                        myObjectInfo.myWidth = myObjectInfo.myImage.Width * myScale;
                        myObjectInfo.myHeight = myObjectInfo.myImage.Height * myScale;
                        myObjectInfo.myX = startX - myObjectInfo.myWidth / 2;
                        myObjectInfo.myY = startY - myObjectInfo.myHeight / 2;

                        myAllObjectsCollection.Add(myObjectInfo);
                    }
                }
            }
        }
        catch (Exception)
        {
            //MessageBox.Show("Error");
        }

        //recursion part.
        for (int j = 1; j <= myTreeInfo.divisionPerStep; j++)
        {
            // calculating angle for next branches.
            angleGrow = myTreeInfo.maxAngle; // range of differences
            if (myTreeInfo.fixedAngle) angleGrow = 
		angleGrow / 2 - angleGrow * (j - myTreeInfo.divisionPerStep / 2);
            else angleGrow *= (float)(myRandom.NextDouble() * 2 - 1);
            newAngle = (startAngle + angleGrow) % 360;

            nextBranch(endX, endY, newAngle);	// runs itself again.
        }

        myTreeInfo.myStep--; // go back
        Application.DoEvents();
    }
}

Drawing

In the previous version, drawing and calculation parts were in the same procedure, but here, I divided them into 2 parts. Drawing of objects is in order by their levels (steps).

 private void startPaint()
{
    try
    {
        // setting graphics
        myBitmapTree = new Bitmap(picTree.Width , picTree.Height );

        Graphics gTree=Graphics.FromImage(myBitmapTree );
        gTree.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

        // drawing landscape
        if (rdoBackgroundTexture.Checked)
        {
            gTree.DrawImage(picBack.BackgroundImage,
            	new Rectangle(0, 0, picTree.Width, picTree.Height),
            	new Rectangle(0, 0, picBack.BackgroundImage.Width,
            	picBack.BackgroundImage.Height), GraphicsUnit.Pixel);
            gTree.DrawImage(picGround.BackgroundImage,
            	new Rectangle(0, picTree.Height * 2 / 3, picTree.Width,
            	picTree.Height / 3), new Rectangle(0, 0,
            	picGround.BackgroundImage.Width,
            	picGround.BackgroundImage.Height), GraphicsUnit.Pixel);
        }
        else
        {
            gTree.FillRectangle(new SolidBrush(lblBackgroundColor.BackColor),
            	0, 0, picTree.Width, picTree.Height);

        }
        // drawing all branches and objects of the collections in order by their steps
        progressBar.Value = 0;
        for (int i = 0; i <= myTreeInfo.totalSteps ; i++)
        {
            foreach (BranchCollection myB in myAllBranchCollection)
            {
                if (i == myB.myStep-1 ) DrawTree(gTree,  myB);
            }

            picTree.Image = myBitmapTree;
            foreach (objectCollection myP in myAllObjectsCollection)
            {
                if (i == myP.myStep ) DrawPicture(gTree,  myP);
            }

            picTree.Image = myBitmapTree;
            progressBar.Value = i * 100 / myTreeInfo.totalSteps;
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

// Draw Tree Branches on the scene
private void DrawTree(Graphics gTree,  BranchCollection myB)
{
    Application.DoEvents();
    if(!plzStopDrawing) gTree.DrawLine(new Pen(myB.myColor, myB.myWidth),
    	myB.startX, myB.startY, myB.endX, myB.endY);
}

// Draw flowers, leaves and fruits on the scene
private void DrawPicture(Graphics gTree,  objectCollection myP)
{
    if (!plzStopDrawing) gTree.DrawImage
    	(myP.myImage, myP.myX, myP.myY, myP.myWidth, myP.myHeight);
}

That's it.

GUI 

The GUI is very simple.

Points of Interest

There are some predefined samples that you can access from the main menu:

You can add your own samples in the menu; add values in Click event of the item as follows:

private void mnuFlower6_Click(object sender, EventArgs e)
{
    putInfo(150, 49, 8, 0, 55, 0, 38, 1, 9800, 40, 40, 20, 50, 1,
		0, 30, 1, 1, 0, 50, 1, 0, 0, 0, 0);
} 

The values are in order by placing Controls on the form. And following the procedure sets the values on the controls.

private void putInfo(params int[] infoArray)
{
updTotalSteps.Value = infoArray[0];
updDivisionPerStep.Value = infoArray[1];
updStartingBranch.Value = infoArray[2];
chkFixedSize.Checked = Convert.ToBoolean(infoArray[3]);
updMaxSize.Value = infoArray[4];
chkFixedAngle.Checked = Convert.ToBoolean(infoArray[5]);
updMaxAngle.Value = infoArray[6];
chkBrokenBranches.Checked = Convert.ToBoolean(infoArray[7]);
updBrokenBranches.Value = (decimal)infoArray[8] / 100;
trbBranchLevel.Value = infoArray[9];
trbTrunkHeight.Value = infoArray[10];
trbWidthSize.Value = infoArray[11];

chkLeafObjects.Checked = Convert.ToBoolean(infoArray[13]);
chkFlowerObjects.Checked = Convert.ToBoolean(infoArray[17]);
chkFruitObjects.Checked = Convert.ToBoolean(infoArray[21]);
if (chkLeafObjects.Checked)
{
    if (infoArray[14] > 0) picLeaf.BackgroundImage = picsLeaves[infoArray[14] - 1];
    if (infoArray[15] > 0) updLeafObjects.Value = infoArray[15];
    chkRandomLeaf.Checked = Convert.ToBoolean(infoArray[16]);
}
if (chkFlowerObjects.Checked)
{
    if (infoArray[18] > 0) picFlower.BackgroundImage = picsFlowers[infoArray[18] - 1];
    if (infoArray[19] > 0) updFlowerObjects.Value = infoArray[19];
    chkRandomFlower.Checked = Convert.ToBoolean(infoArray[20]);
}
if (chkFruitObjects.Checked)
{
    if (infoArray[22] > 0) picFruit.BackgroundImage = picsFruits[infoArray[22] - 1];
    if (infoArray[23] > 0) updFruitObjects.Value = infoArray[23];
    chkRandomFruit.Checked = Convert.ToBoolean(infoArray[24]);
}

Also you can change textures and pictures of objects by yourself. There are several directories in the executable file's place that contain these images and you can change or add more.

History

  • First release (Feb 9, 2009)
  • Update 1 (Feb 16, 2009): Added Timer
  • Update 2 (Feb 25, 2009): Added Save options
  • Update 3 (Mar 23, 2009): Some small changes

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here