1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package com.javafx.experiments.importers.maya;
  33 
  34 import com.javafx.experiments.importers.SmoothingGroups;
  35 
  36 import java.io.File;
  37 import java.net.MalformedURLException;
  38 import java.net.URL;
  39 import java.util.ArrayList;
  40 import java.util.Collection;
  41 import java.util.HashMap;
  42 import java.util.LinkedList;
  43 import java.util.List;
  44 import java.util.Map;
  45 import java.util.TreeMap;
  46 import java.util.logging.Level;
  47 import java.util.logging.Logger;
  48 
  49 import javafx.animation.Interpolator;
  50 import javafx.animation.KeyFrame;
  51 import javafx.animation.KeyValue;
  52 import javafx.beans.property.DoubleProperty;
  53 import javafx.scene.DepthTest;
  54 import javafx.scene.Group;
  55 import javafx.scene.Node;
  56 import javafx.scene.image.Image;
  57 import javafx.scene.paint.Color;
  58 import javafx.scene.paint.PhongMaterial;
  59 import javafx.scene.shape.CullFace;
  60 import javafx.scene.shape.Mesh;
  61 import javafx.scene.shape.MeshView;
  62 import javafx.scene.shape.TriangleMesh;
  63 import javafx.scene.transform.Affine;
  64 import javafx.util.Duration;
  65 
  66 import com.javafx.experiments.importers.maya.parser.MParser;
  67 import com.javafx.experiments.importers.maya.values.MArray;
  68 import com.javafx.experiments.importers.maya.values.MBool;
  69 import com.javafx.experiments.importers.maya.values.MCompound;
  70 import com.javafx.experiments.importers.maya.values.MData;
  71 import com.javafx.experiments.importers.maya.values.MFloat;
  72 import com.javafx.experiments.importers.maya.values.MFloat2Array;
  73 import com.javafx.experiments.importers.maya.values.MFloat3;
  74 import com.javafx.experiments.importers.maya.values.MFloat3Array;
  75 import com.javafx.experiments.importers.maya.values.MFloatArray;
  76 import com.javafx.experiments.importers.maya.values.MInt;
  77 import com.javafx.experiments.importers.maya.values.MInt3Array;
  78 import com.javafx.experiments.importers.maya.values.MIntArray;
  79 import com.javafx.experiments.importers.maya.values.MPolyFace;
  80 import com.javafx.experiments.importers.maya.values.MString;
  81 import com.javafx.experiments.shape3d.PolygonMesh;
  82 import com.javafx.experiments.shape3d.PolygonMeshView;
  83 import com.javafx.experiments.shape3d.SkinningMesh;
  84 import com.sun.javafx.geom.Vec3f;
  85 
  86 import java.util.HashSet;
  87 import java.util.Set;
  88 import java.util.Arrays;
  89 
  90 import javafx.animation.AnimationTimer;
  91 import javafx.scene.Parent;
  92 
  93 /** Loader */
  94 class Loader {
  95     public static final boolean DEBUG = false;
  96     public static final boolean WARN = false;
  97 
  98     MEnv env;
  99 
 100     int startFrame;
 101     int endFrame;
 102 
 103     MNodeType transformType;
 104     MNodeType jointType;
 105     MNodeType meshType;
 106     MNodeType cameraType;
 107     MNodeType animCurve;
 108     MNodeType animCurveTA;
 109     MNodeType animCurveUA;
 110     MNodeType animCurveUL;
 111     MNodeType animCurveUT;
 112     MNodeType animCurveUU;
 113 
 114     MNodeType lambertType;
 115     MNodeType reflectType;
 116     MNodeType blinnType;
 117     MNodeType phongType;
 118     MNodeType fileType;
 119     MNodeType skinClusterType;
 120     MNodeType blendShapeType;
 121     MNodeType groupPartsType;
 122     MNodeType shadingEngineType;
 123 
 124     // [Note to Alex]: I've re-enabled joints, but lets not use rootJoint [John]
 125     // Joint rootJoint; //NO_JOINTS
 126     Map<MNode, Node> loaded = new HashMap<MNode, Node>();
 127 
 128     Map<Float, List<KeyValue>> keyFrameMap = new TreeMap();
 129 
 130     Map<Node, MNode> meshParents = new HashMap();
 131 
 132     private MFloat3Array mVerts;
 133     // Optionally force per-face per-vertex normal generation
 134     private int[] edgeData;
 135 
 136     private List<MData> uvSet;
 137     private int uvChannel;
 138     private MFloat3Array mPointTweaks;
 139     private URL url;
 140     private boolean asPolygonMesh;
 141 
 142     //=========================================================================
 143     // Loader.load
 144     //-------------------------------------------------------------------------
 145     // Called from MayaImporter.load
 146     //=========================================================================
 147     public void load(URL url, boolean asPolygonMesh) {
 148         this.url = url;
 149         this.asPolygonMesh = asPolygonMesh;
 150         env = new MEnv();
 151         MParser parser = new MParser(env);
 152         try {
 153             parser.parse(url);
 154             loadModel();
 155             for (MNode n : env.getNodes()) {
 156                 // System.out.println("____________________________________________________________");
 157                 // System.out.println("==> .......Node: " + n);
 158                 resolveNode(n);
 159             }
 160         } catch (Exception e) {
 161             if (WARN) System.err.println("Error loading url: [" + url + "]");
 162             throw new RuntimeException(e);
 163         }
 164     }
 165 
 166     //=========================================================================
 167     // Loader.loadModel
 168     //=========================================================================
 169     void loadModel() {
 170         startFrame = (int) Math.round(env.getPlaybackStart() - 1);
 171         endFrame = (int) Math.round(env.getPlaybackEnd() - 1);
 172         transformType = env.findNodeType("transform");
 173         jointType = env.findNodeType("joint");
 174         meshType = env.findNodeType("mesh");
 175         cameraType = env.findNodeType("camera");
 176         animCurve = env.findNodeType("animCurve");
 177         animCurveTA = env.findNodeType("animCurveTA");
 178         animCurveUA = env.findNodeType("animCurveUA");
 179         animCurveUL = env.findNodeType("animCurveUL");
 180         animCurveUT = env.findNodeType("animCurveUT");
 181         animCurveUU = env.findNodeType("animCurveUU");
 182 
 183         lambertType = env.findNodeType("lambert");
 184         reflectType = env.findNodeType("reflect");
 185         blinnType = env.findNodeType("blinn");
 186         phongType = env.findNodeType("phong");
 187         fileType = env.findNodeType("file");
 188         skinClusterType = env.findNodeType("skinCluster");
 189         groupPartsType = env.findNodeType("groupParts");
 190         shadingEngineType = env.findNodeType("shadingEngine");
 191         blendShapeType = env.findNodeType("blendShape");
 192     }
 193 
 194     //=========================================================================
 195     // Loader.resolveNode
 196     //-------------------------------------------------------------------------
 197     // Loader.resolveNode looks up MNode in the HashMap Map<MNode, Node> loaded
 198     // and returns the Node to which this map maps the MNode.
 199     // Also, if the node that its looking up hasn't been processed yet,
 200     // it processes the node.
 201     //=========================================================================
 202     Node resolveNode(MNode n) {
 203         // System.out.println("--> resolveNode: " + n);
 204         // if the node hasn't already been processed, then process the node
 205         if (!loaded.containsKey(n)) {
 206             // System.out.println("--> containsKey: " + n);
 207             processNode(n);
 208             // System.out.println("    loaded.get(n) " + loaded.get(n));
 209         }
 210         return loaded.get(n);
 211     }
 212 
 213     //=========================================================================
 214     // Loader.processNode
 215     //=========================================================================
 216     void processNode(MNode n) {
 217         Group parentNode = null;
 218         for (MNode p : n.getParentNodes()) {
 219             parentNode = (Group) resolveNode(p);
 220         }
 221         Node result = loaded.get(n);
 222         // if the result is null, then it hasn't been added to the map yet
 223         // so go ahead and process it
 224         if (result == null) {
 225             if (n.isInstanceOf(shadingEngineType)) {
 226                 //                System.out.println("==> Found a node of shadingEngineType: " + n);
 227             } else if (n.isInstanceOf(lambertType)) {
 228                 //                System.out.println("==> Found a node of lambertType: " + n);
 229             } else if (n.isInstanceOf(reflectType)) {
 230                 //                System.out.println("==> Found a node of reflectType: " + n);
 231             } else if (n.isInstanceOf(blinnType)) {
 232                 //                System.out.println("==> Found a node of blinnType: " + n);
 233             } else if (n.isInstanceOf(phongType)) {
 234                 //                System.out.println("==> Found a node of phongType: " + n);
 235             } else if (n.isInstanceOf(fileType)) {
 236                 //                System.out.println("==> Found a node of fileType: " + n);
 237             } else if (n.isInstanceOf(skinClusterType)) {
 238                 processClusterType(n);
 239             } else if (n.isInstanceOf(meshType)) {
 240                 processMeshType(n, parentNode);
 241             } else if (n.isInstanceOf(jointType)) {
 242                 processJointType(n, parentNode);
 243             } else if (n.isInstanceOf(transformType)) {
 244                 processTransformType(n, parentNode);
 245             } else if (n.isInstanceOf(animCurve)) {
 246                 processAnimCurve(n);
 247             }
 248         }
 249     }
 250 
 251     protected void processClusterType(MNode n) {
 252         loaded.put(n, null);
 253         MArray ma = (MArray) n.getAttr("ma");
 254 
 255         List<Joint> jointNodes = new ArrayList<Joint>();
 256         Set<Parent> jointForest = new HashSet<Parent>(); // root's children that have joints in their trees
 257         for (int i = 0; i < ma.getSize(); i++) {
 258             // hack... ?
 259             MNode c = n.getIncomingConnectionToType("ma[" + i + "]", "joint");
 260             Joint jn = (Joint) resolveNode(c);
 261             jointNodes.add(jn);
 262             
 263             Parent rootChild = jn; // root's child, which is an ancestor of joint jn
 264             while (rootChild.getParent() != null) {
 265                 rootChild = rootChild.getParent();
 266             }
 267             jointForest.add(rootChild);
 268         }
 269         
 270         MNode outputMeshMNode = resolveOutputMesh(n);
 271         MNode inputMeshMNode = resolveInputMesh(n);
 272         if (inputMeshMNode == null || outputMeshMNode == null) {
 273             return;
 274         }
 275         // We must be able to find the original converter in the meshConverters map
 276         MNode origOrigMesh = resolveOrigInputMesh(n);
 277         //               println("ORIG ORIG={origOrigMesh}");
 278         
 279         // TODO: What is with this? origMesh
 280         resolveNode(origOrigMesh).setVisible(false);
 281 
 282         MArray bindPreMatrixArray = (MArray) n.getAttr("pm");
 283         Affine bindGlobalMatrix = convertMatrix((MFloatArray) n.getAttr("gm"));
 284 
 285         Affine[] bindPreMatrix = new Affine[bindPreMatrixArray.getSize()];
 286         for (int i = 0; i < bindPreMatrixArray.getSize(); i++) {
 287             bindPreMatrix[i] = convertMatrix((MFloatArray) bindPreMatrixArray.getData(i));
 288         }
 289 
 290         MArray mayaWeights = (MArray) n.getAttr("wl");
 291         float[][] weights = new float [jointNodes.size()][mayaWeights.getSize()];
 292         for (int i=0; i<mayaWeights.getSize(); i++) {
 293             MFloatArray curWeights = (MFloatArray) mayaWeights.getData(i).getData("w");
 294             for (int j = 0; j < jointNodes.size(); j++) {
 295                 weights[j][i] = j < curWeights.getSize() ? curWeights.get(j) : 0;
 296             }
 297         }
 298         
 299         Node sourceMayaMeshNode = resolveNode(inputMeshMNode);
 300         Node targetMayaMeshNode = resolveNode(outputMeshMNode);
 301         
 302         if (sourceMayaMeshNode.getClass().equals(PolygonMeshView.class)) {
 303             PolygonMeshView sourceMayaMeshView = (PolygonMeshView) sourceMayaMeshNode;
 304             PolygonMeshView targetMayaMeshView = (PolygonMeshView) targetMayaMeshNode;
 305             
 306             PolygonMesh sourceMesh = (PolygonMesh) sourceMayaMeshView.getMesh();
 307             SkinningMesh targetMesh = new SkinningMesh(sourceMesh, weights, bindPreMatrix, bindGlobalMatrix, jointNodes, new ArrayList(jointForest));
 308             targetMayaMeshView.setMesh(targetMesh);
 309 
 310             final SkinningMeshTimer skinningMeshTimer = new SkinningMeshTimer(targetMesh);
 311             if (targetMayaMeshNode.getScene() != null) {
 312                 skinningMeshTimer.start();
 313             }
 314             targetMayaMeshView.sceneProperty().addListener((observable, oldValue, newValue) -> {
 315                 if (newValue == null) {
 316                     skinningMeshTimer.stop();
 317                 } else {
 318                     skinningMeshTimer.start();
 319                 }
 320             });
 321         } else {
 322             Logger.getLogger(MayaImporter.class.getName()).log(Level.INFO, "Mesh skinning is not supported for triangle meshes. Select the 'Load as Polygons' option to load the mesh as polygon mesh.");
 323             MeshView sourceMayaMeshView = (MeshView) sourceMayaMeshNode;
 324             MeshView targetMayaMeshView = (MeshView) targetMayaMeshNode;
 325             TriangleMesh sourceMesh = (TriangleMesh) sourceMayaMeshView.getMesh();
 326             TriangleMesh targetMesh = (TriangleMesh) targetMayaMeshView.getMesh();
 327             targetMesh.getPoints().setAll(sourceMesh.getPoints());
 328             targetMesh.getTexCoords().setAll(sourceMesh.getTexCoords());
 329             targetMesh.getFaces().setAll(sourceMesh.getFaces());
 330             targetMesh.getFaceSmoothingGroups().setAll(sourceMesh.getFaceSmoothingGroups());
 331         }
 332     }
 333     
 334     private class SkinningMeshTimer extends AnimationTimer {
 335         private SkinningMesh mesh;
 336         SkinningMeshTimer(SkinningMesh mesh) {
 337             this.mesh = mesh;
 338         }
 339         @Override
 340         public void handle(long l) {
 341             mesh.update();
 342         }
 343     }
 344 
 345     protected Image loadImageFromFtnAttr(MNode fileNode, String name) {
 346         Image image = null;
 347         MString fileName = (MString) fileNode.getAttr("ftn");
 348         String imageFilename = (String) fileName.get();
 349         try {
 350             File file = new File(imageFilename);
 351             String filePath;
 352             if (file.exists()) {
 353                 filePath = file.toURI().toString();
 354             } else {
 355                 filePath = new URL(url, imageFilename).toString();
 356             }
 357             image = new Image(filePath);
 358             if (DEBUG) {
 359                 System.out.println(name + " = " + filePath);
 360                 System.out.println(name + " w = " + image.getWidth() + " h = " + image.getHeight());
 361             }
 362         } catch (MalformedURLException ex) {
 363             Logger.getLogger(MayaImporter.class.getName()).log(Level.SEVERE, "Failed to load " + name + " '" + imageFilename + "'!", ex);
 364         }
 365         return image;
 366     }
 367 
 368     protected void processMeshType(MNode n, Group parentNode) throws RuntimeException {
 369         //=============================================================
 370         // When JavaFX supports polygon mesh geometry,
 371         // add the polygon mesh geometry here.
 372         // Until then, add a unit square as a placeholder.
 373         //=============================================================
 374         Node node = resolveNode(n.getParentNodes().get(0));
 375         //                if (node != null) {
 376         //                if (node != null && !n.getName().endsWith("Orig")) {
 377         // Original approach to mesh placeholder:
 378         //                     meshParents.put(node, n);
 379 
 380         // Try to find an image or color from n (MNode)
 381         if (DEBUG) { System.out.println("________________________________________"); }
 382         if (DEBUG) { System.out.println("n.getName(): " + n.getName()); }
 383         if (DEBUG) { System.out.println("n.getNodeType(): " + n.getNodeType()); }
 384         MNode shadingGroup = n.getOutgoingConnectionToType("iog", "shadingEngine", true);
 385         MNode mat;
 386         MNode mFile;
 387         if (DEBUG) { System.out.println("shadingGroup: " + shadingGroup); }
 388 
 389         MFloat3 mColor;
 390         Vec3f diffuseColor = null;
 391         Vec3f specularColor = null;
 392 
 393         Image diffuseImage = null;
 394         Image normalImage = null;
 395         Image specularImage = null;
 396         Float specularPower = null;
 397 
 398         if (shadingGroup != null) {
 399             mat = shadingGroup.getIncomingConnectionToType("ss", "lambert");
 400             if (mat != null) {
 401                 // shader = shaderMap.get(mat.getName()) as FixedFunctionShader;
 402                 if (DEBUG) { System.out.println("lambert mat: " + mat); }
 403                 mColor = (MFloat3) mat.getAttr("c");
 404                 float diffuseIntensity = ((MFloat) mat.getAttr("dc")).get();
 405                 if (mColor != null) {
 406                     diffuseColor = new Vec3f(
 407                             mColor.get()[0] * diffuseIntensity,
 408                             mColor.get()[1] * diffuseIntensity,
 409                             mColor.get()[2] * diffuseIntensity);
 410                     if (DEBUG) { System.out.println("diffuseColor = " + diffuseColor); }
 411                 }
 412 
 413                 mFile = mat.getIncomingConnectionToType("c", "file");
 414                 if (mFile != null) {
 415                     diffuseImage = loadImageFromFtnAttr(mFile, "diffuseImage");
 416                 }
 417                 MNode bump2d = mat.getIncomingConnectionToType("n", "bump2d");
 418                 if (bump2d != null) {
 419                     mFile = bump2d.getIncomingConnectionToType("bv", "file");
 420                     if (mFile != null) {
 421                         normalImage = loadImageFromFtnAttr(mFile, "normalImage");
 422                     }
 423                 }
 424             }
 425             mat = shadingGroup.getIncomingConnectionToType("ss", "phong");
 426             if (mat != null) {
 427                 // shader = shaderMap.get(mat.getName()) as FixedFunctionShader;
 428                 if (DEBUG) { System.out.println("phong mat: " + mat); }
 429                 mColor = (MFloat3) mat.getAttr("sc");
 430                 if (mColor != null) {
 431                     specularColor = new Vec3f(
 432                             mColor.get()[0],
 433                             mColor.get()[1],
 434                             mColor.get()[2]);
 435                     if (DEBUG) { System.out.println("specularColor = " + specularColor); }
 436                 }
 437                 mFile = mat.getIncomingConnectionToType("sc", "file");
 438                 if (mFile != null) {
 439                     specularImage = loadImageFromFtnAttr(mFile, "specularImage");
 440                 }
 441 
 442                 specularPower = ((MFloat) mat.getAttr("cp")).get();
 443                 if (DEBUG) { System.out.println("specularPower = " + specularPower); }
 444             }
 445         }
 446 
 447         PhongMaterial material = new PhongMaterial();
 448 
 449         if (diffuseImage != null) {
 450             material.setDiffuseMap(diffuseImage);
 451             material.setDiffuseColor(Color.WHITE);
 452         } else {
 453             if (diffuseColor != null) {
 454                 material.setDiffuseColor(
 455                         new Color(
 456                                 diffuseColor.x,
 457                                 diffuseColor.y,
 458                                 diffuseColor.z, 1));
 459                 //                            material.setDiffuseColor(new Color(
 460                 //                                    0.5,
 461                 //                                    0.5,
 462                 //                                    0.5, 0));
 463             } else {
 464                 material.setDiffuseColor(Color.GRAY);
 465             }
 466         }
 467 
 468         if (normalImage != null) {
 469             material.setBumpMap(normalImage);
 470         }
 471 
 472         if (specularImage != null) {
 473             material.setSpecularMap(specularImage);
 474         } else {
 475             if (specularColor != null && specularPower != null) {
 476                 material.setSpecularColor(
 477                         new Color(
 478                                 specularColor.x,
 479                                 specularColor.y,
 480                                 specularColor.z, 1));
 481                 material.setSpecularPower(specularPower / 33);
 482                 //                            material.setSpecularColor(new Color(
 483                 //                                    0,
 484                 //                                    1,
 485                 //                                    0, 1));
 486                 //                            material.setSpecularPower(1);
 487             } else {
 488                 //                            material.setSpecularColor(new Color(
 489                 //                                    0.2,
 490                 //                                    0.2,
 491                 //                                    0.2, 1));
 492                 //                            material.setSpecularPower(1);
 493                 material.setSpecularColor(null);
 494             }
 495         }
 496 
 497         Object mesh = convertToFXMesh(n);
 498 
 499         if (asPolygonMesh) {
 500             PolygonMeshView mv = new PolygonMeshView();
 501             mv.setId(n.getName());
 502             mv.setMaterial(material);
 503             mv.setMesh((PolygonMesh) mesh);
 504 //            mv.setCullFace(CullFace.NONE); //TODO
 505             loaded.put(n, mv);
 506             if (node != null) {
 507                 ((Group) node).getChildren().add(mv);
 508             }
 509         } else {
 510             MeshView mv = new MeshView();
 511             mv.setId(n.getName());
 512             mv.setMaterial(material);
 513 
 514 //            // TODO HACK for [JIRA] (RT-30449) FX 8 3D: Need to handle mirror transformation (flip culling);
 515 //            mv.setCullFace(CullFace.FRONT);
 516 
 517             mv.setMesh((TriangleMesh) mesh);
 518 
 519             loaded.put(n, mv);
 520             if (node != null) {
 521                 ((Group) node).getChildren().add(mv);
 522             }
 523         }
 524     }
 525     
 526     protected void processJointType(MNode n, Group parentNode) {
 527         // [Note to Alex]: I've re-enabled joints, but not skinning yet [John]
 528         Node result;
 529         MFloat3 t = (MFloat3) n.getAttr("t");
 530         MFloat3 jo = (MFloat3) n.getAttr("jo");
 531         MFloat3 r = (MFloat3) n.getAttr("r");
 532         MFloat3 s = (MFloat3) n.getAttr("s");
 533         String id = n.getName();
 534 
 535         Joint j = new Joint();
 536         j.setId(id);
 537 
 538         // There's various ways to get the same thing:
 539         // n.getAttr("r").get()[0]
 540         // n.getAttr("r").getX()
 541         // n.getAttr("rx")
 542         // Up to you which you prefer
 543 
 544         j.t.setX(t.get()[0]);
 545         j.t.setY(t.get()[1]);
 546         j.t.setZ(t.get()[2]);
 547 
 548         // if ssc (Segment Scale Compensate) is false, then it is = 1, 1, 1
 549         boolean ssc = ((MBool) n.getAttr("ssc")).get();
 550         if (ssc) {
 551             List<MNode> parents = n.getParentNodes();
 552             if (parents.size() > 0) {
 553                 MFloat3 parent_s = (MFloat3) n.getParentNodes().get(0).getAttr("s");
 554                 j.is.setX(1f / parent_s.getX());
 555                 j.is.setY(1f / parent_s.getY());
 556                 j.is.setZ(1f / parent_s.getZ());
 557             } else {
 558                 j.is.setX(1f);
 559                 j.is.setY(1f);
 560                 j.is.setZ(1f);
 561             }
 562         } else {
 563             j.is.setX(1f);
 564             j.is.setY(1f);
 565             j.is.setZ(1f);
 566         }
 567 
 568         /*
 569         // This code doesn't seem to work right:
 570         MFloat jox = (MFloat) n.getAttr("jox");
 571         MFloat joy = (MFloat) n.getAttr("joy");
 572         MFloat joz = (MFloat) n.getAttr("joz");
 573         j.jox.setAngle(jox.get());
 574         j.joy.setAngle(joy.get());
 575         j.joz.setAngle(joz.get());
 576         // The following code works right:
 577         */
 578 
 579         if (jo != null) {
 580             j.jox.setAngle(jo.getX());
 581             j.joy.setAngle(jo.getY());
 582             j.joz.setAngle(jo.getZ());
 583         } else {
 584             j.jox.setAngle(0f);
 585             j.joy.setAngle(0f);
 586             j.joz.setAngle(0f);
 587         }
 588 
 589         MFloat rx = (MFloat) n.getAttr("rx");
 590         MFloat ry = (MFloat) n.getAttr("ry");
 591         MFloat rz = (MFloat) n.getAttr("rz");
 592         j.rx.setAngle(rx.get());
 593         j.ry.setAngle(ry.get());
 594         j.rz.setAngle(rz.get());
 595 
 596         j.s.setX(s.get()[0]);
 597         j.s.setY(s.get()[1]);
 598         j.s.setZ(s.get()[2]);
 599 
 600         result = j;
 601         // Add the Joint to the map
 602         loaded.put(n, j);
 603         j.setDepthTest(DepthTest.ENABLE);
 604         // Add the Joint to its JavaFX parent
 605         if (parentNode != null) {
 606             parentNode.getChildren().add(j);
 607             if (DEBUG) System.out.println("j.getDepthTest() : " + j.getDepthTest());
 608         }
 609         if (parentNode == null || !(parentNode instanceof Joint)) {
 610             // [Note to Alex]: I've re-enabled joints, but lets not use rootJoint [John]
 611             // rootJoint = j;
 612         }
 613     }
 614 
 615     protected void processTransformType(MNode n, Group parentNode) {
 616         MFloat3 t = (MFloat3) n.getAttr("t");
 617         MFloat3 r = (MFloat3) n.getAttr("r");
 618         MFloat3 s = (MFloat3) n.getAttr("s");
 619         String id = n.getName();
 620         // ignore cameras
 621         if ("persp".equals(id) ||
 622                 "top".equals(id) ||
 623                 "front".equals(id) ||
 624                 "side".equals(id)) {
 625             return;
 626         }
 627 
 628         MayaGroup mGroup = new MayaGroup();
 629         mGroup.setId(n.getName());
 630         // g.setBlendMode(BlendMode.SRC_OVER);
 631 
 632         // if (DEBUG) System.out.println("t = " + t);
 633         // if (DEBUG) System.out.println("r = " + r);
 634         // if (DEBUG) System.out.println("s = " + s);
 635 
 636         mGroup.t.setX(t.get()[0]);
 637         mGroup.t.setY(t.get()[1]);
 638         mGroup.t.setZ(t.get()[2]);
 639 
 640         MFloat rx = (MFloat) n.getAttr("rx");
 641         MFloat ry = (MFloat) n.getAttr("ry");
 642         MFloat rz = (MFloat) n.getAttr("rz");
 643         mGroup.rx.setAngle(rx.get());
 644         mGroup.ry.setAngle(ry.get());
 645         mGroup.rz.setAngle(rz.get());
 646 
 647         mGroup.s.setX(s.get()[0]);
 648         mGroup.s.setY(s.get()[1]);
 649         mGroup.s.setZ(s.get()[2]);
 650 
 651         MFloat rptx = (MFloat) n.getAttr("rptx");
 652         MFloat rpty = (MFloat) n.getAttr("rpty");
 653         MFloat rptz = (MFloat) n.getAttr("rptz");
 654         mGroup.rpt.setX(rptx.get());
 655         mGroup.rpt.setY(rpty.get());
 656         mGroup.rpt.setZ(rptz.get());
 657 
 658         MFloat rpx = (MFloat) n.getAttr("rpx");
 659         MFloat rpy = (MFloat) n.getAttr("rpy");
 660         MFloat rpz = (MFloat) n.getAttr("rpz");
 661         mGroup.rp.setX(rpx.get());
 662         mGroup.rp.setY(rpy.get());
 663         mGroup.rp.setZ(rpz.get());
 664 
 665         mGroup.rpi.setX(-rpx.get());
 666         mGroup.rpi.setY(-rpy.get());
 667         mGroup.rpi.setZ(-rpz.get());
 668 
 669         MFloat sptx = (MFloat) n.getAttr("sptx");
 670         MFloat spty = (MFloat) n.getAttr("spty");
 671         MFloat sptz = (MFloat) n.getAttr("sptz");
 672         mGroup.spt.setX(sptx.get());
 673         mGroup.spt.setY(spty.get());
 674         mGroup.spt.setZ(sptz.get());
 675 
 676         MFloat spx = (MFloat) n.getAttr("spx");
 677         MFloat spy = (MFloat) n.getAttr("spy");
 678         MFloat spz = (MFloat) n.getAttr("spz");
 679         mGroup.sp.setX(spx.get());
 680         mGroup.sp.setY(spy.get());
 681         mGroup.sp.setZ(spz.get());
 682 
 683         mGroup.spi.setX(-spx.get());
 684         mGroup.spi.setY(-spy.get());
 685         mGroup.spi.setZ(-spz.get());
 686 
 687         // Add the MayaGroup to the map
 688         loaded.put(n, mGroup);
 689         // Add the MayaGroup to its JavaFX parent
 690         if (parentNode != null) {
 691             parentNode.getChildren().add(mGroup);
 692         }
 693     }
 694 
 695     protected void processAnimCurve(MNode n) {
 696         // if (DEBUG) System.out.println("processing anim curve");
 697         List<MPath> toPaths = n.getPathsConnectingFrom("o");
 698         loaded.put(n, null);
 699         for (MPath path : toPaths) {
 700             MNode toNode = path.getTargetNode();
 701             // if (DEBUG) System.out.println("toNode = "+ toNode.getNodeType());
 702             if (toNode.isInstanceOf(transformType)) {
 703                 Node to = resolveNode(toNode);
 704                 if (to instanceof MayaGroup) {
 705                     MayaGroup g = (MayaGroup) to;
 706                     DoubleProperty ref = null;
 707                     String s = path.getComponentSelector();
 708                     // if (DEBUG) System.out.println("selector = " + s);
 709                     if ("t[0]".equals(s)) {
 710                         ref = g.t.xProperty();
 711                     } else if ("t[1]".equals(s)) {
 712                         ref = g.t.yProperty();
 713                     } else if ("t[2]".equals(s)) {
 714                         ref = g.t.zProperty();
 715                     } else if ("s[0]".equals(s)) {
 716                         ref = g.s.xProperty();
 717                     } else if ("s[1]".equals(s)) {
 718                         ref = g.s.yProperty();
 719                     } else if ("s[2]".equals(s)) {
 720                         ref = g.s.zProperty();
 721                     } else if ("r[0]".equals(s)) {
 722                         ref = g.rx.angleProperty();
 723                     } else if ("r[1]".equals(s)) {
 724                         ref = g.ry.angleProperty();
 725                     } else if ("r[2]".equals(s)) {
 726                         ref = g.rz.angleProperty();
 727                     } else if ("rp[0]".equals(s)) {
 728                         ref = g.rp.xProperty();
 729                     } else if ("rp[1]".equals(s)) {
 730                         ref = g.rp.yProperty();
 731                     } else if ("rp[2]".equals(s)) {
 732                         ref = g.rp.zProperty();
 733                     } else if ("sp[0]".equals(s)) {
 734                         ref = g.sp.xProperty();
 735                     } else if ("sp[1]".equals(s)) {
 736                         ref = g.sp.yProperty();
 737                     } else if ("sp[2]".equals(s)) {
 738                         ref = g.sp.zProperty();
 739                     }
 740                     // Note: may also want to consider adding rpt in addition to rp and sp
 741                     if (ref != null) {
 742                         convertAnimCurveRange(n, ref, true);
 743                     }
 744                 }
 745                 // [Note to Alex]: I've re-enabled joints, but not skinning yet [John]
 746                 if (to instanceof Joint) {
 747                     Joint j = (Joint) to;
 748                     DoubleProperty ref = null;
 749                     String s = path.getComponentSelector();
 750                     // if (DEBUG) System.out.println("selector = " + s);
 751                     if ("t[0]".equals(s)) {
 752                         ref = j.t.xProperty();
 753                     } else if ("t[1]".equals(s)) {
 754                         ref = j.t.yProperty();
 755                     } else if ("t[2]".equals(s)) {
 756                         ref = j.t.zProperty();
 757                     } else if ("s[0]".equals(s)) {
 758                         ref = j.s.xProperty();
 759                     } else if ("s[1]".equals(s)) {
 760                         ref = j.s.yProperty();
 761                     } else if ("s[2]".equals(s)) {
 762                         ref = j.s.zProperty();
 763                     } else if ("jo[0]".equals(s)) {
 764                         ref = j.jox.angleProperty();
 765                     } else if ("jo[1]".equals(s)) {
 766                         ref = j.joy.angleProperty();
 767                     } else if ("jo[2]".equals(s)) {
 768                         ref = j.joz.angleProperty();
 769                     } else if ("r[0]".equals(s)) {
 770                         ref = j.rx.angleProperty();
 771                     } else if ("r[1]".equals(s)) {
 772                         ref = j.ry.angleProperty();
 773                     } else if ("r[2]".equals(s)) {
 774                         ref = j.rz.angleProperty();
 775                     }
 776                     if (ref != null) {
 777                         convertAnimCurveRange(n, ref, true);
 778                     }
 779                 }
 780                 break;
 781             }
 782         }
 783     }
 784 
 785     private Object convertToFXMesh(MNode n) {
 786         mVerts = (MFloat3Array) n.getAttr("vt");
 787         MPolyFace mPolys = (MPolyFace) n.getAttr("fc");
 788         mPointTweaks = (MFloat3Array) n.getAttr("pt");
 789         MInt3Array mEdges = (MInt3Array) n.getAttr("ed");
 790         edgeData = mEdges.get();
 791         uvSet = ((MArray) n.getAttr("uvst")).get();
 792         String currentUVSet = ((MString) n.getAttr("cuvs")).get();
 793         for (int i = 0; i < uvSet.size(); i++) {
 794             if (((MString) uvSet.get(i).getData("uvsn")).get().equals(currentUVSet)) {
 795                 uvChannel = i;
 796             }
 797         }
 798 
 799         if (mPolys.getFaces() == null) {
 800             if (asPolygonMesh) {
 801                 return new PolygonMesh();
 802             } else {
 803                 return new TriangleMesh();
 804             }
 805         }
 806 
 807         MFloat3Array normals = (MFloat3Array) n.getAttr("n");
 808         return buildMeshData(mPolys.getFaces(), normals);
 809     }
 810 
 811     private int edgeVert(int edgeNumber, boolean start) {
 812         boolean reverse = (edgeNumber < 0);
 813         if (reverse) {
 814             edgeNumber = reverse(edgeNumber);
 815             return edgeData[3 * edgeNumber + (start ? 1 : 0)];
 816         } else {
 817             return edgeData[3 * edgeNumber + (start ? 0 : 1)];
 818         }
 819     }
 820 
 821     private int reverse(int edge) {
 822         if (edge < 0) {
 823             return -edge - 1;
 824         }
 825         return edge;
 826     }
 827 
 828     private boolean edgeIsSmooth(int edgeNumber) {
 829         edgeNumber = reverse(edgeNumber);
 830         return edgeData[3 * edgeNumber + 2] != 0;
 831     }
 832 
 833     private int edgeStart(int edgeNumber) {
 834         return edgeVert(edgeNumber, true);
 835     }
 836 
 837     private int edgeEnd(int edgeNumber) {
 838         return edgeVert(edgeNumber, false);
 839     }
 840 
 841     private float[] getTexCoords(int uvChannel) {
 842         if (uvSet == null || uvChannel < 0 || uvChannel >= uvSet.size()) {
 843             return new float[] {0,0};
 844         }
 845         MCompound compound = (MCompound) uvSet.get(uvChannel);
 846         MFloat2Array uvs = (MFloat2Array) compound.getFieldData("uvsp");
 847         if (uvs == null || uvs.get() == null) {
 848             return new float[] {0,0};
 849         }
 850 
 851         float[] texCoords = new float[uvs.getSize() * 2];
 852         float[] uvsData = uvs.get();
 853         for (int i = 0; i < uvs.getSize(); i++) {
 854             //note the 1 - v
 855             texCoords[i * 2] = uvsData[2 * i];
 856             texCoords[i * 2 + 1] = 1 - uvsData[2 * i + 1];
 857         }
 858         return texCoords;
 859     }
 860 
 861     private void getVert(int index, Vec3f vert) {
 862         float[] verts = mVerts.get();
 863         float[] tweaks = null;
 864         if (mPointTweaks != null) {
 865             tweaks = mPointTweaks.get();
 866             if (tweaks != null) {
 867                 if ((3 * index + 2) >= tweaks.length) {
 868                     tweaks = null;
 869                 }
 870             }
 871         }
 872         if (tweaks == null) {
 873             vert.set(verts[3 * index + 0], verts[3 * index + 1], verts[3 * index + 2]);
 874         } else {
 875             vert.set(
 876                     verts[3 * index + 0] + tweaks[3 * index + 0],
 877                     verts[3 * index + 1] + tweaks[3 * index + 1],
 878                     verts[3 * index + 2] + tweaks[3 * index + 2]);
 879         }
 880     }
 881 
 882     float FPS = 24.0f;
 883     float TAN_FIXED = 1;
 884     float TAN_LINEAR = 2;
 885     float TAN_FLAT = 3;
 886     float TAN_STEPPED = 5;
 887     float TAN_SPLINE = 9;
 888     float TAN_CLAMPED = 10;
 889     float TAN_PLATEAU = 16;
 890 
 891     // Experimentally trying to land the frames on whole frame values
 892     // Duration is still double, but internally, in Animation/Timeline,
 893     // the time is discrete.  6000 units per second.
 894     // Without this EPSILON, the frames might not land on whole frame values.
 895     // 0.000001f seems to work for now
 896     // 0.0000001f was too small on a trial run
 897     static final float EPSILON = 0.000001f;
 898 
 899     static final float MAXIMUM = 10000000.0f;
 900 
 901     // Empirically derived from playing with animation curve editor
 902     float TAN_EPSILON = 0.05f;
 903 
 904     //=========================================================================
 905     // Loader.convertAnimCurveRange
 906     //-------------------------------------------------------------------------
 907     // This method adds to keyFrameMap which is a
 908     // TreeMap Map<Float, List<KeyValue>>
 909     //=========================================================================
 910     void convertAnimCurveRange(
 911             MNode n, final DoubleProperty property,
 912             boolean convertAnglesToDegrees) {
 913         Collection inputs = n.getConnectionsTo("i");
 914         boolean isDrivenAnimCurve = (inputs.size() > 0);
 915         boolean useTangentInterpolator = true;  // use the NEW tangent interpolator
 916 
 917         //---------------------------------------------------------------------
 918         // Tangent types we need to handle:
 919         //   2 = Linear
 920         //       - The in/out tangent points in the direction of the previous/next key
 921         //   3 = Flat
 922         //       - The in/out tangent has no y component
 923         //   5 = Stepped
 924         //       - If this is seen on the out tangent of the previous
 925         //         frame, immediately goes to the next value
 926         //   9 = Spline
 927         //       - The in / out tangents around the current keyframe
 928         //         match the slope defined by the previous and next
 929         //         keyframes.
 930         //  10 = Clamped
 931         //       - Uses spline tangents unless the keyframe is very close to the next or
 932         //         previous value, in which case it uses linear tangents.
 933         //  16 = Plateau
 934         //       - Generally speaking, if the keyframe is a local maximum or minimum,
 935         //         uses flat tangents to prevent the curve from overshooting the keyframe.
 936         //         Seems to use spline tangents when the keyframe is not a local extremum.
 937         //         There is an epsilon factor built in when deciding whether the flattening
 938         //         behavior is to be applied.
 939         // Tangent types we aren't handling:
 940         //   1 = Fixed
 941         //  17 = StepNext
 942         //---------------------------------------------------------------------
 943 
 944         MArray ktv = (MArray) n.getAttr("ktv");
 945         MInt tan = (MInt) n.getAttr("tan");
 946         int len = ktv.getSize();
 947 
 948         // Note: the kix, kiy, kox, koy from Maya
 949         // are most likely unit vectors [kix, kiy] and [kox, koy]
 950         // in some tricky units that Ken figured out.
 951         MFloatArray kix = (MFloatArray) n.getAttr("kix");
 952         MFloatArray kiy = (MFloatArray) n.getAttr("kiy");
 953         MFloatArray kox = (MFloatArray) n.getAttr("kox");
 954         MFloatArray koy = (MFloatArray) n.getAttr("koy");
 955         MIntArray kit = (MIntArray) n.getAttr("kit");
 956         MIntArray kot = (MIntArray) n.getAttr("kot");
 957         boolean hasTangent = kix != null && kix.get() != null && kix.get().length > 0;
 958         boolean isRotation = n.isInstanceOf(animCurveTA) || n.isInstanceOf(animCurveUA);
 959         boolean keyTimesInSeconds =
 960                 (n.isInstanceOf(animCurveUA) || n.isInstanceOf(animCurveUL) ||
 961                         n.isInstanceOf(animCurveUT) || n.isInstanceOf(animCurveUU));
 962 
 963         List<KeyFrame> drivenKeys = new LinkedList();
 964 
 965         // Many incoming animation curves start at keyframe 1; to
 966         // correctly interpret these we need to subtract off one frame
 967         // from each key time
 968         boolean needsOneFrameAdjustment = false;
 969 
 970         // For computing tangents around the current point
 971         float[] keyTimes = new float[3];
 972         float[] keyValues = new float[3];
 973         boolean[] keysValid = new boolean[3];
 974         float[] prevOutTan = new float[3];  // for orig interpolator
 975         float[] curOutTan = new float[3];  // for tan interpolator
 976         float[] curInTan = new float[3];  // for both interpolators
 977         Collection toPaths = n.getPathsConnectingFrom("o");
 978         String keyName = null;
 979         String targetName = null;
 980         for (Object obj : toPaths) {
 981             MPath toPath = (MPath) obj;
 982             keyName = toPath.getComponentSelector();
 983             targetName = toPath.getTargetNode().getName();
 984         }
 985 
 986         for (int j = 0; j < len; j++) {
 987             MCompound k1 = (MCompound) ktv.getData(j);
 988 
 989             float kt = ((MFloat) k1.getData("kt")).get();
 990             float kv = ((MFloat) k1.getData("kv")).get();
 991             if (j == 0 && !keyTimesInSeconds) {
 992                 needsOneFrameAdjustment = (kt != 0.0f);
 993                 //                if (DEBUG) System.out.println("needsOneFrameAdjustment = " + needsOneFrameAdjustment);
 994             }
 995 
 996             //------------------------------------------------------------
 997             // Find out the previous times, values, and durations,
 998             // if they exist
 999             // (this code is both for tan interpolator and orig interpolator)
1000             // Ken's duration is now called durationPrev
1001             // Ken's k0 is now called kPrev
1002             //------------------------------------------------------------
1003             float durationPrev = 0.0f;
1004             float ktPrev = 0.0f;
1005             float kvPrev = 0.0f;
1006             if (j > 0) {
1007                 MCompound kPrev = (MCompound) ktv.getData(j - 1);
1008                 ktPrev = ((MFloat) kPrev.getData("kt")).get();
1009                 kvPrev = ((MFloat) kPrev.getData("kv")).get();  // NEW
1010                 durationPrev = kt - ktPrev;
1011             }
1012 
1013             //------------------------------------------------------------
1014             // Find out the next times, values, and durations,
1015             // if they exist
1016             // (this code is specifically for TangentInterpolator)
1017             //------------------------------------------------------------
1018             float durationNext = 0.0f;
1019             float ktNext = 0.0f;
1020             float kvNext = 0.0f;
1021             if ((j + 1) < len) {
1022                 MCompound kNext = (MCompound) ktv.getData(j + 1);
1023                 ktNext = ((MFloat) kNext.getData("kt")).get();
1024                 kvNext = ((MFloat) kNext.getData("kv")).get();  // NEW
1025                 durationNext = ktNext - kt;
1026             }
1027 
1028             if (!keyTimesInSeconds) {
1029                 // convert frames to seconds
1030                 kt /= FPS;
1031                 ktPrev /= FPS;  // NEW
1032                 ktNext /= FPS;  // NEW
1033             } else {
1034                 // convert seconds to frames
1035                 durationPrev *= FPS;
1036                 durationNext *= FPS;  // NEW
1037             }
1038             /*
1039               var ktd = kt;
1040               if (range != null) {
1041               if (range.start > ktd or range.end < ktd) {
1042               continue;
1043               }
1044               }
1045             */
1046 
1047 
1048             // Determine the tangent types on both sides
1049             int prevOutTanType = tan.get();  // for orig interpolator
1050             int curInTanType = tan.get();  // for both interpolators
1051             int curOutTanType = tan.get();  // for tan intepolator
1052             if (j > 0 && j < kot.getSize()) {
1053                 int tmp = kot.get(j - 1);
1054                 // Will be 0 if not actually written in the file
1055                 if (tmp != 0) {
1056                     prevOutTanType = tmp;
1057                 }
1058             }
1059             if (j < kot.getSize()) {  // NEW
1060                 int tmp = kot.get(j);
1061                 if (tmp != 0) {
1062                     curOutTanType = tmp;
1063                 }
1064             }
1065             if (j < kit.getSize()) {
1066                 int tmp = kit.get(j);
1067                 if (tmp != 0) {
1068                     curInTanType = tmp;
1069                 }
1070             }
1071 
1072             // Get previous out tangent
1073             getTangent(
1074                     ktv, kix, kiy, kox, koy,
1075                     j - 1,
1076                     prevOutTanType,
1077                     false,
1078                     isRotation,
1079                     keyTimesInSeconds,
1080                     prevOutTan,
1081                     // Temporaries
1082                     keyTimes, keyValues, keysValid);
1083 
1084             // NEW
1085             // for tangentInterpolator, we also need curOutTangent
1086             // Get current out tangent
1087             getTangent(
1088                     ktv, kix, kiy, kox, koy,
1089                     j,
1090                     curOutTanType,
1091                     false,
1092                     isRotation,
1093                     keyTimesInSeconds,
1094                     curOutTan,
1095                     // Temporaries
1096                     keyTimes, keyValues, keysValid);
1097 
1098             // Get current in tangent
1099             getTangent(
1100                     ktv, kix, kiy, kox, koy,
1101                     j,
1102                     curInTanType,
1103                     true,
1104                     isRotation,
1105                     keyTimesInSeconds,
1106                     curInTan,
1107                     // Temporaries
1108                     keyTimes, keyValues, keysValid);
1109 
1110             // Create the appropriate interpolator type:
1111             // [*] DISCRETE for STEPPED type for prevOutTanType
1112             // [*] Interpolator.TANGENT
1113             // [*] custom Maya animation curve interpolator if specified
1114             Interpolator interp = Interpolator.DISCRETE;
1115             if (prevOutTanType == TAN_STEPPED) {
1116                 // interp = DISCRETE;
1117             } else {
1118                 if (useTangentInterpolator) {
1119                     //--------------------------------------------------
1120                     // TangentIntepolator
1121                     double k_ix = curInTan[0];
1122                     double k_iy = curInTan[1];
1123                     // don't use prevOutTan for tangentInterpolator
1124                     // double k_ox = prevOutTan[0];
1125                     // double k_oy = prevOutTan[1];
1126                     double k_ox = curOutTan[0];
1127                     double k_oy = curOutTan[1];
1128 
1129                     /*
1130                       if (DEBUG) System.out.println("n.getName(): " + n.getName());
1131                       if (DEBUG) System.out.println("(k_ix = " + k_ix + ", " +
1132                       "k_iy = " + k_iy + ", " +
1133                       "k_ox = " + k_ox + ", " +
1134                       "k_oy = " + k_oy + ")"
1135                       );
1136                     */
1137 
1138                     // if (DEBUG) System.out.println("FPS = " + FPS);
1139 
1140                     double inTangent = 0.0;
1141                     double outTangent = 0.0;
1142 
1143                     // Compute the in tangent
1144                     if (k_ix != 0) {
1145                         inTangent = k_iy / (k_ix * FPS);
1146                     }
1147                     // Compute the out tangent
1148                     if (k_ox != 0) {
1149                         outTangent = k_oy / (k_ox * FPS);
1150                     }
1151 
1152                     // Compute 1/3 of the time interval of this keyframe
1153                     double oneThirdDeltaPrev = durationPrev / 3.0f;
1154                     double oneThirdDeltaNext = durationNext / 3.0f;
1155 
1156                     // Note: for angular animation curves, the tangents encode
1157                     // changes in radians rather than degrees. Now that our
1158                     // animation curves also emit radians, no conversion is
1159                     // necessary here.
1160                     double inTangentValue = -1 * inTangent * oneThirdDeltaPrev + kv;
1161                     double outTangentValue = outTangent * oneThirdDeltaNext + kv;
1162                     // We need to add "+ kv", because the value for the tangent
1163                     // interpolator is in "world space" and not relative to the key
1164 
1165                     if (inTangentValue > MAXIMUM) {
1166                         inTangentValue = MAXIMUM;
1167                     }
1168                     if (outTangentValue > MAXIMUM) {
1169                         outTangentValue = MAXIMUM;
1170                     }
1171 
1172                     double timeDeltaPrev = (durationPrev / FPS) * 1000f / 3.0f;  // in ms
1173                     double timeDeltaNext = (durationNext / FPS) * 1000f / 3.0f;  // in ms
1174 
1175                     if (true) {
1176                         //                        if (DEBUG) System.out.println("________________________________________");
1177                         //                        if (DEBUG) System.out.println("n.getName() = " + n.getName());
1178                         //                        if (DEBUG) System.out.println("kv = " + kv);
1179                         //                        if (DEBUG) System.out.println("Interpolator.TANGENT(" +
1180                         //                                           "Duration.valueOf(" +
1181                         //                                           timeDeltaPrev + ")" + ", " +
1182                         //                                           inTangentValue + ", " +
1183                         //                                           "Duration.valueOf(" +
1184                         //                                           timeDeltaNext + ")" + ", " +
1185                         //                                           outTangentValue + ");"
1186                         //                                           );
1187 
1188                     }
1189 
1190                     //--------------------------------------------------
1191                     // Given the diagram below, where
1192                     //     k = keyframe
1193                     //     i = inTangent
1194                     //     o = outTangent
1195                     //     + = timeDelta
1196                     // Its extremely important to note that
1197                     // inTangent's and outTangent's values for "i" and "o"
1198                     // are NOT relative to "k".  They are in "worldSpace".
1199                     // However, the timeDeltaNext and timeDeltaPrev
1200                     // are in fact relative to the keyframe "k",
1201                     // and are always an absolute value.
1202                     // So, in summary,
1203                     // the Y-axis values are not relative, but
1204                     // the X-axis values are relative, and always positive
1205                     //--------------------------------------------------
1206                     // (Y-axis worldSpace value for i)
1207                     //    inTangent i
1208                     //              |
1209                     //              |        timeDeltaNext (relative to x)
1210                     //              |         |<------->|
1211                     //              +---------k---------+
1212                     //              |<------->|         |
1213                     //             timeDeltaPrev        |
1214                     //                                  |
1215                     //                                  o outTangent
1216                     //                  (Y-axis worldSpace value for o)
1217                     //--------------------------------------------------
1218                     Duration inDuration = Duration.millis(timeDeltaPrev);
1219                     if (inDuration.toMillis() == 0) {
1220                         interp = Interpolator.TANGENT(Duration.millis(timeDeltaNext), outTangentValue);
1221                     } else {
1222                         interp = Interpolator.TANGENT(
1223                                 inDuration, inTangentValue,
1224                                 Duration.millis(timeDeltaNext), outTangentValue);
1225                     }
1226                 } else {
1227                     MayaAnimationCurveInterpolator mayaInterp =
1228                             createMayaAnimationCurveInterpolator(
1229                                     prevOutTan[0], prevOutTan[1],
1230                                     curInTan[0], curInTan[1],
1231                                     durationPrev,
1232                                     true);
1233                     // mayaInterp.isRotation = isRotation;  // was commented out long ago by Ken/Chris
1234                     // mayaInterp.debug = targetName + "." + keyName + "@"+ kt;
1235                     interp = mayaInterp;
1236                 }
1237             }
1238 
1239             float t = kt - EPSILON;
1240             if (t < 0.0) {
1241                 continue; // just skipping all the negative frames
1242             }
1243 
1244             /*
1245             // This was the old way of adjusting
1246             // for the one frame adjustment.
1247             if (needsOneFrameAdjustment) {
1248                 t = kt - 1.0f/FPS;
1249             } else {
1250                 t = kt;
1251             }
1252             // The new way is below ...
1253             // See: (needsOneFrameAdjustment && (j == 0))
1254             */
1255 
1256             // if (DEBUG) System.out.println("j = " + j);
1257             //            if (DEBUG) System.out.println("t = " + t);
1258             if (isRotation) {
1259                 // Maya angular animation curves implicitly output in radians.
1260                 // In order to properly process them throughout the utility node
1261                 // network, we have to follow this convention, and implicitly
1262                 // convert the inputs of transforms' rotation angles to degrees
1263                 // at the end.
1264                 if (!convertAnglesToDegrees) {
1265                     kv = (float) Math.toRadians(kv);
1266                 }
1267             }
1268             // if (DEBUG) System.out.println("creating key value at: " + t + ": " + targetName + "." + keyName);
1269             KeyValue keyValue = new KeyValue(property, kv, interp);  // [!] API change
1270 
1271             // If the first frame is at frame 1,
1272             // at least for now, try adding in a frame at frame 0
1273             // which is a duplicate of the frame at frame 1,
1274             // to counter-act some strange behavior we are seeing
1275             // if there is no key at frame 0.
1276             if (needsOneFrameAdjustment && (j == 0)) {
1277                 if (DEBUG) System.out.println("[!] ATTEMPTING FRAME ONE ADJUSTMENT [!]");
1278                 // [!] API change
1279                 // KeyValue keyValue0 = new KeyValue(property, kv, Interpolator.LINEAR);
1280                 KeyValue keyValue0 = new KeyValue(property, kv);
1281                 addKeyframe(0.0f, keyValue0);
1282             }
1283 
1284             // Add keyframe
1285             addKeyframe(t, keyValue);
1286 
1287             /*
1288             // If you're at the last keyframe,
1289             // at least for now, try adding in an extra frame
1290             // to pad the ending
1291             if (j == (len - 1)) {
1292                 addKeyframe((t+0.0001667f), keyValue);
1293             }
1294             */
1295         }
1296     }
1297 
1298     //=========================================================================
1299     // Loader.addKeyframe
1300     //=========================================================================
1301     void addKeyframe(float t, KeyValue keyValue) {
1302         List<KeyValue> vals = keyFrameMap.get(t);
1303         if (vals == null) {
1304             vals = new LinkedList<KeyValue>();
1305             keyFrameMap.put(t, vals);
1306         }
1307         vals.add(keyValue);
1308     }
1309 
1310     //=========================================================================
1311     // Loader.createMayaAnimationCurveInterpolator
1312     //=========================================================================
1313     MayaAnimationCurveInterpolator createMayaAnimationCurveInterpolator(
1314             float kox,
1315             float koy,
1316             float kix,
1317             float kiy,
1318             float duration,
1319             boolean hasTangent) {
1320         if (duration == 0.0f) {
1321             return new MayaAnimationCurveInterpolator(0, 0, true);
1322         } else {
1323             // Compute the out tangent
1324             float outTangent = koy / (kox * FPS);
1325             // Compute the in tangent
1326             float inTangent = kiy / (kix * FPS);
1327             // Compute 1/3 of the time interval of this keyframe
1328             float oneThirdDelta = duration / 3.0f;
1329 
1330             // Note: for angular animation curves, the tangents encode
1331             // changes in radians rather than degrees. Now that our
1332             // animation curves also emit radians, no conversion is
1333             // necessary here.
1334             float p1Delta = outTangent * oneThirdDelta;
1335             float p2Delta = -inTangent * oneThirdDelta;
1336             return new MayaAnimationCurveInterpolator(p1Delta, p2Delta, false);
1337         }
1338     }
1339 
1340     //=========================================================================
1341     // Loader.getTangent
1342     //=========================================================================
1343     void getTangent(
1344             MArray ktv,
1345             MFloatArray kix,
1346             MFloatArray kiy,
1347             MFloatArray kox,
1348             MFloatArray koy,
1349             int index,
1350             int tangentType,
1351             boolean inTangent,
1352             boolean isRotation,
1353             boolean keyTimesInSeconds,
1354             float[] result,
1355             // Temporaries
1356             float[] tmpKeyTimes,
1357             float[] tmpKeyValues,
1358             boolean[] tmpKeysValid) {
1359         float[] output = result;
1360         float[] keyTimes = tmpKeyTimes;
1361         float[] keyValues = tmpKeyValues;
1362         boolean[] keysValid = tmpKeysValid;
1363         if (inTangent) {
1364             if (index >= 0 && index < kix.getSize() && index < kiy.getSize()) {
1365                 output[0] = kix.get(index);
1366                 output[1] = kiy.get(index);
1367                 if (output[0] != 0.0f ||
1368                         output[1] != 0.0f) {
1369                     // A keyframe was specified in the file
1370                     return;
1371                 }
1372             }
1373         } else {
1374             if (index >= 0 && index < kox.getSize() && index < koy.getSize()) {
1375                 output[0] = kox.get(index);
1376                 output[1] = koy.get(index);
1377                 if (output[0] != 0.0f ||
1378                         output[1] != 0.0f) {
1379                     // A keyframe was specified in the file
1380                     return;
1381                 }
1382             }
1383         }
1384 
1385         // Need to compute the tangent from the surrounding key times and values
1386         int i = -1;
1387         while (i < 2) {
1388             int cur = index + i;
1389             if (cur >= 0 && cur < ktv.getSize()) {
1390                 MCompound k1 = (MCompound) ktv.getData(cur);
1391                 float kt = ((MFloat) k1.getData("kt")).get();
1392                 if (keyTimesInSeconds) {
1393                     // Convert seconds to frames
1394                     kt *= FPS;
1395                 }
1396                 float kv = ((MFloat) k1.getData("kv")).get();
1397                 if (isRotation) {
1398                     // Maya angular animation curves implicitly output in radians -- see below
1399                     kv = (float) Math.toRadians(kv);
1400                 }
1401                 keyTimes[1 + i] = kt;
1402                 keyValues[1 + i] = kv;
1403                 keysValid[1 + i] = true;
1404             } else {
1405                 keysValid[1 + i] = false;
1406             }
1407             ++i;
1408         }
1409         computeTangent(keyTimes, keyValues, keysValid, tangentType, inTangent, result);
1410     }
1411 
1412     //=========================================================================
1413     // Loader.computeTangent
1414     //=========================================================================
1415     void computeTangent(
1416             float[] keyTimes,
1417             float[] keyValues,
1418             boolean[] keysValid,
1419             float tangentType,
1420             boolean inTangent,
1421             float[] computedTangent) {
1422         float[] output = computedTangent;
1423         if (tangentType == TAN_LINEAR) {
1424             float x0;
1425             float x1;
1426             float y0;
1427             float y1;
1428             if (inTangent) {
1429                 if (!keysValid[0]) {
1430                     // Start of the animation curve: doesn't matter
1431                     output[0] = 1.0f;
1432                     output[1] = 0.0f;
1433                     return;
1434                 }
1435                 x0 = keyTimes[0];
1436                 x1 = keyTimes[1];
1437                 y0 = keyValues[0];
1438                 y1 = keyValues[1];
1439             } else {
1440                 if (!keysValid[2]) {
1441                     // End of the animation curve: doesn't matter
1442                     output[0] = 1.0f;
1443                     output[1] = 0.0f;
1444                     return;
1445                 }
1446                 x0 = keyTimes[1];
1447                 x1 = keyTimes[2];
1448                 y0 = keyValues[1];
1449                 y1 = keyValues[2];
1450             }
1451             float dx = x1 - x0;
1452             float dy = y1 - y0;
1453             output[0] = dx;
1454             output[1] = dy;
1455             // Fall through to perform normalization
1456         } else if (tangentType == TAN_FLAT) {
1457             output[0] = 1.0f;
1458             output[1] = 0.0f;
1459             return;
1460         } else if (tangentType == TAN_STEPPED) {
1461             // Doesn't matter what the tangent values are -- will use discrete type interpolator
1462             return;
1463         } else if (tangentType == TAN_SPLINE) {
1464             // Whether we're computing the in or out tangent, if we don't have one or the other
1465             // keyframe, it reduces to a simpler case
1466             if (!(keysValid[0] && keysValid[2])) {
1467                 // Reduces to the linear case
1468                 computeTangent(keyTimes, keyValues, keysValid, TAN_LINEAR, inTangent, computedTangent);
1469                 return;
1470             }
1471 
1472             // Figure out the slope between the adjacent keyframes
1473             output[0] = keyTimes[2] - keyTimes[0];
1474             output[1] = keyValues[2] - keyValues[0];
1475         } else if (tangentType == TAN_CLAMPED) {
1476             if (!(keysValid[0] && keysValid[2])) {
1477                 // Reduces to the linear case at the ends of the animation curve
1478                 computeTangent(keyTimes, keyValues, keysValid, TAN_LINEAR, inTangent, computedTangent);
1479                 return;
1480             }
1481 
1482             float inDiff = Math.abs(keyValues[1] - keyValues[0]);
1483             float outDiff = Math.abs(keyValues[2] - keyValues[1]);
1484 
1485             if (inDiff <= TAN_EPSILON || outDiff <= TAN_EPSILON) {
1486                 // The Maya docs say that this reduces to the linear
1487                 // case. If this were true, then the apparent behavior
1488                 // would be to compute the linear tangent between the
1489                 // two keyframes which are closest together, and
1490                 // reflect that tangent about the current keyframe.
1491                 // computeTangent(keyTimes, keyValues, keysValid, TAN_LINEAR, (inDiff < outDiff), computedTangent);
1492 
1493                 // However, experimentation in the curve editor
1494                 // clearly indicates for our test cases that flat
1495                 // rather than linear interpolation is used in this
1496                 // case. Therefore to match Maya's actual behavior
1497                 // more closely we do the following.
1498                 computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
1499             } else {
1500                 // Use spline tangents
1501                 computeTangent(keyTimes, keyValues, keysValid, TAN_SPLINE, inTangent, computedTangent);
1502             }
1503 
1504             return;
1505         } else if (tangentType == TAN_PLATEAU) {
1506             if (!(keysValid[0] && keysValid[2])) {
1507                 // Reduces to the flat case at the ends of the animation curve
1508                 computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
1509                 return;
1510             }
1511 
1512             // Otherwise, figure out whether we have any local extremum
1513             if ((keyValues[1] > keyValues[0] &&
1514                     keyValues[1] > keyValues[2]) ||
1515                     (keyValues[1] < keyValues[0] &&
1516                             keyValues[1] < keyValues[2])) {
1517                 // Use flat tangent
1518                 computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
1519             } else {
1520                 // The rule is that we use spline tangents unless
1521                 // doing so would cause the curve to go outside the
1522                 // envelope of the keyvalues. To figure this out, we
1523                 // have to compute both the in and out tangents as
1524                 // though we were using splines, and see whether the
1525                 // intermediate bezier control points go outside the
1526                 // hull.
1527                 //
1528                 // Note that it doesn't matter whether we compute the
1529                 // "in" or "out" tangent at the current point -- the
1530                 // result is the same.
1531                 computeTangent(keyTimes, keyValues, keysValid, TAN_SPLINE, inTangent, computedTangent);
1532 
1533                 // Compute the values from the keyframe along the
1534                 // tangent 1/3 of the way to the previous and next
1535                 // keyframes
1536                 float tangent = computedTangent[1] / (computedTangent[0] * FPS);
1537                 float prev13 = keyValues[1] - tangent * ((keyTimes[1] - keyTimes[0]) / 3.0f);
1538                 float next13 = keyValues[1] + tangent * ((keyTimes[2] - keyTimes[1]) / 3.0f);
1539 
1540                 if (isBetween(prev13, keyValues[0], keyValues[2]) &&
1541                         isBetween(next13, keyValues[0], keyValues[2])) {
1542                 } else {
1543                     // Use flat tangent
1544                     computeTangent(keyTimes, keyValues, keysValid, TAN_FLAT, inTangent, computedTangent);
1545                 }
1546             }
1547 
1548             return;
1549         }
1550 
1551         // Perform normalization
1552         // NOTE the scaling of the X coordinate -- this is needed to match Maya's math
1553         output[0] /= FPS;
1554         float len = (float) Math.sqrt(
1555                 output[0] * output[0] +
1556                         output[1] * output[1]);
1557         if (len != 0.0f) {
1558             output[0] /= len;
1559             output[1] /= len;
1560         }
1561         // println("TAN LINEAR {output[0]} {output[1]}");
1562     }
1563 
1564     //=========================================================================
1565     // Loader.isBetween
1566     //=========================================================================
1567     boolean isBetween(
1568             float value,
1569             float v1,
1570             float v2) {
1571         return ((v1 <= value && value <= v2) ||
1572                 (v1 >= value && value >= v2));
1573     }
1574 
1575 
1576     static class VertexHash {
1577         private int vertexIndex;
1578         private int normalIndex;
1579         private int[] uvIndices;
1580 
1581         VertexHash(
1582                 int vertexIndex,
1583                 int normalIndex,
1584                 int[] uvIndices) {
1585             this.vertexIndex = vertexIndex;
1586             this.normalIndex = normalIndex;
1587             if (uvIndices != null) {
1588                 this.uvIndices = (int[]) uvIndices.clone();
1589             }
1590         }
1591 
1592         @Override
1593         public int hashCode() {
1594             int code = vertexIndex;
1595             code *= 17;
1596             code += normalIndex;
1597             if (uvIndices != null) {
1598                 for (int i = 0; i < uvIndices.length; i++) {
1599                     code *= 17;
1600                     code += uvIndices[i];
1601                 }
1602             }
1603             return code;
1604         }
1605 
1606         @Override
1607         public boolean equals(Object arg) {
1608             if (arg == null || !(arg instanceof VertexHash)) {
1609                 return false;
1610             }
1611 
1612             VertexHash other = (VertexHash) arg;
1613             if (vertexIndex != other.vertexIndex) {
1614                 return false;
1615             }
1616             if (normalIndex != other.normalIndex) {
1617                 return false;
1618             }
1619             if ((uvIndices != null) != (other.uvIndices != null)) {
1620                 return false;
1621             }
1622             if (uvIndices != null) {
1623                 if (uvIndices.length != other.uvIndices.length) {
1624                     return false;
1625                 }
1626                 for (int i = 0; i < uvIndices.length; i++) {
1627                     if (uvIndices[i] != other.uvIndices[i]) {
1628                         return false;
1629                     }
1630                 }
1631             }
1632             return true;
1633         }
1634     }
1635 
1636     private Object buildMeshData(List<MPolyFace.FaceData> faces, MFloat3Array normals) {
1637         // Setup vertexes
1638         float[] verts = mVerts.get();
1639         float[] tweaks = null;
1640         if (mPointTweaks != null) {
1641             tweaks = mPointTweaks.get();
1642         }
1643         float[] points = new float[verts.length];
1644         for (int index = 0; index < verts.length; index += 3) {
1645             if (tweaks != null && tweaks.length > index + 2) {
1646                 points[index] = verts[index] + tweaks[index];
1647                 points[index + 1] = verts[index + 1] + tweaks[index + 1];
1648                 points[index + 2] = verts[index + 2] + tweaks[index + 2];
1649             } else {
1650                 points[index] = verts[index];
1651                 points[index + 1] = verts[index + 1];
1652                 points[index + 2] = verts[index + 2];
1653             }
1654         }
1655 
1656         // copy UV as-is (if any)
1657         float[] texCoords = getTexCoords(uvChannel);
1658 
1659         if (asPolygonMesh) {
1660             List<int[]> ff = new ArrayList<int[]>();
1661             for (int f = 0; f < faces.size(); f++) {
1662                 MPolyFace.FaceData faceData = faces.get(f);
1663                 int[] faceEdges = faceData.getFaceEdges();
1664                 int[][] uvData = faceData.getUVData();
1665                 int[] uvIndices = uvData == null ? null : uvData[uvChannel];
1666                 if (faceEdges != null && faceEdges.length > 0) {
1667                     int[] polyFace = new int[faceEdges.length * 2];
1668                     for (int i = 0; i < faceEdges.length; i++) {
1669                         int vIndex = edgeStart(faceEdges[i]);
1670                         int uvIndex = uvIndices == null ? 0 : uvIndices[i];
1671                         polyFace[i*2] = vIndex;
1672                         polyFace[i*2+1] = uvIndex;
1673                     }
1674                     ff.add(polyFace);
1675                 }
1676             }
1677             int[][] facesArray = ff.toArray(new int[ff.size()][]);
1678             
1679             int[][] faceNormals = new int[facesArray.length][];
1680             int normalInd = 0;
1681             for (int f = 0; f < faceNormals.length; f++) {
1682                 faceNormals[f] = new int[facesArray[f].length/2];
1683                 for (int e = 0; e < faceNormals[f].length; e++) {
1684                     faceNormals[f][e] = normalInd++;
1685                 }
1686             }
1687             int[] smGroups;
1688             // we can only figure out faces' normal indices if the faces' normal indices have a one-to-one ordered correspondence with the normals
1689             if (normalInd == normals.getSize()) {
1690                 smGroups = SmoothingGroups.calcSmoothGroups(facesArray, faceNormals, normals.get());
1691             } else {
1692                 smGroups = new int[facesArray.length];
1693                 Arrays.fill(smGroups, 1);
1694             }
1695 
1696             PolygonMesh mesh = new PolygonMesh();
1697             mesh.getPoints().setAll(points);
1698             mesh.getTexCoords().setAll(texCoords);
1699             mesh.faces = facesArray;
1700             mesh.getFaceSmoothingGroups().setAll(smGroups);
1701             return mesh;
1702         } else {
1703             // Split the polygonal faces into triangle faces
1704             List<Integer> ff = new ArrayList<Integer>();
1705             List<Integer> nn = new ArrayList<Integer>();
1706             int nIndex = 0;
1707             
1708             for (int f = 0; f < faces.size(); f++) {
1709                 MPolyFace.FaceData faceData = faces.get(f);
1710                 int[] faceEdges = faceData.getFaceEdges();
1711                 int[][] uvData = faceData.getUVData();
1712                 int[] uvIndices = uvData == null ? null : uvData[uvChannel];
1713                 if (faceEdges != null && faceEdges.length > 0) {
1714 
1715                     // Generate triangle fan about the first vertex
1716                     int vIndex0 = edgeStart(faceEdges[0]);
1717                     int uvIndex0 = uvIndices == null ? 0 : uvIndices[0];
1718                     int nIndex0 = nIndex++;
1719 
1720                     int vIndex1 = edgeStart(faceEdges[1]);
1721                     int uvIndex1 = uvIndices == null ? 0 : uvIndices[1];
1722                     int nIndex1 = nIndex++;
1723 
1724                     for (int i = 2; i < faceEdges.length; i++) {
1725                         int vIndex2 = edgeStart(faceEdges[i]);
1726                         int uvIndex2 = uvIndices == null ? 0 : uvIndices[i];
1727                         int nIndex2 = nIndex++;
1728 
1729                         ff.add(vIndex0);
1730                         ff.add(uvIndex0);
1731                         ff.add(vIndex1);
1732                         ff.add(uvIndex1);
1733                         ff.add(vIndex2);
1734                         ff.add(uvIndex2);
1735                         nn.add(nIndex0);
1736                         nn.add(nIndex1);
1737                         nn.add(nIndex2);
1738 
1739                         vIndex1 = vIndex2;
1740                         uvIndex1 = uvIndex2;
1741                     }
1742                 }
1743             }
1744             int[] fff = new int[ff.size()];
1745             for (int i = 0; i < fff.length; i++) {
1746                 fff[i] = ff.get(i);
1747             }
1748 
1749             TriangleMesh mesh = new TriangleMesh();
1750             int[] smGroups;
1751             // we can only figure out faces' normal indices if the faces' normal indices have a one-to-one ordered correspondence with the normals
1752             if (nIndex == normals.getSize()) {
1753                 int[] faceNormals = new int[nn.size()];
1754                 for (int i = 0; i < faceNormals.length; i++) {
1755                     faceNormals[i] = nn.get(i);
1756                 }
1757                 smGroups = SmoothingGroups.calcSmoothGroups(mesh, fff, faceNormals, normals.get());
1758             } else {
1759                 smGroups = new int[fff.length/6];
1760                 Arrays.fill(smGroups, 1);
1761             }
1762             
1763             mesh.getPoints().setAll(points);
1764             mesh.getTexCoords().setAll(texCoords);
1765             mesh.getFaces().setAll(fff);
1766             mesh.getFaceSmoothingGroups().setAll(smGroups);
1767             return mesh;
1768         }
1769     }
1770 
1771     MNode resolveOutputMesh(MNode n) {
1772         MNode og;
1773         List<MPath> ogc0 = n.getPathsConnectingFrom("og[0]");
1774         if (ogc0.size() > 0) {
1775             og = ogc0.get(0).getTargetNode();
1776         } else {
1777             ogc0 = n.getPathsConnectingFrom("og");
1778             if (ogc0.size() > 0) {
1779                 og = ogc0.get(0).getTargetNode();
1780             } else {
1781                 return null;
1782             }
1783         }
1784         if (og.isInstanceOf(meshType)) {
1785             return og;
1786         }
1787         // println("r.OG={og}");
1788         while (og.isInstanceOf(groupPartsType)) {
1789             og = og.getPathsConnectingFrom("og").get(0).getTargetNode();
1790         }
1791         if (og.isInstanceOf(meshType)) {
1792             return og;
1793         }
1794         // println("r1.OG={og}");
1795         if (og == null) {
1796             return null;
1797         }
1798         return resolveOutputMesh(og);
1799     }
1800 
1801     MNode resolveInputMesh(MNode n) {
1802         return resolveInputMesh(n, true);
1803     }
1804 
1805     MNode resolveInputMesh(MNode n, boolean followBlend) {
1806         MNode groupParts;
1807         if (!n.isInstanceOf(groupPartsType)) {
1808             groupParts = n.getIncomingConnectionToType("ip[0].ig", "groupParts");
1809         } else {
1810             groupParts = n;
1811         }
1812         MNode origMesh = groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
1813         if (origMesh == null) {
1814             MNode tweak = groupParts.getIncomingConnectionToType("ig", "tweak");
1815             groupParts = tweak.getIncomingConnectionToType("ip[0].ig", "groupParts");
1816             origMesh =
1817                     groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
1818         }
1819         // println("N={n} ORIG_MESH={origMesh}");
1820         if (origMesh == null) {
1821             return null;
1822         }
1823         if (origMesh.isInstanceOf(meshType)) {
1824             return origMesh;
1825         }
1826         if (origMesh.isInstanceOf(blendShapeType)) {
1827             // return the blend shape's output
1828             return resolveOutputMesh(origMesh);
1829         }
1830         return resolveInputMesh(origMesh);
1831     }
1832 
1833     MNode resolveOrigInputMesh(MNode n) {
1834 
1835         MNode groupParts;
1836         if (!n.isInstanceOf(groupPartsType)) {
1837             groupParts = n.getIncomingConnectionToType("ip[0].ig", "groupParts");
1838         } else {
1839             groupParts = n;
1840         }
1841         MNode origMesh = groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
1842         if (origMesh == null) {
1843             MNode tweak = groupParts.getIncomingConnectionToType("ig", "tweak");
1844             groupParts = tweak.getIncomingConnectionToType("ip[0].ig", "groupParts");
1845             origMesh =
1846                     groupParts.getPathsConnectingTo("ig").get(0).getTargetNode();
1847         }
1848         if (origMesh == null) {
1849             return null;
1850         }
1851         // println("N={n} ORIG_MESH={origMesh}");
1852         if (origMesh.isInstanceOf(meshType)) {
1853             return origMesh;
1854         }
1855         return resolveOrigInputMesh(origMesh);
1856     }
1857 
1858     Affine convertMatrix(MFloatArray mayaMatrix) {
1859         if (mayaMatrix == null || mayaMatrix.getSize() < 16) {
1860             return new Affine();
1861         }
1862 
1863         Affine result = new Affine();
1864         result.setMxx(mayaMatrix.get(0 * 4 + 0));
1865         result.setMxy(mayaMatrix.get(1 * 4 + 0));
1866         result.setMxz(mayaMatrix.get(2 * 4 + 0));
1867         result.setMyx(mayaMatrix.get(0 * 4 + 1));
1868         result.setMyy(mayaMatrix.get(1 * 4 + 1));
1869         result.setMyz(mayaMatrix.get(2 * 4 + 1));
1870         result.setMzx(mayaMatrix.get(0 * 4 + 2));
1871         result.setMzy(mayaMatrix.get(1 * 4 + 2));
1872         result.setMzz(mayaMatrix.get(2 * 4 + 2));
1873         result.setTx(mayaMatrix.get(3 * 4 + 0));
1874         result.setTy(mayaMatrix.get(3 * 4 + 1));
1875         result.setTz(mayaMatrix.get(3 * 4 + 2));
1876         return result;
1877     }
1878 
1879 }