Real Tree 2






4.95/5 (50投票s)
此应用程序展示了一个绘制随机花朵和树木的简单算法。其逻辑基于分形集。


目录
引言
当您观看此软件生成的形状时,您可能会认为背后有巨大而复杂的数学公式,但事实并非如此。该程序仅使用一些简单的公式(如 SIN 和 COS)和递归方法,仅此而已。程序的所有其他部分都是为了使形状看起来更好、更自然。
在上一版本(DotNet Real Tree)中,我解释了一些数学逻辑,现在我想添加一些使其更有趣的新功能(例如真实的叶子和花朵),但我删除了程序中一些我认为可能会引起问题(例如纳粹万字符!)的部分。我还删除了其他一些控件和比例尺,以使算法更简单、更有用。
什么是分形?
分形是重复使用数学公式形成的形状。每一次,它都会执行类似的操作,并且无论您从何处开始,它都会生成相同的形状。有一些著名的公式可以完成这项工作,例如曼德布罗集或朱利亚集。
它是如何工作的?
此应用程序使用的技术与您计算机上的文件系统非常相似。当您在硬盘驱动器上工作时(例如,如图所示的 D:\ 驱动器),您会看到一些文件和目录。当您进入一个目录时,您会看到其他文件和目录,当您继续进入新目录时,您会找到更多;最终会有一个最终点,那里没有更多目录了,所以您必须返回并搜索其他地方。

以及目录结构

在此应用程序中,目录类似于树枝,文件等于树上的叶子、花朵或果实。正如您的目录可能包含不同的文件一样,您的树的树枝也可能包含不同的对象。

关于递归?
此应用程序中的我们的主函数获取当前分支的信息,并计算和绘制新分支。有一个控件可供您选择理想的分区

结果显示为 3 种不同的选择
|
|
|
每步分区 = 2 | 每步分区 = 4 | 每步分区 = 10 |
但该过程只生成树的一个级别(步),其他级别呢?这正是我们要讨论的。
简而言之,这是我们的主过程
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--;
}
我用 4 步生成分支来模拟它,您可以在此处下载 Flash 版本 此处
Using the Code
该算法很简单,类似于以下流程图

部分代码如下
变量
最初我声明了一些列表和类。objectCollection
类用于收集有关花、叶和果实的所有信息(在计算时)。BranchCollection
类用于收集分支的信息(绘制分支的项与花或果实的项不同)。还有一个 treeInfo
类用于保存树的一般信息,例如最大树大小、分支之间的角度等。
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
加载图像
然后我从硬盘和内部资源加载了一些图像,并为应用程序的不同部分(背景、水果...)选择随机图像。
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;
}
}
}
}
}
运行按钮
此按钮有 3 种不同的操作
- 开始一个新过程。
- 停止计算。
- 停止绘制。
以下过程控制此按钮
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";
}
}
在新形状开始时,以下过程会更改随机图像并将控件值设置为变量。
// 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;
}
计算
这是主要部分;以下过程启动了递归方法。
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);
}
这是递归部分
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();
}
}
绘图
在上一版本中,绘图和计算部分在同一个过程中,但在这里,我将它们分成了 2 部分。对象的绘图顺序按其级别(步)进行。
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);
}
就是这样。
GUI
GUI 非常简单。

关注点
有一些预定义的示例,您可以从主菜单访问

您可以在菜单中添加自己的示例;在项的单击事件中添加值,如下所示
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);
}
值按照控件在窗体上的放置顺序排列。以下过程将值设置到控件上。
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]);
}
您还可以自行更改对象的纹理和图片。在可执行文件所在的位置有几个目录,其中包含这些图像,您可以更改或添加更多。

历史
- 首次发布(2009 年 2 月 9 日)
- 更新 1(2009 年 2 月 16 日):添加了计时器
- 更新 2(2009 年 2 月 25 日):添加了保存选项
- 更新 3(2009 年 3 月 23 日):一些小改动