diff --git a/sdk/jme3-core/build.xml b/sdk/jme3-core/build.xml
index eb10553b6..14ba1a03f 100644
--- a/sdk/jme3-core/build.xml
+++ b/sdk/jme3-core/build.xml
@@ -42,7 +42,7 @@
Like Shapes, 3D models are also made up of Meshes, but models are more complex than Shapes. While Shapes are built into jME3, you typically create models in external 3D Mesh Editors.
To use 3D models in a jME3 application:
assets
directory.Spatial model = assetManager.loadModel( - "Models/MonkeyHead/MonkeyHead.mesh.xml" );
To create 3D models and scenes, you need a 3D Mesh Editor such as Blender, with an OgreXML Exporter plugin.
Tip: Consider creating UV textures for more complex models, it looks more professional.
3D mesh editors are third-party products, so please consult their documentation for instructions how to use them. Here is an example workflow for Blender users:
To export your models as Ogre XML meshes with materials:
something.mesh.xml
goes with something.material
, plus (optionally) something.skeleton.xml
, and some JPG files.assets/Models/
directory. E.g. assets/Models/something/
.You can now use the jMonkeyPlatform to load and view models. You can create scenes from them and write cde that loads them into your application.
+ +Like Shapes, 3D models are also made up of Meshes, but models are more complex than Shapes. While Shapes are built into jME3, you typically create models in external 3D Mesh Editors. +
+ ++ +To use 3D models in a jME3 application: +
+assets
directory.Spatial model = assetManager.loadModel( + "Models/MonkeyHead/MonkeyHead.mesh.xml" );+
+ +To create 3D models and scenes, you need a 3D Mesh Editor such as , with an OgreXML Exporter plugin. +
+ ++Tip: Consider creating for more complex models, it looks more professional. +
+ ++3D mesh editors are third-party products, so please consult their documentation for instructions how to use them. Here is an example workflow for Blender users: +
+ ++To export your models as Ogre XML meshes with materials: +
+something.mesh.xml
goes with something.material
, plus (optionally) something.skeleton.xml
, and some JPG files.assets/Models/
directory. E.g. assets/Models/something/
.+ +You can now use the jMonkeyPlatform to load and view models. You can create scenes from them and write cde that loads them into your application. +
+ +In 3D games, you do not only load static 3D models, you also want to be able to trigger animations in the model from the Java code. Animated models must be created in an external mesh editor (for example, Blender).
What is required for the model?
More information: Animation
What is required in your java class?
Create one com.jme3.animation.AnimControl
object in your JME3 application for each animated model that you want to control. You have to register each animated model to one of these Animation Controllers. The control object gives you access to the available animation sequences in the model.
AnimControl playerControl; // you need one controller per model + ++ +Animation in jME3
++ ++ ++ +In 3D games, you do not only load static 3D models, you also want to be able to trigger animations in the model from the Java code. Animated models must be created in an external mesh editor (for example, Blender). +
+ ++What is required for the model? +
++
+ +- +
For each model, you have to define a skeleton (bones rigging).+- +
For each motion, you have to specify how it distorts the model (skinning).+- +
For each animation, you have to specify a series of snapshots of how the bones are positioned (keyframes).+- +
One model can contain several animations. You give every animation a name when you save it in the mesh editor.++ +More information: Animation +
+ ++What is required in your java class? +
++
+ +- +
One animation controller per animated Model+- +
As many channels per controller as you need to play several animations in parallel. In simple cases one channel is enough, sometimes you need two or more per model.+Code Samples
+++ ++
+ +- +
+- +
+- +
+- +
+- +
+- +
+Controlling Animations
++ ++ +The Controller
++ ++ +Create one
+com.jme3.animation.AnimControl
object in your JME3 application for each animated model that you want to control. You have to register each animated model to one of these Animation Controllers. The control object gives you access to the available animation sequences in the model. +AnimControl playerControl; // you need one controller per model Node player = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); // load a model playerControl = player.getControl(AnimControl.class); // get control over this model - playerControl.addListener(this); // add listenerChannels
+ +A controller has several animation channels (
com.jme3.animation.AnimChannel
). Each channel can play one animation sequence at a time.There often are situations where you want to run several animation sequences at the same time, e.g. "shooting while walking" or "boxing while jumping". In this case, you create several channels, assign an animation to each, and play them in parallel.
AnimChannel channel_walk = playerControl.createChannel(); + playerControl.addListener(this); // add listener+ +Channels
++ ++ +A controller has several animation channels (
+ +com.jme3.animation.AnimChannel
). Each channel can play one animation sequence at a time. ++There often are situations where you want to run several animation sequences at the same time, e.g. "shooting while walking" or "boxing while jumping". In this case, you create several channels, assign an animation to each, and play them in parallel. +
+AnimChannel channel_walk = playerControl.createChannel(); AnimChannel channel_jump = playerControl.createChannel(); - ...To reset a controller, call
control.clearChannels();
Animation Control Properties
The following information is available for an AnimControl.
AnimControl Property Usage createChannel() Returns a new channel, controlling all bones by default. getNumChannels() The number of channels registered to this Control. getChannel(0) Gets individual channels by index number. At most getNumChannels()
.clearChannels() Clear all channels in this control. addListener(animEventListener)
removeListener(animEventListener)
clearListeners()Adds or removes listeners to receive animation related events.
AnimControl Property Usage setAnimations(aniHashMap) Sets the animations that this AnimControl is capable of playing. The animations must be compatible with the skeleton given in the constructor. addAnim(boneAnim)
removeAnim(boneAnim)Adds or removes an animation from this Control. getAnimationNames() A String Collection of names of all animations that this Control can play for this model. getAnim("anim") Retrieve an animation from the list of animations. getAnimationLength("anim") Returns the length of the given named animation in seconds
AnimControl Property Usage getSkeleton() The Skeleton object controlled by this Control. getTargets() The Skin objects controlled by this Control, as Mesh array. getAttachmentsNode("bone") Returns the attachment node of a bone. Attach models and effects to this node to make them follow this bone's motions. Animation Channel Properties
The following properties are set per AnimChannel.
AnimChannel Property Usage setLoopMode(LoopMode.Loop); From now on, the animation on this channel will repeat from the beginning when it ends. setLoopMode(LoopMode.DontLoop); From now on, the animation on this channel will play once, and the freeze at the last keyframe. setLoopMode(LoopMode.Cycle); From now on, the animation on this channel will play forward, then backward, then again forward, and so on. setSpeed(1f); From now on, play this animation slower (<1f) or faster (>1f), or with default speed (1f). setTime(1.3f); Fast-forward or rewind to a certain moment in time of this animation. The following information is available for a channel.
AnimChannel Property Usage getAnimationName() The name of the animation playing on this channel. Returns null
when no animation is playing.getLoopMode() The current loop mode on this channel. The returned com.jme3.animation enum can be LoopMode.Loop, LoopMode.DontLoop, or LoopMode.Cycle. getAnimMaxTime() The total length of the animation on this channel. Or 0f
if nothing is playing.getTime() How long the animation on this channel has been playing. It returns 0f
if the channel has not started playing yet, or a value up to getAnimMaxTime().getControl() The AnimControl that belongs to this AnimChannel. Use the following methods to add or remove individual bones to an AnimChannel. This is useful when you play two animations in parallel on two channels, and each controls a subset of the bones (e.g. one the arms, and the other the legs).
AnimChannel Methods Usage addAllBones() Add all the bones of the model's skeleton to be influenced by this animation channel. (default) addBone("bone1")
addBone(bone1)Add a single bone to be influenced by this animation channel. addToRootBone("bone1")
addToRootBone(bone1)Add a series of bones to be influenced by this animation channel: Add all bones, starting from the given bone, to the root bone. addFromRootBone("bone1")
addFromRootBone(bone1)Add a series of bones to be influenced by this animation channel: Add all bones, starting from the given root bone, going towards the children bones. Playing Animations
Animations are played by channel. Note: Whether the animation channel plays continuously or only once, depends on the Loop properties you have set.
Channel Method Usage channel_walk.setAnim("Walk",0.50f); Start the animation named "Walk" on channel channel_walk.
The float value specifies the time how long the animation should overlap with the previous one on this channel. If set to 0f, then no blending will occur and the new animation will be applied instantly.Tip: Use the AnimEventLister below to react at the end or start of an animation cycle.
Usage Example
+ +In this short example, we define the space key to trigger playing the "Walk" animation on channel2.
public void simpleInitApp() { + ...+ ++To reset a controller, call
+ +control.clearChannels();
+Animation Control Properties
++ ++ ++ +The following information is available for an AnimControl. + +
+++
+ +AnimControl Property Usage ++ +createChannel() Returns a new channel, controlling all bones by default. ++ +getNumChannels() The number of channels registered to this Control. ++ +getChannel(0) Gets individual channels by index number. At most +getNumChannels()
.+ +clearChannels() Clear all channels in this control. ++ +addListener(animEventListener)
+removeListener(animEventListener)
+clearListeners()Adds or removes listeners to receive animation related events. +++
+ +AnimControl Property Usage ++ +setAnimations(aniHashMap) Sets the animations that this AnimControl is capable of playing. The animations must be compatible with the skeleton given in the constructor. ++ +addAnim(boneAnim)
+removeAnim(boneAnim)Adds or removes an animation from this Control. ++ +getAnimationNames() A String Collection of names of all animations that this Control can play for this model. ++ +getAnim("anim") Retrieve an animation from the list of animations. ++ +getAnimationLength("anim") Returns the length of the given named animation in seconds ++ ++
+ +AnimControl Property Usage ++ +getSkeleton() The Skeleton object controlled by this Control. ++ +getTargets() The Skin objects controlled by this Control, as Mesh array. ++ +getAttachmentsNode("bone") Returns the attachment node of a bone. Attach models and effects to this node to make them follow this bone's motions. +Animation Channel Properties
++ ++ ++ +The following properties are set per AnimChannel. + +
++ ++
+ +AnimChannel Property Usage ++ +setLoopMode(LoopMode.Loop); From now on, the animation on this channel will repeat from the beginning when it ends. ++ +setLoopMode(LoopMode.DontLoop); From now on, the animation on this channel will play once, and the freeze at the last keyframe. ++ +setLoopMode(LoopMode.Cycle); From now on, the animation on this channel will play forward, then backward, then again forward, and so on. ++ +setSpeed(1f); From now on, play this animation slower (<1f) or faster (>1f), or with default speed (1f). ++ +setTime(1.3f); Fast-forward or rewind to a certain moment in time of this animation. ++ +The following information is available for a channel. + +
++ ++
+ +AnimChannel Property Usage ++ +getAnimationName() The name of the animation playing on this channel. Returns +null
when no animation is playing.+ +getLoopMode() The current loop mode on this channel. The returned com.jme3.animation enum can be LoopMode.Loop, LoopMode.DontLoop, or LoopMode.Cycle. ++ +getAnimMaxTime() The total length of the animation on this channel. Or +0f
if nothing is playing.+ +getTime() How long the animation on this channel has been playing. It returns +0f
if the channel has not started playing yet, or a value up to getAnimMaxTime().+ +getControl() The AnimControl that belongs to this AnimChannel. ++ +Use the following methods to add or remove individual bones to an AnimChannel. This is useful when you play two animations in parallel on two channels, and each controls a subset of the bones (e.g. one the arms, and the other the legs). + +
++ ++
+ +AnimChannel Methods Usage ++ +addAllBones() Add all the bones of the model's skeleton to be influenced by this animation channel. (default) ++ +addBone("bone1")
+addBone(bone1)Add a single bone to be influenced by this animation channel. ++ +addToRootBone("bone1")
+addToRootBone(bone1)Add a series of bones to be influenced by this animation channel: Add all bones, starting from the given bone, to the root bone. ++ +addFromRootBone("bone1")
+addFromRootBone(bone1)Add a series of bones to be influenced by this animation channel: Add all bones, starting from the given root bone, going towards the children bones. +Playing Animations
++ ++ ++ +Animations are played by channel. Note: Whether the animation channel plays continuously or only once, depends on the Loop properties you have set. + +
++ ++
+ +Channel Method Usage ++ +channel_walk.setAnim("Walk",0.50f); Start the animation named "Walk" on channel channel_walk. +
+The float value specifies the time how long the animation should overlap with the previous one on this channel. If set to 0f, then no blending will occur and the new animation will be applied instantly.+ +Tip: Use the AnimEventLister below to react at the end or start of an animation cycle. +
+ +Usage Example
++ ++ +In this short example, we define the space key to trigger playing the "Walk" animation on channel2. +
+public void simpleInitApp() { ... inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE)); inputManager.addListener(actionListener, "Walk"); @@ -167,36 +296,81 @@ class="level3">In this short example, we define the space key to trigger pla } } } - };
Animation Event Listener
A jME3 application that contains animations can implement the
com.jme3.animation.AnimEventListener
interface.public class HelloAnimation extends SimpleApplication - implements AnimEventListener { ... }This optional Listener enables you to respond to animation start and end events, onAnimChange() and onAnimCycleDone().
Responding to Animation End
+ +The onAnimCycleDone() event is invoked when an animation cycle has ended. For non-looping animations, this event is invoked when the animation is finished playing. For looping animations, this even is invoked each time the animation loop is restarted.
You have access to the following objects:
The controller to which the listener is assigned. The animation channel being played. The name of the animation that has just finished playing.public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { + };+ +Animation Event Listener
++ ++ ++ +A jME3 application that contains animations can implement the
+com.jme3.animation.AnimEventListener
interface. +public class HelloAnimation extends SimpleApplication + implements AnimEventListener { ... }+ ++This optional Listener enables you to respond to animation start and end events, onAnimChange() and onAnimCycleDone(). +
+ +Responding to Animation End
++ ++ +The onAnimCycleDone() event is invoked when an animation cycle has ended. For non-looping animations, this event is invoked when the animation is finished playing. For looping animations, this even is invoked each time the animation loop is restarted. +
+ ++You have access to the following objects: +
++
+- +
The controller to which the listener is assigned.+- +
The animation channel being played.+- +
The name of the animation that has just finished playing.+public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { // test for a condition you are interested in, e.g. ... if (animName.equals("Walk")) { // respond to the event here, e.g. ... channel.setAnim("Stand", 0.50f); } - }Responding to Animation Start
+ +The onAnimChange() event is invoked every time before an animation is set by the user to be played on a given channel (
channel.setAnim()
).You have access to the following objects
The controller to which the listener is assigned. The animation channel being played. The name of the animation that will start playing.public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { + }+ +Responding to Animation Start
++ +- \ No newline at end of file + }+ +The onAnimChange() event is invoked every time before an animation is set by the user to be played on a given channel (
+ +channel.setAnim()
). ++You have access to the following objects +
++
+- +
The controller to which the listener is assigned.+- +
The animation channel being played.+- +
The name of the animation that will start playing.+public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { // test for a condition you are interested in, e.g. ... if (animName.equals("Walk")) { // respond to the event here, e.g. ... channel.setAnim("Reset", 0.50f); } - }
com.jme3.app.state.AppState
is a customizable jME3 interface that allows you to control the global game logic (game mechanics). To control the behaviour of a type of Spatial, see Custom Controls instead – both can be used together.
To implement game logic:
stateManager.attach(myAppState);
) to activate it.JME3 comes with a BulletAppState that implements Physical behaviour (using the jBullet library). You, for example, could write an Artificial Intelligence AppState to control all your enemy units. Existing examples in the code base include:
The AppState interface allows you to hook a continously executing piece of code into the main loop.
AppState Method | Usage |
---|---|
isActive() | Test whether AppState is enabled or disabled. |
stateAttached(asm) stateDetached(asm) | The AppState knows when it is attached to, or detached from, the AppStateManager. Then it triggers these methods that you implement. |
isInitialized() | Your implementations of this interface should return the correct respective boolean value. |
initialize(asm,app) | The RenderThread initialized the AppState and then calls this method. |
setActive(true) setActive(false) | Temporarily enables or disables an AppState. |
update(float tpf) | Here you implement the behaviour that you want to hook into the main update loop. |
cleanup() | Called when when the AppState is de-initialized. |
render(RenderManager rm) | Renders the state. |
postRender() | Called after all rendering commands are flushed. |
The AbstractAppState class already implements some common methods and makes creation of custom AppStates a bit easier: isInitialized(), setActive(), isActive(), cleanUp(). Just extend it and override the remaining AppState methods.
Definition:
public class MyAppState extends AbstractAppState { + ++ +Application States
++ ++ ++
+ +com.jme3.app.state.AppState
is a customizable jME3 interface that allows you to control the global game logic (game mechanics). To control the behaviour of a type of Spatial, see Custom Controls instead – both can be used together. +Use Case
++ ++ ++ +There will be situations during your game development where you think: +
++
+ +- +
Can I group a set of input handler settings, and activate and deactivate them all in one step? (e.g. mouse and key inputs are handled different in-game versus in the main menu.)+- +
Can I group a bunch of nodes and swap them in and out in one step? (e.g. the in-game scene, versus a character editor, versus a Captain's Quarters screen)+- +
Can I define what happens while the game is paused/unpaused, and toggle that in one step?+- +
Can I wrap up one set of game mechanics and switch it on and off in one step? (e.g. a conditional block that takes up a lot of space in my update loop)+- +
Can I package all of the above that belongs in-game, and also everthing that belongs to the main menu, and switch between these two "big" states in one step?++ +Yes you can! This is what AppStates are there for. A game state can be a subset of class fields (game data), GUI elements and their interactions, a subset of input handlers, a subset of nodes in the simpleInitApp() method, a subset of actions that you branch to in the simpleUpdate() loop, a set of AppStates and Controls – or combinations thereof. +
+ ++Each AppState is such a grouped subset of such game states. Each AppState has hooks that let you define what happens to this set in the following situations: +
++
+ +- +
the AppState is initialized: You load and initialize game data, InputHandlers, AppStates and Controls and attach nodes.+- +
the AppState is cleaned up: You save the game state, unregister Controls, AppStates and InputHandlers, and detach nodes.+- +
the AppState is temporarly disabled/enabled (paused/unpaused): You toggle a boolean to skip certain actions of the update loop, you display a paused screen GUI, and change the input handlers.++ +Tip: You can create AppStates that enable and disable sets of other AppStates, e.g. InGameState versus MainScreenState. +
+ +Usage
++ ++ ++ +To implement game logic: +
++
+ +- +
You define a custom AppState and implement its behaviour in the AppState's update() method.++
+- +
You can pass custom data as arguments in the constructor.+- +
The AppState has access to everything inside the app's scope via the Application+app
object.- +
Attach all AppStates to your application's AppStateManager (+stateManager.attach(myAppState);
), and activate and deactivate the ones you need.- +
Create one AppState for each set of game mechanics.++ +When you add several AppStates to one Application and activate them, their init methods and update loops are executed in the order in which the AppStates were added. +
+ +Examples
++ ++ ++ +JME3 comes with a BulletAppState that implements Physical behaviour (using the jBullet library). You, for example, could write an Artificial Intelligence AppState to control all your enemy units. Existing examples in the code base include: +
++
+ +- +
controls physical behaviour in PhysicsControl'ed Spatials.+- +
an example of a custom AppState++
+- +
+AppState
++ ++ ++ +The AppState interface allows you to initialize sets of objects, and hook a sets of continously executing code into the main loop. + +
++ ++
+ +AppState Method Usage ++ +stateAttached(asm)
+stateDetached(asm)The AppState knows when it is attached to, or detached from, the AppStateManager, and triggers these two methods. Here you implement what happens then. ++ +initialize(asm,app) The RenderThread initialized the AppState and then calls this method. Here you implement initSimpleApp()-style initialization code. ++ +isInitialized() Your implementations of this interface should return the correct respective boolean value. ++ +setActive(true)
+setActive(false)Temporarily enables or disables an AppState. ++ +isActive() Test whether AppState is enabled or disabled. Your implementation should consider the boolean. ++ +update(float tpf) Here you implement the behaviour that you want to hook into the simpleUpdate() loop. ++ +cleanup() Called when when the AppState is de-initialized. Here you implement what clean-up code for this state. ++ +render(RenderManager rm) Renders the state, plus your optional customizations. ++ +postRender() Called after all rendering commands are flushed, including your optional customizations. +AbstractAppState
++ ++ +The AbstractAppState class already implements some common methods and makes creation of custom AppStates a bit easier: isInitialized(), setActive(), isActive(), cleanUp(). Just extend it and override the remaining AppState methods. +Definition: + +
+public class MyAppState extends AbstractAppState { private Node x = new Node("x"); // some class field + private SimpleApplication app; public Node getX(){ - return x; + return x; // implement custom methods for this field } @Override public void update(float tpf) { - x.doSomething(); // implement behaviour + x.doSomething(); // call some methods here } -}Usage:
public class TestAppStates extends Application { - public static void main(String[] args){ - TestAppStates app = new TestAppStates(); - app.start(); - } - @Override - public void initialize(){ - super.initialize(); - MyAppState state = new MyAppState(); - stateManager.attach(state); - System.out.println("Use the state's methods... " + state.getX()); - } - - @Override - public void update(){ - super.update(); - stateManager.update(tpf); - stateManager.render(renderManager); - renderManager.render(tpf); - } -}Note: If you use the AppState together with a SimpleApplication-based class, then this
update()
loop is already set up.AppStateManager
The com.jme3.app.state.AppStateManager holds the list of AppStates for an application. AppStateManager ensures that active AppStates are updated and rendered. When an AppState is attached, AppStateManager calls its stateAttached() method. When an AppState is detached, AppStateManager calls its stateDetached() method.
There is one AppStateManager per application. You can attach several AppStates to one AppStateManager, but the same state can only be attached once.
AppStateManager Method Usage hasState(s) Is AppState s attached? getState(Class<T> stateClass) Returns the first state that is an instance of a subclass of the specified class. The AppStateManager's update(), render(), postRender(), and cleanUp() methods are internal, users never call them directly.
Best Practices
- \ No newline at end of file + @Override + public void initialize(AppStateManager stateManager, Application app) { + super.initialize(stateManager, app); + this.app=(SimpleApplication)app; + app.doSomething(); // call some methods elsewhere + } +}You can only change AppStates, or read and write to them, from certain places: In a Control's update() method, in an AppState's update() method, and it the SimpleApplication's simpleUpdate() loop (or the Application's update() loop).
To get data from the AppState
MyAppState
:app.getState(MyAppState.class).getInfoAboutSomething();To pass new data into the AppState
MyAppState
:app.getState(MyAppState.class).setSomething(blah);To trigger a one-off method in the AppState
MyAppState
:app.getState(MyAppState.class).doSomeMoreStuff();Don't mess with the AppState from other places, because from other methods you have no control over the order of updates. You don't know when (during which half-finished step of an update), your call was received.
+The com.jme3.app.state.AppStateManager holds the list of AppStates for an application. AppStateManager ensures that active AppStates are updated and rendered. When an AppState is attached, AppStateManager calls its stateAttached() method. When an AppState is detached, AppStateManager calls its stateDetached() method. +There is one AppStateManager per application. You can attach several AppStates to one AppStateManager, but the same state can only be attached once. +
+AppStateManager Method | Usage | +
---|---|
hasState(s) | Is AppState s attached? | +
getState(Class<T> stateClass) | Returns the first state that is an instance of a subclass of the specified class. | +
+The AppStateManager's update(), render(), postRender(), and cleanUp() methods are internal, users never call them directly. + +
+ +
+You can only change AppStates, or read and write to them, from certain places: In a Control's update() method, in an AppState's update() method, and in the SimpleApplication's simpleUpdate() loop (or the Application's update() loop).
+To get data from the AppState MyAppState
:
+
+
app.getState(MyAppState.class).getInfoAboutSomething();+ +
+
+To pass new data into the AppState MyAppState
:
+
+
app.getState(MyAppState.class).setSomething(blah);+ +
+
+To trigger a one-off method in the AppState MyAppState
:
+
+
app.getState(MyAppState.class).doSomeMoreStuff();+ +
+ +Don't mess with the AppState from other places, because from other methods you have no control over the order of updates. You don't know when (during which half-finished step of an update), your call was received. + +
+ +JME3 has an integrated an asset manager that helps you keep your project assets organized. By assets we mean media files, such as 3D models, materials, textures, scenes, shaders, sounds, and fonts. Think of the asset manager as the filesystem of your game, independent of the actual deployment platform. It also manages the appropriate managing of OpenGL objects like textures so that they are e.g. not uploaded to the graphics card multiple times when multiple models use them.
The assetManager
object is an com.jme3.asset.AssetManager instance that every com.jme3.app.Application can access. It maintains a root that also includes your project's classpath by default, so you can load any asset that's on the classpath, that is, the top level of your project directory.
You can use the inherited assetManager
object directly, or use the accessor getAssetManager()
.
Here is an example how you load assets using the AssetManager. This lines loads a default Material from the Common directory:
Material mat = (Material) assetManager.loadAsset( - new AssetKey("Common/Materials/RedColor.j3m"));
The Material is "somewhere" in the jME3 JAR, but the default Asset Manager is configured to handle a Common/…
path correctly, so you don't have to specify the whole path.
Additionally, You can configure the Asset Manager and add any path to its root. This means, you can load assets from any project directory you specify.
In project created with jMonkeyPlatform, jME3 searches for models in the assets
directory of your project by default. This is our recommended directory structure for storing assets:
MyGame/assets/Interface/ + ++ +AssetManager
++ ++ ++ +JME3 has an integrated an asset manager that helps you keep your project assets organized. By assets we mean media files, such as 3D models, materials, textures, scenes, shaders, sounds, and fonts. Think of the asset manager as the filesystem of your game, independent of the actual deployment platform. It also manages the appropriate managing of OpenGL objects like textures so that they are e.g. not uploaded to the graphics card multiple times when multiple models use them. +
+ ++The
+ +assetManager
object is an com.jme3.asset.AssetManager instance that every com.jme3.app.Application can access. It maintains a root that also includes your project's classpath by default, so you can load any asset that's on the classpath, that is, the top level of your project directory. ++You can use the inherited
+ +assetManager
object directly, or use the accessorgetAssetManager()
. ++Here is an example how you load assets using the AssetManager. This lines loads a default Material from the Common directory: +
+Material mat = (Material) assetManager.loadAsset( + new AssetKey("Common/Materials/RedColor.j3m"));+ ++The Material is "somewhere" in the jME3 JAR, but the default Asset Manager is configured to handle a
+ +Common/…
path correctly, so you don't have to specify the whole path. ++Additionally, You can configure the Asset Manager and add any path to its root. This means, you can load assets from any project directory you specify. +
+ +Asset Folder
++ ++ +By default, jME3 searches for models in a directory named
+ +assets
. In Java projects created with the jMonkeyPlatform, an assets folder is created by default. Using any other IDE or the command line, you have to create this assets directory as an extra step (see the Codeless Project tip below). ++This is our recommended directory structure for storing assets: +
+MyGame/assets/Interface/ MyGame/assets/MatDefs/ MyGame/assets/Materials/ MyGame/assets/Models/ @@ -9,65 +50,159 @@ MyGame/assets/Scenes/ MyGame/assets/Shaders/ MyGame/assets/Sounds/ MyGame/assets/Textures/ -MyGame/build.xml -MyGame/src/...These are just the most common examples, you can name the directories inside the assets directory how you like.
Loading Assets
+ +// Creating a material instance with the definition "Unshaded.j3md". -Material mat_brick = new Material( +MyGame/build.xml # build script +MyGame/src/... # source code+ ++These are just the most common examples. You can name the directories inside the
+ +assets
directory what ever you like. +Examples: Loading Assets
++ ++ ++ +Creating a material instance with the definition "Unshaded.j3md": + +
+Material mat_brick = new Material( assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); -// Applying a texture to the material -mat_brick.setTexture("ColorMap", - assetManager.loadTexture("Textures/Terrain/BrickWall/BrickWall.jpg")); - -// Loading a font -guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); - -// Loading a model -Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml"); - -// Loading a scene from an Ogre3D dotScene file stored inside a zip -assetManager.registerLocator("town.zip", ZipLocator.class.getName()); +Applying a texture to the material: +<code java> +mat_brick.setTexture("ColorMap", + assetManager.loadTexture("Textures/Terrain/BrickWall/BrickWall.jpg"));+ ++Loading a font: + +
+guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");+ ++Loading a model: + +
+Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");+ ++Loading a scene from an Ogre3D dotScene file stored inside a zip: + +
+assetManager.registerLocator("town.zip", ZipLocator.class.getName()); +Spatial scene = assetManager.loadModel("main.scene"); +rootNode.attachChild(scene);+ ++Alternatively to ZipLocator, there is also a HttpZipLocator that can stream models from a zip file online: +
+assetManager.registerLocator("http://jmonkeyengine.googlecode.com/files/wildhouse.zip", + HttpZipLocator.class.getName()); Spatial scene = assetManager.loadModel("main.scene"); -rootNode.attachChild(scene);Here is a HttpZipLocator that can stream models from a zip file online:
assetManager.registerLocator("http://jmonkeyengine.googlecode.com/files/wildhouse.zip", - HttpZipLocator.class.getName()); - Spatial scene = assetManager.loadModel("main.scene"); - rootNode.attachChild(scene);JME3 offers ClasspathLocator, ZipLocator, FileLocator, HttpZipLocator, and UrlLocator (see
com.jme3.asset.plugins
).+ +
Task? Solution! Load a model with materials Use the asset managers loadModel()
method and attach the Spatial to the rootNode.Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml"); -rootNode.attachChild(elephant);Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.j3o"); -rootNode.attachChild(elephant);+ Load a model without materials If you have a model without materials, you have to add a default material to make it visible. +Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.obj"); +rootNode.attachChild(scene);+ ++jME3 also offers a ClasspathLocator, ZipLocator, FileLocator, HttpZipLocator, and UrlLocator (see
+ +com.jme3.asset.plugins
). ++Note: The custom build script does not automatically include ZIP files in the executable build. +
+ + + +Comon AssetManager Tasks
+++
+ +Task? Solution! ++ +Load a model with materials Use the asset manager's +loadModel()
method and attach the Spatial to the rootNode.Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml"); +rootNode.attachChild(elephant);+Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.j3o"); +rootNode.attachChild(elephant);++ Load a model without materials If you have a model without materials, you have to add a default material to make it visible. Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.obj"); Material mat = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md"); teapot.setMaterial(mat); -rootNode.attachChild(teapot);Load a scene You load scenes just like you load models: Spatial scene = assetManager.loadModel("Scenes/house/main.scene"); -rootNode.attachChild(scene);NullPointerException: Cannot locate resource?
An error mesage similar to the following can occur in the console when you run executables, even if the game runs fine when started from the jMoneykPlatform.
com.jme3.asset.DesktopAssetManager loadAsset +rootNode.attachChild(teapot);++ +Load a scene You load scenes just like you load models: +Spatial scene = assetManager.loadModel("Scenes/house/main.scene"); +rootNode.attachChild(scene);+NullPointerException: Cannot locate resource?
++ ++ +Even if the game runs fine when started from the jMoneykPlatform, an error message similar to the following can occur in the console when you run the stand-alone executables (.JAR, .JNLP, etc). +
+com.jme3.asset.DesktopAssetManager loadAsset WARNING: Cannot locate resource: Scenes/town/main.scene com.jme3.app.Application handleError SEVERE: Uncaught exception thrown in Thread[LWJGL Renderer Thread,5,main] -java.lang.NullPointerExceptionReason: If you use the default build script created by the jMonkeyPlatform then the original OgreXML files are not included in the executable.
For a stand-alone build, you work with .j3o files only. The default build script makes sure to include .j3o files in the executable.
You must use the jMonkeyPlatform's context menu action to convert OgreXML models to .j3o format to get rid of this error.
Open the JME3 Project in the jMonkeyplatform. Browse the Assets directory in the Projects window. Right-click a .mesh.xml file, and choose "convert to JME3 binary". The converted file appears in the same directory as the .mesh.xml file. It has the same name and a .j3o suffix.Asset Handling: Codeless Projects
- \ No newline at end of file +java.lang.NullPointerExceptionIf you are using another IDE than jMonkeyPlatform, you can create a codeless project in the jMonkeyPlatform to maintain assets. This method will not meddle with your sources or custom build scripts, but you can still browse your assets, and preview, arrange, and convert models. You can, for example, give the designers in your team access to such a codeless project.
+Reason: +
+ ++If you use the default build script created by the jMonkeyPlatform then the original OgreXML files are not included in the executable. A stand-alone executable works with .j3o files only. The default build script makes sure to include .j3o files in the executable. +
+ ++Solution +
+ ++Before building the executable, you must use the jMonkeyPlatform's context menu action to convert OgreXML models to .j3o format. +
+assets
directory in the Projects window. + +If you load the scene from a non.j3o ZIP file, expand the default_build_script to copy the ZIP files. +
+ ++ +If you are using another IDE than jMonkeyPlatform for coding, you should create a so-called codeless project in the jMonkeyPlatform to maintain assets. This method will not meddle with your sources or custom build scripts. It simply makes it easier for you to browse game assets, and preview, arrange, and especially convert models to binary. +
+ There are two ways to handle audio data: Short audio files are to be stored entirely in memory, while long audio files (music) is streamed from the hard drive as it is played.
-Place audio files in the assets/Sound/
directory of your project. jME3 supports Ogg Vorbis (.ogg) and Wave (.wav) formats.
The main class to look at is com.jme3.audio.AudioNode
.
-By default, a new audio node is buffered, i.e. JME loads the whole file into memory before playing:
AudioNode boom = new AudioNode(audioRenderer, assetManager, "Sound/boom.wav");
If it is a long file, you set the boolean to true to stream the audio.
AudioNode music = new AudioNode(audioRenderer, assetManager, "Sound/music.wav", true);
AudioNode Method | Usage |
---|---|
getStatus() | Returns either Status.Playing, Status.Stopped, or Status.Paused. |
setVolume(1) | Sets the volume gain. 1 is the default volume, 2 is twice as loud, 0 is mute. |
setPitch(1) | Makes the sound play in a higher or lower pitch. Default is 1. |
AudioNode Method | Usage |
setLooping(false) | Configures the sound that, if it is played, it plays once and stops. This is the default. |
setLooping(true) | Configures the sound that, if it is played, it plays repeats from the beginning, until stop() or pause() are called. Good for ambient background noises. |
setPositional(false) setDirectional(false) | All 3D effects switched off. This sound is global and comes from everywhere. Good for environmental ambient sounds and background music. |
setTimeOffset(0.5f) | Start playing the sound after waiting the given amount of seconds. Default is 0. |
setMaxDistance(100f) | Maximum distance the sound can be heard, in world units. Default is 20. |
AudioNode Method | Usage |
setPositional(true) setLocalTranslation(new Vector 3f(0,0,0)) | Activates 3D audio, the sound appears to come from a certain position, where it is loudest. Position the AudioNode in the 3D scene if you have setPositional() true. Position it with mobile players or NPCs. |
setReverbEnabled(true) | A 3D echo effect that only makes sense to use with moving positional AudioNodes. The reverb effect is influenced by the environment that the audio renderer is in. See "Setting Environment Properties" below. |
AudioNode Method | Usage |
setDirectional(true) setDirection(new Vector3f(0,0,1)) | Activates 3D audio. This sound can only be heard from a certain direction. Specify the direction and angle in the 3D scene if you have setDirectional() true. Good for noises that should not be heard through a wall. |
setInnerAngle() setOuterAngle() | Set the angle in degrees for the directional audio. The angle is relative to the direction. By default, both angles are 360° and the sound can be heard from all directions. |
You play, pause, and stop a node called myAudioNode by using the respective of the following three methods:
myAudioNode.play();
myAudioNode.pause();
myAudioNode.stop();
Note: Whether an Audio Node plays continuously or only once, depends on the Loop properties you have set above!
Optionally, You can choose from the following environmental presets from com.jme3.audio.Environment
. This presets influence subtle echo effects that evoke associations of different environments in your users. You use it together with setReverbEnbaled(true) mentioned above.
Environment | density | diffusion | gain | gainHf | decayTime | decayHf | reflGain | reflDelay | lateGain | lateDelay |
---|---|---|---|---|---|---|---|---|---|---|
Garage | 1.00f | 1.0f | 1.0f | 1.00f | 0.90f | 0.5f | 0.751f | 0.0039f | 0.661f | 0.0137f |
Dungeon | 0.75f | 1.0f | 1.0f | 0.75f | 1.60f | 1.0f | 0.950f | 0.0026f | 0.930f | 0.0103f |
Cavern | 0.50f | 1.0f | 1.0f | 0.50f | 2.25f | 1.0f | 0.908f | 0.0103f | 0.930f | 0.0410f |
AcousticLab | 0.50f | 1.0f | 1.0f | 1.00f | 0.28f | 1.0f | 0.870f | 0.0020f | 0.810f | 0.0080f |
Closet | 1.00f | 1.0f | 1.0f | 1.00f | 0.15f | 1.0f | 0.600f | 0.0025f | 0.500f | 0.0006f |
Activate the preset with setEnvironment(). E.g. in a dungeon environment:
audioRenderer.setEnvironment(new Environment.Dungeon));
A sound engineer can create a custom com.jme3.audio.Environment
object and specify custom environment factors. Activate your custom environment settings in the Environment constructor:
audioRenderer.setEnvironment( + ++ +Audio in jME3
++ ++ ++ +There are two ways to handle audio data: Short audio files are to be stored entirely in memory, while long audio files (music) is streamed from the hard drive as it is played. +
+ ++Place audio files in the
+ +assets/Sound/
directory of your project. jME3 supports Ogg Vorbis (.ogg) and Wave (.wav) formats. +Creating Audio Nodes
++ ++ ++ +The main class to look at is
+com.jme3.audio.AudioNode
. +By default, a new audio node is buffered, i.e. JME loads the whole file into memory before playing: + +AudioNode boom = new AudioNode(assetManager, "Sound/boom.wav");+ ++ +If it is a long file, you set the boolean to true to stream the audio. + +
+AudioNode music = new AudioNode(assetManager, "Sound/music.wav", true);+ +Setting AudioNode Properties
+++ +++
+ +AudioNode Method Usage ++ +getStatus() Returns either Status.Playing, Status.Stopped, or Status.Paused. ++ +setVolume(1) Sets the volume gain. 1 is the default volume, 2 is twice as loud, 0 is mute. ++ +setPitch(1) Makes the sound play in a higher or lower pitch. Default is 1. +++
+ +AudioNode Method Usage ++ +setLooping(false) Configures the sound that, if it is played, it plays once and stops. This is the default. ++ +setLooping(true) Configures the sound that, if it is played, it plays repeats from the beginning, until stop() or pause() are called. Good for ambient background noises. ++ +setPositional(false)
+setDirectional(false)All 3D effects switched off. This sound is global and comes from everywhere. Good for environmental ambient sounds and background music. ++ +setTimeOffset(0.5f) Start playing the sound after waiting the given amount of seconds. Default is 0. ++ +setMaxDistance(100f) Maximum distance the sound can be heard, in world units. Default is 20. +++
+ +AudioNode Method Usage ++ +setPositional(true)
+setLocalTranslation(…)Activates 3D audio: The sound appears to come from a certain position, where it is loudest. Position the AudioNode in the 3D scene, or move it with mobile players or NPCs. ++ +setReverbEnabled(true) A 3D echo effect that only makes sense to use with positional AudioNodes. The reverb effect is influenced by the environment that the audio renderer is in. See "Setting Environment Properties" below. ++ ++
+ +AudioNode Method Usage ++ +setDirectional(true)
+setDirection(…)Activates 3D audio: This sound can only be heard from a certain direction. Specify the direction and angle in the 3D scene if you have setDirectional() true. Use this to restrict noises that should not be heard, for example, through a wall. ++ +setInnerAngle()
+setOuterAngle()Set the angle in degrees for the directional audio. The angle is relative to the direction. Note: By default, both angles are 360° and the sound can be heard from all directions! +Play, Pause, Stop
++ ++ ++ +You play, pause, and stop a node called myAudioNode by using the respective of the following three methods: +
+myAudioNode.play();+myAudioNode.pause();+myAudioNode.stop();+ ++Note: Whether an Audio Node plays continuously or only once, depends on the Loop properties you have set above! +
+ ++You can also start playing an instance of this AudioNode. Use the playInstance() method if you need to play the same AudioNode multiple times, possibly simulatenously. Note that changes to the parameters of the original AudioNode do not affect the instances that are already playing! +
+myAudioNode.playInstance();+ +The Listener
++ ++ ++ +The default listener object is the user's ear in the scene. If you use positional audio, you have to move the listener with the player: For example, for a first-person player, you move the listener with the camera. For a third-person player, you move the listener with the player avatar Geometry. +
+@Override + public void simpleUpdate(float tpf) { + // keep the audio listener moving with the camera + listener.setLocation(cam.getLocation()); + listener.setRotation(cam.getRotation()); + }+ +Setting Environment Properties
++ +- \ No newline at end of file + reflGain, reflDelay, lateGain, lateDelay ) );+ +Optionally, You can choose from the following environmental presets from
+com.jme3.audio.Environment
. This presets influence subtle echo effects that evoke associations of different environments in your users. You use it together with setReverbEnabled(true) mentioned above. + ++ ++
+ +Environment density diffusion gain gainHf decayTime decayHf reflGain reflDelay lateGain lateDelay ++ +Garage 1.00f 1.0f 1.0f 1.00f 0.90f 0.5f 0.751f 0.0039f 0.661f 0.0137f ++ +Dungeon 0.75f 1.0f 1.0f 0.75f 1.60f 1.0f 0.950f 0.0026f 0.930f 0.0103f ++ +Cavern 0.50f 1.0f 1.0f 0.50f 2.25f 1.0f 0.908f 0.0103f 0.930f 0.0410f ++ +AcousticLab 0.50f 1.0f 1.0f 1.00f 0.28f 1.0f 0.870f 0.0020f 0.810f 0.0080f ++ +Closet 1.00f 1.0f 1.0f 1.00f 0.15f 1.0f 0.600f 0.0025f 0.500f 0.0006f ++ +Activate the preset with setEnvironment(). E.g. in a dungeon environment: +
+audioRenderer.setEnvironment(new Environment.Dungeon));+ ++A sound engineer can create a custom
+com.jme3.audio.Environment
object and specify custom environment factors. You can find many examples of audio environment presets here. Activate your custom environment settings in the Environment constructor: +audioRenderer.setEnvironment( new Environment( density, diffusion, gain, gainHf, decayTime, decayHf, - reflGain, reflDelay, lateGain, lateDelay ) );You can find more info about OpenAL and its advanced features here: OpenAL 1.1 Specification
+You can find more info about OpenAL and its advanced features here: + +
+ ++ +Use these presets together with Audio Nodes to create different "moods" for sounds. Environment effects make your audio sound as if the listener were in various places that have different types of echoes. +
+ ++Usage: + +
+Environment( + new float[]{ 0, 7.5f, 1f, -1000, -100, 0, 1.49f, 0.83f, 1f, -2602, + 0.007f, 0f, 0f, 0f, 200, 0.011f, 0f, 0f, 0f, 0.250f, + 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +audioRenderer.setEnvironment(myEnvironment);+ +
CastleSmallRoom = new Environment ( new float[]{ 26, 8.3f, 0.890f, -1000, -800, -2000, 1.22f, 0.83f, 0.31f, -100, 0.022f, 0f, 0f, 0f, 600, 0.011f, 0f, 0f, 0f, 0.138f, 0.080f, 0.250f, 0f, -5f, 5168.6f, 139.5f, 0f, 0x20} ) ); +CastleShortPassage = new Environment ( new float[]{ 26, 8.3f, 0.890f, -1000, -1000, -2000, 2.32f, 0.83f, 0.31f, -100, 0.007f, 0f, 0f, 0f, 200, 0.023f, 0f, 0f, 0f, 0.138f, 0.080f, 0.250f, 0f, -5f, 5168.6f, 139.5f, 0f, 0x20} ) ); +CastleMediumroom = new Environment ( new float[]{ 26, 8.3f, 0.930f, -1000, -1100, -2000, 2.04f, 0.83f, 0.46f, -400, 0.022f, 0f, 0f, 0f, 400, 0.011f, 0f, 0f, 0f, 0.155f, 0.030f, 0.250f, 0f, -5f, 5168.6f, 139.5f, 0f, 0x20} ) ); +CastleLongpassage = new Environment ( new float[]{ 26, 8.3f, 0.890f, -1000, -800, -2000, 3.42f, 0.83f, 0.31f, -100, 0.007f, 0f, 0f, 0f, 300, 0.023f, 0f, 0f, 0f, 0.138f, 0.080f, 0.250f, 0f, -5f, 5168.6f, 139.5f, 0f, 0x20} ) ); +CastleLargeroom = new Environment ( new float[]{ 26, 8.3f, 0.820f, -1000, -1100, -1800, 2.53f, 0.83f, 0.50f, -700, 0.034f, 0f, 0f, 0f, 200, 0.016f, 0f, 0f, 0f, 0.185f, 0.070f, 0.250f, 0f, -5f, 5168.6f, 139.5f, 0f, 0x20} ) ); +CastleHall = new Environment ( new float[]{ 26, 8.3f, 0.810f, -1000, -1100, -1500, 3.14f, 0.79f, 0.62f, -1500, 0.056f, 0f, 0f, 0f, 100, 0.024f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5168.6f, 139.5f, 0f, 0x20} ) ); +CastleCupboard = new Environment ( new float[]{ 26, 8.3f, 0.890f, -1000, -1100, -2000, 0.67f, 0.87f, 0.31f, 300, 0.010f, 0f, 0f, 0f, 1100, 0.007f, 0f, 0f, 0f, 0.138f, 0.080f, 0.250f, 0f, -5f, 5168.6f, 139.5f, 0f, 0x20} ) ); +CastleCourtyard = new Environment ( new float[]{ 26, 8.3f, 0.420f, -1000, -700, -1400, 2.13f, 0.61f, 0.23f, -1300, 0.160f, 0f, 0f, 0f, -300, 0.036f, 0f, 0f, 0f, 0.250f, 0.370f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x1f} ) ); +CastleAlcove = new Environment ( new float[]{ 26, 8.3f, 0.890f, -1000, -600, -2000, 1.64f, 0.87f, 0.31f, 00, 0.007f, 0f, 0f, 0f, 300, 0.034f, 0f, 0f, 0f, 0.138f, 0.080f, 0.250f, 0f, -5f, 5168.6f, 139.5f, 0f, 0x20} ) );+ +
FactoryAlcove = new Environment ( new float[]{ 26, 1.8f, 0.590f, -1200, -200, -600, 3.14f, 0.65f, 1.31f, 300, 0.010f, 0f, 0f, 0f, 000, 0.038f, 0f, 0f, 0f, 0.114f, 0.100f, 0.250f, 0f, -5f, 3762.6f, 362.5f, 0f, 0x20} ) ); +FactoryShortpassage = new Environment ( new float[]{ 26, 1.8f, 0.640f, -1200, -200, -600, 2.53f, 0.65f, 1.31f, 0, 0.010f, 0f, 0f, 0f, 200, 0.038f, 0f, 0f, 0f, 0.135f, 0.230f, 0.250f, 0f, -5f, 3762.6f, 362.5f, 0f, 0x20} ) ); +FactoryMediumroom = new Environment ( new float[]{ 26, 1.9f, 0.820f, -1200, -200, -600, 2.76f, 0.65f, 1.31f, -1100, 0.022f, 0f, 0f, 0f, 300, 0.023f, 0f, 0f, 0f, 0.174f, 0.070f, 0.250f, 0f, -5f, 3762.6f, 362.5f, 0f, 0x20} ) ); +FactoryLongpassage = new Environment ( new float[]{ 26, 1.8f, 0.640f, -1200, -200, -600, 4.06f, 0.65f, 1.31f, 0, 0.020f, 0f, 0f, 0f, 200, 0.037f, 0f, 0f, 0f, 0.135f, 0.230f, 0.250f, 0f, -5f, 3762.6f, 362.5f, 0f, 0x20} ) ); +FactoryLargeroom = new Environment ( new float[]{ 26, 1.9f, 0.750f, -1200, -300, -400, 4.24f, 0.51f, 1.31f, -1500, 0.039f, 0f, 0f, 0f, 100, 0.023f, 0f, 0f, 0f, 0.231f, 0.070f, 0.250f, 0f, -5f, 3762.6f, 362.5f, 0f, 0x20} ) ); +FactoryHall = new Environment ( new float[]{ 26, 1.9f, 0.750f, -1000, -300, -400, 7.43f, 0.51f, 1.31f, -2400, 0.073f, 0f, 0f, 0f, -100, 0.027f, 0f, 0f, 0f, 0.250f, 0.070f, 0.250f, 0f, -5f, 3762.6f, 362.5f, 0f, 0x20} ) ); +FactoryCupboard = new Environment ( new float[]{ 26, 1.7f, 0.630f, -1200, -200, -600, 0.49f, 0.65f, 1.31f, 200, 0.010f, 0f, 0f, 0f, 600, 0.032f, 0f, 0f, 0f, 0.107f, 0.070f, 0.250f, 0f, -5f, 3762.6f, 362.5f, 0f, 0x20} ) ); +FactoryCourtyard = new Environment ( new float[]{ 26, 1.7f, 0.570f, -1000, -1000, -400, 2.32f, 0.29f, 0.56f, -1300, 0.140f, 0f, 0f, 0f, -800, 0.039f, 0f, 0f, 0f, 0.250f, 0.290f, 0.250f, 0f, -5f, 3762.6f, 362.5f, 0f, 0x20} ) ); +FactorySmallroom = new Environment ( new float[]{ 26, 1.8f, 0.820f, -1000, -200, -600, 1.72f, 0.65f, 1.31f, -300, 0.010f, 0f, 0f, 0f, 500, 0.024f, 0f, 0f, 0f, 0.119f, 0.070f, 0.250f, 0f, -5f, 3762.6f, 362.5f, 0f, 0x20} ) );+ +
IcepalaceAlcove = new Environment ( new float[]{ 26, 2.7f, 0.840f, -1000, -500, -1100, 2.76f, 1.46f, 0.28f, 100, 0.010f, 0f, 0f, 0f, -100, 0.030f, 0f, 0f, 0f, 0.161f, 0.090f, 0.250f, 0f, -5f, 12428.5f, 99.6f, 0f, 0x20} ) ); +IcepalaceShortpassage = new Environment ( new float[]{ 26, 2.7f, 0.750f, -1000, -500, -1100, 1.79f, 1.46f, 0.28f, -600, 0.010f, 0f, 0f, 0f, 100, 0.019f, 0f, 0f, 0f, 0.177f, 0.090f, 0.250f, 0f, -5f, 12428.5f, 99.6f, 0f, 0x20} ) ); +IcepalaceMediumroom = new Environment ( new float[]{ 26, 2.7f, 0.870f, -1000, -500, -700, 2.22f, 1.53f, 0.32f, -800, 0.039f, 0f, 0f, 0f, 100, 0.027f, 0f, 0f, 0f, 0.186f, 0.120f, 0.250f, 0f, -5f, 12428.5f, 99.6f, 0f, 0x20} ) ); +IcepalaceLongpassage = new Environment ( new float[]{ 26, 2.7f, 0.770f, -1000, -500, -800, 3.01f, 1.46f, 0.28f, -200, 0.012f, 0f, 0f, 0f, 200, 0.025f, 0f, 0f, 0f, 0.186f, 0.040f, 0.250f, 0f, -5f, 12428.5f, 99.6f, 0f, 0x20} ) ); +IcepalaceLargeroom = new Environment ( new float[]{ 26, 2.9f, 0.810f, -1000, -500, -700, 3.14f, 1.53f, 0.32f, -1200, 0.039f, 0f, 0f, 0f, 000, 0.027f, 0f, 0f, 0f, 0.214f, 0.110f, 0.250f, 0f, -5f, 12428.5f, 99.6f, 0f, 0x20} ) ); +IcepalaceHall = new Environment ( new float[]{ 26, 2.9f, 0.760f, -1000, -700, -500, 5.49f, 1.53f, 0.38f, -1900, 0.054f, 0f, 0f, 0f, -400, 0.052f, 0f, 0f, 0f, 0.226f, 0.110f, 0.250f, 0f, -5f, 12428.5f, 99.6f, 0f, 0x20} ) ); +IcepalaceCupboard = new Environment ( new float[]{ 26, 2.7f, 0.830f, -1000, -600, -1300, 0.76f, 1.53f, 0.26f, 100, 0.012f, 0f, 0f, 0f, 600, 0.016f, 0f, 0f, 0f, 0.143f, 0.080f, 0.250f, 0f, -5f, 12428.5f, 99.6f, 0f, 0x20} ) ); +IcepalaceCourtyard = new Environment ( new float[]{ 26, 2.9f, 0.590f, -1000, -1100, -1000, 2.04f, 1.20f, 0.38f, -1000, 0.173f, 0f, 0f, 0f, -1000, 0.043f, 0f, 0f, 0f, 0.235f, 0.480f, 0.250f, 0f, -5f, 12428.5f, 99.6f, 0f, 0x20} ) ); +IcepalaceSmallroom = new Environment ( new float[]{ 26, 2.7f, 0.840f, -1000, -500, -1100, 1.51f, 1.53f, 0.27f, -100, 0.010f, 0f, 0f, 0f, 300, 0.011f, 0f, 0f, 0f, 0.164f, 0.140f, 0.250f, 0f, -5f, 12428.5f, 99.6f, 0f, 0x20} ) );+ +
SpacestationAlcove = new Environment ( new float[]{ 26, 1.5f, 0.780f, -1000, -300, -100, 1.16f, 0.81f, 0.55f, 300, 0.007f, 0f, 0f, 0f, 000, 0.018f, 0f, 0f, 0f, 0.192f, 0.210f, 0.250f, 0f, -5f, 3316.1f, 458.2f, 0f, 0x20} ) ); +SpacestationMediumroom = new Environment ( new float[]{ 26, 1.5f, 0.750f, -1000, -400, -100, 3.01f, 0.50f, 0.55f, -800, 0.034f, 0f, 0f, 0f, 100, 0.035f, 0f, 0f, 0f, 0.209f, 0.310f, 0.250f, 0f, -5f, 3316.1f, 458.2f, 0f, 0x20} ) ); +SpacestationShortpassage = new Environment ( new float[]{ 26, 1.5f, 0.870f, -1000, -400, -100, 3.57f, 0.50f, 0.55f, 0, 0.012f, 0f, 0f, 0f, 100, 0.016f, 0f, 0f, 0f, 0.172f, 0.200f, 0.250f, 0f, -5f, 3316.1f, 458.2f, 0f, 0x20} ) ); +SpacestationLongpassage = new Environment ( new float[]{ 26, 1.9f, 0.820f, -1000, -400, -100, 4.62f, 0.62f, 0.55f, 0, 0.012f, 0f, 0f, 0f, 200, 0.031f, 0f, 0f, 0f, 0.250f, 0.230f, 0.250f, 0f, -5f, 3316.1f, 458.2f, 0f, 0x20} ) ); +SpacestationLargeroom = new Environment ( new float[]{ 26, 1.8f, 0.810f, -1000, -400, -100, 3.89f, 0.38f, 0.61f, -1000, 0.056f, 0f, 0f, 0f, -100, 0.035f, 0f, 0f, 0f, 0.233f, 0.280f, 0.250f, 0f, -5f, 3316.1f, 458.2f, 0f, 0x20} ) ); +SpacestationHall = new Environment ( new float[]{ 26, 1.9f, 0.870f, -1000, -400, -100, 7.11f, 0.38f, 0.61f, -1500, 0.100f, 0f, 0f, 0f, -400, 0.047f, 0f, 0f, 0f, 0.250f, 0.250f, 0.250f, 0f, -5f, 3316.1f, 458.2f, 0f, 0x20} ) ); +SpacestationCupboard = new Environment ( new float[]{ 26, 1.4f, 0.560f, -1000, -300, -100, 0.79f, 0.81f, 0.55f, 300, 0.007f, 0f, 0f, 0f, 500, 0.018f, 0f, 0f, 0f, 0.181f, 0.310f, 0.250f, 0f, -5f, 3316.1f, 458.2f, 0f, 0x20} ) ); +SpacestationSmallroom = new Environment ( new float[]{ 26, 1.5f, 0.700f, -1000, -300, -100, 1.72f, 0.82f, 0.55f, -200, 0.007f, 0f, 0f, 0f, 300, 0.013f, 0f, 0f, 0f, 0.188f, 0.260f, 0.250f, 0f, -5f, 3316.1f, 458.2f, 0f, 0x20} ) );+ +
WoodenAlcove = new Environment ( new float[]{ 26, 7.5f, 1f, -1000, -1800, -1000, 1.22f, 0.62f, 0.91f, 100, 0.012f, 0f, 0f, 0f, -300, 0.024f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 4705f, 99.6f, 0f, 0x3f} ) ); +WoodenShortpassage = new Environment ( new float[]{ 26, 7.5f, 1f, -1000, -1800, -1000, 1.75f, 0.50f, 0.87f, -100, 0.012f, 0f, 0f, 0f, -400, 0.024f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 4705f, 99.6f, 0f, 0x3f} ) ); +WoodenMediumroom = new Environment ( new float[]{ 26, 7.5f, 1f, -1000, -2000, -1100, 1.47f, 0.42f, 0.82f, -100, 0.049f, 0f, 0f, 0f, -100, 0.029f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 4705f, 99.6f, 0f, 0x3f} ) ); +WoodenLongpassage = new Environment ( new float[]{ 26, 7.5f, 1f, -1000, -2000, -1000, 1.99f, 0.40f, 0.79f, 000, 0.020f, 0f, 0f, 0f, -700, 0.036f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 4705f, 99.6f, 0f, 0x3f} ) ); +WoodenLargeroom = new Environment ( new float[]{ 26, 7.5f, 1f, -1000, -2100, -1100, 2.65f, 0.33f, 0.82f, -100, 0.066f, 0f, 0f, 0f, -200, 0.049f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 4705f, 99.6f, 0f, 0x3f} ) ); +WoodenHall = new Environment ( new float[]{ 26, 7.5f, 1f, -1000, -2200, -1100, 3.45f, 0.30f, 0.82f, -100, 0.088f, 0f, 0f, 0f, -200, 0.063f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 4705f, 99.6f, 0f, 0x3f} ) ); +WoodenCupboard = new Environment ( new float[]{ 26, 7.5f, 1f, -1000, -1700, -1000, 0.56f, 0.46f, 0.91f, 100, 0.012f, 0f, 0f, 0f, 100, 0.028f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 4705f, 99.6f, 0f, 0x3f} ) ); +WoodenSmallroom = new Environment ( new float[]{ 26, 7.5f, 1f, -1000, -1900, -1000, 0.79f, 0.32f, 0.87f, 00, 0.032f, 0f, 0f, 0f, -100, 0.029f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 4705f, 99.6f, 0f, 0x3f} ) ); +WoodenCourtyard = new Environment ( new float[]{ 26, 7.5f, 0.650f, -1000, -2200, -1000, 1.79f, 0.35f, 0.79f, -500, 0.123f, 0f, 0f, 0f, -2000, 0.032f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 4705f, 99.6f, 0f, 0x3f} ) );+ +
SportEmptystadium = new Environment ( new float[]{ 26, 7.2f, 1f, -1000, -700, -200, 6.26f, 0.51f, 1.10f, -2400, 0.183f, 0f, 0f, 0f, -800, 0.038f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x20} ) ); +SportSquashcourt = new Environment ( new float[]{ 26, 7.5f, 0.750f, -1000, -1000, -200, 2.22f, 0.91f, 1.16f, -700, 0.007f, 0f, 0f, 0f, -200, 0.011f, 0f, 0f, 0f, 0.126f, 0.190f, 0.250f, 0f, -5f, 7176.9f, 211.2f, 0f, 0x20} ) ); +SportSmallswimmingpool = new Environment ( new float[]{ 26, 36.2f, 0.700f, -1000, -200, -100, 2.76f, 1.25f, 1.14f, -400, 0.020f, 0f, 0f, 0f, -200, 0.030f, 0f, 0f, 0f, 0.179f, 0.150f, 0.895f, 0.190f, -5f, 5000f, 250f, 0f, 0x0} ) ); +SportLargeswimmingpool = new Environment ( new float[]{ 26, 36.2f, 0.820f, -1000, -200, 0, 5.49f, 1.31f, 1.14f, -700, 0.039f, 0f, 0f, 0f, -600, 0.049f, 0f, 0f, 0f, 0.222f, 0.550f, 1.159f, 0.210f, -5f, 5000f, 250f, 0f, 0x0} ) ); +SportGymnasium = new Environment ( new float[]{ 26, 7.5f, 0.810f, -1000, -700, -100, 3.14f, 1.06f, 1.35f, -800, 0.029f, 0f, 0f, 0f, -500, 0.045f, 0f, 0f, 0f, 0.146f, 0.140f, 0.250f, 0f, -5f, 7176.9f, 211.2f, 0f, 0x20} ) ); +SportFullstadium = new Environment ( new float[]{ 26, 7.2f, 1f, -1000, -2300, -200, 5.25f, 0.17f, 0.80f, -2000, 0.188f, 0f, 0f, 0f, -1100, 0.038f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x20} ) );+ +
Sewerpipe = new Environment ( new float[]{ 21, 1.7f, 0.800f, -1000, -1000, 0, 2.81f, 0.14f, 1f, 429, 0.014f, 0f, 0f, 0f, 1023, 0.021f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +PipeSmall = new Environment ( new float[]{ 26, 50.3f, 1f, -1000, -900, -1300, 5.04f, 0.10f, 0.10f, -600, 0.032f, 0f, 0f, 0f, 800, 0.015f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 2854.4f, 20f, 0f, 0x3f} ) ); +PipeLongthin = new Environment ( new float[]{ 26, 1.6f, 0.910f, -1000, -700, -1100, 9.21f, 0.18f, 0.10f, -300, 0.010f, 0f, 0f, 0f, -300, 0.022f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 2854.4f, 20f, 0f, 0x0} ) ); +PipeLarge = new Environment ( new float[]{ 26, 50.3f, 1f, -1000, -900, -1300, 8.45f, 0.10f, 0.10f, -800, 0.046f, 0f, 0f, 0f, 400, 0.032f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 2854.4f, 20f, 0f, 0x3f} ) ); +PipeResonant = new Environment ( new float[]{ 26, 1.3f, 0.910f, -1000, -700, -1100, 6.81f, 0.18f, 0.10f, -300, 0.010f, 0f, 0f, 0f, 00, 0.022f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 2854.4f, 20f, 0f, 0x0} ) );+ +
Heaven = new Environment ( new float[]{ 26, 19.6f, 0.940f, -1000, -200, -700, 5.04f, 1.12f, 0.56f, -1230, 0.020f, 0f, 0f, 0f, 200, 0.029f, 0f, 0f, 0f, 0.250f, 0.080f, 2.742f, 0.050f, -2f, 5000f, 250f, 0f, 0x3f} ) ); +Hell = new Environment ( new float[]{ 26, 100f, 0.570f, -1000, -900, -700, 3.57f, 0.49f, 2f, -10000, 0.020f, 0f, 0f, 0f, 300, 0.030f, 0f, 0f, 0f, 0.110f, 0.040f, 2.109f, 0.520f, -5f, 5000f, 139.5f, 0f, 0x40} ) ); +Memory = new Environment ( new float[]{ 26, 8f, 0.850f, -1000, -400, -900, 4.06f, 0.82f, 0.56f, -2800, 0f, 0f, 0f, 0f, 100, 0f, 0f, 0f, 0f, 0.250f, 0f, 0.474f, 0.450f, -10f, 5000f, 250f, 0f, 0x0} ) ); +Drugged = new Environment ( new float[]{ 23, 1.9f, 0.500f, -1000, 0, 0, 8.39f, 1.39f, 1f, -115, 0.002f, 0f, 0f, 0f, 985, 0.030f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 1f, -5f, 5000f, 250f, 0f, 0x1f} ) ); +Dizzy = new Environment ( new float[]{ 24, 1.8f, 0.600f, -1000, -400, 0, 17.23f, 0.56f, 1f, -1713, 0.020f, 0f, 0f, 0f, -613, 0.030f, 0f, 0f, 0f, 0.250f, 1f, 0.810f, 0.310f, -5f, 5000f, 250f, 0f, 0x1f} ) ); +Psychotic = new Environment ( new float[]{ 25, 1f, 0.500f, -1000, -151, 0, 7.56f, 0.91f, 1f, -626, 0.020f, 0f, 0f, 0f, 774, 0.030f, 0f, 0f, 0f, 0.250f, 0f, 4f, 1f, -5f, 5000f, 250f, 0f, 0x1f} ) );+ +
DrivingCommentator = new Environment ( new float[]{ 26, 3f, 0f, 1000, -500, -600, 2.42f, 0.88f, 0.68f, -1400, 0.093f, 0f, 0f, 0f, -1200, 0.017f, 0f, 0f, 0f, 0.250f, 1f, 0.250f, 0f, -10f, 5000f, 250f, 0f, 0x20} ) ); +DrivingPitgarage = new Environment ( new float[]{ 26, 1.9f, 0.590f, -1000, -300, -500, 1.72f, 0.93f, 0.87f, -500, 0f, 0f, 0f, 0f, 200, 0.016f, 0f, 0f, 0f, 0.250f, 0.110f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x0} ) ); +DrivingIncarRacer = new Environment ( new float[]{ 26, 1.1f, 0.800f, -1000, 0, -200, 0.17f, 2f, 0.41f, 500, 0.007f, 0f, 0f, 0f, -300, 0.015f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 10268.2f, 251f, 0f, 0x20} ) ); +DrivingIncarSports = new Environment ( new float[]{ 26, 1.1f, 0.800f, -1000, -400, 0, 0.17f, 0.75f, 0.41f, 0, 0.010f, 0f, 0f, 0f, -500, 0f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 10268.2f, 251f, 0f, 0x20} ) ); +DrivingIncarLuxury = new Environment ( new float[]{ 26, 1.6f, 1f, -1000, -2000, -600, 0.13f, 0.41f, 0.46f, -200, 0.010f, 0f, 0f, 0f, 400, 0.010f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 10268.2f, 251f, 0f, 0x20} ) ); +DrivingFullgrandstand = new Environment ( new float[]{ 26, 8.3f, 1f, -1000, -1100, -400, 3.01f, 1.37f, 1.28f, -900, 0.090f, 0f, 0f, 0f, -1500, 0.049f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 10420.2f, 250f, 0f, 0x1f} ) ); +DrivingEmptygrandstand = new Environment ( new float[]{ 26, 8.3f, 1f, -1000, 0, -200, 4.62f, 1.75f, 1.40f, -1363, 0.090f, 0f, 0f, 0f, -1200, 0.049f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 10420.2f, 250f, 0f, 0x1f} ) ); +DrivingTunnel = new Environment ( new float[]{ 26, 3.1f, 0.810f, -1000, -800, -100, 3.42f, 0.94f, 1.31f, -300, 0.051f, 0f, 0f, 0f, -300, 0.047f, 0f, 0f, 0f, 0.214f, 0.050f, 0.250f, 0f, -5f, 5000f, 155.3f, 0f, 0x20} ) );+ +
CityIndoors = new Environment ( new float[]{ 16, 7.5f, 0.500f, -1000, -800, 0, 1.49f, 0.67f, 1f, -2273, 0.007f, 0f, 0f, 0f, -1691, 0.011f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +CityStreets = new Environment ( new float[]{ 26, 3f, 0.780f, -1000, -300, -100, 1.79f, 1.12f, 0.91f, -1100, 0.046f, 0f, 0f, 0f, -1400, 0.028f, 0f, 0f, 0f, 0.250f, 0.200f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x20} ) ); +CitySubway = new Environment ( new float[]{ 26, 3f, 0.740f, -1000, -300, -100, 3.01f, 1.23f, 0.91f, -300, 0.046f, 0f, 0f, 0f, 200, 0.028f, 0f, 0f, 0f, 0.125f, 0.210f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x20} ) ); +CityMuseum = new Environment ( new float[]{ 26, 80.3f, 0.820f, -1000, -1500, -1500, 3.28f, 1.40f, 0.57f, -1200, 0.039f, 0f, 0f, -0f, -100, 0.034f, 0f, 0f, 0f, 0.130f, 0.170f, 0.250f, 0f, -5f, 2854.4f, 107.5f, 0f, 0x0} ) ); +CityLibrary = new Environment ( new float[]{ 26, 80.3f, 0.820f, -1000, -1100, -2100, 2.76f, 0.89f, 0.41f, -900, 0.029f, 0f, 0f, -0f, -100, 0.020f, 0f, 0f, 0f, 0.130f, 0.170f, 0.250f, 0f, -5f, 2854.4f, 107.5f, 0f, 0x0} ) ); +CityUnderpass = new Environment ( new float[]{ 26, 3f, 0.820f, -1000, -700, -100, 3.57f, 1.12f, 0.91f, -800, 0.059f, 0f, 0f, 0f, -100, 0.037f, 0f, 0f, 0f, 0.250f, 0.140f, 0.250f, 0f, -7f, 5000f, 250f, 0f, 0x20} ) ); +CityAbandoned = new Environment ( new float[]{ 26, 3f, 0.690f, -1000, -200, -100, 3.28f, 1.17f, 0.91f, -700, 0.044f, 0f, 0f, 0f, -1100, 0.024f, 0f, 0f, 0f, 0.250f, 0.200f, 0.250f, 0f, -3f, 5000f, 250f, 0f, 0x20} ) );+ +
Room = new Environment ( new float[]{ 2, 1.9f, 1f, -1000, -454, 0, 0.40f, 0.83f, 1f, -1646, 0.002f, 0f, 0f, 0f, 53, 0.003f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Bathroom = new Environment ( new float[]{ 3, 1.4f, 1f, -1000, -1200, 0, 1.49f, 0.54f, 1f, -370, 0.007f, 0f, 0f, 0f, 1030, 0.011f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Livingroom = new Environment ( new float[]{ 4, 2.5f, 1f, -1000, -6000, 0, 0.50f, 0.10f, 1f, -1376, 0.003f, 0f, 0f, 0f, -1104, 0.004f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Paddedcell = new Environment ( new float[]{ 1, 1.4f, 1f, -1000, -6000, 0, 0.17f, 0.10f, 1f, -1204, 0.001f, 0f, 0f, 0f, 207, 0.002f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Stoneroom = new Environment ( new float[]{ 5, 11.6f, 1f, -1000, -300, 0, 2.31f, 0.64f, 1f, -711, 0.012f, 0f, 0f, 0f, 83, 0.017f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) );+ +
Workshop = new Environment ( new float[]{ 26, 1.9f, 1f, -1000, -1700, -800, 0.76f, 1f, 1f, 0, 0.012f, 0f, 0f, 0f, 100, 0.012f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x0} ) ); +Schoolroom = new Environment ( new float[]{ 26, 1.86f, 0.690f, -1000, -400, -600, 0.98f, 0.45f, 0.18f, 300, 0.017f, 0f, 0f, 0f, 300, 0.015f, 0f, 0f, 0f, 0.095f, 0.140f, 0.250f, 0f, -5f, 7176.9f, 211.2f, 0f, 0x20} ) ); +Practiseroom = new Environment ( new float[]{ 26, 1.86f, 0.870f, -1000, -800, -600, 1.12f, 0.56f, 0.18f, 200, 0.010f, 0f, 0f, 0f, 300, 0.011f, 0f, 0f, 0f, 0.095f, 0.140f, 0.250f, 0f, -5f, 7176.9f, 211.2f, 0f, 0x20} ) ); +Outhouse = new Environment ( new float[]{ 26, 80.3f, 0.820f, -1000, -1900, -1600, 1.38f, 0.38f, 0.35f, -100, 0.024f, 0f, 0f, -0f, -400, 0.044f, 0f, 0f, 0f, 0.121f, 0.170f, 0.250f, 0f, -5f, 2854.4f, 107.5f, 0f, 0x0} ) ); +Caravan = new Environment ( new float[]{ 26, 8.3f, 1f, -1000, -2100, -1800, 0.43f, 1.50f, 1f, 0, 0.012f, 0f, 0f, 0f, 600, 0.012f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x1f} ) ); +Dustyroom = new Environment ( new float[]{ 26, 1.8f, 0.560f, -1000, -200, -300, 1.79f, 0.38f, 0.21f, -600, 0.002f, 0f, 0f, 0f, 200, 0.006f, 0f, 0f, 0f, 0.202f, 0.050f, 0.250f, 0f, -10f, 13046f, 163.3f, 0f, 0x20} ) ); +Chapel = new Environment ( new float[]{ 26, 19.6f, 0.840f, -1000, -500, 0, 4.62f, 0.64f, 1.23f, -700, 0.032f, 0f, 0f, 0f, -200, 0.049f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0.110f, -5f, 5000f, 250f, 0f, 0x3f} ) );+ +
Auditorium = new Environment ( new float[]{ 6, 21.6f, 1f, -1000, -476, 0, 4.32f, 0.59f, 1f, -789, 0.020f, 0f, 0f, 0f, -289, 0.030f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Concerthall = new Environment ( new float[]{ 7, 19.6f, 1f, -1000, -500, 0, 3.92f, 0.70f, 1f, -1230, 0.020f, 0f, 0f, 0f, -02, 0.029f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Cave = new Environment ( new float[]{ 8, 14.6f, 1f, -1000, 0, 0, 2.91f, 1.30f, 1f, -602, 0.015f, 0f, 0f, 0f, -302, 0.022f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x1f} ) ); +Arena = new Environment ( new float[]{ 9, 36.2f, 1f, -1000, -698, 0, 7.24f, 0.33f, 1f, -1166, 0.020f, 0f, 0f, 0f, 16, 0.030f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Hangar = new Environment ( new float[]{ 10, 50.3f, 1f, -1000, -1000, 0, 10.05f, 0.23f, 1f, -602, 0.020f, 0f, 0f, 0f, 198, 0.030f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +DomeTomb = new Environment ( new float[]{ 26, 51.8f, 0.790f, -1000, -900, -1300, 4.18f, 0.21f, 0.10f, -825, 0.030f, 0f, 0f, 0f, 450, 0.022f, 0f, 0f, 0f, 0.177f, 0.190f, 0.250f, 0f, -5f, 2854.4f, 20f, 0f, 0x0} ) ); +DomeSaintPauls = new Environment ( new float[]{ 26, 50.3f, 0.870f, -1000, -900, -1300, 10.48f, 0.19f, 0.10f, -1500, 0.090f, 0f, 0f, 0f, 200, 0.042f, 0f, 0f, 0f, 0.250f, 0.120f, 0.250f, 0f, -5f, 2854.4f, 20f, 0f, 0x3f} ) );+ +
Carpettedhallway = new Environment ( new float[]{ 11, 1.9f, 1f, -1000, -4000, 0, 0.30f, 0.10f, 1f, -1831, 0.002f, 0f, 0f, 0f, -1630, 0.030f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Hallway = new Environment ( new float[]{ 12, 1.8f, 1f, -1000, -300, 0, 1.49f, 0.59f, 1f, -1219, 0.007f, 0f, 0f, 0f, 441, 0.011f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Stonecorridor = new Environment ( new float[]{ 13, 13.5f, 1f, -1000, -237, 0, 2.70f, 0.79f, 1f, -1214, 0.013f, 0f, 0f, 0f, 395, 0.020f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Alley = new Environment ( new float[]{ 14, 7.5f, 0.300f, -1000, -270, 0, 1.49f, 0.86f, 1f, -1204, 0.007f, 0f, 0f, 0f, -4, 0.011f, 0f, 0f, 0f, 0.125f, 0.950f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) );+ +
Backyard = new Environment ( new float[]{ 26, 80.3f, 0.450f, -1000, -1200, -600, 1.12f, 0.34f, 0.46f, -700, 0.069f, 0f, 0f, -0f, -300, 0.023f, 0f, 0f, 0f, 0.218f, 0.340f, 0.250f, 0f, -5f, 4399.1f, 242.9f, 0f, 0x0} ) ); +Plain = new Environment ( new float[]{ 19, 42.5f, 0.210f, -1000, -2000, 0, 1.49f, 0.50f, 1f, -2466, 0.179f, 0f, 0f, 0f, -1926, 0.100f, 0f, 0f, 0f, 0.250f, 1f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Rollingplains = new Environment ( new float[]{ 26, 80.3f, 0f, -1000, -3900, -400, 2.13f, 0.21f, 0.46f, -1500, 0.300f, 0f, 0f, -0f, -700, 0.019f, 0f, 0f, 0f, 0.250f, 1f, 0.250f, 0f, -5f, 4399.1f, 242.9f, 0f, 0x0} ) ); +Deepcanyon = new Environment ( new float[]{ 26, 80.3f, 0.740f, -1000, -1500, -400, 3.89f, 0.21f, 0.46f, -1000, 0.223f, 0f, 0f, -0f, -900, 0.019f, 0f, 0f, 0f, 0.250f, 1f, 0.250f, 0f, -5f, 4399.1f, 242.9f, 0f, 0x0} ) ); +Creek = new Environment ( new float[]{ 26, 80.3f, 0.350f, -1000, -1500, -600, 2.13f, 0.21f, 0.46f, -800, 0.115f, 0f, 0f, -0f, -1400, 0.031f, 0f, 0f, 0f, 0.218f, 0.340f, 0.250f, 0f, -5f, 4399.1f, 242.9f, 0f, 0x0} ) ); +Valley = new Environment ( new float[]{ 26, 80.3f, 0.280f, -1000, -3100, -1600, 2.88f, 0.26f, 0.35f, -1700, 0.263f, 0f, 0f, -0f, -800, 0.100f, 0f, 0f, 0f, 0.250f, 0.340f, 0.250f, 0f, -5f, 2854.4f, 107.5f, 0f, 0x0} ) ); +Forest = new Environment ( new float[]{ 15, 38f, 0.300f, -1000, -3300, 0, 1.49f, 0.54f, 1f, -2560, 0.162f, 0f, 0f, 0f, -229, 0.088f, 0f, 0f, 0f, 0.125f, 1f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Mountains = new Environment ( new float[]{ 17, 100f, 0.270f, -1000, -2500, 0, 1.49f, 0.21f, 1f, -2780, 0.300f, 0f, 0f, 0f, -1434, 0.100f, 0f, 0f, 0f, 0.250f, 1f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x1f} ) ); +Quarry = new Environment ( new float[]{ 18, 17.5f, 1f, -1000, -1000, 0, 1.49f, 0.83f, 1f, -10000, 0.061f, 0f, 0f, 0f, 500, 0.025f, 0f, 0f, 0f, 0.125f, 0.700f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Parkinglot = new Environment ( new float[]{ 20, 8.3f, 1f, -1000, 0, 0, 1.65f, 1.50f, 1f, -1363, 0.008f, 0f, 0f, 0f, -1153, 0.012f, 0f, 0f, 0f, 0.250f, 0f, 0.250f, 0f, -5f, 5000f, 250f, 0f, 0x1f} ) );+ +
Underwater = new Environment ( new float[]{ 22, 1.8f, 1f, -1000, -4000, 0, 1.49f, 0.10f, 1f, -449, 0.007f, 0f, 0f, 0f, 1700, 0.011f, 0f, 0f, 0f, 0.250f, 0f, 1.180f, 0.348f, -5f, 5000f, 250f, 0f, 0x3f} ) ); +Smallwaterroom = new Environment ( new float[]{ 26, 36.2f, 0.700f, -1000, -698, 0, 1.51f, 1.25f, 1.14f, -100, 0.020f, 0f, 0f, 0f, 300, 0.030f, 0f, 0f, 0f, 0.179f, 0.150f, 0.895f, 0.190f, -7f, 5000f, 250f, 0f, 0x0} ) );+ +
Bloom is a popular shader effect in 3D games industry. It usually consist in displaying a glowing halo around light sources or bright areas of a scene. -In practice, the bright areas are extracted from the rendered scene, blurred and finally added up to the render.
Those images gives an idea of what bloom does. The left image has no bloom effect, the right image does.
FilterPostProcessor fpp=new FilterPostProcessor(assetManager); + ++ +Bloom and Glow
++ ++ ++ +Bloom is a popular shader effect in 3D games industry. It usually consist in displaying a glowing halo around light sources or bright areas of a scene. +In practice, the bright areas are extracted from the rendered scene, blurred and finally added up to the render. +
+ ++Those images gives an idea of what bloom does. The left image has no bloom effect, the right image does.
+ +
+ + +Bloom Usage
+++
+- +
Create a FilterPostProcessor+- +
Create a BloomFilter+- +
Add the filter to the processor+- +
Add the processor to the viewPort+FilterPostProcessor fpp=new FilterPostProcessor(assetManager); BloomFilter bloom=new BloomFilter(); fpp.addFilter(bloom); - viewPort.addProcessor(fpp);Here are the parameters that you can tweak :
Parameter Method Default Description blur scale setBlurScale(float)
1.5f the scale of the bloom effect, but be careful, high values does artifacts exposure Power setExposurePower(float)
5.0f the glowing channel color is raised to the value power exposure cut-off setExposureCutOff(float)
0.0f the threshold of color to bloom during extraction bloom intensity setBloomIntensity(float)
2.0f the resulting bloom value is multiplied by this intensity You'll probably need to adjust those parameters depending on your scene.
Bloom with a glow map
Sometimes, you want to have more control over what glows and does not glow. -The bloom filter supports a glow map or a glow color.
Creating a glow-map
+ +Let's take the hover tank example bundled with JME3 test data.
Here you can see the diffuse map of the tank, and the associated glow map that only contains the parts of the texture that will glow and their glowing color:
Glow maps works with Lighting.j3md, Particles.j3md and SolidColor.j3md material definitions. -The tank material looks like that :
Material My Material : Common/MatDefs/Light/Lighting.j3md { + viewPort.addProcessor(fpp);+ ++Here are the parameters that you can tweak : +
++ ++
+ +Parameter Method Default Description ++ +blur scale setBlurScale(float)
1.5f the scale of the bloom effect, but be careful, high values does artifacts ++ +exposure Power setExposurePower(float)
5.0f the glowing channel color is raised to the value power ++ +exposure cut-off setExposureCutOff(float)
0.0f the threshold of color to bloom during extraction ++ +bloom intensity setBloomIntensity(float)
2.0f the resulting bloom value is multiplied by this intensity ++ +You'll probably need to adjust those parameters depending on your scene. +
+ +Bloom with a glow map
++ ++ ++ +Sometimes, you want to have more control over what glows and does not glow. +The bloom filter supports a glow map or a glow color. +
+ +Creating a glow-map
++ ++ +Let's take the hover tank example bundled with JME3 test data.
+ +
+ +Here you can see the diffuse map of the tank, and the associated glow map that only contains the parts of the texture that will glow and their glowing color:
+ + + ++Glow maps works with Lighting.j3md, Particles.j3md and SolidColor.j3md material definitions. +The tank material looks like that : + +
+Material My Material : Common/MatDefs/Light/Lighting.j3md { MaterialParameters { SpecularMap : Models/HoverTank/tank_specular.png Shininess : 8 @@ -67,77 +102,183 @@ The tank material looks like that :Material My Material : Common/MatDef Diffuse : 1.0 1.0 1.0 1.0 Specular : 1.0 1.0 1.0 1.0 } -}The glow map is defined here : GlowMap : Models/HoverTank/tank_glow_map_highres.png
Usage
+ +
Create a FilterPostProcessor Create a BloomFilter with the GlowMode.Objects parameter Add the filter to the processor Add the processor to the viewPortFilterPostProcessor fpp=new FilterPostProcessor(assetManager); +}+ ++ +The glow map is defined here : GlowMap : Models/HoverTank/tank_glow_map_highres.png +
+ +Usage
+++
+- +
Create a FilterPostProcessor+- +
Create a BloomFilter with the GlowMode.Objects parameter+- +
Add the filter to the processor+- +
Add the processor to the viewPort+FilterPostProcessor fpp=new FilterPostProcessor(assetManager); BloomFilter bf=new BloomFilter(BloomFilter.GlowMode.Objects); fpp.addFilter(bf); - viewPort.addProcessor(fpp);Bloom with a glow color
Sometimes you need an entire object to glow, not just parts of it. -In this case you'll need to use the glow color parameter.
Usage
+ +
Create a material for your object and set the GlowColor parameter Create a FilterPostProcessor Create a BloomFilter with the GlowMode.Objects parameter Add the filter to the processor Add the processor to the viewPortMaterial mat = new Material(getAssetManager(), "Common/MatDefs/Misc/SolidColor.j3md"); + viewPort.addProcessor(fpp);+ ++Here is the result :
+ +
+ + +Bloom with a glow color
++ ++ ++ +Sometimes you need an entire object to glow, not just parts of it. +In this case you'll need to use the glow color parameter. +
+ +Usage
+++
+- +
Create a material for your object and set the GlowColor parameter+- +
Create a FilterPostProcessor+- +
Create a BloomFilter with the GlowMode.Objects parameter+- +
Add the filter to the processor+- +
Add the processor to the viewPort+Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/SolidColor.j3md"); mat.setColor("Color", ColorRGBA.Green); mat.setColor("GlowColor", ColorRGBA.Green); fpp=new FilterPostProcessor(assetManager); - bloom= new BloomFilter(BloomFilter.GlowMode.Objects); + bloom= new BloomFilter(BloomFilter.GlowMode.Objects); fpp.addFilter(bloom); - viewPort.addProcessor(fpp);Here is the result on Oto's plasma ball (before and after) :
Hints and tricks
Increasing the blur range and reducing fps cost
The glow render is sampled on a texture that has the same dimensions as the viewport. -You can reduce the size of the bloom sampling just by using the setDownSamplingFactor method like this :
BloomFilter bloom=new BloomFilter(); - bloom.setDownSamplingFactor(2.0f);In this example the sampling size is divided by 4 (width/2,height/2), resulting in less work to blur the scene. -The resulting texture is then up sampled to the screen size using hardware bilinear filtering. this results in a wider blur range.
Using classic bloom combined with a glow map
let's say you want a global bloom on your scene, but you have also a glowing object on it. -You can use only one bloom filter for both effects like that
BloomFilter bloom=new BloomFilter(BloomFilter.GlowMode.SceneAndObjects);However, note that both effects will share the same values of attribute, and sometimes, it won't be what you need.
Making your home brewed material definition support Glow
+ +Let's say you have made a custom material on your own, and that you want it to support glow maps and glow color. -In your material definition you need to add those lines in the MaterialParameters section :
MaterialParameters { + viewPort.addProcessor(fpp);+ ++Here is the result on Oto's plasma ball (before and after) :
+ +
+ + + +Hints and tricks
++ ++ +Increasing the blur range and reducing fps cost
++ ++ ++ +The glow render is sampled on a texture that has the same dimensions as the viewport. +You can reduce the size of the bloom sampling just by using the setDownSamplingFactor method like this :
+
+ + +BloomFilter bloom=new BloomFilter(); + bloom.setDownSamplingFactor(2.0f);+ ++ +In this example the sampling size is divided by 4 (width/2,height/2), resulting in less work to blur the scene. +The resulting texture is then up sampled to the screen size using hardware bilinear filtering. this results in a wider blur range. +
+ +Using classic bloom combined with a glow map
++ ++ ++let's say you want a global bloom on your scene, but you have also a glowing object on it. +You can use only one bloom filter for both effects like that +
+BloomFilter bloom=new BloomFilter(BloomFilter.GlowMode.SceneAndObjects);+ ++However, note that both effects will share the same values of attribute, and sometimes, it won't be what you need. +
+ +Making your home brewed material definition support Glow
++ ++ +Let's say you have made a custom material on your own, and that you want it to support glow maps and glow color. +In your material definition you need to add those lines in the MaterialParameters section : +
+MaterialParameters { + .... + // Texture of the glowing parts of the material Texture2D GlowMap // The glow color of the object Color GlowColor - }Then add the following technique :
Technique Glow { + }+ ++Then add the following technique : + +
+Technique Glow { + LightMode SinglePass + VertexShader GLSL100: Common/MatDefs/Misc/SimpleTextured.vert FragmentShader GLSL100: Common/MatDefs/Light/Glow.frag + WorldParameters { WorldViewProjectionMatrix } + Defines { HAS_GLOWMAP : GlowMap HAS_GLOWCOLOR : GlowColor } - }Then you can use this material with the BloomFilter
Make a glowing object stop to glow
- \ No newline at end of file + }If you are using a glow map, remove the texture from the material.
material.clearTextureParam("GlowMap");If you are using a glow color, set it to black
material.setColor("GlowColor",ColorRGBA.Black);
+Then you can use this material with the BloomFilter +
+ ++ +If you are using a glow map, remove the texture from the material. + +
+material.clearTextureParam("GlowMap");+ +
+If you are using a glow color, set it to black + +
+material.setColor("GlowColor",ColorRGBA.Black);+ +
+Since bullet is not (yet) multithreaded or GPU accelerated the jME3 implementation allows to run each physics space on a separate thread that is executed in parallel to rendering. + +
+ ++A SimpleApplication with a BulletAppState allows setting the threading type via +
+setThreadingType(ThreadingType type);+ +
+ where ThreadingType can be either SEQUENTIAL or PARALLEL. +
+ ++In the simpleInitApp() method: + +
+bulletAppState = new BulletAppState(); +bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL); +stateManager.attach(bulletAppState);+ +
+The physics update happens in parallel to rendering, after the users changes have been made in the update() call. This way the loop logic is still maintained: the user can set and change values in physics and scenegraph objects before render() and physicsUpdate() are called in parallel. More physics spaces can simply be added by using multiple bulletAppStates. + +
+ + The default com.jme3.renderer.Camera object is cam
in com.jme3.app.Application.
The camera object is created with the following defaults:
Method | Usage |
---|---|
cam.getLocation(), setLocation() | The camera position |
cam.getRotation(), setRotation() | The camera rotation |
cam.getLeft(), setLeft() | The left axis of the camera |
cam.getUp(), setUp() | The up axis of the camera, usually Vector3f(0,1,0) |
cam.getDirection(), setDirection() | The vector the camera is facing |
cam.getAxes(), setAxes(left,up,dir) | One accessor for the three properties left/up/direction. |
cam.getFrame(), setFrame(loc,left,up,dir) | One accessor for the four properties location/left/up/direction. |
cam.resize(width, height, fixAspect) | Resize an existing camera object while keeping all other settings. Set fixAspect to true to adjust the aspect ratio (?) |
cam.setFrustum( near, far, left, right, top, bottom ) | The frustrum is defined by the near/far plane, left/rught plane, top/bottom plane (all distances as float values) |
cam.setFrustumPerspective( fovY, aspect ratio, near, far) | The frustrum is defined by view angle along the Y axis (in degrees), aspect ratio, and the near/far plane. |
cam.lookAt(target,up) | Turn the camera to look at Coordinate target, and rotate it around the up axis. |
cam.setParallelProjection(false) | Normal perspective |
cam.setParallelProjection(true) | Parallel projection perspective |
cam.getScreenCoordinates() | ? |
Tip: After you change view port, frustrum, or frame, call cam.update();
The flyby camera is an extension of the default camera in com.jme3.app.SimpleApplication. It is preconfigured to respond to the WASD keys for walking forwards and backwards, and for strafing to the sides. Move the mouse to rotate the camera, scroll the mouse wheel for zooming in or out. The QZ keys raise or lower the camera.
Method | Usage |
---|---|
flyCam.setEnabled(true); | Activate the flyby cam |
flyCam.setMoveSpeed(10); | Control the move speed |
flyCam.setRotationSpeed(10); | Control the rotation speed |
flyCam.setDragToRotate(true) | Must keep mouse button pressed to rotate camera. Used e.g. for Applets. if false, all mouse movement will be captured and interpreted as rotations. |
jME3 also supports a Chase Cam that can follow a moving target Spatial (com.jme3.input.ChaseCamera
). Click and hold the mouse button to rotate around the target.
flyCam.setEnabled(false); -ChaseCamera chaseCam = new ChaseCamera(cam, target, inputManager);
Method | Usage |
---|---|
chaseCam.setSmoothMotion(true); | Interpolates a smoother acceleration/deceleration when the camera moves. |
chaseCam.setChasingSensitivity(5f) | The lower the chasing sensitivity, the slower the camera will follow the target when it moves. |
chaseCam.setTrailingSensitivity(0.5f) | The lower the traling sensitivity, the slower the camera will begin to go after the target when it moves. Default is 0.5; |
chaseCam.setRotationSensitivity(5f) | The lower the sensitivity, the slower the camera will rotate around the target when the mosue is dragged. Default is 5. |
chaseCam.setTrailingRotationInertia(0.1f) | This prevents the camera to stop too abruptly when the target stops rotating before the camera has reached the target's trailing position. Default is 0.1f. |
chaseCam.setDefaultDistance(40); | The default distance to the target at the start of the application. |
chaseCam.setMaxDistance(40); | The maximum zoom distance. Default is 40f. |
chaseCam.setMinDistance(1); | The minimum zoom distance. Default is 1f. |
chaseCam.setMinVerticalRotation(-FastMath.PI/2); | The minimal vertical rotation angle of the camera around the target. Default is 0. |
chaseCam.setDefaultVerticalRotation(-FastMath.PI/2); | The default vertical rotation angle of the camera around the target at the start of the application. |
chaseCam.setDefaultHorizontalRotation(-FastMath.PI/2); | The default horizontal rotation angle of the camera around the target at the start of the application. |
+
+The default com.jme3.renderer.Camera object is cam
in com.jme3.app.Application.
+
+The camera object is created with the following defaults: +
+Method | Usage | +
---|---|
cam.getLocation(), setLocation() | The camera position | +
cam.getRotation(), setRotation() | The camera rotation | +
cam.getLeft(), setLeft() | The left axis of the camera | +
cam.getUp(), setUp() | The up axis of the camera, usually Vector3f(0,1,0) | +
cam.getDirection(), setDirection() | The vector the camera is facing | +
cam.getAxes(), setAxes(left,up,dir) | One accessor for the three properties left/up/direction. | +
cam.getFrame(), setFrame(loc,left,up,dir) | One accessor for the four properties location/left/up/direction. | +
cam.resize(width, height, fixAspect) | Resize an existing camera object while keeping all other settings. Set fixAspect to true to adjust the aspect ratio (?) | +
cam.setFrustum( near, far, left, right, top, bottom ) | The frustrum is defined by the near/far plane, left/rught plane, top/bottom plane (all distances as float values) | +
cam.setFrustumPerspective( fovY, aspect ratio, near, far) | The frustrum is defined by view angle along the Y axis (in degrees), aspect ratio, and the near/far plane. | +
cam.lookAt(target,up) | Turn the camera to look at Coordinate target, and rotate it around the up axis. | +
cam.setParallelProjection(false) | Normal perspective | +
cam.setParallelProjection(true) | Parallel projection perspective | +
cam.getScreenCoordinates() | ? | +
+Tip: After you change view port, frustrum, or frame, call cam.update();
+
+ +The flyby camera is an extension of the default camera in com.jme3.app.SimpleApplication. It is preconfigured to respond to the WASD keys for walking forwards and backwards, and for strafing to the sides. Move the mouse to rotate the camera, scroll the mouse wheel for zooming in or out. The QZ keys raise or lower the camera. + +
+Method | Usage | +
---|---|
flyCam.setEnabled(true); | Activate the flyby cam | +
flyCam.setMoveSpeed(10); | Control the move speed | +
flyCam.setRotationSpeed(10); | Control the rotation speed | +
flyCam.setDragToRotate(true) | Must keep mouse button pressed to rotate camera. Used e.g. for Applets. if false, all mouse movement will be captured and interpreted as rotations. | +
+
+jME3 also supports a Chase Cam that can follow a moving target Spatial (com.jme3.input.ChaseCamera
). Click and hold the mouse button to rotate around the target.
+
flyCam.setEnabled(false); +ChaseCamera chaseCam = new ChaseCamera(cam, target, inputManager);+
Method | Usage | +
---|---|
chaseCam.setSmoothMotion(true); | Interpolates a smoother acceleration/deceleration when the camera moves. | +
chaseCam.setChasingSensitivity(5f) | The lower the chasing sensitivity, the slower the camera will follow the target when it moves. | +
chaseCam.setTrailingSensitivity(0.5f) | The lower the traling sensitivity, the slower the camera will begin to go after the target when it moves. Default is 0.5; | +
chaseCam.setRotationSensitivity(5f) | The lower the sensitivity, the slower the camera will rotate around the target when the mosue is dragged. Default is 5. | +
chaseCam.setTrailingRotationInertia(0.1f) | This prevents the camera to stop too abruptly when the target stops rotating before the camera has reached the target's trailing position. Default is 0.1f. | +
chaseCam.setDefaultDistance(40); | The default distance to the target at the start of the application. | +
chaseCam.setMaxDistance(40); | The maximum zoom distance. Default is 40f. | +
chaseCam.setMinDistance(1); | The minimum zoom distance. Default is 1f. | +
chaseCam.setMinVerticalRotation(-FastMath.PI/2); | The minimal vertical rotation angle of the camera around the target. Default is 0. | +
chaseCam.setDefaultVerticalRotation(-FastMath.PI/2); | The default vertical rotation angle of the camera around the target at the start of the application. | +
chaseCam.setDefaultHorizontalRotation(-FastMath.PI/2); | The default horizontal rotation angle of the camera around the target at the start of the application. | +
JME3 cinematics (com.jme.cinematic) allow you to remote control nodes and cameras in a 3D game. You use cinematics to script and record scenes. Use it for example to create cutscenes of your game.
Cinematics are implemented as AppStates. Attach the scene that you want to be visible in the cinematic to one Node. You create a Cinematic object, and add individual CinematicEvents to it.
Cinematic cinematic = new Cinematic(sceneNode, duration); -cinematic.addCinematicEvent(triggerTime, cinematicEvent);
cinematic.pause()
and cinematic.play();
There are several kinds of cinematic events:
CinematicEvent | Description |
---|---|
MotionTrack | Use this to move a Spatial non-linearly over time. A motionPath is a list of several waypoints added to a MotionPath. The path is interpolated using Catmull-Rom Splines between waypoints. |
PositionTrack | Use this to move a Spatial linearly over time. It translates the Spatial to a destination in the given amount of time by linearly interpolating the positions. |
RotationTrack | Use this to change the rotation of a Spatial over time. It rotates the Spatial in the given amount of time by linearly interpolating the rotation. |
ScaleTrack | Use this to change the size of a Spatial over time. It scales the Spatial in the given amount of time by linearly interpolating the scale. |
SoundTrack | Use this to play a sound at a given time for the given duration. |
GuiTrack | Displays a Nifty GUI at a given time for the given duration. Use it to display subtitles or HUD elements. Bind the Nifty GUI XML to the cinematic using cinematic.bindUi("path/to/nifty/file.xml"); |
AnimationTrack | Use this to start playing a model animation at a given time (a character walking animation for example) |
We will add more types of track implementions over time.
Each CinematicEvent supports the following methods to control the event.
CinematicEvent method | Usage |
---|---|
play() | Starts playing the cinematic. |
stop() | Stops playing the cinematic. |
pause() | Pauses the cinematic. |
Those methods, must be called on the Cinematic and are propagated to the events. Don't use them directly on a sub cinematic event
A motion track is made up of MotionPaths.
MotionPath path = new MotionPath();
MotionPath Method | Usage |
---|---|
setCycle(true) | Sets whether the motion along this path should be closed (true) or not (false). |
addWayPoint(vector) | Adds individual waypoints to this path. The order is relevant. |
removeWayPoint(vector) removeWayPoint(index) | Removes individual waypoints from this path. You can specify a vector or the integer index. |
setCurveTension(0.83f) | Sets the tension of the curve (only for Catmull Rom Spline). A value of 0.0f will give a straight linear line, 1.0 a round curve. |
enableDebugShape(assetManager,rootNode) | Shows a line to visualize the path. Used for debugging. |
disableDebugShape() | Hides the line that visualizes the path. Used for debugging. |
getNbWayPoints() | Returns the number of waypoints in this path. |
MotionTrack thingMotionControl = new MotionTrack(thingNode, path);
MotionTrack method | Usage |
---|---|
setLoopMode(LoopMode.Loop) | Sets whether the animation along this path should loop (true) or play only once (false). |
setDirectionType(MotionTrack.Direction.None) | Sets the direction behavior type of the controled node. Direction.None deactivates this feature. See the following options: |
setDirectionType(MotionTrack.Direction.LookAt) | Rotate to keep facing a point. Specify the point with setLookAt(). |
setDirectionType(MotionTrack.Direction.Path) | Face the direction of the path. |
setDirectionType(MotionTrack.Direction.PathAndRotation) | Face the direction of the path, plus an added rotation. Use together with the setRotation() method. |
setDirectionType(MotionTrack.Direction.Rotation) | Rotate while moving. Use together with the setRotation() method. |
setLookAt(teapot.getWorldTranslation(), Vector3f.UNIT_Y) | Optional: Make the moving face towards a certain location. Use together with setDirectionType(). |
setRotation(quaternion) | Optional: Sets the rotation. Use together with MotionTrack.Direction.Rotation or MotionTrack.Direction.PathAndRotation. |
You can register a MotionPathListener to the MotionPath to track whether waypoints have been reached, and then trigger a custom action. In this example we just print the status. The onWayPointReach() method of the interface gives you access to the MotionTrack object control
, and an integer value representing the current wayPointIndex.
path.addListener( new MotionPathListener() { - public void onWayPointReach(MotionTrack control, int wayPointIndex) { - if (path.getNbWayPoints() == wayPointIndex + 1) { - println(control.getSpatial().getName() + "Finished!!! "); - } else { - println(control.getSpatial().getName() + " Reached way point " + wayPointIndex); - } + ++ +JME3 Cinematics
++ ++ ++ +JME3 cinematics (com.jme.cinematic) allow you to remote control nodes and cameras in a 3D game: You can script and and play cinematic scenes. Combined with screen recording software, you use cinematics to create and movies/trailers of your game. Internally, Cinematics are implemented as AppStates. +
+ ++Short overview of the cinematic process: +
++
+ +- +
Plan the script of your movie.+
+Write down a timeline (e.g. on paper) of which character should be at which spot at which time.- +
Attach the scene objects that you want to remote-control to one Node.+
+This Node can be the rootNode, or a Node that is attached to the rootNode.- +
Create a Cinematic object for this movie scene. The Cinematic will contain and manage the movie script.+- +
For each line in your script (for each frame in your timeline), add a CinematicEvent to the Cinematic.+Sample Code
+++ ++
+ +- +
+How to Use a Cinematic
++ ++ ++ +A Cinematic is like a movie script for a node. +
+Cinematic cinematic = new Cinematic(sceneNode, duration); +cinematic.addCinematicEvent(starttime1, track1); +cinematic.addCinematicEvent(starttime2, track2); +cinematic.addCinematicEvent(starttime2, track3); +... +stateManager.attach(cinematic);++
+- +
Create one Cinematic per scripted scene.++
+- +
+sceneNode
is the node containing the scene (can be the rootNode).- +
+duration
is the duration of the whole scene in seconds.- +
Each Cinematic is a set of CinematicEvents, that are triggered at a given moment on the timeline.+- +
Create one CinematicEvent for each line of your movie script.++
+- +
+track
is one motion of a moving object. You can add several tracks. More details below.- +
+starttime
is the time when this particular cinematic event starts on the timeline. Specify the start time in seconds since the beginning of the cinematic.- +
Attach the Cinematic to the SimpleApplication's stateManager.+- +
Play, stop and pause the Cinematic from your code.++ ++
+ +Method Usage ++ +cinematic.play() Starts playing the cinematic from the start, or from where it was paused. ++ +cinematic.stop() Stops playing the cinematic and rewinds it. ++ +cinematic.pause() Pauses the cinematic. +Tracks (CinematicEvents)
++ ++ ++ +Just like a movie script consists of lines with instructions to the actors, each Cinematic consists of a series of tracks. +
+ ++Here is the list of available CinematicEvents that you use as tracks. Each track remote-controls scene objects in a different way: + +
++ ++
+ +Tracks (CinematicEvents) Description ++ +MotionTrack Use a MotionTrack to move a Spatial non-linearly over time. A MotionTrack is based on a list of waypoints in a MotionPath. The curve goes through each waypoint, and you can adjust the tension of the curve to modify the roundedness of the path. This is the motion interpolation you are going to use in most cases. ++ +PositionTrack Use a PositionTrack to move a Spatial linearly over time. This linear interpolation results in straight motion segments between the way points. Use this to make the remote-controlled objects zig-zag from one way point to the other in a straight line. ++ +RotationTrack Use a RotationTrack to change the rotation of a Spatial over time. It spins the Spatial to the given angle in the given amount of time by linearly interpolating the rotation. ++ +ScaleTrack Use a ScaleTrack to change the size of a Spatial over time. It resizes the Spatial in the given amount of time by linearly interpolating the scale. ++ +SoundTrack Use a SoundTrack to play a sound at a given time for the given duration. ++ +GuiTrack Displays a Nifty GUI at a given time for the given duration. Use it to display subtitles or HUD elements. Bind the Nifty GUI XML to the cinematic using +cinematic.bindUi("path/to/nifty/file.xml");
+ +AnimationTrack Use this to start playing a model animation at a given time (a character walking animation for example) ++ +The jMonkey team can add more types of tracks, just ask in the forum. +
+ +MotionTrack
++ ++ ++ +A MotionTrack moves a Spatial along a complex path. + +
+MotionTrack track = new MotionTrack(thingNode, path);+ ++Details of the constructor: +
++
+ +- +
+thingNode
is the Spatial to be moved.- +
+path
is a complex MotionPath.+ +To create a MotionTrack, do the following: +
++
+- +
+- +
Create a MotionTrack based on the MotionPath.+- +
Configure your MotionTrack (see below).+- +
Add the MotionTrack to a Cinematic.++ ++
+ +MotionTrack configuration method Usage ++ +track.setLoopMode(LoopMode.Loop) Sets whether the animation along this path should loop (LoopMode.Loop) or play only once (LoopMode.DontLoop). ++ +track.setDirectionType(MotionTrack.Direction.None) Sets the direction behavior type of the controled node. Direction.None deactivates this feature. You can choose from the following options: LookAt, Path, PathAndRotation, Rotation. ++ +track.setDirectionType(MotionTrack.Direction.LookAt) The spatial turns (rotates) to keep facing a certain point while moving. Specify the point with the +setLookAt()
method.+ +track.setDirectionType(MotionTrack.Direction.Path) The spatial always faces in the direction of the path while moving. ++ +track.setDirectionType(MotionTrack.Direction.PathAndRotation) The spatial faces the direction of the path, plus an added rotation. Use together with the +setRotation()
method.+ +track.setDirectionType(MotionTrack.Direction.Rotation) The spatial spins (rotates) while moving. You describe the spin by a custom quaternion. Use together with the +setRotation()
method.+ +track.setLookAt(teapot.getWorldTranslation(), Vector3f.UNIT_Y) The spatial always faces towards this location. Use together with +MotionTrack.Direction.LookAt
.+ +track.setRotation(quaternion) Sets the rotation. Use together with +MotionTrack.Direction.Rotation
orMotionTrack.Direction.PathAndRotation
.+ +Tip: Most likely you remote-control more than one object in your scene. Give the tracks and paths useful names such as
+ +dragon_track
,dragon_path
,hero_track
,hero_path
, etc. +PositionTrack
++ ++ ++ +A PositionTrack moves a Spatial in a straight line from its current position to the end position. + +
+PositionTrack track = new PositionTrack( + thingNode, endPosition, duration, loopMode);+ ++Details of the constructor: +
++
+ +- +
+thingNode
is the Spatial to be moved.- +
+endPosition
is the target location as Vector3f.- +
+duration
is the time that it should take from start to end point.- +
+loopMode
can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.+ +The start location is always the current location of the Spatial. +
+ +RotationTrack
++ ++ ++ +A RotationTrack remote-controls the rotation of a spatial. + +
+RotationTrack thingRotationControl = new RotationTrack( + thingNode, endRotation, duration, loopMode);+ ++Details of the constructor: +
++
+ +- +
+thingNode
is the Spatial to be rotated.- +
+endRotation
is the target rotation in Quaternion format.- +
+duration
is the time that it should take from start to target rotation.- +
+loopMode
can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.ScaleTrack
++ ++ ++ +A ScaleTrack remote-controls whether a spatial grows or shrinks. +
+ScaleTrack thingScaleControl = new ScaleTrack( + thingNode, endScale, duration, loopMode);+ ++Details of the constructor: +
++
+ +- +
+thingNode
is the Spatial to be resized.- +
+endScale
is the target Scale in Vector3f format.- +
+duration
is the time that it should take from start to target scale.- +
+loopMode
can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.SoundTrack
++ ++ ++ +A SoundTrack plays a sound as part of the cinematic. + +
+SoundTrack( audioPath, isStream, duration, loopMode )+ ++ +Details of the constructor: +
++
+ +- +
+audioPath
is the path to an audio file as String, e.g. "Sounds/mySound.wav".- +
+isStream
toggles between streaming and buffering. Set to true to stream long audio file, set to false to play short buffered sounds.- +
+duration
is the time that it should take to play.- +
+loopMode
can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.GuiTrack
++ ++ ++ +A GuiTrack shows or hide a NiftyGUI as part of a cinematic. + +
+GuiTrack( screen, duration, loopMode )+ ++ +You must use this together with bindUI() to specify the Nifty GUI XML file that you want to load: + +
+cinematic.bindUi("Interface/subtitle.xml");+ ++Details of the constructor: +
++
+ +- +
+screen
is the name of the Nifty GUI screen to load, as String.- +
+duration
is the time that it should take to play.- +
+loopMode
can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.AnimationTrack
++ ++ ++ +An AnimationTrack triggers an animation as part of a cinematic. + +
+AnimationTrack( thingNode, animationName, duration, loopMode )+ ++ +Details of the constructor: +
++
+ +- +
+thingNode
is the Spatial whose animation you want to play.- +
+animationName
the name of the animation stored in the animated model that you want to trigger, as a String.- +
+duration
is the time that it should take to play.- +
+loopMode
can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.Customizations
++ ++ ++ +You can extend individual CinematicEvents. The shows how to extend a GuiTrack to script subtitles. See how the subtitles are used in the . +
+ ++You can also create new CinematicEvent by extending . An AbstractCinematicEvent implements the CinematicEvent interface and provides duration, time, speed, etc… management. Look at the is to use this for a custom fadeIn/fadeOut effect in combination with a com.jme3.post.filters.FadeFilter. +
+ +Interacting with Cinematics
++ ++ +CinematicEventListener
++CinematicEventListener cel = new CinematicEventListener() { + public void onPlay(CinematicEvent cinematic) { + chaseCam.setEnabled(false); + System.out.println("play"); } -});PositionTrack
PositionTrack thingPositionControl = new PositionTrack( - thingNode, endPosition, duration, loopMode);Details of the constructor:
thingNode is the Spatial to be moved. endPosition is the target location as Vector3f. duration is the time that it should take from start to end point. loopMode can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.The start location is always the current location of the Spatial.
RotationTrack
RotationTrack thingRotationControl = new RotationTrack( - thingNode, endRotation, duration, loopMode);Details of the constructor:
thingNode is the Spatial to be rotated. endRotation is the target rotation in Quaternion format. duration is the time that it should take from start to target rotation. loopMode can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.ScaleTrack
ScaleTrack thingScaleControl = new ScaleTrack( - thingNode, endScale, duration, loopMode);Details of the constructor:
thingNode is the Spatial to be resized. endScale is the target Scale in Vector3f format. duration is the time that it should take from start to target scale. loopMode can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.SoundTrack
SoundTrack( audioPath, isStream, duration, loopMode )Details of the constructor:
audioPath is the path to an audio file as String, e.g. "Sounds/mySound.wav". isStream Set this to true to play a longer audio file as stream, or to false to play a short sound without streaming. duration is the time that it should take to play. loopMode can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.GuiTrack
GuiTrack( screen, duration, loopMode )You must use this together with bindUI() to specify the Nifty GUI XML file that you want to load:
cinematic.bindUi("Interface/subtitle.xml");Details of the constructor:
screen is the name of the Nifty GUI screen to load, as String. duration is the time that it should take to play. loopMode can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.AnimationTrack
AnimationTrack( thingNode, animationName, duration, loopMode )Details of the constructor:
thingNode is the Spatial whose animation you want to play. AnimationName the animation of the animated model that you want to trigger, as a String. duration is the time that it should take to play. loopMode can be LoopMode.Loop, LoopMode.DontLoop, LoopMode.Cycle.Customizations
You can extend individual CinematicEvents. The SubtitleTrack.java example shows how to extend a GuiTrack to script subtitles. See how the subtitles are used in the TestCinematic.java example.
You can also create new CinematicEvent by extending AbstractCinematicEvent. An AbstractCinematicEvent implements the CinematicEvent interface and provides duration, time, speed, etc… management. Look at the TestCinematic.java example is to use this for a custom fadeIn/fadeOut effect in combination with a com.jme3.post.filters.FadeFilter.
Camera Handling
The camera management is handled as follows:
Create a camera Node and bind the camera object to the cinematic. Note that we also give the camera node a name in this step.CameraNode camNode = cinematic.bindCamera("topView", cam); Position the camera node in its start location. Use activateCamera() to give the control of the camera to this node. You now see the scene from this camera's point of view. For example to see through the camera node named "top view", 6 seconds after the start of the cinematic, you'd writecinematic.activateCamera(6, "topView"); If desired, attach the camNode to a MotionTrack to let it travel along waypoints. This is demonstrated in the TestCameraMotionPath.java example.Code samples:
flyCam.setEnabled(false); -CameraNode camNodeTop = cinematic.bindCamera("topView", cam); -camNodeTop.setControlDir(ControlDirection.SpatialToCamera); -camNodeTop.getControl(0).setEnabled(false); + public void onPause(CinematicEvent cinematic) { + chaseCam.setEnabled(true); + System.out.println("pause"); + } -CameraNode camNodeSide = cinematic.bindCamera("sideView", cam); -camNodeSide.setControlDir(ControlDirection.CameraToSpatial); -camNodeSide.getControl(0).setEnabled(false);Physics Interaction
Upcoming.
More Information
- \ No newline at end of file + public void onStop(CinematicEvent cinematic) { + chaseCam.setEnabled(true); + System.out.println("stop"); + } +} +cinematic.addListener(cel);See also: Cinematics by Nehon
+ +Upcoming. +
+ ++See also: + +
+ +The term collision can be used to refer to physical interactions (where physical objects collide and bump off one another), and also to non-physical intersections. This article is about non-physical (mathematical) collisions. -Non-physical collisions are interesting because they take less resources than physical ones. You cn optimize your game if you find a way to simulate a certain effect in a non-physical way, using mathematical techniques such as ray casting. -One example for an optimization is a physical vehicle's wheels. You could make the wheels fully physical disks, and have jME calculate every tiny force – sounds very accurate, but is total overkill. An more performant solution is to cast four rays down from the vehicle and calculate the intersections with the floor and other obstacles. These non-physical wheels require (in the simplest case) only four calculations to achieve an effect that players can hardly distinguish from the real thing.
A com.jme3.bounding.BoundingVolume is an interface for dealing with containment of a collection of points. All BoundingVolumes are Collidable and are used as optimization to calculate non-physical collisions more quickly: It's faster to calculate an intersection between simple shapes like spheres and boxes than between complex shapes. In cases where precision is not relevant, you wrap a complex model in a simpler shape to improve collision detection performance.
Note: Physical objects have their own "bounding volumes" called CollisionShapes.
The interface com.jme3.collision.Collidable declares one method that returns how many collisions were found between two Collidables: collideWith(Collidable other, CollisionResults results)
.
-A com.jme3.collision.CollisionResults
object is an ArrayList of comparable com.jme3.collision.CollisionResult
objects.
-You can iterate over the CollisionResults to identify the other parties involved in the collision. Note that jME counts all collisions, this means a ray intersecting a box will be counted as two hits, one on the front where the ray enters, and one on the back where the ray exits.
CollisionResults Method | Usage |
---|---|
size() | Returns the number of CollisionResult objects. |
getClosestCollision() | Returns the CollisionResult with the lowest distance. |
getFarthestCollision() | Returns the CollisionResult with the farthest distance. |
getCollision(i) | Returns the CollisionResult at index i. |
A CollisionResult object contains information about the second party of the collision event.
CollisionResult Method | Usage |
---|---|
getContactPoint() | Returns the contact point coordinate on the second party, as Vector3f. |
getContactNormal() | Returns the Normal vector at the contact point, as Vector3f. |
getDistance() | Returns the distance between the Collidable and the second party, as float. |
getGeometry() | Returns the Geometry of the second party. |
getTriangle(t) | Binds t to the triangle t on the second party's mesh that was hit. |
getTriangleIndex() | Returns the index of the triangle on the second party's mesh that was hit. (?) |
Assume you have two collidables a and b and want to detect collisions between them. The collision parties can be Geometries, Nodes with Geometries attached (including the rootNode), Planes, Quads, Lines, or Rays.
The following code snippet can be triggered by listeners (e.g. after an input action such as a click), or timed in the update loop.
// Calculate Results + ++ +Collision and Intersection
++ ++ ++ +The term collision can be used to refer to physical interactions (where physical objects collide and bump off one another), and also to non-physical intersections. This article is about non-physical (mathematical) collisions. +
+ ++Non-physical collision detection is interesting because they use less computing resources than physical collision detection. You can optimize your game if you find a way to simulate a certain effect in a non-physical way, using mathematical techniques such as ray casting. +
+ ++One example for an optimization is a physical vehicle's wheels. You could make the wheels fully physical disks, and have jME calculate every tiny force – sounds very accurate, but is total overkill. A more performant solution is to cast four invisible rays down from the vehicle and calculate the intersections with the floor and other obstacles. These non-physical wheels require (in the simplest case) only four calculations to achieve an effect that players can hardly distinguish from the real thing. +
+ +Bounding Volumes
++ ++ ++ +A com.jme3.bounding.BoundingVolume is an interface for dealing with containment of a collection of points. All BoundingVolumes are Collidable and are used as optimization to calculate non-physical collisions more quickly: It's faster to calculate an intersection between simple shapes like spheres and boxes than between complex shapes. In cases where precision is not relevant, you wrap a complex model in a simpler shape to speed up collision detection. +
++
+ +- +
Type.Sphere: com.jme3.bounding.BoundingSphere is a sphere used as a container for a group of vertices of a piece of geometry. A BoundingSphere has a center and a radius.+- +
Type.AABB = Axis-aligned bounding box. A com.jme3.bounding.BoundingBox is an axis-aligned cuboid used as a container for a group of vertices of a piece of geometry. A BoundingBox has a center and extents from that center along the x, y and z axis.+- +
Type.OBB = Oriented bounding box. (not in use)+- +
Type.Capsule = Cylinder with rounded ends. Often used for mobile characters.++ +Note: Physical objects have their own "bounding volumes" called CollisionShapes. +
+ +Collisions
++ ++ ++ +The interface com.jme3.collision.Collidable declares one method that returns how many collisions were found between two Collidables:
+collideWith(Collidable other, CollisionResults results)
. ++
+- +
A+com.jme3.collision.CollisionResults
object is an ArrayList of comparablecom.jme3.collision.CollisionResult
objects.- +
You can iterate over the CollisionResults to identify the other parties involved in the collision.+
+Note that jME counts all collisions, this means a ray intersecting a box will be counted as two hits, one on the front where the ray enters, and one on the back where the ray exits.+ ++
+ +CollisionResults Method Usage ++ +size() Returns the number of CollisionResult objects. ++ +getClosestCollision() Returns the CollisionResult with the lowest distance. ++ +getFarthestCollision() Returns the CollisionResult with the farthest distance. ++ +getCollision(i) Returns the CollisionResult at index i. ++A CollisionResult object contains information about the second party of the collision event. +
++ ++
+ +CollisionResult Method Usage ++ +getContactPoint() Returns the contact point coordinate on the second party, as Vector3f. ++ +getContactNormal() Returns the Normal vector at the contact point, as Vector3f. ++ +getDistance() Returns the distance between the Collidable and the second party, as float. ++ +getGeometry() Returns the Geometry of the second party. ++ +getTriangle(t) Binds t to the triangle t on the second party's mesh that was hit. ++ +getTriangleIndex() Returns the index of the triangle on the second party's mesh that was hit. (?) +Code Sample
++ ++ +Assume you have two collidables a and b and want to detect collisions between them. The collision parties can be Geometries, Nodes with Geometries attached (including the rootNode), Planes, Quads, Lines, or Rays. +
+ ++The following code snippet can be triggered by listeners (e.g. after an input action such as a click), or timed in the update loop. +
+// Calculate detection results CollisionResults results = new CollisionResults(); a.collideWith(b, results); - System.out.println("Number of Collisions between" + a.getName()+ " and " - + b.getName() " : " + results.size()); + System.out.println("Number of Collisions between" + + a.getName()+ " and " + b.getName() ": " + results.size()); // Use the results if (results.size() > 0) { + // how to react when a collision was detected CollisionResult closest = results.getClosestCollision(); System.out.println("What was hit? " + closest.getGeometry().getName() ); System.out.println("Where was it hit? " + closest.getContactPoint() ); @@ -70,7 +129,12 @@ class="col1">Returns the index of the triangle on the second party's mesh t } else { // how to react when no collision occured } -}You can also loop over all results and trigger different reactions depending on what was hit and where it was hit. In this example, we simply print info about them.
// Calculate Results +}+ ++You can also loop over all results and trigger different reactions depending on what was hit and where it was hit. In this example, we simply print info about them. +
+// Calculate Results CollisionResults results = new CollisionResults(); a.collideWith(b, results); System.out.println("Number of Collisions between" + a.getName()+ " and " @@ -86,87 +150,62 @@ class="col1">Returns the index of the triangle on the second party's mesh t System.out.println("Details of Collision #" + i + ":"); System.out.println(" Party " + party + " was hit at " + pt + ", " + dist + " wu away."); System.out.println(" The hit triangle #" + tri + " has a normal vector of " + norm); - }Knowing the distance of the collisions is useful for example when you intersect Lines and Rays with other objects.
Intersection
A com.jme3.math.Ray is an infinite line with a beginning, a direction, and no end; whereas a com.jme3.math.Line is an infinite line with only a direction (no beginning, no end). -Rays are used to detect where a 3D object is "looking" and whether one object can "see" the other.
You can determine where a user has clicked by casting a ray from the camera in the direction of the camera. Now identify the closest collision of the ray with e.g. the rootNode. Or you cast a ray from a player in the direction of another player. Then you detect all collisions of this ray with other entities (walls, foliage, window panes) and use this to calculate whether one can see the other.These simple ray-surface intersection tests are called Ray Casting. (As opposed to the more advanced Ray Tracing, Ray Casting does not follow a ray's reflection after the first hit, but the ray goes straight on.)
Usecase: Picking a target with crosshairs
This
pick target
input mapping implements an action that determines what a user clicked (if you map this to a mouse click). It assumes that there are crosshairs in the center of the screen, and the user aims the crosshairs at an object in the scene. We use a ray casting approach to determine the geometry that was picked by the user. You can extend this code to do whatever with the identified target (shoot it, take it, move it, …) -Use this together withinputManager.setCursorVisible(false)
.private AnalogListener analogListener = new AnalogListener() { - public void onAnalog(String name, float intensity, float tpf) { - if (name.equals("pick target")) { - // Reset results list. - CollisionResults results = new CollisionResults(); - // Aim the ray from camera location in camera direction - // (assuming crosshairs in center of screen). - Ray ray = new Ray(cam.getLocation(), cam.getDirection()); - // Collect intersections between ray and all nodes in results list. - rootNode.collideWith(ray, results); - // Print the results so we see what is going on - for (int i = 0; i < results.size(); i++) { - // For each “hit”, we know distance, impact point, geometry. - float dist = results.getCollision(i).getDistance(); - Vector3f pt = results.getCollision(i).getContactPoint(); - String target = results.getCollision(i).getGeometry().getName(); - System.out.println("Selection #" + i + ": " + target + " at " + pt + ", " + dist + " WU away."); - } - // 5. Use the results -- we rotate the selected geometry. - if (results.size() > 0) { - // The closest result is the target that the player picked: - Geometry target = results.getClosestCollision().getGeometry(); - // Here comes the action: - if(target.getName().equals("Red Box")) - target.rotate(0, - intensity, 0); - else if(target.getName().equals("Blue Box")) - target.rotate(0, intensity, 0); - } - } // else if ... - } - };Usecase: Picking a target with mouse cursor
This
pick target
input mapping implements an action that determines what a user clicked (if you map this to a mouse click). It assumes that the cursor is visible, and the user aims the cursor at an object in the scene. We use a ray casting approach to determine the geometry that was picked by the user. You can extend this code to do whatever with the identified target (shoot it, take it, move it, …) Use this together withinputManager.setCursorVisible(true)
.private AnalogListener analogListener = new AnalogListener() { - public void onAnalog(String name, float intensity, float tpf) { - if (name.equals("pick target")) { - // Reset results list. - CollisionResults results = new CollisionResults(); - // Convert screen click to 3d position - Vector2f click2d = inputManager.getCursorPosition(); - Vector3f click3d = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); - Vector3f dir = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); - // Aim the ray from the clicked spot forwards. - Ray ray = new Ray(click3d, dir); - // Collect intersections between ray and all nodes in results list. - rootNode.collideWith(ray, results); - // (Print the results so we see what is going on:) - for (int i = 0; i < results.size(); i++) { - // (For each “hit”, we know distance, impact point, geometry.) - float dist = results.getCollision(i).getDistance(); - Vector3f pt = results.getCollision(i).getContactPoint(); - String target = results.getCollision(i).getGeometry().getName(); - System.out.println("Selection #" + i + ": " + target + " at " + pt + ", " + dist + " WU away."); - } - // Use the results -- we rotate the selected geometry. - if (results.size() > 0) { - // The closest result is the target that the player picked: - Geometry target = results.getClosestCollision().getGeometry(); - // Here comes the action: - if (target.getName().equals("Red Box")) { - target.rotate(0, -intensity, 0); - } else if (target.getName().equals("Blue Box")) { - target.rotate(0, intensity, 0); - } - } - } // else if ... - } - };Bounding Interval Hierarchy
com.jme3.collision.bih.BIHNode -com.jme3.scene.CollisionData
SweepSphere
- \ No newline at end of file + }A com.jme3.collision.SweepSphere implements a collidable "stretched" sphere that is shaped like a capsule (an upright cylinder with half a sphere on top and the second half at the bottom). -This shape is usually used to simulate simple non-physcial collisions for character entities in games. The sweep sphere can be used to check collision against a triangle or another sweep sphere.
+Knowing the distance of the collisions is useful for example when you intersect Lines and Rays with other objects. +
+ ++ +A com.jme3.math.Ray is an infinite line with a beginning, a direction, and no end; whereas a com.jme3.math.Line is an infinite line with only a direction (no beginning, no end). +
+ ++Rays are used to detect where the user or a player is "looking" when performing an action: +
++ +These simple ray-surface intersection tests are called Ray Casting. As opposed to the more advanced Ray Tracing, Ray Casting does not follow a ray's reflection after the first hit, the ray just goes straight on. +
+ ++Learn how to implement Mouse Picking here. +
+ ++ +com.jme3.collision.bih.BIHNode +com.jme3.scene.CollisionData +
+ ++ +A com.jme3.collision.SweepSphere implements a collidable "stretched" sphere that is shaped like a capsule (an upright cylinder with half a sphere on top and the second half at the bottom). +This shape is usually used to simulate simple non-physcial collisions for character entities in games. The sweep sphere can be used to check collision against a triangle or another sweep sphere. +
+ +The ComboMoves class allows you to define combinations of inputs that trigger special actions. Entering an input combo correctly can bring the player incremental rewards, such as an increased chance to hit, an increased effectiveness, or decreased change of being blocked, whatever the game designer chooses. More background info
Combos are usually a series of inputs, in a fixed order: For example a keyboard combo can look like: "press Down, then Down+Right together, then Right".
Usage:
Copy the two classes ComboMoveExecution.java and ComboMove.java into your application and adjust them to your package paths.
First you define your game's inputs as you usually do: Implement the com.jme3.input.controls.ActionListener interface for your class, and add triggers mappings such as com.jme3.input.controls.KeyTrigger and com.jme3.input.KeyInput.
For example:
inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_LEFT)); + ++ +Combo Moves
++ ++ ++The ComboMoves class allows you to define combinations of inputs that trigger special actions. Entering an input combo correctly can bring the player incremental rewards, such as an increased chance to hit, an increased effectiveness, or decreased change of being blocked, whatever the game designer chooses. +
+ ++Combos are usually a series of inputs, in a fixed order: For example a keyboard combo can look like: "press Down, then Down+Right together, then Right". +
+ ++Usage: +
++
+ +- +
Create input triggers+- +
Define combos+- +
Detect combos in ActionListener+- +
Execute combos in update loop++ +Copy the two classes ComboMoveExecution.java and ComboMove.java into your application and adjust them to your package paths. +
+ +Example Code
+++ ++
+ +- +
+- +
← required+- +
← required+Create Input Triggers
++ ++ +First you define your game's inputs as you usually do: Implement the com.jme3.input.controls.ActionListener interface for your class, and add triggers mappings such as com.jme3.input.controls.KeyTrigger and com.jme3.input.KeyInput. +
+ ++For example: +
+inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_LEFT)); inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_RIGHT)); inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_UP)); inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_DOWN)); inputManager.addMapping("Attack1", new KeyTrigger(KeyInput.KEY_SPACE)); ... -inputManager.addListener(this, "Left", "Right", "Up", "Down", "Attack1");Define Combos
+ +For each of your combo moves, you specify the series of inputs that will trigger it. The order in which you define them is the order the player has to press them for the step to be recorded. When all steps have been recorded, the combo is triggered.
The following example shows how a fireball combo move is triggered by pressing the navigation keys for "down, down+right, right", in this order.
ComboMove fireball = new ComboMove("Fireball"); +inputManager.addListener(this, "Left", "Right", "Up", "Down", "Attack1");+ +Define Combos
++ ++ +For each of your combo moves, you specify the series of inputs that will trigger it. The order in which you define them is the order the player has to press them for the step to be recorded. When all steps have been recorded, the combo is triggered. +
+ ++The following example shows how a fireball combo move is triggered by pressing the navigation keys for "down, down+right, right", in this order. +
+ComboMove fireball = new ComboMove("Fireball"); fireball.press("Down").notPress("Right").done(); fireball.press("Right", "Down").done(); fireball.press("Right").notPress("Down").done(); fireball.notPress("Right", "Down").done(); -fireball.setUseFinalState(false);Also create a ComboMoveExecution object for each ComboMove. You need it later to execute the detected combo.
ComboMoveExecution fireballExec = new ComboMoveExecution(fireball);ComboMove Class Methods
Use the following ComboMove methods to specify the combo:
ComboMove Method Description press("A").done();
press("A","B").done();Combo step is recorded if A is entered.
Combo step is recorded if A and B are entered simultaneously.notPress("A").done();
notPress("A","B").done();Combo step is recorded if A is released.
Combo step is recorded if A and B are both released.press("A").notPress("B").done(); Combo step is recorded if A is entered, and not B press("A").notPress("B").timeElapsed(0.11f).done(); Combo step is recorded a certain time after A and not B is entered.
etc, etc …setPriority(0.5f); If there is an ambiguity, a high-priority combo will trigger instead of a low-priority combo. This prevents that a similar looking combo step "hijacks" another Combo. Use only once per ComboMove. setUseFinalState(false);
setUseFinalState(true);This is the final command of the series.
False: Do not wait on a final state, chain combo steps. (?)
True: This is the final state, do not chain combo steps. (?)The
press()
andnotPress()
methods accept sets of Input Triggers, e.g.fireball.press("A","B","C").done()
.The following getters give you more information about the game state:
ComboMove Method Usage getCastTime() Returns the time since the last step has been recorded. (?) getMoveName() Returns the string of the current combo getPriority() Returns the priority of this move Detect Combos in ActionListener
+ +Now that you have specified the combo steps, you want to detect them. You do that in the onAction() method that you get from the ActionListener interface.
Create a HashSet
pressMappings
to track curently pressed mappings, and a ComboMove objectcurrentMove
to track the current move.We also track the cast time of a combo to determine if it has timed out (see update loop below).
private HashSet<String> pressedMappings = new HashSet<String>(); +fireball.setUseFinalState(false);+ ++Also create a ComboMoveExecution object for each ComboMove. You need it later to execute the detected combo. +
+ComboMoveExecution fireballExec = new ComboMoveExecution(fireball);+ +ComboMove Class Methods
++ ++ ++ +Use the following ComboMove methods to specify the combo: + +
++ ++
+ +ComboMove Method Description ++ +press("A").done();
+press("A","B").done();Combo step is recorded if A is entered. +
+Combo step is recorded if A and B are entered simultaneously.+ +notPress("A").done();
+notPress("A","B").done();Combo step is recorded if A is released. +
+Combo step is recorded if A and B are both released.+ +press("A").notPress("B").done(); Combo step is recorded if A is entered, and not B ++ +press("A").notPress("B").timeElapsed(0.11f).done(); Combo step is recorded a certain time after A and not B is entered. +
+etc, etc …+ +setPriority(0.5f); If there is an ambiguity, a high-priority combo will trigger instead of a low-priority combo. This prevents that a similar looking combo step "hijacks" another Combo. Use only once per ComboMove. ++ +setUseFinalState(false);
+setUseFinalState(true);This is the final command of the series. +
+False: Do not wait on a final state, chain combo steps. (?)
+True: This is the final state, do not chain combo steps. (?)+ +The
+ +press()
andnotPress()
methods accept sets of Input Triggers, e.g.fireball.press("A","B","C").done()
. ++The following getters give you more information about the game state: + +
++ ++
+ +ComboMove Method Usage ++ +getCastTime() Returns the time since the last step has been recorded. (?) ++ +getMoveName() Returns the string of the current combo ++ +getPriority() Returns the priority of this move +Detect Combos in ActionListener
++ ++ +Now that you have specified the combo steps, you want to detect them. You do that in the onAction() method that you get from the ActionListener interface. +
+ ++Create a HashSet
+ +pressMappings
to track curently pressed mappings, and a ComboMove objectcurrentMove
to track the current move. ++We also track the cast time of a combo to determine if it has timed out (see update loop below). +
+private HashSet<String> pressedMappings = new HashSet<String>(); private ComboMove currentMove = null; private float currentMoveCastTime = 0; private float time = 0; @@ -117,12 +211,21 @@ public void onAction(String name, boolean isPressed, float tpf) { currentMove = toExec; currentMoveCastTime = currentMove.getCastTime(); } -}Execute Combos in the Update Loop
+ +Now that you have detected the current move, you want to execute it. You do that in the update loop.
@Override +}+ +Execute Combos in the Update Loop
++ ++ +Now that you have detected the current move, you want to execute it. You do that in the update loop. +
+@Override public void simpleUpdate(float tpf){ time += tpf; - fireballExec.updateExpiration(time); + fireballExec.updateExpiration(time); // ... update more ComboExecs here.... if (currentMove != null){ @@ -134,15 +237,33 @@ public void simpleUpdate(float tpf){ currentMove = null; } } -}Test
currentMove.getMoveName()
and proceed to call methods that implement any special actions and bonuses. This is up to you and depends individually on your game.Why Combos?
- \ No newline at end of file +}Depending on the game genre, the designer can reward the players' intrinsical or extrinsical skills:
(intrinsical:) RPGs typically calculate the success of an attack from the character's in-game training level: The player plays the role of a character whose skill level is defined in numbers. RPGs typically do not offer any Combos. (extrinsical:) Sport and fighter games typically choose to reward the player's "manual" skills: The success of a special move solely depends on the player's own dexterity. These games typically offer optional Combos.
+Test currentMove.getMoveName()
and proceed to call methods that implement any special actions and bonuses. This is up to you and depends individually on your game.
+
+ +Depending on the game genre, the designer can reward the players' intrinsical or extrinsical skills: + +
+ A com.jme3.scene.control.Control
is a customizable jME3 interface that allows you to cleanly implement game logic, such as game rules, or artificially intelligent behaviour in NPCs. You use Controls to control the behaviour of types of spatials. To control global game behaviour see Application States – you can use both together.
-To control the behaviour of types of entities:
spatial.addControl(myControl)
To implement game logic for a type of spatial, you will either extend AbstractControl, or implement the Control interface, as explained in this article.
For example, you could write a CharacterAnimControl that animates a character accordingly while it is being moved by a CharacterControl. Or you can write an AIControl that remote-controls NPC behaviour in fight situatons. Or you could write a DestructionControl that automatically replaces a structure with an appropriate piece of debris after collision with a projectile… The possibilities are endless. Existing examples in the code base include:
The interface can be found under com.jme3.scene.control.Control
. It has the following method signatures:
cloneForSpatial(Spatial)
: Clones the Control and attaches it to a clone of the given Spatial. The AssetManager uses this method if the same spatial is loaded twice. You can specify which fields you want your object to reuse (e.g. collisionshapes) in this case.setEnabled(boolean)
: Enable or disable the control. If disabled, update() does nothing. Goes with accessor isEnabled();
.setSpatial(Spatial s)
, update(float tpf);
, render(RenderManager rm, ViewPort vp)
.If you want to create a Control that also extends an existing class, then create a custom extension of the Control Interface. Usage example: -1. Create a custom control interface
public interface MyControl extends Control { + +Custom Controls
++ ++ ++A
+com.jme3.scene.control.Control
is a customizable jME3 interface that allows you to cleanly implement game logic, such as game rules, or artificially intelligent behaviour in NPCs. You use Controls to control the behaviour of types of spatials. To control global game behaviour see Application States – you can use both together. +To control the behaviour of types of entities: ++
+ +- +
Create one control for each type of behavior. When you add several controls to one spatial, they will be executed in the order they were added.+
+For example, an NPC can be controlled by a PhysicsControl and an AIControl.- +
You define a custom control and implement its behaviour in the Control's update() method.++
+- +
In the control, you can pass arguments and manipulate the spatial in any way: Modify its transformation (move, scale, rotate), play animations, check for enemies around it and react, etc.+- +
Add the control to a spatial and the Spatial's game state is updated automatically from now on.+
+spatial.addControl(myControl)
+To implement game logic for a type of spatial, you will either extend AbstractControl, or implement the Control interface, as explained in this article. + +
+ +Usage Examples
++ ++ ++For example, you could write a CharacterAnimControl that animates a character accordingly while it is being moved by a CharacterControl. Or you can write an AIControl that remote-controls NPC behaviour in fight situatons. Or you could write a DestructionControl that automatically replaces a structure with an appropriate piece of debris after collision with a projectile… The possibilities are endless. +Existing examples in the code base include: +
++
+ +- +
allows manipulation of skeletal animation, including blending and multiple channels.+- +
allows you to sync the camera position with the position of a given spatial.+- +
displays a flat picture orthogonally, e.g. a speech bubble or informational dialog.+- +
subclasses (such as CharacterControl, RigidBodyControl, VehicleControl) allow you to add physical properties to any spatial. PhysicsControls tie into capabilities provided by the BulletAppState.+The Control Interface
++ ++The interface can be found under
+com.jme3.scene.control.Control
. It has the following method signatures: ++
+ +- +
+cloneForSpatial(Spatial)
: Clones the Control and attaches it to a clone of the given Spatial. The AssetManager uses this method if the same spatial is loaded twice. You can specify which fields you want your object to reuse (e.g. collisionshapes) in this case.- +
+setEnabled(boolean)
: Enable or disable the control. If disabled, update() does nothing. Goes with accessorisEnabled();
.- +
There are also some internal methods that you do not call from user code:+setSpatial(Spatial s)
,update(float tpf);
,render(RenderManager rm, ViewPort vp)
.+If you want to create a Control that also extends an existing class, then create a custom extension of the Control Interface. Usage example: +1. Create a custom control interface + +
+public interface MyControl extends Control { public void setSomething(int x); // add your custom methods -}2. Create custom classes implementing your control interface
public class ControlledThing extends MyThing implements MyControl { +}+ ++ +2. Create custom classes implementing your control interface + +
+public class ControlledThing extends MyThing implements MyControl { protected Spatial spatial; protected boolean enabled = true; public ControlledThing() { } @@ -90,15 +131,30 @@ class="li"> There are also some internal methods that you do not call from user spatial = (Spatial) ic.readSavable("spatial", null); // read custom variables .... } -}AbstractControl
+ +This class can be found under
com.jme3.scene.control.AbstractControl
.
This is a default abstract class that implements the Control interface. It gives you access to a booleanenabled
, and a Spatialspatial
. Extend AbstractControl to create a custom Control.Usage: Your custom subclass must implement the three methods
controlUpdate()
,controlRender()
, andcloneForSpatial()
as shown here:public class MyControl extends AbstractControl implements Savable, Cloneable { +}+ +AbstractControl
++ ++This class can be found under
+com.jme3.scene.control.AbstractControl
. ++
+ +- +
This is a default abstract class that implements the Control interface.+- +
It gives you access to a boolean+enabled
, and a Spatialspatial
.- +
Extend AbstractControl to create a custom Control.++Usage: Your custom subclass must implement the three methods
+controlUpdate()
,controlRender()
, andcloneForSpatial()
as shown here: + +public class MyControl extends AbstractControl implements Savable, Cloneable { private Thing thing; // some custom class of yours public MyControl(){} // empty serialization constructor public MyControl(thing) { // some custom constructor @@ -131,11 +187,19 @@ class="li"> Extend AbstractControl to create a custom Control.
Tip: Use the getControl() accessor to get Control objects from Spatials. No need to pass around lots of object references. -Here an example from the MonkeyZone code:
public class CharacterAnimControl implements Control { +}+ +
+Tip: Use the getControl() accessor to get Control objects from Spatials. No need to pass around lots of object references. +Here an example from the code: + +
+public class CharacterAnimControl implements Control { ... public void setSpatial(Spatial spatial) { ... @@ -143,16 +207,52 @@ href="http://code.google.com/p/monkeyzone/">MonkeyZone code:public characterControl = spatial.getControl(CharacterControl.class); ... } -}Tip: You can create custom Control interfaces so a set of different Controls provide the same methods and can be accessed with the interface class type.
public interface ManualControl extends Control { +}+ ++ +Tip: You can create custom Control interfaces so a set of different Controls provide the same methods and can be accessed with the interface class type. + +
+public interface ManualControl extends Control { public void steerX(float value); public void steerY(float value); public void moveX(float value); public void moveY(float value); public void moveZ(float value); ... -}Then you create custom sub-Controls and implement the methods accordingly to the context:
public class ManualVehicleControl extends ManualControl {...}and
public class ManualCharacterControl extends ManualControl {...}Then add the appropriate controls to spatials:
characterSpatial.addControl(new ManualCharacterControl()); +}+ ++ +Then you create custom sub-Controls and implement the methods accordingly to the context: + +
+public class ManualVehicleControl extends ManualControl {...}+ ++ and + +
+public class ManualCharacterControl extends ManualControl {...}+ ++ +Then add the appropriate controls to spatials: + +
+characterSpatial.addControl(new ManualCharacterControl()); ... vehicleSpatial.addControl(new ManualVehicleControl()); -...Tip: Use the getControl() method on a Spatial to get a specific Control object, and activate its behaviour!
ManualControl c = mySpatial.getControl(ManualControl.class); -c.steerX(steerX);
+ +Tip: Use the getControl() method on a Spatial to get a specific Control object, and activate its behaviour! + +
+ManualControl c = mySpatial.getControl(ManualControl.class); +c.steerX(steerX);+ + + \ No newline at end of file diff --git a/sdk/jme3-core/javahelp/com/jme3/gde/core/docs/jme3/advanced/custom_meshes.html b/sdk/jme3-core/javahelp/com/jme3/gde/core/docs/jme3/advanced/custom_meshes.html index 20783cb46..f15c41199 100644 --- a/sdk/jme3-core/javahelp/com/jme3/gde/core/docs/jme3/advanced/custom_meshes.html +++ b/sdk/jme3-core/javahelp/com/jme3/gde/core/docs/jme3/advanced/custom_meshes.html @@ -1,77 +1,202 @@ -
Use the Mesh class to create custom shapes that go beyond Quad, Box, Cylinder, and Sphere, even procedural shapes are possible. Thank you to KayTrance for providing the sample code!
In this tutorial, we (re)create a very simple rectangular mesh, and we have a look at different ways of coloring it. A flat rectangle may not look useful because it's exactly the same as a com.jme3.scene.shape.Quad
. We choose this simple example in order to show you how to build any shape out of triangles – without the distractions of more complex shapes.
Polygon meshes are made up of triangles. The corners of the triangles are vertices. So, when ever you create a new shape, you break it down into triangles.
Let's look at a cube. A cube is made up of 6 rectangles. Each rectangle can be broken down into two triangles. This means you need 12 triangles to create a cube mesh. You also need to know the 8 corner coordinates (vertices). The trick is that you have to specify the vertices in a certain order: Each triangle separately, counter-clockwise.
Sounds worse than it is – here is an example:
Okay, we want to create a Quad. A quad has four vertices, and is made up of two triangles.
The base class for creating meshes is com.jme3.scene.Mesh
.
Mesh m = new Mesh();
To define your own shape, determine its vertex positions in space. Store them in an array using com.jme3.math.Vector3f. For a Quad, we need four vertices: Bottom left, bottom right, top left, top right. We name the array vertices[]
.
Vector3f [] vertices = new Vector3f[4]; + ++ +Custom Mesh Shapes
++ ++ ++ +Use the Mesh class to create custom shapes that go beyond Quad, Box, Cylinder, and Sphere, even procedural shapes are possible. Thank you to KayTrance for providing the sample code! +In this tutorial, we (re)create a very simple rectangular mesh, and we have a look at different ways of coloring it. A flat rectangle may not look useful because it's exactly the same as a
+com.jme3.scene.shape.Quad
. We choose this simple example in order to show you how to build any shape out of triangles – without the distractions of more complex shapes. ++
+ +- +
Full code sample:+Polygon Meshes
++ ++ ++Polygon meshes are made up of triangles. The corners of the triangles are vertices. So, when ever you create a new shape, you break it down into triangles. +Let's look at a cube. A cube is made up of 6 rectangles. Each rectangle can be broken down into two triangles. This means you need 12 triangles to create a cube mesh. You also need to know the 8 corner coordinates (vertices). The trick is that you have to specify the vertices in a certain order: Each triangle separately, counter-clockwise. +Sounds worse than it is – here is an example: + +
+ +Creating a Quad Mesh
++ ++ ++Okay, we want to create a Quad. A quad has four vertices, and is made up of two triangles. +The base class for creating meshes is
+com.jme3.scene.Mesh
. + +Mesh m = new Mesh();+ +Vertices
++ ++To define your own shape, determine its vertex positions in space. Store them in an array using com.jme3.math.Vector3f. For a Quad, we need four vertices: Bottom left, bottom right, top left, top right. We name the array
+vertices[]
. + +Vector3f [] vertices = new Vector3f[4]; vertices[0] = new Vector3f(0,0,0); vertices[1] = new Vector3f(3,0,0); vertices[2] = new Vector3f(0,3,0); -vertices[3] = new Vector3f(3,3,0);Texture Coordinates
+ +Next, define the Quad's 2D texture coordinates for each vertex, in the same order: Bottom left, bottom right, top left, top right. We name this array
texCoord[]
Vector2f[] texCoord = new Vector2f[4]; +vertices[3] = new Vector3f(3,3,0);+ +Texture Coordinates
++ ++Next, define the Quad's 2D texture coordinates for each vertex, in the same order: Bottom left, bottom right, top left, top right. We name this array
+texCoord[]
+ +Vector2f[] texCoord = new Vector2f[4]; texCoord[0] = new Vector2f(0,0); texCoord[1] = new Vector2f(1,0); texCoord[2] = new Vector2f(0,1); -texCoord[3] = new Vector2f(1,1);Connecting the Dots
+ +Next we turn the unrelated coordinates into triangles – We define the order in which the mesh is constructed. Think of these indexes as coming in groups of three. Each group of indexes describes one triangle. Note that you must specify the vertices counter-clockwise!
int [] indexes = { 2,0,1, 1,3,2 };
The 2,0,1 triangle starts at top left, continues bottom left, and ends at bottom right. The 1,3,2 triangle start at bottom right, continues top right, and ends at top left.2\2--3 +texCoord[3] = new Vector2f(1,1);+ +Connecting the Dots
++ ++Next we turn the unrelated coordinates into triangles – We define the order in which the mesh is constructed. Think of these indexes as coming in groups of three. Each group of indexes describes one triangle. Note that you must specify the vertices counter-clockwise! + +
+int [] indexes = { 2,0,1, 1,3,2 };++
+- +
The 2,0,1 triangle starts at top left, continues bottom left, and ends at bottom right.+- +
The 1,3,2 triangle start at bottom right, continues top right, and ends at top left.+2\2--3 | \ | Counter-clockwise | \ | -0--1\1Setting the Mesh Buffer
+ +The Mesh data is stored in a buffer.
Usingcom.jme3.util.BufferUtils
, we create three buffers for the three types of information we have:
vertex positions, texture coordinates, indices. We assign the data to the appropriate type of buffer inside the mesh object. The three buffer types are taken from an enum incom.jme3.scene.VertexBuffer.Type
. The third parameter describes the number of components of the values. Vertex postions are 3 float values, texture coordinates are 2 float values, and the indices are single ints. In order for JMonkey to correctly show the mesh in the scene, it needs to know the bounds of our new mesh. This can easily be achieved by calling the updateBound() method on it.m.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertices)); +0--1\1+ +Setting the Mesh Buffer
++ ++The Mesh data is stored in a buffer. +
++
+- +
Using+com.jme3.util.BufferUtils
, we create three buffers for the three types of information we have:+
+- +
vertex positions,+- +
texture coordinates,+- +
indices.+- +
We assign the data to the appropriate type of buffer inside the mesh object. The three buffer types are taken from an enum in+com.jme3.scene.VertexBuffer.Type
.- +
The third parameter describes the number of components of the values. Vertex postions are 3 float values, texture coordinates are 2 float values, and the indices are single ints.+- +
In order for JMonkey to correctly show the mesh in the scene, it needs to know the bounds of our new mesh. This can easily be achieved by calling the updateBound() method on it.+m.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertices)); m.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoord)); m.setBuffer(Type.Index, 1, BufferUtils.createIntBuffer(indexes)); -m.updateBound();Our Mesh is ready! Now we want to see it.
Using the Mesh in a Scene
+ +We create a
com.jme3.scene.Geometry
, apply a simple color material to it, and attach it to the rootNode to make it appear in the scene.Geometry geom = new Geometry("OurMesh", m); +m.updateBound();+ ++ +Our Mesh is ready! Now we want to see it. + +
+ +Using the Mesh in a Scene
++ ++We create a
+com.jme3.scene.Geometry
, apply a simple color material to it, and attach it to the rootNode to make it appear in the scene. + +Geometry geom = new Geometry("OurMesh", m); Material mat = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md"); mat.setColor("Color", ColorRGBA.Blue); geom.setMaterial(mat); -rootNode.attachChild(geom);Ta-daa!
Optional Mesh Features
There are more vertex buffers in a Mesh than the three shown above. For an overview, see also Meshes and Models.
Example: Vertex Colors
+ +Vertex coloring is a simple way of coloring meshes. Instead of just assigning one solid color, each vertex (corner) has a color assigned. The faces between the vertices are then colored with a gradient.
We will use the same mesh
m
as defined above, but with a special VertexColor material.Geometry coloredMesh = new Geometry ("ColoredMesh", m); -Material matVC = new Material(assetManager, "Common/MatDefs/Misc/VertexColor.j3md");We create a float array color buffer.
We assign 4 color values, RGBA, to each vertex.
To loop over the 4 color values, we use a color indexint colorIndex = 0; The color buffer contains four color values for each vertex.
The Quad in this example has 4 vertices.float[] colorArray = new float[4*4]; Tip: If your mesh has a different number of vertices, you would write:float[] colorArray = new float[yourVertexCount * 4]We loop over the colorArray buffer to quickly set some RGBA value for each vertex. As usual, RGBA color values range from 0.0f to 1.0f. Note that the values we use here are arbitrarily chosen! It's just a quick loop to give every vertex a different RGBA value (a purplish gray, purple, a greenish gray, green, see screenshot), without writing too much code. For your own mesh, you'd assign values for the color buffer depending on which color you want your mesh to have.
for(int i = 0; i < 4; i++){ +rootNode.attachChild(geom);+ ++ +Ta-daa! + +
+ +Optional Mesh Features
++ ++ ++There are more vertex buffers in a Mesh than the three shown above. For an overview, see also mesh. + +
+ +Example: Vertex Colors
++ ++Vertex coloring is a simple way of coloring meshes. Instead of just assigning one solid color, each vertex (corner) has a color assigned. The faces between the vertices are then colored with a gradient. +We will use the same mesh
+m
as defined above, but with a special VertexColor material. + +Geometry coloredMesh = new Geometry ("ColoredMesh", m); +Material matVC = new Material(assetManager, "Common/MatDefs/Misc/VertexColor.j3md");+ ++ +We create a float array color buffer. +
++
+ +- +
We assign 4 color values, RGBA, to each vertex.++
+- +
To loop over the 4 color values, we use a color index+int colorIndex = 0;+- +
The color buffer contains four color values for each vertex.++
+- +
The Quad in this example has 4 vertices.+float[] colorArray = new float[4*4];+- +
Tip: If your mesh has a different number of vertices, you would write:+float[] colorArray = new float[yourVertexCount * 4]++We loop over the colorArray buffer to quickly set some RGBA value for each vertex. As usual, RGBA color values range from 0.0f to 1.0f. Note that the values we use here are arbitrarily chosen! It's just a quick loop to give every vertex a different RGBA value (a purplish gray, purple, a greenish gray, green, see screenshot), without writing too much code. For your own mesh, you'd assign values for the color buffer depending on which color you want your mesh to have. + +
+for(int i = 0; i < 4; i++){ // Red value (is increased by .2 on each next vertex here) colorArray[colorIndex++]= 0.1f+(.2f*i); // Green value (is reduced by .2 on each next vertex) @@ -80,10 +205,32 @@ class="li"> Tip: If your mesh has a different number of vertices, you would writ colorArray[colorIndex++]= 0.5f; // Alpha value (no transparency set here) colorArray[colorIndex++]= 1.0f; -}Next, set the color buffer. An RGBA color value contains four float components, thus the parameter
4
.m.setBuffer(Type.Color, 4, colorArray); -coloredMesh.setMaterial(matVC);Now you see a gradient color extending from each vertex.
Example: Point Mode
+ +Alternatively, you can show the vertices as colored points instead of coloring the faces.
Geometry coloredMesh = new Geometry ("ColoredMesh", cMesh); +}+ ++ +Next, set the color buffer. An RGBA color value contains four float components, thus the parameter
+4
. + +m.setBuffer(Type.Color, 4, colorArray); +coloredMesh.setMaterial(matVC);+ ++ +Now you see a gradient color extending from each vertex. + +
+ +Example: Point Mode
++ ++Alternatively, you can show the vertices as colored points instead of coloring the faces. + +
+Geometry coloredMesh = new Geometry ("ColoredMesh", cMesh); ... m.setMode(Mesh.Mode.Points); m.setPointSize(10f); @@ -92,12 +239,32 @@ m.setStatic(); Geometry points = new Geometry("Points", m); points.setMaterial(mat); rootNode.attachChild(points); - -rootNode.attachChild(coloredMesh);This will result in a 10 px dot being rendered for each of the four vertices. The dot has the vertex color you specified above. The Quad's faces are not rendered at all. This can be used for a special debugging or editing mode.
Tip: Front and Back Faces
- \ No newline at end of file +rootNode.attachChild(coloredMesh);By default, jME3 optimizes a scene by culling all backfaces. It determines which side the front or backface of a mesh is by the order of the vertices. The frontface is the one where the vertices are specified counter-clockwise.
This means your mesh, as created above, is invisible when seen from "behind". This may not be a problem and is often even intended. If you use the custom meshes to form a polyhedron, or flat wallpaper-like object, rendering the backfaces (the inside of the polyhedron) would indeed be a waste of resources.
In case that your use case requires the backfaces to be visible, you have two options:
If you have a very simple scene, you can just deactivate backface culling for this one mesh's material.
mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off));
The recommended solution is to specify each triangle twice, the second time with the opposite order of vertices. The second, reversed triangle makes up the backface.
int[] indexes = { 2,0,1, 1,3,2, 2,3,1, 1,0,2 };
+ +This will result in a 10 px dot being rendered for each of the four vertices. The dot has the vertex color you specified above. The Quad's faces are not rendered at all. This can be used for a special debugging or editing mode. + +
+ ++By default, jME3 optimizes a scene by culling all backfaces. It determines which side the front or backface of a mesh is by the order of the vertices. The frontface is the one where the vertices are specified counter-clockwise. +This means your mesh, as created above, is invisible when seen from "behind". This may not be a problem and is often even intended. If you use the custom meshes to form a polyhedron, or flat wallpaper-like object, rendering the backfaces (the inside of the polyhedron) would indeed be a waste of resources. +In case that your use case requires the backfaces to be visible, you have two options: +
+mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off));
int[] indexes = { 2,0,1, 1,3,2, 2,3,1, 1,0,2 };
When you deal with complex game engine features like animations or physics it is handy to get feedback from the engine how it interpreted the current state. Is the physical object's collision shape really where you think it is? Is the skeleton of the animated character moveing like you think it should? This document shows you how to activate visual debug aides.
What if you just want to quickly write code that loads models and brings them in their start position? You may not want to hunt for a sample model, convert it, add lights, and load materials. Instead you use "hasslefree" simple shapes a wireframe material: No model, no light source, no materials are needed to see them in the scene.
If you ever have problems with objects appearing in the wrong spot, with the wrong scale, or wrong orientation, simply attach debug shapes to your scene to have a point of reference in 3D space – just like a giant ruler. If your code positions the debug shapes correctly, but models remain invisible when you apply the same code to them, you know that the problem must be the model or the light or its material – and not the positioning code.
Here are some different debug shapes:
The coordinate axes (com.jme3.scene.debug.Arrow) help you see the cardinal directions (X,Y,Z) from their center point. Scale the arrows to use them as a "ruler" for a certain length.
private void attachCoordinateAxes(Vector3f pos){ + +Debugging
++ ++ ++ +When you deal with complex game engine features like animations or physics it is handy to get feedback from the engine how it interpreted the current state. Is the physical object's collision shape really where you think it is? Is the skeleton of the animated character moveing like you think it should? This document shows you how to activate visual debug aides. +
+ ++What if you just want to quickly write code that loads models and brings them in their start position? You may not want to hunt for a sample model, convert it, add lights, and load materials. Instead you use "hasslefree" simple shapes a wireframe material: No model, no light source, no materials are needed to see them in the scene. +
+ ++If you ever have problems with objects appearing in the wrong spot, with the wrong scale, or wrong orientation, simply attach debug shapes to your scene to have a point of reference in 3D space – just like a giant ruler. If your code positions the debug shapes correctly, but models remain invisible when you apply the same code to them, you know that the problem must be the model or the light or its material – and not the positioning code. +
+ ++Here are some different debug shapes: +
+ ++ +
+ +Debug Shapes
++ ++ +Coordinate Axes
++ ++ +The coordinate axes (com.jme3.scene.debug.Arrow) help you see the cardinal directions (X,Y,Z) from their center point. Scale the arrows to use them as a "ruler" for a certain length. +
+private void attachCoordinateAxes(Vector3f pos){ Arrow arrow = new Arrow(Vector3f.UNIT_X); arrow.setLineWidth(4); // make arrow thicker putShape(arrow, ColorRGBA.Red).setLocalTranslation(pos); @@ -28,9 +59,18 @@ private Geometry putShape(Mesh shape, ColorRGBA color){ g.setMaterial(mat); rootNode.attachChild(g); return g; -}Wireframe Grid
+ +Use a wireframe grid (com.jme3.scene.debug.Grid) as a ruler or simple floor.
private void attachGrid(Vector3f pos, float size, ColorRGBA color){ +}+ +Wireframe Grid
++ ++ +Use a wireframe grid (com.jme3.scene.debug.Grid) as a ruler or simple floor. +
+private void attachGrid(Vector3f pos, float size, ColorRGBA color){ Geometry g = new Geometry("wireframe grid", new Grid(size, size, 0.2f) ); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.getAdditionalRenderState().setWireframe(true); @@ -39,9 +79,18 @@ class="level3">Use a wireframe grid (com.jme3.scene.debug.Grid) as a ruler o g.center().move(pos); rootNode.attachChild(g); return g; -}
Wireframe Cube
+ +Use a wireframe cube (com.jme3.scene.debug.WireBox) as a stand-in object to see whether your code scales, positions, or orients, loaded models right.
public void attachWireBox(Vector3f pos, float size, ColorRGBA color){ +}+ +Wireframe Cube
++ ++ +Use a wireframe cube (com.jme3.scene.debug.WireBox) as a stand-in object to see whether your code scales, positions, or orients, loaded models right. +
+public void attachWireBox(Vector3f pos, float size, ColorRGBA color){ Geometry g = new Geometry("wireframe cube", new WireBox(size, size, size)); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.getAdditionalRenderState().setWireframe(true); @@ -50,9 +99,18 @@ class="level3">Use a wireframe cube (com.jme3.scene.debug.WireBox) as a stan g.setLocalTranslation(pos); rootNode.attachChild(g); return g; -}
Wireframe Sphere
+ +Use a wireframe sphere (com.jme3.scene.debug.WireSphere) as a stand-in object to see whether your code scales, positions, or orients, loaded models right.
private void attachWireSphere(Vector3f pos, float size, ColorRGBA color){ +}+ +Wireframe Sphere
++ ++ +Use a wireframe sphere (com.jme3.scene.debug.WireSphere) as a stand-in object to see whether your code scales, positions, or orients, loaded models right. +
+private void attachWireSphere(Vector3f pos, float size, ColorRGBA color){ Geometry g = new Geometry("wireframe sphere", new WireSphere(size)); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.getAdditionalRenderState().setWireframe(true); @@ -61,32 +119,69 @@ class="level3">Use a wireframe sphere (com.jme3.scene.debug.WireSphere) as a g.setLocalTranslation(pos); rootNode.attachChild(g); return g; -}
Wireframe for Physics
You can display a wireframe of the (usually invisible) collision shape around all physical objects. Use this for debugging when analyzing unexpected behaviour. Does not work with DETACHED physics, please switch to PARALLEL or SEQUENTIAL for debugging.
physicsSpace.enableDebug(assetManager);Wireframe for Animations
+ +Making the skeleton visible inside animated models can be handy for debugging animations. The
control
object is an AnimControl,player
is the loaded model.SkeletonDebugger skeletonDebug = +}+ +Wireframe for Physics
++ ++ ++ +You can display a wireframe of the (usually invisible) collision shape around all physical objects. Use this for debugging when analyzing unexpected behaviour. Does not work with DETACHED physics, please switch to PARALLEL or SEQUENTIAL for debugging. +
+physicsSpace.enableDebug(assetManager);+ +Wireframe for Animations
++ ++ +Making the skeleton visible inside animated models can be handy for debugging animations. The
+control
object is an AnimControl,player
is the loaded model. +SkeletonDebugger skeletonDebug = new SkeletonDebugger("skeleton", control.getSkeleton()); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", ColorRGBA.Green); mat.getAdditionalRenderState().setDepthTest(false); skeletonDebug.setMaterial(mat); - player.attachChild(skeletonDebug);Example: Toggle Wireframe on Model
+ + + +We assume that you have loaded a model with a material
mat
.Then you can add a switch to toggle the model's wireframe on and off, like this:
Create a key input trigger that switches between the two materials: E.g. we toggle when the T key is pressed:inputManager.addMapping("toggle wireframe", new KeyTrigger(KeyInput.KEY_T)); - inputManager.addListener(actionListener, "toggle wireframe"); Now add the toggle action to the action listener+ +private ActionListener() { + player.attachChild(skeletonDebug);+ +Example: Toggle Wireframe on Model
++ +- \ No newline at end of file + }; ++ +We assume that you have loaded a model with a material
+ +mat
. ++Then you can add a switch to toggle the model's wireframe on and off, like this: + +
++
- +
Create a key input trigger that switches between the two materials: E.g. we toggle when the T key is pressed:+inputManager.addMapping("toggle wireframe", new KeyTrigger(KeyInput.KEY_T)); + inputManager.addListener(actionListener, "toggle wireframe");+ Now add the toggle action to the action listenerprivate ActionListener() { @Override public void onAction(String name, boolean pressed, float tpf) { // toggle wireframe if (name.equals("toggle wireframe") && !pressed) { wireframe = !wireframe; // toggle boolean - mat.getAdditionalRenderState().setWireframe(wireframe); + mat.getAdditionalRenderState().setWireframe(wireframe); } // else ... other input tests. } - };
jME3 supports various post-rendering and particle effects. This list contains screenshots and sample code that demonstrates how to add the effect to a scene.
jme3/src/test/jme3test/post/TestSSAO.java
jme3/src/test/jme3test/post/TestSSAO2.java
jme3/src/test/jme3test/post/TestTransparentSSAO.java
jme3/src/test/jme3test/post/TestFog.java (temporary workaround, will be deprecated)
jme3/src/test/jme3test/light/TestSimpleLighting.java
jme3/src/test/jme3test/light/TestLightRadius.java
jme3/src/test/jme3test/light/TestManyLights.java
jme3/src/test/jme3test/light/TestShadow.java
jme3/src/test/jme3test/light/TestPssmShadow.java = Parallel-Split Shadow Mapping (PSSM)
jme3/src/test/jme3test/effect/TestExplosionEffect.java – debris, flame, flash, shockwave, smoke, sparks
jme3/src/test/jme3test/effect/TestMovingParticle.java
Particles can have any texture, e.g. fog, leaves, meteors, snowflakes, mosquitos
See also:
+ +jME3 supports various post-rendering and particle effects. This list contains screenshots and sample code that demonstrates how to add the effect to a scene. +
+ ++ +
+ ++ +
+ ++ + +
++ +
++ +
+ ++See also: +
+
+TerrainGrid is an extension built on top of the TerraMonkey tools like TerrainQuad and HeightMap, that provides "infinite" Terrain paging routines.
+
+Thanks to Gábor (@anthyon) and Brent (@sploreg) for this contribution!
+
+
+The classes with source code can be found in the org.jme3.terrain.geomipmapping and org.jme3.terrain.heightmap packages. Also there are 3 tests prepared in the jme3test.terrain package: +
++TerrainGrid is made up of the TerrainGrid class, and the HeightMapGrid and TerrainGridListener interfaces. +
+
+
+
+Multiple listeners can be added to the TerrainGrid, they will be called in the order of addition, so it’s possible to have multiple changes to the material before completing the load of the tile.
+
+
+HeightMapGrid adds the possibility of loading terrain tiles on demand instead of having a simple height array. There’s no predefined way of how to store these tiles, it only takes care of loading one HeightMap object at given location at a time.
+
+
+After playing around with the terrain in jME3, soon comes the requirement of having larger explorable lands. Increasing the size of one TerrainQuad leads to more memory usage, while it will still be easy to reach the worlds boundaries. That’s why TerrainGrid was designed. It extends the TerraindQuad class and uses 4 HeightMaps (dark blue) as the four sub-quad. This means that a terrain of size 513 will use tiles of 257. Also an LRUCache is built into the terrain package, so surrounding tiles (green) can be pre-cached on a different thread, lowering the loading time. The quads are updated as the camera approaches the boundary of the light blue section. + +
+ ++The design of the TerrainGrid system was chosen carefully, so that minimal effort needs to be taken to switch from previous TerrainQuad uses. It has the same constructors with the small exception that instead of an array of heightmap it takes a HeightMapGrid instance. All other parameters are forwarded down to the underlying TerrainQuad system. +There exist also two basic HeightMapGrid implementations: +
++Further information about terrain and TerrainQuad can be found in the wiki at: +
++ +When adding multiplayer to your game, you may find that your server needs to know about game state (e.g. where are players, objects? Was that a direct hit? etc.) You can code all this up yourself, but there's an easier way. +
+ ++It's very easy to change your current (client) game to function as a server as well. +
+ ++ +A headless server… +
+simpleUpdate()
loop – you can run tests and trigger events as usual.+ +First, let's take a look at the default way of creating a new game (in its simplest form): +
+public static void main(String[] args) { + Application app = new Main(); + app.start(); +}+ +
+ +Now, with a simple change you can start your game in Headless mode. This means that all input and audio/visual output will be ignored. That's a good thing for a server. +
+import com.jme3.system.JmeContext; +import com.jme3.system.JmeContext.Type; + +public static void main(String[] args) { + Application app = new Main(); + app.start(JmeContext.Type.Headless); +}+ +
+ +Okay, so you can now start your game in a headless 'server mode', where to go from here? + +
+String[] args
from the main
-method to enable server mode on demand (e.g. start your server like java -jar mygame.jar –server
.if (servermode)
-block) (or if (!servermode)
for the client). The jMonkeyEngine3 has built-in support for jBullet physics via the com.jme3.bullet
package.
Game Physics are not only employed to calculate collisions, but they can also simulate hinges and joints. Think of pulley chains, shaky rope bridges, swinging pendulums, or (trap)door and chest hinges. Physics are a great addition to e.g. an action or puzzle game.
In this example, we will create a pendulum. The joint is the (invisible) connection between the pendulum body and the hook. You will see that you can use what you learn from the simple pendulum and apply it to other joint/hinge objects (rope bridges, etc).
hookNode
and a dynamic pendulumNode
.joint.enableMotor();
The hookNode is the fixed point from which the pendulum hangs. It has no mass.
Node hookNode=PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f( .1f, .1f, .1f)),0); + ++ +Physical Hinges and Joints
++ ++ ++ +The jMonkeyEngine3 has built-in support for via the
+ +com.jme3.bullet
package. ++Game Physics are not only employed to calculate collisions, but they can also simulate hinges and joints. Think of pulley chains, shaky rope bridges, swinging pendulums, or (trap)door and chest hinges. Physics are a great addition to e.g. an action or puzzle game. +
+ ++In this example, we will create a pendulum. The joint is the (invisible) connection between the pendulum body and the hook. You will see that you can use what you learn from the simple pendulum and apply it to other joint/hinge objects (rope bridges, etc). +
+ +Sample Code
+++ ++
+ +- +
+Overview of this Physics Application
+++ ++
+ +- +
Create a SimpleApplication with a BulletAppState++
+- +
This gives us a PhysicsSpace for PhysicsControls+- +
For the pendulum, we use a Spatial with a PhysicsControl, and we apply physical forces to them.++
+- +
The parts of the "pendulum" are Physics Control'ed Spatials with Collision Shapes.+- +
We create a fixed+hookNode
and a dynamicpendulumNode
.- +
We can "crank the handle" and rotate the joint like a hinge, or we can let loose and expose the joints freely to gravity.++
+- +
For physical forces we will use the method+joint.enableMotor();
Creating a Fixed Node
++ ++ +The hookNode is the fixed point from which the pendulum hangs. It has no mass. +
+Node hookNode=PhysicsTestHelper.createPhysicsTestNode( + assetManager, new BoxCollisionShape(new Vector3f( .1f, .1f, .1f)),0); hookNode.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f,0,0f)); rootNode.attachChild(hookNode); -getPhysicsSpace().add(hookNode);For a rope bridge, there would be two fixed nodes where the bridge is attached to the mountainside.
Creating a Dynamic Node
+ +The pendulumNode is the dynamic part of the construction. It has a mass.
Node pendulumNode=PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f( .3f, .3f, .3f)),1); +getPhysicsSpace().add(hookNode);+ ++For a rope bridge, there would be two fixed nodes where the bridge is attached to the mountainside. +
+ +Creating a Dynamic Node
++ ++ +The pendulumNode is the dynamic part of the construction. It has a mass. +
+Node pendulumNode=PhysicsTestHelper.createPhysicsTestNode( + assetManager, new BoxCollisionShape(new Vector3f( .3f, .3f, .3f)),1); pendulumNode.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f,-1,0f)); rootNode.attachChild(pendulumNode); -getPhysicsSpace().add(pendulumNode);For a rope bridge, each set of planks would be one dynamic node.
Understanding DOF, Joints, and Hinges
A PhysicsHingeJoint is an invisible connection between two nodes – here between the pendulum body and the hook. Why are hinges and joints represented by the same class? Hinges and joints have something in common: They constrain the mechanical degree of freedom (DOF) of another object.
Consider a free falling, "unchained" object in physical 3D space: It has 6 DOFs:
It translates along 3 axes It rotates around 3 axesNow consider some examples of objects with joints:
An individual chain link is free to spin and move around, but joined into a chain, the link's movement is restricted to stay with the surrounding links. A person's arm can rotate around some axes, but not around others. The shoulder joint allows one and restricts the other. A door hinge is one of the most restricted types of joint: It can only rotate around one axis.You'll understand that, when creating any type of joint, it is important to correctly specify the DOFs that the joint restricts, and the DOFs that the joint allows. For the typical DOF of a ragDoll character's limbs, jME even offers a special joint,
PhysicsConeJoint
.Creating the Joint
+ +You create the PhysicsHingeJoint after you have created the nodes that are to be chained together. In the code snippet you see that the PhysicsHingeJoint constructor requires the two node objects. You also have to specify axes and pivots – they are the degrees of freedom that you just heard about.
private HingeJoint joint; +getPhysicsSpace().add(pendulumNode);+ ++For a rope bridge, each set of planks would be one dynamic node. +
+ +Understanding DOF, Joints, and Hinges
++ ++ ++ +A PhysicsHingeJoint is an invisible connection between two nodes – here between the pendulum body and the hook. Why are hinges and joints represented by the same class? Hinges and joints have something in common: They constrain the mechanical degree of freedom (DOF) of another object. +
+ ++Consider a free falling, "unchained" object in physical 3D space: It has 6 DOFs: +
++
+ +- +
It translates along 3 axes+- +
It rotates around 3 axes++ +Now consider some examples of objects with joints: +
++
+ +- +
An individual chain link is free to spin and move around, but joined into a chain, the link's movement is restricted to stay with the surrounding links.+- +
A person's arm can rotate around some axes, but not around others. The shoulder joint allows one and restricts the other.+- +
A door hinge is one of the most restricted types of joint: It can only rotate around one axis.++ +You'll understand that, when creating any type of joint, it is important to correctly specify the DOFs that the joint restricts, and the DOFs that the joint allows. For the typical DOF of a ragDoll character's limbs, jME even offers a special joint,
+ +PhysicsConeJoint
. +Creating the Joint
++ ++ +You create the PhysicsHingeJoint after you have created the nodes that are to be chained together. In the code snippet you see that the PhysicsHingeJoint constructor requires the two node objects. You also have to specify axes and pivots – they are the degrees of freedom that you just heard about. +
+private HingeJoint joint; ... public void simpleInitApp() { ... @@ -61,26 +149,76 @@ class="level2">You create the PhysicsHingeJoint after you have created the n new Vector3f(0f, 0f, 0f), // pivot point of A new Vector3f(0f,-1f, 0f), // pivot point of B Vector3f.UNIT_Z, // DoF Axis of A (Z axis) - Vector3f.UNIT_Z ); // DoF Axis of B (Z axis)
Specify the following parameters for each joint:
PhysicsControl A and B – the two nodes that are to be joined Vector3f pivot A and pivot B – coordinates of the two attachment points
The points typically lie on the surface of the PhysicsControl's Spatials, rarely in the middle. Vector3f axisA and axisB – around which axes each node is allowed to spin.
In our example, we constrain the pendulum to swing only along the Z axis.Remember to add all joint objects to the physicsSpace, just like you would do with any physical objects.
bulletAppState.getPhysicsSpace().add(joint);Tip: If you want the joint to be visible, attach a geometry to the dynamic node, and translate it to its start position.
Apply Physical Forces
- \ No newline at end of file + Vector3f.UNIT_Z ); // DoF Axis of B (Z axis)You can apply forces to dynamic nodes (the ones that have a mass), and see how other joined ("chained") objects are dragged along.
Alternatively, you can also apply forces to the joint itself. In a game, you may want to spin an automatic revolving door, or slam a door closed in a spooky way, or dramatically open the lid of a treasure chest.
The method to call on the joint is
enableMotor()
.joint.enableMotor(true, 1, .1f); -joint.enableMotor(true, -1, .1f);
Switch the motor on by supplyingtrue
Specify the velocity with which the joint should rotate around the specified axis.
Use positive and negative numbers to change direction. Specify the impulse for this motor. Heavier masses need a bigger impulse to be moved.When you disable the motor, the chained nodes are exposed to gravity again:
joint.enableMotor(false, 0, 0);
+Specify the following parameters for each joint: +
++ +Remember to add all joint objects to the physicsSpace, just like you would do with any physical objects. +
+bulletAppState.getPhysicsSpace().add(joint);+ +
+Tip: If you want the joint to be visible, attach a geometry to the dynamic node, and translate it to its start position. +
+ ++ +You can apply forces to dynamic nodes (the ones that have a mass), and see how other joined ("chained") objects are dragged along. +
+ ++Alternatively, you can also apply forces to the joint itself. In a game, you may want to spin an automatic revolving door, or slam a door closed in a spooky way, or dramatically open the lid of a treasure chest. +
+ +
+The method to call on the joint is enableMotor()
.
+
joint.enableMotor(true, 1, .1f); +joint.enableMotor(true, -1, .1f);+
true
+ +When you disable the motor, the chained nodes are exposed to gravity again: + +
+joint.enableMotor(false, 0, 0);+ +
A HUD (Head-Up Display) is part of a game's visual user interface. It's an overlay that displays additional information as (typically) 2-dimensional text or icons on the screen, on top of the 3D scene.
HUDs are used to supply players with essential information about the game state.
Not all games have, or need a HUD. To avoid breaking the immersion and cluttering the screen, only use a HUD if it is the only way to convey certain information.
You have two options how to create HUDs.
The recommended approach to create HUDs is using Nifty GUI.
The advantage of Nifty GUI is that it is well integrated into jME and the jMonkeyPlatform, and that it offers all the features that you expect from a professional modern user interface. The only small disadvantage is that you (currently still) have to lay out the interface in XML. You can see this as an advantage too, as it enables you to edit the user interface without editing the code afterwards.
For HUDs, you Basically follow the same instructions as for creating a normal Nifty GUI, you just don't pause the game while the HUD is up.
Using the GUI Node is the default approach in jme3 to create very simple, static HUDs. If you just quickly want to display a line of text, or a simple icon on the screen, use this no-frills method. If you want a more advanced HUD with effects and interaction, use Nifty GUI.
Next to the rootNode for the 3-dimensional scene graph, jME3 also offers a 2-dimension (orthogonal) node, the guiNode
. This is how you use it for HUDs:
setQueueBucket(Bucket.Gui)
.The element appears as 2-D, static element on the screen.
By default, the guiNode has some scene graph statistics attached in SimpleApplication. To clear the guiNode and attach your own GUI elements, you detach all children.
guiNode.detachAllChildren();
A simple image can be displayed using com.jme3.ui.Picture
.
Picture pic = new Picture("HUD Picture"); + ++ +Head-Up Display (HUD)
++ ++ ++ + +
+ ++A HUD (Head-Up Display) is part of a game's visual user interface. It's an overlay that displays additional information as (typically) 2-dimensional text or icons on the screen, on top of the 3D scene. Not all games have, or need a HUD. To avoid breaking the immersion and cluttering the screen, only use a HUD if it is the only way to convey certain information. +
+ ++HUDs are used to supply players with essential information about the game state. +
++
+ +- +
Status: Score, minimap, points, stealth mode, …+- +
Resources: Ammunition, lives/health, time, …+- +
Vehicle instruments: Cockpit, speedometer, …+- +
Navigational aides: Crosshairs, mouse pointer or hand, …++ +You have two options how to create HUDs. + +
++ ++
+ +Option Pros Cons ++ +Attach elements to default guiNode: Easy to learn. jMonkeyEngine built-in API for attaching images and bitmap text. Only basic features. +
+You will have to write custom controls / buttons / effects if you need them.+ +Use advanced Nifty GUI integration: Full-featured interactive user interface.
+Includes buttons, effects, controls.
+Supports XML and Java layouts.Steeper learning curve. +Simple HUD: GUI Node
++ ++ ++ +Using the GUI Node is the default approach in jme3 to create simple HUDs. If you just quickly want to display a line of text, or a simple icon on the screen, use this no-frills method. +
+ ++Next to the rootNode for the 3-dimensional scene graph, jME3 also offers a 2-dimension (orthogonal) node, the
+ +guiNode
. ++This is how you use the guiNode for HUDs: +
++
+ +- +
Create a GUI element: BitmapText or Picture.+- +
Attach the element to the guiNode.+- +
Place the element in the orthogonal render queue using+setQueueBucket(Bucket.Gui)
.+The BitmapTexts and Pictures appear as 2 dimensional element on the screen. +
+ ++Note: The size unit for the guiNode is pixels, not world units. +
+ ++By default, the guiNode has some scene graph statistics attached in SimpleApplication. To clear the guiNode and attach your own GUI elements, you detach all children. +
+guiNode.detachAllChildren();+ +Displaying Pictures in the HUD
++ ++ +A simple image can be displayed using
+com.jme3.ui.Picture
. +Picture pic = new Picture("HUD Picture"); pic.setImage(assetManager, "Textures/ColoredTex/Monkey.png", true); pic.setWidth(settings.getWidth()/2); pic.setHeight(settings.getHeight()/2); pic.setPosition(settings.getWidth()/4, settings.getHeight()/4); -guiNode.attachChild(pic);When you set the last boolean in setImage() to true, the alpha channel of your image will be rendered transparent/translucent.
Displaying Text in the HUD
+ +You use
com.jme3.font.BitmapText
to display text on the screen.BitmapText hudText = new BitmapText(guiFont, false); +guiNode.attachChild(pic);+ ++When you set the last boolean in setImage() to true, the alpha channel of your image is rendered transparent/translucent. +
+ +Displaying Text in the HUD
++ ++ +You use
+com.jme3.font.BitmapText
to display text on the screen. +BitmapText hudText = new BitmapText(guiFont, false); hudText.setSize(guiFont.getCharSet().getRenderedSize()); // font size hudText.setColor(ColorRGBA.Blue); // font color hudText.setText("You can write any string here"); // the text hudText.setLocalTranslation(300, hudText.getLineHeight(), 0); // position -guiNode.attachChild(hudText);The BitmapFont object
guiFont
is a default font provided by SimpleApplication. Copy you own fonts as .fnt+.png files into theassets/Interface/Fonts
directory and load them like this:BitmapFont myFont = assetManager.loadFont("Interface/Fonts/Console.fnt"); -hudText = new BitmapText(myFont, false);Displaying Geometries in the HUD
It is technically possible to attach Quads and 3D Geometries to the HUD. They show up as flat, static GUI elements. Note that if you use a lit Material, you must add a light to the guiNode. Also remember that size units are pixels in the HUD (a 2-wu cube is displayed 2 pixels wide).
Positioning HUD Elements
When positioning text and images in 2D, the bottom left corner of the screen is(0f,0f)
, and the top right corner is at(settings.getWidth(),settings.getHeight())
. If you have several 2D elements in the GUI bucket that overlap, define their depth order by specifing a Z value. You can usepic.move(x, y, -2)
orhudText.setLocalTranslation(x,y,-2)
. Size and length values in the orthogonal render queue are treated like pixels. A 20*20-wu big quad is rendered 20 pixels wide.Keeping the HUD Up-To-Date
+ +Use the update loop to keep the content up-to-date.
public void simpleUpdate(float tpf) { +guiNode.attachChild(hudText);+ ++The BitmapFont object
+guiFont
is a default font provided by SimpleApplication. Copy you own fonts as .fnt plus .png files into theassets/Interface/Fonts
directory and load them like this: +BitmapFont myFont = assetManager.loadFont("Interface/Fonts/Console.fnt"); +hudText = new BitmapText(myFont, false);+ +Displaying Geometries in the HUD
++ ++ ++ +It is technically possible to attach Quads and 3D Geometries to the HUD. They show up as flat, static GUI elements. Note that if you use a lit Material, you must add a light to the guiNode. Also remember that size units are pixels in the HUD (a 2-wu cube is displayed tiny 2 pixels wide!). +
+ +Positioning HUD Elements
+++ ++
+ +- +
When positioning text and images in 2D, the bottom left corner of the screen is+(0f,0f)
, and the top right corner is at(settings.getWidth(),settings.getHeight())
.- +
If you have several 2D elements in the GUI bucket that overlap, define their depth order by specifing a Z value. You can use+pic.move(x, y, -2)
orhudText.setLocalTranslation(x,y,-2)
.- +
Size and length values in the orthogonal render queue are treated like pixels. A 20*20-wu big quad is rendered 20 pixels wide.+Keeping the HUD Up-To-Date
++ +- \ No newline at end of file +}+ +Use the update loop to keep the content up-to-date. +
+public void simpleUpdate(float tpf) { ... hudText.setText("Score: " + score); ... picture.setImage(assetManager, "Interface/statechange.png", true); ... -}
+ +The recommended approach to create HUDs is using Nifty GUI. +
++ +The advantage of Nifty GUI is that it is well integrated into jME and the jMonkeyPlatform, and that it offers all the features that you expect from a professional modern user interface. +
+ ++For HUDs, you basically follow the same instructions as for creating a normal Nifty GUI, you just don't pause the game while the HUD is up. +
+ Users interact with your jME3 application with different input devices – the mouse, the keyboard, or a joystick. To respond to inputs we use the inputManager
object in SimpleApplication
.
This is how you add interaction to your game:
Choose one or several key/mouse events for the interaction. We use KeyTrigger
, MouseAxisTrigger
, MouseButtonTrigger
, JoyAxisTrigger
and JoyButtonTrigger
constants from the com.jme3.input.controls
package.
The booleans are used to negate the axes: For inputs that have two axes (MouseAxis, JoyAxis), you have to listen to the negative (true) and positive (false) axis separately.
Trigger | Code |
---|---|
Mouse button: Left Click | MouseButtonTrigger(MouseInput.BUTTON_LEFT) |
Mouse button: Right Click | MouseButtonTrigger(MouseInput.BUTTON_RIGHT) |
Mouse button: Middle Click | MouseButtonTrigger(MouseInput.BUTTON_MIDDLE) |
Mouse movement: | MouseAxisTrigger(MouseInput.AXIS_X, true), MouseAxisTrigger(MouseInput.AXIS_Y, true), MouseAxisTrigger(MouseInput.AXIS_X, false), MouseAxisTrigger(MouseInput.AXIS_Y, false) |
Mouse wheel: | MouseAxisTrigger(MouseInput.AXIS_WHEEL,false) MouseAxisTrigger(MouseInput.AXIS_WHEEL,true) |
Keyboard: Characters and Numbers etc | KeyTrigger(KeyInput.KEY_X) etc |
Keyboard: Spacebar | KeyTrigger(KeyInput.KEY_SPACE) |
Keyboard: Shift | KeyTrigger(KeyInput.KEY_RSHIFT), KeyTrigger(KeyInput.KEY_LSHIFT) |
Keyboard: F1 etc | KeyTrigger(KeyInput.KEY_F1) etc |
Keyboard: Return, Enter | KeyTrigger(KeyInput.KEY_RETURN), KeyTrigger(KeyInput.KEY_NUMPADENTER) |
Keyboard: PageUp, PageDown | KeyTrigger(KeyInput.KEY_PGUP), KeyTrigger(KeyInput.KEY_PGDN) |
Keyboard: Delete, Backspace | KeyTrigger(KeyInput.KEY_BACK), KeyTrigger(KeyInput.KEY_DELETE) |
Keyboard: Escape | KeyTrigger(KeyInput.KEY_ESCAPE) |
Keyboard: Arrows | KeyTrigger(KeyInput.KEY_DOWN), KeyTrigger(KeyInput.KEY_UP) KeyTrigger(KeyInput.KEY_LEFT), KeyTrigger(KeyInput.KEY_RIGHT) |
NumPad: Number 1 etc | KeyTrigger(KeyInput.KEY_NUMPAD1) etc |
Joystick: Button | JoyButtonTrigger(0, JoyInput.AXIS_POV_X), JoyButtonTrigger(0, JoyInput.AXIS_POV_Y) ? |
Joystick: Movement | JoyAxisTrigger(0, JoyInput.AXIS_POV_X, true), JoyAxisTrigger(0, JoyInput.AXIS_POV_X, false), JoyAxisTrigger(0, JoyInput.AXIS_POV_Z, true), JoyAxisTrigger(0, JoyInput.AXIS_POV_Z, false) |
When initializing the application, add a Mapping for each Trigger.
Give the mapping a meaningful name. The name should reflect the action, not the key, since the keys can change. Here some examples:
inputManager.addMapping("Pause Game", new KeyTrigger(KeyInput.KEY_P)); -inputManager.addMapping("Rotate", new KeyTrigger(KeyInput.KEY_SPACE));
There are cases where you may want more then one trigger for one action. For instance some users prefer the WASD keys to navigate, others prefer the arrow keys. You can define both by adding them after a comma.
inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A), - new KeyTrigger(KeyInput.KEY_LEFT)); -inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D), - new KeyTrigger(KeyInput.KEY_RIGHT));
The jME3 input manager supports two types of event listeners for inputs:
com.jme3.input.controls.AnalogListener
com.jme3.input.controls.ActionListener
You can use one or both in the same application. Add one or both of these code snippets to your main SimpleApplication-based class to activate the listener.
private ActionListener() { + ++ +Input Handling
++ ++ ++ +Users interact with your jME3 application with different input devices – the mouse, the keyboard, or a joystick. To respond to inputs we use the
+ +inputManager
object inSimpleApplication
. ++This is how you add interaction to your game: + +
++
+ +- +
For each action, choose the trigger(s) (a key or mouse click etc)+- +
For each action, add a trigger mapping to the inputManager+- +
Create at least one listener in SimpleApplication+- +
For each action, register its mappings to a listener+- +
Implement each action in the listener+1. Choose Trigger
++ ++ ++ +Choose one or several key/mouse events for the interaction. We use
+ +KeyTrigger
,MouseAxisTrigger
,MouseButtonTrigger
,JoyAxisTrigger
andJoyButtonTrigger
constants from thecom.jme3.input.controls
package. ++Note: The MouseAxis and JoyAxis triggers go along the X axis (right/left) or Y axis (up/down). These Triggers come with extra booleans for the negative half of the axis (left, down). Remember to write code that listens to the negative (true) and positive (false) axis! + +
++ ++
+ +Trigger Code ++ +Mouse button: Left Click MouseButtonTrigger(MouseInput.BUTTON_LEFT) ++ +Mouse button: Right Click MouseButtonTrigger(MouseInput.BUTTON_RIGHT) ++ +Mouse button: Middle Click MouseButtonTrigger(MouseInput.BUTTON_MIDDLE) ++ +Mouse movement: Right MouseAxisTrigger(MouseInput.AXIS_X, true) ++ +Mouse movement: Left MouseAxisTrigger(MouseInput.AXIS_X, false) ++ +Mouse movement: Up MouseAxisTrigger(MouseInput.AXIS_Y, true) ++ +Mouse movement: Down MouseAxisTrigger(MouseInput.AXIS_Y, false) ++ +Mouse wheel: Up MouseAxisTrigger(MouseInput.AXIS_WHEEL,false) ++ +Mouse wheel: Down MouseAxisTrigger(MouseInput.AXIS_WHEEL,true) ++ +NumPad: 1, 2, 3, … KeyTrigger(KeyInput.KEY_NUMPAD1) … ++ +Keyboard: 1, 2 , 3, … KeyTrigger(KeyInput.KEY_1) … ++ +Keyboard: A, B, C, … KeyTrigger(KeyInput.KEY_A) … ++ +Keyboard: Spacebar KeyTrigger(KeyInput.KEY_SPACE) ++ +Keyboard: Shift KeyTrigger(KeyInput.KEY_RSHIFT), +
+KeyTrigger(KeyInput.KEY_LSHIFT)+ +Keyboard: F1, F2, … KeyTrigger(KeyInput.KEY_F1) … ++ +Keyboard: Return, Enter KeyTrigger(KeyInput.KEY_RETURN), +
+KeyTrigger(KeyInput.KEY_NUMPADENTER)+ +Keyboard: PageUp, PageDown KeyTrigger(KeyInput.KEY_PGUP), +
+KeyTrigger(KeyInput.KEY_PGDN)+ +Keyboard: Delete, Backspace KeyTrigger(KeyInput.KEY_BACK), +
+KeyTrigger(KeyInput.KEY_DELETE)+ +Keyboard: Escape KeyTrigger(KeyInput.KEY_ESCAPE) ++ +Keyboard: Arrows KeyTrigger(KeyInput.KEY_DOWN), +
+KeyTrigger(KeyInput.KEY_UP)
+KeyTrigger(KeyInput.KEY_LEFT), KeyTrigger(KeyInput.KEY_RIGHT)+ +Joystick Button: JoyButtonTrigger(0, JoyInput.AXIS_POV_X), +
+JoyButtonTrigger(0, JoyInput.AXIS_POV_Y) ?+ +Joystick Movement: Right JoyAxisTrigger(0, JoyInput.AXIS_POV_X, true) ++ +Joystick Movement: Left JoyAxisTrigger(0, JoyInput.AXIS_POV_X, false) ++ +Joystick Movement: Forward JoyAxisTrigger(0, JoyInput.AXIS_POV_Z, true) ++ +Joystick Movement: Backward JoyAxisTrigger(0, JoyInput.AXIS_POV_Z, false) ++ +In your IDE, use code completion to quickly look up Trigger literals. In the jMonkeyPlatform for example, press ctrl-space or ctrl-/ after
+ +KeyInput.|
to choose from the list of all keys. +2. Add a Trigger Mapping
++ ++ ++ +When initializing the application, add a Mapping for each Trigger. +
+ ++Give the mapping a meaningful name. The name should reflect the action, not the button/key (because buttons/keys can change). Here some examples: +
+inputManager.addMapping("Pause Game", new KeyTrigger(KeyInput.KEY_P)); +inputManager.addMapping("Rotate", new KeyTrigger(KeyInput.KEY_SPACE)); +...+ ++There are cases where you may want to provide more then one trigger for one action. For example, some users prefer the WASD keys to navigate, while others prefer the arrow keys. Add several triggers for one mapping, by separating the Trigger objects with commas: +
+inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A), + new KeyTrigger(KeyInput.KEY_LEFT)); // A and left arrow +inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D), + new KeyTrigger(KeyInput.KEY_RIGHT)); // D and right arrow + ...+ +3. Create Listeners
++ ++ ++ +The jME3 input manager supports two types of event listeners for inputs: AnalogListener and ActionListener. You can use one or both listeners in the same application. Add one or both of the following code snippets to your main SimpleApplication-based class to activate the listeners. +
+ ++Note: The two input listeners do not know, and do not care, which actual key was pressed. They only know which named input mapping was triggered. +
+ +ActionListener
++ ++ ++ +
+com.jme3.input.controls.ActionListener
++
+- +
Use for absolute "button pressed or released?", "on or off?" actions.++
+- +
Examples: Pause/unpause, a rifle or revolver shot, jump, click to select.+- +
JME gives you access to:++
+- +
The mapping name of the triggered action.+- +
A boolean whether the trigger is still pressed or has just been released.+- +
A float of the current time-per-frame as timing factor+- +
+private ActionListener() { public void onAction(String name, boolean keyPressed, float tpf) { - /** TODO */ + /** TODO: test for mapping names and implement actions */ } -};private AnalogListener analogListener = new AnalogListener() { +};+ +AnalogListener
++ ++ +
+com.jme3.input.controls.AnalogListener
++
+- +
Use for continuous and gradual actions.++
+- +
Examples: Walk, run, rotate, accelerate vehicle, strafe, (semi-)automatic weapon shot+- +
JME gives you access to:++
+- +
The mapping name of the triggered action.+- +
A gradual float value between how long the trigger has been pressed.+- +
A float of the current time-per-frame as timing factor+private AnalogListener analogListener = new AnalogListener() { public void onAnalog(String name, float keyPressed, float tpf) { - /** TODO */ + /** TODO: test for mapping names and implement actions */ } -};4. Register Mappings to Listeners
To activate the mappings, you must register them to the Listener. Write your registration code after the part where you have added the mappings to the inputManager before.
In this example, we register the "Pause Game" mapping to the
actionListener
object, because pausing a game is in "either/or" decision.inputManager.addListener(actionListener, new String[]{"Left", "Right"});As you see, you can add several listeners in one String array. You can call the addListener() method more than once, each time with a subset of your list, if that helps you keep you code tidy.
Tip: Check the string's capitalization and spelling if you think you have registered an action, but it does not work.
5. Implement Actions
+ +You specify the action to be triggered where it says TODO in the Listener code snippets. -Typically you write a series of if/else conditions, testing for all the mapping names, and specifying each action. Here is one example:
private AnalogListener analogListener = new AnalogListener() { +};+ +4. Register Mappings to Listeners
++ ++ ++ +To activate the mappings, you must register them to a Listener. Write your registration code after the code block where you have added the mappings to the inputManager. +
+ ++In the following example, you register the "Pause Game" mapping to the
+actionListener
object, because pausing a game is in "either/or" decision. +inputManager.addListener(actionListener, new String[]{"Pause Game"});+ ++In the following example, you register navigational mappings to the
+analogListener
object, because walking is a continuous action. Players typically keep the key pressed to express continuity, for example when they want to "walk on" or "accelerate". +inputManager.addListener(analogListener, new String[]{"Left", "Right"});+ ++As you see, you can add several listeners in one String array. You can call the addListener() method more than once, each time with a subset of your list, if that helps you keep you code tidy. Again, the Listeners do not care about actual which keys are configured, you only register named trigger mappings. +
+ ++
Did you register an action, but it does not work? Check the string's capitalization and spelling, it's case sensitive! ++ + +5. Implement Actions in Listeners
++ ++ ++ +You specify the action to be triggered where it says TODO in the Listener code snippets. Typically, you write a series of if/else conditions, testing for all the mapping names, and then calling the respective action. +
+ ++Make use of the distinction between
+if
andelse if
in this conditional. ++
+ +- +
If several actions can be triggered simultaneously, test for all of these with a series of bare+if
s. For example, a character can be running forward and to the left.- +
If certain actions exclude one another, test for them with+else if
, the the rest of the exclusive tests can be skipped and you save some miliseconds. For example, you either shoot or pick something up.ActionListener
++ ++ ++ +In the most common case, you want an action to be triggered once, in the moment when the button or key trigger is released. For example, when the player presses a key to open a door, or clicks to pick up an item. For these cases, use an ActionListener and test for
+&& !keyPressed
, like shown in the following example. +private ActionListener() { + public void onAction(String name, boolean keyPressed, float tpf) { + + if (name.equals("Pause Game") && !keyPressed) { // test? + isRunning = !isRunning; // action! + } + + if ... + + } + };+ +AnalogListener
++ ++ +The following example shows how you define actions with an AnalogListener. Thiese actions are triggered continuously, as long (intensity
+value
) as the named key or mouse button is down. Use this listeners for semi-automatic weapons and navigational actions. +private AnalogListener analogListener = new AnalogListener() { public void onAnalog(String name, float value, float tpf) { if (name.equals("Rotate")) { // test? player.rotate(0, value*speed, 0); // action! - } // else if ... - - } - };It's very common that you want an action to be only triggered once, in the moment when the key is released. Examples are when the player presses an action key to open a door or pick up an item, or to flip a game state, such as pause/unpause. For these cases, use an ActionListener and test for
&& !keyPressed
, like shown in the following example.private ActionListener() { - public void onAction(String name, boolean keyPressed, float tpf) { + } - if (name.equals("Pause Game") && !keyPressed) { // test? - isRunning = !isRunning; // action! - } // else if ... + if ... } - };Remapping Keys
- \ No newline at end of file + };This approach of separating triggers from actions has the advantage that you can remap triggers easily. Maybe your players have different keyboard layouts, are used to "reversed" mouse navigation, or prefer different navigational keys than the ones you defined. In any case, you only need to replace the trigger parts in the
inputManager.addMapping()
lines with variables, and load different sets of trigger objects when the game starts. The rest of the code stays as it is.
+
+It is likely that your players have different keyboard layouts, are used to "reversed" mouse navigation, or prefer different navigational keys than the ones that you defined. You should create an options screen that lets users customize their mouse/key triggers for your mappings. Replace the trigger literals in the inputManager.addMapping()
lines with variables, and load sets of triggers when the game starts.
+
+The abstraction of separating triggers and mappings has the advantage that you can remap triggers easily. Your code only needs to remove and add some trigger mappings. The core of the code (the listeners and actions) remains unchanged. +
+In the Material Definitions article you learned how to configure Materials programmatically in Java code. If you have certain commonly used Materials that never change, you can clean up the amount of Java code that clutters your init method, by moving material settings into .j3m files. Then later in your code, you only need to call one setter instead of several to apply the material.
SimpleBump.j3m
assets/Materials/
directory, e.g. MyGame/src/assets/Materials/SimpleBump.j3m
Material shiny bumpy rock : Common/MatDefs/Light/Lighting.j3md { + +Saving and Loading Materials with .j3m Files
++ ++ ++ +In the Material Definitions article you learned how to configure Materials programmatically in Java code. If you have certain commonly used Materials that never change, you can clean up the amount of Java code that clutters your init method, by moving material settings into .j3m files. Then later in your code, you only need to call one setter instead of several to apply the material. +
+ +Writing the .j3m File
+++
- +
For every Material, create a file and give it a name that describes it: e.g.+SimpleBump.j3m
- +
Place the file in your project's+assets/Materials/
directory, e.g.MyGame/src/assets/Materials/SimpleBump.j3m
Edit the file and add content using the following Syntax, e.g.:Material shiny bumpy rock : Common/MatDefs/Light/Lighting.j3md { MaterialParameters { Shininess: 8.0 NormalMap: Textures/bump_rock_normal.png @@ -19,190 +25,241 @@ class="li"> Edit the file and add content using the following Syntax, e.g.:Diffuse : 1.0 1.0 1.0 1.0 Specular : 0.0 0.0 0.0 1.0 } -}How to this file is structured:
Header
Material
is a fixed keyword, keep it.shiny bumpy rock
is a descriptive string that you can make up. Choose a name to help you remember for what you intend to use this material. After the colon, specify on which Material definition you base this Material. Now look up the choosen Material Definition's parameters and their parameter types from the Material table. Add one line for each parameter.
For example: The series of four numbers in the example above represent RGBA color values. Check the detailed syntax reference below if you are unsure.Tip: In the jMonkeyPlatform, use File>New File>Material>Empty Material File to create .j3m files.
How to Use .j3m Materials
This is how you use the prepared .j3m Material on a Spatial. Since you have saved the .j3m file to your project's Assets directory, the .j3m path is relative to
MyGame/src/assets/…
.myGeometry.setMaterial(assetManager.loadAsset("Materials/SimpleBump.j3m"));Tip: In the jMonkeyPlatform, open Windows>Palette and drag the
JME Material: Set J3M
snippet into your code.Syntax Reference for .j3m Files
Paths
Make sure to get the paths to the textures (.png, .jpg) and material definitions (.j3md) right.
The paths to the built-in .j3md files are relative to jME3's Core Data directory. Just copy the path stated in the Material table.
Common/MatDefs/Misc/Unshaded.j3md
is resolved tojme3/src/src/core-data/Common/MatDefs/Misc/Unshaded.j3md
. The paths to your textures are relative to your project's assets directory.
Textures/bump_rock_normal.png
is resolved toMyGame/src/assets/Textures/bump_rock_normal.png
Data Types
All data types (except Color) are specified in com.jme3.shader.VarType. -"Color" is specified as Vector4 in J3MLoader.java.
Name jME Java class .j3m file syntax Float a float (e.g. 0.72) , no comma or parentheses Vector2 com.jme3.math.Vector2f
Two floats, no comma or parentheses Vector3 com.jme3.math.Vector3f
Three floats, no comma or parentheses Texture2D com.jme3.texture.Texture
Path to texture in assets
directory, no quotation marksBoolean (basic Java type) true
orfalse
Int (basic Java type) Integer number, no comma or parentheses Color com.jme3.math.ColorRGBA
Four floats, no comma or parentheses Vector4 FloatArray Vector2Array Vector3Array Vector4Array Matrix3 Matrix4 Matrix3Array Matrix4Array TextureBuffer Texture3D TextureArray TextureCubeMap Flip and Repeat Syntax
A texture can be flipped using the following syntaxNormalMap: Flip Textures/bump_rock_normal.png
A texture can be set to repeat using the following syntaxNormalMap: Repeat Textures/bump_rock_normal.png
If a texture is set to both being flipped and repeated, Flip must come before RepeatSyntax for Additional Render States
A Boolean can be "On" or "Off" Float is "123.0" etc Enum - values depend on the enum
Name Type Purpose Wireframe (Boolean) FaceCull (Enum: FaceCullMode) DepthWrite (Boolean) DepthTest (Boolean) Blend (Enum: BlendMode) AlphaTestFalloff (Float) PolyOffset (Float, Float) ColorWrite (Boolean) PointSprite (Boolean) Examples
Example 1: Shiny
+ + + +Spatial signpost = (Spatial) assetManager.loadAsset( +}++ +How to this file is structured: +
++
+ +- +
Header++
+- +
+Material
is a fixed keyword, keep it.- +
+shiny bumpy rock
is a descriptive string that you can make up. Choose a name to help you remember for what you intend to use this material.- +
After the colon, specify on which Material definition you base this Material.+- +
Now look up the choosen Material Definition's parameters and their parameter types from the Material table. Add one line for each parameter.++
+- +
For example: The series of four numbers in the example above represent RGBA color values.+- +
Check the detailed syntax reference below if you are unsure.++ +Tip: In the jMonkeyPlatform, use File>New File>Material>Empty Material File to create .j3m files. +
+ +
+
+This is how you use the prepared .j3m Material on a Spatial. Since you have saved the .j3m file to your project's Assets directory, the .j3m path is relative to MyGame/src/assets/…
.
+
myGeometry.setMaterial(assetManager.loadAsset("Materials/SimpleBump.j3m"));+ +
+Tip: In the jMonkeyPlatform, open Windows>Palette and drag the JME Material: Set J3M
snippet into your code.
+
+ +Make sure to get the paths to the textures (.png, .jpg) and material definitions (.j3md) right. + +
+Common/MatDefs/Misc/Unshaded.j3md
is resolved to jme3/src/src/core-data/Common/MatDefs/Misc/Unshaded.j3md
.Textures/bump_rock_normal.png
is resolved to MyGame/src/assets/Textures/bump_rock_normal.png
+ +All data types (except Color) are specified in com.jme3.shader.VarType. +"Color" is specified as Vector4 in J3MLoader.java. + +
+Name | jME Java class | .j3m file syntax | +
---|---|---|
Float | (basic Java type) | a float (e.g. 0.72) , no comma or parentheses | +
Vector2 | com.jme3.math.Vector2f | Two floats, no comma or parentheses | +
Vector3 | com.jme3.math.Vector3f | Three floats, no comma or parentheses | +
Vector4 | com.jme3.math.Vector4f | Four floats, no comma or parentheses | +
Texture2D | com.jme3.texture.Texture2D | Path to texture in assets directory, no quotation marks |
+
Texture3D | com.jme3.texture.Texture3D | Same as texture 2D except it is interpreted as a 3D texture | +
TextureCubeMap | com.jme3.texture.TextureCubeMap | Same as texture 2D except it is interpreted as a cubemap texture | +
Boolean | (basic Java type) | true or false |
+
Int | (basic Java type) | Integer number, no comma or parentheses | +
Color | com.jme3.math.ColorRGBA | Four floats, no comma or parentheses | +
FloatArray | (Currently not supported in J3M) | +|
Vector2Array | (Currently not supported in J3M) | +|
Vector3Array | (Currently not supported in J3M) | +|
Vector4Array | (Currently not supported in J3M) | +|
Matrix3 | (Currently not supported in J3M) | +|
Matrix4 | (Currently not supported in J3M) | +|
Matrix3Array | (Currently not supported in J3M) | +|
Matrix4Array | (Currently not supported in J3M) | +|
TextureBuffer | (Currently not supported in J3M) | +|
TextureArray | (Currently not supported in J3M) | +
NormalMap: Flip Textures/bump_rock_normal.png
NormalMap: Repeat Textures/bump_rock_normal.png
+ +See the javadoc for a detailed explanation of render states. + +
+Name | Type | Purpose | +
---|---|---|
(Boolean) | Enable wireframe rendering mode | +|
(Enum: FaceCullMode) | Set face culling mode (Off, Front, Back, FrontAndBack) | +|
(Boolean) | Enable writing depth to the depth buffer | +|
(Boolean) | Enable depth testing | +|
(Enum: BlendMode) | Set the blending mode | +|
(Float) | Set the alpha testing alpha falloff value (if set, it will enable alpha testing) | +|
(Float, Float) | Set the polygon offset factor and units | +|
(Boolean) | Enable color writing | +|
(Boolean) | Enable point sprite rendering for point meshes | +
Spatial signpost = (Spatial) assetManager.loadAsset( new OgreMeshKey("Models/Sign Post/Sign Post.mesh.xml", null)); signpost.setMaterial( (Material) assetManager.loadAsset( new AssetKey("Models/Sign Post/Sign Post.j3m"))); TangentBinormalGenerator.generate(signpost); -rootNode.attachChild(signpost);
The file assets/Models/Sign Post/Sign Post.j3m
contains:
Material Signpost : Common/MatDefs/Light/Lighting.j3md { +rootNode.attachChild(signpost);+ +
+The file assets/Models/Sign Post/Sign Post.j3m
contains:
+
Material Signpost : Common/MatDefs/Light/Lighting.j3md { MaterialParameters { Shininess: 4.0 DiffuseMap: Models/Sign Post/Sign Post.jpg @@ -213,24 +270,50 @@ rootNode.attachChild(signpost);
The file The JPG files are in the same directory, assets/Models/Si
Diffuse : 1.0 1.0 1.0 1.0
Specular : 1.0 1.0 1.0 1.0
}
-}
assets/Models/Sign Post/…
.
Material mat = assetManager.loadMaterial( +}+ +
+The JPG files are in the same directory, assets/Models/Sign Post/…
.
+
Material mat = assetManager.loadMaterial( "Textures/Terrain/Pond/Pond.j3m"); mat.setColor("Ambient", ColorRGBA.DarkGray); mat.setColor("Diffuse", ColorRGBA.White); -mat.setBoolean("UseMaterialColors", true);
The file assets/Textures/Terrain/Pond/Pond.j3m
contains:
Material Pong Rock : Common/MatDefs/Light/Lighting.j3md { +mat.setBoolean("UseMaterialColors", true);+ +
+The file assets/Textures/Terrain/Pond/Pond.j3m
contains:
+
Material Pong Rock : Common/MatDefs/Light/Lighting.j3md { MaterialParameters { Shininess: 8.0 DiffuseMap: Repeat Textures/Terrain/Pond/Pond.png NormalMap: Repeat Textures/Terrain/Pond/Pond_normal.png } -}
The PNG files are in the same directory, assets/Textures/Terrain/Pond/
The file assets/Models/Tree/Leaves.j3m
contains:
Material Leaves : Common/MatDefs/Light/Lighting.j3md { +}+ +
+The PNG files are in the same directory, assets/Textures/Terrain/Pond/
+
+
+The file assets/Models/Tree/Leaves.j3m
contains:
+
Material Leaves : Common/MatDefs/Light/Lighting.j3md { + Transparent On + MaterialParameters { DiffuseMap : Models/Tree/Leaves.png UseAlpha : true @@ -246,6 +329,11 @@ class="level3">The file
assets/Models/Tree/Leaves.j3m
contains: AlphaTestFalloff 0.50 FaceCull Off } -}
The PNG file is in the same directory, assets/Models/Tree/…
+The PNG file is in the same directory, assets/Models/Tree/…
+
Shaders are sets of instructions that are executed on the GPU. They are used to take advantage of hardware acceleration available on the GPU for rendering purposes.
This paper only covers Vertex and Fragment shaders because they are the only ones supported by JME3 for the moment. But be aware that there are some other types of shaders (geometry, tessellation,…).
There are multiple frequently used languages that you may encounter to code shaders but as JME3 is based on OpenGL, shaders in JME use GLSL and any example in this paper will be written in GLSL.
To keep it Simple: The Vertex shader is executed once for each vertex in the view, then the Fragment shader (also called the Pixel shader) is executed once for each pixel on the screen.
The main purpose of the Vertex shader is to compute the screen coordinate of a vertex (where this vertex will be displayed on screen) while the main purpose of the Fragment shader is to compute the color of a pixel.
This is a very simplified graphic to describe the call stack:
The main program sends mesh data to the vertex shader (vertex position in object space, normals, tangents, etc..). The vertex shader computes the screen position of the vertex and sends it to the Fragment shader. The fragment shader computes the color, and the result is displayed on screen or in a texture.
There are different types of scope for variables in a shader :
There is a large panel of variable types to be used, for more information about it I recommend reading the GLSL specification here.
To understand the coming example you must know about the different spaces in 3D computer graphics, and the matrices used to translate coordinate from one space to another.
The engine passes the object space coordinates to the vertex shader. We need to compute its position in projection space. To do that we transform the object space position by the WorldViewProjectionMatrix which is a combination of the World, View, Projection matrices (who would have guessed?).
Here is the simplest application to shaders, rendering a solid color.
Vertex Shader :
//the global uniform World view projection matrix + ++ +JME3 and Shaders
++ ++ ++
+ +
+ + +Shaders Basics
++ ++ ++Shaders are sets of instructions that are executed on the GPU. They are used to take advantage of hardware acceleration available on the GPU for rendering purposes.
+ +
+ +This paper only covers Vertex and Fragment shaders because they are the only ones supported by JME3 for the moment. But be aware that there are some other types of shaders (geometry, tessellation,…).
+ +There are multiple frequently used languages that you may encounter to code shaders but as JME3 is based on OpenGL, shaders in JME use GLSL and any example in this paper will be written in GLSL.
+ +
+ + +How Does it work?
++ ++ ++To keep it Simple: The Vertex shader is executed once for each vertex in the view, then the Fragment shader (also called the Pixel shader) is executed once for each pixel on the screen.
+ +
+ +The main purpose of the Vertex shader is to compute the screen coordinate of a vertex (where this vertex will be displayed on screen) while the main purpose of the Fragment shader is to compute the color of a pixel.
+ +This is a very simplified graphic to describe the call stack:
+ +
+ +The main program sends mesh data to the vertex shader (vertex position in object space, normals, tangents, etc..). The vertex shader computes the screen position of the vertex and sends it to the Fragment shader. The fragment shader computes the color, and the result is displayed on screen or in a texture. +
+ + +Variables scope
++ ++ ++There are different types of scope for variables in a shader : +
++
+ +- +
uniform : User defined variables that are passed by the main program to the vertex and fragment shader, these variables are global for a given execution of a shader.+- +
attribute : Per-vertex variables passed by the engine to the shader, like position, normal, etc (Mesh data in the graphic)+- +
varrying : Variables passed from the vertex shader to the fragment shader.++There is a large panel of variable types to be used, for more information about it I recommend reading the GLSL specification .
+ +
+ +
+ + +Spaces and Matrices
++ ++ ++To understand the coming example you must know about the different spaces in 3D computer graphics, and the matrices used to translate coordinate from one space to another.
+ +
+ +
+ +The engine passes the object space coordinates to the vertex shader. We need to compute its position in projection space. To do that we transform the object space position by the WorldViewProjectionMatrix which is a combination of the World, View, Projection matrices (who would have guessed?).
+ +
+ + +Simple example : rendering a solid color on an object
++ ++Here is the simplest application to shaders, rendering a solid color.
+
+ +Vertex Shader :
+ + +//the global uniform World view projection matrix //(more on global uniforms below) uniform mat4 g_WorldViewProjectionMatrix; //The attribute inPosition is the Object space position of the vertex attribute vec3 inPosition; - void main(){ //Transformation of the object space coordinate to projection space //coordinates. //- gl_Position is the standard GLSL variable holding projection space //position. It must be filled in the vertex shader //- To convert position we multiply the worldViewProjectionMatrix by - //by the position vector. + //by the position vector. //The multiplication must be done in this order. gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); -}Fragment Shader :
void main(){ +}+ ++ +Fragment Shader :
+
+ + +void main(){ //returning the color of the pixel (here solid blue) //- gl_FragColor is the standard GLSL variable that holds the pixel //color. It must be filled in the Fragment Shader. gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0); -}For example applying this shader to a sphere would render a solid blue sphere on screen.
How to use shaders in JME3
+ +You probably heard that JME3 is “shader oriented”, but what does that mean?
Usually to use shaders you must create what is called a program. This program specify the vertex shader and the fragment shader to use.
JME3 encloses this in the material system. Every material in JME3 uses shaders.
For example let’s have a look at the SolidColor.j3md file :MaterialDef Solid Color { +}+ ++ +For example applying this shader to a sphere would render a solid blue sphere on screen.
+ +
+ +
+ + +How to use shaders in JME3
++ ++You probably heard that JME3 is “shader oriented”, but what does that mean?
+
+ +Usually to use shaders you must create what is called a program. This program specify the vertex shader and the fragment shader to use.
+ +JME3 encloses this in the material system. Every material in JME3 uses shaders.
+ +For example let’s have a look at the SolidColor.j3md file :
+ + +MaterialDef Solid Color { //This is the complete list of user defined uniforms to be used in the //shaders MaterialParameters { Vector4 Color } - Technique { //This is where the vertex and fragment shader files are //specified VertexShader GLSL100: Common/MatDefs/Misc/SolidColor.vert FragmentShader GLSL100: Common/MatDefs/Misc/SolidColor.frag - //This is where you specify which global uniform you need for your //shaders WorldParameters { @@ -64,67 +172,171 @@ class="level1">You probably heard that JME3 is “shader oriented”, but wh } Technique FixedFunc { } -}
For more information on JME3 material system, i suggest you read this topic.
JME3 Global uniforms
JME3 can expose pre-computed global uniforms to your shaders. You must specify the one that are required for your shader in the WorldParameters section of the material definition file (.j3md).
Note that in the shader the uniform names will be prefixed by a “g_”.
In the example above, WorldViewProjectionMatrix is declared as uniform mat4 g_WorldViewProjectionMatrix in the shader.
The complete list of global uniforms that can be used in JME3 can be found here.
JME3 attributes
Those are different attributes that are always passed to your shader.
you can find a complete list of those attribute in the Type enum of the VertexBuffer here.
Note that in the shader the attributes names will be prefixed by a “in”.
User's uniforms
+ +At some point when making your own shader you'll need to pass your own uniforms
Any uniform has to be declared in the material definition file in the "MaterialParameters" section.MaterialParameters { +}+ ++ +For more information on JME3 material system, i suggest you read this .
+ +
+ +
+ + +JME3 Global uniforms
++ ++ ++JME3 can expose pre-computed global uniforms to your shaders. You must specify the one that are required for your shader in the WorldParameters section of the material definition file (.j3md).
+ +
+ +Note that in the shader the uniform names will be prefixed by a “g_”.
+ +In the example above, WorldViewProjectionMatrix is declared as uniform mat4 g_WorldViewProjectionMatrix in the shader.
+ +The complete list of global uniforms that can be used in JME3 can be found .
+ +
+ + +JME3 attributes
++ ++ ++Those are different attributes that are always passed to your shader.
+ +
+ +you can find a complete list of those attribute in the Type enum of the VertexBuffer .
+ +Note that in the shader the attributes names will be prefixed by a “in”.
+ +
+ + +User's uniforms
++ ++At some point when making your own shader you'll need to pass your own uniforms
+
+ +Any uniform has to be declared in the material definition file in the "MaterialParameters" section.
+ + +MaterialParameters { Vector4 Color - }This material parameter will be sent from the engine to the shader as follow
material.setColor("Color", ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f);//red colorNote that there is a setXXXX method for any type of uniform you want to pass.
To use this uniform in the shader, you need to declare it in the .frag or in the .vert files (depending on where you need it) as follow :uniform vec4 m_Color;Note the "m_" prefix that specifies that the uniform is a material parameter.
This uniform will be populated at runtime with the value you sent.Step by step
+ +
Create a vertex shader (.vert) file Create a fragment shader (.frag) file Create a material definition (j3md) file specifying the user defined uniforms, path to the shaders and the global uniforms to use In your initSimpleApplication, create a material using this definition, apply it to a geometry That’s it!!// A cube + }+ ++ +This material parameter will be sent from the engine to the shader as follow + +
+material.setColor("Color", new ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f);//red color+ ++ +Note that there is a setXXXX method for any type of uniform you want to pass.
+
+ +To use this uniform in the shader, you need to declare it in the .frag or in the .vert files (depending on where you need it) as follow : + +uniform vec4 m_Color;+ ++ +Note the "m_" prefix that specifies that the uniform is a material parameter.
+ +
+ +This uniform will be populated at runtime with the value you sent. + +Step by step
+++
+- +
Create a vertex shader (.vert) file+- +
Create a fragment shader (.frag) file+- +
Create a material definition (j3md) file specifying the user defined uniforms, path to the shaders and the global uniforms to use+- +
In your initSimpleApplication, create a material using this definition, apply it to a geometry+- +
That’s it!!+// A cube Box(Vector3f.ZERO, 1f,1f,1f); Geometry cube = new Geometry("box", box); Material mat = new Material(assetManager,"Path/To/My/materialDef.j3md"); cube.setMaterial(mat); - rootNode.attachChild(cube);
JME3 and OpenGL 3 & 4 compatibility
GLSL 1.0 to 1.2 comes with build in attributes and uniforms (ie, gl_Vertex, gl_ModelViewMatrix, etc…).
Those attributes are deprecated since GLSL 1.3 (opengl 3), hence JME3 global uniforms and attributes. Here is a list of deprecated attributes and their equivalent in JME3
GLSL 1.2 attributes JME3 equivalent gl_Vertex inPosition gl_Normal inNormal gl_Color inColor gl_MultiTexCoord0 inTexCoord gl_ModelViewMatrix g_WorldViewMatrix gl_ProjectionMatrix g_ProjectionMatrix gl_ModelViewProjectionMatrix g_WorldViewProjectionMatrix gl_NormalMatrix g_NormalMatrix Useful links
- \ No newline at end of file + rootNode.attachChild(cube);
+
+
+
+
+
+GLSL 1.0 to 1.2 comes with build in attributes and uniforms (ie, gl_Vertex, gl_ModelViewMatrix, etc…).
+Those attributes are deprecated since GLSL 1.3 (opengl 3), hence JME3 global uniforms and attributes. Here is a list of deprecated attributes and their equivalent in JME3
+
+
GLSL 1.2 attributes | JME3 equivalent | +
---|---|
gl_Vertex | inPosition | +
gl_Normal | inNormal | +
gl_Color | inColor | +
gl_MultiTexCoord0 | inTexCoord | +
gl_ModelViewMatrix | g_WorldViewMatrix | +
gl_ProjectionMatrix | g_ProjectionMatrix | +
gl_ModelViewProjectionMatrix | g_WorldViewProjectionMatrix | +
gl_NormalMatrix | g_NormalMatrix | +
+ +
+ +Lighting means that an object is brighter on the side facing the light direction, and darker on the backside. A light source with a direction or location is required for lit Materials to be visible. Lighting does not automatically mean that objects cast a shadow on the floor or other objects: Activating shadow processing is an extra step described below.
You can add several light sources to a scene using rootNode.addLight()
. All Lighting.j3md- based Materials require a light source to be visible.
-The available light sources in com.jme3.light
are SpotLight, PointLight, AmbientLight, and DirectionalLight. You can set the color of the light – normally, it is white. You can choose to set other colors to influence the scene's atmosphere.
-A PointLight has a location and shines from there in all directions as far as its radius reaches, like a lamp. The light intensity decreases with increased distance from the light source.
PointLight lamp_light = new PointLight(); + ++ +Light and Shadow
++ ++ ++Lighting means that an object is brighter on the side facing the light direction, and darker on the backside. A light source with a direction or location is required for lit Materials to be visible. Lighting does not automatically mean that objects cast a shadow on the floor or other objects: Activating shadow processing is an extra step described below. + + +
+ +Light Sources
++ ++You can add several light sources to a scene using
+rootNode.addLight()
. All Lighting.j3md- based Materials require a light source to be visible. +The available light sources incom.jme3.light
are SpotLight (), PointLight, AmbientLight, and DirectionalLight. You can set the color (intensity) of the light – normally, it is white (ColorRGBA(1,1,1,1)). You can choose to set other colors to influence the scene's atmosphere. +A PointLight has a location and shines from there in all directions as far as its radius reaches, like a lamp. The light intensity decreases with increased distance from the light source. + +PointLight lamp_light = new PointLight(); lamp_light.setColor(ColorRGBA.Yellow); lamp_light.setRadius(4f); lamp_light.setPosition(new Vector3f(lamp_geo.getLocalTranslation())); -rootNode.addLight(lamp_light);A DirectionalLight has no position, only a direction. It is considered "infinitely" far away and sends out parallel beams of light. It can cast shadows. You typically use it to simulate sun light:
DirectionalLight sun = new DirectionalLight(); +rootNode.addLight(lamp_light);+ ++ +A DirectionalLight has no position, only a direction. It is considered "infinitely" far away and sends out parallel beams of light. It can cast shadows. You typically use it to simulate sun light: + +
+DirectionalLight sun = new DirectionalLight(); sun.setColor(ColorRGBA.White); -sun.setDirection(new Vector3f(-1, -1, -1).normalizeLocal()); -rootNode.addLight(sun);An AmbientLight influences the brightness of the whole scene globally. It has no direction and no location, and does not cast any shadow.
AmbientLight al = new AmbientLight(); +sun.setDirection(new Vector3f(-1,13, -1,13, 1,13).normalizeLocal()); +rootNode.addLight(sun);+ ++ +An AmbientLight influences the brightness of the scene globally. It has no direction and no location, and does not cast any shadows. + +
+AmbientLight al = new AmbientLight(); al.setColor(ColorRGBA.White.mult(1.3f)); -rootNode.addLight(al);A SpotLight is like a flashlight that sends a distinct beam of light. (Still work in progress, as of alpha-3.)
Simple Lighting
+ +
Here we use a material based on Lighting.j3md (More info about Materials). Lighting.j3md-based materials dynamically support Shininess, and Ambient, Diffuse, and Specular Colors.
Geometry teapot = (Geometry) assetManager.loadModel("Models/Teapot/Teapot.obj"); +rootNode.addLight(al);+ ++ +A SpotLight is like a flashlight that sends a distinct beam, or cone of light. A SpotLight has a direction, a position, distance (range) and an angle. The inner angle is the central maximum of the light cone, the outer angle the edge of the light cone. Everything outside the light cone's angles is not affacted by the light. + +
+SpotLight sl = new SpotLight(); +sl.setDirection(new Vector3f(1,0,1)); // direction +sl.setPosition(new Vector3f(0,0,0)); +sl.setSpotInnerAngle(15f); // inner light cone (maximum) +sl.setSpotOuterAngle(35f); // outer light cone (edge of the light) +sl.setSpotRange(10f); // distance +sl.setColor(ColorRGBA.White.mult(1.3f)); +rootNode.addLight(sl);+ +LightControl
++ ++ ++You can use a com.jme3.scene.control.LightControl to make a SpotLight or PointLight follow a Spatial. + +
+PointLight myLight = new PointLight(); + rootNode.addLight(myLight); + LightControl lightControl = new LightControl(myLight); + spatial.addControl(lightControl);+ +Simple Lighting
+++
+ +- +
+- +
++Here we use a material based on Lighting.j3md (More info about Materials). Lighting.j3md-based materials dynamically support Shininess, and Ambient, Diffuse, and Specular Colors. + +
+Geometry teapot = (Geometry) assetManager.loadModel("Models/Teapot/Teapot.obj"); TangentBinormalGenerator.generate(teapot.getMesh(), true); Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); mat.setBoolean("m_UseMaterialColors", true); @@ -34,7 +95,14 @@ mat.setColor("m_Ambient", ColorRGBA.Black); mat.setColor("m_Diffuse", ColorRGBA.Blue); mat.setColor("m_Specular", ColorRGBA.White); mat.setFloat("m_Shininess", 12); -rootNode.attachChild(teapot);In this example, we use material colors instead of textures. But you can equally well use Lighting.j3md to create a Material that uses texture maps, such as the Diffuse and Normal map used here, but also Specular and Paralax Maps:
Sphere rock = new Sphere(32,32, 2f); +rootNode.attachChild(teapot);+ ++ +In this example, we use material colors instead of textures. But you can equally well use Lighting.j3md to create a Material that uses texture maps, such as the Diffuse and Normal map used here, but also Specular and Paralax Maps: + +
+Sphere rock = new Sphere(32,32, 2f); Geometry shiny_rock = new Geometry("Shiny rock", rock); rock.setTextureMode(Sphere.TextureMode.Projected); // better quality on spheres TangentBinormalGenerator.generate(rock); // for lighting effect @@ -46,14 +114,30 @@ rootNode.attachChild(teapot);In this example, we use material assetManager.loadTexture("Textures/Terrain/Pond/Pond_normal.png")); mat_lit.setFloat("m_Shininess", 5f); // [0,128] shiny_rock.setMaterial(mat_lit); - rootNode.attachChild(shiny_rock);
This lighting updates live when the object or light source moves. If you shine a colored PointLight at this object, you will see a light reflection in the color of the PointLight. -This lighting method doesn't make the node cast a shadow onto other nodes.
BasicShadowRenderer
+ +
Use the Shadow Renderer to make textured scene nodes cast and receive shadows. -Switch off the default shadow mode, and add a jME SceneProcessor named com.jme3.shadow.BasicShadowRenderer to the viewPort.
BasicShadowRenderer bsr; + rootNode.attachChild(shiny_rock);+ ++ +This lighting updates live when the object or light source moves. If you shine a colored PointLight at this object, you will see a light reflection in the color of the PointLight. +This lighting method doesn't make the node cast a shadow onto other nodes. + +
+ +BasicShadowRenderer
+++
+ +- +
++Use the Shadow Renderer to make textured scene nodes cast and receive shadows. +Switch off the default shadow mode, and add a jME SceneProcessor named com.jme3.shadow.BasicShadowRenderer to the viewPort. + +
+BasicShadowRenderer bsr; ... public void simpleInitApp() { ... @@ -61,49 +145,89 @@ public void simpleInitApp() { bsr = new BasicShadowRenderer(assetManager, 256); bsr.setDirection(new Vector3f(-1, -1, -1).normalizeLocal()); viewPort.addProcessor(bsr); - ...For every scene node that needs shadows, individually specify the shadow behaviour: Whether it cast shadows, receive shadows, both, or neither.
wall.setShadowMode(ShadowMode.CastAndReceive); + ...+ ++ +For every scene node that needs shadows, individually specify the shadow behaviour: Whether it cast shadows, receive shadows, both, or neither. + +
+wall.setShadowMode(ShadowMode.CastAndReceive); ... floor.setShadowMode(ShadowMode.Receive); ... airplane.setShadowMode(ShadowMode.Cast); ... ghost.setShadowMode(ShadowMode.Off); -...Parallel-Split Shadow Map
+ +
The PSSM shadow renderer can cast real-time shadows on curved surfaces. -To activate it, add a jME SceneProcessor named
com.jme3.shadow.PssmShadowRenderer
to the viewPort.private PssmShadowRenderer pssmRenderer; +...+ +Parallel-Split Shadow Map
++- \ No newline at end of file +...+
+ +- +
++The PSSM shadow renderer can cast real-time shadows on curved surfaces. +To activate it, add a jME SceneProcessor named
+com.jme3.shadow.PssmShadowRenderer
to the viewPort. + +private PssmShadowRenderer pssmRenderer; ... public void simpleInitApp() { .... pssmRenderer = new PssmShadowRenderer( assetManager,1024,4,PssmShadowRenderer.EDGE_FILTERING_PCF); pssmRenderer.setDirection(new Vector3f(-1, -1, -1).normalizeLocal()); - viewPort.addProcessor(pssmRenderer);The constructor expects the following values:
Your assetManager object The size of the rendered shadowmaps (512, 1024, 2048, etc…) The number of shadow maps rendered (the more shadow maps, the more quality, the less FPS). The type of filtering for shadow edge smoothing:
PSSMShadowRenderer.EDGE_FILTERING_DITHER PSSMShadowRenderer.EDGE_FILTERING_PCF (default).You can set the following properties on the
pssmRenderer
object:
setDirection(Vector3f) – the direction of the light setLambda(0.65f) – Factor to use to reduce the split size setShadowIntensity(0.7f) – shadow darkness (1 black, 0 invisible) setShadowZextend() – distance how far away from camera shadows will still be computedAs usual, specify the shadow behaviour for every scene node.
... + viewPort.addProcessor(pssmRenderer);+ ++ +The constructor expects the following values: +
++
+ +- +
Your assetManager object+- +
The size of the rendered shadowmaps (512, 1024, 2048, etc…)+- +
The number of shadow maps rendered (the more shadow maps, the more quality, the less FPS).+- +
The type of filtering for shadow edge smoothing:++
+- +
PSSMShadowRenderer.EDGE_FILTERING_DITHER+- +
PSSMShadowRenderer.EDGE_FILTERING_PCF (default).++You can set the following properties on the
+pssmRenderer
object: ++
+ +- +
setDirection(Vector3f) – the direction of the light+- +
setLambda(0.65f) – Factor to use to reduce the split size+- +
setShadowIntensity(0.7f) – shadow darkness (1 black, 0 invisible)+- +
setShadowZextend() – distance how far away from camera shadows will still be computed++As usual, specify the shadow behaviour for every scene node. + +
+... teapot.setShadowMode(ShadowMode.CastAndReceive); ... soil.setShadowMode(ShadowMode.Receive); -...
+ +There is a good tutorial about creating a nifty progress bar here: + +
+ ++This example will the existing hello terrain as an example. +It will require these 2 images inside Assets/Interface/ (save them as border.png and inner.png respectively) +
+ ++ + +
+ ++This is the progress bar at 90%: +
+ ++ +
+ ++nifty_loading.xml + +
+<?xml version="1.0" encoding="UTF-8"?> +<nifty> + <useStyles filename="nifty-default-styles.xml" /> + <useControls filename="nifty-default-controls.xml" /> + + <controlDefinition name = "loadingbar" controller = "jme3test.TestLoadingScreen"> + <image filename="Interface/border.png" childLayout="absolute" + imageMode="resize:15,2,15,15,15,2,15,2,15,2,15,15"> + <image id="progressbar" x="0" y="0" filename="Interface/inner.png" width="32px" height="100%" + imageMode="resize:15,2,15,15,15,2,15,2,15,2,15,15" /> + </image> + </controlDefinition> + + <screen id="start" controller = "jme3test.TestLoadingScreen"> + <layer id="layer" childLayout="center"> + <panel id = "panel2" height="30%" width="50%" align="center" valign="center" childLayout="vertical" + visibleToMouse="true"> + <control id="startGame" name="button" backgroundColor="#0000" label="Load Game" align="center"> + <interact onClick="showLoadingMenu()" /> + </control> + </panel> + </layer> + </screen> + + <screen id="loadlevel" controller = "jme3test.TestLoadingScreen"> + <layer id="loadinglayer" childLayout="center" backgroundColor="#000000"> + <panel id = "loadingpanel" childLayout="vertical" align="center" valign="center" height="32px" width="400px"> + <control name="loadingbar" align="center" valign="center" width="400px" height="32px" /> + <control id="loadingtext" name="label" align="center" + text=" "/> + </panel> + </layer> + </screen> + + <screen id="end" controller = "jme3test.TestLoadingScreen"> + </screen> + +</nifty>+ +
+ +The progress bar and text is done statically using nifty XML. +A custom control is created, which represents the progress bar. + +
+<controlDefinition name = "loadingbar" controller = "jme3test.TestLoadingScreen"> + <image filename="Interface/border.png" childLayout="absolute" + imageMode="resize:15,2,15,15,15,2,15,2,15,2,15,15"> + <image id="progressbar" x="0" y="0" filename="Interface/inner.png" width="32px" height="100%" + imageMode="resize:15,2,15,15,15,2,15,2,15,2,15,15"/> + </image> + </controlDefinition>+ +
+This screen simply displays a button in the middle of the screen, which could be seen as a simple main menu UI. + +
+<screen id="start" controller = "jme3test.TestLoadingScreen"> + <layer id="layer" childLayout="center"> + <panel id = "panel2" height="30%" width="50%" align="center" valign="center" childLayout="vertical" + visibleToMouse="true"> + <control id="startGame" name="button" backgroundColor="#0000" label="Load Game" align="center"> + <interact onClick="showLoadingMenu()" /> + </control> + </panel> + </layer> + </screen>+ +
+This screen displays our custom progress bar control with a text control + +
+<screen id="loadlevel" controller = "jme3test.TestLoadingScreen"> + <layer id="loadinglayer" childLayout="center" backgroundColor="#000000"> + <panel id = "loadingpanel" childLayout="vertical" align="center" valign="center" height="32px" width="400px"> + <control name="loadingbar" align="center" valign="center" width="400px" height="32px" /> + <control id="loadingtext" name="label" align="center" + text=" "/> + </panel> + </layer> + </screen>+ +
+There are 3 main ways to update a progress bar. To understand why these methods are necessary, an understanding of the graphics pipeline is needed. +
+ ++Something like this in a single thread will not work: + +
+load_scene(); +update_bar(30%); +load_characters(); +update_bar(60%); +load_sounds(); +update_bar(100%);+ +
+ +If you do all of this in a single frame, then it is sent to the graphics card only after the whole code block has executed. By this time the bar has reached 100% and the game has already begun – for the user, the progressbar on the screen would not have visibly changed. +
+ ++The 3 main good solutions are: +
++ +The idea is to break down the loading of the game into discrete parts + +
+package jme3test; + +import com.jme3.niftygui.NiftyJmeDisplay; +import de.lessvoid.nifty.Nifty; +import de.lessvoid.nifty.elements.Element; +import de.lessvoid.nifty.input.NiftyInputEvent; +import de.lessvoid.nifty.screen.Screen; +import de.lessvoid.nifty.screen.ScreenController; +import de.lessvoid.nifty.tools.SizeValue; +import com.jme3.app.SimpleApplication; +import com.jme3.material.Material; +import com.jme3.renderer.Camera; +import com.jme3.terrain.geomipmap.TerrainLodControl; +import com.jme3.terrain.heightmap.AbstractHeightMap; +import com.jme3.terrain.geomipmap.TerrainQuad; +import com.jme3.terrain.heightmap.ImageBasedHeightMap; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture.WrapMode; +import de.lessvoid.nifty.controls.Controller; +import de.lessvoid.nifty.elements.render.TextRenderer; +import de.lessvoid.xml.xpp3.Attributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import jme3tools.converters.ImageToAwt; + +public class TestLoadingScreen extends SimpleApplication implements ScreenController, Controller { + + private NiftyJmeDisplay niftyDisplay; + private Nifty nifty; + private Element progressBarElement; + private TerrainQuad terrain; + private Material mat_terrain; + private float frameCount = 0; + private boolean load = false; + private TextRenderer textRenderer; + + public static void main(String[] args) { + TestLoadingScreen app = new TestLoadingScreen(); + app.start(); + } + + @Override + public void simpleInitApp() { + flyCam.setEnabled(false); + niftyDisplay = new NiftyJmeDisplay(assetManager, + inputManager, + audioRenderer, + guiViewPort); + nifty = niftyDisplay.getNifty(); + + nifty.fromXml("Interface/nifty_loading.xml", "start", this); + + guiViewPort.addProcessor(niftyDisplay); + } + + @Override + public void simpleUpdate(float tpf) { + + if (load == true) { //loading is done over many frames + if (frameCount == 1) { + Element element = nifty.getScreen("loadlevel").findElementByName("loadingtext"); + textRenderer = element.getRenderer(TextRenderer.class); + + mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md"); + mat_terrain.setTexture("Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png")); + setProgress(0.2f, "Loading grass"); + + } else if (frameCount == 2) { + Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg"); + grass.setWrap(WrapMode.Repeat); + mat_terrain.setTexture("Tex1", grass); + mat_terrain.setFloat("Tex1Scale", 64f); + setProgress(0.4f, "Loading dirt"); + + } else if (frameCount == 3) { + Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"); + + dirt.setWrap(WrapMode.Repeat); + mat_terrain.setTexture("Tex2", dirt); + mat_terrain.setFloat("Tex2Scale", 32f); + setProgress(0.5f, "Loading rocks"); + + } else if (frameCount == 4) { + Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg"); + + rock.setWrap(WrapMode.Repeat); + + mat_terrain.setTexture("Tex3", rock); + mat_terrain.setFloat("Tex3Scale", 128f); + setProgress(0.6f, "Creating terrain"); + + } else if (frameCount == 5) { + AbstractHeightMap heightmap = null; + Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); + heightmap = new ImageBasedHeightMap( + ImageToAwt.convert(heightMapImage.getImage(), false, true, 0)); + + heightmap.load(); + terrain = new TerrainQuad("my terrain", 65, 513, heightmap.getHeightMap()); + setProgress(0.8f, "Positioning terrain"); + + } else if (frameCount == 6) { + terrain.setMaterial(mat_terrain); + + terrain.setLocalTranslation(0, -100, 0); + terrain.setLocalScale(2f, 1f, 2f); + rootNode.attachChild(terrain); + setProgress(0.9f, "Loading cameras"); + + } else if (frameCount == 7) { + List<Camera> cameras = new ArrayList<Camera>(); + cameras.add(getCamera()); + TerrainLodControl control = new TerrainLodControl(terrain, cameras); + terrain.addControl(control); + setProgress(1f, "Loading complete"); + + } else if (frameCount == 8) { + nifty.gotoScreen("end"); + nifty.exit(); + guiViewPort.removeProcessor(niftyDisplay); + flyCam.setEnabled(true); + flyCam.setMoveSpeed(50); + } + + frameCount++; + } + } + + public void setProgress(final float progress, String loadingText) { + final int MIN_WIDTH = 32; + int pixelWidth = (int) (MIN_WIDTH + (progressBarElement.getParent().getWidth() - MIN_WIDTH) * progress); + progressBarElement.setConstraintWidth(new SizeValue(pixelWidth + "px")); + progressBarElement.getParent().layoutElements(); + + textRenderer.setText(loadingText); + } + + public void showLoadingMenu() { + nifty.gotoScreen("loadlevel"); + load = true; + } + + @Override + public void onStartScreen() { + } + + @Override + public void onEndScreen() { + } + + @Override + public void bind(Nifty nifty, Screen screen) { + progressBarElement = nifty.getScreen("loadlevel").findElementByName("progressbar"); + } + + // methods for Controller + @Override + public boolean inputEvent(final NiftyInputEvent inputEvent) { + return false; + } + + @Override + public void bind(Nifty nifty, Screen screen, Attributes atrbts) { + progressBarElement = elmnt.findElementByName("progressbar"); + } + + @Override + public void init(Attributes atrbts) { + } + + public void onFocus(boolean getFocus) { + } +}+ +
+Note: +
++For more info on multithreading: +
+ ++Make sure to change the XML file to point the controller to TestLoadingScreen1 + +
+package jme3test; + +import com.jme3.niftygui.NiftyJmeDisplay; +import de.lessvoid.nifty.Nifty; +import de.lessvoid.nifty.elements.Element; +import de.lessvoid.nifty.input.NiftyInputEvent; +import de.lessvoid.nifty.screen.Screen; +import de.lessvoid.nifty.screen.ScreenController; +import de.lessvoid.nifty.tools.SizeValue; +import com.jme3.app.SimpleApplication; +import com.jme3.material.Material; +import com.jme3.renderer.Camera; +import com.jme3.terrain.geomipmap.TerrainLodControl; +import com.jme3.terrain.heightmap.AbstractHeightMap; +import com.jme3.terrain.geomipmap.TerrainQuad; +import com.jme3.terrain.heightmap.ImageBasedHeightMap; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture.WrapMode; +import de.lessvoid.nifty.controls.Controller; +import de.lessvoid.nifty.elements.render.TextRenderer; +import de.lessvoid.xml.xpp3.Attributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import jme3tools.converters.ImageToAwt; + +public class TestLoadingScreen1 extends SimpleApplication implements ScreenController, Controller { + + private NiftyJmeDisplay niftyDisplay; + private Nifty nifty; + private Element progressBarElement; + private TerrainQuad terrain; + private Material mat_terrain; + private boolean load = false; + private ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(2); + private Future loadFuture = null; + private TextRenderer textRenderer; + + public static void main(String[] args) { + TestLoadingScreen1 app = new TestLoadingScreen1(); + app.start(); + } + + @Override + public void simpleInitApp() { + flyCam.setEnabled(false); + niftyDisplay = new NiftyJmeDisplay(assetManager, + inputManager, + audioRenderer, + guiViewPort); + nifty = niftyDisplay.getNifty(); + + nifty.fromXml("Interface/nifty_loading.xml", "start", this); + + guiViewPort.addProcessor(niftyDisplay); + } + + @Override + public void simpleUpdate(float tpf) { + if (load) { + if (loadFuture == null) { + //if we have not started loading yet, submit the Callable to the executor + loadFuture = exec.submit(loadingCallable); + } + //check if the execution on the other thread is done + if (loadFuture.isDone()) { + //these calls have to be done on the update loop thread, + //especially attaching the terrain to the rootNode + //after it is attached, it's managed by the update loop thread + // and may not be modified from any other thread anymore! + nifty.gotoScreen("end"); + nifty.exit(); + guiViewPort.removeProcessor(niftyDisplay); + flyCam.setEnabled(true); + flyCam.setMoveSpeed(50); + rootNode.attachChild(terrain); + load = false; + } + } + } + //this is the callable that contains the code that is run on the other thread. + //since the assetmananger is threadsafe, it can be used to load data from any thread + //we do *not* attach the objects to the rootNode here! + Callable<Void> loadingCallable = new Callable<Void>() { + + public Void call() { + + Element element = nifty.getScreen("loadlevel").findElementByName("loadingtext"); + textRenderer = element.getRenderer(TextRenderer.class); + + mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md"); + mat_terrain.setTexture("Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png")); + //setProgress is thread safe (see below) + setProgress(0.2f, "Loading grass"); + + Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg"); + grass.setWrap(WrapMode.Repeat); + mat_terrain.setTexture("Tex1", grass); + mat_terrain.setFloat("Tex1Scale", 64f); + setProgress(0.4f, "Loading dirt"); + + Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"); + + dirt.setWrap(WrapMode.Repeat); + mat_terrain.setTexture("Tex2", dirt); + mat_terrain.setFloat("Tex2Scale", 32f); + setProgress(0.5f, "Loading rocks"); + + Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg"); + + rock.setWrap(WrapMode.Repeat); + + mat_terrain.setTexture("Tex3", rock); + mat_terrain.setFloat("Tex3Scale", 128f); + setProgress(0.6f, "Creating terrain"); + + AbstractHeightMap heightmap = null; + Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); + heightmap = new ImageBasedHeightMap( + ImageToAwt.convert(heightMapImage.getImage(), false, true, 0)); + + heightmap.load(); + terrain = new TerrainQuad("my terrain", 65, 513, heightmap.getHeightMap()); + setProgress(0.8f, "Positioning terrain"); + + terrain.setMaterial(mat_terrain); + + terrain.setLocalTranslation(0, -100, 0); + terrain.setLocalScale(2f, 1f, 2f); + setProgress(0.9f, "Loading cameras"); + + List<Camera> cameras = new ArrayList<Camera>(); + cameras.add(getCamera()); + TerrainLodControl control = new TerrainLodControl(terrain, cameras); + terrain.addControl(control); + setProgress(1f, "Loading complete"); + + return null; + } + }; + + public void setProgress(final float progress, final String loadingText) { + //since this method is called from another thread, we enqueue the changes to the progressbar to the update loop thread + enqueue(new Callable() { + + public Exception { + final int MIN_WIDTH = 32; + int pixelWidth = (int) (MIN_WIDTH + (progressBarElement.getParent().getWidth() - MIN_WIDTH) * progress); + progressBarElement.setConstraintWidth(new SizeValue(pixelWidth + "px")); + progressBarElement.getParent().layoutElements(); + + textRenderer.setText(loadingText); + return null; + } + }); + + } + + public void showLoadingMenu() { + nifty.gotoScreen("loadlevel"); + load = true; + } + + @Override + public void onStartScreen() { + } + + @Override + public void onEndScreen() { + } + + @Override + public void bind(Nifty nifty, Screen screen) { + progressBarElement = nifty.getScreen("loadlevel").findElementByName("progressbar"); + } + + // methods for Controller + @Override + public boolean inputEvent(final NiftyInputEvent inputEvent) { + return false; + } + + @Override + public void bind(Nifty nifty, Screen screen, Attributes atrbts) { + progressBarElement = elmnt.findElementByName("progressbar"); + } + + @Override + public void init(Attributes atrbts) { + } + + public void onFocus(boolean getFocus) { + } +}+ +
+TO DO + +
+ +Localizing an application can mean several things:
There are tools that assist you with localizing Java Swing GUIs. jME3 applications do not typically have a Swing GUI, so those tools are not of much help. Just stick to the normal Java rules about using Bundle Properties:
Tip: The jMonkeyPlatform supports opening and editing Bundle.properties files. Also note the Tools > Localization menu.
To prepare the application for localization, you have to first identify all hard-coded messages.
System.out.print("Hello World!"); -UiText.setText("Score: "+score);
Bundle.properties
in each directory where there are Java file that contain messages.Bundle.properties
file: First specify a unique key that identifies this string; then an equal sign; and the literal string itself. greeting=Hello World! -score.display=Score:
ResourceBundle.getBundle("Bundle").getString("greeting")); -UiText.setText(ResourceBundle.getBundle("Bundle").getString("score.display")+score);
The language used in the Bundle.properties files will be the default language for your game.
Each additional language comes in a set of files that is marked with a (usually) two-letter suffix. Common locales are de for German, en for English, fr for French, ja for Japanese, pt for Portuguese, etc.
To translate the messages to another language, for example, German:
Bundle.properties
files.Bundle_de.properties
for German. Note the added suffix _de.Bundle_de.properties
to German.greeting=Hallo Welt! -score.display=Spielstand:
Important: Do not modify any of the keys (text to the left of the equal sign)!
-Duser.language=de
. Note the parameter de
.Tip: In the jMonkeyPlatform, you set this VM Option in the Project properties under Run. Here you can also save individual run configuraions for each language you want to test.
To get the full list of language suffixes use
Locale.getISOLanguages()));
Important: In the Bundle.properties file, do not include any strings that are asset paths, node or geometry names, input mappings, or material layers.
mat.setTexture("ColorMap", tex);
teapot = assetManager.loadModel("Models/Teapot/Teapot.obj");
Geometry thing=new Geometry("A thing", mesh); -Node vehicle = new Node("Vehicle");
inputManager.addMapping("Shoot", trigger); -inputManager.addListener(actionListener, "Shoot");
Only localize messages and UI text!
Typical problems include:
http://java.sun.com/developer/technicalArticles/Intl/ResourceBundles/
http://sourceforge.net/apps/mediawiki/nifty-gui/index.php?title=Localisation
+ +Localizing an application can mean several things: + +
++ +There are tools that assist you with localizing Java Swing GUIs. jME3 applications do not typically have a Swing GUI, so those tools are not of much help. Just stick to the normal Java rules about using Bundle Properties: +
+ ++ +Tip: The jMonkeyPlatform supports opening and editing Bundle.properties files. Also note the Tools > Localization menu. +
+ ++To prepare the application for localization, you have to first identify all hard-coded messages. + +
+System.out.print("Hello World!"); +UiText.setText("Score: "+score);+
Bundle.properties
in each directory where there are Java file that contain messages.Bundle.properties
file: First specify a unique key that identifies this string; then an equal sign; and the literal string itself. greeting=Hello World! +score.display=Score:+
ResourceBundle.getBundle("Bundle").getString("greeting")); +UiText.setText(ResourceBundle.getBundle("Bundle").getString("score.display")+score);+
+ +The language used in the Bundle.properties files will be the default language for your game. + +
+ ++ +Each additional language comes in a set of files that is marked with a (usually) two-letter suffix. Common locales are de for German, en for English, fr for French, ja for Japanese, pt for Portuguese, etc. +
+ ++To translate the messages to another language, for example, German: + +
+Bundle.properties
files.Bundle_de.properties
for German. Note the added suffix _de.Bundle_de.properties
to German. greeting=Hallo Welt! +score.display=Spielstand:+ +
+ Important: Do not modify any of the keys (text to the left of the equal sign)! +
+-Duser.language=de
. Note the parameter de
.+ +Tip: In the jMonkeyPlatform, you set this VM Option in the Project properties under Run. Here you can also save individual run configuraions for each language you want to test. +
+ ++To get the full list of language suffixes use + +
+Locale.getISOLanguages()));+ +
+ +Important: In the Bundle.properties file, do not include any strings that are asset paths, node or geometry names, input mappings, or material layers. +
+mat.setTexture("ColorMap", tex);+
teapot = assetManager.loadModel("Models/Teapot/Teapot.obj");+
Geometry thing=new Geometry("A thing", mesh); +Node vehicle = new Node("Vehicle");+
inputManager.addMapping("Shoot", trigger); +inputManager.addListener(actionListener, "Shoot");+
+ +Only localize messages and UI text! +
+ ++ +Typical problems include: +
++ + +
+ ++ + +
+ +Many developers just use System.out.println() to print diagnostic strings to the terminal. The problem with that is that before the release, you'd have to go through all your code and make certain you removed all these println() calls. You do not want your users to see them and worry about ominous strings babbling about old development diagnostics.
Instead of println(), you use the standard Java logger from java.util.logging
. It has many advantages for professional game development:
So to print comments like a pro, you use the following logger syntax. The variables a, b, c, can be any printable Java object, e.g. Vector3f a = cam.getLocation()
. They are numbered {0},{1},{2},etc for use in the string, in the order you put them in the Object array.
private static final Logger logger = Logger.getLogger(HelloWorld.class.getName());
Replace HelloWorld by the name of the class where you are using this line.
logger.log(Level.WARNING, "ok seriously wtf somebody check why {0} is {1} again?!", - new Object[]{a , b});
or
logger.log(Level.SEVERE, "Game error: {0} must not be {1} after {2}! Please check your flux generator.", - new Object[]{a , b , c});
As you see in the example, you should phrase potentially "customer facing" errors in a neutral way and offer a reason and a solution. If you use WARNINGs as replacement for casual printlns, make sure you deactivate them for the release.
More details about Java log levels here.
In the release version you will deactivate the logging output to the terminal.
To deactivate the default logger, you set the log level to only report severe messages:
Logger.getLogger(””).setLevel(Level.SEVERE);
To reactivate it:
Logger.getLogger(””).setLevel(Level.FINE);
You find the jMonkeyPlatform log file in /dev/var/log/messages.log in the jMonkeyPlatform preferences folder. You can learn the location of the preferences folder in the “About” screen of the jMonkeyPlatform under the label Userdir.
/Users/YOUR_NAME/Library/Application Support/jmonkeyplatform/
You can read the graphic card's capabilities using the com.jme3.renderer.Caps
class:
Collection<Caps> caps = renderer.getCaps(); -Logger.getLogger(HelloWorld.class.getName()).log(Level.INFO, “Caps: {0}” + caps.toString());
Replace HelloWorld by the name of the class where you are using this line.
The result looks like the following example:
Caps: [FrameBuffer, FrameBufferMRT, FrameBufferMultisample, OpenGL20, ARBprogram, GLSL100, GLSL110, GLSL120, VertexTextureFetch, FloatTexture, TextureCompressionLATC]
This would tell you that this user's graphic card only supports OpenGL 2.0 and cannot handle newer OpenGL features.
+ +Many developers just use System.out.println() to print diagnostic strings to the terminal. The problem with that is that before the release, you'd have to go through all your code and make certain you removed all these println() calls. You do not want your users to see them and worry about ominous strings babbling about old development diagnostics. +
+ +
+
+Instead of println(), you use the standard Java logger from java.util.logging
. It has many advantages for professional game development:
+
+
+So to print comments like a pro, you use the following logger syntax. The variables a, b, c, can be any printable Java object, e.g. Vector3f a = cam.getLocation()
. They are numbered {0},{1},{2},etc for use in the string, in the order you put them in the Object array.
+
private static final Logger logger = Logger.getLogger(HelloWorld.class.getName());+ +
+ +Replace HelloWorld by the name of the class where you are using this line. + +
+logger.log(Level.WARNING, "ok seriously wtf somebody check why {0} is {1} again?!", + new Object[]{a , b});+ +
+ +or + +
+logger.log(Level.SEVERE, "Game error: {0} must not be {1} after {2}! Please check your flux generator.", + new Object[]{a , b , c});+ +
+As you see in the example, you should phrase potentially "customer facing" errors in a neutral way and offer a reason and a solution. If you use WARNINGs as replacement for casual printlns, make sure you deactivate them for the release. +
+ ++More details about here. +
+ ++ +In the release version you will deactivate the logging output to the terminal. +
+ ++To deactivate the default logger for a release, you set the log level to only report severe messages: + +
+Logger.getLogger(””).setLevel(Level.SEVERE);+ +
+During development, you can tune down the default logger, and set the log level to only report warnings: + +
+Logger.getLogger(””).setLevel(Level.WARNING);+ +
+To reactivate full logging, e.g. for debugging and testing: + +
+Logger.getLogger(””).setLevel(Level.FINE);+ +
+
+When players steer a game character with 1st-person view, they directly steer the camera (flyCam.setEnabled(true);
), and they never see the walking character itself. In a game with 3rd-person view, however, the players see the character walk, and you (the game developer) want to make the camera follow the character around when it walks.
+
+There are two ways how the camera can do that: +
+
+
+Important: Using third-person view requires you to deactivate the default flyCam (first-person view). This means that you have to configure your own navigation (key inputs and analogListener) that make your player character walk. For moving a physical player character, use player.setWalkDirection()
, for a non-pysical character you can use player.move()
.
+
+Press the WASD or arrow keys to move. Drag with the left mouse button to rotate. +
+
+To make the camera follow a target node, add this camera node code to your init method (e.g. simpleInitApp()
). The target
spatial is typically the player node.
+
// Disable the default flyby cam +flyCam.setEnabled(false); +//create the camera Node +camNode = new CameraNode("Camera Node", cam); +//This mode means that camera copies the movements of the target: +camNode.setControlDir(ControlDirection.SpatialToCamera); +//Move camNode, e.g. behind and above the target: +camNode.setLocalTranslation(new Vector3f(0, 5, -5)); +//Rotate the camNode to look at the target: +camNode.lookAt(target.getLocalTranslation(), Vector3f.UNIT_Y); +//Attach the camNode to the target: +target.attachChild(camNode);+ +
+Important: Where the example says camNode.setLocalTranslation(new Vector3f(0, 5, -5));
, you have to supply your own start position for the camera. This depends on the size of your target (the player character) and its position in your particular scene. Optimally, you set this to a spot a bit behind and above the target.
+
+
Methods | Description | +
---|---|
setControlDir(ControlDirection.SpatialToCamera) | User input steers the target spatial, and the camera follows the spatial. +The spatial's transformation is copied over the camera's transformation. +Example: Use with CharacterControlled spatial. |
+
setControlDir(ControlDirection.CameraToSpatial) | User input steers the camera, and the target spatial follows the camera. +The camera's transformation is copied over the spatial's transformation. |
+
+ +Code sample: +
+
+
+To activate the chase camera, add the following code to your init method (e.g. simpleInitApp()
). The target
spatial is typically the player node. You will be able to rotate the target by dragging (keeping the left mouse button pressed and moving the mouse).
+
// Disable the default flyby cam +flyCam.setEnabled(false); +// Enable a chase cam for this target (typically the player). +ChaseCamera chaseCam = new ChaseCamera(cam, target, inputManager); +chaseCam.setSmoothMotion(true);+
Method | Description | +
---|---|
setInvertVerticalAxis(true) | Invert the camera's vertical rotation Axis | +
setInvertHorizontalAxis(true) | Invert the camera's horizontal rotation Axis | +
setTrailingEnabled(true) | Camera follows the target and flies around and behind when the target moves towards the camera. Trailing only works with smooth motion enabled. (Default) | +
setTrailingEnabled(false) | Camera follows the target, but does not rotate around the target when the target changes direction. | +
setSmoothMotion(true) | Activate SmoothMotion when trailing. This means the camera seems to accelerate and fly after the character, when it has caught up, it slows down again. | +
setSmoothMotion(false) | Disable smooth camera motion. Disabling SmoothMotion also disables trailing. | +
setLookAtOffset(Vector3f.UNIT_Y.mult(3)) | Camera looks at a point 3 world units above the target. | +
setToggleRotationTrigger(new MouseButtonTrigger(MouseInput.BUTTON_MIDDLE)) | Enable rotation by keeping the middle mouse button pressed (like in Blender). This disables the rotation on right and left mouse button click. | +
setToggleRotationTrigger(new MouseButtonTrigger( +MouseInput.BUTTON_MIDDLE), +new KeyTrigger(KeyInput.KEY_SPACE)) | Activate mutiple triggers for the rotation of the camera, e.g. spacebar and middle mouse button, etc. | +
setRotationSensitivity(5f) | How fast the camera rotates. Use values around <1.0f (all bigger values are ignored). | +
+ +Code sample: +
++ +What is the difference of the two code samples above? + +
+CameraNode | ChaseCam | +
---|---|
Camera follows immediately, flies at same speed as target. | Camera moves smoothly and accelerates and decelerates, flies more slowly than the target and catches up. | +
Camera stays attached to the target at a constant distance. | Camera orbits the target and approaches slowly. | +
Drag-to-Rotate rotates the target and the camera. You always see the target from behind. | Drag-to-Rotate rotates only the camera. You can see the target from various sides. | +
Typically, you create a set of custom materials, and use them throughout the game. For example, you can initialize and configure your materials objects in the initSimpleApp()
method, and then load 3D models (Geometries) and use setMaterial() on them.
Tip: If you use one custom material very often, additionally read about storing material configurations in user-friendly j3m Material Files.
Unshaded.j3md
.Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor( "Color", ColorRGBA.White );
mat.setTexture( "ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.png" ));
mat.setFloat( "Shininess", 5f);
mat.setBoolean( "SphereMap", true);
mat.setVector3( "NormalScale", new Vector3f(1f,1f,1f));
myGeometry.setMaterial(mat);
geometry.scaleTextureCoordinates(new Vector2f(1f, .5f));
A simpled textured material.
Material mat = new Material(assetManager, + ++ +How to Use Material Definitions (.j3md)
++ ++ ++ +Typically, you create a set of custom materials, and use them throughout the game. For example, you can initialize and configure your materials objects in the
+ +initSimpleApp()
method, and then load 3D models (Geometries) and use setMaterial() on them. ++Tip: If you use one custom material very often, additionally read about storing material configurations in user-friendly j3m Material Files. +
+ +Preparing a Material
+++ ++
+ +- +
Choose a Material Definition from the Materials Overview list that has the features that you need.++
+- +
Tip: If you don't know, you can always start with+Unshaded.j3md
.- +
Look at the applicable parameters of the Material Definition and determine which ones you need to achieve the desired effect. Most parameters are optional.++
+- +
Create and save the necessary Texture files to the assets directory.++
+- +
E.g. ColorMap; DiffuseMap, NormalMap, AlphaMap, etc…+- +
Determine the required values to achieve the effect that you want.++
+- +
E.g. Colors, floats, booleans, etc…+Using a Material
+++ ++
+ +- +
In you Java code, create a Material object based on the .j3md file: e.g.+Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");+- +
Configure your Material by setting the appropriate values listed in the Materials Overview table. Here are examples of the methods that set the different data types:++
+- +
+mat.setColor( "Color", ColorRGBA.White );
- +
+mat.setTexture( "ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.png" ));
- +
+mat.setFloat( "Shininess", 5f);
- +
+mat.setBoolean( "SphereMap", true);
- +
+mat.setVector3( "NormalScale", new Vector3f(1f,1f,1f));
- +
Use your prepared material on a Geometry:+myGeometry.setMaterial(mat);+- +
(Optional) Adjust the texture scale:+geometry.scaleTextureCoordinates(new Vector2f(1f, .5f));+Examples
++ +- \ No newline at end of file +rootNode.attachChild(geom);+ +A simpled textured material. + +
+Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setTexture("ColorMap", assetManager.loadTexture( - "Interface/Logo/Monkey.jpg"));A textured material with a color bleeding through transparent areas.
Material mat = new Material(assetManager, + "Interface/Logo/Monkey.jpg"));+ ++A textured material with a color bleeding through transparent areas. + +
+Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setTexture("ColorMap", assetManager.loadTexture( "Textures/ColoredTex/Monkey.png")); -mat.setColor("Color", ColorRGBA.Blue);You can test these examples within the following code snippet. It creates a box and applies the material:
Box(Vector3f.ZERO, 1, 1, 1); +mat.setColor("Color", ColorRGBA.Blue);+ ++You can test these examples within the following code snippet. It creates a box and applies the material: +
+Box(Vector3f.ZERO, 1, 1, 1); Geometry geom = new Geometry("Box", b); // ... insert Material definition... geom.setMaterial(mat); -rootNode.attachChild(geom);Tip: You can find these and other common code snippets in the jMonkeyPlatform Code Palette. Drag and drop them into your source code.
+Tip: You can find these and other common code snippets in the jMonkeyPlatform Code Palette. Drag and drop them into your source code. + +
+ + This table shows you which material definitions jME supplies by default, and how to make the most of your designer's 3D models by using material parameters.
-If you are looking for information about how to use these materials in code, look at Material Definitions and j3M Material Files.
Tip: The two most commonly used materials are Lighting.j3md and Unshaded.j3md (standard materials with and without Phong illumination, respectively).
Some parameters are "optional" because they are somewhat advanced. If you don't know what an option means, chances are that you are not using this feature in your textures – and you don't need to specify it. (E.g. YCoCg and LATC are image compression formats; Minnaert and WardIso are shader types.)
-Also note that many other parameters are optional, even if they are not explicitly marked optional. For example, it's okay to specify solely the DiffuseMap
and NormalMap
when using Lighting.j3md
. You are only using a subset of what is possible, but if that's what you want, you can do that. The developer should be in contact with the designer regarding what jME features individual Materials/Textures require.
Material Definition | Usage | Parameter : Type |
---|---|---|
Common/MatDefs/Misc/Unshaded.j3md | Standard unlit Material. Use this for simple coloring, simple texturing, simple glow, simple transparency. See also: Hello Material | ColorMap : Texture LightMap : Texture Color : Color VertexColor : Boolean SeparateTexCoord : Boolean GlowMap : Texture GlowColor: Color |
Common/MatDefs/Misc/Sky.j3md | A solid skyblue, or use with a custom SkyDome texture. See also: Sky | Texture : TextureCubeMap SphereMap : Boolean NormalScale : Vector3 |
Common/MatDefs/Terrain/Terrain.j3md | Splat textures for e.g. terrains. See also: Hello Terrain | Texture1 : Texture (red) Texture1Scale : Float Texture2 : Texture (green) Texture2Scale : Float Texture3 : Texture (blue) Texture3Scale : Float Alpha : Texture |
Common/MatDefs/Misc/Particle.j3md | Used with texture masks for particle effects, or for point sprites. The Quadratic value scales the particle for perspective view (formula). Does support an optional colored glow effect. See also: Hello Effects | Texture : Texture GlowMap : Texture GlowColor : Color Quadratic : Float PointSprite : Boolean |
Material Definition | Usage | Parameters |
---|---|---|
Common/MatDefs/Light/Lighting.j3md | Standard lit material with Phong Illumination. Use this material together with DiffuseMap, SpecularMap, BumpMap (NormalMaps, ParalaxMap) textures. Supports shininess, transparency, and plain material colors (Diffuse, Ambient, Specular colors). See also: Hello Material Note: Lit materials require a light source! Glowing materials require a FilterPostProcessor! | DiffuseMap : Texture UseAlpha1) : Boolean NormalMap : Texture LATC2) : Boolean SpecularMap : Texture Shininess : Float ParallaxMap : Texture AlphaMap : Texture AlphaDiscardThreshold: Float ColorRamp : Texture Glow (optional) GlowMap : Texture GlowColor : Color Performance and quality (optional) VertexLighting : Boolean UseVertexColor : Boolean LowQuality : Boolean HighQuality : Boolean Material Colors (optional) UseMaterialColors : Boolean Diffuse : Color Ambient : Color Specular : Color Tangent shading (optional): VTangent : Boolean Minnaert3) : Boolean WardIso4) : Boolean |
Common/MatDefs/Terrain/TerrainLighting.j3md | Same kind of splat texture as Terrain.j3md, but with shading. Requires a light source. | Color Diffuse : Color Ambient : Color Shininess : Float Specular : Color SpecularMap : Texture WardIso : Boolean useTriPlanarMapping : Boolean Texture Splat Maps DiffuseMap : Texture DiffuseMap_0_scale : Float NormalMap : Texture DiffuseMap_1 : Texture DiffuseMap_1_scale : Float NormalMap_1 : Texture DiffuseMap_2 : Texture DiffuseMap_2_scale : Float NormalMap_2 : Texture DiffuseMap_3 : Texture DiffuseMap_3_scale : Float NormalMap_3 : Texture Alpha Maps AlphaMap : Texture AlphaMap_1 : Texture AlphaMap_2 : Texture Glowing GlowMap : Texture GlowColor : Color |
Common/MatDefs/Light/Reflection.j3md | Reflective glass material with environment map (CubeMap/SphereMap). Requires light source. See also: TestCubeMap.java | Texture : Texture SphereMap: Boolean |
Material Definition | Usage | Parameters |
---|---|---|
Common/MatDefs/Misc/ShowNormals.j3md | A color gradient calculated from the model's surface normals. You can use this built-in material to test models that have no material, or as fall-back default material. | – |
Note: Common/MatDefs/Misc/SimpleTextured.j3md, ColoredTextured.j3md, VertexColor.j3md, Wireframe.j3md have been deprecated. Use equivalent features of Unshaded.j3md instead.
Most Material Definitions support an alpha channel for transparency. In an RGBA color, the last float is the alpha channel: 0.0f is transparent, 1.0f is opaque.
For example: mat.setColor("Color", new ColorRGBA(1,0,0,0.5f));
is a half-opaque red.
Additionally, you must specify a blendmode:
Option | Usage |
---|---|
mat.getAdditionalRenderState().setBlendMode(BlendMode.Off); | Opaque |
mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); | Use this for normal transparency. Interpolates the background pixel with the current by using the current pixel's alpha. E.g. alpha-blended vegetation. |
mat.getAdditionalRenderState().setBlendMode(BlendMode.Additive); | Additive alpha blending adds colors in a commutative way, i.e. the result does not depend on the order of transparent layers. Adds the background pixel color with the current pixel color. E.g. particle effects that have black color as background. |
mat.getAdditionalRenderState().setBlendMode(BlendMode.AlphaAdditive); | Same as "Additive", except first it multiplies the current pixel color by the pixel alpha. E.g. used for particle effects that have alpha as background. |
mat.getAdditionalRenderState().setBlendMode(BlendMode.Color); | Blends by color. Generally useless. |
mat.getAdditionalRenderState().setBlendMode(BlendMode.Modulate); | Multiplies the background pixel by the current pixel. |
mat.getAdditionalRenderState().setBlendMode(BlendMode.ModulateX2); | Same as "Modulate", except the result is doubled. |
mat.getAdditionalRenderState().setBlendMode(BlendMode.PremultAlpha); | Pre-multiplied alpha blending. E.g. if the color of the object has already been multiplied by its alpha, this is used instead of "Alpha" blend mode. |
mat.getAdditionalRenderState().setDepthWrite(false); | Use this if you have several transparent objects obscuring one another. Disables writing of the pixel's depth value to the depth buffer. |
mat.getAdditionalRenderState().setAlphaFallOff(0.5f); mat.getAdditionalRenderState().setAlphaTest(true) | Enables alpha test, generally used for vegetation. Works the same way as "AlphaDiscardThreshold". |
Also note the AlphaDiscardThreshold value for materials based on Lighting.j3md. The renderer does not render pixels whose transparancy is below the threshold.
mat.getAdditionalRenderState().setWireframe(true); | Switch to showing the (textured) Material in wireframe mode |
mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off); mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Front); mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Back); mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.FrontAndBack) | Activate back- or frontface culling, both (=invisible), or off. Backface culling is activated by default as an optimization. |
mat.getAdditionalRenderState().setColorWrite(false); | Disable writing the color of pixels. Use this together with setDepthWrite(true) to write pixels only to the depth buffer for example. |
mat.getAdditionalRenderState().setPointSprite(true); | Enables point-sprite mode, so meshes with "Mode.Points" will be rendered as textured sprites. Note that gl_PointCoord must be set in the shader. Point sprites are used for hardware accelerated particle effects. |
mat.getAdditionalRenderState().setPolyOffset(); | Enable polygon offset. Use this when you have meshes that have triangles really close to each over (e.g. Coplanar), it will shift the depth values to prevent Z-fighting. |
+This table shows you which material definitions jME supplies by default, and how to make the most of your designer's 3D models by using material parameters. +If you are looking for information about how to use these materials in code, look at Material Definitions and j3M Material Files. +
+ ++Tip: Looks complicated? Remember two things: The most commonly used material is Lighting.j3md (which supports Phong illumination). The jMonkeyPlatform can create Material files, and it offers a visual editor where you can select and set properties, and preview the outcome. The Palatte contains code snippets that demo how to load materials. +
+ ++ +Some parameters are "optional" because they are somewhat advanced. If you don't know what an option means, chances are that you are not using this feature in your textures – and you don't need to specify it. (E.g. YCoCg and LATC are image compression formats; Minnaert and WardIso are shader types.) +
+ +
+Also note that many other parameters are optional, even if they are not explicitly marked optional. For example, it's okay to specify solely the DiffuseMap
and NormalMap
when using Lighting.j3md
. You are only using a subset of what is possible, but if that's what you want, you can do that. The developer should be in contact with the designer regarding what jME features individual Materials/Textures require.
+
Material Definition | Usage | Parameter : Type | +
---|---|---|
Common/MatDefs/Misc/Unshaded.j3md | Standard unlit Material. Use this for simple coloring, simple texturing, simple glow, simple transparency. +See also: Hello Material | ColorMap : Texture +LightMap : Texture +Color : Color +VertexColor : Boolean +SeparateTexCoord : Boolean +GlowMap : Texture +GlowColor: Color |
+
Common/MatDefs/Misc/Sky.j3md | A solid skyblue, or use with a custom SkyDome texture. +See also: Sky | Texture : TextureCubeMap +SphereMap : Boolean +NormalScale : Vector3 |
+
Common/MatDefs/Terrain/Terrain.j3md | Splat textures for e.g. terrains. +See also: Hello Terrain | Texture1 : Texture (red) +Texture1Scale : Float +Texture2 : Texture (green) +Texture2Scale : Float +Texture3 : Texture (blue) +Texture3Scale : Float +Alpha : Texture |
+
Common/MatDefs/Misc/Particle.j3md | Used with texture masks for particle effects, or for point sprites. +The Quadratic value scales the particle for perspective view (). +Does support an optional colored glow effect. +See also: Hello Effects | Texture : Texture +GlowMap : Texture +GlowColor : Color +Quadratic : Float +PointSprite : Boolean |
+
+
+
+
+
Material Definition | Usage | Parameters | +
---|---|---|
Common/MatDefs/Light/Lighting.j3md | Standard lit material with Phong Illumination. Use this material together with DiffuseMap, SpecularMap, BumpMap (NormalMaps, ParalaxMap) textures. Supports shininess, transparency, and plain material colors (Diffuse, Ambient, Specular colors). +See also: Hello Material +Note: Lit materials require a light source! Glowing materials require a FilterPostProcessor! | DiffuseMap : Texture +UseAlpha1) : Boolean +NormalMap : Texture +LATC2) : Boolean +SpecularMap : Texture +Shininess : Float [1-128] +ParallaxMap : Texture +AlphaMap : Texture +AlphaDiscardThreshold: Float +ColorRamp : Texture +Glow (optional) +GlowMap : Texture +GlowColor : Color +Performance and quality (optional) +VertexLighting : Boolean +UseVertexColor : Boolean +LowQuality : Boolean +HighQuality : Boolean +Material Colors (optional) +UseMaterialColors : Boolean +Diffuse : Color + Ambient : Color +Specular : Color +Tangent shading (optional): +VTangent : Boolean +Minnaert3) : Boolean +WardIso4) : Boolean |
+
Common/MatDefs/Terrain/TerrainLighting.j3md | Same kind of splat texture as Terrain.j3md, but with shading. +Requires a light source. | Color Diffuse : Color +Ambient : Color +Shininess : Float +Specular : Color +SpecularMap : Texture +WardIso : Boolean +useTriPlanarMapping : Boolean +Texture Splat Maps +DiffuseMap : Texture +DiffuseMap_0_scale : Float +NormalMap : Texture +DiffuseMap_1 : Texture +DiffuseMap_1_scale : Float +NormalMap_1 : Texture +DiffuseMap_2 : Texture +DiffuseMap_2_scale : Float +NormalMap_2 : Texture +DiffuseMap_3 : Texture +DiffuseMap_3_scale : Float +NormalMap_3 : Texture +Alpha Maps +AlphaMap : Texture +AlphaMap_1 : Texture +AlphaMap_2 : Texture +Glowing +GlowMap : Texture +GlowColor : Color |
+
Common/MatDefs/Light/Reflection.j3md | Reflective glass material with environment map (CubeMap/SphereMap). +Requires light source. +See also: | Texture : Texture +SphereMap: Boolean |
+
+
+Shininess Tip: To deactivate Shininess, do not set Shininess
to 0, but instead set the Specular
color to ColorRGBA.Black
.
+
+Bumpiness Tip: Before you can use NormalMaps, you must generate normals for the mesh (not the Geometry). +
+TangentBinormalGenerator.generate(mesh);+ +
Material Definition | Usage | Parameters | +
---|---|---|
Common/MatDefs/Misc/ShowNormals.j3md | A color gradient calculated from the model's surface normals. You can use this built-in material to test models that have no material, or as fall-back default material. | – | +
+
+
+Note: Common/MatDefs/Misc/SimpleTextured.j3md, ColoredTextured.j3md, VertexColor.j3md, Wireframe.j3md have been deprecated. Use equivalent features of Unshaded.j3md instead.
+
+
+Most Material Definitions support an alpha channel for transparency. In an RGBA color, the last float is the alpha channel: 0.0f is transparent, 1.0f is opaque.
+
+For example: mat.setColor("Color", new ColorRGBA(1,0,0,0.5f));
is a half-opaque red.
+
+Additionally, you must specify a blendmode:
+
Option | Usage | +
---|---|
mat.getAdditionalRenderState().setBlendMode(BlendMode.Off); | Opaque | +
mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); | Use this for normal transparency. Interpolates the background pixel with the current by using the current pixel's alpha. E.g. alpha-blended vegetation. | +
mat.getAdditionalRenderState().setBlendMode(BlendMode.Additive); | Additive alpha blending adds colors in a commutative way, i.e. the result does not depend on the order of transparent layers. Adds the background pixel color with the current pixel color. E.g. particle effects that have black color as background. | +
mat.getAdditionalRenderState().setBlendMode(BlendMode.AlphaAdditive); | Same as "Additive", except first it multiplies the current pixel color by the pixel alpha. E.g. used for particle effects that have alpha as background. | +
mat.getAdditionalRenderState().setBlendMode(BlendMode.Color); | Blends by color. Generally useless. | +
mat.getAdditionalRenderState().setBlendMode(BlendMode.Modulate); | Multiplies the background pixel by the current pixel. | +
mat.getAdditionalRenderState().setBlendMode(BlendMode.ModulateX2); | Same as "Modulate", except the result is doubled. | +
mat.getAdditionalRenderState().setBlendMode(BlendMode.PremultAlpha); | Pre-multiplied alpha blending. E.g. if the color of the object has already been multiplied by its alpha, this is used instead of "Alpha" blend mode. | +
mat.getAdditionalRenderState().setDepthWrite(false); | Use this if you have several transparent objects obscuring one another. Disables writing of the pixel's depth value to the depth buffer. | +
mat.getAdditionalRenderState().setAlphaFallOff(0.5f); +mat.getAdditionalRenderState().setAlphaTest(true) | Enables alpha test, generally used for vegetation. Works the same way as "AlphaDiscardThreshold". | +
+
+
+Also note the AlphaDiscardThreshold value for materials based on Lighting.j3md. The renderer does not render pixels whose transparancy is below the threshold.
+
+
mat.getAdditionalRenderState().setWireframe(true); | Switch to showing the (textured) Material in wireframe mode | +
mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off); +mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Front); +mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Back); +mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.FrontAndBack) | Activate back- or frontface culling, both (=invisible), or off. Backface culling is activated by default as an optimization. | +
mat.getAdditionalRenderState().setColorWrite(false); | Disable writing the color of pixels. Use this together with setDepthWrite(true) to write pixels only to the depth buffer for example. | +
mat.getAdditionalRenderState().setPointSprite(true); | Enables point-sprite mode, so meshes with "Mode.Points" will be rendered as textured sprites. Note that gl_PointCoord must be set in the shader. Point sprites are used for hardware accelerated particle effects. | +
mat.getAdditionalRenderState().setPolyOffset(); | Enable polygon offset. Use this when you have meshes that have triangles really close to each over (e.g. ), it will shift the depth values to prevent . | +
All visible game elements in a scene, whether it is a Model or a Shape, are made up of polygon meshes. JME3 has a com.jme3.scene.Mesh class that represents all meshes.
getTriangleCount(…)
and getTriangle(…)
getId()
collideWith()
.setStatic()
and unlocked with setDynamic()
.You can use default Shapes as meshes; load 3D models (i.e. meshes created in external applications); or create free-form custom meshes programmatically.
The VertexBuffer contains a particular type of geometry data used by Meshes. Every VertexBuffer set on a Mesh is sent as an attribute to the vertex shader to be processed.
Vertex Buffer Type | Description |
---|---|
Type.Position | Position of the vertex (3 floats) |
Type.Index | Specifies the index buffer, must contain integer data. |
Type.TexCoord | Texture coordinate |
Type.TexCoord2 | Texture coordinate #2 |
Type.Normal | Normal vector, normalized. |
Type.Tangent | Tangent vector, normalized. |
Type.Binormal | Binormal vector, normalized. |
Type.Color | Color and Alpha (4 floats) |
Type.Size | The size of the point when using point buffers. |
Type.InterleavedData | Specifies the source data for various vertex buffers when interleaving is used. |
Type.BindPosePosition | Inital vertex position, used with animation. |
Type.BindPoseNormal | Inital vertex normals, used with animation |
Type.BoneWeight | Bone weights, used with animation |
Type.BoneIndex | Bone indices, used with animation |
Mesh method | Description |
---|---|
setLineWidth(1) | |
setPointSize(4.0f) | |
setBound(boundingVolume) | |
setStatic() | Locks the mesh so you cannot modify it anymore, thus optimizing its data (faster). |
setDynamic() | Unlocks the mesh so you can modified it, but this will un-optimize the data (slower). |
setMode(Mesh.Mode.Points) | Used to set mesh modes, see below |
getId() | |
getTriangle(int,tri) | |
scaleTextureCoordinates(Vector2f) |
Mesh Mode | Description |
---|---|
Mesh.Mode.Points | Show only corner points |
Mesh.Mode.Lines | Show lines |
Mesh.Mode.LineLoop | ? |
Mesh.Mode.LineStrip | ? |
Mesh.Mode.Triangles | ? |
Mesh.Mode.TriangleStrip | ? |
Mesh.Mode.TriangleFan | ? |
Mesh.Mode.Hybrid | ? |
+ + +
+ ++All visible game elements in a scene, whether it is a Model or a Shape, are made up of polygon meshes. JME3 has a com.jme3.scene.Mesh class that represents all meshes. + +
+getTriangleCount(…)
and getTriangle(…)
getId()
collideWith()
.setStatic()
and unlocked with setDynamic()
. + +You can use default Shapes as meshes; load 3D models (i.e. meshes created in external applications); or create free-form custom meshes programmatically. +
+ ++ +The VertexBuffer contains a particular type of geometry data used by Meshes. Every VertexBuffer set on a Mesh is sent as an attribute to the vertex shader to be processed. + +
+Vertex Buffer Type | Description | +
---|---|
Type.Position | Position of the vertex (3 floats) | +
Type.Index | Specifies the index buffer, must contain integer data. | +
Type.TexCoord | Texture coordinate | +
Type.TexCoord2 | Texture coordinate #2 | +
Type.Normal | Normal vector, normalized. | +
Type.Tangent | Tangent vector, normalized. | +
Type.Binormal | Binormal vector, normalized. | +
Type.Color | Color and Alpha (4 floats) | +
Type.Size | The size of the point when using point buffers. | +
Type.InterleavedData | Specifies the source data for various vertex buffers when interleaving is used. | +
Type.BindPosePosition | Inital vertex position, used with animation. | +
Type.BindPoseNormal | Inital vertex normals, used with animation | +
Type.BoneWeight | Bone weights, used with animation | +
Type.BoneIndex | Bone indices, used with animation | +
Mesh method | Description | +
---|---|
setLineWidth(1) | +|
setPointSize(4.0f) | +|
setBound(boundingVolume) | +|
setStatic() | Locks the mesh so you cannot modify it anymore, thus optimizing its data (faster). | +
setDynamic() | Unlocks the mesh so you can modified it, but this will un-optimize the data (slower). | +
setMode(Mesh.Mode.Points) | Used to set mesh modes, see below | +
getId() | +|
getTriangle(int,tri) | +|
scaleTextureCoordinates(Vector2f) | +
Mesh Mode | Description | +
---|---|
Mesh.Mode.Points | Show only corner points | +
Mesh.Mode.Lines | Show lines | +
Mesh.Mode.LineLoop | ? | +
Mesh.Mode.LineStrip | ? | +
Mesh.Mode.Triangles | ? | +
Mesh.Mode.TriangleStrip | ? | +
Mesh.Mode.TriangleFan | ? | +
Mesh.Mode.Hybrid | ? | +
+MonkeyZone is an multi-player demo game provided by the jME core developer team. +
++This open-source demo: +
++The game idea is based on “BattleZone” arcade game from the 1980s, a first-person shooter the with real-time strategy elements. +The game was written using the jMonkeyPlatform IDE, and it's based off the BasicGame project template. It took us one week to create a playable pre-alpha, including networking. +The project design follows best practices that make it possible to edit maps, vehicles, etc, in jMonkeyPlatform without having to change the code – This allows 3D graphic designers to contribute models more easily. (If you feel like contributing assets or working on parts of the game code, drop us a note!) + +
+ +
+MonkeyZone is a multi-player game with a physics simulation. Both, clients and server, run the physics simulation. The clients send input data from the player group to the server, where they control the entities, and also broadcast to the clients. Additionally, the server sends regular syncronization data for all objects in the game to prevent drifting.
+When a human user or an AI performs an action (presses a button), the actual logic is done on the server. The results are broadcast as data messages to the entities. When the entity is controlled by an AI, the actual AI code (that determines where the entity should move, and when it should perform an action) is executed on the client.
+
+
+
+The way MonkeyZone is implemented is just one of the many possible ways to do a game like this in jME. Some things might be done more efficiently, some might be done in another way completely. MonkeyZone tries to do things the way that are most appropriate to implement the game at hand and it shows nicely how jME3 and the standard Java API can make game development easier and faster. Also note that the way MonkeyZone is designed is not scalable to a MMO style game, it will only work in a FPS style environment where the whole game world can be loaded at once.
+
+
+The game uses certain terms that might be familiar to you but maybe used in another way, so heres a quick rundown on the terms being used. +
++The WorldManager does the main work of organizing players, entities and the world and synchronizing them between the server and client. Both client and server use this class. Some other managers like ClientEffectsManager only exist on the client or server and manage e.g. effects display. +The gameplay is largely controlled by the ServerGameManager which does gameplay logic on the server, combined with the actions issued by the AI and user on the client (see below) it implements the gameplay. It extensively uses the functions exposed by the WorldManager to perform actions and gather data. This is also the class where the actions of the players are actually executed on the server to determine the outcome (ray testing for shooting etc.). + +
+ ++Controls are used extensively in MonkeyZone for many aspects of the game. When a player enters an entity, the Spatials Controls are configured based on the player that enters. For example when the human user enters an entity, Controls that update the user interface (DefaultHUDControl) or user input (UserInputControl) are added to the current entity Spatial. + +
+ ++Controls attached to Spatials are generally used like an “array of capabilities” that the entity posesses. So when an entity has a VehicleControl its expected to be a vehicle, when its got a CharacterControl its expected to be a character. +Other Controls work completely on their own, like CharacterAnimControl which just uses the CharacterControl of the entity to check if the character is running, jumping etc. and then animates the entity if it has an AnimControl. + +
+ ++Furthermore theres special interfaces for Controls that allow abstraction of different Controls into one base interface. For example ManualControl and AutonomousControl are interfaces for controls that manage the movement of a spatial in a generalized way. This way AI code and e.g. the UserInputControl only have to check for a valid AutonomousControl or ManualControl on the spatial to control and move it. The details of the movement are handled by classes like ManualVehicleControl and AutonomousCharacterControl. + +
+ ++A special Control called CommandControl handles the Commands that can be executed by user controlled players, see below. + +
+ ++MonkeyZone includes simple AI functions based on a command queue. + +
+ ++To implement autonomous AI players MonkeyZone uses a system of Commands that are managed by a CommandControl that is attached to each AI player entity controlled by the user. This CommandControl manages a list of Commands that are executed based on priority. For example theres a MoveCommand, a FollowCommand and an AttackCommand, Commands can however implement more complete behavior than that, e.g. the complete logic for a scavenging entity. +
++The SphereTrigger is a TriggerControl that is also attached to each AI players current entity. It consists of a GhostControl that checks the overlapping entities around the entity its attached to. It can be assigned a command that is checked with every entity entering the SphereTrigger and executed if applicable (e.g. normal “attack enemy” mode). + +
+ ++For each map a navigation mesh is generated that allows the entities to navigate the terrain. Autonomous entities automatically get a NavigationControl based on the current map. The AutonomousControl implementations automatically recognize the NavigationControl attached to the Spatial and use it for navigation. The NavMeshNavigationControl implementation contains a reference to the levels NavMesh and implements a navigation algorithm similar to the A* algorithm. + +
+ ++Networking is realized in the PhysicsSyncManager which we hope to extend to a state where it can serve as a general sync system for physics based network games. +The sync manager basically puts a timestamp on every message sent from the server and then buffers all arriving messages on the client within a certain time window. This allows to compensate for messages arriving too soon or too late within the constraints of the buffer, a future version might step the clients physics space different to compensate for network delays without “snapping”. + +
+ ++All assets used in the game, like entity models and loaded maps can be preconfigured and edited using jMonkeyPlatform. For example, to add a new vehicle type, a vehicle is created in the jMP vehicle editor and UserData like Speed, HitPoints etc. is applied directly in the editor. When the model is loaded in the game it is automatically configured based on these settings, the same accounts for maps that are loaded, special Nodes that mark e.g. player start locations are recognized automatically etc. + +
+ ++Entities that are loaded from disk have certain UserData like HitPoints, Speed etc. that is used to configure the entity at runtime. jMP allows adding and editing this UserData so entity properties are editable visually. + +
+ ++VehicleControls, CharacterControls and RigidBodyControls with mesh collision shape for terrain and objects are generated in jMP and saved in the entity j3o file. When an entity is loaded, the type of entity is identified based on the available controls and UserData and it is configured accordingly. + +
+ ++Editable UserData of entity Spatials: +
++Entity Spatial marking Node names: +
++Level Spatial marking Node names: +
++Programmatic UserData of entities: +
++Programmatic PlayerData: +
++Have a look at the code and feel free to ask about it, if you want any new features, you are free to implement them. ;) +MonkeyZone is hosted at GoogleCode, where you can check out the jMonkeyPlatform-ready project via svn: +
+
+ +A MotionPath describes the motion of a spatial between waypoints. The path can be linear or rounded. You use MotionPaths to remote-control a spatial, or the camera. +
+ ++Tip: If you want to remote-control a whole cutscene with several spatials moving at various times, then we recommened you use MotionPaths together with Cinematics. +
+ ++ +When shooting a movie scene, the director tells actors where to walk, for example, by drawing a series of small crosses on the floor. Cameramen often mount the camera on rails (so called dolly track) so they can follow along complex scenes more easily. +
+ ++In JME3, you use MotionPaths to specify a series of positions for a character or the camera. The MotionPath automatically updates the transformation of the spatial in each frame to make it move from one point to the next. +
++The final shape of the path is computed using a linear interpolation or a spline interpolation on the way points. +
+ ++ +Create a Motionpath object and add way points to it. + +
+MotionPath path = new MotionPath(); +path.addWayPoint(new Vector3f(10, 3, 0)); +path.addWayPoint(new Vector3f(8, -2, 1)); +...+ +
+You can configure the path as follows. + +
+MotionPath Method | Usage | +
---|---|
path.setCycle(true) | Sets whether the motion along this path should be closed (true) or open-ended (false). | +
path.addWayPoint(vector) | Adds individual waypoints to this path. The order is relevant. | +
path.removeWayPoint(vector) +removeWayPoint(index) | Removes a way point from this path. You can specify the point that you want to remove as vector or as integer index. | +
path.setCurveTension(0.83f) | Sets the tension of the curve (Catmull-Rom Spline). A value of 0.0f results in a straight linear line, 1.0 a very round curve. | +
path.getNbWayPoints() | Returns the number of waypoints in this path. | +
path.enableDebugShape(assetManager,rootNode) | Shows a line that visualizes the path. Use this during development and for debugging so you see what you are doing. | +
path.disableDebugShape() | Hides the line that visualizes the path. Use this for the release build. | +
+
+You can hook interactions into a playing MotionPath. Register a MotionPathListener to the MotionPath to track whether way points have been reached, and then trigger a custom action. The onWayPointReach() method of the interface gives you access to the MotionTrack object control
, and an integer value representing the current wayPointIndex.
+
+In this example, you just print the status at every way point. In a game you could trigger actions here: Transformations, animations, sounds, game actions (attack, open door, etc). +
+path.addListener( new MotionPathListener() { + public void onWayPointReach(MotionTrack control, int wayPointIndex) { + if (path.getNbWayPoints() == wayPointIndex + 1) { + println(control.getSpatial().getName() + " has finished moving. "); + } else { + println(control.getSpatial().getName() + " has reached way point " + wayPointIndex); + } + } +});+ +
+ +Mouse picking means that the user clicks an object in the scene to select it, or to interact with it otherwise. Games use picking to implement aiming and shooting, casting spells, picking up objects, selecting targets, dragging and moving objects, etc. Mouse picking can be done using fixed crosshairs, or using the mouse pointer. +
+ ++ +
+ ++See Input Handling for details on how to define the necessary input triggers, input mappings, and input listeners. +
+ +
+
+The following pick target
input mapping implements an action that determines what a user clicked. It assumes that the mouse pointer is invisible and there are crosshairs painted in the center of the screen. It assumes that the user aims the crosshairs at an object in the scene and clicks. You use Ray Casting to identify the geometry that was picked by the user. Use use this method together with a first-person flyCam.
+
+
flyCam.setEnabled(true);
inputManager.setCursorVisible(false)
.pick target
action to a MouseButtonTrigger. +The following example rotates Spatials named "Red Box" or "Blue Box" when they are clicked. Modify this code to do whatever your game needs to do with the identified target (shoot it, take it, move it, etc). +
+private AnalogListener analogListener = new AnalogListener() { + public void onAnalog(String name, float intensity, float tpf) { + if (name.equals("pick target")) { + // Reset results list. + CollisionResults results = new CollisionResults(); + // Aim the ray from camera location in camera direction + // (assuming crosshairs in center of screen). + Ray ray = new Ray(cam.getLocation(), cam.getDirection()); + // Collect intersections between ray and all nodes in results list. + rootNode.collideWith(ray, results); + // Print the results so we see what is going on + for (int i = 0; i < results.size(); i++) { + // For each “hit”, we know distance, impact point, geometry. + float dist = results.getCollision(i).getDistance(); + Vector3f pt = results.getCollision(i).getContactPoint(); + String target = results.getCollision(i).getGeometry().getName(); + System.out.println("Selection #" + i + ": " + target + " at " + pt + ", " + dist + " WU away."); + } + // 5. Use the results -- we rotate the selected geometry. + if (results.size() > 0) { + // The closest result is the target that the player picked: + Geometry target = results.getClosestCollision().getGeometry(); + // Here comes the action: + if(target.getName().equals("Red Box")) + target.rotate(0, - intensity, 0); + else if(target.getName().equals("Blue Box")) + target.rotate(0, intensity, 0); + } + } // else if ... + } + };+ +
+
+The following pick target
input mapping implements an action that determines what a user clicked. It assumes that the mouse pointer is visible, and the user aims the cursor at an object in the scene. You use ray casting to determine the geometry that was picked by the user.
+
+Note: Picking with a visible pouse pointer implies that your application can no longer use the default flyCam where the MouseAxisTrigger rotates the camera. You have to deactivate the flyCam mappings and provide custom mappings. Either different inputs rotate the camera, or the camera is fixed. + +
+pick target
action to a MouseButtonTrigger. inputManager.setCursorVisible(true)
.+ +The following example rotates Spatials named "Red Box" or "Blue Box" when they are clicked. Modify this code to do whatever your game needs to do with the identified target (shoot it, take it, move it, etc). +
+private AnalogListener analogListener = new AnalogListener() { + public void onAnalog(String name, float intensity, float tpf) { + if (name.equals("pick target")) { + // Reset results list. + CollisionResults results = new CollisionResults(); + // Convert screen click to 3d position + Vector2f click2d = inputManager.getCursorPosition(); + Vector3f click3d = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); + Vector3f dir = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); + // Aim the ray from the clicked spot forwards. + Ray ray = new Ray(click3d, dir); + // Collect intersections between ray and all nodes in results list. + rootNode.collideWith(ray, results); + // (Print the results so we see what is going on:) + for (int i = 0; i < results.size(); i++) { + // (For each “hit”, we know distance, impact point, geometry.) + float dist = results.getCollision(i).getDistance(); + Vector3f pt = results.getCollision(i).getContactPoint(); + String target = results.getCollision(i).getGeometry().getName(); + System.out.println("Selection #" + i + ": " + target + " at " + pt + ", " + dist + " WU away."); + } + // Use the results -- we rotate the selected geometry. + if (results.size() > 0) { + // The closest result is the target that the player picked: + Geometry target = results.getClosestCollision().getGeometry(); + // Here comes the action: + if (target.getName().equals("Red Box")) { + target.rotate(0, -intensity, 0); + } else if (target.getName().equals("Blue Box")) { + target.rotate(0, intensity, 0); + } + } + } // else if ... + } + };+ + +
You can split the screen and look into the 3D scene from different camera angles at the same time. In this example, we create four views (2x2) with the same aspect ratio as the normal view, but half the size.
The packages used in this example are com.jme3.renderer.Camera
and com.jme3.renderer.ViewPort
. You can get the full sample code here: TestMultiViews.java
We use the preconfigured Camera cam
and viewPort
from SimpleApplication
for the first view.
viewPort.setBackgroundColor(ColorRGBA.Blue); + ++ +Multiple Camera Views
++ ++ ++ +You can split the screen and look into the 3D scene from different camera angles at the same time. In this example, we create four views (2x2) with the same aspect ratio as the normal view, but half the size. +
+ ++The packages used in this example are
+ +com.jme3.renderer.Camera
andcom.jme3.renderer.ViewPort
. You can get the full sample code here: +Set up the First View
++ ++ +We use the preconfigured Camera
+cam
andviewPort
fromSimpleApplication
for the first view. +viewPort.setBackgroundColor(ColorRGBA.Blue); cam.setViewPort(.5f, 1f, 0f, 0.5f); // resize the viewPort cam.setLocation(new Vector3f(3.3212643f, 4.484704f, 4.2812433f)); -cam.setRotation(new Quaternion (-0.07680723f, 0.92299235f, -0.2564353f, -0.27645364f));Place the main camera in the scene and rotate it in its start position.
We will have a detailed look at how we use setViewPort() to position and resize the default view later.
Set up Three Additional Views
+ +Here is the outline for how you create the three other cams and viewPorts (Full code sample is here.) In the code snippet,
cam_n
stand forcam_2
-cam_4
, respectively, same forview_n
.
Clone the first cam to reuse its settings Resize and position the cam's viewPort with setViewPort() – details below. Place the cameras in the scene and rotate them. Create a main view for each camera Reset the cameras' enabled statuses Attach the rootNode to be displayed to this view
It doesn't have to be rootNode, but that is the most common use case You can set other optional view properties such as backgroundColorCamera cam_n = cam.clone(); +cam.setRotation(new Quaternion (-0.07680723f, 0.92299235f, -0.2564353f, -0.27645364f));+ ++Place the main camera in the scene and rotate it in its start position. +
+ ++We will have a detailed look at how we use setViewPort() to position and resize the default view later. +
+ +Set up Three Additional Views
++ ++ +Here is the outline for how you create the three other cams and viewPorts (.) In the code snippet,
+cam_n
stand forcam_2
-cam_4
, respectively, same forview_n
. + ++
+- +
Clone the first cam to reuse its settings+- +
Resize and position the cam's viewPort with setViewPort() – details below.+- +
Place the cameras in the scene and rotate them.+- +
Create a main view for each camera+- +
Reset the cameras' enabled statuses+- +
Attach the rootNode to be displayed to this view++
+- +
It doesn't have to be rootNode, but that is the most common use case+- +
You can set other optional view properties such as backgroundColor+Camera cam_n = cam.clone(); cam_n.setViewPort(...); // resize the viewPort cam_n.setLocation(new Vector3f(...)); cam_n.setRotation(new Quaternion(...)); @@ -33,16 +71,37 @@ cam_n.setRotation(new Quaternion(...)); ViewPort view_n = renderManager.createMainView("View of camera #n", cam_n); view_n.setClearEnabled(true); view_n.attachScene(rootNode); -view_n.setBackgroundColor(ColorRGBA.Black);How to resize and position the ViewPorts
+ +How does jme know which of the four views should appear where on the screen?
Imagine the view as a 1x1-sized box. By default, the settings is
cam.setViewPort(0f, 1f, 0f, 1f);
. This means the view takes up the whole box, from 0 to 1 left to right, and from 0 to 1 bottom to top.In the code sample, note the following four lines:
cam.setViewPort( 0.5f, 1.0f, 0.0f, 0.5f); +view_n.setBackgroundColor(ColorRGBA.Black);+ +How to resize and position the ViewPorts
++ ++ +How does jme know which of the four views should appear where on the screen? +
+ ++Imagine the view as a 1x1-sized box. By default, the settings is
+ +cam.setViewPort(0f, 1f, 0f, 1f);
. This means the view takes up the whole box, from 0 to 1 left to right, and from 0 to 1 bottom to top. ++In the , note the following four lines: +
+cam.setViewPort( 0.5f, 1.0f, 0.0f, 0.5f); ... cam_2.setViewPort(0.0f, 0.5f, 0.0f, 0.5f); ... cam_3.setViewPort(0.0f, 0.5f, 0.5f, 1.0f); ... -cam_4.setViewPort(0.5f, 1.0f, 0.5f, 1.0f);These viewport parameters are, in this order, the left - right - bottom - top extend of a camera's box on the screen. Note that we have set a few values to 0.5f – this is where we resize each view to half its default height and width.
0.0 , 1.0 1.0 , 1.0 +cam_4.setViewPort(0.5f, 1.0f, 0.5f, 1.0f);+ ++These viewport parameters are, in this order, the left - right - bottom - top extend of a camera's box on the screen. Note that we have set a few values to 0.5f – this is where we resize each view to half its default height and width. +
+0.0 , 1.0 1.0 , 1.0 +----+----+ | | | |cam3|cam4| @@ -50,14 +109,35 @@ cam_4.setViewPort(0.5f, 1.0f, 0.5f, 1.0f);These viewport param | | | |cam2|cam | +----+----+ -0.0 , 0.0 1.0 , 0.0
Example: Cam3's rect extends from bottom-left (0.0 , 0.5) to top-right (0.5 , 1.0)
The left corner is at 0, and the right corner is 0.5 on the x axis. The bottom of the box is at 0.5 and the top at 1.0 on the y axis.Other Layouts
- \ No newline at end of file +0.0 , 0.0 1.0 , 0.0This layout shows 2x2 views. For a split screen you may want to lay out two views, one above the other, or one next to the other.
If you scale the views in a way so that the aspect ratio changes, the views will obviously be distorted. In these cases, create custom camera objects with the right aspect ratio (redefine the default cam).
+Example: Cam3's rect extends from bottom-left (0.0 , 0.5) to top-right (0.5 , 1.0) +
++ +This layout shows 2x2 views. For a split screen you may want to lay out two views, one above the other, or one next to the other. +
+ ++If you scale the views in a way so that the aspect ratio changes, the views will obviously be distorted. In these cases, create custom camera objects with the right aspect ratio (redefine the default cam). +
+First, make sure you know what Application States and Custom Controls are.
More complex games may feature complex mathematical operations or artificially intelligent calculations (such as path finding for several NPCs). If you make many time-intensive calls on the same thread (in the update loop), they will block one another, and thus slow down the game to a degree that makes it unplayable. If your game requires long running tasks, you should run them concurrently on separate threads, which speeds up the application considerably.
Often multithreading means having separate detached logical loops going on in parallel, which communicate about their state. (For example, one thread for AI, one Sound, one Graphics). However we recommend to use a global update loop for game logic, and do multithreading within that loop when it is appropriate. This approach scales way better to multiple cores and does not break up your code logic.
Effectively, each for-loop in the main update loop might be a chance for multithreading, if you can break it up into self-contained tasks.
The java.util.concurrent package provides a good foundation for multithreading and dividing work into tasks that can be executed concurrently (hence the name). The three basic components are the Executor, Callable Objects (the tasks), and Future Objects. You can read about the concurrent package more here, I will give just a short introduction.
So how do we implement multithreading in jME3?
Let's take the example of a Control that controls an NPC Spatial. The NPC Control has to compute a lengthy pathfinding operation for each NPC. If we would execute the operations directly in the simpleUpdate() loop, it would block the game each time a NPC wants to move from A to B. Even if we move this behaviour into the update() method of a dedicated NPC Control, we would still get annoying freeze frames, because it still runs on the same update loop thread.
To avoid slowdown, we decide to keep the pathfinding operations in the NPC Control, but execute it on another thread.
You create the executor object in a global AppState (or the initSimpleApp() method), in any case in a high-level place where multiple controls can access it.
/* This constructor creates a new executor with a core pool size of 4. */ -ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(4);
Pool size means the executor will keep four threads alive at any time. Having more threads in the pool means that more tasks can run concurrently. But a bigger pool only results in a speed gain if the PC can handle it! Allocating a pool that is uselessly large just wastes memory, so you need to find a good compromise: About the same to double the size of the number of cores in the computer makes sense.
In the NPC Control, we create the individual objects that the thread manipulates. In our example case (the pathfinding control), the task is about locations and path arrays, so we need the following variables:
//The vector to store the desired location in: + ++ +Multithreading Optimization
++ ++ ++ +First, make sure you know what Application States and Custom Controls are. +
+ ++More complex games may feature complex mathematical operations or artificially intelligent calculations (such as path finding for several NPCs). If you make many time-intensive calls on the same thread (in the update loop), they will block one another, and thus slow down the game to a degree that makes it unplayable. If your game requires long running tasks, you should run them concurrently on separate threads, which speeds up the application considerably. +
+ ++Often multithreading means having separate detached logical loops going on in parallel, which communicate about their state. (For example, one thread for AI, one Sound, one Graphics). However we recommend to use a global update loop for game logic, and do multithreading within that loop when it is appropriate. This approach scales way better to multiple cores and does not break up your code logic. +
+ ++Effectively, each for-loop in the main update loop might be a chance for multithreading, if you can break it up into self-contained tasks. +
+ +Java Multithreading
++ ++ ++ +The java.util.concurrent package provides a good foundation for multithreading and dividing work into tasks that can be executed concurrently (hence the name). The three basic components are the Executor, Callable Objects (the tasks), and Future Objects. You can , I will give just a short introduction. + +
++
+ +- +
A Callable is a class with a method call() that gets executed on a thread in the Executor. It represents one task (e.g, path finding).+- +
The Executor is one central object that manages the threads that are running to execute the Callables. Every time a Callable is added to the Executor, the Executor returns a Future object for it.+- +
A Future is an object that you use to check the status of an individual Callable's execution. It also gives you the return value in case one is created.+Multithreading in jME3
++ ++ ++ +So how do we implement multithreading in jME3? +
+ ++Let's take the example of a Control that controls an NPC Spatial. The NPC Control has to compute a lengthy pathfinding operation for each NPC. If we would execute the operations directly in the simpleUpdate() loop, it would block the game each time a NPC wants to move from A to B. Even if we move this behaviour into the update() method of a dedicated NPC Control, we would still get annoying freeze frames, because it still runs on the same update loop thread. +
+ ++To avoid slowdown, we decide to keep the pathfinding operations in the NPC Control, but execute it on another thread. +
+ +Executor
++ ++ ++ +You create the executor object in a global AppState (or the initSimpleApp() method), in any case in a high-level place where multiple controls can access it. +
+/* This constructor creates a new executor with a core pool size of 4. */ +ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(4);+ ++Pool size means the executor will keep four threads alive at any time. Having more threads in the pool means that more tasks can run concurrently. But a bigger pool only results in a speed gain if the PC can handle it! Allocating a pool that is uselessly large just wastes memory, so you need to find a good compromise: About the same to double the size of the number of cores in the computer makes sense. +
+ +Control Class Fields
++ ++ +In the NPC Control, we create the individual objects that the thread manipulates. In our example case (the pathfinding control), the task is about locations and path arrays, so we need the following variables: +
+//The vector to store the desired location in: Vector3f desiredLocation = new Vector3f(); //The MyWayList object that contains the result waylist: MyWayList wayList = null; //The future that is used to check the execution status: -Future future = null;Here we also created the Future variable to track the state of this task.
Control Update() Method
+ +Next let's look at the update() call of the Control where the time-intensive task starts. In our example, the task is the
findWay
Callable (which contains the pathfinding process). So instead of spelling out the pathfinding process in the Control's update() loop, we start the process viafuture = executor.submit(findWay);
.public void update(float tpf) { +Future future = null;+ ++Here we also created the Future variable to track the state of this task. +
+ +Control Update() Method
++ ++ +Next let's look at the update() call of the Control where the time-intensive task starts. In our example, the task is the
+findWay
Callable (which contains the pathfinding process). So instead of spelling out the pathfinding process in the Control's update() loop, we start the process viafuture = executor.submit(findWay);
. +public void update(float tpf) { try{ //If we have no waylist and not started a callable yet, do so! if(wayList == null && future == null){ @@ -47,20 +123,55 @@ class="level3">Next let's look at the update() call of the Control wher future = null; } } - } - catch(Exception e){ + } + catch(Exception e){ Exceptions.printStackTrace(e); } if(wayList != null){ //.... Success! Let's process the wayList and move the NPC... } -}
Note how this logic makes its decision based on the Future object.
Remember not to mess with the class fields after starting the thread, because they are being accessed and modified on the new thread. In more obvious terms: You cannot change the "desired location" of the NPC while the path finder is calculating a different path. You have to cancel the current Future first.
The Callable
+ +The next code sample shows the Callable that is dedicated to performing the long-running task (here, wayfinding). This is the task that used to block the rest of the application, and is now executed on a thread of its own. You implement the task in the Callable always in an inner method named
call()
.The task code in the Callable should be self-contained! It should not write or read any data of objects that are managed by the scene graph or OpenGL thread directly. Even reading locations of Spatials can be problematic! So ideally all data that is needed for the wayfinding process should be available to the new thread when it starts already, possibly in a cloned version so no concurrent access to the data happens.
In reality, you might need access to the game state. If you must read or write a current state from the scene graph, you must have a clone of the data in your thread. There are only two ways:
Use the execution queueapplication.enqueue()
to create a sub-thread that clones the info. Only disadvantage is, it may be slower.
The example below gets theVector3f location
from the scene objectmySpatial
using this way. Create a separate World class that allows safe access to its data via synchronized methods to access the scene graph. Alternatively it can also internally useapplication.enqueue()
.
The following example gets the objectData data = myWorld.getData();
using this way.These two ways are thread-safe, they don't mess up the game logic, and keep the Callable code readable.
// A self-contained time-intensive task: +}+ ++Note how this logic makes its decision based on the Future object. +
+ ++Remember not to mess with the class fields after starting the thread, because they are being accessed and modified on the new thread. In more obvious terms: You cannot change the "desired location" of the NPC while the path finder is calculating a different path. You have to cancel the current Future first. +
+ +The Callable
++ ++ +The next code sample shows the Callable that is dedicated to performing the long-running task (here, wayfinding). This is the task that used to block the rest of the application, and is now executed on a thread of its own. You implement the task in the Callable always in an inner method named
+ +call()
. ++The task code in the Callable should be self-contained! It should not write or read any data of objects that are managed by the scene graph or OpenGL thread directly. Even reading locations of Spatials can be problematic! So ideally all data that is needed for the wayfinding process should be available to the new thread when it starts already, possibly in a cloned version so no concurrent access to the data happens. +
+ ++In reality, you might need access to the game state. If you must read or write a current state from the scene graph, you must have a clone of the data in your thread. There are only two ways: + +
++
+ +- +
Use the execution queue+application.enqueue()
to create a sub-thread that clones the info. Only disadvantage is, it may be slower.
+The example below gets theVector3f location
from the scene objectmySpatial
using this way.- +
Create a separate World class that allows safe access to its data via synchronized methods to access the scene graph. Alternatively it can also internally use+application.enqueue()
.
+The following example gets the objectData data = myWorld.getData();
using this way.+ +These two ways are thread-safe, they don't mess up the game logic, and keep the Callable code readable. +
+// A self-contained time-intensive task: private Callable<MyWayList> findWay = new Callable<MyWayList>(){ public MyWayList call() throws Exception { @@ -73,20 +184,32 @@ private Callable<MyWayList> findWay = new Callable<MyWayList>( }).get(); // This world class allows safe access via synchronized methods - Data data = myWorld.getData(); + Data data = myWorld.getData(); //... Now process data and find the way ... return wayList; } -};Conclusion
- \ No newline at end of file +};The cool thing about this appraoch is that every entity creates one self-contained Callable for the Executor, and they are all executed in parallel. In theory, you can have one thread per entity without changing anything else but the settings of the executor.
+ +The cool thing about this appraoch is that every entity creates one self-contained Callable for the Executor, and they are all executed in parallel. In theory, you can have one thread per entity without changing anything else but the settings of the executor. + +
+This provides an overview of the new SpiderMonkey API and a path for migrating from the old, now deprecated, API to the newer version. Much has changed.
The original SpiderMonkey implementation was a good concept and a clever implementation but suffered under the weight of rapid patches and some creeping design deficit. In the end, there were enough small problems, long-term maintenance issues, and limitations that a newer design was warranted.
Some things will be very similar but others have changed very much. Hopefully for the better.
Most of the new SpiderMonkey API now exists as a set of interfaces and helper classes in the 'com.jme3.network' package. For most users, this package and the 'message' package will be all they need to worry about. The 'base' and 'kernel' packages only come into play when implementing custom network transports or alternate client/server protocols (which are now possible).
Clients and Servers can be created from the factory methods on the Network helper class. Once a Server instance is created and started, it can accept remote connections from Clients. The Client objects represent the client-side of a client→server connection. Within the Server, these are HostedConnections. This is a distinct change from the old API.
Client | Server | |
---|---|---|
com.jme3.network.Client | ←→ | com.jme3.network.HostedConnection |
HostedConnections can hold application defined client-specific session attributes that the server-side listeners and services can use to track player information, etc..
MessageListeners can be registered with either the Client or the Server to be notified when new messages arrive. As before, these listeners can be registered to be notified about only specific -types of messages.
ClientStateListeners can be registered with a Client to detect changes in connection state.
ConnectionListeners can be registered with a Server to be notified about HostedConnection arrivals and removals.
All of 'connection', 'events', 'queue', 'service', 'streaming', and 'sync' are now deprecated. The 'service', 'streaming', and 'sync' packages were too difficult to easily port to the new API and would have required additional code review for thread-related issues. Since the service manager model has _not_ been ported and will likely live on in a different way, it was better to let these go until better solutions evolve. For example, streaming is probably better done more tightly integrated with the core API and as actual java.io streams.
As a first pass, use the following table for conversion and then see specific class notes.
Old Class | New Class |
---|---|
com.jme3.network.connection.Client | com.jme3.network.Client or com.jme3.network.HostedConnection |
com.jme3.network.connection.Server | com.jme3.network.Server |
com.jme3.network.event.MessageListener | com.jme3.network.MessageListener |
com.jme3.network.event.ConnectionListener | com.jme3.network.ClientStateListener or com.jme3.network.ConnectionListener |
com.jme3.network.event.MessageAdapter | no equivalent class, implement MessageListener directly |
com.jme3.network.event.ConnectionAdapter | no equivalent class, implement ClientStateListener or ConnectionListener directly |
com.jme3.network.message.Message | if used as a reference and not a superclass, com.jme3.network.Message. The base class stays the same for message subclasses. |
Doing all of those changes will certainly break your build… so now let's fix it.
This class is the hardest migration to perform. Do not get discouraged.
The old version used com.jme3.network.connection.Client for both client side and server side. So, depending on context, these references will either change to com.jme3.network.Client or com.jme3.network.HostedConnection. In the case where calling code is not client or server specific, then there is also the common com.jme3.network.MessageConnection interface.
In general, the actual client changes are of one of the following to types:
Client client = new Client( host, port ); - - ...becomes... - - Client client = Network.connectToServer( host, port );
In the delayed connection case:
Client client = new Client(); - ... - client.connect( host, port ); - - ...becomes... - - NetworkClient client = Network.createClient(); - ... - client.connectToServer( host, port );
NetworkClient is a Client. The rest of your code can just refer to Client.
Those are the easy changes. The trickier ones are related to the MessageListeners.
By now you've figured out that all of your MessageListeners are broken because the new method signature is different. The source of a message is no longer stored with the message and is instead provided to the MessageListener.
Depending on whether your MessageListener is being added to the Client or the Server, it will need to refer to either com.jme3.network.Client or com.jme3.network.HostedConnection in its messageReceived(), respectively. The MessageListener interface is generically typed to help make sure the right listener goes where it's supposed to and so the listener implementations don't have to cast all the time.
// An example client-specific listener -public class MyClientListener implements MessageListener<Client> { - - public void messageReceived( Client source, Message m ) { - ...do stuff... - } -} - -// And example server-specific listener -public class MyServerListener implements MessageListener<HostedConnection> { - - public void messageReceived( HostedConnection source, Message m ) { - ...do stuff... - } -} - -// A client or server listener -public class MyGenericListener implements MessageListener<MessageConnection> { - - public void messageReceived( MessageConnection source, Message m ) { - ... do limited stuff.... - } -}
Your listeners will fall into one of those three categories.
Some of the methods on the old Client class have changed or been removed. Here is a basic summary:
Old Method | New Method |
---|---|
Client.disconnect() | Client.close() or HostedConnection.close(reason) |
Client.kick(reason) | HostedConnection.close(reason) |
Client.getClientID() | Client.getId() or HostedConnection.getId() |
Client.get/setPlayerID() | no equivalent |
Client.get/setLabel() | no equivalent |
After you've done all of that, the compiler will be complaining about the fact that send(), broadcast(), etc. no longer throw IOException. So remove all of those try/catch blocks.
Only API methods that actually perform direct IO (such as the Network.connectToServer() and NetworkClient.connectToServer() methods) will ever be declared to throw IOException.
This is important enough to deserve its own sub-heading because your code will break if you use these as they now return null. Any reason for calling them is now provided directly to the MessageListener in the form of the source Client or source HostedConnection.
The ID of the Client and HostedConnection are now the same at both ends of a connection and the ID is given out authoritatively by the hosting Server. This removes some of the inconsistency on when to use the old player ID and when to use the old client ID as the new client ID serves both purposes. This leaves the game to be able to define its own player ID based on whatever user criteria it wants.
Along with the shift from not using the same object at both ends of the client connection was a shift in the interfaces that are notified about those ends.
On the client, there is now com.jme3.network.ClientStateListener which is notified when the client fully connects to the server (including any internal handshaking) and when the client is disconnected.
On the server, com.jme3.network.ConnectionListener will be notified whenever new HostedConnections are added or removed. This listener isn't notified until the connection is fully setup (including any internal handshaking).
Old Method | New Method |
---|---|
clientConnected(Client) | connectionAdded(Server,HostedConnection) |
clientDisconnected(Client) | connectionRemoved(Server,HostedConnection) |
As you've seen above, there are quite a few changes necessary to migrate to the new API. You might be asking yourself if it's worth the trouble.
The bottom line is that the old architecture had threading and stability issues that just couldn't be fixed in any reasonable way. Some were minor, others kind of severe… and they combined to make trouble. If you've ever wondered why sometimes your clients connect and then the network connection hangs or stops sending data. Or if you've ever wondered why UDP/unreliable messages get corrupted or somehow won't deserialize properly then you've run into some of these issues.
Moreover, the lack of thread safety meant that user code sometimes had to do some strange and/or complicated work-arounds. The goal should be that the API should just work like it looks like it will with a minimum of hassle.
The new architecture is built from the ground up for threading stability and for a clean separation between the public API, the message passing layer, and the underlying network transport implementations. You should be able to throw all kinds of stuff at it that would make the old system fall over and it should just hum along.
There will certainly be some growing pains as we work the kinks out of the new system but it is already much more stable in even the most basic of stress tests.
+ +This document introduces you to the SpiderMonkey networking API. A multi-player game is made up of clients and a server. +
++ +Each Client informs the Server about its player's moves and actions. The Server centrally collects the game state and broadcasts the state info back to all connected clients. This way all clients share the same game world and can display it to their players from their perspective. +
+ ++ +The SpiderMonkey API is a set of interfaces and helper classes in the 'com.jme3.network' package. For most users, this package and the 'message' package is all they need to worry about. (The 'base' and 'kernel' packages only come into play when implementing custom network transports or alternate client/server protocols, which is now possible). +
+ ++The SpiderMonkey API assists you in creating a Server, Clients, and Messages. Once a Server instance is created and started, the Server accepts remote connections from Clients, and you can send and receive Messages. Client objects represent the client-side of the client-server connection. Within the Server, these Client objects are referred to as HostedConnections. HostedConnections can hold application-defined client-specific session attributes that the server-side listeners and services can use to track player information, etc. + +
+Seen from the Client | Seen from the Server | +|
---|---|---|
com.jme3.network.Client | == | com.jme3.network.HostedConnection | +
+ +You can register several types of listeners to be notified of changes. +
++ +A com.jme3.network.Server is a headless com.jme3.app.SimpleApplication. Headless means that the update loop runs, but the application does not open a window and does not listen to direct user input. +
+ServerMain app = new ServerMain(); +app.start(JmeContext.Type.Headless);+ +
+Create a Server in the simpleInitApp() method and specify a communication port, for example 6143. +
+Server myServer = Network.createServer(6143); +myServer.start();+ +
+The server is ready to accept clients. Let's create one. +
+ ++ +A com.jme3.network.Client is a standard com.jme3.app.SimpleApplication. +
+ClientMain app = new ClientMain(); +app.start(JmeContext.Type.Display); // standard type+ +
+Create a Client in the simpleInitApp() method and specify the servers IP address, and the same communication port as for the server, here 6143. +
+Client myClient = Network.connectToServer("localhost", 6143); +myClient.start();+ +
+The server address can be in the format "localhost", "127.0.0.1" (for local testing) or “123.456.78.9” (the IP address of a real server). +
+ ++ +The server refers to a connected client as com.jme3.network.HostedConnection. The server can get info about clients as follows: + +
+Accessor | Purpose | +
---|---|
myServer.getConnection(0) | Server gets the first etc connected HostedConnection object. | +
myServer.getConnections() | Server gets a collection of all connected HostedConnection objects. | +
myServer.getConnections().size() | Server gets the number of all connected HostedConnection objects. | +
+ +Your game can define its own player ID based on whatever user criteria you want. If the server needs to look up player/client-specific information, it can store this information directly on the HostedConnection object: + +
+Accessor | Purpose | +
---|---|
conn.setAttribute("MyState", new MyState()); | Server can change an attribute of the HostedConnection. | +
MyState state = conn.getAttribute("MyState") | Server can read an attribute of the HostedConnection. | +
+ +Each message represents data you want to transmit, for instance transformation updates or game actions. For each message type, create a message class that extends com.jme3.network.AbstractMessage. Use the @Serializable annotation from com.jme3.network.serializing.Serializable and create an empty default constructor. Custom constructors, fields, and methods are up to you and depend on the message data that you want to transmit. +
+@Serializable +public class HelloMessage extends AbstractMessage { + private String hello; // message data + public HelloMessage() {} // empty constructor + public HelloMessage(String s) { hello = s; } // custom constructor +}+ +
+Register each message type to the com.jme3.network.serializing.Serializer, in both server and client. +
+Serializer.registerClass(HelloMessage.class);+ +
+ +After a message was received, a listener responds to it. The listener can access fields of the message, and send messages back. Implement responses in the two Listeners’ messageReceived() methods for each message type. +
+ +
+
+Create one ClientListener.java and make it extend com.jme3.network.MessageListener
.
+
+
public class ClientListener implements MessageListener<Client> { + public void messageReceived(Client source, Message message) { + if (message instanceof HelloMessage) { + // do something with the message + HelloMessage helloMessage = (HelloMessage) message; + System.out.println("Client #"+source.getId()+" received: '"+helloMessage.getSomething()+"'"); + } // else... + }+ +
+For each message type, register a client listener to the client. +
+myClient.addMessageListener(new ClientListener(), HelloMessage.class);+ +
+
+Create one ServerListener.java and make it extend com.jme3.network.MessageListener
.
+
public class ServerListener implements MessageListener<HostedConnection> { + public void messageReceived(HostedConnection source, Message message) { + if (message instanceof HelloMessage) { + // do something with the message + HelloMessage helloMessage = (HelloMessage) message; + System.out.println("Server received '" +helloMessage.getSomething() +"' from client #"+source.getId() ); + } // else.... + }+ +
+For each message type, register a server listener to the server: +
+myServer.addMessageListener(new ServerListener(), HelloMessage.class);+ +
+ +A client can send a message to the server: +
+Message message = new HelloMessage("Hello World!"); +myClient.send(message);+ +
+The server can broadcast a message to all HostedConnection (clients): +
+Message message = new HelloMessage("Welcome!"); +myServer.broadcast(message);+ +
+Using com.jme3.network.Filters, the server can send a message to specific HostedConnection (conn1, conn2, conn3), or to all but a few HostedConnections (not to conn4). +
+myServer.broadcast( Filters.in( conn1, conn2, conn3 ), message ); +myServer.broadcast( Filters.notEqualTo( conn4 ), message );+ +
+ +The ID of the Client and HostedConnection are the same at both ends of a connection. The ID is given out authoritatively by the Server. +
+... myClient.getId() ...+ +
+A server has a game version and game name. Each client expects to communicate with a server with a certain game name and version. Test first whether the game name matches, and then whether game version matches, before sending any messages! If they do not match, you should refuse to connect, because the client and server will not be able to communicate. + +
+Accessor | Purpose | +
---|---|
myServer.setName() | Specify the game name (free-form String) | +
myServer.setVersion() | Specify the game version (int) | +
myClient.getGameName() | Client gets the name of the server it is connected to. | +
myClient.getVersion() | Client gets the version of the server it is connected to. | +
+ +Tip: Your game defines its own attributs, such as player ID, based on whatever criteria you want. If you want to look up player/client-specific information, you can set this information directly on the Client/HostedConnection (see Getting Info About a Client). +
+ ++ +You must override the client's destroy() method to close the connection cleanly when the player quits the client: +
+@Override + public void destroy() { + myClient.close(); + super.destroy(); + }+ +
+ +You must override the server's destroy() method to close the connection when the server quits: +
+@Override + public void destroy() { + myServer.close(); + super.destroy(); + }+ +
+ +The server can kick a HostedConnection to make it disconnect. You should provide a String with further info (an explanation to the user what happened) for the server to send along. This info message can be used (displayed to the user) by a ClientStateListener. (See below) +
+conn.close("We kick cheaters.");+ +
+ +The server and clients are notified about connection changes. +
+ ++The com.jme3.network.ClientStateListener notifies the Client when the Client has fully connected to the server (including any internal handshaking), and when the Client is kicked (disconnected) from the server. + +
+ClientStateListener interface | Purpose | +
---|---|
clientConnected(Client c) | Implement here what happens as soon as this clients has fully connected to the server. | +
clientDisconnected(Client c, DisconnectInfo info) | Implement here what happens after the server kicks this client. For example, display the DisconnectInfo to the user. | +
+ +Implement the ClientStateListener interface in the Client, and then register it: +
+myClient.addClientStateListener(this);+ +
+ +The com.jme3.network.ConnectionListener notifies the Server whenever new HostedConnections (clients) come and go. The listener notifies the server after the Client connection is fully established (including any internal handshaking). + +
+ConnectionListener interface | Purpose | +
---|---|
connectionAdded(Server s, HostedConnection c) | Implemenent here what happens after a new HostedConnection has joined the Server. | +
connectionRemoved(Server s, HostedConnection c) | Implement here what happens after a HostedConnection has left. E.g. a player has quit the game and the server removes his character. | +
+ +Implement the ConnectionListener in the Server, and register it. +
+myServer.addConnectionListener(this);+ +
+ +SpiderMonkey supports both UDP (unreliable, fast) and TCP (reliable, slow) transport of messages. +
+message1.setReliable(true); // TCP +message2.setReliable(false); // UDP+
+ +You cannot modify the scenegraph in the NetworkThread. You have to create a Callable, enqueue the Callable in the OpenGLThread, and the callable makes the modification in the correct moment. A Callable is a Java class representing a (possibly time-intensive), self-contained task. +
+app.enqueue(callable);+ +
+ +Learn more about multithreading here. +
+Although it is possible to embed a jME3 canvas in a Swing GUI app, a 3D game typically runs full-screen, or in a window of its own. This soon raises the question of how to add a user interface: Most games respond to the escape key by displaying buttons that allow users to switch to different screens – for example to view high scores, customize settings, or load saved games.
This doc introduces you to Nifty GUI, a Java library for graphical user interfaces (GUIs). Nifty GUI (de.lessvoid.nifty
package) is well integrated with jME3 via the com.jme3.niftygui
package. You define the GUI layout in XML and call it from your Java code. All the JAR libraries that you need are already included in your jME3 download, so you do not need to install anything extra (Just make sure they are on the classpath).
Typically, you lay out the Nifty GUI using XML, but using Java will soon be a second option.
There are three steps needed to add a GUI to your jME3 game:
If you include the following XML schema in the first lines of your NiftyGUI XML files, your IDE will give you helpful hints and code completion.
<?xml version="1.0" encoding="UTF-8"?> -<nifty xmlns="http://nifty-gui.sourceforge.net/nifty.xsd" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://nifty-gui.sourceforge.net/nifty.xsd - http://nifty-gui.sourceforge.net/nifty.xsd"> - - <!-- Example: The IDE will now tell you that one <screen></screen> element is expected here, etc. --> - -</nifty>
Learn more from the NiftyGUI page!
+ +
+ ++You may want your players to press a button to save a game, you want a scrolling text field for highscores, a text label to display the score, drop-downs to select keymap preferences, or checkboxes to specify multi-media options. Usually you solve these tasks by using Swing controls. Although it is possible to embed a jME3 canvas in a Swing GUI, a 3D game typically runs full-screen, or in a window of its own. +
+ +
+This document introduces you to , a Java library for building interactive graphical user interfaces (GUIs) for games or similar applications. Nifty GUI (the de.lessvoid.nifty
package) is well integrated with jME3 through the com.jme3.niftygui
package. You define the base GUI layout in XML, and control it dynamically from your Java code. The necessary JAR libraries are included in your jME3 download, you do not need to install anything extra. (Just make sure they are on the classpath.)
+
+
+ +To add a Nifty GUI to your jME3 game: +
++ + +
+ ++Nifty GUIs are made up of the following elements: + +
+start
. Name any others whatever you like.+ +General usage tips about elements: +
+*
to fill the remaining space automatically.+ +The XML and Java syntax are equivalent, so is it an either-or choice? Not quite. +We recommend to use XML to lay out the static parts of the GUI. Then use Java syntax to control the dynamic parts of the GUI at runtime. +
+ ++ +During development (and during this tutorial), the following debug code makes all panels visible. This helps you arrange them and find errors. + +
+nifty.setDebugOptionPanelColors(true);+ +
+ +Before the release, and for testing, set it to false again. +
+ ++ +The SDK includes an XML editor and previewer for Nifty GUI files. Open an XML file, and switch between XML and Preview mode. +
+ ++ +Include the following XML schema in the first line of your NiftyGUI XML files and your IDE gives you hints and code completion. + +
+<?xml version="1.0" encoding="UTF-8"?> +<nifty xmlns="http://nifty-gui.sourceforge.net/nifty-1.3.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://nifty-gui.sourceforge.net/nifty-1.3.xsd http://nifty-gui.sourceforge.net/nifty-1.3.xsd"> + <!-- Your IDE now tells you that one <screen></screen> element is expected here, etc. --> +</nifty>+ +
+ +Learn more from the NiftyGUI page! +
++ +Next, learn how to lay out your graphical user interface. Typically, you start with XML. +
+The main purpose of the GUI is to send events back to your Java class that indicate what the users clicked, which settings they chose, which values they entered into a field, etc. In the Java class, you want to respond with an appropriate action, or store the entered settings in a file, etc.
How does the XML file send a message back to your Java application? You register a ScreenController (a Java class) to every NiftyGUI screen.
-Create a ScreenController by creating a Java class that implements the de.lessvoid.nifty.screen.ScreenController
interface and its abtract methods.
package my.game; + ++ +Interacting with the GUI from Java
+++ ++
+ +- +
+- +
+- +
+- +
Nifty GUI Java Interaction++ +In the previous parts of the tutorial, you created a two-screen user interface. But it is still static, and when you click the buttons, nothing happens yet. The purpose of the GUI is to communicate with your Java classes: Your game needs to know what the users clicked, which settings they chose, which values they entered into a field, etc. Similarly, the user needs to know what the currently game state is (score, health, etc). +
+ +Connect GUI to Java Controller
++ ++ +To let a Nifty screen communicate with the Java application, you register a
+ +ScreenController
to every NiftyGUI screen. You create a ScreenController by creating a Java class that implements thede.lessvoid.nifty.screen.ScreenController
interface and its abtract methods. ++Pro Tip: Since you are writing a jME3 application, you can additionally make the ScreenController class extend the AbstractAppState class! This gives the ScreenController access to the application object and to the update loop! +
+package tutorial; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.AbstractAppState; +import com.jme3.app.state.AppStateManager; import de.lessvoid.nifty.Nifty; import de.lessvoid.nifty.screen.Screen; import de.lessvoid.nifty.screen.ScreenController; -public class MySettingsScreen implements ScreenController { - public MySettingsScreen(MyGameData data ){ /** constructor */ } - public void bind(Nifty nifty, Screen screen) { } - public void onStartScreen() { } - public void onEndScreen() { } -}The name and package of your custom ScreenController class (here
my.game.MySettingsScreen
) goes into the controller parameter of the respective screen it belongs to:<nifty> - <screen id="settings" controller="my.game.MySettingsScreen"> + +public class MyStartScreen extends AbstractAppState implements ScreenController { + + private Nifty nifty; + private Screen screen; + private SimpleApplication app; + + /** custom methods */ + + public MyStartScreen(String data) { + /** You custom constructor, can accept arguments */ + } + + /** Nifty GUI ScreenControl methods */ + + public void bind(Nifty nifty, Screen screen) { + this.nifty = nifty; + this.screen = screen; + } + + public void onStartScreen() { } + + public void onEndScreen() { } + + /** jME3 AppState methods */ + + @Override + public void initialize(AppStateManager stateManager, Application app) { + super.initialize(stateManager, app); + this.app=(SimpleApplication)app; + } + + @Override + public void update(float tpf) { + /** jME update loop! */ + } + +}+ ++The name and package of your custom ScreenController class (here
+tutorial.MyStartScreen
) goes into the controller parameter of the respective XML screen it belongs to. For example: +<nifty> + <screen id="start" controller="tutorial.MyStartScreen"> <!-- layer and panel code ... --> </screen> -</nifty>Now the Java class
my.game.MySettingsScreen
and this GUI screen (settings
) are connected.Make GUI and Java Interact
In most cases, you will want to pass game data in and out of the ScreenController. Note that you can pass any arguments into your ScreenController constructor. In the example above, the object with the necessary data is is
MyGameData data
. -You can use any of the three following approaches to make Java classes interact with the GUI, and you can also combine them, depending on what you want to do.GUI Calls a Void Java Method
+ +To respond to an interaction, add the <interact /> element to a panel and specify the Java method you want to call. In this example, we want to call
sayHello()
when a panel on the screen is clicked.... -<panel id="panel" height="25%" width="35%" align="center" valign="center" - backgroundColor="#f60f" childLayout="center" visibleToMouse="true"> - <text id="text" font="aurulent-sans-17.fnt" color="#000f" - text="Hello World!" align="center" valign="center" /> - <interact onClick="sayHello(hi)"/> -</panel> -...Back in this screen's Java class, we specify what the
sayHello()
method does. As you see, you can include String arguments in the call.public class MySettingsScreen implements ScreenController { +</nifty>
+ ++Or the same in a Java syntax, respectively: +
+nifty.addScreen("start", new ScreenBuilder("start") {{ + controller(new tutorial.MyStartScreen());+ ++Now the Java class
+ +MyStartScreen
and this GUI screen (start
) are connected. For this example you can also connect thehud
screen to MyStartScreen. +Make GUI and Java Interact
++ ++ ++ +In most cases, you will want to pass game data in and out of the ScreenController. Note that you can pass any custom arguments from your Java class into your ScreenController constructor (
+ +public MyStartScreen(GameData data) {}
). ++Use any combination of the three following approaches to make Java classes interact with the GUI. +
+ +GUI Calls a Void Java Method
++ ++ +This is how you respond to an GUI interaction such as clicks in XML GUIs: +
++
+ +- +
Add+visibleToMouse="true"
to the parent element!- +
Embed the+<interact />
element into the parent element.- +
Specify the Java methods that you want to call when the users performs certain actions, such as clicking.+
+Example:<interact onClick="startGame(hud)" />
+ +Or this is how you respond to an GUI interaction such as clicks in Java GUIs: +
++
+ +- +
Add+visibleToMouse(true);
to the parent element!- +
Embed one of the+interact…()
elements into the parent element- +
Specify the Java method that you want to call after the interaction.+
+Example:interactOnClick("startGame(hud)");
+ +In the following example, we call the
+startGame()
method when the player clicks the Start button, andquitGame()
when the player clicks the Quit button. +<panel id="panel_bottom_left" height="50%" width="50%" valign="center" childLayout="center"> + <control name="button" label="Start" id="StartButton" align="center" valign="center" + visibleToMouse="true" > + <interact onClick="startGame(hud)"/> + </control> + </panel> + + <panel id="panel_bottom_right" height="50%" width="50%" valign="center" childLayout="center"> + <control name="button" label="Quit" id="QuitButton" align="center" valign="center" + visibleToMouse="true" > + <interact onClick="quitGame()"/> + </control> + </panel>+ ++Or the same in a Java syntax, respectively: +
+control(new ButtonBuilder("StartButton", "Start") {{ + alignCenter(); + valignCenter(); + height("50%"); + width("50%"); + visibleToMouse(true); + interactOnClick("startGame(hud)"); +}}); +... + +control(new ButtonBuilder("QuitButton", "Quit") {{ + alignCenter(); + valignCenter(); + height("50%"); + width("50%"); + visibleToMouse(true); + interactOnClick("quitGame()"); +}});+ ++Back in the MyStartScreen class, you specify what the
+startGame()
andquitGame()
methods do. As you see, you can pass String arguments (herehud
) in the method call. You also see that you have access to the app object. +public class MyStartScreen implements ScreenController { ... - public void sayHello(String myarg) { - System.out.println("Nifty says "+myarg); + + /** custom methods */ + public void startGame(String nextScreen) { + nifty.gotoScreen(nextScreen); // switch to another screen + // start the game and do some more stuff... } -}GUI Gets Return Value from Java Method
+ +You can send a message from Java to Nifty. In this example, the Java class
callThis()
in MySettingsScreen defines the Text that is displayed in the textfield after the wordsHello World, …!
First define a Java method in the screen controller, in this example,callThis()
.public class MySettingsScreen implements ScreenController { + + public void quitGame() { + app.stop(); + } + + ... +}+ ++The startGame() example simply switches the GUI to the
+ +hud
screen when the user clicks Start. Of course, in a real game, you would perform more steps here: Load the game level, switch to in-game input and navigation handling, set a customrunning
boolean to true, attach custom in-game AppStates – and lots more. ++The quitGame() example shows that you have access to the application
+ +app
object because you made the ScreenController extend AbstractAppState. +GUI Gets Return Value from Java Method
++ ++ +When the Nifty GUI is initialized, you can get data from Java. In this example, the Java class
+ +getPlayerName()
inMyStartScreen
defines the Text that is displayed in the textfield before the words's Cool Game
. ++First define a Java method in the screen controller, in this example,
+getPlayerName()
. +public class MySettingsScreen implements ScreenController { ... - public String callThis() { - return "my friend"; + public String getPlayerName(){ + return System.getProperty("user.name"); } -}Nifty uses
${CALL.callThis()}
to get the return value of a method from your ScreenController Java class.... -<panel id="panel" height="25%" width="35%" align="center" valign="center" - backgroundColor="#f60f" childLayout="center" visibleToMouse="true"> - <text id="text" font="aurulent-sans-17.fnt" color="#000f" - text="Hello World, ${CALL.callThis()}!" align="center" valign="center" /> - <interact onClick="sayHello(hi)"/> -</panel> -...You can also use this for Strings and numeric values, e.g. when you read settings from a file, you read them like this.
Java Modifies Nifty Elements and Events
You can also alter the appearance and functions of your nifty elements from Java. -Here's an example of how to change the image
myElement
:NiftyImage img = nifty.getRenderEngine().createImage("Interface/Images/image.png", false); -Element niftyElement = nifty.getCurrentScreen().findElementByName("myElement"); -niftyElement.getRenderer(ImageRenderer.class).setImage(img);The same is valid for other elements, for example text fields:
niftyElement.getRenderer(TextRenderer.class).setText("New text");Similarly, to change the onClick() event of an element, create an
ElementInteraction
object:Element niftyElement = nifty.getCurrentScreen().findElementByName("myElement"); -niftyElement.getElementInteraction().getPrimary().setOnMouseOver(new NiftyMethodInvoker(nifty, "myCustomMethod()", this));For this to work, there already needs to be an < interact > tag inside your xml element:
<interact onClick="doNothing()"/>Concepts & Examples
note: Nifty-gui 1.3 oriented.
How do I create a popup menu that I populate in Java?
Even though you create and populate the popup menu in Java you still need a "placeholder" in your XML file, below is an example extract:
<useControls filename="nifty-default-controls.xml"/> -... - <popup id="niftyPopupMenu" childLayout="absolute-inside" - controller="ControllerOfYourChoice" width="10%"> - <interact onClick="closePopup()" - onSecondaryClick="closePopup()" onTertiaryClick="closePopup()" /> - <control id="#menu" name="niftyMenu" /> - </popup> -...A brief explanation of some the attributes above:
the popup id will be used within your Java code so that nifty knows which popup placeholder to create controller will tell nifty which Java class handles MenuItemActivatedEvent on(Secondary/Tertiary)Click tells nifty to close the popup if the user clicks anywhere except on the menu items (in this example) control id will be used by the Java class to define a control type (i.e. Menu)Java code within your defined ScreenController implementation:
private Element popup; -... -public void createMyPopupMenu(){ - popup = nifty.createPopup("niftyPopupMenu"); - Menu.class); - myMenu.setWidth(new SizeValue("100px")); //must be set - myMenu.addMenuItem("Click me!", "menuItemIcon.png", new menuItem("menuItemid", "blah blah")); //menuItem is a custom class - nifty.subscribe(nifty.getCurrentScreen(), myMenu.getId(), MenuItemActivatedEvent.class, new MenuItemActivatedEventSubscriber()); -} -public void showMenu(){ //the method to trigger the menu - createMyPopupMenu() //you should call this in your constructor rather than here if it is a menu that is going to be used many times - nifty.showPopup(nifty.getCurrentScreen(), popup.getId(), null); //call the popup to screen of your choice -} -private class menuItem{ - public String id; - public String name; - public menuItem(String name){ - this.id= id; - this.name = name; - } -}
createMyPopupMenu() creates the menu with set width so that you can populate it showMenu() is called by something to trigger the menu (i.e. could be a Key or some other method)To handle menu item events (i.e. calling a method when you click on a menu item) you subscribe a EventTopicSubscriber<MenuItemActivatedEvent> class implementation to a nifty screen and element.
private class MenuItemActivatedEventSubscriber implements EventTopicSubscriber<MenuItemActivatedEvent> { - @Override - public void onEvent(final String id, final MenuItemActivatedEvent event) { - menuItem item = (menuItem) event.getItem(); - if ("menuItemid".equals(item.id)) { - //do something !!! - } - } -};Useful Links
- \ No newline at end of file +}Nifty 1.3 controls Java Docs: http://nifty-gui.sourceforge.net/projects/1.3-SNAPSHOT/nifty-default-controls/apidocs/
Nifty 1.3 Java Docs: http://nifty-gui.sourceforge.net/projects/1.3-SNAPSHOT/nifty/apidocs/index.html
Examples of standard controls in Nifty 1.3: http://sourceforge.net/apps/mediawiki/nifty-gui/index.php?title=Nifty_Standard_Controls_%28Nifty_1.3%29
Learn more: Nifty Syntax
+Nifty uses ${CALL.getPlayerName()}
to get the return value of the getPlayerName() method from your ScreenController Java class.
+
<text text="${CALL.getPlayerName()}'s Cool Game" font="Interface/Fonts/Default.fnt" width="100%" height="100%" />
+
++Or the same in a Java syntax, respectively: +
+text(new TextBuilder() {{ + text("${CALL.getPlayerName()}'s Cool Game"); + font("Interface/Fonts/Default.fnt"); + height("100%"); + width("100%"); +}});+ +
+You can use this for Strings and numeric values (e.g. when you read settings from a file, you display the results in the GUI) and also for methods with side effects. +
+ +
+
+You can also alter the appearance and functions of your nifty elements from Java. Make certain that the element that you want to alter has its id="name"
attribute set, so you can identy and address it.
+
+Here's an example of how to change an image called playerhealth
:
+
// load or create new image +NiftyImage img = nifty.getRenderEngine().createImage("Interface/Images/face2.png", false); +// find old image +Element niftyElement = nifty.getCurrentScreen().findElementByName("playerhealth"); +// swap old with new image +niftyElement.getRenderer(ImageRenderer.class).setImage(img);+ +
+The same is valid for other elements, for example a text label "score": +
+// find old text +Element niftyElement = nifty.getCurrentScreen().findElementByName("score"); +// swap old with new text +niftyElement.getRenderer(TextRenderer.class).setText("124");+ +
+Similarly, to change the onClick() event of an element, create an ElementInteraction
object:
+
Element niftyElement = nifty.getCurrentScreen().findElementByName("myElement"); +niftyElement.getElementInteraction().getPrimary().setOnMouseOver(new NiftyMethodInvoker(nifty, "myCustomMethod()", this));+ +
+For this to work, there already needs to be a (possibly inactive) <interact />
tag inside your xml element:
+
+
<interact onClick="doNothing()"/>
+
++ +You're done with the basic Nifty GUI for jME3 tutorial. You can proceed to advanced topics and learn how add controls and effects: +
++ +Work in progress You can "draw" the GUI to the screen by writing Java code – alternatively to using XML. Typically you lay out the static base GUI in XML, and use Java commands if you need to change the GUI dynamically at runtime. In theory, you can also lay out the whole GUI in Java (but we don't cover that here). +
+ ++ +Sample project +
+de.lessvoid.nifty.examples.controls.ControlsDemo.java
.assets/Interface/
directory. (In this example, I copied them from nifty-default-controls-examples/trunk/src/main/resources/
to assets/Interface/
).assets/
directory.filename("Interface/yang.png");
( not filename("yang.png");
).filename("Interface/sounds/gong.wav");
(not filename("sounds/gong.wav");
).+ +Just so you get a quick picture what Nifty GUI's Java Syntax looks like, here is the most basic example. It creates a screen with a layer and a panel that contains a button. +
+package mygame;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.niftygui.NiftyJmeDisplay;
+import de.lessvoid.nifty.Nifty;
+import de.lessvoid.nifty.builder.ScreenBuilder;
+import de.lessvoid.nifty.builder.LayerBuilder;
+import de.lessvoid.nifty.builder.PanelBuilder;
+import de.lessvoid.nifty.controls.button.builder.ButtonBuilder;
+import de.lessvoid.nifty.screen.DefaultScreenController;
+
+/**
+ * @author iamcreasy
+*/
+public class Main extends SimpleApplication {
+
+ public static void main(String[] args) {
+ Main app = new Main();
+ app.start();
+ }
+
+ @Override
+ public void simpleInitApp() {
+ NiftyJmeDisplay niftyDisplay = new NiftyJmeDisplay(
+ assetManager, inputManager, audioRenderer, guiViewPort);
+ Nifty nifty = niftyDisplay.getNifty();
+ guiViewPort.addProcessor(niftyDisplay);
+ flyCam.setDragToRotate(true);
+
+ nifty.loadStyleFile("nifty-default-styles.xml");
+ nifty.loadControlFile("nifty-default-controls.xml");
+
+ // <screen>
+ nifty.addScreen("Screen_ID", new ScreenBuilder("Hello Nifty Screen"){{
+ controller(new DefaultScreenController()); // Screen properties
+
+ // <layer>
+ layer(new LayerBuilder("Layer_ID") {{
+ childLayoutVertical(); // layer properties, add more...
+
+ // <panel>
+ panel(new PanelBuilder("Panel_ID") {{
+ childLayoutCenter(); // panel properties, add more...
+
+ // GUI elements
+ control(new ButtonBuilder("Button_ID", "Hello Nifty"){{
+ alignCenter();
+ valignCenter();
+ height("5%");
+ width("15%");
+ }});
+
+ //.. add more GUI elements here
+
+ }});
+ // </panel>
+ }});
+ // </layer>
+ }}.build(nifty));
+ // </screen>
+
+ nifty.gotoScreen("Screen_ID"); // start the screen
+ }
+}
+
++ + +
+ ++In this tutorial, you recreate the same screen as in the Nifty GUI XML example. +
+ +
+Create an Screen.Java file in the assets/Interfaces/
directory of your project. One Java file can contain several, or even all screens. As a reminder: Nifty displays one screen at a time; a screen contains several layers on top of one another; each layer contains panels that are embedded into another; the panels contain the actual content (text, images, or controls).
+
+ +The following minimal Java file contains a start screen and a HUD screen. (Neither has been defined yet.) +
+nifty.addScreen("start", new ScreenBuilder("start"){{ + controller(new DefaultScreenController()); + // <!-- ... --> + }}.build(nifty)); + +nifty.addScreen("hud", new ScreenBuilder("hud"){{ + controller(new DefaultScreenController()); + // <!-- ... --> + }}.build(nifty));+ +
+Every Nifty GUI must have a start screen. The others (in this example, the HUD screen) are optional. +
+ ++ +The following Java code shows how we add layers to the start screen and HUD screen: +
+nifty.addScreen("start", new ScreenBuilder("start"){{ + controller(new DefaultScreenController()); + + // layer added + layer(new LayerBuilder("background") {{ + childLayoutCenter(); + backgroundColor("#000f"); + + // <!-- ... --> + }}); + + layer(new LayerBuilder("foreground") {{ + childLayoutVertical(); + backgroundColor("#0000"); + + // <!-- ... --> + }}); + // layer added + + }}.build(nifty));+ +
+Repeat the same, but use +
+nifty.addScreen("hud", new ScreenBuilder("hud"){{+ +
+ for the HUD screen. +
+ ++In a layer, you can now add panels and arrange them. Panels are containers that mark the areas where you want to display text, images, or controls (buttons etc) later. +
+ +
+
+A panel is the inner-most container (that will contain the actual content: text, images, or controls). You place panels inside layers. The following panels go into in the start
screen:
+
nifty.addScreen("start", new ScreenBuilder("start") {{ + controller(new DefaultScreenController()); + layer(new LayerBuilder("background") {{ + childLayoutCenter(); + backgroundColor("#000f"); + // <!-- ... --> + }}); + + layer(new LayerBuilder("foreground") {{ + childLayoutVertical(); + backgroundColor("#0000"); + + // panel added + panel(new PanelBuilder("panel_top") {{ + childLayoutCenter(); + alignCenter(); + backgroundColor("#f008"); + height("25%"); + width("75%"); + }}); + + panel(new PanelBuilder("panel_mid") {{ + childLayoutCenter(); + alignCenter(); + backgroundColor("#0f08"); + height("50%"); + width("75%"); + }}); + + panel(new PanelBuilder("panel_bottom") {{ + childLayoutHorizontal(); + alignCenter(); + backgroundColor("#00f8"); + height("25%"); + width("75%"); + + panel(new PanelBuilder("panel_bottom_left") {{ + childLayoutCenter(); + valignCenter(); + backgroundColor("#44f8"); + height("50%"); + width("50%"); + }}); + + panel(new PanelBuilder("panel_bottom_right") {{ + childLayoutCenter(); + valignCenter(); + backgroundColor("#88f8"); + height("50%"); + width("50%"); + }}); + }}); // panel added + }}); + + }}.build(nifty));+ +
+The following panels go into in the hud
screen:
+
nifty.addScreen("hud", new ScreenBuilder("hud") {{ + controller(new DefaultScreenController()); + + layer(new LayerBuilder("background") {{ + childLayoutCenter(); + backgroundColor("#000f"); + // <!-- ... --> + }}); + + layer(new LayerBuilder("foreground") {{ + childLayoutHorizontal(); + backgroundColor("#0000"); + + // panel added + panel(new PanelBuilder("panel_left") {{ + childLayoutVertical(); + backgroundColor("#0f08"); + height("100%"); + width("80%"); + // <!-- spacer --> + }}); + + panel(new PanelBuilder("panel_right") {{ + childLayoutVertical(); + backgroundColor("#00f8"); + height("100%"); + width("20%"); + + panel(new PanelBuilder("panel_top_right1") {{ + childLayoutCenter(); + backgroundColor("#00f8"); + height("15%"); + width("100%"); + }}); + + panel(new PanelBuilder("panel_top_right2") {{ + childLayoutCenter(); + backgroundColor("#44f8"); + height("15%"); + width("100%"); + }}); + + panel(new PanelBuilder("panel_bot_right") {{ + childLayoutCenter(); + valignCenter(); + backgroundColor("#88f8"); + height("70%"); + width("100%"); + }}); + }}); // panel added + }}); + }}.build(nifty));+ +
+Try the sample. Remember to activate a screen using nifty.gotoScreen("start");
or hud
respectively.
+The result should look as follows:
+
+ +
+ ++ +See also on the Nifty GUI site. +
+ +
+
+The start-background.png image is a fullscreen background picture. In the start
screen, add the following image element:
+
+
nifty.addScreen("start", new ScreenBuilder("start") {{ + controller(new DefaultScreenController()); + layer(new LayerBuilder("background") {{ + childLayoutCenter(); + backgroundColor("#000f"); + + // add image + image(new ImageBuilder() {{ + filename("Interface/tutorial/start-background.png"); + }}); + + }});+ +
+The hud-frame.png image is a transparent frame that we use as HUD decoration. In the hud
screen, add the following image element:
+
+
nifty.addScreen("hud", new ScreenBuilder("hud") {{ + controller(new DefaultScreenController()); + + layer(new LayerBuilder("background") {{ + childLayoutCenter(); + backgroundColor("#000f"); + + // add image + image(new ImageBuilder() {{ + filename("Interface/tutorial/hud-frame.png"); + }}); + + }});+ +
+The face1.png image is an image that you want to use as a status icon.
+In the hud
screen's foreground
layer, add the following image element:
+
+
panel(new PanelBuilder("panel_top_right2") {{ + childLayoutCenter(); + backgroundColor("#44f8"); + height("15%"); + width("100%"); + + // add image + image(new ImageBuilder() {{ + filename("Interface/tutorial/face1.png"); + valignCenter(); + alignCenter(); + height("50%"); + width("30%"); + }}); + + }});+ +
+ +This image is scaled to use 50% of the height and 30% of the width of its container. +
+ +
+
+The game title is a typical example of static text. In the start
screen, add the following text element:
+
+
// panel added + panel(new PanelBuilder("panel_top") {{ + childLayoutCenter(); + alignCenter(); + backgroundColor("#f008"); + height("25%"); + width("75%"); + + // add text + text(new TextBuilder() {{ + text("My Cool Game"); + font("Interface/Fonts/Default.fnt"); + height("100%"); + width("100%"); + }}); + + }});+ +
+For longer pieces of static text, such as an introduction, you can use wrap="true". Add the following text element to the Start screen
:
+
panel(new PanelBuilder("panel_mid") {{ + childLayoutCenter(); + alignCenter(); + backgroundColor("#0f08"); + height("50%"); + width("75%"); + // add text + text(new TextBuilder() {{ + text("Here goes some text describing the game and the rules and stuff. "+ + "Incidentally, the text is quite long and needs to wrap at the end of lines. "); + font("Interface/Fonts/Default.fnt"); + wrap(true); + height("100%"); + width("100%"); + }}); + + }});+ +
+The font used is jME3's default font "Interface/Fonts/Default.fnt" which is included in the jMonkeyEngine.JAR. You can add your own fonts to your own assets/Interface
directory.
+
+ +Before you can use any control, you must load a Control Definition first. Add the following two lines before your screen definitions: +
+nifty.loadStyleFile("nifty-default-styles.xml"); + nifty.loadControlFile("nifty-default-controls.xml");+ +
+
+Use label controls for text that you want to edit dynamically from Java. One example for this is the score display.
+In the hud
screen's foreground
layer, add the following text element:
+
panel(new PanelBuilder("panel_top_right1") {{ + childLayoutCenter(); + backgroundColor("#00f8"); + height("15%"); + width("100%"); + + control(new LabelBuilder(){{ + color("#000"); + text("123"); + width("100%"); + height("100%"); + }});+ +
+Note that the width and height do not scale the bitmap font, but the make indirectly certain it is centered. If you want a different size for the font, you need to provide an extra bitmap font (they come with fixes sizes and don't scale well). +
+ +
+
+Our GUI plan asks for two buttons on the start screen. You add the Start and Quit buttons to the bottom panel of the start
screen using the <control>
element:
+
panel(new PanelBuilder("panel_bottom_left") {{ + childLayoutCenter(); + valignCenter(); + backgroundColor("#44f8"); + height("50%"); + width("50%"); + + // add control + control(new ButtonBuilder("StartButton", "Start") {{ + alignCenter(); + valignCenter(); + height("50%"); + width("50%"); + }}); + + }}); + + panel(new PanelBuilder("panel_bottom_right") {{ + childLayoutCenter(); + valignCenter(); + backgroundColor("#88f8"); + height("50%"); + width("50%"); + + // add control + control(new ButtonBuilder("QuitButton", "Quit") {{ + alignCenter(); + valignCenter(); + height("50%"); + width("50%"); + }}); + + }});+ +
+Note that these controls don't do anything yet – we'll get to that soon. +
+ ++ +Nifty additionally offers many customizable controls such as check boxes, text fields, menus, chats, tabs, … See also on the Nifty GUI site. +
+ ++ +When you preview this code in the jMonkeyEngine SDK, our tutorial demo should looks as follows: A start screen with two buttons, and a game screen with a simple HUD frame and a blue cube (which stands for any jME3 game content). +
+ ++Tip: Remove all lines that set background colors, you only needed them to see the arrangement. +
+ ++ +
+ ++ +Before initializing the nifty screens, you set up properties and register media. +
+Nifty Method | Description | +
---|---|
registerSound("mysound", "Interface/abc.wav"); | + |
registerMusic("mymusic", "Interface/xyz.ogg"); | + |
registerMouseCursor("mypointer", "Interface/abc.png", 5, 4); | + |
registerEffect(?); | ? | +
setDebugOptionPanelColors(true); | Highlight all panels, makes it easier to arrange them. | +
+ +Example: +
+nifty.registerMouseCursor("hand", "Interface/mouse-cursor-hand.png", 5, 4);+ +
+ +Integrate the GUI into the game. Typically, you will overlay the GUI. +
+Define a key (for example escape) that switches the GUI on and off. -You can either overlay the running game with the GUI (you will most likely pause the game then), or even project it as a texture onto a mesh texture (but then you cannot click to select). -On this page, we look at the overlay variant (more commonly used).
This code shows you how to overlay anything on the screen with the GUI. This is the most common usecase.
NiftyJmeDisplay niftyDisplay = new NiftyJmeDisplay( + ++ +Integrating Nifty GUI: Overlay
+++ ++
+ +- +
+- +
+- +
Nifty GUI Overlay or Nifty GUI Projection+- +
++ + +
+ ++Typically, you define a key (for example escape) that switches the GUI on and off. The GUI can be a StartScreen, OptionsScreen, CharacterCreationScreen, etc. While the GUI is up, you pause the running game, and then overlay the GUI. You also must switch to a different set of user inputs while the game is paused, so the player can use the mouse pointer and keyboard to interact with the GUI. +
+ ++You can also project the GUI as a texture onto a mesh texture (but then you cannot click to select). +On this page, we look at the overlay variant, which is more commonly used in games. +
+ +Sample Code
+++ ++
+ +- +
+Overlaying the User Interface Over the Screen
++ +- \ No newline at end of file +flyCam.setDragToRotate(true);+ +This code shows you how to overlay anything on the screen with the GUI. This is the most common usecase. +
+NiftyJmeDisplay niftyDisplay = new NiftyJmeDisplay( assetManager, inputManager, audioRenderer, guiViewPort); /** Create a new NiftyGUI object */ Nifty nifty = niftyDisplay.getNifty(); /** Read your XML and initialize your custom ScreenController */ -nifty.fromXml("Interface/helloworld.xml", "start", new MySettingsScreen(data)); +nifty.fromXml("Interface/tutorial/step2/screen.xml", "start"); +// nifty.fromXml("Interface/helloworld.xml", "start", new MySettingsScreen(data)); // attach the Nifty display to the gui view port as a processor guiViewPort.addProcessor(niftyDisplay); // disable the fly cam -flyCam.setDragToRotate(true);The
MySettingsScreen
class is a custom de.lessvoid.nifty.screen.ScreenController in which you implement your GUI behaviour. The variabledata
contains an object that you use to exchange state info with the game. See Nifty GUI Java Interaction for details on how we created this class.
Nifty GUI Overlay or Nifty GUI Projection
+Currently you do not have a ScreenController – we will create one in the next exercise. As soon as you have a screen controller, you will use the commented variant of the XML loading method: +
+nifty.fromXml("Interface/helloworld.xml", "start", new MySettingsScreen());+ +
+The MySettingsScreen
class is a custom de.lessvoid.nifty.screen.ScreenController in which you will implement your GUI behaviour.
+
+ +Now that you have layed out and integrated the GUI in your app, you want to respond to user input and display the current game. Time to create a ScreenController! +
+Define a key (for example escape) that switches the GUI on and off. You can either overlay the running game with the GUI (you will most likely pause the game then), or even project it as a texture onto a mesh textures (but then you cannot click to select). -On this page, we look at the projection variant (less commonly used).
You can project the Nifty GUI onto a texture, load the texture into a material, and assign it to a 3D mesh. Allthough this is possible the approach is rarely used since it is difficult to record clicks this way, you can only interact with this UI by keyboard or programmatically. (E.g. an action performed by a character standing in front of a "computer")
/** Create a new viewport for the GUI */ + ++ +Integrating Nifty GUI: Projection
+++ ++
+ +- +
+- +
+- +
Nifty GUI Overlay or Nifty GUI Projection+- +
++ + +
+ ++Typically you define a key (for example escape) to switch the GUI on and off. Then you overlay the running game with the GUI (you will most likely pause the game then). +
+ ++Alternatively, you can also project the GUI as a texture onto a mesh textures inside the game. Allthough this looks cool and "immersive", this approach is rarely used since it is difficult to record clicks this way. You can only interact with this projected GUI by keyboard, or programmatically. You can select input fields using the arrow keys, and trigger actions using the return key. +
+ ++This GUI projection variant is less commonly used than the GUI overlay variant. Usecases for GUI projection are, for example, a player avatar using an in-game computer screen. +
+ +Sample Code
+++ ++
+ +- +
+Projecting the User Interface Onto a Texture
++ +- \ No newline at end of file +rootNode.attachChild(geom);+ +You can project the Nifty GUI onto a texture, load the texture into a material, and assign it to a Geometry (Quads or Boxes are best). +
+/** Create a special viewport for the Nifty GUI */ ViewPort niftyView = renderManager.createPreView("NiftyView", new Camera(1024, 768)); niftyView.setClearEnabled(true); /** Create a new NiftyJmeDisplay for the integration */ @@ -34,7 +57,8 @@ NiftyJmeDisplay niftyDisplay = new NiftyJmeDisplay( Nifty nifty = niftyDisplay.getNifty(); /** Read your XML and initialize your custom ScreenController */ nifty.fromXml("Interface/helloworld.xml", "start", new MySettingsScreen(data)); -/** We prepare a framebuffer for the texture niftytex */ + +/** Prepare a framebuffer for the texture niftytex */ niftyView.addProcessor(niftyDisplay); FrameBuffer fb = new FrameBuffer(1024, 768, 0); fb.setDepthBuffer(Format.Depth); @@ -42,32 +66,41 @@ Texture2D niftytex = new Texture2D(1024, 768, Format.RGB8); fb.setColorTexture(niftytex); niftyView.setClearEnabled(true); niftyView.setOutputFrameBuffer(fb); + /** This is the 3D cube we project the GUI on */ Box(Vector3f.ZERO, 1, 1, 1); Geometry geom = new Geometry("Box", b); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setTexture("m_ColorMap", niftytex); /** Here comes the texture! */ geom.setMaterial(mat); -rootNode.attachChild(geom);The MySettingsScreen class is a custom de.lessvoid.nifty.screen.ScreenController in which you implement your GUI behaviour. The variable
data
contains an object that you use to exchange state info with the game. See Nifty GUI Java Interaction for details on how we created this class. -Run the code sample. You select buttons on this GUI with the arrow keys and then press return. Note that clicking on the texture will not work! -Again, check the Nifty GUI wiki to get all the "bells and whistles" of the syntax!
Nifty GUI Overlay or Nifty GUI Projection
+The MySettingsScreen class is a custom de.lessvoid.nifty.screen.ScreenController in which you implement your GUI behaviour. The variable data
contains an object that you use to exchange state info with the game. See Nifty GUI Java Interaction for details on how to create this class.
+
+Run the code sample. You select buttons on this GUI with the arrow keys and then press return. Note that clicking on the texture will not work! +
+ ++ +Now that you have layed out and integrated the GUI in your app, you want to respond to user input and display the current game. +
+You "draw" the GUI to the screen by writing XML code. We will be referring to the following elements:
id="start"
. Name any others whatever you like. Create an empty helloworld.xml file in the assets/Interfaces/
directory of your project.
Here's a minimal example showing an empty centered layer on the start screen:
<?xml version="1.0" encoding="UTF-8"?> -<nifty xmlns="http://nifty-gui.sourceforge.net/nifty.xsd" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://nifty-gui.sourceforge.net/nifty.xsd - http://nifty-gui.sourceforge.net/nifty.xsd"> - - <screen id="start" controller="de.lessvoid.nifty.examples.helloworld.HelloWorldStartScreen"> - <layer id="layer1" backgroundColor="#003f" childLayout="center"> - <!-- ... panels go here... --> + ++ +Laying out the GUI in XML
+++ ++
+ +- +
+- +
Nifty GUI XML Layout or Nifty GUI Java Layout+- +
+- +
++ +You can "draw" the GUI to the screen by writing XML code (alternatively you can also use Java). +
+ +Plan Your GUI Layout
++ ++ ++ + +
+ ++In this tutorial, you want to create two game screens: An out-of-game StartScreen that the players see before the game starts; and an in-game that displays info during the game. Before writing code, you plan the GUI layout, either on paper or in a graphic application. +
+ ++The StartScreen contains: +
++
+ +- +
The background layer has a centered layout and contains an image.+- +
The top layer has a vertical layout, containing 3 panels:++
+- +
The top panel contains a label with the game title,+- +
The middle panel contains a text field with the game description.+- +
The bottom panel has a horizontal layout and contains two more panels:++
+- +
The left panel contains a Start button.+- +
The right panel contains a Quit button.++ +The HUD contains: +
++
+ +- +
The background layer has a centered layout, and contains a the partially transparent HUD image.+- +
The top layer has a horizontal layout, containing 2 panels:++
+- +
The left panel as transparent spacer.+- +
The right panel has a vertical layout containing 2 panels, a label and an image.+Implement Your GUI Layout
++ ++ ++ + +
+ ++Create an empty screen.xml file in the
+ +assets/Interfaces/
directory of your project. One XML file can contain several, or even all screens. As a reminder: Nifty displays one screen at a time; a screen contains several layers on top of one another; each layer contains panels that are embedded into another; the panels contain the actual content (text, images, or controls). +Make Screens
++ ++ ++ +The following minimal XML file contains a start screen and a HUD screen. (Neither has been defined yet.) +
+<?xml version="1.0" encoding="UTF-8"?> +<nifty xmlns="http://nifty-gui.sourceforge.net/nifty.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://nifty-gui.sourceforge.net/nifty.xsd http://nifty-gui.sourceforge.net/nifty.xsd"> + <screen id="start"> + <!-- ... --> + </screen> + <screen id="hud"> + <!-- ... --> + </screen> +</nifty>+ ++Every Nifty GUI must have a start screen. The others (in this example, the HUD screen) are optional. +
+ ++Note: In the following examples, the XML schema header is abbreviated to just
+ +<nifty>
. +Make Layers
++ ++ +The following minimal XML file shows how we added layers to the start screen and HUD screen: +
+<nifty> + <screen id="start"> + <layer id="background" backgroundColor="#000f"> + <!-- ... --> + </layer> + <layer id="foreground" backgroundColor="#0000" childLayout="vertical"> + <!-- ... --> + </layer> + </screen> + <screen id="hud"> + <layer id="background" backgroundColor="#000f"> + <!-- ... --> + </layer> + <layer id="foreground" backgroundColor="#0000" childLayout="vertical"> + <!-- ... --> </layer> </screen> - -</nifty>Into a layer, you add panels (text, images, etc), and specify their properties:
Panel
A panels looks like a rectangular colored box.
... -<panel height="25%" width="35%" align="center" valign="center" backgroundColor="#f60f" - childLayout="center" visibleToMouse="true"> -</panel> -...Text
... -<text font="verdana-24-shadow.fnt" text="Hello World!" align="center" valign="center" /> -...
or
... -<label text="this is my text" align="left"/> -...
Image
<image filename="Textures/jme-logo.png" ></image>
Nifty additionally offers predefined controls – learn more from the NiftyGUI page:
Effects
- \ No newline at end of file +</nifty>You can register effects to screen elements.
Respond to element events such as onStartScreen, onEndScreen, onHover, onFocus, onActive, Trigger effects that change movement, blending, size, color, fading, and much more.Here is an example that moves a panel when the startScreen opens. You place an < effect > tag inside the element that you want to want to be affected.
... - <panel height="25%" width="35%" ...> - <effect> - <onStartScreen name="move" mode="in" direction="top" - length="300" startDelay="0" inherit="true"/> - </effect> - </panel> -...Playing sounds using nifty is also possible with effects as triggers. Remember to first register the sound you're going to play:
... - <registerSound id="click" filename="Sounds/Gui/ButtonClick.ogg" /> -... - <label> - <effect> - <onClick name="playSound" sound="click"/> - </effect> - </label> -...Learn more from the NiftyGUI page:
Nifty GUI XML Layout
+In a layer, you can now add panels and arrange them. Panels are containers that mark the areas where you want to display text, images, or controls (buttons etc) later. +
+ +
+
+A panel is the inner-most container (that will contain the actual content: text, images, or controls). You place panels inside layers. The following panels go into in the start
screen's foreground
layer:
+
<panel id="panel_top" height="25%" width="75%" align="center" childLayout="center" + backgroundColor="#f008"> + </panel> + <panel id="panel_mid" height="50%" width="75%" align="center" childLayout="center" + backgroundColor="#0f08"> + </panel> + <panel id="panel_bottom" height="25%" width="75%" align="center" childLayout="horizontal" + backgroundColor="#00f8"> + <panel id="panel_bottom_left" height="50%" width="50%" valign="center" childLayout="center" + backgroundColor="#44f8"> + </panel> + <panel id="panel_bottom_right" height="50%" width="50%" valign="center" childLayout="center" + backgroundColor="#88f8"> + </panel> + </panel>+ +
+The following panels go into in the hud
screen's foreground
layer:
+
<panel id="panel_left" width="80%" height="100%" childLayout="vertical" + backgroundColor="#0f08"> + <!-- spacer --> + </panel> + <panel id="panel_right" width="20%" height="100%" childLayout="vertical" + backgroundColor="#00f8" > + <panel id="panel_top_right1" width="100%" height="15%" childLayout="center" + backgroundColor="#00f8"> + </panel> + <panel id="panel_top_right2" width="100%" height="15%" childLayout="center" + backgroundColor="#44f8"> + </panel> + <panel id="panel_bot_right" width="100%" height="70%" valign="center" + backgroundColor="#88f8"> + </panel> + </panel>+ +
+The result should look as follows: +
+ ++ +
+ ++ +See also on the Nifty GUI site. +
+ +
+
+The start-background.png image is a fullscreen background picture. In the start
screen, add the following image element:
+
+
<layer id="background" childLayout="center"> + <image filename="Interface/tutorial/step2/start-background.png"></image> + </layer>+ +
+The hud-frame.png image is a transparent frame that we use as HUD decoration. In the hud
screen, add the following image element:
+
+
<layer id="background" childLayout="center"> + <image filename="Interface/tutorial/step2/hud-frame.png"></image> + </layer>+ +
+The face1.png image is an image that you want to use as a status icon.
+In the hud
screen's foreground
layer, add the following image element:
+
+
<panel id="panel_bottom_left" height="75%" width="20%" valign="center" childLayout="center"> + <image filename="Interface/tutorial/step2/face1.png" + valign="center" align="center" height="50%" width="30%" > + </image> + </panel>+ +
+ +This image is scaled to use 50% of the height and 30% of the width of its container. +
+ +
+
+The game title is a typical exmaple of static text. In the start
screen, add the following text element:
+
+
<panel id="panel_top" height="25%" width="75%" align="center" childLayout="center"> + <text text="My Cool Game" font="Interface/Fonts/Default.fnt" width="100%" height="100%" /> + </panel>+ +
+For longer pieces of static text, such as an introduction, you can use wrap="true". Add the following text element to the Start screen
:
+
<panel id="panel_mid" height="50%" width="75%" align="center" childLayout="center"> + <text text="Here goes some text describing the game and the rules and stuff. Incidentally, + the text is quite long and needs to wrap at the end of lines. ..." + font="Interface/Fonts/Default.fnt" width="100%" height="100%" wrap="true" /> + </panel>+ +
+The font used is jME3's default font "Interface/Fonts/Default.fnt" which is included in the jMonkeyEngine.JAR. You can add your own fonts to your own assets/Interface
directory.
+
+ +Before you can use any control, you must load a Control Definition first. Add the following two lines before your screen definitions: +
+<useControls filename="nifty-default-controls.xml" /> + <useStyles filename="nifty-default-styles.xml" />+ +
+
+Use label controls for text that you want to edit dynamically from Java. One example for this is the score display.
+In the hud
screen's foreground
layer, add the following text element:
+
<panel id="panel_top_right" height="100%" width="15%" childLayout="center"> + <control name="label" color="#000" text="123" width="100%" height="100%" /> + </panel>+ +
+Note that the width and height do not scale the bitmap font, but the make indirectly certain it is centered. If you want a different size for the font, you need to provide an extra bitmap font (they come with fixes sizes and don't scale well). +
+ +
+
+Our GUI plan asks for two buttons on the start screen. You add the Start and Quit buttons to the bottom panel of the start
screen using the <control>
element:
+
<panel id="panel_bottom_left" height="50%" width="50%" valign="center" childLayout="center"> + <control name="button" label="Start" id="StartButton" align="center" valign="center"> + </control> + </panel> + <panel id="panel_bottom_right" height="50%" width="50%" valign="center" childLayout="center"> + <control name="button" label="Quit" id="QuitButton" align="center" valign="center"> + </control> + </panel>+ +
+Note that these controls don't do anything yet – we'll get to that soon. +
+ ++ +Nifty additionally offers many customizable controls such as check boxes, text fields, menus, chats, tabs, … See also on the Nifty GUI site. +
+ ++ +When you preview this code in the jMonkeyEngine SDK, our tutorial demo should looks as follows: A start screen with two buttons, and a game screen with a simple HUD frame and a blue cube (which stands for any jME3 game content). +
+ ++ +
+ ++Compare this result with the layout draft above. +
+ ++ +Integrate the GUI into the game. Typically, you will overlay the GUI. +
++The Open Game Finder (OGF) by Mark Schrijver can be plugged into any Java game. OGF enables you to find other people playing the same multiplayer game, and join them. +
++Both on the client and the server side of OGF is written purely in Java. OGF has a pluggable architecture and comes with a full set of plugins to get the job done. You can add your own plugins, or replace existing plugins to make them more in line with your game. OGF uses NiftyGUI as the main GUI plugin. + +
+ ++The OGF server uses an embedded Apache Derby database. You have to install the database, this means creating the data files and adding the tables. You can do this straight from the command line by running a script file. +
+java -jar lib/Server-0.1.jar install
in the Terminal.java -jar lib/Server-0.1.jar update
in the Terminal. +Change into the OGF-Server directory and run the server: +
+java -jar lib/Server-1.0.jar
in the Terminal.
+The server is now running and ready to accept connections.
+
+Note: In the alpha release, the server runs on localhost. In the final release, you will be able to configure the host!
+
+
java -jar lib/Client-1.0.jar
in the Terminal.+A client is now running, connects to the server, and displays a registration/login window. + +Note: You can run several clients on localhost for testing. + +
+ ++If clients use OGF for the first time, they need to register. +On the main screen of the client: +
++The client registers the account and opens the chat window directly. + +
+ ++If returning clients are already registered to an OGF server, they can log in. +On the main screen of the client: +
++The client logs you in and opens the chat window. + +
+ ++The chat window shows a list of all users logged in to the server. Logged-in users can send public messages, and can receive public messages from others. + +
+ +
+Q: I want to gather players using the OGF client to connect to the game server. How do I start my multiplayer game?
+
+A: The following sample code demos the typical use case:
+
+
+In a JME3 Application's init method:
+
+After this, continue writing your JME3 init method. + +
+ +jMonkeyProjects/OGF-Client-1.0-bin/OGF/resources/avatars/
You cannot create a 3D model for delicate things like fire, smoke, or explosions. Particle Emitters are quite an efficient solution to create these kinds of effects: The emitter renders a series of flat orthogonal images and manipulates them in a way that creates the illusion of a anything from a delicate smoke cloud to individual flames, etc.
Creating an effect involves some trial and error to get the settings just right, and it's worth exploring the expressiveness of the options described below. Tip: Use the Scene Editor in the jMonkeyPlatform to design and preview effects.
ParticleEmitter explosion = new ParticleEmitter( -"My explosion effect", ParticleMesh.Type.Triangle, 30);
rootNode.attachChild(explosion); -explosion.setLocalTranslation(bomb.getLocalTranslation());
explosion.emitAllParticles()
explosion.killAllParticles()
Choose one of the following mesh shapes
Not all of these parameters are required for all kinds of effects. If you don't specify one of them, a default value will be used.
Parameter | Method | Default | Description |
---|---|---|---|
number | setNumParticles() | The maximum number of particles visible at the same time. Specified by user in constructor. | |
emission rate | setParticlesPerSec() | 20 | Density of the effect, how many new particles are emitted per second. Set to zero to control the start of the effect. Set to a number for a constantly running effect. |
size | setStartSize() , setEndSize() | 0.2f, 2f | Set both to same value for constant size effect. Set to different values for shrink/grow effect. |
color | setStartColor() , setEndColor() | gray, darkgray | Set both to the same color for single-colored effects (e.g. fog). Set both to different colors for a gradient effect (e.g. fire). |
velocity/direction | setInitialVelocity() | Vector3f(0,0,0) | A vector specifying how fast or slow particles fly, and it which direction. |
randomness | setVelocityVariation() | 0.2f | How much the direction/speed (setInitialVelocity() ) can vary. 1 = Maximum variation (particles emit in random directions) 0 = No variation (particles fly straight with start velocity only). |
direction | setFacingVelocity() | false | true = Flying particles pitch in the direction they're flying (e.g. missiles). false = Particles keep flying rotated the way they started (e.g. debris). |
direction | setRandomAngle() | false | true = Flying particle should face at a random angle (e.g. explosion). false = Flying particle flies straight. |
direction | setFaceNormal() | Vector3f.NAN | Vector3f = Flying particles face in the given direction. Vector3f.NAN = Flying particles face the camera. |
lifetime | setLowLife() | 3f | Minimum time period before particles fade |
lifetime | setHighLife() | 7f | Maximum time period before particles fade |
rotation | setRotateSpeed() | 0f | 0 = Flying particles don't spin. > 0 = How fast particle spins while flying. |
gravity | setGravity() | 0.1f | >0 = Particles fall "down" (e.g. debris, sparks). 0.0f = Particles keep flying (e.g. flames, zero g explosion.) |
Build up you effect by specifying one parameter after the other. If you change several parameters at the same time, it's difficult to tell which of the values caused which outcome.
Use the common Particle.j3md Material Definition to specify the shape of the particles. The shape is only limited by the texture you provide and can be anything – debris, flames, smoke, mosquitoes, leaves, butterflies… be creative.
Material mat_flash = new Material( + ++ +Particle Emmitter Settings
++ ++ ++ +You cannot create a 3D model for delicate things like fire, smoke, or explosions. Particle Emitters are quite an efficient solution to create these kinds of effects: The emitter renders a series of flat orthogonal images and manipulates them in a way that creates the illusion of a anything from a delicate smoke cloud to individual flames, etc. +Creating an effect involves some trial and error to get the settings just right, and it's worth exploring the expressiveness of the options described below. +
+ ++Tip: Use the Scene Editor in the jMonkeyPlatform to design and preview effects. +
+ ++ +
+ +Create an Emitter
+++ ++
+ +- +
Create one emitter for each effect:+ParticleEmitter explosion = new ParticleEmitter( +"My explosion effect", ParticleMesh.Type.Triangle, 30);+- +
Attach the emitter to the rootNode and position it in the scene:+rootNode.attachChild(explosion); +explosion.setLocalTranslation(bomb.getLocalTranslation());+- +
Trigger the effect by calling+explosion.emitAllParticles()+- +
End the effect by calling+explosion.killAllParticles()++Choose one of the following mesh shapes +
++
+ +- +
ParticleMesh.Type.Triangle+- +
ParticleMesh.Type.Point+Configure Parameters
++ ++ ++Not all of these parameters are required for all kinds of effects. If you don't specify one of them, a default value will be used. +
++ ++
+ +Parameter Method Default Description ++ +number setNumParticles()
The maximum number of particles visible at the same time. Specified by user in constructor. ++ +emission rate setParticlesPerSec()
20 Density of the effect, how many new particles are emitted per second. +
+Set to zero to control the start/end of the effect.
+Set to a number for a constantly running effect.+ +size setStartSize()
,setEndSize()
0.2f, 2f The radius of the scaled sprite image. Set both to same value for constant size effect. +
+Set to different values for shrink/grow effect.+ +color setStartColor()
,setEndColor()
gray Controls how the opaque (non-black) parts of the texture are colorized. +
+Set both to the same color for single-colored effects (e.g. fog, debris).
+Set both to different colors for a gradient effect (e.g. fire).+ +direction/velocity getParticleInfluencer(). setInitialVelocity(initialVelocity)
Vector3f(0,0,0) A vector specifying the initial direction and speed of particles. The longer the vector, the faster. ++ +fanning out getParticleInfluencer(). setVelocityVariation(variation)
0.2f How much the direction ( +setInitialVelocity()
) can vary among particles. Use a value between 1 and 0 to create a directed swarm-like cloud of particles.
+1 = Maximum variation, particles emit in random 360° directions (e.g. explosion, butterflies).
+0.5f = particles are emitted within 180° of the initial direction.
+0 = No variation, particles fly in a straight line in direction of start velocity (e.g. lasergun blasts).+ +direction
+(pick one)setFacingVelocity()
false true = Flying particles pitch in the direction they're flying (e.g. missiles). +
+false = Particles keep flying rotated the way they started (e.g. debris).+ +direction
+(pick one)setFaceNormal()
Vector3f.NAN Vector3f = Flying particles face in the given direction (e.g. horizontal shockwave faces up = Vector3f.UNIT_Y). +
+Vector3f.NAN = Flying particles face the camera.+ +lifetime setLowLife()
,setHighLife()
3f, 7f The time period before a particle fades is set to a random value between minimum and maximum; minimum must be smaller than maximum. A minimum < 1f makes the effect more busy, a higher minimum looks more steady. Use a maximum < 1f for short bursts, and higher maxima for long lasting swarms or smoke. Set maximum and minimum to similar values to create an evenly spaced effect (e.g. fountain), set the to very different values to create a distorted effect (e.g. fire with individual long flames). ++ +spinning setRotateSpeed()
0f 0 = Flying particles don't spin while flying (e.g. smoke, insects, controlled projectiles). +
+> 0 = How fast particle spins while flying (e.g. debris, shuriken, missiles out of control).+ +rotation setRandomAngle()
false true = The particle sprite is rotated at a random angle when it is emitted (e.g. explosion, debris). +
+false = Particles fly straight like you drew them in the sprite texture (e.g. insects).+ +gravity setGravity()
Vector3f(0.0f,0.1f,0.0f) Particles fall in the direction of the vector (e.g. debris, sparks). +
+(0,0,0) = Particles keep flying in start direction (e.g. flames, zero-gravity explosion.)+ +start area setShape(new EmitterSphereShape( Vector3f.ZERO, 2f));
EmitterPointShape() By default, particles are emitted from the emitters location (a point). You can increase the emitter shape to occupy a sphere, so that the start point of new particles can be anywhere inside the sphere, which makes the effect a bit more irregular. ++Build up you effect by specifying one parameter after the other. If you change several parameters at the same time, it's difficult to tell which of the values caused which outcome. +
+ +Create an Effect Material
++ ++ + +
+ ++Use the common Particle.j3md Material Definition and a texture to specify the shape of the particles. The shape is defined by the texture you provide and can be anything – debris, flames, smoke, mosquitoes, leaves, butterflies… be creative. +
+Material flash_mat = new Material( assetManager, "Common/MatDefs/Misc/Particle.j3md"); - mat_flash.setTexture("Texture", + flash_mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/flash.png")); - flash.setMaterial(debris_mat); + flash.setMaterial(flash_mat); flash.setImagesX(2); // columns flash.setImagesY(2); // rows - flash.setSelectRandomImage(true);The effect texture can contain Sprite animations – a series of different pictures in equally spaced rows and columns.
Specify the number of rows and columns Specify whether you want to play the series in order or at random.Have a look at the following default textures and you will see that you can easily create your own Sprite textures after the same fashion.
Default Particle Textures
The Material is used together with grayscale texture: The black parts will be transparent and the white parts will be opaque.
The following effect textures are available by default from
test-data.jar
. You can also load your own textures from your assets directory.Tip: Use the
setStartColor()
/setEndColor()
settings described above to colorize the textures.Usage Example
+ +ParticleEmitter fire = new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 30); + flash.setSelectRandomImage(true);+ ++The effect texture can be one image, or contain a sprite animation – a series of slightly different pictures in equally spaced rows and columns. If you choose the sprite animation: +
++
+ +- +
Specify the number of rows and columns using setImagesX(2) and setImagesY().+- +
Specify whether you want to play the sprite series in order (animation), or at random (explosion, flame), by setting setSelectRandomImage() true or false.++ +Examples: Have a look at the following default textures and you will see how you can create your own sprite textures after the same fashion. +
+ +Default Particle Textures
++ ++ ++The Material is used together with grayscale texture: The black parts will be transparent and the white parts will be opaque (colored). +The following effect textures are available by default from
+test-data.jar
. You can also load your own textures from your assets directory. ++ ++
+ +Texture Path Dimension Preview ++ +Effects/Explosion/Debris.png 3*3 + + +Effects/Explosion/flame.png 2*2 + + +Effects/Explosion/flash.png 2*2 + + +Effects/Explosion/roundspark.png 1*1 + + +Effects/Explosion/shockwave.png 1*1 + + +Effects/Explosion/smoketrail.png 1*3 + + +Effects/Explosion/spark.png 1*1 + + +Effects/Smoke/Smoke.png 1*15 + + +Tip: Use the
+ +setStartColor()
/setEndColor()
settings described above to colorize the white and gray parts of textures. +Usage Example
++- \ No newline at end of file + fire.getParticleInfluencer().setVelocityVariation(0.3f); + rootNode.attachChild(fire);ParticleEmitter fire = new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 30); Material mat_red = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md"); mat_red.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/flame.png")); fire.setMaterial(mat_red); fire.setImagesX(2); fire.setImagesY(2); // 2x2 texture animation fire.setEndColor( new ColorRGBA(1f, 0f, 0f, 1f)); // red fire.setStartColor(new ColorRGBA(1f, 1f, 0f, 0.5f)); // yellow - fire.setInitialVelocity(new Vector3f(0, 2, 0)); + fire.getParticleInfluencer().setInitialVelocity(new Vector3f(0,2,0)); fire.setStartSize(1.5f); fire.setEndSize(0.1f); - fire.setGravity(0); + fire.setGravity(0,0,0); fire.setLowLife(0.5f); fire.setHighLife(3f); - fire.setVelocityVariation(0.3f); - rootNode.attachChild(fire);Browse the full source code of all effect examples here.
See also: Effects Overview
+ +Browse the full source code of all here. +
++ +See also: Effects Overview + +
+ + The jMonkeyEngine3 has built-in support for jBullet physics via the com.jme3.bullet
package.
+
+
+
+The jMonkeyEngine3 has built-in support for via the com.jme3.bullet
package.
+
Game Physics are used in applications that simulate mass/gravity, collisions, and friction. Think of pool billiard or car racing simulations. -If you are looking for info on how to respond to physics events, read about Physics Listeners.
Bullet physics runs internally at 60fps by default. This rate is not dependent on the actual framerate and it does not lock the framerate at 60fps. Instead, when the actual fps is higher than the physics framerate the system will display interpolated positions for the physics objects. When the framerate is lower than the physics framerate the physics space will be stepped multiple times per frame to make up for the missing calculations. +
+ ++If you are looking for info on how to respond to physics events, read about Physics Listeners. +
+ ++ +Bullet physics runs internally at 60fps by default. This rate is not dependent on the actual framerate and it does not lock the framerate at 60fps. Instead, when the actual fps is higher than the physics framerate the system will display interpolated positions for the physics objects. When the framerate is lower than the physics framerate the physics space will be stepped multiple times per frame to make up for the missing calculations. +
+ +A bullet physics space can be created with a BulletAppState. The updating and syncing of the actual physics objects happens in the following way: -A "normal" update loop with physics looks like this:
When you use physics, 1 unit (1.0f) equals 1 meter, weight is expressed in kilograms, most torque and rotation values are expressed in radians.
Full code samples are here:
A short overview of how to write a jME application with Physics capabilities:
-Do the following once per application to gain access to the physicsSpace
object:
com.jme3.app.SimpleApplication
.private BulletAppState bulletAppState;
public void simpleInitApp() { + + ++A "normal" update loop with physics looks like this: +
++
+ +- +
collision callbacks (BulletAppState.update())+- +
user update (simpleUpdate / update)+- +
physics to scenegraph syncing/applying (updateLogicalState())+- +
stepping physics (before / in parallel to Application.render())++When you use physics, 1 unit (1.0f) equals 1 meter, weight is expressed in kilograms, most torque and rotation values are expressed in radians. +
+ +
+ +Full code samples are here: +
++ +A short overview of how to write a jME application with Physics capabilities: +
+ +
+Do the following once per application to gain access to the physicsSpace
object:
+
com.jme3.app.SimpleApplication
.private BulletAppState bulletAppState;+
public void simpleInitApp() { bulletAppState = new BulletAppState(); stateManager.attach(bulletAppState); - ...
You can also access the BulletAppState via the state manager:
stateManager.getState(BulletAppState.class)
For each Spatial that you want to be physical:
com.jme3.bullet.control.RigidBodyControl
PhysicsCollisionListener
interface to respond to PhysicsCollisionEvent
s if desired. Before you can create a Physics Control, you must create a Collision Shape from the com.jme3.bullet.collision.shapes
package.
-The Collision Shape is a simplified shape for which physics are easier to calculate than for the real shape of the model. This approach speeds up the simulation greatly.
Shape | Purpose |
---|---|
BoxCollisionShape | Box shaped objects such as bricks, crates, simple obstacles. Does not roll. |
SphereCollisionShape | Spherical objects such as balls. Can roll. |
CylinderCollisionShape | Tube-shaped pillars, disc-shaped wheels. Can roll on one side. |
CapsuleCollisionShape | A compound of a cylinder plus two spheres at the top and bottom. Rotated upright, this shape is optimal for character nodes: A cylinder-shaped body does not get stuck at corners and vertical obstacles; the rounded top and bottom do not get stuck on stair steps and ground obstacles. Is locked to stay upright, does not roll. |
CompoundCollisionShape | A CompoundCollisionShape allows custom combinations of box/sphere/cylinder shapes to form another more complex shape. |
MeshCollisionShape | A free-form mesh-accurate shape that wraps itself around a mesh. Limitations: Only non-mesh collision shapes (sphere, box, cylinder, compound) can collide with mesh-accurate collision shapes. The Mesh Collision Shape only works for static obstacles, e.g. for a game level model. |
GImpactCollisionShape | This free-form Mesh Collision Shape can be used for moving objects. Uses http://gimpact.sourceforge.net/. Limitations: CPU intensive, use sparingly! We recommend using HullCollisionShapes or CompoundShapes made of simple shapes if you need improved performance. |
HeightFieldCollisionShape | Optimized Mesh Collision Shape for static terrains. This shape is much faster than a other Free-Form Mesh Shapes. Requires heightmap data. |
HullCollisionShape | A collision shape that is based on a mesh but is a simplified convex version. |
SimplexCollisionShape | A physical point, line, triangle, or quad Collision Shape, defined by one to four points. |
PlaneCollisionShape | A 2D plane that can be used as flat solid floor or wall. |
Pick the right shape for the mesh for what you want to do: If you give a box a sphere collision shape, it will roll; if you give a ball a box collision shape, it will sit on a slope. -Let's look at the constructor:
MeshCompoundShape and MeshCollisionShape are both mesh-accurate and are intended for immobile scene objects, such as terrains, buildings, or whole shooter levels. Limitation: Only collisions of non-mesh-accurate shapes (sphere, box, etc) shapes can be detected against mesh-accurate shapes.
CompoundCollisionShape myComplexShape = - CollisionShapeFactory.createMeshShape((Node) myComplexGeometry );
An angular, non-mesh-accurate compound shape:
CompoundCollisionShape boxShape = - CollisionShapeFactory.createBoxCompoundShape((Node) someBox);
SphereCollisionShape, BoxCollisionShape, CapsuleCollisionShape are also not mesh-accurate, but have better performance. The can be added to anything, and collisions between them and any other shape can be detected.
SphereCollisionShape sphereShape = - new SphereCollisionShape(1.0f);
Available PhysicsControls in the com.jme3.bullet.control package are:
Physics Control | Purpose |
---|---|
RigidBodyControl | Use for physical objects in the scene, e.g. projectiles and obstacles – things that are freely affected by physical forces, be it by collision or falling. |
CharacterControl | Use for characters (persons, animals) that stand upright, orthogonally to the X/Z plane. When directional forces are applied to a CharacterControl'ed Spatial, it does not tip over (as a RigidBodyControl'ed Spatial would), but it moves upright (as a walking character would). |
GhostControl | A GhostControl is a PhysicsControl that detects overlaps with other physical objects. A GhostControl is non-solid and moves with the Spatial it is attached to. Use this for game elements that do not have a visible solid Geometry: Aggro radius, motion detectors, photoelectric sensors, radioactive areas, life-draining ghosts, etc. |
VehicleControl PhysicsVehicleWheel | Implements terrestric vehicle behaviour. |
RagDollControl | Implements Ragdoll behaviour. |
Bullet Physics are available in jME3 through a several classes. You will use PhysicsControls in 99% of the time.
PhysicsControls are the recommended way to use physics in a jME3 application. PhysicsControls are flexible and can be added to any Spatial to make it act according to physical properties. These Control classes directly extend Bullet Physics Objects. -Package: com.jme3.bullet.control
Physics Objects are mostly standard Bullet classes like RigidBody, GhostObject etc., that jME3's other classes are built upon. Advanced users can use these classes to create custom physics functions. -Package: com.jme3.bullet.objects
Physics Nodes have Bullet Controllers attached and wrap their methods to “simulate” the old physics nodes that were available before alpha-4. The setLocalTranslation/Rotation() info is transferred to the Bullet Objects for simplicity. Do not use Physics Nodes, use PhysicsControls instead. -Package: com.jme3.bullet.nodes
The various Physics Control constructors expect a Collision Shape (here thingShape) and a mass (a float).
RigidBodyControl myControl=new RigidBodyControl( thingShape , 1.0f );
To make the Physics Control visible in the scene, you must attach the Control to a Geometry (e.g. a model named myGeometry):
myGeometry.addControl(myControl);
This code sample creates a physical Character:
// Load a normal model + ...+
+ +You can also access the BulletAppState via the state manager: +
+stateManager.getState(BulletAppState.class)+ +
+For each Spatial that you want to be physical: +
+com.jme3.bullet.control.RigidBodyControl
PhysicsCollisionListener
interface to respond to PhysicsCollisionEvent
s if desired.+ +A Collision Shape is a simplified shape for which physics are easier to calculate than for the true shape of the model. This simplication approach speeds up the simulation greatly. +
+ +
+Before you can create a Physics Control, you must create a Collision Shape from the com.jme3.bullet.collision.shapes
package. (Read the tip under "PhysicsControls Code Samples" to learn how to use default CollisionShapes for Boxes and Spheres.)
+
+
Shape | Purpose | +
---|---|
BoxCollisionShape | Box shaped objects such as bricks, crates, simple obstacles. Does not roll. | +
SphereCollisionShape | Spherical objects such as balls. Can roll. | +
CylinderCollisionShape | Tube-shaped pillars, disc-shaped wheels. Can roll on one side. | +
CapsuleCollisionShape | A compound of a cylinder plus two spheres at the top and bottom. Rotated upright, this shape is optimal for CharacterControls: A cylinder-shaped body does not get stuck at corners and vertical obstacles; the rounded top and bottom do not get stuck on stair steps and ground obstacles. Is locked to stay upright, does not roll. | +
CompoundCollisionShape | A CompoundCollisionShape allows custom combinations of box/sphere/cylinder shapes to form another more complex shape. | +
MeshCollisionShape | A free-form mesh-accurate shape that wraps itself around a mesh. +Limitations: Only non-mesh collision shapes (sphere, box, cylinder, compound) can collide with mesh-accurate collision shapes. The Mesh Collision Shape only works for static obstacles, e.g. for a game level model. |
+
GImpactCollisionShape | This free-form Mesh Collision Shape can be used for moving objects. Uses . Limitations: CPU intensive, use sparingly! We recommend using HullCollisionShapes or CompoundShapes made of simple shapes if you need improved performance. | +
HeightFieldCollisionShape | Optimized Mesh Collision Shape for static terrains. This shape is much faster than a other Free-Form Mesh Shapes. Requires heightmap data. | +
HullCollisionShape | A collision shape that is based on a mesh but is a simplified convex version. | +
SimplexCollisionShape | A physical point, line, triangle, or quad Collision Shape, defined by one to four points. | +
PlaneCollisionShape | A 2D plane that can be used as flat solid floor or wall. | +
+ +Tip: Pick the right shape for the mesh for what you want to do: If you give a box a sphere collision shape, it will roll; if you give a ball a box collision shape, it will sit on a slope. Make collision shapes visible by adding the following line after the bulletAppState initialization: +
+bulletAppState.getPhysicsSpace().enableDebug(assetManager);+ +
+Let's look at examples of how to use the CollisionShape constructor: +
+ ++ +MeshCompoundShape and MeshCollisionShape are both mesh-accurate and are intended for immobile scene objects, such as terrains, buildings, or whole shooter levels. Limitation: Only collisions of non-mesh-accurate shapes (sphere, box, etc) shapes can be detected against mesh-accurate shapes. + +
+CompoundCollisionShape myComplexShape = + CollisionShapeFactory.createMeshShape((Node) myComplexGeometry );+ +
+An angular, non-mesh-accurate compound shape: +
+CompoundCollisionShape boxShape = + CollisionShapeFactory.createBoxCompoundShape((Node) someBox);+ +
+SphereCollisionShape, BoxCollisionShape, CapsuleCollisionShape are also not mesh-accurate, but have better performance. The can be added to anything, and collisions between them and any other shape can be detected. +
+SphereCollisionShape sphereShape = + new SphereCollisionShape(1.0f);+ +
+ +BulletPhysics are available in jME3 through Bullet Physics Controls from the com.jme3.bullet.control package. PhysicsControls are the recommended way to use physics in a jME3 application. PhysicsControls are flexible and can be added to any Spatial to make it act according to physical properties. These Control classes directly extend Bullet Physics Objects. + +
+Physics Control | Purpose | +
---|---|
RigidBodyControl | Use for physical objects in the scene, e.g. projectiles and obstacles – things that are freely affected by physical forces, be it by collision or falling. | +
CharacterControl | Use for characters (persons, animals) that stand upright, orthogonally to the X/Z plane. When directional forces are applied to a CharacterControl'ed Spatial, it does not tip over (as a RigidBodyControl'ed Spatial would), but it moves upright (as a walking character would). | +
GhostControl | A GhostControl is a PhysicsControl that detects overlaps with other physical objects. A GhostControl is non-solid and moves with the Spatial it is attached to. Use this for game elements that do not have a visible solid Geometry: Aggro radius, motion detectors, photoelectric sensors, radioactive areas, life-draining ghosts, etc. | +
VehicleControl +PhysicsVehicleWheel | Implements terrestric vehicle behaviour. | +
RagDollControl | Implements Ragdoll behaviour. | +
+ +The various Physics Control constructors expect a Collision Shape (here thingShape) and a mass (a float). +
+RigidBodyControl myControl=new RigidBodyControl( thingShape , 1.0f );+ +
+To make the Physics Control visible in the scene, you must attach the Control to a Geometry (e.g. a model named myGeometry): +
+myGeometry.addControl(myControl);+ +
+This code sample creates a physical Character: +
+// Load any model Node model = (Node) assetManager.loadModel("Models/myCharacterModel.mesh.xml"); rootNode.attachChild(model); // Create a appropriate physical shape for it @@ -147,156 +270,262 @@ CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1.5f, 6f, 1 CharacterControl character_phys = new CharacterControl(capsuleShape, 0.01f); // Attach physical properties to model and PhysicsSpace model.addControl(character_phys); -bulletAppState.getPhysicsSpace().add(character_phys);
Tip: Spheres and Boxes will fall back to their correct default Collision Shape if you don't specify a shape in the RigidBodyControl constructor. The following creates a box with Box Collision Shape:
Box(1,1,1); -myBox.addControl(new RigidBodyControl( 1.0f )); -bulletAppState.getPhysicsSpace().add(myBox);
The Physics Space is an object in BulletAppState that is like a rootNode for Physics Controls.
bulletAppState.getPhysicsSpace().setGravity(new Vector3f(0f,-1f,0f)); -bulletAppState.getPhysicsSpace().setAccuracy(0.005f);
bulletAppState.getPhysicsSpace().add(myPhysicsControl); ...
myModel.addControl(myPhysicsControl); ...
rootNode.attachChild(myModel); ...
bulletAppState.getPhysicsSpace().remove(myPhysicsControl); -myModel.removeFromParent();
On a PhysicsControl, you can set the following physical properties.
RigidBodyControl Method | Property |
---|---|
setFriction(1f) | Friction. |
setMass(1f) | Sets the mass. Dynamic objects have masses > 0.0f. Static immobile obstacles (including buildings and terrains) have mass 0.0f. |
setPhysicsLocation() | Positions the object. Do not use setLocalTranslation(). |
setPhysicsRotation() | Rotates the object. Do not use setLocalRotate(). |
setRestitution(0.0f) | How bouncy the object is. For a rubber object set this > 0.0f. This setting has an impact on performance. |
setKinematic(true) | A kinematic node is not affected by gravity, but it is solid and affects other physics objects. It has a mass its position is updated from the spatials translation. You can attach joints to it. |
setGravity(new Vector3f(0f,-1f,0f)) | You can change the gravity of a physics object after it was added to the physics space. |
setCcdMotionThreshold(0.1f) | The amount of motion in 1 physics tick to trigger the continuous motion detection. |
CharacterControl Method | Property |
setFallSpeed(1f) | Fall speed (down) |
setJumpSpeed(1f) | Jump speed (up) |
setMaxSlope(1.5f) | How steep the slopes are that the character can still climb. |
setUpAxis(1) | 0 = X axis , 1 = Y axis , 2 = Z axis. E.g. for characters and vehicle, up is usually along the the Y axis. |
setGravity(1f) | You can change the Gravity of a physics object after it was added to the physics space |
Physical objects…
A dynamic physics object is one that falls when in mid-air, it bounces off obstacles, and is pushed around when it collides with another physical object. It has a mass.
You can create static physical objects without mass. They are still treated as solid objects, but they cannot be dynamically pushed around. They act as static, immobile attached physical obstacles such as terrains and building models.
Kinematic RigidBodys have a mass, but they are not affected by gravity. When non-kinematic objects collide with a kinematic object, only the non-kinematic ones are pushed away by the collision. The intesity of the kinematic's effect against other objects depends on their speed and mass: Ekin = mass * speed^2
(well, approximately, bullet doesn't use Einsteins formula ;)) Tip: Spatials with a kinematic RigidBodyControl can be moved programmatically, e.g. using setLocalTranslation() in the update() loop, or by an Animation Path. You can also "hang them up in mid-air" and attach other PhysicsNodes to them using hinges and joints.
airhook.setKinematic(true);
Use the following methods to move physics objects.
Method | Motion |
---|---|
setAngularVelocity(new Vector3f(0f,0f,1f)) | Set the current rotational speed of the object; the x, y and z component are the speed of rotation around that axis. |
setLinearVelocity(new Vector3f(0f,0f,1f)) | Set the current linear speed of this object |
setWalkDirection(new Vector3f(0f,0f,0.1f)) | Make a physical character walk (characters are locked to prevent falling over and use a simple physics simulation). Use setWalkDirection(Vector3f.ZERO) to stop a directional motion. |
applyCentralForce(…) | Move (push) the object once with a certain moment, expressed as a Vector3f. |
applyForce(…) | Move (push) the object once with a certain moment, expressed as a Vector3f. Optionally, you can specify where on the object the pushing force hits. |
applyContinuousForce(…) | Keep moving (pushing) the object with continuous force in one direction, expressed as a Vector3f. Optionally, you can specifiy where on the object the pushing force hits. You can applyContinuousForce(false) to stop the force. |
applyTorque(…) | Rotate (twist) the object once around its axes, expressed as a Vector3f. |
applyContinuousTorque(…) | Keep rotating (twisting) the object continuously around its axes, expressed as a Vector3f. You can applyContinuousTorque(false) to stop the rotation. |
applyImpulse(…) | An idealised change of momentum. This is the kind of push that you would use on a pool billiard ball. |
applyTorqueImpulse(…) | An idealised change of momentum. This is the kind of push that you would use on a pool billiard ball. |
clearForces() | Cancels out all forces (force, torque) etc and stops the motion. |
Note: It is possible to position physics nodes using setLocalTranslation(), e.g. to place them in their start position in the scene. However you must be very careful not to cause an "impossible state" where one physical object overlaps with another! Within the game, you typically use the setters shown here exclusively. -Physics also supports the following features:
Method | Property |
---|---|
setCollisionShape(collisionShape) | Changes the collision shape. |
setCollideWithGroups() setCollisionGroup() addCollideWithGroup(COLLISION_GROUP_01) removeCollideWithGroup(COLLISION_GROUP_01) | Collision Groups are integer bit masks – enums are available in CollisionObject. All physics objects are by default in COLLISION_GROUP_01. Two objects collide when the collideWithGroups set of one contains the collisionGroup of the other. |
setDamping(float, float) | The first value is the linear threshold and the second the angular. |
setAngularFactor(1f) | Set the amount of rotation that will be applied. A value of zero will cancel all rotational force outcome. |
setCcdSweptSphereRadius() | ? |
setSleepingThreshold(float,float) | Sets the sleeping thresholds wich define when the object gets deactivated to save ressources. Low values keep the object active when it barely moves. The first value is the linear threshold and the second the angular. |
You can control the game by triggering forces; or may want to respond to collisions, e.g. by substracting health points, or by playing a sound. To specify how the game responds to physics events, you use Physics Listeners. -Do not overuse physics nodes. Although the physics nodes are put to “sleep” when they are not moved, creating a world solely out of dynamic physics nodes will quickly bring you to the limits of your computer's capabilities. -You can use normal non-physical Nodes in the same scene next to physical ones. Use the non-physical ones for non-solid things for which you do not want to detect collisions (ghost, foliage, plants, effects, …). This improves performance. -If you get weird behaviour, such as physical nodes jittering wildy and being ejected for no apparent reason, it usually means you have created an impossible state. Verify that none of the collision shapes overlap. This can happen when you create physical nodes in positions that are too close to other nodes; or if you position a physical node using setLocalTranslation() and it touches another node's collision shape. -For large static meshes like shooter levels or terrain its best to divide the mesh into multiple physics objects to allow the less cpu intense broadphase to filter out most collision items.
+Tip: Spheres and Boxes can fall back to the correct default Collision Shape if you do not specify a Collision Shape in the RigidBodyControl constructor. For example, the following creates a box with the correct Box Collision Shape: +
+Box(1,1,1); +myBox.addControl(new RigidBodyControl( 1.0f )); // implicit BoxCollisionShape +bulletAppState.getPhysicsSpace().add(myBox);+ +
+ +The Physics Space is an object in BulletAppState that is like a rootNode for Physics Controls. +
+bulletAppState.getPhysicsSpace().setGravity(new Vector3f(0f,-1f,0f)); +bulletAppState.getPhysicsSpace().setAccuracy(0.005f);+
bulletAppState.getPhysicsSpace().add(myPhysicsControl); ...+
myModel.addControl(myPhysicsControl); ...+
rootNode.attachChild(myModel); ...+
+ +You remove physical objects from the scene like this: + +
+bulletAppState.getPhysicsSpace().remove(myPhysicsControl); +myModel.removeFromParent();+ +
+ +On a PhysicsControl, you can set the following physical properties. +
+RigidBodyControl Method | Property | +
---|---|
setFriction(1f) | Friction. | +
setMass(1f) | Sets the mass. Dynamic objects have masses > 0.0f. +Static immobile obstacles (including buildings and terrains) have mass 0.0f. |
+
setPhysicsLocation() | Positions the object. Do not use setLocalTranslation(). | +
setPhysicsRotation() | Rotates the object. Do not use setLocalRotate(). | +
setRestitution(0.0f) | How bouncy the object is. For a rubber object set this > 0.0f. This setting has an impact on performance. | +
setKinematic(true) | A kinematic Spatial is not affected by gravity, but it is solid and affects other physics objects. It has a mass its position is updated from the spatials translation. You can attach joints to it. | +
setGravity(new Vector3f(0f,-1f,0f)) | You can change the gravity of a physics object after it was added to the physics space. | +
setCcdMotionThreshold(0.1f) | The amount of motion in 1 physics tick to trigger the continuous motion detection. | +
CharacterControl Method | Property | +
setFallSpeed(1f) | Fall speed (down) | +
setJumpSpeed(1f) | Jump speed (up) | +
setMaxSlope(1.5f) | How steep the slopes are that the character can still climb. | +
setUpAxis(1) | 0 = X axis , 1 = Y axis , 2 = Z axis. E.g. for characters and vehicle, up is usually along the the Y axis. | +
setGravity(1f) | You can change the Gravity of a physics object after it was added to the physics space | +
+ +All physical objects… +
+Property | Static | Dynamic | Kinematic | +
---|---|---|---|
Does it have a mass? | no, 0f | yes, >0f | yes, >0f (Inertia is calculated for kinematic objects you need mass to do that) | +
How does it move? | never | setWalkDirection(), setLinearVelocity(), applyForce(), etc | setLocalTranslation(), move() | +
Can it move and push others? | no | yes | yes | +
Is is affected by forces? +(Falls when it mid-air? Can be pushed by others?) | no | yes | no | +
Examples | Immobile obstacles: Floor, wall, buildings, … | Interactive objects: Soccer ball, movable crate, falling pillar, … | Remote-controlled objects: Airship, meteorites, networked NPCs, invisible "airhooks" for hinges and joints. | +
How to activate? | setMass(0f), (By default, objects are not kinematics) | setMass(1f), setKinematic(false) | setMass(1f), setKinematic(true) | +
+ +Tip: Typically, Spatials with a kinematic RigidBodyControl are moved programmatically, e.g. using setLocalTranslation() or move() in the update() loop, or by an Animation Path. You can also "hang them up in mid-air" and attach other PhysicsControls to them using hinges and joints. + +
+airhook.setKinematic(true);+ +
+ +Use the following methods to move physics objects. +
+PhysicsControl Method | Motion | +
---|---|
setLinearVelocity(new Vector3f(0f,0f,1f)) | Set the linear speed of this object. | +
setAngularVelocity(new Vector3f(0f,0f,1f)) | Set the rotational speed of the object; the x, y and z component are the speed of rotation around that axis. | +
applyCentralForce(…) | Move (push) the object once with a certain moment, expressed as a Vector3f. | +
applyForce(…) | Move (push) the object once with a certain moment, expressed as a Vector3f. Optionally, you can specify where on the object the pushing force hits. | +
applyContinuousForce(…) | Keep moving (pushing) the object with continuous force in one direction, expressed as a Vector3f. Optionally, you can specifiy where on the object the pushing force hits. You can applyContinuousForce(false) to stop the force. |
+
applyTorque(…) | Rotate (twist) the object once around its axes, expressed as a Vector3f. | +
applyContinuousTorque(…) | Keep rotating (twisting) the object continuously around its axes, expressed as a Vector3f. You can applyContinuousTorque(false) to stop the rotation. |
+
applyImpulse(…) | An idealised change of momentum. This is the kind of push that you would use on a pool billiard ball. | +
applyTorqueImpulse(…) | An idealised change of momentum. This is the kind of push that you would use on a pool billiard ball. | +
setWalkDirection(new Vector3f(0f,0f,0.1f)) | (CharacterControl only) Make a physical character walk. CharacterControls are locked upright to prevent falling over. Use setWalkDirection(Vector3f.ZERO) to stop a directional motion. |
+
clearForces() | Cancels out all forces (force, torque) etc and stops the motion. | +
+ +Note: It is technically possible to position PhysicsControls using setLocalTranslation(), e.g. to place them in their start position in the scene. However you must be very careful not to cause an "impossible state" where one physical object overlaps with another! Within the game, you typically use the setters shown here exclusively. +
+ ++PhysicsControls also supports the following features: +
+PhysicsControl Method | Property | +
---|---|
setCollisionShape(collisionShape) | Changes the collision shape. | +
setCollideWithGroups() +setCollisionGroup() +addCollideWithGroup(COLLISION_GROUP_01) +removeCollideWithGroup(COLLISION_GROUP_01) | Collision Groups are integer bit masks – enums are available in CollisionObject. All physics objects are by default in COLLISION_GROUP_01. Two objects collide when the collideWithGroups set of one contains, the Collision Group of the other. | +
setDamping(float, float) | The first value is the linear threshold and the second the angular. | +
setAngularFactor(1f) | Set the amount of rotation that will be applied. A value of zero will cancel all rotational force outcome. | +
setCcdSweptSphereRadius() | ? | +
setSleepingThreshold(float,float) | Sets the sleeping thresholds wich define when the object gets deactivated to save ressources. Low values keep the object active when it barely moves. The first value is the linear threshold and the second the angular. | +
You can control physical objects by triggering forces. Or maybe you want to respond to collisions, e.g. by substracting health points, or by playing a sound. To specify how the game responds to such physics events, you use Physics Listeners.
The jBullet Physics implementation is stepped at a constant 60 physics ticks per second frame rate. -Applying forces or checking for overlaps only has an effect right at a physics update cycle, which is not every frame. If you do physics interactions at arbitrary spots in the simpleUpdate() loop, calls will be dropped at irregular intervals, because they happen out of cycle.
When you write game mechanics that apply forces, you must implement a tick listener (com.jme3.bullet.PhysicsTickListener) for it. The tick listener makes certain the forces are not dropped, but applied in time for the next physics tick. + +
+You can control physical objects by triggering forces. Or maybe you want to respond to collisions, e.g. by substracting health points, or by playing a sound. To specify how the game responds to such physics events, you use Physics Listeners. + +
+ ++The jBullet Physics implementation is stepped at a constant 60 physics ticks per second frame rate. +Applying forces or checking for overlaps only has an effect right at a physics update cycle, which is not every frame. If you do physics interactions at arbitrary spots in the simpleUpdate() loop, calls will be dropped at irregular intervals, because they happen out of cycle. + +
+ +
+When you write game mechanics that apply forces, you must implement a tick listener (com.jme3.bullet.PhysicsTickListener) for it. The tick listener makes certain the forces are not dropped, but applied in time for the next physics tick.
Also, when you check for overlaps of physical objects with a PhysicsGhostObject, you cannot just go physicsSpace.add(ghost); ghost.getOverLappingObjects()
somewhere. You have to make certain 1 physics tick has passed before the overlapping objects list is filled with data. Again, the PhysicsTickListener does that for you.
-When your game mechanics however just poll the current state (e.g. location) of physical objects, or if you only use the Ghost control like a sphere trigger, then you don't need a PhysicsTickListener.
Here's is the declaration of an examplary Physics Control that listens to ticks.
public class MyCustomControl - extends RigidBodyControl implements PhysicsTickListener { ... }
When you implement the interface, you have to implement preTick() and postTick() methods.
prePhysicsTick()
is called before the step, here you apply forces (change the state).physicsTick()
is called after the step, here you poll the results (get the current state).@override +When your game mechanics however just poll the current state (e.g. location) of physical objects, or if you only use the Ghost control like a sphere trigger, then you don't need a PhysicsTickListener. + + + +
+Here's is the declaration of an examplary Physics Control that listens to ticks. + +
+public class MyCustomControl + extends RigidBodyControl implements PhysicsTickListener { ... }+ +
+ +When you implement the interface, you have to implement preTick() and postTick() methods. +
+prePhysicsTick()
is called before the step, here you apply forces (change the state).physicsTick()
is called after the step, here you poll the results (get the current state).@override public void prePhysicsTick(PhysicsSpace space, float f){ // apply state changes ... } @override public void physicsTick(PhysicsSpace space, float f){ // poll game state ... -}
If you do not implement the Collision Listener interface (com.jme3.bullet.collision.PhysicsCollisionListener), a collisions will just mean that physical forces are applied automatically. If you just want "Balls rolling, bricks falling" you do not need a listener. -If however you want to respond to a collision event (com.jme3.bullet.collision.PhysicsCollisionEvent) with a custom action, then you need to implement the PhysicsCollisionListener interface. Typical actions triggered by collisions include:
Again, here's the example declaration of a Physics Control that uses a collision listener.
public class MyCustomControl +}+ +
+If you do not implement the Collision Listener interface (com.jme3.bullet.collision.PhysicsCollisionListener), a collisions will just mean that physical forces are applied automatically. If you just want "Balls rolling, bricks falling" you do not need a listener. +If however you want to respond to a collision event (com.jme3.bullet.collision.PhysicsCollisionEvent) with a custom action, then you need to implement the PhysicsCollisionListener interface. Typical actions triggered by collisions include: +
++You need to add the PhysicsCollisionListener to the physics space before collisions will be listened for. Again, here's the example declaration of a Physics Control that uses a collision listener. + +
+public class MyCustomControl extends RigidBodyControl - implements PhysicsCollisionListener { ... }
To respond to the PhysicsCollisionEvent you have to override the collision()
method. This gives you access to the event object. Mostly you will be interested in the identity of any two nodes that collided: event.getNodeA()
and event.getNodeB()
.
-After you identify the colliding nodes, specify the action to trigger when this pair collides. Note that you cannot know which one will be Node A or Node B, you have to deal with either variant.
public void collision(PhysicsCollisionEvent event) { + implements PhysicsCollisionListener { + public MyCustomControl() { + bulletAppState.getPhysicsSpace().addCollisionListener(this); + ... + }+ +
+
+To respond to the PhysicsCollisionEvent you have to override the collision()
method. This gives you access to the event object. Mostly you will be interested in the identity of any two nodes that collided: event.getNodeA()
and event.getNodeB()
.
+After you identify the colliding nodes, specify the action to trigger when this pair collides. Note that you cannot know which one will be Node A or Node B, you have to deal with either variant.
+
+
public void collision(PhysicsCollisionEvent event) { if ( event.getNodeA().getName().equals("player") ) { final Node node = event.getNodeA(); /** ... do something with the node ... */ @@ -49,31 +119,52 @@ After you identify the colliding nodes, specify the action to trigger when this final Node node = event.getNodeB(); /** ... do something with the node ... */ } - }
The PhysicsCollisionEvent event
gives you access to detailed information about the collision. You already know the event objects can identify which nodes collided, but it even knows how hard they collided:
Method | Purpose |
---|---|
getObjectA() getObjectB() | The two participants in the collision. You cannot know in advance whether some node will be recorded as A or B, you always have to consider both cases. |
getAppliedImpulse() | A float value representing the collision impulse |
getAppliedImpulseLateral1() | A float value representing the lateral collision impulse |
getAppliedImpulseLateral2() | A float value representing the lateral collision impulse |
getCombinedFriction() | A float value representing the collision friction |
getCombinedRestitution() | A float value representing the collision restitution (bounciness) |
Note that after the collision method has been called the object is not valid anymore so you should copy any data you want to keep into local variables.
+ +
+The PhysicsCollisionEvent event
gives you access to detailed information about the collision. You already know the event objects can identify which nodes collided, but it even knows how hard they collided:
+
Method | Purpose | +
---|---|
getObjectA() +getObjectB() | The two participants in the collision. You cannot know in advance whether some node will be recorded as A or B, you always have to consider both cases. | +
getAppliedImpulse() | A float value representing the collision impulse | +
getAppliedImpulseLateral1() | A float value representing the lateral collision impulse | +
getAppliedImpulseLateral2() | A float value representing the lateral collision impulse | +
getCombinedFriction() | A float value representing the collision friction | +
getCombinedRestitution() | A float value representing the collision restitution (bounciness) | +
+Note that after the collision method has been called the object is not valid anymore so you should copy any data you want to keep into local variables. + +
+ +This awesome water effect is highly configurable and can render any type of water. It is is based on Wojciech Toman’s Rendering Water as a Post-process Effect published on gamedev.net. Here's a video:
In this article the author uses the effect in a deferred rendering process, taking advantage of the pre-computed position buffer and back buffer (a texture representing the screen’s pixels position in view space, and a texture of the rendered scene).
After some calculation this allows to reconstruct the position in world space for each pixel on the screen. If a pixel is under a given water height, let’s render it as a blue pixel! Blue pixel? Not exactly, we want waves, we want ripples, we want foam, we want reflection and refraction.
The GameDev.net article describes how those effects are achieved, but the main idea is to generate waves from a height map, create ripples from a normal map, blend in the foam texture when the water depth is below a certain height, compute the refraction color with a clever color extinction algorithm, and then, display the reflection and specular effect by computing a Fresnel term (like in standard water effect). In addition this effect, allow to blend the water shore with the ground to avoid the hard edges effect of classic water effects based on grids or quads.
jME3 default behavior is to use a forward rendering process, so there is no position buffer rendered that we can take advantage of. But while rendering the main scene to a frame buffer in the FilterPostPorcessor, we can write the hardware depth buffer to a texture, with nearly no additional cost.
I won’t go into the details of this, but there are several ways of reconstructing the world space position of a pixel from the depth buffer. The computational cost is higher than just fetching the position form a position buffer, but the bandwidth and the memory required is a lot lower.
We have the rendered scene in a texture, and we can reconstruct the position in world space of each pixel… We’re good to go!
See also: JME3's Water Post-Process Effect by Nehon
There are two test cases in the jME3 repository:
In the simpleInitApp()
method, you attach your scene to the rootNode, typically a terrain with a sky. Remember to add a directional light, since the water relies on the light direction vector. The WaterFilter constrcutor expects a node with the scene attached that should be reflected in the water, and vector information from the light source's direction.
This is how you use the water filter post-processor code in your code:
private FilterPostProcessor fpp; + ++Rendering Water as Post-Process Effect
++ ++ ++ +The awesome SeaMonkey water Filter is highly configurable. It can render any type of water and also simulates the underwater part of the effect, including "caustics". It is is based on published on gamedev.net. Here's a video: +
+ ++ +
+ +The Theory
++ ++ ++ +The effect is part of a deferred rendering process, taking advantage of the pre-computed position buffer and back buffer (a texture representing the screen’s pixels position in view space, and a texture of the rendered scene). +
+ ++After some calculation, this allows to reconstruct the position in world space for each pixel on the screen. "If a pixel is under a given water height, let’s render it as a blue pixel!" Blue pixel? Not exactly, we want waves, we want ripples, we want foam, we want reflection and refraction. +
+ ++The GameDev.net article describes how those effects are achieved, but the main idea is to generate waves from a height map, create ripples from a normal map, blend in the foam texture when the water depth is below a certain height, compute the refraction color with a clever color extinction algorithm, and then, display the reflection and specular effect by computing a Fresnel term (like in the simple water effect). In addition, this effect allows to blend the water shore with the ground to avoid the hard edges of classic water effects based on grids or quads. +
+ +How Did We Implement it in jME3?
++ ++ ++ +jME3 default behavior is to use a forward rendering process, so there is no position buffer rendered that we can take advantage of. But while rendering the main scene to a frame buffer in the FilterPostPorcessor, we can write the hardware depth buffer to a texture, with nearly no additional cost. +
+ ++There are several ways of reconstructing the world space position of a pixel from the depth buffer. The computational cost is higher than just fetching the position from a position buffer, but the bandwidth and the memory required is a lot lower. +
+ ++Now we have the rendered scene in a texture, and we can reconstruct the position in world space of each pixel. We’re good to go! +
+ ++– Nehon +
+ +Sample Code
++ ++ ++ +There are two test cases in the jME3 repository: + +
++
+- +
(ocean island)++
+ +- +
(calm and muddy water pond)+Using the Water Filter
++ ++ +In the
+ +simpleInitApp()
method, you attach your scene to the rootNode, typically a terrain with a sky. Remember to add a directional light, since the water relies on the light direction vector. The WaterFilter constrcutor expects a node with the scene attached that should be reflected in the water, and vector information from the light source's direction. ++This is how you use the water filter post-processor code in your code: +
+private FilterPostProcessor fpp; private WaterFilter water; private Vector3f lightDir = new Vector3f(-4.9f, -1.3f, 5.9f); // same as light source private float initialWaterHeight = 0.8f; // choose a value for your scene @@ -32,10 +97,23 @@ public void simpleInitApp() { fpp.addFilter(water); viewPort.addProcessor(fpp); ... -}Usually you make the water reflect everything attached to the rootNode. But you can also give a custom node (a subnode of the rootNode) to the WaterFilter constructor that has only a subset of scene nodes attached. This would be a relevant optimization if you have lots of nodes that are far away from the water, or covered, and will never be reflected.
Optional: Waves
+ +If you want waves, set the water height in the update loop. We reuse the initialWaterHeight variable, and repeatedly reset the waterHeight value according to time. This causes the waves.
private float time = 0.0f; -private float waterHeight = 0.0f; +}+ ++Usually you make the water reflect everything attached to the rootNode. But you can also give a custom node (a subnode of the rootNode) to the WaterFilter constructor that has only a subset of scene nodes attached. This would be a relevant optimization if you have lots of nodes that are far away from the water, or covered, and will never be reflected. +
+ +Optional: Waves
++ ++ +If you want waves, set the water height in the update loop. We reuse the initialWaterHeight variable, and repeatedly reset the waterHeight value according to time. This causes the waves. +
+private float time = 0.0f; +private float waterHeight = 0.0f; @Override public void simpleUpdate(float tpf) { @@ -43,155 +121,158 @@ public void simpleUpdate(float tpf) { time += tpf; waterHeight = (float) Math.cos(((time * 0.6f) % FastMath.TWO_PI)) * 1.5f; water.setWaterHeight(initialWaterHeight + waterHeight); -}Optional: Water Wave and Color Effects
All these effects are optional. Every setter also has a getter.
Water method example Effects: Waves Default water.setWaterHeight(-6); Use this waterheight method for causing waves. 0.0f water.setMaxAmplitude(0.3f); How high the highest waves are. 1.0f water.setWaveScale(0.008f); Sets the scale factor of the waves height map. The smaller the value, the bigger the waves! 0.005f water.setWindDirection(new Vector2f(0,1)) Sets the wind direction, which is the direction where the waves move Vector2f(0.0f, -1.0f) water.setSpeed(0.7f); How fast the waves move. Set it to 0.0f for still water. 1.0f water.setHeightTexture( (Texture2D)
manager.loadTexture("Textures/waveheight.png") )This height map describes the shape of the waves "Common/MatDefs/Water/Textures/heightmap.jpg" water.setNormalTexture( (Texture2D)
manager.loadTexture("Textures/wavenormals.png") )This normal map describes the shape of the waves "Common/MatDefs/Water/Textures/gradient_map.jpg" water.setUseRipples(false); Switches the ripples effect on or off. true water.setNormalScale(0.5f) Sets the normal scaling factors to apply to the normal map. The higher the value, the more small ripples will be visible on the waves. 1.0f
Water method example Effects: Color Default water.setLightDirection(new Vector3f(-0.37f,-0.50f,-0.78f)) Usually you set this to the same as the light source's direction. Use this to set the light direction if the sun is moving. Value given to WaterFilter() constructor. water.setLightColor(ColorRGBA.White) Usually you set this to the same as the light source's color. RGBA.White water.setWaterColor(ColorRGBA.Brown.mult(2.0f)); Sets the main water color. greenish blue
Vector3f(0.0f,0.5f,0.5f,1.0f)water.setDeepWaterColor(ColorRGBA.Brown); Sets the deep water color. dark blue
Vector3f(0.0f, 0.0f,0.2f,1.0f)water.setWaterTransparency(0.2f); Sets how fast colors fade out. use this to control how clear (e.g. 0.05f) or muddy (0.2f) water is. 0.1f water.setColorExtinction(new Vector3f(10f,20f,30f)); Sets At what depth the refraction color extincts. The three values are RGB (red, green, blue) in this order. Play with these parameters to "muddy" the water. Vector3f(5f,20f,30f)
Water method example Effects: Shore Default water.setShoreHardness(1.0f); Sets how soft the transition between shore and water should be. High values mean a harder transition between shore and water. 0.1f water.setUseHQShoreline(false); Renders shoreline with better quality ? true
Water method example Effects: Foam Default water.setUseFoam(false); Switches the white foam on or off true water.setFoamHardness(0.5f) Sets how much the foam will blend with the shore to avoid a hard edged water plane. 1.0f water.setFoamExistence(new Vector3f(0.5f,5f,1.0f)) The three values describe what depth foam starts to fade out, at what depth it is completely invisible, at what height foam for waves appears (+ waterHeight). Vector3f(0.45f,4.35f,1.0f) water.setFoamTexture( (Texture2D)
manager.loadTexture("Textures/foam.png") )This foam texture will be used with WrapMode.Repeat "Common/MatDefs/Water/Textures/foam.jpg"
Water method example Effects: Light Default water.setSunScale(1f); Sets how big the sun should appear in the light's specular effect on the water. 3.0f water.setUseSpecular(false) Switches specular effect on or off true water.setShininess(0.8f) Sets the shininess of the water reflections 0.7f water.setUseRefraction(true) Switches the refraction effect on or off. true water.setRefractionConstant(0.2f); The lower the value, the less reflection can be seen on water. This is a constant related to the index of refraction (IOR) used to compute the fresnel term. 0.3f water.setRefractionStrength(-0.1) This value modifies the current Fresnel term. If you want to weaken reflections use bigger value. If you want to empasize them, use a value smaller than 0. 0.0f water.setReflectionMapSize(256) Sets the size of the reflection map. The higher, the better the quality, but the slower the effect. 512 Sound Effects
+ +You should also add audio nodes with water sounds to complete the effect.
AudioNode waves = new AudioNode(assetManager, "Sound/Environment/Ocean Waves.ogg", false); +}+ +Optional: Water Wave and Color Effects
++ ++ ++ + +
+ ++All these effects are optional. Every setter also has a getter. + +
+++
+ +Water method example Effects: Waves Default ++ +water.setWaterHeight(-6); Use this waterheight method for causing waves. 0.0f ++ +water.setMaxAmplitude(0.3f); How high the highest waves are. 1.0f ++ +water.setWaveScale(0.008f); Sets the scale factor of the waves height map. The smaller the value, the bigger the waves! 0.005f ++ +water.setWindDirection(new Vector2f(0,1)) Sets the wind direction, which is the direction where the waves move Vector2f(0.0f, -1.0f) ++ +water.setSpeed(0.7f); How fast the waves move. Set it to 0.0f for still water. 1.0f ++ +water.setHeightTexture( (Texture2D)
+manager.loadTexture("Textures/waveheight.png") )This height map describes the shape of the waves "Common/MatDefs/Water/Textures/heightmap.jpg" ++ +water.setNormalTexture( (Texture2D)
+manager.loadTexture("Textures/wavenormals.png") )This normal map describes the shape of the waves "Common/MatDefs/Water/Textures/gradient_map.jpg" ++ +water.setUseRipples(false); Switches the ripples effect on or off. true ++ +water.setNormalScale(0.5f) Sets the normal scaling factors to apply to the normal map. The higher the value, the more small ripples will be visible on the waves. 1.0f +++
+ +Water method example Effects: Color Default ++ +water.setLightDirection(new Vector3f(-0.37f,-0.50f,-0.78f)) Usually you set this to the same as the light source's direction. Use this to set the light direction if the sun is moving. Value given to WaterFilter() constructor. ++ +water.setLightColor(ColorRGBA.White) Usually you set this to the same as the light source's color. RGBA.White ++ +water.setWaterColor(ColorRGBA.Brown.mult(2.0f)); Sets the main water color. greenish blue +
+Vector3f(0.0f,0.5f,0.5f,1.0f)+ +water.setDeepWaterColor(ColorRGBA.Brown); Sets the deep water color. dark blue +
+Vector3f(0.0f, 0.0f,0.2f,1.0f)+ +water.setWaterTransparency(0.2f); Sets how fast colors fade out. use this to control how clear (e.g. 0.05f) or muddy (0.2f) water is. 0.1f ++ +water.setColorExtinction(new Vector3f(10f,20f,30f)); Sets At what depth the refraction color extincts. The three values are RGB (red, green, blue) in this order. Play with these parameters to "muddy" the water. Vector3f(5f,20f,30f) +++
+ +Water method example Effects: Shore Default ++ +water.setShoreHardness(1.0f); Sets how soft the transition between shore and water should be. High values mean a harder transition between shore and water. 0.1f ++ +water.setUseHQShoreline(false); Renders shoreline with better quality ? true +++
+ +Water method example Effects: Foam Default ++ +water.setUseFoam(false); Switches the white foam on or off true ++ +water.setFoamHardness(0.5f) Sets how much the foam will blend with the shore to avoid a hard edged water plane. 1.0f ++ +water.setFoamExistence(new Vector3f(0.5f,5f,1.0f)) The three values describe what depth foam starts to fade out, at what depth it is completely invisible, at what height foam for waves appears (+ waterHeight). Vector3f(0.45f,4.35f,1.0f) ++ +water.setFoamTexture( (Texture2D)
+manager.loadTexture("Textures/foam.png") )This foam texture will be used with WrapMode.Repeat "Common/MatDefs/Water/Textures/foam.jpg" ++ ++
+ +Water method example Effects: Light Default ++ +water.setSunScale(1f); Sets how big the sun should appear in the light's specular effect on the water. 3.0f ++ +water.setUseSpecular(false) Switches specular effect on or off true ++ +water.setShininess(0.8f) Sets the shininess of the water reflections 0.7f ++ +water.setUseRefraction(true) Switches the refraction effect on or off. true ++ +water.setRefractionConstant(0.2f); The lower the value, the less reflection can be seen on water. This is a constant related to the index of refraction (IOR) used to compute the fresnel term. 0.3f ++ +water.setRefractionStrength(-0.1) This value modifies the current Fresnel term. If you want to weaken reflections use bigger value. If you want to empasize them, use a value smaller than 0. 0.0f ++ +water.setReflectionMapSize(256) Sets the size of the reflection map. The higher, the better the quality, but the slower the effect. 512 +Sound Effects
++ +- \ No newline at end of file +audioRenderer.playSource(waves);+ +You should also add audio nodes with water sounds to complete the effect. +
+AudioNode waves = new AudioNode(assetManager, "Sound/Environment/Ocean Waves.ogg", false); waves.setLooping(true); -audioRenderer.playSource(waves);
+See also: +
+ The jMonkeyEngine3 has built-in support for jBullet physics via the com.jme3.bullet
package. Physics are not only responsible for handing collisions, but they also make hinges and joints possible. One special example of physical joints are ragdoll physics, shown here.
The ragdoll is a simple dummy that we build out of cylinder collision shapes. It has 11 limbs: shoulders, a body, and hips; plus 2 arms and 2 legs are made up of two limbs each. In your game, you replace the cylinders with your own limb models.
Since we're just creating the ragdoll for this example, all the limbs have the same shape, and we can write a simple helper method to create them. The function returns a PhysicsNode with CollisionShape with the width, height, location, and rotation (vertical or horizontal) that we specify. We choose a CapsuleCollisionShape (a cylinder with rounded top and bottom) so the limbs collide smoothly against one another.
private Node createLimb(float width, float height, Vector3f location, boolean rotate) { + +Ragdoll Physics
++ ++ ++ +The jMonkeyEngine3 has built-in support for via the
+ +com.jme3.bullet
package. Physics are not only responsible for handing collisions, but they also make hinges and joints possible. One special example of physical joints are ragdoll physics, shown here. ++ +
+ +Sample Code
+++ ++
+ +- +
(Tip: Click to pull the ragdoll up)+- +
– This ragdoll replaces a rigged model of a character in the moment it is "shot" to simulate a collapsing person. (Also note DoF of the limbs.)+Preparing the Physics Game
+++ ++
+ +- +
Create a SimpleApplication with a BulletAppState++
+- +
This gives us a PhysicsSpace for PhysicControls+- +
Add a physical floor (A box collision shape with mass zero)+Creating the Ragdoll
++ ++ ++ +A ragdoll is a simple "person" (dummy) that you build out of cylinder collision shapes. The ragdoll has 11 limbs: 1 for shoulders, 1 for the body, 1 for hips; plus 2 arms and 2 legs that are made up of two limbs each. In your game, you will likely replace the cylinders with your own (better looking) limb models. In this example here we just use simple cylinders. +
+ +Limbs
++ ++ +Since you're just creating the ragdoll for this example, all the limbs have the same shape, and you can write a simple helper method to create them. The function returns a PhysicsControl with CollisionShape with the width, height, location, and rotation (vertical or horizontal) that you specify. You choose a CapsuleCollisionShape (a cylinder with rounded top and bottom) so the limbs collide smoothly against one another. +
+private Node createLimb(float width, float height, Vector3f location, boolean rotate) { int axis = rotate ? PhysicsSpace.AXIS_X : PhysicsSpace.AXIS_Y; CapsuleCollisionShape shape = new CapsuleCollisionShape(width, height, axis); Node node = new Node("Limb"); @@ -33,15 +64,22 @@ class="level3">Since we're just creating the ragdoll for this example, node.setLocalTranslation(location); node.addControl(rigidBodyControl); return node; -}
We use this helper method to initialize the 11 limbs. Look at the screenshot above for orientation.
All cylinders have the same diameter, 0.2f. We make the body and shoulders longer than the other limbs, 1.0f instead of 0.5f. We determine the coordinates for positioning the limbs to form a person. The shoulders and hips are vertical cylinders, this is why we set the rotation to true.Node shoulders = createLimb(0.2f, 1.0f, new Vector3f( 0.00f, 1.5f, 0), true); +}+ ++You write a custom helper method to initialize the limbs. Look at the screenshot above for orientation. +
++
+- +
All cylinders have the same diameter, 0.2f.+- +
You make the body and shoulders longer than the other limbs, 1.0f instead of 0.5f.+- +
You determine the coordinates for positioning the limbs to form a person.+- +
The shoulders and hips are vertical cylinders, this is why we set the rotation to true.+Node shoulders = createLimb(0.2f, 1.0f, new Vector3f( 0.00f, 1.5f, 0), true); Node uArmL = createLimb(0.2f, 0.5f, new Vector3f(-0.75f, 0.8f, 0), false); Node uArmR = createLimb(0.2f, 0.5f, new Vector3f( 0.75f, 0.8f, 0), false); Node lArmL = createLimb(0.2f, 0.5f, new Vector3f(-0.75f,-0.2f, 0), false); @@ -51,15 +89,27 @@ Node hips = createLimb(0.2f, 0.5f, new Vector3f( 0.00f,-0.5f, 0) Node uLegL = createLimb(0.2f, 0.5f, new Vector3f(-0.25f,-1.2f, 0), false); Node uLegR = createLimb(0.2f, 0.5f, new Vector3f( 0.25f,-1.2f, 0), false); Node lLegL = createLimb(0.2f, 0.5f, new Vector3f(-0.25f,-2.2f, 0), false); -Node lLegR = createLimb(0.2f, 0.5f, new Vector3f( 0.25f,-2.2f, 0), false);We now have the outline of a person. But if we ran the application now, the individual limbs would fall down independently of one another – the ragdoll is still lacking joints.
Joints
+ +As before, we write a small helper method. This time its purpose is to quickly join two limbs A and B at the connection point that we specify.
We convert A's and B's connectionPoint vector from world coordinate space to local coordinate space. We use a ConeJoint, a special joint that approximates the degree of freedom that limbs typically have. The ConeJoint constructor requires the two nodes, and the two local pivot coordinates that we just determined. We set the joints limits to allow swinging, but not twisting.private PhysicsJoint join(Node A, Node B, Vector3f connectionPoint) { +Node lLegR = createLimb(0.2f, 0.5f, new Vector3f( 0.25f,-2.2f, 0), false);+ ++You now have the outline of a person. But if you ran the application now, the individual limbs would fall down independently of one another – the ragdoll is still lacking joints. +
+ +Joints
++ ++ + + ++ +As before, you write a small helper method. This time its purpose is to quickly join two limbs A and B at the connection point that we specify. +
++
- +
Convert A's and B's connectionPoint vector from world coordinate space to local coordinate space.+- +
Use a ConeJoint, a special joint that approximates the degree of freedom that limbs typically have. The ConeJoint constructor requires the two nodes, and the two local pivot coordinates that we just determined.+ Set the joints limits to allow swinging, but not twisting.< pivotA, pivotB); joint.setLimit(1f, 1f, 0); return joint; -}private PhysicsJoint join(Node A, Node B, Vector3f connectionPoint) { Vector3f pivotA = A.worldToLocal(connectionPoint, new Vector3f()); Vector3f pivotB = B.worldToLocal(connectionPoint, new Vector3f()); ConeJoint joint = new ConeJoint(A.getControl(RigidBodyControl.class), @@ -67,7 +117,16 @@ class="li"> We set the joints limits to allow swinging, but not twisting.We use the helper method to connect all limbs with joints where they belong, at one end of the limb.
join(body, shoulders, new Vector3f( 0.00f, 1.4f, 0)); +}++ +Use the helper method to connect all limbs with joints where they belong, at one end of the limb. +
+join(body, shoulders, new Vector3f( 0.00f, 1.4f, 0)); join(body, hips, new Vector3f( 0.00f, -0.5f, 0)); join(uArmL, shoulders, new Vector3f(-0.75f, 1.4f, 0)); join(uArmR, shoulders, new Vector3f( 0.75f, 1.4f, 0)); @@ -76,9 +135,22 @@ join(uArmR, lArmR, new Vector3f( 0.75f, 0.4f, 0)); join(uLegL, hips, new Vector3f(-0.25f, -0.5f, 0)); join(uLegR, hips, new Vector3f( 0.25f, -0.5f, 0)); join(uLegL, lLegL, new Vector3f(-0.25f, -1.7f, 0)); -join(uLegR, lLegR, new Vector3f( 0.25f, -1.7f, 0));Now the ragdoll is connected. If we ran the app now, the doll would collapse, but the limbs would stay together.
We create one (non-physical) Node named ragDoll, and attach all other nodes to it.
ragDoll.attachChild(shoulders); +join(uLegR, lLegR, new Vector3f( 0.25f, -1.7f, 0));+ +
+Now the ragdoll is connected. If you ran the app now, the doll would collapse, but the limbs would stay together. +
+ ++ +We create one (non-physical) Node named ragDoll, and attach all other nodes to it. +
+ragDoll.attachChild(shoulders); ragDoll.attachChild(body); ragDoll.attachChild(hips); ragDoll.attachChild(uArmL); @@ -88,15 +160,49 @@ ragDoll.attachChild(lArmR); ragDoll.attachChild(uLegL); ragDoll.attachChild(uLegR); ragDoll.attachChild(lLegL); -ragDoll.attachChild(lLegR);
To use the ragdoll in a scene, we attach its main node to the rootNode, and to the PhysicsSpace.
rootNode.attachChild(ragDoll); -bulletAppState.getPhysicsSpace().addAll(ragDoll);
To pull the doll up, you could add an input handler that triggers the following action:
Vector3f upforce = new Vector3f(0, 200, 0); -shoulders.applyContinuousForce(true, upforce);
We can use the action to pick the doll up and put it back on its feet, or what ever. Read more about Forces here.
Read the Responding to a PhysicsCollisionEvent chapter in the general physics documentation on how to detect collisions.
If you are seeing weird behaviour in a ragdoll – such as exploding into pieces and then reassembling – check your collision shapes. Verify you did not position the limbs too close to one another when assmebling the ragdoll. You typically see physical nodes being ejected when their collision shapes intersect, which puts physics in an impossible state.
+To use the ragdoll in a scene, we attach its main node to the rootNode, and to the PhysicsSpace. +
+rootNode.attachChild(ragDoll); +bulletAppState.getPhysicsSpace().addAll(ragDoll);+ + + +
+ +To pull the doll up, you could add an input handler that triggers the following action: +
+Vector3f upforce = new Vector3f(0, 200, 0); +shoulders.applyContinuousForce(true, upforce);+ +
+We can use the action to pick the doll up and put it back on its feet, or what ever. Read more about Forces here. +
+ ++ +Read the Responding to a PhysicsCollisionEvent chapter in the general physics documentation on how to detect collisions. You can detect collisions between limbs or between limbs and the floor, and trigger game events. +
+ ++ +If you experience weird behaviour in a ragdoll – such as exploding into pieces and then reassembling – check your collision shapes. Verify you did not position the limbs too close to one another when assmebling the ragdoll. You typically see physical nodes being ejected when their collision shapes intersect, which puts physics in an impossible state. +
+ +
+
+You can read the graphic card's capabilities using the com.jme3.renderer.Caps
class:
+
Collection<Caps> caps = renderer.getCaps(); +Logger.getLogger(HelloWorld.class.getName()).log(Level.INFO, “Caps: {0}” + caps.toString());+ +
+Replace HelloWorld by the name of the class where you are using this line. +
+ ++The result looks like the following example: +
+Caps: [FrameBuffer, FrameBufferMRT, FrameBufferMultisample, +OpenGL20, ARBprogram, GLSL100, GLSL110, GLSL120, +VertexTextureFetch, FloatTexture, TextureCompressionLATC]+ +
+This would tell you that this user's graphic card only supports OpenGL 2.0 and cannot handle newer OpenGL features. + +
+ ++ +You can steer the camera using Cinematics: +
+CameraNode camNode = cinematic.bindCamera("topView", cam);+
cinematic.activateCamera(6, "topView");+
flyCam.setEnabled(false); +Cinematic cinematic = new Cinematic(rootNode, 20); + +CameraNode camNodeTop = cinematic.bindCamera("topView", cam); +camNodeTop.setControlDir(ControlDirection.SpatialToCamera); +camNodeTop.getControl(0).setEnabled(false); + +CameraNode camNodeSide = cinematic.bindCamera("sideView", cam); +camNodeSide.setControlDir(ControlDirection.CameraToSpatial); +camNodeSide.getControl(0).setEnabled(false);+ +
+ +If desired, attach the camNode to a MotionTrack to let it travel along waypoints. This is demonstrated in the . +
+ ++ +You can save and load scenes and individual Nodes using com.jme3.export.binary.BinaryExporter and com.jme3.export.binary.BinaryImporter. Use standard Java serialization to load game data. The jMonkeyEngine binary file format is .j3o. You can open, view, and edit .j3o files in the jMonkeyPlatform. +
+ +@Override + public void destroy() { + System.getProperty("user.home"); + BinaryExporter exporter = BinaryExporter.getInstance(); + File(userHome+"/somefile.j3o"); + try { + exporter.save(rootNode, file); + } catch (IOException ex) { + Logger.getLogger(Main.class.getName()).log(Level.SEVERE, "Failed to save node!", ex); + } + super.destroy(); + }+ +
@Override + public void simpleInitApp() { + System.getProperty("user.home"); + BinaryImporter importer = BinaryImporter.getInstance(); + importer.setAssetManager(assetManager); + File(userHome+"/somefile.j3o"); + try { + Node loadedNode = (Node)importer.load(file); + loadedNode.setName("loaded node"); + rootNode.attachChild(loadedNode); + } catch (IOException ex) { + Logger.getLogger(Main.class.getName()).log(Level.SEVERE, "No saved node loaded.", ex); + } + ... ++ +
The simplest type of Meshes are jME's default Shapes. You can use Shapes to build complex Geometries. Shapes are created without using the AssetManager.
3D shapes:
Non-3D shapes:
To add a shape to the scene:
Sphere sphereMesh = new Sphere(32, 32, 10, false, true); -Geometry sphere = new Geometry("Sky", sphereMesh); -rootNode.attachChild(sphere);
+The simplest type of Meshes are the built-in JME Shapes. You can create Shapes without using the AssetManager. + +
+ ++Do not mix up these visible Shapes with similarly named classes from the maths package. Choose the right package when letting your IDE fill in the import statements! +
++These maths objects are invisible and are used for collision testing (ray casting) or to describe motion paths. They cannot be wrapped into a Geometry. + +
+ ++To add a shape to the scene: +
++You can compose more complex custom Geometries out of simple Shapes. Think of the buildings in games like Angry Birds, or the building blocks in Second Life ("prims") and in Tetris ("Tetrominos"). +
+setLocalTranslation()
) so that the Node is in the center of the new constellation. The central Node is the pivot point for transformations (move/scale/rotate).
+The order is important: First arrange around origin, then transform. Otherwise, transformations are applied around the wrong center (pivot). Of course, you can attach your constellation to other pivot Nodes to create even more complex shapes (a chair, a furnished room, a house, a city, …), but again, arrange them around the origin first before you transform them.
+
+
+Note: Obviously, these composed Geometries are simpler than hand-sculpted meshes from a mesh editor.
+
+
Sphere mesh = new Sphere(32, 32, 10, false, true);+
Dome mesh = new Dome(Vector3f.ZERO, 2, 4, 1f,false); // Pyramid+
Dome mesh = new Dome(Vector3f.ZERO, 2, 32, 1f,false); // Cone+
Dome mesh = new Dome(Vector3f.ZERO, 32, 32, 1f,false); // Small hemisphere+
Dome mesh = new Dome(Vector3f.ZERO, 32, 32, 1000f,true); // SkyDome+
PQTorus mesh = new PQTorus(5,3, 2f, 1f, 32, 32); // Spiral torus+
PQTorus mesh = new PQTorus(3,8, 2f, 1f, 32, 32); // Flower torus+ +
+ +Use one of the above examples together with the following geometry in a scene: + +
+Geometry geom = new Geometry("A shape", mesh); +Material mat = new Material(assetManager, + "Common/MatDefs/Misc/ShowNormals.j3md"); +geom.setMaterial(mat); +rootNode.attachChild(geom);+ +
Here is an example for how you add a static horizon (a background landscape and a sky) to a scene. -Having a discernable horizon with a suitable landscape (or space, or ocean, or whatever) in the background makes scenes look more realistic than just a single-colored "sky" background.
Adding a sky is extremely easy using the com.jme3.util.SkyFactory
.
rootNode.attachChild(SkyFactory.createSky( - assetManager, "Textures/Sky/Bright/BrightSky.dds", false));
To add a sky you need to supply:
Internally, the SkyFactory calls the following methods:
sky.setQueueBucket(Bucket.Sky);
makes certain the sky is rendered in the right order, behind everything else.sky.setCullHint(Spatial.CullHint.Never);
makes certain that the sky is never culled.Sky.j3md
. This Material definition works with sphere and cube maps.As the sky texture we use the sample BrightSky.dds file from jme3test-test-data.
How to create a sky textures?
Box or Sphere?
+
+
+
+
+Here is an example for how you add a static horizon (a background landscape and a sky) to a scene. +Having a discernable horizon with a suitable landscape (or space, or ocean, or whatever) in the background makes scenes look more realistic than just a single-colored "sky" background. +
+ +
+
+Adding a sky is extremely easy using the com.jme3.util.SkyFactory
.
+
rootNode.attachChild(SkyFactory.createSky( + assetManager, "Textures/Sky/Bright/BrightSky.dds", false));+ +
+To add a sky you need to supply: +
++ +Internally, the SkyFactory calls the following methods: +
+sky.setQueueBucket(Bucket.Sky);
makes certain the sky is rendered in the right order, behind everything else.sky.setCullHint(Spatial.CullHint.Never);
makes certain that the sky is never culled.Sky.j3md
. This Material definition works with sphere and cube maps. + +As the sky texture we use the sample BrightSky.dds file from jme3test-test-data. +
+ ++How to create a sky textures? + +
++ +Box or Sphere? +
+This is an introduction to the concept of of Spatials, the elements of the 3D scene graph. The scene graph is a data structure that manages all objects in your 3D world. For example it keeps track of the 3D models that you load and position. When you extend a Java class from com.jme3.app.SimpleApplication, you inherit the scene graph and rootNode.
The main element of the scene graph is a Spatial called rootNode. All other Spatials are attached to the rootNode in a parent-child relationship. If you think you want to understand the scene graph better, please read Scenegraph for dummies first.
A Spatial is either a Node or a Geometry.
Spatials | ||
---|---|---|
Purpose: | A Spatial is an abstract data structure that stores transformations (translation, rotation, scale) of elements of the scene graph. A Spatial can be saved and loaded using the AssetManager. | |
com.jme3.scene.Geometry | com.jme3.scene.Node | |
Visibility: | A Geometry represents a visible 3-D object in the scene graph. | A Node is an invisible "handle" for a group of objects in the scene graph. |
Purpose: | Use Geometries to represent an object's looks: Every Geometry contains a polygon mesh and a material, specifying its shape, color, texture, and opacity/transparency. You can attach a Geometry to an Node. | Use nodes to structure and group Geometries and other Nodes. Every Node is attached to parent node, and the node can have children attached to itself. When you transform a parent node, all its children are transformed as well. |
Content: | Transformations, mesh, material. | Transformations. No mesh, no material. |
Examples: | A box, a sphere, player, a building, a piece of terrain, a vehicle, missiles, NPCs, etc… | The rootNode, the guiNode, an audio node, a custom grouping node, etc… |
Important: You never create a Spatial with Spatial s = new Spatial();
– it's abstract. Instead you create (e.g. load) a Node or Geometry object, and cast it to Spatial. You use the Spatial type in methods that accept both Nodes and Geometries as arguments.
The polygon Mesh inside a Geometry can be one of three things:
Often after you load a scene or model, you need to access a part of it as an individual Geometry in the scene graph. Maybe you want to swap a character's weapon, or you want to play a door-opening animation. First you need to know the unique name of the sub-mesh.
In the following example, the Node house
is the loaded model. The sub-meshes in the Node are called its children. The String, here door 12
, is the name of the mesh that you are searching.
Geometry submesh = (Geometry) houseScene.getChild("door 12");
+ +This is an introduction to the concept of Spatials, the elements of the 3D scene graph. The scene graph is a data structure that manages all objects in your 3D world. For example, the scene graph keeps track of the 3D models that you load and position. When you extend a Java class from com.jme3.app.SimpleApplication, you automatically inherit the scene graph and its rootNode. +
+ ++The rootNode is the central element of the scene graph. Even if the scenegraph is empty, it always has at least its rootNode. All other Spatials are attached to the rootNode in a parent-child relationship. If you think you need to understand the scene graph concept better, please read Scenegraph for dummies first. +
+ ++ +In your Java code, a Spatial is either a com.jme3.scene.Node or a com.jme3.scene.Geometry. You use the two for different purposes: +
+ ++ + +
+com.jme3.scene.Spatial | +||
---|---|---|
Purpose: | A Spatial is an abstract data structure that stores transformations (translation, rotation, scale) of elements of the scene graph. Spatials can be saved and loaded using the AssetManager. | +|
com.jme3.scene.Geometry | com.jme3.scene.Node | +|
Visibility: | A Geometry represents a visible 3-D object in the scene graph. | A Node is an invisible "handle" for a group of objects in the scene graph. | +
Purpose: | Use Geometries to represent an object's looks: Every Geometry contains a polygon mesh and a material, specifying its shape, color, texture, and opacity/transparency. +You can attach a Geometry to a Node. | Use Nodes to structure and group Geometries and other Nodes. Every Node is attached to one parent node, and each node can have zero or more children attached to itself. When you transform a parent node, all its children are transformed as well. | +
Content: | Transformations; custom user data; +mesh, material; | Transformations; custom user data; +no mesh, no material. |
+
Examples: | A box, a sphere, player, a building, a piece of terrain, a vehicle, missiles, NPCs, etc… | The rootNode, the guiNode, an audio node, a custom grouping node, etc… | +
+ +
Spatial s = new Spatial();
! A Spatial is an abstract concept, like a mammal (there is no actual creature called "mammal" walking around here). You create a Node, or load a Geometry object. Some methods however require a Spatial argement: This is because they are able to accept both Nodes and Geometries as arguments. In this case, you must cast a Node or Geometry to Spatial.
++ +The polygon Mesh inside a Geometry can be one of three things: + +
++ +You can include custom Java objects in Nodes and Geometries. This is useful for maintaining information about a game element, such as health, budget, ammunition, inventory, equipment, etc for players, or landmark locations for terrains, and much more. Where ever the spatial is accessible, you can also access the object's game data. +
+// create and instance of your custom data class +PlayerData playerData = new PlayerData("joe", 0, 100); +// store custom data in Node or Geometry +player.setUserData("player data", playerData); +... +// Elsewhere: retrieved data from Node or Geometry... +PlayerData playerData = player.getUserData("player data"); +// ... set the data... +playerData.setHealth("99"); +// ... or get the data for tests or to display it in the HUD. +health = playerData.getHealth();+ +
+You can add as many data objects to a Spatial as you need. Just make sure to label them with different Strings (player data
, player inventory
, player equipment
, etc).
+
+You can also list all data keys that are defined for one Spatial: +
+for(String key : geom.getUserDataKeys()){ + System.out.println(geom.getName()+"'s keys: "+key); +}+ +
+ +Often after you load a scene or model, you need to access a part of it as an individual Geometry in the scene graph. Maybe you want to swap a character's weapon, or you want to play a door-opening animation. First you need to know the unique name of the sub-mesh. + +
+
+
+In the following example, the Node house
is the loaded model. The sub-meshes in the Node are called its children. The String, here door 12
, is the name of the mesh that you are searching.
+
Geometry submesh = (Geometry) houseScene.getChild("door 12");+ +
3D games are typically played full-screen, or in a window that takes over the mouse and all inputs. However it is also possible to embed a jME 3 canvas in a standard Swing application.
This can be useful when you create some sort of interactive 3D viewer with a user interface that is more complex than just a HUD: For instance an interactive scientific demo, a level editor, or a game character designer.
Here is the full TestCanvas.java code sample.
You start out just the same as for any jME3 game: The base application, here SwingCanvasTest, extends com.jme3.app.SimpleApplication
. As usual, you use simpleInitApp()
to initialize the scene, and simpleUpdate()
as event loop.
The camera's default behaviour in SimpleApplication is to capture the mouse, which doesn't make sense in a Swing window. You have to deactivate and replace this behaviour by flyCam.setDragToRotate(true);
when you initialize the application:
public void simpleInitApp() { + ++ +JME3 Canvas in a Swing GUI
++ ++ ++3D games are typically played full-screen, or in a window that takes over the mouse and all inputs. However it is also possible to embed a jME 3 canvas in a standard Swing application.
+
+
+ +This can be useful when you create some sort of interactive 3D viewer with a user interface that is more complex than just a HUD: For instance an interactive scientific demo, a level editor, or a game character designer.
+
+ ++
+ +- +
Advantages:++
+- +
You can use Swing components (frame, panels, menus, controls) next to your jME3 game.+- +
The NetBeans GUI builder is compatible with the jMonkeyEngine; you can use it it to lay out the Swing GUI frame, and then add() the jME canvas into it. Install the GUI builder via Tools → Plugins → Available Plugins.+- +
Disadvantages:++
+- +
You cannot use SimpleApplication's default mouse capturing for camera navigation, but have to come up with a custom solution.++Here is the full code sample. + +
+ +Extending SimpleApplication
++ ++You start out just the same as for any jME3 game: The base application, here SwingCanvasTest, extends
+com.jme3.app.SimpleApplication
. As usual, you usesimpleInitApp()
to initialize the scene, andsimpleUpdate()
as event loop.
+
+ +The camera's default behaviour in SimpleApplication is to capture the mouse, which doesn't make sense in a Swing window. You have to deactivate and replace this behaviour byflyCam.setDragToRotate(true);
when you initialize the application: + +public void simpleInitApp() { // activate windowed input behaviour flyCam.setDragToRotate(true); // Set up inputs and load your scene as usual ... -}In short: The first thing that is different is the
main()
method. We don't call start() on the SwingCanvasTest object as usual. Instead we create a Runnable() that creates and opens a standard Swing jFrame. In the runnable, we also create our SwingCanvasTest game with special settings, create a Canvas for it, and add that to the jFrame. Then we call startCanvas().Main() and Runnable()
+ +The Swing isn't thread-safe and doesn't allow us to keep the jME3 canvas up-to-date. This is why we create a runnable for the jME canvas and queue it in the AWT event thread, so it can be invoked "later" in the loop, when Swing is ready with updating its own stuff.
In the SwingCanvasTest's main() method, create a queued runnable(). It will contain the jME canvas and the Swing frame.public static void main(String[] args) { +}+ ++ +In short: The first thing that is different is the
+ +main()
method. We don't call start() on the SwingCanvasTest object as usual. Instead we create a Runnable() that creates and opens a standard Swing jFrame. In the runnable, we also create our SwingCanvasTest game with special settings, create a Canvas for it, and add that to the jFrame. Then we call startCanvas(). + +Main() and Runnable()
++ ++The Swing isn't thread-safe and doesn't allow us to keep the jME3 canvas up-to-date. This is why we create a runnable for the jME canvas and queue it in the AWT event thread, so it can be invoked "later" in the loop, when Swing is ready with updating its own stuff.
+
+
+ +In the SwingCanvasTest's main() method, create a queued runnable(). It will contain the jME canvas and the Swing frame. + +public static void main(String[] args) { java.awt.Runnable() { public void run() { // ... see below ... } }); - }Creating the Canvas
+ +Here in the
run()
method, we start the jME application, create its canvas, create a Swing frame, and add everything together.
Specify the com.jme3.system.AppSettings so jME knows the size of the Swing panel that we put it into. The application will not ask the user for display settings, you have to specify them in advance.AppSettings settings = new AppSettings(true); + }+ +Creating the Canvas
++ ++Here in the
+run()
method, we start the jME application, create its canvas, create a Swing frame, and add everything together.
+
+ +Specify the com.jme3.system.AppSettings so jME knows the size of the Swing panel that we put it into. The application will not ask the user for display settings, you have to specify them in advance. + +AppSettings settings = new AppSettings(true); settings.setWidth(640); -settings.setHeight(480);We create our canvas application SwingCanvasTest, and give it the settings. We manually create a canvas for this game and configure the com.jme3.system.JmeCanvasContext. The method setSystemListener() makes sure that the listener receives events relating to context creation, update, and destroy.
SwingCanvasTest canvasApplication = new SwingCanvasTest(); +settings.setHeight(480);+ ++ +We create our canvas application SwingCanvasTest, and give it the settings. We manually create a canvas for this game and configure the com.jme3.system.JmeCanvasContext. The method setSystemListener() makes sure that the listener receives events relating to context creation, update, and destroy. + +
+SwingCanvasTest canvasApplication = new SwingCanvasTest(); canvasApplication.setSettings(settings); canvasApplication.createCanvas(); // create canvas! JmeCanvasContext ctx = (JmeCanvasContext) canvasApplication.getContext(); ctx.setSystemListener(canvasApplication); Dimension(640, 480); -ctx.getCanvas().setPreferredSize(dim);Note that we have not called start() on the application, as we would usually do in the main() method. We will call startCanvas() later instead.
Creating the Swing Frame
+ +Inside the run() method, you create the Swing window as you would usually do. Create an empty jFrame and add() components to it, or create a custom jFrame object in another class file (for example, by using the NetBeans GUI builder) and create an instance of it here. -Which ever you do, let's call the jFrame
window
.JFrame("Swing Application"); -window.setDefaultCloseOperation(FlowLayout()); // a panel +ctx.getCanvas().setPreferredSize(dim);+ ++ +Note that we have not called start() on the application, as we would usually do in the main() method. We will call startCanvas() later instead. + +
+ +Creating the Swing Frame
++ ++Inside the run() method, you create the Swing window as you would usually do. Create an empty jFrame and add() components to it, or create a custom jFrame object in another class file (for example, by using the NetBeans GUI builder) and create an instance of it here. +Which ever you do, let's call the jFrame
+window
. + +JFrame("Swing Application"); +window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);+ ++ +We create a standard JPanel inside the JFrame. Give it any Layout you wish – here we use a simple Flow Layout. Where the code sample says "Some Swing Component", this is where you add your buttons and controls.
+
+
+ +The important step is to add() the canvas component into the panel, like all the other Swing components. + +FlowLayout()); // a panel // add all your Swing components ... panel.add(new JButton("Some Swing Component")); ... // add the JME canvas -panel.add(ctx.getCanvas());OK, the jFrame and the panel are ready. We add the panel into the jFrame, and pack everything together. Set the window's visibility to true make it appear.
window.add(panel); +panel.add(ctx.getCanvas());+ ++ +OK, the jFrame and the panel are ready. We add the panel into the jFrame, and pack everything together. Set the window's visibility to true make it appear. + +
+window.add(panel); window.pack(); -window.setVisible(true);Remember that we haven't called start() on the jME appliation yet? For the canvas, there is a special
startCanvas()
method that you must call now:canvasApplication.startCanvas();Clean, build, and run!
Navigation
- \ No newline at end of file +window.setVisible(true);Remember, to navigate in the scene, click and drag (!) the mouse, or press the WASD keys. Depending on your game you may even want to define custom inputs to handle navigation in this untypical environment.
+
+Remember that we haven't called start() on the jME appliation yet? For the canvas, there is a special startCanvas()
method that you must call now:
+
+
canvasApplication.startCanvas();+ +
+ +Clean, build, and run! + +
+ ++Remember, to navigate in the scene, click and drag (!) the mouse, or press the WASD keys. Depending on your game you may even want to define custom inputs to handle navigation in this untypical environment. + +
+The goal of TerraMonkey is to provide a base implementation that will be usable for 80% of people's goals, while providing tools and a good foundation for the other 20% to build off of.
TerraMonkey is a GeoMipMapping quad tree of terrain tiles that supports real time editing and texture splatting. That's a mouth full! Lets look at each part:
You have seen GeoMipMapping implemented in games before. This is where the farther away terrain has fewer polygons, and as you move closer, more polygons fill in. The whole terrain is divided into a grid of patches, and each one has its own LOD. The GeoMipMapping algorithm will look at each patch, and its neighbours, to determine how to render the geometry. It will seam the edges between two patches with different LOD. + +
+The goal of TerraMonkey is to provide a base implementation that will be usable for 80% of people's goals, while providing tools and a good foundation for the other 20% to build off of. Check out the videos in the following announcements: +
++TerraMonkey is a GeoMipMapping quad tree of terrain tiles that supports real time editing and texture splatting. That's a mouth full! Lets look at each part: +
++You have seen GeoMipMapping implemented in games before. This is where the farther away terrain has fewer polygons, and as you move closer, more polygons fill in. The whole terrain is divided into a grid of patches, and each one has its own LOD. The GeoMipMapping algorithm will look at each patch, and its neighbours, to determine how to render the geometry. It will seam the edges between two patches with different LOD. This often leads to "popping" where you see the terrain switch from one LOD to another. TerraMonkey has been designed so you can swap out different LOD calculation algorithms based on what will look best for your game. You can do this with the LodCalculator interface. GeoMipMapping in TerraMonkey has been split into several parts: the terrain quad tree, and the LODGeomap. The geomap deals with the actual LOD and seaming algorithm. So if you want a different data structure for your terrain system, you can re-use this piece of code. The quad tree (TerrainQuad and TerrainPatch) provide a means to organize the LODGeomaps, notify them of their neighbour's LOD change, and to update the geometry when the LOD does change. -To change the LOD it does this by changing the index buffer of the triangle strip, so the whole geometry doesn't have to be re-loaded onto the video card.
If you are eager, you can read up more detail how GeoMipMapping works here: www.flipcode.com/archives/article_geomipmaps.pdf
TerraMonkey is a quad tree. Each node is a TerrainQuad, and each leaf is a TerrainPatch. A TerrainQuad has either 4 child TerrainQuads, or 4 child TerrainPatches. The TerrainPatch holds the actual mesh geometry. This structure is almost exactly the same as JME2's TerrainPage system. Except now each leaf has a reference to its neighbours, so it doesn't ever have to traverse the tree to get them.
The default material for TerraMonkey is Terrain.j3md. This material combines an alphamap with several textures to produce the final texture. +To change the LOD it does this by changing the index buffer of the triangle strip, so the whole geometry doesn't have to be re-loaded onto the video card. +If you are eager, you can read up more detail how GeoMipMapping works here: + +
+ ++TerraMonkey is a quad tree. Each node is a TerrainQuad, and each leaf is a TerrainPatch. A TerrainQuad has either 4 child TerrainQuads, or 4 child TerrainPatches. The TerrainPatch holds the actual mesh geometry. This structure is almost exactly the same as JME2's TerrainPage system. Except now each leaf has a reference to its neighbours, so it doesn't ever have to traverse the tree to get them. + +
+ ++The default material for TerraMonkey is Terrain.j3md. This material combines an alphamap with several textures to produce the final texture. Right now there is support for only 3 textures and an alpha map. This is in place until we finish the terrain editor in JMP, and then the texture support will be 16 textures. -It is only 3 right now so you can hand-paint them in a drawing program, like Photoshop, setting each splat texture in either Red, Green, or Blue. The test case has an example texture to show you how this works.
Along with getting more splat texture support, we will be adding in lighting and normal mapping support. The normal mapping isn't fully planned out yet. We need to decide how we are going to handle a normal map for each texture that is passed in. That could generate some very odd effects. -Thoughts, ideas, and recommendations are appreciated!
First, we load our textures and the heightmap texture for the terrain
// Create material from Terrain Material Definition +It is only 3 right now so you can hand-paint them in a drawing program, like Photoshop, setting each splat texture in either Red, Green, or Blue. The test case has an example texture to show you how this works. +Along with getting more splat texture support, we will be adding in lighting and normal mapping support. The normal mapping isn't fully planned out yet. We need to decide how we are going to handle a normal map for each texture that is passed in. That could generate some very odd effects. +Thoughts, ideas, and recommendations are appreciated! + + + +
+First, we load our textures and the heightmap texture for the terrain + +
+// Create material from Terrain Material Definition matRock = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md"); // Load alpha map (for splat textures) matRock.setTexture("m_Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png")); // load heightmap image (for the terrain heightmap) Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); - // load grass texture Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg"); grass.setWrap(WrapMode.Repeat); matRock.setTexture("m_Tex1", grass); matRock.setFloat("m_Tex1Scale", 64f); - // load dirt texture Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"); dirt.setWrap(WrapMode.Repeat); matRock.setTexture("m_Tex2", dirt); matRock.setFloat("m_Tex2Scale", 32f); - // load rock texture Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg"); rock.setWrap(WrapMode.Repeat); matRock.setTexture("m_Tex3", rock); -matRock.setFloat("m_Tex3Scale", 128f);
We create the heightmap from the heightMapImage
.
AbstractHeightMap heightmap = null; +matRock.setFloat("m_Tex3Scale", 128f);+ +
+
+We create the heightmap from the heightMapImage
.
+
+
AbstractHeightMap heightmap = null; heightmap = new ImageBasedHeightMap( ImageToAwt.convert(heightMapImage.getImage(), false, true, 0), 1f); -heightmap.load();
Next we create the actual terrain.
terrain = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap()); +heightmap.load();+ +
+ +Next we create the actual terrain. +
+terrain = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap()); terrain.setMaterial(matRock); terrain.setModelBound(new BoundingBox()); terrain.updateModelBound(); terrain.setLocalScale(2f, 1f, 2f); // scale to make it less steep - List<Camera> cameras = new ArrayList<Camera>(); cameras.add(getCamera()); TerrainLodControl control = new TerrainLodControl(terrain, cameras); terrain.addControl(control); - -rootNode.attachChild(terrain);
PS: As an alternative to an image-based height map, you can also generate a Hill hightmap:
heightmap = new HillHeightMap(1025, 1000, 50, 100, (byte) 3);
+ +PS: As an alternative to an image-based height map, you can also generate a Hill hightmap: + +
+heightmap = new HillHeightMap(1025, 1000, 50, 100, (byte) 3);+ +
+ +This tutorial expands the HelloTerrain tutorial and makes the terrain solid. You combine what you learned in Hello Terrain and Hello Collision and add a CollisionShape to the terrain. The terrain's CollisionShape lets the first-person player (who is also a CollisionShape) collide with the terrain, i.e. walk on it and stand on it. +
+ +package jme3test.helloworld; + +import com.jme3.app.SimpleApplication; +import com.jme3.bullet.BulletAppState; +import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; +import com.jme3.bullet.collision.shapes.CollisionShape; +import com.jme3.bullet.control.CharacterControl; +import com.jme3.bullet.control.RigidBodyControl; +import com.jme3.bullet.util.CollisionShapeFactory; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.material.Material; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.scene.Node; +import com.jme3.terrain.geomipmap.TerrainLodControl; +import com.jme3.terrain.heightmap.AbstractHeightMap; +import com.jme3.terrain.geomipmap.TerrainQuad; +import com.jme3.terrain.heightmap.ImageBasedHeightMap; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture.WrapMode; +import java.util.ArrayList; +import java.util.List; +import jme3tools.converters.ImageToAwt; + +/** + * This demo shows a terrain with collision detection, + * that you can walk around in with a first-person perspective. + * This code combines HelloCollision and HelloTerrain. + */ +public class HelloTerrainCollision extends SimpleApplication + implements ActionListener { + + private BulletAppState bulletAppState; + private RigidBodyControl landscape; + private CharacterControl player; + private Vector3f walkDirection = new Vector3f(); + private boolean left = false, right = false, up = false, down = false; + private TerrainQuad terrain; + private Material mat_terrain; + + public static void main(String[] args) { + HelloTerrainCollision app = new HelloTerrainCollision(); + app.start(); + } + + @Override + public void simpleInitApp() { + /** Set up Physics */ + bulletAppState = new BulletAppState(); + stateManager.attach(bulletAppState); + //bulletAppState.getPhysicsSpace().enableDebug(assetManager); + + flyCam.setMoveSpeed(100); + setUpKeys(); + + /** 1. Create terrain material and load four textures into it. */ + mat_terrain = new Material(assetManager, + "Common/MatDefs/Terrain/Terrain.j3md"); + + /** 1.1) Add ALPHA map (for red-blue-green coded splat textures) */ + mat_terrain.setTexture("Alpha", assetManager.loadTexture( + "Textures/Terrain/splat/alphamap.png")); + + /** 1.2) Add GRASS texture into the red layer (Tex1). */ + Texture grass = assetManager.loadTexture( + "Textures/Terrain/splat/grass.jpg"); + grass.setWrap(WrapMode.Repeat); + mat_terrain.setTexture("Tex1", grass); + mat_terrain.setFloat("Tex1Scale", 64f); + + /** 1.3) Add DIRT texture into the green layer (Tex2) */ + Texture dirt = assetManager.loadTexture( + "Textures/Terrain/splat/dirt.jpg"); + dirt.setWrap(WrapMode.Repeat); + mat_terrain.setTexture("Tex2", dirt); + mat_terrain.setFloat("Tex2Scale", 32f); + + /** 1.4) Add ROAD texture into the blue layer (Tex3) */ + Texture rock = assetManager.loadTexture( + "Textures/Terrain/splat/road.jpg"); + rock.setWrap(WrapMode.Repeat); + mat_terrain.setTexture("Tex3", rock); + mat_terrain.setFloat("Tex3Scale", 128f); + + /** 2. Create the height map */ + AbstractHeightMap heightmap = null; + Texture heightMapImage = assetManager.loadTexture( + "Textures/Terrain/splat/mountains512.png"); + heightmap = new ImageBasedHeightMap( + ImageToAwt.convert(heightMapImage.getImage(), false, true, 0)); + heightmap.load(); + + /** 3. We have prepared material and heightmap. + * Now we create the actual terrain: + * 3.1) Create a TerrainQuad and name it "my terrain". + * 3.2) A good value for terrain tiles is 64x64 -- so we supply 64+1=65. + * 3.3) We prepared a heightmap of size 512x512 -- so we supply 512+1=513. + * 3.4) As LOD step scale we supply Vector3f(1,1,1). + * 3.5) We supply the prepared heightmap itself. + */ + terrain = new TerrainQuad("my terrain", 65, 513, heightmap.getHeightMap()); + + /** 4. We give the terrain its material, position & scale it, and attach it. */ + terrain.setMaterial(mat_terrain); + terrain.setLocalTranslation(0, -100, 0); + terrain.setLocalScale(2f, 1f, 2f); + rootNode.attachChild(terrain); + + /** 5. The LOD (level of detail) depends on were the camera is: */ + List<Camera> cameras = new ArrayList<Camera>(); + cameras.add(getCamera()); + TerrainLodControl control = new TerrainLodControl(terrain, cameras); + terrain.addControl(control); + + /** 6. Add physics: */ + // We set up collision detection for the scene by creating a + // compound collision shape and a static RigidBodyControl with mass zero.*/ + CollisionShape terrainShape = + CollisionShapeFactory.createMeshShape((Node) terrain); + landscape = new RigidBodyControl(terrainShape, 0); + terrain.addControl(landscape); + + // We set up collision detection for the player by creating + // a capsule collision shape and a CharacterControl. + // The CharacterControl offers extra settings for + // size, stepheight, jumping, falling, and gravity. + // We also put the player in its starting position. + CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1.5f, 6f, 1); + player = new CharacterControl(capsuleShape, 0.05f); + player.setJumpSpeed(20); + player.setFallSpeed(30); + player.setGravity(30); + player.setPhysicsLocation(new Vector3f(0, 10, 0)); + + // We attach the scene and the player to the rootnode and the physics space, + // to make them appear in the game world. + bulletAppState.getPhysicsSpace().add(terrain); + bulletAppState.getPhysicsSpace().add(player); + + } + /** We over-write some navigational key mappings here, so we can + * add physics-controlled walking and jumping: */ + private void setUpKeys() { + inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A)); + inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D)); + inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_W)); + inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_S)); + inputManager.addMapping("Jump", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addListener(this, "Left"); + inputManager.addListener(this, "Right"); + inputManager.addListener(this, "Up"); + inputManager.addListener(this, "Down"); + inputManager.addListener(this, "Jump"); + } + + /** These are our custom actions triggered by key presses. + * We do not walk yet, we just keep track of the direction the user pressed. */ + public void onAction(String binding, boolean value, float tpf) { + if (binding.equals("Left")) { + if (value) { left = true; } else { left = false; } + } else if (binding.equals("Right")) { + if (value) { right = true; } else { right = false; } + } else if (binding.equals("Up")) { + if (value) { up = true; } else { up = false; } + } else if (binding.equals("Down")) { + if (value) { down = true; } else { down = false; } + } else if (binding.equals("Jump")) { + player.jump(); + } + } + + /** + * This is the main event loop--walking happens here. + * We check in which direction the player is walking by interpreting + * the camera direction forward (camDir) and to the side (camLeft). + * The setWalkDirection() command is what lets a physics-controlled player walk. + * We also make sure here that the camera moves with player. + */ + @Override + public void simpleUpdate(float tpf) { + Vector3f camDir = cam.getDirection().clone().multLocal(0.6f); + Vector3f camLeft = cam.getLeft().clone().multLocal(0.4f); + walkDirection.set(0, 0, 0); + if (left) { walkDirection.addLocal(camLeft); } + if (right) { walkDirection.addLocal(camLeft.negate()); } + if (up) { walkDirection.addLocal(camDir); } + if (down) { walkDirection.addLocal(camDir.negate()); } + player.setWalkDirection(walkDirection); + cam.setLocation(player.getPhysicsLocation()); + } +}+ +
+To try this code, create a New Project → JME3 → BasicGame using the default settings. Paste the sample code over the pregenerated Main.java class. Chnage the package to "mygame" if necessary. Open the Project Properties, Libraries, and add the jme3-test-data
library to make certain you have all the files.
+
+Compile and run the code. You should see a terrain. You can use the WASD keys and the mouse to run up and down the hills. +
+ ++ +Read Hello Terrain for details of the following parts that we reuse: +
+AbstractHeightMap
is an efficient way to describe the shape of the terrain.Terrain.j3md
-based Material and its texture layers let you colorize rocky mountain, grassy valleys, and a paved path criss-crossing over the landscape. terrain
Spatial that you attach to the rootNode.+Read Hello Collision for details of the following parts that we reuse: +
+BulletAppState
lines activate physics.ActionListener
(onAction()
) lets you reconfigure the input handling for the first-person player, so it takes collision detection into account.setUpKeys()
method loads your reconfigured input handlers. They now don't just walk blindly, but calculate the walkDirection
vector that we need for collision detection.simpleUpdate()
uses the walkDirection
vector and makes the character walk, while taking obstacles and solid walls/floor into account. player.setWalkDirection(walkDirection);+
landscape
is the CollisionShape of the terrain.+ +Here are the changed parts to combine the two: + +
+terrainShape
for the terrain
node.terrainShape
, you create a static (zero-mass) RigidBodyControl landscape
. landscape
control to the terrain
to make it physical./** 6. Add physics: */ + CollisionShape terrainShape = + CollisionShapeFactory.createMeshShape((Node) terrain); + landscape = new RigidBodyControl(terrainShape, 0); + terrain.addControl(landscape);+ +
+You attach the terrain
and the first-person player
to the rootNode, and to the physics space, to make them appear in the game world.
+
bulletAppState.getPhysicsSpace().add(terrain); + bulletAppState.getPhysicsSpace().add(player);+ +
+ +You see that you can combine snippets of sample code (such as HelloTerrain and HelloCollision), and create a new application from it that combines two features into soemthing new. +
+ ++You should spawn high up in the area and fall down to the map, giving you a few seconds to survey the area. Then walk around and see how you like the lay of the land. +
++See also: + +
+Extending your application from com.jme3.app.SimpleApplication provides you with an update loop. This is where you implement your game logic (game mechanics).
Examples: Here you remote-control NPCs (computer controlled characters), generate game events, and respond to user input.
simpleInit()
)simpleUpdate()
method)simpleRender()
method)Use…
simpleUpdate()
to implement the rest, or for testing during development.+ +Extending your application from com.jme3.app.SimpleApplication provides you with an update loop. This is where you implement your game logic (game mechanics). +
+ ++Examples: Here you remote-control NPCs (computer controlled characters), generate game events, and respond to user input. + +
+simpleInitApp()
methodsimpleUpdate()
methodsimpleRender()
method+ +There are two strategies how advanced developers can spread out their init and update code over several Java objects: +
+For physical vehicles, jME's uses the jBullet ray-cast vehicle. In this vehicle implementation, the physical chassis 'floats' along on four non-physical vertical rays.
Internally, each wheel casts a ray down, and using the ray's intersection point, jBullet calculates the suspension length, and the suspension force. The suspension force is applied to the chassis, keeping it from hitting the ground. The friction force is calculated for each wheel where the ray intersects with the ground. Friction is applied as a sideways and forwards force. 1)
This article shows how you use this vehicle implementation in a jME3 application.
Full code samples are here:
The goal is to create a physical vehicle with wheels that can be steered and that interacts (collides with) with the floor and obstacles.
vehicle.addWheel()
).vehicle.steer()
vehicle.accelerate()
vehicle.brake()
The vehicle that we create here in the TestPhysicsCar.java example is just a "box on wheels", a basic vehicle shape that you can replace with a fancy car model, as demonstrated in TestFancyCar.java.
Every physical object must have a collision shape, that we prepare first. For the vehicle, we choose a compound collision shape that is made up of a box-shaped body of the right size for the vehicle. We will add the wheels later.
CompoundCollisionShape compoundShape = new CompoundCollisionShape(); -BoxCollisionShape box = new BoxCollisionShape(new Vector3f(1.2f, 0.5f, 2.4f));
Best Practice: We attach the BoxCollisionShape (the vehicle body) to the CompoundCollisionShape at a Vector of (0,1,0): This shifts the effective center of mass of the BoxCollisionShape downwards to 0,-1,0 and makes a moving vehicle more stable!
compoundShape.addChildShape(box, new Vector3f(0, 1, 0));
Any kind of geometry can make up the visible part of the vehicle, here we use a wireframe box. We create a node that we use to group the geometry.
Node vehicleNode=new Node("vehicleNode"); - vehicle = new VehicleControl(compoundShape, 400); - vehicleNode.addControl(vehicle);
We initialize the Vehicle Control with the compound shape, and set its mass to a heavy value, 400f. The Vehicle Control represents the car's physical behaviour.
vehicle = new VehicleControl(compoundShape, 400);
Finally we add the behaviour (VehicleControl) to the visible Geometry (node).
vehicleNode.addControl(vehicle);
We configure the physical properties of the vehicle's suspension: Compresion, Damping, Stiffness, and MaxSuspenionForce. Picking workable values for the wheel suspension can be tricky – for background info have a look at these Suspension Settings Tips. For now, let's work with the following values:
float stiffness = 60.0f;//200=f1 car - float compValue = .3f; //(should be lower than damp) - float dampValue = .4f; - vehicle.setSuspensionCompression(compValue * 2.0f * FastMath.sqrt(stiffness)); - vehicle.setSuspensionDamping(dampValue * 2.0f * FastMath.sqrt(stiffness)); - vehicle.setSuspensionStiffness(stiffness); - vehicle.setMaxSuspensionForce(10000.0f);
We now have a node vehicleNode
with a visible "car" geometry, which acts like a vehicle. One thing that's missing are wheels.
We create four wheel Geometries and add them to the vehicle. Our wheel geometries are simple, non-physical discs (flat Cylinders), they are just visual decorations. Note that the physical wheel behaviour (the com.jme3.bullet.objects.VehicleWheel objects) is created internally by the vehicle.addWheel()
method.
The addWheel()
method sets following properties:
We initialize a few variables that we will reuse when we add the four wheels. yOff, etc, are the particular wheel offsets for our small vehicle model.
Vector3f wheelDirection = new Vector3f(0, -1, 0); + ++ +Controlling a Physical Vehicle
++ ++ ++ +For physical vehicles, jME's uses the jBullet ray-cast vehicle. In this vehicle implementation, the physical chassis 'floats' along on four non-physical vertical rays. +
+ ++Internally, each wheel casts a ray down, and using the ray's intersection point, jBullet calculates the suspension length, and the suspension force. The suspension force is applied to the chassis, keeping it from hitting the ground. The friction force is calculated for each wheel where the ray intersects with the ground. Friction is applied as a sideways and forwards force. 1) +
+ ++This article shows how you use this vehicle implementation in a jME3 application. +
+ ++ +
+ +Sample Code
++ ++ ++ +Full code samples are here: + +
++
+ +- +
+- +
+Overview of this Physics Application
++ ++ ++ +The goal is to create a physical vehicle with wheels that can be steered and that interacts (collides with) with the floor and obstacles. + +
++
+ +- +
Create a SimpleApplication with a BulletAppState++
+- +
This gives us a PhysicsSpace for PhysicsNodes+- +
Create a VehicleControl + CompoundCollisionShape for the physical vehicle behaviour++
+- +
Set physical properties of the vehicle, such as suspension.+- +
Create a VehicleNode for the car model++
+- +
Create a box plus 4 cylinders as wheels (using+vehicle.addWheel()
).- +
Add the VehicleControl behaviour to the VehicleNode geometry.+- +
Create a RigidBodyControl and CollisionShape for the floor+- +
Map key triggers and add input listeners++
+- +
Navigational commands Left, Right, Foward, Brake.+- +
Define the steering actions to be triggered by the key events.++
+- +
+vehicle.steer()
- +
+vehicle.accelerate()
- +
+vehicle.brake()
Creating the Vehicle Chassis
++ ++ ++ +The vehicle that we create here in the example is just a "box on wheels", a basic vehicle shape that you can replace with a fancy car model, as demonstrated in . +
+ ++Every physical object must have a collision shape, that we prepare first. For the vehicle, we choose a compound collision shape that is made up of a box-shaped body of the right size for the vehicle. We will add the wheels later. +
+CompoundCollisionShape compoundShape = new CompoundCollisionShape(); +BoxCollisionShape box = new BoxCollisionShape(new Vector3f(1.2f, 0.5f, 2.4f));+ ++Best Practice: We attach the BoxCollisionShape (the vehicle body) to the CompoundCollisionShape at a Vector of (0,1,0): This shifts the effective center of mass of the BoxCollisionShape downwards to 0,-1,0 and makes a moving vehicle more stable! +
+compoundShape.addChildShape(box, new Vector3f(0, 1, 0));+ ++Any kind of geometry can make up the visible part of the vehicle, here we use a wireframe box. We create a node that we use to group the geometry. +
+Node vehicleNode=new Node("vehicleNode"); +vehicle = new VehicleControl(compoundShape, 400); +vehicleNode.addControl(vehicle);+ ++We initialize the Vehicle Control with the compound shape, and set its mass to a heavy value, 400f. The Vehicle Control represents the car's physical behaviour. +
+vehicle = new VehicleControl(compoundShape, 400);+ ++Finally we add the behaviour (VehicleControl) to the visible Geometry (node). +
+vehicleNode.addControl(vehicle);+ ++We configure the physical properties of the vehicle's suspension: Compresion, Damping, Stiffness, and MaxSuspenionForce. Picking workable values for the wheel suspension can be tricky – for background info have a look at these . For now, let's work with the following values: +
+float stiffness = 60.0f;//200=f1 car +float compValue = .3f; //(should be lower than damp) +float dampValue = .4f; +vehicle.setSuspensionCompression(compValue * 2.0f * FastMath.sqrt(stiffness)); +vehicle.setSuspensionDamping(dampValue * 2.0f * FastMath.sqrt(stiffness)); +vehicle.setSuspensionStiffness(stiffness); +vehicle.setMaxSuspensionForce(10000.0f);+ ++We now have a node
+ +vehicleNode
with a visible "car" geometry, which acts like a vehicle. One thing that's missing are wheels. +Adding the Wheels
++ ++ +We create four wheel Geometries and add them to the vehicle. Our wheel geometries are simple, non-physical discs (flat Cylinders), they are just visual decorations. Note that the physical wheel behaviour (the com.jme3.bullet.objects.VehicleWheel objects) is created internally by the
+ +vehicle.addWheel()
method. ++The
+addWheel()
method sets following properties: ++
+ +- +
Vector3f connectionPoint – Coordinate where the suspension connects to the chassis (internally, this is where the Ray is casted downwards).+- +
Vector3f direction – Wheel direction is typically a (0,-1,0) vector.+- +
Vector3f axle – Axle direction is typically a (-1,0,0) vector.+- +
float suspensionRestLength – Suspension rest length in world units+- +
float wheelRadius – Wheel radius in world units+- +
boolean isFrontWheel – Whether this wheel is one of the steering wheels.+
+Front wheels are the ones that rotate visibly when the vehicle turns.+ +We initialize a few variables that we will reuse when we add the four wheels. yOff, etc, are the particular wheel offsets for our small vehicle model. +
+Vector3f wheelDirection = new Vector3f(0, -1, 0); Vector3f wheelAxle = new Vector3f(-1, 0, 0); float radius = 0.5f; float restLength = 0.3f; float yOff = 0.5f; float xOff = 1f; -float zOff = 2f;We create a Cylinder mesh shape that we use to create the four visible wheel geometries.
Cylinder wheelMesh = new Cylinder(16, 16, radius, radius * 0.6f, true);For each wheel, we create a Node and a Geometry. We attach the Cylinder Geometry to the Node. We rotate the wheel by 90° around the Y axis. We set a material to make it visible. Finally we add the wheel (plus its properties) to the vehicle.
Node node1 = new Node("wheel 1 node"); +float zOff = 2f;+ ++We create a Cylinder mesh shape that we use to create the four visible wheel geometries. +
+Cylinder wheelMesh = new Cylinder(16, 16, radius, radius * 0.6f, true);+ ++For each wheel, we create a Node and a Geometry. We attach the Cylinder Geometry to the Node. We rotate the wheel by 90° around the Y axis. We set a material to make it visible. Finally we add the wheel (plus its properties) to the vehicle. +
+Node node1 = new Node("wheel 1 node"); Geometry wheels1 = new Geometry("wheel 1", wheelMesh); node1.attachChild(wheels1); wheels1.rotate(0, FastMath.HALF_PI, 0); wheels1.setMaterial(mat); vehicle.addWheel(node1, new Vector3f(-xOff, yOff, zOff), - wheelDirection, wheelAxle, restLength, radius, true);The three next wheels are created in the same fashion, only the offsets are different. Remember to set the Boolean parameter correctly to indicate whether it's a front wheel.
... + wheelDirection, wheelAxle, restLength, radius, true);+ ++The three next wheels are created in the same fashion, only the offsets are different. Remember to set the Boolean parameter correctly to indicate whether it's a front wheel. +
+... vehicle.addWheel(node2, new Vector3f(xOff, yOff, zOff), - wheelDirection, wheelAxle, restLength, radius, true); + wheelDirection, wheelAxle, restLength, radius, true); ... vehicle.addWheel(node3, new Vector3f(-xOff, yOff, -zOff), - wheelDirection, wheelAxle, restLength, radius, false); + wheelDirection, wheelAxle, restLength, radius, false); ... vehicle.addWheel(node4, new Vector3f(xOff, yOff, -zOff), - wheelDirection, wheelAxle, restLength, radius, false);Attach the wheel Nodes to the vehicle Node to group them, so they move together.
vehicleNode.attachChild(node1); + wheelDirection, wheelAxle, restLength, radius, false);+ ++Attach the wheel Nodes to the vehicle Node to group them, so they move together. +
+vehicleNode.attachChild(node1); vehicleNode.attachChild(node2); vehicleNode.attachChild(node3); -vehicleNode.attachChild(node4);As always, attach the vehicle Node to the rootNode to make it visible, and add the Vehicle Control to the PhysicsSpace to make the car physical.
rootNode.attachChild(vehicleNode); -getPhysicsSpace().add(vehicle);Not shown here is that we also created a Material
mat
.Steering the Vehicle
+ +Not shown here is the standard way how we map the input keys to actions (see full code sample). Also refer to Input Handling).
In the ActionListener, we implement the actions that control the vehicle's direction and speed. For the four directions (accelerate=up, brake=down, left, right), we specify how we want the vehicle to move.
The braking action is pretty straightforward:
vehicle.brake(brakeForce)
For left and right turns, we add a constant tosteeringValue
when the key is pressed, and subtract it when the key is released.
vehicle.steer(steeringValue);
For acceleration we add a constant toaccelerationValue
when the key is pressed, and substract it when the key is released.
vehicle.accelerate(accelerationValue);
Because we can and it's fun, we also add a turbo booster that makes the vehicle jump when you press the assigned key (spacebar).
vehicle.applyImpulse(jumpForce, Vector3f.ZERO);
public void onAction(String binding, boolean value, float tpf) { - if (binding.equals("Lefts")) { - if (value) { - steeringValue += .5f; - } else { - steeringValue += -.5f; - } - vehicle.steer(steeringValue); - } else if (binding.equals("Rights")) { - if (value) { - steeringValue += -.5f; - } else { - steeringValue += .5f; - } - vehicle.steer(steeringValue); - } else if (binding.equals("Ups")) { - if (value) { - accelerationValue += accelerationForce; - } else { - accelerationValue -= accelerationForce; - } - vehicle.accelerate(accelerationValue); - } else if (binding.equals("Downs")) { - if (value) { - vehicle.brake(brakeForce); - } else { - vehicle.brake(0f); - } - } else if (binding.equals("Space")) { - if (value) { - vehicle.applyImpulse(jumpForce, Vector3f.ZERO); - } - } else if (binding.equals("Reset")) { - if (value) { - System.out.println("Reset"); - vehicle.setPhysicsLocation(Vector3f.ZERO); - vehicle.setPhysicsRotation(new Matrix3f()); - vehicle.setLinearVelocity(Vector3f.ZERO); - vehicle.setAngularVelocity(Vector3f.ZERO); - vehicle.resetSuspension(); - } else { - } - } - }For your reference, this is how we initialized the constants for this example:
private final float accelerationForce = 1000.0f; +vehicleNode.attachChild(node4);+ ++As always, attach the vehicle Node to the rootNode to make it visible, and add the Vehicle Control to the PhysicsSpace to make the car physical. +
+rootNode.attachChild(vehicleNode); +getPhysicsSpace().add(vehicle);+ ++Not shown here is that we also created a Material
+ +mat
. +Steering the Vehicle
++ ++ +Not shown here is the standard way how we map the input keys to actions (see full code sample). Also refer to Input Handling). +
+ ++In the ActionListener, we implement the actions that control the vehicle's direction and speed. For the four directions (accelerate=up, brake=down, left, right), we specify how we want the vehicle to move. + +
++
+- +
The braking action is pretty straightforward:+
+vehicle.brake(brakeForce)
- +
For left and right turns, we add a constant to+steeringValue
when the key is pressed, and subtract it when the key is released.
+vehicle.steer(steeringValue);
- +
For acceleration we add a constant to+accelerationValue
when the key is pressed, and substract it when the key is released.
+vehicle.accelerate(accelerationValue);
- +
Because we can and it's fun, we also add a turbo booster that makes the vehicle jump when you press the assigned key (spacebar).+
+vehicle.applyImpulse(jumpForce, Vector3f.ZERO);
public void onAction(String binding, boolean value, float tpf) { + if (binding.equals("Lefts")) { + if (value) { steeringValue += .5f; } else { steeringValue += -.5f; } + vehicle.steer(steeringValue); + } else if (binding.equals("Rights")) { + if (value) { steeringValue += -.5f; } else { steeringValue += .5f; } + vehicle.steer(steeringValue); + } else if (binding.equals("Ups")) { + if (value) { + accelerationValue += accelerationForce; + } else { + accelerationValue -= accelerationForce; + } + vehicle.accelerate(accelerationValue); + } else if (binding.equals("Downs")) { + if (value) { vehicle.brake(brakeForce); } else { vehicle.brake(0f); } + } else if (binding.equals("Space")) { + if (value) { + vehicle.applyImpulse(jumpForce, Vector3f.ZERO); + } + } else if (binding.equals("Reset")) { + if (value) { + System.out.println("Reset"); + vehicle.setPhysicsLocation(Vector3f.ZERO); + vehicle.setPhysicsRotation(new Matrix3f()); + vehicle.setLinearVelocity(Vector3f.ZERO); + vehicle.setAngularVelocity(Vector3f.ZERO); + vehicle.resetSuspension(); + } else { + } + } +}+ ++For your reference, this is how we initialized the constants for this example: +
+private final float accelerationForce = 1000.0f; private final float brakeForce = 100.0f; private float steeringValue = 0; private float accelerationValue = 0; -private Vector3f jumpForce = new Vector3f(0, 3000, 0);Remember, the standard input listener code that maps the actions to keys can be found in the code samples.
Detecting Collisions
Read the Responding to a PhysicsCollisionEvent chapter in the general physics documentation on how to detect collisions. You would do this if you want to react to collisions with custom events, such as adding points or substracting health.
Best Practices
- \ No newline at end of file +private Vector3f jumpForce = new Vector3f(0, 3000, 0);This example shows a very simple but functional vehicle. For a game you would implement steering behaviour and acceleration with values that are typical for the type of vehicle that you want to simulate. Instead of a box, you load a chassis model. You can consider using an AnalogListener to respond to key events in a more sophisticated way.
For a more advanced example, look at TestFancyCar.java.
+Remember, the standard input listener code that maps the actions to keys can be found in the code samples. +
+ ++ +Read the Responding to a PhysicsCollisionEvent chapter in the general physics documentation on how to detect collisions. You would do this if you want to react to collisions with custom events, such as adding points or substracting health. +
+ ++ +This example shows a very simple but functional vehicle. For a game you would implement steering behaviour and acceleration with values that are typical for the type of vehicle that you want to simulate. Instead of a box, you load a chassis model. You can consider using an AnalogListener to respond to key events in a more sophisticated way. +
+ ++For a more advanced example, look at . +
+ +Work in progress.
In other code samples we have seen how to create collidable landscapes and walk around in a first-person perspective, by enclosing the camera with a collision shape.
Many games however require a third-person perspective of the character. If you load a character model, create a PhysicsControl for it, and use forces to push it around, you may not get the desired effect: Phyical objects often fall over when pushed, and that is not what you expect of a walking character.
This is why jME3 offers a special CharacterControl to implement walking characters.
The full code sample can be found here:
// initialze physical character behaviour, including collision shape -CapsuleCollisionShape capsule = new CapsuleCollisionShape(3f, 4f); -CharacterControl character = new CharacterControl(capsule, 0.01f); -// load the visible character model and add the physical behaviour to it -Node model = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); -model.addControl(character); -// Make character visible and physical -rootNode.attachChild(model); // make it visible -getPhysicsSpace().add(character); // make it physical
We create two AninChannels, for example one for walking, one for shooting. The shootingChannel only controls one arm, while the walking channels controls the whole animation.
AnimControl animationControl = model.getControl(AnimControl.class); -animationControl.addListener(this); - -AnimChannel animationChannel = animationControl.createChannel(); -AnimChannel shootingChannel = animationControl.createChannel(); - -shootingChannel.addBone(animationControl.getSkeleton().getBone("uparm.right")); -shootingChannel.addBone(animationControl.getSkeleton().getBone("arm.right")); -shootingChannel.addBone(animationControl.getSkeleton().getBone("hand.right"));
The extra shooting channel exists so the character can lift an arm to shoot and walk at the same time.
Work in progress (this is being updated for the new physics and chase cam.)
+In the code sample you have seen how to create collidable landscapes and walk around in a first-person perspective, where the camera is enclosed by a collision shape. Other games however require a third-person perspective of the character: In these cases you use a CharacterControl. This example uses a custom navigation – press WASD to walk and drag the mouse to rotate. + +
+ ++When you load a character model with a RigidBodyControl, and use forces to push it around, you do not get the desired effect: RigidBodyControl'ed objects can tip over when pushed, and that is not what you expect of a walking character. jMonkeyEngine offers a special CharacterControl with a special walking methods to implement characters that walk upright. + +
+ ++The several related code samples can be found here: +
++The code in this tutorial is a combination of them. + +
+ +public class WalkingCharacterDemo extends SimpleApplication + implements ActionListener, AnimEventListener { + public static void main(String[] args) { + WalkingCharacterDemo app = new WalkingCharacterDemo(); + app.start(); + } + public void simpleInitApp() { } + public void simpleUpdate(float tpf) { } + public void onAction(String name, boolean isPressed, float tpf) { } + public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { } + public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { }+ +
private BulletAppState bulletAppState; +... +public void simpleInitApp() { + bulletAppState = new BulletAppState(); + //bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL); + stateManager.attach(bulletAppState); + ... +}+ +
+In the simpleInitApp() method you initialize the scene and give it a MeshCollisionShape. The sample in the jme3 sources uses a custom helper class that simply creates a flat floor and drops some cubes and spheres on it: + +
+public void simpleInitApp() { + ... + PhysicsTestHelper.createPhysicsTestWorld(rootNode, + assetManager, bulletAppState.getPhysicsSpace()); + ...+ +
+ +In a real game, you would load a scene model here instead of a test world. You can load a model from a local or remote zip file, and scale and position it: + +
+private Node gameLevel; +.. +public void simpleInitApp() { + ... + //assetManager.registerLocator("quake3level.zip", ZipLocator.class.getName()); + assetManager.registerLocator( + "http://jmonkeyengine.googlecode.com/files/quake3level.zip", + HttpZipLocator.class.getName()); + MaterialList matList = (MaterialList) assetManager.loadAsset("Scene.material"); + OgreMeshKey key = new OgreMeshKey("main.meshxml", matList); + gameLevel = (Node) assetManager.loadAsset(key); + gameLevel.setLocalTranslation(-20, -16, 20); + gameLevel.setLocalScale(0.10f); + gameLevel.addControl(new RigidBodyControl(0)); + rootNode.attachChild(gameLevel); + bulletAppState.getPhysicsSpace().addAll(gameLevel); + ...+ +
+ +Also, add a light source to be able to see the scene. + +
+AmbientLight light = new AmbientLight(); + light.setColor(ColorRGBA.White.mult(2)); + rootNode.addLight(light);+ +
+You create an animated model, such as Oto.mesh.xml. +
+assets/Models/Oto/
directory of your project.private CharacterControl character; +private Node model; +... +public void simpleInitApp() { + ... + CapsuleCollisionShape capsule = new CapsuleCollisionShape(3f, 4f); + character = new CharacterControl(capsule, 0.05f); + character.setJumpSpeed(20f); + model = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + model.addControl(character); + bulletAppState.getPhysicsSpace().add(character); + rootNode.attachChild(model); + ...+ +
+
+
+
+Did you know? A CapsuleCollisionShape is a cylinder with rounded top and bottom. A capsule rotated upright is a good collision shape for a humanoid character since its roundedness reduces the risk of getting stuck on obstacles.
+
+
+Create several AnimChannels, one for each animation that can happen simultaneously. In this example, you create one channel for walking and one for attacking. (Because the character can attack with its arms and walk with the rest of the body at the same time.) + +
+private AnimChannel animationChannel; +private AnimChannel attackChannel; +private AnimControl animationControl; +... +public void simpleInitApp() { + ... + animationControl = model.getControl(AnimControl.class); + animationControl.addListener(this); + animationChannel = animationControl.createChannel(); + attackChannel = animationControl.createChannel(); + attackChannel.addBone(animationControl.getSkeleton().getBone("uparm.right")); + attackChannel.addBone(animationControl.getSkeleton().getBone("arm.right")); + attackChannel.addBone(animationControl.getSkeleton().getBone("hand.right")); + ...+ +
+ +The attackChannel only controls one arm, while the walking channels controls the whole character. + +
+ +private ChaseCamera chaseCam; +... +public void simpleInitApp() { + ... + flyCam.setEnabled(false); + chaseCam = new ChaseCamera(cam, model, inputManager); + ...+ +
+Configure custom key bindings for WASD keys that you will use to make the character walk. + +
+private boolean left = false, right = false, up = false, down = false; +... +public void simpleInitApp() { + ... + inputManager.addMapping("CharLeft", new KeyTrigger(KeyInput.KEY_A)); + inputManager.addMapping("CharRight", new KeyTrigger(KeyInput.KEY_D)); + inputManager.addMapping("CharForward", new KeyTrigger(KeyInput.KEY_W)); + inputManager.addMapping("CharBackward", new KeyTrigger(KeyInput.KEY_S)); + inputManager.addMapping("CharJump", new KeyTrigger(KeyInput.KEY_RETURN)); + inputManager.addMapping("CharAttack", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addListener(this, "CharLeft", "CharRight"); + inputManager.addListener(this, "CharForward", "CharBackward"); + inputManager.addListener(this, "CharJump", "CharAttack"); + ... +}+ +
+ +Respond to the key bindings by setting variables that track in which direction you will go. (No actual walking happens here yet) + +
+@Override +public void onAction(String binding, boolean value, float tpf) { + if (binding.equals("CharLeft")) { + if (value) left = true; + else left = false; + } else if (binding.equals("CharRight")) { + if (value) right = true; + else right = false; + } else if (binding.equals("CharForward")) { + if (value) up = true; + else up = false; + } else if (binding.equals("CharBackward")) { + if (value) down = true; + else down = false; + } else if (binding.equals("CharJump")) + character.jump(); + if (binding.equals("CharAttack")) + attack(); +}+ +
+ +The player can attack and walk at the same time. Attack() is a custom method that triggers an attack animation in the arms. Here you should also add custom code to play an effect and sound, and to determine whether the hit was successful. + +
+private void attack() { + attackChannel.setAnim("Dodge", 0.1f); + attackChannel.setLoopMode(LoopMode.DontLoop); +}+ +
+ +The update loop looks at the directional variables and moves the character accordingly. Since it's a physical character, we use setWalkDirection(). The variable airTime tracks how long the character is off the ground (e.g. when jumping or falling) and adjusts the walk and stand animations acccordingly. + +
+private Vector3f walkDirection = new Vector3f(0,0,0); +private float airTime = 0; +public void simpleUpdate(float tpf) { + Vector3f camDir = cam.getDirection().clone().multLocal(0.25f); + Vector3f camLeft = cam.getLeft().clone().multLocal(0.25f); + camDir.y = 0; + camLeft.y = 0; + walkDirection.set(0, 0, 0); + if (left) walkDirection.addLocal(camLeft); + if (right) walkDirection.addLocal(camLeft.negate()); + if (up) walkDirection.addLocal(camDir); + if (down) walkDirection.addLocal(camDir.negate()); + if (!character.onGround()) { + airTime = airTime + tpf; + } else { + airTime = 0; + } + if (walkDirection.length() == 0) { + if (!"stand".equals(animationChannel.getAnimationName())) { + animationChannel.setAnim("stand", 1f); + } + } else { + character.setViewDirection(walkDirection); + if (airTime > .3f) { + if (!"stand".equals(animationChannel.getAnimationName())) { + animationChannel.setAnim("stand"); + } + } else if (!"Walk".equals(animationChannel.getAnimationName())) { + animationChannel.setAnim("Walk", 0.7f); + } + } + character.setWalkDirection(walkDirection); +}+ +
+ +This method resets the walk animation. + +
+public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { + if (channel == attackChannel) channel.setAnim("stand"); +} +public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { }+ +
DRAFT
Here is some background info for JME3's frist water implementation, nicknamed SeaMonkey:
A JME3 scene with water uses a com.jme3.water.SimpleWaterProcessor
(which implements the SceneProcessor interface).
To achieve a water effect, JME3 uses shaders and a special material, Common/MatDefs/Water/SimpleWater.j3md
. The water surface is a quad, and we use normal map and dU/dV map texturing to simulate the waves.
mainScene
NodemainScene
Node to the rootNode
scene
Spatialscene
Spatialscene
Spatial to the mainScene
NodemainScene
NodewaterProcessor
mainScene
Spatial (!)viewPort
quad
water
Geometry from the Quadwater
Geometry to the rootNode
. (Not to the mainScene!) The sample code can be found in jme3/src/jme3test/water/TestSimpleWater.java
and jme3/src/jme3test/water/TestSceneWater.java
.
Here is the most important part of the code:
// we create a water processor + ++ +Simple Water
++ ++ ++ +Here is some background info for JME3's basic water implementation: +
++
+ +- +
+- +
+- +
++ + +
+ +SimpleWaterProcessor
++ ++ ++ +A JME3 scene with water can use a
+ +com.jme3.water.SimpleWaterProcessor
(which implements the SceneProcessor interface). ++To achieve a water effect, JME3 uses shaders and a special material,
+Common/MatDefs/Water/SimpleWater.j3md
. The water surface is a quad, and we use normal map and dU/dV map texturing to simulate the waves. + ++
+ +- +
Every frame, we render to three texture maps:++
+- +
For the water surface (reflection), we take a snapshot of the environment, flip it upside down, and clip it to the visible water surface. Note that we do not actually use a "water texture" color map: The "texture" of the water is solely a distorted reflection.+- +
For the "wavy" distortion (refraction), we use the derivative of a normal map, a dU/dV map.+- +
For the fogginess of water (depth) we use a depth map from the terrains z-buffer.+- +
In the shaders, we add all of the texture maps together.++
+- +
For the "bumpy" displacement of the waves, we use a normal map and a du/dv map that are shifted against each other over time to create the wave effect.+- +
For the light reflection vectors on the water surface, we use the Fresnel formula, together with normal vectors.+- +
We add specular lighting.+- +
(For the underwater caustics effect, we use splatted textures. – WIP/TODO)+Usage
++ ++ ++ + + +
++
+ +- +
Create a+mainScene
Node+
+- +
Attach the+mainScene
Node to therootNode
- +
Load your+scene
Spatial+
+- +
Add a light source to the+scene
Spatial- +
Attach the+scene
Spatial to themainScene
Node- +
Load your sky Geometry++
+- +
Attach the sky Geometry to the+mainScene
Node- +
Create the SimpleWaterProcessor+waterProcessor
+
+- +
Set the processor's ReflectionScene to the+mainScene
Spatial (!)- +
Set the processor's Plane to where you want your water surface to be+- +
Set the processor's WaterDepth, DistortionScale, and WaveSpeed+- +
Attach the processor to the+viewPort
- +
Create a Quad+quad
+
+- +
Set the quad's TextureCoordinates to specify the size of the waves+- +
Create a+water
Geometry from the Quad+
+- +
Set the water's translation and rotation (same Y value as Plane above!)+- +
Set the water's material to the processor's output material+- +
Attach the+water
Geometry to therootNode
. (Not to the mainScene!)Sample Code
++ ++ +The sample code can be found in
+ +jme3/src/jme3test/water/TestSimpleWater.java
andjme3/src/jme3test/water/TestSceneWater.java
. ++Here is the most important part of the code: +
+// we create a water processor SimpleWaterProcessor waterProcessor = new SimpleWaterProcessor(assetManager); waterProcessor.setReflectionScene(mainScene); @@ -98,9 +157,56 @@ water.setLocalRotation(new Quaternion().fromAngleAxis(-FastMath. water.setLocalTranslation(-200, -6, 250); water.setShadowMode(ShadowMode.Receive); water.setMaterial(waterProcessor.getMaterial()); -rootNode.attachChild(water);Settings
+ +You can lower the render size to gain higher performance:
waterProcessor.setRenderSize(128,128);The deeper the water, the more transparent. (?)
waterProcessor.setWaterDepth(40);A higher distortion scale makes bigger waves.
waterProcessor.setDistortionScale(0.05f);A lower wave speed makes calmer water.
waterProcessor.setWaveSpeed(0.05f);If your scene does not have a lightsource, you can set the light direction for the water:
waterProcessor.setLightDirection( new Vector3f(0.55f, -0.82f, 0.15f));Instead of creating a quad and specifying a plane, you can get a default waterplane from the processor:
Geometry waterPlane = waterProcessor.createWaterGeometry(10, 10); +rootNode.attachChild(water);+ +Settings
++ +- \ No newline at end of file +waterPlane.setMaterial(waterProcessor.getMaterial());+ +You can lower the render size to gain higher performance: + +
+waterProcessor.setRenderSize(128,128);+ ++The deeper the water, the more transparent. (?) + +
+waterProcessor.setWaterDepth(40);+ ++A higher distortion scale makes bigger waves. + +
+waterProcessor.setDistortionScale(0.05f);+ ++A lower wave speed makes calmer water. + +
+waterProcessor.setWaveSpeed(0.05f);+ ++If your scene does not have a lightsource, you can set the light direction for the water: + +
+waterProcessor.setLightDirection( new Vector3f(0.55f, -0.82f, 0.15f));+ ++Instead of creating a quad and specifying a plane, you can get a default waterplane from the processor: + +
+Geometry waterPlane = waterProcessor.createWaterGeometry(10, 10); waterPlane.setLocalTranslation(-5, 0, 5); -waterPlane.setMaterial(waterProcessor.getMaterial());You can offer a switch to set the water Material to a static texture – for users with slow PCs.
+You can offer a switch to set the water Material to a static texture – for users with slow PCs. + +
+ ++ +This is a draft of a feature that is work in progress. If you have questions or suggestions, please leave a comment on the ! +
+ ++ +Mobile deployment is a "one-click" option next to Desktop/WebStart/Applet deployment in the jMonkeyPlatform. +
+/opt/android-sdk
+ + +
+ ++The Android deployment option creates a separate sub-project for android and makes the main project and associated libraries available to the sub-project as libraries. The sub-project can be edited using NBAndroid (see below) or using Eclipse or any other IDE that supports standard android projects. Normally you do not need to edit the android project files. Exceptions are described further below. +
+ ++Activating the nbandroid plugin in the jMonkeyPlatform is optional, but recommended. You do not need the nbandroid plugin for Android support to work, however nbandroid will not interfere and will in fact allow you to edit the android source files and project more conveniently. To be able to edit, extend and code android-specific code in Android projects, install NBAndroid from the update center: +
++ +Restart the jMonkeyPlatform. +
+ ++ +Open your game project in the jMonkeyPlatform. +
++ +Optionally, download +
+ ++ +You can use the jMonkeyPlatform to save (theoretically) any jMonkeyEngine app as Android app. But the application has to be prepared for the fact that Android devices have a smaller screen resolution, touchscreens instead of mouse buttons, and (typically) no keyboards. + +
++ +Best Practice: Ideally, you write the core application code in a way that it checks for the environment it's being run on, and automatically adapts the device's limitations by switching off effects, changing input mechanisms etc. Learn how to read graphic card capabilites here. +
+ ++As described above, you should always try to design your application as platform independent as possible. If your game becomes a sudden hit on android, why not release it on Facebook as an applet as well? When your application is designed in a platform independent way, you don't have to do much more but check a checkbox to deploy your application as an applet. But what if you want to for example access the camera of an android device? Inevitably you will have to use android specific api to access the camera. +
+ ++Since the main project is not configured to access the android api directly, you have to install NBAndroid (see above) to be able to edit the created android project in the SDK. After installing, click the "open project" button and navigate to the "mobile" folder inside the main project folder (it should show up with an android "a" icon) and open it. +
+ ++ +Although you will use android specific api, using a camera is not exactly android specific and so you should try to design this part of the application as platform independent as possible as well. As an example, if you want to use the phones camera as an image input stream for a texture, you can create e.g. the AppState that manages the image and makes it available to the application inside the main project (no android code is needed). Then in the android part of the code you make a connection to the camera and update the image in the AppState. This also allows you to easily support cameras on other platforms in the same way or fallback to something else in case the platform doesn't support a camera. +
+ ++ +The SDK will later provide tools to adapt the material and other graphics settings of the Android deployment version automatically. + +
+Previous: Hello Material, -Next: Hello Picking This tutorial shows how to add an animation controller and channels, and how to respond to user input by triggering an animation in a loaded model.
package jme3test.helloworld; + ++ +JME 3 Tutorial (7) - Hello Animation
++ ++ ++Previous: Hello Material, +Next: Hello Picking +
+ ++This tutorial shows how to add an animation controller and channels, and how to respond to user input by triggering an animation in a loaded model. +
+ ++ +
+ +Sample Code
++package jme3test.helloworld; + import com.jme3.animation.AnimChannel; import com.jme3.animation.AnimControl; import com.jme3.animation.AnimEventListener; @@ -20,6 +33,7 @@ import com.jme3.light.DirectionalLight; import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.scene.Node; + /** Sample 7 - how to load an OgreXML model and play an animation, * using channels, a controller, and an AnimEventListener. */ public class HelloAnimation extends SimpleApplication @@ -31,6 +45,7 @@ public class HelloAnimation extends SimpleApplication HelloAnimation app = new HelloAnimation(); app.start(); } + @Override public void simpleInitApp() { viewPort.setBackgroundColor(ColorRGBA.LightGray); @@ -46,6 +61,7 @@ public class HelloAnimation extends SimpleApplication channel = control.createChannel(); channel.setAnim("stand"); } + public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { if (animName.equals("Walk")) { channel.setAnim("stand", 0.50f); @@ -53,9 +69,11 @@ public class HelloAnimation extends SimpleApplication channel.setSpeed(1f); } } + public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { // unused } + /** Custom Keybinding: Map named actions to inputs. */ private void initKeys() { inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE)); @@ -71,34 +89,82 @@ public class HelloAnimation extends SimpleApplication } } }; -}
Creating and Loading Animated Models
+ +You create animated models with a tool such as Blender. Take some time and learn how to create your own models in these Blender Animation Tutorials. For now, download and use a free model, such as the one included here as an example (Oto Golem, Ninja).
Loading an animated model is pretty straight-forward, just as you have learned in the previous chapters. Animated Ogre models come as a set of files: The model is inOto.mesh.xml
, and the animation details are inOto.skeleton.xml
(plus materials and textures). Check that all files of the model are indeed in the rightModel
subdirectory./* Displaying the model requires a light source */ +}+ +Creating and Loading Animated Models
++ ++ +You create animated models with a tool such as Blender. Take some time and learn how to create your own models in these . For now, download and use a free model, such as the one included here as an example (, and ). +
+ ++Loading an animated model is pretty straight-forward, just as you have learned in the previous chapters. Animated Ogre models come as a set of files: The model is in
+Oto.mesh.xml
, and the animation details are inOto.skeleton.xml
, plus the usual files for materials and textures. Check that all files of the model are together in the sameModel
subdirectory. +public void simpleInitApp() { + /* Displaying the model requires a light source */ DirectionalLight dl = new DirectionalLight(); dl.setDirection(new Vector3f(-0.1f, -1f, -1).normalizeLocal()); rootNode.addLight(dl); /* load and attach the model as usual */ player = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); player.setLocalScale(0.5f); // resize - rootNode.attachChild(player);Don't forget to add a light source to make the material visible.
Animation Controler and Channel
+ +After you load the animated model, you register it to the Animation Controller.
The controller object gives you access to the available animation sequences. The controller can have several channels, each channel can run one animation sequence at a time. To run several sequences, you create several channels, and set them each to their animation./* Load the animation controls, listen to animation events, + rootNode.attachChild(player); + ... + }
+ ++Don't forget to add a light source to make the material visible. +
+ +Animation Controler and Channel
++ ++ +After you load the animated model, you register it to the Animation Controller. +
++
+- +
The controller object gives you access to the available animation sequences.+- +
The controller can have several channels, each channel can run one animation sequence at a time.+- +
To run several sequences, you create several channels, and set them each to their animation.+private AnimChannel channel; + private AnimControl control; + + public void simpleInitApp() { + ... + /* Load the animation controls, listen to animation events, * create an animation channel, and bring the model in its default position. */ control = player.getControl(AnimControl.class); control.addListener(this); channel = control.createChannel(); - channel.setAnim("stand");
Responding to Animation Events
+ +Add
implements AnimEventListener
to the class declaration. This interface gives you access to events that notify you when a sequence is done, or when you change from one sequence to another, so you can respond to it. In this example, you reset the character to a standing position after aWalk
cycle is done.public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { + channel.setAnim("stand"); + ...+ +Responding to Animation Events
++ ++ +Add
+implements AnimEventListener
to the class declaration. This interface gives you access to events that notify you when a sequence is done, or when you change from one sequence to another, so you can respond to it. In this example, you reset the character to a standing position after aWalk
cycle is done. +public class HelloAnimation extends SimpleApplication + implements AnimEventListener { + ... + + public void onAnimCycleDone(AnimControl control, + AnimChannel channel, String animName) { if (animName.equals("Walk")) { channel.setAnim("stand", 0.50f); channel.setLoopMode(LoopMode.DontLoop); @@ -107,31 +173,54 @@ class="level2">Add
implements AnimEventListener
to the class de } public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { // unused - }Trigger Animations After User Input
+ +There are ambient animations like animals or trees that you may want to trigger in the main event loop. In other cases, animations are triggered by user interaction, such as key input. You want to play the Walk animation when the player presses a certain key (here the spacebar), at the same time as the avatar performs the walk action and changes its location.
Initialize a new input controller (insimpleInitApp()
).
Don't forget to call theinitKey()
convenience method fromsimpleInitApp()
. Add a key mapping with the name the action you want to trigger.
Here for example, you mapWalk
to the Spacebar key. Add an input listener for theWalk
action.private void initKeys() { + }+ +Trigger Animations After User Input
++ ++ +There are ambient animations like animals or trees that you may want to trigger in the main event loop. In other cases, animations are triggered by user interaction, such as key input. You want to play the Walk animation when the player presses a certain key (here the spacebar), at the same time as the avatar performs the walk action and changes its location. +
++
+- +
Initialize a new input controller (in+simpleInitApp()
).+
+- +
Write the+initKey()
convenience method and call it fromsimpleInitApp()
.- +
Add a key mapping with the name as the action you want to trigger.++
+- +
Here for example, you map+Walk
to the Spacebar key.- +
Add an input listener for the+Walk
action.private void initKeys() { inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE)); inputManager.addListener(actionListener, "Walk"); - }To use the input controller, you need to implement the actionLister: -Test for each action by name, and set the channel to the corresponding animation to run.
The second parameter of setAnim() is the blendTime (how long the animation should overlap the last one). LoopMode can be Loop (repeat), Cycle (forward then backward), and DontLoop (only once). If needed, use channel.setSpeed() to set the speed of this animation. Optionally, use channel.setTime() to Fast-forward or rewind to a certain moment in time of this animation.private ActionListener() { + }+ ++To use the input controller, you need to implement the actionLister: +Test for each action by name, and set the channel to the corresponding animation to run. + +
++
+- +
The second parameter of setAnim() is the blendTime (how long the current animation should overlap with the last one).+- +
LoopMode can be Loop (repeat), Cycle (forward then backward), and DontLoop (only once).+- +
If needed, use channel.setSpeed() to set the speed of this animation.+- +
Optionally, use channel.setTime() to Fast-forward or rewind to a certain moment in time of this animation.+private ActionListener() { public void onAction(String name, boolean keyPressed, float tpf) { if (name.equals("Walk") && !keyPressed) { if (!channel.getAnimationName().equals("Walk")){ @@ -140,48 +229,101 @@ class="li"> Optionally, use channel.setTime() to Fast-forward or rewind to a cer } } } - };Exercises
Two Animations
Make the a mouse click trigger another animation sequence!
Create a second channel in the controller Create a new key trigger mapping and action (see: Hello Input) Tip: Do you want to find out what animation sequences are available in the model? Use:for (System.out.println(anim); }Revealing the Skeleton (1)
Open the
skeleton.xml
file in a text editor of your choice. -You don't have to be able to read or write these xml files (Blender does that for you) – but it is good to know what the xml files are there for and how skeletons work. "There's no magic to it!"
Note how the bones are numbered and named. All names of animated models follow a naming scheme. Note the bone hierarchy that specifies how the bones are connected. Note the list of animations: Each animation has a name, and several tracks. Each track tells individual bones how and when to transform. These animation steps are called keyframes.Revealing the Skeleton (2)
+ +Add the following code snippet to
simpleInitApp()
to make the bones (that you just read about) visible!SkeletonDebugger skeletonDebug = new SkeletonDebugger("skeleton", control.getSkeleton()); + };+ +Exercises
++ ++ +Exercise 1: Two Animations
++ ++ ++ +Make a mouse click trigger another animation sequence! +
++
+ +- +
Create a second channel in the controller+- +
Create a new key trigger mapping and action (see: Hello Input)+- +
Tip: Do you want to find out what animation sequences are available in the model? Use:+for (System.out.println(anim); }+Exercise 2: Revealing the Skeleton (1)
++ ++ ++ +Open the
+skeleton.xml
file in a text editor of your choice. You don't have to be able to read or write these xml files (Blender does that for you) – but it is good to know how skeletons work. "There's no magic to it!" ++
+ +- +
Note how the bones are numbered and named. All names of animated models follow a naming scheme.+- +
Note the bone hierarchy that specifies how the bones are connected.+- +
Note the list of animations: Each animation has a name, and several tracks. Each track tells individual bones how and when to transform. These animation steps are called keyframes.+Exercise 3: Revealing the Skeleton (2)
++ ++ +Add the following code snippet to
+simpleInitApp()
to make the bones (that you just read about) visible! +SkeletonDebugger skeletonDebug = + new SkeletonDebugger("skeleton", control.getSkeleton()); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", ColorRGBA.Green); mat.getAdditionalRenderState().setDepthTest(false); skeletonDebug.setMaterial(mat); - player.attachChild(skeletonDebug);Can you identify individual bones in the skeleton?
Conclusion
- \ No newline at end of file + player.attachChild(skeletonDebug);Now you can load animated models, identify stored animations, and trigger by using onAnimCycleDone() and onAnimChange(). You also learned that you can play several animations simultaneously, by starting each in a channel of its own. This could be useful if you ever want to animate the lower and upper part of the characters body independently, for example the legs run, while the arms use a weapon. -Now that your character can walk, wouldn't it be cool if it could also pick up things, or aim a weapon at things, or open doors? Time to learn more about the secrets of picking!
+Can you identify individual bones in the skeleton? +
+ ++ +Now you can load animated models, identify stored animations, and trigger animations by using onAnimCycleDone() and onAnimChange(). You also learned that you can play several animations simultaneously, by starting each in a channel of its own. This could be useful if you ever want to animate the lower and upper part of the characters body independently, for example the legs run, while the arms use a weapon. +
+ ++Now that your character can walk, wouldn't it be cool if it could also pick up things, or aim a weapon at things, or open doors? Time to reveal the secrets of mouse picking! + +
++See also: +
+ + +Previous: Hello Node, -Next: Hello Update Loop
In this tutorial we will learn to load 3-D models and text into the scene graph, using the jME asset manager. You also learn how to arrive at the correct paths, and which file formats to use.
package jme3test.helloworld; + ++ +JME 3 Tutorial (3) - Hello Assets
++ ++ ++ +Previous: Hello Node, +Next: Hello Update Loop +
+ ++In this tutorial we will learn to load 3-D models and text into the scene graph, using the jME asset manager. You also learn how to arrive at the correct paths, and which file formats to use. +
+ ++ +
+ ++
To use the example assets in a new jMonkeyPlatform project, right-click your project, select "Properties", go to "Libraries", press "Add Library" and add the "jme3-test-data" library. ++ + +Code Sample
++package jme3test.helloworld; import com.jme3.app.SimpleApplication; import com.jme3.font.BitmapText; @@ -19,7 +36,7 @@ import com.jme3.scene.Geometry; import com.jme3.scene.Spatial; import com.jme3.scene.shape.Box; -/** Sample 3 - how to load an OBJ model, and OgreXML model, +/** Sample 3 - how to load an OBJ model, and OgreXML model, * a material/texture, or text. */ public class HelloAssets extends SimpleApplication { @@ -32,7 +49,7 @@ public class HelloAssets extends SimpleApplication { public void simpleInitApp() { Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.obj"); - Material mat_default = new Material( + Material mat_default = new Material( assetManager, "Common/MatDefs/Misc/ShowNormals.j3md"); teapot.setMaterial(mat_default); rootNode.attachChild(teapot); @@ -40,9 +57,9 @@ public class HelloAssets extends SimpleApplication { // Create a wall with a simple texture from test_data Box(Vector3f.ZERO, 2.5f,2.5f,1.0f); Spatial wall = new Geometry("Box", box ); - Material mat_brick = new Material( + Material mat_brick = new Material( assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat_brick.setTexture("ColorMap", + mat_brick.setTexture("ColorMap", assetManager.loadTexture("Textures/Terrain/BrickWall/BrickWall.jpg")); wall.setMaterial(mat_brick); wall.setLocalTranslation(2.0f,-2.5f,0.0f); @@ -69,211 +86,415 @@ public class HelloAssets extends SimpleApplication { rootNode.addLight(sun); } -}
Build and run the code sample. You should see a green Ninja with a colorful teapot standing behind a wall. The text on the screen should say "Hello World".
The Asset Manager
JME3 comes with a handy asset manager that helps you keep your assets structured. Project assets are media files such as models, materials, textures, scenes, shaders, sounds, and fonts. The asset manager maintains a root that contains the classpath, so it can load any file from the current classpath (the top level of your project directory).
Additionally, the assetManager can be configured to add any path to the assets root, so assets can be loaded from directories you specify. In a jMonkeyPlatform project, jME3 seaches for models in the
assets
directory of your project. This is our recommended directory structure for storing assets:assets/Interface/ -assets/MatDefs/ -assets/Materials/ -assets/Models/ -assets/Scenes/ -assets/Shaders/ -assets/Sounds/ -assets/Textures/ -build.xml -src/... -dist/...These are just the most common examples, you can name the directories inside the assets directory how you like.
Loading Textures
Place the textures in a subdirectory of
assets/Textures/
. Load the texture into the material before you set the Material. The following code sample is from thesimpleInitApp()
method:// Create a wall with a simple texture from test_data - Box(Vector3f.ZERO, 2.5f,2.5f,1.0f); - Spatial wall = new Geometry("Box", box ); - Material mat_brick = new Material( - assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat_brick.setTexture("ColorMap", - assetManager.loadTexture("Textures/Terrain/BrickWall/BrickWall.jpg")); - wall.setMaterial(mat_brick); - wall.setLocalTranslation(2.0f,-2.5f,0.0f); - rootNode.attachChild(wall);In most cases, you use default material descriptions such as "SimpleTextured.j3md", as we do in this example. Advanced users can create custom Materials.
Loading Text and Fonts
This example displays "Hello Text" in the default font at the bottom edge of the window. You attach text to the
guiNode
, a special node for flat (orthogonal) display elements. You can clear existing text in the guiNode by detaching all its children. -The following code sample goes into thesimpleInitApp()
method.// Display a line of text with a default font - guiNode.detachAllChildren(); - guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); - BitmapText helloText = new BitmapText(guiFont, false); - helloText.setSize(guiFont.getCharSet().getRenderedSize()); - helloText.setText("Hello World"); - helloText.setLocalTranslation(300, helloText.getLineHeight(), 0); - guiNode.attachChild(helloText);Loading an Ogre XML Model
Export your model in OgreXML format (.mesh.xml, .scene, .material, .skeleton.xml) and place it in a subdirectory of
assets/Models/
. The following code sample goes into thesimpleInitApp()
method.// Load a model from test_data (OgreXML + material + texture) - Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml"); - ninja.scale(0.05f, 0.05f, 0.05f); - ninja.rotate(0.0f, -3.0f, 0.0f); - ninja.setLocalTranslation(0.0f, -5.0f, -2.0f); - rootNode.attachChild(ninja); - // You must add a light to make the model visible - DirectionalLight sun = new DirectionalLight(); - sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f)); - rootNode.addLight(sun);If you use the build script created by the jMonkeyPlatform then, by default, the original OgreXML files will not be included in your distributed game. You will get an error message when trying to load them.
com.jme3.asset.DesktopAssetManager loadAsset -WARNING: Cannot locate resource: Scenes/town/main.scene -com.jme3.app.Application handleError -SEVERE: Uncaught exception thrown in Thread[LWJGL Renderer Thread,5,main] -java.lang.NullPointerExceptionFor the release build, you should work with .j3o files only. Use the jMonkeyPlatform's context menu action to convert OgreXML models to .j3o format.
Loading Assets From Custom Paths
+ +What if your game relies on user supplied model files, that will not be included in your distribution? If a file is not located in the default location, you can register a custom Locator and load it from any path.
Here is a usage example of a ZipLocator that is registered to a file
town.zip
in the top level of your project directory:assetManager.registerLocator("town.zip", ZipLocator.class.getName()); +}+ ++Build and run the code sample. You should see a green Ninja with a colorful teapot standing behind a wall. The text on the screen should say "Hello World". +
+ +The Asset Manager
++ ++ ++ +JME3 comes with a handy asset manager that helps you keep your assets organized. Project assets are media files such as models, materials, textures, scenes, shaders, sounds, and fonts. +The Asset manager can load files from: +
++
+ +- +
the current classpath (the top level of your project directory),+- +
the+assets
directory of your project, and- +
optionally, custom paths.++ +This is our recommended directory structure for storing assets: +
+MyGame/assets/Interface/ +MyGame/assets/MatDefs/ +MyGame/assets/Materials/ +MyGame/assets/Models/ +MyGame/assets/Scenes/ +MyGame/assets/Shaders/ +MyGame/assets/Sounds/ +MyGame/assets/Textures/ +MyGame/build.xml +MyGame/src/... +MyGame/...+ ++This is just a suggested best practice, you can name the directories in the assets directory what ever you like. +
+ +Loading Textures
++ ++ ++ +Place your textures in a subdirectory of
+assets/Textures/
. Load the texture into the material before you set the Material. The following code sample is from thesimpleInitApp()
method and loads a simple wall model: +// Create a wall with a simple texture from test_data +Box(Vector3f.ZERO, 2.5f,2.5f,1.0f); +Spatial wall = new Geometry("Box", box ); +Material mat_brick = new Material( + assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); +mat_brick.setTexture("ColorMap", + assetManager.loadTexture("Textures/Terrain/BrickWall/BrickWall.jpg")); +wall.setMaterial(mat_brick); +wall.setLocalTranslation(2.0f,-2.5f,0.0f); +rootNode.attachChild(wall);+ ++In this case, you create your own Material and apply it to a Geometry. You base Materials on default material descriptions (such as "Unshaded.j3md"), as shown in this example. +
+ +Loading Text and Fonts
++ ++ ++ +This example displays the text "Hello World" in the default font at the bottom edge of the window. You attach text to the
+guiNode
– this is a special node for flat (orthogonal) display elements. You display text to show the game score, player health, etc. +The following code sample goes into thesimpleInitApp()
method. +// Display a line of text with a default font +guiNode.detachAllChildren(); +guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); +BitmapText helloText = new BitmapText(guiFont, false); +helloText.setSize(guiFont.getCharSet().getRenderedSize()); +helloText.setText("Hello World"); +helloText.setLocalTranslation(300, helloText.getLineHeight(), 0); +guiNode.attachChild(helloText);+ ++Tip: Clear existing text in the guiNode by detaching all its children. +
+ +Loading a Model
++ ++ ++ +Export your 3D model in OgreXML format (.mesh.xml, .scene, .material, .skeleton.xml) and place it in a subdirectory of
+assets/Models/
. The following code sample goes into thesimpleInitApp()
method. +// Load a model from test_data (OgreXML + material + texture) +Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml"); +ninja.scale(0.05f, 0.05f, 0.05f); +ninja.rotate(0.0f, -3.0f, 0.0f); +ninja.setLocalTranslation(0.0f, -5.0f, -2.0f); +rootNode.attachChild(ninja); +// You must add a directional light to make the model visible! +DirectionalLight sun = new DirectionalLight(); +sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal()); +rootNode.addLight(sun);+ ++Note that you do no need to create a Material if you exported the model with a material. Remember to add a light source, as shown, otherwise the material (and the whole model) is not visible! +
+ +Loading Assets From Custom Paths
++ ++ +What if your game relies on user supplied model files, that are not included in the distribution? If a file is not located in the default location (e.g. assets directory), you can register a custom Locator and load it from any path. +
+ ++Here is a usage example of a ZipLocator that is registered to a file
+town.zip
in the top level of your project directory: +assetManager.registerLocator("town.zip", ZipLocator.class.getName()); Spatial scene = assetManager.loadModel("main.scene"); - rootNode.attachChild(scene);Here is a HttpZipLocator that can download zipped models:
assetManager.registerLocator("http://jmonkeyengine.googlecode.com/files/wildhouse.zip", - HttpZipLocator.class.getName()); + rootNode.attachChild(scene);+ ++Here is a HttpZipLocator that can download zipped models and load them: +
+assetManager.registerLocator( + "http://jmonkeyengine.googlecode.com/files/wildhouse.zip", + HttpZipLocator.class.getName()); Spatial scene = assetManager.loadModel("main.scene"); - rootNode.attachChild(scene);JME3 offers ClasspathLocator, ZipLocator, FileLocator, HttpZipLocator, and UrlLocator (see
com.jme3.asset.plugins
).Creating Models and Scenes
To create 3D models and scenes, you need a 3D Mesh Editor such as Blender, with an OgreXML Exporter plugin. Create your models with UV textures. -You can use the jMonkeyPlatform to load models and create scenes from them.
Export your models as Ogre XML meshes with materials.
Open the menu File > Export > OgreXML Exporter to open the exporter dialog. In the Export Materials field: Give the material the same name as the model. For example, the modelsomething.mesh.xml
goes withsomething.material
, plus (optionally)something.skeleton.xml
and some JPG files. In the Export Meshes field: Select a subdirectory of yourassets/Models/
directory. E.g.assets/Models/something/
. Activate the following exporter settings:
Copy Textures: YES Rendering Materials: YES Flip Axis: YES Require Materials: YES Skeleton name follows mesh: YES Click export.File Format: JME 3 can load Ogre XML models, materials, and scenes, as well as Wavefront OBJ+MTL models. For the game release, you should optimize model loading by converting all models to JME3's .j3o format. We recommend creating your project in the jMonkeyPlatform, it contains an integrated .j3o converter.
Open your JME3 Project in the jMonkeyplatform. Right-click a .mesh.xml file in the Projects (or Favorites) window, and choose "convert to JME3 binary". The .j3o file appears next to the .mesh.xml file with the same name. If you use the build script generated by the jMonkeyPlatform, mesh.xml files and obj files will be excluded from the executable JAR. If you get a runtime exception, make sure you have converted all models to .j3o.Loading Models and Scenes
+ ++ +
Task? Solution! Load a model with materials Use the asset managers loadModel()
method and attach the Spatial to the rootNode.Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml"); -rootNode.attachChild(elephant);Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.j3o"); -rootNode.attachChild(elephant);+ Load a model without materials If you have a model without materials, you have to add a default material to make it visible. +Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.obj"); -Material mat = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md"); + rootNode.attachChild(scene);+ ++JME3 offers ClasspathLocator, ZipLocator, FileLocator, HttpZipLocator, and UrlLocator (see
+ + + +com.jme3.asset.plugins
). +Creating Models and Scenes
++ ++ ++ +To create 3D models and scenes, you need a 3D Mesh Editor with an OgreXML Exporter plugin. For example, you can . +You use the jMonkeyPlatform to load models, convert models and create scenes from them. +
+ ++If you use Blender, export your models as Ogre XML meshes with materials as follows: +
++
+ +- +
Open the menu File > Export > OgreXML Exporter to open the exporter dialog.+- +
In the Export Materials field: Give the material the same name as the model. For example, the model+something.mesh.xml
goes withsomething.material
, plus (optionally)something.skeleton.xml
and some JPG texture files.- +
In the Export Meshes field: Select a subdirectory of your+assets/Models/
directory. E.g.assets/Models/something/
.- +
Activate the following exporter settings:++
+- +
Copy Textures: YES+- +
Rendering Materials: YES+- +
Flip Axis: YES+- +
Require Materials: YES+- +
Skeleton name follows mesh: YES+- +
Click export.+Model File Formats
++ ++ ++ +JME3 can load Ogre XML models + materials, Ogre DotScenes, as well as Wavefront OBJ+MTL models. The loadModel() code works with these files when you run the code directly from the jMonkeyPlatform. +
+ ++If you build the executables using the default build script, then the original model files (XML, OBJ, etc) are not included. When you run the executable, you get an error message if you try to load any models directly: +
+com.jme3.asset.DesktopAssetManager loadAsset +WARNING: Cannot locate resource: Models/Ninja/Ninja.mesh.xml +com.jme3.app.Application handleError +SEVERE: Uncaught exception thrown in Thread[LWJGL Renderer Thread,5,main] +java.lang.NullPointerException+ ++Loading the XML/OBJ files directly is only acceptable during the development phase. If your graphic designer pushes updated files to the asset directory, you can quickly review the latest version in your development environment. +
+ ++For testing and for the final release build, you use .j3o files exclusively. J3o is an optimized binary format for jME3 applications, and .j3o files are automatically included in the distributable JAR file by the build script. When you do QA test builds or are ready to release, use the jMonkeyPlatform to convert all .obj/.scene/.xml/.blend files to .j3o files, and only load the .j3o versions. +
+ ++Open your JME3 Project in the jMonkeyplatform. +
++
+ +- +
Right-click a .Blend, .OBJ, or .mesh.xml file in the Projects window, and choose "convert to JME3 binary".+- +
The .j3o file appears next to the .mesh.xml file and has the same name.+- +
Change all your loadModel() lines accordingly. For example:+Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.j3o");++ +If your executable gets a runtime exception, make sure you have converted all models to .j3o! +
+ +Loading Models and Scenes
+++
+ +Task? Solution! ++ +Load a model with materials Use the asset manager's +loadModel()
method and attach the Spatial to the rootNode.Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml"); +rootNode.attachChild(elephant);+Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.j3o"); +rootNode.attachChild(elephant);++ Load a model without materials If you have a model without materials, you have to give it a material to make it visible. Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.j3o"); +Material mat = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md"); // default material teapot.setMaterial(mat); -rootNode.attachChild(teapot);Load a scene You load scenes just like you load models: Spatial scene = assetManager.loadModel("Scenes/house/main.scene"); -rootNode.attachChild(scene);Excercise - How to Load Assets
As an exercise, let's try different ways of loading a scene. You can load a scene directly, or from a zip file:
Download the town.zip sample scene. (Optional:) Unzip the town.zip to see the structure of the contained Ogre dotScene: You'll get a directory namedtown
. It contains XML and texture files, and file called main.scene. (This is just for your information, you do not need to do anything with it.) Place the town.zip file in the top level directory of your JME3 project, like so:jMonkeyProjects/MyGameProject/assets/ +rootNode.attachChild(teapot);++ +Load a scene You load scenes just like you load models: +Spatial scene = assetManager.loadModel("Scenes/town/main.scene"); +rootNode.attachChild(scene);+Spatial scene = assetManager.loadModel("Scenes/town/main.j3o"); +rootNode.attachChild(scene);+Excercise - How to Load Assets
++ ++ +As an exercise, let's try different ways of loading a scene. You will learn how to load the scene directly, or from a zip file. +
++
- +
sample scene.+- +
(Optional:) Unzip the town.zip to see the structure of the contained Ogre dotScene: You'll get a directory named+town
. It contains XML and texture files, and file called main.scene. (This is just for your information, you do not need to do anything with it.) Place the town.zip file in the top level directory of your JME3 project, like so:jMonkeyProjects/MyGameProject/assets/ jMonkeyProjects/MyGameProject/build.xml jMonkeyProjects/MyGameProject/src/ jMonkeyProjects/MyGameProject/town.zip -...Use the following method to load models from a zip file:
+ +
Make suretown.zip
is in the project directory. We register a zip file locator to the project directory. The loadModel() method now searches this zip directly for the files to load.
(That is, do not writetown.zip/main.scene
or similar.)- +
Add the following code under+simpleInitApp() {
assetManager.registerLocator("town.zip", ZipLocator.class.getName()); +...++ +Use the following method to load models from a zip file: + +
++
- +
Verify+town.zip
is in the project directory. Register a zip file locator to the project directory: Add the following code undersimpleInitApp() {
assetManager.registerLocator("town.zip", ZipLocator.class.getName()); Spatial gameLevel = assetManager.loadModel("main.scene"); gameLevel.setLocalTranslation(0, -5.2f, 0); gameLevel.setLocalScale(2); - rootNode.attachChild(gameLevel); Clean, build and run the project. You should see the Ninja+wall+teapot standing in a town.If you register new locators, make sure you do not get any file name conflicts: Give each scene a unique name.
Earlier in this tutorial, you loaded scenes and models from the asset directory. This is the most common way you will be loading scenes and models. Here is the typical procedure:
+ +
Remove the code from the previous exercise. Move the unzippedtown/
directory into theassets/Scenes/
directory of your project.- +
Add the following code under+simpleInitApp() {
Spatial gameLevel = assetManager.loadModel("Scenes/town/main.scene"); + rootNode.attachChild(gameLevel);+ ++The loadModel() method now searches this zip directly for the files to load.
+
+(This means, do not writeloadModel(town.zip/main.scene)
or similar!) +- +
Clean, build and run the project.+
+You should now see the Ninja+wall+teapot standing in a town.+ +Tip: If you register new locators, make sure you do not get any file name conflicts: Don't name all scenes
+ +main.scene
but give each scene a unique name. ++Earlier in this tutorial, you loaded scenes and models from the asset directory. This is the most common way you will be loading scenes and models. Here is the typical procedure: + +
++
- +
Remove the code that you added for the previous exercise.+- +
Move the unzipped+town/
directory into theassets/Scenes/
directory of your project. Add the following code undersimpleInitApp() {
Spatial gameLevel = assetManager.loadModel("Scenes/town/main.scene"); gameLevel.setLocalTranslation(0, -5.2f, 0); gameLevel.setLocalScale(2); - rootNode.attachChild(gameLevel); Note that the path starts relative to theassets/…
directory. Clean, build and run the project. Again, you should see the Ninja+wall+teapot standing in a town.Here is a third method you must know. Loading a scene/model from a .j3o file:
+ +
Remove the code from the previous exercise. If you haven't already, open the jMonkeyPlatform and open the project that contains the Hello Asset class. In the projects window, browse to theassets/Scenes/town
directory. Right-click themain.scene
and convert the scene to binary: The jMoneyPlatform generates a main.j3o file.- +
Add the following code under+simpleInitApp() {
Spatial gameLevel = assetManager.loadModel("Scenes/town/main.j3o"); + rootNode.attachChild(gameLevel);+ ++ Note that the path is relative to the
+assets/…
directory. +- +
Clean, build and run the project. Again, you should see the Ninja+wall+teapot standing in a town.++ +Here is a third method you must know, loading a scene/model from a .j3o file: + +
++
- +
Remove the code from the previous exercise.+- +
If you haven't already, open the jMonkeyPlatform and open the project that contains the HelloAsset class.+- +
In the projects window, browse to the+assets/Scenes/town
directory.- +
Right-click the+main.scene
and convert the scene to binary: The jMoneyPlatform generates a main.j3o file. Add the following code undersimpleInitApp() {
Spatial gameLevel = assetManager.loadModel("Scenes/town/main.j3o"); gameLevel.setLocalTranslation(0, -5.2f, 0); gameLevel.setLocalScale(2); - rootNode.attachChild(gameLevel); Again, note that the path starts relative to theassets/…
directory. Clean, Build and run the project. Again, you should see the Ninja+wall+teapot standing in a town.What is the difference between loading
Scenes/town/main.j3o
andScenes/town/main.scene
?
You can work with *.scene and .xml files during the development phase: When your designer pushes updated files to the asset directory, you can quickly review the latest version in your development environment. Create and load *.j3o files for QA and release builds: .j3o is a binary format for jME3 applications, and .j3o files will be automatically included in the distributable JAR file. When you do QA test builds or are ready to release, use the jMonkeyPlatform to convert all .scene and .xml files to .j3o files, and load these.Conclusion
- \ No newline at end of file + rootNode.attachChild(gameLevel);Now you know how to populate the scenegraph with static shapes and models, and how to build scenes. You have learned how to load assets using the
assetManager
and you have seen that the paths start relative to your project directory. Another important thing you have learned is to convert models to .j3o format for the JAR builds.Let's add some action to the scene and continue with the Update Loop.
See also:
If you want to learn how to load sounds, see Hello Audio If you want to learn more about loading textures and materials, see Hello Material
+ Again, note that the path is relative to the assets/…
directory.
+
+
+Now you know how to populate the scenegraph with static shapes and models, and how to build scenes. You have learned how to load assets using the assetManager
and you have seen that the paths start relative to your project directory. Another important thing you have learned is to convert models to .j3o format for the executable JARs etc.
+
+Let's add some action to the scene and continue with the Update Loop! + +
++See also: +
+ Previous: Hello Terrain,
-Next: Hello Effects
This tutorial explains how to add 3D sound to a game, and how make sounds play together with other game events. We learn how to use the Audio Renderer, an Audio Listener, and Audio Nodes. We also make use of an Action Listener and a MouseButtonTrigger from the previous Hello Input tutorial to make a mouse click trigger a gun shot sound.
package jme3test.helloworld; + +JME 3 Tutorial (11) - Hello Audio
++ ++ ++Previous: Hello Terrain, Next: Hello Effects +
+ ++This tutorial explains how to add 3D sound to a game, and how make sounds play together with events, such as clicking. You learn how to use an Audio Listener and Audio Nodes. You also make use of an Action Listener and a MouseButtonTrigger from the previous Hello Input tutorial to make a mouse click trigger a gun shot sound. +
+ +Sample Code
++package jme3test.helloworld; + import com.jme3.app.SimpleApplication; import com.jme3.audio.AudioNode; import com.jme3.input.controls.ActionListener; @@ -16,144 +25,360 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; import com.jme3.scene.shape.Box; + /** Sample 11 - playing 3D audio. */ public class HelloAudio extends SimpleApplication { + private AudioNode audio_gun; private AudioNode audio_nature; private Geometry player; + public static void main(String[] args) { HelloAudio app = new HelloAudio(); app.start(); } + @Override public void simpleInitApp() { flyCam.setMoveSpeed(40); + /** just a blue box floating in space */ Box(Vector3f.ZERO, 1, 1, 1); player = new Geometry("Player", box1); - Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + Material mat1 = new Material(assetManager, + "Common/MatDefs/Misc/Unshaded.j3md"); mat1.setColor("Color", ColorRGBA.Blue); player.setMaterial(mat1); rootNode.attachChild(player); + /** custom init methods, see below */ initKeys(); initAudio(); } + /** We create two audio nodes. */ private void initAudio() { /* gun shot sound is to be triggered by a mouse click. */ - audio_gun = new AudioNode(audioRenderer, assetManager, "Sound/Effects/Gun.wav"); + audio_gun = new AudioNode(assetManager, "Sound/Effects/Gun.wav", false); audio_gun.setLooping(false); audio_gun.setVolume(2); + rootNode.attachChild(audio_gun); + /* nature sound - keeps playing in a loop. */ - audio_nature = new AudioNode(audioRenderer, assetManager, "Sound/Environment/Nature.ogg"); - audio_nature.setLooping(true); + audio_nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", false); + audio_nature.setLooping(true); // activate continuous playing audio_nature.setPositional(true); audio_nature.setLocalTranslation(Vector3f.ZERO.clone()); audio_nature.setVolume(3); - audioRenderer.playSource(audio_nature); // play continuously! + rootNode.attachChild(audio_nature); + audio_nature.play(); // play continuously! } - /** Declaring the "Shoot" action, and - * mapping it to a trigger (mouse click). */ + + /** Declaring "Shoot" action, mapping it to a trigger (mouse click). */ private void initKeys() { inputManager.addMapping("Shoot", new MouseButtonTrigger(0)); inputManager.addListener(actionListener, "Shoot"); } + /** Defining the "Shoot" action: Play a gun sound. */ private ActionListener() { @Override public void onAction(String name, boolean keyPressed, float tpf) { if (name.equals("Shoot") && !keyPressed) { - audioRenderer.playSource(audio_gun); // play once! + audio_gun.playInstance(); // play each instance once! } } }; + /** Move the listener with the a camera - for 3D audio. */ @Override public void simpleUpdate(float tpf) { listener.setLocation(cam.getLocation()); listener.setRotation(cam.getRotation()); } -}
When you run the sample, you should see a blue cube, and hear nature ambient sounds. When you click you hear a loud shot.
Understanding the Code Sample
+ +In the game's
initSampleApp()
method, we create a simple blue cube geometry calledplayer
and attach it to the scene – it's just sample content so you see something when running the audio sample. -FrominitSampleApp()
, we initialize the game by calling the two custom methodsinitKeys()
andinitAudio()
. You could call the lines of code in these two methods directly frominitSampleApp()
; we simply moved them into extra methods to keep the code more readable. -Let's look atinitKeys()
: As we learned in previous tutorials, we use jme'sinputManager
to respond to user input. We add a mapping for a left mouse button click, and we name this new actionShoot
./** Declaring the "Shoot" action and mapping to a trigger. */ + +}+ ++When you run the sample, you should see a blue cube. You should hear a nature-like ambient sound. When you click, you hear a loud shot. +
+ +Understanding the Code Sample
++ ++ ++ +In the
+ +initSimpleApp()
method, you create a simple blue cube geometry calledplayer
and attach it to the scene – this just arbitrary sample content, so you see something when running the audio sample. ++From
+ +initSampleApp()
, you initialize the game by calling the two custom methodsinitKeys()
andinitAudio()
. You could call the lines of code in these two methods directly frominitSampleApp()
; they merely are in extra methods to keep the code more readable. ++Let's have a closer look at
+ +initAudio()
to learn how to useAudioNode
s. +AudioNodes
++ ++ ++ +Adding sound to your game is quite simple: Save your audio files into your
+ +assets/Sound
directory. JME3 supports both Ogg Vorbis (.ogg) and Wave (.wav) file formats. ++For each sound, you create an AudioNode. You can use an AudioNode like any node in the JME scene graph, e.g. attach it to other Nodes. You create one node for a gunshot sound, and one node for a nature sound. +
+private AudioNode audio_gun; + private AudioNode audio_nature;+ ++Look at the custom
+initAudio()
method: Here you initialize the sound objects and set their parameters. +audio_gun = new AudioNode(assetManager, "Sound/Effects/Gun.wav", false); + ... +audio_nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", false);+ ++These two lines create new sound nodes from the given audio files in the AssetManager. The
+ +false
flag means that you want to buffer these sounds before playing. (If you set this flag to true, the sound will be streamed, which makes sense for really long sounds.) ++You want the gunshot sound to play once (you don't want it to loop). You also specify its volume as gain factor (at 0, sound is muted, at 2, it is twice as loud, etc.). +
+audio_gun.setLooping(false); + audio_gun.setVolume(2); + rootNode.attachChild(audio_gun);+ ++The nature sound is different: You want it to loop continuously as background sound. This is why you set looping to true, and immediately call the play() method on the node. You also choose to set its volume to 3. +
+audio_nature.setLooping(true); // activate continuous playing + ... + audio_nature.setVolume(3); + rootNode.attachChild(audio_nature); + audio_nature.play(); // play continuously! + }+ ++Here you make audio_nature a positional sound that comes from a certain place. For that you give the node an explicit translation, in this example, you choose Vector3f.ZERO (which stands for the coordinates
+0.0f,0.0f,0.0f
, the center of the scene.) Since jME supports 3D audio, you are now able to hear this sound coming from this particular location. Making the sound positional is optional. If you don't use these lines, the ambient sound comes from every direction. +... + audio_nature.setPositional(true); + audio_nature.setLocalTranslation(Vector3f.ZERO.clone()); + ...+ ++Tip: Attach AudioNodes to the rootNode like all nodes, to make certain moving nodes stay up-to-date. If you don't attach them, they are still audible and you don't get an error message. But 3D sound will not work as expected if the nodes are not updated regularly. +
+ +Triggering Sound
++ ++ +Let's have a closer look at
+initKeys()
: As you learned in previous tutorials, you use theinputManager
to respond to user input. Here you add a mapping for a left mouse button click, and name this new actionShoot
. +/** Declaring "Shoot" action, mapping it to a trigger (mouse click). */ private void initKeys() { - // click to shoot inputManager.addMapping("Shoot", new MouseButtonTrigger(0)); inputManager.addListener(actionListener, "Shoot"); - }Setting up the ActionListener should also be familiar from previous tutorials. We declare that, when the trigger (the mouse button) is pressed and released, we want to play a gun sound.
/** Defining the "Shoot" action: play a sound. */ + }+ ++Setting up the ActionListener should also be familiar from previous tutorials. You declare that, when the trigger (the mouse button) is pressed and released, you want to play a gun sound. +
+/** Defining the "Shoot" action: Play a gun sound. */ private ActionListener() { @Override public void onAction(String name, boolean keyPressed, float tpf) { if (name.equals("Shoot") && !keyPressed) { - audioRenderer.playSource(audio_gun); + audio_gun.playInstance(); // play each instance once! } } - };Next we have a closer look at
initAudio()
to learn how to use theAudioRenderer
andAudioNode
s.AudioNodes
Using audio is quite simple. Save your audio files into your
assets/Sound
directory. jME3 supports both Ogg Vorbis (.ogg) and Wave (.wav) files. -For each sound you create an audio node. A sound node can be used like any node in the jME scene graph. We create one node for a gunshot sound, and one for a nature sound.private AudioNode audio_gun; - private AudioNode audio_nature;Look at our custom
initAudio()
method: Here we initialize the sound objects and set their parameters.audio_gun = new AudioNode( audioRenderer, assetManager, "Sound/Effects/Gun.wav"); - ... - audio_nature = new AudioNode( audioRenderer, assetManager, "Sound/Environment/Nature.ogg");These two lines create new sound nodes from the given audio files in the assetManager. -In the next lines we specify that we want the gunshot sound to play only once – we don't want it to loop. We specify its volume as gain factor (at 0, sound is muted, at 2, it is twice as loud, etc.).
audio_gun.setLooping(false); - audio_gun.setVolume(2);The nature sound is different: We want it to loop continuously as background sound. This is why we set looping to true, and we already call the play() method on the node. We also choose to set its volume to 3.
audio_nature.setLooping(true); - audio_nature.setVolume(3); -... - audioRenderer.playSource(audio_nature); // play continuously! - }We can choose to make the audio_nature a positional sound that comes from a certain place. We have to give the node an explicit translation, in this example we choose Vector3f.ZERO (which stands for the coordinates
0.0f,0.0f,0.0f
, the center of the scene.) Since jME supports 3D audio, you will be able to hear this sound coming from this particular location. Making the sound positional is optional.audio_nature.setPositional(true); - audio_nature.setLocalTranslation(Vector3f.ZERO.clone());Triggering Audio
+ +The two sounds are used differently:
The gunshot is situational. We want to play it only once, right when it is triggered.
This is why we made itsetLooping(false)
The nature sound is a background noise. We want it to start playing and loop on as long as the game runs.
This is why we made itsetLooping(true)
Now every sound knows whether it loops or not. The actual
play
command is the same for both files:audioRenderer.playSource(audio_nature); -... - audioRenderer.playSource(audio_gun);Appart from the looping Boolean, the only difference is where play() is called:
We start playing the background nature sound right after we have created it, in the initAudio() method. The gunshot sound, however, is triggered situationally, only as part of theShoot
input action that we defined in the ActionListener./** Defining the "Shoot" action: Play a gun sound. */ + };+ ++Since you want to be able to shoot fast repeatedly, so you do not want to wait for the previous gunshot sound to end before the next one can start. This is why you play this sound using the
+ +playInstance()
method. This means that every click starts a new instance of the sound, so two instances can overlap. You set this sound not to loop, so each instance only plays once. As you would expect it of a gunshot. +Ambient or Situational?
++ ++ + + ++ +The two sounds are two different use cases: +
++
+ +- +
A gunshot is situational. You want to play it only once, right when it is triggered.++
+- +
This is why you+setLooping(false)
.- +
The nature sound is an ambient, background noise. You want it to start playing from the start, as long as the game runs.++
+- +
This is why you+setLooping(true)
.+ +Now every sound knows whether it should loop or not. +
+ ++Apart from the looping boolean, another difference is where
+play()
(playInstace()
) is called on those nodes: ++
- +
You start playing the background nature sound right after you have created it, in the initAudio() method.+audio_nature.play(); // play continuously!+ The gunshot sound, however, is triggered situationally, once, only as part of theShoot
input action that you defined in the ActionListener./** Defining the "Shoot" action: Play a gun sound. */ private ActionListener() { @Override public void onAction(String name, boolean keyPressed, float tpf) { if (name.equals("Shoot") && !keyPressed) { - audioRenderer.playSource(audio_gun); // play once! + audio_gun.playInstance(); // play each instance once! } } - };Your Ear in the Scene - 3D Audio Listener
To keep up the 3D audio effect, jME needs to know the position of the sound source, and the position of the ears of the player. The ear is represented by an 3D Audio Listener object. The
listener
is a built-in object in a SimpleApplication (it is not related to any other Java Listeners, such as the input manager's ActionListener). -In order to make the most of the 3D audio effect, we use thesimpleUpdate()
method to move and rotate the listener (the player's ears) together with the camera (the player's eyes).public void simpleUpdate(float tpf) { - listener.setLocation(cam.getLocation()); - listener.setRotation(cam.getRotation()); -}Global, Directional, Positional?
In this example, we defined the nature sound as coming from a certain position, but not the gunshot sound. This means our gunshot is global and can be heard everywhere with the same volume. JME3 also supports directional sounds, that can only be heard from a certain direction (More about Audio here). How do you make this decision?
In a game with moving enemies you may want to make the gun shot or footsteps positional sounds. In these cases you must move the audio nodes to the location of the enemy before playing it. This way a player with stereo speakers could hear from which direction the steps or gun shots are coming. Similarly, you may have game levels where you want one background sound to play globally. In this case, you would make the audio_nature neither positional nor directional (set both to false). If you want sound to be "absorbed by the walls" and only broadcast in one direction, you would make it directional. (More about Audio here.)In short, you must choose in every situation whether you want a sound to be global, directional, or positional.
Conclusion
- \ No newline at end of file + }; +You now know how to add the two most common types of sound to your game: Global sounds and positional sounds. You can play sounds in two ways: Either continuously in a loop, or situationally just once. You also learned to use sound files that are in either .ogg or .wav format. Tip: jME's Audio implementation also supports more advanced effects such as reverberation and the Doppler effect. You can also create directional sounds, and stream long sound files instead of buffering first. Find out more about these features from the sample code included in the jme3test directory and from the advanced Audio docs. -Want some fire and explosions to go with your sounds? Read on to learn more about effects.
+ +The Boolean in the AudioNode constructor defines whether the audio is buffered (false) or streamed (true). For example: + +
+audio_nature = new AudioNode(assetManager, "Sound/Effects/Gun.wav", false); // buffered +... +audio_nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", true); // streamed+ +
+Typically, you stream long sounds, and buffer short sounds. +
+ +audio.play() | audio.playInstance() | +
---|---|
Plays buffered sounds. | Plays buffered sounds. | +
Plays streamed sounds. | Cannot play streamed sounds. | +
The same sound cannot play twice at the same time. | The same sounds can play multiple times and overlap. | +
+
+To create a 3D audio effect, JME3 needs to know the position of the sound source, and the position of the ears of the player. The ears are represented by an 3D Audio Listener object. The listener
object is a default object in a SimpleApplication.
+
+In order to make the most of the 3D audio effect, you must use the simpleUpdate()
method to move and rotate the listener (the player's ears) together with the camera (the player's eyes).
+
public void simpleUpdate(float tpf) { + listener.setLocation(cam.getLocation()); + listener.setRotation(cam.getRotation()); + }+ +
+If you don't do that, the results of 3D audio will be quite random. +
+ ++ +In this example, you defined the nature sound as coming from a certain position, but not the gunshot sound. This means your gunshot is global and can be heard everywhere with the same volume. JME3 also supports directional sounds which you can only hear from a certain direction. +
+ ++It makes equally sense to make the gunshot positional, and let the ambient sound come from every direction. How do you decide which type of 3D sound to use from case to case? +
+playInstance()
ing it. This way a player with stereo speakers hears from which direction the enemy is coming.+ +In short, you must choose in every situation whether it makes sense for a sound to be global, directional, or positional. +
+ ++ +You now know how to add the two most common types of sound to your game: Global sounds and positional sounds. You can play sounds in two ways: Either continuously in a loop, or situationally just once. You know the difference between buffering short sounds and streaming long sounds. You know the difference between playing overlapping sound instances, and playing unique sounds that cannot overlap with themselves. You also learned to use sound files that are in either .ogg or .wav format. +
+ ++Tip: JME's Audio implementation also supports more advanced effects such as reverberation and Doppler effect. Use these "pro" features to make audio sound different depending on whether it's in the hallway, in a cave, outdoors, or in a carpeted room. Find out more about environmental effects from the sample code included in the jme3test directory and from the advanced Audio docs. +
+ ++Want some fire and explosions to go with your sounds? Read on to learn more about effects. + +
++ +See also: +
+ Previous: Hello Picking,
-Next: Hello Terrain
This tutorial demonstrates how you load a scene model and give it solid walls and floors for a character to walk around.
-We will use a PhysicsNode
for the static collidable scene, and a PhysicsCharacterNode
for the mobile first-person character. We will also adapt the default first-person camera to work with physics-controlled navigation.
-The solution shown here can be used for first-person shooters, mazes, and similar games.
If you don't have it yet, download the town.zip sample scene.
jMonkeyProjects$ ls -1 BasicGame + ++ +JME 3 Tutorial (9) - Hello Collision
++ ++ ++Previous: Hello Picking, +Next: Hello Terrain +
+ ++This tutorial demonstrates how you load a scene model and give it solid walls and floors for a character to walk around. +You use a
+ +RigidBodyControl
for the static collidable scene, and aCharacterControl
for the mobile first-person character. You also learn how to set up the default first-person camera to work with physics-controlled navigation. +You can use the solution shown here for first-person shooters, mazes, and similar games. ++ +
+ +Sample Code
++ ++ +If you don't have it yet, sample scene. +
+jMonkeyProjects$ ls -1 BasicGame assets/ build.xml town.zip -src/Place town.zip in the root directory of your JME3 project.
package jme3test.helloworld; +src/+ ++Place town.zip in the root directory of your JME3 project. Here is the code: +
+package jme3test.helloworld; + import com.jme3.app.SimpleApplication; import com.jme3.asset.plugins.ZipLocator; import com.jme3.bullet.BulletAppState; @@ -32,45 +54,54 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.scene.Node; import com.jme3.scene.Spatial; + /** * Example 9 - How to make walls and floors solid. - * This version uses Physics and a custom Action Listener. + * This collision code uses Physics and a custom Action Listener. * @author normen, with edits by Zathras */ public class HelloCollision extends SimpleApplication - implements ActionListener { + implements ActionListener { + private Spatial sceneModel; private BulletAppState bulletAppState; private RigidBodyControl landscape; private CharacterControl player; private Vector3f walkDirection = new Vector3f(); private boolean left = false, right = false, up = false, down = false; + public static void main(String[] args) { HelloCollision app = new HelloCollision(); app.start(); } + public void simpleInitApp() { /** Set up Physics */ bulletAppState = new BulletAppState(); stateManager.attach(bulletAppState); - // We re-use the flyby camera control for rotation, while positioning is handled by physics - viewPort.setBackgroundColor(new ColorRGBA(0.7f,0.8f,1f,1f)); + //bulletAppState.getPhysicsSpace().enableDebug(assetManager); + + // We re-use the flyby camera for rotation, while positioning is handled by physics + viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f)); flyCam.setMoveSpeed(100); setUpKeys(); setUpLight(); + // We load the scene from the zip file and adjust its size. assetManager.registerLocator("town.zip", ZipLocator.class.getName()); sceneModel = assetManager.loadModel("main.scene"); sceneModel.setLocalScale(2f); + // We set up collision detection for the scene by creating a - // compound collision shape and a static physics node with mass zero. + // compound collision shape and a static RigidBodyControl with mass zero. CollisionShape sceneShape = - CollisionShapeFactory.createMeshShape((Node) sceneModel); + CollisionShapeFactory.createMeshShape((Node) sceneModel); landscape = new RigidBodyControl(sceneShape, 0); sceneModel.addControl(landscape); + // We set up collision detection for the player by creating - // a capsule collision shape and a physics character node. - // The physics character node offers extra settings for + // a capsule collision shape and a CharacterControl. + // The CharacterControl offers extra settings for // size, stepheight, jumping, falling, and gravity. // We also put the player in its starting position. CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1.5f, 6f, 1); @@ -79,51 +110,57 @@ public class HelloCollision extends SimpleApplication player.setFallSpeed(30); player.setGravity(30); player.setPhysicsLocation(new Vector3f(0, 10, 0)); - // We attach the scene and the player to the rootnode and the physics space, + + // We attach the scene and the player to the rootNode and the physics space, // to make them appear in the game world. rootNode.attachChild(sceneModel); bulletAppState.getPhysicsSpace().add(landscape); bulletAppState.getPhysicsSpace().add(player); } - private void setUpLight() { - // We add light so we see the scene - AmbientLight al = new AmbientLight(); - al.setColor(ColorRGBA.White.mult(1.3f)); - rootNode.addLight(al); - DirectionalLight dl = new DirectionalLight(); - dl.setColor(ColorRGBA.White); - dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal()); - rootNode.addLight(dl); - } + + private void setUpLight() { + // We add light so we see the scene + AmbientLight al = new AmbientLight(); + al.setColor(ColorRGBA.White.mult(1.3f)); + rootNode.addLight(al); + + DirectionalLight dl = new DirectionalLight(); + dl.setColor(ColorRGBA.White); + dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal()); + rootNode.addLight(dl); + } + /** We over-write some navigational key mappings here, so we can * add physics-controlled walking and jumping: */ private void setUpKeys() { - inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_A)); - inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_D)); - inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_W)); - inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_S)); - inputManager.addMapping("Jumps", new KeyTrigger(KeyInput.KEY_SPACE)); - inputManager.addListener(this, "Lefts"); - inputManager.addListener(this, "Rights"); - inputManager.addListener(this, "Ups"); - inputManager.addListener(this, "Downs"); - inputManager.addListener(this, "Jumps"); + inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A)); + inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D)); + inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_W)); + inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_S)); + inputManager.addMapping("Jump", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addListener(this, "Left"); + inputManager.addListener(this, "Right"); + inputManager.addListener(this, "Up"); + inputManager.addListener(this, "Down"); + inputManager.addListener(this, "Jump"); } + /** These are our custom actions triggered by key presses. * We do not walk yet, we just keep track of the direction the user pressed. */ public void onAction(String binding, boolean value, float tpf) { - if (binding.equals("Lefts")) { - left = value; - } else if (binding.equals("Rights")) { - right = value; - } else if (binding.equals("Ups")) { - up = value; - } else if (binding.equals("Downs")) { - down = value; - } else if (binding.equals("Jumps")) { + if (binding.equals("Left")) { + if (value) { left = true; } else { left = false; } + } else if (binding.equals("Right")) { + if (value) { right = true; } else { right = false; } + } else if (binding.equals("Up")) { + if (value) { up = true; } else { up = false; } + } else if (binding.equals("Down")) { + if (value) { down = true; } else { down = false; } + } else if (binding.equals("Jump")) { player.jump(); } } + /** * This is the main event loop--walking happens here. * We check in which direction the player is walking by interpreting @@ -143,122 +180,361 @@ public class HelloCollision extends SimpleApplication player.setWalkDirection(walkDirection); cam.setLocation(player.getPhysicsLocation()); } -}Run the sample. You should see a town square with houses and a monument. Use the WASD keys and the mouse to navigate around in first person view. Run forward and jump by pressing W and Space. Note how you step over the sidewalk, and up the steps to the monument. You can walk in the alleys between the houses, but the walls are solid. Don't walk over the edge of the world!
Understanding the Code
+ +Let's start with the class declaration:
extends SimpleApplication implements ActionListenerSimpleApplication is the base class for all jME3 games. We also have this class implement the
ActionListener
interface because we want to customize the navigational inputs later.private Spatial sceneModel; +}+ ++Run the sample. You should see a town square with houses and a monument. Use the WASD keys and the mouse to navigate around with a first-person perspective. Run forward and jump by pressing W and Space. Note how you step over the sidewalk, and up the steps to the monument. You can walk in the alleys between the houses, but the walls are solid. Don't walk over the edge of the world! +
+ +Understanding the Code
++ ++ +Let's start with the class declaration: +
+public class HelloCollision extends SimpleApplication + implements ActionListener { ... }+ ++You already know that SimpleApplication is the base class for all jME3 games. You make this class implement the
+ActionListener
interface because you want to customize the navigational inputs later. +private Spatial sceneModel; private BulletAppState bulletAppState; private RigidBodyControl landscape; private CharacterControl player; private Vector3f walkDirection = new Vector3f(); - private boolean left = false, right = false, up = false, down = false;We initialize a few private fields:
The BulletAppState gives this SimpleApplication access to physics features (such as collision detection) supplied by jME3's jBullet integration The Spatial sceneModel is a normal OgreXML model loaded as a Spatial. You use the model as the game's landscape by adding a RigidBodyControl to it. The (invisible) first-person player is represented by a CharacterControl object. The fieldswalkDirection
and the four Booleans are used for physics-controlled navigation.Let's have a look at all the details:
Initializing the Game
+ +As usual, you initialize the game in the
simpleInitApp()
method.viewPort.setBackgroundColor(new ColorRGBA(0.7f,0.8f,1f,1f)); + private boolean left = false, right = false, up = false, down = false;+ ++You initialize a few private fields: +
++
+ +- +
The BulletAppState gives this SimpleApplication access to physics features (such as collision detection) supplied by jME3's jBullet integration+- +
The Spatial sceneModel is for loading an OgreXML model of a town.+- +
You need a RigidBodyControl to make the town model solid.+- +
The (invisible) first-person player is represented by a CharacterControl object.+- +
The fields+walkDirection
and the four Booleans are used for physics-controlled navigation.+ +Let's have a look at all the details: +
+ +Initializing the Game
++ ++ +As usual, you initialize the game in the
+simpleInitApp()
method. +viewPort.setBackgroundColor(new ColorRGBA(0.7f,0.8f,1f,1f)); flyCam.setMoveSpeed(100); setUpKeys(); - setUpLight();We switch the background color from balck to light blue, since this is a scene with a sky. -You repurpose the default camera control "flyCam" as first-person camera and set its speed. -The auxiliary method
setUpLights()
adds light sources. -The auxiliary methodsetUpKeys()
configures input mappings–we will look at it later.The Physics-Controlled Scene
+ +The first thing you do in every physics game is create a BulletAppState object. It gives you access to jME3's jBullet integration which handles physical forces and collisions.
bulletAppState = new BulletAppState(); - stateManager.attach(bulletAppState);For the scene, you load the
sceneModel
from a zip file, and adjust the size.assetManager.registerLocator("town.zip", ZipLocator.class.getName()); + setUpLight();++
+ +- +
You set the background color to light blue, since this is a scene with a sky.+- +
You repurpose the default camera control "flyCam" as first-person camera and set its speed.+- +
The auxiliary method+setUpLights()
adds your light sources.- +
The auxiliary method+setUpKeys()
configures input mappings–we will look at it later.The Physics-Controlled Scene
++ ++ +The first thing you do in every physics game is create a BulletAppState object. It gives you access to jME3's jBullet integration which handles physical forces and collisions. +
+bulletAppState = new BulletAppState(); + stateManager.attach(bulletAppState);+ ++For the scene, you load the
+sceneModel
from a zip file, and adjust the size. +assetManager.registerLocator("town.zip", ZipLocator.class.getName()); sceneModel = assetManager.loadModel("main.scene"); - sceneModel.setLocalScale(2f);The file
town.zip
is included as a sample model in the JME3 sources – you can download it here. (Optionally, use any OgreXML scene of your own.) For this sample, place the zip file in the application's top level directory (that is, next to src/, assets/, build.xml).CollisionShape sceneShape = + sceneModel.setLocalScale(2f);+ ++The file
+town.zip
is included as a sample model in the JME3 sources – you can . (Optionally, use any OgreXML scene of your own.) For this sample, place the zip file in the application's top level directory (that is, next to src/, assets/, build.xml). +CollisionShape sceneShape = CollisionShapeFactory.createMeshShape((Node) sceneModel); landscape = new RigidBodyControl(sceneShape, 0); - sceneModel.addControl(landscape);To use collision detection, you want to add a RigidBodyControl to the
sceneModel
Spatial. The RigidBodyControl for a complex model takes two arguments: A Collision Shape, and the object's mass.
JME3 offers aCollisionShapeFactory
that precalculates a mesh-accurate collision shape for a Spatial. We choose to generate aCompoundCollisionShape
, which has MeshCollisionShapes as its children. Note: This type of collision shape is optimal for immobile objects, such as terrain, houses, and whole shooter levels. You set the mass to zero since a scene is static and its mass irrevelant.Add the control to the Spatial to give it physical properties. Attach the sceneModel to the rootNode to make it visible.
rootNode.attachChild(sceneModel);Tip: Remember to always add a light source so you can see the scene.
The Physics-Controlled Player
+ +Next you set up collision detection for the first-person player.
CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1.5f, 6f, 1);Again, you create a CollisionShape: This time we choose a CapsuleCollisionShape, a cylinder with a rounded top and bottom. Note: This shape is optimal for a person: It's tall and the roundness helps to get stuck less often on obstacles.
Supply the CapsuleCollisionShape constructor with the desired size of the bounding capsule to fit the shape of your character. In this example the character is 2*1.5f units wide, and 6f units tall. The final integer argument specifies the orientation of the cylinder: 1 is the Y-axis, which fits an upright person. For many animals and vehicles you would use 0 or 2.player = new CharacterControl(capsuleShape, 0.05f);Now you use the CollisionShape to create a
CharacterControl
that represents the player. The last argument of the CharacterControl constructor (here.05f
) is the size of a step that the character should be able to surmount.player.setJumpSpeed(20); + sceneModel.addControl(landscape); + rootNode.attachChild(sceneModel);+ ++To use collision detection, you add a RigidBodyControl to the
+sceneModel
Spatial. The RigidBodyControl for a complex model takes two arguments: A Collision Shape, and the object's mass. ++
+ +- +
JME3 offers a+CollisionShapeFactory
that precalculates a mesh-accurate collision shape for a Spatial. You choose to generate aCompoundCollisionShape
(which has MeshCollisionShapes as its children) because this type of collision shape is optimal for immobile objects, such as terrain, houses, and whole shooter levels.- +
You set the mass to zero since a scene is static and its mass is irrevelant.+- +
Add the control to the Spatial to give it physical properties.+- +
As always, attach the sceneModel to the rootNode to make it visible.++ +Tip: Remember to add a light source so you can see the scene. +
+ +The Physics-Controlled Player
++ ++ +A first-person player is typically invisible. When you use the default flyCam as first-person cam, it does not even test for collisons and runs through walls. This is because the flyCam control does not have any physical shape assigned. In this code sample, you represent the first-person player as an (invisible) physical shape. You use the WASD keys to steer this physical shape around, while the physics engine manages for you how it walks along solid walls and on solid floors and jumps over solid obstacles. Then you simply make the camera follow the walking shape's location – and you get the illusion of being a physical body in a solid environment seeing through the camera. +
+ ++So let's set up collision detection for the first-person player. +
+CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1.5f, 6f, 1);+ ++Again, you create a CollisionShape: This time you choose a CapsuleCollisionShape, a cylinder with a rounded top and bottom. This shape is optimal for a person: It's tall and the roundness helps to get stuck less often on obstacles. +
++
+- +
Supply the CapsuleCollisionShape constructor with the desired radius and height of the bounding capsule to fit the shape of your character. In this example the character is 2*1.5f units wide, and 6f units tall.+- +
The final integer argument specifies the orientation of the cylinder: 1 is the Y-axis, which fits an upright person. For animals which are longer than high you would use 0 or 2 (depending on how it is rotated).+player = new CharacterControl(capsuleShape, 0.05f);+ ++
"Does that CollisionShape make me look fat?" If you ever get confusing physics behaviour, remember to have a look at the collision shapes. Add the following line after the bulletAppState initialization to make the shapes visible: + ++ + +bulletAppState.getPhysicsSpace().enableDebug(assetManager);+ ++ +
+Now you use the CollisionShape to create a
+CharacterControl
that represents the first-person player. The last argument of the CharacterControl constructor (here.05f
) is the size of a step that the character should be able to surmount. +player.setJumpSpeed(20); player.setFallSpeed(30); - player.setGravity(30);Apart from step height and character size, the
CharacterControl
lets you configure jumping, falling, and gravity speeds. Adjust the values to fit your game situation.player.setPhysicsLocation(new Vector3f(0, 10, 0));Finally we put the player in its starting position and update its state – remember to use setPhysicsLocation() instead of setLocalTranslation(). since you are dealing with a physical object.
Activating the PhysicsSpace
As in every JME3 application, you must attach the scene and the player to the
rootNode
to make them appear in the game world.rootNode.attachChild(landscape); - rootNode.attachChild(player);Remember that for physical games, you must also add all solid objects (usually the characters and the scene) to the
PhysicsSpace
!bulletAppState.getPhysicsSpace().add(landscape); - bulletAppState.getPhysicsSpace().add(player);Navigation
The default camera controller
cam
is a third-person camera. JME3 also offers a first-person controller,flyCam
, which we use here to handle camera rotation. -However we must redefine how walking is handled for physics-controlled objects: When you navigate a physical node, you do not specify a target location, but a walk direction. The physics space calculates how far the character can actually go in the desired direction – or whether it will be stoped by an obstacle. -This is why we must re-define the flyCam's navigational key mappings to usesetWalkDirection()
instead ofsetLocalTranslation()
. Here are the steps:1. inputManager
Configure the familiar WASD inputs for walking, and Space for jumping.
inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_A)); - inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_D)); - inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_W)); - inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_S)); - inputManager.addMapping("Jumps", new KeyTrigger(KeyInput.KEY_SPACE)); - inputManager.addListener(this, "Lefts"); - inputManager.addListener(this, "Rights"); - inputManager.addListener(this, "Ups"); - inputManager.addListener(this, "Downs"); - inputManager.addListener(this, "Jumps");In the code sample above, this block of code was moved into an auxiliary method
setupKeys()
that is called fromsimpleInitApp()
–this is just to keep the code more readable.2. onAction()
+ +Remember that we declared this class an
ActionListener
so we could customize the flyCam. TheActionListener
interface requires us to implement theonAction()
method: You re-define the actions triggered by navigation key presses to work with physics.public void onAction(String binding, boolean value, float tpf) { - if (binding.equals("Lefts")) { - left = value; - } else if (binding.equals("Rights")) { - right = value; - } else if (binding.equals("Ups")) { - up = value; - } else if (binding.equals("Downs")) { - down = value; - } else if (binding.equals("Jumps")) { + player.setGravity(30);+ ++Apart from step height and character size, the
+CharacterControl
lets you configure jumping, falling, and gravity speeds. Adjust the values to fit your game situation. +player.setPhysicsLocation(new Vector3f(0, 10, 0));+ ++Finally we put the player in its starting position and update its state – remember to use
+ +setPhysicsLocation()
instead ofsetLocalTranslation()
now, since you are dealing with a physical object. +PhysicsSpace
++ ++ ++ +Remember, in physical games, you must register all solid objects (usually the characters and the scene) to the PhysicsSpace! +
+bulletAppState.getPhysicsSpace().add(landscape); + bulletAppState.getPhysicsSpace().add(player);+ ++The invisible body of the character just sits there on the physical floor. It cannot walk yet – you will deal with that next. +
+ +Navigation
++ ++ ++ +The default camera controller
+ +cam
is a third-person camera. JME3 also offers a first-person controller,flyCam
, which we use here to handle camera rotation. TheflyCam
control moves the camera usingsetLocation()
. ++However, you must redefine how walking (camera movement) is handled for physics-controlled objects: When you navigate a non-physical node (e.g. the default flyCam), you simply specify the target location. There are no tests that prevent the flyCam from getting stuck in a wall! When you move a PhysicsControl, you want to specify a walk direction instead. Then the PhysicsSpace can calculate for you how far the character can actually move in the desired direction – or whether an obstacle prevents it from going any further. +
+ ++In short, you must re-define the flyCam's navigational key mappings to use
+ +setWalkDirection()
instead ofsetLocalTranslation()
. Here are the steps: +1. inputManager
++ ++ ++ +In the
+simpleInitApp()
method, you re-configure the familiar WASD inputs for walking, and Space for jumping. +private void setUpKeys() { + inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A)); + inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D)); + inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_W)); + inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_S)); + inputManager.addMapping("Jump", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addListener(this, "Left"); + inputManager.addListener(this, "Right"); + inputManager.addListener(this, "Up"); + inputManager.addListener(this, "Down"); + inputManager.addListener(this, "Jump"); +}+ ++You can move this block of code into an auxiliary method
+ +setupKeys()
and call this method fromsimpleInitApp()
– to keep the code more readable. +2. onAction()
++ ++ +Remember that this class implements the an
+ActionListener
interface, so you can customize the flyCam inputs. TheActionListener
interface requires you to implement theonAction()
method: You re-define the actions triggered by navigation key presses to work with physics. +public void onAction(String binding, boolean value, float tpf) { + if (binding.equals("Left")) { + if (value) { left = true; } else { left = false; } + } else if (binding.equals("Right")) { + if (value) { right = true; } else { right = false; } + } else if (binding.equals("Up")) { + if (value) { up = true; } else { up = false; } + } else if (binding.equals("Down")) { + if (value) { down = true; } else { down = false; } + } else if (binding.equals("Jump")) { player.jump(); } - }Every time the user presses one of the WASD keys, you keep track of the direction the user wants to go – by storing this info in four directional Booleans. We will use them soon. -Note that no actual walking happens here – not yet! -The only movement that you do not have to implement yourself is the jumping action. The call
player.jump()
is a special method that handles a correct jumping motion for yourPhysicsCharacterNode
.3. setWalkDirection()
+ +In
onAction()
you have determined in which direction the user wants to go in terms of "forward" or "left". -Now you need poll the current rotation of the camera to find to which vectors "forward" and "left" correspond in the coordinate system. -This last and most important code snippet goes into the main event loop,simpleUpdate()
.Vector3f camDir = cam.getDirection().clone().multLocal(0.6f); + }+ ++The only movement that you do not have to implement yourself is the jumping action. The call
+ +player.jump()
is a special method that handles a correct jumping motion for yourPhysicsCharacterNode
. ++For all other directions: Every time the user presses one of the WASD keys, you keep track of the direction the user wants to go, by storing this info in four directional Booleans. No actual walking happens here yet. The update loop is what acts out the directional info stored in the booleans, and makes the player move, as shown in the next code snippet: +
+ +3. setWalkDirection()
++ ++ +Previously in the
+ +onAction()
method, you have collected the info in which direction the user wants to go in terms of "forward" or "left". In the update loop, you repatedly poll the current rotation of the camera. You calculate the actual vectors to which "forward" or "left" corresponds in the coordinate system. ++This last and most important code snippet goes into the
+simpleUpdate()
method. +public void simpleUpdate(float tpf) { + Vector3f camDir = cam.getDirection().clone().multLocal(0.6f); Vector3f camLeft = cam.getLeft().clone().multLocal(0.4f); walkDirection.set(0, 0, 0); if (left) { walkDirection.addLocal(camLeft); } if (right) { walkDirection.addLocal(camLeft.negate()); } if (up) { walkDirection.addLocal(camDir); } - if (down) { walkDirection.addLocal(camDir.negate()); }Reset the variable
walkDirection
to zero. Then add to it all latest motion vectors that you polled from the camera. It is posible for a character to move forward and to the left simultaneously.player.setWalkDirection(walkDirection);This one line does the "walking" magic: Always use
setWalkDirection()
to make a physics-controlled object move continuously, and the physics engine automatically handles collision detection for you! Important: Do not usesetLocalTranslation()
to walk the player around. You may get it stuck by overlapping with another physical object. You can put the player in a start position withsetPhysicalLocation()
if you make sure to place it a bit above the floor and away from obstacles. -Lastly, do not forget to make the first-person camera object move along with the physics-controlled player node:cam.setLocation(player.getPhysicsLocation());That's it!
Conclusion
- \ No newline at end of file + if (down) { walkDirection.addLocal(camDir.negate()); } + player.setWalkDirection(walkDirection); + cam.setLocation(player.getPhysicsLocation()); + }You have learned how to load a "solid" physical scene model and walk around in it with a first-person perspective. -You had JME3 calculate the CollisionShapes, and you represented collidables as PhysicsNodes that you registered to the Physics Space. -You also made certain to use
player.setWalkDirection(walkDirection)
to move physical characters around. -To learn more about different ways of loading models and scene have a look at Hello Asset,Scene Explorer and Scene Composer There are also other possible solutions for this task that do not require physics. -Have a look at jme3test.collision.TestSimpleCollision.java (and SphereMotionAllowedListener.java). -To learn more about complex physics scenes where several mobile physical objects bump into each other, read Hello Physics. -Do you want to hear your player say "ouch!" when he bumps into a wall? Continue with learning how to add sound to your game.
+This is how the walking is triggered: +
+walkDirection
to zero. This is where you want to store the calculated walk direction.walkDirection
the recent motion vectors that you polled from the camera. This way it is posible for a character to move forward and to the left simultaneously, for example! player.setWalkDirection(walkDirection);+ +
+ Always use setWalkDirection()
to make a physics-controlled object move continuously, and the physics engine handles collision detection for you.
+
cam.setLocation(player.getPhysicsLocation());+
+
+Important: Again, do not use setLocalTranslation()
to walk the player around. You will get it stuck by overlapping with another physical object. You can put the player in a start position with setPhysicalLocation()
if you make sure to place it a bit above the floor and away from obstacles.
+
+
+You have learned how to load a "solid" physical scene model and walk around in it with a first-person perspective.
+You learned to speed up the physics calculations by using the CollisionShapeFactory to create efficient CollisionShapes for complex Geometries. You know how to add PhysicsControls to your collidable geometries and you register them to the PhysicsSpace. You also learned to use player.setWalkDirection(walkDirection)
to move collision-aware characters around, and not setLocalTranslation()
.
+
+Terrains are another type of scene in which you will want to walk around. Let's proceed with learning how to generate terrains now. + +
++ +Related info: +
+Previous: Hello Audio, -Next: Hello Physics
When you see one of the following in a game, then a particle system is likely behind it:
These things typically cannot be modeled by meshes. -In very simple terms:
Particle effects can be animated (e.g. sparks, drops) and static (strands of grass, hair). Non-particle effects include bloom/glow, and motion blur/afterimage. In this tutorial we will look at animated particles (com.jme3.effect).
package jme3test.helloworld; + ++ +JME 3 Tutorial (12) - Hello Effects
++ ++ ++ +Previous: Hello Audio, +Next: Hello Physics +
+ ++ +
+ ++When you see one of the following in a game, then a particle system is likely behind it: +
++
+ +- +
Fire, flames, sparks;+- +
Rain, snow, waterfalls, leaves;+- +
Explosions, debris, shockwaves;+- +
Dust, fog, clouds, smoke;+- +
Insects swarms, meteor showers;+- +
Magic spells.++ +These scene elements cannot be modeled by meshes. In very simple terms: +
++
+ +- +
The difference between an explosion and a dust cloud is the speed of the particle effect.+- +
The difference between flames and a waterfall is the direction and the color of the particle effect.++ +Particle effects can be animated (e.g. sparks, drops) and static (strands of grass, hair). Non-particle effects include bloom/glow, and motion blur/afterimage. In this tutorial you learn how to make animated particles (com.jme3.effect). +
+ +Sample Code
++package jme3test.helloworld; import com.jme3.app.SimpleApplication; import com.jme3.effect.ParticleEmitter; @@ -44,178 +70,308 @@ public class HelloEffects extends SimpleApplication { @Override public void simpleInitApp() { - ParticleEmitter fire = new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 30); - Material mat_red = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md"); - mat_red.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/flame.png")); + ParticleEmitter fire = + new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 30); + Material mat_red = new Material(assetManager, + "Common/MatDefs/Misc/Particle.j3md"); + mat_red.setTexture("Texture", assetManager.loadTexture( + "Effects/Explosion/flame.png")); fire.setMaterial(mat_red); - fire.setImagesX(2); fire.setImagesY(2); // 2x2 texture animation + fire.setImagesX(2); + fire.setImagesY(2); // 2x2 texture animation fire.setEndColor( new ColorRGBA(1f, 0f, 0f, 1f)); // red fire.setStartColor(new ColorRGBA(1f, 1f, 0f, 0.5f)); // yellow - fire.setInitialVelocity(new Vector3f(0, 2, 0)); + fire.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 2, 0)); fire.setStartSize(1.5f); fire.setEndSize(0.1f); - fire.setGravity(0); - fire.setLowLife(0.5f); + fire.setGravity(0, 0, 0); + fire.setLowLife(1f); fire.setHighLife(3f); - fire.setVelocityVariation(0.3f); + fire.getParticleInfluencer().setVelocityVariation(0.3f); rootNode.attachChild(fire); - ParticleEmitter debris = new ParticleEmitter("Debris", ParticleMesh.Type.Triangle, 10); - Material debris_mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md"); - debris_mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/Debris.png")); + ParticleEmitter debris = + new ParticleEmitter("Debris", ParticleMesh.Type.Triangle, 10); + Material debris_mat = new Material(assetManager, + "Common/MatDefs/Misc/Particle.j3md"); + debris_mat.setTexture("Texture", assetManager.loadTexture( + "Effects/Explosion/Debris.png")); debris.setMaterial(debris_mat); - debris.setImagesX(3); debris.setImagesY(3); // 3x3 texture animation + debris.setImagesX(3); + debris.setImagesY(3); // 3x3 texture animation debris.setRotateSpeed(4); debris.setSelectRandomImage(true); - debris.setInitialVelocity(new Vector3f(0, 4, 0)); - debris.setStartColor(new ColorRGBA(1f, 1f, 1f, 1f)); - debris.setGravity(6f); - debris.setVelocityVariation(.60f); + debris.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 4, 0)); + debris.setStartColor(ColorRGBA.White); + debris.setGravity(0, 6, 0); + debris.getParticleInfluencer().setVelocityVariation(.60f); rootNode.attachChild(debris); debris.emitAllParticles(); - } -}You should see an explosion that sends debris flying, and a fire. More example code
Texture Animation and Variation
+ +Start by choosing a material texture for your effect. If you provide the emitter with a set of textures (see image), it can use them either for variation (random order), or as animation steps (fixed order).
Setting emitter textures works just as you have already learned in previous chapters. This time we use the
Particle.j3md
default material. In the following example, we have a closer look at the Debris effect.... - Material debris_mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md"); - debris_mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/Debris.png")); +}+ ++You should see an explosion that sends debris flying, and a fire. + +
+ +Texture Animation and Variation
++ ++ + +
+ ++Start by choosing a material texture for your effect. If you provide the emitter with a set of textures (see image), it can use them either for variation (random order), or as animation steps (fixed order). +
+ ++Setting emitter textures works just as you have already learned in previous chapters. This time you base the material on the
+Particle.j3md
material definition. Let's have a closer look at the material for the Debris effect. +ParticleEmitter debris = + new ParticleEmitter("Debris", ParticleMesh.Type.Triangle, 10); + Material debris_mat = new Material(assetManager, + "Common/MatDefs/Misc/Particle.j3md"); + debris_mat.setTexture("Texture", assetManager.loadTexture( + "Effects/Explosion/Debris.png")); debris.setMaterial(debris_mat); - debris.setImagesX(3); // columns - debris.setImagesY(3); // rows + debris.setImagesX(3); + debris.setImagesY(3); // 3x3 texture animation debris.setSelectRandomImage(true); - ...
Load the texture in the emitter's material. Tell the Emitter into how many animation steps (x*y) the texture is divided. 1x1 is default. Optionally, tell the Emitter whether the animation steps are to be at random, or in order.As you see in the debris example, texture animations improve effects because each "flame" or "piece of debris" looks different. Also think of electric or magic effects, where you can create very interesting animations by using an ordered morphing series of lightning bolts; or flying leaves or snow flakes, for instance.
Default Particle Textures
The following effect textures are some of the example textures included in
test-data.jar
.
Texture Path Dimension Preview Effects/Explosion/Debris.png 3*3 Effects/Explosion/flame.png 2*2 Effects/Explosion/shockwave.png 1*1 Effects/Explosion/smoketrail.png 1*3 Effects/Smoke/Smoke.png 1*15 Copy them into you
assets/Effects
directory to use them.Creating Custom Textures
+ +For your game, you will likely create custom textures. Look at the fire example again.
ParticleEmitter fire = new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 30); - Material mat_red = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md"); - mat_red.setTexture("m_Texture", assetManager.loadTexture("Effects/Explosion/flame.png")); + ...++
+ +- +
Create a material and load the texture.+- +
Tell the Emitter into how many animation steps (x*y) the texture is divided.+
+The debris texture has 3x3 frames.- +
Optionally, tell the Emitter whether the animation steps are to be at random, or in order.+
+For the debris, the frames play at random.+ +As you see in the debris example, texture animations improve effects because each "flame" or "piece of debris" now looks different. Also think of electric or magic effects, where you can create very interesting animations by using an ordered morphing series of lightning bolts; or flying leaves or snow flakes, for instance. +
+ ++The fire material is created the same way, just using "Effects/Explosion/flame.png" texture, which has with 2x2 ordered animation steps. +
+ +Default Particle Textures
++ ++ ++ +The following particle textures included in
+test-data.jar
. You can copy and use them in your own effects. + ++ ++
+ +Texture Path Dimension Preview ++ +Effects/Explosion/Debris.png 3*3 + + +Effects/Explosion/flame.png 2*2 + + +Effects/Explosion/shockwave.png 1*1 + + +Effects/Explosion/smoketrail.png 1*3 + + +Effects/Smoke/Smoke.png 1*15 + + +Copy them into you
+ +assets/Effects
directory to use them. +Creating Custom Textures
++ ++ +For your game, you will likely create custom particle textures. Look at the fire example again. +
+ParticleEmitter fire = + new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 30); + Material mat_red = new Material(assetManager, + "Common/MatDefs/Misc/Particle.j3md"); + mat_red.setTexture("Texture", assetManager.loadTexture( + "Effects/Explosion/flame.png")); fire.setMaterial(mat_red); - fire.setImagesX(2); // columns - fire.setImagesY(2); // rows + fire.setImagesX(2); + fire.setImagesY(2); // 2x2 texture animation fire.setEndColor( new ColorRGBA(1f, 0f, 0f, 1f)); // red - fire.setStartColor(new ColorRGBA(1f, 1f, 0f, 0.5f)); // yellow
Black parts of the image will be rendered transparent. White/gray parts of the image are transparent and will be colored. You set the color (here, a gradient from yellow to red) in the code. By default the animation will be played in order (?) and loop.Create a grayscale texture in a graphic editor, and save it to your
assets/Effects
directory. If you split up one image file into x*y animation steps, make sure each animation step is of equal size–just as you see in the examples here.Emitter Parameters
A particle system is always centered around an emitter.
Use the
setShape()
method to change the EmitterShape:
EmitterPointShape(Vector3f.ZERO) – default shape EmitterSphereShape(Vector3f.ZERO,2f) EmitterBoxShape(new Vector3f(-1f,-1f,-1f),new Vector3f(1f,1f,1f))Example:
emitter.setShape(new EmitterPointShape(Vector3f.ZERO));
You create different effects by changing the emitter parameters:
Parameter Method (default value) Description number setNumParticles()
The maximum number of particles visible at the same time. Value is specified by user in constructor. velocity setInitialVelocity()
(Vector3f.ZERO)You must specify a vector how fast particles should move and it which start direction. direction setVelocityVariation()
(0.2f)
setFacingVelocity()
(false)
setRandomAngle()
(false)
setFaceNormal()
(Vector3f.NAN)
setRotateSpeed()
(0f)Optional accessors that control in which direction particles face when flying. lifetime setLowLife()
(3f)
setHighLife()
(7f)Minimum and maximum time period before particles fade. emission rate setParticlesPerSec()
(20)How many new particles are emitted per second. color setStartColor()
setEndColor()
(gray)Set to two different colors for gradient effects, or to same color. size setStartSize()
(0.2f)
setEndSize()
(2f)Set to two different values for shrink/grow effect, or to same size. gravity setGravity()
(0.1f)Whether particles falls down eventually. Set to 0f for zero-g effects. You can find details about effect parameters in the user guide. -Add and modify one paramter at a time, and try different values until you get the effect you want. Tip: Use the jMonkeyPlatform SceneComposer to preview effects settings (instructions: TODO).
Exercise
Can you "invert" the campfire into a small waterfall?
Change the Red and Yellow color to Cyan and Blue… Invert the velocity vector (direction) by using a negative number… Swap start and end size… Activate gravity by setting it to 1…Conclusion
- \ No newline at end of file + fire.setStartColor(new ColorRGBA(1f, 1f, 0f, 0.5f)); // yellow +You have learned that many different effects can be created by changing the parameters and textures of one general emitter object.
Now we move on to the next exciting chapter – the simulation of physical objects. Let's shoot some cannon balls at a brick wall!
+ +
+ ++Compare the texture with the resulting effect. +
+setStartColor()
and setEndColor()
.
+
+Create a grayscale texture in a graphic editor, and save it to your assets/Effects
directory. If you split up one image file into x*y animation steps, make sure each square is of equal size–just as you see in the examples here.
+
+ +A particle system is always centered around an emitter. +
+ +
+Use the setShape()
method to change the EmitterShape:
+
+Example: +
+emitter.setShape(new EmitterPointShape(Vector3f.ZERO));+ +
+You create different effects by changing the emitter parameters: + +
+Parameter | Method | Default | Description | +
---|---|---|---|
number | setNumParticles() | N/A | The maximum number of particles visible at the same time. Value is specified by user in constructor. This influences the density and length of the "trail". | +
velocity | getParticleInfluencer(). setInitialVelocity() | Vector3f.ZERO | Specify a vector how fast particles move and in which start direction. | +
direction | getParticleInfluencer(). setVelocityVariation() + setFacingVelocity() + setRandomAngle() + setFaceNormal() + setRotateSpeed() | 0.2f +false +false +Vector3f.NAN +0.0f | Optional accessors that control in which direction particles face while flying. | +
lifetime | setLowLife() + setHighLife() | 3f +7f | Minimum and maximum time period before particles fade. | +
emission rate | setParticlesPerSec() | 20 | How many new particles are emitted per second. | +
color | setStartColor() + setEndColor() | gray | Set to the same colors, or to two different colors for a gradient effect. | +
size | setStartSize() + setEndSize() | 0.2f +2f | Set to two different values for shrink/grow effect, or to same size for constant effect. | +
gravity | setGravity() | 0,1,0 | Whether particles fall down (positive) or fly up (negative). Set to 0f for a zero-g effect where particles keep flying. | +
+ +You can find details about effect parameters here. +Add and modify one parameter at a time, and try different values until you get the effect you want. +
+ ++
+ +Can you "invert" the fire effect into a small waterfall? Here some tips: +
++ +You have learned that many different effects can be created by changing the parameters and textures of one general emitter object. +
+ ++Now you move on to another exciting chapter – the simulation of . Let's shoot some cannon balls at a brick wall! + +
+Previous: Hello Update Loop, -Next: Hello Material
By default, SimpleApplication sets up an input system that allows you to steer the camera with the WASD keys, the arrow keys, and the mouse. You can use it as a flying first-person camera right away. But what if you need a third-person camera, or you want keys to trigger special game actions?
Every game has its custom keybindings, and this tutorial explains how you define them. We first define the key presses and mouse events, and then we define the actions they should trigger.
package jme3test.helloworld; + +JME 3 Tutorial (5) - Hello Input System
++ ++ ++ +Previous: Hello Update Loop, +Next: Hello Material +
+ ++By default, SimpleApplication sets up an input system that allows you to steer the camera with the WASD keys, the arrow keys, and the mouse. You can use it as a flying first-person camera right away. But what if you need a third-person camera, or you want keys to trigger special game actions? +
+ ++Every game has its custom keybindings, and this tutorial explains how you define them. We first define the key presses and mouse events, and then we define the actions they should trigger. +
+ +Sample Code
++package jme3test.helloworld; import com.jme3.app.SimpleApplication; import com.jme3.material.Material; @@ -82,49 +97,109 @@ public class HelloInput extends SimpleApplication { } } }; -}Build and run the example.
Press the Spacebar or click to rotate the cube. Press the J and K keys to move the cube. Press P to pause and unpause the game. While paused, the game should not respond to any input, other thanP
.Defining Mappings and Triggers
+ +First you register each mapping name with its trigger(s). Remember the following:
The trigger can be a key press or mouse action. The mapping name is a string that you can choose.
The name should describe the action, not the trigger. One named mapping can have several triggers.
The "Rotate" action can be triggered by a click and by pressing the spacebar.Have a look at the code:
You register the mapping named "Rotate" to the Spacebar trigger
new KeyTrigger(KeyInput.KEY_SPACE)
). In the same line, you also register "Rotate" to the mouse button trigger
new MouseButtonTrigger(MouseInput.BUTTON_LEFT)
You map thePause, Left, Right
mappings to the P, J, K keys, respectively. You register the (on/off) pause action to the ActionListener You register the (gradual) movement actions to the AnalogListener// You can map one or several inputs to one named action +}+ ++Build and run the example. +
++
+ +- +
Press the Spacebar or click to rotate the cube.+- +
Press the J and K keys to move the cube.+- +
Press P to pause and unpause the game. While paused, the game should not respond to any input, other than+P
.Defining Mappings and Triggers
++ ++ +First you register each mapping name with its trigger(s). Remember the following: +
++
+ +- +
An input trigger can be a key press or mouse action.+
+For example a mouse movement, a mouse click, or pressing the letter "P".- +
The mapping name is a string that you can choose.+
+The name should describe the action (e.g. "Rotate"), and not the trigger. Because the trigger can change.- +
One named mapping can have several triggers.+
+For example, the "Rotate" action can be triggered by a click and by pressing the spacebar.+ +Have a look at the code: +
++
+- +
You register the mapping named "Rotate" to the Spacebar key trigger.+
+new KeyTrigger(KeyInput.KEY_SPACE)
).- +
In the same line, you also register "Rotate" to an alternative mouse click trigger.+
+new MouseButtonTrigger(MouseInput.BUTTON_LEFT)
- +
You map the+Pause
,Left
,Right
mappings to the P, J, K keys, respectively.// You can map one or several inputs to one named action inputManager.addMapping("Pause", new KeyTrigger(KeyInput.KEY_P)); inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_J)); inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_K)); inputManager.addMapping("Rotate", new KeyTrigger(KeyInput.KEY_SPACE), - new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); - // Add the names to the action listener. + new MouseButtonTrigger(MouseInput.BUTTON_LEFT));+ ++ +Now you need to register your trigger mappings. +
++
+- +
You register the pause action to the ActionListener, because it is an "on/off" action.+- +
You register the movement actions to the AnalogListener, because they are gradual actions.+// Add the names to the action listener. inputManager.addListener(actionListener, new String[]{"Pause"}); - inputManager.addListener(analogListener, new String[]{"Left", "Right", "Rotate"});This code usually goes into the
simpleInitApp()
method. But since we will likely add many keybindings, we extract these lines and wrap them in an auxiliary method,initKeys()
. TheinitKeys()
method is not part of the Input Controls interface – so you can name it whatever you like. Just don't forget to call your method from theinitSimpleApp()
method.Implementing the Actions
+ +Now you have mapped action names to input triggers. Now you specify the actions themselves.
The two important methods here are the
ActionListener
with itsonAction()
method, and theAnalogListener
with itsonAnalog()
method. In these two methods, you test for each named mapping, and call the game action you want to trigger.In this example, we set the following mappings:
The Rotate mapping triggers the actionplayer.rotate(0, value, 0)
. The Left and Right mappings increase and decrease the player's x coordinate. The Pause mapping flips a booleanisRunning
. We also want to check the booleanisRunning
before any action (other than unpausing) is executed.private ActionListener() { + inputManager.addListener(analogListener, new String[]{"Left", "Right", "Rotate"});+ ++This code goes into the
+ +simpleInitApp()
method. But since we will likely add many keybindings, we extract these lines and wrap them in an auxiliary method,initKeys()
. TheinitKeys()
method is not part of the Input Controls interface – you can name it whatever you like. Just don't forget to call your method from theinitSimpleApp()
method. +Implementing the Actions
++ ++ +You have mapped action names to input triggers. Now you specify the actions themselves. +
+ ++The two important methods here are the
+ +ActionListener
with itsonAction()
method, and theAnalogListener
with itsonAnalog()
method. In these two methods, you test for each named mapping, and call the game action you want to trigger. ++In this example, we trigger the following actions: + +
++
+- +
The Rotate mapping triggers the action+player.rotate(0, value, 0)
.- +
The Left and Right mappings increase and decrease the player's x coordinate.+- +
The Pause mapping flips a boolean+isRunning
.- +
We also want to check the boolean+isRunning
before any action (other than unpausing) is executed.private ActionListener() { public void onAction(String name, boolean keyPressed, float tpf) { if (name.equals("Pause") && !keyPressed) { isRunning = !isRunning; @@ -150,76 +225,148 @@ class="li"> We also want to check the booleanisRunning
before any System.out.println("Press P to unpause."); } } - };It's okay to use only one of the two Listeners, and not implement the other one, if you are not using this type of interaction. In the following, we have a closer look how to decide which of the two listeners is best suited for which situation.
Analog, Pressed, or Released?
+ +Technically, every input can be an "Analog" or a "on-off Action". Here is how you find out which listener is the right one for which type of input.
Mappings registered to the AnalogListener are triggered repeatedly and gradually.
Parameters:
JME gives you access to the name of the triggered action. JME gives you access to a gradual value between 0-9 how long the key has been pressed. Example: Navigational events (e.g. Left, Right, Rotate, Run, Strafe), situations where you interact continuously.Mappings registered to the ActionListener are treated in an absolute way – "Pressed or released? On or off?"
Parameters:
JME gives you access to the name of the triggered action. JME gives you access to a boolean whether the key is pressed or not. Example: Pause button, shooting, selecting, jumping, one-time click interactions.Tip: It's very common that you want an action to be only triggered once, in the moment when the key is released. For instance when opening a door, flipping a boolean state, or picking up an item. To achieve that, you use an
ActionListener
and test for… && !keyPressed
. For an example, look at the Pause button code.if (name.equals("Pause") && !keyPressed) { + };+ ++It's okay to use only one of the two Listeners, and not implement the other one, if you are not using this type of interaction. In the following, we have a closer look how to decide which of the two listeners is best suited for which situation. +
+ +Analog, Pressed, or Released?
++ ++ +Technically, every input can be either an "analog" or a "digital" action. Here is how you find out which listener is the right one for which type of input. +
+ ++Mappings registered to the AnalogListener are triggered repeatedly and gradually. +
++
+ +- +
Parameters:++
+- +
JME gives you access to the name of the triggered action.+- +
JME gives you access to a gradual value how long the key has been pressed.+- +
Example: Navigational events (e.g. Left, Right, Rotate, Run, Strafe), situations where you interact continuously.++ +Mappings registered to the ActionListener are digital either-or actions – "Pressed or released? On or off?" +
++
+ +- +
Parameters:++
+- +
JME gives you access to the name of the triggered action.+- +
JME gives you access to a boolean whether the key is pressed or not.+- +
Example: Pause button, shooting, selecting, jumping, one-time click interactions.++ +Tip: It's very common that you want an action to be only triggered once, in the moment when the key is released. For instance when opening a door, flipping a boolean state, or picking up an item. To achieve that, you use an
+ActionListener
and test for… && !keyPressed
. For an example, look at the Pause button code: +if (name.equals("Pause") && !keyPressed) { isRunning = !isRunning; - }Table of Triggers
You can find the list of input constants in the files
src/core/com/jme3/input/KeyInput.java
,JoyInput.java
, andMouseInput.java
. Here is an overview of the most common triggers constants:
Trigger Code Mouse button: Left Click MouseButtonTrigger(MouseInput.BUTTON_LEFT) Mouse button: Right Click MouseButtonTrigger(MouseInput.BUTTON_RIGHT) Keyboard: Characters and Numbers KeyTrigger(KeyInput.KEY_X) Keyboard: Spacebar KeyTrigger(KeyInput.KEY_SPACE) Keyboard: Return, Enter KeyTrigger(KeyInput.KEY_RETURN), KeyTrigger(KeyInput.KEY_NUMPADENTER) Keyboard: Escape KeyTrigger(KeyInput.KEY_ESCAPE) Keyboard: Arrows KeyTrigger(KeyInput.KEY_UP), KeyTrigger(KeyInput.KEY_DOWN)
KeyTrigger(KeyInput.KEY_LEFT), KeyTrigger(KeyInput.KEY_RIGHT)Tip: If you don't recall an input constant during development, you benefit from an IDE's code completion functionality: Place the caret after e.g.
KeyInput.|
and trigger code completion to select possible input identifiers.Exercises
+ + + +
Add mappings for moving the player (box) up and down with the H and L keys. Modify the mappings so that you can also trigger the up an down motion with the mouse scroll wheel.
Tip: Usenew MouseAxisTrigger(MouseInput.AXIS_WHEEL, true)
In which situation would it be better to use variables for the MouseInput/KeyInput definitions?+ +int usersPauseKey = KeyInput.KEY_P; + }+ +Table of Triggers
++ ++ ++ +You can find the list of input constants in the files
+src/core/com/jme3/input/KeyInput.java
,JoyInput.java
, andMouseInput.java
. Here is an overview of the most common triggers constants: + ++ ++
+ +Trigger Code ++ +Mouse button: Left Click MouseButtonTrigger(MouseInput.BUTTON_LEFT) ++ +Mouse button: Right Click MouseButtonTrigger(MouseInput.BUTTON_RIGHT) ++ +Keyboard: Characters and Numbers KeyTrigger(KeyInput.KEY_X) ++ +Keyboard: Spacebar KeyTrigger(KeyInput.KEY_SPACE) ++ +Keyboard: Return, Enter KeyTrigger(KeyInput.KEY_RETURN), KeyTrigger(KeyInput.KEY_NUMPADENTER) ++ +Keyboard: Escape KeyTrigger(KeyInput.KEY_ESCAPE) ++ +Keyboard: Arrows KeyTrigger(KeyInput.KEY_UP), KeyTrigger(KeyInput.KEY_DOWN) +
+KeyTrigger(KeyInput.KEY_LEFT), KeyTrigger(KeyInput.KEY_RIGHT)+ +Tip: If you don't recall an input constant during development, you benefit from an IDE's code completion functionality: Place the caret after e.g.
+ +KeyInput.|
and trigger code completion to select possible input identifiers. +Exercises
+++
- +
Add mappings for moving the player (box) up and down with the H and L keys!+- +
Modify the mappings so that you can also trigger the up an down motion with the mouse scroll wheel!++
+- +
Tip: Use+new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true)
In which situation would it be better to use variables instead of literals for the MouseInput/KeyInput definitions?int usersPauseKey = KeyInput.KEY_P; ... -inputManager.addMapping("Pause", new KeyTrigger(usersPauseKey));Conclusion
- \ No newline at end of file +inputManager.addMapping("Pause", new KeyTrigger(usersPauseKey)); +You now how to add custom interactions to your game: You know now that you first have to define the key mappings, and then the actions for each mapping. You have learned to respond to mouse events and to the keyboard. You understand the difference between "analog" (gradually repeated) and "digital" (on/off) inputs.
Now you can already write a little interactive game! But wouldn't it be cooler if these old boxes were a bit more fancy? Let's continue with learning about materials.
+ +You are now able to add custom interactions to your game: You know that you first have to define the key mappings, and then the actions for each mapping. You have learned to respond to mouse events and to the keyboard. You understand the difference between "analog" (gradually repeated) and "digital" (on/off) inputs. +
+ ++Now you can already write a little interactive game! But wouldn't it be cooler if these old boxes were a bit more fancy? Let's continue with learning about materials. +
+ + +Previous: Hello Assets, -Next: Hello Input System
Now that you know how to load assets such as 3-D models, you want them to implement the actual gameplay. In this tutorial we look at the update loop. The update loop of your game is where the action happens.
package jme3test.helloworld; + ++ +JME 3 Tutorial (4) - Hello Update Loop
++ ++ ++ +Previous: Hello Assets, +Next: Hello Input System +
+ ++Now that you know how to load assets, such as 3D models, you want to implement some gameplay that uses these assets. In this tutorial we look at the update loop. The update loop of your game is where the action happens. +
+ +Code Sample
++package jme3test.helloworld; import com.jme3.app.SimpleApplication; import com.jme3.material.Material; @@ -30,7 +41,8 @@ public class HelloLoop extends SimpleApplication { Box(Vector3f.ZERO, 1, 1, 1); player = new Geometry("blue cube", b); - Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + Material mat = new Material(assetManager, + "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", ColorRGBA.Blue); player.setMaterial(mat); rootNode.attachChild(player); @@ -40,52 +52,147 @@ public class HelloLoop extends SimpleApplication { @Override public void simpleUpdate(float tpf) { // make the player rotate - player.rotate(0, 2*tpf, 0); + player.rotate(0, 2*tpf, 0); } -}Build and run the file: You see a constantly rotating cube.
Understanding the Code
Compared to our previous code samples you note that the player Geometry is now a class field. This is because we want the update loop to be able to access and transform this Geometry. As you can see, we initialize the player object in the
simpleInitApp()
method.Now have a closer look at the
simpleUpdate()
method, this is the update loop.
Theplayer.rotate(0, 2*tpf, 0);
line changes the rotation of the player object. We use thetpf
variable ("time per frame") to time this action depending on the current frames per second rate. This means the cube rotates with the same speed on fast machines, and the game remains playable.The Update Loop
A rotating object is just a simple example. In the update loop, you can update score and health points, check for collisions, make enemies calculate their next move, roll the dice whether a trap has been set off, play random ambient sounds, and much more.
The update loop starts after thesimpleInitApp()
method has set up the scenegraph and state variables. JME executes everything in thesimpleUpdate()
method repeatedly, as fast as possible.
Use the loop to poll the game state and initiate actions. Use the loop to trigger reactions and update the game state. Use it wisely, because having too many calls in the loop also slows down the game.Exercises
Here are some fun things to try:
What happens if you give the rotate() method negative numbers? Can you create two models next to each other, and make one rotate twice as fast as the other? (use thetpf
) Can you make a cube that pulsates? (grows and shrinks) Can you make a rolling cube? (spin around the x axis, and translate along the z axis)Look back at the Hello Node tutorial if you do not remember the transformation methods for scaling, translating, and rotating.
Conclusion
- \ No newline at end of file +}Now you are listening to the update loop, "the heart beat" of the game, and you can add all kinds of action to it.
The next thing the game needs is some interaction! Continue learning how to respond to user input.
See also: Advanced jME3 developers additionally use Application States and Custom Controls to implement more complex mechanics in their game loops. You will come across these topics later when you proceed to advanced documentation.
+Build and run the file: You see a constantly rotating cube. +
+ +
+
+Compared to our previous code samples you note that the player Geometry is now a class field. This is because we want the update loop to be able to access and transform this Geometry. As usual, we initialize the player object in the simpleInitApp()
method.
+
+Now have a closer look at the simpleUpdate()
method – this is the update loop.
+
player.rotate(0, 2*tpf, 0);
line changes the rotation of the player object. tpf
variable ("time per frame") to time this action depending on the current frames per second rate. This simply means that the cube rotates with the same speed on fast and slow machines, and the game remains playable.+ +A rotating object is just a simple example. In the update loop, you typically have many tests and trigger various game actions. This is were you update score and health points, check for collisions, make enemies calculate their next move, roll the dice whether a trap has been set off, play random ambient sounds, and much more. + +
+simpleUpdate()
method starts running after the simpleInitApp()
method has initialized the scene graph and state variables.simpleUpdate()
method repeatedly, as fast as possible.
+
+Note the contrast: The simpleUpdate()
method runs repeatedly, while the simpleInitApp()
method is executed only once, right at the beginning. These two are the most important methods in a SimpleApplication-derived game. From these two methods, you create other class instances (your game data) and change their properties (your game state).
+
simpleInitApp()
is the application's "first breath".simpleUpdate()
is the application's heartbeat.+Basically everything in your game happens in either one or the other method. This means that these methods grow very long over time. There are two strategies how advanced developers can spread out their init and update code over several Java classes: +
++Keep this in mind for later when your game application grows. +
+ ++ +Here are some fun things to try: +
+tpf
variable)+ +Look back at the Hello Node tutorial if you do not remember the transformation methods for scaling, translating, and rotating. +
+ ++ +Now you are listening to the update loop, "the heart beat" of the game, and you can add all kinds of action to it. +
+ ++The next thing the game needs is some interaction! Continue learning how to respond to user input. +
++See also: Advanced jME3 developers additionally use Application States and Custom Controls to implement more complex mechanics in their game loops. You will come across these topics later when you proceed to advanced documentation. +
+Previous: Hello Input System, -Next: Hello Animation When we speak of Materials, we mean everything that influences what the surface of a 3D model looks like: The color, texture, and material (shininess, opacity/transparency). Simple coloring is covered in Hello Node. Loading models that come with materials is covered in Hello Asset. Here we focus on using and creating JME3 Material Definitions.
package jme3test.helloworld; + +JME 3 Tutorial (6) - Hello Materials
++ ++ ++Previous: Hello Input System, +Next: Hello Animation +
+ ++The term Material includes everything that influences what the surface of a 3D model looks like: The color, texture, shininess, and opacity/transparency. Plain coloring is covered in Hello Node. Loading models that come with materials is covered in Hello Asset. In this tutorial you learn to create and use custom JME3 Material Definitions. + +
+ +Sample Code
++package jme3test.helloworld; + import com.jme3.app.SimpleApplication; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; @@ -22,14 +30,17 @@ import com.jme3.scene.shape.Sphere; import com.jme3.texture.Texture; import com.jme3.util.TangentBinormalGenerator; import com.jme3.renderer.queue.RenderQueue.Bucket; + /** Sample 6 - how to give an object's surface a material and texture. * How to make objects transparent, or let colors "leak" through partially * transparent textures. How to make bumpy and shiny surfaces. */ + public class HelloMaterial extends SimpleApplication { public static void main(String[] args) { HelloMaterial app = new HelloMaterial(); app.start(); } + @Override public void simpleInitApp() { /** A simple textured cube -- in good MIP map quality. */ @@ -40,227 +51,394 @@ public class HelloMaterial extends SimpleApplication { mat_stl.setTexture("ColorMap", tex_ml); cube.setMaterial(mat_stl); rootNode.attachChild(cube); + /** A translucent/transparent texture, similar to a window frame. */ Box(new Vector3f(0f,0f,0f), 1f,1f,0.01f); Geometry window_frame = new Geometry("window frame", boxshape3); Material mat_tt = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat_tt.setTexture("ColorMap", - assetManager.loadTexture("Textures/ColoredTex/Monkey.png")); + mat_tt.setTexture("ColorMap", assetManager.loadTexture("Textures/ColoredTex/Monkey.png")); mat_tt.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); window_frame.setMaterial(mat_tt); + /** Objects with transparency need to be in the render bucket for transparent objects: */ window_frame.setQueueBucket(Bucket.Transparent); rootNode.attachChild(window_frame); + /** A cube with base color "leaking" through a partially transparent texture */ Box(new Vector3f(3f,-1f,0f), 1f,1f,1f); Geometry cube_leak = new Geometry("Leak-through color cube", boxshape4); Material mat_tl = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat_tl.setTexture("ColorMap", - assetManager.loadTexture("Textures/ColoredTex/Monkey.png")); + mat_tl.setTexture("ColorMap", assetManager.loadTexture("Textures/ColoredTex/Monkey.png")); mat_tl.setColor("Color", new ColorRGBA(1f,0f,1f, 1f)); // purple cube_leak.setMaterial(mat_tl); rootNode.attachChild(cube_leak); + /** A bumpy rock with a shiny light effect */ Sphere rock = new Sphere(32,32, 2f); Geometry shiny_rock = new Geometry("Shiny rock", rock); rock.setTextureMode(Sphere.TextureMode.Projected); // better quality on spheres TangentBinormalGenerator.generate(rock); // for lighting effect Material mat_lit = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); - mat_lit.setTexture("DiffuseMap", - assetManager.loadTexture("Textures/Terrain/Pond/Pond.png")); - mat_lit.setTexture("NormalMap", - assetManager.loadTexture("Textures/Terrain/Pond/Pond_normal.png")); - mat_lit.setFloat("Shininess", 5f); // [0,128] + mat_lit.setTexture("DiffuseMap", assetManager.loadTexture("Textures/Terrain/Pond/Pond.jpg")); + mat_lit.setTexture("NormalMap", assetManager.loadTexture("Textures/Terrain/Pond/Pond_normal.png")); + mat_lit.setFloat("Shininess", 5f); // [1,128] shiny_rock.setMaterial(mat_lit); shiny_rock.setLocalTranslation(0,2,-2); // Move it a bit shiny_rock.rotate(1.6f, 0, 0); // Rotate it a bit rootNode.attachChild(shiny_rock); + /** Must add a light to make the lit object visible! */ DirectionalLight sun = new DirectionalLight(); sun.setDirection(new Vector3f(1,0,-2).normalizeLocal()); sun.setColor(ColorRGBA.White); rootNode.addLight(sun); } -}
You should see
Left – A cube with a brown monkey texture. Middle – A translucent monkey picture in front of a shiny rock. Right – A cube with a purple monkey texture.Move around with the WASD keys to have a closer look at the translucency, and the rock's bumpiness.
Simple Unshaded Texture
+ +Typically you want to give objects in your scene textures: It can be rock, grass, brick, wood, water, metal, paper… A texture is a normal image file in JPG or PNG format. In this example, we create a box with a simple unshaded Monkey texture as material.
/** A simple textured cube. */ +}+ ++You should see +
++
+ +- +
Left – A cube with a brown monkey texture.+- +
Middle – A translucent monkey picture in front of a shiny rock.+- +
Right – A cube with a purple monkey texture.++Move around with the WASD keys to have a closer look at the translucency, and the rock's bumpiness. +
+ +Simple Unshaded Texture
++ ++ +Typically you want to give objects in your scene textures: It can be rock, grass, brick, wood, water, metal, paper… A texture is a normal image file in JPG or PNG format. In this example, you create a box with a simple unshaded Monkey texture as material. +
+/** A simple textured cube. */ Box(new Vector3f(-3f,1.1f,0f), 1f,1f,1f); Geometry cube = new Geometry("My Textured Box", boxshape1); - Material mat_stl = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + Material mat_stl = new Material(assetManager, + "Common/MatDefs/Misc/Unshaded.j3md"); Texture tex_ml = assetManager.loadTexture("Interface/Logo/Monkey.jpg"); mat_stl.setTexture("ColorMap", tex_ml); cube.setMaterial(mat_stl); - rootNode.attachChild(cube);Here is what we did:
Create a Geometry from a mesh. This geometry is a cube. Create a Material based on jME3's defaultUnshaded.j3md
material definition. Create a texture from theMonkey.jpg
file and load it into the material.
The ColorMap is the material layer where textures go. Apply the material to the cube and attach the cube to the rootnode.Transparent Unshaded Texture
+ +
Monkey.png
is the same texture asMonkey.jpg
, but with an added alpha channel. The alpha channel allows you to specify which areas of the texture you want to be translucent: Black areas remain opaque, gray areas become translucent, and white areas become transparent. In combination with setting the texture blend mode toBlendMode.Alpha
, this results in a partially translucent/transparent texture! -You also need to set the render bucket of the object with the translucent texture toBucket.Transparent
. This ensures that the translucent object is drawn on top of objects behind it, and they show up correctly under the translucent parts. For non-translucent objects the drawing order is not so important, because the z-buffer keeps track of whether a pixel is behind something else or not, and the color of a pixel doesn't depend on the pixels under it, so they can be drawn in any order./** A translucent/transparent texture. */ + rootNode.attachChild(cube);+ ++Here is what we did: +
++
+ +- +
Create a Geometry from a Box mesh. Let's call it+cube
.- +
Create a Material based on jME3's default+Unshaded.j3md
material definition.- +
Create a texture from the+Monkey.jpg
file and load it into the material.
+The ColorMap is the typical material layer where textures go.- +
Apply the material to the cube, and attach the cube to the rootnode.+Transparent Unshaded Texture
++ ++ +
+ +Monkey.png
is the same texture asMonkey.jpg
, but with an added alpha channel. The alpha channel allows you to specify which areas of the texture you want to be opaque or transparent: Black areas remain opaque, gray areas become translucent, and white areas become transparent. ++For a partially translucent/transparent texture, you need: +
++
+- +
A texture with alpha channel+- +
A Texture blend mode of+BlendMode.Alpha
- +
A geometry in the+Bucket.Transparent
render bucket. This bucket ensures that the translucent object is drawn on top of objects behind it, and they show up correctly under the translucent parts. (For non-translucent objects the drawing order is not so important, because the z-buffer keeps track of whether a pixel is behind something else or not, and the color of a pixel doesn't depend on the pixels under it, this is why opaque Geometries can be drawn in any order.)/** A translucent/transparent texture. */ Box(new Vector3f(0f,0f,0f), 1f,1f,0.01f); Geometry seethrough = new Geometry("see-through box", boxshape3); Material mat_tt = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat_tt.setTexture("ColorMap", - assetManager.loadTexture("Textures/ColoredTex/Monkey.png")); + mat_tt.setTexture("ColorMap", assetManager.loadTexture("Textures/ColoredTex/Monkey.png")); mat_tt.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); // activate transparency seethrough.setMaterial(mat_tt); seethrough.setQueueBucket(Bucket.Transparent); - rootNode.attachChild(seethrough);What we did it the same as before, with only one added step for the transparency.
Create a Geometry from a mesh. This Geometry is flat upright box. Create a Material based on jME3's defaultUnshaded.j3md
material definition. Create a texture from theMonkey.png
file and load it into the material.
The ColorMap is the material layer where textures go. The PNG file must have an alpha layer. Activate transparency in the material by setting the blend mode to Alpha! Apply the material to the cube. Set the QueueBucket of the cube toBucket.Transparent
to ensure that the translucent objects is drawn after objects behind it. Attach the cube to the rootnode.Tip: Learn more about creating PNG images with an alpha layer in the help system of your graphic editor.
Shininess and Bumpiness
+ +But textures are not all. Have a look at the shiny bumpy sphere again – you cannot get such a nice material with just a texture. We will have a quick look at some advanced jme features here – lit materials: -In a lit material, the standard texture layer is refered to as Diffuse Map, any material can have it. A lit material can additionally have lighting effects such as Shininess used together with the Specular Map layer, and even a realistically bumpy or cracked surface with help of the Normal Map layer. -Let's have a look at the part of the code example where you create the shiny bumpy rock.
+ +
Create a Geometry from a mesh. This Geometrx is a normal smooth sphere.Sphere rock = new Sphere(32,32, 2f); - Geometry shiny_rock = new Geometry("Shiny rock", rock);
(Only for Spheres) Change the sphere's TextureMode to make the square texture project better onto the sphere.rock.setTextureMode(Sphere.TextureMode.Projected); You generate TangentBinormals to enable rendering the bumpiness (stored in the NormalMap) of the texture.TangentBinormalGenerator.generate(rock); Create a material based on theLighting.j3md
default material.Material mat_lit = new Material(assetManager, - "Common/MatDefs/Light/Lighting.j3md");
Set the material's Shininess to a value between 0 and 127.mat_lit.setFloat("Shininess", 5f); // [0,128] Assign your newly created material to the rock.shiny_rock.setMaterial(mat_lit);- +
Let's move and rotate the geometry a bit to position it better.+ +shiny_rock.setLocalTranslation(0,2,-2); // Move it a bit + rootNode.attachChild(seethrough);+ ++What you did is the same as before, with only one added step for the transparency. +
++
+ +- +
Create a Geometry from a mesh. This Geometry is flat upright box.+- +
Create a Material based on jME3's default+Unshaded.j3md
material definition.- +
Create a texture from the+Monkey.png
file and load it into the material.
+The ColorMap is the material layer where textures go. This PNG file must have an alpha layer.- +
Activate transparency in the material by setting the blend mode to Alpha!+- +
Apply the material to the Geometry.+- +
Set the QueueBucket of the Geometry to+Bucket.Transparent
.- +
Attach the cube to the rootnode.++ +Tip: Learn more about creating PNG images with an alpha layer in the help system of your graphic editor. +
+ +Shininess and Bumpiness
++ ++ +But textures are not all. Have a close look at the shiny sphere – you cannot get such a nice bumpy material with just a texture. JME3 also supports so-called Phong-illuminated materials: +
+ ++In a lit material, the standard texture layer is refered to as Diffuse Map, any material can use this layer. A lit material can additionally have lighting effects such as Shininess used together with the Specular Map layer, and even a realistically bumpy or cracked surface with help of the Normal Map layer. +
+ ++Let's have a look at the part of the code example where you create the shiny bumpy rock. +
++
- +
Create a Geometry from a Sphere shape. Note that this shape is a normal smooth sphere mesh.+Sphere rock = new Sphere(32,32, 2f); + Geometry shiny_rock = new Geometry("Shiny rock", rock);++
+- +
(Only for Spheres) Change the sphere's TextureMode to make the square texture project better onto the sphere.+rock.setTextureMode(Sphere.TextureMode.Projected);+- +
You generate TangentBinormals for the sphere mesh so you can use the NormalMap layer of the texture.+TangentBinormalGenerator.generate(rock);+- +
Create a material based on the+Lighting.j3md
default material.Material mat_lit = new Material(assetManager, + "Common/MatDefs/Light/Lighting.j3md");++
+- +
Set a standard rocky texture in the+DiffuseMap
layer.
+mat_lit.setTexture("DiffuseMap", assetManager.loadTexture( + "Textures/Terrain/Pond/Pond.jpg"));+- +
Set the+NormalMap
layer that contains the bumpiness. The NormalMap was generated for this particular DiffuseMap with a special tool (e.g. Blender).mat_lit.setTexture("NormalMap", assetManager.loadTexture( + "Textures/Terrain/Pond/Pond_normal.png"));+- +
Set the Material's Shininess to a value between 1 and 128. For a rock, a low fuzzy shininess is appropriate.+mat_lit.setFloat("Shininess", 5f); // [1,128]+- +
Assign your newly created material to the Geometry.+shiny_rock.setMaterial(mat_lit);+ Let's move and rotate the geometry a bit to position it better.shiny_rock.setLocalTranslation(0,2,-2); // Move it a bit shiny_rock.rotate(1.6f, 0, 0); // Rotate it a bit - rootNode.attachChild(shiny_rock); Note that any lighting material requires a light source.Default Material Definitions
As you have seen, the following default materials can always be found in
jme/core-data/Common
.
Default Definition Usage Parameters Common/MatDefs/Misc/Unshaded.j3md
Textured: Use with mat.setTexture() and TextureKey.
Colored: Use with mat.setColor() and RGBAColor.ColorMap : Texture. Common/MatDefs/Light/Lighting.j3md
Use with shiny Textures, Bump- and NormalMaps textures.
Requires a light source.Ambient, Diffuse, Specular : Color
DiffuseMap, NormalMap, SpecularMap : Texture
Shininess : FloatIn a real game, you will create your custom Materials based these existing ones – as you have just seen in the example with the shiny rock.
Exercises
Exercise 1: Custom Materials
+Look at the purple leak-through sample above again. It takes four lines to create and set the Material.
Note how it loads theUnshaded.j3md
Material definition. Note how it sets toColor
parameter to purple (new ColorRGBA(1f,0f,1f,1f)
). Note how it sets theColorMap
to a texture path.If you want to use one custom material for several models, you can store it in a .j3m file, and save a few lines of code every time. Here is an example: -Create a file
assets/Materials/LeakThrough.j3m
with the following content:Material Leak Through : Common/MatDefs/Misc/Unshaded.j3md { + rootNode.attachChild(shiny_rock);++ +Remember that any Lighting.j3md-based material requires a light source, as shown in the full code sample above. +
+ ++Tip: To deactivate Shininess, do not set
+ +Shininess
to 0, but instead set theSpecular
color toColorRGBA.Black
. +Default Material Definitions
++ ++ ++ +As you have seen, you can find the following default materials in
+jme/core-data/Common/…
. + ++ ++
+ +Default Definition Usage Parameters ++ +Common/MatDefs/Misc/Unshaded.j3md
Colored: Use with mat.setColor() and ColorRGBA.
+Textured: Use with mat.setTexture() and Texture.Color : Color +
+ColorMap : Texture2D+ +Common/MatDefs/Light/Lighting.j3md
Use with shiny Textures, Bump- and NormalMaps textures.
+Requires a light source.Ambient, Diffuse, Specular : Color +
+DiffuseMap, NormalMap, SpecularMap : Texture2D
+Shininess : Float+ +For a game, you create custom Materials based on these existing MaterialDefintions – as you have just seen in the example with the shiny rock's material. +
+ +Exercises
++ ++ +Exercise 1: Custom .j3m Material
++ +++ +Look at the purple leak-through sample above again. It takes four lines to create and set the Material. +
++
+ +- +
Note how it loads the+Unshaded.j3md
Material definition.- +
Note how it sets the+Color
parameter to purple (new ColorRGBA(1f,0f,1f,1f)
).- +
Note how it sets the+ColorMap
to a texture path.+If you want to use one custom material for several models, you can store it in a .j3m file, and save a few lines of code every time. +You create a j3m file as follows: +
++
Create a fileassets/Materials/LeakThrough.j3m
with the following content:Material Leak Through : Common/MatDefs/Misc/Unshaded.j3md { MaterialParameters { Color : 1 0 1 1 ColorMap : Textures/ColoredTex/Monkey.png } -}
Note thatMaterial
is a keyword. Note thatLeak Through
is a name that you can choose. Note how it sets the same three properties, Color, ColorMap, and Unshaded.j3md.Using this new custom material
LeakThrough.j3m
only takes one line.
In the code sample, comment out the three lines withmat_tl
in them. Below them, add the following line:cube_leak.setMaterial((Material) assetManager.loadAsset( "Materials/LeakThrough.j3m")); Run the app. The result is the same.You have replaced the three lines of an on-the-fly material definition with one line that loads a custom material from a file. This method is very handy if you use the same material often.
Exercise 2: Bumpiness and Shininess
Go back to the bumpy rock sample above:
Comment out the DiffuseMap line, and run the app. (Uncomment it again.) Comment out the NormalMap line, and run the app. (Uncomment it again.)
Compare: Which property of the rock is lost in either case? Change the value of Shininess to values like 0, 63, 127.
What aspect of the Shininess changes?Conclusion
- \ No newline at end of file +} +You have learned how to create a Material, specify its properties, and use it on a Geometry. You know how to load an image file (.png, .jpg) as texture into a material. You know to save texture files in a subfolder of your project's
assets/Textures/
directory. -You have also learned that a material can be stored in a .j3m file. The file references a built-in Material Definition and specifies values for properties of that MaterialDefinition. You know to save your custom .j3m files in your project'sassets/Materials/
directory. -Now that you know how to load models and how to assign good-looking materials to them, let's have a look at how to animate models in the next chapter, Hello Animation.See also
Materials forum thread Various Material screenshots (Not done with JME3, this is just to show the fantastic range of Material parameters in the hands of an expert, until we have a JME3 demo for it.)
Material
is a fixed keyword.Leak Through
is a String that you can choose to name the material.mat_tl
in them.cube_leak.setMaterial((Material) assetManager.loadAsset( "Materials/LeakThrough.j3m"));+
+
+Using this new custom material LeakThrough.j3m
only takes one line. You have replaced the three lines of an on-the-fly material definition with one line that loads a custom material from a file. This method is very handy if you use the same material often.
+
+ +Go back to the bumpy rock sample above: +
+
+
+You have learned how to create a Material, specify its properties, and use it on a Geometry. You know how to load an image file (.png, .jpg) as texture into a material. You know to save texture files in a subfolder of your project's assets/Textures/
directory.
+
+You have also learned that a material can be stored in a .j3m file. The file references a built-in MaterialDefinition and specifies values for properties of that MaterialDefinition. You know to save your custom .j3m files in your project's assets/Materials/
directory.
+
+Now that you know how to load models and how to assign good-looking materials to them, let's have a look at how to animate models in the next chapter, Hello Animation. + +
++See also +
+ Previous: Hello SimpleApplication,
-Next: Hello Assets.
When creating a 3D game, you start out with creating a scene and some objects. You place the objects (player tokens, obstacles, etc) in the scene, and move, resize, rotate, color, and animate them.
In this tutorial we will have a look at a simple 3D scene. You will learn that the 3D world is represented in a scene graph, and why the rootNode is important. You will learn how to create simple objects and how to transform them – move, scale, rotate. You will understand the difference between the two types of Spatials in the scene graph, Node and Geometry. For a visual introduction to the scene graph check out our Scene Graph for Dummies presentation.
package jme3test.helloworld; + ++ +JME 3 Tutorial (2) - Hello Node
++ ++ ++ +Previous: Hello SimpleApplication, +Next: Hello Assets. +
+ ++In this tutorial we will have a look at the creation of a 3D scene. +
++
+ +- +
This tutorial assumes that you know what the Scene Graph is.+- +
For a visual introduction, check out Scene Graph for Dummies.++ +When creating a 3D game +
++
+ +- +
You create some scene objects like players, buildings, etc.+- +
You add the objects to the scene.+- +
You move, resize, rotate, color, and animate them.++ +You will learn that the scene graph represents the 3D world, and why the rootNode is important. You will learn how to create simple objects, and how to "transform" them by moving, scaling, and rotating. You will understand the difference between the two types of "Spatials" in the scene graph: Nodes and Geometries. +
+ +Code Sample
++package jme3test.helloworld; + import com.jme3.app.SimpleApplication; import com.jme3.material.Material; import com.jme3.math.Vector3f; @@ -14,203 +49,341 @@ import com.jme3.scene.Geometry; import com.jme3.scene.shape.Box; import com.jme3.math.ColorRGBA; import com.jme3.scene.Node; -/** Sample 2 - How to use nodes as handles to manipulate objects in the scene graph. + +/** Sample 2 - How to use nodes as handles to manipulate objects in the scene. * You can rotate, translate, and scale objects by manipulating their parent nodes. * The Root Node is special: Only what is attached to the Root Node appears in the scene. */ public class HelloNode extends SimpleApplication { + public static void main(String[] args){ HelloNode app = new HelloNode(); app.start(); } + @Override public void simpleInitApp() { - // create a blue box at coordinates (1,-1,1) + + /** create a blue box at coordinates (1,-1,1) */ Box( new Vector3f(1,-1,1), 1,1,1); Geometry blue = new Geometry("Box", box1); - Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + Material mat1 = new Material(assetManager, + "Common/MatDefs/Misc/Unshaded.j3md"); mat1.setColor("Color", ColorRGBA.Blue); blue.setMaterial(mat1); - // create a red box straight above the blue one at (1,3,1) + + /** create a red box straight above the blue one at (1,3,1) */ Box( new Vector3f(1,3,1), 1,1,1); Geometry red = new Geometry("Box", box2); - Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + Material mat2 = new Material(assetManager, + "Common/MatDefs/Misc/Unshaded.j3md"); mat2.setColor("Color", ColorRGBA.Red); red.setMaterial(mat2); - // create a pivot node at (0,0,0) and attach it to root + + /** Create a pivot node at (0,0,0) and attach it to the root node */ Node pivot = new Node("pivot"); - rootNode.attachChild(pivot); - // attach the two boxes to the *pivot* node! + rootNode.attachChild(pivot); // put this node in the scene + + /** Attach the two boxes to the *pivot* node. */ pivot.attachChild(blue); pivot.attachChild(red); - // rotate pivot node: Both boxes have rotated! - pivot.rotate( 0.4f , 0.4f , 0.0f ); + /** Rotate the pivot node: Note that both boxes have rotated! */ + pivot.rotate(.4f,.4f,0f); } -}
Build and run the code sample. You should see two colored boxes tilted at the same angle.
Understanding the Terminology
In this tutorial, you will learn some new terms:
The scene graph represents your 3D world. Objects in the scene graph (such as the boxes in this example) are called Spatials.
A Spatial is a collection of information about an object: its location, rotation, and scale. A Spatial can be loaded, transformed, and saved. There are two types of Spatials, Nodes and Geometries. To add a Spatial to the scene graph, you attach the Spatial to the rootNode. Everything attached to the rootNode is part of the scene graph.Understanding the Code
So what exactly happens in this code snippet? Note that we are using the
simpleInitApp()
method that was introduced in the first tutorial.
We create a box Geometry.
The box Geometry's extends are (1,1,1), that makes it 2x2x2 world units big. We place the box at (1,-1,1) We give it a solid blue material.Box( new Vector3f(1,-1,1), 1,1,1); - Geometry blue = new Geometry("Box", box1); - Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat1.setColor("Color", ColorRGBA.Blue); - blue.setMaterial(mat1); We create a second box Geometry.
This box Geometry is also 2x2x2 world units big. We place the second box at (1,3,1). This is straight above the blue box, with a gap of 2 world units inbetween. We give it a solid red materialBox( new Vector3f(1,3,1), 1,1,1); - Geometry red = new Geometry("Box", box2); - Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat2.setColor("Color", ColorRGBA.Red); - red.setMaterial(mat2); We create a Node.
By default the Node is placed at (0,0,0). We attach the Node to the rootNode. An attached Node has no visible appearance in the scene.Node pivot = new Node("pivot"); - rootNode.attachChild(pivot); Note that we have not attached the two boxes to anything yet!
If we ran the application now, the scenegraph would appear empty. We attach the two boxes to the node.
If we ran the app now, we would see two boxes: one straight above the other.pivot.attachChild(blue); - pivot.attachChild(red); Now, we rotate the node.
When we run the application now, we see two boxes on top of each other – but both are tilted at the same angle.pivot.rotate( 0.4f , 0.4f , 0.0f );What has happened? We have attached two box Geometries to a Node. Then we used the Node as a handle to grab the two boxes and transform (rotate) both, in one step. This is a common task and you will use this method a lot in your games when you move game characters around.
Definition: Geometry vs Node
You work with two types of Spatials in your scenegraph: Nodes and Geometries. Here is the difference:
Geometry Node Visibility: A visible 3-D object. An invisible "handle". Purpose: A Geometry stores an object's looks. A Node groups Geometries and other Nodes together. Examples: A box, a sphere, player, a building, a piece of terrain, a vehicle, missiles, NPCs, etc… The default rootNode
, theguiNode
(for on-screen text); a floor node, a custom vehicle-with-passengers node, an audio node, etc…FAQ: How to Populate the Scenegraph?
+ ++ +
Task? Solution! + Create a Spatial Create a shape and give it a Material. For instance a box shape: +Box(Vector3f.ZERO, 1, 1, 1); +}+ ++Build and run the code sample. You should see two colored boxes tilted at the same angle. +
+ + + +Understanding the Terminology
++ ++ ++ +In this tutorial, you learn some new terms: + +
++ ++
+ +What you want to do How you say it in JME3 terminology ++ +Structure the 3D scene Define the scene graph ++ +Create scene objects Create Spatials ++ +Make an object appear in the scene Attach a Spatial to the rootNode ++ +Make an object disappear from the scene Detach the Spatial from the rootNode ++ +Every JME3 application has a rootNode: Your game automatically inherits the
+rootNode
object from SimpleApplication. Everything attached to the rootNode is part of the scene graph. The elements of the scene graph are Spatials. ++
+- +
A Spatial contains the location, rotation, and scale of an object.+- +
A Spatial can be loaded, transformed, and saved.+- +
There are two types of Spatials: Nodes and Geometries.++ ++
+ +Geometry Node ++ +Visibility: A Geometry is a visible scene object. A Node is an invisible "handle" for scene objects. ++ +Purpose: A Geometry stores an object's looks. A Node groups Geometries and other Nodes together. ++ +Examples: A box, a sphere, a player, a building, a piece of terrain, a vehicle, missiles, NPCs, etc… The +rootNode
, a floor node grouping several terrains, a custom vehicle-with-passengers node, a player-with-weapon node, an audio node, etc…Understanding the Code
++ ++ ++ +What happens in the code snippet? You use the
+simpleInitApp()
method that was introduced in the first tutorial to initialize the scene. + ++
+ +- +
You create the first box Geometry.++
+- +
Create a Box shape with a radius of (1,1,1), that makes the box 2x2x2 world units big.+- +
Position the box at (1,-1,1).+- +
Wrap the Box shape into a Geometry.+- +
Create a blue material.+- +
Apply the blue material to the Box Geometry.+Box( new Vector3f(1,-1,1), 1,1,1); + Geometry blue = new Geometry("Box", box1); + Material mat1 = new Material(assetManager, + "Common/MatDefs/Misc/Unshaded.j3md"); + mat1.setColor("Color", ColorRGBA.Blue); + blue.setMaterial(mat1);+- +
You create a second box Geometry.++
+- +
Create a second Box shape with the same size.+- +
Position the second box at (1,3,1). This is straight above the first box, with a gap of 2 world units inbetween.+- +
Wrap the Box shape into a Geometry.+- +
Create a red material.+- +
Apply the red material to the Box Geometry.+Box( new Vector3f(1,3,1), 1,1,1); + Geometry red = new Geometry("Box", box2); + Material mat2 = new Material(assetManager, + "Common/MatDefs/Misc/Unshaded.j3md"); + mat2.setColor("Color", ColorRGBA.Red); + red.setMaterial(mat2);+- +
You create a Node.++
+- +
Name the Node "pivot".+- +
By default the Node is positioned at (0,0,0).+- +
Attach the Node to the rootNode.+- +
The Node has no visible appearance in the scene.+Node pivot = new Node("pivot"); + rootNode.attachChild(pivot);+ ++If you run the application with only the code up to here, the scene appears empty. This is because the Node is invisible and you have not yet attached any visible Geometries to the rootNode. +
+- +
Attach the two boxes to the node.+pivot.attachChild(blue); + pivot.attachChild(red);+ ++If you run the app with only the code up to here, you see two cubes: A red cube straight above a blue cube. +
+- +
Rotate the pivot node.+pivot.rotate( 0.4f , 0.4f , 0.0f );+ ++ If you run the app now, you see two boxes on top of each other – both tilted at the same angle. +
++ +What has happened? You have grouped two Geometries by attaching them to one pivot Node. You can now use the pivot Node as a handle to move the two Geometries. Rotating the pivot Node rotates both attached Geometries, in one step. Transforming a Node to transform attached Spatials is a common task. You will use this method a lot in your games when you move Spatials around, e.g. for game characters. You can also rotate Spatials around their own center – then you do not need a pivot Node. +
+ +How do I Populate the Scenegraph?
+++
+ +Task…? Solution! ++ Create a Spatial Create a shape and give it a Material. For example: Box(Vector3f.ZERO, 1, 1, 1); Geometry thing = new Geometry("thing", mesh); -Material mat = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md"); -thing.setMaterial(mat);Make an object appear in the scene Attach the Spatial to the rootNode
, or to any node that is attached to the rootNode.rootNode.attachChild(thing);Remove objects from the scene Detach the Spatial from the rootNode
, and from any node that is attached to the rootNode.rootNode.detachChild(thing);rootNode.detachAllChildren();Find a Spatial in the scene by the object's name or ID Look at the node's children. Spatial thing = rootNode.getChild("thing");Spatial twentyThird = rootNode.getChild(22);Specify what should be loaded at the start Everything you initialize and attach to the rootNode
in thesimpleInitApp()
method is part of the scene at the start of the game.How to Transform Objects?
There are three types of 3D transformation: Translation (moving), Scaling (resizing), and Rotation (turning).
Task? Solution! X Y Z Position and move objects Translation: Specify the new location in three dimensions: right/left, up/down, forward/backward.
Example 1. To move an object to specific coordinates, such as (0,40.2f,-2), use:thing.setLocalTranslation( new Vector3f( 0.0f, 40.2f, -2.0f ) );
Example 2: To move an object by a certain amount, e.g. higher up (y=40.2f) and further back (z=-2.0f):thing.move( 0.0f, 40.2f, -2.0f );right/left up/down forward/ backward Resize objects Scaling: To resize a Spatial, specify the scale factor in each dimension: length, height, width. A value between 0.0f and 1.0f will shrink the object; a value bigger than 1.0f will make it grow; and 1.0f will keep this dimension the same. Using the same value for each dimension scales an object proportionally, using different values stretches it.
Example: Make it 10 times longer, one tenth of the height, same width:thing.setLocalScale( 10.0f, 0.1f, 1.0f );thing.scale( 10.0f, 0.1f, 1.0f );length height width Turn objects Rotation: 3-D rotation is a bit tricky (learn details here). In short: You can rotate around three axes, pitch, yaw, and roll.
Important: You do not specify the rotation in degrees from 0° to 360°, but in radians from 0.0f to 6.28f (FastMath.PI*2) !
Example: To roll an object 180° around the z axis:thing.rotate( 0f , 0f , FastMath.PI );If you do want to specify angles in degrees then multiply your degrees value with FastMath.DEG_TO_RAD
Example:thing.rotate( 0f , 0f , 180*FastMath.DEG_TO_RAD );Tip: If your game idea calls for a serious amount of rotations, it is worth looking into quaternions, a data structure that can combine and store rotations efficiently.
thing.setLocalRotation( new Quaternion(). fromAngleAxis(FastMath.PI/2, new Vector3f(1,0,0)));pitch yaw roll How to Troubleshoot Nodes?
If you get unexpected results, check whether you made the following common mistakes:
Problem? Solution! Created Geometry does not appear in scene Have you attached it to (a node that is attached to) the rootNode?
Does it have a Material?
What is its translation (position)? Is it covered up by another Geometry?
Is it too far from the camera? try cam.setFrustumFar(111111f);Spatial rotates wrong Did you use radian values, and not degrees? (if you used degrees multiply them with FastMath.DEG_TO_RAD to get them converted to radians)
Did you rotate the intended pivot node?
Did you rotate around the right axis?Geometry has an unexpected Material Did you reuse a Material from another Geometry and have inadvertently changed its properties?
(if so, maybe consider cloning: mat2 = mat.clone(); )Conclusion
- \ No newline at end of file +Material mat = new Material(assetManager, + "Common/MatDefs/Misc/ShowNormals.j3md"); +thing.setMaterial(mat); +You have learned that the 3D world is a Scene Graph of Spatials: Visible Geometries and invisible Nodes. You can transform Spatials, or attach them to nodes and transform the nodes.
Since standard shapes like spheres and boxes get old fast, continue with the next chapter where you learn to load assets, such as 3-D models.+ +Make an object appear in the scene Attach the Spatial to the +rootNode
, or to any node that is attached to the rootNode.rootNode.attachChild(thing);++ +Remove objects from the scene Detach the Spatial from the +rootNode
, and from any node that is attached to the rootNode.rootNode.detachChild(thing);+rootNode.detachAllChildren();++ +Find a Spatial in the scene by the object's name or ID. Look at the node's children: +Spatial thing = rootNode.getChild("thing");+Spatial twentyThird = rootNode.getChild(22);++ +Specify what should be loaded at the start Everything you initialize and attach to the +rootNode
in thesimpleInitApp()
method is part of the scene at the start of the game.How do I Transform Spatials?
++ ++ ++ +There are three types of 3D transformation: Translation, Scaling, and Rotation. + +
+++
+ +Translation moves Spatials X-axis Y-axis Z-axis ++ +Specify the new location in three dimensions: How far away is it from the origin going right-up-forward?
+To move a Spatial to specific coordinates, such as (0,40.2f,-2), use:thing.setLocalTranslation( new Vector3f( 0.0f, 40.2f, -2.0f ) );+ ++ To move a Spatial by a certain amount, e.g. higher up (y=40.2f) and further back (z=-2.0f): +
+thing.move( 0.0f, 40.2f, -2.0f );++right -left +up -down +forward -backward +++
+ +Scaling resizes Spatials X-axis Y-axis Z-axis ++ +Specify the scaling factor in each dimension: length, height, width.
+A value between 0.0f and 1.0f shrinks the Spatial; bigger than 1.0f stretches it; 1.0f keeps it the same.
+Using the same value for each dimension scales proportionally, different values stretch it.
+To scale a Spatial 10 times longer, one tenth the height, and keep the same width:thing.scale( 10.0f, 0.1f, 1.0f );+length height width ++ ++
+ +Rotation turns Spatials X-axis Y-axis Z-axis ++ +3-D rotation is a bit tricky (learn details here). In short: You can rotate around three axes: Pitch, yaw, and roll. You can specify angles in degrees by multiplying the degrees value with FastMath.DEG_TO_RAD
.
+To roll an object 180° around the z axis:thing.rotate( 0f , 0f , 180*FastMath.DEG_TO_RAD );+ ++ Tip: If your game idea calls for a serious amount of rotations, it is worth looking into quaternions, a data structure that can combine and store rotations efficiently. +
+thing.setLocalRotation( + new Quaternion().fromAngleAxis(180*FastMath.DEG_TO_RAD, new Vector3f(1,0,0)));+pitch = nodding your head yaw = shaking your head roll = cocking your head +How do I Troubleshoot Spatials?
++ ++ ++ +If you get unexpected results, check whether you made the following common mistakes: + +
++ ++
+ +Problem? Solution! ++ +A created Geometry does not appear in the scene. Have you attached it to (a node that is attached to) the rootNode? +
+Does it have a Material?
+What is its translation (position)? Is it behind the camera or covered up by another Geometry?
+Is it to tiny or too gigantic to see?
+Is it too far from the camera? (Try (111111f); to see further)+ +A Spatial rotates in unexpected ways. Did you use radian values, and not degrees? (If you used degrees, multiply them with FastMath.DEG_TO_RAD to convert them to radians) +
+Did you rotate around the intended pivot node or something else?
+Did you rotate around the right axis?+ +A Geometry has an unexpected Color or Material. Did you reuse a Material from another Geometry and have inadvertently changed its properties? (If so, consider cloning it: mat2 = mat.clone(); ) +Conclusion
++ ++ \ No newline at end of file diff --git a/sdk/jme3-core/javahelp/com/jme3/gde/core/docs/jme3/beginner/hello_physics.html b/sdk/jme3-core/javahelp/com/jme3/gde/core/docs/jme3/beginner/hello_physics.html index e78b52ecb..11bf3a0e8 100644 --- a/sdk/jme3-core/javahelp/com/jme3/gde/core/docs/jme3/beginner/hello_physics.html +++ b/sdk/jme3-core/javahelp/com/jme3/gde/core/docs/jme3/beginner/hello_physics.html @@ -1,26 +1,53 @@ -+ +You have learned that your 3D scene is a scene graph made up of Spatials: Visible Geometries and invisible Nodes. You can transform Spatials, or attach them to nodes and transform the nodes. +
+ ++Since standard shapes like spheres and boxes get old fast, continue with the next chapter where you learn to load assets, such as 3-D models. + +
+ + +JME 3 Tutorial (13) - Hello Physics
Previous: Hello Effects, -Next: JME 3 documentation
For the simulation of physical forces, jME3 integrates the jBullet library. The most common use cases for physics in 3D games are:
Driving vehicles with suspensions, tyre friction, ramp jumping, drifting – example: car racers Rolling and bouncing balls – example: pong, pool billiard, bowling Sliding and falling boxes – example: Breakout, Arkanoid Exposing objects to forces and gravity – example: spaceships or zero-g flight Animating ragdolls – example: "realistic" character simulations … and much more: swinging pendulums, flexible chains…All that can be done in JME3. Let's have a look at a Physics simulation in this example where we shoot cannon balls at a wall.
Sample Code
Thanks to double1984 for contributing this fun sample!
package jme3test.helloworld; + +JME 3 Tutorial (13) - Hello Physics
++ ++ ++Previous: Hello Effects, +Next: JME 3 documentation +
+ ++Do you remember the Hello Collision tutorial where you made the model of a town solid and walked through it in a first-person perspective? Then you may remember that, for the simulation of physical forces, jME3 integrates the library. +
+ ++Apart from making models "solid", the most common use cases for physics in 3D games are: +
++
+ +- +
Driving vehicles with suspensions, tyre friction, ramp jumping, drifting – Example: car racers+- +
Rolling and bouncing balls – Example: pong, pool billiard, bowling+- +
Sliding and falling boxes – Example: Breakout, Arkanoid+- +
Exposing objects to forces and gravity – Example: spaceships or zero-g flight+- +
Animating ragdolls – Example: "realistic" character simulations+- +
Swinging pendulums, rope bridges, flexible chains, and much more…++ +All these physical properties can be simulated in JME3. Let's have a look at a simulation of physical forces in this example where you shoot cannon balls at a brick wall. +
+ ++ +
+ +Sample Code
++ ++Thanks to double1984 for contributing this fun sample! + +
+package jme3test.helloworld; import com.jme3.app.SimpleApplication; import com.jme3.asset.TextureKey; @@ -33,12 +60,10 @@ import com.jme3.input.controls.MouseButtonTrigger; import com.jme3.material.Material; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; -import com.jme3.renderer.queue.RenderQueue.ShadowMode; import com.jme3.scene.Geometry; import com.jme3.scene.shape.Box; import com.jme3.scene.shape.Sphere; import com.jme3.scene.shape.Sphere.TextureMode; -import com.jme3.shadow.BasicShadowRenderer; import com.jme3.texture.Texture; import com.jme3.texture.Texture.WrapMode; @@ -55,9 +80,6 @@ public class HelloPhysics extends SimpleApplication { /** Prepare the Physics Application State (jBullet) */ private BulletAppState bulletAppState; - - /** Activate custom rendering of shadows */ - BasicShadowRenderer bsr; /** Prepare Materials */ Material wall_mat; @@ -94,19 +116,20 @@ public class HelloPhysics extends SimpleApplication { /** Set up Physics Game */ bulletAppState = new BulletAppState(); stateManager.attach(bulletAppState); + //bulletAppState.getPhysicsSpace().enableDebug(assetManager); + /** Configure cam to look at scene */ - cam.setLocation(new Vector3f(0, 6f, 6f)); - cam.lookAt(Vector3f.ZERO, new Vector3f(0, 1, 0)); - cam.setFrustumFar(15); + cam.setLocation(new Vector3f(0, 4f, 6f)); + cam.lookAt(new Vector3f(2, 2, 0), Vector3f.UNIT_Y); /** Add InputManager action: Left click triggers shooting. */ - inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + inputManager.addMapping("shoot", + new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); inputManager.addListener(actionListener, "shoot"); /** Initialize the scene, materials, and physics space */ initMaterials(); initWall(); initFloor(); initCrossHairs(); - initShadows(); } /** @@ -136,7 +159,7 @@ public class HelloPhysics extends SimpleApplication { stone_mat.setTexture("ColorMap", tex2); floor_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - TextureKey key3 = new TextureKey("Textures/Terrain/Pond/Pond.png"); + TextureKey key3 = new TextureKey("Textures/Terrain/Pond/Pond.jpg"); key3.setGenerateMips(true); Texture tex3 = assetManager.loadTexture(key3); tex3.setWrap(WrapMode.Repeat); @@ -147,7 +170,6 @@ public class HelloPhysics extends SimpleApplication { public void initFloor() { Geometry floor_geo = new Geometry("Floor", floor); floor_geo.setMaterial(floor_mat); - floor_geo.setShadowMode(ShadowMode.Receive); floor_geo.setLocalTranslation(0, -0.1f, 0); this.rootNode.attachChild(floor_geo); /* Make the floor physical with mass 0.0f! */ @@ -161,7 +183,7 @@ public class HelloPhysics extends SimpleApplication { float startpt = brickLength / 4; float height = 0; for (int j = 0; j < 15; j++) { - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 6; i++) { Vector3f vt = new Vector3f(i * brickLength * 2 + startpt, brickHeight + height, 0); makeBrick(vt); @@ -170,15 +192,6 @@ public class HelloPhysics extends SimpleApplication { height += 2 * brickHeight; } } - - /** Activate shadow casting and light direction */ - private void initShadows() { - bsr = new BasicShadowRenderer(assetManager, 256); - bsr.setDirection(new Vector3f(-1, -1, -1).normalizeLocal()); - viewPort.addProcessor(bsr); - // Default mode is Off -- Every node declares own shadow mode! - rootNode.setShadowMode(ShadowMode.Off); - } /** This method creates one individual physical brick. */ public void makeBrick(Vector3f loc) { @@ -186,9 +199,8 @@ public class HelloPhysics extends SimpleApplication { Geometry brick_geo = new Geometry("brick", box); brick_geo.setMaterial(wall_mat); rootNode.attachChild(brick_geo); - /** Position the brick geometry and activate shadows */ + /** Position the brick geometry */ brick_geo.setLocalTranslation(loc); - brick_geo.setShadowMode(ShadowMode.CastAndReceive); /** Make brick physical with a mass > 0.0f. */ brick_phy = new RigidBodyControl(2f); /** Add physical brick to physics space. */ @@ -199,14 +211,13 @@ public class HelloPhysics extends SimpleApplication { /** This method creates one individual physical cannon ball. * By defaul, the ball is accelerated and flies * from the camera position in the camera direction.*/ - public void makeCannonBall() { + public void makeCannonBall() { /** Create a cannon ball geometry and attach to scene graph. */ Geometry ball_geo = new Geometry("cannon ball", sphere); ball_geo.setMaterial(stone_mat); rootNode.attachChild(ball_geo); - /** Position the cannon ball and activate shadows */ + /** Position the cannon ball */ ball_geo.setLocalTranslation(cam.getLocation()); - ball_geo.setShadowMode(ShadowMode.CastAndReceive); /** Make the ball physcial with a mass > 0.0f */ ball_phy = new RigidBodyControl(1f); /** Add physical ball to physics space. */ @@ -228,29 +239,62 @@ public class HelloPhysics extends SimpleApplication { settings.getHeight() / 2 + ch.getLineHeight() / 2, 0); guiNode.attachChild(ch); } -}
You should see a brick wall that is casting a shadow on a floor. Click to shoot cannon balls. Watch the bricks fall and bounce off one another!
A Basic Physics Application
+ +In the previous tutorials, we were using Geometries (boxes, spheres, and models) that we placed in the scene. Geometries can float in mid-air and even overlap – they are not affected by "gravity" and have no physical mass. This tutorial shows how to add physical properties to Geometries.
As always, we start with a standard com.jme3.app.SimpleApplication. To activate physics, we create a com.jme3.bullet.BulletAppState, and and attach it to the SimpleApplication's application state manager.
public class HelloPhysics extends SimpleApplication { +}+ ++You should see a brick wall. Click to shoot cannon balls. Watch the bricks fall and bounce off one another! +
+ +A Basic Physics Application
++ ++ +In the previous tutorials, you used static Geometries (boxes, spheres, and models) that you placed in the scene. Depending on their translation, Geometries can "float in mid-air" and even overlap – they are not affected by "gravity" and have no physical mass. This tutorial shows how to add physical properties to Geometries. +
+ ++As always, start with a standard com.jme3.app.SimpleApplication. To activate physics, create a com.jme3.bullet.BulletAppState, and and attach it to the SimpleApplication's AppState manager. +
+public class HelloPhysics extends SimpleApplication { private BulletAppState bulletAppState; public void simpleInitApp() { bulletAppState = new BulletAppState(); stateManager.attach(bulletAppState); + ... } -}The BulletAppState gives the game access to a Physics Space. The Physics Space allows us to use com.jme3.bullet.control.PhysicsControls that add physical properties to Nodes.
Creating Bricks and Cannon Balls
Geometries
+ +In this "shoot at the wall" example, we use Geometries such as cannon balls and bricks. A geometry just describes the shape and look of an object.
/** Prepare geometries and physical nodes for bricks and cannon balls. */ + ... +}+ ++The BulletAppState gives the game access to a PhysicsSpace. The PhysicsSpace lets you use com.jme3.bullet.control.PhysicsControls that add physical properties to Nodes. +
+ +Creating Bricks and Cannon Balls
++ ++ +Geometries
++ ++ +In this "shoot at the wall" example, you use Geometries such as cannon balls and bricks. Geometries contain meshes, such as Shapes. Let's create and initialize some Shapes: Boxes and Spheres. +
+/** Prepare geometries and physical nodes for bricks and cannon balls. */ private static final Box box; private static final Sphere sphere; private static final Box floor; - /** dimensions used for bricks and wall */ private static final float brickLength = 0.48f; private static final float brickWidth = 0.24f; private static final float brickHeight = 0.12f; - static { /** Initialize the cannon ball geometry */ sphere = new Sphere(32, 32, 0.4f, true, false); @@ -261,51 +305,91 @@ class="level3">In this "shoot at the wall" example, we use Geometr /** Initialize the floor geometry */ floor = new Box(Vector3f.ZERO, 10f, 0.1f, 5f); floor.scaleTextureCoordinates(new Vector2f(3, 6)); - }
RigidBodyControl: Brick
+ +For each Geometry that we want to have physcial properties, we add a RigidBodyControl.
private RigidBodyControl brick_phy;The
makeBrick(loc)
methods creates a physics nodebrickNode
at a location loc. Our brick shall have
a visible Geometrybrick_geo
physical propertiesbrick_phy
a mass of 2.0f.public void makeBrick(Vector3f loc) { + }+ +RigidBodyControl: Brick
++ ++ +We want to create brick Geometries from those boxes. For each Geometry with physcial properties, you create a RigidBodyControl. +
+private RigidBodyControl brick_phy;+ ++The custom
+makeBrick(loc)
methods creates individual bricks at the locationloc
. A brick has the following properties: ++
+- +
It has a visible Geometry+brick_geo
(Box Shape Geometry).- +
It has physical properties+brick_phy
(RigidBodyControl)public void makeBrick(Vector3f loc) { /** Create a brick geometry and attach to scene graph. */ Geometry brick_geo = new Geometry("brick", box); brick_geo.setMaterial(wall_mat); rootNode.attachChild(brick_geo); - /** Position the brick geometry and activate shadows */ + /** Position the brick geometry */ brick_geo.setLocalTranslation(loc); - brick_geo.setShadowMode(ShadowMode.CastAndReceive); /** Make brick physical with a mass > 0.0f. */ brick_phy = new RigidBodyControl(2f); /** Add physical brick to physics space. */ brick_geo.addControl(brick_phy); bulletAppState.getPhysicsSpace().add(brick_phy); - }This code sample does the following:
We use a box shape as brick, and give it a brick-colored material. We attach the brick to the rootNode and position it at the position loc in the scene graph. (Optionally, we activate a "Cast and Receive" shadow mode for each brick.) We create a RigidBodyControl for the brick, add it to the brick Geometry, and register it to the PhysicsSpace.RigidBodyControl: Cannonball
+ +You will notice that the cannon ball is created in the same way:
The
makeCannonBall()
methods creates a physics nodecannonballNode
. The cannon ball shall have
a visible Geometryball_geo
physical propertiesball_phy
a mass of 1.0f.public void makeCannonBall() { - /** Create a cannon ball geometry and attach to scene graph. */ + }+ ++This code sample does the following: +
++
+ +- +
You create a brick Geometry brick_geo. A Geometry describes the shape and look of an object.++
+- +
brick_geo has a box shape+- +
brick_geo has a brick-colored material.+- +
You attach brick_geo to the rootNode+- +
You position brick_geo at+loc
.- +
You create a RigidBodyControl brick_phy for brick_geo.++
+- +
brick_phy has a mass of 2f.+- +
You add brick_phy to brick_geo.+- +
You register brick_phy to the PhysicsSpace.+RigidBodyControl: Cannonball
++ ++ +You notice that the cannon ball is created in the same way, using the custom
+makeCannonBall()
method. The cannon ball has the following properties: ++
+- +
It has a visible Geometry+ball_geo
(Sphere Shape Geometry)- +
It has physical properties+ball_phy
(RigidBodyControl)/** Create a cannon ball geometry and attach to scene graph. */ Geometry ball_geo = new Geometry("cannon ball", sphere); ball_geo.setMaterial(stone_mat); rootNode.attachChild(ball_geo); - /** Position the cannon ball and activate shadows */ + /** Position the cannon ball */ ball_geo.setLocalTranslation(cam.getLocation()); - ball_geo.setShadowMode(ShadowMode.CastAndReceive); /** Make the ball physcial with a mass > 0.0f */ ball_phy = new RigidBodyControl(1f); /** Add physical ball to physics space. */ @@ -313,86 +397,251 @@ class="li"> a mass of 1.0f.public void makeCann bulletAppState.getPhysicsSpace().add(ball_phy); /** Accelerate the physcial ball to shoot it. */ ball_phy.setLinearVelocity(cam.getDirection().mult(25)); - }This code sample does the following:
We use a sphere shape as cannonball, and give it a stone material. We attach the ball to the rootNode and position it where the camera is. (Optionally, we activate a "Cast and Receive" shadow mode for the ball.) We create a RigidBodyControl for the ball, add it to the ball Geometry, and register it to the PhysicsSpace. Since are are shooting cannon balls here, we accelerate the ball in the direction the camera is looking, with a speed of 25f.RigidBodyControl: Floor
+ +The (static) floor has one important difference compared to the (dynamic) bricks and cannonballs: A mass of zero.
As before, we write a custom
initFloor()
method that creates a flat box with a rock texture that we use as floor. The floor shall have:
a visible Geometryfloor_geo
physical propertiesfloor_phy
A mass of 0.0f!private RigidBodyControl floor_phy; - ... - public void initFloor() { - Box(Vector3f.ZERO, 10f, 0.1f, 5f); - floorBox.scaleTextureCoordinates(new Vector2f(3, 6)); - floor_geo = new Geometry("floor", floorBox); ++ ++This code sample does the following: +
++
+ +- +
You create a brick Geometry ball_geo. A Geometry describes the shape and look of an object.++
+- +
ball_geo has a sphere shape+- +
ball_geo has a stone-colored material.+- +
You attach ball_geo to the rootNode+- +
You position ball_geo at the camera location.+- +
You create a RigidBodyControl ball_phy for ball_geo.++
+- +
ball_phy has a mass of 1f.+- +
You add ball_phy to ball_geo.+- +
You register ball_phy to the PhysicsSpace.++ +Since you are shooting cannon balls, the last line accelerates the ball in the direction the camera is looking, with a speed of 25f. +
+ +RigidBodyControl: Floor
++ ++ +The (static) floor has one important difference compared to the (dynamic) bricks and cannonballs: Static objects have a mass of zero. +As before, you write a custom
+initFloor()
method that creates a flat box with a rock texture that you use as floor. The floor has the following properties: ++
+- +
It has a visible Geometry+floor_geo
(Box Shape Geometry)- +
It has physical properties+floor_phy
(RigidBodyControl)public void initFloor() { + Geometry floor_geo = new Geometry("Floor", floor); floor_geo.setMaterial(floor_mat); - floor_geo.setShadowMode(ShadowMode.Receive); floor_geo.setLocalTranslation(0, -0.1f, 0); this.rootNode.attachChild(floor_geo); /* Make the floor physical with mass 0.0f! */ floor_phy = new RigidBodyControl(0.0f); floor_geo.addControl(floor_phy); bulletAppState.getPhysicsSpace().add(floor_phy); -This code sample does the following:
We use a box shape as floor, and give it a floor material. We attach the floor to the rootNode and position it a bit below the origin – to prevent overlap with other physical nodes. (Optionally, we activate a "Receive" shadow mode for the floor. The floor does not cast any shadows, this saves computing time.) Static objects such as floors are mass-less and are not affected by gravity! Therefor we create a RigidBodyControl with a mass of 0.0f. We add the RigidBodyControl to the floor Geometry, and register it to the PhysicsSpace.Creating the Scene
Let's have a quick look at the remaining custom helper methods:
initMaterial()
,initShadows()
,initCrossHairs()
, andinitWall()
.
initMaterial()
– This method initializes all the materials we use in this demo.initShadows()
– (Optional) We deactivate the rootNode's default ShadowMode and use a JME SceneProcessor called BasicShadowRenderer from thecom.jme3.shadow
package. For every relevant scene node (floor, cannon balls, bricks) we specify individually what shadow behaviour we want, Cast, Receive, or both.initWall()
– A double loop that generates a wall by positioning brick objects: 15 rows high with 4 bricks per row. It's important to space the bricks so the do not overlap.initCrossHairs()
– This method simply displays a plus sign that we use as crosshairs for aiming. Note that screen elements such as crosshairs are attached to theguiNode
, not therootNode
.These methods are each called once from the
simpleInitApp()
method at the start of the game. As you see, you write any number of custom methods to set up your game's scene.The Cannon Ball Shooting Action
+ +In the
initSimpleApp()
we add an input mapping that triggers a shoot action when the left mouse button is pressed.public void simpleInitApp() { - ... - inputManager.addMapping("shoot", new MouseButtonTrigger(0)); - inputManager.addListener(actionListener, "shoot"); - ... - }The action of shooting a new cannon ball is defined as follows:
private ActionListener() { + }+ ++This code sample does the following: +
++
+ +- +
You create a floor Geometry floor_geo. A Geometry describes the shape and look of an object.++
+- +
floor_geo has a box shape+- +
floor_geo has a pebble-colored material.+- +
You attach floor_geo to the rootNode+- +
You position floor_geo a bit below y=0 (to prevent overlap with other PhysicControl'ed Spatials).+- +
You create a RigidBodyControl floor_phy for floor_geo.++
+- +
floor_phy has a mass of 0f+- +
You add floor_phy to floor_geo.+- +
You register floor_phy to the PhysicsSpace.+Creating the Scene
++ ++ ++ +Let's have a quick look at the custom helper methods: +
++
+ +- +
+initMaterial()
– This method initializes all the materials we use in this demo.- +
+initWall()
– A double loop that generates a wall by positioning brick objects: 15 rows high with 6 bricks per row. It's important to space the physical bricks so they do not overlap.- +
+initCrossHairs()
– This method simply displays a plus sign that you use as crosshairs for aiming. Note that screen elements such as crosshairs are attached to theguiNode
, not therootNode
!- +
+initInputs()
– This method sets up the click-to-shoot action.+ +These methods are each called once from the
+ +simpleInitApp()
method at the start of the game. As you see, you can write any number of custom methods to set up your game's scene. +The Cannon Ball Shooting Action
++ ++ +In the
+initInputs()
method, you add an input mapping that triggers a shoot action when the left mouse button is pressed. +private void initInputs() { + inputManager.addMapping("shoot", + new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + inputManager.addListener(actionListener, "shoot"); + }+ ++You define the actual action of shooting a new cannon ball as follows: +
+private ActionListener() { public void onAction(String name, boolean keyPressed, float tpf) { if (name.equals("shoot") && !keyPressed) { makeCannonBall(); } } - };In the moment the cannonball appears in the scene, it flies off with the velocity (and in the direction) that we have specified using
setLinearVelocity()
insidemakeCannonBall()
. The newly created cannon ball flies off, hits the wall, and exerts a physical force that shifts the individual bricks.Movement of the physics enabled Spatial
The location of the spatial is defined by the RigidBodyControl, move that to move the spatial or if its a non-world-object set the RigidBodyControl to kinematic mode to have it move along with the spatial. This will make the RigidBody be unaffected by the physics but it will effect the physics objects around it based on its location and amount of movement that is applied. Note that a kinematic RigidBody needs to have a mass!
Excercises
Exercise 1
What happens if you give a static node such as the floor a mass of more than 0.0f?
Exercise 2
Popular AAA games use a clever mix of physics, animation and prerendered graphics to give you the illusion of a real, "physical" world. Look at your favorite games and try to spot where and how the game designers trick you into believing that the whole scene is physical. – For example, a building "breaking" into 4-8 parts when falling apart is most likely being replaced by dynamic physics nodes only after it has been destroyed… Now that you start to implement game physics yourself, look behind the curtain.
Conclusion
- \ No newline at end of file + };Using physics everywhere in a game sounds like a cool idea, but it is easily overused. Although the physics nodes are put to "sleep" when they are not moved, creating a world solely out of dynamic physics nodes will quickly bring you to the limits of your computer's capabilities.
You have learned how to add a PhysicsSpace to an application by attaching a
BulletAppState
. You know how to create PhysicsNodes from a geometry, a collision shape, and a mass value. You have learned that physical objects are not only attached to the rootNode but also registered to the PhysicsSpace. You are aware that overusing physics has a huge performance impact.Additionally you have learned how to add shadows to a scene.
This is the last beginner tutorial for now. Now you are ready to start combining what you learned to create a game of your own!
+In the moment the cannonball appears in the scene, it flies off with the velocity (and in the direction) that you specified using setLinearVelocity()
inside makeCannonBall()
. The newly created cannon ball flies off, hits the wall, and exerts a physical force that impacts individual bricks.
+
+ +The location of the dynamic Spatial is controlled by its RigidBodyControl. Move the RigidBodyControl to move the Spatial. If it's a dynamic PhysicsControl, you can use setLinearVelocity() and apply forces and torques to it. Other RigidBodyControl'led objects can push the dynamic Spatial around (like pool billard balls). +
+ ++You can make Spatials that are not dynamic: Switch the RigidBodyControl to setKinematic(true) to have it move along with its Spatial. +
++ +Learn more about static versus kinematic versus dynamic in the advanced physics doc. +
+ ++ +Add the following line after the bulletAppState initialization. + +
+bulletAppState.getPhysicsSpace().enableDebug(assetManager);+ +
+ +Now you see the collisionShapes of the bricks and spheres, and the floor highlighted. +
+ ++ +What happens if you give a static node, such as the floor, a mass of more than 0.0f? +
+ ++ +Fill your scene with walls, bricks, and cannon balls. When do you begin to see a performance impact? +
+ ++Popular AAA games use a clever mix of physics, animation and prerendered graphics to give you the illusion of a real, "physical" world. Think of your favorite video games and try to spot where and how the game designers trick you into believing that the whole scene is physical. For example, think of a building "breaking" into 4-8 parts after an explosion. The pieces most likely fly on predefined (so called kinematic) paths and are only replaced by dynamic Spatials after they touch the ground… Now that you start to implement game physics yourself, look behind the curtain! +
+ ++Using physics everywhere in a game sounds like a cool idea, but it is easily overused. Although the physics nodes are put to "sleep" when they are not moving, creating a world solely out of dynamic physics nodes will quickly bring you to the limits of your computer's capabilities. +
+ +
+
+You have learned how to activate the jBullet PhysicsSpace in an application by adding a BulletAppState
. You have created PhysicsControls for simple Shape-based Geometries (for more complex shapes, read up on CollisionShapes). You have learned that physical objects are not only attached to the rootNode, but also registered to the PhysicsSpace. You know that it makes a difference whether a physical object has a mass (dynamic) or not (static). You are aware that overusing physics has a huge performance impact.
+
+
Previous: Hello Animation, -Next: Hello Collision
Typical interactions in games include shooting, picking up objects, and opening doors.
-From an implementation point of view, these apparently different interactions are very similar: The user first aims and selects a target in the 3D scene, and then triggers an action on it. We call this process picking.
-You can pick something by either pressing a key on the keyboard, or by clicking with the mouse. In either case, you identify the target by aiming a ray –a straight line– into the scene. This method to implement picking is called ray casting (which is not the same as ray tracing). This tutorial relies on what you have learned in the Hello Input tutorial.
package jme3test.helloworld; + ++ +JME 3 Tutorial (8) - Hello Picking
++ ++ ++Previous: Hello Animation, +Next: Hello Collision +
+ ++Typical interactions in games include shooting, picking up objects, and opening doors. From an implementation point of view, these apparently different interactions are surprisingly similar: The user first aims and selects a target in the 3D scene, and then triggers an action on it. We call this process picking. +
+ ++You can pick something by either pressing a key on the keyboard, or by clicking with the mouse. In either case, you identify the target by aiming a ray –a straight line– into the scene. This method to implement picking is called ray casting (which is not the same as ray tracing). +
+ ++This tutorial relies on what you have learned in the Hello Input tutorial. You find more related code samples under Mouse Picking and Collision and Intersection. +
+ ++ +
+ +Sample Code
++package jme3test.helloworld; + import com.jme3.app.SimpleApplication; import com.jme3.collision.CollisionResult; import com.jme3.collision.CollisionResults; import com.jme3.font.BitmapText; import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.light.DirectionalLight; import com.jme3.material.Material; import com.jme3.math.ColorRGBA; import com.jme3.math.Ray; import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; import com.jme3.scene.Node; +import com.jme3.scene.Spatial; import com.jme3.scene.shape.Box; import com.jme3.scene.shape.Sphere; -/** Sample 8 - how to let the user pick (select) objects in the scene + +/** Sample 8 - how to let the user pick (select) objects in the scene * using the mouse or key presses. Can be used for shooting, opening doors, etc. */ public class HelloPicking extends SimpleApplication { + public static void main(String[] args) { HelloPicking app = new HelloPicking(); app.start(); } Node shootables; Geometry mark; + @Override public void simpleInitApp() { initCrossHairs(); // a "+" in the middle of the screen to help aiming initKeys(); // load custom key mappings initMark(); // a red sphere to mark the hit + /** create four colored boxes and a floor to shoot at: */ shootables = new Node("Shootables"); rootNode.attachChild(shootables); - shootables.attachChild(makeCube("a Dragon", -2f, 0f, 1f)); - shootables.attachChild(makeCube("a tin can", 1f,-2f, 0f)); - shootables.attachChild(makeCube("the Sheriff", 0f, 1f,-2f)); - shootables.attachChild(makeCube("the Deputy", 1f, 0f,-4f)); + shootables.attachChild(makeCube("a Dragon", -2f, 0f, 1f)); + shootables.attachChild(makeCube("a tin can", 1f, -2f, 0f)); + shootables.attachChild(makeCube("the Sheriff", 0f, 1f, -2f)); + shootables.attachChild(makeCube("the Deputy", 1f, 0f, -4f)); shootables.attachChild(makeFloor()); + shootables.attachChild(makeCharacter()); } + /** Declaring the "Shoot" action and mapping to its triggers. */ private void initKeys() { inputManager.addMapping("Shoot", new KeyTrigger(KeyInput.KEY_SPACE), // trigger 1: spacebar - new MouseButtonTrigger(0)); // trigger 2: left-button click + new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); // trigger 2: left-button click inputManager.addListener(actionListener, "Shoot"); } /** Defining the "Shoot" action: Determine what was hit and how to respond. */ private ActionListener() { - @Override + public void onAction(String name, boolean keyPressed, float tpf) { if (name.equals("Shoot") && !keyPressed) { // 1. Reset results list. @@ -68,7 +95,7 @@ public class HelloPicking extends SimpleApplication { Ray ray = new Ray(cam.getLocation(), cam.getDirection()); // 3. Collect intersections between Ray and Shootables in results list. shootables.collideWith(ray, results); - // 4. Print the results. + // 4. Print the results System.out.println("----- Collisions? " + results.size() + "-----"); for (int i = 0; i < results.size(); i++) { // For each hit, we know distance, impact point, name of geometry. @@ -79,19 +106,20 @@ public class HelloPicking extends SimpleApplication { System.out.println(" You shot " + hit + " at " + pt + ", " + dist + " wu away."); } // 5. Use the results (we mark the hit object) - if (results.size() > 0){ + if (results.size() > 0) { // The closest collision point is what was truly hit: CollisionResult closest = results.getClosestCollision(); // Let's interact - we mark the hit with a red dot. mark.setLocalTranslation(closest.getContactPoint()); rootNode.attachChild(mark); } else { - // No hits? Then remove the red mark. + // No hits? Then remove the red mark. rootNode.detachChild(mark); } } } }; + /** A cube object for target practice */ protected Geometry makeCube(String name, float x, float y, float z) { Box(new Vector3f(x, y, z), 1, 1, 1); @@ -101,15 +129,17 @@ public class HelloPicking extends SimpleApplication { cube.setMaterial(mat1); return cube; } + /** A floor to show that the "shot" can go through several objects. */ protected Geometry makeFloor() { - Box(new Vector3f(0,-4,-5), 15,.2f,15); + Box(new Vector3f(0, -4, -5), 15, .2f, 15); Geometry floor = new Geometry("the Floor", box); Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat1.setColor("Color", ColorRGBA.Gray); floor.setMaterial(mat1); return floor; } + /** A red ball that marks the last spot that was "hit" by the "shot". */ protected void initMark() { Sphere sphere = new Sphere(30, 30, 0.2f); @@ -118,6 +148,7 @@ public class HelloPicking extends SimpleApplication { mark_mat.setColor("Color", ColorRGBA.Red); mark.setMaterial(mark_mat); } + /** A centred plus sign to help the player aim. */ protected void initCrossHairs() { guiNode.detachAllChildren(); @@ -126,53 +157,123 @@ public class HelloPicking extends SimpleApplication { ch.setSize(guiFont.getCharSet().getRenderedSize() * 2); ch.setText("+"); // crosshairs ch.setLocalTranslation( // center - settings.getWidth()/2 - guiFont.getCharSet().getRenderedSize()/3*2, - settings.getHeight()/2 + ch.getLineHeight()/2, 0); + settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2, + settings.getHeight() / 2 + ch.getLineHeight() / 2, 0); guiNode.attachChild(ch); } -}
You should see four colored cubes floating over a gray floor, and cross-hairs. Aim the cross-hairs and click, or press the spacebar to shoot. The last hit will be marked with a red dot. -Keep an eye on the application's output stream, it will give you more details: The name of the mesh that was hit, the coordinates of the hit, and the distance.
Understanding the Helper Methods
The methods
makeCube()
,makeFloor()
,initMark()
, andinitCrossHairs
, are custom helper methods. We call them fromsimpleInitApp()
to initialize the scenegraph with sample content.
makeCube()
creates simple colored boxes for "target practice".makeFloor()
creates a gray floor node for "target practice".initMark()
creates a red sphere ("mark"). We will use it later to mark the spot that was hit.
Note that the mark is not attached and therefor not visible at the start.initCrossHairs()
creates simple cross-hairs by printing a "+" sign in the middle of the screen.
Note that the cross-hairs are attached to theguiNode
, not to therootNode
.In this example, we attached all "shootable" objects to one custom node,
Shootables
. This is an optimization so the engine only has to calculate intersections with objects we are actually interested in. TheShootables
node is attached to therootNode
as usual.Understanding Ray Casting for Hit Testing
Our goal is to determine which box the user "shot" (picked). In general, we want to determine which mesh the user has selected by aiming the cross-hairs at it. Mathematically, we draw a line from the camera and see whether it intersects with objects in the 3D scene. This line is called a ray. -Here is our simple ray casting algorithm for picking objects:
Reset the results list. Cast a ray from cam location into the cam direction. Collect all intersections between the ray andShootable
nodes in theresults
list. Use the results list to determine what was hit:
For each hit, JME reports its distance from the camera, impact point, and the name of the mesh. Sort the results by distance. Take the closest result, it is the mesh that was hit.Implementing Hit Testing
Loading the scene
+ +First initialize some shootable nodes and attach them to the scene. You will use the
mark
object later.Node shootables; + + protected Spatial makeCharacter() { + // load a character from jme3test-test-data + Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + golem.scale(0.5f); + golem.setLocalTranslation(-1.0f, -1.5f, -0.6f); + + // We must add a light to make the model visible + DirectionalLight sun = new DirectionalLight(); + sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f)); + golem.addLight(sun); + return golem; + } +}+ ++You should see four colored cubes floating over a gray floor, and cross-hairs. Aim the cross-hairs and click, or press the spacebar to shoot. The hit spot is marked with a red dot. +
+ ++Keep an eye on the application's output stream, it will give you more details: The name of the mesh that was hit, the coordinates of the hit, and the distance. +
+ +Understanding the Helper Methods
++ ++ ++ +The methods
+makeCube()
,makeFloor()
,initMark()
, andinitCrossHairs
, are custom helper methods. We call them fromsimpleInitApp()
to initialize the scenegraph with sample content. + ++
+ +- +
+makeCube()
creates simple colored boxes for "target practice".- +
+makeFloor()
creates a gray floor node for "target practice".- +
+initMark()
creates a red sphere ("mark"). We will use it later to mark the spot that was hit.+
+- +
Note that the mark is not attached and therefor not visible at the start!+- +
+initCrossHairs()
creates simple cross-hairs by printing a "+" sign in the middle of the screen.+
+- +
Note that the cross-hairs are attached to the+guiNode
, not to therootNode
.+ +In this example, we attached all "shootable" objects to one custom node,
+ +Shootables
. This is an optimization so the engine only has to calculate intersections with objects we are actually interested in. TheShootables
node is attached to therootNode
as usual. +Understanding Ray Casting for Hit Testing
++ ++ ++ +Our goal is to determine which box the user "shot" (picked). In general, we want to determine which mesh the user has selected by aiming the cross-hairs at it. Mathematically, we draw a line from the camera and see whether it intersects with objects in the 3D scene. This line is called a ray. +
+ ++Here is our simple ray casting algorithm for picking objects: +
++
+ +- +
Reset the results list.+- +
Cast a ray from cam location into the cam direction.+- +
Collect all intersections between the ray and+Shootable
nodes in theresults
list.- +
Use the results list to determine what was hit:++
+- +
For each hit, JME reports its distance from the camera, impact point, and the name of the mesh.+- +
Sort the results by distance.+- +
Take the closest result, it is the mesh that was hit.+Implementing Hit Testing
++ ++ +Loading the scene
++ ++ +First initialize some shootable nodes and attach them to the scene. You will use the
+mark
object later. + +Node shootables; Geometry mark; + @Override public void simpleInitApp() { initCrossHairs(); initKeys(); initMark(); + shootables = new Node("Shootables"); rootNode.attachChild(shootables); shootables.attachChild(makeCube("a Dragon", -2f, 0f, 1f)); @@ -180,27 +281,53 @@ class="level3">First initialize some shootable nodes and attach them to the shootables.attachChild(makeCube("the Sheriff", 0f, 1f,-2f)); shootables.attachChild(makeCube("the Deputy", 1f, 0f, -4)); shootables.attachChild(makeFloor()); - }
Setting Up the Input Listener
+ +Next you declare the shooting action. It can be triggered either by clicking, or by pressing the space bar. The
initKeys()
method is called fromsimpleInitApp()
to set up these input mappings./** Declaring the "Shoot" action and its triggers. */ + }+ +Setting Up the Input Listener
++ ++ +Next you declare the shooting action. It can be triggered either by clicking, or by pressing the space bar. The
+initKeys()
method is called fromsimpleInitApp()
to set up these input mappings. + +/** Declaring the "Shoot" action and its triggers. */ private void initKeys() { inputManager.addMapping("Shoot", // Declare... new KeyTrigger(KeyInput.KEY_SPACE), // trigger 1: spacebar, or new MouseButtonTrigger(0)); // trigger 2: left-button click inputManager.addListener(actionListener, "Shoot"); // ... and add. - }Defining the Picking Action
+ +Next we implement the ActionListener that responds to the Shoot trigger with an action. The action follows the ray casting algorithm described above:
For every click or press of the spacebar, theShoot
action is triggered. The action casts a ray forward and determines intersections with shootable objects (= ray casting). For any target that has been hit, it prints name, distance, and coordinates of the hit. Finally it attaches a red mark to the closest result, to highlight the spot that was actually hit. When nothing was hit, the results list is empty, and the red mark is removed.Note how it prints a lot of output to show you which hits were registered.
/** Defining the "Shoot" action: Determine what was hit and how to respond. */ + }+ +Picking Action Using Crosshairs
++ ++ +Next we implement the ActionListener that responds to the Shoot trigger with an action. The action follows the ray casting algorithm described above: +
++
+ +- +
For every click or press of the spacebar, the+Shoot
action is triggered.- +
The action casts a ray forward and determines intersections with shootable objects (= ray casting).+- +
For any target that has been hit, it prints name, distance, and coordinates of the hit.+- +
Finally it attaches a red mark to the closest result, to highlight the spot that was actually hit.+- +
When nothing was hit, the results list is empty, and the red mark is removed.++ +Note how it prints a lot of output to show you which hits were registered. +
+/** Defining the "Shoot" action: Determine what was hit and how to respond. */ private ActionListener() { @Override public void onAction(String name, boolean keyPressed, float tpf) { @@ -234,56 +361,182 @@ class="li"> When nothing was hit, the results list is empty, and the red mark is } } } - };Tip: Notice how you use the provided method
results.getClosestCollision().getContactPoint()
to determine the closest hit's location. If your game includes a "weapon" or "spell" that can hit multiple targets, you would instead loop over the list of results, and interact with each of them.Exercises
After a hit was registered, the closest object is identified as target, and marked with a red dot. -Modify the code sample to solve these exercises:
Magic Spell
Change the color of the closest clicked target!
Here are some tips:
Go to the line where the closest target is indentified, and add you changes after that. To change an object's color, you must first know its node. Identify the node by identifying the target's name.
UserootNode.getChild(closest.getGeometry().getName())
Create a new color material and set the node's Material to this color.
Look inside themakeCube()
method for an example of how to set random colors.Shoot at a Character
Shooting boxes isn't very exciting – can you add code that loads and positions a model in the scene, and shoot at it?
Tip: You can useSpatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
from the engine's jme3-test-data.jar.Pick up into Inventory
Change the code as follows to simulate the player picking up objects into the inventory: When you click once, the closest target is identified and detached from the scene. When you click a second time, the target is reattached at the location that you have clicked. Here are some tips:
Create an inventory node to store the detached nodes. The inventory node is not attached to the rootNode. You can make the inventory visible by attaching the inventory node to the guiNode (which attaches it to the HUD). Note the following caveats:
If your nodes use a lit Material (not "Unshaded.j3md"), also add a light to the guiNode. Size units are pixels in the HUD, therefor a 2-wu cube is displayed only 2 pixels wide. – Scale it. Position the nodes: The bottom left corner of the HUD is (0f,0f), and the top right corner is at (settings.getWidth(),settings.getHeight()).Conclusion
- \ No newline at end of file + };You have learned how to use ray casting to solve the task of determining what object a user selected on the screen. You learned that this can be used for a variety of interactions, such as shooting, opening, picking up and dropping items, pressing a button or lever, etc.
Use your imagination from here:
In your game, the click can trigger any action on the identified object: Detach it and put it into the inventory, attach something to it, trigger an animation or effect, open a door or crate, – etc. In your game, you could replace the red mark with a particle emitter, add an explosion effect, play a sound, calculate the new score after each hit depending on what was hit – etc.Now, wouldn't it be nice if those targets and the floor were solid objects and you could walk among them? Let's continue with Collision Detection.
+Tip: Notice how you use the provided method results.getClosestCollision().getContactPoint()
to determine the closest hit's location. If your game includes a "weapon" or "spell" that can hit multiple targets, you could also loop over the list of results, and interact with each of them.
+
+ +The above example assumes that the player is aiming crosshairs (attached to the center of the screen) at the target. But you can change the picking code to allow you to freely click at objects in the scene with a visible mouse pointer. In order to do this, and you have to convert the 2d screen coordinates of the click to 3D world coordinates to get the start point of the picking ray. + +
+... +CollisionResults results = new CollisionResults(); +Vector2f click2d = inputManager.getCursorPosition(); +Vector3f click3d = cam.getWorldCoordinates( + new Vector2f(click2d.x, click2d.y), 0f).clone(); +Vector3f dir = cam.getWorldCoordinates( + new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); +Ray ray = new Ray(click3d, dir); +shootables.collideWith(ray, results); +...+ +
+Use this together with inputManager.setCursorVisible(true)
to make certain the cursor is visible.
+
+Note that since you now use the mouse for picking, you can no longer use it to rotate the camera. If you want to have a visible mouse pointer for picking in your game, you have to redefine the camera rotation mappings. +
+ ++ +After a hit was registered, the closest object is identified as target, and marked with a red dot. +Modify the code sample to solve these exercises: +
+ +
+
+Change the color of the closest clicked target!
+Here are some tips:
+
Geometry g = closest.getGeometry();
makeCube()
method for an example of how to set random colors.+ +Shooting boxes isn't very exciting – can you add code that loads and positions a model in the scene, and shoot at it? +
+Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
from the engine's jme3-test-data.jar.+ +Change the code as follows to simulate the player picking up objects into the inventory: When you click once, the closest target is identified and detached from the scene. When you click a second time, the target is reattached at the location that you have clicked. Here are some tips: +
++ +You have learned how to use ray casting to solve the task of determining what object a user selected on the screen. You learned that this can be used for a variety of interactions, such as shooting, opening, picking up and dropping items, pressing a button or lever, etc. +
+ ++Use your imagination from here: +
++ +Now, wouldn't it be nice if those targets and the floor were solid objects and you could walk around between them? Let's continue to learn about Collision Detection. + +
++See also: + +
+ Previous: Installing JME3,
-Next: Hello Node
This tutorial assumes that you have already downloaded and set up jMonkeyEngine3 in an IDE of your choice, and are able to run the bundled samples.
You are ready to create your first jMonkeyEngine3 game! You can generally use the tutorials in this introductory series with any integrated development environment (IDE), such as the jMonkeyPlatform, NetBeans, Eclipse, or run them straight from the commandline.
Create a jme3test.helloworld
package and a file HelloJME3.java
in it.
In NetBeans, you would right-click the Source Packages node
New… > Java Class
to create a new file.HelloJME3
jme3test.helloworld
Replace the contents of the HelloJME3.java file with the following code:
package jme3test.helloworld; + ++ +JME 3 Tutorial (1) - Hello SimpleApplication
++ ++ ++ +Previous: Installing JME3, +Next: Hello Node +
+ ++Prerequisites: This tutorial assumes that: +
++
+ +- +
You have (or set up the jMonkeyEngine3 in another IDE of your choice)+- +
++ +You are ready to create your first jMonkeyEngine3 game! You can generally use the tutorials in this introductory series with any integrated development environment (IDE), such as the jMonkeyPlatform, NetBeans, Eclipse, or run them straight from the commandline. In the following, we will use the jMonkeyPlatform. +
+ +Writing a SimpleApplication
++ ++ ++ +Create a
+ +jme3test.helloworld
package and a fileHelloJME3.java
in it. ++In the jMonkeyPlatform, you right-click the Source Packages node: +
++
+ +- +
Select+New… > Java Class
to create a new file.- +
Enter a class name:+HelloJME3
- +
Enter a package:+jme3test.helloworld
- +
Click Finish.+Sample Code
++ ++ +Replace the contents of the HelloJME3.java file with the following code: +
+package jme3test.helloworld; + import com.jme3.app.SimpleApplication; import com.jme3.material.Material; import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; import com.jme3.scene.shape.Box; import com.jme3.math.ColorRGBA; + /** Sample 1 - how to get started with the most simple JME 3 application. * Display a blue 3D cube and view from all sides by * moving the mouse and pressing the WASD keys. */ public class HelloJME3 extends SimpleApplication { + public static void main(String[] args){ HelloJME3 app = new HelloJME3(); - app.start(); + app.start(); // start the game } + @Override public void simpleInitApp() { - Box(Vector3f.ZERO, 1, 1, 1); - Geometry geom = new Geometry("Box", b); - Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", ColorRGBA.Blue); - geom.setMaterial(mat); - rootNode.attachChild(geom); + Box(Vector3f.ZERO, 1, 1, 1); // create cube shape at the origin + Geometry geom = new Geometry("Box", b); // create cube geometry from the shape + Material mat = new Material(assetManager, + "Common/MatDefs/Misc/Unshaded.j3md"); // create a simple material + mat.setColor("Color", ColorRGBA.Blue); // set color of material to blue + geom.setMaterial(mat); // set the cube's material + rootNode.attachChild(geom); // make the cube appear in the scene } -}
Build and run the HelloJME3 class. If a jme settings dialog pops up, confirm the default settings.
You should see a simple window displaying a 3-D cube. Use the WASD keys and the mouse to navigate around. Press Escape to close the application.Congratulations, it works! How did we do that?
Understanding the Code
Here some basic rules that are valid for all JME3 games:
Starting the Game
Note that the HelloJME3.java class extends
com.jme3.app.SimpleApplication
, which is a subclass ofcom.jme3.app.Application
. Every JME3 game is an instance ofcom.jme3.app.Application
(directly, or indirectly).
To run a JME3 game, you first instantiate yourApplication
-based class, and then call itsstart()
method:HelloJME3 app = new HelloJME3(); -app.start();Usually, you do that from your Java application's main method.
Tip: Advanced Java developers may want to make a copy ofSimpleApplication
and use it as a template for a custom application class.Initializing the Scene
+ +This simple "game" consists of nothing but a cube. Here is how we create it, position it, give it a color, and attach it to the scene. (We will have a closer look at the details later.)
public void simpleInitApp() { - Box(Vector3f.ZERO, 1, 1, 1); // create cube shape +}+ ++Build and run the HelloJME3 class. If a jme settings dialog pops up, confirm the default settings. +
++
+ +- +
You should see a simple window displaying a 3-D cube.+- +
Use the WASD keys and the mouse to navigate around.+- +
Press Escape to close the application.++Congratulations, it works! How did we do that? +
+ +Understanding the Code
++ ++ ++ +The code above has initialized the scene, and started the game. +
+ +Understanding the Terminology
+++ ++ ++
+ +What you want to do How you say it in JME3 terminology ++ +You want to create a cube. You create a Geometry with a 1x1x1 Box shape. ++ +You want to use a blue color. You create a Material with a blue Color property. ++ +You want to colorize the cube blue. You set the Geometry's Material. ++ +You want the cube to appear in the scene. You attach the cube to the rootNode. +Initializing the Scene
++ ++ +In Java, the creation of a blue cube looks as follows: +
+public void simpleInitApp() { + Box(Vector3f.ZERO, 1, 1, 1); // create cube shape at the origin Geometry geom = new Geometry("Box", b); // create cube geometry from the shape Material mat = new Material(assetManager, - "Common/MatDefs/Misc/Unshaded.j3md"); // create a simple material - mat.setColor("Color", ColorRGBA.Blue); // set color of material + "Common/MatDefs/Misc/Unshaded.j3md"); // create a simple material + mat.setColor("Color", ColorRGBA.Blue); // set color of material to blue geom.setMaterial(mat); // set the cube's material - rootNode.attachChild(geom); // attach the cube to the scene - }The
simpleInitApp()
method is automatically called once at the beginning of every JME3 game. In this method you create or load game objects before the game starts! Here is the usual process:
Initialize game objects:
Create or load all objects, and position them. To make a geometry (like the box) appear in the scene, attach it to therootNode
. Examples: Load player, terrain, sky, enemies, obstacles, and place them in their start positions. Initialize game variables
Game variables track the game state. Set them to their start values. Examples: Here you set thescore
to 0, andhealth
to 100%, and so on. Initialize navigation
The following key bindings are pre-configured by default:
W,A,S,D keys – Move around Mouse and arrow keys – Turn the camera Escape key - Quit gameThe important part is: The JME3 Application has a
rootNode
object. Your game automatically inherits the rootNode. Everything attached to the rootNode appears in the scene. Or in other words: An object that has been created, but is not attached to the rootNode, remains invisible.Conclusion
- \ No newline at end of file + rootNode.attachChild(geom); // make the cube appear in the scene + }These few lines of code do nothing but display a static object in 3-D, but they already allow you to navigate around in 3D. You have learned that a SimpleApplication is a good starting point because it provides you with:
asimpleInitApp()
method to initialize the game objects arootNode
where you attach geometries to make them appear in the scene useful default navigation settingsIn a real game, you will want to:
Initialize the game world, Trigger actions in the event loop, Respond to user input.In the following tutorials you will learn how these tasks are accomplished with the jMonkeyEngine 3!
Continue with the Hello Node tutorial, where we will first show you more details about how to initialize the game world, also known as the scene graph.See also: SimpleApplication from the commandline
+In the simpleInitApp()
method, you create or load all game objects before the game starts. The simpleInitApp()
method is automatically called once at the beginning of every JME3 game.
+
+A typical JME3 game has the following initialization process: +
+rootNode
.score
to 0, set health
to 100%, and so on.
+
+The HelloJME3.java class extends com.jme3.app.SimpleApplication
, which is a subclass of com.jme3.app.Application
. Every JME3 game is an instance of com.jme3.app.SimpleApplication
.
+
+To run a JME3 game, you first instantiate your SimpleApplication
-based class, and then call its start()
method:
+
public static void main(String[] args){ + HelloJME3 app = new HelloJME3(); + app.start(); // start the game + }+ +
+Typically you start a game from your Java application's main() method. +
+ ++ +These few lines of code simply display a static 3D cube. You can navigate around in this 3D scene. +
+ ++You have learned that a SimpleApplication is a good starting point because it provides you with: +
+simpleInitApp()
method where you create objects.rootNode
where you attach objects to make them appear in the scene.+ +When developing a game application, you will now want to: +
++In the following tutorials you learn how accomplish these tasks with the jMonkeyEngine 3. +
+ ++Continue with the Hello Node tutorial, where we will first show you more details about how to initialize the game world, also known as the scene graph. + +
++See also: +
+Previous: Hello Collision, -Next: Hello Audio
One way to create a 3D landscape is to sculpt a huge terrain model. This will give you a lot of artistic freedom – but rendering such a huge model can be quite slow. jME supports heightmaps to solve this common performance issue of terrains.
This tutorial explains how to create terrains from heightmaps and how to use splat textures to make the terrain look good.
package jme3test.helloworld; + +JME 3 Tutorial (10) - Hello Terrain
++ ++ ++Previous: Hello Collision, +Next: Hello Audio +
+ ++One way to create a 3D landscape is to sculpt a huge terrain model. This gives you a lot of artistic freedom – but rendering such a huge model can be quite slow. This tutorial explains how to create fast-rendering terrains from heightmaps, and how to use texture splatting to make the terrain look good. +
+ ++ +
+ +Sample Code
++package jme3test.helloworld; import com.jme3.app.SimpleApplication; import com.jme3.material.Material; -import com.jme3.renderer.Camera; import com.jme3.terrain.geomipmap.TerrainLodControl; import com.jme3.terrain.heightmap.AbstractHeightMap; import com.jme3.terrain.geomipmap.TerrainQuad; -import com.jme3.terrain.heightmap.HillHeightMap; // is used in example 2 +import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator; +import com.jme3.terrain.heightmap.HillHeightMap; // for exercise 2 import com.jme3.terrain.heightmap.ImageBasedHeightMap; import com.jme3.texture.Texture; import com.jme3.texture.Texture.WrapMode; -import java.util.ArrayList; -import java.util.List; import jme3tools.converters.ImageToAwt; public class HelloTerrain extends SimpleApplication { @@ -38,47 +48,52 @@ public class HelloTerrain extends SimpleApplication { flyCam.setMoveSpeed(50); /** 1. Create terrain material and load four textures into it. */ - mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md"); + mat_terrain = new Material(assetManager, + "Common/MatDefs/Terrain/Terrain.j3md"); /** 1.1) Add ALPHA map (for red-blue-green coded splat textures) */ - mat_terrain.setTexture("m_Alpha", - assetManager.loadTexture("Textures/Terrain/splat/alphamap.png")); + mat_terrain.setTexture("Alpha", assetManager.loadTexture( + "Textures/Terrain/splat/alphamap.png")); - /** 1.2) Add GRASS texture into the red layer (m_Tex1). */ - Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg"); + /** 1.2) Add GRASS texture into the red layer (Tex1). */ + Texture grass = assetManager.loadTexture( + "Textures/Terrain/splat/grass.jpg"); grass.setWrap(WrapMode.Repeat); - mat_terrain.setTexture("m_Tex1", grass); - mat_terrain.setFloat("m_Tex1Scale", 64f); + mat_terrain.setTexture("Tex1", grass); + mat_terrain.setFloat("Tex1Scale", 64f); - /** 1.3) Add DIRT texture into the green layer (m_Tex2) */ - Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"); + /** 1.3) Add DIRT texture into the green layer (Tex2) */ + Texture dirt = assetManager.loadTexture( + "Textures/Terrain/splat/dirt.jpg"); dirt.setWrap(WrapMode.Repeat); - mat_terrain.setTexture("m_Tex2", dirt); - mat_terrain.setFloat("m_Tex2Scale", 32f); + mat_terrain.setTexture("Tex2", dirt); + mat_terrain.setFloat("Tex2Scale", 32f); - /** 1.4) Add ROAD texture into the blue layer (m_Tex3) */ - Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg"); + /** 1.4) Add ROAD texture into the blue layer (Tex3) */ + Texture rock = assetManager.loadTexture( + "Textures/Terrain/splat/road.jpg"); rock.setWrap(WrapMode.Repeat); - mat_terrain.setTexture("m_Tex3", rock); - mat_terrain.setFloat("m_Tex3Scale", 128f); + mat_terrain.setTexture("Tex3", rock); + mat_terrain.setFloat("Tex3Scale", 128f); /** 2. Create the height map */ - final Texture heightMapImage = - assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); - final AbstractHeightMap heightmap = - new ImageBasedHeightMap( - ImageToAwt.convert( - heightMapImage.getImage(), false, true, 0)); + AbstractHeightMap heightmap = null; + Texture heightMapImage = assetManager.loadTexture( + "Textures/Terrain/splat/mountains512.png"); + heightmap = new ImageBasedHeightMap( + ImageToAwt.convert(heightMapImage.getImage(), false, true, 0)); heightmap.load(); - /** 3. We have prepared material and heightmap. Now we create the actual terrain: - * 3.1) We create a TerrainQuad and name it "my terrain". + /** 3. We have prepared material and heightmap. + * Now we create the actual terrain: + * 3.1) Create a TerrainQuad and name it "my terrain". * 3.2) A good value for terrain tiles is 64x64 -- so we supply 64+1=65. * 3.3) We prepared a heightmap of size 512x512 -- so we supply 512+1=513. * 3.4) As LOD step scale we supply Vector3f(1,1,1). - * 3.5) At last, we supply the prepared heightmap itself. + * 3.5) We supply the prepared heightmap itself. */ - terrain = new TerrainQuad("my terrain", 65, 513, heightmap.getHeightMap()); + int patchSize = 65; + terrain = new TerrainQuad("my terrain", patchSize, 513, heightmap.getHeightMap()); /** 4. We give the terrain its material, position & scale it, and attach it. */ terrain.setMaterial(mat_terrain); @@ -87,187 +102,471 @@ public class HelloTerrain extends SimpleApplication { rootNode.attachChild(terrain); /** 5. The LOD (level of detail) depends on were the camera is: */ - List<Camera> cameras = new ArrayList<Camera>(); - cameras.add(getCamera()); - TerrainLodControl control = new TerrainLodControl(terrain, cameras); + TerrainLodControl control = new TerrainLodControl(terrain, getCamera()); + control.setLodCalculator( new DistanceLodCalculator(patchSize, 2.7f) ); // patch size, and a multiplier terrain.addControl(control); - } -}
When you run this sample you should see a landscape with dirt mountains, grass plains, plus some winding roads in between.
What is a Heightmap?
Heightmaps are an efficient way of representing the shape of a hilly landscape. Picture a heightmap as a float array containing height values between 0f and 255f. Here is a very simple example of a heightmap with 25 height values.
Important things to note:
Low values (e.g. 0 or 50) are valeys. High values (e.g. 200, 255) are hills. We only specified a few points, and the engine interpolates the rest. Interpolation is more efficient than creating a model with several millions of vertices.Now when looking at Java data types to hold an array of floats between 0 and 255, the Image class comes to mind. Storing a terrain's height values as a grayscale image has one big advantage: The outcome is a very userfriendly, almost topographical, representation of a landscape:
Low values (e.g. 0 or 50) are dark gray – these are valleys. High values (e.g. 200, 255) are light grays – these are hills.Look at the next screenshot: In the top left you see the 128x128 grayscale image (heightmap) that was used as a base to generate the depicted terrain. To make the hilly shape better visible, the mountain tops are colored white, valleys brown, and the areas inbetween green:
In a real game, you will want to use more complex and smoother terrains than the simple heightmaps shown here. Heightmaps typically have square sizes of 512x512 or 1024x1024, and contain hundred thousands to 1 million height values. No matter which size, the concept is the same as described here.
Looking at the Heightmap Code
The first step is always to create the heightmap. You can create it yourself in any standard graphic application. Make sure it has the following properties:
The size must be square and a power of two
Examples: 128x128, 256x256, 512x512, 1024x1024 Color mode: 255 grayscales.
If you supply a color image, it will be converted to grayscale (with possibly weird results). Save it as a normal .jpg or .png fileThe file
mountains512.png
that you see here is a typical example of a heightmap.Here is how you create the heightmap object in your jME code:
Create a Texture object Load your prepared heightmap texture into the texture object Create an AbstractHeightmap object from an ImageBasedHeightMap.
ImageBasedHeightMap expects the following parameters:
AnImageToAwt.convert()
ed image file A boolean whether you are using 16 bit – here false. A boolean whether you are using an alphamap – here true. Load the heightmap.final Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); - final AbstractHeightMap heightmap = - new ImageBasedHeightMap( - ImageToAwt.convert( - heightMapImage.getImage(), false, true, 0)); - heightmap.load();What is Texture Splatting?
Texture splatting allows you create a custom textured material and "paint" on it. This is very useful for terrains: As you see in the example here, you can paint a grass texture into the valleys, a dirt texture onto the mountains, and free-form roads inbetween.
How is it done? We have three texture layers to paint on,
m_Tex1
,m_Tex2
andm_Tex3
(these names are found by opening the Terrain.j3md file, under the Material Parameters section; they may be changed) . before we start we have to make a few decisions:
You will "paint" three texture layers with three colors: Red, blue and, green. We arbitrarily chose that…
… everything red will be grass – this goes into layerm_Tex1
… everything green will be dirt – this goes into layerm_Tex2
… everything blue will be roads – this goes into layerm_Tex3
Now we start painting the texture:
Make a copy of your terrains heightmap,mountains512.png
, so you know the shape of the landscape. Name the copyalphamap.png
. Openalphamap.png
in a graphic editor and switch the image mode to color image.
Paint the black valleys in the image red – this will be the grass. Paint the white hills in shades of green – this will be the dirt of the mountains. Paint blue lines where you want roads to cross the landscape. The end result should look similar to this:Note: In the future, the jMonkeyPlatform will take over some of these steps so you don't have to worry about the details.
Looking at the Texturing Code
+ +As usual, we create a Material object. We base it on the Material Definition
Terrain.j3md
that is included in the jME3 framework.Material mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");Now we load four textures into this material. The first one,
m_Alpha
, is the alphamap that we just created.mat_terrain.setTexture("m_Alpha", - assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));Three other textures are the layers that we have previously decided to paint: grass, dirt, and road. We create texture objects and load the three textures as usual. Note how we assign them to their respective texture layers (m_Tex1, m_Tex2, and m_Tex3) inside the Material!
/** 1.2) Add GRASS texture into the red layer (m_Tex1). */ - Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg"); +}+ ++When you run this sample you should see a landscape with dirt mountains, grass plains, plus some winding roads in between. +
+ +What is a Heightmap?
++ ++ ++ +Heightmaps are an efficient way of representing the shape of a hilly landscape. Not every pixel of the landscape is stored, instead, a grid of sample values is used to outline the terrain height at certain points. The heights between the samples is interpolated. +
+ ++In Java, a heightmap is a float array containing height values between 0f and 255f. Here is a very simple example of a terrain generated from a heightmap with 5x5=25 height values. +
+ ++ +
+ ++Important things to note: +
++
+ +- +
Low values (e.g. 0 or 50) are valeys.+- +
High values (e.g. 200, 255) are hills.+- +
The heightmap only specifies a few points, and the engine interpolates the rest. Interpolation is more efficient than creating a model with several millions vertices.++ +When looking at Java data types to hold an array of floats between 0 and 255, the Image class comes to mind. Storing a terrain's height values as a grayscale image has one big advantage: The outcome is a very userfriendly, like a topographical map: +
++
+ +- +
Low values (e.g. 0 or 50) are dark gray – these are valleys.+- +
High values (e.g. 200, 255) are light grays – these are hills.++ +Look at the next screenshot: In the top left you see a 128x128 grayscale image (heightmap) that was used as a base to generate the depicted terrain. To make the hilly shape better visible, the mountain tops are colored white, valleys brown, and the areas inbetween green: +
+ ++} +
+ ++In a real game, you will want to use more complex and smoother terrains than the simple heightmaps shown here. Heightmaps typically have square sizes of 512x512 or 1024x1024, and contain hundred thousands to 1 million height values. No matter which size, the concept is the same as described here. +
+ +Looking at the Heightmap Code
++ ++ ++ + +
+ ++The first step of terrain creation is the heightmap. You can create one yourself in any standard graphic application. Make sure it has the following properties: +
++
+ +- +
The size must be square, and a power of two.++
+- +
Examples: 128x128, 256x256, 512x512, 1024x1024+- +
Color mode must be 255 grayscales.++
+- +
Don't supply a color image, it will be interpreted as grayscale, with possibly weird results.+- +
Save the map as a .jpg or .png image file++ +The file
+ +mountains512.png
that you see here is a typical example of an image heightmap. ++Here is how you create the heightmap object in your jME code: +
++
+- +
Create a Texture object.+- +
Load your prepared heightmap image into the texture object.+- +
Create an AbstractHeightmap object from an ImageBasedHeightMap.+
+ImageBasedHeightMap expects the following parameters:+
+- +
An+ImageToAwt.convert()
ed image file.- +
A boolean whether you are using 16-bit – here: false, this image is 8-bit.+- +
A boolean whether you are using an alphamap – here: true, you will use one.+- +
Load the heightmap.+AbstractHeightMap heightmap = null; + Texture heightMapImage = assetManager.loadTexture( + "Textures/Terrain/splat/mountains512.png"); + heightmap = new ImageBasedHeightMap( + ImageToAwt.convert(heightMapImage.getImage(), false, true, 0)); + heightmap.load();+ +What is Texture Splatting?
++ ++ ++ +Previously you learned how to create a material for a simple shape such as a cube. All sides of the cube have the same color. You can apply the same material to a terrain, but then you have one big meadow, one big rock desert, etc. This is not always what you want. +
+ ++Texture splatting allows you create a custom material, and "paint" textures on it like with a "paint brush". This is very useful for terrains: As you see in the example here, you can paint a grass texture into the valleys, a dirt texture onto the mountains, and free-form roads inbetween. +
+ ++
The jMonkeyPlatform comes with a TerrainEditor plugin. Using the TerrainEditor plugin, you can sculpt the terrain with the mouse, and save the result as heightmap. You can paint textures on the terrain and the plugin saves the resulting splat textures as alphamap(s). The following paragraphs describe the manual process for you. You can choose to create the terrain by hand, or using the TerrainEditor plugin. ++ + ++Splat textures are based on the
+ +Terrain.j3md
material defintion. If you open the Terrain.j3md file, and look in the Material Parameters section, you see that you have several texture layers to paint on:Tex1
,Tex2
,Tex3
, etc. ++Before you can start painting, you have to make a few decisions: +
++
+ +- +
Choose three textures. For example grass.jpg, dirt.jpg, and road.jpg.+- +
You "paint" three texture layers by using three colors: Red, blue and, green. You arbitrarily decide that…++
+- +
… everything red will be grass – red goes into layer+Tex1
- +
… everything green will be dirt – green goes into layer+Tex2
- +
… everything blue will be roads – blue goes into layer+Tex3
+ +Now you start painting the texture: +
++
+ +- +
Make a copy of your terrains heightmap,+mountains512.png
. You want it as a reference for the shape of the landscape.- +
Name the copy+alphamap.png
.- +
Open+alphamap.png
in a graphic editor and switch the image mode to color image.+
+- +
Paint the black valleys red – this will be the grass.+- +
Paint the white hills green – this will be the dirt of the mountains.+- +
Paint blue lines where you want roads to criss-cross the landscape.+- +
The end result should look similar to this:++ ⇒ +
+ +Looking at the Texturing Code
++ ++ +As usual, you create a Material object. Base it on the Material Definition
+Terrain.j3md
that is included in the jME3 framework. +Material mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");+ ++Load four textures into this material. The first one,
+Alpha
, is the alphamap that you just created. +mat_terrain.setTexture("Alpha", + assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));+ ++The three other textures are the layers that you have previously decided to paint: grass, dirt, and road. You create texture objects and load the three textures as usual. Note how you assign them to their respective texture layers (Tex1, Tex2, and Tex3) inside the Material! +
+/** 1.2) Add GRASS texture into the red layer (Tex1). */ + Texture grass = assetManager.loadTexture( + "Textures/Terrain/splat/grass.jpg"); grass.setWrap(WrapMode.Repeat); - mat_terrain.setTexture("m_Tex1", grass); - mat_terrain.setFloat("m_Tex1Scale", 64f); + mat_terrain.setTexture("Tex1", grass); + mat_terrain.setFloat("Tex1Scale", 64f); - /** 1.3) Add DIRT texture into the green layer (m_Tex2) */ - Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"); + /** 1.3) Add DIRT texture into the green layer (Tex2) */ + Texture dirt = assetManager.loadTexture( + "Textures/Terrain/splat/dirt.jpg"); dirt.setWrap(WrapMode.Repeat); - mat_terrain.setTexture("m_Tex2", dirt); - mat_terrain.setFloat("m_Tex2Scale", 32f); + mat_terrain.setTexture("Tex2", dirt); + mat_terrain.setFloat("Tex2Scale", 32f); - /** 1.4) Add ROAD texture into the blue layer (m_Tex3) */ - Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg"); + /** 1.4) Add ROAD texture into the blue layer (Tex3) */ + Texture rock = assetManager.loadTexture( + "Textures/Terrain/splat/road.jpg"); rock.setWrap(WrapMode.Repeat); - mat_terrain.setTexture("m_Tex3", rock); - mat_terrain.setFloat("m_Tex3Scale", 128f);The individual texture scales (e.g.
mat_terrain.setFloat("m_Tex3Scale", 128f);
) depend on the size of the textures you use. You can tell you picked a too small scale if, for example, your road tiles appear like tiny grains of sand, or to big if the blades of grass look like twigs.We use
setWrap(WrapMode.Repeat)
to make the small texture fill the wide area. If the repetition is too visible, try adjusting the Tex*Scale value.Later, after we have created the actual terrain object, we must not forgot to set the material on it:
terrain.setMaterial(mat_terrain);Looking at the Terrain Generation Code
+ +Internally, the generated terrain mesh is broken down into tiles and blocks. This is an optimization for culling. You do not need to worry about tiles and blocks too much, just use recommended values for now.
Let's assume we want to generate a small 512x512 terrain. We already have created the heightmap object. Here are the steps that we perform everytime we create a new terrain.
We create a TerrainQuad with the following arguments:
Name: E.g.my terrain
. Tile size: We want to create terrain tiles of size 64x64, so we supply 64+1 = 65.
In general, 64 is a good value for terrain tiles. Block size: Since we prepared a heightmap of size 512x512, we supply 512+1 = 513.
If the the block size is double the heightmap size (1024+1=1025), you get a stretched out, wider, flatter terrain. If the the block size is half the heightmap size (256+1=257), you get a smaller, more detailed terrain. Finally, we supply the 512x512 heightmap object that we have previously created.Here's the code:
terrain = new TerrainQuad( + mat_terrain.setTexture("Tex3", rock); + mat_terrain.setFloat("Tex3Scale", 128f);+ ++The individual texture scales (e.g.
+mat_terrain.setFloat("Tex3Scale", 128f);
) depend on the size of the textures you use. ++
+ +- +
You can tell you picked too small a scale if, for example, your road tiles appear like tiny grains of sand.+- +
You can tell you picked too big a scale if, for example, the blades of grass look like twigs.++ +Use
+ +setWrap(WrapMode.Repeat)
to make the small texture fill the wide area. If the repetition is too visible, try adjusting the respectiveTex*Scale
value. +What is a Terrain?
++ ++ ++ +Internally, the generated terrain mesh is broken down into tiles and blocks. This is an optimization to make culling easier. You do not need to worry about "tiles and blocks" too much, just use recommended values for now – 64 is a good start. +
+ ++Let's assume you want to generate a 512x512 terrain. You already have created the heightmap object. Here are the steps that you perform everytime you create a new terrain. +
+ ++Create a TerrainQuad with the following arguments: +
++
+ +- +
Specify a name: E.g.+my terrain
.- +
Specify tile size: You want to terrain tiles of size 64x64, so you supply 64+1 = 65.++
+- +
In general, 64 is a good starting value for terrain tiles.+- +
Specify block size: Since you prepared a heightmap of size 512x512, you supply 512+1 = 513.++
+- +
If you supply a block size of 2x the heightmap size (1024+1=1025), you get a stretched out, wider, flatter terrain.+- +
If you supply a block size 1/2 the heightmap size (256+1=257), you get a smaller, more detailed terrain.+- +
Supply the 512x512 heightmap object that you created.+Looking at the Terrain Code
++ ++ +Here's the code: + +
+terrain = new TerrainQuad( "my terrain", // name 65, // tile size 513, // block size - heightmap.getHeightMap()); // heightmapDon't forget to attach the terrain to the rootNode. You can scale and translate the terrain just like any other Spatial.
Looking at the Level of Detail Code
+ +jME3 includes an optimization that adjusts the level of detail of the rendered terrain depending on how close or far the camera is.
List<Camera> cameras = new ArrayList<Camera>(); + heightmap.getHeightMap()); // heightmap+ ++You have created the terrain object. +
++
+ +- +
Remember to apply the created material:+terrain.setMaterial(mat_terrain);+- +
Remember to attach the terrain to the rootNode.+rootNode.attachChild(terrain);+- +
If needed, scale and translate the terrain object, just like any other Spatial.++ +Tip: Terrain.j3md is an unshaded material definition, so you do not need a light source. You can also use TerrainLighting.j3md plus a light, if you want a shaded terrain. +
+ +What is LOD (Level of Detail)?
++ ++ +JME3 includes an optimization that adjusts the level of detail (LOD) of the rendered terrain depending on how close or far the camera is. +
+List<Camera> cameras = new ArrayList<Camera>(); cameras.add(getCamera()); TerrainLodControl control = new TerrainLodControl(terrain, cameras); - terrain.addControl(control);Close parts of the terrain are rendered in full detail. Terrain parts that are further away are not clearly visible anyway, and jME improves performance by rendering them less detailed. This way you can afford to load huge terrains with no penalty caused by invisible details.
Exercises
Exercise 1: Texture Layers
+ +What happens if you swap two layers, for example
m_Tex1
andm_Tex2
?... -mat_terrain.setTexture("m_Tex2", grass); + terrain.addControl(control);+ ++Close parts of the terrain are rendered in full detail. Terrain parts that are further away are not clearly visible anyway, and JME3 improves performance by rendering them less detailed. This way you can afford to load huge terrains with no penalty caused by invisible details. +
+ +Exercises
++ ++ +Exercise 1: Texture Layers
++ ++ +What happens when you swap two layers, for example
+Tex1
andTex2
? + +... +mat_terrain.setTexture("Tex2", grass); ... -mat_terrain.setTexture("m_Tex1", dirt);It's easier to swap layers in the code than to change the color in the alphamap.
Exercise 2: Randomized Terrains
+ +These two lines generate the hightmap from a user defined image:
Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); +mat_terrain.setTexture("Tex1", dirt);+ ++You see it's easier to swap layers in the code, than to change the colors in the alphamap. +
+ +Exercise 2: Randomized Terrains
++ ++ ++ +The following two lines generate the heightmap object based on your user-defined image: +
+Texture heightMapImage = assetManager.loadTexture( + "Textures/Terrain/splat/mountains512.png"); heightmap = new ImageBasedHeightMap( - ImageToAwt.convert(heightMapImage.getImage(), false, true, 0), 1f);You can also let jME create a random landscape.
What result do you get when you replace the two heightmap lines by the following lines and run the sample?HillHeightMap heightmap = null; - + ImageToAwt.convert(heightMapImage.getImage(), false, true, 0), 1f);+ ++Instead, you can also let JME3 generate a random landscape for you: +
++
What result do you get when you replace the above two heightmap lines by the following lines and run the sample?HillHeightMap heightmap = null; try { heightmap = new HillHeightMap(513, 1000, 50, 100, (byte) 3); } catch (Exception ex) { ex.printStackTrace(); -}
Change one value at a time and the run the sample again. Note the differences. Can you find out which of the values has which effect on the generated terrain?
Which value controls the size?
What happens if the size is not a square number +1 ? Which value controls the number of hills generated? Which values control the minimum and maximum radius of the hills?
What happens if the minimum is bigger than the maximum? Which value controls the flattening of the hills?
What happens if this value is 1 ?Tip: You can keep using the splatted texture from the sample code above. Just don't be surprised that the textures do not automatically adjust to the randomized landscape.
Conclusion
- \ No newline at end of file +} +You have learned how to create terrains that are more efficient as loading a giant model. You are now also able to texture a terrain.
In the next chapter, you will learn how to add sounds to your game.
See also: Terrain Collision
+ +Tip: For this exercise, you can keep using the splat Material from the sample code above. Just don't be surprised that the Material does not match the shape of the newly randomized landscape. If you want to generate real matching splat textures for randomized heightmaps, you need to write a custom method that, for example, creates an alphamap from the heightmap by replacing certain grayscales with certain RGB values. +
+ ++ +Can you combine what you learned here and in Hello Collision, and make the terrain solid? +
+ ++ +You have learned how to create terrains that are more efficient as loading one giant model. You know how to create generate random or handmade heightmaps. You can add a LOD control to render large terrains faster. You are aware that you can combine what you learned about collison detection to make the terrain solid to a physical player. You are also able to texture a terrain "like a boss" using layered Materials and texture splatting. You are aware that the jMonkeyPlatform provides a TerrainEditor that helps with most of these manual tasks. +
+ ++Do you want to hear your players say "ouch!" when they bump into a wall or fall off a hill? Continue with learning how to add sound to your game. + +
++See also: +
++ +We recommend downloading the - but of course you can also build the jMonkeyEngine yourself from the sources. You need Subversion installed. + +
+svn checkout http://jmonkeyengine.googlecode.com/svn/trunk/engine jme3+
ant jar
dist/jMonkeyEngine3.jar
and dist/libs/*
ant javadoc
dist/javadoc
directory.ant run
dist/jMonkeyEngine3.jar
and all JARs from the dist/lib
directory on the classpath.com.jme3.app.SimpleApplication
. +Learn more about: +
+You are welcome to try out the new jME3, and contribute patches and features! This document shows how to download, set up, build, and run the latest development version from the sources. (As of Spring 2010, we are in alpha.) These instructions work in NetBeans IDE 6 or better.
Note: In the following, always replace "~" with the path to your home directory.
Check out the sources from the repository. (The following NetBeans instructions are equivalent to executing cd ~/NetBeansProjects; svn checkout http://jmonkeyengine.googlecode.com/svn/trunk/engine jme3
on the commandline.)
https://jmonkeyengine.googlecode.com/svn
trunk/engine
~/NetBeansProjects/jme3
The jme3 project opens in the Project window. It already includes a working ANT build script for building and running.
Look into the Libraries node and confirm that the project depends on the following libraries in the classpath:
jME3-natives-joal.jar lwjgl.jar gluegen-rt.jar + ++ +Setting up JME3 in Netbeans 6.x
++ ++ ++ +You are welcome to try out the new jME3, and contribute patches and features! This document shows how to download, set up, build, and run the latest development version from the sources. (As of Spring 2010, we are in alpha.) These instructions work in NetBeans IDE 6 or better. +
+ ++Note: In the following, always replace "~" with the path to your home directory. +
+ +Downloading the Sources
++ ++ +Check out the sources from the repository. (The following NetBeans instructions are equivalent to executing
+cd ~/NetBeansProjects; svn checkout jme3
on the commandline.) + ++
+ +- +
In NetBeans go to Team > Subversion > Checkout++
+- +
Repository URL:+- +
You can leave user/pw blank for anonymous access.+- +
Click Next++
+- +
Repository Folders:+trunk/engine
- +
Enable the checkbox to Skip "engine" and only checkout its contents.+- +
Local Folder:+~/NetBeansProjects/jme3
- +
Click Finish and wait.++ +The jme3 project opens in the Project window. It already includes a working ANT build script for building and running. +
+ ++Look into the Libraries node and confirm that the project depends on the following libraries in the classpath: +
+jME3-natives-joal.jar lwjgl.jar gluegen-rt.jar jME3-lwjgl-natives.jar jinput.jar swing-layout-1.0.4.jar j-ogg-oggd.jar vecmath.jar stack-alloc.jar -j-ogg-vorbisd.jar asm-all-3.1.jar jbullet.jar +j-ogg-vorbisd.jar asm-all-3.1.jar jbullet.jar jheora-jst-debug-0.6.0.jar xmlpull.xpp3-1.1.4c.jar -nifty*.jar eventbus-1.4.jarOptional: Setting up Android Support
Work in progress …
A jme3 application can either be deployed to the desktop (as Java Swing application) and web browser (as JNLP/WebStart or Applet), or to an Android phone. While the former is the default, switching to Android deployment can be done in a few steps.
Open Project Properties, go to Sources category. At Source Packages Folders, click "Add Folder".
Addsrc/android/
Removesrc/desktop
Removesrc/desktop_fx
build.xml…Build and Run
That's it!
Right-click the jme3 project node and "Clean and Build" the project. In the Projects window, browse to thesrc/test/jme3test
folder. Right-click e.g. the filesrc/test/jme3test/model/TestHoverTank.java
and choose "Run" to run a sample.
In the sample application, use the mouse and the AWSD keys to move around the test object. Press escape to quit the sample application.Sample code for cool features is in the
src/test/jme3test
folder. A sample game can be found insrc/games/jme3game/cubefield/CubeField.java
.Tips:
To run runnable classes from the Projects window, right-click and choose Run. To run any runnable class that is open in the editor, press shift-F6.Optional: Javadoc Popups and Source Navigation in NetBeans
- \ No newline at end of file +nifty*.jar eventbus-1.4.jarIf you are working on the jme3 sources:
In the Projects window, right-click the jme3 project and choose Generate Javadoc. Wait. Confirm in the Files window that the javadoc has been created in~/NetBeansProjects/jme3/dist/javadoc
In the editor, place the caret in a jme class and press ctrl-space to view javadoc.If you are working on a game project that depends on jme3:
First follow the previous tip. (In the future, we may offer jme javadoc as download instead.) In your game project, right-click the Libraries node and choose "Properties". In the Library properties, select jme3.jar and click the Edit button.
For the Javadoc field, browse to~/NetBeansProjects/jme3/dist/javadoc
. Check "as relative path" and click select. For the Sources field, browse to~/NetBeansProjects/jme3/src
. Check "as relative path" and click select. Click OK. In the editor, place the caret in a jme class and press ctrl-space to view javadoc. Ctrl-click any jme3 method to jump to its definition in the sources.This tip works for any third-party JAR library that you use. (You may have to download the javadoc/sources from their home page separately).
Sources used: BuildJme3, netbeans tutorial from forum
+ +Work in progress … +
+ ++A jme3 application can either be deployed to the desktop (as Java Swing application) and web browser (as JNLP/WebStart or Applet), or to an Android phone. While the former is the default, switching to Android deployment can be done in a few steps. + +
+src/android/
src/desktop
src/desktop_fx
+ +That's it! +
+src/test/jme3test
folder. src/test/jme3test/model/TestHoverTank.java
and choose "Run" to run a sample.
+
+Sample code for cool features is in the src/test/jme3test
folder. A sample game can be found in src/games/jme3game/cubefield/CubeField.java
.
+
+Tips: +
++ +If you are working on the jme3 sources: +
+~/NetBeansProjects/jme3/dist/javadoc
+ +If you are working on a game project that depends on jme3: +
+~/NetBeansProjects/jme3/dist/javadoc
. Check "as relative path" and click select.~/NetBeansProjects/jme3/src
. Check "as relative path" and click select.+ +This tip works for any third-party JAR library that you use. (You may have to download the javadoc/sources from their home page separately). +
++ +Sources used: , +
+ +
+Extend com.jme3.app.SimpleApplication.
+
+Learn more: Hello SimpleApplication, .
+
+
viewPort.setBackgroundColor(ColorRGBA.Blue);+ +
+Yes! For your own games, you should create a custom base class that extends class, configure application settings, and customize away.
+
+Learn more: SimpleApplication, AppSettings.
+
+During development, you can switch the severity level of the default logger to no longer print FINE warnings, but only WARNINGs. + +
+java.util.logging.Logger.getLogger("").setLevel(Level.WARNING);+ +
+ +For the release, switch the severity level of the default logger to print only SEVERE errors. + +
+java.util.logging.Logger.getLogger("").setLevel(Level.SEVERE);+ +
+ +Learn more: Logging. +
+ ++To make a spatial appear in the scene, you attach it to the rootNode, To remove a spatial, you detach it. + +
+rootNode.attachChild(spatial); // appear+
rootNode.detachChild(spatial); // remove+ +
+ +Optionally, you can control whether the engine culls an object always or never. + +
+spatial.setCullHint(CullHint.Never); // always drawn+
spatial.setCullHint(CullHint.Always); // never drawn+ +
+ +Learn more: The Scene Graph, Hello Node, Hello Asset, Spatial, and . +
+ ++First check whether the file path of the asset is correct. By default it is relative to your project's assets directory: + +
+// To load .../jMonkeyProjects/MyGame/assets/Models/Ninja/Ninja.mesh.xml +Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");+ +
+
+If you are not using the default assets
directory, verify that you have registered a locator to the AssetManager. are available.
+
+
this.assetManager.registerLocator("assets/", FileLocator.class); // default +this.assetManager.registerLocator("c:/jme3User/JMEisSoCool/myAwesomeFolder", FileLocator.class); +this.assetManager.registerLocator("town.zip", ZipLocator.class.getName());+ +
+ +Learn more: Asset Manager +
+ +
+You create 3-D models in a 3-D mesh editor, for example Blender, and export it in Ogre Mesh XML or Wavefront OBJ format.
+
+Learn more: 3D Models, , , .
+
+Use the jMonkeyPlatform to convert models from Ogre XML or Wavefront OBJ formats to .j3o binary format. Load the .j3o file using the AssetManager. + +
+// To load .../jMonkeyProjects/MyGame/assets/Models/Ninja/Ninja.mesh.xml +Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");+ +
+
+Learn more: Hello Asset, Asset Manager, , , jMonkeyPlatform j3o converter,
+
+Code sample: , .
+
+
+Use the simpleInitApp() method in SimpleApplication (or initApp() in Application).
+
+Learn more: Hello SimpleApplication, .
+
+To move or turn or resize a spatial you use transformations. You can concatenate transformations (e.g. perform rotations around several axes in one step using a Quaternion with slerp()
or a com.jme3.math.Transform with interpolateTransforms().
+
+
spatial.setLocalTranslation(1,-3,2.5f); spatial.rotate(0,3.14f,0); spatial.scale(2,2,2);+ +
+ +Learn more: Hello Node, Spatial, rotate, rotate_about_a_point, quaternion, math_for_dummies. +
+ +
+Change the geometry's translation (position) live in the update loop using setLocalTranslation() for non-physical and setWalkDirection() for physical objects. You can also define and remote-control a spatial's motion using Cinematics, e.g. to record cut scenes.
+
+Learn more: Hello Loop, Update Loop, Custom Controls, Cinematics
+
+Code sample: ,
+
Geometry result = spatial.getName().startsWith(name);+ +
+ +Learn more: Spatial +
+ +
+You can programmatically create com.jme3.scene.Mesh'es.
+
+Learn more: Custom Meshes
+
+The most likely reason is the flipping of textures. You may be using the following default method: + +
+material.setTexture("ColorMap", assetManager.loadTexture("myTexture.jpg"));+ +
+ +You can set the boolean value in the constructor of TextureKey to flipped or not flipped. Toggle the boolean to see if it fixes your UV wrapping/texture problem: + +
+material.setTexture("ColorMap", this.assetManager.loadTexture(new TextureKey("myTexture.jpg", false)));+ +
+You cannot scale a texture, but you scale the texture coordinates of the mesh the texture is applied to: + +
+mesh.scaleTextureCoordinates(new Vector2f(2,2));+ +
+
+You can choose among various com.jme3.texture.Texture.WrapMode
s for individual texture maps of a material: BorderClamp, EdgeClamp, Clamp; MirrorBorderClamp, MirrorEdgeClamp, MirrorClamp; Repeat, MirroredRepeat.
+
+
material.getTextureParam("DiffuseMap").getTextureValue().setWrap(WrapMode.Repeat);+ +
+Use the AssetManager to load Materials, and change material settings.
+
+Learn more: Hello Material, Materials Overview, Asset Manager
+
+Code sample: , .
+
+Create Textures as image files. Use the AssetManager to load a Material and set the material's texture maps.
+
+Learn more: Hello Material, Materials Overview, Asset Manager, ,
+
+Code sample:
+
+If you use a lit material (based on Lighting.j3md) then you must attach a light source to the rootNode, otherwise you see nothing. If you use lit material colors, make sure you have specified an Ambient color (can be the same as the Diffuse color) if you use an AmbientLight. If you see objects, but they are gray or too dark, set the light color to white, or make it brighter (you can multiply the color value with a scalar), or add a global light source (AmbientLight). Similarly, if everything is white, tune down the lights. If materials flicker under a directional light, change the light direction vector. Change the background color (which is independent of light sources) to get a better contrast while debugging a light problem. +
+ +
+Use com.jme3.shadow.BasicShadowRenderer together with com.jme3.light.DirectionalLight, and setShadowMode().
+
+Learn more: Light and Shadow
+
+Code sample: ,
+
+Assign a texture with an alpha channel to a Material and set the Material's blend mode to alpha. Use this to create transparent or translucent materials such as glass, window panes, water, tree leaves, etc. + +
+material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);+ +
+ +Learn more: Hello Material, Materials Overview +
+ ++You can switch the com.jme3.material.RenderState.FaceCullMode to Back, Front, FrontAndBack, or Off. This influences whether the front or backside of an object is being drawn. By default, backsides are culled (not drawn) because they are usually not visible anyway. + +
+material.getAdditionalRenderState().setFaceCullMode(FaceCullMode.FrontAndBack);+ +
+Create a material and switch its renders state to wireframe. + +
+material.getAdditionalRenderState().setWireframe(true);+ +
+ +Learn more: Debugging. +
+ +
+The default camera is the cam
object. Learn more:
+
flyCam.setEnabled(true);+
flyCam.setEnabled(false); +chaseCam = new ChaseCamera(cam, spatial, inputManager);+
flyCam.setMoveSpeed(50f);+ +
+Use Controls to define the behaviour of types of Spatials. Use Application States to implement global behaviour. Use the simpleUpdate()
loop for the remaining tests and interactions. Use Cinematics to remote-control objects in scenes.
+
+Learn more: Hello Loop, Update Loop, Custom Controls, Application States, Cinematics
+
+Use com.jme3.input.KeyInput and a Input Listener.
+
+Learn more: Hello Input, Input Handling
+
+Players use the mouse to pick up objects, to open doors, to shoot a weapon, etc. Use an Input Listener to respond to mouse clicks, then cast a ray from the player; if it intersect with the bounding volume of a spatial, this is the selected target. The links below contain code samples for both "fixed crosshair" picking and "free mouse pointer" picking.
+
+Learn more: Hello Picking, Mouse Picking, Collision and Intersection, Input Handling, com.jme3.bounding.*, com.jme3.math.Ray, com.jme3.collision.CollisionResults.
+
+Code sample:
+
+Create an animated OgreMesh model with bones in a 3-D mesh editor (e.g. Blender).
+
+Learn more: com.jme3.animation.*, Hello Animation, Animation,
+
+Code sample:
+
+Use collision detection. The most common solution is to use jme's physics integration.
+
+Learn more: Hello Collision, Physics, com.jme3.bullet.*, CapsuleCollisionShape versus CompoundCollisionShape, CharacterControl versus RigidBodyControl.
+
+Add physics controls to Spatials and give them spherical or cylindrical bounding volumes.
+
+Learn more: Hello Physics, Physics, com.jme3.bounding.*, com.jme3.bullet.collisions, com.jme3.bullet.controls.RigidBodyControl,
+
+Code sample: ,
+
+Maybe your collision shapes overlap – or they are not where you think they are. Make the collision shapes visible by adding the following line after the bulletAppState initialization: +
+bulletAppState.getPhysicsSpace().enableDebug(assetManager);+ +
+Use a CharacterControl that locks the physical object upright, so it does not tip over when moving/walking (as tall physical objects are wont to do).
+
+Learn more: CharacterControl
+
+Code samples: (first-person), (third-person)
+
+Use a VehicleControl that supports suspension behavior.
+
+Learn more: Vehicles, com.jme3.bullet.*, VehicleControl
+
+Code samples: , (Press HUJK keys to steer, spacebar to jump.)
+
+Use PhysicsControl's joints.
+
+Learn more: Hinges and Joints, com.jme3.bullet.joints.PhysicsHingeJoint,
+ (Press HK keys to turn, spacebar to swing.)
+
+In the application's simpleInitApp() method, call: +
+setDisplayFps(false); +setDisplayStatView(false);+ +
+Attach text and pictures to the orthogonal guiNode
to create a heads-up display ().
+
+Learn more: HUD, com.jme3.font.*, com.jme3.ui.Picture, guiNode.attachChild()
+
+Code sample: , |
+
+You may want to display buttons to let the player switch between the game, settings screen, and score screens. For buttons and other more advanced UI controls, jME supports the Nifty GUI library.
+
+Learn more: Nifty GUI
+
+Sample Code:
+
+Instead of having a frozen frame while your games loads, you can have a loading screen while it loads.
+
+Learn more: Loading screen
+
+Verify that you include a controls definition file link in your XML: This is the default: + +
+<useControls filename="nifty-default-controls.xml"/>
+
++ +
+ +
+Use AudioRenderer, Listener, and AudioNode from com.jme3.audio.*.
+
+Learn more: Hello Audio, Audio
+
+Code sample:
+
+For swarm like effects you use particle emitters.
+
+Learn more: Hello Effects, Particle Emitters, Bloom and Glow, Effects Overview, com.jme3.effect.EmitterSphereShape, com.jme3.effect.ParticleEmitter
+
+Code sample: ,
+
+Use a special post-processor renderer from com.jme3.water.*.
+
+Learn more: Water, Post-Processor Water
+
+Code sample: , , ,
+
+Use special post-processor renderers from com.jme3.post.*.
+
+Learn more: effects_overview
+
+Use com.jme3.terrain.*. The JMonkeyEngine also provides you with a Terrain Editor plugin.
+
+Learn more: Hello Terrain, Terrain, Terrain Editor
+
+Code sample:
+
+Code sample: + +
+rootNode.attachChild(SkyFactory.createSky( assetManager, + "Textures/Sky/Bright/BrightSky.dds", false)); +skyGeo.setQueueBucket(Bucket.Sky)+ +
+ +Learn more: Sky +
+ ++If your game is heavily using features that older cards do not support, you can add a check of the JME3 Renderer Caps. + +
+Collection<com.jme3.renderer.Caps> caps = renderer.getCaps(); +Logger.getLogger(HelloJME3.class.getName()).log(Level.INFO, "Capabilities: {0}" + caps.toString());+ +
+ +The following shortened example shows the capabilities of an older graphic card. In this case you decide whether to branch to a low-quality rendering of the unsupported features (if you still want to support this card), or print an error message explaining the user what capabilities the card is missing to play the game. + +
+INFO: Running on jMonkey Engine 3 Alpha 0.6 +INFO: Using LWJGL 2.7.1 +INFO: Selected display mode: 1024 x 768 x 0 @0Hz +INFO: Adapter: null +INFO: Driver Version: null +INFO: Vendor: ATI Technologies Inc. +INFO: OpenGL Version: 2.0 ATI-1.6.36 +INFO: Renderer: ATI Radeon X1600 OpenGL Engine +INFO: GLSL Ver: 1.20 +INFO: Timer resolution: 1.000 ticks per second +INFO: Capabilities: [FrameBuffer, FrameBufferMRT, FrameBufferMultisample, +OpenGL20, ARBprogram, GLSL100, GLSL110, GLSL120, VertexTextureFetch, +FloatTexture, TextureCompressionLATC, NonPowerOfTwoTextures]+ +
jme3tools.optimize.GeometryBatchFactory.optimize(rootNode);+ +
+Many maths functions (mult(), add(), subtract(), etc) come as local and a non-local variant (multLocal(), addLocal(), subtractLocal(), etc). +
+Quaternion q1 = q2.mult(q3);
v.mult(b).add(b);
q2.multLocal(q3)
v.multLocal(a).addLocal(b);
+World coordinates of a Spatial are its absolute coordinates in the 3D scene (this is like giving GPS coordinates). Local coordinates are relative to the Spatial's parent Spatial (this is like saying, "I'm ten meters left of the entrance"). + +
+ ++ +Multiply degree value by FastMath.DEG_TO_RAD to convert it to radians. + +
+ + Every class that extends jme3.app.Application (or jme3.app.SimpleApplication) has properties that can be configured by customizing a com.jme3.system.AppSettings
object. Configure the settings before you call app.start()
on the application object. If you change display settings during runtime, call app.restart()
to make them take effect.
Note: Other runtime settings are covered in SimpleApplication.
AppSettings settings = new AppSettings(true); -settings.setRenderer(AppSettings.LWJGL_OPENGL3); -Application app = new Application(); -app.setSettings(settings); -app.start();
Set the boolean in the AppSettings contructor to true if you want to keep the default settings for everything that you do not specify. Set this parameter to false if you want to specify each property yourself (you'll get an exception if you missed one).
Use app.setShowSettings(false);
to disable the default settings-window at startup.
Property | Dscription | Default |
---|---|---|
setRenderer(AppSettings.LWJGL_OPENGL2) setRenderer(AppSettings.LWJGL_OPENGL3) | Switch Video Renderer | OpenGL 2 |
setAudioRenderer(AppSettings.LWJGL_OPENAL) setAudioRenderer(AppSettings.LWJGL_JOAL) | Switch Audio Renderer | OpenAL |
setBitsPerPixel(8) | Set color depth. 1 = black and white, 2 bit = gray, 4 = 16 colors, 8 = 256 colors, 24 or 32 = "truecolor". | 24 |
setFrequency(60) | The screen frequency (refresh rate of the graphics card), usually 60 or 75 fps. | 60 fps |
setFramerate(60) | How often per second the engine should try to refresh the frame. For the release, usually 60 fps. Can be lower (59-30) for FX-intensive games. Do not set to a higher value than the screen frequency. | -1 (auto) |
setFullscreen(true) | Set this to true to make the display fill the whole screen; you need to provide a key that calls app.stop() to exit the fullscreen view gracefully (default: escape). Set it to false to play the game in a normal window of its own. | False (windowed) |
setHeight(480), setWidth(640) setResolution(640,480) | Two equivalent ways of setting the display resolution. | 640x480 pixels |
setSamples(4) | Set multisampling to 0 to switch antialiasing off (harder edges, faster.) Set multisampling to 2 or 4 to activate antialising (softer edges, may be slower.) Depending on your graphic card, you may be able to go set multisampling to 8, 16, or 32 samples. | 0 |
setVSync(true) | Set vertical syncing to true to time the frame buffer to coincide with the refresh interval of the screen: Prevents page tearing, but slower; recommened for release. Set to false to deactivate vertical syncing (faster, but possible page tearing artifacts); can be deactivated during development. | false |
useInput(false) | Respond to user input by mouse and keyboard. Can be deactivated for use cases where you only display a 3D scene on the canvas without any interaction. | true |
useJoysticks(true) | Activate optional joystick support | false |
setSettingsDialogImage("/path/in/assets.png") | A custom image to display when the settings dialog is shown. | "/com/jme3/app/Monkey.png" |
setTitle("My Game") | This string will be visible in the titlebar, unless the window is fullscreen. | "jMonkey Engine 3.0" |
An AppSettings object also supports the following methods:
settings.save(outstream)
and settings.load(instream)
to save and load your settings via standard java.io serialization.newSettings.copyFrom(oldSettings)
to copy a settings object.
+
+Every class that extends jme3.app.SimpleApplication has properties that can be configured by customizing a com.jme3.system.AppSettings
object. Configure the settings before you call app.start()
on the application object. If you change display settings during runtime, call app.restart()
to make them take effect.
+
+Note: Other runtime settings are covered in SimpleApplication. +
+ +public static void main(String[] args) { + AppSettings settings = new AppSettings(true); + settings.setResolution(640,480); + ... // other properties see below + + MyGame app = new MyGame(); // or Main or whatever you called your SimpleApplication + app.setSettings(settings); + app.start(); +}+ +
+Set the boolean in the AppSettings contructor to true if you want to keep the default settings for everything that you do not specify. Set this parameter to false if you want to specify each property yourself (you'll get an exception if you missed one). +
+ ++
app.setShowSettings(false);
in the main()
method to hide the default settings window (default splash screen) at startup.
+Settings Property | Dscription | Default | +
---|---|---|
setRenderer(AppSettings.LWJGL_OPENGL2) +setRenderer(AppSettings.LWJGL_OPENGL3) | Switch Video Renderer | OpenGL 2 | +
setAudioRenderer(AppSettings.LWJGL_OPENAL) +setAudioRenderer(AppSettings.LWJGL_JOAL) | Switch Audio Renderer | OpenAL | +
setBitsPerPixel(8) | Set color depth. +1 = black and white, 2 bit = gray, +4 = 16 colors, 8 = 256 colors, 24 or 32 = "truecolor". | 24 | +
setFrequency(60) | The screen frequency (refresh rate of the graphics card), usually 60 or 75 fps. | 60 fps | +
setFramerate(60) | How often per second the engine should try to refresh the frame. For the release, usually 60 fps. Can be lower (59-30) for FX-intensive games. No use setting it to a higher value than the screen frequency. If the framerate goes below 30 fps, viewers start to notice choppiness or flickering. | -1 (auto) | +
setFullscreen(true) | Set this to true to make the game window fill the whole screen; you need to provide a key that calls app.stop() to exit the fullscreen view gracefully (default: escape). +Set this to false to play the game in a normal window of its own. | False (windowed) | +
setHeight(480), setWidth(640) +setResolution(640,480) | Two equivalent ways of setting the display resolution. | 640x480 pixels | +
setSamples(4) | Set multisampling to 0 to switch antialiasing off (harder edges, faster.) +Set multisampling to 2 or 4 to activate antialising (softer edges, may be slower.) +Depending on your graphic card, you may be able to set multisampling to higher values such as 8, 16, or 32 samples. | 0 | +
setVSync(true) | Set vertical syncing to true to time the frame buffer to coincide with the refresh interval of the screen: Prevents page tearing, but slower; recommened for release. +Set to false to deactivate vertical syncing (faster, but possible page tearing artifacts); can be deactivated during development. | false | +
useInput(false) | Respond to user input by mouse and keyboard. Can be deactivated for use cases where you only display a 3D scene on the canvas without any interaction. | true | +
useJoysticks(true) | Activate optional joystick support | false | +
setSettingsDialogImage("/path/in/assets.png") | A custom image to display when the settings dialog is shown. | "/com/jme3/app/Monkey.png" | +
setTitle("My Game") | This string will be visible in the titlebar, unless the window is fullscreen. | "jMonkey Engine 3.0" | +
+ +An AppSettings object also supports the following methods: +
+settings.save(outstream)
to save your settings via standard java.io serialization.settings.load(instream)
to load your settings.settings2.copyFrom(settings)
to copy a settings object.A collection of recommendations and expert tips. Feel free to add your own!
If you are a beginner, you should first read some articles about game development. We cannot cover all general tips here.
As a quick overview, answer yourself the following questions:
How you actually name or number these milestones is up to you. People use the words "milestone", Greek letters, version numbers, or combinations thereof.
Every milestone is made up of a development phase and a test phase. Here are some best practices:
You have a list of features that you want in game, but which one do you implement first? You will keep adding features to a project that grows more and more complex, how can you minimize the amount of rewriting required?
Acknowledge whether you want a feature because it is necessary for gameplay, or simply because "everyone else has it". Successful high-performance games are the ones where someone made smart decisions what to keep and what to drop.
Typically, developers extend a custom class off of jME3's com.jme3.app.SimpleApplication (or even com.jme3.app.Application). For a racing game you would create a different base game class than for a space game or a shooter.
my.company.MyBaseGame.java
.my.company.zombieshooter.MyGame.java
.As your jME3-based application grows more advanced, you may find yourself putting more and more tests in the simpleUpdate() loop, and passing around lots of object references. Don't implement game behaviour by copying and pasting boilerplate code! It is a best practice to move game behaviour into classes of their own. In jME3 these classes are Controls and AppStates.
Both classes automatically hook into the main update loop. Instead of remote controlling game entities via simpleUpdate(), you define the desired behaviour in the update methods of custom Controls and AppStates. You then add Controls to Spatials, and AppStates to the application, and jME3 will automatically trigger the update methods. This cleans up your simpleUpdate() loop code considerably.
Learn more about Custom Controls and Application States.
Put your assets into subfolders of your project's assets
directory. This is the default path where the assetManager looks for files.
jMonkeyProjects/Pong/assets/ # Store assets here -jMonkeyProjects/Pong/build/ # jMP generates built classes here * -jMonkeyProjects/Pong/build.xml # Customize Ant build script here -jMonkeyProjects/Pong/nbproject/ # jMP stores default build.xml and meta data * -jMonkeyProjects/Pong/dist/ # jMP generates executables here * -jMonkeyProjects/Pong/src/ # Store Java sources here -jMonkeyProjects/Pong/test/ # Store test classes here (optional) -(*) managed by jMonkeyPlatform, don't edit
assets
in any way that suits the project – but stick with one system.Textures/vehicles/car/
, Materials/vehicles/car/
, Models/vehicles/car/
Here is an example of a commonly used directory structure:
jMonkeyProjects/Pong/assets/Interface/ # .font, .jpg, .png, .xml -jMonkeyProjects/Pong/assets/MatDefs/ # .j3md -jMonkeyProjects/Pong/assets/Materials/ # .j3m -jMonkeyProjects/Pong/assets/Models/ # .j3o -jMonkeyProjects/Pong/assets/Scenes/ # .j3o -jMonkeyProjects/Pong/assets/Shaders/ # .vert, .frag -jMonkeyProjects/Pong/assets/Sounds/ # .ogg, .wav -jMonkeyProjects/Pong/assets/Textures/ # .mesh.xml+.material, .mtl+.obj, .jpg, .png
See also: Asset Packs
Here are some tips especially for users who already know jME2. Automatic handling of the Geometric State has improved in jME3, and it is now a best practice to not mess with it.
It's unlikely you will be willing to fully document every class you write. You should at minimum javadoc all crucial methods/parameters in a meaningful way.
Whether you work in a team or alone, keeping a version controlled repository of your code will help you roll-back buggy changes or recover that code that you or someone deleted and now is needed.
From the beta on, convert all Ogre mesh models and scenes to the binary .j3o format. Use the jMonkeyPlatform for the conversion, and save the .j3o files into the Models directory.
Unit Tests (Java Assertions) have a different status in 3D graphics development than in other types of software. You cannot write any assertions that automatically test whether the rendered image looks correct, or whether interactions are intuitive. Still you should create simple test cases for separate game features such as loaders, content generators, effects. Run them now and then to see whether they still work as intended – or whether they are affected by side effects. Keep the test classes in a test directory in the project, but don't include them in the distribution.
Quality Assurance (QA) means maintaining a clear list of steps that must always work, and checking them. There can be bugs in software, but tasks such as installing and de-installing, saving and loading, starting/pausing/quitting the game, must work, no excuse. After every milestone, you go through the list again, on every supported operating system, and systematically look for regressions or bugs.
Alpha and Beta Testing means that you ask someone to try to install and run your game. It should be a real user situation, where they are left to figure it out by themselves (you only can include the usual read-me and help docs). Provide the testers with an easy method to report back descriptions of their problems, e.g. why they gave up. Evaluate whether these problems are exceptions or must be fixed for the game to be playable.
A Java Debugger is included in the jMonkeyPlatform. It allows you to set a break point in your code near the point where an exception happens. Then you step through the execution line by line and watch object and variable states to detect where the bug starts.
Use the Logger to print status messages during the development and debugging phase, instead of System.out.println().
A Java Profiler can be added to the jMonkeyPlatform via Tools → Plugins → Available. The profiler presents statistics on the lifecycle of methods and objects. Performance problems may be caused by just a few methods that take long, or are called too often. If object creation and garbage collection counts keep increasing, you are looking at a memory leak.
Pre-Release To-Do List
Distributable Executable
The SDK can help you with deployment. Do you release your game as WebStart, Desktop JAR, or Applet? Each has its pros and cons.
Distribution | Pros | Cons |
---|---|---|
Desktop Launcher (.EXE, .app, .jar+.sh) | This is the standard way of distributing desktop applications. The jMonkeyPlatform can be configured to automatically create zipped launchers for each operating system. | You need to offer three separate, platform-dependent downloads. |
Desktop Application (.JAR) | Platform independent desktop application. | User must have Java configured to run JARs when they are opened; or user must know how to run JARs from command line; or you must provide a custom JAR wrapper. |
Web Start (.JNLP) | The user accesses a URL, saves the game as one executable file. Easy process, no installer required. You can allow the game to be played offline. | Users need network connection to install the game. Downloading bigger games takes a while as opposed to running them from a CD. |
Browser Applet (.HTML+.JAR) | Easy to access and play game via most web browsers. Userfriendly solution for quick small games. | Game only runs in the browser. Game or settings cannot be saved to disk. Some restrictions in default camera navigation (jME cannot capture mouse.) |
Which ever method you choose, a Java-Application works on the three main operating systems: Windows, Mac OS, Linux.
+ +A collection of recommendations and expert tips. Feel free to add your own! +If you are a beginner, you should first game development. We cannot cover all general tips here. +
+ ++ +As a quick overview, answer yourself the following questions: +
++How you actually name or number these milestones is up to you. People use the words "milestone", Greek letters, version numbers, or combinations thereof. +Every milestone is made up of a development phase and a test phase. Here are some best practices: + +
+ ++You have a list of features that you want in game, but which one do you implement first? You will keep adding features to a project that grows more and more complex, how can you minimize the amount of rewriting required? +
+
+Acknowledge whether you want a feature because it is necessary for gameplay, or simply because "everyone else has it". Successful high-performance games are the ones where someone made smart decisions what to keep and what to drop.
+
+Consider this: Everybody wants "full physics, AI, post-rendering effects, and multi-player networking"… Make certain you truly understand what that requires (e.g. client-server synchonization)! Your goal should be to bring out the essence of your game idea, don't water down gameplay but attempting to make it "do everything, but better".
+
+
+Typically, developers extend a custom base class off of jME3's com.jme3.app.SimpleApplication. For all your games you will want a certain basic frame – for example methods for loading and saving scenes, physics, networking, and multi-player logon screen, switching to settings screen, etc. Then you reuse (extend) your own generic game class and create a specific game, for example a racing game, or a space game, or a shooter.
+
+Follow these steps:
+
my.company.MyBaseGame.java
.my.company.zombieshooter.MyGame.java
.
+
+Game elements often carry custom data with them. For example, players have health, gold coins, an inventory, equipment, etc. jME3 lets you store custom Java objects in Spatials. This way, your custom data is accessible where ever the Spatial is accessible. Read the Spatial documentation to learn more about how to use the setUserData()
method on Nodes and Geometries.
+
+
+As your SimpleApplication-based game grows more advanced, you may find yourself putting more and more tests in the simpleUpdate()
loop, passing around lots of object references, and your simpleInitApp() methods grows longer and longer….
+
+Move game behaviour into reusable classes of their own. In jME3 these resuable classes are Controls and AppStates. +
+simpleUpdate()
loop.simpleInitApp()
method. + +Controls and AppStates can work together: +
++ +Both Control and AppState automatically hook into the main update loop. +
++ +Read more about Custom Controls and Application States here. +
+ +
+
+Put your assets into subfolders of your project's assets
directory. This is the default path where the AssetManager looks for files.
+
jMonkeyProjects/MyGame/assets/ # Store assets in subfolders here! +jMonkeyProjects/MyGame/build/ # jMP generates built classes here * +jMonkeyProjects/MyGame/build.xml # Customize Ant build script here +jMonkeyProjects/MyGame/nbproject/ # jMP stores default build.xml and meta data * +jMonkeyProjects/MyGame/dist/ # jMP generates executables here * +jMonkeyProjects/MyGame/src/ # Store Java sources here +jMonkeyProjects/MyGame/test/ # Store test classes here (optional) +(*) managed by jMonkeyPlatform, don't edit+
assets
in any way that suits the project – but stick with one system.Textures/vehicles/car/
, Materials/vehicles/car/
, Models/vehicles/car/
+ +Here is an example of a commonly used directory structure for various file types: + +
+jMonkeyProjects/MyGame/assets/Interface/ # .font, .jpg, .png, .xml +jMonkeyProjects/MyGame/assets/MatDefs/ # .j3md +jMonkeyProjects/MyGame/assets/Materials/ # .j3m +jMonkeyProjects/MyGame/assets/Models/ # .j3o +jMonkeyProjects/MyGame/assets/Scenes/ # .j3o +jMonkeyProjects/MyGame/assets/Shaders/ # .vert, .frag +jMonkeyProjects/MyGame/assets/Sounds/ # .ogg, .wav +jMonkeyProjects/MyGame/assets/Textures/ # .mesh.xml+.material, .mtl+.obj, .jpg, .png+ +
+ +See also: Asset Packs and Asset Manager. +
+ ++ +Here are some tips especially for users who already know jME2. Automatic handling of the Geometric State has improved in jME3, and it is now a best practice to not mess with it. +
++ +It's unlikely you will be willing to fully document every class you write. You should at minimum javadoc the most crucial methods/parameters in a meaningful way. +
++ +Treat javadoc as messages to your future self. "genNextVal() generates the next value" and "@param float factor A factor influencing the result" do not count as documentation. +
+ ++ +Whether you work in a team or alone, keeping a version controlled repository of your code will help you roll-back buggy changes, or recover old code that someone deleted and that is now needed again. +
++ +From the beta on, convert all models and scenes (Ogre mesh and Wavefront and Blender) to jME3's binary .j3o format. Use the jMonkeyPlatform for the conversion, and save the .j3o files into the Models directory. +
++ +See also: Model Loader and Viewer +
+ ++ +Unit Tests () have a different status in 3D graphics development than in other types of software. You cannot write any assertions that automatically test whether the rendered image looks correct, or whether interactions are intuitive. Still you should create simple test cases for individual game features such as loaders, content generators, effects. Run the test cases now and then to see whether they still work as intended – or whether they are affected by side effects. Keep the test classes in a test directory in the project, but don't include them in the distribution. +
+ ++Quality Assurance (QA) means maintaining a clear list of steps that must always work, and checking them. It will always happen that there are hard-to-find bugs in the gameplay somewhere – but basic tasks such as installing and de-installing, saving and loading, starting/pausing/quitting the game, must work, no excuse. After every milestone, you go through the QA list again, on every supported operating system, and systematically look for regressions or newly introduced bugs. +
+ ++Alpha and Beta Testing means that you ask someone to try to install and run your game. It should be a real user situation, where they are left to figure it out by themselves (you only can include the usual read-me and help docs). Provide the testers with an easy method to report back descriptions of their problems, or why they gave up. Evaluate whether these problems are exceptions or must be fixed for the game to be playable. +
+ ++ +A Java Debugger is included in the jMonkeyPlatform. It allows you to set a break point in your code near the point where an exception happens. Then you step through the execution line by line and watch object and variable states to detect where the bug starts. +
+ ++Use the Logger to print status messages during the development and debugging phase, instead of System.out.println(). The logger can be switched off with one line of code, whereas commenting out your println()s takes a while. +
+ ++ +You can add a Java Profiler to the jMonkeyPlatform via Tools → Plugins → Available. The profiler presents statistics on the lifecycle of methods and objects. Performance problems may be caused by just a few methods that take long, or are called too often. If object creation and garbage collection counts keep increasing, you are looking at a memory leak. +
+ ++ +Pre-Release To-Do List +
++ +Distributable Executable +
+ ++The jMonkeyPlatform SDK helps you with deployment (unless you used another IDE, then consult the IDE's documentation). Do you want to release your game as WebStart, Desktop JAR, or Applet? Each has its pros and cons. + +
+Distribution | Pros | Cons | +
---|---|---|
Desktop Launcher +(.EXE, .app, .jar+.sh) | This is the standard way of distributing desktop applications. The jMonkeyPlatform can be configured to automatically create zipped launchers for each operating system. | You need to offer three separate, platform-dependent downloads. | +
Desktop Application +(.JAR) | Platform independent desktop application. | User must have Java configured to run JARs when they are opened; or user must know how to run JARs from command line; or you must provide a custom JAR wrapper. | +
Web Start +(.JNLP) | The user accesses a URL, saves the game as one executable file. Easy process, no installer required. You can allow the game to be played offline. | Users need network connection to install the game. Downloading bigger games takes a while as opposed to running them from a CD. | +
Browser Applet +(.HTML+.JAR) | Easy to access and play game via most web browsers. Userfriendly solution for quick small games. | Game only runs in the browser. Game or settings cannot be saved to disk. Some restrictions in default camera navigation (jME cannot capture mouse.) | +
Android +(.APK) | Game runs on Android devices | Android devices do not support post-procesor effects. | +
+ +Which ever method you choose, a Java-Application works on the main operating systems: Windows, Mac OS, Linux, Android. + +
+ +Suffix | Usage | Learn more |
---|---|---|
.j3o | Binary 3D model or scene. From the Beta release of your game on, you should convert all models to .j3o format. During alpha and earlier development phases (when models still change a lot) you can alternatively load OgreXML/OBJ models directly. | Model Loader and Viewer |
.j3m | A custom Material. You create these Material objects to store Material configurations for your 3D models. | Materials Overview Material Editing |
.j3md | A Material definition. These are templates for advanced shader-based Materials. Each custom .j3m Material is based on a .j3md Material Definition. | Materials Overview |
File Suffix | Description |
---|---|
.mesh.xml, .meshxml, .scene | Ogre Mesh XML for 3D models, Ogre DotScene for 3D scenes |
.OBJ, .MTL | Wavefront, 3D model format |
.JPG, .PNG, .GIF | 2D Images, textures |
.DDS, .HDR, .PFM, .TGA | Textures |
.fnt | Bitmap fonts |
.WAV, .OGG | Wave and OGG Vorbis audio |
.OGV | Ogg Theora video |
Suffix | Usage | Learn more | +
---|---|---|
.j3o | Binary 3D model or scene. From the Beta release of your game on, you should convert all models to .j3o format. During alpha and earlier development phases (when models still change a lot) you can alternatively load OgreXML/OBJ models directly. | Model Loader and Viewer | +
.j3m | A custom Material. You create these Material objects to store Material configurations for your 3D models. | Materials Overview Material Editing | +
.j3md | A Material definition. These are templates for advanced shader-based Materials. Each custom .j3m Material is based on a .j3md Material Definition. | Materials Overview | +
.j3f | A file containing a FilterPostProcessor with preconfigured filters. | Filters | +
File Suffix | Description | +
---|---|
.mesh.xml, .meshxml, .scene | Ogre Mesh XML for 3D models, Ogre DotScene for 3D scenes | +
.OBJ, .MTL | Wavefront, 3D model format | +
.blend | Blender 3D model file | +
.JPG, .PNG, .GIF | 2D Images, textures | +
.DDS, .HDR, .PFM, .TGA | Textures | +
.fnt | Bitmap fonts | +
.WAV, .OGG | Wave and OGG Vorbis audio | +
+
+You've followed a link to a topic that doesn't exist yet. If permissions allow, you may create it by using the Create this page
button.
+
Proudly powered by and .
++ + + |
+Just some maths notes to myself… + +
+ ++Note: Typically you have to string these formulas together. Look in the table for what you want, and what you have. If the two are not the same line, than you need conversion steps inbetween. E.g. if you have an angle in degrees, but the formula expects radians: 1) convert degrees to radians, 2) use the radians formula on the result. +
+I have… | I want… | Formula | +
---|---|---|
normalized direction and length +n1,d1 | a vector that goes that far in that direction +new direction vector v0 | v0 = n1.mult(d1) | +
point and direction vector +p1,v1 | to move the point +new point p0 | p0 = p1.add(v1) | +
two direction vectors or normals +v1,v2 | to combine both directions +new direction vector v0 | v0 = v1.add(v2) | +
two points +p1, p2 | distance between two points +new scalar d0 | d0 = p1.subtract(p2).length() +d0 = p1.distance(p2) |
+
two points +p1, p2 | the direction from p2 to p1 +new direction vector v0 | v0 = p1.substract(p2) | +
two points, a fraction +p1, p2, h=0.5f | the point "halfway" (h=0.5f) between the two points +new interpolated point p0 | p0 = FastMath.interpolateLinear(h,p1,p2) | +
+
+
+
I have… | I want… | Formula | +
---|---|---|
angle in degrees +a | to convert angle a from degrees to radians +new float angle phi | phi = a / 180 * FastMath.PI; +OR +phi=a.mult(FastMath.DEG_TO_RAD); |
+
angle in radians +phi | to convert angle phi from radians to degrees +new float angle a | a = phi * 180 / FastMath.PI | +
radian angle and x axis +phi, x | to rotate around x axis +new quaternion q0 | q0.fromAngleAxis( phi, Vector3f.UNIT_X ) | +
radian angle and y axis +phi, y | to rotate around y axis +new quaternion q0 | q0.fromAngleAxis( phi, Vector3f.UNIT_Y ) | +
radian angle and z axis +phi, z | to rotate around z axis +new quaternion q0 | q0.fromAngleAxis( phi, Vector3f.UNIT_Z ) | +
several quaternions +q1, q2, q3 | to combine rotations, in that order +new quaternion q0 | q0 = q1.mult(q2).mult(q3) | +
point and quaternion +p1, q1 | to rotate the point around origin +new point p0 | p0 = q1.mult(p1) | +
angle in radians and radius +phi,r | to arrange or move objects horizontally in a circle (with y=0) +x and z coordinates | float x = FastMath.cos(phi)*r; +float z = FastMath.sin(phi)*r; |
+
+v2 = v.mult(); v2 = v.add(); v2 = v.subtract(); etc
+
+v.multLocal(); v.addLocal(); v.subtractLocal(); etc
+
This page is intended as a reference collection of optimization tricks that can be used to speed up JME3 applications.
The more Geometry objects are added to the scene, the harder it gets to handle them in a speedy fashion. -The reson for this is, that for every object a render command must be done, here is a bottleneck betwenn the CPU and the Graficcard. Possible optimization techniques
Side-effects
When you use math operations like vectorA.mult(vectorB); new objects are created that have to be garbage collected when you don't use them anymore. Check your math operations for opportunities to use the local version of the math operations, e.g. vectorA.multLocal(vectorB). This way the result is stored in vectorA and no new object needs to be created.
SimpleApplication displays a HUD with statistics. Use app.setDisplayStatView(true);
to activate it, and false to deactivate it.
-It counts how many FrameBuffers, Textures, or Shaders…
Example:
Genereally jME3 is well optimized and optimizes these things correctly. The normal state is that the (S/F/M) values should be in the same order of magnitude; (S) values can be lower than (F).
+This page is intended as a reference collection of optimization tricks that can be used to speed up JME3 applications. + +
+ ++The more Geometry objects are added to the scene, the harder it gets to handle them in a speedy fashion. +The reson for this is, that for every object a render command must be done, here is a bottleneck betwenn the CPU and the Graficcard. +Possible optimization techniques +
++Side-effects +
++When you use math operations like vectorA.mult(vectorB); new objects are created that have to be garbage collected when you don't use them anymore. Check your math operations for opportunities to use the local version of the math operations, e.g. vectorA.multLocal(vectorB). This way the result is stored in vectorA and no new object needs to be created. + +
+ ++To offload much computation to the less CPU intense physics broadphase collision check, avoid having large meshes that cover e.g. the whole map of your level. Instead separate the collision shapes into multiple smaller parts. Obviously this can also be overdone as excessive amounts of physics objects can bring other performance problems. + +
+ +
+SimpleApplication displays a HUD with statistics. Use app.setDisplayStatView(true);
to activate it, and false to deactivate it.
+It counts how many FrameBuffers, Textures, or Shaders…
+
+Example: +
++Genereally jME3 is well optimized and optimizes these things correctly. The normal state is that the (S/F/M) values should be in the same order of magnitude; (S) values can be lower than (F). +
+ The base classes of the jMonkeyEngine3 are com.jme3.app.Application
and its subclass com.jme3.app.SimpleApplication
. SimpleApplication extends Application, and offers everything that Application offers, plus pre-configured features such as a scene graph and a flyby cam.
-Your game class typically extends com.jme3.app.SimpleApplication. You call app.start() and app.stop() on your game instance to start or quit the application. The following code sample shows the typical base structure of a jME3 game:
import com.jme3.app.SimpleApplication; -public class HelloWorld extends SimpleApplication { + ++ +SimpleApplication and Application
++ ++ +The base class of the jMonkeyEngine3 is
+ +com.jme3.app.SimpleApplication
. Your first game's Main class extends SimpleApplication directly. When you feel confident you understand the features, you will typically extend SimpleApplication to create a custom base class for the type of games that you want to develop. ++SimpleApplication offers standard game features such as a scene graph, input handling, and a fly-by camera. You call app.start() and app.stop() on your game instance to start or quit the application. +
+ ++The following code sample shows the typical base structure of a jME3 game: +
+import com.jme3.app.SimpleApplication; + +public class MyBaseGame extends SimpleApplication { + public static void main(String[] args){ - HelloWorld app = new HelloWorld(); + MyBaseGame app = new MyBaseGame(); app.start(); } + @Override public void simpleInitApp() { /* Initialize the game scene here */ } + @Override public void simpleUpdate(float tpf) { - /* (optional) Interact with game events in the main loop */ + /* Interact with game events in the main loop */ } + @Override public void simpleRender(RenderManager rm) { /* (optional) Make advanced modifications to frameBuffer and scene graph. */ } -}Let's have a look at what the base classes have to offer.
Application Class
The com.jme3.app.Application class represents an instance of a real-time 3D rendering jME3 application. A com.jme3.app.Application provides all the tools that are commonly used in jME3 applications.
Class field or method Purpose context The application context contains renderer, AppSettings, timer, etc. The context object is not usually directly accessed by users. getRenderManager()
getRenderer();Low-level and high-level rendering interface. getStateManager() The Application state manager is used to activate physics. viewPort The view object for the default camera. guiViewPort The view object for the orthogonal GUI view. Used internally for HUDs. settings
setSettings()The AppSettings let you specify the display width and height (by default 640x480), color bit depth, z-buffer bits, anti-aliasing samples, and update frequency, video and audio renderer, asset manager. timer Internal update loop timer. Users have access to the tpf float variable in the simpleUpdate() loop to time actions. cam The default camera with perspective projection, 45° field of view, near plane = 1 wu, far plane = 1000 wu. assetManager
getAssetManager()An object that manages paths for loading models, textures, materials, sounds, etc. By default the AssetManager paths are relative to your projects root directory. getAudioRenderer()
getListener()The audio system. keyInput
mouseInput
joyInputDefault input contexts for keyboard, mouse, and joystick getInputManager() Use the inputManager to configure your custom inputs. inputEnabled Set this boolean whether the system should listen for user inputs (or instead just play a non-interactive scene). setPauseOnLostFocus() Set this boolean whether the game should pause when ever the window loses focus. paused Set this boolean during runtime to pause/unpause a game. start()
start(jMEContextType)Call this method to start a jME3 game. By default this opens a new jME3 window, initializes the scene, and starts the event loop. Can optionally accept com.jme3.system.JmeContext.Type.* as argument to run headless or embedded. (See below.) restart() Reloads the AppSettings into the current application context. stop() Stops the running jME3 game and closes the jME3 window. The jME Context Type (com.jme3.system.JmeContext.Type.*) is one of the following:
Display – jME application runs in a window of its own. This is the Default. Canvas – jME application is embedded in a Swing Canvas) Headless – jME application runs its event loop without calculating any view and without opening any window. Can be used for a Headless Server application. OffscreenSurface – jME application view is not shown, but calculated and cached as bitmap (back buffer).You can switch Context Types when starting the application, for example:
app.start(JmeContext.Type.Headless);
See also: AppSettingsSimpleApplication Class
The com.jme3.app.SimpleApplication class extends the com.jme3.app.Application class to provide default functionality like a first-person (fly-by) camera, and a scenegraph with a rootNode that is updated and rendered regularly. -By default, a SimpleApplication displays statistics (such as frames per second) on-screen, using the com.jme3.app.StatsView class. You deactivate the statistics HUD by calling
app.setDisplayFps(false); -app.setDisplayStatView(false);
. -Some input keys are pre-configured by default: You quit the application by pressing the Escape key, and you can control the camera using the mouse, the arrow keys, and WASD keys. (More details below.) -Additional to what Application offers, SimpleApplication offers:
SimpleApplication Feature Purpose getRootNode()
rootNodeThe root node of the scene graph. A Spatial that you attach to the rootNode appears in the 3D scene. getGuiNode()
guiNodeAttach flat GUI elements (such as HUD images and text) to this orthogonal GUI node. loadStatsView() Call this method to print live statistic information to the screen, such as current frames-per-second and triangles/vertices counts. Use this info during the development phase. setDisplayFps(false);
setDisplayStatView(false);Call these method to remove the statistic information from the screen, for example, before releases. getFlyByCamera()
flyCamThe default fly-by camera object, controlled by the WASD and arrow keys. setShowSettings(true) Whether the user should be presented with a splashscreen and display settings dialog when starting the game. Set this boolean before calling start() on the SimpleApplication. simpleInitApp() Override this method to initialize the game scene. Here you load and create objects, attach Spatials to the rootNode, and bring everything in its starts position. simpleUpdate(float tpf) Override this method to have access to the main event loop. Use this loop to poll the current game state and respond to changes, or to initiate state changes and to generate encounters. For advanced info how to hook into the update loop, see also Application States and Custom Controls. simpleRender() Optionally, override this method to do advanced modifications to the frameBuffer and scene graph. Input Defaults
- \ No newline at end of file +}The following default actions are available for a running game:
Key Action KEY_ESCAPE Quits the game by calling app.stop()
KEY_C Prints camera position, rotation, and direction KEY_M Prints memory usage stats If useInput() is true, the default Flyby Cam is active. Then the following so-called "WASD" inputs are additionally available:
Camera Motion Key or Mouse Input Move Forward KEY_W Move Left (Strafe) KEY_A Move Backward KEY_S Move Right (Strafe) KEY_D Move Vertical Upward KEY_Q Move Vertical Downward KEY_Z Rotate Left KEY_LEFT, or move mouse horizontally left (-x) Rotate Right KEY_RIGHT, or move mouse horizontally right (+x) Rotate Up KEY_UP, or move mouse vertically forward (+y) Rotate Down KEY_DOWN, or move mouse vertically backward (-y) Zoom In Scroll mouse wheel backward Zoom Out Scroll mouse wheel forward Rotate drag Hold left mouse button and move display, basegame, documentation, intro, intermediate, init, input, game, loop, rootnode
+Let's have a look at the API of the base class. +
+ ++ +Internally, com.jme3.app.SimpleApplication extends com.jme3.app.Application. The Application class represents a generic real-time 3D rendering jME3 application (i.e., not necessarily a game). Typically, you do not extend com.jme3.app.Application directly to create a game. + +
+Application class fields | Purpose | +
---|---|
viewPort +getViewPort() | The view object for the default camera. You can register advanced post-processor filters here. | +
settings +setSettings() | Use this AppSettings object to specify the display width and height (by default 640x480), color bit depth, z-buffer bits, anti-aliasing samples, and update frequency, video and audio renderer, asset manager. See: AppSettings. | +
cam +getCamera() | The default camera provides perspective projection, 45° field of view, near plane = 1 wu, far plane = 1000 wu. | +
assetManager +getAssetManager() | An object that manages paths for loading models, textures, materials, sounds, etc. By default the Asset Manager paths are relative to your project's root directory. | +
audioRenderer +getAudioRenderer() | This object gives you access to the jME3 audio system. | +
listener +getListener() | This object represents the user's ear for the jME3 audio system. | +
inputManager +getInputManager() | Use the inputManager to configure your custom inputs (mouse movement, clicks, key presses, etc) and set mouse pointer visibility. | +
stateManager +getStateManager() | You use the Application's state manager to activate AppStates, such as Physics. | +
Application methods | Purpose | +
---|---|
setPauseOnLostFocus(true) | Set this boolean whether the game should pause when ever the window loses focus. | +
start() | Call this method to start a jME3 game. By default this opens a new jME3 window, initializes the scene, and starts the event loop. | +
restart() | Loads modified AppSettings into the current application context. | +
stop() | Stops the running jME3 game and closes the jME3 window. | +
start(Type.Headless) etc | Switch Context com.jme3.system.JmeContext.Type when starting the application: +Type.Display – jME application runs in a window of its own. (This is the default.) +Type.Canvas – jME application is embedded in a Swing Canvas. +Type.Headless – jME application runs its event loop without calculating any view and without opening any window. Can be used for a Headless Server application. +Type.OffscreenSurface – jME application view is not shown and no window opens, but everything calculated and cached as bitmap (back buffer) for use by other applications. |
+
Internal class field/method | Purpose | +
---|---|
context +getContext() | The application context contains the renderer, AppSettings, timer, etc. Typically, you do not directly access the context object. | +
inputEnabled | this internal boolean is true if you want the system to listen for user inputs, and false if you just want to play a non-interactive scene. You change the boolean using AppSettings. | +
keyInput, mouseInput +joyInput, touchInput | Default input contexts for keyboard, mouse, and joystick. Internally used to enable handling of joysticks or touch devices. The base classes contain key and mouse button enums. | +
renderManager +getRenderManager() +renderer +getRenderer(); | Low-level and high-level rendering interface. Mostly used internally. | +
guiViewPort +getGuiViewPort() | The view object for the orthogonal GUI view. Only used internally for HUDs. | +
timer | An internally used update loop timer. You already have access to the float tpf in the simpleUpdate() loop to time actions according to the time per frame. |
+
paused | Boolean is used only internally during runtime to pause/unpause a game. (You need to implement your own isRunning boolean or so.) | +
+ +See also: AppSettings +
+ ++ +The com.jme3.app.SimpleApplication class extends the generic com.jme3.app.Application class. SimpleApplication makes it easy to start writing a game because it adds typical functionality: +
++ +
setDisplayFps(false); setDisplayStatView(false);
inside the simpleInitApp()
method. The displays contain valuable information during the development and debugging phase. At the latest, you remove these displays for the release.
++Additional to the functionality that Application brings, SimpleApplication offers: + +
+SimpleApplication Class Field | Purpose | +
---|---|
rootNode +getRootNode() | The root node of the scene graph. Attach a Spatial to the rootNode and it appears in the 3D scene. | +
guiNode +getGuiNode() | Attach flat GUI elements (such as HUD images and text) to this orthogonal GUI node to make them appear on the screen. | +
flyCam +getFlyByCamera() | The default first-person fly-by camera control. This default camera control lets you navigate the 3D scene using the preconfigured WASD and arrow keys and the mouse. | +
SimpleApplication Method | Purpose | +
---|---|
loadStatsView() | Call this method to print live statistic information to the screen, such as current frames-per-second and triangles/vertices counts. You use this info typically only during development or debugging. | +
loadFPSText() | Call this method to print the current framerate (frames per second) to the screen. | +
setShowSettings(true) | Whether the user should be presented with a splashscreen and display settings dialog when starting the game. Set this boolean before calling start() on the SimpleApplication. | +
SimpleApplication Interface | Purpose | +
---|---|
simpleInitApp() | Override this method to initialize the game scene. Here you load and create objects, attach Spatials to the rootNode, and bring everything in its starts position. See also Application States for best practies. | +
simpleUpdate(float tpf) | Override this method to have access to the update loop. Use this loop to poll the current game state and respond to changes, or to let the game mechanics generate encounters and initiate state changes. Use tpd (time per frame) as a factor to time events. For more info on how to hook into the update loop, see Application States and Custom Controls. | +
simpleRender() | Optional: Override this method to implement advanced modifications of the frameBuffer and scene graph. | +
+
+The following default navigational input actions are mapped by the default flyCam
control in a SimpleApplication: You can use these mappings for debugging and testing until you implement custom input handling.
+
+
Key | Action | +
---|---|
KEY_ESCAPE | Quits the game by calling app.stop() |
+
KEY_C | Prints camera position, rotation, and direction to the out stream. | +
KEY_M | Prints memory usage stats the out stream. | +
+ +As long as useInput() is true, the default Flyby Cam is active. Then the following so-called "WASD" inputs are additionally available: + +
+Camera Motion | Key or Mouse Input | +
---|---|
Move Forward | KEY_W | +
Move Left (Strafe) | KEY_A | +
Move Backward | KEY_S | +
Move Right (Strafe) | KEY_D | +
Move Vertical Upward | KEY_Q | +
Move Vertical Downward | KEY_Z | +
Rotate Left | KEY_LEFT, or move mouse horizontally left (-x) | +
Rotate Right | KEY_RIGHT, or move mouse horizontally right (+x) | +
Rotate Up | KEY_UP, or move mouse vertically forward (+y) | +
Rotate Down | KEY_DOWN, or move mouse vertically backward (-y) | +
Zoom In | Scroll mouse wheel backward | +
Zoom Out | Scroll mouse wheel forward | +
Rotate drag | Hold left mouse button and move | +
It's a fact of life, math is hard. Unfortunately, 3D graphics require a fair bit of knowledge about the subject. Fortunately, jME is able to hide the majority of the details away from the user. Vectors are the fundamental type in the 3D environment, and it is used extensively. Matrices are also a basic necessity of 3D for representing linear systems. Quaternions are perhaps the most complicated of the basic types and are used for rotation in jME.
I'll discuss how these are used in the system for the core functionality. Including Transforming, Visibility Determination, Collision Detection, and the Coordinate System. Note, that these are low level details. Further chapters will discuss how to use these various systems from a high level perspective.
To get a visual introduction to math in jME3 for the absolute beginner, check out our Math for Dummies introduction class.
A coordinate system consists of an origin (single point in space) and three coordinate axes that are each unit length and mutually perpendicular. The axes can be written as the column of a Matrix, R = [U1|U2|U3]. In fact, this is exactly how CameraNode works. The coordinate system defined by Camera is stored in a Matrix.
jME uses a Right-Handed coordinate system (as OpenGL does).
The definition of a coordinate system is defined in jME by the properties sent to Camera. There are no error checks to insure that: 1) the coordinate system is right-handed and 2) The axes are mutually perpendicular. Therefore, if the user sets the axes incorrectly, they are going to experience very odd rendering artifacts (random culling, etc).
Transformations define an operation that converts points from one coordinate system to another. This includes translation, rotation and scaling. In jME, local transforms are used to represent the positioning of objects relative to a parent coordinate system. While, world transforms are used to represent the positioning of objects in a global coordinate system.
Visibility Determination concerns itself with minimizing the amount of data that is sent to the graphics card for rendering. Specifically, we do not want to send data that will not be seen. Data not sent to the graphics card is said to be culled. The primary focus of this section is Frustum Culling based on the Camera's view frustum. In essence, this frustum creates six standard view planes. The BoundingVolume of an object is tested against the frustum planes to determine if it is contained in the frustum. If at any point the object's bounding is outside of the plane, it is tossed out and no longer processed for rendering. This also includes any children that it managed, allowing fast culling of large sections of the scene.
ColorRGBA defines a color value in the jME library. The color value is made of three components, red, green and blue. A fourth component defines the alpha value (transparent) of the color. Every value is set between [0, 1]. Anything less than 0 will be clamped to 0 and anything greater than 1 will be clamped to 1.
Note: If you would like to "convert" an ordinary RGB value (0-255) to the format used here (0-1), simply multiply it with: 1/255.
ColorRGBA defines a few static color values for ease of use. That is, rather than:
ColorRGBA red = new ColorRGBA(1,0,0,1); -object.setSomeColor(red);
you can simply say:
object.setSomeColor(ColorRGBA.red)
ColorRGBA will also handle interpolation between two colors. Given a second color and a value between 0 and 1, a the owning ColorRGBA object will have its color values altered to this new interpolated color.
A Matrix is typically used as a linear transformation to map vectors to vectors. That is: Y = MX where X is a Vector and M is a Matrix applying any or all transformations (scale, rotate, translate).
There are a few special matrices:
zero matrix is the Matrix with all zero entries.
0 | 0 | 0 |
0 | 0 | 0 |
0 | 0 | 0 |
The Identity Matrix is the matrix with 1 on the diagonal entries and 0 for all other entries.
1 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 1 |
A Matrix is invertible if there is a matrix M-1 where MM-1 = M-1 = I.
The transpose of a matrix M = [mij] is MT = [mji]. This causes the rows of M to become the columns of MT.
1 | 1 | 1 | 1 | 2 | 3 | |
2 | 2 | 2 | ⇒ | 1 | 2 | 3 |
3 | 3 | 3 | 1 | 2 | 3 |
A Matrix is symmetric if M = MT.
X | A | B |
A | X | C |
B | C | X |
Where X, A, B, and C equal numbers
jME includes two types of Matrix classes: Matrix3f and Matrix4f. Matrix3f is a 3x3 matrix and is the most commonly used (able to handle scaling and rotating), while Matrix4f is a 4x4 matrix that can also handle translation.
Multiplying a vector with a Matrix allows the vector to be transformed. Either rotating, scaling or translating that vector.
If a diagonal Matrix, defined by D = [dij] and dij = 0 for i != j, has all positive entries it is a scaling matrix. If di is greater than 1 then the resulting vector will grow, while if di is less than 1 it will shrink.
A rotation matrix requires that the transpose and inverse are the same matrix (R-1 = RT). The rotation matrix R can then be calculated as: R = I + (sin(angle)) S + (1 - cos(angle)S2 where S is:
0 | u2 | -u1 |
-u2 | 0 | u0 |
u1 | -u0 | 0 |
Translation requires a 4x4 matrix, where the vector (x,y,z) is mapped to (x,y,z,1) for multiplication. The Translation Matrix is then defined as:
M | T |
ST | 1 |
where M is the 3x3 matrix (containing any rotation/scale information), T is the translation vector and ST is the transpose Vector of T. 1 is just a constant.
Both Matrix3f and Matrix4f store their values as floats and are publicly available as (m00, m01, m02, …, mNN) where N is either 2 or 3.
Most methods are straight forward, and I will leave documentation to the Javadoc.
Vectors are used to represent a multitude of things in jME, points in space, vertices in a triangle mesh, normals, etc. These classes (Vector3f in particular) are probably the most used class in jME.
A Vector is defined by an n-tuple of real numbers. V = <V1, V2,…, Vn>.
We have two Vectors (2f and 3f) meaning we have tuples of 2 float values or 3 float values.
A Vector can be multiplied by a scalar value to produce a second Vector with the same proportions as the first. aV = Va = <aV1, aV2,…,aVn>
Adding or subtracting two Vectors occurs component-wise. That is the first component is added (subtracted) with the first component of the second Vector and so on.
P + Q = <P1+Q1, P2+Q2, …, Pn+Qn>
The magnitude defines the length of a Vector. A Vector of magnitude 1 is unit length.
For example, if V = (x, y, z), the magnitude is the square root of (x2 + y2 + z2).
A Vector can be normalized or made unit length by multiplying the Vector by (1/magnitude).
The dot product of two vectors is defined as: P dot Q = PxQx + PyQy + PzQz
Using the dot product allows us to determine how closely two Vectors are pointing to the same point. If the dot product is negative they are facing in relatively opposite directions, while postive tells us they are pointing in the relative same direction.
If the dot product is 0 then the two Vectors are orthogonal or 90 degrees off.
The Cross Product of two Vectors returns a third Vector that is prependicular to the two Vectors. This is very useful for calculating surface normals.
P X Q = <PyQz - PzQy, PzQx - PxQz, PxQy - PyQx>
Vector3f and Vector2f store their values (x, y, z) and (x, y) respectively as floats. Most methods are straight forward, and I will leave documentation to the Javadoc.
See Javadoc
Quaternions define a subset of a hypercomplex number system. Quaternions are defined by (i2 = j2 = k2 = ijk = -1). jME makes use of Quaternions because they allow for compact representations of rotations, or correspondingly, orientations, in 3D space. With only four float values, we can represent an object's orientation, where a rotation matrix would require nine. They also require fewer arithmetic operations for concatenation.
Additional benefits of the Quaternion is reducing the chance of Gimbal Lock and allowing for easily interpolation between two rotations (spherical linear interpolation or slerp).
While Quaternions are quite difficult to fully understand, there are an exceeding number of convenience methods to allow you to use them without having to understand the math behind it. Basically, these methods involve nothing more than setting the Quaternion's x,y,z,w values using other means of representing rotations. The Quaternion is then contained in Spatial as its local rotation component.
Quaternion q has the form
q = <w,x,y,z> = w + xi + yj + zk
or alternatively, it can be written as:
q = s + v, where s represents the scalar part corresponding to the w-component of q, and v represents the vector part of the (x, y, z) components of q.
Multiplication of Quaternions uses the distributive law and adheres to the following rules with multiplying the imaginary components (i, j, k):
i2 = j2 = k2 = -1
ij = -ji = k
jk = -kj = i
ki = -ik = j
However, Quaternion multiplication is not commutative, so we have to pay attention to order.
q1q2 = s1s2 - v1 dot v2 + s1v2 + s2v1 + v1 X v2
Quaternions also have conjugates where the conjugate of q is (s - v)
These basic operations allow us to convert various rotation representations to Quaternions.
You might wish to represent your rotations as Angle Axis pairs. That is, you define a axis of rotation and the angle with which to rotate about this axis. Quaternion defines a method fromAngleAxis
(and fromAngleNormalAxis
) to create a Quaternion from this pair. This is acutally used quite a bit in jME demos to continually rotate objects. You can also obtain a Angle Axis rotation from an existing Quaternion using toAngleAxis
.
//rotate about the Y-Axis by approximately 1 pi -Vector3f axis = Vector3f.UNIT_Y; + ++ +Introduction to Mathematical Functionality
++ ++ ++ +It's a fact of life, math is hard. Unfortunately, 3D graphics require a fair bit of knowledge about the subject. Fortunately, jME is able to hide the majority of the details away from the user. Vectors are the fundamental type in the 3D environment, and it is used extensively. Matrices are also a basic necessity of 3D for representing linear systems. Quaternions are perhaps the most complicated of the basic types and are used for rotation in jME. +
+ ++I'll discuss how these are used in the system for the core functionality. Including Transforming, Visibility Determination, Collision Detection, and the Coordinate System. Note, that these are low level details. Further chapters will discuss how to use these various systems from a high level perspective. +
+ ++To get a visual introduction to math in jME3 for the absolute beginner, check out our Math for Dummies introduction class. +
+ +Coordinate System
++ ++ +Definition
++ ++ ++ +A coordinate system consists of an origin (single point in space) and three coordinate axes that are each unit length and mutually perpendicular. The axes can be written as the column of a Matrix, R = [U1|U2|U3]. In fact, this is exactly how CameraNode works. The coordinate system defined by Camera is stored in a Matrix. +
+ ++jME uses a Right-Handed coordinate system (as OpenGL does). +
+ ++The definition of a coordinate system is defined in jME by the properties sent to Camera. There are no error checks to insure that: 1) the coordinate system is right-handed and 2) The axes are mutually perpendicular. Therefore, if the user sets the axes incorrectly, they are going to experience very odd rendering artifacts (random culling, etc). +
+ ++ +
+ +Transformations
++ ++ ++ +Transformations define an operation that converts points from one coordinate system to another. This includes translation, rotation and scaling. In jME, local transforms are used to represent the positioning of objects relative to a parent coordinate system. While, world transforms are used to represent the positioning of objects in a global coordinate system. +
+ +Visibility Determination
++ ++ ++ +Visibility Determination concerns itself with minimizing the amount of data that is sent to the graphics card for rendering. Specifically, we do not want to send data that will not be seen. Data not sent to the graphics card is said to be culled. The primary focus of this section is Frustum Culling based on the Camera's view frustum. In essence, this frustum creates six standard view planes. The BoundingVolume of an object is tested against the frustum planes to determine if it is contained in the frustum. If at any point the object's bounding is outside of the plane, it is tossed out and no longer processed for rendering. This also includes any children that it managed, allowing fast culling of large sections of the scene. +
+ +Fundamental Types
++ ++ +ColorRGBA
++ ++ +Definition
++ ++ ++ +ColorRGBA defines a color value in the jME library. The color value is made of three components, red, green and blue. A fourth component defines the alpha value (transparent) of the color. Every value is set between [0, 1]. Anything less than 0 will be clamped to 0 and anything greater than 1 will be clamped to 1. +
+ ++Note: If you would like to "convert" an ordinary RGB value (0-255) to the format used here (0-1), simply multiply it with: 1/255. +
+ +jME Class
++ ++ ++ +ColorRGBA defines a few static color values for ease of use. That is, rather than: +
+ColorRGBA red = new ColorRGBA(1,0,0,1); +object.setSomeColor(red);+ ++you can simply say: + +
+object.setSomeColor(ColorRGBA.red)+ ++ColorRGBA will also handle interpolation between two colors. Given a second color and a value between 0 and 1, a the owning ColorRGBA object will have its color values altered to this new interpolated color. +
+ +Matrix
++ ++ ++ +See
+ +
+ +and +Definition
++ ++ ++ +A Matrix is typically used as a linear transformation to map vectors to vectors. That is: Y = MX where X is a Vector and M is a Matrix applying any or all transformations (scale, rotate, translate). +
+ ++There are a few special matrices: +
+ ++zero matrix is the Matrix with all zero entries. +
++ ++
+ +0 0 0 ++ +0 0 0 ++ +0 0 0 ++ +The Identity Matrix is the matrix with 1 on the diagonal entries and 0 for all other entries. +
++ ++
+ +1 0 0 ++ +0 1 0 ++ +0 0 1 ++ +A Matrix is invertible if there is a matrix M-1 where MM-1 = M-1M = I. +
+ ++ +The transpose of a matrix M = [mij] is MT = [mji]. This causes the rows of M to become the columns of MT. +
++ ++
+ +1 1 1 1 2 3 ++ +2 2 2 ⇒ 1 2 3 ++ +3 3 3 1 2 3 ++ +A Matrix is symmetric if M = MT. +
++ ++
+ +X A B ++ +A X C ++ +B C X ++Where X, A, B, and C equal numbers +
+ ++jME includes two types of Matrix classes: Matrix3f and Matrix4f. Matrix3f is a 3x3 matrix and is the most commonly used (able to handle scaling and rotating), while Matrix4f is a 4x4 matrix that can also handle translation. +
+ +Transformations
++ ++ ++ +Multiplying a vector with a Matrix allows the vector to be transformed. Either rotating, scaling or translating that vector. +
+ +Scaling
++ ++ ++ +If a diagonal Matrix, defined by D = [dij] and dij = 0 for i != j, has all positive entries it is a scaling matrix. If di is greater than 1 then the resulting vector will grow, while if di is less than 1 it will shrink. +
+ +Rotation
++ ++ ++ +A rotation matrix requires that the transpose and inverse are the same matrix (R-1 = RT). The rotation matrix R can then be calculated as: R = I + (sin(angle)) S + (1 - cos(angle)S2 where S is: + +
++ ++
+ +0 u2 -u1 ++ +-u2 0 u0 ++ +u1 -u0 0 +Translation
++ ++ ++ +Translation requires a 4x4 matrix, where the vector (x,y,z) is mapped to (x,y,z,1) for multiplication. The Translation Matrix is then defined as: + +
++ ++
+ +M T ++ +ST 1 ++ +where M is the 3x3 matrix (containing any rotation/scale information), T is the translation vector and ST is the transpose Vector of T. 1 is just a constant. +
+ +jME Class
++ ++ ++ +Both Matrix3f and Matrix4f store their values as floats and are publicly available as (m00, m01, m02, …, mNN) where N is either 2 or 3. +
+ ++Most methods are straight forward, and I will leave documentation to the Javadoc. +
+ +Vector
++ ++ ++ +See
+ +
+ +and +Definition
++ ++ ++ +Vectors are used to represent a multitude of things in jME, points in space, vertices in a triangle mesh, normals, etc. These classes (Vector3f in particular) are probably the most used class in jME. +
+ ++A Vector is defined by an n-tuple of real numbers. V = <V1, V2,…, Vn>. +
+ ++We have two Vectors (2f and 3f) meaning we have tuples of 2 float values or 3 float values. +
+ +Operations
++ ++ +Multiplication by Scalar
++ ++ ++ +A Vector can be multiplied by a scalar value to produce a second Vector with the same proportions as the first. aV = Va = <aV1, aV2,…,aVn> +
+ +Addition and Subtraction
++ ++ ++ +Adding or subtracting two Vectors occurs component-wise. That is the first component is added (subtracted) with the first component of the second Vector and so on. +
+ ++P + Q = <P1+Q1, P2+Q2, …, Pn+Qn> +
+ +Magnitude
++ ++ ++ +The magnitude defines the length of a Vector. A Vector of magnitude 1 is unit length. +
+ ++For example, if V = (x, y, z), the magnitude is the square root of (x2 + y2 + z2). +
+ ++A Vector can be normalized or made unit length by multiplying the Vector by (1/magnitude). +
+ +Dot Products
++ ++ ++ +The dot product of two vectors is defined as: +P dot Q = PxQx + PyQy + PzQz +
+ ++Using the dot product allows us to determine how closely two Vectors are pointing to the same point. If the dot product is negative they are facing in relatively opposite directions, while postive tells us they are pointing in the relative same direction. +
+ ++If the dot product is 0 then the two Vectors are orthogonal or 90 degrees off. +
+ +Cross Product
++ ++ ++ +The Cross Product of two Vectors returns a third Vector that is prependicular to the two Vectors. This is very useful for calculating surface normals. +
+ ++P X Q = <PyQz - PzQy, PzQx - PxQz, PxQy - PyQx> +
+ +jME Class
++ ++ ++ +Vector3f and Vector2f store their values (x, y, z) and (x, y) respectively as floats. Most methods are straight forward, and I will leave documentation to the Javadoc. +
+ +Quaternion
++ ++ ++ +See +
+ +Definition
++ ++ ++ +Quaternions define a subset of a hypercomplex number system. Quaternions are defined by (i2 = j2 = k2 = ijk = -1). jME makes use of Quaternions because they allow for compact representations of rotations, or correspondingly, orientations, in 3D space. With only four float values, we can represent an object's orientation, where a rotation matrix would require nine. They also require fewer arithmetic operations for concatenation. +
+ ++Additional benefits of the Quaternion is reducing the chance of Gimbal Lock and allowing for easily interpolation between two rotations (spherical linear interpolation or slerp). +
+ ++While Quaternions are quite difficult to fully understand, there are an exceeding number of convenience methods to allow you to use them without having to understand the math behind it. Basically, these methods involve nothing more than setting the Quaternion's x,y,z,w values using other means of representing rotations. The Quaternion is then contained in Spatial as its local rotation component. +
+ ++Quaternion q has the form +
+ ++q = <w,x,y,z> = w + xi + yj + zk +
+ ++or alternatively, it can be written as: +
+ ++q = s + v, where s represents the scalar part corresponding to the w-component of q, and v represents the vector part of the (x, y, z) components of q. +
+ ++Multiplication of Quaternions uses the distributive law and adheres to the following rules with multiplying the imaginary components (i, j, k): +
+ ++
+ +i2 = j2 = k2 = -1
+ +ij = -ji = k
+ +jk = -kj = i
+ +ki = -ik = j
++However, Quaternion multiplication is not commutative, so we have to pay attention to order. +
+ ++q1q2 = s1s2 - v1 dot v2 + s1v2 + s2v1 + v1 X v2 +
+ ++Quaternions also have conjugates where the conjugate of q is (s - v) +
+ ++These basic operations allow us to convert various rotation representations to Quaternions. +
+ +Angle Axis
++ ++ ++ +You might wish to represent your rotations as Angle Axis pairs. That is, you define a axis of rotation and the angle with which to rotate about this axis. Quaternion defines a method
+ +fromAngleAxis
(andfromAngleNormalAxis
) to create a Quaternion from this pair. This is acutally used quite a bit in jME demos to continually rotate objects. You can also obtain a Angle Axis rotation from an existing Quaternion usingtoAngleAxis
. +Example - Rotate a Spatial Using fromAngleAxis
++//rotate about the Y-Axis by approximately 1 pi +Vector3f axis = Vector3f.UNIT_Y; // UNIT_Y equals (0,1,0) and does not require to create a new object float angle = 3.14f; -s.getLocalRotation().fromAngleAxis(angle, axis);Three Angles
You can also represent a rotation by defining three angles. The angles represent the rotation about the individual axes. Passing in a three-element array of floats defines the angles where the first element is X, second Y and third is Z. The method provided by Quaternion is
fromAngles
and can also fill an array usingtoAngles
Example - Rotate a Spatial Using fromAngles
+ +//rotate 1 radian on the x, 3 on the y and 0 on z +s.getLocalRotation().fromAngleAxis(angle, axis);+ +Three Angles
++ ++ ++ +You can also represent a rotation by defining three angles. The angles represent the rotation about the individual axes. Passing in a three-element array of floats defines the angles where the first element is X, second Y and third is Z. The method provided by Quaternion is
+ +fromAngles
and can also fill an array usingtoAngles
+Example - Rotate a Spatial Using fromAngles
++//rotate 1 radian on the x, 3 on the y and 0 on z float[] angles = {1, 3, 0}; -s.getLocalRotation().fromAngles(angles);Three Axes
If you have three axes that define your rotation, where the axes define the left axis, up axis and directional axis respectively) you can make use of
fromAxes
to generate the Quaternion. It should be noted that this will generate a new Matrix object that is then garbage collected, thus, this method should not be used if it will be called many times. Again,toAxes
will populate a Vector3f array.Example - Rotate a Spatial Using fromAxes
+ +//rotate a spatial to face up ~45 degrees +s.getLocalRotation().fromAngles(angles);+ +Three Axes
++ ++ ++ +If you have three axes that define your rotation, where the axes define the left axis, up axis and directional axis respectively) you can make use of
+ +fromAxes
to generate the Quaternion. It should be noted that this will generate a new Matrix object that is then garbage collected, thus, this method should not be used if it will be called many times. Again,toAxes
will populate a Vector3f array. +Example - Rotate a Spatial Using fromAxes
++//rotate a spatial to face up ~45 degrees Vector3f[] axes = new Vector3f[3]; axes[0] = new Vector3f(-1, 0, 0); //left axes[1] = new Vector3f(0, 0.5f, 0.5f); //up axes[2] = new Vector3f(0, 0.5f, 0.5f); //dir -s.getLocalRotation().fromAxes(axes);Rotation Matrix
Commonly you might find yourself with a Matrix defining a rotation. In fact, it's very common to contain a rotation in a Matrix create a Quaternion, rotate the Quaternion, and then get the Matrix back. Quaternion contains a
fromRotationMatrix
method that will create the appropriate Quaternion based on the give Matrix. ThetoRotationMatrix
will populate a given Matrix.Example - Rotate a Spatial Using a Rotation Matrix
+ +Matrix3f mat = new Matrix3f(); +s.getLocalRotation().fromAxes(axes);+ +Rotation Matrix
++ ++ ++ +Commonly you might find yourself with a Matrix defining a rotation. In fact, it's very common to contain a rotation in a Matrix create a Quaternion, rotate the Quaternion, and then get the Matrix back. Quaternion contains a
+ +fromRotationMatrix
method that will create the appropriate Quaternion based on the give Matrix. ThetoRotationMatrix
will populate a given Matrix. +Example - Rotate a Spatial Using a Rotation Matrix
++Matrix3f mat = new Matrix3f(); mat.setColumn(0, new Vector3f(1,0,0)); mat.setColumn(1, new Vector3f(0,-1,0)); mat.setColumn(2, new Vector3f(0,0,1)); -s.getLocalRotation().fromRotationMatrix(mat);As you can see there are many ways to build a Quaternion. This allows you to work with rotations in a way that is conceptually easier to picture, but still build Quaternions for internal representation.
Slerp
One of the biggest advantages to using Quaternions is allowing interpolation between two rotations. That is, if you have an initial Quaternion representing the original orientation of an object, and you have a final Quaternion representing the orientation you want the object to face, you can do this very smoothly with slerp. Simply supply the time, where time is [0, 1] and 0 is the initial rotation and 1 is the final rotation.
Example - Use Slerp to Rotate Between two Quaternions
+ +Quaternion q1; +s.getLocalRotation().fromRotationMatrix(mat);+ ++As you can see there are many ways to build a Quaternion. This allows you to work with rotations in a way that is conceptually easier to picture, but still build Quaternions for internal representation. +
+ +Slerp
++ ++ ++ +One of the biggest advantages to using Quaternions is allowing interpolation between two rotations. That is, if you have an initial Quaternion representing the original orientation of an object, and you have a final Quaternion representing the orientation you want the object to face, you can do this very smoothly with slerp. Simply supply the time, where time is [0, 1] and 0 is the initial rotation and 1 is the final rotation. +
+ +Example - Use Slerp to Rotate Between two Quaternions
++Quaternion q1; Quaternion q2; //the rotation half-way between these two -Quaternion q3 = q1.slerp(q2, 0.5f);Multiplication
+ +You can concatenate (add) rotations: This means you turn the object first around one axis, then around the other, in one step.
Quaternion myRotation = pitch90.mult(roll45); /* pitch and roll */To rotate a Vector3f around its origin by the Quaternion amount, use the multLocal method of the Quaternion:
Quaternion myRotation = pitch90; +Quaternion q3 = q1.slerp(q2, 0.5f);+ +Multiplication
++ ++ +You can concatenate (add) rotations: This means you turn the object first around one axis, then around the other, in one step. +
+Quaternion myRotation = pitch90.mult(roll45); /* pitch and roll */+ ++To rotate a Vector3f around its origin by the Quaternion amount, use the multLocal method of the Quaternion: +
+Quaternion myRotation = pitch90; Vector3f myVector = new Vector3f(0,0,-1); -myRotation.multLocal(myVector);Utility Classes
Along with the base Math classes, jME provides a number of Math classes to make development easier (and, hopefully, faster). Most of these classes find uses throughout the jME system internally. They can also prove beneficial to users as well.
Fast Math
See Javadoc
Definition
FastMath provides a number of convience methods, and where possible faster versions (although this can be at the sake of accuracy).
Usage
FastMath provides a number of constants that can help with general math equations. One important attribute is
USE_FAST_TRIG
if you set this to true, a look-up table will be used for trig functions rather than Java's standard Math library. This provides significant speed increases, but might suffer from accuracy so care should be taken.There are five major categories of functions that FastMath provides.
Trig Functions
cos and acos - provide cosine and arc cosine values (make use of the look-up table ifUSE_FAST_TRIG
is true) tan and atan - provide tangent and arc tangent valuesNumerical Methods
ceil - provides the ceiling (smallest value that is greater than or equal to a given value and an integer)of a value. floor - provides the floor (largest value that is less than or equal to a given value and an integer) of a value. exp - provides the euler number (e) raised to the provided value. sqr - provides the square of a value (i.e. value * value). pow - provides the first given number raised to the second. isPowerOfTwo - provides a boolean if a value is a power of two or not (e.g. 32, 64, 4). abs - provides the absolute value of a given number. sign - provides the sign of a value (1 if positive, -1 if negative, 0 if 0). log - provides the natural logarithm of a value. sqrt - provides the square root of a value. invSqrt - provides the inverse square root of a value (1 / sqrt(value).Linear Algebra
LERP - calculate the linear interpolation of two points given a time between 0 and 1. determinant - calculates the determinant of a 4x4 matrix.Geometric Functions
counterClockwise - given three points (defining a triangle), the winding is determined. 1 if counter-clockwise, -1 if clockwise and 0 if the points define a line. pointInsideTriangle - calculates if a point is inside a triangle. sphericalToCartesian - converts a point from spherical coordinates to cartesian coordinates. cartesianToSpherical - converts a point from cartesian coordinates to spherical coordinates.Misc.
newRandomFloat - obtains a random float.Line
See Javadoc
Definition
A line is a straight one-dimensional figure having no thickness and extending infinitely in both directions. A line is defined by two points A and B with the line passing through both.
Usage
jME defines a Line class that is defined by an origin and direction. In reality, this Line class is typically used as a line segment. Where the line is finite and contained between these two points.
random
provides a means of generate a random point that falls on the line between the origin and direction points.Example 1 - Find a Random Point on a Line
Line(new Vector3f(0,1,0), new Vector3f(3,2,1)); -Vector3f randomPoint = l.random();Plane
See Javadoc
Definition
A plane is defined by the equation N . (X - X0) = 0 where N = (a, b, c) and passes through the point X0 = (x0, y0, z0). X defines another point on this plane (x, y, z).
N . (X - X0) = 0 can be described as (N . X) + (N . -X0) = 0
or
(ax + by + cz) + (-ax0-by0-cz0) = 0
where (-ax0-by0-cz0) = d
Where d is the negative value of a point in the plane times the unit vector describing the orientation of the plane.
This gives us the general equation: (ax + by + cz + d = 0)
Usage in jME
jME defines the Plane as ax + by + cz = -d. Therefore, during creation of the plane, the normal of the plane (a,b,c) and the constant d is supplied.
The most common usage of Plane is Camera frustum planes. Therefore, the primary purpose of Plane is to determine if a point is on the positive side, negative side, or intersecting a plane.
Plane defines the constants:
NEGATIVE_SIDE
- represents a point on the opposite side to which the normal points.NO_SIDE
- represents a point that lays on the plane itself.POSITIVE_SIDE
- represents a point on the side to which the normal points.These values are returned on a call to
whichSide
.Example 1 - Determining if a Point is On the Positive Side of a Plane
+ +Vector3f normal = new Vector3f(0,1,0); +myRotation.multLocal(myVector);+ +Utility Classes
++ ++ ++ +Along with the base Math classes, jME provides a number of Math classes to make development easier (and, hopefully, faster). Most of these classes find uses throughout the jME system internally. They can also prove beneficial to users as well. +
+ +Fast Math
++ ++ ++ +See +
+ +Definition
++ ++ ++ +FastMath provides a number of convience methods, and where possible faster versions (although this can be at the sake of accuracy). +
+ +Usage
++ ++ ++ +FastMath provides a number of constants that can help with general math equations. One important attribute is
+ +USE_FAST_TRIG
if you set this to true, a look-up table will be used for trig functions rather than Java's standard Math library. This provides significant speed increases, but might suffer from accuracy so care should be taken. ++There are five major categories of functions that FastMath provides. +
+ +Trig Functions
+++ ++
+ +- +
cos and acos - provide and values (make use of the look-up table if+USE_FAST_TRIG
is true)- +
sin and asin - provide and values (make use of the look-up table if+USE_FAST_TRIG
is true)- +
tan and atan - provide and values+Numerical Methods
+++ ++
+ +- +
ceil - provides the ceiling (smallest value that is greater than or equal to a given value and an integer)of a value.+- +
floor - provides the floor (largest value that is less than or equal to a given value and an integer) of a value.+- +
exp - provides the (e) raised to the provided value.+- +
sqr - provides the square of a value (i.e. value * value).+- +
pow - provides the first given number raised to the second.+- +
isPowerOfTwo - provides a boolean if a value is a power of two or not (e.g. 32, 64, 4).+- +
abs - provides the of a given number.+- +
sign - provides the sign of a value (1 if positive, -1 if negative, 0 if 0).+- +
log - provides the of a value.+- +
sqrt - provides the of a value.+- +
invSqrt - provides the inverse of a value (1 / sqrt(value).+Linear Algebra
+++ ++
+ +- +
LERP - calculate the of two points given a time between 0 and 1.+- +
determinant - calculates the of a 4x4 matrix.+Geometric Functions
+++ ++
+ +- +
counterClockwise - given three points (defining a triangle), the winding is determined. 1 if counter-clockwise, -1 if clockwise and 0 if the points define a line.+- +
pointInsideTriangle - calculates if a point is inside a triangle.+- +
sphericalToCartesian - converts a point from to .+- +
cartesianToSpherical - converts a point from to .+Misc.
+++ ++
+ +- +
newRandomFloat - obtains a random float.+Line
++ ++ ++ +See +
+ +Definition
++ ++ ++ +A line is a straight one-dimensional figure having no thickness and extending infinitely in both directions. A line is defined by two points A and B with the line passing through both. +
+ +Usage
++ ++ ++ +jME defines a Line class that is defined by an origin and direction. In reality, this Line class is typically used as a line segment. Where the line is finite and contained between these two points. +
+ ++
+ +random
provides a means of generate a random point that falls on the line between the origin and direction points. +Example 1 - Find a Random Point on a Line
+++ +Line(new Vector3f(0,1,0), new Vector3f(3,2,1)); +Vector3f randomPoint = l.random();+ +Plane
++ ++ ++ +See +
+ +Definition
++ ++ ++ +A plane is defined by the equation N . (X - X0) = 0 where N = (a, b, c) and passes through the point X0 = (x0, y0, z0). X defines another point on this plane (x, y, z). +
+ ++N . (X - X0) = 0 can be described as (N . X) + (N . -X0) = 0 +
+ ++or +
+ ++(ax + by + cz) + (-ax0-by0-cz0) = 0 +
+ ++where (-ax0-by0-cz0) = d +
+ ++Where d is the negative value of a point in the plane times the unit vector describing the orientation of the plane. +
+ ++This gives us the general equation: (ax + by + cz + d = 0) +
+ +Usage in jME
++ ++ ++ +jME defines the Plane as ax + by + cz = -d. Therefore, during creation of the plane, the normal of the plane (a,b,c) and the constant d is supplied. +
+ ++The most common usage of Plane is Camera frustum planes. Therefore, the primary purpose of Plane is to determine if a point is on the positive side, negative side, or intersecting a plane. +
+ ++Plane defines the constants: + +
++
+ +- +
+NEGATIVE_SIDE
- represents a point on the opposite side to which the normal points.- +
+NO_SIDE
- represents a point that lays on the plane itself.- +
+POSITIVE_SIDE
- represents a point on the side to which the normal points.+ +These values are returned on a call to
+ +whichSide
. +Example 1 - Determining if a Point is On the Positive Side of a Plane
++Vector3f normal = new Vector3f(0,1,0); float constant = new Vector3f(1,1,1).dot(normal); Plane testPlane = new Plane(normal, constant); @@ -317,9 +857,18 @@ int side = testPlane.whichSide(new Vector3f(2,1,0); if(side == Plane.NO_SIDE) { System.out.println("This point lies on the plane"); -}Example 2 - For the Layperson
+ +Using the standard constructor Plane(Vector3f normal, float constant), here is what you need to do to create a plane, and then use it to check which side of the plane a point is on.
package test; +}+ +Example 2 - For the Layperson
++ ++ +Using the standard constructor Plane(Vector3f normal, float constant), here is what you need to do to create a plane, and then use it to check which side of the plane a point is on. +
+package test; import java.util.logging.Logger; @@ -380,53 +929,143 @@ public class TestPlanes logger.info("p2 position relative to plane is "+plane.whichSide(p2)); //outputs POSITIVE logger.info("p3 position relative to plane is "+plane.whichSide(p3)); //outputs NONE } -}Ray
See Javadoc
Definition
Ray defines a line that starts at a point A and continues in a direction through B into infinity.
This Ray is used extensively in jME for Picking. A Ray is cast from a point in screen space into the scene. Intersections are found and returned. To create a ray supply the object with two points, where the first point is the origin.
Example 1 - Create a Ray That Represents Where the Camera is Looking
Ray ray = new Ray(cam.getLocation(), cam.getDirection());Rectangle
See Javadoc
Definition
Rectangle defines a finite plane within three dimensional space that is specified via three points (A, B, C). These three points define a triangle with the forth point defining the rectangle ( (B + C) - A ).
jME Usage
Rectangle is a straight forward data class that simply maintains values that defines a Rectangle in 3D space. One interesting use is the
random
method that will create a random point on the Rectangle. The Particle System makes use of this to define an area that generates Particles.Example 1 : Define a Rectangle and Get a Point From It
+ +Vector3f v1 = new Vector3f(1,0,0); +}+ +Ray
++ ++ ++ +See +
+ +Definition
++ ++ ++ +Ray defines a line that starts at a point A and continues in a direction through B into infinity. +
+ ++This Ray is used extensively in jME for Picking. A Ray is cast from a point in screen space into the scene. Intersections are found and returned. To create a ray supply the object with two points, where the first point is the origin. +
+ +Example 1 - Create a Ray That Represents Where the Camera is Looking
+++ +Ray ray = new Ray(cam.getLocation(), cam.getDirection());+ +Rectangle
++ ++ ++ +See +
+ +Definition
++ ++ ++ +Rectangle defines a finite plane within three dimensional space that is specified via three points (A, B, C). These three points define a triangle with the forth point defining the rectangle ( (B + C) - A ). +
+ +jME Usage
++ ++ ++ +Rectangle is a straight forward data class that simply maintains values that defines a Rectangle in 3D space. One interesting use is the
+ +random
method that will create a random point on the Rectangle. The Particle System makes use of this to define an area that generates Particles. +Example 1 : Define a Rectangle and Get a Point From It
++Vector3f v1 = new Vector3f(1,0,0); Vector3f v2 = new Vector3f(1,1,0); Vector3f v3 = new Vector3f(0,1,0); Rectangle(v1, v2, v3); -Vector3f point = r.random();Triangle
See Javadoc
Definition
A triangle is a 3-sided polygon. Every triangle has three sides and three angles, some of which may be the same. If the triangle is a right triangle (one angle being 90 degrees), the side opposite the 90 degree angle is the hypotenuse, while the other two sides are the legs. All triangles are convex and bicentric.
Usage
jME's Triangle class is a simple data class. It contains three Vector3f objects that represent the three points of the triangle. These can be retrieved via the
get
method. Theget
method, obtains the point based on the index provided. Similarly, the values can be set via theset
method.Example 1 - Creating a Triangle
+ +//the three points that make up the triangle +Vector3f point = r.random();+ +Triangle
++ ++ ++ +See +
+ +Definition
++ ++ ++ +A triangle is a 3-sided polygon. Every triangle has three sides and three angles, some of which may be the same. If the triangle is a right triangle (one angle being 90 degrees), the side opposite the 90 degree angle is the hypotenuse, while the other two sides are the legs. All triangles are and . +
+ +Usage
++ ++ ++ +jME's Triangle class is a simple data class. It contains three Vector3f objects that represent the three points of the triangle. These can be retrieved via the
+ +get
method. Theget
method, obtains the point based on the index provided. Similarly, the values can be set via theset
method. +Example 1 - Creating a Triangle
++//the three points that make up the triangle Vector3f p1 = new Vector3f(0,1,0); Vector3f p2 = new Vector3f(1,1,0); Vector3f p3 = new Vector3f(0,1,1); -Triangle t = new Triangle(p1, p2, p3);Tips and Tricks
How do I get height/width of a spatial?
+ +Cast the spatial to com.jme3.bounding.BoundingBox to be able to use getExtent().
Vector3f extent = ((BoundingBox) spatial.getWorldBound()).getExtent(new Vector3f()); +Triangle t = new Triangle(p1, p2, p3);+ +Tips and Tricks
++ ++ +How do I get height/width of a spatial?
++ ++ +Cast the spatial to com.jme3.bounding.BoundingBox to be able to use getExtent(). + +
+Vector3f extent = ((BoundingBox) spatial.getWorldBound()).getExtent(new Vector3f()); float x = ( (BoundingBox)spatial.getWorldBound()).getXExtent(); float y = ( (BoundingBox)spatial.getWorldBound()).getYExtent(); -float z = ( (BoundingBox)spatial.getWorldBound()).getZExtent();How do I position the center of a Geomtry?
- \ No newline at end of file +float z = ( (BoundingBox)spatial.getWorldBound()).getZExtent();geo.center().move(pos);
geo.center().move(pos);+ +
+ +Although we recommend the jMonkeyPlatform for developing JME3 games, you can use any IDE (integrated development environment) such as NetBeans or Eclipse, and even work freely from the commandline. Here is a generic IDE-independent "getting started" tutorial. +
+ ++This example shows how to set up and run a simple application (HelloJME3) that depends on the jMonkeyEngine3 libraries. +
+ ++The directory structure will look as follows: + +
+jme3/ +jme3/lib +jme3/src +... +HelloJME3/ +HelloJME3/lib +HelloJME3/assets +HelloJME3/src +...+ +
+
+To install the development version of jme3, , unzip the folder into a directory named jme3
. The filenames here are just an example, but they will always be something like jME3_xx-xx-2011
.
+
mkdir jme3 +cd jme3 +unzip jME3_01-18-2011.zip+ +
+Alternatively, you can build JME3 from the sources. (Recommended for JME3 developers.) +
+svn checkout https://jmonkeyengine.googlecode.com/svn/trunk/engine jme3 +cd jme3 +ant run +cd ..+ +
+If you see a Test Chooser open now, the build was successful. Tip: Use ant
to build the libraries without running the demos.
+
+
+First we set up the directory and source package structure for your game project. Note that the game project directory HelloJME3
is on the same level as your jme3
checkout. In this example, we create a Java package that we call hello
in the source directory.
+
mkdir HelloJME3 +mkdir HelloJME3/src +mkdir HelloJME3/src/hello+ +
+ +Next we copy the necessary JAR libraries. You only have to do this set of steps once every time you download a new JME3 build. +
+mkdir HelloJME3/build +mkdir HelloJME3/lib +cp jme3/jMonkeyEngine3.jar HelloJME3/lib +cp jme3/lib/*.* HelloJME3/lib+ +
+If you have built JME3 from the sources, then the paths are different: +
+mkdir HelloJME3/build +mkdir HelloJME3/lib +cp jme3/dist/jMonkeyEngine3.jar HelloJME3/lib +cp jme3/dist/*.* HelloJME3/lib+ +
+
+To test your setup, create the file HelloJME3/src/hello/HelloJME3.java
with any text editor, paste the following sample code, and save.
+
package hello; + +import com.jme3.app.SimpleApplication; +import com.jme3.material.Material; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Box; +import com.jme3.math.ColorRGBA; + +public class HelloJME3 extends SimpleApplication { + + public static void main(String[] args){ + HelloJME3 app = new HelloJME3(); + app.start(); + } + + @Override + public void simpleInitApp() { + Box(Vector3f.ZERO, 1, 1, 1); + Geometry geom = new Geometry("Box", b); + Material mat = new Material(assetManager, + "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", ColorRGBA.Blue); + geom.setMaterial(mat); + rootNode.attachChild(geom); + } +}+ +
+ +We build the sample application into the build directory… +
+cd HelloJME3 +javac -d build -cp "lib/eventbus-1.4.jar:lib/j-ogg-oggd.jar:lib/j-ogg-vorbisd.jar:lib/jME3-lwjgl-natives.jar:lib/jMonkeyEngine3.jar:lib/jbullet.jar:lib/jheora-jst-debug-0.6.0.jar:lib/jinput.jar:lib/jme3test.jar:lib/jme3testdata.jar:lib/lwjgl.jar:lib/nifty-1.3-SNAPSHOT.jar:lib/nifty-default-controls-1.3-SNAPSHOT.jar:lib/nifty-examples-1.3-SNAPSHOT.jar:lib/nifty-style-black-1.3-SNAPSHOT.jar:lib/nifty-style-grey-1.0.jar:lib/stack-alloc.jar:lib/vecmath.jar:lib/xmlpull-xpp3-1.1.4c.jar:." src/hello/HelloJME3.java+ +
+… and run it. +
+cd build +java -cp "../lib/eventbus-1.4.jar:../lib/j-ogg-oggd.jar:../lib/j-ogg-vorbisd.jar:../lib/jME3-lwjgl-natives.jar:../lib/jMonkeyEngine3.jar:../lib/jbullet.jar:../lib/jheora-jst-debug-0.6.0.jar:../lib/jinput.jar:../lib/jme3test.jar:../lib/jme3testdata.jar:../lib/lwjgl.jar:../lib/nifty-1.3-SNAPSHOT.jar:../lib/nifty-default-controls-1.3-SNAPSHOT.jar:../lib/nifty-examples-1.3-SNAPSHOT.jar:../lib/nifty-style-black-1.3-SNAPSHOT.jar:../lib/nifty-style-grey-1.0.jar:../lib/stack-alloc.jar:../lib/vecmath.jar:../lib/xmlpull-xpp3-1.1.4c.jar:." hello/HelloJME3+ +
+Note: If you use Windows, the classpath separator is ";" instead of ":". +
+ ++If a settings dialog pops up, confirm the default settings. You should now see a simple window with a 3-D cube. Use the mouse and the WASD keys to move. It works! +
+ ++ +For media files and other assets, we recommend creating the following project structure: +
+cd HelloJME3 +mkdir assets +mkdir assets/Interface +mkdir assets/Materials +mkdir assets/MatDefs +mkdir assets/Models +mkdir assets/Scenes +mkdir assets/Shaders +mkdir assets/Sounds +mkdir assets/Textures+ +
+This will allow the default assetManager to load media files stored in the assets
directory, like in this example:
+
import com.jme3.scene.Spatial; +... +Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.meshxml"); +rootNode.attachChild(elephant); +...+ +
+You will learn more about the asset manager and how to customize it later. For now feel free to structure your assets (images, textures, models) into further sub-directories, like in this example the assets/models/Elephant
directory that contains the elephant.meshxml
model and its materials.
+
+ +Now follow the tutorials and write your first jMonkeyEngine game. +
+ +Before you start, make certain you are familiar with the following concepts and terminology.
OpenGL is the Open Graphics Library, a platform-independent specification for rendering 2D/3D computer graphics. For Java, there are two implementations of OpenGL-based renderers:
OpenAL is the Open Audio Library, a platform-independent 3D audio API.
The jME Context makes settings, renderer, timer, input and event listeners, display system, accessible to a JME game.
Coordinates represent a location in a coordinate system, relative to the origin (0,0,0). m. In 3D space, you need to specify three coordinate values to locate a point: x,y,z. -As opposed to a vector (which looks similar), a coordinate does not have a "direction".
The origin is the central point in the 3D world. It's at the coordinates (0,0,0).
-Code sample: Vector3f origin = new Vector3f( Vector3f.ZERO );
A vector has a length and a direction. It is used like an arrow pointing at a point in 3D space. A vector starts at the origin (0,0,0), and ends at the target coordinate (x,y,z). Backwards directions are expressed with negative values.
-Code sample: Vector3f v = new Vector3f( 17 , -4 , 0 );
A unit vector is a basic vector with a length of 1 world unit. Since its length is fixed (and it thus can only point at one location anyway), the only interesting thing about this vector is its direction.
Vector3f.UNIT_X
= ( 1, 0, 0) = rightVector3f.UNIT_Y
= ( 0, 1, 0) = upVector3f.UNIT_Z
= ( 0, 0, 1) = forwardsVector3f.UNIT_XYZ
= 1 wu diagonal right-up-forewardsA normalized vector is a custom unit vector. A normalized vector is not the same as a (surface) normal vector. + +
+Before you start, make certain you are familiar with the following concepts and terminology. + +
+ ++OpenGL is the Open Graphics Library, a platform-independent specification for rendering 2D/3D computer graphics. For Java, there are two implementations of OpenGL-based renderers: +
++OpenAL is the Open Audio Library, a platform-independent 3D audio API. + +
+ ++The jME Context makes settings, renderer, timer, input and event listeners, display system, accessible to a JME game. +
++Coordinates represent a location in a coordinate system, relative to the origin (0,0,0). m. In 3D space, you need to specify three coordinate values to locate a point: x,y,z. +As opposed to a vector (which looks similar), a coordinate does not have a "direction". + + +
+ +
+The origin is the central point in the 3D world. It's at the coordinates (0,0,0).
+Code sample: Vector3f origin = new Vector3f( Vector3f.ZERO );
+
+
+A vector has a length and a direction. It is used like an arrow pointing at a point in 3D space. A vector starts at the origin (0,0,0), and ends at the target coordinate (x,y,z). Backwards directions are expressed with negative values.
+Code sample: Vector3f v = new Vector3f( 17 , -4 , 0 );
+
+
+A unit vector is a basic vector with a length of 1 world unit. Since its length is fixed (and it thus can only point at one location anyway), the only interesting thing about this vector is its direction. +
+Vector3f.UNIT_X
= ( 1, 0, 0) = rightVector3f.UNIT_Y
= ( 0, 1, 0) = upVector3f.UNIT_Z
= ( 0, 0, 1) = forwardsVector3f.UNIT_XYZ
= 1 wu diagonal right-up-forewards+A normalized vector is a custom unit vector. A normalized vector is not the same as a (surface) normal vector. When you normalize a vector, it still has the same direction, but you lose the information where the vector originally pointed. -For instance, you normalize vectors before calculating angles.
A surface normal is a vector that is perpendicular to a plane. -You calculate the Surface Normal by calculating the cross product.
The cross product is a calculation that you use to find a perpendicular vector (an orthogonal, a "right angle" at 90°). +For instance, you normalize vectors before calculating angles. + +
+ ++A surface normal is a vector that is perpendicular to a plane. +You calculate the Surface Normal by calculating the cross product. + +
+ +
+The cross product is a calculation that you use to find a perpendicular vector (an orthogonal, a "right angle" at 90°).
In 3D space, speaking of an orthogonal only makes sense with respect to a plane. You need two vectors to uniquely define a plane. The cross product of the two vectors, v1 × v2
, is a new vector that is perpendicular to this plane. A vector perpendicular to a plane is a called Surface Normal.
-Example: The x unit vector and the y unit vector together define the x/y plane. The vector perpendicular to them is the z axis. JME can calculate that this equation is true:
( Vector3f.UNIT_X.cross( Vector3f.UNIT_Y ) ).equals( Vector3f.UNIT_Z )
== true
Most visible objects in a 3D scene are made up of polygon meshes – characters, terrains, buildings, etc. A mesh is a grid-like structure that represents a complex shape. The advantage of a mesh is that it is mathematically simple enough to render in real time, and detailed enough to be recognizable.
+Example: The x unit vector and the y unit vector together define the x/y plane. The vector perpendicular to them is the z axis. JME can calculate that this equation is true:
+
+( Vector3f.UNIT_X.cross( Vector3f.UNIT_Y ) ).equals( Vector3f.UNIT_Z )
== true
+
+
+ +Most visible objects in a 3D scene are made up of polygon meshes – characters, terrains, buildings, etc. A mesh is a grid-like structure that represents a complex shape. The advantage of a mesh is that it is mathematically simple enough to render in real time, and detailed enough to be recognizable. Every shape is reduced to a number of connected polygons, usually triangles; even round surfaces such as spheres are reduced to a grid of triangles. The polygons' corner points are called vertices. Every vertex is positioned at a coordinate, all vertices together describe the outline of the shape. -You create 3D meshes in tools called mesh editors, e.g in Blender. Learn more about 3D maths here.
What we call "color" is merely part of an object's light reflection. The onlooker's brain uses shading and reflecting properties to infer an object's shape and material. Factors like these make all the difference between chalk vs milk, skin vs paper, water vs plastic, etc! (External examples)
Textures are part of Materials. In the simplest case, an object could have just one texture, the Color Map, loaded from one image file. When you think back of old computer games you'll remember this looks quite plain. -The more information you (the game designer) provide additionally to the Color Map, the higher the degree of detail and realism. Whether you want photo-realistic rendering or "toon" rendering (Cel Shading), everything depends on the quality of your materials and texture maps. Modern 3D graphics use several layers of information to describe one material, each layer is a Texture Map.
Bump maps are used to describe detailed shapes that would be too hard or simply too inefficient to sculpt in a mesh editor. You use Normal Maps to model cracks in walls, rust, skin texture, or a canvas weave. You use Height Maps to model whole terrains with valleys and mountains.
This is a very simple, commonly used type of texture. When texturing a wide area (e.g. walls, floors), you don't create one huge texture, instead you tile a small texture repeatedly to fill the area. -A seamless texture is an image file that has been designed so that it can be used as tiles: The right edge matches the left edge, and the top edge matches the bottom edge. The onlooker cannot easily tell where one starts and the next one ends, thus creating an illusion of a huge texture. The downside is that the tiling becomes painfully obvious when the area is viewed from a distance. Also you cannot use it on more complex models such as characters.
Creating a texture for a cube is easy – but what about a character with a face and extremities? For more complex objects, you design the texture in the same ways as a sewing pattern: One image file contains the outline of the front, back, and side of the object, next to one another. Specific areas of the flat texture (UV coordinates) map onto certain areas of your 3D model (XYZ coordinates), hence the name UV map. -Getting the seams and mappings right is crucial: You will have to use a graphic tool like Blender to create UV Maps. It's worth the while to learn this, UV mapped models look a lot more professional.
Environment Maps are used to create the impression of reflections and refractions. You create a Cube Map to represent your environment, similar to a skybox. (Sphere Maps are possible, but often look too distorted.) You give the Cube Map a set of images showing a "360° view" of the completed scene. The renderer uses this as base to texture the reflective surface. -Of course these reflections are static and not "real", e.g. the player will not see his avatar's face reflected, etc… But they cause the desired "glass/mirror/water" effect, and are fast enough to render in real usecases, it's better than nothing.
You provide the texture in two or three resolutions to be stored in one file (MIP = "multum in parvo" = "many in one"). Depending on how close (or far) the camera is, the engine automatically renders a more (or less) detailed texture for the object. Thus objects look smooth from close up, but don't waste resources with unspottable details when far away. Good for everything, but requires more time to create and more space to store textures.
A procedural texture is generated from repeating one small image, plus some pseudo-random, gradient variations (Perlin noise). Procedural textures look more natural than static rectangular textures, for instance, they look less distorted on spheres. On big meshes, their repetitiveness is much less noticable than tiled seamless textures. Procedural textures are ideal for irregular large-area textures like grass, soil, rock, rust, and walls. See also: jMonkeyPlatform NeoTexture plugin See also: Creating Materials in Blender, Blender: Every Material Known to Man
In 3D games, Skeletal Animation is used for animated characters, but in principle the skeleton approach can be extended to any 3D mesh (for example, an opening crate's hinge can be considered a joint). -Unless you animate a 3D cartoon, realism of animated characters is generally a problem: Movement can look alien-like mechanical or broken, the character appears hollow, or as if floating. Professional game designers invest a lot of effort to make characters animate in a natural way (including motion capturing).
An animated character has an armature: An internal skeleton (Bones) and an external surface (Skin). The Skin is the visible outside of the character and it includes clothing. The Bones are not visible and are used to interpolate (calculate) the morphing steps of the skin. -JME3, the game engine, only loads and plays your recorded animations. You must use a tool (such as Blender) to set up (rig, skin, and animate) a character.
In the JME3 application, you register animated models to the Animation Controller. The controller object gives you access to the available animation sequences. The controller has several channels, each channels can run one animation sequence at a time. To run several sequences, you create several channels, and run them in parallel.
Non-player (computer-controlled) characters (NPCs) are only fun in a game if they do not stupidly bump into walls, or blindly run into the line of fire. You want to make NPCs "aware" of their surroundings and let them make decisions based on the game state – otherwise the player can just ignore them. The most common use case is that you want to make enemies interact in a way so they offer a more interesting challenge for the player. -"Smart" game elements are called artificially intelligent agents (AI agents). An AI agent can be an NPC, but also an "automatic alarm system" that locks doors after an intruder alert, or a trained pet. -The domain of artificial intelligence deals, among other things, with:
There are lots of resources explaining interesting AI algorithms:
+What we call "color" is merely part of an object's light reflection. The onlooker's brain uses shading and reflecting properties to infer an object's shape and material. Factors like these make all the difference between chalk vs milk, skin vs paper, water vs plastic, etc! () + +
+ ++ + +
+ ++Textures are part of Materials. In the simplest case, an object could have just one texture, the Color Map, loaded from one image file. When you think back of old computer games you'll remember this looks quite plain. +The more information you (the game designer) provide additionally to the Color Map, the higher the degree of detail and realism. Whether you want photo-realistic rendering or "toon" rendering (Cel Shading), everything depends on the quality of your materials and texture maps. Modern 3D graphics use several layers of information to describe one material, each layer is a Texture Map. + +
+ ++ +
++ + +
+ ++Bump maps are used to describe detailed shapes that would be too hard or simply too inefficient to sculpt in a mesh editor. You use Normal Maps to model cracks in walls, rust, skin texture, or a canvas weave. You use Height Maps to model whole terrains with valleys and mountains. + + +
+ ++ +
++ +
++ +This is a very simple, commonly used type of texture. When texturing a wide area (e.g. walls, floors), you don't create one huge texture, instead you tile a small texture repeatedly to fill the area. +A seamless texture is an image file that has been designed so that it can be used as tiles: The right edge matches the left edge, and the top edge matches the bottom edge. The onlooker cannot easily tell where one starts and the next one ends, thus creating an illusion of a huge texture. The downside is that the tiling becomes painfully obvious when the area is viewed from a distance. Also you cannot use it on more complex models such as characters. + +
+ ++ +Creating a texture for a cube is easy – but what about a character with a face and extremities? For more complex objects, you design the texture in the same ways as a sewing pattern: One image file contains the outline of the front, back, and side of the object, next to one another. Specific areas of the flat texture (UV coordinates) map onto certain areas of your 3D model (XYZ coordinates), hence the name UV map. +Getting the seams and mappings right is crucial: You will have to use a graphic tool like Blender to create UV Maps. It's worth the while to learn this, UV mapped models look a lot more professional. + +
+ ++ +Environment Maps are used to create the impression of reflections and refractions. You create a Cube Map to represent your environment, similar to a skybox. (Sphere Maps are possible, but often look too distorted.) You give the Cube Map a set of images showing a "360° view" of the completed scene. The renderer uses this as base to texture the reflective surface. +Of course these reflections are static and not "real", e.g. the player will not see his avatar's face reflected, etc… But they cause the desired "glass/mirror/water" effect, and are fast enough to render in real usecases, it's better than nothing. + +
+ ++You provide the texture in two or three resolutions to be stored in one file (MIP = "multum in parvo" = "many in one"). Depending on how close (or far) the camera is, the engine automatically renders a more (or less) detailed texture for the object. Thus objects look smooth from close up, but don't waste resources with unspottable details when far away. Good for everything, but requires more time to create and more space to store textures. + +
+ ++A procedural texture is generated from repeating one small image, plus some pseudo-random, gradient variations (Perlin noise). Procedural textures look more natural than static rectangular textures, for instance, they look less distorted on spheres. On big meshes, their repetitiveness is much less noticable than tiled seamless textures. Procedural textures are ideal for irregular large-area textures like grass, soil, rock, rust, and walls. See also: + +See also: , + +
+ ++In 3D games, Skeletal Animation is used for animated characters, but in principle the skeleton approach can be extended to any 3D mesh (for example, an opening crate's hinge can be considered a joint). +Unless you animate a 3D cartoon, realism of animated characters is generally a problem: Movement can look alien-like mechanical or broken, the character appears hollow, or as if floating. Professional game designers invest a lot of effort to make characters animate in a natural way (including motion capturing). + +
+ ++ +An animated character has an armature: An internal skeleton (Bones) and an external surface (Skin). The Skin is the visible outside of the character and it includes clothing. The Bones are not visible and are used to interpolate (calculate) the morphing steps of the skin. +JME3, the game engine, only loads and plays your recorded animations. You must use a tool (such as Blender) to set up (rig, skin, and animate) a character. +
++In the JME3 application, you register animated models to the Animation Controller. The controller object gives you access to the available animation sequences. The controller has several channels, each channels can run one animation sequence at a time. To run several sequences, you create several channels, and run them in parallel. + +
+ ++Non-player (computer-controlled) characters (NPCs) are only fun in a game if they do not stupidly bump into walls, or blindly run into the line of fire. You want to make NPCs "aware" of their surroundings and let them make decisions based on the game state – otherwise the player can just ignore them. The most common use case is that you want to make enemies interact in a way so they offer a more interesting challenge for the player. +
+ ++"Smart" game elements are called artificially intelligent agents (AI agents). An AI agent can be used to implement enemy NPCs as well as trained pets; you also use them to create automatic alarm systems that lock doors and "call the guards" after the player triggers an intruder alert. +
+ ++The domain of artificial intelligence deals, among other things, with: +
++ +More advanced AIs can also learn, for example using neural networks. +
+ ++There are lots of resources explaining interesting AI algorithms: +
+Before you start making games, make sure you understand general 3D Gaming terminology.
Second, if you are a beginner, we recommend our Scene Graph for Dummies presentation for a visual introduction to the concept of a scene graph.
Then continue learning about jME3 concepts here.
The jMonkeyEngine uses a right-handed coordinate system, just as OpenGL does.
The coordinate system consists of:
Every point in 3D space is defined by its (x,y,z) coordinates. The data type for vectors is com.jme3.math.Vector3f.
For your orientation, the default camera's location is (0.0f,0.0f,10.0f), and it is looking in the direction described by the unit vector (0.0f, 0.0f, -1.0f). This means your point of view is on the positive side of the Z axis, looking towards the origin, down the Z axis.
The unit of meassurement is world unit
(wu). Typically, 1 wu is considered to be one meter. All scales, vectors and points are relative to this coordinate system.
The scene graph represents your 3D world. Objects in the jME3 scene graph are called Spatials. Everything attached to the rootNode is part of the scene graph. Attaching a Spatial to the rootNode (or other nodes) adds the Spatial to the scene; detaching removes it.
A Spatial can be transformed, loaded and saved. There are two types of Spatials, Nodes and Geometries.
Spatial | ||
---|---|---|
Purpose: | A Spatial is an abstract data structure that stores transformations (translation, rotation, scale). | |
Geometry | Node | |
Visibility: | A visible 3-D object. | An invisible "handle" for a group of objects. |
Purpose: | Represents the "look" of an object: Shape, color, texture, opacity/transparency. | Groups Geometries and other Nodes together: You transform a Node to affect all attached Nodes. |
Content: | Transformations, mesh, material. | Transformations. No mesh, no material. |
Examples: | A box, a sphere, player, a building, a piece of terrain, a vehicle, missiles, NPCs, etc… | The rootNode, the guiNode, an audio node, a custom grouping node, etc. |
Before you start creating your game, you should have completed the Hello World tutorial series. It shows how to load and create Spatials, how to lay out a scene by attaching and transforming Spatials, and how to add interaction and effects to a game.
The intermediate and advanced documentation gives you more details on how to put all the parts together to create an awesome Java 3D game!
+ +Before you start making games, make sure you understand general 3D Gaming terminology. +
+ ++Second, if you are a beginner, we recommend our Scene Graph for Dummies presentation for a visual introduction to the concept of a scene graph. +
+ ++Then continue learning about jME3 concepts here. +
+ ++ + +
+ ++The jMonkeyEngine uses a right-handed coordinate system, just as OpenGL does. +
+ ++The coordinate system consists of: + +
++ +Every point in 3D space is defined by its (x,y,z) coordinates. The data type for vectors is com.jme3.math.Vector3f. +
+ ++For your orientation, the default camera's location is (0.0f,0.0f,10.0f), and it is looking in the direction described by the unit vector (0.0f, 0.0f, -1.0f). This means your point of view is on the positive side of the Z axis, looking towards the origin, down the Z axis. +
+ +
+The unit of meassurement is world unit
(wu). Typically, 1 wu is considered to be one meter. All scales, vectors and points are relative to this coordinate system.
+
+ +The scene graph represents your 3D world. Objects in the jME3 scene graph are called Spatials. Everything attached to the rootNode is part of the scene graph. Attaching a Spatial to the rootNode (or other nodes) adds the Spatial to the scene; detaching removes it. +
+ ++ +
+ ++ +A Spatial can be transformed, loaded and saved. There are two types of Spatials, Nodes and Geometries. + +
+Spatial | +||
---|---|---|
Purpose: | A Spatial is an abstract data structure that stores transformations (translation, rotation, scale). | +|
Geometry | Node | +|
Visibility: | A visible 3-D object. | An invisible "handle" for a group of objects. | +
Purpose: | Represents the "look" of an object: Shape, color, texture, opacity/transparency. | Groups Geometries and other Nodes together: You transform a Node to affect all attached Nodes. | +
Content: | Transformations, mesh, material. | Transformations. No mesh, no material. | +
Examples: | A box, a sphere, player, a building, a piece of terrain, a vehicle, missiles, NPCs, etc… | The rootNode, the guiNode, an audio node, a custom grouping node, etc. | +
+ +Before you start creating your game, you should have completed the Hello World tutorial series. It shows how to load and create Spatials, how to lay out a scene by attaching and transforming Spatials, and how to add interaction and effects to a game. +
+ ++The intermediate and advanced documentation gives you more details on how to put all the parts together to create an awesome Java 3D game! + +
+ +When you use the jMonkeyPlatform to deploy your application, you can configure the project to build files required for WebStart automatically. If you use another IDE, or work on the command line, use the following tips to set up WebStart correctly:
Problem:
When running under WebStart, jMonkeyEngine may not have permission to extract the native libraries to the current directory.
Solution:
You can instruct WebStart to load the native libraries itself using the JNLP file, and then instruct jME3 not to try to do so itself.
You can import the LWJGL JNLP extension directly into your extension, however be aware that your application will break whenever they update their jars. Simply add this line to your JNLP:
<extension name="lwjgl" href="http://lwjgl.org/webstart/2.7.1/extension.jnlp" />
You can download the LWJGL native jars from their site, or to ensure you're using the exact same version as bundled with your jME3 release, make your own:
mkdir tmp + ++ +WebStart (JNLP) Deployment
++ ++ ++ +When you use the jMonkeyPlatform to deploy your application, you can configure the project to build files required for WebStart automatically. If you use another IDE, or work on the command line, use the following tips to set up WebStart correctly: +
+ ++Problem: +
+ ++When running under WebStart, jMonkeyEngine may not have permission to extract the native libraries to the current directory. +
+ ++Solution: +
+ ++You can instruct WebStart to load the native libraries itself using the JNLP file, and then instruct jME3 not to try to do so itself. +
+ +Simple way
++ ++ ++ +You can import the LWJGL JNLP extension directly into your extension, however be aware that your application will break whenever they update their jars. Simply add this line to your JNLP: +
+<extension name="lwjgl" href="http://lwjgl.org/webstart/2.7.1/extension.jnlp" />
+ +Reliable way
++ ++ +Native jars
++ ++ +You can download the LWJGL native jars from their site, or to ensure you're using the exact same version as bundled with your jME3 release, make your own: +
+mkdir tmp cd tmp jar xfv ../jME3-lwjgl-natives.jar cd native @@ -15,9 +56,22 @@ for i in *; do cd $i jar cfv ../../native_$i.jar . cd .. -doneRemember to sign all the jar files and move them into the right place from the tmp directory.
JNLP file
+ +Add the following to your JNLP file:
<resources os="Windows"> +done
+ ++Remember to sign all the jar files and move them into the right place from the tmp directory. +
+ +JNLP file
++ ++ +Add the following to your JNLP file: +
+<resources os="Windows"> <j2se version="1.4+"/> <nativelib href="native_windows.jar"/> </resources> @@ -32,11 +86,22 @@ class="level3">Add the following to your JNLP file:
<reso <resources os="SunOS" arch="x86"> <j2se version="1.4+"/> <nativelib href="native_solaris.jar"/> - </resources>
Set low-permissions mode
+ +In your main() method, if running under WebStart, tell jME3 it is running in a low-permission environment so that it doesn't try to load the natives itself:
public static void main(String[] args) + </resources>
+ +Set low-permissions mode
++ +- \ No newline at end of file + }+ +In your main() method, if running under WebStart, tell jME3 it is running in a low-permission environment so that it doesn't try to load the natives itself: +
+public static void main(String[] args) { if (System.getProperty("javawebstart.version") != null) { JmeSystem.setLowPermissions(true); - }
After you have written and tested your game, you want to brand it and distribute it to your users. If you use the build script provided by the jMonkeyPlatform's BaseGame, you have the following deployment options:
Since JAR files are platform independent, your customers can play your jMonkeyEngine application on Windows, Mac OS, or Linux. The only requirement is that the user has the free Java 5 or 6 Runtime (or browser plugin) installed. For more information see http://java.com.
When you run the build script provided by the jMonkeyPlatform, it automatically compiles your classes, libraries, and assets. It creates a dist
directory in your project directory which contains the executable JAR and a directory with libraries.
In the simplest case, you zip up the dist
directory and distribute it to your customers. Companies often have additional tools to create executables and installers.
Here are your deployment options in detail:
The JAR file is the most common deployment method for Java desktop applications. The user downloads the executable JAR file to his machine and runs it to start the game.
Building jar: /home/joe/jMonkeyPlatform/MySuperTestGame/dist/MySuperTestGame.jar
dist
directory and distribute it to your users. Make sure to keep the lib
directory in it!Most operating systems execute a JAR when users double-click on it, but you can also create a launcher.
jMonkeyPlatform allows you to create launchers for different desktop platforms, like an .exe file for Windows systems, an Application for MaxOSX and a launcher for Linux systems.
When you build your project, zip files for each selected platform will be created in the dist
folder that contain all that is needed to run your application on that platform.
Web Start allows your users to start your application by simply clicking a link that you provide, for example in a button on your web page. The browser downloads the JAR file and then automatically runs your game in an extra window. The only requirement is that the user's browser has the Java plugin installed. This is a very user-friendly way for your customers to play your game without any extra steps to install it. Optionally, you can set it up so the file is saved to their desktop and can be restarted later, so they do not need to be online to play.
Application Descriptor
is activated. Click OK.dist
directory is generated.dist
directory to a public http serverLook at the sample launch.html, you can have any custom content around the link. Keep a copy of your launcher file because the jMonkeyPlatform will always regenerate its default launch.html.
Also, see this demo video on creating WebStarts.
A browser Applet is a Java application that runs in the web browser while the user is visiting your web page. The only requirement is that the user's browser has the Java plugin installed. There is no installation step, the user can play right away in the browser. The user will not be able to save the game to his harddrive, nor can he play offline.
These instrcutions assume that you have already written a game that you want to turn into an Applet. As opposed to other jME3 games, Applets cannot capture the mouse for navigation, so the camera will be switched to dragToRotate mode. The jMonkeyPlatform and the included build script already contain what you need.
The dist/Applet
directory now contains all the files necessary for the game to run as Applet. To test the Applet-based game, run the project in the jMonkeyPlatform.
dist/Applet/run-applet.html
file in anyway you like. Just keep the Applet code.dist/Applet
directory to a public http server.TODO
The jMonkeyPlatform has a Run Configuration menu in the toolbar. Use it to save your various sets of Project Property configuations, and switch between them.
Set Project Configuration
popup in the toolbar and choose Customize.Set Project Configuration
popup above the editor. Now you can use the Set Project Configuration
popup menu to switch between your run/build configurations.
There may be several parts of the full jMonkeyEngine library that you do not even use in your application. You should leave out the corresponding libraries from your distribution.
To remove unused libraries:
+ +After you have written and tested your game, you want to brand it and distribute it to your users. If you use the build script provided by the jMonkeyPlatform's BaseGame, you have the following deployment options: +
++ +Since JAR files are platform independent, your customers can play your jMonkeyEngine application on Windows, Mac OS, or Linux. The only requirement is that the user has the free Java 5 or 6 Runtime (or browser plugin) installed. For more information see . +
+ ++ + +Make your game unique and recognizable: +
++Your executables are now branded. +
+ +
+
+
+When you run the build script provided by the jMonkeyPlatform, it automatically compiles your classes, libraries, and assets. It creates a dist
directory in your project directory which contains the executable JAR and a directory with libraries.
+
+In the simplest case, you zip up the dist
directory and distribute it to your customers. Companies often have additional tools to create executables and installers.
+
+Here are your deployment options in detail: +
+ ++ +The JAR file is the most common deployment method for Java desktop applications. The user downloads the executable JAR file to his machine and runs it to start the game. +
+Building jar: /home/joe/jMonkeyPlatform/MySuperTestGame/dist/MySuperTestGame.jar
dist
directory and distribute it to your users. Make sure to keep the lib
directory in it!+Most operating systems execute a JAR when users double-click on it, but you can also create a launcher. +
+ ++ +jMonkeyPlatform allows you to create launchers for different desktop platforms, like an .exe file for Windows systems, an Application for MaxOSX and a launcher for Linux systems. +
+
+When you build your project, zip files for each selected platform will be created in the dist
folder that contain all that is needed to run your application on that platform.
+
+ +Web Start allows your users to start your application by simply clicking a link that you provide, for example in a button on your web page. The browser downloads the JAR file and then automatically runs your game in an extra window. The only requirement is that the user's browser has the Java plugin installed. This is a very user-friendly way for your customers to play your game without any extra steps to install it. Optionally, you can set it up so the file is saved to their desktop and can be restarted later, so they do not need to be online to play. +
+Application Descriptor
is activated. Click OK.dist
directory is generated.dist
directory to a public http server+Look at the sample launch.html, you can have any custom content around the link. Keep a copy of your launcher file because the jMonkeyPlatform will always regenerate its default launch.html. +Also, see this on creating WebStarts. +
+ ++ +A browser Applet is a Java application that runs in the web browser while the user is visiting your web page. The only requirement is that the user's browser has the Java plugin installed. There is no installation step, the user can play right away in the browser. The user will not be able to save the game to his harddrive, nor can he play offline. +
+ ++These instrcutions assume that you have already written a game that you want to turn into an Applet. As opposed to other jME3 games, Applets cannot capture the mouse for navigation, so the camera will be switched to dragToRotate mode. The jMonkeyPlatform and the included build script already contain what you need. + +
+ +
+The dist/Applet
directory now contains all the files necessary for the game to run as Applet. To test the Applet-based game, run the project in the jMonkeyPlatform.
+
+
dist/Applet/run-applet.html
file in anyway you like. Just keep the Applet code.dist/Applet
directory to a public http server.+ +You can set the jMonkeyPlatform to build an executable for Android mobile platforms. Learn more about Android Support here. +
+ ++The jMonkeyPlatform has a Run Configuration menu in the toolbar. Use it to save your various sets of Project Property configuations, and switch between them. +
+Set Project Configuration
popup in the toolbar and choose Customize.Set Project Configuration
popup above the editor.
+Now you can use the Set Project Configuration
popup menu to switch between your run/build configurations.
+
+ +There may be several parts of the full jMonkeyEngine library that you do not even use in your application. You should leave out the corresponding libraries from your distribution. +
+ ++To remove unused libraries: +
+ AssetPacks are a way to package jME3 compatible assets like models, textures, sounds and whole scenes into a package that contains publisher info, license info, descriptions etc. for all of the assets. An AssetPack basically consists of an assetpack.xml
file that describes the content and an assets
folder that contains the content. The integrated browser in jMP allows you to add the assets of installed AssetPacks to any project you are doing.
The AssetPack browser in jMP makes browsing the installed AssetPacks easy. Browse categories, search for tags and find the right asset for your project. When you have found it, you can add it with one click to your current scene. The AssetPack manager will automagically copy all needed textures, sounds etc. to your projects assets folder.
You can also browse a selection of online assetpacks that are available on jMonkeyEngine.org for download and install them to your jMonkeyPlatforms AssetPack browser.
The AssetPack browser uses a predefined directory to store the AssetPacks which is also used for new AssetPack projects. You can see and change the folder path in the AssetPack preferences (jMonkeyPlatform→Settings).
To preview a model from the browser, right-click it and select "Preview Asset"
To add a model from the AssetPack browser to a scene do the following:
The model will be added to your scene and all needed texture files will be copied to your projects assets folder.
AssetPack projects are a way to create your own AssetPacks, either for personal use or for publishing. With this project type in jMonkeyPlatform you can create a library of assets on your computer that you can use in any of your projects via the AssetPack browser. -Editing of asset and project info, adding of new assets and setting their properties, everything you need to create and publish your AssetPacks is there.
You can access and change the project properties by right-clicking the project and selecting "Properties".
To add new assets to your AssetPack do the following:
The global asset type can be "model", "scene", "texture", "sound", "shader" or "other"
With the "model" or "scene" types, the AssetPack browser will try to load and add a model file from the selected assets when the user selects "Add to SceneComposer". Specify the "load this model with material" flag for the model file that should be loaded via the AssetManager, you can also specify multiple mesh or scene files to be loaded. All texture and other needed files will be copied to the users project folder.
On the "Add Files" page you define the path of the files in the AssetPack. The importer tries to generate a proper path from the info entered on the first page. Note that for j3o binary models, the texture paths have to be exactly like they were during conversion. The given paths will also be used when copying the data to the users "assets" folder.
With the "add files" button you can open a file browser to select files from your harddisk that will be copied into the assets/
folder of the AssetPack project. With the "add existing" button you can add a file thats already in your AssetPack assets folder to a new asset item. This way you can reuse e.g. textures for asset items or make items for an existing collection of asset files that you copied to the projects assets folder.
You can specify a specific material to be used for the single mesh/scene files, just select it in the dropdown below the mesh or scene file. If you don't select a material file here, the first found material file is used or none if none is found.
If the material file is an Ogre material file (.material) it will be used for loading an Ogre scene or mesh file. If it is a jMonkeyEngine3 material file (.j3m), it will be applied to the mesh regardless of model type. Note that for j3o models you don't need material files as the material is stored inside the j3o file.
In your AssetPack Project, if you right-click your asset and select "preview asset" you should be able to see your model. If you do, it should work for the user as well.
You can change the single assets properties in the properties window after you have added them. Just select an asset and open the properties window (Windows→Properties).
Supported formats for models are:
You can publish your AssetPacks either as a zip file or directly to jmonkeyengine.org, using your website user name and login. This means other jMonkeyPlatform users can download your AssetPacks and install them to their local database right off the AssetPack online packages browser.
To make sure you can upload, you have to be registered on jmonkeyengine.org and have to enter your login info in the AssetPack preferences (jMonkeyPlatform→Settings).
+AssetPacks are a way to package jME3 compatible assets like models, textures, sounds and whole scenes into a package that contains publisher info, license info, descriptions etc. for all of the assets. An AssetPack basically consists of an assetpack.xml
file that describes the content and an assets
folder that contains the content. The integrated browser in jMP allows you to add the assets of installed AssetPacks to any project you are doing.
+
+ + +
+ ++The AssetPack browser in jMP makes browsing the installed AssetPacks easy. Browse categories, search for tags and find the right asset for your project. When you have found it, you can add it with one click to your current scene. The AssetPack manager will automagically copy all needed textures, sounds etc. to your projects assets folder. +
+ ++You can also browse a selection of online assetpacks that are available on jMonkeyEngine.org for download and install them to your jMonkeyPlatforms AssetPack browser. +
+ ++The AssetPack browser uses a predefined directory to store the AssetPacks which is also used for new AssetPack projects. You can see and change the folder path in the AssetPack preferences (jMonkeyPlatform→Settings). +
+ ++To preview a model from the browser, right-click it and select "Preview Asset" +
+ ++To add a model from the AssetPack browser to a scene do the following: +
++The model will be added to your scene and all needed texture files will be copied to your projects assets folder. +
+ ++ +AssetPack projects are a way to create your own AssetPacks, either for personal use or for publishing. With this project type in jMonkeyPlatform you can create a library of assets on your computer that you can use in any of your projects via the AssetPack browser. +Editing of asset and project info, adding of new assets and setting their properties, everything you need to create and publish your AssetPacks is there. + +
++ +You can access and change the project properties by right-clicking the project and selecting "Properties". + +
+ ++ +
+ ++To add new assets to your AssetPack do the following: +
++ +The global asset type can be "model", "scene", "texture", "sound", "shader" or "other" +
+ ++With the "model" or "scene" types, the AssetPack browser will try to load and add a model file from the selected assets when the user selects "Add to SceneComposer". Specify the "load this model with material" flag for the model file that should be loaded via the AssetManager, you can also specify multiple mesh or scene files to be loaded. All texture and other needed files will be copied to the users project folder. +
+ ++On the "Add Files" page you define the path of the files in the AssetPack. The importer tries to generate a proper path from the info entered on the first page. Note that for j3o binary models, the texture paths have to be exactly like they were during conversion. The given paths will also be used when copying the data to the users "assets" folder. +
+ +
+With the "add files" button you can open a file browser to select files from your harddisk that will be copied into the assets/
folder of the AssetPack project. With the "add existing" button you can add a file thats already in your AssetPack assets folder to a new asset item. This way you can reuse e.g. textures for asset items or make items for an existing collection of asset files that you copied to the projects assets folder.
+
+ +
+ ++You can specify a specific material to be used for the single mesh/scene files, just select it in the dropdown below the mesh or scene file. If you don't select a material file here, the first found material file is used or none if none is found. +
+ ++If the material file is an Ogre material file (.material) it will be used for loading an Ogre scene or mesh file. If it is a jMonkeyEngine3 material file (.j3m), it will be applied to the mesh regardless of model type. Note that for j3o models you don't need material files as the material is stored inside the j3o file. +
+ ++In your AssetPack Project, if you right-click your asset and select "preview asset" you should be able to see your model. If you do, it should work for the user as well. +
+ ++You can change the single assets properties in the properties window after you have added them. Just select an asset and open the properties window (Windows→Properties). +
+ ++Supported formats for models are: +
++ + +
+ ++You can publish your AssetPacks either as a zip file or directly to jmonkeyengine.org, using your website user name and login. This means other jMonkeyPlatform users can download your AssetPacks and install them to their local database right off the AssetPack online packages browser. +
+ ++To make sure you can upload, you have to be registered on jmonkeyengine.org and have to enter your login info in the AssetPack preferences (jMonkeyPlatform→Settings). + +
++Importing models to any game engine is as important as using them. The quality of the models depends on the abilities of the people who create it and on the tools they use. +Blender is one of the best free tools for creating 3D enviroments. Its high amount of features attract many model designers. +So far jMonkeyEngine used Ogre mesh files to import 3D data. These files were created by the python script that exported data from blender. +It was important to have always the lates version of the script that is compatible with the version of blender and to use it before importing data to jme. +Now we have an opportunity to simplify the import process by loading data directly from blender binary files: *.blend. +
+ ++To use it in your game or the SDK you should follow the standard asset loading instructions. +By default a BlenderModelLoader is registered with your assetManager to load blend files. This means you can load and convert .blend model files to .j3o format, just like any other supported model format. +
+ ++I'll fix these when I fix more important stuff ;) +
+ ++You have two loaders available. + +
+public static class LoadingResults extends Spatial { + /** Bitwise mask of features that are to be loaded. */ + private final int featuresToLoad; + /** The scenes from the file. */ + private List<Node> scenes; + /** Objects from all scenes. */ + private List<Node> objects; + /** Materials from all objects. */ + private List<Material> materials; + /** Textures from all objects. */ + private List<Texture> textures; + /** Animations of all objects. */ + private List<AnimData> animations; + /** All cameras from the file. */ + private List<Camera> cameras; + /** All lights from the file. */ + private List<Light> lights; + /** Access Methods goes here. */ +}+
+To register the model do the following: + +
+assetManager.registerLoader(BlenderLoader.class, "blend");+ +
+ +or + +
+assetManager.registerLoader(BlenderModelLoader.class, "blend");+
+You can use com.jme3.asset.BlenderKey for that. +The simplest use is to create the key with the asset's name. +It has many differens settings descibing the blender file more precisely, but all of them have default values so you do not need to worry about it at the beggining. +You can use ModelKey as well. This will give the same result as using default BlenderKey. +
+ ++BlenderLoader (as well as BlenderModelLoader) is looking for all kinds of known assets to load. +It's primary use is of course to load the models withon the files. +Each blender object is imported as scene Node. The node should have applied textures and materials as well. +If you define animations in your BlenderKey the animations will as well be imported and attached to the model. +
+ ++Here is the list of how blender features are mapped into jme. + +
++ +Using BlenderLoader can allow you to use blend file as your local assets repository. +You can store your textures, materials or meshes there and simply import it when needed. +Currently blender 2.49 and 2.5+ are supported (only the stable versions). +Probably versions before 2.49 will work pretty well too, but I never checked that :) +
+ ++I know that the current version of loader is not yet fully functional, but belive me – Blender is a very large issue ;) +Hope I will meet your expectations. +
+ ++Cheers, +Marcin Roguski (Kaelthas) +
+ ++P.S. +This text might be edited in a meantime if I forgot about something ;) + +
+ +The Source Code Editor is the central part of the jMonkeyPlatform. This documentation shows you how to make the most of the jMonkeyPlatform's assistive features.
Note: Since the jMonkeyPlatform is based on the NetBeans Platform framework, you can learn about certain jMonkeyPlatform features by reading the corresponding NetBeans IDE tutorials (in the "see also links").
While typing Java code in the source code editor, you will see popups that help you to write more quickly by completing keywords, and generating code snippets. Additionally, they will let you see the javadoc for the classes you are working with.
Code Completion
Code Generation
The text color in the editor gives you important hints how the compiler will interpret what you typed, even before you compiled it.
Examples:
To customize Colors and indentation:
Editor hints and quick fixes show as lightbulbs along the left edge of the editor. They point out warnings and errors, and often propose useful solutions!
/**
and press Enter: The editor generates skeleton code for a Javadoc comment.To display a javadoc popup in the editor, place the caret in a line and press Ctrl-Space (Alternatively use Ctrl-\).
When the JavaDoc does not deliver enough information, you can have a look at the source of every method or object of jME3 that you use. Just right-click the variable or method, select "Navigate > Go to source.." and an editor will open showing you the source file of jME3.
Choose Windows > Palette to open the context-sensitive Palette. The jMonkeyPlatform provides you with jme3 code snippets here that you can drag and drop into your source files.
Tip: Choose Tools > Add to Palette… from the menu to add your own code snippets to the Palette. (not available yet in alpha build)
Keyboard Shortcuts save you time when when you need to repeat common actions such as Build&Run or navigation to files.
By default, jMonkeyEngine uses the same Editor Shortcuts as the NetBeans IDE, but you can also switch to an Eclipse Keymap, or create your own set.
See also
+ +The Source Code Editor is the central part of the jMonkeyPlatform. This documentation shows you how to make the most of the jMonkeyPlatform's assistive features. +
+ ++Note: Since the jMonkeyPlatform is based on the NetBeans Platform framework, you can learn about certain jMonkeyPlatform features by reading the corresponding NetBeans IDE tutorials (in the "see also links"). +
+ ++ +While typing Java code in the source code editor, you will see popups that help you to write more quickly by completing keywords, and generating code snippets. Additionally, they will let you see the javadoc for the classes you are working with. +
+ ++ +
+ ++Code Completion +
++ +Code Generation +
++ + +
+ ++The text color in the editor gives you important hints how the compiler will interpret what you typed, even before you compiled it. +
+ ++Examples: +
++ +To customize Colors and indentation: +
++ +Editor hints and quick fixes show as lightbulbs along the left edge of the editor. They point out warnings and errors, and often propose useful solutions! + +
+/**
and press Enter: The editor generates skeleton code for a Javadoc comment. + +To display a javadoc popup in the editor, place the caret in a line and press Ctrl-Space (Alternatively use Ctrl-\). + +
++ +When the JavaDoc does not deliver enough information, you can have a look at the source of every method or object of jME3 that you use. Just right-click the variable or method, select "Navigate > Go to source.." and an editor will open showing you the source file of jME3. +
+ ++ + +
+ ++Choose Windows > Palette to open the context-sensitive Palette. The jMonkeyPlatform provides you with jme3 code snippets here that you can drag and drop into your source files. +
++ +Tip: Choose Tools > Add to Palette… from the menu to add your own code snippets to the Palette. (not available yet in alpha build) +
+ ++ +Keyboard Shortcuts save you time when when you need to repeat common actions such as Build&Run or navigation to files. +
++ +By default, jMonkeyEngine uses the same as the NetBeans IDE, but you can also switch to an Eclipse Keymap, or create your own set. +
++See also + +
+Debugging, testing and profiling are important parts of the development cycle. This documentation shows you how to make the most of the jMonkeyPlatform's assistive features.
Note: Since the jMonkeyPlatform is based on the NetBeans Platform framework, you can learn about certain jMonkeyPlatform features by reading the corresponding NetBeans IDE tutorials (in the "see also links").
The jMonkeyPlatform supports the JUnit testing framework. It is a good practice to write tests (assertions) for each of your classes. Each test makes certain this "unit" (e.g. method) meets its design and behaves as intended. Run your tests after each major change and you immediately see if you broke something.
assertTrue(), assertFalse(), assertEquals()
, or assert()
.assert( add(1, 1) == 2); assert( add(2,-5) == -3); …
Tip: Use the Navigate menu to jump from a test to its tested class, and back.
Using unit tests regularly allows you to detect side-effects on classes that you thought were unaffected by a code change.
See also:
In the jMonkeyPlatform, you have access to a debugger to examine your application for errors such as deadlocks and NullPointerExeptions. You can set breakpoints, watch variables, and execute your code line-by-line to identify the source of a problem.
The profiler tool is used to monitor thread states, CPU performance, and memory usage of your jme3 application. It helps you detect memory leaks and bottlenecks in your game while it's running.
If you do not see a Profiler menu in the jMonkeyPlatform, you need to download the Profiler plugin first.
Click the Take Snapshot button to capture the profiling data for later!
Profiling points are similar to debugger breakpoints: You place them directly in the source code and they can trigger profiling behaviour when hit.
See also:
+ +Debugging, testing and profiling are important parts of the development cycle. This documentation shows you how to make the most of the jMonkeyPlatform's assistive features. +
+ ++Note: Since the jMonkeyPlatform is based on the NetBeans Platform framework, you can learn about certain jMonkeyPlatform features by reading the corresponding NetBeans IDE tutorials (in the "see also links"). +
+ ++ +The jMonkeyPlatform supports the JUnit testing framework. It is a good practice to write tests (assertions) for each of your classes. Each test makes certain this "unit" (e.g. method) meets its design and behaves as intended. Run your tests after each major change and you immediately see if you broke something. +
+ +assertTrue(), assertFalse(), assertEquals()
, or assert()
.assert( add(1, 1) == 2); assertTrue( add(7,-5) == add(-5,7) )…
+ +Tip: Use the Navigate menu to jump from a test to its tested class, and back. +
+ ++ +Using unit tests regularly allows you to detect side-effects on classes that you thought were unaffected by a code change. +
+ ++See also: +
++ +In the jMonkeyPlatform, you have access to a debugger to examine your application for errors such as deadlocks and NullPointerExeptions. You can set breakpoints, watch variables, and execute your code line-by-line to identify the source of a problem. + +
++ +The profiler tool is used to monitor thread states, CPU performance, and memory usage of your jme3 application. It helps you detect memory leaks and bottlenecks in your game while it's running. +
+ ++ +If you do not see a Profiler menu in the jMonkeyPlatform, you need to download the Profiler plugin first. +
++ +Click the Take Snapshot button to capture the profiling data for later! +
++ +Profiling points are similar to debugger breakpoints: You place them directly in the source code and they can trigger profiling behaviour when hit. +
++ +See also: +
+If you use jME3 together with the jMonkeyPlatform (recommended) then you benefit from the provided build script. Every new project comes with a default Ant script. The toolbar buttons and clean/build/run actions in the jMonkeyPlatform are already pre-configured.
The build script includes targets for the following tasks:
Usage on the command line:
ant clean + ++ +Default Build Script
++ ++ ++ +If you use jME3 together with the jMonkeyPlatform (recommended) then you benefit from the provided build script. Every new project comes with a default Ant script. The toolbar buttons and clean/build/run actions in the jMonkeyPlatform are already pre-configured. +
+ +Default Targets
++ ++ +The build script includes targets for the following tasks: + +
++
+ +- +
clean – deletes the built classes and executables in the dist directory.+- +
jar – compiles the classes, builds the executable JAR in the dist directory, copies libraries.+- +
run – runs the executable JAR.+- +
javadoc – compiles javadoc from your java files into html files.+- +
debug – used by the jMonkeyPlatform Debugger plugin.+- +
test – Used by the jMonkeyPlatform JUnit Test plugin.++ +Usage on the command line: + +
+ant clean ant jar -ant runRecommended Usage: Use the menu items or toolbar buttons in the jMonkeyPlatform to trigger clean, build, and run actions.
Browsing the Build Script
To see the build script and the predefined tasks
Open your project in the jMonkeyPlatform if it isn't already open (File > Open Project…) Open the Files window (Window > Files) Open the project node. You seebuild.xml
listed.
Double-clickbuild.xml
to see how the jme3-specify build targets were defined. You typically do not need to edit the existing ones, but you can. Click the triangle next tobuild.xml
to see all targets.
Double-click a target in the Files window, or the Navigator, to see how the target was defined.
You will notice that the filenbproject/build-impl.xml
opens. It contains very generic targets that you typically will never need to edit. Note thatbuild.xml
includesbuild-impl.xml
!Adding Custom Targets
- \ No newline at end of file +ant runThe build script is a non-proprietary Apache Ant script. It will work out-of-the-box, but if necessary, you can extend and customize it.
Read the comments in
build.xml
, they explain how to override targets, or extend them, to customize the build process without breaking the existing functionality.
+Recommended Usage: Use the menu items or toolbar buttons in the jMonkeyPlatform to trigger clean, build, and run actions. +
+ ++ +To see the build script and the predefined tasks +
+build.xml
listed.build.xml
to see how the jme3-specify build targets were defined. You typically do not need to edit the existing ones, but you can.build.xml
to see all targets.nbproject/build-impl.xml
opens. It contains very generic targets that you typically will never need to edit. Note that build.xml
includes build-impl.xml
!+ +The build script is a non-proprietary Apache Ant script. It will work out-of-the-box, but if necessary, you can extend and customize it. +
+ +
+Read the comments in build.xml
, they explain how to override targets, or extend them, to customize the build process without breaking the existing functionality.
+
+
Note that all info is subject to change while jMP is still in alpha! In general, developing plugins for jMonkeyPlatform is not much different than creating plugins for the NetBeans Platform which in turn is not much different than creating Swing applications. You can use jMonkeyPlatform to develop plugins, be it for personal use or to contribute to the community. -If you feel like you want to make an addition to jMonkeyPlatform dont hesitate to contact the jme team regardless of your knowledge in NetBeans platform development. For new plugins, the basic project creation and layout of the plugin can always be handled by a core developer and you can go on from there fleshing out the plugin. -By using the Platform functions, your plugin feels more like a Platform application (global save button, file type support etc.).
Learn more about NetBeans Plugin Development at http://platform.netbeans.org .\\
Also check out this Essential NetBeans Platform Refcard: http://refcardz.dzone.com/refcardz/essential-netbeans-platform
+Note that all info is subject to change while jMP is still in alpha! +
+ ++In general, developing plugins for jMonkeyPlatform is not much different than creating plugins for the NetBeans Platform which in turn is not much different than creating Swing applications. You can use jMonkeyPlatform to develop plugins, be it for personal use or to contribute to the community. +
+ ++If you feel like you want to make an addition to jMonkeyPlatform dont hesitate to contact the jme team regardless of your knowledge in NetBeans platform development. For new plugins, the basic project creation and layout of the plugin can always be handled by a core developer and you can go on from there fleshing out the plugin. By using the Platform functions, your plugin feels more like a Platform application (global save button, file type support etc.). +
+ ++ +Learn more about NetBeans Plugin Development at +
+ ++Also check out this Essential NetBeans Platform Refcard: +
+ +This page describes how you can wrap any jar library into a plugin that a jMP user can download, install and then use the contained library in his own game projects.
Creating the plugin project (in jMP):
my-library
com.mycompany.plugins.mylibrary
Adding the library:
You will notice a new file "MyLibrary.xml" is created in the plugins base package and linked to in the layer.xml file. This is basically it, you can configure a version number, license file (should be placed in Module root folder) and more via the Module Properties.
After you are done, you can contribute the plugin in the jMP contribution update center.
+This page describes how you can wrap any jar library into a plugin that a jMP user can download, install and then use the contained library in his own game projects. +
+ +
+
+
+Creating the plugin project (in jMP):
+
my-library
com.mycompany.plugins.mylibrary
+ +Adding the library: +
++ +You will notice a new file "MyLibrary.xml" is created in the plugins base package and linked to in the layer.xml file. This is basically it, you can configure a version number, license file (should be placed in Module root folder) and more via the Module Properties. +
+ +
+
+
+After you are done, you can contribute the plugin in the jMP contribution update center.
+
+
For the most common extensions like Windows, File Types, Libraries etc. there exist templates that you can add to your plugin.
You will see a list of components you can add to your project. A wizard will guide you through the creation of the component.
+For the most common extensions like Windows, File Types, Libraries etc. there exist templates that you can add to your plugin. + +
++ +You will see a list of components you can add to your project. A wizard will guide you through the creation of the component. +
+ +See also:
+See also: +
+The SDK heavily uses the systems provided by the base platform for the handling of assets and projects and extends the system with jME3 specific features.
All AssetDataObjects and SceneExplorerNodes allow access to the ProjectAssetManager of the project they were loaded from.
ProjectAssetManager pm = node.getLookup().lookup(ProjectAssetManager.class)
The ProjectAssetManager is basically a normal DesktopAssetManager for each project with some added functionality:
Most "files" that you encounter in the SDK come in the form of AssetDataObjects. All Nodes that you encounter contain the AssetDataObject they were loaded from. It provides not just access to the FileObject of the specific file but also an AssetData object that allows access to jME specific properties and data. The AssetData object also allows loading the object via the jME3 assetManager. It is accessible via the lookup of the Node or AssetDataObject:
assetDataObject.getLookup().lookup(AssetData.class)
When you add a new file type for a model format or other asset file that can be loaded in jME3 you can start by using new file type template (New File→Module Development→File Type). Change the DataObject to extend AssetDataObject (general), SpatialAssetDataObject (some type of model) or BinaryModelDataObject (basically a j3o savable file). And possibly override the loadAsset and saveAsset methods which are used by the AssetData object.
public class BlenderDataObject extends SpatialAssetDataObject { + ++ +Projects and Assets
++ ++ ++The SDK heavily uses the systems provided by the base platform for the handling of assets and projects and extends the system with jME3 specific features. + +
+ +ProjectAssetManager
++ ++ ++All AssetDataObjects and SceneExplorerNodes allow access to the ProjectAssetManager of the project they were loaded from. + +
+ProjectAssetManager pm = node.getLookup().lookup(ProjectAssetManager.class)+ ++ +The ProjectAssetManager is basically a normal DesktopAssetManager for each project with some added functionality: +
++
+ +- +
Access to the FileObject of the assets folder of the project to load and save data+- +
Convert absolute file paths to relative asset paths and vice versa+- +
Get lists of all textures, materials etc. in the project+- +
more convenient stuff.. :)+AssetDataObject
++ ++ ++Most "files" that you encounter in the SDK come in the form of AssetDataObjects. All Nodes that you encounter contain the AssetDataObject they were loaded from. It provides not just access to the FileObject of the specific file but also an AssetData object that allows access to jME specific properties and data. The AssetData object also allows loading the object via the jME3 assetManager. It is accessible via the lookup of the Node or AssetDataObject: + +
+assetDataObject.getLookup().lookup(AssetData.class)+ +New Asset File Types
++ +- \ No newline at end of file +}+When you add a new file type for a model format or other asset file that can be loaded in jME3 you can start by using new file type template (New File→Module Development→File Type). Change the DataObject to extend AssetDataObject (general), SpatialAssetDataObject (some type of model) or BinaryModelDataObject (basically a j3o savable file). And possibly override the loadAsset and saveAsset methods which are used by the AssetData object. + +
+public class BlenderDataObject extends SpatialAssetDataObject { public BlenderDataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException { super(pf, loader); } -}An AssetManagerConfigurator class can be created to configure the assetManager of the projects and model importer to use the new asset type:
@org.openide.util.lookup.ServiceProvider(service = AssetManagerConfigurator.class) +}+ ++ +An AssetManagerConfigurator class can be created to configure the assetManager of the projects and model importer to use the new asset type: + +
+@org.openide.util.lookup.ServiceProvider(service = AssetManagerConfigurator.class) public class BlenderAssetManagerConfigurator implements AssetManagerConfigurator { public void prepareManager(AssetManager manager) { manager.registerLoader(com.jme3.scene.plugins.blender.BlenderModelLoader.class, "blend"); } -}
To reduce system overhead the jMonkeyPlatform Core supplies one scene/jme3 application that is shared between plugins. Furthermore there's the "SceneExplorer" that shows a visual representation of the scenegraph and its objects properties across plugins.
Note: All info due to change until jMP is stable!
There are several ways for your plugin to interact with the Scene:
In jMP, all objects are wrapped into NetBeans "Nodes" (different thing than jme Nodes!). Such nodes can have properties and icons and can be displayed and selected in the jMP UI. The SceneExplorer shows a tree of Nodes that wrap the Spatials of the current scene and allows manipulating their properties on selection. A jME "Spatial" is wrapped by a "JmeSpatial" node, for example. One advantage of these Nodes is that one can manipulate properties of Spatials directly from the AWT thread.
To listen to the current selection, implement org.openide.util.LookupListener and register like this:
private final Result<JmeSpatial> result; + ++ +The Scene
++ ++ ++ +To reduce system overhead the jMonkeyPlatform Core supplies one scene/jme3 application that is shared between plugins. Furthermore there's the "SceneExplorer" that shows a visual representation of the scenegraph and its objects properties across plugins. +
+ ++
+ +
+ +Note: All info due to change until jMP is stable! +How to access the Scene
++ ++ ++ +There are several ways for your plugin to interact with the Scene: +
++
+ +- +
It listens for selected spatials / objects and offers options for those+- +
It requests the whole scene for itself and loads/arranges the content in it (e.g. a terrain editor or model animation plugin).+Listening for Node selection
++ ++ +In jMP, all objects are wrapped into NetBeans "Nodes" (different thing than jme Nodes!). Such nodes can have properties and icons and can be displayed and selected in the jMP UI. The SceneExplorer shows a tree of Nodes that wrap the Spatials of the current scene and allows manipulating their properties on selection. A jME "Spatial" is wrapped by a "JmeSpatial" node, for example. One advantage of these Nodes is that one can manipulate properties of Spatials directly from the AWT thread. +
+ ++
+
+ +To listen to the current selection, implement org.openide.util.LookupListener and register like this: +private final Result<JmeSpatial> result; //method to register the listener; private void registerListener(){ @@ -24,7 +60,12 @@ public void resultChanged(LookupEvent ev) { spatial.getPropertySets()[0].setValue("Local Translation", Vector3f.ZERO); return; } -}You can also access the "real" spatial but since its part of the scenegraph you will have to modify it on that thread:
//retrieve the "real" spatial class from the JmeNode +}+ ++You can also access the "real" spatial but since its part of the scenegraph you will have to modify it on that thread: +
+//retrieve the "real" spatial class from the JmeNode for (JmeSpatial jmeSpatial : items) { //the spatial is stored inside the JmeSpatials "Lookup", a general container for Objects final Spatial realSpatial = jmeSpatial.getLookup().lookup(Spatial.class); @@ -36,9 +77,24 @@ for (JmeSpatial jmeSpatial : items) { } }); return; -}Requesting the Scene
+ +If your plugin wants to use the scene by itself, it first has to implement SceneListener and register at the scene and then send a SceneRequest to the SceneApplication. When the SceneRequest has been approved and the current Scene has been closed, the SceneListener (your class) is called with its own SceneRequest as a parameter. When another plugin sends a SceneRequest it is also reported to you and its a hint that your RootNode has been removed from the Scene and you are no longer in control of it. You could also hook into the SceneRequests of other plugins to see if/when they are activated to display add-on plugins for that plugin.
The SceneRequest object has to contain several things. A thing that you must supply is a jme "Node" wrapped into a "JmeNode" object. This is your rootNode that you use to display and build your scene. As soon as you control the scene, you will have to control the camera etc. yourself.com.jme3.scene.Node rootNode = new com.jme3.scene.Node("MyRootNode"); +}+ +Requesting the Scene
++ ++ +If your plugin wants to use the scene by itself, it first has to implement SceneListener and register at the scene and then send a SceneRequest to the SceneApplication. When the SceneRequest has been approved and the current Scene has been closed, the SceneListener (your class) is called with its own SceneRequest as a parameter. When another plugin sends a SceneRequest it is also reported to you and its a hint that your RootNode has been removed from the Scene and you are no longer in control of it. You could also hook into the SceneRequests of other plugins to see if/when they are activated to display add-on plugins for that plugin. +
+ ++
+
+ +The SceneRequest object has to contain several things. A thing that you must supply is a jme "Node" wrapped into a "JmeNode" object. This is your rootNode that you use to display and build your scene. As soon as you control the scene, you will have to control the camera etc. yourself. +com.jme3.scene.Node rootNode = new com.jme3.scene.Node("MyRootNode"); private void registerSceneListener(){ SceneApplication.getApplication().addSceneListener(this); @@ -69,31 +125,53 @@ public boolean sceneClose(SceneRequest request) { } } return true; -}Undo/Redo support
+ +jMP has a global undo/redo queue that activates the undo/redo buttons. To use it in your TopComponent, add the following method:
@Override -public UndoRedo getUndoRedo() { -return Lookup.getDefault().lookup(SceneUndoRedoManager.class); -}To add a undo/redo event that modifies objects on the Scenegraph, theres a special version of AbstractUndoableEdit which executes the undo/redo calls on the scene thread. Simply implement that class and add it to the queue like this:
Lookup.getDefault().lookup(SceneUndoRedoManager.class).addEdit(this, new AbstractUndoableSceneEdit() { +}+ +Undo/Redo support
++ +- \ No newline at end of file +} +});+jMP has a global undo/redo queue that activates the undo/redo buttons. To use it in your TopComponent, add the following method: + +
+@Override +public UndoRedo getUndoRedo() { +return Lookup.getDefault().lookup(SceneUndoRedoManager.class); +}+ ++To add a undo/redo event that modifies objects on the Scenegraph, theres a special version of AbstractUndoableEdit which executes the undo/redo calls on the scene thread. Simply implement that class and add it to the queue like this: + +
+Lookup.getDefault().lookup(SceneUndoRedoManager.class).addEdit(this, new AbstractUndoableSceneEdit() { -@Override -public void sceneUndo() { +@Override +public void sceneUndo() { //undo stuff in scene here -} +} -@Override -public void sceneRedo() { +@Override +public void sceneRedo() { //redo stuff in scene here -} +} -@Override -public void awtUndo() { +@Override +public void awtUndo() { //undo stuff on awt thread here (updating of visual nodes etc, called post scene edit) -} +} -@Override -public void awtRedo() { +@Override +public void awtRedo() { //redo stuff on awt thread here -} -});Note: Its important that you use the method addEdit(Object source, UndoableEdit edit);
+Note: Its important that you use the method addEdit(Object source, UndoableEdit edit); + +
+ +If your plugin brings in its own SceneGraph objects you can still have them work like any other SceneExplorer item, including its special properties.
You will have to create your own class that extends org.openide.nodes.Node and implement the interface com.jme3.gde.core.sceneexplorer.nodes.SceneExplorerNode. Then you register that class by adding
@org.openide.util.lookup.ServiceProvider(service=SceneExplorerNode.class)
above the body of your class. Thats all, your Spatial type will automatically be used and displayed in the SceneExplorer. Make sure you register a jar with the used classes in the plugin preferences under "wrapped libraries", otherwise the IDE cannot access those classes.
Theres also AbstractSceneExplorerNode which brings some other useful features you might want to include like automatic creation of properly threaded properties etc. JmeSpatial for example bases on it. A simple SceneExplorerNode example for an object extending Spatial would be JmeGeometry (see below). Editors for special variable types can be added using the SceneExplorerPropertyEditor interface, which can be registered as a ServiceProvider as well.
The SceneExplorerNode can be used for Spatial and Control type objects.
@org.openide.util.lookup.ServiceProvider(service=SceneExplorerNode.class) + ++ +The SceneExplorer
++ ++ +Adding Node types to SceneExplorer
++ ++ ++ +If your plugin brings in its own SceneGraph objects you can still have them work like any other SceneExplorer item, including its special properties. +
+ ++You will have to create your own class that extends org.openide.nodes.Node and implement the interface com.jme3.gde.core.sceneexplorer.nodes.SceneExplorerNode. Then you register that class by adding +
+@org.openide.util.lookup.ServiceProvider(service=SceneExplorerNode.class)+ ++ above the body of your class. Thats all, your Spatial type will automatically be used and displayed in the SceneExplorer. Make sure you register a jar with the used classes in the plugin preferences under "wrapped libraries", otherwise the IDE cannot access those classes. +
+ ++Theres also AbstractSceneExplorerNode which brings some other useful features you might want to include like automatic creation of properly threaded properties etc. JmeSpatial for example bases on it. A simple SceneExplorerNode example for an object extending Spatial would be JmeGeometry (see below). Editors for special variable types can be added using the SceneExplorerPropertyEditor interface, which can be registered as a ServiceProvider as well. +
+ ++The SceneExplorerNode can be used for Spatial and Control type objects. + +
++
+ +- +
Add the "Nodes API" and "Lookup API" libraries to your project when you want to use this+Spatial Example
++@org.openide.util.lookup.ServiceProvider(service=SceneExplorerNode.class) public class JmeGeometry extends JmeSpatial { private static Image smallImage = @@ -68,9 +98,13 @@ public class JmeGeometry extends JmeSpatial { children.setReadOnly(readOnly); return new org.openide.nodes.Node[]{new JmeGeometry((Geometry) key, children).setReadOnly(readOnly)}; } -}Control Example
+ +@org.openide.util.lookup.ServiceProvider(service=SceneExplorerNode.class) +}+ +Control Example
++@org.openide.util.lookup.ServiceProvider(service=SceneExplorerNode.class) public class JmeGhostControl extends AbstractSceneExplorerNode { private static Image smallImage = @@ -166,10 +200,26 @@ public class JmeGhostControl extends AbstractSceneExplorerNode { public org.openide.nodes.Node[] createNodes(Object key, DataObject key2, boolean cookie) { return new org.openide.nodes.Node[]{new JmeGhostControl((GhostControl) key, key2).setReadOnly(cookie)}; } -}Adding items to the add and tools menus
+ +For adding Spatials, Contols and for general tools theres premade abstract classes that you can use to extend the options. Undo/Redo is handled by the abstract class. AbstractNewSpatialWizardAction allows you to show an AWT wizard before creating the Spatial. You can also just implement the base ServiceProvider class and return any kind of action (such as a wizard), in this case you have to handle the threading yourself!
Note that the classes you create are singletons which are used across multiple nodes and you should not store any data in local variables!To add a new Tool, create a new AbstractToolAction:
@org.openide.util.lookup.ServiceProvider(service = ToolAction.class) +}+ +Adding items to the add and tools menus
++ ++For adding Spatials, Contols and for general tools theres premade abstract classes that you can use to extend the options. Undo/Redo is handled by the abstract class. AbstractNewSpatialWizardAction allows you to show an AWT wizard before creating the Spatial. You can also just implement the base ServiceProvider class and return any kind of action (such as a wizard), in this case you have to handle the threading yourself! +
+ ++
Note that the classes you create are singletons which are used across multiple nodes and you should not store any data in local variables! ++ + ++To add a new Tool, create a new AbstractToolAction: +
+@org.openide.util.lookup.ServiceProvider(service = ToolAction.class) public class GenerateTangentsTool extends AbstractToolAction { public GenerateTangentsTool() { @@ -199,7 +249,12 @@ public class GenerateTangentsTool extends AbstractToolAction { return JmeGeometry.class; } -}For a new Spatial or Control, use AbstractNewSpatialAction
@org.openide.util.lookup.ServiceProvider(service = NewSpatialAction.class) +}+ ++For a new Spatial or Control, use AbstractNewSpatialAction +
+@org.openide.util.lookup.ServiceProvider(service = NewSpatialAction.class) public class NewSpecialSpatialAction extends AbstractNewSpatialAction { public NewSpecialSpatialAction() { @@ -211,7 +266,12 @@ public class NewSpecialSpatialAction extends AbstractNewSpatialAction { Spatial spatial=new Node(); return spatial; } -}or AbstractNewControlAction:
@org.openide.util.lookup.ServiceProvider(service = NewControlAction.class) +}+ ++or AbstractNewControlAction: +
+@org.openide.util.lookup.ServiceProvider(service = NewControlAction.class) public class NewRigidBodyAction extends AbstractNewControlAction { public NewRigidBodyAction() { @@ -232,10 +292,22 @@ public class NewRigidBodyAction extends AbstractNewControlAction { } return control; } -}Adding using a Wizard
+ +You can create a new wizard using the wizard template in the SDK (New File→Module Development→Wizard). The Action that the template creates can easily be changed to one for adding a Control or Spatial or for applying a Tool. Note that we extend AbstractNewSpatialWizardAction here.
A good example is the "Add SkyBox" Wizard:
@org.openide.util.lookup.ServiceProvider(service = NewSpatialAction.class) +}+ +Adding using a Wizard
++ +- \ No newline at end of file +}+You can create a new wizard using the wizard template in the SDK (New File→Module Development→Wizard). The Action that the template creates can easily be changed to one for adding a Control or Spatial or for applying a Tool. Note that we extend AbstractNewSpatialWizardAction here. +
+ ++A good example is the "Add SkyBox" Wizard: + +
+@org.openide.util.lookup.ServiceProvider(service = NewSpatialAction.class) public class AddSkyboxAction extends AbstractNewSpatialWizardAction { private WizardDescriptor.Panel[] panels; @@ -320,6 +392,13 @@ public class AddSkyboxAction extends AbstractNewSpatialWizardAction { } return panels; } -}The abstract spatial and control actions implement undo/redo automatically, for the ToolActions you have to implement it yourself.
+ +
Note that the creation of a Module Suite is only necessary if you want to upload your plugin to the contribution update center.
my-library
com.mycompany.plugins.mylibrary
my-library
com.mycompany.plugins.mylibrary
If you want your plugin to appear in the "jMP Contributions Update Center" so users can download, install and update it easily via the plugin manager, you can host your plugin in the contributions update center svn repository. The contributions update center is based on a googlecode project for contributed plugins which will be automatically compiled, version-labeled and added to the contributions update center like the core jMP plugins.
To add your plugin to the repository, do the following:
https://jmonkeyplatform-contributions.googlecode.com/svn/trunk
in the URL fieldtrunk/mypluginfolder
and enter an import messageAnd thats it, from now on each time you commit changes to your module it will be built and added to the contributions center automatically and the version number will be extended by the svn revision number (e.g. 0.8.1.1234)
+Note that the creation of a Module Suite is only necessary if you want to upload your plugin to the contribution update center. + +
+ +my-library
com.mycompany.plugins.mylibrary
my-library
com.mycompany.plugins.mylibrary
+If you want your plugin to appear in the "jMP Contributions Update Center" so users can download, install and update it easily via the plugin manager, you can host your plugin in the contributions update center svn repository. The contributions update center is based on a googlecode project for contributed plugins which will be automatically compiled, version-labeled and added to the contributions update center like the core jMP plugins. +
+ +
+
+
+To add your plugin to the repository, do the following:
+
in the URL fieldtrunk/mypluginfolder
and enter an import message+ +And thats it, from now on each time you commit changes to your module it will be built and added to the contributions center automatically and the version number will be extended by the svn revision number (e.g. 0.8.1.1234) + +
+ ++The SDK allows you to create combinations of filters and preview them on a loaded scene. +
+ ++ +To create a new filter: +
++To see a loaded filter, you have to load some model or scene and press the "show filter" button in the OpenGL window. + +
+ ++You can add a new filter by right-clicking the filter root node. Select a filter to see its properties in the properties window. +
+ ++To load a filter in a game and add it to a viewport add the following to your games simpleInit() method or some other place: + +
+FilterPostProcessor processor = (FilterPostProcessor) assetManager.loadAsset("Filters/MyFilter.j3f"); +viewPort.addProcessor(processor);+ +
The jMonkeyEngine uses a special Material format, which comes in .j3m files. You use .j3m files to store sets of material properties that you use repeatedly. This enables you write one short line of code that simply loads the presets from a custom j3m file. Without a .j3m file you need to write several lines of material property setters every time when you want to use a non-default material.
To create new .j3m files,
assets/Materials
directory and choose New… > Other.mat_wall
for a wall material.mat_wall.j3m
is created in the Materials directory and opens in the Material Editor.You can edit the source of the material, or use the user-friendly visual editor to set the properties of the material. Set the properties to the same values as you would otherwise specify with setters on a Material object in Java code:
Material mat_wall = new Material( + ++ +The Material Editor
++ ++ +Materials
++ ++ ++ +The jMonkeyEngine uses a special Material format, which comes in .j3m files. You use .j3m files to store sets of material properties that you use repeatedly. This enables you write one short line of code that simply loads the presets from a custom j3m file. Without a .j3m file you need to write several lines of material property setters every time when you want to use a non-default material. + +
+ +Creating .j3m Materials
++ ++ +
+ ++To create new .j3m files, +
++
+ +- +
Right-click the+assets/Materials
directory and choose New… > Other.- +
In the New File Wizard, choose Material > Empty Material File, and click Next.+- +
Give the file a name, for example+mat_wall
for a wall material.- +
A new file+mat_wall.j3m
is created in the Materials directory and opens in the Material Editor.+ +You can edit the source of the material, or use the user-friendly visual editor to set the properties of the material. Set the properties to the same values as you would otherwise specify with setters on a Material object in Java code: +
+Material mat_wall = new Material( assetManager, "Common/MatDefs/Light/Lighting.j3md"); -mat_wall.setTexture("DiffuseMap", +mat_wall.setTexture("DiffuseMap", assetManager.loadTexture("Textures/wall.png")); -mat_wall.setTexture("NormalMap", +mat_wall.setTexture("NormalMap", assetManager.loadTexture("Textures/wall-normals.png")); -mat_wall.setFloat("Shininess", 5f);The source code of the j3m file is always accessible and can be modified in the "source" tab of the editor.
Using .j3m Materials
- \ No newline at end of file +mat_wall.setFloat("Shininess", 5f);When the material is ready and saved into your projects assets directory, you can assign the .j3m to a Geometry.
In the jMonkeyPlatform
Right-click the j3o file and select "Edit in SceneComposer" Open the SceneExplorer window In the SceneExplorer, click the geometry to which you want to assign the material. Open the Properties window Assign the .j3m material to the .j3o in the Properties>Geometry>Material section Save the j3o and load it into you game.Or in your Java code
Use a loader and a setter to assign the material to a Geometrymywall.setMaterial(assetManager.loadAsset( "Materials/mat_wall.j3m"));See also
Neotexture (Procedural textures)
+The source code of the j3m file is always accessible and can be modified in the "source" tab of the editor. +
+ ++ +
+ ++When the material is ready and saved into your projects assets directory, you can assign the .j3m to a Geometry. +
+ ++In the jMonkeyPlatform +
++ +Or in your Java code +
+mywall.setMaterial(assetManager.loadAsset( "Materials/mat_wall.j3m"));+
+See also +
+JMonkeyPlatform imports models from your project and stores them in the assets folder. The imported models are converted to a jME3 compatible binary format called .j3o. Double-click .j3o files in the jMonkeyPlatform to display them in the SceneViewer, or load them in-game using the AssetManager.
Presently, Blender 3D is the preferred modelling tool for jME3 as it is also Open-Source Software and an exporter for OgreXML files exists. Note that the OgreXML exporter is not yet compatible with Blender 2.5alpha!
Also, see this demo video on importing models.
jMonkeyPlatform includes a tool to install the correct exporter tools in Blender to export OgreXML files. To install the exporter, do the following:
jMonkeyPlatform includes a model importer tool that allows you to preview and import supported models into your jMonkeyEngine3 project.
The model is converted to j3o and all necessary files are copied to the project assets folder. If you have specified the corresponding option, all original model files will also be copied to your assets folder.
assets
folder of your project.asset
folder of your project,Note: It is important that you copy the model file and its textures to the correct assets folder before creating the j3o file because the paths for textures (and possibly other things) will be stored as absolute (to the assets folder root) when you convert that model. This means the texture location should not change after the import.
The original OgreXML .mesh.xml
, .scene
, .material
and .skeleton.xml
model files will not be included in the distribution assets.jar
file of your distributed game, they are only available in the assets folder so you are able to recreate the .j3o
file from the original if you ever come to change it in blender and have to export it again.
The SceneViewer and SceneExplorer windows are shared among plugins to save system resources. This means that you will have to keep an eye on what plugin is using the scene right now and what you are actually modifying in these windows.
Most plugins will deliver their own UI elements to modify the scene so the SceneExplorer is more of a global tool. The simple SceneComposer however heavily relies on its functions as other plugins might too in the future.
Each jMP project has its own internal AssetManager that has the projects assets folder registered as a FileLocator. When the project is run, the assets folder is compressed into a jar file and added to the projects main jar classpath. This way the editors in jMP and the running game have the same asset folder structure.
You might wonder why jMP requires you to copy the model that is to be converted to j3o into the assets folder before. The Model Import Tool also copies the model and associated files to the project directory before converting. To load the model it needs to be in a folder (or jar etc..) that belongs to the projects AssetManager root. To load a model from some other folder of the filesystem, that folder would have to be added to the AssetManager root. If every folder that contains a model was in the root of the AssetManager, all textures named "hull.jpg" for example would be the same for the AssetManager and it would only deliver the texture of the first model folder that was added.
To have a valid jME3 object, the paths to textures and other assets belonging to the model have to be read with the correct, final path that can then be stored in the j3o object. The j3o object will use those paths when it is loaded with the AssetManager and it requires the AssetManager to deliver the assets on those paths, this is why the folder structure while converting has to be the same as when loading.
+ +JMonkeyPlatform imports models from your project and stores them in the assets folder. The imported models are converted to a jME3 compatible binary format called .j3o. Double-click .j3o files in the jMonkeyPlatform to display them in the SceneViewer, or load them in-game using the AssetManager. +
+ ++Presently, is the preferred modelling tool for jME3 as it is also Open-Source Software and an exporter for OgreXML files exists. Note that the OgreXML exporter is not yet compatible with Blender 2.5alpha! +
+ ++Also, see this on importing models. +
+ ++ +jMonkeyPlatform includes a tool to install the correct exporter tools in Blender to export OgreXML files. To install the exporter, do the following: + +
++jMonkeyPlatform includes a model importer tool that allows you to preview and import supported models into your jMonkeyEngine3 project. +
+ ++ + + +
++ +The model is converted to j3o and all necessary files are copied to the project assets folder. If you have specified the corresponding option, all original model files will also be copied to your assets folder. +
+ ++ + +
+assets
folder of your project.asset
folder of your project, + +Note: It is important that you copy the model file and its textures to the correct assets folder before creating the j3o file because the paths for textures (and possibly other things) will be stored as absolute (to the assets folder root) when you convert that model. This means the texture location should not change after the import. +
+ +
+
+The original OgreXML .mesh.xml
, .scene
, .material
and .skeleton.xml
model files will not be included in the distribution assets.jar
file of your distributed game, they are only available in the assets folder so you are able to recreate the .j3o
file from the original if you ever come to change it in blender and have to export it again.
+
+ +The SceneViewer and SceneExplorer windows are shared among plugins to save system resources. This means that you will have to keep an eye on what plugin is using the scene right now and what you are actually modifying in these windows. +
+ ++Most plugins will deliver their own UI elements to modify the scene so the SceneExplorer is more of a global tool. The simple SceneComposer however heavily relies on its functions as other plugins might too in the future. +
+ ++Each jMP project has its own internal AssetManager that has the projects assets folder registered as a FileLocator. When the project is run, the assets folder is compressed into a jar file and added to the projects main jar classpath. This way the editors in jMP and the running game have the same asset folder structure. +
+ ++You might wonder why jMP requires you to copy the model that is to be converted to j3o into the assets folder before. The Model Import Tool also copies the model and associated files to the project directory before converting. To load the model it needs to be in a folder (or jar etc..) that belongs to the projects AssetManager root. To load a model from some other folder of the filesystem, that folder would have to be added to the AssetManager root. If every folder that contains a model was in the root of the AssetManager, all textures named "hull.jpg" for example would be the same for the AssetManager and it would only deliver the texture of the first model folder that was added. +
+ ++To have a valid jME3 object, the paths to textures and other assets belonging to the model have to be read with the correct, final path that can then be stored in the j3o object. The j3o object will use those paths when it is loaded with the AssetManager and it requires the AssetManager to deliver the assets on those paths, this is why the folder structure while converting has to be the same as when loading. + +
+ +The jMonkeyPlatform makes it easy to get started with developing 3-D games based on the jMonkeyEngine.
Let's have a look at the abstract project structure in the Project Explorer (ctrl-1).
Main.java
. Double click Main.java
to open it in the editor.Now let's have a look at the project's file structure in the File Explorer (ctrl-2). This explorer shows the physical directory structure on your hard drive.
assets/Interface
assets/MatDefs
assets/Materials
assets/Models
assets/Scenes
assets/Shaders
assets/Sounds
assets/Textures
Right-Click the project to open the Project properties.
BasicGame
).Add the library to the global library list:
Add the library to a project:
Thats it, your project can now use the external library.
FIXME
, @todo, TODO.More than one project open? The toolbar buttons and the F-keys are bound to the main project, which is shown in bold in the Project Explorer. Right-click a project and select Set As Main Project to make it respond to the toolbar buttons and F-keys.
Worried About Proprietary Lock-in? You are never locked into the jMonkeyPlatform: At any time, you can change into your project directory on the command line, and clean, build, and run your project, using non-proprietary Apache Ant commands:
ant clean; ant jar; ant run;
JME3Tests
project.JME3Tests
project and choose Run.JME3 Tests
template.+ +The jMonkeyPlatform makes it easy to get started with developing 3-D games based on the jMonkeyEngine. +
+ ++ + +
+ ++Let's have a look at the abstract project structure in the Project Explorer (ctrl-1). + +
+Main.java
. Double click Main.java
to open it in the editor.+ +Now let's have a look at the project's file structure in the File Explorer (ctrl-2). This explorer shows the physical directory structure on your hard drive. + +
+assets/Interface
assets/MatDefs
assets/Materials
assets/Models
assets/Scenes
assets/Shaders
assets/Sounds
assets/Textures
+ +Right-Click the project to open the Project properties. +
+BasicGame
).+ +Add the library to the global library list: +
++ +Add the library to a project: +
++ +Thats it, your project can now use the external library. +
+ +FIXME
, @todo, TODO.+ + + +
++ +More than one project open? The toolbar buttons and the F-keys are bound to the main project, which is shown in bold in the Project Explorer. Right-click a project and select Set As Main Project to make it respond to the toolbar buttons and F-keys. +
+ ++Worried About Proprietary Lock-in? You are never locked into the jMonkeyPlatform: At any time, you can change into your project directory on the command line, and clean, build, and run your project, using non-proprietary Apache Ant commands: +
+ant clean; ant jar; ant run;+ +
JME3Tests
project.JME3Tests
project and choose Run.JME3 Tests
template.SceneComposer allows you to edit scenes stored in j3o files and add content or modify existing content. Please note that the Scene Composer and Explorer are a work in progress and will provide more powerful functions in the future. Also other plugins will allow creation of more specific game type scenes in jMP.
Most buttons in the SceneComposer have tooltips that appear when you hover the mouse over the button for a short while.
In the SceneComposer toolbar are buttons to snap the camera to the cursor, snap the cursor to the selection and etc.
jMP stores the scene in a j3o file, this binary file contains the whole scenegraph including all settings for spatials, materials, physics, effects etc. Textures are not stored in the j3o file but as absolute locators to the textures.
To create a blank scene file do the following:
To open a scene
Now the SceneComposer window opens at the bottom and displays the scene in the SceneViewer. The SceneExplorer displays the contained scene graph as a tree and when selecting a node, you can edit the properties of the corresponding scene graph object in the Properties window.
For now, you only see the cursor in the SceneViewer and a single node (the root node of the scene) in the SceneExplorer.
A directional light has been added to your scene, you can see it in the SceneExplorer.
You can add a variety of special objects with the SceneComposer, including lights, effects, audio etc.
You can directly import 3d models to your scene so that they will be part of your scene file. To be able to import for example an OgreXML file, first export it from your 3D editor to a separate folder in the assets folder of your project (e.g. assets/Models/MyModel/).
Note that when importing a model the texture paths are stored absolute, so the folder you import the model from will later only be a textures folder because the original model file is not included in the release.
Also note that when adding models this way, changes in the original model file will not be reflected in the scene file as its a complete copy of the original file. If you change the original model, delete the models node from the scene and import it again.
You can also link models/objects into your scene, this way they are reloaded dynamically from the other/original file.
Note that when linking objects this way, you cannot edit them as part of the scene. To change the model you have to change the original j3o file.
Also note that although it its possible to directly link external model files (OgreXML, OBJ etc.), this is not recommended. Convert the original file to a j3o file by right-clicking it and selecting "Convert to jME Binary" before linking it. This is required because the original model files are not included in the release version of the application.
+ +SceneComposer allows you to edit scenes stored in j3o files and add content or modify existing content. Please note that the Scene Composer and Explorer are a work in progress and will provide more powerful functions in the future. Also other plugins will allow creation of more specific game type scenes in jMP. +
+ ++Most buttons in the SceneComposer have tooltips that appear when you hover the mouse over the button for a short while. +
+ ++ +In the SceneComposer toolbar are buttons to snap the camera to the cursor, snap the cursor to the selection and etc. +
+ ++ +jMP stores the scene in a j3o file, this binary file contains the whole scenegraph including all settings for spatials, materials, physics, effects etc. Textures are not stored in the j3o file but as absolute locators to the textures. +
+ ++To create a blank scene file do the following: +
++ + +
+ ++To open a scene +
++ +Now the SceneComposer window opens at the bottom and displays the scene in the SceneViewer. The SceneExplorer displays the contained scene graph as a tree and when selecting a node, you can edit the properties of the corresponding scene graph object in the Properties window. +
+ ++For now, you only see the cursor in the SceneViewer and a single node (the root node of the scene) in the SceneExplorer. +
+ ++ +A directional light has been added to your scene, you can see it in the SceneExplorer. +
+ ++ +You can add a variety of special objects with the SceneComposer, including lights, effects, audio etc. + +
++ + +
+ ++You can directly import 3d models to your scene so that they will be part of your scene file. To be able to import for example an OgreXML file, first export it from your 3D editor to a separate folder in the assets folder of your project (e.g. assets/Models/MyModel/). + +
++ +Note that when importing a model the texture paths are stored absolute, so the folder you import the model from will later only be a textures folder because the original model file is not included in the release. +
+ ++Also note that when adding models this way, changes in the original model file will not be reflected in the scene file as its a complete copy of the original file. If you change the original model, delete the models node from the scene and import it again. +
+ ++ +You can also link models/objects into your scene, this way they are reloaded dynamically from the other/original file. + +
++ +Note that when linking objects this way, you cannot edit them as part of the scene. To change the model you have to change the original j3o file. +
+ ++Also note that although it its possible to directly link external model files (OgreXML, OBJ etc.), this is not recommended. Convert the original file to a j3o file by right-clicking it and selecting "Convert to jME Binary" before linking it. This is required because the original model files are not included in the release version of the application. +
+ +The SceneExplorer gives you a structural overview of the currently edited scene and is active among all plugins
Most plugins will deliver their own UI elements to modify the scene so the SceneExplorer is more of a global tool. The simple SceneComposer however heavily relies on its functions as other plugins might too in the future.
The SceneExplorer displays Nodes in a tree that represents the tree of Spatials in your scene. Spatial controls, lights and geometry meshes are also displayed in the tree.
Right-click a Spatial or Node in the SceneExplorer to add other Spatials like ParticleEmitters or Lights, you can also add UserData to a Spatial that can be read during runtime.
+The SceneExplorer gives you a structural overview of the currently edited scene and is active among all plugins +
+ ++Most plugins will deliver their own UI elements to modify the scene so the SceneExplorer is more of a global tool. The simple SceneComposer however heavily relies on its functions as other plugins might too in the future. + +
+ ++ + +The SceneExplorer displays Nodes in a tree that represents the tree of Spatials in your scene. Spatial controls, lights and geometry meshes are also displayed in the tree. +
+ ++ +Right-click a Spatial or Node in the SceneExplorer to add other Spatials like ParticleEmitters or Lights, you can also add UserData to a Spatial that can be read during runtime. +
+ +The terrain lets you create, modify, and paint terrain.
Terrain controls are the same as the Scene Composer, you rotate the camera with the left mouse button and pan the camera with the right mouse button. Until you select one of the terrain tools in the toolbar, then the controls change for that tool. Then left mouse button will use that tool: painting, raising/lowering terrain, etc. The right mouse button might do something, depending on the tool.
In order to see the terrain, you will need to add light to your scene. To do this you will need to head to the Scene Composer:
Then, from the Palette list, select 'Directional Light'
Then, hit the 'add selected item' button.
Save your scene.
You now have light in your scene and will be able to see your terrain.
To create terrain, first select a node (probably your root node) in your scene.
Then click the add terrain button.
This will pop up the Create Terrain wizard that will walk you through the steps for creating terrain. Make sure you decide now how large you want your terrain to be and how detailed you want the textures to be as you cannot change it later on!
Here you determine the size of your terrain, the total size and the patch size. You should probably leave the patch size alone. But the total size should be defined; this is how big your terrain is.
Here you can select a heightmap generation technique to give you a default/random look of the terrain. You can also select a heightmap image to pre-define how your terrain looks. -By default, it will give you a flat terrain.
This step determines how large the alpha blend images are for your terrain. The smaller the alpha image, the less detailed you can paint the terrain. Play around with this to see how big you need it to be. Remember that terrain does not have to be that detailed, and is often covered by vegetation, rocks, and other items. So having a really detailed texture is not always necessary.
Right now there are two terrain modification tools: raise and lower terrain.
There are two sliders that affect how these tools operate:
Once a tool is selected, you will see the tool marker (now an ugly wire sphere) on the map where your mouse is. Click and drag on the terrain to see the tool change the height of the terrain.
Your terrain comes with one diffuse default texture, and you can have up to 12 total diffuse textures. It also allows you to add normal maps to each texture layer. + +
+The terrain editor lets you create, modify, and paint terrain. + +
+ ++Terrain controls are the same as the Scene Composer, you rotate the camera with the left mouse button and pan the camera with the right mouse button. Until you select one of the terrain tools in the toolbar, then the controls change for that tool. Then left mouse button will use that tool: painting, raising/lowering terrain, etc. The right mouse button might do something, depending on the tool. + +
+ +
+To create terrain, first select a node (probably your root node) in your scene.
+
+
+
+Then click the add terrain button.
+
+
+
+This will pop up the Create Terrain wizard that will walk you through the steps for creating terrain. Make sure you decide now how large you want your terrain to be and how detailed you want the textures to be as you cannot change it later on!
+
+In order to see the terrain, you will need to add light to your scene. To do this right-click the root node in the SceneExplorer window and select "Add Light→Directional Light" + +
+ ++Here you determine the size of your terrain, the total size and the patch size. You should probably leave the patch size alone. But the total size should be defined; this is how big your terrain is. + +
+ ++Here you can select a heightmap generation technique to give you a default/random look of the terrain. You can also select a heightmap image to pre-define how your terrain looks. +By default, it will give you a flat terrain. + +
+ ++This step determines how large the alpha blend images are for your terrain. The smaller the alpha image, the less detailed you can paint the terrain. Play around with this to see how big you need it to be. Remember that terrain does not have to be that detailed, and is often covered by vegetation, rocks, and other items. So having a really detailed texture is not always necessary. + +
+ +
+Right now there are two terrain modification tools: raise and lower terrain.
+
+
+
+There are two sliders that affect how these tools operate:
+
+Once a tool is selected, you will see the tool marker (now an ugly wire sphere) on the map where your mouse is. Click and drag on the terrain to see the tool change the height of the terrain. + +
+ ++Your terrain comes with one diffuse default texture, and you can have up to 12 total diffuse textures. It also allows you to add normal maps to each texture layer. There can be a maximum of 13 textures, including Diffuse and Normal (3 textures are taken up by alpha maps). All of the textures can be controlled in the Texture Layer table by clicking on the textures. -There are two sliders that affect how the paint tool operates:
Adds a new texture layer to the terrain. The will be drawn over top of the other texture layers listed above it in the list.
Click on the diffuse texture image in the texture table. This will pop up a window with the available textures in your assets directory.
When you add a texture layer, by default it does not add a normal map for you. To add one, click on the button next to the diffuse texture for that texture layer. This will pop up the texture browser. Select a texture and hit Ok, and you are done.
To remove a normal map from the texture layer, hit the normal map button for that texture layer again, and deselect the texture. Then hit Ok and the texture should be removed.
The field in the table to the right of the diffuse and normal textures for your texture layer is the scale. Changing that value changes the scale of the texture.
You will notice that the scale changes when you switch between Tri-Planar and normal texture mapping. Tri-planar mapping does not use the texture coordinates of the geometry, but real world coordinates. And because of this, in order for the texture to look the same when you switch between the two texture mapping methods, the terrain editor will automatically convert the scales for you.
-Essentially if your scale in normal texture coordinates is 16, then for tri-planar gets converted like this: 1/terrainSize/16
Tri-planar texture mapping is recommended if you have lots of near-vertical terrain. With normal texture mapping the textures can look stretched because it is rendered on the one plane: X-Z. Tri-planar mapping renders the textures on three planes: X-Z, X-Y, Z-Y; and blends them together based on what plane the normal of the triangle is facing most on. +There are two sliders that affect how the paint tool operates: +
+
+Adds a new texture layer to the terrain. The will be drawn over top of the other texture layers listed above it in the list.
+
+
+
+
+
+Click on the diffuse texture image in the texture table. This will pop up a window with the available textures in your assets directory.
+
+
+
+
+
+When you add a texture layer, by default it does not add a normal map for you. To add one, click on the button next to the diffuse texture for that texture layer. This will pop up the texture browser. Select a texture and hit Ok, and you are done.
+
+
+
+
+
+To remove a normal map from the texture layer, hit the normal map button for that texture layer again, and deselect the texture. Then hit Ok and the texture should be removed. + +
+ +
+The field in the table to the right of the diffuse and normal textures for your texture layer is the scale. Changing that value changes the scale of the texture.
+
+
+
+You will notice that the scale changes when you switch between Tri-Planar and normal texture mapping. Tri-planar mapping does not use the texture coordinates of the geometry, but real world coordinates. And because of this, in order for the texture to look the same when you switch between the two texture mapping methods, the terrain editor will automatically convert the scales for you.
+Essentially if your scale in normal texture coordinates is 16, then for tri-planar gets converted like this: 1/terrainSize/16
+
+
+Tri-planar texture mapping is recommended if you have lots of near-vertical terrain. With normal texture mapping the textures can look stretched because it is rendered on the one plane: X-Z. Tri-planar mapping renders the textures on three planes: X-Z, X-Y, Z-Y; and blends them together based on what plane the normal of the triangle is facing most on. This makes the terrain look much better, but it does have a performance hit! -Here is an article on tri-planar mapping: http://http.developer.nvidia.com/GPUGems3/gpugems3_ch01.html
Terrain will support a maximum of 12 diffuse texture. And a combined total of 13 diffuse and normal maps. -Most video cards are limited to 16 texture units (textures), and 3 are used behind the scenes of the terrain material for alpha blending of the textures, so you are left with a maximum of 13 textures.
If you are using the recommended PerspectiveLodCalculator for calculating LOD levels of the terrain, then you will want to pre-generate the entropy levels for the terrain. This is a slow process. If they are not pre-generated, the LOD control will generate them for you, but this will lag the user when they load the scene, and the terrain will flicker. +Here is an article on tri-planar mapping: + +
+ ++Terrain will support a maximum of 12 diffuse texture. And a combined total of 13 diffuse and normal maps. +Most video cards are limited to 16 texture units (textures), and 3 are used behind the scenes of the terrain material for alpha blending of the textures, so you are left with a maximum of 13 textures. + +
+ ++If you are using the recommended PerspectiveLodCalculator for calculating LOD levels of the terrain, then you will want to pre-generate the entropy levels for the terrain. This is a slow process. If they are not pre-generated, the LOD control will generate them for you, but this will lag the user when they load the scene, and the terrain will flicker. Use the 'Generate Entropies' button to pre-generate the entropies for the terrain, they will be saved with it. -Note that whenever you modify the height of the terrain, you should re-generate the entropies. Of course, don't do this every time, but maybe just before you are ready to send the map out for testing.
+When you want to load a scene into your game that has terrain, you have to set the camera on the LOD control in order for it to work correctly: + +
+TerrainLodControl lodControl = ((Node)terrain).getControl(TerrainLodControl.class); + if (lodControl != null) + lodControl.setCamera(getCamera());+ +
If jMonkeyPlatform cannot find a valid JDK although you have it installed you have to specify the location manually.
-To do that edit the file jmonkeyplatform.conf
in the etc
directory of your jMonkeyPlatform installation location. Mac users have to right-click the application and select "Show package contents" and then navigate to Contents/Resources/jmonkeyplatform
.
+If jMonkeyPlatform cannot find a valid JDK although you have it installed you have to specify the location manually. +
+ +
+To do that edit the file jmonkeyplatform.conf
in the etc
directory of your jMonkeyPlatform installation location.
+Mac users have to right-click the application and select "Show package contents" and then navigate to Contents/Resources/jmonkeyplatform
.
+
+ + +
+ +jMonkeyEngine3.jar
from the dist
dir of the compiled jme3 versionsrc
folder of the jme3 project in the "sources" tabjMonkeyEngine3.jar
from the dist
dir of the compiled jme3 versionsrc
folder of the jme3 project in the "sources" tabBest results when car is facing in z direction (towards you)
Usage:
New wheels will get the current suspension settings, you can edit single wheels via the SceneExplorer (update VehicleControl if wheels dont show up) or apply settings created with the settings generator to wheel groups.
Press the "test vehicle" button to drive the vehicle, use WASD to control, Enter to reset.
Known Issues: Dont save while testing the vehicle, you will save the location and acceleration info in the j3o.
Code Example to load vehicle:
//load vehicle and access VehicleControl + ++ +Vehicle Creator
++ +- \ No newline at end of file +control.accelerate(100);+ +Best results when car is facing in z direction (towards you) +
+ ++Usage: +
++
+ +- +
Select a j3o that contains a vehicle model and press the vehicle button (or right-click → edit vehicle)+- +
The VehicleCreator automatically adds a PhysicsVehicleControl to the rootNode of the model if there is none+- +
Select the Geometry or Node that contains the chassis in the SceneExplorer and press "create hull shape from selected"+- +
Select a Geometry that contains a wheel and press "make selected spatial wheel", select the "front wheel" checkboxes for front wheels+- +
Do so for all wheels++ +New wheels will get the current suspension settings, you can edit single wheels via the SceneExplorer (update VehicleControl if wheels dont show up) or apply settings created with the settings generator to wheel groups. +
+ ++Press the "test vehicle" button to drive the vehicle, use WASD to control, Enter to reset. +
+ ++Known Issues: Dont save while testing the vehicle, you will save the location and acceleration info in the j3o. +
+ ++Code Example to load vehicle: + +
+//load vehicle and access VehicleControl Spatial car=assetManager.loadModel("Models/MyCar.j3o"); VehicleControl control=car.getControl(VehicleControl.class); rootNode.attachChild(car); @@ -18,5 +48,7 @@ physicsSpace.add(control); //then use the control to control the vehicle: control.setPhysicsLocation(new Vector3f(10,2,10)); -control.accelerate(100);
Whether you work in a development team or alone: File versioning is a handy method to keep your code consistent, compare files line-by-line, and even roll back unwanted changes. This documentation shows you how to make the most of the jMonkeyPlatform's integrated version control features for Subversion, Mercurial, and CVS.
Note: Since the jMonkeyPlatform is based on the NetBeans Platform framework, you can learn about certain jMonkeyPlatform features by reading the corresponding NetBeans IDE tutorials (in the "see also links").
The jMonkeyPlatform supports various Version Control Systems such as Subversion, Mercurial, and CVS. No matter which of them you use, they all share a common user interface.
You can use file versioning alone or in a team. The advantages are that you can keep track of changes (who did it, when, and why), you can compare versions line-by-line, you can revert changes to files and lines, merge multiple changes to one file, and you can even undelete files.
Requirements:
svnadmin create /home/joe/mysvnrepo
Now you create a repository to store your project's files.
You and your team mates check out (download) the repository to their individual workstations.
http://jmonkeyengine.googlecode.com/svn/trunk/engine
Of course you can also check out existing repositories and access code from other open-source projects (e.g. SourceForge, GoogleCode, dev.java.net).
Receiving the latest changes from the team's repository is referred to as updating
. Sending your changes to the team's repository is refered to as commiting
.
If you do not use any version control, you can still track changes in projects to a certain degree.
See also:
+ +Whether you work in a development team or alone: File versioning is a handy method to keep your code consistent, compare files line-by-line, and even roll back unwanted changes. This documentation shows you how to make the most of the jMonkeyPlatform's integrated version control features for Subversion, Mercurial, and CVS. +
+ +
+
+
+
+Note: Since the jMonkeyPlatform is based on the NetBeans Platform framework, you can learn about certain jMonkeyPlatform features by reading the corresponding NetBeans IDE tutorials (in the "see also links"). +
+ ++ +The jMonkeyPlatform supports various Version Control Systems such as Subversion, Mercurial, and CVS. No matter which of them you use, they all share a common user interface. +
+ ++You can use file versioning alone or in a team. The advantages are that you can keep track of changes (who did it, when, and why), you can compare versions line-by-line, you can revert changes to files and lines, merge multiple changes to one file, and you can even undelete files. +
+ ++ +Requirements: +
+svnadmin create /home/joe/mysvnrepo
+ +Now you create a repository to store your project's files. + +
++ +You and your team mates check out (download) the repository to their individual workstations. + +
+
+
+
+
+Of course you can also check out existing repositories and access code from other open-source projects (e.g. SourceForge, GoogleCode, dev.java.net).
+
+
+Receiving the latest changes from the team's repository is referred to as updating
. Sending your changes to the team's repository is refered to as commiting
.
+
+
+ +If you do not use any version control, you can still track changes in projects to a certain degree. + +
+
+
+
+See also:
+
+