Merge remote-tracking branch 'upstream/master'

experimental
Daniel Johansson 10 years ago
commit 013f67ee89
  1. 1
      .gitignore
  2. 35
      .travis.yml
  3. 7
      CONTRIBUTING.md
  4. 6
      README.md
  5. 37
      build.gradle
  6. 13
      common-android-app.gradle
  7. 51
      common.gradle
  8. 12
      gradle.properties
  9. 40
      jme3-android-examples/build.gradle
  10. 20
      jme3-android-examples/src/main/AndroidManifest.xml
  11. 12
      jme3-android-examples/src/main/java/jme3test/android/TestChooserAndroid.java
  12. 6
      jme3-android-examples/src/main/res/values/strings.xml
  13. 1166
      jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
  14. 13
      jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java
  15. 2
      jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java
  16. 5
      jme3-android/src/main/java/com/jme3/audio/android/AndroidMediaPlayerAudioRenderer.java
  17. 274
      jme3-android/src/main/java/com/jme3/input/android/AndroidGestureProcessor.java
  18. 686
      jme3-android/src/main/java/com/jme3/input/android/AndroidInput.java
  19. 503
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java
  20. 159
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java
  21. 238
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java
  22. 108
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java
  23. 416
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoystickJoyInput14.java
  24. 140
      jme3-android/src/main/java/com/jme3/input/android/AndroidKeyHandler.java
  25. 17
      jme3-android/src/main/java/com/jme3/input/android/AndroidKeyMapping.java
  26. 200
      jme3-android/src/main/java/com/jme3/input/android/AndroidSensorJoyInput.java
  27. 257
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler.java
  28. 475
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java
  29. 76
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput14.java
  30. 20
      jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java
  31. 2566
      jme3-android/src/main/java/com/jme3/renderer/android/OGLESShaderRenderer.java
  32. 22
      jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
  33. 12
      jme3-android/src/main/java/jme3test/android/DemoAndroidHarness.java
  34. 5
      jme3-blender/build.gradle
  35. 312
      jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java
  36. 138
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/AbstractBlenderHelper.java
  37. 91
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  38. 263
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java
  39. 71
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderModelLoader.java
  40. 7
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationHelper.java
  41. 12
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BlenderAction.java
  42. 11
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BoneContext.java
  43. 13
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java
  44. 15
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/cameras/CameraHelper.java
  45. 4
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/Constraint.java
  46. 69
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SimulationNode.java
  47. 6
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinition.java
  48. 12
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionFactory.java
  49. 261
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionIK.java
  50. 45
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/BezierCurve.java
  51. 106
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/FileBlockHeader.java
  52. 3
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Structure.java
  53. 2
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/landscape/LandscapeHelper.java
  54. 8
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/lights/LightHelper.java
  55. 25
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java
  56. 10
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialHelper.java
  57. 128
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/DQuaternion.java
  58. 26
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/DTransform.java
  59. 214
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/Matrix.java
  60. 13
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java
  61. 70
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/ObjectHelper.java
  62. 8
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/CombinedTexture.java
  63. 11
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/ImageUtils.java
  64. 96
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TextureHelper.java
  65. 27
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TriangulatedTexture.java
  66. 18
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/AbstractTextureBlender.java
  67. 4
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderAWT.java
  68. 5
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderDDS.java
  69. 7
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderFactory.java
  70. 4
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderLuminance.java
  71. 18
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/io/AWTPixelInputOutput.java
  72. 2
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/io/PixelIOFactory.java
  73. 2
      jme3-bullet-native/build.gradle
  74. BIN
      jme3-bullet-native/libs/native/windows/x86/bulletjme.dll
  75. BIN
      jme3-bullet-native/libs/native/windows/x86_64/bulletjme.dll
  76. 73
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_PhysicsSpace.cpp
  77. 17
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_PhysicsSpace.h
  78. 90
      jme3-bullet-native/src/native/cpp/jmeBulletUtil.cpp
  79. 3
      jme3-bullet-native/src/native/cpp/jmeBulletUtil.h
  80. 79
      jme3-bullet-native/src/native/cpp/jmeClasses.cpp
  81. 12
      jme3-bullet-native/src/native/cpp/jmeClasses.h
  82. 12
      jme3-bullet/src/common/java/com/jme3/bullet/BulletAppState.java
  83. 2
      jme3-bullet/src/common/java/com/jme3/bullet/control/RigidBodyControl.java
  84. 103
      jme3-bullet/src/main/java/com/jme3/bullet/PhysicsSpace.java
  85. 27
      jme3-core/build.gradle
  86. 1
      jme3-core/src/main/java/com/jme3/animation/AnimChannel.java
  87. 20
      jme3-core/src/main/java/com/jme3/animation/Bone.java
  88. 19
      jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
  89. 24
      jme3-core/src/main/java/com/jme3/app/StatsView.java
  90. 2
      jme3-core/src/main/java/com/jme3/asset/AssetConfig.java
  91. 5
      jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java
  92. 10
      jme3-core/src/main/java/com/jme3/asset/ImplHandler.java
  93. 2
      jme3-core/src/main/java/com/jme3/asset/ShaderNodeDefinitionKey.java
  94. 6
      jme3-core/src/main/java/com/jme3/asset/cache/WeakRefCloneAssetCache.java
  95. 10
      jme3-core/src/main/java/com/jme3/audio/AudioNode.java
  96. 1
      jme3-core/src/main/java/com/jme3/audio/AudioRenderer.java
  97. 5
      jme3-core/src/main/java/com/jme3/audio/AudioSource.java
  98. 13
      jme3-core/src/main/java/com/jme3/audio/AudioStream.java
  99. 65
      jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java
  100. 3
      jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java
  101. Some files were not shown because too many files have changed in this diff Show More

1
.gitignore vendored

@ -11,6 +11,7 @@
/jme3-desktop/build/ /jme3-desktop/build/
/jme3-android-native/build/ /jme3-android-native/build/
/jme3-android/build/ /jme3-android/build/
/jme3-android-examples/build/
/jme3-blender/build/ /jme3-blender/build/
/jme3-effects/build/ /jme3-effects/build/
/jme3-bullet/build/ /jme3-bullet/build/

@ -1,12 +1,41 @@
language: java language: java
# jdk: sudo: false
# - oraclejdk8 env:
- GRADLE_USER_HOME=gradle-cache
cache:
directories:
- gradle-cache
- netbeans
branches: branches:
only: only:
- master - master
before_install: notifications:
slack:
on_success: change
on_failure: always
rooms:
secure: "PWEk4+VL986c3gAjWp12nqyifvxCjBqKoESG9d7zWh1uiTLadTHhZJRMdsye36FCpz/c/Jt7zCRO/5y7FaubQptnRrkrRfjp5f99MJRzQVXnUAM+y385qVkXKRKd/PLpM7XPm4AvjvxHCyvzX2wamRvul/TekaXKB9Ti5FCN87s="
install:
- ./gradlew assemble
script:
- ./gradlew check
- ./gradlew createZipDistribution
deploy:
provider: releases
api_key:
secure: "KbFiMt0a8FxUKvCJUYwikLYaqqGMn1p6k4OsXnGqwptQZEUIayabNLHeaD2kTNT3e6AY1ETwQLff/lB2LttmIo4g5NWW63g1K3A/HwgnhJwETengiProZ/Udl+ugPeDL/+ar43HUhFq4knBnzFKnEcHAThTPVqH/RMDvZf1UUYI="
file: build/distributions/jME3.1.0_snapshot-github_2015-06-20.zip
skip_cleanup: true
on:
tags: true
# before_install:
# required libs for android build tools # required libs for android build tools
# sudo apt-get update # sudo apt-get update
# sudo apt-get install -qq p7zip-full # sudo apt-get install -qq p7zip-full

@ -16,13 +16,6 @@ When you're ready to submit your code, just make a [pull request](https://help.g
- When committing, always be sure to run an update before you commit. If there is a conflict between the latest revision and your patch after the update, then it is your responsibility to track down the update that caused the conflict and determine the issue (and fix it). In the case where the breaking commit has no thread linked (and one cannot be found in the forum), then the contributor should contact an administrator and wait for feedback before committing. - When committing, always be sure to run an update before you commit. If there is a conflict between the latest revision and your patch after the update, then it is your responsibility to track down the update that caused the conflict and determine the issue (and fix it). In the case where the breaking commit has no thread linked (and one cannot be found in the forum), then the contributor should contact an administrator and wait for feedback before committing.
- If your code is committed and it introduces new functionality, please edit the wiki accordingly. We can easily roll back to previous revisions, so just do your best; point us to it and we’ll see if it sticks! - If your code is committed and it introduces new functionality, please edit the wiki accordingly. We can easily roll back to previous revisions, so just do your best; point us to it and we’ll see if it sticks!
**Note to Eclipse users:** The Eclipse [git client does not support https](http://hub.jmonkeyengine.org/forum/topic/problem-cloning-the-new-git-repository/#post-265594). The current workaround is to use the command line to clone the repository.
To import the local repository as a project follow these steps:
1. Add a line 'apply plugin: eclipse' to your common.gradle file in the main project directory.
2. Navigate to the project directory in command line and execute command 'gradle eclipse'. This will load all the dependancies for eclipse.
3. In Eclipse, add the repository as an existing Java Project.
p.s. We will try hold ourselves to a [certain standard](http://www.defmacro.org/2013/04/03/issue-etiquette.html) when it comes to GitHub etiquette. If at any point we fail to uphold this standard, let us know. p.s. We will try hold ourselves to a [certain standard](http://www.defmacro.org/2013/04/03/issue-etiquette.html) when it comes to GitHub etiquette. If at any point we fail to uphold this standard, let us know.
#### Core Contributors #### Core Contributors

@ -1,7 +1,9 @@
jMonkeyEngine jMonkeyEngine
============= =============
jMonkeyEngine is a 3D game engine for adventurous Java developers. It’s open source, cross platform and cutting edge. And it is all beautifully documented. The 3.0 branch is the latest stable version of the jMonkeyEngine 3 SDK, a complete game development suite. We'll be frequently submitting stable 3.0.x updates until the major 3.1 version arrives in Q4 2014. [![Build Status](https://travis-ci.org/jMonkeyEngine/jmonkeyengine.svg?branch=master)](https://travis-ci.org/jMonkeyEngine/jmonkeyengine)
jMonkeyEngine is a 3D game engine for adventurous Java developers. It’s open source, cross platform and cutting edge. And it is all beautifully documented. The 3.0 branch is the latest stable version of the jMonkeyEngine 3 SDK, a complete game development suite. We'll be frequently submitting stable 3.0.x updates until the major 3.1 version arrives.
The engine is used by several commercial game studios and computer-science courses. Here's a taste: The engine is used by several commercial game studios and computer-science courses. Here's a taste:

@ -1,14 +1,23 @@
import org.gradle.api.artifacts.* import org.gradle.api.artifacts.*
apply plugin: 'base' // To add "clean" task to the root project. buildscript {
//apply plugin: 'java-library-distribution' repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.1.0'
}
}
apply plugin: 'base'
// This is applied to all sub projects // This is applied to all sub projects
subprojects { subprojects {
// Don't add to native builds if(!project.name.equals('jme3-android-examples')) {
// if(!project.name.endsWith('native')){ apply from: rootProject.file('common.gradle')
apply from: rootProject.file('common.gradle') } else {
// } apply from: rootProject.file('common-android-app.gradle')
}
} }
task run(dependsOn: ':jme3-examples:run') { task run(dependsOn: ':jme3-examples:run') {
@ -166,11 +175,11 @@ ext {
// } // }
//} //}
allprojects { //allprojects {
tasks.withType(JavaExec) { // tasks.withType(JavaExec) {
enableAssertions = true // false by default // enableAssertions = true // false by default
} // }
tasks.withType(Test) { // tasks.withType(Test) {
enableAssertions = true // true by default // enableAssertions = true // true by default
} // }
} //}

@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
group = 'com.jme3'
version = jmeVersion + '-' + jmeVersionTag
sourceCompatibility = '1.6'
repositories {
mavenCentral()
maven {
url "http://nifty-gui.sourceforge.net/nifty-maven-repo"
}
}

@ -4,16 +4,17 @@
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'maven' apply plugin: 'maven'
apply plugin: 'maven-publish'
String mavenGroupId = 'com.jme3' group = 'com.jme3'
String mavenVersion = jmeVersion + '-' + jmeVersionTag //'-SNAPSHOT' version = jmeVersion + '-' + jmeVersionTag
sourceCompatibility = '1.6' sourceCompatibility = '1.6'
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
repositories { repositories {
mavenCentral() mavenCentral()
maven{ maven {
url "http://nifty-gui.sourceforge.net/nifty-maven-repo" url "http://nifty-gui.sourceforge.net/nifty-maven-repo"
} }
} }
@ -23,11 +24,6 @@ dependencies {
testCompile group: 'junit', name: 'junit', version: '4.10' testCompile group: 'junit', name: 'junit', version: '4.10'
} }
String mavenArtifactId = name
group = mavenGroupId
version = mavenVersion
javadoc { javadoc {
failOnError = false failOnError = false
options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED
@ -60,11 +56,40 @@ artifacts {
} }
} }
configure(install.repositories.mavenInstaller) { publishing {
pom.project { publications {
groupId = mavenGroupId maven(MavenPublication) {
artifactId = mavenArtifactId from components.java
version = mavenVersion artifact sourcesJar
artifact javadocJar
pom.withXml {
asNode().children().last() + {
resolveStrategy = Closure.DELEGATE_FIRST
name POM_NAME
description POM_DESCRIPTION
url POM_URL
scm {
url POM_SCM_URL
connection POM_SCM_CONNECTION
developerConnection POM_SCM_DEVELOPER_CONNECTION
}
licenses {
license {
name POM_LICENSE_NAME
url POM_LICENSE_URL
distribution POM_LICENSE_DISTRIBUTION
}
}
}
}
}
}
repositories {
maven {
url "${rootProject.buildDir}/repo" // change to point to your repo, e.g. http://my.org/repo
}
} }
} }

@ -11,6 +11,7 @@ buildJavaDoc = true
# specify if SDK and Native libraries get built # specify if SDK and Native libraries get built
buildSdkProject = true buildSdkProject = true
buildNativeProjects = false buildNativeProjects = false
buildAndroidExamples = false
# Path to android NDK for building native libraries # Path to android NDK for building native libraries
#ndkPath=/Users/normenhansen/Documents/Code-Import/android-ndk-r7 #ndkPath=/Users/normenhansen/Documents/Code-Import/android-ndk-r7
@ -23,3 +24,14 @@ bulletZipFile = bullet.zip
# Path for downloading NetBeans Base # Path for downloading NetBeans Base
netbeansUrl = http://download.netbeans.org/netbeans/8.0.2/final/zip/netbeans-8.0.2-201411181905-javase.zip netbeansUrl = http://download.netbeans.org/netbeans/8.0.2/final/zip/netbeans-8.0.2-201411181905-javase.zip
# POM settings
POM_NAME=jMonkeyEngine
POM_DESCRIPTION=jMonkeyEngine is a 3D game engine for adventurous Java developers
POM_URL=http://jmonkeyengine.org
POM_SCM_URL=https://github.com/jMonkeyEngine/jmonkeyengine
POM_SCM_CONNECTION=scm:git:git://github.com/jMonkeyEngine/jmonkeyengine.git
POM_SCM_DEVELOPER_CONNECTION=scm:git:git@github.com:jMonkeyEngine/jmonkeyengine.git
POM_LICENSE_NAME=New BSD (3-clause) License
POM_LICENSE_URL=http://opensource.org/licenses/BSD-3-Clause
POM_LICENSE_DISTRIBUTION=repo

@ -0,0 +1,40 @@
dependencies {
compile project(':jme3-core')
compile project(':jme3-android')
compile project(':jme3-effects')
compile project(':jme3-bullet')
compile project(':jme3-bullet-native-android')
compile project(':jme3-networking')
compile project(':jme3-niftygui')
compile project(':jme3-plugins')
compile project(':jme3-terrain')
compile project(':jme3-testdata')
}
android {
compileSdkVersion 10
buildToolsVersion "22.0.1"
lintOptions {
// Fix nifty gui referencing "java.awt" package.
disable 'InvalidPackage'
}
defaultConfig {
applicationId "com.jme3.android"
minSdkVersion 10 // Android 2.3 GINGERBREAD
targetSdkVersion 22 // Android 5.1 LOLLIPOP
versionCode 1
versionName "1.0" // TODO: from settings.gradle
}
buildTypes {
release {
minifyEnabled false
}
debug {
applicationIdSuffix ".debug"
debuggable true
}
}
}

@ -0,0 +1,20 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jme3.android">
<!-- 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:label="@string/app_name" android:allowBackup="true">
<activity android:name="jme3test.android.TestChooserAndroid"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

@ -0,0 +1,12 @@
package jme3test.android;
import android.content.pm.ActivityInfo;
import android.app.*;
import android.os.Bundle;
public class TestChooserAndroid extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

@ -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>

File diff suppressed because it is too large Load Diff

@ -195,19 +195,6 @@ public class AndroidHarnessFragment extends Fragment implements
*/ */
protected String exitDialogMessage = "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 * 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 to the value of the Resource ID (i.e. R.drawable.picname). If

@ -74,7 +74,7 @@ public class VideoRecorderAppState extends AbstractAppState {
public Thread newThread(Runnable r) { public Thread newThread(Runnable r) {
Thread th = new Thread(r); Thread th = new Thread(r);
th.setName("jME Video Processing Thread"); th.setName("jME3 Video Processor");
th.setDaemon(true); th.setDaemon(true);
return th; return th;
} }

@ -525,4 +525,9 @@ public class AndroidMediaPlayerAudioRenderer implements AudioRenderer,
@Override @Override
public void deleteFilter(Filter filter) { public void deleteFilter(Filter filter) {
} }
@Override
public float getSourcePlaybackTime(AudioSource src) {
throw new UnsupportedOperationException("Not supported yet.");
}
} }

@ -35,314 +35,240 @@ package com.jme3.input.android;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.ScaleGestureDetector; 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 com.jme3.input.event.TouchEvent;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* AndroidGestureHandler uses Gesture type listeners to create jME TouchEvents * AndroidGestureHandler uses Gesture type listeners to create jME TouchEvents
* for gestures. This class is designed to handle the gestures supported * for gestures. This class is designed to handle the gestures supported
* on Android rev 9 (Android 2.3). Extend this class to add functionality * on Android rev 9 (Android 2.3). Extend this class to add functionality
* added by Android after rev 9. * added by Android after rev 9.
* *
* @author iwgeric * @author iwgeric
*/ */
public class AndroidGestureHandler implements public class AndroidGestureProcessor implements
GestureDetector.OnGestureListener, GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener, GestureDetector.OnDoubleTapListener,
ScaleGestureDetector.OnScaleGestureListener { ScaleGestureDetector.OnScaleGestureListener {
private static final Logger logger = Logger.getLogger(AndroidGestureHandler.class.getName()); private static final Logger logger = Logger.getLogger(AndroidGestureProcessor.class.getName());
private AndroidInputHandler androidInput;
private GestureDetector gestureDetector; private AndroidTouchInput touchInput;
private ScaleGestureDetector scaleDetector;
float gestureDownX = -1f; float gestureDownX = -1f;
float gestureDownY = -1f; float gestureDownY = -1f;
float scaleStartX = -1f; float scaleStartX = -1f;
float scaleStartY = -1f; float scaleStartY = -1f;
public AndroidGestureHandler(AndroidInputHandler androidInput) { public AndroidGestureProcessor(AndroidTouchInput touchInput) {
this.androidInput = androidInput; this.touchInput = touchInput;
}
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 */ /* Events from onGestureListener */
@Override
public boolean onDown(MotionEvent event) { public boolean onDown(MotionEvent event) {
// start of all GestureListeners. Not really a gesture by itself // start of all GestureListeners. Not really a gesture by itself
// so we don't create an event. // so we don't create an event.
// However, reset the scaleInProgress here since this is the beginning // However, reset the scaleInProgress here since this is the beginning
// of a series of gesture events. // of a series of gesture events.
// logger.log(Level.INFO, "onDown pointerId: {0}, action: {1}, x: {2}, y: {3}", // logger.log(Level.INFO, "onDown pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); // new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
gestureDownX = androidInput.getJmeX(event.getX()); gestureDownX = touchInput.getJmeX(event.getX());
gestureDownY = androidInput.invertY(androidInput.getJmeY(event.getY())); gestureDownY = touchInput.invertY(touchInput.getJmeY(event.getY()));
return true; return true;
} }
@Override
public boolean onSingleTapUp(MotionEvent event) { public boolean onSingleTapUp(MotionEvent event) {
// Up of single tap. May be followed by a double tap later. // Up of single tap. May be followed by a double tap later.
// use onSingleTapConfirmed instead. // use onSingleTapConfirmed instead.
// logger.log(Level.INFO, "onSingleTapUp pointerId: {0}, action: {1}, x: {2}, y: {3}", // logger.log(Level.INFO, "onSingleTapUp pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); // new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
return true; return true;
} }
@Override
public void onShowPress(MotionEvent event) { public void onShowPress(MotionEvent event) {
// logger.log(Level.INFO, "onShowPress pointerId: {0}, action: {1}, x: {2}, y: {3}", // logger.log(Level.INFO, "onShowPress pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); // new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
float jmeX = androidInput.getJmeX(event.getX()); float jmeX = touchInput.getJmeX(event.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY())); float jmeY = touchInput.invertY(touchInput.getJmeY(event.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent(); TouchEvent touchEvent = touchInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SHOWPRESS, jmeX, jmeY, 0, 0); touchEvent.set(TouchEvent.Type.SHOWPRESS, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(getPointerId(event)); touchEvent.setPointerId(touchInput.getPointerId(event));
touchEvent.setTime(event.getEventTime()); touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure()); touchEvent.setPressure(event.getPressure());
processEvent(touchEvent); touchInput.addEvent(touchEvent);
} }
@Override
public void onLongPress(MotionEvent event) { public void onLongPress(MotionEvent event) {
// logger.log(Level.INFO, "onLongPress pointerId: {0}, action: {1}, x: {2}, y: {3}", // logger.log(Level.INFO, "onLongPress pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); // new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
float jmeX = androidInput.getJmeX(event.getX()); float jmeX = touchInput.getJmeX(event.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY())); float jmeY = touchInput.invertY(touchInput.getJmeY(event.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent(); TouchEvent touchEvent = touchInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.LONGPRESSED, jmeX, jmeY, 0, 0); touchEvent.set(TouchEvent.Type.LONGPRESSED, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(getPointerId(event)); touchEvent.setPointerId(touchInput.getPointerId(event));
touchEvent.setTime(event.getEventTime()); touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure()); touchEvent.setPressure(event.getPressure());
processEvent(touchEvent); touchInput.addEvent(touchEvent);
} }
@Override
public boolean onScroll(MotionEvent startEvent, MotionEvent endEvent, float distX, float distY) { public boolean onScroll(MotionEvent startEvent, MotionEvent endEvent, float distX, float distY) {
// if not scaleInProgess, send scroll events. This is to avoid sending // 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. // scroll events when one of the fingers is lifted just before the other one.
// Avoids sending the scroll for that brief period of time. // 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. // Return true so that the next event doesn't accumulate the distX and distY values.
// Apparantly, both distX and distY are negative. // Apparantly, both distX and distY are negative.
// Negate distX to get the real value, but leave distY negative to compensate // 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. // for the fact that jME has y=0 at bottom where Android has y=0 at top.
// if (!scaleInProgress) { if (!touchInput.getScaleDetector().isInProgress()) {
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}",
// 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[]{touchInput.getPointerId(startEvent), touchInput.getAction(startEvent), startEvent.getX(), startEvent.getY(), touchInput.getAction(endEvent), endEvent.getX(), endEvent.getY(), distX, distY});
// 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 jmeX = touchInput.getJmeX(endEvent.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(endEvent.getY())); float jmeY = touchInput.invertY(touchInput.getJmeY(endEvent.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent(); TouchEvent touchEvent = touchInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SCROLL, jmeX, jmeY, androidInput.getJmeX(-distX), androidInput.getJmeY(distY)); touchEvent.set(TouchEvent.Type.SCROLL, jmeX, jmeY, touchInput.getJmeX(-distX), touchInput.getJmeY(distY));
touchEvent.setPointerId(getPointerId(endEvent)); touchEvent.setPointerId(touchInput.getPointerId(endEvent));
touchEvent.setTime(endEvent.getEventTime()); touchEvent.setTime(endEvent.getEventTime());
touchEvent.setPressure(endEvent.getPressure()); touchEvent.setPressure(endEvent.getPressure());
processEvent(touchEvent); touchInput.addEvent(touchEvent);
} }
return true; return true;
} }
@Override
public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float velocityX, float velocityY) { 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 happens only once at the end of the gesture (all fingers up).
// Fling returns the velocity of the finger movement in pixels/sec. // Fling returns the velocity of the finger movement in pixels/sec.
// Therefore, the dX and dY values are actually velocity instead of distance values // 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. // 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()); // logger.log(Level.INFO, "onFling pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, velocityX: {7}, velocityY: {8}",
float jmeY = androidInput.invertY(androidInput.getJmeY(startEvent.getY())); // new Object[]{touchInput.getPointerId(startEvent), touchInput.getAction(startEvent), startEvent.getX(), startEvent.getY(), touchInput.getAction(endEvent), endEvent.getX(), endEvent.getY(), velocityX, velocityY});
TouchEvent touchEvent = androidInput.getFreeTouchEvent();
float jmeX = touchInput.getJmeX(startEvent.getX());
float jmeY = touchInput.invertY(touchInput.getJmeY(startEvent.getY()));
TouchEvent touchEvent = touchInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.FLING, jmeX, jmeY, velocityX, velocityY); touchEvent.set(TouchEvent.Type.FLING, jmeX, jmeY, velocityX, velocityY);
touchEvent.setPointerId(getPointerId(endEvent)); touchEvent.setPointerId(touchInput.getPointerId(endEvent));
touchEvent.setTime(endEvent.getEventTime()); touchEvent.setTime(endEvent.getEventTime());
touchEvent.setPressure(endEvent.getPressure()); touchEvent.setPressure(endEvent.getPressure());
processEvent(touchEvent); touchInput.addEvent(touchEvent);
return true; return true;
} }
/* Events from onDoubleTapListener */ /* Events from onDoubleTapListener */
@Override
public boolean onSingleTapConfirmed(MotionEvent event) { public boolean onSingleTapConfirmed(MotionEvent event) {
// Up of single tap when no double tap followed. // Up of single tap when no double tap followed.
// logger.log(Level.INFO, "onSingleTapConfirmed pointerId: {0}, action: {1}, x: {2}, y: {3}", // logger.log(Level.INFO, "onSingleTapConfirmed pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); // new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
float jmeX = androidInput.getJmeX(event.getX()); float jmeX = touchInput.getJmeX(event.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY())); float jmeY = touchInput.invertY(touchInput.getJmeY(event.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent(); TouchEvent touchEvent = touchInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.TAP, jmeX, jmeY, 0, 0); touchEvent.set(TouchEvent.Type.TAP, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(getPointerId(event)); touchEvent.setPointerId(touchInput.getPointerId(event));
touchEvent.setTime(event.getEventTime()); touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure()); touchEvent.setPressure(event.getPressure());
processEvent(touchEvent); touchInput.addEvent(touchEvent);
return true; return true;
} }
@Override
public boolean onDoubleTap(MotionEvent event) { public boolean onDoubleTap(MotionEvent event) {
//The down motion event of the first tap of the double-tap //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 // We could use this event to fire off a double tap event, or use
// DoubleTapEvent with a check for the UP action // DoubleTapEvent with a check for the UP action
// logger.log(Level.INFO, "onDoubleTap pointerId: {0}, action: {1}, x: {2}, y: {3}", // logger.log(Level.INFO, "onDoubleTap pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); // new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
float jmeX = androidInput.getJmeX(event.getX()); float jmeX = touchInput.getJmeX(event.getX());
float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY())); float jmeY = touchInput.invertY(touchInput.getJmeY(event.getY()));
TouchEvent touchEvent = androidInput.getFreeTouchEvent(); TouchEvent touchEvent = touchInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.DOUBLETAP, jmeX, jmeY, 0, 0); touchEvent.set(TouchEvent.Type.DOUBLETAP, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(getPointerId(event)); touchEvent.setPointerId(touchInput.getPointerId(event));
touchEvent.setTime(event.getEventTime()); touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure()); touchEvent.setPressure(event.getPressure());
processEvent(touchEvent); touchInput.addEvent(touchEvent);
return true; return true;
} }
@Override
public boolean onDoubleTapEvent(MotionEvent event) { public boolean onDoubleTapEvent(MotionEvent event) {
//Notified when an event within a double-tap gesture occurs, including the down, move(s), and up events. //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 // 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}", // logger.log(Level.INFO, "onDoubleTapEvent pointerId: {0}, action: {1}, x: {2}, y: {3}",
// new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); // new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
// if (getAction(event) == MotionEvent.ACTION_UP) { if (touchInput.getAction(event) == MotionEvent.ACTION_UP) {
// TouchEvent touchEvent = touchEventPool.getNextFreeEvent(); TouchEvent touchEvent = touchInput.getFreeTouchEvent();
// touchEvent.set(TouchEvent.Type.DOUBLETAP, event.getX(), androidInput.invertY(event.getY()), 0, 0); touchEvent.set(TouchEvent.Type.DOUBLETAP, event.getX(), touchInput.invertY(event.getY()), 0, 0);
// touchEvent.setPointerId(getPointerId(event)); touchEvent.setPointerId(touchInput.getPointerId(event));
// touchEvent.setTime(event.getEventTime()); touchEvent.setTime(event.getEventTime());
// touchEvent.setPressure(event.getPressure()); touchEvent.setPressure(event.getPressure());
// processEvent(touchEvent); touchInput.addEvent(touchEvent);
// } }
return true; return true;
} }
/* Events from ScaleGestureDetector */ /* Events from ScaleGestureDetector */
@Override
public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
// Scale uses a focusX and focusY instead of x and y. Focus is the middle // 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 // 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. // 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 // return true or all gestures for this beginning event will be discarded
logger.log(Level.INFO, "onScaleBegin"); // logger.log(Level.INFO, "onScaleBegin");
scaleStartX = gestureDownX; scaleStartX = gestureDownX;
scaleStartY = gestureDownY; scaleStartY = gestureDownY;
TouchEvent touchEvent = androidInput.getFreeTouchEvent(); TouchEvent touchEvent = touchInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SCALE_START, scaleStartX, scaleStartY, 0f, 0f); touchEvent.set(TouchEvent.Type.SCALE_START, scaleStartX, scaleStartY, 0f, 0f);
touchEvent.setPointerId(0); touchEvent.setPointerId(0);
touchEvent.setTime(scaleGestureDetector.getEventTime()); touchEvent.setTime(scaleGestureDetector.getEventTime());
touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan()); touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
touchEvent.setDeltaScaleSpan(0f); touchEvent.setDeltaScaleSpan(0f);
touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor()); touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress()); touchEvent.setScaleSpanInProgress(touchInput.getScaleDetector().isInProgress());
processEvent(touchEvent); touchInput.addEvent(touchEvent);
return true; return true;
} }
@Override
public boolean onScale(ScaleGestureDetector scaleGestureDetector) { public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
// return true or all gestures for this event will be accumulated // return true or all gestures for this event will be accumulated
logger.log(Level.INFO, "onScale"); // logger.log(Level.INFO, "onScale");
scaleStartX = gestureDownX; scaleStartX = gestureDownX;
scaleStartY = gestureDownY; scaleStartY = gestureDownY;
TouchEvent touchEvent = androidInput.getFreeTouchEvent(); TouchEvent touchEvent = touchInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SCALE_MOVE, scaleStartX, scaleStartY, 0f, 0f); touchEvent.set(TouchEvent.Type.SCALE_MOVE, scaleStartX, scaleStartY, 0f, 0f);
touchEvent.setPointerId(0); touchEvent.setPointerId(0);
touchEvent.setTime(scaleGestureDetector.getEventTime()); touchEvent.setTime(scaleGestureDetector.getEventTime());
touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan()); touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan()); touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor()); touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress()); touchEvent.setScaleSpanInProgress(touchInput.getScaleDetector().isInProgress());
processEvent(touchEvent); touchInput.addEvent(touchEvent);
return true; return true;
} }
@Override
public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) { public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
logger.log(Level.INFO, "onScaleEnd"); // logger.log(Level.INFO, "onScaleEnd");
scaleStartX = gestureDownX; scaleStartX = gestureDownX;
scaleStartY = gestureDownY; scaleStartY = gestureDownY;
TouchEvent touchEvent = androidInput.getFreeTouchEvent(); TouchEvent touchEvent = touchInput.getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.SCALE_END, scaleStartX, scaleStartY, 0f, 0f); touchEvent.set(TouchEvent.Type.SCALE_END, scaleStartX, scaleStartY, 0f, 0f);
touchEvent.setPointerId(0); touchEvent.setPointerId(0);
touchEvent.setTime(scaleGestureDetector.getEventTime()); touchEvent.setTime(scaleGestureDetector.getEventTime());
touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan()); touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan()); touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor()); touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress()); touchEvent.setScaleSpanInProgress(touchInput.getScaleDetector().isInProgress());
processEvent(touchEvent); touchInput.addEvent(touchEvent);
} }
} }

@ -1,686 +0,0 @@
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 boolean isSimulateKeyboard() {
return keyboardEventsEnabled;
}
}

@ -1,265 +1,238 @@
/* /*
* Copyright (c) 2009-2012 jMonkeyEngine * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are * modification, are permitted provided that the following conditions are
* met: * met:
* *
* * Redistributions of source code must retain the above copyright * * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer. * notice, this list of conditions and the following disclaimer.
* *
* * Redistributions in binary form must reproduce the above copyright * * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the * notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution. * documentation and/or other materials provided with the distribution.
* *
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software * may be used to endorse or promote products derived from this software
* without specific prior written permission. * without specific prior written permission.
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package com.jme3.input.android; package com.jme3.input.android;
import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView;
import android.os.Build; import android.view.GestureDetector;
import android.view.View; import android.view.InputDevice;
import com.jme3.input.RawInputListener; import android.view.KeyEvent;
import com.jme3.input.TouchInput; import android.view.MotionEvent;
import com.jme3.input.event.InputEvent; import android.view.ScaleGestureDetector;
import com.jme3.input.event.KeyInputEvent; import android.view.View;
import com.jme3.input.event.MouseButtonEvent; import com.jme3.input.JoyInput;
import com.jme3.input.event.MouseMotionEvent; import com.jme3.input.TouchInput;
import com.jme3.input.event.TouchEvent; import com.jme3.system.AppSettings;
import com.jme3.system.AppSettings; import java.util.logging.Level;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Logger;
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 receives the inputs from the Android View and passes them
* <code>AndroidInput</code> is the main class that connects the Android system * to the appropriate classes based on the source of the input.</br>
* inputs to jME. It serves as the manager that gathers inputs from the various * This class is to be extended when new functionality is released in Android.
* Android input methods and provides them to jME's <code>InputManager</code>. *
* * @author iwgeric
* @author iwgeric */
*/ public class AndroidInputHandler implements View.OnTouchListener,
public class AndroidInputHandler implements TouchInput { View.OnKeyListener {
private static final Logger logger = Logger.getLogger(AndroidInputHandler.class.getName());
private static final Logger logger = Logger.getLogger(AndroidInputHandler.class.getName());
// Custom settings
private boolean mouseEventsEnabled = true; protected GLSurfaceView view;
private boolean mouseEventsInvertX = false; protected AndroidTouchInput touchInput;
private boolean mouseEventsInvertY = false; protected AndroidJoyInput joyInput;
private boolean keyboardEventsEnabled = false;
private boolean joystickEventsEnabled = false;
private boolean dontSendHistory = false; public AndroidInputHandler() {
touchInput = new AndroidTouchInput(this);
joyInput = new AndroidJoyInput(this);
// Internal }
private GLSurfaceView view;
private AndroidTouchHandler touchHandler; public void setView(View view) {
private AndroidKeyHandler keyHandler; if (this.view != null && view != null && this.view.equals(view)) {
private AndroidGestureHandler gestureHandler; return;
private boolean initialized = false; }
private RawInputListener listener = null;
private ConcurrentLinkedQueue<InputEvent> inputEventQueue = new ConcurrentLinkedQueue<InputEvent>(); if (this.view != null) {
private final static int MAX_TOUCH_EVENTS = 1024; removeListeners(this.view);
private final TouchEventPool touchEventPool = new TouchEventPool(MAX_TOUCH_EVENTS); }
private float scaleX = 1f;
private float scaleY = 1f; this.view = (GLSurfaceView)view;
if (this.view != null) {
public AndroidInputHandler() { addListeners(this.view);
int buildVersion = Build.VERSION.SDK_INT; }
logger.log(Level.INFO, "Android Build Version: {0}", buildVersion);
if (buildVersion >= 14) { joyInput.setView((GLSurfaceView)view);
// add support for onHover and GenericMotionEvent (ie. gamepads) }
gestureHandler = new AndroidGestureHandler(this);
touchHandler = new AndroidTouchHandler14(this, gestureHandler); public View getView() {
keyHandler = new AndroidKeyHandler(this); return view;
} else if (buildVersion >= 8){ }
gestureHandler = new AndroidGestureHandler(this);
touchHandler = new AndroidTouchHandler(this, gestureHandler); protected void removeListeners(GLSurfaceView view) {
keyHandler = new AndroidKeyHandler(this); view.setOnTouchListener(null);
} view.setOnKeyListener(null);
} touchInput.setGestureDetector(null);
touchInput.setScaleDetector(null);
public AndroidInputHandler(AndroidTouchHandler touchInput, }
AndroidKeyHandler keyInput, AndroidGestureHandler gestureHandler) {
this.touchHandler = touchInput; protected void addListeners(GLSurfaceView view) {
this.keyHandler = keyInput; view.setOnTouchListener(this);
this.gestureHandler = gestureHandler; view.setOnKeyListener(this);
} AndroidGestureProcessor gestureHandler = new AndroidGestureProcessor(touchInput);
touchInput.setGestureDetector(new GestureDetector(
public void setView(View view) { view.getContext(), gestureHandler));
if (touchHandler != null) { touchInput.setScaleDetector(new ScaleGestureDetector(
touchHandler.setView(view); view.getContext(), gestureHandler));
} }
if (keyHandler != null) {
keyHandler.setView(view); public void loadSettings(AppSettings settings) {
} touchInput.loadSettings(settings);
if (gestureHandler != null) { }
gestureHandler.setView(view);
} public TouchInput getTouchInput() {
this.view = (GLSurfaceView)view; return touchInput;
} }
public View getView() { public JoyInput getJoyInput() {
return view; return joyInput;
} }
public float invertX(float origX) { /*
return getJmeX(view.getWidth()) - origX; * Android input events include the source from which the input came from.
} * We must look at the source of the input event to determine which type
* of jME input it belongs to.
public float invertY(float origY) { * If the input is from a gamepad or joystick source, the event is sent
return getJmeY(view.getHeight()) - origY; * to the JoyInput class to convert the event into jME joystick events.
} * </br>
* If the input is from a touchscreen source, the event is sent to the
public float getJmeX(float origX) { * TouchProcessor to convert the event into touch events.
return origX * scaleX; * The TouchProcessor also converts the events into Mouse and Key events
} * if AppSettings is set to simulate Mouse or Keyboard events.
*
public float getJmeY(float origY) { * Android reports the source as a bitmask as shown below.</br>
return origY * scaleY; *
} * InputDevice Sources
* 0000 0000 0000 0000 0000 0000 0000 0000 - 32 bit bitmask
public void loadSettings(AppSettings settings) { *
keyboardEventsEnabled = settings.isEmulateKeyboard(); * 0000 0000 0000 0000 0000 0000 1111 1111 - SOURCE_CLASS_MASK (0x000000ff)
mouseEventsEnabled = settings.isEmulateMouse(); * 0000 0000 0000 0000 0000 0000 0000 0000 - SOURCE_CLASS_NONE (0x00000000)
mouseEventsInvertX = settings.isEmulateMouseFlipX(); * 0000 0000 0000 0000 0000 0000 0000 0001 - SOURCE_CLASS_BUTTON (0x00000001)
mouseEventsInvertY = settings.isEmulateMouseFlipY(); * 0000 0000 0000 0000 0000 0000 0000 0010 - SOURCE_CLASS_POINTER (0x00000002)
joystickEventsEnabled = settings.useJoysticks(); * 0000 0000 0000 0000 0000 0000 0000 0100 - SOURCE_CLASS_TRACKBALL (0x00000004)
* 0000 0000 0000 0000 0000 0000 0000 1000 - SOURCE_CLASS_POSITION (0x00000008)
// view width and height are 0 until the view is displayed on the screen * 0000 0000 0000 0000 0000 0000 0001 0000 - SOURCE_CLASS_JOYSTICK (0x00000010)
if (view.getWidth() != 0 && view.getHeight() != 0) { *
scaleX = (float)settings.getWidth() / (float)view.getWidth(); * 1111 1111 1111 1111 1111 1111 0000 0000 - Source_Any (0xffffff00)
scaleY = (float)settings.getHeight() / (float)view.getHeight(); * 0000 0000 0000 0000 0000 0000 0000 0000 - SOURCE_UNKNOWN (0x00000000)
} * 0000 0000 0000 0000 0000 0001 0000 0001 - SOURCE_KEYBOARD (0x00000101)
logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}", * 0000 0000 0000 0000 0000 0010 0000 0001 - SOURCE_DPAD (0x00000201)
new Object[]{scaleX, scaleY}); * 0000 0000 0000 0000 0000 0100 0000 0001 - SOURCE_GAMEPAD (0x00000401)
* 0000 0000 0000 0000 0001 0000 0000 0010 - SOURCE_TOUCHSCREEN (0x00001002)
} * 0000 0000 0000 0000 0010 0000 0000 0010 - SOURCE_MOUSE (0x00002002)
* 0000 0000 0000 0000 0100 0000 0000 0010 - SOURCE_STYLUS (0x00004002)
// ----------------------------------------- * 0000 0000 0000 0001 0000 0000 0000 0100 - SOURCE_TRACKBALL (0x00010004)
// JME3 Input interface * 0000 0000 0001 0000 0000 0000 0000 1000 - SOURCE_TOUCHPAD (0x00100008)
@Override * 0000 0000 0010 0000 0000 0000 0000 0000 - SOURCE_TOUCH_NAVIGATION (0x00200000)
public void initialize() { * 0000 0001 0000 0000 0000 0000 0001 0000 - SOURCE_JOYSTICK (0x01000010)
touchEventPool.initialize(); * 0000 0010 0000 0000 0000 0000 0000 0001 - SOURCE_HDMI (0x02000001)
if (touchHandler != null) { *
touchHandler.initialize(); * Example values reported by Android for Source
} * 4,098 = 0x00001002 =
if (keyHandler != null) { * 0000 0000 0000 0000 0001 0000 0000 0010 - SOURCE_CLASS_POINTER
keyHandler.initialize(); * SOURCE_TOUCHSCREEN
} * 1,281 = 0x00000501 =
if (gestureHandler != null) { * 0000 0000 0000 0000 0000 0101 0000 0001 - SOURCE_CLASS_BUTTON
gestureHandler.initialize(); * SOURCE_KEYBOARD
} * SOURCE_GAMEPAD
* 16,777,232 = 0x01000010 =
initialized = true; * 0000 0001 0000 0000 0000 0000 0001 0000 - SOURCE_CLASS_JOYSTICK
} * SOURCE_JOYSTICK
*
@Override * 16,778,513 = 0x01000511 =
public void destroy() { * 0000 0001 0000 0000 0000 0101 0001 0001 - SOURCE_CLASS_BUTTON
initialized = false; * SOURCE_CLASS_JOYSTICK
* SOURCE_GAMEPAD
touchEventPool.destroy(); * SOURCE_KEYBOARD
if (touchHandler != null) { * SOURCE_JOYSTICK
touchHandler.destroy(); *
} * 257 = 0x00000101 =
if (keyHandler != null) { * 0000 0000 0000 0000 0000 0001 0000 0001 - SOURCE_CLASS_BUTTON
keyHandler.destroy(); * SOURCE_KEYBOARD
} *
if (gestureHandler != null) { *
gestureHandler.destroy(); *
} */
setView(null);
} @Override
public boolean onTouch(View view, MotionEvent event) {
@Override if (view != getView()) {
public boolean isInitialized() { return false;
return initialized; }
}
boolean consumed = false;
@Override
public void setInputListener(RawInputListener listener) { int source = event.getSource();
this.listener = listener; // logger.log(Level.INFO, "onTouch source: {0}", source);
}
boolean isTouch = ((source & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN);
@Override // logger.log(Level.INFO, "onTouch source: {0}, isTouch: {1}",
public long getInputTimeNanos() { // new Object[]{source, isTouch});
return System.nanoTime();
} if (isTouch && touchInput != null) {
// send the event to the touch processor
public void update() { consumed = touchInput.onTouch(event);
if (listener != null) { }
InputEvent inputEvent;
return consumed;
while ((inputEvent = inputEventQueue.poll()) != null) {
if (inputEvent instanceof TouchEvent) { }
listener.onTouchEvent((TouchEvent)inputEvent);
} else if (inputEvent instanceof MouseButtonEvent) { @Override
listener.onMouseButtonEvent((MouseButtonEvent)inputEvent); public boolean onKey(View view, int keyCode, KeyEvent event) {
} else if (inputEvent instanceof MouseMotionEvent) { if (view != getView()) {
listener.onMouseMotionEvent((MouseMotionEvent)inputEvent); return false;
} else if (inputEvent instanceof KeyInputEvent) { }
listener.onKeyEvent((KeyInputEvent)inputEvent);
} boolean consumed = false;
}
} int source = event.getSource();
} // logger.log(Level.INFO, "onKey source: {0}", source);
// ----------------------------------------- boolean isTouch =
((source & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN) ||
public TouchEvent getFreeTouchEvent() { ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD);
return touchEventPool.getNextFreeEvent(); // logger.log(Level.INFO, "onKey source: {0}, isTouch: {1}",
} // new Object[]{source, isTouch});
public void addEvent(InputEvent event) { if (touchInput != null) {
inputEventQueue.add(event); consumed = touchInput.onKey(event);
if (event instanceof TouchEvent) { }
touchEventPool.storeEvent((TouchEvent)event);
} return consumed;
}
}
public void setSimulateMouse(boolean simulate) {
this.mouseEventsEnabled = simulate; }
}
public boolean isSimulateMouse() {
return mouseEventsEnabled;
}
public boolean isMouseEventsInvertX() {
return mouseEventsInvertX;
}
public boolean isMouseEventsInvertY() {
return mouseEventsInvertY;
}
public void setSimulateKeyboard(boolean simulate) {
this.keyboardEventsEnabled = simulate;
}
public boolean isSimulateKeyboard() {
return keyboardEventsEnabled;
}
public void setOmitHistoricEvents(boolean dontSendHistory) {
this.dontSendHistory = dontSendHistory;
}
}

@ -0,0 +1,159 @@
/*
* 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.opengl.GLSurfaceView;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <code>AndroidInputHandler14</code> extends <code>AndroidInputHandler</code> to
* add the onHover and onGenericMotion events that where added in Android rev 14 (Android 4.0).</br>
* The onGenericMotion events are the main interface to Joystick axes. They
* were actually released in Android rev 12.
*
* @author iwgeric
*/
public class AndroidInputHandler14 extends AndroidInputHandler implements View.OnHoverListener,
View.OnGenericMotionListener {
private static final Logger logger = Logger.getLogger(AndroidInputHandler14.class.getName());
public AndroidInputHandler14() {
touchInput = new AndroidTouchInput14(this);
joyInput = new AndroidJoyInput14(this);
}
@Override
protected void removeListeners(GLSurfaceView view) {
super.removeListeners(view);
view.setOnHoverListener(null);
view.setOnGenericMotionListener(null);
}
@Override
protected void addListeners(GLSurfaceView view) {
super.addListeners(view);
view.setOnHoverListener(this);
view.setOnGenericMotionListener(this);
}
@Override
public boolean onHover(View view, MotionEvent event) {
if (view != getView()) {
return false;
}
boolean consumed = false;
int source = event.getSource();
// logger.log(Level.INFO, "onTouch source: {0}", source);
boolean isTouch = ((source & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN);
// logger.log(Level.INFO, "onTouch source: {0}, isTouch: {1}",
// new Object[]{source, isTouch});
if (isTouch && touchInput != null) {
// send the event to the touch processor
consumed = ((AndroidTouchInput14)touchInput).onHover(event);
}
return consumed;
}
@Override
public boolean onGenericMotion(View view, MotionEvent event) {
if (view != getView()) {
return false;
}
boolean consumed = false;
int source = event.getSource();
// logger.log(Level.INFO, "onGenericMotion source: {0}", source);
boolean isJoystick =
((source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
((source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK);
if (isJoystick && joyInput != null) {
// logger.log(Level.INFO, "onGenericMotion source: {0}, isJoystick: {1}",
// new Object[]{source, isJoystick});
// send the event to the touch processor
consumed = consumed || ((AndroidJoyInput14)joyInput).onGenericMotion(event);
}
return consumed;
}
@Override
public boolean onKey(View view, int keyCode, KeyEvent event) {
if (view != getView()) {
return false;
}
boolean consumed = false;
// logger.log(Level.INFO, "onKey keyCode: {0}, action: {1}, event: {2}",
// new Object[]{KeyEvent.keyCodeToString(keyCode), event.getAction(), event});
int source = event.getSource();
// logger.log(Level.INFO, "onKey source: {0}", source);
boolean isTouch =
((source & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN) ||
((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD);
boolean isJoystick =
((source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
((source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK);
if (isTouch && touchInput != null) {
// logger.log(Level.INFO, "onKey source: {0}, isTouch: {1}",
// new Object[]{source, isTouch});
consumed = touchInput.onKey(event);
}
if (isJoystick && joyInput != null) {
// logger.log(Level.INFO, "onKey source: {0}, isJoystick: {1}",
// new Object[]{source, isJoystick});
// use inclusive OR to make sure the onKey method is called.
consumed = consumed | ((AndroidJoyInput14)joyInput).onKey(event);
}
return consumed;
}
}

@ -0,0 +1,238 @@
/*
* Copyright (c) 2009-2015 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.opengl.GLSurfaceView;
import android.os.Vibrator;
import com.jme3.input.InputManager;
import com.jme3.input.JoyInput;
import com.jme3.input.Joystick;
import com.jme3.input.RawInputListener;
import com.jme3.input.event.InputEvent;
import com.jme3.input.event.JoyAxisEvent;
import com.jme3.input.event.JoyButtonEvent;
import com.jme3.system.AppSettings;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Main class that manages various joystick devices. Joysticks can be many forms
* including a simulated joystick to communicate the device orientation as well
* as physical gamepads. </br>
* This class manages all the joysticks and feeds the inputs from each back
* to jME's InputManager.
*
* This handler 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 or gamepads
* are 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 AndroidJoyInput implements JoyInput {
private static final Logger logger = Logger.getLogger(AndroidJoyInput.class.getName());
public static boolean disableSensors = false;
protected AndroidInputHandler inputHandler;
protected List<Joystick> joystickList = new ArrayList<Joystick>();
// private boolean dontSendHistory = false;
// Internal
private boolean initialized = false;
private RawInputListener listener = null;
private ConcurrentLinkedQueue<InputEvent> eventQueue = new ConcurrentLinkedQueue<InputEvent>();
private AndroidSensorJoyInput sensorJoyInput;
private Vibrator vibrator = null;
private boolean vibratorActive = false;
private long maxRumbleTime = 250; // 250ms
public AndroidJoyInput(AndroidInputHandler inputHandler) {
this.inputHandler = inputHandler;
sensorJoyInput = new AndroidSensorJoyInput(this);
}
public void setView(GLSurfaceView view) {
if (view == null) {
vibrator = null;
} else {
// Get instance of Vibrator from current Context
vibrator = (Vibrator) view.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator == null) {
logger.log(Level.FINE, "Vibrator Service not found.");
}
}
if (sensorJoyInput != null) {
sensorJoyInput.setView(view);
}
}
public void loadSettings(AppSettings settings) {
}
public void addEvent(InputEvent event) {
eventQueue.add(event);
}
/**
* Pauses the joystick device listeners to save battery life if they are not needed.
* Used to pause when the activity pauses
*/
public void pauseJoysticks() {
if (sensorJoyInput != null) {
sensorJoyInput.pauseSensors();
}
if (vibrator != null && vibratorActive) {
vibrator.cancel();
}
}
/**
* Resumes the joystick device listeners.
* Used to resume when the activity comes to the top of the stack
*/
public void resumeJoysticks() {
if (sensorJoyInput != null) {
sensorJoyInput.resumeSensors();
}
}
@Override
public void initialize() {
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void destroy() {
initialized = false;
if (sensorJoyInput != null) {
sensorJoyInput.destroy();
}
setView(null);
}
@Override
public void setInputListener(RawInputListener listener) {
this.listener = listener;
}
@Override
public long getInputTimeNanos() {
return System.nanoTime();
}
@Override
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;
}
}
}
@Override
public Joystick[] loadJoysticks(InputManager inputManager) {
logger.log(Level.INFO, "loading joysticks for {0}", this.getClass().getName());
if (!disableSensors) {
joystickList.add(sensorJoyInput.loadJoystick(joystickList.size(), inputManager));
}
return joystickList.toArray( new Joystick[joystickList.size()] );
}
@Override
public void update() {
if (sensorJoyInput != null) {
sensorJoyInput.update();
}
if (listener != null) {
InputEvent inputEvent;
while ((inputEvent = eventQueue.poll()) != null) {
if (inputEvent instanceof JoyAxisEvent) {
listener.onJoyAxisEvent((JoyAxisEvent)inputEvent);
} else if (inputEvent instanceof JoyButtonEvent) {
listener.onJoyButtonEvent((JoyButtonEvent)inputEvent);
}
}
}
}
}

@ -0,0 +1,108 @@
/*
* Copyright (c) 2009-2015 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.KeyEvent;
import android.view.MotionEvent;
import com.jme3.input.InputManager;
import com.jme3.input.Joystick;
import java.util.logging.Logger;
/**
* <code>AndroidJoyInput14</code> extends <code>AndroidJoyInput</code>
* to include support for physical joysticks/gamepads.</br>
*
* @author iwgeric
*/
public class AndroidJoyInput14 extends AndroidJoyInput {
private static final Logger logger = Logger.getLogger(AndroidJoyInput14.class.getName());
private AndroidJoystickJoyInput14 joystickJoyInput;
public AndroidJoyInput14(AndroidInputHandler inputHandler) {
super(inputHandler);
joystickJoyInput = new AndroidJoystickJoyInput14(this);
}
/**
* Pauses the joystick device listeners to save battery life if they are not needed.
* Used to pause when the activity pauses
*/
@Override
public void pauseJoysticks() {
super.pauseJoysticks();
if (joystickJoyInput != null) {
joystickJoyInput.pauseJoysticks();
}
}
/**
* Resumes the joystick device listeners.
* Used to resume when the activity comes to the top of the stack
*/
@Override
public void resumeJoysticks() {
super.resumeJoysticks();
if (joystickJoyInput != null) {
joystickJoyInput.resumeJoysticks();
}
}
@Override
public void destroy() {
super.destroy();
if (joystickJoyInput != null) {
joystickJoyInput.destroy();
}
}
@Override
public Joystick[] loadJoysticks(InputManager inputManager) {
// load the simulated joystick for device orientation
super.loadJoysticks(inputManager);
// load physical gamepads/joysticks
joystickList.addAll(joystickJoyInput.loadJoysticks(joystickList.size(), inputManager));
// return the list of joysticks back to InputManager
return joystickList.toArray( new Joystick[joystickList.size()] );
}
public boolean onGenericMotion(MotionEvent event) {
return joystickJoyInput.onGenericMotion(event);
}
public boolean onKey(KeyEvent event) {
return joystickJoyInput.onKey(event);
}
}

@ -0,0 +1,416 @@
/*
* Copyright (c) 2009-2015 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.InputDevice;
import android.view.InputDevice.MotionRange;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import com.jme3.input.AbstractJoystick;
import com.jme3.input.DefaultJoystickAxis;
import com.jme3.input.DefaultJoystickButton;
import com.jme3.input.InputManager;
import com.jme3.input.JoyInput;
import com.jme3.input.Joystick;
import com.jme3.input.JoystickAxis;
import com.jme3.input.JoystickButton;
import com.jme3.input.JoystickCompatibilityMappings;
import com.jme3.input.event.JoyAxisEvent;
import com.jme3.input.event.JoyButtonEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Main class that creates and manages Android inputs for physical gamepads/joysticks.
*
* @author iwgeric
*/
public class AndroidJoystickJoyInput14 {
private static final Logger logger = Logger.getLogger(AndroidJoystickJoyInput14.class.getName());
private boolean loaded = false;
private AndroidJoyInput joyInput;
private Map<Integer, AndroidJoystick> joystickIndex = new HashMap<Integer, AndroidJoystick>();
private static int[] AndroidGamepadButtons = {
// Dpad buttons
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_DPAD_CENTER,
// pressing joystick down
KeyEvent.KEYCODE_BUTTON_THUMBL, KeyEvent.KEYCODE_BUTTON_THUMBR,
// buttons
KeyEvent.KEYCODE_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y,
// buttons on back of device
KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_BUTTON_R1,
KeyEvent.KEYCODE_BUTTON_L2, KeyEvent.KEYCODE_BUTTON_R2,
// start / select buttons
KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_BUTTON_MODE,
};
public AndroidJoystickJoyInput14(AndroidJoyInput joyInput) {
this.joyInput = joyInput;
}
public void pauseJoysticks() {
}
public void resumeJoysticks() {
}
public void destroy() {
}
public List<Joystick> loadJoysticks(int joyId, InputManager inputManager) {
logger.log(Level.INFO, "loading Joystick devices");
ArrayList<Joystick> joysticks = new ArrayList<Joystick>();
joysticks.clear();
joystickIndex.clear();
ArrayList gameControllerDeviceIds = new ArrayList();
int[] deviceIds = InputDevice.getDeviceIds();
for (int deviceId : deviceIds) {
InputDevice dev = InputDevice.getDevice(deviceId);
int sources = dev.getSources();
logger.log(Level.FINE, "deviceId[{0}] sources: {1}", new Object[]{deviceId, sources});
// Verify that the device has gamepad buttons, control sticks, or both.
if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) {
// This device is a game controller. Store its device ID.
if (!gameControllerDeviceIds.contains(deviceId)) {
gameControllerDeviceIds.add(deviceId);
logger.log(Level.FINE, "Attempting to create joystick for device: {0}", dev);
// Create an AndroidJoystick and store the InputDevice so we
// can later correspond the input from the InputDevice to the
// appropriate jME Joystick event
AndroidJoystick joystick = new AndroidJoystick(inputManager,
joyInput,
dev,
joyId+joysticks.size(),
dev.getName());
joystickIndex.put(deviceId, joystick);
joysticks.add(joystick);
// Each analog input is reported as a MotionRange
// The axis number corresponds to the type of axis
// The AndroidJoystick.addAxis(MotionRange) converts the axis
// type reported by Android into the jME Joystick axis
List<MotionRange> motionRanges = dev.getMotionRanges();
for (MotionRange motionRange: motionRanges) {
logger.log(Level.INFO, "motion range: {0}", motionRange.toString());
logger.log(Level.INFO, "axis: {0}", motionRange.getAxis());
JoystickAxis axis = joystick.addAxis(motionRange);
logger.log(Level.INFO, "added axis: {0}", axis);
}
// InputDevice has a method for determining if a keyCode is
// supported (InputDevice public boolean[] hasKeys (int... keys)).
// But this method wasn't added until rev 19 (Android 4.4)
// Therefore, we only can query the entire device and see if
// any InputDevice supports the keyCode. This may result in
// buttons being configured that don't exist on the specific
// device, but I haven't found a better way yet.
for (int keyCode: AndroidGamepadButtons) {
logger.log(Level.INFO, "button[{0}]: {1}",
new Object[]{keyCode, KeyCharacterMap.deviceHasKey(keyCode)});
if (KeyCharacterMap.deviceHasKey(keyCode)) {
// add button even though we aren't sure if the button
// actually exists on this InputDevice
logger.log(Level.INFO, "button[{0}] exists somewhere", keyCode);
JoystickButton button = joystick.addButton(keyCode);
logger.log(Level.INFO, "added button: {0}", button);
}
}
}
}
}
loaded = true;
return joysticks;
}
public boolean onGenericMotion(MotionEvent event) {
boolean consumed = false;
// logger.log(Level.INFO, "onGenericMotion event: {0}", event);
event.getDeviceId();
event.getSource();
// logger.log(Level.INFO, "deviceId: {0}, source: {1}", new Object[]{event.getDeviceId(), event.getSource()});
AndroidJoystick joystick = joystickIndex.get(event.getDeviceId());
if (joystick != null) {
for (int androidAxis: joystick.getAndroidAxes()) {
String axisName = MotionEvent.axisToString(androidAxis);
float value = event.getAxisValue(androidAxis);
int action = event.getAction();
if (action == MotionEvent.ACTION_MOVE) {
// logger.log(Level.INFO, "MOVE axis num: {0}, axisName: {1}, value: {2}",
// new Object[]{androidAxis, axisName, value});
JoystickAxis axis = joystick.getAxis(androidAxis);
if (axis != null) {
// logger.log(Level.INFO, "MOVE axis num: {0}, axisName: {1}, value: {2}, deadzone: {3}",
// new Object[]{androidAxis, axisName, value, axis.getDeadZone()});
JoyAxisEvent axisEvent = new JoyAxisEvent(axis, value);
joyInput.addEvent(axisEvent);
consumed = true;
} else {
// logger.log(Level.INFO, "axis was null for axisName: {0}", axisName);
}
} else {
// logger.log(Level.INFO, "action: {0}", action);
}
}
}
return consumed;
}
public boolean onKey(KeyEvent event) {
boolean consumed = false;
// logger.log(Level.INFO, "onKey event: {0}", event);
event.getDeviceId();
event.getSource();
AndroidJoystick joystick = joystickIndex.get(event.getDeviceId());
if (joystick != null) {
JoystickButton button = joystick.getButton(event.getKeyCode());
boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
if (button != null) {
JoyButtonEvent buttonEvent = new JoyButtonEvent(button, pressed);
joyInput.addEvent(buttonEvent);
consumed = true;
} else {
JoystickButton newButton = joystick.addButton(event.getKeyCode());
JoyButtonEvent buttonEvent = new JoyButtonEvent(newButton, pressed);
joyInput.addEvent(buttonEvent);
consumed = true;
}
}
return consumed;
}
protected class AndroidJoystick extends AbstractJoystick {
private JoystickAxis nullAxis;
private InputDevice device;
private JoystickAxis xAxis;
private JoystickAxis yAxis;
private JoystickAxis povX;
private JoystickAxis povY;
private Map<Integer, JoystickAxis> axisIndex = new HashMap<Integer, JoystickAxis>();
private Map<Integer, JoystickButton> buttonIndex = new HashMap<Integer, JoystickButton>();
public AndroidJoystick( InputManager inputManager, JoyInput joyInput, InputDevice device,
int joyId, String name ) {
super( inputManager, joyInput, joyId, name );
this.device = device;
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 JoystickAxis getAxis(int androidAxis) {
return axisIndex.get(androidAxis);
}
protected Set<Integer> getAndroidAxes() {
return axisIndex.keySet();
}
protected JoystickButton getButton(int keyCode) {
return buttonIndex.get(keyCode);
}
protected JoystickButton addButton( int keyCode ) {
// logger.log(Level.FINE, "Adding button: {0}", keyCode);
String name = KeyEvent.keyCodeToString(keyCode);
String original = KeyEvent.keyCodeToString(keyCode);
// A/B/X/Y buttons
if (keyCode == KeyEvent.KEYCODE_BUTTON_Y) {
original = JoystickButton.BUTTON_0;
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_B) {
original = JoystickButton.BUTTON_1;
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_A) {
original = JoystickButton.BUTTON_2;
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_X) {
original = JoystickButton.BUTTON_3;
// Front buttons Some of these have the top ones and the bottoms ones flipped.
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_L1) {
original = JoystickButton.BUTTON_4;
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_R1) {
original = JoystickButton.BUTTON_5;
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_L2) {
original = JoystickButton.BUTTON_6;
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_R2) {
original = JoystickButton.BUTTON_7;
// // Dpad buttons
// } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
// original = JoystickButton.BUTTON_8;
// } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
// original = JoystickButton.BUTTON_9;
// } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
// original = JoystickButton.BUTTON_8;
// } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
// original = JoystickButton.BUTTON_9;
// Select and start buttons
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_SELECT) {
original = JoystickButton.BUTTON_8;
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_START) {
original = JoystickButton.BUTTON_9;
// Joystick push buttons
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_THUMBL) {
original = JoystickButton.BUTTON_10;
} else if (keyCode == KeyEvent.KEYCODE_BUTTON_THUMBR) {
original = JoystickButton.BUTTON_11;
}
String logicalId = JoystickCompatibilityMappings.remapComponent( getName(), original );
if( logicalId == null ? original != null : !logicalId.equals(original) ) {
logger.log(Level.FINE, "Remapped: {0} to: {1}",
new Object[]{original, logicalId});
}
JoystickButton button = new DefaultJoystickButton( getInputManager(), this, getButtonCount(),
name, logicalId );
addButton(button);
buttonIndex.put( keyCode, button );
return button;
}
protected JoystickAxis addAxis(MotionRange motionRange) {
String name = MotionEvent.axisToString(motionRange.getAxis());
String original = MotionEvent.axisToString(motionRange.getAxis());
if (motionRange.getAxis() == MotionEvent.AXIS_X) {
original = JoystickAxis.X_AXIS;
} else if (motionRange.getAxis() == MotionEvent.AXIS_Y) {
original = JoystickAxis.Y_AXIS;
} else if (motionRange.getAxis() == MotionEvent.AXIS_Z) {
original = JoystickAxis.Z_AXIS;
} else if (motionRange.getAxis() == MotionEvent.AXIS_RZ) {
original = JoystickAxis.Z_ROTATION;
} else if (motionRange.getAxis() == MotionEvent.AXIS_HAT_X) {
original = JoystickAxis.POV_X;
} else if (motionRange.getAxis() == MotionEvent.AXIS_HAT_Y) {
original = JoystickAxis.POV_Y;
}
String logicalId = JoystickCompatibilityMappings.remapComponent( getName(), original );
if( logicalId == null ? original != null : !logicalId.equals(original) ) {
logger.log(Level.FINE, "Remapped: {0} to: {1}",
new Object[]{original, logicalId});
}
JoystickAxis axis = new DefaultJoystickAxis(getInputManager(),
this,
getAxisCount(),
name,
logicalId,
true,
true,
motionRange.getFlat());
if (motionRange.getAxis() == MotionEvent.AXIS_X) {
xAxis = axis;
}
if (motionRange.getAxis() == MotionEvent.AXIS_Y) {
yAxis = axis;
}
if (motionRange.getAxis() == MotionEvent.AXIS_HAT_X) {
povX = axis;
}
if (motionRange.getAxis() == MotionEvent.AXIS_HAT_Y) {
povY = axis;
}
addAxis(axis);
axisIndex.put(motionRange.getAxis(), axis);
return 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;
}
@Override
public int getXAxisIndex(){
return xAxis.getAxisId();
}
@Override
public int getYAxisIndex(){
return yAxis.getAxisId();
}
}
}

@ -1,140 +0,0 @@
/*
* 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);
}
if (androidInput.isSimulateKeyboard()) {
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;
}
}
}

@ -37,13 +37,14 @@ import java.util.logging.Logger;
/** /**
* AndroidKeyMapping is just a utility to convert the Android keyCodes into * AndroidKeyMapping is just a utility to convert the Android keyCodes into
* jME KeyCodes received in jME's KeyEvent will match between Desktop and Android. * jME KeyCodes so that events received in jME's KeyEvent will match between
* * Desktop and Android.
*
* @author iwgeric * @author iwgeric
*/ */
public class AndroidKeyMapping { public class AndroidKeyMapping {
private static final Logger logger = Logger.getLogger(AndroidKeyMapping.class.getName()); private static final Logger logger = Logger.getLogger(AndroidKeyMapping.class.getName());
private static final int[] ANDROID_TO_JME = { private static final int[] ANDROID_TO_JME = {
0x0, // unknown 0x0, // unknown
0x0, // key code soft left 0x0, // key code soft left
@ -141,9 +142,13 @@ public class AndroidKeyMapping {
0x0,//media fastforward 0x0,//media fastforward
0x0,//mute 0x0,//mute
}; };
public static int getJmeKey(int androidKey) { public static int getJmeKey(int androidKey) {
return ANDROID_TO_JME[androidKey]; if (androidKey > ANDROID_TO_JME.length) {
return androidKey;
} else {
return ANDROID_TO_JME[androidKey];
}
} }
} }

@ -37,7 +37,7 @@ import android.hardware.Sensor;
import android.hardware.SensorEvent; import android.hardware.SensorEvent;
import android.hardware.SensorEventListener; import android.hardware.SensorEventListener;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import android.os.Vibrator; import android.opengl.GLSurfaceView;
import android.view.Surface; import android.view.Surface;
import android.view.WindowManager; import android.view.WindowManager;
import com.jme3.input.AbstractJoystick; import com.jme3.input.AbstractJoystick;
@ -47,10 +47,8 @@ import com.jme3.input.JoyInput;
import com.jme3.input.Joystick; import com.jme3.input.Joystick;
import com.jme3.input.JoystickAxis; import com.jme3.input.JoystickAxis;
import com.jme3.input.SensorJoystickAxis; import com.jme3.input.SensorJoystickAxis;
import com.jme3.input.RawInputListener;
import com.jme3.input.event.JoyAxisEvent; import com.jme3.input.event.JoyAxisEvent;
import com.jme3.math.FastMath; import com.jme3.math.FastMath;
import com.jme3.system.android.JmeAndroidSystem;
import com.jme3.util.IntMap; import com.jme3.util.IntMap;
import com.jme3.util.IntMap.Entry; import com.jme3.util.IntMap.Entry;
import java.util.ArrayList; import java.util.ArrayList;
@ -63,7 +61,7 @@ import java.util.logging.Logger;
* A single joystick is configured and includes data for all configured sensors * A single joystick is configured and includes data for all configured sensors
* as seperate axes of the joystick. * as seperate axes of the joystick.
* *
* Each axis is named accounting to the static strings in SensorJoystickAxis. * Each axis is named according to the static strings in SensorJoystickAxis.
* Refer to the strings defined in SensorJoystickAxis for a list of supported * Refer to the strings defined in SensorJoystickAxis for a list of supported
* sensors and their axis data. Each sensor type defined in SensorJoystickAxis * 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 * will be attempted to be configured. If the device does not support a particular
@ -72,46 +70,21 @@ import java.util.logging.Logger;
* The joystick.getXAxis and getYAxis methods of the joystick are configured to * 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. * 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 * @author iwgeric
*/ */
public class AndroidSensorJoyInput implements JoyInput, SensorEventListener { public class AndroidSensorJoyInput implements SensorEventListener {
private final static Logger logger = Logger.getLogger(AndroidSensorJoyInput.class.getName()); private final static Logger logger = Logger.getLogger(AndroidSensorJoyInput.class.getName());
private Context context = null; private AndroidJoyInput joyInput;
private InputManager inputManager = null;
private SensorManager sensorManager = null; private SensorManager sensorManager = null;
private WindowManager windowManager = null; private WindowManager windowManager = 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 IntMap<SensorData> sensors = new IntMap<SensorData>();
private AndroidJoystick[] joysticks;
private int lastRotation = 0; private int lastRotation = 0;
private boolean initialized = false;
private boolean loaded = false; private boolean loaded = false;
private final ArrayList<JoyAxisEvent> eventQueue = new ArrayList<JoyAxisEvent>(); public AndroidSensorJoyInput(AndroidJoyInput joyInput) {
this.joyInput = joyInput;
}
/** /**
* Internal class to enclose data for each sensor. * Internal class to enclose data for each sensor.
@ -120,10 +93,10 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
int androidSensorType = -1; int androidSensorType = -1;
int androidSensorSpeed = SensorManager.SENSOR_DELAY_GAME; int androidSensorSpeed = SensorManager.SENSOR_DELAY_GAME;
Sensor sensor = null; Sensor sensor = null;
int sensorAccuracy = 0; int sensorAccuracy = -1;
float[] lastValues; float[] lastValues;
final Object valuesLock = new Object(); final Object valuesLock = new Object();
ArrayList<AndroidJoystickAxis> axes = new ArrayList<AndroidJoystickAxis>(); ArrayList<AndroidSensorJoystickAxis> axes = new ArrayList<AndroidSensorJoystickAxis>();
boolean enabled = false; boolean enabled = false;
boolean haveData = false; boolean haveData = false;
@ -134,16 +107,19 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
} }
private void initSensorManager() { public void setView(GLSurfaceView view) {
this.context = JmeAndroidSystem.getView().getContext(); pauseSensors();
// Get instance of the WindowManager from the current Context if (sensorManager != null) {
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); sensorManager.unregisterListener(this);
// Get instance of the SensorManager from the current Context }
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); if (view == null) {
// Get instance of Vibrator from current Context windowManager = null;
vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); sensorManager = null;
if (vibrator == null) { } else {
logger.log(Level.FINE, "Vibrator Service not found."); // Get instance of the WindowManager from the current Context
windowManager = (WindowManager) view.getContext().getSystemService(Context.WINDOW_SERVICE);
// Get instance of the SensorManager from the current Context
sensorManager = (SensorManager) view.getContext().getSystemService(Context.SENSOR_SERVICE);
} }
} }
@ -222,9 +198,6 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
unRegisterListener(entry.getKey()); unRegisterListener(entry.getKey());
} }
} }
if (vibrator != null && vibratorActive) {
vibrator.cancel();
}
} }
/** /**
@ -333,7 +306,7 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
*/ */
private boolean updateOrientation() { private boolean updateOrientation() {
SensorData sensorData; SensorData sensorData;
AndroidJoystickAxis axis; AndroidSensorJoystickAxis axis;
final float[] curInclinationMat = new float[16]; final float[] curInclinationMat = new float[16];
final float[] curRotationMat = new float[16]; final float[] curRotationMat = new float[16];
final float[] rotatedRotationMat = new float[16]; final float[] rotatedRotationMat = new float[16];
@ -400,10 +373,8 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
if (!sensorData.haveData) { if (!sensorData.haveData) {
sensorData.haveData = true; sensorData.haveData = true;
} else { } else {
synchronized (eventQueue){ if (axis.isChanged()) {
if (axis.isChanged()) { joyInput.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
eventQueue.add(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
}
} }
} }
} }
@ -428,47 +399,14 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
// Start of JoyInput methods // Start of JoyInput methods
public void setJoyRumble(int joyId, float amount) { public Joystick loadJoystick(int joyId, InputManager inputManager) {
// 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; SensorData sensorData;
List<Joystick> list = new ArrayList<Joystick>(); AndroidSensorJoystickAxis axis;
AndroidJoystick joystick;
AndroidJoystickAxis axis;
joystick = new AndroidJoystick(inputManager, AndroidSensorJoystick joystick = new AndroidSensorJoystick(inputManager,
this, joyInput,
list.size(), joyId,
"AndroidSensorsJoystick"); "AndroidSensorsJoystick");
list.add(joystick);
List<Sensor> availSensors = sensorManager.getSensorList(Sensor.TYPE_ALL); List<Sensor> availSensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
for (Sensor sensor: availSensors) { for (Sensor sensor: availSensors) {
@ -555,14 +493,8 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
// } // }
joysticks = list.toArray( new AndroidJoystick[list.size()] );
loaded = true; loaded = true;
return joysticks; return joystick;
}
public void initialize() {
initialized = true;
loaded = false;
} }
public void update() { public void update() {
@ -570,15 +502,6 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
return; return;
} }
updateOrientation(); 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() { public void destroy() {
@ -588,39 +511,27 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
sensorManager.unregisterListener(this); sensorManager.unregisterListener(this);
} }
sensors.clear(); sensors.clear();
eventQueue.clear();
initialized = false;
loaded = false; loaded = false;
joysticks = null;
sensorManager = null; sensorManager = null;
vibrator = null;
context = 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 // Start of Android SensorEventListener methods
@Override
public void onSensorChanged(SensorEvent se) { public void onSensorChanged(SensorEvent se) {
if (!initialized || !loaded) { if (!loaded) {
return; return;
} }
// logger.log(Level.FINE, "onSensorChanged for {0}: accuracy: {1}, values: {2}",
// new Object[]{se.sensor.getName(), se.accuracy, se.values});
int sensorType = se.sensor.getType(); int sensorType = se.sensor.getType();
SensorData sensorData = sensors.get(sensorType); SensorData sensorData = sensors.get(sensorType);
if (sensorData != null) {
// logger.log(Level.FINE, "sensorData name: {0}, enabled: {1}, unreliable: {2}",
// new Object[]{sensorData.sensor.getName(), sensorData.enabled, sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE});
}
if (sensorData != null && sensorData.sensor.equals(se.sensor) && sensorData.enabled) { if (sensorData != null && sensorData.sensor.equals(se.sensor) && sensorData.enabled) {
if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) { if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
@ -632,8 +543,8 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
} }
} }
if (sensorData != null && sensorData.axes.size() > 0) { if (sensorData.axes.size() > 0) {
AndroidJoystickAxis axis; AndroidSensorJoystickAxis axis;
for (int i=0; i<se.values.length; i++) { for (int i=0; i<se.values.length; i++) {
axis = sensorData.axes.get(i); axis = sensorData.axes.get(i);
if (axis != null) { if (axis != null) {
@ -641,10 +552,11 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
if (!sensorData.haveData) { if (!sensorData.haveData) {
sensorData.haveData = true; sensorData.haveData = true;
} else { } else {
synchronized (eventQueue){ if (axis.isChanged()) {
if (axis.isChanged()) { JoyAxisEvent event = new JoyAxisEvent(axis, axis.getJoystickAxisValue());
eventQueue.add(new JoyAxisEvent(axis, axis.getJoystickAxisValue())); // logger.log(Level.INFO, "adding JoyAxisEvent: {0}", event);
} joyInput.addEvent(event);
// joyHandler.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
} }
} }
} }
@ -658,6 +570,7 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
} }
} }
@Override
public void onAccuracyChanged(Sensor sensor, int i) { public void onAccuracyChanged(Sensor sensor, int i) {
int sensorType = sensor.getType(); int sensorType = sensor.getType();
SensorData sensorData = sensors.get(sensorType); SensorData sensorData = sensors.get(sensorType);
@ -672,14 +585,14 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
// End of SensorEventListener methods // End of SensorEventListener methods
protected class AndroidJoystick extends AbstractJoystick { protected class AndroidSensorJoystick extends AbstractJoystick {
private JoystickAxis nullAxis; private JoystickAxis nullAxis;
private JoystickAxis xAxis; private JoystickAxis xAxis;
private JoystickAxis yAxis; private JoystickAxis yAxis;
private JoystickAxis povX; private JoystickAxis povX;
private JoystickAxis povY; private JoystickAxis povY;
public AndroidJoystick( InputManager inputManager, JoyInput joyInput, public AndroidSensorJoystick( InputManager inputManager, JoyInput joyInput,
int joyId, String name){ int joyId, String name){
super( inputManager, joyInput, joyId, name ); super( inputManager, joyInput, joyId, name );
@ -693,11 +606,11 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
} }
protected AndroidJoystickAxis addAxis(String axisName, String logicalName, int axisNum, float maxRawValue) { protected AndroidSensorJoystickAxis addAxis(String axisName, String logicalName, int axisNum, float maxRawValue) {
AndroidJoystickAxis axis; AndroidSensorJoystickAxis axis;
axis = new AndroidJoystickAxis( axis = new AndroidSensorJoystickAxis(
inputManager, // InputManager (InputManager) getInputManager(), // InputManager (InputManager)
this, // parent Joystick (Joystick) this, // parent Joystick (Joystick)
axisNum, // Axis Index (int) axisNum, // Axis Index (int)
axisName, // Axis Name (String) axisName, // Axis Name (String)
@ -741,7 +654,7 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
} }
public class AndroidJoystickAxis extends DefaultJoystickAxis implements SensorJoystickAxis { public class AndroidSensorJoystickAxis extends DefaultJoystickAxis implements SensorJoystickAxis {
float zeroRawValue = 0f; float zeroRawValue = 0f;
float curRawValue = 0f; float curRawValue = 0f;
float lastRawValue = 0f; float lastRawValue = 0f;
@ -749,7 +662,7 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
float maxRawValue = FastMath.HALF_PI; float maxRawValue = FastMath.HALF_PI;
boolean enabled = true; boolean enabled = true;
public AndroidJoystickAxis(InputManager inputManager, Joystick parent, public AndroidSensorJoystickAxis(InputManager inputManager, Joystick parent,
int axisIndex, String name, String logicalId, int axisIndex, String name, String logicalId,
boolean isAnalog, boolean isRelative, float deadZone, boolean isAnalog, boolean isRelative, float deadZone,
float maxRawValue) { float maxRawValue) {
@ -758,10 +671,12 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
this.maxRawValue = maxRawValue; this.maxRawValue = maxRawValue;
} }
@Override
public float getMaxRawValue() { public float getMaxRawValue() {
return maxRawValue; return maxRawValue;
} }
@Override
public void setMaxRawValue(float maxRawValue) { public void setMaxRawValue(float maxRawValue) {
this.maxRawValue = maxRawValue; this.maxRawValue = maxRawValue;
} }
@ -787,6 +702,7 @@ public class AndroidSensorJoyInput implements JoyInput, SensorEventListener {
return hasChanged; return hasChanged;
} }
@Override
public void calibrateCenter() { public void calibrateCenter() {
zeroRawValue = lastRawValue; zeroRawValue = lastRawValue;
logger.log(Level.FINE, "Calibrating axis {0} to {1}", logger.log(Level.FINE, "Calibrating axis {0} to {1}",

@ -1,257 +0,0 @@
/*
* 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,475 @@
/*
* 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.KeyEvent;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
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 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 com.jme3.system.AppSettings;
import java.util.HashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* AndroidTouchInput 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 AndroidTouchInput implements TouchInput {
private static final Logger logger = Logger.getLogger(AndroidTouchInput.class.getName());
private boolean mouseEventsEnabled = true;
private boolean mouseEventsInvertX = false;
private boolean mouseEventsInvertY = false;
private boolean keyboardEventsEnabled = false;
private boolean dontSendHistory = false;
protected int numPointers = 0;
final private HashMap<Integer, Vector2f> lastPositions = new HashMap<Integer, Vector2f>();
final 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;
private boolean initialized = false;
private RawInputListener listener = null;
private GestureDetector gestureDetector;
private ScaleGestureDetector scaleDetector;
protected AndroidInputHandler androidInput;
public AndroidTouchInput(AndroidInputHandler androidInput) {
this.androidInput = androidInput;
}
public GestureDetector getGestureDetector() {
return gestureDetector;
}
public void setGestureDetector(GestureDetector gestureDetector) {
this.gestureDetector = gestureDetector;
}
public ScaleGestureDetector getScaleDetector() {
return scaleDetector;
}
public void setScaleDetector(ScaleGestureDetector scaleDetector) {
this.scaleDetector = scaleDetector;
}
public float invertX(float origX) {
return getJmeX(androidInput.getView().getWidth()) - origX;
}
public float invertY(float origY) {
return getJmeY(androidInput.getView().getHeight()) - origY;
}
public float getJmeX(float origX) {
return origX * scaleX;
}
public float getJmeY(float origY) {
return origY * scaleY;
}
public void loadSettings(AppSettings settings) {
keyboardEventsEnabled = settings.isEmulateKeyboard();
mouseEventsEnabled = settings.isEmulateMouse();
mouseEventsInvertX = settings.isEmulateMouseFlipX();
mouseEventsInvertY = settings.isEmulateMouseFlipY();
// view width and height are 0 until the view is displayed on the screen
if (androidInput.getView().getWidth() != 0 && androidInput.getView().getHeight() != 0) {
scaleX = (float)settings.getWidth() / (float)androidInput.getView().getWidth();
scaleY = (float)settings.getHeight() / (float)androidInput.getView().getHeight();
}
logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}",
new Object[]{scaleX, scaleY});
}
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;
}
public boolean onTouch(MotionEvent event) {
if (!isInitialized()) {
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 = getJmeX(event.getX(pointerIndex));
jmeY = invertY(getJmeY(event.getY(pointerIndex)));
touch = 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);
addEvent(touch);
addEvent(generateMouseEvent(touch));
bWasHandled = true;
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
jmeX = getJmeX(event.getX(pointerIndex));
jmeY = invertY(getJmeY(event.getY(pointerIndex)));
touch = 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);
addEvent(touch);
addEvent(generateMouseEvent(touch));
bWasHandled = true;
break;
case MotionEvent.ACTION_MOVE:
// Convert all pointers into events
for (int p = 0; p < event.getPointerCount(); p++) {
jmeX = getJmeX(event.getX(p));
jmeY = invertY(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 = 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);
addEvent(touch);
addEvent(generateMouseEvent(touch));
bWasHandled = true;
}
}
break;
case MotionEvent.ACTION_OUTSIDE:
break;
}
// Try to detect gestures
if (gestureDetector != null) {
gestureDetector.onTouchEvent(event);
}
if (scaleDetector != null) {
scaleDetector.onTouchEvent(event);
}
return bWasHandled;
}
// TODO: Ring Buffer for mouse events?
public InputEvent generateMouseEvent(TouchEvent event) {
InputEvent inputEvent = null;
int newX;
int newY;
int newDX;
int newDY;
// MouseEvents do not support multi-touch, so only evaluate 1 finger pointer events
if (!isSimulateMouse() || numPointers > 1) {
return null;
}
if (isMouseEventsInvertX()) {
newX = (int) (invertX(event.getX()));
newDX = (int)event.getDeltaX() * -1;
} else {
newX = (int) event.getX();
newDX = (int)event.getDeltaX();
}
if (isMouseEventsInvertY()) {
newY = (int) (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;
}
public boolean onKey(KeyEvent event) {
if (!isInitialized()) {
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(event.getKeyCode());
evt.setCharacters(event.getCharacters());
evt.setTime(event.getEventTime());
// Send the event
addEvent(evt);
} else if (event.getAction() == KeyEvent.ACTION_UP) {
evt = new TouchEvent();
evt.set(TouchEvent.Type.KEY_UP);
evt.setKeyCode(event.getKeyCode());
evt.setCharacters(event.getCharacters());
evt.setTime(event.getEventTime());
// Send the event
addEvent(evt);
}
if (isSimulateKeyboard()) {
KeyInputEvent kie;
char unicodeChar = (char)event.getUnicodeChar();
int jmeKeyCode = AndroidKeyMapping.getJmeKey(event.getKeyCode());
boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
boolean repeating = pressed && event.getRepeatCount() > 0;
kie = new KeyInputEvent(jmeKeyCode, unicodeChar, pressed, repeating);
kie.setTime(event.getEventTime());
addEvent(kie);
// logger.log(Level.FINE, "onKey keyCode: {0}, jmeKeyCode: {1}, pressed: {2}, repeating: {3}",
// new Object[]{event.getKeyCode(), 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 ((event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP) ||
(event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_DOWN) ||
(event.getKeyCode() == KeyEvent.KEYCODE_MENU)) {
return false;
} else {
return true;
}
}
// -----------------------------------------
// JME3 Input interface
@Override
public void initialize() {
touchEventPool.initialize();
initialized = true;
}
@Override
public void destroy() {
initialized = false;
touchEventPool.destroy();
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void setInputListener(RawInputListener listener) {
this.listener = listener;
}
@Override
public long getInputTimeNanos() {
return System.nanoTime();
}
@Override
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) {
if (event == null) {
return;
}
logger.log(Level.INFO, "event: {0}", event);
inputEventQueue.add(event);
if (event instanceof TouchEvent) {
touchEventPool.storeEvent((TouchEvent)event);
}
}
@Override
public void setSimulateMouse(boolean simulate) {
this.mouseEventsEnabled = simulate;
}
@Override
public boolean isSimulateMouse() {
return mouseEventsEnabled;
}
public boolean isMouseEventsInvertX() {
return mouseEventsInvertX;
}
public boolean isMouseEventsInvertY() {
return mouseEventsInvertY;
}
@Override
public void setSimulateKeyboard(boolean simulate) {
this.keyboardEventsEnabled = simulate;
}
@Override
public boolean isSimulateKeyboard() {
return keyboardEventsEnabled;
}
@Override
public void setOmitHistoricEvents(boolean dontSendHistory) {
this.dontSendHistory = dontSendHistory;
}
}

@ -33,7 +33,6 @@
package com.jme3.input.android; package com.jme3.input.android;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View;
import com.jme3.input.event.TouchEvent; import com.jme3.input.event.TouchEvent;
import com.jme3.math.Vector2f; import com.jme3.math.Vector2f;
import java.util.HashMap; import java.util.HashMap;
@ -41,36 +40,20 @@ import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* AndroidTouchHandler14 is an extension of AndroidTouchHander that adds the * AndroidTouchHandler14 extends AndroidTouchHandler to process the onHover
* Android touch event functionality between Android rev 9 (Android 2.3) and * events added in Android rev 14 (Android 4.0).
* Android rev 14 (Android 4.0). *
*
* @author iwgeric * @author iwgeric
*/ */
public class AndroidTouchHandler14 extends AndroidTouchHandler implements public class AndroidTouchInput14 extends AndroidTouchInput {
View.OnHoverListener { private static final Logger logger = Logger.getLogger(AndroidTouchInput14.class.getName());
private static final Logger logger = Logger.getLogger(AndroidTouchHandler14.class.getName());
final private HashMap<Integer, Vector2f> lastHoverPositions = new HashMap<Integer, Vector2f>(); final private HashMap<Integer, Vector2f> lastHoverPositions = new HashMap<Integer, Vector2f>();
public AndroidTouchHandler14(AndroidInputHandler androidInput, AndroidGestureHandler gestureHandler) {
super(androidInput, gestureHandler);
}
@Override public AndroidTouchInput14(AndroidInputHandler androidInput) {
public void setView(View view) { super(androidInput);
if (view != null) {
view.setOnHoverListener(this);
} else {
androidInput.getView().setOnHoverListener(null);
}
super.setView(view);
} }
public boolean onHover(View view, MotionEvent event) { public boolean onHover(MotionEvent event) {
if (view == null || view != androidInput.getView()) {
return false;
}
boolean consumed = false; boolean consumed = false;
int action = getAction(event); int action = getAction(event);
int pointerId = getPointerId(event); int pointerId = getPointerId(event);
@ -78,34 +61,34 @@ public class AndroidTouchHandler14 extends AndroidTouchHandler implements
Vector2f lastPos = lastHoverPositions.get(pointerId); Vector2f lastPos = lastHoverPositions.get(pointerId);
float jmeX; float jmeX;
float jmeY; float jmeY;
numPointers = event.getPointerCount(); numPointers = event.getPointerCount();
logger.log(Level.INFO, "onHover pointerId: {0}, action: {1}, x: {2}, y: {3}, numPointers: {4}", // 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()}); // new Object[]{pointerId, action, event.getX(), event.getY(), event.getPointerCount()});
TouchEvent touchEvent; TouchEvent touchEvent;
switch (action) { switch (action) {
case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_ENTER:
jmeX = androidInput.getJmeX(event.getX(pointerIndex)); jmeX = getJmeX(event.getX(pointerIndex));
jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex))); jmeY = invertY(getJmeY(event.getY(pointerIndex)));
touchEvent = androidInput.getFreeTouchEvent(); touchEvent = getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.HOVER_START, jmeX, jmeY, 0, 0); touchEvent.set(TouchEvent.Type.HOVER_START, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(pointerId); touchEvent.setPointerId(pointerId);
touchEvent.setTime(event.getEventTime()); touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure(pointerIndex)); touchEvent.setPressure(event.getPressure(pointerIndex));
lastPos = new Vector2f(jmeX, jmeY); lastPos = new Vector2f(jmeX, jmeY);
lastHoverPositions.put(pointerId, lastPos); lastHoverPositions.put(pointerId, lastPos);
processEvent(touchEvent); addEvent(touchEvent);
consumed = true; consumed = true;
break; break;
case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_HOVER_MOVE:
// Convert all pointers into events // Convert all pointers into events
for (int p = 0; p < event.getPointerCount(); p++) { for (int p = 0; p < event.getPointerCount(); p++) {
jmeX = androidInput.getJmeX(event.getX(p)); jmeX = getJmeX(event.getX(p));
jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(p))); jmeY = invertY(getJmeY(event.getY(p)));
lastPos = lastHoverPositions.get(event.getPointerId(p)); lastPos = lastHoverPositions.get(event.getPointerId(p));
if (lastPos == null) { if (lastPos == null) {
lastPos = new Vector2f(jmeX, jmeY); lastPos = new Vector2f(jmeX, jmeY);
@ -115,38 +98,39 @@ public class AndroidTouchHandler14 extends AndroidTouchHandler implements
float dX = jmeX - lastPos.x; float dX = jmeX - lastPos.x;
float dY = jmeY - lastPos.y; float dY = jmeY - lastPos.y;
if (dX != 0 || dY != 0) { if (dX != 0 || dY != 0) {
touchEvent = androidInput.getFreeTouchEvent(); touchEvent = getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.HOVER_MOVE, jmeX, jmeY, dX, dY); touchEvent.set(TouchEvent.Type.HOVER_MOVE, jmeX, jmeY, dX, dY);
touchEvent.setPointerId(event.getPointerId(p)); touchEvent.setPointerId(event.getPointerId(p));
touchEvent.setTime(event.getEventTime()); touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure(p)); touchEvent.setPressure(event.getPressure(p));
lastPos.set(jmeX, jmeY); lastPos.set(jmeX, jmeY);
processEvent(touchEvent); addEvent(touchEvent);
} }
} }
consumed = true; consumed = true;
break; break;
case MotionEvent.ACTION_HOVER_EXIT: case MotionEvent.ACTION_HOVER_EXIT:
jmeX = androidInput.getJmeX(event.getX(pointerIndex)); jmeX = getJmeX(event.getX(pointerIndex));
jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex))); jmeY = invertY(getJmeY(event.getY(pointerIndex)));
touchEvent = androidInput.getFreeTouchEvent(); touchEvent = getFreeTouchEvent();
touchEvent.set(TouchEvent.Type.HOVER_END, jmeX, jmeY, 0, 0); touchEvent.set(TouchEvent.Type.HOVER_END, jmeX, jmeY, 0, 0);
touchEvent.setPointerId(pointerId); touchEvent.setPointerId(pointerId);
touchEvent.setTime(event.getEventTime()); touchEvent.setTime(event.getEventTime());
touchEvent.setPressure(event.getPressure(pointerIndex)); touchEvent.setPressure(event.getPressure(pointerIndex));
lastHoverPositions.remove(pointerId); lastHoverPositions.remove(pointerId);
processEvent(touchEvent); addEvent(touchEvent);
consumed = true; consumed = true;
break; break;
default: default:
consumed = false; consumed = false;
break; break;
} }
return consumed; return consumed;
} }
} }

@ -35,39 +35,43 @@ import android.opengl.GLES20;
import com.jme3.renderer.RendererException; import com.jme3.renderer.RendererException;
import com.jme3.renderer.opengl.GL; import com.jme3.renderer.opengl.GL;
import com.jme3.renderer.opengl.GLExt; import com.jme3.renderer.opengl.GLExt;
import com.jme3.renderer.opengl.GLFbo;
import java.nio.Buffer; import java.nio.Buffer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import java.nio.ShortBuffer; import java.nio.ShortBuffer;
public class AndroidGL implements GL, GLExt { public class AndroidGL implements GL, GLExt, GLFbo {
public void resetStats() {
}
private static int getLimitBytes(ByteBuffer buffer) { private static int getLimitBytes(ByteBuffer buffer) {
checkLimit(buffer); checkLimit(buffer);
return buffer.limit(); return buffer.limit();
} }
private static int getLimitBytes(ShortBuffer buffer) { private static int getLimitBytes(ShortBuffer buffer) {
checkLimit(buffer); checkLimit(buffer);
return buffer.limit() * 2; return buffer.limit() * 2;
} }
private static int getLimitBytes(IntBuffer buffer) { private static int getLimitBytes(IntBuffer buffer) {
checkLimit(buffer); checkLimit(buffer);
return buffer.limit() * 4; return buffer.limit() * 4;
} }
private static int getLimitBytes(FloatBuffer buffer) { private static int getLimitBytes(FloatBuffer buffer) {
checkLimit(buffer); checkLimit(buffer);
return buffer.limit() * 4; return buffer.limit() * 4;
} }
private static int getLimitCount(Buffer buffer, int elementSize) { private static int getLimitCount(Buffer buffer, int elementSize) {
checkLimit(buffer); checkLimit(buffer);
return buffer.limit() / elementSize; return buffer.limit() / elementSize;
} }
private static void checkLimit(Buffer buffer) { private static void checkLimit(Buffer buffer) {
if (buffer == null) { if (buffer == null) {
return; return;
@ -79,7 +83,7 @@ public class AndroidGL implements GL, GLExt {
throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error");
} }
} }
public void glActiveTexture(int texture) { public void glActiveTexture(int texture) {
GLES20.glActiveTexture(texture); GLES20.glActiveTexture(texture);
} }
@ -127,7 +131,7 @@ public class AndroidGL implements GL, GLExt {
public void glBufferSubData(int target, long offset, ByteBuffer data) { public void glBufferSubData(int target, long offset, ByteBuffer data) {
GLES20.glBufferSubData(target, (int) offset, getLimitBytes(data), data); GLES20.glBufferSubData(target, (int) offset, getLimitBytes(data), data);
} }
public void glGetBufferSubData(int target, long offset, ByteBuffer data) { public void glGetBufferSubData(int target, long offset, ByteBuffer data) {
throw new UnsupportedOperationException("OpenGL ES 2 does not support glGetBufferSubData"); throw new UnsupportedOperationException("OpenGL ES 2 does not support glGetBufferSubData");
} }

@ -46,17 +46,16 @@ import android.view.ViewGroup.LayoutParams;
import android.widget.EditText; import android.widget.EditText;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import com.jme3.input.*; import com.jme3.input.*;
import com.jme3.input.android.AndroidSensorJoyInput;
import com.jme3.input.android.AndroidInputHandler; import com.jme3.input.android.AndroidInputHandler;
import com.jme3.input.android.AndroidInputHandler14;
import com.jme3.input.controls.SoftTextDialogInputListener; import com.jme3.input.controls.SoftTextDialogInputListener;
import com.jme3.input.dummy.DummyKeyInput; import com.jme3.input.dummy.DummyKeyInput;
import com.jme3.input.dummy.DummyMouseInput; import com.jme3.input.dummy.DummyMouseInput;
import com.jme3.renderer.android.AndroidGL; import com.jme3.renderer.android.AndroidGL;
import com.jme3.renderer.opengl.GL; import com.jme3.renderer.opengl.GL;
import com.jme3.renderer.opengl.GLDebugES;
import com.jme3.renderer.opengl.GLExt; import com.jme3.renderer.opengl.GLExt;
import com.jme3.renderer.opengl.GLFbo;
import com.jme3.renderer.opengl.GLRenderer; import com.jme3.renderer.opengl.GLRenderer;
import com.jme3.renderer.opengl.GLTracer;
import com.jme3.system.*; import com.jme3.system.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level; import java.util.logging.Level;
@ -79,7 +78,6 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
protected AndroidInputHandler androidInput; protected AndroidInputHandler androidInput;
protected long minFrameDuration = 0; // No FPS cap protected long minFrameDuration = 0; // No FPS cap
protected long lastUpdateTime = 0; protected long lastUpdateTime = 0;
protected JoyInput androidSensorJoyInput = null;
public OGLESContext() { public OGLESContext() {
} }
@ -113,8 +111,13 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
// Start to set up the view // Start to set up the view
GLSurfaceView view = new GLSurfaceView(context); GLSurfaceView view = new GLSurfaceView(context);
logger.log(Level.INFO, "Android Build Version: {0}", Build.VERSION.SDK_INT);
if (androidInput == null) { if (androidInput == null) {
androidInput = new AndroidInputHandler(); if (Build.VERSION.SDK_INT >= 14) {
androidInput = new AndroidInputHandler14();
} else if (Build.VERSION.SDK_INT >= 9){
androidInput = new AndroidInputHandler();
}
} }
androidInput.setView(view); androidInput.setView(view);
androidInput.loadSettings(settings); androidInput.loadSettings(settings);
@ -194,7 +197,7 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
Object gl = new AndroidGL(); Object gl = new AndroidGL();
// gl = GLTracer.createGlesTracer((GL)gl, (GLExt)gl); // gl = GLTracer.createGlesTracer((GL)gl, (GLExt)gl);
// gl = new GLDebugES((GL)gl, (GLExt)gl); // gl = new GLDebugES((GL)gl, (GLExt)gl);
renderer = new GLRenderer((GL)gl, (GLExt)gl); renderer = new GLRenderer((GL)gl, (GLExt)gl, (GLFbo)gl);
renderer.initialize(); renderer.initialize();
JmeSystem.setSoftTextDialogInput(this); JmeSystem.setSoftTextDialogInput(this);
@ -267,15 +270,12 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
@Override @Override
public JoyInput getJoyInput() { public JoyInput getJoyInput() {
if (androidSensorJoyInput == null) { return androidInput.getJoyInput();
androidSensorJoyInput = new AndroidSensorJoyInput();
}
return androidSensorJoyInput;
} }
@Override @Override
public TouchInput getTouchInput() { public TouchInput getTouchInput() {
return androidInput; return androidInput.getTouchInput();
} }
@Override @Override

@ -7,21 +7,19 @@ import com.jme3.app.AndroidHarness;
public class DemoAndroidHarness extends AndroidHarness public class DemoAndroidHarness extends AndroidHarness
{ {
@Override @Override
public void onCreate(Bundle savedInstanceState) public void onCreate(Bundle savedInstanceState)
{ {
// Set the application class to run // Set the application class to run
// First Extract the bundle from intent // First Extract the bundle from intent
Bundle bundle = getIntent().getExtras(); Bundle bundle = getIntent().getExtras();
//Next extract the values using the key as //Next extract the values using the key as
appClass = bundle.getString("APPCLASSNAME"); appClass = bundle.getString("APPCLASSNAME");
exitDialogTitle = "Close Demo?"; exitDialogTitle = "Close Demo?";
exitDialogMessage = "Press Yes"; exitDialogMessage = "Press Yes";
screenOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; super.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
} }
} }

@ -6,4 +6,7 @@ dependencies {
compile project(':jme3-core') compile project(':jme3-core')
compile project(':jme3-desktop') compile project(':jme3-desktop')
compile project(':jme3-effects') compile project(':jme3-effects')
} compile ('org.ejml:core:0.27')
compile ('org.ejml:dense64:0.27')
compile ('org.ejml:simple:0.27')
}

@ -32,36 +32,19 @@
package com.jme3.asset; package com.jme3.asset;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import com.jme3.animation.Animation;
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.InputCapsule;
import com.jme3.export.JmeExporter; import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter; import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule; import com.jme3.export.OutputCapsule;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState.FaceCullMode; import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.ColorRGBA;
import com.jme3.post.Filter;
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.texture.Texture;
/** /**
* Blender key. Contains path of the blender file and its loading properties. * Blender key. Contains path of the blender file and its loading properties.
* @author Marcin Roguski (Kaelthas) * @author Marcin Roguski (Kaelthas)
*/ */
public class BlenderKey extends ModelKey { public class BlenderKey extends ModelKey {
protected static final int DEFAULT_FPS = 25; protected static final int DEFAULT_FPS = 25;
/** /**
* FramesPerSecond parameter describe how many frames there are in each second. It allows to calculate the time * FramesPerSecond parameter describe how many frames there are in each second. It allows to calculate the time
@ -72,7 +55,7 @@ public class BlenderKey extends ModelKey {
* This variable is a bitwise flag of FeatureToLoad interface values; By default everything is being loaded. * This variable is a bitwise flag of FeatureToLoad interface values; By default everything is being loaded.
*/ */
protected int featuresToLoad = FeaturesToLoad.ALL; protected int featuresToLoad = FeaturesToLoad.ALL;
/** This variable determines if assets that are not linked to the objects should be loaded. */ /** The variable that tells if content of the file (along with data unlinked to any feature on the scene) should be stored as 'user data' in the result spatial. */
protected boolean loadUnlinkedAssets; protected boolean loadUnlinkedAssets;
/** The root path for all the assets. */ /** The root path for all the assets. */
protected String assetRootPath; protected String assetRootPath;
@ -268,6 +251,7 @@ public class BlenderKey extends ModelKey {
* @param featuresToLoad * @param featuresToLoad
* bitwise flag of FeaturesToLoad interface values * bitwise flag of FeaturesToLoad interface values
*/ */
@Deprecated
public void includeInLoading(int featuresToLoad) { public void includeInLoading(int featuresToLoad) {
this.featuresToLoad |= featuresToLoad; this.featuresToLoad |= featuresToLoad;
} }
@ -277,10 +261,12 @@ public class BlenderKey extends ModelKey {
* @param featuresNotToLoad * @param featuresNotToLoad
* bitwise flag of FeaturesToLoad interface values * bitwise flag of FeaturesToLoad interface values
*/ */
@Deprecated
public void excludeFromLoading(int featuresNotToLoad) { public void excludeFromLoading(int featuresNotToLoad) {
featuresToLoad &= ~featuresNotToLoad; featuresToLoad &= ~featuresNotToLoad;
} }
@Deprecated
public boolean shouldLoad(int featureToLoad) { public boolean shouldLoad(int featureToLoad) {
return (featuresToLoad & featureToLoad) != 0; return (featuresToLoad & featureToLoad) != 0;
} }
@ -290,6 +276,7 @@ public class BlenderKey extends ModelKey {
* the blender file loader. * the blender file loader.
* @return features that will be loaded by the blender file loader * @return features that will be loaded by the blender file loader
*/ */
@Deprecated
public int getFeaturesToLoad() { public int getFeaturesToLoad() {
return featuresToLoad; return featuresToLoad;
} }
@ -317,15 +304,6 @@ public class BlenderKey extends ModelKey {
this.loadUnlinkedAssets = 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 * 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. * is up axis.
@ -699,8 +677,11 @@ public class BlenderKey extends ModelKey {
/** /**
* This interface describes the features of the scene that are to be loaded. * This interface describes the features of the scene that are to be loaded.
* @deprecated this interface is deprecated and is not used anymore; to ensure the loading models consistency
* everything must be loaded because in blender one feature might depend on another
* @author Marcin Roguski (Kaelthas) * @author Marcin Roguski (Kaelthas)
*/ */
@Deprecated
public static interface FeaturesToLoad { public static interface FeaturesToLoad {
int SCENES = 0x0000FFFF; int SCENES = 0x0000FFFF;
@ -745,281 +726,4 @@ public class BlenderKey extends ModelKey {
*/ */
ALL_NAMES_MATCH; ALL_NAMES_MATCH;
} }
/**
* 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<Animation> animations;
/** All cameras from the file. */
private List<CameraNode> cameras;
/** All lights from the file. */
private List<LightNode> lights;
/** Loaded sky. */
private Spatial sky;
/** Scene filters (ie. FOG). */
private List<Filter> filters;
/**
* 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<Animation>();
}
}
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;
}
/**
* This method adds a scene filter. Filters are used to load FOG or other
* scene effects that blender can define.
* @param filter
* the filter to be added
*/
public void addFilter(Filter filter) {
if (filter != null) {
if (filters == null) {
filters = new ArrayList<Filter>(5);
}
filters.add(filter);
}
}
/**
* @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<Animation> 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 scene filters
*/
public List<Filter> getFilters() {
return filters;
}
/**
* @return the background color
*/
public ColorRGBA getBackgroundColor() {
return backgroundColor;
}
@Override
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) {
}
}
} }

@ -31,17 +31,34 @@
*/ */
package com.jme3.scene.plugins.blender; package com.jme3.scene.plugins.blender;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; 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.animation.Animation;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.asset.BlenderKey;
import com.jme3.export.Savable; import com.jme3.export.Savable;
import com.jme3.light.Light;
import com.jme3.math.FastMath; import com.jme3.math.FastMath;
import com.jme3.math.Quaternion; import com.jme3.math.Quaternion;
import com.jme3.post.Filter;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException; import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer; import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure; import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.objects.Properties; import com.jme3.scene.plugins.blender.objects.Properties;
import com.jme3.texture.Texture;
/** /**
* A purpose of the helper class is to split calculation code into several classes. Each helper after use should be cleared because it can * A purpose of the helper class is to split calculation code into several classes. Each helper after use should be cleared because it can
@ -49,14 +66,16 @@ import com.jme3.scene.plugins.blender.objects.Properties;
* @author Marcin Roguski * @author Marcin Roguski
*/ */
public abstract class AbstractBlenderHelper { public abstract class AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(AbstractBlenderHelper.class.getName());
/** The blender context. */ /** The blender context. */
protected BlenderContext blenderContext; protected BlenderContext blenderContext;
/** The version of the blend file. */ /** The version of the blend file. */
protected final int blenderVersion; protected final int blenderVersion;
/** This variable indicates if the Y asxis is the UP axis or not. */ /** This variable indicates if the Y asxis is the UP axis or not. */
protected boolean fixUpAxis; protected boolean fixUpAxis;
/** Quaternion used to rotate data when Y is up axis. */ /** Quaternion used to rotate data when Y is up axis. */
protected Quaternion upAxisRotationQuaternion; protected Quaternion upAxisRotationQuaternion;
/** /**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender * This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
@ -129,4 +148,115 @@ public abstract class AbstractBlenderHelper {
} }
} }
} }
/**
* The method loads library of a given ID from linked blender file.
* @param id
* the ID of the linked feature (it contains its name and blender path)
* @return loaded feature or null if none was found
* @throws BlenderFileException
* and exception is throw when problems with reading a blend file occur
*/
@SuppressWarnings("unchecked")
protected Object loadLibrary(Structure id) throws BlenderFileException {
Pointer pLib = (Pointer) id.getFieldValue("lib");
if (pLib.isNotNull()) {
String fullName = id.getFieldValue("name").toString();// we need full name with the prefix
String nameOfFeatureToLoad = id.getName();
Structure library = pLib.fetchData().get(0);
String path = library.getFieldValue("filepath").toString();
if (!blenderContext.getLinkedFeatures().keySet().contains(path)) {
File file = new File(path);
List<String> pathsToCheck = new ArrayList<String>();
String currentPath = file.getName();
do {
pathsToCheck.add(currentPath);
file = file.getParentFile();
if (file != null) {
currentPath = file.getName() + '/' + currentPath;
}
} while (file != null);
Spatial loadedAsset = null;
BlenderKey blenderKey = null;
for (String p : pathsToCheck) {
blenderKey = new BlenderKey(p);
blenderKey.setLoadUnlinkedAssets(true);
try {
loadedAsset = blenderContext.getAssetManager().loadAsset(blenderKey);
break;// break if no exception was thrown
} catch (AssetNotFoundException e) {
LOGGER.log(Level.FINEST, "Cannot locate linked resource at path: {0}.", p);
}
}
if (loadedAsset != null) {
Map<String, Map<String, Object>> linkedData = loadedAsset.getUserData("linkedData");
for (Entry<String, Map<String, Object>> entry : linkedData.entrySet()) {
String linkedDataFilePath = "this".equals(entry.getKey()) ? path : entry.getKey();
List<Node> scenes = (List<Node>) entry.getValue().get("scenes");
for (Node scene : scenes) {
blenderContext.addLinkedFeature(linkedDataFilePath, "SC" + scene.getName(), scene);
}
List<Node> objects = (List<Node>) entry.getValue().get("objects");
for (Node object : objects) {
blenderContext.addLinkedFeature(linkedDataFilePath, "OB" + object.getName(), object);
}
List<TemporalMesh> meshes = (List<TemporalMesh>) entry.getValue().get("meshes");
for (TemporalMesh mesh : meshes) {
blenderContext.addLinkedFeature(linkedDataFilePath, "ME" + mesh.getName(), mesh);
}
List<MaterialContext> materials = (List<MaterialContext>) entry.getValue().get("materials");
for (MaterialContext materialContext : materials) {
blenderContext.addLinkedFeature(linkedDataFilePath, "MA" + materialContext.getName(), materialContext);
}
List<Texture> textures = (List<Texture>) entry.getValue().get("textures");
for (Texture texture : textures) {
blenderContext.addLinkedFeature(linkedDataFilePath, "TE" + texture.getName(), texture);
}
List<Texture> images = (List<Texture>) entry.getValue().get("images");
for (Texture image : images) {
blenderContext.addLinkedFeature(linkedDataFilePath, "IM" + image.getName(), image);
}
List<Animation> animations = (List<Animation>) entry.getValue().get("animations");
for (Animation animation : animations) {
blenderContext.addLinkedFeature(linkedDataFilePath, "AC" + animation.getName(), animation);
}
List<Camera> cameras = (List<Camera>) entry.getValue().get("cameras");
for (Camera camera : cameras) {
blenderContext.addLinkedFeature(linkedDataFilePath, "CA" + camera.getName(), camera);
}
List<Light> lights = (List<Light>) entry.getValue().get("lights");
for (Light light : lights) {
blenderContext.addLinkedFeature(linkedDataFilePath, "LA" + light.getName(), light);
}
Spatial sky = (Spatial) entry.getValue().get("sky");
if (sky != null) {
blenderContext.addLinkedFeature(linkedDataFilePath, sky.getName(), sky);
}
List<Filter> filters = (List<Filter>) entry.getValue().get("filters");
for (Filter filter : filters) {
blenderContext.addLinkedFeature(linkedDataFilePath, filter.getName(), filter);
}
}
} else {
LOGGER.log(Level.WARNING, "No features loaded from path: {0}.", path);
}
}
Object result = blenderContext.getLinkedFeature(path, fullName);
if (result == null) {
LOGGER.log(Level.WARNING, "Could NOT find asset named {0} in the library of path: {1}.", new Object[] { nameOfFeatureToLoad, path });
} else {
blenderContext.addLoadedFeatures(id.getOldMemoryAddress(), LoadedDataType.STRUCTURE, id);
blenderContext.addLoadedFeatures(id.getOldMemoryAddress(), LoadedDataType.FEATURE, result);
}
return result;
} else {
LOGGER.warning("Library link points to nothing!");
}
return null;
}
} }

@ -53,6 +53,7 @@ import com.jme3.scene.plugins.blender.constraints.Constraint;
import com.jme3.scene.plugins.blender.file.BlenderInputStream; import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.DnaBlockData; import com.jme3.scene.plugins.blender.file.DnaBlockData;
import com.jme3.scene.plugins.blender.file.FileBlockHeader; import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
import com.jme3.scene.plugins.blender.file.Structure; import com.jme3.scene.plugins.blender.file.Structure;
/** /**
@ -64,49 +65,51 @@ import com.jme3.scene.plugins.blender.file.Structure;
*/ */
public class BlenderContext { public class BlenderContext {
/** The blender file version. */ /** The blender file version. */
private int blenderVersion; private int blenderVersion;
/** The blender key. */ /** The blender key. */
private BlenderKey blenderKey; private BlenderKey blenderKey;
/** The header of the file block. */ /** The header of the file block. */
private DnaBlockData dnaBlockData; private DnaBlockData dnaBlockData;
/** The scene structure. */ /** The scene structure. */
private Structure sceneStructure; private Structure sceneStructure;
/** The input stream of the blend file. */ /** The input stream of the blend file. */
private BlenderInputStream inputStream; private BlenderInputStream inputStream;
/** The asset manager. */ /** The asset manager. */
private AssetManager assetManager; private AssetManager assetManager;
/** The blocks read from the file. */ /** The blocks read from the file. */
protected List<FileBlockHeader> blocks; protected List<FileBlockHeader> blocks;
/** /**
* A map containing the file block headers. The key is the old memory address. * A map containing the file block headers. The key is the old memory address.
*/ */
private Map<Long, FileBlockHeader> fileBlockHeadersByOma = new HashMap<Long, FileBlockHeader>(); private Map<Long, FileBlockHeader> fileBlockHeadersByOma = new HashMap<Long, FileBlockHeader>();
/** A map containing the file block headers. The key is the block code. */ /** A map containing the file block headers. The key is the block code. */
private Map<Integer, List<FileBlockHeader>> fileBlockHeadersByCode = new HashMap<Integer, List<FileBlockHeader>>(); private Map<BlockCode, List<FileBlockHeader>> fileBlockHeadersByCode = new HashMap<BlockCode, List<FileBlockHeader>>();
/** /**
* This map stores the loaded features by their old memory address. The * 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 - * first object in the value table is the loaded structure and the second -
* the structure already converted into proper data. * the structure already converted into proper data.
*/ */
private Map<Long, Map<LoadedDataType, Object>> loadedFeatures = new HashMap<Long, Map<LoadedDataType, Object>>(); private Map<Long, Map<LoadedDataType, Object>> loadedFeatures = new HashMap<Long, Map<LoadedDataType, Object>>();
/** Features loaded from external blender files. The key is the file path and the value is a map between feature name and loaded feature. */
private Map<String, Map<String, Object>> linkedFeatures = new HashMap<String, Map<String, Object>>();
/** A stack that hold the parent structure of currently loaded feature. */ /** A stack that hold the parent structure of currently loaded feature. */
private Stack<Structure> parentStack = new Stack<Structure>(); private Stack<Structure> parentStack = new Stack<Structure>();
/** A list of constraints for the specified object. */ /** A list of constraints for the specified object. */
protected Map<Long, List<Constraint>> constraints = new HashMap<Long, List<Constraint>>(); protected Map<Long, List<Constraint>> constraints = new HashMap<Long, List<Constraint>>();
/** Animations loaded for features. */ /** Animations loaded for features. */
private Map<Long, List<Animation>> animations = new HashMap<Long, List<Animation>>(); private Map<Long, List<Animation>> animations = new HashMap<Long, List<Animation>>();
/** Loaded skeletons. */ /** Loaded skeletons. */
private Map<Long, Skeleton> skeletons = new HashMap<Long, Skeleton>(); private Map<Long, Skeleton> skeletons = new HashMap<Long, Skeleton>();
/** A map between skeleton and node it modifies. */ /** A map between skeleton and node it modifies. */
private Map<Skeleton, Node> nodesWithSkeletons = new HashMap<Skeleton, Node>(); private Map<Skeleton, Node> nodesWithSkeletons = new HashMap<Skeleton, Node>();
/** A map of bone contexts. */ /** A map of bone contexts. */
protected Map<Long, BoneContext> boneContexts = new HashMap<Long, BoneContext>(); protected Map<Long, BoneContext> boneContexts = new HashMap<Long, BoneContext>();
/** A map og helpers that perform loading. */ /** A map og helpers that perform loading. */
private Map<String, AbstractBlenderHelper> helpers = new HashMap<String, AbstractBlenderHelper>(); 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. */ /** 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>>(); private Map<String, Map<Object, Object>> markers = new HashMap<String, Map<Object, Object>>();
/** A map of blender actions. The key is the action name and the value is the action itself. */ /** A map of blender actions. The key is the action name and the value is the action itself. */
private Map<String, BlenderAction> actions = new HashMap<String, BlenderAction>(); private Map<String, BlenderAction> actions = new HashMap<String, BlenderAction>();
/** /**
* This method sets the blender file version. * This method sets the blender file version.
@ -231,10 +234,10 @@ public class BlenderContext {
*/ */
public void addFileBlockHeader(Long oldMemoryAddress, FileBlockHeader fileBlockHeader) { public void addFileBlockHeader(Long oldMemoryAddress, FileBlockHeader fileBlockHeader) {
fileBlockHeadersByOma.put(oldMemoryAddress, fileBlockHeader); fileBlockHeadersByOma.put(oldMemoryAddress, fileBlockHeader);
List<FileBlockHeader> headers = fileBlockHeadersByCode.get(Integer.valueOf(fileBlockHeader.getCode())); List<FileBlockHeader> headers = fileBlockHeadersByCode.get(fileBlockHeader.getCode());
if (headers == null) { if (headers == null) {
headers = new ArrayList<FileBlockHeader>(); headers = new ArrayList<FileBlockHeader>();
fileBlockHeadersByCode.put(Integer.valueOf(fileBlockHeader.getCode()), headers); fileBlockHeadersByCode.put(fileBlockHeader.getCode(), headers);
} }
headers.add(fileBlockHeader); headers.add(fileBlockHeader);
} }
@ -258,7 +261,7 @@ public class BlenderContext {
* the code of file blocks * the code of file blocks
* @return a list of file blocks' headers of a specified code * @return a list of file blocks' headers of a specified code
*/ */
public List<FileBlockHeader> getFileBlocks(Integer code) { public List<FileBlockHeader> getFileBlocks(BlockCode code) {
return fileBlockHeadersByCode.get(code); return fileBlockHeadersByCode.get(code);
} }
@ -299,7 +302,7 @@ public class BlenderContext {
throw new IllegalArgumentException("One of the given arguments is null!"); throw new IllegalArgumentException("One of the given arguments is null!");
} }
Map<LoadedDataType, Object> map = loadedFeatures.get(oldMemoryAddress); Map<LoadedDataType, Object> map = loadedFeatures.get(oldMemoryAddress);
if(map == null) { if (map == null) {
map = new HashMap<BlenderContext.LoadedDataType, Object>(); map = new HashMap<BlenderContext.LoadedDataType, Object>();
loadedFeatures.put(oldMemoryAddress, map); loadedFeatures.put(oldMemoryAddress, map);
} }
@ -325,6 +328,48 @@ public class BlenderContext {
return null; return null;
} }
/**
* The method adds linked content to the blender context.
* @param blenderFilePath
* the path of linked blender file
* @param featureName
* the linked feature name
* @param feature
* the linked feature
*/
public void addLinkedFeature(String blenderFilePath, String featureName, Object feature) {
if (feature != null) {
Map<String, Object> linkedFeatures = this.linkedFeatures.get(blenderFilePath);
if (linkedFeatures == null) {
linkedFeatures = new HashMap<String, Object>();
this.linkedFeatures.put(blenderFilePath, linkedFeatures);
}
if (!linkedFeatures.containsKey(featureName)) {
linkedFeatures.put(featureName, feature);
}
}
}
/**
* The method returns linked feature of a given name from the specified blender path.
* @param blenderFilePath
* the blender file path
* @param featureName
* the feature name we want to get
* @return linked feature or null if none was found
*/
public Object getLinkedFeature(String blenderFilePath, String featureName) {
Map<String, Object> linkedFeatures = this.linkedFeatures.get(blenderFilePath);
return linkedFeatures != null ? linkedFeatures.get(featureName) : null;
}
/**
* @return all linked features for the current blend file
*/
public Map<String, Map<String, Object>> getLinkedFeatures() {
return linkedFeatures;
}
/** /**
* This method adds the structure to the parent stack. * This method adds the structure to the parent stack.
* *

@ -33,17 +33,21 @@ package com.jme3.scene.plugins.blender;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.jme3.animation.Animation;
import com.jme3.asset.AssetInfo; import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader; import com.jme3.asset.AssetLoader;
import com.jme3.asset.BlenderKey; 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.asset.ModelKey;
import com.jme3.light.Light; import com.jme3.light.Light;
import com.jme3.math.ColorRGBA;
import com.jme3.post.Filter;
import com.jme3.renderer.Camera;
import com.jme3.scene.CameraNode; import com.jme3.scene.CameraNode;
import com.jme3.scene.LightNode; import com.jme3.scene.LightNode;
import com.jme3.scene.Node; import com.jme3.scene.Node;
@ -55,16 +59,20 @@ import com.jme3.scene.plugins.blender.curves.CurvesHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException; import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream; import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.FileBlockHeader; import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
import com.jme3.scene.plugins.blender.file.Pointer; import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure; import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.landscape.LandscapeHelper; import com.jme3.scene.plugins.blender.landscape.LandscapeHelper;
import com.jme3.scene.plugins.blender.lights.LightHelper; import com.jme3.scene.plugins.blender.lights.LightHelper;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.materials.MaterialHelper; import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.meshes.MeshHelper; import com.jme3.scene.plugins.blender.meshes.MeshHelper;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.modifiers.ModifierHelper; import com.jme3.scene.plugins.blender.modifiers.ModifierHelper;
import com.jme3.scene.plugins.blender.objects.ObjectHelper; import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.scene.plugins.blender.particles.ParticlesHelper; import com.jme3.scene.plugins.blender.particles.ParticlesHelper;
import com.jme3.scene.plugins.blender.textures.TextureHelper; import com.jme3.scene.plugins.blender.textures.TextureHelper;
import com.jme3.texture.Texture;
/** /**
* This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures. * This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures.
@ -83,72 +91,130 @@ public class BlenderLoader implements AssetLoader {
try { try {
this.setup(assetInfo); this.setup(assetInfo);
List<FileBlockHeader> sceneBlocks = new ArrayList<FileBlockHeader>();
BlenderKey blenderKey = blenderContext.getBlenderKey();
LoadingResults loadingResults = blenderKey.prepareLoadingResults();
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class);
animationHelper.loadAnimations(); animationHelper.loadAnimations();
BlenderKey blenderKey = blenderContext.getBlenderKey();
LoadedFeatures loadedFeatures = new LoadedFeatures();
for (FileBlockHeader block : blocks) { for (FileBlockHeader block : blocks) {
switch (block.getCode()) { switch (block.getCode()) {
case FileBlockHeader.BLOCK_OB00:// Object case BLOCK_OB00:
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Object object = objectHelper.toObject(block.getStructure(blenderContext), blenderContext); Node object = (Node) objectHelper.toObject(block.getStructure(blenderContext), blenderContext);
if (object instanceof LightNode) { if (LOGGER.isLoggable(Level.FINE)) {
loadingResults.addLight((LightNode) object); LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { object.getName(), object.getLocalTranslation().toString(), object.getParent() == null ? "null" : object.getParent().getName() });
} 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);
}
} }
if (object.getParent() == null) {
loadedFeatures.objects.add(object);
}
if (object instanceof LightNode && ((LightNode) object).getLight() != null) {
loadedFeatures.lights.add(((LightNode) object).getLight());
} else if (object instanceof CameraNode && ((CameraNode) object).getCamera() != null) {
loadedFeatures.cameras.add(((CameraNode) object).getCamera());
}
break;
case BLOCK_SC00:// Scene
loadedFeatures.sceneBlocks.add(block);
break; break;
// case FileBlockHeader.BLOCK_MA00:// Material case BLOCK_MA00:// Material
// MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class); MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
// MaterialContext materialContext = materialHelper.toMaterialContext(block.getStructure(blenderContext), blenderContext); MaterialContext materialContext = materialHelper.toMaterialContext(block.getStructure(blenderContext), blenderContext);
// if (blenderKey.isLoadUnlinkedAssets() && blenderKey.shouldLoad(FeaturesToLoad.MATERIALS)) { loadedFeatures.materials.add(materialContext);
// loadingResults.addMaterial(this.toMaterial(block.getStructure(blenderContext))); break;
// } case BLOCK_ME00:// Mesh
// break; MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
case FileBlockHeader.BLOCK_SC00:// Scene TemporalMesh temporalMesh = meshHelper.toTemporalMesh(block.getStructure(blenderContext), blenderContext);
if (blenderKey.shouldLoad(FeaturesToLoad.SCENES)) { loadedFeatures.meshes.add(temporalMesh);
sceneBlocks.add(block); break;
case BLOCK_IM00:// Image
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
Texture image = textureHelper.loadImageAsTexture(block.getStructure(blenderContext), 0, blenderContext);
if (image != null && image.getImage() != null) {// render results are stored as images but are not being loaded
loadedFeatures.images.add(image);
} }
break; break;
case FileBlockHeader.BLOCK_WO00:// World case BLOCK_TE00:
if (blenderKey.shouldLoad(FeaturesToLoad.WORLD)) { Structure textureStructure = block.getStructure(blenderContext);
Structure worldStructure = block.getStructure(blenderContext); int type = ((Number) textureStructure.getFieldValue("type")).intValue();
String worldName = worldStructure.getName(); if (type == TextureHelper.TEX_IMAGE) {
if (blenderKey.getUsedWorld() == null || blenderKey.getUsedWorld().equals(worldName)) { TextureHelper texHelper = blenderContext.getHelper(TextureHelper.class);
LandscapeHelper landscapeHelper = blenderContext.getHelper(LandscapeHelper.class); Texture texture = texHelper.getTexture(textureStructure, null, blenderContext);
Light ambientLight = landscapeHelper.toAmbientLight(worldStructure); if (texture != null) {// null is returned when texture has no image
if(ambientLight != null) { loadedFeatures.textures.add(texture);
loadingResults.addLight(new LightNode(null, ambientLight));
}
loadingResults.setSky(landscapeHelper.toSky(worldStructure));
loadingResults.addFilter(landscapeHelper.toFog(worldStructure));
loadingResults.setBackgroundColor(landscapeHelper.toBackgroundColor(worldStructure));
} }
} else {
LOGGER.fine("Only image textures can be loaded as unlinked assets. Generated textures will be applied to an existing object.");
} }
break; break;
case BLOCK_WO00:// World
LandscapeHelper landscapeHelper = blenderContext.getHelper(LandscapeHelper.class);
Structure worldStructure = block.getStructure(blenderContext);
String worldName = worldStructure.getName();
if (blenderKey.getUsedWorld() == null || blenderKey.getUsedWorld().equals(worldName)) {
Light ambientLight = landscapeHelper.toAmbientLight(worldStructure);
if (ambientLight != null) {
loadedFeatures.objects.add(new LightNode(null, ambientLight));
loadedFeatures.lights.add(ambientLight);
}
loadedFeatures.sky = landscapeHelper.toSky(worldStructure);
loadedFeatures.backgroundColor = landscapeHelper.toBackgroundColor(worldStructure);
Filter fogFilter = landscapeHelper.toFog(worldStructure);
if (fogFilter != null) {
loadedFeatures.filters.add(landscapeHelper.toFog(worldStructure));
}
}
break;
case BLOCK_AC00:
LOGGER.fine("Loading unlinked animations is not yet supported!");
break;
default:
LOGGER.log(Level.FINEST, "Ommiting the block: {0}.", block.getCode());
} }
} }
// bake constraints after everything is loaded LOGGER.fine("Baking constraints after every feature is loaded.");
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
constraintHelper.bakeConstraints(blenderContext); constraintHelper.bakeConstraints(blenderContext);
// load the scene at the very end so that the root nodes have no parent during loading or constraints applying LOGGER.fine("Loading scenes and attaching them to the root object.");
for (FileBlockHeader sceneBlock : sceneBlocks) { for (FileBlockHeader sceneBlock : loadedFeatures.sceneBlocks) {
loadingResults.addScene(this.toScene(sceneBlock.getStructure(blenderContext))); loadedFeatures.scenes.add(this.toScene(sceneBlock.getStructure(blenderContext)));
}
LOGGER.fine("Creating the root node of the model and applying loaded nodes of the scene and loaded features to it.");
Node modelRoot = new Node(blenderKey.getName());
for (Node scene : loadedFeatures.scenes) {
modelRoot.attachChild(scene);
} }
return loadingResults; if (blenderKey.isLoadUnlinkedAssets()) {
LOGGER.fine("Setting loaded content as user data in resulting sptaial.");
Map<String, Map<String, Object>> linkedData = new HashMap<String, Map<String, Object>>();
Map<String, Object> thisFileData = new HashMap<String, Object>();
thisFileData.put("scenes", loadedFeatures.scenes == null ? new ArrayList<Object>() : loadedFeatures.scenes);
thisFileData.put("objects", loadedFeatures.objects == null ? new ArrayList<Object>() : loadedFeatures.objects);
thisFileData.put("meshes", loadedFeatures.meshes == null ? new ArrayList<Object>() : loadedFeatures.meshes);
thisFileData.put("materials", loadedFeatures.materials == null ? new ArrayList<Object>() : loadedFeatures.materials);
thisFileData.put("textures", loadedFeatures.textures == null ? new ArrayList<Object>() : loadedFeatures.textures);
thisFileData.put("images", loadedFeatures.images == null ? new ArrayList<Object>() : loadedFeatures.images);
thisFileData.put("animations", loadedFeatures.animations == null ? new ArrayList<Object>() : loadedFeatures.animations);
thisFileData.put("cameras", loadedFeatures.cameras == null ? new ArrayList<Object>() : loadedFeatures.cameras);
thisFileData.put("lights", loadedFeatures.lights == null ? new ArrayList<Object>() : loadedFeatures.lights);
thisFileData.put("filters", loadedFeatures.filters == null ? new ArrayList<Object>() : loadedFeatures.filters);
thisFileData.put("backgroundColor", loadedFeatures.backgroundColor);
thisFileData.put("sky", loadedFeatures.sky);
linkedData.put("this", thisFileData);
linkedData.putAll(blenderContext.getLinkedFeatures());
modelRoot.setUserData("linkedData", linkedData);
}
return modelRoot;
} catch (BlenderFileException e) { } catch (BlenderFileException e) {
throw new IOException(e.getLocalizedMessage(), e); throw new IOException(e.getLocalizedMessage(), e);
} catch (Exception e) { } catch (Exception e) {
@ -158,62 +224,36 @@ public class BlenderLoader implements AssetLoader {
} }
} }
/**
* 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. * This method converts the given structure to a scene node.
* @param structure * @param structure
* structure of a scene * structure of a scene
* @return scene's node * @return scene's node
* @throws BlenderFileException
* an exception throw when problems with blender file occur
*/ */
private Node toScene(Structure structure) { private Node toScene(Structure structure) throws BlenderFileException {
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Node result = new Node(structure.getName()); Node result = new Node(structure.getName());
try { List<Structure> base = ((Structure) structure.getFieldValue("base")).evaluateListBase();
List<Structure> base = ((Structure) structure.getFieldValue("base")).evaluateListBase(); for (Structure b : base) {
for (Structure b : base) { Pointer pObject = (Pointer) b.getFieldValue("object");
Pointer pObject = (Pointer) b.getFieldValue("object"); if (pObject.isNotNull()) {
if (pObject.isNotNull()) { Structure objectStructure = pObject.fetchData().get(0);
Structure objectStructure = pObject.fetchData().get(0);
Object object = objectHelper.toObject(objectStructure, blenderContext); Object object = objectHelper.toObject(objectStructure, blenderContext);
if (object instanceof LightNode) { if (object instanceof LightNode) {
result.addLight(((LightNode) object).getLight()); result.addLight(((LightNode) object).getLight());// FIXME: check if this is needed !!!
result.attachChild((LightNode) object); result.attachChild((LightNode) object);
} else if (object instanceof Node) { } else if (object instanceof Node) {
if (LOGGER.isLoggable(Level.FINE)) { 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() }); 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) { if (((Node) object).getParent() == null) {
result.attachChild((Spatial) object); result.attachChild((Spatial) object);
}
} }
} }
} }
} catch (BlenderFileException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
} }
return result; return result;
} }
@ -261,7 +301,7 @@ public class BlenderLoader implements AssetLoader {
blenderContext.putHelper(ConstraintHelper.class, new ConstraintHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(ConstraintHelper.class, new ConstraintHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(ParticlesHelper.class, new ParticlesHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(ParticlesHelper.class, new ParticlesHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(LandscapeHelper.class, new LandscapeHelper(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) // reading the blocks (dna block is automatically saved in the blender context when found)
FileBlockHeader sceneFileBlock = null; FileBlockHeader sceneFileBlock = null;
do { do {
@ -269,7 +309,7 @@ public class BlenderLoader implements AssetLoader {
if (!fileBlock.isDnaBlock()) { if (!fileBlock.isDnaBlock()) {
blocks.add(fileBlock); blocks.add(fileBlock);
// save the scene's file block // save the scene's file block
if (fileBlock.getCode() == FileBlockHeader.BLOCK_SC00) { if (fileBlock.getCode() == BlockCode.BLOCK_SC00) {
sceneFileBlock = fileBlock; sceneFileBlock = fileBlock;
} }
} }
@ -287,4 +327,39 @@ public class BlenderLoader implements AssetLoader {
blenderContext = null; blenderContext = null;
blocks = null; blocks = null;
} }
/**
* This class holds the loading results according to the given loading flag.
* @author Marcin Roguski (Kaelthas)
*/
private static class LoadedFeatures {
private List<FileBlockHeader> sceneBlocks = new ArrayList<FileBlockHeader>();
/** The scenes from the file. */
private List<Node> scenes = new ArrayList<Node>();
/** Objects from all scenes. */
private List<Node> objects = new ArrayList<Node>();
/** All meshes. */
private List<TemporalMesh> meshes = new ArrayList<TemporalMesh>();
/** Materials from all objects. */
private List<MaterialContext> materials = new ArrayList<MaterialContext>();
/** Textures from all objects. */
private List<Texture> textures = new ArrayList<Texture>();
/** The images stored in the blender file. */
private List<Texture> images = new ArrayList<Texture>();
/** Animations of all objects. */
private List<Animation> animations = new ArrayList<Animation>();
/** All cameras from the file. */
private List<Camera> cameras = new ArrayList<Camera>();
/** All lights from the file. */
private List<Light> lights = new ArrayList<Light>();
/** Loaded sky. */
private Spatial sky;
/** Scene filters (ie. FOG). */
private List<Filter> filters = new ArrayList<Filter>();
/**
* 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;
}
} }

@ -31,79 +31,10 @@
*/ */
package com.jme3.scene.plugins.blender; 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.animations.AnimationHelper;
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. * This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures.
* * @deprecated this class is deprecated; use BlenderLoader instead
* @author Marcin Roguski (Kaelthas) * @author Marcin Roguski (Kaelthas)
*/ */
public class BlenderModelLoader extends BlenderLoader { 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);
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class);
animationHelper.loadAnimations();
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();
}
}
} }

@ -25,6 +25,7 @@ import com.jme3.scene.plugins.blender.curves.BezierCurve;
import com.jme3.scene.plugins.blender.file.BlenderFileException; import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream; import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.FileBlockHeader; import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
import com.jme3.scene.plugins.blender.file.Pointer; import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure; import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.ObjectHelper; import com.jme3.scene.plugins.blender.objects.ObjectHelper;
@ -48,7 +49,7 @@ public class AnimationHelper extends AbstractBlenderHelper {
*/ */
public void loadAnimations() throws BlenderFileException { public void loadAnimations() throws BlenderFileException {
LOGGER.info("Loading animations that will be later applied to scene features."); LOGGER.info("Loading animations that will be later applied to scene features.");
List<FileBlockHeader> actionHeaders = blenderContext.getFileBlocks(Integer.valueOf(FileBlockHeader.BLOCK_AC00)); List<FileBlockHeader> actionHeaders = blenderContext.getFileBlocks(BlockCode.BLOCK_AC00);
if (actionHeaders != null) { if (actionHeaders != null) {
for (FileBlockHeader header : actionHeaders) { for (FileBlockHeader header : actionHeaders) {
Structure actionStructure = header.getStructure(blenderContext); Structure actionStructure = header.getStructure(blenderContext);
@ -70,7 +71,7 @@ public class AnimationHelper extends AbstractBlenderHelper {
if (actions.size() > 0) { if (actions.size() > 0) {
List<Animation> animations = new ArrayList<Animation>(); List<Animation> animations = new ArrayList<Animation>();
for (BlenderAction action : actions) { for (BlenderAction action : actions) {
SpatialTrack[] tracks = action.toTracks(node); SpatialTrack[] tracks = action.toTracks(node, blenderContext);
if (tracks != null && tracks.length > 0) { if (tracks != null && tracks.length > 0) {
Animation spatialAnimation = new Animation(action.getName(), action.getAnimationTime()); Animation spatialAnimation = new Animation(action.getName(), action.getAnimationTime());
spatialAnimation.setTracks(tracks); spatialAnimation.setTracks(tracks);
@ -109,7 +110,7 @@ public class AnimationHelper extends AbstractBlenderHelper {
if (actions.size() > 0) { if (actions.size() > 0) {
List<Animation> animations = new ArrayList<Animation>(); List<Animation> animations = new ArrayList<Animation>();
for (BlenderAction action : actions) { for (BlenderAction action : actions) {
BoneTrack[] tracks = action.toTracks(skeleton); BoneTrack[] tracks = action.toTracks(skeleton, blenderContext);
if (tracks != null && tracks.length > 0) { if (tracks != null && tracks.length > 0) {
Animation boneAnimation = new Animation(action.getName(), action.getAnimationTime()); Animation boneAnimation = new Animation(action.getName(), action.getAnimationTime());
boneAnimation.setTracks(tracks); boneAnimation.setTracks(tracks);

@ -10,9 +10,8 @@ import java.util.Map.Entry;
import com.jme3.animation.BoneTrack; import com.jme3.animation.BoneTrack;
import com.jme3.animation.Skeleton; import com.jme3.animation.Skeleton;
import com.jme3.animation.SpatialTrack; import com.jme3.animation.SpatialTrack;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.BlenderContext;
/** /**
* An abstract representation of animation. The data stored here is mainly a * An abstract representation of animation. The data stored here is mainly a
@ -69,10 +68,10 @@ public class BlenderAction implements Cloneable {
* the node that will be animated * the node that will be animated
* @return the spatial tracks for the node * @return the spatial tracks for the node
*/ */
public SpatialTrack[] toTracks(Node node) { public SpatialTrack[] toTracks(Node node, BlenderContext blenderContext) {
List<SpatialTrack> tracks = new ArrayList<SpatialTrack>(featuresTracks.size()); List<SpatialTrack> tracks = new ArrayList<SpatialTrack>(featuresTracks.size());
for (Entry<String, Ipo> entry : featuresTracks.entrySet()) { for (Entry<String, Ipo> entry : featuresTracks.entrySet()) {
tracks.add((SpatialTrack) entry.getValue().calculateTrack(0, node.getLocalTranslation(), node.getLocalRotation(), node.getLocalScale(), 1, stopFrame, fps, true)); tracks.add((SpatialTrack) entry.getValue().calculateTrack(0, null, node.getLocalTranslation(), node.getLocalRotation(), node.getLocalScale(), 1, stopFrame, fps, true));
} }
return tracks.toArray(new SpatialTrack[tracks.size()]); return tracks.toArray(new SpatialTrack[tracks.size()]);
} }
@ -84,11 +83,12 @@ public class BlenderAction implements Cloneable {
* the skeleton that will be animated * the skeleton that will be animated
* @return the bone tracks for the node * @return the bone tracks for the node
*/ */
public BoneTrack[] toTracks(Skeleton skeleton) { public BoneTrack[] toTracks(Skeleton skeleton, BlenderContext blenderContext) {
List<BoneTrack> tracks = new ArrayList<BoneTrack>(featuresTracks.size()); List<BoneTrack> tracks = new ArrayList<BoneTrack>(featuresTracks.size());
for (Entry<String, Ipo> entry : featuresTracks.entrySet()) { for (Entry<String, Ipo> entry : featuresTracks.entrySet()) {
int boneIndex = skeleton.getBoneIndex(entry.getKey()); int boneIndex = skeleton.getBoneIndex(entry.getKey());
tracks.add((BoneTrack) entry.getValue().calculateTrack(boneIndex, Vector3f.ZERO, Quaternion.IDENTITY, Vector3f.UNIT_XYZ, 1, stopFrame, fps, false)); BoneContext boneContext = blenderContext.getBoneContext(skeleton.getBone(boneIndex));
tracks.add((BoneTrack) entry.getValue().calculateTrack(boneIndex, boneContext, boneContext.getBone().getBindPosition(), boneContext.getBone().getBindRotation(), boneContext.getBone().getBindScale(), 1, stopFrame, fps, false));
} }
return tracks.toArray(new BoneTrack[tracks.size()]); return tracks.toArray(new BoneTrack[tracks.size()]);
} }

@ -25,10 +25,13 @@ import com.jme3.scene.plugins.blender.objects.ObjectHelper;
*/ */
public class BoneContext { public class BoneContext {
// the flags of the bone // the flags of the bone
public static final int SELECTED = 0x0001; public static final int SELECTED = 0x000001;
public static final int CONNECTED_TO_PARENT = 0x0010; public static final int CONNECTED_TO_PARENT = 0x000010;
public static final int DEFORM = 0x1000; public static final int DEFORM = 0x001000;
public static final int NO_LOCAL_LOCATION = 0x400000;
public static final int NO_INHERIT_SCALE = 0x008000;
public static final int NO_INHERIT_ROTATION = 0x000200;
/** /**
* 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). * 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. * 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.

@ -137,7 +137,7 @@ public class Ipo {
* as jme while other features have different one (Z is UP) * as jme while other features have different one (Z is UP)
* @return bone track for the specified bone * @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) { public Track calculateTrack(int targetIndex, BoneContext boneContext, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean spatialTrack) {
if (calculatedTrack == null) { if (calculatedTrack == null) {
// preparing data for track // preparing data for track
int framesAmount = stopFrame - startFrame; int framesAmount = stopFrame - startFrame;
@ -236,6 +236,15 @@ public class Ipo {
} }
} }
translations[index] = localRotation.multLocal(new Vector3f(translation[0], translation[1], translation[2])); translations[index] = localRotation.multLocal(new Vector3f(translation[0], translation[1], translation[2]));
if(boneContext != null) {
if(boneContext.getBone().getParent() == null && boneContext.is(BoneContext.NO_LOCAL_LOCATION)) {
float temp = translations[index].z;
translations[index].z = -translations[index].y;
translations[index].y = temp;
}
}
if (queternionRotationUsed) { if (queternionRotationUsed) {
rotations[index] = new Quaternion(quaternionRotation[0], quaternionRotation[1], quaternionRotation[2], quaternionRotation[3]); rotations[index] = new Quaternion(quaternionRotation[0], quaternionRotation[1], quaternionRotation[2], quaternionRotation[3]);
} else { } else {
@ -292,7 +301,7 @@ public class Ipo {
} }
@Override @Override
public BoneTrack calculateTrack(int boneIndex, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean boneTrack) { public BoneTrack calculateTrack(int boneIndex, BoneContext boneContext, 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!"); throw new IllegalStateException("Constatnt ipo object cannot be used for calculating bone tracks!");
} }
} }

@ -5,7 +5,6 @@ import java.util.logging.Logger;
import com.jme3.math.FastMath; import com.jme3.math.FastMath;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
import com.jme3.scene.CameraNode;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext; import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException; import com.jme3.scene.plugins.blender.file.BlenderFileException;
@ -43,7 +42,7 @@ public class CameraHelper extends AbstractBlenderHelper {
* an exception is thrown when there are problems with the * an exception is thrown when there are problems with the
* blender file * blender file
*/ */
public CameraNode toCamera(Structure structure, BlenderContext blenderContext) throws BlenderFileException { public Camera toCamera(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
if (blenderVersion >= 250) { if (blenderVersion >= 250) {
return this.toCamera250(structure, blenderContext.getSceneStructure()); return this.toCamera250(structure, blenderContext.getSceneStructure());
} else { } else {
@ -63,7 +62,7 @@ public class CameraHelper extends AbstractBlenderHelper {
* an exception is thrown when there are problems with the * an exception is thrown when there are problems with the
* blender file * blender file
*/ */
private CameraNode toCamera250(Structure structure, Structure sceneStructure) throws BlenderFileException { private Camera toCamera250(Structure structure, Structure sceneStructure) throws BlenderFileException {
int width = DEFAULT_CAM_WIDTH; int width = DEFAULT_CAM_WIDTH;
int height = DEFAULT_CAM_HEIGHT; int height = DEFAULT_CAM_HEIGHT;
if (sceneStructure != null) { if (sceneStructure != null) {
@ -99,7 +98,7 @@ public class CameraHelper extends AbstractBlenderHelper {
sensor = ((Number) structure.getFieldValue(sensorName)).floatValue(); sensor = ((Number) structure.getFieldValue(sensorName)).floatValue();
} }
float focalLength = ((Number) structure.getFieldValue("lens")).floatValue(); float focalLength = ((Number) structure.getFieldValue("lens")).floatValue();
float fov = 2.0f * FastMath.atan((sensor / 2.0f) / focalLength); float fov = 2.0f * FastMath.atan(sensor / 2.0f / focalLength);
if (sensorVertical) { if (sensorVertical) {
fovY = fov * FastMath.RAD_TO_DEG; fovY = fov * FastMath.RAD_TO_DEG;
} else { } else {
@ -111,7 +110,8 @@ public class CameraHelper extends AbstractBlenderHelper {
fovY = ((Number) structure.getFieldValue("ortho_scale")).floatValue(); fovY = ((Number) structure.getFieldValue("ortho_scale")).floatValue();
} }
camera.setFrustumPerspective(fovY, aspect, clipsta, clipend); camera.setFrustumPerspective(fovY, aspect, clipsta, clipend);
return new CameraNode(null, camera); camera.setName(structure.getName());
return camera;
} }
/** /**
@ -124,7 +124,7 @@ public class CameraHelper extends AbstractBlenderHelper {
* an exception is thrown when there are problems with the * an exception is thrown when there are problems with the
* blender file * blender file
*/ */
private CameraNode toCamera249(Structure structure) throws BlenderFileException { private Camera toCamera249(Structure structure) throws BlenderFileException {
Camera camera = new Camera(DEFAULT_CAM_WIDTH, DEFAULT_CAM_HEIGHT); Camera camera = new Camera(DEFAULT_CAM_WIDTH, DEFAULT_CAM_HEIGHT);
int type = ((Number) structure.getFieldValue("type")).intValue(); int type = ((Number) structure.getFieldValue("type")).intValue();
if (type != 0 && type != 1) { if (type != 0 && type != 1) {
@ -142,6 +142,7 @@ public class CameraHelper extends AbstractBlenderHelper {
aspect = ((Number) structure.getFieldValue("ortho_scale")).floatValue(); aspect = ((Number) structure.getFieldValue("ortho_scale")).floatValue();
} }
camera.setFrustumPerspective(aspect, camera.getWidth() / camera.getHeight(), clipsta, clipend); camera.setFrustumPerspective(aspect, camera.getWidth() / camera.getHeight(), clipsta, clipend);
return new CameraNode(null, camera); camera.setName(structure.getName());
return camera;
} }
} }

@ -64,7 +64,7 @@ public abstract class Constraint {
Pointer pData = (Pointer) constraintStructure.getFieldValue("data"); Pointer pData = (Pointer) constraintStructure.getFieldValue("data");
if (pData.isNotNull()) { if (pData.isNotNull()) {
Structure data = pData.fetchData().get(0); Structure data = pData.fetchData().get(0);
constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(data, ownerOMA, blenderContext); constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(data, name, ownerOMA, blenderContext);
Pointer pTar = (Pointer) data.getFieldValue("tar"); Pointer pTar = (Pointer) data.getFieldValue("tar");
if (pTar != null && pTar.isNotNull()) { if (pTar != null && pTar.isNotNull()) {
targetOMA = pTar.getOldMemoryAddress(); targetOMA = pTar.getOldMemoryAddress();
@ -77,7 +77,7 @@ public abstract class Constraint {
} }
} else { } else {
// Null constraint has no data, so create it here // Null constraint has no data, so create it here
constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(null, null, blenderContext); constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(null, name, null, blenderContext);
} }
ownerSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("ownspace")).byteValue()); ownerSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("ownspace")).byteValue());
ipo = influenceIpo; ipo = influenceIpo;

@ -7,6 +7,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.Stack;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.jme3.animation.AnimChannel; import com.jme3.animation.AnimChannel;
@ -38,9 +39,9 @@ import com.jme3.util.TempVars;
* @author Marcin Roguski (Kaelthas) * @author Marcin Roguski (Kaelthas)
*/ */
public class SimulationNode { public class SimulationNode {
private static final Logger LOGGER = Logger.getLogger(SimulationNode.class.getName()); private static final Logger LOGGER = Logger.getLogger(SimulationNode.class.getName());
private Long featureOMA; private Long featureOMA;
/** The blender context. */ /** The blender context. */
private BlenderContext blenderContext; private BlenderContext blenderContext;
/** The name of the node (for debugging purposes). */ /** The name of the node (for debugging purposes). */
@ -51,11 +52,11 @@ public class SimulationNode {
private List<Animation> animations; private List<Animation> animations;
/** The nodes spatial (if null then the boneContext should be set). */ /** The nodes spatial (if null then the boneContext should be set). */
private Spatial spatial; private Spatial spatial;
/** The skeleton of the bone (not null if the node simulated the bone). */ /** The skeleton of the bone (not null if the node simulated the bone). */
private Skeleton skeleton; private Skeleton skeleton;
/** Animation controller for the node's feature. */ /** Animation controller for the node's feature. */
private AnimControl animControl; private AnimControl animControl;
/** /**
* The star transform of a spatial. Needed to properly reset the spatial to * The star transform of a spatial. Needed to properly reset the spatial to
@ -64,7 +65,7 @@ public class SimulationNode {
private Transform spatialStartTransform; private Transform spatialStartTransform;
/** Star transformations for bones. Needed to properly reset the bones. */ /** Star transformations for bones. Needed to properly reset the bones. */
private Map<Bone, Transform> boneStartTransforms; private Map<Bone, Transform> boneStartTransforms;
/** /**
* Builds the nodes tree for the given feature. The feature (bone or * 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) is found by its OMA. The feature must be a root bone or a root
@ -208,8 +209,7 @@ public class SimulationNode {
if (animations != null) { if (animations != null) {
TempVars vars = TempVars.get(); TempVars vars = TempVars.get();
AnimChannel animChannel = animControl.createChannel(); AnimChannel animChannel = animControl.createChannel();
// List<Bone> bonesWithConstraints = this.collectBonesWithConstraints(skeleton);
for (Animation animation : animations) { for (Animation animation : animations) {
float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation); float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation);
int maxFrame = (int) animationTimeBoundaries[0]; int maxFrame = (int) animationTimeBoundaries[0];
@ -233,7 +233,7 @@ public class SimulationNode {
for (Bone rootBone : skeleton.getRoots()) { for (Bone rootBone : skeleton.getRoots()) {
// ignore the 0-indexed bone // ignore the 0-indexed bone
if (skeleton.getBoneIndex(rootBone) > 0) { if (skeleton.getBoneIndex(rootBone) > 0) {
this.applyConstraints(rootBone, alteredOmas, applied, frame); this.applyConstraints(rootBone, alteredOmas, applied, frame, new Stack<Bone>());
} }
} }
@ -294,34 +294,39 @@ public class SimulationNode {
* the set of OMAS of the altered bones (is populated if necessary) * the set of OMAS of the altered bones (is populated if necessary)
* @param frame * @param frame
* the current frame of the animation * the current frame of the animation
* @param bonesStack
* the stack of bones used to avoid infinite loops while applying constraints
*/ */
private void applyConstraints(Bone bone, Set<Long> alteredOmas, Set<Long> applied, int frame) { private void applyConstraints(Bone bone, Set<Long> alteredOmas, Set<Long> applied, int frame, Stack<Bone> bonesStack) {
BoneContext boneContext = blenderContext.getBoneContext(bone); if (!bonesStack.contains(bone)) {
if(!applied.contains(boneContext.getBoneOma())) { bonesStack.push(bone);
List<Constraint> constraints = this.findConstraints(boneContext.getBoneOma(), blenderContext); BoneContext boneContext = blenderContext.getBoneContext(bone);
if (constraints != null && constraints.size() > 0) { if (!applied.contains(boneContext.getBoneOma())) {
// TODO: BEWARE OF INFINITE LOOPS !!!!!!!!!!!!!!!!!!!!!!!!!! List<Constraint> constraints = this.findConstraints(boneContext.getBoneOma(), blenderContext);
for (Constraint constraint : constraints) { if (constraints != null && constraints.size() > 0) {
if (constraint.getTargetOMA() != null && constraint.getTargetOMA() > 0L) { for (Constraint constraint : constraints) {
// first apply constraints of the target bone if (constraint.getTargetOMA() != null && constraint.getTargetOMA() > 0L) {
BoneContext targetBone = blenderContext.getBoneContext(constraint.getTargetOMA()); // first apply constraints of the target bone
this.applyConstraints(targetBone.getBone(), alteredOmas, applied, frame); BoneContext targetBone = blenderContext.getBoneContext(constraint.getTargetOMA());
} this.applyConstraints(targetBone.getBone(), alteredOmas, applied, frame, bonesStack);
constraint.apply(frame); }
if (constraint.getAlteredOmas() != null) { constraint.apply(frame);
alteredOmas.addAll(constraint.getAlteredOmas()); if (constraint.getAlteredOmas() != null) {
alteredOmas.addAll(constraint.getAlteredOmas());
}
alteredOmas.add(boneContext.getBoneOma());
} }
alteredOmas.add(boneContext.getBoneOma());
} }
applied.add(boneContext.getBoneOma());
} }
applied.add(boneContext.getBoneOma());
} List<Bone> children = bone.getChildren();
if (children != null && children.size() > 0) {
List<Bone> children = bone.getChildren(); for (Bone child : bone.getChildren()) {
if (children != null && children.size() > 0) { this.applyConstraints(child, alteredOmas, applied, frame, bonesStack);
for (Bone child : bone.getChildren()) { }
this.applyConstraints(child, alteredOmas, applied, frame);
} }
bonesStack.pop();
} }
} }

@ -30,6 +30,8 @@ public abstract class ConstraintDefinition {
protected Set<Long> alteredOmas; protected Set<Long> alteredOmas;
/** The variable that determines if the constraint will alter the track in any way. */ /** The variable that determines if the constraint will alter the track in any way. */
protected boolean trackToBeChanged = true; protected boolean trackToBeChanged = true;
/** The name of the constraint. */
protected String constraintName;
/** /**
* Loads a constraint definition based on the constraint definition * Loads a constraint definition based on the constraint definition
@ -53,6 +55,10 @@ public abstract class ConstraintDefinition {
constraintHelper = (ConstraintHelper) (blenderContext == null ? null : blenderContext.getHelper(ConstraintHelper.class)); constraintHelper = (ConstraintHelper) (blenderContext == null ? null : blenderContext.getHelper(ConstraintHelper.class));
this.ownerOMA = ownerOMA; this.ownerOMA = ownerOMA;
} }
public void setConstraintName(String constraintName) {
this.constraintName = constraintName;
}
/** /**
* @return determines if the definition of the constraint will change the bone in any way; in most cases * @return determines if the definition of the constraint will change the bone in any way; in most cases

@ -92,7 +92,7 @@ public class ConstraintDefinitionFactory {
* this exception is thrown when the blender file is somehow * this exception is thrown when the blender file is somehow
* corrupted * corrupted
*/ */
public static ConstraintDefinition createConstraintDefinition(Structure constraintStructure, Long ownerOMA, BlenderContext blenderContext) throws BlenderFileException { public static ConstraintDefinition createConstraintDefinition(Structure constraintStructure, String constraintName, Long ownerOMA, BlenderContext blenderContext) throws BlenderFileException {
if (constraintStructure == null) { if (constraintStructure == null) {
return new ConstraintDefinitionNull(null, ownerOMA, blenderContext); return new ConstraintDefinitionNull(null, ownerOMA, blenderContext);
} }
@ -100,7 +100,9 @@ public class ConstraintDefinitionFactory {
Class<? extends ConstraintDefinition> constraintDefinitionClass = CONSTRAINT_CLASSES.get(constraintClassName); Class<? extends ConstraintDefinition> constraintDefinitionClass = CONSTRAINT_CLASSES.get(constraintClassName);
if (constraintDefinitionClass != null) { if (constraintDefinitionClass != null) {
try { try {
return (ConstraintDefinition) constraintDefinitionClass.getDeclaredConstructors()[0].newInstance(constraintStructure, ownerOMA, blenderContext); ConstraintDefinition def = (ConstraintDefinition) constraintDefinitionClass.getDeclaredConstructors()[0].newInstance(constraintStructure, ownerOMA, blenderContext);
def.setConstraintName(constraintName);
return def;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new BlenderFileException(e.getLocalizedMessage(), e); throw new BlenderFileException(e.getLocalizedMessage(), e);
} catch (SecurityException e) { } catch (SecurityException e) {
@ -113,9 +115,9 @@ public class ConstraintDefinitionFactory {
throw new BlenderFileException(e.getLocalizedMessage(), e); throw new BlenderFileException(e.getLocalizedMessage(), e);
} }
} else { } else {
String constraintName = UNSUPPORTED_CONSTRAINTS.get(constraintClassName); String unsupportedConstraintClassName = UNSUPPORTED_CONSTRAINTS.get(constraintClassName);
if (constraintName != null) { if (unsupportedConstraintClassName != null) {
return new UnsupportedConstraintDefinition(constraintName); return new UnsupportedConstraintDefinition(unsupportedConstraintClassName);
} else { } else {
throw new BlenderFileException("Unknown constraint type: " + constraintClassName); throw new BlenderFileException("Unknown constraint type: " + constraintClassName);
} }

@ -1,40 +1,44 @@
package com.jme3.scene.plugins.blender.constraints.definitions; package com.jme3.scene.plugins.blender.constraints.definitions;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import org.ejml.simple.SimpleMatrix;
import com.jme3.animation.Bone; import com.jme3.animation.Bone;
import com.jme3.math.Transform; import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext; import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.BoneContext; 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.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure; import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.math.DQuaternion; import com.jme3.scene.plugins.blender.math.DQuaternion;
import com.jme3.scene.plugins.blender.math.DTransform; import com.jme3.scene.plugins.blender.math.DTransform;
import com.jme3.scene.plugins.blender.math.Matrix;
import com.jme3.scene.plugins.blender.math.Vector3d; import com.jme3.scene.plugins.blender.math.Vector3d;
/** /**
* The Inverse Kinematics constraint. * A definiotion of a Inverse Kinematics constraint. This implementation uses Jacobian pseudoinverse algorithm.
* *
* @author Wesley Shillingford (wezrule)
* @author Marcin Roguski (Kaelthas) * @author Marcin Roguski (Kaelthas)
*/ */
public class ConstraintDefinitionIK extends ConstraintDefinition { public class ConstraintDefinitionIK extends ConstraintDefinition {
private static final float MIN_DISTANCE = 0.0001f; private static final float MIN_DISTANCE = 0.001f;
private static final int FLAG_USE_TAIL = 0x01; private static final float MIN_ANGLE_CHANGE = 0.001f;
private static final int FLAG_POSITION = 0x20; private static final int FLAG_USE_TAIL = 0x01;
private static final int FLAG_POSITION = 0x20;
private BonesChain bones;
/** The number of affected bones. Zero means that all parent bones of the current bone should take part in baking. */ /** The number of affected bones. Zero means that all parent bones of the current bone should take part in baking. */
private int bonesAffected; private int bonesAffected;
/** The total length of the bone chain. Useful for optimisation of computations speed in some cases. */
private double chainLength;
/** Indicates if the tail of the bone should be used or not. */ /** Indicates if the tail of the bone should be used or not. */
private boolean useTail; private boolean useTail;
/** The amount of iterations of the algorithm. */ /** The amount of iterations of the algorithm. */
private int iterations; private int iterations;
/** The count of bones' chain. */
private int bonesCount = -1;
public ConstraintDefinitionIK(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { public ConstraintDefinitionIK(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext); super(constraintData, ownerOMA, blenderContext);
@ -51,101 +55,104 @@ public class ConstraintDefinitionIK extends ConstraintDefinition {
} }
} }
/**
* Below are the variables that only need to be allocated once for IK constraint instance.
*/
/** Temporal quaternion. */
private DQuaternion tempDQuaternion = new DQuaternion();
/** Temporal matrix column. */
private Vector3d col = new Vector3d();
/** Effector's position change. */
private Matrix deltaP = new Matrix(3, 1);
/** The current target position. */
private Vector3d target = new Vector3d();
/** Rotation vectors for each joint (allocated when we know the size of a bones' chain. */
private Vector3d[] rotationVectors;
/** The Jacobian matrix. Allocated when the bones' chain size is known. */
private Matrix J;
@Override @Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
if (influence == 0 || !trackToBeChanged || targetTransform == null) { if (influence == 0 || !trackToBeChanged || targetTransform == null || bonesCount == 0) {
return;// no need to do anything return;// no need to do anything
} }
DQuaternion q = new DQuaternion();
Vector3d t = new Vector3d(targetTransform.getTranslation()); if (bones == null) {
List<BoneContext> bones = this.loadBones(); bones = new BonesChain((Bone) this.getOwner(), useTail, bonesAffected, alteredOmas, blenderContext);
}
if (bones.size() == 0) { if (bones.size() == 0) {
bonesCount = 0;
return;// no need to do anything return;// no need to do anything
} }
double distanceFromTarget = Double.MAX_VALUE; double distanceFromTarget = Double.MAX_VALUE;
target.set(targetTransform.getTranslation().x, targetTransform.getTranslation().y, targetTransform.getTranslation().z);
int iterations = this.iterations; if (bonesCount < 0) {
if (bones.size() == 1) { bonesCount = bones.size();
iterations = 1;// if only one bone is in the chain then only one iteration that will properly rotate it will be needed rotationVectors = new Vector3d[bonesCount];
} else { for (int i = 0; i < bonesCount; ++i) {
// if the target cannot be rached by the bones' chain then the chain will become straight and point towards the target rotationVectors[i] = new Vector3d();
// in this case only one iteration will be needed, computed from the root to top bone
BoneContext rootBone = bones.get(bones.size() - 1);
Transform rootBoneTransform = constraintHelper.getTransform(rootBone.getArmatureObjectOMA(), rootBone.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD);
if (t.distance(new Vector3d(rootBoneTransform.getTranslation())) >= chainLength) {
Collections.reverse(bones);
for (BoneContext boneContext : bones) {
Bone bone = boneContext.getBone();
DTransform boneTransform = new DTransform(constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD));
Vector3d e = boneTransform.getTranslation().add(boneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(boneContext.getLength()));// effector
Vector3d j = boneTransform.getTranslation(); // current join position
Vector3d currentDir = e.subtractLocal(j).normalizeLocal();
Vector3d target = t.subtract(j).normalizeLocal();
double angle = currentDir.angleBetween(target);
if (angle != 0) {
Vector3d cross = currentDir.crossLocal(target).normalizeLocal();
q.fromAngleAxis(angle, cross);
if (boneContext.isLockX()) {
q.set(0, q.getY(), q.getZ(), q.getW());
}
if (boneContext.isLockY()) {
q.set(q.getX(), 0, q.getZ(), q.getW());
}
if (boneContext.isLockZ()) {
q.set(q.getX(), q.getY(), 0, q.getW());
}
boneTransform.getRotation().set(q.multLocal(boneTransform.getRotation()));
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD, boneTransform.toTransform());
}
}
iterations = 0;
} }
J = new Matrix(3, bonesCount);
} }
BoneContext topBone = bones.get(0); BoneContext topBone = bones.get(0);
for (int i = 0; i < iterations && distanceFromTarget > MIN_DISTANCE; ++i) { for (int i = 0; i < iterations; ++i) {
for (BoneContext boneContext : bones) { DTransform topBoneTransform = bones.getWorldTransform(topBone);
Bone bone = boneContext.getBone(); Vector3d e = topBoneTransform.getTranslation().add(topBoneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(topBone.getLength()));// effector
DTransform topBoneTransform = new DTransform(constraintHelper.getTransform(topBone.getArmatureObjectOMA(), topBone.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD)); distanceFromTarget = e.distance(target);
DTransform boneWorldTransform = new DTransform(constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD)); if (distanceFromTarget <= MIN_DISTANCE) {
break;
}
Vector3d e = topBoneTransform.getTranslation().addLocal(topBoneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(topBone.getLength()));// effector deltaP.setColumn(0, 0, target.x - e.x, target.y - e.y, target.z - e.z);
int column = 0;
for (BoneContext boneContext : bones) {
DTransform boneWorldTransform = bones.getWorldTransform(boneContext);
Vector3d j = boneWorldTransform.getTranslation(); // current join position Vector3d j = boneWorldTransform.getTranslation(); // current join position
Vector3d vectorFromJointToEffector = e.subtract(j);
vectorFromJointToEffector.cross(target.subtract(j), rotationVectors[column]).normalizeLocal();
rotationVectors[column].cross(vectorFromJointToEffector, col);
J.setColumn(col, column++);
}
Matrix J_1 = J.pseudoinverse();
Vector3d currentDir = e.subtractLocal(j).normalizeLocal(); SimpleMatrix deltaThetas = J_1.mult(deltaP);
Vector3d target = t.subtract(j).normalizeLocal(); if (deltaThetas.elementMaxAbs() < MIN_ANGLE_CHANGE) {
double angle = currentDir.angleBetween(target); break;
if (angle != 0) { }
Vector3d cross = currentDir.crossLocal(target).normalizeLocal(); for (int j = 0; j < deltaThetas.numRows(); ++j) {
q.fromAngleAxis(angle, cross); double angle = deltaThetas.get(j, 0);
Vector3d rotationVector = rotationVectors[j];
tempDQuaternion.fromAngleAxis(angle, rotationVector);
BoneContext boneContext = bones.get(j);
Bone bone = boneContext.getBone();
if (bone.equals(this.getOwner())) {
if (boneContext.isLockX()) { if (boneContext.isLockX()) {
q.set(0, q.getY(), q.getZ(), q.getW()); tempDQuaternion.set(0, tempDQuaternion.getY(), tempDQuaternion.getZ(), tempDQuaternion.getW());
} }
if (boneContext.isLockY()) { if (boneContext.isLockY()) {
q.set(q.getX(), 0, q.getZ(), q.getW()); tempDQuaternion.set(tempDQuaternion.getX(), 0, tempDQuaternion.getZ(), tempDQuaternion.getW());
} }
if (boneContext.isLockZ()) { if (boneContext.isLockZ()) {
q.set(q.getX(), q.getY(), 0, q.getW()); tempDQuaternion.set(tempDQuaternion.getX(), tempDQuaternion.getY(), 0, tempDQuaternion.getW());
} }
boneWorldTransform.getRotation().set(q.multLocal(boneWorldTransform.getRotation()));
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD, boneWorldTransform.toTransform());
} else {
iterations = 0;
break;
} }
DTransform boneTransform = bones.getWorldTransform(boneContext);
boneTransform.getRotation().set(tempDQuaternion.mult(boneTransform.getRotation()));
bones.setWorldTransform(boneContext, boneTransform);
} }
}
DTransform topBoneTransform = new DTransform(constraintHelper.getTransform(topBone.getArmatureObjectOMA(), topBone.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD)); // applying the results
Vector3d e = topBoneTransform.getTranslation().addLocal(topBoneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(topBone.getLength()));// effector for (int i = bonesCount - 1; i >= 0; --i) {
distanceFromTarget = e.distance(t); BoneContext boneContext = bones.get(i);
DTransform transform = bones.getWorldTransform(boneContext);
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD, transform.toTransform());
} }
bones = null;// need to reload them again
} }
@Override @Override
@ -153,56 +160,68 @@ public class ConstraintDefinitionIK extends ConstraintDefinition {
return "Inverse kinematics"; return "Inverse kinematics";
} }
@Override
public boolean isTargetRequired() {
return true;
}
/** /**
* @return the bone contexts of all bones that will be used in this constraint computations * Loaded bones' chain. This class allows to operate on transform matrices that use double precision in computations.
* Only the final result is being transformed to single precision numbers.
*
* @author Marcin Roguski (Kaelthas)
*/ */
private List<BoneContext> loadBones() { private static class BonesChain extends ArrayList<BoneContext> {
List<BoneContext> bones = new ArrayList<BoneContext>(); private static final long serialVersionUID = -1850524345643600718L;
Bone bone = (Bone) this.getOwner();
if (bone == null) {
return bones;
}
if (!useTail) {
bone = bone.getParent();
}
chainLength = 0;
while (bone != null) {
BoneContext boneContext = blenderContext.getBoneContext(bone);
chainLength += boneContext.getLength();
bones.add(boneContext);
alteredOmas.add(boneContext.getBoneOma());
if (bonesAffected != 0 && bones.size() >= bonesAffected) {
break;
}
// need to add spaces between bones to the chain length
Transform boneWorldTransform = constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD);
Vector3f boneWorldTranslation = boneWorldTransform.getTranslation();
bone = bone.getParent(); private List<Matrix> bonesMatrices = new ArrayList<Matrix>();
public BonesChain(Bone bone, boolean useTail, int bonesAffected, Collection<Long> alteredOmas, BlenderContext blenderContext) {
if (bone != null) { if (bone != null) {
boneContext = blenderContext.getBoneContext(bone); ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
Transform parentWorldTransform = constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD); if (!useTail) {
Vector3f parentWorldTranslation = parentWorldTransform.getTranslation(); bone = bone.getParent();
chainLength += boneWorldTranslation.distance(parentWorldTranslation); }
while (bone != null && (bonesAffected <= 0 || this.size() < bonesAffected)) {
BoneContext boneContext = blenderContext.getBoneContext(bone);
this.add(boneContext);
alteredOmas.add(boneContext.getBoneOma());
Space space = this.size() < bonesAffected ? Space.CONSTRAINT_SPACE_LOCAL : Space.CONSTRAINT_SPACE_WORLD;
Transform transform = constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), space);
bonesMatrices.add(new DTransform(transform).toMatrix());
bone = bone.getParent();
}
} }
} }
return bones;
}
@Override public DTransform getWorldTransform(BoneContext bone) {
public boolean isTrackToBeChanged() { int index = this.indexOf(bone);
if (trackToBeChanged) { return this.getWorldMatrix(index).toTransform();
// need to check the bone structure too (when constructor was called not all of the bones might have been loaded yet)
// that is why it is also checked here
List<BoneContext> bones = this.loadBones();
trackToBeChanged = bones.size() > 0;
} }
return trackToBeChanged;
}
@Override public void setWorldTransform(BoneContext bone, DTransform transform) {
public boolean isTargetRequired() { int index = this.indexOf(bone);
return true; Matrix boneMatrix = transform.toMatrix();
if (index < this.size() - 1) {
// computing the current bone local transform
Matrix parentWorldMatrix = this.getWorldMatrix(index + 1);
SimpleMatrix m = parentWorldMatrix.invert().mult(boneMatrix);
boneMatrix = new Matrix(m);
}
bonesMatrices.set(index, boneMatrix);
}
public Matrix getWorldMatrix(int index) {
if (index == this.size() - 1) {
return new Matrix(bonesMatrices.get(this.size() - 1));
}
SimpleMatrix result = this.getWorldMatrix(index + 1);
result = result.mult(bonesMatrices.get(index));
return new Matrix(result);
}
} }
} }

@ -1,10 +1,11 @@
package com.jme3.scene.plugins.blender.curves; package com.jme3.scene.plugins.blender.curves;
import java.util.ArrayList;
import java.util.List;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.file.DynamicArray; import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Structure; 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 * A class that helps to calculate the bezier curves calues. It uses doubles for performing calculations to minimize
@ -12,21 +13,26 @@ import java.util.List;
* @author Marcin Roguski (Kaelthas) * @author Marcin Roguski (Kaelthas)
*/ */
public class BezierCurve { public class BezierCurve {
private static final int IPO_CONSTANT = 0;
private static final int IPO_LINEAR = 1;
private static final int IPO_BEZIER = 2;
public static final int X_VALUE = 0; public static final int X_VALUE = 0;
public static final int Y_VALUE = 1; public static final int Y_VALUE = 1;
public static final int Z_VALUE = 2; public static final int Z_VALUE = 2;
/** /**
* The type of the curve. Describes the data it modifies. * The type of the curve. Describes the data it modifies.
* Used in ipos calculations. * Used in ipos calculations.
*/ */
private int type; private int type;
/** The dimension of the curve. */ /** The dimension of the curve. */
private int dimension; private int dimension;
/** A table of the bezier points. */ /** A table of the bezier points. */
private double[][][] bezierPoints; private double[][][] bezierPoints;
/** Array that stores a radius for each bezier triple. */ /** Array that stores a radius for each bezier triple. */
private double[] radiuses; private double[] radiuses;
/** Interpolation types of the bezier triples. */
private int[] interpolations;
public BezierCurve(final int type, final List<Structure> bezTriples, final int dimension) { public BezierCurve(final int type, final List<Structure> bezTriples, final int dimension) {
this(type, bezTriples, dimension, false); this(type, bezTriples, dimension, false);
@ -44,6 +50,7 @@ public class BezierCurve {
// the third index specifies the coordinates of the specific point in a bezier triple // the third index specifies the coordinates of the specific point in a bezier triple
bezierPoints = new double[bezTriples.size()][3][dimension]; bezierPoints = new double[bezTriples.size()][3][dimension];
radiuses = new double[bezTriples.size()]; radiuses = new double[bezTriples.size()];
interpolations = new int[bezTriples.size()];
int i = 0, j, k; int i = 0, j, k;
for (Structure bezTriple : bezTriples) { for (Structure bezTriple : bezTriples) {
DynamicArray<Number> vec = (DynamicArray<Number>) bezTriple.getFieldValue("vec"); DynamicArray<Number> vec = (DynamicArray<Number>) bezTriple.getFieldValue("vec");
@ -57,7 +64,8 @@ public class BezierCurve {
bezierPoints[i][j][1] = temp; bezierPoints[i][j][1] = temp;
} }
} }
radiuses[i++] = ((Number) bezTriple.getFieldValue("radius")).floatValue(); radiuses[i] = ((Number) bezTriple.getFieldValue("radius")).floatValue();
interpolations[i++] = ((Number) bezTriple.getFieldValue("ipo", IPO_BEZIER)).intValue();
} }
} }
@ -75,10 +83,19 @@ public class BezierCurve {
for (int i = 0; i < bezierPoints.length - 1; ++i) { for (int i = 0; i < bezierPoints.length - 1; ++i) {
if (frame >= bezierPoints[i][1][0] && frame <= bezierPoints[i + 1][1][0]) { if (frame >= bezierPoints[i][1][0] && frame <= bezierPoints[i + 1][1][0]) {
double t = (frame - bezierPoints[i][1][0]) / (bezierPoints[i + 1][1][0] - bezierPoints[i][1][0]); double t = (frame - bezierPoints[i][1][0]) / (bezierPoints[i + 1][1][0] - bezierPoints[i][1][0]);
double oneMinusT = 1.0f - t; switch (interpolations[i]) {
double oneMinusT2 = oneMinusT * oneMinusT; case IPO_BEZIER:
double t2 = t * t; double oneMinusT = 1.0f - 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; double oneMinusT2 = oneMinusT * oneMinusT;
double 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;
case IPO_LINEAR:
return (1f - t) * bezierPoints[i][1][valuePart] + t * bezierPoints[i + 1][1][valuePart];
case IPO_CONSTANT:
return bezierPoints[i][1][valuePart];
default:
throw new IllegalStateException("Unknown interpolation type for curve: " + interpolations[i]);
}
} }
} }
if (frame < bezierPoints[0][1][0]) { if (frame < bezierPoints[0][1][0]) {

@ -31,6 +31,9 @@
*/ */
package com.jme3.scene.plugins.blender.file; package com.jme3.scene.plugins.blender.file;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.scene.plugins.blender.BlenderContext; import com.jme3.scene.plugins.blender.BlenderContext;
/** /**
@ -39,39 +42,23 @@ import com.jme3.scene.plugins.blender.BlenderContext;
* @author Marcin Roguski * @author Marcin Roguski
*/ */
public class FileBlockHeader { public class FileBlockHeader {
private static final Logger LOGGER = Logger.getLogger(FileBlockHeader.class.getName());
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]. */ /** Identifier of the file-block [4 bytes]. */
private int code; private BlockCode code;
/** Total length of the data after the file-block-header [4 bytes]. */ /** Total length of the data after the file-block-header [4 bytes]. */
private int size; private int size;
/** /**
* Memory address the structure was located when written to disk [4 or 8 bytes (defined in file header as a pointer * Memory address the structure was located when written to disk [4 or 8 bytes (defined in file header as a pointer
* size)]. * size)].
*/ */
private long oldMemoryAddress; private long oldMemoryAddress;
/** Index of the SDNA structure [4 bytes]. */ /** Index of the SDNA structure [4 bytes]. */
private int sdnaIndex; private int sdnaIndex;
/** Number of structure located in this file-block [4 bytes]. */ /** Number of structure located in this file-block [4 bytes]. */
private int count; private int count;
/** Start position of the block's data in the stream. */ /** Start position of the block's data in the stream. */
private int blockPosition; private int blockPosition;
/** /**
* Constructor. Loads the block header from the given stream during instance creation. * Constructor. Loads the block header from the given stream during instance creation.
@ -84,13 +71,13 @@ public class FileBlockHeader {
*/ */
public FileBlockHeader(BlenderInputStream inputStream, BlenderContext blenderContext) throws BlenderFileException { public FileBlockHeader(BlenderInputStream inputStream, BlenderContext blenderContext) throws BlenderFileException {
inputStream.alignPosition(4); inputStream.alignPosition(4);
code = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte(); code = BlockCode.valueOf(inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte());
size = inputStream.readInt(); size = inputStream.readInt();
oldMemoryAddress = inputStream.readPointer(); oldMemoryAddress = inputStream.readPointer();
sdnaIndex = inputStream.readInt(); sdnaIndex = inputStream.readInt();
count = inputStream.readInt(); count = inputStream.readInt();
blockPosition = inputStream.getPosition(); blockPosition = inputStream.getPosition();
if (FileBlockHeader.BLOCK_DNA1 == code) { if (BlockCode.BLOCK_DNA1 == code) {
blenderContext.setBlockData(new DnaBlockData(inputStream, blenderContext)); blenderContext.setBlockData(new DnaBlockData(inputStream, blenderContext));
} else { } else {
inputStream.setPosition(blockPosition + size); inputStream.setPosition(blockPosition + size);
@ -116,7 +103,7 @@ public class FileBlockHeader {
* This method returns the code of this data block. * This method returns the code of this data block.
* @return the code of this data block * @return the code of this data block
*/ */
public int getCode() { public BlockCode getCode() {
return code; return code;
} }
@ -157,7 +144,7 @@ public class FileBlockHeader {
* @return true if this block is the last one in the file nad false otherwise * @return true if this block is the last one in the file nad false otherwise
*/ */
public boolean isLastBlock() { public boolean isLastBlock() {
return FileBlockHeader.BLOCK_ENDB == code; return BlockCode.BLOCK_ENDB == code;
} }
/** /**
@ -165,25 +152,62 @@ public class FileBlockHeader {
* @return true if this block is the SDNA block and false otherwise * @return true if this block is the SDNA block and false otherwise
*/ */
public boolean isDnaBlock() { public boolean isDnaBlock() {
return FileBlockHeader.BLOCK_DNA1 == code; return BlockCode.BLOCK_DNA1 == code;
} }
@Override @Override
public String toString() { public String toString() {
return "FILE BLOCK HEADER [" + this.codeToString(code) + " : " + size + " : " + oldMemoryAddress + " : " + sdnaIndex + " : " + count + "]"; return "FILE BLOCK HEADER [" + code.toString() + " : " + size + " : " + oldMemoryAddress + " : " + sdnaIndex + " : " + count + "]";
} }
/** public static enum BlockCode {
* This method transforms the coded bloch id into a string value. BLOCK_ME00('M' << 24 | 'E' << 16), // mesh
* @param code BLOCK_CA00('C' << 24 | 'A' << 16), // camera
* the id of the block BLOCK_LA00('L' << 24 | 'A' << 16), // lamp
* @return the string value of the block id BLOCK_OB00('O' << 24 | 'B' << 16), // object
*/ BLOCK_MA00('M' << 24 | 'A' << 16), // material
protected String codeToString(int code) { BLOCK_SC00('S' << 24 | 'C' << 16), // scene
char c1 = (char) ((code & 0xFF000000) >> 24); BLOCK_WO00('W' << 24 | 'O' << 16), // world
char c2 = (char) ((code & 0xFF0000) >> 16); BLOCK_TX00('T' << 24 | 'X' << 16), // texture
char c3 = (char) ((code & 0xFF00) >> 8); BLOCK_IP00('I' << 24 | 'P' << 16), // ipo
char c4 = (char) (code & 0xFF); BLOCK_AC00('A' << 24 | 'C' << 16), // action
return String.valueOf(c1) + c2 + c3 + c4; BLOCK_IM00('I' << 24 | 'M' << 16), // image
BLOCK_TE00('T' << 24 | 'E' << 16),
BLOCK_WM00('W' << 24 | 'M' << 16),
BLOCK_SR00('S' << 24 | 'R' << 16),
BLOCK_SN00('S' << 24 | 'N' << 16),
BLOCK_BR00('B' << 24 | 'R' << 16),
BLOCK_LS00('L' << 24 | 'S' << 16),
BLOCK_GR00('G' << 24 | 'R' << 16),
BLOCK_AR00('A' << 24 | 'R' << 16),
BLOCK_GLOB('G' << 24 | 'L' << 16 | 'O' << 8 | 'B'),
BLOCK_REND('R' << 24 | 'E' << 16 | 'N' << 8 | 'D'),
BLOCK_DATA('D' << 24 | 'A' << 16 | 'T' << 8 | 'A'),
BLOCK_DNA1('D' << 24 | 'N' << 16 | 'A' << 8 | '1'),
BLOCK_ENDB('E' << 24 | 'N' << 16 | 'D' << 8 | 'B'),
BLOCK_TEST('T' << 24 | 'E' << 16 | 'S' << 8 | 'T'),
BLOCK_UNKN(0);
private int code;
private BlockCode(int code) {
this.code = code;
}
public static BlockCode valueOf(int code) {
for (BlockCode blockCode : BlockCode.values()) {
if (blockCode.code == code) {
return blockCode;
}
}
byte[] codeBytes = new byte[] { (byte) (code >> 24 & 0xFF), (byte) (code >> 16 & 0xFF), (byte) (code >> 8 & 0xFF), (byte) (code & 0xFF) };
for (int i = 0; i < codeBytes.length; ++i) {
if (codeBytes[i] == 0) {
codeBytes[i] = '0';
}
}
LOGGER.log(Level.WARNING, "Unknown block header: {0}", new String(codeBytes));
return BLOCK_UNKN;
}
} }
} }

@ -254,7 +254,8 @@ public class Structure implements Cloneable {
Structure id = (Structure) fieldValue; Structure id = (Structure) fieldValue;
return id == null ? null : id.getFieldValue("name").toString().substring(2);// blender adds 2-charactes as a name prefix return id == null ? null : id.getFieldValue("name").toString().substring(2);// blender adds 2-charactes as a name prefix
} }
return null; Object name = this.getFieldValue("name", null);
return name == null ? null : name.toString().substring(2);
} }
@Override @Override

@ -208,6 +208,6 @@ public class LandscapeHelper extends AbstractBlenderHelper {
} }
LOGGER.fine("Sky texture created. Creating sky."); LOGGER.fine("Sky texture created. Creating sky.");
return SkyFactory.createSky(blenderContext.getAssetManager(), texture, false); return SkyFactory.createSky(blenderContext.getAssetManager(), texture, SkyFactory.EnvMapType.CubeMap);
} }
} }

@ -40,7 +40,6 @@ import com.jme3.light.PointLight;
import com.jme3.light.SpotLight; import com.jme3.light.SpotLight;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath; import com.jme3.math.FastMath;
import com.jme3.scene.LightNode;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext; import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
@ -67,8 +66,8 @@ public class LightHelper extends AbstractBlenderHelper {
super(blenderVersion, blenderContext); super(blenderVersion, blenderContext);
} }
public LightNode toLight(Structure structure, BlenderContext blenderContext) throws BlenderFileException { public Light toLight(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
LightNode result = (LightNode) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedDataType.FEATURE); Light result = (Light) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedDataType.FEATURE);
if (result != null) { if (result != null) {
return result; return result;
} }
@ -111,6 +110,7 @@ public class LightHelper extends AbstractBlenderHelper {
float g = ((Number) structure.getFieldValue("g")).floatValue(); float g = ((Number) structure.getFieldValue("g")).floatValue();
float b = ((Number) structure.getFieldValue("b")).floatValue(); float b = ((Number) structure.getFieldValue("b")).floatValue();
light.setColor(new ColorRGBA(r, g, b, 1.0f)); light.setColor(new ColorRGBA(r, g, b, 1.0f));
return new LightNode(structure.getName(), light); light.setName(structure.getName());
return light;
} }
} }

@ -1,11 +1,15 @@
package com.jme3.scene.plugins.blender.materials; package com.jme3.scene.plugins.blender.materials;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.Savable;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode; import com.jme3.material.RenderState.BlendMode;
import com.jme3.material.RenderState.FaceCullMode; import com.jme3.material.RenderState.FaceCullMode;
@ -30,7 +34,7 @@ import com.jme3.util.BufferUtils;
* This class holds the data about the material. * This class holds the data about the material.
* @author Marcin Roguski (Kaelthas) * @author Marcin Roguski (Kaelthas)
*/ */
public final class MaterialContext { public final class MaterialContext implements Savable {
private static final Logger LOGGER = Logger.getLogger(MaterialContext.class.getName()); private static final Logger LOGGER = Logger.getLogger(MaterialContext.class.getName());
// texture mapping types // texture mapping types
@ -67,7 +71,7 @@ public final class MaterialContext {
int diff_shader = ((Number) structure.getFieldValue("diff_shader")).intValue(); int diff_shader = ((Number) structure.getFieldValue("diff_shader")).intValue();
diffuseShader = DiffuseShader.values()[diff_shader]; diffuseShader = DiffuseShader.values()[diff_shader];
ambientFactor = ((Number) structure.getFieldValue("amb")).floatValue(); ambientFactor = ((Number) structure.getFieldValue("amb")).floatValue();
if (shadeless) { if (shadeless) {
float r = ((Number) structure.getFieldValue("r")).floatValue(); float r = ((Number) structure.getFieldValue("r")).floatValue();
float g = ((Number) structure.getFieldValue("g")).floatValue(); float g = ((Number) structure.getFieldValue("g")).floatValue();
@ -107,6 +111,13 @@ public final class MaterialContext {
this.transparent = transparent; this.transparent = transparent;
} }
/**
* @return the name of the material
*/
public String getName() {
return name;
}
/** /**
* Applies material to a given geometry. * Applies material to a given geometry.
* *
@ -314,4 +325,14 @@ public final class MaterialContext {
float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue(); float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue();
return new ColorRGBA(r, g, b, alpha); return new ColorRGBA(r, g, b, alpha);
} }
@Override
public void write(JmeExporter e) throws IOException {
throw new IOException("Material context is not for saving! It implements savable only to be passed to another blend file as a Savable in user data!");
}
@Override
public void read(JmeImporter e) throws IOException {
throw new IOException("Material context is not for loading! It implements savable only to be passed to another blend file as a Savable in user data!");
}
} }

@ -51,6 +51,7 @@ import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.shader.VarType; import com.jme3.shader.VarType;
import com.jme3.texture.Image; import com.jme3.texture.Image;
import com.jme3.texture.Image.Format; import com.jme3.texture.Image.Format;
import com.jme3.texture.image.ColorSpace;
import com.jme3.texture.Texture; import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
@ -161,12 +162,17 @@ public class MaterialHelper extends AbstractBlenderHelper {
* an exception is throw when problems with blend file occur * an exception is throw when problems with blend file occur
*/ */
public MaterialContext toMaterialContext(Structure structure, BlenderContext blenderContext) throws BlenderFileException { public MaterialContext toMaterialContext(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading material.");
MaterialContext result = (MaterialContext) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedDataType.FEATURE); MaterialContext result = (MaterialContext) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedDataType.FEATURE);
if (result != null) { if (result != null) {
return result; return result;
} }
if ("ID".equals(structure.getType())) {
LOGGER.fine("Loading material from external blend file.");
return (MaterialContext) this.loadLibrary(structure);
}
LOGGER.fine("Loading material.");
result = new MaterialContext(structure, blenderContext); result = new MaterialContext(structure, blenderContext);
LOGGER.log(Level.FINE, "Material''s name: {0}", result.name); LOGGER.log(Level.FINE, "Material''s name: {0}", result.name);
Long oma = structure.getOldMemoryAddress(); Long oma = structure.getOldMemoryAddress();
@ -212,7 +218,7 @@ public class MaterialHelper extends AbstractBlenderHelper {
} }
} }
image = new Image(Format.RGBA8, w, h, bb); image = new Image(Format.RGBA8, w, h, bb, ColorSpace.Linear);
texture.setImage(image); texture.setImage(image);
result.setTextureParam("Texture", VarType.Texture2D, texture); result.setTextureParam("Texture", VarType.Texture2D, texture);

@ -164,6 +164,134 @@ public final class DQuaternion implements Savable, Cloneable, java.io.Serializab
w = 1; w = 1;
} }
/**
* <code>norm</code> returns the norm of this quaternion. This is the dot
* product of this quaternion with itself.
*
* @return the norm of the quaternion.
*/
public double norm() {
return w * w + x * x + y * y + z * z;
}
public DQuaternion fromRotationMatrix(double m00, double m01, double m02,
double m10, double m11, double m12, double m20, double m21, double m22) {
// first normalize the forward (F), up (U) and side (S) vectors of the rotation matrix
// so that the scale does not affect the rotation
double lengthSquared = m00 * m00 + m10 * m10 + m20 * m20;
if (lengthSquared != 1f && lengthSquared != 0f) {
lengthSquared = 1.0 / Math.sqrt(lengthSquared);
m00 *= lengthSquared;
m10 *= lengthSquared;
m20 *= lengthSquared;
}
lengthSquared = m01 * m01 + m11 * m11 + m21 * m21;
if (lengthSquared != 1 && lengthSquared != 0f) {
lengthSquared = 1.0 / Math.sqrt(lengthSquared);
m01 *= lengthSquared;
m11 *= lengthSquared;
m21 *= lengthSquared;
}
lengthSquared = m02 * m02 + m12 * m12 + m22 * m22;
if (lengthSquared != 1f && lengthSquared != 0f) {
lengthSquared = 1.0 / Math.sqrt(lengthSquared);
m02 *= lengthSquared;
m12 *= lengthSquared;
m22 *= lengthSquared;
}
// Use the Graphics Gems code, from
// ftp://ftp.cis.upenn.edu/pub/graphics/shoemake/quatut.ps.Z
// *NOT* the "Matrix and Quaternions FAQ", which has errors!
// the trace is the sum of the diagonal elements; see
// http://mathworld.wolfram.com/MatrixTrace.html
double t = m00 + m11 + m22;
// we protect the division by s by ensuring that s>=1
if (t >= 0) { // |w| >= .5
double s = Math.sqrt(t + 1); // |s|>=1 ...
w = 0.5f * s;
s = 0.5f / s; // so this division isn't bad
x = (m21 - m12) * s;
y = (m02 - m20) * s;
z = (m10 - m01) * s;
} else if (m00 > m11 && m00 > m22) {
double s = Math.sqrt(1.0 + m00 - m11 - m22); // |s|>=1
x = s * 0.5f; // |x| >= .5
s = 0.5f / s;
y = (m10 + m01) * s;
z = (m02 + m20) * s;
w = (m21 - m12) * s;
} else if (m11 > m22) {
double s = Math.sqrt(1.0 + m11 - m00 - m22); // |s|>=1
y = s * 0.5f; // |y| >= .5
s = 0.5f / s;
x = (m10 + m01) * s;
z = (m21 + m12) * s;
w = (m02 - m20) * s;
} else {
double s = Math.sqrt(1.0 + m22 - m00 - m11); // |s|>=1
z = s * 0.5f; // |z| >= .5
s = 0.5f / s;
x = (m02 + m20) * s;
y = (m21 + m12) * s;
w = (m10 - m01) * s;
}
return this;
}
/**
* <code>toRotationMatrix</code> converts this quaternion to a rotational
* matrix. The result is stored in result. 4th row and 4th column values are
* untouched. Note: the result is created from a normalized version of this quat.
*
* @param result
* The Matrix4f to store the result in.
* @return the rotation matrix representation of this quaternion.
*/
public Matrix toRotationMatrix(Matrix result) {
Vector3d originalScale = new Vector3d();
result.toScaleVector(originalScale);
result.setScale(1, 1, 1);
double norm = this.norm();
// we explicitly test norm against one here, saving a division
// at the cost of a test and branch. Is it worth it?
double s = norm == 1f ? 2f : norm > 0f ? 2f / norm : 0;
// compute xs/ys/zs first to save 6 multiplications, since xs/ys/zs
// will be used 2-4 times each.
double xs = x * s;
double ys = y * s;
double zs = z * s;
double xx = x * xs;
double xy = x * ys;
double xz = x * zs;
double xw = w * xs;
double yy = y * ys;
double yz = y * zs;
double yw = w * ys;
double zz = z * zs;
double zw = w * zs;
// using s=2/norm (instead of 1/norm) saves 9 multiplications by 2 here
result.set(0, 0, 1 - (yy + zz));
result.set(0, 1, xy - zw);
result.set(0, 2, xz + yw);
result.set(1, 0, xy + zw);
result.set(1, 1, 1 - (xx + zz));
result.set(1, 2, yz - xw);
result.set(2, 0, xz - yw);
result.set(2, 1, yz + xw);
result.set(2, 2, 1 - (xx + yy));
result.setScale(originalScale);
return result;
}
/** /**
* <code>fromAngleAxis</code> sets this quaternion to the values specified * <code>fromAngleAxis</code> sets this quaternion to the values specified
* by an angle and an axis of rotation. This method creates an object, so * by an angle and an axis of rotation. This method creates an object, so

@ -31,11 +31,15 @@
*/ */
package com.jme3.scene.plugins.blender.math; package com.jme3.scene.plugins.blender.math;
import com.jme3.export.*;
import com.jme3.math.Transform;
import java.io.IOException; import java.io.IOException;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;
import com.jme3.math.Transform;
/** /**
* Started Date: Jul 16, 2004<br> * Started Date: Jul 16, 2004<br>
* <br> * <br>
@ -57,6 +61,12 @@ public final class DTransform implements Savable, Cloneable, java.io.Serializabl
private Vector3d translation; private Vector3d translation;
private Vector3d scale; private Vector3d scale;
public DTransform() {
translation = new Vector3d();
rotation = new DQuaternion();
scale = new Vector3d();
}
public DTransform(Transform transform) { public DTransform(Transform transform) {
translation = new Vector3d(transform.getTranslation()); translation = new Vector3d(transform.getTranslation());
rotation = new DQuaternion(transform.getRotation()); rotation = new DQuaternion(transform.getRotation());
@ -66,7 +76,15 @@ public final class DTransform implements Savable, Cloneable, java.io.Serializabl
public Transform toTransform() { public Transform toTransform() {
return new Transform(translation.toVector3f(), rotation.toQuaternion(), scale.toVector3f()); return new Transform(translation.toVector3f(), rotation.toQuaternion(), scale.toVector3f());
} }
public Matrix toMatrix() {
Matrix m = Matrix.identity(4);
m.setTranslation(translation);
m.setRotationQuaternion(rotation);
m.setScale(scale);
return m;
}
/** /**
* Sets this translation to the given value. * Sets this translation to the given value.
* @param trans * @param trans

@ -0,0 +1,214 @@
package com.jme3.scene.plugins.blender.math;
import java.text.DecimalFormat;
import org.ejml.ops.CommonOps;
import org.ejml.simple.SimpleMatrix;
import org.ejml.simple.SimpleSVD;
import com.jme3.math.FastMath;
/**
* Encapsulates a 4x4 matrix
*
*
*/
public class Matrix extends SimpleMatrix {
private static final long serialVersionUID = 2396600537315902559L;
public Matrix(int rows, int cols) {
super(rows, cols);
}
/**
* Copy constructor
*/
public Matrix(SimpleMatrix m) {
super(m);
}
public Matrix(double[][] data) {
super(data);
}
public static Matrix identity(int size) {
Matrix result = new Matrix(size, size);
CommonOps.setIdentity(result.mat);
return result;
}
public Matrix pseudoinverse() {
return this.pseudoinverse(1);
}
@SuppressWarnings("unchecked")
public Matrix pseudoinverse(double lambda) {
SimpleSVD<SimpleMatrix> simpleSVD = this.svd();
SimpleMatrix U = simpleSVD.getU();
SimpleMatrix S = simpleSVD.getW();
SimpleMatrix V = simpleSVD.getV();
int N = Math.min(this.numRows(),this.numCols());
double maxSingular = 0;
for( int i = 0; i < N; ++i ) {
if( S.get(i, i) > maxSingular ) {
maxSingular = S.get(i, i);
}
}
double tolerance = FastMath.DBL_EPSILON * Math.max(this.numRows(),this.numCols()) * maxSingular;
for(int i=0;i<Math.min(S.numRows(), S.numCols());++i) {
double a = S.get(i, i);
if(a <= tolerance) {
a = 0;
} else {
a = a/(a * a + lambda * lambda);
}
S.set(i, i, a);
}
return new Matrix(V.mult(S.transpose()).mult(U.transpose()));
}
public void setColumn(Vector3d col, int column) {
this.setColumn(column, 0, col.x, col.y, col.z);
}
/**
* Just for some debug informations in order to compare the results with the scilab computation program.
* @param name the name of the matrix
* @param m the matrix to print out
* @return the String format of the matrix to easily input it to Scilab
*/
public String toScilabString(String name, SimpleMatrix m) {
String result = name + " = [";
for(int i=0;i<m.numRows();++i) {
for(int j=0;j<m.numCols();++j) {
result += m.get(i, j) + " ";
}
result += ";";
}
return result;
}
/**
* @return a String representation of the matrix
*/
@Override
public String toString() {
DecimalFormat df = new DecimalFormat("#.0000");
StringBuilder buf = new StringBuilder();
for (int r = 0; r < this.numRows(); ++r) {
buf.append("\n| ");
for (int c = 0; c < this.numCols(); ++c) {
buf.append(df.format(this.get(r, c))).append(' ');
}
buf.append('|');
}
return buf.toString();
}
public void setTranslation(Vector3d translation) {
this.setColumn(translation, 3);
}
/**
* Sets the scale.
*
* @param scale
* the scale vector to set
*/
public void setScale(Vector3d scale) {
this.setScale(scale.x, scale.y, scale.z);
}
/**
* Sets the scale.
*
* @param x
* the X scale
* @param y
* the Y scale
* @param z
* the Z scale
*/
public void setScale(double x, double y, double z) {
Vector3d vect1 = new Vector3d(this.get(0, 0), this.get(1, 0), this.get(2, 0));
vect1.normalizeLocal().multLocal(x);
this.set(0, 0, vect1.x);
this.set(1, 0, vect1.y);
this.set(2, 0, vect1.z);
vect1.set(this.get(0, 1), this.get(1, 1), this.get(2, 1));
vect1.normalizeLocal().multLocal(y);
this.set(0, 1, vect1.x);
this.set(1, 1, vect1.y);
this.set(2, 1, vect1.z);
vect1.set(this.get(0, 2), this.get(1, 2), this.get(2, 2));
vect1.normalizeLocal().multLocal(z);
this.set(0, 2, vect1.x);
this.set(1, 2, vect1.y);
this.set(2, 2, vect1.z);
}
/**
* <code>setRotationQuaternion</code> builds a rotation from a
* <code>Quaternion</code>.
*
* @param quat
* the quaternion to build the rotation from.
* @throws NullPointerException
* if quat is null.
*/
public void setRotationQuaternion(DQuaternion quat) {
quat.toRotationMatrix(this);
}
public DTransform toTransform() {
DTransform result = new DTransform();
result.setTranslation(this.toTranslationVector());
result.setRotation(this.toRotationQuat());
result.setScale(this.toScaleVector());
return result;
}
public Vector3d toTranslationVector() {
return new Vector3d(this.get(0, 3), this.get(1, 3), this.get(2, 3));
}
public DQuaternion toRotationQuat() {
DQuaternion quat = new DQuaternion();
quat.fromRotationMatrix(this.get(0, 0), this.get(0, 1), this.get(0, 2), this.get(1, 0), this.get(1, 1), this.get(1, 2), this.get(2, 0), this.get(2, 1), this.get(2, 2));
return quat;
}
/**
* Retreives the scale vector from the matrix and stores it into a given
* vector.
*
* @param the
* vector where the scale will be stored
*/
public Vector3d toScaleVector() {
Vector3d result = new Vector3d();
this.toScaleVector(result);
return result;
}
/**
* Retreives the scale vector from the matrix and stores it into a given
* vector.
*
* @param the
* vector where the scale will be stored
*/
public void toScaleVector(Vector3d vector) {
double scaleX = Math.sqrt(this.get(0, 0) * this.get(0, 0) + this.get(1, 0) * this.get(1, 0) + this.get(2, 0) * this.get(2, 0));
double scaleY = Math.sqrt(this.get(0, 1) * this.get(0, 1) + this.get(1, 1) * this.get(1, 1) + this.get(2, 1) * this.get(2, 1));
double scaleZ = Math.sqrt(this.get(0, 2) * this.get(0, 2) + this.get(1, 2) * this.get(1, 2) + this.get(2, 2) * this.get(2, 2));
vector.set(scaleX, scaleY, scaleZ);
}
}

@ -40,7 +40,6 @@ import java.util.Map.Entry;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.jme3.asset.BlenderKey.FeaturesToLoad;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f; import com.jme3.math.Vector2f;
@ -52,7 +51,6 @@ import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray; import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Pointer; import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure; 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.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.objects.Properties; import com.jme3.scene.plugins.blender.objects.Properties;
@ -106,17 +104,18 @@ public class MeshHelper extends AbstractBlenderHelper {
return temporalMesh.clone(); return temporalMesh.clone();
} }
if ("ID".equals(meshStructure.getType())) {
LOGGER.fine("Loading mesh from external blend file.");
return (TemporalMesh) this.loadLibrary(meshStructure);
}
String name = meshStructure.getName(); String name = meshStructure.getName();
LOGGER.log(Level.FINE, "Reading mesh: {0}.", name); LOGGER.log(Level.FINE, "Reading mesh: {0}.", name);
temporalMesh = new TemporalMesh(meshStructure, blenderContext); temporalMesh = new TemporalMesh(meshStructure, blenderContext);
LOGGER.fine("Loading materials."); LOGGER.fine("Loading materials.");
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class); MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
MaterialContext[] materials = null; temporalMesh.setMaterials(materialHelper.getMaterials(meshStructure, blenderContext));
if ((blenderContext.getBlenderKey().getFeaturesToLoad() & FeaturesToLoad.MATERIALS) != 0) {
materials = materialHelper.getMaterials(meshStructure, blenderContext);
}
temporalMesh.setMaterials(materials);
LOGGER.fine("Reading custom properties."); LOGGER.fine("Reading custom properties.");
Properties properties = this.loadProperties(meshStructure, blenderContext); Properties properties = this.loadProperties(meshStructure, blenderContext);

@ -40,12 +40,15 @@ import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.jme3.asset.BlenderKey.FeaturesToLoad; import com.jme3.light.Light;
import com.jme3.math.FastMath; import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f; import com.jme3.math.Matrix4f;
import com.jme3.math.Transform; import com.jme3.math.Transform;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.CameraNode;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.LightNode;
import com.jme3.scene.Mesh.Mode; import com.jme3.scene.Mesh.Mode;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
@ -106,39 +109,34 @@ public class ObjectHelper extends AbstractBlenderHelper {
* an exception is thrown when the given data is inapropriate * an exception is thrown when the given data is inapropriate
*/ */
public Object toObject(Structure objectStructure, BlenderContext blenderContext) throws BlenderFileException { public Object toObject(Structure objectStructure, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.fine("Loading blender object."); Object loadedResult = blenderContext.getLoadedFeature(objectStructure.getOldMemoryAddress(), LoadedDataType.FEATURE);
if (loadedResult != null) {
return loadedResult;
}
LOGGER.fine("Loading blender object.");
if ("ID".equals(objectStructure.getType())) {
Node object = (Node) this.loadLibrary(objectStructure);
if (object.getParent() != null) {
LOGGER.log(Level.FINEST, "Detaching object {0}, loaded from external file, from its parent.", object);
object.getParent().detachChild(object);
}
return object;
}
int type = ((Number) objectStructure.getFieldValue("type")).intValue(); int type = ((Number) objectStructure.getFieldValue("type")).intValue();
ObjectType objectType = ObjectType.valueOf(type); ObjectType objectType = ObjectType.valueOf(type);
LOGGER.log(Level.FINE, "Type of the object: {0}.", objectType); LOGGER.log(Level.FINE, "Type of the object: {0}.", objectType);
if (objectType == ObjectType.LAMP && !blenderContext.getBlenderKey().shouldLoad(FeaturesToLoad.LIGHTS)) {
LOGGER.fine("Lamps are not included in loading.");
return null;
}
if (objectType == ObjectType.CAMERA && !blenderContext.getBlenderKey().shouldLoad(FeaturesToLoad.CAMERAS)) {
LOGGER.fine("Cameras are not included in loading.");
return null;
}
if (!blenderContext.getBlenderKey().shouldLoad(FeaturesToLoad.OBJECTS)) {
LOGGER.fine("Objects are not included in loading.");
return null;
}
int lay = ((Number) objectStructure.getFieldValue("lay")).intValue(); int lay = ((Number) objectStructure.getFieldValue("lay")).intValue();
if ((lay & blenderContext.getBlenderKey().getLayersToLoad()) == 0) { if ((lay & blenderContext.getBlenderKey().getLayersToLoad()) == 0) {
LOGGER.fine("The layer this object is located in is not included in loading."); LOGGER.fine("The layer this object is located in is not included in loading.");
return null; return null;
} }
LOGGER.fine("Checking if the object has not been already loaded.");
Object loadedResult = blenderContext.getLoadedFeature(objectStructure.getOldMemoryAddress(), LoadedDataType.FEATURE);
if (loadedResult != null) {
return loadedResult;
}
blenderContext.pushParent(objectStructure); blenderContext.pushParent(objectStructure);
String name = objectStructure.getName(); String name = objectStructure.getName();
LOGGER.log(Level.FINE, "Loading obejct: {0}", name); LOGGER.log(Level.FINE, "Loading obejct: {0}", name);
int restrictflag = ((Number) objectStructure.getFieldValue("restrictflag")).intValue(); int restrictflag = ((Number) objectStructure.getFieldValue("restrictflag")).intValue();
boolean visible = (restrictflag & 0x01) != 0; boolean visible = (restrictflag & 0x01) != 0;
@ -171,7 +169,7 @@ public class ObjectHelper extends AbstractBlenderHelper {
Pointer pMesh = (Pointer) objectStructure.getFieldValue("data"); Pointer pMesh = (Pointer) objectStructure.getFieldValue("data");
List<Structure> meshesArray = pMesh.fetchData(); List<Structure> meshesArray = pMesh.fetchData();
TemporalMesh temporalMesh = meshHelper.toTemporalMesh(meshesArray.get(0), blenderContext); TemporalMesh temporalMesh = meshHelper.toTemporalMesh(meshesArray.get(0), blenderContext);
if(temporalMesh != null) { if (temporalMesh != null) {
result.attachChild(temporalMesh); result.attachChild(temporalMesh);
} }
break; break;
@ -183,7 +181,7 @@ public class ObjectHelper extends AbstractBlenderHelper {
CurvesHelper curvesHelper = blenderContext.getHelper(CurvesHelper.class); CurvesHelper curvesHelper = blenderContext.getHelper(CurvesHelper.class);
Structure curveData = pCurve.fetchData().get(0); Structure curveData = pCurve.fetchData().get(0);
TemporalMesh curvesTemporalMesh = curvesHelper.toCurve(curveData, blenderContext); TemporalMesh curvesTemporalMesh = curvesHelper.toCurve(curveData, blenderContext);
if(curvesTemporalMesh != null) { if (curvesTemporalMesh != null) {
result.attachChild(curvesTemporalMesh); result.attachChild(curvesTemporalMesh);
} }
} }
@ -193,10 +191,12 @@ public class ObjectHelper extends AbstractBlenderHelper {
if (pLamp.isNotNull()) { if (pLamp.isNotNull()) {
LightHelper lightHelper = blenderContext.getHelper(LightHelper.class); LightHelper lightHelper = blenderContext.getHelper(LightHelper.class);
List<Structure> lampsArray = pLamp.fetchData(); List<Structure> lampsArray = pLamp.fetchData();
result = lightHelper.toLight(lampsArray.get(0), blenderContext); Light light = lightHelper.toLight(lampsArray.get(0), blenderContext);
if (result == null) { if (light == null) {
// probably some light type is not supported, just create a node so that we can maintain child-parent relationship for nodes // probably some light type is not supported, just create a node so that we can maintain child-parent relationship for nodes
result = new Node(name); result = new Node(name);
} else {
result = new LightNode(name, light);
} }
} }
break; break;
@ -205,19 +205,25 @@ public class ObjectHelper extends AbstractBlenderHelper {
if (pCamera.isNotNull()) { if (pCamera.isNotNull()) {
CameraHelper cameraHelper = blenderContext.getHelper(CameraHelper.class); CameraHelper cameraHelper = blenderContext.getHelper(CameraHelper.class);
List<Structure> camerasArray = pCamera.fetchData(); List<Structure> camerasArray = pCamera.fetchData();
result = cameraHelper.toCamera(camerasArray.get(0), blenderContext); Camera camera = cameraHelper.toCamera(camerasArray.get(0), blenderContext);
if (camera == null) {
// just create a node so that we can maintain child-parent relationship for nodes
result = new Node(name);
} else {
result = new CameraNode(name, camera);
}
} }
break; break;
default: default:
LOGGER.log(Level.WARNING, "Unsupported object type: {0}", type); LOGGER.log(Level.WARNING, "Unsupported object type: {0}", type);
} }
if (result != null) { if (result != null) {
LOGGER.fine("Storing loaded feature in blender context and applying markers (those will be removed before the final result is released)."); LOGGER.fine("Storing loaded feature in blender context and applying markers (those will be removed before the final result is released).");
Long oma = objectStructure.getOldMemoryAddress(); Long oma = objectStructure.getOldMemoryAddress();
blenderContext.addLoadedFeatures(oma, LoadedDataType.STRUCTURE, objectStructure); blenderContext.addLoadedFeatures(oma, LoadedDataType.STRUCTURE, objectStructure);
blenderContext.addLoadedFeatures(oma, LoadedDataType.FEATURE, result); blenderContext.addLoadedFeatures(oma, LoadedDataType.FEATURE, result);
blenderContext.addMarker(OMA_MARKER, result, objectStructure.getOldMemoryAddress()); blenderContext.addMarker(OMA_MARKER, result, objectStructure.getOldMemoryAddress());
if (objectType == ObjectType.ARMATURE) { if (objectType == ObjectType.ARMATURE) {
blenderContext.addMarker(ARMATURE_NODE_MARKER, result, Boolean.TRUE); blenderContext.addMarker(ARMATURE_NODE_MARKER, result, Boolean.TRUE);
@ -235,13 +241,13 @@ public class ObjectHelper extends AbstractBlenderHelper {
for (Modifier modifier : modifiers) { for (Modifier modifier : modifiers) {
modifier.apply(result, blenderContext); modifier.apply(result, blenderContext);
} }
if (result.getChildren() != null && result.getChildren().size() > 0) { if (result.getChildren() != null && result.getChildren().size() > 0) {
if(result.getChildren().size() == 1 && result.getChild(0) instanceof TemporalMesh) { if (result.getChildren().size() == 1 && result.getChild(0) instanceof TemporalMesh) {
LOGGER.fine("Converting temporal mesh into jme geometries."); LOGGER.fine("Converting temporal mesh into jme geometries.");
((TemporalMesh)result.getChild(0)).toGeometries(); ((TemporalMesh) result.getChild(0)).toGeometries();
} }
LOGGER.fine("Applying proper scale to the geometries."); LOGGER.fine("Applying proper scale to the geometries.");
for (Spatial child : result.getChildren()) { for (Spatial child : result.getChildren()) {
if (child instanceof Geometry) { if (child instanceof Geometry) {

@ -444,13 +444,17 @@ public class CombinedTexture {
case RGB8: case RGB8:
return true;// these types have no alpha by definition return true;// these types have no alpha by definition
case ABGR8: case ABGR8:
case DXT1A:
case DXT3: case DXT3:
case DXT5: case DXT5:
case Luminance16FAlpha16F: case Luminance16FAlpha16F:
case Luminance8Alpha8: case Luminance8Alpha8:
case RGBA16F: case RGBA16F:
case RGBA32F: case RGBA32F:
case RGBA8:// with these types it is better to make sure if the texture is or is not transparent case RGBA8:
case ARGB8:
case BGRA8:
case RGB5A1:// with these types it is better to make sure if the texture is or is not transparent
PixelInputOutput pixelInputOutput = PixelIOFactory.getPixelIO(image.getFormat()); PixelInputOutput pixelInputOutput = PixelIOFactory.getPixelIO(image.getFormat());
TexturePixel pixel = new TexturePixel(); TexturePixel pixel = new TexturePixel();
int depth = image.getDepth() == 0 ? 1 : image.getDepth(); int depth = image.getDepth() == 0 ? 1 : image.getDepth();
@ -465,6 +469,8 @@ public class CombinedTexture {
} }
} }
return true; return true;
default:
throw new IllegalStateException("Unknown image format: " + image.getFormat());
} }
} }
} }

@ -41,13 +41,13 @@ public final class ImageUtils {
public static Image createEmptyImage(Format format, int width, int height, int depth) { public static Image createEmptyImage(Format format, int width, int height, int depth) {
int bufferSize = width * height * (format.getBitsPerPixel() >> 3); int bufferSize = width * height * (format.getBitsPerPixel() >> 3);
if (depth < 2) { if (depth < 2) {
return new Image(format, width, height, BufferUtils.createByteBuffer(bufferSize)); return new Image(format, width, height, BufferUtils.createByteBuffer(bufferSize), com.jme3.texture.image.ColorSpace.Linear);
} }
ArrayList<ByteBuffer> data = new ArrayList<ByteBuffer>(depth); ArrayList<ByteBuffer> data = new ArrayList<ByteBuffer>(depth);
for (int i = 0; i < depth; ++i) { for (int i = 0; i < depth; ++i) {
data.add(BufferUtils.createByteBuffer(bufferSize)); data.add(BufferUtils.createByteBuffer(bufferSize));
} }
return new Image(Format.RGB8, width, height, depth, data); return new Image(Format.RGB8, width, height, depth, data, com.jme3.texture.image.ColorSpace.Linear);
} }
/** /**
@ -337,7 +337,7 @@ public final class ImageUtils {
alphas[0] = data.get() * 255.0f; alphas[0] = data.get() * 255.0f;
alphas[1] = data.get() * 255.0f; alphas[1] = data.get() * 255.0f;
//the casts to long must be done here because otherwise 32-bit integers would be shifetd by 32 and 40 bits which would result in improper values //the casts to long must be done here because otherwise 32-bit integers would be shifetd by 32 and 40 bits which would result in improper values
long alphaIndices = (long)data.get() | (long)data.get() << 8 | (long)data.get() << 16 | (long)data.get() << 24 | (long)data.get() << 32 | (long)data.get() << 40; long alphaIndices = data.get() | (long)data.get() << 8 | (long)data.get() << 16 | (long)data.get() << 24 | (long)data.get() << 32 | (long)data.get() << 40;
if (alphas[0] > alphas[1]) {// 6 interpolated alpha values. if (alphas[0] > alphas[1]) {// 6 interpolated alpha values.
alphas[2] = (6 * alphas[0] + alphas[1]) / 7; alphas[2] = (6 * alphas[0] + alphas[1]) / 7;
alphas[3] = (5 * alphas[0] + 2 * alphas[1]) / 7; alphas[3] = (5 * alphas[0] + 2 * alphas[1]) / 7;
@ -404,7 +404,8 @@ public final class ImageUtils {
dataArray.add(BufferUtils.createByteBuffer(bytes)); dataArray.add(BufferUtils.createByteBuffer(bytes));
} }
Image result = depth > 1 ? new Image(Format.RGBA8, image.getWidth(), image.getHeight(), depth, dataArray) : new Image(Format.RGBA8, image.getWidth(), image.getHeight(), dataArray.get(0)); Image result = depth > 1 ? new Image(Format.RGBA8, image.getWidth(), image.getHeight(), depth, dataArray, com.jme3.texture.image.ColorSpace.Linear) :
new Image(Format.RGBA8, image.getWidth(), image.getHeight(), dataArray.get(0), com.jme3.texture.image.ColorSpace.Linear);
if (newMipmapSizes != null) { if (newMipmapSizes != null) {
result.setMipMapSizes(newMipmapSizes); result.setMipMapSizes(newMipmapSizes);
} }
@ -467,6 +468,6 @@ public final class ImageUtils {
private static Image toJmeImage(BufferedImage bufferedImage, Format format) { private static Image toJmeImage(BufferedImage bufferedImage, Format format) {
ByteBuffer byteBuffer = BufferUtils.createByteBuffer(bufferedImage.getWidth() * bufferedImage.getHeight() * 3); ByteBuffer byteBuffer = BufferUtils.createByteBuffer(bufferedImage.getWidth() * bufferedImage.getHeight() * 3);
ImageToAwt.convert(bufferedImage, format, byteBuffer); ImageToAwt.convert(bufferedImage, format, byteBuffer);
return new Image(format, bufferedImage.getWidth(), bufferedImage.getHeight(), byteBuffer); return new Image(format, bufferedImage.getWidth(), bufferedImage.getHeight(), byteBuffer, com.jme3.texture.image.ColorSpace.Linear);
} }
} }

@ -121,7 +121,7 @@ public class TextureHelper extends AbstractBlenderHelper {
* data. The returned texture has the name set to the value of its blender * data. The returned texture has the name set to the value of its blender
* type. * type.
* *
* @param tex * @param textureStructure
* texture structure filled with data * texture structure filled with data
* @param blenderContext * @param blenderContext
* the blender context * the blender context
@ -130,23 +130,29 @@ public class TextureHelper extends AbstractBlenderHelper {
* this exception is thrown when the blend file structure is * this exception is thrown when the blend file structure is
* somehow invalid or corrupted * somehow invalid or corrupted
*/ */
public Texture getTexture(Structure tex, Structure mTex, BlenderContext blenderContext) throws BlenderFileException { public Texture getTexture(Structure textureStructure, Structure mTex, BlenderContext blenderContext) throws BlenderFileException {
Texture result = (Texture) blenderContext.getLoadedFeature(tex.getOldMemoryAddress(), LoadedDataType.FEATURE); Texture result = (Texture) blenderContext.getLoadedFeature(textureStructure.getOldMemoryAddress(), LoadedDataType.FEATURE);
if (result != null) { if (result != null) {
return result; return result;
} }
int type = ((Number) tex.getFieldValue("type")).intValue();
int imaflag = ((Number) tex.getFieldValue("imaflag")).intValue(); if ("ID".equals(textureStructure.getType())) {
LOGGER.fine("Loading texture from external blend file.");
return (Texture) this.loadLibrary(textureStructure);
}
int type = ((Number) textureStructure.getFieldValue("type")).intValue();
int imaflag = ((Number) textureStructure.getFieldValue("imaflag")).intValue();
switch (type) { switch (type) {
case TEX_IMAGE:// (it is first because probably this will be most commonly used) case TEX_IMAGE:// (it is first because probably this will be most commonly used)
Pointer pImage = (Pointer) tex.getFieldValue("ima"); Pointer pImage = (Pointer) textureStructure.getFieldValue("ima");
if (pImage.isNotNull()) { if (pImage.isNotNull()) {
Structure image = pImage.fetchData().get(0); Structure image = pImage.fetchData().get(0);
Texture loadedTexture = this.loadTexture(image, imaflag, blenderContext); Texture loadedTexture = this.loadImageAsTexture(image, imaflag, blenderContext);
if (loadedTexture != null) { if (loadedTexture != null) {
result = loadedTexture; result = loadedTexture;
this.applyColorbandAndColorFactors(tex, result.getImage(), blenderContext); this.applyColorbandAndColorFactors(textureStructure, result.getImage(), blenderContext);
} }
} }
break; break;
@ -160,7 +166,7 @@ public class TextureHelper extends AbstractBlenderHelper {
case TEX_MUSGRAVE: case TEX_MUSGRAVE:
case TEX_VORONOI: case TEX_VORONOI:
case TEX_DISTNOISE: case TEX_DISTNOISE:
result = new GeneratedTexture(tex, mTex, textureGeneratorFactory.createTextureGenerator(type), blenderContext); result = new GeneratedTexture(textureStructure, mTex, textureGeneratorFactory.createTextureGenerator(type), blenderContext);
break; break;
case TEX_NONE:// No texture, do nothing case TEX_NONE:// No texture, do nothing
break; break;
@ -169,13 +175,13 @@ public class TextureHelper extends AbstractBlenderHelper {
case TEX_PLUGIN: case TEX_PLUGIN:
case TEX_ENVMAP: case TEX_ENVMAP:
case TEX_OCEAN: case TEX_OCEAN:
LOGGER.log(Level.WARNING, "Unsupported texture type: {0} for texture: {1}", new Object[] { type, tex.getName() }); LOGGER.log(Level.WARNING, "Unsupported texture type: {0} for texture: {1}", new Object[] { type, textureStructure.getName() });
break; break;
default: default:
throw new BlenderFileException("Unknown texture type: " + type + " for texture: " + tex.getName()); throw new BlenderFileException("Unknown texture type: " + type + " for texture: " + textureStructure.getName());
} }
if (result != null) { if (result != null) {
result.setName(tex.getName()); result.setName(textureStructure.getName());
result.setWrap(WrapMode.Repeat); result.setWrap(WrapMode.Repeat);
// decide if the mipmaps will be generated // decide if the mipmaps will be generated
@ -195,14 +201,14 @@ public class TextureHelper extends AbstractBlenderHelper {
} }
if (type != TEX_IMAGE) {// only generated textures should have this key if (type != TEX_IMAGE) {// only generated textures should have this key
result.setKey(new GeneratedTextureKey(tex.getName())); result.setKey(new GeneratedTextureKey(textureStructure.getName()));
} }
if (LOGGER.isLoggable(Level.FINE)) { if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Adding texture {0} to the loaded features with OMA = {1}", new Object[] { result.getName(), tex.getOldMemoryAddress() }); LOGGER.log(Level.FINE, "Adding texture {0} to the loaded features with OMA = {1}", new Object[] { result.getName(), textureStructure.getOldMemoryAddress() });
} }
blenderContext.addLoadedFeatures(tex.getOldMemoryAddress(), LoadedDataType.STRUCTURE, tex); blenderContext.addLoadedFeatures(textureStructure.getOldMemoryAddress(), LoadedDataType.STRUCTURE, textureStructure);
blenderContext.addLoadedFeatures(tex.getOldMemoryAddress(), LoadedDataType.FEATURE, result); blenderContext.addLoadedFeatures(textureStructure.getOldMemoryAddress(), LoadedDataType.FEATURE, result);
} }
return result; return result;
} }
@ -222,29 +228,39 @@ public class TextureHelper extends AbstractBlenderHelper {
* this exception is thrown when the blend file structure is * this exception is thrown when the blend file structure is
* somehow invalid or corrupted * somehow invalid or corrupted
*/ */
protected Texture loadTexture(Structure imageStructure, int imaflag, BlenderContext blenderContext) throws BlenderFileException { public Texture loadImageAsTexture(Structure imageStructure, int imaflag, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Fetching texture with OMA = {0}", imageStructure.getOldMemoryAddress()); LOGGER.log(Level.FINE, "Fetching texture with OMA = {0}", imageStructure.getOldMemoryAddress());
Texture result = null; Texture result = null;
Image im = (Image) blenderContext.getLoadedFeature(imageStructure.getOldMemoryAddress(), LoadedDataType.FEATURE); Image im = (Image) blenderContext.getLoadedFeature(imageStructure.getOldMemoryAddress(), LoadedDataType.FEATURE);
if (im == null) { // if (im == null) { HACK force reaload always, as constructor in else case is destroying the TextureKeys!
String texturePath = imageStructure.getFieldValue("name").toString(); if ("ID".equals(imageStructure.getType())) {
Pointer pPackedFile = (Pointer) imageStructure.getFieldValue("packedfile"); LOGGER.fine("Loading texture from external blend file.");
if (pPackedFile.isNull()) { result = (Texture) this.loadLibrary(imageStructure);
LOGGER.log(Level.FINE, "Reading texture from file: {0}", texturePath);
result = this.loadImageFromFile(texturePath, imaflag, blenderContext);
} else { } else {
LOGGER.fine("Packed texture. Reading directly from the blend file!"); String texturePath = imageStructure.getFieldValue("name").toString();
Structure packedFile = pPackedFile.fetchData().get(0); Pointer pPackedFile = (Pointer) imageStructure.getFieldValue("packedfile");
Pointer pData = (Pointer) packedFile.getFieldValue("data"); if (pPackedFile.isNull()) {
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pData.getOldMemoryAddress()); LOGGER.log(Level.FINE, "Reading texture from file: {0}", texturePath);
blenderContext.getInputStream().setPosition(dataFileBlock.getBlockPosition()); result = this.loadImageFromFile(texturePath, imaflag, blenderContext);
ImageLoader imageLoader = new ImageLoader(); } else {
LOGGER.fine("Packed texture. Reading directly from the blend file!");
// Should the texture be flipped? It works for sinbad .. Structure packedFile = pPackedFile.fetchData().get(0);
result = new Texture2D(imageLoader.loadImage(blenderContext.getInputStream(), dataFileBlock.getBlockPosition(), true)); Pointer pData = (Pointer) packedFile.getFieldValue("data");
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pData.getOldMemoryAddress());
blenderContext.getInputStream().setPosition(dataFileBlock.getBlockPosition());
// Should the texture be flipped? It works for sinbad ..
result = new Texture2D(new ImageLoader().loadImage(blenderContext.getInputStream(), dataFileBlock.getBlockPosition(), true));
}
} }
} else { //} else {
result = new Texture2D(im); // result = new Texture2D(im);
// }
if (result != null) {// render result is not being loaded
blenderContext.addLoadedFeatures(imageStructure.getOldMemoryAddress(), LoadedDataType.STRUCTURE, imageStructure);
blenderContext.addLoadedFeatures(imageStructure.getOldMemoryAddress(), LoadedDataType.FEATURE, result.getImage());
result.setName(imageStructure.getName());
} }
return result; return result;
} }
@ -524,6 +540,18 @@ public class TextureHelper extends AbstractBlenderHelper {
return result; return result;
} }
/**
* Reads the texture data from the given material or sky structure.
* @param structure
* the structure of material or sky
* @param diffuseColorArray
* array of diffuse colors
* @param skyTexture
* indicates it we're going to read sky texture or not
* @return a list of combined textures
* @throws BlenderFileException
* an exception is thrown when problems with reading the blend file occur
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List<CombinedTexture> readTextureData(Structure structure, float[] diffuseColorArray, boolean skyTexture) throws BlenderFileException { public List<CombinedTexture> readTextureData(Structure structure, float[] diffuseColorArray, boolean skyTexture) throws BlenderFileException {
DynamicArray<Pointer> mtexsArray = (DynamicArray<Pointer>) structure.getFieldValue("mtex"); DynamicArray<Pointer> mtexsArray = (DynamicArray<Pointer>) structure.getFieldValue("mtex");

@ -31,6 +31,7 @@ import com.jme3.texture.Image;
import com.jme3.texture.Image.Format; import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture; import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D; import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
/** /**
@ -77,7 +78,7 @@ import com.jme3.util.BufferUtils;
for (int i = 0; i < facesCount; ++i) { for (int i = 0; i < facesCount; ++i) {
faceTextures.add(new TriangleTextureElement(i, texture2d.getImage(), uvs, true, blenderContext)); faceTextures.add(new TriangleTextureElement(i, texture2d.getImage(), uvs, true, blenderContext));
} }
this.format = texture2d.getImage().getFormat(); format = texture2d.getImage().getFormat();
} }
/** /**
@ -113,7 +114,7 @@ import com.jme3.util.BufferUtils;
*/ */
public void blend(TextureBlender textureBlender, TriangulatedTexture baseTexture, BlenderContext blenderContext) { public void blend(TextureBlender textureBlender, TriangulatedTexture baseTexture, BlenderContext blenderContext) {
Format newFormat = null; Format newFormat = null;
for (TriangleTextureElement triangleTextureElement : this.faceTextures) { for (TriangleTextureElement triangleTextureElement : faceTextures) {
Image baseImage = baseTexture == null ? null : baseTexture.getFaceTextureElement(triangleTextureElement.faceIndex).image; Image baseImage = baseTexture == null ? null : baseTexture.getFaceTextureElement(triangleTextureElement.faceIndex).image;
triangleTextureElement.image = textureBlender.blend(triangleTextureElement.image, baseImage, blenderContext); triangleTextureElement.image = textureBlender.blend(triangleTextureElement.image, baseImage, blenderContext);
if (newFormat == null) { if (newFormat == null) {
@ -122,7 +123,7 @@ import com.jme3.util.BufferUtils;
throw new IllegalArgumentException("Face texture element images MUST have the same image format!"); throw new IllegalArgumentException("Face texture element images MUST have the same image format!");
} }
} }
this.format = newFormat; format = newFormat;
} }
/** /**
@ -242,7 +243,7 @@ import com.jme3.util.BufferUtils;
resultUVS.set(entry.getKey().faceIndex * 3 + 2, uvs[2]); resultUVS.set(entry.getKey().faceIndex * 3 + 2, uvs[2]);
} }
Image resultImage = new Image(format, resultImageWidth, resultImageHeight, BufferUtils.createByteBuffer(resultImageWidth * resultImageHeight * (format.getBitsPerPixel() >> 3))); Image resultImage = new Image(format, resultImageWidth, resultImageHeight, BufferUtils.createByteBuffer(resultImageWidth * resultImageHeight * (format.getBitsPerPixel() >> 3)), ColorSpace.Linear);
resultTexture = new Texture2D(resultImage); resultTexture = new Texture2D(resultImage);
for (Entry<TriangleTextureElement, Integer[]> entry : imageLayoutData.entrySet()) { for (Entry<TriangleTextureElement, Integer[]> entry : imageLayoutData.entrySet()) {
if (!duplicatedFaceIndexes.contains(entry.getKey().faceIndex)) { if (!duplicatedFaceIndexes.contains(entry.getKey().faceIndex)) {
@ -420,7 +421,7 @@ import com.jme3.util.BufferUtils;
data.put(pixel.getA8()); data.put(pixel.getA8());
} }
} }
image = new Image(Format.RGBA8, width, height, data); image = new Image(Format.RGBA8, width, height, data, ColorSpace.Linear);
// modify the UV values so that they fit the new image // modify the UV values so that they fit the new image
float heightUV = maxUVY - minUVY; float heightUV = maxUVY - minUVY;
@ -481,7 +482,7 @@ import com.jme3.util.BufferUtils;
imageHeight = 1; imageHeight = 1;
} }
ByteBuffer data = BufferUtils.createByteBuffer(imageWidth * imageHeight * (imageFormat.getBitsPerPixel() >> 3)); ByteBuffer data = BufferUtils.createByteBuffer(imageWidth * imageHeight * (imageFormat.getBitsPerPixel() >> 3));
image = new Image(texture.getImage().getFormat(), imageWidth, imageHeight, data); image = new Image(texture.getImage().getFormat(), imageWidth, imageHeight, data, ColorSpace.Linear);
// computing the pixels // computing the pixels
PixelInputOutput pixelWriter = PixelIOFactory.getPixelIO(imageFormat); PixelInputOutput pixelWriter = PixelIOFactory.getPixelIO(imageFormat);
@ -529,8 +530,8 @@ import com.jme3.util.BufferUtils;
public void computeFinalUVCoordinates(int totalImageWidth, int totalImageHeight, int xPos, int yPos, Vector2f[] result) { public void computeFinalUVCoordinates(int totalImageWidth, int totalImageHeight, int xPos, int yPos, Vector2f[] result) {
for (int i = 0; i < 3; ++i) { for (int i = 0; i < 3; ++i) {
result[i] = new Vector2f(); result[i] = new Vector2f();
result[i].x = xPos / (float) totalImageWidth + this.uv[i].x * (this.image.getWidth() / (float) totalImageWidth); result[i].x = xPos / (float) totalImageWidth + uv[i].x * (image.getWidth() / (float) totalImageWidth);
result[i].y = yPos / (float) totalImageHeight + this.uv[i].y * (this.image.getHeight() / (float) totalImageHeight); result[i].y = yPos / (float) totalImageHeight + uv[i].y * (image.getHeight() / (float) totalImageHeight);
} }
} }
@ -623,9 +624,9 @@ import com.jme3.util.BufferUtils;
* a position in 3D space * a position in 3D space
*/ */
public RectangleEnvelope(Vector3f pointPosition) { public RectangleEnvelope(Vector3f pointPosition) {
this.min = pointPosition; min = pointPosition;
this.h = this.w = Vector3f.ZERO; h = w = Vector3f.ZERO;
this.width = this.height = 1; width = height = 1;
} }
/** /**
@ -642,8 +643,8 @@ import com.jme3.util.BufferUtils;
this.min = min; this.min = min;
this.h = h; this.h = h;
this.w = w; this.w = w;
this.width = w.length(); width = w.length();
this.height = h.length(); height = h.length();
} }
@Override @Override

@ -1,10 +1,12 @@
package com.jme3.scene.plugins.blender.textures.blending; package com.jme3.scene.plugins.blender.textures.blending;
import java.util.logging.Logger;
import jme3tools.converters.MipMapGenerator;
import com.jme3.scene.plugins.blender.BlenderContext; import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.materials.MaterialHelper; import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.texture.Image; import com.jme3.texture.Image;
import java.util.logging.Logger;
import jme3tools.converters.MipMapGenerator;
/** /**
* An abstract class that contains the basic methods used by the classes that * An abstract class that contains the basic methods used by the classes that
@ -103,12 +105,12 @@ import jme3tools.converters.MipMapGenerator;
public void copyBlendingData(TextureBlender textureBlender) { public void copyBlendingData(TextureBlender textureBlender) {
if (textureBlender instanceof AbstractTextureBlender) { if (textureBlender instanceof AbstractTextureBlender) {
this.flag = ((AbstractTextureBlender) textureBlender).flag; flag = ((AbstractTextureBlender) textureBlender).flag;
this.negateTexture = ((AbstractTextureBlender) textureBlender).negateTexture; negateTexture = ((AbstractTextureBlender) textureBlender).negateTexture;
this.blendType = ((AbstractTextureBlender) textureBlender).blendType; blendType = ((AbstractTextureBlender) textureBlender).blendType;
this.materialColor = ((AbstractTextureBlender) textureBlender).materialColor.clone(); materialColor = ((AbstractTextureBlender) textureBlender).materialColor.clone();
this.color = ((AbstractTextureBlender) textureBlender).color.clone(); color = ((AbstractTextureBlender) textureBlender).color.clone();
this.blendFactor = ((AbstractTextureBlender) textureBlender).blendFactor; blendFactor = ((AbstractTextureBlender) textureBlender).blendFactor;
} else { } else {
LOGGER.warning("Cannot copy blending data from other types than " + this.getClass()); LOGGER.warning("Cannot copy blending data from other types than " + this.getClass());
} }

@ -38,7 +38,9 @@ import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput; import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image; import com.jme3.texture.Image;
import com.jme3.texture.Image.Format; import com.jme3.texture.Image.Format;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
@ -141,7 +143,7 @@ public class TextureBlenderAWT extends AbstractTextureBlender {
dataArray.add(newData); dataArray.add(newData);
} }
Image result = depth > 1 ? new Image(Format.RGBA8, width, height, depth, dataArray) : new Image(Format.RGBA8, width, height, dataArray.get(0)); Image result = depth > 1 ? new Image(Format.RGBA8, width, height, depth, dataArray, ColorSpace.Linear) : new Image(Format.RGBA8, width, height, dataArray.get(0), ColorSpace.Linear);
if (image.getMipMapSizes() != null) { if (image.getMipMapSizes() != null) {
result.setMipMapSizes(image.getMipMapSizes().clone()); result.setMipMapSizes(image.getMipMapSizes().clone());
} }

@ -6,9 +6,12 @@ import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput; import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image; import com.jme3.texture.Image;
import com.jme3.texture.Image.Format; import com.jme3.texture.Image.Format;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import jme3tools.converters.RGB565; import jme3tools.converters.RGB565;
/** /**
@ -119,7 +122,7 @@ public class TextureBlenderDDS extends TextureBlenderAWT {
dataArray.add(newData); dataArray.add(newData);
} }
Image result = dataArray.size() > 1 ? new Image(format, width, height, depth, dataArray) : new Image(format, width, height, dataArray.get(0)); Image result = dataArray.size() > 1 ? new Image(format, width, height, depth, dataArray, ColorSpace.Linear) : new Image(format, width, height, dataArray.get(0), ColorSpace.Linear);
if (image.getMipMapSizes() != null) { if (image.getMipMapSizes() != null) {
result.setMipMapSizes(image.getMipMapSizes().clone()); result.setMipMapSizes(image.getMipMapSizes().clone());
} }

@ -31,13 +31,13 @@
*/ */
package com.jme3.scene.plugins.blender.textures.blending; package com.jme3.scene.plugins.blender.textures.blending;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.scene.plugins.blender.BlenderContext; import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.texture.Image; import com.jme3.texture.Image;
import com.jme3.texture.Image.Format; import com.jme3.texture.Image.Format;
import java.util.logging.Level;
import java.util.logging.Logger;
/** /**
* This class creates the texture blending class depending on the texture type. * This class creates the texture blending class depending on the texture type.
* *
@ -66,7 +66,6 @@ public class TextureBlenderFactory {
* the texture format * the texture format
* @return texture blending class * @return texture blending class
*/ */
@SuppressWarnings("deprecation")
public static TextureBlender createTextureBlender(Format format, int flag, boolean negate, int blendType, float[] materialColor, float[] color, float colfac) { public static TextureBlender createTextureBlender(Format format, int flag, boolean negate, int blendType, float[] materialColor, float[] color, float colfac) {
switch (format) { switch (format) {
case Luminance8: case Luminance8:

@ -7,7 +7,9 @@ import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput; import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image; import com.jme3.texture.Image;
import com.jme3.texture.Image.Format; import com.jme3.texture.Image.Format;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.logging.Level; import java.util.logging.Level;
@ -93,7 +95,7 @@ public class TextureBlenderLuminance extends AbstractTextureBlender {
dataArray.add(newData); dataArray.add(newData);
} }
Image result = depth > 1 ? new Image(Format.RGBA8, width, height, depth, dataArray) : new Image(Format.RGBA8, width, height, dataArray.get(0)); Image result = depth > 1 ? new Image(Format.RGBA8, width, height, depth, dataArray, ColorSpace.Linear) : new Image(Format.RGBA8, width, height, dataArray.get(0), ColorSpace.Linear);
if (image.getMipMapSizes() != null) { if (image.getMipMapSizes() != null) {
result.setMipMapSizes(image.getMipMapSizes().clone()); result.setMipMapSizes(image.getMipMapSizes().clone());
} }

@ -17,12 +17,18 @@ import jme3tools.converters.RGB565;
case RGBA8: case RGBA8:
pixel.fromARGB8(data.get(index + 3), data.get(index), data.get(index + 1), data.get(index + 2)); pixel.fromARGB8(data.get(index + 3), data.get(index), data.get(index + 1), data.get(index + 2));
break; break;
case ARGB8:
pixel.fromARGB8(data.get(index), data.get(index + 1), data.get(index + 2), data.get(index + 3));
break;
case ABGR8: case ABGR8:
pixel.fromARGB8(data.get(index), data.get(index + 3), data.get(index + 2), data.get(index + 1)); pixel.fromARGB8(data.get(index), data.get(index + 3), data.get(index + 2), data.get(index + 1));
break; break;
case BGR8: case BGR8:
pixel.fromARGB8((byte) 0xFF, data.get(index + 2), data.get(index + 1), data.get(index)); pixel.fromARGB8((byte) 0xFF, data.get(index + 2), data.get(index + 1), data.get(index));
break; break;
case BGRA8:
pixel.fromARGB8(data.get(index + 3), data.get(index + 2), data.get(index + 1), data.get(index));
break;
case RGB8: case RGB8:
pixel.fromARGB8((byte) 0xFF, data.get(index), data.get(index + 1), data.get(index + 2)); pixel.fromARGB8((byte) 0xFF, data.get(index), data.get(index + 1), data.get(index + 2));
break; break;
@ -72,6 +78,12 @@ import jme3tools.converters.RGB565;
data.put(index + 2, pixel.getB8()); data.put(index + 2, pixel.getB8());
data.put(index + 3, pixel.getA8()); data.put(index + 3, pixel.getA8());
break; break;
case ARGB8:
data.put(index, pixel.getA8());
data.put(index + 1, pixel.getR8());
data.put(index + 2, pixel.getG8());
data.put(index + 3, pixel.getB8());
break;
case ABGR8: case ABGR8:
data.put(index, pixel.getA8()); data.put(index, pixel.getA8());
data.put(index + 1, pixel.getB8()); data.put(index + 1, pixel.getB8());
@ -83,6 +95,12 @@ import jme3tools.converters.RGB565;
data.put(index + 1, pixel.getG8()); data.put(index + 1, pixel.getG8());
data.put(index + 2, pixel.getR8()); data.put(index + 2, pixel.getR8());
break; break;
case BGRA8:
data.put(index, pixel.getB8());
data.put(index + 1, pixel.getG8());
data.put(index + 2, pixel.getR8());
data.put(index + 3, pixel.getA8());
break;
case RGB8: case RGB8:
data.put(index, pixel.getR8()); data.put(index, pixel.getR8());
data.put(index + 1, pixel.getG8()); data.put(index + 1, pixel.getG8());

@ -26,7 +26,9 @@ public class PixelIOFactory {
case ABGR8: case ABGR8:
case RGBA8: case RGBA8:
case BGR8: case BGR8:
case BGRA8:
case RGB8: case RGB8:
case ARGB8:
case RGB111110F: case RGB111110F:
case RGB16F: case RGB16F:
case RGB16F_to_RGB111110F: case RGB16F_to_RGB111110F:

@ -175,7 +175,7 @@ binaries.withType(SharedLibraryBinary) { binary ->
// Add depend on build // Add depend on build
jar.dependsOn builderTask jar.dependsOn builderTask
// Add output to libs folder // Add output to libs folder
task "copyBinaryToLibs${targetPlatform}"(type: Copy) { task "copyBinaryToLibs${targetPlatform}"(type: Copy, dependsOn: builderTask) {
from builderTask.outputFile from builderTask.outputFile
into "libs/native/${targetPlatform.operatingSystem.name}/${targetPlatform.architecture.name}" into "libs/native/${targetPlatform.operatingSystem.name}/${targetPlatform.architecture.name}"
} }

@ -468,6 +468,79 @@ extern "C" {
return; return;
} }
JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_sweepTest_1native
(JNIEnv * env, jobject object, jlong shapeId, jobject from, jobject to, jlong spaceId, jobject resultlist, jfloat allowedCcdPenetration) {
jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
if (space == NULL) {
jclass newExc = env->FindClass("java/lang/NullPointerException");
env->ThrowNew(newExc, "The physics space does not exist.");
return;
}
btCollisionShape* shape = reinterpret_cast<btCollisionShape*> (shapeId);
if (shape == NULL) {
jclass newExc = env->FindClass("java/lang/NullPointerException");
env->ThrowNew(newExc, "The shape does not exist.");
return;
}
struct AllConvexResultCallback : public btCollisionWorld::ConvexResultCallback {
AllConvexResultCallback(const btTransform& convexFromWorld, const btTransform & convexToWorld) : m_convexFromWorld(convexFromWorld), m_convexToWorld(convexToWorld) {
}
jobject resultlist;
JNIEnv* env;
btTransform m_convexFromWorld; //used to calculate hitPointWorld from hitFraction
btTransform m_convexToWorld;
btVector3 m_hitNormalWorld;
btVector3 m_hitPointWorld;
virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) {
if (normalInWorldSpace) {
m_hitNormalWorld = convexResult.m_hitNormalLocal;
}
else {
m_hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis() * convexResult.m_hitNormalLocal;
}
m_hitPointWorld.setInterpolate3(m_convexFromWorld.getBasis() * m_convexFromWorld.getOrigin(), m_convexToWorld.getBasis() * m_convexToWorld.getOrigin(), convexResult.m_hitFraction);
jmeBulletUtil::addSweepResult(env, resultlist, &m_hitNormalWorld, &m_hitPointWorld, convexResult.m_hitFraction, convexResult.m_hitCollisionObject);
return 1.f;
}
};
btTransform native_to = btTransform();
jmeBulletUtil::convert(env, to, &native_to);
btTransform native_from = btTransform();
jmeBulletUtil::convert(env, from, &native_from);
btScalar native_allowed_ccd_penetration = btScalar(allowedCcdPenetration);
AllConvexResultCallback resultCallback(native_from, native_to);
resultCallback.env = env;
resultCallback.resultlist = resultlist;
space->getDynamicsWorld()->convexSweepTest((btConvexShape *) shape, native_from, native_to, resultCallback, native_allowed_ccd_penetration);
return;
}
JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_setSolverNumIterations
(JNIEnv *env, jobject object, jlong spaceId, jint value) {
jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
if (space == NULL) {
jclass newExc = env->FindClass("java/lang/NullPointerException");
env->ThrowNew(newExc, "The physics space does not exist.");
return;
}
space->getDynamicsWorld()->getSolverInfo().m_numIterations = value;
}
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

@ -165,6 +165,23 @@ JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_initNativePhysics
JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_finalizeNative JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_finalizeNative
(JNIEnv *, jobject, jlong); (JNIEnv *, jobject, jlong);
/*
* Class: com_jme3_bullet_PhysicsSpace
* Method : sweepTest_native
* Signature: (J;L;Lcom/jme3/math/Transform;Lcom/jme3/math/Transform;L;JLjava/util/List;F)V
*/
JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_sweepTest_1native
(JNIEnv *, jobject, jlong, jobject, jobject, jlong, jobject, jfloat);
/*
* Class: com_jme3_bullet_PhysicsSpace
* Method: setSolverNumIterations
* Signature: (JI)V
*/
JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_setSolverNumIterations
(JNIEnv *, jobject, jlong, jint);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

@ -59,6 +59,38 @@ void jmeBulletUtil::convert(JNIEnv* env, jobject in, btVector3* out) {
out->setZ(z); out->setZ(z);
} }
void jmeBulletUtil::convert(JNIEnv* env, jobject in, btQuaternion* out) {
if (in == NULL || out == NULL) {
jmeClasses::throwNPE(env);
}
float x = env->GetFloatField(in, jmeClasses::Quaternion_x);
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
float y = env->GetFloatField(in, jmeClasses::Quaternion_y); //env->CallFloatMethod(in, jmeClasses::Vector3f_getY);
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
float z = env->GetFloatField(in, jmeClasses::Quaternion_z); //env->CallFloatMethod(in, jmeClasses::Vector3f_getZ);
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
float w = env->GetFloatField(in, jmeClasses::Quaternion_w); //env->CallFloatMethod(in, jmeClasses::Vector3f_getZ);
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
out->setX(x);
out->setY(y);
out->setZ(z);
out->setW(w);
}
void jmeBulletUtil::convert(JNIEnv* env, const btVector3* in, jobject out) { void jmeBulletUtil::convert(JNIEnv* env, const btVector3* in, jobject out) {
if (in == NULL || out == NULL) { if (in == NULL || out == NULL) {
jmeClasses::throwNPE(env); jmeClasses::throwNPE(env);
@ -325,3 +357,61 @@ void jmeBulletUtil::addResult(JNIEnv* env, jobject resultlist, btVector3* hitnor
return; return;
} }
} }
void jmeBulletUtil::addSweepResult(JNIEnv* env, jobject resultlist, btVector3* hitnormal, btVector3* m_hitPointWorld, btScalar m_hitFraction, const btCollisionObject* hitobject) {
jobject singleresult = env->AllocObject(jmeClasses::PhysicsSweep_Class);
jobject hitnormalvec = env->AllocObject(jmeClasses::Vector3f);
convert(env, hitnormal, hitnormalvec);
jmeUserPointer *up1 = (jmeUserPointer*)hitobject->getUserPointer();
env->SetObjectField(singleresult, jmeClasses::PhysicsSweep_normalInWorldSpace, hitnormalvec);
env->SetFloatField(singleresult, jmeClasses::PhysicsSweep_hitfraction, m_hitFraction);
env->SetObjectField(singleresult, jmeClasses::PhysicsSweep_collisionObject, up1->javaCollisionObject);
env->CallVoidMethod(resultlist, jmeClasses::PhysicsSweep_addmethod, singleresult);
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
}
void jmeBulletUtil::convert(JNIEnv* env, jobject in, btTransform* out) {
if (in == NULL || out == NULL) {
jmeClasses::throwNPE(env);
}
jobject translation_vec = env->CallObjectMethod(in, jmeClasses::Transform_translation);
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
jobject rot_quat = env->CallObjectMethod(in, jmeClasses::Transform_rotation);
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
/*
//Scale currently not supported by bullet
//@TBD: Create an assertion somewhere to avoid scale values
jobject scale_vec = env->GetObjectField(in, jmeClasses::Transform_scale);
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
*/
btVector3 native_translation_vec = btVector3();
//btVector3 native_scale_vec = btVector3();
btQuaternion native_rot_quat = btQuaternion();
convert(env, translation_vec, &native_translation_vec);
//convert(env, scale_vec, native_scale_vec);
convert(env, rot_quat, &native_rot_quat);
out->setRotation(native_rot_quat);
out->setOrigin(native_translation_vec);
}

@ -42,10 +42,13 @@ public:
static void convert(JNIEnv* env, jobject in, btVector3* out); static void convert(JNIEnv* env, jobject in, btVector3* out);
static void convert(JNIEnv* env, const btVector3* in, jobject out); static void convert(JNIEnv* env, const btVector3* in, jobject out);
static void convert(JNIEnv* env, jobject in, btMatrix3x3* out); static void convert(JNIEnv* env, jobject in, btMatrix3x3* out);
static void convert(JNIEnv* env, jobject in, btQuaternion* out);
static void convert(JNIEnv* env, const btMatrix3x3* in, jobject out); static void convert(JNIEnv* env, const btMatrix3x3* in, jobject out);
static void convertQuat(JNIEnv* env, jobject in, btMatrix3x3* out); static void convertQuat(JNIEnv* env, jobject in, btMatrix3x3* out);
static void convertQuat(JNIEnv* env, const btMatrix3x3* in, jobject out); static void convertQuat(JNIEnv* env, const btMatrix3x3* in, jobject out);
static void convert(JNIEnv* env, jobject in, btTransform* out);
static void addResult(JNIEnv* env, jobject resultlist, btVector3* hitnormal, btVector3* m_hitPointWorld,const btScalar m_hitFraction,const btCollisionObject* hitobject); static void addResult(JNIEnv* env, jobject resultlist, btVector3* hitnormal, btVector3* m_hitPointWorld,const btScalar m_hitFraction,const btCollisionObject* hitobject);
static void addSweepResult(JNIEnv* env, jobject resultlist, btVector3* hitnormal, btVector3* m_hitPointWorld, const btScalar m_hitFraction, const btCollisionObject* hitobject);
private: private:
jmeBulletUtil(){}; jmeBulletUtil(){};
~jmeBulletUtil(){}; ~jmeBulletUtil(){};

@ -91,6 +91,21 @@ jfieldID jmeClasses::PhysicsRay_collisionObject;
jclass jmeClasses::PhysicsRay_listresult; jclass jmeClasses::PhysicsRay_listresult;
jmethodID jmeClasses::PhysicsRay_addmethod; jmethodID jmeClasses::PhysicsRay_addmethod;
jclass jmeClasses::PhysicsSweep_Class;
jmethodID jmeClasses::PhysicsSweep_newSingleResult;
jfieldID jmeClasses::PhysicsSweep_normalInWorldSpace;
jfieldID jmeClasses::PhysicsSweep_hitfraction;
jfieldID jmeClasses::PhysicsSweep_collisionObject;
jclass jmeClasses::PhysicsSweep_listresult;
jmethodID jmeClasses::PhysicsSweep_addmethod;
jclass jmeClasses::Transform;
jmethodID jmeClasses::Transform_rotation;
jmethodID jmeClasses::Transform_translation;
//private fields //private fields
//JNIEnv* jmeClasses::env; //JNIEnv* jmeClasses::env;
JavaVM* jmeClasses::vm; JavaVM* jmeClasses::vm;
@ -240,6 +255,70 @@ void jmeClasses::initJavaClasses(JNIEnv* env) {
env->Throw(env->ExceptionOccurred()); env->Throw(env->ExceptionOccurred());
return; return;
} }
PhysicsSweep_Class = (jclass)env->NewGlobalRef(env->FindClass("com/jme3/bullet/collision/PhysicsSweepTestResult"));
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
PhysicsSweep_newSingleResult = env->GetMethodID(PhysicsSweep_Class, "<init>", "()V");
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
PhysicsSweep_normalInWorldSpace = env->GetFieldID(PhysicsSweep_Class, "hitNormalLocal", "Lcom/jme3/math/Vector3f;");
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
PhysicsSweep_hitfraction = env->GetFieldID(PhysicsSweep_Class, "hitFraction", "F");
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
PhysicsSweep_collisionObject = env->GetFieldID(PhysicsSweep_Class, "collisionObject", "Lcom/jme3/bullet/collision/PhysicsCollisionObject;");
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
PhysicsSweep_listresult = env->FindClass("java/util/List");
PhysicsSweep_listresult = (jclass)env->NewGlobalRef(PhysicsSweep_listresult);
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
PhysicsSweep_addmethod = env->GetMethodID(PhysicsSweep_listresult, "add", "(Ljava/lang/Object;)Z");
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
Transform = (jclass)env->NewGlobalRef(env->FindClass("com/jme3/math/Transform"));
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
Transform_rotation = env->GetMethodID(Transform, "getRotation", "()Lcom/jme3/math/Quaternion;");
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
Transform_translation = env->GetMethodID(Transform, "getTranslation", "()Lcom/jme3/math/Vector3f;");
if (env->ExceptionCheck()) {
env->Throw(env->ExceptionOccurred());
return;
}
} }
void jmeClasses::throwNPE(JNIEnv* env) { void jmeClasses::throwNPE(JNIEnv* env) {

@ -89,6 +89,18 @@ public:
static jclass PhysicsRay_listresult; static jclass PhysicsRay_listresult;
static jmethodID PhysicsRay_addmethod; static jmethodID PhysicsRay_addmethod;
static jclass PhysicsSweep_Class;
static jmethodID PhysicsSweep_newSingleResult;
static jfieldID PhysicsSweep_normalInWorldSpace;
static jfieldID PhysicsSweep_hitfraction;
static jfieldID PhysicsSweep_collisionObject;
static jclass PhysicsSweep_listresult;
static jmethodID PhysicsSweep_addmethod;
static jclass Transform;
static jmethodID Transform_rotation;
static jmethodID Transform_translation;
static jclass DebugMeshCallback; static jclass DebugMeshCallback;
static jmethodID DebugMeshCallback_addVector; static jmethodID DebugMeshCallback_addVector;

@ -166,7 +166,7 @@ public class BulletAppState implements AppState, PhysicsTickListener {
pSpace.addTickListener(this); pSpace.addTickListener(this);
initialized = true; initialized = true;
} }
public void stopPhysics() { public void stopPhysics() {
if(!initialized){ if(!initialized){
return; return;
@ -226,19 +226,9 @@ public class BulletAppState implements AppState, PhysicsTickListener {
if (debugEnabled && debugAppState == null && pSpace != null) { if (debugEnabled && debugAppState == null && pSpace != null) {
debugAppState = new BulletDebugAppState(pSpace); debugAppState = new BulletDebugAppState(pSpace);
stateManager.attach(debugAppState); stateManager.attach(debugAppState);
pSpace.enableDebug(app.getAssetManager());
} else if (!debugEnabled && debugAppState != null) { } else if (!debugEnabled && debugAppState != null) {
stateManager.detach(debugAppState); stateManager.detach(debugAppState);
debugAppState = null; debugAppState = null;
if (pSpace != null) {
pSpace.enableDebug(null);
}
}
//TODO: remove when deprecation of PhysicsSpace.enableDebug is through
if (pSpace.getDebugManager() != null && !debugEnabled) {
debugEnabled = true;
} else if (pSpace.getDebugManager() == null && debugEnabled) {
debugEnabled = false;
} }
if (!active) { if (!active) {
return; return;

@ -195,7 +195,7 @@ public class RigidBodyControl extends PhysicsRigidBody implements PhysicsControl
/** /**
* When set to true, the physics coordinates will be applied to the local * When set to true, the physics coordinates will be applied to the local
* translation of the Spatial instead of the world traslation. * translation of the Spatial instead of the world translation.
* @param applyPhysicsLocal * @param applyPhysicsLocal
*/ */
public void setApplyPhysicsLocal(boolean applyPhysicsLocal) { public void setApplyPhysicsLocal(boolean applyPhysicsLocal) {

@ -102,7 +102,7 @@ public class PhysicsSpace {
private Vector3f worldMax = new Vector3f(10000f, 10000f, 10000f); private Vector3f worldMax = new Vector3f(10000f, 10000f, 10000f);
private float accuracy = 1f / 60f; private float accuracy = 1f / 60f;
private int maxSubSteps = 4, rayTestFlags = 1 << 2; private int maxSubSteps = 4, rayTestFlags = 1 << 2;
private AssetManager debugManager; private int solverNumIterations = 10;
static { static {
// System.loadLibrary("bulletjme"); // System.loadLibrary("bulletjme");
@ -702,7 +702,7 @@ public class PhysicsSpace {
public Vector3f getGravity(Vector3f gravity) { public Vector3f getGravity(Vector3f gravity) {
return gravity.set(this.gravity); return gravity.set(this.gravity);
} }
// /** // /**
// * applies gravity value to all objects // * applies gravity value to all objects
// */ // */
@ -783,7 +783,7 @@ public class PhysicsSpace {
public void SetRayTestFlags(int flags) { public void SetRayTestFlags(int flags) {
rayTestFlags = flags; rayTestFlags = flags;
} }
/** /**
* Gets m_flags for raytest, see https://code.google.com/p/bullet/source/browse/trunk/src/BulletCollision/NarrowPhaseCollision/btRaycastCallback.h * Gets m_flags for raytest, see https://code.google.com/p/bullet/source/browse/trunk/src/BulletCollision/NarrowPhaseCollision/btRaycastCallback.h
* for possible options. * for possible options.
@ -792,7 +792,7 @@ public class PhysicsSpace {
public int GetRayTestFlags() { public int GetRayTestFlags() {
return rayTestFlags; return rayTestFlags;
} }
/** /**
* Performs a ray collision test and returns the results as a list of * Performs a ray collision test and returns the results as a list of
* PhysicsRayTestResults * PhysicsRayTestResults
@ -820,6 +820,10 @@ public class PhysicsSpace {
// return lrr.hitFraction; // return lrr.hitFraction;
// } // }
// } // }
//
//
/** /**
* Performs a sweep collision test and returns the results as a list of * Performs a sweep collision test and returns the results as a list of
* PhysicsSweepTestResults<br/> You have to use different Transforms for * PhysicsSweepTestResults<br/> You have to use different Transforms for
@ -828,48 +832,47 @@ public class PhysicsSpace {
* center. * center.
*/ */
public List<PhysicsSweepTestResult> sweepTest(CollisionShape shape, Transform start, Transform end) { public List<PhysicsSweepTestResult> sweepTest(CollisionShape shape, Transform start, Transform end) {
List<PhysicsSweepTestResult> results = new LinkedList<PhysicsSweepTestResult>(); List results = new LinkedList();
// if (!(shape.getCShape() instanceof ConvexShape)) { sweepTest(shape, start, end , results);
// logger.log(Level.WARNING, "Trying to sweep test with incompatible mesh shape!"); return (List<PhysicsSweepTestResult>) results;
// return results; }
// }
// dynamicsWorld.convexSweepTest((ConvexShape) shape.getCShape(), Converter.convert(start, sweepTrans1), Converter.convert(end, sweepTrans2), new InternalSweepListener(results));
return results;
public List<PhysicsSweepTestResult> sweepTest(CollisionShape shape, Transform start, Transform end, List<PhysicsSweepTestResult> results) {
return sweepTest(shape, start, end, results, 0.0f);
} }
public native void sweepTest_native(long shape, Transform from, Transform to, long physicsSpaceId, List<PhysicsSweepTestResult> results, float allowedCcdPenetration);
/** /**
* Performs a sweep collision test and returns the results as a list of * Performs a sweep collision test and returns the results as a list of
* PhysicsSweepTestResults<br/> You have to use different Transforms for * PhysicsSweepTestResults<br/> You have to use different Transforms for
* start and end (at least distance > 0.4f). SweepTest will not see a * start and end (at least distance > allowedCcdPenetration). SweepTest will not see a
* collision if it starts INSIDE an object and is moving AWAY from its * collision if it starts INSIDE an object and is moving AWAY from its
* center. * center.
*/ */
public List<PhysicsSweepTestResult> sweepTest(CollisionShape shape, Transform start, Transform end, List<PhysicsSweepTestResult> results) { public List<PhysicsSweepTestResult> sweepTest(CollisionShape shape, Transform start, Transform end, List<PhysicsSweepTestResult> results, float allowedCcdPenetration ) {
results.clear(); results.clear();
// if (!(shape.getCShape() instanceof ConvexShape)) { sweepTest_native(shape.getObjectId(), start, end, physicsSpaceId, results, allowedCcdPenetration);
// logger.log(Level.WARNING, "Trying to sweep test with incompatible mesh shape!");
// return results;
// }
// dynamicsWorld.convexSweepTest((ConvexShape) shape.getCShape(), Converter.convert(start, sweepTrans1), Converter.convert(end, sweepTrans2), new InternalSweepListener(results));
return results; return results;
} }
// private class InternalSweepListener extends CollisionWorld.ConvexResultCallback { /* private class InternalSweepListener extends CollisionWorld.ConvexResultCallback {
//
// private List<PhysicsSweepTestResult> results; private List<PhysicsSweepTestResult> results;
//
// public InternalSweepListener(List<PhysicsSweepTestResult> results) { public InternalSweepListener(List<PhysicsSweepTestResult> results) {
// this.results = results; this.results = results;
// } }
//
// @Override @Override
// public float addSingleResult(LocalConvexResult lcr, boolean bln) { public float addSingleResult(LocalConvexResult lcr, boolean bln) {
// PhysicsCollisionObject obj = (PhysicsCollisionObject) lcr.hitCollisionObject.getUserPointer(); PhysicsCollisionObject obj = (PhysicsCollisionObject) lcr.hitCollisionObject.getUserPointer();
// results.add(new PhysicsSweepTestResult(obj, Converter.convert(lcr.hitNormalLocal), lcr.hitFraction, bln)); results.add(new PhysicsSweepTestResult(obj, Converter.convert(lcr.hitNormalLocal), lcr.hitFraction, bln));
// return lcr.hitFraction; return lcr.hitFraction;
// } }
// } }
*/
/** /**
* destroys the current PhysicsSpace so that a new one can be created * destroys the current PhysicsSpace so that a new one can be created
*/ */
@ -957,28 +960,28 @@ public class PhysicsSpace {
} }
/** /**
* Enable debug display for physics. * Set the number of iterations used by the contact solver.
* *
* @deprecated in favor of BulletDebugAppState, use * The default is 10. Use 4 for low quality, 20 for high quality.
* <code>BulletAppState.setDebugEnabled(boolean)</code> to add automatically *
* @param manager AssetManager to use to create debug materials * @param numIterations The number of iterations used by the contact & constraint solver.
*/ */
@Deprecated public void setSolverNumIterations(int numIterations) {
public void enableDebug(AssetManager manager) { this.solverNumIterations = numIterations;
debugManager = manager; setSolverNumIterations(physicsSpaceId, numIterations);
} }
/** /**
* Disable debug display * Get the number of iterations used by the contact solver.
*
* @return The number of iterations used by the contact & constraint solver.
*/ */
public void disableDebug() { public int getSolverNumIterations() {
debugManager = null; return solverNumIterations;
} }
public AssetManager getDebugManager() { private static native void setSolverNumIterations(long physicsSpaceId, int numIterations);
return debugManager;
}
public static native void initNativePhysics(); public static native void initNativePhysics();
/** /**

@ -26,14 +26,29 @@ import org.ajoberstar.grgit.*
task updateVersion << { task updateVersion << {
def grgit = Grgit.open(project.file('.').parent) def verfile = file('src/main/java/com/jme3/system/JmeVersion.java')
def jmeGitHash
def jmeShortGitHash
def jmeBuildDate
def jmeBranchName
def jmeGitHash = grgit.head().id try {
def jmeShortGitHash = grgit.head().abbreviatedId def grgit = Grgit.open(project.file('.').parent)
def jmeBuildDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date()) jmeGitHash = grgit.head().id
def jmeBranchName = grgit.branch.current.name jmeShortGitHash = grgit.head().abbreviatedId
jmeBuildDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date())
jmeBranchName = grgit.branch.current.name
} catch (ex) {
// Failed to get repo info
logger.warn("Failed to get repository info: " + ex.message + ". " + \
"Only partial build info will be generated.")
jmeGitHash = ""
jmeShortGitHash = ""
jmeBuildDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date())
jmeBranchName = "unknown"
}
def verfile = file('src/main/java/com/jme3/system/JmeVersion.java')
verfile.text = "\npackage com.jme3.system;\n\n" + verfile.text = "\npackage com.jme3.system;\n\n" +
"/**\n * THIS IS AN AUTO-GENERATED FILE..\n * DO NOT MODIFY!\n */\n" + "/**\n * THIS IS AN AUTO-GENERATED FILE..\n * DO NOT MODIFY!\n */\n" +
"public class JmeVersion {\n" + "public class JmeVersion {\n" +

@ -312,7 +312,6 @@ public final class AnimChannel {
} }
} }
animation = null; animation = null;
// System.out.println("Setting notified false");
notified = false; notified = false;
} }

@ -313,6 +313,26 @@ public final class Bone implements Savable {
return modelBindInverseScale; return modelBindInverseScale;
} }
public Transform getModelBindInverseTransform() {
Transform t = new Transform();
t.setTranslation(modelBindInversePos);
t.setRotation(modelBindInverseRot);
if (modelBindInverseScale != null) {
t.setScale(modelBindInverseScale);
}
return t;
}
public Transform getBindInverseTransform() {
Transform t = new Transform();
t.setTranslation(bindPos);
t.setRotation(bindRot);
if (bindScale != null) {
t.setScale(bindScale);
}
return t.invert();
}
/** /**
* @deprecated use {@link #getBindPosition()} * @deprecated use {@link #getBindPosition()}
*/ */

@ -82,7 +82,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
/** /**
* User wishes to use hardware skinning if available. * User wishes to use hardware skinning if available.
*/ */
private transient boolean hwSkinningDesired = false; private transient boolean hwSkinningDesired = true;
/** /**
* Hardware skinning is currently being used. * Hardware skinning is currently being used.
@ -347,11 +347,22 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
public Control cloneForSpatial(Spatial spatial) { public Control cloneForSpatial(Spatial spatial) {
Node clonedNode = (Node) spatial; Node clonedNode = (Node) spatial;
AnimControl ctrl = spatial.getControl(AnimControl.class);
SkeletonControl clone = new SkeletonControl(); SkeletonControl clone = new SkeletonControl();
clone.skeleton = ctrl.getSkeleton(); AnimControl ctrl = spatial.getControl(AnimControl.class);
if (ctrl != null) {
// AnimControl is responsible for cloning the skeleton, not
// SkeletonControl.
clone.skeleton = ctrl.getSkeleton();
} else {
// If there's no AnimControl, create the clone ourselves.
clone.skeleton = new Skeleton(skeleton);
}
clone.hwSkinningDesired = this.hwSkinningDesired;
clone.hwSkinningEnabled = this.hwSkinningEnabled;
clone.hwSkinningSupported = this.hwSkinningSupported;
clone.hwSkinningTested = this.hwSkinningTested;
clone.setSpatial(clonedNode); clone.setSpatial(clonedNode);
// Fix attachments for the cloned node // Fix attachments for the cloned node

@ -60,7 +60,7 @@ import com.jme3.scene.control.Control;
*/ */
public class StatsView extends Node implements Control { public class StatsView extends Node implements Control {
private BitmapText[] labels; private BitmapText statText;
private Statistics statistics; private Statistics statistics;
private String[] statLabels; private String[] statLabels;
@ -81,20 +81,17 @@ public class StatsView extends Node implements Control {
statLabels = statistics.getLabels(); statLabels = statistics.getLabels();
statData = new int[statLabels.length]; statData = new int[statLabels.length];
labels = new BitmapText[statLabels.length];
BitmapFont font = manager.loadFont("Interface/Fonts/Console.fnt"); BitmapFont font = manager.loadFont("Interface/Fonts/Console.fnt");
for (int i = 0; i < labels.length; i++){ statText = new BitmapText(font);
labels[i] = new BitmapText(font); statText.setLocalTranslation(0, statText.getLineHeight() * statLabels.length, 0);
labels[i].setLocalTranslation(0, labels[i].getLineHeight() * (i+1), 0); attachChild(statText);
attachChild(labels[i]);
}
addControl(this); addControl(this);
} }
public float getHeight() { public float getHeight() {
return labels[0].getLineHeight() * statLabels.length; return statText.getLineHeight() * statLabels.length;
} }
public void update(float tpf) { public void update(float tpf) {
@ -103,11 +100,14 @@ public class StatsView extends Node implements Control {
return; return;
statistics.getData(statData); statistics.getData(statData);
for (int i = 0; i < labels.length; i++) { stringBuilder.setLength(0);
stringBuilder.setLength(0);
stringBuilder.append(statLabels[i]).append(" = ").append(statData[i]); // Need to walk through it backwards, as the first label
labels[i].setText(stringBuilder); // should appear at the bottom, not the top.
for (int i = statLabels.length - 1; i >= 0; i--) {
stringBuilder.append(statLabels[i]).append(" = ").append(statData[i]).append('\n');
} }
statText.setText(stringBuilder);
// Moved to ResetStatsState to make sure it is // Moved to ResetStatsState to make sure it is
// done even if there is no StatsView or the StatsView // done even if there is no StatsView or the StatsView

@ -69,7 +69,7 @@ public final class AssetConfig {
public static void loadText(AssetManager assetManager, URL configUrl) throws IOException{ public static void loadText(AssetManager assetManager, URL configUrl) throws IOException{
InputStream in = configUrl.openStream(); InputStream in = configUrl.openStream();
try { try {
Scanner scan = new Scanner(in); Scanner scan = new Scanner(in, "UTF-8");
scan.useLocale(Locale.US); // Fix commas / periods ?? scan.useLocale(Locale.US); // Fix commas / periods ??
while (scan.hasNext()){ while (scan.hasNext()){
String cmd = scan.next(); String cmd = scan.next();

@ -312,13 +312,12 @@ public class DesktopAssetManager implements AssetManager {
protected <T> T registerAndCloneSmartAsset(AssetKey<T> key, T obj, AssetProcessor proc, AssetCache cache) { protected <T> T registerAndCloneSmartAsset(AssetKey<T> key, T obj, AssetProcessor proc, AssetCache cache) {
// object obj is the original asset // object obj is the original asset
// create an instance for user // create an instance for user
T clone = (T) obj;
if (proc == null) { if (proc == null) {
throw new IllegalStateException("Asset implements " throw new IllegalStateException("Asset implements "
+ "CloneableSmartAsset but doesn't " + "CloneableSmartAsset but doesn't "
+ "have processor to handle cloning"); + "have processor to handle cloning");
} else { } else {
clone = (T) proc.createClone(obj); T clone = (T) proc.createClone(obj);
if (cache != null && clone != obj) { if (cache != null && clone != obj) {
cache.registerAssetClone(key, clone); cache.registerAssetClone(key, clone);
} else { } else {
@ -326,8 +325,8 @@ public class DesktopAssetManager implements AssetManager {
+ "CloneableSmartAsset but doesn't have cache or " + "CloneableSmartAsset but doesn't have cache or "
+ "was not cloned"); + "was not cloned");
} }
return clone;
} }
return clone;
} }
@Override @Override

@ -47,7 +47,7 @@ import java.util.logging.Logger;
* This is done by keeping an instance of each asset loader and asset * This is done by keeping an instance of each asset loader and asset
* locator object in a thread local. * locator object in a thread local.
*/ */
public class ImplHandler { final class ImplHandler {
private static final Logger logger = Logger.getLogger(ImplHandler.class.getName()); private static final Logger logger = Logger.getLogger(ImplHandler.class.getName());
@ -75,7 +75,7 @@ public class ImplHandler {
this.assetManager = assetManager; this.assetManager = assetManager;
} }
protected class ImplThreadLocal<T> extends ThreadLocal { protected static class ImplThreadLocal<T> extends ThreadLocal {
private final Class<T> type; private final Class<T> type;
private final String path; private final String path;
@ -83,7 +83,7 @@ public class ImplHandler {
public ImplThreadLocal(Class<T> type, String[] extensions){ public ImplThreadLocal(Class<T> type, String[] extensions){
this.type = type; this.type = type;
this.extensions = extensions; this.extensions = extensions.clone();
this.path = null; this.path = null;
} }
@ -195,8 +195,8 @@ public class ImplHandler {
// No need to synchronize() against map, its concurrent // No need to synchronize() against map, its concurrent
ImplThreadLocal local = extensionToLoaderMap.get(key.getExtension()); ImplThreadLocal local = extensionToLoaderMap.get(key.getExtension());
if (local == null){ if (local == null){
throw new IllegalStateException("No loader registered for type \"" + throw new AssetLoadException("No loader registered for type \"" +
key.getExtension() + "\""); key.getExtension() + "\"");
} }
return (AssetLoader) local.get(); return (AssetLoader) local.get();

@ -39,8 +39,6 @@ import java.util.List;
* Used for loading {@link ShaderNodeDefinition shader nodes definition} * Used for loading {@link ShaderNodeDefinition shader nodes definition}
* *
* Tells if the defintion has to be loaded with or without its documentation * Tells if the defintion has to be loaded with or without its documentation
*
* @author Kirill Vainer
*/ */
public class ShaderNodeDefinitionKey extends AssetKey<List<ShaderNodeDefinition>> { public class ShaderNodeDefinitionKey extends AssetKey<List<ShaderNodeDefinition>> {

@ -155,11 +155,7 @@ public class WeakRefCloneAssetCache implements AssetCache {
} }
public <T> T getFromCache(AssetKey<T> key) { public <T> T getFromCache(AssetKey<T> key) {
AssetRef smartInfo; AssetRef smartInfo = smartCache.get(key);
synchronized (smartCache){
smartInfo = smartCache.get(key);
}
if (smartInfo == null) { if (smartInfo == null) {
return null; return null;
} else { } else {

@ -77,7 +77,7 @@ public class AudioNode extends Node implements AudioSource {
protected transient volatile AudioSource.Status status = AudioSource.Status.Stopped; protected transient volatile AudioSource.Status status = AudioSource.Status.Stopped;
protected transient volatile int channel = -1; protected transient volatile int channel = -1;
protected Vector3f velocity = new Vector3f(); protected Vector3f velocity = new Vector3f();
protected boolean reverbEnabled = true; protected boolean reverbEnabled = false;
protected float maxDistance = 200; // 200 meters protected float maxDistance = 200; // 200 meters
protected float refDistance = 10; // 10 meters protected float refDistance = 10; // 10 meters
protected Filter reverbFilter; protected Filter reverbFilter;
@ -409,6 +409,14 @@ public class AudioNode extends Node implements AudioSource {
play(); play();
} }
} }
@Override
public float getPlaybackTime() {
if (channel >= 0)
return getRenderer().getSourcePlaybackTime(this);
else
return 0;
}
public Vector3f getPosition() { public Vector3f getPosition() {
return getWorldTranslation(); return getWorldTranslation();

@ -59,6 +59,7 @@ public interface AudioRenderer {
public void updateSourceParam(AudioSource src, AudioParam param); public void updateSourceParam(AudioSource src, AudioParam param);
public void updateListenerParam(Listener listener, ListenerParam param); public void updateListenerParam(Listener listener, ListenerParam param);
public float getSourcePlaybackTime(AudioSource src);
public void deleteFilter(Filter filter); public void deleteFilter(Filter filter);
public void deleteAudioData(AudioData ad); public void deleteAudioData(AudioData ad);

@ -95,6 +95,11 @@ public interface AudioSource {
* @return the time offset in the sound sample when to start playing. * @return the time offset in the sound sample when to start playing.
*/ */
public float getTimeOffset(); public float getTimeOffset();
/**
* @return the current playback position of the source in seconds.
*/
public float getPlaybackTime();
/** /**
* @return The velocity of the audio source. * @return The velocity of the audio source.

@ -54,6 +54,8 @@ public class AudioStream extends AudioData implements Closeable {
protected boolean eof = false; protected boolean eof = false;
protected int[] ids; protected int[] ids;
protected int unqueuedBuffersBytes = 0;
public AudioStream() { public AudioStream() {
super(); super();
} }
@ -196,10 +198,21 @@ public class AudioStream extends AudioData implements Closeable {
return in instanceof SeekableStream; return in instanceof SeekableStream;
} }
public int getUnqueuedBufferBytes() {
return unqueuedBuffersBytes;
}
public void setUnqueuedBufferBytes(int unqueuedBuffers) {
this.unqueuedBuffersBytes = unqueuedBuffers;
}
public void setTime(float time) { public void setTime(float time) {
if (in instanceof SeekableStream) { if (in instanceof SeekableStream) {
((SeekableStream) in).setTime(time); ((SeekableStream) in).setTime(time);
eof = false; eof = false;
// TODO: when we actually support seeking, this will need to be properly set.
unqueuedBuffersBytes = 0;
} else { } else {
throw new IllegalStateException( throw new IllegalStateException(
"Cannot use setTime on a stream that " "Cannot use setTime on a stream that "

@ -51,6 +51,9 @@ import static com.jme3.audio.openal.EFX.*;
public class ALAudioRenderer implements AudioRenderer, Runnable { public class ALAudioRenderer implements AudioRenderer, Runnable {
private static final Logger logger = Logger.getLogger(ALAudioRenderer.class.getName()); private static final Logger logger = Logger.getLogger(ALAudioRenderer.class.getName());
private static final String THREAD_NAME = "jME3 Audio Decoder";
private final NativeObjectManager objManager = new NativeObjectManager(); private final NativeObjectManager objManager = new NativeObjectManager();
// When multiplied by STREAMING_BUFFER_COUNT, will equal 44100 * 2 * 2 // When multiplied by STREAMING_BUFFER_COUNT, will equal 44100 * 2 * 2
// which is exactly 1 second of audio. // which is exactly 1 second of audio.
@ -75,7 +78,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
// Fill streaming sources every 50 ms // Fill streaming sources every 50 ms
private static final float UPDATE_RATE = 0.05f; private static final float UPDATE_RATE = 0.05f;
private final Thread decoderThread = new Thread(this, "jME3 Audio Decoding Thread"); private final Thread decoderThread = new Thread(this, THREAD_NAME);
private final Object threadLock = new Object(); private final Object threadLock = new Object();
private final AL al; private final AL al;
@ -298,6 +301,58 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
f.clearUpdateNeeded(); f.clearUpdateNeeded();
} }
@Override
public float getSourcePlaybackTime(AudioSource src) {
checkDead();
synchronized (threadLock) {
if (audioDisabled) {
return 0;
}
// See comment in updateSourceParam().
if (src.getChannel() < 0) {
return 0;
}
int id = channels[src.getChannel()];
AudioData data = src.getAudioData();
int playbackOffsetBytes = 0;
if (data instanceof AudioStream) {
// Because audio streams are processed in buffer chunks,
// we have to compute the amount of time the stream was already
// been playing based on the number of buffers that were processed.
AudioStream stream = (AudioStream) data;
// NOTE: the assumption is that all enqueued buffers are the same size.
// this is currently enforced by fillBuffer().
// The number of unenqueued bytes that the decoder thread
// keeps track of.
int unqueuedBytes = stream.getUnqueuedBufferBytes();
// Additional processed buffers that the decoder thread
// did not unenqueue yet (it only updates 20 times per second).
int unqueuedBytesExtra = al.alGetSourcei(id, AL_BUFFERS_PROCESSED) * BUFFER_SIZE;
// Total additional bytes that need to be considered.
playbackOffsetBytes = unqueuedBytes; // + unqueuedBytesExtra;
}
// Add byte offset from source (for both streams and buffers)
playbackOffsetBytes += al.alGetSourcei(id, AL_BYTE_OFFSET);
// Compute time value from bytes
// E.g. for 44100 source with 2 channels and 16 bits per sample:
// (44100 * 2 * 16 / 8) = 176400
int bytesPerSecond = (data.getSampleRate() *
data.getChannels() *
data.getBitsPerSample() / 8);
return (float)playbackOffsetBytes / bytesPerSecond;
}
}
public void updateSourceParam(AudioSource src, AudioParam param) { public void updateSourceParam(AudioSource src, AudioParam param) {
checkDead(); checkDead();
synchronized (threadLock) { synchronized (threadLock) {
@ -645,6 +700,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean looping) { private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean looping) {
boolean success = false; boolean success = false;
int processed = al.alGetSourcei(sourceId, AL_BUFFERS_PROCESSED); int processed = al.alGetSourcei(sourceId, AL_BUFFERS_PROCESSED);
int unqueuedBufferBytes = 0;
for (int i = 0; i < processed; i++) { for (int i = 0; i < processed; i++) {
int buffer; int buffer;
@ -653,6 +709,11 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
al.alSourceUnqueueBuffers(sourceId, 1, ib); al.alSourceUnqueueBuffers(sourceId, 1, ib);
buffer = ib.get(0); buffer = ib.get(0);
// XXX: assume that reading from AudioStream always
// gives BUFFER_SIZE amount of bytes! This might not always
// be the case...
unqueuedBufferBytes += BUFFER_SIZE;
boolean active = fillBuffer(stream, buffer); boolean active = fillBuffer(stream, buffer);
if (!active && !stream.isEOF()) { if (!active && !stream.isEOF()) {
@ -679,6 +740,8 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
break; break;
} }
} }
stream.setUnqueuedBufferBytes(stream.getUnqueuedBufferBytes() + unqueuedBufferBytes);
return success; return success;
} }

@ -640,8 +640,7 @@ public class BoundingSphere extends BoundingVolume {
return rVal; return rVal;
} }
return new BoundingSphere(radius, return new BoundingSphere(radius, center.clone());
(center != null ? (Vector3f) center.clone() : null));
} }
/** /**

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

Loading…
Cancel
Save