/**
* Copyright (c) 2003-2009, Xith3D Project Group all rights reserved.
*
* Portions based on the Java3D interface, Copyright by Sun Microsystems.
* Many thanks to the developers of Java3D and Sun Microsystems for their
* innovation and design.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the 'Xith3D Project Group' nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) A
* RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE
*/
package org.xith3d.loaders.models;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import org.jagatoo.datatypes.NamedObject;
import org.openmali.spatial.bodies.Frustum;
import org.openmali.vecmath2.Matrix4f;
import org.xith3d.loaders.models.animations.AnimationListener;
import org.xith3d.loaders.models.animations.ModelAnimation;
import org.xith3d.loaders.models.animations._Anim_PrivilegedAccess;
import org.xith3d.loaders.models.util.meta.ModelMetaData;
import org.xith3d.loop.UpdatingThread.TimingMode;
import org.xith3d.scenegraph.Fog;
import org.xith3d.scenegraph.Group;
import org.xith3d.scenegraph.GroupNode;
import org.xith3d.scenegraph.Light;
import org.xith3d.scenegraph.Node;
import org.xith3d.scenegraph.UpdatableNode;
import org.xith3d.scenegraph.Shape3D;
import org.xith3d.scenegraph.Sound;
import org.xith3d.scenegraph.TransformGroup;
import org.xith3d.scenegraph.View;
import org.xith3d.scenegraph.primitives.SkyBox;
import org.xith3d.scenegraph.utils.CopyListener;
/**
* This class represents a Model loaded from some model file formats.
* This class is responsible for both the storage and retrieval of data from
* the Model. The storage methods (used only by Loader writers) are all
* of the add*() routines. The retrieval methods (used primarily by Loader
* users) are all of the get*() routines.
*
* @author Marvin Freohlich (aka Qudus)
* @author Andrew Hanson (aka Patheros) [added ModelMetaData]
*/
public class Model extends Group implements UpdatableNode
{
private static final boolean debug = false;
private static boolean defaultPickHostValue = true;
private TransformGroup[] nestedTransforms = null;
private Shape3D[] shapes = null;
private org.xith3d.utility.geometry.NormalsVisualizer[] nvs;
private Light[] lights = null;
private Fog[] fogs = null;
private Sound[] sounds = null;
private View[] cameras = null;
private Matrix4f[] spawnTransforms = null;
private GroupNode mainSceneGroup = null;
private SkyBox skyBox = null;
private TreeMap<String, NamedObject> namedObjects = null;
private final HashMap<String, ModelAnimation> animsMap = new HashMap<String, ModelAnimation>();
private ModelAnimation[] anims = null;
private TransformGroup[] mountTransforms = null;
private ModelMetaData metaData = null;
private float animationStartTime = -1f;
private ModelAnimation currentAnimation = null;
private final ArrayList<AnimationListener> animListeners = new ArrayList<AnimationListener>();
private boolean finishedFired = false;
/**
* Sets the initial (default) value for a new Model's pick-host flag.
*
* @param value
*/
public static void setDefaultPickHost( boolean value )
{
Model.defaultPickHostValue = value;
}
/**
* @return the initial (default) value for a new Model's pick-host flag.
*/
public static boolean getDefaultPickHost()
{
return ( Model.defaultPickHostValue );
}
protected void setNestedTransforms( TransformGroup[] nestedTransforms )
{
this.nestedTransforms = nestedTransforms;
}
/**
* @param index
*
* @return the nested TransformGroup by the given index.
*/
public final TransformGroup getNestedTransform( int index )
{
return ( nestedTransforms[index] );
}
/**
* @return the list of nested TransformGroups in this model.
*/
public final TransformGroup[] getNestedTransforms()
{
return ( nestedTransforms );
}
protected void setShapes( Shape3D[] shapes )
{
this.shapes = shapes;
if ( debug )
{
this.nvs = new org.xith3d.utility.geometry.NormalsVisualizer[ shapes.length ];
for ( int i = 0; i < nvs.length; i++ )
{
nvs[i] = new org.xith3d.utility.geometry.NormalsVisualizer( shapes[i], 0.5f );
this.addChild( nvs[i] );
}
}
}
/**
* @return the number of Shape nodes defined in the file.
*/
public final int getShapesCount()
{
if ( shapes == null )
return ( 0 );
return ( shapes.length );
}
/**
* @param index
*
* @return a Shape contained in the model.
*/
public final Shape3D getShape( int index )
{
return ( shapes[index] );
}
/**
* @return an array of all Shapes contained in the model.
*/
public final Shape3D[] getShapes()
{
return ( shapes );
}
protected void setLights( Light[] lights )
{
this.lights = lights;
}
/**
* @return the number of Light nodes defined in the file.
*/
public final int getLightsCount()
{
if ( lights == null )
return ( 0 );
return ( lights.length );
}
/**
* @return a Light defined in the file.
*/
public final Light getLight( int index )
{
return ( lights[index] );
}
/**
* @return an array of all Lights defined in the file (may be null).
*/
public final Light[] getLights()
{
return ( lights );
}
protected void setFogs( Fog[] fogs )
{
this.fogs = fogs;
}
/**
* @return the number of Fog nodes defined in the file.
*/
public final int getFogssCount()
{
if ( fogs == null )
return ( 0 );
return ( fogs.length );
}
/**
* @return a Fog defined in the file.
*/
public final Fog getFog( int index )
{
return ( fogs[index] );
}
/**
* @return an array of all Fogs defined in the file (may be null).
*/
public final Fog[] getFogs()
{
return ( fogs );
}
protected void setSounds( Sound[] sounds )
{
this.sounds = sounds;
}
/**
* @return the number of Sound nodes defined in the file.
*/
public final int getSoundsCount()
{
if ( sounds == null )
return ( 0 );
return ( sounds.length );
}
/**
* @return a Sound node defined in the file.
*/
public final Sound getSound( int index )
{
return ( sounds[index] );
}
/**
* @return an array of all the Sound nodes defined
* in the file (may be null).
*/
public final Sound[] getSounds()
{
return ( sounds );
}
protected void setCameras( View[] cameras )
{
this.cameras = cameras;
}
/**
* @return the number of cameras defined in the file.
*/
public final int getCamerasCount()
{
if ( cameras == null )
return ( 0 );
return ( cameras.length );
}
/**
* @return a camera defined in the file.
*/
public final View getCamera( int index )
{
return ( cameras[index] );
}
/**
* @return an array of all the cameras defined
* in the file (may be null).
*/
public final View[] getCameras()
{
return ( cameras );
}
protected void setSpawnTransforms( Matrix4f[] spawnTransforms )
{
this.spawnTransforms = spawnTransforms;
}
/**
* @return the number of spawn transforms.
*/
public final int getSpawnTransformsCount()
{
if ( spawnTransforms == null )
return ( 0 );
return ( spawnTransforms.length );
}
/**
* @return a spawn transform defined in this scene.
*/
public final Matrix4f getSpawnTransform( int index )
{
return ( spawnTransforms[index] );
}
/**
* @return an array of all spawn transforms defined in this scene.
*/
public final Matrix4f[] getSpawnTransforms()
{
return ( spawnTransforms );
}
protected void setMainGroup( GroupNode mainGroup )
{
this.mainSceneGroup = mainGroup;
}
/**
* @return the main group of this model.
* This may be the model itself of a nested BSPTreeGroup, OcTreeGroup, etc.
*/
public final GroupNode getMainGroup()
{
return ( mainSceneGroup );
}
protected void setSkyBox( SkyBox skyBox )
{
this.skyBox = skyBox;
}
/**
* @return this scene's SkyBox (if any, null otherwise).
*/
public final SkyBox getSkyBox()
{
return ( skyBox );
}
/**
* Adds the given String/Object pair to the table of named objects.
*/
protected void addNamedObject( String name, NamedObject object )
{
if ( namedObjects == null )
{
namedObjects = new TreeMap<String, NamedObject>();
}
NamedObject existingObject = namedObjects.get( name );
if ( existingObject == null )
{
namedObjects.put( name, object );
}
else if ( existingObject != object )
{
// key already exists - append a unique integer to end of name
int nameIndex = 1;
boolean done = false;
while ( !done )
{
// Iterate starting from "[1]" until we find a unique key
String tempName = name + "[" + nameIndex + "]";
if ( namedObjects.get( tempName ) == null )
{
namedObjects.put( tempName, object );
done = true;
}
nameIndex++;
}
}
}
/**
* @return the number of named objects in this model file.
*/
public final int getNamedObjectsCount()
{
if ( namedObjects == null )
return ( 0 );
return ( namedObjects.size() );
}
/**
* @return a Map, which contains a list of all named
* objects in the file and their associated scene graph objects. The
* naming scheme for file objects is file-type dependent, but may include
* such names as the DEF names of Vrml or filenames of objects (as
* in Lightwave 3D).
*/
public final Map<String, NamedObject > getNamedObjects()
{
return ( namedObjects );
}
/**
* @return the named object with the given name.
* The naming scheme for file objects is file-type dependent, but may
* include such names as the DEF names of Vrml or filenames of subjects
* (as in Lightwave 3D).
*
* @param name the name of the named object to retrieve
*/
public final NamedObject getNamedObject( String name )
{
if ( namedObjects == null )
return ( null );
return ( namedObjects.get( name ) );
}
public void dumpNamedObjects( boolean printValues )
{
if ( namedObjects == null )
{
System.out.println( "[No named objects]" );
}
else
{
for ( String name : namedObjects.keySet() )
{
if ( printValues )
System.out.println( "\"" + name + "\": " + namedObjects.get( name ) );
else
System.out.println( "\"" + name + "\"" );
}
}
}
/**
* Sets the meta data for this object.
* Primalaly used by MetaLoader
*/
public void setMetaData( ModelMetaData metaData )
{
this.metaData = metaData;
}
/**
* @return the meta data associated with this object.
* Usualy only objects loaded from the MetaLoader have meta data.
* For objects without meta data this returns null.
*/
public final ModelMetaData getMetaData()
{
return ( metaData );
}
public void setMountTransforms( TransformGroup[] mountTransforms )
{
if ( this.mountTransforms != null )
{
for ( int i = this.mountTransforms.length - 1; i >= 0; i-- )
{
this.removeChild( this.mountTransforms[i] );
}
}
this.mountTransforms = mountTransforms;
if ( this.mountTransforms != null )
{
for ( int i = 0; i < this.mountTransforms.length; i++ )
{
this.addChild( this.mountTransforms[i] );
}
}
}
public final int getMountTransformsCount()
{
if ( mountTransforms == null )
return ( 0 );
return ( mountTransforms.length );
}
public final TransformGroup[] getMountTransforms()
{
return ( mountTransforms );
}
public final TransformGroup getMountTransform( int index )
{
return ( mountTransforms[index] );
}
public final TransformGroup getMountTransform( String name )
{
// We don't need a Map here, since there will be very few MTs!
for ( int i = 0; i < mountTransforms.length; i++ )
{
if ( mountTransforms[i].getName().equals( name ) )
return ( mountTransforms[i] );
}
return ( null );
}
public void addAnimationListener( AnimationListener l )
{
this.animListeners.add( l );
}
public void removeAnimationListener( AnimationListener l )
{
this.animListeners.remove( l );
}
protected final void fireOnAnimationStarted( ModelAnimation anim )
{
if ( anim != null )
{
for ( int i = 0; i < animListeners.size(); i++ )
{
animListeners.get( i ).onAnimationStarted( (ModelAnimation)anim );
}
finishedFired = false;
}
}
protected final void fireOnAnimationFinished( ModelAnimation anim )
{
if ( ( anim != null ) && !finishedFired )
{
finishedFired = true;
for ( int i = 0; i < animListeners.size(); i++ )
{
animListeners.get( i ).onAnimationFinished( (ModelAnimation)anim );
}
}
}
private void addAnimation( ModelAnimation anim )
{
if ( animsMap.containsKey( anim.getName() ) )
return;
_Anim_PrivilegedAccess.setModel( this, anim );
animsMap.put( anim.getName(), anim );
if ( anims == null )
{
anims = new ModelAnimation[ 64 ];
}
else if ( anims.length < animsMap.size() )
{
ModelAnimation[] tmp = new ModelAnimation[ (int)( ( anims.length + 1 ) * 1.5 ) ];
System.arraycopy( anims, 0, tmp, 0, anims.length );
anims = tmp;
}
anims[ animsMap.size() - 1 ] = anim;
}
public void setAnimations( ModelAnimation[] anims )
{
animsMap.clear();
this.anims = null;
for ( int i = 0; i < anims.length; i++ )
{
addAnimation( anims[i] );
}
}
/**
* @return true, if the Model contains at least one animation.
*/
public final boolean hasAnimations()
{
return ( !animsMap.isEmpty() );
}
/**
* @return the number of animations for this model.
*/
public final int getAnimationsCount()
{
return ( animsMap.size() );
}
/**
* @return a List of all animations contained in the model
*/
public ModelAnimation[] getAnimations()
{
if ( animsMap.size() == 0 )
return ( null );
if ( anims.length != animsMap.size() )
{
ModelAnimation[] tmp = new ModelAnimation[ animsMap.size() ];
System.arraycopy( anims, 0, tmp, 0, animsMap.size() );
anims = tmp;
}
return ( anims );
}
/**
* @return an animation by index
*/
public ModelAnimation getAnimation( int index )
{
if ( index >= animsMap.size() )
throw new ArrayIndexOutOfBoundsException( index );
return ( anims[index] );
}
/**
* @return an animation by name
*/
public ModelAnimation getAnimation( String name )
{
return ( animsMap.get( name ) );
}
/**
* Dumps all animations contained in the model.
*/
public final void dumpAnimations()
{
if ( animsMap.size() == 0 )
{
System.out.println( "This model doesn't have any animations." );
return;
}
for ( int i = 0; i < anims.length; i++ )
{
System.out.println( anims[i] );
}
}
/**
* Sets the current animation being used.
*
* @param anim the animation to use
*/
public void setCurrentAnimation( ModelAnimation anim )
{
if ( ( getCurrentAnimation() != null ) && ( animationStartTime >= 0f ) )
{
fireOnAnimationFinished( getCurrentAnimation() );
}
currentAnimation = anim;
animationStartTime = -1f;
if ( anim != null )
{
anim.reset();
fireOnAnimationStarted( anim );
}
}
/**
* Sets the current animation being used.
*
* @param name the animation to use
*/
public final void setCurrentAnimation( String name )
{
setCurrentAnimation( animsMap.get( name ) );
}
/**
* Sets the current animation being used.
*
* @param index the animation to use
*/
public final void setCurrentAnimation( int index )
{
setCurrentAnimation( anims[index] );
}
/**
* @return the current animation being used.
*/
public final ModelAnimation getCurrentAnimation()
{
return ( currentAnimation );
}
/**
* Interpolates the animations towards the next frame.
*
* @param animStartTime the game-time, at which the current loop of the animation started
* @param absAnimTime the amount of game-time, the current loop of the current animation runs
*/
public void interpolateAnimation( float animStartTime, float absAnimTime )
{
if ( getCurrentAnimation() == null )
return;
ModelAnimation anim = getCurrentAnimation();
if ( anim.update( false, absAnimTime, getMountTransforms() ) )
{
fireOnAnimationFinished( anim );
fireOnAnimationStarted( anim );
}
if ( debug )
{
for ( int i = 0; i < nvs.length; i++ )
{
nvs[i].update();
}
}
}
/**
* {@inheritDoc}
*/
public boolean update( View view, Frustum frustum, long nanoTime, long nanoStep )
{
if ( getCurrentAnimation() != null )
{
float gameSeconds = TimingMode.NANOSECONDS.getSecondsAsFloat( nanoTime );
if ( animationStartTime < 0f )
{
animationStartTime = gameSeconds;
}
interpolateAnimation( animationStartTime, ( gameSeconds - animationStartTime ) );
}
return ( true );
}
/**
* @return a new Instance of this Model. It will at least share it's
* Geometry and animation data with this one.
*/
public Model getSharedInstance()
{
final Model newModel = new Model();
final ArrayList<TransformGroup> newNestedTransforms = new ArrayList<TransformGroup>();
final ArrayList<Shape3D> newShapes = new ArrayList<Shape3D>();
final ArrayList<Light> newLights = new ArrayList<Light>();
final ArrayList<Fog> newFogs = new ArrayList<Fog>();
final ArrayList<Sound> newSounds = new ArrayList<Sound>();
CopyListener listener = new CopyListener()
{
public void onNodeCopied( Node original, Node newInstance, boolean shared )
{
if ( original == Model.this.mainSceneGroup )
newModel.mainSceneGroup = (GroupNode)newInstance;
if ( newInstance instanceof TransformGroup )
newNestedTransforms.add( (TransformGroup)newInstance );
else if ( newInstance instanceof Shape3D )
newShapes.add( (Shape3D)newInstance );
else if ( newInstance instanceof Light )
newLights.add( (Light)newInstance );
else if ( newInstance instanceof Fog )
newFogs.add( (Fog)newInstance );
else if ( newInstance instanceof Sound )
newSounds.add( (Sound)newInstance );
if ( ( newInstance.getName() != null ) && ( newInstance.getName().length() > 0 ) )
newModel.addNamedObject( newInstance.getName(), newInstance );
}
};
for ( int i = 0; i < numChildren(); i++ )
{
newModel.addChild( this.getChild( i ).sharedCopy( listener ) );
}
if ( newNestedTransforms.size() > 0 )
{
newModel.setNestedTransforms( newNestedTransforms.toArray( new TransformGroup[ newNestedTransforms.size() ] ) );
}
newModel.setShapes( newShapes.toArray( new Shape3D[ newShapes.size() ] ) );
if ( newLights.size() > 0 )
{
newModel.setLights( newLights.toArray( new Light[ newLights.size() ] ) );
}
if ( newFogs.size() > 0 )
{
newModel.setFogs( newFogs.toArray( new Fog[ newFogs.size() ] ) );
}
if ( newSounds.size() > 0 )
{
newModel.setSounds( newSounds.toArray( new Sound[ newSounds.size() ] ) );
}
newModel.setCameras( this.getCameras() );
newModel.setSpawnTransforms( this.getSpawnTransforms() );
newModel.setSkyBox( this.getSkyBox() );
if ( hasAnimations() )
{
for ( ModelAnimation animation : getAnimations() )
{
newModel.addAnimation( animation.getSharedCopy( newModel.getNamedObjects() ) );
}
}
if ( this.getMountTransforms() != null )
{
TransformGroup[] newMTs = new TransformGroup[ getMountTransformsCount() ];
for ( int i = 0; i < getMountTransformsCount(); i++ )
{
newMTs[i] = new TransformGroup( this.getMountTransform( i ).getTransform() );
}
}
return ( newModel );
}
public Model()
{
super();
this.setPickHost( Model.defaultPickHostValue );
}
}