[英]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?
我怎么做?
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
填充圆圈。
I have a sample project on GitHub on simple rendering of 3D geometry in Winforms.我在 GitHub 上有一个关于在 Winforms 中简单渲染 3D 几何的示例项目。
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 对象的过程。
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
很方便,它支持一开始就进行双缓冲。
Camera
is a class that does the rendering. Camera
是一个进行渲染的类。 It is responsible for the following tasks.它负责以下任务。
Paint
event.Paint
事件。Graphics
object before rendering on screen.Graphics
对象。 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; } }
Scene
and VisibleObject
. Scene
和VisibleObject
。 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.这样做是为了可以在屏幕上的不同位置绘制同一对象的多个副本。
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 }
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.此外,它保留了一个当前
Pen
和SolidFill
对象,以便重新使用,定义要在低级 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.