65.9K
CodeProject 正在变化。 阅读更多。
Home

波斯湾 3D 立体图像

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (74投票s)

2013年1月5日

CPOL

19分钟阅读

viewsIcon

111750

downloadIcon

5660

此应用程序使用一些简单的二维方法来创建可以使用红/青色红蓝眼镜观看的 3D 场景。

要获取可执行文件,请下载所有四个演示部分。

目录

引言

3D 系统如今非常流行,并且不断发明新的技术来观看这些图像。其中大多数技术都需要特殊的硬件,而且对某些人来说仍然很昂贵,因此红蓝眼镜足以满足我的需求。红蓝立体法是 3D 技术中最著名但也是最古老的技术之一,它使用简单的特殊眼镜。眼镜有不同的类型,但红/青色眼镜最受欢迎且易于获得,因此我们在本文中将更多地讨论这些眼镜。所有这些技术的目的是为每只眼睛制作不同的图像。随后,大脑会将它们混合成一个 3D 图像。

背景

在之前的文章,即“立体视觉”和“Park3D”中,我们使用了“自由眼”方法,平行眼和交叉眼。之后,我在 2009 年对这些系列的第三部分进行了研究,但由于一些个人问题,这篇文章从未完成。几周前,我看到了一些未完成的应用程序,并决定至少完成其中一个并在此处发布。然而,现在谈论红蓝立体图像有些过时,但对于喜欢 3D 技术或拥有红/青色眼镜的人来说,这可能仍然合适。我的最初应用程序基于波斯湾的风景,但我做了一些修改,并添加了另一个场景,即 G oles t an 国家公园,因此您可以轻松地在海洋或丛林场景之间移动并享受。

工作原理?

此应用程序遵循的原理与其他 3D 系统一样,基于两张具有细微差异的图像,分别用于每只眼睛,然后我们的大脑利用这些差异在我们的想象中制造深度。

让我们开始吧;准备两个相同大小的画布,一个用于左眼,一个用于右眼。

将天空图像放在两个画布上的相同位置,以营造无限感。

现在,将底层图像放在两者上。

为了制造深度,请倾斜右侧底层。

然后放置所有您喜欢的对象,在这个级别,您可以通过平行视图方法看到 3D 场景。首先,将两个对象放在两个画布上的相同位置,然后水平移动右侧对象以改变其深度。

如您所知,我们有 3 种主要的可见颜色:红、绿、蓝 (RGB)。在红蓝立体图像中,我们从左图像中去除绿色和蓝色,只剩下红色。然后,我们从右图像中去除红色,使其变为青色(绿色 + 蓝色 = 青色)。

如果您使用 Photoshop,对于右图像,请转到“色阶”菜单,选择“红色通道”,并将输出色阶设置为 0。

对于左图像,您需要将绿色和蓝色通道的输出色阶都设置为 0。

现在我们再次拥有 3 种颜色:左图像中的红色和右图像中的青色(绿色 + 蓝色),我们将它们放在一张图像上。在 Photoshop 中,将左图像移动到右侧,并在图层工具箱中使用“差异”选项。

红蓝立体图像已准备就绪。 

您可以在此处下载上述 Photoshop 文件。下载红蓝立体 Photoshop 示例 - 3.1 MB 

使用代码

算法如我所解释的。

现在我们一步一步来。

定义变量并设置一些值

从硬盘和内部资源加载图像

我使用了一些不同的对象,如下所示: 

public enum myObjectTypes
{
    Misc=0,
    Cloud = 1,
    Rock = 2,
    Bird = 3,
    Ship = 4,
    Animal = 5,
    Flower = 6,
    Tree = 7,
    Fish = 8,
    Insect = 9,
    Human = 10,
    Snow = 11,
    Fog = 12,
    Sky = 13,
    Ground = 14,
    Water = 15,
    _3D = 16
}

我有一个包含所有图像的集合,我已将它们保存在硬盘和内部资源的子文件夹中 

private List<Image>[] picsAll= ...  

其中 picsAll[1] 包含云,picsAll [2] 包含岩石,依此类推。 

我有一个类来保存场景计算图像的信息,如下定义:

public class picCollection
{
    internal myObjectTypes myType = 0;
    internal int myTypeNumber = 0;
    internal int myImageNumber = 0;
    internal Image myImage = null;
    internal Image myImage2 = null;
    internal float myX = 0;
    internal float myY = 0;
    internal float myScale = 0;
    internal float myDepth = 0;
    internal int myOrientation = 0;
}

以及来自上述类的集合:

List<picCollection> myAllPicCollection = new List<picCollection>() 

我还有一个集合来保存菜单项信息,如下所示:

private List<ToolStripItem> myMenuItemCollection = new List<ToolStripItem>(); 

以下数组保存颜色矩阵信息:

float[][][] picMatrix;
float[][][] anaglyphMatrix;

最初,我加载驻留在 exe 文件内部的资源,如下所示: 

private void loadPictures()
{
    // add some pictures from internal resources

    picsAll[myObjectTypes.Snow.GetHashCode()].Add(Properties.Resources.snow1);

    picsAll[myObjectTypes.Cloud.GetHashCode()].Add(Properties.Resources.cloud1);
    picsAll[myObjectTypes.Cloud.GetHashCode()].Add(Properties.Resources.cloud2);
    picsAll[myObjectTypes.Cloud.GetHashCode()].Add(Properties.Resources.cloud3);
…
}

在与您的硬盘上的 exe 文件相同的文件夹中,有几个文件夹包含外部附加图像,您也可以在其中添加自己的图像。我们可以使用以下方法加载这些文件:

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;
                }
            }
        }
    }
}

我加载了用作图像效果的颜色矩阵: 

private void setMatrices()
{

    float[][] matrixNormal = new float[][] {
        new float[] {1, 0, 0, 0, 0}, //  Red  
        new float[] {0, 1, 0, 0, 0}, //  Green
        new float[] {0, 0, 1, 0, 0}, //  Blue
        new float[] {0, 0, 0, 1, 0}, // Transparency
        new float[] {0, 0, 0, 0, 1}
    };

    float[][] matrixBW = new float[][] {
        new float[] {.34f, .34f, .34f, 0, 0}, //  Red  
        new float[] {.34f, .34f, .34f, 0, 0}, //  Green
        new float[] {.34f, .34f, .34f, 0, 0}, //  Blue
        new float[] {0, 0, 0, 1, 0}, // Transparency
        new float[] {0, 0, 0, 0, 1}
    };
}

并创建一个用于矩阵的三维数组:

picMatrix = new float[][][] { 
    matrixNormal,matrixNormal,
    matrixNormal,matrixBW,matrixBW_Contrast,matrixBW_Contrast_More,matrixBW_Contrast_Most,
    matrixBW_Brighter,matrixBW_Brighter_More,matrixBW_Brighter_Most,
    ...

显示控件并等待用户指令

控件面板最初是隐藏的;您可以单击红蓝立体图像或单击“显示控件”按钮来查看此部分。 

然后

控件分为几类。

第一类是选择海洋或丛林场景。

在第一次更新中,我向此类别添加了一个新项目;3D 图像项目,您可以在其中放入一些通过相机拍摄的真实 3D 图像并添加一些对象。目前使用此项目并不容易,但我将在下次做得更好。

通用信息类别包含影响场景中所有对象的值。例如,如果您在此处更改大小,所有类型对象的大小都将更改。 

  • 整体大小:对象的主要大小。
  • 初始大小:对象的起始大小,从无限远(背景图像)开始,并持续到所有距离。
  • 透视:深度变化的项目。较低的值意味着所有深度的尺寸几乎相等,较高的值意味着近距离的对象尺寸比远距离的成比例地更大。
  • 起始深度:此选项在此处添加,用于处理用作背景且具有不同深度的真实 3D 图像。
  • 视场角:较低的值等于观察到的地面较少,天空较多;较高的值等于观察到的地面较多,天空较少。
  • 高度:您的视线高度。
  • 挖掘:一种简单的效果,可以改变场景中所有对象在所有深度下的基本放置。它会改变所有对象的 Y 值。
  • 显示器后面的深度:深度的主要控制。如果您将其移至 0,所有对象都会在屏幕前面;当您将其移至更高的值时,屏幕后面和屏幕前面都会有一些空间。

我将在计算部分更详细地解释这些控件。

另一类是天空、地面或水的图像;它们也可以随机选择: 

在此处放置处理每种类型对象的主要控件: 

您可以通过单击以下框中的“运行新场景”按钮或从菜单栏中选择来创建新场景: 

效果组合框在此处,影响所有未选择任何效果的对象类型。

最后一类是计时器和自动运行。

我制作了一些不完美的示例,但它们将帮助您更好地理解控件。我将示例放在三个不同的菜单中(第一次更新为 4 个);

波斯湾菜单仅包含海洋场景。

戈勒斯坦公园菜单仅包含丛林场景。

特效菜单专注于夜景和冬景。

在第一次更新中,我向示例菜单添加了一个新项目;混合 3D 示例展示了如何将对象放置在相机拍摄的真实 3D 图像上。

如果用户单击示例菜单项之一,程序将加载数据并将其放置在控件上。这部分如下: 

private void runMenuItem(ToolStripItem myMenuItemSelected)
{
    mnuOut1.Text = " -- " + myMenuItemSelected.Text + " -- ";
    bool noRun = false;
    switch (myMenuItemSelected.Name)
    {
    …
   
    case "mnuPersianGulfBinocular":
        putInfo(0,                      //0=Persian Gulf   1=Golestan Park
            20, 10, 45, 15, 20,        //General Info
            20, 1, 5,                   //Depth Control
            0,
            1, 100, 28, 65, 5, 0, 100, 45, 35, 0,       //Clouds
            1, 10, 60, 15, 10, 30, 100, 55, 50, 88,     //Fog
            1, 300, 25, 0, -40, 0, 60, 0, 0, 0,         //Rocks
            1, 40, 32, 0, 0, 0, 10, 50, 40, 0,          //Birds
            1, 10, 40, 45, -10, 10, 300, 100, 80, 0,    //Ships
            1, 5, 20, 5, 0, 0, 50, 0, 0, 0,             //Fishes
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,               //Humans
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,               //Animals
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,               //Flowers
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,               //Trees
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            1, 20,              //Snow
            0, 0, 0, 0, 0, 0, 0, 0,
            0, 1,              //Sky image
            0, 0, 0, 0, 0, 0, 0, 0,
            0, 1,              //Ground image
            0, 0, 0, 0, 0, 0, 0, 0,
            0, 1,              //Water image
            0);
        break;
    …
}

项目顺序与控制面板上的控件相似,将菜单项数据放置在控件上的方法是: 

private void putInfo(params int[] infoArray)
{
    if (infoArray[0] == 0) rdoLandscapePersianGulf.Checked = true; 
         else rdoLandscapeGolestanPark.Checked = true;
    trbGeneralSize.Value = infoArray[1];
    trbInitialSize.Value = infoArray[2];
    trbViewAngle.Value = infoArray[3];
    trbAltitude.Value = infoArray[4];
    trbZoom.Value = infoArray[5];
 
    trbDepth.Value = infoArray[6];
    chkDepthRandom.Checked = Convert.ToBoolean(infoArray[7]);
    if (chkDepthRandom.Checked) updDepthRandom.Value = infoArray[8];

    chkCloudObjects.Checked = Convert.ToBoolean(infoArray[10]);
    if (chkCloudObjects.Checked)
    {
        updCloudObjects.Value = infoArray[11];
        trbSizeCloud.Value = infoArray[12];
        trbInitialCloud.Value = infoArray[13];
        trbZoomCloud.Value = infoArray[14];
        trbDepthCloudsStart.Value = infoArray[15];
        trbDepthCloudsMinimum.Value = infoArray[16];
        trbDepthClouds.Value = infoArray[17];
        trbDepthCloudsNoise.Value = infoArray[18];
        cmbCloudEffect.SelectedIndex = infoArray[19];

    }
….

选择示例项时,程序也会运行。

“运行新场景”和“刷新”的区别:“运行新场景”会重置所有对象,并更改大小、类型、深度等;而在“刷新”中,只会更改对象效果及其方向。天空、地面和水可以在两种情况下更改,如果您想在不更改的情况下保留它们,则必须取消选中每个项目的随机复选框。

根据控件随机计算新对象并将其保存在集合中

在计算部分,我们将滑块值设置为我们的变量。

private void startCalculation()
{
…         
    myGeneralSize = trbGeneralSize.Value * .05f;
    myViewAngle = (140 - trbViewAngle.Value) * .03f
    myAltitude = trbAltitude.Value / 4f;
    myGroundHeight = (int)(picLeft3D.Height / myViewAngle);
    myBackHeight = picLeft3D.Height - myGroundHeight;
…

然后,按如下方式计算每种类型的项目: 

// selecting Flowers  with different size for painting on the scene
if (chkFlowerObject.Checked && !rdoLandscapePersianGulf.Checked)
{
    if (picsAll[myObjectTypes.Flower.GetHashCode()].Count > 0)
    {
        for (int i = 1; i <= updFlowerObject.Value; i++)
        {
            try
            {
                myPicInfo = new picCollection();
                myPicInfo.myType = myObjectTypes.Flower;
                myPicInfo.myTypeNumber = myObjectTypes.Flower.GetHashCode();
                myPicInfo.myOrientation = myRandom.Next(2);
                myPicInfo.myImageNumber = myRandom.Next(picsAll[myObjectTypes.Flower.GetHashCode()].Count);
                myPicInfo.myImage = picsAll[myObjectTypes.Flower.GetHashCode()][myPicInfo.myImageNumber];
                myPicInfo.myImage2 = myPicInfo.myImage;
                myPicInfo.myDepth = (float)Math.Round(
                    (((Math.Abs(baseDepth) * (trbStartingDepthGeneral.Value + trbDepthFlowerStart.Value) / 100f)
                    + myRandom.NextDouble() * ((Math.Abs(baseDepth) * trbDepthFlowerMinimum.Value / 100f)))
                    + (myRandom.NextDouble() * ((float)trbDepthFlower.Value / 
                      (15 * myRandom.NextDouble() + (100 - trbDepthFlowerNoise.Value) / 100f))))
                    , 2);

                myPicInfo.myScale = (float)(Math.Pow(myPicInfo.myDepth, (1 + trbPerpective.Value / 
                  100f + trbZoomFlowers.Value / 100f)) * Math.Pow(trbSizeFlower.Value, 1.3) / 
                  12000f * myGeneralSize / Math.Pow(myAltitude, .4f) + 
                  (trbInitialSize.Value / 100f + trbInitialFlowers.Value / 100f));

                myPicInfo.myX = calculateX(myPicInfo, cmbXAlineFlowers.Text, 0);
                myAllPicCollection.Add(myPicInfo);
            }
            catch (ArgumentOutOfRangeException)
            {
                continue;
            }
        }
    }
}

...

计算算法的大部分对于所有对象类型都相同,但我没有为它们全部创建一个方法,以使代码更容易理解。 

首先,从集合中随机选择一个图像。

myPicInfo.myImageNumber = myRandom.Next(picsAll[myObjectTypes.Flower.GetHashCode()].Count);
myPicInfo.myImage = picsAll[myObjectTypes.Flower.GetHashCode()][myPicInfo.myImageNumber];
myPicInfo.myImage2 = myPicInfo.myImage;

正如您所见,每个对象有两个图像;这是针对您有两个立体图像在两个文件中的情况,但目前我没有在子文件夹中放置任何这类对象,并且这两个图像是相同的。

所有对象都可以水平翻转,使其出现在右侧或左侧。

myPicInfo.myOrientation = myRandom.Next(2); 

正常方向,如文件所示。

水平翻转。

我为对象使用了更大的画布尺寸。翻转它们时,放置会发生一些变化。这是因为当您刷新场景时,可以使用相同的对象在场景中进行更多更改。在这种情况下,如果狗后面有另一个对象,您可以在翻转狗的图像时看到它。 

计算对象的深度是一个复杂的过程,因为它取决于太多项目。

myPicInfo.myDepth = (float)Math.Round(
    (((Math.Abs(baseDepth) * trbDepthFlowerStart.Value / 100f)
    + myRandom.NextDouble() * ((Math.Abs(baseDepth) * trbDepthFlowerMinimum.Value / 100f)))
    + (myRandom.NextDouble() * ((float)trbDepthFlower.Value / 
      (15 * myRandom.NextDouble() + (100 - trbDepthFlowerNoise.Value) / 100f))))
    , 2);

在以下指南中,您和您的显示器的位置被考虑如下:

baseDepth 变量由名为 trbBaseDepth 的滑块控制,我在以下图像中展示了效果。

在之前的文章(立体视觉、Park3D)中,基本深度始终为 0,无限远在左右图像上的位置相同,但在这里,为了腾出更多空间,我更倾向于将天空发送到后面,并在天空和显示器屏幕之间以及显示器屏幕和用户之间留出一些空间。

还有 4 个滑块用于控制每个对象的深度,它们的作用方式如下:

计算完深度后,我们需要根据对象的深度计算其大小。自然,所有对象在离您越近时都应该越大。我为山脉、岩石和云朵设置了例外。

myPicInfo.myScale = 
  (float)(Math.Pow(myPicInfo.myDepth, (1 + trbZoom.Value / 100f + trbZoomFlowers.Value / 100f)) * 
  Math.Pow(trbSizeFlower.Value, 1.3) / 12000f * myGeneralSize / Math.Pow(myAltitude, .4f) + 
  (trbInitialSize.Value / 100f + trbInitialFlowers.Value / 100f));

有一些通用控件和一些特定于对象大小的控件,如下所示: 

“透视”滑块的零值;允许对象在所有距离上以相同大小查看。

当您使用更高的“透视”控件值时,对象在不同距离上看起来差异更大。

而且,您也可以在特定情况下使用负值: 

然后,我将对象水平放置在随机位置(X 值)。您可以选择水平分布方法,方法是更改 X 对齐组合框: 

“+B”(或 +Border)字样表示对象可以跨越边界。“总宽度”和“总宽度 + 边界”的分布如下:

而“LR”表示对象同时位于左侧和右侧。

// Calculation of horizontal Position of objects
private float calculateX(picCollection myPicInfo, string alignType, int myType)
{
    string alignTypeEdited = alignType.Trim();
    float newX = 0;

    // X alignment
    switch (alignTypeEdited)
    {
        case "Total Width":
            newX = myRandom.Next(
                ((myPicInfo.myDepth + baseDepth) < 0 ? 0 : (int)(myPicInfo.myDepth + baseDepth)),
                        myOutputWidthCorrected - (int)(myPicInfo.myImage.Width * myPicInfo.myScale) - 
                        ((myPicInfo.myDepth + baseDepth) < 0 ? 0 : 
                        -(int)(myPicInfo.myDepth + baseDepth)));
            break;
        case "Total+Border":
            newX = myRandom.Next(
                -(int)(myPicInfo.myImage.Width),
                myOutputWidthCorrected);
            break;
        case "Left 10%":
            newX = myRandom.Next(
                ((myPicInfo.myDepth + baseDepth) < 0 ? 0 : (int)(myPicInfo.myDepth + baseDepth)),
                (int)(myOutputWidthCorrected * .1f - (myPicInfo.myImage.Width * myPicInfo.myScale)));
            break;
        case "Left 10%+B":
            newX = myRandom.Next(
                -(int)(myPicInfo.myImage.Width),
                    (int)(myOutputWidthCorrected * .1f - 
                     (myPicInfo.myImage.Width * myPicInfo.myScale)));
            break;
...

对象的 Y 值自动计算取决于其深度。出于某些原因,我将此计算放在绘图的最终位置。云和鸟类也有例外,因为它们可以在天空中的任何地方,而不管它们的深度或大小。

这是大多数对象在 DrawObjects(…) 方法中放置的 Y 值计算:

// myP= myPicInfo;
myP.myY = myBackHeight 
- (float)myP.myImage.Height * myP.myScale 
+ myP.myDepth * myAltitude + myAltitude ;

这是云的特殊 Y 值计算: 

myPicInfo.myY = myRandom.Next(
        (int)(1 - myPicInfo.myImage.Height * myPicInfo.myScale),
        (int)(myBackHeight * 3 / 4f - myPicInfo.myImage.Height * myPicInfo.myScale
        - myPicInfo.myDepth * myAltitude 
        +  (trbInitialCloud.Value - 50) * 10 
        + (myPicInfo.myDepth * trbZoomCloud.Value / 2)));

正如您在上面的代码中所见,我以另一种方式使用了“初始大小”和“透视”控件来处理云。“初始大小”控件改变了远距离云的高度,“透视”控件用于近距离云的高度。

最后,我将对象添加到集合中。

myAllPicCollection.Add(myPicInfo);

在左右图像上绘制项目

此部分以 Draw3D() 方法开始。

首先,我们定义两个位图并为左图像和右图像创建 Graphics 对象。

private void Draw3D()
{
    leftBitmap = new Bitmap(picLeft3D.Width, picLeft3D.Height);
    Graphics gLeft = Graphics.FromImage(leftBitmap);
    gLeft.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
    gLeft.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
 
    rightBitmap = new Bitmap(picRight3D.Width, picRight3D.Height);
    Graphics gRight = Graphics.FromImage(rightBitmap); ;
    gRight.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
    gRight.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
…

之后,我们选择天空、地面或水的图像并在位图上绘制。有不同的方法可以完成此任务。

if (chkRandomSky.Checked)
    picSkySelected = myRandom.Next(2, picsAll[myObjectTypes.Sky.GetHashCode()].Count);
picSky.BackgroundImage = picsAll[myObjectTypes.Sky.GetHashCode()][picSkySelected];
 
if (chkRandomGround.Checked) picGroundSelected = 
    myRandom.Next(2, picsAll[myObjectTypes.Ground.GetHashCode()].Count);
picGround.BackgroundImage = picsAll[myObjectTypes.Ground.GetHashCode()][picGroundSelected];
 
if (chkRandomWater.Checked) picWaterSelected = 
    myRandom.Next(1, picsAll[myObjectTypes.Water.GetHashCode()].Count);
picWater.BackgroundImage = picsAll[myObjectTypes.Water.GetHashCode()][picWaterSelected];
 
...
 
private void DrawSky(Graphics gLeftx, Graphics gRightx)
{
    // Create ImageAttributes and Set Color Matrix
    ImageAttributes imgAtt = new ImageAttributes();
    // if you select a special effect for sky, system uses it, otherwise system uses general effect
    ColorMatrix myMatrix = new ColorMatrix(picMatrix[(cmbSkyEffect.SelectedIndex > 0 ? 
                cmbSkyEffect.SelectedIndex : cmbGeneralEffect.SelectedIndex)]);
    imgAtt.SetColorMatrix(myMatrix, ColorMatrixFlag.Default, ColorAdjustType.Default);
 
    gLeftx.DrawImage(picsAll[myObjectTypes.Sky.GetHashCode()][picSkySelected], 
      new Rectangle(baseDepth, 0, picLeft3D.Width, myBackHeight), 0, 0, 
      picsAll[myObjectTypes.Sky.GetHashCode()][picSkySelected].Width, 
      picsAll[myObjectTypes.Sky.GetHashCode()][picSkySelected].Height, 
      GraphicsUnit.Pixel, imgAtt);
    gRightx.DrawImage(picsAll[myObjectTypes.Sky.GetHashCode()][picSkySelected], 
      new Rectangle(0, 0, picRight3D.Width, myBackHeight), 0, 0, 
      picsAll[myObjectTypes.Sky.GetHashCode()][picSkySelected].Width, 
      picsAll[myObjectTypes.Sky.GetHashCode()][picSkySelected].Height, 
      GraphicsUnit.Pixel, imgAtt);
} 
 
// Draw Ground or Water on the scene
private void DrawBase(Graphics gLeftx, Graphics gRightx)
{
    Image myBaseImage;
    ColorMatrix myMatrix;
 
    // Persian Gulf or Golestan Park selected?
    if (rdoLandscapePersianGulf.Checked)
    {
        // Persian Gulf
        myBaseImage = picsAll[myObjectTypes.Water.GetHashCode()][picWaterSelected];
        // if you select a special effect for  water,
        // system uses it, otherwise system uses general effect
        myMatrix = new ColorMatrix(picMatrix[(cmbWaterEffect.SelectedIndex > 0 ? 
               cmbWaterEffect.SelectedIndex : cmbGeneralEffect.SelectedIndex)]);
 
    }
    else
    {
        // Golestan Park
        myBaseImage = picsAll[myObjectTypes.Ground.GetHashCode()][picGroundSelected];
        // if you select a special effect for  ground,
        // system uses it, otherwie system uses general effect
        myMatrix = new ColorMatrix(picMatrix[(cmbGroundEffect.SelectedIndex > 0 ? 
          cmbGroundEffect.SelectedIndex : cmbGeneralEffect.SelectedIndex)]);
 
    }
 
    // Create ImageAttributes and Set Color Matrix
    ImageAttributes imgAtt = new ImageAttributes();
    imgAtt.SetColorMatrix(myMatrix, ColorMatrixFlag.Default, ColorAdjustType.Default);
 
    //by skewing one of the images, we make landscape depth
    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 
            };
 
    gLeftx.DrawImage(myBaseImage, new Rectangle(baseDepth, myBackHeight, 
      (int)(picLeft3D.Width + myGroundHeight / myAltitude), myGroundHeight), 0, 0, 
      myBaseImage.Width, myBaseImage.Height, GraphicsUnit.Pixel, imgAtt);
    gRightx.DrawImage(myBaseImage, skewPoints, new Rectangle(0, 0, 
      myBaseImage.Width, myBaseImage.Height), GraphicsUnit.Pixel, imgAtt);
}

然后,我们创建一个循环,将集合中的所有对象绘制在左侧和右侧 Graphics 对象上。对象将按照它们的深度顺序放置在场景中。

stbInfo = new StringBuilder();
// drawing all objects of the collection in order by depth on the scene
for (int d = 0; d <= maxDepth * 100; d++)
{
    foreach (picCollection myP in myAllPicCollection)
    {
        if (d == (int)(myP.myDepth * 100))
        {
            try
            {
                {
                    DrawObjects(gLeft, gRight, myP);
 
                    //append data to save on file
                    stbInfo.Append(stbPicInfo(myP).ToString());
                }
            }
            catch (Exception e) { Debug.WriteLine("error 234: " + e.Message); }
        }
    }
}

在上面的循环中,我还将所有对象的信息收集到一个字符串(stbinfo)中,以便将来保存到文件中。

DrawObjects(…) 方法中,我们为每种对象类型添加颜色效果。然后,选择方向,计算 Y 位置,最后在 Graphics 对象上绘制左图像和右图像。

private void DrawObjects(Graphics gLeftx, Graphics gRightx, picCollection myP)
{
    ColorMatrix myMatrix = new ColorMatrix();

    // you can deactivate some objects in refresh time.
    // also make special effects for groups of objects
    switch (myP.myType)
    {
        case myObjectTypes.Cloud:
            if (!chkCloudObjects.Checked) return;
            myMatrix = new ColorMatrix(picMatrix[(cmbCloudEffect.SelectedIndex > 0 ? 
              cmbCloudEffect.SelectedIndex : cmbGeneralEffect.SelectedIndex)]);
            break;
….
 
    }
 
    Bitmap tempImage = new Bitmap(myP.myImage);
    Bitmap tempImage2 = new Bitmap(myP.myImage2);
 
    switch (myObjectsOrientation)
    {
        case 0:
        case 1:
            if (myP.myOrientation == 1)
            {
                tempImage.RotateFlip(RotateFlipType.RotateNoneFlipX);
                tempImage2.RotateFlip(RotateFlipType.RotateNoneFlipX);
            }
            break;
        case 2:
            if (myP.myOrientation == 0)
            {
                tempImage.RotateFlip(RotateFlipType.RotateNoneFlipX);
                tempImage2.RotateFlip(RotateFlipType.RotateNoneFlipX);
            }
            break;
        case 3:
            if (myRandom.Next(2) == 0)
            {
                tempImage.RotateFlip(RotateFlipType.RotateNoneFlipX);
                tempImage2.RotateFlip(RotateFlipType.RotateNoneFlipX);
            }
 
            break;
        case 4:
            // 
            break;
        case 5:
            {
                tempImage.RotateFlip(RotateFlipType.RotateNoneFlipX);
                tempImage2.RotateFlip(RotateFlipType.RotateNoneFlipX);
            }
            break;
    }
 
    // Create ImageAttributes and Set Color Matrix
    ImageAttributes imgAtt = new ImageAttributes();
    imgAtt.SetColorMatrix(myMatrix, ColorMatrixFlag.Default, ColorAdjustType.Default);
 
 
    if (myP.myType != myObjectTypes.Bird &&  myP.myType != myObjectTypes.Cloud)
        myP.myY = myBackHeight 
        - (float)myP.myImage.Height * myP.myScale 
        + myP.myDepth * myAltitude + myAltitude ;
 
    int mynewY =(int) myP.myY + trbdigging.Value;
 
    gLeftx.DrawImage(tempImage, new Rectangle((int)myP.myX, (int)(mynewY  ), 
      (int)(tempImage.Width * myP.myScale), (int)(tempImage.Height * myP.myScale)), 
      0, 0, tempImage.Width, tempImage.Height, GraphicsUnit.Pixel, imgAtt);
    gRightx.DrawImage(tempImage2, new Rectangle((int)(myP.myX - myP.myDepth) - 
      baseDepth, (int)(mynewY  ), (int)(tempImage2.Width * myP.myScale), 
      (int)(tempImage2.Height * myP.myScale)), 0, 0, tempImage2.Width, 
      tempImage2.Height, GraphicsUnit.Pixel, imgAtt);
    tempImage.Dispose();
    tempImage2.Dispose();

您可以通过单击工具菜单中的“显示左图像”或“显示右图像”来查看左右图像的结果。 

您还可以通过在工具菜单中选择“保存左右图像”来保存这些左右图像。

在此级别的结果是类似于以下图像的左右图像。

这是左图像。

这是右图像。

从绘制的图像创建红蓝立体图像

在此部分,我们从左图像中删除所有绿色和蓝色颜色数据,因此它只有红色颜色数据。为此,使用了以下颜色矩阵:

float[][] matrixRedChannel = new float[][] {
    new float[] {1, 0, 0, 0, 0}, //  Red  
    new float[] {0, 0, 0, 0, 0}, //  Green
    new float[] {0, 0, 0, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

每个像素将在 RGB (0,0,0) 到 RGB(255,0,0) 之间,左图像将变为红色: 

同样,我们从右图像中删除所有红色颜色数据,因此它只有青色颜色数据。为此,使用了以下颜色矩阵:

float[][] matrixCyanChannel = new float[][] {
    new float[] {0, 0, 0, 0, 0}, //  Red  
    new float[] {0, 1, 0, 0, 0}, //  Green
    new float[] {0, 0, 1, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

每个像素将在 RGB (0, 0, 0) 到 RGB (0,255,255) 之间,右图像将变为青色。

如果您想获得您的工作的红色和青色图像,请在工具菜单中单击“保存红青色图像”。

现在我们将这两张图像合并成一张图像。为此,我们将红青色图像中每个像素的颜色信息添加到新图像中。颜色混合结果如下表所示:

unsafe 
{
    BitmapData bmpData = bmpOutputLeft.LockBits(myRectangle, 
       ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    Color myPixel;


    for (int y = 0; y < bmpData.Height; y++)
    {
        byte* myRow = (byte*)bmpData.Scan0 + (y * bmpData.Stride);
        for (int x = 0, p = 0; x < bmpData.Width; p += 3, x++)
        {
            myPixel = bmpOutputRight.GetPixel(x, y);
            myRow[p] += myPixel.B;
            myRow[p + 1] += myPixel.G;
            myRow[p + 2] += myPixel.R;

        }
    }
    bmpOutputLeft.UnlockBits(bmpData);
}

结果就是我们的最终图像。 

显示最终图像

颜色矩阵

setMatrices() 方法中预定义了许多颜色效果。红蓝立体部分有两个必需的方法(青色和红色矩阵),所有其他矩阵仅供娱乐。

private void setMatrices()
{
    float[][] matrixNormal = new float[][] {
        new float[] {1, 0, 0, 0, 0}, //  Red  
        new float[] {0, 1, 0, 0, 0}, //  Green
        new float[] {0, 0, 1, 0, 0}, //  Blue
        new float[] {0, 0, 0, 1, 0}, // Transparency
        new float[] {0, 0, 0, 0, 1}
    };
 
 
 
    float[][] matrixBW = new float[][] {
        new float[] {.34f, .34f, .34f, 0, 0}, //  Red  
        new float[] {.34f, .34f, .34f, 0, 0}, //  Green
        new float[] {.34f, .34f, .34f, 0, 0}, //  Blue
        new float[] {0, 0, 0, 1, 0}, // Transparency
        new float[] {0, 0, 0, 0, 1}
    };
    ...

在同一场景上显示了一些颜色矩阵的作用。为此,我选择了“戈勒斯坦公园”菜单中的“危险区域”,在第一张图像后,我更改了通用效果并选择了刷新(相同方向): 

这是危险区域的基础图像,颜色矩阵是:

float[][] matrixNormal = new float[][] {
    new float[] {1, 0, 0, 0, 0}, //  Red  
    new float[] {0, 1, 0, 0, 0}, //  Green
    new float[] {0, 0, 1, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

半色效果

float[][] matrixHalfColor = new float[][] {
    new float[] {.7f, .2f, .2f, 0, 0}, //  Red  
    new float[] {.2f, .7f, .2f, 0, 0}, //  Green
    new float[] {.2f, .2f, .7f, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

黑白效果

float[][] matrixBW = new float[][] {
    new float[] {.34f, .34f, .34f, 0, 0}, //  Red  
    new float[] {.34f, .34f, .34f, 0, 0}, //  Green
    new float[] {.34f, .34f, .34f, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

黑白效果,对比度更高。

float[][] matrixBW_Contrast_More = new float[][] {
    new float[] {.5f, .5f,.5f, 0, 0}, //  Red  
    new float[] {.5f, .5f,.5f, 0, 0}, //  Green
    new float[] {.5f, .5f, .5f, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

色彩增强效果

float[][] matrixColorize_More = new float[][] {
    new float[] {1.3f, -.1f, -.1f, 0, 0}, //  Red  
    new float[] {-.1f, 1.3f, -.1f, 0, 0}, //  Green
    new float[] {-.1f, -.1f, 1.3f, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

全色彩效果

float[][] matrixColorize_Full = new float[][] {
    new float[] {1.8f, -.3f, -.3f, 0, 0}, //  Red  
    new float[] {-.3f, 1.8f, -.3f, 0, 0}, //  Green
    new float[] {-.3f, -.3f, 1.8f, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

更暗效果+

float[][] matrixDarker_MoreP = new float[][] {
    new float[] {.8f, -.1f, -.1f, 0, 0}, //  Red  
    new float[] {-.1f, .8f, -.1f, 0, 0}, //  Green
    new float[] {-.1f, -.1f, .8f, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {-.2f, -.2f, -.2f, 0, 1}
};

仅红色效果

float[][] matrixRedOnly = new float[][] {
    new float[] {1,0, 0,  0,0}, //  Red  
    new float[] {.34f, .34f, .34f, 0, 0}, //  Green
    new float[] {.34f, .34f, .34f, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

金色效果

float[][] matrixGolden = new float[][] {
    new float[] {1.2f, 0, 0, 0, 0}, //  Red  
    new float[] {0, 1, 0, 0, 0}, //  Green
    new float[] {1, 1, 1, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, 0, 1}
};

更多透明效果

float[][] matrixTransparentMore = new float[][] {
    new float[] {1, 0, 0, 0, 0}, //  Red  
    new float[] {0, 1, 0, 0, 0}, //  Green
    new float[] {0, 0, 1, 0, 0}, //  Blue
    new float[] {0, 0, 0, 1, 0}, // Transparency
    new float[] {0, 0, 0, -.5f, 1}
};

在最后一张图像中,我将通用滤镜设置为无,并将对象的特定滤镜设置为:

Clouds = Darker More
Rocks= Golden
Animal = Darker More
Flowers= Colorize More+
Tree = Darker Full
Sky: Darker Most
Ground: Darker More+
Fog and Flying Birds = no Effect

将数据和图像保存到硬盘

程序会自动将所有对象信息保存在一个位于 Save/Data 文件夹中的文件中。这在某种程度上是为了“加载”选项的前向兼容性。setDataForSave()stbPicInfo(…) 方法收集信息,saveData(…) 方法将它们写入硬盘。

程序还可以自动或手动保存图像。文件菜单中有一些菜单选项,菜单栏上还有一个自动保存图标。

保存图像的默认格式是 PNG,它具有非常好的质量但需要更多磁盘空间。您也可以使用 JPG 格式,它需要的磁盘空间非常少(几乎是 PNG 的 10 倍),质量中等。(请参阅工具菜单)

您还可以保存左右对或红青色图像以备某些用途。所有这些选项都在工具菜单中。ImageSave()ImageSave2(…) 方法用于此任务。

雪花动画非常简单,我用它来达到三个目的:

  1. 显示显示器屏幕后面的对象层和屏幕前面的对象层。
  2. 显示底层(地面或水)与显示器屏幕相遇的地方。
  3. 冬季。

对于雪花动画,我使用了一种非常简单的方法。它只在一个图层上工作,我把它放在左右图像完全重叠的地方,所以不需要改变颜色或制作红蓝立体对象。每个雪花都是一个 Windows 窗体;这个窗体包含一个雪图像和一个计时器来控制从上到下的移动。

我用来制作雪的代码如下:

if (mnuShowSnow.Checked && chkSnowObjects.Checked)
{
    if (picsAll[myObjectTypes.Snow.GetHashCode()].Count > 0)
    {
 
        for (int i = 1; i < trbSnow.Value; i++)
        {
            try
            {
                frmObject frmSnow = new frmObject();
 
                frmSnow.MyBasePosition = this.Location;
                frmSnow.MyBaseSize = this.Size;
                frmSnow.borderLeft = borderLeft;
                frmSnow.borderTop = borderTop;
                frmSnow.borderMenu = mainMenu.Height;
                frmSnow.isFullScreen = isFullScreen;
                frmSnow.myDepth = myRandom.Next(40 / myRandom.Next(1, 4));
                frmSnow.tmrMain.Interval = myRandom.Next(60 - frmSnow.myDepth, 70 - frmSnow.myDepth);
                frmSnow.MyDirectionVertical = true;
                frmSnow.MyMovementType = 1;
                frmSnow.MyMoveSpeed = 1 + frmSnow.myDepth / 20;
                frmSnow.myMoveDirection = myDirection.Positive;
                //(myRandom.Next(2) == 0 ? myDirection.Positive : myDirection.Negative);

                frmSnow.MyDirectionLimitMin = myRandom.Next(-500, -100);
                frmSnow.MyDirectionLimitMax = picAnaglyph.Height + myRandom.Next(100, 500);
                frmSnow.mySurface = mySnowSurface;
 
                frmSnow.myPosition = new Point(myRandom.Next(this.Width), this.Height + 100);
 
                frmSnow.myType = myObjectTypes.Snow;
                frmSnow.btmR = (Bitmap)picsAll[myObjectTypes.Snow.GetHashCode()][
                  myRandom.Next(picsAll[myObjectTypes.Snow.GetHashCode()].Count)];
                frmSnow.btmL = null;
 
                mySnowCollection.Add(frmSnow);
            }
            catch (ArgumentOutOfRangeException)
            {
                continue;
            }
        }
    }
}

我将所有这些雪花窗体放在一个集合中,这样在我想停止或激活它们时就更容易了。

我像这样处理它们:

foreach (frmObject mySnow in mySnowCollection) mySnow.Show(); 

或者这个:

foreach (frmObject mySnow in mySnowCollection) mySnow.MyBaseSize = this.Size; 

您可以通过单击菜单栏上的雪花图标或单击工具 -> 显示雪来禁用整个雪花动画。 

关于雪的速度、形状和其他错误,请轻松对待,它只是一个运行时效果,不会与图像一起保存。

检查自动运行计时器、用户指令、编辑图像...

如果您想在显示器后面休息并观看 3D 图像,可以激活自动运行计时器。 

在这种情况下,“全屏模式”是一个不错的选择。 

如果您选中“刷新次数”复选框,它将创建一些具有相同对象但通过改变天空、底层(地面或水)和对象方向的场景。

编辑工作室

该程序完全使用随机对象、随机深度和大小进行工作。因此,大多数情况下,您会遇到同一张人(或其他主要对象)的图像在场景中出现多次或对象过于靠近等缺点。所以,我决定添加一个非常简单的编辑窗口,通过它可以更改、删除或调整对象大小。编辑窗口的用户友好性不是很好,但它将在大多数情况下有所帮助。

最初,我从菜单中选择一个示例,在这种情况下,我选择了示例 -> 特效 -> 波斯寒冷之路。

在这张图像中,我首先讨厌的是我看到了相同的两辆车。所以我想删除一辆。从编辑 -> 编辑面板菜单中选择编辑窗口。

窗口如下所示:

在数据网格中,您将看到场景中的所有对象。向下滚动以找到车辆。 

当您单击数据网格中的对象时,您将看到它在真实位置和方向上的放置。现在,我想将这辆车更改为其他东西,所以双击数据网格中的图像。 

出现了一个新的较小窗口,您可以滚动以找到正确的对象。在这种情况下,我选择孔雀。当我返回到上一个窗口并选择确定时,需要几秒钟才能重新绘制场景。 

哦,那里还有另一辆车。我再次编辑并用雪人替换了汽车。 

您还可以通过更改值来更改每个对象的 X 位置、深度和比例(大小)。Y 值的更改仅适用于云和飞鸟,对于其他对象,Y 值根据深度自动计算。新结果很好,但我决定将人物更改为其他东西。所以我再次编辑了图像。

我希望上帝不要对我们做这样的事情。

更新说明

现在您可以单击对象来选择它们。如果单击区域中有多个对象,它们将在下面的数据网格中显示,您可以选择其中一个。

这是一个示例编辑窗口。

如果您单击船,船及其后面的船将被选中,并在下面的数据网格中显示,其中一个将被选中。

然后您可以将包含对象的窗口拖动到您想要的位置。

正如我之前解释的,垂直位置 (Y) 是根据对象的深度计算的,所以当您移动对象时,只有水平位置会被保存。但对于云、鸟类和编辑过的对象等某些对象,您必须能够垂直移动它们;所以有一个选项可以选择您移动对象时会发生什么。

  • “仅保存 X”选项是默认选项,即使您更改了对象的垂直位置,它也不会被保存,只有水平更改会被保存。
  • “保存 X,Y”将允许您同时保存水平和垂直更改。
  • “显示确认”将在您单击“是”按钮时保存更改。

您还可以将图像替换为硬盘上的另一个图像。右键单击数据网格视图中的对象。

玩得开心 

我希望您能从这个程序中获得一些乐趣。微笑 | <img src= 

对象类型

我将对象分为几类,例如鸟类、动物、人类、花朵……每种类型都有一个特殊的文件夹来存放图像。您可以添加自己的图像到这些文件夹,但您必须考虑对象的高度。例如,如果您想将您的个人家庭照片添加到人类文件夹,在裁剪了人物照片并使周围空间透明后,请按如下方式调整大小:如果一个人大约 180 厘米,则图像高度应约为 900 像素。我的公式是:180 / 2 -> 结果 * 10。

还有一个杂项文件夹,您可以添加任何图片而无需注意其大小或类型。此文件夹仅在编辑模式下可用,并且其中包含的图像不会在自动运行中使用。

对象最佳格式是带有 alpha 通道的 PNG(Photoshop 中的 PNG-24),但这种格式的图像文件大小巨大,因此在某些情况下您可以选择 PNG-8 或 GIF 格式。

场景质量

我有一些不同质量的红/青色眼镜。建议选择高质量的眼镜,并使用高分辨率的高对比度显示器(我使用三星 22 英寸全高清 LED 显示器,它们非常适合这项工作)。

最近,我在市场上发现了两种类型的眼镜。左边的便宜一些,但质量更好!

兴趣点

在后续更新中添加加载选项会很好。编辑窗口应该更具交互性和用户友好性,并且您应该能够在运行时添加对象。另外,能够处理其他类型的眼镜也很好。所有这些都取决于这篇文章的受欢迎程度,这篇文章仍然是一种老式的文章,有待观察。

历史

  • 首次上传:2013 年 1 月 5 日。
  • 更新:2013 年 1 月 12 日。不同的输出分辨率,能够使用 3D 图像作为背景和一些新的效果。
  • 更新:2013 年 1 月 24 日。“编辑”面板的一些改进。
© . All rights reserved.