diff --git a/CommonUtil/Matrix44.cs b/CommonUtil/Matrix44.cs new file mode 100644 index 0000000..ec73371 --- /dev/null +++ b/CommonUtil/Matrix44.cs @@ -0,0 +1,125 @@ +/* + * 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.Text; + +namespace CommonUtil { + /// + /// Simple 4x4 matrix. + /// + public class Matrix44 { + public double[,] Val { + get { return mVal; } + private set { mVal = value; } + } + + private double[,] mVal; + + public Matrix44() { + Val = new double[4, 4]; + } + + public void Clear() { + for (int col = 0; col < 4; col++) { + for (int row = 0; row < 4; row++) { + Val[col, row] = 0.0; + } + } + } + + public void SetToIdentity() { + Clear(); + Val[0, 0] = Val[1, 1] = Val[2, 2] = Val[3, 3] = 1.0; + } + + /// + /// Sets the matrix to perform rotation about Euler angles in the order X, Y, Z. + /// + /// Rotation about the X axis, in degrees. + /// Rotation about the Y axis, in degrees. + /// Rotation about the Z axis, in degrees. + public void SetRotationEuler(int xdeg, int ydeg, int zdeg) { + const double degToRad = Math.PI / 180.0; + double xrad = xdeg * degToRad; + double yrad = ydeg * degToRad; + double zrad = zdeg * degToRad; + + double cx = Math.Cos(xrad); + double sx = Math.Sin(xrad); + double cy = Math.Cos(yrad); + double sy = Math.Sin(yrad); + double cz = Math.Cos(zrad); + double sz = Math.Sin(zrad); + double sycx = sy * cx; + double sysx = sy * sx; + + bool useXyz = false; + if (useXyz) { + // R = Rz * Ry * Rx (from wikipedia) + Val[0, 0] = cz * cy; + Val[0, 1] = sz * cy; + Val[0, 2] = -sy; + + Val[1, 0] = cz * sysx - sz * cx; + Val[1, 1] = sz * sysx + cz * cx; + Val[1, 2] = cy * sx; + + Val[2, 0] = cz * sycx + sz * sx; + Val[2, 1] = sz * sycx - cz * sx; + Val[2, 2] = cy * cx; + } else { + // R = Rx * Ry * Rz (from Arc3D) + Val[0, 0] = cz * cy; + Val[0, 1] = -sz * cy; + Val[0, 2] = sy; + + Val[1, 0] = cz * sysx + sz * cx; + Val[1, 1] = -sz * sysx + cz * cx; + Val[1, 2] = -cy * sx; + + Val[2, 0] = -cz * sycx + sz * sx; + Val[2, 1] = sz * sycx + cz * sx; + Val[2, 2] = cy * cx; + } + //Val[0, 3] = Val[1, 3] = Val[2, 3] = Val[3, 0] = Val[3, 1] = Val[3, 2] = 0.0; + Val[3, 3] = 1.0; + } + + /// + /// Multiplies a 3-element vector. The vector's 4th element is implicitly set to 1. + /// + /// Column vector to multiply. + /// Result vector. + public Vector3 Multiply(Vector3 vec) { + double rx = vec.X * Val[0, 0] + vec.Y * Val[1, 0] + vec.Z * Val[2, 0] + Val[3, 0]; + double ry = vec.X * Val[0, 1] + vec.Y * Val[1, 1] + vec.Z * Val[2, 1] + Val[3, 1]; + double rz = vec.X * Val[0, 2] + vec.Y * Val[1, 2] + vec.Z * Val[2, 2] + Val[3, 2]; + return new Vector3(rx, ry, rz); + } + + + public override string ToString() { + StringBuilder sb = new StringBuilder(); + for (int row = 0; row < 4; row++) { + sb.AppendFormat("|{0,8:N3} {1,8:N3} {2,8:N3} {3,8:N3}|", + Val[0, row], Val[1, row], Val[2, row], Val[3, row]); + sb.AppendLine(); + } + return sb.ToString(); + } + } +} diff --git a/CommonUtil/Vector3.cs b/CommonUtil/Vector3.cs index 92364ce..cce04e6 100644 --- a/CommonUtil/Vector3.cs +++ b/CommonUtil/Vector3.cs @@ -51,5 +51,20 @@ namespace CommonUtil { mY *= len_r; mZ *= len_r; } + + public void Multiply(double sc) { + mX *= sc; + mY *= sc; + mZ *= sc; + } + + public static double Dot(Vector3 v0, Vector3 v1) { + return v0.X * v1.X + v0.Y * v1.Y + v0.Z * v1.Z; + } + + + public override string ToString() { + return string.Format("|{0,8:N3} {1,8:N3} {2,8:N3}|", X, Y, Z); + } } } diff --git a/PluginCommon/Interfaces.cs b/PluginCommon/Interfaces.cs index b8e9c80..ad85d7a 100644 --- a/PluginCommon/Interfaces.cs +++ b/PluginCommon/Interfaces.cs @@ -333,11 +333,12 @@ namespace PluginCommon { /// All objects will have vertices and edges. Face normals are optional. /// /// - /// The face-normal stuff is designed specifically for Elite, which is one of the very - /// few games to use backface removal. + /// The face-normal stuff is designed specifically for Elite. Besides being one of the + /// very few 6502-based games to use backface culling, it extended the concept to allow + /// convex shapes to have protrusions. /// - /// We favor multiple arrays over compound objects for this interface to avoid having - /// to define those at the plugin interface level. + /// We favor multiple arrays over compound objects for this interface to avoid making + /// such objects part of the plugin interface. /// /// TODO(maybe): specify colors for edges. Not widely used? /// diff --git a/PluginCommon/VisWireframe.cs b/PluginCommon/VisWireframe.cs index 6609f10..89a54f5 100644 --- a/PluginCommon/VisWireframe.cs +++ b/PluginCommon/VisWireframe.cs @@ -26,7 +26,7 @@ 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_IS_BFC_ENABLED = "_isBfcEnabled"; public const string P_EULER_ROT_X = "_eulerRotX"; public const string P_EULER_ROT_Y = "_eulerRotY"; public const string P_EULER_ROT_Z = "_eulerRotZ"; @@ -34,8 +34,8 @@ namespace PluginCommon { public static VisParamDescr Param_IsPerspective(string uiLabel, bool 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, P_IS_BACKFACE_REMOVED, typeof(bool), 0, 0, 0, defaultVal); + public static VisParamDescr Param_IsBfcEnabled(string uiLabel, bool defaultVal) { + return new VisParamDescr(uiLabel, P_IS_BFC_ENABLED, typeof(bool), 0, 0, 0, defaultVal); } public static VisParamDescr Param_EulerX(string uiLabel, int defaultVal) { return new VisParamDescr(uiLabel, P_EULER_ROT_X, typeof(int), 0, 359, 0, defaultVal); @@ -183,6 +183,9 @@ namespace PluginCommon { } } + // TODO(maybe): confirm that every face has a vertex. Not strictly necessary + // since you can do orthographic-projection BFC without it... but who does that? + msg = string.Empty; return true; } diff --git a/SourceGen/SGTestData/Visualization/VisWireframeTest.cs b/SourceGen/SGTestData/Visualization/VisWireframeTest.cs index 37c670f..dff385b 100644 --- a/SourceGen/SGTestData/Visualization/VisWireframeTest.cs +++ b/SourceGen/SGTestData/Visualization/VisWireframeTest.cs @@ -43,10 +43,11 @@ namespace WireframeTest { P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0), // These are interpreted by the main app. - VisWireframe.Param_EulerX("Euler rotation X", 0), - VisWireframe.Param_EulerY("Euler rotation Y", 0), - VisWireframe.Param_EulerZ("Euler rotation Z", 0), + VisWireframe.Param_EulerX("Rotation about X", 0), + VisWireframe.Param_EulerY("Rotation about Y", 0), + VisWireframe.Param_EulerZ("Rotation about Z", 0), VisWireframe.Param_IsPerspective("Perspective projection", true), + VisWireframe.Param_IsBfcEnabled("Backface culling", true), }), }; diff --git a/SourceGen/SGTestData/Visualization/wireframe-test b/SourceGen/SGTestData/Visualization/wireframe-test index 1804db8..8f644ca 100644 Binary files a/SourceGen/SGTestData/Visualization/wireframe-test and b/SourceGen/SGTestData/Visualization/wireframe-test differ diff --git a/SourceGen/SGTestData/Visualization/wireframe-test.S b/SourceGen/SGTestData/Visualization/wireframe-test.S index 87d94c2..e568f8b 100644 --- a/SourceGen/SGTestData/Visualization/wireframe-test.S +++ b/SourceGen/SGTestData/Visualization/wireframe-test.S @@ -29,21 +29,21 @@ vertices ; List of edges (vertex0, vertex1, face0, face1). edges - dfb 0,1, 0,1 ;0 - dfb 1,2, 0,3 ;1 - dfb 2,3, 0,4 ;2 - dfb 3,0, 0,2 ;3 - dfb 4,5, 1,5 ;4 - dfb 5,6, 1,3 ;5 - dfb 6,7, 1,4 ;6 - dfb 7,4, 1,2 ;7 - dfb 0,4, 2,5 ;8 - dfb 1,5, 3,5 ;9 - dfb 2,6, 3,4 ;10 - dfb 3,7, 2,4 ;11 + dfb 0,1, 0,5 ;0 + dfb 1,2, 0,3 ;1 + dfb 2,3, 0,4 ;2 + dfb 3,0, 0,2 ;3 + dfb 4,5, 1,5 ;4 + dfb 5,6, 1,3 ;5 + dfb 6,7, 1,4 ;6 + dfb 7,4, 1,2 ;7 + dfb 0,4, 2,5 ;8 + dfb 1,5, 3,5 ;9 + dfb 2,6, 3,4 ;10 + dfb 3,7, 2,4 ;11 - dfb 8,9, 0,0 ;12 - dfb 9,10,0,0 ;13 + dfb 8,9, 0,0 ;12 + dfb 9,10, 0,0 ;13 dfb $80 ; List of faces (surface normal X,Y,Z). diff --git a/SourceGen/SGTestData/Visualization/wireframe-test.dis65 b/SourceGen/SGTestData/Visualization/wireframe-test.dis65 index bf0eebf..0dd080d 100644 --- a/SourceGen/SGTestData/Visualization/wireframe-test.dis65 +++ b/SourceGen/SGTestData/Visualization/wireframe-test.dis65 @@ -2,7 +2,7 @@ { "_ContentVersion":3, "FileDataLength":120, -"FileDataCrc32":1432202158, +"FileDataCrc32":1015994132, "ProjectProps":{ "CpuName":"6502", "IncludeUndocumentedInstr":false, @@ -245,10 +245,11 @@ "VisGenIdent":"wireframe-test", "VisGenParams":{ "offset":10, -"_eulerRotX":0, -"_eulerRotY":0, +"_eulerRotX":15, +"_eulerRotY":30, "_eulerRotZ":0, -"_isPerspective":true}}, +"_isPerspective":true, +"_isBfcEnabled":true}}, { "Tag":"bmp_data", diff --git a/SourceGen/Visualization.cs b/SourceGen/Visualization.cs index baab770..cce9457 100644 --- a/SourceGen/Visualization.cs +++ b/SourceGen/Visualization.cs @@ -37,6 +37,8 @@ namespace SourceGen { /// that weren't changed while sitting in the undo buffer. /// public class Visualization { + public const double THUMBNAIL_DIM = 64; + /// /// Unique user-specified tag. This may be any valid string that is at least two /// characters long after the leading and trailing whitespace have been trimmed. @@ -181,7 +183,7 @@ namespace SourceGen { ReadOnlyDictionary parms) { Debug.Assert(visWire != null); Debug.Assert(parms != null); - CachedImage = GenerateWireframeImage(visWire, parms, 64); + CachedImage = GenerateWireframeImage(visWire, parms, THUMBNAIL_DIM); } /// @@ -322,7 +324,6 @@ namespace SourceGen { // bounds, then draw lines with coordinates from 0.5 to 7.5. GeometryGroup geo = new GeometryGroup(); - Debug.WriteLine("using max=" + dim); // Draw invisible line segments to establish Path bounds. Point topLeft = new Point(0, 0); @@ -330,12 +331,14 @@ namespace SourceGen { geo.Children.Add(new LineGeometry(topLeft, topLeft)); geo.Children.Add(new LineGeometry(botRight, botRight)); - // Generate a list of clip-space line segments. Coordinate values are [-1,1]. + // Generate a list of clip-space line segments. Coordinate values are in the + // range [-1,1], with +X to the right and +Y upward. WireframeObject wireObj = WireframeObject.Create(visWire); List segs = wireObj.Generate(parms); - // 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. + // Convert clip-space coords to screen. We need to translate to [0,2] with +Y + // toward the bottom of the screen, scale up, round to the nearest whole pixel, + // and add +0.5 to make thumbnail-size bitmaps look crisp. double scale = (dim - 0.5) / 2; double adj = 0.5; foreach (WireframeObject.LineSeg seg in segs) { diff --git a/SourceGen/WireframeObject.cs b/SourceGen/WireframeObject.cs index bacf49a..95020f6 100644 --- a/SourceGen/WireframeObject.cs +++ b/SourceGen/WireframeObject.cs @@ -67,11 +67,17 @@ namespace SourceGen { } private class Face { + // Surface normal. public Vector3 Normal { get; private set; } + // One vertex on the face, for BFC. + public Vertex Vert { get; set; } + // Flag set during BFC calculation. + public bool IsVisible { get; set; } public Face(double x, double y, double z) { Normal = new Vector3(x, y, z); - Normal.Normalize(); + Normal.Normalize(); // not necessary, but easier to read in debug output + IsVisible = true; } } @@ -154,7 +160,11 @@ namespace SourceGen { return null; } - wireObj.mVertices[vindex].Faces.Add(wireObj.mFaces[findex]); + Face face = wireObj.mFaces[findex]; + wireObj.mVertices[vindex].Faces.Add(face); + if (face.Vert == null) { + face.Vert = wireObj.mVertices[vindex]; + } } IntPair[] efaces = visWire.GetEdgeFaces(); @@ -168,7 +178,11 @@ namespace SourceGen { return null; } - wireObj.mEdges[eindex].Faces.Add(wireObj.mFaces[findex]); + Face face = wireObj.mFaces[findex]; + wireObj.mEdges[eindex].Faces.Add(face); + if (face.Vert == null) { + face.Vert = wireObj.mEdges[eindex].Vertex0; + } } // @@ -197,33 +211,89 @@ namespace SourceGen { /// was especially successful. public List Generate(ReadOnlyDictionary parms) { List segs = new List(mEdges.Count); + bool doPersp = Util.GetFromObjDict(parms, VisWireframe.P_IS_PERSPECTIVE, false); + bool doBfc = Util.GetFromObjDict(parms, VisWireframe.P_IS_BFC_ENABLED, false); - // Perspective distance adjustment. + // Camera Z coordinate adjustment, used to control how perspective projections + // appear. The larger the value, the farther the object appears to be. Very + // large values approximate an orthographic projection. const double zadj = 3.0; - // Scale values to [-1,1]. - bool doPersp = Util.GetFromObjDict(parms, VisWireframe.P_IS_PERSPECTIVE, false); + // Scale coordinate values to [-1,1]. double scale = 1.0 / mBigMag; if (doPersp) { - scale = (scale * zadj) / (zadj + 1); + // objects closer to camera are bigger; reduce scale slightly + scale = (scale * zadj) / (zadj + 0.5); + } + + int eulerX = Util.GetFromObjDict(parms, VisWireframe.P_EULER_ROT_X, 0); + int eulerY = Util.GetFromObjDict(parms, VisWireframe.P_EULER_ROT_Y, 0); + int eulerZ = Util.GetFromObjDict(parms, VisWireframe.P_EULER_ROT_Z, 0); + Matrix44 rotMat = new Matrix44(); + rotMat.SetRotationEuler(eulerX, eulerY, eulerZ); + + if (doBfc) { + // Mark faces as visible or not. This is determined with the surface normal, + // rather than by checking whether a transformed triangle is clockwise. + foreach (Face face in mFaces) { + // Transform the surface normal. + Vector3 rotNorm = rotMat.Multiply(face.Normal); + if (doPersp) { + // Transform one vertex to get a vector from the camera to the + // surface. We want (V0 - C), where C is the camera; since we're + // at the origin, we just need -C. + if (face.Vert == null) { + Debug.WriteLine("GLITCH: no vertex for face"); + face.IsVisible = true; + continue; + } + Vector3 camVec = rotMat.Multiply(face.Vert.Vec); + camVec.Multiply(-scale); // scale to [-1,1] and negate to get -C + camVec.Z += zadj; // translate + + // Now compute the dot product of the camera vector. + double dot = Vector3.Dot(camVec, rotNorm); + face.IsVisible = (dot >= 0); + //Debug.WriteLine(string.Format( + // "Face {0} vis={1,-5} dot={2,-8:N2}: camVec={3} rotNorm={4}", + // index++, face.IsVisible, dot, camVec, rotNorm)); + } else { + // For orthographic projection, the camera is essentially looking + // down the Z axis at every X,Y, so we can trivially check the + // value of Z in the transformed normal. + face.IsVisible = (rotNorm.Z >= 0); + } + } } foreach (Edge edge in mEdges) { + if (doBfc) { + // To be visible, vertices and edges must either not specify any + // faces, or must specify a visible face. + if (!IsVertexVisible(edge.Vertex0) || + !IsVertexVisible(edge.Vertex1) || + !IsEdgeVisible(edge)) { + continue; + } + } + + Vector3 trv0 = rotMat.Multiply(edge.Vertex0.Vec); + Vector3 trv1 = rotMat.Multiply(edge.Vertex1.Vec); 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); + // +Z on the shape is closer to the viewer, so we negate it here + double z0 = -trv0.Z * scale; + double z1 = -trv1.Z * scale; + x0 = (trv0.X * scale * zadj) / (zadj + z0); + y0 = (trv0.Y * scale * zadj) / (zadj + z0); + x1 = (trv1.X * scale * zadj) / (zadj + z1); + y1 = (trv1.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; + x0 = trv0.X * scale; + y0 = trv0.Y * scale; + x1 = trv1.X * scale; + y1 = trv1.Y * scale; } segs.Add(new LineSeg(x0, y0, x1, y1)); @@ -231,5 +301,29 @@ namespace SourceGen { return segs; } + + private bool IsVertexVisible(Vertex vert) { + if (vert.Faces.Count == 0) { + return true; + } + foreach (Face face in vert.Faces) { + if (face.IsVisible) { + return true; + } + } + return false; + } + + private bool IsEdgeVisible(Edge edg) { + if (edg.Faces.Count == 0) { + return true; + } + foreach (Face face in edg.Faces) { + if (face.IsVisible) { + return true; + } + } + return false; + } } } diff --git a/SourceGen/WpfGui/EditVisualization.xaml.cs b/SourceGen/WpfGui/EditVisualization.xaml.cs index c15f906..6c9a1ec 100644 --- a/SourceGen/WpfGui/EditVisualization.xaml.cs +++ b/SourceGen/WpfGui/EditVisualization.xaml.cs @@ -43,6 +43,8 @@ namespace SourceGen.WpfGui { private SortedList mEditedList; private Visualization mOrigVis; + public BitmapSource mThumbnail; + /// /// Visualization generation identifier for the last visualizer we used, for the benefit /// of "new". @@ -320,7 +322,7 @@ namespace SourceGen.WpfGui { string trimTag = Visualization.TrimAndValidateTag(TagString, out bool isTagValid); Debug.Assert(isTagValid); NewVis = new Visualization(trimTag, item.VisDescriptor.Ident, valueDict, mOrigVis); - NewVis.CachedImage = (BitmapSource)previewImage.Source; + NewVis.CachedImage = mThumbnail; sLastVisIdent = NewVis.VisGenIdent; @@ -446,6 +448,8 @@ namespace SourceGen.WpfGui { BitmapDimensions = "?"; if (!IsValid || item == null) { previewImage.Source = sBadParamsImage; + previewGrid.Background = null; + wireframePath.Data = new GeometryGroup(); } else { // Invoke the plugin. PluginErrMessage = string.Empty; @@ -491,12 +495,17 @@ namespace SourceGen.WpfGui { wireframePath.Data = new GeometryGroup(); BitmapDimensions = string.Format("{0}x{1}", previewImage.Source.Width, previewImage.Source.Height); + + mThumbnail = (BitmapSource)previewImage.Source; } else { previewGrid.Background = Brushes.Black; previewImage.Source = Visualization.BLANK_IMAGE; wireframePath.Data = Visualization.GenerateWireframePath(visWire, parms, previewImage.ActualWidth / 2); BitmapDimensions = "n/a"; + + mThumbnail = Visualization.GenerateWireframeImage(visWire, parms, + Visualization.THUMBNAIL_DIM); } }