Kean专题(3)—Graphics_System
本帖最后由 作者 于 2009-5-16 11:16:38 编辑原帖:http://through-the-interface.typepad.com/through_the_interface/graphics_system/
一、使用NetApi渲染Cad模型
April 05, 2007
Rendering AutoCAD models offscreen using .NET
这个问题是在内部讨论,我想分享的代码提供了我们的技术团队与您(当然我们有一些小的补充)。
This question came up in an internal discussion, and I thought I'd share the code provided by our Engineering team with you (with a few minor additions from my side, of course).
这个想法是提供一个三维场景离屏(即保存到文件所提供的不可见的图像编辑器) 。The idea is to render a 3D scene off-screen (i.e. save to file the rendered image not visible in the editor). In the below code, we do zoom to the extents of the 3D model that gets created, just to show it's there, but the rendering activity itself is not performed by the 3D view that is used to generate the graphics for AutoCAD's editor window.
A 3D model does need to be loaded in the editor for this to work, so I decided to add a simple sphere to the modelspace and set its material to one that renders nicely (Sitework.Paving - Surfacing.Riverstone.Mortared). The code doesn't import the material programmatically, so you'll want to make sure it's there in the drawing to get the full effect of the rendering (by dragging the material into the drawing view from the materials tool palette, for instance), or to change the code to point to one that exists in the active drawing.
Here's the C# code:using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.GraphicsSystem;
using Autodesk.AutoCAD.Interop;
using System.Drawing;
namespace OffscreenRendering
{
public class Commands
{
static public void OffscreenRender()
{
CreateSphere();
RenderToFile("c:\\sphere.png");
}
static public void CreateSphere()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt,
OpenMode.ForWrite
);
Solid3d sol = new Solid3d();
sol.CreateSphere(10.0);
const string matname =
"Sitework.Paving - Surfacing.Riverstone.Mortared";
DBDictionary matdict =
(DBDictionary)tr.GetObject(
db.MaterialDictionaryId,
OpenMode.ForRead
);
if (matdict.Contains(matname))
{
sol.Material = matname;
}
else
{
ed.WriteMessage(
"\nMaterial (" + matname + ") not found" +
" - sphere will be rendered without it.",
matname
);
}
btr.AppendEntity(sol);
tr.AddNewlyCreatedDBObject(sol, true);
tr.Commit();
}
AcadApplication acadApp =
(AcadApplication)Application.AcadApplication;
acadApp.ZoomExtents();
}
static public void RenderToFile(string filename)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
int vpn =
System.Convert.ToInt32(
Application.GetSystemVariable("CVPORT")
);
View gsv =
doc.GraphicsManager.GetGsView(vpn, true);
using (View view = gsv.Clone(true, true))
{
Device dev =
doc.GraphicsManager.CreateAutoCADOffScreenDevice();
using (dev)
{
dev.OnSize(doc.GraphicsManager.DisplaySize);
dev.DeviceRenderType = RendererType.FullRender;
dev.Add(view);
using (Bitmap bitmap = view.RenderToImage())
{
bitmap.Save(filename);
ed.WriteMessage(
"\nRendered image saved to: " +
filename
);
}
}
}
}
}
}
And here's the resized contents of the file created at c:\sphere.png by the OSR command:
二、模型快照
April 18, 2007
Taking a snapshot of the AutoCAD model (take 2)
In this previous post, we looked at some code to do a programmatic snapshot of AutoCAD's modelspace, saving the results to an image file.
From the discussion that followed, I realised that the code had an undesired (and unnecessary) side-effect of creating a new 3D GS View and leaving the modelspace with that view active. GS Views in AutoCAD 2007 have grey backgrounds by default, and so this change can be quite disturbing for users. The only reason we created the GS View in the first place (if one didn't already exist), was to use it to query the view position/target/up vector/field width and height and apply it to our new view. Thankfully it seems this can also be determined directly from the viewport.
So rather than calling GetGSView() and using the returned view to get that information, we now simply call SetViewFromViewport() specifying the viewport number held in CVPORT, and the graphics system manager for that document handles the rest.
Here's the updated C# code, which appears to achieve the same goals without the side-effect. Check line 124 for the new code, a few extraneous lines around it having been removed:using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.GraphicsSystem;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Interop;
using System.Drawing;
namespace OffscreenImageCreation
{
public class Commands
{
static public void OffscreenSnapshot()
{
CreateSphere();
SnapshotToFile(
"c:\\sphere-Wireframe2D.png",
VisualStyleType.Wireframe2D
);
SnapshotToFile(
"c:\\sphere-Hidden.png",
VisualStyleType.Hidden
);
SnapshotToFile(
"c:\\sphere-Basic.png",
VisualStyleType.Basic
);
SnapshotToFile(
"c:\\sphere-ColorChange.png",
VisualStyleType.ColorChange
);
SnapshotToFile(
"c:\\sphere-Conceptual.png",
VisualStyleType.Conceptual
);
SnapshotToFile(
"c:\\sphere-Flat.png",
VisualStyleType.Flat
);
SnapshotToFile(
"c:\\sphere-Gouraud.png",
VisualStyleType.Gouraud
);
SnapshotToFile(
"c:\\sphere-Realistic.png",
VisualStyleType.Realistic
);
}
static public void CreateSphere()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt,
OpenMode.ForWrite
);
Solid3d sol = new Solid3d();
sol.CreateSphere(10.0);
const string matname =
"Sitework.Paving - Surfacing.Riverstone.Mortared";
DBDictionary matdict =
(DBDictionary)tr.GetObject(
db.MaterialDictionaryId,
OpenMode.ForRead
);
if (matdict.Contains(matname))
{
sol.Material = matname;
}
else
{
ed.WriteMessage(
"\nMaterial (" + matname + ") not found" +
" - sphere will be rendered without it.",
matname
);
}
btr.AppendEntity(sol);
tr.AddNewlyCreatedDBObject(sol, true);
tr.Commit();
}
AcadApplication acadApp =
(AcadApplication)Application.AcadApplication;
acadApp.ZoomExtents();
}
static public void SnapshotToFile(
string filename,
VisualStyleType vst
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
Database db = doc.Database;
Manager gsm = doc.GraphicsManager;
// Get some AutoCAD system variables
int vpn =
System.Convert.ToInt32(
Application.GetSystemVariable("CVPORT")
);
using (View view = new View())
{
gsm.SetViewFromViewport(view, vpn);
// Set the visual style to the one passed in
view.VisualStyle = new VisualStyle(vst);
Device dev =
gsm.CreateAutoCADOffScreenDevice();
using (dev)
{
dev.OnSize(gsm.DisplaySize);
// Set the render type and the background color
dev.DeviceRenderType = RendererType.Default;
dev.BackgroundColor = Color.White;
// Add the view to the device and update it
dev.Add(view);
dev.Update();
using (Model model = gsm.CreateAutoCADModel())
{
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
// Add the modelspace to the view
// It's a container but also a drawable
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt,
OpenMode.ForRead
);
view.Add(btr, model);
tr.Commit();
}
// Take the snapshot
Rectangle rect = view.Viewport;
using (Bitmap bitmap = view.GetSnapshot(rect))
{
bitmap.Save(filename);
ed.WriteMessage(
"\nSnapshot image saved to: " +
filename
);
// Clean up
view.EraseAll();
dev.Erase(view);
}
}
}
}
}
}
}And here are the images created by the OSS command, listed by their visual style type...
三、使用Zoom
June 06, 2008
Zooming to a window or entity inside AutoCAD with .NET
This post was inspired by a recent email from Sreekar Devatha, who is just about to leave DevTech India to work in one of our Engineering teams in Singapore (all the best on your new adventure, Sreekar! :-).
It shows how to perform a programmatic zoom in .NET, whether to a window or to an entity. The .NET API in AutoCAD doesn't expose a handy "ZoomWindow" method, but there are a few options open that use officially supported APIs:
Create a ViewTableRecord and set it as the current view (the classic ObjectARX technique, as described in this DevNote on the ADN site)
Use the COM API to perform a ZoomWindow
Call the ZOOM command using one of the techniques in this previous post
The first two change the view very effectively, but don't show the nice view transitions you get with the ZOOM command (hence the 3rd option).
I ended up implementing the following C# code to show each of these techniques, which can be applied to commands that zoom either to a window (ZW) or to an entity (ZE). Just change the functions used from ZoomWin() to ZoomWin2() or ZoomWin3(), as needed:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Interop;
namespace ZoomZoom
{
public class Commands
{
// Zoom to a window specified by the user
static public void ZoomWindow()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Get the window coordinates
PromptPointOptions ppo =
new PromptPointOptions(
"\nSpecify first corner:"
);
PromptPointResult ppr =
ed.GetPoint(ppo);
if (ppr.Status != PromptStatus.OK)
return;
Point3d min = ppr.Value;
PromptCornerOptions pco =
new PromptCornerOptions(
"\nSpecify opposite corner: ",
ppr.Value
);
ppr = ed.GetCorner(pco);
if (ppr.Status != PromptStatus.OK)
return;
Point3d max = ppr.Value;
// Call out helper function
// [Change this to ZoomWin2 or WoomWin3 to
// use different zoom techniques]
ZoomWin(ed, min, max);
}
// Zoom to the extents of an entity
static public void ZoomToEntity()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Get the entity to which we'll zoom
PromptEntityOptions peo =
new PromptEntityOptions(
"\nSelect an entity:"
);
PromptEntityResult per = ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
// Extract its extents
Extents3d ext;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
Entity ent =
(Entity)tr.GetObject(
per.ObjectId,
OpenMode.ForRead
);
ext =
ent.GeometricExtents;
tr.Commit();
}
ext.TransformBy(
ed.CurrentUserCoordinateSystem.Inverse()
);
// Call our helper function
// [Change this to ZoomWin2 or WoomWin3 to
// use different zoom techniques]
ZoomWin(ed, ext.MinPoint, ext.MaxPoint);
}
// Helper functions to zoom using different techniques
// Zoom using a view object
private static void ZoomWin(
Editor ed, Point3d min, Point3d max
)
{
Point2d min2d = new Point2d(min.X, min.Y);
Point2d max2d = new Point2d(max.X, max.Y);
ViewTableRecord view =
new ViewTableRecord();
view.CenterPoint =
min2d + ((max2d - min2d) / 2.0);
view.Height = max2d.Y - min2d.Y;
view.Width = max2d.X - min2d.X;
ed.SetCurrentView(view);
}
// Zoom via COM
private static void ZoomWin2(
Editor ed, Point3d min, Point3d max
)
{
AcadApplication app =
(AcadApplication)Application.AcadApplication;
double[] lower =
new double { min.X, min.Y, min.Z };
double[] upper
= new double { max.X, max.Y, max.Z };
app.ZoomWindow(lower, upper);
}
// Zoom by sending a command
private static void ZoomWin3(
Editor ed, Point3d min, Point3d max
)
{
string lower =
min.ToString().Substring(
1,
min.ToString().Length - 2
);
string upper =
max.ToString().Substring(
1,
max.ToString().Length - 2
);
string cmd =
"_.ZOOM _W " + lower + " " + upper + " ";
// Call the command synchronously using COM
AcadApplication app =
(AcadApplication)Application.AcadApplication;
app.ActiveDocument.SendCommand(cmd);
// Could also use async command calling:
//ed.Document.SendStringToExecute(
//cmd, true, false, true
//);
}
}
}
The code isn't as complete as I'd like: I haven't coded for all the view parameters (which are addressed in the DevNote quoted above in item 1, for those of you who are ADN members and can understand ObjectARX), and haven't tested for handling views not-aligned with the current UCS (etc., etc.). My main purpose was to outline the techniques, rather than to provide an exhaustive solution (and I'm currently on the train back from Geneva airport, having just flown to California and back for 3 days in the San Rafael office, which means any attempt on my side to make this exhaustive might end up exhausting me :-).
<p>四、.Net版本的BlockView例子</p><p>June 13, 2008<br/>AutoCAD .NET version of the ObjectARX BlockView sample<br/>Fenton Webb, from DevTech Americas, has been beavering away on a .NET port of the ObjectARX BlockView sample (found on the ObjectARX SDK under samples/graphics/BlockView). Thanks, Fents! :-)</p><p>Here is the C# source project for Visual Studio 2005. To build it you will almost certainly need to remap the acdbmgd.dll and acmgd.dll assembly references in the project.</p><p></p><p>One important note: if you load this project and try to view the BlockViewDialog in the Visual Studio Designer, the Visual Studio application will almost certainly crash. This is because the Designer is attempting to load into the dialog the GsPreviewCtrl component which depends on AutoCAD (and therefore cannot be instantiated outside of the AutoCAD process). At least that's what appears to be happening.</p><p>To temporarily allow the dialog to load - so you can make changes to the dialog's menu, etc. - you will need to edit the C# code in the BlockViewDialog.Designer.cs file. There are two edits that are needed - one that declares a variable for the control (lines 650-651) and one that instantiates it (lines 107-108):</p><p>Comment out line 650 and uncomment line 651, so...</p><p> 650 private GsPreviewCtrl mPreviewCtrl;</p><p> 651 //private System.Windows.Forms.Panel mPreviewCtrl;</p><p>... becomes...</p><p> 650 //private GsPreviewCtrl mPreviewCtrl;</p><p> 651 private System.Windows.Forms.Panel mPreviewCtrl;</p><p>And do the same for lines 107 and 108, so...</p><p> 107 //this.mPreviewCtrl = new System.Windows.Forms.Panel();</p><p> 108 this.mPreviewCtrl = new GsPreviewCtrl();</p><p>... becomes...</p><p> 107 this.mPreviewCtrl = new System.Windows.Forms.Panel();</p><p> 108 //this.mPreviewCtrl = new GsPreviewCtrl();</p><p>Then the dialog should load properly in the Visual Studio Designer, with no crash (although you'll need to change the code back to the way it was to build the application, of course).</p><p>As for what the application does, here's the original sample's ReadMe, updated to reflect the new application's behaviour:</p><p>------------------------</p><p>Block View Sample Readme</p><p>------------------------</p><p></p><p>- NETLOAD the BlockView.NET.dll module</p><p>- Run command BView at the AutoCAD command line.</p><p>- Default operation is to display the current drawing using the current view settings of the current drawing.</p><p></p><p>Function Descriptions</p><p>---------------------</p><p>File->Open</p><p>Opens an existing drawing into the Block View dialog (by clearing the GraphicsSystem.View).</p><p>File->Output Image to Disk</p><p>Allows you to output a JPG;BMP;TIFF;PNG files as a snaphot of the current view shown in the BlockView dialog.</p><p>File->AcGsDevice Config</p><p>View or edit the current GraphicsSystem.Device configuration settings.</p><p>View->Zoom</p><p>Allows Zoom Extents/Zoom Window/Zoom In-Out functionality</p><p>View->Settings->Show</p><p>Allows the toggling of various GraphicsSystem.View settings such as Linetype/Material and Sectioning</p><p>View->Render Mode</p><p>Allows you to switch the rendering mode.</p><p>View->View Style</p><p>Allows you to change the view style of the GraphicsSystem.View. It has the same options as the SHADEMODE command.</p><p>Tools->ReMap Colors->Custom</p><p>Allows you to remap the color palette. This is particularily useful if you want to show a GraphicsSystem.View as a paperspace layout, with a white background. In this case you will need to remap white entities to appear in a different color.</p><p>Tools->ReMap Colors->Standard Colors</p><p>Restores the Color Palette back to the original one.</p><p>Tools->Add an Entity->To This Database</p><p>Adds an entity to the Model Space and adds to the GraphicsSystem.View.</p><p>Tools->Add an Entity->Temporary</p><p>Adds an entity just to the GraphicsSystem.View.</p><p>Here's a quick snapshot of the BlockView dialog with a conceptual view of the standard 3D view sample:</p> 五、自定义捕捉模式
(实现对曲线1/4和3/4长的捕捉。)
October 31, 2008
Implementing a custom AutoCAD object snap mode using .NET
Thanks again to Augusto Gonçalves, from our DevTech Americas team, for providing the original VB.NET code for this sample, as well as helping investigate an issue I faced during implementation.
When I saw a recent reply to a developer, showing how to implement a custom object snap in AutoCAD using .NET, I had a really strong sense of nostalgia: it reminded me of a couple of early samples I contributed to the ObjectARX SDK: the "third" sample, which showed how to create a custom osnap that snapped to a third of the way along a curve, and "divisor" which generalised the approach to fractions of any size and was my first real attempt at using C++ templates. Ah, the memories. The samples were retired from this year's SDK, but were still included up to and including the ObjectARX SDK for AutoCAD 2008.
Anyway, the code Augusto sent was very familiar, and it turns out he based it on some documentation that was probably, in turn, based on my C++ sample. So it has come full circle. :-)
One thing I hadn't realised until I saw Augusto's email was that the ability to define custom object snaps had been exposed through .NET.
Here's the C# code that implements a new "quarter" object snap, which snaps to 1/4 and 3/4 along the length of a curve.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
[assembly:ExtensionApplication(
typeof(OsnapApp.CustomOSnapApp))
]
namespace OsnapApp
{
// Register and unregister custom osnap
public class CustomOSnapApp : IExtensionApplication
{
private QuarterOsnapInfo _info =
new QuarterOsnapInfo();
private QuarterGlyph _glyph =
new QuarterGlyph();
private CustomObjectSnapMode _mode;
public void Initialize()
{
// Register custom osnap on initialize
_mode =
new CustomObjectSnapMode(
"Quarter",
"Quarter",
"Quarter of length",
_glyph
);
// Which kind of entity will use the osnap
_mode.ApplyToEntityType(
RXObject.GetClass(typeof(Polyline)),
new AddObjectSnapInfo(_info.SnapInfoPolyline)
);
_mode.ApplyToEntityType(
RXObject.GetClass(typeof(Curve)),
new AddObjectSnapInfo(_info.SnapInfoCurve)
);
_mode.ApplyToEntityType(
RXObject.GetClass(typeof(Entity)),
new AddObjectSnapInfo(_info.SnapInfoEntity)
);
// Activate the osnap
CustomObjectSnapMode.Activate("_Quarter");
}
// Unregister custom osnap on terminate
public void Terminate()
{
CustomObjectSnapMode.Deactivate("_Quarter");
}
}
// Create new quarter object snap
public class QuarterGlyph : Glyph
{
private Point3d _pt;
public override void SetLocation(Point3d point)
{
_pt = point;
}
public override void ViewportDraw(ViewportDraw vd)
{
int glyphPixels =
CustomObjectSnapMode.GlyphSize;
Point2d glyphSize =
vd.Viewport.GetNumPixelsInUnitSquare(_pt);
// Calculate the size of the glyph in WCS
//(use for text height factor)
// We'll add 20% to the size, as otherwise
//it looks a little too small
double glyphHeight =
(glyphPixels / glyphSize.Y) * 1.2;
string text = "¼";
// Translate the X-axis of the DCS to WCS
//(for the text direction) and the snap
//point itself (for the text location)
Matrix3d e2w = vd.Viewport.EyeToWorldTransform;
Vector3d dir = Vector3d.XAxis.TransformBy(e2w);
Point3d pt = _pt.TransformBy(e2w);
//Draw the centered text representing the glyph
vd.Geometry.Text(
pt,
vd.Viewport.ViewDirection,
dir,
glyphHeight,
1,
0,
text
);
}
}
// OSnap info
public class QuarterOsnapInfo
{
public void SnapInfoEntity(
ObjectSnapContext context,
ObjectSnapInfo result)
{
// Nothing here
}
public void SnapInfoCurve(
ObjectSnapContext context,
ObjectSnapInfo result
)
{
// For any curve
Curve cv = context.PickedObject as Curve;
if (cv == null)
return;
double startParam = cv.StartParam;
double endParam = cv.EndParam;
// Add osnap at first quarter
double param =
startParam + ((endParam - startParam) * 0.25);
Point3d pt = cv.GetPointAtParameter(param);
result.SnapPoints.Add(pt);
// Add osnap at third quarter
param =
startParam + ((endParam - startParam) * 0.75);
pt = cv.GetPointAtParameter(param);
result.SnapPoints.Add(pt);
if (cv.Closed)
{
pt = cv.StartPoint;
result.SnapPoints.Add(pt);
}
}
public void SnapInfoPolyline(
ObjectSnapContext context,
ObjectSnapInfo result)
{
// For polylines
Polyline pl = context.PickedObject as Polyline;
if (pl == null)
return;
// Get the overall start and end parameters
double plStartParam = pl.StartParam;
double plEndParam = pl.EndParam;
// Get the local
double startParam = plStartParam;
double endParam = startParam + 1.0;
while (endParam <= plEndParam)
{
// Calculate the snap point per vertex...
// Add osnap at first quarter
double param =
startParam + ((endParam - startParam) * 0.25);
Point3d pt = pl.GetPointAtParameter(param);
result.SnapPoints.Add(pt);
// Add osnap at third quarter
param =
startParam + ((endParam - startParam) * 0.75);
pt = pl.GetPointAtParameter(param);
result.SnapPoints.Add(pt);
startParam = endParam;
endParam += 1.0;
}
}
}
}
Some comments on the implementation:
There's a blank callback that is the base implementation for entities
We then override that for all Curve objects, using some code to divide a curve into quarters
We do yet another implementation for all Polyline objects (which are Curves, but we want to treat them as a special case)
For Polylines we snap within segments
We could have implemented this by retrieving each segment and dividing that into quarters
Instead I chose to rely on the fact that a Polyline's parameter is a "whole number" at each vertex, which means the code is the same for any kind of segment
In my original sample I adjusted the position of the text, to centre it on the snap point
In this example I haven't done this, as when I looked at the code it wasn't accurate - when you zoomed in the text appeared in the wrong position
As we're just using a single character (¼) as our glyph, this isn't a significant problem
Here's what happens when we load our module and try snapping to a line inside AutoCAD:
<p>六、在绘图界面动态显示坐标</p><p>见[<a href="http://bbs.mjtd.com/forum.php?mod=viewthread&tid=75618">Jigs</a>]的相关例子之二</p><p>Drawing text planar to the screen inside AutoCAD's drawing window using .NET</p> 七、在3D视图中变换视角
February 25, 2009
Smoothly transitioning between 3D AutoCAD views using .NET - Part 1
This inspiration for this post came during the research for this previous post, where we looked at implementing LookAt inside AutoCAD. It also has roots in the need to perform smooth transitions when zooming inside AutoCAD, which the ZOOM command manages for transitions between 2D views. Fenton Webb, from our DevTech Americas team, kindly volunteered to put together an ObjectARX sample that formed the basis for the code in this post. A huge thanks to Fents for his hard work on this!
This code presents a technique that allows smooth transitioning between 3D views in AutoCAD: you set up the parameters of the view to which you wish to change, and the function takes care of the heavy lifting. The technique follows more-or-less the same approach than that used to implement the ViewCube's smooth view transitions. It creates a GraphicsSystem.View object and manipulates it to transition smoothly to the new view.
Here's the C# code:
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 ViewTransitions
{
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
static Matrix3d SmoothViewTo(
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)
{
// Interpolate position
intPos =
new Point3d(
CosInterp(cv.position.X, nv.position.X, mu),
CosInterp(cv.position.Y, nv.position.Y, mu),
CosInterp(cv.position.Z, nv.position.Z, mu)
);
// Interpolate target
intTgt =
new Point3d(
CosInterp(cv.target.X, nv.target.X, mu),
CosInterp(cv.target.Y, nv.target.Y, mu),
CosInterp(cv.target.Z, nv.target.Z, mu)
);
// Interpolate upVector
intUpVec =
new Vector3d(
CosInterp(cv.upVector.X, nv.upVector.X, mu),
CosInterp(cv.upVector.Y, nv.upVector.Y, mu),
CosInterp(cv.upVector.Z, nv.upVector.Z, mu)
);
// 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);
}
// 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();
}
}
static public void TransitionView()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db =
doc.Database;
ObjectId vtId = CreateBackground();
SmoothViewTo(defaultView, 10);
SmoothViewTo(SWIso, 10);
SmoothViewTo(topView, 10);
SmoothViewTo(SEIso, 10);
SmoothViewTo(bottomView, 10);
SmoothViewTo(NEIso, 10);
SmoothViewTo(leftView, 10);
SmoothViewTo(NWIso, 10);
SmoothViewTo(rightView, 10);
SmoothViewTo(defaultView, 10);
if (vtId != ObjectId.Null)
RemoveBackground(vtId);
}
}
}
A few comments on the code...
Fenton interpolates between views using his own "secret sauce", the CosInterp() function. This does some clever stuff to interpolate between the values provided. It's used to interpolate between the beginning and end states of the individual members of the co-ordinates of the various points and vectors - and the field width and height - that define a view.
I added some functionality to create a temporary background image attached to the active ViewportTableRecord with the same colour as the drawing canvas background (if in a standard 2D view). This allows the 3D view that gets created to have the same background colour, avoiding the shock of it flashing to grey and back. I admit that this code (in CreateBackground() and RemoveBackground()) doesn't feel especially elegant - I tried various different approaches such as modifying the view and the device attached to the view, but none of them worked in the way I wanted. So this is what I ended up with. I'd be very happy to hear from people who have found a better way to address this issue...
To see how the function works, draw some 3D geometry, load the application and run the TV command.
Here's a sample view prior to running the command:
And here's a snapshot I managed to take during the command, as the view was transitioning:
It's hard to do it justice with a static image, so the best is to give it a try.
I can see a few changes that people might want to make to the code:
the view definitions (to change their parameters or even to generate them dynamically).
the formula in the CosInterp() function.
the code towards the end of the SmoothViewTo() which pauses for a variable amount of time, depending on how close the view is from being transitioned.
When Fenton shared his original code within the DevTech team, Jeremy Tammik mentioned another interpolation algorithm based onquaternion mathematics,spherical linear interpolation (Slerp). In the next post in this series we'll take a look at a version implementing Slerp to do something very similar to the code in this post.
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 asspherical 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();
}
}
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 aquaternion 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.
<p>看样子,我要补习英语了</p> 学习了!谢谢!
页:
[1]
2