My first JME3 "App" - trying out lot's of different effects and stuff

Thought I’d post this here in case any other “newbies” could use some examples of doing stuff combined together from the tutorials. I’ve got some interesting things going on with respect to driving things in the update loop and filters. Also demonstrates some custom input handling, camera auto-orbiting, etc. Here’s a screen-shot:

And here is the code (NOTE: This isn’t production/good code - organizationally, this should all be broken up into AppStates and Controllers, but, I’m just trying to get a feel for what can be done in JME3 and how):

package com.butlersoftwarellc.jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.DirectionalLight;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.DepthOfFieldFilter;
import com.jme3.post.filters.FogFilter;
import com.jme3.post.filters.LightScatteringFilter;
import com.jme3.post.filters.TranslucentBucketFilter;
import com.jme3.post.ssao.SSAOFilter;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.scene.shape.Torus;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.shadow.EdgeFilteringMode;
import com.jme3.util.TangentBinormalGenerator;

/**
 *
 * @author gbutler
 * <p>
 * Sample 4 - how to trigger repeating actions from the main event loop.
 * In this example, you use the loop to make the player character
 * rotate continuously. */
public class HelloKitchenSink
        extends SimpleApplication
{

    public static void main ( String[] args )
    {
        HelloKitchenSink app = new HelloKitchenSink();
        app.start();
    }

    private Geometry player;
    private Geometry npc;
    private Geometry npcTorus;
    private float npc_scale_factor = 0f;
    private float third_npc_scale_factor = 0f;
    private boolean isOrbiting = true;
    private float orbitRadius = 20.0f;
    private boolean isLookingAtPlayer = true;
    private boolean isOscillatingNpc = true;
    private ParticleEmitter fireEffect;
    private boolean isFoggy = true;
    private FogFilter fog;
    private FilterPostProcessor fpp;

    private final ActionListener actionListener = new ActionListener()
    {
        @Override
        public void onAction ( String name,
                               boolean isPressed,
                               float tpf )
        {
            if ( name.equals( "Orbit" ) && !isPressed ) {
                isOrbiting = !isOrbiting;
            } else if ( name.equals( "LookAtPlayer" ) && !isPressed ) {
                isLookingAtPlayer = !isLookingAtPlayer;
            } else if ( name.equals( "OrbitRadiusPlus" ) && !isPressed ) {
                orbitRadius = Math.min( orbitRadius + 1.0f, 40.0f );
            } else if ( name.equals( "OrbitRadiusMinus" ) && !isPressed ) {
                orbitRadius = Math.max( orbitRadius - 1.0f, 2.0f );
            } else if ( name.equals( "OscillateNPC" ) && !isPressed ) {
                isOscillatingNpc = !isOscillatingNpc;
            } else if ( name.equals( "Fog" ) && !isPressed ) {
                if ( isFoggy ) {
                    fpp.removeFilter( fog );
                } else {
                    fpp.addFilter( fog );
                }
                isFoggy = !isFoggy;
            }
        }
    };

    @Override
    public void simpleInitApp ()
    {
        // Load the town scene as a background
        Spatial gameLevel = assetManager.loadModel( "Scenes/town/main.j3o" );
        gameLevel.setLocalTranslation( 0, -1f, 0 );
        gameLevel.setLocalScale( 2 );
        rootNode.attachChild( gameLevel );

        // You must add a light to make the model visible
        Vector3f sunDirection = new Vector3f( -0.1f, -0.75f, -1.0f );
        DirectionalLight sun = new DirectionalLight();
        sun.setColor( new ColorRGBA( 0.5f, 0.3f, 0.3f, 0.8f ) );
        sun.setDirection( sunDirection );
        rootNode.addLight( sun );

        /** Uses Texture from jme3-test-data library! */
        fireEffect = new ParticleEmitter( "Emitter", ParticleMesh.Type.Triangle, 250 );
        Material fireMat = new Material( assetManager, "Common/MatDefs/Misc/Particle.j3md" );
        fireMat.setTexture( "Texture", assetManager.loadTexture( "Effects/Explosion/flame.png" ) );
        fireEffect.setMaterial( fireMat );
        fireEffect.setImagesX( 2 );
        fireEffect.setImagesY( 2 ); // 2x2 texture animation
        fireEffect.setStartColor( new ColorRGBA( 1f, .55f, 0.1f, 0.9f ) ); // yellow
        fireEffect.setEndColor( new ColorRGBA( 1f, 0f, 0f, 1f ) );   // red
        fireEffect.getParticleInfluencer()
                .setInitialVelocity( new Vector3f( 0f, 2.5f, 0f ) );
        fireEffect.getParticleInfluencer()
                .setVelocityVariation( 0.15f );
        fireEffect.setStartSize( 1.5f );
        fireEffect.setEndSize( 0.01f );
        fireEffect.setGravity( -2.0f, 0.25f, 0f );
        fireEffect.setLowLife( 0.75f );
        fireEffect.setHighLife( 3f );
        fireEffect.setRotateSpeed( 0.37f );
        fireEffect.setLocalTranslation( new Vector3f( 0f, 0f, -4f ) );
        //fireEffect.setShape( new EmitterPointShape( Vector3f.ZERO ) );
        rootNode.attachChild( fireEffect );

        // Add Point Light Source of Fire
        PointLight fireLight = new PointLight( new Vector3f( 0f, 0.1f, -4f ), ColorRGBA.Orange, 10.0f );
        rootNode.addLight( fireLight );

        /** this textured sphere is our player character */
        Sphere sphere = new Sphere( 16, 16, 2f );
        sphere.setTextureMode( Sphere.TextureMode.Polar ); // better quality on spheres
        TangentBinormalGenerator.generate( sphere );
        player = new Geometry( "Player", sphere );
        player.setLocalTranslation( new Vector3f( 0f, 1f, 0f ) );
        Material mat = new Material( assetManager,
                                     "Common/MatDefs/Light/Lighting.j3md" );
        mat.setTexture( "DiffuseMap", assetManager.loadTexture( "Textures/Terrain/BrickWall/BrickWall.jpg" ) );
        mat.setTexture( "NormalMap", assetManager.loadTexture( "Textures/Terrain/BrickWall/BrickWall_normal.jpg" ) );
        mat.setBoolean( "UseMaterialColors", true );
        mat.setColor( "Ambient", ColorRGBA.Green );
        mat.setColor( "Diffuse", ColorRGBA.Orange );
        mat.setColor( "Specular", ColorRGBA.Brown );
        mat.setFloat( "Shininess", 127f );

        player.setMaterial( mat );
        rootNode.attachChild( player );

        // this is a similar textured sphere for comparison with the player (Dead Player)
        Sphere sphere2 = new Sphere( 16, 16, 2f );
        sphere2.setTextureMode( Sphere.TextureMode.Polar ); // better quality on spheres
        TangentBinormalGenerator.generate( sphere2 );
        Geometry p2 = new Geometry( "Dead Player", sphere2 );
        p2.setLocalTranslation( new Vector3f( -4.0f, 1f, 1f ) );
        mat = new Material( assetManager,
                            "Common/MatDefs/Light/Lighting.j3md" );
        mat.setTexture( "DiffuseMap", assetManager.loadTexture( "Textures/Terrain/BrickWall/BrickWall.jpg" ) );
        mat.setTexture( "NormalMap", assetManager.loadTexture( "Textures/Terrain/BrickWall/BrickWall_normal.jpg" ) );
        mat.setBoolean( "UseMaterialColors", true );
        mat.setColor( "Ambient", ColorRGBA.Green );
        mat.setColor( "Diffuse", ColorRGBA.Orange );
        mat.setColor( "Specular", ColorRGBA.Brown );
        mat.setFloat( "Shininess", 127f );

        p2.setMaterial( mat );
        rootNode.attachChild( p2 );

        /** this alpha-blended box is our NPC character */
        Box b = new Box( 1, 1, 1 );
        npc = new Geometry( "NPC", b );
        mat = new Material( assetManager,
                            "Common/MatDefs/Misc/Unshaded.j3md" );
        mat.setColor( "Color", ColorRGBA.Red );     // This color is mostly irrelevant, as we'll be pulsing it in the update loop
        mat.setTransparent( true );                 // we want to the npc to be slightly transparent - see the update loop
        mat.getAdditionalRenderState()
                .setBlendMode( BlendMode.Alpha );
        npc.setMaterial( mat );
        npc.setLocalTranslation( new Vector3f( 6f, 2f, -8f ) );
        npc.setQueueBucket( Bucket.Transparent );   // transparent items need to be in the transparent queue bucket
        rootNode.attachChild( npc );

        // strip box for comparison
        Torus torus = new Torus( 64, 16, 0.5f, 1.5f );
        npcTorus = new Geometry( "NPC StripBox Comparison", torus );
        mat = new Material( assetManager,
                            "Common/MatDefs/Misc/Unshaded.j3md" );
        mat.setColor( "Color", new ColorRGBA( 1.0f, 0.0f, 0.0f, 0.5f ) );
        mat.setTransparent( true );                 // we want to the npc to be slightly transparent
        mat.getAdditionalRenderState()
                .setBlendMode( BlendMode.Alpha );
        npcTorus.setMaterial( mat );
        npcTorus.setLocalTranslation( new Vector3f( 6f, 2f, 4f ) );
        npcTorus.setQueueBucket( Bucket.Transparent );   // transparent items need to be in the transparent queue bucket
        rootNode.attachChild( npcTorus );

        /** Add fog to a scene */
        fpp = new FilterPostProcessor( assetManager );
        fog = new FogFilter();
        fog.setFogColor( new ColorRGBA( 0.9f, 0.9f, 0.9f, 0.96f ) );
        fog.setFogDistance( 155 );
        fog.setFogDensity( 0.98f );
        fpp.addFilter( fog );
        viewPort.addProcessor( fpp );

        // Ambient Shadows
        SSAOFilter ssaoFilter = new SSAOFilter( 12.94f, 43.93f, 0.33f, 0.60f );
        fpp.addFilter( ssaoFilter );

        // Scatter Sunlight
        LightScatteringFilter sunlightScatteringFilter = new LightScatteringFilter( sunDirection.multLocal( -3000 ) );
        sunlightScatteringFilter.setEnabled( true );
        sunlightScatteringFilter.setLightDensity( 4.0f );
        fpp.addFilter( sunlightScatteringFilter );

        /* this shadow needs a directional light */
        DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter( assetManager, 1024, 4 );
        dlsf.setLight( sun );
        dlsf.setEnabled( true );
        dlsf.setEdgeFilteringMode( EdgeFilteringMode.Bilinear );
        fpp.addFilter( dlsf );

        // Add a couple of additional post-processing filters that makes things "nicer"
        fpp.addFilter( new TranslucentBucketFilter( false ) );
        DepthOfFieldFilter depthOfFieldFilter = new DepthOfFieldFilter();
        depthOfFieldFilter.setFocusDistance( 10.0f );
        depthOfFieldFilter.setFocusRange( 5.0f );
        fpp.addFilter( depthOfFieldFilter );

        // Initialize the Key Map
        initKeys();
    }

    /* Use the main event loop to trigger repeating actions. */
    @Override
    public void simpleUpdate ( float tpf )
    {
        // make the player rotate:
        player.rotate( 0, 2 * tpf, 0 );

        // make the npc roll erattically on all axes
        if ( isOscillatingNpc ) {
            npc.rotate( 1 * tpf, 1.25f * tpf, 2.5f * tpf );

            // compute a scale factor from 0 to TAU (2PI) and get the sin so we can have harmonic motion
            npc_scale_factor = ( npc_scale_factor + FastMath.TWO_PI * tpf * 0.25f );
            while ( npc_scale_factor >= FastMath.TWO_PI ) {
                npc_scale_factor = ( npc_scale_factor - FastMath.TWO_PI );
            }
            float sin_npc_scale_factor = FastMath.sin( npc_scale_factor );

            // maeke the npc grow-and-shrink and move back and forth through the donut harmonically while pulsating it's color
            npc.setLocalScale( 0.1f + 1.9f * FastMath.abs( sin_npc_scale_factor ) );
            npc.getMaterial()
                    .setColor( "Color", new ColorRGBA( 1f, 0.25f, 0.5f * FastMath.abs( sin_npc_scale_factor ) + 0.25f, 0.25f ) );
            npc.setLocalTranslation( new Vector3f( 6f,
                                                   2f + FastMath.abs( sin_npc_scale_factor ),
                                                   4f + 8f * sin_npc_scale_factor ) );

            // Let's make the donut rotate such that it synchronizes with the cube passing through it
            npcTorus.setLocalRotation( new Quaternion().fromAngleAxis( npc_scale_factor, Vector3f.UNIT_Y ) );
        }

        // make the camera orbit the scene
        third_npc_scale_factor = ( third_npc_scale_factor + FastMath.TWO_PI * tpf * 0.5f * 0.025f );
        while ( third_npc_scale_factor >= FastMath.TWO_PI ) {
            third_npc_scale_factor = ( third_npc_scale_factor - FastMath.TWO_PI );
        }
        float sin_third_npc_scale_factor = FastMath.sin( third_npc_scale_factor );
        float cos_third_npc_scale_factor = FastMath.cos( third_npc_scale_factor );

        if ( isOrbiting ) {
            Camera cam = getCamera();
            cam.setLocation( new Vector3f( orbitRadius * sin_third_npc_scale_factor,
                                           12f * ( orbitRadius / 39.0f ),
                                           orbitRadius * cos_third_npc_scale_factor ) );
            if ( isLookingAtPlayer ) {
                cam.lookAt( Vector3f.ZERO, Vector3f.UNIT_Y );
            }
        }

        // adjust wind for fire
        Vector3f feg = fireEffect.getGravity();
        float feg_x = feg.getX();
        float feg_z = feg.getZ();
        feg_x = ( feg_x + ( FastMath.rand.nextFloat() * 0.25f - 0.125f ) );
        feg_z = ( feg_z + ( FastMath.rand.nextFloat() * 0.25f - 0.125f ) );
        feg_x = Math.max( Math.min( feg_x, 3.5f ), -3.5f ); // don't let the "wind" get too fast in any direction
        feg_z = Math.max( Math.min( feg_z, 3.5f ), -3.5f );
        feg.setX( feg_x );
        feg.setZ( feg_z );
        fireEffect.setGravity( feg );
    }

    private void initKeys ()
    {
        inputManager.addMapping( "Orbit", new KeyTrigger( KeyInput.KEY_O ) );
        inputManager.addMapping( "LookAtPlayer", new KeyTrigger( KeyInput.KEY_L ) );
        inputManager.addMapping( "OrbitRadiusPlus", new KeyTrigger( KeyInput.KEY_ADD ) );
        inputManager.addMapping( "OrbitRadiusMinus", new KeyTrigger( KeyInput.KEY_SUBTRACT ) );
        inputManager.addMapping( "OscillateNPC", new KeyTrigger( KeyInput.KEY_H ) );
        inputManager.addMapping( "Fog", new KeyTrigger( KeyInput.KEY_F ) );

        inputManager.addListener( actionListener, "Orbit" );
        inputManager.addListener( actionListener, "LookAtPlayer" );
        inputManager.addListener( actionListener, "OrbitRadiusPlus" );
        inputManager.addListener( actionListener, "OrbitRadiusMinus" );
        inputManager.addListener( actionListener, "OscillateNPC" );
        inputManager.addListener( actionListener, "Fog" );
    }
}
4 Likes

It’s always super fun to see more stuff from the new people. This is usually the first step into really doing something that’s worthwhile.

I did it similarly in one large class rathar than split it up the first time I did the tutorial. Good luck in your future games.

Having a big main class is the best way for a beginner to get into things, how the update loop works etc. Then it will become much more apparent how and why Controls and AppStates etc. can help designing an actual application.