Lecture 16 – May 14, 2003
Splines
boston.gif
environs.gif
house.gif
sky.gif
The virtual city is composed of the following elements:
• A square with a high-resolution satellite image of the city texture mapped onto it.
• A larger square with a lower resolution satellite image applied.
• A large blue square to act as a base for the model and supply a consistent horizon.
• A cloudy sky backdrop texture mapped onto a Background Sphere.
• Some randomly created texture mapped buildings (Boxes).
heli.obj
SplineInterpolatorTest.java
/**
* This example creates a 3D fly-over of the city of Boston.
* The viewer is animated using a RotPosScaleTCBSplinePathInterpolator
* as well as 3 helicopters. The example uses PointSounds attached
* to the helicopters to generate 3D spatial audio.
*/
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.awt.image.*;
import javax.media.j3d.*;
import javax.vecmath.*;
import com.sun.j3d.utils.applet.MainFrame;
import com.sun.j3d.utils.geometry.*;
import com.sun.j3d.utils.image.*;
import com.sun.j3d.utils.behaviors.interpolators.*;
import com.sun.j3d.audioengines.javasound.*;
import org.selman.java3d.book.common.*;
/*
* This example uses a Spline Interpolator
* to animate a fly-over the city of Boston.
* The city is rendered using satellite images
* with a Level of Detail behavior. The scene includes
* a number of moving helicopters, each with an associated
* sound.
*/
public class SplineInterpolatorTest extends Java3dApplet
{
// size of the 3D window - enlage on powerful systems
private static int m_kWidth = 200;
private static int m_kHeight = 200;
// a shared appearance for the buildings we create
private Appearance m_BuildingAppearance = null;
// the size of the high resolution "world".
// the world is centered at 0,0,0 and extends
// to +- LAND_WIDTH in the x direction and
//+- LAND_LENGTH in the z direction.
// These dimensions are loosely based on pixel
// coordinates from the texture images
private static final floatLAND_WIDTH = 180;
private static final floatLAND_LENGTH = 180;
// the satellite images used as textures have
// been manually edited so that the water in the
// images corresponds to the following RGB values.
// this allows the application to avoid creating
// buildings in the water!
private static final floatWATER_COLOR_RED = 0;
private static final floatWATER_COLOR_GREEN = 57;
private static final floatWATER_COLOR_BLUE = 123;
public SplineInterpolatorTest( )
{
initJava3d( );
}
// scale eveything so we can use pixel coordinates
protected double getScale( )
{
return 0.1;
}
protected int getCanvas3dWidth( Canvas3D c3d )
{
return m_kWidth;
}
protected int getCanvas3dHeight( Canvas3D c3d )
{
return m_kHeight;
}
protected Bounds createApplicationBounds( )
{
m_ApplicationBounds = new BoundingSphere( new Point3d( 0.0,0.0,0.0 ), 10.0 );
return m_ApplicationBounds;
}
// we want a texture mapped background of a sky
protected Background createBackground( )
{
// add the sky backdrop
Background back = new Background( );
back.setApplicationBounds( getApplicationBounds( ) );
BranchGroup bgGeometry = new BranchGroup( );
// create an appearance and assign the texture image
Appearance app = new Appearance( );
Texture tex = new TextureLoader( "sky.gif", this ).getTexture( );
app.setTexture( tex );
Sphere sphere = new Sphere( 1.0f, Primitive.GENERATE_TEXTURE_COORDS | Primitive.GENERATE_NORMALS_INWARD, app );
bgGeometry.addChild( sphere );
back.setGeometry( bgGeometry );
return back;
}
// this controls how close to a helicopter we can
// be and still hear it. If the helicopters sound
// scheduling bounds intersect our ViewPlatformActivationRadius
// the sound of the helicopter is potentially audible.
protected float getViewPlatformActivationRadius( )
{
return 20;
}
// creates the objects within our world
protected BranchGroup createSceneBranchGroup( )
{
BranchGroup objRoot = super.createSceneBranchGroup( );
// create a root TG in case we need to scale the scene
TransformGroup objTrans = new TransformGroup( );
objTrans.setCapability( TransformGroup.ALLOW_TRANSFORM_WRITE );
objTrans.setCapability( TransformGroup.ALLOW_TRANSFORM_READ );
Transform3D t3d = new Transform3D( );
objTrans.setTransform( t3d );
Group hiResGroup = createLodLand( objTrans );
createBuildings( objTrans );
createHelicopters( objTrans );
// connect
objRoot.addChild( objTrans );
return objRoot;
}
// we create 2 TransformGroups above the ViewPlatform:
// the first merely applies a scale, while the second
// has a RotPosScaleTCBSplinePathInterpolator attached
// so that the viewer of the scene is animated along
// a spline curve.
public TransformGroup[] getViewTransformGroupArray( )
{
TransformGroup[] tgArray = new TransformGroup[2];
tgArray[0] = new TransformGroup( );
tgArray[1] = new TransformGroup( );
Transform3D t3d = new Transform3D( );
t3d.setScale( getScale( ) );
t3d.invert( );
tgArray[0].setTransform( t3d );
// create an Alpha object for the Interpolator
Alpha alpha = new Alpha( -1,
Alpha.INCREASING_ENABLE | Alpha.DECREASING_ENABLE,
0,
0,
25000,
4000,
100,
20000,
5000,
50 );
// ensure the Interpolator can access the TG
tgArray[1].setCapability( TransformGroup.ALLOW_TRANSFORM_WRITE );
try
{
// create the Interpolator and load the keyframes from disk
RotPosScaleTCBSplinePathInterpolator splineInterpolator =
Utils.createSplinePathInterpolator( new UiAlpha( alpha ),
tgArray[1],
new Transform3D( ),
new URL( getWorkingDirectory( ), "rotate_viewer_spline.xls" ) );
// set the scheduling bounds and attach to the scenegraph
splineInterpolator.setSchedulingBounds( getApplicationBounds( ) );
tgArray[1].addChild( splineInterpolator );
}
catch( Exception e )
{
System.err.println( e.toString( ) );
}
return tgArray;
}
// overidden so that the example can use audio
protected AudioDevice createAudioDevice( PhysicalEnvironment pe )
{
return new JavaSoundMixer( pe );
}
// creates a Switch group that contains two versions
// of the world - the first is a high resolution version,
// the second if a lower resolution version.
public Group createLodLand( Group g )
{
Switch switchNode = new Switch( );
switchNode.setCapability( Switch.ALLOW_SWITCH_WRITE );
Group hiResGroup = createLand( switchNode );
createEnvirons( switchNode );
// create a DistanceLOD that will select the child of
// the Switch node based on distance. Here we are selecting
// child 0 (high res) if we are closer than 180 units to
// 0,0,0 and child 1 (low res) otherwise.
float[] distanceArray = {180};
DistanceLOD distanceLod = new DistanceLOD( distanceArray );
distanceLod.setSchedulingBounds( getApplicationBounds( ) );
distanceLod.addSwitch( switchNode );
g.addChild( distanceLod );
g.addChild( switchNode );
return hiResGroup;
}
// creates a high resolution representation of the world.
// a single texture mapped square and a larger (water colored)
// square to act as a horizon.
public Group createLand( Group g )
{
Land land = new Land( this, g, ComplexObject.GEOMETRY | ComplexObject.TEXTURE );
Group hiResGroup = land.createObject( new Appearance( ), new Vector3d( ), new Vector3d( LAND_WIDTH,1,LAND_LENGTH ) , "boston.gif", null, null );
Appearance app = new Appearance( );
app.setColoringAttributes( new ColoringAttributes( WATER_COLOR_RED/255f, WATER_COLOR_GREEN/255f,WATER_COLOR_BLUE/255f, ColoringAttributes.FASTEST ) );
Land base = new Land( this, hiResGroup, ComplexObject.GEOMETRY );
base.createObject( app, new Vector3d( 0,-5,0 ), new Vector3d( 4 * LAND_WIDTH,1,4 * LAND_LENGTH ), null, null, null );
return hiResGroup;
}
// creates a low resolution version of the world and
// applies the low resolution satellite image
public Group createEnvirons( Group g )
{
Land environs = new Land( this, g, ComplexObject.GEOMETRY | ComplexObject.TEXTURE );
return environs.createObject( new Appearance( ), new Vector3d( ), new Vector3d( 2 * LAND_WIDTH,1, 2 * LAND_LENGTH ) , "environs.gif", null, null );
}
// returns true if the given x,z location in the world
// corresponds to water in the satellite image
protected boolean isLocationWater( BufferedImage image, float posX, float posZ )
{
Color color = null;
float imageWidth = image.getWidth( );
float imageHeight = image.getHeight( );
// range from 0 to 1
float nPixelX = (posX + LAND_WIDTH)/(2 * LAND_WIDTH);
float nPixelY = (posZ + LAND_LENGTH)/(2 * LAND_LENGTH);
// rescale
nPixelX *= imageWidth;
nPixelY *= imageHeight;
if( nPixelX >= 0 & nPixelX < imageWidth & nPixelY >= 0 & nPixelY < imageHeight )
{
color = new Color( image.getRGB( (int) nPixelX, (int) nPixelY ) );
return ( color.getBlue( ) >= WATER_COLOR_BLUE & color.getGreen( ) <= WATER_COLOR_GREEN & color.getRed( ) <= WATER_COLOR_RED );
}
return false;
}
// creates up to 120 building objects - ensures that
// buildings are not positioned over water.
public Group createBuildings( Group g )
{
m_BuildingAppearance = new Appearance( );
BranchGroup bg = new BranchGroup( );
Texture tex = new TextureLoader( "boston.gif", this ).getTexture( );
BufferedImage image = ((ImageComponent2D) tex.getImage( 0 )).getImage( );
final int nMaxBuildings = 120;
for( int n = 0; n < nMaxBuildings; n++ )
{
Cuboid building = new Cuboid( this, bg, ComplexObject.GEOMETRY | ComplexObject.TEXTURE );
float posX = (int) Utils.getRandomNumber( 0, LAND_WIDTH );
float posZ = (int) Utils.getRandomNumber( 0, LAND_LENGTH );
if( isLocationWater( image, posX, posZ ) == false )
{
building.createObject( m_BuildingAppearance,
new Vector3d( posX,
0,
posZ ),
new Vector3d( Utils.getRandomNumber( 3, 2 ),
Utils.getRandomNumber( 8, 7 ),
Utils.getRandomNumber( 3, 2 ) ),
"house.gif",
null,
null );
}
}
g.addChild( bg );
return bg;
}
// creates three helicopters
public void createHelicopters( Group g )
{
for( int n = 0; n < 3; n++ )
createHelicopter( g );
}
// edit the positions of the clipping
// planes so we don't clip on the front
// plane prematurely
protected double getBackClipDistance( )
{
return 50.0;
}
protected double getFrontClipDistance( )
{
return 0.1;
}
// creates a single helicopter object
public Group createHelicopter( Group g )
{
BranchGroup bg = new BranchGroup( );
Helicopter heli = new Helicopter( this, bg, ComplexObject.GEOMETRY | ComplexObject.SOUND );
heli.createObject( new Appearance( ),
new Vector3d( Utils.getRandomNumber( 0, LAND_WIDTH ),
Utils.getRandomNumber( 15, 5 ),
Utils.getRandomNumber( 0, LAND_LENGTH ) ),
new Vector3d( 10,10,10 ),
null,
"heli.wav",
null );
g.addChild( bg );
return bg;
}
public static void main( String[] args )
{
SplineInterpolatorTest splineInterpolatorTest = new SplineInterpolatorTest( );
splineInterpolatorTest.saveCommandLineArguments( args );
new MainFrame( splineInterpolatorTest, m_kWidth, m_kHeight );
}
}
Helicopter.java
import javax.vecmath.*;
import javax.media.j3d.*;
import java.awt.*;
import java.net.*;
import com.sun.j3d.utils.image.*;
import com.sun.j3d.utils.geometry.*;
import org.selman.java3d.book.common.*;
public class Helicopter extends ComplexObject
{
public static final floatWIDTH = 2.0f;
public static final floatHEIGHT = 2.0f;
public static final floatLENGTH = 2.0f;
public Helicopter( Component comp, Group g, int nFlags )
{
super( comp, g, nFlags );
}
protected Group createGeometryGroup( Appearance app, Vector3d position, Vector3d scale, String szTextureFile, String szSoundFile )
{
TransformGroup tg = new TransformGroup( );
// we need to flip the helicopter model
// 90 degrees about the X axis
Transform3D t3d = new Transform3D( );
t3d.rotX( Math.toRadians( -90 ) );
tg.setTransform( t3d );
try
{
tg.addChild( loadGeometryGroup( "heli.obj", app ) );
// create an Alpha object for the Interpolator
Alpha alpha = new Alpha( -1,
Alpha.INCREASING_ENABLE | Alpha.DECREASING_ENABLE,
(long) Utils.getRandomNumber( 0, 500 ),
(long)Utils.getRandomNumber( 0, 500 ),
(long)Utils.getRandomNumber( 20000, 5000 ),
4000,
100,
(long) Utils.getRandomNumber( 20000, 5000 ),
5000,
50 );
attachSplinePathInterpolator( alpha,
new Transform3D( ),
new URL( ((Java3dApplet) m_Component).getWorkingDirectory( ), "heli_spline.xls" ) );
}
catch( Exception e )
{
System.err.println( e.toString( ) );
}
return tg;
}
protected int getSoundLoop( boolean bCollide )
{
return -1;
}
protected float getSoundPriority( boolean bCollide )
{
return 1.0f;
}
protected float getSoundInitialGain( boolean bCollide )
{
return 3.0f;
}
protected Point2f[] getSoundDistanceGain( boolean bCollide )
{
Point2f[] gainArray = new Point2f[2];
gainArray[0] = new Point2f( 2, 0.2f );
gainArray[1] = new Point2f( 20, 0.05f );
return gainArray;
}
protected boolean getSoundInitialEnable( boolean bCollide )
{
return true;
}
protected boolean getSoundContinuousEnable( boolean bCollide )
{
return false;
}
protected Bounds getSoundSchedulingBounds( boolean bCollide )
{
return new BoundingSphere( new Point3d( 0,0,0 ), 20 );
}
protected boolean getSoundReleaseEnable( boolean bCollide )
{
return true;
}
}
// package org.selman.java3d.book.common;
import javax.vecmath.*;
import javax.media.j3d.*;
import java.io.*;
import java.net.*;
import java.awt.*;
import com.sun.j3d.utils.geometry.*;
import com.sun.j3d.utils.image.*;
import com.sun.j3d.loaders.objectfile.ObjectFile;
import com.sun.j3d.loaders.Scene;
import com.sun.j3d.utils.behaviors.interpolators.*;
public abstract class ComplexObject extends BranchGroup
{
protected Groupm_ParentGroup = null;
protected intm_nFlags = 0;
protected BackgroundSound m_CollideSound = null;
protected Componentm_Component = null;
protected TransformGroup m_TransformGroup = null;
protected TransformGroup m_BehaviorTransformGroup = null;
public static final intSOUND = 0x001;
public static final intGEOMETRY = 0x002;
public static final intTEXTURE = 0x004;
public static final intCOLLISION = 0x008;
public static final intCOLLISION_SOUND = 0x010;
public ComplexObject( Component comp, Group group, int nFlags )
{
m_ParentGroup = group;
m_nFlags = nFlags;
m_Component = comp;
}
public Bounds getGeometryBounds( )
{
return new BoundingSphere( new Point3d( 0,0,0 ), 100 );
}
private MediaContainer loadSoundFile( String szFile )
{
try
{
File file = new File( System.getProperty( "user.dir" ) );
URL url = file.toURL( );
URL soundUrl = new URL( url, szFile );
return new MediaContainer( soundUrl );
}
catch( Exception e )
{
System.err.println( "Error could not load sound file: " + e );
System.exit( -1 );
}
return null;
}
protected void setTexture( Appearance app, String szFile )
{
Texture tex = new TextureLoader( szFile, m_Component ).getTexture( );
app.setTexture( tex );
}
abstract protected Group createGeometryGroup( Appearance app, Vector3d position, Vector3d scale, String szTextureFile, String szSoundFile );
protected Group loadGeometryGroup( String szModel, Appearance app )
throws java.io.FileNotFoundException
{
// load the object file
Scene scene = null;
Shape3D shape = null;
// read in the geometry information from the data file
ObjectFile objFileloader = new ObjectFile( ObjectFile.RESIZE );
scene = objFileloader.load( szModel );
// retrieve the Shape3D object from the scene
BranchGroup branchGroup = scene.getSceneGroup( );
shape = (Shape3D) branchGroup.getChild( 0 );
shape.setAppearance( app );
return branchGroup;
}
protected int getSoundLoop( boolean bCollide )
{
return 1;
}
protected float getSoundPriority( boolean bCollide )
{
return 1.0f;
}
protected float getSoundInitialGain( boolean bCollide )
{
return 1.0f;
}
protected boolean getSoundInitialEnable( boolean bCollide )
{
return true;
}
protected boolean getSoundContinuousEnable( boolean bCollide )
{
return false;
}
protected Bounds getSoundSchedulingBounds( boolean bCollide )
{
return new BoundingSphere( new Point3d( 0,0,0 ), 1.0 );
}
protected boolean getSoundReleaseEnable( boolean bCollide )
{
return true;
}
protected Point2f[] getSoundDistanceGain( boolean bCollide )
{
return null;
}
protected void setSoundAttributes( Sound sound, boolean bCollide )
{
sound.setCapability( Sound.ALLOW_ENABLE_WRITE );
sound.setCapability( Sound.ALLOW_ENABLE_READ );
sound.setSchedulingBounds( getSoundSchedulingBounds( bCollide ) );
sound.setEnable( getSoundInitialEnable( bCollide ) );
sound.setLoop( getSoundLoop( bCollide ) );
sound.setPriority( getSoundPriority( bCollide ) );
sound.setInitialGain( getSoundInitialGain( bCollide ) );
sound.setContinuousEnable( getSoundContinuousEnable( bCollide ) );
sound.setReleaseEnable( bCollide );
if( sound instanceof PointSound )
{
PointSound pointSound = (PointSound) sound;
pointSound.setInitialGain( getSoundInitialGain( bCollide ) );
Point2f[] gainArray = getSoundDistanceGain( bCollide );
if( gainArray != null )
pointSound.setDistanceGain( gainArray );
}
}
public Group createObject( Appearance app,
Vector3d position,
Vector3d scale,
String szTextureFile,
String szSoundFile,
String szCollisionSound )
{
m_TransformGroup = new TransformGroup( );
Transform3D t3d = new Transform3D( );
t3d.setScale( scale );
t3d.setTranslation( position );
m_TransformGroup.setTransform( t3d );
m_BehaviorTransformGroup = new TransformGroup( );
if( (m_nFlags & GEOMETRY) == GEOMETRY)
m_BehaviorTransformGroup.addChild( createGeometryGroup( app, position, scale, szTextureFile, szSoundFile ) );
if( (m_nFlags & SOUND) == SOUND)
{
MediaContainer media = loadSoundFile( szSoundFile );
PointSound pointSound = new PointSound( media, getSoundInitialGain( false ), 0, 0, 0 );
setSoundAttributes( pointSound, false );
m_BehaviorTransformGroup.addChild( pointSound );
}
if( (m_nFlags & COLLISION) == COLLISION)
{
m_BehaviorTransformGroup.setCapability( Node.ENABLE_COLLISION_REPORTING );
m_BehaviorTransformGroup.setCollidable( true );
m_BehaviorTransformGroup.setCollisionBounds( getGeometryBounds( ) );
if( (m_nFlags & COLLISION_SOUND) == COLLISION_SOUND )
{
MediaContainer collideMedia = loadSoundFile( szCollisionSound );
m_CollideSound = new BackgroundSound( collideMedia, 1 );
setSoundAttributes( m_CollideSound, true );
m_TransformGroup.addChild( m_CollideSound );
}
CollisionBehavior collision = new CollisionBehavior( m_BehaviorTransformGroup, this );
collision.setSchedulingBounds( getGeometryBounds( ) );
m_BehaviorTransformGroup.addChild( collision );
}
m_TransformGroup.addChild( m_BehaviorTransformGroup );
m_ParentGroup.addChild( m_TransformGroup );
return m_BehaviorTransformGroup;
}
public void onCollide( boolean bCollide )
{
System.out.println( "Collide: " + bCollide );
if( m_CollideSound != null & bCollide == true )
m_CollideSound.setEnable( true );
}
public void attachBehavior( Behavior beh )
{
m_BehaviorTransformGroup.setCapability( TransformGroup.ALLOW_TRANSFORM_WRITE );
beh.setSchedulingBounds( getGeometryBounds( ) );
m_BehaviorTransformGroup.addChild( beh );
}
public TransformGroup getBehaviorTransformGroup( )
{
return m_BehaviorTransformGroup;
}
public void attachSplinePathInterpolator( Alpha alpha, Transform3D axis, URL urlKeyframes )
{
// read a spline path definition file and
// add a Spline Path Interpolator to the TransformGroup for the object.
m_BehaviorTransformGroup.setCapability( TransformGroup.ALLOW_TRANSFORM_WRITE );
RotPosScaleTCBSplinePathInterpolator splineInterpolator =
Utils.createSplinePathInterpolator( alpha, m_BehaviorTransformGroup, axis, urlKeyframes );
if( splineInterpolator != null )
{
splineInterpolator.setSchedulingBounds( getGeometryBounds( ) );
m_BehaviorTransformGroup.addChild( splineInterpolator );
}
else
{
System.out.println( "attachSplinePathInterpolator failed for: " + urlKeyframes );
}
}
}
heli_spline.xls
0-20 10 20
0 / 0 / 0 / 1 / 1 / 1 / 0 / 0 / 0 / 0
0.2
20 20 / -20
0 / 0 / 0 / 1 / 1 / 1 / 0 / 0 / 0 / 0
0.4
20 50 / 50
0 / 0 / 0 / 1 / 1 / 1 / 0 / 0 / 0 / 0
0.6
-20 10 10
0 / 0 / 0 / 1 / 1 / 1 / 0 / 0 / 0 / 0
1
0 30 / 0
0 / 0 / 0 / 1 / 1 / 1 / 0 / 0 / 0 / 0
rotate_viewer_spline.xls
05 6 5
-0.4 0 0
1 1 1
0 1 0
0
0.3
2 4 10
1.0 0.2 0
1 1 1
0 0 0
0
0.5
-2 4 8
-0.3 0.6 0.2
1 1 1
-1 1 -1
0
0.7
-2 5 10
-0.4 -0.6 0.5
1 1 1
-1 1 -1
0
0.8
-1 4 5
-1.7
1 1 1
1 1 1
0
0.9
0 10 15
-1.2 0 0
1 1 1
1 0 1
0
1
0 52 0
-1.5 0 0
1 1 1
0 1 1
0
1