new gradle-based structure. git-svn-id: https://jmonkeyengine.googlecode.com/svn/branches/gradle-restructure@10964 75d07b2b-3a1a-0410-a2c5-0572b91ccdcaexperimental
commit
ed77d40c63
@ -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) { |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -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…
Reference in new issue