From ee6f03d5ce19dd92b1bd994a31327077d832f2cf Mon Sep 17 00:00:00 2001 From: wo80 Date: Wed, 2 Mar 2022 17:36:33 +0100 Subject: [PATCH] Triangle.Rendering projection expects normalized device coordinates now. This should allow correct rendering of meshes that use more than 32bit float precision for their coordinates (no more unchecked cast from double to float). --- src/MeshExplorer/FormTopology.cs | 10 +- src/MeshExplorer/IO/FileProcessor.cs | 5 +- .../Topology/TopologyControlView.cs | 4 +- .../Topology/TopologyRenderControl.cs | 4 +- src/MeshExplorer/Topology/TopologyRenderer.cs | 43 +++--- src/Triangle.Rendering/Buffer/IndexBuffer.cs | 10 +- src/Triangle.Rendering/Buffer/VertexBuffer.cs | 66 ++++++--- .../GDI/FunctionRenderer.cs | 4 +- src/Triangle.Rendering/GDI/MeshRenderer.cs | 12 +- src/Triangle.Rendering/Projection.cs | 127 +++++++++++------- 10 files changed, 181 insertions(+), 104 deletions(-) diff --git a/src/MeshExplorer/FormTopology.cs b/src/MeshExplorer/FormTopology.cs index af7ea42..3ec3d89 100644 --- a/src/MeshExplorer/FormTopology.cs +++ b/src/MeshExplorer/FormTopology.cs @@ -52,20 +52,20 @@ namespace MeshExplorer topoControlView.SetTriangle(current.Triangle); } - private ITriangle FindTriangleAt(float x, float y) + private ITriangle FindTriangleAt(float xr, float yr) { // Get mesh coordinates - var p = new System.Drawing.PointF(x, y); - renderControl.Zoom.ScreenToWorld(ref p); + var p = new System.Drawing.PointF(xr, yr); + renderControl.Zoom.ScreenToWorld(p, out double x, out double y); - topoControlView.SetPosition(p); + topoControlView.SetPosition(x, y); if (tree == null) { tree = new TriangleQuadTree(mesh, 5, 2); } - return tree.Query(p.X, p.Y); + return tree.Query(x, y); } private void InvokePrimitive(string name) diff --git a/src/MeshExplorer/IO/FileProcessor.cs b/src/MeshExplorer/IO/FileProcessor.cs index eb22796..d676664 100644 --- a/src/MeshExplorer/IO/FileProcessor.cs +++ b/src/MeshExplorer/IO/FileProcessor.cs @@ -6,13 +6,12 @@ namespace MeshExplorer.IO { + using MeshExplorer.IO.Formats; using System; using System.Collections.Generic; using System.IO; - using MeshExplorer.IO.Formats; - using TriangleNet.IO; - using TriangleNet.Geometry; using TriangleNet; + using TriangleNet.Geometry; /// /// Provides static methods to read and write mesh files. diff --git a/src/MeshExplorer/Topology/TopologyControlView.cs b/src/MeshExplorer/Topology/TopologyControlView.cs index 665af76..351684e 100644 --- a/src/MeshExplorer/Topology/TopologyControlView.cs +++ b/src/MeshExplorer/Topology/TopologyControlView.cs @@ -15,11 +15,11 @@ namespace MeshExplorer.Topology InitializeComponent(); } - public void SetPosition(PointF p) + public void SetPosition(double x, double y) { var nfi = NumberFormatInfo.InvariantInfo; - lbPosition.Text = String.Format(nfi, "X: {0:0.0}, Y: {1:0.0}", p.X, p.Y); + lbPosition.Text = string.Format(nfi, "X: {0:0.0}, Y: {1:0.0}", x, y); } public void SetTriangle(ITriangle tri) diff --git a/src/MeshExplorer/Topology/TopologyRenderControl.cs b/src/MeshExplorer/Topology/TopologyRenderControl.cs index 5818118..d8090c9 100644 --- a/src/MeshExplorer/Topology/TopologyRenderControl.cs +++ b/src/MeshExplorer/Topology/TopologyRenderControl.cs @@ -45,15 +45,13 @@ namespace MeshExplorer.Topology renderer = new TopologyRenderer(mesh); zoom = new Projection(ClientRectangle); - //zoom.ClipMargin = 10.0f; - zoom.Initialize(mesh.Bounds); InitializeBuffer(); initialized = true; - this.Render(); + Render(); } public void Update(Otri otri) diff --git a/src/MeshExplorer/Topology/TopologyRenderer.cs b/src/MeshExplorer/Topology/TopologyRenderer.cs index e2c6f4c..cf815b9 100644 --- a/src/MeshExplorer/Topology/TopologyRenderer.cs +++ b/src/MeshExplorer/Topology/TopologyRenderer.cs @@ -6,7 +6,7 @@ namespace MeshExplorer.Topology using TriangleNet; using TriangleNet.Geometry; using TriangleNet.Rendering; - + public class TopologyRenderer { Projection zoom; @@ -39,9 +39,16 @@ namespace MeshExplorer.Topology int k = 0; + var b = mesh.Bounds; + + double s = 1d / Math.Max(b.Width, b.Height); + double dx = b.X; + double dy = b.Y; + foreach (var v in mesh.Vertices) { - points[k++] = new PointF((float)v.X, (float)v.Y); + // Normalized device coordinates. + points[k++] = new PointF((float)((v.X - dx) * s), (float)((v.Y - dy) * s)); } font = new Font("Arial", 7.5f); @@ -146,7 +153,7 @@ namespace MeshExplorer.Topology var brush = i == id ? Brushes.DarkRed : Point; pt = points[i]; - zoom.WorldToScreen(ref pt); + zoom.NdcToScreen(ref pt); g.FillEllipse(brush, pt.X - 10f, pt.Y - 10f, 20, 20); pt.X -= i > 9 ? 7 : 4; @@ -168,9 +175,9 @@ namespace MeshExplorer.Topology p1 = points[tri.GetVertexID(1)]; p2 = points[tri.GetVertexID(2)]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); - zoom.WorldToScreen(ref p2); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); + zoom.NdcToScreen(ref p2); g.DrawLine(Line, p0, p1); g.DrawLine(Line, p1, p2); @@ -197,9 +204,9 @@ namespace MeshExplorer.Topology p1 = points[tri.GetVertexID(1)]; p2 = points[tri.GetVertexID(2)]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); - zoom.WorldToScreen(ref p2); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); + zoom.NdcToScreen(ref p2); center = GetIncenter(p0, p1, p2); center.X -= 5; @@ -221,8 +228,8 @@ namespace MeshExplorer.Topology p0 = points[edge.P0]; p1 = points[edge.P1]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); g.DrawLine(Line, p0, p1); } @@ -239,8 +246,8 @@ namespace MeshExplorer.Topology p0 = points[seg.P0]; p1 = points[seg.P1]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); g.DrawLine(Segment, p0, p1); } @@ -255,8 +262,8 @@ namespace MeshExplorer.Topology p0 = points[currentOrg.ID]; p1 = points[currentDest.ID]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); g.DrawLine(SelectedEdge, p0, p1); } @@ -272,9 +279,9 @@ namespace MeshExplorer.Topology p[1] = points[currentTri.GetVertexID(1)]; p[2] = points[currentTri.GetVertexID(2)]; - zoom.WorldToScreen(ref p[0]); - zoom.WorldToScreen(ref p[1]); - zoom.WorldToScreen(ref p[2]); + zoom.NdcToScreen(ref p[0]); + zoom.NdcToScreen(ref p[1]); + zoom.NdcToScreen(ref p[2]); g.FillPolygon(SelectedTriangle, p); } diff --git a/src/Triangle.Rendering/Buffer/IndexBuffer.cs b/src/Triangle.Rendering/Buffer/IndexBuffer.cs index 108d8a7..a9a2152 100644 --- a/src/Triangle.Rendering/Buffer/IndexBuffer.cs +++ b/src/Triangle.Rendering/Buffer/IndexBuffer.cs @@ -1,11 +1,11 @@  +using System.Collections.Generic; +using System.Linq; +using TriangleNet.Geometry; +using TriangleNet.Topology; + namespace TriangleNet.Rendering.Buffer { - using System.Collections.Generic; - using System.Linq; - using TriangleNet.Geometry; - using TriangleNet.Topology; - public class IndexBuffer : BufferBase { #region Static methods diff --git a/src/Triangle.Rendering/Buffer/VertexBuffer.cs b/src/Triangle.Rendering/Buffer/VertexBuffer.cs index 3dcb1f7..b8b8815 100644 --- a/src/Triangle.Rendering/Buffer/VertexBuffer.cs +++ b/src/Triangle.Rendering/Buffer/VertexBuffer.cs @@ -1,9 +1,10 @@  +using System; +using System.Collections.Generic; +using TriangleNet.Geometry; + namespace TriangleNet.Rendering.Buffer { - using System.Collections.Generic; - using TriangleNet.Geometry; - public class VertexBuffer : BufferBase { #region Static methods @@ -12,25 +13,40 @@ namespace TriangleNet.Rendering.Buffer /// Create a vertex buffer from given point collection. /// /// The points to render. - /// The points bounding box. - /// + /// Returns the vertex buffer. + public static IBuffer Create(ICollection points) + { + return Create(points, new Rectangle(0d, 0d, 1d, 1d)); + } + + /// + /// Create a normalized vertex buffer from given point collection. + /// + /// The points to render. + /// The bounding box used for normalization. + /// Returns a buffer of normalized coordinates. public static IBuffer Create(ICollection points, Rectangle bounds) { var buffer = new VertexBuffer(2 * points.Count); var data = buffer.Data; - float x, y; + double dx = bounds.X; + double dy = bounds.Y; + + double scale = 1.0 / Math.Max(bounds.Width, bounds.Height); int i = 0; + double x, y; + foreach (var p in points) { - x = (float)p.X; - y = (float)p.Y; + x = (p.X - dx) * scale; + y = (p.Y - dy) * scale; - data[2 * i] = x; - data[2 * i + 1] = y; + data[2 * i] = (float)x; + data[2 * i + 1] = (float)y; i++; } @@ -39,23 +55,43 @@ namespace TriangleNet.Rendering.Buffer } /// - /// Create a vertex buffer from given vertex collection. + /// Create a vertex buffer from given point collection. + /// + /// The points to render. + /// Returns the vertex buffer. + public static IBuffer Create(ICollection points) + { + return Create(points, new Rectangle(0d, 0d, 1d, 1d)); + } + + /// + /// Create a normalized vertex buffer from given vertex collection. /// /// The vertices to render. - /// The vertices bounding box. - /// + /// The bounding box used for normalization. + /// Returns a buffer of normalized coordinates. public static IBuffer Create(ICollection points, Rectangle bounds) { var buffer = new VertexBuffer(2 * points.Count); var data = buffer.Data; + double dx = bounds.X; + double dy = bounds.Y; + + double scale = 1.0 / Math.Max(bounds.Width, bounds.Height); + int i = 0; + double x, y; + foreach (var p in points) { - data[2 * i] = (float)p.X; - data[2 * i + 1] = (float)p.Y; + x = (p.X - dx) * scale; + y = (p.Y - dy) * scale; + + data[2 * i] = (float)x; + data[2 * i + 1] = (float)y; i++; } diff --git a/src/Triangle.Rendering/GDI/FunctionRenderer.cs b/src/Triangle.Rendering/GDI/FunctionRenderer.cs index 24f0c08..2af4cac 100644 --- a/src/Triangle.Rendering/GDI/FunctionRenderer.cs +++ b/src/Triangle.Rendering/GDI/FunctionRenderer.cs @@ -48,7 +48,7 @@ namespace TriangleNet.Rendering.GDI Color color; PointF p = new PointF((float)data[0], (float)data[1]); - zoom.WorldToScreen(ref p); + zoom.NdcToScreen(ref p); // Get correction distance float dx = (p.X - (int)p.X) * 2.0f; @@ -60,7 +60,7 @@ namespace TriangleNet.Rendering.GDI p.X = (float)data[size * i]; p.Y = (float)data[size * i + 1]; - zoom.WorldToScreen(ref p); + zoom.NdcToScreen(ref p); color = colors[i]; diff --git a/src/Triangle.Rendering/GDI/MeshRenderer.cs b/src/Triangle.Rendering/GDI/MeshRenderer.cs index df19ca5..f630cee 100644 --- a/src/Triangle.Rendering/GDI/MeshRenderer.cs +++ b/src/Triangle.Rendering/GDI/MeshRenderer.cs @@ -58,7 +58,7 @@ namespace TriangleNet.Rendering.GDI if (zoom.Viewport.Contains(p)) { - zoom.WorldToScreen(ref p); + zoom.NdcToScreen(ref p); g.FillEllipse(brush, p.X - 1.5f, p.Y - 1.5f, 3, 3); } } @@ -109,9 +109,9 @@ namespace TriangleNet.Rendering.GDI if (zoom.Viewport.Intersects(tri[0], tri[1], tri[2])) { - zoom.WorldToScreen(ref tri[0]); - zoom.WorldToScreen(ref tri[1]); - zoom.WorldToScreen(ref tri[2]); + zoom.NdcToScreen(ref tri[0]); + zoom.NdcToScreen(ref tri[1]); + zoom.NdcToScreen(ref tri[2]); if (filled) { @@ -162,8 +162,8 @@ namespace TriangleNet.Rendering.GDI if (zoom.Viewport.Intersects(p0, p1)) { - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); g.DrawLine(pen, p0, p1); } diff --git a/src/Triangle.Rendering/Projection.cs b/src/Triangle.Rendering/Projection.cs index 5ed8f5f..090e199 100644 --- a/src/Triangle.Rendering/Projection.cs +++ b/src/Triangle.Rendering/Projection.cs @@ -1,6 +1,6 @@ // ----------------------------------------------------------------------- // -// TODO: Update copyright text. +// Triangle.NET code by Christian Woltering, http://triangle.codeplex.com/ // // ----------------------------------------------------------------------- @@ -9,74 +9,95 @@ namespace TriangleNet.Rendering using System; using System.Drawing; + using TRectangle = Geometry.Rectangle; + /// /// Manages a world to screen transformation (2D orthographic projection). /// + /// + /// + /// The projection implementation is actually not world-to-screen, but NDC-to-screen + /// (Normalized-Device-Coordinates). NDC here is - in contrast for example to OpenGL, the + /// transformation of world coordinates to a unit rectangle with origin (0,0) and a max + /// side length 1 (the width/height ratio is preserved). It's a simple translate-scale + /// transform, which is automatically applied in VertexBuffer.Create(points, bounds). + /// + /// + /// Since the upper-left corner of the display is usually the screen coordinate origin + /// (0,0), the project will automatically invert the y-axis. + /// + /// public class Projection { - // The screen. + // The original mesh bounds (needed for screen-to-world projection). + TRectangle world_; + + // Precomputed scaling factor for normalized coordinates. + double scale_; + + // The screen dimensions. Rectangle screen; - // The complete mesh. - RectangleF world; - - RectangleF viewport; + // The original mesh and the viewport in normalized coordinates. + RectangleF world, viewport; /// - /// Gets or sets the current viewport (visible mesh). + /// Gets or sets the current viewport (normalized coordinates). /// public RectangleF Viewport => viewport; - /// - /// Gets the current scale. - /// - public float Scale => screen.Width / viewport.Width; - /// /// Gets the zoom level. /// public int Level { get; private set; } - // The y-direction of windows screen coordinates is upside down, - // so invertY should be set to true. - bool invertY; + private const int MAX_ZOOM = 100; - const int maxZoomLevel = 100; - - public Projection(Rectangle screen, bool invertY = true) + /// + /// Initializes a new instance of the class. + /// + /// The current screen (viewport) dimensions. + public Projection(Rectangle screen) { this.screen = screen; - this.invertY = invertY; - world = viewport = screen; + world = viewport = new RectangleF(screen.X, screen.Y, screen.Width, screen.Height); Level = 1; } /// - /// Inititialize the projection. + /// Initialize the projection. /// - /// The world that should be transformed to screen coordinates. - public void Initialize(Geometry.Rectangle world) + /// The world that should be transformed to screen coordinates. + public void Initialize(TRectangle world) { Level = 1; - float ww = (float)world.Width; - float wh = (float)world.Height; + // Bounding box of original (non-normalized) coordinates. + world_ = world; + + // Scaling factor for normalized coordinates. + scale_ = Math.Max(world.Width, world.Height); + + // Dimensions in normalized coordinates. + float ww = (float)(world.Width / scale_); + float wh = (float)(world.Height / scale_); + + // Add a margin so there's some space around the screen borders. + float margin = (ww < wh) ? wh * 0.05f : ww * 0.05f; int sw = screen.Width; int sh = screen.Height; - // Add a margin so there's some space around the border - float margin = (ww < wh) ? wh * 0.05f : ww * 0.05f; - float wRatio = ww / wh; float sRatio = sw / (float)sh; - float scale = (sRatio < wRatio) ? (ww + margin) / sw : (wh + margin) / sh; + float scale = (sRatio < wRatio) ? (ww + margin) / sw : (wh + margin) / sh; - float centerX = (float)world.X + ww / 2; - float centerY = (float)world.Y + wh / 2; + // Center in normalized coordinates (left = bottom = 0) + float centerX = ww / 2; + float centerY = wh / 2; // Get the initial viewport (complete mesh centered on the screen) this.world = viewport = new RectangleF( @@ -87,9 +108,9 @@ namespace TriangleNet.Rendering } /// - /// Handle resize of the screen. + /// Handle resize of the screen (viewport). /// - /// The new screen dimensions. + /// The new screen (viewport) dimensions. public void Resize(Rectangle newScreen) { // The viewport has to be updated, but we want to keep @@ -149,25 +170,23 @@ namespace TriangleNet.Rendering /// /// Zoom in or out of the viewport. /// - /// Zoom amount - /// Relative x point position - /// Relative y point position + /// Zoom amount. + /// Relative x point position (in [0..1] range). + /// Relative y point position (in [0..1] range). public bool Zoom(int amount, float focusX, float focusY) { float width, height; - if (invertY) - { - focusY = 1 - focusY; - } + // Invert y coordinate. + focusY = 1 - focusY; if (amount > 0) // Zoom in { Level++; - if (Level > maxZoomLevel) + if (Level > MAX_ZOOM) { - Level = maxZoomLevel; + Level = MAX_ZOOM; return false; } @@ -221,22 +240,40 @@ namespace TriangleNet.Rendering return true; } + /// + /// Reset the zoom to initial state. + /// public void Reset() { viewport = world; Level = 1; } - public void WorldToScreen(ref PointF pt) + /// + /// Project a normalized device coordinate to screen coordinates. + /// + /// Input normalized device coordinate, output screen coordinate. + public void NdcToScreen(ref PointF pt) { pt.X = (pt.X - viewport.X) / viewport.Width * screen.Width; pt.Y = (1 - (pt.Y - viewport.Y) / viewport.Height) * screen.Height; } + /// + /// Project a screen coordinate to world coordinates. + /// + /// Normalized position on screen (both coordinates in [0..1] range). + /// The world x-coordinate. + /// The world y-coordinate. public void ScreenToWorld(PointF pt, out double x, out double y) { - x = viewport.X + viewport.Width * pt.X; - y = viewport.Y + viewport.Height * (1 - pt.Y); + // Position in normalized coordinates. + var nx = viewport.X + viewport.Width * pt.X; + var ny = viewport.Y + viewport.Height * (1 - pt.Y); + + // Translate and scale to world coordinates. + x = world_.X + nx * scale_; + y = world_.Y + ny * scale_; } } }