Beispiel #1
0
  /**
   * Constructor
   *
   * @param state
   */
  public Grid(GridState state) {
    super(state.name);
    this.state = state;
    this.cellSize = state.size;
    this.color = state.color;
    this.lineWidth = state.lineWidth;
    offset = new Vector3();
    setLocation(state.location, false);

    lattice = new HiddenLine("_lattice", IndexMode.Lines);
    SpatialUtil.setPickHost(lattice, this);
    lattice.setColor(color);
    lattice.setModelBound(new BoundingBox());

    MaterialState ms = new MaterialState();
    ms.setColorMaterial(ColorMaterial.Emissive);
    ms.setColorMaterialFace(MaterialState.MaterialFace.FrontAndBack);
    ms.setEnabled(true);
    text = new Node("_text");
    text.getSceneHints().setLightCombineMode(LightCombineMode.Off);
    text.getSceneHints().setPickingHint(PickingHint.Pickable, false);
    text.setRenderState(ms);
    setLabelVisible(state.labelVisible);

    attachChild(lattice);
    attachChild(text);

    setVisible(state.visible);
    setPinned(state.pinned);

    state.setMapElement(this);
  }
Beispiel #2
0
 /**
  * Set the color
  *
  * @param color
  */
 public void setColor(ReadOnlyColorRGBA color) {
   this.color.set(color);
   MaterialState ms = new MaterialState();
   this.color.setAlpha(0.3f);
   ms.setDiffuse(MaterialState.MaterialFace.FrontAndBack, this.color);
   ms.setAmbient(MaterialState.MaterialFace.FrontAndBack, ColorRGBA.BLACK);
   ms.setEmissive(MaterialState.MaterialFace.FrontAndBack, this.color);
   ms.setEnabled(true);
   setRenderState(ms);
 }
 private void addMesh(final Spatial spatial) {
   spatial.setTranslation(
       (index % wrapCount) * 8 - wrapCount * 4, (index / wrapCount) * 8 - wrapCount * 4, -50);
   if (spatial instanceof Mesh) {
     ((Mesh) spatial).updateModelBound();
   }
   final MaterialState ms = new MaterialState();
   ms.setAmbient(ColorRGBA.DARK_GRAY);
   spatial.setRenderState(ms);
   _root.attachChild(spatial);
   index++;
 }
Beispiel #4
0
 /**
  * Constructor
  *
  * @param color
  */
 public SimpleCrosshair(ReadOnlyColorRGBA color) {
   super("Crosshair");
   ReadOnlyColorRGBA[] crosshairColor = {color, color, color, color};
   getMeshData().setIndexMode(IndexMode.Lines);
   getMeshData().setVertexBuffer(BufferUtils.createFloatBuffer(crosshairVertex));
   FloatBuffer colorBuffer = BufferUtils.createFloatBuffer(crosshairColor);
   colorBuffer.rewind();
   getMeshData().setColorBuffer(colorBuffer);
   getMeshData().setIndexBuffer(BufferUtils.createIntBuffer(crosshairIndex));
   getMeshData().getIndexBuffer().limit(4);
   getMeshData().getIndexBuffer().rewind();
   getSceneHints().setAllPickingHints(false);
   setModelBound(new BoundingBox());
   updateModelBound();
   MaterialState crosshairMaterialState = new MaterialState();
   crosshairMaterialState.setColorMaterial(MaterialState.ColorMaterial.Emissive);
   crosshairMaterialState.setEnabled(true);
   getSceneHints().setLightCombineMode(LightCombineMode.Off);
   setRenderState(crosshairMaterialState);
   updateGeometricState(0, true);
 }
  /**
   * Find and apply the given material to the given Mesh.
   *
   * @param materialName our material name
   * @param mesh the mesh to apply material to.
   */
  public void applyMaterial(final String materialName, final Mesh mesh) {
    if (materialName == null) {
      logger.warning("materialName is null");
      return;
    }

    Element mat = _dataCache.getBoundMaterial(materialName);
    if (mat == null) {
      logger.warning("material not bound: " + materialName + ", trying search with id.");
      mat = _colladaDOMUtil.findTargetWithId(materialName);
    }
    if (mat == null || !"material".equals(mat.getName())) {
      logger.warning("material not found: " + materialName);
      return;
    }

    final String originalMaterial = mat.getAttributeValue("id");
    MaterialInfo mInfo = null;
    if (!_dataCache.getMaterialInfoMap().containsKey(originalMaterial)) {
      mInfo = new MaterialInfo();
      mInfo.setMaterialName(originalMaterial);
      _dataCache.getMaterialInfoMap().put(originalMaterial, mInfo);
    }
    _dataCache.getMeshMaterialMap().put(mesh, originalMaterial);

    final Element child = mat.getChild("instance_effect");
    final Element effectNode = _colladaDOMUtil.findTargetWithId(child.getAttributeValue("url"));
    if (effectNode == null) {
      logger.warning(
          "material effect not found: "
              + mat.getChild("instance_material").getAttributeValue("url"));
      return;
    }

    if ("effect".equals(effectNode.getName())) {
      /*
       * temp cache for textures, we do not want to add textures twice (for example, transparant map might point
       * to diffuse texture)
       */
      final HashMap<String, Texture> loadedTextures = new HashMap<String, Texture>();
      final Element effect = effectNode;
      // XXX: For now, just grab the common technique:
      if (effect.getChild("profile_COMMON") != null) {
        if (mInfo != null) {
          mInfo.setProfile("COMMON");
        }
        final Element technique = effect.getChild("profile_COMMON").getChild("technique");
        String type = "blinn";
        if (technique.getChild(type) == null) {
          type = "phong";
          if (technique.getChild(type) == null) {
            type = "lambert";
            if (technique.getChild(type) == null) {
              type = "constant";
              if (technique.getChild(type) == null) {
                ColladaMaterialUtils.logger.warning(
                    "COMMON material has unusuable techinque. " + child.getAttributeValue("url"));
                return;
              }
            }
          }
        }
        final Element blinnPhongLambert = technique.getChild(type);
        if (mInfo != null) {
          mInfo.setTechnique(type);
        }
        final MaterialState mState = new MaterialState();

        // TODO: implement proper transparency handling
        Texture diffuseTexture = null;
        ColorRGBA transparent = new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
        float transparency = 1.0f;
        boolean useTransparency = false;
        String opaqueMode = "A_ONE";

        /*
         * place holder for current property, we import material properties in fixed order (for texture order)
         */
        Element property = null;
        /* Diffuse property */
        property = blinnPhongLambert.getChild("diffuse");
        if (property != null) {
          final Element propertyValue = (Element) property.getChildren().get(0);
          if ("color".equals(propertyValue.getName())) {
            final ColorRGBA color = _colladaDOMUtil.getColor(propertyValue.getText());
            mState.setDiffuse(MaterialFace.FrontAndBack, color);
          } else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
            diffuseTexture =
                populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "diffuse");
          }
        }
        /* Ambient property */
        property = blinnPhongLambert.getChild("ambient");
        if (property != null) {
          final Element propertyValue = (Element) property.getChildren().get(0);
          if ("color".equals(propertyValue.getName())) {
            final ColorRGBA color = _colladaDOMUtil.getColor(propertyValue.getText());
            mState.setAmbient(MaterialFace.FrontAndBack, color);
          } else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
            populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "ambient");
          }
        }
        /* Transparent property */
        property = blinnPhongLambert.getChild("transparent");
        if (property != null) {
          final Element propertyValue = (Element) property.getChildren().get(0);
          if ("color".equals(propertyValue.getName())) {
            transparent = _colladaDOMUtil.getColor(propertyValue.getText());
            // TODO: use this
            useTransparency = true;
          } else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
            populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "transparent");
          }
          opaqueMode = property.getAttributeValue("opaque", "A_ONE");
        }
        /* Transparency property */
        property = blinnPhongLambert.getChild("transparency");
        if (property != null) {
          final Element propertyValue = (Element) property.getChildren().get(0);
          if ("float".equals(propertyValue.getName())) {
            transparency = Float.parseFloat(propertyValue.getText().replace(",", "."));
            // TODO: use this
            if (_flipTransparency) {
              transparency = 1f - transparency;
            }
            useTransparency = true;
          } else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
            populateTextureState(
                mesh, propertyValue, effect, loadedTextures, mInfo, "transparency");
          }
        }
        /* Emission property */
        property = blinnPhongLambert.getChild("emission");
        if (property != null) {
          final Element propertyValue = (Element) property.getChildren().get(0);
          if ("color".equals(propertyValue.getName())) {
            mState.setEmissive(
                MaterialFace.FrontAndBack, _colladaDOMUtil.getColor(propertyValue.getText()));
          } else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
            populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "emissive");
          }
        }
        /* Specular property */
        property = blinnPhongLambert.getChild("specular");
        if (property != null) {
          final Element propertyValue = (Element) property.getChildren().get(0);
          if ("color".equals(propertyValue.getName())) {
            mState.setSpecular(
                MaterialFace.FrontAndBack, _colladaDOMUtil.getColor(propertyValue.getText()));
          } else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
            populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "specular");
          }
        }
        /* Shininess property */
        property = blinnPhongLambert.getChild("shininess");
        if (property != null) {
          final Element propertyValue = (Element) property.getChildren().get(0);
          if ("float".equals(propertyValue.getName())) {
            float shininess = Float.parseFloat(propertyValue.getText().replace(",", "."));
            if (shininess >= 0.0f && shininess <= 1.0f) {
              final float oldShininess = shininess;
              shininess *= 128;
              logger.finest(
                  "Shininess - "
                      + oldShininess
                      + " - was in the [0,1] range. Scaling to [0, 128] - "
                      + shininess);
            } else if (shininess < 0 || shininess > 128) {
              final float oldShininess = shininess;
              shininess = MathUtils.clamp(shininess, 0, 128);
              logger.warning(
                  "Shininess must be between 0 and 128. Shininess "
                      + oldShininess
                      + " was clamped to "
                      + shininess);
            }
            mState.setShininess(MaterialFace.FrontAndBack, shininess);
          } else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
            populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "shininess");
          }
        }
        /* Reflectivity property */
        float reflectivity = 1.0f;
        property = blinnPhongLambert.getChild("reflectivity");
        if (property != null) {
          final Element propertyValue = (Element) property.getChildren().get(0);
          if ("float".equals(propertyValue.getName())) {
            reflectivity = Float.parseFloat(propertyValue.getText().replace(",", "."));
          }
        }
        /* Reflective property. Texture only */
        property = blinnPhongLambert.getChild("reflective");
        if (property != null) {
          final Element propertyValue = (Element) property.getChildren().get(0);
          if ("texture".equals(propertyValue.getName()) && _loadTextures) {
            final Texture reflectiveTexture =
                populateTextureState(
                    mesh, propertyValue, effect, loadedTextures, mInfo, "reflective");

            reflectiveTexture.setEnvironmentalMapMode(Texture.EnvironmentalMapMode.SphereMap);
            reflectiveTexture.setApply(ApplyMode.Combine);

            reflectiveTexture.setCombineFuncRGB(CombinerFunctionRGB.Interpolate);
            // color 1
            reflectiveTexture.setCombineSrc0RGB(CombinerSource.CurrentTexture);
            reflectiveTexture.setCombineOp0RGB(CombinerOperandRGB.SourceColor);
            // color 2
            reflectiveTexture.setCombineSrc1RGB(CombinerSource.Previous);
            reflectiveTexture.setCombineOp1RGB(CombinerOperandRGB.SourceColor);
            // interpolate param will come from alpha of constant color
            reflectiveTexture.setCombineSrc2RGB(CombinerSource.Constant);
            reflectiveTexture.setCombineOp2RGB(CombinerOperandRGB.SourceAlpha);

            reflectiveTexture.setConstantColor(new ColorRGBA(1, 1, 1, reflectivity));
          }
        }

        /*
         * An extra tag defines some materials not part of the collada standard. Since we're not able to parse
         * we simply extract the textures from the element (such that shaders etc can at least pick up on them)
         */
        property = technique.getChild("extra");
        if (property != null) {
          getTexturesFromElement(mesh, property, effect, loadedTextures, mInfo);
        }

        // XXX: There are some issues with clarity on how to use alpha blending in OpenGL FFP.
        // The best interpretation I have seen is that if transparent has a texture == diffuse,
        // Turn on alpha blending and use diffuse alpha.

        // check to make sure we actually need this.
        // testing separately for a transparency of 0.0 is to hack around erroneous exports, since
        // usually
        // there is no use in exporting something with 100% transparency.
        if ("A_ONE".equals(opaqueMode) && ColorRGBA.WHITE.equals(transparent) && transparency == 1.0
            || transparency == 0.0) {
          useTransparency = false;
        }

        if (useTransparency) {
          if (diffuseTexture != null) {
            final BlendState blend = new BlendState();
            blend.setBlendEnabled(true);
            blend.setTestEnabled(true);
            blend.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
            blend.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
            mesh.setRenderState(blend);
          } else {
            final BlendState blend = new BlendState();
            blend.setBlendEnabled(true);
            blend.setTestEnabled(true);
            transparent.setAlpha(transparent.getAlpha() * transparency);
            blend.setConstantColor(transparent);
            blend.setSourceFunction(BlendState.SourceFunction.ConstantAlpha);
            blend.setDestinationFunction(BlendState.DestinationFunction.OneMinusConstantAlpha);
            mesh.setRenderState(blend);
          }

          mesh.getSceneHints().setRenderBucketType(RenderBucketType.Transparent);
        }

        if (mInfo != null) {
          if (useTransparency) {
            mInfo.setUseTransparency(useTransparency);
            if (diffuseTexture == null) {
              mInfo.setTransparency(transparent.getAlpha() * transparency);
            }
          }
          mInfo.setMaterialState(mState);
        }
        mesh.setRenderState(mState);
      }
    } else {
      ColladaMaterialUtils.logger.warning(
          "material effect not found: "
              + mat.getChild("instance_material").getAttributeValue("url"));
    }
  }
  @Override
  protected void initExample() {
    _canvas.setTitle("Various size imposters - Example");

    _canvas.getCanvasRenderer().getCamera().setLocation(new Vector3(0, 60, 80));
    _canvas.getCanvasRenderer().getCamera().lookAt(new Vector3(), Vector3.UNIT_Y);

    final BasicText keyText =
        BasicText.createDefaultTextLabel("Text", "[SPACE] Switch imposters off");
    keyText.getSceneHints().setRenderBucketType(RenderBucketType.Ortho);
    keyText.getSceneHints().setLightCombineMode(LightCombineMode.Off);
    keyText.setTranslation(new Vector3(0, 20, 0));
    _root.attachChild(keyText);

    final Box box = new Box("Box", new Vector3(), 150, 1, 150);
    box.setModelBound(new BoundingBox());
    box.setTranslation(new Vector3(0, -10, 0));
    _root.attachChild(box);

    final QuadImposterNode imposter0 =
        new QuadImposterNode(
            "Imposter1", 256, 256, _settings.getDepthBits(), _settings.getSamples(), _timer);
    imposter0.setRedrawRate(0.0); // No timed update
    imposter0.setCameraAngleThreshold(1.0 * MathUtils.DEG_TO_RAD);
    imposter0.setCameraDistanceThreshold(0.1);
    _root.attachChild(imposter0);

    final Node scene1 = createModel();
    scene1.setTranslation(0, 0, 0);
    imposter0.attachChild(scene1);

    final QuadImposterNode imposter1 =
        new QuadImposterNode(
            "Imposter1", 128, 128, _settings.getDepthBits(), _settings.getSamples(), _timer);
    imposter1.setRedrawRate(0.0); // No timed update
    imposter1.setCameraAngleThreshold(1.0 * MathUtils.DEG_TO_RAD);
    imposter1.setCameraDistanceThreshold(0.1);
    _root.attachChild(imposter1);

    final Node scene2 = createModel();
    scene2.setTranslation(-15, 0, -25);
    imposter1.attachChild(scene2);

    final QuadImposterNode imposter2 =
        new QuadImposterNode(
            "Imposter2", 64, 64, _settings.getDepthBits(), _settings.getSamples(), _timer);
    imposter2.setRedrawRate(0.0); // No timed update
    imposter2.setCameraAngleThreshold(1.0 * MathUtils.DEG_TO_RAD);
    imposter2.setCameraDistanceThreshold(0.1);
    _root.attachChild(imposter2);

    final Node scene3 = createModel();
    scene3.setTranslation(15, 0, -25);
    imposter2.attachChild(scene3);

    _logicalLayer.registerTrigger(
        new InputTrigger(
            new KeyPressedCondition(Key.SPACE),
            new TriggerAction() {
              public void perform(
                  final Canvas source, final TwoInputStates inputStates, final double tpf) {
                showImposter = !showImposter;
                if (showImposter) {
                  _root.detachChild(scene1);
                  _root.detachChild(scene2);
                  _root.detachChild(scene3);
                  imposter0.attachChild(scene1);
                  imposter1.attachChild(scene2);
                  imposter2.attachChild(scene3);
                  _root.attachChild(imposter0);
                  _root.attachChild(imposter1);
                  _root.attachChild(imposter2);

                  keyText.setText("[SPACE] Switch imposters off");
                } else {
                  _root.detachChild(imposter0);
                  _root.detachChild(imposter1);
                  _root.detachChild(imposter2);
                  _root.attachChild(scene1);
                  _root.attachChild(scene2);
                  _root.attachChild(scene3);

                  keyText.setText("[SPACE] Switch imposters on");
                }
              }
            }));

    final TextureState ts = new TextureState();
    ts.setEnabled(true);
    ts.setTexture(
        TextureManager.load(
            "images/ardor3d_white_256.jpg", Texture.MinificationFilter.Trilinear, true));

    final MaterialState ms = new MaterialState();
    ms.setColorMaterial(ColorMaterial.Diffuse);
    _root.setRenderState(ms);

    _root.setRenderState(ts);

    _root.acceptVisitor(new UpdateModelBoundVisitor(), false);
  }
Beispiel #7
0
  /**
   * Save a mesh to the given files.
   *
   * @param mesh mesh to export
   * @param objFile WaveFront OBJ file
   * @param mtlFile material file, optional
   * @param append indicates whether the data are written to the end of the OBJ file
   * @param firstVertexIndex first vertex index used for this mesh during the export
   * @param firstFiles indicates whether the couple of files is used for the first time, i.e there
   *     is nothing to append despite the value of <code>append</code>
   * @param materialList list of materials already exported in this material file
   * @param customTextureName texture name that overrides the one of the mesh, optional
   * @throws IOException
   */
  protected void save(
      final Mesh mesh,
      final File objFile,
      final File mtlFile,
      final boolean append,
      final int firstVertexIndex,
      final boolean firstFiles,
      final List<ObjMaterial> materialList,
      final String customTextureName)
      throws IOException {
    File parentDirectory = objFile.getParentFile();
    if (parentDirectory != null && !parentDirectory.exists()) {
      parentDirectory.mkdirs();
    }
    if (mtlFile != null) {
      parentDirectory = mtlFile.getParentFile();
      if (parentDirectory != null && !parentDirectory.exists()) {
        parentDirectory.mkdirs();
      }
    }
    PrintWriter objPw = null, mtlPw = null;
    try {
      // fills the MTL file
      final String mtlName;
      if (mtlFile != null) {
        final FileOutputStream mtlOs = new FileOutputStream(mtlFile, append);
        mtlPw = new PrintWriter(new BufferedOutputStream(mtlOs));
        // writes some comments
        if (firstFiles) {
          mtlPw.println("# Ardor3D 1.0 MTL file");
        }
        final ObjMaterial currentMtl = new ObjMaterial(null);
        final MaterialState mtlState = (MaterialState) mesh.getLocalRenderState(StateType.Material);
        if (mtlState != null) {
          final ReadOnlyColorRGBA ambientColor = mtlState.getAmbient();
          if (ambientColor != null) {
            currentMtl.d = ambientColor.getAlpha();
            currentMtl.Ka =
                new float[] {
                  ambientColor.getRed(),
                  ambientColor.getGreen(),
                  ambientColor.getBlue(),
                  ambientColor.getAlpha()
                };
          }
          final ReadOnlyColorRGBA diffuseColor = mtlState.getDiffuse();
          if (diffuseColor != null) {
            currentMtl.Kd =
                new float[] {
                  diffuseColor.getRed(),
                  diffuseColor.getGreen(),
                  diffuseColor.getBlue(),
                  diffuseColor.getAlpha()
                };
          }
          final ReadOnlyColorRGBA specularColor = mtlState.getSpecular();
          if (specularColor != null) {
            currentMtl.Ks =
                new float[] {
                  specularColor.getRed(),
                  specularColor.getGreen(),
                  specularColor.getBlue(),
                  specularColor.getAlpha()
                };
          }
          currentMtl.Ns = mtlState.getShininess();
        }
        if (customTextureName == null) {
          currentMtl.textureName = getLocalMeshTextureName(mesh);
        } else {
          currentMtl.textureName = customTextureName;
        }
        if (mesh.getSceneHints().getLightCombineMode() == LightCombineMode.Off) {
          // Color on and Ambient off
          currentMtl.illumType = 0;
        } else {
          // Color on and Ambient on
          currentMtl.illumType = 1;
        }
        ObjMaterial sameObjMtl = null;
        if (materialList != null && !materialList.isEmpty()) {
          for (final ObjMaterial mtl : materialList) {
            if (mtl.illumType == currentMtl.illumType
                && mtl.Ns == currentMtl.Ns
                && mtl.forceBlend == currentMtl.forceBlend
                && mtl.d == currentMtl.d
                && Arrays.equals(mtl.Ka, currentMtl.Ka)
                && Arrays.equals(mtl.Kd, currentMtl.Kd)
                && Arrays.equals(mtl.Ks, currentMtl.Ks)
                // && Objects.equals(mtl.textureName, currentMtl.textureName)) {
                && mtl.textureName.equals(currentMtl.textureName)) {
              sameObjMtl = mtl;
              break;
            }
          }
        }
        if (sameObjMtl == null) {
          // writes the new material library
          mtlName =
              mtlFile.getName().trim().replaceAll(" ", "")
                  + "_"
                  + (materialList == null ? 1 : materialList.size() + 1);
          if (materialList != null) {
            final ObjMaterial mtl = new ObjMaterial(mtlName);
            mtl.illumType = currentMtl.illumType;
            mtl.textureName = currentMtl.textureName;
            materialList.add(mtl);
          }
          mtlPw.println("newmtl " + mtlName);
          if (currentMtl.Ns != -1) {
            mtlPw.println("Ns " + currentMtl.Ns);
          }
          if (currentMtl.Ka != null) {
            mtlPw.print("Ka");
            for (final float KaCoef : currentMtl.Ka) {
              mtlPw.print(" " + KaCoef);
            }
            mtlPw.println();
          }
          if (currentMtl.Kd != null) {
            mtlPw.print("Kd");
            for (final float KdCoef : currentMtl.Kd) {
              mtlPw.print(" " + KdCoef);
            }
            mtlPw.println();
          }
          if (currentMtl.Ks != null) {
            mtlPw.print("Ks");
            for (final float KsCoef : currentMtl.Ks) {
              mtlPw.print(" " + KsCoef);
            }
            mtlPw.println();
          }
          if (currentMtl.d != -1) {
            mtlPw.println("d " + currentMtl.d);
          }
          mtlPw.println("illum " + currentMtl.illumType);
          if (currentMtl.textureName != null) {
            mtlPw.println("map_Kd " + currentMtl.textureName);
          }
        } else {
          mtlName = sameObjMtl.getName();
        }
      } else {
        mtlName = null;
      }

      final FileOutputStream objOs = new FileOutputStream(objFile, append);
      objPw = new PrintWriter(new BufferedOutputStream(objOs));
      // writes some comments
      if (firstFiles) {
        objPw.println("# Ardor3D 1.0 OBJ file");
        objPw.println("# www.ardor3d.com");
        // writes the material file name if any
        if (mtlFile != null) {
          final String mtlLibFilename = mtlFile.getName();
          objPw.println("mtllib " + mtlLibFilename);
        }
      }
      // writes the object name
      final String objName;
      String meshName = mesh.getName();
      // removes all spaces from the mesh name
      if (meshName != null && !meshName.isEmpty()) {
        meshName = meshName.trim().replaceAll(" ", "");
      }
      if (meshName != null && !meshName.isEmpty()) {
        objName = meshName;
      } else {
        objName = "obj_mesh" + mesh.hashCode();
      }
      objPw.println("o " + objName);
      final MeshData meshData = mesh.getMeshData();
      // writes the coordinates
      final FloatBufferData verticesData = meshData.getVertexCoords();
      if (verticesData == null) {
        throw new IllegalArgumentException("cannot export a mesh with no vertices");
      }
      final int expectedTupleCount = verticesData.getTupleCount();
      saveFloatBufferData(verticesData, objPw, "v", expectedTupleCount);
      final FloatBufferData texCoordsData = meshData.getTextureCoords(0);
      saveFloatBufferData(texCoordsData, objPw, "vt", expectedTupleCount);
      final FloatBufferData normalsData = meshData.getNormalCoords();
      saveFloatBufferData(normalsData, objPw, "vn", expectedTupleCount);
      // writes the used material library
      if (mtlFile != null) {
        objPw.println("usemtl " + mtlName);
      }
      // writes the faces
      for (int sectionIndex = 0; sectionIndex < meshData.getSectionCount(); sectionIndex++) {
        final IndexMode indexMode = meshData.getIndexMode(sectionIndex);
        final int[] indices = new int[indexMode.getVertexCount()];
        switch (indexMode) {
          case TriangleFan:
          case Triangles:
          case TriangleStrip:
          case Quads:
            for (int primIndex = 0, primCount = meshData.getPrimitiveCount(sectionIndex);
                primIndex < primCount;
                primIndex++) {
              meshData.getPrimitiveIndices(primIndex, sectionIndex, indices);
              objPw.print("f");
              for (int vertexIndex = 0; vertexIndex < indices.length; vertexIndex++) {
                // indices start at 1 in the WaveFront OBJ format whereas indices start at 0 in
                // Ardor3D
                final int shiftedIndex = indices[vertexIndex] + 1 + firstVertexIndex;
                // vertex index
                objPw.print(" " + shiftedIndex);
                // texture coordinate index
                if (texCoordsData != null) {
                  objPw.print("/" + shiftedIndex);
                }
                // normal coordinate index
                if (normalsData != null) {
                  objPw.print("/" + shiftedIndex);
                }
              }
              objPw.println();
            }
            break;
          default:
            throw new IllegalArgumentException("index mode " + indexMode + " not supported");
        }
      }
    } catch (final Throwable t) {
      throw new Error("Unable to save the mesh into an obj", t);
    } finally {
      if (objPw != null) {
        objPw.flush();
        objPw.close();
      }
      if (mtlPw != null) {
        mtlPw.flush();
        mtlPw.close();
      }
    }
  }