The overwhelming majority of workflows CAD Exchanger customers adopt the SDK for involve visualization of 3D models. Most often they already have some legacy visualization vehicle, are already capable to work with some simple formats (such as STL or just some simple in-house triangular meshes) and want to expand their app with a broader set of complex CAD formats (STEP, IGES, Parasolid, JT, ACIS and others). So they wonder how this task can be accomplished and hopefully this blog post should help save their time.
High-level workflow
Accomplishing the task of displaying a 3D model involves 3 steps:
- Creating a 3D scene and enabling interactive behavior
- Creating a polygonal representation from a 3D model
- Feeding the above polygonal representation into the 3D scene
Creating a 3D scene and enabling interactive behavior
This is effectively driven by the customer’s choice of a 3D library he/she will adopt in the application. If the legacy visualization vehicle already exists (which we assume the case) then this choice was already made some time ago. The options include:
- lower level direct OpenGL viewport and manual processing of mouse/keyboard events;
- higher-level libraries such as Qt or Open Scene Graph (in C++), Java3D in Java, WPF in C# and so on.
Regardless of the choice there is eventually some abstraction that represents a scene that is able to display arbitrary 3D objects specified in the form of meshes and support interactive camera behavior (rotation, zooming, panning, etc.).
In this blog post I will use the sample created a few months ago for our prospect who used Java programming language. They used some specific, perhaps in-house library but for the sake of clarity I used just the JavaFX library. Below is a short code excerpt demonstrating creation of a simple JavaFX-enabled 3D scene:
public class cadexjavafxviewer extends Application {
double mousePosX;
double mousePosY;
double mouseOldX;
double mouseOldY;
double mouseDeltaX;
double mouseDeltaY;
static {
try {
System.loadLibrary("CadExCore");
System.loadLibrary("CadExSTEP");
} catch (UnsatisfiedLinkError e) {
System.err.println("Native code library failed to load.\n" + e);
System.exit(1);
}
}
private PerspectiveCamera addCamera(Scene scene) {
PerspectiveCamera perspectiveCamera = new PerspectiveCamera(false /*true*/);
scene.setCamera(perspectiveCamera);
return perspectiveCamera;
}
public static void main(String[] args) {
if (args.length != 1) {
System.out.println ("Usage: " + " , where:");
System.out.println (" is a name of the STEP file to be read");
return;
}
launch(args);
}
@Override
public void start(Stage primaryStage) {
final Group aGroup = new Group();
Rotate rx = new Rotate();
rx.setAxis(Rotate.X_AXIS);
Rotate ry = new Rotate();
ry.setAxis(Rotate.Y_AXIS);
aGroup.getTransforms().addAll (rx, ry);
aGroup.setTranslateX(250);
aGroup.setTranslateY(250);
//render a scene
final Scene scene = new Scene (aGroup, 500, 500, true /*depthBuffer*/);
scene.setOnMousePressed(new EventHandler() {
@Override public void handle(MouseEvent event) {
mousePosX = event.getX();
mousePosY = event.getY();
mouseOldX = event.getX();
mouseOldY = event.getY();
}
});
scene.setOnMouseDragged(new EventHandler() {
@Override public void handle(MouseEvent event) {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = event.getX();
mousePosY = event.getY();
mouseDeltaX = mousePosX - mouseOldX;
mouseDeltaY = mousePosY - mouseOldY;
ry.setAngle (ry.getAngle() - mouseDeltaX);
rx.setAngle (rx.getAngle() + mouseDeltaY);
}
});
addCamera (scene);
primaryStage.setTitle ("CAD Exchanger JavaFX 3D viewer");
primaryStage.setScene (scene);
primaryStage.show();
}
}
Creating a polygonal representation from a 3D model
As mentioned above, a display vehicle is supposed to able to work with arbitrary meshes. The mesh is most often provided in the form of triangles which are specified as:
- array of vertices’ 3D coordinates (XYZ) and triplets of indices into that array;
- vertex normals (vectors associated with each vertex) for smooth shading;
- optionally colors (per vertex or per triangle).
Thus, there is a need to represent any 3D model saved in an external file (IGES, STEP, Parasolid, etc.) with the help of such a mesh prior to displaying it.
Depending on input format capabilities the file either already contains such a mesh or contains a precise geometrical definition of a 3D model. Examples of the former are STL, OBJ, VRML, X3D; examples of the latter – STEP, IGES, ACIS, Parasolid, Solidworks, etc. JT is capable to carry both mesh and precise definitions.
If the mesh (also known as polygonal or facetted representation) is not available then it needs to be generated from the precise one (also known as B-Rep, or Boundary Representation). This task is accomplished by the so-called meshers whose objective is exactly that – mesh generation.
CAD Exchanger comes with a few meshers serving different purposes but the visualization mesher serves the task of generation of a mesh tailored for visualization purposes. It favors higher performance and smaller memory footprint (in terms of triangle number) to triangle quality. There are other meshers with different trade-offs but we do not consider them here.
Importing a 3D file and generating a mesh are very easy with the help of CAD Exchanger SDK:
static boolean Import (String theSource, ModelData_Model theModel)
{
STEP_Reader aReader = new STEP_Reader();
return aReader.ReadFile (new Base_UTF16String (theSource))
&& aReader.Transfer (theModel);
}
String aSource = getParameters().getRaw().get (0);
ModelData_Model aModel = new ModelData_Model();
if (!Import (aSource, aModel)) {
System.out.println ("Failed to read the file " + aSource);
return;
}
//generate polygonal representations for parts
ModelAlgo_BRepMesherParameters aParameters = new
ModelAlgo_BRepMesherParameters (ModelAlgo_BRepMesherParameters.Granularity.Fine);
ModelAlgo_BRepMesher aMesher = new ModelAlgo_BRepMesher (aParameters);
aMesher.Compute (aModel);
After that, each part in the imported 3D model will contain a polygonal representation (i.e. a mesh) in addition to a precise B-Rep imported from the file. The mesh quality can be specified with the help of parameters, using either pre-defined settings (coarse/medium/fine granularity) or fine-tuned parameters (chordal and angular deviations) – see mesher parameters
Feeding the polygonal representation into the 3D scene
Once the polygonal representation (the mesh) has been generated it needs to be fed into the display vehicle.
This essentially involves traversing the scene graph (hierarchy of assemblies and parts), retrieving polygonal representations, applying transformations (accumulated during graph traversal) and sending data into the display vehicle API.
Graph traversal can be implemented using the scene graph visitors which are applied to each node (assembly, instance or part) in the scene graph. In our example we will use the subclass of ModelData_Model::VoidElementVisitor. Our visitor accumulates the transformations into the stack and creates a mesh for each part (i.e. leaf node in the scene graph):
class SceneGraphVisitor extends ModelData_Model.VoidElementVisitor
{
//resulting meshes, one for each IndexedTriangleSet
private List myMeshViews;
public SceneGraphVisitor()
{
ModelData_Transformation anIdentity = new ModelData_Transformation();
myApps = new Stack<>();
myApps.push (new ModelData_Appearance());
myTrsfs = new Stack<>();
myTrsfs.push (anIdentity);
myMeshViews = new ArrayList ();
}
List MeshViews()
{
return myMeshViews;
}
@Override
public void Apply (ModelData_Part thePart)
{
VisitElement (thePart);
//retrieve polygonal representation
ModelData_PolyRepresentation aPoly = thePart.PolyRepresentation (ModelData_RepresentationMask.ModelData_RM_Poly);
if (!aPoly.IsNull()) {
ModelData_Transformation aTrsf = CurrentTransformation();
ModelData_PolyShapeList aList = aPoly.Get();
for (long i = 0; i < aList.Size(); ++i) {
ModelData_PolyVertexSet aPVS = aList.Element(i);
if (aPVS.TypeId() != ModelData_IndexedTriangleSet.GetTypeId()) {
continue;
}
ModelData_IndexedTriangleSet anITS = ModelData_IndexedTriangleSet.static_cast (aPVS);
ModelData_Appearance anApp = CurrentAppearance();
myMeshViews.add (Helper.CreateMesh (anITS, anApp, aTrsf));
}
}
LeaveElement (thePart);
}
@Override
public boolean VisitEnter (ModelData_Assembly theAssembly)
{
VisitElement (theAssembly);
return true;
}
@Override
public void VisitLeave (ModelData_Assembly theAssembly)
{
LeaveElement (theAssembly);
}
@Override
public boolean VisitEnter (ModelData_Instance theInstance)
{
VisitElement (theInstance);
//get transformation matrix
ModelData_Transformation aTrsf = new ModelData_Transformation();
if (theInstance.HasTransformation()) { aTrsf = theInstance.Transformation();
}
ModelData_Transformation aCumulativeTrsf = CurrentTransformation().Multiply (aTrsf);
myTrsfs.push (aCumulativeTrsf);
return true;
}
@Override
public void VisitLeave (ModelData_Instance theInstance)
{
myTrsfs.pop();
LeaveElement (theInstance);
}
protected void VisitElement (ModelData_SceneGraphElement theElement)
{
ModelData_Appearance anApp = Helper.CombineAppearances (CurrentAppearance(), theElement.Appearance());
myApps.push (anApp);
}
protected void LeaveElement (ModelData_SceneGraphElement theElement)
{
myApps.pop();
}
protected ModelData_Transformation CurrentTransformation()
{
return myTrsfs.peek();
}
protected ModelData_Appearance CurrentAppearance()
{
return myApps.peek();
}
private final Stack myTrsfs;
private final Stack myApps;
}
The visitor also accumulates appearances (such as colors and materials) while traversing from the root down the tree, i.e. if a child has its own appearance it overrides the parent’s, otherwise it is inherited from the parent.
Once the meshes have been accumulated during traversing the scene graph, they are displayed into the scene:
//create triangular meshes from polygonal representations SceneGraphVisitor aSGVisitor = new SceneGraphVisitor();
aModel.Accept (aSGVisitor);
List aMeshList = aSGVisitor.MeshViews();
//populate a group
final Group aGroup = new Group();
for (int i = 0; i < aMeshList.size(); i++) {
MeshView aMeshView = aMeshList.get(i);
aMeshView.setCullFace (CullFace.NONE);
aGroup.getChildren().add (aMeshView);
}
Depending on the display vehicle’s API the way mesh data should be fed into it will vary. Some API (like in JavaFX) will allow independent indexing of vertex normal vs vertex coordinates. Some will require that there is one-to-one correspondence between them and so on. Anyway, using ModelData_IndexedTriangleSet API you should be able to retrieve data the way you need and feed into your display vehicle.
Please consult the attached source code of the JavaFX-based viewer code for a complete example and adjust to your particular case.
Hope this tutorial will give you general understanding of using CAD Exchanger SDK for displaying 3D models in your apps. Please feel free to drop a comment here or send us an email at info@cadexchanger.com.
Thanks for reading!