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

0
5 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