C# 将 3D 点投影到 2D 屏幕坐标

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/519106/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 06:31:30  来源:igfitidea点击:

Projecting a 3D point to a 2D screen coordinate

c#wpfmath3d

提问by Ash

Based on information in Chapter 7 of 3D Programming For Windows (Charles Petzold), I've attempted to write as helper function that projects a Point3D to a standard 2D Point that contains the corresponding screen coordinates (x,y):

根据3D Programming For Windows (Charles Petzold) 的第 7 章中的信息,我尝试编写将 Point3D 投影到包含相应屏幕坐标 (x,y) 的标准 2D 点的辅助函数:

public Point Point3DToScreen2D(Point3D point3D,Viewport3D viewPort )
{
    double screenX = 0d, screenY = 0d;

    // Camera is defined in XAML as:
    //        <Viewport3D.Camera>
    //             <PerspectiveCamera Position="0,0,800" LookDirection="0,0,-1" />
    //        </Viewport3D.Camera>

    PerspectiveCamera cam = viewPort.Camera as PerspectiveCamera;

    // Translate input point using camera position
    double inputX = point3D.X - cam.Position.X;
    double inputY = point3D.Y - cam.Position.Y;
    double inputZ = point3D.Z - cam.Position.Z;

    double aspectRatio = viewPort.ActualWidth / viewPort.ActualHeight;

    // Apply projection to X and Y
    screenX = inputX / (-inputZ * Math.Tan(cam.FieldOfView / 2));

    screenY = (inputY * aspectRatio) / (-inputZ * Math.Tan(cam.FieldOfView / 2));

    // Convert to screen coordinates
    screenX = screenX * viewPort.ActualWidth;

    screenY = screenY * viewPort.ActualHeight;


    // Additional, currently unused, projection scaling factors
    /*
    double xScale = 1 / Math.Tan(Math.PI * cam.FieldOfView / 360);
    double yScale = aspectRatio * xScale;

    double zFar = cam.FarPlaneDistance;
    double zNear = cam.NearPlaneDistance;

    double zScale = zFar == Double.PositiveInfinity ? -1 : zFar / (zNear - zFar);
    double zOffset = zNear * zScale;

    */

    return new Point(screenX, screenY);
}

On testing however this function returns incorrect screen coordinates (checked by comparing 2D mouse coordinates against a simple 3D shape). Due to my lack of 3D programming experience I am confused as to why.

然而,在测试时,此函数返回错误的屏幕坐标(通过将 2D 鼠标坐标与简单的 3D 形状进行比较来检查)。由于我缺乏 3D 编程经验,我对原因感到困惑。

The block commented section contains scaling calculations that may be essential, however I am not sure how, and the book continues with the MatrixCamera using XAML. Initially I just want to get a basic calculation working regardless of how inefficient it may be compared to Matrices.

块注释部分包含可能必不可少的缩放计算,但是我不确定如何,本书继续使用 XAML 使用 MatrixCamera。最初我只想让一个基本的计算工作,不管它与矩阵相比效率有多低。

Can anyone advise what needs to be added or changed?

任何人都可以建议需要添加或更改的内容吗?

采纳答案by Bill Perkins

Since Windows coordinates are z into the screen (x cross y), I would use something like

由于 Windows 坐标是 z 进入屏幕(x 交叉 y),我会使用类似的东西

screenY = viewPort.ActualHeight * (1 - screenY);

instead of

代替

screenY = screenY * viewPort.ActualHeight;

to correct screenY to accomodate Windows.

更正 screenY 以适应 Windows。

Alternately, you could use OpenGL. When you set the viewport x/y/z range, you could leave it in "native" units, and let OpenGL convert to screen coordinates.

或者,您可以使用 OpenGL。当您设置视口 x/y/z 范围时,您可以将其保留为“本机”单位,并让 OpenGL 转换为屏幕坐标。

Edit: Since your origin is the center. I would try

编辑:因为你的原点是中心。我会尝试

screenX = viewPort.ActualWidth * (screenX + 1.0) / 2.0
screenY = viewPort.ActualHeight * (1.0 - ((screenY + 1.0) / 2.0))

The screen + 1.0 converts from [-1.0, 1.0] to [0.0, 2.0]. At which point, you divide by 2.0 to get [0.0, 1.0] for the multiply. To account for Windows y being flipped from Cartesian y, you convert from [1.0, 0.0] (upper left to lower left), to [0.0, 1.0] (upper to lower) by subtracting the previous screen from 1.0. Then, you can scale to the ActualHeight.

屏幕 + 1.0 从 [-1.0, 1.0] 转换为 [0.0, 2.0]。此时,您除以 2.0 得到 [0.0, 1.0] 的乘法。为了解决从笛卡尔 y 翻转的 Windows y,您可以通过从 1.0 中减去前一个屏幕来从 [1.0, 0.0](左上到左下)转换为 [0.0, 1.0](从上到下)。然后,您可以缩放到 ActualHeight。

回答by user57697

  1. It's not clear what you are trying to achieve with aspectRatio coeff. If the point is on the edge of field of view, then it should be on the edge of screen, but if aspectRatio!=1 it isn't. Try setting aspectRatio=1 and make window square. Are the coordinates still incorrect?

  2. ActualWidthand ActualHeightseem to be half of the window size really, so screenX will be [-ActualWidth; ActualWidth], but not [0; ActualWidth]. Is that what you want?

  1. 目前尚不清楚您要使用 aspectRatio coeff 实现什么目标。如果点在视野的边缘,那么它应该在屏幕的边缘,但如果 aspectRatio!=1 它不是。尝试设置 aspectRatio=1 并使窗口变为正方形。坐标还是不正确吗?

  2. ActualWidth并且ActualHeight似乎真的是窗口大小的一半,所以 screenX 将是 [-ActualWidth; ActualWidth],但不是 [0; 实际宽度]。那是你要的吗?

回答by Scott Evernden

screenX and screenY should be getting computed relative to screen center ...

screenX 和 screenY 应该相对于屏幕中心计算......

回答by Bill Perkins

I don't see a correction for the fact that when drawing using the Windows API, the origin is in the upper left corner of the screen. I am assuming that your coordinate system is

我没有看到对使用 Windows API 绘图时原点位于屏幕左上角这一事实的更正。我假设你的坐标系是

y
|
|
+------x

Also, is your coordinate system assuming origin in the center, per Scott's question, or is it in the lower left corner?

另外,根据斯科特的问题,您的坐标系是假设原点在中心还是在左下角?

But, the Windows screen API is

但是,Windows 屏幕 API 是

+-------x
|
|
|
y

You would need the coordinate transform to go from classic Cartesian to Windows.

您需要坐标变换才能从经典笛卡尔坐标转换为 Windows。

回答by Ash

I've created and succesfully tested a working method by using the 3DUtilsCodeplex source library.

我已经使用3DUtilsCodeplex 源库创建并成功测试了一种工作方法。

The real work is performed in the TryWorldToViewportTransform() method from 3DUtils. This method will not work without it (see the above link).

真正的工作是在 3DUtils 的 TryWorldToViewportTransform() 方法中执行的。没有它,此方法将无法工作(请参阅上面的链接)。

Very useful information was also found in the article by Eric Sink: Auto-Zoom.

在 Eric Sink 的文章中也找到了非常有用的信息:自动缩放

NB. There may be more reliable/efficient approaches, if so please add them as an answer. In the meantime this is good enough for my needs.

注意。可能有更可靠/更有效的方法,如果是这样,请将它们添加为答案。与此同时,这足以满足我的需求。

    /// <summary>
    /// Takes a 3D point and returns the corresponding 2D point (X,Y) within the viewport.  
    /// Requires the 3DUtils project available at http://www.codeplex.com/Wiki/View.aspx?ProjectName=3DTools
    /// </summary>
    /// <param name="point3D">A point in 3D space</param>
    /// <param name="viewPort">An instance of Viewport3D</param>
    /// <returns>The corresponding 2D point or null if it could not be calculated</returns>
    public Point? Point3DToScreen2D(Point3D point3D, Viewport3D viewPort)
    {
        bool bOK = false;

        // We need a Viewport3DVisual but we only have a Viewport3D.
        Viewport3DVisual vpv =VisualTreeHelper.GetParent(viewPort.Children[0]) as Viewport3DVisual;

        // Get the world to viewport transform matrix
        Matrix3D m = MathUtils.TryWorldToViewportTransform(vpv, out bOK);

        if (bOK)
        {
            // Transform the 3D point to 2D
            Point3D transformedPoint = m.Transform(point3D);

            Point screen2DPoint = new Point(transformedPoint.X, transformedPoint.Y);

            return new Nullable<Point>(screen2DPoint);
        }
        else
        {
            return null; 
        }
    }

回答by Grokys

This doesn't address the algoritm in question but it may be useful for peple coming across this question (as I did).

这并没有解决有问题的算法,但它可能对遇到这个问题的人有用(就像我所做的那样)。

In .NET 3.5 you can use Visual3D.TransformToAncestor(Visual ancestor). I use this to draw a wireframe on a canvas over my 3D viewport:

在 .NET 3.5 中,您可以使用Visual3D.TransformToAncestor(Visual Founder)。我使用它在我的 3D 视口上的画布上绘制线框:

    void CompositionTarget_Rendering(object sender, EventArgs e)
    {
        UpdateWireframe();
    }

    void UpdateWireframe()
    {
        GeometryModel3D model = cube.Content as GeometryModel3D;

        canvas.Children.Clear();

        if (model != null)
        {
            GeneralTransform3DTo2D transform = cube.TransformToAncestor(viewport);
            MeshGeometry3D geometry = model.Geometry as MeshGeometry3D;

            for (int i = 0; i < geometry.TriangleIndices.Count;)
            {
                Polygon p = new Polygon();
                p.Stroke = Brushes.Blue;
                p.StrokeThickness = 0.25;
                p.Points.Add(transform.Transform(geometry.Positions[geometry.TriangleIndices[i++]]));
                p.Points.Add(transform.Transform(geometry.Positions[geometry.TriangleIndices[i++]]));
                p.Points.Add(transform.Transform(geometry.Positions[geometry.TriangleIndices[i++]]));
                canvas.Children.Add(p);
            }
        }
    }

This also takes into account any transforms on the model etc.

这也考虑了模型上的任何变换等。

See also: http://blogs.msdn.com/wpf3d/archive/2009/05/13/transforming-bounds.aspx

另见:http: //blogs.msdn.com/wpf3d/archive/2009/05/13/transforming-bounds.aspx