简体   繁体   English

如何在 C# WinForms 上的 3d 坐标中绘制一个点?

[英]How to draw a point in 3d coordinates on C# WinForms?

I want to have points drawn in runtime in a WinForms application.我想在 WinForms 应用程序的运行时绘制点。 How do I do that?我怎么做?

The simple answer简单的答案

You need to implement some kind of projective transformation to convert each Vector3 from (X,Y,Z) coordinates to PointF with (X,Y) coordinates.您需要实现某种投影变换以将每个Vector3从 (X,Y,Z) 坐标转换为具有 (X,Y) 坐标的PointF

On each Paint event, move the origin to the center of the drawing surface and project the 3D points with some math as follows在每个Paint事件中,将原点移动到绘图表面的中心,并使用一些数学投影 3D 点,如下所示

g.TranslateTransform(ClientSize.Width/2, ClientSize.Height/2);
pixel.X = scale * vector.X / (camera.Z -vector.Z);
pixel.Y = -scale * vector.Y / (camera.Z -vector.Z);
g.DrawEllipse(Pens.Black, pixel.X-2, pixel.Y-2, 4,4);

Where g is the Graphics object passed from the Paint event.其中g是从Paint事件传递的Graphics对象。 The reason for the negative for the Y coordinate is because in WinForms positive Y is downwards, and for 3D graphics it makes sense to objey the right hand rules and have Y pointing upwards. Y坐标为负的原因是因为在 WinForms 中正Y是向下的,对于 3D 图形,遵循右手规则并使Y向上是有意义的。 The DrawEllipse method draws a little circle where the point is. DrawEllipse方法在点所在的位置绘制一个小圆圈。 Use FillEllipse to fill in the circle instead if you want.如果需要,请使用FillEllipse填充圆圈。


The detailed answer详细解答

I have a sample project on GitHub on simple rendering of 3D geometry in Winforms.在 GitHub 上有一个关于在 Winforms 中简单渲染 3D 几何的示例项目

图。1

There are other parts to the sample you can ignore, but I will explain the process I came up to render simple 3D objects on a WinForms control.您可以忽略该示例的其他部分,但我将解释在 WinForms 控件上呈现简单 3D 对象的过程。

  1. PictureBox is where the target control for the rendering. PictureBox是用于渲染的目标控件所在的位置。 This is the only requirement for the form.这是表单的唯一要求。 To have Control placed for things to show on.控制要显示的东西。 PictureBox is convenient, and it supports double buffering on the get-go. PictureBox很方便,它支持一开始就进行双缓冲。

  2. Camera is a class that does the rendering. Camera是一个进行渲染的类。 It is responsible for the following tasks.它负责以下任务。

    • Reference to the target control and handling the Paint event.引用目标控件并处理Paint事件。
    • Handling the math for projecting 3D points into pixels.处理将 3D 点投影到像素的数学运算。
    • Checks if an object is visible (backface culling).检查对象是否可见(背面剔除)。
    • Configure the Graphics object before rendering on screen.在屏幕上渲染之前配置Graphics对象。
    • Handling any mouse events if needed.如果需要,处理任何鼠标事件。
    • Define the 3D viewpoint properties.定义 3D 视点属性。

    A simplified version of my Camera class is below.下面是我的Camera类的简化版本。 The things to look into is the Project() methods that take up geometry Vector3 objects and return PointF objects.要研究的是占用几何Vector3对象并返回PointF对象的Project()方法。

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

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

    and the base VisibleObject class和基础VisibleObject

    public abstract class VisibleObject { public abstract void Render(Graphics g, Camera camera, Pose pose); }

    One point to understand is that the location of each VisibleObject is not contained within.要理解的一点是每个VisibleObject的位置都不包含在其中。 This is done so multiple copies of the same object can be drawn at various locations on the screen.这样做是为了可以在屏幕上的不同位置绘制同一对象的多个副本。

  4. Pose The 3D position and orientation of each object is defined with the Pose class, which contains a Vector3 origin, and a Quaternion orientation. Pose每个对象的 3D 位置和方向由Pose类定义,该类包含一个Vector3原点和一个Quaternion方向。

    The main functionality is in the FromLocal() and ToLoca() methods that do the local to world or reverse transformations.主要功能在FromLocal()ToLoca()方法中,它们执行本地到世界或反向转换。

     public readonly struct Pose : IEquatable<Pose> { readonly (Vector3 position, Quaternion orientation) data; public Pose(Quaternion orientation) : this(Vector3.Zero, orientation) { } public Pose(Vector3 position) : this(position, Quaternion.Identity) { } public Pose(Vector3 position, Quaternion orientation) : this() { data = (position, orientation); } public static readonly Pose Identity = new Pose(Vector3.Zero, Quaternion.Identity); public static implicit operator Pose(Vector3 posiiton) => new Pose(posiiton); public static implicit operator Pose(Quaternion rotation) => new Pose(rotation); public Vector3 Position { get => data.position; } public Quaternion Orientation { get => data.orientation; } public Vector3 FromLocal(Vector3 position) => Position + Vector3.Transform(position, Orientation); public Vector3[] FromLocal(Vector3[] positions) { var R = Matrix4x4.CreateFromQuaternion(Orientation); Vector3[] result = new Vector3[positions.Length]; for (int i = 0; i < result.Length; i++) { result[i] = Position + Vector3.Transform(positions[i], R); } return result; } public Vector3 FromLocalDirection(Vector3 direction) => Vector3.Transform(direction, Orientation); public Vector3[] FromLocalDirection(Vector3[] directions) { var R = Matrix4x4.CreateFromQuaternion(Orientation); Vector3[] result = new Vector3[directions.Length]; for (int i = 0; i < result.Length; i++) { result[i] = Vector3.TransformNormal(directions[i], R); } return result; } public Quaternion FromLocal(Quaternion orientation) => Quaternion.Multiply(Orientation, orientation); public Pose FromLocal(Pose local) => new Pose(FromLocal(local.Position), FromLocal(local.Orientation)); public Vector3 ToLocal(Vector3 position) => Vector3.Transform(position-Position, Quaternion.Inverse(Orientation)); public Vector3[] ToLocal(Vector3[] positions) { var R = Matrix4x4.CreateFromQuaternion(Quaternion.Inverse(Orientation)); Vector3[] result = new Vector3[positions.Length]; for (int i = 0; i < result.Length; i++) { result[i] = Vector3.Transform(positions[i]-Position, R); } return result; } public Vector3 ToLocalDirection(Vector3 direction) => Vector3.Transform(direction, Quaternion.Inverse(Orientation)); public Vector3[] ToLocalDirection(Vector3[] directions) { var R = Matrix4x4.CreateFromQuaternion(Quaternion.Inverse(Orientation)); Vector3[] result = new Vector3[directions.Length]; for (int i = 0; i < result.Length; i++) { result[i] = Vector3.TransformNormal(directions[i], R); } return result; } public Quaternion ToLocal(Quaternion orientation) => Quaternion.Multiply(orientation, Quaternion.Inverse(Orientation)); public Pose ToLocal(Pose pose) => new Pose(ToLocal(pose.Position), ToLocal(pose.Orientation)); #region Algebra public static Pose Add(Pose A, Pose B) { return new Pose(A.data.position+B.data.position, A.data.orientation+B.data.orientation); } public static Pose Subtract(Pose A, Pose B) { return new Pose(A.data.position+B.data.position, A.data.orientation-B.data.orientation); } public static Pose Scale(float factor, Pose A) { return new Pose(factor*A.data.position, Quaternion.Multiply(A.data.orientation, factor)); } #endregion #region Operators public static Pose operator +(Pose a, Pose b) => Add(a, b); public static Pose operator -(Pose a) => Scale(-1, a); public static Pose operator -(Pose a, Pose b) => Subtract(a, b); public static Pose operator *(float a, Pose b) => Scale(a, b); public static Pose operator *(Pose a, float b) => Scale(b, a); public static Pose operator /(Pose a, float b) => Scale(1 / b, a); #endregion }
  5. Gdi is a graphics library to handle specific sub-tasks such as drawing points on the screen, drawing labels, drawing curves, and various shapes but with a specific style and handing color operations. Gdi是一个图形库,用于处理特定的子任务,例如在屏幕上绘制点、绘制标签、绘制曲线和各种形状,但具有特定的样式和处理颜色操作。 In addition, it keeps a current Pen and SolidFill object for re-use defining the stroke and fill colors to be used in the low-level Gdi drawing operations.此外,它保留了一个当前PenSolidFill对象,以便重新使用,定义要在低级 Gdi 绘图操作中使用的笔触和填充颜色。 Some details are removed from below:从下面删除了一些细节:

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

    For example to draw a 3D point on the screen at location (100,30) with color Red you would issue code like this from a paint handler with access to a Graphics g object例如,要在屏幕上的位置(100,30)处绘制一个 3D 点,颜色为Red ,您可以从可以访问Graphics g对象的绘制处理程序发出这样的代码

    PointF point = new PointF(100,30); // Calls extension method `Gdi.DrawPoint()` g.DrawPoint(Color.Red, point, 4f);

Using the above framework to draw a single point defined by a Vector3 from System.Numerics on the screen you will need the following VisibleObject derived class.使用上述框架在屏幕上绘制由System.Numerics中的Vector3定义的单个点,您将需要以下VisibleObject派生类。 The magic happens in the Render() method which uses the supplied Camera to project the 3D point into a pixel location.魔术发生在Render()方法中,该方法使用提供的Camera将 3D 点投影到像素位置。 In addition, it defines a text label to draw next to the point.此外,它定义了一个文本标签以在该点旁边绘制。

    public class VisiblePoint : VisibleObject
    {
        public VisiblePoint(string label, Color color, float size = 4f)
        {
            Label = label;
            Color=color;
            Size=size;
        }

        public Color Color { get; }
        public float Size { get; }
        public string Label { get; }

        public override void Render(Graphics g, Camera camera, Pose pose)
        {
            var pixel = camera.Project(pose.Position);
            g.DrawPoint(Color, pixel, Size);
            if (!string.IsNullOrEmpty(Label))
            {
                g.DrawLabel(Color, pixel, Label, ContentAlignment.BottomRight);
            }
        }
    }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM