Park 3D






4.88/5 (34投票s)
这个简单的应用程序基于立体计算,模拟了一个包含不同深度和大小物体的3D环境。


目录
引言
我们的大脑是一个非常聪明的器官;它能做一些我们甚至不知道它是如何做到的事情。它能捕捉到我们双眼所拍摄的两幅图像之间的微小差异,并识别出物体的深度和距离。
在这个应用程序中,我使用了我们大脑所做的方法,通过创造一个不真实的3D环境来“欺骗”它。
背景
在我之前的文章(立体视觉)中,我试图解释立体系统背后的一些逻辑,以及如何通过在成对图像中水平移动物体来创建一个3D环境。现在我希望展示如何仅通过倾斜一个2D对象来创造它的3D想象。在这部分中,地面是通过倾斜一张简单的图像形成的,从而创造出美丽的景观。
超越无限!
我必须先解释一些事情。在成对的图像中,当物体在相同的位置时,我们看到它们位于场景的基准面上。实际上,这个基准面相当于现实世界中的无限远处。当我们在其中一张图像中水平改变物体的位置时,我们的大脑会将其感知为与基准面有不同的深度。例如,在平行观看模式下,将左侧图像中的物体向右移动,会使它们看起来离我们更近,移动得越多,它们就显得越近。这里有一个问题。在现实世界中,无限远位于所有其他物体之后,但在这里,我们可以让物体出现在无限远的后面!只需将左侧图像中的一个物体向左移动(而不是向右);你就会看到它在基准面的后面。你完成了一项不可能完成的任务!
鱼在无限远的后面!(平行模式)

倾斜是如何工作的?
当我们在一个图像中改变物体的位置时,它们的深度就会不同。但如果我们对一个物体的不同部分进行改变,会发生什么呢?
思考下图中的箭头

自然地,我们会看到它处于不同的深度,从远到近。箭头某些部分之间的差异量如下图所示

下图显示了在一个表面上进行的倾斜操作,就像我在此应用程序中对地面所做的那样。

我们需要3个点的位置来倾斜一个形状:左上角、右上角和左下角的点。
左上角和右上角的点与未倾斜的形状相同。左下角点的X轴值必须减去倾斜量。
Point[] skewPoints = {
new Point(0, 0 ), // upper-left point
new Point(ground.Width, 0), // upper-right point
new Point(-skewAmount, ground.Height) // lower-left point
};
当我们倾斜形状时,右下角会出现一个空白区域,所以我们需要给形状的宽度增加一些值来在场景中隐藏它。这个值等于倾斜量。

倾斜量是通过将地面的高度除以高度值(myGroundHeight/myAltitude
)计算得出的,并且必须加到倾斜和未倾斜形状的宽度上。代码如下
Point[] skewPoints = {
new Point(0, myBackHeight ), // upper-left point
new Point((int)(picLeft3D.Width+(myGroundHeight /myAltitude)),
myBackHeight), // upper-right point
new Point((int)(-myGroundHeight /myAltitude ),
picLeft3D.Height) // lower-left point
};
gRight.DrawImage(picGround.BackgroundImage, skewPoints);
gLeft.DrawImage(picGround.BackgroundImage, new Rectangle
(0, myBackHeight, (int)(picLeft3D.Width + myGroundHeight /
myAltitude ), myGroundHeight), new Rectangle(0, 0,
picGround.BackgroundImage.Width, picGround.BackgroundImage.Height),
GraphicsUnit.Pixel);
有两个控件用于选择合适的地面(视角)和高度(海拔)量。
Using the Code
算法大致如下面的流程图所示
首先,我从内部资源或硬盘中收集所有物体的图像(树、鸟、花……)并放入相应的列表中。
private List<Image> picsBack=new List<Image> ();
private List<Image> picsGround=new List<Image>() ;
private List<Image> picsBirds=new List<Image>() ;
private List<Image> picsTrees=new List<Image>() ;
private List<Image> picsAnimals=new List<Image>() ;
private List<Image> picsFlowers=new List<Image>() ;
.
.
.
private void loadPictures()
// add some pictures from internal resources
picsGround.Add(Properties.Resources.ground1);
picsBirds.Add(Properties.Resources.Bird_03_june);
picsFlowers.Add(Properties.Resources.sweetpea2);
.
.
.
//load some pictures from Hard disk (different sub directories)
loadFromHard("Ground", picsGround, null);
loadFromHard("Back", picsBack, null);
loadFromHard("Birds", picsBirds, null);
loadFromHard("Trees", picsTrees, null);
loadFromHard("Animals", picsAnimals, null);
loadFromHard("Flowers", picsFlowers , null);
}
private void loadFromHard(string myDir, List<Image> myPicList, List<Image> myPicList2)
{
DirectoryInfo myFileDir = new DirectoryInfo(Application.StartupPath + "\\" + myDir);
if (myFileDir.Exists)
{
// For each image extension (.jpg, .bmp, 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;
}
}
}
}
}
每次我们点击“运行”按钮时,都需要计算并保存物体的信息,所以我使用 myAllPicCollection
来完成这项工作。这个 List
是由一个类创建的,如下所示
private class picCollection
{
internal Image myImage = null;
internal Image myImage2 = null;
internal float myX = 0;
internal float myY = 0;
internal float myScale = 0;
internal float myDepth = 0;
}
private picCollection myPicInfo;
private List myAllPicCollection ;
现在,我们想要创建物体。下面的代码是用于创建动物的,与其他物体的创建过程类似。
首先,我从相应的集合中随机选择一个对象
myPicInfo = new picCollection();
myPicInfo.myImage = picsAnimals[myRandom.Next(picsAnimals.Count)];
myPicInfo.myImage2 = myPicInfo.myImage;
然后根据窗体上的 trbDepthAnimals
控件计算一个随机深度。
myPicInfo.myDepth = (int)(myRandom.NextDouble() *
((float)trbDepthAnimals.Value / (15 * myRandom.NextDouble())));
物体的大小取决于其深度、通用大小控件、物体类型大小控件、海拔高度及初始大小。
myPicInfo.myScale = (float)(myPicInfo.myDepth * Math.Pow(trbSizeAnimal.Value, 1.3) /
1000 * myGeneralSize / Math.Pow(myAltitude, .5) + myStartingSize/10);
X坐标是随机选择的,取决于屏幕的宽度。
myPicInfo.myX = (myRandom.Next((int)myPicInfo.myDepth, picLeft3D.Width -
(int)(myPicInfo.myImage.Width * myPicInfo.myScale) - 5) ) + 5;
但Y坐标取决于更多的因素。
myPicInfo.myY = (myBackHeight - myPicInfo.myImage.Height *
myPicInfo.myScale + myPicInfo.myDepth * myAltitude);
myAllPicCollection.Add(myPicInfo);
收集完所有信息后,我们准备绘制它们。首先,我们为左右图像定义2个位图,并绘制背景。
Bitmap leftBitmap = new Bitmap(picLeft3D.Width, picLeft3D.Height );
Graphics gLeft = Graphics.FromImage(leftBitmap);
gLeft.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Bitmap rightBitmap = new Bitmap(picRight3D.Width, picRight3D.Height );
Graphics gRight = Graphics.FromImage(rightBitmap ); ;
gRight.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
.
.
.
float myGeneralSize = .05f * trbGeneralSize.Value;
float myViewAngle = .03f * (140 - trbViewAngle.Value);//(float)Math.Pow ((110-
// (float)trbViewAngle.Value)/5,.8)/3;
float myStartingSize = .5f;
float myAltitude = (float)trbAltitude.Value / 4;
int myGroundHeight = (int)(picLeft3D.Height / myViewAngle );
int myBackHeight = picLeft3D.Height - myGroundHeight;
//by skewing the right image, we make landscape
Point[] skewPoints = {
new Point(0, myBackHeight ), // upper-left point
new Point((int)(picLeft3D.Width+(myGroundHeight /myAltitude)),
myBackHeight), // upper-right point
new Point((int)(-myGroundHeight /myAltitude ),
picLeft3D.Height) // lower-left point
};
gLeft.DrawImage(picBack.BackgroundImage, new Rectangle(0, 0, picLeft3D.Width,
myBackHeight+2), new Rectangle(0, 0, picBack.BackgroundImage.Width,
picBack.BackgroundImage.Height), GraphicsUnit.Pixel);
gRight.DrawImage(picBack.BackgroundImage, new Rectangle(0, 0, picRight3D.Width,
myBackHeight+2), new Rectangle(0, 0, picBack.BackgroundImage.Width,
picBack.BackgroundImage.Height), GraphicsUnit.Pixel);
gLeft.DrawImage(picGround.BackgroundImage, new Rectangle(0, myBackHeight,
(int)(picLeft3D.Width + myGroundHeight / myAltitude ), myGroundHeight),
new Rectangle(0, 0, picGround.BackgroundImage.Width,
picGround.BackgroundImage.Height), GraphicsUnit.Pixel);
gRight.DrawImage(picGround.BackgroundImage, skewPoints);
最后根据深度在场景上绘制所有物体。
// drawing all objects of the collection in order by depth on the scene
for (int d = 0; d < 1000; d++)
{
foreach (picCollection myP in myAllPicCollection)
{
if (d == myP.myDepth) DrawPicture(gLeft, gRight, myP);
}
}
picLeft3D.Image = leftBitmap;
picRight3D.Image = rightBitmap;
if (mnuAutoSave.Checked) ImageSvae();
}
// Draw Graphics on the scene
private void DrawPicture(Graphics gLeft, Graphics gRight, picCollection myP)
{
gLeft.DrawImage(myP.myImage, myP.myX, myP.myY,
myP.myImage.Width * myP.myScale, myP.myImage.Height * myP.myScale);
gRight.DrawImage(myP.myImage2, myP.myX - myP.myDepth,
myP.myY, myP.myImage2.Width * myP.myScale, myP.myImage2.Height * myP.myScale);
}
GUI
GUI项目如下图所示

视角 比例尺的工作方式如下
当它向左移动时
![]() |
|
|
|
当它向右移动时
![]() |
|
|
|
而海拔比例尺的工作方式是这样的
当它向左移动时
![]() |
|
|
|
当它向右移动时
![]() |
|
|
|
关注点
你可以更改或添加新的物体到环境中。在可执行文件的位置有几个目录包含了这些图片。目前,对图片没有任何控制,所以你需要将它们调整到合适的大小。

保存图片
有两种方法可以将你的场景保存到硬盘上。两种方法中,图片都将保存在可执行文件所在位置的Save目录中。
- 从菜单中选择保存选项(或按F2键)可以保存当前场景。
- 从菜单中选择自动保存选项将自动保存所有场景。

如何观看立体图片?
有多种方法可以完成这项工作,但最简单的是平行眼和交叉眼法。使用这些方法,你不需要任何额外的设备,所以可以随时随地进行。我在另一篇文章中解释了这些方法,你可以在这里尝试一下:立体视觉。
历史
- 2009年3月1日:首次发布
- 2009年3月2日:文章更新
- 2009年3月13日:文章更新