1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-05-31 22:41:37 +00:00

Add rotation and backface culling

Also, correctly update the thumbnail when leaving the visualization
editor.
This commit is contained in:
Andy McFadden 2020-03-06 16:51:47 -08:00
parent eec847d5f1
commit b686d2d208
11 changed files with 304 additions and 52 deletions

125
CommonUtil/Matrix44.cs Normal file
View File

@ -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 {
/// <summary>
/// Simple 4x4 matrix.
/// </summary>
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;
}
/// <summary>
/// Sets the matrix to perform rotation about Euler angles in the order X, Y, Z.
/// </summary>
/// <param name="xdeg">Rotation about the X axis, in degrees.</param>
/// <param name="ydeg">Rotation about the Y axis, in degrees.</param>
/// <param name="zdeg">Rotation about the Z axis, in degrees.</param>
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;
}
/// <summary>
/// Multiplies a 3-element vector. The vector's 4th element is implicitly set to 1.
/// </summary>
/// <param name="vec">Column vector to multiply.</param>
/// <returns>Result vector.</returns>
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();
}
}
}

View File

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

View File

@ -333,11 +333,12 @@ namespace PluginCommon {
/// All objects will have vertices and edges. Face normals are optional.
/// </summary>
/// <remarks>
/// 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?
/// </remarks>

View File

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

View File

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

View File

@ -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).

View File

@ -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",

View File

@ -37,6 +37,8 @@ namespace SourceGen {
/// that weren't changed while sitting in the undo buffer.
/// </remarks>
public class Visualization {
public const double THUMBNAIL_DIM = 64;
/// <summary>
/// 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<string, object> parms) {
Debug.Assert(visWire != null);
Debug.Assert(parms != null);
CachedImage = GenerateWireframeImage(visWire, parms, 64);
CachedImage = GenerateWireframeImage(visWire, parms, THUMBNAIL_DIM);
}
/// <summary>
@ -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<WireframeObject.LineSeg> 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) {

View File

@ -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.</returns>
public List<LineSeg> Generate(ReadOnlyDictionary<string, object> parms) {
List<LineSeg> segs = new List<LineSeg>(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;
}
}
}

View File

@ -43,6 +43,8 @@ namespace SourceGen.WpfGui {
private SortedList<int, VisualizationSet> mEditedList;
private Visualization mOrigVis;
public BitmapSource mThumbnail;
/// <summary>
/// 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);
}
}