First pass at copying the source files with history into the

new gradle-based structure.


git-svn-id: https://jmonkeyengine.googlecode.com/svn/branches/gradle-restructure@10964 75d07b2b-3a1a-0410-a2c5-0572b91ccdca
experimental
PSp..om 11 years ago
commit ed77d40c63
  1. 640
      jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
  2. 20
      jme3-android/src/main/java/com/jme3/app/R.java
  3. 121
      jme3-android/src/main/java/com/jme3/asset/AndroidAssetManager.java
  4. 138
      jme3-android/src/main/java/com/jme3/asset/AndroidImageInfo.java
  5. 87
      jme3-android/src/main/java/com/jme3/asset/plugins/AndroidLocator.java
  6. 1054
      jme3-android/src/main/java/com/jme3/audio/android/AL.java
  7. 67
      jme3-android/src/main/java/com/jme3/audio/android/AndroidAudioData.java
  8. 24
      jme3-android/src/main/java/com/jme3/audio/android/AndroidAudioRenderer.java
  9. 523
      jme3-android/src/main/java/com/jme3/audio/android/AndroidMediaPlayerAudioRenderer.java
  10. 1423
      jme3-android/src/main/java/com/jme3/audio/android/AndroidOpenALSoftAudioRenderer.java
  11. 20
      jme3-android/src/main/java/com/jme3/audio/plugins/AndroidAudioLoader.java
  12. 348
      jme3-android/src/main/java/com/jme3/input/android/AndroidGestureHandler.java
  13. 686
      jme3-android/src/main/java/com/jme3/input/android/AndroidInput.java
  14. 273
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java
  15. 156
      jme3-android/src/main/java/com/jme3/input/android/AndroidKeyHandler.java
  16. 149
      jme3-android/src/main/java/com/jme3/input/android/AndroidKeyMapping.java
  17. 795
      jme3-android/src/main/java/com/jme3/input/android/AndroidSensorJoyInput.java
  18. 257
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler.java
  19. 152
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler14.java
  20. 121
      jme3-android/src/main/java/com/jme3/input/android/TouchEventPool.java
  21. 14
      jme3-android/src/main/java/com/jme3/renderer/android/Android22Workaround.java
  22. 26
      jme3-android/src/main/java/com/jme3/renderer/android/AndroidGLSurfaceView.java
  23. 2530
      jme3-android/src/main/java/com/jme3/renderer/android/OGLESShaderRenderer.java
  24. 129
      jme3-android/src/main/java/com/jme3/renderer/android/RendererUtil.java
  25. 571
      jme3-android/src/main/java/com/jme3/renderer/android/TextureUtil.java
  26. 518
      jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java
  27. 96
      jme3-android/src/main/java/com/jme3/system/android/AndroidTimer.java
  28. 226
      jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java
  29. 467
      jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
  30. 20
      jme3-android/src/main/java/com/jme3/texture/plugins/AndroidImageLoader.java
  31. 110
      jme3-android/src/main/java/com/jme3/util/AndroidLogHandler.java
  32. 42
      jme3-android/src/main/java/com/jme3/util/AndroidScreenshots.java
  33. 76
      jme3-android/src/main/java/com/jme3/util/RingBuffer.java
  34. 29
      jme3-android/src/main/java/jme3test/android/AndroidManifest.xml
  35. 54
      jme3-android/src/main/java/jme3test/android/DemoAndroidHarness.java
  36. 72
      jme3-android/src/main/java/jme3test/android/DemoLaunchAdapter.java
  37. 38
      jme3-android/src/main/java/jme3test/android/DemoLaunchEntry.java
  38. 131
      jme3-android/src/main/java/jme3test/android/DemoMainActivity.java
  39. 46
      jme3-android/src/main/java/jme3test/android/R.java
  40. 40
      jme3-android/src/main/java/jme3test/android/SimpleSoundTest.java
  41. 150
      jme3-android/src/main/java/jme3test/android/SimpleTexturedTest.java
  42. 97
      jme3-android/src/main/java/jme3test/android/TestAmbient.java
  43. 95
      jme3-android/src/main/java/jme3test/android/TestBumpModel.java
  44. 102
      jme3-android/src/main/java/jme3test/android/TestMovingParticle.java
  45. 99
      jme3-android/src/main/java/jme3test/android/TestNormalMapping.java
  46. 70
      jme3-android/src/main/java/jme3test/android/TestSkyLoadingLagoon.java
  47. 68
      jme3-android/src/main/java/jme3test/android/TestSkyLoadingPrimitives.java
  48. 44
      jme3-android/src/main/java/jme3test/android/TestUnshadedModel.java
  49. 31
      jme3-android/src/main/resources/res/layout/about.xml
  50. 25
      jme3-android/src/main/resources/res/layout/tests.xml
  51. 12
      jme3-android/src/main/resources/res/menu/options.xml
  52. 6
      jme3-android/src/main/resources/res/values/strings.xml
  53. 909
      jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java
  54. 69
      jme3-blender/src/main/java/com/jme3/asset/GeneratedTextureKey.java
  55. 132
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/AbstractBlenderHelper.java
  56. 636
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  57. 282
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java
  58. 105
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderModelLoader.java
  59. 29
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationData.java
  60. 276
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/ArmatureHelper.java
  61. 223
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BoneContext.java
  62. 243
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java
  63. 194
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/IpoHelper.java
  64. 147
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/cameras/CameraHelper.java
  65. 73
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/BoneConstraint.java
  66. 166
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/Constraint.java
  67. 478
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/ConstraintHelper.java
  68. 451
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SimulationNode.java
  69. 35
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SkeletonConstraint.java
  70. 27
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SpatialConstraint.java
  71. 146
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/VirtualTrack.java
  72. 135
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinition.java
  73. 77
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionDistLimit.java
  74. 124
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionFactory.java
  75. 117
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionIK.java
  76. 96
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLike.java
  77. 95
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLimit.java
  78. 28
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionNull.java
  79. 78
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLike.java
  80. 119
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLimit.java
  81. 64
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLike.java
  82. 86
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLimit.java
  83. 34
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/UnsupportedConstraintDefinition.java
  84. 146
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/BezierCurve.java
  85. 838
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/CurvesHelper.java
  86. 77
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderFileException.java
  87. 371
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderInputStream.java
  88. 203
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DnaBlockData.java
  89. 135
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DynamicArray.java
  90. 327
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Field.java
  91. 189
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/FileBlockHeader.java
  92. 189
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Pointer.java
  93. 315
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Structure.java
  94. 186
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/landscape/LandscapeHelper.java
  95. 118
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/lights/LightHelper.java
  96. 26
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/IAlphaMask.java
  97. 327
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java
  98. 371
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialHelper.java
  99. 88
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshContext.java
  100. 243
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,640 @@
package com.jme3.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.pm.ActivityInfo;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.NinePatchDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.*;
import android.view.ViewGroup.LayoutParams;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.jme3.audio.AudioRenderer;
import com.jme3.audio.android.AndroidAudioRenderer;
import com.jme3.input.JoyInput;
import com.jme3.input.TouchInput;
import com.jme3.input.android.AndroidSensorJoyInput;
import com.jme3.input.controls.TouchListener;
import com.jme3.input.controls.TouchTrigger;
import com.jme3.input.event.TouchEvent;
import com.jme3.renderer.android.AndroidGLSurfaceView;
import com.jme3.system.AppSettings;
import com.jme3.system.SystemListener;
import com.jme3.system.android.AndroidConfigChooser.ConfigType;
import com.jme3.system.android.JmeAndroidSystem;
import com.jme3.system.android.OGLESContext;
import com.jme3.util.AndroidLogHandler;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/**
* <code>AndroidHarness</code> wraps a jme application object and runs it on
* Android
*
* @author Kirill
* @author larynx
*/
public class AndroidHarness extends Activity implements TouchListener, DialogInterface.OnClickListener, SystemListener {
protected final static Logger logger = Logger.getLogger(AndroidHarness.class.getName());
/**
* The application class to start
*/
protected String appClass = "jme3test.android.Test";
/**
* The jme3 application object
*/
protected Application app = null;
/**
* ConfigType.FASTEST is RGB565, GLSurfaceView default ConfigType.BEST is
* RGBA8888 or better if supported by the hardware
* @deprecated ConfigType has been deprecated. AppSettings are now used
* to determine the desired configuration to match how LWJGL is implemented.
* Use eglBitsPerPixel, eglAlphaBits, eglDepthBits, eglStencilBits in MainActivity to
* override the default values
* (default values: RGB888, 0 alpha bits, 16 bit depth, 0 stencil bits)
*/
@Deprecated
protected ConfigType eglConfigType = null;
/**
* Sets the desired RGB size for the surfaceview. 16 = RGB565, 24 = RGB888.
* (default = 24)
*/
protected int eglBitsPerPixel = 24;
/**
* Sets the desired number of Alpha bits for the surfaceview. This affects
* how the surfaceview is able to display Android views that are located
* under the surfaceview jME uses to render the scenegraph.
* 0 = Opaque surfaceview background (fastest)
* 1->7 = Transparent surfaceview background
* 8 or higher = Translucent surfaceview background
* (default = 0)
*/
protected int eglAlphaBits = 0;
/**
* The number of depth bits specifies the precision of the depth buffer.
* (default = 16)
*/
protected int eglDepthBits = 16;
/**
* Sets the number of samples to use for multisampling.</br>
* Leave 0 (default) to disable multisampling.</br>
* Set to 2 or 4 to enable multisampling.
*/
protected int eglSamples = 0;
/**
* Set the number of stencil bits.
* (default = 0)
*/
protected int eglStencilBits = 0;
/**
* If true all valid and not valid egl configs are logged
* @deprecated this has no use
*/
@Deprecated
protected boolean eglConfigVerboseLogging = false;
/**
* set to 2, 4 to enable multisampling.
* @deprecated Use eglSamples
*/
@Deprecated
protected int antiAliasingSamples = 0;
/**
* Sets the type of Audio Renderer to be used.
* <p>
* Android MediaPlayer / SoundPool is the default and can be used on all
* supported Android platform versions (2.2+)<br>
* OpenAL Soft uses an OpenSL backend and is only supported on Android
* versions 2.3+.
* <p>
* Only use ANDROID_ static strings found in AppSettings
*
*/
protected String audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
/**
* If true Android Sensors are used as simulated Joysticks. Users can use the
* Android sensor feedback through the RawInputListener or by registering
* JoyAxisTriggers.
*/
protected boolean joystickEventsEnabled = false;
/**
* If true MouseEvents are generated from TouchEvents
*/
protected boolean mouseEventsEnabled = true;
/**
* Flip X axis
*/
protected boolean mouseEventsInvertX = false;
/**
* Flip Y axis
*/
protected boolean mouseEventsInvertY = false;
/**
* if true finish this activity when the jme app is stopped
*/
protected boolean finishOnAppStop = true;
/**
* set to false if you don't want the harness to handle the exit hook
*/
protected boolean handleExitHook = true;
/**
* Title of the exit dialog, default is "Do you want to exit?"
*/
protected String exitDialogTitle = "Do you want to exit?";
/**
* Message of the exit dialog, default is "Use your home key to bring this
* app into the background or exit to terminate it."
*/
protected String exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it.";
/**
* Set the screen window mode. If screenFullSize is true, then the
* notification bar and title bar are removed and the screen covers the
* entire display. If screenFullSize is false, then the notification bar
* remains visible if screenShowTitle is true while screenFullScreen is
* false, then the title bar is also displayed under the notification bar.
*/
protected boolean screenFullScreen = true;
/**
* if screenShowTitle is true while screenFullScreen is false, then the
* title bar is also displayed under the notification bar
*/
protected boolean screenShowTitle = true;
/**
* Splash Screen picture Resource ID. If a Splash Screen is desired, set
* splashPicID to the value of the Resource ID (i.e. R.drawable.picname). If
* splashPicID = 0, then no splash screen will be displayed.
*/
protected int splashPicID = 0;
/**
* Set the screen orientation, default is SENSOR
* ActivityInfo.SCREEN_ORIENTATION_* constants package
* android.content.pm.ActivityInfo
*
* SCREEN_ORIENTATION_UNSPECIFIED SCREEN_ORIENTATION_LANDSCAPE
* SCREEN_ORIENTATION_PORTRAIT SCREEN_ORIENTATION_USER
* SCREEN_ORIENTATION_BEHIND SCREEN_ORIENTATION_SENSOR (default)
* SCREEN_ORIENTATION_NOSENSOR
*/
protected int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR;
protected OGLESContext ctx;
protected AndroidGLSurfaceView view = null;
protected boolean isGLThreadPaused = true;
protected ImageView splashImageView = null;
protected FrameLayout frameLayout = null;
final private String ESCAPE_EVENT = "TouchEscape";
private boolean firstDrawFrame = true;
private boolean inConfigChange = false;
private class DataObject {
protected Application app = null;
}
@Override
public Object onRetainNonConfigurationInstance() {
logger.log(Level.FINE, "onRetainNonConfigurationInstance");
final DataObject data = new DataObject();
data.app = this.app;
inConfigChange = true;
return data;
}
@Override
public void onCreate(Bundle savedInstanceState) {
initializeLogHandler();
logger.fine("onCreate");
super.onCreate(savedInstanceState);
JmeAndroidSystem.setActivity(this);
if (screenFullScreen) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
if (!screenShowTitle) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
}
}
setRequestedOrientation(screenOrientation);
final DataObject data = (DataObject) getLastNonConfigurationInstance();
if (data != null) {
logger.log(Level.FINE, "Using Retained App");
this.app = data.app;
} else {
// Discover the screen reolution
//TODO try to find a better way to get a hand on the resolution
WindowManager wind = this.getWindowManager();
Display disp = wind.getDefaultDisplay();
Log.d("AndroidHarness", "Resolution from Window, width:" + disp.getWidth() + ", height: " + disp.getHeight());
// Create Settings
logger.log(Level.FINE, "Creating settings");
AppSettings settings = new AppSettings(true);
settings.setEmulateMouse(mouseEventsEnabled);
settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY);
settings.setUseJoysticks(joystickEventsEnabled);
if (eglConfigType == null) {
logger.log(Level.FINE, "using new appsettings for eglConfig");
settings.setBitsPerPixel(eglBitsPerPixel);
settings.setAlphaBits(eglAlphaBits);
settings.setDepthBits(eglDepthBits);
settings.setSamples(eglSamples);
settings.setStencilBits(eglStencilBits);
} else {
logger.log(Level.FINE, "using old eglConfigType {0} for eglConfig", eglConfigType);
switch (eglConfigType) {
case BEST:
settings.setBitsPerPixel(24);
settings.setAlphaBits(0);
settings.setDepthBits(16);
settings.setStencilBits(0);
break;
case FASTEST:
case LEGACY:
settings.setBitsPerPixel(16);
settings.setAlphaBits(0);
settings.setDepthBits(16);
settings.setStencilBits(0);
break;
case BEST_TRANSLUCENT:
settings.setBitsPerPixel(24);
settings.setAlphaBits(8);
settings.setDepthBits(16);
settings.setStencilBits(0);
break;
default:
throw new IllegalArgumentException("Invalid eglConfigType");
}
settings.setSamples(antiAliasingSamples);
}
settings.setResolution(disp.getWidth(), disp.getHeight());
settings.setAudioRenderer(audioRendererType);
// Create application instance
try {
if (app == null) {
@SuppressWarnings("unchecked")
Class<? extends Application> clazz = (Class<? extends Application>) Class.forName(appClass);
app = clazz.newInstance();
}
app.setSettings(settings);
app.start();
} catch (Exception ex) {
handleError("Class " + appClass + " init failed", ex);
setContentView(new TextView(this));
}
}
ctx = (OGLESContext) app.getContext();
view = ctx.createView();
// AndroidHarness wraps the app as a SystemListener.
ctx.setSystemListener(this);
layoutDisplay();
}
@Override
protected void onRestart() {
logger.fine("onRestart");
super.onRestart();
if (app != null) {
app.restart();
}
}
@Override
protected void onStart() {
logger.fine("onStart");
super.onStart();
}
@Override
protected void onResume() {
logger.fine("onResume");
super.onResume();
gainFocus();
}
@Override
protected void onPause() {
logger.fine("onPause");
loseFocus();
super.onPause();
}
@Override
protected void onStop() {
logger.fine("onStop");
super.onStop();
}
@Override
protected void onDestroy() {
logger.fine("onDestroy");
final DataObject data = (DataObject) getLastNonConfigurationInstance();
if (data != null || inConfigChange) {
logger.fine("In Config Change, not stopping app.");
} else {
if (app != null) {
app.stop(!isGLThreadPaused);
}
}
setContentView(new TextView(this));
JmeAndroidSystem.setActivity(null);
ctx = null;
app = null;
view = null;
super.onDestroy();
}
public Application getJmeApplication() {
return app;
}
/**
* Called when an error has occurred. By default, will show an error message
* to the user and print the exception/error to the log.
*/
@Override
public void handleError(final String errorMsg, final Throwable t) {
String stackTrace = "";
String title = "Error";
if (t != null) {
// Convert exception to string
StringWriter sw = new StringWriter(100);
t.printStackTrace(new PrintWriter(sw));
stackTrace = sw.toString();
title = t.toString();
}
final String finalTitle = title;
final String finalMsg = (errorMsg != null ? errorMsg : "Uncaught Exception")
+ "\n" + stackTrace;
logger.log(Level.SEVERE, finalMsg);
runOnUiThread(new Runnable() {
@Override
public void run() {
AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon)
.setTitle(finalTitle).setPositiveButton("Kill", AndroidHarness.this).setMessage(finalMsg).create();
dialog.show();
}
});
}
/**
* Called by the android alert dialog, terminate the activity and OpenGL
* rendering
*
* @param dialog
* @param whichButton
*/
public void onClick(DialogInterface dialog, int whichButton) {
if (whichButton != -2) {
if (app != null) {
app.stop(true);
}
app = null;
this.finish();
}
}
/**
* Gets called by the InputManager on all touch/drag/scale events
*/
@Override
public void onTouch(String name, TouchEvent evt, float tpf) {
if (name.equals(ESCAPE_EVENT)) {
switch (evt.getType()) {
case KEY_UP:
runOnUiThread(new Runnable() {
@Override
public void run() {
AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon)
.setTitle(exitDialogTitle).setPositiveButton("Yes", AndroidHarness.this).setNegativeButton("No", AndroidHarness.this).setMessage(exitDialogMessage).create();
dialog.show();
}
});
break;
default:
break;
}
}
}
public void layoutDisplay() {
logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID);
if (view == null) {
logger.log(Level.FINE, "view is null!");
}
if (splashPicID != 0) {
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT,
Gravity.CENTER);
frameLayout = new FrameLayout(this);
splashImageView = new ImageView(this);
Drawable drawable = this.getResources().getDrawable(splashPicID);
if (drawable instanceof NinePatchDrawable) {
splashImageView.setBackgroundDrawable(drawable);
} else {
splashImageView.setImageResource(splashPicID);
}
if (view.getParent() != null) {
((ViewGroup) view.getParent()).removeView(view);
}
frameLayout.addView(view);
if (splashImageView.getParent() != null) {
((ViewGroup) splashImageView.getParent()).removeView(splashImageView);
}
frameLayout.addView(splashImageView, lp);
setContentView(frameLayout);
logger.log(Level.FINE, "Splash Screen Created");
} else {
logger.log(Level.FINE, "Splash Screen Skipped.");
setContentView(view);
}
}
public void removeSplashScreen() {
logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID);
if (splashPicID != 0) {
if (frameLayout != null) {
if (splashImageView != null) {
this.runOnUiThread(new Runnable() {
@Override
public void run() {
splashImageView.setVisibility(View.INVISIBLE);
frameLayout.removeView(splashImageView);
}
});
} else {
logger.log(Level.FINE, "splashImageView is null");
}
} else {
logger.log(Level.FINE, "frameLayout is null");
}
}
}
/**
* Removes the standard Android log handler due to an issue with not logging
* entries lower than INFO level and adds a handler that produces
* JME formatted log messages.
*/
protected void initializeLogHandler() {
Logger log = LogManager.getLogManager().getLogger("");
for (Handler handler : log.getHandlers()) {
if (log.getLevel() != null && log.getLevel().intValue() <= Level.FINE.intValue()) {
Log.v("AndroidHarness", "Removing Handler class: " + handler.getClass().getName());
}
log.removeHandler(handler);
}
Handler handler = new AndroidLogHandler();
log.addHandler(handler);
handler.setLevel(Level.ALL);
}
public void initialize() {
app.initialize();
if (handleExitHook) {
// remove existing mapping from SimpleApplication that stops the app
// when the esc key is pressed (esc key = android back key) so that
// AndroidHarness can produce the exit app dialog box.
if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) {
app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT);
}
app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT});
}
}
public void reshape(int width, int height) {
app.reshape(width, height);
}
public void update() {
app.update();
// call to remove the splash screen, if present.
// call after app.update() to make sure no gap between
// splash screen going away and app display being shown.
if (firstDrawFrame) {
removeSplashScreen();
firstDrawFrame = false;
}
}
public void requestClose(boolean esc) {
app.requestClose(esc);
}
public void destroy() {
if (app != null) {
app.destroy();
}
if (finishOnAppStop) {
finish();
}
}
public void gainFocus() {
logger.fine("gainFocus");
if (view != null) {
view.onResume();
}
if (app != null) {
//resume the audio
AudioRenderer result = app.getAudioRenderer();
if (result != null) {
if (result instanceof AndroidAudioRenderer) {
AndroidAudioRenderer renderer = (AndroidAudioRenderer) result;
renderer.resumeAll();
}
}
//resume the sensors (aka joysticks)
if (app.getContext() != null) {
JoyInput joyInput = app.getContext().getJoyInput();
if (joyInput != null) {
if (joyInput instanceof AndroidSensorJoyInput) {
AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput;
androidJoyInput.resumeSensors();
}
}
}
}
isGLThreadPaused = false;
if (app != null) {
app.gainFocus();
}
}
public void loseFocus() {
logger.fine("loseFocus");
if (app != null) {
app.loseFocus();
}
if (view != null) {
view.onPause();
}
if (app != null) {
//pause the audio
AudioRenderer result = app.getAudioRenderer();
if (result != null) {
logger.log(Level.FINE, "pause: {0}", result.getClass().getSimpleName());
if (result instanceof AndroidAudioRenderer) {
AndroidAudioRenderer renderer = (AndroidAudioRenderer) result;
renderer.pauseAll();
}
}
//pause the sensors (aka joysticks)
if (app.getContext() != null) {
JoyInput joyInput = app.getContext().getJoyInput();
if (joyInput != null) {
if (joyInput instanceof AndroidSensorJoyInput) {
AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput;
androidJoyInput.pauseSensors();
}
}
}
}
isGLThreadPaused = true;
}
}

@ -0,0 +1,20 @@
/* AUTO-GENERATED FILE. DO NOT MODIFY.
*
* This class was automatically generated by the
* aapt tool from the resource data it found. It
* should not be modified by hand.
*/
package com.jme3.app;
public final class R {
public static final class attr {
}
public static final class layout {
public static final int main=0x7f020000;
}
public static final class string {
public static final int app_name=0x7f030000;
public static final int jme3_appclass=0x7f030001;
}
}

@ -0,0 +1,121 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.asset;
import com.jme3.asset.plugins.AndroidLocator;
import com.jme3.asset.plugins.ClasspathLocator;
import com.jme3.audio.android.AndroidAudioRenderer;
import com.jme3.audio.plugins.AndroidAudioLoader;
import com.jme3.audio.plugins.WAVLoader;
import com.jme3.system.AppSettings;
import com.jme3.system.android.JmeAndroidSystem;
import com.jme3.texture.plugins.AndroidImageLoader;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <code>AndroidAssetManager</code> is an implementation of DesktopAssetManager for Android
*
* @author larynx
*/
public class AndroidAssetManager extends DesktopAssetManager {
private static final Logger logger = Logger.getLogger(AndroidAssetManager.class.getName());
public AndroidAssetManager() {
this(null);
}
@Deprecated
public AndroidAssetManager(boolean loadDefaults) {
//this(Thread.currentThread().getContextClassLoader().getResource("com/jme3/asset/Android.cfg"));
this(null);
}
private void registerLoaderSafe(String loaderClass, String ... extensions) {
try {
Class<? extends AssetLoader> loader = (Class<? extends AssetLoader>) Class.forName(loaderClass);
registerLoader(loader, extensions);
} catch (Exception e){
logger.log(Level.WARNING, "Failed to load AssetLoader", e);
}
}
/**
* AndroidAssetManager constructor
* If URL == null then a default list of locators and loaders for android is set
* @param configFile
*/
public AndroidAssetManager(URL configFile) {
System.setProperty("org.xml.sax.driver", "org.xmlpull.v1.sax2.Driver");
// Set Default Android config
registerLocator("", AndroidLocator.class);
registerLocator("", ClasspathLocator.class);
registerLoader(AndroidImageLoader.class, "jpg", "bmp", "gif", "png", "jpeg");
if (JmeAndroidSystem.getAudioRendererType().equals(AppSettings.ANDROID_MEDIAPLAYER)) {
registerLoader(AndroidAudioLoader.class, "ogg", "mp3", "wav");
} else if (JmeAndroidSystem.getAudioRendererType().equals(AppSettings.ANDROID_OPENAL_SOFT)) {
registerLoader(WAVLoader.class, "wav");
// TODO jogg is not in core, need to add some other way to get around compile errors, or not.
// registerLoader(com.jme3.audio.plugins.OGGLoader.class, "ogg");
registerLoaderSafe("com.jme3.audio.plugins.OGGLoader", "ogg");
} else {
throw new IllegalStateException("No Audio Renderer Type defined!");
}
registerLoader(com.jme3.material.plugins.J3MLoader.class, "j3m");
registerLoader(com.jme3.material.plugins.J3MLoader.class, "j3md");
registerLoader(com.jme3.material.plugins.ShaderNodeDefinitionLoader.class, "j3sn");
registerLoader(com.jme3.shader.plugins.GLSLLoader.class, "vert", "frag", "glsl", "glsllib");
registerLoader(com.jme3.export.binary.BinaryImporter.class, "j3o");
registerLoader(com.jme3.font.plugins.BitmapFontLoader.class, "fnt");
// Less common loaders (especially on Android)
registerLoaderSafe("com.jme3.texture.plugins.DDSLoader", "dds");
registerLoaderSafe("com.jme3.texture.plugins.PFMLoader", "pfm");
registerLoaderSafe("com.jme3.texture.plugins.HDRLoader", "hdr");
registerLoaderSafe("com.jme3.texture.plugins.TGALoader", "tga");
registerLoaderSafe("com.jme3.scene.plugins.OBJLoader", "obj");
registerLoaderSafe("com.jme3.scene.plugins.MTLLoader", "mtl");
registerLoaderSafe("com.jme3.scene.plugins.ogre.MeshLoader", "mesh.xml");
registerLoaderSafe("com.jme3.scene.plugins.ogre.SkeletonLoader", "skeleton.xml");
registerLoaderSafe("com.jme3.scene.plugins.ogre.MaterialLoader", "material");
registerLoaderSafe("com.jme3.scene.plugins.ogre.SceneLoader", "scene");
logger.fine("AndroidAssetManager created.");
}
}

@ -0,0 +1,138 @@
package com.jme3.asset;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import com.jme3.math.ColorRGBA;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.image.ImageRaster;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <code>AndroidImageInfo</code> is set in a jME3 image via the {@link Image#setEfficentData(java.lang.Object) }
* method to retrieve a {@link Bitmap} when it is needed by the renderer.
* User code may extend <code>AndroidImageInfo</code> and provide their own implementation of the
* {@link AndroidImageInfo#loadBitmap()} method to acquire a bitmap by their own means.
*
* @author Kirill Vainer
*/
public class AndroidImageInfo extends ImageRaster {
private static final Logger logger = Logger.getLogger(AndroidImageInfo.class.getName());
protected AssetInfo assetInfo;
protected Bitmap bitmap;
protected Format format;
public AndroidImageInfo(AssetInfo assetInfo) {
this.assetInfo = assetInfo;
}
public Bitmap getBitmap(){
if (bitmap == null || bitmap.isRecycled()){
try {
loadBitmap();
} catch (IOException ex) {
// If called first inside AssetManager, the error will propagate
// correctly. Assuming that if the first calls succeeds
// then subsequent calls will as well.
throw new AssetLoadException("Failed to load image " + assetInfo.getKey(), ex);
}
}
return bitmap;
}
public void notifyBitmapUploaded() {
// Default function is to recycle the bitmap.
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
logger.log(Level.FINE, "Bitmap was deleted. ");
}
}
public Format getFormat(){
return format;
}
@Override
public int getWidth() {
return getBitmap().getWidth();
}
@Override
public int getHeight() {
return getBitmap().getHeight();
}
@Override
public void setPixel(int x, int y, ColorRGBA color) {
getBitmap().setPixel(x, y, color.asIntARGB());
}
@Override
public ColorRGBA getPixel(int x, int y, ColorRGBA store) {
if (store == null) {
store = new ColorRGBA();
}
store.fromIntARGB(getBitmap().getPixel(x, y));
return store;
}
/**
* Loads the bitmap directly from the asset info, possibly updating
* or creating the image object.
*/
protected void loadBitmap() throws IOException{
InputStream in = null;
try {
in = assetInfo.openStream();
bitmap = BitmapFactory.decodeStream(in);
if (bitmap == null) {
throw new IOException("Failed to load image: " + assetInfo.getKey().getName());
}
} finally {
if (in != null) {
in.close();
}
}
switch (bitmap.getConfig()) {
case ALPHA_8:
format = Image.Format.Alpha8;
break;
case ARGB_4444:
format = Image.Format.ARGB4444;
break;
case ARGB_8888:
format = Image.Format.RGBA8;
break;
case RGB_565:
format = Image.Format.RGB565;
break;
default:
// This should still work as long
// as renderer doesn't check format
// but just loads bitmap directly.
format = null;
}
TextureKey texKey = (TextureKey) assetInfo.getKey();
if (texKey.isFlipY()) {
// Flip the image, then delete the old one.
Matrix flipMat = new Matrix();
flipMat.preScale(1.0f, -1.0f);
Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), flipMat, false);
bitmap.recycle();
bitmap = newBitmap;
if (bitmap == null) {
throw new IOException("Failed to flip image: " + texKey);
}
}
}
}

@ -0,0 +1,87 @@
package com.jme3.asset.plugins;
import com.jme3.asset.*;
import com.jme3.system.android.JmeAndroidSystem;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
public class AndroidLocator implements AssetLocator {
private static final Logger logger = Logger.getLogger(AndroidLocator.class.getName());
private android.content.res.AssetManager androidManager;
private String rootPath = "";
private class AndroidAssetInfo extends AssetInfo {
private InputStream in;
private final String assetPath;
public AndroidAssetInfo(com.jme3.asset.AssetManager assetManager, AssetKey<?> key, String assetPath, InputStream in) {
super(assetManager, key);
this.assetPath = assetPath;
this.in = in;
}
@Override
public InputStream openStream() {
if (in != null){
// Reuse the already existing stream (only once)
InputStream in2 = in;
in = null;
return in2;
}else{
// Create a new stream for subsequent invocations.
try {
return androidManager.open(assetPath);
} catch (IOException ex) {
throw new AssetLoadException("Failed to open asset " + assetPath, ex);
}
}
}
}
private AndroidAssetInfo create(AssetManager assetManager, AssetKey key, String assetPath) throws IOException {
try {
InputStream in = androidManager.open(assetPath);
if (in == null){
return null;
}else{
return new AndroidAssetInfo(assetManager, key, assetPath, in);
}
} catch (IOException ex) {
// XXX: Prefer to show warning here?
// Should only surpress exceptions for "file missing" type errors.
return null;
}
}
public AndroidLocator() {
androidManager = JmeAndroidSystem.getActivity().getAssets();
}
public void setRootPath(String rootPath) {
this.rootPath = rootPath;
}
@SuppressWarnings("rawtypes")
@Override
public AssetInfo locate(com.jme3.asset.AssetManager manager, AssetKey key) {
String assetPath = rootPath + key.getName();
// Fix path issues
if (assetPath.startsWith("/")) {
// Remove leading /
assetPath = assetPath.substring(1);
}
assetPath = assetPath.replace("//", "/");
try {
return create(manager, key, assetPath);
} catch (IOException ex) {
// This is different handling than URL locator
// since classpath locating would return null at the getResource()
// call, otherwise there's a more critical error...
throw new AssetLoadException("Failed to open asset " + assetPath, ex);
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,67 @@
package com.jme3.audio.android;
import com.jme3.asset.AssetKey;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioRenderer;
import com.jme3.util.NativeObject;
public class AndroidAudioData extends AudioData {
protected AssetKey<?> assetKey;
protected float currentVolume = 0f;
public AndroidAudioData(){
super();
}
protected AndroidAudioData(int id){
super(id);
}
public AssetKey<?> getAssetKey() {
return assetKey;
}
public void setAssetKey(AssetKey<?> assetKey) {
this.assetKey = assetKey;
}
@Override
public DataType getDataType() {
return DataType.Buffer;
}
@Override
public float getDuration() {
return 0; // TODO: ???
}
@Override
public void resetObject() {
this.id = -1;
setUpdateNeeded();
}
@Override
public void deleteObject(Object rendererObject) {
((AudioRenderer)rendererObject).deleteAudioData(this);
}
public float getCurrentVolume() {
return currentVolume;
}
public void setCurrentVolume(float currentVolume) {
this.currentVolume = currentVolume;
}
@Override
public NativeObject createDestructableClone() {
return new AndroidAudioData(id);
}
@Override
public long getUniqueId() {
return ((long)OBJTYPE_AUDIOBUFFER << 32) | ((long)id);
}
}

@ -0,0 +1,24 @@
package com.jme3.audio.android;
import com.jme3.audio.AudioRenderer;
/**
* Android specific AudioRenderer interface that supports pausing and resuming
* audio files when the app is minimized or placed in the background
*
* @author iwgeric
*/
public interface AndroidAudioRenderer extends AudioRenderer {
/**
* Pauses all Playing audio. To be used when the app is placed in the
* background.
*/
public void pauseAll();
/**
* Resumes all Paused audio. To be used when the app is brought back to
* the foreground.
*/
public void resumeAll();
}

@ -0,0 +1,523 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.audio.android;
import android.app.Activity;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.SoundPool;
import com.jme3.asset.AssetKey;
import com.jme3.audio.*;
import com.jme3.audio.AudioSource.Status;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import java.io.IOException;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class is the android implementation for {@link AudioRenderer}
*
* @author larynx
* @author plan_rich
*/
public class AndroidMediaPlayerAudioRenderer implements AndroidAudioRenderer,
SoundPool.OnLoadCompleteListener, MediaPlayer.OnCompletionListener {
private static final Logger logger = Logger.getLogger(AndroidMediaPlayerAudioRenderer.class.getName());
private final static int MAX_NUM_CHANNELS = 16;
private final HashMap<AudioSource, MediaPlayer> musicPlaying = new HashMap<AudioSource, MediaPlayer>();
private SoundPool soundPool = null;
private final Vector3f listenerPosition = new Vector3f();
// For temp use
private final Vector3f distanceVector = new Vector3f();
private final AssetManager assetManager;
private HashMap<Integer, AudioSource> soundpoolStillLoading = new HashMap<Integer, AudioSource>();
private Listener listener;
private boolean audioDisabled = false;
private final AudioManager manager;
public AndroidMediaPlayerAudioRenderer(Activity context) {
manager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
context.setVolumeControlStream(AudioManager.STREAM_MUSIC);
assetManager = context.getAssets();
}
@Override
public void initialize() {
soundPool = new SoundPool(MAX_NUM_CHANNELS, AudioManager.STREAM_MUSIC,
0);
soundPool.setOnLoadCompleteListener(this);
}
@Override
public void updateSourceParam(AudioSource src, AudioParam param) {
if (audioDisabled) {
return;
}
if (src.getChannel() < 0) {
return;
}
switch (param) {
case Position:
if (!src.isPositional()) {
return;
}
Vector3f pos = src.getPosition();
break;
case Velocity:
if (!src.isPositional()) {
return;
}
Vector3f vel = src.getVelocity();
break;
case MaxDistance:
if (!src.isPositional()) {
return;
}
break;
case RefDistance:
if (!src.isPositional()) {
return;
}
break;
case ReverbFilter:
if (!src.isPositional() || !src.isReverbEnabled()) {
return;
}
break;
case ReverbEnabled:
if (!src.isPositional()) {
return;
}
if (src.isReverbEnabled()) {
updateSourceParam(src, AudioParam.ReverbFilter);
}
break;
case IsPositional:
break;
case Direction:
if (!src.isDirectional()) {
return;
}
Vector3f dir = src.getDirection();
break;
case InnerAngle:
if (!src.isDirectional()) {
return;
}
break;
case OuterAngle:
if (!src.isDirectional()) {
return;
}
break;
case IsDirectional:
if (src.isDirectional()) {
updateSourceParam(src, AudioParam.Direction);
updateSourceParam(src, AudioParam.InnerAngle);
updateSourceParam(src, AudioParam.OuterAngle);
} else {
}
break;
case DryFilter:
if (src.getDryFilter() != null) {
Filter f = src.getDryFilter();
if (f.isUpdateNeeded()) {
// updateFilter(f);
}
}
break;
case Looping:
if (src.isLooping()) {
}
break;
case Volume:
MediaPlayer mp = musicPlaying.get(src);
if (mp != null) {
mp.setVolume(src.getVolume(), src.getVolume());
} else {
soundPool.setVolume(src.getChannel(), src.getVolume(),
src.getVolume());
}
break;
case Pitch:
break;
}
}
@Override
public void updateListenerParam(Listener listener, ListenerParam param) {
if (audioDisabled) {
return;
}
switch (param) {
case Position:
listenerPosition.set(listener.getLocation());
break;
case Rotation:
Vector3f dir = listener.getDirection();
Vector3f up = listener.getUp();
break;
case Velocity:
Vector3f vel = listener.getVelocity();
break;
case Volume:
// alListenerf(AL_GAIN, listener.getVolume());
break;
}
}
@Override
public void update(float tpf) {
float distance;
float volume;
// Loop over all mediaplayers
for (AudioSource src : musicPlaying.keySet()) {
MediaPlayer mp = musicPlaying.get(src);
// Calc the distance to the listener
distanceVector.set(listenerPosition);
distanceVector.subtractLocal(src.getPosition());
distance = FastMath.abs(distanceVector.length());
if (distance < src.getRefDistance()) {
distance = src.getRefDistance();
}
if (distance > src.getMaxDistance()) {
distance = src.getMaxDistance();
}
volume = src.getRefDistance() / distance;
AndroidAudioData audioData = (AndroidAudioData) src.getAudioData();
if (FastMath.abs(audioData.getCurrentVolume() - volume) > FastMath.FLT_EPSILON) {
// Left / Right channel get the same volume by now, only
// positional
mp.setVolume(volume, volume);
audioData.setCurrentVolume(volume);
}
}
}
public void setListener(Listener listener) {
if (audioDisabled) {
return;
}
if (this.listener != null) {
// previous listener no longer associated with current
// renderer
this.listener.setRenderer(null);
}
this.listener = listener;
this.listener.setRenderer(this);
}
@Override
public void cleanup() {
// Cleanup sound pool
if (soundPool != null) {
soundPool.release();
soundPool = null;
}
// Cleanup media player
for (AudioSource src : musicPlaying.keySet()) {
MediaPlayer mp = musicPlaying.get(src);
{
mp.stop();
mp.release();
src.setStatus(Status.Stopped);
}
}
musicPlaying.clear();
}
@Override
public void onCompletion(MediaPlayer mp) {
if (mp.isPlaying()) {
mp.seekTo(0);
mp.stop();
}
// XXX: This has bad performance -> maybe change overall structure of
// mediaplayer in this audiorenderer?
for (AudioSource src : musicPlaying.keySet()) {
if (musicPlaying.get(src) == mp) {
src.setStatus(Status.Stopped);
break;
}
}
}
/**
* Plays using the {@link SoundPool} of Android. Due to hard limitation of
* the SoundPool: After playing more instances of the sound you only have
* the channel of the last played instance.
*
* It is not possible to get information about the state of the soundpool of
* a specific streamid, so removing is not possilbe -> noone knows when
* sound finished.
*/
public void playSourceInstance(AudioSource src) {
if (audioDisabled) {
return;
}
AndroidAudioData audioData = (AndroidAudioData) src.getAudioData();
if (!(audioData.getAssetKey() instanceof AudioKey)) {
throw new IllegalArgumentException("Asset is not a AudioKey");
}
AudioKey assetKey = (AudioKey) audioData.getAssetKey();
try {
if (audioData.getId() < 0) { // found something to load
int soundId = soundPool.load(
assetManager.openFd(assetKey.getName()), 1);
audioData.setId(soundId);
}
int channel = soundPool.play(audioData.getId(), 1f, 1f, 1, 0, 1f);
if (channel == 0) {
soundpoolStillLoading.put(audioData.getId(), src);
} else {
if (src.getStatus() != Status.Stopped) {
soundPool.stop(channel);
src.setStatus(Status.Stopped);
}
src.setChannel(channel); // receive a channel at the last
setSourceParams(src);
// playing at least
}
} catch (IOException e) {
logger.log(Level.SEVERE,
"Failed to load sound " + assetKey.getName(), e);
audioData.setId(-1);
}
}
@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
AudioSource src = soundpoolStillLoading.remove(sampleId);
if (src == null) {
logger.warning("Something went terribly wrong! onLoadComplete"
+ " had sampleId which was not in the HashMap of loading items");
return;
}
AudioData audioData = src.getAudioData();
// load was successfull
if (status == 0) {
int channelIndex;
channelIndex = soundPool.play(audioData.getId(), 1f, 1f, 1, 0, 1f);
src.setChannel(channelIndex);
setSourceParams(src);
}
}
public void playSource(AudioSource src) {
if (audioDisabled) {
return;
}
AndroidAudioData audioData = (AndroidAudioData) src.getAudioData();
MediaPlayer mp = musicPlaying.get(src);
if (mp == null) {
mp = new MediaPlayer();
mp.setOnCompletionListener(this);
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
try {
if (src.getStatus() == Status.Stopped) {
mp.reset();
AssetKey<?> key = audioData.getAssetKey();
AssetFileDescriptor afd = assetManager.openFd(key.getName()); // assetKey.getName()
mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
afd.getLength());
mp.prepare();
setSourceParams(src, mp);
src.setChannel(0);
src.setStatus(Status.Playing);
musicPlaying.put(src, mp);
mp.start();
} else {
mp.start();
}
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
private void setSourceParams(AudioSource src, MediaPlayer mp) {
mp.setLooping(src.isLooping());
mp.setVolume(src.getVolume(), src.getVolume());
//src.getDryFilter();
}
private void setSourceParams(AudioSource src) {
soundPool.setLoop(src.getChannel(), src.isLooping() ? -1 : 0);
soundPool.setVolume(src.getChannel(), src.getVolume(), src.getVolume());
}
/**
* Pause the current playing sounds. Both from the {@link SoundPool} and the
* active {@link MediaPlayer}s
*/
public void pauseAll() {
if (soundPool != null) {
soundPool.autoPause();
for (MediaPlayer mp : musicPlaying.values()) {
if(mp.isPlaying()){
mp.pause();
}
}
}
}
/**
* Resume all paused sounds.
*/
public void resumeAll() {
if (soundPool != null) {
soundPool.autoResume();
for (MediaPlayer mp : musicPlaying.values()) {
mp.start(); //no resume -> api says call start to resume
}
}
}
public void pauseSource(AudioSource src) {
if (audioDisabled) {
return;
}
MediaPlayer mp = musicPlaying.get(src);
if (mp != null) {
mp.pause();
src.setStatus(Status.Paused);
} else {
int channel = src.getChannel();
if (channel != -1) {
soundPool.pause(channel); // is not very likley to make
} // something useful :)
}
}
public void stopSource(AudioSource src) {
if (audioDisabled) {
return;
}
// can be stream or buffer -> so try to get mediaplayer
// if there is non try to stop soundpool
MediaPlayer mp = musicPlaying.get(src);
if (mp != null) {
mp.stop();
mp.reset();
src.setStatus(Status.Stopped);
} else {
int channel = src.getChannel();
if (channel != -1) {
soundPool.pause(channel); // is not very likley to make
// something useful :)
}
}
}
@Override
public void deleteAudioData(AudioData ad) {
for (AudioSource src : musicPlaying.keySet()) {
if (src.getAudioData() == ad) {
MediaPlayer mp = musicPlaying.remove(src);
mp.stop();
mp.release();
src.setStatus(Status.Stopped);
src.setChannel(-1);
ad.setId(-1);
break;
}
}
if (ad.getId() > 0) {
soundPool.unload(ad.getId());
ad.setId(-1);
}
}
@Override
public void setEnvironment(Environment env) {
// not yet supported
}
@Override
public void deleteFilter(Filter filter) {
}
}

@ -0,0 +1,20 @@
package com.jme3.audio.plugins;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
import com.jme3.audio.android.AndroidAudioData;
import java.io.IOException;
/**
* <code>AndroidAudioLoader</code> will create an
* {@link AndroidAudioData} object with the specified asset key.
*/
public class AndroidAudioLoader implements AssetLoader {
@Override
public Object load(AssetInfo assetInfo) throws IOException {
AndroidAudioData result = new AndroidAudioData();
result.setAssetKey(assetInfo.getKey());
return result;
}
}

@ -0,0 +1,348 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.input.android;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import com.jme3.input.event.InputEvent;
import com.jme3.input.event.MouseMotionEvent;
import com.jme3.input.event.TouchEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* AndroidGestureHandler uses Gesture type listeners to create jME TouchEvents
* for gestures. This class is designed to handle the gestures supported
* on Android rev 9 (Android 2.3). Extend this class to add functionality
* added by Android after rev 9.
*
* @author iwgeric
*/
public class AndroidGestureHandler implements
GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener,
ScaleGestureDetector.OnScaleGestureListener {
private static final Logger logger = Logger.getLogger(AndroidGestureHandler.class.getName());
private AndroidInputHandler androidInput;
private GestureDetector gestureDetector;
private ScaleGestureDetector scaleDetector;
float gestureDownX = -1f;
float gestureDownY = -1f;
float scaleStartX = -1f;
float scaleStartY = -1f;
public AndroidGestureHandler(AndroidInputHandler androidInput) {
this.androidInput = androidInput;
}
public void initialize() {
}
public void destroy() {
setView(null);
}
public void setView(View view) {
if (view != null) {
gestureDetector = new GestureDetector(view.getContext(), this);
scaleDetector = new ScaleGestureDetector(view.getContext(), this);
} else {
gestureDetector = null;
scaleDetector = null;
}
}
public void detectGesture(MotionEvent event) {
if (gestureDetector != null && scaleDetector != null) {
gestureDetector.onTouchEvent(event);
scaleDetector.onTouchEvent(event);
}
}
private int getPointerIndex(MotionEvent event) {
return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
}
private int getPointerId(MotionEvent event) {
return event.getPointerId(getPointerIndex(event));
}
private void processEvent(TouchEvent event) {
// Add the touch event
androidInput.addEvent(event);
if (androidInput.isSimulateMouse()) {
InputEvent mouseEvent = generateMouseEvent(event);
if (mouseEvent != null) {
// Add the mouse event
androidInput.addEvent(mouseEvent);
}
}
}
// TODO: Ring Buffer for mouse events?
private InputEvent generateMouseEvent(TouchEvent event) {
InputEvent inputEvent = null;
int newX;
int newY;
int newDX;
int newDY;
if (androidInput.isMouseEventsInvertX()) {
newX = (int) (androidInput.invertX(event.getX()));
newDX = (int)event.getDeltaX() * -1;
} else {
newX = (int) event.getX();
newDX = (int)event.getDeltaX();
}
int wheel = (int) (event.getScaleSpan()); // might need to scale to match mouse wheel
int dWheel = (int) (event.getDeltaScaleSpan()); // might need to scale to match mouse wheel
if (androidInput.isMouseEventsInvertY()) {
newY = (int) (androidInput.invertY(event.getY()));
newDY = (int)event.getDeltaY() * -1;
} else {
newY = (int) event.getY();
newDY = (int)event.getDeltaY();
}
switch (event.getType()) {
case SCALE_MOVE:
inputEvent = new MouseMotionEvent(newX, newY, newDX, newDY, wheel, dWheel);
inputEvent.setTime(event.getTime());
break;
}
return inputEvent;
}
/* Events from onGestureListener */
public boolean onDown(MotionEvent event) {
// start of all GestureListeners. Not really a gesture by itself
// so we don't create an event.
// However, reset the scaleInProgress here since this is the beginning
// of a series of gesture events.
// logger.log(Level.INFO, "onDown pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
gestureDownX = androidInput.getJmeX(event.getX());
gestureDownY = androidInput.invertY(androidInput.getJmeY(event.getY()));
return true;
}
public boolean onSingleTapUp(MotionEvent event) {
// Up of single tap. May be followed by a double tap later.
// use onSingleTapConfirmed instead.
// logger.log(Level.INFO, "onSingleTapUp pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
return true;
}
public void onShowPress(MotionEvent event) {
// logger.log(Level.INFO, "onShowPress pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
float jmeX = androidInput.getJmeX(event.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SHOWPRESS, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(getPointerId(event));
touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure());
processEvent(touchEvent);
}
public void onLongPress(MotionEvent event) {
// logger.log(Level.INFO, "onLongPress pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
float jmeX = androidInput.getJmeX(event.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.LONGPRESSED, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(getPointerId(event));
touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure());
processEvent(touchEvent);
}
public boolean onScroll(MotionEvent startEvent, MotionEvent endEvent, float distX, float distY) {
// if not scaleInProgess, send scroll events. This is to avoid sending
// scroll events when one of the fingers is lifted just before the other one.
// Avoids sending the scroll for that brief period of time.
// Return true so that the next event doesn't accumulate the distX and distY values.
// Apparantly, both distX and distY are negative.
// Negate distX to get the real value, but leave distY negative to compensate
// for the fact that jME has y=0 at bottom where Android has y=0 at top.
// if (!scaleInProgress) {
if (!scaleDetector.isInProgress()) {
// logger.log(Level.INFO, "onScroll pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, dx: {7}, dy: {8}",
// new Object[]{getPointerId(startEvent), getAction(startEvent), startEvent.getX(), startEvent.getY(), getAction(endEvent), endEvent.getX(), endEvent.getY(), distX, distY});
float jmeX = androidInput.getJmeX(endEvent.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(endEvent.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SCROLL, jmeX, jmeY, androidInput.getJmeX(-distX), androidInput.getJmeY(distY));
touchEvent.setPointerId(getPointerId(endEvent));
touchEvent.setTime(endEvent.getEventTime());
touchEvent.setPressure(endEvent.getPressure());
processEvent(touchEvent);
}
return true;
}
public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float velocityX, float velocityY) {
// Fling happens only once at the end of the gesture (all fingers up).
// Fling returns the velocity of the finger movement in pixels/sec.
// Therefore, the dX and dY values are actually velocity instead of distance values
// Since this does not track the movement, use the start position and velocity values.
// logger.log(Level.INFO, "onFling pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, velocityX: {7}, velocityY: {8}",
// new Object[]{getPointerId(startEvent), getAction(startEvent), startEvent.getX(), startEvent.getY(), getAction(endEvent), endEvent.getX(), endEvent.getY(), velocityX, velocityY});
float jmeX = androidInput.getJmeX(startEvent.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(startEvent.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.FLING, jmeX, jmeY, velocityX, velocityY);
touchEvent.setPointerId(getPointerId(endEvent));
touchEvent.setTime(endEvent.getEventTime());
touchEvent.setPressure(endEvent.getPressure());
processEvent(touchEvent);
return true;
}
/* Events from onDoubleTapListener */
public boolean onSingleTapConfirmed(MotionEvent event) {
// Up of single tap when no double tap followed.
// logger.log(Level.INFO, "onSingleTapConfirmed pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
float jmeX = androidInput.getJmeX(event.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.TAP, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(getPointerId(event));
touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure());
processEvent(touchEvent);
return true;
}
public boolean onDoubleTap(MotionEvent event) {
//The down motion event of the first tap of the double-tap
// We could use this event to fire off a double tap event, or use
// DoubleTapEvent with a check for the UP action
// logger.log(Level.INFO, "onDoubleTap pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
float jmeX = androidInput.getJmeX(event.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.DOUBLETAP, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(getPointerId(event));
touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure());
processEvent(touchEvent);
return true;
}
public boolean onDoubleTapEvent(MotionEvent event) {
//Notified when an event within a double-tap gesture occurs, including the down, move(s), and up events.
// this means it will get called multiple times for a single double tap
// logger.log(Level.INFO, "onDoubleTapEvent pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
// if (getAction(event) == MotionEvent.ACTION_UP) {
// TouchEvent touchEvent = touchEventPool.getNextFreeEvent();
// touchEvent.set(TouchEvent.Type.DOUBLETAP, event.getX(), androidInput.invertY(event.getY()), 0, 0);
// touchEvent.setPointerId(getPointerId(event));
// touchEvent.setTime(event.getEventTime());
// touchEvent.setPressure(event.getPressure());
// processEvent(touchEvent);
// }
return true;
}
/* Events from ScaleGestureDetector */
public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
// Scale uses a focusX and focusY instead of x and y. Focus is the middle
// of the fingers. Therefore, use the x and y values from the Down event
// so that the x and y values don't jump to the middle position.
// return true or all gestures for this beginning event will be discarded
logger.log(Level.INFO, "onScaleBegin");
scaleStartX = gestureDownX;
scaleStartY = gestureDownY;
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SCALE_START, scaleStartX, scaleStartY, 0f, 0f);
touchEvent.setPointerId(0);
touchEvent.setTime(scaleGestureDetector.getEventTime());
touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
touchEvent.setDeltaScaleSpan(0f);
touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress());
processEvent(touchEvent);
return true;
}
public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
// return true or all gestures for this event will be accumulated
logger.log(Level.INFO, "onScale");
scaleStartX = gestureDownX;
scaleStartY = gestureDownY;
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SCALE_MOVE, scaleStartX, scaleStartY, 0f, 0f);
touchEvent.setPointerId(0);
touchEvent.setTime(scaleGestureDetector.getEventTime());
touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress());
processEvent(touchEvent);
return true;
}
public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
logger.log(Level.INFO, "onScaleEnd");
scaleStartX = gestureDownX;
scaleStartY = gestureDownY;
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SCALE_END, scaleStartX, scaleStartY, 0f, 0f);
touchEvent.setPointerId(0);
touchEvent.setTime(scaleGestureDetector.getEventTime());
touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress());
processEvent(touchEvent);
}
}

@ -0,0 +1,686 @@
package com.jme3.input.android;
import android.view.*;
import com.jme3.input.KeyInput;
import com.jme3.input.RawInputListener;
import com.jme3.input.TouchInput;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.input.event.MouseMotionEvent;
import com.jme3.input.event.TouchEvent;
import com.jme3.input.event.TouchEvent.Type;
import com.jme3.math.Vector2f;
import com.jme3.system.AppSettings;
import com.jme3.util.RingBuffer;
import java.util.HashMap;
import java.util.logging.Logger;
/**
* <code>AndroidInput</code> is one of the main components that connect jme with android. Is derived from GLSurfaceView and handles all Inputs
* @author larynx
*
*/
public class AndroidInput implements
TouchInput,
View.OnTouchListener,
View.OnKeyListener,
GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener,
ScaleGestureDetector.OnScaleGestureListener {
final private static int MAX_EVENTS = 1024;
// Custom settings
public boolean mouseEventsEnabled = true;
public boolean mouseEventsInvertX = false;
public boolean mouseEventsInvertY = false;
public boolean keyboardEventsEnabled = false;
public boolean dontSendHistory = false;
// Used to transfer events from android thread to GLThread
final private RingBuffer<TouchEvent> eventQueue = new RingBuffer<TouchEvent>(MAX_EVENTS);
final private RingBuffer<TouchEvent> eventPoolUnConsumed = new RingBuffer<TouchEvent>(MAX_EVENTS);
final private RingBuffer<TouchEvent> eventPool = new RingBuffer<TouchEvent>(MAX_EVENTS);
final private HashMap<Integer, Vector2f> lastPositions = new HashMap<Integer, Vector2f>();
// Internal
private View view;
private ScaleGestureDetector scaledetector;
private boolean scaleInProgress = false;
private GestureDetector detector;
private int lastX;
private int lastY;
private final static Logger logger = Logger.getLogger(AndroidInput.class.getName());
private boolean isInitialized = false;
private RawInputListener listener = null;
private static final int[] ANDROID_TO_JME = {
0x0, // unknown
0x0, // key code soft left
0x0, // key code soft right
KeyInput.KEY_HOME,
KeyInput.KEY_ESCAPE, // key back
0x0, // key call
0x0, // key endcall
KeyInput.KEY_0,
KeyInput.KEY_1,
KeyInput.KEY_2,
KeyInput.KEY_3,
KeyInput.KEY_4,
KeyInput.KEY_5,
KeyInput.KEY_6,
KeyInput.KEY_7,
KeyInput.KEY_8,
KeyInput.KEY_9,
KeyInput.KEY_MULTIPLY,
0x0, // key pound
KeyInput.KEY_UP,
KeyInput.KEY_DOWN,
KeyInput.KEY_LEFT,
KeyInput.KEY_RIGHT,
KeyInput.KEY_RETURN, // dpad center
0x0, // volume up
0x0, // volume down
KeyInput.KEY_POWER, // power (?)
0x0, // camera
0x0, // clear
KeyInput.KEY_A,
KeyInput.KEY_B,
KeyInput.KEY_C,
KeyInput.KEY_D,
KeyInput.KEY_E,
KeyInput.KEY_F,
KeyInput.KEY_G,
KeyInput.KEY_H,
KeyInput.KEY_I,
KeyInput.KEY_J,
KeyInput.KEY_K,
KeyInput.KEY_L,
KeyInput.KEY_M,
KeyInput.KEY_N,
KeyInput.KEY_O,
KeyInput.KEY_P,
KeyInput.KEY_Q,
KeyInput.KEY_R,
KeyInput.KEY_S,
KeyInput.KEY_T,
KeyInput.KEY_U,
KeyInput.KEY_V,
KeyInput.KEY_W,
KeyInput.KEY_X,
KeyInput.KEY_Y,
KeyInput.KEY_Z,
KeyInput.KEY_COMMA,
KeyInput.KEY_PERIOD,
KeyInput.KEY_LMENU,
KeyInput.KEY_RMENU,
KeyInput.KEY_LSHIFT,
KeyInput.KEY_RSHIFT,
// 0x0, // fn
// 0x0, // cap (?)
KeyInput.KEY_TAB,
KeyInput.KEY_SPACE,
0x0, // sym (?) symbol
0x0, // explorer
0x0, // envelope
KeyInput.KEY_RETURN, // newline/enter
KeyInput.KEY_DELETE,
KeyInput.KEY_GRAVE,
KeyInput.KEY_MINUS,
KeyInput.KEY_EQUALS,
KeyInput.KEY_LBRACKET,
KeyInput.KEY_RBRACKET,
KeyInput.KEY_BACKSLASH,
KeyInput.KEY_SEMICOLON,
KeyInput.KEY_APOSTROPHE,
KeyInput.KEY_SLASH,
KeyInput.KEY_AT, // at (@)
KeyInput.KEY_NUMLOCK, //0x0, // num
0x0, //headset hook
0x0, //focus
KeyInput.KEY_ADD,
KeyInput.KEY_LMETA, //menu
0x0,//notification
0x0,//search
0x0,//media play/pause
0x0,//media stop
0x0,//media next
0x0,//media previous
0x0,//media rewind
0x0,//media fastforward
0x0,//mute
};
public AndroidInput() {
}
public void setView(View view) {
this.view = view;
if (view != null) {
detector = new GestureDetector(null, this, null, false);
scaledetector = new ScaleGestureDetector(view.getContext(), this);
view.setOnTouchListener(this);
view.setOnKeyListener(this);
}
}
private TouchEvent getNextFreeTouchEvent() {
return getNextFreeTouchEvent(false);
}
/**
* Fetches a touch event from the reuse pool
* @param wait if true waits for a reusable event to get available/released
* by an other thread, if false returns a new one if needed.
*
* @return a usable TouchEvent
*/
private TouchEvent getNextFreeTouchEvent(boolean wait) {
TouchEvent evt = null;
synchronized (eventPoolUnConsumed) {
int size = eventPoolUnConsumed.size();
while (size > 0) {
evt = eventPoolUnConsumed.pop();
if (!evt.isConsumed()) {
eventPoolUnConsumed.push(evt);
evt = null;
} else {
break;
}
size--;
}
}
if (evt == null) {
if (eventPool.isEmpty() && wait) {
logger.warning("eventPool buffer underrun");
boolean isEmpty;
do {
synchronized (eventPool) {
isEmpty = eventPool.isEmpty();
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
} while (isEmpty);
synchronized (eventPool) {
evt = eventPool.pop();
}
} else if (eventPool.isEmpty()) {
evt = new TouchEvent();
logger.warning("eventPool buffer underrun");
} else {
synchronized (eventPool) {
evt = eventPool.pop();
}
}
}
return evt;
}
/**
* onTouch gets called from android thread on touchpad events
*/
public boolean onTouch(View view, MotionEvent event) {
if (view != this.view) {
return false;
}
boolean bWasHandled = false;
TouchEvent touch;
// System.out.println("native : " + event.getAction());
int action = event.getAction() & MotionEvent.ACTION_MASK;
int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
int pointerId = event.getPointerId(pointerIndex);
Vector2f lastPos = lastPositions.get(pointerId);
// final int historySize = event.getHistorySize();
//final int pointerCount = event.getPointerCount();
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
touch = getNextFreeTouchEvent();
touch.set(Type.DOWN, event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex), 0, 0);
touch.setPointerId(pointerId);
touch.setTime(event.getEventTime());
touch.setPressure(event.getPressure(pointerIndex));
processEvent(touch);
lastPos = new Vector2f(event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex));
lastPositions.put(pointerId, lastPos);
bWasHandled = true;
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
touch = getNextFreeTouchEvent();
touch.set(Type.UP, event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex), 0, 0);
touch.setPointerId(pointerId);
touch.setTime(event.getEventTime());
touch.setPressure(event.getPressure(pointerIndex));
processEvent(touch);
lastPositions.remove(pointerId);
bWasHandled = true;
break;
case MotionEvent.ACTION_MOVE:
// Convert all pointers into events
for (int p = 0; p < event.getPointerCount(); p++) {
lastPos = lastPositions.get(event.getPointerId(p));
if (lastPos == null) {
lastPos = new Vector2f(event.getX(p), view.getHeight() - event.getY(p));
lastPositions.put(event.getPointerId(p), lastPos);
}
float dX = event.getX(p) - lastPos.x;
float dY = view.getHeight() - event.getY(p) - lastPos.y;
if (dX != 0 || dY != 0) {
touch = getNextFreeTouchEvent();
touch.set(Type.MOVE, event.getX(p), view.getHeight() - event.getY(p), dX, dY);
touch.setPointerId(event.getPointerId(p));
touch.setTime(event.getEventTime());
touch.setPressure(event.getPressure(p));
touch.setScaleSpanInProgress(scaleInProgress);
processEvent(touch);
lastPos.set(event.getX(p), view.getHeight() - event.getY(p));
}
}
bWasHandled = true;
break;
case MotionEvent.ACTION_OUTSIDE:
break;
}
// Try to detect gestures
this.detector.onTouchEvent(event);
this.scaledetector.onTouchEvent(event);
return bWasHandled;
}
/**
* onKey gets called from android thread on key events
*/
public boolean onKey(View view, int keyCode, KeyEvent event) {
if (view != this.view) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
TouchEvent evt;
evt = getNextFreeTouchEvent();
evt.set(TouchEvent.Type.KEY_DOWN);
evt.setKeyCode(keyCode);
evt.setCharacters(event.getCharacters());
evt.setTime(event.getEventTime());
// Send the event
processEvent(evt);
// Handle all keys ourself except Volume Up/Down
if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) {
return false;
} else {
return true;
}
} else if (event.getAction() == KeyEvent.ACTION_UP) {
TouchEvent evt;
evt = getNextFreeTouchEvent();
evt.set(TouchEvent.Type.KEY_UP);
evt.setKeyCode(keyCode);
evt.setCharacters(event.getCharacters());
evt.setTime(event.getEventTime());
// Send the event
processEvent(evt);
// Handle all keys ourself except Volume Up/Down
if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) {
return false;
} else {
return true;
}
} else {
return false;
}
}
public void loadSettings(AppSettings settings) {
mouseEventsEnabled = settings.isEmulateMouse();
mouseEventsInvertX = settings.isEmulateMouseFlipX();
mouseEventsInvertY = settings.isEmulateMouseFlipY();
}
// -----------------------------------------
// JME3 Input interface
@Override
public void initialize() {
TouchEvent item;
for (int i = 0; i < MAX_EVENTS; i++) {
item = new TouchEvent();
eventPool.push(item);
}
isInitialized = true;
}
@Override
public void destroy() {
isInitialized = false;
// Clean up queues
while (!eventPool.isEmpty()) {
eventPool.pop();
}
while (!eventQueue.isEmpty()) {
eventQueue.pop();
}
this.view = null;
}
@Override
public boolean isInitialized() {
return isInitialized;
}
@Override
public void setInputListener(RawInputListener listener) {
this.listener = listener;
}
@Override
public long getInputTimeNanos() {
return System.nanoTime();
}
// -----------------------------------------
private void processEvent(TouchEvent event) {
synchronized (eventQueue) {
//Discarding events when the ring buffer is full to avoid buffer overflow.
if(eventQueue.size()< MAX_EVENTS){
eventQueue.push(event);
}
}
}
// --------------- INSIDE GLThread ---------------
@Override
public void update() {
generateEvents();
}
private void generateEvents() {
if (listener != null) {
TouchEvent event;
MouseButtonEvent btn;
MouseMotionEvent mot;
int newX;
int newY;
while (!eventQueue.isEmpty()) {
synchronized (eventQueue) {
event = eventQueue.pop();
}
if (event != null) {
listener.onTouchEvent(event);
if (mouseEventsEnabled) {
if (mouseEventsInvertX) {
newX = view.getWidth() - (int) event.getX();
} else {
newX = (int) event.getX();
}
if (mouseEventsInvertY) {
newY = view.getHeight() - (int) event.getY();
} else {
newY = (int) event.getY();
}
switch (event.getType()) {
case DOWN:
// Handle mouse down event
btn = new MouseButtonEvent(0, true, newX, newY);
btn.setTime(event.getTime());
listener.onMouseButtonEvent(btn);
// Store current pos
lastX = -1;
lastY = -1;
break;
case UP:
// Handle mouse up event
btn = new MouseButtonEvent(0, false, newX, newY);
btn.setTime(event.getTime());
listener.onMouseButtonEvent(btn);
// Store current pos
lastX = -1;
lastY = -1;
break;
case SCALE_MOVE:
if (lastX != -1 && lastY != -1) {
newX = lastX;
newY = lastY;
}
int wheel = (int) (event.getScaleSpan() / 4f); // scale to match mouse wheel
int dwheel = (int) (event.getDeltaScaleSpan() / 4f); // scale to match mouse wheel
mot = new MouseMotionEvent(newX, newX, 0, 0, wheel, dwheel);
mot.setTime(event.getTime());
listener.onMouseMotionEvent(mot);
lastX = newX;
lastY = newY;
break;
case MOVE:
if (event.isScaleSpanInProgress()) {
break;
}
int dx;
int dy;
if (lastX != -1) {
dx = newX - lastX;
dy = newY - lastY;
} else {
dx = 0;
dy = 0;
}
mot = new MouseMotionEvent(newX, newY, dx, dy, (int)event.getScaleSpan(), (int)event.getDeltaScaleSpan());
mot.setTime(event.getTime());
listener.onMouseMotionEvent(mot);
lastX = newX;
lastY = newY;
break;
}
}
}
if (event.isConsumed() == false) {
synchronized (eventPoolUnConsumed) {
eventPoolUnConsumed.push(event);
}
} else {
synchronized (eventPool) {
eventPool.push(event);
}
}
}
}
}
// --------------- ENDOF INSIDE GLThread ---------------
// --------------- Gesture detected callback events ---------------
public boolean onDown(MotionEvent event) {
return false;
}
public void onLongPress(MotionEvent event) {
TouchEvent touch = getNextFreeTouchEvent();
touch.set(Type.LONGPRESSED, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
}
public boolean onFling(MotionEvent event, MotionEvent event2, float vx, float vy) {
TouchEvent touch = getNextFreeTouchEvent();
touch.set(Type.FLING, event.getX(), view.getHeight() - event.getY(), vx, vy);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
return true;
}
public boolean onSingleTapConfirmed(MotionEvent event) {
//Nothing to do here the tap has already been detected.
return false;
}
public boolean onDoubleTap(MotionEvent event) {
TouchEvent touch = getNextFreeTouchEvent();
touch.set(Type.DOUBLETAP, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
return true;
}
public boolean onDoubleTapEvent(MotionEvent event) {
return false;
}
public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
scaleInProgress = true;
TouchEvent touch = getNextFreeTouchEvent();
touch.set(Type.SCALE_START, scaleGestureDetector.getFocusX(), scaleGestureDetector.getFocusY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(scaleGestureDetector.getEventTime());
touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
touch.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
touch.setScaleFactor(scaleGestureDetector.getScaleFactor());
touch.setScaleSpanInProgress(scaleInProgress);
processEvent(touch);
// System.out.println("scaleBegin");
return true;
}
public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
TouchEvent touch = getNextFreeTouchEvent();
touch.set(Type.SCALE_MOVE, scaleGestureDetector.getFocusX(), view.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(scaleGestureDetector.getEventTime());
touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
touch.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
touch.setScaleFactor(scaleGestureDetector.getScaleFactor());
touch.setScaleSpanInProgress(scaleInProgress);
processEvent(touch);
// System.out.println("scale");
return false;
}
public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
scaleInProgress = false;
TouchEvent touch = getNextFreeTouchEvent();
touch.set(Type.SCALE_END, scaleGestureDetector.getFocusX(), view.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(scaleGestureDetector.getEventTime());
touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
touch.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
touch.setScaleFactor(scaleGestureDetector.getScaleFactor());
touch.setScaleSpanInProgress(scaleInProgress);
processEvent(touch);
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
TouchEvent touch = getNextFreeTouchEvent();
touch.set(Type.SCROLL, e1.getX(), view.getHeight() - e1.getY(), distanceX, distanceY * (-1));
touch.setPointerId(0);
touch.setTime(e1.getEventTime());
processEvent(touch);
//System.out.println("scroll " + e1.getPointerCount());
return false;
}
public void onShowPress(MotionEvent event) {
TouchEvent touch = getNextFreeTouchEvent();
touch.set(Type.SHOWPRESS, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
}
public boolean onSingleTapUp(MotionEvent event) {
TouchEvent touch = getNextFreeTouchEvent();
touch.set(Type.TAP, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
return true;
}
@Override
public void setSimulateKeyboard(boolean simulate) {
keyboardEventsEnabled = simulate;
}
@Override
public void setOmitHistoricEvents(boolean dontSendHistory) {
this.dontSendHistory = dontSendHistory;
}
/**
* @deprecated Use {@link #getSimulateMouse()};
*/
@Deprecated
public boolean isMouseEventsEnabled() {
return mouseEventsEnabled;
}
@Deprecated
public void setMouseEventsEnabled(boolean mouseEventsEnabled) {
this.mouseEventsEnabled = mouseEventsEnabled;
}
public boolean isMouseEventsInvertY() {
return mouseEventsInvertY;
}
public void setMouseEventsInvertY(boolean mouseEventsInvertY) {
this.mouseEventsInvertY = mouseEventsInvertY;
}
public boolean isMouseEventsInvertX() {
return mouseEventsInvertX;
}
public void setMouseEventsInvertX(boolean mouseEventsInvertX) {
this.mouseEventsInvertX = mouseEventsInvertX;
}
public void setSimulateMouse(boolean simulate) {
mouseEventsEnabled = simulate;
}
public boolean getSimulateMouse() {
return isSimulateMouse();
}
public boolean isSimulateMouse() {
return mouseEventsEnabled;
}
public void showVirtualKeyboard(boolean visible) {
throw new UnsupportedOperationException("Not supported yet.");
}
}

@ -0,0 +1,273 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.input.android;
import android.os.Build;
import android.view.View;
import com.jme3.input.RawInputListener;
import com.jme3.input.TouchInput;
import com.jme3.input.event.InputEvent;
import com.jme3.input.event.KeyInputEvent;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.input.event.MouseMotionEvent;
import com.jme3.input.event.TouchEvent;
import com.jme3.renderer.android.AndroidGLSurfaceView;
import com.jme3.system.AppSettings;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <code>AndroidInput</code> is the main class that connects the Android system
* inputs to jME. It serves as the manager that gathers inputs from the various
* Android input methods and provides them to jME's <code>InputManager</code>.
*
* @author iwgeric
*/
public class AndroidInputHandler implements TouchInput {
private static final Logger logger = Logger.getLogger(AndroidInputHandler.class.getName());
// Custom settings
private boolean mouseEventsEnabled = true;
private boolean mouseEventsInvertX = false;
private boolean mouseEventsInvertY = false;
private boolean keyboardEventsEnabled = false;
private boolean joystickEventsEnabled = false;
private boolean dontSendHistory = false;
// Internal
private AndroidGLSurfaceView view;
private AndroidTouchHandler touchHandler;
private AndroidKeyHandler keyHandler;
private AndroidGestureHandler gestureHandler;
private boolean initialized = false;
private RawInputListener listener = null;
private ConcurrentLinkedQueue<InputEvent> inputEventQueue = new ConcurrentLinkedQueue<InputEvent>();
private final static int MAX_TOUCH_EVENTS = 1024;
private final TouchEventPool touchEventPool = new TouchEventPool(MAX_TOUCH_EVENTS);
private float scaleX = 1f;
private float scaleY = 1f;
public AndroidInputHandler() {
int buildVersion = Build.VERSION.SDK_INT;
logger.log(Level.INFO, "Android Build Version: {0}", buildVersion);
if (buildVersion >= 14) {
// add support for onHover and GenericMotionEvent (ie. gamepads)
gestureHandler = new AndroidGestureHandler(this);
touchHandler = new AndroidTouchHandler14(this, gestureHandler);
keyHandler = new AndroidKeyHandler(this);
} else if (buildVersion >= 8){
gestureHandler = new AndroidGestureHandler(this);
touchHandler = new AndroidTouchHandler(this, gestureHandler);
keyHandler = new AndroidKeyHandler(this);
}
}
public AndroidInputHandler(AndroidTouchHandler touchInput,
AndroidKeyHandler keyInput, AndroidGestureHandler gestureHandler) {
this.touchHandler = touchInput;
this.keyHandler = keyInput;
this.gestureHandler = gestureHandler;
}
public void setView(View view) {
if (touchHandler != null) {
touchHandler.setView(view);
}
if (keyHandler != null) {
keyHandler.setView(view);
}
if (gestureHandler != null) {
gestureHandler.setView(view);
}
this.view = (AndroidGLSurfaceView)view;
}
public View getView() {
return view;
}
public float invertX(float origX) {
return getJmeX(view.getWidth()) - origX;
}
public float invertY(float origY) {
return getJmeY(view.getHeight()) - origY;
}
public float getJmeX(float origX) {
return origX * scaleX;
}
public float getJmeY(float origY) {
return origY * scaleY;
}
public void loadSettings(AppSettings settings) {
// TODO: add simulate keyboard to settings
// keyboardEventsEnabled = true;
mouseEventsEnabled = settings.isEmulateMouse();
mouseEventsInvertX = settings.isEmulateMouseFlipX();
mouseEventsInvertY = settings.isEmulateMouseFlipY();
joystickEventsEnabled = settings.useJoysticks();
// view width and height are 0 until the view is displayed on the screen
if (view.getWidth() != 0 && view.getHeight() != 0) {
scaleX = (float)settings.getWidth() / (float)view.getWidth();
scaleY = (float)settings.getHeight() / (float)view.getHeight();
}
logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}",
new Object[]{scaleX, scaleY});
}
// -----------------------------------------
// JME3 Input interface
@Override
public void initialize() {
touchEventPool.initialize();
if (touchHandler != null) {
touchHandler.initialize();
}
if (keyHandler != null) {
keyHandler.initialize();
}
if (gestureHandler != null) {
gestureHandler.initialize();
}
initialized = true;
}
@Override
public void destroy() {
initialized = false;
touchEventPool.destroy();
if (touchHandler != null) {
touchHandler.destroy();
}
if (keyHandler != null) {
keyHandler.destroy();
}
if (gestureHandler != null) {
gestureHandler.destroy();
}
setView(null);
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void setInputListener(RawInputListener listener) {
this.listener = listener;
}
@Override
public long getInputTimeNanos() {
return System.nanoTime();
}
public void update() {
if (listener != null) {
InputEvent inputEvent;
while ((inputEvent = inputEventQueue.poll()) != null) {
if (inputEvent instanceof TouchEvent) {
listener.onTouchEvent((TouchEvent)inputEvent);
} else if (inputEvent instanceof MouseButtonEvent) {
listener.onMouseButtonEvent((MouseButtonEvent)inputEvent);
} else if (inputEvent instanceof MouseMotionEvent) {
listener.onMouseMotionEvent((MouseMotionEvent)inputEvent);
} else if (inputEvent instanceof KeyInputEvent) {
listener.onKeyEvent((KeyInputEvent)inputEvent);
}
}
}
}
// -----------------------------------------
public TouchEvent getFreeTouchEvent() {
return touchEventPool.getNextFreeEvent();
}
public void addEvent(InputEvent event) {
inputEventQueue.add(event);
if (event instanceof TouchEvent) {
touchEventPool.storeEvent((TouchEvent)event);
}
}
public void setSimulateMouse(boolean simulate) {
this.mouseEventsEnabled = simulate;
}
public boolean isSimulateMouse() {
return mouseEventsEnabled;
}
public boolean getSimulateMouse() {
return mouseEventsEnabled;
}
public boolean isMouseEventsInvertX() {
return mouseEventsInvertX;
}
public boolean isMouseEventsInvertY() {
return mouseEventsInvertY;
}
public void setSimulateKeyboard(boolean simulate) {
this.keyboardEventsEnabled = simulate;
}
public void setOmitHistoricEvents(boolean dontSendHistory) {
this.dontSendHistory = dontSendHistory;
}
public void showVirtualKeyboard(boolean visible) {
if (keyHandler != null) {
keyHandler.showVirtualKeyboard(visible);
}
}
}

@ -0,0 +1,156 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.input.android;
import android.content.Context;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import com.jme3.input.event.KeyInputEvent;
import com.jme3.input.event.TouchEvent;
import java.util.logging.Logger;
/**
* AndroidKeyHandler recieves onKey events from the Android system and creates
* the jME KeyEvents. onKey is used by Android to receive keys from the keyboard
* or device buttons. All key events are consumed by jME except for the Volume
* buttons and menu button.
*
* This class also provides the functionality to display or hide the soft keyboard
* for inputing single key events. Use OGLESContext to display an dialog to type
* in complete strings.
*
* @author iwgeric
*/
public class AndroidKeyHandler implements View.OnKeyListener {
private static final Logger logger = Logger.getLogger(AndroidKeyHandler.class.getName());
private AndroidInputHandler androidInput;
private boolean sendKeyEvents = true;
public AndroidKeyHandler(AndroidInputHandler androidInput) {
this.androidInput = androidInput;
}
public void initialize() {
}
public void destroy() {
}
public void setView(View view) {
if (view != null) {
view.setOnKeyListener(this);
} else {
androidInput.getView().setOnKeyListener(null);
}
}
/**
* onKey gets called from android thread on key events
*/
public boolean onKey(View view, int keyCode, KeyEvent event) {
if (androidInput.isInitialized() && view != androidInput.getView()) {
return false;
}
TouchEvent evt;
// TODO: get touch event from pool
if (event.getAction() == KeyEvent.ACTION_DOWN) {
evt = new TouchEvent();
evt.set(TouchEvent.Type.KEY_DOWN);
evt.setKeyCode(keyCode);
evt.setCharacters(event.getCharacters());
evt.setTime(event.getEventTime());
// Send the event
androidInput.addEvent(evt);
} else if (event.getAction() == KeyEvent.ACTION_UP) {
evt = new TouchEvent();
evt.set(TouchEvent.Type.KEY_UP);
evt.setKeyCode(keyCode);
evt.setCharacters(event.getCharacters());
evt.setTime(event.getEventTime());
// Send the event
androidInput.addEvent(evt);
}
KeyInputEvent kie;
char unicodeChar = (char)event.getUnicodeChar();
int jmeKeyCode = AndroidKeyMapping.getJmeKey(keyCode);
boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
boolean repeating = pressed && event.getRepeatCount() > 0;
kie = new KeyInputEvent(jmeKeyCode, unicodeChar, pressed, repeating);
kie.setTime(event.getEventTime());
androidInput.addEvent(kie);
// logger.log(Level.FINE, "onKey keyCode: {0}, jmeKeyCode: {1}, pressed: {2}, repeating: {3}",
// new Object[]{keyCode, jmeKeyCode, pressed, repeating});
// logger.log(Level.FINE, "creating KeyInputEvent: {0}", kie);
// consume all keys ourself except Volume Up/Down and Menu
// Don't do Menu so that typical Android Menus can be created and used
// by the user in MainActivity
if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) ||
(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) ||
(keyCode == KeyEvent.KEYCODE_MENU)) {
return false;
} else {
return true;
}
}
public void showVirtualKeyboard (final boolean visible) {
androidInput.getView().getHandler().post(new Runnable() {
public void run() {
InputMethodManager manager =
(InputMethodManager)androidInput.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (visible) {
manager.showSoftInput(androidInput.getView(), 0);
sendKeyEvents = true;
} else {
manager.hideSoftInputFromWindow(androidInput.getView().getWindowToken(), 0);
sendKeyEvents = false;
}
}
});
}
}

@ -0,0 +1,149 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.input.android;
import com.jme3.input.KeyInput;
import java.util.logging.Logger;
/**
* AndroidKeyMapping is just a utility to convert the Android keyCodes into
* jME KeyCodes received in jME's KeyEvent will match between Desktop and Android.
*
* @author iwgeric
*/
public class AndroidKeyMapping {
private static final Logger logger = Logger.getLogger(AndroidKeyMapping.class.getName());
private static final int[] ANDROID_TO_JME = {
0x0, // unknown
0x0, // key code soft left
0x0, // key code soft right
KeyInput.KEY_HOME,
KeyInput.KEY_ESCAPE, // key back
0x0, // key call
0x0, // key endcall
KeyInput.KEY_0,
KeyInput.KEY_1,
KeyInput.KEY_2,
KeyInput.KEY_3,
KeyInput.KEY_4,
KeyInput.KEY_5,
KeyInput.KEY_6,
KeyInput.KEY_7,
KeyInput.KEY_8,
KeyInput.KEY_9,
KeyInput.KEY_MULTIPLY,
0x0, // key pound
KeyInput.KEY_UP,
KeyInput.KEY_DOWN,
KeyInput.KEY_LEFT,
KeyInput.KEY_RIGHT,
KeyInput.KEY_RETURN, // dpad center
0x0, // volume up
0x0, // volume down
KeyInput.KEY_POWER, // power (?)
0x0, // camera
0x0, // clear
KeyInput.KEY_A,
KeyInput.KEY_B,
KeyInput.KEY_C,
KeyInput.KEY_D,
KeyInput.KEY_E,
KeyInput.KEY_F,
KeyInput.KEY_G,
KeyInput.KEY_H,
KeyInput.KEY_I,
KeyInput.KEY_J,
KeyInput.KEY_K,
KeyInput.KEY_L,
KeyInput.KEY_M,
KeyInput.KEY_N,
KeyInput.KEY_O,
KeyInput.KEY_P,
KeyInput.KEY_Q,
KeyInput.KEY_R,
KeyInput.KEY_S,
KeyInput.KEY_T,
KeyInput.KEY_U,
KeyInput.KEY_V,
KeyInput.KEY_W,
KeyInput.KEY_X,
KeyInput.KEY_Y,
KeyInput.KEY_Z,
KeyInput.KEY_COMMA,
KeyInput.KEY_PERIOD,
KeyInput.KEY_LMENU,
KeyInput.KEY_RMENU,
KeyInput.KEY_LSHIFT,
KeyInput.KEY_RSHIFT,
// 0x0, // fn
// 0x0, // cap (?)
KeyInput.KEY_TAB,
KeyInput.KEY_SPACE,
0x0, // sym (?) symbol
0x0, // explorer
0x0, // envelope
KeyInput.KEY_RETURN, // newline/enter
KeyInput.KEY_BACK, //used to be KeyInput.KEY_DELETE,
KeyInput.KEY_GRAVE,
KeyInput.KEY_MINUS,
KeyInput.KEY_EQUALS,
KeyInput.KEY_LBRACKET,
KeyInput.KEY_RBRACKET,
KeyInput.KEY_BACKSLASH,
KeyInput.KEY_SEMICOLON,
KeyInput.KEY_APOSTROPHE,
KeyInput.KEY_SLASH,
KeyInput.KEY_AT, // at (@)
KeyInput.KEY_NUMLOCK, //0x0, // num
0x0, //headset hook
0x0, //focus
KeyInput.KEY_ADD,
KeyInput.KEY_LMETA, //menu
0x0,//notification
0x0,//search
0x0,//media play/pause
0x0,//media stop
0x0,//media next
0x0,//media previous
0x0,//media rewind
0x0,//media fastforward
0x0,//mute
};
public static int getJmeKey(int androidKey) {
return ANDROID_TO_JME[androidKey];
}
}

@ -0,0 +1,795 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.input.android;
import android.app.Activity;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Vibrator;
import android.view.Surface;
import android.view.View;
import com.jme3.input.AbstractJoystick;
import com.jme3.input.DefaultJoystickAxis;
import com.jme3.input.InputManager;
import com.jme3.input.JoyInput;
import com.jme3.input.Joystick;
import com.jme3.input.JoystickAxis;
import com.jme3.input.SensorJoystickAxis;
import com.jme3.input.RawInputListener;
import com.jme3.input.event.JoyAxisEvent;
import com.jme3.math.FastMath;
import com.jme3.system.android.JmeAndroidSystem;
import com.jme3.util.IntMap;
import com.jme3.util.IntMap.Entry;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* AndroidSensorJoyInput converts the Android Sensor system into Joystick events.
* A single joystick is configured and includes data for all configured sensors
* as seperate axes of the joystick.
*
* Each axis is named accounting to the static strings in SensorJoystickAxis.
* Refer to the strings defined in SensorJoystickAxis for a list of supported
* sensors and their axis data. Each sensor type defined in SensorJoystickAxis
* will be attempted to be configured. If the device does not support a particular
* sensor, the axis will return null if joystick.getAxis(String name) is called.
*
* The joystick.getXAxis and getYAxis methods of the joystick are configured to
* return the device orientation values in the device's X and Y directions.
*
* This joystick also supports the joystick.rumble(rumbleAmount) method. In this
* case, when joystick.rumble(rumbleAmount) is called, the Android device will vibrate
* if the device has a built in vibrate motor.
*
* Because Andorid does not allow for the user to define the intensity of the
* vibration, the rumble amount (ie strength) is converted into vibration pulses
* The stronger the strength amount, the shorter the delay between pulses. If
* amount is 1, then the vibration stays on the whole time. If amount is 0.5,
* the vibration will a pulse of equal parts vibration and delay.
* To turn off vibration, set rumble amount to 0.
*
* MainActivity needs the following line to enable Joysticks on Android platforms
* joystickEventsEnabled = true;
* This is done to allow for battery conservation when sensor data is not required
* by the application.
*
* To use the joystick rumble feature, the following line needs to be
* added to the Android Manifest File
* <uses-permission android:name="android.permission.VIBRATE"/>
*
* @author iwgeric
*/
public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
private final static Logger logger = Logger.getLogger(AndroidSensorJoyInput.class.getName());
private Activity activity = null;
private InputManager inputManager = null;
private SensorManager sensorManager = null;
private Vibrator vibrator = null;
private boolean vibratorActive = false;
private long maxRumbleTime = 250; // 250ms
private RawInputListener listener = null;
private IntMap<SensorData> sensors = new IntMap<SensorData>();
private AndroidJoystick[] joysticks;
private int lastRotation = 0;
private boolean initialized = false;
private boolean loaded = false;
private final ArrayList<JoyAxisEvent> eventQueue = new ArrayList<JoyAxisEvent>();
/**
* Internal class to enclose data for each sensor.
*/
private class SensorData {
int androidSensorType = -1;
int androidSensorSpeed = SensorManager.SENSOR_DELAY_GAME;
Sensor sensor = null;
int sensorAccuracy = 0;
float[] lastValues;
final Object valuesLock = new Object();
ArrayList<AndroidJoystickAxis> axes = new ArrayList<AndroidJoystickAxis>();
boolean enabled = false;
boolean haveData = false;
public SensorData(int androidSensorType, Sensor sensor) {
this.androidSensorType = androidSensorType;
this.sensor = sensor;
}
}
private void initSensorManager() {
this.activity = JmeAndroidSystem.getActivity();
// Get instance of the SensorManager from the current Context
sensorManager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE);
// Get instance of Vibrator from current Context
vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator == null) {
logger.log(Level.FINE, "Vibrator Service not found.");
}
}
private SensorData initSensor(int sensorType) {
boolean success = false;
SensorData sensorData = sensors.get(sensorType);
if (sensorData != null) {
unRegisterListener(sensorType);
} else {
sensorData = new SensorData(sensorType, null);
sensors.put(sensorType, sensorData);
}
sensorData.androidSensorType = sensorType;
sensorData.sensor = sensorManager.getDefaultSensor(sensorType);
if (sensorData.sensor != null) {
logger.log(Level.FINE, "Sensor Type {0} found.", sensorType);
success = registerListener(sensorType);
} else {
logger.log(Level.FINE, "Sensor Type {0} not found.", sensorType);
}
if (success) {
return sensorData;
} else {
return null;
}
}
private boolean registerListener(int sensorType) {
SensorData sensorData = sensors.get(sensorType);
if (sensorData != null) {
if (sensorData.enabled) {
logger.log(Level.FINE, "Sensor Already Active: SensorType: {0}, active: {1}",
new Object[]{sensorType, sensorData.enabled});
return true;
}
sensorData.haveData = false;
if (sensorData.sensor != null) {
if (sensorManager.registerListener(this, sensorData.sensor, sensorData.androidSensorSpeed)) {
sensorData.enabled = true;
logger.log(Level.FINE, "SensorType: {0}, actived: {1}",
new Object[]{sensorType, sensorData.enabled});
return true;
} else {
sensorData.enabled = false;
logger.log(Level.FINE, "Sensor Type {0} activation failed.", sensorType);
}
}
}
return false;
}
private void unRegisterListener(int sensorType) {
SensorData sensorData = sensors.get(sensorType);
if (sensorData != null) {
if (sensorData.sensor != null) {
sensorManager.unregisterListener(this, sensorData.sensor);
}
sensorData.enabled = false;
sensorData.haveData = false;
logger.log(Level.FINE, "SensorType: {0} deactivated, active: {1}",
new Object[]{sensorType, sensorData.enabled});
}
}
/**
* Pauses the sensors to save battery life if the sensors are not needed.
* Used to pause sensors when the activity pauses
*/
public void pauseSensors() {
for (Entry entry: sensors) {
if (entry.getKey() != Sensor.TYPE_ORIENTATION) {
unRegisterListener(entry.getKey());
}
}
if (vibrator != null && vibratorActive) {
vibrator.cancel();
}
}
/**
* Resumes the sensors.
* Used to resume sensors when the activity comes to the top of the stack
*/
public void resumeSensors() {
for (Entry entry: sensors) {
if (entry.getKey() != Sensor.TYPE_ORIENTATION) {
registerListener(entry.getKey());
}
}
}
/*
* Allows the orientation data to be rotated based on the current device
* rotation. This keeps the data aligned with the game when the user
* rotates the device during game play.
*
* Android remapCoordinateSystem from the Android docs
* remapCoordinateSystem(float[] inR, int X, int Y, float[] outR)
*
* @param inR the rotation matrix to be transformed. Usually it is the matrix
* returned by getRotationMatrix(float[], float[], float[], float[]).
*
* @param outR the transformed rotation matrix. inR and outR can be the same
* array, but it is not recommended for performance reason.
*
* X defines on which world (Earth) axis and direction the X axis of the device is mapped.
* Y defines on which world (Earth) axis and direction the Y axis of the device is mapped.
*
* @return True if successful
*/
private boolean remapCoordinates(float[] inR, float[] outR) {
int xDir = SensorManager.AXIS_X;
int yDir = SensorManager.AXIS_Y;
int curRotation = getScreenRotation();
if (lastRotation != curRotation) {
logger.log(Level.FINE, "Device Rotation changed to: {0}", curRotation);
}
lastRotation = curRotation;
// logger.log(Level.FINE, "Screen Rotation: {0}", getScreenRotation());
switch (getScreenRotation()) {
// device natural position
case Surface.ROTATION_0:
xDir = SensorManager.AXIS_X;
yDir = SensorManager.AXIS_Y;
break;
// device rotated 90 deg counterclockwise
case Surface.ROTATION_90:
xDir = SensorManager.AXIS_Y;
yDir = SensorManager.AXIS_MINUS_X;
break;
// device rotated 180 deg counterclockwise
case Surface.ROTATION_180:
xDir = SensorManager.AXIS_MINUS_X;
yDir = SensorManager.AXIS_MINUS_Y;
break;
// device rotated 270 deg counterclockwise
case Surface.ROTATION_270:
xDir = SensorManager.AXIS_MINUS_Y;
yDir = SensorManager.AXIS_X;
break;
default:
break;
}
return SensorManager.remapCoordinateSystem(inR, xDir, yDir, outR);
}
/**
* Returns the current device rotation.
* Surface.ROTATION_0 = device in natural default rotation
* Surface.ROTATION_90 = device in rotated 90deg counterclockwise
* Surface.ROTATION_180 = device in rotated 180deg counterclockwise
* Surface.ROTATION_270 = device in rotated 270deg counterclockwise
*
* When the Manifest locks the orientation, this value will not change during
* gametime, but if the orientation of the screen is based off the sensor,
* this value will change as the device is rotated.
* @return Current device rotation amount
*/
private int getScreenRotation() {
return activity.getWindowManager().getDefaultDisplay().getRotation();
}
/**
* Calculates the device orientation based off the data recieved from the
* Acceleration Sensor and Mangetic Field sensor
* Values are returned relative to the Earth.
*
* From the Android Doc
*
* Computes the device's orientation based on the rotation matrix. When it returns, the array values is filled with the result:
* values[0]: azimuth, rotation around the Z axis.
* values[1]: pitch, rotation around the X axis.
* values[2]: roll, rotation around the Y axis.
*
* The reference coordinate-system used is different from the world
* coordinate-system defined for the rotation matrix:
* X is defined as the vector product Y.Z (It is tangential to the ground at the device's current location and roughly points West).
* Y is tangential to the ground at the device's current location and points towards the magnetic North Pole.
* Z points towards the center of the Earth and is perpendicular to the ground.
*
* @return True if Orientation was calculated
*/
private boolean updateOrientation() {
SensorData sensorData;
AndroidJoystickAxis axis;
final float[] curInclinationMat = new float[16];
final float[] curRotationMat = new float[16];
final float[] rotatedRotationMat = new float[16];
final float[] accValues = new float[3];
final float[] magValues = new float[3];
final float[] orderedOrientation = new float[3];
// if the Gravity Sensor is available, use it for orientation, if not
// use the accelerometer
// NOTE: Seemed to work worse, so just using accelerometer
// sensorData = sensors.get(Sensor.TYPE_GRAVITY);
// if (sensorData == null) {
sensorData = sensors.get(Sensor.TYPE_ACCELEROMETER);
// }
if (sensorData == null || !sensorData.enabled || !sensorData.haveData) {
return false;
}
if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
return false;
}
synchronized(sensorData.valuesLock) {
accValues[0] = sensorData.lastValues[0];
accValues[1] = sensorData.lastValues[1];
accValues[2] = sensorData.lastValues[2];
}
sensorData = sensors.get(Sensor.TYPE_MAGNETIC_FIELD);
if (sensorData == null || !sensorData.enabled || !sensorData.haveData) {
return false;
}
if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
return false;
}
synchronized(sensorData.valuesLock) {
magValues[0] = sensorData.lastValues[0];
magValues[1] = sensorData.lastValues[1];
magValues[2] = sensorData.lastValues[2];
}
if (SensorManager.getRotationMatrix(curRotationMat, curInclinationMat, accValues, magValues)) {
final float [] orientValues = new float[3];
if (remapCoordinates(curRotationMat, rotatedRotationMat)) {
SensorManager.getOrientation(rotatedRotationMat, orientValues);
// logger.log(Level.FINE, "Orientation Values: {0}, {1}, {2}",
// new Object[]{orientValues[0], orientValues[1], orientValues[2]});
// need to reorder to make it x, y, z order instead of z, x, y order
orderedOrientation[0] = orientValues[1];
orderedOrientation[1] = orientValues[2];
orderedOrientation[2] = orientValues[0];
sensorData = sensors.get(Sensor.TYPE_ORIENTATION);
if (sensorData != null && sensorData.axes.size() > 0) {
for (int i=0; i<orderedOrientation.length; i++) {
axis = sensorData.axes.get(i);
if (axis != null) {
axis.setCurRawValue(orderedOrientation[i]);
if (!sensorData.haveData) {
sensorData.haveData = true;
} else {
synchronized (eventQueue){
if (axis.isChanged()) {
eventQueue.add(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
}
}
}
}
}
} else if (sensorData != null) {
if (!sensorData.haveData) {
sensorData.haveData = true;
}
}
return true;
} else {
logger.log(Level.FINE, "remapCoordinateSystem failed");
}
} else {
logger.log(Level.FINE, "getRotationMatrix returned false");
}
return false;
}
// Start of JoyInput methods
public void setJoyRumble(int joyId, float amount) {
// convert amount to pulses since Android doesn't allow intensity
if (vibrator != null) {
final long rumbleOnDur = (long)(amount * maxRumbleTime); // ms to pulse vibration on
final long rumbleOffDur = maxRumbleTime - rumbleOnDur; // ms to delay between pulses
final long[] rumblePattern = {
0, // start immediately
rumbleOnDur, // time to leave vibration on
rumbleOffDur // time to delay between vibrations
};
final int rumbleRepeatFrom = 0; // index into rumble pattern to repeat from
logger.log(Level.FINE, "Rumble amount: {0}, rumbleOnDur: {1}, rumbleOffDur: {2}",
new Object[]{amount, rumbleOnDur, rumbleOffDur});
if (rumbleOnDur > 0) {
vibrator.vibrate(rumblePattern, rumbleRepeatFrom);
vibratorActive = true;
} else {
vibrator.cancel();
vibratorActive = false;
}
}
}
public Joystick[] loadJoysticks(InputManager inputManager) {
this.inputManager = inputManager;
initSensorManager();
SensorData sensorData;
List<Joystick> list = new ArrayList<Joystick>();
AndroidJoystick joystick;
AndroidJoystickAxis axis;
joystick = new AndroidJoystick(inputManager,
this,
list.size(),
"AndroidSensorsJoystick");
list.add(joystick);
List<Sensor> availSensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
for (Sensor sensor: availSensors) {
logger.log(Level.FINE, "{0} Sensor is available, Type: {1}, Vendor: {2}, Version: {3}",
new Object[]{sensor.getName(), sensor.getType(), sensor.getVendor(), sensor.getVersion()});
}
// manually create orientation sensor data since orientation is not a physical sensor
sensorData = new SensorData(Sensor.TYPE_ORIENTATION, null);
sensorData.lastValues = new float[3];
sensors.put(Sensor.TYPE_ORIENTATION, sensorData);
axis = joystick.addAxis(SensorJoystickAxis.ORIENTATION_X, SensorJoystickAxis.ORIENTATION_X, joystick.getAxisCount(), FastMath.HALF_PI);
joystick.setYAxis(axis); // joystick y axis = rotation around device x axis
sensorData.axes.add(axis);
axis = joystick.addAxis(SensorJoystickAxis.ORIENTATION_Y, SensorJoystickAxis.ORIENTATION_Y, joystick.getAxisCount(), FastMath.HALF_PI);
joystick.setXAxis(axis); // joystick x axis = rotation around device y axis
sensorData.axes.add(axis);
axis = joystick.addAxis(SensorJoystickAxis.ORIENTATION_Z, SensorJoystickAxis.ORIENTATION_Z, joystick.getAxisCount(), FastMath.HALF_PI);
sensorData.axes.add(axis);
// add axes for physical sensors
sensorData = initSensor(Sensor.TYPE_MAGNETIC_FIELD);
if (sensorData != null) {
sensorData.lastValues = new float[3];
sensors.put(Sensor.TYPE_MAGNETIC_FIELD, sensorData);
// axis = joystick.addAxis(SensorJoystickAxis.MAGNETIC_X, "MagneticField_X", joystick.getAxisCount(), 1f);
// sensorData.axes.add(axis);
// axis = joystick.addAxis(SensorJoystickAxis.MAGNETIC_Y, "MagneticField_Y", joystick.getAxisCount(), 1f);
// sensorData.axes.add(axis);
// axis = joystick.addAxis(SensorJoystickAxis.MAGNETIC_Z, "MagneticField_Z", joystick.getAxisCount(), 1f);
// sensorData.axes.add(axis);
}
sensorData = initSensor(Sensor.TYPE_ACCELEROMETER);
if (sensorData != null) {
sensorData.lastValues = new float[3];
sensors.put(Sensor.TYPE_ACCELEROMETER, sensorData);
// axis = joystick.addAxis(SensorJoystickAxis.ACCELEROMETER_X, "Accelerometer_X", joystick.getAxisCount(), 1f);
// sensorData.axes.add(axis);
// axis = joystick.addAxis(SensorJoystickAxis.ACCELEROMETER_Y, "Accelerometer_Y", joystick.getAxisCount(), 1f);
// sensorData.axes.add(axis);
// axis = joystick.addAxis(SensorJoystickAxis.ACCELEROMETER_Z, "Accelerometer_Z", joystick.getAxisCount(), 1f);
// sensorData.axes.add(axis);
}
// sensorData = initSensor(Sensor.TYPE_GYROSCOPE);
// if (sensorData != null) {
// sensorData.lastValues = new float[3];
// }
//
// sensorData = initSensor(Sensor.TYPE_GRAVITY);
// if (sensorData != null) {
// sensorData.lastValues = new float[3];
// }
//
// sensorData = initSensor(Sensor.TYPE_LINEAR_ACCELERATION);
// if (sensorData != null) {
// sensorData.lastValues = new float[3];
// }
//
// sensorData = initSensor(Sensor.TYPE_ROTATION_VECTOR);
// if (sensorData != null) {
// sensorData.lastValues = new float[4];
// }
//
// sensorData = initSensor(Sensor.TYPE_PROXIMITY);
// if (sensorData != null) {
// sensorData.lastValues = new float[1];
// }
//
// sensorData = initSensor(Sensor.TYPE_LIGHT);
// if (sensorData != null) {
// sensorData.lastValues = new float[1];
// }
//
// sensorData = initSensor(Sensor.TYPE_PRESSURE);
// if (sensorData != null) {
// sensorData.lastValues = new float[1];
// }
//
// sensorData = initSensor(Sensor.TYPE_TEMPERATURE);
// if (sensorData != null) {
// sensorData.lastValues = new float[1];
// }
joysticks = list.toArray( new AndroidJoystick[list.size()] );
loaded = true;
return joysticks;
}
public void initialize() {
initialized = true;
loaded = false;
}
public void update() {
if (!loaded) {
return;
}
updateOrientation();
synchronized (eventQueue){
// flush events to listener
if (listener != null && eventQueue.size() > 0) {
for (int i = 0; i < eventQueue.size(); i++){
listener.onJoyAxisEvent(eventQueue.get(i));
}
eventQueue.clear();
}
}
}
public void destroy() {
logger.log(Level.FINE, "Doing Destroy.");
pauseSensors();
if (sensorManager != null) {
sensorManager.unregisterListener(this);
}
sensors.clear();
eventQueue.clear();
initialized = false;
loaded = false;
joysticks = null;
sensorManager = null;
vibrator = null;
activity = null;
}
public boolean isInitialized() {
return initialized;
}
public void setInputListener(RawInputListener listener) {
this.listener = listener;
}
public long getInputTimeNanos() {
return System.nanoTime();
}
// End of JoyInput methods
// Start of Android SensorEventListener methods
public void onSensorChanged(SensorEvent se) {
if (!initialized || !loaded) {
return;
}
int sensorType = se.sensor.getType();
SensorData sensorData = sensors.get(sensorType);
if (sensorData != null && sensorData.sensor.equals(se.sensor) && sensorData.enabled) {
if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
return;
}
synchronized(sensorData.valuesLock) {
for (int i=0; i<sensorData.lastValues.length; i++) {
sensorData.lastValues[i] = se.values[i];
}
}
if (sensorData != null && sensorData.axes.size() > 0) {
AndroidJoystickAxis axis;
for (int i=0; i<se.values.length; i++) {
axis = sensorData.axes.get(i);
if (axis != null) {
axis.setCurRawValue(se.values[i]);
if (!sensorData.haveData) {
sensorData.haveData = true;
} else {
synchronized (eventQueue){
if (axis.isChanged()) {
eventQueue.add(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
}
}
}
}
}
} else if (sensorData != null) {
if (!sensorData.haveData) {
sensorData.haveData = true;
}
}
}
}
public void onAccuracyChanged(Sensor sensor, int i) {
int sensorType = sensor.getType();
SensorData sensorData = sensors.get(sensorType);
if (sensorData != null) {
logger.log(Level.FINE, "onAccuracyChanged for {0}: accuracy: {1}",
new Object[]{sensor.getName(), i});
logger.log(Level.FINE, "MaxRange: {0}, Resolution: {1}",
new Object[]{sensor.getMaximumRange(), sensor.getResolution()});
sensorData.sensorAccuracy = i;
}
}
// End of SensorEventListener methods
protected class AndroidJoystick extends AbstractJoystick {
private JoystickAxis nullAxis;
private JoystickAxis xAxis;
private JoystickAxis yAxis;
private JoystickAxis povX;
private JoystickAxis povY;
public AndroidJoystick( InputManager inputManager, JoyInput joyInput,
int joyId, String name){
super( inputManager, joyInput, joyId, name );
this.nullAxis = new DefaultJoystickAxis( getInputManager(), this, -1,
"Null", "null", false, false, 0 );
this.xAxis = nullAxis;
this.yAxis = nullAxis;
this.povX = nullAxis;
this.povY = nullAxis;
}
protected AndroidJoystickAxis addAxis(String axisName, String logicalName, int axisNum, float maxRawValue) {
AndroidJoystickAxis axis;
axis = new AndroidJoystickAxis(
inputManager, // InputManager (InputManager)
this, // parent Joystick (Joystick)
axisNum, // Axis Index (int)
axisName, // Axis Name (String)
logicalName, // Logical ID (String)
true, // isAnalog (boolean)
false, // isRelative (boolean)
0.01f, // Axis Deadzone (float)
maxRawValue); // Axis Max Raw Value (float)
super.addAxis(axis);
return axis;
}
protected void setXAxis(JoystickAxis axis) {
xAxis = axis;
}
protected void setYAxis(JoystickAxis axis) {
yAxis = axis;
}
@Override
public JoystickAxis getXAxis() {
return xAxis;
}
@Override
public JoystickAxis getYAxis() {
return yAxis;
}
@Override
public JoystickAxis getPovXAxis() {
return povX;
}
@Override
public JoystickAxis getPovYAxis() {
return povY;
}
}
public class AndroidJoystickAxis extends DefaultJoystickAxis implements SensorJoystickAxis {
float zeroRawValue = 0f;
float curRawValue = 0f;
float lastRawValue = 0f;
boolean hasChanged = false;
float maxRawValue = FastMath.HALF_PI;
boolean enabled = true;
public AndroidJoystickAxis(InputManager inputManager, Joystick parent,
int axisIndex, String name, String logicalId,
boolean isAnalog, boolean isRelative, float deadZone,
float maxRawValue) {
super(inputManager, parent, axisIndex, name, logicalId, isAnalog, isRelative, deadZone);
this.maxRawValue = maxRawValue;
}
public float getMaxRawValue() {
return maxRawValue;
}
public void setMaxRawValue(float maxRawValue) {
this.maxRawValue = maxRawValue;
}
protected float getLastRawValue() {
return lastRawValue;
}
protected void setCurRawValue(float rawValue) {
this.curRawValue = rawValue;
if (Math.abs(curRawValue - lastRawValue) > getDeadZone()) {
hasChanged = true;
lastRawValue = curRawValue;
} else {
hasChanged = false;
}
}
protected float getJoystickAxisValue() {
return (lastRawValue-zeroRawValue) / maxRawValue;
}
protected boolean isChanged() {
return hasChanged;
}
public void calibrateCenter() {
zeroRawValue = lastRawValue;
logger.log(Level.FINE, "Calibrating axis {0} to {1}",
new Object[]{getName(), zeroRawValue});
}
}
}

@ -0,0 +1,257 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.input.android;
import android.view.MotionEvent;
import android.view.View;
import com.jme3.input.event.InputEvent;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.input.event.MouseMotionEvent;
import com.jme3.input.event.TouchEvent;
import static com.jme3.input.event.TouchEvent.Type.DOWN;
import static com.jme3.input.event.TouchEvent.Type.MOVE;
import static com.jme3.input.event.TouchEvent.Type.UP;
import com.jme3.math.Vector2f;
import java.util.HashMap;
import java.util.logging.Logger;
/**
* AndroidTouchHandler is the base class that receives touch inputs from the
* Android system and creates the TouchEvents for jME. This class is designed
* to handle the base touch events for Android rev 9 (Android 2.3). This is
* extended by other classes to add features that were introducted after
* Android rev 9.
*
* @author iwgeric
*/
public class AndroidTouchHandler implements View.OnTouchListener {
private static final Logger logger = Logger.getLogger(AndroidTouchHandler.class.getName());
final private HashMap<Integer, Vector2f> lastPositions = new HashMap<Integer, Vector2f>();
protected int numPointers = 0;
protected AndroidInputHandler androidInput;
protected AndroidGestureHandler gestureHandler;
public AndroidTouchHandler(AndroidInputHandler androidInput, AndroidGestureHandler gestureHandler) {
this.androidInput = androidInput;
this.gestureHandler = gestureHandler;
}
public void initialize() {
}
public void destroy() {
setView(null);
}
public void setView(View view) {
if (view != null) {
view.setOnTouchListener(this);
} else {
androidInput.getView().setOnTouchListener(null);
}
}
protected int getPointerIndex(MotionEvent event) {
return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
}
protected int getPointerId(MotionEvent event) {
return event.getPointerId(getPointerIndex(event));
}
protected int getAction(MotionEvent event) {
return event.getAction() & MotionEvent.ACTION_MASK;
}
/**
* onTouch gets called from android thread on touch events
*/
public boolean onTouch(View view, MotionEvent event) {
if (!androidInput.isInitialized() || view != androidInput.getView()) {
return false;
}
boolean bWasHandled = false;
TouchEvent touch = null;
// System.out.println("native : " + event.getAction());
int action = getAction(event);
int pointerIndex = getPointerIndex(event);
int pointerId = getPointerId(event);
Vector2f lastPos = lastPositions.get(pointerId);
float jmeX;
float jmeY;
numPointers = event.getPointerCount();
// final int historySize = event.getHistorySize();
//final int pointerCount = event.getPointerCount();
switch (getAction(event)) {
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
jmeX = androidInput.getJmeX(event.getX(pointerIndex));
jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
touch = androidInput.getFreeTouchEvent();
touch.set(TouchEvent.Type.DOWN, jmeX, jmeY, 0, 0);
touch.setPointerId(pointerId);
touch.setTime(event.getEventTime());
touch.setPressure(event.getPressure(pointerIndex));
lastPos = new Vector2f(jmeX, jmeY);
lastPositions.put(pointerId, lastPos);
processEvent(touch);
bWasHandled = true;
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
jmeX = androidInput.getJmeX(event.getX(pointerIndex));
jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
touch = androidInput.getFreeTouchEvent();
touch.set(TouchEvent.Type.UP, jmeX, jmeY, 0, 0);
touch.setPointerId(pointerId);
touch.setTime(event.getEventTime());
touch.setPressure(event.getPressure(pointerIndex));
lastPositions.remove(pointerId);
processEvent(touch);
bWasHandled = true;
break;
case MotionEvent.ACTION_MOVE:
// Convert all pointers into events
for (int p = 0; p < event.getPointerCount(); p++) {
jmeX = androidInput.getJmeX(event.getX(p));
jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(p)));
lastPos = lastPositions.get(event.getPointerId(p));
if (lastPos == null) {
lastPos = new Vector2f(jmeX, jmeY);
lastPositions.put(event.getPointerId(p), lastPos);
}
float dX = jmeX - lastPos.x;
float dY = jmeY - lastPos.y;
if (dX != 0 || dY != 0) {
touch = androidInput.getFreeTouchEvent();
touch.set(TouchEvent.Type.MOVE, jmeX, jmeY, dX, dY);
touch.setPointerId(event.getPointerId(p));
touch.setTime(event.getEventTime());
touch.setPressure(event.getPressure(p));
lastPos.set(jmeX, jmeY);
processEvent(touch);
bWasHandled = true;
}
}
break;
case MotionEvent.ACTION_OUTSIDE:
break;
}
// Try to detect gestures
if (gestureHandler != null) {
gestureHandler.detectGesture(event);
}
return bWasHandled;
}
protected void processEvent(TouchEvent event) {
// Add the touch event
androidInput.addEvent(event);
// MouseEvents do not support multi-touch, so only evaluate 1 finger pointer events
if (androidInput.isSimulateMouse() && numPointers == 1) {
InputEvent mouseEvent = generateMouseEvent(event);
if (mouseEvent != null) {
// Add the mouse event
androidInput.addEvent(mouseEvent);
}
}
}
// TODO: Ring Buffer for mouse events?
protected InputEvent generateMouseEvent(TouchEvent event) {
InputEvent inputEvent = null;
int newX;
int newY;
int newDX;
int newDY;
if (androidInput.isMouseEventsInvertX()) {
newX = (int) (androidInput.invertX(event.getX()));
newDX = (int)event.getDeltaX() * -1;
} else {
newX = (int) event.getX();
newDX = (int)event.getDeltaX();
}
if (androidInput.isMouseEventsInvertY()) {
newY = (int) (androidInput.invertY(event.getY()));
newDY = (int)event.getDeltaY() * -1;
} else {
newY = (int) event.getY();
newDY = (int)event.getDeltaY();
}
switch (event.getType()) {
case DOWN:
// Handle mouse down event
inputEvent = new MouseButtonEvent(0, true, newX, newY);
inputEvent.setTime(event.getTime());
break;
case UP:
// Handle mouse up event
inputEvent = new MouseButtonEvent(0, false, newX, newY);
inputEvent.setTime(event.getTime());
break;
case HOVER_MOVE:
case MOVE:
inputEvent = new MouseMotionEvent(newX, newY, newDX, newDY, (int)event.getScaleSpan(), (int)event.getDeltaScaleSpan());
inputEvent.setTime(event.getTime());
break;
}
return inputEvent;
}
}

@ -0,0 +1,152 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.input.android;
import android.view.MotionEvent;
import android.view.View;
import com.jme3.input.event.TouchEvent;
import com.jme3.math.Vector2f;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* AndroidTouchHandler14 is an extension of AndroidTouchHander that adds the
* Android touch event functionality between Android rev 9 (Android 2.3) and
* Android rev 14 (Android 4.0).
*
* @author iwgeric
*/
public class AndroidTouchHandler14 extends AndroidTouchHandler implements
View.OnHoverListener {
private static final Logger logger = Logger.getLogger(AndroidTouchHandler14.class.getName());
final private HashMap<Integer, Vector2f> lastHoverPositions = new HashMap<Integer, Vector2f>();
public AndroidTouchHandler14(AndroidInputHandler androidInput, AndroidGestureHandler gestureHandler) {
super(androidInput, gestureHandler);
}
@Override
public void setView(View view) {
if (view != null) {
view.setOnHoverListener(this);
} else {
androidInput.getView().setOnHoverListener(null);
}
super.setView(view);
}
public boolean onHover(View view, MotionEvent event) {
if (view == null || view != androidInput.getView()) {
return false;
}
boolean consumed = false;
int action = getAction(event);
int pointerId = getPointerId(event);
int pointerIndex = getPointerIndex(event);
Vector2f lastPos = lastHoverPositions.get(pointerId);
float jmeX;
float jmeY;
numPointers = event.getPointerCount();
logger.log(Level.INFO, "onHover pointerId: {0}, action: {1}, x: {2}, y: {3}, numPointers: {4}",
new Object[]{pointerId, action, event.getX(), event.getY(), event.getPointerCount()});
TouchEvent touchEvent;
switch (action) {
case MotionEvent.ACTION_HOVER_ENTER:
jmeX = androidInput.getJmeX(event.getX(pointerIndex));
jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.HOVER_START, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(pointerId);
touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure(pointerIndex));
lastPos = new Vector2f(jmeX, jmeY);
lastHoverPositions.put(pointerId, lastPos);
processEvent(touchEvent);
consumed = true;
break;
case MotionEvent.ACTION_HOVER_MOVE:
// Convert all pointers into events
for (int p = 0; p < event.getPointerCount(); p++) {
jmeX = androidInput.getJmeX(event.getX(p));
jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(p)));
lastPos = lastHoverPositions.get(event.getPointerId(p));
if (lastPos == null) {
lastPos = new Vector2f(jmeX, jmeY);
lastHoverPositions.put(event.getPointerId(p), lastPos);
}
float dX = jmeX - lastPos.x;
float dY = jmeY - lastPos.y;
if (dX != 0 || dY != 0) {
touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.HOVER_MOVE, jmeX, jmeY, dX, dY);
touchEvent.setPointerId(event.getPointerId(p));
touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure(p));
lastPos.set(jmeX, jmeY);
processEvent(touchEvent);
}
}
consumed = true;
break;
case MotionEvent.ACTION_HOVER_EXIT:
jmeX = androidInput.getJmeX(event.getX(pointerIndex));
jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
touchEvent = androidInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.HOVER_END, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(pointerId);
touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure(pointerIndex));
lastHoverPositions.remove(pointerId);
processEvent(touchEvent);
consumed = true;
break;
default:
consumed = false;
break;
}
return consumed;
}
}

@ -0,0 +1,121 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.input.android;
import com.jme3.input.event.TouchEvent;
import com.jme3.util.RingBuffer;
import java.util.logging.Logger;
/**
* TouchEventPool provides a RingBuffer of jME TouchEvents to help with garbage
* collection on Android. Each TouchEvent is stored in the RingBuffer and is
* reused if the TouchEvent has been consumed.
*
* If a TouchEvent has not been consumed, it is placed back into the pool at the
* end for later use. If a TouchEvent has been consumed, it is reused to avoid
* creating lots of little objects.
*
* If the pool is full of unconsumed events, then a new event is created and provided.
*
*
* @author iwgeric
*/
public class TouchEventPool {
private static final Logger logger = Logger.getLogger(TouchEventPool.class.getName());
private final RingBuffer<TouchEvent> eventPool;
private final int maxEvents;
public TouchEventPool (int maxEvents) {
eventPool = new RingBuffer<TouchEvent>(maxEvents);
this.maxEvents = maxEvents;
}
public void initialize() {
TouchEvent newEvent;
while (!eventPool.isEmpty()) {
eventPool.pop();
}
for (int i = 0; i < maxEvents; i++) {
newEvent = new TouchEvent();
newEvent.setConsumed();
eventPool.push(newEvent);
}
}
public void destroy() {
// Clean up queues
while (!eventPool.isEmpty()) {
eventPool.pop();
}
}
/**
* Fetches a touch event from the reuse pool
*
* @return a usable TouchEvent
*/
public TouchEvent getNextFreeEvent() {
TouchEvent evt = null;
int curSize = eventPool.size();
while (curSize > 0) {
evt = (TouchEvent)eventPool.pop();
if (evt.isConsumed()) {
break;
} else {
eventPool.push(evt);
evt = null;
}
curSize--;
}
if (evt == null) {
logger.warning("eventPool full of unconsumed events");
evt = new TouchEvent();
}
return evt;
}
/**
* Stores the TouchEvent back in the pool for later reuse. It is only reused
* if the TouchEvent has been consumed.
*
* @param event TouchEvent to store for later use if consumed.
*/
public void storeEvent(TouchEvent event) {
if (eventPool.size() < maxEvents) {
eventPool.push(event);
} else {
logger.warning("eventPool full");
}
}
}

@ -0,0 +1,14 @@
package com.jme3.renderer.android;
import android.opengl.GLES20;
public class Android22Workaround {
public static void glVertexAttribPointer(int location, int components, int format, boolean normalize, int stride, int offset){
GLES20.glVertexAttribPointer(location,
components,
format,
normalize,
stride,
offset);
}
}

@ -0,0 +1,26 @@
package com.jme3.renderer.android;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import java.util.logging.Logger;
/**
* <code>AndroidGLSurfaceView</code> is derived from GLSurfaceView
* @author iwgeric
*
*/
public class AndroidGLSurfaceView extends GLSurfaceView {
private final static Logger logger = Logger.getLogger(AndroidGLSurfaceView.class.getName());
public AndroidGLSurfaceView(Context ctx, AttributeSet attribs) {
super(ctx, attribs);
}
public AndroidGLSurfaceView(Context ctx) {
super(ctx);
}
}

@ -0,0 +1,129 @@
package com.jme3.renderer.android;
import android.opengl.GLES20;
import android.opengl.GLU;
import com.jme3.renderer.RendererException;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGL11;
/**
* Utility class used by the {@link OGLESShaderRenderer renderer} and sister
* classes.
*
* @author Kirill Vainer
*/
public class RendererUtil {
/**
* When set to true, every OpenGL call will check for errors and throw an
* exception if there is one, if false, no error checking is performed.
*/
public static boolean ENABLE_ERROR_CHECKING = true;
/**
* Checks for an OpenGL error and throws a {@link RendererException} if
* there is one. Ignores the value of
* {@link RendererUtil#ENABLE_ERROR_CHECKING}.
*/
public static void checkGLErrorForced() {
int error = GLES20.glGetError();
if (error != 0) {
String message = GLU.gluErrorString(error);
if (message == null) {
throw new RendererException("An unknown OpenGL error has occurred.");
} else {
throw new RendererException("An OpenGL error has occurred: " + message);
}
}
}
/**
* Checks for an EGL error and throws a {@link RendererException} if there
* is one. Ignores the value of {@link RendererUtil#ENABLE_ERROR_CHECKING}.
*/
public static void checkEGLError(EGL10 egl) {
int error = egl.eglGetError();
if (error != EGL10.EGL_SUCCESS) {
String errorMessage;
switch (error) {
case EGL10.EGL_SUCCESS:
return;
case EGL10.EGL_NOT_INITIALIZED:
errorMessage = "EGL is not initialized, or could not be "
+ "initialized, for the specified EGL display connection. ";
break;
case EGL10.EGL_BAD_ACCESS:
errorMessage = "EGL cannot access a requested resource "
+ "(for example a context is bound in another thread). ";
break;
case EGL10.EGL_BAD_ALLOC:
errorMessage = "EGL failed to allocate resources for the requested operation.";
break;
case EGL10.EGL_BAD_ATTRIBUTE:
errorMessage = "An unrecognized attribute or attribute "
+ "value was passed in the attribute list. ";
break;
case EGL10.EGL_BAD_CONTEXT:
errorMessage = "An EGLContext argument does not name a valid EGL rendering context. ";
break;
case EGL10.EGL_BAD_CONFIG:
errorMessage = "An EGLConfig argument does not name a valid EGL frame buffer configuration. ";
break;
case EGL10.EGL_BAD_CURRENT_SURFACE:
errorMessage = "The current surface of the calling thread "
+ "is a window, pixel buffer or pixmap that is no longer valid. ";
break;
case EGL10.EGL_BAD_DISPLAY:
errorMessage = "An EGLDisplay argument does not name a valid EGL display connection. ";
break;
case EGL10.EGL_BAD_SURFACE:
errorMessage = "An EGLSurface argument does not name a "
+ "valid surface (window, pixel buffer or pixmap) configured for GL rendering. ";
break;
case EGL10.EGL_BAD_MATCH:
errorMessage = "Arguments are inconsistent (for example, a "
+ "valid context requires buffers not supplied by a valid surface). ";
break;
case EGL10.EGL_BAD_PARAMETER:
errorMessage = "One or more argument values are invalid.";
break;
case EGL10.EGL_BAD_NATIVE_PIXMAP:
errorMessage = "A NativePixmapType argument does not refer to a valid native pixmap. ";
break;
case EGL10.EGL_BAD_NATIVE_WINDOW:
errorMessage = "A NativeWindowType argument does not refer to a valid native window. ";
break;
case EGL11.EGL_CONTEXT_LOST:
errorMessage = "A power management event has occurred. "
+ "The application must destroy all contexts and reinitialise "
+ "OpenGL ES state and objects to continue rendering. ";
break;
default:
errorMessage = "Unknown";
}
throw new RendererException("EGL error 0x" + Integer.toHexString(error) + ": " + errorMessage);
}
}
/**
* Checks for an OpenGL error and throws a {@link RendererException} if
* there is one. Does nothing if {@link RendererUtil#ENABLE_ERROR_CHECKING}
* is set to
* <code>false</code>.
*/
public static void checkGLError() {
if (!ENABLE_ERROR_CHECKING) {
return;
}
int error = GLES20.glGetError();
if (error != 0) {
String message = GLU.gluErrorString(error);
if (message == null) {
throw new RendererException("An unknown OpenGL error has occurred.");
} else {
throw new RendererException("An OpenGL error has occurred: " + message);
}
}
}
}

@ -0,0 +1,571 @@
package com.jme3.renderer.android;
import android.graphics.Bitmap;
import android.opengl.ETC1;
import android.opengl.ETC1Util.ETC1Texture;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import com.jme3.asset.AndroidImageInfo;
import com.jme3.math.FastMath;
import com.jme3.renderer.RendererException;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;
public class TextureUtil {
private static final Logger logger = Logger.getLogger(TextureUtil.class.getName());
//TODO Make this configurable through appSettings
public static boolean ENABLE_COMPRESSION = true;
private static boolean NPOT = false;
private static boolean ETC1support = false;
private static boolean DXT1 = false;
private static boolean DEPTH24_STENCIL8 = false;
private static boolean DEPTH_TEXTURE = false;
private static boolean RGBA8 = false;
// Same constant used by both GL_ARM_rgba8 and GL_OES_rgb8_rgba8.
private static final int GL_RGBA8 = 0x8058;
private static final int GL_DXT1 = 0x83F0;
private static final int GL_DXT1A = 0x83F1;
private static final int GL_DEPTH_STENCIL_OES = 0x84F9;
private static final int GL_UNSIGNED_INT_24_8_OES = 0x84FA;
private static final int GL_DEPTH24_STENCIL8_OES = 0x88F0;
public static void loadTextureFeatures(String extensionString) {
ETC1support = extensionString.contains("GL_OES_compressed_ETC1_RGB8_texture");
DEPTH24_STENCIL8 = extensionString.contains("GL_OES_packed_depth_stencil");
NPOT = extensionString.contains("GL_IMG_texture_npot")
|| extensionString.contains("GL_OES_texture_npot")
|| extensionString.contains("GL_NV_texture_npot_2D_mipmap");
DXT1 = extensionString.contains("GL_EXT_texture_compression_dxt1");
DEPTH_TEXTURE = extensionString.contains("GL_OES_depth_texture");
RGBA8 = extensionString.contains("GL_ARM_rgba8") ||
extensionString.contains("GL_OES_rgb8_rgba8");
logger.log(Level.FINE, "Supports ETC1? {0}", ETC1support);
logger.log(Level.FINE, "Supports DEPTH24_STENCIL8? {0}", DEPTH24_STENCIL8);
logger.log(Level.FINE, "Supports NPOT? {0}", NPOT);
logger.log(Level.FINE, "Supports DXT1? {0}", DXT1);
logger.log(Level.FINE, "Supports DEPTH_TEXTURE? {0}", DEPTH_TEXTURE);
logger.log(Level.FINE, "Supports RGBA8? {0}", RGBA8);
}
private static void buildMipmap(Bitmap bitmap, boolean compress) {
int level = 0;
int height = bitmap.getHeight();
int width = bitmap.getWidth();
logger.log(Level.FINEST, " - Generating mipmaps for bitmap using SOFTWARE");
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
while (height >= 1 || width >= 1) {
//First of all, generate the texture from our bitmap and set it to the according level
if (compress) {
logger.log(Level.FINEST, " - Uploading LOD level {0} ({1}x{2}) with compression.", new Object[]{level, width, height});
uploadBitmapAsCompressed(GLES20.GL_TEXTURE_2D, level, bitmap, false, 0, 0);
} else {
logger.log(Level.FINEST, " - Uploading LOD level {0} ({1}x{2}) directly.", new Object[]{level, width, height});
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, level, bitmap, 0);
}
if (height == 1 || width == 1) {
break;
}
//Increase the mipmap level
height /= 2;
width /= 2;
Bitmap bitmap2 = Bitmap.createScaledBitmap(bitmap, width, height, true);
// Recycle any bitmaps created as a result of scaling the bitmap.
// Do not recycle the original image (mipmap level 0)
if (level != 0) {
bitmap.recycle();
}
bitmap = bitmap2;
level++;
}
}
private static void uploadBitmapAsCompressed(int target, int level, Bitmap bitmap, boolean subTexture, int x, int y) {
if (bitmap.hasAlpha()) {
logger.log(Level.FINEST, " - Uploading bitmap directly. Cannot compress as alpha present.");
if (subTexture) {
GLUtils.texSubImage2D(target, level, x, y, bitmap);
RendererUtil.checkGLError();
} else {
GLUtils.texImage2D(target, level, bitmap, 0);
RendererUtil.checkGLError();
}
} else {
// Convert to RGB565
int bytesPerPixel = 2;
Bitmap rgb565 = bitmap.copy(Bitmap.Config.RGB_565, true);
// Put texture data into ByteBuffer
ByteBuffer inputImage = BufferUtils.createByteBuffer(bitmap.getRowBytes() * bitmap.getHeight());
rgb565.copyPixelsToBuffer(inputImage);
inputImage.position(0);
// Delete the copied RGB565 image
rgb565.recycle();
// Encode the image into the output bytebuffer
int encodedImageSize = ETC1.getEncodedDataSize(bitmap.getWidth(), bitmap.getHeight());
ByteBuffer compressedImage = BufferUtils.createByteBuffer(encodedImageSize);
ETC1.encodeImage(inputImage, bitmap.getWidth(),
bitmap.getHeight(),
bytesPerPixel,
bytesPerPixel * bitmap.getWidth(),
compressedImage);
// Delete the input image buffer
BufferUtils.destroyDirectBuffer(inputImage);
// Create an ETC1Texture from the compressed image data
ETC1Texture etc1tex = new ETC1Texture(bitmap.getWidth(), bitmap.getHeight(), compressedImage);
// Upload the ETC1Texture
if (bytesPerPixel == 2) {
int oldSize = (bitmap.getRowBytes() * bitmap.getHeight());
int newSize = compressedImage.capacity();
logger.log(Level.FINEST, " - Uploading compressed image to GL, oldSize = {0}, newSize = {1}, ratio = {2}", new Object[]{oldSize, newSize, (float) oldSize / newSize});
if (subTexture) {
GLES20.glCompressedTexSubImage2D(target,
level,
x, y,
bitmap.getWidth(),
bitmap.getHeight(),
ETC1.ETC1_RGB8_OES,
etc1tex.getData().capacity(),
etc1tex.getData());
RendererUtil.checkGLError();
} else {
GLES20.glCompressedTexImage2D(target,
level,
ETC1.ETC1_RGB8_OES,
bitmap.getWidth(),
bitmap.getHeight(),
0,
etc1tex.getData().capacity(),
etc1tex.getData());
RendererUtil.checkGLError();
}
// ETC1Util.loadTexture(target, level, 0, GLES20.GL_RGB,
// GLES20.GL_UNSIGNED_SHORT_5_6_5, etc1Texture);
// } else if (bytesPerPixel == 3) {
// ETC1Util.loadTexture(target, level, 0, GLES20.GL_RGB,
// GLES20.GL_UNSIGNED_BYTE, etc1Texture);
}
BufferUtils.destroyDirectBuffer(compressedImage);
}
}
/**
* <code>uploadTextureBitmap</code> uploads a native android bitmap
*/
public static void uploadTextureBitmap(final int target, Bitmap bitmap, boolean needMips) {
uploadTextureBitmap(target, bitmap, needMips, false, 0, 0);
}
/**
* <code>uploadTextureBitmap</code> uploads a native android bitmap
*/
public static void uploadTextureBitmap(final int target, Bitmap bitmap, boolean needMips, boolean subTexture, int x, int y) {
boolean recycleBitmap = false;
//TODO, maybe this should raise an exception when NPOT is not supported
boolean willCompress = ENABLE_COMPRESSION && ETC1support && !bitmap.hasAlpha();
if (needMips && willCompress) {
// Image is compressed and mipmaps are desired, generate them
// using software.
buildMipmap(bitmap, willCompress);
} else {
if (willCompress) {
// Image is compressed but mipmaps are not desired, upload directly.
logger.log(Level.FINEST, " - Uploading compressed bitmap. Mipmaps are not generated.");
uploadBitmapAsCompressed(target, 0, bitmap, subTexture, x, y);
} else {
// Image is not compressed, mipmaps may or may not be desired.
logger.log(Level.FINEST, " - Uploading bitmap directly.{0}",
(needMips
? " Mipmaps will be generated in HARDWARE"
: " Mipmaps are not generated."));
if (subTexture) {
System.err.println("x : " + x + " y :" + y + " , " + bitmap.getWidth() + "/" + bitmap.getHeight());
GLUtils.texSubImage2D(target, 0, x, y, bitmap);
RendererUtil.checkGLError();
} else {
GLUtils.texImage2D(target, 0, bitmap, 0);
RendererUtil.checkGLError();
}
if (needMips) {
// No pregenerated mips available,
// generate from base level if required
GLES20.glGenerateMipmap(target);
RendererUtil.checkGLError();
}
}
}
if (recycleBitmap) {
bitmap.recycle();
}
}
public static void uploadTextureAny(Image img, int target, int index, boolean needMips) {
if (img.getEfficentData() instanceof AndroidImageInfo) {
logger.log(Level.FINEST, " === Uploading image {0}. Using BITMAP PATH === ", img);
// If image was loaded from asset manager, use fast path
AndroidImageInfo imageInfo = (AndroidImageInfo) img.getEfficentData();
uploadTextureBitmap(target, imageInfo.getBitmap(), needMips);
} else {
logger.log(Level.FINEST, " === Uploading image {0}. Using BUFFER PATH === ", img);
boolean wantGeneratedMips = needMips && !img.hasMipmaps();
if (wantGeneratedMips && img.getFormat().isCompressed()) {
logger.log(Level.WARNING, "Generating mipmaps is only"
+ " supported for Bitmap based or non-compressed images!");
}
// Upload using slower path
logger.log(Level.FINEST, " - Uploading bitmap directly.{0}",
(wantGeneratedMips
? " Mipmaps will be generated in HARDWARE"
: " Mipmaps are not generated."));
uploadTexture(img, target, index);
// Image was uploaded using slower path, since it is not compressed,
// then compress it
if (wantGeneratedMips) {
// No pregenerated mips available,
// generate from base level if required
GLES20.glGenerateMipmap(target);
}
}
}
private static void unsupportedFormat(Format fmt) {
throw new UnsupportedOperationException("The image format '" + fmt + "' is unsupported by the video hardware.");
}
public static AndroidGLImageFormat getImageFormat(Format fmt) throws UnsupportedOperationException {
AndroidGLImageFormat imageFormat = new AndroidGLImageFormat();
switch (fmt) {
case RGBA16:
case RGB16:
case RGB10:
case Luminance16:
case Luminance16Alpha16:
case Alpha16:
case Depth32:
case Depth32F:
throw new UnsupportedOperationException("The image format '"
+ fmt + "' is not supported by OpenGL ES 2.0 specification.");
case Alpha8:
imageFormat.format = GLES20.GL_ALPHA;
imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
if (RGBA8) {
imageFormat.renderBufferStorageFormat = GL_RGBA8;
} else {
// Highest precision alpha supported by vanilla OGLES2
imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
}
break;
case Luminance8:
imageFormat.format = GLES20.GL_LUMINANCE;
imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
if (RGBA8) {
imageFormat.renderBufferStorageFormat = GL_RGBA8;
} else {
// Highest precision luminance supported by vanilla OGLES2
imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
}
break;
case Luminance8Alpha8:
imageFormat.format = GLES20.GL_LUMINANCE_ALPHA;
imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
if (RGBA8) {
imageFormat.renderBufferStorageFormat = GL_RGBA8;
} else {
imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
}
break;
case RGB565:
imageFormat.format = GLES20.GL_RGB;
imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT_5_6_5;
imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
break;
case ARGB4444:
imageFormat.format = GLES20.GL_RGBA4;
imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT_4_4_4_4;
imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
break;
case RGB5A1:
imageFormat.format = GLES20.GL_RGBA;
imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT_5_5_5_1;
imageFormat.renderBufferStorageFormat = GLES20.GL_RGB5_A1;
break;
case RGB8:
imageFormat.format = GLES20.GL_RGB;
imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
if (RGBA8) {
imageFormat.renderBufferStorageFormat = GL_RGBA8;
} else {
// Fallback: Use RGB565 if RGBA8 is not available.
imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
}
break;
case BGR8:
imageFormat.format = GLES20.GL_RGB;
imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
if (RGBA8) {
imageFormat.renderBufferStorageFormat = GL_RGBA8;
} else {
imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
}
break;
case RGBA8:
imageFormat.format = GLES20.GL_RGBA;
imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
if (RGBA8) {
imageFormat.renderBufferStorageFormat = GL_RGBA8;
} else {
imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
}
break;
case Depth:
case Depth16:
if (!DEPTH_TEXTURE) {
unsupportedFormat(fmt);
}
imageFormat.format = GLES20.GL_DEPTH_COMPONENT;
imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT;
imageFormat.renderBufferStorageFormat = GLES20.GL_DEPTH_COMPONENT16;
break;
case Depth24:
case Depth24Stencil8:
if (!DEPTH_TEXTURE) {
unsupportedFormat(fmt);
}
if (DEPTH24_STENCIL8) {
// NEW: True Depth24 + Stencil8 format.
imageFormat.format = GL_DEPTH_STENCIL_OES;
imageFormat.dataType = GL_UNSIGNED_INT_24_8_OES;
imageFormat.renderBufferStorageFormat = GL_DEPTH24_STENCIL8_OES;
} else {
// Vanilla OGLES2, only Depth16 available.
imageFormat.format = GLES20.GL_DEPTH_COMPONENT;
imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT;
imageFormat.renderBufferStorageFormat = GLES20.GL_DEPTH_COMPONENT16;
}
break;
case DXT1:
if (!DXT1) {
unsupportedFormat(fmt);
}
imageFormat.format = GL_DXT1;
imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
imageFormat.compress = true;
break;
case DXT1A:
if (!DXT1) {
unsupportedFormat(fmt);
}
imageFormat.format = GL_DXT1A;
imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
imageFormat.compress = true;
break;
default:
throw new UnsupportedOperationException("Unrecognized format: " + fmt);
}
return imageFormat;
}
public static class AndroidGLImageFormat {
boolean compress = false;
int format = -1;
int renderBufferStorageFormat = -1;
int dataType = -1;
}
private static void uploadTexture(Image img,
int target,
int index) {
if (img.getEfficentData() instanceof AndroidImageInfo) {
throw new RendererException("This image uses efficient data. "
+ "Use uploadTextureBitmap instead.");
}
// Otherwise upload image directly.
// Prefer to only use power of 2 textures here to avoid errors.
Image.Format fmt = img.getFormat();
ByteBuffer data;
if (index >= 0 || img.getData() != null && img.getData().size() > 0) {
data = img.getData(index);
} else {
data = null;
}
int width = img.getWidth();
int height = img.getHeight();
if (!NPOT) {
// Check if texture is POT
if (!FastMath.isPowerOfTwo(width) || !FastMath.isPowerOfTwo(height)) {
throw new RendererException("Non-power-of-2 textures "
+ "are not supported by the video hardware "
+ "and no scaling path available for image: " + img);
}
}
AndroidGLImageFormat imageFormat = getImageFormat(fmt);
if (data != null) {
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
}
int[] mipSizes = img.getMipMapSizes();
int pos = 0;
if (mipSizes == null) {
if (data != null) {
mipSizes = new int[]{data.capacity()};
} else {
mipSizes = new int[]{width * height * fmt.getBitsPerPixel() / 8};
}
}
for (int i = 0; i < mipSizes.length; i++) {
int mipWidth = Math.max(1, width >> i);
int mipHeight = Math.max(1, height >> i);
if (data != null) {
data.position(pos);
data.limit(pos + mipSizes[i]);
}
if (imageFormat.compress && data != null) {
GLES20.glCompressedTexImage2D(target,
i,
imageFormat.format,
mipWidth,
mipHeight,
0,
data.remaining(),
data);
} else {
GLES20.glTexImage2D(target,
i,
imageFormat.format,
mipWidth,
mipHeight,
0,
imageFormat.format,
imageFormat.dataType,
data);
}
pos += mipSizes[i];
}
}
/**
* Update the texture currently bound to target at with data from the given
* Image at position x and y. The parameter index is used as the zoffset in
* case a 3d texture or texture 2d array is being updated.
*
* @param image Image with the source data (this data will be put into the
* texture)
* @param target the target texture
* @param index the mipmap level to update
* @param x the x position where to put the image in the texture
* @param y the y position where to put the image in the texture
*/
public static void uploadSubTexture(
Image img,
int target,
int index,
int x,
int y) {
if (img.getEfficentData() instanceof AndroidImageInfo) {
AndroidImageInfo imageInfo = (AndroidImageInfo) img.getEfficentData();
uploadTextureBitmap(target, imageInfo.getBitmap(), true, true, x, y);
return;
}
// Otherwise upload image directly.
// Prefer to only use power of 2 textures here to avoid errors.
Image.Format fmt = img.getFormat();
ByteBuffer data;
if (index >= 0 || img.getData() != null && img.getData().size() > 0) {
data = img.getData(index);
} else {
data = null;
}
int width = img.getWidth();
int height = img.getHeight();
if (!NPOT) {
// Check if texture is POT
if (!FastMath.isPowerOfTwo(width) || !FastMath.isPowerOfTwo(height)) {
throw new RendererException("Non-power-of-2 textures "
+ "are not supported by the video hardware "
+ "and no scaling path available for image: " + img);
}
}
AndroidGLImageFormat imageFormat = getImageFormat(fmt);
if (data != null) {
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
}
int[] mipSizes = img.getMipMapSizes();
int pos = 0;
if (mipSizes == null) {
if (data != null) {
mipSizes = new int[]{data.capacity()};
} else {
mipSizes = new int[]{width * height * fmt.getBitsPerPixel() / 8};
}
}
for (int i = 0; i < mipSizes.length; i++) {
int mipWidth = Math.max(1, width >> i);
int mipHeight = Math.max(1, height >> i);
if (data != null) {
data.position(pos);
data.limit(pos + mipSizes[i]);
}
if (imageFormat.compress && data != null) {
GLES20.glCompressedTexSubImage2D(target, i, x, y, mipWidth, mipHeight, imageFormat.format, data.remaining(), data);
RendererUtil.checkGLError();
} else {
GLES20.glTexSubImage2D(target, i, x, y, mipWidth, mipHeight, imageFormat.format, imageFormat.dataType, data);
RendererUtil.checkGLError();
}
pos += mipSizes[i];
}
}
}

@ -0,0 +1,518 @@
package com.jme3.system.android;
import android.opengl.GLSurfaceView.EGLConfigChooser;
import com.jme3.renderer.android.RendererUtil;
import com.jme3.system.AppSettings;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
/**
* AndroidConfigChooser is used to determine the best suited EGL Config
*
* @author iwgeric
*/
public class AndroidConfigChooser implements EGLConfigChooser {
private static final Logger logger = Logger.getLogger(AndroidConfigChooser.class.getName());
protected AppSettings settings;
private final static int EGL_OPENGL_ES2_BIT = 4;
@Deprecated
public enum ConfigType {
/**
* RGB565, 0 alpha, 16 depth, 0 stencil
*/
FASTEST(5, 6, 5, 0, 16, 0, 5, 6, 5, 0, 16, 0),
/**
* min RGB888, 0 alpha, 16 depth, 0 stencil max RGB888, 0 alpha, 32
* depth, 8 stencil
*/
BEST(8, 8, 8, 0, 32, 8, 8, 8, 8, 0, 16, 0),
/**
* Turn off config chooser and use hardcoded
* setEGLContextClientVersion(2); setEGLConfigChooser(5, 6, 5, 0, 16,
* 0);
*/
LEGACY(5, 6, 5, 0, 16, 0, 5, 6, 5, 0, 16, 0),
/**
* min RGB888, 8 alpha, 16 depth, 0 stencil max RGB888, 8 alpha, 32
* depth, 8 stencil
*/
BEST_TRANSLUCENT(8, 8, 8, 8, 32, 8, 8, 8, 8, 8, 16, 0);
/**
* red, green, blue, alpha, depth, stencil (max values)
*/
int r, g, b, a, d, s;
/**
* minimal values
*/
int mr, mg, mb, ma, md, ms;
private ConfigType(int r, int g, int b, int a, int d, int s, int mr, int mg, int mb, int ma, int md, int ms) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
this.d = d;
this.s = s;
this.mr = mr;
this.mg = mg;
this.mb = mb;
this.ma = ma;
this.md = md;
this.ms = ms;
}
}
public AndroidConfigChooser(AppSettings settings) {
this.settings = settings;
}
/**
* Gets called by the GLSurfaceView class to return the best config
*/
@Override
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
logger.fine("GLSurfaceView asking for egl config");
Config requestedConfig = getRequestedConfig();
EGLConfig[] configs = getConfigs(egl, display);
// First try to find an exact match, but allowing a higher stencil
EGLConfig choosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true);
if (choosenConfig == null && requestedConfig.d > 16) {
logger.log(Level.INFO, "EGL configuration not found, reducing depth");
requestedConfig.d = 16;
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true);
}
if (choosenConfig == null) {
logger.log(Level.INFO, "EGL configuration not found, allowing higher RGB");
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
}
if (choosenConfig == null && requestedConfig.a > 0) {
logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha");
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
}
if (choosenConfig == null && requestedConfig.s > 0) {
logger.log(Level.INFO, "EGL configuration not found, allowing higher samples");
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true);
}
if (choosenConfig == null && requestedConfig.a > 0) {
logger.log(Level.INFO, "EGL configuration not found, reducing alpha");
requestedConfig.a = 1;
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
}
if (choosenConfig == null && requestedConfig.s > 0) {
logger.log(Level.INFO, "EGL configuration not found, reducing samples");
requestedConfig.s = 1;
if (requestedConfig.a > 0) {
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true);
} else {
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, true, true);
}
}
if (choosenConfig == null && requestedConfig.getBitsPerPixel() > 16) {
logger.log(Level.INFO, "EGL configuration not found, setting to RGB565");
requestedConfig.r = 5;
requestedConfig.g = 6;
requestedConfig.b = 5;
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
if (choosenConfig == null) {
logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha");
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
}
}
if (choosenConfig == null) {
logger.log(Level.INFO, "EGL configuration not found, looking for best config with >= 16 bit Depth");
//failsafe, should pick best config with at least 16 depth
requestedConfig = new Config(0, 0, 0, 0, 16, 0, 0);
choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
}
if (choosenConfig != null) {
logger.fine("GLSurfaceView asks for egl config, returning: ");
logEGLConfig(choosenConfig, display, egl, Level.FINE);
storeSelectedConfig(egl, display, choosenConfig);
return choosenConfig;
} else {
logger.severe("No EGL Config found");
return null;
}
}
private Config getRequestedConfig() {
int r, g, b;
if (settings.getBitsPerPixel() == 24) {
r = g = b = 8;
} else {
if (settings.getBitsPerPixel() != 16) {
logger.log(Level.SEVERE, "Invalid bitsPerPixel setting: {0}, setting to RGB565 (16)", settings.getBitsPerPixel());
settings.setBitsPerPixel(16);
}
r = 5;
g = 6;
b = 5;
}
logger.log(Level.FINE, "Requested Display Config:");
logger.log(Level.FINE, "RGB: {0}, alpha: {1}, depth: {2}, samples: {3}, stencil: {4}",
new Object[]{settings.getBitsPerPixel(),
settings.getAlphaBits(), settings.getDepthBits(),
settings.getSamples(), settings.getStencilBits()});
return new Config(
r, g, b,
settings.getAlphaBits(),
settings.getDepthBits(),
settings.getSamples(),
settings.getStencilBits());
}
/**
* Query egl for the available configs
* @param egl
* @param display
* @return
*/
private EGLConfig[] getConfigs(EGL10 egl, EGLDisplay display) {
int[] num_config = new int[1];
int[] configSpec = new int[]{
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL10.EGL_NONE};
if (!egl.eglChooseConfig(display, configSpec, null, 0, num_config)) {
RendererUtil.checkEGLError(egl);
throw new AssertionError();
}
int numConfigs = num_config[0];
EGLConfig[] configs = new EGLConfig[numConfigs];
if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, num_config)) {
RendererUtil.checkEGLError(egl);
throw new AssertionError();
}
logger.fine("--------------Display Configurations---------------");
for (EGLConfig eGLConfig : configs) {
logEGLConfig(eGLConfig, display, egl, Level.FINE);
logger.fine("----------------------------------------");
}
return configs;
}
private EGLConfig chooseConfig(
EGL10 egl, EGLDisplay display, EGLConfig[] configs, Config requestedConfig,
boolean higherRGB, boolean higherAlpha,
boolean higherSamples, boolean higherStencil) {
EGLConfig keptConfig = null;
int kr = 0;
int kg = 0;
int kb = 0;
int ka = 0;
int kd = 0;
int ks = 0;
int kst = 0;
// first pass through config list. Try to find an exact match.
for (EGLConfig config : configs) {
int r = eglGetConfigAttribSafe(egl, display, config,
EGL10.EGL_RED_SIZE);
int g = eglGetConfigAttribSafe(egl, display, config,
EGL10.EGL_GREEN_SIZE);
int b = eglGetConfigAttribSafe(egl, display, config,
EGL10.EGL_BLUE_SIZE);
int a = eglGetConfigAttribSafe(egl, display, config,
EGL10.EGL_ALPHA_SIZE);
int d = eglGetConfigAttribSafe(egl, display, config,
EGL10.EGL_DEPTH_SIZE);
int s = eglGetConfigAttribSafe(egl, display, config,
EGL10.EGL_SAMPLES);
int st = eglGetConfigAttribSafe(egl, display, config,
EGL10.EGL_STENCIL_SIZE);
logger.log(Level.FINE, "Checking Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}",
new Object[]{r, g, b, a, d, s, st});
if (higherRGB && r < requestedConfig.r) { continue; }
if (!higherRGB && r != requestedConfig.r) { continue; }
if (higherRGB && g < requestedConfig.g) { continue; }
if (!higherRGB && g != requestedConfig.g) { continue; }
if (higherRGB && b < requestedConfig.b) { continue; }
if (!higherRGB && b != requestedConfig.b) { continue; }
if (higherAlpha && a < requestedConfig.a) { continue; }
if (!higherAlpha && a != requestedConfig.a) { continue; }
if (d < requestedConfig.d) { continue; } // always allow higher depth
if (higherSamples && s < requestedConfig.s) { continue; }
if (!higherSamples && s != requestedConfig.s) { continue; }
if (higherStencil && st < requestedConfig.st) { continue; }
if (!higherStencil && !inRange(st, 0, requestedConfig.st)) { continue; }
//we keep the config if it is better
if ( r >= kr || g >= kg || b >= kb || a >= ka ||
d >= kd || s >= ks || st >= kst ) {
kr = r; kg = g; kb = b; ka = a;
kd = d; ks = s; kst = st;
keptConfig = config;
logger.log(Level.FINE, "Keeping Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}",
new Object[]{r, g, b, a, d, s, st});
}
}
if (keptConfig != null) {
return keptConfig;
}
//no match found
logger.log(Level.SEVERE, "No egl config match found");
return null;
}
private static int eglGetConfigAttribSafe(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute) {
int[] value = new int[1];
if (!egl.eglGetConfigAttrib(display, config, attribute, value)) {
RendererUtil.checkEGLError(egl);
throw new AssertionError();
}
return value[0];
}
private void storeSelectedConfig(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
int r = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_RED_SIZE);
int g = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_GREEN_SIZE);
int b = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_BLUE_SIZE);
settings.setBitsPerPixel(r+g+b);
settings.setAlphaBits(
eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_ALPHA_SIZE));
settings.setDepthBits(
eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_DEPTH_SIZE));
settings.setSamples(
eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_SAMPLES));
settings.setStencilBits(
eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_STENCIL_SIZE));
}
/**
* log output with egl config details
*
* @param conf
* @param display
* @param egl
*/
private void logEGLConfig(EGLConfig conf, EGLDisplay display, EGL10 egl, Level level) {
logger.log(level, "EGL_RED_SIZE = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_RED_SIZE));
logger.log(level, "EGL_GREEN_SIZE = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_GREEN_SIZE));
logger.log(level, "EGL_BLUE_SIZE = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_BLUE_SIZE));
logger.log(level, "EGL_ALPHA_SIZE = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_ALPHA_SIZE));
logger.log(level, "EGL_DEPTH_SIZE = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_DEPTH_SIZE));
logger.log(level, "EGL_STENCIL_SIZE = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_STENCIL_SIZE));
logger.log(level, "EGL_RENDERABLE_TYPE = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_RENDERABLE_TYPE));
logger.log(level, "EGL_SURFACE_TYPE = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SURFACE_TYPE));
logger.log(level, "EGL_SAMPLE_BUFFERS = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SAMPLE_BUFFERS));
logger.log(level, "EGL_SAMPLES = {0}",
eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SAMPLES));
}
private boolean inRange(int val, int min, int max) {
return min <= val && val <= max;
}
private class Config {
/**
* red, green, blue, alpha, depth, samples, stencil
*/
int r, g, b, a, d, s, st;
private Config(int r, int g, int b, int a, int d, int s, int st) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
this.d = d;
this.s = s;
this.st = st;
}
private int getBitsPerPixel() {
return r+g+b;
}
}
//DON'T REMOVE THIS, USED FOR UNIT TESTING FAILING CONFIGURATION LISTS.
// private static class Config {
//
// int r, g, b, a, d, s, ms, ns;
//
// public Config(int r, int g, int b, int a, int d, int s, int ms, int ns) {
// this.r = r;
// this.g = g;
// this.b = b;
// this.a = a;
// this.d = d;
// this.s = s;
// this.ms = ms;
// this.ns = ns;
// }
//
// @Override
// public String toString() {
// return "Config{" + "r=" + r + ", g=" + g + ", b=" + b + ", a=" + a + ", d=" + d + ", s=" + s + ", ms=" + ms + ", ns=" + ns + '}';
// }
// }
//
// public static Config chooseConfig(List<Config> configs, ConfigType configType, int mSamples) {
//
// Config keptConfig = null;
// int kd = 0;
// int knbMs = 0;
//
//
// // first pass through config list. Try to find an exact match.
// for (Config config : configs) {
//// logEGLConfig(config, display, egl);
// int r = config.r;
// int g = config.g;
// int b = config.b;
// int a = config.a;
// int d = config.d;
// int s = config.s;
// int isMs = config.ms;
// int nbMs = config.ns;
//
// if (inRange(r, configType.mr, configType.r)
// && inRange(g, configType.mg, configType.g)
// && inRange(b, configType.mb, configType.b)
// && inRange(a, configType.ma, configType.a)
// && inRange(d, configType.md, configType.d)
// && inRange(s, configType.ms, configType.s)) {
// if (mSamples == 0 && isMs != 0) {
// continue;
// }
// boolean keep = false;
// //we keep the config if the depth is better or if the AA setting is better
// if (d >= kd) {
// kd = d;
// keep = true;
// } else {
// keep = false;
// }
//
// if (mSamples != 0) {
// if (nbMs >= knbMs && nbMs <= mSamples) {
// knbMs = nbMs;
// keep = true;
// } else {
// keep = false;
// }
// }
//
// if (keep) {
// keptConfig = config;
// }
// }
// }
//
// if (keptConfig != null) {
// return keptConfig;
// }
//
// if (configType == ConfigType.BEST) {
// keptConfig = chooseConfig(configs, ConfigType.BEST_TRANSLUCENT, mSamples);
//
// if (keptConfig != null) {
// return keptConfig;
// }
// }
//
// if (configType == ConfigType.BEST_TRANSLUCENT) {
// keptConfig = chooseConfig(configs, ConfigType.FASTEST, mSamples);
//
// if (keptConfig != null) {
// return keptConfig;
// }
// }
// // failsafe. pick the 1st config.
//
// for (Config config : configs) {
// if (config.d >= 16) {
// return config;
// }
// }
//
// return null;
// }
//
// private static boolean inRange(int val, int min, int max) {
// return min <= val && val <= max;
// }
//
// public static void main(String... argv) {
// List<Config> confs = new ArrayList<Config>();
// confs.add(new Config(5, 6, 5, 0, 0, 0, 0, 0));
// confs.add(new Config(5, 6, 5, 0, 16, 0, 0, 0));
// confs.add(new Config(5, 6, 5, 0, 24, 8, 0, 0));
// confs.add(new Config(8, 8, 8, 8, 0, 0, 0, 0));
//// confs.add(new Config(8, 8, 8, 8, 16, 0, 0, 0));
//// confs.add(new Config(8, 8, 8, 8, 24, 8, 0, 0));
//
// confs.add(new Config(5, 6, 5, 0, 0, 0, 1, 2));
// confs.add(new Config(5, 6, 5, 0, 16, 0, 1, 2));
// confs.add(new Config(5, 6, 5, 0, 24, 8, 1, 2));
// confs.add(new Config(8, 8, 8, 8, 0, 0, 1, 2));
//// confs.add(new Config(8, 8, 8, 8, 16, 0, 1, 2));
//// confs.add(new Config(8, 8, 8, 8, 24, 8, 1, 2));
//
// confs.add(new Config(5, 6, 5, 0, 0, 0, 1, 4));
// confs.add(new Config(5, 6, 5, 0, 16, 0, 1, 4));
// confs.add(new Config(5, 6, 5, 0, 24, 8, 1, 4));
// confs.add(new Config(8, 8, 8, 8, 0, 0, 1, 4));
//// confs.add(new Config(8, 8, 8, 8, 16, 0, 1, 4));
//// confs.add(new Config(8, 8, 8, 8, 24, 8, 1, 4));
//
// Config chosen = chooseConfig(confs, ConfigType.BEST, 0);
//
// System.err.println(chosen);
//
// }
}

@ -0,0 +1,96 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.system.android;
import com.jme3.system.Timer;
/**
* <code>AndroidTimer</code> is a System.nanoTime implementation of <code>Timer</code>.
*/
public class AndroidTimer extends Timer {
//private static final long TIMER_RESOLUTION = 1000L;
//private static final float INVERSE_TIMER_RESOLUTION = 1f/1000L;
private static final long TIMER_RESOLUTION = 1000000000L;
private static final float INVERSE_TIMER_RESOLUTION = 1f/1000000000L;
private long startTime;
private long previousTime;
private float tpf;
private float fps;
public AndroidTimer() {
//startTime = System.currentTimeMillis();
startTime = System.nanoTime();
}
/**
* Returns the time in seconds. The timer starts
* at 0.0 seconds.
*
* @return the current time in seconds
*/
@Override
public float getTimeInSeconds() {
return getTime() * INVERSE_TIMER_RESOLUTION;
}
public long getTime() {
//return System.currentTimeMillis() - startTime;
return System.nanoTime() - startTime;
}
public long getResolution() {
return TIMER_RESOLUTION;
}
public float getFrameRate() {
return fps;
}
public float getTimePerFrame() {
return tpf;
}
public void update() {
tpf = (getTime() - previousTime) * (1.0f / TIMER_RESOLUTION);
fps = 1.0f / tpf;
previousTime = getTime();
}
public void reset() {
//startTime = System.currentTimeMillis();
startTime = System.nanoTime();
previousTime = getTime();
}
}

@ -0,0 +1,226 @@
package com.jme3.system.android;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Environment;
import com.jme3.asset.AndroidAssetManager;
import com.jme3.asset.AndroidImageInfo;
import com.jme3.asset.AssetManager;
import com.jme3.audio.AudioRenderer;
import com.jme3.audio.android.AndroidAudioRenderer;
import com.jme3.audio.android.AndroidMediaPlayerAudioRenderer;
import com.jme3.audio.android.AndroidOpenALSoftAudioRenderer;
import com.jme3.system.*;
import com.jme3.system.JmeContext.Type;
import com.jme3.texture.Image;
import com.jme3.texture.image.DefaultImageRaster;
import com.jme3.texture.image.ImageRaster;
import com.jme3.util.AndroidScreenshots;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.logging.Level;
public class JmeAndroidSystem extends JmeSystemDelegate {
private static Activity activity;
private static String audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
static {
try {
System.loadLibrary("bulletjme");
} catch (UnsatisfiedLinkError e) {
}
}
@Override
public void writeImageFile(OutputStream outStream, String format, ByteBuffer imageData, int width, int height) throws IOException {
Bitmap bitmapImage = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
AndroidScreenshots.convertScreenShot(imageData, bitmapImage);
Bitmap.CompressFormat compressFormat;
if (format.equals("png")) {
compressFormat = Bitmap.CompressFormat.PNG;
} else if (format.equals("jpg")) {
compressFormat = Bitmap.CompressFormat.JPEG;
} else {
throw new UnsupportedOperationException("Only 'png' and 'jpg' formats are supported on Android");
}
bitmapImage.compress(compressFormat, 95, outStream);
bitmapImage.recycle();
}
@Override
public ImageRaster createImageRaster(Image image, int slice) {
if (image.getEfficentData() != null) {
return (AndroidImageInfo) image.getEfficentData();
} else {
return new DefaultImageRaster(image, slice);
}
}
@Override
public AssetManager newAssetManager(URL configFile) {
logger.log(Level.FINE, "Creating asset manager with config {0}", configFile);
return new AndroidAssetManager(configFile);
}
@Override
public AssetManager newAssetManager() {
logger.log(Level.FINE, "Creating asset manager with default config");
return new AndroidAssetManager(null);
}
@Override
public void showErrorDialog(String message) {
final String finalMsg = message;
final String finalTitle = "Error in application";
final Activity context = JmeAndroidSystem.getActivity();
context.runOnUiThread(new Runnable() {
@Override
public void run() {
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(finalTitle).setMessage(finalMsg).create();
dialog.show();
}
});
}
@Override
public boolean showSettingsDialog(AppSettings sourceSettings, boolean loadFromRegistry) {
return true;
}
@Override
public JmeContext newContext(AppSettings settings, Type contextType) {
if (settings.getAudioRenderer().equals(AppSettings.ANDROID_MEDIAPLAYER)) {
audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
} else if (settings.getAudioRenderer().equals(AppSettings.ANDROID_OPENAL_SOFT)) {
audioRendererType = AppSettings.ANDROID_OPENAL_SOFT;
} else {
logger.log(Level.INFO, "AudioRenderer not set. Defaulting to Android MediaPlayer / SoundPool");
audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
}
initialize(settings);
JmeContext ctx = new OGLESContext();
ctx.setSettings(settings);
return ctx;
}
@Override
public AudioRenderer newAudioRenderer(AppSettings settings) {
if (settings.getAudioRenderer().equals(AppSettings.ANDROID_MEDIAPLAYER)) {
logger.log(Level.INFO, "newAudioRenderer settings set to Android MediaPlayer / SoundPool");
audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
return new AndroidMediaPlayerAudioRenderer(activity);
} else if (settings.getAudioRenderer().equals(AppSettings.ANDROID_OPENAL_SOFT)) {
logger.log(Level.INFO, "newAudioRenderer settings set to Android OpenAL Soft");
audioRendererType = AppSettings.ANDROID_OPENAL_SOFT;
return new AndroidOpenALSoftAudioRenderer();
} else {
logger.log(Level.INFO, "AudioRenderer not set. Defaulting to Android MediaPlayer / SoundPool");
audioRendererType = AppSettings.ANDROID_MEDIAPLAYER;
return new AndroidMediaPlayerAudioRenderer(activity);
}
}
@Override
public void initialize(AppSettings settings) {
if (initialized) {
return;
}
initialized = true;
logger.log(Level.INFO, "Running on {0}", getFullName());
}
@Override
public Platform getPlatform() {
String arch = System.getProperty("os.arch").toLowerCase();
if (arch.contains("arm")) {
if (arch.contains("v5")) {
return Platform.Android_ARM5;
} else if (arch.contains("v6")) {
return Platform.Android_ARM6;
} else if (arch.contains("v7")) {
return Platform.Android_ARM7;
} else {
return Platform.Android_ARM5; // unknown ARM
}
} else {
throw new UnsupportedOperationException("Unsupported Android Platform");
}
}
@Override
public synchronized File getStorageFolder(JmeSystem.StorageFolderType type) {
File storageFolder = null;
switch (type) {
case Internal:
// http://developer.android.com/guide/topics/data/data-storage.html
// http://developer.android.com/guide/topics/data/data-storage.html#filesInternal
// http://developer.android.com/reference/android/content/Context.html#getFilesDir()
// http://developer.android.com/reference/android/content/Context.html#getDir(java.lang.String, int)
// getDir automatically creates the directory if necessary.
// Directory structure should be: /data/data/<packagename>/app_
// When created this way, the directory is automatically removed by the Android
// system when the app is uninstalled.
// The directory is NOT accessible by a PC connected to the device
// The files can only be accessed by this application
storageFolder = storageFolders.get(type);
if (storageFolder == null) {
storageFolder = activity.getApplicationContext().getDir("", Context.MODE_PRIVATE);
storageFolders.put(type, storageFolder);
}
break;
case External:
//http://developer.android.com/reference/android/content/Context.html#getExternalFilesDir
//http://developer.android.com/guide/topics/data/data-storage.html
// getExternalFilesDir automatically creates the directory if necessary.
// Directory structure should be: /mnt/sdcard/Android/data/<packagename>/files
// When created this way, the directory is automatically removed by the Android
// system when the app is uninstalled.
// The directory is also accessible by a PC connected to the device
// so the files can be copied to the PC (ie. screenshots)
storageFolder = storageFolders.get(type);
if (storageFolder == null) {
String state = Environment.getExternalStorageState();
logger.log(Level.FINE, "ExternalStorageState: {0}", state);
if (state.equals(Environment.MEDIA_MOUNTED)) {
storageFolder = activity.getApplicationContext().getExternalFilesDir(null);
storageFolders.put(type, storageFolder);
}
}
break;
default:
break;
}
if (storageFolder != null) {
logger.log(Level.FINE, "Base Storage Folder Path: {0}", storageFolder.getAbsolutePath());
} else {
logger.log(Level.FINE, "Base Storage Folder not found!");
}
return storageFolder;
}
public static void setActivity(Activity activity) {
JmeAndroidSystem.activity = activity;
}
public static Activity getActivity() {
return activity;
}
public static String getAudioRendererType() {
return audioRendererType;
}
}

@ -0,0 +1,467 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.system.android;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ConfigurationInfo;
import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.text.InputType;
import android.view.Gravity;
import android.view.SurfaceHolder;
import android.view.ViewGroup.LayoutParams;
import android.widget.EditText;
import android.widget.FrameLayout;
import com.jme3.input.*;
import com.jme3.input.android.AndroidInput;
import com.jme3.input.android.AndroidSensorJoyInput;
import com.jme3.input.android.AndroidInputHandler;
import com.jme3.input.controls.SoftTextDialogInputListener;
import com.jme3.input.dummy.DummyKeyInput;
import com.jme3.input.dummy.DummyMouseInput;
import com.jme3.renderer.android.AndroidGLSurfaceView;
import com.jme3.renderer.android.OGLESShaderRenderer;
import com.jme3.system.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTextDialogInput {
private static final Logger logger = Logger.getLogger(OGLESContext.class.getName());
protected final AtomicBoolean created = new AtomicBoolean(false);
protected final AtomicBoolean renderable = new AtomicBoolean(false);
protected final AtomicBoolean needClose = new AtomicBoolean(false);
protected AppSettings settings = new AppSettings(true);
/*
* >= OpenGL ES 2.0 (Android 2.2+)
*/
protected OGLESShaderRenderer renderer;
protected Timer timer;
protected SystemListener listener;
protected boolean autoFlush = true;
protected AndroidInputHandler androidInput;
protected int minFrameDuration = 0; // No FPS cap
protected JoyInput androidSensorJoyInput = null;
/**
* EGL_RENDERABLE_TYPE: EGL_OPENGL_ES_BIT = OpenGL ES 1.0 |
* EGL_OPENGL_ES2_BIT = OpenGL ES 2.0
*/
protected int clientOpenGLESVersion = 1;
public OGLESContext() {
}
@Override
public Type getType() {
return Type.Display;
}
/**
* <code>createView</code> creates the GLSurfaceView that the renderer will
* draw to. <p> The result GLSurfaceView will receive input events and
* forward them to the Application. Any rendering will be done into the
* GLSurfaceView. Only one GLSurfaceView can be created at this time. The
* given configType specifies how to determine the display configuration.
*
* @return GLSurfaceView The newly created view
*/
public AndroidGLSurfaceView createView() {
AndroidGLSurfaceView view;
int buildVersion = Build.VERSION.SDK_INT;
// Start to set up the view
view = new AndroidGLSurfaceView(JmeAndroidSystem.getActivity().getApplication());
if (androidInput == null) {
androidInput = new AndroidInputHandler();
}
androidInput.setView(view);
androidInput.loadSettings(settings);
// setEGLContextClientVersion must be set before calling setRenderer
// this means it cannot be set in AndroidConfigChooser (too late)
int rawOpenGLESVersion = getOpenGLESVersion();
// logger.log(Level.FINE, "clientOpenGLESVersion {0}.{1}",
// new Object[]{clientOpenGLESVersion>>16, clientOpenGLESVersion<<16});
if (rawOpenGLESVersion < 0x20000) {
throw new UnsupportedOperationException("OpenGL ES 2.0 is not supported on this device");
} else {
clientOpenGLESVersion = 2;
view.setEGLContextClientVersion(clientOpenGLESVersion);
}
view.setFocusableInTouchMode(true);
view.setFocusable(true);
view.getHolder().setType(SurfaceHolder.SURFACE_TYPE_GPU);
// setFormat must be set before AndroidConfigChooser is called by the surfaceview.
// if setFormat is called after ConfigChooser is called, then execution
// stops at the setFormat call without a crash.
// We look at the user setting for alpha bits and set the surfaceview
// PixelFormat to either Opaque, Transparent, or Translucent.
// ConfigChooser will do it's best to honor the alpha requested by the user
// For best rendering performance, use Opaque (alpha bits = 0).
int curAlphaBits = settings.getAlphaBits();
logger.log(Level.FINE, "curAlphaBits: {0}", curAlphaBits);
if (curAlphaBits >= 8) {
logger.log(Level.FINE, "Pixel Format: TRANSLUCENT");
view.getHolder().setFormat(PixelFormat.TRANSLUCENT);
view.setZOrderOnTop(true);
} else if (curAlphaBits >= 1) {
logger.log(Level.FINE, "Pixel Format: TRANSPARENT");
view.getHolder().setFormat(PixelFormat.TRANSPARENT);
} else {
logger.log(Level.FINE, "Pixel Format: OPAQUE");
view.getHolder().setFormat(PixelFormat.OPAQUE);
}
AndroidConfigChooser configChooser = new AndroidConfigChooser(settings);
view.setEGLConfigChooser(configChooser);
view.setRenderer(this);
// Attempt to preserve the EGL Context on app pause/resume.
// Not destroying and recreating the EGL context
// will help with resume time by reusing the existing context to avoid
// reloading all the OpenGL objects.
if (buildVersion >= 11) {
view.setPreserveEGLContextOnPause(true);
}
return view;
}
/**
* Get the OpenGL ES version
* @return version returns the int value of the GLES version
*/
public int getOpenGLESVersion() {
ActivityManager am =
(ActivityManager) JmeAndroidSystem.getActivity().getApplication().getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo info = am.getDeviceConfigurationInfo();
logger.log(Level.FINE, "OpenGL Version {0}:", info.getGlEsVersion());
return info.reqGlEsVersion;
// return (info.reqGlEsVersion >= 0x20000);
}
// renderer:initialize
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig cfg) {
if (created.get() && renderer != null) {
renderer.resetGLObjects();
} else {
if (!created.get()) {
logger.fine("GL Surface created, initializing JME3 renderer");
initInThread();
} else {
logger.warning("GL Surface already created");
}
}
}
protected void initInThread() {
created.set(true);
logger.fine("OGLESContext create");
logger.log(Level.FINE, "Running on thread: {0}", Thread.currentThread().getName());
// Setup unhandled Exception Handler
Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread thread, Throwable thrown) {
listener.handleError("Exception thrown in " + thread.toString(), thrown);
}
});
timer = new AndroidTimer();
renderer = new OGLESShaderRenderer();
renderer.initialize();
JmeSystem.setSoftTextDialogInput(this);
needClose.set(false);
}
/**
* De-initialize in the OpenGL thread.
*/
protected void deinitInThread() {
if (renderable.get()) {
created.set(false);
if (renderer != null) {
renderer.cleanup();
}
listener.destroy();
listener = null;
renderer = null;
timer = null;
// do android specific cleaning here
logger.fine("Display destroyed.");
renderable.set(false);
}
}
@Override
public void setSettings(AppSettings settings) {
this.settings.copyFrom(settings);
if (androidInput != null) {
androidInput.loadSettings(settings);
}
}
@Override
public void setSystemListener(SystemListener listener) {
this.listener = listener;
}
@Override
public AppSettings getSettings() {
return settings;
}
@Override
public com.jme3.renderer.Renderer getRenderer() {
return renderer;
}
@Override
public MouseInput getMouseInput() {
return new DummyMouseInput();
}
@Override
public KeyInput getKeyInput() {
return new DummyKeyInput();
}
@Override
public JoyInput getJoyInput() {
if (androidSensorJoyInput == null) {
androidSensorJoyInput = new AndroidSensorJoyInput();
}
return androidSensorJoyInput;
}
@Override
public TouchInput getTouchInput() {
return androidInput;
}
@Override
public Timer getTimer() {
return timer;
}
@Override
public void setTitle(String title) {
}
@Override
public boolean isCreated() {
return created.get();
}
@Override
public void setAutoFlushFrames(boolean enabled) {
this.autoFlush = enabled;
}
// SystemListener:reshape
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
logger.log(Level.FINE, "GL Surface changed, width: {0} height: {1}", new Object[]{width, height});
// update the application settings with the new resolution
settings.setResolution(width, height);
// reload settings in androidInput so the correct touch event scaling can be
// calculated in case the surface resolution is different than the view
androidInput.loadSettings(settings);
// if the application has already been initialized (ie renderable is set)
// then call reshape so the app can adjust to the new resolution.
if (renderable.get()) {
logger.log(Level.FINE, "App already initialized, calling reshape");
listener.reshape(width, height);
}
}
// SystemListener:update
@Override
public void onDrawFrame(GL10 gl) {
if (needClose.get()) {
deinitInThread();
return;
}
if (!renderable.get()) {
if (created.get()) {
logger.fine("GL Surface is setup, initializing application");
listener.initialize();
renderable.set(true);
}
} else {
if (!created.get()) {
throw new IllegalStateException("onDrawFrame without create");
}
long milliStart = System.currentTimeMillis();
listener.update();
if (autoFlush) {
renderer.onFrame();
}
long milliDelta = System.currentTimeMillis() - milliStart;
// Enforce a FPS cap
if (milliDelta < minFrameDuration) {
//logger.log(Level.FINE, "Time per frame {0}", milliDelta);
try {
Thread.sleep(minFrameDuration - milliDelta);
} catch (InterruptedException e) {
}
}
}
}
@Override
public boolean isRenderable() {
return renderable.get();
}
@Override
public void create(boolean waitFor) {
if (waitFor) {
waitFor(true);
}
}
public void create() {
create(false);
}
@Override
public void restart() {
}
@Override
public void destroy(boolean waitFor) {
needClose.set(true);
if (waitFor) {
waitFor(false);
}
}
public void destroy() {
destroy(true);
}
protected void waitFor(boolean createdVal) {
while (renderable.get() != createdVal) {
try {
Thread.sleep(10);
} catch (InterruptedException ex) {
}
}
}
public void requestDialog(final int id, final String title, final String initialValue, final SoftTextDialogInputListener listener) {
logger.log(Level.FINE, "requestDialog: title: {0}, initialValue: {1}",
new Object[]{title, initialValue});
final Activity activity = JmeAndroidSystem.getActivity();
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
final FrameLayout layoutTextDialogInput = new FrameLayout(activity);
final EditText editTextDialogInput = new EditText(activity);
editTextDialogInput.setWidth(LayoutParams.FILL_PARENT);
editTextDialogInput.setHeight(LayoutParams.FILL_PARENT);
editTextDialogInput.setPadding(20, 20, 20, 20);
editTextDialogInput.setGravity(Gravity.FILL_HORIZONTAL);
//editTextDialogInput.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
editTextDialogInput.setText(initialValue);
switch (id) {
case SoftTextDialogInput.TEXT_ENTRY_DIALOG:
editTextDialogInput.setInputType(InputType.TYPE_CLASS_TEXT);
break;
case SoftTextDialogInput.NUMERIC_ENTRY_DIALOG:
editTextDialogInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED);
break;
case SoftTextDialogInput.NUMERIC_KEYPAD_DIALOG:
editTextDialogInput.setInputType(InputType.TYPE_CLASS_PHONE);
break;
default:
break;
}
layoutTextDialogInput.addView(editTextDialogInput);
AlertDialog dialogTextInput = new AlertDialog.Builder(activity).setTitle(title).setView(layoutTextDialogInput).setPositiveButton("OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
/* User clicked OK, send COMPLETE action
* and text */
listener.onSoftText(SoftTextDialogInputListener.COMPLETE, editTextDialogInput.getText().toString());
}
}).setNegativeButton("Cancel",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
/* User clicked CANCEL, send CANCEL action
* and text */
listener.onSoftText(SoftTextDialogInputListener.CANCEL, editTextDialogInput.getText().toString());
}
}).create();
dialogTextInput.show();
}
});
}
}

@ -0,0 +1,20 @@
package com.jme3.texture.plugins;
import android.graphics.Bitmap;
import com.jme3.asset.AndroidImageInfo;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
import com.jme3.texture.Image;
import java.io.IOException;
public class AndroidImageLoader implements AssetLoader {
public Object load(AssetInfo info) throws IOException {
AndroidImageInfo imageInfo = new AndroidImageInfo(info);
Bitmap bitmap = imageInfo.getBitmap();
Image image = new Image(imageInfo.getFormat(), bitmap.getWidth(), bitmap.getHeight(), null);
image.setEfficentData(imageInfo);
return image;
}
}

@ -0,0 +1,110 @@
package com.jme3.util;
import android.util.Log;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
/**
* Converts from Java based logging ({@link Logger} to Android based logging
* {@link Log}.
*/
public class AndroidLogHandler extends Handler {
private static final Formatter JME_FORMATTER = new JmeFormatter() {
String lineSeperator = System.getProperty("line.separator");
@Override
public String format(LogRecord record) {
StringBuilder sb = new StringBuilder();
sb.append(record.getLevel().getLocalizedName()).append(" ");
sb.append(formatMessage(record)).append(lineSeperator);
if (record.getThrown() != null) {
try {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
record.getThrown().printStackTrace(pw);
pw.close();
sb.append(sw.toString());
} catch (Exception ex) {
}
}
return sb.toString();
}
};
@Override
public void close() {
}
@Override
public void flush() {
}
@Override
public void publish(LogRecord record) {
int level = getAndroidLevel(record.getLevel());
// String tag = loggerNameToTag(record.getLoggerName());
String tag = record.getLoggerName();
try {
String message = JME_FORMATTER.format(record);
Log.println(level, tag, message);
} catch (RuntimeException e) {
Log.e("AndroidHandler", "Error logging message.", e);
}
}
/**
* Converts a {@link java.util.logging.Logger} logging level into an Android
* one.
*
* @param level The {@link java.util.logging.Logger} logging level.
*
* @return The resulting Android logging level.
*/
static int getAndroidLevel(Level level) {
int value = level.intValue();
if (value >= 1000) { // SEVERE
return Log.ERROR;
} else if (value >= 900) { // WARNING
return Log.WARN;
} else if (value >= 800) { // INFO
return Log.INFO;
} else {
return Log.DEBUG;
}
}
/**
* Returns the short logger tag for the given logger name.
* Traditionally loggers are named by fully-qualified Java classes; this
* method attempts to return a concise identifying part of such names.
*/
public static String loggerNameToTag(String loggerName) {
// Anonymous logger.
if (loggerName == null) {
return "null";
}
int length = loggerName.length();
int lastPeriod = loggerName.lastIndexOf(".");
if (lastPeriod == -1) {
return loggerName;
}
return loggerName.substring(lastPeriod + 1);
}
}

@ -0,0 +1,42 @@
package com.jme3.util;
import android.graphics.Bitmap;
import java.nio.ByteBuffer;
import java.util.logging.Logger;
public final class AndroidScreenshots {
private static final Logger logger = Logger.getLogger(AndroidScreenshots.class.getName());
/**
* Convert OpenGL GLES20.GL_RGBA to Bitmap.Config.ARGB_8888 and store result
* in a Bitmap
*
* @param buf ByteBuffer that has the pixel color data from OpenGL
* @param bitmapImage Bitmap to be used after converting the data
*/
public static void convertScreenShot(ByteBuffer buf, Bitmap bitmapImage) {
int width = bitmapImage.getWidth();
int height = bitmapImage.getHeight();
int size = width * height;
// Grab data from ByteBuffer as Int Array to manipulate data and send to image
int[] data = new int[size];
buf.asIntBuffer().get(data);
// convert from GLES20.GL_RGBA to Bitmap.Config.ARGB_8888
// ** need to swap RED and BLUE **
for (int idx = 0; idx < data.length; idx++) {
int initial = data[idx];
int pb = (initial >> 16) & 0xff;
int pr = (initial << 16) & 0x00ff0000;
int pix1 = (initial & 0xff00ff00) | pr | pb;
data[idx] = pix1;
}
// OpenGL and Bitmap have opposite starting points for Y axis (top vs bottom)
// Need to write the data in the image from the bottom to the top
// Use size-width to indicate start with last row and increment by -width for each row
bitmapImage.setPixels(data, size - width, -width, 0, 0, width, height);
}
}

@ -0,0 +1,76 @@
package com.jme3.util;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* Ring buffer (fixed size queue) implementation using a circular array (array
* with wrap-around).
*/
// suppress unchecked warnings in Java 1.5.0_6 and later
@SuppressWarnings("unchecked")
public class RingBuffer<T> implements Iterable<T> {
private T[] buffer; // queue elements
private int count = 0; // number of elements on queue
private int indexOut = 0; // index of first element of queue
private int indexIn = 0; // index of next available slot
// cast needed since no generic array creation in Java
public RingBuffer(int capacity) {
buffer = (T[]) new Object[capacity];
}
public boolean isEmpty() {
return count == 0;
}
public int size() {
return count;
}
public void push(T item) {
if (count == buffer.length) {
throw new RuntimeException("Ring buffer overflow");
}
buffer[indexIn] = item;
indexIn = (indexIn + 1) % buffer.length; // wrap-around
count++;
}
public T pop() {
if (isEmpty()) {
throw new RuntimeException("Ring buffer underflow");
}
T item = buffer[indexOut];
buffer[indexOut] = null; // to help with garbage collection
count--;
indexOut = (indexOut + 1) % buffer.length; // wrap-around
return item;
}
public Iterator<T> iterator() {
return new RingBufferIterator();
}
// an iterator, doesn't implement remove() since it's optional
private class RingBufferIterator implements Iterator<T> {
private int i = 0;
public boolean hasNext() {
return i < count;
}
public void remove() {
throw new UnsupportedOperationException();
}
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return buffer[i++];
}
}
}

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jme3.androiddemo"
android:versionCode="6"
android:versionName="1.2.2">
<uses-sdk android:targetSdkVersion="8" android:minSdkVersion="8" />
<!-- Tell the system that you need ES 2.0. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<!-- Tell the system that you need distinct touches (for the zoom gesture). -->
<uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="true" />
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".DemoMainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".DemoAndroidHarness"
android:label="@string/app_name">
</activity>
</application>
</manifest>

@ -0,0 +1,54 @@
package jme3test.android;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import com.jme3.app.AndroidHarness;
import com.jme3.system.android.AndroidConfigChooser.ConfigType;
public class DemoAndroidHarness extends AndroidHarness
{
@Override
public void onCreate(Bundle savedInstanceState)
{
// Set the application class to run
// First Extract the bundle from intent
Bundle bundle = getIntent().getExtras();
//Next extract the values using the key as
appClass = bundle.getString("APPCLASSNAME");
String eglConfig = bundle.getString("EGLCONFIG");
if (eglConfig.equals("Best"))
{
eglConfigType = ConfigType.BEST;
}
else if (eglConfig.equals("Legacy"))
{
eglConfigType = ConfigType.LEGACY;
}
else
{
eglConfigType = ConfigType.FASTEST;
}
if (bundle.getBoolean("VERBOSE"))
{
eglConfigVerboseLogging = true;
}
else
{
eglConfigVerboseLogging = false;
}
exitDialogTitle = "Close Demo?";
exitDialogMessage = "Press Yes";
screenOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
super.onCreate(savedInstanceState);
}
}

@ -0,0 +1,72 @@
package jme3test.android;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.List;
/**
* The view adapter which gets a list of LaunchEntries and displaqs them
* @author larynx
*
*/
public class DemoLaunchAdapter extends BaseAdapter implements OnClickListener
{
private Context context;
private List<DemoLaunchEntry> listDemos;
public DemoLaunchAdapter(Context context, List<DemoLaunchEntry> listDemos) {
this.context = context;
this.listDemos = listDemos;
}
public int getCount() {
return listDemos.size();
}
public Object getItem(int position) {
return listDemos.get(position);
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup viewGroup) {
DemoLaunchEntry entry = listDemos.get(position);
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.demo_row, null);
}
TextView tvDemoName = (TextView) convertView.findViewById(R.id.tvDemoName);
tvDemoName.setText(entry.getName());
TextView tvDescription = (TextView) convertView.findViewById(R.id.tvDescription);
tvDescription.setText(entry.getDescription());
return convertView;
}
@Override
public void onClick(View view) {
DemoLaunchEntry entry = (DemoLaunchEntry) view.getTag();
}
private void showDialog(DemoLaunchEntry entry) {
// Create and show your dialog
// Depending on the Dialogs button clicks delete it or do nothing
}
}

@ -0,0 +1,38 @@
package jme3test.android;
/**
* Name (=appClass) and Description of one demo launch inside the main apk
* @author larynx
*
*/
public class DemoLaunchEntry
{
private String name;
private String description;
/**
* @param name
* @param description
*/
public DemoLaunchEntry(String name, String description) {
super();
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

@ -0,0 +1,131 @@
package jme3test.android;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
import java.util.ArrayList;
import java.util.List;
public class DemoMainActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
final Intent myIntent = new Intent(DemoMainActivity.this, DemoAndroidHarness.class);
//Next create the bundle and initialize it
final Bundle bundle = new Bundle();
final Spinner spinnerConfig = (Spinner) findViewById(R.id.spinnerConfig);
ArrayAdapter<CharSequence> adapterDropDownConfig = ArrayAdapter.createFromResource(
this, R.array.eglconfig_array, android.R.layout.simple_spinner_item);
adapterDropDownConfig.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerConfig.setAdapter(adapterDropDownConfig);
spinnerConfig.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
Toast.makeText(parent.getContext(), "Set EGLConfig " +
parent.getItemAtPosition(pos).toString(), Toast.LENGTH_LONG).show();
//Add the parameters to bundle as
bundle.putString("EGLCONFIG", parent.getItemAtPosition(pos).toString());
}
public void onNothingSelected(AdapterView parent) {
// Do nothing.
}
});
final Spinner spinnerLogging = (Spinner) findViewById(R.id.spinnerLogging);
ArrayAdapter<CharSequence> adapterDropDownLogging = ArrayAdapter.createFromResource(
this, R.array.logging_array, android.R.layout.simple_spinner_item);
adapterDropDownLogging.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerLogging.setAdapter(adapterDropDownLogging);
spinnerLogging.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
Toast.makeText(parent.getContext(), "Set Logging " +
parent.getItemAtPosition(pos).toString(), Toast.LENGTH_LONG).show();
//Add the parameters to bundle as
bundle.putBoolean("VERBOSE", parent.getItemAtPosition(pos).toString().equals("Verbose"));
}
public void onNothingSelected(AdapterView parent) {
// Do nothing.
}
});
ListView list = (ListView) findViewById(R.id.ListView01);
list.setClickable(true);
final List<DemoLaunchEntry> listDemos = new ArrayList<DemoLaunchEntry>();
listDemos.add(new DemoLaunchEntry("jme3test.android.SimpleTexturedTest", "An field of textured boxes rotating"));
listDemos.add(new DemoLaunchEntry("jme3test.android.TestSkyLoadingLagoon", "Sky box demonstration with jpg"));
listDemos.add(new DemoLaunchEntry("jme3test.android.TestSkyLoadingPrimitives", "Sky box demonstration with png"));
listDemos.add(new DemoLaunchEntry("jme3test.android.TestBumpModel", "Shows a bump mapped well with a moving light"));
listDemos.add(new DemoLaunchEntry("jme3test.android.TestNormalMapping", "Shows a normal mapped sphere"));
listDemos.add(new DemoLaunchEntry("jme3test.android.TestUnshadedModel", "Shows an unshaded model of the sphere"));
listDemos.add(new DemoLaunchEntry("jme3test.android.TestMovingParticle", "Demonstrates particle effects"));
listDemos.add(new DemoLaunchEntry("jme3test.android.TestAmbient", "Positional sound - You sit in a dark cave under a waterfall"));
//listDemos.add(new DemoLaunchEntry("jme3test.effect.TestParticleEmitter", ""));
//listDemos.add(new DemoLaunchEntry("jme3test.effect.TestPointSprite", ""));
//listDemos.add(new DemoLaunchEntry("jme3test.light.TestLightRadius", ""));
listDemos.add(new DemoLaunchEntry("jme3test.android.TestMotionPath", "Shows cinematics - see a teapot on its journey - model loading needs a long time - just let it load, looks like freezed"));
//listDemos.add(new DemoLaunchEntry("com.jme3.androiddemo.TestSimpleWater", "Post processors - not working correctly due to missing framebuffer support, looks interresting :)"));
//listDemos.add(new DemoLaunchEntry("jme3test.model.TestHoverTank", ""));
//listDemos.add(new DemoLaunchEntry("jme3test.niftygui.TestNiftyGui", ""));
//listDemos.add(new DemoLaunchEntry("com.jme3.androiddemo.TestNiftyGui", ""));
DemoLaunchAdapter adapterList = new DemoLaunchAdapter(this, listDemos);
list.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View view, int position, long index) {
System.out.println("onItemClick");
showToast(listDemos.get(position).getName());
//Add the parameters to bundle as
bundle.putString("APPCLASSNAME", listDemos.get(position).getName());
//Add this bundle to the intent
myIntent.putExtras(bundle);
//Start the JME3 app harness activity
DemoMainActivity.this.startActivity(myIntent);
}
});
list.setAdapter(adapterList);
}
private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}

@ -0,0 +1,46 @@
/* AUTO-GENERATED FILE. DO NOT MODIFY.
*
* This class was automatically generated by the
* aapt tool from the resource data it found. It
* should not be modified by hand.
*/
package jme3test.android;
public final class R {
public static final class array {
public static final int eglconfig_array=0x7f060000;
public static final int logging_array=0x7f060001;
}
public static final class attr {
}
public static final class drawable {
public static final int icon=0x7f020000;
}
public static final class id {
public static final int LinearLayout01=0x7f070000;
public static final int LinearLayout02=0x7f070002;
public static final int ListView01=0x7f070009;
public static final int TextView01=0x7f070003;
public static final int spinnerConfig=0x7f070006;
public static final int spinnerLogging=0x7f070008;
public static final int tvConfig=0x7f070005;
public static final int tvDemoName=0x7f070001;
public static final int tvDescription=0x7f070004;
public static final int tvLogging=0x7f070007;
}
public static final class layout {
public static final int demo_row=0x7f030000;
public static final int main=0x7f030001;
}
public static final class raw {
public static final int oddbounce=0x7f040000;
}
public static final class string {
public static final int app_name=0x7f050000;
public static final int eglconfig_prompt=0x7f050001;
public static final int eglconfig_text=0x7f050002;
public static final int logging_prompt=0x7f050003;
public static final int logging_text=0x7f050004;
}
}

@ -0,0 +1,40 @@
package jme3test.android;
import com.jme3.app.SimpleApplication;
import com.jme3.audio.AudioNode;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.InputListener;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.math.Vector3f;
public class SimpleSoundTest extends SimpleApplication implements InputListener {
private AudioNode gun;
private AudioNode nature;
@Override
public void simpleInitApp() {
gun = new AudioNode(assetManager, "Sound/Effects/Gun.wav");
gun.setPositional(true);
gun.setLocalTranslation(new Vector3f(0, 0, 0));
gun.setMaxDistance(100);
gun.setRefDistance(5);
nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", true);
nature.setVolume(3);
nature.setLooping(true);
nature.play();
inputManager.addMapping("click", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
inputManager.addListener(this, "click");
rootNode.attachChild(gun);
rootNode.attachChild(nature);
}
public void onAction(String name, boolean isPressed, float tpf) {
if (name.equals("click") && isPressed) {
gun.playInstance();
}
}
}

@ -0,0 +1,150 @@
/*
* Android 2.2+ SimpleTextured test.
*
* created: Mon Nov 8 00:08:22 EST 2010
*/
package jme3test.android;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.TextureKey;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.texture.Texture;
import com.jme3.util.TangentBinormalGenerator;
public class SimpleTexturedTest extends SimpleApplication {
private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(SimpleTexturedTest.class.getName());
private Node spheresContainer = new Node("spheres-container");
private boolean lightingEnabled = true;
private boolean texturedEnabled = true;
private boolean spheres = true;
@Override
public void simpleInitApp() {
//flyCam.setRotationSpeed(0.01f);
Mesh shapeSphere = null;
Mesh shapeBox = null;
shapeSphere = new Sphere(16, 16, .5f);
shapeBox = new Box(Vector3f.ZERO, 0.3f, 0.3f, 0.3f);
// ModelConverter.optimize(geom);
Texture texture = assetManager.loadTexture(new TextureKey("Interface/Logo/Monkey.jpg"));
Texture textureMonkey = assetManager.loadTexture(new TextureKey("Interface/Logo/Monkey.jpg"));
Material material = null;
Material materialMonkey = null;
if (texturedEnabled) {
if (lightingEnabled) {
material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
material.setBoolean("VertexLighting", true);
material.setFloat("Shininess", 127);
material.setBoolean("LowQuality", true);
material.setTexture("DiffuseMap", texture);
materialMonkey = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
materialMonkey.setBoolean("VertexLighting", true);
materialMonkey.setFloat("Shininess", 127);
materialMonkey.setBoolean("LowQuality", true);
materialMonkey.setTexture("DiffuseMap", textureMonkey);
} else {
material = new Material(assetManager, "Common/MatDefs/Misc/SimpleTextured.j3md");
material.setTexture("ColorMap", texture);
materialMonkey = new Material(assetManager, "Common/MatDefs/Misc/SimpleTextured.j3md");
materialMonkey.setTexture("ColorMap", textureMonkey);
}
} else {
material = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");
material.setColor("Color", ColorRGBA.Red);
materialMonkey = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");
materialMonkey.setColor("Color", ColorRGBA.Red);
}
TangentBinormalGenerator.generate(shapeSphere);
TangentBinormalGenerator.generate(shapeBox);
int iFlipper = 0;
for (int y = -1; y < 2; y++) {
for (int x = -1; x < 2; x++){
Geometry geomClone = null;
//iFlipper++;
if (iFlipper % 2 == 0)
{
geomClone = new Geometry("geometry-" + y + "-" + x, shapeBox);
}
else
{
geomClone = new Geometry("geometry-" + y + "-" + x, shapeSphere);
}
if (iFlipper % 3 == 0)
{
geomClone.setMaterial(materialMonkey);
}
else
{
geomClone.setMaterial(material);
}
geomClone.setLocalTranslation(x, y, 0);
// Transform t = geom.getLocalTransform().clone();
// Transform t2 = geomClone.getLocalTransform().clone();
// t.combineWithParent(t2);
// geomClone.setLocalTransform(t);
spheresContainer.attachChild(geomClone);
}
}
spheresContainer.setLocalTranslation(new Vector3f(0, 0, -4f));
spheresContainer.setLocalScale(2.0f);
rootNode.attachChild(spheresContainer);
PointLight pointLight = new PointLight();
pointLight.setColor(new ColorRGBA(0.7f, 0.7f, 1.0f, 1.0f));
pointLight.setPosition(new Vector3f(0f, 0f, 0f));
pointLight.setRadius(8);
rootNode.addLight(pointLight);
}
@Override
public void simpleUpdate(float tpf) {
// secondCounter has been removed from SimpleApplication
//if (secondCounter == 0)
// logger.fine("Frames per second: " + timer.getFrameRate());
spheresContainer.rotate(0.2f * tpf, 0.4f * tpf, 0.8f * tpf);
}
}

@ -0,0 +1,97 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package jme3test.android;
import android.media.SoundPool;
import com.jme3.app.SimpleApplication;
import com.jme3.audio.AudioNode;
import com.jme3.math.Vector3f;
public class TestAmbient extends SimpleApplication {
private AudioNode footsteps, beep;
private AudioNode nature, waves;
SoundPool soundPool;
// private PointAudioSource waves;
private float time = 0;
private float nextTime = 1;
public static void main(String[] args){
TestAmbient test = new TestAmbient();
test.start();
}
@Override
public void simpleInitApp()
{
/*
footsteps = new AudioNode(audioRenderer, assetManager, "Sound/Effects/Foot steps.ogg", true);
footsteps.setPositional(true);
footsteps.setLocalTranslation(new Vector3f(4, -1, 30));
footsteps.setMaxDistance(5);
footsteps.setRefDistance(1);
footsteps.setLooping(true);
beep = new AudioNode(audioRenderer, assetManager, "Sound/Effects/Beep.ogg", true);
beep.setVolume(3);
beep.setLooping(true);
audioRenderer.playSourceInstance(footsteps);
audioRenderer.playSource(beep);
*/
waves = new AudioNode(assetManager, "Sound/Environment/Ocean Waves.ogg", true);
waves.setPositional(true);
nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", true);
waves.setLocalTranslation(new Vector3f(4, -1, 30));
waves.setMaxDistance(5);
waves.setRefDistance(1);
nature.setVolume(3);
audioRenderer.playSourceInstance(waves);
audioRenderer.playSource(nature);
}
@Override
public void simpleUpdate(float tpf)
{
}
}

@ -0,0 +1,95 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package jme3test.android;
import com.jme3.app.SimpleApplication;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.ogre.OgreMeshKey;
import com.jme3.scene.shape.Sphere;
import com.jme3.util.TangentBinormalGenerator;
public class TestBumpModel extends SimpleApplication {
float angle;
PointLight pl;
Spatial lightMdl;
public static void main(String[] args){
TestBumpModel app = new TestBumpModel();
app.start();
}
@Override
public void simpleInitApp() {
Spatial signpost = (Spatial) assetManager.loadAsset(new OgreMeshKey("Models/Sign Post/Sign Post.mesh.xml"));
signpost.setMaterial( (Material) assetManager.loadMaterial("Models/Sign Post/Sign Post.j3m"));
TangentBinormalGenerator.generate(signpost);
rootNode.attachChild(signpost);
lightMdl = new Geometry("Light", new Sphere(10, 10, 0.1f));
lightMdl.setMaterial( (Material) assetManager.loadMaterial("Common/Materials/RedColor.j3m"));
rootNode.attachChild(lightMdl);
// flourescent main light
pl = new PointLight();
pl.setColor(new ColorRGBA(0.88f, 0.92f, 0.95f, 1.0f));
rootNode.addLight(pl);
AmbientLight al = new AmbientLight();
al.setColor(new ColorRGBA(0.44f, 0.40f, 0.20f, 1.0f));
rootNode.addLight(al);
DirectionalLight dl = new DirectionalLight();
dl.setDirection(new Vector3f(1,-1,-1).normalizeLocal());
dl.setColor(new ColorRGBA(0.92f, 0.85f, 0.8f, 1.0f));
rootNode.addLight(dl);
}
@Override
public void simpleUpdate(float tpf){
angle += tpf * 0.25f;
angle %= FastMath.TWO_PI;
pl.setPosition(new Vector3f(FastMath.cos(angle) * 6f, 3f, FastMath.sin(angle) * 6f));
lightMdl.setLocalTranslation(pl.getPosition());
}
}

@ -0,0 +1,102 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package jme3test.android;
import com.jme3.app.SimpleApplication;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh.Type;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
/**
* Particle that moves in a circle.
*
* @author Kirill Vainer
*/
public class TestMovingParticle extends SimpleApplication {
private ParticleEmitter emit;
private float angle = 0;
public static void main(String[] args) {
TestMovingParticle app = new TestMovingParticle();
app.start();
}
@Override
public void simpleInitApp() {
emit = new ParticleEmitter("Emitter", Type.Triangle, 300);
emit.setGravity(0, 0, 0);
emit.setVelocityVariation(1);
emit.setLowLife(1);
emit.setHighLife(1);
emit.setInitialVelocity(new Vector3f(0, .5f, 0));
emit.setImagesX(15);
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
mat.setTexture("Texture", assetManager.loadTexture("Effects/Smoke/Smoke.png"));
emit.setMaterial(mat);
rootNode.attachChild(emit);
AmbientLight al = new AmbientLight();
al.setColor(new ColorRGBA(0.84f, 0.80f, 0.80f, 1.0f));
rootNode.addLight(al);
inputManager.addListener(new ActionListener() {
public void onAction(String name, boolean isPressed, float tpf) {
if ("setNum".equals(name) && isPressed) {
emit.setNumParticles(1000);
}
}
}, "setNum");
inputManager.addMapping("setNum", new KeyTrigger(KeyInput.KEY_SPACE));
}
@Override
public void simpleUpdate(float tpf) {
angle += tpf;
angle %= FastMath.TWO_PI;
float x = FastMath.cos(angle) * 2;
float y = FastMath.sin(angle) * 2;
emit.setLocalTranslation(x, 0, y);
}
}

@ -0,0 +1,99 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package jme3test.android;
import com.jme3.app.SimpleApplication;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Sphere;
import com.jme3.util.TangentBinormalGenerator;
public class TestNormalMapping extends SimpleApplication {
float angle;
PointLight pl;
Spatial lightMdl;
public static void main(String[] args){
TestNormalMapping app = new TestNormalMapping();
app.start();
}
@Override
public void simpleInitApp() {
Sphere sphMesh = new Sphere(32, 32, 1);
sphMesh.setTextureMode(Sphere.TextureMode.Projected);
sphMesh.updateGeometry(32, 32, 1, false, false);
TangentBinormalGenerator.generate(sphMesh);
Geometry sphere = new Geometry("Rock Ball", sphMesh);
Material mat = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m");
sphere.setMaterial(mat);
rootNode.attachChild(sphere);
lightMdl = new Geometry("Light", new Sphere(10, 10, 0.1f));
lightMdl.setMaterial(assetManager.loadMaterial("Common/Materials/RedColor.j3m"));
rootNode.attachChild(lightMdl);
pl = new PointLight();
pl.setColor(ColorRGBA.White);
pl.setPosition(new Vector3f(0f, 0f, 4f));
rootNode.addLight(pl);
AmbientLight al = new AmbientLight();
al.setColor(new ColorRGBA(0.44f, 0.40f, 0.20f, 1.0f));
rootNode.addLight(al);
DirectionalLight dl = new DirectionalLight();
dl.setDirection(new Vector3f(1,-1,-1).normalizeLocal());
dl.setColor(new ColorRGBA(0.92f, 0.85f, 0.8f, 1.0f));
rootNode.addLight(dl);
}
@Override
public void simpleUpdate(float tpf){
angle += tpf * 0.25f;
angle %= FastMath.TWO_PI;
pl.setPosition(new Vector3f(FastMath.cos(angle) * 4f, 0.5f, FastMath.sin(angle) * 4f));
lightMdl.setLocalTranslation(pl.getPosition());
}
}

@ -0,0 +1,70 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package jme3test.android;
import com.jme3.app.SimpleApplication;
import com.jme3.scene.Spatial;
import com.jme3.texture.Texture;
import com.jme3.util.SkyFactory;
public class TestSkyLoadingLagoon extends SimpleApplication {
public static void main(String[] args){
TestSkyLoadingLagoon app = new TestSkyLoadingLagoon();
app.start();
}
public void simpleInitApp() {
Texture west = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_west.jpg");
Texture east = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_east.jpg");
Texture north = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_north.jpg");
Texture south = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_south.jpg");
Texture up = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_up.jpg");
Texture down = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_down.jpg");
/*
Texture west = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_x.png");
Texture east = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_x.png");
Texture north = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_z.png");
Texture south = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_z.png");
Texture up = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_y.png");
Texture down = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_y.png");
*/
Spatial sky = SkyFactory.createSky(assetManager, west, east, north, south, up, down);
rootNode.attachChild(sky);
}
}

@ -0,0 +1,68 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package jme3test.android;
import com.jme3.app.SimpleApplication;
import com.jme3.scene.Spatial;
import com.jme3.texture.Texture;
import com.jme3.util.SkyFactory;
public class TestSkyLoadingPrimitives extends SimpleApplication {
public static void main(String[] args){
TestSkyLoadingPrimitives app = new TestSkyLoadingPrimitives();
app.start();
}
public void simpleInitApp() {
/*
Texture west = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_west.jpg");
Texture east = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_east.jpg");
Texture north = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_north.jpg");
Texture south = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_south.jpg");
Texture up = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_up.jpg");
Texture down = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_down.jpg");
*/
Texture west = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_x.png");
Texture east = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_x.png");
Texture north = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_z.png");
Texture south = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_z.png");
Texture up = assetManager.loadTexture("Textures/Sky/Primitives/primitives_positive_y.png");
Texture down = assetManager.loadTexture("Textures/Sky/Primitives/primitives_negative_y.png");
Spatial sky = SkyFactory.createSky(assetManager, west, east, north, south, up, down);
rootNode.attachChild(sky);
}
}

@ -0,0 +1,44 @@
package jme3test.android;
import com.jme3.app.SimpleApplication;
import com.jme3.light.AmbientLight;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Sphere;
import com.jme3.util.TangentBinormalGenerator;
public class TestUnshadedModel extends SimpleApplication {
public static void main(String[] args){
TestUnshadedModel app = new TestUnshadedModel();
app.start();
}
@Override
public void simpleInitApp() {
Sphere sphMesh = new Sphere(32, 32, 1);
sphMesh.setTextureMode(Sphere.TextureMode.Projected);
sphMesh.updateGeometry(32, 32, 1, false, false);
TangentBinormalGenerator.generate(sphMesh);
Geometry sphere = new Geometry("Rock Ball", sphMesh);
Material mat = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m");
mat.setColor("Ambient", ColorRGBA.DarkGray);
mat.setColor("Diffuse", ColorRGBA.White);
mat.setBoolean("UseMaterialColors", true);
sphere.setMaterial(mat);
rootNode.attachChild(sphere);
PointLight pl = new PointLight();
pl.setColor(ColorRGBA.White);
pl.setPosition(new Vector3f(4f, 0f, 0f));
rootNode.addLight(pl);
AmbientLight al = new AmbientLight();
al.setColor(ColorRGBA.White);
rootNode.addLight(al);
}
}

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<LinearLayout
android:id="@+id/buttonsContainer"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="copyright (c) 2009-2010 JMonkeyEngine"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="http://www.jmonkeyengine.org"
/>
</LinearLayout>
</LinearLayout>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<LinearLayout
android:id="@+id/buttonsContainer"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<!--
<Button
android:id="@+id/SimpleTextured"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:text="Simple Textured"
android:layout_weight="1"
/>
-->
</LinearLayout>
</LinearLayout>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/about_button"
android:title="@string/about"
/>
<item
android:id="@+id/quit_button"
android:title="@string/quit"
/>
</menu>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">JMEAndroidTest</string>
<string name="about">About</string>
<string name="quit">Quit</string>
</resources>

@ -0,0 +1,909 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.asset;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import com.jme3.bounding.BoundingVolume;
import com.jme3.collision.Collidable;
import com.jme3.collision.CollisionResults;
import com.jme3.collision.UnsupportedCollisionException;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.material.Material;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.CameraNode;
import com.jme3.scene.LightNode;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitor;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.animations.AnimationData;
import com.jme3.texture.Texture;
/**
* Blender key. Contains path of the blender file and its loading properties.
* @author Marcin Roguski (Kaelthas)
*/
public class BlenderKey extends ModelKey {
protected static final int DEFAULT_FPS = 25;
/**
* FramesPerSecond parameter describe how many frames there are in each second. It allows to calculate the time
* between the frames.
*/
protected int fps = DEFAULT_FPS;
/**
* This variable is a bitwise flag of FeatureToLoad interface values; By default everything is being loaded.
*/
protected int featuresToLoad = FeaturesToLoad.ALL;
/** This variable determines if assets that are not linked to the objects should be loaded. */
protected boolean loadUnlinkedAssets;
/** The root path for all the assets. */
protected String assetRootPath;
/** This variable indicate if Y axis is UP axis. If not then Z is up. By default set to true. */
protected boolean fixUpAxis = true;
/** Generated textures resolution (PPU - Pixels Per Unit). */
protected int generatedTexturePPU = 128;
/**
* The name of world settings that the importer will use. If not set or specified name does not occur in the file
* then the first world settings in the file will be used.
*/
protected String usedWorld;
/**
* User's default material that is set fo objects that have no material definition in blender. The default value is
* null. If the value is null the importer will use its own default material (gray color - like in blender).
*/
protected Material defaultMaterial;
/** Face cull mode. By default it is disabled. */
protected FaceCullMode faceCullMode = FaceCullMode.Back;
/**
* Variable describes which layers will be loaded. N-th bit set means N-th layer will be loaded.
* If set to -1 then the current layer will be loaded.
*/
protected int layersToLoad = -1;
/** A variable that toggles the object custom properties loading. */
protected boolean loadObjectProperties = true;
/**
* Maximum texture size. Might be dependant on the graphic card.
* This value is taken from <b>org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE</b>.
*/
protected int maxTextureSize = 8192;
/** Allows to toggle generated textures loading. Disabled by default because it very often takes too much memory and needs to be used wisely. */
protected boolean loadGeneratedTextures;
/** Tells if the mipmaps will be generated by jme or not. By default generation is dependant on the blender settings. */
protected MipmapGenerationMethod mipmapGenerationMethod = MipmapGenerationMethod.GENERATE_WHEN_NEEDED;
/**
* If the sky has only generated textures applied then they will have the following size (both width and height). If 2d textures are used then the generated
* textures will get their proper size.
*/
protected int skyGeneratedTextureSize = 1000;
/** The radius of a shape that will be used while creating the generated texture for the sky. The higher it is the larger part of the texture will be seen. */
protected float skyGeneratedTextureRadius = 1;
/** The shape against which the generated texture for the sky will be created. */
protected SkyGeneratedTextureShape skyGeneratedTextureShape = SkyGeneratedTextureShape.SPHERE;
/**
* This field tells if the importer should optimise the use of textures or not. If set to true, then textures of the same mapping type will be merged together
* and textures that in the final result will never be visible - will be discarded.
*/
protected boolean optimiseTextures;
/**
* Constructor used by serialization mechanisms.
*/
public BlenderKey() {
}
/**
* Constructor. Creates a key for the given file name.
* @param name
* the name (path) of a file
*/
public BlenderKey(String name) {
super(name);
}
/**
* This method returns frames per second amount. The default value is BlenderKey.DEFAULT_FPS = 25.
* @return the frames per second amount
*/
public int getFps() {
return fps;
}
/**
* This method sets frames per second amount.
* @param fps
* the frames per second amount
*/
public void setFps(int fps) {
this.fps = fps;
}
/**
* This method returns the face cull mode.
* @return the face cull mode
*/
public FaceCullMode getFaceCullMode() {
return faceCullMode;
}
/**
* This method sets the face cull mode.
* @param faceCullMode
* the face cull mode
*/
public void setFaceCullMode(FaceCullMode faceCullMode) {
this.faceCullMode = faceCullMode;
}
/**
* This method sets layers to be loaded.
* @param layersToLoad
* layers to be loaded
*/
public void setLayersToLoad(int layersToLoad) {
this.layersToLoad = layersToLoad;
}
/**
* This method returns layers to be loaded.
* @return layers to be loaded
*/
public int getLayersToLoad() {
return layersToLoad;
}
/**
* This method sets the properies loading policy.
* By default the value is true.
* @param loadObjectProperties
* true to load properties and false to suspend their loading
*/
public void setLoadObjectProperties(boolean loadObjectProperties) {
this.loadObjectProperties = loadObjectProperties;
}
/**
* @return the current properties loading properties
*/
public boolean isLoadObjectProperties() {
return loadObjectProperties;
}
/**
* The default value for this parameter is the same as defined by: org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE.
* If by any means this is too large for user's hardware configuration use the 'setMaxTextureSize' method to change that.
* @return maximum texture size (width/height)
*/
public int getMaxTextureSize() {
return maxTextureSize;
}
/**
* This method sets the maximum texture size.
* @param maxTextureSize
* the maximum texture size
*/
public void setMaxTextureSize(int maxTextureSize) {
this.maxTextureSize = maxTextureSize;
}
/**
* This method sets the flag that toggles the generated textures loading.
* @param loadGeneratedTextures
* <b>true</b> if generated textures should be loaded and <b>false</b> otherwise
*/
public void setLoadGeneratedTextures(boolean loadGeneratedTextures) {
this.loadGeneratedTextures = loadGeneratedTextures;
}
/**
* @return tells if the generated textures should be loaded (<b>false</b> is the default value)
*/
public boolean isLoadGeneratedTextures() {
return loadGeneratedTextures;
}
/**
* This method sets the asset root path.
* @param assetRootPath
* the assets root path
*/
public void setAssetRootPath(String assetRootPath) {
this.assetRootPath = assetRootPath;
}
/**
* This method returns the asset root path.
* @return the asset root path
*/
public String getAssetRootPath() {
return assetRootPath;
}
/**
* This method adds features to be loaded.
* @param featuresToLoad
* bitwise flag of FeaturesToLoad interface values
*/
public void includeInLoading(int featuresToLoad) {
this.featuresToLoad |= featuresToLoad;
}
/**
* This method removes features from being loaded.
* @param featuresNotToLoad
* bitwise flag of FeaturesToLoad interface values
*/
public void excludeFromLoading(int featuresNotToLoad) {
featuresToLoad &= ~featuresNotToLoad;
}
public boolean shouldLoad(int featureToLoad) {
return (featuresToLoad & featureToLoad) != 0;
}
/**
* This method returns bitwise value of FeaturesToLoad interface value. It describes features that will be loaded by
* the blender file loader.
* @return features that will be loaded by the blender file loader
*/
public int getFeaturesToLoad() {
return featuresToLoad;
}
/**
* This method determines if unlinked assets should be loaded.
* If not then only objects on selected layers will be loaded and their assets if required.
* If yes then all assets will be loaded even if they are on inactive layers or are not linked
* to anything.
* @return <b>true</b> if unlinked assets should be loaded and <b>false</b> otherwise
*/
public boolean isLoadUnlinkedAssets() {
return loadUnlinkedAssets;
}
/**
* This method sets if unlinked assets should be loaded.
* If not then only objects on selected layers will be loaded and their assets if required.
* If yes then all assets will be loaded even if they are on inactive layers or are not linked
* to anything.
* @param loadUnlinkedAssets
* <b>true</b> if unlinked assets should be loaded and <b>false</b> otherwise
*/
public void setLoadUnlinkedAssets(boolean loadUnlinkedAssets) {
this.loadUnlinkedAssets = loadUnlinkedAssets;
}
/**
* This method creates an object where loading results will be stores. Only those features will be allowed to store
* that were specified by features-to-load flag.
* @return an object to store loading results
*/
public LoadingResults prepareLoadingResults() {
return new LoadingResults(featuresToLoad);
}
/**
* This method sets the fix up axis state. If set to true then Y is up axis. Otherwise the up i Z axis. By default Y
* is up axis.
* @param fixUpAxis
* the up axis state variable
*/
public void setFixUpAxis(boolean fixUpAxis) {
this.fixUpAxis = fixUpAxis;
}
/**
* This method returns the fix up axis state. If set to true then Y is up axis. Otherwise the up i Z axis. By
* default Y is up axis.
* @return the up axis state variable
*/
public boolean isFixUpAxis() {
return fixUpAxis;
}
/**
* This method sets the generated textures resolution.
* @param generatedTexturePPU
* the generated textures resolution
*/
public void setGeneratedTexturePPU(int generatedTexturePPU) {
this.generatedTexturePPU = generatedTexturePPU;
}
/**
* @return the generated textures resolution
*/
public int getGeneratedTexturePPU() {
return generatedTexturePPU;
}
/**
* @return mipmaps generation method
*/
public MipmapGenerationMethod getMipmapGenerationMethod() {
return mipmapGenerationMethod;
}
/**
* @param mipmapGenerationMethod
* mipmaps generation method
*/
public void setMipmapGenerationMethod(MipmapGenerationMethod mipmapGenerationMethod) {
this.mipmapGenerationMethod = mipmapGenerationMethod;
}
/**
* @return the size of the generated textures for the sky (used if no flat textures are applied)
*/
public int getSkyGeneratedTextureSize() {
return skyGeneratedTextureSize;
}
/**
* @param skyGeneratedTextureSize
* the size of the generated textures for the sky (used if no flat textures are applied)
*/
public void setSkyGeneratedTextureSize(int skyGeneratedTextureSize) {
if (skyGeneratedTextureSize <= 0) {
throw new IllegalArgumentException("The texture size must be a positive value (the value given as a parameter: " + skyGeneratedTextureSize + ")!");
}
this.skyGeneratedTextureSize = skyGeneratedTextureSize;
}
/**
* @return the radius of a shape that will be used while creating the generated texture for the sky, the higher it is the larger part of the texture will be seen
*/
public float getSkyGeneratedTextureRadius() {
return skyGeneratedTextureRadius;
}
/**
* @param skyGeneratedTextureRadius
* the radius of a shape that will be used while creating the generated texture for the sky, the higher it is the larger part of the texture will be seen
*/
public void setSkyGeneratedTextureRadius(float skyGeneratedTextureRadius) {
this.skyGeneratedTextureRadius = skyGeneratedTextureRadius;
}
/**
* @return the shape against which the generated texture for the sky will be created (by default it is a sphere).
*/
public SkyGeneratedTextureShape getSkyGeneratedTextureShape() {
return skyGeneratedTextureShape;
}
/**
* @param skyGeneratedTextureShape
* the shape against which the generated texture for the sky will be created
*/
public void setSkyGeneratedTextureShape(SkyGeneratedTextureShape skyGeneratedTextureShape) {
if (skyGeneratedTextureShape == null) {
throw new IllegalArgumentException("The sky generated shape type cannot be null!");
}
this.skyGeneratedTextureShape = skyGeneratedTextureShape;
}
/**
* If set to true, then textures of the same mapping type will be merged together
* and textures that in the final result will never be visible - will be discarded.
* @param optimiseTextures
* the variable that tells if the textures should be optimised or not
*/
public void setOptimiseTextures(boolean optimiseTextures) {
this.optimiseTextures = optimiseTextures;
}
/**
* @return the variable that tells if the textures should be optimised or not (by default the optimisation is disabled)
*/
public boolean isOptimiseTextures() {
return optimiseTextures;
}
/**
* This mehtod sets the name of the WORLD data block taht should be used during file loading. By default the name is
* not set. If no name is set or the given name does not occur in the file - the first WORLD data block will be used
* during loading (assumin any exists in the file).
* @param usedWorld
* the name of the WORLD block used during loading
*/
public void setUsedWorld(String usedWorld) {
this.usedWorld = usedWorld;
}
/**
* This mehtod returns the name of the WORLD data block taht should be used during file loading.
* @return the name of the WORLD block used during loading
*/
public String getUsedWorld() {
return usedWorld;
}
/**
* This method sets the default material for objects.
* @param defaultMaterial
* the default material
*/
public void setDefaultMaterial(Material defaultMaterial) {
this.defaultMaterial = defaultMaterial;
}
/**
* This method returns the default material.
* @return the default material
*/
public Material getDefaultMaterial() {
return defaultMaterial;
}
@Override
public void write(JmeExporter e) throws IOException {
super.write(e);
OutputCapsule oc = e.getCapsule(this);
oc.write(fps, "fps", DEFAULT_FPS);
oc.write(featuresToLoad, "features-to-load", FeaturesToLoad.ALL);
oc.write(loadUnlinkedAssets, "load-unlinked-assets", false);
oc.write(assetRootPath, "asset-root-path", null);
oc.write(fixUpAxis, "fix-up-axis", true);
oc.write(generatedTexturePPU, "generated-texture-ppu", 128);
oc.write(usedWorld, "used-world", null);
oc.write(defaultMaterial, "default-material", null);
oc.write(faceCullMode, "face-cull-mode", FaceCullMode.Off);
oc.write(layersToLoad, "layers-to-load", -1);
oc.write(mipmapGenerationMethod, "mipmap-generation-method", MipmapGenerationMethod.GENERATE_WHEN_NEEDED);
oc.write(skyGeneratedTextureSize, "sky-generated-texture-size", 1000);
oc.write(skyGeneratedTextureRadius, "sky-generated-texture-radius", 1f);
oc.write(skyGeneratedTextureShape, "sky-generated-texture-shape", SkyGeneratedTextureShape.SPHERE);
oc.write(optimiseTextures, "optimise-textures", false);
}
@Override
public void read(JmeImporter e) throws IOException {
super.read(e);
InputCapsule ic = e.getCapsule(this);
fps = ic.readInt("fps", DEFAULT_FPS);
featuresToLoad = ic.readInt("features-to-load", FeaturesToLoad.ALL);
loadUnlinkedAssets = ic.readBoolean("load-unlinked-assets", false);
assetRootPath = ic.readString("asset-root-path", null);
fixUpAxis = ic.readBoolean("fix-up-axis", true);
generatedTexturePPU = ic.readInt("generated-texture-ppu", 128);
usedWorld = ic.readString("used-world", null);
defaultMaterial = (Material) ic.readSavable("default-material", null);
faceCullMode = ic.readEnum("face-cull-mode", FaceCullMode.class, FaceCullMode.Off);
layersToLoad = ic.readInt("layers-to=load", -1);
mipmapGenerationMethod = ic.readEnum("mipmap-generation-method", MipmapGenerationMethod.class, MipmapGenerationMethod.GENERATE_WHEN_NEEDED);
skyGeneratedTextureSize = ic.readInt("sky-generated-texture-size", 1000);
skyGeneratedTextureRadius = ic.readFloat("sky-generated-texture-radius", 1f);
skyGeneratedTextureShape = ic.readEnum("sky-generated-texture-shape", SkyGeneratedTextureShape.class, SkyGeneratedTextureShape.SPHERE);
optimiseTextures = ic.readBoolean("optimise-textures", false);
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + (assetRootPath == null ? 0 : assetRootPath.hashCode());
result = prime * result + (defaultMaterial == null ? 0 : defaultMaterial.hashCode());
result = prime * result + (faceCullMode == null ? 0 : faceCullMode.hashCode());
result = prime * result + featuresToLoad;
result = prime * result + (fixUpAxis ? 1231 : 1237);
result = prime * result + fps;
result = prime * result + generatedTexturePPU;
result = prime * result + layersToLoad;
result = prime * result + (loadGeneratedTextures ? 1231 : 1237);
result = prime * result + (loadObjectProperties ? 1231 : 1237);
result = prime * result + (loadUnlinkedAssets ? 1231 : 1237);
result = prime * result + maxTextureSize;
result = prime * result + (mipmapGenerationMethod == null ? 0 : mipmapGenerationMethod.hashCode());
result = prime * result + (optimiseTextures ? 1231 : 1237);
result = prime * result + Float.floatToIntBits(skyGeneratedTextureRadius);
result = prime * result + (skyGeneratedTextureShape == null ? 0 : skyGeneratedTextureShape.hashCode());
result = prime * result + skyGeneratedTextureSize;
result = prime * result + (usedWorld == null ? 0 : usedWorld.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
BlenderKey other = (BlenderKey) obj;
if (assetRootPath == null) {
if (other.assetRootPath != null) {
return false;
}
} else if (!assetRootPath.equals(other.assetRootPath)) {
return false;
}
if (defaultMaterial == null) {
if (other.defaultMaterial != null) {
return false;
}
} else if (!defaultMaterial.equals(other.defaultMaterial)) {
return false;
}
if (faceCullMode != other.faceCullMode) {
return false;
}
if (featuresToLoad != other.featuresToLoad) {
return false;
}
if (fixUpAxis != other.fixUpAxis) {
return false;
}
if (fps != other.fps) {
return false;
}
if (generatedTexturePPU != other.generatedTexturePPU) {
return false;
}
if (layersToLoad != other.layersToLoad) {
return false;
}
if (loadGeneratedTextures != other.loadGeneratedTextures) {
return false;
}
if (loadObjectProperties != other.loadObjectProperties) {
return false;
}
if (loadUnlinkedAssets != other.loadUnlinkedAssets) {
return false;
}
if (maxTextureSize != other.maxTextureSize) {
return false;
}
if (mipmapGenerationMethod != other.mipmapGenerationMethod) {
return false;
}
if (optimiseTextures != other.optimiseTextures) {
return false;
}
if (Float.floatToIntBits(skyGeneratedTextureRadius) != Float.floatToIntBits(other.skyGeneratedTextureRadius)) {
return false;
}
if (skyGeneratedTextureShape != other.skyGeneratedTextureShape) {
return false;
}
if (skyGeneratedTextureSize != other.skyGeneratedTextureSize) {
return false;
}
if (usedWorld == null) {
if (other.usedWorld != null) {
return false;
}
} else if (!usedWorld.equals(other.usedWorld)) {
return false;
}
return true;
}
/**
* This enum tells the importer if the mipmaps for textures will be generated by jme. <li>NEVER_GENERATE and ALWAYS_GENERATE are quite understandable <li>GENERATE_WHEN_NEEDED is an option that checks if the texture had 'Generate mipmaps' option set in blender, mipmaps are generated only when the option is set
* @author Marcin Roguski (Kaelthas)
*/
public static enum MipmapGenerationMethod {
NEVER_GENERATE, ALWAYS_GENERATE, GENERATE_WHEN_NEEDED;
}
/**
* This interface describes the features of the scene that are to be loaded.
* @author Marcin Roguski (Kaelthas)
*/
public static interface FeaturesToLoad {
int SCENES = 0x0000FFFF;
int OBJECTS = 0x0000000B;
int ANIMATIONS = 0x00000004;
int MATERIALS = 0x00000003;
int TEXTURES = 0x00000001;
int CAMERAS = 0x00000020;
int LIGHTS = 0x00000010;
int WORLD = 0x00000040;
int ALL = 0xFFFFFFFF;
}
/**
* The shape againts which the sky generated texture will be created.
*
* @author Marcin Roguski (Kaelthas)
*/
public static enum SkyGeneratedTextureShape {
CUBE, SPHERE;
}
/**
* This class holds the loading results according to the given loading flag.
* @author Marcin Roguski (Kaelthas)
*/
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<AnimationData> animations;
/** All cameras from the file. */
private List<CameraNode> cameras;
/** All lights from the file. */
private List<LightNode> lights;
/** Loaded sky. */
private Spatial sky;
/**
* The background color of the render loaded from the horizon color of the world. If no world is used than the gray color
* is set to default (as in blender editor.
*/
private ColorRGBA backgroundColor = ColorRGBA.Gray;
/**
* Private constructor prevents users to create an instance of this class from outside the
* @param featuresToLoad
* bitwise mask of features that are to be loaded
* @see FeaturesToLoad FeaturesToLoad
*/
private LoadingResults(int featuresToLoad) {
this.featuresToLoad = featuresToLoad;
if ((featuresToLoad & FeaturesToLoad.SCENES) != 0) {
scenes = new ArrayList<Node>();
}
if ((featuresToLoad & FeaturesToLoad.OBJECTS) != 0) {
objects = new ArrayList<Node>();
if ((featuresToLoad & FeaturesToLoad.MATERIALS) != 0) {
materials = new ArrayList<Material>();
if ((featuresToLoad & FeaturesToLoad.TEXTURES) != 0) {
textures = new ArrayList<Texture>();
}
}
if ((featuresToLoad & FeaturesToLoad.ANIMATIONS) != 0) {
animations = new ArrayList<AnimationData>();
}
}
if ((featuresToLoad & FeaturesToLoad.CAMERAS) != 0) {
cameras = new ArrayList<CameraNode>();
}
if ((featuresToLoad & FeaturesToLoad.LIGHTS) != 0) {
lights = new ArrayList<LightNode>();
}
}
/**
* This method returns a bitwise flag describing what features of the blend file will be included in the result.
* @return bitwise mask of features that are to be loaded
* @see FeaturesToLoad FeaturesToLoad
*/
public int getLoadedFeatures() {
return featuresToLoad;
}
/**
* This method adds a scene to the result set.
* @param scene
* scene to be added to the result set
*/
public void addScene(Node scene) {
if (scenes != null) {
scenes.add(scene);
}
}
/**
* This method adds an object to the result set.
* @param object
* object to be added to the result set
*/
public void addObject(Node object) {
if (objects != null) {
objects.add(object);
}
}
/**
* This method adds a material to the result set.
* @param material
* material to be added to the result set
*/
public void addMaterial(Material material) {
if (materials != null) {
materials.add(material);
}
}
/**
* This method adds a texture to the result set.
* @param texture
* texture to be added to the result set
*/
public void addTexture(Texture texture) {
if (textures != null) {
textures.add(texture);
}
}
/**
* This method adds a camera to the result set.
* @param camera
* camera to be added to the result set
*/
public void addCamera(CameraNode camera) {
if (cameras != null) {
cameras.add(camera);
}
}
/**
* This method adds a light to the result set.
* @param light
* light to be added to the result set
*/
public void addLight(LightNode light) {
if (lights != null) {
lights.add(light);
}
}
/**
* This method sets the sky of the scene. Only one sky can be set.
* @param sky
* the sky to be set
*/
public void setSky(Spatial sky) {
this.sky = sky;
}
/**
* @param backgroundColor
* the background color
*/
public void setBackgroundColor(ColorRGBA backgroundColor) {
this.backgroundColor = backgroundColor;
}
/**
* @return all loaded scenes
*/
public List<Node> getScenes() {
return scenes;
}
/**
* @return all loaded objects
*/
public List<Node> getObjects() {
return objects;
}
/**
* @return all loaded materials
*/
public List<Material> getMaterials() {
return materials;
}
/**
* @return all loaded textures
*/
public List<Texture> getTextures() {
return textures;
}
/**
* @return all loaded animations
*/
public List<AnimationData> getAnimations() {
return animations;
}
/**
* @return all loaded cameras
*/
public List<CameraNode> getCameras() {
return cameras;
}
/**
* @return all loaded lights
*/
public List<LightNode> getLights() {
return lights;
}
/**
* @return the scene's sky
*/
public Spatial getSky() {
return sky;
}
/**
* @return the background color
*/
public ColorRGBA getBackgroundColor() {
return backgroundColor;
}
public int collideWith(Collidable other, CollisionResults results) throws UnsupportedCollisionException {
return 0;
}
@Override
public void updateModelBound() {
}
@Override
public void setModelBound(BoundingVolume modelBound) {
}
@Override
public int getVertexCount() {
return 0;
}
@Override
public int getTriangleCount() {
return 0;
}
@Override
public Spatial deepClone() {
return null;
}
@Override
public void depthFirstTraversal(SceneGraphVisitor visitor) {
}
@Override
protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
}
}
}

@ -0,0 +1,69 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.asset;
/**
* This key is mostly used to distinguish between textures that are loaded from
* the given assets and those being generated automatically. Every generated
* texture will have this kind of key attached.
*
* @author Marcin Roguski (Kaelthas)
*/
public class GeneratedTextureKey extends TextureKey {
/**
* Constructor. Stores the name. Extension and folder name are empty
* strings.
*
* @param name
* the name of the texture
*/
public GeneratedTextureKey(String name) {
super(name);
}
@Override
public String getExtension() {
return "";
}
@Override
public String getFolder() {
return "";
}
@Override
public String toString() {
return "Generated texture [" + name + "]";
}
}

@ -0,0 +1,132 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender;
import java.util.Arrays;
import java.util.List;
import com.jme3.export.Savable;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.Properties;
/**
* A purpose of the helper class is to split calculation code into several classes. Each helper after use should be cleared because it can
* hold the state of the calculations.
* @author Marcin Roguski
*/
public abstract class AbstractBlenderHelper {
/** The blender context. */
protected BlenderContext blenderContext;
/** The version of the blend file. */
protected final int blenderVersion;
/** This variable indicates if the Y asxis is the UP axis or not. */
protected boolean fixUpAxis;
/** Quaternion used to rotate data when Y is up axis. */
protected Quaternion upAxisRotationQuaternion;
/**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
* versions.
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public AbstractBlenderHelper(String blenderVersion, BlenderContext blenderContext) {
this.blenderVersion = Integer.parseInt(blenderVersion);
this.blenderContext = blenderContext;
fixUpAxis = blenderContext.getBlenderKey().isFixUpAxis();
if (fixUpAxis) {
upAxisRotationQuaternion = new Quaternion().fromAngles(-FastMath.HALF_PI, 0, 0);
}
}
/**
* This method loads the properties if they are available and defined for the structure.
* @param structure
* the structure we read the properties from
* @param blenderContext
* the blender context
* @return loaded properties or null if they are not available
* @throws BlenderFileException
* an exception is thrown when the blend file is somehow corrupted
*/
protected Properties loadProperties(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
Properties properties = null;
Structure id = (Structure) structure.getFieldValue("ID");
if (id != null) {
Pointer pProperties = (Pointer) id.getFieldValue("properties");
if (pProperties.isNotNull()) {
Structure propertiesStructure = pProperties.fetchData().get(0);
properties = new Properties();
properties.load(propertiesStructure, blenderContext);
}
}
return properties;
}
/**
* The method applies properties to the given spatial. The Properties
* instance cannot be directly applied because the end-user might not have
* the blender plugin jar file and thus receive ClassNotFoundException. The
* values are set by name instead.
*
* @param spatial
* the spatial that is to have properties applied
* @param properties
* the properties to be applied
*/
protected void applyProperties(Spatial spatial, Properties properties) {
List<String> propertyNames = properties.getSubPropertiesNames();
if (propertyNames != null && propertyNames.size() > 0) {
for (String propertyName : propertyNames) {
Object value = properties.findValue(propertyName);
if (value instanceof Savable || value instanceof Boolean || value instanceof String || value instanceof Float || value instanceof Integer || value instanceof Long) {
spatial.setUserData(propertyName, value);
} else if (value instanceof Double) {
spatial.setUserData(propertyName, ((Double) value).floatValue());
} else if (value instanceof int[]) {
spatial.setUserData(propertyName, Arrays.toString((int[]) value));
} else if (value instanceof float[]) {
spatial.setUserData(propertyName, Arrays.toString((float[]) value));
} else if (value instanceof double[]) {
spatial.setUserData(propertyName, Arrays.toString((double[]) value));
}
}
}
}
}

@ -0,0 +1,636 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender;
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import com.jme3.animation.Bone;
import com.jme3.animation.Skeleton;
import com.jme3.asset.AssetManager;
import com.jme3.asset.BlenderKey;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.animations.AnimationData;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.Constraint;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.DnaBlockData;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.MeshContext;
/**
* The class that stores temporary data and manages it during loading the belnd
* file. This class is intended to be used in a single loading thread. It holds
* the state of loading operations.
*
* @author Marcin Roguski (Kaelthas)
*/
public class BlenderContext {
/** The blender file version. */
private int blenderVersion;
/** The blender key. */
private BlenderKey blenderKey;
/** The header of the file block. */
private DnaBlockData dnaBlockData;
/** The scene structure. */
private Structure sceneStructure;
/** The input stream of the blend file. */
private BlenderInputStream inputStream;
/** The asset manager. */
private AssetManager assetManager;
/** The blocks read from the file. */
protected List<FileBlockHeader> blocks;
/**
* A map containing the file block headers. The key is the old memory address.
*/
private Map<Long, FileBlockHeader> fileBlockHeadersByOma = new HashMap<Long, FileBlockHeader>();
/** A map containing the file block headers. The key is the block code. */
private Map<Integer, List<FileBlockHeader>> fileBlockHeadersByCode = new HashMap<Integer, List<FileBlockHeader>>();
/**
* This map stores the loaded features by their old memory address. The
* first object in the value table is the loaded structure and the second -
* the structure already converted into proper data.
*/
private Map<Long, Object[]> loadedFeatures = new HashMap<Long, Object[]>();
/**
* This map stores the loaded features by their name. Only features with ID
* structure can be stored here. The first object in the value table is the
* loaded structure and the second - the structure already converted into
* proper data.
*/
private Map<String, Object[]> loadedFeaturesByName = new HashMap<String, Object[]>();
/** A stack that hold the parent structure of currently loaded feature. */
private Stack<Structure> parentStack = new Stack<Structure>();
/** A list of constraints for the specified object. */
protected Map<Long, List<Constraint>> constraints = new HashMap<Long, List<Constraint>>();
/** Anim data loaded for features. */
private Map<Long, AnimationData> animData = new HashMap<Long, AnimationData>();
/** Loaded skeletons. */
private Map<Long, Skeleton> skeletons = new HashMap<Long, Skeleton>();
/** A map between skeleton and node it modifies. */
private Map<Skeleton, Node> nodesWithSkeletons = new HashMap<Skeleton, Node>();
/** A map of mesh contexts. */
protected Map<Long, MeshContext> meshContexts = new HashMap<Long, MeshContext>();
/** A map of bone contexts. */
protected Map<Long, BoneContext> boneContexts = new HashMap<Long, BoneContext>();
/** A map og helpers that perform loading. */
private Map<String, AbstractBlenderHelper> helpers = new HashMap<String, AbstractBlenderHelper>();
/** Markers used by loading classes to store some custom data. This is made to avoid putting this data into user properties. */
private Map<String, Map<Object, Object>> markers = new HashMap<String, Map<Object, Object>>();
/**
* This method sets the blender file version.
*
* @param blenderVersion
* the blender file version
*/
public void setBlenderVersion(String blenderVersion) {
this.blenderVersion = Integer.parseInt(blenderVersion);
}
/**
* @return the blender file version
*/
public int getBlenderVersion() {
return blenderVersion;
}
/**
* This method sets the blender key.
*
* @param blenderKey
* the blender key
*/
public void setBlenderKey(BlenderKey blenderKey) {
this.blenderKey = blenderKey;
}
/**
* This method returns the blender key.
*
* @return the blender key
*/
public BlenderKey getBlenderKey() {
return blenderKey;
}
/**
* This method sets the dna block data.
*
* @param dnaBlockData
* the dna block data
*/
public void setBlockData(DnaBlockData dnaBlockData) {
this.dnaBlockData = dnaBlockData;
}
/**
* This method returns the dna block data.
*
* @return the dna block data
*/
public DnaBlockData getDnaBlockData() {
return dnaBlockData;
}
/**
* This method sets the scene structure data.
*
* @param sceneStructure
* the scene structure data
*/
public void setSceneStructure(Structure sceneStructure) {
this.sceneStructure = sceneStructure;
}
/**
* This method returns the scene structure data.
*
* @return the scene structure data
*/
public Structure getSceneStructure() {
return sceneStructure;
}
/**
* This method returns the asset manager.
*
* @return the asset manager
*/
public AssetManager getAssetManager() {
return assetManager;
}
/**
* This method sets the asset manager.
*
* @param assetManager
* the asset manager
*/
public void setAssetManager(AssetManager assetManager) {
this.assetManager = assetManager;
}
/**
* This method returns the input stream of the blend file.
*
* @return the input stream of the blend file
*/
public BlenderInputStream getInputStream() {
return inputStream;
}
/**
* This method sets the input stream of the blend file.
*
* @param inputStream
* the input stream of the blend file
*/
public void setInputStream(BlenderInputStream inputStream) {
this.inputStream = inputStream;
}
/**
* This method adds a file block header to the map. Its old memory address
* is the key.
*
* @param oldMemoryAddress
* the address of the block header
* @param fileBlockHeader
* the block header to store
*/
public void addFileBlockHeader(Long oldMemoryAddress, FileBlockHeader fileBlockHeader) {
fileBlockHeadersByOma.put(oldMemoryAddress, fileBlockHeader);
List<FileBlockHeader> headers = fileBlockHeadersByCode.get(Integer.valueOf(fileBlockHeader.getCode()));
if (headers == null) {
headers = new ArrayList<FileBlockHeader>();
fileBlockHeadersByCode.put(Integer.valueOf(fileBlockHeader.getCode()), headers);
}
headers.add(fileBlockHeader);
}
/**
* This method returns the block header of a given memory address. If the
* header is not present then null is returned.
*
* @param oldMemoryAddress
* the address of the block header
* @return loaded header or null if it was not yet loaded
*/
public FileBlockHeader getFileBlock(Long oldMemoryAddress) {
return fileBlockHeadersByOma.get(oldMemoryAddress);
}
/**
* This method returns a list of file blocks' headers of a specified code.
*
* @param code
* the code of file blocks
* @return a list of file blocks' headers of a specified code
*/
public List<FileBlockHeader> getFileBlocks(Integer code) {
return fileBlockHeadersByCode.get(code);
}
/**
* This method adds a helper instance to the helpers' map.
*
* @param <T>
* the type of the helper
* @param clazz
* helper's class definition
* @param helper
* the helper instance
*/
public <T> void putHelper(Class<T> clazz, AbstractBlenderHelper helper) {
helpers.put(clazz.getSimpleName(), helper);
}
@SuppressWarnings("unchecked")
public <T> T getHelper(Class<?> clazz) {
return (T) helpers.get(clazz.getSimpleName());
}
/**
* This method adds a loaded feature to the map. The key is its unique old
* memory address.
*
* @param oldMemoryAddress
* the address of the feature
* @param featureName
* the name of the feature
* @param structure
* the filled structure of the feature
* @param feature
* the feature we want to store
*/
public void addLoadedFeatures(Long oldMemoryAddress, String featureName, Structure structure, Object feature) {
if (oldMemoryAddress == null || structure == null || feature == null) {
throw new IllegalArgumentException("One of the given arguments is null!");
}
Object[] storedData = new Object[] { structure, feature };
loadedFeatures.put(oldMemoryAddress, storedData);
if (featureName != null) {
loadedFeaturesByName.put(featureName, storedData);
}
}
/**
* This method returns the feature of a given memory address. If the feature
* is not yet loaded then null is returned.
*
* @param oldMemoryAddress
* the address of the feature
* @param loadedFeatureDataType
* the type of data we want to retreive it can be either filled
* structure or already converted feature
* @return loaded feature or null if it was not yet loaded
*/
public Object getLoadedFeature(Long oldMemoryAddress, LoadedFeatureDataType loadedFeatureDataType) {
Object[] result = loadedFeatures.get(oldMemoryAddress);
if (result != null) {
return result[loadedFeatureDataType.getIndex()];
}
return null;
}
/**
* This method adds the structure to the parent stack.
*
* @param parent
* the structure to be added to the stack
*/
public void pushParent(Structure parent) {
parentStack.push(parent);
}
/**
* This method removes the structure from the top of the parent's stack.
*
* @return the structure that was removed from the stack
*/
public Structure popParent() {
try {
return parentStack.pop();
} catch (EmptyStackException e) {
return null;
}
}
/**
* This method retreives the structure at the top of the parent's stack but
* does not remove it.
*
* @return the structure from the top of the stack
*/
public Structure peekParent() {
try {
return parentStack.peek();
} catch (EmptyStackException e) {
return null;
}
}
/**
* This method adds a new modifier to the list.
*
* @param ownerOMA
* the owner's old memory address
* @param constraints
* the object's constraints
*/
public void addConstraints(Long ownerOMA, List<Constraint> constraints) {
List<Constraint> objectConstraints = this.constraints.get(ownerOMA);
if (objectConstraints == null) {
objectConstraints = new ArrayList<Constraint>();
this.constraints.put(ownerOMA, objectConstraints);
}
objectConstraints.addAll(constraints);
}
/**
* Returns constraints applied to the feature of the given OMA.
* @param ownerOMA
* the constraints' owner OMA
* @return a list of constraints or <b>null</b> if no constraints are applied to the feature
*/
public List<Constraint> getConstraints(Long ownerOMA) {
return constraints.get(ownerOMA);
}
/**
* @return all available constraints
*/
public List<Constraint> getAllConstraints() {
List<Constraint> result = new ArrayList<Constraint>();
for (Entry<Long, List<Constraint>> entry : constraints.entrySet()) {
result.addAll(entry.getValue());
}
return result;
}
/**
* This method sets the anim data for the specified OMA of its owner.
*
* @param ownerOMA
* the owner's old memory address
* @param animData
* the animation data for the feature specified by ownerOMA
*/
public void setAnimData(Long ownerOMA, AnimationData animData) {
this.animData.put(ownerOMA, animData);
}
/**
* This method returns the animation data for the specified owner.
*
* @param ownerOMA
* the old memory address of the animation data owner
* @return the animation data or null if none exists
*/
public AnimationData getAnimData(Long ownerOMA) {
return animData.get(ownerOMA);
}
/**
* This method sets the skeleton for the specified OMA of its owner.
*
* @param skeletonOMA
* the skeleton's old memory address
* @param skeleton
* the skeleton specified by the given OMA
*/
public void setSkeleton(Long skeletonOMA, Skeleton skeleton) {
skeletons.put(skeletonOMA, skeleton);
}
/**
* The method stores a binding between the skeleton and the proper armature
* node.
*
* @param skeleton
* the skeleton
* @param node
* the armature node
*/
public void setNodeForSkeleton(Skeleton skeleton, Node node) {
nodesWithSkeletons.put(skeleton, node);
}
/**
* This method returns the armature node that is defined for the skeleton.
*
* @param skeleton
* the skeleton
* @return the armature node that defines the skeleton in blender
*/
public Node getControlledNode(Skeleton skeleton) {
return nodesWithSkeletons.get(skeleton);
}
/**
* This method returns the skeleton for the specified OMA of its owner.
*
* @param skeletonOMA
* the skeleton's old memory address
* @return the skeleton specified by the given OMA
*/
public Skeleton getSkeleton(Long skeletonOMA) {
return skeletons.get(skeletonOMA);
}
/**
* This method sets the mesh context for the given mesh old memory address.
* If the context is already set it will be replaced.
*
* @param meshOMA
* the mesh's old memory address
* @param meshContext
* the mesh's context
*/
public void setMeshContext(Long meshOMA, MeshContext meshContext) {
meshContexts.put(meshOMA, meshContext);
}
/**
* This method returns the mesh context for the given mesh old memory
* address. If no context exists then <b>null</b> is returned.
*
* @param meshOMA
* the mesh's old memory address
* @return mesh's context
*/
public MeshContext getMeshContext(Long meshOMA) {
return meshContexts.get(meshOMA);
}
/**
* This method sets the bone context for the given bone old memory address.
* If the context is already set it will be replaced.
*
* @param boneOMA
* the bone's old memory address
* @param boneContext
* the bones's context
*/
public void setBoneContext(Long boneOMA, BoneContext boneContext) {
boneContexts.put(boneOMA, boneContext);
}
/**
* This method returns the bone context for the given bone old memory
* address. If no context exists then <b>null</b> is returned.
*
* @param boneOMA
* the bone's old memory address
* @return bone's context
*/
public BoneContext getBoneContext(Long boneOMA) {
return boneContexts.get(boneOMA);
}
/**
* Returns bone by given name.
*
* @param skeletonOMA the OMA of the skeleton where the bone will be searched
* @param name
* the name of the bone
* @return found bone or null if none bone of a given name exists
*/
public BoneContext getBoneByName(Long skeletonOMA, String name) {
for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) {
if(entry.getValue().getArmatureObjectOMA().equals(skeletonOMA)) {
Bone bone = entry.getValue().getBone();
if (bone != null && name.equals(bone.getName())) {
return entry.getValue();
}
}
}
return null;
}
/**
* Returns bone context for the given bone.
*
* @param bone
* the bone
* @return the bone's bone context
*/
public BoneContext getBoneContext(Bone bone) {
for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) {
if (entry.getValue().getBone().equals(bone)) {
return entry.getValue();
}
}
throw new IllegalStateException("Cannot find context for bone: " + bone);
}
/**
* This metod returns the default material.
*
* @return the default material
*/
public synchronized Material getDefaultMaterial() {
if (blenderKey.getDefaultMaterial() == null) {
Material defaultMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
defaultMaterial.setColor("Color", ColorRGBA.DarkGray);
blenderKey.setDefaultMaterial(defaultMaterial);
}
return blenderKey.getDefaultMaterial();
}
/**
* Adds a custom marker for scene's feature.
*
* @param marker
* the marker name
* @param feature
* te scene's feature (can be node, material or texture or
* anything else)
* @param markerValue
* the marker value
*/
public void addMarker(String marker, Object feature, Object markerValue) {
if (markerValue == null) {
throw new IllegalArgumentException("The marker's value cannot be null.");
}
Map<Object, Object> markersMap = markers.get(marker);
if (markersMap == null) {
markersMap = new HashMap<Object, Object>();
markers.put(marker, markersMap);
}
markersMap.put(feature, markerValue);
}
/**
* Returns the marker value. The returned value is null if no marker was
* defined for the given feature.
*
* @param marker
* the marker name
* @param feature
* the scene's feature
* @return marker value or null if it was not defined
*/
public Object getMarkerValue(String marker, Object feature) {
Map<Object, Object> markersMap = markers.get(marker);
return markersMap == null ? null : markersMap.get(feature);
}
/**
* This enum defines what loaded data type user wants to retreive. It can be
* either filled structure or already converted data.
*
* @author Marcin Roguski (Kaelthas)
*/
public static enum LoadedFeatureDataType {
LOADED_STRUCTURE(0), LOADED_FEATURE(1);
private int index;
private LoadedFeatureDataType(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
}
}

@ -0,0 +1,282 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
import com.jme3.asset.BlenderKey;
import com.jme3.asset.BlenderKey.FeaturesToLoad;
import com.jme3.asset.BlenderKey.LoadingResults;
import com.jme3.asset.ModelKey;
import com.jme3.scene.CameraNode;
import com.jme3.scene.LightNode;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
import com.jme3.scene.plugins.blender.animations.IpoHelper;
import com.jme3.scene.plugins.blender.cameras.CameraHelper;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.curves.CurvesHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.landscape.LandscapeHelper;
import com.jme3.scene.plugins.blender.lights.LightHelper;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.meshes.MeshHelper;
import com.jme3.scene.plugins.blender.modifiers.ModifierHelper;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.scene.plugins.blender.particles.ParticlesHelper;
import com.jme3.scene.plugins.blender.textures.TextureHelper;
/**
* This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures.
* @author Marcin Roguski (Kaelthas)
*/
public class BlenderLoader implements AssetLoader {
private static final Logger LOGGER = Logger.getLogger(BlenderLoader.class.getName());
/** The blocks read from the file. */
protected List<FileBlockHeader> blocks;
/** The blender context. */
protected BlenderContext blenderContext;
public Spatial load(AssetInfo assetInfo) throws IOException {
try {
this.setup(assetInfo);
List<FileBlockHeader> sceneBlocks = new ArrayList<FileBlockHeader>();
BlenderKey blenderKey = blenderContext.getBlenderKey();
LoadingResults loadingResults = blenderKey.prepareLoadingResults();
for (FileBlockHeader block : blocks) {
switch (block.getCode()) {
case FileBlockHeader.BLOCK_OB00:// Object
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Object object = objectHelper.toObject(block.getStructure(blenderContext), blenderContext);
if (object instanceof LightNode) {
loadingResults.addLight((LightNode) object);
} else if (object instanceof CameraNode) {
loadingResults.addCamera((CameraNode) object);
} else if (object instanceof Node) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() });
}
if (this.isRootObject(loadingResults, (Node) object)) {
loadingResults.addObject((Node) object);
}
}
break;
// case FileBlockHeader.BLOCK_MA00:// Material
// MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
// MaterialContext materialContext = materialHelper.toMaterialContext(block.getStructure(blenderContext), blenderContext);
// if (blenderKey.isLoadUnlinkedAssets() && blenderKey.shouldLoad(FeaturesToLoad.MATERIALS)) {
// loadingResults.addMaterial(this.toMaterial(block.getStructure(blenderContext)));
// }
// break;
case FileBlockHeader.BLOCK_SC00:// Scene
if (blenderKey.shouldLoad(FeaturesToLoad.SCENES)) {
sceneBlocks.add(block);
}
break;
case FileBlockHeader.BLOCK_WO00:// World
if (blenderKey.shouldLoad(FeaturesToLoad.WORLD)) {
Structure worldStructure = block.getStructure(blenderContext);
String worldName = worldStructure.getName();
if (blenderKey.getUsedWorld() == null || blenderKey.getUsedWorld().equals(worldName)) {
LandscapeHelper landscapeHelper = blenderContext.getHelper(LandscapeHelper.class);
loadingResults.addLight(landscapeHelper.toAmbientLight(worldStructure));
loadingResults.setSky(landscapeHelper.toSky(worldStructure));
loadingResults.setBackgroundColor(landscapeHelper.toBackgroundColor(worldStructure));
}
}
break;
}
}
// bake constraints after everything is loaded
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
constraintHelper.bakeConstraints(blenderContext);
// load the scene at the very end so that the root nodes have no parent during loading or constraints applying
for (FileBlockHeader sceneBlock : sceneBlocks) {
loadingResults.addScene(this.toScene(sceneBlock.getStructure(blenderContext)));
}
return loadingResults;
} catch (BlenderFileException e) {
throw new IOException(e.getLocalizedMessage(), e);
} catch (Exception e) {
throw new IOException("Unexpected importer exception occured: " + e.getLocalizedMessage(), e);
} finally {
this.clear();
}
}
/**
* This method indicates if the given spatial is a root object. It means it
* has no parent or is directly attached to one of the already loaded scene
* nodes.
*
* @param loadingResults
* loading results containing the scene nodes
* @param spatial
* spatial object
* @return <b>true</b> if the given spatial is a root object and
* <b>false</b> otherwise
*/
protected boolean isRootObject(LoadingResults loadingResults, Spatial spatial) {
if (spatial.getParent() == null) {
return true;
}
for (Node scene : loadingResults.getScenes()) {
if (spatial.getParent().equals(scene)) {
return true;
}
}
return false;
}
/**
* This method converts the given structure to a scene node.
* @param structure
* structure of a scene
* @return scene's node
*/
private Node toScene(Structure structure) {
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Node result = new Node(structure.getName());
try {
List<Structure> base = ((Structure) structure.getFieldValue("base")).evaluateListBase();
for (Structure b : base) {
Pointer pObject = (Pointer) b.getFieldValue("object");
if (pObject.isNotNull()) {
Structure objectStructure = pObject.fetchData().get(0);
Object object = objectHelper.toObject(objectStructure, blenderContext);
if (object instanceof LightNode) {
result.addLight(((LightNode) object).getLight());
result.attachChild((LightNode) object);
} else if (object instanceof Node) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() });
}
if (((Node) object).getParent() == null) {
result.attachChild((Spatial) object);
}
}
}
}
} catch (BlenderFileException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
}
return result;
}
/**
* This method sets up the loader.
* @param assetInfo
* the asset info
* @throws BlenderFileException
* an exception is throw when something wrong happens with blender file
*/
protected void setup(AssetInfo assetInfo) throws BlenderFileException {
// registering loaders
ModelKey modelKey = (ModelKey) assetInfo.getKey();
BlenderKey blenderKey;
if (modelKey instanceof BlenderKey) {
blenderKey = (BlenderKey) modelKey;
} else {
blenderKey = new BlenderKey(modelKey.getName());
blenderKey.setAssetRootPath(modelKey.getFolder());
}
// opening stream
BlenderInputStream inputStream = new BlenderInputStream(assetInfo.openStream());
// reading blocks
blocks = new ArrayList<FileBlockHeader>();
FileBlockHeader fileBlock;
blenderContext = new BlenderContext();
blenderContext.setBlenderVersion(inputStream.getVersionNumber());
blenderContext.setAssetManager(assetInfo.getManager());
blenderContext.setInputStream(inputStream);
blenderContext.setBlenderKey(blenderKey);
// creating helpers
blenderContext.putHelper(ArmatureHelper.class, new ArmatureHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(TextureHelper.class, new TextureHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(MeshHelper.class, new MeshHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(ObjectHelper.class, new ObjectHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(CurvesHelper.class, new CurvesHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(LightHelper.class, new LightHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(CameraHelper.class, new CameraHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(ModifierHelper.class, new ModifierHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(MaterialHelper.class, new MaterialHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(ConstraintHelper.class, new ConstraintHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(IpoHelper.class, new IpoHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(ParticlesHelper.class, new ParticlesHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(LandscapeHelper.class, new LandscapeHelper(inputStream.getVersionNumber(), blenderContext));
// reading the blocks (dna block is automatically saved in the blender context when found)
FileBlockHeader sceneFileBlock = null;
do {
fileBlock = new FileBlockHeader(inputStream, blenderContext);
if (!fileBlock.isDnaBlock()) {
blocks.add(fileBlock);
// save the scene's file block
if (fileBlock.getCode() == FileBlockHeader.BLOCK_SC00) {
sceneFileBlock = fileBlock;
}
}
} while (!fileBlock.isLastBlock());
if (sceneFileBlock != null) {
blenderContext.setSceneStructure(sceneFileBlock.getStructure(blenderContext));
}
}
/**
* The internal data is only needed during loading so make it unreachable so that the GC can release
* that memory (which can be quite large amount).
*/
protected void clear() {
blenderContext = null;
blocks = null;
}
}

@ -0,0 +1,105 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.BlenderKey;
import com.jme3.asset.BlenderKey.FeaturesToLoad;
import com.jme3.scene.LightNode;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
/**
* This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures.
*
* @author Marcin Roguski (Kaelthas)
*/
public class BlenderModelLoader extends BlenderLoader {
private static final Logger LOGGER = Logger.getLogger(BlenderModelLoader.class.getName());
@Override
public Spatial load(AssetInfo assetInfo) throws IOException {
try {
this.setup(assetInfo);
BlenderKey blenderKey = blenderContext.getBlenderKey();
List<Node> rootObjects = new ArrayList<Node>();
for (FileBlockHeader block : blocks) {
if (block.getCode() == FileBlockHeader.BLOCK_OB00) {
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Object object = objectHelper.toObject(block.getStructure(blenderContext), blenderContext);
if (object instanceof LightNode && (blenderKey.getFeaturesToLoad() & FeaturesToLoad.LIGHTS) != 0) {
rootObjects.add((LightNode) object);
} else if (object instanceof Node && (blenderKey.getFeaturesToLoad() & FeaturesToLoad.OBJECTS) != 0) {
LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() });
if (((Node) object).getParent() == null) {
rootObjects.add((Node) object);
}
}
}
}
// bake constraints after everything is loaded
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
constraintHelper.bakeConstraints(blenderContext);
// attach the nodes to the root node at the very end so that the root objects have no parents during constraint applying
LOGGER.fine("Creating the root node of the model and applying loaded nodes of the scene to it.");
Node modelRoot = new Node(blenderKey.getName());
for (Node node : rootObjects) {
if (node instanceof LightNode) {
modelRoot.addLight(((LightNode) node).getLight());
}
modelRoot.attachChild(node);
}
return modelRoot;
} catch (BlenderFileException e) {
throw new IOException(e.getLocalizedMessage(), e);
} catch (Exception e) {
throw new IOException("Unexpected importer exception occured: " + e.getLocalizedMessage(), e);
} finally {
this.clear();
}
}
}

@ -0,0 +1,29 @@
package com.jme3.scene.plugins.blender.animations;
import java.util.List;
import com.jme3.animation.Animation;
import com.jme3.animation.Skeleton;
/**
* A simple class that sotres animation data.
* If skeleton is null then we deal with object animation.
*
* @author Marcin Roguski (Kaelthas)
*/
public class AnimationData {
/** The skeleton. */
public final Skeleton skeleton;
/** The animations list. */
public final List<Animation> anims;
public AnimationData(List<Animation> anims) {
this.anims = anims;
skeleton = null;
}
public AnimationData(Skeleton skeleton, List<Animation> anims) {
this.skeleton = skeleton;
this.anims = anims;
}
}

@ -0,0 +1,276 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.animations;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.animation.Bone;
import com.jme3.animation.BoneTrack;
import com.jme3.animation.Skeleton;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.curves.BezierCurve;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class defines the methods to calculate certain aspects of animation and
* armature functionalities.
*
* @author Marcin Roguski (Kaelthas)
*/
public class ArmatureHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(ArmatureHelper.class.getName());
public static final String ARMATURE_NODE_MARKER = "armature-node";
/**
* This constructor parses the given blender version and stores the result.
* Some functionalities may differ in different blender versions.
*
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public ArmatureHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* This method builds the object's bones structure.
*
* @param armatureObjectOMA
* the OMa of the armature node
* @param boneStructure
* the structure containing the bones' data
* @param parent
* the parent bone
* @param result
* the list where the newly created bone will be added
* @param spatialOMA
* the OMA of the spatial that will own the skeleton
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is thrown when there is problem with the blender
* file
*/
public void buildBones(Long armatureObjectOMA, Structure boneStructure, Bone parent, List<Bone> result, Long spatialOMA, BlenderContext blenderContext) throws BlenderFileException {
BoneContext bc = new BoneContext(armatureObjectOMA, boneStructure, blenderContext);
bc.buildBone(result, spatialOMA, blenderContext);
}
/**
* This method returns a map where the key is the object's group index that
* is used by a bone and the key is the bone index in the armature.
*
* @param defBaseStructure
* a bPose structure of the object
* @return bone group-to-index map
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public Map<Integer, Integer> getGroupToBoneIndexMap(Structure defBaseStructure, Skeleton skeleton) throws BlenderFileException {
Map<Integer, Integer> result = null;
if (skeleton.getBoneCount() != 0) {
result = new HashMap<Integer, Integer>();
List<Structure> deformGroups = defBaseStructure.evaluateListBase();// bDeformGroup
int groupIndex = 0;
for (Structure deformGroup : deformGroups) {
String deformGroupName = deformGroup.getFieldValue("name").toString();
int boneIndex = skeleton.getBoneIndex(deformGroupName);
if (boneIndex >= 0) {
result.put(groupIndex, boneIndex);
}
++groupIndex;
}
}
return result;
}
/**
* This method retuns the bone tracks for animation.
*
* @param actionStructure
* the structure containing the tracks
* @param blenderContext
* the blender context
* @return a list of tracks for the specified animation
* @throws BlenderFileException
* an exception is thrown when there are problems with the blend
* file
*/
public BoneTrack[] getTracks(Structure actionStructure, Skeleton skeleton, BlenderContext blenderContext) throws BlenderFileException {
if (blenderVersion < 250) {
return this.getTracks249(actionStructure, skeleton, blenderContext);
} else {
return this.getTracks250(actionStructure, skeleton, blenderContext);
}
}
/**
* This method retuns the bone tracks for animation for blender version 2.50
* and higher.
*
* @param actionStructure
* the structure containing the tracks
* @param blenderContext
* the blender context
* @return a list of tracks for the specified animation
* @throws BlenderFileException
* an exception is thrown when there are problems with the blend
* file
*/
private BoneTrack[] getTracks250(Structure actionStructure, Skeleton skeleton, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Getting tracks!");
IpoHelper ipoHelper = blenderContext.getHelper(IpoHelper.class);
int fps = blenderContext.getBlenderKey().getFps();
Structure groups = (Structure) actionStructure.getFieldValue("groups");
List<Structure> actionGroups = groups.evaluateListBase();// bActionGroup
List<BoneTrack> tracks = new ArrayList<BoneTrack>();
for (Structure actionGroup : actionGroups) {
String name = actionGroup.getFieldValue("name").toString();
int boneIndex = skeleton.getBoneIndex(name);
if (boneIndex >= 0) {
List<Structure> channels = ((Structure) actionGroup.getFieldValue("channels")).evaluateListBase();
BezierCurve[] bezierCurves = new BezierCurve[channels.size()];
int channelCounter = 0;
for (Structure c : channels) {
int type = ipoHelper.getCurveType(c, blenderContext);
Pointer pBezTriple = (Pointer) c.getFieldValue("bezt");
List<Structure> bezTriples = pBezTriple.fetchData();
bezierCurves[channelCounter++] = new BezierCurve(type, bezTriples, 2);
}
Bone bone = skeleton.getBone(boneIndex);
Ipo ipo = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion());
tracks.add((BoneTrack) ipo.calculateTrack(boneIndex, bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale(), 0, ipo.getLastFrame(), fps, false));
}
}
this.equaliseBoneTracks(tracks);
return tracks.toArray(new BoneTrack[tracks.size()]);
}
/**
* This method retuns the bone tracks for animation for blender version 2.49
* (and probably several lower versions too).
*
* @param actionStructure
* the structure containing the tracks
* @param blenderContext
* the blender context
* @return a list of tracks for the specified animation
* @throws BlenderFileException
* an exception is thrown when there are problems with the blend
* file
*/
private BoneTrack[] getTracks249(Structure actionStructure, Skeleton skeleton, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Getting tracks!");
IpoHelper ipoHelper = blenderContext.getHelper(IpoHelper.class);
int fps = blenderContext.getBlenderKey().getFps();
Structure chanbase = (Structure) actionStructure.getFieldValue("chanbase");
List<Structure> actionChannels = chanbase.evaluateListBase();// bActionChannel
List<BoneTrack> tracks = new ArrayList<BoneTrack>();
for (Structure bActionChannel : actionChannels) {
String name = bActionChannel.getFieldValue("name").toString();
int boneIndex = skeleton.getBoneIndex(name);
if (boneIndex >= 0) {
Pointer p = (Pointer) bActionChannel.getFieldValue("ipo");
if (!p.isNull()) {
Structure ipoStructure = p.fetchData().get(0);
Bone bone = skeleton.getBone(boneIndex);
Ipo ipo = ipoHelper.fromIpoStructure(ipoStructure, blenderContext);
if (ipo != null) {
tracks.add((BoneTrack) ipo.calculateTrack(boneIndex, bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale(), 0, ipo.getLastFrame(), fps, false));
}
}
}
}
this.equaliseBoneTracks(tracks);
return tracks.toArray(new BoneTrack[tracks.size()]);
}
/**
* The method makes all the tracks to have equal frame lengths.
* @param tracks
* the tracks to be equalized
*/
private void equaliseBoneTracks(List<BoneTrack> tracks) {
// first compute the maximum amount of frames
int maximumFrameCount = -1;
float[] maximumTrackTimes = null;
for (BoneTrack track : tracks) {
if (track.getTimes().length > maximumFrameCount) {
maximumTrackTimes = track.getTimes();
maximumFrameCount = maximumTrackTimes.length;
}
}
// now widen all the tracks that have less frames by repeating the last values in the frame
for (BoneTrack track : tracks) {
int currentTrackLength = track.getTimes().length;
if (currentTrackLength < maximumFrameCount) {
Vector3f[] translations = new Vector3f[maximumFrameCount];
Quaternion[] rotations = new Quaternion[maximumFrameCount];
Vector3f[] scales = new Vector3f[maximumFrameCount];
Vector3f[] currentTranslations = track.getTranslations();
Quaternion[] currentRotations = track.getRotations();
Vector3f[] currentScales = track.getScales();
for (int i = 0; i < currentTrackLength; ++i) {
translations[i] = currentTranslations[i];
rotations[i] = currentRotations[i];
scales[i] = currentScales[i];
}
for (int i = currentTrackLength; i < maximumFrameCount; ++i) {
translations[i] = currentTranslations[currentTranslations.length - 1];
rotations[i] = currentRotations[currentRotations.length - 1];
scales[i] = currentScales[currentScales.length - 1];
}
track.setKeyframes(maximumTrackTimes, translations, rotations, scales);
}
}
}
}

@ -0,0 +1,223 @@
package com.jme3.scene.plugins.blender.animations;
import java.util.ArrayList;
import java.util.List;
import com.jme3.animation.Bone;
import com.jme3.animation.Skeleton;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
/**
* This class holds the basic data that describes a bone.
*
* @author Marcin Roguski (Kaelthas)
*/
public class BoneContext {
// the flags of the bone
public static final int CONNECTED_TO_PARENT = 0x10;
/**
* The bones' matrices have, unlike objects', the coordinate system identical to JME's (Y axis is UP, X to the right and Z toward us).
* So in order to have them loaded properly we need to transform their armature matrix (which blender sees as rotated) to make sure we get identical results.
*/
public static final Matrix4f BONE_ARMATURE_TRANSFORMATION_MATRIX = new Matrix4f(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1);
private BlenderContext blenderContext;
/** The OMA of the bone's armature object. */
private Long armatureObjectOMA;
/** The OMA of the model that owns the bone's skeleton. */
private Long skeletonOwnerOma;
/** The structure of the bone. */
private Structure boneStructure;
/** Bone's name. */
private String boneName;
/** The bone's flag. */
private int flag;
/** The bone's matrix in world space. */
private Matrix4f globalBoneMatrix;
/** The bone's matrix in the model space. */
private Matrix4f boneMatrixInModelSpace;
/** The parent context. */
private BoneContext parent;
/** The children of this context. */
private List<BoneContext> children = new ArrayList<BoneContext>();
/** Created bone (available after calling 'buildBone' method). */
private Bone bone;
/** The length of the bone. */
private float length;
/**
* Constructor. Creates the basic set of bone's data.
*
* @param armatureObjectOMA
* the OMA of the bone's armature object
* @param boneStructure
* the bone's structure
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is thrown when problem with blender data reading
* occurs
*/
public BoneContext(Long armatureObjectOMA, Structure boneStructure, BlenderContext blenderContext) throws BlenderFileException {
this(boneStructure, armatureObjectOMA, null, blenderContext);
}
/**
* Constructor. Creates the basic set of bone's data.
*
* @param boneStructure
* the bone's structure
* @param armatureObjectOMA
* the OMA of the bone's armature object
* @param parent
* bone's parent (null if the bone is the root bone)
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is thrown when problem with blender data reading
* occurs
*/
private BoneContext(Structure boneStructure, Long armatureObjectOMA, BoneContext parent, BlenderContext blenderContext) throws BlenderFileException {
this.parent = parent;
this.blenderContext = blenderContext;
this.boneStructure = boneStructure;
this.armatureObjectOMA = armatureObjectOMA;
boneName = boneStructure.getFieldValue("name").toString();
flag = ((Number) boneStructure.getFieldValue("flag")).intValue();
length = ((Number) boneStructure.getFieldValue("length")).floatValue();
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
// first get the bone matrix in its armature space
globalBoneMatrix = objectHelper.getMatrix(boneStructure, "arm_mat", blenderContext.getBlenderKey().isFixUpAxis());
// then make sure it is rotated in a proper way to fit the jme bone transformation conventions
globalBoneMatrix.multLocal(BONE_ARMATURE_TRANSFORMATION_MATRIX);
Spatial armature = (Spatial) objectHelper.toObject(blenderContext.getFileBlock(armatureObjectOMA).getStructure(blenderContext), blenderContext);
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
Matrix4f armatureWorldMatrix = constraintHelper.toMatrix(armature.getWorldTransform(), new Matrix4f());
// and now compute the final bone matrix in world space
globalBoneMatrix = armatureWorldMatrix.mult(globalBoneMatrix);
// create the children
List<Structure> childbase = ((Structure) boneStructure.getFieldValue("childbase")).evaluateListBase();
for (Structure child : childbase) {
children.add(new BoneContext(child, armatureObjectOMA, this, blenderContext));
}
blenderContext.setBoneContext(boneStructure.getOldMemoryAddress(), this);
}
/**
* This method builds the bone. It recursively builds the bone's children.
*
* @param bones
* a list of bones where the newly created bone will be added
* @param skeletonOwnerOma
* the spatial of the object that will own the skeleton
* @param blenderContext
* the blender context
* @return newly created bone
*/
public Bone buildBone(List<Bone> bones, Long skeletonOwnerOma, BlenderContext blenderContext) {
this.skeletonOwnerOma = skeletonOwnerOma;
Long boneOMA = boneStructure.getOldMemoryAddress();
bone = new Bone(boneName);
bones.add(bone);
blenderContext.addLoadedFeatures(boneOMA, boneName, boneStructure, bone);
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Structure skeletonOwnerObjectStructure = (Structure) blenderContext.getLoadedFeature(skeletonOwnerOma, LoadedFeatureDataType.LOADED_STRUCTURE);
Matrix4f invertedObjectOwnerGlobalMatrix = objectHelper.getMatrix(skeletonOwnerObjectStructure, "imat", blenderContext.getBlenderKey().isFixUpAxis());
if (objectHelper.isParent(skeletonOwnerOma, armatureObjectOMA)) {
boneMatrixInModelSpace = globalBoneMatrix.mult(invertedObjectOwnerGlobalMatrix);
} else {
boneMatrixInModelSpace = invertedObjectOwnerGlobalMatrix.mult(globalBoneMatrix);
}
Matrix4f boneLocalMatrix = parent == null ? boneMatrixInModelSpace : parent.boneMatrixInModelSpace.invert().multLocal(boneMatrixInModelSpace);
Vector3f poseLocation = parent == null || !this.is(CONNECTED_TO_PARENT) ? boneLocalMatrix.toTranslationVector() : new Vector3f(0, parent.length, 0);
Quaternion rotation = boneLocalMatrix.toRotationQuat().normalizeLocal();
Vector3f scale = boneLocalMatrix.toScaleVector();
bone.setBindTransforms(poseLocation, rotation, scale);
for (BoneContext child : children) {
bone.addChild(child.buildBone(bones, skeletonOwnerOma, blenderContext));
}
return bone;
}
/**
* @return built bone (available after calling 'buildBone' method)
*/
public Bone getBone() {
return bone;
}
/**
* @return the old memory address of the bone
*/
public Long getBoneOma() {
return boneStructure.getOldMemoryAddress();
}
/**
* The method returns the length of the bone.
* If you want to use it for bone debugger take model space scale into account and do
* something like this:
* <b>boneContext.getLength() * boneContext.getBone().getModelSpaceScale().y</b>.
* Otherwise the bones might not look as they should in the bone debugger.
* @return the length of the bone
*/
public float getLength() {
return length;
}
/**
* @return OMA of the bone's armature object
*/
public Long getArmatureObjectOMA() {
return armatureObjectOMA;
}
/**
* @return the OMA of the model that owns the bone's skeleton
*/
public Long getSkeletonOwnerOma() {
return skeletonOwnerOma;
}
/**
* @return the skeleton the bone of this context belongs to
*/
public Skeleton getSkeleton() {
return blenderContext.getSkeleton(armatureObjectOMA);
}
/**
* Tells if the bone is of specified property defined by its flag.
* @param flagMask
* the mask of the flag (constants defined in this class)
* @return <b>true</b> if the bone IS of specified proeprty and <b>false</b> otherwise
*/
public boolean is(int flagMask) {
return (flag & flagMask) != 0;
}
@Override
public String toString() {
return "BoneContext: " + boneName;
}
}

@ -0,0 +1,243 @@
package com.jme3.scene.plugins.blender.animations;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.animation.BoneTrack;
import com.jme3.animation.SpatialTrack;
import com.jme3.animation.Track;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.curves.BezierCurve;
/**
* This class is used to calculate bezier curves value for the given frames. The
* Ipo (interpolation object) consists of several b-spline curves (connected 3rd
* degree bezier curves) of a different type.
*
* @author Marcin Roguski
*/
public class Ipo {
private static final Logger LOGGER = Logger.getLogger(Ipo.class.getName());
public static final int AC_LOC_X = 1;
public static final int AC_LOC_Y = 2;
public static final int AC_LOC_Z = 3;
public static final int OB_ROT_X = 7;
public static final int OB_ROT_Y = 8;
public static final int OB_ROT_Z = 9;
public static final int AC_SIZE_X = 13;
public static final int AC_SIZE_Y = 14;
public static final int AC_SIZE_Z = 15;
public static final int AC_QUAT_W = 25;
public static final int AC_QUAT_X = 26;
public static final int AC_QUAT_Y = 27;
public static final int AC_QUAT_Z = 28;
/** A list of bezier curves for this interpolation object. */
private BezierCurve[] bezierCurves;
/** Each ipo contains one bone track. */
private Track calculatedTrack;
/** This variable indicates if the Y asxis is the UP axis or not. */
protected boolean fixUpAxis;
/**
* Depending on the blender version rotations are stored in degrees or
* radians so we need to know the version that is used.
*/
protected final int blenderVersion;
/**
* Constructor. Stores the bezier curves.
*
* @param bezierCurves
* a table of bezier curves
* @param fixUpAxis
* indicates if the Y is the up axis or not
* @param blenderVersion
* the blender version that is currently used
*/
public Ipo(BezierCurve[] bezierCurves, boolean fixUpAxis, int blenderVersion) {
this.bezierCurves = bezierCurves;
this.fixUpAxis = fixUpAxis;
this.blenderVersion = blenderVersion;
}
/**
* This method calculates the ipo value for the first curve.
*
* @param frame
* the frame for which the value is calculated
* @return calculated ipo value
*/
public float calculateValue(int frame) {
return this.calculateValue(frame, 0);
}
/**
* This method calculates the ipo value for the curve of the specified
* index. Make sure you do not exceed the curves amount. Alway chech the
* amount of curves before calling this method.
*
* @param frame
* the frame for which the value is calculated
* @param curveIndex
* the index of the curve
* @return calculated ipo value
*/
public float calculateValue(int frame, int curveIndex) {
return bezierCurves[curveIndex].evaluate(frame, BezierCurve.Y_VALUE);
}
/**
* This method returns the frame where last bezier triple center point of
* the specified bezier curve is located.
*
* @return the frame number of the last defined bezier triple point for the
* specified ipo
*/
public int getLastFrame() {
int result = 1;
for (int i = 0; i < bezierCurves.length; ++i) {
int tempResult = bezierCurves[i].getLastFrame();
if (tempResult > result) {
result = tempResult;
}
}
return result;
}
/**
* This method calculates the value of the curves as a bone track between
* the specified frames.
*
* @param targetIndex
* the index of the target for which the method calculates the
* tracks IMPORTANT! Aet to -1 (or any negative number) if you
* want to load spatial animation.
* @param localTranslation
* the local translation of the object/bone that will be animated by
* the track
* @param localRotation
* the local rotation of the object/bone that will be animated by
* the track
* @param localScale
* the local scale of the object/bone that will be animated by
* the track
* @param startFrame
* the first frame of tracks (inclusive)
* @param stopFrame
* the last frame of the tracks (inclusive)
* @param fps
* frame rate (frames per second)
* @param spatialTrack
* this flag indicates if the track belongs to a spatial or to a
* bone; the difference is important because it appears that bones
* in blender have the same type of coordinate system (Y as UP)
* as jme while other features have different one (Z is UP)
* @return bone track for the specified bone
*/
public Track calculateTrack(int targetIndex, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean spatialTrack) {
if (calculatedTrack == null) {
// preparing data for track
int framesAmount = stopFrame - startFrame;
float timeBetweenFrames = 1.0f / fps;
float[] times = new float[framesAmount + 1];
Vector3f[] translations = new Vector3f[framesAmount + 1];
float[] translation = new float[] { localTranslation.x, localTranslation.y, localTranslation.z };
Quaternion[] rotations = new Quaternion[framesAmount + 1];
float[] quaternionRotation = new float[] { localRotation.getX(), localRotation.getY(), localRotation.getZ(), localRotation.getW(), };
float[] objectRotation = localRotation.toAngles(null);
Vector3f[] scales = new Vector3f[framesAmount + 1];
float[] scale = new float[] { localScale.x, localScale.y, localScale.z };
float degreeToRadiansFactor = 1;
if (blenderVersion < 250) {// in blender earlier than 2.50 the values are stored in degrees
degreeToRadiansFactor *= FastMath.DEG_TO_RAD * 10;// the values in blender are divided by 10, so we need to mult it here
}
int yIndex = 1, zIndex = 2;
if (spatialTrack && fixUpAxis) {
yIndex = 2;
zIndex = 1;
}
// calculating track data
for (int frame = startFrame; frame <= stopFrame; ++frame) {
int index = frame - startFrame;
times[index] = index * timeBetweenFrames;// start + (frame - 1)
// * timeBetweenFrames;
for (int j = 0; j < bezierCurves.length; ++j) {
double value = bezierCurves[j].evaluate(frame, BezierCurve.Y_VALUE);
switch (bezierCurves[j].getType()) {
// LOCATION
case AC_LOC_X:
translation[0] = (float) value;
break;
case AC_LOC_Y:
if (fixUpAxis && value != 0) {
value = -value;
}
translation[yIndex] = (float) value;
break;
case AC_LOC_Z:
translation[zIndex] = (float) value;
break;
// ROTATION (used with object animation)
case OB_ROT_X:
objectRotation[0] = (float) value * degreeToRadiansFactor;
break;
case OB_ROT_Y:
if (fixUpAxis && value != 0) {
value = -value;
}
objectRotation[yIndex] = (float) value * degreeToRadiansFactor;
break;
case OB_ROT_Z:
objectRotation[zIndex] = (float) value * degreeToRadiansFactor;
break;
// SIZE
case AC_SIZE_X:
scale[0] = (float) value;
break;
case AC_SIZE_Y:
scale[fixUpAxis ? 2 : 1] = (float) value;
break;
case AC_SIZE_Z:
scale[fixUpAxis ? 1 : 2] = (float) value;
break;
// QUATERNION ROTATION (used with bone animation)
case AC_QUAT_W:
quaternionRotation[3] = (float) value;
break;
case AC_QUAT_X:
quaternionRotation[0] = (float) value;
break;
case AC_QUAT_Y:
if (fixUpAxis && value != 0) {
value = -value;
}
quaternionRotation[yIndex] = (float) value;
break;
case AC_QUAT_Z:
quaternionRotation[zIndex] = (float) value;
break;
default:
LOGGER.log(Level.WARNING, "Unknown ipo curve type: {0}.", bezierCurves[j].getType());
}
}
translations[index] = localRotation.multLocal(new Vector3f(translation[0], translation[1], translation[2]));
rotations[index] = spatialTrack ? new Quaternion().fromAngles(objectRotation) : new Quaternion(quaternionRotation[0], quaternionRotation[1], quaternionRotation[2], quaternionRotation[3]);
scales[index] = new Vector3f(scale[0], scale[1], scale[2]);
}
if (spatialTrack) {
calculatedTrack = new SpatialTrack(times, translations, rotations, scales);
} else {
calculatedTrack = new BoneTrack(targetIndex, times, translations, rotations, scales);
}
}
return calculatedTrack;
}
}

@ -0,0 +1,194 @@
package com.jme3.scene.plugins.blender.animations;
import java.util.List;
import java.util.logging.Logger;
import com.jme3.animation.BoneTrack;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.curves.BezierCurve;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class helps to compute values from interpolation curves for features
* like animation or constraint influence. The curves are 3rd degree bezier
* curves.
*
* @author Marcin Roguski
*/
public class IpoHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(IpoHelper.class.getName());
/**
* This constructor parses the given blender version and stores the result.
* Some functionalities may differ in different blender versions.
*
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public IpoHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* This method creates an ipo object used for interpolation calculations.
*
* @param ipoStructure
* the structure with ipo definition
* @param blenderContext
* the blender context
* @return the ipo object
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public Ipo fromIpoStructure(Structure ipoStructure, BlenderContext blenderContext) throws BlenderFileException {
Structure curvebase = (Structure) ipoStructure.getFieldValue("curve");
// preparing bezier curves
Ipo result = null;
List<Structure> curves = curvebase.evaluateListBase();// IpoCurve
if (curves.size() > 0) {
BezierCurve[] bezierCurves = new BezierCurve[curves.size()];
int frame = 0;
for (Structure curve : curves) {
Pointer pBezTriple = (Pointer) curve.getFieldValue("bezt");
List<Structure> bezTriples = pBezTriple.fetchData();
int type = ((Number) curve.getFieldValue("adrcode")).intValue();
bezierCurves[frame++] = new BezierCurve(type, bezTriples, 2);
}
curves.clear();
result = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion());
blenderContext.addLoadedFeatures(ipoStructure.getOldMemoryAddress(), ipoStructure.getName(), ipoStructure, result);
}
return result;
}
/**
* This method creates an ipo object used for interpolation calculations. It
* should be called for blender version 2.50 and higher.
*
* @param actionStructure
* the structure with action definition
* @param blenderContext
* the blender context
* @return the ipo object
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public Ipo fromAction(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException {
Ipo result = null;
List<Structure> curves = ((Structure) actionStructure.getFieldValue("curves")).evaluateListBase();// FCurve
if (curves.size() > 0) {
BezierCurve[] bezierCurves = new BezierCurve[curves.size()];
int frame = 0;
for (Structure curve : curves) {
Pointer pBezTriple = (Pointer) curve.getFieldValue("bezt");
List<Structure> bezTriples = pBezTriple.fetchData();
int type = this.getCurveType(curve, blenderContext);
bezierCurves[frame++] = new BezierCurve(type, bezTriples, 2);
}
curves.clear();
result = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion());
}
return result;
}
/**
* This method returns the type of the ipo curve.
*
* @param structure
* the structure must contain the 'rna_path' field and
* 'array_index' field (the type is not important here)
* @param blenderContext
* the blender context
* @return the type of the curve
*/
public int getCurveType(Structure structure, BlenderContext blenderContext) {
// reading rna path first
BlenderInputStream bis = blenderContext.getInputStream();
int currentPosition = bis.getPosition();
Pointer pRnaPath = (Pointer) structure.getFieldValue("rna_path");
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pRnaPath.getOldMemoryAddress());
bis.setPosition(dataFileBlock.getBlockPosition());
String rnaPath = bis.readString();
bis.setPosition(currentPosition);
int arrayIndex = ((Number) structure.getFieldValue("array_index")).intValue();
// determining the curve type
if (rnaPath.endsWith("location")) {
return Ipo.AC_LOC_X + arrayIndex;
}
if (rnaPath.endsWith("rotation_quaternion")) {
return Ipo.AC_QUAT_W + arrayIndex;
}
if (rnaPath.endsWith("scale")) {
return Ipo.AC_SIZE_X + arrayIndex;
}
if (rnaPath.endsWith("rotation") || rnaPath.endsWith("rotation_euler")) {
return Ipo.OB_ROT_X + arrayIndex;
}
LOGGER.warning("Unknown curve rna path: " + rnaPath);
return -1;
}
/**
* This method creates an ipo with only a single value. No track type is
* specified so do not use it for calculating tracks.
*
* @param constValue
* the value of this ipo
* @return constant ipo
*/
public Ipo fromValue(float constValue) {
return new ConstIpo(constValue);
}
/**
* Ipo constant curve. This is a curve with only one value and no specified
* type. This type of ipo cannot be used to calculate tracks. It should only
* be used to calculate single value for a given frame.
*
* @author Marcin Roguski (Kaelthas)
*/
private class ConstIpo extends Ipo {
/** The constant value of this ipo. */
private float constValue;
/**
* Constructor. Stores the constant value of this ipo.
*
* @param constValue
* the constant value of this ipo
*/
public ConstIpo(float constValue) {
super(null, false, 0);// the version is not important here
this.constValue = constValue;
}
@Override
public float calculateValue(int frame) {
return constValue;
}
@Override
public float calculateValue(int frame, int curveIndex) {
return constValue;
}
@Override
public BoneTrack calculateTrack(int boneIndex, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean boneTrack) {
throw new IllegalStateException("Constatnt ipo object cannot be used for calculating bone tracks!");
}
}
}

@ -0,0 +1,147 @@
package com.jme3.scene.plugins.blender.cameras;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.math.FastMath;
import com.jme3.renderer.Camera;
import com.jme3.scene.CameraNode;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* A class that is used to load cameras into the scene.
* @author Marcin Roguski
*/
public class CameraHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(CameraHelper.class.getName());
protected static final int DEFAULT_CAM_WIDTH = 640;
protected static final int DEFAULT_CAM_HEIGHT = 480;
/**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in
* different blender versions.
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public CameraHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* This method converts the given structure to jme camera.
*
* @param structure
* camera structure
* @return jme camera object
* @throws BlenderFileException
* an exception is thrown when there are problems with the
* blender file
*/
public CameraNode toCamera(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
if (blenderVersion >= 250) {
return this.toCamera250(structure, blenderContext.getSceneStructure());
} else {
return this.toCamera249(structure);
}
}
/**
* This method converts the given structure to jme camera. Should be used form blender 2.5+.
*
* @param structure
* camera structure
* @param sceneStructure
* scene structure
* @return jme camera object
* @throws BlenderFileException
* an exception is thrown when there are problems with the
* blender file
*/
private CameraNode toCamera250(Structure structure, Structure sceneStructure) throws BlenderFileException {
int width = DEFAULT_CAM_WIDTH;
int height = DEFAULT_CAM_HEIGHT;
if (sceneStructure != null) {
Structure renderData = (Structure) sceneStructure.getFieldValue("r");
width = ((Number) renderData.getFieldValue("xsch")).shortValue();
height = ((Number) renderData.getFieldValue("ysch")).shortValue();
}
Camera camera = new Camera(width, height);
int type = ((Number) structure.getFieldValue("type")).intValue();
if (type != 0 && type != 1) {
LOGGER.log(Level.WARNING, "Unknown camera type: {0}. Perspective camera is being used!", type);
type = 0;
}
// type==0 - perspective; type==1 - orthographic; perspective is used as default
camera.setParallelProjection(type == 1);
float aspect = width / (float) height;
float fovY; // Vertical field of view in degrees
float clipsta = ((Number) structure.getFieldValue("clipsta")).floatValue();
float clipend = ((Number) structure.getFieldValue("clipend")).floatValue();
if (type == 0) {
// Convert lens MM to vertical degrees in fovY, see Blender rna_Camera_angle_get()
// Default sensor size prior to 2.60 was 32.
float sensor = 32.0f;
boolean sensorVertical = false;
Number sensorFit = (Number) structure.getFieldValue("sensor_fit");
if (sensorFit != null) {
// If sensor_fit is vert (2), then sensor_y is used
sensorVertical = sensorFit.byteValue() == 2;
String sensorName = "sensor_x";
if (sensorVertical) {
sensorName = "sensor_y";
}
sensor = ((Number) structure.getFieldValue(sensorName)).floatValue();
}
float focalLength = ((Number) structure.getFieldValue("lens")).floatValue();
float fov = 2.0f * FastMath.atan((sensor / 2.0f) / focalLength);
if (sensorVertical) {
fovY = fov * FastMath.RAD_TO_DEG;
} else {
// Convert fov from horizontal to vertical
fovY = 2.0f * FastMath.atan(FastMath.tan(fov / 2.0f) / aspect) * FastMath.RAD_TO_DEG;
}
} else {
// This probably is not correct.
fovY = ((Number) structure.getFieldValue("ortho_scale")).floatValue();
}
camera.setFrustumPerspective(fovY, aspect, clipsta, clipend);
return new CameraNode(null, camera);
}
/**
* This method converts the given structure to jme camera. Should be used form blender 2.49.
*
* @param structure
* camera structure
* @return jme camera object
* @throws BlenderFileException
* an exception is thrown when there are problems with the
* blender file
*/
private CameraNode toCamera249(Structure structure) throws BlenderFileException {
Camera camera = new Camera(DEFAULT_CAM_WIDTH, DEFAULT_CAM_HEIGHT);
int type = ((Number) structure.getFieldValue("type")).intValue();
if (type != 0 && type != 1) {
LOGGER.log(Level.WARNING, "Unknown camera type: {0}. Perspective camera is being used!", type);
type = 0;
}
// type==0 - perspective; type==1 - orthographic; perspective is used as default
camera.setParallelProjection(type == 1);
float aspect = 0;
float clipsta = ((Number) structure.getFieldValue("clipsta")).floatValue();
float clipend = ((Number) structure.getFieldValue("clipend")).floatValue();
if (type == 0) {
aspect = ((Number) structure.getFieldValue("lens")).floatValue();
} else {
aspect = ((Number) structure.getFieldValue("ortho_scale")).floatValue();
}
camera.setFrustumPerspective(aspect, camera.getWidth() / camera.getHeight(), clipsta, clipend);
return new CameraNode(null, camera);
}
}

@ -0,0 +1,73 @@
package com.jme3.scene.plugins.blender.constraints;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.animations.Ipo;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* Constraint applied on the bone.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class BoneConstraint extends Constraint {
private static final Logger LOGGER = Logger.getLogger(BoneConstraint.class.getName());
/**
* The bone constraint constructor.
*
* @param constraintStructure
* the constraint's structure
* @param ownerOMA
* the OMA of the bone that owns the constraint
* @param influenceIpo
* the influence interpolation curve
* @param blenderContext
* the blender context
* @throws BlenderFileException
* exception thrown when problems with blender file occur
*/
public BoneConstraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
super(constraintStructure, ownerOMA, influenceIpo, blenderContext);
}
@Override
public boolean validate() {
if (targetOMA != null) {
Spatial nodeTarget = (Spatial) blenderContext.getLoadedFeature(targetOMA, LoadedFeatureDataType.LOADED_FEATURE);
if (nodeTarget == null) {
LOGGER.log(Level.WARNING, "Cannot find target for constraint: {0}.", name);
return false;
}
// the second part of the if expression verifies if the found node
// (if any) is an armature node
if (blenderContext.getMarkerValue(ArmatureHelper.ARMATURE_NODE_MARKER, nodeTarget) != null) {
if (subtargetName.trim().isEmpty()) {
LOGGER.log(Level.WARNING, "No bone target specified for constraint: {0}.", name);
return false;
}
// if the target is not an object node then it is an Armature,
// so make sure the bone is in the current skeleton
BoneContext boneContext = blenderContext.getBoneContext(ownerOMA);
if (targetOMA.longValue() != boneContext.getArmatureObjectOMA().longValue()) {
LOGGER.log(Level.WARNING, "Bone constraint {0} must target bone in the its own skeleton! Targeting bone in another skeleton is not supported!", name);
return false;
}
}
}
return true;
}
@Override
public void apply(int frame) {
super.apply(frame);
blenderContext.getBoneContext(ownerOMA).getBone().updateWorldVectors();
}
}

@ -0,0 +1,166 @@
package com.jme3.scene.plugins.blender.constraints;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.Ipo;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.constraints.definitions.ConstraintDefinition;
import com.jme3.scene.plugins.blender.constraints.definitions.ConstraintDefinitionFactory;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* The implementation of a constraint.
*
* @author Marcin Roguski (Kaelthas)
*/
public abstract class Constraint {
private static final Logger LOGGER = Logger.getLogger(Constraint.class.getName());
/** The name of this constraint. */
protected final String name;
/** Indicates if the constraint is already baked or not. */
protected boolean baked;
protected Space ownerSpace;
protected final ConstraintDefinition constraintDefinition;
protected Long ownerOMA;
protected Long targetOMA;
protected Space targetSpace;
protected String subtargetName;
/** The ipo object defining influence. */
protected final Ipo ipo;
/** The blender context. */
protected final BlenderContext blenderContext;
protected final ConstraintHelper constraintHelper;
/**
* This constructor creates the constraint instance.
*
* @param constraintStructure
* the constraint's structure (bConstraint clss in blender 2.49).
* @param ownerOMA
* the old memory address of the constraint owner
* @param ownerType
* the type of the constraint owner
* @param influenceIpo
* the ipo curve of the influence factor
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public Constraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
this.blenderContext = blenderContext;
name = constraintStructure.getFieldValue("name").toString();
Pointer pData = (Pointer) constraintStructure.getFieldValue("data");
if (pData.isNotNull()) {
Structure data = pData.fetchData().get(0);
constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(data, ownerOMA, blenderContext);
Pointer pTar = (Pointer) data.getFieldValue("tar");
if (pTar != null && pTar.isNotNull()) {
targetOMA = pTar.getOldMemoryAddress();
targetSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("tarspace")).byteValue());
Object subtargetValue = data.getFieldValue("subtarget");
if (subtargetValue != null) {// not all constraint data have the
// subtarget field
subtargetName = subtargetValue.toString();
}
}
} else {
// Null constraint has no data, so create it here
constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(null, null, blenderContext);
}
ownerSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("ownspace")).byteValue());
ipo = influenceIpo;
this.ownerOMA = ownerOMA;
constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
LOGGER.log(Level.INFO, "Created constraint: {0} with definition: {1}", new Object[] { name, constraintDefinition });
}
/**
* @return <b>true</b> if the constraint is implemented and <b>false</b>
* otherwise
*/
public boolean isImplemented() {
return constraintDefinition == null ? true : constraintDefinition.isImplemented();
}
/**
* @return the name of the constraint type, similar to the constraint name
* used in Blender
*/
public String getConstraintTypeName() {
return constraintDefinition.getConstraintTypeName();
}
/**
* @return the OMAs of the features whose transform had been altered beside the constraint owner
*/
public Set<Long> getAlteredOmas() {
return constraintDefinition.getAlteredOmas();
}
/**
* Performs validation before baking. Checks factors that can prevent
* constraint from baking that could not be checked during constraint
* loading.
*/
public abstract boolean validate();
/**
* Applies the constraint to owner (and in some cases can alter other bones of the skeleton).
* @param frame
* the frame of the animation
*/
public void apply(int frame) {
Transform targetTransform = targetOMA != null ? constraintHelper.getTransform(targetOMA, subtargetName, targetSpace) : null;
constraintDefinition.bake(ownerSpace, targetSpace, targetTransform, ipo.calculateValue(frame));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (name == null ? 0 : name.hashCode());
result = prime * result + (ownerOMA == null ? 0 : ownerOMA.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
Constraint other = (Constraint) obj;
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
if (ownerOMA == null) {
if (other.ownerOMA != null) {
return false;
}
} else if (!ownerOMA.equals(other.ownerOMA)) {
return false;
}
return true;
}
}

@ -0,0 +1,478 @@
package com.jme3.scene.plugins.blender.constraints;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import com.jme3.animation.Bone;
import com.jme3.animation.Skeleton;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.animations.Ipo;
import com.jme3.scene.plugins.blender.animations.IpoHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.util.TempVars;
/**
* This class should be used for constraint calculations.
*
* @author Marcin Roguski (Kaelthas)
*/
public class ConstraintHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(ConstraintHelper.class.getName());
private static final Quaternion POS_PARLOC_SPACE_QUATERNION = new Quaternion(new float[] { FastMath.HALF_PI, 0, 0 });
private static final Quaternion NEG_PARLOC_SPACE_QUATERNION = new Quaternion(new float[] { -FastMath.HALF_PI, 0, 0 });
/**
* Helper constructor.
*
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public ConstraintHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* This method reads constraints for for the given structure. The
* constraints are loaded only once for object/bone.
*
* @param objectStructure
* the structure we read constraint's for
* @param blenderContext
* the blender context
* @throws BlenderFileException
*/
public void loadConstraints(Structure objectStructure, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.fine("Loading constraints.");
// reading influence ipos for the constraints
IpoHelper ipoHelper = blenderContext.getHelper(IpoHelper.class);
Map<String, Map<String, Ipo>> constraintsIpos = new HashMap<String, Map<String, Ipo>>();
Pointer pActions = (Pointer) objectStructure.getFieldValue("action");
if (pActions.isNotNull()) {
List<Structure> actions = pActions.fetchData();
for (Structure action : actions) {
Structure chanbase = (Structure) action.getFieldValue("chanbase");
List<Structure> actionChannels = chanbase.evaluateListBase();
for (Structure actionChannel : actionChannels) {
Map<String, Ipo> ipos = new HashMap<String, Ipo>();
Structure constChannels = (Structure) actionChannel.getFieldValue("constraintChannels");
List<Structure> constraintChannels = constChannels.evaluateListBase();
for (Structure constraintChannel : constraintChannels) {
Pointer pIpo = (Pointer) constraintChannel.getFieldValue("ipo");
if (pIpo.isNotNull()) {
String constraintName = constraintChannel.getFieldValue("name").toString();
Ipo ipo = ipoHelper.fromIpoStructure(pIpo.fetchData().get(0), blenderContext);
ipos.put(constraintName, ipo);
}
}
String actionName = actionChannel.getFieldValue("name").toString();
constraintsIpos.put(actionName, ipos);
}
}
}
// loading constraints connected with the object's bones
Pointer pPose = (Pointer) objectStructure.getFieldValue("pose");
if (pPose.isNotNull()) {
List<Structure> poseChannels = ((Structure) pPose.fetchData().get(0).getFieldValue("chanbase")).evaluateListBase();
for (Structure poseChannel : poseChannels) {
List<Constraint> constraintsList = new ArrayList<Constraint>();
Long boneOMA = Long.valueOf(((Pointer) poseChannel.getFieldValue("bone")).getOldMemoryAddress());
// the name is read directly from structure because bone might
// not yet be loaded
String name = blenderContext.getFileBlock(boneOMA).getStructure(blenderContext).getFieldValue("name").toString();
List<Structure> constraints = ((Structure) poseChannel.getFieldValue("constraints")).evaluateListBase();
for (Structure constraint : constraints) {
String constraintName = constraint.getFieldValue("name").toString();
Map<String, Ipo> ipoMap = constraintsIpos.get(name);
Ipo ipo = ipoMap == null ? null : ipoMap.get(constraintName);
if (ipo == null) {
float enforce = ((Number) constraint.getFieldValue("enforce")).floatValue();
ipo = ipoHelper.fromValue(enforce);
}
constraintsList.add(new BoneConstraint(constraint, boneOMA, ipo, blenderContext));
}
blenderContext.addConstraints(boneOMA, constraintsList);
}
}
// loading constraints connected with the object itself
List<Structure> constraints = ((Structure) objectStructure.getFieldValue("constraints")).evaluateListBase();
if (constraints != null && constraints.size() > 0) {
Pointer pData = (Pointer) objectStructure.getFieldValue("data");
String dataType = pData.isNotNull() ? pData.fetchData().get(0).getType() : null;
List<Constraint> constraintsList = new ArrayList<Constraint>(constraints.size());
for (Structure constraint : constraints) {
String constraintName = constraint.getFieldValue("name").toString();
String objectName = objectStructure.getName();
Map<String, Ipo> objectConstraintsIpos = constraintsIpos.get(objectName);
Ipo ipo = objectConstraintsIpos != null ? objectConstraintsIpos.get(constraintName) : null;
if (ipo == null) {
float enforce = ((Number) constraint.getFieldValue("enforce")).floatValue();
ipo = ipoHelper.fromValue(enforce);
}
constraintsList.add(this.createConstraint(dataType, constraint, objectStructure.getOldMemoryAddress(), ipo, blenderContext));
}
blenderContext.addConstraints(objectStructure.getOldMemoryAddress(), constraintsList);
}
}
/**
* This method creates a proper constraint object depending on the object's
* data type. Supported data types: <li>Mesh <li>Armature <li>Camera <li>
* Lamp Bone constraints are created in a different place.
*
* @param dataType
* the type of the object's data
* @param constraintStructure
* the constraint structure
* @param ownerOMA
* the owner OMA
* @param influenceIpo
* the influence interpolation curve
* @param blenderContext
* the blender context
* @return constraint object for the required type
* @throws BlenderFileException
* thrown when problems with blender file occured
*/
private Constraint createConstraint(String dataType, Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
if (dataType == null || "Mesh".equalsIgnoreCase(dataType) || "Camera".equalsIgnoreCase(dataType) || "Lamp".equalsIgnoreCase(dataType)) {
return new SpatialConstraint(constraintStructure, ownerOMA, influenceIpo, blenderContext);
} else if ("Armature".equalsIgnoreCase(dataType)) {
return new SkeletonConstraint(constraintStructure, ownerOMA, influenceIpo, blenderContext);
} else {
throw new IllegalArgumentException("Unsupported data type for applying constraints: " + dataType);
}
}
/**
* The method bakes all available and valid constraints.
*
* @param blenderContext
* the blender context
*/
public void bakeConstraints(BlenderContext blenderContext) {
List<SimulationNode> simulationRootNodes = new ArrayList<SimulationNode>();
for (Constraint constraint : blenderContext.getAllConstraints()) {
boolean constraintUsed = false;
for (SimulationNode node : simulationRootNodes) {
if (node.contains(constraint)) {
constraintUsed = true;
break;
}
}
if (!constraintUsed) {
if (constraint instanceof BoneConstraint) {
BoneContext boneContext = blenderContext.getBoneContext(constraint.ownerOMA);
simulationRootNodes.add(new SimulationNode(boneContext.getArmatureObjectOMA(), blenderContext));
} else if (constraint instanceof SpatialConstraint) {
Spatial spatial = (Spatial) blenderContext.getLoadedFeature(constraint.ownerOMA, LoadedFeatureDataType.LOADED_FEATURE);
while (spatial.getParent() != null) {
spatial = spatial.getParent();
}
simulationRootNodes.add(new SimulationNode((Long) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, spatial), blenderContext));
} else {
throw new IllegalStateException("Unsupported constraint type: " + constraint);
}
}
}
for (SimulationNode node : simulationRootNodes) {
node.simulate();
}
}
/**
* The method retreives the transform from a feature in a given space.
*
* @param oma
* the OMA of the feature (spatial or armature node)
* @param subtargetName
* the feature's subtarget (bone in a case of armature's node)
* @param space
* the space the transform is evaluated to
* @return thensform of a feature in a given space
*/
public Transform getTransform(Long oma, String subtargetName, Space space) {
Spatial feature = (Spatial) blenderContext.getLoadedFeature(oma, LoadedFeatureDataType.LOADED_FEATURE);
boolean isArmature = blenderContext.getMarkerValue(ArmatureHelper.ARMATURE_NODE_MARKER, feature) != null;
if (isArmature) {
blenderContext.getSkeleton(oma).updateWorldVectors();
BoneContext targetBoneContext = blenderContext.getBoneByName(oma, subtargetName);
Bone bone = targetBoneContext.getBone();
if (bone.getParent() == null && (space == Space.CONSTRAINT_SPACE_LOCAL || space == Space.CONSTRAINT_SPACE_PARLOCAL)) {
space = Space.CONSTRAINT_SPACE_POSE;
}
TempVars tempVars = TempVars.get();// use readable names of the matrices so that the code is more clear
Transform result;
switch (space) {
case CONSTRAINT_SPACE_WORLD:
Spatial model = (Spatial) blenderContext.getLoadedFeature(targetBoneContext.getSkeletonOwnerOma(), LoadedFeatureDataType.LOADED_FEATURE);
Matrix4f boneModelMatrix = this.toMatrix(bone.getModelSpacePosition(), bone.getModelSpaceRotation(), bone.getModelSpaceScale(), tempVars.tempMat4);
Matrix4f modelWorldMatrix = this.toMatrix(model.getWorldTransform(), tempVars.tempMat42);
Matrix4f boneMatrixInWorldSpace = modelWorldMatrix.multLocal(boneModelMatrix);
result = new Transform(boneMatrixInWorldSpace.toTranslationVector(), boneMatrixInWorldSpace.toRotationQuat(), boneMatrixInWorldSpace.toScaleVector());
break;
case CONSTRAINT_SPACE_LOCAL:
assert bone.getParent() != null : "CONSTRAINT_SPACE_LOCAL should be evaluated as CONSTRAINT_SPACE_POSE if the bone has no parent!";
result = new Transform(bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale());
break;
case CONSTRAINT_SPACE_POSE:
Matrix4f boneWorldMatrix = this.toMatrix(this.getTransform(oma, subtargetName, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat4);
Matrix4f armatureInvertedWorldMatrix = this.toMatrix(feature.getWorldTransform(), tempVars.tempMat42).invertLocal();
Matrix4f bonePoseMatrix = armatureInvertedWorldMatrix.multLocal(boneWorldMatrix);
result = new Transform(bonePoseMatrix.toTranslationVector(), bonePoseMatrix.toRotationQuat(), bonePoseMatrix.toScaleVector());
break;
case CONSTRAINT_SPACE_PARLOCAL:
Matrix4f parentLocalMatrix = tempVars.tempMat4;
if (bone.getParent() != null) {
Bone parent = bone.getParent();
this.toMatrix(parent.getLocalPosition(), parent.getLocalRotation(), parent.getLocalScale(), parentLocalMatrix);
} else {
parentLocalMatrix.loadIdentity();
}
Matrix4f boneLocalMatrix = this.toMatrix(bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale(), tempVars.tempMat42);
Matrix4f resultMatrix = parentLocalMatrix.multLocal(boneLocalMatrix);
Vector3f loc = resultMatrix.toTranslationVector();
Quaternion rot = resultMatrix.toRotationQuat().normalizeLocal().multLocal(NEG_PARLOC_SPACE_QUATERNION);
Vector3f scl = resultMatrix.toScaleVector();
result = new Transform(loc, rot, scl);
break;
default:
throw new IllegalStateException("Unknown space type: " + space);
}
tempVars.release();
return result;
} else {
switch (space) {
case CONSTRAINT_SPACE_LOCAL:
return feature.getLocalTransform();
case CONSTRAINT_SPACE_WORLD:
return feature.getWorldTransform();
case CONSTRAINT_SPACE_PARLOCAL:
case CONSTRAINT_SPACE_POSE:
throw new IllegalStateException("Nodes can have only Local and World spaces applied!");
default:
throw new IllegalStateException("Unknown space type: " + space);
}
}
}
/**
* Applies transform to a feature (bone or spatial). Computations transform
* the given transformation from the given space to the feature's local
* space.
*
* @param oma
* the OMA of the feature we apply transformation to
* @param subtargetName
* the name of the feature's subtarget (bone in case of armature)
* @param space
* the space in which the given transform is to be applied
* @param transform
* the transform we apply
*/
public void applyTransform(Long oma, String subtargetName, Space space, Transform transform) {
Spatial feature = (Spatial) blenderContext.getLoadedFeature(oma, LoadedFeatureDataType.LOADED_FEATURE);
boolean isArmature = blenderContext.getMarkerValue(ArmatureHelper.ARMATURE_NODE_MARKER, feature) != null;
if (isArmature) {
Skeleton skeleton = blenderContext.getSkeleton(oma);
BoneContext targetBoneContext = blenderContext.getBoneByName(oma, subtargetName);
Bone bone = targetBoneContext.getBone();
if (bone.getParent() == null && (space == Space.CONSTRAINT_SPACE_LOCAL || space == Space.CONSTRAINT_SPACE_PARLOCAL)) {
space = Space.CONSTRAINT_SPACE_POSE;
}
TempVars tempVars = TempVars.get();
switch (space) {
case CONSTRAINT_SPACE_LOCAL:
assert bone.getParent() != null : "CONSTRAINT_SPACE_LOCAL should be evaluated as CONSTRAINT_SPACE_POSE if the bone has no parent!";
bone.setBindTransforms(transform.getTranslation(), transform.getRotation(), transform.getScale());
break;
case CONSTRAINT_SPACE_WORLD: {
Matrix4f boneMatrixInWorldSpace = this.toMatrix(transform, tempVars.tempMat4);
Matrix4f modelWorldMatrix = this.toMatrix(this.getTransform(targetBoneContext.getSkeletonOwnerOma(), null, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat42);
Matrix4f boneMatrixInModelSpace = modelWorldMatrix.invertLocal().multLocal(boneMatrixInWorldSpace);
Bone parent = bone.getParent();
if (parent != null) {
Matrix4f parentMatrixInModelSpace = this.toMatrix(parent.getModelSpacePosition(), parent.getModelSpaceRotation(), parent.getModelSpaceScale(), tempVars.tempMat4);
boneMatrixInModelSpace = parentMatrixInModelSpace.invertLocal().multLocal(boneMatrixInModelSpace);
}
bone.setBindTransforms(boneMatrixInModelSpace.toTranslationVector(), boneMatrixInModelSpace.toRotationQuat(), boneMatrixInModelSpace.toScaleVector());
break;
}
case CONSTRAINT_SPACE_POSE: {
Matrix4f armatureWorldMatrix = this.toMatrix(feature.getWorldTransform(), tempVars.tempMat4);
Matrix4f boneMatrixInWorldSpace = armatureWorldMatrix.multLocal(this.toMatrix(transform, tempVars.tempMat42));
Matrix4f invertedModelMatrix = this.toMatrix(this.getTransform(targetBoneContext.getSkeletonOwnerOma(), null, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat42).invertLocal();
Matrix4f boneMatrixInModelSpace = invertedModelMatrix.multLocal(boneMatrixInWorldSpace);
Bone parent = bone.getParent();
if (parent != null) {
Matrix4f parentMatrixInModelSpace = this.toMatrix(parent.getModelSpacePosition(), parent.getModelSpaceRotation(), parent.getModelSpaceScale(), tempVars.tempMat4);
boneMatrixInModelSpace = parentMatrixInModelSpace.invertLocal().multLocal(boneMatrixInModelSpace);
}
bone.setBindTransforms(boneMatrixInModelSpace.toTranslationVector(), boneMatrixInModelSpace.toRotationQuat(), boneMatrixInModelSpace.toScaleVector());
break;
}
case CONSTRAINT_SPACE_PARLOCAL:
Matrix4f parentLocalInverseMatrix = tempVars.tempMat4;
if (bone.getParent() != null) {
this.toMatrix(bone.getParent().getLocalPosition(), bone.getParent().getLocalRotation(), bone.getParent().getLocalScale(), parentLocalInverseMatrix);
parentLocalInverseMatrix.invertLocal();
} else {
parentLocalInverseMatrix.loadIdentity();
}
Matrix4f m = this.toMatrix(transform.getTranslation(), transform.getRotation(), transform.getScale(), tempVars.tempMat42);
Matrix4f result = parentLocalInverseMatrix.multLocal(m);
Vector3f loc = result.toTranslationVector();
Quaternion rot = result.toRotationQuat().normalizeLocal().multLocal(POS_PARLOC_SPACE_QUATERNION);
Vector3f scl = result.toScaleVector();
bone.setBindTransforms(loc, rot, scl);
break;
default:
tempVars.release();
throw new IllegalStateException("Invalid space type for target object: " + space.toString());
}
tempVars.release();
skeleton.updateWorldVectors();
} else {
switch (space) {
case CONSTRAINT_SPACE_LOCAL:
feature.getLocalTransform().set(transform);
break;
case CONSTRAINT_SPACE_WORLD:
if (feature.getParent() == null) {
feature.setLocalTransform(transform);
} else {
Transform parentWorldTransform = feature.getParent().getWorldTransform();
TempVars tempVars = TempVars.get();
Matrix4f parentInverseMatrix = this.toMatrix(parentWorldTransform, tempVars.tempMat4).invertLocal();
Matrix4f m = this.toMatrix(transform, tempVars.tempMat42);
m = m.multLocal(parentInverseMatrix);
tempVars.release();
transform.setTranslation(m.toTranslationVector());
transform.setRotation(m.toRotationQuat());
transform.setScale(m.toScaleVector());
feature.setLocalTransform(transform);
}
break;
default:
throw new IllegalStateException("Invalid space type for spatial object: " + space.toString());
}
}
}
/**
* Converts given transform to the matrix.
*
* @param transform
* the transform to be converted
* @param store
* the matrix where the result will be stored
* @return the store matrix
*/
public Matrix4f toMatrix(Transform transform, Matrix4f store) {
if (transform != null) {
return this.toMatrix(transform.getTranslation(), transform.getRotation(), transform.getScale(), store);
}
store.loadIdentity();
return store;
}
/**
* Converts given transformation parameters into the matrix.
*
* @param position
* the position of the feature
* @param rotation
* the rotation of the feature
* @param scale
* the scale of the feature
* @param store
* the matrix where the result will be stored
* @return the store matrix
*/
private Matrix4f toMatrix(Vector3f position, Quaternion rotation, Vector3f scale, Matrix4f store) {
store.loadIdentity();
store.setTranslation(position);
store.setRotationQuaternion(rotation);
store.setScale(scale);
return store;
}
/**
* The space of target or owner transformation.
*
* @author Marcin Roguski (Kaelthas)
*/
public static enum Space {
/** A transformation of the bone or spatial in the world space. */
CONSTRAINT_SPACE_WORLD,
/**
* For spatial it is the transformation in its parent space or in WORLD space if it has no parent.
* For bone it is a transformation in its bone parent space or in armature space if it has no parent.
*/
CONSTRAINT_SPACE_LOCAL,
/**
* This space IS NOT applicable for spatials.
* For bone it is a transformation in the blender's armature object space.
*/
CONSTRAINT_SPACE_POSE,
CONSTRAINT_SPACE_PARLOCAL;
/**
* This method returns the enum instance when given the appropriate
* value from the blend file.
*
* @param c
* the blender's value of the space modifier
* @return the scape enum instance
*/
public static Space valueOf(byte c) {
switch (c) {
case 0:
return CONSTRAINT_SPACE_WORLD;
case 1:
return CONSTRAINT_SPACE_LOCAL;
case 2:
return CONSTRAINT_SPACE_POSE;
case 3:
return CONSTRAINT_SPACE_PARLOCAL;
default:
throw new IllegalArgumentException("Value: " + c + " cannot be converted to Space enum instance!");
}
}
}
}

@ -0,0 +1,451 @@
package com.jme3.scene.plugins.blender.constraints;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.Animation;
import com.jme3.animation.Bone;
import com.jme3.animation.BoneTrack;
import com.jme3.animation.Skeleton;
import com.jme3.animation.SpatialTrack;
import com.jme3.animation.Track;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.util.TempVars;
/**
* A node that represents either spatial or bone in constraint simulation. The
* node is applied its translation, rotation and scale for each frame of its
* animation. Then the constraints are applied that will eventually alter it.
* After that the feature's transformation is stored in VirtualTrack which is
* converted to new bone or spatial track at the very end.
*
* @author Marcin Roguski (Kaelthas)
*/
public class SimulationNode {
private static final Logger LOGGER = Logger.getLogger(SimulationNode.class.getName());
/** The blender context. */
private BlenderContext blenderContext;
/** The name of the node (for debugging purposes). */
private String name;
/** A list of children for the node (either bones or child spatials). */
private List<SimulationNode> children = new ArrayList<SimulationNode>();
/** A list of constraints that the current node has. */
private List<Constraint> constraints;
/** A list of node's animations. */
private List<Animation> animations;
/** The nodes spatial (if null then the boneContext should be set). */
private Spatial spatial;
/** The skeleton of the bone (not null if the node simulated the bone). */
private Skeleton skeleton;
/** Animation controller for the node's feature. */
private AnimControl animControl;
/**
* The star transform of a spatial. Needed to properly reset the spatial to
* its start position.
*/
private Transform spatialStartTransform;
/** Star transformations for bones. Needed to properly reset the bones. */
private Map<Bone, Transform> boneStartTransforms;
/**
* Builds the nodes tree for the given feature. The feature (bone or
* spatial) is found by its OMA. The feature must be a root bone or a root
* spatial.
*
* @param featureOMA
* the OMA of either bone or spatial
* @param blenderContext
* the blender context
*/
public SimulationNode(Long featureOMA, BlenderContext blenderContext) {
this(featureOMA, blenderContext, true);
}
/**
* Creates the node for the feature.
*
* @param featureOMA
* the OMA of either bone or spatial
* @param blenderContext
* the blender context
* @param rootNode
* indicates if the feature is a root bone or root spatial or not
*/
private SimulationNode(Long featureOMA, BlenderContext blenderContext, boolean rootNode) {
this.blenderContext = blenderContext;
Node spatial = (Node) blenderContext.getLoadedFeature(featureOMA, LoadedFeatureDataType.LOADED_FEATURE);
if (blenderContext.getMarkerValue(ArmatureHelper.ARMATURE_NODE_MARKER, spatial) != null) {
skeleton = blenderContext.getSkeleton(featureOMA);
Node nodeWithAnimationControl = blenderContext.getControlledNode(skeleton);
animControl = nodeWithAnimationControl.getControl(AnimControl.class);
boneStartTransforms = new HashMap<Bone, Transform>();
for (int i = 0; i < skeleton.getBoneCount(); ++i) {
Bone bone = skeleton.getBone(i);
boneStartTransforms.put(bone, new Transform(bone.getWorldBindPosition(), bone.getWorldBindRotation(), bone.getWorldBindScale()));
}
} else {
if (rootNode && spatial.getParent() != null) {
throw new IllegalStateException("Given spatial must be a root node!");
}
this.spatial = spatial;
spatialStartTransform = spatial.getLocalTransform().clone();
}
name = '>' + spatial.getName() + '<';
constraints = this.findConstraints(featureOMA, blenderContext);
if (constraints == null) {
constraints = new ArrayList<Constraint>();
}
// add children nodes
if (skeleton != null) {
// bone with index 0 is a root bone and should not be considered
// here
for (int i = 1; i < skeleton.getBoneCount(); ++i) {
BoneContext boneContext = blenderContext.getBoneContext(skeleton.getBone(i));
List<Constraint> boneConstraints = this.findConstraints(boneContext.getBoneOma(), blenderContext);
if (boneConstraints != null) {
constraints.addAll(boneConstraints);
}
}
// each bone of the skeleton has the same anim data applied
BoneContext boneContext = blenderContext.getBoneContext(skeleton.getBone(1));
Long boneOma = boneContext.getBoneOma();
animations = blenderContext.getAnimData(boneOma) == null ? null : blenderContext.getAnimData(boneOma).anims;
} else {
animations = blenderContext.getAnimData(featureOMA) == null ? null : blenderContext.getAnimData(featureOMA).anims;
for (Spatial child : spatial.getChildren()) {
if (child instanceof Node) {
children.add(new SimulationNode((Long) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, child), blenderContext, false));
}
}
}
LOGGER.info("Removing invalid constraints.");
List<Constraint> validConstraints = new ArrayList<Constraint>(constraints.size());
for (Constraint constraint : constraints) {
if (constraint.validate()) {
validConstraints.add(constraint);
} else {
LOGGER.log(Level.WARNING, "Constraint {0} is invalid and will not be applied.", constraint.name);
}
}
constraints = validConstraints;
}
/**
* Tells if the node already contains the given constraint (so that it is
* not applied twice).
*
* @param constraint
* the constraint to be checked
* @return <b>true</b> if the constraint already is stored in the node and
* <b>false</b> otherwise
*/
public boolean contains(Constraint constraint) {
boolean result = false;
if (constraints != null && constraints.size() > 0) {
for (Constraint c : constraints) {
if (c.equals(constraint)) {
return true;
}
}
}
return result;
}
/**
* Resets the node's feature to its starting transformation.
*/
private void reset() {
if (spatial != null) {
spatial.setLocalTransform(spatialStartTransform);
for (SimulationNode child : children) {
child.reset();
}
} else if (skeleton != null) {
for (Entry<Bone, Transform> entry : boneStartTransforms.entrySet()) {
Transform t = entry.getValue();
entry.getKey().setBindTransforms(t.getTranslation(), t.getRotation(), t.getScale());
}
skeleton.reset();
}
}
/**
* Simulates the spatial node.
*/
private void simulateSpatial() {
if (constraints != null && constraints.size() > 0) {
boolean applyStaticConstraints = true;
if (animations != null) {
for (Animation animation : animations) {
float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation);
int maxFrame = (int) animationTimeBoundaries[0];
float maxTime = animationTimeBoundaries[1];
VirtualTrack vTrack = new VirtualTrack(maxFrame, maxTime);
for (Track track : animation.getTracks()) {
for (int frame = 0; frame < maxFrame; ++frame) {
spatial.setLocalTranslation(((SpatialTrack) track).getTranslations()[frame]);
spatial.setLocalRotation(((SpatialTrack) track).getRotations()[frame]);
spatial.setLocalScale(((SpatialTrack) track).getScales()[frame]);
for (Constraint constraint : constraints) {
constraint.apply(frame);
vTrack.setTransform(frame, spatial.getLocalTransform());
}
}
Track newTrack = vTrack.getAsSpatialTrack();
if (newTrack != null) {
animation.removeTrack(track);
animation.addTrack(newTrack);
}
applyStaticConstraints = false;
}
}
}
// if there are no animations then just constraint the static
// object's transformation
if (applyStaticConstraints) {
for (Constraint constraint : constraints) {
constraint.apply(0);
}
}
}
for (SimulationNode child : children) {
child.simulate();
}
}
/**
* Simulates the bone node.
*/
private void simulateSkeleton() {
if (constraints != null && constraints.size() > 0) {
Set<Long> alteredOmas = new HashSet<Long>();
if (animations != null) {
TempVars vars = TempVars.get();
AnimChannel animChannel = animControl.createChannel();
for (Animation animation : animations) {
float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation);
int maxFrame = (int) animationTimeBoundaries[0];
float maxTime = animationTimeBoundaries[1];
Map<Integer, VirtualTrack> tracks = new HashMap<Integer, VirtualTrack>();
Map<Integer, Transform> previousTransforms = this.getInitialTransforms();
for (int frame = 0; frame < maxFrame; ++frame) {
// this MUST be done here, otherwise setting next frame of animation will
// lead to possible errors
this.reset();
// first set proper time for all bones in all the tracks ...
for (Track track : animation.getTracks()) {
float time = ((BoneTrack) track).getTimes()[frame];
track.setTime(time, 1, animControl, animChannel, vars);
skeleton.updateWorldVectors();
}
// ... and then apply constraints from the root bone to the last child ...
for (Bone rootBone : skeleton.getRoots()) {
if(skeleton.getBoneIndex(rootBone) > 0) {
//ommit the 0 - indexed root bone as it is the bone added by importer
this.applyConstraints(rootBone, alteredOmas, frame);
}
}
// ... add virtual tracks if neccessary, for bones that were altered but had no tracks before ...
for (Long boneOMA : alteredOmas) {
BoneContext boneContext = blenderContext.getBoneContext(boneOMA);
int boneIndex = skeleton.getBoneIndex(boneContext.getBone());
if (!tracks.containsKey(boneIndex)) {
tracks.put(boneIndex, new VirtualTrack(maxFrame, maxTime));
}
}
alteredOmas.clear();
// ... and fill in another frame in the result track
for (Entry<Integer, VirtualTrack> trackEntry : tracks.entrySet()) {
Integer boneIndex = trackEntry.getKey();
Bone bone = skeleton.getBone(boneIndex);
// take the initial transform of a bone and its virtual track
Transform previousTransform = previousTransforms.get(boneIndex);
VirtualTrack vTrack = trackEntry.getValue();
Vector3f bonePositionDifference = bone.getLocalPosition().subtract(previousTransform.getTranslation());
Quaternion boneRotationDifference = bone.getLocalRotation().mult(previousTransform.getRotation().inverse()).normalizeLocal();
Vector3f boneScaleDifference = bone.getLocalScale().divide(previousTransform.getScale());
if (frame > 0) {
bonePositionDifference = vTrack.translations.get(frame - 1).add(bonePositionDifference);
boneRotationDifference = vTrack.rotations.get(frame - 1).mult(boneRotationDifference);
boneScaleDifference = vTrack.scales.get(frame - 1).mult(boneScaleDifference);
}
vTrack.setTransform(frame, new Transform(bonePositionDifference, boneRotationDifference, boneScaleDifference));
previousTransform.setTranslation(bone.getLocalPosition());
previousTransform.setRotation(bone.getLocalRotation());
previousTransform.setScale(bone.getLocalScale());
}
}
for (Entry<Integer, VirtualTrack> trackEntry : tracks.entrySet()) {
Track newTrack = trackEntry.getValue().getAsBoneTrack(trackEntry.getKey());
if (newTrack != null) {
boolean trackReplaced = false;
for (Track track : animation.getTracks()) {
if (((BoneTrack) track).getTargetBoneIndex() == trackEntry.getKey().intValue()) {
animation.removeTrack(track);
animation.addTrack(newTrack);
trackReplaced = true;
break;
}
}
if (!trackReplaced) {
animation.addTrack(newTrack);
}
}
}
}
vars.release();
animControl.clearChannels();
this.reset();
}
}
}
/**
* Applies constraints to the given bone and its children.
* The goal is to apply constraint from root bone to the last child.
* @param bone
* the bone whose constraints will be applied
* @param alteredOmas
* the set of OMAS of the altered bones (is populated if necessary)
* @param frame
* the current frame of the animation
*/
private void applyConstraints(Bone bone, Set<Long> alteredOmas, int frame) {
BoneContext boneContext = blenderContext.getBoneContext(bone);
List<Constraint> constraints = this.findConstraints(boneContext.getBoneOma(), blenderContext);
if (constraints != null && constraints.size() > 0) {
for (Constraint constraint : constraints) {
constraint.apply(frame);
if (constraint.getAlteredOmas() != null) {
alteredOmas.addAll(constraint.getAlteredOmas());
}
alteredOmas.add(boneContext.getBoneOma());
}
}
for (Bone child : bone.getChildren()) {
this.applyConstraints(child, alteredOmas, frame);
}
}
/**
* Simulates the node.
*/
public void simulate() {
this.reset();
if (spatial != null) {
this.simulateSpatial();
} else {
this.simulateSkeleton();
}
}
/**
* Computes the maximum frame and time for the animation. Different tracks
* can have different lengths so here the maximum one is being found.
*
* @param animation
* the animation
* @return maximum frame and time of the animation
*/
private float[] computeAnimationTimeBoundaries(Animation animation) {
int maxFrame = Integer.MIN_VALUE;
float maxTime = Float.MIN_VALUE;
for (Track track : animation.getTracks()) {
if (track instanceof BoneTrack) {
maxFrame = Math.max(maxFrame, ((BoneTrack) track).getTranslations().length);
maxTime = Math.max(maxTime, ((BoneTrack) track).getTimes()[((BoneTrack) track).getTimes().length - 1]);
} else if (track instanceof SpatialTrack) {
maxFrame = Math.max(maxFrame, ((SpatialTrack) track).getTranslations().length);
maxTime = Math.max(maxTime, ((SpatialTrack) track).getTimes()[((SpatialTrack) track).getTimes().length - 1]);
} else {
throw new IllegalStateException("Unsupported track type for simuation: " + track);
}
}
return new float[] { maxFrame, maxTime };
}
/**
* Finds constraints for the node's features.
*
* @param ownerOMA
* the feature's OMA
* @param blenderContext
* the blender context
* @return a list of feature's constraints or empty list if none were found
*/
private List<Constraint> findConstraints(Long ownerOMA, BlenderContext blenderContext) {
List<Constraint> result = new ArrayList<Constraint>();
List<Constraint> constraints = blenderContext.getConstraints(ownerOMA);
if(constraints != null) {
for (Constraint constraint : constraints) {
if (constraint.isImplemented() && constraint.validate()) {
result.add(constraint);
} else {
LOGGER.log(Level.WARNING, "Constraint named: ''{0}'' of type ''{1}'' is not implemented and will NOT be applied!", new Object[] { constraint.name, constraint.getConstraintTypeName() });
}
}
}
return result.size() > 0 ? result : null;
}
/**
* Creates the initial transforms for all bones in the skelketon.
* @return the map where the key is the bone index and the value us the bone's initial transformation
*/
private Map<Integer, Transform> getInitialTransforms() {
Map<Integer, Transform> result = new HashMap<Integer, Transform>();
for (int i = 0; i < skeleton.getBoneCount(); ++i) {
Bone bone = skeleton.getBone(i);
result.put(i, new Transform(bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale()));
}
return result;
}
@Override
public String toString() {
return name;
}
}

@ -0,0 +1,35 @@
package com.jme3.scene.plugins.blender.constraints;
import java.util.logging.Logger;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.Ipo;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* Constraint applied on the skeleton. This constraint is here only to make the
* application not crash when loads constraints applied to armature. But
* skeleton movement is not supported by jme so the constraint will never be
* applied.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class SkeletonConstraint extends Constraint {
private static final Logger LOGGER = Logger.getLogger(SkeletonConstraint.class.getName());
public SkeletonConstraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
super(constraintStructure, ownerOMA, influenceIpo, blenderContext);
}
@Override
public boolean validate() {
LOGGER.warning("Constraints for skeleton are not supported.");
return false;
}
@Override
public void apply(int frame) {
LOGGER.warning("Applying constraints to skeleton is not supported.");
}
}

@ -0,0 +1,27 @@
package com.jme3.scene.plugins.blender.constraints;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.animations.Ipo;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* Constraint applied on the spatial objects. This includes: nodes, cameras
* nodes and light nodes.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class SpatialConstraint extends Constraint {
public SpatialConstraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
super(constraintStructure, ownerOMA, influenceIpo, blenderContext);
}
@Override
public boolean validate() {
if (targetOMA != null) {
return blenderContext.getLoadedFeature(targetOMA, LoadedFeatureDataType.LOADED_FEATURE) != null;
}
return true;
}
}

@ -0,0 +1,146 @@
package com.jme3.scene.plugins.blender.constraints;
import java.util.ArrayList;
import com.jme3.animation.BoneTrack;
import com.jme3.animation.SpatialTrack;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
/**
* A virtual track that stores computed frames after constraints are applied.
* Not all the frames need to be inserted. If there are lacks then the class
* will fill the gaps.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class VirtualTrack {
/** The last frame for the track. */
public int maxFrame;
/** The max time for the track. */
public float maxTime;
/** Translations of the track. */
public ArrayList<Vector3f> translations;
/** Rotations of the track. */
public ArrayList<Quaternion> rotations;
/** Scales of the track. */
public ArrayList<Vector3f> scales;
/**
* Constructs the object storing the maximum frame and time.
*
* @param maxFrame
* the last frame for the track
* @param maxTime
* the max time for the track
*/
public VirtualTrack(int maxFrame, float maxTime) {
this.maxFrame = maxFrame;
this.maxTime = maxTime;
}
/**
* Sets the transform for the given frame.
*
* @param frameIndex
* the frame for which the transform will be set
* @param transform
* the transformation to be set
*/
public void setTransform(int frameIndex, Transform transform) {
if (translations == null) {
translations = this.createList(Vector3f.ZERO, frameIndex);
}
this.append(translations, Vector3f.ZERO, frameIndex - translations.size());
translations.add(transform.getTranslation().clone());
if (rotations == null) {
rotations = this.createList(Quaternion.IDENTITY, frameIndex);
}
this.append(rotations, Quaternion.IDENTITY, frameIndex - rotations.size());
rotations.add(transform.getRotation().clone());
if (scales == null) {
scales = this.createList(Vector3f.UNIT_XYZ, frameIndex);
}
this.append(scales, Vector3f.UNIT_XYZ, frameIndex - scales.size());
scales.add(transform.getScale().clone());
}
/**
* Returns the track as a bone track.
*
* @param targetBoneIndex
* the bone index
* @return the bone track
*/
public BoneTrack getAsBoneTrack(int targetBoneIndex) {
if (translations == null && rotations == null && scales == null) {
return null;
}
return new BoneTrack(targetBoneIndex, this.createTimes(), translations.toArray(new Vector3f[maxFrame]), rotations.toArray(new Quaternion[maxFrame]), scales.toArray(new Vector3f[maxFrame]));
}
/**
* Returns the track as a spatial track.
*
* @return the spatial track
*/
public SpatialTrack getAsSpatialTrack() {
if (translations == null && rotations == null && scales == null) {
return null;
}
return new SpatialTrack(this.createTimes(), translations.toArray(new Vector3f[maxFrame]), rotations.toArray(new Quaternion[maxFrame]), scales.toArray(new Vector3f[maxFrame]));
}
/**
* The method creates times for the track based on the given maximum values.
*
* @return the times for the track
*/
private float[] createTimes() {
float[] times = new float[maxFrame];
float dT = maxTime / (float) maxFrame;
float t = 0;
for (int i = 0; i < maxFrame; ++i) {
times[i] = t;
t += dT;
}
return times;
}
/**
* Helper method that creates a list of a given size filled with given
* elements.
*
* @param element
* the element to be put into the list
* @param count
* the list size
* @return the list
*/
private <T> ArrayList<T> createList(T element, int count) {
ArrayList<T> result = new ArrayList<T>(count);
for (int i = 0; i < count; ++i) {
result.add(element);
}
return result;
}
/**
* Appends the element to the given list.
*
* @param list
* the list where the element will be appended
* @param element
* the element to be appended
* @param count
* how many times the element will be appended
*/
private <T> void append(ArrayList<T> list, T element, int count) {
for (int i = 0; i < count; ++i) {
list.add(element);
}
}
}

@ -0,0 +1,135 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import java.util.Set;
import com.jme3.animation.Bone;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* A base class for all constraint definitions.
*
* @author Marcin Roguski (Kaelthas)
*/
public abstract class ConstraintDefinition {
protected ConstraintHelper constraintHelper;
/** Constraints flag. Used to load user's options applied to the constraint. */
protected int flag;
/** The constraint's owner. Loaded during runtime. */
private Object owner;
/** The blender context. */
protected BlenderContext blenderContext;
/** The constraint's owner OMA. */
protected Long ownerOMA;
/** Stores the OMA addresses of all features whose transform had been altered beside the constraint owner. */
protected Set<Long> alteredOmas;
/**
* Loads a constraint definition based on the constraint definition
* structure.
*
* @param constraintData
* the constraint definition structure
* @param ownerOMA
* the constraint's owner OMA
* @param blenderContext
* the blender context
*/
public ConstraintDefinition(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
if (constraintData != null) {// Null constraint has no data
Number flag = (Number) constraintData.getFieldValue("flag");
if (flag != null) {
this.flag = flag.intValue();
}
}
this.blenderContext = blenderContext;
constraintHelper = (ConstraintHelper) (blenderContext == null ? null : blenderContext.getHelper(ConstraintHelper.class));
this.ownerOMA = ownerOMA;
}
/**
* This method is here because we have no guarantee that the owner is loaded
* when constraint is being created. So use it to get the owner when it is
* needed for computations.
*
* @return the owner of the constraint or null if none is set
*/
protected Object getOwner() {
if (ownerOMA != null && owner == null) {
owner = blenderContext.getLoadedFeature(ownerOMA, LoadedFeatureDataType.LOADED_FEATURE);
if (owner == null) {
throw new IllegalStateException("Cannot load constraint's owner for constraint type: " + this.getClass().getName());
}
}
return owner;
}
/**
* The method gets the owner's transformation. The owner can be either bone or spatial.
* @param ownerSpace
* the space in which the computed transformation is given
* @return the constraint owner's transformation
*/
protected Transform getOwnerTransform(Space ownerSpace) {
if (this.getOwner() instanceof Bone) {
BoneContext boneContext = blenderContext.getBoneContext(ownerOMA);
return constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), ownerSpace);
}
return constraintHelper.getTransform(ownerOMA, null, ownerSpace);
}
/**
* The method applies the given transformation to the owner.
* @param ownerTransform
* the transformation to apply to the owner
* @param ownerSpace
* the space that defines which owner's transformation (ie. global, local, etc. will be set)
*/
protected void applyOwnerTransform(Transform ownerTransform, Space ownerSpace) {
if (this.getOwner() instanceof Bone) {
BoneContext boneContext = blenderContext.getBoneContext(ownerOMA);
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), ownerSpace, ownerTransform);
} else {
constraintHelper.applyTransform(ownerOMA, null, ownerSpace, ownerTransform);
}
}
/**
* @return <b>true</b> if the definition is implemented and <b>false</b>
* otherwise
*/
public boolean isImplemented() {
return true;
}
/**
* @return a list of all OMAs of the features that the constraint had altered beside its owner
*/
public Set<Long> getAlteredOmas() {
return alteredOmas;
}
/**
* @return the type name of the constraint
*/
public abstract String getConstraintTypeName();
/**
* Bakes the constraint for the current feature (bone or spatial) position.
*
* @param ownerSpace
* the space where owner transform will be evaluated in
* @param targetSpace
* the space where target transform will be evaluated in
* @param targetTransform
* the target transform used by some of the constraints
* @param influence
* the influence of the constraint (from range <0; 1>)
*/
public abstract void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence);
}

@ -0,0 +1,77 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.animation.Bone;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class represents 'Dist limit' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ConstraintDefinitionDistLimit extends ConstraintDefinition {
private static final int LIMITDIST_INSIDE = 0;
private static final int LIMITDIST_OUTSIDE = 1;
private static final int LIMITDIST_ONSURFACE = 2;
protected int mode;
protected float dist;
public ConstraintDefinitionDistLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
mode = ((Number) constraintData.getFieldValue("mode")).intValue();
dist = ((Number) constraintData.getFieldValue("dist")).floatValue();
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
if (this.getOwner() instanceof Bone && ((Bone) this.getOwner()).getParent() != null &&
blenderContext.getBoneContext(ownerOMA).is(BoneContext.CONNECTED_TO_PARENT)) {
// distance limit does not work on bones who are connected to their parent
return;
}
Transform ownerTransform = this.getOwnerTransform(ownerSpace);
Vector3f v = ownerTransform.getTranslation().subtract(targetTransform.getTranslation());
float currentDistance = v.length();
switch (mode) {
case LIMITDIST_INSIDE:
if (currentDistance >= dist) {
v.normalizeLocal();
v.multLocal(dist + (currentDistance - dist) * (1.0f - influence));
ownerTransform.getTranslation().set(v.addLocal(targetTransform.getTranslation()));
}
break;
case LIMITDIST_ONSURFACE:
if (currentDistance > dist) {
v.normalizeLocal();
v.multLocal(dist + (currentDistance - dist) * (1.0f - influence));
ownerTransform.getTranslation().set(v.addLocal(targetTransform.getTranslation()));
} else if (currentDistance < dist) {
v.normalizeLocal().multLocal(dist * influence);
ownerTransform.getTranslation().set(targetTransform.getTranslation().add(v));
}
break;
case LIMITDIST_OUTSIDE:
if (currentDistance <= dist) {
v = targetTransform.getTranslation().subtract(ownerTransform.getTranslation()).normalizeLocal().multLocal(dist * influence);
ownerTransform.getTranslation().set(targetTransform.getTranslation().add(v));
}
break;
default:
throw new IllegalStateException("Unknown distance limit constraint mode: " + mode);
}
this.applyOwnerTransform(ownerTransform, ownerSpace);
}
@Override
public String getConstraintTypeName() {
return "Limit distance";
}
}

@ -0,0 +1,124 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.constraints.definitions;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
public class ConstraintDefinitionFactory {
private static final Map<String, Class<? extends ConstraintDefinition>> CONSTRAINT_CLASSES = new HashMap<String, Class<? extends ConstraintDefinition>>();
static {
CONSTRAINT_CLASSES.put("bDistLimitConstraint", ConstraintDefinitionDistLimit.class);
CONSTRAINT_CLASSES.put("bLocateLikeConstraint", ConstraintDefinitionLocLike.class);
CONSTRAINT_CLASSES.put("bLocLimitConstraint", ConstraintDefinitionLocLimit.class);
CONSTRAINT_CLASSES.put("bNullConstraint", ConstraintDefinitionNull.class);
CONSTRAINT_CLASSES.put("bRotateLikeConstraint", ConstraintDefinitionRotLike.class);
CONSTRAINT_CLASSES.put("bRotLimitConstraint", ConstraintDefinitionRotLimit.class);
CONSTRAINT_CLASSES.put("bSizeLikeConstraint", ConstraintDefinitionSizeLike.class);
CONSTRAINT_CLASSES.put("bSizeLimitConstraint", ConstraintDefinitionSizeLimit.class);
CONSTRAINT_CLASSES.put("bKinematicConstraint", ConstraintDefinitionIK.class);
}
private static final Map<String, String> UNSUPPORTED_CONSTRAINTS = new HashMap<String, String>();
static {
UNSUPPORTED_CONSTRAINTS.put("bActionConstraint", "Action");
UNSUPPORTED_CONSTRAINTS.put("bChildOfConstraint", "Child of");
UNSUPPORTED_CONSTRAINTS.put("bClampToConstraint", "Clamp to");
UNSUPPORTED_CONSTRAINTS.put("bFollowPathConstraint", "Follow path");
UNSUPPORTED_CONSTRAINTS.put("bLockTrackConstraint", "Lock track");
UNSUPPORTED_CONSTRAINTS.put("bMinMaxConstraint", "Min max");
UNSUPPORTED_CONSTRAINTS.put("bPythonConstraint", "Python/Script");
UNSUPPORTED_CONSTRAINTS.put("bRigidBodyJointConstraint", "Rigid body joint");
UNSUPPORTED_CONSTRAINTS.put("bShrinkWrapConstraint", "Shrinkwrap");
UNSUPPORTED_CONSTRAINTS.put("bStretchToConstraint", "Stretch to");
UNSUPPORTED_CONSTRAINTS.put("bTransformConstraint", "Transform");
// Blender 2.50+
UNSUPPORTED_CONSTRAINTS.put("bSplineIKConstraint", "Spline inverse kinematics");
UNSUPPORTED_CONSTRAINTS.put("bDampTrackConstraint", "Damp track");
UNSUPPORTED_CONSTRAINTS.put("bPivotConstraint", "Pivot");
// Blender 2.56+
UNSUPPORTED_CONSTRAINTS.put("bTrackToConstraint", "Track to");
UNSUPPORTED_CONSTRAINTS.put("bSameVolumeConstraint", "Same volume");
UNSUPPORTED_CONSTRAINTS.put("bTransLikeConstraint", "Trans like");
// Blender 2.62+
UNSUPPORTED_CONSTRAINTS.put("bCameraSolverConstraint", "Camera solver");
UNSUPPORTED_CONSTRAINTS.put("bObjectSolverConstraint", "Object solver");
UNSUPPORTED_CONSTRAINTS.put("bFollowTrackConstraint", "Follow track");
}
/**
* This method creates the constraint instance.
*
* @param constraintStructure
* the constraint's structure (bConstraint clss in blender 2.49).
* If the value is null the NullConstraint is created.
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public static ConstraintDefinition createConstraintDefinition(Structure constraintStructure, Long ownerOMA, BlenderContext blenderContext) throws BlenderFileException {
if (constraintStructure == null) {
return new ConstraintDefinitionNull(null, ownerOMA, blenderContext);
}
String constraintClassName = constraintStructure.getType();
Class<? extends ConstraintDefinition> constraintDefinitionClass = CONSTRAINT_CLASSES.get(constraintClassName);
if (constraintDefinitionClass != null) {
try {
return (ConstraintDefinition) constraintDefinitionClass.getDeclaredConstructors()[0].newInstance(constraintStructure, ownerOMA, blenderContext);
} catch (IllegalArgumentException e) {
throw new BlenderFileException(e.getLocalizedMessage(), e);
} catch (SecurityException e) {
throw new BlenderFileException(e.getLocalizedMessage(), e);
} catch (InstantiationException e) {
throw new BlenderFileException(e.getLocalizedMessage(), e);
} catch (IllegalAccessException e) {
throw new BlenderFileException(e.getLocalizedMessage(), e);
} catch (InvocationTargetException e) {
throw new BlenderFileException(e.getLocalizedMessage(), e);
}
} else {
String constraintName = UNSUPPORTED_CONSTRAINTS.get(constraintClassName);
if (constraintName != null) {
return new UnsupportedConstraintDefinition(constraintName);
} else {
throw new BlenderFileException("Unknown constraint type: " + constraintClassName);
}
}
}
}

@ -0,0 +1,117 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import com.jme3.animation.Bone;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
public class ConstraintDefinitionIK extends ConstraintDefinition {
private static final int FLAG_POSITION = 0x20;
/** The number of affected bones. Zero means that all parent bones of the current bone should take part in baking. */
private int bonesAffected;
private float chainLength;
private BoneContext[] bones;
private boolean needToCompute = true;
public ConstraintDefinitionIK(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
bonesAffected = ((Number) constraintData.getFieldValue("rootbone")).intValue();
if ((flag & FLAG_POSITION) == 0) {
needToCompute = false;
}
if (needToCompute) {
alteredOmas = new HashSet<Long>();
}
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
if (needToCompute && influence != 0) {
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
BoneContext[] boneContexts = this.getBones();
float b = chainLength;
Quaternion boneWorldRotation = new Quaternion();
for (int i = 0; i < boneContexts.length; ++i) {
Bone bone = boneContexts[i].getBone();
bone.updateWorldVectors();
Transform boneWorldTransform = constraintHelper.getTransform(boneContexts[i].getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD);
Vector3f head = boneWorldTransform.getTranslation();
Vector3f tail = head.add(bone.getModelSpaceRotation().mult(Vector3f.UNIT_Y.mult(boneContexts[i].getLength())));
Vector3f vectorA = tail.subtract(head);
float a = vectorA.length();
vectorA.normalizeLocal();
Vector3f vectorC = targetTransform.getTranslation().subtract(head);
float c = vectorC.length();
vectorC.normalizeLocal();
b -= a;
float theta = 0;
if (c >= a + b) {
theta = vectorA.angleBetween(vectorC);
} else if (c <= FastMath.abs(a - b) && i < boneContexts.length - 1) {
theta = vectorA.angleBetween(vectorC) - FastMath.HALF_PI;
} else {
theta = vectorA.angleBetween(vectorC) - FastMath.acos(-(b * b - a * a - c * c) / (2 * a * c));
}
theta *= influence;
if (theta != 0) {
Vector3f vectorR = vectorA.cross(vectorC);
boneWorldRotation.fromAngleAxis(theta, vectorR);
boneWorldTransform.getRotation().multLocal(boneWorldRotation);
constraintHelper.applyTransform(boneContexts[i].getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD, boneWorldTransform);
}
bone.updateWorldVectors();
alteredOmas.add(boneContexts[i].getBoneOma());
}
}
}
@Override
public String getConstraintTypeName() {
return "Inverse kinematics";
}
/**
* @return the bone contexts of all bones that will be used in this constraint computations
*/
private BoneContext[] getBones() {
if (bones == null) {
List<BoneContext> bones = new ArrayList<BoneContext>();
Bone bone = (Bone) this.getOwner();
while (bone != null) {
BoneContext boneContext = blenderContext.getBoneContext(bone);
bones.add(0, boneContext);
chainLength += boneContext.getLength();
if (bonesAffected != 0 && bones.size() >= bonesAffected) {
break;
}
bone = bone.getParent();
}
this.bones = bones.toArray(new BoneContext[bones.size()]);
}
return bones;
}
}

@ -0,0 +1,96 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.animation.Bone;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class represents 'Loc like' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ConstraintDefinitionLocLike extends ConstraintDefinition {
private static final int LOCLIKE_X = 0x01;
private static final int LOCLIKE_Y = 0x02;
private static final int LOCLIKE_Z = 0x04;
// protected static final int LOCLIKE_TIP = 0x08;//this is deprecated in
// blender
private static final int LOCLIKE_X_INVERT = 0x10;
private static final int LOCLIKE_Y_INVERT = 0x20;
private static final int LOCLIKE_Z_INVERT = 0x40;
private static final int LOCLIKE_OFFSET = 0x80;
public ConstraintDefinitionLocLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
if (blenderContext.getBlenderKey().isFixUpAxis()) {
// swapping Y and X limits flag in the bitwise flag
int y = flag & LOCLIKE_Y;
int invY = flag & LOCLIKE_Y_INVERT;
int z = flag & LOCLIKE_Z;
int invZ = flag & LOCLIKE_Z_INVERT;
// clear the other flags to swap them
flag &= LOCLIKE_X | LOCLIKE_X_INVERT | LOCLIKE_OFFSET;
flag |= y << 1;
flag |= invY << 1;
flag |= z >> 1;
flag |= invZ >> 1;
}
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
if (this.getOwner() instanceof Bone && ((Bone) this.getOwner()).getParent() != null &&
blenderContext.getBoneContext(ownerOMA).is(BoneContext.CONNECTED_TO_PARENT)) {
// location copy does not work on bones who are connected to their parent
return;
}
Transform ownerTransform = this.getOwnerTransform(ownerSpace);
Vector3f ownerLocation = ownerTransform.getTranslation();
Vector3f targetLocation = targetTransform.getTranslation();
Vector3f startLocation = ownerTransform.getTranslation().clone();
Vector3f offset = Vector3f.ZERO;
if ((flag & LOCLIKE_OFFSET) != 0) {// we add the original location to the copied location
offset = startLocation;
}
if ((flag & LOCLIKE_X) != 0) {
ownerLocation.x = targetLocation.x;
if ((flag & LOCLIKE_X_INVERT) != 0) {
ownerLocation.x = -ownerLocation.x;
}
}
if ((flag & LOCLIKE_Y) != 0) {
ownerLocation.y = targetLocation.y;
if ((flag & LOCLIKE_Y_INVERT) != 0) {
ownerLocation.y = -ownerLocation.y;
}
}
if ((flag & LOCLIKE_Z) != 0) {
ownerLocation.z = targetLocation.z;
if ((flag & LOCLIKE_Z_INVERT) != 0) {
ownerLocation.z = -ownerLocation.z;
}
}
ownerLocation.addLocal(offset);
if (influence < 1.0f) {
startLocation.subtractLocal(ownerLocation).normalizeLocal().mult(influence);
ownerLocation.addLocal(startLocation);
}
this.applyOwnerTransform(ownerTransform, ownerSpace);
}
@Override
public String getConstraintTypeName() {
return "Copy location";
}
}

@ -0,0 +1,95 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.animation.Bone;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class represents 'Loc limit' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ConstraintDefinitionLocLimit extends ConstraintDefinition {
private static final int LIMIT_XMIN = 0x01;
private static final int LIMIT_XMAX = 0x02;
private static final int LIMIT_YMIN = 0x04;
private static final int LIMIT_YMAX = 0x08;
private static final int LIMIT_ZMIN = 0x10;
private static final int LIMIT_ZMAX = 0x20;
protected float[][] limits = new float[3][2];
public ConstraintDefinitionLocLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
if (blenderContext.getBlenderKey().isFixUpAxis()) {
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
limits[2][0] = -((Number) constraintData.getFieldValue("ymin")).floatValue();
limits[2][1] = -((Number) constraintData.getFieldValue("ymax")).floatValue();
limits[1][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
limits[1][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
// swapping Y and X limits flag in the bitwise flag
int ymin = flag & LIMIT_YMIN;
int ymax = flag & LIMIT_YMAX;
int zmin = flag & LIMIT_ZMIN;
int zmax = flag & LIMIT_ZMAX;
flag &= LIMIT_XMIN | LIMIT_XMAX;// clear the other flags to swap
// them
flag |= ymin << 2;
flag |= ymax << 2;
flag |= zmin >> 2;
flag |= zmax >> 2;
} else {
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
limits[1][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue();
limits[1][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue();
limits[2][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
limits[2][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
}
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
if (this.getOwner() instanceof Bone && ((Bone) this.getOwner()).getParent() != null &&
blenderContext.getBoneContext(ownerOMA).is(BoneContext.CONNECTED_TO_PARENT)) {
// location limit does not work on bones who are connected to their parent
return;
}
Transform ownerTransform = this.getOwnerTransform(ownerSpace);
Vector3f translation = ownerTransform.getTranslation();
if ((flag & LIMIT_XMIN) != 0 && translation.x < limits[0][0]) {
translation.x -= (translation.x - limits[0][0]) * influence;
}
if ((flag & LIMIT_XMAX) != 0 && translation.x > limits[0][1]) {
translation.x -= (translation.x - limits[0][1]) * influence;
}
if ((flag & LIMIT_YMIN) != 0 && translation.y < limits[1][0]) {
translation.y -= (translation.y - limits[1][0]) * influence;
}
if ((flag & LIMIT_YMAX) != 0 && translation.y > limits[1][1]) {
translation.y -= (translation.y - limits[1][1]) * influence;
}
if ((flag & LIMIT_ZMIN) != 0 && translation.z < limits[2][0]) {
translation.z -= (translation.z - limits[2][0]) * influence;
}
if ((flag & LIMIT_ZMAX) != 0 && translation.z > limits[2][1]) {
translation.z -= (translation.z - limits[2][1]) * influence;
}
this.applyOwnerTransform(ownerTransform, ownerSpace);
}
@Override
public String getConstraintTypeName() {
return "Limit location";
}
}

@ -0,0 +1,28 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class represents 'Null' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ConstraintDefinitionNull extends ConstraintDefinition {
public ConstraintDefinitionNull(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
// null constraint does nothing so no need to implement this one
}
@Override
public String getConstraintTypeName() {
return "Null";
}
}

@ -0,0 +1,78 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class represents 'Rot like' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ConstraintDefinitionRotLike extends ConstraintDefinition {
private static final int ROTLIKE_X = 0x01;
private static final int ROTLIKE_Y = 0x02;
private static final int ROTLIKE_Z = 0x04;
private static final int ROTLIKE_X_INVERT = 0x10;
private static final int ROTLIKE_Y_INVERT = 0x20;
private static final int ROTLIKE_Z_INVERT = 0x40;
private static final int ROTLIKE_OFFSET = 0x80;
private transient float[] ownerAngles = new float[3];
private transient float[] targetAngles = new float[3];
public ConstraintDefinitionRotLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
Transform ownerTransform = this.getOwnerTransform(ownerSpace);
Quaternion ownerRotation = ownerTransform.getRotation();
ownerAngles = ownerRotation.toAngles(ownerAngles);
targetAngles = targetTransform.getRotation().toAngles(targetAngles);
Quaternion startRotation = ownerRotation.clone();
Quaternion offset = Quaternion.IDENTITY;
if ((flag & ROTLIKE_OFFSET) != 0) {// we add the original rotation to
// the copied rotation
offset = startRotation;
}
if ((flag & ROTLIKE_X) != 0) {
ownerAngles[0] = targetAngles[0];
if ((flag & ROTLIKE_X_INVERT) != 0) {
ownerAngles[0] = -ownerAngles[0];
}
}
if ((flag & ROTLIKE_Y) != 0) {
ownerAngles[1] = targetAngles[1];
if ((flag & ROTLIKE_Y_INVERT) != 0) {
ownerAngles[1] = -ownerAngles[1];
}
}
if ((flag & ROTLIKE_Z) != 0) {
ownerAngles[2] = targetAngles[2];
if ((flag & ROTLIKE_Z_INVERT) != 0) {
ownerAngles[2] = -ownerAngles[2];
}
}
ownerRotation.fromAngles(ownerAngles).multLocal(offset);
if (influence < 1.0f) {
// startLocation.subtractLocal(ownerLocation).normalizeLocal().mult(influence);
// ownerLocation.addLocal(startLocation);
// TODO
}
this.applyOwnerTransform(ownerTransform, ownerSpace);
}
@Override
public String getConstraintTypeName() {
return "Copy rotation";
}
}

@ -0,0 +1,119 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.math.FastMath;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class represents 'Rot limit' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ConstraintDefinitionRotLimit extends ConstraintDefinition {
private static final int LIMIT_XROT = 0x01;
private static final int LIMIT_YROT = 0x02;
private static final int LIMIT_ZROT = 0x04;
private transient float[][] limits = new float[3][2];
private transient float[] angles = new float[3];
public ConstraintDefinitionRotLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
if (blenderContext.getBlenderKey().isFixUpAxis()) {
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
limits[2][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue();
limits[2][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue();
limits[1][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
limits[1][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
// swapping Y and X limits flag in the bitwise flag
int limitY = flag & LIMIT_YROT;
int limitZ = flag & LIMIT_ZROT;
flag &= LIMIT_XROT;// clear the other flags to swap them
flag |= limitY << 1;
flag |= limitZ >> 1;
} else {
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
limits[1][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue();
limits[1][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue();
limits[2][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
limits[2][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
}
// until blender 2.49 the rotations values were stored in degrees
if (blenderContext.getBlenderVersion() <= 249) {
for (int i = 0; i < 3; ++i) {
limits[i][0] *= FastMath.DEG_TO_RAD;
limits[i][1] *= FastMath.DEG_TO_RAD;
}
}
// make sure that the limits are always in range [0, 2PI)
// TODO: left it here because it is essential to make sure all cases
// work poperly
// but will do it a little bit later ;)
/*
* for (int i = 0; i < 3; ++i) { for (int j = 0; j < 2; ++j) { int
* multFactor = (int)Math.abs(limits[i][j] / FastMath.TWO_PI) ; if
* (limits[i][j] < 0) { limits[i][j] += FastMath.TWO_PI * (multFactor +
* 1); } else { limits[i][j] -= FastMath.TWO_PI * multFactor; } } //make
* sure the lower limit is not greater than the upper one
* if(limits[i][0] > limits[i][1]) { float temp = limits[i][0];
* limits[i][0] = limits[i][1]; limits[i][1] = temp; } }
*/
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
Transform ownerTransform = this.getOwnerTransform(ownerSpace);
ownerTransform.getRotation().toAngles(angles);
// make sure that the rotations are always in range [0, 2PI)
// TODO: same comment as in constructor
/*
* for (int i = 0; i < 3; ++i) { int multFactor =
* (int)Math.abs(angles[i] / FastMath.TWO_PI) ; if(angles[i] < 0) {
* angles[i] += FastMath.TWO_PI * (multFactor + 1); } else { angles[i]
* -= FastMath.TWO_PI * multFactor; } }
*/
if ((flag & LIMIT_XROT) != 0) {
float difference = 0.0f;
if (angles[0] < limits[0][0]) {
difference = (angles[0] - limits[0][0]) * influence;
} else if (angles[0] > limits[0][1]) {
difference = (angles[0] - limits[0][1]) * influence;
}
angles[0] -= difference;
}
if ((flag & LIMIT_YROT) != 0) {
float difference = 0.0f;
if (angles[1] < limits[1][0]) {
difference = (angles[1] - limits[1][0]) * influence;
} else if (angles[1] > limits[1][1]) {
difference = (angles[1] - limits[1][1]) * influence;
}
angles[1] -= difference;
}
if ((flag & LIMIT_ZROT) != 0) {
float difference = 0.0f;
if (angles[2] < limits[2][0]) {
difference = (angles[2] - limits[2][0]) * influence;
} else if (angles[2] > limits[2][1]) {
difference = (angles[2] - limits[2][1]) * influence;
}
angles[2] -= difference;
}
ownerTransform.getRotation().fromAngles(angles);
this.applyOwnerTransform(ownerTransform, ownerSpace);
}
@Override
public String getConstraintTypeName() {
return "Limit rotation";
}
}

@ -0,0 +1,64 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class represents 'Size like' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ConstraintDefinitionSizeLike extends ConstraintDefinition {
private static final int SIZELIKE_X = 0x01;
private static final int SIZELIKE_Y = 0x02;
private static final int SIZELIKE_Z = 0x04;
private static final int LOCLIKE_OFFSET = 0x80;
public ConstraintDefinitionSizeLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
if (blenderContext.getBlenderKey().isFixUpAxis()) {
// swapping Y and X limits flag in the bitwise flag
int y = flag & SIZELIKE_Y;
int z = flag & SIZELIKE_Z;
flag &= SIZELIKE_X | LOCLIKE_OFFSET;// clear the other flags to swap
// them
flag |= y << 1;
flag |= z >> 1;
}
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
Transform ownerTransform = this.getOwnerTransform(ownerSpace);
Vector3f ownerScale = ownerTransform.getScale();
Vector3f targetScale = targetTransform.getScale();
Vector3f offset = Vector3f.ZERO;
if ((flag & LOCLIKE_OFFSET) != 0) {// we add the original scale to the
// copied scale
offset = ownerScale.clone();
}
if ((flag & SIZELIKE_X) != 0) {
ownerScale.x = targetScale.x * influence + (1.0f - influence) * ownerScale.x;
}
if ((flag & SIZELIKE_Y) != 0) {
ownerScale.y = targetScale.y * influence + (1.0f - influence) * ownerScale.y;
}
if ((flag & SIZELIKE_Z) != 0) {
ownerScale.z = targetScale.z * influence + (1.0f - influence) * ownerScale.z;
}
ownerScale.addLocal(offset);
this.applyOwnerTransform(ownerTransform, ownerSpace);
}
@Override
public String getConstraintTypeName() {
return "Copy scale";
}
}

@ -0,0 +1,86 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class represents 'Size limit' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ConstraintDefinitionSizeLimit extends ConstraintDefinition {
private static final int LIMIT_XMIN = 0x01;
private static final int LIMIT_XMAX = 0x02;
private static final int LIMIT_YMIN = 0x04;
private static final int LIMIT_YMAX = 0x08;
private static final int LIMIT_ZMIN = 0x10;
private static final int LIMIT_ZMAX = 0x20;
protected transient float[][] limits = new float[3][2];
public ConstraintDefinitionSizeLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
if (blenderContext.getBlenderKey().isFixUpAxis()) {
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
limits[2][0] = -((Number) constraintData.getFieldValue("ymin")).floatValue();
limits[2][1] = -((Number) constraintData.getFieldValue("ymax")).floatValue();
limits[1][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
limits[1][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
// swapping Y and X limits flag in the bitwise flag
int ymin = flag & LIMIT_YMIN;
int ymax = flag & LIMIT_YMAX;
int zmin = flag & LIMIT_ZMIN;
int zmax = flag & LIMIT_ZMAX;
flag &= LIMIT_XMIN | LIMIT_XMAX;// clear the other flags to swap
// them
flag |= ymin << 2;
flag |= ymax << 2;
flag |= zmin >> 2;
flag |= zmax >> 2;
} else {
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue();
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue();
limits[1][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue();
limits[1][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue();
limits[2][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue();
limits[2][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue();
}
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
Transform ownerTransform = this.getOwnerTransform(ownerSpace);
Vector3f scale = ownerTransform.getScale();
if ((flag & LIMIT_XMIN) != 0 && scale.x < limits[0][0]) {
scale.x -= (scale.x - limits[0][0]) * influence;
}
if ((flag & LIMIT_XMAX) != 0 && scale.x > limits[0][1]) {
scale.x -= (scale.x - limits[0][1]) * influence;
}
if ((flag & LIMIT_YMIN) != 0 && scale.y < limits[1][0]) {
scale.y -= (scale.y - limits[1][0]) * influence;
}
if ((flag & LIMIT_YMAX) != 0 && scale.y > limits[1][1]) {
scale.y -= (scale.y - limits[1][1]) * influence;
}
if ((flag & LIMIT_ZMIN) != 0 && scale.z < limits[2][0]) {
scale.z -= (scale.z - limits[2][0]) * influence;
}
if ((flag & LIMIT_ZMAX) != 0 && scale.z > limits[2][1]) {
scale.z -= (scale.z - limits[2][1]) * influence;
}
this.applyOwnerTransform(ownerTransform, ownerSpace);
}
@Override
public String getConstraintTypeName() {
return "Limit scale";
}
}

@ -0,0 +1,34 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
/**
* This class represents a constraint that is defined by blender but not
* supported by either importer ot jme. It only wirtes down a warning when
* baking is called.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class UnsupportedConstraintDefinition extends ConstraintDefinition {
private String typeName;
public UnsupportedConstraintDefinition(String typeName) {
super(null, null, null);
this.typeName = typeName;
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
}
@Override
public boolean isImplemented() {
return false;
}
@Override
public String getConstraintTypeName() {
return typeName;
}
}

@ -0,0 +1,146 @@
package com.jme3.scene.plugins.blender.curves;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Structure;
import java.util.ArrayList;
import java.util.List;
/**
* A class that helps to calculate the bezier curves calues. It uses doubles for performing calculations to minimize
* floating point operations errors.
* @author Marcin Roguski (Kaelthas)
*/
public class BezierCurve {
public static final int X_VALUE = 0;
public static final int Y_VALUE = 1;
public static final int Z_VALUE = 2;
/**
* The type of the curve. Describes the data it modifies.
* Used in ipos calculations.
*/
private int type;
/** The dimension of the curve. */
private int dimension;
/** A table of the bezier points. */
private float[][][] bezierPoints;
/** Array that stores a radius for each bezier triple. */
private float[] radiuses;
@SuppressWarnings("unchecked")
public BezierCurve(final int type, final List<Structure> bezTriples, final int dimension) {
if (dimension != 2 && dimension != 3) {
throw new IllegalArgumentException("The dimension of the curve should be 2 or 3!");
}
this.type = type;
this.dimension = dimension;
// first index of the bezierPoints table has the length of triples amount
// the second index points to a table od three points of a bezier triple (handle, point, handle)
// the third index specifies the coordinates of the specific point in a bezier triple
bezierPoints = new float[bezTriples.size()][3][dimension];
radiuses = new float[bezTriples.size()];
int i = 0, j, k;
for (Structure bezTriple : bezTriples) {
DynamicArray<Number> vec = (DynamicArray<Number>) bezTriple.getFieldValue("vec");
for (j = 0; j < 3; ++j) {
for (k = 0; k < dimension; ++k) {
bezierPoints[i][j][k] = vec.get(j, k).floatValue();
}
}
radiuses[i++] = ((Number) bezTriple.getFieldValue("radius")).floatValue();
}
}
/**
* This method evaluates the data for the specified frame. The Y value is returned.
* @param frame
* the frame for which the value is being calculated
* @param valuePart
* this param specifies wheather we should return the X, Y or Z part of the result value; it should have
* one of the following values: X_VALUE - the X factor of the result Y_VALUE - the Y factor of the result
* Z_VALUE - the Z factor of the result
* @return the value of the curve
*/
public float evaluate(int frame, int valuePart) {
for (int i = 0; i < bezierPoints.length - 1; ++i) {
if (frame >= bezierPoints[i][1][0] && frame <= bezierPoints[i + 1][1][0]) {
float t = (frame - bezierPoints[i][1][0]) / (bezierPoints[i + 1][1][0] - bezierPoints[i][1][0]);
float oneMinusT = 1.0f - t;
float oneMinusT2 = oneMinusT * oneMinusT;
float t2 = t * t;
return bezierPoints[i][1][valuePart] * oneMinusT2 * oneMinusT + 3.0f * bezierPoints[i][2][valuePart] * t * oneMinusT2 + 3.0f * bezierPoints[i + 1][0][valuePart] * t2 * oneMinusT + bezierPoints[i + 1][1][valuePart] * t2 * t;
}
}
if (frame < bezierPoints[0][1][0]) {
return bezierPoints[0][1][1];
} else { // frame>bezierPoints[bezierPoints.length-1][1][0]
return bezierPoints[bezierPoints.length - 1][1][1];
}
}
/**
* This method returns the frame where last bezier triple center point of the bezier curve is located.
* @return the frame number of the last defined bezier triple point for the curve
*/
public int getLastFrame() {
return (int) bezierPoints[bezierPoints.length - 1][1][0];
}
/**
* This method returns the type of the bezier curve. The type describes the parameter that this curve modifies
* (ie. LocationX or rotationW of the feature).
* @return the type of the bezier curve
*/
public int getType() {
return type;
}
/**
* The method returns the radius for the required bezier triple.
*
* @param bezierTripleIndex
* index of the bezier triple
* @return radius of the required bezier triple
*/
public float getRadius(int bezierTripleIndex) {
return radiuses[bezierTripleIndex];
}
/**
* This method returns a list of control points for this curve.
* @return a list of control points for this curve.
*/
public List<Vector3f> getControlPoints() {
List<Vector3f> controlPoints = new ArrayList<Vector3f>(bezierPoints.length * 3);
for (int i = 0; i < bezierPoints.length; ++i) {
controlPoints.add(new Vector3f(bezierPoints[i][0][0], bezierPoints[i][0][1], bezierPoints[i][0][2]));
controlPoints.add(new Vector3f(bezierPoints[i][1][0], bezierPoints[i][1][1], bezierPoints[i][1][2]));
controlPoints.add(new Vector3f(bezierPoints[i][2][0], bezierPoints[i][2][1], bezierPoints[i][2][2]));
}
return controlPoints;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Bezier curve: ").append(type).append('\n');
for (int i = 0; i < bezierPoints.length; ++i) {
sb.append(this.toStringBezTriple(i)).append('\n');
}
return sb.toString();
}
/**
* This method converts the bezier triple of a specified index into text.
* @param tripleIndex
* index of the triple
* @return text representation of the triple
*/
private String toStringBezTriple(int tripleIndex) {
if (this.dimension == 2) {
return "[(" + bezierPoints[tripleIndex][0][0] + ", " + bezierPoints[tripleIndex][0][1] + ") (" + bezierPoints[tripleIndex][1][0] + ", " + bezierPoints[tripleIndex][1][1] + ") (" + bezierPoints[tripleIndex][2][0] + ", " + bezierPoints[tripleIndex][2][1] + ")]";
} else {
return "[(" + bezierPoints[tripleIndex][0][0] + ", " + bezierPoints[tripleIndex][0][1] + ", " + bezierPoints[tripleIndex][0][2] + ") (" + bezierPoints[tripleIndex][1][0] + ", " + bezierPoints[tripleIndex][1][1] + ", " + bezierPoints[tripleIndex][1][2] + ") (" + bezierPoints[tripleIndex][2][0] + ", " + bezierPoints[tripleIndex][2][1] + ", " + bezierPoints[tripleIndex][2][2] + ")]";
}
}
}

@ -0,0 +1,838 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.curves;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.logging.Logger;
import com.jme3.material.Material;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Spline;
import com.jme3.math.Spline.SplineType;
import com.jme3.math.Vector3f;
import com.jme3.math.Vector4f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.objects.Properties;
import com.jme3.scene.shape.Curve;
import com.jme3.scene.shape.Surface;
import com.jme3.util.BufferUtils;
/**
* A class that is used in mesh calculations.
*
* @author Marcin Roguski (Kaelthas)
*/
public class CurvesHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(CurvesHelper.class.getName());
/** Minimum basis U function degree for NURBS curves and surfaces. */
protected int minimumBasisUFunctionDegree = 4;
/** Minimum basis V function degree for NURBS curves and surfaces. */
protected int minimumBasisVFunctionDegree = 4;
/**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in
* different blender versions.
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public CurvesHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* This method converts given curve structure into a list of geometries representing the curve. The list is used here because on object
* can have several separate curves.
* @param curveStructure
* the curve structure
* @param blenderContext
* the blender context
* @return a list of geometries repreenting a single curve object
* @throws BlenderFileException
*/
public List<Geometry> toCurve(Structure curveStructure, BlenderContext blenderContext) throws BlenderFileException {
String name = curveStructure.getName();
int flag = ((Number) curveStructure.getFieldValue("flag")).intValue();
boolean is3D = (flag & 0x01) != 0;
boolean isFront = (flag & 0x02) != 0 && !is3D;
boolean isBack = (flag & 0x04) != 0 && !is3D;
if (isFront) {
LOGGER.warning("No front face in curve implemented yet!");// TODO: implement front face
}
if (isBack) {
LOGGER.warning("No back face in curve implemented yet!");// TODO: implement back face
}
// reading nurbs (and sorting them by material)
List<Structure> nurbStructures = ((Structure) curveStructure.getFieldValue("nurb")).evaluateListBase();
Map<Number, List<Structure>> nurbs = new HashMap<Number, List<Structure>>();
for (Structure nurb : nurbStructures) {
Number matNumber = (Number) nurb.getFieldValue("mat_nr");
List<Structure> nurbList = nurbs.get(matNumber);
if (nurbList == null) {
nurbList = new ArrayList<Structure>();
nurbs.put(matNumber, nurbList);
}
nurbList.add(nurb);
}
// getting materials
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
MaterialContext[] materialContexts = materialHelper.getMaterials(curveStructure, blenderContext);
Material defaultMaterial = null;
if (materialContexts != null) {
for (MaterialContext materialContext : materialContexts) {
materialContext.setFaceCullMode(FaceCullMode.Off);
}
} else {
defaultMaterial = blenderContext.getDefaultMaterial().clone();
defaultMaterial.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);
}
// getting or creating bevel object
List<Geometry> bevelObject = null;
Pointer pBevelObject = (Pointer) curveStructure.getFieldValue("bevobj");
if (pBevelObject.isNotNull()) {
Pointer pBevelStructure = (Pointer) pBevelObject.fetchData().get(0).getFieldValue("data");
Structure bevelStructure = pBevelStructure.fetchData().get(0);
bevelObject = this.toCurve(bevelStructure, blenderContext);
} else {
int bevResol = ((Number) curveStructure.getFieldValue("bevresol")).intValue();
float extrude = ((Number) curveStructure.getFieldValue("ext1")).floatValue();
float bevelDepth = ((Number) curveStructure.getFieldValue("ext2")).floatValue();
if (bevelDepth > 0.0f) {
float handlerLength = bevelDepth / 2.0f;
List<Vector3f> conrtolPoints = new ArrayList<Vector3f>(extrude > 0.0f ? 19 : 13);
if (extrude > 0.0f) {
conrtolPoints.add(new Vector3f(-bevelDepth, 0, extrude));
conrtolPoints.add(new Vector3f(-bevelDepth, 0, -handlerLength + extrude));
conrtolPoints.add(new Vector3f(-bevelDepth, 0, handlerLength - extrude));
}
conrtolPoints.add(new Vector3f(-bevelDepth, 0, -extrude));
conrtolPoints.add(new Vector3f(-bevelDepth, 0, -handlerLength - extrude));
conrtolPoints.add(new Vector3f(-handlerLength, 0, -bevelDepth - extrude));
conrtolPoints.add(new Vector3f(0, 0, -bevelDepth - extrude));
conrtolPoints.add(new Vector3f(handlerLength, 0, -bevelDepth - extrude));
if (extrude > 0.0f) {
conrtolPoints.add(new Vector3f(bevelDepth, 0, -extrude - handlerLength));
conrtolPoints.add(new Vector3f(bevelDepth, 0, -extrude));
conrtolPoints.add(new Vector3f(bevelDepth, 0, -extrude + handlerLength));
}
conrtolPoints.add(new Vector3f(bevelDepth, 0, extrude - handlerLength));
conrtolPoints.add(new Vector3f(bevelDepth, 0, extrude));
conrtolPoints.add(new Vector3f(bevelDepth, 0, extrude + handlerLength));
conrtolPoints.add(new Vector3f(handlerLength, 0, bevelDepth + extrude));
conrtolPoints.add(new Vector3f(0, 0, bevelDepth + extrude));
conrtolPoints.add(new Vector3f(-handlerLength, 0, bevelDepth + extrude));
conrtolPoints.add(new Vector3f(-bevelDepth, 0, handlerLength + extrude));
conrtolPoints.add(new Vector3f(-bevelDepth, 0, extrude));
Spline bevelSpline = new Spline(SplineType.Bezier, conrtolPoints, 0, false);
Curve bevelCurve = new Curve(bevelSpline, bevResol);
bevelObject = new ArrayList<Geometry>(1);
bevelObject.add(new Geometry("", bevelCurve));
} else if (extrude > 0.0f) {
Spline bevelSpline = new Spline(SplineType.Linear, new Vector3f[] { new Vector3f(0, 0, -extrude), new Vector3f(0, 0, extrude) }, 1, false);
Curve bevelCurve = new Curve(bevelSpline, bevResol);
bevelObject = new ArrayList<Geometry>(1);
bevelObject.add(new Geometry("", bevelCurve));
}
}
// getting taper object
Spline taperObject = null;
Pointer pTaperObject = (Pointer) curveStructure.getFieldValue("taperobj");
if (bevelObject != null && pTaperObject.isNotNull()) {
Pointer pTaperStructure = (Pointer) pTaperObject.fetchData().get(0).getFieldValue("data");
Structure taperStructure = pTaperStructure.fetchData().get(0);
taperObject = this.loadTaperObject(taperStructure);
}
Vector3f loc = this.getLoc(curveStructure);
// creating the result curves
List<Geometry> result = new ArrayList<Geometry>(nurbs.size());
for (Entry<Number, List<Structure>> nurbEntry : nurbs.entrySet()) {
for (Structure nurb : nurbEntry.getValue()) {
int type = ((Number) nurb.getFieldValue("type")).intValue();
List<Geometry> nurbGeoms = null;
if ((type & 0x01) != 0) {// Bezier curve
nurbGeoms = this.loadBezierCurve(loc, nurb, bevelObject, taperObject, blenderContext);
} else if ((type & 0x04) != 0) {// NURBS
nurbGeoms = this.loadNurb(loc, nurb, bevelObject, taperObject, blenderContext);
}
if (nurbGeoms != null) {// setting the name and assigning materials
for (Geometry nurbGeom : nurbGeoms) {
if (materialContexts != null) {
materialContexts[nurbEntry.getKey().intValue()].applyMaterial(nurbGeom, curveStructure.getOldMemoryAddress(), null, blenderContext);
} else {
nurbGeom.setMaterial(defaultMaterial);
}
nurbGeom.setName(name);
result.add(nurbGeom);
}
}
}
}
// reading custom properties
if (blenderContext.getBlenderKey().isLoadObjectProperties() && result.size() > 0) {
Properties properties = this.loadProperties(curveStructure, blenderContext);
// the loaded property is a group property, so we need to get each value and set it to every geometry of the curve
if (properties != null && properties.getValue() != null) {
for(Geometry geom : result) {
this.applyProperties(geom, properties);
}
}
}
return result;
}
/**
* This method loads the bezier curve.
* @param loc
* the translation of the curve
* @param nurb
* the nurb structure
* @param bevelObject
* the bevel object
* @param taperObject
* the taper object
* @param blenderContext
* the blender context
* @return a list of geometries representing the curves
* @throws BlenderFileException
* an exception is thrown when there are problems with the blender file
*/
protected List<Geometry> loadBezierCurve(Vector3f loc, Structure nurb, List<Geometry> bevelObject, Spline taperObject, BlenderContext blenderContext) throws BlenderFileException {
Pointer pBezierTriple = (Pointer) nurb.getFieldValue("bezt");
List<Geometry> result = new ArrayList<Geometry>();
if (pBezierTriple.isNotNull()) {
boolean smooth = (((Number) nurb.getFlatFieldValue("flag")).intValue() & 0x01) != 0;
int resolution = ((Number) nurb.getFieldValue("resolu")).intValue();
boolean cyclic = (((Number) nurb.getFieldValue("flagu")).intValue() & 0x01) != 0;
// creating the curve object
BezierCurve bezierCurve = new BezierCurve(0, pBezierTriple.fetchData(), 3);
List<Vector3f> controlPoints = bezierCurve.getControlPoints();
if (fixUpAxis) {
for (Vector3f v : controlPoints) {
float y = v.y;
v.y = v.z;
v.z = -y;
}
}
if (bevelObject != null && taperObject == null) {// create taper object using the scales of the bezier triple
int triplesCount = controlPoints.size() / 3;
List<Vector3f> taperControlPoints = new ArrayList<Vector3f>(triplesCount);
for (int i = 0; i < triplesCount; ++i) {
taperControlPoints.add(new Vector3f(controlPoints.get(i * 3 + 1).x, bezierCurve.getRadius(i), 0));
}
taperObject = new Spline(SplineType.Linear, taperControlPoints, 0, false);
}
if (cyclic) {
// copy the first three points at the end
for (int i = 0; i < 3; ++i) {
controlPoints.add(controlPoints.get(i));
}
}
// removing the first and last handles
controlPoints.remove(0);
controlPoints.remove(controlPoints.size() - 1);
// creating curve
Spline spline = new Spline(SplineType.Bezier, controlPoints, 0, false);
Curve curve = new Curve(spline, resolution);
if (bevelObject == null) {// creating a normal curve
Geometry curveGeometry = new Geometry(null, curve);
result.add(curveGeometry);
// TODO: use front and back flags; surface excluding algorithm for bezier circles should be added
} else {// creating curve with bevel and taper shape
result = this.applyBevelAndTaper(curve, bevelObject, taperObject, smooth, blenderContext);
}
}
return result;
}
/**
* This method loads the NURBS curve or surface.
* @param loc
* object's location
* @param nurb
* the NURBS data structure
* @param bevelObject
* the bevel object to be applied
* @param taperObject
* the taper object to be applied
* @param blenderContext
* the blender context
* @return a list of geometries that represents the loaded NURBS curve or surface
* @throws BlenderFileException
* an exception is throw when problems with blender loaded data occurs
*/
@SuppressWarnings("unchecked")
protected List<Geometry> loadNurb(Vector3f loc, Structure nurb, List<Geometry> bevelObject, Spline taperObject, BlenderContext blenderContext) throws BlenderFileException {
// loading the knots
List<Float>[] knots = new List[2];
Pointer[] pKnots = new Pointer[] { (Pointer) nurb.getFieldValue("knotsu"), (Pointer) nurb.getFieldValue("knotsv") };
for (int i = 0; i < knots.length; ++i) {
if (pKnots[i].isNotNull()) {
FileBlockHeader fileBlockHeader = blenderContext.getFileBlock(pKnots[i].getOldMemoryAddress());
BlenderInputStream blenderInputStream = blenderContext.getInputStream();
blenderInputStream.setPosition(fileBlockHeader.getBlockPosition());
int knotsAmount = fileBlockHeader.getCount() * fileBlockHeader.getSize() / 4;
knots[i] = new ArrayList<Float>(knotsAmount);
for (int j = 0; j < knotsAmount; ++j) {
knots[i].add(Float.valueOf(blenderInputStream.readFloat()));
}
}
}
// loading the flags and orders (basis functions degrees)
int flagU = ((Number) nurb.getFieldValue("flagu")).intValue();
int flagV = ((Number) nurb.getFieldValue("flagv")).intValue();
int orderU = ((Number) nurb.getFieldValue("orderu")).intValue();
int orderV = ((Number) nurb.getFieldValue("orderv")).intValue();
// loading control points and their weights
int pntsU = ((Number) nurb.getFieldValue("pntsu")).intValue();
int pntsV = ((Number) nurb.getFieldValue("pntsv")).intValue();
List<Structure> bPoints = ((Pointer) nurb.getFieldValue("bp")).fetchData();
List<List<Vector4f>> controlPoints = new ArrayList<List<Vector4f>>(pntsV);
for (int i = 0; i < pntsV; ++i) {
List<Vector4f> uControlPoints = new ArrayList<Vector4f>(pntsU);
for (int j = 0; j < pntsU; ++j) {
DynamicArray<Float> vec = (DynamicArray<Float>) bPoints.get(j + i * pntsU).getFieldValue("vec");
if (fixUpAxis) {
uControlPoints.add(new Vector4f(vec.get(0).floatValue(), vec.get(2).floatValue(), -vec.get(1).floatValue(), vec.get(3).floatValue()));
} else {
uControlPoints.add(new Vector4f(vec.get(0).floatValue(), vec.get(1).floatValue(), vec.get(2).floatValue(), vec.get(3).floatValue()));
}
}
if ((flagU & 0x01) != 0) {
for (int k = 0; k < orderU - 1; ++k) {
uControlPoints.add(uControlPoints.get(k));
}
}
controlPoints.add(uControlPoints);
}
if ((flagV & 0x01) != 0) {
for (int k = 0; k < orderV - 1; ++k) {
controlPoints.add(controlPoints.get(k));
}
}
int resolu = ((Number) nurb.getFieldValue("resolu")).intValue() + 1;
List<Geometry> result;
if (knots[1] == null) {// creating the curve
Spline nurbSpline = new Spline(controlPoints.get(0), knots[0]);
Curve nurbCurve = new Curve(nurbSpline, resolu);
if (bevelObject != null) {
result = this.applyBevelAndTaper(nurbCurve, bevelObject, taperObject, true, blenderContext);// TODO: smooth
} else {
result = new ArrayList<Geometry>(1);
Geometry nurbGeometry = new Geometry("", nurbCurve);
result.add(nurbGeometry);
}
} else {// creating the nurb surface
int resolv = ((Number) nurb.getFieldValue("resolv")).intValue() + 1;
Surface nurbSurface = Surface.createNurbsSurface(controlPoints, knots, resolu, resolv, orderU, orderV);
Geometry nurbGeometry = new Geometry("", nurbSurface);
result = new ArrayList<Geometry>(1);
result.add(nurbGeometry);
}
return result;
}
/**
* The method computes the taper scale on the given point on the curve.
*
* @param taper
* the taper object that defines the scale
* @param percent
* the percent of the 'road' along the curve
* @return scale on the pointed place along the curve
*/
protected float getTaperScale(Spline taper, float percent) {
if (taper == null) {
return 1;// return scale = 1 if no taper is applied
}
percent = FastMath.clamp(percent, 0, 1);
List<Float> segmentLengths = taper.getSegmentsLength();
float percentLength = taper.getTotalLength() * percent;
float partLength = 0;
int i;
for (i = 0; i < segmentLengths.size(); ++i) {
partLength += segmentLengths.get(i);
if (partLength > percentLength) {
partLength -= segmentLengths.get(i);
percentLength -= partLength;
percent = percentLength / segmentLengths.get(i);
break;
}
}
// do not cross the line :)
if (percent >= 1) {
percent = 1;
--i;
}
if (taper.getType() == SplineType.Bezier) {
i *= 3;
}
return taper.interpolate(percent, i, null).y;
}
/**
* This method applies bevel and taper objects to the curve.
* @param curve
* the curve we apply the objects to
* @param bevelObject
* the bevel object
* @param taperObject
* the taper object
* @param smooth
* the smooth flag
* @param blenderContext
* the blender context
* @return a list of geometries representing the beveled and/or tapered curve
*/
protected List<Geometry> applyBevelAndTaper(Curve curve, List<Geometry> bevelObject, Spline taperObject, boolean smooth, BlenderContext blenderContext) {
Vector3f[] curvePoints = BufferUtils.getVector3Array(curve.getFloatBuffer(Type.Position));
Vector3f subtractResult = new Vector3f();
float curveLength = curve.getLength();
FloatBuffer[] vertexBuffers = new FloatBuffer[bevelObject.size()];
FloatBuffer[] normalBuffers = new FloatBuffer[bevelObject.size()];
IndexBuffer[] indexBuffers = new IndexBuffer[bevelObject.size()];
for (int geomIndex = 0; geomIndex < bevelObject.size(); ++geomIndex) {
Mesh mesh = bevelObject.get(geomIndex).getMesh();
Vector3f[] positions = BufferUtils.getVector3Array(mesh.getFloatBuffer(Type.Position));
Vector3f[] bevelPoints = this.transformToFirstLineOfBevelPoints(positions, curvePoints[0], curvePoints[1]);
List<Vector3f[]> bevels = new ArrayList<Vector3f[]>(curvePoints.length);
bevels.add(bevelPoints);
vertexBuffers[geomIndex] = BufferUtils.createFloatBuffer(bevelPoints.length * 3 * curvePoints.length * (smooth ? 1 : 6));
for (int i = 1; i < curvePoints.length - 1; ++i) {
bevelPoints = this.transformBevel(bevelPoints, curvePoints[i - 1], curvePoints[i], curvePoints[i + 1]);
bevels.add(bevelPoints);
}
bevelPoints = this.transformBevel(bevelPoints, curvePoints[curvePoints.length - 2], curvePoints[curvePoints.length - 1], null);
bevels.add(bevelPoints);
if (bevels.size() > 2) {
// changing the first and last bevel so that they are parallel to their neighbours (blender works this way)
// notice this implicates that the distances of every corresponding point in th two bevels must be identical and
// equal to the distance between the points on curve that define the bevel position
// so instead doing complicated rotations on each point we will simply properly translate each of them
int[][] pointIndexes = new int[][] { { 0, 1 }, { curvePoints.length - 1, curvePoints.length - 2 } };
for (int[] indexes : pointIndexes) {
float distance = curvePoints[indexes[1]].subtract(curvePoints[indexes[0]], subtractResult).length();
Vector3f[] bevel = bevels.get(indexes[0]);
Vector3f[] nextBevel = bevels.get(indexes[1]);
for (int i = 0; i < bevel.length; ++i) {
float d = bevel[i].subtract(nextBevel[i], subtractResult).length();
subtractResult.normalizeLocal().multLocal(distance - d);
bevel[i].addLocal(subtractResult);
}
}
}
// apply scales to the bevels
float lengthAlongCurve = 0;
for (int i = 0; i < curvePoints.length; ++i) {
if (i > 0) {
lengthAlongCurve += curvePoints[i].subtract(curvePoints[i - 1], subtractResult).length();
}
float taperScale = this.getTaperScale(taperObject, i == 0 ? 0 : lengthAlongCurve / curveLength);
this.applyScale(bevels.get(i), curvePoints[i], taperScale);
}
if (smooth) {// add everything to the buffer
for (Vector3f[] bevel : bevels) {
for (Vector3f d : bevel) {
vertexBuffers[geomIndex].put(d.x);
vertexBuffers[geomIndex].put(d.y);
vertexBuffers[geomIndex].put(d.z);
}
}
} else {// add vertices to the buffer duplicating them so that every vertex belongs only to a single triangle
for (int i = 0; i < curvePoints.length - 1; ++i) {
for (int j = 0; j < bevelPoints.length - 1; ++j) {
// first triangle
vertexBuffers[geomIndex].put(bevels.get(i)[j].x);
vertexBuffers[geomIndex].put(bevels.get(i)[j].y);
vertexBuffers[geomIndex].put(bevels.get(i)[j].z);
vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].x);
vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].y);
vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].z);
vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].x);
vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].y);
vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].z);
// second triangle
vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].x);
vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].y);
vertexBuffers[geomIndex].put(bevels.get(i)[j + 1].z);
vertexBuffers[geomIndex].put(bevels.get(i + 1)[j + 1].x);
vertexBuffers[geomIndex].put(bevels.get(i + 1)[j + 1].y);
vertexBuffers[geomIndex].put(bevels.get(i + 1)[j + 1].z);
vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].x);
vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].y);
vertexBuffers[geomIndex].put(bevels.get(i + 1)[j].z);
}
}
}
indexBuffers[geomIndex] = this.generateIndexes(bevelPoints.length, curvePoints.length, smooth);
normalBuffers[geomIndex] = this.generateNormals(indexBuffers[geomIndex], vertexBuffers[geomIndex], smooth);
}
// creating and returning the result
List<Geometry> result = new ArrayList<Geometry>(vertexBuffers.length);
Float oneReferenceToCurveLength = new Float(curveLength);// its important for array modifier to use one reference here
for (int i = 0; i < vertexBuffers.length; ++i) {
Mesh mesh = new Mesh();
mesh.setBuffer(Type.Position, 3, vertexBuffers[i]);
if (indexBuffers[i].getBuffer() instanceof IntBuffer) {
mesh.setBuffer(Type.Index, 3, (IntBuffer) indexBuffers[i].getBuffer());
} else {
mesh.setBuffer(Type.Index, 3, (ShortBuffer) indexBuffers[i].getBuffer());
}
mesh.setBuffer(Type.Normal, 3, normalBuffers[i]);
Geometry g = new Geometry("g" + i, mesh);
g.setUserData("curveLength", oneReferenceToCurveLength);
g.updateModelBound();
result.add(g);
}
return result;
}
/**
* the method applies scale for the given bevel points. The points table is
* being modified so expect ypur result there.
*
* @param points
* the bevel points
* @param centerPoint
* the center point of the bevel
* @param scale
* the scale to be applied
*/
private void applyScale(Vector3f[] points, Vector3f centerPoint, float scale) {
Vector3f taperScaleVector = new Vector3f();
for (Vector3f p : points) {
taperScaleVector.set(centerPoint).subtractLocal(p).multLocal(1 - scale);
p.addLocal(taperScaleVector);
}
}
/**
* The method generates normal buffer for the created mesh of the curve.
*
* @param indexes
* the indexes of the mesh points
* @param points
* the mesh's points
* @param smooth
* the flag indicating if the result is to be smooth or solid
* @return normals buffer for the mesh
*/
private FloatBuffer generateNormals(IndexBuffer indexes, FloatBuffer points, boolean smooth) {
Map<Integer, Vector3f> normalMap = new TreeMap<Integer, Vector3f>();
Vector3f[] allVerts = BufferUtils.getVector3Array(points);
for (int i = 0; i < indexes.size(); i += 3) {
int index1 = indexes.get(i);
int index2 = indexes.get(i + 1);
int index3 = indexes.get(i + 2);
Vector3f n = FastMath.computeNormal(allVerts[index1], allVerts[index2], allVerts[index3]);
this.addNormal(n, normalMap, smooth, index1, index2, index3);
}
FloatBuffer normals = BufferUtils.createFloatBuffer(normalMap.size() * 3);
for (Entry<Integer, Vector3f> entry : normalMap.entrySet()) {
normals.put(entry.getValue().x);
normals.put(entry.getValue().y);
normals.put(entry.getValue().z);
}
return normals;
}
/**
* The amount of faces in the final mesh is the amount of edges in the bevel
* curve (which is less by 1 than its number of vertices) multiplied by 2
* (because each edge has two faces assigned on both sides) and multiplied
* by the amount of bevel curve repeats which is equal to the amount of
* vertices on the target curve finally we need to subtract the bevel edges
* amount 2 times because the border edges have only one face attached and
* at last multiply everything by 3 because each face needs 3 indexes to be
* described
*
* @param bevelShapeVertexCount
* amount of points in bevel shape
* @param bevelRepeats
* amount of bevel shapes along the curve
* @param smooth
* the smooth flag
* @return index buffer for the mesh
*/
private IndexBuffer generateIndexes(int bevelShapeVertexCount, int bevelRepeats, boolean smooth) {
int putIndex = 0;
if (smooth) {
int indexBufferSize = (bevelRepeats - 1) * (bevelShapeVertexCount - 1) * 6;
IndexBuffer result = IndexBuffer.createIndexBuffer(indexBufferSize, indexBufferSize);
for (int i = 0; i < bevelRepeats - 1; ++i) {
for (int j = 0; j < bevelShapeVertexCount - 1; ++j) {
result.put(putIndex++, i * bevelShapeVertexCount + j);
result.put(putIndex++, i * bevelShapeVertexCount + j + 1);
result.put(putIndex++, (i + 1) * bevelShapeVertexCount + j);
result.put(putIndex++, i * bevelShapeVertexCount + j + 1);
result.put(putIndex++, (i + 1) * bevelShapeVertexCount + j + 1);
result.put(putIndex++, (i + 1) * bevelShapeVertexCount + j);
}
}
return result;
} else {
// every pair of bevel vertices belongs to two triangles
// we have the same amount of pairs as the amount of vertices in bevel
// so the amount of triangles is: bevelShapeVertexCount * 2 * (bevelRepeats - 1)
// and this gives the amount of vertices in non smooth shape as below ...
int indexBufferSize = bevelShapeVertexCount * bevelRepeats * 6;// 6 = 2 * 3 where 2 is stated above and 3 is the count of vertices for each triangle
IndexBuffer result = IndexBuffer.createIndexBuffer(indexBufferSize, indexBufferSize);
for (int i = 0; i < indexBufferSize; ++i) {
result.put(putIndex++, i);
}
return result;
}
}
/**
* The method transforms the bevel along the curve.
*
* @param bevel
* the bevel to be transformed
* @param prevPos
* previous curve point
* @param currPos
* current curve point (here the center of the new bevel will be
* set)
* @param nextPos
* next curve point
* @return points of transformed bevel
*/
private Vector3f[] transformBevel(Vector3f[] bevel, Vector3f prevPos, Vector3f currPos, Vector3f nextPos) {
bevel = bevel.clone();
// currPos and directionVector define the line in 3D space
Vector3f directionVector = prevPos != null ? currPos.subtract(prevPos) : nextPos.subtract(currPos);
directionVector.normalizeLocal();
// plane is described by equation: Ax + By + Cz + D = 0 where planeNormal = [A, B, C] and D = -(Ax + By + Cz)
Vector3f planeNormal = null;
if (prevPos != null) {
planeNormal = currPos.subtract(prevPos).normalizeLocal();
if (nextPos != null) {
planeNormal.addLocal(nextPos.subtract(currPos).normalizeLocal()).normalizeLocal();
}
} else {
planeNormal = nextPos.subtract(currPos).normalizeLocal();
}
float D = -planeNormal.dot(currPos);// D = -(Ax + By + Cz)
// now we need to compute paralell cast of each bevel point on the plane, the leading line is already known
// parametric equation of a line: x = px + vx * t; y = py + vy * t; z = pz + vz * t
// where p = currPos and v = directionVector
// using x, y and z in plane equation we get value of 't' that will allow us to compute the point where plane and line cross
float temp = planeNormal.dot(directionVector);
for (int i = 0; i < bevel.length; ++i) {
float t = -(planeNormal.dot(bevel[i]) + D) / temp;
if (fixUpAxis) {
bevel[i] = new Vector3f(bevel[i].x + directionVector.x * t, bevel[i].y + directionVector.y * t, bevel[i].z + directionVector.z * t);
} else {
bevel[i] = new Vector3f(bevel[i].x + directionVector.x * t, -bevel[i].z + directionVector.z * t, bevel[i].y + directionVector.y * t);
}
}
return bevel;
}
/**
* This method transforms the first line of the bevel points positioning it
* on the first point of the curve.
*
* @param startingLinePoints
* the vbevel shape points
* @param firstCurvePoint
* the first curve's point
* @param secondCurvePoint
* the second curve's point
* @return points of transformed bevel
*/
private Vector3f[] transformToFirstLineOfBevelPoints(Vector3f[] startingLinePoints, Vector3f firstCurvePoint, Vector3f secondCurvePoint) {
Vector3f planeNormal = secondCurvePoint.subtract(firstCurvePoint).normalizeLocal();
float angle = FastMath.acos(planeNormal.dot(Vector3f.UNIT_Y));
planeNormal.crossLocal(Vector3f.UNIT_Y).normalizeLocal();// planeNormal is the rotation axis now
Quaternion pointRotation = new Quaternion();
pointRotation.fromAngleAxis(angle, planeNormal);
Matrix4f m = new Matrix4f();
m.setRotationQuaternion(pointRotation);
m.setTranslation(firstCurvePoint);
float[] temp = new float[] { 0, 0, 0, 1 };
Vector3f[] verts = new Vector3f[startingLinePoints.length];
for (int j = 0; j < verts.length; ++j) {
temp[0] = startingLinePoints[j].x;
temp[1] = startingLinePoints[j].y;
temp[2] = startingLinePoints[j].z;
temp = m.mult(temp);// the result is stored in the array
if (fixUpAxis) {
verts[j] = new Vector3f(temp[0], -temp[2], temp[1]);
} else {
verts[j] = new Vector3f(temp[0], temp[1], temp[2]);
}
}
return verts;
}
/**
* The method adds a normal to the given map. Depending in the smooth factor
* it is either merged with the revious normal or not.
*
* @param normalToAdd
* the normal vector to be added
* @param normalMap
* the normal map where we add vectors
* @param smooth
* the smooth flag
* @param indexes
* the indexes of the normals
*/
private void addNormal(Vector3f normalToAdd, Map<Integer, Vector3f> normalMap, boolean smooth, int... indexes) {
for (int index : indexes) {
Vector3f n = normalMap.get(index);
if (!smooth || n == null) {
normalMap.put(index, normalToAdd.clone());
} else {
n.addLocal(normalToAdd).normalizeLocal();
}
}
}
/**
* This method loads the taper object.
*
* @param taperStructure
* the taper structure
* @return the taper object
* @throws BlenderFileException
*/
protected Spline loadTaperObject(Structure taperStructure) throws BlenderFileException {
// reading nurbs
List<Structure> nurbStructures = ((Structure) taperStructure.getFieldValue("nurb")).evaluateListBase();
for (Structure nurb : nurbStructures) {
Pointer pBezierTriple = (Pointer) nurb.getFieldValue("bezt");
if (pBezierTriple.isNotNull()) {
// creating the curve object
BezierCurve bezierCurve = new BezierCurve(0, pBezierTriple.fetchData(), 3);
List<Vector3f> controlPoints = bezierCurve.getControlPoints();
// removing the first and last handles
controlPoints.remove(0);
controlPoints.remove(controlPoints.size() - 1);
// return the first taper curve that has more than 3 control points
if (controlPoints.size() > 3) {
return new Spline(SplineType.Bezier, controlPoints, 0, false);
}
}
}
return null;
}
/**
* This method returns the translation of the curve. The UP axis is taken
* into account here.
*
* @param curveStructure
* the curve structure
* @return curve translation
*/
@SuppressWarnings("unchecked")
protected Vector3f getLoc(Structure curveStructure) {
DynamicArray<Number> locArray = (DynamicArray<Number>) curveStructure.getFieldValue("loc");
if (fixUpAxis) {
return new Vector3f(locArray.get(0).floatValue(), locArray.get(1).floatValue(), -locArray.get(2).floatValue());
} else {
return new Vector3f(locArray.get(0).floatValue(), locArray.get(2).floatValue(), locArray.get(1).floatValue());
}
}
}

@ -0,0 +1,77 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.file;
/**
* This exception is thrown when blend file data is somehow invalid.
* @author Marcin Roguski
*/
public class BlenderFileException extends Exception {
private static final long serialVersionUID = 7573482836437866767L;
/**
* Constructor. Creates an exception with no description.
*/
public BlenderFileException() {
// this constructor has no message
}
/**
* Constructor. Creates an exception containing the given message.
* @param message
* the message describing the problem that occured
*/
public BlenderFileException(String message) {
super(message);
}
/**
* Constructor. Creates an exception that is based upon other thrown object. It contains the whole stacktrace then.
* @param throwable
* an exception/error that occured
*/
public BlenderFileException(Throwable throwable) {
super(throwable);
}
/**
* Constructor. Creates an exception with both a message and stacktrace.
* @param message
* the message describing the problem that occured
* @param throwable
* an exception/error that occured
*/
public BlenderFileException(String message, Throwable throwable) {
super(message, throwable);
}
}

@ -0,0 +1,371 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.file;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
/**
* An input stream with random access to data.
* @author Marcin Roguski
*/
public class BlenderInputStream extends InputStream {
private static final Logger LOGGER = Logger.getLogger(BlenderInputStream.class.getName());
/** The default size of the blender buffer. */
private static final int DEFAULT_BUFFER_SIZE = 1048576; // 1MB
/**
* Size of a pointer; all pointers in the file are stored in this format. '_' means 4 bytes and '-' means 8 bytes.
*/
private int pointerSize;
/**
* Type of byte ordering used; 'v' means little endian and 'V' means big endian.
*/
private char endianess;
/** Version of Blender the file was created in; '248' means version 2.48. */
private String versionNumber;
/** The buffer we store the read data to. */
protected byte[] cachedBuffer;
/** The total size of the stored data. */
protected int size;
/** The current position of the read cursor. */
protected int position;
/**
* Constructor. The input stream is stored and used to read data.
* @param inputStream
* the stream we read data from
* @throws BlenderFileException
* this exception is thrown if the file header has some invalid data
*/
public BlenderInputStream(InputStream inputStream) throws BlenderFileException {
// the size value will canche while reading the file; the available() method cannot be counted on
try {
size = inputStream.available();
} catch (IOException e) {
size = 0;
}
if (size <= 0) {
size = BlenderInputStream.DEFAULT_BUFFER_SIZE;
}
// buffered input stream is used here for much faster file reading
BufferedInputStream bufferedInputStream;
if (inputStream instanceof BufferedInputStream) {
bufferedInputStream = (BufferedInputStream) inputStream;
} else {
bufferedInputStream = new BufferedInputStream(inputStream);
}
try {
this.readStreamToCache(bufferedInputStream);
} catch (IOException e) {
throw new BlenderFileException("Problems occured while caching the file!", e);
} finally {
try {
inputStream.close();
} catch (IOException e) {
LOGGER.warning("Unable to close stream with blender file.");
}
}
try {
this.readFileHeader();
} catch (BlenderFileException e) {// the file might be packed, don't panic, try one more time ;)
this.decompressFile();
position = 0;
this.readFileHeader();
}
}
/**
* This method reads the whole stream into a buffer.
* @param inputStream
* the stream to read the file data from
* @throws IOException
* an exception is thrown when data read from the stream is invalid or there are problems with i/o
* operations
*/
private void readStreamToCache(InputStream inputStream) throws IOException {
int data = inputStream.read();
cachedBuffer = new byte[size];
size = 0;// this will count the actual size
while (data != -1) {
if (size >= cachedBuffer.length) {// widen the cached array
byte[] newBuffer = new byte[cachedBuffer.length + (cachedBuffer.length >> 1)];
System.arraycopy(cachedBuffer, 0, newBuffer, 0, cachedBuffer.length);
cachedBuffer = newBuffer;
}
cachedBuffer[size++] = (byte) data;
data = inputStream.read();
}
}
/**
* This method is used when the blender file is gzipped. It decompresses the data and stores it back into the
* cachedBuffer field.
*/
private void decompressFile() {
GZIPInputStream gis = null;
try {
gis = new GZIPInputStream(new ByteArrayInputStream(cachedBuffer));
this.readStreamToCache(gis);
} catch (IOException e) {
throw new IllegalStateException("IO errors occured where they should NOT! " + "The data is already buffered at this point!", e);
} finally {
try {
if (gis != null) {
gis.close();
}
} catch (IOException e) {
LOGGER.warning(e.getMessage());
}
}
}
/**
* This method loads the header from the given stream during instance creation.
* @param inputStream
* the stream we read the header from
* @throws BlenderFileException
* this exception is thrown if the file header has some invalid data
*/
private void readFileHeader() throws BlenderFileException {
byte[] identifier = new byte[7];
int bytesRead = this.readBytes(identifier);
if (bytesRead != 7) {
throw new BlenderFileException("Error reading header identifier. Only " + bytesRead + " bytes read and there should be 7!");
}
String strIdentifier = new String(identifier);
if (!"BLENDER".equals(strIdentifier)) {
throw new BlenderFileException("Wrong file identifier: " + strIdentifier + "! Should be 'BLENDER'!");
}
char pointerSizeSign = (char) this.readByte();
if (pointerSizeSign == '-') {
pointerSize = 8;
} else if (pointerSizeSign == '_') {
pointerSize = 4;
} else {
throw new BlenderFileException("Invalid pointer size character! Should be '_' or '-' and there is: " + pointerSizeSign);
}
endianess = (char) this.readByte();
if (endianess != 'v' && endianess != 'V') {
throw new BlenderFileException("Unknown endianess value! 'v' or 'V' expected and found: " + endianess);
}
byte[] versionNumber = new byte[3];
bytesRead = this.readBytes(versionNumber);
if (bytesRead != 3) {
throw new BlenderFileException("Error reading version numberr. Only " + bytesRead + " bytes read and there should be 3!");
}
this.versionNumber = new String(versionNumber);
}
@Override
public int read() throws IOException {
return this.readByte();
}
/**
* This method reads 1 byte from the stream.
* It works just in the way the read method does.
* It just not throw an exception because at this moment the whole file
* is loaded into buffer, so no need for IOException to be thrown.
* @return a byte from the stream (1 bytes read)
*/
public int readByte() {
return cachedBuffer[position++] & 0xFF;
}
/**
* This method reads a bytes number big enough to fill the table.
* It does not throw exceptions so it is for internal use only.
* @param bytes
* an array to be filled with data
* @return number of read bytes (a length of array actually)
*/
private int readBytes(byte[] bytes) {
for (int i = 0; i < bytes.length; ++i) {
bytes[i] = (byte) this.readByte();
}
return bytes.length;
}
/**
* This method reads 2-byte number from the stream.
* @return a number from the stream (2 bytes read)
*/
public int readShort() {
int part1 = this.readByte();
int part2 = this.readByte();
if (endianess == 'v') {
return (part2 << 8) + part1;
} else {
return (part1 << 8) + part2;
}
}
/**
* This method reads 4-byte number from the stream.
* @return a number from the stream (4 bytes read)
*/
public int readInt() {
int part1 = this.readByte();
int part2 = this.readByte();
int part3 = this.readByte();
int part4 = this.readByte();
if (endianess == 'v') {
return (part4 << 24) + (part3 << 16) + (part2 << 8) + part1;
} else {
return (part1 << 24) + (part2 << 16) + (part3 << 8) + part4;
}
}
/**
* This method reads 4-byte floating point number (float) from the stream.
* @return a number from the stream (4 bytes read)
*/
public float readFloat() {
int intValue = this.readInt();
return Float.intBitsToFloat(intValue);
}
/**
* This method reads 8-byte number from the stream.
* @return a number from the stream (8 bytes read)
*/
public long readLong() {
long part1 = this.readInt();
long part2 = this.readInt();
long result = -1;
if (endianess == 'v') {
result = part2 << 32 | part1;
} else {
result = part1 << 32 | part2;
}
return result;
}
/**
* This method reads 8-byte floating point number (double) from the stream.
* @return a number from the stream (8 bytes read)
*/
public double readDouble() {
long longValue = this.readLong();
return Double.longBitsToDouble(longValue);
}
/**
* This method reads the pointer value. Depending on the pointer size defined in the header, the stream reads either
* 4 or 8 bytes of data.
* @return the pointer value
*/
public long readPointer() {
if (pointerSize == 4) {
return this.readInt();
}
return this.readLong();
}
/**
* This method reads the string. It assumes the string is terminated with zero in the stream.
* @return the string read from the stream
*/
public String readString() {
StringBuilder stringBuilder = new StringBuilder();
int data = this.readByte();
while (data != 0) {
stringBuilder.append((char) data);
data = this.readByte();
}
return stringBuilder.toString();
}
/**
* This method sets the current position of the read cursor.
* @param position
* the position of the read cursor
*/
public void setPosition(int position) {
this.position = position;
}
/**
* This method returns the position of the read cursor.
* @return the position of the read cursor
*/
public int getPosition() {
return position;
}
/**
* This method returns the blender version number where the file was created.
* @return blender version number
*/
public String getVersionNumber() {
return versionNumber;
}
/**
* This method returns the size of the pointer.
* @return the size of the pointer
*/
public int getPointerSize() {
return pointerSize;
}
/**
* This method aligns cursor position forward to a given amount of bytes.
* @param bytesAmount
* the byte amount to which we aligh the cursor
*/
public void alignPosition(int bytesAmount) {
if (bytesAmount <= 0) {
throw new IllegalArgumentException("Alignment byte number shoulf be positivbe!");
}
long move = position % bytesAmount;
if (move > 0) {
position += bytesAmount - move;
}
}
@Override
public void close() throws IOException {
// this method is unimplemented because some loaders (ie. TGALoader) tend close the stream given from the outside
// because the images can be stored directly in the blender file then this stream is properly positioned and given to the loader
// to read the image file, that is why we do not want it to be closed before the reading is done
// and anyway this stream is only a cached buffer, so it does not hold any open connection to anything
}
}

@ -0,0 +1,203 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.file;
import com.jme3.scene.plugins.blender.BlenderContext;
import java.util.HashMap;
import java.util.Map;
/**
* The data block containing the description of the file.
* @author Marcin Roguski (Kaelthas)
*/
public class DnaBlockData {
private static final int SDNA_ID = 'S' << 24 | 'D' << 16 | 'N' << 8 | 'A'; // SDNA
private static final int NAME_ID = 'N' << 24 | 'A' << 16 | 'M' << 8 | 'E'; // NAME
private static final int TYPE_ID = 'T' << 24 | 'Y' << 16 | 'P' << 8 | 'E'; // TYPE
private static final int TLEN_ID = 'T' << 24 | 'L' << 16 | 'E' << 8 | 'N'; // TLEN
private static final int STRC_ID = 'S' << 24 | 'T' << 16 | 'R' << 8 | 'C'; // STRC
/** Structures available inside the file. */
private final Structure[] structures;
/** A map that helps finding a structure by type. */
private final Map<String, Structure> structuresMap;
/**
* Constructor. Loads the block from the given stream during instance creation.
* @param inputStream
* the stream we read the block from
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is throw if the blend file is invalid or somehow corrupted
*/
public DnaBlockData(BlenderInputStream inputStream, BlenderContext blenderContext) throws BlenderFileException {
int identifier;
// reading 'SDNA' identifier
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
if (identifier != SDNA_ID) {
throw new BlenderFileException("Invalid identifier! '" + this.toString(SDNA_ID) + "' expected and found: " + this.toString(identifier));
}
// reading names
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
if (identifier != NAME_ID) {
throw new BlenderFileException("Invalid identifier! '" + this.toString(NAME_ID) + "' expected and found: " + this.toString(identifier));
}
int amount = inputStream.readInt();
if (amount <= 0) {
throw new BlenderFileException("The names amount number should be positive!");
}
String[] names = new String[amount];
for (int i = 0; i < amount; ++i) {
names[i] = inputStream.readString();
}
// reding types
inputStream.alignPosition(4);
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
if (identifier != TYPE_ID) {
throw new BlenderFileException("Invalid identifier! '" + this.toString(TYPE_ID) + "' expected and found: " + this.toString(identifier));
}
amount = inputStream.readInt();
if (amount <= 0) {
throw new BlenderFileException("The types amount number should be positive!");
}
String[] types = new String[amount];
for (int i = 0; i < amount; ++i) {
types[i] = inputStream.readString();
}
// reading lengths
inputStream.alignPosition(4);
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
if (identifier != TLEN_ID) {
throw new BlenderFileException("Invalid identifier! '" + this.toString(TLEN_ID) + "' expected and found: " + this.toString(identifier));
}
int[] lengths = new int[amount];// theamount is the same as int types
for (int i = 0; i < amount; ++i) {
lengths[i] = inputStream.readShort();
}
// reading structures
inputStream.alignPosition(4);
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
if (identifier != STRC_ID) {
throw new BlenderFileException("Invalid identifier! '" + this.toString(STRC_ID) + "' expected and found: " + this.toString(identifier));
}
amount = inputStream.readInt();
if (amount <= 0) {
throw new BlenderFileException("The structures amount number should be positive!");
}
structures = new Structure[amount];
structuresMap = new HashMap<String, Structure>(amount);
for (int i = 0; i < amount; ++i) {
structures[i] = new Structure(inputStream, names, types, blenderContext);
if (structuresMap.containsKey(structures[i].getType())) {
throw new BlenderFileException("Blend file seems to be corrupted! The type " + structures[i].getType() + " is defined twice!");
}
structuresMap.put(structures[i].getType(), structures[i]);
}
}
/**
* This method returns the amount of the structures.
* @return the amount of the structures
*/
public int getStructuresCount() {
return structures.length;
}
/**
* This method returns the structure of the given index.
* @param index
* the index of the structure
* @return the structure of the given index
*/
public Structure getStructure(int index) {
try {
return (Structure) structures[index].clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException("Structure should be clonable!!!", e);
}
}
/**
* This method returns a structure of the given name. If the name does not exists then null is returned.
* @param name
* the name of the structure
* @return the required structure or null if the given name is inapropriate
*/
public Structure getStructure(String name) {
try {
return (Structure) structuresMap.get(name).clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
/**
* This method indicates if the structure of the given name exists.
* @param name
* the name of the structure
* @return true if the structure exists and false otherwise
*/
public boolean hasStructure(String name) {
return structuresMap.containsKey(name);
}
/**
* This method converts the given identifier code to string.
* @param code
* the code taht is to be converted
* @return the string value of the identifier
*/
private String toString(int code) {
char c1 = (char) ((code & 0xFF000000) >> 24);
char c2 = (char) ((code & 0xFF0000) >> 16);
char c3 = (char) ((code & 0xFF00) >> 8);
char c4 = (char) (code & 0xFF);
return String.valueOf(c1) + c2 + c3 + c4;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("=============== ").append(SDNA_ID).append('\n');
for (Structure structure : structures) {
stringBuilder.append(structure.toString()).append('\n');
}
return stringBuilder.append("===============").toString();
}
}

@ -0,0 +1,135 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.file;
/**
* An array that can be dynamically modified/
* @author Marcin Roguski
* @param <T>
* the type of stored data in the array
*/
public class DynamicArray<T> implements Cloneable {
/** An array object that holds the required data. */
private T[] array;
/**
* This table holds the sizes of dimetions of the dynamic table. It's length specifies the table dimension or a
* pointer level. For example: if tableSizes.length == 3 then it either specifies a dynamic table of fixed lengths:
* dynTable[a][b][c], where a,b,c are stored in the tableSizes table.
*/
private int[] tableSizes;
/**
* Constructor. Builds an empty array of the specified sizes.
* @param tableSizes
* the sizes of the table
* @throws IllegalArgumentException
* an exception is thrown if one of the sizes is not a positive number
*/
public DynamicArray(int[] tableSizes, T[] data) {
this.tableSizes = tableSizes;
int totalSize = 1;
for (int size : tableSizes) {
if (size <= 0) {
throw new IllegalArgumentException("The size of the table must be positive!");
}
totalSize *= size;
}
if (totalSize != data.length) {
throw new IllegalArgumentException("The size of the table does not match the size of the given data!");
}
this.array = data;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
/**
* This method returns a value on the specified position. The dimension of the table is not taken into
* consideration.
* @param position
* the position of the data
* @return required data
*/
public T get(int position) {
return array[position];
}
/**
* This method returns a value on the specified position in multidimensional array. Be careful not to exceed the
* table boundaries. Check the table's dimension first.
* @param position
* the position of the data indices of data position
* @return required data required data
*/
public T get(int... position) {
if (position.length != tableSizes.length) {
throw new ArrayIndexOutOfBoundsException("The table accepts " + tableSizes.length + " indexing number(s)!");
}
int index = 0;
for (int i = 0; i < position.length - 1; ++i) {
index += position[i] * tableSizes[i + 1];
}
index += position[position.length - 1];
return array[index];
}
/**
* This method returns the total amount of data stored in the array.
* @return the total amount of data stored in the array
*/
public int getTotalSize() {
return array.length;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
if (array instanceof Character[]) {// in case of character array we convert it to String
for (int i = 0; i < array.length && (Character) array[i] != '\0'; ++i) {// strings are terminater with '0'
result.append(array[i]);
}
} else {
result.append('[');
for (int i = 0; i < array.length; ++i) {
result.append(array[i].toString());
if (i + 1 < array.length) {
result.append(',');
}
}
result.append(']');
}
return result.toString();
}
}

@ -0,0 +1,327 @@
package com.jme3.scene.plugins.blender.file;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.Structure.DataType;
import java.util.ArrayList;
import java.util.List;
/**
* This class represents a single field in the structure. It can be either a primitive type or a table or a reference to
* another structure.
* @author Marcin Roguski
*/
/* package */
class Field implements Cloneable {
private static final int NAME_LENGTH = 24;
private static final int TYPE_LENGTH = 16;
/** The blender context. */
public BlenderContext blenderContext;
/** The type of the field. */
public String type;
/** The name of the field. */
public String name;
/** The value of the field. Filled during data reading. */
public Object value;
/** This variable indicates the level of the pointer. */
public int pointerLevel;
/**
* This variable determines the sizes of the array. If the value is null the n the field is not an array.
*/
public int[] tableSizes;
/** This variable indicates if the field is a function pointer. */
public boolean function;
/**
* Constructor. Saves the field data and parses its name.
* @param name
* the name of the field
* @param type
* the type of the field
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown if the names contain errors
*/
public Field(String name, String type, BlenderContext blenderContext) throws BlenderFileException {
this.type = type;
this.blenderContext = blenderContext;
this.parseField(new StringBuilder(name));
}
/**
* Copy constructor. Used in clone method. Copying is not full. The value in the new object is not set so that we
* have a clead empty copy of the filed to fill with data.
* @param field
* the object that we copy
*/
private Field(Field field) {
type = field.type;
name = field.name;
blenderContext = field.blenderContext;
pointerLevel = field.pointerLevel;
if (field.tableSizes != null) {
tableSizes = field.tableSizes.clone();
}
function = field.function;
}
@Override
public Object clone() throws CloneNotSupportedException {
return new Field(this);
}
/**
* This method fills the field wth data read from the input stream.
* @param blenderInputStream
* the stream we read data from
* @throws BlenderFileException
* an exception is thrown when the blend file is somehow invalid or corrupted
*/
public void fill(BlenderInputStream blenderInputStream) throws BlenderFileException {
int dataToRead = 1;
if (tableSizes != null && tableSizes.length > 0) {
for (int size : tableSizes) {
if (size <= 0) {
throw new BlenderFileException("The field " + name + " has invalid table size: " + size);
}
dataToRead *= size;
}
}
DataType dataType = pointerLevel == 0 ? DataType.getDataType(type, blenderContext) : DataType.POINTER;
switch (dataType) {
case POINTER:
if (dataToRead == 1) {
Pointer pointer = new Pointer(pointerLevel, function, blenderContext);
pointer.fill(blenderInputStream);
value = pointer;
} else {
Pointer[] data = new Pointer[dataToRead];
for (int i = 0; i < dataToRead; ++i) {
Pointer pointer = new Pointer(pointerLevel, function, blenderContext);
pointer.fill(blenderInputStream);
data[i] = pointer;
}
value = new DynamicArray<Pointer>(tableSizes, data);
}
break;
case CHARACTER:
// character is also stored as a number, because sometimes the new blender version uses
// other number type instead of character as a field type
// and characters are very often used as byte number stores instead of real chars
if (dataToRead == 1) {
value = Byte.valueOf((byte) blenderInputStream.readByte());
} else {
Character[] data = new Character[dataToRead];
for (int i = 0; i < dataToRead; ++i) {
data[i] = Character.valueOf((char) blenderInputStream.readByte());
}
value = new DynamicArray<Character>(tableSizes, data);
}
break;
case SHORT:
if (dataToRead == 1) {
value = Integer.valueOf(blenderInputStream.readShort());
} else {
Number[] data = new Number[dataToRead];
for (int i = 0; i < dataToRead; ++i) {
data[i] = Integer.valueOf(blenderInputStream.readShort());
}
value = new DynamicArray<Number>(tableSizes, data);
}
break;
case INTEGER:
if (dataToRead == 1) {
value = Integer.valueOf(blenderInputStream.readInt());
} else {
Number[] data = new Number[dataToRead];
for (int i = 0; i < dataToRead; ++i) {
data[i] = Integer.valueOf(blenderInputStream.readInt());
}
value = new DynamicArray<Number>(tableSizes, data);
}
break;
case LONG:
if (dataToRead == 1) {
value = Long.valueOf(blenderInputStream.readLong());
} else {
Number[] data = new Number[dataToRead];
for (int i = 0; i < dataToRead; ++i) {
data[i] = Long.valueOf(blenderInputStream.readLong());
}
value = new DynamicArray<Number>(tableSizes, data);
}
break;
case FLOAT:
if (dataToRead == 1) {
value = Float.valueOf(blenderInputStream.readFloat());
} else {
Number[] data = new Number[dataToRead];
for (int i = 0; i < dataToRead; ++i) {
data[i] = Float.valueOf(blenderInputStream.readFloat());
}
value = new DynamicArray<Number>(tableSizes, data);
}
break;
case DOUBLE:
if (dataToRead == 1) {
value = Double.valueOf(blenderInputStream.readDouble());
} else {
Number[] data = new Number[dataToRead];
for (int i = 0; i < dataToRead; ++i) {
data[i] = Double.valueOf(blenderInputStream.readDouble());
}
value = new DynamicArray<Number>(tableSizes, data);
}
break;
case VOID:
break;
case STRUCTURE:
if (dataToRead == 1) {
Structure structure = blenderContext.getDnaBlockData().getStructure(type);
structure.fill(blenderContext.getInputStream());
value = structure;
} else {
Structure[] data = new Structure[dataToRead];
for (int i = 0; i < dataToRead; ++i) {
Structure structure = blenderContext.getDnaBlockData().getStructure(type);
structure.fill(blenderContext.getInputStream());
data[i] = structure;
}
value = new DynamicArray<Structure>(tableSizes, data);
}
break;
default:
throw new IllegalStateException("Unimplemented filling of type: " + type);
}
}
/**
* This method parses the field name to determine how the field should be used.
* @param nameBuilder
* the name of the field (given as StringBuilder)
* @throws BlenderFileException
* this exception is thrown if the names contain errors
*/
private void parseField(StringBuilder nameBuilder) throws BlenderFileException {
this.removeWhitespaces(nameBuilder);
// veryfying if the name is a pointer
int pointerIndex = nameBuilder.indexOf("*");
while (pointerIndex >= 0) {
++pointerLevel;
nameBuilder.deleteCharAt(pointerIndex);
pointerIndex = nameBuilder.indexOf("*");
}
// veryfying if the name is a function pointer
if (nameBuilder.indexOf("(") >= 0) {
function = true;
this.removeCharacter(nameBuilder, '(');
this.removeCharacter(nameBuilder, ')');
} else {
// veryfying if the name is a table
int tableStartIndex = 0;
List<Integer> lengths = new ArrayList<Integer>(3);// 3 dimensions will be enough in most cases
do {
tableStartIndex = nameBuilder.indexOf("[");
if (tableStartIndex > 0) {
int tableStopIndex = nameBuilder.indexOf("]");
if (tableStopIndex < 0) {
throw new BlenderFileException("Invalid structure name: " + name);
}
try {
lengths.add(Integer.valueOf(nameBuilder.substring(tableStartIndex + 1, tableStopIndex)));
} catch (NumberFormatException e) {
throw new BlenderFileException("Invalid structure name caused by invalid table length: " + name, e);
}
nameBuilder.delete(tableStartIndex, tableStopIndex + 1);
}
} while (tableStartIndex > 0);
if (!lengths.isEmpty()) {
tableSizes = new int[lengths.size()];
for (int i = 0; i < tableSizes.length; ++i) {
tableSizes[i] = lengths.get(i).intValue();
}
}
}
name = nameBuilder.toString();
}
/**
* This method removes the required character from the text.
* @param text
* the text we remove characters from
* @param toRemove
* the character to be removed
*/
private void removeCharacter(StringBuilder text, char toRemove) {
for (int i = 0; i < text.length(); ++i) {
if (text.charAt(i) == toRemove) {
text.deleteCharAt(i);
--i;
}
}
}
/**
* This method removes all whitespaces from the text.
* @param text
* the text we remove whitespaces from
*/
private void removeWhitespaces(StringBuilder text) {
for (int i = 0; i < text.length(); ++i) {
if (Character.isWhitespace(text.charAt(i))) {
text.deleteCharAt(i);
--i;
}
}
}
/**
* This method builds the full name of the field (with function, pointer and table indications).
* @return the full name of the field
*/
/*package*/ String getFullName() {
StringBuilder result = new StringBuilder();
if (function) {
result.append('(');
}
for (int i = 0; i < pointerLevel; ++i) {
result.append('*');
}
result.append(name);
if (tableSizes != null) {
for (int i = 0; i < tableSizes.length; ++i) {
result.append('[').append(tableSizes[i]).append(']');
}
}
if (function) {
result.append(")()");
}
return result.toString();
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append(this.getFullName());
// insert appropriate amount of spaces to format the output corrently
int nameLength = result.length();
result.append(' ');// at least one space is a must
for (int i = 1; i < NAME_LENGTH - nameLength; ++i) {// we start from i=1 because one space is already added
result.append(' ');
}
result.append(type);
nameLength = result.length();
for (int i = 0; i < NAME_LENGTH + TYPE_LENGTH - nameLength; ++i) {
result.append(' ');
}
if (value instanceof Character) {
result.append(" = ").append((int) ((Character) value).charValue());
} else {
result.append(" = ").append(value != null ? value.toString() : "null");
}
return result.toString();
}
}

@ -0,0 +1,189 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.file;
import com.jme3.scene.plugins.blender.BlenderContext;
/**
* A class that holds the header data of a file block. The file block itself is not implemented. This class holds its
* start position in the stream and using this the structure can fill itself with the proper data.
* @author Marcin Roguski
*/
public class FileBlockHeader {
public static final int BLOCK_TE00 = 'T' << 24 | 'E' << 16; // TE00
public static final int BLOCK_ME00 = 'M' << 24 | 'E' << 16; // ME00
public static final int BLOCK_SR00 = 'S' << 24 | 'R' << 16; // SR00
public static final int BLOCK_CA00 = 'C' << 24 | 'A' << 16; // CA00
public static final int BLOCK_LA00 = 'L' << 24 | 'A' << 16; // LA00
public static final int BLOCK_OB00 = 'O' << 24 | 'B' << 16; // OB00
public static final int BLOCK_MA00 = 'M' << 24 | 'A' << 16; // MA00
public static final int BLOCK_SC00 = 'S' << 24 | 'C' << 16; // SC00
public static final int BLOCK_WO00 = 'W' << 24 | 'O' << 16; // WO00
public static final int BLOCK_TX00 = 'T' << 24 | 'X' << 16; // TX00
public static final int BLOCK_IP00 = 'I' << 24 | 'P' << 16; // IP00
public static final int BLOCK_AC00 = 'A' << 24 | 'C' << 16; // AC00
public static final int BLOCK_GLOB = 'G' << 24 | 'L' << 16 | 'O' << 8 | 'B'; // GLOB
public static final int BLOCK_REND = 'R' << 24 | 'E' << 16 | 'N' << 8 | 'D'; // REND
public static final int BLOCK_DATA = 'D' << 24 | 'A' << 16 | 'T' << 8 | 'A'; // DATA
public static final int BLOCK_DNA1 = 'D' << 24 | 'N' << 16 | 'A' << 8 | '1'; // DNA1
public static final int BLOCK_ENDB = 'E' << 24 | 'N' << 16 | 'D' << 8 | 'B'; // ENDB
/** Identifier of the file-block [4 bytes]. */
private int code;
/** Total length of the data after the file-block-header [4 bytes]. */
private int size;
/**
* Memory address the structure was located when written to disk [4 or 8 bytes (defined in file header as a pointer
* size)].
*/
private long oldMemoryAddress;
/** Index of the SDNA structure [4 bytes]. */
private int sdnaIndex;
/** Number of structure located in this file-block [4 bytes]. */
private int count;
/** Start position of the block's data in the stream. */
private int blockPosition;
/**
* Constructor. Loads the block header from the given stream during instance creation.
* @param inputStream
* the stream we read the block header from
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the pointer size is neither 4 nor 8
*/
public FileBlockHeader(BlenderInputStream inputStream, BlenderContext blenderContext) throws BlenderFileException {
inputStream.alignPosition(4);
code = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte();
size = inputStream.readInt();
oldMemoryAddress = inputStream.readPointer();
sdnaIndex = inputStream.readInt();
count = inputStream.readInt();
blockPosition = inputStream.getPosition();
if (FileBlockHeader.BLOCK_DNA1 == code) {
blenderContext.setBlockData(new DnaBlockData(inputStream, blenderContext));
} else {
inputStream.setPosition(blockPosition + size);
blenderContext.addFileBlockHeader(Long.valueOf(oldMemoryAddress), this);
}
}
/**
* This method returns the structure described by the header filled with appropriate data.
* @param blenderContext
* the blender context
* @return structure filled with data
* @throws BlenderFileException
*/
public Structure getStructure(BlenderContext blenderContext) throws BlenderFileException {
blenderContext.getInputStream().setPosition(blockPosition);
Structure structure = blenderContext.getDnaBlockData().getStructure(sdnaIndex);
structure.fill(blenderContext.getInputStream());
return structure;
}
/**
* This method returns the code of this data block.
* @return the code of this data block
*/
public int getCode() {
return code;
}
/**
* This method returns the size of the data stored in this block.
* @return the size of the data stored in this block
*/
public int getSize() {
return size;
}
/**
* This method returns the sdna index.
* @return the sdna index
*/
public int getSdnaIndex() {
return sdnaIndex;
}
/**
* This data returns the number of structure stored in the data block after this header.
* @return the number of structure stored in the data block after this header
*/
public int getCount() {
return count;
}
/**
* This method returns the start position of the data block in the blend file stream.
* @return the start position of the data block
*/
public int getBlockPosition() {
return blockPosition;
}
/**
* This method indicates if the block is the last block in the file.
* @return true if this block is the last one in the file nad false otherwise
*/
public boolean isLastBlock() {
return FileBlockHeader.BLOCK_ENDB == code;
}
/**
* This method indicates if the block is the SDNA block.
* @return true if this block is the SDNA block and false otherwise
*/
public boolean isDnaBlock() {
return FileBlockHeader.BLOCK_DNA1 == code;
}
@Override
public String toString() {
return "FILE BLOCK HEADER [" + this.codeToString(code) + " : " + size + " : " + oldMemoryAddress + " : " + sdnaIndex + " : " + count + "]";
}
/**
* This method transforms the coded bloch id into a string value.
* @param code
* the id of the block
* @return the string value of the block id
*/
protected String codeToString(int code) {
char c1 = (char) ((code & 0xFF000000) >> 24);
char c2 = (char) ((code & 0xFF0000) >> 16);
char c3 = (char) ((code & 0xFF00) >> 8);
char c4 = (char) (code & 0xFF);
return String.valueOf(c1) + c2 + c3 + c4;
}
}

@ -0,0 +1,189 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.file;
import java.util.ArrayList;
import java.util.List;
import com.jme3.scene.plugins.blender.BlenderContext;
/**
* A class that represents a pointer of any level that can be stored in the file.
* @author Marcin Roguski
*/
public class Pointer {
/** The blender context. */
private BlenderContext blenderContext;
/** The level of the pointer. */
private int pointerLevel;
/** The address in file it points to. */
private long oldMemoryAddress;
/** This variable indicates if the field is a function pointer. */
public boolean function;
/**
* Constructr. Stores the basic data about the pointer.
* @param pointerLevel
* the level of the pointer
* @param function
* this variable indicates if the field is a function pointer
* @param blenderContext
* the repository f data; used in fetching the value that the pointer points
*/
public Pointer(int pointerLevel, boolean function, BlenderContext blenderContext) {
this.pointerLevel = pointerLevel;
this.function = function;
this.blenderContext = blenderContext;
}
/**
* This method fills the pointer with its address value (it doesn't get the actual data yet. Use the 'fetch' method
* for this.
* @param inputStream
* the stream we read the pointer value from
*/
public void fill(BlenderInputStream inputStream) {
oldMemoryAddress = inputStream.readPointer();
}
/**
* This method fetches the data stored under the given address.
* @return the data read from the file
* @throws BlenderFileException
* this exception is thrown when the blend file structure is somehow invalid or corrupted
*/
public List<Structure> fetchData() throws BlenderFileException {
if (oldMemoryAddress == 0) {
throw new NullPointerException("The pointer points to nothing!");
}
List<Structure> structures = null;
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(oldMemoryAddress);
if (dataFileBlock == null) {
throw new BlenderFileException("No data stored for address: " + oldMemoryAddress + ". Make sure you did not open the newer blender file with older blender version.");
}
BlenderInputStream inputStream = blenderContext.getInputStream();
if (pointerLevel > 1) {
int pointersAmount = dataFileBlock.getSize() / inputStream.getPointerSize() * dataFileBlock.getCount();
for (int i = 0; i < pointersAmount; ++i) {
inputStream.setPosition(dataFileBlock.getBlockPosition() + inputStream.getPointerSize() * i);
long oldMemoryAddress = inputStream.readPointer();
if (oldMemoryAddress != 0L) {
Pointer p = new Pointer(pointerLevel - 1, function, blenderContext);
p.oldMemoryAddress = oldMemoryAddress;
if (structures == null) {
structures = p.fetchData();
} else {
structures.addAll(p.fetchData());
}
} else {
// it is necessary to put null's if the pointer is null, ie. in materials array that is attached to the mesh, the index
// of the material is important, that is why we need null's to indicate that some materials' slots are empty
if (structures == null) {
structures = new ArrayList<Structure>();
}
structures.add(null);
}
}
} else {
inputStream.setPosition(dataFileBlock.getBlockPosition());
structures = new ArrayList<Structure>(dataFileBlock.getCount());
for (int i = 0; i < dataFileBlock.getCount(); ++i) {
Structure structure = blenderContext.getDnaBlockData().getStructure(dataFileBlock.getSdnaIndex());
structure.fill(blenderContext.getInputStream());
structures.add(structure);
}
return structures;
}
return structures;
}
/**
* This method indicates if this pointer points to a function.
* @return <b>true</b> if this is a function pointer and <b>false</b> otherwise
*/
public boolean isFunction() {
return function;
}
/**
* This method indicates if this is a null-pointer or not.
* @return <b>true</b> if the pointer is null and <b>false</b> otherwise
*/
public boolean isNull() {
return oldMemoryAddress == 0;
}
/**
* This method indicates if this is a null-pointer or not.
* @return <b>true</b> if the pointer is not null and <b>false</b> otherwise
*/
public boolean isNotNull() {
return oldMemoryAddress != 0;
}
/**
* This method returns the old memory address of the structure pointed by the pointer.
* @return the old memory address of the structure pointed by the pointer
*/
public long getOldMemoryAddress() {
return oldMemoryAddress;
}
@Override
public String toString() {
return oldMemoryAddress == 0 ? "{$null$}" : "{$" + oldMemoryAddress + "$}";
}
@Override
public int hashCode() {
return 31 + (int) (oldMemoryAddress ^ oldMemoryAddress >>> 32);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
Pointer other = (Pointer) obj;
if (oldMemoryAddress != other.oldMemoryAddress) {
return false;
}
return true;
}
}

@ -0,0 +1,315 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.file;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.jme3.scene.plugins.blender.BlenderContext;
/**
* A class representing a single structure in the file.
* @author Marcin Roguski
*/
public class Structure implements Cloneable {
/** The address of the block that fills the structure. */
private transient Long oldMemoryAddress;
/** The type of the structure. */
private String type;
/**
* The fields of the structure. Each field consists of a pair: name-type.
*/
private Field[] fields;
/**
* Constructor that copies the data of the structure.
* @param structure
* the structure to copy.
* @throws CloneNotSupportedException
* this exception should never be thrown
*/
private Structure(Structure structure) throws CloneNotSupportedException {
type = structure.type;
fields = new Field[structure.fields.length];
for (int i = 0; i < fields.length; ++i) {
fields[i] = (Field) structure.fields[i].clone();
}
oldMemoryAddress = structure.oldMemoryAddress;
}
/**
* Constructor. Loads the structure from the given stream during instance creation.
* @param inputStream
* the stream we read the structure from
* @param names
* the names from which the name of structure and its fields will be taken
* @param types
* the names of types for the structure
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception occurs if the amount of fields, defined in the file, is negative
*/
public Structure(BlenderInputStream inputStream, String[] names, String[] types, BlenderContext blenderContext) throws BlenderFileException {
int nameIndex = inputStream.readShort();
type = types[nameIndex];
int fieldsAmount = inputStream.readShort();
if (fieldsAmount < 0) {
throw new BlenderFileException("The amount of fields of " + type + " structure cannot be negative!");
}
if (fieldsAmount > 0) {
fields = new Field[fieldsAmount];
for (int i = 0; i < fieldsAmount; ++i) {
int typeIndex = inputStream.readShort();
nameIndex = inputStream.readShort();
fields[i] = new Field(names[nameIndex], types[typeIndex], blenderContext);
}
}
oldMemoryAddress = Long.valueOf(-1L);
}
/**
* This method fills the structure with data.
* @param inputStream
* the stream we read data from, its read cursor should be placed at the start position of the data for the
* structure
* @throws BlenderFileException
* an exception is thrown when the blend file is somehow invalid or corrupted
*/
public void fill(BlenderInputStream inputStream) throws BlenderFileException {
int position = inputStream.getPosition();
inputStream.setPosition(position - 8 - inputStream.getPointerSize());
oldMemoryAddress = Long.valueOf(inputStream.readPointer());
inputStream.setPosition(position);
for (Field field : fields) {
field.fill(inputStream);
}
}
/**
* This method returns the value of the filed with a given name.
* @param fieldName
* the name of the field
* @return the value of the field or null if no field with a given name is found
*/
public Object getFieldValue(String fieldName) {
for (Field field : fields) {
if (field.name.equalsIgnoreCase(fieldName)) {
return field.value;
}
}
return null;
}
/**
* This method returns the value of the filed with a given name. The structure is considered to have flat fields
* only (no substructures).
* @param fieldName
* the name of the field
* @return the value of the field or null if no field with a given name is found
*/
public Object getFlatFieldValue(String fieldName) {
for (Field field : fields) {
Object value = field.value;
if (field.name.equalsIgnoreCase(fieldName)) {
return value;
} else if (value instanceof Structure) {
value = ((Structure) value).getFlatFieldValue(fieldName);
if (value != null) {// we can compare references here, since we use one static object as a NULL field value
return value;
}
}
}
return null;
}
/**
* This methos should be used on structures that are of a 'ListBase' type. It creates a List of structures that are
* held by this structure within the blend file.
* @return a list of filled structures
* @throws BlenderFileException
* this exception is thrown when the blend file structure is somehow invalid or corrupted
* @throws IllegalArgumentException
* this exception is thrown if the type of the structure is not 'ListBase'
*/
public List<Structure> evaluateListBase() throws BlenderFileException {
if (!"ListBase".equals(type)) {
throw new IllegalStateException("This structure is not of type: 'ListBase'");
}
Pointer first = (Pointer) this.getFieldValue("first");
Pointer last = (Pointer) this.getFieldValue("last");
long currentAddress = 0;
long lastAddress = last.getOldMemoryAddress();
List<Structure> result = new LinkedList<Structure>();
while (currentAddress != lastAddress) {
currentAddress = first.getOldMemoryAddress();
Structure structure = first.fetchData().get(0);
result.add(structure);
first = (Pointer) structure.getFlatFieldValue("next");
}
return result;
}
/**
* This method returns the type of the structure.
* @return the type of the structure
*/
public String getType() {
return type;
}
/**
* This method returns the amount of fields for the current structure.
* @return the amount of fields for the current structure
*/
public int getFieldsAmount() {
return fields.length;
}
/**
* This method returns the full field name of the given index.
* @param fieldIndex
* the index of the field
* @return the full field name of the given index
*/
public String getFieldFullName(int fieldIndex) {
return fields[fieldIndex].getFullName();
}
/**
* This method returns the field type of the given index.
* @param fieldIndex
* the index of the field
* @return the field type of the given index
*/
public String getFieldType(int fieldIndex) {
return fields[fieldIndex].type;
}
/**
* This method returns the address of the structure. The strucutre should be filled with data otherwise an exception
* is thrown.
* @return the address of the feature stored in this structure
*/
public Long getOldMemoryAddress() {
if (oldMemoryAddress.longValue() == -1L) {
throw new IllegalStateException("Call the 'fill' method and fill the structure with data first!");
}
return oldMemoryAddress;
}
/**
* This method returns the name of the structure. If the structure has an ID field then the name is returned.
* Otherwise the name does not exists and the method returns null.
* @return the name of the structure read from the ID field or null
*/
public String getName() {
Object fieldValue = this.getFieldValue("ID");
if (fieldValue instanceof Structure) {
Structure id = (Structure) fieldValue;
return id == null ? null : id.getFieldValue("name").toString().substring(2);// blender adds 2-charactes as a name prefix
}
return null;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder("struct ").append(type).append(" {\n");
for (int i = 0; i < fields.length; ++i) {
result.append(fields[i].toString()).append('\n');
}
return result.append('}').toString();
}
@Override
public Object clone() throws CloneNotSupportedException {
return new Structure(this);
}
/**
* This enum enumerates all known data types that can be found in the blend file.
* @author Marcin Roguski (Kaelthas)
*/
/* package */static enum DataType {
CHARACTER, SHORT, INTEGER, LONG, FLOAT, DOUBLE, VOID, STRUCTURE, POINTER;
/** The map containing the known primary types. */
private static final Map<String, DataType> PRIMARY_TYPES = new HashMap<String, DataType>(10);
static {
PRIMARY_TYPES.put("char", CHARACTER);
PRIMARY_TYPES.put("uchar", CHARACTER);
PRIMARY_TYPES.put("short", SHORT);
PRIMARY_TYPES.put("ushort", SHORT);
PRIMARY_TYPES.put("int", INTEGER);
PRIMARY_TYPES.put("long", LONG);
PRIMARY_TYPES.put("ulong", LONG);
PRIMARY_TYPES.put("uint64_t", LONG);
PRIMARY_TYPES.put("float", FLOAT);
PRIMARY_TYPES.put("double", DOUBLE);
PRIMARY_TYPES.put("void", VOID);
}
/**
* This method returns the data type that is appropriate to the given type name. WARNING! The type recognition
* is case sensitive!
* @param type
* the type name of the data
* @param blenderContext
* the blender context
* @return appropriate enum value to the given type name
* @throws BlenderFileException
* this exception is thrown if the given type name does not exist in the blend file
*/
public static DataType getDataType(String type, BlenderContext blenderContext) throws BlenderFileException {
DataType result = PRIMARY_TYPES.get(type);
if (result != null) {
return result;
}
if (blenderContext.getDnaBlockData().hasStructure(type)) {
return STRUCTURE;
}
throw new BlenderFileException("Unknown data type: " + type);
}
/**
* @return a collection of known primary types names
*/
/* package */static Collection<String> getKnownPrimaryTypesNames() {
return PRIMARY_TYPES.keySet();
}
}
}

@ -0,0 +1,186 @@
package com.jme3.scene.plugins.blender.landscape;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.light.AmbientLight;
import com.jme3.light.Light;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.textures.ColorBand;
import com.jme3.scene.plugins.blender.textures.CombinedTexture;
import com.jme3.scene.plugins.blender.textures.ImageUtils;
import com.jme3.scene.plugins.blender.textures.TextureHelper;
import com.jme3.scene.plugins.blender.textures.TexturePixel;
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.TextureCubeMap;
import com.jme3.util.SkyFactory;
/**
* The class that allows to load the following: <li>the ambient light of the scene <li>the sky of the scene (with or without texture)
*
* @author Marcin Roguski (Kaelthas)
*/
public class LandscapeHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(LandscapeHelper.class.getName());
private static final int SKYTYPE_BLEND = 1;
private static final int SKYTYPE_REAL = 2;
private static final int SKYTYPE_PAPER = 4;
public LandscapeHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* Loads scene ambient light.
* @param worldStructure
* the world's blender structure
* @return the scene's ambient light
*/
public Light toAmbientLight(Structure worldStructure) {
LOGGER.fine("Loading ambient light.");
AmbientLight ambientLight = new AmbientLight();
float ambr = ((Number) worldStructure.getFieldValue("ambr")).floatValue();
float ambg = ((Number) worldStructure.getFieldValue("ambg")).floatValue();
float ambb = ((Number) worldStructure.getFieldValue("ambb")).floatValue();
ColorRGBA ambientLightColor = new ColorRGBA(ambr, ambg, ambb, 0.0f);
ambientLight.setColor(ambientLightColor);
LOGGER.log(Level.FINE, "Loaded ambient light: {0}.", ambientLightColor);
return ambientLight;
}
/**
* Loads the background color.
* @param worldStructure
* the world's structure
* @return the horizon color of the world which is used as a background color.
*/
public ColorRGBA toBackgroundColor(Structure worldStructure) {
float horr = ((Number) worldStructure.getFieldValue("horr")).floatValue();
float horg = ((Number) worldStructure.getFieldValue("horg")).floatValue();
float horb = ((Number) worldStructure.getFieldValue("horb")).floatValue();
return new ColorRGBA(horr, horg, horb, 1);
}
/**
* Loads scene's sky. Sky can be plain or textured.
* If no sky type is selected in blender then no sky is loaded.
* @param worldStructure
* the world's structure
* @return the scene's sky
* @throws BlenderFileException
* blender exception is thrown when problems with blender file occur
*/
public Spatial toSky(Structure worldStructure) throws BlenderFileException {
int skytype = ((Number) worldStructure.getFieldValue("skytype")).intValue();
if (skytype == 0) {
return null;
}
LOGGER.fine("Loading sky.");
ColorRGBA horizontalColor = this.toBackgroundColor(worldStructure);
float zenr = ((Number) worldStructure.getFieldValue("zenr")).floatValue();
float zeng = ((Number) worldStructure.getFieldValue("zeng")).floatValue();
float zenb = ((Number) worldStructure.getFieldValue("zenb")).floatValue();
ColorRGBA zenithColor = new ColorRGBA(zenr, zeng, zenb, 1);
// jutr for this case load generated textures wheather user had set it or not because those might be needed to properly load the sky
boolean loadGeneratedTextures = blenderContext.getBlenderKey().isLoadGeneratedTextures();
blenderContext.getBlenderKey().setLoadGeneratedTextures(true);
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
List<CombinedTexture> loadedTextures = null;
try {
loadedTextures = textureHelper.readTextureData(worldStructure, new float[] { horizontalColor.r, horizontalColor.g, horizontalColor.b, horizontalColor.a }, true);
} finally {
blenderContext.getBlenderKey().setLoadGeneratedTextures(loadGeneratedTextures);
}
TextureCubeMap texture = null;
if (loadedTextures != null && loadedTextures.size() > 0) {
if (loadedTextures.size() > 1) {
throw new IllegalStateException("There should be only one combined texture for sky!");
}
CombinedTexture combinedTexture = loadedTextures.get(0);
texture = combinedTexture.generateSkyTexture(horizontalColor, zenithColor, blenderContext);
} else {
LOGGER.fine("Preparing colors for colorband.");
int colorbandType = ColorBand.IPO_CARDINAL;
List<ColorRGBA> colorbandColors = new ArrayList<ColorRGBA>(3);
colorbandColors.add(horizontalColor);
if ((skytype & SKYTYPE_BLEND) != 0) {
if ((skytype & SKYTYPE_PAPER) != 0) {
colorbandType = ColorBand.IPO_LINEAR;
}
if ((skytype & SKYTYPE_REAL) != 0) {
colorbandColors.add(0, zenithColor);
}
colorbandColors.add(zenithColor);
}
int size = blenderContext.getBlenderKey().getSkyGeneratedTextureSize();
List<Integer> positions = new ArrayList<Integer>(colorbandColors.size());
positions.add(0);
if (colorbandColors.size() == 2) {
positions.add(size - 1);
} else if (colorbandColors.size() == 3) {
positions.add(size / 2);
positions.add(size - 1);
}
LOGGER.fine("Generating sky texture.");
float[][] values = new ColorBand(colorbandType, colorbandColors, positions, size).computeValues();
Image image = ImageUtils.createEmptyImage(Format.RGB8, size, size, 6);
PixelInputOutput pixelIO = PixelIOFactory.getPixelIO(image.getFormat());
TexturePixel pixel = new TexturePixel();
LOGGER.fine("Creating side textures.");
int[] sideImagesIndexes = new int[] { 0, 1, 4, 5 };
for (int i : sideImagesIndexes) {
for (int y = 0; y < size; ++y) {
pixel.red = values[y][0];
pixel.green = values[y][1];
pixel.blue = values[y][2];
for (int x = 0; x < size; ++x) {
pixelIO.write(image, i, pixel, x, y);
}
}
}
LOGGER.fine("Creating top texture.");
pixelIO.read(image, 0, pixel, 0, image.getHeight() - 1);
for (int y = 0; y < size; ++y) {
for (int x = 0; x < size; ++x) {
pixelIO.write(image, 3, pixel, x, y);
}
}
LOGGER.fine("Creating bottom texture.");
pixelIO.read(image, 0, pixel, 0, 0);
for (int y = 0; y < size; ++y) {
for (int x = 0; x < size; ++x) {
pixelIO.write(image, 2, pixel, x, y);
}
}
texture = new TextureCubeMap(image);
}
LOGGER.fine("Sky texture created. Creating sky.");
return SkyFactory.createSky(blenderContext.getAssetManager(), texture, false);
}
}

@ -0,0 +1,118 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.lights;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.light.DirectionalLight;
import com.jme3.light.Light;
import com.jme3.light.PointLight;
import com.jme3.light.SpotLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.scene.LightNode;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* A class that is used in light calculations.
* @author Marcin Roguski
*/
public class LightHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(LightHelper.class.getName());
/**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in
* different blender versions.
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public LightHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
public LightNode toLight(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
LightNode result = (LightNode) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
if (result != null) {
return result;
}
Light light = null;
int type = ((Number) structure.getFieldValue("type")).intValue();
switch (type) {
case 0:// Lamp
light = new PointLight();
float distance = ((Number) structure.getFieldValue("dist")).floatValue();
((PointLight) light).setRadius(distance);
break;
case 1:// Sun
LOGGER.log(Level.WARNING, "'Sun' lamp is not supported in jMonkeyEngine.");
break;
case 2:// Spot
light = new SpotLight();
// range
((SpotLight) light).setSpotRange(((Number) structure.getFieldValue("dist")).floatValue());
// outer angle
float outerAngle = ((Number) structure.getFieldValue("spotsize")).floatValue() * FastMath.DEG_TO_RAD * 0.5f;
((SpotLight) light).setSpotOuterAngle(outerAngle);
// inner angle
float spotblend = ((Number) structure.getFieldValue("spotblend")).floatValue();
spotblend = FastMath.clamp(spotblend, 0, 1);
float innerAngle = outerAngle * (1 - spotblend);
((SpotLight) light).setSpotInnerAngle(innerAngle);
break;
case 3:// Hemi
LOGGER.log(Level.WARNING, "'Hemi' lamp is not supported in jMonkeyEngine.");
break;
case 4:// Area
light = new DirectionalLight();
break;
default:
throw new BlenderFileException("Unknown light source type: " + type);
}
if (light != null) {
float r = ((Number) structure.getFieldValue("r")).floatValue();
float g = ((Number) structure.getFieldValue("g")).floatValue();
float b = ((Number) structure.getFieldValue("b")).floatValue();
light.setColor(new ColorRGBA(r, g, b, 1.0f));
result = new LightNode(null, light);
}
return result;
}
}

@ -0,0 +1,26 @@
package com.jme3.scene.plugins.blender.materials;
/**
* An interface used in calculating alpha mask during particles' texture calculations.
* @author Marcin Roguski (Kaelthas)
*/
/* package */interface IAlphaMask {
/**
* This method sets the size of the texture's image.
* @param width
* the width of the image
* @param height
* the height of the image
*/
void setImageSize(int width, int height);
/**
* This method returns the alpha value for the specified texture position.
* @param x
* the X coordinate of the texture position
* @param y
* the Y coordinate of the texture position
* @return the alpha value for the specified texture position
*/
byte getAlpha(float x, float y);
}

@ -0,0 +1,327 @@
package com.jme3.scene.plugins.blender.materials;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Geometry;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Format;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialHelper.DiffuseShader;
import com.jme3.scene.plugins.blender.materials.MaterialHelper.SpecularShader;
import com.jme3.scene.plugins.blender.textures.CombinedTexture;
import com.jme3.scene.plugins.blender.textures.TextureHelper;
import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils;
/**
* This class holds the data about the material.
* @author Marcin Roguski (Kaelthas)
*/
public final class MaterialContext {
private static final Logger LOGGER = Logger.getLogger(MaterialContext.class.getName());
// texture mapping types
public static final int MTEX_COL = 0x01;
public static final int MTEX_NOR = 0x02;
public static final int MTEX_SPEC = 0x04;
public static final int MTEX_EMIT = 0x40;
public static final int MTEX_ALPHA = 0x80;
public static final int MTEX_AMB = 0x800;
/* package */final String name;
/* package */final List<CombinedTexture> loadedTextures;
/* package */final ColorRGBA diffuseColor;
/* package */final DiffuseShader diffuseShader;
/* package */final SpecularShader specularShader;
/* package */final ColorRGBA specularColor;
/* package */final ColorRGBA ambientColor;
/* package */final float shininess;
/* package */final boolean shadeless;
/* package */final boolean vertexColor;
/* package */final boolean transparent;
/* package */final boolean vTangent;
/* package */FaceCullMode faceCullMode;
/* package */MaterialContext(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
name = structure.getName();
int mode = ((Number) structure.getFieldValue("mode")).intValue();
shadeless = (mode & 0x4) != 0;
vertexColor = (mode & 0x80) != 0;
vTangent = (mode & 0x4000000) != 0; // NOTE: Requires tangents
int diff_shader = ((Number) structure.getFieldValue("diff_shader")).intValue();
diffuseShader = DiffuseShader.values()[diff_shader];
if (shadeless) {
float r = ((Number) structure.getFieldValue("r")).floatValue();
float g = ((Number) structure.getFieldValue("g")).floatValue();
float b = ((Number) structure.getFieldValue("b")).floatValue();
float alpha = ((Number) structure.getFieldValue("alpha")).floatValue();
diffuseColor = new ColorRGBA(r, g, b, alpha);
specularShader = null;
specularColor = ambientColor = null;
shininess = 0.0f;
} else {
diffuseColor = this.readDiffuseColor(structure, diffuseShader);
int spec_shader = ((Number) structure.getFieldValue("spec_shader")).intValue();
specularShader = SpecularShader.values()[spec_shader];
specularColor = this.readSpecularColor(structure);
float shininess = ((Number) structure.getFieldValue("har")).floatValue();// this is (probably) the specular hardness in blender
this.shininess = shininess > 0.0f ? shininess : MaterialHelper.DEFAULT_SHININESS;
float r = ((Number) structure.getFieldValue("ambr")).floatValue();
float g = ((Number) structure.getFieldValue("ambg")).floatValue();
float b = ((Number) structure.getFieldValue("ambb")).floatValue();
float alpha = ((Number) structure.getFieldValue("alpha")).floatValue();
ambientColor = new ColorRGBA(r, g, b, alpha);
}
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
loadedTextures = textureHelper.readTextureData(structure, new float[] { diffuseColor.r, diffuseColor.g, diffuseColor.b, diffuseColor.a }, false);
// veryfying if the transparency is present
// (in blender transparent mask is 0x10000 but its better to verify it because blender can indicate transparency when
// it is not required
boolean transparent = false;
if (diffuseColor != null) {
transparent = diffuseColor.a < 1.0f;
if (loadedTextures.size() > 0) {// texutre covers the material color
diffuseColor.set(1, 1, 1, 1);
}
}
if (specularColor != null) {
transparent = transparent || specularColor.a < 1.0f;
}
if (ambientColor != null) {
transparent = transparent || ambientColor.a < 1.0f;
}
this.transparent = transparent;
}
/**
* Applies material to a given geometry.
*
* @param geometry
* the geometry
* @param geometriesOMA
* the geometries OMA
* @param userDefinedUVCoordinates
* UV coords defined by user
* @param blenderContext
* the blender context
*/
public void applyMaterial(Geometry geometry, Long geometriesOMA, LinkedHashMap<String, List<Vector2f>> userDefinedUVCoordinates, BlenderContext blenderContext) {
Material material = null;
if (shadeless) {
material = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
if (!transparent) {
diffuseColor.a = 1;
}
material.setColor("Color", diffuseColor);
} else {
material = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Light/Lighting.j3md");
material.setBoolean("UseMaterialColors", Boolean.TRUE);
// setting the colors
material.setBoolean("Minnaert", diffuseShader == DiffuseShader.MINNAERT);
if (!transparent) {
diffuseColor.a = 1;
}
material.setColor("Diffuse", diffuseColor);
material.setBoolean("WardIso", specularShader == SpecularShader.WARDISO);
material.setColor("Specular", specularColor);
material.setFloat("Shininess", shininess);
material.setColor("Ambient", ambientColor);
}
// applying textures
if (loadedTextures != null && loadedTextures.size() > 0) {
int textureIndex = 0;
if (loadedTextures.size() > TextureHelper.TEXCOORD_TYPES.length) {
LOGGER.log(Level.WARNING, "The blender file has defined more than {0} different textures. JME supports only {0} UV mappings.", TextureHelper.TEXCOORD_TYPES.length);
}
for (CombinedTexture combinedTexture : loadedTextures) {
if (textureIndex < TextureHelper.TEXCOORD_TYPES.length) {
combinedTexture.flatten(geometry, geometriesOMA, userDefinedUVCoordinates, blenderContext);
this.setTexture(material, combinedTexture.getMappingType(), combinedTexture.getResultTexture());
List<Vector2f> uvs = combinedTexture.getResultUVS();
VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]);
uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()])));
geometry.getMesh().setBuffer(uvCoordsBuffer);
} else {
LOGGER.log(Level.WARNING, "The texture could not be applied because JME only supports up to {0} different UV's.", TextureHelper.TEXCOORD_TYPES.length);
}
}
} else if (userDefinedUVCoordinates != null && userDefinedUVCoordinates.size() > 0) {
LOGGER.fine("No textures found for the mesh, but UV coordinates are applied.");
int textureIndex = 0;
if (userDefinedUVCoordinates.size() > TextureHelper.TEXCOORD_TYPES.length) {
LOGGER.log(Level.WARNING, "The blender file has defined more than {0} different UV coordinates for the mesh. JME supports only {0} UV coordinates buffers.", TextureHelper.TEXCOORD_TYPES.length);
}
for (Entry<String, List<Vector2f>> entry : userDefinedUVCoordinates.entrySet()) {
if (textureIndex < TextureHelper.TEXCOORD_TYPES.length) {
List<Vector2f> uvs = entry.getValue();
VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]);
uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()])));
geometry.getMesh().setBuffer(uvCoordsBuffer);
} else {
LOGGER.log(Level.WARNING, "The texture could not be applied because JME only supports up to {0} different UV's.", TextureHelper.TEXCOORD_TYPES.length);
}
}
}
// applying additional data
material.setName(name);
if (vertexColor) {
material.setBoolean(shadeless ? "VertexColor" : "UseVertexColor", true);
}
material.getAdditionalRenderState().setFaceCullMode(faceCullMode != null ? faceCullMode : blenderContext.getBlenderKey().getFaceCullMode());
if (transparent) {
material.setTransparent(true);
material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
geometry.setQueueBucket(Bucket.Transparent);
}
geometry.setMaterial(material);
}
/**
* Sets the texture to the given material.
*
* @param material
* the material that we add texture to
* @param mapTo
* the texture mapping type
* @param texture
* the added texture
*/
private void setTexture(Material material, int mapTo, Texture texture) {
switch (mapTo) {
case MTEX_COL:
material.setTexture(shadeless ? MaterialHelper.TEXTURE_TYPE_COLOR : MaterialHelper.TEXTURE_TYPE_DIFFUSE, texture);
break;
case MTEX_NOR:
material.setTexture(MaterialHelper.TEXTURE_TYPE_NORMAL, texture);
break;
case MTEX_SPEC:
material.setTexture(MaterialHelper.TEXTURE_TYPE_SPECULAR, texture);
break;
case MTEX_EMIT:
material.setTexture(MaterialHelper.TEXTURE_TYPE_GLOW, texture);
break;
case MTEX_ALPHA:
if (!shadeless) {
material.setTexture(MaterialHelper.TEXTURE_TYPE_ALPHA, texture);
} else {
LOGGER.warning("JME does not support alpha map on unshaded material. Material name is " + name);
}
break;
case MTEX_AMB:
material.setTexture(MaterialHelper.TEXTURE_TYPE_LIGHTMAP, texture);
break;
default:
LOGGER.severe("Unknown mapping type: " + mapTo);
}
}
/**
* @return <b>true</b> if the material has at least one generated texture and <b>false</b> otherwise
*/
public boolean hasGeneratedTextures() {
if (loadedTextures != null) {
for (CombinedTexture generatedTextures : loadedTextures) {
if (generatedTextures.hasGeneratedTextures()) {
return true;
}
}
}
return false;
}
/**
* This method sets the face cull mode.
* @param faceCullMode
* the face cull mode
*/
public void setFaceCullMode(FaceCullMode faceCullMode) {
this.faceCullMode = faceCullMode;
}
/**
* This method returns the diffuse color.
*
* @param materialStructure
* the material structure
* @param diffuseShader
* the diffuse shader
* @return the diffuse color
*/
private ColorRGBA readDiffuseColor(Structure materialStructure, DiffuseShader diffuseShader) {
// bitwise 'or' of all textures mappings
int commonMapto = ((Number) materialStructure.getFieldValue("mapto")).intValue();
// diffuse color
float r = ((Number) materialStructure.getFieldValue("r")).floatValue();
float g = ((Number) materialStructure.getFieldValue("g")).floatValue();
float b = ((Number) materialStructure.getFieldValue("b")).floatValue();
float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue();
if ((commonMapto & 0x01) == 0x01) {// Col
return new ColorRGBA(r, g, b, alpha);
} else {
switch (diffuseShader) {
case FRESNEL:
case ORENNAYAR:
case TOON:
break;// TODO: find what is the proper modification
case MINNAERT:
case LAMBERT:// TODO: check if that is correct
float ref = ((Number) materialStructure.getFieldValue("ref")).floatValue();
r *= ref;
g *= ref;
b *= ref;
break;
default:
throw new IllegalStateException("Unknown diffuse shader type: " + diffuseShader.toString());
}
return new ColorRGBA(r, g, b, alpha);
}
}
/**
* This method returns a specular color used by the material.
*
* @param materialStructure
* the material structure filled with data
* @return a specular color used by the material
*/
private ColorRGBA readSpecularColor(Structure materialStructure) {
float specularIntensity = ((Number) materialStructure.getFieldValue("spec")).floatValue();
float r = ((Number) materialStructure.getFieldValue("specr")).floatValue() * specularIntensity;
float g = ((Number) materialStructure.getFieldValue("specg")).floatValue() * specularIntensity;
float b = ((Number) materialStructure.getFieldValue("specb")).floatValue() * specularIntensity;
float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue();
return new ColorRGBA(r, g, b, alpha);
}
}

@ -0,0 +1,371 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.materials;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.material.MatParam;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.shader.VarType;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils;
public class MaterialHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(MaterialHelper.class.getName());
protected static final float DEFAULT_SHININESS = 20.0f;
public static final String TEXTURE_TYPE_COLOR = "ColorMap";
public static final String TEXTURE_TYPE_DIFFUSE = "DiffuseMap";
public static final String TEXTURE_TYPE_NORMAL = "NormalMap";
public static final String TEXTURE_TYPE_SPECULAR = "SpecularMap";
public static final String TEXTURE_TYPE_GLOW = "GlowMap";
public static final String TEXTURE_TYPE_ALPHA = "AlphaMap";
public static final String TEXTURE_TYPE_LIGHTMAP = "LightMap";
public static final Integer ALPHA_MASK_NONE = Integer.valueOf(0);
public static final Integer ALPHA_MASK_CIRCLE = Integer.valueOf(1);
public static final Integer ALPHA_MASK_CONE = Integer.valueOf(2);
public static final Integer ALPHA_MASK_HYPERBOLE = Integer.valueOf(3);
protected final Map<Integer, IAlphaMask> alphaMasks = new HashMap<Integer, IAlphaMask>();
/**
* The type of the material's diffuse shader.
*/
public static enum DiffuseShader {
LAMBERT, ORENNAYAR, TOON, MINNAERT, FRESNEL
}
/**
* The type of the material's specular shader.
*/
public static enum SpecularShader {
COOKTORRENCE, PHONG, BLINN, TOON, WARDISO
}
/**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
* versions.
*
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public MaterialHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
// setting alpha masks
alphaMasks.put(ALPHA_MASK_NONE, new IAlphaMask() {
public void setImageSize(int width, int height) {
}
public byte getAlpha(float x, float y) {
return (byte) 255;
}
});
alphaMasks.put(ALPHA_MASK_CIRCLE, new IAlphaMask() {
private float r;
private float[] center;
public void setImageSize(int width, int height) {
r = Math.min(width, height) * 0.5f;
center = new float[] { width * 0.5f, height * 0.5f };
}
public byte getAlpha(float x, float y) {
float d = FastMath.abs(FastMath.sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1])));
return (byte) (d >= r ? 0 : 255);
}
});
alphaMasks.put(ALPHA_MASK_CONE, new IAlphaMask() {
private float r;
private float[] center;
public void setImageSize(int width, int height) {
r = Math.min(width, height) * 0.5f;
center = new float[] { width * 0.5f, height * 0.5f };
}
public byte getAlpha(float x, float y) {
float d = FastMath.abs(FastMath.sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1])));
return (byte) (d >= r ? 0 : -255.0f * d / r + 255.0f);
}
});
alphaMasks.put(ALPHA_MASK_HYPERBOLE, new IAlphaMask() {
private float r;
private float[] center;
public void setImageSize(int width, int height) {
r = Math.min(width, height) * 0.5f;
center = new float[] { width * 0.5f, height * 0.5f };
}
public byte getAlpha(float x, float y) {
float d = FastMath.abs(FastMath.sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1]))) / r;
return d >= 1.0f ? 0 : (byte) ((-FastMath.sqrt((2.0f - d) * d) + 1.0f) * 255.0f);
}
});
}
/**
* This method converts the material structure to jme Material.
* @param structure
* structure with material data
* @param blenderContext
* the blender context
* @return jme material
* @throws BlenderFileException
* an exception is throw when problems with blend file occur
*/
public MaterialContext toMaterialContext(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading material.");
MaterialContext result = (MaterialContext) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
if (result != null) {
return result;
}
result = new MaterialContext(structure, blenderContext);
LOGGER.log(Level.FINE, "Material''s name: {0}", result.name);
blenderContext.addLoadedFeatures(structure.getOldMemoryAddress(), structure.getName(), structure, result);
return result;
}
/**
* This method converts the given material into particles-usable material.
* The texture and glow color are being copied.
* The method assumes it receives the Lighting type of material.
* @param material
* the source material
* @param blenderContext
* the blender context
* @return material converted into particles-usable material
*/
public Material getParticlesMaterial(Material material, Integer alphaMaskIndex, BlenderContext blenderContext) {
Material result = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
// copying texture
MatParam diffuseMap = material.getParam("DiffuseMap");
if (diffuseMap != null) {
Texture texture = ((Texture) diffuseMap.getValue()).clone();
// applying alpha mask to the texture
Image image = texture.getImage();
ByteBuffer sourceBB = image.getData(0);
sourceBB.rewind();
int w = image.getWidth();
int h = image.getHeight();
ByteBuffer bb = BufferUtils.createByteBuffer(w * h * 4);
IAlphaMask iAlphaMask = alphaMasks.get(alphaMaskIndex);
iAlphaMask.setImageSize(w, h);
for (int x = 0; x < w; ++x) {
for (int y = 0; y < h; ++y) {
bb.put(sourceBB.get());
bb.put(sourceBB.get());
bb.put(sourceBB.get());
bb.put(iAlphaMask.getAlpha(x, y));
}
}
image = new Image(Format.RGBA8, w, h, bb);
texture.setImage(image);
result.setTextureParam("Texture", VarType.Texture2D, texture);
}
// copying glow color
MatParam glowColor = material.getParam("GlowColor");
if (glowColor != null) {
ColorRGBA color = (ColorRGBA) glowColor.getValue();
result.setParam("GlowColor", VarType.Vector3, color);
}
return result;
}
/**
* This method returns the table of materials connected to the specified structure. The given structure can be of any type (ie. mesh or
* curve) but needs to have 'mat' field/
*
* @param structureWithMaterials
* the structure containing the mesh data
* @param blenderContext
* the blender context
* @return a list of vertices colors, each color belongs to a single vertex
* @throws BlenderFileException
* this exception is thrown when the blend file structure is somehow invalid or corrupted
*/
public MaterialContext[] getMaterials(Structure structureWithMaterials, BlenderContext blenderContext) throws BlenderFileException {
Pointer ppMaterials = (Pointer) structureWithMaterials.getFieldValue("mat");
MaterialContext[] materials = null;
if (ppMaterials.isNotNull()) {
List<Structure> materialStructures = ppMaterials.fetchData();
if (materialStructures != null && materialStructures.size() > 0) {
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
materials = new MaterialContext[materialStructures.size()];
int i = 0;
for (Structure s : materialStructures) {
materials[i++] = s == null ? null : materialHelper.toMaterialContext(s, blenderContext);
}
}
}
return materials;
}
/**
* This method converts rgb values to hsv values.
*
* @param r
* red value of the color
* @param g
* green value of the color
* @param b
* blue value of the color
* @param hsv
* hsv values of a color (this table contains the result of the transformation)
*/
public void rgbToHsv(float r, float g, float b, float[] hsv) {
float cmax = r;
float cmin = r;
cmax = g > cmax ? g : cmax;
cmin = g < cmin ? g : cmin;
cmax = b > cmax ? b : cmax;
cmin = b < cmin ? b : cmin;
hsv[2] = cmax; /* value */
if (cmax != 0.0) {
hsv[1] = (cmax - cmin) / cmax;
} else {
hsv[1] = 0.0f;
hsv[0] = 0.0f;
}
if (hsv[1] == 0.0) {
hsv[0] = -1.0f;
} else {
float cdelta = cmax - cmin;
float rc = (cmax - r) / cdelta;
float gc = (cmax - g) / cdelta;
float bc = (cmax - b) / cdelta;
if (r == cmax) {
hsv[0] = bc - gc;
} else if (g == cmax) {
hsv[0] = 2.0f + rc - bc;
} else {
hsv[0] = 4.0f + gc - rc;
}
hsv[0] *= 60.0f;
if (hsv[0] < 0.0f) {
hsv[0] += 360.0f;
}
}
hsv[0] /= 360.0f;
if (hsv[0] < 0.0f) {
hsv[0] = 0.0f;
}
}
/**
* This method converts rgb values to hsv values.
*
* @param h
* hue
* @param s
* saturation
* @param v
* value
* @param rgb
* rgb result vector (should have 3 elements)
*/
public void hsvToRgb(float h, float s, float v, float[] rgb) {
h *= 360.0f;
if (s == 0.0) {
rgb[0] = rgb[1] = rgb[2] = v;
} else {
if (h == 360) {
h = 0;
} else {
h /= 60;
}
int i = (int) Math.floor(h);
float f = h - i;
float p = v * (1.0f - s);
float q = v * (1.0f - s * f);
float t = v * (1.0f - s * (1.0f - f));
switch (i) {
case 0:
rgb[0] = v;
rgb[1] = t;
rgb[2] = p;
break;
case 1:
rgb[0] = q;
rgb[1] = v;
rgb[2] = p;
break;
case 2:
rgb[0] = p;
rgb[1] = v;
rgb[2] = t;
break;
case 3:
rgb[0] = p;
rgb[1] = q;
rgb[2] = v;
break;
case 4:
rgb[0] = t;
rgb[1] = p;
rgb[2] = v;
break;
case 5:
rgb[0] = v;
rgb[1] = p;
rgb[2] = q;
break;
}
}
}
}

@ -0,0 +1,88 @@
package com.jme3.scene.plugins.blender.meshes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.jme3.scene.Geometry;
/**
* Class that holds information about the mesh.
*
* @author Marcin Roguski (Kaelthas)
*/
public class MeshContext {
/** A map between material index and the geometry. */
private Map<Integer, List<Geometry>> geometries = new HashMap<Integer, List<Geometry>>();
/** The vertex reference map. */
private Map<Integer, Map<Integer, List<Integer>>> vertexReferenceMap;
/**
* Adds a geometry for the specified material index.
* @param materialIndex
* the material index
* @param geometry
* the geometry
*/
public void putGeometry(Integer materialIndex, Geometry geometry) {
List<Geometry> geomList = geometries.get(materialIndex);
if (geomList == null) {
geomList = new ArrayList<Geometry>();
geometries.put(materialIndex, geomList);
}
geomList.add(geometry);
}
/**
* @param materialIndex
* the material index
* @return vertices amount that is used by mesh with the specified material
*/
public int getVertexCount(int materialIndex) {
int result = 0;
for (Geometry geometry : geometries.get(materialIndex)) {
result += geometry.getVertexCount();
}
return result;
}
/**
* Returns material index for the geometry.
* @param geometry
* the geometry
* @return material index
* @throws IllegalStateException
* this exception is thrown when no material is found for the specified geometry
*/
public int getMaterialIndex(Geometry geometry) {
for (Entry<Integer, List<Geometry>> entry : geometries.entrySet()) {
for (Geometry g : entry.getValue()) {
if (g.equals(geometry)) {
return entry.getKey();
}
}
}
throw new IllegalStateException("Cannot find material index for the given geometry: " + geometry);
}
/**
* This method returns the vertex reference map.
*
* @return the vertex reference map
*/
public Map<Integer, List<Integer>> getVertexReferenceMap(int materialIndex) {
return vertexReferenceMap.get(materialIndex);
}
/**
* This method sets the vertex reference map.
*
* @param vertexReferenceMap
* the vertex reference map
*/
public void setVertexReferenceMap(Map<Integer, Map<Integer, List<Integer>>> vertexReferenceMap) {
this.vertexReferenceMap = vertexReferenceMap;
}
}

@ -0,0 +1,243 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.meshes;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.asset.BlenderKey.FeaturesToLoad;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Mesh.Mode;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Format;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.meshes.builders.MeshBuilder;
import com.jme3.scene.plugins.blender.objects.Properties;
import com.jme3.scene.plugins.blender.textures.TextureHelper;
import com.jme3.util.BufferUtils;
/**
* A class that is used in mesh calculations.
*
* @author Marcin Roguski (Kaelthas)
*/
public class MeshHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(MeshHelper.class.getName());
/** A type of UV data layer in traditional faced mesh (triangles or quads). */
public static final int UV_DATA_LAYER_TYPE_FMESH = 5;
/** A type of UV data layer in bmesh type. */
public static final int UV_DATA_LAYER_TYPE_BMESH = 16;
/** A material used for single lines and points. */
private Material blackUnshadedMaterial;
/**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
* versions.
*
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public MeshHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* This method reads converts the given structure into mesh. The given structure needs to be filled with the appropriate data.
*
* @param structure
* the structure we read the mesh from
* @return the mesh feature
* @throws BlenderFileException
*/
@SuppressWarnings("unchecked")
public List<Geometry> toMesh(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
List<Geometry> geometries = (List<Geometry>) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
if (geometries != null) {
List<Geometry> copiedGeometries = new ArrayList<Geometry>(geometries.size());
for (Geometry geometry : geometries) {
copiedGeometries.add(geometry.clone());
}
return copiedGeometries;
}
String name = structure.getName();
MeshContext meshContext = new MeshContext();
LOGGER.log(Level.FINE, "Reading mesh: {0}.", name);
LOGGER.fine("Loading materials.");
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
MaterialContext[] materials = null;
if ((blenderContext.getBlenderKey().getFeaturesToLoad() & FeaturesToLoad.MATERIALS) != 0) {
materials = materialHelper.getMaterials(structure, blenderContext);
}
LOGGER.fine("Reading vertices.");
MeshBuilder meshBuilder = new MeshBuilder(structure, materials, blenderContext);
if (meshBuilder.isEmpty()) {
LOGGER.fine("The geometry is empty.");
geometries = new ArrayList<Geometry>(0);
blenderContext.addLoadedFeatures(structure.getOldMemoryAddress(), structure.getName(), structure, geometries);
blenderContext.setMeshContext(structure.getOldMemoryAddress(), meshContext);
return geometries;
}
meshContext.setVertexReferenceMap(meshBuilder.getVertexReferenceMap());
LOGGER.fine("Reading vertices groups (from the Object structure).");
Structure parent = blenderContext.peekParent();
Structure defbase = (Structure) parent.getFieldValue("defbase");
List<Structure> defs = defbase.evaluateListBase();
String[] verticesGroups = new String[defs.size()];
int defIndex = 0;
for (Structure def : defs) {
verticesGroups[defIndex++] = def.getFieldValue("name").toString();
}
LOGGER.fine("Reading custom properties.");
Properties properties = this.loadProperties(structure, blenderContext);
LOGGER.fine("Generating meshes.");
Map<Integer, List<Mesh>> meshes = meshBuilder.buildMeshes();
geometries = new ArrayList<Geometry>(meshes.size());
for (Entry<Integer, List<Mesh>> meshEntry : meshes.entrySet()) {
int materialIndex = meshEntry.getKey();
for (Mesh mesh : meshEntry.getValue()) {
LOGGER.fine("Preparing the result part.");
Geometry geometry = new Geometry(name + (geometries.size() + 1), mesh);
if (properties != null && properties.getValue() != null) {
this.applyProperties(geometry, properties);
}
geometries.add(geometry);
meshContext.putGeometry(materialIndex, geometry);
}
}
// store the data in blender context before applying the material
blenderContext.addLoadedFeatures(structure.getOldMemoryAddress(), structure.getName(), structure, geometries);
blenderContext.setMeshContext(structure.getOldMemoryAddress(), meshContext);
// apply materials only when all geometries are in place
if (materials != null) {
for (Geometry geometry : geometries) {
int materialNumber = meshContext.getMaterialIndex(geometry);
if (materialNumber < 0) {
geometry.setMaterial(this.getBlackUnshadedMaterial(blenderContext));
} else if (materials[materialNumber] != null) {
LinkedHashMap<String, List<Vector2f>> uvCoordinates = meshBuilder.getUVCoordinates(materialNumber);
MaterialContext materialContext = materials[materialNumber];
materialContext.applyMaterial(geometry, structure.getOldMemoryAddress(), uvCoordinates, blenderContext);
} else {
geometry.setMaterial(blenderContext.getDefaultMaterial());
LOGGER.warning("The importer came accross mesh that points to a null material. Default material is used to prevent loader from crashing, " + "but the model might look not the way it should. Sometimes blender does not assign materials properly. " + "Enter the edit mode and assign materials once more to your faces.");
}
}
} else {
// add UV coordinates if they are defined even if the material is not applied to the model
List<VertexBuffer> uvCoordsBuffer = null;
if (meshBuilder.hasUVCoordinates()) {
Map<String, List<Vector2f>> uvs = meshBuilder.getUVCoordinates(0);
if (uvs != null && uvs.size() > 0) {
uvCoordsBuffer = new ArrayList<VertexBuffer>(uvs.size());
int uvIndex = 0;
for (Entry<String, List<Vector2f>> entry : uvs.entrySet()) {
VertexBuffer buffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[uvIndex++]);
buffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(entry.getValue().toArray(new Vector2f[uvs.size()])));
uvCoordsBuffer.add(buffer);
}
}
}
for (Geometry geometry : geometries) {
Mode mode = geometry.getMesh().getMode();
if (mode != Mode.Triangles && mode != Mode.TriangleFan && mode != Mode.TriangleStrip) {
geometry.setMaterial(this.getBlackUnshadedMaterial(blenderContext));
} else {
Material defaultMaterial = blenderContext.getDefaultMaterial();
if(geometry.getMesh().getBuffer(Type.Color) != null) {
defaultMaterial = defaultMaterial.clone();
defaultMaterial.setBoolean("VertexColor", true);
}
geometry.setMaterial(defaultMaterial);
}
if (uvCoordsBuffer != null) {
for (VertexBuffer buffer : uvCoordsBuffer) {
geometry.getMesh().setBuffer(buffer);
}
}
}
}
return geometries;
}
/**
* Tells if the given mesh structure supports BMesh.
*
* @param meshStructure
* the mesh structure
* @return <b>true</b> if BMesh is supported and <b>false</b> otherwise
*/
public boolean isBMeshCompatible(Structure meshStructure) {
Pointer pMLoop = (Pointer) meshStructure.getFieldValue("mloop");
Pointer pMPoly = (Pointer) meshStructure.getFieldValue("mpoly");
return pMLoop != null && pMPoly != null && pMLoop.isNotNull() && pMPoly.isNotNull();
}
private synchronized Material getBlackUnshadedMaterial(BlenderContext blenderContext) {
if (blackUnshadedMaterial == null) {
blackUnshadedMaterial = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
blackUnshadedMaterial.setColor("Color", ColorRGBA.Black);
}
return blackUnshadedMaterial;
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save