如何在 C# WinForms 上以 3d 坐标绘制点?

Neo*_*Neo 1 .net c# gdi+ winforms

我想在 WinForms 应用程序的运行时绘制点。我怎么做?

ja7*_*a72 7

简单的答案

\n

您需要实现某种投影变换,将每个Vector3坐标从 (X,Y,Z) 坐标转换为PointF(X,Y) 坐标。

\n

在每个Paint事件中,将原点移动到绘图表面的中心,并使用一些数学方法投影 3D 点,如下所示

\n
g.TranslateTransform(ClientSize.Width/2, ClientSize.Height/2);\npixel.X = scale * vector.X / (camera.Z -vector.Z);\npixel.Y = -scale * vector.Y / (camera.Z -vector.Z);\ng.DrawEllipse(Pens.Black, pixel.X-2, pixel.Y-2, 4,4);\n
Run Code Online (Sandbox Code Playgroud)\n

从事件传递的对象g在哪里。坐标为负数的原因是因为在 WinForms 中正数是向下的,而对于 3D 图形来说,违反右手规则并指向向上是有意义的。该方法在点所在的位置画一个小圆圈。如果需要,可以使用来填充圆圈。GraphicsPaintYYYDrawEllipseFillEllipse

\n
\n

详细答案

\n

在 GitHub 上有一个关于在 Winforms 中简单渲染 3D 几何的示例项目

\n

图。1

\n

该示例的其他部分您可以忽略,但我将解释我在 WinForms 控件上渲染简单 3D 对象的过程。

\n
    \n
  1. PictureBox是渲染的目标控件所在的位置。这是表格的唯一要求。放置控制以显示事物。PictureBox很方便,而且一开始就支持双缓冲。

    \n
  2. \n
  3. Camera是一个进行渲染的类。它负责以下任务。

    \n
      \n
    • 引用目标控件并处理Paint事件。
    • \n
    • 处理将 3D 点投影为像素的数学运算。
    • \n
    • 检查对象是否可见(背面剔除)。
    • \n
    • 在屏幕上渲染之前配置Graphics对象。
    • \n
    • 如果需要,处理任何鼠标事件。
    • \n
    • 定义 3D 视点属性。
    • \n
    \n

    我的课程的简化版本Camera如下。需要研究的是Project()获取几何Vector3对象和返回PointF对象的方法。

    \n
    using static SingleConstants;\n\npublic delegate void CameraPaintHandler(Camera camera, Graphics g);\n\npublic class Camera\n{\n    public event CameraPaintHandler Paint;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref="Camera" /> class.\n    /// </summary>\n    /// <param name="control">The target control to draw scene.</param>\n    /// <param name="fov">\n    /// The FOV angle (make zero for orthographic projection).\n    /// </param>\n    /// <param name="sceneSize">Size of the scene across/</param>\n    public Camera(string name, Control control, float fov, float sceneSize = 1f)\n    {\n        Name = name;\n        OnControl = control;\n        FOV = fov;\n        SceneSize = sceneSize;\n        LightPos = new Vector3(0 * sceneSize, 0 * sceneSize / 2, -sceneSize);\n        Orientation = Quaternion.Identity;\n        Target = Vector3.Zero;\n        control.Paint += (s, ev) =>\n        {\n            Paint?.Invoke(this, ev.Graphics);\n        };\n    }\n\n    public GraphicsState SetupView(Graphics g, SmoothingMode smoothing = SmoothingMode.AntiAlias)\n    {\n        var gs = g.Save();\n        g.SmoothingMode = smoothing;\n        var center = ViewCenter;\n        g.TranslateTransform(center.X, center.Y);\n        return gs;\n    }\n    public Point ViewCenter => new Point(\n        OnControl.Margin.Left + OnControl.ClientSize.Width/2,\n        OnControl.Margin.Top + OnControl.ClientSize.Height/2);\n    public string Name { get; set; }\n    public Control OnControl { get; }\n    public float SceneSize { get; set; }\n    public float FOV { get; set; }\n    public Quaternion Orientation { get; set; }\n    public Vector3 LightPos { get; set; }\n    public Vector3 Target { get; set; }\n    public int ViewSize\n    {\n        get => Math.Min(\n        OnControl.ClientSize.Width - OnControl.Margin.Left - OnControl.Margin.Right,\n        OnControl.ClientSize.Height - OnControl.Margin.Top - OnControl.Margin.Bottom);\n    }\n    public int ViewHalfSize => ViewSize/2;\n    public float Scale => ViewHalfSize/SceneSize;\n    public float DrawSize\n    {\n        get => 2 * (float)Math.Tan(FOV / 2 * Math.PI / 180);\n        set\n        {\n            FOV = 360/pi*Atan(value/2);\n        }\n    }\n    /// <summary>\n    /// Get the pixels per model unit scale.\n    /// </summary>\n    public Vector3 EyePos { get => Target + Vector3.Transform(Vector3.UnitZ * SceneSize / DrawSize, Quaternion.Inverse(Orientation)); }\n    public float EyeDistance\n    {\n        get => SceneSize/DrawSize;\n        set\n        {\n            DrawSize = SceneSize/value;\n        }\n    }\n    public Vector3 RightDir { get => Vector3.TransformNormal(Vector3.UnitX, Matrix4x4.CreateFromQuaternion(Quaternion.Inverse(Orientation))); }\n    public Vector3 UpDir { get => Vector3.TransformNormal(Vector3.UnitY, Matrix4x4.CreateFromQuaternion(Quaternion.Inverse(Orientation))); }\n    public Vector3 EyeDir { get => Vector3.TransformNormal(Vector3.UnitZ, Matrix4x4.CreateFromQuaternion(Quaternion.Inverse(Orientation))); }\n\n    public PointF Project(Vector3 node)\n    {\n        float r = 2 * (float)Math.Tan(FOV / 2 * Math.PI / 180);\n        int sz = ViewHalfSize;\n        float f = sz/r;\n        float camDist = SceneSize / r;\n        var R = Matrix4x4.CreateFromQuaternion(Orientation);\n        return Project(node, f, camDist, R);\n    }\n\n    protected PointF Project(Vector3 node, float f, float camDist, Matrix4x4 R)\n    {\n        var point = Vector3.Transform(node-Target, R);\n        return new PointF(\n            +f  * point.X / (camDist - point.Z),\n            -f  * point.Y / (camDist - point.Z));\n    }\n\n    public RectangleF Project(Bounds bounds)\n    {\n        var nodes = bounds.GetNodes();\n        var points = Project(nodes);\n        if (points.Length>0) {\n\n            RectangleF box = new RectangleF(points[0], SizeF.Empty);\n            for (int i = 1; i < points.Length; i++)\n            {\n                box.X = Math.Min(box.X, points[i].X);\n                box.Y = Math.Min(box.Y, points[i].Y);\n                box.Width = Math.Max(box.Width, points[i].X-box.X);\n                box.Height = Math.Max(box.Height, points[i].Y-box.Y);\n            }\n            return box;\n        }\n        return RectangleF.Empty;\n    }\n\n    public PointF[] Project(Triangle triangle) => Project(new[] { triangle.A, triangle.B, triangle.C });\n    public PointF[] Project(Polygon polygon) => Project(polygon.Nodes);\n    /// <summary>\n    /// Projects the specified nodes into a 2D canvas by applied the camera \n    /// orientation and projection.\n    /// </summary>\n    /// <param name="nodes">The nodes to project.</param>\n    /// <returns>A list of Gdi points</returns>\n    public PointF[] Project(Vector3[] nodes)\n    {\n        float r = 2 * (float)Math.Tan(FOV / 2 * Math.PI / 180);\n        float camDist = SceneSize / r;\n        float f = ViewHalfSize/r;\n        var R = Matrix4x4.CreateFromQuaternion(Orientation);\n\n        var points = new PointF[nodes.Length];\n        for (int i = 0; i < points.Length; i++)\n        {\n            points[i] = Project(nodes[i], f, camDist, R);\n        }\n\n        return points;\n    }\n    /// <summary>\n    /// Uses the arc-ball calculation to find the 3D point corresponding to a\n    /// particular pixel on the screen\n    /// </summary>\n    /// <param name="pixel">The pixel with origin on center of control.</param>\n    /// <param name="arcBallFactor"></param>\n    public Vector3 UnProject(Point pixel, float arcBallFactor = 1)\n    {\n        Ray ray = CastRayThroughPixel(pixel);\n        Sphere arcBall = new Sphere(Target, arcBallFactor * SceneSize/2);\n        var Rt = Matrix4x4.CreateFromQuaternion(Quaternion.Inverse(Orientation));\n        bool hit = arcBall.Hit(ray, out var t);\n        return Vector3.Transform(ray.GetPointAlong(t), Rt);\n    }\n\n    public bool IsVisible(Polygon polygon)\n        => polygon.Nodes.Length <3 || IsVisible(polygon.Nodes[0]-Target, polygon.Normal);\n    /// <summary>\n    /// Determines whether a face is visible. \n    /// </summary>\n    /// <param name="position">Any position on the face.</param>\n    /// <param name="normal">The face normal.</param>\n    public bool IsVisible(Vector3 position, Vector3 normal)\n    {\n        float \xce\xbb = Vector3.Dot(normal, position - EyePos);\n\n        return \xce\xbb < 0;\n    }\n\n}\n
    Run Code Online (Sandbox Code Playgroud)\n
  4. \n
  5. SceneVisibleObject。要绘制的对象的基类称为VisibleObject。您在上面的屏幕截图中看到的所有内容都源自VisibleObject. 这包括实体、曲线和坐标三元组。AScene只是要绘制的集合VisibleObject,它处理Paint发出的事件Camera。最后它遍历对象并发出要渲染的命令。

    \n
    public class Scene\n{\n    readonly List<VisibleObject> drawable;\n\n    public Scene()\n    {\n        drawable = new List<VisibleObject>();\n        Triad = new VisibleTriad("W");\n    }\n    [Category("Model")]\n    public VisibleObject[] Drawable => drawable.ToArray();\n    public T AddDrawing<T>(T drawing) where T : VisibleObject\n    {\n        drawable.Add(drawing);\n        return drawing;\n    }\n    [Category("Model")]\n    public VisibleTriad Triad { get; }\n    public void Render(Graphics g, Camera camera)\n    {\n        var state = camera.SetupView(g);\n\n        Triad.Render(g, camera, Pose.Identity);\n\n        foreach (var item in drawable)\n        {\n            item.Render(g, camera, Pose.Identity);\n        }\n        Gdi.Style.Clear();\n        g.Restore(state);\n\n    }\n\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    和基VisibleObject

    \n
    public abstract class VisibleObject \n{\n    public abstract void Render(Graphics g, Camera camera, Pose pose);\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    需要理解的一点是,每个位置VisibleObject都不包含在其中。这样做是为了可以在屏幕上的不同位置绘制同一对象的多个副本。

    \n
  6. \n
  7. Pose每个对象的 3D 位置和方向由类定义Pose,其中包含Vector3原点和Quaternion方向。

    \n

    主要功能是执行本地到世界或反向转换的FromLocal()和方法。ToLoca()

    \n
    public readonly struct Pose \n    : IEquatable<Pose>\n{\n    readonly (Vector3 position, Quaternion orientation) data;\n    public Pose(Quaternion orientation) : this(Vector3.Zero, orientation) { }\n    public Pose(Vector3 position) : this(position, Quaternion.Identity) { }\n    public Pose(Vector3 position, Quaternion orientation) : this()\n    {\n        data = (position, orientation);\n    }\n    public static readonly Pose Identity = new Pose(Vector3.Zero, Quaternion.Identity);\n    public static implicit operator Pose(Vector3 posiiton) => new Pose(posiiton);\n    public static implicit operator Pose(Quaternion rotation) => new Pose(rotation);\n    public Vector3 Position { get => data.position; }\n    public Quaternion Orientation { get => data.orientation; }\n\n    public Vector3 FromLocal(Vector3 position)\n        => Position +  Vector3.Transform(position, Orientation);\n    public Vector3[] FromLocal(Vector3[] positions)\n    {\n        var R = Matrix4x4.CreateFromQuaternion(Orientation);\n        Vector3[] result = new Vector3[positions.Length];\n        for (int i = 0; i < result.Length; i++)\n        {\n            result[i] = Position + Vector3.Transform(positions[i], R);\n        }\n        return result;\n    }\n    public Vector3 FromLocalDirection(Vector3 direction)\n        => Vector3.Transform(direction, Orientation);\n    public Vector3[] FromLocalDirection(Vector3[] directions)\n    {\n        var R = Matrix4x4.CreateFromQuaternion(Orientation);\n        Vector3[] result = new Vector3[directions.Length];\n        for (int i = 0; i < result.Length; i++)\n        {\n            result[i] = Vector3.TransformNormal(directions[i], R);\n        }\n        return result;\n    }\n    public Quaternion FromLocal(Quaternion orientation)\n        => Quaternion.Multiply(Orientation, orientation);\n    public Pose FromLocal(Pose local) \n        => new Pose(FromLocal(local.Position), FromLocal(local.Orientation));\n    public Vector3 ToLocal(Vector3 position)\n        => Vector3.Transform(position-Position, Quaternion.Inverse(Orientation));\n    public Vector3[] ToLocal(Vector3[] positions)\n    {\n        var R = Matrix4x4.CreateFromQuaternion(Quaternion.Inverse(Orientation));\n        Vector3[] result = new Vector3[positions.Length];\n        for (int i = 0; i < result.Length; i++)\n        {\n            result[i] = Vector3.Transform(positions[i]-Position, R);\n        }\n        return result;\n    }\n    public Vector3 ToLocalDirection(Vector3 direction)\n        => Vector3.Transform(direction, Quaternion.Inverse(Orientation));\n    public Vector3[] ToLocalDirection(Vector3[] directions)\n    {\n        var R = Matrix4x4.CreateFromQuaternion(Quaternion.Inverse(Orientation));\n        Vector3[] result = new Vector3[directions.Length];\n        for (int i = 0; i < result.Length; i++)\n        {\n            result[i] = Vector3.TransformNormal(directions[i], R);\n        }\n        return result;\n    }\n    public Quaternion ToLocal(Quaternion orientation)\n        => Quaternion.Multiply(orientation, Quaternion.Inverse(Orientation));\n    public Pose ToLocal(Pose pose)\n        => new Pose(ToLocal(pose.Position), ToLocal(pose.Orientation));\n\n    #region Algebra\n    public static Pose Add(Pose A, Pose B)\n    {\n        return new Pose(A.data.position+B.data.position, A.data.orientation+B.data.orientation);\n    }\n    public static Pose Subtract(Pose A, Pose B)\n    {\n        return new Pose(A.data.position+B.data.position, A.data.orientation-B.data.orientation);\n    }\n\n    public static Pose Scale(float factor, Pose A)\n    {\n        return new Pose(factor*A.data.position, Quaternion.Multiply(A.data.orientation, factor));\n    }\n    #endregion\n\n    #region Operators\n    public static Pose operator +(Pose a, Pose b) => Add(a, b);\n    public static Pose operator -(Pose a) => Scale(-1, a);\n    public static Pose operator -(Pose a, Pose b) => Subtract(a, b);\n    public static Pose operator *(float a, Pose b) => Scale(a, b);\n    public static Pose operator *(Pose a, float b) => Scale(b, a);\n    public static Pose operator /(Pose a, float b) => Scale(1 / b, a);\n    #endregion\n}\n
    Run Code Online (Sandbox Code Playgroud)\n
  8. \n
  9. Gdi是一个图形库,用于处理特定的子任务,例如在屏幕上绘制点、绘制标签、绘制曲线和各种形状,但具有特定的样式和处理颜色操作。此外,它还保留一个当前 Pen对象SolidFill以供重复使用,定义要在低级 Gdi 绘图操作中使用的描边和填充颜色。下面删除了一些细节:

    \n
    public static class Gdi\n{\n    /// <summary> \n    /// Converts RGB to HSL \n    /// </summary> \n    /// <remarks>Takes advantage of whats already built in to .NET by using the Color.GetHue, Color.GetSaturation and Color.GetBrightness methods</remarks> \n    /// <param name="color">A Color to convert</param> \n    /// <returns>An HSL tuple</returns> \n    public static (float H, float S, float L) GetHsl(this Color color)\n    {\n        var H = color.GetHue() / 360f;\n        var L = color.GetBrightness();\n        var S = color.GetSaturation();\n\n        return (H, S, L);\n    }\n    /// <summary>\n    /// Converts a color from HSL to RGB\n    /// </summary>\n    /// <remarks>Adapted from the algorithm in Foley and Van-Dam</remarks>\n    /// <param name="hsl">The HSL tuple</param>\n    /// <returns>A Color structure containing the equivalent RGB values</returns>\n    public static Color GetColor(this (float H, float S, float L);\n    public static Style Style { get; } = new Style();\n\n    public static void DrawPoint(this Graphics g, Color color, PointF point, float size = 4f)\n    {\n        Style.Clear();\n        Style.Fill.Color = color;\n        g.FillEllipse(Style.Fill, point.X - size/2, point.Y - size/2, size, size);\n    }\n    public static void DrawLine(this Graphics g, Color color, PointF start, PointF end, float width = 1f)\n    {\n        Style.Stroke.Color = color;\n        Style.Stroke.Width = width;\n        g.DrawLine(Style.Stroke, start, end);\n    }\n    public static void DrawArrow(this Graphics g, Color color, PointF start, PointF end, float width = 1f);\n    public static void DrawLabel(this Graphics g, Color color, PointF point, string text, ContentAlignment alignment, int offset = 2);\n    public static void DrawPath(this Graphics g, GraphicsPath path, Color color, bool fill = true);\n\n    public static void DrawCircle(this Graphics g, PointF center, float radius, Color color, bool fill = true);\n    public static void DrawEllipse(this Graphics g, PointF center, float majorAxis, float minorAxis, float angle, Color color, bool fill = true);\n    public static void DrawCurve(this Graphics g, PointF[] points, Color color, bool fill = true);\n    public static void DrawClosedCurve(this Graphics g, PointF[] points, Color color, bool fill = true);\n    public static void DrawPolygon(this Graphics g, PointF[] points, Color color, bool fill = true);\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    例如,要在屏幕上(100,30)具有颜色的位置绘制 3D 点Red,您可以从可访问Graphics g对象的绘制处理程序中发出如下代码

    \n
    PointF point = new PointF(100,30);\n// Calls extension method `Gdi.DrawPoint()`\ng.DrawPoint(Color.Red, point, 4f); \n
    Run Code Online (Sandbox Code Playgroud)\n
  10. \n
\n

使用上面的框架在屏幕上绘制由Vector3from定义的单个点,您将需要以下派生类。神奇的事情发生在使用所提供的将 3D 点投影到像素位置的方法中。此外,它还定义了一个要在该点旁边绘制的文本标签。System.NumericsVisibleObjectRender()Camera

\n
    public class VisiblePoint : VisibleObject\n    {\n        public VisiblePoint(string label, Color color, float size = 4f)\n        {\n            Label = label;\n            Color=color;\n            Size=size;\n        }\n\n        public Color Color { get; }\n        public float Size { get; }\n        public string Label { get; }\n\n        public override void Render(Graphics g, Camera camera, Pose pose)\n        {\n            var pixel = camera.Project(pose.Position);\n            g.DrawPoint(Color, pixel, Size);\n            if (!string.IsNullOrEmpty(Label))\n            {\n                g.DrawLabel(Color, pixel, Label, ContentAlignment.BottomRight);\n            }\n        }\n    }\n
Run Code Online (Sandbox Code Playgroud)\n