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);
}
}