How do I actually load a model in jME 2.0?

How do I actually load a model in jME 2.0? The feature list says jME can do it, but the user guide in the wiki is sketchy at best on this topic. It's missing most of the relevant sections. I've tried to make it work, but it seems to be for jME 1.0 with some corrections, which are more confusing than helpful.



I'm in a software engineering class making a game as a group project. I got assigned the task of figuring out the engine. We're all using Eclipse, if that helps.



I have some .3ds files with their .jpg texture files made by another group member. (if jME can't handle .3ds, I can probably convert it to something else.) I need to find the jME function that will take the relative path as input and spit out a node or something that I can just attach to the scenegraph.



If jME can't do that, then I'll need help writing a class that can, because loading a model into the scene shouldn't be any harder than that. (It will take too long to explain it to the group.) I'll even write it into the wiki if I get it working.


If you can't find the info for JME 2.0 (almost identical to 1.0) on the wiki, I suggest looking at the HelloModelLoading example in the actual source (under the jmetest.TutorialGuide package). It's pretty self-explanatory (get your model URLs, setup a new converter, set properties (e.g. texture urls), setup a ByteArrayOutputStream, use the converter to convert, then load it in with the BinaryImporter.



JME does support 3ds (and supports it well, some of the other importers I've found have been finicky, but the 3ds importer has almost never failed me). Do note the following for the MaxToJme converter:



This is how you set where your 3ds textures are:



setProperty("texurl", URL_TO_TEXTURE_DIR);



And you most likely will have to change the orientation of your model (Max is Z-up by default if I remember right)

Also be sure to make use of the ResourceLocatorTool. (example)

Here's the code I use, which is just stripped down from one of the jmetest examples (I don't use 3DS really but it worked for various quick tests).



    public static Node load3DS(URL modelURL) {
       if(modelURL == null) {System.err.println("3DS Loading error: URL was null."); return null;}
       Node r1 = null;
       try {
            MaxToJme C1 = new MaxToJme();
            ByteArrayOutputStream BO = new ByteArrayOutputStream();
            C1.convert(new BufferedInputStream(modelURL.openStream()), BO);
            r1 = (Node)BinaryImporter.getInstance().load(new ByteArrayInputStream(BO.toByteArray()));
            if (r1.getChild(0).getControllers().size() != 0) r1.getChild(0).getController(0).setSpeed(20);   // What is this?!
        } catch (IOException e) {
            System.err.println("Failed to load 3DS file" + e);
        }
        return r1;
    }

I've been playing with a model viewer that can tap World of Warcraft models.  Although it's not yet "perfect" (i.e. the scaling doesn't work right yet), you can see the code at:  http://code.google.com/p/jwowmodelview



If it's a standard format model that has an importer, then just check the samples.  There are plenty of examples that I've found that demonstrates loading models.

Okay, here's my first working attempt. I can call the load3ds from a SimpleGame extension, and attach it to the scenegraph. Let me know if I've made any serious mistakes. I think I should be calling close() on these streams? But I'm not sure where or how. It still doesn't load textures though.


package testing;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import com.jme.bounding.BoundingBox;
import com.jme.scene.Node;
import com.jme.util.export.binary.BinaryImporter;
import com.jmex.model.converters.FormatConverter;
import com.jmex.model.converters.MaxToJme;

public class ModelLoader {
   
   static final FormatConverter CONVERTER_3DS = new MaxToJme();
   
   // I'm thinking I should close() something or something? Can anyone fix it?
   public static Node load3ds(String modelpath) {
      Node output = null; // the geometry will go here.
      ByteArrayOutputStream outStream = new ByteArrayOutputStream();
      try {
         // read .3ds file into memory and convert it to a jME usable format.
         FileInputStream inStream = new FileInputStream(modelpath);
         CONVERTER_3DS.convert(inStream, outStream);
         // import the stream to jME and cast as a node
         output = (Node) BinaryImporter.getInstance().load(
               new ByteArrayInputStream(outStream.toByteArray()));
      } catch (FileNotFoundException e) {
         e.printStackTrace();
         System.err.println("File not found at:");
         System.err.println(modelpath);
         System.err.println();
      } catch (IOException e) {
         e.printStackTrace();
         System.err.println("Unable read model at:");
         System.err.println(modelpath);
         System.err.println();
      }
      /* The bounding box is an important optimization.
      * There is no point in rendering geometry outside of the
      * camera's field of view. However, testing whether each individual triangle
      * is visible is nearly as expensive as actually rendering it. So you don't
      * test every triangle. Instead, you just test the bounding box. If the box
      * isn't in view, don't bother looking for triangles inside.       */
      output.setModelBound(new BoundingBox());
      output.updateModelBound();
      return output;
   }
}



The ResourceLocatorTool is not well documented. I'm not really sure what it's for.

maybe this will help, this is what I use to load models:



    public static Spatial getModelFromDisk( final String[] textureDirs, final String modelPath ) throws Exception {

        if( modelPath == null ){
            throw new Exception( "ModelPath cannot be null!" );
        } else if( !isSupportedModel( modelPath ) ){
            throw new Exception( "Only 3Ds, MS3D, OBJ, and ASE models are currently supported!" );
        }

        Logger.getLogger( "" ).log( Level.INFO, "Reading from disk: " + modelPath );

        final URL modelUrl = Utils.getURL( modelPath );

        try{
            if( textureDirs != null ){
                for( String textureDir : textureDirs ){
                    final SimpleResourceLocator location = new SimpleResourceLocator( Utils.getURL( textureDir ) );
                    ResourceLocatorTool.addResourceLocator( ResourceLocatorTool.TYPE_TEXTURE, location );
                }
            }

        } catch( Exception e ){
        }


        FormatConverter converter = null;
        final String extension = FileUtils.getExtension( modelPath );

        if( extension.equalsIgnoreCase( ".ms3d" ) ){
            converter = new MilkToJme();

        } else if( extension.equalsIgnoreCase( ".3ds" ) ){
            converter = new MaxToJme();

        } else if( extension.equalsIgnoreCase( ".obj" ) ){
            converter = new ObjToJme();

        } else if( extension.equalsIgnoreCase( ".ase" ) ){
            converter = new AseToJme();
        }

        final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        final BufferedInputStream rawInputStream = new BufferedInputStream( modelUrl.openStream() );

        converter.convert( rawInputStream, byteStream );

        final ByteArrayInputStream inputStream = new ByteArrayInputStream( byteStream.toByteArray() );
        final Spatial output = (Spatial) BinaryImporter.getInstance().load( inputStream );
       
        byteStream.close();
        rawInputStream.close();
        inputStream.close();

        return output;
    }




And the getURL helper method:


    public static URL getURL( String path ) {

        if( path == null ){
            return null;
        }

        if( path.contains( "://" ) || path.contains( "file:" ) ){
            try{
                return new URL( path );
            } catch( MalformedURLException e ){
            }
        }

        URL url = Thread.currentThread().getContextClassLoader().getResource( path );

        if( url == null ){
            try{
                url = new File( path ).toURI().toURL();
            } catch( MalformedURLException e ){
            }
        }

        return url;
    }

The ResourceLocaterTool is 'queried' for the texture (among other things) when a model is loaded…



Also, if a model only has a single mesh the converter will return a TriMesh; only when there are multiple meshes will it return a Node.  Therefore it is much safer to return a Spatial (which TriMesh and Node both extend), then cast to the appropriate class…

Okay, here's attempt number two. It loads textures now, at least for my two test models. I think this is probably good enough for our purposes, but I'm not sure if I should put it in the wiki.

package testing;



import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.net.URISyntaxException;





import com.jme.bounding.BoundingBox;

import com.jme.scene.Spatial;

import com.jme.util.export.binary.BinaryImporter;

import com.jme.util.resource.ResourceLocatorTool;

import com.jme.util.resource.SimpleResourceLocator;

import com.jmex.model.converters.FormatConverter;

import com.jmex.model.converters.MaxToJme; //.3ds





public class ModelLoader {

   

   static final FormatConverter CONVERTER_3DS = new MaxToJme();

   

   /

    * Imports a .3ds model from file system. Like the 2-argument method

    * with a null textureDir. The texture file(s) are assumed to be in the

    * same directory as the model file.

    * @param modelPath the path to the model file.

    * Can be relative to the project directory.

    * @return a Spatial containing the model geometry

    * (with provided texture, if any) that can be attached to

    *  the scenegraph, or null instead if unable to load geometry.

    */

   public static Spatial load3ds(String modelPath) {

      return load3ds(modelPath, null);

   }

   

   /


    * Imports a .3ds model from file system.

    * @param modelPath the path to the model file.

    * Can be relative to the project directory.

    * @param textureDir the path to the directory with the model's

      textures. If null, this will attempt to infer textureDir from

      modelPath, which assumes that the texture file(s) are in the same

      directory as the model file.

    * @return a Spatial containing the model geometry

    * (with provided texture, if any) that can be attached to

      the scenegraph, or null instead if unable to load geometry

    /

   public static Spatial load3ds(String modelPath, String textureDir) {

      Spatial output = null; // the geometry will go here.

      final ByteArrayOutputStream outStream =

         new ByteArrayOutputStream();

      try {

         final File textures;

         if(textureDir != null) { // set textureDir location

            textures = new File( textureDir );

         } else {// try to infer textureDir from modelPath.

            textures = new File(

                  modelPath.substring(0, modelPath.lastIndexOf('/')) );

         }   // Add texture URL to auto-locator

         final SimpleResourceLocator location =

            new SimpleResourceLocator(textures.toURI().toURL());

            ResourceLocatorTool.addResourceLocator(

                  ResourceLocatorTool.TYPE_TEXTURE, location );

                  

         // read .3ds file into memory & convert it to a jME usable format.

         final FileInputStream rawIn = new FileInputStream(modelPath);

         CONVERTER_3DS.convert(rawIn, outStream);

         rawIn.close();

         

         // prepare outStream for loading.

         final ByteArrayInputStream convertedIn =

            new ByteArrayInputStream(outStream.toByteArray());

         

         // import the converted stream to jME as a Spatial

         output = (Spatial) BinaryImporter.getInstance().load(convertedIn);

      } catch (FileNotFoundException e) {

         e.printStackTrace();

         System.err.println("File not found at: " + modelPath);

      } catch (IOException e) {

         e.printStackTrace();

         System.err.println("Unable read model at: " + modelPath);

      } catch (URISyntaxException e) {

         e.printStackTrace();

         System.err.println("Invalid texture location at:" + textureDir);

      }   /


      
The bounding box is an important optimization.

      
There is no point in rendering geometry outside of the camera's

      
field of view. However, testing whether each individual triangle

      
is visible is nearly as expensive as actually rendering it. So you

      * don't test every triangle. Instead, you just test the bounding box.

      * If the box isn't in view, don't bother looking for triangles inside.

         */

      output.setModelBound(new BoundingBox());

      output.updateModelBound();

      return output;

   }

}

Apperently, basixs, closing a ByteArrayInputStream or ByteArrayOutputStream has no effect whatsoever. It says so in the Javadoc. I still close() the FileInputStream though.

yeah, IMO always a good habit to close a stream unless its needed…

Hi,



is the texture also displayed, when you assign it in 3ds?

Obviously there is a problem loading models AND their texture. I can't explain why, but only the "bike.3ds" from Flagrush was imported correctly - with texture.

If I try to export my Milkshape model myself to .3ds the "skin" is lost. The same happens when I use the MilkToJme Converter or the ObjToJme (after exporting from milk to .obj)



The only way to put a texture on my models is to do it manually:



// ts is my renderstate

      Texture terrainTexture = TextureManager.loadTexture(
                Terrain.class.getClassLoader().getResource("meshes/terrain1.png"),
                Texture.MinificationFilter.BilinearNearestMipMap,
                Texture.MagnificationFilter.Bilinear);
      this.ts.setTexture(terrainTexture,0);
      terrainModel.setRenderState(ts);



But how can I put e.g. 3 different textures on ONE model manually, of course each texture should cover an other area?


Thanks for your help!

I have no problem loading milkshape, 3DS, OBJ or ASE models here…

So could you please tell me, wheter the ms3d modell in the attachment is correctly displayed ingame?

(I renamed the file to .txt, just to load ot up)

I've also uploaded a screenshot out of Milkshape, how it should look like.



Here's my code, to load the model:

(excerpt from class Terrain which loads the model in it's constructor)

        MilkToJme converter = new MilkToJme();
        ByteArrayOutputStream BO = new ByteArrayOutputStream();
        URL maxFile = Terrain.class.getClassLoader().getResource("jmetest/data/model/terrain_milk.ms3d");
        converter.convert(new BufferedInputStream(maxFile.openStream()),BO);
        terrainModel = (Spatial)BinaryImporter.getInstance().load(new ByteArrayInputStream(BO.toByteArray()));
        terrainModel.setLocalScale(2f);
        terrainModel.setModelBound(new BoundingBox());
        terrainModel.updateModelBound();



And here to attach to the scene node:

        rootNode = new Node("Scene graph node");

        terrain = new Terrain("foo", display.getRenderer().createTextureState());
        rootNode.attachChild(terrain.getTerrainModel());

        //update the scene graph for rendering
        rootNode.updateGeometricState(0.0f, true);
        rootNode.updateRenderState();



Is this correct, or do I need to change a certain setting so that textures are taken from the original ms3d file?
In my eyes it is unlikely that something is missing, because i tried out the bike.3ds from flagrush and the textures were visible.
Assuming that the 3ds format is more compatible, I converted my own model in 3ds (out of Milkshape) but it did not work.

Thanks

i think your model got corrupted when i downloaded it as a txt file, the jme importer throws a IndexOutOfBounds Exception and milkshape3d dies when i try to load it.

so I uploades it somewhere else as the original file and one assigned texture.

In the middle of the terrain some area should stay grey because there is no texure assigned at the moment



Model:

http://www.upload4free.com/download.php?file=547294609-terrain_milk.ms3d



Texture:

http://www.upload4free.com/download.php?file=257378780-terrain1.png



They should be placed in the same directory to fin each other

The path: "./" which is passed to the ResourceLocatorTool, means the root directory where i put the ms3d and texture.



If your resource were in a "resources/models" package, you would pass "resources/models/"  (notice the trailing slash).



src/Test.java

    terrain_milk.ms3d

    terrain1.png



    @Override
    protected void simpleInitGame() {
        try {
            ResourceLocatorTool.addResourceLocator(
                    ResourceLocatorTool.TYPE_TEXTURE,new SimpleResourceLocator(
                            Test.class.getClassLoader().getResource("./")));
            ResourceLocatorTool.addResourceLocator(
                    ResourceLocatorTool.TYPE_MODEL,new SimpleResourceLocator(
                            Test.class.getClassLoader().getResource("./")));
        } catch (URISyntaxException e1) {
            e1.printStackTrace();
        }
       
        Spatial terrainModel = null;
        MilkToJme converter = new MilkToJme();
        ByteArrayOutputStream BO = new ByteArrayOutputStream();
        URL maxFile = ResourceLocatorTool.locateResource( ResourceLocatorTool.TYPE_MODEL, "terrain_milk.ms3d");
        try {
            converter.convert(new BufferedInputStream(maxFile.openStream()),BO);
            terrainModel = (Spatial)BinaryImporter.getInstance().load(new ByteArrayInputStream(BO.toByteArray()));
        } catch (IOException e) {
            e.printStackTrace();
        }
        terrainModel.setLocalScale(2f);
        terrainModel.setModelBound(new BoundingBox());
        terrainModel.updateModelBound();
        rootNode.attachChild(terrainModel);
    }

This would mean, that I have to use the ResourceLocator, becaus everything else is equal to my code.



But what is the reason? The model file is obviously found, doing it my way…



I try to do use the ResourceLocatorTool.



UPDATE:

Wonderful, it works. But I don't understand why.

Just the RessourceocatorTool thing…



Maybe JME is not able to handle the information stored in the model file about the texture if you don't use the Locator tool.





But nevertheless thank you a lot!

jme uses the ResourceLocator internally to locate textures.



if you load a Model, you need to tell the model loader somehow where to look for the textures.

You do that either by using absolute or relative paths in your model file (which is bad cause paths will change), or you set the directory when loading the model.



The ResourceLocatorTool tries to fix those problems.

You just specify in which folders / packages you place the resources and the ResourceLocator will find them for you.