September 2014

Sun Mon Tue Wed Thu Fri Sat
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30        








« Wizards vs. code samples? | Main | Accessing the properties of a dynamic AutoCAD block using .NET »

March 02, 2009

Smoothly transitioning between 3D AutoCAD views using .NET - Part 2

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.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e20112790f190728a4

Listed below are links to weblogs that reference Smoothly transitioning between 3D AutoCAD views using .NET - Part 2:

blog comments powered by Disqus

Feed/Share

10 Random Posts