From eec847d5f1047a0134799731b7b0dfefc2162969 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Tue, 3 Mar 2020 19:29:15 -0800 Subject: [PATCH] Implement basic wireframe rendering We extract the data from the wireframe visualization, perform a trivial transform, and display it. The perspective vs. orthographic flag in the parameters is respected. (No rotation or backface removal yet.) Also, increased the thumbnail sizes in the visualization set editor list from 48x48 to 64x64, because the nearest-pixel-scaled 48x48 looks nasty when used for wireframes. --- CommonUtil/Vector3.cs | 55 ++++ PluginCommon/Interfaces.cs | 2 +- PluginCommon/VisWireframe.cs | 22 +- .../Visualization/VisWireframeTest.cs | 4 +- SourceGen/SourceGen.csproj | 1 + SourceGen/Visualization.cs | 73 +++--- SourceGen/WireframeObject.cs | 235 ++++++++++++++++++ SourceGen/WpfGui/EditVisualizationSet.xaml | 12 +- 8 files changed, 353 insertions(+), 51 deletions(-) create mode 100644 CommonUtil/Vector3.cs create mode 100644 SourceGen/WireframeObject.cs diff --git a/CommonUtil/Vector3.cs b/CommonUtil/Vector3.cs new file mode 100644 index 0000000..92364ce --- /dev/null +++ b/CommonUtil/Vector3.cs @@ -0,0 +1,55 @@ +/* + * Copyright 2020 faddenSoft + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; + +namespace CommonUtil { + /// + /// Simple 3-element column vector. + /// + public class Vector3 { + public double X { + get { return mX; } + set { mX = value; } + } + public double Y { + get { return mY; } + set { mY = value; } + } + public double Z { + get { return mZ; } + set { mZ = value; } + } + + private double mX, mY, mZ; + + public Vector3(double x, double y, double z) { + mX = x; + mY = y; + mZ = z; + } + + public double Magnitude() { + return Math.Sqrt(X * X + Y * Y + Z * Z); + } + + public void Normalize() { + double len_r = 1.0 / Magnitude(); + mX *= len_r; + mY *= len_r; + mZ *= len_r; + } + } +} diff --git a/PluginCommon/Interfaces.cs b/PluginCommon/Interfaces.cs index 9f8b6ac..b8e9c80 100644 --- a/PluginCommon/Interfaces.cs +++ b/PluginCommon/Interfaces.cs @@ -342,7 +342,7 @@ namespace PluginCommon { /// TODO(maybe): specify colors for edges. Not widely used? /// public interface IVisualizationWireframe { - // Each function returns the specified data. Do not modify. + // Each function returns the specified data. Do not modify the returned arrays. float[] GetVerticesX(); float[] GetVerticesY(); diff --git a/PluginCommon/VisWireframe.cs b/PluginCommon/VisWireframe.cs index d0fde4c..6609f10 100644 --- a/PluginCommon/VisWireframe.cs +++ b/PluginCommon/VisWireframe.cs @@ -25,20 +25,26 @@ namespace PluginCommon { /// [Serializable] public class VisWireframe : IVisualizationWireframe { + public const string P_IS_PERSPECTIVE = "_isPerspective"; + public const string P_IS_BACKFACE_REMOVED = "_isBackfaceRemoved"; + public const string P_EULER_ROT_X = "_eulerRotX"; + public const string P_EULER_ROT_Y = "_eulerRotY"; + public const string P_EULER_ROT_Z = "_eulerRotZ"; + public static VisParamDescr Param_IsPerspective(string uiLabel, bool defaultVal) { - return new VisParamDescr(uiLabel, "_isPerspective", typeof(bool), 0, 0, 0, defaultVal); + return new VisParamDescr(uiLabel, P_IS_PERSPECTIVE, typeof(bool), 0, 0, 0, defaultVal); } public static VisParamDescr Param_IsBackfaceRemoved(string uiLabel, bool defaultVal) { - return new VisParamDescr(uiLabel, "_isBackfaceRemoved", typeof(bool), 0, 0, 0, defaultVal); + return new VisParamDescr(uiLabel, P_IS_BACKFACE_REMOVED, typeof(bool), 0, 0, 0, defaultVal); } public static VisParamDescr Param_EulerX(string uiLabel, int defaultVal) { - return new VisParamDescr(uiLabel, "_eulerRotX", typeof(int), 0, 359, 0, defaultVal); + return new VisParamDescr(uiLabel, P_EULER_ROT_X, typeof(int), 0, 359, 0, defaultVal); } public static VisParamDescr Param_EulerY(string uiLabel, int defaultVal) { - return new VisParamDescr(uiLabel, "_eulerRotY", typeof(int), 0, 359, 0, defaultVal); + return new VisParamDescr(uiLabel, P_EULER_ROT_Y, typeof(int), 0, 359, 0, defaultVal); } public static VisParamDescr Param_EulerZ(string uiLabel, int defaultVal) { - return new VisParamDescr(uiLabel, "_eulerRotZ", typeof(int), 0, 359, 0, defaultVal); + return new VisParamDescr(uiLabel, P_EULER_ROT_Z, typeof(int), 0, 359, 0, defaultVal); } private List mVerticesX = new List(); @@ -136,6 +142,12 @@ namespace PluginCommon { int faceCount = mNormalsX.Count; int edgeCount = mEdges.Count; + // complain about empty objects (should we fail if no edges were defined?) + if (vertexCount == 0) { + msg = "no vertices defined"; + return false; + } + // check edges foreach (IntPair ip in mEdges) { if (ip.Val0 < 0 || ip.Val0 >= vertexCount || diff --git a/SourceGen/SGTestData/Visualization/VisWireframeTest.cs b/SourceGen/SGTestData/Visualization/VisWireframeTest.cs index 6baba6d..37c670f 100644 --- a/SourceGen/SGTestData/Visualization/VisWireframeTest.cs +++ b/SourceGen/SGTestData/Visualization/VisWireframeTest.cs @@ -124,7 +124,9 @@ namespace WireframeTest { int edge = vw.AddEdge(v0, v1); vw.AddEdgeFace(edge, f0); - vw.AddEdgeFace(edge, f1); + if (f1 != f0) { + vw.AddEdgeFace(edge, f1); + } } while (true) { diff --git a/SourceGen/SourceGen.csproj b/SourceGen/SourceGen.csproj index 10cd9fd..a718bf8 100644 --- a/SourceGen/SourceGen.csproj +++ b/SourceGen/SourceGen.csproj @@ -105,6 +105,7 @@ + AboutBox.xaml diff --git a/SourceGen/Visualization.cs b/SourceGen/Visualization.cs index 1935c15..baab770 100644 --- a/SourceGen/Visualization.cs +++ b/SourceGen/Visualization.cs @@ -172,13 +172,16 @@ namespace SourceGen { } } + /// + /// Updates the cached thumbnail image. + /// + /// Visualization object. + /// Visualization parameters. public void SetThumbnail(IVisualizationWireframe visWire, ReadOnlyDictionary parms) { - if (visWire == null) { - CachedImage = BROKEN_IMAGE; - } else { - CachedImage = GenerateWireframeImage(visWire, parms, 64); - } + Debug.Assert(visWire != null); + Debug.Assert(parms != null); + CachedImage = GenerateWireframeImage(visWire, parms, 64); } /// @@ -250,8 +253,8 @@ namespace SourceGen { // Create bitmap. RenderTargetBitmap bitmap = new RenderTargetBitmap( - (int)bounds.Width, - (int)bounds.Height, + (int)dim, + (int)dim, 96, 96, PixelFormats.Pbgra32); @@ -297,7 +300,8 @@ namespace SourceGen { // but because the thickness doesn't extend past the endpoints, the filled // area is only three. If you have a window of size 10x10, and you draw from // 0,0 to 9,9, the line will extend for half a line-thickness off the top, - // but will not go past the right/left edges. + // but will not go past the right/left edges. (This becomes very obvious when + // you're working with an up-scaled 8x8 path.) // // Similarly, drawing a horizontal line two units long results in a square, and // drawing a line that starts and ends at the same point doesn't appear to @@ -309,46 +313,39 @@ namespace SourceGen { // (9.5,1.5) is drawn as a single-wide full-brightness line. This is because of // the anti-aliasing. // - // The path has a bounding box that starts at (0,0) in the top left, and extends - // out as far as needed. If we want a path-drawn shape to animate smoothly we - // want to ensure that the bounds are constant across all renderings of a shape - // (which could get thinner or wider as it rotates), so we draw an invisible - // pixel in our desired bottom-right corner. + // The path has an axis-aligned bounding box that covers the pixel centers. If we + // want a path-drawn shape to animate smoothly we want to ensure that the bounds + // are constant across all renderings of a shape (which could get thinner or wider + // as it rotates), so we draw an invisible pixel in our desired bottom-right corner. // // If we want an 8x8 bitmap, we draw a line from (8,8) to (8,8) to establish the // bounds, then draw lines with coordinates from 0.5 to 7.5. GeometryGroup geo = new GeometryGroup(); - // This establishes the geometry bounds. It's a zero-length line segment, so - // nothing is actually drawn. Debug.WriteLine("using max=" + dim); - // TODO: currently ignoring dim - Point corner = new Point(8, 8); - geo.Children.Add(new LineGeometry(corner, corner)); - corner = new Point(0, 0); - geo.Children.Add(new LineGeometry(corner, corner)); - // TODO(xyzzy): render - //geo.Children.Add(new LineGeometry(new Point(0.0, 0.0), new Point(1.0, 0.0))); - //geo.Children.Add(new LineGeometry(new Point(0.5, 0.5), new Point(1.5, 0.5))); - //geo.Children.Add(new LineGeometry(new Point(0.75, 0.75), new Point(1.75, 0.75))); - geo.Children.Add(new LineGeometry(new Point(0.0, 0.0), new Point(5.0, 7.0))); + // Draw invisible line segments to establish Path bounds. + Point topLeft = new Point(0, 0); + Point botRight = new Point(dim, dim); + geo.Children.Add(new LineGeometry(topLeft, topLeft)); + geo.Children.Add(new LineGeometry(botRight, botRight)); - geo.Children.Add(new LineGeometry(new Point(0.5, 2), new Point(0.5, 3))); - geo.Children.Add(new LineGeometry(new Point(1.5, 3), new Point(1.5, 4))); - geo.Children.Add(new LineGeometry(new Point(2.5, 2), new Point(2.5, 3))); - geo.Children.Add(new LineGeometry(new Point(3.5, 3), new Point(3.5, 4))); - geo.Children.Add(new LineGeometry(new Point(4.5, 2), new Point(4.5, 3))); - geo.Children.Add(new LineGeometry(new Point(5.5, 3), new Point(5.5, 4))); - geo.Children.Add(new LineGeometry(new Point(6.5, 2), new Point(6.5, 3))); - geo.Children.Add(new LineGeometry(new Point(7.5, 3), new Point(7.5, 4))); + // Generate a list of clip-space line segments. Coordinate values are [-1,1]. + WireframeObject wireObj = WireframeObject.Create(visWire); + List segs = wireObj.Generate(parms); - //geo.Children.Add(new LineGeometry(new Point(4, 5), new Point(3, 5))); - //geo.Children.Add(new LineGeometry(new Point(2, 5), new Point(1, 5))); + // Convert clip-space coords to screen. We need to scale up, round them to the + // nearest whole pixel, and add +0.5 to make the thumbnails look crisp. + double scale = (dim - 0.5) / 2; + double adj = 0.5; + foreach (WireframeObject.LineSeg seg in segs) { + Point start = new Point(Math.Round((seg.X0 + 1) * scale) + adj, + Math.Round((1 - seg.Y0) * scale) + adj); + Point end = new Point(Math.Round((seg.X1 + 1) * scale) + adj, + Math.Round((1 - seg.Y1) * scale) + adj); + geo.Children.Add(new LineGeometry(start, end)); + } - //geo.Children.Add(new LineGeometry(new Point(4, 7), new Point(1, 7))); - //geo.Children.Add(new LineGeometry(new Point(5, 7), new Point(9, 7))); - //geo.Children.Add(new LineGeometry(new Point(0, 8.5), new Point(9, 8.5))); return geo; } diff --git a/SourceGen/WireframeObject.cs b/SourceGen/WireframeObject.cs new file mode 100644 index 0000000..bacf49a --- /dev/null +++ b/SourceGen/WireframeObject.cs @@ -0,0 +1,235 @@ +/* + * Copyright 2020 faddenSoft + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; + +using CommonUtil; +using PluginCommon; + +namespace SourceGen { + /// + /// Renders a wireframe visualization, generating a collection of line segments in clip space. + /// + public class WireframeObject { + /// + /// Line segment. + /// + public class LineSeg { + public double X0 { get; private set; } + public double Y0 { get; private set; } + public double X1 { get; private set; } + public double Y1 { get; private set; } + + public LineSeg(double x0, double y0, double x1, double y1) { + X0 = x0; + Y0 = y0; + X1 = x1; + Y1 = y1; + } + } + + private class Vertex { + public List Faces { get; private set; } + + public Vector3 Vec { get; private set; } + + public Vertex(double x, double y, double z) { + Vec = new Vector3(x, y, z); + Faces = new List(); + } + } + + private class Edge { + public Vertex Vertex0 { get; private set; } + public Vertex Vertex1 { get; private set; } + public List Faces { get; private set; } + + public Edge(Vertex v0, Vertex v1) { + Vertex0 = v0; + Vertex1 = v1; + Faces = new List(); + } + } + + private class Face { + public Vector3 Normal { get; private set; } + + public Face(double x, double y, double z) { + Normal = new Vector3(x, y, z); + Normal.Normalize(); + } + } + + private List mVertices = new List(); + private List mEdges = new List(); + private List mFaces = new List(); + private double mBigMag = -1.0; + + + // private constructor; use Create() + private WireframeObject() { } + + /// + /// Creates a new object from a wireframe visualization. + /// + /// Visualization object. + /// New object. + public static WireframeObject Create(IVisualizationWireframe visWire) { + WireframeObject wireObj = new WireframeObject(); + + // + // Start by extracting data from the visualization object. Everything stored + // there is loaded into this object. + // + + float[] normalsX = visWire.GetNormalsX(); + if (normalsX.Length > 0) { + float[] normalsY = visWire.GetNormalsY(); + float[] normalsZ = visWire.GetNormalsZ(); + + if (normalsX.Length != normalsY.Length || normalsX.Length != normalsZ.Length) { + Debug.Assert(false); + return null; + } + + for (int i = 0; i < normalsX.Length; i++) { + wireObj.mFaces.Add(new Face(normalsX[i], normalsY[i], normalsZ[i])); + } + } + + float[] verticesX = visWire.GetVerticesX(); + float[] verticesY = visWire.GetVerticesY(); + float[] verticesZ = visWire.GetVerticesZ(); + if (verticesX.Length == 0) { + Debug.Assert(false); + return null; + } + if (verticesX.Length != verticesY.Length || verticesX.Length != verticesZ.Length) { + Debug.Assert(false); + return null; + } + + for (int i = 0; i < verticesX.Length; i++) { + wireObj.mVertices.Add(new Vertex(verticesX[i], verticesY[i], verticesZ[i])); + } + + IntPair[] edges = visWire.GetEdges(); + for (int i = 0; i < edges.Length; i++) { + int v0index = edges[i].Val0; + int v1index = edges[i].Val1; + + if (v0index < 0 || v0index >= wireObj.mVertices.Count || + v1index < 0 || v1index >= wireObj.mVertices.Count) { + Debug.Assert(false); + return null; + } + + wireObj.mEdges.Add( + new Edge(wireObj.mVertices[v0index], wireObj.mVertices[v1index])); + } + + IntPair[] vfaces = visWire.GetVertexFaces(); + for (int i = 0; i < vfaces.Length; i++) { + int vindex = vfaces[i].Val0; + int findex = vfaces[i].Val1; + + if (vindex < 0 || vindex >= wireObj.mVertices.Count || + findex < 0 || findex >= wireObj.mFaces.Count) { + Debug.Assert(false); + return null; + } + + wireObj.mVertices[vindex].Faces.Add(wireObj.mFaces[findex]); + } + + IntPair[] efaces = visWire.GetEdgeFaces(); + for (int i = 0; i < efaces.Length; i++) { + int eindex = efaces[i].Val0; + int findex = efaces[i].Val1; + + if (eindex < 0 || eindex >= wireObj.mEdges.Count || + findex < 0 || findex >= wireObj.mFaces.Count) { + Debug.Assert(false); + return null; + } + + wireObj.mEdges[eindex].Faces.Add(wireObj.mFaces[findex]); + } + + // + // All data has been loaded into friendly classes. + // + + // Compute the magnitude of the largest vertex, for scaling. + double bigMag = -1.0; + for (int i = 0; i < wireObj.mVertices.Count; i++) { + double mag = wireObj.mVertices[i].Vec.Magnitude(); + if (bigMag < mag) { + bigMag = mag; + } + } + wireObj.mBigMag = bigMag; + + return wireObj; + } + + /// + /// Generates a list of line segments for the wireframe data and the specified + /// parameters. + /// + /// Visualization parameters. + /// List of line segments, which could be empty if backface removal + /// was especially successful. + public List Generate(ReadOnlyDictionary parms) { + List segs = new List(mEdges.Count); + + // Perspective distance adjustment. + const double zadj = 3.0; + + // Scale values to [-1,1]. + bool doPersp = Util.GetFromObjDict(parms, VisWireframe.P_IS_PERSPECTIVE, false); + double scale = 1.0 / mBigMag; + if (doPersp) { + scale = (scale * zadj) / (zadj + 1); + } + + foreach (Edge edge in mEdges) { + double x0, y0, x1, y1; + + if (doPersp) { + // +Z is closer to the viewer, so we negate it here + double z0 = -edge.Vertex0.Vec.Z * scale; + double z1 = -edge.Vertex1.Vec.Z * scale; + x0 = (edge.Vertex0.Vec.X * scale * zadj) / (zadj + z0); + y0 = (edge.Vertex0.Vec.Y * scale * zadj) / (zadj + z0); + x1 = (edge.Vertex1.Vec.X * scale * zadj) / (zadj + z1); + y1 = (edge.Vertex1.Vec.Y * scale * zadj) / (zadj + z1); + } else { + x0 = edge.Vertex0.Vec.X * scale; + y0 = edge.Vertex0.Vec.Y * scale; + x1 = edge.Vertex1.Vec.X * scale; + y1 = edge.Vertex1.Vec.Y * scale; + } + + segs.Add(new LineSeg(x0, y0, x1, y1)); + } + + return segs; + } + } +} diff --git a/SourceGen/WpfGui/EditVisualizationSet.xaml b/SourceGen/WpfGui/EditVisualizationSet.xaml index bc2322e..8c8b661 100644 --- a/SourceGen/WpfGui/EditVisualizationSet.xaml +++ b/SourceGen/WpfGui/EditVisualizationSet.xaml @@ -23,7 +23,7 @@ limitations under the License. xmlns:local="clr-namespace:SourceGen.WpfGui" mc:Ignorable="d" Title="Edit Visualization Set" - Width="600" Height="400" ResizeMode="NoResize" + Width="640" Height="480" ResizeMode="NoResize" ShowInTaskbar="False" WindowStartupLocation="CenterOwner" Closing="Window_Closing"> @@ -71,21 +71,21 @@ limitations under the License. - + - - + - - + +