您需要实现某种投影变换,将每个Vector3坐标从 (X,Y,Z) 坐标转换为PointF(X,Y) 坐标。
在每个Paint事件中,将原点移动到绘图表面的中心,并使用一些数学方法投影 3D 点,如下所示
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);\nRun Code Online (Sandbox Code Playgroud)\n从事件传递的对象g在哪里。坐标为负数的原因是因为在 WinForms 中正数是向下的,而对于 3D 图形来说,违反右手规则并指向向上是有意义的。该方法在点所在的位置画一个小圆圈。如果需要,可以使用来填充圆圈。GraphicsPaintYYYDrawEllipseFillEllipse
我在 GitHub 上有一个关于在 Winforms 中简单渲染 3D 几何的示例项目。
\n\n该示例的其他部分您可以忽略,但我将解释我在 WinForms 控件上渲染简单 3D 对象的过程。
\nPictureBox是渲染的目标控件所在的位置。这是表格的唯一要求。放置控制以显示事物。PictureBox很方便,而且一开始就支持双缓冲。
Camera是一个进行渲染的类。它负责以下任务。
Paint事件。Graphics对象。我的课程的简化版本Camera如下。需要研究的是Project()获取几何Vector3对象和返回PointF对象的方法。
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}\nRun Code Online (Sandbox Code Playgroud)\nScene和VisibleObject。要绘制的对象的基类称为VisibleObject。您在上面的屏幕截图中看到的所有内容都源自VisibleObject. 这包括实体、曲线和坐标三元组。AScene只是要绘制的集合VisibleObject,它处理Paint发出的事件Camera。最后它遍历对象并发出要渲染的命令。
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}\nRun Code Online (Sandbox Code Playgroud)\n和基VisibleObject类
public abstract class VisibleObject \n{\n public abstract void Render(Graphics g, Camera camera, Pose pose);\n}\nRun Code Online (Sandbox Code Playgroud)\n需要理解的一点是,每个位置VisibleObject都不包含在其中。这样做是为了可以在屏幕上的不同位置绘制同一对象的多个副本。
Pose每个对象的 3D 位置和方向由类定义Pose,其中包含Vector3原点和Quaternion方向。
主要功能是执行本地到世界或反向转换的FromLocal()和方法。ToLoca()
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}\nRun Code Online (Sandbox Code Playgroud)\nGdi是一个图形库,用于处理特定的子任务,例如在屏幕上绘制点、绘制标签、绘制曲线和各种形状,但具有特定的样式和处理颜色操作。此外,它还保留一个当前 Pen对象SolidFill以供重复使用,定义要在低级 Gdi 绘图操作中使用的描边和填充颜色。下面删除了一些细节:
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}\nRun Code Online (Sandbox Code Playgroud)\n例如,要在屏幕上(100,30)具有颜色的位置绘制 3D 点Red,您可以从可访问Graphics g对象的绘制处理程序中发出如下代码
PointF point = new PointF(100,30);\n// Calls extension method `Gdi.DrawPoint()`\ng.DrawPoint(Color.Red, point, 4f); \nRun Code Online (Sandbox Code Playgroud)\n使用上面的框架在屏幕上绘制由Vector3from定义的单个点,您将需要以下派生类。神奇的事情发生在使用所提供的将 3D 点投影到像素位置的方法中。此外,它还定义了一个要在该点旁边绘制的文本标签。System.NumericsVisibleObjectRender()Camera
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 }\nRun Code Online (Sandbox Code Playgroud)\n