Thanks again to Fenton Webb for providing the code behind the first post in the series and to Jeremy Tammik for providing the suggestion of this alternative implementation.
This post follows on from this previous post, which introduced a technique to smoothly transition between 3D views in AutoCAD. It applies a more standard algorithm - known as spherical linear interpolation (or Slerp to its friends :-) - to interpolate between views, rather than interpolating individual values using Fenton's custom-built CosInterp() function. We still use CosInterp() to interpolate the width and height of the field of view, but otherwise the below code makes use of Slerp for the points and vectors it needs to adjust.
Here's the modified C# code, which can be added to the same project as that containing the code from the previous post (to compare the execution):
using System;
using System.Threading;
using System.Drawing;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.GraphicsSystem;
using Autodesk.AutoCAD.Interop;
namespace ViewTransitionsSlerp
{
public class MyView
{
public Point3d position;
public Point3d target;
public Vector3d upVector;
public double fieldWidth;
public double fieldHeight;
// Default constructor
public MyView(){}
// For constant defines below SWIso etc
public MyView(
double x1, double y1, double z1,
double x2, double y2, double z2,
double x3, double y3, double z3,
double x4, double y4
)
{
position = new Point3d(x1, y1, z1);
target = new Point3d(x2, y2, z2);
upVector = new Vector3d(x3, y3, z3);
fieldWidth = x4;
fieldHeight= y4;
}
public MyView(
Point3d position, Point3d target, Vector3d upVector,
double fieldWidth, double fieldHeight
)
{
this.position = position;
this.target = target;
this.upVector = upVector;
this.fieldWidth = fieldWidth;
this.fieldHeight = fieldHeight;
}
};
public class Commands
{
static MyView defaultView =
new MyView(
1930.1,1339.3,4399.3, 1930.1,1339.3,0.0,
0.0,1.0,0.0, 3279.8, 1702.6
);
static MyView topView =
new MyView(
1778.1,1108.2,635.7, 1778.1,1108.2,0.0,
0.0,1.0,0.0, 474.0, 246.0
);
static MyView bottomView =
new MyView(
1778.1,1108.2,-635.7, 1778.1,1108.2,0.0,
0.0,1.0,0.0, 474.0, 246.0
);
static MyView leftView =
new MyView(
-344.1,1108.2,66.1, 0.0,1108.2,66.1,
0.0,0.0,1.0, 256.5, 133.2
);
static MyView rightView =
new MyView(
344.1,1108.2,66.1, 0.0,1108.2,66.1,
0.0,0.0,1.0, 256.5, 133.2
);
static MyView SWIso =
new MyView(
265.1,-404.7,1579.0, 838.0,168.2,1006.2,
0.4,0.4,0.8, 739.7, 384.0
);
static MyView SEIso =
new MyView(
2105.6,780.7,393.7, 1532.7,1353.5,-179.2,
-0.4,0.4,0.8, 739.7, 384.0
);
static MyView NEIso =
new MyView(
1366.8,697.0,-345.2, 793.9,124.1,-918.0,
-0.4,-0.4,0.8, 739.7, 384.0
);
static MyView NWIso =
new MyView(
1003.9, 1882.3, 840.2, 1576.8, 1309.5,
267.3, 0.4, -0.4, 0.8, 739.7, 384.0
);
// Enacts a smooth transition from the current view to a
// new view using spherical linear interpolation (Slerp)
static Matrix3d SmoothViewToSlerp(
MyView nv, double timeToTake
)
{
Matrix3d newViewMatrix = Matrix3d.Identity;
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db =
doc.Database;
Manager gsm =
doc.GraphicsManager;
// Get the current viewport
int vpn =
Convert.ToInt32(
Application.GetSystemVariable("CVPORT")
);
View view = gsm.GetGsView(vpn, true);
using (view)
{
// Set the frame rate to the standard eye FPS
view.BeginInteractivity(24);
Matrix3d viewMatrix = view.ViewingMatrix;
// Get the current view settings
MyView cv =
new MyView(
view.Position, view.Target, view.UpVector,
view.FieldWidth, view.FieldHeight
);
// Set up the start positions
Point3d intPos = cv.position;
Point3d intTgt = cv.target;
Vector3d intUpVec = cv.upVector;
double intWid = cv.fieldWidth;
double intHgt = cv.fieldHeight;
// Now animate the view change between the
// currentview and the viewToChangeTo
for (float mu = 0; mu <= 1; mu += 0.01F)
{
// First convert the positions to vectors
// (so we can simply call the existing function)
Vector3d startPos = cv.position - Point3d.Origin,
endPos = nv.position - Point3d.Origin,
// Then get the target vectors relative
// to the view position
from = cv.target - cv.position,
to = nv.target - nv.position,
// Now Slerp the various vectors
res1 = Slerp(startPos, endPos, mu),
res2 = Slerp(from, to, mu),
res3 = Slerp(cv.upVector, nv.upVector, mu);
// And then we extract the relevant information...
// Get a point from the position vector
intPos = Point3d.Origin + res1;
// Get the target point relative to that position
intTgt = intPos + res2;
// And the up-vector is easy :-)
intUpVec = res3;
// Let's use our previous interpolate function
// for the field width and height
// Interpolate Width
intWid =
CosInterp(cv.fieldWidth, nv.fieldWidth, mu);
// Interpolate Height
intHgt =
CosInterp(cv.fieldHeight, nv.fieldHeight, mu);
// Now set the interpolated view
view.SetView(intPos, intTgt, intUpVec, intWid, intHgt);
// Update the control
view.Update();
// Decrease the sleep time, or rather increase the
// speed of the view change as we work
double sleepTime = timeToTake - (mu * 10);
Thread.Sleep((int)(sleepTime > 50 ? 0 : sleepTime));
}
view.EndInteractivity();
// Finally set the new view
gsm.SetViewportFromView(vpn, view, true, true, false);
System.Windows.Forms.Application.DoEvents();
}
return newViewMatrix;
}
// Cosine interpolation
static double CosInterp(double y1, double y2, double mu)
{
double mu2;
mu2 = (1-Math.Cos(mu*Math.PI))/2;
return(y1*(1-mu2)+y2*mu2);
}
// Spherical linear interpolation
static Vector3d Slerp(Vector3d from, Vector3d to, float step)
{
if (step == 0)
return from;
if (from == to || step == 1)
return to;
// Normalize the vectors
Vector3d unitfrom = from.GetNormal(),
unitto = to.GetNormal();
// Calculate the included angle
double theta =
Math.Acos(unitfrom.DotProduct(unitto));
if (theta == 0)
return to;
// Avoid the repeated sine calculation
double st =
Math.Sin(theta);
// Return the geometric spherical linear interpolation
return
from * (Math.Sin((1 - step) * theta) / st) +
to * Math.Sin(step * theta) / st;
}
// Function to create a solid background of the same
// colour as the background of our 2D modelspace view
// (reduces the visual shock as the colour would
// otherwise switch to grey and back)
private static ObjectId CreateBackground()
{
const string bgKey = "TTIF_BG";
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db =
doc.Database;
ObjectId vtId = ObjectId.Null;
// Get the current viewport number
int vpn =
Convert.ToInt32(
Application.GetSystemVariable("CVPORT")
);
// No need to set the background if a corresponding
// 3D view already exists
View view =
doc.GraphicsManager.GetGsView(vpn, false);
if (view == null)
{
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
ObjectId bgId = ObjectId.Null;
// Get or create our background dictionary
ObjectId bgdId =
Background.GetBackgroundDictionaryId(db, true);
DBDictionary bgd =
(DBDictionary)tr.GetObject(
bgdId,
OpenMode.ForRead
);
if (bgd.Contains(bgKey))
{
bgId = bgd.GetAt(bgKey);
}
else
{
// If our background doesn't exist...
// Get the 2D modelspace background colour
AcadPreferences prefs =
(AcadPreferences)Application.Preferences;
int rawCol =
(int)prefs.Display.GraphicsWinModelBackgrndColor;
// Create a background with the corresponding RGB
SolidBackground sb = new SolidBackground();
sb.Color =
new Autodesk.AutoCAD.Colors.EntityColor(
(byte)(rawCol & 0x000000FF),
(byte)((rawCol & 0x0000FF00) >> 8),
(byte)((rawCol & 0x00FF0000) >> 16)
);
// Add it to the background dictionary
bgd.UpgradeOpen();
bgId = bgd.SetAt(bgKey, sb);
tr.AddNewlyCreatedDBObject(sb, true);
}
// Set the background on the active modelspace viewport
ViewportTable vt =
(ViewportTable)tr.GetObject(
db.ViewportTableId,
OpenMode.ForRead
);
foreach (ObjectId id in vt)
{
ViewportTableRecord vtr =
(ViewportTableRecord)tr.GetObject(
id,
OpenMode.ForRead
);
if (vtr.Name == "*Active")
{
vtId = id;
vtr.UpgradeOpen();
vtr.Background = bgId;
}
}
tr.Commit();
}
}
else
view.Dispose();
return vtId;
}
private static void RemoveBackground(ObjectId vtId)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db =
doc.Database;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
// Open up the previously-modified viewport
ViewportTableRecord vtr =
(ViewportTableRecord)tr.GetObject(
vtId,
OpenMode.ForWrite
);
// And set its previous background
ObjectId obgId =
vtr.GetPreviousBackground(
DrawableType.SolidBackground
);
vtr.Background = obgId;
tr.Commit();
}
}
[CommandMethod("TVS")]
static public void TransitionViewSlerp()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db =
doc.Database;
ObjectId vtId = CreateBackground();
SmoothViewToSlerp(defaultView, 10);
SmoothViewToSlerp(SWIso, 10);
SmoothViewToSlerp(topView, 10);
SmoothViewToSlerp(SEIso, 10);
SmoothViewToSlerp(bottomView, 10);
SmoothViewToSlerp(NEIso, 10);
SmoothViewToSlerp(leftView, 10);
SmoothViewToSlerp(NWIso, 10);
SmoothViewToSlerp(rightView, 10);
SmoothViewToSlerp(defaultView, 10);
if (vtId != ObjectId.Null)
RemoveBackground(vtId);
}
}
}
What's especially notable about this implementation is actually how similarly it works to the previous one. Fenton came up with a pretty nice interpolation technique without knowing about Slerp which produces very similar - possibly identical, although I haven't verified them - results. Very cool.
From my side I hadn't heard of Slerp and only had the vaguest idea of what a quaternion was - even now I wouldn't know a quaternion if it bit me on the nose, so it's a good thing the wikipedia article contains a geometric alternative to the quaternion Slerp formula.
So why use one technique over the other? There are a couple of possible differentiators that may make a difference to people.
The first is the possibility - and this is not something I've verified through performance benchmarking - that the Slerp implementation is more efficient. We need fewer calls to Slerp() than we used for CosInterp(), simply because we're interpolating multiple values at the same time. But this isn't likely to be a noticeable difference in any real-world application, so isn't something that would concern me, either way.
The second differentiator is a potential deployment issue: a bug was introduced with Service Pack 1 of the .NET Framework 3.5 that can cause problems with vector arithmetic in AutoCAD .NET applications. Jimmy Bergmark reported on this, back in August, and a hotfix was posted by Microsoft in early December. I hit this issue after having installed a pre-release version of AutoCAD 2010 (which installed the .NET Framework 3.5 SP1) but I hit the problem when executing code in AutoCAD 2009. Installing the hotfix solved the problem, but in this case the more fundamental implementation not relying on Vector3d objects proved to be more reliable.
In reality, though, avoiding vector arithmetic isn't really an option for most developers, so this is being addressed on a few fronts: AutoCAD 2009 Update 2 apparently works around it (not sure how I missed that update, but there you go) as does AutoCAD 2010... and it also seems that any day now Microsoft will be pushing out a fixed version of the .NET Framework 3.5 SP1 in a general distribution release via Windows Update (which means that any Windows user running .NET Framework 2.0 or higher will get it). So I'm a little less worried about the impact of this issue than I was when I first saw it manifest itself. For ADN members who would like additional, detailed information on this issue, please visit this DevNote on the ADN site (login required).
All this to say that the two versions of the code are much the same, when all is said and done. I've provided both mainly for the purposes of intellectual curiosity and in case the techniques shown are relevant for other scenarios.